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.
@@ -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,2 @@
1
+ """ZCode supervisor helpers."""
2
+
@@ -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())