workflow-toolkit 1.0.1

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.
Files changed (142) hide show
  1. package/.agents/skills/ponytail/SKILL.md +111 -0
  2. package/.agents/skills/ponytail-audit/SKILL.md +37 -0
  3. package/.agents/skills/ponytail-debt/SKILL.md +41 -0
  4. package/.agents/skills/ponytail-gain/SKILL.md +47 -0
  5. package/.agents/skills/ponytail-help/SKILL.md +70 -0
  6. package/.agents/skills/ponytail-review/SKILL.md +52 -0
  7. package/.agents/skills/prompt-review/SKILL.md +60 -0
  8. package/.agents/skills/wtk/SKILL.md +64 -0
  9. package/.agents/skills/wtk/references/artifacts.md +82 -0
  10. package/.agents/skills/wtk/references/evidence.md +68 -0
  11. package/.agents/skills/wtk/references/git.md +75 -0
  12. package/.agents/skills/wtk/references/test-contract.md +76 -0
  13. package/.agents/skills/wtk/references/validation.md +127 -0
  14. package/.agents/skills/wtk-config/SKILL.md +102 -0
  15. package/.agents/skills/wtk-config/assets/agents/claude/deep-reviewer.md +40 -0
  16. package/.agents/skills/wtk-config/assets/agents/claude/designer.md +57 -0
  17. package/.agents/skills/wtk-config/assets/agents/claude/explorer.md +41 -0
  18. package/.agents/skills/wtk-config/assets/agents/claude/implementer.md +57 -0
  19. package/.agents/skills/wtk-config/assets/agents/claude/planner.md +68 -0
  20. package/.agents/skills/wtk-config/assets/agents/claude/verifier.md +80 -0
  21. package/.agents/skills/wtk-config/assets/agents/codex/deep-reviewer.toml +41 -0
  22. package/.agents/skills/wtk-config/assets/agents/codex/designer.toml +55 -0
  23. package/.agents/skills/wtk-config/assets/agents/codex/explorer.toml +35 -0
  24. package/.agents/skills/wtk-config/assets/agents/codex/implementer.toml +52 -0
  25. package/.agents/skills/wtk-config/assets/agents/codex/planner.toml +66 -0
  26. package/.agents/skills/wtk-config/assets/agents/codex/verifier.toml +78 -0
  27. package/.agents/skills/wtk-config/assets/agents/cursor/deep-reviewer.md +38 -0
  28. package/.agents/skills/wtk-config/assets/agents/cursor/designer.md +55 -0
  29. package/.agents/skills/wtk-config/assets/agents/cursor/explorer.md +39 -0
  30. package/.agents/skills/wtk-config/assets/agents/cursor/implementer.md +55 -0
  31. package/.agents/skills/wtk-config/assets/agents/cursor/planner.md +66 -0
  32. package/.agents/skills/wtk-config/assets/agents/cursor/verifier.md +79 -0
  33. package/.agents/skills/wtk-config/scripts/ad-index.py +123 -0
  34. package/.agents/skills/wtk-config/scripts/repository_intelligence.py +671 -0
  35. package/.agents/skills/wtk-config/scripts/workflow_config.py +972 -0
  36. package/.agents/skills/wtk-deep-review/SKILL.md +164 -0
  37. package/.agents/skills/wtk-deep-review/assets/PROMPT.md +56 -0
  38. package/.agents/skills/wtk-deep-review/assets/REVIEW_UI.html +1340 -0
  39. package/.agents/skills/wtk-deep-review/assets/findings.schema.json +113 -0
  40. package/.agents/skills/wtk-deep-review/references/context-pack.md +88 -0
  41. package/.agents/skills/wtk-deep-review/references/orchestration.md +164 -0
  42. package/.agents/skills/wtk-deep-review/references/output-contracts.md +149 -0
  43. package/.agents/skills/wtk-deep-review/references/publish-github.md +83 -0
  44. package/.agents/skills/wtk-deep-review/references/state-and-learnings.md +64 -0
  45. package/.agents/skills/wtk-deep-review/references/subagent-runtimes.md +33 -0
  46. package/.agents/skills/wtk-deep-review/references/taxonomy.md +70 -0
  47. package/.agents/skills/wtk-deep-review/scripts/_common.py +371 -0
  48. package/.agents/skills/wtk-deep-review/scripts/build_jobs.py +561 -0
  49. package/.agents/skills/wtk-deep-review/scripts/build_knowledge.py +349 -0
  50. package/.agents/skills/wtk-deep-review/scripts/build_manifest.py +479 -0
  51. package/.agents/skills/wtk-deep-review/scripts/graft_context.py +160 -0
  52. package/.agents/skills/wtk-deep-review/scripts/graphify_context.py +75 -0
  53. package/.agents/skills/wtk-deep-review/scripts/merge_findings.py +313 -0
  54. package/.agents/skills/wtk-deep-review/scripts/render_html.py +240 -0
  55. package/.agents/skills/wtk-deep-review/scripts/render_review.py +282 -0
  56. package/.agents/skills/wtk-deep-review/scripts/run_jobs.py +436 -0
  57. package/.agents/skills/wtk-deep-review/scripts/token_metrics.py +369 -0
  58. package/.agents/skills/wtk-discover/SKILL.md +270 -0
  59. package/.agents/skills/wtk-discover/references/document-format.md +149 -0
  60. package/.agents/skills/wtk-implement/SKILL.md +95 -0
  61. package/.agents/skills/wtk-implement/references/checklist-format.md +116 -0
  62. package/.agents/skills/wtk-implement/references/screens.md +129 -0
  63. package/.agents/skills/wtk-implement/references/test-policy.md +152 -0
  64. package/.agents/skills/wtk-implement/references/verify.md +297 -0
  65. package/.agents/skills/wtk-knowledge-check/SKILL.md +16 -0
  66. package/.agents/skills/wtk-knowledge-check/scripts/check.ts +502 -0
  67. package/.agents/skills/wtk-knowledge-check/scripts/cli.ts +32 -0
  68. package/.agents/skills/wtk-knowledge-check/scripts/frontmatter.ts +44 -0
  69. package/.agents/skills/wtk-lean/.skill-meta.json +6 -0
  70. package/.agents/skills/wtk-lean/NOTICE.md +18 -0
  71. package/.agents/skills/wtk-lean/SKILL.md +137 -0
  72. package/.agents/skills/wtk-lean/references/build.md +168 -0
  73. package/.agents/skills/wtk-lean/references/checks.md +253 -0
  74. package/.agents/skills/wtk-lean/references/memory.md +156 -0
  75. package/.agents/skills/wtk-lean/references/plan.md +514 -0
  76. package/.agents/skills/wtk-lean/references/verify.md +336 -0
  77. package/.agents/skills/wtk-lean/scripts/check_commit.py +121 -0
  78. package/.agents/skills/wtk-lean/scripts/fixtures/checks.md +98 -0
  79. package/.agents/skills/wtk-lean/scripts/fixtures/plan.md +119 -0
  80. package/.agents/skills/wtk-lean/scripts/fixtures/verification.md +41 -0
  81. package/.agents/skills/wtk-lean/scripts/lessons.py +412 -0
  82. package/.agents/skills/wtk-lean/scripts/selftest.py +401 -0
  83. package/.agents/skills/wtk-lean/scripts/validate_checks.py +417 -0
  84. package/.agents/skills/wtk-lean/scripts/validate_plan.py +580 -0
  85. package/.agents/skills/wtk-lean/scripts/validate_verification.py +357 -0
  86. package/.agents/skills/wtk-plan/SKILL.md +102 -0
  87. package/.agents/skills/wtk-plan/references/document-format.md +222 -0
  88. package/.agents/skills/wtk-qa/SKILL.md +14 -0
  89. package/.agents/skills/wtk-qa-execute/SKILL.md +115 -0
  90. package/.agents/skills/wtk-qa-execute/references/fix-loop.md +23 -0
  91. package/.agents/skills/wtk-qa-execute/references/session-protocol.md +25 -0
  92. package/.agents/skills/wtk-qa-plan/SKILL.md +111 -0
  93. package/.agents/skills/wtk-qa-plan/references/profile.md +35 -0
  94. package/.agents/skills/wtk-ship/SKILL.md +49 -0
  95. package/.agents/skills/wtk-ship/remediation.py +170 -0
  96. package/.agents/skills/wtk-ship/scripts/close_feature.py +83 -0
  97. package/.agents/skills/wtk-ship/scripts/review_convergence.py +373 -0
  98. package/.wtk.toml.example +93 -0
  99. package/AGENTS.md +129 -0
  100. package/NOTICE.md +12 -0
  101. package/README.md +440 -0
  102. package/bin/wtk.js +25 -0
  103. package/docs/toolkit/README.md +56 -0
  104. package/docs/toolkit/decisions.md +61 -0
  105. package/docs/toolkit/guidelines/CONTEXT-BUDGET.md +64 -0
  106. package/docs/toolkit/guidelines/DX.md +67 -0
  107. package/docs/toolkit/guidelines/FRONTEND.md +73 -0
  108. package/docs/toolkit/guidelines/KNOWLEDGE-WIKI.md +128 -0
  109. package/docs/toolkit/guidelines/MODELING.md +134 -0
  110. package/docs/toolkit/guidelines/QA-EXECUTION.md +46 -0
  111. package/docs/toolkit/guidelines/QA-SCENARIOS.md +165 -0
  112. package/docs/toolkit/guidelines/REVIEW-ROUNDS.md +160 -0
  113. package/docs/toolkit/guidelines/SECURITY.md +160 -0
  114. package/docs/toolkit/guidelines/UI-UX.md +117 -0
  115. package/docs/toolkit/guidelines/WORKFLOW-MEMORY.md +73 -0
  116. package/docs/toolkit/guidelines.md +50 -0
  117. package/docs/toolkit/loop.md +77 -0
  118. package/docs/toolkit/purpose.md +51 -0
  119. package/docs/toolkit/repository-intelligence.md +53 -0
  120. package/docs/toolkit/reviews.md +77 -0
  121. package/knowledge/AGENTS.md +316 -0
  122. package/knowledge/raw/README.md +17 -0
  123. package/package.json +74 -0
  124. package/scripts/install_security_skills.py +816 -0
  125. package/scripts/installer/engine.js +254 -0
  126. package/scripts/installer/knowledge.js +28 -0
  127. package/scripts/installer/packets.js +77 -0
  128. package/scripts/installer/terminal.js +98 -0
  129. package/scripts/installer/transaction.js +75 -0
  130. package/skills-lock.json +92 -0
  131. package/templates/adoption/agents/core.md +23 -0
  132. package/templates/adoption/agents/quality.md +3 -0
  133. package/templates/adoption/knowledge/wiki/architecture/index.md +3 -0
  134. package/templates/adoption/knowledge/wiki/decisions/index.md +3 -0
  135. package/templates/adoption/knowledge/wiki/design/index.md +3 -0
  136. package/templates/adoption/knowledge/wiki/domain/index.md +3 -0
  137. package/templates/adoption/knowledge/wiki/index.md +3 -0
  138. package/templates/adoption/knowledge/wiki/log.md +3 -0
  139. package/templates/adoption/knowledge/wiki/open-questions/index.md +3 -0
  140. package/templates/adoption/knowledge/wiki/product/index.md +3 -0
  141. package/templates/adoption/knowledge/wiki/research/index.md +3 -0
  142. package/templates/adoption/product/AGENT-CONTEXT.md +25 -0
