@khoavu882/doflow 2.0.0-beta.4 → 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 (497) hide show
  1. package/README.md +12 -11
  2. package/bin/doflow.js +21 -11
  3. package/core/.antigravity-plugin/plugin.json +1 -3
  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/mcp-tool-guard.sh +7 -17
  9. package/core/harnesses/claude/hooks/permission-audit.sh +3 -0
  10. package/core/harnesses/claude/hooks/post-compact.sh +35 -32
  11. package/core/harnesses/claude/hooks/post-edit-lint.sh +2 -0
  12. package/core/harnesses/claude/hooks/post-shell-observe.sh +48 -0
  13. package/core/harnesses/claude/hooks/post-tool-failure.sh +3 -0
  14. package/core/harnesses/claude/hooks/pre-bash-guard.sh +7 -17
  15. package/core/harnesses/claude/hooks/pre-compact.sh +23 -27
  16. package/core/harnesses/claude/hooks/pre-implement-gate.sh +7 -18
  17. package/core/harnesses/claude/hooks/session-end.sh +23 -10
  18. package/core/harnesses/claude/hooks/session-start.sh +5 -3
  19. package/core/harnesses/claude/hooks/skill-config-audit.sh +3 -0
  20. package/core/harnesses/claude/hooks/stop-check.sh +5 -3
  21. package/core/harnesses/claude/hooks/subagent-audit.sh +5 -5
  22. package/core/harnesses/claude/hooks/user-prompt-submit.sh +5 -3
  23. package/core/harnesses/claude/settings/settings.json +18 -0
  24. package/core/harnesses/claude/statusline/doflow-statusline.sh +1 -0
  25. package/core/harnesses/codex/agents/core-implementer.toml +1 -0
  26. package/core/harnesses/codex/agents/delivery-lead.toml +10 -0
  27. package/core/harnesses/codex/agents/quality-guardian.toml +1 -1
  28. package/core/harnesses/codex/agents/research-writer.toml +1 -1
  29. package/core/harnesses/codex/agents/reviewer.toml +10 -0
  30. package/core/harnesses/codex/agents/spec-analyst.toml +1 -1
  31. package/core/harnesses/codex/agents/system-architect.toml +1 -1
  32. package/core/harnesses/codex/hooks/lib.sh +44 -25
  33. package/core/harnesses/codex/hooks/mcp-tool-guard.sh +7 -15
  34. package/core/harnesses/codex/hooks/post-edit-lint.impl.sh +3 -1
  35. package/core/harnesses/codex/hooks/pre-bash-guard.sh +7 -15
  36. package/core/harnesses/codex/hooks/pre-compact.impl.sh +29 -26
  37. package/core/harnesses/codex/hooks/pre-implement-gate.sh +7 -16
  38. package/core/harnesses/codex/hooks/session-end.impl.sh +23 -10
  39. package/core/harnesses/codex/hooks/session-start.sh +5 -3
  40. package/core/harnesses/codex/hooks/stop-check.sh +6 -5
  41. package/core/harnesses/codex/hooks/subagent-audit.sh +18 -6
  42. package/core/harnesses/codex/hooks/user-prompt-submit.sh +5 -4
  43. package/core/harnesses/copilot/hooks/doflow-lib.sh +93 -0
  44. package/core/harnesses/copilot/hooks/doflow-mcp-tool-guard.sh +9 -0
  45. package/core/harnesses/copilot/hooks/doflow-pre-bash-guard.sh +9 -0
  46. package/core/harnesses/copilot/hooks/doflow-pre-implement-gate.sh +9 -0
  47. package/core/harnesses/copilot/hooks/doflow-session-start.sh +9 -0
  48. package/core/harnesses/copilot/hooks/doflow-stop-check.sh +9 -0
  49. package/core/harnesses/copilot/hooks/doflow-subagent-start.sh +9 -0
  50. package/core/harnesses/copilot/hooks/doflow-subagent-stop.sh +9 -0
  51. package/core/harnesses/copilot/hooks/doflow.json +57 -0
  52. package/core/harnesses/kiro/agents/doflow.json +1 -1
  53. package/core/harnesses/kiro/hooks/lib.sh +26 -6
  54. package/core/harnesses/kiro/hooks/mcp-tool-guard.sh +21 -5
  55. package/core/harnesses/kiro/hooks/pre-bash-guard.sh +21 -5
  56. package/core/harnesses/kiro/hooks/pre-implement-gate.sh +21 -5
  57. package/core/harnesses/kiro/hooks/session-start.sh +12 -5
  58. package/core/harnesses/kiro/hooks/stop-check.sh +22 -30
  59. package/core/harnesses/pi/extensions/doflow-guard.js +167 -0
  60. package/core/harnesses/shared/hooks/ENVELOPE_MIGRATION.md +27 -12
  61. package/core/harnesses/shared/hooks/adapters/antigravity.js +14 -57
  62. package/core/harnesses/shared/hooks/bridge/bridge.sh +277 -0
  63. package/core/harnesses/shared/hooks/bridge/envelope.jq +129 -0
  64. package/core/harnesses/shared/hooks/bridge/observe-model.sh +75 -0
  65. package/core/harnesses/shared/hooks/bridge/respond.sh +70 -0
  66. package/core/harnesses/shared/hooks/policies/blocked-patterns.conf +28 -14
  67. package/core/harnesses/shared/hooks/policies/capture-failure.sh +51 -16
  68. package/core/harnesses/shared/hooks/policies/compact-text.sh +46 -0
  69. package/core/harnesses/shared/hooks/policies/contract.sh +78 -0
  70. package/core/harnesses/shared/hooks/policies/deny-json.sh +35 -9
  71. package/core/harnesses/shared/hooks/policies/emission-rules.sh +37 -0
  72. package/core/harnesses/shared/hooks/policies/emission.jq +209 -0
  73. package/core/harnesses/shared/hooks/policies/emission.sh +383 -0
  74. package/core/harnesses/shared/hooks/policies/identity.sh +177 -0
  75. package/core/harnesses/shared/hooks/policies/lib.sh +65 -40
  76. package/core/harnesses/shared/hooks/policies/mcp-tool-guard.sh +48 -35
  77. package/core/harnesses/shared/hooks/policies/pre-bash-guard.sh +284 -33
  78. package/core/harnesses/shared/hooks/policies/pre-implementation-gate.sh +180 -110
  79. package/core/harnesses/shared/hooks/policies/session-context.sh +66 -7
  80. package/core/harnesses/shared/hooks/policies/stop-check.sh +61 -23
  81. package/core/harnesses/shared/hooks/policies/subagent-audit.sh +39 -6
  82. package/core/harnesses/shared/hooks/policies/summary-store.sh +347 -0
  83. package/core/harnesses/shared/hooks/policies/user-prompt-submit.sh +97 -159
  84. package/core/harnesses/shared/hooks/stream-hook-runner.js +114 -187
  85. package/core/harnesses/shared/locator/doflow-run +2 -3
  86. package/core/registry/advice.json +3 -3
  87. package/core/registry/assets.json +72 -80
  88. package/core/registry/capabilities.json +12 -0
  89. package/core/registry/contracts.json +60 -33
  90. package/core/registry/contribution-points.json +32 -0
  91. package/core/registry/gates.json +32 -0
  92. package/core/registry/guidance-caps.json +40 -0
  93. package/core/registry/harnesses.json +1542 -277
  94. package/core/registry/lifecycle.json +2837 -37
  95. package/core/registry/mcp.json +5 -1
  96. package/core/registry/models.json +7 -1
  97. package/core/registry/package-templates/README.md +19 -0
  98. package/core/registry/package-templates/agent/files/agent.md +9 -0
  99. package/core/registry/package-templates/agent/template.json +4 -0
  100. package/core/registry/package-templates/capability-provider/files/provider.json +9 -0
  101. package/core/registry/package-templates/capability-provider/template.json +11 -0
  102. package/core/registry/package-templates/debate-preset/files/preset.json +26 -0
  103. package/core/registry/package-templates/debate-preset/template.json +4 -0
  104. package/core/registry/package-templates/review-preset/files/preset.json +30 -0
  105. package/core/registry/package-templates/review-preset/template.json +4 -0
  106. package/core/registry/package-templates/skill/files/SKILL.md +8 -0
  107. package/core/registry/package-templates/skill/template.json +4 -0
  108. package/core/registry/package-templates/worker/files/worker.json +92 -0
  109. package/core/registry/package-templates/worker/template.json +11 -0
  110. package/core/registry/packages/doflow/doflow-package.json +15 -0
  111. package/core/registry/packages/doflow/presets/debate-3.json +26 -0
  112. package/core/registry/protocols.json +61 -0
  113. package/core/registry/routes.json +17 -0
  114. package/core/registry/rules.json +431 -0
  115. package/core/registry/settings.json +276 -0
  116. package/core/registry/storage.json +701 -30
  117. package/core/registry/telemetry.json +236 -0
  118. package/core/registry/transcripts.json +142 -0
  119. package/core/registry/verification.json +76 -1
  120. package/core/registry/workflows.json +55 -0
  121. package/core/shared/agent-specs/core-implementer.md +3 -0
  122. package/core/shared/agent-specs/delivery-lead.md +68 -0
  123. package/core/shared/agent-specs/quality-guardian.md +5 -1
  124. package/core/shared/agent-specs/research-writer.md +10 -3
  125. package/core/shared/agent-specs/reviewer.md +53 -0
  126. package/core/shared/agent-specs/spec-analyst.md +5 -6
  127. package/core/shared/agent-specs/system-architect.md +5 -1
  128. package/core/shared/guidance/DOFLOW_CORE.md +1 -5
  129. package/core/shared/guidance/modes/MODE_DeepResearch.md +1 -1
  130. package/core/shared/guidance/modes/MODE_Orchestration.md +4 -3
  131. package/core/shared/guidance/modes/MODE_Task_Management.md +14 -14
  132. package/core/shared/guidance/pointers/codex.md +1 -4
  133. package/core/shared/guidance/references/ARTIFACT_FORMAT.md +30 -16
  134. package/core/shared/guidance/references/ARTIFACT_VOICE.md +3 -2
  135. package/core/shared/guidance/references/CONSTITUTION_BASE.md +17 -15
  136. package/core/shared/guidance/references/DIAGRAM_GUIDE.md +1 -1
  137. package/core/shared/guidance/references/DOFLOW_CHAIN.md +17 -19
  138. package/core/shared/guidance/references/EVIDENCE_LEDGER.md +7 -8
  139. package/core/shared/guidance/references/MODEL_SELECTION.md +5 -3
  140. package/core/shared/guidance/references/ORCHESTRATION.md +1 -2
  141. package/core/shared/guidance/rules/RULES.md +45 -0
  142. package/core/shared/scripts/doflow/bash/do-args.sh +37 -0
  143. package/core/shared/scripts/doflow/bash/do-exec-paths.sh +4 -0
  144. package/core/shared/scripts/doflow/bash/do-git-state.sh +15 -4
  145. package/core/shared/scripts/doflow/bash/do-parallel-check.sh +4 -0
  146. package/core/shared/scripts/doflow/bash/do-paths.sh +40 -2
  147. package/core/shared/scripts/doflow/bash/do-prereqs.sh +2 -1
  148. package/core/shared/scripts/doflow/bash/do-review-package.sh +4 -0
  149. package/core/shared/scripts/doflow/bash/do-task-brief.sh +58 -16
  150. package/core/shared/scripts/doflow/bash/render-audit.sh +26 -2
  151. package/core/shared/scripts/doflow/bash/render-diagrams.sh +1 -0
  152. package/core/shared/scripts/doflow/bash/render-puml.sh +2 -1
  153. package/core/shared/scripts/doflow/bash/validate-artifacts.sh +48 -4
  154. package/core/shared/scripts/doflow/bin/doflow-run +154 -35
  155. package/core/shared/skills/do/SKILL.md +12 -10
  156. package/core/shared/skills/do/references/advice_calls.md +26 -0
  157. package/core/shared/skills/do/references/pm_routing.md +1 -2
  158. package/core/shared/skills/do/references/program_mode.md +31 -0
  159. package/core/shared/skills/do-brainstorm/SKILL.md +20 -74
  160. package/core/shared/skills/do-brainstorm/references/handoff_branches.md +35 -0
  161. package/core/shared/skills/do-brainstorm/references/intent_seed.md +21 -0
  162. package/core/shared/skills/do-brainstorm/references/round_log.md +17 -0
  163. package/core/shared/skills/do-code-review/SKILL.md +41 -144
  164. package/core/shared/skills/do-code-review/content-types/markdown.md +24 -2
  165. package/core/shared/skills/do-code-review/references/tools.md +90 -0
  166. package/core/shared/skills/do-code-review/scripts/doc_quality_checker.py +549 -7
  167. package/core/shared/skills/do-design/SKILL.md +24 -23
  168. package/core/shared/skills/do-diagnose/SKILL.md +14 -4
  169. package/core/shared/skills/do-document/SKILL.md +11 -1
  170. package/core/shared/skills/do-document/references/deep_research.md +1 -2
  171. package/core/shared/skills/do-document/references/embedded-research.md +18 -6
  172. package/core/shared/skills/do-document/references/implementation-flow.md +5 -6
  173. package/core/shared/skills/do-execute-plan/SKILL.md +17 -59
  174. package/core/shared/skills/do-execute-plan/references/handoff_record.md +7 -2
  175. package/core/shared/skills/do-execute-plan/references/parallel_dispatch.md +13 -0
  176. package/core/shared/skills/do-execute-plan/references/readiness_gate.md +28 -1
  177. package/core/shared/skills/do-execute-plan/references/run_position.md +13 -1
  178. package/core/shared/skills/do-execute-plan/references/scaffold.md +17 -9
  179. package/core/shared/skills/do-execute-plan/references/state_checkpoint.md +11 -0
  180. package/core/shared/skills/do-execute-plan/references/task_attempts.md +78 -24
  181. package/core/shared/skills/do-flow/SKILL.md +8 -9
  182. package/core/shared/skills/do-flow/references/task_classes.md +2 -2
  183. package/core/shared/skills/do-git/SKILL.md +4 -0
  184. package/core/shared/skills/do-implement/SKILL.md +7 -9
  185. package/core/shared/skills/do-plan/SKILL.md +29 -19
  186. package/core/shared/skills/do-test/SKILL.md +7 -4
  187. package/core/shared/templates/doflow/constitution-template.md +9 -3
  188. package/core/shared/templates/doflow/data-model-template.md +1 -1
  189. package/core/shared/templates/doflow/design-template.md +113 -16
  190. package/core/shared/templates/doflow/plan-template.md +11 -10
  191. package/core/shared/templates/doflow/question-log-template.md +14 -2
  192. package/core/shared/templates/doflow/requirement-template.md +1 -1
  193. package/core/worker-evidence/antigravity/checks.json +14 -0
  194. package/core/worker-evidence/codex/2026-10-18-cli-check.json +107 -0
  195. package/core/worker-evidence/codex/2026-10-18-live-run-edit.json +73 -0
  196. package/core/worker-evidence/codex/2026-10-18-live-run-read-only.json +62 -0
  197. package/core/worker-evidence/codex/2026-10-24-cli-check.json +126 -0
  198. package/core/worker-evidence/codex/checks.json +38 -0
  199. package/package.json +10 -6
  200. package/src/adapters/antigravity/index.js +226 -113
  201. package/src/adapters/claude/index.js +39 -5
  202. package/src/adapters/codex/agents.js +25 -15
  203. package/src/adapters/codex/hooks.js +40 -5
  204. package/src/adapters/codex/index.js +100 -22
  205. package/src/adapters/codex/native-projection.js +6 -2
  206. package/src/adapters/copilot/index.js +58 -23
  207. package/src/adapters/copy-tree.js +294 -48
  208. package/src/adapters/guidance-manifest.js +71 -0
  209. package/src/adapters/guidance-pointer.js +19 -0
  210. package/src/adapters/hook-commands.js +6 -10
  211. package/src/adapters/index.js +1 -0
  212. package/src/adapters/kiro/index.js +38 -13
  213. package/src/adapters/opencode/index.js +8 -8
  214. package/src/adapters/pi/hooks.js +120 -0
  215. package/src/adapters/pi/index.js +48 -16
  216. package/src/adapters/resolve-source.js +26 -2
  217. package/src/cli/commands/install.js +3 -2
  218. package/src/cli/commands/reconcile.js +53 -9
  219. package/src/cli/commands/remove.js +2 -2
  220. package/src/cli/commands/rollback.js +6 -3
  221. package/src/cli/commands/self-update.js +18 -9
  222. package/src/cli/commands/status.js +21 -2
  223. package/src/cli/commands/ui.js +99 -0
  224. package/src/cli/commands/update.js +3 -2
  225. package/src/cli/index.js +338 -33
  226. package/src/cli/runtime-commands.js +243 -52
  227. package/src/cli/shared.js +0 -2
  228. package/src/helper/git.js +1 -1
  229. package/src/helper/guardrails.js +1 -1
  230. package/src/helper/marker-merge.js +31 -13
  231. package/src/helper/package-ids.js +37 -0
  232. package/src/helper/semver.js +94 -0
  233. package/src/install/backup.js +105 -15
  234. package/src/install/context.js +1 -1
  235. package/src/install/targets.js +31 -16
  236. package/src/lifecycle/coverage.js +74 -0
  237. package/src/lifecycle/hook-step.js +61 -0
  238. package/src/lifecycle/hook-tier.js +160 -0
  239. package/src/lifecycle/index.js +434 -86
  240. package/src/lifecycle/retired-harnesses.js +318 -0
  241. package/src/lifecycle/view.js +35 -15
  242. package/src/registry/index.js +686 -32
  243. package/src/registry/worker-ids.js +45 -0
  244. package/src/release/build-package.js +0 -3
  245. package/src/release/docs-blocks.js +492 -0
  246. package/src/release/preflight.js +425 -0
  247. package/src/release/rehearse.js +401 -0
  248. package/src/release/release-workflow.js +733 -0
  249. package/src/release/sync-plugin-versions.js +43 -7
  250. package/src/runtime/advice/bench-gate.js +25 -0
  251. package/src/runtime/advice/cli.js +59 -16
  252. package/src/runtime/advice/client.js +32 -9
  253. package/src/runtime/advice/config.js +98 -133
  254. package/src/runtime/advice/doctor.js +1 -1
  255. package/src/runtime/advice/elevation.js +97 -0
  256. package/src/runtime/advice/endpoint-guard.js +9 -3
  257. package/src/runtime/advice/mode.js +37 -0
  258. package/src/runtime/advice/record.js +18 -5
  259. package/src/runtime/advice/registry.js +4 -1
  260. package/src/runtime/advice/shadow-config.js +229 -0
  261. package/src/runtime/advice/shadow-dispatcher.js +276 -0
  262. package/src/runtime/brief-rules.js +81 -0
  263. package/src/runtime/brief-tools.js +111 -34
  264. package/src/runtime/candidate.js +514 -0
  265. package/src/runtime/capabilities/executor.js +450 -0
  266. package/src/runtime/capabilities/loopback.js +41 -0
  267. package/src/runtime/capabilities/sanitizer.js +88 -0
  268. package/src/runtime/capability-router.js +135 -3
  269. package/src/runtime/checkouts.js +73 -11
  270. package/src/runtime/claims.js +27 -12
  271. package/src/runtime/cli-result.js +11 -4
  272. package/src/runtime/cli.js +221 -277
  273. package/src/runtime/config-home.js +42 -0
  274. package/src/runtime/context/arbitrate.js +767 -0
  275. package/src/runtime/context/budget.js +203 -0
  276. package/src/runtime/context/compose.js +1367 -0
  277. package/src/runtime/context/delivery-store.js +100 -0
  278. package/src/runtime/context/emission-rules.js +118 -0
  279. package/src/runtime/context/identity.js +227 -0
  280. package/src/runtime/context/mandatory.js +594 -0
  281. package/src/runtime/context/pointer.js +142 -0
  282. package/src/runtime/context/primary-command.js +117 -0
  283. package/src/runtime/context/primary.js +344 -0
  284. package/src/runtime/context/summary-store.js +655 -0
  285. package/src/runtime/context-pack.js +1106 -245
  286. package/src/runtime/decision-register.js +13 -4
  287. package/src/runtime/dispatch/admission.js +392 -0
  288. package/src/runtime/dispatch/attempt-end.js +274 -0
  289. package/src/runtime/dispatch/command.js +628 -0
  290. package/src/runtime/dispatch/controls.js +904 -0
  291. package/src/runtime/dispatch/envelope.js +276 -0
  292. package/src/runtime/dispatch/grants.js +310 -0
  293. package/src/runtime/dispatch/integrate.js +617 -0
  294. package/src/runtime/dispatch/loop.js +1835 -0
  295. package/src/runtime/dispatch/store.js +352 -0
  296. package/src/runtime/dispatch/verify-run.js +201 -0
  297. package/src/runtime/evidence-fields.js +39 -0
  298. package/src/runtime/evidence-ledger.js +41 -7
  299. package/src/runtime/failure/capture.js +3 -1
  300. package/src/runtime/failure/cli.js +25 -9
  301. package/src/runtime/failure/home.js +57 -30
  302. package/src/runtime/failure/store.js +7 -2
  303. package/src/runtime/feature-pointer.js +174 -0
  304. package/src/runtime/feature-resolve.js +13 -2
  305. package/src/runtime/fleet/command.js +77 -0
  306. package/src/runtime/fleet/projects.js +178 -0
  307. package/src/runtime/fleet/tree.js +383 -0
  308. package/src/runtime/freshness.js +77 -10
  309. package/src/runtime/git-memo.js +46 -0
  310. package/src/runtime/guidance/ablation.js +427 -0
  311. package/src/runtime/health.js +565 -17
  312. package/src/runtime/history-compactor.js +13 -3
  313. package/src/runtime/implementation-gate.js +277 -22
  314. package/src/runtime/inventory/coverage.js +1 -1
  315. package/src/runtime/inventory/index.js +1 -1
  316. package/src/runtime/inventory/precedence.js +2 -2
  317. package/src/runtime/inventory/siblings.js +6 -4
  318. package/src/runtime/knowledge/code-corpus.js +277 -0
  319. package/src/runtime/knowledge/code-lexical.js +130 -0
  320. package/src/runtime/knowledge/code-retrieval.js +307 -0
  321. package/src/runtime/knowledge/code-validation.js +106 -0
  322. package/src/runtime/knowledge/gp3-gate.js +74 -0
  323. package/src/runtime/knowledge/graph-adapter.js +288 -0
  324. package/src/runtime/knowledge/graph-fallback.js +394 -0
  325. package/src/runtime/knowledge/hybrid.js +136 -8
  326. package/src/runtime/knowledge/retrieval.js +34 -1
  327. package/src/runtime/knowledge/rtk-compress.js +77 -0
  328. package/src/runtime/knowledge/semble-bridge.py +106 -0
  329. package/src/runtime/knowledge/semble-lifecycle.js +114 -0
  330. package/src/runtime/knowledge/semble-provider.js +365 -0
  331. package/src/runtime/lifecycle/cli.js +16 -6
  332. package/src/runtime/lifecycle/event-store.js +8 -6
  333. package/src/runtime/lifecycle/followup.js +6 -1
  334. package/src/runtime/lifecycle/overview.js +24 -1
  335. package/src/runtime/lifecycle/report-store.js +8 -10
  336. package/src/runtime/lifecycle/retention.js +95 -31
  337. package/src/runtime/lifecycle/root.js +9 -1
  338. package/src/runtime/lifecycle/status.js +34 -17
  339. package/src/runtime/lifecycle/store-upkeep.js +56 -7
  340. package/src/runtime/mask.js +134 -7
  341. package/src/runtime/mcp-consultations.js +40 -6
  342. package/src/runtime/model-router.js +79 -7
  343. package/src/runtime/models/catalogue.js +191 -0
  344. package/src/runtime/models/command.js +153 -0
  345. package/src/runtime/off-words.js +12 -0
  346. package/src/runtime/outcome.js +3 -3
  347. package/src/runtime/packages/command.js +1009 -0
  348. package/src/runtime/packages/contributions.js +116 -0
  349. package/src/runtime/packages/digest.js +132 -0
  350. package/src/runtime/packages/fetch.js +182 -0
  351. package/src/runtime/packages/grants.js +192 -0
  352. package/src/runtime/packages/index-file.js +134 -0
  353. package/src/runtime/packages/instruction.js +90 -0
  354. package/src/runtime/packages/manifest.js +331 -0
  355. package/src/runtime/packages/points.js +477 -0
  356. package/src/runtime/packages/programs.js +216 -0
  357. package/src/runtime/packages/scaffold.js +277 -0
  358. package/src/runtime/packages/source.js +90 -0
  359. package/src/runtime/packages/state.js +164 -0
  360. package/src/runtime/packages/store.js +449 -0
  361. package/src/runtime/packages/tar.js +106 -0
  362. package/src/runtime/packages/view.js +406 -0
  363. package/src/runtime/plan-check.js +59 -0
  364. package/src/runtime/plan-content.js +324 -0
  365. package/src/runtime/policy-init.js +118 -0
  366. package/src/runtime/profiles/doctor.js +96 -0
  367. package/src/runtime/profiles/resolve.js +244 -0
  368. package/src/runtime/profiles/rollback.js +254 -0
  369. package/src/runtime/profiles/sampler.js +41 -0
  370. package/src/runtime/readiness-record.js +16 -11
  371. package/src/runtime/readiness.js +127 -16
  372. package/src/runtime/research-request.js +50 -25
  373. package/src/runtime/retrieval-plan.js +402 -73
  374. package/src/runtime/retrieval-record.js +293 -0
  375. package/src/runtime/review/command.js +246 -0
  376. package/src/runtime/review/dominance.js +68 -0
  377. package/src/runtime/review/findings.js +434 -0
  378. package/src/runtime/review/identity.js +255 -0
  379. package/src/runtime/review/protocol-command.js +109 -0
  380. package/src/runtime/review/protocols.js +405 -0
  381. package/src/runtime/review/resolve-protocol.js +302 -0
  382. package/src/runtime/review/start.js +530 -0
  383. package/src/runtime/roadmap/facts.js +479 -0
  384. package/src/runtime/roadmap/gates.js +270 -0
  385. package/src/runtime/roadmap/loop.js +1636 -0
  386. package/src/runtime/roadmap/merge.js +381 -0
  387. package/src/runtime/roadmap/rows.js +1113 -0
  388. package/src/runtime/roadmap/seed.js +951 -0
  389. package/src/runtime/rules-report.js +336 -0
  390. package/src/runtime/scaffold/artifacts.js +63 -9
  391. package/src/runtime/scaffold/generate.js +21 -9
  392. package/src/runtime/settings/autopilot.js +106 -0
  393. package/src/runtime/settings/cli.js +280 -0
  394. package/src/runtime/settings/config-write.js +314 -0
  395. package/src/runtime/settings/files.js +271 -0
  396. package/src/runtime/settings/legacy.js +154 -0
  397. package/src/runtime/settings/lexical.js +56 -0
  398. package/src/runtime/settings/prepared-run.js +269 -0
  399. package/src/runtime/settings/project-layers.js +118 -0
  400. package/src/runtime/settings/refs.js +288 -0
  401. package/src/runtime/settings/registry.js +553 -0
  402. package/src/runtime/settings/resolve.js +713 -0
  403. package/src/runtime/settings/shared.js +37 -0
  404. package/src/runtime/settings/worker-billing.js +107 -0
  405. package/src/runtime/settings/write.js +269 -0
  406. package/src/runtime/slot-command.js +78 -0
  407. package/src/runtime/slots.js +671 -0
  408. package/src/runtime/source-version.js +290 -0
  409. package/src/runtime/state-root.js +217 -0
  410. package/src/runtime/storage/cli.js +12 -5
  411. package/src/runtime/storage/engine.js +9 -2
  412. package/src/runtime/storage/registry.js +14 -1
  413. package/src/runtime/storage/roots.js +66 -16
  414. package/src/runtime/storage/rules.js +82 -10
  415. package/src/runtime/storage/upkeep.js +10 -2
  416. package/src/runtime/storage/work-status.js +245 -26
  417. package/src/runtime/task-attempts.js +589 -69
  418. package/src/runtime/task-classifier.js +47 -11
  419. package/src/runtime/task-graph.js +373 -0
  420. package/src/runtime/task-run.js +778 -0
  421. package/src/runtime/task-scope.js +202 -56
  422. package/src/runtime/task-sources.js +147 -0
  423. package/src/runtime/task-state.js +440 -65
  424. package/src/runtime/team/admission.js +301 -0
  425. package/src/runtime/team/envelope.js +324 -0
  426. package/src/runtime/team/review-guard.js +156 -0
  427. package/src/runtime/trace/analyze.js +427 -0
  428. package/src/runtime/trace/baseline.js +770 -0
  429. package/src/runtime/trace/ledger.js +63 -140
  430. package/src/runtime/trace/otlp-export.js +568 -0
  431. package/src/runtime/trace/otlp-map.js +481 -0
  432. package/src/runtime/trace/spans.js +745 -0
  433. package/src/runtime/trace/telemetry.js +225 -0
  434. package/src/runtime/trace/views.js +1 -0
  435. package/src/runtime/transcripts/command.js +98 -0
  436. package/src/runtime/transcripts/cursor.js +326 -0
  437. package/src/runtime/transcripts/read.js +126 -0
  438. package/src/runtime/transcripts/readers/claude-jsonl.js +74 -0
  439. package/src/runtime/transcripts/readers/codex-rollout.js +70 -0
  440. package/src/runtime/transcripts/readers/copilot-events.js +90 -0
  441. package/src/runtime/transcripts/readers/kiro-jsonl.js +77 -0
  442. package/src/runtime/transcripts/readers/pi-jsonl.js +59 -0
  443. package/src/runtime/transcripts/registry.js +161 -0
  444. package/src/runtime/transcripts/stats.js +161 -0
  445. package/src/runtime/verification/check-key.js +628 -0
  446. package/src/runtime/verification/contract-runner.js +546 -11
  447. package/src/runtime/verification/engine.js +499 -9
  448. package/src/runtime/verification/impact.js +617 -0
  449. package/src/runtime/verification/inflight.js +205 -0
  450. package/src/runtime/verification/registry.js +176 -0
  451. package/src/runtime/verification/reuse.js +184 -0
  452. package/src/runtime/verification/telemetry-emit.js +254 -0
  453. package/src/runtime/verification/test-selection.js +67 -7
  454. package/src/runtime/worker/active.js +90 -0
  455. package/src/runtime/worker/cancel.js +222 -0
  456. package/src/runtime/worker/catalogue.js +122 -0
  457. package/src/runtime/worker/command.js +677 -0
  458. package/src/runtime/worker/evidence.js +288 -0
  459. package/src/runtime/worker/launch.js +288 -0
  460. package/src/runtime/worker/models.js +137 -0
  461. package/src/runtime/worker/parsers.js +218 -0
  462. package/src/runtime/worker/probes/index.js +706 -0
  463. package/src/runtime/worker/process-tree.js +415 -0
  464. package/src/runtime/worker/quota.js +43 -0
  465. package/src/runtime/worker/readiness.js +713 -0
  466. package/src/runtime/worker/run-store.js +166 -0
  467. package/src/runtime/worker/supervisor.js +541 -0
  468. package/src/runtime/worker/worktree-check.js +252 -0
  469. package/src/runtime/workflow-engine.js +5 -1
  470. package/src/runtime/workflow-orchestrator.js +578 -63
  471. package/src/runtime/worktree.js +149 -22
  472. package/src/state/index.js +1 -1
  473. package/src/state/lockfile.js +64 -10
  474. package/src/state/run-lock.js +1 -1
  475. package/types/node-commonjs.d.ts +106 -7
  476. package/core/.antigravity-plugin/hooks.json +0 -39
  477. package/core/harnesses/gemini/hooks/hooks.json +0 -77
  478. package/core/harnesses/gemini/hooks/lib.sh +0 -302
  479. package/core/harnesses/gemini/hooks/mcp-tool-guard.sh +0 -21
  480. package/core/harnesses/gemini/hooks/post-edit-lint.sh +0 -39
  481. package/core/harnesses/gemini/hooks/pre-bash-guard.sh +0 -21
  482. package/core/harnesses/gemini/hooks/pre-compact.sh +0 -38
  483. package/core/harnesses/gemini/hooks/pre-implement-gate.sh +0 -22
  484. package/core/harnesses/gemini/hooks/session-end.impl.sh +0 -89
  485. package/core/harnesses/gemini/hooks/session-end.sh +0 -5
  486. package/core/harnesses/gemini/hooks/session-start.sh +0 -5
  487. package/core/harnesses/gemini/settings/adapter-defaults.json +0 -4
  488. package/core/harnesses/shared/hooks/adapters/arg-fields.js +0 -19
  489. package/core/harnesses/shared/hooks/adapters/gemini.js +0 -114
  490. package/core/harnesses/shared/hooks/hooks.json +0 -47
  491. package/core/shared/guidance/FLAGS.md +0 -25
  492. package/core/shared/guidance/rules/RULE_01_SAFETY.md +0 -25
  493. package/core/shared/guidance/rules/RULE_02_WORKFLOW.md +0 -23
  494. package/core/shared/guidance/rules/RULE_03_QUALITY.md +0 -14
  495. package/core/shared/guidance/rules/RULE_04_QUESTIONS.md +0 -46
  496. package/src/adapters/gemini/hooks.js +0 -210
  497. package/src/adapters/gemini/index.js +0 -390
