Skip to content
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,10 @@ Windows以外では定期実行の登録に未対応です。Codexが終了し

### 通常Chatの一時Chat(GPT-6 Pro)へ渡す(任意)

0.7.0では、確認済み接続の操作・範囲・有効期限をローカル台帳に保持し、`chat_plan.py` が必要な項目だけ機械照合します。分類専用のAI呼出しや全接続の再調査を避け、`chat_transfer.py` の一回限りのローカルフォームで、準備済み本文と最終回答をBrowser内の変数のまま受け渡せます。親のモデルへ全文を出力して再転記する必要を減らします。[接続と分類](plugins/codex-task-routing/skills/task-routing/references/chat-capabilities.md)、[転送補助](plugins/codex-task-routing/skills/task-routing/references/chat-transfer.md)。これらは明示実行の補助で、フックから起動・送信しません。

通常ChatのGPT-6 ProはWork/Codexと別枠ですが、親の準備・操作・検収はCodex側に残ります。6 Proには契約別の上限があり、無制限ではありません。モデルや残量は実際のUIで確認し、第三者ツール固有の料金まで無料と扱いません。[公式の利用枠](https://help.openai.com/en/articles/20001354)、[Work/Codexの共有枠](https://learn.chatgpt.com/docs/pricing)。

0.6.0では、この経路を有効にしたrootが、まとまった独立工程の担当選定時にまずChatへの適合を検討します。材料と確認済みツールで完結し、準備・待機・検収・手戻りが見合う比較・分析・設計・文案・レビューは6 Proを優先します。開発も証拠取得→Chatで分析→Codexで実装・検証と分けられます。短い仕事や頻繁な現物照会はCodexに残し、親にも独立作業がある条件を維持します。経路自体は既定で無効、子のmodel・effort表は従来どおりです。有効化後は毎回プラグインを指定する必要はありません。

新規依頼には、目的・材料・合格条件に加え、`handoff` で必要情報、出典要件、鮮度、許可ツール、返却形式、停止条件を渡します。MCPはChat側で必要な操作が使えるかを確認し、回答に実取得の出典・版や日時・取得範囲を求めます。設定済み・選択済みだけを取得成功と扱いません。本文だけの仕事はツールなしで完結させ、承認済み範囲の確認を繰り返しません。
Expand Down
9 changes: 9 additions & 0 deletions docs/validation.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,14 @@
# 検証記録

## 0.7.1: PR #9の境界値レビュー

2026-09-15、PR #9のローカル転送、接続台帳、キャッシュ公開の差分を確認しました。

- JSONで表現できてもUTF-8へ保存できない単独サロゲートを、共通の文字列検証で拒否します。依頼のhash生成前・回答の保存前に安全なプロトコルエラーを返し、修正した回答は同じ転送で受理できます。通常の補助平面Unicodeは保持します。
- 接続期限の上限判定を日時への加算から差分比較へ変更し、UTC変換の範囲外も安全な入力エラーとして扱います。
- CIから取得したPRソースをLinux/Python 3.13.5で検証し、104件の単体テストが成功(環境依存2件はskip)、package検査とdiffの空白検査も通過しました。初回の旧101件では既存の子プロセス起動テストが1件失敗しましたが、対象再実行と修正後の全件で成功しました。タイミング依存の可能性は残します。
- Browser実画面、Windowsの実プラグイン導入、実行モデル・利用量の測定は今回追加していません。3 OSの最終コミットの結果はPRのCIを参照してください。

## 0.6.0: Chat優先判定と引継ぎ契約

有効なrootの工程選定にChat適合の優先判定を加え、分類表・スキル・起動時の追加指示をそろえました。既定オフ、子からのChat再委譲禁止、親の独立作業、nativeモデルとeffortの範囲は維持しています。
Expand Down
2 changes: 1 addition & 1 deletion plugins/codex-task-routing/.codex-plugin/plugin.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "codex-task-routing",
"version": "0.6.0",
"version": "0.7.1",
"description": "Task delegation policies for research, documents, images, operations, and development in Codex.",
"author": {
"name": "Acecore",
Expand Down
123 changes: 123 additions & 0 deletions plugins/codex-task-routing/scripts/chat_plan.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
#!/usr/bin/env python3
"""Local, deterministic suitability checks; never discover tools or send work."""
from __future__ import annotations

import argparse
from datetime import datetime, timedelta, timezone
import json
from pathlib import Path
import sys

from chatgpt_route import ChatRouteError, _load_json_object, _require_keys

FLAGS = {"substantial", "parent_has_independent_work", "materials_approved",
"handoff_proportionate", "requires_repeated_local_access"}
STATES = {"advertised", "verified", "blocked"}


def _keys(value, required, purpose):
if not isinstance(value, dict):
raise ChatRouteError(f"{purpose} must be an object")
_require_keys(value, required=required, allowed=required, purpose=purpose)


def _text(value):
if not isinstance(value, str) or not value.strip() or len(value) > 512:
raise ChatRouteError("metadata must be a nonempty string of at most 512 characters")
return value


def _time(value):
try:
parsed = datetime.fromisoformat(_text(value).replace("Z", "+00:00"))
if parsed.tzinfo is None:
raise ValueError()
return parsed.astimezone(timezone.utc)
except (ValueError, OverflowError) as exc:
raise ChatRouteError("observation time requires an ISO timestamp with timezone") from exc


def validate_inventory(value):
_keys(value, {"schema_version", "surface", "model", "installed_plugins", "capabilities"}, "inventory")
if type(value["schema_version"]) is not int or value["schema_version"] != 1:
raise ChatRouteError("unsupported inventory schema")
if value["surface"] != "chat-temporary" or value["model"] != "6 Pro":
raise ChatRouteError("inventory must describe Temporary Chat with 6 Pro")
installed, caps = value["installed_plugins"], value["capabilities"]
if not isinstance(installed, list) or len(installed) > 128:
raise ChatRouteError("installed_plugins must be a bounded list")
for name in installed:
_text(name)
if len(set(installed)) != len(installed):
raise ChatRouteError("duplicate installed plugin")
if not isinstance(caps, list) or len(caps) > 256:
raise ChatRouteError("capabilities must be a bounded list")
seen = set()
for cap in caps:
_keys(cap, {"tool", "operation", "scope", "state", "observed_at", "expires_at"}, "capability")
key = tuple(_text(cap[k]) for k in ("tool", "operation", "scope"))
if key in seen:
raise ChatRouteError("duplicate capability scope")
seen.add(key)
if cap["tool"] not in installed or not isinstance(cap["state"], str) or cap["state"] not in STATES:
raise ChatRouteError("unknown tool or capability state")
observed, expires = _time(cap["observed_at"]), _time(cap["expires_at"])
if not timedelta(0) < expires - observed <= timedelta(days=7):
raise ChatRouteError("capability lifetime must be positive and at most seven days")
return value


def plan(facts, inventory=None, *, now=None):
_keys(facts, FLAGS | {"needs"}, "facts")
if any(type(facts[k]) is not bool for k in FLAGS):
raise ChatRouteError("suitability flags must be JSON booleans")
needs = facts["needs"]
if not isinstance(needs, list) or len(needs) > 16:
raise ChatRouteError("needs must be a list of at most sixteen capabilities")
for need in needs:
_keys(need, {"tool", "operation", "scope"}, "need")
for val in need.values():
_text(val)
if inventory is not None:
validate_inventory(inventory)
# The caller supplies semantic judgments once. No classifier model is run.
if not facts["materials_approved"]:
return {"route": "not_ready", "reason": "materials_approval_not_established"}
for flag in ("substantial", "parent_has_independent_work", "handoff_proportionate"):
if not facts[flag]:
return {"route": "codex", "reason": flag + "_false"}
if facts["requires_repeated_local_access"]:
return {"route": "codex", "reason": "repeated_local_access"}
instant = now or datetime.now(timezone.utc)
checks = []
caps = inventory["capabilities"] if inventory else []
for need in needs:
cap = next((c for c in caps if all(c[k] == v for k, v in need.items())), None)
state = "unknown"
if cap:
state = cap["state"] if _time(cap["observed_at"]) <= instant < _time(cap["expires_at"]) else "stale"
checks.append({**need, "state": state, "installed": bool(inventory and need["tool"] in inventory["installed_plugins"])})
if any(c["state"] == "blocked" for c in checks):
return {"route": "not_ready", "reason": "required_capability_blocked", "checks": checks}
if any(c["state"] != "verified" for c in checks):
return {"route": "preflight_needed", "reason": "verify_only_required_capabilities", "checks": checks}
return {"route": "chat_candidate", "reason": "verify_live_surface_model_and_permissions", "checks": checks}


def main(argv=None):
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument("--facts", type=Path, required=True)
parser.add_argument("--inventory", type=Path)
args = parser.parse_args(argv)
try:
facts = _load_json_object(args.facts, purpose="facts")
inventory = _load_json_object(args.inventory, purpose="inventory") if args.inventory else None
print(json.dumps(plan(facts, inventory), ensure_ascii=False, separators=(",", ":")))
return 0
except (ChatRouteError, TypeError) as exc:
print(json.dumps({"error": str(exc)}, ensure_ascii=False), file=sys.stderr)
return 2


if __name__ == "__main__":
raise SystemExit(main())
Loading
Loading