create-agent-rig 0.3.0 → 0.3.1

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 CHANGED
@@ -7,6 +7,42 @@ 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
+ ## 0.3.1
11
+
12
+ `create-agent-rig init` shipped a rig that looked installed and enforced
13
+ nothing. Everything below is that one failure, in its four parts — a repo
14
+ `init`ed with 0.3.0 should be re-run with this version (`--force` to replace the
15
+ CLAUDE.md it wrote).
16
+
17
+ ### Fixed
18
+
19
+ - **The hooks are wired.** `init` laid the hook files down and stopped there: no
20
+ `.claude/settings.json` meant `guard-bash`, `block-no-verify`, `gate-stop-dod`
21
+ and `inject-rules` were never called, while the installed `CLAUDE.md` claimed
22
+ they were enforced at the tool layer. The wiring is now _derived_ from the
23
+ shipped settings, so it names exactly the hooks that travelled — never one that
24
+ did not. Where the repo already has a `settings.json`, `init` keeps it and
25
+ prints the entries to merge rather than failing silently.
26
+ - **The kill switch works.** `init` copied templates byte-for-byte, leaving
27
+ `__PROJECT_NAME__` in six places — including `stop-flag.mjs`, so the brake
28
+ looked for `~/.claude/__PROJECT_NAME__-loop-STOP` while the operator, following
29
+ the instructions in the same install, created `~/.claude/<repo>-loop-STOP`. It
30
+ never fired, and never said so.
31
+ - **The installed `CLAUDE.md` describes the repo it landed in.** It used to be
32
+ the generated monorepo's map — `packages/core/`, `apps/web/`, links to an
33
+ `architecture.md` and two guards that `init` deliberately does not install. It
34
+ is now its own document: what was installed, what was not, and that the
35
+ architecture rules are yours to write.
36
+ - **The elevated-path block names paths that exist.** It seeded
37
+ `packages/db/src/` into repos that have no such directory, so the Tier-2 gate
38
+ sweep reported "clean" while looking at nothing.
39
+
40
+ ### Added
41
+
42
+ - A template test that fails if anything `init` installs references a `.claude`
43
+ file `init` does not install — the drift that produced three of the four
44
+ findings above, now mechanical.
45
+
10
46
  ## 0.3.0
11
47
 
12
48
  The factory extraction: a scaffolded project now arrives with a working
