@erclx/aitk 3.51.1 → 3.52.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.
@@ -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.51.1",
4
+ "version": "3.52.1",
5
5
  "author": {
6
6
  "name": "Eric Le",
7
7
  "url": "https://github.com/erclx"
@@ -5,7 +5,12 @@
5
5
  # holds. This answers "what just changed" on a loop of its own, and it exists
6
6
  # because the two readings a dispatch needs are not one reading: a worker that
7
7
  # finishes goes idle and a worker that crashes vanishes, so a watch matching
8
- # only the pull request list stays silent through the second.
8
+ # only the pull request list stays silent through the second. A third case is
9
+ # neither: a worker that stops on a question or a prompt neither finishes nor
10
+ # crashes, and sits reading "waiting" beside the two statuses that resolve on
11
+ # their own until something reports it, which is the WORKER-STOPPED line
12
+ # below, or WORKER-UNMEASURABLE for a "waiting" row too old to carry a stamp
13
+ # this reads.
9
14
  #
10
15
  # `set -e` is deliberately not set. This runs for hours, and one transient `gh`
11
16
  # failure aborting the loop is a silent stop rather than a reported one. Every
@@ -17,6 +22,28 @@ set -uo pipefail
17
22
  # in a project whose workers run long.
18
23
  INTERVAL=60
19
24
 
25
+ # Seconds a worker can sit in "waiting" before this reports it as stopped
26
+ # rather than folding it into the ordinary status-change lines below. "busy"
27
+ # and "idle" resolve on their own; "waiting" does not, so a dwell that keeps
28
+ # growing there is a session blocked on something outside itself.
29
+ #
30
+ # Measured against the live session registry: 341 usable records carried a
31
+ # status at all, and exactly one carried "waiting", too sparse a sample to fit
32
+ # a distribution. The number is picked from the two bounds the measurement can
33
+ # still name rather than from a round guess: well above INTERVAL, so a handful
34
+ # of passes confirm the row before it reports rather than one slow tool call
35
+ # tripping it, and well inside the ten-to-thirty-minute span a dispatched build
36
+ # ordinarily runs, so a real stall is caught with most of that window still
37
+ # open to act on it.
38
+ #
39
+ # That one record carried `waitingFor: "approve Bash"`, a permission prompt
40
+ # rather than a question, and the two clear on different schedules: a prompt
41
+ # resolves the moment a person approves it, where a question can sit
42
+ # legitimately while a controller finishes a turn. This number is defensible
43
+ # for the second and generous for the first, and the gap stays open rather
44
+ # than splitting the constant on a sample of one.
45
+ STALL_THRESHOLD_S=300
46
+
20
47
  # Resolving the main worktree root rather than this file's folder keeps a watch
21
48
  # started from a linked worktree reading the same repository as one started from
22
49
  # main. The repository field on a roster row is that root's `.git`.
@@ -40,13 +67,19 @@ BASE_BRANCH="${BASE_REF#origin/}"
40
67
  # worker, whoever launched it. The prototype matched the `orchestrator-` prefix
41
68
  # instead, which reads a dispatched worker and misses every hand-launched one,
42
69
  # and those are the ordinary shape whenever the operator is launching.
70
+ #
71
+ # The row goes out tab-separated and comes back read with IFS set to a tab. A
72
+ # name this client actually writes carries spaces, `Update session markdown
73
+ # and check runnable commands (3)` among them, and splitting on whitespace
74
+ # took that one apart into a wrong name, branch, status, and dwell.
43
75
  read_workers() {
44
76
  aitk sessions list --json 2>/dev/null | tail -1 | jq -r \
45
77
  --arg repository "$REPOSITORY" --arg base "$BASE_BRANCH" '
46
78
  .sessions[]?
47
79
  | select(.repository == $repository)
48
80
  | select(.branch != null and .branch != $base)
49
- | "\(.name) \(.branch) \(.status)"
81
+ | [.name, .branch, .status, (.statusDwellMs // -1 | tostring)]
82
+ | @tsv
50
83
  ' 2>/dev/null | sort
51
84
  }
52
85
 
@@ -70,6 +103,15 @@ prev_names=""
70
103
  pulls_seen=0
71
104
  workers_seen=0
72
105
 
106
+ # One row per worker name currently reported, waiting past STALL_THRESHOLD_S
107
+ # or waiting with no stamp to measure, so a reader watching the log is told
108
+ # once rather than on every remaining pass. The prototype for that shape is
109
+ # the WORKER-GONE/WORKER pair above, tracked in the same state a status change
110
+ # is: a name drops out the pass it stops reading "waiting", which lets the
111
+ # same worker report a second stall later in its life rather than being
112
+ # marked forever by its first one.
113
+ declare -A stalled
114
+
73
115
  while true; do
74
116
  pulls="$(read_pulls)"
75
117
  pulls_read=$?
@@ -83,7 +125,7 @@ while true; do
83
125
  echo "watch: the session roster failed to load, so no worker is classified this pass"
84
126
  fi
85
127
 
86
- names="$(printf '%s\n' "$workers" | awk 'NF {print $1}' | sort -u)"
128
+ names="$(printf '%s\n' "$workers" | awk -F'\t' 'NF {print $1}' | sort -u)"
87
129
 
88
130
  if [ "$pulls_seen" -eq 1 ] && [ "$pulls_read" -eq 0 ]; then
89
131
  comm -13 <(printf '%s\n' "$prev_pulls") <(printf '%s\n' "$pulls") | grep . || true
@@ -96,6 +138,35 @@ while true; do
96
138
  grep . | sed 's/^/WORKER-GONE /' || true
97
139
  fi
98
140
 
141
+ if [ "$workers_read" -eq 0 ]; then
142
+ while IFS=$'\t' read -r w_name w_branch w_status w_dwell_ms; do
143
+ [ -z "$w_name" ] && continue
144
+
145
+ if [ "$w_status" != "waiting" ]; then
146
+ unset "stalled[$w_name]"
147
+ continue
148
+ fi
149
+
150
+ if [ "$w_dwell_ms" = "-1" ]; then
151
+ if [ -z "${stalled[$w_name]:-}" ]; then
152
+ echo "WORKER-UNMEASURABLE $w_name $w_branch"
153
+ stalled[$w_name]=1
154
+ fi
155
+ continue
156
+ fi
157
+
158
+ w_dwell_s=$((w_dwell_ms / 1000))
159
+ if [ "$w_dwell_s" -ge "$STALL_THRESHOLD_S" ]; then
160
+ if [ -z "${stalled[$w_name]:-}" ]; then
161
+ echo "WORKER-STOPPED $w_name $w_branch ${w_dwell_s}s"
162
+ stalled[$w_name]=1
163
+ fi
164
+ else
165
+ unset "stalled[$w_name]"
166
+ fi
167
+ done <<<"$workers"
168
+ fi
169
+
99
170
  if [ "$pulls_read" -eq 0 ]; then
100
171
  prev_pulls="$pulls"
101
172
  pulls_seen=1
@@ -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
 
@@ -64,6 +64,7 @@ Full help: `aitk <command> --help`. Behavior notes for the install and sync verb
64
64
  | `aitk census [path]` | Report tracked file count, a breakdown by extension, and a line total that skips whatever reads as binary (`--json`) |
65
65
  | `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
66
  | `aitk audits list` | List every audit the set runs, with the corpus each reads and whether it gates (`--json`) |
67
+ | `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
68
  | `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
69
  | `aitk capture [source]` | Render HTML capture sources to PNG, toolkit-only and absent from an installed package |
69
70
  | `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 +117,7 @@ Each domain exposes a consistent shape where applicable: `list`, `install`, `syn
116
117
  | `labels` | `audit` |
117
118
  | `autoship` | `classify` |
118
119
  | `audits` | `run`, `list` |
120
+ | `gate` | `run` |
119
121
 
120
122
  Common patterns:
121
123
 
@@ -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,6 +18,7 @@ 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
@@ -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
 
@@ -105,6 +105,14 @@ Every report states how liveness was decided, on a pass as well as a failure.
105
105
 
106
106
  The registry holds one record per session and is never pruned, so it accumulates thousands of entries. On the `unverified` path a stale record whose pid has been reused reads as live, which is why the field is reported rather than assumed.
107
107
 
108
+ ## The status dwell
109
+
110
+ Every row carries `statusUpdatedAt`, the stamp a client writes beside `status` at the moment it last changed, and `statusDwellMs`, the elapsed milliseconds since that stamp. Measured over the live registry, 23 of 341 usable records carry `statusUpdatedAt`, so `null` is the ordinary answer rather than an edge case, and the absence tracks a client version rather than a record's age alone: the one record ever measured carrying `status: "waiting"` is among the 318 without it.
111
+
112
+ `statusDwellMs` falls back to the coarser `updatedAt` stamp when `statusUpdatedAt` is absent, so it is `null` only where a record carries neither. `statusUpdatedAt` itself is never backfilled from the fallback and stays `null` in that case, since it names the exact stamp rather than an estimate. A stamp ahead of the reading clock clamps the dwell to zero rather than reporting a negative one.
113
+
114
+ The dwell is what separates a status that resolves on its own from one that does not. `busy` and `idle` transition without help, so a long dwell there is ordinary. `waiting` does not: a session in that state is blocked on something outside itself, and a dwell that keeps growing past the ordinary span of a prompt is a session stalled rather than paused. `aitk sessions list` renders the dwell beside the status at the coarsest unit that keeps it a whole number, and the JSON record carries both fields on every row.
115
+
108
116
  ## What the read depends on
109
117
 
110
118
  The records live under the Claude Code configuration directory, which the verb resolves from `CLAUDE_CONFIG_DIR` and falls back to `~/.claude`. Their location, their filenames, and the fields inside them are a client implementation detail rather than a published interface, so a client change can move them. The verb reports an absent registry as a refusal rather than as a machine running no sessions, which is what surfaces the move instead of burying it in an empty roster.
@@ -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
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@erclx/aitk",
3
3
  "type": "module",
4
- "version": "3.51.1",
4
+ "version": "3.52.1",
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,6 +30,7 @@ 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'
@@ -84,6 +85,7 @@ function showHelp(): void {
84
85
  `${GREY}│${NC} autoship [cmd] ${GREY}# Decide whether a changed set needs the review pass (classify)${NC}`,
85
86
  `${GREY}│${NC} census [path] ${GREY}# Report tracked file count, extension breakdown, and line totals${NC}`,
86
87
  `${GREY}│${NC} audits [cmd] ${GREY}# Run every health check as one set (run, list)${NC}`,
88
+ `${GREY}│${NC} gate [cmd] ${GREY}# Run the merge gate stage by stage (run)${NC}`,
87
89
  `${GREY}│${NC} upgrade ${GREY}# Reinstall the CLI globally with the manager that installed it${NC}`,
88
90
  `${GREY}│${NC}`,
89
91
  `${GREY}│${NC} ${WHITE}Sandbox:${NC}`,
@@ -132,6 +134,7 @@ function showHelp(): void {
132
134
  `${GREY}│${NC} aitk labels audit --json`,
133
135
  `${GREY}│${NC} aitk census --json`,
134
136
  `${GREY}│${NC} aitk audits run --json`,
137
+ `${GREY}│${NC} aitk gate run --all --no-write`,
135
138
  `${GREY}│${NC} aitk upgrade --json`,
136
139
  `${GREY}└${NC}`,
137
140
  ]
@@ -186,6 +189,7 @@ labels(program)
186
189
  autoship(program)
187
190
  census(program)
188
191
  audits(program)
192
+ gate(program)
189
193
  upgrade(program)
190
194
 
191
195
  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 {