create-agent-rig 0.4.0 → 0.5.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 (105) hide show
  1. package/CHANGELOG.md +207 -8
  2. package/README.md +50 -17
  3. package/package.json +2 -1
  4. package/packages/cli/dist/commands/create.js +8 -3
  5. package/packages/cli/dist/commands/init.js +82 -34
  6. package/packages/cli/dist/commands/upgrade.js +112 -30
  7. package/packages/cli/dist/index.js +38 -14
  8. package/packages/cli/dist/lib/copy-tree.js +35 -6
  9. package/packages/cli/dist/lib/init-settings.js +12 -0
  10. package/packages/cli/dist/lib/install-set.js +6 -8
  11. package/packages/cli/dist/lib/manifest.js +21 -9
  12. package/packages/cli/dist/lib/safe-path.js +30 -0
  13. package/templates/agent-os/init/AGENTS.md +191 -0
  14. package/templates/agent-os/init/CLAUDE.md +61 -9
  15. package/templates/agent-os/stack/aws-cdk/.agents/skills/post-deploy-verify/SKILL.md +98 -0
  16. package/templates/agent-os/stack/aws-cdk/.agents/skills/ro-debug/SKILL.md +117 -0
  17. package/templates/agent-os/stack/aws-cdk/.claude/agents/cdk-diff-reviewer.md +34 -3
  18. package/templates/agent-os/stack/aws-cdk/.claude/rules/aws-cdk.md +1 -1
  19. package/templates/agent-os/stack/aws-cdk/.claude/skills/post-deploy-verify/SKILL.md +34 -0
  20. package/templates/agent-os/stack/aws-cdk/.codex/agents/cdk-diff-reviewer.toml +4 -0
  21. package/templates/agent-os/universal/.agents/skills/check-premises/SKILL.md +233 -0
  22. package/templates/agent-os/universal/.agents/skills/loop/SKILL.md +676 -0
  23. package/templates/agent-os/universal/.agents/skills/new-invariant/SKILL.md +102 -0
  24. package/templates/agent-os/universal/.agents/skills/new-invariant/guard-invariant.example.mjs +78 -0
  25. package/templates/agent-os/universal/.agents/skills/new-invariant/guard-invariant.example.test.mjs +89 -0
  26. package/templates/agent-os/universal/.agents/skills/pr-ship/SKILL.md +338 -0
  27. package/templates/agent-os/universal/.agents/skills/worktree-task/SKILL.md +73 -0
  28. package/templates/agent-os/universal/.claude/agents/code-reviewer.md +33 -1
  29. package/templates/agent-os/universal/.claude/agents/prose-reviewer.md +64 -1
  30. package/templates/agent-os/universal/.claude/agents/security-scanner.md +33 -0
  31. package/templates/agent-os/universal/.claude/hooks/gate-stop-dod.mjs +274 -24
  32. package/templates/agent-os/universal/.claude/hooks/guard-bash.mjs +43 -5
  33. package/templates/agent-os/universal/.claude/hooks/guard-core-purity.mjs +25 -11
  34. package/templates/agent-os/universal/.claude/hooks/guard-secret-file.mjs +180 -0
  35. package/templates/agent-os/universal/.claude/hooks/guard-web-boundary.mjs +32 -13
  36. package/templates/agent-os/universal/.claude/hooks/inject-rules.mjs +175 -7
  37. package/templates/agent-os/universal/.claude/hooks/lib/edit-input.mjs +480 -0
  38. package/templates/agent-os/universal/.claude/rules/autonomy.md +61 -1
  39. package/templates/agent-os/universal/.claude/rules/invariants.md +71 -16
  40. package/templates/agent-os/universal/.claude/rules/workflow.md +55 -5
  41. package/templates/agent-os/universal/.claude/scripts/decision-router.mjs +1242 -0
  42. package/templates/agent-os/universal/.claude/scripts/detect-missed-gate.mjs +65 -2
  43. package/templates/agent-os/universal/.claude/scripts/git-env.mjs +49 -0
  44. package/templates/agent-os/universal/.claude/scripts/lib/secrets.mjs +490 -0
  45. package/templates/agent-os/universal/.claude/scripts/lib/verdict.mjs +433 -0
  46. package/templates/agent-os/universal/.claude/scripts/preflight.mjs +6 -16
  47. package/templates/agent-os/universal/.claude/scripts/queue/checkout.mjs +89 -0
  48. package/templates/agent-os/universal/.claude/scripts/queue/core.mjs +334 -23
  49. package/templates/agent-os/universal/.claude/scripts/queue/gate-rounds.mjs +160 -0
  50. package/templates/agent-os/universal/.claude/scripts/queue/github-issues.mjs +7 -2
  51. package/templates/agent-os/universal/.claude/scripts/queue/index.mjs +413 -8
  52. package/templates/agent-os/universal/.claude/scripts/queue/jira.mjs +75 -13
  53. package/templates/agent-os/universal/.claude/scripts/queue/plan-md.mjs +222 -21
  54. package/templates/agent-os/universal/.claude/scripts/queue/state.mjs +197 -0
  55. package/templates/agent-os/universal/.claude/scripts/run-journal.mjs +435 -0
  56. package/templates/agent-os/universal/.claude/scripts/run-state.mjs +441 -0
  57. package/templates/agent-os/universal/.claude/scripts/verdict.mjs +101 -0
  58. package/templates/agent-os/universal/.claude/settings.json +7 -2
  59. package/templates/agent-os/universal/.claude/skills/check-premises/SKILL.md +115 -7
  60. package/templates/agent-os/universal/.claude/skills/loop/SKILL.md +378 -31
  61. package/templates/agent-os/universal/.claude/skills/pr-ship/SKILL.md +300 -25
  62. package/templates/agent-os/universal/.codex/agents/code-reviewer.toml +4 -0
  63. package/templates/agent-os/universal/.codex/agents/prose-reviewer.toml +4 -0
  64. package/templates/agent-os/universal/.codex/agents/security-scanner.toml +4 -0
  65. package/templates/agent-os/universal/.codex/agents/test-writer.toml +4 -0
  66. package/templates/agent-os/universal/.codex/hooks.json +65 -0
  67. package/templates/agent-os/universal/AGENTS.md +164 -0
  68. package/templates/agent-os/universal/CLAUDE.md +47 -14
  69. package/templates/agent-os/universal/PLAN.md +7 -40
  70. package/templates/agent-os/universal/docs/decisions/closing-a-task.md +98 -0
  71. package/templates/agent-os/universal/docs/decisions/codex-adapter.md +108 -0
  72. package/templates/agent-os/universal/docs/decisions/fail-open-guards.md +43 -0
  73. package/templates/agent-os/universal/docs/decisions/review-lanes.md +72 -0
  74. package/templates/agent-os/universal/docs/decisions/run-directory.md +44 -0
  75. package/templates/agent-os/universal/docs/decisions/spacing-rations-mechanisms.md +125 -0
  76. package/templates/agent-os/universal/docs/decisions/stop-conditions-in-a-file.md +46 -0
  77. package/templates/agent-os/universal/docs/decisions/two-empty-endings.md +62 -0
  78. package/templates/agent-os/universal/journal/README.md +101 -0
  79. package/templates/agent-os/universal/layers.json +36 -2
  80. package/templates/hash-history.json +2 -1
  81. package/templates/skeleton/aws-serverless/.github/workflows/deploy.yml +34 -1
  82. package/templates/skeleton/aws-serverless/README.md +91 -9
  83. package/templates/skeleton/aws-serverless/apps/web/src/lib/api.ts +9 -2
  84. package/templates/skeleton/aws-serverless/gitignore +37 -0
  85. package/templates/skeleton/aws-serverless/infra/bin/app.ts +73 -13
  86. package/templates/skeleton/aws-serverless/infra/lib/app-stack.ts +136 -4
  87. package/templates/skeleton/aws-serverless/infra/lib/web-stack.ts +14 -1
  88. package/templates/skeleton/aws-serverless/infra/test/allowed-origins.test.ts +301 -0
  89. package/templates/skeleton/aws-serverless/infra/test/app-composition.test.ts +137 -0
  90. package/templates/skeleton/aws-serverless/infra/test/app-stack.test.ts +36 -20
  91. package/templates/skeleton/aws-serverless/infra/test/web-stack.test.ts +15 -12
  92. package/templates/skeleton/aws-serverless/services/api/src/handlers/create-note.ts +33 -6
  93. package/templates/skeleton/aws-serverless/services/api/src/handlers/list-notes.ts +18 -5
  94. package/templates/skeleton/aws-serverless/services/api/src/usecases/create-note.ts +11 -0
  95. package/templates/skeleton/aws-serverless/services/api/test/create-note.handler.test.ts +120 -1
  96. package/templates/skeleton/aws-serverless/services/api/test/list-notes.test.ts +72 -1
  97. package/templates/skeleton/node-service/README.md +11 -1
  98. package/templates/skeleton/node-service/gitignore +34 -0
  99. package/templates/skeleton/node-service/packages/db/src/note-store.ts +47 -10
  100. package/templates/skeleton/node-service/packages/db/test/note-store.test.ts +20 -0
  101. package/templates/skeleton/node-service/services/api/src/main.ts +2 -9
  102. package/templates/skeleton/node-service/services/api/src/server.ts +93 -10
  103. package/templates/skeleton/node-service/services/api/src/static-dir.ts +20 -0
  104. package/templates/skeleton/node-service/services/api/test/server.test.ts +98 -13
  105. package/templates/skeleton/node-service/services/api/test/static-dir.test.ts +28 -0
