create-agent-rig 0.2.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.
Files changed (33) hide show
  1. package/CHANGELOG.md +134 -0
  2. package/README.md +57 -9
  3. package/package.json +9 -2
  4. package/packages/cli/dist/lib/summary.js +19 -5
  5. package/templates/agent-os/stack/aws-cdk/.claude/rules/aws-cdk.md +46 -0
  6. package/templates/agent-os/stack/aws-cdk/.claude/skills/ro-debug/SKILL.md +117 -0
  7. package/templates/agent-os/universal/.claude/hooks/block-no-verify.mjs +12 -2
  8. package/templates/agent-os/universal/.claude/hooks/guard-bash.mjs +808 -0
  9. package/templates/agent-os/universal/.claude/queue.json +3 -0
  10. package/templates/agent-os/universal/.claude/rules/autonomy.md +43 -0
  11. package/templates/agent-os/universal/.claude/rules/invariants.md +169 -0
  12. package/templates/agent-os/universal/.claude/scripts/detect-missed-gate.mjs +489 -0
  13. package/templates/agent-os/universal/.claude/scripts/preflight.mjs +161 -0
  14. package/templates/agent-os/universal/.claude/scripts/queue/core.mjs +305 -0
  15. package/templates/agent-os/universal/.claude/scripts/queue/github-issues.mjs +231 -0
  16. package/templates/agent-os/universal/.claude/scripts/queue/index.mjs +175 -0
  17. package/templates/agent-os/universal/.claude/scripts/queue/jira.mjs +345 -0
  18. package/templates/agent-os/universal/.claude/scripts/queue/plan-md.mjs +239 -0
  19. package/templates/agent-os/universal/.claude/scripts/reconcile-external-prs.mjs +280 -0
  20. package/templates/agent-os/universal/.claude/scripts/stop-flag.mjs +62 -0
  21. package/templates/agent-os/universal/.claude/settings.json +4 -0
  22. package/templates/agent-os/universal/.claude/skills/loop/SKILL.md +297 -40
  23. package/templates/agent-os/universal/.claude/skills/new-invariant/SKILL.md +102 -0
  24. package/templates/agent-os/universal/.claude/skills/new-invariant/guard-invariant.example.mjs +78 -0
  25. package/templates/agent-os/universal/.claude/skills/new-invariant/guard-invariant.example.test.mjs +89 -0
  26. package/templates/agent-os/universal/.claude/skills/worktree-task/SKILL.md +73 -0
  27. package/templates/agent-os/universal/CLAUDE.md +57 -7
  28. package/templates/agent-os/universal/PLAN.md +28 -2
  29. package/templates/agent-os/universal/layers.json +20 -1
  30. package/templates/skeleton/aws-serverless/.github/workflows/ci.yml +6 -1
  31. package/templates/skeleton/aws-serverless/gitignore +8 -0
  32. package/templates/skeleton/node-service/.github/workflows/ci.yml +6 -1
  33. package/templates/skeleton/node-service/gitignore +8 -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
@@ -39,19 +39,55 @@ never inflated). The hooks live in `.claude/hooks/` and are wired in
39
39
  - **`block-no-verify`** — refuses bypassing pre-commit checks (and knows the
40
40
  difference between using the `--no-verify`/`-n` flag and merely mentioning it
41
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);
42
49
  - **`gate-stop-dod`** — refuses to end the session while a Definition-of-Done
43
50
  check is red; it fails open (a missing or corrupt config never makes the
44
51
  session unquittable) and never blocks twice in a row;
45
52
  - **`inject-rules`** — re-injects the autonomy rules at session start, so they
46
53
  survive compaction and resumes.
47
54
 
48
- Around the hooks, the operating system: **autonomy tiers** (what an agent does
49
- alone / after review / never), **stop rules** (three strikes, flaky retry,
50
- session staleness), **subagent gates** (`test-writer`, `code-reviewer`,
51
- `security-scanner`, and `cdk-diff-reviewer` on the AWS target), **skills**
52
- (`pr-ship` pre-merge gate; `loop` queue driver; `post-deploy-verify` with its
53
- binary HEALTHY/REGRESSION verdict on the AWS target), and a one-page
54
- `CLAUDE.md` map a fresh session orients by.
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.
55
91
 
56
92
  The skeleton around it is real and runnable — pure core shared by server _and_