@@ -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,19 +98,24 @@ 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,
106
+ coverage: loaded.lifecycle.coverage ?? null,
86
107
  contracts: loaded.contracts.contracts,
87
108
  externalTools: loaded.externalTools.tools,
88
109
  modelProviders: loaded.models.providers,
89
110
  modelRoles: loaded.models.roles,
90
- 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,
91
113
  dispatchTiers: loaded.models.dispatchTiers,
92
114
  capabilities,
93
115
  versions: Object.fromEntries((Object.entries(loaded) ).map(([name, value]) => [name, value.version])),
94
116
  };
95
- 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 });
96
119
  if (!validation.ok) {
97
120
  const error = new Error(`Invalid DoFlow registry:\n- ${validation.errors.join('\n- ')}`);
98
121
  error.validation = validation;
@@ -117,11 +140,23 @@ function validateContract(value , location , errors , harnes
117
140
  if (!CONTRACT_COMPLETENESS.has(value.completeness)) {
118
141
  issue(errors, location, 'completeness must be verified or lower-bound');
119
142
  }
120
- // skillFields/agentFields are optional: shared skills and agent specs are byte-identical across
121
- // harnesses, so a per-harness field list is necessarily a subset of the union and cannot change
122
- // any guard outcome. Declaring one anyway would restate an unverified claim as fact — the exact
123
- // shape of inert declaration these guards exist to remove. hookEvents is required because those
124
- // 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
+ }
125
160
  for (const field of ['skillFields', 'agentFields']) {
126
161
  if (value[field] === undefined) continue;
127
162
  const list = value[field];
@@ -132,11 +167,10 @@ function validateContract(value , location , errors , harnes
132
167
  if (new Set(list).size !== list.length) issue(errors, `${location}.${field}`, 'must not contain duplicates');
133
168
  }
134
169
  // An empty hookEvents used to be rejected outright, which assumed every harness wires at least one
135
- // hook. OpenCode and Pi break that: both implement lifecycle handlers as JS/TS code modules, so
136
- // DoFlow's shell hooks have nothing to attach to and it wires none. Forcing a non-empty list there
137
- // would mean declaring a hook DoFlow does not install — an inert claim of exactly the kind these
138
- // guards exist to remove. Empty is therefore allowed, but only with a note, so the distinction
139
- // between "wires nothing, deliberately" and "nobody filled this in" stays visible.
170
+ // hook. OpenCode has no DoFlow hook projection; Pi uses a code extension only for tool_call.
171
+ // Forcing a non-empty list for an unsupported event would declare a hook DoFlow does not install —
172
+ // an inert claim of exactly the kind these guards exist to remove. Empty is therefore allowed, but
173
+ // only with a note, so deliberate gaps stay distinct from missing registry data.
140
174
  const events = value.hookEvents;
141
175
  const noted = typeof value.note === 'string' && value.note.trim().length > 0;
142
176
  if (!Array.isArray(events) || events.some((item) => typeof item !== 'string' || !item)) {
@@ -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,7 +456,439 @@ function validateOrchestration(value , location , errors )
412
456
  }
413
457
  }
414
458
 
415
- function validateRegistry(registry , { repoRoot, fsImpl = fs } = {}) {
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
+
748
+ const CONTEXT_PROFILE_FIELDS = Object.freeze([
749
+ 'capacityTokens',
750
+ 'reserveTokens',
751
+ 'tokenTelemetry',
752
+ 'toolResultLimitBytes',
753
+ 'retention',
754
+ ]);
755
+
756
+ /** Validates one harness's `contextProfile` block (075 IC-013): capacity, reserve, telemetry,
757
+ * toolResultLimitBytes and retention. Each is a Fact<T> with {value, source, verifiedOn} or
758
+ * {unverified, verifiedOn}. */
759
+ function validateContextProfile(value , location , errors ) {
760
+ if (!object(value)) {
761
+ issue(errors, location, 'contextProfile block is required and must be an object');
762
+ return;
763
+ }
764
+ for (const field of CONTEXT_PROFILE_FIELDS) {
765
+ if (!Object.hasOwn(value, field)) {
766
+ issue(errors, location, `contextProfile is missing field '${field}'`);
767
+ }
768
+ }
769
+ for (const key of Object.keys(value)) {
770
+ if (!CONTEXT_PROFILE_FIELDS.includes(key)) {
771
+ issue(errors, location, `contextProfile has unknown key '${key}'`);
772
+ }
773
+ }
774
+ for (const field of CONTEXT_PROFILE_FIELDS) {
775
+ if (!Object.hasOwn(value, field)) continue;
776
+ const at = `${location} contextProfile '${field}'`;
777
+ const fact = value[field];
778
+ if (!object(fact)) {
779
+ issue(errors, at, 'must be a Fact object');
780
+ continue;
781
+ }
782
+ const hasUnverified = Object.hasOwn(fact, 'unverified');
783
+ const hasValue = Object.hasOwn(fact, 'value');
784
+
785
+ if (hasUnverified) {
786
+ if (typeof fact.unverified !== 'string' || !fact.unverified.trim()) {
787
+ issue(errors, at, 'unverified must be a non-empty string');
788
+ }
789
+ if (hasValue) {
790
+ issue(errors, at, 'cannot declare both value and unverified');
791
+ }
792
+ } else if (hasValue) {
793
+ if (typeof fact.source !== 'string' || !fact.source.trim()) {
794
+ issue(errors, at, 'a declared value requires a non-empty source');
795
+ }
796
+ } else {
797
+ issue(errors, at, 'requires either value with source or a non-empty unverified');
798
+ }
799
+
800
+ if (
801
+ typeof fact.verifiedOn !== 'string' ||
802
+ !/^\d{4}-\d{2}-\d{2}$/.test(fact.verifiedOn) ||
803
+ Number.isNaN(Date.parse(fact.verifiedOn)) ||
804
+ new Date(fact.verifiedOn).toISOString().slice(0, 10) !== fact.verifiedOn
805
+ ) {
806
+ issue(errors, at, 'requires verifiedOn as an ISO date (YYYY-MM-DD)');
807
+ }
808
+
809
+ if (hasValue && fact.value !== undefined) {
810
+ const val = fact.value;
811
+ if (field === 'capacityTokens') {
812
+ if (typeof val !== 'number' || !Number.isInteger(val) || val <= 0) {
813
+ issue(errors, at, 'value must be a positive integer');
814
+ }
815
+ } else if (field === 'reserveTokens') {
816
+ if (typeof val !== 'number' || !Number.isInteger(val) || val < 0) {
817
+ issue(errors, at, 'value must be a non-negative integer');
818
+ }
819
+ } else if (field === 'tokenTelemetry') {
820
+ if (val !== 'harness-reported' && val !== 'none') {
821
+ issue(errors, at, "value must be 'harness-reported' or 'none'");
822
+ }
823
+ } else if (field === 'toolResultLimitBytes') {
824
+ if (typeof val !== 'number' || !Number.isInteger(val) || val <= 0) {
825
+ issue(errors, at, 'value must be a positive integer');
826
+ }
827
+ } else if (field === 'retention') {
828
+ if (val !== 'supported' && val !== 'unsupported') {
829
+ issue(errors, at, "value must be 'supported' or 'unsupported'");
830
+ }
831
+ }
832
+ }
833
+ }
834
+ }
835
+
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
+ }
416
892
  const errors = [];
417
893
  const harnesses = Array.isArray(registry?.harnesses) ? registry.harnesses : null;
418
894
  const assets = Array.isArray(registry?.assets) ? registry.assets : null;
@@ -430,11 +906,12 @@ function validateRegistry(registry , { repoRoot, fsImpl = fs }
430
906
  idsUnique(mcp, 'mcp', errors); idsUnique(lifecycle, 'lifecycle', errors);
431
907
  idsUnique(externalTools, 'externalTools', errors);
432
908
 
433
- const harnessIds = new Set((harnesses || []).filter(object).map((item ) => item.id));
909
+ const harnessIds = new Set ((harnesses || []).filter(object).map((item ) => item.id ));
434
910
  for (const harness of harnesses || []) {
435
911
  const at = `harness '${harness?.id ?? '?'}'`;
436
912
  if (!object(harness)) { issue(errors, 'harnesses', 'entries must be objects'); continue; }
437
- 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`);
438
915
  if (typeof harness.displayName !== 'string' || !harness.displayName.trim()) issue(errors, at, 'requires displayName');
439
916
  if (typeof harness.adapter !== 'string' || !harness.adapter.trim()) issue(errors, at, 'requires adapter');
440
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');
@@ -451,9 +928,23 @@ function validateRegistry(registry , { repoRoot, fsImpl = fs }
451
928
  validateCapability(declaration, `${at} capability '${capability}'`, errors);
452
929
  }
453
930
  validateOrchestration(harness.orchestration, at, errors);
454
- if (harness.id === 'codex') validateNativeProjection(harness.nativeProjection, harness, { repoRoot, fsImpl }, 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
+ }
941
+ validateContextProfile(harness.contextProfile, at, 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);
455
944
  }
456
945
 
946
+ validateRetired(registry?.retired, harnessIds, errors);
947
+
457
948
  const contractHarnesses = new Set();
458
949
  for (const contract of contracts || []) {
459
950
  validateContract(contract, `contract '${contract?.harness ?? '?'}'`, errors, harnessIds);
@@ -474,7 +965,8 @@ function validateRegistry(registry , { repoRoot, fsImpl = fs }
474
965
  for (const field of ['id', 'kind', 'source', 'ownership']) if (typeof asset[field] !== 'string' || !asset[field].trim()) issue(errors, at, `requires ${field}`);
475
966
  if (typeof asset.source === 'string' && repoRoot) {
476
967
  const source = path.resolve(repoRoot, asset.source);
477
- 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}`);
478
970
  }
479
971
  if (!Array.isArray(asset.appliesTo) || asset.appliesTo.length === 0) issue(errors, at, 'appliesTo must be a non-empty array');
480
972
  if (!object(asset.projection)) issue(errors, at, 'projection must be an object');
@@ -488,6 +980,12 @@ function validateRegistry(registry , { repoRoot, fsImpl = fs }
488
980
  const capability = harness?.capabilities?.[projection.capability];
489
981
  if (!capability) issue(errors, at, `projection '${harnessId}' references unknown capability '${projection.capability}'`);
490
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
+ }
491
989
  }