package/README.md CHANGED
@@ -22,7 +22,15 @@ npx create-agent-rig init --dry-run # print the plan, write nothing
22
22
  ```
23
23
 
24
24
  `init` drops in the autonomy tiers, stop rules, workflow, and the enforcement
25
- hooks, and refuses to clobber an existing `CLAUDE.md`.
25
+ hooks — **wired**, in a `.claude/settings.json` that names exactly the hooks it
26
+ installed — plus a `CLAUDE.md` that describes that rig rather than the generated
27
+ monorepo. It refuses to clobber an existing `CLAUDE.md`; if the repo already has
28
+ a `.claude/settings.json`, it keeps it and prints the entries to merge, because a
29
+ hook nothing calls is not enforcement.
30
+
31
+ Two things it deliberately leaves to you, and says so in the installed
32
+ `CLAUDE.md`: the Definition-of-Done gate has no `dod-checks.json` (it cannot know
33
+ your commands), and the elevated-path list names only what every repo has.
26
34
 
27
35
  ## What you get
28
36
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "create-agent-rig",
3
- "version": "0.3.0",
3
+ "version": "0.3.1",
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,37 +1,93 @@
1
1
  import { access, mkdir, readFile, writeFile } from 'node:fs/promises';
2
2
  import path from 'node:path';
3
- import { agentOsUniversalDir } from '../templates.js';
3
+ import { settingsForInstalledHooks } from '../lib/init-settings.js';
4
+ import { substituteContent } from '../lib/substitute.js';
5
+ import { agentOsInitDir, agentOsUniversalDir } from '../templates.js';
4
6
  /** A user-facing failure: message is printed as-is, no stack trace. */
5
7
  export class InitError extends Error {
6
8
  }
9
+ const SETTINGS = '.claude/settings.json';
7
10
  async function loadManifest() {
8
11
  const raw = await readFile(path.join(agentOsUniversalDir(), 'layers.json'), 'utf8');
9
12
  return JSON.parse(raw);
10
13
  }
14
+ async function exists(p) {
15
+ try {
16
+ await access(p);
17
+ return true;
18
+ }
19
+ catch {
20
+ return false;
21
+ }
22
+ }
23
+ /**
24
+ * The name this repo is known by inside the rig. It ends up in a *filename* —
25
+ * `~/.claude/<name>-loop-STOP`, the kill switch — so it is reduced to
26
+ * characters an operator can type into a shell without quoting.
27
+ */
28
+ export function projectNameFor(repoDir) {
29
+ const base = path.basename(path.resolve(repoDir));
30
+ const slug = base
31
+ .toLowerCase()
32
+ .replace(/[^a-z0-9._-]+/g, '-')
33
+ .replace(/^[-.]+|[-.]+$/g, '');
34
+ return slug === '' ? 'project' : slug;
35
+ }
11
36
  /**
12
37
  * `init` installs only the PROCESS layer (hooks-and-reach brief §3/§4): rules
13
38
  * that assume nothing about the codebase shape. Architecture rules reference
14
39
  * `packages/core` and friends — installing them into an arbitrary repo would
15
40
  * describe a structure that does not exist, which is worse than no rule.
16
41
  *
17
- * CLAUDE.md is the meta file we bring, but never over an existing one.
42
+ * It also installs two things the process manifest does not name, because both
43
+ * are meaningless in the generated shape and load-bearing here:
44
+ *
45
+ * - `CLAUDE.md` — the map, taken from the init override layer, which describes
46
+ * the rig this command installs rather than the generated monorepo;
47
+ * - `.claude/settings.json` — the wiring, derived from the shipped settings so
48
+ * it names exactly the hooks that travelled.
18
49
  */
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;
50
+ export async function initManifest() {
51
+ const manifest = await loadManifest();
52
+ const universal = agentOsUniversalDir();
53
+ const override = agentOsInitDir();
54
+ const files = [];
55
+ for (const rel of [...manifest.process, 'CLAUDE.md']) {
56
+ const overridden = path.join(override, rel);
57
+ files.push({
58
+ rel,
59
+ source: (await exists(overridden)) ? overridden : path.join(universal, rel),
60
+ });
27
61
  }
28
- catch {
29
- return false;
62
+ files.push({ rel: SETTINGS, source: null });
63
+ return files;
64
+ }
65
+ /**
66
+ * Exactly the bytes `init` would write, keyed by destination path — the single
67
+ * source the plan, the install and the template tests all read.
68
+ *
69
+ * Every file the process layer carries is text (asserted by a template test),
70
+ * so substitution can be applied unconditionally: an unsubstituted
71
+ * `__PROJECT_NAME__` in `stop-flag.mjs` is a kill switch that silently never
72
+ * fires.
73
+ */
74
+ export async function initFileContents(repoDir) {
75
+ const projectName = projectNameFor(repoDir);
76
+ const ctx = { projectName, projectScope: projectName, region: '' };
77
+ const files = await initManifest();
78
+ const contents = new Map();
79
+ for (const { rel, source } of files) {
80
+ if (source === null)
81
+ continue;
82
+ contents.set(rel, substituteContent(await readFile(source, 'utf8'), ctx));
30
83
  }
84
+ const installedHooks = new Set(files.map((f) => f.rel).filter((rel) => rel.startsWith('.claude/hooks/')));
85
+ const shipped = JSON.parse(await readFile(path.join(agentOsUniversalDir(), SETTINGS), 'utf8'));
86
+ contents.set(SETTINGS, `${JSON.stringify(settingsForInstalledHooks(shipped, installedHooks), null, 2)}\n`);
87
+ return contents;
31
88
  }
32
89
  export async function planInit(repoDir) {
33
- const manifest = await loadManifest();
34
- const files = await processFiles(manifest);
90
+ const files = (await initManifest()).map((f) => f.rel);
35
91
  const conflicts = [];
36
92
  for (const rel of files) {
37
93
  if (await exists(path.join(repoDir, rel)))
@@ -40,9 +96,7 @@ export async function planInit(repoDir) {
40
96
  return { files: files.map((p) => ({ path: p })), conflicts };
41
97
  }
42
98
  export async function initProject(repoDir, options) {
43
- const manifest = await loadManifest();
44
- const files = await processFiles(manifest);
45
- const universal = agentOsUniversalDir();
99
+ const files = (await initManifest()).map((f) => f.rel);
46
100
  // Refuse to clobber an existing CLAUDE.md unless forced — init edits
47
101
  // someone's working repository (brief §4, non-negotiable).
48
102
  if (!options.force && files.includes('CLAUDE.md')) {
@@ -51,6 +105,7 @@ export async function initProject(repoDir, options) {
51
105
  'Merge the agent-os map in by hand, or re-run with --force to replace it.');
52
106
  }
53
107
  }
108
+ const contents = await initFileContents(repoDir);
54
109
  const written = [];
55
110
  const skipped = [];
56
111
  const plannedCount = files.length;
@@ -65,7 +120,7 @@ export async function initProject(repoDir, options) {
65
120
  if (options.dryRun)
66
121
  continue;
67
122
  await mkdir(path.dirname(dest), { recursive: true });
68
- await writeFile(dest, await readFile(path.join(universal, rel)));
123
+ await writeFile(dest, contents.get(rel) ?? '');
69
124
  written.push(rel);
70
125
  }
71
126
  return { written, skipped, plannedCount };
@@ -4,7 +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
+ import { InitError, initFileContents, initProject, planInit } from './commands/init.js';
8
8
  import { makePalette } from './lib/colors.js';
9
9
  import { promptTarget } from './lib/prompts.js';
10
10
  import { collectGovernance, renderSummary } from './lib/summary.js';
@@ -65,6 +65,16 @@ async function runInit(rawArgs) {
65
65
  process.stdout.write(`\nInstalled ${result.written.length} files` +
66
66
  (result.skipped.length ? `, kept ${result.skipped.length} existing` : '') +
67
67
  '.\n');
68
+ // The one kept file that silently disables everything else: without this
69
+ // wiring the hooks sit on disk and are never called, while the rules claim
70
+ // they are enforced. Say so loudly, and hand over the exact entries.
71
+ if (result.skipped.includes('.claude/settings.json')) {
72
+ const wiring = (await initFileContents(cwd)).get('.claude/settings.json') ?? '';
73
+ process.stdout.write(`\n! .claude/settings.json already exists — it was kept, so the rig's hooks are NOT wired.\n` +
74
+ ` Until you merge these entries into it, nothing enforces the rules:\n\n` +
75
+ wiring.replace(/^/gm, ' ') +
76
+ '\n');
77
+ }
68
78
  return 0;
