@erclx/aitk 0.53.0 โ†’ 0.55.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.53.0",
4
+ "version": "0.55.0",
5
5
  "author": {
6
6
  "name": "Eric Le",
7
7
  "url": "https://github.com/erclx"
@@ -9,11 +9,14 @@ description: Scope boundary for pipeline structure against job contents, and the
9
9
 
10
10
  Without this skill, a session writes a pipeline whose jobs run in sequence because `needs` was used to express the order a person reads them in rather than a data dependency. The pipeline then costs the sum of its jobs where it could have cost the longest one, and the waste compounds on every push.
11
11
 
12
+ The same session splits a gate that finishes in a minute into three parallel jobs, which fails in the other direction. Each job repays checkout, dependency install, and toolchain setup before it reaches a stage, so the gate gets slower while reporting no earlier, and the roster reads as a design decision rather than as a cost nobody measured.
13
+
12
14
  The rest are reproducibility failures that surface as flakes. An action pinned to a moving ref changes under the project, so a run that passed yesterday fails today with no commit behind it and the diff explains nothing. A cache keyed on a static string serves a stale browser or toolchain after a version bump, and the failure reads as a broken test rather than a stale cache. Artifacts upload on every run and never expire, so storage grows with the commit count while the ones worth reading are the failures. And a workflow with no manual trigger can only be reproduced by pushing a commit, which is the wrong instrument for a run that failed for an environmental reason.
13
15
 
14
16
  ## Must
15
17
 
16
18
  - Gate a job only on a data dependency or on a cost that justifies the wait, and leave every other job parallel
19
+ - Start the checks that share setup in one job and split them only once a run log puts the gate past roughly two minutes
17
20
  - Pin every action to a tag that cannot cross a major version
18
21
  - Key a cache on the version string of the thing it caches
19
22
  - Give every workflow a manual trigger beside its primary one
@@ -20,14 +20,21 @@ Generate GitHub Actions workflow files for CI pipelines. Enforce parallel job ex
20
20
 
21
21
  ## Job naming
22
22
 
23
- - Name jobs with emoji + title: `๐Ÿ›ก๏ธ Static Checks`, `๐Ÿงช Unit Tests`, `๐Ÿ“ฆ Build Check`, `๐ŸŽญ E2E Tests`, `๐Ÿš€ Deploy`, `๐Ÿ” Code Quality`, `๐Ÿท๏ธ Release`, `๐Ÿ”’ Security`.
23
+ - Name jobs with emoji + title: `๐Ÿ›ก๏ธ Checks`, `๐Ÿ›ก๏ธ Static Checks`, `๐Ÿงช Unit Tests`, `๐Ÿ“ฆ Build Check`, `๐ŸŽญ E2E Tests`, `๐Ÿš€ Deploy`, `๐Ÿ” Code Quality`, `๐Ÿท๏ธ Release`, `๐Ÿ”’ Security`.
24
+
25
+ ## Job granularity
26
+
27
+ - Start a new pipeline with the checks that share setup in one job: static analysis, unit tests, and build. A project with no run history has nothing to measure, and one job is the shape that costs least to reverse.
28
+ - Keep them in one job while the gate runs under roughly two minutes end to end. Each extra job repays checkout, dependency install, and toolchain setup before it reaches a stage, so a split at that duration costs more than the parallelism returns.
29
+ - Split once a run log puts the gate past that, then apply the dependency rules below. The run log is the revision trigger rather than the entry condition, since repository size and stage count answer nothing.
30
+ - Give E2E, release, and deploy their own jobs from the start. Each carries a data dependency or a gate, so this rule never folds them into the check job.
24
31
 
25
32
  ## Job dependencies
26
33
 
27
34
  - Run independent jobs in parallel.
28
35
  - Use `needs` only when there is a data dependency (a job requires an artifact) or the job is prohibitively expensive relative to its gate.
29
- - Run static, unit, and build jobs in parallel.
30
- - Gate E2E on build, since it requires the built artifact.
36
+ - Run static, unit, and build jobs in parallel once the gate is split.
37
+ - Gate E2E on whichever job uploads the build artifact, since it consumes that artifact. That job is the folded check job in a new pipeline and the separate build job once the gate is split.
31
38
  - Gate release and deploy on E2E.
32
39
 
33
40
  ## Artifacts
@@ -42,7 +49,7 @@ Generate GitHub Actions workflow files for CI pipelines. Enforce parallel job ex
42
49
 
43
50
  ## Template
44
51
 
45
- Load `${CLAUDE_SKILL_DIR}/references/workflows.md` for the base workflow template. Adapt it to the project's stack, test commands, and build output. Add or remove jobs as needed while preserving the parallel and gated structure.
52
+ Load `${CLAUDE_SKILL_DIR}/references/workflows.md` for the base workflow template. Adapt it to the project's stack, test commands, and build output. Add or remove jobs as needed. The template already carries the folded shape, and the split beside it applies once the granularity rule above calls for it.
46
53
 
47
54
  ## Validation
48
55
 
@@ -50,8 +57,8 @@ Before responding, verify:
50
57
 
51
58
  - `workflow_dispatch` is present alongside the primary trigger.
52
59
  - All actions pinned to major version tags, no `@latest` or `@main`.
53
- - Static, unit, and build jobs have no `needs` and run in parallel.
54
- - E2E uses `needs: build`. Release and deploy use `needs: e2e`.
60
+ - A new pipeline folds static, unit, and build into one job, and a gate a run log put past two minutes gives them no `needs` so they run in parallel.
61
+ - Every name in a `needs` matches a job declared in the same file. E2E names the artifact producer, release and deploy name E2E.
55
62
  - Artifacts upload on `if: failure()` only with `retention-days: 7`.
56
63
  - Job names use emoji + title format.
57
64
  - Deploy, publish, and release jobs carry a placeholder step and a named handoff, never a guessed deploy command.
@@ -1,6 +1,6 @@
1
1
  # CI workflow template
2
2
 
3
- Copy this base template and adapt it to the project's stack, test commands, and build output. Add or remove jobs as needed while preserving the parallel and gated structure.
3
+ Copy this base template and adapt it to the project's stack, test commands, and build output. It carries the shape a new pipeline starts in, with the checks that share setup in one job, per the granularity rule in the skill body. `e2e` and anything gated on it stay separate jobs at either size.
4
4
 
5
5
  ```yaml
6
6
  name: CI
@@ -13,8 +13,8 @@ on:
13
13
  workflow_dispatch:
14
14
 
15
15
  jobs:
16
- static:
17
- name: '๐Ÿ›ก๏ธ Static Checks'
16
+ checks:
17
+ name: '๐Ÿ›ก๏ธ Checks'
18
18
  runs-on: ubuntu-latest
19
19
  steps:
20
20
  - uses: actions/checkout@v4
@@ -23,9 +23,16 @@ jobs:
23
23
  bun-version: latest
24
24
  - run: bun install --frozen-lockfile
25
25
  - run: bun run check
26
+ - run: bun run test
27
+ - run: bun run build
28
+ - uses: actions/upload-artifact@v4
29
+ with:
30
+ name: build-output
31
+ path: dist/
26
32
 
27
- unit:
28
- name: '๐Ÿงช Unit Tests'
33
+ e2e:
34
+ name: '๐ŸŽญ E2E Tests'
35
+ needs: checks
29
36
  runs-on: ubuntu-latest
30
37
  steps:
31
38
  - uses: actions/checkout@v4
@@ -33,10 +40,27 @@ jobs:
33
40
  with:
34
41
  bun-version: latest
35
42
  - run: bun install --frozen-lockfile
36
- - run: bun run test
43
+ - uses: actions/download-artifact@v4
44
+ with:
45
+ name: build-output
46
+ path: dist/
47
+ - run: bun run test:e2e
48
+ - uses: actions/upload-artifact@v4
49
+ if: failure()
50
+ with:
51
+ name: e2e-results
52
+ path: test-results/
53
+ retention-days: 7
54
+ ```
37
55
 
38
- build:
39
- name: '๐Ÿ“ฆ Build Check'
56
+ ## Splitting the check job
57
+
58
+ Apply this once a run log puts the gate past roughly two minutes, never before. Each job below repays checkout, dependency install, and toolchain setup before it reaches a stage, which is the cost the duration has to cover. Replace the `checks` job above with these three and point `e2e` at `needs: build`.
59
+
60
+ ```yaml
61
+ jobs:
62
+ static:
63
+ name: '๐Ÿ›ก๏ธ Static Checks'
40
64
  runs-on: ubuntu-latest
41
65
  steps:
42
66
  - uses: actions/checkout@v4
@@ -44,15 +68,21 @@ jobs:
44
68
  with:
45
69
  bun-version: latest
46
70
  - run: bun install --frozen-lockfile
47
- - run: bun run build
48
- - uses: actions/upload-artifact@v4
71
+ - run: bun run check
72
+
73
+ unit:
74
+ name: '๐Ÿงช Unit Tests'
75
+ runs-on: ubuntu-latest
76
+ steps:
77
+ - uses: actions/checkout@v4
78
+ - uses: oven-sh/setup-bun@v2
49
79
  with:
50
- name: build-output
51
- path: dist/
80
+ bun-version: latest
81
+ - run: bun install --frozen-lockfile
82
+ - run: bun run test
52
83
 
53
- e2e:
54
- name: '๐ŸŽญ E2E Tests'
55
- needs: build
84
+ build:
85
+ name: '๐Ÿ“ฆ Build Check'
56
86
  runs-on: ubuntu-latest
57
87
  steps:
58
88
  - uses: actions/checkout@v4
@@ -60,15 +90,9 @@ jobs:
60
90
  with:
61
91
  bun-version: latest
62
92
  - run: bun install --frozen-lockfile
63
- - uses: actions/download-artifact@v4
93
+ - run: bun run build
94
+ - uses: actions/upload-artifact@v4
64
95
  with:
65
96
  name: build-output
66
97
  path: dist/
67
- - run: bun run test:e2e
68
- - uses: actions/upload-artifact@v4
69
- if: failure()
70
- with:
71
- name: e2e-results
72
- path: test-results/
73
- retention-days: 7
74
98
  ```
@@ -16,7 +16,7 @@ Review is the step that varies most. It gets skipped on a diff that needed one,
16
16
  - Take the approved plan for the branch as the scope, and implement only what it describes
17
17
  - Give every step a stop condition, and leave the code on the branch and the receipts on disk at each one
18
18
  - Classify the changed-file list by path as well as by extension, so informational prose skips a code review with no signal on it and executable prose still reaches one
19
- - Stop on any critical or should-fix finding rather than acting on it
19
+ - Split findings by origin, stopping on a critical or should-fix one the branch inherited and repairing one this run caused
20
20
  - Open the pull request as a draft, then watch continuous integration to a terminal state
21
21
  - Name the recovery for the stop it took, since the value of stopping is that the user knows where to resume
22
22
 
@@ -24,7 +24,9 @@ Review is the step that varies most. It gets skipped on a diff that needed one,
24
24
 
25
25
  - Expand past the plan, refactor a neighbor, or touch a file outside it without reason
26
26
  - Loop on a failed verify. One fix attempt against the reported errors, then stop.
27
- - Fix a review finding or a failing check. Both stops are deliberate, since a green pull request reached by auto-fix hides what broke.
27
+ - Fix an inherited review finding or a failing check. Both stops are deliberate, since a green pull request reached by auto-fix hides what broke.
28
+ - Loop on a self-introduced finding. One repair pass, then stop, which is the bound a failed verify already carries.
29
+ - Read the plan's file list as the boundary on a repair. It scopes what the run builds, and a finding this run caused is in reach wherever it landed.
28
30
  - Run the memory Apply phase. Promoting an entry changes how the agent operates and ships as its own change.
29
31
  - Read an empty changed-file list as prose-only. It satisfies that test vacuously and would route the branch past review instead of through it.
30
32
  - Read a markdown extension as evidence the change only informs. A skill body, a governance rule, and a standard are behavior written in prose.
@@ -90,12 +90,15 @@ The list covers this toolkit's authoring layout and the layout it installs, whic
90
90
 
91
91
  ## Step 6: evaluate findings
92
92
 
93
- Skip this step when Step 5 skipped review. Otherwise read `.claude/review/review-<slug>.md` at the main worktree root. Parse the summary line (`X critical, Y should-fix, Z minor`):
93
+ Skip this step when Step 5 skipped review. Otherwise read `.claude/review/review-<slug>.md` at the main worktree root. Split every finding by origin before parsing the summary line (`X critical, Y should-fix, Z minor`), since the stop exists for a defect the branch inherited rather than for one this run introduced.
94
94
 
95
- - Any critical or should-fix count greater than zero, stop: `โŒ Review found non-minor issues. See .claude/review/review-<slug>.md. Fix and run /git-ship.`
96
- - Zero critical and zero should-fix, continue. The minor findings stay in the on-disk review receipt. Fold any a reviewer needs into the PR's `## Technical Context`. Do not add a separate review-notes section to the PR body.
95
+ - **This run caused it, at any severity.** Fix it, re-run the Step 3 verify commands, re-read the fixed file against what the finding claimed, and continue. Do not report it as a stop and do not offer the fix as a choice, which is the same stop wearing a proposal.
96
+ - **It predates this run, critical or should-fix.** Stop: `โŒ Review found non-minor issues that predate this run. See .claude/review/review-<slug>.md. Fix and run /git-ship.`
97
+ - **It predates this run, minor only.** Continue. The minor findings stay in the on-disk review receipt. Fold any a reviewer needs into the PR's `## Technical Context`. Do not add a separate review-notes section to the PR body.
97
98
 
98
- Do not auto-fix findings. The stop here is deliberate.
99
+ Read origin as causation rather than authorship. Staleness this run induced in a file it never opened is a finding it caused, and the plan's "Files to touch" list scopes what the run builds rather than what it may repair.
100
+
101
+ Bound the repair at one pass, the way Step 3 bounds verify. When that re-read shows the finding still standing, stop: `โŒ A self-introduced finding survived one fix pass. See .claude/review/review-<slug>.md. Fix and run /git-ship.`
99
102
 
100
103
  ## Step 7: ship
101
104
 
@@ -151,5 +154,6 @@ Every stop point leaves recoverable state. The user resumes manually from the ap
151
154
  | Branch collision on worktree entry | `claude-worktree` Step 5 found `<slug>` already as a local branch. Resolve manually (rename or delete the stale branch), then re-run autoship. |
152
155
  | Verify fails | Read logs, fix manually, run `/git-ship` |
153
156
  | UI checklist | Verify visually, run `/git-ship` |
154
- | Review findings | Fix findings, run `/git-ship` |
157
+ | Inherited review findings | Fix findings, run `/git-ship` |
158
+ | Self-introduced finding survived | Read the receipt for what the one repair pass left open, fix it, run `/git-ship` |
155
159
  | git-ship fails | Inspect hook or remote error, run again |
@@ -14,6 +14,7 @@ Read `${CLAUDE_SKILL_DIR}/references/folder-format.md` before writing any file i
14
14
  - If no topic is given, stop: `โŒ No topic. Name what needs measuring.`
15
15
  - Apply the qualifying test in open mode alone, after Step 1 resolves the mode and before the folder is created. Two of these three must hold: the current state is not known, more than one approach is live, and committing wrong costs more than a day of measuring. When one or fewer holds, stop: `โŒ Already decided enough to plan. Run /claude-feature instead.`
16
16
  - Resume and close are exempt from the test above. A track that has already been measured fails it by definition, since its current state is now known and its approaches have narrowed, so applying the test to either mode refuses the folder that same test admitted.
17
+ - A refused topic that is a broad dump rather than one question routes to `claude-intake`, not to the planning skill the stop names. Intake dispositions many findings in breadth from what the repository already holds, and one folder holding dozens of unrelated threads is what forcing them past this guard produces.
17
18
  - Do not pause for approval between steps. The write scope below is what makes that safe.
18
19
 
19
20
  ## Write scope
@@ -0,0 +1,46 @@
1
+ ---
2
+ name: claude-intake
3
+ description: Why a brain dump gets a filed inventory rather than ten plans, and why an empty operator slot means unread
4
+ ---
5
+
6
+ # Claude intake requirement
7
+
8
+ ## Gap
9
+
10
+ Without this skill, a brain dump reaches a session that has nowhere to put it. `claude-feature` answers with one plan per independent concern, so forty findings produce ten plan files before anything has been measured. `claude-groundwork` refuses a breadth pass outright, since its qualifying test asks whether the current state is unknown and most items are knowable by grep. What gets filed instead is a list of opinions, because nothing forces a count against the tree and a complaint reads the same whether it covers three sites or three hundred.
11
+
12
+ Two failure modes cost more than the rest. An operator's silence on an item reads as consent when the folder borrows the plan file's blank-means-accept contract, which ships changes nobody approved across a folder read over weeks. And a report naming only a path cannot distinguish three new items from one reworded sentence in a file that holds a dozen items, so every reader diffs it against memory to find out what moved.
13
+
14
+ Four more are cheaper to name than to rediscover. A question filed without a pick comes back unresolved, measured across one folder's 19 open items, where every one carrying a suggestion resolved on a bare `ok` and the five carrying none did not. A session with no numbering convention re-decides the folder shape per dump, so no two intakes are readable the same way and the second one has to be learned from scratch. A question answerable both in the index and on its item resolves to whichever a reader opens first, with no rule saying which wins. And a pass with no write scope starts fixing what it files, which turns a triage into a branch nobody asked for and nobody reviewed.
15
+
16
+ ## Must
17
+
18
+ - Route each item on whether the repository can answer it today, sending what needs an experiment or an outside source to the groundwork skill and what is already decided to the planning skill
19
+ - Measure every problem line against the tree during this pass, carrying a number or a file path rather than a figure from recall
20
+ - Close every item with a verdict, and pair every open question with a suggested pick
21
+ - Treat an empty operator slot as unread rather than as agreement
22
+ - Reserve the index number and carry the domain in every other filename, leaving the rest of the numbering as read order
23
+ - Keep answers on items alone, with the index pointing at them
24
+ - Name the heading and the act beside every path the pass wrote
25
+ - Confine writes to the intake folder
26
+
27
+ ## Must not
28
+
29
+ - Write a plan, a task file, a standard, a rule, or a source change
30
+ - Fill an operator's answer slot, or infer a disposition from an empty one
31
+ - Replace a verdict with an overlap line, which drops the call on exactly the items where a live board task might be the thing that is wrong
32
+ - Reserve mid-range numbers, which would force every future intake into one dump's shape
33
+ - Date every file, since the first edit to one leaves the rest stale
34
+ - Open a folder for a single question, which is either a groundwork track or a plan
35
+
36
+ ## Guards
37
+
38
+ - No dump given: stop rather than inferring one
39
+ - One question rather than a set of findings: stop and route to the groundwork or planning skill
40
+
41
+ ## Out of scope
42
+
43
+ - Measuring one question in depth, which `claude-groundwork` owns
44
+ - Planning a promoted item, which `claude-feature` owns
45
+ - Promoting an item onto the board, which `claude-tasks` owns
46
+ - Enforcing any of this. The folder is gitignored, so no check reaches its contents and every rule holds only while a session reads it.
@@ -0,0 +1,139 @@
1
+ ---
2
+ name: claude-intake
3
+ description: Files a raw brain dump into a numbered intake folder under `.claude/intake/<slug>/`, one item per finding carrying a measured problem, a proposed fix, and a verdict. Use when asked to "file this dump", "triage my notes", "work through this list", "sort out this brain dump", or "run an intake pass". Do NOT use for one question that has to be measured before anyone can plan it. That is `claude-groundwork`.
4
+ ---
5
+
6
+ # Claude intake
7
+
8
+ Intake dispositions many findings in breadth. A dump goes in, an inventory comes out, and every item carries a problem measured against the tree, one proposed fix, and a verdict. The item that turns out to be already settled is the highest-value output, and it is the one thing neither a plan nor a groundwork track has anywhere to put.
9
+
10
+ Read `${CLAUDE_SKILL_DIR}/references/folder-format.md` before writing any file in the folder. It holds the numbering, the file map, the frontmatter, and the item template.
11
+
12
+ ## Routing
13
+
14
+ The test is one question. Can the item be answered by reading the repository today?
15
+
16
+ - Yes: intake owns it, and the cost is a session of grepping
17
+ - No, because it needs an experiment or a source outside the project: route it to `claude-groundwork`, where the cost is measured in runs and days
18
+ - Already decided, with only the work left: route it to `claude-feature`
19
+
20
+ Apply the test per item rather than per dump. A dump of forty items typically yields one groundwork candidate, so routing the whole dump on its worst item buys a folder nobody can close.
21
+
22
+ Using the wrong one fails in two shapes. Intake on a question that needs measuring yields a confident verdict with nothing behind it. Groundwork on a brain dump is refused by its own qualifying guard, and forcing past that refusal gives one folder holding dozens of unrelated threads and a decision file that can close one of them.
23
+
24
+ ## Guards
25
+
26
+ - If no dump is given, stop: `โŒ No dump to file. Paste the notes or name what to triage.`
27
+ - If the dump is one question rather than a set of findings, stop: `โŒ One question, not a dump. Run /claude-groundwork to measure it or /claude-feature to plan it.`
28
+ - Do not pause for approval between steps. The write scope below is what makes that safe.
29
+
30
+ ## Write scope
31
+
32
+ - Write only inside `.claude/intake/<slug>/`. A plan file, a task file, a source change, a standard, and a rule all live outside that folder, so this one rule forbids every one of them.
33
+ - There is no exception. Promoting an item onto the board runs through `claude-tasks` after the operator has answered, which is a separate invocation.
34
+ - Reading is unrestricted inside the project. Measuring is the work.
35
+ - Treat the folder as gitignored and unbacked. No check reaches its contents, so every rule stated here holds only while a session reads it.
36
+
37
+ Nothing outside this body carries the floor, and no path-scoped rule can. The item format and the answer contract are heading for a standard covering this folder and a groundwork track together, which is queued rather than written.
38
+
39
+ ## Step 1: detect open or resume
40
+
41
+ List `.claude/intake/` from the project root and match the topic against the folders already there before deriving a slug. A second pass over the same subject rarely phrases the topic the way the folder was named, so a fresh slug would open a duplicate beside a live folder.
42
+
43
+ Never match against `.claude/` itself. That directory holds every other workflow surface, so a topic matched there lands on a folder that was never an intake.
44
+
45
+ With no match, derive a kebab-case slug named for the subject rather than the activity. Prefer `toolkit-overview` over `august-triage`. An absent folder opens, and a present one resumes by appending items and revising verdicts the tree has moved under.
46
+
47
+ ## Step 2: orient
48
+
49
+ Read these in parallel from the project root, skipping any that do not exist:
50
+
51
+ - `CLAUDE.md`: behavior rules, conventions, commands
52
+ - `.claude/REQUIREMENTS.md`: scope and non-goals
53
+ - `.claude/ARCHITECTURE.md`: decisions already made
54
+ - `.claude/tasks/index.md`: what is already tracked. Open a task file whose entry looks related to an item.
55
+
56
+ Then read only what a live item needs. Do not read entire directories speculatively. Where a folder carries an `index.md`, read it first and load only the files it points at.
57
+
58
+ ## Step 3: measure against the tree
59
+
60
+ Grep for each construct an item names and count the sites. Every problem line carries a number or a file path taken during this pass.
61
+
62
+ Never carry a figure from an earlier session, a summary, or another document. The dump states the complaint and the tree states the size of it, and that measurement is the whole difference between an inventory and a list of opinions. Confirm that any work an item sequences behind is still open, so no item leads with something that already shipped.
63
+
64
+ Name the commit the pass measured against in the overview body. It is the half a later reader can check.
65
+
66
+ ## Step 4: cluster
67
+
68
+ Split items by domain, one file per cluster, and let the file count follow the number of separable domains rather than the size of the dump. An item belongs to the domain its fix touches, not the domain the complaint arrived from.
69
+
70
+ Two heading levels is the right depth. A third means the cluster should have been split into its own file.
71
+
72
+ ## Step 5: disposition each item
73
+
74
+ Write every item in the format below, in the cluster file its fix belongs to. Close each item with a verdict and an empty operator slot.
75
+
76
+ ## Step 6: write the index
77
+
78
+ Write `00-overview.md` last, once the clusters are filed and the counts are real. It carries the format block, the cluster table, the verdict counts, the ready list, and the open questions.
79
+
80
+ Each open question in the index is a labeled markdown link to its owning item's heading anchor. The index points and the item owns, so no answer slot appears in the index. One question in two answerable places has no rule for which wins, and retrieval walks item headings, so an answer typed into the index is found by nothing and lost silently.
81
+
82
+ Add `99-next-session.md` only where the pass ends holding context no cluster file carries, such as a dump half filed or a measurement that has to be redone. Write it self-contained, since the folder is unbacked and the conversation behind it compacts away.
83
+
84
+ ## Item format
85
+
86
+ ```markdown
87
+ ### N. Short title stating the defect
88
+
89
+ - **Problem:** what is wrong today, stated against the tree and carrying a number or a file path
90
+ - **Fix:** the one change proposed
91
+ - **Worth it:** yes, later, or no, with the reason
92
+ - **Open:** only where the call is the operator's
93
+ - **Suggested:** the pick in one sentence, then the reason and the main tradeoff in one or two
94
+ - **Overlaps:** the live board task that already owns this item
95
+ - **You:**
96
+ ```
97
+
98
+ - `Problem:`, `Fix:`, `Worth it:`, and the empty `You:` slot ship on every item. The other two are conditional.
99
+ - `Suggested:` is required whenever `Open:` is present. A bare question invites a bare answer, and `ok` against two defensible options carries no information. Where the answer is the operator's preference rather than a technical call, say so in that form rather than inventing a default. The toolkit's `decision-help` snippet writes the same four-line shape for chat use, and the four lines above are the whole spec.
100
+ - `Overlaps:` never replaces `Worth it:`. The items where a live board task might be the thing that is wrong are exactly the ones whose verdict matters most.
101
+
102
+ ## The answer contract
103
+
104
+ `You:` belongs to the operator and ships empty on every item.
105
+
106
+ Empty means unread. It never means agreement. Accepting a verdict is typed as one token, `- **You:** ok`.
107
+
108
+ That inverts the plan file's contract, where a blank `- Answer:` means accept the suggestion, and the inversion is deliberate. A plan is read in one sitting with every question already surfaced in chat. An intake folder is read over weeks, so an empty slot is ambiguous between accepting the verdict and never having reached the item, and the second reading is far more likely. Acting on silence as consent ships a change nobody approved.
109
+
110
+ Never fill a `You:` slot, and never infer a disposition from an empty one. On a resume pass, report unread items by count rather than deciding them.
111
+
112
+ ## Numbering
113
+
114
+ Numbers are read order and nothing else. Reserve `00` for the index and carry the domain in every other filename, so `07-tooling.md` says what it holds before anyone opens it.
115
+
116
+ Do not reserve mid-range numbers. Clusters differ per dump, so a contract over `06` would force every future intake into one dump's shape. `claude-groundwork` reserves its numbers because its shape is fixed, and that half of the convention does not transfer.
117
+
118
+ ## Output
119
+
120
+ Emit the full relative path from the project root for every file written, and name the heading and the act beside it. A path alone cannot distinguish three new items from one reworded sentence in a file that holds a dozen items and lives for weeks, so a bare path sends the reader to diff it against memory. This overrides the paths-only reporting the project states generally, which stays right wherever the reader is about to see a diff.
121
+
122
+ A file the pass only read gets no line, which is what keeps the block short.
123
+
124
+ ```plaintext
125
+ ๐Ÿ“‚ Opened .claude/intake/<slug>/
126
+
127
+ **Filed:**
128
+
129
+ - `.claude/intake/<slug>/05-coverage.md` gains items 6 to 8 under a new `## What the merge gate covers`
130
+ - `.claude/intake/<slug>/00-overview.md` cluster rows and verdict counts updated
131
+
132
+ **Routing:** <N> plan-ready, <N> groundwork candidates, <N> already settled
133
+
134
+ **Open questions:** <N> awaiting your call
135
+
136
+ Next: answer the `You:` slots, then /claude-tasks to promote what is ready
137
+ ```
138
+
139
+ Use `๐Ÿ“‚ Resumed` in place of `๐Ÿ“‚ Opened` on a resume pass.
@@ -0,0 +1,97 @@
1
+ ---
2
+ title: Intake folder reference
3
+ description: Reserved index number, file map, frontmatter and dating, the item template, retrieval, and anti-patterns
4
+ ---
5
+
6
+ # Intake folder reference
7
+
8
+ Applies to an intake folder at `.claude/intake/<slug>/`. One folder holds one dump, filed by domain, and every finding in it is an item carrying a measured problem and a verdict.
9
+
10
+ The folder is gitignored and unbacked. No check reaches its contents, so the shape below survives only by being read.
11
+
12
+ ## Files
13
+
14
+ | File | Holds | Required |
15
+ | -------------------- | ------------------------------------------------------------ | -------- |
16
+ | `00-overview.md` | Index: format block, cluster table, verdicts, open questions | Always |
17
+ | `NN-<domain>.md` | One cluster of items, filed by the domain their fixes touch | Always |
18
+ | `99-next-session.md` | What a compaction destroys that no cluster file carries | Optional |
19
+
20
+ `00` is the only reserved number. Everything else is read order, and the domain rides in the filename so a reader knows what `07-tooling.md` holds without opening it.
21
+
22
+ Do not reserve mid-range numbers. Clusters differ per dump, so a contract over `06` would force every future intake into one dump's shape. Groundwork reserves its numbers because its shape is fixed, and that half of the convention does not transfer.
23
+
24
+ Let the file count follow the number of separable domains. A large dump with two domains is a small folder.
25
+
26
+ ## Frontmatter and dating
27
+
28
+ Every file carries `title` and `description` per the project's prose standard. `00-overview.md` carries one field the others do not, a `date` holding the day the folder opened.
29
+
30
+ Date the folder once rather than every file. Twelve dated files leave eleven stale the first time one cluster is edited, and the opening date never rots. The checkable half is the commit, which the overview body names as what the claims were measured against.
31
+
32
+ ## 00-overview.md
33
+
34
+ The index. It points at items and answers nothing itself.
35
+
36
+ - The item format block, copied so a returning session picks the shape up from the folder
37
+ - The answer contract stated out loud, since it inverts the plan file's
38
+ - A cluster table of file, what it holds, item count, and open count
39
+ - The verdict counts across the folder
40
+ - A ready list, grouped by what shipping one actually costs
41
+ - The open questions, each a labeled markdown link to its owning item's heading anchor
42
+
43
+ The index carries no answer slot. One question in two answerable places has no rule for which wins, and retrieval walks item headings, so an answer typed into the index is found by nothing and lost silently.
44
+
45
+ Where an item touches a task already on the board, say so in the index rather than only inside the item. A reader deciding what to promote reads the index first.
46
+
47
+ ## Item template
48
+
49
+ ```markdown
50
+ ### N. Short title stating the defect
51
+
52
+ - **Problem:** what is wrong today, stated against the tree and carrying a number or a file path
53
+ - **Fix:** the one change proposed
54
+ - **Worth it:** yes, later, or no, with the reason
55
+ - **Open:** only where the call is the operator's
56
+ - **Suggested:** the pick in one sentence, then the reason and the main tradeoff in one or two
57
+ - **Overlaps:** the live board task that already owns this item
58
+ - **You:**
59
+ ```
60
+
61
+ `Problem:`, `Fix:`, `Worth it:`, and the empty `You:` slot ship on every item. `Open:` appears only where the call is the operator's, `Suggested:` is required whenever it does, and `Overlaps:` is optional and never replaces the verdict.
62
+
63
+ Two heading levels is the right depth inside a cluster file. A third means the cluster should have been its own file.
64
+
65
+ An item may carry a bolded standalone line between the bullets where a finding needs a name of its own. Keep it rare. Everything that fits the four bullets belongs in them.
66
+
67
+ ## Retrieval
68
+
69
+ Answers live on items, so one pass over the folder reports every touched slot.
70
+
71
+ ```bash
72
+ awk '/^### /{h=FILENAME": "$0} /^- \*\*You:\*\*./{print h; print " "$0}' *.md
73
+ ```
74
+
75
+ Counting what is still unread runs against the empty slot instead.
76
+
77
+ ```bash
78
+ grep -c '^- \*\*You:\*\*$' *.md
79
+ ```
80
+
81
+ Both walk `###` headings, which is the mechanical reason an answer typed anywhere else is lost.
82
+
83
+ ## Conventions
84
+
85
+ - State a number with what it settles. The strongest items are the ones where a measurement decides the verdict and says so.
86
+ - File an item under the domain its fix touches, not the domain the complaint arrived from.
87
+ - Name a live board task an item overlaps, and keep the verdict beside it.
88
+ - Revise a verdict the tree has moved under rather than appending a second one narrating the change.
89
+ - Report unread items by count on a resume pass. Never decide one.
90
+
91
+ ## Anti-patterns
92
+
93
+ - **Silence read as consent.** An empty slot on a folder read over weeks means nobody reached the item, and treating it as acceptance ships a change nobody approved.
94
+ - **A verdict with nothing behind it.** An item whose problem line carries no number is an opinion, and it reads exactly like the ones that were measured.
95
+ - **The overlap that ate the verdict.** Replacing `Worth it:` with `Overlaps:` drops the call on the items most likely to change what a live task should do.
96
+ - **A question in two places.** An open question answerable in the index and on the item resolves to whichever a reader happens to open.
97
+ - **The dump filed as one concern.** Forty findings under one heading is a folder nobody can promote from, and the split by domain is what makes each item liftable on its own.
@@ -16,7 +16,7 @@ CLI catalog and invocation rules for agents, split by command domain. Start with
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
19
- - [Output shape](output-shape.md): The two framed shapes every command renders into, and how JSON and --names modes keep stdout clean
19
+ - [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
20
20
  - [Overview](overview.md): What this folder covers, the invocation rules every command inherits, and where domain behavior is documented instead
21
21
  - [Sandbox](sandbox.md): Scenario routing, the expectation scoring surface, and the coverage census over scenarios and skills
22
22
  - [Scripting](scripting.md): The runtime catalogs that replace hardcoded names, what each carries, and a headless invocation per domain
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  title: Output shape
3
- description: The two framed shapes every command renders into, and how JSON and --names modes keep stdout clean
3
+ description: Two framed shapes every command renders into, how JSON and --names modes keep stdout clean, and the exit discipline that lets piped output drain
4
4
  ---
5
5
 
6
6
  # Output shape
@@ -43,3 +43,13 @@ Help skips the banner. The `Usage:` line sits directly on `โ”œ`. Help writes to
43
43
  ## JSON and `--names` modes
44
44
 
45
45
  `--json` and `--names` keep stdout clean and machine-readable. The frame still renders on stderr (open, banner, close) so the stream discipline is consistent across modes. Consumers that only read stdout see pure data.
46
+
47
+ ## Process exit
48
+
49
+ A command action sets `process.exitCode` and returns. Calling `process.exit()` there ends the process before a stdout write drains, which truncates piped output at the 64K pipe buffer while still reporting the right exit code. Redirecting to a file hides the truncation, so it surfaces only through a pipe, which is what a check has to use to catch it.
50
+
51
+ The rule reaches an error path that writes to stderr alone, where the truncation has nothing to cut. Scoping it to the stdout writers was the alternative and it makes the floor depend on a detail that moves, since an action grows a stdout write long after its error branches are written. An action always has a return path, so the requirement costs it a line.
52
+
53
+ The exit belongs to a helper that has no caller to unwind through. A prompt inside a promise executor and a wrapper propagating a child process status both qualify, and neither can hand a value back to a caller expecting one. A validation helper called from an action does not: it throws from a `never` return, which keeps its caller exhaustive to the compiler while the action catches and owns the code.
54
+
55
+ Diagnostics reach stderr in every mode, including `--json`. Name the file and the field that failed, because a JSON record carries an action and a reason and an operator reading stderr alone sees neither.
@@ -15,7 +15,7 @@ See `CLAUDE.md` design principles. They apply to every command in this folder.
15
15
 
16
16
  ## Where to start
17
17
 
18
- - `output-shape.md`: the stream contract every command renders into, which is what a caller parsing stdout depends on
18
+ - `output-shape.md`: the stream contract every command renders into and the exit discipline behind it, which is what a caller parsing stdout depends on
19
19
  - `commands.md`: the full command catalog, project-level and per-domain
20
20
  - `scripting.md`: the runtime catalogs that replace hardcoded names, plus headless invocation examples
21
21
 
@@ -44,6 +44,7 @@ One session works for most features. Prefer splitting across two sessions only w
44
44
 
45
45
  Work in Claude Code directly. It reads `CLAUDE.md` automatically and has full file access, no pasting needed.
46
46
 
47
+ - When the input is a pile of findings rather than one feature, invoke `aitk:claude-intake` first. It files the dump into `.claude/intake/<slug>/`, one item per finding carrying a problem measured against the tree, a proposed fix, and a verdict, then names which items are plan-ready, which need measuring, and which are already settled. The routing test is whether the repository can answer an item today, so a session grepping handles the yes and the next bullet handles the no.
47
48
  - When the current state is unmeasured and more than one approach is live, invoke `aitk:claude-groundwork` first. It opens a track folder under `.claude/groundwork/<slug>/` and ends in a decision, which may be to do nothing. Skip it when the approach is already settled. A track may run experiments to settle a question, writing a fixture it reads itself under `.claude/.tmp/groundwork-fixtures/<slug>/` and spawning up to three billed headless runs before it asks. A fixture a headless run is pointed at sits outside the repository, since a session started under the project root inherits that project's `CLAUDE.md` and rules and would measure them instead of the arm.
48
49
  - Invoke `aitk:claude-feature` to scan for code-level conflicts and ambiguities, confirm approach before proceeding
49
50
  - Implement the feature, then Claude Code runs the commands defined in `CLAUDE.md`, fixes failures, and iterates until all pass
@@ -101,7 +102,8 @@ It announces and moves nothing, so `aitk:claude-tasks` stays the only writer. A
101
102
  For features on a mature stack, chain the post-plan pipeline in one session. Approve the plan, invoke `aitk:claude-autoship`, and the skill runs implement โ†’ verify โ†’ review โ†’ ship sequentially.
102
103
 
103
104
  - Use when the plan is tight and the stack has real verify commands and test coverage
104
- - Autoship stops on: verify failure after one fix attempt, UI manual checklist non-empty, any review finding above minor, no diff baseline resolving against `main`, an empty changed-file list, or hook failure
105
+ - Autoship stops on: verify failure after one fix attempt, UI manual checklist non-empty, an inherited review finding above minor, no diff baseline resolving against `main`, an empty changed-file list, or hook failure
106
+ - Review findings split by origin before severity is read. One the branch inherited stops the chain, and one the run itself caused is repaired in place at any severity, bounded at a single pass. Origin is causation rather than authorship, so staleness the run induced in a file it never opened counts as its own and the plan's file list bounds what it builds rather than what it may repair.
105
107
  - Review is skipped when the diff is prose that only informs: every changed file matches `*.md` or `*.txt`, and none sits under a behavior path. Behavior paths cover skills, rules, standards, snippets, and `tooling/` in both the authoring and the installed spelling, plus root `CLAUDE.md`, so the list matches whether a repository authors those surfaces or consumed them from the toolkit. Markdown under one states what an agent does, so a branch touching it reaches review while `docs/` and `wiki/` still skip and stay gated by `docs-sync`, `claude-standards-audit`, and pre-push hooks.
106
108
  - An empty changed-file list stops the chain rather than counting as prose-only. The filename test passes vacuously on an empty set, which routed a branch past review instead of through it.
107
109
  - Every stop leaves recoverable state. Fix and resume with `/git-ship`
@@ -131,6 +133,7 @@ Before the first feature session on a UI-heavy project, pick a design tier. The
131
133
 
132
134
  | Skill | When to use |
133
135
  | ---------------------------- | -------------------------------------------------------------------------------------------------------------------- |
136
+ | `aitk:claude-intake` | File a brain dump into an inventory under `.claude/intake/`, one item per finding with a verdict |
134
137
  | `aitk:claude-groundwork` | Before a plan is warranted, measure an unknown in a track folder under `.claude/groundwork/` |
135
138
  | `aitk:claude-feature` | Before implementation, scan for conflicts and ambiguities |
136
139
  | `aitk:claude-roadmap` | Sequence MVP scope into ordered versions in `.claude/ROADMAP.md` |
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@erclx/aitk",
3
3
  "type": "module",
4
- "version": "0.53.0",
4
+ "version": "0.55.0",
5
5
  "description": "Infrastructure and quality tooling for developer workflows",
6
6
  "license": "MIT",
7
7
  "bin": {
@@ -13,6 +13,11 @@ WRITE="${VERIFY_WRITE:-true}"
13
13
  SCOPED=true
14
14
  CHANGED_FILES=""
15
15
 
16
+ # Scenarios declaring no expectation, taken from `aitk sandbox coverage` against a
17
+ # clean tree. Raising it is a deliberate edit that says which scenario shipped
18
+ # unarmed and why.
19
+ SANDBOX_UNDECLARED_CEILING=47
20
+
16
21
  check_dependencies() {
17
22
  command -v bun >/dev/null 2>&1 || log_error "bun is not installed"
18
23
  }
@@ -138,6 +143,13 @@ seed_entry_count() {
138
143
  awk '{ total += $1 } END { print total + 0 }'
139
144
  }
140
145
 
146
+ # One numeric summary key out of a coverage report. The per-scenario objects carry
147
+ # neither key this is called with, so the match reaches the top level alone and
148
+ # the caller does not depend on the order the keys are emitted in.
149
+ sandbox_summary_field() {
150
+ printf '%s' "$2" | grep -o "\"$1\":[0-9]\+" | grep -o '[0-9]\+'
151
+ }
152
+
141
153
  assert_no_drift() {
142
154
  local paths=$1
143
155
  local err_msg=$2
@@ -274,8 +286,47 @@ main() {
274
286
  run_check "cd $PROJECT_ROOT && bun src/cli.ts claude skills audit --requirements-only" "A skill folder carries no REQUIREMENT.md. Run bun src/cli.ts claude skills audit."
275
287
  log_info "Skill requirements present"
276
288
 
289
+ # `aitk sandbox coverage` moves only when a person runs it, so a scenario added
290
+ # with no expectation ships unnoticed. The gate is an absolute count of
291
+ # undeclared scenarios rather than a ratio or a floor under the declared count.
292
+ # A floor under the declared count passes the case this exists to catch, since
293
+ # adding an unarmed scenario leaves that number where it was. A ratio moves
294
+ # when a scenario is legitimately deleted, and this ceiling does not: deleting
295
+ # an unarmed scenario lowers it and deleting an armed one leaves it alone.
296
+ log_step "Sandbox coverage"
297
+ local coverage_output coverage_status=0 total armed undeclared
298
+ coverage_output=$(cd "$PROJECT_ROOT" && bun src/cli.ts sandbox coverage --json 2>/dev/null) || coverage_status=$?
299
+ if [ "$coverage_status" -ne 0 ]; then
300
+ if [ "${CI:-false}" = true ]; then
301
+ log_error "bun src/cli.ts sandbox coverage --json exited $coverage_status. The scenario tree ships in the checkout, so a run that does not report is a broken command rather than an absent tree, and skipping would report the pass this stage exists to withhold."
302
+ fi
303
+ log_warn "Skipped, the scenario tree did not report"
304
+ else
305
+ # `|| x=""` on both, because a grep that matches nothing exits non-zero and
306
+ # errexit would take the script down at the assignment, before the guard
307
+ # below could name what went missing.
308
+ total=$(sandbox_summary_field totalScenarios "$coverage_output") || total=""
309
+ armed=$(sandbox_summary_field armedScenarios "$coverage_output") || armed=""
310
+ if [ -z "$total" ] || [ -z "$armed" ]; then
311
+ log_error "The coverage report carried no scenario totals, so the stage measured nothing. Run bun src/cli.ts sandbox coverage --json."
312
+ fi
313
+ undeclared=$((total - armed))
314
+ if [ "$undeclared" -gt "$SANDBOX_UNDECLARED_CEILING" ]; then
315
+ log_error "$undeclared of $total scenarios declare no expectation, over the ceiling of $SANDBOX_UNDECLARED_CEILING. Declare expectations on the new scenario, or raise SANDBOX_UNDECLARED_CEILING in this script and say which scenario shipped unarmed."
316
+ fi
317
+ log_info "$armed of $total scenarios declare expectations, $undeclared undeclared against a ceiling of $SANDBOX_UNDECLARED_CEILING"
318
+ fi
319
+
320
+ # The plugin is the second delivery path and this is the only stage gating it,
321
+ # so the skip below is for a contributor's machine rather than for the merge
322
+ # gate. A runner installs the CLI as a workflow step, which makes an absent
323
+ # binary there a broken workflow, and skipping would report a pass for every
324
+ # manifest on the way to a marketplace install.
277
325
  log_step "Plugin manifests"
278
326
  if ! command -v claude >/dev/null 2>&1; then
327
+ if [ "${CI:-false}" = true ]; then
328
+ log_error "claude is not installed. CI installs it before this stage, so read the Install Plugin CLI step in .github/workflows/verify.yml."
329
+ fi
279
330
  log_info "Skipped, claude is not installed"
280
331
  else
281
332
  local manifests manifest
@@ -26,7 +26,8 @@ export function register(program: Command): void {
26
26
  process.stderr.write(
27
27
  `${GREY}โ”Œ${NC}\n${GREY}โ”‚${NC} ${RED}โœ—${NC} ${opts.source} not found\n${GREY}โ””${NC}\n`,
28
28
  )
29
- process.exit(1)
29
+ process.exitCode = 1
30
+ return
30
31
  }
31
32
  process.stderr.write(
32
33
  `${GREY}โ”Œ${NC}\n${GREY}โ”‚${NC} ${WHITE}Render design tokens${NC}\n`,
@@ -60,12 +60,14 @@ export function register(program: Command): void {
60
60
  frameError(
61
61
  'No feedback on stdin. Pipe a markdown block: pbpaste | aitk feedback',
62
62
  )
63
- process.exit(1)
63
+ process.exitCode = 1
64
+ return
64
65
  }
65
66
  const body = (await readStdin()).trim()
66
67
  if (!body) {
67
68
  frameError('Empty feedback body. Provide a markdown block on stdin.')
68
- process.exit(1)
69
+ process.exitCode = 1
70
+ return
69
71
  }
70
72
 
71
73
  if (opts.github) {
@@ -83,7 +85,8 @@ export function register(program: Command): void {
83
85
  frameError(
84
86
  'gh unavailable and no toolkit source to fall back to. Install gh, or file it at https://github.com/erclx/aitk/issues/new',
85
87
  )
86
- process.exit(1)
88
+ process.exitCode = 1
89
+ return
87
90
  }
88
91
  process.stderr.write(
89
92
  `${YELLOW}! gh unavailable, wrote local scratch instead${NC}\n`,
@@ -94,7 +97,8 @@ export function register(program: Command): void {
94
97
  frameError(
95
98
  'Local scratch needs the toolkit source. Re-run with --github to open an issue instead.',
96
99
  )
97
- process.exit(1)
100
+ process.exitCode = 1
101
+ return
98
102
  }
99
103
 
100
104
  const filePath = writeLocal(body)
@@ -36,35 +36,39 @@ export function register(program: Command): void {
36
36
  mirror?: string
37
37
  open?: boolean
38
38
  }) => {
39
- const sourcePath = resolve(process.cwd(), opts.source)
40
- const outDir = resolve(process.cwd(), opts.out)
41
- if (!existsSync(sourcePath)) {
42
- fail(`${opts.source} not found`)
43
- }
44
- const variant = parseVariant(opts.variant)
45
- const mirror = resolveMirror(opts.mirror)
46
- intro('Render slides')
47
- const result = await renderSlidesDoc(sourcePath, outDir, {
48
- variant,
49
- mirror,
50
- })
51
- process.stderr.write(
52
- `${GREY}โ”‚${NC} ${GREEN}โœ“${NC} ${result.slideCount} slides\n${GREY}โ”‚${NC} ${GREEN}โœ“${NC} ${result.pptxPath}\n`,
53
- )
54
- if (result.mirrorPath) {
39
+ try {
40
+ const sourcePath = resolve(process.cwd(), opts.source)
41
+ const outDir = resolve(process.cwd(), opts.out)
42
+ if (!existsSync(sourcePath)) {
43
+ fail(`${opts.source} not found`)
44
+ }
45
+ const variant = parseVariant(opts.variant)
46
+ const mirror = resolveMirror(opts.mirror)
47
+ intro('Render slides')
48
+ const result = await renderSlidesDoc(sourcePath, outDir, {
49
+ variant,
50
+ mirror,
51
+ })
55
52
  process.stderr.write(
56
- `${GREY}โ”‚${NC} ${GREEN}โœ“${NC} mirrored to ${result.mirrorPath}\n`,
53
+ `${GREY}โ”‚${NC} ${GREEN}โœ“${NC} ${result.slideCount} slides\n${GREY}โ”‚${NC} ${GREEN}โœ“${NC} ${result.pptxPath}\n`,
57
54
  )
55
+ if (result.mirrorPath) {
56
+ process.stderr.write(
57
+ `${GREY}โ”‚${NC} ${GREEN}โœ“${NC} mirrored to ${result.mirrorPath}\n`,
58
+ )
59
+ }
60
+ if (opts.open) {
61
+ const target = result.mirrorPath ?? result.pptxPath
62
+ const opened = await openDeck(target)
63
+ const mark = opened ? `${GREEN}โœ“${NC}` : `${RED}โœ—${NC}`
64
+ process.stderr.write(
65
+ `${GREY}โ”‚${NC} ${mark} ${opened ? 'opened' : 'could not open'} ${target}\n`,
66
+ )
67
+ }
68
+ outro()
69
+ } catch (error) {
70
+ reportFailure(error)
58
71
  }
59
- if (opts.open) {
60
- const target = result.mirrorPath ?? result.pptxPath
61
- const opened = await openDeck(target)
62
- const mark = opened ? `${GREEN}โœ“${NC}` : `${RED}โœ—${NC}`
63
- process.stderr.write(
64
- `${GREY}โ”‚${NC} ${mark} ${opened ? 'opened' : 'could not open'} ${target}\n`,
65
- )
66
- }
67
- outro()
68
72
  },
69
73
  )
70
74
 
@@ -98,9 +102,27 @@ function resolveMirror(value: string | undefined): string | undefined {
98
102
  return mirror ? resolve(process.cwd(), mirror) : undefined
99
103
  }
100
104
 
105
+ /**
106
+ * Carries a fail-fast message from a validation helper to the action that
107
+ * called it. `fail` has to keep its `never` return, since that is what makes
108
+ * `parseVariant` exhaustive to the compiler, and a helper deep in the call
109
+ * stack cannot set `process.exitCode` and unwind on its own.
110
+ */
111
+ class SlidesError extends Error {}
112
+
101
113
  function fail(message: string): never {
114
+ throw new SlidesError(message)
115
+ }
116
+
117
+ /**
118
+ * `src/cli.ts` calls `program.parse()` without awaiting it, so a rejected
119
+ * action promise reaches no handler and Bun prints a stack trace. Every action
120
+ * that calls `fail` catches at its own boundary.
121
+ */
122
+ function reportFailure(error: unknown): void {
123
+ if (!(error instanceof SlidesError)) throw error
102
124
  process.stderr.write(
103
- `${GREY}โ”Œ${NC}\n${GREY}โ”‚${NC} ${RED}โœ—${NC} ${message}\n${GREY}โ””${NC}\n`,
125
+ `${GREY}โ”Œ${NC}\n${GREY}โ”‚${NC} ${RED}โœ—${NC} ${error.message}\n${GREY}โ””${NC}\n`,
104
126
  )
105
- process.exit(1)
127
+ process.exitCode = 1
106
128
  }
@@ -42,7 +42,7 @@ export function register(program: Command): void {
42
42
  process.stderr.write(
43
43
  `${GREY}โ”‚${NC} ${RED}โœ—${NC} ${message}\n${GREY}โ””${NC}\n`,
44
44
  )
45
- process.exit(1)
45
+ process.exitCode = 1
46
46
  }
47
47
  })
48
48
  }
package/standards/rule.md CHANGED
@@ -19,6 +19,14 @@ Does not govern:
19
19
  - Skill folders and skill frontmatter: `skill.md`
20
20
  - Cross-domain behavior rules, which live in `CLAUDE.md` at the project root
21
21
 
22
+ ## Whether a skill belongs behind the rule
23
+
24
+ A rule fires on a path match with no decision from the session, which is what makes it a floor. Every bullet is one directive and nothing else, so an invariant needing procedure, worked cases, or a branch on project state has no room in the body.
25
+
26
+ Run the two-part test in reverse before calling the rule finished. The rule already holds what fires on a path edit and ships silently when violated. Ask what a session still needs past the directive, and give that to a skill the rule points at, because a rule that grows a procedure has become a skill body wearing rule frontmatter.
27
+
28
+ Write both when both apply. A rule stating the directive and a skill stating how to carry it out are one invariant at two depths rather than two copies of it, and `skill.md` carries the same checkpoint for a session arriving from the other side. Nothing checks either one.
29
+
22
30
  ## Location
23
31
 
24
32
  - Rules live at `.claude/rules/<subdirectory>/<n>-<slug>.md`
@@ -22,6 +22,25 @@ Does not govern:
22
22
  - The transform from a branch name to a slug a skill carries in a filename: `slug.md`
23
23
  - The domain conventions a skill cites, each of which belongs to the standard that owns it
24
24
 
25
+ ## Changing a skill
26
+
27
+ Answer all four before editing, and carry the answers into wherever the change is proposed.
28
+
29
+ - What problem does this solve? Name the run that went wrong, rather than the improvement the change makes.
30
+ - Which surface owns the rule today? A rule already stated in a standard, a governance rule, or a sibling skill is cited or moved, never restated in the body.
31
+ - What deterministic check catches a regression? Name it, or say none exists and the step holds on a session reading it.
32
+ - What does this collide with? Name the sibling skill, rule, or requirement it contradicts, or state that nothing does.
33
+
34
+ The second question is the one that decides between a body and a rule, which the next section tests in two parts.
35
+
36
+ ## Whether a rule belongs beside the skill
37
+
38
+ A skill fires when a session invokes it or its description matches the request. A path-scoped rule fires when a session reads a file matching its glob, with no decision from the session at all. The two are layers rather than alternatives, so the rule is the floor and the skill is the depth.
39
+
40
+ Run the two-part test over what the body states before calling the skill finished. Does the invariant fire when a specific path is edited, and does violating it ship silently? An invariant passing both halves belongs in a rule as well, because a session that never invoked the skill still edits that path and needs the floor under it. An invariant failing either half stays here, which is most of a body, since procedure and orientation are what a rule cannot carry.
41
+
42
+ Write that rule to the shape `rule.md` sets and leave the procedure here, since the two carry one invariant at two depths rather than two copies of it. Nothing checks the split. The checkpoint is a judgment prompt rather than an invariant, so it ships as prose with no gate behind it, and a skill that skips it fails silently in the same way the invariants it is meant to catch do.
43
+
25
44
  ## Skill types
26
45
 
27
46
  Pick the type before writing. It decides the body shape.
@@ -82,6 +82,17 @@ A standard failing these questions is non-conforming even when it satisfies ever
82
82
 
83
83
  ## Changing a standard
84
84
 
85
+ ### The checkpoint
86
+
87
+ Answer all four before editing, and carry the answers into wherever the change is proposed. This section governs itself, so the next edit to this file answers them too.
88
+
89
+ - What problem does this solve? Name the artifact that went wrong, rather than the improvement the change makes.
90
+ - Which surface owns the rule today? A rule already stated somewhere moves or is cited, never restated in a second place.
91
+ - What deterministic check catches a regression? Name it, or say none exists and the rule holds on reading alone.
92
+ - What does this collide with? Name the sibling standard, rule, or template it contradicts, or state that nothing does.
93
+
94
+ ### What justifies a change
95
+
85
96
  - Change a standard on a failure, not on a finding. A finding is that the docs say X or a paper suggests Y. A failure is a conforming artifact that satisfied every shape rule and still missed the success criterion.
86
97
  - Park findings wherever the project tracks pending work, or in the standard's own backlog section when it tracks none. They are hypotheses to test, not instructions to apply.
87
98
  - Cite the failing artifact in the change that fixes it, so the next reader can tell which rules were paid for by evidence.
@@ -1,114 +1,96 @@
1
1
  # Tooling base reference
2
2
 
3
- ## Runtime
4
-
5
- - Use `bun` as package manager and script runner.
6
- - Use `bunx` instead of `npx` for one-off executables.
7
-
8
- ## Prettier
9
-
10
- - Config: `.prettierrc` (JSON) at root.
11
- - Rules: `semi: false`, `singleQuote: true`.
12
- - Add parser overrides for non-standard extensions (e.g., `.mdx` โ†’ `markdown`).
13
- - Ignore paths via `.gitignore` and `.prettierignore`. Pass both as `--ignore-path` on all prettier invocations.
14
- - `.prettierignore` is a user-owned seed. It is created empty on install. Projects add their own entries.
15
- - Use `--log-level warn` on all prettier invocations to suppress per-file `(unchanged)` output.
16
-
17
- ## Dev Dependencies
18
-
19
- - `prettier`, `cspell`, `husky`, `@commitlint/cli`, `@commitlint/config-conventional`.
20
- - Install via `bun add -D`.
21
- - Ensure `.gitignore` contains `node_modules/`.
22
-
23
- ## CSpell
24
-
25
- - `cspell.json` is a user-owned seed at root. Sync drops it once on first install and never overwrites it. Projects extend it with extra `import` and `dictionaryDefinitions` entries.
26
- - The seeded baseline includes `version: "0.2"`, `language: "en"`, `useGitignore: true`, `gitignoreRoot: ["."]`, dictionary definitions for `project-terms` and `tech-stack`, and `ignorePaths: [".cspell/**", ".git/**"]` to skip dictionary self-checks and git object files. Both dictionary definitions set `addWords: true`.
27
- - `gitignoreRoot: ["."]` pins the `.gitignore` search to the project root. Without it, cspell run from inside a linked worktree under `.claude/worktrees/` walks up into the parent repo's `.gitignore`, resolves every worktree file as living under the ignored worktree path, and checks zero files. New words then pass locally and fail in CI. `gitignoreRoot` stops the walk at the root while the worktree's own `.gitignore` still excludes `node_modules` and build output.
28
- - Dictionary files in `.cspell/`: `project-terms.txt`, `tech-stack.txt`.
29
- - Include dotfolders in the spell glob: `cspell '**' '.*/**' '.*' ...`. The default `**` skips dot-prefixed folders, so `.claude/`, `.github/`, and `.husky/` go unchecked without explicit globs.
30
- - Keep dictionary entries sorted alphabetically, one word per line.
31
-
32
- ## Shell Tooling
33
-
34
- - Format: `shfmt --indent 2 scripts/`. shfmt supports directory args natively, no `find` needed.
35
- - Lint: `find scripts -name '*.sh' -exec shellcheck --severity=warning {} +`. shellcheck has no directory mode, `find` is required.
36
- - Config: `.shellcheckrc` with `external-sources=true`. Required for shellcheck to follow `source` directives. Keep even with EditorConfig present.
37
- - All shell scripts live in `scripts/`. Do not place `.sh` files outside `scripts/`.
38
- - EditorConfig: `.editorconfig` at root with `[*.sh]` block enforcing `indent_style = space`, `indent_size = 2`. Prevents editor/shfmt conflicts that produce spurious git diffs.
39
-
40
- ## Commit Lint
41
-
42
- - Config: `commitlint.config.js` (ESM default export).
43
- - Extends: `@commitlint/config-conventional`.
44
- - Rules: `header-max-length: 72`, `scope-case: lower-case`, `subject-full-stop: never`, `subject-case: disabled`.
45
- - Format: `<type>(<scope>): <subject>` (imperative mood, no trailing period).
46
-
47
- ## Husky + Lint-Staged
48
-
49
- - `.lintstagedrc` is a user-owned seed at root. Sync drops it once on first install and never overwrites it. Projects extend it with extra glob โ†’ command entries (e.g. `aitk indexes regen`).
50
- - Seeded baseline globs:
51
- - `**/*.{json,md,mdc}` โ†’ `["prettier --write --ignore-path .gitignore --ignore-path .prettierignore", "cspell --no-must-find-files"]`
52
- - `**/*.md` โ†’ `["aitk indexes regen"]`
53
- - `**/*.sh` โ†’ `["shfmt --write --indent 2", "shellcheck --severity=warning"]`
54
- - Hooks in `.husky/`:
55
- - `pre-commit` โ†’ `bunx lint-staged`
56
- - `commit-msg` โ†’ `bunx commitlint --edit "$1"`
57
- - `pre-push` โ†’ `bun run check`
58
- - `post-merge` โ†’ names `.claude/tasks/` archive candidates, silent otherwise and when the board is absent
59
- - `post-rewrite` โ†’ delegates to `post-merge` on `rebase`, so a `pull.rebase=true` machine still gets the check
60
- - Husky runs hooks as `sh -e`, so a hook carrying logic is POSIX sh under errexit no matter what its shebang says.
61
- - Note: lint-staged handles its own glob expansion and passes matched files as arguments. `**/*.sh` is safe here, unlike in package.json scripts.
62
-
63
- ## GitHub
64
-
65
- - PR template: `.github/pull_request_template.md`.
66
- - Sections: `## Summary`, `## Key Changes`, `## Technical Context`, `## Testing`.
67
- - Visuals: HTML comment only, never a visible section header.
68
- - Imperative mood, no "This PR" opener, no buzzwords, name specific files and functions.
69
- - CI workflow: `.github/workflows/verify.yml`. Runs on pull requests targeting `main` and on `workflow_dispatch`.
70
- - Workflow steps: checkout, setup Bun (latest), `bun install --frozen-lockfile`, install `shfmt` and `shellcheck` via apt, then `check:format`, `check:spell`, `check:shell`.
71
- - Does not run `format` before asserting. CI asserts only. Format must be clean before push.
3
+ ## Overview
72
4
 
73
- ## Gitignore
5
+ The base layer covers every project the toolkit scaffolds, whatever language sits on top. It ships formatting, spelling, shell linting, conventional commits, git hooks, CI, and three maintenance scripts. Every other stack extends it, so a decision made here is one every stack inherits.
74
6
 
75
- - `# System`: `.DS_Store`
76
- - `# Dependencies`: `node_modules/`
77
- - `# Secrets`: `.env`, `.env.*`, `*.local`, `!.env.example`
7
+ ## What ships as golden configs
8
+
9
+ Golden config files live in `tooling/base/configs/` and are copied into the target on `aitk tooling sync base .`. They are the source of truth. The reference covers rationale and tradeoffs. Configs show the concrete setup.
10
+
11
+ - `.prettierrc`: `semi: false`, `singleQuote: true`, plus a parser override per non-standard extension (`.mdx` to `markdown`).
12
+ - `.shellcheckrc`: `external-sources=true`. Required for shellcheck to follow `source` directives.
13
+ - `.editorconfig`: `root = true`, with an `[*.sh]` block setting `indent_style = space` and `indent_size = 2`.
14
+ - `commitlint.config.js`: ESM default export extending `@commitlint/config-conventional`. Rules are `header-max-length: 72`, `scope-case: lower-case`, `subject-full-stop: never`, and `subject-case` disabled.
15
+ - `.husky/`: `pre-commit`, `commit-msg`, `pre-push`, `post-merge`, `post-rewrite`.
16
+ - `.github/workflows/verify.yml`: runs on pull requests targeting `main` and on `workflow_dispatch`.
17
+ - `.github/pull_request_template.md`: `## Summary`, `## Key Changes`, `## Technical Context`, `## Testing`.
18
+ - `.vscode/extensions.json` and `.vscode/settings.json`: editor wiring for Prettier, cspell, shfmt, and shellcheck.
19
+ - `scripts/verify.sh`, `scripts/clean.sh`, `scripts/update.sh`: the maintenance entry points behind `check`, `clean`, and `update`.
20
+
21
+ ## What ships as user-owned seeds
22
+
23
+ Seeds live in `tooling/base/seeds/`. Sync drops each once on first install and never overwrites it, so a project extends them freely.
24
+
25
+ - `cspell.json`: `version: "0.2"`, `language: "en"`, `useGitignore: true`, `gitignoreRoot: ["."]`, dictionary definitions for `project-terms` and `tech-stack` with `addWords: true` on both, and `ignorePaths: [".cspell/**", ".git/**"]` to skip dictionary self-checks and git object files.
26
+ - `.cspell/project-terms.txt` and `.cspell/tech-stack.txt`: one word per line, sorted alphabetically.
27
+ - `.lintstagedrc`: the glob map below.
28
+ - `.prettierignore`: created empty. Projects add their own entries.
29
+ - `.claude/context/ci.md` and `.claude/context/development.md`: extend with project-specific commands, workflows, or deploy steps. Canonical rationale stays in this reference.
78
30
 
79
- ## Scripts
31
+ ## Tool pairing
80
32
 
81
- - Entry: `scripts/` directory with `verify.sh`, `clean.sh`, `update.sh`.
82
- - All scripts use logging functions from the bash script reference.
83
- - `verify.sh`: self-healing: runs `format` first to auto-fix AI-generated or drifted code, then asserts with `check:format`. Supports `VERIFY_NESTED=true` to suppress timeline boundaries when called by other scripts.
84
- - `clean.sh`: removes `node_modules/`, clears bun cache, reinstalls dependencies fresh.
85
- - `update.sh`: runs `bun update --interactive` then calls `verify.sh` with `VERIFY_NESTED=true` to confirm project health after updates.
33
+ - Runtime: `bun` as package manager and script runner, `bunx` over `npx` for a one-off executable.
34
+ - Formatting: Prettier for what it parses, shfmt for shell. Two formatters because Prettier has no shell parser.
35
+ - Spelling: cspell over the whole tree, with project vocabulary split into a project-terms dictionary and a tech-stack one.
36
+ - Shell: shfmt formats and shellcheck lints at warning severity. shfmt takes a directory argument, shellcheck has no directory mode and needs `find`.
37
+ - Commits: commitlint against conventional commits, wired through the husky `commit-msg` hook. Format is `<type>(<scope>): <subject>` in imperative mood with no trailing period.
38
+ - Dev dependencies: `prettier`, `cspell`, `husky`, `@commitlint/cli`, `@commitlint/config-conventional`. Install via `bun add -D`.
86
39
 
87
- ## EditorConfig
40
+ ## File layout
88
41
 
89
- - Config: `.editorconfig` at root, `root = true`.
90
- - `[*.sh]`: `indent_style = space`, `indent_size = 2`.
91
- - Ensures consistent shell script indentation across editors, preventing shfmt vs editor conflicts that produce spurious git diffs.
42
+ - All shell scripts live in `scripts/`. Do not place a `.sh` file outside it.
43
+ - Dictionaries live in `.cspell/`, hooks in `.husky/`, seeded context docs in `.claude/context/`.
44
+ - The `.claude/context/` location matches the three-tier context model: project-wide invariants in `CLAUDE.md`, `.claude/REQUIREMENTS.md`, and `.claude/ARCHITECTURE.md`, path-scoped rules in `.claude/rules/`, and on-demand domain narrative in `.claude/context/`. Indexes stay opt-in.
92
45
 
93
- ## VS Code
46
+ ## Hooks
94
47
 
95
- - Extensions: `esbenp.prettier-vscode`, `streetsidesoftware.code-spell-checker`, `mkhl.shfmt`, `timonwong.shellcheck`, `mads-hartmann.bash-ide-vscode`.
96
- - Settings: `shellcheck.customArgs: ["--severity=warning"]`.
48
+ - `pre-commit` runs `bunx lint-staged`.
49
+ - `commit-msg` runs `bunx commitlint --edit "$1"`.
50
+ - `pre-push` runs `bun run check`.
51
+ - `post-merge` names `.claude/tasks/` archive candidates, staying silent otherwise and when the board is absent.
52
+ - `post-rewrite` delegates to `post-merge` on `rebase`, so a `pull.rebase=true` machine still gets the check.
97
53
 
98
- ## Context
54
+ ## lint-staged
99
55
 
100
- - Seeded at `.claude/context/development.md` and `.claude/context/ci.md` on install. User-owned, never overwritten by sync.
101
- - Each carries `title` and `description` frontmatter so the files slot into `.claude/context/index.md` if the project adopts the `indexes` system. Indexes stay opt-in.
102
- - Extend freely with project-specific commands, workflows, or deploy steps. Canonical rationale stays in this reference.
103
- - The `.claude/context/` location matches the three-tier context model: project-wide invariants in `CLAUDE.md` and `.claude/REQUIREMENTS.md`/`.claude/ARCHITECTURE.md`, path-scoped rules in `.claude/rules/`, and on-demand domain narrative in `.claude/context/`.
56
+ Seeded baseline globs:
104
57
 
105
- ## Package Scripts
58
+ - `**/*.{json,md,mdc}` runs `prettier --write --ignore-path .gitignore --ignore-path .prettierignore` then `cspell --no-must-find-files`
59
+ - `**/*.md` runs `aitk indexes regen`
60
+ - `**/*.sh` runs `shfmt --write --indent 2` then `shellcheck --severity=warning`
61
+
62
+ ## CI
63
+
64
+ - Steps: checkout, setup Bun at latest, `bun install --frozen-lockfile`, install `shfmt` and `shellcheck` via apt, then `check:format`, `check:spell`, and `check:shell`.
65
+ - CI asserts and never writes. Format must be clean before push.
66
+
67
+ ## Gitignore
68
+
69
+ - `# System`: `.DS_Store`
70
+ - `# Dependencies`: `node_modules/`
71
+ - `# Secrets`: `.env`, `.env.*`, `*.local`, `!.env.example`
106
72
 
107
- - `check:spell`: runs cspell across all files, shows context on failures.
108
- - `check:format`: checks prettier and shfmt formatting without writing. shfmt targets `scripts/` directory directly. Uses `--log-level warn` and both `--ignore-path .gitignore --ignore-path .prettierignore`.
109
- - `check:shell`: runs shellcheck at warning severity via `find scripts -name '*.sh'` (shellcheck has no directory mode).
110
- - `format`: writes prettier and shfmt formatting in place. shfmt targets `scripts/` directory directly. Uses `--log-level warn` and both `--ignore-path .gitignore --ignore-path .prettierignore`.
111
- - `prepare`: initializes husky hooks (runs automatically on `bun install`).
112
- - `check`: runs `scripts/verify.sh`, the full verification suite. Auto-formats before asserting.
113
- - `clean`: runs `scripts/clean.sh`, wipes and reinstalls dependencies.
114
- - `update`: runs `scripts/update.sh`, interactive dependency update with verification.
73
+ ## Anti-patterns
74
+
75
+ Sticky negative knowledge. Do not relearn.
76
+
77
+ - Do NOT drop `gitignoreRoot: ["."]` from `cspell.json`. Without it, cspell run from inside a linked worktree under `.claude/worktrees/` walks up into the parent repo's `.gitignore`, resolves every worktree file as living under the ignored worktree path, and checks zero files. New words then pass locally and fail in CI.
78
+ - Do NOT rely on the default `**` glob for spelling. It skips dot-prefixed folders, leaving `.claude/`, `.github/`, and `.husky/` unchecked. Pass `'**' '.*/**' '.*'` explicitly.
79
+ - Do NOT invoke prettier without both `--ignore-path .gitignore --ignore-path .prettierignore`. Passing one drops the other, since the flag replaces the default rather than adding to it.
80
+ - Do NOT omit `--log-level warn` from a prettier invocation. The default prints a line per unchanged file.
81
+ - Do NOT put logic in a husky hook and expect its shebang to hold. Husky runs hooks as `sh -e`, so a hook carrying logic is POSIX sh under errexit whatever the first line says.
82
+ - Do NOT drop the `[*.sh]` block from `.editorconfig` because shfmt already sets indentation. The editor and shfmt then disagree and produce spurious git diffs.
83
+ - Do NOT copy lint-staged's `**/*.sh` glob into a package.json script. lint-staged expands its own globs and passes matched files as arguments, which a bare shell script does not.
84
+
85
+ ## CLI
86
+
87
+ | Script | What it does |
88
+ | ---------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
89
+ | `bun run check` | Full verification suite via `scripts/verify.sh`. Runs `format` first to auto-fix drifted code, then asserts. Honors `VERIFY_NESTED=true` to suppress timeline boundaries when another script calls it. |
90
+ | `bun run check:format` | Asserts prettier and shfmt formatting without writing |
91
+ | `bun run check:spell` | Runs cspell across every file, with context on failures |
92
+ | `bun run check:shell` | Runs shellcheck at warning severity |
93
+ | `bun run format` | Writes prettier and shfmt formatting in place |
94
+ | `bun run prepare` | Initializes husky hooks, run automatically on `bun install` |
95
+ | `bun run clean` | Removes `node_modules/`, clears the bun cache, reinstalls fresh |
96
+ | `bun run update` | Runs `bun update --interactive`, then `verify.sh` with `VERIFY_NESTED=true` |
@@ -1,6 +1,13 @@
1
1
  #!/usr/bin/env bash
2
2
 
3
- input=$(cat)
3
+ # Claude Code sends a payload and closes stdin. A bare read with nothing feeding
4
+ # it blocks forever and holds the session open, so the read is bounded. `read`
5
+ # rather than `timeout cat`, which macOS does not ship.
6
+ IFS= read -r -d '' -t 2 input
7
+ [ -n "$input" ] || {
8
+ printf '%s reads a Claude Code hook payload on stdin and cannot be run by hand.\n' "${0##*/}" >&2
9
+ exit 1
10
+ }
4
11
 
5
12
  tool=$(printf '%s' "$input" | jq -r '.tool_name // empty')
6
13
  case "$tool" in
@@ -3,10 +3,18 @@
3
3
  # Regenerates .claude/memory/index.md after a memory file changes.
4
4
  #
5
5
  # The memory folder is gitignored, so the whole-repo walk in `bun run check`
6
- # drops it and never regenerates this index. A positional path bypasses that
7
- # filter, which makes this hook the only trigger that reaches the folder.
6
+ # drops it and never regenerates this index. Naming the file as a positional
7
+ # argument to `aitk indexes regen` below bypasses that filter, which makes this
8
+ # hook the only trigger that reaches the folder.
8
9
 
9
- input=$(cat)
10
+ # Claude Code sends a payload and closes stdin. A bare read with nothing feeding
11
+ # it blocks forever and holds the session open, so the read is bounded. `read`
12
+ # rather than `timeout cat`, which macOS does not ship.
13
+ IFS= read -r -d '' -t 2 input
14
+ [ -n "$input" ] || {
15
+ printf '%s reads a Claude Code hook payload on stdin and cannot be run by hand.\n' "${0##*/}" >&2
16
+ exit 1
17
+ }
10
18
 
11
19
  tool=$(printf '%s' "$input" | jq -r '.tool_name // empty')
12
20
  case "$tool" in
@@ -1,6 +1,13 @@
1
1
  #!/usr/bin/env bash
2
2
 
3
- input=$(cat)
3
+ # Claude Code sends a payload and closes stdin. A bare read with nothing feeding
4
+ # it blocks forever and holds the session open, so the read is bounded. `read`
5
+ # rather than `timeout cat`, which macOS does not ship.
6
+ IFS= read -r -d '' -t 2 input
7
+ [ -n "$input" ] || {
8
+ printf '%s reads a Claude Code hook payload on stdin and cannot be run by hand.\n' "${0##*/}" >&2
9
+ exit 1
10
+ }
4
11
 
5
12
  tool=$(printf '%s' "$input" | jq -r '.tool_name // empty')
6
13
  case "$tool" in
@@ -1,6 +1,14 @@
1
1
  #!/usr/bin/env bash
2
2
 
3
- input=$(cat)
3
+ # Claude Code sends a payload and closes stdin. A bare read with nothing feeding
4
+ # it blocks forever and holds the session open, so the read is bounded. `read`
5
+ # rather than `timeout cat`, which macOS does not ship.
6
+ IFS= read -r -d '' -t 2 input
7
+ [ -n "$input" ] || {
8
+ printf '%s reads a Claude Code hook payload on stdin and cannot be run by hand.\n' "${0##*/}" >&2
9
+ exit 1
10
+ }
11
+
4
12
  file=$(printf '%s' "$input" | jq -r '.tool_input.file_path // .tool_response.filePath // empty')
5
13
 
6
14
  file="${file//\\//}"
@@ -3,10 +3,18 @@
3
3
  # Regenerates .claude/tasks/index.md after a task file changes.
4
4
  #
5
5
  # The task folder is gitignored, so the whole-repo walk in `bun run check`
6
- # drops it and never regenerates this index. A positional path bypasses that
7
- # filter, which makes this hook the only trigger that reaches the folder.
6
+ # drops it and never regenerates this index. Naming the file as a positional
7
+ # argument to `aitk indexes regen` below bypasses that filter, which makes this
8
+ # hook the only trigger that reaches the folder.
8
9
 
9
- input=$(cat)
10
+ # Claude Code sends a payload and closes stdin. A bare read with nothing feeding
11
+ # it blocks forever and holds the session open, so the read is bounded. `read`
12
+ # rather than `timeout cat`, which macOS does not ship.
13
+ IFS= read -r -d '' -t 2 input
14
+ [ -n "$input" ] || {
15
+ printf '%s reads a Claude Code hook payload on stdin and cannot be run by hand.\n' "${0##*/}" >&2
16
+ exit 1
17
+ }
10
18
 
11
19
  tool=$(printf '%s' "$input" | jq -r '.tool_name // empty')
12
20
  case "$tool" in