create-agent-rig 0.9.1 → 0.10.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 (234) hide show
  1. package/CHANGELOG.md +419 -3
  2. package/README.md +267 -305
  3. package/package.json +8 -14
  4. package/packages/cli/dist/commands/create.js +56 -88
  5. package/packages/cli/dist/commands/doctor.js +213 -0
  6. package/packages/cli/dist/commands/init.js +111 -50
  7. package/packages/cli/dist/commands/integrations.js +468 -0
  8. package/packages/cli/dist/commands/setup-wizard.js +61 -0
  9. package/packages/cli/dist/commands/uninstall.js +1223 -0
  10. package/packages/cli/dist/commands/upgrade.js +472 -90
  11. package/packages/cli/dist/index.js +651 -46
  12. package/packages/cli/dist/integrations/declaration.js +158 -0
  13. package/packages/cli/dist/integrations/doctor-guards.js +126 -0
  14. package/packages/cli/dist/integrations/doctor-workflow.js +25 -0
  15. package/packages/cli/dist/integrations/mcp-json.js +107 -0
  16. package/packages/cli/dist/integrations/memory-doctor.js +124 -0
  17. package/packages/cli/dist/integrations/registry.js +22 -0
  18. package/packages/cli/dist/integrations/spawn.js +228 -0
  19. package/packages/cli/dist/integrations/spec-kit.js +280 -0
  20. package/packages/cli/dist/integrations/verify.js +193 -0
  21. package/packages/cli/dist/integrations/windows-job.js +275 -0
  22. package/packages/cli/dist/lib/elevated-paths.js +74 -0
  23. package/packages/cli/dist/lib/install-set.js +1 -44
  24. package/packages/cli/dist/lib/manifest.js +64 -5
  25. package/packages/cli/dist/lib/prompts.js +0 -23
  26. package/packages/cli/dist/lib/safe-path.js +193 -2
  27. package/packages/cli/dist/lib/safe-text.js +78 -0
  28. package/packages/cli/dist/lib/substitute.js +1 -52
  29. package/packages/cli/dist/lib/summary.js +5 -5
  30. package/packages/cli/dist/templates.js +4 -14
  31. package/scripts/prepare.mjs +28 -12
  32. package/templates/agent-os/subagent-routing.json +4 -4
  33. package/templates/agent-os/universal/.agents/skills/check-premises/SKILL.md +13 -9
  34. package/templates/agent-os/universal/.agents/skills/loop/SKILL.md +31 -6
  35. package/templates/agent-os/universal/.agents/skills/new-invariant/SKILL.md +5 -5
  36. package/templates/agent-os/universal/.agents/skills/pr-ship/SKILL.md +1 -1
  37. package/templates/agent-os/universal/.agents/skills/worktree-task/SKILL.md +3 -3
  38. package/templates/agent-os/universal/.claude/agents/code-reviewer.md +8 -6
  39. package/templates/agent-os/universal/.claude/agents/implementation-agent.md +41 -0
  40. package/templates/agent-os/universal/.claude/agents/prose-reviewer.md +8 -4
  41. package/templates/agent-os/universal/.claude/agents/security-scanner.md +4 -2
  42. package/templates/agent-os/universal/.claude/agents/test-writer.md +2 -2
  43. package/templates/agent-os/universal/.claude/hooks/inject-rules.mjs +83 -4
  44. package/templates/agent-os/universal/.claude/rules/autonomy.md +55 -27
  45. package/templates/agent-os/universal/.claude/rules/invariants.md +9 -8
  46. package/templates/agent-os/universal/.claude/rules/workflow.md +47 -22
  47. package/templates/agent-os/universal/.claude/scripts/decision-router.mjs +1 -1
  48. package/templates/agent-os/universal/.claude/scripts/detect-missed-gate.mjs +12 -9
  49. package/templates/agent-os/universal/.claude/scripts/doctor.mjs +100 -2
  50. package/templates/agent-os/universal/.claude/scripts/lib/claim-records.mjs +236 -6
  51. package/templates/agent-os/universal/.claude/scripts/queue/state.mjs +2 -2
  52. package/templates/agent-os/universal/.claude/scripts/reconcile-external-prs.mjs +4 -3
  53. package/templates/agent-os/universal/.claude/scripts/revalidate.mjs +16 -0
  54. package/templates/agent-os/universal/.claude/settings.json +0 -8
  55. package/templates/agent-os/universal/.claude/skills/check-premises/SKILL.md +13 -9
  56. package/templates/agent-os/universal/.claude/skills/loop/SKILL.md +31 -6
  57. package/templates/agent-os/universal/.claude/skills/new-invariant/SKILL.md +5 -5
  58. package/templates/agent-os/universal/.claude/skills/pr-ship/SKILL.md +1 -1
  59. package/templates/agent-os/universal/.claude/skills/worktree-task/SKILL.md +3 -3
  60. package/templates/agent-os/universal/.codex/agents/code-reviewer.toml +2 -2
  61. package/templates/agent-os/universal/.codex/agents/implementation-agent.toml +6 -0
  62. package/templates/agent-os/universal/.codex/agents/prose-reviewer.toml +2 -2
  63. package/templates/agent-os/universal/.codex/agents/security-scanner.toml +1 -1
  64. package/templates/agent-os/universal/.codex/agents/test-writer.toml +1 -1
  65. package/templates/agent-os/universal/.codex/hooks.json +0 -10
  66. package/templates/agent-os/universal/AGENTS.md +208 -107
  67. package/templates/agent-os/universal/CLAUDE.md +15 -162
  68. package/templates/agent-os/universal/PLAN.md +17 -10
  69. package/templates/agent-os/universal/docs/decisions/agents-md-canonical.md +313 -0
  70. package/templates/agent-os/universal/docs/decisions/codex-adapter.md +15 -10
  71. package/templates/agent-os/universal/docs/decisions/review-lanes.md +12 -8
  72. package/templates/agent-os/universal/docs/decisions/session-start-wire-format.md +206 -0
  73. package/templates/agent-os/universal/docs/decisions/subagent-routing.md +6 -3
  74. package/templates/agent-os/universal/docs/decisions/workflow-layer-split.md +235 -0
  75. package/templates/agent-os/universal/layers.json +25 -30
  76. package/templates/hash-history.json +8 -4
  77. package/templates/release-ledger.json +2 -1
  78. package/packages/cli/dist/lib/composition.js +0 -20
  79. package/packages/cli/dist/lib/targets.js +0 -28
  80. package/packages/cli/dist/policy/benchmark/corpus.js +0 -165
  81. package/packages/cli/dist/policy/core/adapter.js +0 -18
  82. package/packages/cli/dist/policy/core/coverage.js +0 -253
  83. package/packages/cli/dist/policy/core/decision-record.js +0 -287
  84. package/packages/cli/dist/policy/core/declaration.js +0 -127
  85. package/packages/cli/dist/policy/core/evidence-matrix.js +0 -94
  86. package/packages/cli/dist/policy/core/probe.js +0 -442
  87. package/packages/cli/dist/policy/core/registry.js +0 -115
  88. package/packages/cli/dist/policy/core/validation.js +0 -275
  89. package/packages/cli/dist/policy/core/vocabulary.js +0 -123
  90. package/packages/cli/dist/policy/harness/claude.js +0 -47
  91. package/packages/cli/dist/policy/harness/codex.js +0 -87
  92. package/packages/cli/dist/policy/harness/index.js +0 -15
  93. package/packages/cli/dist/policy/harness/shared-hooks.js +0 -28
  94. package/packages/cli/dist/policy/index.js +0 -17
  95. package/templates/agent-os/init/AGENTS.md +0 -201
  96. package/templates/agent-os/init/CLAUDE.md +0 -201
  97. package/templates/agent-os/stack/aws-cdk/.agents/skills/post-deploy-verify/SKILL.md +0 -105
  98. package/templates/agent-os/stack/aws-cdk/.agents/skills/ro-debug/SKILL.md +0 -117
  99. package/templates/agent-os/stack/aws-cdk/.claude/agents/cdk-diff-reviewer.md +0 -89
  100. package/templates/agent-os/stack/aws-cdk/.claude/rules/aws-cdk.md +0 -105
  101. package/templates/agent-os/stack/aws-cdk/.claude/skills/post-deploy-verify/SKILL.md +0 -105
  102. package/templates/agent-os/stack/aws-cdk/.claude/skills/ro-debug/SKILL.md +0 -117
  103. package/templates/agent-os/stack/aws-cdk/.codex/agents/cdk-diff-reviewer.toml +0 -6
  104. package/templates/agent-os/stack/node-ts/.claude/hooks/dod-checks.json +0 -1
  105. package/templates/agent-os/stack/node-ts/.claude/rules/node-ts.md +0 -85
  106. package/templates/agent-os/universal/.claude/hooks/guard-core-purity.mjs +0 -89
  107. package/templates/agent-os/universal/.claude/hooks/guard-web-boundary.mjs +0 -68
  108. package/templates/agent-os/universal/.claude/rules/architecture.md +0 -81
  109. package/templates/skeleton/aws-serverless/.github/workflows/ci.yml +0 -28
  110. package/templates/skeleton/aws-serverless/.github/workflows/deploy.yml +0 -90
  111. package/templates/skeleton/aws-serverless/README.md +0 -181
  112. package/templates/skeleton/aws-serverless/apps/web/next.config.mjs +0 -17
  113. package/templates/skeleton/aws-serverless/apps/web/package.json +0 -19
  114. package/templates/skeleton/aws-serverless/apps/web/src/app/layout.tsx +0 -17
  115. package/templates/skeleton/aws-serverless/apps/web/src/app/page.tsx +0 -96
  116. package/templates/skeleton/aws-serverless/apps/web/src/lib/api.ts +0 -36
  117. package/templates/skeleton/aws-serverless/apps/web/src/lib/validate.ts +0 -23
  118. package/templates/skeleton/aws-serverless/apps/web/test/shared-validation.test.ts +0 -38
  119. package/templates/skeleton/aws-serverless/apps/web/tsconfig.json +0 -14
  120. package/templates/skeleton/aws-serverless/eslint.config.mjs +0 -20
  121. package/templates/skeleton/aws-serverless/gitignore +0 -56
  122. package/templates/skeleton/aws-serverless/infra/bin/app.ts +0 -79
  123. package/templates/skeleton/aws-serverless/infra/cdk.json +0 -3
  124. package/templates/skeleton/aws-serverless/infra/lib/app-stack.ts +0 -248
  125. package/templates/skeleton/aws-serverless/infra/lib/web-stack.ts +0 -45
  126. package/templates/skeleton/aws-serverless/infra/package.json +0 -18
  127. package/templates/skeleton/aws-serverless/infra/test/allowed-origins.test.ts +0 -301
  128. package/templates/skeleton/aws-serverless/infra/test/app-composition.test.ts +0 -137
  129. package/templates/skeleton/aws-serverless/infra/test/app-stack.test.ts +0 -120
  130. package/templates/skeleton/aws-serverless/infra/test/web-stack.test.ts +0 -44
  131. package/templates/skeleton/aws-serverless/package.json +0 -30
  132. package/templates/skeleton/aws-serverless/packages/core/package.json +0 -11
  133. package/templates/skeleton/aws-serverless/packages/core/src/events.ts +0 -14
  134. package/templates/skeleton/aws-serverless/packages/core/src/index.ts +0 -15
  135. package/templates/skeleton/aws-serverless/packages/core/src/note.ts +0 -69
  136. package/templates/skeleton/aws-serverless/packages/core/test/events.test.ts +0 -23
  137. package/templates/skeleton/aws-serverless/packages/core/test/note.test.ts +0 -101
  138. package/templates/skeleton/aws-serverless/packages/db/package.json +0 -14
  139. package/templates/skeleton/aws-serverless/packages/db/src/client.ts +0 -17
  140. package/templates/skeleton/aws-serverless/packages/db/src/index.ts +0 -2
  141. package/templates/skeleton/aws-serverless/packages/db/src/note-model.ts +0 -52
  142. package/templates/skeleton/aws-serverless/packages/db/test/note-model.test.ts +0 -91
  143. package/templates/skeleton/aws-serverless/packages/shared/package.json +0 -11
  144. package/templates/skeleton/aws-serverless/packages/shared/src/env.ts +0 -17
  145. package/templates/skeleton/aws-serverless/packages/shared/src/errors.ts +0 -33
  146. package/templates/skeleton/aws-serverless/packages/shared/src/index.ts +0 -3
  147. package/templates/skeleton/aws-serverless/packages/shared/src/logger.ts +0 -20
  148. package/templates/skeleton/aws-serverless/packages/shared/test/env.test.ts +0 -26
  149. package/templates/skeleton/aws-serverless/packages/shared/test/errors.test.ts +0 -28
  150. package/templates/skeleton/aws-serverless/packages/shared/test/logger.test.ts +0 -19
  151. package/templates/skeleton/aws-serverless/pnpm-lock.yaml +0 -2855
  152. package/templates/skeleton/aws-serverless/pnpm-workspace.yaml +0 -14
  153. package/templates/skeleton/aws-serverless/services/api/package.json +0 -15
  154. package/templates/skeleton/aws-serverless/services/api/src/adapters/sqs-publisher.ts +0 -26
  155. package/templates/skeleton/aws-serverless/services/api/src/handlers/create-note.ts +0 -69
  156. package/templates/skeleton/aws-serverless/services/api/src/handlers/list-notes.ts +0 -37
  157. package/templates/skeleton/aws-serverless/services/api/src/list-main.ts +0 -12
  158. package/templates/skeleton/aws-serverless/services/api/src/main.ts +0 -21
  159. package/templates/skeleton/aws-serverless/services/api/src/usecases/create-note.ts +0 -41
  160. package/templates/skeleton/aws-serverless/services/api/src/usecases/list-notes.ts +0 -14
  161. package/templates/skeleton/aws-serverless/services/api/test/create-note.handler.test.ts +0 -211
  162. package/templates/skeleton/aws-serverless/services/api/test/create-note.usecase.test.ts +0 -45
  163. package/templates/skeleton/aws-serverless/services/api/test/list-notes.test.ts +0 -122
  164. package/templates/skeleton/aws-serverless/services/api/test/sqs-publisher.test.ts +0 -22
  165. package/templates/skeleton/aws-serverless/services/worker/package.json +0 -12
  166. package/templates/skeleton/aws-serverless/services/worker/src/handlers/note-created.ts +0 -15
  167. package/templates/skeleton/aws-serverless/services/worker/src/main.ts +0 -7
  168. package/templates/skeleton/aws-serverless/services/worker/src/usecases/process-note-created.ts +0 -37
  169. package/templates/skeleton/aws-serverless/services/worker/test/note-created.test.ts +0 -61
  170. package/templates/skeleton/aws-serverless/tsconfig.base.json +0 -15
  171. package/templates/skeleton/aws-serverless/tsconfig.json +0 -16
  172. package/templates/skeleton/aws-serverless/vitest.config.ts +0 -14
  173. package/templates/skeleton/node-service/.github/workflows/ci.yml +0 -27
  174. package/templates/skeleton/node-service/.github/workflows/deploy.yml +0 -29
  175. package/templates/skeleton/node-service/README.md +0 -104
  176. package/templates/skeleton/node-service/apps/web/next.config.mjs +0 -17
  177. package/templates/skeleton/node-service/apps/web/package.json +0 -19
  178. package/templates/skeleton/node-service/apps/web/src/app/layout.tsx +0 -17
  179. package/templates/skeleton/node-service/apps/web/src/app/page.tsx +0 -96
  180. package/templates/skeleton/node-service/apps/web/src/lib/api.ts +0 -29
  181. package/templates/skeleton/node-service/apps/web/src/lib/validate.ts +0 -23
  182. package/templates/skeleton/node-service/apps/web/test/shared-validation.test.ts +0 -38
  183. package/templates/skeleton/node-service/apps/web/tsconfig.json +0 -14
  184. package/templates/skeleton/node-service/eslint.config.mjs +0 -20
  185. package/templates/skeleton/node-service/gitignore +0 -54
  186. package/templates/skeleton/node-service/package.json +0 -30
  187. package/templates/skeleton/node-service/packages/core/package.json +0 -11
  188. package/templates/skeleton/node-service/packages/core/src/events.ts +0 -14
  189. package/templates/skeleton/node-service/packages/core/src/index.ts +0 -15
  190. package/templates/skeleton/node-service/packages/core/src/note.ts +0 -69
  191. package/templates/skeleton/node-service/packages/core/test/events.test.ts +0 -23
  192. package/templates/skeleton/node-service/packages/core/test/note.test.ts +0 -101
  193. package/templates/skeleton/node-service/packages/db/package.json +0 -12
  194. package/templates/skeleton/node-service/packages/db/src/index.ts +0 -1
  195. package/templates/skeleton/node-service/packages/db/src/note-store.ts +0 -100
  196. package/templates/skeleton/node-service/packages/db/test/note-store.test.ts +0 -100
  197. package/templates/skeleton/node-service/packages/shared/package.json +0 -11
  198. package/templates/skeleton/node-service/packages/shared/src/env.ts +0 -17
  199. package/templates/skeleton/node-service/packages/shared/src/errors.ts +0 -33
  200. package/templates/skeleton/node-service/packages/shared/src/index.ts +0 -3
  201. package/templates/skeleton/node-service/packages/shared/src/logger.ts +0 -20
  202. package/templates/skeleton/node-service/packages/shared/test/env.test.ts +0 -26
  203. package/templates/skeleton/node-service/packages/shared/test/errors.test.ts +0 -28
  204. package/templates/skeleton/node-service/packages/shared/test/logger.test.ts +0 -19
  205. package/templates/skeleton/node-service/pnpm-lock.yaml +0 -2402
  206. package/templates/skeleton/node-service/pnpm-workspace.yaml +0 -13
  207. package/templates/skeleton/node-service/scripts/build-artifact.mjs +0 -34
  208. package/templates/skeleton/node-service/services/api/package.json +0 -17
  209. package/templates/skeleton/node-service/services/api/src/adapters/spool-publisher.ts +0 -23
  210. package/templates/skeleton/node-service/services/api/src/handlers/create-note.ts +0 -40
  211. package/templates/skeleton/node-service/services/api/src/handlers/list-notes.ts +0 -23
  212. package/templates/skeleton/node-service/services/api/src/main.ts +0 -40
  213. package/templates/skeleton/node-service/services/api/src/server.ts +0 -172
  214. package/templates/skeleton/node-service/services/api/src/static-dir.ts +0 -20
  215. package/templates/skeleton/node-service/services/api/src/usecases/create-note.ts +0 -30
  216. package/templates/skeleton/node-service/services/api/src/usecases/list-notes.ts +0 -14
  217. package/templates/skeleton/node-service/services/api/test/artifact.test.ts +0 -92
  218. package/templates/skeleton/node-service/services/api/test/create-note.handler.test.ts +0 -64
  219. package/templates/skeleton/node-service/services/api/test/create-note.usecase.test.ts +0 -43
  220. package/templates/skeleton/node-service/services/api/test/list-notes.test.ts +0 -48
  221. package/templates/skeleton/node-service/services/api/test/package-manager.test.ts +0 -40
  222. package/templates/skeleton/node-service/services/api/test/package-manager.ts +0 -51
  223. package/templates/skeleton/node-service/services/api/test/server.test.ts +0 -208
  224. package/templates/skeleton/node-service/services/api/test/spool-publisher.test.ts +0 -32
  225. package/templates/skeleton/node-service/services/api/test/static-dir.test.ts +0 -29
  226. package/templates/skeleton/node-service/services/worker/package.json +0 -16
  227. package/templates/skeleton/node-service/services/worker/src/main.ts +0 -28
  228. package/templates/skeleton/node-service/services/worker/src/spool.ts +0 -60
  229. package/templates/skeleton/node-service/services/worker/src/usecases/process-note-created.ts +0 -38
  230. package/templates/skeleton/node-service/services/worker/test/process-note-created.test.ts +0 -34
  231. package/templates/skeleton/node-service/services/worker/test/spool.test.ts +0 -76
  232. package/templates/skeleton/node-service/tsconfig.base.json +0 -15
  233. package/templates/skeleton/node-service/tsconfig.json +0 -13
  234. package/templates/skeleton/node-service/vitest.config.ts +0 -12
