create-agent-rig 0.3.1 → 0.4.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 (36) hide show
  1. package/CHANGELOG.md +192 -6
  2. package/README.md +40 -2
  3. package/package.json +1 -1
  4. package/packages/cli/dist/commands/create.js +40 -10
  5. package/packages/cli/dist/commands/init.js +41 -3
  6. package/packages/cli/dist/commands/upgrade.js +300 -0
  7. package/packages/cli/dist/index.js +100 -13
  8. package/packages/cli/dist/lib/copy-tree.js +9 -1
  9. package/packages/cli/dist/lib/git-env.js +48 -0
  10. package/packages/cli/dist/lib/history.js +49 -0
  11. package/packages/cli/dist/lib/install-set.js +46 -0
  12. package/packages/cli/dist/lib/manifest.js +99 -0
  13. package/packages/cli/dist/lib/prompts.js +20 -0
  14. package/packages/cli/dist/lib/safe-path.js +41 -0
  15. package/packages/cli/dist/lib/substitute.js +32 -0
  16. package/packages/cli/dist/lib/targets.js +15 -1
  17. package/packages/cli/dist/lib/version.js +15 -0
  18. package/scripts/prepare.mjs +54 -17
  19. package/templates/agent-os/init/CLAUDE.md +11 -5
  20. package/templates/agent-os/universal/.claude/agents/code-reviewer.md +15 -0
  21. package/templates/agent-os/universal/.claude/agents/prose-reviewer.md +104 -0
  22. package/templates/agent-os/universal/.claude/hooks/gate-stop-dod.mjs +20 -0
  23. package/templates/agent-os/universal/.claude/rules/workflow.md +4 -0
  24. package/templates/agent-os/universal/.claude/scripts/detect-missed-gate.mjs +32 -4
  25. package/templates/agent-os/universal/.claude/scripts/preflight.mjs +34 -1
  26. package/templates/agent-os/universal/.claude/scripts/queue/core.mjs +125 -0
  27. package/templates/agent-os/universal/.claude/scripts/queue/github-issues.mjs +6 -0
  28. package/templates/agent-os/universal/.claude/scripts/queue/jira.mjs +3 -0
  29. package/templates/agent-os/universal/.claude/scripts/queue/plan-md.mjs +6 -0
  30. package/templates/agent-os/universal/.claude/skills/check-premises/SKILL.md +125 -0
  31. package/templates/agent-os/universal/.claude/skills/loop/SKILL.md +36 -9
  32. package/templates/agent-os/universal/.claude/skills/pr-ship/SKILL.md +12 -2
  33. package/templates/agent-os/universal/CLAUDE.md +12 -3
  34. package/templates/agent-os/universal/PLAN.md +14 -3
  35. package/templates/agent-os/universal/layers.json +2 -0
  36. package/templates/hash-history.json +263 -0
