create-agent-rig 0.8.0 → 0.9.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.
- package/CHANGELOG.md +105 -1
- package/README.md +92 -3
- package/package.json +4 -3
- package/packages/cli/dist/commands/memory.js +123 -0
- package/packages/cli/dist/commands/setup.js +45 -0
- package/packages/cli/dist/index.js +107 -3
- package/packages/cli/dist/lib/subsystems.js +269 -0
- package/packages/cli/dist/lib/version.js +15 -0
- package/packages/cli/dist/policy/benchmark/corpus.js +165 -0
- package/packages/cli/dist/policy/core/coverage.js +253 -0
- package/packages/cli/dist/policy/core/decision-record.js +130 -44
- package/packages/cli/dist/policy/core/declaration.js +58 -17
- package/packages/cli/dist/policy/core/evidence-matrix.js +94 -0
- package/packages/cli/dist/policy/core/probe.js +442 -0
- package/packages/cli/dist/policy/core/validation.js +194 -1
- package/packages/cli/dist/policy/core/vocabulary.js +70 -3
- package/packages/cli/dist/policy/harness/claude.js +9 -1
- package/packages/cli/dist/policy/harness/codex.js +48 -1
- package/packages/cli/dist/policy/harness/shared-hooks.js +18 -0
- package/packages/cli/dist/policy/index.js +9 -2
- package/templates/agent-os/stack/aws-cdk/.claude/agents/cdk-diff-reviewer.md +2 -0
- package/templates/agent-os/stack/aws-cdk/.codex/agents/cdk-diff-reviewer.toml +2 -0
- package/templates/agent-os/subagent-routing.json +32 -0
- package/templates/agent-os/universal/.agents/skills/loop/SKILL.md +41 -4
- package/templates/agent-os/universal/.claude/agents/code-reviewer.md +2 -0
- package/templates/agent-os/universal/.claude/agents/prose-reviewer.md +2 -0
- package/templates/agent-os/universal/.claude/agents/security-scanner.md +2 -0
- package/templates/agent-os/universal/.claude/agents/test-writer.md +2 -0
- package/templates/agent-os/universal/.claude/hooks/guard-subagent-model.mjs +234 -0
- package/templates/agent-os/universal/.claude/hooks/lib/edit-input.mjs +75 -32
- package/templates/agent-os/universal/.claude/hooks/warn-subagent-routing.mjs +120 -0
- package/templates/agent-os/universal/.claude/rules/workflow.md +5 -0
- package/templates/agent-os/universal/.claude/scripts/preflight.mjs +27 -3
- package/templates/agent-os/universal/.claude/scripts/queue/gate-rounds.mjs +70 -2
- package/templates/agent-os/universal/.claude/scripts/queue/index.mjs +12 -4
- package/templates/agent-os/universal/.claude/scripts/unattended-flag.mjs +64 -1
- package/templates/agent-os/universal/.claude/settings.json +16 -0
- package/templates/agent-os/universal/.claude/skills/loop/SKILL.md +41 -4
- package/templates/agent-os/universal/.codex/agents/code-reviewer.toml +2 -0
- package/templates/agent-os/universal/.codex/agents/prose-reviewer.toml +2 -0
- package/templates/agent-os/universal/.codex/agents/security-scanner.toml +2 -0
- package/templates/agent-os/universal/.codex/agents/test-writer.toml +2 -0
- package/templates/agent-os/universal/.codex/config.toml +3 -0
- package/templates/agent-os/universal/docs/decisions/codex-adapter.md +31 -5
- package/templates/agent-os/universal/docs/decisions/subagent-routing.md +142 -0
- package/templates/agent-os/universal/layers.json +4 -0
- package/templates/hash-history.json +8 -4
- package/templates/release-ledger.json +2 -1
- package/templates/skeleton/node-service/services/api/test/artifact.test.ts +3 -4
- package/templates/skeleton/node-service/services/api/test/package-manager.test.ts +40 -0
- package/templates/skeleton/node-service/services/api/test/package-manager.ts +51 -0
- package/templates/skeleton/node-service/services/api/test/static-dir.test.ts +9 -8
package/CHANGELOG.md
CHANGED
|
@@ -14,6 +14,99 @@ second recorded departure; its own entry states the direction and the reason,
|
|
|
14
14
|
and this paragraph deliberately does not restate them — a numbering rule with
|
|
15
15
|
two copies of its exceptions is the shape 0.8.0 exists to remove.
|
|
16
16
|
|
|
17
|
+
## 0.9.0
|
|
18
|
+
|
|
19
|
+
**The harness ↔ Memory boundary is executable, and the rig is a consumer of
|
|
20
|
+
Memory rather than a host for it.** The CLI gains a `setup --memory-root
|
|
21
|
+
<checkout>` command that records the Memory executable in a machine-scoped
|
|
22
|
+
subsystem manifest after a `--version --json` handshake, and a `memory
|
|
23
|
+
<doctor|load>` command that runs the registered executable through that
|
|
24
|
+
handshake and refuses a foreign contract major with exit 4 before any verb
|
|
25
|
+
runs; the CLI itself answers `--version --json` with its name, version and
|
|
26
|
+
contract version. These are commands of the tool, not files in a project: the
|
|
27
|
+
manifest is machine-scoped and nothing Memory-related is added to what
|
|
28
|
+
`create`, `init` or `upgrade` write into a project. None of this puts
|
|
29
|
+
Memory code inside the rig: the executable is spawned across a process
|
|
30
|
+
boundary and only its handshake is parsed (`docs/command-contract.md`, "The
|
|
31
|
+
version handshake"; `docs/decisions/memory-rig-boundary.md`).
|
|
32
|
+
|
|
33
|
+
**Deprecated: the application skeletons.** `create <dir> [--target <name>]`
|
|
34
|
+
still scaffolds `aws-serverless` and `node-service` in this release, unchanged, and they are
|
|
35
|
+
**scheduled for removal in 0.10.0**. The product boundary the owner fixed on
|
|
36
|
+
2026-09-13 is a package manager for repository-scoped Claude Code and Codex
|
|
37
|
+
configuration — `init` into an existing repository, `upgrade`, `doctor`, the
|
|
38
|
+
Memory handshake — and application scaffolding is outside it. Take `init` for
|
|
39
|
+
a new project's rig; a skeleton generated today is yours and stays yours, but
|
|
40
|
+
`upgrade` will not carry skeleton files forward once they are gone.
|
|
41
|
+
|
|
42
|
+
**Numbered a minor by the rule at the top of this file**: two new commands and
|
|
43
|
+
a new payload are additive.
|
|
44
|
+
|
|
45
|
+
### Added
|
|
46
|
+
|
|
47
|
+
- **`setup --memory-root <checkout> [--memory-ref <sha>] [--dry-run]`** and the
|
|
48
|
+
machine subsystem manifest it writes (`~/.config/create-agent-rig/subsystems.json`,
|
|
49
|
+
`%APPDATA%` on Windows) — RP-147. `upgrade` re-runs the same derivation when a
|
|
50
|
+
manifest exists, so `installedVersion` follows the executable the root holds.
|
|
51
|
+
- **`memory <doctor|load> [args…]`** through the registered executable, with the
|
|
52
|
+
handshake first and Memory's answer passed through unchanged; `--version
|
|
53
|
+
--json` on the rig bin — RP-19. Exit codes on that surface: 0 unsupported /
|
|
54
|
+
absent, 1 integration-failed, 2 invalid invocation, 3 prerequisite unmet, 4
|
|
55
|
+
foreign contract major.
|
|
56
|
+
- **Role-specific subagent routing** in the payload: one routing table
|
|
57
|
+
(`templates/agent-os/subagent-routing.json`) pins each reviewer's model and
|
|
58
|
+
effort for both harnesses; the Claude agent specs carry the pins, the Codex
|
|
59
|
+
profiles are derived from the same table, and two Claude Code hooks —
|
|
60
|
+
`guard-subagent-model` (refuses a call-site `model` override on a pinned
|
|
61
|
+
reviewer) and `warn-subagent-routing` (says so at session start when an
|
|
62
|
+
effort override is in force) — are wired in `.claude/settings.json`
|
|
63
|
+
(RP-166, RP-173; `docs/decisions/subagent-routing.md`).
|
|
64
|
+
- **`preflight` fails on an unreadable configured queue** instead of selecting
|
|
65
|
+
from nothing (RP-56).
|
|
66
|
+
- **Concurrent sessions on one machine** — the ruling on what shared state
|
|
67
|
+
they may touch, and a bounded rename retry so a Windows gate-round counter
|
|
68
|
+
is not lost to a transient lock (RP-120; `docs/decisions/concurrent-sessions.md`).
|
|
69
|
+
- **A mechanical release preflight**, `node scripts/release-preflight.mjs`,
|
|
70
|
+
for the step the owner types by hand (`docs/releasing.md`).
|
|
71
|
+
- **The Rig-side conformance runner** for the Memory boundary,
|
|
72
|
+
`scripts/memory-conformance.mjs --from <checkout> --json`, and the contract
|
|
73
|
+
schemas under `contracts/conformance/v1/` (RP-13) — generator repository
|
|
74
|
+
only, never delivered to a rig; the authoritative cross-repository run lives
|
|
75
|
+
in the private Memory repository and checks this one out at an exact SHA.
|
|
76
|
+
- **The policy benchmark** (`docs/policy-benchmark.md`, RP-111): adapter-process
|
|
77
|
+
evidence for both harnesses on an immutable snapshot of the tree. It remains
|
|
78
|
+
in 0.9.0; whether it stays is a 0.10.0 decision.
|
|
79
|
+
|
|
80
|
+
### Fixed
|
|
81
|
+
|
|
82
|
+
- **`edit-input`** — an unreadable `tool_input` is a refusal on every edit
|
|
83
|
+
surface, never a clean edit (RP-85); a widening `--allow` entry no longer
|
|
84
|
+
leaves an unattended run with no flag on disk, and the `loop` skill verifies
|
|
85
|
+
the flag armed instead of trusting that it did (RP-103).
|
|
86
|
+
- **Capability coverage** — the policy capability contract names exactly what
|
|
87
|
+
the code covers, and the two silent passes it had are closed (RP-36); policy
|
|
88
|
+
validators read own-and-enumerable record fields (RP-153).
|
|
89
|
+
- **Windows** — the node-service skeleton's static-dir tests use a native file
|
|
90
|
+
URL (RP-168); the e2e harness spawns package managers and file checks on
|
|
91
|
+
Windows (RP-169); the one PowerShell case carries its own measured budget
|
|
92
|
+
(RP-162); the benchmark's guard-spawning cases are classified UNVERIFIABLE
|
|
93
|
+
on the hosted Windows image rather than reported green on nothing (RP-111),
|
|
94
|
+
and the Windows governance fixtures are set up under a bound that names the
|
|
95
|
+
stage that timed out (RP-172).
|
|
96
|
+
- **Repository scans skip sibling worktrees** under `.claude/worktrees/`, so a
|
|
97
|
+
second checkout is no longer reported as this one's drift (RP-155).
|
|
98
|
+
|
|
99
|
+
### Generator CI (not a rig-facing change)
|
|
100
|
+
|
|
101
|
+
The pull-request path runs on GitHub-hosted runners only: in `ci.yml`, `ci`
|
|
102
|
+
and the two template checks on Linux and `windows-smoke` (the CLI's unit
|
|
103
|
+
project) on Windows; in `e2e.yml`, the Linux `e2e` job — the full suite with
|
|
104
|
+
the e2e installs and the benchmark — when the pull request touches the CLI,
|
|
105
|
+
the templates, the e2e harness or that workflow. The full suite on Windows
|
|
106
|
+
runs in `e2e.yml` on master, nightly and by dispatch only, and a
|
|
107
|
+
`runner_mode` switch there selects a self-hosted fallback that no pull
|
|
108
|
+
request can reach (`docs/runners.md`).
|
|
109
|
+
|
|
17
110
|
## 0.8.0
|
|
18
111
|
|
|
19
112
|
**Three stale second copies, spread over three payload files a rig obeys,
|
|
@@ -991,7 +1084,18 @@ sometimes earlier (step 6). Everything before that is mechanical:
|
|
|
991
1084
|
`test/template/hash-history.test.ts` › "builds the table from the ledger
|
|
992
1085
|
alone — tags are a warning source, never an input".
|
|
993
1086
|
|
|
994
|
-
8. **Owner:** `npm publish
|
|
1087
|
+
8. **Owner:** `npm publish` — and immediately before it, from the checkout you
|
|
1088
|
+
are about to publish from:
|
|
1089
|
+
|
|
1090
|
+
```sh
|
|
1091
|
+
node scripts/release-preflight.mjs # exit 0, or it names what to fix
|
|
1092
|
+
```
|
|
1093
|
+
|
|
1094
|
+
It checks the manifests, the ledger, the checkout, and the tarball `npm pack`
|
|
1095
|
+
would actually produce. What it looks at is the code, not this list; what it
|
|
1096
|
+
cannot see is stated in its own header. It is a preflight, not a gate —
|
|
1097
|
+
nothing runs it for you, and exit 0 is not a verdict on the release.
|
|
1098
|
+
|
|
995
1099
|
9. **Owner:** smoke the published artifact — `npx create-agent-rig@<version>` in
|
|
996
1100
|
an empty directory, then `pnpm install && pnpm check` inside it; and
|
|
997
1101
|
`upgrade --dry-run` in a rig installed from the previous version.
|
package/README.md
CHANGED
|
@@ -85,6 +85,44 @@ enough for this one, and a replacement that would stop calling a hook the
|
|
|
85
85
|
current wiring names — while that hook's file is still in `.claude/hooks/` — is
|
|
86
86
|
handed over instead.
|
|
87
87
|
|
|
88
|
+
### Registering Memory on this machine
|
|
89
|
+
|
|
90
|
+
Memory is a separate subsystem with its own version; the rig never imports it
|
|
91
|
+
and never searches for it. `setup` records where it is, once per machine:
|
|
92
|
+
|
|
93
|
+
```sh
|
|
94
|
+
npx create-agent-rig@latest setup --memory-root ~/claude-config # the checkout that holds shared-memory/memory.mjs
|
|
95
|
+
npx create-agent-rig@latest setup --memory-root ~/claude-config --dry-run # handshake only, write nothing
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
It derives the invocation from that one root, runs Memory's `--version --json`
|
|
99
|
+
first, and refuses a foreign contract major with exit 4 before writing anything.
|
|
100
|
+
What it writes is one machine-scoped manifest —
|
|
101
|
+
`~/.config/create-agent-rig/subsystems.json` (`%APPDATA%\create-agent-rig\` on
|
|
102
|
+
Windows) — carrying the invocation, the required contract major, the pinned
|
|
103
|
+
Memory ref (`--memory-ref`) and the version the handshake observed. `upgrade`
|
|
104
|
+
re-runs the same derivation when the manifest exists; it never creates one. The
|
|
105
|
+
behaviour is pinned in `packages/cli/test/setup.test.ts` and
|
|
106
|
+
`packages/cli/test/subsystems.test.ts`; the seam itself is ADR-RP-002 R6
|
|
107
|
+
(`docs/decisions/memory-rig-boundary.md`).
|
|
108
|
+
|
|
109
|
+
Both bins answer the same handshake, and the rig consumes Memory only through it
|
|
110
|
+
(RP-19):
|
|
111
|
+
|
|
112
|
+
```sh
|
|
113
|
+
npx create-agent-rig@latest --version --json # {"schemaVersion":1,"name":"create-agent-rig","version":"…","contractVersion":"1.0"}
|
|
114
|
+
npx create-agent-rig@latest memory doctor --json # handshake first, then Memory's doctor, answer passed through unchanged
|
|
115
|
+
npx create-agent-rig@latest memory load --json # same, for load; every argument after the verb goes to Memory verbatim
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
`memory` reads the manifest above, runs Memory's `--version --json`, and only
|
|
119
|
+
then the verb: a foreign contract major exits 4 and the verb never runs; no
|
|
120
|
+
manifest (or an executable that has moved) is `unsupported`/`absent`, exit 0;
|
|
121
|
+
an executable that answers but not as the manifest promised — a broken
|
|
122
|
+
`VERSION`, a malformed handshake — is `integration-failed`, exit 1, never
|
|
123
|
+
"absent". Pinned in `packages/cli/test/memory.test.ts` and
|
|
124
|
+
`packages/cli/test/cli-version.test.ts`.
|
|
125
|
+
|
|
88
126
|
**A file you deleted stays deleted.** The rules invite you to delete the ones
|
|
89
127
|
whose invariant your project does not have, so an upgrade that quietly restored
|
|
90
128
|
them would be undoing your work. With a manifest that is direct — it names the
|
|
@@ -94,13 +132,42 @@ it covers was there to be removed. The single case nothing can tell apart is a
|
|
|
94
132
|
file a **later** release added, which your rig never had — that one is installed,
|
|
95
133
|
and `--dry-run` lists it before anything is written.
|
|
96
134
|
|
|
135
|
+
### Conformance runner
|
|
136
|
+
|
|
137
|
+
`contracts/conformance/v1/` holds the JSON schemas of the command contract's
|
|
138
|
+
`--version --json`, `doctor --json` and `load --json` answers, and
|
|
139
|
+
`scripts/memory-conformance.mjs` checks a Memory checkout against them:
|
|
140
|
+
|
|
141
|
+
```sh
|
|
142
|
+
pnpm build
|
|
143
|
+
node scripts/memory-conformance.mjs --from <claude-config checkout> --json [--out report.json]
|
|
144
|
+
```
|
|
145
|
+
|
|
146
|
+
It is offline by construction — `--from` is mandatory, nothing is fetched, no
|
|
147
|
+
credential is read — and it never imports Memory code or copies a Memory
|
|
148
|
+
fixture into this repository (`test/template/memory-conformance.test.ts` ›
|
|
149
|
+
"carries no fetch, clone or credential: the checkout is always the caller's"
|
|
150
|
+
and › "the repository carries no Memory fixture"). The contract directory is
|
|
151
|
+
this repository's own, not a rig payload: `create`, `init` and `upgrade` do
|
|
152
|
+
not deliver it (`test/template/conformance-contract.test.ts` › "is not
|
|
153
|
+
delivered to rigs: no template carries a conformance contract"). The report's
|
|
154
|
+
rows, its `rigSha` / `memorySha` / `verifierDigest` fields and the `--out`
|
|
155
|
+
file are pinned by the same test file's › "passes every row against a
|
|
156
|
+
well-formed local fixture root and names both SHAs and the verifier digest"
|
|
157
|
+
and › "derives verifierDigest from the runner, its validator and the contract
|
|
158
|
+
files, in that order, and writes the same report to --out". The authoritative
|
|
159
|
+
cross-repository run lives in the private `claude-config` repository, which
|
|
160
|
+
checks this repository out at an explicit full SHA and runs the command above
|
|
161
|
+
against its own tree; the CI here runs only the offline tests.
|
|
162
|
+
|
|
97
163
|
## What you get
|
|
98
164
|
|
|
99
165
|
**A system of boundaries, each held by tooling.** An agent (or a human using
|
|
100
166
|
one) cannot talk its way past them — each guard is a pre-write scan that stops
|
|
101
167
|
the normal path cold (review and tests back it; the claim is stated exactly,
|
|
102
168
|
never inflated). The hook implementations live once in `.claude/hooks/` and are
|
|
103
|
-
wired by both `.claude/settings.json` and `.codex/hooks.json
|
|
169
|
+
wired by both `.claude/settings.json` and `.codex/hooks.json` — except the two
|
|
170
|
+
marked Claude Code, which only `.claude/settings.json` wires:
|
|
104
171
|
|
|
105
172
|
- **`guard-core-purity`** — refuses any edit that puts I/O, clock, randomness,
|
|
106
173
|
environment access, or a non-allowlisted import into the pure domain core;
|
|
@@ -135,7 +202,16 @@ wired by both `.claude/settings.json` and `.codex/hooks.json`:
|
|
|
135
202
|
- **`inject-rules`** — re-injects the autonomy rules at session start, so they
|
|
136
203
|
survive compaction and resumes: the whole file, minus the regions the file
|
|
137
204
|
itself marks as reference. What is left out is a decision written in
|
|
138
|
-
`autonomy.md` on the line above it, not one this hook infers
|
|
205
|
+
`autonomy.md` on the line above it, not one this hook infers;
|
|
206
|
+
- **`guard-subagent-model`** (Claude Code) — refuses an `Agent` dispatch that
|
|
207
|
+
passes a call-site `model` for a subagent whose definition pins one: the
|
|
208
|
+
definition, not the call, decides which model a gate reads with;
|
|
209
|
+
- **`warn-subagent-routing`** (Claude Code) — at session start, warns when
|
|
210
|
+
`CLAUDE_CODE_SUBAGENT_MODEL_FORCE` is set (it replaces every model pin below),
|
|
211
|
+
when `CLAUDE_CODE_EFFORT_LEVEL` is set (it replaces every effort pin), when
|
|
212
|
+
Claude Code is older than 2.1.251 (the unnamed default then replaces the model
|
|
213
|
+
pins), or when its version cannot be read (so the pins cannot be confirmed to
|
|
214
|
+
hold). It warns and never blocks.
|
|
139
215
|
|
|
140
216
|
**A brake that is a real file.** `touch ~/.claude/<project>-loop-STOP` and no
|
|
141
217
|
merge lands until it is removed — enforced at the tool layer, so it holds even if
|
|
@@ -167,6 +243,19 @@ generator for the invariant→hook→test pattern; `post-deploy-verify` and
|
|
|
167
243
|
`ro-debug` on the AWS target), and matching one-page `CLAUDE.md` / `AGENTS.md`
|
|
168
244
|
maps a fresh session orients by.
|
|
169
245
|
|
|
246
|
+
**Each gate reads with a pinned model and effort**, so a SHIP does not change
|
|
247
|
+
meaning with whatever model the session was started on. `code-reviewer`,
|
|
248
|
+
`security-scanner` and `cdk-diff-reviewer` pin `claude-opus-5`; `test-writer` and
|
|
249
|
+
`prose-reviewer` pin `claude-sonnet-5`; all pin `high` effort — and their Codex
|
|
250
|
+
profiles pin `gpt-5.6-sol` / `gpt-5.6-terra` from the same role table. A subagent
|
|
251
|
+
with no definition defaults to `claude-sonnet-5` through
|
|
252
|
+
`CLAUDE_CODE_SUBAGENT_MODEL` in `.claude/settings.json`; its effort cannot be
|
|
253
|
+
pinned and follows the session. The driver session's own model and effort stay
|
|
254
|
+
yours. To change a role in a generated project, edit `model:` / `effort:` in its
|
|
255
|
+
`.claude/agents/<role>.md` — and the matching `.codex/agents/<role>.toml` — in a
|
|
256
|
+
reviewed change; `upgrade` then reports the edited file as yours instead of
|
|
257
|
+
replacing it. Why these values, and what voids them: `docs/decisions/subagent-routing.md`.
|
|
258
|
+
|
|
170
259
|
**The hooks are examples, not laws.** `.claude/rules/invariants.md` states the
|
|
171
260
|
pattern behind each one — a stated invariant, a mechanical check, a test for the
|
|
172
261
|
check — so you can delete the ones whose invariant your project does not have and
|
|
@@ -249,6 +338,6 @@ that fails open must do provably bounded work, because fail-open turns every lin
|
|
|
249
338
|
of its own work into a potential bypass.
|
|
250
339
|
|
|
251
340
|
Development (from a clone — `PLAN.md` and `demo.sh` live in the repository, not
|
|
252
|
-
in the published tarball): `pnpm test` (full), `pnpm test:unit` (fast loop),
|
|
341
|
+
in the published tarball): `pnpm test` (full), `pnpm test:unit` (fast loop), `pnpm test:smoke` (the unit project only — the Windows pull-request lane),
|
|
253
342
|
`pnpm template:check` (templates in place). The plan of record is `PLAN.md`;
|
|
254
343
|
release notes and the release checklist ship in `CHANGELOG.md`.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "create-agent-rig",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.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",
|
|
@@ -43,7 +43,8 @@
|
|
|
43
43
|
"format": "prettier --write .",
|
|
44
44
|
"typecheck": "tsc -p packages/cli/tsconfig.json && tsc -p tsconfig.json",
|
|
45
45
|
"test": "pnpm build && vitest run",
|
|
46
|
-
"test:unit": "vitest run --project unit --project template",
|
|
46
|
+
"test:unit": "vitest run --project unit --project template --project benchmark",
|
|
47
|
+
"test:smoke": "vitest run --project unit",
|
|
47
48
|
"test:e2e": "pnpm build && vitest run --project e2e",
|
|
48
49
|
"template:install": "pnpm --dir templates/skeleton/aws-serverless install",
|
|
49
50
|
"template:check": "pnpm --dir templates/skeleton/aws-serverless run check"
|
|
@@ -59,6 +60,6 @@
|
|
|
59
60
|
"prettier": "^3.9.6",
|
|
60
61
|
"typescript": "~6.0.3",
|
|
61
62
|
"typescript-eslint": "^8.65.0",
|
|
62
|
-
"vitest": "^
|
|
63
|
+
"vitest": "^5.0.0"
|
|
63
64
|
}
|
|
64
65
|
}
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
// `create-agent-rig memory <doctor|load> [args…]`: the Rig side of the
|
|
2
|
+
// RP-19 version handshake. The Rig is a CONSUMER of the Memory subsystem: it
|
|
3
|
+
// resolves the executable from the machine subsystem manifest `setup` wrote
|
|
4
|
+
// (RP-147), performs `--version --json` first, refuses a foreign contract
|
|
5
|
+
// major with exit 4 before doctor/load ever run, and otherwise hands the verb
|
|
6
|
+
// and its arguments to Memory verbatim and returns Memory's answer unchanged.
|
|
7
|
+
//
|
|
8
|
+
// What it never does, by construction: import Memory code, read Memory's
|
|
9
|
+
// storage tree, or reinterpret a doctor/load payload — the only Memory bytes
|
|
10
|
+
// it parses are the handshake's (`docs/command-contract.md`, "The version
|
|
11
|
+
// handshake"; "Storage-tree ownership"). Pinned in packages/cli/test/memory.test.ts.
|
|
12
|
+
//
|
|
13
|
+
// Exit codes on this surface follow the contract: 0 for UNSUPPORTED/absent
|
|
14
|
+
// (Memory is not registered here, or its executable is gone — the relocation
|
|
15
|
+
// case), 1 for INTEGRATION-FAILED (the executable answered, but not with a
|
|
16
|
+
// handshake the manifest promised — a broken VERSION is `manifest-stale`,
|
|
17
|
+
// never `absent`), 3 for an unmet prerequisite (no configuration root: APPDATA
|
|
18
|
+
// or HOME unset), 4 for a foreign contract major. For 0, 1, 3 and 4 the stdout
|
|
19
|
+
// is exactly one JSON object with no file path in it; an invalid invocation
|
|
20
|
+
// (exit 2 — no verb, or one outside doctor/load) writes nothing to stdout and
|
|
21
|
+
// names the verbs on stderr; human hints always go to stderr.
|
|
22
|
+
import { execFileRunner } from './setup.js';
|
|
23
|
+
import { MEMORY_CONTRACT_MAJOR, SubsystemsError, handshake, readSubsystemsManifest, subsystemsManifestPath, } from '../lib/subsystems.js';
|
|
24
|
+
const MEMORY_VERBS = ['doctor', 'load'];
|
|
25
|
+
/** doctor/load do real work; the caller-supplied `--timeout-ms` is Memory's own bound. */
|
|
26
|
+
const VERB_TIMEOUT_MS = 60_000;
|
|
27
|
+
const jsonLine = (payload) => `${JSON.stringify(payload)}\n`;
|
|
28
|
+
const isVerb = (verb) => MEMORY_VERBS.includes(verb);
|
|
29
|
+
export async function runMemory(options) {
|
|
30
|
+
if (!isVerb(options.verb)) {
|
|
31
|
+
return {
|
|
32
|
+
exitCode: 2,
|
|
33
|
+
stdout: '',
|
|
34
|
+
stderr: `memory needs a verb: create-agent-rig memory <${MEMORY_VERBS.join('|')}> [args…]\n`,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
const env = options.env ?? process.env;
|
|
38
|
+
const platform = options.platform ?? process.platform;
|
|
39
|
+
const run = options.run ?? execFileRunner;
|
|
40
|
+
let file;
|
|
41
|
+
try {
|
|
42
|
+
file = subsystemsManifestPath(env, platform);
|
|
43
|
+
}
|
|
44
|
+
catch (error) {
|
|
45
|
+
if (error instanceof SubsystemsError && error.code === 'config-root-unavailable') {
|
|
46
|
+
// Nothing was attempted: without a configuration root there is no place
|
|
47
|
+
// the manifest could be. The contract's first exit-3 occasion — a
|
|
48
|
+
// variable that is not set — with the variable named, never a path.
|
|
49
|
+
const variable = platform === 'win32' ? 'APPDATA' : 'HOME';
|
|
50
|
+
return {
|
|
51
|
+
exitCode: 3,
|
|
52
|
+
stdout: jsonLine({
|
|
53
|
+
schemaVersion: 1,
|
|
54
|
+
result: 'prerequisites-unmet',
|
|
55
|
+
missing: [{ kind: 'environment', name: variable, detail: 'not set' }],
|
|
56
|
+
}),
|
|
57
|
+
stderr: `memory: ${variable} is not set, so there is no configuration root to read the subsystem manifest from\n`,
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
throw error;
|
|
61
|
+
}
|
|
62
|
+
let manifest;
|
|
63
|
+
try {
|
|
64
|
+
manifest = await readSubsystemsManifest(file);
|
|
65
|
+
}
|
|
66
|
+
catch (error) {
|
|
67
|
+
if (error instanceof SubsystemsError)
|
|
68
|
+
return {
|
|
69
|
+
exitCode: 1,
|
|
70
|
+
stdout: jsonLine({ schemaVersion: 1, result: 'integration-failed', reason: 'unreadable' }),
|
|
71
|
+
stderr: `memory: the subsystem manifest is not readable (${error.code}); run setup --memory-root again\n`,
|
|
72
|
+
};
|
|
73
|
+
throw error;
|
|
74
|
+
}
|
|
75
|
+
if (manifest === null)
|
|
76
|
+
return {
|
|
77
|
+
exitCode: 0,
|
|
78
|
+
stdout: jsonLine({ schemaVersion: 1, result: 'unsupported', reason: 'absent' }),
|
|
79
|
+
stderr: 'memory: Memory is not registered on this machine; run setup --memory-root <checkout>\n',
|
|
80
|
+
};
|
|
81
|
+
const entry = manifest.entries.memory;
|
|
82
|
+
const result = await handshake(entry, run);
|
|
83
|
+
if (result.status === 'foreign-major')
|
|
84
|
+
return {
|
|
85
|
+
exitCode: 4,
|
|
86
|
+
stdout: jsonLine({
|
|
87
|
+
schemaVersion: 1,
|
|
88
|
+
result: 'foreign-major',
|
|
89
|
+
contractVersion: result.contractVersion,
|
|
90
|
+
requiredMajor: MEMORY_CONTRACT_MAJOR,
|
|
91
|
+
}),
|
|
92
|
+
stderr: `memory: the registered Memory implements contract ${result.contractVersion}, ` +
|
|
93
|
+
`this rig requires major ${MEMORY_CONTRACT_MAJOR}; ${options.verb} was not run\n`,
|
|
94
|
+
};
|
|
95
|
+
if (result.status === 'unsupported')
|
|
96
|
+
return {
|
|
97
|
+
exitCode: 0,
|
|
98
|
+
stdout: jsonLine({ schemaVersion: 1, result: 'unsupported', reason: 'absent' }),
|
|
99
|
+
stderr: 'memory: the registered Memory executable is no longer there; ' +
|
|
100
|
+
'run setup --memory-root <checkout> again\n',
|
|
101
|
+
};
|
|
102
|
+
if (result.status === 'integration-failed')
|
|
103
|
+
return {
|
|
104
|
+
exitCode: 1,
|
|
105
|
+
stdout: jsonLine({ schemaVersion: 1, result: 'integration-failed', reason: result.reason }),
|
|
106
|
+
stderr: `memory: the handshake did not answer as the manifest promised (${result.reason}); ${options.verb} was not run\n`,
|
|
107
|
+
};
|
|
108
|
+
const [command, script] = entry.invocation;
|
|
109
|
+
const answer = await run(command, [script, options.verb, ...options.args], {
|
|
110
|
+
timeoutMs: VERB_TIMEOUT_MS,
|
|
111
|
+
});
|
|
112
|
+
if (answer.spawnError)
|
|
113
|
+
return {
|
|
114
|
+
exitCode: 1,
|
|
115
|
+
stdout: jsonLine({
|
|
116
|
+
schemaVersion: 1,
|
|
117
|
+
result: 'integration-failed',
|
|
118
|
+
reason: 'backend-failed',
|
|
119
|
+
}),
|
|
120
|
+
stderr: `memory: ${options.verb} could not be started (${answer.spawnError.code ?? 'spawn error'})\n`,
|
|
121
|
+
};
|
|
122
|
+
return { exitCode: answer.code, stdout: answer.stdout, stderr: answer.stderr };
|
|
123
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
// `create-agent-rig setup --memory-root <checkout>`: the sole writer of the
|
|
2
|
+
// machine subsystem manifest for 0.9.0 (ADR-RP-002 R6, RP-147). Order is the
|
|
3
|
+
// contract: derive the entry from the one declared root, perform the
|
|
4
|
+
// `--version --json` handshake, refuse a foreign contract major with exit 4
|
|
5
|
+
// before anything is written, then write the manifest atomically.
|
|
6
|
+
import { execFile } from 'node:child_process';
|
|
7
|
+
import { SUBSYSTEMS_SCHEMA_VERSION, SubsystemsError, deriveMemoryEntry, handshake, subsystemsManifestPath, writeSubsystemsManifest, } from '../lib/subsystems.js';
|
|
8
|
+
/** The default runner: one child, one JSON line, a bounded wait. */
|
|
9
|
+
export const execFileRunner = (file, args, { timeoutMs }) => new Promise((resolve) => {
|
|
10
|
+
execFile(file, args, { timeout: timeoutMs, maxBuffer: 64 * 1024, windowsHide: true }, (error, stdout, stderr) => {
|
|
11
|
+
if (error && error.code === 'ENOENT')
|
|
12
|
+
resolve({ code: 1, stdout, stderr, spawnError: error });
|
|
13
|
+
else if (error && typeof error.code === 'number')
|
|
14
|
+
resolve({ code: error.code, stdout, stderr });
|
|
15
|
+
else if (error)
|
|
16
|
+
resolve({ code: 1, stdout, stderr, spawnError: error });
|
|
17
|
+
else
|
|
18
|
+
resolve({ code: 0, stdout, stderr });
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
export async function setupSubsystems(options) {
|
|
22
|
+
const env = options.env ?? process.env;
|
|
23
|
+
const platform = options.platform ?? process.platform;
|
|
24
|
+
const run = options.run ?? execFileRunner;
|
|
25
|
+
const file = subsystemsManifestPath(env, platform);
|
|
26
|
+
const entry = deriveMemoryEntry({
|
|
27
|
+
memoryRoot: options.memoryRoot,
|
|
28
|
+
nodeExecutable: options.nodeExecutable ?? process.execPath,
|
|
29
|
+
memoryRef: options.memoryRef ?? null,
|
|
30
|
+
installedVersion: '',
|
|
31
|
+
}, platform);
|
|
32
|
+
const result = await handshake(entry, run);
|
|
33
|
+
if (result.status === 'foreign-major')
|
|
34
|
+
return { outcome: 'refused', exitCode: 4, handshake: result };
|
|
35
|
+
if (result.status !== 'ok')
|
|
36
|
+
return { outcome: 'refused', exitCode: 1, handshake: result };
|
|
37
|
+
if (options.dryRun === true)
|
|
38
|
+
return { outcome: 'dry-run', file, handshake: result };
|
|
39
|
+
await writeSubsystemsManifest(file, {
|
|
40
|
+
schemaVersion: SUBSYSTEMS_SCHEMA_VERSION,
|
|
41
|
+
entries: { memory: { ...entry, installedVersion: result.version } },
|
|
42
|
+
});
|
|
43
|
+
return { outcome: 'written', file, handshake: result };
|
|
44
|
+
}
|
|
45
|
+
export { SubsystemsError };
|
|
@@ -2,13 +2,16 @@
|
|
|
2
2
|
import { parseArgs } from 'node:util';
|
|
3
3
|
import { CreateError, createProject } from './commands/create.js';
|
|
4
4
|
import { InitError, initFileContents, initProject, planInit } from './commands/init.js';
|
|
5
|
+
import { execFileRunner, setupSubsystems } from './commands/setup.js';
|
|
5
6
|
import { UpgradeError, applyUpgrade, planUpgrade } from './commands/upgrade.js';
|
|
6
7
|
import { makePalette } from './lib/colors.js';
|
|
7
8
|
import { readManifest } from './lib/manifest.js';
|
|
9
|
+
import { SubsystemsError, refreshSubsystems, subsystemsManifestPath } from './lib/subsystems.js';
|
|
8
10
|
import { promptConfirm, promptTarget } from './lib/prompts.js';
|
|
9
11
|
import { collectGovernance, renderSummary } from './lib/summary.js';
|
|
10
12
|
import { DEFAULT_TARGET, TARGET_NAMES } from './lib/targets.js';
|
|
11
|
-
import { packageVersion } from './lib/version.js';
|
|
13
|
+
import { packageVersion, rigHandshake } from './lib/version.js';
|
|
14
|
+
import { runMemory } from './commands/memory.js';
|
|
12
15
|
const USAGE = `Usage: create-agent-rig <dir> [options]
|
|
13
16
|
|
|
14
17
|
Scaffolds a new project into <dir>: a Claude Code + Codex agent operating system
|
|
@@ -20,7 +23,8 @@ Options
|
|
|
20
23
|
required when not a terminal — default: ${DEFAULT_TARGET})
|
|
21
24
|
--no-git skip git init + the pristine-template baseline commit
|
|
22
25
|
--no-color plain output (NO_COLOR is respected too)
|
|
23
|
-
--version print the version
|
|
26
|
+
--version print the version (--version --json: the contract handshake,
|
|
27
|
+
one JSON object with the name, version and contract version)
|
|
24
28
|
-h, --help this text
|
|
25
29
|
|
|
26
30
|
Also: create-agent-rig init [--dry-run]
|
|
@@ -32,7 +36,68 @@ Also: create-agent-rig init [--dry-run]
|
|
|
32
36
|
|
|
33
37
|
Also: create-agent-rig upgrade [--dry-run] [--yes]
|
|
34
38
|
Bring the rig in the CURRENT repo up to this version. Replaces the files it
|
|
35
|
-
installed and you did not touch; everything else is reported, never merged
|
|
39
|
+
installed and you did not touch; everything else is reported, never merged.
|
|
40
|
+
Re-runs the subsystem manifest derivation when one exists (see setup).
|
|
41
|
+
|
|
42
|
+
Also: create-agent-rig setup --memory-root <checkout> [--memory-ref <sha>] [--dry-run]
|
|
43
|
+
Record the Memory executable in this machine's subsystem manifest
|
|
44
|
+
(~/.config/create-agent-rig/subsystems.json; %APPDATA% on Windows) from the
|
|
45
|
+
one declared root. Performs the --version --json handshake first and refuses
|
|
46
|
+
a foreign contract major with exit 4 before writing anything.
|
|
47
|
+
|
|
48
|
+
Also: create-agent-rig memory <doctor|load> [args…]
|
|
49
|
+
Run a Memory verb through the registered executable: the --version --json
|
|
50
|
+
handshake first (a foreign contract major exits 4 and the verb never runs),
|
|
51
|
+
then the verb and its arguments verbatim, Memory's answer passed through
|
|
52
|
+
unchanged. No manifest answers unsupported/absent (exit 0); an invalid
|
|
53
|
+
invocation exits 2.`;
|
|
54
|
+
async function runSetup(rawArgs) {
|
|
55
|
+
let values;
|
|
56
|
+
try {
|
|
57
|
+
({ values } = parseArgs({
|
|
58
|
+
args: rawArgs,
|
|
59
|
+
options: {
|
|
60
|
+
'memory-root': { type: 'string' },
|
|
61
|
+
'memory-ref': { type: 'string' },
|
|
62
|
+
'dry-run': { type: 'boolean' },
|
|
63
|
+
'no-color': { type: 'boolean' },
|
|
64
|
+
},
|
|
65
|
+
allowPositionals: false,
|
|
66
|
+
}));
|
|
67
|
+
}
|
|
68
|
+
catch (error) {
|
|
69
|
+
process.stderr.write(`${error.message}\n\n${USAGE}\n`);
|
|
70
|
+
return 1;
|
|
71
|
+
}
|
|
72
|
+
const memoryRoot = values['memory-root'];
|
|
73
|
+
if (memoryRoot === undefined) {
|
|
74
|
+
process.stderr.write(`setup needs --memory-root <checkout>\n\n${USAGE}\n`);
|
|
75
|
+
return 1;
|
|
76
|
+
}
|
|
77
|
+
try {
|
|
78
|
+
const result = await setupSubsystems({
|
|
79
|
+
memoryRoot,
|
|
80
|
+
memoryRef: values['memory-ref'] ?? null,
|
|
81
|
+
dryRun: values['dry-run'] === true,
|
|
82
|
+
});
|
|
83
|
+
if (result.outcome === 'refused') {
|
|
84
|
+
process.stderr.write(`Memory handshake: ${JSON.stringify(result.handshake)}\n`);
|
|
85
|
+
return result.exitCode;
|
|
86
|
+
}
|
|
87
|
+
process.stdout.write(`Memory handshake: ${JSON.stringify(result.handshake)}\n` +
|
|
88
|
+
(result.outcome === 'dry-run'
|
|
89
|
+
? `Dry run — nothing written (would write ${result.file}).\n`
|
|
90
|
+
: `Wrote ${result.file}\n`));
|
|
91
|
+
return 0;
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
if (error instanceof SubsystemsError) {
|
|
95
|
+
process.stderr.write(`setup: ${error.message} (${error.code})\n`);
|
|
96
|
+
return 1;
|
|
97
|
+
}
|
|
98
|
+
throw error;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
36
101
|
async function runInit(rawArgs) {
|
|
37
102
|
let values;
|
|
38
103
|
try {
|
|
@@ -219,15 +284,47 @@ async function runUpgrade(rawArgs) {
|
|
|
219
284
|
}
|
|
220
285
|
const result = await applyUpgrade(cwd, plan);
|
|
221
286
|
process.stdout.write(`\nWrote ${result.written.length} files.\n`);
|
|
287
|
+
// The subsystem manifest is machine-scoped and written by `setup`; an
|
|
288
|
+
// upgrade re-runs the same derivation so `installedVersion` follows the
|
|
289
|
+
// executable the root now holds. It is never part of the rig manifest or of
|
|
290
|
+
// `result.written` — that file list is the repository's, this one is the
|
|
291
|
+
// user's. Absent stays absent: creating it is `setup`'s act.
|
|
292
|
+
try {
|
|
293
|
+
const refreshed = await refreshSubsystems({
|
|
294
|
+
file: subsystemsManifestPath(process.env, process.platform),
|
|
295
|
+
run: execFileRunner,
|
|
296
|
+
nodeExecutable: process.execPath,
|
|
297
|
+
platform: process.platform,
|
|
298
|
+
});
|
|
299
|
+
if (refreshed !== 'absent')
|
|
300
|
+
process.stdout.write(`Subsystem manifest: ${typeof refreshed === 'string' ? refreshed : JSON.stringify(refreshed)}\n`);
|
|
301
|
+
}
|
|
302
|
+
catch (error) {
|
|
303
|
+
if (!(error instanceof SubsystemsError))
|
|
304
|
+
throw error;
|
|
305
|
+
process.stdout.write(`Subsystem manifest: not refreshed — ${error.message} (${error.code})\n`);
|
|
306
|
+
}
|
|
222
307
|
return 0;
|
|
223
308
|
}
|
|
224
309
|
async function main() {
|
|
225
310
|
if (process.argv[2] === 'init') {
|
|
226
311
|
return runInit(process.argv.slice(3));
|
|
227
312
|
}
|
|
313
|
+
if (process.argv[2] === 'setup') {
|
|
314
|
+
return runSetup(process.argv.slice(3));
|
|
315
|
+
}
|
|
228
316
|
if (process.argv[2] === 'upgrade') {
|
|
229
317
|
return runUpgrade(process.argv.slice(3));
|
|
230
318
|
}
|
|
319
|
+
if (process.argv[2] === 'memory') {
|
|
320
|
+
// The consumer path of the RP-19 handshake: manifest → `--version --json`
|
|
321
|
+
// → exit 4 on a foreign major → doctor/load passed through verbatim.
|
|
322
|
+
const [verb = '', ...args] = process.argv.slice(3);
|
|
323
|
+
const result = await runMemory({ verb, args });
|
|
324
|
+
process.stdout.write(result.stdout);
|
|
325
|
+
process.stderr.write(result.stderr);
|
|
326
|
+
return result.exitCode;
|
|
327
|
+
}
|
|
231
328
|
let positionals;
|
|
232
329
|
let values;
|
|
233
330
|
try {
|
|
@@ -237,6 +334,9 @@ async function main() {
|
|
|
237
334
|
help: { type: 'boolean', short: 'h' },
|
|
238
335
|
target: { type: 'string' },
|
|
239
336
|
version: { type: 'boolean' },
|
|
337
|
+
// `--json` is read on `--version` alone: the handshake object of
|
|
338
|
+
// docs/command-contract.md, one JSON line and nothing else on stdout.
|
|
339
|
+
json: { type: 'boolean' },
|
|
240
340
|
'no-git': { type: 'boolean' },
|
|
241
341
|
'no-color': { type: 'boolean' },
|
|
242
342
|
},
|
|
@@ -248,6 +348,10 @@ async function main() {
|
|
|
248
348
|
return 1;
|
|
249
349
|
}
|
|
250
350
|
if (values.version) {
|
|
351
|
+
if (values.json) {
|
|
352
|
+
process.stdout.write(`${JSON.stringify(await rigHandshake())}\n`);
|
|
353
|
+
return 0;
|
|
354
|
+
}
|
|
251
355
|
process.stdout.write(`${await packageVersion()}\n`);
|
|
252
356
|
return 0;
|
|
253
357
|
}
|