57
93
  browser (one schema validates on both sides of the wire), a mandatory usecase
@@ -117,5 +153,17 @@ dependencies fresh to catch upstream breakage early. This repo dogfoods its own
117
153
  rulebook — `CLAUDE.md` and `.claude/` are composed from the templates, and
118
154
  drift fails the suite.
119
155
 
120
- Development: `pnpm test` (full), `pnpm test:unit` (fast loop),
121
- `pnpm template:check` (templates in place). The plan of record is `PLAN.md`.
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.2.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"
@@ -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)).map((entry) => entry.replace(strip, '')).sort();
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
- hooks: (await names(path.join(claude, 'hooks'), /\.mjs$/))
17
- // guard-core-purity "core purity": the mechanism, not the filename
18
- .map((hook) => hook.replace(/^(guard|block)-/, '').replaceAll('-', ' ')),
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
 
@@ -0,0 +1,117 @@
1
+ ---
2
+ name: ro-debug
3
+ description: Investigate the deployed runtime read-only — Lambda logs and error rates, DLQ depth and message age, table reads — with the traps that have produced confidently-wrong diagnoses before. Use when something on the deployed stage misbehaves, and before writing any fix.
4
+ allowed-tools: Bash, Read, Grep
5
+ ---
6
+
7
+ # Read-only runtime investigation
8
+
9
+ A fix is always a code change through a PR. This skill only **looks** — and it is
10
+ scoped read-only so that a session diagnosing an incident cannot become a session
11
+ mutating production state under pressure, which is when that decision is worst.
12
+
13
+ ## The role this assumes, and the honest caveat
14
+
15
+ Every command below wants a **read-only profile**: permission to read logs,
16
+ metrics, queue attributes and table items, and nothing else. No decrypt, no
17
+ secret reads, no mutations — so a credential cannot leak through this path even
18
+ by accident.
19
+
20
+ ⚠ **The skeleton does not provision that role.** It ships the application, not
21
+ your account's access model, and minting a role is an **owner action** (a Tier-2
22
+ decision — it is IAM). Until it exists, either create it once with those four
23
+ read scopes, or accept that you are investigating with wider credentials than the
24
+ task needs and say so in the write-up. Do not silently upgrade to an admin
25
+ profile and carry on.
26
+
27
+ ```sh
28
+ export AWS_PROFILE=<your read-only profile>
29
+ export AWS_REGION=__REGION__
30
+ ```
31
+
32
+ ## Before reading ANY code to explain a runtime behaviour
33
+
34
+ ```sh
35
+ git fetch origin && git rev-parse HEAD "origin/$(git symbolic-ref --short HEAD)"
36
+ ```
37
+
38
+ **If the local branch differs from its remote, diagnose from the remote**
39
+ (`git show origin/<branch>:<file>`, or a worktree). **A fetch does not move your
40
+ checkout** — reading stale local code while describing deployed behaviour is
41
+ the single most reliable way to produce a confident, wrong diagnosis, and it has
42
+ happened more than once. See `.claude/rules/autonomy.md`, "Session staleness".
43
+
44
+ ## Recipes
45
+
46
+ **Find the function first** — CDK generates the physical names, so never guess one:
47
+
48
+ ```sh
49
+ aws lambda list-functions \
50
+ --query "Functions[?contains(FunctionName,'Notes')].FunctionName" --output text
51
+ ```
52
+
53
+ **Errors in a window:**
54
+
55
+ ```sh
56
+ aws logs filter-log-events --log-group-name "/aws/lambda/<fn>" \
57
+ --start-time <epoch-ms> --filter-pattern "ERROR" \
58
+ --max-items 20 --query 'events[].message' --output text
59
+ ```
60
+
61
+ **DLQ depth, then message age.** Depth comes from the queue; **age does not** —
62
+ `ApproximateAgeOfOldestMessage` is a **CloudWatch metric, not an SQS attribute**,
63
+ and asking for it as an attribute fails with `InvalidAttributeName`:
64
+
65
+ ```sh
66
+ aws sqs get-queue-attributes --queue-url <notes-dlq-url> \
67
+ --attribute-names ApproximateNumberOfMessages
68
+
69
+ aws cloudwatch get-metric-statistics --namespace AWS/SQS \
70
+ --metric-name ApproximateAgeOfOldestMessage \
71
+ --dimensions Name=QueueName,Value=<notes-dlq-name> \
72
+ --start-time <iso> --end-time <iso> --period 300 --statistics Maximum
73
+ ```
74
+
75
+ Age is what tells you whether a non-empty DLQ **predates** the thing you are
76
+ investigating. A days-old backlog is not your regression, and treating it as one
77
+ sends the whole diagnosis in the wrong direction.
78
+
79
+ **Table reads** — key by whatever `packages/db` composes; nothing else knows the
80
+ key shape:
81
+
82
+ ```sh
83
+ aws dynamodb query --table-name <NotesTable output> \
84
+ --key-condition-expression "pk = :pk" \
85
+ --expression-attribute-values '{":pk":{"S":"NOTE#<id>"}}' --max-items 3
86
+ ```
87
+
88
+ **Error rate:** `AWS/Lambda` `Errors`, `Sum`, by `FunctionName`, over the suspect
89
+ window.
90
+
91
+ ## Interpretation rules — where wrong diagnoses actually come from
92
+
93
+ - 🔴 **An empty metric result is "no signal", never "healthy".** Zero datapoints
94
+ means **no invocations in the window** — the function was not exercised, so the
95
+ metric says nothing about whether it works. Report it as no signal and go find
96
+ a window with traffic. This is the same rule the `post-deploy-verify` skill
97
+ states, and for the same reason: a vacuous pass is worse than a missing one,
98
+ because it gets believed.
99
+ - 🔴 **`StackStatus: UPDATE_COMPLETE` is stale evidence.** It persists from the
100
+ previous deploy, so it is true of a stack whose latest deploy failed. The
101
+ authoritative signals are the **deploy job's conclusion**
102
+ (`gh run list --workflow deploy`) and the stack's `LastUpdatedTime` freshness.
103
+ - **DNS and asset-upload failures are infrastructure flakes, not code
104
+ regressions.** A deploy that died at asset publish or checkout tells you nothing
105
+ about the change. Your own read-only calls can hit the same flake — a failed
106
+ probe is `unknown`, not a finding.
107
+ - **Never work around the profile's denials — that is the point of the profile.**
108
+ Needing a decrypt, a secret or a mutation means the investigation has reached
109
+ its boundary: escalate to the human with what you found
110
+ (`.claude/rules/autonomy.md`, "Escalation format").
111
+
112
+ ## What to hand back
113
+
114
+ What was observed (verbatim, not summarised), which window, which signals were
115
+ **unavailable** and why, the current hypothesis, and the narrowest reproduction.
116
+ A read-only investigation that ends in a named uncertainty is a good outcome; one
117
+ that ends in a confident story built on a stale read is not.
@@ -4,6 +4,14 @@
4
4
  //
