@erclx/aitk 0.101.0 → 0.102.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.101.0",
4
+ "version": "0.102.0",
5
5
  "author": {
6
6
  "name": "Eric Le",
7
7
  "url": "https://github.com/erclx"
@@ -25,6 +25,7 @@ session's review are different passes, and how a feature is sized.
25
25
  Read the board in parallel, resolving the paths at the main worktree root per Worktrees in `CLAUDE.md`:
26
26
 
27
27
  - `.claude/tasks/priority.md`: execution order and what each task is waiting on
28
+ - `.claude/tasks/backlog.md`: what is not being scheduled, when the file exists
28
29
  - `.claude/tasks/index.md`: what is queued
29
30
  - `.claude/plans/*.md`: features already planned and ready to hand off
30
31
  - `.claude/ROADMAP.md`: sequencing rationale, when the file exists
@@ -34,6 +35,8 @@ Then output the state of play so the human knows what to launch, review, and mer
34
35
 
35
36
  `priority.md` is the ordering source. `index.md` sorts by filename and says nothing about order, so read the sequence from the first and never infer it from the second. When `priority.md` is absent, report the queue and say the order is unrecorded.
36
37
 
38
+ Row position under `## Needs a plan` is what gets planned next, top first, per `.claude/standards/tasks.md`. `backlog.md` carries what nobody is scheduling and is explicitly unordered, so read it for what exists and never as a queue. Report a backlog count rather than its rows, since listing them puts the length back in front of the reader the cut took it away from.
39
+
37
40
  The roadmap is optional and this skill does not require it. It carries why a sequence is what it is, changes only when strategy changes, and is absent in a project whose scope has already shipped. Report what it says and name it as the source. Never assert an active version the file does not state, and say nothing about one when the file is missing.
38
41
 
39
42
  A compaction is a moment this skill cannot detect, so the human asks for each side of it and this skill reads the matching runbook when they do.
@@ -155,7 +158,8 @@ It counts unclaimed plans against workers rather than reading the reserve in ste
155
158
  - A task whose outcomes are all `[x]` runs `claude-docs` for the plan sweep, then `claude-tasks` to archive
156
159
  - A task whose outcomes describe standing policy rather than a deliverable never closes on its own, so hand it to a worker to encode the policy where it is enforced, then cut the outcomes with the reason recorded and archive once that branch merges. Encoding it from this session would write a tracked file, which Boundaries forbids.
157
160
  4. Read `.claude/tasks/priority.md` and count entries under its `## Run now` heading that carry a written plan. Keep one in reserve beyond what is running.
158
- 5. Promote by whether a task establishes functionality rather than by age. Prefer a task that adds or proves a mechanism over one that trims, tidies, or audits an existing surface.
161
+ 5. Promote from the top of `## Needs a plan`, which is where the last sweep recorded what to plan next. Depart from that order when something has changed under it and say what changed, since a position nobody honors is the ordering going stale on the surface built to hold it. What sets the order in the first place is whether a task establishes functionality rather than how old it is, so prefer a task that adds or proves a mechanism over one that trims, tidies, or audits an existing surface.
162
+ - Re-take the board-or-backlog call while the file is open. A row that has stopped being near-term moves to `backlog.md`, and a backlogged task the last wave made near-term moves onto the board. Both are one line removed from one surface and written to the other, and the standard states the test.
159
163
  6. Before promoting a candidate, list the files it touches against every task already running, per Parallelism below. Name the overlap and serialize when the sets are not disjoint.
160
164
  - A candidate held by something outside the tree stays where it is whatever those sets show. A collision is one of the reasons a task cannot start, so disjointness clears that reason alone and leaves an external condition standing.
161
165
  7. Write a plan for each newly promoted task with `claude-feature`, carrying the in-flight constraint that The loop above states, then report:
@@ -166,6 +170,7 @@ Findings placed: <finding> → <destination>
166
170
  Archived: <task>
167
171
  Promoted: <task>, touches <surfaces>, parallel with <task> because <disjoint sets>
168
172
  Serialized: <task> behind <task>, both write <file>
173
+ Backlogged: <task>, because <what stopped being near-term>
169
174
  Ready now: <tasks with plans, and what each waits on>
170
175
  ```
171
176
 
@@ -179,9 +184,11 @@ Do not promote a task to fill the queue when nothing qualifies. A thin queue is
179
184
 
180
185
  ### Writing the board
181
186
 
182
- Promoting, demoting, and archiving a row all write `.claude/tasks/priority.md`, and this session is the only writer apart from `aitk tasks archive`.
187
+ Promoting, demoting, and archiving a row all write `.claude/tasks/priority.md`, and this session is the only writer apart from `aitk tasks archive`. Moving a task between the board and `.claude/tasks/backlog.md` writes both files, and this session is that file's only writer.
183
188
 
184
189
  - Edit the file with the file-editing tool. A shell stream editor and an inline string replace both exit clean on a non-match, so a promotion that matched nothing leaves the board wrong with nothing reporting it, and the file-editing tool errors instead.
190
+ - Write both halves of a move before reporting it. A row removed from one surface and not written to the other leaves a task file nothing names, and the folder is gitignored with no history to recover the row from. `aitk tasks validate` reports that state, so run it after any move.
191
+ - Put the reason a row sits where it does in its Waiting on cell. Position is the ordering and the cell is where the ordering's rationale lives, so a row promoted with the cell left alone carries an order the next sweep cannot check.
185
192
  - Put a pointer in the Plan column, never prose. `## Run now` claims a written plan covers every open outcome, and `claude-autoship` refuses at its guard when it follows the column and finds no plan, which spends a worker dispatch to learn what the row should have said.
186
193
  - Name the file set in the Touches column. The disjointness call in step 6 is only checkable later when the sets are written down rather than reasoned once and discarded.
187
194
  - Re-resolve every Plan pointer after anything archives a plan
@@ -10,7 +10,7 @@ Sweep the board as orchestrator after merging. Run this once per batch of merges
10
10
  3. Re-verify every plan already written, not only the ones this sweep writes. A queued plan goes stale from whatever merged while it waited, and the loop's verify step fires at handoff rather than after a merge, so nothing else catches it. Grep each construct the plan names and count the sites against its claim, then open each file rather than trusting its account.
11
11
  4. Re-check any precondition a plan states about live state outside the repository. A remote branch, an open issue, or an installed version was true when the plan was written and is not a fact about the tree.
12
12
 
13
- Run `aitk tasks validate` once the board is rewritten and before reporting it. It resolves every plan pointer, maps rows against task files both ways, tests the `## Run now` file sets for overlap, and re-takes the two blocker kinds a command can settle. It reports and never writes, so fix each row it names and run it again. A finding it reports is a board defect rather than a task finding, so it goes nowhere but the board.
13
+ Run `aitk tasks validate` once the board is rewritten and before reporting it. It resolves every plan pointer, accounts every task file against the board and the backlog both, tests the `## Run now` file sets for overlap, and re-takes the two blocker kinds a command can settle. It reports and never writes, so fix each row it names and run it again. A finding it reports is a board defect rather than a task finding, so it goes nowhere but the board.
14
14
 
15
15
  Then run `orchestrator-parked.md` over the rows the validator listed as untested. Those carry the three blocker kinds no command settles, and the merge this sweep followed changed the tree under every one of them at once while the refill above re-read none. Take the untested rows alone rather than the whole board, since the validator already answered the rest.
16
16
 
@@ -43,7 +43,15 @@ Never write a `Pull request:` line here. `git-pr` adds it when a pull request op
43
43
 
44
44
  Write it immediately. Claude Code's tool permission dialog is the confirmation gate. Do not pause for approval.
45
45
 
46
- ### Step 4: report unlinked origins
46
+ ### Step 4: place it on a surface
47
+
48
+ A task file with no row is a dropped task, so name the surface it lands on in the same pass that creates it. A task that would plausibly be planned within the next few waves takes a row under `## Needs a plan` in `.claude/tasks/priority.md`, positioned by where it sits against the rows already there, with the reason for that position in its `Waiting on` cell. Anything else takes a line in `.claude/tasks/backlog.md`, which is unordered and where in the file it goes means nothing.
49
+
50
+ The test and both file shapes are in `.claude/standards/tasks.md`. From a linked worktree the file-editing tools refuse the main root, so a row lands through the same `Bash` route the file itself took.
51
+
52
+ Say which surface it went to and why in the report. The call is a judgment restated on every sweep rather than a property of the task, and a placement with no stated reason is one the next sweep re-derives from nothing.
53
+
54
+ ### Step 5: report unlinked origins
47
55
 
48
56
  Scan for work that has been decided and would otherwise be forgotten. List `.claude/groundwork/` and run `gh issue list --state open` when a remote is configured, then grep the board for each track name and issue number. Report any with no task, one line each.
49
57
 
@@ -105,6 +113,7 @@ Create:
105
113
  ✅ Created: .claude/tasks/vXX.Y-<slug>.md
106
114
 
107
115
  <label> sits between <neighbor> and <neighbor>.
116
+ <board or backlog, and why it landed there>.
108
117
 
109
118
  **Origin with no task:**
110
119
 
@@ -12,7 +12,7 @@ Resolve `.claude/plans/`, `.claude/memory/`, and `.claude/tasks/` at the main wo
12
12
  Read these in parallel, skipping any that do not exist:
13
13
 
14
14
  - the newest `.claude/tasks/session-*.md`: the handoff a previous session wrote before a compaction, per `.claude/standards/session.md`, or `${CLAUDE_SKILL_DIR}/../../standards/session.md` when the project does not have it. It leads the report rather than the reads.
15
- - `.claude/tasks/index.md`: the folder catalog. Read this before any individual task file, and take the backlog from it by dropping the `index`, `priority`, and `session-` rows, which are siblings rather than tasks.
15
+ - `.claude/tasks/index.md`: the folder catalog. Read this before any individual task file, and take the task list from it by dropping the `index`, `priority`, `backlog`, and `session-` rows, which are siblings rather than tasks.
16
16
  - `.claude/plans/*.md`: execution detail for in-progress tasks
17
17
  - `.claude/memory/index.md` and any memory files relevant to the top backlog item
18
18
 
@@ -25,5 +25,5 @@ CLI catalog and invocation rules for agents, split by command domain. Start with
25
25
  - [Scripting](scripting.md): The runtime catalogs that replace hardcoded names, what each carries, and a headless invocation per domain
26
26
  - [Sessions](sessions.md): Resolving live peer sessions to the worktree and branch each holds, the liveness confidence field, the unresolved reasons, and what the read depends on
27
27
  - [Skill audit](skills-audit.md): Measuring both skill corpora against standards/skill.md, the checks it reads, the requirement gate that is the only failing one, and the drift verb that names bodies rewritten since a ref
28
- - [Tasks](tasks.md): Selecting a shipped task by stem or pull request, recording a number and closing an outcome, the refusal reasons, the board checks validate runs, and why the board root defaults to the main worktree
28
+ - [Tasks](tasks.md): Selecting a shipped task by stem or pull request, recording a number and closing an outcome, the refusal reasons, the board and backlog checks validate runs, and why the board root defaults to the main worktree
29
29
  - [Teach](teach.md): Listing learning workspaces and the ordinal a new one takes, opening one with its required files, recording sources and glossary terms, the refusal reasons, and why every write here runs through a verb
@@ -27,21 +27,25 @@ A bare run measures every markdown file git lists, tracked plus untracked-and-no
27
27
 
28
28
  ## Where the rules come from
29
29
 
30
- Both ban sets and all five checkpoints are read out of the standards per run, resolved under `.claude/standards/` first, the authoring root second, and the corpus inside the `aitk` package last, so a target project measures against the copy it actually has and a project that installed none is measured all the same. Holding the lists in code was the alternative and it puts each ban in two places, where an author adding one gets no enforcement until someone edits TypeScript.
30
+ The three ban sets and all six checkpoints ship with the `aitk` package as data, in `src/markdown/bans.ts` and `src/markdown/structure.ts`. Every project is measured against the same sets whether or not it installed any standards, and no file has to resolve for a run to mean something.
31
31
 
32
- A project copy wins wherever one exists. An installed standard is a seed a project edits, so a package copy overriding it would discard that edit with nothing said, and the fallback answers absence rather than staleness. The package copy is reachable because `package.json` ships `standards/` beside the code doing the reading, which is what leaves the install optional for every machine consumer.
32
+ Reading them out of the standards per run was the original design. It put a parser contract on two documents authored for people, and `prose.md` had to carry a paragraph of its own warning an author that a one-word backticked example in a `- Do not use ` bullet would be lifted into a literal ban set and ban that word everywhere. A rule existing to protect a parser from the prose it parses is the argument for separating them.
33
33
 
34
- The report names the copy it read on every run, spelling a package copy under `<aitk>/` rather than as a path anything would join to a project root. A published package can lag the toolkit a session reads from, the same two-speed skew `.claude/ARCHITECTURE.md` records between the plugin cache and the CLI, and naming the source is what keeps that diagnosable instead of invisible.
34
+ `markdown.md` and `prose.md` still state every ban and every checkpoint, and a reader follows those rather than the code. Nothing compares the two, so a number moved in one place and left in the other drifts silently. Move both in the same change.
35
35
 
36
- `aitk standards <name>` resolves the same three roots in the same order and prints the standard, so the human catalog reads without a project copy on disk too.
36
+ The sets are closed rather than extensible, so a project cannot add a term by editing a file. What decides that is the measurement behind them: 21 terms across 483 markdown files report a clean exit, and every occurrence of a banned word in the corpus sits inside the ban list itself or inside an example demonstrating the ban. The set is a prior an author already knows rather than a filter that has caught anything, and enumeration cannot close the gap it aims at, since `just`, `allows`, and `very` carry honest uses no literal match separates.
37
37
 
38
- The trade is a reader of prose that a reformat can break. `src/markdown/bans.test.ts` answers it by asserting the parsed sets against the shipped standards, so a rewrite that narrows a set fails there rather than passing quietly. A checkpoint falls back per number rather than per file, and the depth legend names every checkpoint that fell back on the run that used one.
38
+ Freezing the spellings gave up a property worth naming. They were derived by applying the standard's own suffix pairs to its own examples, so an example added there extended the check with no code edit. The set is now carried whole, and `analyse` stays out of it for the reason it was always out: the standard's example is `analyze`, which ends in `-yze` rather than the `-ize` its rule states.
39
+
40
+ A set shipped empty is reported rather than passed. It finds nothing and would exit clean, which reports a corpus nobody checked as a corpus carrying no violation, so the run names the empty set and exits `1`. The sets ship with the package, so a defect in the build is the only cause left.
41
+
42
+ `aitk standards <name>` still resolves a standard under `.claude/standards/`, then the authoring root, then the package corpus, and prints it, so the human catalog reads without a project copy on disk.
39
43
 
40
44
  ## What each check reports
41
45
 
42
46
  ### Bans
43
47
 
44
- Three closed sets report a hit: the characters `markdown.md` bans under `## Punctuation`, the single lowercase words `prose.md` bans under `## Language`, and the British spellings derived by applying that section's own suffix rules to its own examples.
48
+ Three closed sets report a hit: the characters `markdown.md` bans under `## Punctuation`, the single lowercase words `prose.md` bans under `## Language`, and the British spellings of the American examples that section lists.
45
49
 
46
50
  Deriving the spellings rather than pattern-matching a suffix is what keeps `exercises`, `promises`, and `revised` out of the report. A suffix pattern over the same corpus produced 46 false positives from words of that shape, and a closed set of whole words reaches none of them.
47
51
 
@@ -96,7 +100,9 @@ A bullet, a heading, a table row, a blockquote, a blank line, and a fence each e
96
100
 
97
101
  ## Exit codes
98
102
 
99
- Exit codes are `0` for a completed run with no gating finding, `1` for a refusal, and `2` for a ban hit. A banned character, word, or spelling fails the run. Bullet, paragraph, and depth weight are judgments a reader settles, so all three report under every code.
103
+ Exit codes are `0` for a completed run with no gating finding, `1` for a refusal, `2` for a ban hit, and `3` for a shipped ban set that arrived empty. A banned character, word, or spelling fails the run. Bullet, paragraph, and depth weight are judgments a reader settles, so all three report under every code.
104
+
105
+ `3` is separate from `1` because the two want different responses from a caller. A refusal means no corpus was built, and the `Markdown bans` stage in `scripts/core/verify.sh` is right to warn and skip. An empty set means the corpus was walked and nothing was looked for, so that stage fails the push on `3` rather than skipping.
100
106
 
101
107
  `2` rather than `1` for the gate keeps a measurement that succeeded and found something distinct from the audit declining to measure at all. A caller reading one as the other sends a reader hunting a defect that does not exist, which is the distinction `aitk context audit` and the `verify.sh` seed stage already draw between the same two codes.
102
108
 
@@ -118,13 +124,17 @@ A hit the closed set cannot separate from correct prose is the case with no thir
118
124
 
119
125
  ### Where the rules are enforced
120
126
 
121
- Four surfaces read the ban sets and two of them go through this verb. `.claude/hooks/standards-audit.sh` runs it against a single file after each markdown edit, and the `Markdown bans` stage in `scripts/core/verify.sh` runs it across the whole corpus before a push. The hook parsed its own copy of the word bans in awk until the gate landed, which left a British spelling passing at edit time and failing the push with nothing in between explaining the difference.
127
+ Four surfaces apply the ban sets and three of them go through this verb. `.claude/hooks/standards-audit.sh` runs it against a single file after each markdown edit, the seed copy a project installs does the same, and the `Markdown bans` stage in `scripts/core/verify.sh` runs it across the whole corpus before a push. Each hook parsed its own copy of the word bans in awk before that, which left a British spelling passing at edit time and failing the push with nothing in between explaining the difference.
128
+
129
+ The seed copy moved onto the verb when the sets became data, since its awk had nothing left to parse. It resolves one runner where the toolkit copy resolves two, looking for no checkout source, and a machine carrying no `aitk` gets a report naming the binary to install rather than a silent pass. `scripts/core/check-seed-independence.sh` scopes its walk to markdown and leaves the seed hooks outside it, which its own comment records as deliberate.
130
+
131
+ The fourth surface reads the standards directly and is not a consolidation left half done. `claude/skills/claude-standards-audit/SKILL.md` greps the banned tokens agent-side, which is a session reading prose rather than a process it can shell out to, and it ships to every target. It is the likeliest place for the next drift, since nothing compares it against the verb.
122
132
 
123
- The other two read the standards directly and neither is a consolidation left half done. `claude/skills/claude-standards-audit/SKILL.md` greps the banned tokens agent-side, which is a session reading prose rather than a process it can shell out to, and it ships to every target. The seed copy of the hook keeps its awk, because a scaffolded project may carry no `aitk` and `scripts/core/check-seed-independence.sh` exists to catch seed content depending on the toolkit CLI. Both are the likelier place for the next drift, since nothing compares either against the verb.
133
+ The hook prefers a checkout's own `src/cli.ts` over a globally installed binary, so it and the push stage read one build. A published binary lags a branch by whatever has not been released, which would put a ban kind added on the branch into the push and not into the edit. It reads its findings out of the `--json` record rather than off the exit code, so an older binary still reports where the fallback applies. It reads `bans.emptySets` out of the same record, so a set the verb shipped empty reaches the author as a check narrowed to what it could measure rather than as a clean pass.
124
134
 
125
- The hook prefers a checkout's own `src/cli.ts` over a globally installed binary, so it and the push stage read one build. A published binary lags a branch by whatever has not been released, which would put a ban kind added on the branch into the push and not into the edit. It reads its findings out of the `--json` record rather than off the exit code, so an older binary still reports where the fallback applies. It reads `bans.missingStandards` out of the same record, so a standard the verb found under none of the three roots reaches the author as a check narrowed to what it could read.
135
+ That field replaced `bans.missingStandards`, which answered a standard resolving under none of three roots. The sets ship with the package now, so the state it named cannot occur and the narrowed check has one cause left, a defect in the build. The hook keeps reading a field either way, since a reader cannot tell a narrowed check from a clean one without it.
126
136
 
127
- The package fallback leaves that message reachable in one case rather than none. A CLI running out of a package carrying the corpus always resolves a standard, so the narrowed-check path fires only for a checkout whose own source tree lacks the file. The message stays right for that case and the hook keeps reading the field, since a reader cannot tell a narrowed check from a clean one without it.
137
+ Both hooks answer an absent record as well. A completed run always writes the record and a refusal writes none, so an empty one means the verb declined to measure rather than measured and found nothing. The verb needs a git repository to build its corpus and refuses without one, which is a project the seeded hook can be installed into, and reading the findings alone reported that as a clean file.
128
138
 
129
139
  A machine with neither runner still blocks no edit, and it says so rather than exiting clean. The push stage holds either way. An edit nobody checked and an edit carrying no violation are one silence to a reader, so the enforcement a machine lacks is reported rather than inferred.
130
140
 
@@ -1,6 +1,6 @@
1
1
  ---
2
2
  title: Tasks
3
- description: Selecting a shipped task by stem or pull request, recording a number and closing an outcome, the refusal reasons, the board checks validate runs, and why the board root defaults to the main worktree
3
+ description: Selecting a shipped task by stem or pull request, recording a number and closing an outcome, the refusal reasons, the board and backlog checks validate runs, and why the board root defaults to the main worktree
4
4
  ---
5
5
 
6
6
  # Tasks
@@ -95,11 +95,15 @@ Five checks run. Plan and Collisions reach one half each of the `## Run now` tes
95
95
  | Check | What it reports |
96
96
  | ---------- | ------------------------------------------------------------------------------------ |
97
97
  | Plan | A `## Run now` row whose Plan column carries no link, or one resolving to no file |
98
- | Mapping | A row naming no task file, and a task file no row names |
99
- | Grouping | A task carrying a row in more than one readiness group |
98
+ | Mapping | A row or backlog line naming no task file, and a task file neither surface names |
99
+ | Grouping | A task carrying a row in more than one readiness group, or on both surfaces |
100
100
  | Collisions | Two `## Run now` rows whose Touches columns name a path in common |
101
101
  | Blockers | A parked row whose blocker has stopped holding, or whose cited task resolves nowhere |
102
102
 
103
+ Mapping spans two surfaces, because a task sits on `priority.md` when it would plausibly be planned soon and on `backlog.md` otherwise. A task file either surface names is accounted for, a file neither names is `row-missing`, and a file both name is `row-duplicated` for the reason a task in two groups is: it claims two things about itself and only one can hold. One check across both is what lets a task move between them without the move reading as a dropped file.
104
+
105
+ A backlog line is a bullet carrying a link to a sibling task, since the backlog is a flat unordered list rather than a table. A bullet holding prose is skipped rather than reported, which keeps the file's own intro out of the findings, and the task that bullet meant to name is still reported as reaching neither surface. A project carrying no `backlog.md` reads as an empty backlog rather than a refusal, which leaves the one-to-one mapping this check ran before the second surface existed.
106
+
103
107
  The collision check is the one a person cannot run by eye. Paths come from the backticked spans in the Touches column, a span naming no file is dropped, and a directory collides with any file beneath it. A `## Run now` row whose column parses to nothing is reported rather than skipped, since a row stating no file set makes a claim nothing can check.
104
108
 
105
109
  The blocker check re-takes a measurement the board records once and never repeats. Two of the five blocker kinds put a fact on disk: a dependency is settled by the cited task being archived or closing every outcome, and a collision is settled by nothing under `## Run now` still holding the file the cell cites.
@@ -126,7 +130,7 @@ An untested row is not a finding and moves no exit code. Reading a clean finding
126
130
 
127
131
  Exit codes: `0` every check passed, `1` refused, `2` at least one finding. The `reason` field carries which gate refused: `no-board`, `no-ordering`, or `no-groups`. A board grouping under headings of its own trips `no-groups` rather than being read against columns it never declared.
128
132
 
129
- Columns are read from each table's own header rather than by position, so a project whose board differs from this one is reported for what it lacks. The `index` and `priority` siblings are skipped, along with every pre-compaction handoff, which takes one file per session under a `session-` prefix. None of them is a task, and a handoff counted as one would be reported as a task carrying no row on every session that wrote one.
133
+ Columns are read from each table's own header rather than by position, so a project whose board differs from this one is reported for what it lacks. The `index`, `priority`, and `backlog` siblings are skipped, along with every pre-compaction handoff, which takes one file per session under a `session-` prefix. None of them is a task, and a handoff counted as one would be reported as a task carrying no row on every session that wrote one.
130
134
 
131
135
  Skills branch on the findings rather than on the exit code:
132
136
 
@@ -89,7 +89,7 @@ Run `aitk tooling list --json` and `aitk gov list --json` to see the current cat
89
89
  `governance`, `standards`, and `wiki` are skippable:
90
90
 
91
91
  - `--skip governance`: leave `.claude/rules/` empty. Standards still install, so `.claude/standards/prose.md` lands with nothing pointing at it and no coding standard loads on a file match. The preview names any `--add` rules the skip drops, and the run prints the `aitk gov install <stack> <path>` command to add rules afterward, carrying those extras so one paste restores what the skip declined.
92
- - `--skip standards`: leave standards out. The governance rules still reference `.claude/standards/`, so their authority lines resolve to nothing for a reader following the path. Toolkit skills are unaffected, since each falls back to the copy in its own plugin root, and so is the CLI, which searches the corpus inside its own package behind both project roots. `aitk markdown audit` and `aitk standards <name>` therefore answer in a project that skipped the install, and what the skip costs is the editable copy rather than the enforcement.
92
+ - `--skip standards`: leave standards out. The governance rules still reference `.claude/standards/`, so their authority lines resolve to nothing for a reader following the path. Toolkit skills are unaffected, since each falls back to the copy in its own plugin root. `aitk standards <name>` searches the corpus inside the CLI's own package behind both project roots, so it prints a standard in a project that skipped the install, and `aitk markdown audit` needs no standard at all, its ban sets and checkpoints shipping with the package as data. What the skip costs is the editable copy rather than the enforcement.
93
93
  - `--skip wiki`: skip the `.claude/wiki/` scaffold. A target that already carries a root `wiki/` keeps it, since the verb reports that folder rather than migrating it.
94
94
 
95
95
  That standards fallback carries runtime behavior rather than reference prose alone, because the pre-publish scan and the branch-slug transform each have a standard of their own, `publish.md` and `slug.md`, cited by the skills that run them.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@erclx/aitk",
3
3
  "type": "module",
4
- "version": "0.101.0",
4
+ "version": "0.102.0",
5
5
  "description": "Infrastructure and quality tooling for developer workflows",
6
6
  "license": "MIT",
7
7
  "bin": {
@@ -363,6 +363,9 @@ main() {
363
363
  1)
364
364
  log_warn "Skipped, the markdown audit refused and measured nothing"
365
365
  ;;
366
+ 3)
367
+ log_error "The markdown audit shipped an empty ban set, so the corpus was walked and nothing was looked for. Check src/markdown/bans.ts."
368
+ ;;
366
369
  2)