package/CHANGELOG.md CHANGED
@@ -11,6 +11,180 @@ Numbering is ordinary semver — **additive is a minor, a fix is a patch** — s
11
11
  that "I only take minors" remains a usable policy; 0.3.2 shipped additive
12
12
  content as a patch by the owner's call and stays recorded as one.
13
13
 
14
+ ## 0.5.0
15
+
16
+ **Codex is a harness of this rig now, not a thing you adapt it to.** A generated
17
+ project carries one rulebook and two readers: `CLAUDE.md` for Claude Code and
18
+ the same text as `AGENTS.md` for Codex, with repository skills in
19
+ `.agents/skills/`, agent profiles in `.codex/agents/` and portable hook wiring
20
+ in `.codex/hooks.json`. Neither harness gets the weaker policy, and the derived
21
+ half is drift-checked rather than maintained twice.
22
+
23
+ **This release ships untagged, by the owner's decision, and it has exactly one
24
+ consequence — read it if you ever run `upgrade` on a rig whose
25
+ `.claude/.rig-manifest.json` is missing or unreadable.**
26
+ `templates/hash-history.json`, the table such a rig is measured against, is
27
+ built from `v*` tags. So 0.5.0's bytes never enter it, and the `0.4.0` row it
28
+ gained in this release carries the _previous_ release's bytes: that tag points at
29
+ 0.3.2's content, which is why the row adds no hash to any path. Both paths 0.4.0
30
+ actually changed — `.claude/skills/loop/SKILL.md` and `PLAN.md` — are in the
31
+ table with their 0.3.x hashes; what is absent is 0.4.0's bytes from their hash
32
+ lists.
33
+
34
+ **Who that reaches, and who it does not.** `create`, `init` and `upgrade` each
35
+ write the manifest, and `upgrade` matches it **before** it consults the table, so
36
+ a rig whose manifest is present and parseable is unaffected whatever the table
37
+ says. Without a readable manifest the table decides, and it decides in the
38
+ conservative direction: bytes it recognises are replaced, bytes it does not are
39
+ kept and reported as yours — see `packages/cli/test/upgrade.test.ts` › "replaces
40
+ a file that matches a released version, and reports the rest".
41
+
42
+ A rig installed **before** 0.4.0 is not the exposed case: its 0.3.x bytes are in
43
+ the table, so those two files are recognised and replaced. The rig that keeps
44
+ them is one installed at **0.4.0** whose manifest is unreadable.
45
+
46
+ **And the scale of it grows with this release, which matters more than those two
47
+ files.** Because 0.5.0 is untagged, nothing it ships enters the table either — so
48
+ a rig installed at 0.5.0 and later upgraded **without a readable manifest** has
49
+ most of its agent-os files unrecognised, and many of them are paths the table has
50
+ no row for at all. Every one is kept and reported as yours, so no edit is lost
51
+ and no file is silently skipped, but almost nothing would be refreshed either. **Commit `.claude/.rig-manifest.json`** — that single habit makes the
52
+ table irrelevant to you, and it is what `README.md` puts in bold.
53
+
54
+ ### Added
55
+
56
+ - **Codex is now a native target of the Agent OS.** Generated and `init`ed
57
+ projects receive `AGENTS.md`, repository skills in `.agents/skills/`, custom
58
+ agent profiles in `.codex/agents/`, and portable `.codex/hooks.json` wiring.
59
+ These files are derived from the Claude Code sources and drift-checked.
60
+ - Architecture guards now understand Codex `apply_patch` payloads, inspecting
61
+ additions and bounded existing content for moves, so removing an old
62
+ violation does not create a false block.
63
+
64
+ ### Changed
65
+
66
+ - **`upgrade` now replaces `.claude/settings.json` when the manifest proves you
67
+ never touched it** — closing the decision 0.4.0's notes left open below. The
68
+ case it exists for is a release that adds a hook: the hook file arrived and
69
+ the wiring that calls it did not, so the guard sat on disk doing nothing. When
70
+ the on-disk bytes hash-match the entry the manifest recorded for the installed
71
+ release, they are provably the rig's own and the release's version is written.
72
+
73
+ **Three limits. The first two are there because not having them was tried,
74
+ and each produced a regression two reviewers reproduced independently.**
75
+
76
+ 1. **The released-hash fallback does not apply to this file.** Every other
77
+ file the rig installed can be recognised by matching a tagged release even
78
+ with no manifest entry. This one cannot: a rig with no manifest that has
79
+ run `init` is recorded as `kind: "init"`, and the wiring that flavour
80
+ writes deliberately omits the hooks `init` does not install.
81
+ 2. **A replacement that would stop calling a hook still present in
82
+ `.claude/hooks/` is handed over instead**, whatever the manifest says. This
83
+ is the guard that does not depend on getting `kind` right — a manifest
84
+ saying `init` on a rig `create` produced reaches the same wrong wiring
85
+ through the hash arm alone.
86
+ 3. **Anything else is unchanged:** the new entries are printed for you to
87
+ merge, and nothing is written.
88
+
89
+ - **`init --force` is deprecated.** It refuses, names `upgrade` as the command
90
+ that refreshes a rig, and writes nothing. It only ever replaced `CLAUDE.md`,
91
+ which `upgrade` now does per file and with the manifest behind it. **The flag
92
+ is removed in 0.6** — this release is the one warning you get.
93
+
94
+ The way into a `create` rig that `--force` used to provide is a deleted
95
+ `CLAUDE.md`; that is what `init`'s refusal is actually about, and it is the
96
+ case the manifest-preserving fix below was written for.
97
+
98
+ ### Security
99
+
100
+ - 🔴 **A committed `.claude/.rig-manifest.json` could run code on the machine
101
+ of whoever upgraded the rig.** `project.name`, `project.scope`,
102
+ `project.region` and `stacks` were each validated — but only as _path_
103
+ segments, a predicate that asks whether a value can steer a write. Two of
104
+ them are also substituted into installed **files**:
105
+ `.claude/scripts/stop-flag.mjs` embeds the name inside a single-quoted
106
+ JavaScript string literal that `guard-bash` imports on every Bash call. A
107
+ value closing that quote steers no path at all and passed — it executed in
108
+ the hook process, **and** moved the kill switch's path off
109
+ `~/.claude/<name>-loop-STOP`, so the brake read as installed while doing
110
+ nothing. The manifest travels in pull requests, so the delivery was an
111
+ ordinary PR plus an `upgrade`. All four are now held to the shape the rig
112
+ actually produces (`^[a-z0-9_][a-z0-9._-]*$`), and a manifest carrying
113
+ anything else is void as a whole rather than corrected.
114
+
115
+ **Checking a rig you upgraded from a manifest you did not write — three
116
+ places, because the name is not the only value that travelled.**
117
+ `.claude/scripts/stop-flag.mjs` is the executable sink: its kill-switch line
118
+ must read your own project name. `region` lands in
119
+ `.claude/skills/ro-debug/SKILL.md` as `export AWS_REGION=…` on rigs carrying
120
+ the `aws-cdk` overlay — and a manifest also declares `stacks`, so it can
121
+ request that overlay on a rig that never had it. The name is substituted into
122
+ the documents the agent obeys as well (`CLAUDE.md`, `PLAN.md`, the `loop`
123
+ skill), where a hostile value arrives as injected text rather than as code.
124
+
125
+ Nothing `create` or `init` writes is rejected by the new rule — including an
126
+ empty `region` and a name with a leading underscore, which
127
+ `projectNameFor` really can produce.
128
+
129
+ ### Fixed
130
+
131
+ - **`init --force` inside a generated project used to make `upgrade` stop
132
+ refreshing the stack overlays — silently.** ⚠ Read this next to the
133
+ deprecation above: `--force` is refused in this same release, so
134
+ the route described here is gone. The fix is not idle — the manifest is
135
+ preserved on **every** `init` over a `create` rig, and the remaining route in
136
+ is a deleted `CLAUDE.md`. `init` rewrote the rig manifest
137
+ as `kind: "init"`, `stacks: []`, empty `region`, and `upgrade` trusts a
138
+ manifest wholesale rather than re-detecting: the stack files simply left the
139
+ plan, reported neither as deleted nor as a conflict, and `CLAUDE.md` came
140
+ back in the `init` flavour. `init` now carries the `kind`, `project` and
141
+ `stacks` it found in the manifest through unchanged, and adds an entry for
142
+ each file it wrote without dropping the entries already there. It also says,
143
+ before writing anything, that this rig came from `create` and `upgrade` is
144
+ the command that refreshes it.
145
+
146
+ ⚠ **Both halves read the manifest, so a rig that has none — anything
147
+ installed before 0.4.0 — is not covered.** There `init` still writes
148
+ `kind: "init"`, `stacks: []`, empty `region`, and prints no advisory; worse,
149
+ such a rig could previously be recovered by `upgrade`, which re-detects the
150
+ install from the files on disk **only when there is no manifest at all**, and
151
+ the one `init` writes takes that route away. On a pre-0.4.0 rig, run
152
+ `upgrade` before `init`.
153
+
154
+ **Recovering a rig whose manifest was already flattened:** delete
155
+ `.claude/.rig-manifest.json` and run `upgrade` — the detection restores
156
+ `kind`, `stacks` and `region` from the files themselves; hand-writing the
157
+ manifest is not needed and `parseManifest` rejects the whole file on any
158
+ malformed field. What that does **not** repair is `CLAUDE.md`: the flattening
159
+ `init` overwrote it with the `init` flavour, so `upgrade` reports it as
160
+ `conflict` ("not a version this rig ever released — treated as yours") and
161
+ the create flavour has to be merged back by hand.
162
+
163
+ - **The `jira` queue adapter was calling an endpoint Atlassian removed.** Both
164
+ selection and the triage dedupe went through `GET /rest/api/3/search`, which
165
+ answers `410 Gone`; the adapter threw on the status line and the loop read
166
+ that as an unreadable queue. It now uses `POST /rest/api/3/search/jql`. If
167
+ your rig is on the `jira` adapter, this is the difference between a loop that
168
+ works and one that reports an empty board. Cursor pagination
169
+ (`nextPageToken`) is **not** implemented yet, so a board with more open issues
170
+ than `limit` (default 100) still loses its tail.
171
+
172
+ ### Changed — action needed if your board uses the `jira` adapter
173
+
174
+ - 🔴 **The elevated-tier marker on Jira is now the `elevated` label, not
175
+ `human-review`.** A board that marked elevated work with `human-review` will,
176
+ after this upgrade, hand every item to the loop as `normal` — the
177
+ elevated-spacing ration silently stops holding anything back. **Relabel those
178
+ issues to `elevated` before running the loop again.** The change is
179
+ deliberate: on a Jira board `human-review` reads as "a human is looking at
180
+ it", which is a different claim from "this change is expensive to reverse".
181
+ The `github-issues` adapter is unaffected and still reads `human-review`,
182
+ where it does mean a human reviewed the diff.
183
+ - **Selection now excludes the `operator-queue` label as well as `triage`.** An
184
+ item in the owner's lane is work a human has taken, so the loop no longer
185
+ picks one up. If you used `operator-queue` for something else, rename it
186
+ first.
187
+
14
188
  ## 0.4.0