5
5
  // Contract (Claude Code): JSON on stdin; exit 0 = allow, exit 2 = block, and
6
6
  // stderr is shown to the agent as the reason.
7
+ //
8
+ // Stated limit: it strips QUOTED text so prose about the flag is not a bypass,
9
+ // but it does not parse heredocs. Writing `git commit -nm …` inside a heredoc
10
+ // body — a doc, a test fixture, a PR description — is therefore blocked. That
11
+ // happened while writing this hook's own tests. The fix is not to teach this file
12
+ // to tokenise: it owns exactly one invariant and stays readable because of it
13
+ // (see .claude/rules/invariants.md, "One invariant per hook"). Use a file rather
14
+ // than a heredoc, or quote the example.
7
15
  import { readFileSync } from 'node:fs';
8
16
 
9
17
  function main() {
@@ -26,8 +34,10 @@ function main() {
26
34
  const [segment, verb] = match;
27
35
  const bypasses =
28
36
  /(^|\s)--no-verify\b/.test(segment) ||
29
- // -n is --no-verify for commit only (for push it means --dry-run).
30
- (verb === 'commit' && /(^|\s)-n\b/.test(segment));
37
+ // `-n` is --no-verify for commit only (for push it means --dry-run), and it
38
+ // counts inside a COMBINED cluster: `git commit -nm "msg"` bypassed the
39
+ // pre-commit gate outright, which is the one thing this hook exists to stop.
40
+ (verb === 'commit' && /(^|\s)-[a-zA-Z]*n[a-zA-Z]*(\s|$)/.test(segment));
31
41
  if (bypasses) {
32
42
  process.stderr.write(
33
43
  'BLOCKED — bypassing pre-commit checks is never allowed. ' +