@@ -0,0 +1,313 @@
1
+ # AGENTS.md is canonical; CLAUDE.md is a shim
2
+
3
+ The rule lives in `AGENTS.md`, under "One operating system, two harnesses".
4
+ This file explains the model behind it, what is and is not claimed about
5
+ Claude Code's own `AGENTS.md` support, and what an existing rig's upgrade does
6
+ with a `CLAUDE.md`/`AGENTS.md` pair it already has (RP-186).
7
+
8
+ ## The model
9
+
10
+ Before this change, `templates/agent-os/universal/CLAUDE.md` was the authored
11
+ source and `AGENTS.md` was a byte-identical Codex-facing projection of it,
12
+ mechanically generated by `scripts/sync-codex-adapter.mjs`. That inverted the
13
+ provider-neutral framing the rest of this rulebook states everywhere else:
14
+ the rules are not Claude Code's, and authoring them under a Claude-Code-named
15
+ file made `AGENTS.md` — a name several tools other than Claude Code already
16
+ read — the copy.
17
+
18
+ `AGENTS.md` is now the canonical, provider-neutral source: the full rulebook
19
+ text lives there, once, and the generator authors it in that file directly.
20
+ `CLAUDE.md` is a short compatibility shim: a Claude Code `@AGENTS.md` import
21
+ (see "Claude Code's own import syntax" below) plus anything genuinely
22
+ specific to Claude Code — nothing that duplicates the rulebook. Neither file
23
+ is generated from the other any more; both are authored tree files under
24
+ `templates/agent-os/universal/`, and `scripts/sync-agent-os.mjs` composes each
25
+ one (elevated paths and this repository's addendum go into `AGENTS.md` only)
26
+ independently.
27
+
28
+ ## What is claimed about Claude Code reading AGENTS.md — and what is not
29
+
30
+ **Verified against Anthropic's Claude Code documentation** (`docs.claude.com`
31
+ → `code.claude.com/docs/en/memory`, "AGENTS.md" section, read 2026-09-20):
32
+
33
+ - Claude Code can read a repository's `AGENTS.md` directly, without a
34
+ `CLAUDE.md`, starting with Claude Code v2.1.277.
35
+ - That support is conditional, not universal. Paraphrased from the docs (this
36
+ rulebook names no cloud provider — `test/template/composition.test.ts`):
37
+ in some sessions, such as ones on certain third-party model-hosting
38
+ providers or with telemetry disabled, Claude Code cannot read `AGENTS.md`
39
+ at all — and separately, whenever a `CLAUDE.md`
40
+ (or a same-named file nested under a project's `.claude` directory, or
41
+ `CLAUDE.local.md`) exists anywhere in the working directory or above it,
42
+ Claude Code reads that file **instead of** `AGENTS.md` by default (the
43
+ `claude-md-or-agents-md` project-instructions setting, the default).
44
+ - The documented remedy for exactly this situation is the pattern this
45
+ project ships: *"When Claude isn't reading your AGENTS.md directly, you can
46
+ still keep it as the one file every tool shares by putting an `@AGENTS.md`
47
+ import in a CLAUDE.md next to it… Add any Claude-specific instructions
48
+ below the import, and Claude reads the imported file first, then the
49
+ rest"* — with the worked example:
50
+
51
+ ```markdown
52
+ @AGENTS.md
53
+
54
+ ## Claude Code
55
+
56
+ Use plan mode for changes under `src/billing/`.
57
+ ```
58
+
59
+ This project's `CLAUDE.md` template follows that exact shape.
60
+
61
+ **What this project does NOT claim**, stated because the shim's own text and
62
+ this record both have to survive a reader who takes them literally:
63
+
64
+ - It does **not** claim Claude Code always reads `AGENTS.md` on its own. It
65
+ might, depending on version and configuration — the shim exists precisely
66
+ for the sessions and configurations where it does not, and costs nothing
67
+ when it does (the docs state the import is never read twice).
68
+ - It does **not** claim `AGENTS.md` appears in `/memory` or `/context` when
69
+ Claude reads it directly through the setting — the docs say it does not,
70
+ and point at the `AGENTS.md loaded` session-start line instead.
71
+ - The `@AGENTS.md` import syntax itself: relative and absolute paths are both
72
+ allowed, imports are expanded at launch, and recursive imports are capped
73
+ at four hops — none of which this project's shim relies on beyond the one,
74
+ first-line import.
75
+
76
+ ## The migration (RP-186)
77
+
78
+ A rig installed before this change has a `CLAUDE.md`/`AGENTS.md` pair that is
79
+ byte-identical and recorded as such in its manifest (`.claude/.rig-manifest.json`
80
+ — see `packages/cli/src/lib/manifest.ts`). Both files are ordinary
81
+ manifest-tracked paths (`MAPS` in `packages/cli/src/commands/init.ts`), so
82
+ `upgrade`/`uninstall` need no special-cased migration logic for the
83
+ *ordinary* per-file cases — the same hash-based verdict machinery that
84
+ decides `update` / `conflict` / `deleted` for every other tracked file
85
+ governs both, independently. One coupling IS special-cased: CLAUDE.md's own
86
+ verdict is never allowed to become `update` — writing the `@AGENTS.md`
87
+ shim — while the on-disk AGENTS.md cannot actually SERVE as the rulebook.
88
+
89
+ **Round 5 design ruling (replacing round 4's verdict-only rule, gate cycle 4
90
+ blocker 1): the hold condition is CONTENT, not verdict.** CLAUDE.md is held
91
+ back only when AGENTS.md is absent (`deleted`), or present but not a
92
+ readable rulebook — no fenced ` ```elevated-paths ` block at all, or one
93
+ that parses to nothing once comments and blank lines are stripped (see
94
+ `packages/cli/src/lib/elevated-paths.ts`'s `isReadableRulebook`, mirroring
95
+ the gate sweep's own `parseElevatedPaths` in
96
+ `templates/agent-os/universal/.claude/scripts/detect-missed-gate.mjs` — a
97
+ script that ships only with the opt-in workflow layer, so this Core rule is
98
+ implemented independently of it rather than importing it — kept in step by a
99
+ correspondence test). An AGENTS.md the user CUSTOMISED but that
100
+ still carries a non-empty `elevated-paths` block lets the shim through — the
101
+ shim then imports the user's own rulebook, which is the point of it — and
102
+ AGENTS.md itself stays a perfectly ordinary, QUIET `conflict`: the same
103
+ `edited since it was installed` line every other kept file gets, no rescue
104
+ file, no special wording anywhere. This matters because a customised
105
+ AGENTS.md is not a broken rulebook — it is this project's own SHIPPED,
106
+ DESIGNED steady state: `AGENTS.md` itself tells every project to extend its
107
+ `elevated-paths` block for its own paths. Round 4's rule kept AGENTS.md's
108
+ own *verdict* (`conflict` or `deleted`) as the trigger, which made every
109
+ customisation indistinguishable from a genuinely broken rulebook — measured
110
+ directly against the real pre-RP-186 payload with one added path line: every
111
+ `upgrade` said "the migration is NOT finished", wrote a rescue file that
112
+ carried NONE of the user's own lines, and closed on `mv AGENTS.md.rig-new
113
+ AGENTS.md` — following it would have deleted user-authored rulebook content,
114
+ and merging into it and `mv`-ing looped forever.
115
+
116
+ When genuinely held back, `upgrade` keeps CLAUDE.md as its current bytes,
117
+ verdict `conflict`, with a reason naming AGENTS.md's own state and pointing
118
+ at the rescue-file section below for the exact remedy (round 5, blocker 2:
119
+ the reason text never hardcodes `mv` — the actual remedy depends on the
120
+ rescue file's own status, decided separately, and a fixed `mv` promise here
121
+ survived even the case where `mv` would be wrong).
122
+
123
+ **Held back re-vouches, and that has an `uninstall` consequence (PR #241
124
+ round 3, blocker 2; measured correct in round 4).** Holding CLAUDE.md back
125
+ does not merely skip writing it — it also records `sha256` of its CURRENT
126
+ bytes into the next manifest (`nextFiles['CLAUDE.md']`), the same way an
127
+ ordinary `update` would record the new bytes. This is deliberate (see "The
128
+ remedy" below: it is what lets a LATER `upgrade` resolve cleanly instead of
129
+ falling through to "not a version this rig ever released" forever) — but it
130
+ means `uninstall`, which reads `manifest.files` exactly like every other
131
+ rig-owned path, now sees a held-back CLAUDE.md as rig-owned and unedited
132
+ since, and **removes it**, precisely as it would any other untouched file.
133
+ It does not "leave it in place" the way a genuine `conflict` (bytes the
134
+ manifest never vouched for) does. Pinned by `packages/cli/test/upgrade.test.ts`'s
135
+ test "uninstall removes a held-back CLAUDE.md — the re-vouch makes it
136
+ rig-owned again" (absent in a generated rig — `packages/cli/test/` ships no
137
+ test into the payload, so a mention of it is not a citation this rulebook's
138
+ own checker, `evidence-pointers.test.ts`, examines: it reads `test/` and
139
+ `templates/agent-os/`, never `packages/`).
140
+
141
+ **Round 4, blocker 2 (CLI-UX): that removal is now disclosed, not silent.**
142
+ Removing CLAUDE.md while AGENTS.md is not a clean removal (preserved as
143
+ edited, or already gone) — or the other way around — is exactly the moment
144
+ the rulebook could end up with no readable copy left at all, and a bare
145
+ `- CLAUDE.md` line did not say so. `UninstallAction` now carries an optional
146
+ `note` (distinct from `reason`, which stays `preserved`-only) for this one
147
+ case, set at PLAN time so `--dry-run` and a real run print it identically,
148
+ and carried in `--json` as a `notes` array alongside `preserved`. Pinned by
149
+ `packages/cli/test/uninstall.test.ts`'s tests "discloses that removing
150
+ CLAUDE.md leaves AGENTS.md, preserved as edited, as the only rulebook copy",
151
+ "discloses the same thing in the opposite direction when AGENTS.md is the
152
+ one being removed", "discloses when the sibling is already gone (absent),
153
+ not only when it is preserved as edited", "adds no note when both are a
154
+ clean removal", and "the disclosure is identical on --dry-run and a real
155
+ run — set at plan time, never at apply time" (absent in a generated rig,
156
+ same reason as above).
157
+
158
+ | Case | Upgrade outcome | Uninstall outcome |
159
+ | --- | --- | --- |
160
+ | Both untouched since install | Both `update`: `CLAUDE.md` becomes the shim, `AGENTS.md` becomes canonical | Both removed (rig-owned, unedited); no `note` — a clean removal on both sides |
161
+ | `CLAUDE.md` edited by the user, AGENTS.md untouched | `conflict`: kept exactly as the user left it, never force-shimmed; if the edited content is not already the `@AGENTS.md` shim, the reason also says it shadows AGENTS.md (Claude Code reads CLAUDE.md instead, by default) and names the fix; AGENTS.md upgrades independently | `CLAUDE.md` left in place — its own `conflict` verdict is never recorded in `manifest.files` (`upgrade.ts`, the per-file loop's `conflict` branch: "deliberately NOT recorded in `files`"), so `planUninstall` gives it no action at all; it is simply never touched. AGENTS.md removed WITH a `note`, measured verbatim: `this is the rig's own AGENTS.md — removing it leaves CLAUDE.md, which exists and is yours (untracked by this rig), as the only rulebook copy` — the untracked-but-present sibling wording, pinned by `packages/cli/test/uninstall.test.ts`'s test "when CLAUDE.md is the untracked sibling after a real upgrade dropped its conflict verdict from the manifest, the AGENTS.md removal note says it EXISTS — never "absent" (mirror direction)" (absent in a generated rig, same reason as above) |
162
+ | `AGENTS.md` **customised WITH a readable `elevated-paths` block**, CLAUDE.md untouched (round 5's central case — the shipped rulebook's own designed steady state) | AGENTS.md `conflict`, perfectly QUIET — the ordinary `edited since it was installed` line, nothing else. CLAUDE.md is **NOT held back**: verdict `update`, the shim IS adopted, and it imports the user's own customised rulebook | CLAUDE.md removed WITH a `note` (never held back, so never re-vouched — same as any ordinary untouched shim), measured verbatim: `this is the rig's own CLAUDE.md — removing it leaves AGENTS.md, which exists and is yours (untracked by this rig), as the only rulebook copy` — pinned by `packages/cli/test/uninstall.test.ts`'s test "when the sibling is untracked by the manifest (present on disk, never recorded), says it EXISTS — never "absent"" (absent in a generated rig, same reason as above). AGENTS.md itself is left in place — an ordinary, untracked `conflict`, never an uninstall action at all |
163
+ | `AGENTS.md` edited WITHOUT a readable `elevated-paths` block (no block at all, or one that parses to nothing), CLAUDE.md untouched | AGENTS.md `conflict`: kept exactly as the user left it, never overwritten with the canonical text. CLAUDE.md is **held back**: verdict `conflict` (not `update`), kept as its current (old, still-readable) bytes, never written as the shim | AGENTS.md left in place — untracked `conflict`, no uninstall action. **CLAUDE.md is REMOVED** — the hold re-vouches it, so it reads as rig-owned and unedited — WITH a `note`, measured verbatim: `this is the rig's own CLAUDE.md — removing it leaves AGENTS.md, which exists and is yours (untracked by this rig), as the only rulebook copy` — pinned by `packages/cli/test/uninstall.test.ts`'s test "when the sibling is untracked by the manifest (present on disk, never recorded), says it EXISTS — never "absent"" (absent in a generated rig, same reason as above) |
164
+ | `AGENTS.md` deleted by the user, CLAUDE.md untouched | AGENTS.md `deleted`: stays deleted. CLAUDE.md is **held back** the same way, for the same reason (there is no rulebook file at all otherwise) | AGENTS.md stays absent — a genuinely absent path the manifest still records a hash for (`upgrade.ts`'s `deleted` branch keeps `nextFiles['AGENTS.md']` pointing at the recognised, pre-deletion hash, so it stays a manifest-tracked path rather than dropping out the way an ordinary `conflict` does); the `upgrade` REPORT — not the manifest — is what labels it `you removed (left removed)`, in its own summary line (`index.ts`). **CLAUDE.md is REMOVED**, for the same re-vouching reason as the row above — WITH a `note`, measured verbatim: `this is the rig's own CLAUDE.md — removing it leaves AGENTS.md, which is already gone, as the only rulebook copy` — the genuinely-absent sibling wording, pinned by `packages/cli/test/uninstall.test.ts`'s test "discloses when the sibling is already gone (absent), not only when it is preserved as edited" (absent in a generated rig, same reason as above) |
165
+ | `CLAUDE.md` deleted by the user | `deleted`: stays deleted, never restored as the new shim; unaffected by AGENTS.md's own state | already absent |
166
+ | `AGENTS.md` deleted by the user, CLAUDE.md deleted too | Both `deleted`: stays deleted on both sides — there is nothing left to hold back | already absent |
167
+ | The migration already finished (CLAUDE.md is already the shim), THEN AGENTS.md is deleted on a later run | AGENTS.md `deleted`: stays deleted. CLAUDE.md's own verdict is `unchanged` — the held-back coupling above only overrides a verdict that would otherwise become `update`, and an already-adopted shim's verdict never is, so it is left exactly as it is rather than resurrected, rewritten, or held back a second time | CLAUDE.md (the shim) removed like any other untouched file — WITH a `note`, measured verbatim: `this is the rig's own CLAUDE.md — removing it leaves AGENTS.md, which is already gone, as the only rulebook copy` — pinned by `packages/cli/test/uninstall.test.ts`'s test "discloses when the sibling is already gone (absent), not only when it is preserved as edited" (absent in a generated rig, same reason as above); AGENTS.md already absent. Upgrade side pinned by `packages/cli/test/upgrade.test.ts`'s test "AGENTS.md deleted after the migration already finished: the already-adopted shim is left exactly alone" (absent in a generated rig, same reason as above) |
168
+ | A repo that had its own `CLAUDE.md` (or `AGENTS.md`) before `init` | `init` **refuses outright** (`InitError`, non-zero exit) rather than installing over it or recording it as `kept` — `MAPS` files are the one install-time exception to the generic "kept" path every other pre-existing process file gets. Pinned by `packages/cli/test/init.test.ts`'s tests "refuses to clobber an existing CLAUDE.md unless forced" and "refuses to clobber an existing AGENTS.md" — named by full path deliberately: `test/e2e/init.test.ts` (a different suite) shares the bare basename `init.test.ts`, and either mention is absent in a generated rig for the same reason as above regardless | N/A — `init` never installed here, so there is nothing for `uninstall` to have owned |
169
+
170
+ The round-5 rule was re-derived and pinned as a 4×3 grid — AGENTS.md's axis
171
+ split into `pristine` / `edited-with-block` / `edited-without-block` /
172
+ `deleted`, crossed with CLAUDE.md's `pristine` / `edited` / `deleted` — each
173
+ cell's expected verdicts and hold-back flag hand-derived from the rule
174
+ itself, never copied from `upgrade.ts`'s own branching, by
175
+ `packages/cli/test/upgrade.test.ts`'s parametrised test "AGENTS.md %s ×
176
+ CLAUDE.md %s" and its own central case "AGENTS.md customised WITH a readable
177
+ elevated-paths block: the shim is adopted, not held back, and no rescue file
178
+ appears" (absent in a generated rig, same reason as above). Verified AT THE
179
+ CLI BOUNDARY against a customised, still-readable AGENTS.md by
180
+ `packages/cli/test/cli-report.test.ts`'s test "fresh init, then a path line
181
+ added to elevated-paths: upgrade --yes is silent about it, exit 0, AGENTS.md
182
+ unchanged" (same absence), and against the real pre-RP-186 payload by
183
+ `test/e2e/agents-md-migration.test.ts` (absent in a generated rig) › "a
184
+ legacy rig whose AGENTS.md was customised (still a readable rulebook) before
185
+ upgrading: the shim is adopted in one run, and a second run is a no-op".
186
+
187
+ Round 4, blocker 1's rescue file (below) is invisible to the table above — it
188
+ is never a manifest-tracked path — so `uninstall` treats it out of band: a
189
+ byte-identical copy of what THIS release would render right now is
190
+ `remove`d like any other untouched file; anything else is the user's,
191
+ `preserved`. Pinned by `packages/cli/test/uninstall.test.ts`'s tests "treats
192
+ a byte-identical rescue file as removable" and "leaves a differing rescue
193
+ file alone, reported as preserved — not this release's own bytes" (absent
194
+ in a generated rig, same reason as above).
195
+
196
+ ## The remedy that actually works — measured, not assumed (PR #241 rounds 3–5)
197
+
198
+ AGENTS.md's own conflict names a fix, but three readings of it do not work,
199
+ and this project measured each rather than asserting any of them:
200
+
201
+ - **Copying the file the conflict's own `new version:` line points at.**
202
+ That path is the RAW template — still carrying the unsubstituted
203
+ per-project placeholder (`substituteContent` replaces it by exact match;
204
+ see `packages/cli/src/lib/substitute.ts` — spelled out here would itself
205
+ be substituted away by this repo's own dogfood sync, which is exactly why
206
+ it is not), because templates are substituted at install/upgrade time,
207
+ not at rest. Pasting it verbatim gives Claude Code the wrong project name.
208
+ (Round 5: the raw template DOES still carry a real, non-empty
209
+ `elevated-paths` block, so under the content-based hold rule it no longer
210
+ latches the hold the way it used to — but it is still the wrong remedy,
211
+ since it hands the project the wrong name throughout its own rulebook.)
212
+ - **Restoring "the exact bytes the rig installed".** Once AGENTS.md is
213
+ already a conflict, the manifest no longer records what those bytes were
214
+ (the generic rule: "a conflict is not recorded" — deliberately unchanged
215
+ by this ticket, see the top of this section) — there is nothing left on
216
+ disk or in the manifest to restore TO, and this record makes no claim
217
+ about whether any specific past release's bytes would be recognised
218
+ another way (that would need its own test, not an assertion here).
219
+ - **Round 3's own remedy: printing the rendered content to stdout for a
220
+ verbatim paste.** Gate cycle 3 measured this directly, with the built CLI,
221
+ and it does not work either: pasting the block verbatim, or de-indented,
222
+ left AGENTS.md at `conflict` and CLAUDE.md held back regardless — nothing
223
+ about a terminal's own line-wrapping and a human's own copy-paste survives
224
+ byte-for-byte reliably enough for `planUpgrade`'s exact-bytes comparison.
225
+ The dump was also ~270 lines sitting between the plan and the consent
226
+ prompt, scrolling the actionable lines off screen, and the run ended on
227
+ "Wrote N files." even while CLAUDE.md stayed held back — a bare success
228
+ line over an unfinished migration.
229
+ - **Round 4's own remedy: a rescue file keyed on AGENTS.md's verdict alone.**
230
+ Gate cycle 4 measured this against the real pre-RP-186 payload and found
231
+ it did the wrong thing for the project's own designed steady state (a
232
+ customised, still-readable AGENTS.md) — see "Round 5 design ruling" above
233
+ — and, separately, that its `differs` case still closed on `mv`: pasting a
234
+ hostile or merely stale pre-existing rescue file over AGENTS.md as if this
235
+ run had written or verified it.
236
+
237
+ **The fix that is always reachable, with no dependency on release history,
238
+ a terminal, or a human's paste at all — and that only ever appears in the
239
+ GENUINELY held-back state (round 5):** `create-agent-rig upgrade` WRITES the
240
+ already-rendered (substituted) content for AGENTS.md — the same bytes
241
+ `UpgradePlan.contents` carries — to a real sibling file, `AGENTS_MD_RESCUE`
242
+ (`AGENTS.md.rig-new`, `packages/cli/src/commands/upgrade.ts`), through the
243
+ same symlink-refusing write path (`resolveWritableInside`) every other file
244
+ in the plan goes through. It is never recorded in the manifest. The rescue
245
+ file's status is decided ENTIRELY AT PLAN TIME (round 5, blocker 3) —
246
+ `would-write` | `identical` | `differs` | `unsafe` — so a dry run and a real
247
+ run report the identical status, and `applyUpgrade` refuses `unsafe`
248
+ (a symlink, a directory, or similar sitting at the path) BEFORE touching
249
+ anything else at all, including the ordinary payload writes and the
250
+ manifest — closing gate cycle 4's own finding that a symlinked rescue file
251
+ used to leave other files rewritten and the manifest stale, and that a
252
+ directory there crashed with an uncaught `EISDIR`.
253
+
254
+ The closing instruction is printed as a short, delimited block, LAST, after
255
+ the plan, after any wiring hand-over, after "Wrote N files." — the previous
256
+ remedy's placement between the plan and the consent prompt is exactly why
257
+ cycle 3 found "Wrote N files." reading as success while the migration was
258
+ not finished. **`mv` is printed ONLY for `would-write` and `identical`** —
259
+ bytes this run either just wrote or independently verified byte-identical to
260
+ its own rendering. **For `differs`, the remedy is `rm` (or restoring
261
+ AGENTS.md some other way), never `mv`** (round 5, blocker 2: printing `mv`
262
+ there installs bytes this run never wrote or verified as the live rulebook
263
+ the moment the instruction is followed literally). A leftover, byte-identical
264
+ rescue file is cleaned up once AGENTS.md resolves and the rig is NOT held
265
+ back; unrelated clutter at the path (a stray directory or symlink) on an
266
+ otherwise healthy rig is never touched and never even mentioned (round 5,
267
+ blocker 3's own "must not affect upgrade at all").
268
+
269
+ Verified AT THE CLI BOUNDARY — the spawned, built binary, never
270
+ `plan.contents` as the oracle for "the bytes are right" (that circularity was
271
+ cycle 3's finding about round 3's own test) — by
272
+ `packages/cli/test/cli-report.test.ts`'s describe block "AGENTS.md.rig-new —
273
+ the CLI-boundary remedy for a GENUINELY held-back AGENTS.md", including "the
274
+ rescue file is byte-identical to an independently-rendered AGENTS.md for the
275
+ same project — not `plan.contents`" (the independent oracle is a SEPARATE
276
+ `initProject` call, forced to the same project name), "never overwrites a
277
+ pre-existing AGENTS.md.rig-new that differs from the rendered bytes, and
278
+ NEVER prints `mv` for it", "a symlinked AGENTS.md.rig-new in the held-back
279
+ state: exit 1, a clean message, zero files changed", and "a directory at
280
+ AGENTS.md.rig-new in the held-back state: exit 1, a clean message, no
281
+ EISDIR crash" (absent in a generated rig, same reason as above). The
282
+ plan-time state machine itself is pinned at the `planUpgrade`/`applyUpgrade`
283
+ level by `packages/cli/test/upgrade.test.ts`'s describe blocks "the
284
+ AGENTS.md.rig-new rescue file — GENUINELY held back only (round 5)" and
285
+ "AGENTS.md.rig-new clutter on a HEALTHY (never held back) rig must not
286
+ affect upgrade at all" (same absence). And the whole remedy is followed end
287
+ to end against the real pre-RP-186 payload — not a fixture — by
288
+ `test/e2e/agents-md-migration.test.ts` (absent in a generated rig) › "a
289
+ legacy rig whose AGENTS.md was edited before upgrading: CLAUDE.md is held
290
+ back, never shimmed over an unreadable rulebook — and the rescue file
291
+ resolves it end to end".
292
+
293
+ Also see the byte-identity tests this ticket replaced, listed in the PR
294
+ description as an old-test → new-test table.
295
+
296
+ ## What did not change
297
+
298
+ - Provider-specific wiring stays provider-specific: `.claude/settings.json`,
299
+ `.claude/agents/`, `.codex/hooks.json`, `.codex/config.toml` are untouched
300
+ by this ticket and are never duplicated into either rulebook file.
301
+ - The `elevated-paths` block moved with the canonical text: it is declared in
302
+ `AGENTS.md` now, not `CLAUDE.md`. Every mechanical reader of that block —
303
+ `readDeclaredPaths`, and through it the PR-lane router and the queue's own
304
+ tier bookkeeping, all of them part of the opt-in workflow layer and named
305
+ by the core rulebook only to describe what that layer adds — reads
306
+ `AGENTS.md` first and `CLAUDE.md` second, so a not-yet-migrated rig (or a
307
+ test fixture that still writes the block into a `CLAUDE.md`) is not
308
+ silently un-declared.
309
+ - Zero new dependencies; the project-name token this generator substitutes
310
+ everywhere else is unaffected (it is not written literally here — this
311
+ file is itself substituted); no application skeleton, benchmark, agent-bus
312
+ or mandatory Memory
313
+ came back.
@@ -4,15 +4,20 @@ Status: accepted for AR-113; subagent routing extended for RP-166.
4
4
 
