@erclx/aitk 3.52.0 → 3.53.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.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "aitk",
3
3
  "description": "Automated governance, versioning, and discovery tools for Claude Code.",
4
- "version": "3.52.0",
4
+ "version": "3.53.0",
5
5
  "author": {
6
6
  "name": "Eric Le",
7
7
  "url": "https://github.com/erclx"
@@ -117,6 +117,14 @@ Test every unchecked box against the testing discipline in `${CLAUDE_SKILL_DIR}/
117
117
 
118
118
  Test every ticked box too, bounded to one naming a file or a command. Confirm what it names still exists at `<headRefOid>`, `git show <headRefOid>:<path>` for a file or `git grep <command> <headRefOid>` for a command, rather than re-running what it claims. A box naming neither stays untested, since nothing here can confirm a claim carrying no artifact. A box ticked before a later commit removed or replaced what it names is a `should-fix` finding on the body itself, filed under a `**PR body**` block ahead of the file blocks, since what it corrupts is the merge record rather than a file in the diff.
119
119
 
120
+ Test what `## Key Changes` claims against the diff it describes, with `aitk pr key-changes <number> --json`. This repository squash-merges, so that section becomes the commit message and the record on the trunk once the branch is gone, which is the same thing a stale ticked box corrupts. Branch on the record rather than on the exit code, which a shell function wrapping `aitk` can flatten to zero.
121
+
122
+ Read `unmet` as a `should-fix` finding under the same `**PR body**` block the stale box takes, one entry per path, quoting the bullet the record carries on `preview` and naming the `head` the comparison ran at. Read the bullet before filing it, since one class survives the reader: a bullet citing where something is defined while claiming an edit elsewhere, which puts a real path in the claim region and points the change at a locative the path does not name. Report that as the class rather than as a stale claim. A body is edited between pushes, so the reading is true at that commit and can be false a minute later. Read `unnamed` as a question rather than a finding, since the class covers a real omission and equally a lockfile, a generated asset, or a regenerated index that earns no bullet, and raise it only where a reader would want the bullet. Report `unresolved` nowhere: a path written partially was judged in neither direction and says nothing about the body.
123
+
124
+ A `reason` of `no-claims` means the reader resolved no path out of a section that exists, which is the extractor failing over prose rather than the body being wrong. Say so and raise nothing. `no-section` is the body carrying no such heading, which is a finding only where the pull request template asks for one.
125
+
126
+ Compose this on the body read Step 4 already performs rather than opening a second one. That step scans the title and body for a leaked phase label, and both reads answer from the same text at the same head.
127
+
120
128
  Ask rather than grade. Whether a human is genuinely required is a reading the branch author may hold a reason this session cannot see, so the question carries no severity and enters no count. What it does carry is the heading and the dispatch, on the rule Step 4 states, because a question the author never receives corrects nothing and the author is the only party who can answer it. Answering it closes it, and the answer may be that the requirement holds.
121
129
 
122
130
  Read `## For the reviewer` the same way, bounded to the bullets under that heading rather than the Summary or the Technical Context around it. Those two carry the author's argument for the change, and reading them while judging it is most of what an independent pass exists to avoid, so the read stops at the section itself.
@@ -34,7 +34,7 @@ Each is invoked as the CLI the caller is running rather than as a global `aitk`.
34
34
 
35
35
  Four findings fail the run: an unresolved context citation, a banned character, word, or spelling, a skill folder carrying no `REQUIREMENT.md`, and a credential-shaped value in the tree the package ships. Each is a fact with no false-positive class behind it.
36
36
 
37
- Three of the four are the ones `scripts/core/verify.sh` already fails a push on. The secret scan is the one entry gating without a stage behind it, added on that same fact-or-judgment test rather than as a side effect of registering a measure, and the architecture record already ranks content leaving the repository above content that stays.
37
+ Three of the four are the ones `aitk gate run` already fails a push on. The secret scan is the one entry gating without a stage behind it, added on that same fact-or-judgment test rather than as a side effect of registering a measure, and the architecture record already ranks content leaving the repository above content that stays.
38
38
 
39
39
  Everything else reports. A heavy bullet, a long entry, a board row nothing resolves, a degradation term in a comment, and an implementation reaching history ahead of its test are judgments a reader settles. A push failing on one of those teaches contributors to route around the stage, which is the split recorded across every audit here and the one this command inherits rather than moves.
40
40
 
@@ -61,9 +61,11 @@ Full help: `aitk <command> --help`. Behavior notes for the install and sync verb
61
61
  | `aitk labels audit` | Report the labels a changed set earns from the pull request label map and the paths no row reaches (`--json`) |
62
62
  | `aitk labels scan` | Fail a pull request whose title or body carries a phase label, sorting a release pull request's tokens as semver rather than as a leak (`--event`, `--json`) |
63
63
  | `aitk autoship classify` | Decide whether a changed set needs the review pass, naming the file and the test that decided it (`--json`) |
64
+ | `aitk pr key-changes` | Compare the files a pull request body's Key Changes names against its own diff, in both directions (`--body`, `--base`, `--json`) |
64
65
  | `aitk census [path]` | Report tracked file count, a breakdown by extension, and a line total that skips whatever reads as binary (`--json`) |
65
66
  | `aitk audits run` | Run every audit as one set, report per check under one verdict, and compare each count to the recorded baseline (`--json`, `--record`) |
66
67
  | `aitk audits list` | List every audit the set runs, with the corpus each reads and whether it gates (`--json`) |
68
+ | `aitk gate run` | Run every stage that guards a branch here, scoping shell, types, and tests to the changed set (`--all`, `--no-write`, `--nested`, `--json`) |
67
69
  | `aitk inventory [subject]` | Walk every route a project declares and group its elements by the property each computes, as a listing rather than a gate (`--json`) |
68
70
  | `aitk capture [source]` | Render HTML capture sources to PNG, toolkit-only and absent from an installed package |
69
71
  | `aitk serve [dir]` | Serve a directory on the loopback interface and print the link that opens it, running until interrupted (`--port`, `--entry`, `--json`) |
@@ -116,6 +118,7 @@ Each domain exposes a consistent shape where applicable: `list`, `install`, `syn
116
118
  | `labels` | `audit` |
117
119
  | `autoship` | `classify` |
118
120
  | `audits` | `run`, `list` |
121
+ | `gate` | `run` |
119
122
 
120
123
  Common patterns:
121
124
 
@@ -0,0 +1,84 @@
1
+ ---
2
+ title: Merge gate
3
+ description: Running the gate this repository verifies a branch with, what the stage table holds and what stays a script, how the changed set scopes three stages, and why a stage that cannot read its input reports rather than passing
4
+ ---
5
+
6
+ # Merge gate
7
+
8
+ `aitk gate run` runs every stage that guards a branch here, in order, stopping at the first stage that finds a fact. `bun run check` and `bun run check:ci` resolve to it, and `scripts/core/update.sh` calls it with `--nested` after a dependency update.
9
+
10
+ ```bash
11
+ aitk gate run
12
+ aitk gate run --all --no-write
13
+ aitk gate run --json
14
+ ```
15
+
16
+ | Option | Behavior |
17
+ | ------------ | ----------------------------------------------------------------------------- |
18
+ | `--all` | Run every stage instead of scoping shell, types, and tests to the changed set |
19
+ | `--no-write` | Check formatting instead of applying it, which is what a merge gate wants |
20
+ | `--nested` | Suppress the outer frame when a calling script has already opened one |
21
+ | `--json` | Add a machine-readable record on stdout |
22
+
23
+ ## What the command owns and what it runs
24
+
25
+ Three things sit in the command: the stage table in `src/gate/stages.ts`, the changed-file scoping and the run loop in `src/gate/sequencer.ts`, and every threshold comparison in `src/gate/measures.ts`. Each individual check is the script or the verb it already was, under `scripts/core/` or behind another `aitk` command, and the move changed none of them. Sequencing, scoping, and comparison are where the recurring defects were, and a check whose behavior changed while its sequencing moved would make any regression impossible to attribute.
26
+
27
+ A stage is a list of checks and a check is one of four kinds:
28
+
29
+ | Kind | What it is |
30
+ | --------- | ------------------------------------------------------------------------------- |
31
+ | `command` | Any binary, run from the project root |
32
+ | `cli` | This checkout's own `src/cli.ts`, never a globally installed `aitk` |
33
+ | `drift` | A regenerated pathspec asserted against the index and against the untracked set |
34
+ | `measure` | A reading whose verdict is a comparison rather than an exit code |
35
+
36
+ A `cli` check runs the source rather than the binary because a globally installed `aitk` resolves to the main checkout no matter which worktree is running, so a gate reading through it would measure the wrong tree and pass a branch it never opened.
37
+
38
+ Every check is an argument vector rather than a shell line, so no stage carries a quoting hazard and a `drift` pathspec reaches git exactly as the table spells it.
39
+
40
+ ## Scoping
41
+
42
+ Shell, types, and tests read the changed set. Everything else always runs, because its input is diffuse enough that no path predicts it.
43
+
44
+ The changed set unions the branch diff against the merge base with `origin/main`, the working tree, and untracked files, which is what a pull request will contain. The baseline is the remote ref and not local `main`, since on `main` itself the local ref is HEAD and every unpushed commit would drop out. Every fallback widens rather than narrows: no merge base at all runs every stage, and a local baseline equal to HEAD does the same. `--all` turns scoping off outright, which is what `bun run check:ci` passes so CI stays the backstop for a wrong local scoping decision.
45
+
46
+ ## A stage that cannot read its input
47
+
48
+ A stage reports one of four states. It passed, it was scoped out, it found a fact, or it could not measure its input at all.
49
+
50
+ The fourth is the one worth naming. An absent tool, a catalog that did not report, a corpus with nothing under it: each used to print a line that read like a pass. Now the run records it as unmeasured, the closing line says how many stages measured nothing, and the reader is not told a verdict nobody took.
51
+
52
+ What happens next depends on where the run is. On a contributor's machine it warns and the run still exits 0, because an absent tool there is somebody mid-setup. Under CI, read off `CI=true`, it refuses, because the same absence on a runner is a broken workflow step.
53
+
54
+ ## Exit codes
55
+
56
+ | Code | Meaning |
57
+ | ---- | ------------------------------------------------------------- |
58
+ | `0` | every stage that ran reported, and none found a fact |
59
+ | `1` | a stage found a fact, or could not measure its input under CI |
60
+
61
+ One code for a failure, which is what a `bun run` caller and a git hook both read. An unmeasured stage takes no code of its own, since it has already refused under CI and reports on a contributor's machine, so a second code would name a state no caller branches on.
62
+
63
+ ## The record
64
+
65
+ `--json` puts one record on stdout and keeps every diagnostic on stderr:
66
+
67
+ ```json
68
+ {
69
+ "ok": true,
70
+ "root": "/path/to/checkout",
71
+ "scoped": true,
72
+ "changed": 12,
73
+ "summary": {
74
+ "ran": 23,
75
+ "passed": 22,
76
+ "skipped": 1,
77
+ "unmeasured": 0,
78
+ "failed": 0
79
+ },
80
+ "stages": [{ "id": "indexes", "label": "Indexes", "status": "passed" }]
81
+ }
82
+ ```
83
+
84
+ A failing stage carries its remedy in `failure`, and the same line goes to stderr so a caller reading neither the record nor the frame is still told what to fix.
@@ -18,9 +18,11 @@ CLI catalog and invocation rules for agents, split by command domain. Start with
18
18
  - [Self-stated counts](counts.md): Reading a sentence that asserts a closed catalog's size, how a match is decided, the plausibility filter that keeps a generic word from matching a subset, and why the sweep reports rather than gates
19
19
  - [Demo](demo.md): Compiling a screencast draft into a runnable plan, driving a served application to a recording and a still, the pointer the recording paints, and what each refusal reports
20
20
  - [Docs](docs.md): How aitk docs resolves the toolkit's own reference surface from an install root, and how a split domain is named
21
+ - [Merge gate](gate.md): Running the gate this repository verifies a branch with, what the stage table holds and what stays a script, how the changed set scopes three stages, and why a stage that cannot read its input reports rather than passing
21
22
  - [Indexes](indexes.md): Flags, exit codes, and JSON shape for aitk indexes regen, plus when it auto-stages what it rewrote
22
23
  - [Install and sync](install-and-sync.md): What each install and sync verb writes, refuses, or leaves alone, and how drift is attributed in a target project
23
24
  - [Intake](intake.md): Reading intake folder counts and items, the three read states an item can be in, landing a batch of selections in one cluster, the refusal reasons, and why a call is scoped to one file
25
+ - [Key Changes bijection](key-changes.md): Comparing the files a pull request body's Key Changes names against its own diff, the two directions and why they are graded apart, the span rules the extractor was measured into, and the three refusals that separate a clean pass from a read that produced nothing
24
26
  - [Label coverage](label-coverage.md): Reading a changed set against the pull request label map, the two tables it matches, how a gap is separated from a decision, and why an absent map is an answer
25
27
  - [Markdown audit](markdown-audit.md): Running the audit over any markdown path, where its bans and checkpoints are read from, what each check reports, and why the ban half gates while the structural half reports
26
28
  - [Output shape](output-shape.md): Two framed shapes every command renders into, how JSON and --names modes keep stdout clean, and the exit discipline that lets piped output drain
@@ -0,0 +1,103 @@
1
+ ---
2
+ title: Key Changes bijection
3
+ description: Comparing the files a pull request body's Key Changes names against its own diff, the two directions and why they are graded apart, the span rules the extractor was measured into, and the three refusals that separate a clean pass from a read that produced nothing
4
+ ---
5
+
6
+ # Key Changes bijection
7
+
8
+ `aitk pr key-changes` reads the `## Key Changes` section of a pull request body, lifts the paths its bullets claim, and compares that set against the files the pull request actually changed. This repository squash-merges, so the body becomes the commit message and the record on the trunk once the branch is gone.
9
+
10
+ ```bash
11
+ aitk pr key-changes
12
+ aitk pr key-changes 1265 --json
13
+ aitk pr key-changes --body .claude/.tmp/pr/body.md --base origin/main
14
+ ```
15
+
16
+ The positional is the pull request to read, defaulting to the one open on this branch.
17
+
18
+ | Option | Behavior |
19
+ | --------------- | ------------------------------------------------------------------ |
20
+ | `--body <path>` | Read the body from a file, taking the changed set from git instead |
21
+ | `--base <ref>` | Far side of the range when `--body` supplies the body |
22
+ | `--root <path>` | Repository to read, defaulting to the cwd |
23
+ | `--json` | Add a machine-readable record on stdout, keeping the frame |
24
+
25
+ `--body` decides where both halves come from, so a number passed beside it is never read. The body comes off disk and the changed set from the local range, which is the shape a fixture and a body still being drafted both need.
26
+
27
+ Without `--body` the body, the file list, and the head commit come back from one `gh pr view` call, because the three have to describe the same commit and reading them apart leaves a window where a push between them compares a body against another head's files.
28
+
29
+ That view caps its file list at 100 rows and says nothing about having done so, which was measured against `#1250`: the pull request carries 101 files and the view reports 100. A pull request at the cap therefore takes a second read through the paginated endpoint, and a failure there refuses with `gh-truncated` rather than comparing against a set known to be short. A set silently one file short is the worst input this takes, since the missing file is exactly what a correct bullet would then be accused of inventing.
30
+
31
+ ## The two directions
32
+
33
+ They are reported apart because they want different tolerances.
34
+
35
+ - **`unmet`** is a whole path the body claims and the diff does not carry. This is the graded direction and it sets the exit code. A bullet naming an untouched file is wrong more often than not, and it corrupts the record that reaches the trunk.
36
+ - **`unnamed`** is a changed file no bullet reached. Reported with no grade, since the class covers a real omission and equally a lockfile, a generated asset, or a regenerated index that earns no bullet. Grading it would fire on nearly every branch.
37
+ - **`unresolved`** is a path written partially, such as `claude-worker/SKILL.md` for a file under `claude/skills/`. It is judged in neither direction.
38
+
39
+ A partial path can credit a changed file and can never accuse one. Nothing separates a path written short from a path written wrong, so the asymmetry keeps the accusing direction to spans the tree can anchor.
40
+
41
+ ## What counts as a claim
42
+
43
+ Only `## Key Changes` is read. `## Technical Context` legitimately names files a branch never touched, such as an install stamp inside a target, so widening the read manufactures a finding out of every argument an author made for the change.
44
+
45
+ Inside the section, the claim region of a bullet ends at its first comma outside a backticked span. That one lever was chosen by measurement. Over the 23 merged pull requests in this repository carrying the section, reading whole bullets reported 16 paths as claimed-but-untouched and every one was a file the body named for context. Cutting at the comma left 110 claims of the original 149 and took the false reports to 2. A list of sixteen clause-opening words tried beside it removed nothing the comma had not already removed, because this corpus punctuates every one of them.
46
+
47
+ A span inside the claim region has to survive all of these:
48
+
49
+ | Rule | What it keeps out |
50
+ | ------------------------------------------------- | ------------------------------------------------------ |
51
+ | No whitespace, `<`, `>`, `$`, `*`, `\|`, `?`, `^` | A backticked command, a placeholder, a glob, a pattern |
52
+ | No `://` and no leading `/`, `~`, `@`, `#`, `!` | A URL, an absolute path, a module alias |
53
+ | Contains `/` | A bare filename with no folder around it |
54
+ | Extension starts with a letter, or ends with `/` | A dotted number such as an address |
55
+ | Not a single top-level folder | `src/`, which nobody claims to have rewritten whole |
56
+ | A `file:line` span leads its bullet | A citation into a file the bullet is describing |
57
+ | The region carries no no-change marker | A bullet recording a file it deliberately left alone |
58
+
59
+ The last three rows earn their place from the corpus rather than from a rule. Every body that spelled a lone top-level folder was naming where something lives, and a line citation that follows another claim in the same bullet points into the file being described rather than naming a second one. A line citation leading its bullet stays a claim, which is how a body names the exact line it rewrote.
60
+
61
+ The no-change marker is the one rule the region cut cannot substitute for. A body writes "Leave `x` untouched, since the decision keeps it" to record a change it declined, and the path sits ahead of the first comma, so a stricter cut would not reach it and a looser one would find more. Since `keep` and `leave` each open a real claim often enough, the marker decides it rather than the leading verb. Three words carry the set: `untouched`, `unchanged`, and `as written`. `in place` was measured and dropped, because rewriting a file in place is an ordinary claim, and `no other line` was dropped because a correct bullet writes "as one insertion that touches no other line" about a change it did make. `alone` shipped in the set too, until review found every corpus occurrence sitting past the first comma, where the cut already excludes it, so the word voided no true claim there. Restrictive use is the more common one in this repository's own prose, and a comma-free bullet exposed it: "Move the threshold read into `src/gate/stages.ts` alone" asserts an edit, and the marker voided it while the word was still in the set. Over the 40-pull-request corpus the rule still voids no true claim.
62
+
63
+ A folder claim covers every changed file beneath it. A bare filename drops outright: resolved as a sibling of a path earlier in its bullet it produced seven wrong paths across this corpus against two right ones, because a compound bullet names a sibling folder as often as a sibling file.
64
+
65
+ ## Anchoring
66
+
67
+ A claim is anchored when its first segment names a folder the tree holds. The roots come from the tracked file list joined with the first segment of every changed path, and the second half is what admits a folder the branch created. Reading the tree alone would mark every claim under a new top-level directory unanchored, and an unanchored claim never accuses, so the first branch to open one would lose the graded direction with nothing reporting it.
68
+
69
+ ## Exit codes and refusals
70
+
71
+ | Code | Meaning |
72
+ | ---- | ------------------------------------------------- |
73
+ | `0` | every claimed path is in the diff |
74
+ | `1` | refused, with `reason` naming the cause |
75
+ | `2` | at least one claimed path is absent from the diff |
76
+
77
+ Branch on the record rather than on the exit code. A shell function wrapping `aitk` takes its status from whatever it runs last, so every non-zero exit can reach a caller as zero.
78
+
79
+ Three refusals separate a clean pass from a read that produced nothing:
80
+
81
+ - `no-section`. The body carries no such heading, so it claims nothing.
82
+ - `no-claims`. The heading is there and no span resolved, which is the extractor failing over prose rather than the body being wrong. Nothing is raised.
83
+ - `no-changes`. The pull request changed no files, so no claim has anything to answer.
84
+
85
+ An empty extraction read as a clean pass is the failure shape this repository has recorded twice, which is why the middle one is its own reason rather than a zero count.
86
+
87
+ `gh-missing`, `gh-failed`, `gh-truncated`, `unreadable-body`, `unreadable-tree`, `no-base`, `bad-base`, and `unreadable-changes` cover the reads that never reached a comparison.
88
+
89
+ ## What it was measured against
90
+
91
+ Driven over the 40 most recent merged pull requests in this repository, 23 of which carry the section, the shipped verb reported zero unmet paths and one `no-claims` refusal. The naive extractor it replaced reported 16.
92
+
93
+ The first body written after that corpus closed produced two, which is what the no-change marker and the open class above come from. One was the disclaiming bullet and the rule now covers it at no cost to the corpus. The other is the definition-site class, left open with the report naming it.
94
+
95
+ The bound is that the corpus is one repository writing to one house style. A project that punctuates differently loses claims to the comma cut rather than gaining false ones, since every rule above drops rather than invents, so the check degrades toward silence rather than toward noise.
96
+
97
+ What it cannot see is a bullet claiming a change to a file the branch did touch for another reason, a path written without backticks, and a claim in a second coordinated clause after the comma. The last of those falls to the ungraded direction rather than out of the reading.
98
+
99
+ One class stays open and is named rather than closed. A bullet can cite where something is defined while claiming an edit somewhere else, as `#1274` does with "Name the slug transform in `standards/slug.md` inline in the same step". The path is a definition site, the edit target is the step, and separating the two needs the sentence parsed rather than cut. A finding on such a bullet is this class rather than a stale claim, and a reader weighing an `unmet` path checks whether the bullet's real target is a locative the path does not name.
100
+
101
+ ## Where it runs
102
+
103
+ `claude-pr-review` Step 3 calls it and files an `unmet` path as a `should-fix` finding under the `**PR body**` block the stale ticked box already takes, since what both corrupt is the merge record rather than a file in the diff. A body is edited between pushes, so a finding names the head the comparison ran at.
@@ -126,9 +126,9 @@ The condition on that was something identifying a finite verb rather than guessi
126
126
 
127
127
  Exit codes are `0` for a completed run with no gating finding, `1` for a refusal, `2` for a ban hit, and `3` for a shipped ban set that arrived empty. A banned character, word, or spelling fails the run. Bullet, paragraph, and depth weight are judgments a reader settles, and cadence is a distribution whose healthy range moves with the surface, so all four report under every code.
128
128
 
129
- `3` is separate from `1` because the two want different responses from a caller. A refusal means no corpus was built, and the `Markdown bans` stage in `scripts/core/verify.sh` is right to warn and skip. An empty set means the corpus was walked and nothing was looked for, so that stage fails the push on `3` rather than skipping.
129
+ `3` is separate from `1` because the two want different responses from a caller. A refusal means no corpus was built, and the `Markdown bans` stage in `aitk gate run` is right to report it as unmeasured rather than as a pass. An empty set means the corpus was walked and nothing was looked for, so that stage fails the push on `3` rather than skipping.
130
130
 
131
- `2` rather than `1` for the gate keeps a measurement that succeeded and found something distinct from the audit declining to measure at all. A caller reading one as the other sends a reader hunting a defect that does not exist, which is the distinction `aitk context audit` and the `verify.sh` seed stage already draw between the same two codes.
131
+ `2` rather than `1` for the gate keeps a measurement that succeeded and found something distinct from the audit declining to measure at all. A caller reading one as the other sends a reader hunting a defect that does not exist, which is the distinction `aitk context audit` and the gate's own seed stage already draw between the same two codes.
132
132
 
133
133
  A banned character is a fact rather than a judgment, which is the test that admits it to a gate. What held it back was that gating on day one against a corpus never checked mechanically fails loudly on work nobody has had a chance to fix. The order was to land the verb reporting, measure the corpus once, fix what it finds, and turn the gate on as its own change, and the gate is the last of the four.
134
134
 
@@ -148,7 +148,7 @@ A hit the closed set cannot separate from correct prose is the case with no thir
148
148
 
149
149
  ### Where the rules are enforced
150
150
 
151
- Four surfaces apply the ban sets and three of them go through this verb. `.claude/hooks/standards-audit.sh` runs it against a single file after each markdown edit, the seed copy a project installs does the same, and the `Markdown bans` stage in `scripts/core/verify.sh` runs it across the whole corpus before a push. Each hook parsed its own copy of the word bans in awk before that, which left a British spelling passing at edit time and failing the push with nothing in between explaining the difference.
151
+ Four surfaces apply the ban sets and three of them go through this verb. `.claude/hooks/standards-audit.sh` runs it against a single file after each markdown edit, the seed copy a project installs does the same, and the `Markdown bans` stage in `aitk gate run` runs it across the whole corpus before a push. Each hook parsed its own copy of the word bans in awk before that, which left a British spelling passing at edit time and failing the push with nothing in between explaining the difference.
152
152
 
153
153
  The seed copy moved onto the verb when the sets became data, since its awk had nothing left to parse. It resolves one runner where the toolkit copy resolves two, looking for no checkout source, and a machine carrying no `aitk` gets a report naming the binary to install rather than a silent pass. `scripts/core/check-seed-independence.sh` scopes its walk to markdown and leaves the seed hooks outside it, which its own comment records as deliberate.
154
154
 
@@ -59,7 +59,7 @@ The exemption travels with the line rather than sitting in a path list away from
59
59
 
60
60
  Exit codes are `0` when the shipped tree carries no credential-shaped value, `1` for a refusal, and `2` for at least one value found.
61
61
 
62
- This is the one entry in `aitk audits run` that gates without a `verify.sh` stage behind it. A credential in the published tree is a fact rather than a judgment, which is the test the catalog asks any gating addition to pass, and the architecture record already ranks content leaving the repository above content that stays.
62
+ This is the one entry in `aitk audits run` that gates without an `aitk gate run` stage behind it. A credential in the published tree is a fact rather than a judgment, which is the test the catalog asks any gating addition to pass, and the architecture record already ranks content leaving the repository above content that stays.
63
63
 
64
64
  A refusal is never a clean tree. Five reasons produce one, and each exits `1`, because zero findings over zero files reads in the report exactly like zero findings over the whole shipped tree.
65
65
 
@@ -73,6 +73,14 @@ They collide only if the worker also runs a deep pass. Keep the worker's review
73
73
  light and let the orchestrator own the deep, independent one. The human read at
74
74
  merge is the final gate. No layer repeats another.
75
75
 
76
+ The orchestrator's pass also reads the pull request body against itself, which is
77
+ a vantage the worker never has. This repository squash-merges, so the body
78
+ becomes the commit message and the record on the trunk once the branch is gone.
79
+ `aitk pr key-changes` compares the paths the body's Key Changes claims against
80
+ its own changed-file list, and a claim the diff does not carry is a finding on
81
+ the body rather than on a file. See
82
+ [Key Changes bijection](agents/key-changes.md).
83
+
76
84
  ## The review channel
77
85
 
78
86
  Findings travel on the PR. `claude-pr-review` posts them there.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@erclx/aitk",
3
3
  "type": "module",
4
- "version": "3.52.0",
4
+ "version": "3.53.0",
5
5
  "description": "Infrastructure and quality tooling for developer workflows",
6
6
  "license": "MIT",
7
7
  "bin": {
@@ -34,8 +34,8 @@
34
34
  "check:install": "./scripts/core/install-check.sh",
35
35
  "test": "bun --bun vitest run",
36
36
  "prepare": "husky",
37
- "check": "./scripts/core/verify.sh",
38
- "check:ci": "VERIFY_WRITE=false ./scripts/core/verify.sh --all",
37
+ "check": "bun src/cli.ts gate run",
38
+ "check:ci": "bun src/cli.ts gate run --all --no-write",
39
39
  "update": "./scripts/core/update.sh",
40
40
  "clean": "./scripts/core/clean.sh",
41
41
  "snapshot": "./scripts/core/snapshot.sh",
@@ -0,0 +1,18 @@
1
+ #!/usr/bin/env bash
2
+ # Prints every seed root carrying a `.claude/`, one per line and relative to
3
+ # the project root.
4
+ #
5
+ # The discovery rule has one definition, `collect_seed_roots` in
6
+ # `scripts/lib/tooling.sh`, which `check-seed-independence.sh` already reads.
7
+ # This file is the route a caller outside bash takes to that same answer, so a
8
+ # stack seeding `.claude/` later reaches both readers and the two stages
9
+ # measuring seed content cannot disagree about which roots exist.
10
+ set -e
11
+ set -o pipefail
12
+
13
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
14
+ PROJECT_ROOT="${PROJECT_ROOT:-$(cd "$SCRIPT_DIR/../.." && pwd)}"
15
+
16
+ source "$PROJECT_ROOT/scripts/lib/tooling.sh"
17
+
18
+ collect_seed_roots
@@ -0,0 +1,19 @@
1
+ #!/usr/bin/env bash
2
+ # Repairs `core.bare`, which Claude Code's worktree entry leaves set in the
3
+ # shared config with nothing restoring it. The flag strands the main worktree
4
+ # and breaks the git reads that scope a verification run, so this runs ahead of
5
+ # every stage rather than as one of them.
6
+ #
7
+ # The rule itself lives in `scripts/lib/worktree.sh`, the one bash function in
8
+ # this repository under test, so this file is the invocation and never a second
9
+ # copy of the guard that spares a genuinely bare repository.
10
+ set -e
11
+ set -o pipefail
12
+
13
+ SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
14
+ PROJECT_ROOT="${PROJECT_ROOT:-$(cd "$SCRIPT_DIR/../.." && pwd)}"
15
+
16
+ source "$PROJECT_ROOT/scripts/lib/ui.sh"
17
+ source "$PROJECT_ROOT/scripts/lib/worktree.sh"
18
+
19
+ repair_bare_flag "$PROJECT_ROOT"
@@ -21,11 +21,11 @@ main() {
21
21
  bun update --interactive
22
22
 
23
23
  log_step "Verifying project health"
24
- if [ -f "$SCRIPT_DIR/verify.sh" ]; then
25
- VERIFY_NESTED=true "$SCRIPT_DIR/verify.sh"
24
+ if [ -f "$PROJECT_ROOT/src/cli.ts" ]; then
25
+ bun "$PROJECT_ROOT/src/cli.ts" gate run --nested
26
26
  log_info "All checks passed"
27
27
  else
28
- log_warn "Verification script not found, skipping."
28
+ log_warn "Verification entry point not found, skipping."
29
29
  fi
30
30
 
31
31
  echo -e "${GREY}└${NC}\n"
@@ -469,8 +469,8 @@ const RECORD_KINDS: readonly (readonly [string, Corpus])[] = [
469
469
  /**
470
470
  * Every audit the aggregate runs.
471
471
  *
472
- * `context`, `markdown`, and `skills` gate because `scripts/core/verify.sh`
473
- * already fails a push on each. `secrets` is the one entry that gates without
472
+ * `context`, `markdown`, and `skills` gate because the merge gate in
473
+ * `src/gate/stages.ts` already fails a push on each. `secrets` is the one entry that gates without
474
474
  * a stage behind it, added deliberately rather than as a side effect, since a
475
475
  * credential in the published tree is a fact and the split this repository
476
476
  * records gates a fact and reports a judgment. Weigh any further addition
package/src/audits/run.ts CHANGED
@@ -32,7 +32,8 @@ export type Spawn = (spec: AuditSpec) => Promise<SpawnResult>
32
32
  * Runs each verb out of the checkout this CLI is executing from.
33
33
  *
34
34
  * `process.execPath` and the resolved `cli.ts` rather than a bare `aitk`, for
35
- * the reason `verify.sh` already names: a globally installed binary resolves to
35
+ * the reason `cliRunner` in `src/gate/sequencer.ts` already names: a globally
36
+ * installed binary resolves to
36
37
  * the main checkout no matter which worktree is running, so the aggregate would
37
38
  * measure the wrong tree and report a pass over a branch it never read.
38
39
  *
package/src/cli.ts CHANGED
@@ -30,10 +30,12 @@ import { register as records } from '@/commands/records'
30
30
  import { register as sessions } from '@/commands/sessions'
31
31
  import { register as worktrees } from '@/commands/worktrees'
32
32
  import { register as audits } from '@/commands/audits'
33
+ import { register as gate } from '@/commands/gate'
33
34
  import { register as secrets } from '@/commands/secrets'
34
35
  import { register as deps } from '@/commands/deps'
35
36
  import { register as labels } from '@/commands/labels'
36
37
  import { register as autoship } from '@/commands/autoship'
38
+ import { register as pr } from '@/commands/pr'
37
39
  import { register as census } from '@/commands/census'
38
40
  import { register as targets } from '@/commands/targets'
39
41
  import { register as upgrade } from '@/commands/upgrade'
@@ -82,8 +84,10 @@ function showHelp(): void {
82
84
  `${GREY}│${NC} deps [cmd] ${GREY}# Read the resolved dependency set for advisories (audit)${NC}`,
83
85
  `${GREY}│${NC} labels [cmd] ${GREY}# Read a changed set against the pull request label map (audit)${NC}`,
84
86
  `${GREY}│${NC} autoship [cmd] ${GREY}# Decide whether a changed set needs the review pass (classify)${NC}`,
87
+ `${GREY}│${NC} pr [cmd] ${GREY}# Read a pull request body against its own diff (key-changes)${NC}`,
85
88
  `${GREY}│${NC} census [path] ${GREY}# Report tracked file count, extension breakdown, and line totals${NC}`,
86
89
  `${GREY}│${NC} audits [cmd] ${GREY}# Run every health check as one set (run, list)${NC}`,
90
+ `${GREY}│${NC} gate [cmd] ${GREY}# Run the merge gate stage by stage (run)${NC}`,
87
91
  `${GREY}│${NC} upgrade ${GREY}# Reinstall the CLI globally with the manager that installed it${NC}`,
88
92
  `${GREY}│${NC}`,
89
93
  `${GREY}│${NC} ${WHITE}Sandbox:${NC}`,
@@ -132,6 +136,7 @@ function showHelp(): void {
132
136
  `${GREY}│${NC} aitk labels audit --json`,
133
137
  `${GREY}│${NC} aitk census --json`,
134
138
  `${GREY}│${NC} aitk audits run --json`,
139
+ `${GREY}│${NC} aitk gate run --all --no-write`,
135
140
  `${GREY}│${NC} aitk upgrade --json`,
136
141
  `${GREY}└${NC}`,
137
142
  ]
@@ -184,8 +189,10 @@ secrets(program)
184
189
  deps(program)
185
190
  labels(program)
186
191
  autoship(program)
192
+ pr(program)
187
193
  census(program)
188
194
  audits(program)
195
+ gate(program)
189
196
  upgrade(program)
190
197
 
191
198
  program.parse()
@@ -1136,8 +1136,8 @@ function refuseAudit(
1136
1136
  /**
1137
1137
  * Prints nothing when every skill carries a requirement.
1138
1138
  *
1139
- * `--requirements-only` is what `verify.sh` runs on every push, and that script
1140
- * pipes a stage's whole output into its own frame. A passing gate that printed
1139
+ * `--requirements-only` is what the merge gate runs on every push, and it pipes
1140
+ * a stage's whole output into its own frame. A passing gate that printed
1141
1141
  * its frame would nest one inside the other on every contributor's push.
1142
1142
  */
1143
1143
  function reportRequirementGate(report: SkillsAudit): void {
@@ -349,8 +349,8 @@ function refuse(
349
349
  /**
350
350
  * Prints nothing when every path resolves.
351
351
  *
352
- * `--citations-only` is what `verify.sh` runs on every push, and that script
353
- * pipes a stage's whole output into its own frame. A passing gate that printed
352
+ * `--citations-only` is what the merge gate runs on every push, and it pipes
353
+ * a stage's whole output into its own frame. A passing gate that printed
354
354
  * its frame would nest one inside the other on every contributor's push.
355
355
  */
356
356
  function reportGate(report: ScannedCitations): void {