package/CHANGELOG.md CHANGED
@@ -7,6 +7,174 @@ the generator.
7
7
  Versions are published to npm as [`create-agent-rig`](https://www.npmjs.com/package/create-agent-rig);
8
8
  `npx github:serhii-baksheiev/create-agent-rig` keeps working for either path.
9
9
 
10
+ Numbering is ordinary semver — **additive is a minor, a fix is a patch** — so
11
+ that "I only take minors" remains a usable policy; 0.3.2 shipped additive
12
+ content as a patch by the owner's call and stays recorded as one.
13
+
14
+ ## 0.4.0
15
+
16
+ Upgrading is a command now: **`npx create-agent-rig@0.4.0 upgrade`** (`@latest`
17
+ once you know what latest is — this section will not). That sentence replaces
18
+ the six-file manual procedure 0.3.2 had to print, and it is the whole point of
19
+ this release: a rig you cannot bring forward stops being maintained at whatever
20
+ version you installed it at.
21
+
22
+ Read the [Upgrading](README.md#upgrading-a-rig-you-already-have) section before
23
+ the first run on an existing rig; `--dry-run` prints the plan and writes
24
+ nothing.
25
+
26
+ ### Added
27
+
28
+ - **`create-agent-rig upgrade [--dry-run] [--yes]`** — brings an installed rig
29
+ to this version: it replaces the files the rig wrote **and you have not
30
+ touched**, installs what the release adds, and **reports everything else**.
31
+ There is no three-way merge and no patching, by decision rather than
32
+ omission: silently folding your edits into the documents an agent loop obeys
33
+ is how a rig stops meaning what its owner thinks it means. Every conflict
34
+ names the file, why it was kept, and the path to the new version, so the diff
35
+ you may want is one command away.
36
+ - **`.claude/.rig-manifest.json`** — written by `create` and by `init`: the rig
37
+ version and a hash per installed file. **Commit it.** It is what lets an
38
+ upgrade tell a file the rig wrote from a file you own, and without it in the
39
+ repository the command is blind on CI and on a colleague's machine. `init`
40
+ records only files it actually wrote — never one it kept, which would be
41
+ claiming somebody else's document.
42
+ - **A released-hash table travels in the package**, generated from the git tags
43
+ at release time and never by hand. It is what makes a 0.3.x rig upgradable at
44
+ all, and it answers a second question too: a file that shipped in every
45
+ release it covers and is gone from disk was **deleted on purpose**, so it
46
+ stays deleted. The rules tell you to delete the invariants your project does
47
+ not have; an upgrade that quietly restored them would be undoing your work.
48
+ - **The `loop` skill writes back what a close unblocked**, in a required
49
+ journal field with three distinct answers — the items that were waiting, by
50
+ name; "nothing was waiting"; or "this queue has no dependency links" where
51
+ the adapter cannot answer at all. It is a **report, not an edit** to those
52
+ items: correcting queue state by hand destroys the evidence that the state is
53
+ unreliable, which the rules forbid by name.
54
+
55
+ ### Two things `upgrade` deliberately will not do
56
+
57
+ - **Replace `.claude/settings.json`.** It is where your own hooks live, so the
58
+ new wiring is printed for you to merge — in the dry run too. The cost is
59
+ real and stated: a release that adds a hook delivers the file and not its
60
+ wiring, and whether a manifest-proven-unmodified settings file should be
61
+ refreshed is an open decision for 0.5.
62
+ - **Touch the skeleton.** After `create`, the code is your project. The manifest
63
+ covers the agent-os layer and nothing else.
64
+
65
+ ### Deferred, and on what condition
66
+
67
+ - The clarify-gate (`C-0…C-2`) — unchanged from 0.3.2: it enters once that gate
68
+ has fired at least once anywhere. Until then there is nothing to copy but an
69
+ intention.
70
+ - `init --force` now overlaps `upgrade`, and its future is an open question
71
+ rather than a deprecation: decided in 0.5, unchanged here.
72
+
73
+ ## 0.3.2
74
+
75
+ Numbered as a patch by the owner's call; the content below is additive, so
76
+ nothing that shipped in 0.3.1 changed shape.
77
+
78
+ A generated project gains two review gates it did not have — one before the work
79
+ starts, one over the prose that instructs it — and three more queue-hygiene
80
+ checks.
81
+
82
+ **Upgrading an existing rig: `init` alone is not enough, and here is exactly
83
+ why.** `create-agent-rig init` installs files that are not there and **keeps
84
+ every file that is** — `--force` replaces `CLAUDE.md` and nothing else
85
+ (`packages/cli/src/commands/init.ts`). Re-running it on a 0.3.1 rig therefore
86
+ delivers the two new files, `.claude/agents/prose-reviewer.md` and
87
+ `.claude/skills/check-premises/SKILL.md`, and **none of their wiring**: the
88
+ skill arrives with nothing calling it, and the agent arrives with `pr-ship`
89
+ never launching it. Six files below changed rather than appeared, and `init`
90
+ will not touch them:
91
+
92
+ ```
93
+ .claude/agents/code-reviewer.md # the sixth blocking item
94
+ .claude/skills/loop/SKILL.md # calls check-premises, and §3/§6/§8
95
+ .claude/skills/pr-ship/SKILL.md # fans out prose-reviewer, passes the item
96
+ .claude/scripts/queue/core.mjs # the three hygiene checks + Ticket.body
97
+ .claude/scripts/detect-missed-gate.mjs # sees a rulebook outside the repo root
98
+ .claude/hooks/gate-stop-dod.mjs # judges the tree it is in
99
+ ```
100
+
101
+ Delete those six and re-run `init`, or copy them across by hand. This note tells
102
+ you the manual steps rather than an easy sentence that leaves half the release
103
+ inert — that failure mode is the whole subject of 0.3.1, immediately below.
104
+
105
+ > **Superseded in 0.4.0.** `create-agent-rig upgrade` delivers exactly these
106
+ > files, and the ones every release after it changes. The procedure above is
107
+ > kept as the record of what 0.3.2 asked of its users; do not follow it if you
108
+ > have 0.4.0 or later.
109
+
110
+ ### Added
111
+
112
+ - **`check-premises` skill** — a queue item is a _claim about the code_, written
113
+ by someone who was not reading the code at the time, and nothing downstream
114
+ re-checks it: the failing test is written against the item, the implementation
115
+ against the test, and the reviewer compares the diff to the item. A false
116
+ premise therefore produces work that is correct, tested, reviewed and useless.
117
+ The skill runs between taking the item and the Red step, is read-only by
118
+ frontmatter so it cannot start implementing, and returns `PREMISES HOLD` /
119
+ `PREMISE FALSE` / `UNVERIFIABLE`. Its two boundaries are the point: a false
120
+ load-bearing premise is **stop and report**, never a silent re-aim of the task,
121
+ and only load-bearing claims are checked — an audit is what makes the step
122
+ expensive enough to skip. The `loop` skill calls it, and treats `PREMISE FALSE`
123
+ as a per-task escalation rather than a licence to rewrite the item.
124
+ - **`prose-reviewer` agent** — a fourth gate, read-only. In this layer the prose
125
+ _is_ the implementation: a rule that overstates its own enforcement fails
126
+ exactly like broken code, silently and in the direction of false confidence. It
127
+ blocks on five things — enforcement claimed beyond the mechanism, a dead
128
+ reference, two rules that contradict each other, stated limits gone stale in
129
+ either direction, and domain that must not travel (a vendor name, a host path,
130
+ a tracker key or a credential in a layer meant to be neutral) — and its
131
+ boundary comes before its checklist: it is **not
132
+ a literary editor**, and prose that is merely clumsy is not a finding. Wired
133
+ into the `pr-ship` fan-out and named in both maps.
134
+ - **A sixth blocking item for `code-reviewer`** — a change that contradicts the
135
+ queue item it claims to implement. The instruction is to report the mismatch,
136
+ never to decide which side "must have been meant": a reviewer who reconciles
137
+ the two silently turns a visible mismatch into an invisible one. Where no item
138
+ was supplied, it says so rather than reconstructing one from the PR body —
139
+ which is evidence `autonomy.md` refuses by name. `pr-ship` now passes the item.
140
+ - **Three queue-hygiene checks** — a parent that says it was split up and is
141
+ still open; a dependency line naming a blocker no link carries (worse than a
142
+ stale label: selection reads the item as unblocked); and a document link that
143
+ is broken on its face. The neutral `Ticket` shape gains a **nullable `body`**
144
+ so these live in one pure function instead of once per adapter — and `null`
145
+ means "this adapter cannot answer", never "checked, found nothing".
146
+
147
+ ### Fixed
148
+
149
+ - **The baseline commit of a generated project could land in the caller's
150
+ repository.** Git hands its hooks an absolute `GIT_DIR`, and the CLI spawned
151
+ git with the environment intact — so `git init` re-initialised the caller's
152
+ repo, `add -A` staged its tree, and the commit landed on whatever branch it had
153
+ checked out, while the generated project got no `.git` at all. A redirected
154
+ `git init` can also flip the caller's repository to `core.bare=true`. The path
155
+ that triggers it is a pre-commit hook running a suite that generates projects —
156
+ which is what made the `worktree-task` skill unusable. Every git call site now
157
+ strips the variables that locate a repository, including the shipped
158
+ `gate-stop-dod` hook (which asked git whether _which_ tree was clean) and
159
+ `preflight`.
160
+ - **The Tier-2 gate sweep could not see a rulebook outside the repository root.**
161
+ `detect-missed-gate` exempts the rulebook from its inert-file rule so a merge
162
+ rewriting the autonomy tiers cannot pass as "just prose" — but the exemption
163
+ was anchored at `CLAUDE.md` / `.claude/`. Any project that vendors, templates
164
+ or nests a rig keeps its rulebook elsewhere, and every `.md` there was dropped
165
+ before the elevated-path test ran. It is now recognised wherever it sits, and
166
+ the sweep's verdict vocabulary knows the words `pr-ship` actually emits.
167
+
168
+ ### Deferred, and on what condition
169
+
170
+ Two pieces of the source brief did **not** travel, because shipping an unproven
171
+ gate into other people's projects is worse than not having one:
172
+
173
+ - the queue-closing discipline for blocked dependents — enters when it has been
174
+ merged and used in the project it came from;
175
+ - the clarify-gate (`C-0…C-2`) — enters once that gate has fired at least once
176
+ anywhere. Until then there is nothing to copy but an intention.
177
+
10
178
  ## 0.3.1
11
179
 
12
180
  `create-agent-rig init` shipped a rig that looked installed and enforced
@@ -155,7 +323,8 @@ layers, and the `aws-serverless` and `node-service` targets.
155
323
  ## Releasing
156
324
 
157
325
  `npm publish` needs 2FA and cannot be undone, so an agent prepares a release and
158
- **stops at that command**. Everything before it is mechanical:
326
+ **stops at the first step it is not allowed to take** — normally `npm publish`,
327
+ sometimes earlier (step 6). Everything before that is mechanical:
159
328
 
160
329
  1. `pnpm test` — the full suite, including the e2e that generates both targets
161
330
  cold and runs their own checks through the git path **and** the pack path.
@@ -163,8 +332,25 @@ layers, and the `aws-serverless` and `node-service` targets.
163
332
  tree, are in the tarball. This is where scaffolders break, and the git path
164
333
  cannot catch it.
165
334
  3. Version in `package.json` (and the private inner package, kept in step).
166
- 4. This file, and `PLAN.md` if the plan's claims changed.
167
- 5. `git tag v<version> && git push --tags`.
168
- 6. **Owner:** `npm publish`.
169
- 7. **Owner:** smoke the published artifact — `npx create-agent-rig@<version>` in
170
- an empty directory, then `pnpm install && pnpm check` inside it.
335
+ 4. `node scripts/build-hash-history.mjs` — regenerate the released-hash table
336
+ from the tags **after** the version bump, so the version now shipping is the
337
+ first one it excludes. Forgetting it would leave `upgrade` unable to
338
+ recognise the previous release.
339
+ 5. This file, and `PLAN.md` if the plan's claims changed.
340
+ 6. **`pnpm test` again — this run, not step 1, is the one that can catch a
341
+ stale hash table.** The check compares the table against the versions this
342
+ file lists below the one in `package.json`, so before steps 3–5 it is
343
+ comparing the _old_ release to the _old_ table and passes either way. A
344
+ guard that can only fire after the thing it guards has changed has to be run
345
+ 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.
353
+ 8. **Owner:** `npm publish`.
354
+ 9. **Owner:** smoke the published artifact — `npx create-agent-rig@<version>` in
355
+ an empty directory, then `pnpm install && pnpm check` inside it; and
356
+ `upgrade --dry-run` in a rig installed from the previous version.
package/README.md CHANGED
@@ -32,6 +32,44 @@ Two things it deliberately leaves to you, and says so in the installed
32
32
  `CLAUDE.md`: the Definition-of-Done gate has no `dod-checks.json` (it cannot know
33
33
  your commands), and the elevated-path list names only what every repo has.
34
34
 
35
+ ## Upgrading a rig you already have
36
+
37
+ A release changes files, and `init` only ever _adds_ — so bringing an existing
38
+ rig forward is its own command:
39
+
40
+ ```sh
41
+ npx create-agent-rig@latest upgrade --dry-run # print the plan, write nothing
42
+ npx create-agent-rig@latest upgrade # print the plan, then ask before writing
43
+ npx create-agent-rig@latest upgrade --yes # the answer up front (required off a terminal)
44
+ ```
45
+
46
+ It replaces the files the rig installed **and you have not touched**, installs
47
+ what the release added, and **reports everything else** — no three-way merge, no
48
+ patching. Silently merging your edits into the documents an agent loop obeys is
49
+ how a rig quietly stops meaning what you think it means; a conflict report is how
50
+ it does not. Each conflict names the file, why it was kept, and the path to the
51
+ new version so you can diff it yourself.
52
+
53
+ How it knows: `create` and `init` write `.claude/.rig-manifest.json` — the rig
54
+ version plus a hash per installed file. **Commit it**; without it in the
55
+ repository the command is blind on CI and on a colleague's machine. Rigs
56
+ installed before 0.4.0 have no manifest, so the package also carries the hashes
57
+ of every **tagged** release (0.3.0 onward — 0.1.0 and 0.2.0 shipped untagged,
58
+ 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.
63
+
64
+ **A file you deleted stays deleted.** The rules invite you to delete the ones
65
+ whose invariant your project does not have, so an upgrade that quietly restored
66
+ them would be undoing your work. With a manifest that is direct — it names the
67
+ file, the disk does not have it, and the manifest is _evidence_, not a command.
68
+ Without one, the shipped table answers instead: a file that was in every release
69
+ it covers was there to be removed. The single case nothing can tell apart is a
70
+ file a **later** release added, which your rig never had — that one is installed,
71
+ and `--dry-run` lists it before anything is written.
72
+
35
73
  ## What you get
36
74
 
37
75
  **A system of boundaries, each held by tooling.** An agent (or a human using
@@ -83,8 +121,8 @@ never labels**, and **the agent never files its own work items**.
83
121
 
84
122
  Around all of it: **autonomy tiers** (what an agent does alone / after review /
85
123
  never), **stop rules** (three strikes, flaky ≠ retry, session staleness),
86
- **subagent gates** (`test-writer`, `code-reviewer`, `security-scanner`, and
87
- `cdk-diff-reviewer` on the AWS target), **skills** (`pr-ship` pre-merge gate;
124
+ **subagent gates** (`test-writer`, `code-reviewer`, `security-scanner`,
125
+ `prose-reviewer`, and `cdk-diff-reviewer` on the AWS target), **skills** (`pr-ship` pre-merge gate;
88
126
  `loop` queue driver; `worktree-task` for concurrent sessions; `new-invariant`, a
89
127
  generator for the invariant→hook→test pattern; `post-deploy-verify` and
90
128
  `ro-debug` on the AWS target), and a one-page `CLAUDE.md` map a fresh session
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-agent-rig",
3
- "version": "0.3.1",
3
+ "version": "0.4.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",
@@ -1,12 +1,16 @@
1
1
  import { execFile } from 'node:child_process';
2
- import { mkdir, readdir, stat } from 'node:fs/promises';
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
5
  import { copyTree, listTree } from '../lib/copy-tree.js';
6
6
  import { ALLOWED_OVERWRITES, detectCollisions } from '../lib/composition.js';
7
+ import { agentOsLayerDirs } from '../lib/install-set.js';
8
+ import { sha256, writeManifest } from '../lib/manifest.js';
7
9
  import { substituteContent, substituteFileName } from '../lib/substitute.js';
10
+ import { gitEnv } from '../lib/git-env.js';
8
11
  import { DEFAULT_TARGET, TARGETS, TARGET_NAMES } from '../lib/targets.js';
9
- import { agentOsStackDir, agentOsUniversalDir, skeletonDir } from '../templates.js';
12
+ import { skeletonDir } from '../templates.js';
13
+ import { packageVersion } from '../lib/version.js';
10
14
  /** A user-facing failure: message is printed as-is, no stack trace. */
11
15
  export class CreateError extends Error {
12
16
  }
@@ -35,13 +39,10 @@ export async function createProject(dirArg, options) {
35
39
  transformName: (name) => substituteFileName(name, ctx),
36
40
  };
37
41
  // Layer 2 (the skeleton) + layer 1 (agent-os: universal + stack overlays).
42
+ const agentOsLayers = agentOsLayerDirs(target.stacks);
38
43
  const layers = [
39
44
  { name: `skeleton/${target.skeletonDir}`, dir: skeletonDir(target.skeletonDir) },
40
- { name: 'agent-os/universal', dir: agentOsUniversalDir() },
41
- ...target.stacks.map((stack) => ({
42
- name: `agent-os/stack/${stack}`,
43
- dir: agentOsStackDir(stack),
44
- })),
45
+ ...agentOsLayers,
45
46
  ];
46
47
  // Composition safety: layers must claim disjoint paths. Checked before any
47
48
  // copy — a collision is a template bug and must never be resolved by order.
@@ -60,11 +61,39 @@ export async function createProject(dirArg, options) {
60
61
  for (const layer of layers) {
61
62
  await copyTree(layer.dir, projectDir, transforms);
62
63
  }
64
+ await recordInstall(projectDir, agentOsLayers, transforms, ctx, targetName);
63
65
  if (options.git !== false) {
64
66
  await initGitBaseline(projectDir);
65
67
  }
66
68
  return { projectDir, projectName };
67
69
  }
70
+ /**
71
+ * Record the agent-os layer in `.claude/.rig-manifest.json`, so a later
72
+ * `upgrade` can tell a file the rig wrote from a file the project's own people
73
+ * changed.
74
+ *
75
+ * The skeleton is **not** recorded, and that is the boundary of the whole
76
+ * upgrade story: once generated, the code belongs to the project. The hashes
77
+ * are read back off the disk rather than recomputed, so the manifest states
78
+ * what is actually there and cannot drift from what was copied. The manifest
79
+ * lands before the baseline commit — it is part of the pristine template, and
80
+ * it belongs in the project's git history.
81
+ */
82
+ async function recordInstall(projectDir, agentOsLayers, transforms, ctx, target) {
83
+ const files = {};
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'));
87
+ }
88
+ }
89
+ await writeManifest(projectDir, {
90
+ version: await packageVersion(),
91
+ kind: 'create',
92
+ project: { name: ctx.projectName, scope: ctx.projectScope, region: ctx.region },
93
+ stacks: [...(TARGETS[target]?.stacks ?? [])],
94
+ files,
95
+ });
96
+ }
68
97
  const run = promisify(execFile);
69
98
  async function initGitBaseline(projectDir) {
70
99
  // Disable git's background maintenance for these one-shot commands: a commit
@@ -72,9 +101,10 @@ async function initGitBaseline(projectDir) {
72
101
  // .git/objects/pack after we return — a non-deterministic tail that races any
73
102
  // caller cleaning up the directory, and pointless work on a one-commit repo.
74
103
  const quiet = ['-c', 'gc.auto=0', '-c', 'maintenance.auto=false'];
104
+ const where = { cwd: projectDir, env: gitEnv() };
75
105
  try {
76
- await run('git', [...quiet, 'init', '--quiet'], { cwd: projectDir });
77
- await run('git', [...quiet, 'add', '-A'], { cwd: projectDir });
106
+ await run('git', [...quiet, 'init', '--quiet'], where);
107
+ await run('git', [...quiet, 'add', '-A'], where);
78
108
  // Explicit identity: the baseline must commit even where git has no
79
109
  // global user configured (fresh machines, CI). --no-verify here shields
80
110
  // the baseline from the USER'S global hooks only — the generated
@@ -90,7 +120,7 @@ async function initGitBaseline(projectDir) {
90
120
  '--no-verify',
91
121
  '-m',
92
122
  'Pristine template (create-agent-rig)',
93
- ], { cwd: projectDir });
123
+ ], where);
94
124
  }
95
125
  catch {
96
126
  // git missing or unusable — generation never fails on this.
@@ -1,8 +1,10 @@
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 { readManifest, sha256, writeManifest } from '../lib/manifest.js';
4
5
  import { substituteContent } from '../lib/substitute.js';
5
6
  import { agentOsInitDir, agentOsUniversalDir } from '../templates.js';
7
+ import { packageVersion } from '../lib/version.js';
6
8
  /** A user-facing failure: message is printed as-is, no stack trace. */
7
9
  export class InitError extends Error {
8
10
  }
@@ -71,9 +73,13 @@ export async function initManifest() {
71
73
  * `__PROJECT_NAME__` in `stop-flag.mjs` is a kill switch that silently never
72
74
  * fires.
73
75
  */
74
- export async function initFileContents(repoDir) {
75
- const projectName = projectNameFor(repoDir);
76
- const ctx = { projectName, projectScope: projectName, region: '' };
76
+ export async function initFileContents(repoDir, project) {
77
+ const projectName = project?.name ?? projectNameFor(repoDir);
78
+ const ctx = {
79
+ projectName,
80
+ projectScope: project?.scope ?? projectName,
81
+ region: project?.region ?? '',
82
+ };
77
83
  const files = await initManifest();
78
84
  const contents = new Map();
79
85
  for (const { rel, source } of files) {
@@ -86,6 +92,12 @@ export async function initFileContents(repoDir) {
86
92
  contents.set(SETTINGS, `${JSON.stringify(settingsForInstalledHooks(shipped, installedHooks), null, 2)}\n`);
87
93
  return contents;
88
94
  }
95
+ /** The process layer as a set of {@link InstalledFile}s — what `upgrade` reads. */
96
+ export async function initInstallSet(repoDir, project) {
97
+ const files = await initManifest();
98
+ const contents = await initFileContents(repoDir, project);
99
+ return files.map(({ rel, source }) => ({ rel, source, content: contents.get(rel) ?? '' }));
100
+ }
89
101
  export async function planInit(repoDir) {
90
102
  const files = (await initManifest()).map((f) => f.rel);
91
103
  const conflicts = [];
@@ -123,5 +135,31 @@ export async function initProject(repoDir, options) {
123
135
  await writeFile(dest, contents.get(rel) ?? '');
124
136
  written.push(rel);
125
137
  }
138
+ if (!options.dryRun)
139
+ await recordInstall(repoDir, written, contents);
126
140
  return { written, skipped, plannedCount };
127
141
  }
142
+ /**
143
+ * Record what was installed, so a later `upgrade` can tell a file it wrote
144
+ * from a file the user owns.
145
+ *
146
+ * Only files actually **written** are recorded. A file `init` kept is
147
+ * somebody else's — claiming it here would let the next upgrade replace a
148
+ * user's own document with the rig's. Earlier entries are preserved: a re-run
149
+ * writes nothing and must not therefore un-remember everything.
150
+ */
151
+ async function recordInstall(repoDir, written, contents) {
152
+ const previous = await readManifest(repoDir);
153
+ const name = projectNameFor(repoDir);
154
+ const files = { ...(previous?.files ?? {}) };
155
+ for (const rel of written)
156
+ files[rel] = sha256(contents.get(rel) ?? '');
157
+ const manifest = {
158
+ version: await packageVersion(),
159
+ kind: 'init',
160
+ project: { name, scope: name, region: '' },
161
+ stacks: [],
162
+ files,
163
+ };
164
+ await writeManifest(repoDir, manifest);
165
+ }