492
990
  }
493
991
 
@@ -520,6 +1018,8 @@ function validateRegistry(registry , { repoRoot, fsImpl = fs }
520
1018
  if (mapping.status === 'unavailable' && typeof mapping.fallback !== 'string') issue(errors, at, `unavailable mapping '${harnessId}' requires fallback guidance`);
521
1019
  }
522
1020
  }
1021
+
1022
+ validateCoverage(registry?.coverage, registry?.lifecycle, harnessIds, errors);
523
1023
  for (const tool of externalTools || []) validateExternalTool(tool, `external tool '${tool?.id ?? '?'}'`, errors);
524
1024
 
525
1025
  const modelProviders = Array.isArray(registry?.modelProviders) ? registry.modelProviders : null;
@@ -542,9 +1042,163 @@ function validateRegistry(registry , { repoRoot, fsImpl = fs }
542
1042
  const providerIds = new Set((modelProviders || []).filter(object).map((item ) => item.id));
543
1043
  for (const slot of modelSlots) validateModelSlot(slot, `model slot '${slot?.id ?? '?'}'`, errors, providerIds);
544
1044
  }
545
- return { ok: errors.length === 0, errors };
1045
+ return { ok: errors.length === 0, errors, sources };
1046
+ }
1047
+
1048
+ const COVERAGE_CLASSES = new Set(['blocking', 'observation', 'advisory', 'unsupported', 'unknown']);
1049
+ const COVERAGE_TIMINGS = new Set(['pre-tool', 'post-tool', 'session', 'prompt', 'stop', 'compact', 'subagent']);
1050
+ const COVERAGE_PROFILES = new Set(['any', 'user-interactive', 'user-headless', 'project-interactive', 'project-headless', 'cli-extension', 'no-extensions', 'ide', 'cli-v3', 'cli-v3-agent', 'cli-2x-agent']);
1051
+ const COVERAGE_KINDS = new Set(['doc', 'types', 'fixture', 'live']);
1052
+
1053
+ function validateCoverage(value , lifecycle , harnessIds , errors ) {
1054
+ if (value === undefined || value === null) return;
1055
+ const at = 'lifecycle.coverage';
1056
+ if (!object(value)) { issue(errors, at, 'must be an object'); return; }
1057
+ if (value.schemaVersion !== 1) issue(errors, `${at}.schemaVersion`, 'must be 1');
1058
+ if (!Number.isInteger(value.revision) || value.revision < 1) issue(errors, `${at}.revision`, 'must be a positive integer');
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');
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');
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);
1064
+ if (!Array.isArray(value.rows)) { issue(errors, `${at}.rows`, 'must be an array'); return; }
1065
+ const keys = new Set ();
1066
+ for (const [index, row] of value.rows.entries()) {
1067
+ const rowAt = `${at}.rows[${index}]`;
1068
+ if (!object(row)) { issue(errors, rowAt, 'must be an object'); continue; }
1069
+ for (const field of ['policy', 'harness', 'event']) if (typeof row[field] !== 'string' || !row[field]) issue(errors, `${rowAt}.${field}`, 'must be a non-empty string');
1070
+ if (row.policy !== '*' && !(lifecycle || []).some((policy ) => policy.id === row.policy)) issue(errors, `${rowAt}.policy`, 'must name a lifecycle policy or *');
1071
+ if (!harnessIds.has(row.harness)) issue(errors, `${rowAt}.harness`, 'must name a registry harness');
1072
+ if (!COVERAGE_TIMINGS.has(row.timing)) issue(errors, `${rowAt}.timing`, 'has an invalid timing');
1073
+ if (!COVERAGE_PROFILES.has(row.profile)) issue(errors, `${rowAt}.profile`, 'has an invalid profile');
1074
+ if (typeof row.wired !== 'boolean') issue(errors, `${rowAt}.wired`, 'must be a boolean');
1075
+ if (row.configSource !== null && (typeof row.configSource !== 'string' || !row.configSource)) issue(errors, `${rowAt}.configSource`, 'must be a path:line or null');
1076
+ if (row.wired && typeof row.configSource !== 'string') issue(errors, `${rowAt}.configSource`, 'is required when wired');
1077
+ if (!object(row.harnessVersion) || typeof row.harnessVersion.tested !== 'string' || !row.harnessVersion.tested || (row.harnessVersion.range !== undefined && typeof row.harnessVersion.range !== 'string')) issue(errors, `${rowAt}.harnessVersion`, 'requires tested and optional range strings');
1078
+ if (!COVERAGE_CLASSES.has(row.class)) issue(errors, `${rowAt}.class`, 'has an invalid class');
1079
+ if (!['verified', 'unverified'].includes(row.verification)) issue(errors, `${rowAt}.verification`, 'must be verified or unverified');
1080
+ for (const field of ['prerequisites', 'covers', 'doesNotCover', 'caveats']) if (!Array.isArray(row[field]) || row[field].some((item ) => typeof item !== 'string')) issue(errors, `${rowAt}.${field}`, 'must be an array of strings');
1081
+ if (!Array.isArray(row.provenance) || row.provenance.some((item ) => !object(item) || !COVERAGE_KINDS.has(item.kind) || typeof item.locator !== 'string' || !item.locator || (['doc', 'types', 'live'].includes(item.kind) && (typeof item.date !== 'string' || !/^\d{4}-\d{2}-\d{2}$/.test(item.date))))) issue(errors, `${rowAt}.provenance`, 'contains malformed provenance');
1082
+ const key = JSON.stringify([row.policy, row.harness, row.event, row.profile]);
1083
+ if (keys.has(key)) issue(errors, rowAt, 'duplicates a policy/harness/event/profile tuple');
1084
+ keys.add(key);
1085
+ }
1086
+ }
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;
546
1198
  }
547
1199
 
1200
+ function isRetired(registry , id ) { return retiredFor(registry, id) !== null; }
1201
+
548
1202
  function harnessFor(registry , id ) {
549
1203
  const harness = registry.harnesses.find((item ) => item.id === id);
550
1204
  if (!harness) throw new Error(`Unknown registry harness '${id}'`);
@@ -605,7 +1259,7 @@ function capabilityMapData(registry ) {
605
1259
  }
606
1260
 
607
1261
  module.exports = {
608
- REGISTRY_FILES, CAPABILITY_STATUS, ORCHESTRATION_STATUS, ORCHESTRATION_OPS, validateOrchestration, PROVIDER_SOURCE_MODES, validateCapabilityProviders, EXTERNAL_TOOL_IDS, EXTERNAL_TOOL_ACTIONS,
609
- parseRegistryFile, registryDir, loadRegistry, validateRegistry,
610
- 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,
611
1265
  };