zcode-supervisor 0.0.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- tools/__init__.py +2 -0
- tools/zcode_control/__init__.py +16 -0
- tools/zcode_control/browser_scripts.mjs +106 -0
- tools/zcode_control/provider_errors.mjs +135 -0
- tools/zcode_control/zcodectl.mjs +2097 -0
- tools/zcode_eval/__init__.py +2 -0
- tools/zcode_eval/duel_import.py +304 -0
- tools/zcode_eval/zcode_eval.py +687 -0
- tools/zcode_eval/zcode_release.py +221 -0
- tools/zcode_supervisor/__init__.py +2 -0
- tools/zcode_supervisor/auto_route.py +393 -0
- tools/zcode_supervisor/repo_setup.py +439 -0
- tools/zcode_supervisor/zcode_supervisor.py +696 -0
- zcode_supervisor-0.0.1.dist-info/METADATA +928 -0
- zcode_supervisor-0.0.1.dist-info/RECORD +19 -0
- zcode_supervisor-0.0.1.dist-info/WHEEL +5 -0
- zcode_supervisor-0.0.1.dist-info/entry_points.txt +7 -0
- zcode_supervisor-0.0.1.dist-info/licenses/LICENSE +21 -0
- zcode_supervisor-0.0.1.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Check official ZCode releases against this supervisor baseline."""
|
|
3
|
+
|
|
4
|
+
from __future__ import annotations
|
|
5
|
+
|
|
6
|
+
import argparse
|
|
7
|
+
import json
|
|
8
|
+
import re
|
|
9
|
+
import sys
|
|
10
|
+
import urllib.request
|
|
11
|
+
from dataclasses import dataclass
|
|
12
|
+
from datetime import datetime, timezone
|
|
13
|
+
from html import unescape
|
|
14
|
+
from html.parser import HTMLParser
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
from typing import Any
|
|
17
|
+
|
|
18
|
+
if __package__ in {None, ""}:
|
|
19
|
+
sys.path.insert(0, str(Path(__file__).resolve().parents[2]))
|
|
20
|
+
|
|
21
|
+
from tools.zcode_eval.zcode_eval import find_zcode_apps, read_app_info
|
|
22
|
+
|
|
23
|
+
CHANGELOG_URL = "https://zcode.z.ai/en/changelog"
|
|
24
|
+
VERSION_LINE = re.compile(r"^(?P<version>\d+\.\d+\.\d+)\s+Released\s+(?P<date>.+)$")
|
|
25
|
+
BARE_VERSION_LINE = re.compile(r"^(?P<version>\d+\.\d+\.\d+)$")
|
|
26
|
+
RELEASED_LINE = re.compile(r"^Released\s+(?P<date>.+)$")
|
|
27
|
+
SKIP_NOTE_LINES = {"Download", "New Features", "Bug Fixes"}
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@dataclass(frozen=True)
|
|
31
|
+
class Release:
|
|
32
|
+
version: str
|
|
33
|
+
released: str
|
|
34
|
+
notes: list[str]
|
|
35
|
+
source_url: str
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
class TextExtractor(HTMLParser):
|
|
39
|
+
def __init__(self) -> None:
|
|
40
|
+
super().__init__()
|
|
41
|
+
self.parts: list[str] = []
|
|
42
|
+
|
|
43
|
+
def handle_data(self, data: str) -> None:
|
|
44
|
+
text = data.strip()
|
|
45
|
+
if text:
|
|
46
|
+
self.parts.append(unescape(text))
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def utc_now() -> str:
|
|
50
|
+
return datetime.now(timezone.utc).replace(microsecond=0).isoformat()
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def version_tuple(version: str) -> tuple[int, int, int]:
|
|
54
|
+
return tuple(int(part) for part in version.split(".")) # type: ignore[return-value]
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def text_from_html(html: str) -> str:
|
|
58
|
+
parser = TextExtractor()
|
|
59
|
+
parser.feed(html)
|
|
60
|
+
return "\n".join(parser.parts)
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def normalize_changelog_text(raw: str) -> str:
|
|
64
|
+
if "<" in raw and ">" in raw:
|
|
65
|
+
return text_from_html(raw)
|
|
66
|
+
return raw
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
def fetch_text(url: str, timeout: int) -> str:
|
|
70
|
+
request = urllib.request.Request(url, headers={"User-Agent": "ZCode-supervisor release monitor"})
|
|
71
|
+
with urllib.request.urlopen(request, timeout=timeout) as response:
|
|
72
|
+
return text_from_html(response.read().decode("utf-8", errors="replace"))
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
def parse_releases(text: str, source_url: str = CHANGELOG_URL) -> list[Release]:
|
|
76
|
+
lines = [line.strip() for line in normalize_changelog_text(text).splitlines() if line.strip()]
|
|
77
|
+
version_rows: list[tuple[int, int, str, str]] = []
|
|
78
|
+
for index, line in enumerate(lines):
|
|
79
|
+
match = VERSION_LINE.match(line)
|
|
80
|
+
if match:
|
|
81
|
+
version_rows.append((index, index + 1, match.group("version"), match.group("date")))
|
|
82
|
+
continue
|
|
83
|
+
bare = BARE_VERSION_LINE.match(line)
|
|
84
|
+
if bare and index + 1 < len(lines):
|
|
85
|
+
released = RELEASED_LINE.match(lines[index + 1])
|
|
86
|
+
if released:
|
|
87
|
+
version_rows.append((index, index + 2, bare.group("version"), released.group("date")))
|
|
88
|
+
releases: list[Release] = []
|
|
89
|
+
for row_index, (line_index, notes_start, version, released) in enumerate(version_rows):
|
|
90
|
+
next_index = version_rows[row_index + 1][0] if row_index + 1 < len(version_rows) else len(lines)
|
|
91
|
+
section = lines[notes_start:next_index]
|
|
92
|
+
notes = clean_notes(section, version)
|
|
93
|
+
releases.append(Release(version=version, released=released, notes=notes, source_url=source_url))
|
|
94
|
+
return releases
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def clean_notes(section: list[str], version: str) -> list[str]:
|
|
98
|
+
notes: list[str] = []
|
|
99
|
+
for line in section:
|
|
100
|
+
if line in SKIP_NOTE_LINES:
|
|
101
|
+
continue
|
|
102
|
+
if line == f"Release v{version}" or line == f"ZCode {version} Update":
|
|
103
|
+
continue
|
|
104
|
+
if line.startswith("##"):
|
|
105
|
+
continue
|
|
106
|
+
notes.append(line)
|
|
107
|
+
return notes[:40]
|
|
108
|
+
|
|
109
|
+
|
|
110
|
+
def latest_release(text: str, source_url: str) -> Release:
|
|
111
|
+
releases = parse_releases(text, source_url)
|
|
112
|
+
if not releases:
|
|
113
|
+
raise ValueError("no ZCode release rows found in changelog")
|
|
114
|
+
return max(releases, key=lambda release: version_tuple(release.version))
|
|
115
|
+
|
|
116
|
+
|
|
117
|
+
def load_baseline(path: Path | None) -> dict[str, Any]:
|
|
118
|
+
if path is None or not path.exists():
|
|
119
|
+
return {}
|
|
120
|
+
return json.loads(path.read_text(encoding="utf-8"))
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def local_apps() -> list[dict[str, Any]]:
|
|
124
|
+
return [read_app_info(path).__dict__ for path in find_zcode_apps()]
|
|
125
|
+
|
|
126
|
+
|
|
127
|
+
def build_payload(args: argparse.Namespace) -> dict[str, Any]:
|
|
128
|
+
text = Path(args.html_file).read_text(encoding="utf-8") if args.html_file else fetch_text(args.url, args.timeout)
|
|
129
|
+
release = latest_release(text, args.url)
|
|
130
|
+
baseline = load_baseline(args.baseline)
|
|
131
|
+
baseline_version = args.known_version or baseline.get("version")
|
|
132
|
+
update_available = bool(baseline_version and version_tuple(release.version) > version_tuple(baseline_version))
|
|
133
|
+
return {
|
|
134
|
+
"checked_at": utc_now(),
|
|
135
|
+
"source_url": args.url,
|
|
136
|
+
"baseline_version": baseline_version,
|
|
137
|
+
"latest": release.__dict__,
|
|
138
|
+
"update_available": update_available,
|
|
139
|
+
"installed_apps": local_apps() if args.include_installed else [],
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def write_json(path: Path, payload: dict[str, Any]) -> None:
|
|
144
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
145
|
+
path.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
|
146
|
+
|
|
147
|
+
|
|
148
|
+
def write_markdown(path: Path, payload: dict[str, Any]) -> None:
|
|
149
|
+
latest = payload["latest"]
|
|
150
|
+
lines = [
|
|
151
|
+
f"# ZCode Release Check: {latest['version']}",
|
|
152
|
+
"",
|
|
153
|
+
f"- checked_at: {payload['checked_at']}",
|
|
154
|
+
f"- baseline_version: {payload.get('baseline_version') or 'unknown'}",
|
|
155
|
+
f"- update_available: {payload['update_available']}",
|
|
156
|
+
f"- source: {payload['source_url']}",
|
|
157
|
+
"",
|
|
158
|
+
"## Release Notes",
|
|
159
|
+
"",
|
|
160
|
+
]
|
|
161
|
+
lines.extend(f"- {note}" for note in latest.get("notes", []))
|
|
162
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
163
|
+
path.write_text("\n".join(lines) + "\n", encoding="utf-8")
|
|
164
|
+
|
|
165
|
+
|
|
166
|
+
def append_github_output(path: Path, payload: dict[str, Any]) -> None:
|
|
167
|
+
latest = payload["latest"]
|
|
168
|
+
lines = [
|
|
169
|
+
f"latest_version={latest['version']}",
|
|
170
|
+
f"baseline_version={payload.get('baseline_version') or ''}",
|
|
171
|
+
f"update_available={str(payload['update_available']).lower()}",
|
|
172
|
+
f"release_url={payload['source_url']}",
|
|
173
|
+
f"issue_title=ZCode update detected: v{latest['version']}",
|
|
174
|
+
]
|
|
175
|
+
with path.open("a", encoding="utf-8") as handle:
|
|
176
|
+
handle.write("\n".join(lines) + "\n")
|
|
177
|
+
|
|
178
|
+
|
|
179
|
+
def check_command(args: argparse.Namespace) -> int:
|
|
180
|
+
try:
|
|
181
|
+
payload = build_payload(args)
|
|
182
|
+
except (OSError, ValueError, json.JSONDecodeError) as exc:
|
|
183
|
+
print(json.dumps({"ok": False, "error": str(exc)}, indent=2, sort_keys=True), file=sys.stderr)
|
|
184
|
+
return 1
|
|
185
|
+
if args.json_out:
|
|
186
|
+
write_json(args.json_out, payload)
|
|
187
|
+
if args.markdown_out:
|
|
188
|
+
write_markdown(args.markdown_out, payload)
|
|
189
|
+
if args.github_output:
|
|
190
|
+
append_github_output(args.github_output, payload)
|
|
191
|
+
print(json.dumps(payload, indent=2, sort_keys=True))
|
|
192
|
+
return 0
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
196
|
+
parser = argparse.ArgumentParser(description="Check official ZCode release updates.")
|
|
197
|
+
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
198
|
+
|
|
199
|
+
check = subparsers.add_parser("check", help="Compare latest official ZCode release to a baseline.")
|
|
200
|
+
check.add_argument("--url", default=CHANGELOG_URL)
|
|
201
|
+
check.add_argument("--baseline", type=Path)
|
|
202
|
+
check.add_argument("--known-version")
|
|
203
|
+
check.add_argument("--html-file", type=Path, help="Use a local changelog HTML/text fixture.")
|
|
204
|
+
check.add_argument("--timeout", type=int, default=15)
|
|
205
|
+
check.add_argument("--include-installed", action="store_true")
|
|
206
|
+
check.add_argument("--json-out", type=Path)
|
|
207
|
+
check.add_argument("--markdown-out", type=Path)
|
|
208
|
+
check.add_argument("--github-output", type=Path)
|
|
209
|
+
check.set_defaults(func=check_command)
|
|
210
|
+
|
|
211
|
+
return parser
|
|
212
|
+
|
|
213
|
+
|
|
214
|
+
def main(argv: list[str] | None = None) -> int:
|
|
215
|
+
parser = build_parser()
|
|
216
|
+
args = parser.parse_args(argv)
|
|
217
|
+
return args.func(args)
|
|
218
|
+
|
|
219
|
+
|
|
220
|
+
if __name__ == "__main__":
|
|
221
|
+
sys.exit(main())
|
|
@@ -0,0 +1,393 @@
|
|
|
1
|
+
"""Repo-local auto-routing for Codex-to-ZCode delegation."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import argparse
|
|
6
|
+
import json
|
|
7
|
+
import re
|
|
8
|
+
import subprocess
|
|
9
|
+
import sys
|
|
10
|
+
from datetime import datetime, timezone
|
|
11
|
+
from pathlib import Path
|
|
12
|
+
from typing import Any
|
|
13
|
+
|
|
14
|
+
ROUTING_FILE = Path(".codex/zcode-routing.json")
|
|
15
|
+
DEFAULT_USAGE_SNAPSHOT_SOURCE = "auto"
|
|
16
|
+
IMPLEMENTATION_WORDS = (
|
|
17
|
+
"add",
|
|
18
|
+
"build",
|
|
19
|
+
"change",
|
|
20
|
+
"edit",
|
|
21
|
+
"fix",
|
|
22
|
+
"implement",
|
|
23
|
+
"refactor",
|
|
24
|
+
"test",
|
|
25
|
+
"update",
|
|
26
|
+
"write",
|
|
27
|
+
"作って",
|
|
28
|
+
"修正",
|
|
29
|
+
"変更",
|
|
30
|
+
"実装",
|
|
31
|
+
"追加",
|
|
32
|
+
"直して",
|
|
33
|
+
)
|
|
34
|
+
READ_ONLY_WORDS = (
|
|
35
|
+
"audit",
|
|
36
|
+
"explain",
|
|
37
|
+
"inspect",
|
|
38
|
+
"plan",
|
|
39
|
+
"review",
|
|
40
|
+
"summarize",
|
|
41
|
+
"調べ",
|
|
42
|
+
"説明",
|
|
43
|
+
"レビュー",
|
|
44
|
+
"計画",
|
|
45
|
+
)
|
|
46
|
+
TRIVIAL_WORDS = ("typo", "comment", "one-line", "one line", "誤字", "一行")
|
|
47
|
+
HIGH_RISK_WORDS = (
|
|
48
|
+
".env",
|
|
49
|
+
"api key",
|
|
50
|
+
"billing",
|
|
51
|
+
"credential",
|
|
52
|
+
"delete",
|
|
53
|
+
"deploy",
|
|
54
|
+
"destructive",
|
|
55
|
+
"migration",
|
|
56
|
+
"password",
|
|
57
|
+
"payment",
|
|
58
|
+
"production",
|
|
59
|
+
"secret",
|
|
60
|
+
"token",
|
|
61
|
+
"trading",
|
|
62
|
+
"remove data",
|
|
63
|
+
"本番",
|
|
64
|
+
"秘密",
|
|
65
|
+
"認証",
|
|
66
|
+
"決済",
|
|
67
|
+
"削除",
|
|
68
|
+
"移行",
|
|
69
|
+
)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
def utc_slug() -> str:
|
|
73
|
+
return datetime.now(timezone.utc).strftime("%Y%m%d-%H%M%S")
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def emit_json(payload: dict[str, Any]) -> None:
|
|
77
|
+
sys.stdout.write(json.dumps(payload, indent=2, sort_keys=True) + "\n")
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def read_json(path: Path) -> dict[str, Any]:
|
|
81
|
+
return json.loads(path.read_text(encoding="utf-8"))
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def write_json(path: Path, payload: dict[str, Any]) -> None:
|
|
85
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
86
|
+
path.write_text(json.dumps(payload, indent=2, sort_keys=True) + "\n", encoding="utf-8")
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def routing_path(workspace: Path) -> Path:
|
|
90
|
+
return workspace / ROUTING_FILE
|
|
91
|
+
|
|
92
|
+
|
|
93
|
+
def load_routing(workspace: Path) -> dict[str, Any] | None:
|
|
94
|
+
path = routing_path(workspace)
|
|
95
|
+
if not path.exists():
|
|
96
|
+
return None
|
|
97
|
+
payload = read_json(path)
|
|
98
|
+
if not isinstance(payload, dict):
|
|
99
|
+
raise ValueError(f"routing config must be a JSON object: {path}")
|
|
100
|
+
return payload
|
|
101
|
+
|
|
102
|
+
|
|
103
|
+
def lowered_words(text: str) -> str:
|
|
104
|
+
return re.sub(r"\s+", " ", text.strip().lower())
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
def contains_any(text: str, needles: tuple[str, ...]) -> bool:
|
|
108
|
+
lowered = lowered_words(text)
|
|
109
|
+
return any(needle in lowered for needle in needles)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
def classify_task(objective: str, task_kind: str) -> dict[str, Any]:
|
|
113
|
+
if contains_any(objective, HIGH_RISK_WORDS):
|
|
114
|
+
return {"route": "ask_user", "reason": "high_risk_task"}
|
|
115
|
+
if "no-zcode" in lowered_words(objective):
|
|
116
|
+
return {"route": "codex_direct", "reason": "no_zcode_requested"}
|
|
117
|
+
if task_kind == "read-only":
|
|
118
|
+
return {"route": "codex_direct", "reason": "read_only_task"}
|
|
119
|
+
if task_kind == "trivial":
|
|
120
|
+
return {"route": "codex_direct", "reason": "trivial_task"}
|
|
121
|
+
if task_kind in {"plan", "audit"}:
|
|
122
|
+
return {"route": "codex_direct", "reason": f"{task_kind}_owned_by_codex"}
|
|
123
|
+
if task_kind == "implementation":
|
|
124
|
+
return {"route": "delegate_zcode", "reason": "implementation_task"}
|
|
125
|
+
if contains_any(objective, TRIVIAL_WORDS) and len(objective) < 140:
|
|
126
|
+
return {"route": "codex_direct", "reason": "trivial_task"}
|
|
127
|
+
if contains_any(objective, IMPLEMENTATION_WORDS):
|
|
128
|
+
return {"route": "delegate_zcode", "reason": "implementation_task"}
|
|
129
|
+
if contains_any(objective, READ_ONLY_WORDS):
|
|
130
|
+
return {"route": "codex_direct", "reason": "read_only_task"}
|
|
131
|
+
return {"route": "codex_direct", "reason": "unclear_or_planning_task"}
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def slugify(value: str) -> str:
|
|
135
|
+
slug = re.sub(r"[^A-Za-z0-9_.-]+", "-", value.strip()).strip("-").lower()
|
|
136
|
+
return slug[:48] or "task"
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def route_defaults(config: dict[str, Any]) -> dict[str, Any]:
|
|
140
|
+
defaults = config.get("defaults") if isinstance(config.get("defaults"), dict) else {}
|
|
141
|
+
return {
|
|
142
|
+
"effort": defaults.get("effort", "max"),
|
|
143
|
+
"task_class": defaults.get("task_class", "root-cause"),
|
|
144
|
+
"risk_budget": defaults.get("risk_budget", "low"),
|
|
145
|
+
"workspace_kind": defaults.get("workspace_kind", "regular"),
|
|
146
|
+
"usage_snapshot_source": defaults.get("usage_snapshot_source", DEFAULT_USAGE_SNAPSHOT_SOURCE),
|
|
147
|
+
"max_attempts": int(defaults.get("max_attempts", 2)),
|
|
148
|
+
"retry_delay_ms": int(defaults.get("retry_delay_ms", 60000)),
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def trusted_supervisor_path() -> str:
|
|
153
|
+
return str(Path(__file__).resolve().with_name("zcode_supervisor.py"))
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def trusted_controller_path(args: argparse.Namespace) -> str:
|
|
157
|
+
if args.trusted_zcodectl:
|
|
158
|
+
return str(args.trusted_zcodectl.resolve())
|
|
159
|
+
return str(Path(__file__).resolve().parents[1] / "zcode_control" / "zcodectl.mjs")
|
|
160
|
+
|
|
161
|
+
|
|
162
|
+
def workspace_output_path(workspace: Path, raw: str) -> Path:
|
|
163
|
+
if not isinstance(raw, str) or not raw.strip():
|
|
164
|
+
raise ValueError("routing output path must be a non-empty string")
|
|
165
|
+
raw_path = Path(raw)
|
|
166
|
+
if raw_path.is_absolute():
|
|
167
|
+
raise ValueError(f"routing output path must be relative: {raw}")
|
|
168
|
+
visible = workspace / raw_path
|
|
169
|
+
cursor = visible
|
|
170
|
+
workspace_resolved = workspace.resolve()
|
|
171
|
+
while True:
|
|
172
|
+
if cursor.exists() or cursor.is_symlink():
|
|
173
|
+
if cursor.is_symlink():
|
|
174
|
+
raise ValueError(f"routing output path uses symlink: {cursor.relative_to(workspace)}")
|
|
175
|
+
if cursor == workspace:
|
|
176
|
+
break
|
|
177
|
+
cursor = cursor.parent
|
|
178
|
+
candidate = (workspace / raw_path).resolve()
|
|
179
|
+
try:
|
|
180
|
+
candidate.relative_to(workspace_resolved)
|
|
181
|
+
except ValueError as exc:
|
|
182
|
+
raise ValueError(f"routing output path escapes workspace: {raw}") from exc
|
|
183
|
+
return candidate
|
|
184
|
+
|
|
185
|
+
|
|
186
|
+
def packet_command(args: argparse.Namespace, config: dict[str, Any], packet_path: Path, prompt_path: Path) -> list[str]:
|
|
187
|
+
defaults = route_defaults(config)
|
|
188
|
+
command = [
|
|
189
|
+
sys.executable,
|
|
190
|
+
trusted_supervisor_path(),
|
|
191
|
+
"packet",
|
|
192
|
+
"--workspace",
|
|
193
|
+
str(args.workspace),
|
|
194
|
+
"--objective",
|
|
195
|
+
args.objective,
|
|
196
|
+
"--validation",
|
|
197
|
+
args.validation,
|
|
198
|
+
"--effort",
|
|
199
|
+
args.effort or defaults["effort"],
|
|
200
|
+
"--task-class",
|
|
201
|
+
args.task_class or defaults["task_class"],
|
|
202
|
+
"--risk-budget",
|
|
203
|
+
args.risk_budget or defaults["risk_budget"],
|
|
204
|
+
"--workspace-kind",
|
|
205
|
+
args.workspace_kind or defaults["workspace_kind"],
|
|
206
|
+
"--out",
|
|
207
|
+
str(packet_path),
|
|
208
|
+
"--prompt-out",
|
|
209
|
+
str(prompt_path),
|
|
210
|
+
]
|
|
211
|
+
for item in args.allowed:
|
|
212
|
+
command.extend(["--allowed", item])
|
|
213
|
+
for item in args.forbidden:
|
|
214
|
+
command.extend(["--forbidden", item])
|
|
215
|
+
if args.max_changed_files is not None:
|
|
216
|
+
command.extend(["--max-changed-files", str(args.max_changed_files)])
|
|
217
|
+
if args.goal:
|
|
218
|
+
command.append("--goal")
|
|
219
|
+
return command
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
def run_packet_command(args: argparse.Namespace, config: dict[str, Any], packet_path: Path, run_path: Path) -> list[str]:
|
|
223
|
+
defaults = route_defaults(config)
|
|
224
|
+
max_attempts = args.max_attempts if args.max_attempts is not None else defaults["max_attempts"]
|
|
225
|
+
retry_delay_ms = args.retry_delay_ms if args.retry_delay_ms is not None else defaults["retry_delay_ms"]
|
|
226
|
+
return [
|
|
227
|
+
"node",
|
|
228
|
+
trusted_controller_path(args),
|
|
229
|
+
"run-packet",
|
|
230
|
+
"--packet",
|
|
231
|
+
str(packet_path),
|
|
232
|
+
"--mode",
|
|
233
|
+
args.run_mode,
|
|
234
|
+
"--max-attempts",
|
|
235
|
+
str(max_attempts),
|
|
236
|
+
"--retry-delay-ms",
|
|
237
|
+
str(retry_delay_ms),
|
|
238
|
+
"--usage-snapshot-source",
|
|
239
|
+
args.usage_snapshot_source or defaults["usage_snapshot_source"],
|
|
240
|
+
"--out",
|
|
241
|
+
str(run_path),
|
|
242
|
+
]
|
|
243
|
+
|
|
244
|
+
|
|
245
|
+
def run_json_command(command: list[str], cwd: Path) -> tuple[int, dict[str, Any] | None, str, str]:
|
|
246
|
+
result = subprocess.run(command, cwd=cwd, text=True, capture_output=True, check=False)
|
|
247
|
+
parsed = None
|
|
248
|
+
if result.stdout.strip():
|
|
249
|
+
try:
|
|
250
|
+
parsed = json.loads(result.stdout)
|
|
251
|
+
except json.JSONDecodeError:
|
|
252
|
+
parsed = None
|
|
253
|
+
return result.returncode, parsed, result.stdout, result.stderr
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
def build_paths(workspace: Path, config: dict[str, Any], objective: str) -> tuple[Path, Path, Path]:
|
|
257
|
+
paths = config.get("paths") if isinstance(config.get("paths"), dict) else {}
|
|
258
|
+
task_id = f"{utc_slug()}-{slugify(objective)}"
|
|
259
|
+
packet_dir = workspace_output_path(workspace, paths.get("packets", ".codex/zcode/packets"))
|
|
260
|
+
run_dir = workspace_output_path(workspace, paths.get("runs", ".codex/zcode/runs"))
|
|
261
|
+
packet_path = packet_dir / f"{task_id}.json"
|
|
262
|
+
prompt_path = packet_dir / f"{task_id}.prompt.txt"
|
|
263
|
+
run_path = run_dir / f"{task_id}.zcode.json"
|
|
264
|
+
return packet_path, prompt_path, run_path
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
def explain_decision(
|
|
268
|
+
*,
|
|
269
|
+
workspace: Path,
|
|
270
|
+
config: dict[str, Any] | None,
|
|
271
|
+
classification: dict[str, Any],
|
|
272
|
+
args: argparse.Namespace,
|
|
273
|
+
) -> dict[str, Any]:
|
|
274
|
+
route = classification["route"]
|
|
275
|
+
needs_planning = route == "delegate_zcode" and (not args.allowed or not args.validation)
|
|
276
|
+
return {
|
|
277
|
+
"ok": True,
|
|
278
|
+
"workspace": str(workspace),
|
|
279
|
+
"routing_config": str(routing_path(workspace)) if config else None,
|
|
280
|
+
"routing_mode": config.get("routing_mode") if config else None,
|
|
281
|
+
"route": "needs_codex_planning" if needs_planning else route,
|
|
282
|
+
"reason": "missing_allowed_or_validation" if needs_planning else classification["reason"],
|
|
283
|
+
"codex_owns": (config or {}).get("policy", {}).get("codex_owns", []),
|
|
284
|
+
"zcode_owns": (config or {}).get("policy", {}).get("zcode_owns", []),
|
|
285
|
+
"next_action": next_action(route, classification["reason"], needs_planning),
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
|
|
289
|
+
def next_action(route: str, reason: str, needs_planning: bool) -> str:
|
|
290
|
+
if needs_planning:
|
|
291
|
+
return "Codex should choose a tight allowed-file set and validation command, then rerun auto-route --execute."
|
|
292
|
+
if route == "delegate_zcode":
|
|
293
|
+
return "Run with --execute to create a packet and delegate bounded implementation to ZCode."
|
|
294
|
+
if route == "ask_user":
|
|
295
|
+
return "Pause for a concise plan because this matches an ask-before risk category."
|
|
296
|
+
if route == "codex_direct":
|
|
297
|
+
return f"Codex may handle this directly because {reason}."
|
|
298
|
+
return "Inspect the routing decision before proceeding."
|
|
299
|
+
|
|
300
|
+
|
|
301
|
+
def auto_route_command(args: argparse.Namespace) -> int:
|
|
302
|
+
workspace = args.workspace.resolve()
|
|
303
|
+
if not workspace.is_dir():
|
|
304
|
+
raise ValueError(f"workspace does not exist: {workspace}")
|
|
305
|
+
config = load_routing(workspace)
|
|
306
|
+
if config is None:
|
|
307
|
+
emit_json({
|
|
308
|
+
"ok": True,
|
|
309
|
+
"workspace": str(workspace),
|
|
310
|
+
"route": "codex_direct",
|
|
311
|
+
"reason": "routing_config_missing",
|
|
312
|
+
"next_action": "Run zcode-install-repo for this repo if ZCode delegation should be enabled.",
|
|
313
|
+
})
|
|
314
|
+
return 0
|
|
315
|
+
|
|
316
|
+
classification = classify_task(args.objective, args.task_kind)
|
|
317
|
+
decision = explain_decision(workspace=workspace, config=config, classification=classification, args=args)
|
|
318
|
+
if decision["route"] != "delegate_zcode" or not args.execute:
|
|
319
|
+
emit_json(decision)
|
|
320
|
+
return 0
|
|
321
|
+
|
|
322
|
+
packet_path, prompt_path, run_path = build_paths(workspace, config, args.objective)
|
|
323
|
+
packet_cmd = packet_command(args, config, packet_path, prompt_path)
|
|
324
|
+
packet_rc, packet_json, packet_stdout, packet_stderr = run_json_command(packet_cmd, workspace)
|
|
325
|
+
if packet_rc != 0:
|
|
326
|
+
emit_json({
|
|
327
|
+
**decision,
|
|
328
|
+
"ok": False,
|
|
329
|
+
"route": "packet_failed",
|
|
330
|
+
"packet_command": packet_cmd,
|
|
331
|
+
"packet_stdout": packet_stdout[-4000:],
|
|
332
|
+
"packet_stderr": packet_stderr[-4000:],
|
|
333
|
+
})
|
|
334
|
+
return 1
|
|
335
|
+
|
|
336
|
+
run_cmd = run_packet_command(args, config, packet_path, run_path)
|
|
337
|
+
run_rc, run_json, run_stdout, run_stderr = run_json_command(run_cmd, workspace)
|
|
338
|
+
payload = {
|
|
339
|
+
**decision,
|
|
340
|
+
"executed": True,
|
|
341
|
+
"packet": str(packet_path),
|
|
342
|
+
"prompt": str(prompt_path),
|
|
343
|
+
"run": str(run_path),
|
|
344
|
+
"packet_command": packet_cmd,
|
|
345
|
+
"run_command": run_cmd,
|
|
346
|
+
"packet_result": packet_json,
|
|
347
|
+
"run_result": run_json,
|
|
348
|
+
"run_stdout_tail": run_stdout[-4000:] if run_json is None else "",
|
|
349
|
+
"run_stderr_tail": run_stderr[-4000:],
|
|
350
|
+
"ok": run_rc == 0,
|
|
351
|
+
}
|
|
352
|
+
write_json(run_path.with_suffix(".route.json"), payload)
|
|
353
|
+
emit_json(payload)
|
|
354
|
+
return 0 if run_rc == 0 else 1
|
|
355
|
+
|
|
356
|
+
|
|
357
|
+
def add_auto_route_parser(subparsers: argparse._SubParsersAction[argparse.ArgumentParser]) -> None:
|
|
358
|
+
route = subparsers.add_parser("auto-route", help="Route a task through the repo-local ZCode delegation contract.")
|
|
359
|
+
route.add_argument("--workspace", type=Path, default=Path("."))
|
|
360
|
+
route.add_argument("--objective", required=True)
|
|
361
|
+
route.add_argument("--task-kind", choices=("auto", "implementation", "read-only", "plan", "audit", "trivial"), default="auto")
|
|
362
|
+
route.add_argument("--allowed", action="append", default=[])
|
|
363
|
+
route.add_argument("--forbidden", action="append", default=[])
|
|
364
|
+
route.add_argument("--validation", default="")
|
|
365
|
+
route.add_argument("--execute", action="store_true")
|
|
366
|
+
route.add_argument("--run-mode", choices=("plan", "edit", "build", "yolo"), default="edit")
|
|
367
|
+
route.add_argument("--effort", choices=("high", "max"))
|
|
368
|
+
route.add_argument("--task-class", choices=("small-fix", "long-horizon", "architecture", "root-cause", "production-gate", "mobile-debug", "research"))
|
|
369
|
+
route.add_argument("--risk-budget", choices=("low", "medium", "high"))
|
|
370
|
+
route.add_argument("--workspace-kind", choices=("regular", "worktree", "disposable", "fixture"))
|
|
371
|
+
route.add_argument("--max-changed-files", type=int)
|
|
372
|
+
route.add_argument("--max-attempts", type=int)
|
|
373
|
+
route.add_argument("--retry-delay-ms", type=int)
|
|
374
|
+
route.add_argument("--usage-snapshot-source", choices=("auto", "zai-api", "codexbar", "none"))
|
|
375
|
+
route.add_argument("--trusted-zcodectl", type=Path, help=argparse.SUPPRESS)
|
|
376
|
+
route.add_argument("--goal", action="store_true")
|
|
377
|
+
route.set_defaults(func=auto_route_command)
|
|
378
|
+
|
|
379
|
+
|
|
380
|
+
def auto_route_entrypoint(argv: list[str] | None = None) -> int:
|
|
381
|
+
parser = argparse.ArgumentParser(description="Route a task through a repo-local ZCode delegation contract.")
|
|
382
|
+
subparsers = parser.add_subparsers(dest="command", required=True)
|
|
383
|
+
add_auto_route_parser(subparsers)
|
|
384
|
+
args = parser.parse_args(["auto-route", *(argv if argv is not None else sys.argv[1:])])
|
|
385
|
+
try:
|
|
386
|
+
return args.func(args)
|
|
387
|
+
except (OSError, ValueError, json.JSONDecodeError) as exc:
|
|
388
|
+
emit_json({"ok": False, "error": str(exc)})
|
|
389
|
+
return 1
|
|
390
|
+
|
|
391
|
+
|
|
392
|
+
if __name__ == "__main__":
|
|
393
|
+
raise SystemExit(auto_route_entrypoint())
|