@erclx/aitk 0.51.0 → 0.52.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": "0.51.0",
4
+ "version": "0.52.0",
5
5
  "author": {
6
6
  "name": "Eric Le",
7
7
  "url": "https://github.com/erclx"
@@ -13,7 +13,9 @@ The required-section check reports what does not declare `## Overview` and `## L
13
13
 
14
14
  Which unit answers depends on the folder. Entries of the folder named under `.claude/` are one domain each, so each answers for itself and a finding names the entry. Entries of a folder a domain split into describe that one domain between them, so any sibling answers and a finding names the folder. Holding a split folder to the rule per file would report every child beside its `overview.md`, and rolling the named folder up would let one conforming entry stand in for every other domain sitting next to it.
15
15
 
16
- It reports rather than gates, the closer call because a missing section reads more like a fact than the other judgments. What settles it is that the standard sanctions omitting `## Layout` from a domain owning no paths in the repo, and no measure separates that from an entry that forgot it. A domain covering only external tools is that case, and it reports on every run. The JSON record carries the findings as `missingSections` and the list as `checkpoints.requiredSections`.
16
+ It reports rather than gates by default, the closer call because a missing section reads more like a fact than the other judgments. What settles it is that the standard sanctions omitting `## Layout` from a domain owning no paths in the repo, and no measure separates that from an entry that forgot it. A domain covering only external tools is that case, and it reports on every run. The JSON record carries the findings as `missingSections` and the list as `checkpoints.requiredSections`.
17
+
18
+ `--gate` promotes the finding to a failing exit code, which the seed stage runs and no other caller does. That mode needs an answer to the sanctioned omission above, so a file declaring `stub: true` in its frontmatter is dropped before the check and reported nowhere. Both are described in `context-audit.md`.
17
19
 
18
20
  ## Length and depth
19
21
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  title: Context audit
3
- description: Running the audit, its flags and folder scope, the exit codes, and the citation gate that is the only failing one
3
+ description: Running the audit, its flags and folder scope, the exit codes, the citation gate, and the widened gate the seed stage runs
4
4
  ---
5
5
 
6
6
  # Context audit
@@ -13,6 +13,7 @@ aitk context audit --json
13
13
  aitk context audit --citations-only
14
14
  aitk context audit --folder context,diagrams
15
15
  aitk context audit --folder docs
16
+ aitk context audit tooling/base/seeds --gate
16
17
  ```
17
18
 
18
19
  | Option | Behavior |
@@ -20,6 +21,9 @@ aitk context audit --folder docs
20
21
  | `--json` | Add a machine-readable record on stdout, keeping the frame |
21
22
  | `--folder <list>` | Comma-separated folder names (default: the three below) |
22
23
  | `--citations-only` | Run the gating citation check alone, printing nothing when it passes |
24
+ | `--gate` | Fail on a missing required section or index drift as well |
25
+
26
+ `--citations-only` and `--gate` refuse together. The first runs the citation check alone, so the two findings the second adds are never measured, and a run honoring both would exit clean on a file short a required section.
23
27
 
24
28
  ## Folder scope
25
29
 
@@ -31,7 +35,36 @@ A run where no requested name resolves refuses, whichever list it read. Naming t
31
35
 
32
36
  ## Exit codes
33
37
 
34
- Exit codes are `0` for a clean run, `1` for a refusal, and `2` for an unresolved citation. Only the citation check sets a failing code. Required-section, length, depth, bullet weight, table, provenance, and index findings print and return `0`, because each is a judgment and failing a push on one would make the check something to route around.
38
+ Exit codes are `0` for a clean run, `1` for a refusal, and `2` for a gating finding. An unresolved citation gates under every mode. Length, depth, bullet weight, table, and provenance findings print and return `0` under every mode, because each is a judgment and failing a push on one would make the check something to route around.
39
+
40
+ Required-section and index findings sit between the two. Both are answerable from the file rather than weighed, so `--gate` promotes them to failing codes while a bare run leaves them advisory. The toolkit runs the bare form against itself and the widened form against the seed tree, described below.
41
+
42
+ ## The seed gate
43
+
44
+ `bun run check` runs `--gate` against every `tooling/<stack>/seeds/` directory carrying a `.claude/` folder, discovered per run so a new stack is covered without a script edit. The seed tree installs into every scaffolded project, so a seed breaking the standard it seeds teaches the wrong shape to each one, and no rule path reaches the tree to report it.
45
+
46
+ The widened gate is correct here and wrong at the project root. A seed is authored once and read by every target, while a context entry in a live project is edited under time pressure by the people who own it. A missing section in the first is a defect shipping outward, and in the second it is a threshold worth reporting and not worth blocking a push over.
47
+
48
+ Coverage follows the index-plus-entry contract, so it reaches seeded entries and the indexes beside them. Seed files sitting directly under `.claude/`, currently `ARCHITECTURE.md`, `DESIGN.md`, and `REQUIREMENTS.md`, belong to no audited folder and stay outside it.
49
+
50
+ The stage prints the entries it measured per root and warns on a root that measured none. A root can resolve an audited folder and hold no entry in it, which `tooling/claude/seeds` does today, so a single pass line over the set would report coverage of a tree nothing opened.
51
+
52
+ It reads the exit code rather than pass against fail, since the two failing codes mean opposite things. A root carrying a `.claude/` that resolves no audited folder refuses at 1 and warns, while a seed short a required section gates at 2 and fails the push. A new stack seeding `.claude/` alone therefore reports what it is rather than a violation it does not have.
53
+
54
+ ### Exempting a skeleton
55
+
56
+ A seed that deliberately omits a required section sets `stub: true` in its frontmatter, and the section check then skips it. Every other measure still reads the file, since a stub is exempt from owing sections rather than from being well formed.
57
+
58
+ ```markdown
59
+ ---
60
+ title: Architecture
61
+ stub: true
62
+ ---
63
+ ```
64
+
65
+ Only `true` counts. A field holding anything else reads as a seed that meant to turn the exemption off, so a typo cannot silence the gate. Both install paths strip the field on the way into a target, which keeps toolkit bookkeeping out of a project whose own tooling would never read it. A block holding nothing else is dropped whole.
66
+
67
+ The exemption exists because the section check has a false-positive class. A standard may sanction omitting a section, and no measure separates that from a file that forgot it, which is why the finding stays advisory everywhere the widened gate is not running. No seed sets the field today.
35
68
 
36
69
  ## The citation gate
37
70
 
@@ -12,7 +12,7 @@ CLI catalog and invocation rules for agents, split by command domain. Start with
12
12
  - [Command catalog](commands.md): Every project-level command and every domain subcommand, plus the shape each domain exposes
13
13
  - [Comments](comments.md): Comment density by language and kind, the two structural exclusions, and how the degradation sweep finds its vocabulary
14
14
  - [Context audit checks](context-audit-checks.md): What each non-gating check reports, the unit each checkpoint is measured in, and which folders each check reaches
15
- - [Context audit](context-audit.md): Running the audit, its flags and folder scope, the exit codes, and the citation gate that is the only failing one
15
+ - [Context audit](context-audit.md): Running the audit, its flags and folder scope, the exit codes, the citation gate, and the widened gate the seed stage runs
16
16
  - [Docs](docs.md): How aitk docs resolves the toolkit's own reference surface from an install root, and how a split domain is named
17
17
  - [Indexes](indexes.md): Flags, exit codes, and JSON shape for aitk indexes regen, plus when it auto-stages what it rewrote
18
18
  - [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
@@ -144,6 +144,12 @@ needs a stamp and seeds carry none, so a file history cannot attribute stays
144
144
  `drifted`. Reconcile the section with `claude-seed-sync`, which merges one
145
145
  section at a time rather than replacing a file the project edits.
146
146
 
147
+ A markdown seed installs rewritten rather than copied, since the `stub: true`
148
+ marker the toolkit's own seed gate reads is stripped on the way in. The
149
+ comparison above runs against what the install would write, so a marked seed a
150
+ target never touched still reports `matching`. Every other seed copies byte for
151
+ byte.
152
+
147
153
  `superseded` names a file a newer seed folder replaced, such as `.claude/TASKS.md`
148
154
  against the `.claude/tasks/` that now ships. The entry carries `replacedBy` and
149
155
  nothing else, and the file is never deleted, since the content belongs to the
@@ -76,7 +76,7 @@ Run one orchestrator at a time. The board is gitignored, so a second session rea
76
76
 
77
77
  Before a handoff, the orchestrator checks the plan against the tree rather than reading it: grep each construct it names and count the sites, confirm every phase label it cites is still open, and open each file it describes. A plan goes stale from whatever merged after it was written, and reading cannot catch that.
78
78
 
79
- `.claude/plans/`, `.claude/review/`, and `.claude/memory/` all resolve at the main worktree root, so artifacts created in any session are visible from any sibling worktree. See [Claude Code and git worktrees](../wiki/claude-worktrees.md) for the full rule and the domain-level fan-out guidance.
79
+ `.claude/plans/`, `.claude/review/`, and `.claude/memory/` all resolve at the main worktree root, so artifacts created in any session are visible from any sibling worktree. See [Claude Code and git worktrees](../wiki/claude/claude-worktrees.md) for the full rule and the domain-level fan-out guidance.
80
80
 
81
81
  A plan that ships is archived, never deleted. `aitk:claude-docs` moves it to `.claude/plans-archive/` and retargets the task file's `Plan:` line at the new location, so a completed task still leads to the reasoning behind it. Both folders are gitignored, which is why a deleted plan had no recovery path. A plan cited by more than one task stays put until the last of them closes, since moving it early would strand every other pointer.
82
82
 
@@ -10,7 +10,7 @@ A way to build fast and reliably across parallel Claude Code sessions without a
10
10
  loop and without losing the human review gate. One warm session plans and
11
11
  reviews. Cold worker sessions build. The human launches workers and merges. This
12
12
  page covers the roles and the loop. For the worktree mechanism (isolation, merge
13
- order, port collisions), see [Claude Code and git worktrees](../wiki/claude-worktrees.md).
13
+ order, port collisions), see [Claude Code and git worktrees](../wiki/claude/claude-worktrees.md).
14
14
 
15
15
  ## Two roles
16
16
 
@@ -105,11 +105,11 @@ at two or three worker tracks and split them across the stack so they do not
105
105
  collide on the same files. Unit checks run freely in many worktrees at once.
106
106
  Only fixed-port work (a dev server, an end-to-end run, a screenshot) and
107
107
  singleton resources (one local model server, one GPU) serialize. See
108
- [Claude Code and git worktrees](../wiki/claude-worktrees.md) for merge order and the
108
+ [Claude Code and git worktrees](../wiki/claude/claude-worktrees.md) for merge order and the
109
109
  port-collision detail.
110
110
 
111
111
  ## Related
112
112
 
113
- - [Claude Code and git worktrees](../wiki/claude-worktrees.md) for the isolation and fan-out mechanics
114
- - [Claude Code subagents](../wiki/claude-subagents.md) for in-session parallelism without worktrees
113
+ - [Claude Code and git worktrees](../wiki/claude/claude-worktrees.md) for the isolation and fan-out mechanics
114
+ - [Claude Code subagents](../wiki/claude/claude-subagents.md) for in-session parallelism without worktrees
115
115
  - `.claude/context/claude-plugin/skill-strategy.md` for how the skills in the loop are categorized
@@ -8,9 +8,9 @@ category: Workflow
8
8
 
9
9
  Three tiers cover the range from prose-only design docs to a fully graphical design source of truth. Pick one per project based on how UI-heavy the work is, whether stakeholders review visuals, and whether a designer is involved. Tiers stack, so moving up does not invalidate work done at a lower tier.
10
10
 
11
- The tier framework sits alongside [Claude Design](../wiki/claude-design.md), [visual wireframes](../wiki/visual-wireframes.md), [community skills and plugins](../wiki/community-skills.md), and [community MCP servers](../wiki/community-mcp-servers.md). Those pages catalog the tooling. This page decides when to reach for what.
11
+ The tier framework sits alongside [Claude Design](../wiki/claude/claude-design.md), [visual wireframes](../wiki/tools/visual-wireframes.md), [community skills and plugins](../wiki/tools/community-skills.md), and [community MCP servers](../wiki/tools/community-mcp-servers.md). Those pages catalog the tooling. This page decides when to reach for what.
12
12
 
13
- Two tools anchor tier 1 and tier 2. [Stitch](../wiki/stitch.md) is the agent-addressable default through its MCP server at `stitch.googleapis.com/mcp`, with a free tier of 400 daily credits that covers daily iteration. [Claude Design](../wiki/claude-design.md), released 2026-04-17 and priced inside Claude subscriptions, is the ceiling tool reserved for codebase extraction and the richly annotated handoff bundle. Each covers a different job, they are not swappable.
13
+ Two tools anchor tier 1 and tier 2. [Stitch](../wiki/tools/stitch.md) is the agent-addressable default through its MCP server at `stitch.googleapis.com/mcp`, with a free tier of 400 daily credits that covers daily iteration. [Claude Design](../wiki/claude/claude-design.md), released 2026-04-17 and priced inside Claude subscriptions, is the ceiling tool reserved for codebase extraction and the richly annotated handoff bundle. Each covers a different job, they are not swappable.
14
14
 
15
15
  ## Tier 0: prose only
16
16
 
@@ -51,16 +51,16 @@ Impeccable, if installed, keeps its own root `DESIGN.md` and `PRODUCT.md` in the
51
51
 
52
52
  ### Tools
53
53
 
54
- - Stitch via MCP at `stitch.googleapis.com/mcp`. Default pick for agent-driven visual generation. Free tier of 400 daily credits. See [Stitch](../wiki/stitch.md).
55
- - Excalidraw canvas server on localhost plus the `yctimlin/mcp_excalidraw` MCP shim, for projects that need an agent to draw, read back, and revise a canvas. See [visual wireframes](../wiki/visual-wireframes.md) for setup and footguns.
56
- - Playwright MCP for browser-side verification. See [Playwright](../wiki/community-mcp-servers.md#playwright-microsoft).
57
- - Chrome DevTools MCP for live frontend debugging. See [Chrome DevTools](../wiki/community-mcp-servers.md#chrome-devtools-google).
58
- - Claude Design as the ceiling option for codebase extraction or polished handoff bundles. See [Claude Design](../wiki/claude-design.md).
54
+ - Stitch via MCP at `stitch.googleapis.com/mcp`. Default pick for agent-driven visual generation. Free tier of 400 daily credits. See [Stitch](../wiki/tools/stitch.md).
55
+ - Excalidraw canvas server on localhost plus the `yctimlin/mcp_excalidraw` MCP shim, for projects that need an agent to draw, read back, and revise a canvas. See [visual wireframes](../wiki/tools/visual-wireframes.md) for setup and footguns.
56
+ - Playwright MCP for browser-side verification. See [Playwright](../wiki/tools/community-mcp-servers.md#playwright-microsoft).
57
+ - Chrome DevTools MCP for live frontend debugging. See [Chrome DevTools](../wiki/tools/community-mcp-servers.md#chrome-devtools-google).
58
+ - Claude Design as the ceiling option for codebase extraction or polished handoff bundles. See [Claude Design](../wiki/claude/claude-design.md).
59
59
 
60
60
  ### Skills
61
61
 
62
62
  - Everything from tier 0
63
- - A frontend design skill to steer visual quality. Pick one of [Impeccable](../wiki/community-skills.md#pbakausimpeccable), [UI/UX Pro Max](../wiki/community-skills.md#nextlevelbuilderui-ux-pro-max-skill), or Anthropic's `frontend-design` plugin. Impeccable is the strongest default because of its curated anti-patterns.
63
+ - A frontend design skill to steer visual quality. Pick one of [Impeccable](../wiki/tools/community-skills.md#pbakausimpeccable), [UI/UX Pro Max](../wiki/tools/community-skills.md#nextlevelbuilderui-ux-pro-max-skill), or Anthropic's `frontend-design` plugin. Impeccable is the strongest default because of its curated anti-patterns.
64
64
 
65
65
  ### When to pick
66
66
 
@@ -91,9 +91,9 @@ Design happens in a graphical tool. `.claude/DESIGN.md` either regenerates from
91
91
 
92
92
  ### Tools
93
93
 
94
- - Figma desktop app with the [Figma Dev Mode MCP](../wiki/community-skills.md#figma-mcp-and-code-to-canvas) for teams with a dedicated designer already on Figma. Bidirectional sync and Code to Canvas capture.
95
- - Claude Design with its Claude Code handoff bundle for teams without an existing Figma investment and for solo founders or PMs driving design themselves. One-way handoff, no bidirectional sync. See [Claude Design](../wiki/claude-design.md).
96
- - Stitch via MCP as a low-cost complement to either, used for bulk screen generation driven by Claude Code. See [Stitch](../wiki/stitch.md).
94
+ - Figma desktop app with the [Figma Dev Mode MCP](../wiki/tools/community-skills.md#figma-mcp-and-code-to-canvas) for teams with a dedicated designer already on Figma. Bidirectional sync and Code to Canvas capture.
95
+ - Claude Design with its Claude Code handoff bundle for teams without an existing Figma investment and for solo founders or PMs driving design themselves. One-way handoff, no bidirectional sync. See [Claude Design](../wiki/claude/claude-design.md).
96
+ - Stitch via MCP as a low-cost complement to either, used for bulk screen generation driven by Claude Code. See [Stitch](../wiki/tools/stitch.md).
97
97
  - Playwright and Chrome DevTools MCPs as in tier 1
98
98
 
99
99
  ### Skills
@@ -128,9 +128,9 @@ Zero or one yes: tier 0. Two or three: tier 1. Four: tier 2. Resist over-tiering
128
128
 
129
129
  ## References
130
130
 
131
- - [Stitch](../wiki/stitch.md): Gemini-powered design product with the MCP server that anchors tier 1
132
- - [Claude Design](../wiki/claude-design.md): first-party hosted design product and handoff bundle
131
+ - [Stitch](../wiki/tools/stitch.md): Gemini-powered design product with the MCP server that anchors tier 1
132
+ - [Claude Design](../wiki/claude/claude-design.md): first-party hosted design product and handoff bundle
133
133
  - `.claude/context/claude-plugin/skill-strategy.md`: how to decide between workflow and domain-knowledge skills
134
- - [Visual wireframes](../wiki/visual-wireframes.md): Excalidraw research and setup for the tier 1 wireframe companion
135
- - [Community skills and plugins](../wiki/community-skills.md): catalog of frontend design skills and integrations
136
- - [Community MCP servers](../wiki/community-mcp-servers.md): catalog of MCPs referenced across all tiers
134
+ - [Visual wireframes](../wiki/tools/visual-wireframes.md): Excalidraw research and setup for the tier 1 wireframe companion
135
+ - [Community skills and plugins](../wiki/tools/community-skills.md): catalog of frontend design skills and integrations
136
+ - [Community MCP servers](../wiki/tools/community-mcp-servers.md): catalog of MCPs referenced across all tiers
@@ -56,7 +56,7 @@ Use `clp` in any other repository where you want the toolkit skills available. W
56
56
 
57
57
  Use `cls` or `clps` to save Opus usage on routine sessions. Switch mid-session with `/model` to avoid restarting.
58
58
 
59
- Use `clw <name>` for features that will take more than one session. A worktree isolates the branch, the transcripts, and the `/resume` history. See [Claude Code and git worktrees](../wiki/claude-worktrees.md) for fan-out rules.
59
+ Use `clw <name>` for features that will take more than one session. A worktree isolates the branch, the transcripts, and the `/resume` history. See [Claude Code and git worktrees](../wiki/claude/claude-worktrees.md) for fan-out rules.
60
60
 
61
61
  Use `clc` to resume the last session without a picker. Use `clr` when you have several sessions and need to pick by name or recency. Outside the toolkit repo, `clpc` is the same shortcut as `clc` with the plugin loaded.
62
62
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@erclx/aitk",
3
3
  "type": "module",
4
- "version": "0.51.0",
4
+ "version": "0.52.0",
5
5
  "description": "Infrastructure and quality tooling for developer workflows",
6
6
  "license": "MIT",
7
7
  "bin": {
@@ -116,6 +116,28 @@ assert_hero_pair() {
116
116
  [ "$html_commit" = "$png_commit" ]
117
117
  }
118
118
 
119
+ # Whatever stacks the repo currently carries, so a new one is covered without an
120
+ # edit here. A seed root holding no `.claude/` seeds nothing a standard governs.
121
+ collect_seed_roots() {
122
+ local dir
123
+ for dir in "$PROJECT_ROOT"/tooling/*/seeds; do
124
+ [ -d "$dir/.claude" ] || continue
125
+ printf '%s\n' "${dir#"$PROJECT_ROOT"/}"
126
+ done
127
+ }
128
+
129
+ # Entries the audit actually measured, summed across the folders it resolved.
130
+ # `--json` carries one `"entries":<n>` per folder object, and the top-level key
131
+ # of that name holds an array, so the numeric match reaches folders alone.
132
+ #
133
+ # A root can resolve a folder and measure nothing in it, which is a passing gate
134
+ # over an empty set. The stage prints this per root rather than reporting one
135
+ # verdict for every root, or a tree nobody measured reads as a tree that passed.
136
+ seed_entry_count() {
137
+ printf '%s' "$1" | grep -o '"entries":[0-9]\+' | grep -o '[0-9]\+' |
138
+ awk '{ total += $1 } END { print total + 0 }'
139
+ }
140
+
119
141
  assert_no_drift() {
120
142
  local paths=$1
121
143
  local err_msg=$2
@@ -183,6 +205,67 @@ main() {
183
205
  run_check "cd $PROJECT_ROOT && bun src/cli.ts context audit --citations-only" "A cited context path does not resolve. Run bun src/cli.ts context audit."
184
206
  log_info "Context citations resolve"
185
207
 
208
+ # The stage above audits this repository. Its seed tree ships into every
209
+ # scaffolded project, so a seed breaking the standard it seeds propagates
210
+ # instead of sitting still, and no rule path reaches the tree to report it.
211
+ # `--gate` fails on the two findings beside citations that are facts, a
212
+ # missing required section and index drift, and leaves the thresholds
213
+ # advisory for the reason the stage above leaves them so. A passing run stays
214
+ # silent because the audit prints a frame that would nest inside this one.
215
+ log_step "Seed standards"
216
+ local seed_roots seed_root seed_output seed_frame seed_entries seed_measured seed_status
217
+ seed_roots=$(collect_seed_roots)
218
+ if [ -z "$seed_roots" ]; then
219
+ log_info "Skipped, no seed root carries .claude/"
220
+ else
221
+ seed_measured=0
222
+ while IFS= read -r seed_root; do
223
+ # `--json` puts the record on stdout and the frame on stderr, so the
224
+ # passing run stays silent and the failing one is re-run for its frame
225
+ # rather than parsed out of a stream this script would have to strip.
226
+ seed_status=0
227
+ seed_output=$(cd "$PROJECT_ROOT" && bun src/cli.ts context audit "$seed_root" --gate --json 2>/dev/null) || seed_status=$?
228
+
229
+ # The audit separates 1 from 2 and they mean opposite things. 2 is a seed
230
+ # breaking the standard it seeds. 1 is the audit refusing, which a seed
231
+ # root carrying no audited folder produces, and reporting that as a
232
+ # violation sends a reader hunting one that does not exist. Discovery is
233
+ # what puts this in reach, since a new stack seeding `.claude/` alone
234
+ # arrives here with no edit to this script.
235
+ case $seed_status in
236
+ 0) ;;
237
+ 1)
238
+ log_warn "$seed_root: no audited folder under .claude/, nothing measured"
239
+ continue
240
+ ;;
241
+ *)
242
+ # `|| true` because the re-run exits non-zero by construction, and
243
+ # `set -e` would take the script down before log_error names the root.
244
+ seed_frame=$(cd "$PROJECT_ROOT" && bun src/cli.ts context audit "$seed_root" --gate 2>&1 || true)
245
+ echo "$seed_frame" | pipe_output
246
+ if [ "$seed_status" -eq 2 ]; then
247
+ log_error "A seed breaks the standard governing the folder it seeds: $seed_root"
248
+ else
249
+ log_error "The seed audit exited $seed_status against $seed_root, which is neither a pass nor a finding."
250
+ fi
251
+ ;;
252
+ esac
253
+
254
+ seed_entries=$(seed_entry_count "$seed_output")
255
+ seed_measured=$((seed_measured + seed_entries))
256
+
257
+ if [ "$seed_entries" -eq 0 ]; then
258
+ log_warn "$seed_root: no entry under an audited folder, nothing measured"
259
+ else
260
+ log_info "$seed_root: $seed_entries entries measured"
261
+ fi
262
+ done <<<"$seed_roots"
263
+
264
+ if [ "$seed_measured" -eq 0 ]; then
265
+ log_warn "No seed entry was measured. The stage covered nothing."
266
+ fi
267
+ fi
268
+
186
269
  # Presence of a required file is a fact, so it gates. The name, description,
187
270
  # folder, and requirement-section measures beside it report and are read from a
188
271
  # bare run. `bun src/cli.ts` for the reason the stage above uses it, and the
@@ -1,7 +1,8 @@
1
1
  import { existsSync } from 'node:fs'
2
- import { chmod } from 'node:fs/promises'
3
- import { join } from 'node:path'
2
+ import { chmod, mkdir, readFile, writeFile } from 'node:fs/promises'
3
+ import { dirname, join } from 'node:path'
4
4
  import { copyPreservingMode } from '@/copy'
5
+ import { rewritesOnInstall, stripSeedMarker } from '@/seed-marker'
5
6
 
6
7
  const SEEDS_DIR = join('tooling', 'claude', 'seeds')
7
8
  const CLAUDE_DIR = '.claude'
@@ -122,12 +123,25 @@ export function countByScope(seeds: readonly Seed[]): SeedCounts {
122
123
  /**
123
124
  * Copies each pending seed. Hooks get the executable bit the way `chmod +x`
124
125
  * granted it, added on top of whatever mode the destination already carried.
126
+ *
127
+ * A markdown seed is rewritten rather than copied, so the stub marker the seed
128
+ * gate reads does not reach the target. Every other seed copies byte for byte,
129
+ * which is what keeps the hook scripts and `settings.json` untouched.
125
130
  */
126
131
  export async function applySeeds(seeds: readonly Seed[]): Promise<string[]> {
127
132
  const applied: string[] = []
128
133
 
129
134
  for (const seed of seeds) {
130
- await copyPreservingMode(seed.src, seed.dest)
135
+ if (rewritesOnInstall(seed.src)) {
136
+ await mkdir(dirname(seed.dest), { recursive: true })
137
+ await writeFile(
138
+ seed.dest,
139
+ stripSeedMarker(await readFile(seed.src, 'utf8')),
140
+ )
141
+ } else {
142
+ await copyPreservingMode(seed.src, seed.dest)
143
+ }
144
+
131
145
  if (seed.executable) await chmod(seed.dest, 0o755)
132
146
  applied.push(seed.applyLabel)
133
147
  }
@@ -21,6 +21,7 @@ import {
21
21
  presentNames,
22
22
  resolveFolders,
23
23
  } from '@/context/folders'
24
+ import { isGating } from '@/context/gate'
24
25
  import { auditIndexes, type FolderDrift } from '@/context/index-drift'
25
26
  import {
26
27
  frameError,
@@ -34,8 +35,8 @@ import {
34
35
  plural,
35
36
  } from '@/ui'
36
37
 
37
- /** Returned when an unresolved citation is found, which is the gating check. */
38
- const EXIT_UNRESOLVED = 2
38
+ /** Returned when a gating finding is present. */
39
+ const EXIT_GATE = 2
39
40
 
40
41
  /** A name of dots alone is `.` or `..`, both of which escape the audit root. */
41
42
  const FOLDER_NAME = /^(?!\.+$)[A-Za-z0-9._-]+$/
@@ -44,6 +45,7 @@ interface AuditCommandOptions {
44
45
  readonly json?: boolean
45
46
  readonly folder?: string
46
47
  readonly citationsOnly?: boolean
48
+ readonly gate?: boolean
47
49
  }
48
50
 
49
51
  export function register(program: Command): void {
@@ -65,17 +67,23 @@ export function register(program: Command): void {
65
67
  'Comma-separated folder names, resolved under .claude/ then the project root',
66
68
  )
67
69
  .option('--citations-only', 'Run the gating citation check alone')
70
+ .option(
71
+ '--gate',
72
+ 'Also fail on a missing required section or index drift, the findings that are facts',
73
+ )
68
74
  .addHelpText(
69
75
  'after',
70
76
  [
71
77
  '',
72
78
  'Exit codes:',
73
- ' 0 the audit completed with every cited path resolving',
79
+ ' 0 the audit completed with no gating finding',
74
80
  ' 1 refused, with the reason on stderr',
75
- ' 2 a cited path did not resolve',
81
+ ' 2 a gating finding is present',
76
82
  '',
77
- 'Only unresolved citations set a failing exit code. Section, length,',
78
- 'depth, bullet, table, provenance, and index findings are advisory.',
83
+ 'An unresolved citation always gates. --gate widens the gate to the',
84
+ 'other two findings that are facts rather than judgments: a missing',
85
+ 'required section and index drift. Length, depth, bullet, table, and',
86
+ 'provenance findings are thresholds and stay advisory under both.',
79
87
  '',
80
88
  'Examples:',
81
89
  ' aitk context audit',
@@ -83,6 +91,7 @@ export function register(program: Command): void {
83
91
  ' aitk context audit --citations-only',
84
92
  ' aitk context audit --folder context,diagrams',
85
93
  ' aitk context audit --folder docs',
94
+ ' aitk context audit tooling/base/seeds --gate',
86
95
  '',
87
96
  ].join('\n'),
88
97
  )
@@ -118,6 +127,17 @@ async function runAudit(
118
127
  const root = resolve(path ?? process.cwd())
119
128
  const names = parseFolders(opts.folder)
120
129
  const gateOnly = opts.citationsOnly ?? false
130
+ const widened = opts.gate ?? false
131
+
132
+ // `--citations-only` runs the citation check alone, so the two findings
133
+ // `--gate` adds are never measured. Honouring both would exit 0 on a seed
134
+ // short a required section, which is the pass a gate exists to prevent.
135
+ if (gateOnly && widened) {
136
+ return refuse(
137
+ '--citations-only runs the citation check alone, so --gate would widen the gate to findings the run never measures. Pass one.',
138
+ gateOnly,
139
+ )
140
+ }
121
141
 
122
142
  if (typeof names === 'string') return refuse(names, gateOnly)
123
143
 
@@ -213,7 +233,14 @@ async function runAudit(
213
233
  )
214
234
  }
215
235
 
216
- return citations.unresolved.length > 0 ? EXIT_UNRESOLVED : 0
236
+ const gating = isGating({
237
+ unresolvedCitations: citations.unresolved.length,
238
+ sections,
239
+ drift,
240
+ widened,
241
+ })
242
+
243
+ return gating ? EXIT_GATE : 0
217
244
  }
218
245
 
219
246
  function refuse(message: string, gateOnly: boolean): number {
@@ -1,6 +1,7 @@
1
1
  import { readFile } from 'node:fs/promises'
2
2
  import { relative } from 'node:path'
3
3
  import type { AuditedFolder } from '@/context/folders'
4
+ import { isStubSeed } from '@/seed-marker'
4
5
 
5
6
  /** Checkpoints quoted from `standards/context.md`. Neither is a cap. */
6
7
  export const LENGTH_CHECKPOINT = 150
@@ -166,6 +167,12 @@ export interface EntryReport {
166
167
  * is `missingSections`, since one entry answers for its siblings.
167
168
  */
168
169
  readonly sections: readonly string[]
170
+ /**
171
+ * Whether the file declares itself a skeleton, which excludes it from the
172
+ * section check alone. Every other measure still reads it, since a stub is
173
+ * exempt from owing sections rather than from being well formed.
174
+ */
175
+ readonly stub: boolean
169
176
  }
170
177
 
171
178
  export interface SectionFinding {
@@ -544,6 +551,7 @@ export function measureEntry(
544
551
  provenance: governsContent ? provenance(lines) : [],
545
552
  heavyBullets: governsContent ? heavyBullets(lines) : [],
546
553
  sections: governsContent ? declaredSections(lines) : [],
554
+ stub: isStubSeed(source),
547
555
  }
548
556
  }
549
557
 
@@ -615,9 +623,15 @@ export function missingSections(
615
623
  for (const folder of folders) {
616
624
  if (!governsContent(folder) || folder.entries.length === 0) continue
617
625
 
626
+ // A stub owes no sections, so it is dropped before either branch rather
627
+ // than inside them. Leaving one in the split-folder aggregate would let a
628
+ // skeleton answer for the siblings that do owe the sections.
618
629
  const reports = folder.entries
619
630
  .map((path) => byRel.get(relative(root, path)))
620
631
  .filter((entry) => entry !== undefined)
632
+ .filter((entry) => !entry.stub)
633
+
634
+ if (reports.length === 0) continue
621
635
 
622
636
  if (folder.nested) {
623
637
  const missing = shortOf(reports.flatMap((entry) => entry.sections))
@@ -0,0 +1,43 @@
1
+ import type { SectionFinding } from '@/context/audit'
2
+ import type { FolderDrift } from '@/context/index-drift'
3
+
4
+ export interface GateInput {
5
+ /** Cited paths that resolved to nothing, which gate under either mode. */
6
+ readonly unresolvedCitations: number
7
+ readonly sections: readonly SectionFinding[]
8
+ readonly drift: readonly FolderDrift[]
9
+ /**
10
+ * Whether the caller asked for the widened gate. False leaves a missing
11
+ * section and a drifted index advisory, which is what the project-root stage
12
+ * runs so a judgment threshold never fails a push.
13
+ */
14
+ readonly widened: boolean
15
+ }
16
+
17
+ /** Whether any folder disagrees with its own index. */
18
+ export function hasDrift(drift: readonly FolderDrift[]): boolean {
19
+ return drift.some(
20
+ (folder) => folder.unlisted.length > 0 || folder.missing.length > 0,
21
+ )
22
+ }
23
+
24
+ /**
25
+ * Whether the audit found something that should fail the caller.
26
+ *
27
+ * An unresolved citation is a broken pointer and gates unconditionally. The two
28
+ * findings `--gate` adds are the ones answerable from the file itself: a
29
+ * required section it does not declare, and an index disagreeing with its
30
+ * folder. Length, depth, bullet, table, and provenance findings are thresholds
31
+ * a reader weighs, so they stay out under both modes.
32
+ */
33
+ export function isGating({
34
+ unresolvedCitations,
35
+ sections,
36
+ drift,
37
+ widened,
38
+ }: GateInput): boolean {
39
+ if (unresolvedCitations > 0) return true
40
+ if (!widened) return false
41
+
42
+ return sections.length > 0 || hasDrift(drift)
43
+ }
@@ -0,0 +1,74 @@
1
+ /**
2
+ * The frontmatter field exempting a seed from the section check the seed gate
3
+ * runs.
4
+ *
5
+ * The check has a false-positive class its own comment records: a standard may
6
+ * sanction omitting a section, and no measure separates that from a file that
7
+ * forgot it. Reporting is the right response in a live project, where the
8
+ * finding is advisory. The seed gate promotes the same finding to a failing
9
+ * exit code, so the seed tree needs a way to say the omission is deliberate.
10
+ */
11
+ export const SEED_STUB_FIELD = 'stub'
12
+
13
+ /**
14
+ * Whether an install rewrites this seed rather than copying it byte for byte.
15
+ *
16
+ * This answers what the caller branches on and not whether the file holds a
17
+ * marker, which is `isStubSeed` reading content. Only markdown carries
18
+ * frontmatter, so only markdown can hold one, and the extension decides the
19
+ * copy path without opening the file. Every install path and the drift report
20
+ * ask this one question, or the report compares a marked source against a
21
+ * stripped target and reads a seed nobody touched as drifted.
22
+ */
23
+ export function rewritesOnInstall(src: string): boolean {
24
+ return src.endsWith('.md')
25
+ }
26
+
27
+ const FRONTMATTER = /^---\n([\s\S]*?)\n---(\n|$)/
28
+ const STUB_LINE = new RegExp(`^${SEED_STUB_FIELD}:[ \\t]*true[ \\t]*$`, 'm')
29
+
30
+ /**
31
+ * Whether the source declares itself a skeleton for the target to fill.
32
+ *
33
+ * Only `true` counts. A field set to anything else reads as a seed that meant
34
+ * to turn the exemption off, and treating an unparsed value as exempt would
35
+ * make a typo silence the gate.
36
+ */
37
+ export function isStubSeed(source: string): boolean {
38
+ const block = source.match(FRONTMATTER)
39
+ if (!block) return false
40
+
41
+ return STUB_LINE.test(block[1])
42
+ }
43
+
44
+ /**
45
+ * Removes the marker so it reaches no target.
46
+ *
47
+ * The field is toolkit bookkeeping about the seed tree, and a project that
48
+ * received it would carry a field its own tooling never reads. A file whose
49
+ * frontmatter holds nothing else loses the block entirely rather than keeping
50
+ * an empty one.
51
+ */
52
+ export function stripSeedMarker(source: string): string {
53
+ const block = source.match(FRONTMATTER)
54
+ if (!block) return source
55
+
56
+ const kept = block[1]
57
+ .split('\n')
58
+ .filter((line) => !STUB_LINE.test(line))
59
+ .join('\n')
60
+
61
+ // A function replacement, because a string one reads `$1` and `$&` in the
62
+ // kept fields as references to this match. A `description` naming a dollar
63
+ // amount would otherwise substitute the whole frontmatter into itself and
64
+ // carry the marker along with it.
65
+ if (kept.trim() !== '') {
66
+ const replacement = `---\n${kept}\n---${block[2] ?? ''}`
67
+ return source.replace(FRONTMATTER, () => replacement)
68
+ }
69
+
70
+ // Dropping the block takes the blank line that separated it from the body
71
+ // with it. Leaving that behind opens the installed file on whitespace, which
72
+ // is a diff every target would carry against its own formatter.
73
+ return source.slice(block[0].length).replace(/^\n+/, '')
74
+ }
@@ -1,6 +1,7 @@
1
1
  import { readFileSync } from 'node:fs'
2
2
  import { relative } from 'node:path'
3
3
  import { planSeeds, type Seed } from '@/claude/seeds'
4
+ import { rewritesOnInstall, stripSeedMarker } from '@/seed-marker'
4
5
  import { findInstalledOrigin, readHistoryIndex } from '@/sync/history'
5
6
 
6
7
  /**
@@ -106,6 +107,20 @@ function attribute(
106
107
  return false
107
108
  }
108
109
 
110
+ /**
111
+ * Compares the target against what an install would write, not against the seed
112
+ * source. The two differ for a markdown seed carrying the stub marker, which the
113
+ * install strips, so comparing sources would report a file the target never
114
+ * touched as drifted for as long as the marker stays set.
115
+ */
109
116
  function sameContent(source: string, dest: string): boolean {
110
- return readFileSync(source).equals(readFileSync(dest))
117
+ const installed = readFileSync(dest)
118
+
119
+ if (!rewritesOnInstall(source)) {
120
+ return readFileSync(source).equals(installed)
121
+ }
122
+
123
+ return (
124
+ stripSeedMarker(readFileSync(source, 'utf8')) === installed.toString('utf8')
125
+ )
111
126
  }
@@ -10,6 +10,7 @@ import {
10
10
  import { dirname, join } from 'node:path'
11
11
  import { $ } from 'bun'
12
12
  import { copyPreservingMode } from '@/copy'
13
+ import { rewritesOnInstall, stripSeedMarker } from '@/seed-marker'
13
14
  import { mergeSections, pruneSections } from '@/tooling/gitignore'
14
15
  import { ancestorsFirst, listFiles, type Manifest } from '@/tooling/manifest'
15
16
  import {
@@ -44,6 +45,21 @@ export async function injectConfigs(
44
45
  return applied
45
46
  }
46
47
 
48
+ /**
49
+ * Writes a seed to a target that does not have it, dropping the stub marker on
50
+ * the way. The marker is toolkit bookkeeping read by the seed gate, so a target
51
+ * receiving it would carry a field its own tooling never reads. Only markdown
52
+ * carries frontmatter, and every other seed copies byte for byte.
53
+ */
54
+ async function writeSeed(src: string, dest: string): Promise<void> {
55
+ if (!rewritesOnInstall(src)) {
56
+ await copyFile(src, dest)
57
+ return
58
+ }
59
+
60
+ await writeFile(dest, stripSeedMarker(await readFile(src, 'utf8')))
61
+ }
62
+
47
63
  /**
48
64
  * Copies a seed when the target lacks it. When the target already has one and
49
65
  * the seed is a `.txt` word list, missing lines are appended and the file is
@@ -53,7 +69,7 @@ async function mergeSeedFile(src: string, dest: string): Promise<void> {
53
69
  await mkdir(dirname(dest), { recursive: true })
54
70
 
55
71
  if (!existsSync(dest)) {
56
- await copyFile(src, dest)
72
+ await writeSeed(src, dest)
57
73
  return
58
74
  }
59
75