5
5
  ## Decision
6
6
 
7
- Claude-shaped files remain the authoring surface. The generator derives the
8
- Codex rulebook (`AGENTS.md`), skills (`.agents/`), custom agents and hook
9
- wiring (`.codex/`); the generated files are checked for drift in the generator
10
- repository and are materialised into a
11
- generated project as its local, versioned operating system. A generated
12
- project does not ship the generator's projector. Its Claude and Codex files are
13
- versioned snapshots: a downstream project that deliberately changes one side
14
- must either make the equivalent local change on the other side or take a newer
15
- generated release. There is no downstream automatic drift check.
7
+ Claude-shaped files remain the authoring surface for skills, custom agents and
8
+ hook wiring. The generator derives the Codex skills (`.agents/`), custom
9
+ agents and hook wiring (`.codex/`) from them; the generated files are checked
10
+ for drift in the generator repository and are materialised into a generated
11
+ project as its local, versioned operating system. The rulebook itself is the
12
+ one exception, since RP-186: `AGENTS.md` is the canonical, authored,
13
+ provider-neutral source — not derived from `CLAUDE.md` — and `CLAUDE.md` is a
14
+ short, separately authored compatibility shim (`@AGENTS.md` import plus
15
+ Claude-Code-specific notes; see `docs/decisions/agents-md-canonical.md`). A
16
+ generated project does not ship the generator's projector for the derived
17
+ files. Its Claude and Codex files are versioned snapshots: a downstream
18
+ project that deliberately changes one side must either make the equivalent
19
+ local change on the other side or take a newer generated release. There is no
20
+ downstream automatic drift check.
16
21
 