367
370
  # `|| true` because the re-run exits non-zero by construction, and `set -e`
368
371
  # would take the script down before log_error names the remedy.
@@ -1,14 +1,19 @@
1
1
  import { readFile } from 'node:fs/promises'
2
2
  import { resolve } from 'node:path'
3
3
  import type { Command } from 'commander'
4
- import { type BanReport, banReport, loadStandards } from '@/markdown/bans'
4
+ import { BAN_SETS, emptyBanSets } from '@/markdown/bans'
5
5
  import { resolveMarkdown } from '@/markdown/files'
6
6
  import { isGating } from '@/markdown/gate'
7
- import { type BanFinding, bodyLines, scanBans } from '@/markdown/scan'
8
7
  import {
8
+ type BanFinding,
9
+ type BanSets,
10
+ bodyLines,
11
+ scanBans,
12
+ } from '@/markdown/scan'
13
+ import {
14
+ CHECKPOINTS,
9
15
  type Checkpoints,
10
16
  measureStructure,
11
- parseCheckpoints,
12
17
  type StructureReport,
13
18
  } from '@/markdown/structure'
14
19
  import {
@@ -21,8 +26,19 @@ import {
21
26
  plural,
22
27
  } from '@/ui'
23
28
 
29
+ const EXIT_REFUSED = 1
24
30
  const EXIT_GATE = 2
25
31
 
32
+ /**
33
+ * A set shipped empty, so the run measured a corpus against nothing.
34
+ *
35
+ * Distinct from `EXIT_REFUSED` because the two want different responses. A
36
+ * refusal means no corpus was built and a push stage is right to skip, while an
37
+ * empty set means the corpus was walked and nothing was looked for, which is a
38
+ * defect in the build and has to fail.
39
+ */
40
+ const EXIT_UNUSABLE = 3
41
+
26
42
  interface AuditCommandOptions {
27
43
  readonly json?: boolean
28
44
  }
@@ -58,6 +74,7 @@ export function register(program: Command): void {
58
74
  ' 0 the audit completed with no gating finding',
59
75
  ' 1 refused, with the reason on stderr',
60
76
  ' 2 a banned character, word, or spelling is present',
77
+ ' 3 a shipped ban set is empty, so the run measured nothing',
61
78
  '',
62
79
  'A ban hit is a fact and gates unconditionally. Bullet, paragraph, and',
63
80
  'depth weight are judgments a reader settles, so all three report and',
@@ -68,12 +85,11 @@ export function register(program: Command): void {
68
85
  'where the token is genuinely an identifier under discussion, which is',
69
86
  'what markdown.md reserves the span for.',
70
87
  '',
71
- 'Bans and checkpoints are read from markdown.md and prose.md, resolved',
72
- 'under .claude/standards/, then standards/, then the corpus inside the',
73
- 'aitk package, so a project that installed neither is still measured.',
74
- 'The report names the copy it read. No folder has to resolve and no',
75
- 'index.md has to exist, so .claude/rules/, governance/, and snippets/',
76
- 'are in reach.',
88
+ 'Bans and checkpoints ship with the aitk package rather than being read',
89
+ 'out of a standards file, so a project that installed no standards is',
90
+ 'measured the same as one that did. markdown.md and prose.md still state',
91
+ 'every rule for a reader. No folder has to resolve and no index.md has',
92
+ 'to exist, so .claude/rules/, governance/, and snippets/ are in reach.',
77
93
  '',
78
94
  'Examples:',
79
95
  ' aitk markdown audit',
@@ -110,9 +126,9 @@ async function runAudit(
110
126
  )
111
127
  }
112
128
 
113
- const standards = await loadStandards(root)
114
- const bans = banReport(standards)
115
- const checkpoints = parseCheckpoints(standards.markdown?.text ?? '')
129
+ const bans = BAN_SETS
130
+ const empty = emptyBanSets(bans)
131
+ const checkpoints = CHECKPOINTS
116
132
 
117
133
  const reports: FileReport[] = await Promise.all(
118
134
  scope.files.map(async (rel) => {
@@ -127,7 +143,7 @@ async function runAudit(
127
143
 
128
144
  intro('aitk markdown audit')
129
145
  reportScope(scope.files, scope.unmatched)
130
- reportBans(reports, bans)
146
+ reportBans(reports, bans, empty)
131
147
  reportBullets(reports, checkpoints)
132
148
  reportParagraphs(reports, checkpoints)
133
149
  reportDepth(reports, checkpoints)
@@ -143,8 +159,7 @@ async function runAudit(
143
159
  characters: bans.characters,
144
160
  words: bans.words,
145
161
  spellings: bans.spellings,
146
- sources: bans.sources,
147
- missingStandards: bans.missing,
162
+ emptySets: empty,
148
163
  },
149
164
  checkpoints: {
150
165
  run: checkpoints.run,
@@ -153,7 +168,6 @@ async function runAudit(
153
168
  paragraph: checkpoints.paragraph,
154
169
  sentences: checkpoints.sentences,
155
170
  renderWidth: checkpoints.renderWidth,
156
- fellBack: checkpoints.fellBack,
157
171
  },
158
172
  entries: reports.map((report) => ({
159
173
  path: report.rel,
@@ -167,6 +181,10 @@ async function runAudit(
167
181
  )
168
182
  }
169
183
 
184
+ // An empty set finds nothing and would exit clean, which reports a corpus
185
+ // nobody checked as a corpus carrying no violation.
186
+ if (empty.length > 0) return EXIT_UNUSABLE
187
+
170
188
  const gating = isGating({
171
189
  bans: reports.flatMap((report) => report.bans),
172
190
  structure: reports.map((report) => report.structure),
@@ -180,7 +198,7 @@ function refuse(message: string): number {
180
198
  logStep('Refused')
181
199
  logWarn(message)
182
200
  outro()
183
- return 1
201
+ return EXIT_REFUSED
184
202
  }
185
203
 
186
204
  function reportScope(
@@ -203,18 +221,22 @@ function reportScope(
203
221
  * the sentence and every voice rule is a judgment, so a report listing hits
204
222
  * without naming those would read as a verdict on the whole standard.
205
223
  */
206
- function reportBans(reports: readonly FileReport[], bans: BanReport): void {
224
+ function reportBans(
225
+ reports: readonly FileReport[],
226
+ bans: BanSets,
227
+ empty: readonly string[],
228
+ ): void {
207
229
  logStep('Bans')
208
230
 
209
- if (bans.missing.length > 0) {
231
+ if (empty.length > 0) {
210
232
  logWarn(
211
- `Not measured. Found no copy of: ${bans.missing.join(', ')}. Looked under .claude/standards/, then standards/, then the aitk package.`,
233
+ `Not measured. The shipped set is empty for: ${empty.join(', ')}. The sets ship with the aitk package, so an empty one is a defect in the build rather than a missing install.`,
212
234
  )
213
- if (bans.sources.length === 0) return
235
+ return
214
236
  }
215
237
 
216
238
  logInfo(
217
- `${plural(bans.characters.length, 'character')}, ${plural(bans.words.length, 'word')}, and ${plural(bans.spellings.length, 'spelling')} read from ${bans.sources.join(' and ')}`,
239
+ `${plural(bans.characters.length, 'character')}, ${plural(bans.words.length, 'word')}, and ${plural(bans.spellings.length, 'spelling')} shipped with the aitk package`,
218
240
  )
219
241
  logInfo(
220
242
  'Frontmatter, fenced blocks, code spans, and link destinations are excluded.',
@@ -379,12 +401,6 @@ function reportDepth(
379
401
  'A run that is entirely table rows is excluded too, since a heading inside a table splits the table rather than the run.',
380
402
  )
381
403
 
382
- if (checkpoints.fellBack.length > 0) {
383
- logWarn(
384
- `Read no number from the standard for: ${checkpoints.fellBack.join(', ')}. Measured against the shipped default instead.`,
385
- )
386
- }
387
-
388
404
  const over = reports
389
405
  .filter((report) => report.structure.longestRun > checkpoints.run)
390
406
  .sort((a, b) => b.structure.longestRun - a.structure.longestRun)
@@ -104,7 +104,7 @@ export function register(program: Command): void {
104
104
  '',
105
105
  'Checks:',
106
106
  ' every Run now row points at a plan file that resolves',
107
- ' every row maps to a task file and every task file to a row',
107
+ ' every task file carries a board row or a backlog line, never both',
108
108
  ' no task carries more than one row',
109
109
  ' no two Run now rows touch the same file',
110
110
  '',
@@ -450,12 +450,14 @@ function reportValidation(
450
450
  intro('aitk tasks validate')
451
451
  logStep('Board')
452
452
  logInfo(
453
- `${outcome.rows} row(s) across the readiness groups, ${outcome.tasks} task file(s)`,
453
+ `${outcome.rows} row(s) across the readiness groups, ${outcome.backlog} backlog line(s), ${outcome.tasks} task file(s)`,
454
454
  )
455
455
 
456
456
  logStep(outcome.findings.length === 0 ? 'Clean' : 'Findings')
457
457
  if (outcome.findings.length === 0) {
458
- logInfo('every row resolves, maps one to one, and touches its own files')
458
+ logInfo(
459
+ 'every row resolves, every task sits on one surface, and each touches its own files',
460
+ )
459
461
  } else {
460
462
  for (const finding of outcome.findings) logWarn(describe(finding))
461
463
  }
@@ -481,6 +483,7 @@ function reportValidation(
481
483
  ok: true,
482
484
  root,
483
485
  rows: outcome.rows,
486
+ backlog: outcome.backlog,
484
487
  tasks: outcome.tasks,
485
488
  findings: outcome.findings,
486
489
  untested: outcome.untested,