15
189
 
16
190
  Upgrading is a command now: **`npx create-agent-rig@0.4.0 upgrade`** (`@latest`
@@ -335,7 +509,10 @@ sometimes earlier (step 6). Everything before that is mechanical:
335
509
  4. `node scripts/build-hash-history.mjs` — regenerate the released-hash table
336
510
  from the tags **after** the version bump, so the version now shipping is the
337
511
  first one it excludes. Forgetting it would leave `upgrade` unable to
338
- recognise the previous release.
512
+ recognise the previous release. ⚠ With no tag cut (step 7), what excludes the
513
+ shipping version is the absent tag rather than that filter — and the same
514
+ absence keeps it out of every later table too, which is the cost step 7
515
+ states.
339
516
  5. This file, and `PLAN.md` if the plan's claims changed.
340
517
  6. **`pnpm test` again — this run, not step 1, is the one that can catch a
341
518
  stale hash table.** The check compares the table against the versions this
@@ -343,13 +520,35 @@ sometimes earlier (step 6). Everything before that is mechanical:
343
520
  comparing the _old_ release to the _old_ table and passes either way. A
344
521
  guard that can only fire after the thing it guards has changed has to be run
345
522
  after it.
346
- 7. `git tag v<version> && git push --tags` **first check that the tag does not
347
- already exist** (`git ls-remote --tags origin`). A leftover from an abandoned
348
- attempt is a published ref: deleting or moving it is an **owner** action, and
349
- the release stops here until it is gone. A tag pointing at the wrong commit
350
- is not cosmetic — the next release builds its hash table from it, and every
351
- file that changed in between then reads as user-modified and stops being
352
- upgradable.
523
+ 7. **Tagging is not part of this project's release process** standing owner
524
+ decision, recorded at 0.5.0: the owner publishes by hand and does not tag. So
525
+ this step is deliberately _not_ performed, and the cost is stated here rather
526
+ than discovered later.
527
+
528
+ Step 4 builds the table from `v*` tags, so an untagged release never enters
529
+ it. That is free for the release being prepared and **not free for the one
530
+ after it**: step 6 fails as soon as the CHANGELOG lists an untagged release
531
+ _below_ the version being prepared, and its message says "stale table", which
532
+ running the builder cannot satisfy. Measured against this repository at
533
+ 0.5.0 preparation: with `0.5.0` untagged, a `0.5.0` bump passes and an
534
+ `0.6.0` bump fails. The check is
535
+ `test/template/hash-history.test.ts` › "covers every released version below
536
+ the one being prepared". **AR-35 carries the fix**, and which shape it takes
537
+ is that item's to decide, not this note's.
538
+
539
+ **What a releaser may do when step 6 fails, stated so it is not inferred:**
540
+ stop, and land AR-35 first. Cutting the missing tag after the fact is _not_
541
+ the sanctioned way out — it would put the table's honesty back on a step this
542
+ process does not perform, which is the whole reason the decision is recorded
543
+ here.
544
+
545
+ If a tag is ever cut anyway, the older warning still applies: check first
546
+ that it does not exist (`git ls-remote --tags origin`), because a leftover
547
+ from an abandoned attempt is a published ref, deleting or moving one is an
548
+ **owner** action, and a tag on the wrong commit makes the next table **name a
549
+ version whose bytes it does not carry**. `v0.4.0` is in exactly that state —
550
+ it points at 0.3.2's content.
551
+
353
552
  8. **Owner:** `npm publish`.
354
553
  9. **Owner:** smoke the published artifact — `npx create-agent-rig@<version>` in
355
554
  an empty directory, then `pnpm install && pnpm check` inside it; and
package/README.md CHANGED
@@ -3,6 +3,12 @@
3
3
  Scaffold a project that ships with an **agent operating system** — rules,
4
4
  gates, and hooks that hold the architecture mechanically, not by prose.
5
5
 
6
+ The same Agent OS is native to both **Claude Code and Codex**. Claude-facing
7
+ files remain the authoring surface; the generator derives Codex's `AGENTS.md`,
8
+ repository skills under `.agents/skills/`, custom agents under `.codex/agents/`,
9
+ and `.codex/hooks.json`. `node scripts/sync-codex-adapter.mjs --check` refuses
10
+ drift between the two projections.
11
+
6
12
  ```sh
7
13
  npx create-agent-rig my-app # choose a target interactively
8
14
  npx create-agent-rig my-app --target node-service # or name it up front
@@ -22,16 +28,24 @@ npx create-agent-rig init --dry-run # print the plan, write nothing
22
28
  ```
23
29
 
24
30
  `init` drops in the autonomy tiers, stop rules, workflow, and the enforcement
25
- hooks — **wired**, in a `.claude/settings.json` that names exactly the hooks it
26
- installed plus a `CLAUDE.md` that describes that rig rather than the generated
27
- monorepo. It refuses to clobber an existing `CLAUDE.md`; if the repo already has
28
- a `.claude/settings.json`, it keeps it and prints the entries to merge, because a
29
- hook nothing calls is not enforcement.
31
+ hooks — **wired** for both harnesses, in `.claude/settings.json` and
32
+ `.codex/hooks.json`, each naming exactly the hooks it installed plus matching
33
+ `CLAUDE.md` and `AGENTS.md` maps that describe that rig rather than the generated
34
+ monorepo. It refuses to clobber either existing map; if the repo already has a
35
+ Claude or Codex hook config, it keeps it and prints the entries to merge,
36
+ because a hook nothing calls is not enforcement.
30
37
 
31
38
  Two things it deliberately leaves to you, and says so in the installed
32
- `CLAUDE.md`: the Definition-of-Done gate has no `dod-checks.json` (it cannot know
39
+ maps: the Definition-of-Done gate has no `dod-checks.json` (it cannot know
33
40
  your commands), and the elevated-path list names only what every repo has.
34
41
 
42
+ After generation or upgrade, review the checked-in `.codex/hooks.json` in Codex's
43
+ `/hooks` view and explicitly trust it if Codex presents a trust prompt. The
44
+ [official Codex hooks documentation](https://learn.chatgpt.com/docs/hooks)
45
+ records trust against the current hook hash, so a changed hook definition may
46
+ require that review again; the adapter does not silently replace user-owned hook
47
+ configuration.
48
+
35
49
  ## Upgrading a rig you already have
36
50
 
37
51
  A release changes files, and `init` only ever _adds_ — so bringing an existing
@@ -56,10 +70,19 @@ repository the command is blind on CI and on a colleague's machine. Rigs
56
70
  installed before 0.4.0 have no manifest, so the package also carries the hashes
57
71
  of every **tagged** release (0.3.0 onward — 0.1.0 and 0.2.0 shipped untagged,
58
72
  and a rig from those reports every file as yours) and recognises a file matching
59
- one of them.
60
-
61
- `.claude/settings.json` is never replaced it is where your own hooks live, so
62
- the new wiring is printed for you to merge.
73
+ one of them. Releases from 0.5.0 on ship untagged as well, so the table does not
74
+ carry them either: on a rig with no manifest most of their files are reported as yours
75
+ rather than refreshed. This is why committing the manifest is the sentence in
76
+ bold above and not an aside.
77
+
78
+ `.claude/settings.json` is replaced only when the manifest's recorded hash
79
+ proves the rig wrote those exact bytes and you have not touched them — the case
80
+ where a release adds a hook and the wiring that calls it. Anything else, and it
81
+ is where your own hooks live: the new wiring is printed for you to merge, never
82
+ written. Unlike every other file, a match against the released hashes is not
83
+ enough for this one, and a replacement that would stop calling a hook the
84
+ current wiring names — while that hook's file is still in `.claude/hooks/` — is
85
+ handed over instead.
63
86
 
64
87
  **A file you deleted stays deleted.** The rules invite you to delete the ones
65
88
  whose invariant your project does not have, so an upgrade that quietly restored
@@ -75,13 +98,21 @@ and `--dry-run` lists it before anything is written.
75
98
  **A system of boundaries, each held by tooling.** An agent (or a human using
76
99
  one) cannot talk its way past them — each guard is a pre-write scan that stops
77
100
  the normal path cold (review and tests back it; the claim is stated exactly,
78
- never inflated). The hooks live in `.claude/hooks/` and are wired in
79
- `.claude/settings.json`:
101
+ never inflated). The hook implementations live once in `.claude/hooks/` and are
102
+ wired by both `.claude/settings.json` and `.codex/hooks.json`:
80
103
 
81
104
  - **`guard-core-purity`** — refuses any edit that puts I/O, clock, randomness,
82
105
  environment access, or a non-allowlisted import into the pure domain core;
83
106
  - **`guard-web-boundary`** — refuses `db`/service imports from the frontend;
84
107
  the web talks to the backend over HTTP only;
108
+ - **`guard-secret-file`** — refuses an edit that writes a credential: either the
109
+ path names one (`jira.env`, `id_rsa`, anything under `secrets/`) or the text
110
+ carries a credential VALUE. Both arms read one vocabulary,
111
+ `.claude/scripts/lib/secrets.mjs`, and a refusal names the pattern and the line
112
+ and **never the matched value** — printing it would leak the secret in the act
113
+ of refusing it. Its four blind spots are in its own header, each naming the
114
+ test that pins it or saying plainly that none does — and those tests live in
115
+ this generator, not in the rig;
85
116
  - **`block-no-verify`** — refuses bypassing pre-commit checks (and knows the
86
117
  difference between using the `--no-verify`/`-n` flag and merely mentioning it
87
118
  in a message);
@@ -96,7 +127,9 @@ never inflated). The hooks live in `.claude/hooks/` and are wired in
96
127
  check is red; it fails open (a missing or corrupt config never makes the
97
128
  session unquittable) and never blocks twice in a row;
98
129
  - **`inject-rules`** — re-injects the autonomy rules at session start, so they
99
- survive compaction and resumes.
130
+ survive compaction and resumes: the whole file, minus the regions the file
131
+ itself marks as reference. What is left out is a decision written in
132
+ `autonomy.md` on the line above it, not one this hook infers.
100
133
 
101
134
  **A brake that is a real file.** `touch ~/.claude/<project>-loop-STOP` and no
102
135
  merge lands until it is removed — enforced at the tool layer, so it holds even if
@@ -125,8 +158,8 @@ never), **stop rules** (three strikes, flaky ≠ retry, session staleness),
125
158
  `prose-reviewer`, and `cdk-diff-reviewer` on the AWS target), **skills** (`pr-ship` pre-merge gate;
126
159
  `loop` queue driver; `worktree-task` for concurrent sessions; `new-invariant`, a
127
160
  generator for the invariant→hook→test pattern; `post-deploy-verify` and
128
- `ro-debug` on the AWS target), and a one-page `CLAUDE.md` map a fresh session
129
- orients by.
161
+ `ro-debug` on the AWS target), and matching one-page `CLAUDE.md` / `AGENTS.md`
162
+ maps a fresh session orients by.
130
163
 