17
22
  ## Why
18
23
 
@@ -26,7 +31,7 @@ table, and the projector refuses a missing or orphaned agent profile. It also
26
31
  refuses duplicate source-agent names across layers, since one profile name
27
32
  cannot route two definitions.
28
33
 
29
- Frequent bounded work (`test-writer`, `prose-reviewer`) uses `gpt-5.6-terra`;
34
+ Frequent bounded work (`test-writer`, `implementation-agent`, `prose-reviewer`) uses `gpt-5.6-terra`;
30
35
  correctness, security, and infrastructure gates use `gpt-5.6-sol`. Every named
31
36
  gate uses `high` reasoning effort. Unnamed subagents inherit repository defaults
32
37
  of `gpt-5.6-terra` and `medium` from `.codex/config.toml`. `xhigh` is not a
@@ -4,10 +4,13 @@ The rule lives in `.claude/rules/workflow.md`, under "PR flow". This file
4
4
  explains the one part of it that reads like an inconsistency and is not, so that
5
5
  nobody "tidies" the two sets into agreement. It is not loaded into any session.
6
6
 
7
- ## The sets
7
+ ## The sets (opt-in workflow layer)
8
8
 
9
- Three different mechanisms each carry their own notion of "this file is only
10
- words":
9
+ All three mechanisms below — the gate sweep, the router, and the queue's
10
+ elevated ration — ship with the opt-in workflow layer (`init --layer
11
+ workflow`); a Core-only rig applies the same rulebook exemptions by hand,
12
+ since none of these scripts exist for it to run. Three different mechanisms
13
+ each carry their own notion of "this file is only words":
11
14
 
