create-agent-rig 0.1.0 → 0.3.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 +134 -0
- package/README.md +112 -30
- package/package.json +9 -2
- package/packages/cli/dist/commands/create.js +8 -2
- package/packages/cli/dist/commands/init.js +72 -0
- package/packages/cli/dist/index.js +44 -2
- package/packages/cli/dist/lib/copy-tree.js +2 -0
- package/packages/cli/dist/lib/summary.js +19 -5
- package/templates/agent-os/stack/aws-cdk/.claude/rules/aws-cdk.md +46 -0
- package/templates/agent-os/stack/aws-cdk/.claude/skills/post-deploy-verify/SKILL.md +24 -11
- package/templates/agent-os/stack/aws-cdk/.claude/skills/ro-debug/SKILL.md +117 -0
- package/templates/agent-os/stack/node-ts/.claude/hooks/dod-checks.json +1 -0
- package/templates/agent-os/stack/node-ts/.claude/rules/node-ts.md +18 -0
- package/templates/agent-os/universal/.claude/hooks/block-no-verify.mjs +12 -2
- package/templates/agent-os/universal/.claude/hooks/gate-stop-dod.mjs +66 -0
- package/templates/agent-os/universal/.claude/hooks/guard-bash.mjs +808 -0
- package/templates/agent-os/universal/.claude/hooks/inject-rules.mjs +36 -0
- package/templates/agent-os/universal/.claude/queue.json +3 -0
- package/templates/agent-os/universal/.claude/rules/architecture.md +7 -0
- package/templates/agent-os/universal/.claude/rules/autonomy.md +43 -0
- package/templates/agent-os/universal/.claude/rules/invariants.md +169 -0
- package/templates/agent-os/universal/.claude/rules/workflow.md +33 -7
- package/templates/agent-os/universal/.claude/scripts/detect-missed-gate.mjs +489 -0
- package/templates/agent-os/universal/.claude/scripts/preflight.mjs +161 -0
- package/templates/agent-os/universal/.claude/scripts/queue/core.mjs +305 -0
- package/templates/agent-os/universal/.claude/scripts/queue/github-issues.mjs +231 -0
- package/templates/agent-os/universal/.claude/scripts/queue/index.mjs +175 -0
- package/templates/agent-os/universal/.claude/scripts/queue/jira.mjs +345 -0
- package/templates/agent-os/universal/.claude/scripts/queue/plan-md.mjs +239 -0
- package/templates/agent-os/universal/.claude/scripts/reconcile-external-prs.mjs +280 -0
- package/templates/agent-os/universal/.claude/scripts/stop-flag.mjs +62 -0
- package/templates/agent-os/universal/.claude/settings.json +24 -0
- package/templates/agent-os/universal/.claude/skills/loop/SKILL.md +302 -0
- package/templates/agent-os/universal/.claude/skills/new-invariant/SKILL.md +102 -0
- package/templates/agent-os/universal/.claude/skills/new-invariant/guard-invariant.example.mjs +78 -0
- package/templates/agent-os/universal/.claude/skills/new-invariant/guard-invariant.example.test.mjs +89 -0
- package/templates/agent-os/universal/.claude/skills/worktree-task/SKILL.md +73 -0
- package/templates/agent-os/universal/CLAUDE.md +64 -4
- package/templates/agent-os/universal/PLAN.md +53 -0
- package/templates/agent-os/universal/layers.json +40 -0
- package/templates/skeleton/aws-serverless/.github/workflows/ci.yml +6 -1
- package/templates/skeleton/aws-serverless/.github/workflows/deploy.yml +57 -0
- package/templates/skeleton/aws-serverless/README.md +28 -7
- package/templates/skeleton/aws-serverless/gitignore +8 -0
- package/templates/skeleton/node-service/.github/workflows/ci.yml +6 -1
- package/templates/skeleton/node-service/.github/workflows/deploy.yml +29 -0
- package/templates/skeleton/node-service/README.md +21 -1
- package/templates/skeleton/node-service/gitignore +9 -0
- package/templates/skeleton/node-service/package.json +4 -2
- package/templates/skeleton/node-service/pnpm-lock.yaml +3 -0
- package/templates/skeleton/node-service/scripts/build-artifact.mjs +34 -0
- package/templates/skeleton/node-service/services/api/test/artifact.test.ts +93 -0
package/CHANGELOG.md
ADDED
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
# Changelog
|
|
2
|
+
|
|
3
|
+
Notable changes per release. The generated projects are the product, so an entry
|
|
4
|
+
says what a **newly scaffolded project** gains or loses — not what moved inside
|
|
5
|
+
the generator.
|
|
6
|
+
|
|
7
|
+
Versions are published to npm as [`create-agent-rig`](https://www.npmjs.com/package/create-agent-rig);
|
|
8
|
+
`npx github:serhii-baksheiev/create-agent-rig` keeps working for either path.
|
|
9
|
+
|
|
10
|
+
## 0.3.0
|
|
11
|
+
|
|
12
|
+
The factory extraction: a scaffolded project now arrives with a working
|
|
13
|
+
autonomous loop and the mechanisms that watch it, rather than an empty `.claude/`.
|
|
14
|
+
|
|
15
|
+
### Added
|
|
16
|
+
|
|
17
|
+
- **`guard-bash` hook** — the "Never" tier made mechanical: force-pushing or
|
|
18
|
+
deleting a shared branch, a direct push to the default branch, a production
|
|
19
|
+
deploy trigger, a catastrophic delete. It **parses** the command (quotes
|
|
20
|
+
honoured) instead of pattern-matching, so a commit message that mentions a
|
|
21
|
+
forbidden flag is prose, not a bypass.
|
|
22
|
+
- **A kill switch that is a real file.** `touch ~/.claude/<project>-loop-STOP`
|
|
23
|
+
and no merge lands until it is removed. Everything short of the merge stays
|
|
24
|
+
allowed on purpose — stopping cleanly must not mean losing work.
|
|
25
|
+
- **The queue seam.** `loop` no longer reads one tracker: selection goes through
|
|
26
|
+
`.claude/scripts/queue/`, with a pure core (filters, blocker resolution, tier
|
|
27
|
+
ration, sort, stop conditions) and three adapters — `plan-md` (the default; the
|
|
28
|
+
only one that works before a project has a remote), `github-issues`, `jira`.
|
|
29
|
+
- **Two sweeps that run outside any session** — `detect-missed-gate` finds merges
|
|
30
|
+
that crossed an elevated path with no recorded reviewer verdict;
|
|
31
|
+
`reconcile-external-prs` accounts for work that reached the default branch
|
|
32
|
+
outside the queue. Both exist because a run cannot report its own missed gate.
|
|
33
|
+
- **`preflight`** — the pre-run checks, which also print the items it did _not_
|
|
34
|
+
check, every time.
|
|
35
|
+
- **Skills** — `worktree-task` (isolation when a second session may run) and
|
|
36
|
+
`new-invariant` (a generator for the invariant→hook→test pattern, with a
|
|
37
|
+
working example and its test).
|
|
38
|
+
- **`rules/invariants.md`** — the pattern behind every hook here, stated once, so
|
|
39
|
+
the hooks read as examples rather than as laws.
|
|
40
|
+
- **`aws-cdk` target extras** — the `ro-debug` skill (read-only runtime
|
|
41
|
+
investigation, with the traps that produce confident wrong diagnoses) and the
|
|
42
|
+
transferable AWS rules.
|
|
43
|
+
- **`elevated-paths`** — a declaration in `CLAUDE.md`, composed with any block in
|
|
44
|
+
`.claude/rules/`, naming the paths where Tier-2 changes live.
|
|
45
|
+
|
|
46
|
+
### Changed
|
|
47
|
+
|
|
48
|
+
- The governance summary counts `.mjs` hooks only — a config file listed as an
|
|
49
|
+
enforced hook overstated the one number this tool exists to make credible.
|
|
50
|
+
- `autonomy.md`: the tier is decided by what a change **touches**, not by what
|
|
51
|
+
the task predicted it would touch.
|
|
52
|
+
|
|
53
|
+
### Fixed
|
|
54
|
+
|
|
55
|
+
Four review rounds, ten reviewers, on this release's own code. What they caught,
|
|
56
|
+
each reproduced before the fix and re-verified after:
|
|
57
|
+
|
|
58
|
+
- a PR body could **forge its own reviewer verdict** and suppress the gate sweep
|
|
59
|
+
— the body is written by the actor being audited, so only the `human-review`
|
|
60
|
+
label (which needs repository permission) suppresses now;
|
|
61
|
+
- the `plan-md` adapter's close **deleted the wrong line**, destroying a human's
|
|
62
|
+
Operator-queue entry and leaving the shipped item selectable;
|
|
63
|
+
- every `github-issues` write **threw on success** (those `gh` subcommands print
|
|
64
|
+
text, not JSON) — `escalate` posted its diagnosis and then died before applying
|
|
65
|
+
the label that stops the item being re-picked;
|
|
66
|
+
- the kill switch could be **disarmed by an env variable**, and was fixed in the
|
|
67
|
+
hook while the identical hole sat in `preflight` — the brake now has one
|
|
68
|
+
implementation;
|
|
69
|
+
- three **total bypasses** in the guard, each an exception inside its own work
|
|
70
|
+
that the fail-open catch turned into "allow": an unbounded spread, a recursive
|
|
71
|
+
brace expansion, and a quadratic path collapse;
|
|
72
|
+
- a heredoc pre-pass that could **hide any command** from every rule;
|
|
73
|
+
- a ReDoS in the blocker parser reachable by anyone able to open an issue.
|
|
74
|
+
|
|
75
|
+
Rounds 4 and 5, on the fixes themselves:
|
|
76
|
+
|
|
77
|
+
- a here-string (`cat <<<X`) and an arithmetic left shift (`$((1<<n))`) were each
|
|
78
|
+
read as heredoc markers, **hiding every command up to the next matching line**;
|
|
79
|
+
- `git commit -nm "msg"` bypassed the pre-commit gate outright — the one thing
|
|
80
|
+
that hook exists to stop, in the spelling people actually type;
|
|
81
|
+
- with the kill switch armed, `git merge feat/x && git push` still landed a merge
|
|
82
|
+
on the default branch; a push must now name its ref while stopped;
|
|
83
|
+
- pointing `HOME` at an empty directory disarmed the brake; it is now found
|
|
84
|
+
through the password database as well as the environment;
|
|
85
|
+
- `gh --json files` truncates at 100 with **no marker**, and the gate sweep read
|
|
86
|
+
the short list as "touched nothing elevated" — a PR padded past 100 files hid
|
|
87
|
+
its elevated change. The sweep now compares against `changedFiles`;
|
|
88
|
+
- declaring `.claude/` elevated was a no-op, because every `.md` under it counted
|
|
89
|
+
as inert — so a merge rewriting the autonomy tiers passed the gate meant to
|
|
90
|
+
catch exactly that;
|
|
91
|
+
- a quadratic reviewer-name regex cost ~4 s per crafted PR body, minutes across a
|
|
92
|
+
sweep that reports nothing when killed.
|
|
93
|
+
|
|
94
|
+
The README's enforcement claims were overstated and are now scoped to what the
|
|
95
|
+
guard actually inspects, with the omissions listed in the hook itself: only `rm`
|
|
96
|
+
for deletes, only a workflow dispatch for deploys, only a push that names its
|
|
97
|
+
branch, and nothing carried as a flag value.
|
|
98
|
+
|
|
99
|
+
The lesson that generalises is now a rule (`invariants.md`): **a guard that fails
|
|
100
|
+
open must do provably bounded work**, because fail-open makes every line of work
|
|
101
|
+
a potential total bypass — and prefer deleting a rule to adding one.
|
|
102
|
+
|
|
103
|
+
## 0.2.0
|
|
104
|
+
|
|
105
|
+
Distribution hardening (file modes, the `gitignore`→`.gitignore` trick, a
|
|
106
|
+
pack-path e2e per target), agent-os v2 (`pr-ship`, `post-deploy-verify`,
|
|
107
|
+
`cdk-diff-reviewer`, review-context isolation, session staleness), the `apps/web`
|
|
108
|
+
frontend proving core purity across the wire, the `gate-stop-dod` and
|
|
109
|
+
`inject-rules` hooks, `agent-rig init`, the `loop` skill, and the dev deploy
|
|
110
|
+
workflows.
|
|
111
|
+
|
|
112
|
+
## 0.1.0
|
|
113
|
+
|
|
114
|
+
First release: the CLI, `agent-os/universal` + the `node-ts`/`aws-cdk` stack
|
|
115
|
+
layers, and the `aws-serverless` and `node-service` targets.
|
|
116
|
+
|
|
117
|
+
---
|
|
118
|
+
|
|
119
|
+
## Releasing
|
|
120
|
+
|
|
121
|
+
`npm publish` needs 2FA and cannot be undone, so an agent prepares a release and
|
|
122
|
+
**stops at that command**. Everything before it is mechanical:
|
|
123
|
+
|
|
124
|
+
1. `pnpm test` — the full suite, including the e2e that generates both targets
|
|
125
|
+
cold and runs their own checks through the git path **and** the pack path.
|
|
126
|
+
2. `npm pack --dry-run` — confirm the templates, including the dotted `.claude/`
|
|
127
|
+
tree, are in the tarball. This is where scaffolders break, and the git path
|
|
128
|
+
cannot catch it.
|
|
129
|
+
3. Version in `package.json` (and the private inner package, kept in step).
|
|
130
|
+
4. This file, and `PLAN.md` if the plan's claims changed.
|
|
131
|
+
5. `git tag v<version> && git push --tags`.
|
|
132
|
+
6. **Owner:** `npm publish`.
|
|
133
|
+
7. **Owner:** smoke the published artifact — `npx create-agent-rig@<version>` in
|
|
134
|
+
an empty directory, then `pnpm install && pnpm check` inside it.
|
package/README.md
CHANGED
|
@@ -4,38 +4,102 @@ Scaffold a project that ships with an **agent operating system** — rules,
|
|
|
4
4
|
gates, and hooks that hold the architecture mechanically, not by prose.
|
|
5
5
|
|
|
6
6
|
```sh
|
|
7
|
-
npx create-agent-rig my-app
|
|
7
|
+
npx create-agent-rig my-app # choose a target interactively
|
|
8
|
+
npx create-agent-rig my-app --target node-service # or name it up front
|
|
8
9
|
```
|
|
9
10
|
|
|
11
|
+
Two coherent targets — `aws-serverless` (the default) and `node-service`. On a
|
|
12
|
+
terminal the CLI prompts; in CI it wants `--target` explicitly. `--no-git`
|
|
13
|
+
skips the initial baseline commit; `--no-color` (and `NO_COLOR`) plainens the
|
|
14
|
+
output.
|
|
15
|
+
|
|
16
|
+
Already have a repo? Install just the **process layer** into it — no
|
|
17
|
+
architecture assumptions, no skeleton:
|
|
18
|
+
|
|
19
|
+
```sh
|
|
20
|
+
npx create-agent-rig init # rules, gates, stop rules into the current repo
|
|
21
|
+
npx create-agent-rig init --dry-run # print the plan, write nothing
|
|
22
|
+
```
|
|
23
|
+
|
|
24
|
+
`init` drops in the autonomy tiers, stop rules, workflow, and the enforcement
|
|
25
|
+
hooks, and refuses to clobber an existing `CLAUDE.md`.
|
|
26
|
+
|
|
10
27
|
## What you get
|
|
11
28
|
|
|
12
29
|
**A system of boundaries, each held by tooling.** An agent (or a human using
|
|
13
|
-
one) cannot talk its way past them
|
|
30
|
+
one) cannot talk its way past them — each guard is a pre-write scan that stops
|
|
31
|
+
the normal path cold (review and tests back it; the claim is stated exactly,
|
|
32
|
+
never inflated). The hooks live in `.claude/hooks/` and are wired in
|
|
33
|
+
`.claude/settings.json`:
|
|
14
34
|
|
|
15
35
|
- **`guard-core-purity`** — refuses any edit that puts I/O, clock, randomness,
|
|
16
|
-
|
|
36
|
+
environment access, or a non-allowlisted import into the pure domain core;
|
|
17
37
|
- **`guard-web-boundary`** — refuses `db`/service imports from the frontend;
|
|
18
38
|
the web talks to the backend over HTTP only;
|
|
19
39
|
- **`block-no-verify`** — refuses bypassing pre-commit checks (and knows the
|
|
20
|
-
difference between using the flag and merely mentioning it
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
40
|
+
difference between using the `--no-verify`/`-n` flag and merely mentioning it
|
|
41
|
+
in a message);
|
|
42
|
+
- **`guard-bash`** — refuses the part of the "Never" tier a text scan can decide:
|
|
43
|
+
a force-push or `--delete` naming a shared branch, a push that names the default
|
|
44
|
+
branch, `gh workflow run`/`gh api …/dispatches` against a production workflow,
|
|
45
|
+
and `rm` on a catastrophic target. It **parses** the command rather than
|
|
46
|
+
pattern-matching it, so a commit message mentioning a forbidden flag is prose,
|
|
47
|
+
not a bypass — and the file states exactly what it does **not** inspect
|
|
48
|
+
(`cdk deploy`, `find -delete`, a bare `git push`, and more);
|
|
49
|
+
- **`gate-stop-dod`** — refuses to end the session while a Definition-of-Done
|
|
50
|
+
check is red; it fails open (a missing or corrupt config never makes the
|
|
51
|
+
session unquittable) and never blocks twice in a row;
|
|
52
|
+
- **`inject-rules`** — re-injects the autonomy rules at session start, so they
|
|
53
|
+
survive compaction and resumes.
|
|
54
|
+
|
|
55
|
+
**A brake that is a real file.** `touch ~/.claude/<project>-loop-STOP` and no
|
|
56
|
+
merge lands until it is removed — enforced at the tool layer, so it holds even if
|
|
57
|
+
nothing reads the rule. Everything short of the merge stays allowed on purpose:
|
|
58
|
+
finish the task, push the branch, open the PR, write the journal. Stopping
|
|
59
|
+
cleanly must not mean losing work.
|
|
60
|
+
|
|
61
|
+
**Two sweeps meant to run outside any session** — nothing schedules them for you;
|
|
62
|
+
that is deliberate, because a check a run performs on itself is one a hurried run
|
|
63
|
+
skips. `detect-missed-gate` finds merges
|
|
64
|
+
that crossed an elevated path with no recorded reviewer verdict;
|
|
65
|
+
`reconcile-external-prs` accounts for work that reached the default branch outside
|
|
66
|
+
the queue. They exist because the one failure a run cannot report is its own
|
|
67
|
+
missed gate — the run that skipped it is exactly the run that will not mention it.
|
|
68
|
+
|
|
69
|
+
**A queue behind an adapter.** The `loop` driver selects through
|
|
70
|
+
`.claude/scripts/queue/`: a pure core (filters in order, blocker resolution, the
|
|
71
|
+
elevated-tier ration, stop conditions) with adapters for `PLAN.md` (the default,
|
|
72
|
+
working before a project has a remote), GitHub Issues, and Jira. Two rules are
|
|
73
|
+
load-bearing and tested from both directions — **blockers resolve from links,
|
|
74
|
+
never labels**, and **the agent never files its own work items**.
|
|
75
|
+
|
|
76
|
+
Around all of it: **autonomy tiers** (what an agent does alone / after review /
|
|
77
|
+
never), **stop rules** (three strikes, flaky ≠ retry, session staleness),
|
|
78
|
+
**subagent gates** (`test-writer`, `code-reviewer`, `security-scanner`, and
|
|
79
|
+
`cdk-diff-reviewer` on the AWS target), **skills** (`pr-ship` pre-merge gate;
|
|
80
|
+
`loop` queue driver; `worktree-task` for concurrent sessions; `new-invariant`, a
|
|
81
|
+
generator for the invariant→hook→test pattern; `post-deploy-verify` and
|
|
82
|
+
`ro-debug` on the AWS target), and a one-page `CLAUDE.md` map a fresh session
|
|
83
|
+
orients by.
|
|
84
|
+
|
|
85
|
+
**The hooks are examples, not laws.** `.claude/rules/invariants.md` states the
|
|
86
|
+
pattern behind each one — a stated invariant, a mechanical check, a test for the
|
|
87
|
+
check — so you can delete the ones whose invariant your project does not have and
|
|
88
|
+
spend the slot on one it does. An inherited rule nobody chose is worse than an
|
|
89
|
+
empty rule file: the empty one is visibly incomplete, the inherited one is
|
|
90
|
+
invisibly wrong.
|
|
28
91
|
|
|
29
92
|
The skeleton around it is real and runnable — pure core shared by server _and_
|
|
30
93
|
browser (one schema validates on both sides of the wire), a mandatory usecase
|
|
31
|
-
layer, a queue with DLQ discipline,
|
|
94
|
+
layer (`payload → handler → usecase → model`), a queue with DLQ discipline,
|
|
95
|
+
tests at every layer.
|
|
32
96
|
|
|
33
97
|
## Targets
|
|
34
98
|
|
|
35
|
-
| Target | One line
|
|
36
|
-
| ---------------- |
|
|
37
|
-
| `aws-serverless` | DynamoDB single-table, SQS + DLQ + alarm, three Lambdas behind an HTTP API, static web
|
|
38
|
-
| `node-service` | `node:http` server that also serves the web bundle, JSON-file store behind the same model boundary, spool-directory queue, worker process
|
|
99
|
+
| Target | One line |
|
|
100
|
+
| ---------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
101
|
+
| `aws-serverless` | DynamoDB single-table, SQS + DLQ + CloudWatch alarm, three Lambdas (POST/GET `/notes` behind an HTTP API, plus an SQS worker), static web on S3 + CloudFront, CDK with least-privilege IAM grants |
|
|
102
|
+
| `node-service` | `node:http` server that also serves the built web bundle, atomic JSON-file store behind the same model boundary, spool-directory queue + DLQ, worker process |
|
|
39
103
|
|
|
40
104
|
Coherent alternatives, not a parameterized abstraction. Flexibility is
|
|
41
105
|
**subtraction**: the generated project is yours — delete what you don't need.
|
|
@@ -56,9 +120,10 @@ gets fought. If you need one of these, add it — the project is yours.
|
|
|
56
120
|
./demo.sh # from a clone of this repo
|
|
57
121
|
```
|
|
58
122
|
|
|
59
|
-
|
|
60
|
-
violation is refused live by the hook** → the
|
|
61
|
-
travels every layer, the worker drains the queue,
|
|
123
|
+
generates the `node-service` target → the generated project's own gates pass →
|
|
124
|
+
**an attempted core-purity violation is refused live by the hook** → the
|
|
125
|
+
service runs, a smoke request travels every layer, the worker drains the queue,
|
|
126
|
+
the DLQ stays empty:
|
|
62
127
|
|
|
63
128
|
```
|
|
64
129
|
== 3/4 an agent tries to put I/O and clock access into the pure core… ==
|
|
@@ -71,17 +136,34 @@ Move the impure part behind the usecase layer or into an adapter.
|
|
|
71
136
|
|
|
72
137
|
## Requirements
|
|
73
138
|
|
|
74
|
-
- Node ≥ 20 (pnpm recommended for the generated workspace)
|
|
139
|
+
- Node ≥ 20 (pnpm recommended for the generated workspace). The CLI itself
|
|
140
|
+
carries zero runtime dependencies — the `npx github:…`, tarball, and
|
|
141
|
+
published-package paths all work.
|
|
75
142
|
|
|
76
143
|
## How it stays honest
|
|
77
144
|
|
|
78
|
-
Every template is a real project
|
|
79
|
-
generates a project cold and runs the
|
|
80
|
-
(install → lint → typecheck → test
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
145
|
+
Every template is a real project, installed with a frozen lockfile and run in
|
|
146
|
+
place on every push. Every e2e run generates a project cold and runs the
|
|
147
|
+
generated project's own checks (install → lint → typecheck → test, plus
|
|
148
|
+
`cdk synth` on the AWS target); the pack-path and git-path installs are both
|
|
149
|
+
under test, because that is exactly where scaffolders break. A grep-test keeps
|
|
150
|
+
the universal rules free of any provider mention; the hook-blocking behavior
|
|
151
|
+
itself is under test; and a weekly lockfile-free run resolves each template's
|
|
152
|
+
dependencies fresh to catch upstream breakage early. This repo dogfoods its own
|
|
153
|
+
rulebook — `CLAUDE.md` and `.claude/` are composed from the templates, and
|
|
154
|
+
drift fails the suite.
|
|
155
|
+
|
|
156
|
+
**And the enforcement layer is adversarially reviewed, not just tested.** The
|
|
157
|
+
Bash guard went through four review rounds with ten reviewers, who executed it
|
|
158
|
+
rather than read it. They found a PR body that could forge its own reviewer
|
|
159
|
+
verdict, a queue write that deleted the wrong line, and three ways to make the
|
|
160
|
+
guard crash into permitting everything. Each round's findings — including the
|
|
161
|
+
ones introduced by the previous round's _fix_ — are in the git history and in
|
|
162
|
+
`CHANGELOG.md`. The rule that came out of it is now part of what ships: a guard
|
|
163
|
+
that fails open must do provably bounded work, because fail-open turns every line
|
|
164
|
+
of its own work into a potential bypass.
|
|
165
|
+
|
|
166
|
+
Development (from a clone — `PLAN.md` and `demo.sh` live in the repository, not
|
|
167
|
+
in the published tarball): `pnpm test` (full), `pnpm test:unit` (fast loop),
|
|
168
|
+
`pnpm template:check` (templates in place). The plan of record is `PLAN.md`;
|
|
169
|
+
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.3.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",
|
|
@@ -15,6 +15,12 @@
|
|
|
15
15
|
"node-service"
|
|
16
16
|
],
|
|
17
17
|
"license": "MIT",
|
|
18
|
+
"repository": {
|
|
19
|
+
"type": "git",
|
|
20
|
+
"url": "git+https://github.com/serhii-baksheiev/create-agent-rig.git"
|
|
21
|
+
},
|
|
22
|
+
"homepage": "https://github.com/serhii-baksheiev/create-agent-rig#readme",
|
|
23
|
+
"bugs": "https://github.com/serhii-baksheiev/create-agent-rig/issues",
|
|
18
24
|
"type": "module",
|
|
19
25
|
"bin": {
|
|
20
26
|
"create-agent-rig": "packages/cli/dist/index.js"
|
|
@@ -22,7 +28,8 @@
|
|
|
22
28
|
"files": [
|
|
23
29
|
"packages/cli/dist",
|
|
24
30
|
"templates",
|
|
25
|
-
"scripts/prepare.mjs"
|
|
31
|
+
"scripts/prepare.mjs",
|
|
32
|
+
"CHANGELOG.md"
|
|
26
33
|
],
|
|
27
34
|
"engines": {
|
|
28
35
|
"node": ">=20"
|
|
@@ -67,14 +67,20 @@ export async function createProject(dirArg, options) {
|
|
|
67
67
|
}
|
|
68
68
|
const run = promisify(execFile);
|
|
69
69
|
async function initGitBaseline(projectDir) {
|
|
70
|
+
// Disable git's background maintenance for these one-shot commands: a commit
|
|
71
|
+
// can otherwise fork an auto-gc / maintenance process that keeps writing to
|
|
72
|
+
// .git/objects/pack after we return — a non-deterministic tail that races any
|
|
73
|
+
// caller cleaning up the directory, and pointless work on a one-commit repo.
|
|
74
|
+
const quiet = ['-c', 'gc.auto=0', '-c', 'maintenance.auto=false'];
|
|
70
75
|
try {
|
|
71
|
-
await run('git', ['init', '--quiet'], { cwd: projectDir });
|
|
72
|
-
await run('git', ['add', '-A'], { cwd: projectDir });
|
|
76
|
+
await run('git', [...quiet, 'init', '--quiet'], { cwd: projectDir });
|
|
77
|
+
await run('git', [...quiet, 'add', '-A'], { cwd: projectDir });
|
|
73
78
|
// Explicit identity: the baseline must commit even where git has no
|
|
74
79
|
// global user configured (fresh machines, CI). --no-verify here shields
|
|
75
80
|
// the baseline from the USER'S global hooks only — the generated
|
|
76
81
|
// project's own gates do not exist yet, so nothing is being bypassed.
|
|
77
82
|
await run('git', [
|
|
83
|
+
...quiet,
|
|
78
84
|
'-c',
|
|
79
85
|
'user.name=create-agent-rig',
|
|
80
86
|
'-c',
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
import { access, mkdir, readFile, writeFile } from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { agentOsUniversalDir } from '../templates.js';
|
|
4
|
+
/** A user-facing failure: message is printed as-is, no stack trace. */
|
|
5
|
+
export class InitError extends Error {
|
|
6
|
+
}
|
|
7
|
+
async function loadManifest() {
|
|
8
|
+
const raw = await readFile(path.join(agentOsUniversalDir(), 'layers.json'), 'utf8');
|
|
9
|
+
return JSON.parse(raw);
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* `init` installs only the PROCESS layer (hooks-and-reach brief §3/§4): rules
|
|
13
|
+
* that assume nothing about the codebase shape. Architecture rules reference
|
|
14
|
+
* `packages/core` and friends — installing them into an arbitrary repo would
|
|
15
|
+
* describe a structure that does not exist, which is worse than no rule.
|
|
16
|
+
*
|
|
17
|
+
* CLAUDE.md is the meta file we bring, but never over an existing one.
|
|
18
|
+
*/
|
|
19
|
+
async function processFiles(manifest) {
|
|
20
|
+
// the process layer, plus CLAUDE.md as the map (guarded separately)
|
|
21
|
+
return [...manifest.process, 'CLAUDE.md'];
|
|
22
|
+
}
|
|
23
|
+
async function exists(p) {
|
|
24
|
+
try {
|
|
25
|
+
await access(p);
|
|
26
|
+
return true;
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
export async function planInit(repoDir) {
|
|
33
|
+
const manifest = await loadManifest();
|
|
34
|
+
const files = await processFiles(manifest);
|
|
35
|
+
const conflicts = [];
|
|
36
|
+
for (const rel of files) {
|
|
37
|
+
if (await exists(path.join(repoDir, rel)))
|
|
38
|
+
conflicts.push(rel);
|
|
39
|
+
}
|
|
40
|
+
return { files: files.map((p) => ({ path: p })), conflicts };
|
|
41
|
+
}
|
|
42
|
+
export async function initProject(repoDir, options) {
|
|
43
|
+
const manifest = await loadManifest();
|
|
44
|
+
const files = await processFiles(manifest);
|
|
45
|
+
const universal = agentOsUniversalDir();
|
|
46
|
+
// Refuse to clobber an existing CLAUDE.md unless forced — init edits
|
|
47
|
+
// someone's working repository (brief §4, non-negotiable).
|
|
48
|
+
if (!options.force && files.includes('CLAUDE.md')) {
|
|
49
|
+
if (await exists(path.join(repoDir, 'CLAUDE.md'))) {
|
|
50
|
+
throw new InitError('This repo already has a CLAUDE.md. Refusing to overwrite it. ' +
|
|
51
|
+
'Merge the agent-os map in by hand, or re-run with --force to replace it.');
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
const written = [];
|
|
55
|
+
const skipped = [];
|
|
56
|
+
const plannedCount = files.length;
|
|
57
|
+
for (const rel of files) {
|
|
58
|
+
const dest = path.join(repoDir, rel);
|
|
59
|
+
const isForceableMeta = rel === 'CLAUDE.md';
|
|
60
|
+
if ((await exists(dest)) && !(isForceableMeta && options.force)) {
|
|
61
|
+
// never overwrite a file init did not write (a user's own copy)
|
|
62
|
+
skipped.push(rel);
|
|
63
|
+
continue;
|
|
64
|
+
}
|
|
65
|
+
if (options.dryRun)
|
|
66
|
+
continue;
|
|
67
|
+
await mkdir(path.dirname(dest), { recursive: true });
|
|
68
|
+
await writeFile(dest, await readFile(path.join(universal, rel)));
|
|
69
|
+
written.push(rel);
|
|
70
|
+
}
|
|
71
|
+
return { written, skipped, plannedCount };
|
|
72
|
+
}
|
|
@@ -4,6 +4,7 @@ import path from 'node:path';
|
|
|
4
4
|
import { fileURLToPath } from 'node:url';
|
|
5
5
|
import { parseArgs } from 'node:util';
|
|
6
6
|
import { CreateError, createProject } from './commands/create.js';
|
|
7
|
+
import { InitError, initProject, planInit } from './commands/init.js';
|
|
7
8
|
import { makePalette } from './lib/colors.js';
|
|
8
9
|
import { promptTarget } from './lib/prompts.js';
|
|
9
10
|
import { collectGovernance, renderSummary } from './lib/summary.js';
|
|
@@ -20,7 +21,11 @@ Options
|
|
|
20
21
|
--no-git skip git init + the pristine-template baseline commit
|
|
21
22
|
--no-color plain output (NO_COLOR is respected too)
|
|
22
23
|
--version print the version
|
|
23
|
-
-h, --help this text
|
|
24
|
+
-h, --help this text
|
|
25
|
+
|
|
26
|
+
Also: create-agent-rig init [--dry-run] [--force]
|
|
27
|
+
Install the process layer (rules, gates, stop rules — no architecture
|
|
28
|
+
assumptions) into the CURRENT existing repo. Refuses to clobber CLAUDE.md.`;
|
|
24
29
|
async function packageVersion() {
|
|
25
30
|
// dist/index.js lives three levels under the package root — same walk as
|
|
26
31
|
// the templates resolver, valid in the repo and in the published package.
|
|
@@ -28,7 +33,44 @@ async function packageVersion() {
|
|
|
28
33
|
const pkg = JSON.parse(await readFile(pkgPath, 'utf8'));
|
|
29
34
|
return pkg.version;
|
|
30
35
|
}
|
|
36
|
+
async function runInit(rawArgs) {
|
|
37
|
+
let values;
|
|
38
|
+
try {
|
|
39
|
+
({ values } = parseArgs({
|
|
40
|
+
args: rawArgs,
|
|
41
|
+
options: { 'dry-run': { type: 'boolean' }, force: { type: 'boolean' } },
|
|
42
|
+
allowPositionals: false,
|
|
43
|
+
}));
|
|
44
|
+
}
|
|
45
|
+
catch (error) {
|
|
46
|
+
process.stderr.write(`${error.message}\n\n${USAGE}\n`);
|
|
47
|
+
return 1;
|
|
48
|
+
}
|
|
49
|
+
const cwd = process.cwd();
|
|
50
|
+
const dryRun = values['dry-run'] === true;
|
|
51
|
+
const plan = await planInit(cwd);
|
|
52
|
+
process.stdout.write(`agent-rig init — process layer into ${cwd}\n\n` +
|
|
53
|
+
plan.files.map((f) => ` + ${f.path}`).join('\n') +
|
|
54
|
+
'\n');
|
|
55
|
+
if (plan.conflicts.length > 0) {
|
|
56
|
+
process.stdout.write(`\nAlready present (kept, not overwritten):\n` +
|
|
57
|
+
plan.conflicts.map((c) => ` · ${c}`).join('\n') +
|
|
58
|
+
'\n');
|
|
59
|
+
}
|
|
60
|
+
const result = await initProject(cwd, { dryRun, force: values.force === true });
|
|
61
|
+
if (dryRun) {
|
|
62
|
+
process.stdout.write(`\nDry run — nothing written (${result.plannedCount} files planned).\n`);
|
|
63
|
+
return 0;
|
|
64
|
+
}
|
|
65
|
+
process.stdout.write(`\nInstalled ${result.written.length} files` +
|
|
66
|
+
(result.skipped.length ? `, kept ${result.skipped.length} existing` : '') +
|
|
67
|
+
'.\n');
|
|
68
|
+
return 0;
|
|
69
|
+
}
|
|
31
70
|
async function main() {
|
|
71
|
+
if (process.argv[2] === 'init') {
|
|
72
|
+
return runInit(process.argv.slice(3));
|
|
73
|
+
}
|
|
32
74
|
let positionals;
|
|
33
75
|
let values;
|
|
34
76
|
try {
|
|
@@ -92,7 +134,7 @@ main()
|
|
|
92
134
|
process.exitCode = code;
|
|
93
135
|
})
|
|
94
136
|
.catch((error) => {
|
|
95
|
-
if (error instanceof CreateError) {
|
|
137
|
+
if (error instanceof CreateError || error instanceof InitError) {
|
|
96
138
|
process.stderr.write(`${error.message}\n`);
|
|
97
139
|
}
|
|
98
140
|
else {
|
|
@@ -16,6 +16,8 @@ export const DEFAULT_IGNORE = [
|
|
|
16
16
|
'.next',
|
|
17
17
|
'out',
|
|
18
18
|
'next-env.d.ts',
|
|
19
|
+
// the init-manifest of the universal layer — tooling metadata, not payload
|
|
20
|
+
'layers.json',
|
|
19
21
|
];
|
|
20
22
|
/** A file is treated as binary if its first bytes contain a NUL byte. */
|
|
21
23
|
function isBinary(buffer) {
|
|
@@ -1,21 +1,35 @@
|
|
|
1
1
|
import { readdir } from 'node:fs/promises';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
const names = async (dir, strip) => {
|
|
3
|
+
const names = async (dir, strip, keep) => {
|
|
4
4
|
try {
|
|
5
|
-
return (await readdir(dir))
|
|
5
|
+
return (await readdir(dir))
|
|
6
|
+
.filter((entry) => !keep || keep.test(entry))
|
|
7
|
+
.map((entry) => entry.replace(strip, ''))
|
|
8
|
+
.sort();
|
|
6
9
|
}
|
|
7
10
|
catch {
|
|
8
11
|
return [];
|
|
9
12
|
}
|
|
10
13
|
};
|
|
14
|
+
/**
|
|
15
|
+
* Hooks whose filename names the *tool* they intercept rather than the
|
|
16
|
+
* invariant they enforce. Stripping the prefix would print "bash", which tells
|
|
17
|
+
* the reader nothing — and a screen that sells enforcement may not be vague.
|
|
18
|
+
*/
|
|
19
|
+
const HOOK_LABELS = {
|
|
20
|
+
'guard-bash': 'never tier',
|
|
21
|
+
};
|
|
11
22
|
export async function collectGovernance(projectDir) {
|
|
12
23
|
const claude = path.join(projectDir, '.claude');
|
|
13
24
|
return {
|
|
14
25
|
rules: await names(path.join(claude, 'rules'), /\.md$/),
|
|
15
26
|
agents: await names(path.join(claude, 'agents'), /\.md$/),
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
27
|
+
// `.mjs` only: the stack layer also drops config (dod-checks.json) in this
|
|
28
|
+
// directory, and counting a config file as an enforced hook would make the
|
|
29
|
+
// screen overstate the one thing this tool actually sells.
|
|
30
|
+
hooks: (await names(path.join(claude, 'hooks'), /\.mjs$/, /\.mjs$/)).map(
|
|
31
|
+
// guard-core-purity → "core purity": the mechanism, not the filename
|
|
32
|
+
(hook) => HOOK_LABELS[hook] ?? hook.replace(/^(guard|block)-/, '').replaceAll('-', ' ')),
|
|
19
33
|
skills: await names(path.join(claude, 'skills'), /$^/),
|
|
20
34
|
};
|
|
21
35
|
}
|
|
@@ -3,6 +3,17 @@
|
|
|
3
3
|
How the universal boundaries land on AWS. If a rule here seems to fight a
|
|
4
4
|
universal rule, that is an invariant conflict — stop and surface it.
|
|
5
5
|
|
|
6
|
+
## The elevated paths this layer adds
|
|
7
|
+
|
|
8
|
+
`CLAUDE.md` declares the project's own elevated paths; this block adds the ones
|
|
9
|
+
that exist only because this layer does. The gate sweep
|
|
10
|
+
(`.claude/scripts/detect-missed-gate.mjs`) unions every declaration it finds, so a
|
|
11
|
+
target without infrastructure never declares a directory it does not have.
|
|
12
|
+
|
|
13
|
+
```elevated-paths
|
|
14
|
+
infra/
|
|
15
|
+
```
|
|
16
|
+
|
|
6
17
|
## Infrastructure is code, and only code
|
|
7
18
|
|
|
8
19
|
- Everything lives in the CDK app under `infra/`. A console change ("click-ops")
|
|
@@ -17,6 +28,24 @@ universal rule, that is an invariant conflict — stop and surface it.
|
|
|
17
28
|
teardown; flip to RETAIN before real data arrives — that flip is a Tier-2
|
|
18
29
|
decision.
|
|
19
30
|
|
|
31
|
+
## Stacks: what may move, and what may never
|
|
32
|
+
|
|
33
|
+
- 🔴 **Never move a stateful construct between stacks.** A Table, Bucket, Secret
|
|
34
|
+
or user pool that changes stack is **deleted and recreated** by
|
|
35
|
+
CloudFormation — that is data loss, not a refactor, and it passes review as
|
|
36
|
+
"tidying" if nobody knows this rule. Stateful constructs stay put and are
|
|
37
|
+
referenced cross-stack.
|
|
38
|
+
- **A stack has a hard resource ceiling (500).** One HTTP route costs several
|
|
39
|
+
resources, so a growing API stack approaches it long before it looks big. When
|
|
40
|
+
it does, split out the least-coupled domain as a **stateless-only** stack —
|
|
41
|
+
functions, roles, routes — attached to the same API cross-stack, with a
|
|
42
|
+
**one-way** dependency. Splitting stateless costs nothing; splitting stateful
|
|
43
|
+
costs the data (above).
|
|
44
|
+
- **Fleet-wide function defaults live in one module, not in a stack.**
|
|
45
|
+
Architecture, runtime, `NODE_OPTIONS`, tracing: one edit there reaches every
|
|
46
|
+
function. A per-stack override of a fleet default is the thing to reject in
|
|
47
|
+
review — it is invisible from anywhere except that stack.
|
|
48
|
+
|
|
20
49
|
## IAM: least privilege, by construction
|
|
21
50
|
|
|
22
51
|
- Use the narrow grant for the operation actually performed
|
|
@@ -48,6 +77,23 @@ universal rule, that is an invariant conflict — stop and surface it.
|
|
|
48
77
|
clients constructed once, handler exported. Nothing else imports an entry.
|
|
49
78
|
- Functions stay single-purpose — one route or one event source each. Fan-out
|
|
50
79
|
belongs to infrastructure (queues, topics), not to in-process branching.
|
|
80
|
+
- **SDK clients are constructed at module top level, never inside the handler.**
|
|
81
|
+
Containers are reused across warm invocations, so a client built in the handler
|
|
82
|
+
body is paid on every invocation and defeats connection reuse — the most common
|
|
83
|
+
Lambda performance bug, and invisible in tests because tests are always cold.
|
|
84
|
+
The same applies to anything expensive and stateless: build once per container.
|
|
85
|
+
- Environment is parsed **once, at module scope**, through `loadEnv(zod)`. A
|
|
86
|
+
misconfigured function then fails its cold start loudly instead of misbehaving
|
|
87
|
+
quietly per request, and no code below the entry file reads the environment
|
|
88
|
+
directly.
|
|
89
|
+
|
|
90
|
+
## When the deployed runtime misbehaves
|
|
91
|
+
|
|
92
|
+
Investigate before writing anything: the **`ro-debug` skill** has the read-only
|
|
93
|
+
recipes and, more usefully, the traps that have produced confident wrong
|
|
94
|
+
diagnoses — a stale local branch read as current, `UPDATE_COMPLETE` left over
|
|
95
|
+
from the previous deploy, an empty metric read as healthy when it means the
|
|
96
|
+
function was never invoked.
|
|
51
97
|
|
|
52
98
|
## Post-deploy verification (target-specific means)
|
|
53
99
|
|