131
164
  **The hooks are examples, not laws.** `.claude/rules/invariants.md` states the
132
165
  pattern behind each one — a stated invariant, a mechanical check, a test for the
@@ -196,8 +229,8 @@ under test, because that is exactly where scaffolders break. A grep-test keeps
196
229
  the universal rules free of any provider mention; the hook-blocking behavior
197
230
  itself is under test; and a weekly lockfile-free run resolves each template's
198
231
  dependencies fresh to catch upstream breakage early. This repo dogfoods its own
199
- rulebook — `CLAUDE.md` and `.claude/` are composed from the templates, and
200
- drift fails the suite.
232
+ rulebook — the Claude and Codex projections are composed from the templates,
233
+ and drift fails the suite.
201
234
 
202
235
  **And the enforcement layer is adversarially reviewed, not just tested.** The
203
236
  Bash guard went through four review rounds with ten reviewers, who executed it
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-agent-rig",
3
- "version": "0.4.0",
3
+ "version": "0.5.0",
4
4
  "description": "Scaffold a new project with an agent operating system (rules, gates, hooks) and a runnable code skeleton",
5
5
  "keywords": [
6
6
  "create",
@@ -10,6 +10,7 @@
10
10
  "template",
11
11
  "agent",
12
12
  "claude-code",
13
+ "codex",
13
14
  "agent-os",
14
15
  "aws-serverless",
15
16
  "node-service"
@@ -2,7 +2,7 @@ import { execFile } from 'node:child_process';
2
2
  import { mkdir, readFile, readdir, stat } from 'node:fs/promises';
3
3
  import path from 'node:path';
4
4
  import { promisify } from 'node:util';
5
- import { copyTree, listTree } from '../lib/copy-tree.js';
5
+ import { copyTree, listTree, mapConcurrent } from '../lib/copy-tree.js';
6
6
  import { ALLOWED_OVERWRITES, detectCollisions } from '../lib/composition.js';
7
7
  import { agentOsLayerDirs } from '../lib/install-set.js';
8
8
  import { sha256, writeManifest } from '../lib/manifest.js';
@@ -82,8 +82,13 @@ export async function createProject(dirArg, options) {
82
82
  async function recordInstall(projectDir, agentOsLayers, transforms, ctx, target) {
83
83
  const files = {};
84
84
  for (const layer of agentOsLayers) {
85
- for (const rel of await listTree(layer.dir, transforms)) {
86
- files[rel] = sha256(await readFile(path.join(projectDir, ...rel.split('/')), 'utf8'));
85
+ const paths = await listTree(layer.dir, transforms);
86
+ const hashes = await mapConcurrent(paths, 16, async (rel) => ({
87
+ rel,
88
+ hash: sha256(await readFile(path.join(projectDir, ...rel.split('/')), 'utf8')),
89
+ }));
90
+ for (const { rel, hash } of hashes) {
91
+ files[rel] = hash;
87
92
  }
88
93
  }
89
94
  await writeManifest(projectDir, {
@@ -1,6 +1,7 @@
1
1
  import { access, mkdir, readFile, writeFile } from 'node:fs/promises';
2
2
  import path from 'node:path';
3
3
  import { settingsForInstalledHooks } from '../lib/init-settings.js';
4
+ import { mapConcurrent } from '../lib/copy-tree.js';
4
5
  import { readManifest, sha256, writeManifest } from '../lib/manifest.js';
5
6
  import { substituteContent } from '../lib/substitute.js';
6
7
  import { agentOsInitDir, agentOsUniversalDir } from '../templates.js';
@@ -8,7 +9,11 @@ import { packageVersion } from '../lib/version.js';
8
9
  /** A user-facing failure: message is printed as-is, no stack trace. */
9
10
  export class InitError extends Error {
10
11
  }
12
+ /** What `--force` answers with now that `upgrade` owns the case it stood in for. */
13
+ export const FORCE_DEPRECATED = 'deprecated — init --force replaced only CLAUDE.md; run create-agent-rig upgrade instead';
11
14
  const SETTINGS = '.claude/settings.json';
15
+ const CODEX_HOOKS = '.codex/hooks.json';
16
+ const MAPS = ['CLAUDE.md', 'AGENTS.md'];
12
17
  async function loadManifest() {
13
18
  const raw = await readFile(path.join(agentOsUniversalDir(), 'layers.json'), 'utf8');
14
19
  return JSON.parse(raw);
@@ -53,15 +58,15 @@ export async function initManifest() {
53
58
  const manifest = await loadManifest();
54
59
  const universal = agentOsUniversalDir();
55
60
  const override = agentOsInitDir();
56
- const files = [];
57
- for (const rel of [...manifest.process, 'CLAUDE.md']) {
61
+ const files = await mapConcurrent([...manifest.process, ...MAPS], 16, async (rel) => {
58
62
  const overridden = path.join(override, rel);
59
- files.push({
63
+ return {
60
64
  rel,
61
65
  source: (await exists(overridden)) ? overridden : path.join(universal, rel),
62
- });
63
- }
66
+ };
67
+ });
64
68
  files.push({ rel: SETTINGS, source: null });
69
+ files.push({ rel: CODEX_HOOKS, source: null });
65
70
  return files;
66
71
  }
67
72
  /**
@@ -82,14 +87,23 @@ export async function initFileContents(repoDir, project) {
82
87
  };
83
88
  const files = await initManifest();
84
89
  const contents = new Map();
85
- for (const { rel, source } of files) {
86
- if (source === null)
87
- continue;
88
- contents.set(rel, substituteContent(await readFile(source, 'utf8'), ctx));
90
+ const sourceFiles = files.filter((file) => file.source !== null);
91
+ const rendered = await mapConcurrent(sourceFiles, 16, async ({ rel, source }) => ({
92
+ rel,
93
+ content: substituteContent(await readFile(source, 'utf8'), ctx),
94
+ }));
95
+ for (const { rel, content } of rendered) {
96
+ contents.set(rel, content);
89
97
  }
90
98
  const installedHooks = new Set(files.map((f) => f.rel).filter((rel) => rel.startsWith('.claude/hooks/')));
91
- const shipped = JSON.parse(await readFile(path.join(agentOsUniversalDir(), SETTINGS), 'utf8'));
99
+ const [shippedSettings, shippedCodexHooks] = await Promise.all([
100
+ readFile(path.join(agentOsUniversalDir(), SETTINGS), 'utf8'),
101
+ readFile(path.join(agentOsUniversalDir(), CODEX_HOOKS), 'utf8'),
102
+ ]);
103
+ const shipped = JSON.parse(shippedSettings);
92
104
  contents.set(SETTINGS, `${JSON.stringify(settingsForInstalledHooks(shipped, installedHooks), null, 2)}\n`);
105
+ const shippedCodex = JSON.parse(shippedCodexHooks);
106
+ contents.set(CODEX_HOOKS, `${JSON.stringify(settingsForInstalledHooks(shippedCodex, installedHooks), null, 2)}\n`);
93
107
  return contents;
94
108
  }
95
109
  /** The process layer as a set of {@link InstalledFile}s — what `upgrade` reads. */
@@ -100,41 +114,49 @@ export async function initInstallSet(repoDir, project) {
100
114
  }
101
115
  export async function planInit(repoDir) {
102
116
  const files = (await initManifest()).map((f) => f.rel);
103
- const conflicts = [];
104
- for (const rel of files) {
105
- if (await exists(path.join(repoDir, rel)))
106
- conflicts.push(rel);
107
- }
117
+ const conflicts = (await mapConcurrent(files, 16, async (rel) => (await exists(path.join(repoDir, rel))) ? rel : null)).filter((rel) => rel !== null);
108
118
  return { files: files.map((p) => ({ path: p })), conflicts };
109
119
  }
110
120
  export async function initProject(repoDir, options) {
121
+ // Refused before anything is read or written, so a deprecated flag cannot
122
+ // half-install: `upgrade` covers what this stood in for, and it decides per
123
+ // file from the manifest instead of overriding one refusal wholesale.
124
+ if (options.force)
125
+ throw new InitError(FORCE_DEPRECATED);
111
126
  const files = (await initManifest()).map((f) => f.rel);
112
- // Refuse to clobber an existing CLAUDE.md unless forced — init edits
113
- // someone's working repository (brief §4, non-negotiable).
114
- if (!options.force && files.includes('CLAUDE.md')) {
115
- if (await exists(path.join(repoDir, 'CLAUDE.md'))) {
116
- throw new InitError('This repo already has a CLAUDE.md. Refusing to overwrite it. ' +
117
- 'Merge the agent-os map in by hand, or re-run with --force to replace it.');
127
+ const previous = await readManifest(repoDir);
128
+ // Refuse to clobber an existing CLAUDE.md — init edits someone's working
129
+ // repository (brief §4, non-negotiable).
130
+ for (const map of MAPS) {
131
+ if (files.includes(map) && (await exists(path.join(repoDir, map)))) {
132
+ // A create rig whose CLAUDE.md was deleted is the legacy route into init.
133
+ // Its generated AGENTS.md must not newly close that route, but only the
134
+ // manifest can distinguish that file from a user's own Codex guidance.
135
+ if (map === 'AGENTS.md' &&
136
+ previous?.files[map] !== undefined &&
137
+ sha256(await readFile(path.join(repoDir, map), 'utf8')) === previous.files[map]) {
138
+ continue;
139
+ }
140
+ throw new InitError(`This repo already has an ${map}. Refusing to overwrite it. ` +
141
+ 'Merge the agent-os map in by hand, or run create-agent-rig upgrade to refresh a rig.');
118
142
  }
119
143
  }
120
144
  const contents = await initFileContents(repoDir);
121
- const written = [];
122
- const skipped = [];
123
145
  const plannedCount = files.length;
124
- for (const rel of files) {
146
+ const actions = await mapConcurrent(files, 16, async (rel) => {
125
147
  const dest = path.join(repoDir, rel);
126
- const isForceableMeta = rel === 'CLAUDE.md';
127
- if ((await exists(dest)) && !(isForceableMeta && options.force)) {
148
+ if (await exists(dest)) {
128
149
  // never overwrite a file init did not write (a user's own copy)
129
- skipped.push(rel);
130
- continue;
150
+ return { rel, verdict: 'skipped' };
131
151
  }
132
152
  if (options.dryRun)
133
- continue;
153
+ return { rel, verdict: 'planned' };
134
154
  await mkdir(path.dirname(dest), { recursive: true });
135
155
  await writeFile(dest, contents.get(rel) ?? '');
136
- written.push(rel);
137
- }
156
+ return { rel, verdict: 'written' };
157
+ });
158
+ const written = actions.filter(({ verdict }) => verdict === 'written').map(({ rel }) => rel);
159
+ const skipped = actions.filter(({ verdict }) => verdict === 'skipped').map(({ rel }) => rel);
138
160
  if (!options.dryRun)
139
161
  await recordInstall(repoDir, written, contents);
140
162
  return { written, skipped, plannedCount };
@@ -147,6 +169,29 @@ export async function initProject(repoDir, options) {
147
169
  * somebody else's — claiming it here would let the next upgrade replace a
148
170
  * user's own document with the rig's. Earlier entries are preserved: a re-run
149
171
  * writes nothing and must not therefore un-remember everything.
172
+ *
173
+ * 🔴 **`kind`, `project` and `stacks` are preserved, not rewritten.** Reached
174
+ * inside a rig `create` produced, this used to stamp `kind: 'init'`,
175
+ * `stacks: []` and an empty `region` over the truth — and `planUpgrade` trusts
176
+ * a manifest wholesale (it never re-detects), so the next upgrade routed to the
177
+ * `init` install set and the stack overlays left the plan entirely: not
178
+ * reported as deleted, not as a conflict, simply absent. `init` describes what
179
+ * it wrote; it does not get to re-describe how the rig was installed.
180
+ *
181
+ * ⚠ **The limit, stated because the fix reads as wider than it is:** this
182
+ * preserves a manifest, so a rig that has none — anything from before 0.4.0 —
183
+ * still gets `kind: 'init'`, no stacks and an empty region, and the advisory in
184
+ * `runInit` stays silent for the same reason. `upgrade`'s `detectInstall`
185
+ * recovers all three from the files on disk, so those values are not
186
+ * unavailable, only unavailable *here*: reaching for it would point
187
+ * `commands/init` at `commands/upgrade`, which already imports this module.
188
+ * The fallback below is the honest floor, not the best available answer.
189
+ *
190
+ * The item that asked for this also floated refusing `init` outright on a
191
+ * `create` manifest. It is already refused a step earlier and for a different
192
+ * reason — {@link initProject} throws on the existing `CLAUDE.md`. The gap that
193
+ * leaves is a `create` rig whose `CLAUDE.md` was deleted, and this function is
194
+ * what makes that case safe.
150
195
  */
151
196
  async function recordInstall(repoDir, written, contents) {
152
197
  const previous = await readManifest(repoDir);
@@ -156,9 +201,12 @@ async function recordInstall(repoDir, written, contents) {
156
201
  files[rel] = sha256(contents.get(rel) ?? '');
157
202
  const manifest = {
158
203
  version: await packageVersion(),
159
- kind: 'init',
160
- project: { name, scope: name, region: '' },
161
- stacks: [],
204
+ kind: previous?.kind ?? 'init',
205
+ // No manifest: fall back to the directory name, which is all this module
206
+ // reads. See the limit above — `upgrade` can do better from the files
207
+ // themselves, and `init` deliberately does not reach for it.
208
+ project: previous?.project ?? { name, scope: name, region: '' },
209
+ stacks: previous?.stacks ?? [],
162
210
  files,
163
211
  };
164
212
  await writeManifest(repoDir, manifest);