12
15
  **The first two carve out the rulebook**, and only then differ; the third runs
13
16
  *after* the sweep has already answered, so it sees only what survived:
@@ -29,7 +32,7 @@ test in the rig belongs there. The router's sets stay where they are, for the
29
32
  reason this whole record exists.
30
33
 
31
34
  **Neither row applies to a decision record, including this one.** Both
32
- predicates recognise `CLAUDE.md` anywhere, everything under `.claude/`, and —
35
+ predicates recognise `CLAUDE.md` and `AGENTS.md` anywhere, everything under `.claude/`, and —
33
36
  since these records were extracted — everything under `docs/decisions/`, through
34
37
  the one shared `isDecisionRecord`. A change here escalates the sweep and takes
35
38
  the `model` lane, exactly as the rule it explains would. The router's call is
@@ -58,10 +61,11 @@ is one of the risk flags — and its extension happens to be `.txt`.
58
61
 
59
62
  Copying the sweep's `.mdx` into the router's prose set would put executable MDX
60
63
  back on the prose lane. MDX carries components and imports; it is a program that
61
- renders, not a document that is read. The ration reads it the same way and for
62
- the same reason (`spacing-rations-mechanisms.md`): calling it prose there would
63
- clear the spacing hold on a file this rig treats as a program, on the permissive
64
- side.
64
+ renders, not a document that is read. The ration (workflow layer, like the
65
+ mechanism it rations) reads it the same way and for the same reason
66
+ (`spacing-rations-mechanisms.md`, also workflow layer): calling it prose there
67
+ would clear the spacing hold on a file this rig treats as a program, on the
68
+ permissive side.
65
69
 
