@khoavu882/doflow 2.0.0-beta.5 → 2.0.0

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 (471) hide show
  1. package/README.md +12 -11
  2. package/bin/doflow.js +21 -11
  3. package/core/.antigravity-plugin/plugin.json +1 -2
  4. package/core/.claude-plugin/plugin.json +1 -1
  5. package/core/.codex-plugin/plugin.json +1 -1
  6. package/core/.plugin/plugin.json +1 -1
  7. package/core/harnesses/claude/hooks/lib.sh +43 -24
  8. package/core/harnesses/claude/hooks/permission-audit.sh +3 -0
  9. package/core/harnesses/claude/hooks/post-compact.sh +35 -32
  10. package/core/harnesses/claude/hooks/post-edit-lint.sh +2 -0
  11. package/core/harnesses/claude/hooks/post-shell-observe.sh +48 -0
  12. package/core/harnesses/claude/hooks/post-tool-failure.sh +3 -0
  13. package/core/harnesses/claude/hooks/pre-compact.sh +23 -27
  14. package/core/harnesses/claude/hooks/session-end.sh +23 -10
  15. package/core/harnesses/claude/hooks/skill-config-audit.sh +3 -0
  16. package/core/harnesses/claude/settings/settings.json +18 -0
  17. package/core/harnesses/claude/statusline/doflow-statusline.sh +1 -0
  18. package/core/harnesses/codex/agents/core-implementer.toml +1 -0
  19. package/core/harnesses/codex/agents/delivery-lead.toml +10 -0
  20. package/core/harnesses/codex/agents/quality-guardian.toml +1 -1
  21. package/core/harnesses/codex/agents/research-writer.toml +1 -1
  22. package/core/harnesses/codex/agents/reviewer.toml +10 -0
  23. package/core/harnesses/codex/agents/spec-analyst.toml +1 -1
  24. package/core/harnesses/codex/agents/system-architect.toml +1 -1
  25. package/core/harnesses/codex/hooks/lib.sh +44 -25
  26. package/core/harnesses/codex/hooks/post-edit-lint.impl.sh +3 -1
  27. package/core/harnesses/codex/hooks/pre-compact.impl.sh +29 -26
  28. package/core/harnesses/codex/hooks/session-end.impl.sh +23 -10
  29. package/core/harnesses/codex/hooks/subagent-audit.sh +1 -0
  30. package/core/harnesses/copilot/hooks/doflow-lib.sh +93 -0
  31. package/core/harnesses/copilot/hooks/doflow-mcp-tool-guard.sh +9 -0
  32. package/core/harnesses/copilot/hooks/doflow-pre-bash-guard.sh +9 -0
  33. package/core/harnesses/copilot/hooks/doflow-pre-implement-gate.sh +9 -0
  34. package/core/harnesses/copilot/hooks/doflow-session-start.sh +9 -0
  35. package/core/harnesses/copilot/hooks/doflow-stop-check.sh +9 -0
  36. package/core/harnesses/copilot/hooks/doflow-subagent-start.sh +9 -0
  37. package/core/harnesses/copilot/hooks/doflow-subagent-stop.sh +9 -0
  38. package/core/harnesses/copilot/hooks/doflow.json +57 -0
  39. package/core/harnesses/kiro/agents/doflow.json +1 -1
  40. package/core/harnesses/kiro/hooks/lib.sh +26 -6
  41. package/core/harnesses/kiro/hooks/mcp-tool-guard.sh +1 -0
  42. package/core/harnesses/kiro/hooks/pre-bash-guard.sh +1 -0
  43. package/core/harnesses/kiro/hooks/pre-implement-gate.sh +1 -0
  44. package/core/harnesses/kiro/hooks/session-start.sh +1 -0
  45. package/core/harnesses/kiro/hooks/stop-check.sh +1 -0
  46. package/core/harnesses/pi/extensions/doflow-guard.js +8 -0
  47. package/core/harnesses/shared/hooks/ENVELOPE_MIGRATION.md +0 -1
  48. package/core/harnesses/shared/hooks/bridge/bridge.sh +102 -9
  49. package/core/harnesses/shared/hooks/bridge/envelope.jq +87 -18
  50. package/core/harnesses/shared/hooks/bridge/observe-model.sh +75 -0
  51. package/core/harnesses/shared/hooks/bridge/respond.sh +10 -2
  52. package/core/harnesses/shared/hooks/policies/blocked-patterns.conf +28 -14
  53. package/core/harnesses/shared/hooks/policies/capture-failure.sh +48 -15
  54. package/core/harnesses/shared/hooks/policies/compact-text.sh +46 -0
  55. package/core/harnesses/shared/hooks/policies/contract.sh +3 -0
  56. package/core/harnesses/shared/hooks/policies/deny-json.sh +2 -11
  57. package/core/harnesses/shared/hooks/policies/emission-rules.sh +37 -0
  58. package/core/harnesses/shared/hooks/policies/emission.jq +209 -0
  59. package/core/harnesses/shared/hooks/policies/emission.sh +383 -0
  60. package/core/harnesses/shared/hooks/policies/identity.sh +177 -0
  61. package/core/harnesses/shared/hooks/policies/lib.sh +63 -40
  62. package/core/harnesses/shared/hooks/policies/mcp-tool-guard.sh +3 -2
  63. package/core/harnesses/shared/hooks/policies/pre-bash-guard.sh +213 -12
  64. package/core/harnesses/shared/hooks/policies/pre-implementation-gate.sh +114 -38
  65. package/core/harnesses/shared/hooks/policies/session-context.sh +30 -1
  66. package/core/harnesses/shared/hooks/policies/stop-check.sh +20 -9
  67. package/core/harnesses/shared/hooks/policies/subagent-audit.sh +5 -2
  68. package/core/harnesses/shared/hooks/policies/summary-store.sh +347 -0
  69. package/core/harnesses/shared/hooks/policies/user-prompt-submit.sh +60 -155
  70. package/core/harnesses/shared/hooks/stream-hook-runner.js +47 -8
  71. package/core/harnesses/shared/locator/doflow-run +2 -3
  72. package/core/registry/advice.json +3 -3
  73. package/core/registry/assets.json +55 -82
  74. package/core/registry/capabilities.json +12 -0
  75. package/core/registry/contracts.json +58 -31
  76. package/core/registry/contribution-points.json +32 -0
  77. package/core/registry/gates.json +32 -0
  78. package/core/registry/guidance-caps.json +40 -0
  79. package/core/registry/harnesses.json +1442 -363
  80. package/core/registry/lifecycle.json +485 -304
  81. package/core/registry/mcp.json +5 -1
  82. package/core/registry/models.json +7 -1
  83. package/core/registry/package-templates/README.md +19 -0
  84. package/core/registry/package-templates/agent/files/agent.md +9 -0
  85. package/core/registry/package-templates/agent/template.json +4 -0
  86. package/core/registry/package-templates/capability-provider/files/provider.json +9 -0
  87. package/core/registry/package-templates/capability-provider/template.json +11 -0
  88. package/core/registry/package-templates/debate-preset/files/preset.json +26 -0
  89. package/core/registry/package-templates/debate-preset/template.json +4 -0
  90. package/core/registry/package-templates/review-preset/files/preset.json +30 -0
  91. package/core/registry/package-templates/review-preset/template.json +4 -0
  92. package/core/registry/package-templates/skill/files/SKILL.md +8 -0
  93. package/core/registry/package-templates/skill/template.json +4 -0
  94. package/core/registry/package-templates/worker/files/worker.json +92 -0
  95. package/core/registry/package-templates/worker/template.json +11 -0
  96. package/core/registry/packages/doflow/doflow-package.json +15 -0
  97. package/core/registry/packages/doflow/presets/debate-3.json +26 -0
  98. package/core/registry/protocols.json +61 -0
  99. package/core/registry/routes.json +17 -0
  100. package/core/registry/rules.json +431 -0
  101. package/core/registry/settings.json +276 -0
  102. package/core/registry/storage.json +679 -31
  103. package/core/registry/telemetry.json +236 -0
  104. package/core/registry/transcripts.json +142 -0
  105. package/core/registry/verification.json +76 -1
  106. package/core/registry/workflows.json +55 -0
  107. package/core/shared/agent-specs/core-implementer.md +3 -0
  108. package/core/shared/agent-specs/delivery-lead.md +68 -0
  109. package/core/shared/agent-specs/quality-guardian.md +5 -1
  110. package/core/shared/agent-specs/research-writer.md +10 -3
  111. package/core/shared/agent-specs/reviewer.md +53 -0
  112. package/core/shared/agent-specs/spec-analyst.md +5 -6
  113. package/core/shared/agent-specs/system-architect.md +5 -1
  114. package/core/shared/guidance/DOFLOW_CORE.md +1 -5
  115. package/core/shared/guidance/modes/MODE_DeepResearch.md +1 -1
  116. package/core/shared/guidance/modes/MODE_Orchestration.md +4 -3
  117. package/core/shared/guidance/modes/MODE_Task_Management.md +14 -14
  118. package/core/shared/guidance/pointers/codex.md +1 -4
  119. package/core/shared/guidance/references/ARTIFACT_FORMAT.md +30 -16
  120. package/core/shared/guidance/references/ARTIFACT_VOICE.md +3 -2
  121. package/core/shared/guidance/references/CONSTITUTION_BASE.md +17 -15
  122. package/core/shared/guidance/references/DIAGRAM_GUIDE.md +1 -1
  123. package/core/shared/guidance/references/DOFLOW_CHAIN.md +17 -19
  124. package/core/shared/guidance/references/EVIDENCE_LEDGER.md +7 -8
  125. package/core/shared/guidance/references/MODEL_SELECTION.md +5 -3
  126. package/core/shared/guidance/references/ORCHESTRATION.md +1 -2
  127. package/core/shared/guidance/rules/RULES.md +45 -0
  128. package/core/shared/scripts/doflow/bash/do-args.sh +37 -0
  129. package/core/shared/scripts/doflow/bash/do-exec-paths.sh +4 -0
  130. package/core/shared/scripts/doflow/bash/do-git-state.sh +15 -4
  131. package/core/shared/scripts/doflow/bash/do-parallel-check.sh +4 -0
  132. package/core/shared/scripts/doflow/bash/do-paths.sh +40 -2
  133. package/core/shared/scripts/doflow/bash/do-prereqs.sh +2 -1
  134. package/core/shared/scripts/doflow/bash/do-review-package.sh +4 -0
  135. package/core/shared/scripts/doflow/bash/do-task-brief.sh +58 -16
  136. package/core/shared/scripts/doflow/bash/render-audit.sh +26 -2
  137. package/core/shared/scripts/doflow/bash/render-diagrams.sh +1 -0
  138. package/core/shared/scripts/doflow/bash/render-puml.sh +2 -1
  139. package/core/shared/scripts/doflow/bash/validate-artifacts.sh +48 -4
  140. package/core/shared/scripts/doflow/bin/doflow-run +149 -34
  141. package/core/shared/skills/do/SKILL.md +12 -10
  142. package/core/shared/skills/do/references/advice_calls.md +26 -0
  143. package/core/shared/skills/do/references/pm_routing.md +1 -2
  144. package/core/shared/skills/do/references/program_mode.md +31 -0
  145. package/core/shared/skills/do-brainstorm/SKILL.md +20 -74
  146. package/core/shared/skills/do-brainstorm/references/handoff_branches.md +35 -0
  147. package/core/shared/skills/do-brainstorm/references/intent_seed.md +21 -0
  148. package/core/shared/skills/do-brainstorm/references/round_log.md +17 -0
  149. package/core/shared/skills/do-code-review/SKILL.md +41 -144
  150. package/core/shared/skills/do-code-review/content-types/markdown.md +24 -2
  151. package/core/shared/skills/do-code-review/references/tools.md +90 -0
  152. package/core/shared/skills/do-code-review/scripts/doc_quality_checker.py +549 -7
  153. package/core/shared/skills/do-design/SKILL.md +18 -17
  154. package/core/shared/skills/do-diagnose/SKILL.md +14 -4
  155. package/core/shared/skills/do-document/SKILL.md +11 -1
  156. package/core/shared/skills/do-document/references/deep_research.md +1 -2
  157. package/core/shared/skills/do-document/references/embedded-research.md +18 -6
  158. package/core/shared/skills/do-document/references/implementation-flow.md +5 -6
  159. package/core/shared/skills/do-execute-plan/SKILL.md +15 -58
  160. package/core/shared/skills/do-execute-plan/references/handoff_record.md +7 -2
  161. package/core/shared/skills/do-execute-plan/references/parallel_dispatch.md +13 -0
  162. package/core/shared/skills/do-execute-plan/references/readiness_gate.md +26 -1
  163. package/core/shared/skills/do-execute-plan/references/run_position.md +13 -1
  164. package/core/shared/skills/do-execute-plan/references/scaffold.md +17 -9
  165. package/core/shared/skills/do-execute-plan/references/state_checkpoint.md +11 -0
  166. package/core/shared/skills/do-execute-plan/references/task_attempts.md +78 -24
  167. package/core/shared/skills/do-flow/SKILL.md +8 -9
  168. package/core/shared/skills/do-flow/references/task_classes.md +2 -2
  169. package/core/shared/skills/do-git/SKILL.md +4 -0
  170. package/core/shared/skills/do-implement/SKILL.md +4 -7
  171. package/core/shared/skills/do-plan/SKILL.md +26 -15
  172. package/core/shared/skills/do-test/SKILL.md +7 -4
  173. package/core/shared/templates/doflow/constitution-template.md +9 -3
  174. package/core/shared/templates/doflow/data-model-template.md +1 -1
  175. package/core/shared/templates/doflow/design-template.md +113 -16
  176. package/core/shared/templates/doflow/plan-template.md +11 -10
  177. package/core/shared/templates/doflow/question-log-template.md +14 -2
  178. package/core/shared/templates/doflow/requirement-template.md +1 -1
  179. package/core/worker-evidence/antigravity/checks.json +14 -0
  180. package/core/worker-evidence/codex/2026-10-18-cli-check.json +107 -0
  181. package/core/worker-evidence/codex/2026-10-18-live-run-edit.json +73 -0
  182. package/core/worker-evidence/codex/2026-10-18-live-run-read-only.json +62 -0
  183. package/core/worker-evidence/codex/2026-10-24-cli-check.json +126 -0
  184. package/core/worker-evidence/codex/checks.json +38 -0
  185. package/package.json +10 -6
  186. package/src/adapters/antigravity/index.js +143 -66
  187. package/src/adapters/claude/index.js +24 -4
  188. package/src/adapters/codex/agents.js +25 -15
  189. package/src/adapters/codex/index.js +48 -13
  190. package/src/adapters/codex/native-projection.js +6 -2
  191. package/src/adapters/copilot/index.js +58 -23
  192. package/src/adapters/copy-tree.js +290 -44
  193. package/src/adapters/guidance-manifest.js +71 -0
  194. package/src/adapters/guidance-pointer.js +19 -0
  195. package/src/adapters/hook-commands.js +6 -10
  196. package/src/adapters/index.js +1 -0
  197. package/src/adapters/kiro/index.js +23 -12
  198. package/src/adapters/opencode/index.js +8 -8
  199. package/src/adapters/pi/hooks.js +2 -1
  200. package/src/adapters/pi/index.js +14 -7
  201. package/src/adapters/resolve-source.js +26 -2
  202. package/src/cli/commands/install.js +2 -1
  203. package/src/cli/commands/reconcile.js +53 -9
  204. package/src/cli/commands/remove.js +2 -2
  205. package/src/cli/commands/rollback.js +6 -3
  206. package/src/cli/commands/self-update.js +18 -9
  207. package/src/cli/commands/status.js +21 -2
  208. package/src/cli/commands/ui.js +99 -0
  209. package/src/cli/commands/update.js +2 -1
  210. package/src/cli/index.js +322 -29
  211. package/src/cli/runtime-commands.js +209 -57
  212. package/src/cli/shared.js +0 -2
  213. package/src/helper/git.js +1 -1
  214. package/src/helper/guardrails.js +1 -1
  215. package/src/helper/marker-merge.js +31 -13
  216. package/src/helper/package-ids.js +37 -0
  217. package/src/helper/semver.js +94 -0
  218. package/src/install/backup.js +105 -15
  219. package/src/install/context.js +1 -1
  220. package/src/install/targets.js +31 -16
  221. package/src/lifecycle/coverage.js +20 -3
  222. package/src/lifecycle/hook-tier.js +160 -0
  223. package/src/lifecycle/index.js +412 -78
  224. package/src/lifecycle/retired-harnesses.js +318 -0
  225. package/src/lifecycle/view.js +35 -15
  226. package/src/registry/index.js +551 -26
  227. package/src/registry/worker-ids.js +45 -0
  228. package/src/release/build-package.js +0 -3
  229. package/src/release/docs-blocks.js +492 -0
  230. package/src/release/preflight.js +425 -0
  231. package/src/release/rehearse.js +401 -0
  232. package/src/release/release-workflow.js +733 -0
  233. package/src/release/sync-plugin-versions.js +43 -7
  234. package/src/runtime/advice/bench-gate.js +25 -0
  235. package/src/runtime/advice/cli.js +59 -16
  236. package/src/runtime/advice/client.js +32 -9
  237. package/src/runtime/advice/config.js +98 -133
  238. package/src/runtime/advice/doctor.js +1 -1
  239. package/src/runtime/advice/elevation.js +97 -0
  240. package/src/runtime/advice/endpoint-guard.js +9 -3
  241. package/src/runtime/advice/mode.js +37 -0
  242. package/src/runtime/advice/record.js +18 -5
  243. package/src/runtime/advice/registry.js +4 -1
  244. package/src/runtime/advice/shadow-config.js +229 -0
  245. package/src/runtime/advice/shadow-dispatcher.js +276 -0
  246. package/src/runtime/brief-rules.js +81 -0
  247. package/src/runtime/brief-tools.js +111 -34
  248. package/src/runtime/candidate.js +20 -11
  249. package/src/runtime/capabilities/executor.js +450 -0
  250. package/src/runtime/capabilities/loopback.js +41 -0
  251. package/src/runtime/capabilities/sanitizer.js +88 -0
  252. package/src/runtime/capability-router.js +135 -3
  253. package/src/runtime/checkouts.js +73 -11
  254. package/src/runtime/claims.js +13 -7
  255. package/src/runtime/cli.js +196 -19
  256. package/src/runtime/config-home.js +42 -0
  257. package/src/runtime/context/arbitrate.js +767 -0
  258. package/src/runtime/context/delivery-store.js +4 -6
  259. package/src/runtime/context/emission-rules.js +118 -0
  260. package/src/runtime/context/identity.js +227 -0
  261. package/src/runtime/context/primary-command.js +117 -0
  262. package/src/runtime/context/primary.js +344 -0
  263. package/src/runtime/context/summary-store.js +655 -0
  264. package/src/runtime/context-pack.js +154 -78
  265. package/src/runtime/decision-register.js +13 -4
  266. package/src/runtime/dispatch/admission.js +392 -0
  267. package/src/runtime/dispatch/attempt-end.js +274 -0
  268. package/src/runtime/dispatch/command.js +628 -0
  269. package/src/runtime/dispatch/controls.js +904 -0
  270. package/src/runtime/dispatch/envelope.js +276 -0
  271. package/src/runtime/dispatch/grants.js +310 -0
  272. package/src/runtime/dispatch/integrate.js +617 -0
  273. package/src/runtime/dispatch/loop.js +1835 -0
  274. package/src/runtime/dispatch/store.js +352 -0
  275. package/src/runtime/dispatch/verify-run.js +201 -0
  276. package/src/runtime/evidence-ledger.js +34 -7
  277. package/src/runtime/failure/capture.js +3 -1
  278. package/src/runtime/failure/cli.js +25 -9
  279. package/src/runtime/failure/home.js +57 -30
  280. package/src/runtime/failure/store.js +7 -2
  281. package/src/runtime/feature-pointer.js +174 -0
  282. package/src/runtime/feature-resolve.js +13 -2
  283. package/src/runtime/fleet/command.js +77 -0
  284. package/src/runtime/fleet/projects.js +178 -0
  285. package/src/runtime/fleet/tree.js +383 -0
  286. package/src/runtime/freshness.js +33 -3
  287. package/src/runtime/git-memo.js +46 -0
  288. package/src/runtime/guidance/ablation.js +427 -0
  289. package/src/runtime/health.js +565 -17
  290. package/src/runtime/history-compactor.js +13 -3
  291. package/src/runtime/implementation-gate.js +277 -22
  292. package/src/runtime/inventory/coverage.js +1 -1
  293. package/src/runtime/inventory/index.js +1 -1
  294. package/src/runtime/inventory/precedence.js +2 -2
  295. package/src/runtime/inventory/siblings.js +6 -4
  296. package/src/runtime/knowledge/code-corpus.js +277 -0
  297. package/src/runtime/knowledge/code-lexical.js +130 -0
  298. package/src/runtime/knowledge/code-retrieval.js +307 -0
  299. package/src/runtime/knowledge/code-validation.js +106 -0
  300. package/src/runtime/knowledge/gp3-gate.js +74 -0
  301. package/src/runtime/knowledge/graph-adapter.js +288 -0
  302. package/src/runtime/knowledge/graph-fallback.js +394 -0
  303. package/src/runtime/knowledge/hybrid.js +136 -8
  304. package/src/runtime/knowledge/retrieval.js +34 -1
  305. package/src/runtime/knowledge/rtk-compress.js +77 -0
  306. package/src/runtime/knowledge/semble-bridge.py +106 -0
  307. package/src/runtime/knowledge/semble-lifecycle.js +114 -0
  308. package/src/runtime/knowledge/semble-provider.js +365 -0
  309. package/src/runtime/lifecycle/cli.js +16 -6
  310. package/src/runtime/lifecycle/event-store.js +8 -6
  311. package/src/runtime/lifecycle/followup.js +6 -1
  312. package/src/runtime/lifecycle/overview.js +24 -1
  313. package/src/runtime/lifecycle/report-store.js +8 -10
  314. package/src/runtime/lifecycle/retention.js +95 -31
  315. package/src/runtime/lifecycle/root.js +9 -1
  316. package/src/runtime/lifecycle/status.js +34 -17
  317. package/src/runtime/lifecycle/store-upkeep.js +56 -7
  318. package/src/runtime/mask.js +134 -7
  319. package/src/runtime/mcp-consultations.js +40 -6
  320. package/src/runtime/model-router.js +79 -7
  321. package/src/runtime/models/catalogue.js +191 -0
  322. package/src/runtime/models/command.js +153 -0
  323. package/src/runtime/off-words.js +12 -0
  324. package/src/runtime/outcome.js +2 -2
  325. package/src/runtime/packages/command.js +1009 -0
  326. package/src/runtime/packages/contributions.js +116 -0
  327. package/src/runtime/packages/digest.js +132 -0
  328. package/src/runtime/packages/fetch.js +182 -0
  329. package/src/runtime/packages/grants.js +192 -0
  330. package/src/runtime/packages/index-file.js +134 -0
  331. package/src/runtime/packages/instruction.js +90 -0
  332. package/src/runtime/packages/manifest.js +331 -0
  333. package/src/runtime/packages/points.js +477 -0
  334. package/src/runtime/packages/programs.js +216 -0
  335. package/src/runtime/packages/scaffold.js +277 -0
  336. package/src/runtime/packages/source.js +90 -0
  337. package/src/runtime/packages/state.js +164 -0
  338. package/src/runtime/packages/store.js +449 -0
  339. package/src/runtime/packages/tar.js +106 -0
  340. package/src/runtime/packages/view.js +406 -0
  341. package/src/runtime/plan-check.js +59 -0
  342. package/src/runtime/plan-content.js +324 -0
  343. package/src/runtime/policy-init.js +118 -0
  344. package/src/runtime/profiles/doctor.js +96 -0
  345. package/src/runtime/profiles/resolve.js +244 -0
  346. package/src/runtime/profiles/rollback.js +254 -0
  347. package/src/runtime/profiles/sampler.js +41 -0
  348. package/src/runtime/readiness-record.js +16 -11
  349. package/src/runtime/readiness.js +46 -5
  350. package/src/runtime/research-request.js +50 -25
  351. package/src/runtime/retrieval-plan.js +10 -30
  352. package/src/runtime/retrieval-record.js +75 -5
  353. package/src/runtime/review/command.js +246 -0
  354. package/src/runtime/review/dominance.js +68 -0
  355. package/src/runtime/review/findings.js +434 -0
  356. package/src/runtime/review/identity.js +255 -0
  357. package/src/runtime/review/protocol-command.js +109 -0
  358. package/src/runtime/review/protocols.js +405 -0
  359. package/src/runtime/review/resolve-protocol.js +302 -0
  360. package/src/runtime/review/start.js +530 -0
  361. package/src/runtime/roadmap/facts.js +479 -0
  362. package/src/runtime/roadmap/gates.js +270 -0
  363. package/src/runtime/roadmap/loop.js +1636 -0
  364. package/src/runtime/roadmap/merge.js +381 -0
  365. package/src/runtime/roadmap/rows.js +1113 -0
  366. package/src/runtime/roadmap/seed.js +951 -0
  367. package/src/runtime/rules-report.js +336 -0
  368. package/src/runtime/scaffold/artifacts.js +60 -9
  369. package/src/runtime/scaffold/generate.js +21 -9
  370. package/src/runtime/settings/autopilot.js +106 -0
  371. package/src/runtime/settings/cli.js +280 -0
  372. package/src/runtime/settings/config-write.js +314 -0
  373. package/src/runtime/settings/files.js +271 -0
  374. package/src/runtime/settings/legacy.js +154 -0
  375. package/src/runtime/settings/lexical.js +56 -0
  376. package/src/runtime/settings/prepared-run.js +269 -0
  377. package/src/runtime/settings/project-layers.js +118 -0
  378. package/src/runtime/settings/refs.js +288 -0
  379. package/src/runtime/settings/registry.js +553 -0
  380. package/src/runtime/settings/resolve.js +713 -0
  381. package/src/runtime/settings/shared.js +37 -0
  382. package/src/runtime/settings/worker-billing.js +107 -0
  383. package/src/runtime/settings/write.js +269 -0
  384. package/src/runtime/slot-command.js +78 -0
  385. package/src/runtime/slots.js +671 -0
  386. package/src/runtime/source-version.js +1 -0
  387. package/src/runtime/state-root.js +217 -0
  388. package/src/runtime/storage/cli.js +12 -5
  389. package/src/runtime/storage/engine.js +9 -2
  390. package/src/runtime/storage/registry.js +14 -1
  391. package/src/runtime/storage/roots.js +66 -16
  392. package/src/runtime/storage/rules.js +82 -10
  393. package/src/runtime/storage/upkeep.js +10 -2
  394. package/src/runtime/storage/work-status.js +245 -26
  395. package/src/runtime/task-attempts.js +589 -69
  396. package/src/runtime/task-classifier.js +47 -11
  397. package/src/runtime/task-graph.js +373 -0
  398. package/src/runtime/task-run.js +778 -0
  399. package/src/runtime/task-scope.js +176 -55
  400. package/src/runtime/task-sources.js +147 -0
  401. package/src/runtime/task-state.js +374 -64
  402. package/src/runtime/team/admission.js +301 -0
  403. package/src/runtime/team/envelope.js +324 -0
  404. package/src/runtime/team/review-guard.js +156 -0
  405. package/src/runtime/trace/analyze.js +427 -0
  406. package/src/runtime/trace/baseline.js +770 -0
  407. package/src/runtime/trace/ledger.js +63 -140
  408. package/src/runtime/trace/otlp-export.js +568 -0
  409. package/src/runtime/trace/otlp-map.js +481 -0
  410. package/src/runtime/trace/spans.js +745 -0
  411. package/src/runtime/trace/telemetry.js +225 -0
  412. package/src/runtime/trace/views.js +1 -0
  413. package/src/runtime/transcripts/command.js +98 -0
  414. package/src/runtime/transcripts/cursor.js +326 -0
  415. package/src/runtime/transcripts/read.js +126 -0
  416. package/src/runtime/transcripts/readers/claude-jsonl.js +74 -0
  417. package/src/runtime/transcripts/readers/codex-rollout.js +70 -0
  418. package/src/runtime/transcripts/readers/copilot-events.js +90 -0
  419. package/src/runtime/transcripts/readers/kiro-jsonl.js +77 -0
  420. package/src/runtime/transcripts/readers/pi-jsonl.js +59 -0
  421. package/src/runtime/transcripts/registry.js +161 -0
  422. package/src/runtime/transcripts/stats.js +161 -0
  423. package/src/runtime/verification/check-key.js +628 -0
  424. package/src/runtime/verification/contract-runner.js +546 -11
  425. package/src/runtime/verification/engine.js +499 -9
  426. package/src/runtime/verification/impact.js +617 -0
  427. package/src/runtime/verification/inflight.js +205 -0
  428. package/src/runtime/verification/registry.js +176 -0
  429. package/src/runtime/verification/reuse.js +184 -0
  430. package/src/runtime/verification/telemetry-emit.js +254 -0
  431. package/src/runtime/verification/test-selection.js +67 -7
  432. package/src/runtime/worker/active.js +90 -0
  433. package/src/runtime/worker/cancel.js +222 -0
  434. package/src/runtime/worker/catalogue.js +122 -0
  435. package/src/runtime/worker/command.js +677 -0
  436. package/src/runtime/worker/evidence.js +288 -0
  437. package/src/runtime/worker/launch.js +288 -0
  438. package/src/runtime/worker/models.js +137 -0
  439. package/src/runtime/worker/parsers.js +218 -0
  440. package/src/runtime/worker/probes/index.js +706 -0
  441. package/src/runtime/worker/process-tree.js +415 -0
  442. package/src/runtime/worker/quota.js +43 -0
  443. package/src/runtime/worker/readiness.js +713 -0
  444. package/src/runtime/worker/run-store.js +166 -0
  445. package/src/runtime/worker/supervisor.js +541 -0
  446. package/src/runtime/worker/worktree-check.js +252 -0
  447. package/src/runtime/workflow-engine.js +5 -1
  448. package/src/runtime/workflow-orchestrator.js +578 -63
  449. package/src/runtime/worktree.js +149 -22
  450. package/src/state/index.js +1 -1
  451. package/src/state/lockfile.js +64 -10
  452. package/src/state/run-lock.js +1 -1
  453. package/types/node-commonjs.d.ts +106 -7
  454. package/core/harnesses/gemini/hooks/hooks.json +0 -77
  455. package/core/harnesses/gemini/hooks/lib.sh +0 -302
  456. package/core/harnesses/gemini/hooks/mcp-tool-guard.sh +0 -10
  457. package/core/harnesses/gemini/hooks/post-edit-lint.sh +0 -39
  458. package/core/harnesses/gemini/hooks/pre-bash-guard.sh +0 -10
  459. package/core/harnesses/gemini/hooks/pre-compact.sh +0 -38
  460. package/core/harnesses/gemini/hooks/pre-implement-gate.sh +0 -10
  461. package/core/harnesses/gemini/hooks/session-end.impl.sh +0 -89
  462. package/core/harnesses/gemini/hooks/session-end.sh +0 -5
  463. package/core/harnesses/gemini/hooks/session-start.sh +0 -7
  464. package/core/harnesses/gemini/settings/adapter-defaults.json +0 -4
  465. package/core/shared/guidance/FLAGS.md +0 -25
  466. package/core/shared/guidance/rules/RULE_01_SAFETY.md +0 -25
  467. package/core/shared/guidance/rules/RULE_02_WORKFLOW.md +0 -23
  468. package/core/shared/guidance/rules/RULE_03_QUALITY.md +0 -14
  469. package/core/shared/guidance/rules/RULE_04_QUESTIONS.md +0 -46
  470. package/src/adapters/gemini/hooks.js +0 -251
  471. package/src/adapters/gemini/index.js +0 -429
