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.
- package/CHANGELOG.md +192 -6
- package/README.md +40 -2
- package/package.json +1 -1
- package/packages/cli/dist/commands/create.js +40 -10
- package/packages/cli/dist/commands/init.js +41 -3
- package/packages/cli/dist/commands/upgrade.js +300 -0
- package/packages/cli/dist/index.js +100 -13
- package/packages/cli/dist/lib/copy-tree.js +9 -1
- package/packages/cli/dist/lib/git-env.js +48 -0
- package/packages/cli/dist/lib/history.js +49 -0
- package/packages/cli/dist/lib/install-set.js +46 -0
- package/packages/cli/dist/lib/manifest.js +99 -0
- package/packages/cli/dist/lib/prompts.js +20 -0
- package/packages/cli/dist/lib/safe-path.js +41 -0
- package/packages/cli/dist/lib/substitute.js +32 -0
- package/packages/cli/dist/lib/targets.js +15 -1
- package/packages/cli/dist/lib/version.js +15 -0
- package/scripts/prepare.mjs +54 -17
- package/templates/agent-os/init/CLAUDE.md +11 -5
- package/templates/agent-os/universal/.claude/agents/code-reviewer.md +15 -0
- package/templates/agent-os/universal/.claude/agents/prose-reviewer.md +104 -0
- package/templates/agent-os/universal/.claude/hooks/gate-stop-dod.mjs +20 -0
- package/templates/agent-os/universal/.claude/rules/workflow.md +4 -0
- package/templates/agent-os/universal/.claude/scripts/detect-missed-gate.mjs +32 -4
- package/templates/agent-os/universal/.claude/scripts/preflight.mjs +34 -1
- package/templates/agent-os/universal/.claude/scripts/queue/core.mjs +125 -0
- package/templates/agent-os/universal/.claude/scripts/queue/github-issues.mjs +6 -0
- package/templates/agent-os/universal/.claude/scripts/queue/jira.mjs +3 -0
- package/templates/agent-os/universal/.claude/scripts/queue/plan-md.mjs +6 -0
- package/templates/agent-os/universal/.claude/skills/check-premises/SKILL.md +125 -0
- package/templates/agent-os/universal/.claude/skills/loop/SKILL.md +36 -9
- package/templates/agent-os/universal/.claude/skills/pr-ship/SKILL.md +12 -2
- package/templates/agent-os/universal/CLAUDE.md +12 -3
- package/templates/agent-os/universal/PLAN.md +14 -3
- package/templates/agent-os/universal/layers.json +2 -0
- 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
|
|
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.
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
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`,
|
|
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,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 {
|
|
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
|
-
|
|
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'],
|
|
77
|
-
await run('git', [...quiet, 'add', '-A'],
|
|
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
|
-
],
|
|
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 = {
|
|
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
|
+
}
|