@@ -0,0 +1,436 @@
1
+ #!/usr/bin/env python3
2
+ """Deep-review job runner/validator (mutating helper; writes only under --out).
3
+
4
+ --validate-only Report each job as VALID / PENDING / INVALID.
5
+ --command '<template>' Execute pending jobs with bounded concurrency and retries,
6
+ output validation, and provider-block detection.
7
+
8
+ Valid outputs are preserved and resumed. Optional metrics observe the run without
9
+ changing dispatch, output ordering, retries, or exit behavior. Exit codes are 0 for
10
+ valid outputs, 1 for failures/pending outputs, 2 for provider blocks, and 3 for
11
+ source drift.
12
+ """
13
+
14
+ from __future__ import annotations
15
+
16
+ import argparse
17
+ from concurrent.futures import FIRST_COMPLETED, ThreadPoolExecutor, wait
18
+ import json
19
+ import shlex
20
+ import subprocess
21
+ import sys
22
+ import threading
23
+ from pathlib import Path
24
+
25
+ sys.dont_write_bytecode = True
26
+
27
+ from _common import check_freeze, load_jobs, rel, repo_root, validate_job_output, write_json
28
+ from token_metrics import (
29
+ checkpoint_metrics,
30
+ finalize_metrics,
31
+ start_metrics,
32
+ write_unavailable_metrics,
33
+ )
34
+
35
+ PRINT_LOCK = threading.Lock()
36
+ STOP_EVENT = threading.Event()
37
+ STOP_REASON: dict[str, str] = {}
38
+ STOP_LOCK = threading.Lock()
39
+
40
+
41
+ def say(message: str) -> None:
42
+ with PRINT_LOCK:
43
+ print(message, flush=True)
44
+
45
+
46
+ def job_state(repo: Path, out: Path, job: dict) -> tuple[str, str]:
47
+ if not (repo / job["output"]).is_file():
48
+ return "pending", "missing output"
49
+ try:
50
+ validate_job_output(repo, out, job)
51
+ except ValueError as error:
52
+ return "invalid", str(error)
53
+ return "valid", ""
54
+
55
+
56
+ def manifest_concurrency(out: Path) -> int:
57
+ """Read the frozen scheduler bound; old fixtures without it use the default."""
58
+ manifest_path = out / "manifest.json"
59
+ if not manifest_path.is_file():
60
+ return 3
61
+ try:
62
+ manifest = json.loads(manifest_path.read_text(encoding="utf-8"))
63
+ except (OSError, json.JSONDecodeError) as error:
64
+ raise RuntimeError(f"cannot read manifest concurrency: {error}") from error
65
+ value = manifest.get("concurrency", 3)
66
+ if isinstance(value, bool) or not isinstance(value, int) or not 1 <= value <= 6:
67
+ raise RuntimeError("manifest concurrency must be an integer from 1 through 6")
68
+ return value
69
+
70
+
71
+ def blocked_pattern(stdout_text: str, stderr_text: str, patterns: list[str]) -> str | None:
72
+ """Match block patterns in structured error events and raw non-JSON/stderr lines — never in tool output."""
73
+ def hit(text: str) -> str | None:
74
+ return next((pattern for pattern in patterns if pattern in text), None)
75
+
76
+ for line in stdout_text.splitlines():
77
+ try:
78
+ event = json.loads(line)
79
+ except json.JSONDecodeError:
80
+ found = hit(line) # unknown transport: the raw line is the signal
81
+ else:
82
+ found = hit(line) if isinstance(event, dict) and event.get("type") in {"error", "turn.failed"} else None
83
+ if found:
84
+ return found
85
+ return hit(stderr_text)
86
+
87
+
88
+ def record_block(label: str, reason: str) -> None:
89
+ with STOP_LOCK:
90
+ STOP_EVENT.set()
91
+ STOP_REASON.setdefault("reason", reason)
92
+ STOP_REASON.setdefault("label", label)
93
+
94
+
95
+ def render_command(template: str, job: dict, prompt: str) -> list[str]:
96
+ return [
97
+ token.replace("{prompt}", prompt)
98
+ .replace("{output}", job["output"])
99
+ .replace("{label}", job["label"])
100
+ for token in shlex.split(template)
101
+ ]
102
+
103
+
104
+ def repair_prompt(repo: Path, out: Path, job: dict, attempt: int, error: str, invalid: Path) -> str:
105
+ """Keep the invalid artifact and write the prompt that fixes it instead of re-reviewing."""
106
+ path = out / "prompts" / f"{job['label']}.repair-{attempt}.md"
107
+ path.parent.mkdir(parents=True, exist_ok=True)
108
+ path.write_text(
109
+ f"Original prompt: `{job['prompt']}`\nInvalid artifact: `{rel(invalid, repo)}`\n"
110
+ f"Output file: `{job['output']}`\nValidation errors:\n{error}\n\n"
111
+ "Your previous artifact failed validation for the reasons above. Do not re-review. Read the "
112
+ "invalid artifact, correct only what the errors name, keep every finding, disposition and "
113
+ "coverage row otherwise unchanged, and rewrite the output file.\n",
114
+ encoding="utf-8",
115
+ )
116
+ return rel(path, repo)
117
+
118
+
119
+ def run_one(repo: Path, out: Path, job: dict, args) -> dict:
120
+ label = job["label"]
121
+ output = repo / job["output"]
122
+ state, _ = job_state(repo, out, job)
123
+ if state == "valid":
124
+ say(f"SKIP {label} existing-valid-output")
125
+ return {"label": label, "status": "pass", "attempt": 0, "preserved": True}
126
+
127
+ runs_dir = out / "runs"
128
+ last_error, exit_code, prompt = "not run", None, job["prompt"]
129
+ for attempt in range(1, args.attempts + 1):
130
+ if STOP_EVENT.is_set():
131
+ return {"label": label, "status": "blocked", "attempt": attempt - 1,
132
+ "error": STOP_REASON.get("reason", "run stopped")}
133
+ output.parent.mkdir(parents=True, exist_ok=True)
134
+ if output.exists():
135
+ output.unlink()
136
+ stdout_path = runs_dir / f"{label}.attempt-{attempt}.events.jsonl"
137
+ stderr_path = runs_dir / f"{label}.attempt-{attempt}.err"
138
+ with stdout_path.open("w", encoding="utf-8") as out_file, stderr_path.open("w", encoding="utf-8") as err_file:
139
+ try:
140
+ completed = subprocess.run(
141
+ render_command(args.command, job, prompt), cwd=repo, stdout=out_file, stderr=err_file,
142
+ check=False, timeout=args.timeout_min * 60,
143
+ )
144
+ exit_code = completed.returncode
145
+ except subprocess.TimeoutExpired:
146
+ exit_code = None
147
+ last_error = f"runner timeout after {args.timeout_min}m"
148
+ say(f"RETRY {label} attempt={attempt} reason={last_error}")
149
+ continue
150
+ blocked_on = blocked_pattern(
151
+ stdout_path.read_text(encoding="utf-8", errors="replace"),
152
+ stderr_path.read_text(encoding="utf-8", errors="replace"),
153
+ args.block_on,
154
+ )
155
+ if blocked_on:
156
+ record_block(label, blocked_on) # stop refilling slots even when this artifact landed
157
+ if job_state(repo, out, job)[0] == "valid":
158
+ say(f"PASS {label} attempt={attempt} (block {blocked_on} after a valid artifact)")
159
+ return {"label": label, "status": "pass", "attempt": attempt, "exit_code": exit_code}
160
+ say(f"BLOCKED {label} pattern={blocked_on}")
161
+ return {"label": label, "status": "blocked", "attempt": attempt, "exit_code": exit_code, "error": blocked_on}
162
+ if exit_code != 0:
163
+ last_error = f"command exit {exit_code}"
164
+ else:
165
+ state, reason = job_state(repo, out, job)
166
+ if state == "valid":
167
+ say(f"PASS {label} attempt={attempt}")
168
+ return {"label": label, "status": "pass", "attempt": attempt, "exit_code": 0}
169
+ last_error = reason or "output invalid"
170
+ if attempt < args.attempts: # keep the invalid artifact; the next attempt repairs it
171
+ kept = output.with_name(f"{output.name}.attempt-{attempt}-invalid.json")
172
+ output.replace(kept)
173
+ prompt = repair_prompt(repo, out, job, attempt + 1, reason, kept)
174
+ say(f"REPAIR {label} attempt={attempt + 1}")
175
+ say(f"RETRY {label} attempt={attempt} reason={last_error}")
176
+ return {"label": label, "status": "fail", "attempt": args.attempts, "exit_code": exit_code, "error": last_error}
177
+
178
+
179
+ def stage_report(repo: Path, out: Path, jobs: list[dict]) -> tuple[int, list[dict]]:
180
+ pending = []
181
+ for job in jobs:
182
+ state, reason = job_state(repo, out, job)
183
+ if state != "valid":
184
+ pending.append({"label": job["label"], "state": state, "reason": reason})
185
+ return len(jobs) - len(pending), pending
186
+
187
+
188
+ def metrics_path(args, out: Path) -> Path:
189
+ return Path(args.metrics_ledger).resolve() if args.metrics_ledger else out / "runs" / "review-metrics.json"
190
+
191
+
192
+ def mark_metrics_unavailable(path: Path, reason: str) -> None:
193
+ try:
194
+ write_unavailable_metrics(path, reason)
195
+ except Exception:
196
+ pass
197
+
198
+
199
+ def start_observation(args, out: Path, scope_jobs: list[dict]) -> tuple[str, Path, dict]:
200
+ try:
201
+ path = metrics_path(args, out)
202
+ configured = bool(args.metrics or args.metrics_db or args.metrics_ledger or args.metrics_reviewer_prefix)
203
+ if not configured:
204
+ mark_metrics_unavailable(path, "compatible telemetry unavailable")
205
+ return "unavailable", path, {"status": "unavailable"}
206
+ metrics = start_metrics(
207
+ path,
208
+ args.metrics_db,
209
+ args.metrics_reviewer_prefix,
210
+ repository=str(repo_root()), round=args.round, base=args.base, head=args.head,
211
+ selected_files=args.selected_files or len(scope_jobs), carried_files=args.carried_files,
212
+ jobs=len(scope_jobs), model=args.model, reasoning=args.reasoning,
213
+ )
214
+ return metrics["status"], path, metrics
215
+ except Exception:
216
+ fallback = out / "runs" / "review-metrics.json"
217
+ mark_metrics_unavailable(fallback, "metrics unavailable")
218
+ return "unavailable", fallback, {"status": "unavailable"}
219
+
220
+
221
+ def checkpoint_observation(path: Path, completed_jobs: int) -> str:
222
+ try:
223
+ return checkpoint_metrics(path, completed_jobs)["status"]
224
+ except Exception:
225
+ mark_metrics_unavailable(path, "metrics unavailable")
226
+ return "unavailable"
227
+
228
+
229
+ def finalize_observation(path: Path) -> str:
230
+ try:
231
+ return finalize_metrics(path)["status"]
232
+ except Exception:
233
+ mark_metrics_unavailable(path, "metrics unavailable")
234
+ return "unavailable"
235
+
236
+
237
+ def run_pending_jobs(repo: Path, out: Path, jobs: list[dict], args, concurrency: int, on_complete=None) -> dict[str, dict]:
238
+ """Run pending jobs in a bounded pool; all result ordering is restored by the caller."""
239
+ pending = [job for job in jobs if job_state(repo, out, job)[0] != "valid"]
240
+ results: dict[str, dict] = {}
241
+ if not pending:
242
+ return results
243
+
244
+ active: dict[object, dict] = {}
245
+ next_index = 0
246
+ blocked = False
247
+ with ThreadPoolExecutor(max_workers=min(concurrency, len(pending))) as executor:
248
+ while next_index < len(pending) and len(active) < concurrency:
249
+ job = pending[next_index]
250
+ next_index += 1
251
+ active[executor.submit(run_one, repo, out, job, args)] = job
252
+
253
+ while active:
254
+ done, _ = wait(tuple(active), return_when=FIRST_COMPLETED)
255
+ for future in done:
256
+ job = active.pop(future)
257
+ try:
258
+ result = future.result()
259
+ except Exception as error: # keep sibling jobs independent
260
+ result = {
261
+ "label": job["label"], "status": "fail", "attempt": 0,
262
+ "error": f"runner exception: {error}",
263
+ }
264
+ results[job["label"]] = result
265
+ if on_complete is not None:
266
+ on_complete(result)
267
+ if result["status"] == "fail":
268
+ say(f"FAIL {job['label']}: {result.get('error', 'job failed')}")
269
+ blocked = blocked or result["status"] == "blocked" or STOP_EVENT.is_set()
270
+
271
+ if not blocked:
272
+ while next_index < len(pending) and len(active) < concurrency:
273
+ job = pending[next_index]
274
+ next_index += 1
275
+ active[executor.submit(run_one, repo, out, job, args)] = job
276
+
277
+ if blocked:
278
+ reason = STOP_REASON.get("reason", "provider block")
279
+ for job in pending[next_index:]:
280
+ results[job["label"]] = {
281
+ "label": job["label"], "status": "blocked", "attempt": 0, "error": reason,
282
+ }
283
+ return results
284
+
285
+
286
+ def main() -> int:
287
+ parser = argparse.ArgumentParser(description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
288
+ parser.add_argument("--out", required=True)
289
+ parser.add_argument("--jobs-file", help="default: <out>/jobs.json")
290
+ parser.add_argument("--only", nargs="*", help="exact job labels to consider")
291
+ parser.add_argument("--validate-only", action="store_true")
292
+ parser.add_argument("--command", help="subprocess template; {prompt} required, {output}/{label} optional")
293
+ parser.add_argument("--attempts", type=int, default=2)
294
+ parser.add_argument("--timeout-min", type=int, default=35)
295
+ parser.add_argument("--block-on", action="append", default=None, help="provider-block substring (repeatable)")
296
+ parser.add_argument("--status-file", help="default: <out>/runs/<jobs-stem>-status.json")
297
+ parser.add_argument("--no-freeze-check", action="store_true")
298
+ parser.add_argument("--metrics", action="store_true", help="observe compatible provider token metrics")
299
+ parser.add_argument("--metrics-db", help="provider telemetry database supplied by an adapter")
300
+ parser.add_argument("--metrics-ledger", help="content-safe observational metrics path")
301
+ parser.add_argument("--metrics-reviewer-prefix", help="explicit provider reviewer path for telemetry")
302
+ parser.add_argument("--round", type=int, default=0)
303
+ parser.add_argument("--base", default="unknown")
304
+ parser.add_argument("--head", default="unknown")
305
+ parser.add_argument("--selected-files", type=int)
306
+ parser.add_argument("--carried-files", type=int, default=0)
307
+ parser.add_argument("--model", default="unknown")
308
+ parser.add_argument("--reasoning", default="unknown")
309
+ args = parser.parse_args()
310
+ if bool(args.validate_only) == bool(args.command):
311
+ parser.error("pass exactly one of --validate-only or --command")
312
+ if not 1 <= args.attempts <= 3:
313
+ parser.error("--attempts must be between 1 and 3")
314
+ if args.command and "{prompt}" not in args.command:
315
+ parser.error("--command must contain the {prompt} placeholder")
316
+ args.block_on = args.block_on or ["usageLimitExceeded"]
317
+ STOP_EVENT.clear()
318
+ STOP_REASON.clear()
319
+
320
+ repo = repo_root()
321
+ out = Path(args.out).resolve()
322
+ jobs_path = Path(args.jobs_file).resolve() if args.jobs_file else out / "jobs.json"
323
+ try:
324
+ scope_jobs = load_jobs(jobs_path)
325
+ except RuntimeError as error:
326
+ sys.stderr.write(f"{error}\n")
327
+ return 1
328
+ try:
329
+ concurrency = manifest_concurrency(out)
330
+ except RuntimeError as error:
331
+ sys.stderr.write(f"{error}\n")
332
+ return 1
333
+ if args.only:
334
+ unknown = set(args.only) - {job["label"] for job in scope_jobs}
335
+ if unknown:
336
+ parser.error(f"unknown jobs: {sorted(unknown)}")
337
+ jobs = [job for job in scope_jobs if job["label"] in args.only]
338
+ else:
339
+ jobs = scope_jobs
340
+
341
+ status_path = Path(args.status_file).resolve() if args.status_file else out / "runs" / f"{jobs_path.stem}-status.json"
342
+ if args.validate_only:
343
+ if not args.no_freeze_check:
344
+ try:
345
+ drift = check_freeze(repo, out, "validate")
346
+ except RuntimeError as error:
347
+ sys.stderr.write(f"{error}\n")
348
+ return 1
349
+ if drift:
350
+ sys.stderr.write(drift[0] + "\n")
351
+ return 3
352
+ rows = []
353
+ for job in jobs:
354
+ state, reason = job_state(repo, out, job)
355
+ row = {"label": job["label"], "status": state, "reason": reason or None}
356
+ if state != "valid":
357
+ row["prompt"], row["output"] = job["prompt"], job["output"]
358
+ rows.append(row)
359
+ say(f"{state.upper()} {job['label']}" + (f" — {reason}" if reason else ""))
360
+ write_json(status_path, {"mode": "validate", "jobs": rows})
361
+ pending = [row for row in rows if row["status"] != "valid"]
362
+ say(f"SUMMARY valid={len(rows) - len(pending)} pending={len(pending)} of {len(rows)}")
363
+ return 0 if not pending else 1
364
+
365
+ if not args.no_freeze_check:
366
+ try:
367
+ drift = check_freeze(repo, out, "before run")
368
+ except RuntimeError as error:
369
+ sys.stderr.write(f"{error}\n")
370
+ return 1
371
+ if drift:
372
+ sys.stderr.write(drift[0] + "\n")
373
+ return 3
374
+
375
+ (out / "runs").mkdir(parents=True, exist_ok=True)
376
+ metrics_status, metrics_file, _ = start_observation(args, out, scope_jobs)
377
+ initial_results = {
378
+ job["label"]: {"label": job["label"], "status": "pass", "attempt": 0, "preserved": True}
379
+ for job in jobs if job_state(repo, out, job)[0] == "valid"
380
+ }
381
+ completed_jobs = sum(job_state(repo, out, scope_job)[0] == "valid" for scope_job in scope_jobs)
382
+
383
+ def checkpoint_completed(_result: dict) -> None:
384
+ nonlocal completed_jobs, metrics_status
385
+ completed_jobs = min(len(scope_jobs), completed_jobs + 1)
386
+ if metrics_status == "running":
387
+ metrics_status = checkpoint_observation(metrics_file, completed_jobs)
388
+
389
+ pending_results = run_pending_jobs(repo, out, jobs, args, concurrency, checkpoint_completed)
390
+ results_by_label = {**initial_results, **pending_results}
391
+ results = [
392
+ results_by_label.get(job["label"], {
393
+ "label": job["label"], "status": "blocked", "attempt": 0,
394
+ "error": STOP_REASON.get("reason", "run stopped"),
395
+ })
396
+ for job in jobs
397
+ ]
398
+ valid_count, pending = stage_report(repo, out, jobs)
399
+ _, scope_pending = stage_report(repo, out, scope_jobs)
400
+ failed = [item for item in results if item["status"] == "fail"]
401
+ blocked = [item for item in results if item["status"] == "blocked"]
402
+ if metrics_status == "running" and not failed and not scope_pending:
403
+ metrics_status = finalize_observation(metrics_file)
404
+ if blocked:
405
+ try:
406
+ write_json(out / "run-blocker.json", {
407
+ "status": "blocked", "pattern": STOP_REASON.get("reason"),
408
+ "first_label": STOP_REASON.get("label"), "jobs_file": str(jobs_path),
409
+ "valid_outputs": valid_count, "total_jobs": len(jobs),
410
+ "pending": [row["label"] for row in pending],
411
+ })
412
+ except Exception:
413
+ pass
414
+ write_json(status_path, {
415
+ "mode": "run", "metrics": metrics_status, "metrics_ledger": str(metrics_file), "jobs": results,
416
+ "summary": {"pass": len(results) - len(failed) - len(blocked), "fail": len(failed), "blocked": len(blocked), "stage_valid": valid_count, "stage_total": len(jobs)},
417
+ })
418
+ say(f"SUMMARY pass={len(results) - len(failed) - len(blocked)} fail={len(failed)} blocked={len(blocked)}; stage {valid_count}/{len(jobs)} outputs valid")
419
+
420
+ if not args.no_freeze_check:
421
+ try:
422
+ drift = check_freeze(repo, out, "after run")
423
+ except RuntimeError as error:
424
+ sys.stderr.write(f"{error}\n")
425
+ return 1
426
+ if drift:
427
+ sys.stderr.write(drift[0] + "\n")
428
+ return 3
429
+ if blocked:
430
+ say(f"resume: rerun this command after the provider block clears — see {out / 'run-blocker.json'}")
431
+ return 2
432
+ return 1 if failed or pending else 0
433
+
434
+
435
+ if __name__ == "__main__":
436
+ sys.exit(main())