@@ -6,6 +6,7 @@
6
6
  const fs = require('node:fs');
7
7
  const path = require('node:path');
8
8
  const { validatePathsSection } = require('../helper/harness-paths.js');
9
+ const { BUILTIN_WORKER_ID_RE, RESERVED_WORKER_IDS } = require('./worker-ids.js');
9
10
 
10
11
  const REGISTRY_FILES = Object.freeze({
11
12
  harnesses: 'harnesses.json',
@@ -17,10 +18,16 @@ const REGISTRY_FILES = Object.freeze({
17
18
  models: 'models.json',
18
19
  });
19
20
  const CAPABILITY_STATUS = new Set(['supported', 'different', 'unavailable']);
21
+ // Kept local to avoid a registry -> adapter import cycle; registry.test.js ties this list to SELECTORS.
22
+ const COPY_TREE_SELECTS = new Set(['guidance-manifest']);
23
+ // Renderers that materialize through the copy-tree engine and so may carry a `select`: the generic one and
24
+ // Copilot's rule renderer, which differs only by its transform and layout.
25
+ const COPY_TREE_SELECT_RENDERERS = new Set(['copy-tree', 'copilot-rule-instructions']);
20
26
  /** The orchestration block's own status vocabulary (074 IC-026): whether a harness exposes a
21
27
  * primitive, kept apart from CAPABILITY_STATUS because "unknown" is a recorded state here, not a gap. */
22
28
  const ORCHESTRATION_STATUS = new Set(['supported', 'unsupported', 'unknown']);
23
- const ORCHESTRATION_OPS = ['spawn', 'wait', 'status', 'cancel', 'resume'];
29
+ // `nest` (118 IC-004): a subagent can start a subagent.
30
+ const ORCHESTRATION_OPS = ['spawn', 'wait', 'status', 'cancel', 'resume', 'nest'];
24
31
  const ORCHESTRATION_FALLBACK = new Set(['sequential', 'manual']);
25
32
  // 'core' = must be supported/different (never unavailable) on every currently-integrated harness;
26
33
  // 'enhanced' = best-effort, wired wherever a harness's native surface allows. Optional field —
@@ -37,6 +44,7 @@ const EXTERNAL_TOOL_ACTIONS = new Set(['install', 'update', 'uninstall']);
37
44
  const MODEL_KINDS = new Set(['hosted', 'local']);
38
45
  const MODEL_SLOT_IDS = new Set(['dense', 'rerank']);
39
46
  const MCP_STEP_KEYS = new Set(['skill', 'file', 'expectedWhen']);
47
+ const MCP_EXPECTED_WHENS = Object.freeze(new Set(['research-request', 'dependency-change', 'manifest-review']));
40
48
  const MCP_NAME = /^[A-Za-z0-9_.-]+$/;
41
49
  /** How a capability provider answers: from the current files or commands (`live`), or from a built
42
50
  * artifact that can lag the source (`index`). Required on every provider (068). */
@@ -65,7 +73,17 @@ function parseRegistryFile(file , fsImpl = fs) {
65
73
 
66
74
  function registryDir(repoRoot ) { return path.join(path.resolve(repoRoot), 'core', 'registry'); }
67
75
 
68
- function loadRegistry({ repoRoot, dir, fsImpl = fs } = {}) {
76
+
77
+
78
+ function registrySourceMode(repoRoot , fsImpl = fs) {
79
+ if (typeof repoRoot !== 'string' || !repoRoot.trim()) return 'skip';
80
+ return fsImpl.existsSync(path.join(path.resolve(repoRoot), 'package.json')) ? 'require' : 'skip';
81
+ }
82
+
83
+ function loadRegistry({ repoRoot, dir, fsImpl = fs, sources } = {}) {
84
+ if (sources !== undefined && sources !== 'require' && sources !== 'skip') {
85
+ throw new TypeError(`sources must be 'require' or 'skip', got ${JSON.stringify(sources)}`);
86
+ }
69
87
  if (!repoRoot && !dir) throw new Error('loadRegistry requires repoRoot or dir');
70
88
  const base = dir ? path.resolve(dir) : registryDir(repoRoot );
71
89
  const loaded = {};
@@ -80,6 +98,8 @@ function loadRegistry({ repoRoot, dir, fsImpl = fs }
80
98
  directory: base,
81
99
  repoRoot: repoRoot ? path.resolve(repoRoot) : path.resolve(base, '..', '..'),
82
100
  harnesses: loaded.harnesses.harnesses,
101
+ // Optional in the file; an absent list is an empty one. Anything present is validated as written.
102
+ retired: loaded.harnesses.retired === undefined ? [] : loaded.harnesses.retired,
83
103
  assets: loaded.assets.assets,
84
104
  mcp: loaded.mcp.servers,
85
105
  lifecycle: loaded.lifecycle.policies,
@@ -88,12 +108,14 @@ function loadRegistry({ repoRoot, dir, fsImpl = fs }
88
108
  externalTools: loaded.externalTools.tools,
89
109
  modelProviders: loaded.models.providers,
90
110
  modelRoles: loaded.models.roles,
91
- retrievalSlots: Array.isArray(loaded.models.slots) ? loaded.models.slots : [],
111
+ // Absent is empty; anything else is passed on as written so validation can name a non-array.
112
+ retrievalSlots: loaded.models.slots === undefined ? [] : loaded.models.slots,
92
113
  dispatchTiers: loaded.models.dispatchTiers,
93
114
  capabilities,
94
115
  versions: Object.fromEntries((Object.entries(loaded) ).map(([name, value]) => [name, value.version])),
95
116
  };
96
- const validation = validateRegistry(registry, { repoRoot: registry.repoRoot, fsImpl });
117
+ const sourceMode = sources ?? registrySourceMode(registry.repoRoot, fsImpl);
118
+ const validation = validateRegistry(registry, { repoRoot: registry.repoRoot, fsImpl, sources: sourceMode });
97
119
  if (!validation.ok) {
98
120
  const error = new Error(`Invalid DoFlow registry:\n- ${validation.errors.join('\n- ')}`);
99
121
  error.validation = validation;
@@ -118,11 +140,23 @@ function validateContract(value , location , errors , harnes
118
140
  if (!CONTRACT_COMPLETENESS.has(value.completeness)) {
119
141
  issue(errors, location, 'completeness must be verified or lower-bound');
120
142
  }
121
- // skillFields/agentFields are optional: shared skills and agent specs are byte-identical across
122
- // harnesses, so a per-harness field list is necessarily a subset of the union and cannot change
123
- // any guard outcome. Declaring one anyway would restate an unverified claim as fact — the exact
124
- // shape of inert declaration these guards exist to remove. hookEvents is required because those
125
- // genuinely differ per harness and G5 reads them.
143
+ // agentFields is optional: shared agent specs are byte-identical across harnesses, so a per-harness
144
+ // field list is necessarily a subset of the union and cannot change any guard outcome. Declaring one
145
+ // anyway would restate an unverified claim as fact — the exact shape of inert declaration these
146
+ // guards exist to remove. skillFields is per-harness since row 131: a projected SKILL.md keeps only
147
+ // the spec keys and its harness's skillFields at the top level, so a list needs its vendor page and
148
+ // the date it was read. hookEvents is required because those genuinely differ per harness and G5 reads them.
149
+ if (value.skillFields !== undefined) {
150
+ if (typeof value.skillFieldsSource !== 'string' || !/^https:\/\//.test(value.skillFieldsSource)) {
151
+ issue(errors, `${location}.skillFieldsSource`, 'must be the HTTPS vendor page skillFields was read from');
152
+ }
153
+ if (typeof value.skillFieldsVerifiedOn !== 'string' || !/^\d{4}-\d{2}-\d{2}$/.test(value.skillFieldsVerifiedOn)) {
154
+ issue(errors, `${location}.skillFieldsVerifiedOn`, 'must be the YYYY-MM-DD date skillFields was read');
155
+ }
156
+ if (!['ignored', 'warned', 'rejected', 'not-documented'].includes(value.unknownSkillFields)) {
157
+ issue(errors, `${location}.unknownSkillFields`, 'must be ignored, warned, rejected or not-documented');
158
+ }
159
+ }
126
160
  for (const field of ['skillFields', 'agentFields']) {
127
161
  if (value[field] === undefined) continue;
128
162
  const list = value[field];
@@ -191,22 +225,24 @@ function validateCapability(value , location , errors )
191
225
  }
192
226
  }
193
227
 
194
- function validateSource(repoRoot , source , location , errors , fsImpl , expected = 'any') {
228
+ function validateSource(repoRoot , source , location , errors , fsImpl , expected = 'any', sources = 'require') {
195
229
  if (typeof source !== 'string' || !source.trim()) { issue(errors, location, 'requires a repository-relative source'); return; }
196
230
  const root = path.resolve(repoRoot);
197
231
  const resolved = path.resolve(root, source);
198
- if (!resolved.startsWith(`${root}${path.sep}`) || !fsImpl.existsSync(resolved)) { issue(errors, location, `source does not exist inside repository: ${source}`); return; }
232
+ if (!resolved.startsWith(`${root}${path.sep}`)) { issue(errors, location, `source does not exist inside repository: ${source}`); return; }
233
+ if (sources === 'skip') return;
234
+ if (!fsImpl.existsSync(resolved)) { issue(errors, location, `source does not exist inside repository: ${source}`); return; }
199
235
  if (expected === 'file' && !fsImpl.statSync(resolved).isFile()) issue(errors, location, `source must be a file: ${source}`);
200
236
  if (expected === 'directory' && !fsImpl.statSync(resolved).isDirectory()) issue(errors, location, `source must be a directory: ${source}`);
201
237
  }
202
238
 
203
- function validateNativeProjection(projection , harness , { repoRoot, fsImpl } , errors ) {
239
+ function validateNativeProjection(projection , harness , { repoRoot, fsImpl, sources } , errors ) {
204
240
  const at = `harness '${harness.id}' nativeProjection`;
205
241
  if (!object(projection)) { issue(errors, at, 'must be an object'); return; }
206
242
  const config = projection.config;
207
243
  if (!object(config)) issue(errors, at, 'requires config declaration');
208
244
  else {
209
- validateSource(repoRoot, config.source, `${at} config`, errors, fsImpl, 'file');
245
+ validateSource(repoRoot, config.source, `${at} config`, errors, fsImpl, 'file', sources);
210
246
  if (!Array.isArray(config.resources) || !config.resources.length) issue(errors, `${at} config`, 'requires non-empty resources');
211
247
  for (const resource of config.resources || []) {
212
248
  if (!object(resource) || resource.kind !== 'configuration-entry' || typeof resource.identity !== 'string' || !/^[A-Za-z0-9_-]+(?:\.[A-Za-z0-9_-]+)+$/.test(resource.identity) || !Object.hasOwn(resource, 'value')) {
@@ -218,12 +254,12 @@ function validateNativeProjection(projection , harness , { repoRoot, fsI
218
254
  if (!object(mcp) || mcp.catalog !== 'registry.mcp' || !['optional', 'required'].includes(mcp.selection)) issue(errors, `${at} mcp`, 'requires catalog registry.mcp and optional/required selection');
219
255
  const agents = projection.agents;
220
256
  if (!object(agents)) issue(errors, `${at} agents`, 'requires agents declaration');
221
- else validateSource(repoRoot, agents.source, `${at} agents`, errors, fsImpl, 'directory');
257
+ else validateSource(repoRoot, agents.source, `${at} agents`, errors, fsImpl, 'directory', sources);
222
258
  const hooks = projection.hooks;
223
259
  if (!object(hooks)) issue(errors, `${at} hooks`, 'requires hooks declaration');
224
260
  else {
225
- validateSource(repoRoot, hooks.configSource, `${at} hooks`, errors, fsImpl, 'file');
226
- validateSource(repoRoot, hooks.scriptsSource, `${at} hooks`, errors, fsImpl, 'directory');
261
+ validateSource(repoRoot, hooks.configSource, `${at} hooks`, errors, fsImpl, 'file', sources);
262
+ validateSource(repoRoot, hooks.scriptsSource, `${at} hooks`, errors, fsImpl, 'directory', sources);
227
263
  if (typeof hooks.trusted !== 'boolean') issue(errors, `${at} hooks`, 'trusted must be boolean');
228
264
  }
229
265
  }
@@ -282,6 +318,12 @@ function validateModelProvider(value , location , errors )
282
318
  if (typeof value.id !== 'string' || !/^[a-z][a-z0-9-]*$/.test(value.id)) issue(errors, location, 'id must be a lowercase identifier');
283
319
  if (typeof value.displayName !== 'string' || !value.displayName.trim()) issue(errors, location, 'requires displayName');
284
320
  if (!MODEL_KINDS.has(value.kind)) issue(errors, location, `kind must be one of: ${[...MODEL_KINDS].join(', ')}`);
321
+ if (!Array.isArray(value.families) || value.families.length === 0
322
+ || value.families.some((family ) => typeof family !== 'string' || !/^[a-z][a-z0-9-]*$/.test(family))) {
323
+ issue(errors, location, 'families must be a non-empty array of lowercase identifiers');
324
+ } else if (new Set(value.families).size !== value.families.length) {
325
+ issue(errors, location, 'families must not contain duplicate identifiers');
326
+ }
285
327
  if (!Array.isArray(value.evidence) || value.evidence.length === 0
286
328
  || value.evidence.some((url ) => typeof url !== 'string' || !/^https:\/\//.test(url))) {
287
329
  issue(errors, location, 'must include one or more HTTPS evidence URLs');
@@ -337,8 +379,10 @@ function validateMcpStageUse(server , at , errors ) {
337
379
  if (typeof step.file !== 'string' || !step.file.trim() || path.isAbsolute(step.file) || step.file.split(/[\\/]/).includes('..')) {
338
380
  issue(errors, at, `${where}.file must be a path relative to the skill directory`);
339
381
  }
340
- if (step.expectedWhen !== undefined && step.expectedWhen !== 'research-request') issue(errors, at, `${where}.expectedWhen must be 'research-request' when present`);
341
- const key = `${step.skill}\u0000${step.file}`;
382
+ if (step.expectedWhen !== undefined && !MCP_EXPECTED_WHENS.has(step.expectedWhen)) {
383
+ issue(errors, at, `${where}.expectedWhen must be one of: ${[...MCP_EXPECTED_WHENS].join(', ')} when present`);
384
+ }
385
+ const key = `${step.skill}\u0000${step.file}\u0000${step.expectedWhen ?? ''}`;
342
386
  if (seen.has(key)) issue(errors, at, `${where} duplicates skill '${step.skill}' and file '${step.file}'`);
343
387
  seen.add(key);
344
388
  });
@@ -412,6 +456,295 @@ function validateOrchestration(value , location , errors )
412
456
  }
413
457
  }
414
458
 
459
+ /** The `agentLimits.readOnly` status vocabulary (118 IC-003): `native` = the harness enforces the
460
+ * limit from keys DoFlow writes into the agent file; `unenforced` = it does not, and says why. */
461
+ const AGENT_LIMIT_STATUS = new Set(['native', 'unenforced']);
462
+ const AGENT_LIMIT_KEYS = ['readOnly', 'toolScope'];
463
+ /** The abstract tool categories an agent spec's `tool-scope` names (row 135); `toolScope.map` gives
464
+ * each built-in category's tools on a harness, and `mcpSelector` (a template holding `{server}`)
465
+ * writes the `mcp` category, one selector per MCP server DoFlow's registry names. `form` is how the
466
+ * adapter writes them: `comma` a comma-separated string, `list` an array, `closed` a permission
467
+ * object that denies `*` first and then allows only the declared tools. */
468
+ const TOOL_SCOPE_CATEGORIES = ['read', 'edit', 'shell', 'web', 'agents', 'mcp'];
469
+ const TOOL_SCOPE_MAPPED = TOOL_SCOPE_CATEGORIES.filter((category) => category !== 'mcp');
470
+ const TOOL_SCOPE_FORMS = new Set(['comma', 'list', 'closed']);
471
+ const TOOL_SCOPE_ENTRY_KEYS = new Set(['status', 'field', 'form', 'map', 'mcpSelector', 'evidence', 'checked', 'verifiedOn', 'reason']);
472
+ const AGENT_LIMIT_ENTRY_KEYS = new Set(['status', 'apply', 'evidence', 'checked', 'verifiedOn', 'reason']);
473
+
474
+ /** True when `value` is a non-blank string that parses as an http: or https: URL. */
475
+ function isHttpUrl(value ) {
476
+ if (!value.trim()) return false;
477
+ try { return ['http:', 'https:'].includes(new URL(value).protocol); } catch { return false; }
478
+ }
479
+
480
+ /** Validates one harness's `agentLimits` block (118 IC-003): how a `read-only` agent spec is limited
481
+ * on that harness. Declared, never inferred; the adapters read `apply` and nothing else. */
482
+ /** One row per harness for doctor and capabilities (135): how it writes an agent's tool scope, or why
483
+ * it writes none. An unenforced harness projects no tool field, so its agents keep every tool. */
484
+ function agentToolScopeView(registry ) {
485
+ return registry.harnesses.map((h ) => {
486
+ const entry = h.agentLimits?.toolScope || {};
487
+ return entry.status === 'native'
488
+ ? { harness: h.id, status: 'native', field: entry.field }
489
+ : { harness: h.id, status: entry.status || 'unenforced', reason: entry.reason || 'not declared' };
490
+ });
491
+ }
492
+
493
+ function validateAgentLimits(value , location , errors ) {
494
+ if (!object(value)) { issue(errors, location, 'agentLimits block is required and must be an object'); return; }
495
+ for (const key of AGENT_LIMIT_KEYS) {
496
+ if (!Object.hasOwn(value, key)) issue(errors, location, `agentLimits is missing '${key}'`);
497
+ }
498
+ for (const key of Object.keys(value)) {
499
+ if (!AGENT_LIMIT_KEYS.includes(key)) issue(errors, location, `agentLimits has unknown key '${key}'`);
500
+ }
501
+ if (Object.hasOwn(value, 'toolScope')) validateToolScope(value.toolScope, `${location} agentLimits toolScope`, errors);
502
+ if (!Object.hasOwn(value, 'readOnly')) return;
503
+ const at = `${location} agentLimits readOnly`;
504
+ const entry = value.readOnly;
505
+ if (!object(entry)) { issue(errors, at, 'must be an object'); return; }
506
+ for (const key of Object.keys(entry)) {
507
+ if (!AGENT_LIMIT_ENTRY_KEYS.has(key)) issue(errors, at, `has unknown key '${key}'`);
508
+ }
509
+ if (!AGENT_LIMIT_STATUS.has(entry.status)) { issue(errors, at, 'status must be native or unenforced'); return; }
510
+ const evidenceOk = Array.isArray(entry.evidence) && entry.evidence.every((item ) => typeof item === 'string');
511
+ if (!evidenceOk) issue(errors, at, 'evidence must be an array of strings');
512
+ else for (const item of entry.evidence) if (!isHttpUrl(item)) issue(errors, at, `evidence entry ${JSON.stringify(item)} must be an http(s) URL`);
513
+ if (typeof entry.verifiedOn !== 'string' || !/^\d{4}-\d{2}-\d{2}$/.test(entry.verifiedOn)) issue(errors, at, 'requires verifiedOn as an ISO date (YYYY-MM-DD)');
514
+ if (entry.status === 'native') {
515
+ if (!object(entry.apply) || Object.keys(entry.apply).length === 0) issue(errors, at, 'native requires apply as a non-empty object');
516
+ if (evidenceOk && entry.evidence.length === 0) issue(errors, at, 'native requires non-empty evidence');
517
+ if (entry.checked !== 'docs' && entry.checked !== 'live') issue(errors, at, 'native requires checked as docs or live');
518
+ return;
519
+ }
520
+ if (entry.apply !== undefined) issue(errors, at, 'unenforced must not carry apply');
521
+ if (typeof entry.reason !== 'string' || !entry.reason.trim()) { issue(errors, at, 'unenforced requires a reason'); return; }
522
+ if (evidenceOk && entry.evidence.length === 0 && entry.reason.trim().toLowerCase() !== 'no agent surface') {
523
+ issue(errors, at, 'unenforced requires non-empty evidence unless the reason is "no agent surface"');
524
+ }
525
+ }
526
+
527
+ /** Validates one harness's `agentLimits.toolScope` (row 135): how an agent spec's `tool-scope` is
528
+ * written on that harness, or why it is not. A `native` map names every category with at least one tool. */
529
+ function validateToolScope(entry , at , errors ) {
530
+ if (!object(entry)) { issue(errors, at, 'must be an object'); return; }
531
+ for (const key of Object.keys(entry)) if (!TOOL_SCOPE_ENTRY_KEYS.has(key)) issue(errors, at, `has unknown key '${key}'`);
532
+ if (!AGENT_LIMIT_STATUS.has(entry.status)) { issue(errors, at, 'status must be native or unenforced'); return; }
533
+ const evidenceOk = Array.isArray(entry.evidence) && entry.evidence.every((item ) => typeof item === 'string' && isHttpUrl(item));
534
+ if (!evidenceOk) issue(errors, at, 'evidence must be an array of http(s) URLs');
535
+ if (typeof entry.verifiedOn !== 'string' || !/^\d{4}-\d{2}-\d{2}$/.test(entry.verifiedOn)) issue(errors, at, 'requires verifiedOn as an ISO date (YYYY-MM-DD)');
536
+ if (entry.status === 'unenforced') {
537
+ for (const key of ['field', 'form', 'map', 'mcpSelector']) if (entry[key] !== undefined) issue(errors, at, `unenforced must not carry ${key}`);
538
+ if (typeof entry.reason !== 'string' || !entry.reason.trim()) issue(errors, at, 'unenforced requires a reason');
539
+ else if (evidenceOk && entry.evidence.length === 0 && entry.reason.trim().toLowerCase() !== 'no agent surface') issue(errors, at, 'unenforced requires non-empty evidence unless the reason is "no agent surface"');
540
+ return;
541
+ }
542
+ if (typeof entry.field !== 'string' || !entry.field.trim()) issue(errors, at, 'native requires field');
543
+ if (!TOOL_SCOPE_FORMS.has(entry.form)) issue(errors, at, `form must be one of ${[...TOOL_SCOPE_FORMS].join(', ')}`);
544
+ if (entry.checked !== 'docs' && entry.checked !== 'live') issue(errors, at, 'native requires checked as docs or live');
545
+ if (evidenceOk && entry.evidence.length === 0) issue(errors, at, 'native requires non-empty evidence');
546
+ // A selector names one server: no wildcard may stand before `{server}`, and the server is not the whole selector.
547
+ if (typeof entry.mcpSelector !== 'string' || !entry.mcpSelector.includes('{server}') || entry.mcpSelector === '{server}' || /[*?].*\{server\}/.test(entry.mcpSelector)) {
548
+ issue(errors, at, 'native requires mcpSelector, a template naming one {server} with no wildcard before it');
549
+ }
550
+ if (!object(entry.map)) { issue(errors, at, 'native requires map'); return; }
551
+ for (const key of Object.keys(entry.map)) if (!TOOL_SCOPE_MAPPED.includes(key)) issue(errors, at, `map has unknown category '${key}'`);
552
+ for (const category of TOOL_SCOPE_MAPPED) {
553
+ const tools = entry.map[category];
554
+ if (!Array.isArray(tools) || tools.length === 0 || tools.some((tool ) => typeof tool !== 'string' || !tool.trim())) issue(errors, at, `map.${category} must be a non-empty array of tool names`);
555
+ }
556
+ }
557
+
558
+ /** Keys of a harness's `worker` block (081 IC-001) and the recipe fields each capability entry may
559
+ * carry when, and only when, its status is `supported`. `required` lists the fields a `supported`
560
+ * entry must carry. The last three are declared, not implemented, so they take no recipe field. */
561
+ const WORKER_CAPABILITIES = Object.freeze({
562
+ cli: { allowed: ['binary', 'versionArgv'], required: ['binary', 'versionArgv'] },
563
+ oneShot: { allowed: ['argv', 'promptVia'], required: ['argv', 'promptVia'] },
564
+ structuredOutput: { allowed: ['argv', 'parser', 'quotaSignals'], required: ['parser'] },
565
+ readOnly: { allowed: ['argv'], required: ['argv'] },
566
+ spawnDenial: { allowed: ['argv'], required: ['argv'] },
567
+ runnableProbe: { allowed: ['argv'], required: ['argv'] },
568
+ hooksProbe: { allowed: ['argv'], required: ['argv'] },
569
+ guidanceProbe: { allowed: ['argv', 'marker'], required: ['argv'] },
570
+ loginProbe: { allowed: ['argv'], required: ['argv'] },
571
+ persistentSession: { allowed: [], required: [] },
572
+ resume: { allowed: [], required: [] },
573
+ steering: { allowed: [], required: [] },
574
+ });
575
+ /** Capability entries a `worker` block may carry but need not (082 IC-013, DEC-020; 120 IC-011).
576
+ * Absent reads as `unknown`; present, the entry takes every IC-001 capability-entry rule. */
577
+ const WORKER_OPTIONAL_CAPABILITIES = Object.freeze({
578
+ isolatedEdit: { allowed: ['argv'], required: ['argv'] },
579
+ model: { allowed: ['argv'], required: ['argv'] },
580
+ });
581
+ const WORKER_BILLING = new Set(['free', 'metered', 'unknown']);
582
+ const WORKER_KEYS = Object.freeze(['profile', 'billing', ...Object.keys(WORKER_CAPABILITIES), 'modelChecks']);
583
+ const WORKER_ALLOWED_KEYS = Object.freeze([...WORKER_KEYS, ...Object.keys(WORKER_OPTIONAL_CAPABILITIES), 'family']);
584
+ const WORKER_ENTRY_FIELDS = new Set(['status', 'evidence', 'verification', 'verifiedOn', 'reason']);
585
+ const WORKER_RECIPE_FIELDS = new Set(['binary', 'versionArgv', 'argv', 'promptVia', 'parser', 'marker', 'quotaSignals']);
586
+ const WORKER_QUOTA_SIGNALS_FIELDS = new Set(['signals', 'basis', 'evidence']);
587
+ const WORKER_FACTS = new Set(['hooksLoaded', 'identityForwarded', 'guidanceLoaded', 'login']);
588
+ const WORKER_MODEL_CHECK_FIELDS = new Set(['id', 'argv', 'establishes', 'estimateTokens', 'timeoutSeconds']);
589
+ const WORKER_ARGV_MAX = 32;
590
+ const WORKER_ARG_MAX_CHARS = 200;
591
+
592
+ /** A worker argv: an array of 0 to 32 strings, each at most 200 characters and free of NUL. */
593
+ function workerArgvProblem(value ) {
594
+ if (!Array.isArray(value) || value.some((item ) => typeof item !== 'string')) return 'argv must be an array of strings';
595
+ if (value.length > WORKER_ARGV_MAX) return `argv must have at most ${WORKER_ARGV_MAX} elements`;
596
+ if (value.some((item ) => item.length > WORKER_ARG_MAX_CHARS)) return `argv elements must be at most ${WORKER_ARG_MAX_CHARS} characters`;
597
+ if (value.some((item ) => item.includes('\0'))) return 'argv elements must not contain NUL';
598
+ return null;
599
+ }
600
+
601
+ /** Validates one harness's `worker` block (081 IC-001): the external-worker capabilities and launch
602
+ * recipe beside `orchestration`. Every message reads `harness '<id>' worker '<key>': ...` and never
603
+ * contains ` orchestration`, so G33.15's orchestration filter reads none of them. Membership of
604
+ * `structuredOutput.parser` in the parser table is checked where that table lives (G41.3, IC-008),
605
+ * not here, so the registry does not require a runtime module. */
606
+ function validateWorker(value , location , errors ) {
607
+ const fail = (key , message ) => issue(errors, `${location} worker '${key}'`, message);
608
+ if (!object(value)) { fail('*', 'worker block is required and must be an object'); return; }
609
+ for (const key of WORKER_KEYS) if (!Object.hasOwn(value, key)) fail(key, 'is missing');
610
+ for (const key of Object.keys(value)) if (!WORKER_ALLOWED_KEYS.includes(key)) fail(key, 'is not a worker key');
611
+
612
+ if (Object.hasOwn(value, 'profile') && value.profile !== null && !COVERAGE_PROFILES.has(value.profile)) {
613
+ fail('profile', 'must be a lifecycle coverage profile or null');
614
+ }
615
+ if (Object.hasOwn(value, 'billing') && !WORKER_BILLING.has(value.billing)) {
616
+ fail('billing', 'must be free, metered, or unknown');
617
+ }
618
+ if (Object.hasOwn(value, 'family') && (typeof value.family !== 'string' || !value.family.trim())) {
619
+ fail('family', 'must be a non-empty string');
620
+ }
621
+
622
+ for (const [key, rule] of [...Object.entries(WORKER_CAPABILITIES), ...Object.entries(WORKER_OPTIONAL_CAPABILITIES)]) {
623
+ if (!Object.hasOwn(value, key)) continue;
624
+ const entry = value[key];
625
+ if (!object(entry)) { fail(key, 'must be a capability entry object'); continue; }
626
+ if (!ORCHESTRATION_STATUS.has(entry.status)) fail(key, 'status must be supported, unsupported, or unknown');
627
+ if (!Array.isArray(entry.evidence) || entry.evidence.some((item ) => typeof item !== 'string')) {
628
+ fail(key, 'evidence must be an array of strings');
629
+ } else if (entry.evidence.length === 0 && (entry.status === 'supported' || entry.status === 'unsupported')) {
630
+ fail(key, `evidence must not be empty when status is ${entry.status}`);
631
+ }
632
+ if (typeof entry.verification !== 'string' || !entry.verification.trim()) fail(key, 'requires a verification instruction');
633
+ if (typeof entry.verifiedOn !== 'string' || !/^\d{4}-\d{2}-\d{2}$/.test(entry.verifiedOn)) fail(key, 'requires verifiedOn as an ISO date (YYYY-MM-DD)');
634
+ if (entry.status === 'unknown' && (typeof entry.reason !== 'string' || !entry.reason.trim())) fail(key, 'requires a reason when status is unknown');
635
+ if (Object.hasOwn(entry, 'reason') && entry.status !== 'unknown' && (typeof entry.reason !== 'string' || !entry.reason.trim())) fail(key, 'reason must be a non-empty string');
636
+
637
+ for (const field of Object.keys(entry)) {
638
+ if (WORKER_ENTRY_FIELDS.has(field)) continue;
639
+ if (!WORKER_RECIPE_FIELDS.has(field) || !rule.allowed.includes(field)) { fail(key, `has unknown field '${field}'`); continue; }
640
+ if (entry.status !== 'supported') fail(key, `recipe field '${field}' is allowed only when status is supported`);
641
+ }
642
+ if (entry.status !== 'supported') continue;
643
+ for (const field of rule.required) if (!Object.hasOwn(entry, field)) fail(key, `requires recipe field '${field}' when status is supported`);
644
+
645
+ if (Object.hasOwn(entry, 'binary') && (typeof entry.binary !== 'string' || !/^[A-Za-z0-9][A-Za-z0-9._-]*$/.test(entry.binary))) {
646
+ fail(key, 'binary must be a bare command name');
647
+ }
648
+ for (const field of ['versionArgv', 'argv']) {
649
+ if (!Object.hasOwn(entry, field)) continue;
650
+ const problem = workerArgvProblem(entry[field]);
651
+ if (problem) {
652
+ fail(key, problem.replace(/^argv/, field));
653
+ } else {
654
+ // 120 IC-011: only model.argv carries one whole model placeholder.
655
+ if (key === 'model' && field === 'argv') {
656
+ const count = entry.argv.filter((item ) => item === '{model}').length;
657
+ if (count !== 1 || entry.argv.some((item ) => item.includes('{model}') && item !== '{model}')) {
658
+ fail(key, "argv must contain the element '{model}' exactly once, as a whole element");
659
+ }
660
+ } else if (entry[field].some((item ) => item.includes('{model}'))) {
661
+ fail(key, `${field} must not contain '{model}' outside model.argv`);
662
+ }
663
+ // DEC-066: only oneShot.argv carries the prompt; a second placeholder would duplicate it.
664
+ if (!(key === 'oneShot' && field === 'argv') && entry[field].includes('{prompt}')) {
665
+ fail(key, `${field} must not contain '{prompt}' outside oneShot.argv`);
666
+ }
667
+ if (key === 'structuredOutput' && field === 'argv') {
668
+ const count = entry.argv.filter((item ) => item === '{lastMessageFile}').length;
669
+ if (count > 1) {
670
+ fail(key, "argv must contain '{lastMessageFile}' only once and only in structuredOutput.argv");
671
+ }
672
+ } else if (entry[field].includes('{lastMessageFile}')) {
673
+ fail(key, "argv must contain '{lastMessageFile}' only once and only in structuredOutput.argv");
674
+ }
675
+ }
676
+ }
677
+ if (Object.hasOwn(entry, 'parser') && (typeof entry.parser !== 'string' || !entry.parser.trim())) fail(key, 'parser must be a parser id');
678
+ if (key === 'structuredOutput' && Object.hasOwn(entry, 'quotaSignals')) {
679
+ const quota = entry.quotaSignals;
680
+ if (!object(quota)) fail(key, 'quotaSignals must be an object');
681
+ else {
682
+ for (const field of Object.keys(quota)) if (!WORKER_QUOTA_SIGNALS_FIELDS.has(field)) fail(key, `quotaSignals has unknown field '${field}'`);
683
+ if (!Array.isArray(quota.signals) || quota.signals.length < 1 || quota.signals.length > 16) fail(key, 'quotaSignals.signals must contain 1 to 16 signals');
684
+ else for (const signal of quota.signals) {
685
+ if (typeof signal !== 'string' || signal !== signal.trim() || signal.length < 3 || signal.length > 64 || /[\u0000-\u001f\u007f]/.test(signal)) {
686
+ fail(key, 'quotaSignals.signals must be trimmed strings of 3 to 64 characters without control characters');
687
+ break;
688
+ }
689
+ }
690
+ if (quota.basis !== 'unverified' && quota.basis !== 'transcript') fail(key, "quotaSignals.basis must be 'unverified' or 'transcript'");
691
+ if (!Array.isArray(quota.evidence) || quota.evidence.some((item ) => typeof item !== 'string')) fail(key, 'quotaSignals.evidence must be an array of strings');
692
+ else if (quota.basis === 'transcript' && quota.evidence.length === 0) fail(key, 'quotaSignals transcript basis requires evidence');
693
+ else if (quota.basis === 'unverified' && quota.evidence.length !== 0) fail(key, 'quotaSignals unverified basis requires empty evidence');
694
+ else if (quota.basis === 'transcript') {
695
+ const harnessMatch = location.match(/^harness '([^']+)'/);
696
+ for (const item of quota.evidence) {
697
+ if (path.isAbsolute(item) || item.split(/[\\/]/).includes('..')) {
698
+ fail(key, 'quotaSignals transcript evidence must be under its harness worker-evidence directory');
699
+ } else if (harnessMatch) {
700
+ const prefix = `core/worker-evidence/${harnessMatch[1]}/`;
701
+ if (!item.startsWith(prefix)) fail(key, 'quotaSignals transcript evidence must be under its harness worker-evidence directory');
702
+ }
703
+ }
704
+ }
705
+ }
706
+ }
707
+ if (Object.hasOwn(entry, 'marker') && (typeof entry.marker !== 'string' || entry.marker.length < 1 || entry.marker.length > 200)) {
708
+ fail(key, 'marker must be 1 to 200 characters');
709
+ }
710
+ if (key === 'oneShot' && Object.hasOwn(entry, 'promptVia')) {
711
+ const count = Array.isArray(entry.argv) ? entry.argv.filter((item ) => item === '{prompt}').length : 0;
712
+ if (entry.promptVia === 'argv') {
713
+ if (count !== 1) fail(key, "argv must contain the element '{prompt}' exactly once when promptVia is argv");
714
+ } else if (entry.promptVia === 'stdin') {
715
+ if (count !== 0) fail(key, "argv must not contain '{prompt}' when promptVia is stdin");
716
+ } else {
717
+ fail(key, 'promptVia must be argv or stdin');
718
+ }
719
+ }
720
+ }
721
+
722
+ if (!Object.hasOwn(value, 'modelChecks')) return;
723
+ if (!Array.isArray(value.modelChecks)) { fail('modelChecks', 'must be an array'); return; }
724
+ const seen = new Set ();
725
+ value.modelChecks.forEach((check , index ) => {
726
+ const key = `modelChecks[${index}]`;
727
+ if (!object(check)) { fail(key, 'must be an object'); return; }
728
+ for (const field of Object.keys(check)) if (!WORKER_MODEL_CHECK_FIELDS.has(field)) fail(key, `has unknown field '${field}'`);
729
+ if (typeof check.id !== 'string' || !/^[a-z][a-z0-9-]*$/.test(check.id)) fail(key, 'id must be a lowercase identifier');
730
+ else if (seen.has(check.id)) fail(key, `duplicate id '${check.id}'`);
731
+ else seen.add(check.id);
732
+ const problem = workerArgvProblem(check.argv);
733
+ if (problem) {
734
+ fail(key, problem);
735
+ } else {
736
+ if (check.argv.some((item ) => item.includes('{model}'))) fail(key, "argv must not contain '{model}' outside model.argv");
737
+ if (check.argv.includes('{prompt}')) fail(key, "argv must not contain '{prompt}' outside oneShot.argv");
738
+ if (check.argv.includes('{lastMessageFile}')) fail(key, "argv must contain '{lastMessageFile}' only once and only in structuredOutput.argv");
739
+ }
740
+ if (!Array.isArray(check.establishes) || check.establishes.length === 0 || check.establishes.some((fact ) => !WORKER_FACTS.has(fact))) {
741
+ fail(key, `establishes must be a non-empty subset of ${[...WORKER_FACTS].join(', ')}`);
742
+ }
743
+ if (!Number.isInteger(check.estimateTokens) || check.estimateTokens < 1) fail(key, 'estimateTokens must be an integer of at least 1');
744
+ if (!Number.isInteger(check.timeoutSeconds) || check.timeoutSeconds < 1 || check.timeoutSeconds > 600) fail(key, 'timeoutSeconds must be an integer from 1 to 600');
745
+ });
746
+ }
747
+
415
748
  const CONTEXT_PROFILE_FIELDS = Object.freeze([
416
749
  'capacityTokens',
417
750
  'reserveTokens',
@@ -500,7 +833,62 @@ function validateContextProfile(value , location , errors )
500
833
  }
501
834
  }
502
835
 
503
- function validateRegistry(registry , { repoRoot, fsImpl = fs } = {}) {
836
+ /** Validates the IC-006 primary model probe declaration for one harness. */
837
+ function validatePrimaryProbe(probe , harnessId , at , errors ) {
838
+ const location = `${at} primaryProbe`;
839
+ const fields = ['model', 'events', 'status', 'evidence', 'verifiedOn', 'reason'];
840
+ if (!object(probe)) {
841
+ issue(errors, location, 'must be an object');
842
+ return;
843
+ }
844
+ for (const key of Object.keys(probe)) {
845
+ if (!fields.includes(key)) issue(errors, location, `has an unknown key '${key}'`);
846
+ }
847
+ for (const field of fields) {
848
+ if (!Object.prototype.hasOwnProperty.call(probe, field)) issue(errors, location, `is missing field '${field}'`);
849
+ }
850
+ if (probe.model !== 'payload' && probe.model !== 'none') issue(errors, location, "model must be 'payload' or 'none'");
851
+ if (!Array.isArray(probe.events)) {
852
+ issue(errors, location, 'events must be an array of distinct non-empty strings');
853
+ } else {
854
+ if (probe.events.some((event ) => typeof event !== 'string' || !event.trim())) {
855
+ issue(errors, location, 'events must contain only non-empty strings');
856
+ }
857
+ if (new Set(probe.events).size !== probe.events.length) issue(errors, location, 'events must contain distinct strings');
858
+ if (probe.model === 'payload' && probe.events.length === 0) issue(errors, location, "events must be non-empty when model is 'payload'");
859
+ if (probe.model === 'none' && probe.events.length !== 0) issue(errors, location, "events must be empty when model is 'none'");
860
+ }
861
+ if (probe.status !== 'supported' && probe.status !== 'unverified') issue(errors, location, "status must be 'supported' or 'unverified'");
862
+ if (!Array.isArray(probe.evidence) || probe.evidence.some((item ) => typeof item !== 'string')) {
863
+ issue(errors, location, 'evidence must be an array of strings');
864
+ }
865
+ if (probe.verifiedOn !== null && (typeof probe.verifiedOn !== 'string' || !/^\d{4}-\d{2}-\d{2}$/.test(probe.verifiedOn))) {
866
+ issue(errors, location, 'verifiedOn must be null or a YYYY-MM-DD string');
867
+ }
868
+ if (typeof probe.reason !== 'string' || !probe.reason.trim()) {
869
+ issue(errors, location, 'reason must be a non-empty line');
870
+ } else if (/[\r\n]/.test(probe.reason)) {
871
+ issue(errors, location, 'reason must be a non-empty line');
872
+ }
873
+ if (probe.status === 'supported') {
874
+ if (probe.model !== 'payload') issue(errors, location, "status 'supported' requires model 'payload'");
875
+ const prefix = `core/worker-evidence/${harnessId}/primary-probe/`;
876
+ if (!Array.isArray(probe.evidence) || !probe.evidence.some((item ) => typeof item === 'string' && item.startsWith(prefix) && item.length > prefix.length)) {
877
+ issue(errors, location, `status 'supported' requires evidence under '${prefix}'`);
878
+ }
879
+ if (typeof probe.verifiedOn !== 'string' || !/^\d{4}-\d{2}-\d{2}$/.test(probe.verifiedOn)) {
880
+ issue(errors, location, "status 'supported' requires a verifiedOn date");
881
+ }
882
+ }
883
+ }
884
+
885
+ function validateRegistry(
886
+ registry ,
887
+ { repoRoot, fsImpl = fs, sources = 'require' } = {},
888
+ ) {
889
+ if (sources !== 'require' && sources !== 'skip') {
890
+ throw new TypeError(`sources must be 'require' or 'skip', got ${JSON.stringify(sources)}`);
891
+ }
504
892
  const errors = [];
505
893
  const harnesses = Array.isArray(registry?.harnesses) ? registry.harnesses : null;
506
894
  const assets = Array.isArray(registry?.assets) ? registry.assets : null;
@@ -522,7 +910,8 @@ function validateRegistry(registry , { repoRoot, fsImpl = fs }
522
910
  for (const harness of harnesses || []) {
523
911
  const at = `harness '${harness?.id ?? '?'}'`;
524
912
  if (!object(harness)) { issue(errors, 'harnesses', 'entries must be objects'); continue; }
525
- if (typeof harness.id !== 'string' || !/^[a-z][a-z0-9-]*$/.test(harness.id)) issue(errors, at, 'id must be a lowercase identifier');
913
+ if (typeof harness.id !== 'string' || !BUILTIN_WORKER_ID_RE.test(harness.id)) issue(errors, at, 'id must be a lowercase identifier');
914
+ else if (RESERVED_WORKER_IDS.includes(harness.id)) issue(errors, at, `id '${harness.id}' is a reserved worker id`);
526
915
  if (typeof harness.displayName !== 'string' || !harness.displayName.trim()) issue(errors, at, 'requires displayName');
527
916
  if (typeof harness.adapter !== 'string' || !harness.adapter.trim()) issue(errors, at, 'requires adapter');
528
917
  if (!Array.isArray(harness.scopes) || harness.scopes.length === 0 || harness.scopes.some((scope ) => !SCOPES.has(scope))) issue(errors, at, 'scopes must contain project and/or user');
@@ -539,10 +928,23 @@ function validateRegistry(registry , { repoRoot, fsImpl = fs }
539
928
  validateCapability(declaration, `${at} capability '${capability}'`, errors);
540
929
  }
541
930
  validateOrchestration(harness.orchestration, at, errors);
931
+ validateAgentLimits(harness.agentLimits, at, errors);
932
+ validateWorker(harness.worker, at, errors);
933
+ const quota = harness.worker?.structuredOutput?.quotaSignals;
934
+ if (object(quota) && quota.basis === 'transcript' && Array.isArray(quota.evidence) && typeof repoRoot === 'string') {
935
+ for (const evidence of quota.evidence) {
936
+ if (typeof evidence === 'string' && !fsImpl.existsSync(path.resolve(repoRoot, evidence))) {
937
+ issue(errors, `${at} worker 'structuredOutput'`, `quotaSignals transcript evidence '${evidence}' does not exist`);
938
+ }
939
+ }
940
+ }
542
941
  validateContextProfile(harness.contextProfile, at, errors);
543
- if (harness.id === 'codex') validateNativeProjection(harness.nativeProjection, harness, { repoRoot, fsImpl }, errors);
942
+ validatePrimaryProbe(harness.primaryProbe, typeof harness.id === 'string' ? harness.id : '?', at, errors);
943
+ if (harness.id === 'codex') validateNativeProjection(harness.nativeProjection, harness, { repoRoot, fsImpl, sources }, errors);
544
944
  }
545
945
 
946
+ validateRetired(registry?.retired, harnessIds, errors);
947
+
546
948
  const contractHarnesses = new Set();
547
949
  for (const contract of contracts || []) {
548
950
  validateContract(contract, `contract '${contract?.harness ?? '?'}'`, errors, harnessIds);
@@ -563,7 +965,8 @@ function validateRegistry(registry , { repoRoot, fsImpl = fs }
563
965
  for (const field of ['id', 'kind', 'source', 'ownership']) if (typeof asset[field] !== 'string' || !asset[field].trim()) issue(errors, at, `requires ${field}`);
564
966
  if (typeof asset.source === 'string' && repoRoot) {
565
967
  const source = path.resolve(repoRoot, asset.source);
566
- if (!source.startsWith(`${path.resolve(repoRoot)}${path.sep}`) || !fsImpl.existsSync(source)) issue(errors, at, `source does not exist inside repository: ${asset.source}`);
968
+ const outside = !source.startsWith(`${path.resolve(repoRoot)}${path.sep}`);
969
+ if (outside || (sources === 'require' && !fsImpl.existsSync(source))) issue(errors, at, `source does not exist inside repository: ${asset.source}`);
567
970
  }
568
971
  if (!Array.isArray(asset.appliesTo) || asset.appliesTo.length === 0) issue(errors, at, 'appliesTo must be a non-empty array');
569
972
  if (!object(asset.projection)) issue(errors, at, 'projection must be an object');
@@ -577,6 +980,12 @@ function validateRegistry(registry , { repoRoot, fsImpl = fs }
577
980
  const capability = harness?.capabilities?.[projection.capability];
578
981
  if (!capability) issue(errors, at, `projection '${harnessId}' references unknown capability '${projection.capability}'`);
579
982
  else if (capability.status === 'unavailable') issue(errors, at, `projection '${harnessId}' cannot render unavailable capability '${projection.capability}'`);
983
+ if (projection.select !== undefined) {
984
+ if (!COPY_TREE_SELECT_RENDERERS.has(projection.renderer)) issue(errors, at, `projection '${harnessId}' select is only valid for ${[...COPY_TREE_SELECT_RENDERERS].map((name) => `'${name}'`).join(' and ')}`);
985
+ if (typeof projection.select !== 'string' || !COPY_TREE_SELECTS.has(projection.select)) {
986
+ issue(errors, at, `projection '${harnessId}' has unknown select '${projection.select}' (known: ${[...COPY_TREE_SELECTS].join(', ')})`);
987
+ }
988
+ }
580
989
  }
581
990
  }
582
991
 
@@ -633,7 +1042,7 @@ function validateRegistry(registry , { repoRoot, fsImpl = fs }
633
1042
  const providerIds = new Set((modelProviders || []).filter(object).map((item ) => item.id));
634
1043
  for (const slot of modelSlots) validateModelSlot(slot, `model slot '${slot?.id ?? '?'}'`, errors, providerIds);
635
1044
  }
636
- return { ok: errors.length === 0, errors };
1045
+ return { ok: errors.length === 0, errors, sources };
637
1046
  }
638
1047
 
639
1048
  const COVERAGE_CLASSES = new Set(['blocking', 'observation', 'advisory', 'unsupported', 'unknown']);
@@ -650,6 +1059,8 @@ function validateCoverage(value , lifecycle , harnessIds ,
650
1059
  if (typeof value.rowsDigest !== 'string' || !/^sha256:[a-f0-9]{64}$/.test(value.rowsDigest)) issue(errors, `${at}.rowsDigest`, 'must be sha256 followed by 64 lowercase hex characters');
651
1060
  if (!object(value.refresh) || typeof value.refresh.trigger !== 'string' || !value.refresh.trigger.trim() || typeof value.refresh.owner !== 'string' || !value.refresh.owner.trim()) issue(errors, `${at}.refresh`, 'requires non-empty trigger and owner');
652
1061
  if (!object(value.profiles) || [...COVERAGE_PROFILES].some((profile) => typeof value.profiles?.[profile] !== 'string' || !value.profiles[profile].trim())) issue(errors, `${at}.profiles`, 'must define every supported profile with a non-empty meaning');
1062
+ if (value.endOfTurn !== undefined) validateEndOfTurn(value.endOfTurn, `${at}.endOfTurn`, harnessIds, errors);
1063
+ if (value.behaviours !== undefined) validateBehaviours(value.behaviours, `${at}.behaviours`, harnessIds, errors);
653
1064
  if (!Array.isArray(value.rows)) { issue(errors, `${at}.rows`, 'must be an array'); return; }
654
1065
  const keys = new Set ();
655
1066
  for (const [index, row] of value.rows.entries()) {
@@ -674,6 +1085,120 @@ function validateCoverage(value , lifecycle , harnessIds ,
674
1085
  }
675
1086
  }
676
1087
 
1088
+ const nonEmptyString = (item ) => typeof item === 'string' && item.trim() !== '';
1089
+ const provenanceItemOk = (item ) => object(item) && COVERAGE_KINDS.has(item.kind) && nonEmptyString(item.locator)
1090
+ && (!['doc', 'types', 'live'].includes(item.kind) || (typeof item.date === 'string' && /^\d{4}-\d{2}-\d{2}$/.test(item.date)));
1091
+
1092
+ /** Each registry harness appears exactly once under `key`; reports missing, unknown and duplicate entries. */
1093
+ function oncePerHarness(entries , at , harnessIds , key , label , groups , errors ) {
1094
+ const seen = new Set ();
1095
+ for (const entry of entries) {
1096
+ if (!object(entry)) continue;
1097
+ const id = key(entry);
1098
+ if (seen.has(id)) issue(errors, at, `duplicates ${label(entry)}`);
1099
+ seen.add(id);
1100
+ }
1101
+ for (const group of groups) for (const harness of harnessIds) if (!seen.has(JSON.stringify([group, harness]))) issue(errors, at, `missing an entry for ${group ? `${group} × ` : ''}harness '${harness}'`);
1102
+ }
1103
+
1104
+ /** IC-017: one end-of-turn entry per harness; `none` when no event, provenance when any. */
1105
+ function validateEndOfTurn(value , at , harnessIds , errors ) {
1106
+ if (!Array.isArray(value)) { issue(errors, at, 'must be an array'); return; }
1107
+ for (const [index, entry] of value.entries()) {
1108
+ const entryAt = `${at}[${index}]`;
1109
+ if (!object(entry)) { issue(errors, entryAt, 'must be an object'); continue; }
1110
+ if (!harnessIds.has(entry.harness)) issue(errors, `${entryAt}.harness`, 'must name a registry harness');
1111
+ if (!Array.isArray(entry.events) || !entry.events.every(nonEmptyString)) { issue(errors, `${entryAt}.events`, 'must be an array of event names'); continue; }
1112
+ if (!Array.isArray(entry.provenance) || !entry.provenance.every(provenanceItemOk)) issue(errors, `${entryAt}.provenance`, 'must be an array of well-formed provenance items');
1113
+ if (entry.events.length === 0 && !nonEmptyString(entry.none)) issue(errors, `${entryAt}.none`, 'is required, non-empty, when events is empty');
1114
+ if (entry.events.length > 0 && !(Array.isArray(entry.provenance) && entry.provenance.some((item ) => object(item) && ['doc', 'types'].includes(item.kind)))) issue(errors, `${entryAt}.provenance`, 'needs a doc or types item when events is non-empty');
1115
+ }
1116
+ oncePerHarness(value, at, harnessIds, (entry) => JSON.stringify(['', entry.harness]), (entry) => `harness '${entry.harness}'`, [''], errors);
1117
+ }
1118
+
1119
+ const COVERAGE_BEHAVIOURS = Object.freeze(['context-authority', 'default-silence', 'summary-scope', 'summary-grant', 'identity', 'check-freshness']);
1120
+
1121
+ /** IC-018: one entry per behaviour × harness; `enforced` names tests, `limitation` states its reason. */
1122
+ function validateBehaviours(value , at , harnessIds , errors ) {
1123
+ if (!Array.isArray(value)) { issue(errors, at, 'must be an array'); return; }
1124
+ for (const [index, entry] of value.entries()) {
1125
+ const entryAt = `${at}[${index}]`;
1126
+ if (!object(entry)) { issue(errors, entryAt, 'must be an object'); continue; }
1127
+ if (!COVERAGE_BEHAVIOURS.includes(entry.behaviour)) issue(errors, `${entryAt}.behaviour`, `must be one of ${COVERAGE_BEHAVIOURS.join(', ')}`);
1128
+ if (!harnessIds.has(entry.harness)) issue(errors, `${entryAt}.harness`, 'must name a registry harness');
1129
+ if (entry.status === 'enforced') {
1130
+ if (!Array.isArray(entry.tests) || entry.tests.length === 0 || !entry.tests.every(nonEmptyString)) issue(errors, `${entryAt}.tests`, 'must be a non-empty array of test paths when enforced');
1131
+ } else if (entry.status === 'limitation') {
1132
+ if (!nonEmptyString(entry.limitation)) issue(errors, `${entryAt}.limitation`, 'must be non-empty when status is limitation');
1133
+ } else issue(errors, `${entryAt}.status`, 'must be enforced or limitation');
1134
+ }
1135
+ oncePerHarness(value, at, harnessIds, (entry) => JSON.stringify([entry.behaviour, entry.harness]), (entry) => `behaviour '${entry.behaviour}' × harness '${entry.harness}'`, [...COVERAGE_BEHAVIOURS], errors);
1136
+ }
1137
+
1138
+ const RETIRED_ID_RE = /^[a-z][a-z0-9-]*$/;
1139
+ const HANDOVER_SCOPES = new Set(['global', 'project']);
1140
+
1141
+ /** A harness DoFlow no longer supports, with what replaces it and how its installed files are
1142
+ * released (row 127 IC-001; `removal.handover` is IC-007). */
1143
+
1144
+
1145
+
1146
+
1147
+
1148
+
1149
+
1150
+
1151
+
1152
+ function isCalendarDate(value ) {
1153
+ if (typeof value !== 'string' || !/^\d{4}-\d{2}-\d{2}$/.test(value)) return false;
1154
+ const date = new Date(`${value}T00:00:00Z`);
1155
+ return !Number.isNaN(date.getTime()) && date.toISOString().slice(0, 10) === value;
1156
+ }
1157
+
1158
+ /** Validates harnesses.json `retired[]`: optional; every entry fully typed; ids unique and disjoint
1159
+ * from the live harness ids; `replacement` and every `removal.handover[].to` name a live harness. */
1160
+ function validateRetired(value , harnessIds , errors ) {
1161
+ if (value === undefined) return;
1162
+ if (!Array.isArray(value)) { issue(errors, 'retired', 'must be an array when present'); return; }
1163
+ idsUnique(value, 'retired', errors);
1164
+ const strings = (items , allowEmpty ) => Array.isArray(items) && (allowEmpty || items.length > 0) && items.every(nonEmptyString);
1165
+ for (const [index, entry] of value.entries()) {
1166
+ if (!object(entry)) { issue(errors, `retired[${index}]`, 'must be an object'); continue; }
1167
+ const at = `retired harness '${typeof entry.id === 'string' ? entry.id : `#${index}`}'`;
1168
+ if (typeof entry.id !== 'string' || !RETIRED_ID_RE.test(entry.id)) issue(errors, at, `id must match ${RETIRED_ID_RE}`);
1169
+ else if (harnessIds.has(entry.id)) issue(errors, at, `id collides with live harness '${entry.id}'; an id is either live or retired`);
1170
+ for (const field of ['displayName', 'replacement', 'reason']) if (!nonEmptyString(entry[field])) issue(errors, at, `${field} must be a non-empty string`);
1171
+ if (nonEmptyString(entry.replacement) && !harnessIds.has(entry.replacement)) issue(errors, at, `replacement '${entry.replacement}' is not a live harness`);
1172
+ if (!isCalendarDate(entry.retiredOn)) issue(errors, at, 'retiredOn must be a YYYY-MM-DD date');
1173
+ if (!strings(entry.evidence, false)) issue(errors, at, 'evidence must be a non-empty array of non-empty strings');
1174
+ const removal = entry.removal;
1175
+ if (!object(removal)) { issue(errors, at, 'removal must be an object'); continue; }
1176
+ const hooks = removal.hookCommands;
1177
+ if (!object(hooks)) issue(errors, at, 'removal.hookCommands must be an object');
1178
+ else {
1179
+ for (const field of ['renderer', 'key']) if (!nonEmptyString(hooks[field])) issue(errors, at, `removal.hookCommands.${field} must be a non-empty string`);
1180
+ if (!strings(hooks.commands, false)) issue(errors, at, 'removal.hookCommands.commands must be a non-empty array of non-empty strings');
1181
+ }
1182
+ if (!strings(removal.markerSectionRenderers, true)) issue(errors, at, 'removal.markerSectionRenderers must be an array of non-empty strings');
1183
+ if (!Array.isArray(removal.handover)) { issue(errors, at, 'removal.handover must be an array'); continue; }
1184
+ for (const [h, handover] of removal.handover.entries()) {
1185
+ const hat = `removal.handover[${h}]`;
1186
+ if (!object(handover)) { issue(errors, at, `${hat} must be an object`); continue; }
1187
+ for (const field of ['renderer', 'to', 'surface']) if (!nonEmptyString(handover[field])) issue(errors, at, `${hat}.${field} must be a non-empty string`);
1188
+ if (!HANDOVER_SCOPES.has(handover.scope)) issue(errors, at, `${hat}.scope must be global or project`);
1189
+ if (nonEmptyString(handover.to) && !harnessIds.has(handover.to)) issue(errors, at, `${hat}.to '${handover.to}' is not a live harness`);
1190
+ }
1191
+ }
1192
+ }
1193
+
1194
+ /** The retired record for `id`, or null when `id` is live or unknown. */
1195
+ function retiredFor(registry , id ) {
1196
+ const retired = Array.isArray(registry?.retired) ? registry.retired : [];
1197
+ return retired.find((entry ) => entry?.id === id) ?? null;
1198
+ }
1199
+
1200
+ function isRetired(registry , id ) { return retiredFor(registry, id) !== null; }
1201
+
677
1202
  function harnessFor(registry , id ) {
678
1203
  const harness = registry.harnesses.find((item ) => item.id === id);
679
1204
  if (!harness) throw new Error(`Unknown registry harness '${id}'`);
@@ -734,7 +1259,7 @@ function capabilityMapData(registry ) {
734
1259
  }
735
1260
 
736
1261
  module.exports = {
737
- REGISTRY_FILES, CAPABILITY_STATUS, ORCHESTRATION_STATUS, ORCHESTRATION_OPS, validateOrchestration, CONTEXT_PROFILE_FIELDS, validateContextProfile, PROVIDER_SOURCE_MODES, validateCapabilityProviders, EXTERNAL_TOOL_IDS, EXTERNAL_TOOL_ACTIONS, validateCoverage,
738
- parseRegistryFile, registryDir, loadRegistry, validateRegistry,
739
- harnessFor, selectAssets, selectMcpServers, mcpCapable, nativeMcpCatalog, capabilityMapData,
1262
+ REGISTRY_FILES, CAPABILITY_STATUS, COPY_TREE_SELECTS, ORCHESTRATION_STATUS, ORCHESTRATION_OPS, validateOrchestration, AGENT_LIMIT_STATUS, validateAgentLimits, TOOL_SCOPE_CATEGORIES, agentToolScopeView, WORKER_KEYS, WORKER_CAPABILITIES, WORKER_OPTIONAL_CAPABILITIES, validateWorker, CONTEXT_PROFILE_FIELDS, validateContextProfile, PROVIDER_SOURCE_MODES, validateCapabilityProviders, EXTERNAL_TOOL_IDS, EXTERNAL_TOOL_ACTIONS, validateCoverage,
1263
+ parseRegistryFile, registryDir, loadRegistry, validateRegistry, registrySourceMode,
1264
+ harnessFor, retiredFor, isRetired, selectAssets, selectMcpServers, mcpCapable, nativeMcpCatalog, capabilityMapData,
740
1265
  };