66
70
  ## The shape of the mistake this prevents
67
71
 
@@ -0,0 +1,206 @@
1
+ # SessionStart hook output: one JSON wire format, both harnesses
2
+
3
+ Status: accepted for RP-185.
4
+
5
+ ## Decision
6
+
7
+ `inject-rules.mjs` (the SessionStart hook that re-injects the autonomy rules on
8
+ startup, resume and compaction) prints one JSON object to stdout, unconditionally,
9
+ for both Claude Code and Codex:
10
+
11
+ ```json
12
+ {"hookSpecificOutput":{"hookEventName":"SessionStart","additionalContext":"…"}}
13
+ ```
14
+
15
+ No trailing newline, no other top-level fields, no provider branching. The
16
+ `additionalContext` value carries exactly the text the hook used to write
17
+ directly to stdout (the `[agent-os] …` banner, the notice, the excerpted rules).
18
+
19
+ ## Why
20
+
21
+ Measured, 2026-09-17, Codex 0.154.0 on Windows: with the previous plain-text
22
+ output — stdout beginning with the literal characters `[agent-os] Autonomy
23
+ rules refresh …` — Codex printed `Hook failed └ hook returned invalid session
24
+ start JSON output`, and the autonomy refresh never reached the session's
25
+ context. Claude Code was unaffected by that same plain-text form. A second
26
+ SessionStart hook in the same measured session, one that already emitted
27
+ `{"hookSpecificOutput":{"hookEventName":"SessionStart","additionalContext":…}}`,
28
+ did reach the session — which is what pointed at the wire format rather than at
29
+ "Codex rejects plain text" as the fix.
30
+
31
+ That framing matters because it is not what Codex's own documentation says.
32
+ [Codex's hooks reference](https://learn.chatgpt.com/docs/hooks) states plainly
33
+ that plain text on stdout is accepted for `session_start` and is added as
34
+ developer context — so "Codex does not support plain-text SessionStart output"
35
+ would be false, and this file does not claim it. What the page does not
36
+ document is *how* Codex decides a given stdout is JSON rather than plain text,
37
+ or what happens when that decision goes the wrong way. The measured symptom —
38
+ an output whose first character is `[` reported as *invalid* JSON, rather than
39
+ silently read as plain text — is consistent with a sniff that treats a leading
40
+ `[` (or `{`) as a signal to attempt a JSON parse, and reports a failure rather
41
+ than falling back when that parse does not succeed. That mechanism is inferred
42
+ from the symptom, not read off the page, and is stated here as an inference,
43
+ not a documented fact.
44
+
45
+ ## Why the JSON envelope, not a reworded plain-text banner
46
+
47
+ The alternative fix — keep printing plain text, just not starting with `[` —
48
+ would have worked too, but it leaves the same undocumented sniffing behaviour
49
+ one character choice away from breaking again, for a reason nobody would think
50
+ to look for. The JSON form sidesteps the ambiguity entirely rather than
51
+ tip-toeing around it, and it turns out to need no provider branching, because
52
+ both harnesses already document the identical shape:
53
+
54
+ - **Codex** ([learn.chatgpt.com/docs/hooks](https://learn.chatgpt.com/docs/hooks)):
55
+ gives the worked example
56
+ `{"hookSpecificOutput":{"hookEventName":"SessionStart","additionalContext":"Load the workspace conventions before editing."}}`
57
+ as JSON output for a `session_start` hook.
58
+ - **Claude Code** ([code.claude.com/docs/en/hooks](https://code.claude.com/docs/en/hooks)):
59
+ documents the same nested shape for `SessionStart` — `hookSpecificOutput`
60
+ with `hookEventName` (must equal `"SessionStart"`) and `additionalContext` —
61
+ and states that JSON is detected when stdout starts with `{` and ends with
62
+ `}`; anything else is treated as plain text and added to context the same
63
+ way. `systemMessage` and `terminalSequence` are also accepted fields there,
64
+ unused here because nothing today needs them.
65
+
66
+ One shape, honoured by both harnesses' own documentation, is what
67
+ `invariants.md` ("One mechanism, one implementation") asks for whenever a
68
+ single source can serve two consumers — the alternative, a provider check that
69
+ picks plain text for one harness and JSON for the other, would be exactly the
70
+ kind of guessed, undocumented branching this repository's rules warn against
71
+ ("never invent APIs or behavior").
72
+
73
+ ## What this does not claim
74
+
75
+ - Not that Codex rejects plain text in general — its own docs say otherwise.
76
+ - Not a documented account of Codex's JSON-vs-plain-text sniffing — that
77
+ mechanism is not published; only the fix (always emit the one documented
78
+ JSON shape) is asserted, not the internal reason it was needed.
79
+ - Not that `source: "startup"`, `"resume"` and `"compact"` are guaranteed
80
+ identical by either harness's spec beyond what each page states; both are
81
+ silent on any per-source difference in output handling, and the hook applies
82
+ the same output uniformly across all three because nothing in either
83
+ contract says to do otherwise.
84
+
85
+ ## The exit path: exitCode vs exit(), and what it trades
86
+
87
+ The envelope change alone re-armed the original defect at a different trigger.
88
+ `inject-rules.mjs` ended with `process.exit(main())`, and `process.exit()` tears
89
+ the process down without waiting for a queued `stdout.write()` to drain. Under
90
+ the old plain-text wire format a write a pipe's buffer could not hold in one
91
+ piece degraded to *partial rules text* — readable, if incomplete. Under the
92
+ JSON envelope the same truncation is *invalid JSON* — precisely the state
93
+ Codex was measured rejecting wholesale, just moved from "the output starts
94
+ with `[`" to "the output was cut off mid-object". Exit code 0 either way, so
95
+ nothing downstream reports it.
96
+
97
+ Reproduced independently by two review passes at HEAD before the fix: a
98
+ consumer that does not start reading until well after the child would have
99
+ exited loses everything past the pipe's buffer — at one measurement, a 74 KB
100
+ rules file delivered 0 bytes; at another, 65536 of 73893. Both parse as
101
+ `Unterminated string`.
102
+
103
+ The fix is `process.exitCode = main()` in place of `process.exit(main())`.
104
+ Every path through `main()` returns `0`, so the exit STATUS does not change.
105
+ What changes is whether the process terminates before the write finishes:
106
+ `exitCode` lets Node's event loop drain naturally, which is what lets a large
107
+ payload actually reach a reader. Verified against the same probe shape that
108
+ found the defect, at four payload sizes (7 083 B through 1 002 552 B) and
109
+ three consumer shapes (a non-draining reader, a slow reader at 4 KiB/50 ms,
110
+ and a plain file redirect): every case delivered the complete envelope and
111
+ parsed. Pinned in the generator's `hooks.test.ts` (absent in a generated rig)
112
+ › "delivers the whole envelope even when the reader does not drain until
113
+ process.exit(main()) would already have torn the process down", which goes
114
+ red (`Unterminated string`, at a byte count that is host-dependent — kernel
115
+ pipe buffer size and scheduler timing both vary) if the single line is
116
+ reverted. The pinned payload is sized for a deterministic kill rather than a
117
+ merely likely one: a smaller payload truncated on nearly every reversion run
118
+ on every host checked, but not every one, and a pin the defect can slip
119
+ through occasionally is a pin that will eventually be green on a real
120
+ revert. That pin's coverage is Linux-shaped: Node documents pipe writes as
121
+ synchronous on Windows and asynchronous on POSIX, so the same reversion is
122
+ expected to have little or nothing to catch on a Windows lane — the test's
123
+ own comment says so, so a future reader does not mistake a Linux-only kill
124
+ for cross-platform cover.
125
+
126
+ **What this trades away, stated plainly rather than left to be discovered:**
127
+ `process.exit()` also GUARANTEED teardown, and `exitCode` does not. A consumer
128
+ that never reads stdout at all no longer gets a fast, wrong exit 0 — it gets
129
+ a hook that stays alive indefinitely, waiting on the write. Measured: still
130
+ running 8 seconds in in one review's reproduction, at 74 KB and 1 MB payloads
131
+ with nobody draining; completing the instant a reader appeared. A probe built
132
+ to refuse reading until the child would already have exited measurably
133
+ DEADLOCKS this version, where `process.exit()` would have terminated
134
+ (truncated, but terminated). Nothing in this file bounds that wait — the
135
+ calling harness's own hook timeout does. Not reachable at the size this hook
136
+ ships today (a few KB, well under a second to write), but a real behaviour
137
+ change on a project whose `autonomy.md` grows large, or whose harness stops
138
+ reading a hook's stdout entirely. The trade is made on purpose: a loud hang
139
+ bounded by the harness's own timeout is preferred over a silent, truncated
140
+ "success" with no bound on how wrong it can be.
141
+
142
+ A second, smaller consequence of the same change: a reader that vanishes
143
+ MID-write (a closed pipe, a harness that kills this process before reading)
144
+ now surfaces as an unhandled `error` event on `process.stdout` — exit 1 with
145
+ a Node stack trace on stderr, where the old `process.exit()` path exited 0
146
+ silently in the same situation. The handler is narrow rather than blanket,
147
+ and the difference is measured, not theoretical: an earlier draft of this
148
+ fix silenced every stdout error unconditionally, and security review found
149
+ that with stdout redirected to `/dev/full` — a genuine write failure
150
+ (ENOSPC), with the reader still fully attached — that blanket form exited 0
151
+ with nothing delivered and no diagnostic, the exact silent-loss shape this
152
+ whole file exists to avoid, moved one write call over. The handler now
153
+ distinguishes the two: EPIPE (the reader is gone; there is nothing left to
154
+ report to) stays silent, and anything else is written to stderr and marks
155
+ the exit non-zero. Pinned in the generator's `hooks.test.ts` (absent in a
156
+ generated rig) › "silently exits 0 when the reader is gone before the write
157
+ starts (EPIPE)" and › "reports a genuine stdout write failure on stderr and
158
+ marks the exit non-zero, rather than looking like a healthy session".
159
+
160
+ **Left for a separate decision, not for this one:** seven sibling hooks in
161
+ this same directory still end in `process.exit(…)` with no wait for a
162
+ pending write — `block-no-verify.mjs`, `guard-rulebook.mjs`,
163
+ `guard-subagent-model.mjs`, `guard-bash.mjs`, `guard-secret-file.mjs`
164
+ (`process.exit(status)`), `gate-stop-dod.mjs` (`process.exit(code)`) and
165
+ `warn-subagent-routing.mjs` (the first four and the last end
166
+ `process.exit(main())`). Their payloads are short (a refusal message, not a
167
+ whole rules file), so the exposure is far smaller, but the reasoning above
168
+ now lives in one hook's comments only — `invariants.md`'s "one mechanism,
169
+ one implementation" would ask for the same pattern everywhere it applies.
170
+ This change deliberately does not touch the other seven: changing every
171
+ `process.exit()` call in the hooks directory in a PR whose stated purpose is
172
+ a SessionStart wire-format fix is exactly the scope creep `autonomy.md`'s
173
+ Tier-2 discipline exists to catch. Recorded here so the inconsistency is a
174
+ known, named backlog item rather than something the next reader has to
175
+ rediscover.
176
+
177
+ ## Risk and rollback
178
+
179
+ Tier 2 (`templates/agent-os/universal/.claude/hooks/` is a declared elevated
180
+ path) for both decisions this record carries, each with its own risk and its
181
+ own rollback:
182
+
183
+ **The wire format.** The blast radius is narrow: this hook's own stdout
184
+ contract, read only by the two harnesses' SessionStart machinery. If either
185
+ harness's documented shape turns out to differ from what was fetched here, or
186
+ a future harness version stops accepting it, rollback is reverting
187
+ `inject-rules.mjs` to write plain text again — a one-line change, the same
188
+ one this decision replaces. The old plain-text form is pinned by a regression
189
+ test precisely so it is not reintroduced by accident while chasing an
190
+ unrelated fix: the generator's `hooks.test.ts` (absent in a generated rig) ›
191
+ "never regresses to the old bare [agent-os]-prefixed plain-text stdout".
192
+
193
+ **The exit path** ("The exit path: exitCode vs exit(), and what it trades",
194
+ above). The blast radius is this hook's shutdown behaviour, not its output
195
+ shape: a consumer that never drains stdout at all now holds this process
196
+ alive rather than letting it exit truncated, bounded only by the calling
197
+ harness's own hook timeout. If that trade turns out to be wrong — a harness
198
+ with no such timeout, or one where a hung hook process is worse than a
199
+ truncated one — rollback is reverting the single `process.exitCode = main()`
200
+ line to `process.exit(main())`, independently of the wire-format decision
201
+ above; the two lines do not depend on each other. That reintroduces the
202
+ flush defect this record measures, so a revert of this line alone should
203
+ also remove or explicitly override the test that pins it: the generator's
204
+ `hooks.test.ts` (absent in a generated rig) › "delivers the whole envelope
205
+ even when the reader does not drain until process.exit(main()) would already
206
+ have torn the process down".
@@ -11,7 +11,7 @@ Each named subagent this rig ships pins the model and the effort it reads with:
11
11
  | Role | Claude Code | Codex |
12
12
  | --- | --- | --- |
13
13
  | `code-reviewer`, `security-scanner`, a stack's infrastructure reviewer | `claude-opus-5`, `high` | `gpt-5.6-sol`, `high` |
14
- | `test-writer`, `prose-reviewer` | `claude-sonnet-5`, `high` | `gpt-5.6-terra`, `high` |
14
+ | `test-writer`, `implementation-agent`, `prose-reviewer` | `claude-sonnet-5`, `high` | `gpt-5.6-terra`, `high` |
15
15
  | a subagent with no definition | `claude-sonnet-5`; effort follows the session | `gpt-5.6-terra`, `medium` |
16
16
 
17
17
  In this project the pins are the files themselves: `model:` and `effort:` in
@@ -93,8 +93,11 @@ environment: `subagent-routing.test.ts` (absent in a generated rig) ›
93
93
  In a generated project the definitions are the policy (next section), so an agent
94
94
  a project adds with a pin is a role, and a role whose definition is changed to
95
95
  `model: inherit` has stopped being one — by that project's reviewed decision.
96
- - **The built-in agents get no definitions here.** A general-purpose subagent follows
97
- the unnamed default like any subagent without a definition.
96
+ - **The built-in agents get no definitions here.** Ordinary implementation has its
97
+ own role, `implementation-agent`, on the same tier as `test-writer`: frequent,
98
+ bounded work, not the reviewer tier. A general-purpose subagent remains for
99
+ ad-hoc work no role covers, and follows the unnamed default like any subagent
100
+ without a definition.
98
101
  - **The warning is a warning.** A session started with `CLAUDE_CODE_EFFORT_LEVEL` set
99
102
  still runs every gate — at that level.
100
103