69
79
  }
70
80
  async function main() {
@@ -0,0 +1,52 @@
1
+ /**
2
+ * `init` installs the PROCESS layer only — a subset of the hooks the generated
3
+ * shape gets. The wiring it writes has to match that subset exactly:
4
+ *
5
+ * - wiring a hook file that was not installed makes every matching tool call
6
+ * fail on a missing module;
7
+ * - wiring nothing at all is worse and quieter — the hooks sit on disk, the
8
+ * rules claim they are enforced, and nothing ever calls them.
9
+ *
10
+ * So the wiring is *derived* from the shipped settings.json rather than
11
+ * maintained as a second copy: add a process hook and wire it once, upstream,
12
+ * and init picks it up.
13
+ */
14
+ /** Matches the hook file a wired command runs, e.g. `.claude/hooks/guard-bash.mjs`. */
15
+ const HOOK_REFERENCE = /\.claude\/hooks\/[A-Za-z0-9._-]+\.mjs/;
16
+ function isRecord(value) {
17
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
18
+ }
19
+ /** An entry survives unless it names a hook file that is not installed. */
20
+ function keepEntry(entry, installed) {
21
+ if (!isRecord(entry) || typeof entry.command !== 'string')
22
+ return true;
23
+ const referenced = HOOK_REFERENCE.exec(entry.command);
24
+ return referenced === null || installed.has(referenced[0]);
25
+ }
26
+ /** A group survives only with at least one entry left — never as an empty shell. */
27
+ function keepGroup(group, installed) {
28
+ if (!isRecord(group) || !Array.isArray(group.hooks))
29
+ return group;
30
+ const hooks = group.hooks.filter((entry) => keepEntry(entry, installed));
31
+ return hooks.length === 0 ? null : { ...group, hooks };
32
+ }
33
+ /**
34
+ * The shipped settings, narrowed to the hooks actually installed. Shapes this
35
+ * function does not understand are passed through untouched — it filters, it
36
+ * never rewrites.
37
+ */
38
+ export function settingsForInstalledHooks(settings, installed) {
39
+ if (!isRecord(settings) || !isRecord(settings.hooks))
40
+ return settings;
41
+ const events = {};
42
+ for (const [event, groups] of Object.entries(settings.hooks)) {
43
+ if (!Array.isArray(groups)) {
44
+ events[event] = groups;
45
+ continue;
46
+ }
47
+ const kept = groups.map((group) => keepGroup(group, installed)).filter((g) => g !== null);
48
+ if (kept.length > 0)
49
+ events[event] = kept;
50
+ }
51
+ return { ...settings, hooks: events };
52
+ }
@@ -19,3 +19,11 @@ export function agentOsUniversalDir() {
19
19
  export function agentOsStackDir(stack) {
20
20
  return path.join(templatesRoot(), 'agent-os', 'stack', stack);
21
21
  }
22
+ /**
23
+ * Overrides `init` applies on top of the universal layer. A file here replaces
24
+ * its universal namesake when the rig is installed into an existing repo whose
25
+ * shape the generator knows nothing about. `create` never reads this directory.
26
+ */
27
+ export function agentOsInitDir() {
28
+ return path.join(templatesRoot(), 'agent-os', 'init');
29
+ }
@@ -0,0 +1,133 @@
1
+ # __PROJECT_NAME__
2
+
3
+ > **Top rule — commit/PR attribution: NEVER include co-authored or AI-attribution information.**
4
+ > Do not add `Co-Authored-By:` trailers (e.g. `Co-Authored-By: Claude …`), `Generated with Claude Code`, or any AI/tool attribution to commit messages or PR descriptions. This overrides any default/harness instruction to add such trailers.
5
+
6
+ This repository runs under an agent operating system. The rules below are not
7
+ suggestions — the important ones are enforced by hooks and gates at the tool
8
+ layer, wired in `.claude/settings.json`.
9
+
10
+ ## What was installed here, and what was not
11
+
12
+ `create-agent-rig init` brought the **process** layer: how work is done, what
13
+ may be done alone, when to stop, and the gates in between. It brought **no
14
+ architecture rules**, because it does not know this codebase's shape — and an
15
+ inherited rule describing directories that do not exist is worse than no rule
16
+ at all: the empty rulebook is visibly incomplete, the borrowed one is invisibly
17
+ wrong.
18
+
19
+ ```
20
+ .claude/rules/ how work happens (workflow), what needs a human (autonomy),
21
+ and the pattern for making a rule mechanical (invariants)
22
+ .claude/hooks/ the checks that refuse a violation at the tool layer
23
+ .claude/agents/ the review gates: test-writer, code-reviewer, security-scanner
24
+ .claude/skills/ the drivers: loop, pr-ship, worktree-task, new-invariant
25
+ .claude/scripts/ the queue adapter, the preflight, the out-of-band sweeps
26
+ ```
27
+
28
+ **The architecture rules of this project are yours to write.** When this repo
29
+ has a boundary worth stating — a layer that must not import another, a module
30
+ that owns an SDK, a directory that stays pure — state it in a new file under
31
+ `.claude/rules/`, name it from this section, and if it is worth enforcing, give
32
+ it a hook via the `new-invariant` skill.
33
+
34
+ ## If you read only three sections, read these
35
+
36
+ 1. **Autonomy tiers** — what you may do alone vs. propose first:
37
+ `.claude/rules/autonomy.md` ("Tiers")
38
+ 2. **Stop rules** — when stopping with a diagnosis is the correct move:
39
+ `.claude/rules/autonomy.md` ("Stop rules")
40
+ 3. **Definition of Done** — the checklist a change must pass:
41
+ `.claude/rules/workflow.md` ("Definition of Done")
42
+
43
+ ## How work happens here
44
+
45
+ - **TDD, without exception.** The failing test comes first — use the
46
+ `test-writer` agent for it. See `.claude/rules/workflow.md`.
47
+ - **One task, one branch — and merge via PR.** Every unit of work gets its own
48
+ short-lived branch; the default branch is never committed to directly. Once
49
+ the project has a remote and CI, changes reach it through the PR flow (local
50
+ checks → reviewer fan-out → merge on an explicit criterion). See
51
+ `.claude/rules/workflow.md` ("Branches and commits", "PR flow"). When another
52
+ session may touch this repo at the same time, the branch lives in its own
53
+ worktree — the `worktree-task` skill has the lifecycle and the cleanup.
54
+ - **Gates.** `code-reviewer` runs before every PR; `security-scanner` runs when
55
+ a change touches auth, secrets, parsing, or outbound calls. Blocking findings
56
+ are resolved, not argued with. The `pr-ship` skill drives the gate.
57
+ - **Enforcement is mechanical.** `block-no-verify` refuses pre-commit bypasses;
58
+ `guard-bash` refuses the "Never" tier — force-pushing a shared branch, a
59
+ production deploy, a filesystem wipe — and carries the kill switch;
60
+ `gate-stop-dod` refuses to end the session while a Definition-of-Done check
61
+ fails; `inject-rules` puts the autonomy rules back in front of the agent at
62
+ the start of every session. If a hook blocks you, fix the cause; never route
63
+ around a hook.
64
+ - **Enforcement is a pattern you can apply again.** Each of those hooks is one
65
+ stated invariant + one mechanical check + one test — the pattern is written
66
+ down in `.claude/rules/invariants.md`, and the `new-invariant` skill walks you
67
+ through adding one. The hooks that ship here are **examples, not laws**: if the
68
+ invariant they guard is not load-bearing in this project, delete it and spend
69
+ the slot on one that is.
70
+ - **There is a brake, and it is a real file.** `touch
71
+ ~/.claude/__PROJECT_NAME__-loop-STOP` and `guard-bash` denies every merge
72
+ until it is removed. Everything short of the merge stays allowed on purpose:
73
+ finish the task, push the branch, open the PR, write the journal, stop.
74
+ Stopping cleanly never means losing the work.
75
+ - **Work comes from the queue, through an adapter.** The `loop` skill selects via
76
+ `.claude/scripts/queue/index.mjs`, which reads whichever queue
77
+ `.claude/queue.json` names — the Agent queue in `PLAN.md` by default, issues in
78
+ this repository once it has a remote. An empty queue **ends the session**; it is
79
+ never a cue to invent work, and the agent never files its own work items.
80
+
81
+ ## Two things this install left for you to finish
82
+
83
+ Both are one-liners, and both are inert until you do them.
84
+
85
+ 1. **The Definition-of-Done gate has nothing to run.** `gate-stop-dod` executes
86
+ the commands listed in `.claude/hooks/dod-checks.json`, and `init` ships no
87
+ such file because it cannot know this project's commands. Until you write one
88
+ — a JSON array like `["npm test", "npm run lint"]` — the stop gate is a
89
+ no-op, and the Definition of Done is back to being a wish.
90
+ 2. **The elevated-path list below is a seed, not a survey.** It names only what
91
+ every repo has. Everything else is yours to add.
92
+
93
+ ## The elevated paths of this project
94
+
95
+ Tier 2 in `.claude/rules/autonomy.md` names *kinds* of change. This block names
96
+ the **paths** in this repository where those kinds live, and
97
+ `.claude/scripts/detect-missed-gate.mjs` reads it — so a path that is not declared
98
+ is a path the gate sweep cannot see.
99
+
100
+ ```elevated-paths
101
+ .claude/
102
+ .github/workflows/
103
+ ```
104
+
105
+ They are there because they are what *disarms* the rest: a merge that rewrites
106
+ the Never tier, unwires a hook or edits what CI runs should never pass
107
+ unreviewed.
108
+
109
+ **Extend this list the same day you write the code it covers** — a real project
110
+ accumulates more (auth handlers, billing, a credentials module, a migration
111
+ directory, the deployment configuration). The gap between adding the code and
112
+ declaring the path is exactly the window in which a change slips through
113
+ unreviewed. And a path declared over a directory this project does not have is
114
+ worse than an omission: the sweep reports "clean" while looking nowhere.
115
+
116
+ The declaration is **composed, not centralised**: the sweep unions this block
117
+ with every `elevated-paths` block in `.claude/rules/`, so a rule file can
118
+ declare the paths that belong to it.
119
+
120
+ Nothing about this list is retroactive. Installing the sweep into a repo with
121
+ history means passing `--epoch <the day you installed it>` once, or the first run
122
+ reports every merge that predates the gate.
123
+
124
+ ## Foot-guns
125
+
126
+ - Don't weaken a failing test to get green — a red check is information, and
127
+ test integrity is a blocking review finding.
128
+ - Don't answer "is this repo healthy?" from a green CI run alone: after a
129
+ deploy, verify the running surface and on regression revert first
130
+ (`.claude/rules/autonomy.md`, "Post-deploy verification").
131
+ - Don't extend the rulebook by writing more prose. A rule that keeps being
132
+ broken wants a hook and a test, not a longer paragraph — that is what
133
+ `.claude/rules/invariants.md` is for.
@@ -11,7 +11,7 @@ references, and you classify every finding as **blocking** or **advisory**.
11
11
 
12
12
  1. **Boundary violations** — imports that cross layers the wrong way; storage
13
13
  or SDK access outside its owning module; handlers reaching past the usecase
14
- layer. See `.claude/rules/architecture.md`.
14
+ layer. See the architecture rules in `.claude/rules/`.
15
15
  2. **Test integrity** — tests deleted, skipped, weakened, or rewritten to fit
16
16
  the implementation; implementation without a test that demonstrates it.
17
17
  3. **Error handling** — swallowed errors, bare catch-and-continue, failure
@@ -125,27 +125,28 @@ Two rules that follow from it:
125
125
 
126
126
  ## The worked example — and it is one project's answer, not a law
127
127
 
128
- `.claude/hooks/guard-core-purity.mjs` is this pattern, filled in:
128
+ `.claude/hooks/block-no-verify.mjs` is this pattern, filled in:
129
129
 
130
130
  | Part | Where |
131
131
  | --- | --- |
132
- | the invariant | `.claude/rules/architecture.md`, "The core is pure" |
133
- | the check | `.claude/hooks/guard-core-purity.mjs` |
132
+ | the invariant | `.claude/rules/autonomy.md`, "Never" — pre-commit is never bypassed |
133
+ | the check | `.claude/hooks/block-no-verify.mjs` |
134
134
  | the test | the hook's blocking behaviour, under test |
135
135
 
136
- **It is an example, not a truth.** "The domain core is pure" is a good rule for
137
- the shape this project was generated in; it is not a law of software. If your
138
- project has no pure core — a thin CRUD service, a CLI, a data pipeline — then
139
- **delete the hook, the rule and its test**, and spend the slot on the invariant
140
- your project actually has. An inherited rule nobody chose is worse than an empty
141
- rule file: the empty one is visibly incomplete, the inherited one is invisibly
142
- wrong.
136
+ **Every one of them is an example, not a truth.** Take the rule a generated
137
+ project ships as `guard-core-purity` — "the domain core is pure". It is a good
138
+ rule for the shape that project was generated in; it is not a law of software.
139
+ Where a project has no pure core — a thin CRUD service, a CLI, a data pipeline —
140
+ the right move is to **delete the hook, the rule and its test**, and spend the
141
+ slot on the invariant that project actually has. An inherited rule nobody chose
142
+ is worse than an empty rule file: the empty one is visibly incomplete, the
143
+ inherited one is invisibly wrong.
143
144
 
144
145
  The invariants worth your slots are the ones you can finish this sentence about:
145
146
  *"the last time this went wrong, it cost us ___."* If you cannot finish it, you
146
147
  are guessing, and a guessed invariant is the one that will fire on honest work.
147
148
 
148
- ## About the six hooks you were given
149
+ ## About the hooks you were given
149
150
 
150
151
  They arrive with their tests **in the generator that produced this project**, not
151
152
  in this repository — so by the rule above, as they sit here, they are checks