@erclx/aitk 0.40.0 → 0.41.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "aitk",
3
3
  "description": "Automated governance, versioning, and discovery tools for Claude Code.",
4
- "version": "0.40.0",
4
+ "version": "0.41.1",
5
5
  "author": {
6
6
  "name": "Eric Le",
7
7
  "url": "https://github.com/erclx"
@@ -7,13 +7,15 @@ description: What the post-plan pipeline is for, the gaps it closes, and why eve
7
7
 
8
8
  ## Gap
9
9
 
10
- Without this skill, the run from an approved plan to an open pull request is a conversation. A session implements, then asks what comes next, and the answer varies by session. Review gets skipped on a diff that needed one, or spent on prose already gated by a hook. A run that halts leaves no stated resume point, so the user reconstructs how far it got from the working tree.
10
+ Without this skill, the run from an approved plan to an open pull request is a conversation. A session implements, then asks what comes next, and the answer varies by session. A run that halts leaves no stated resume point, so the user reconstructs how far it got from the working tree.
11
+
12
+ Review is the step that varies most. It gets skipped on a diff that needed one, or spent on prose already gated by a hook. A file-extension test cannot separate the two, since a skill body and a documentation page are both markdown, so a branch changing what an agent does takes the skip a documentation branch earned.
11
13
 
12
14
  ## Must
13
15
 
14
16
  - Take the approved plan for the branch as the scope, and implement only what it describes
15
17
  - Give every step a stop condition, and leave the code on the branch and the receipts on disk at each one
16
- - Classify the changed-file list before review, so a prose-only diff skips a code review with no signal on it
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
17
19
  - Stop on any critical or should-fix finding rather than acting on it
18
20
  - Open the pull request as a draft, then watch continuous integration to a terminal state
19
21
  - Name the recovery for the stop it took, since the value of stopping is that the user knows where to resume
@@ -25,6 +27,7 @@ Without this skill, the run from an approved plan to an open pull request is a c
25
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.
26
28
  - Run the memory Apply phase. Promoting an entry changes how the agent operates and ships as its own change.
27
29
  - 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
+ - Read a markdown extension as evidence the change only informs. A skill body, a governance rule, and a standard are behavior written in prose.
28
31
 
29
32
  ## Guards
30
33
 
@@ -71,13 +71,26 @@ An empty list stops the chain: `❌ No changed files to classify. Re-run when th
71
71
 
72
72
  The two causes want different responses. A plan that has yet to produce its output is a re-run once it has. A plan whose output is gitignored by design, such as a read pass writing to `.claude/.tmp/`, is work the chain cannot carry at all, since `git-stage` finds nothing to commit six steps later. Never advise removing the output from `.gitignore`, which trades a stopped run for scratch committed into the repository.
73
73
 
74
- If every changed file matches `*.md` or `*.txt`, skip review entirely and continue to Step 7. Prose-only changes are already gated by `docs-sync`, `claude-standards-audit`, and pre-push hooks. Running a code-style review on them burns tokens with no signal.
74
+ The skip needs both tests to pass: every changed file matches `*.md` or `*.txt`, and no changed file sits under a behavior path. On a pass, skip review entirely and continue to Step 7. Otherwise invoke `aitk:claude-review`.
75
75
 
76
- Otherwise invoke `aitk:claude-review`.
76
+ Behavior paths carry two spellings, the one a surface authors at and the one it installs to, so the rule reads the same in a toolkit and in a project that consumed one:
77
+
78
+ - `claude/skills/` and `.claude/skills/`
79
+ - `governance/rules/` and `.claude/rules/`
80
+ - `standards/` and `.claude/standards/`
81
+ - `snippets/` and `.claude/snippets/`
82
+ - `internal/` and `tooling/`, which hold the stack references and the seed documents a target is handed
83
+ - `CLAUDE.md` at the repository root, named as a file because a path prefix reaches nothing that sits in no folder
84
+
85
+ Markdown under one of them states what an agent does, so a change there is a behavior change wearing a prose extension. Everything outside them is informational, which keeps `docs/`, `README.md`, and `CHANGELOG.md` skipping without naming them. One behavior file sends the whole branch to review, since documentation shipped beside a behavior change does not cancel it.
86
+
87
+ Informational prose is already gated by `docs-sync`, `claude-standards-audit`, and pre-push hooks. Running a code-style review on it burns tokens with no signal.
88
+
89
+ The list covers this toolkit's authoring layout and the layout it installs, which is not every layout. A project keeping executable prose where neither spelling reaches adds the path, and until it does every branch touching it skips review silently.
77
90
 
78
91
  ## Step 6: evaluate findings
79
92
 
80
- Skip this step if Step 5 was skipped (prose-only diff). 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. Parse the summary line (`X critical, Y should-fix, Z minor`):
81
94
 
82
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.`
83
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.
@@ -11,6 +11,10 @@ Without this skill, the session holding the cross-feature picture starts buildin
11
11
 
12
12
  The queue fails in both directions. It empties and a free worker waits, or it fills with whatever is oldest rather than with what establishes a mechanism. A plan handed over unverified is the quiet one, since it goes stale from whatever merged after it was written, and a worker that trusts its account of the tree builds against a shape that no longer exists. A second orchestrator makes all of this unrecoverable, because the board is gitignored and neither session can read the other's writes.
13
13
 
14
+ The session also writes surfaces it is told only how to read. Refilling the queue promotes and demotes rows in the priority file with no stated method, so a session invents one, and an inline rewrite exits clean when it matches nothing and leaves the board wrong with nothing reporting it. A row carrying prose where a plan pointer belongs costs a worker dispatch, because the ship chain refuses at its guard after the worktree is already open. A plan archived from a worker's own branch strands the pointer the board still carries, and the row reads as correct until someone follows it.
15
+
16
+ Output drifts everywhere the contract stops. The specified shape covers invocation alone, so a sweep report, a board report, and an analysis each end in a decision the human owns and each buries it under the evidence they would have skipped. A compaction is the same failure one step earlier, since the file that survives it has a stated reader and no stated writer, and the session improvises what to save and loses the reasoning the board never held.
17
+
14
18
  ## Must
15
19
 
16
20
  - Read the priority file for execution order, since the index sorts by filename and states no order
@@ -20,6 +24,11 @@ The queue fails in both directions. It empties and a free worker waits, or it fi
20
24
  - Place every finding a merged pull request produced against the surface that owns it
21
25
  - Date the roadmap line from that file's last commit, so an old sequence reads as old
22
26
  - Keep one planned, non-conflicting task in reserve beyond what is running
27
+ - Write the priority file with an editing tool that errors on a non-match, since the board is where a silent failure costs a dispatch
28
+ - Carry a plan pointer in the Plan column and a file set in the Touches column, so a row's readiness and disjointness claims stay checkable
29
+ - Re-resolve the board's plan pointers after any archive, since the archiving skill rewrites the task file and knows nothing about the board
30
+ - Lead a sweep report, a board report, and an analysis with the state, the open decisions, and the next action, keeping the evidence below them
31
+ - Write the pre-compact handoff with what no other file carries, and name the snippet that reads it back
23
32
 
24
33
  ## Must not
25
34
 
@@ -29,6 +38,9 @@ The queue fails in both directions. It empties and a free worker waits, or it fi
29
38
  - Hand a worker anything but a plan, because scope lives there
30
39
  - Run a second orchestrator against the same board
31
40
  - Promote a task to fill the queue when nothing qualifies. A thin queue is a real answer.
41
+ - Rewrite the board with a shell stream editor or an inline string replace, both of which exit clean when they match nothing
42
+ - Restate the board, a task file, or a groundwork folder in the pre-compact handoff, which is the padding that makes a handoff stop being read
43
+ - Specify a shape for a correction, since a format for admitting error invites ceremony where plainness is the whole value
32
44
 
33
45
  ## Guards
34
46
 
@@ -35,6 +35,10 @@ Then output the state of play so the human knows what to launch, review, and mer
35
35
 
36
36
  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.
37
37
 
38
+ A compaction is a moment this skill cannot detect, so both sides of it are snippets the human fires. Before one, `@.claude/snippets/claude/orchestrator-handoff.md` writes `.claude/tasks/session.md` with the state of play, the decisions taken under delegated authority, the mistakes worth not repeating, and the standing cautions. After one, `@.claude/snippets/claude/orchestrator-resume.md` reads that file back with the board and the groundwork behind the live work. Write nothing to the handoff that the board, a task file, or a groundwork folder already carries.
39
+
40
+ Name `${CLAUDE_SKILL_DIR}/../../snippets/claude/<name>.md` as the fallback for any orchestrator snippet the project copy lacks. Snippets install by named bundle rather than wholesale, so a project that installed one bundle has the `.claude/snippets/` directory and none of these files, and the plugin ships the whole snippets folder beside `skills/`.
41
+
38
42
  ## Output
39
43
 
40
44
  ```plaintext
@@ -68,10 +72,27 @@ Omit the `Roadmap` line when `.claude/ROADMAP.md` is absent. Quote the `Now` row
68
72
 
69
73
  That command returns nothing for a roadmap that exists but has never been committed, which is the state `claude-roadmap` leaves behind when it writes the file and declines to stage it. Write `uncommitted` as the date in that case. A blank there would read as a formatting slip rather than as the newest possible sequence.
70
74
 
75
+ ### Every later turn
76
+
77
+ The block above covers invocation alone. A sweep report, a board report, and an analysis each end in something the human decides, so each opens with the same three slots and puts its evidence underneath:
78
+
79
+ ```plaintext
80
+ State: <what changed since they last looked>
81
+
82
+ Decisions:
83
+ 1. <one line each, or "none open">
84
+
85
+ Next: <the single most useful action>
86
+ ```
87
+
88
+ Keep the detail below the block and keep it skippable. A decision reached at the bottom of three paragraphs has been buried, which is the failure this shape exists to prevent. Reuse the vocabulary above rather than inventing a second one, and keep a file set on the row claiming it so the reader can check a disjointness claim instead of taking it.
89
+
90
+ Write no shape for a correction. A correction is a sentence, and a format for admitting error invites ceremony where plainness is the whole value.
91
+
71
92
  ## The loop
72
93
 
73
94
  1. Own the roadmap while a scope exists to sequence. Capture a needed draft or resequence of `.claude/ROADMAP.md` in the plan or a task file, naming the MVP list in `.claude/REQUIREMENTS.md` as the source, so a worker runs `claude-roadmap` in its branch and the tracked edit ships in a PR rather than dirtying main. Stop owning it once that list has shipped, since later work then arrives as discrete items rather than as versions.
74
- 2. Plan the next feature. Run `claude-feature` here, with the cross-feature context, to write a plan to `.claude/plans/`. Planning stays in this warm session so the plan front-loads reasoning a cold worker would otherwise re-derive.
95
+ 2. Plan the next feature. Run `claude-feature` here, with the cross-feature context, to write a plan to `.claude/plans/`. Planning stays in this warm session so the plan front-loads reasoning a cold worker would otherwise re-derive. A constraint supplied from here that names a surface to leave alone states which of two acts it forbids, and the rule governing that is Step 3 of `claude-feature` under Constraints.
75
96
  3. Decide parallelism and merge order. Note which plans touch a shared wiring seam so their PRs merge in sequence, not at once.
76
97
  4. Verify the plan against the tree. Reading it is not enough, since a plan goes stale from whatever merged after it was written. Grep for each construct it names and count the sites against the count it claims. Check that every phase label it cites is still open. Open each file it describes rather than trusting its account of the contents. Correct the plan before handing it over.
77
98
  5. Hand off. The human opens a worker worktree with `claude-worktree` and runs `claude-autoship` against the plan. The orchestrator does not spawn workers.
@@ -89,7 +110,7 @@ That command returns nothing for a roadmap that exists but has never been commit
89
110
 
90
111
  ## Refilling the ready queue
91
112
 
92
- Keep enough planned, non-conflicting tasks available that a free worker never waits, and place the findings the last merge produced before promoting anything new. Run this after every merge and whenever the ready list thins.
113
+ Keep enough planned, non-conflicting tasks available that a free worker never waits, and place the findings the last merge produced before promoting anything new. Run this after every merge and whenever the ready list thins. `@.claude/snippets/claude/orchestrator-sweep.md` fires this procedure after a batch of merges and adds the plan re-verification that a merge invalidates.
93
114
 
94
115
  1. Run `gh pr list --state open` and `git log --oneline -8`. Report any pull request whose review has not been posted and stop for that one first.
95
116
  2. For each pull request merged since the last sweep, place every finding it produced. Route a finding that changes a rule to the standard or rule that states it, one that changes another task to that task's Findings, and one that overturns a groundwork lean to that folder marked answered. Never leave a finding in a pull request thread alone.
@@ -107,10 +128,22 @@ Serialized: <task> behind <task>, both write <file>
107
128
  Ready now: <tasks with plans, and what each waits on>
108
129
  ```
109
130
 
131
+ That block is the detail. Lead the reply with the three slots under Every later turn above, so the human reads what they own before the evidence for it.
132
+
110
133
  Treat a task that edits `.claude/context/` entries wholesale as conflicting with every other task, because the root instruction file requires each task to update its own domain entry as it lands.
111
134
 
112
135
  Do not promote a task to fill the queue when nothing qualifies. A thin queue is a real answer and it beats a plan nobody needed.
113
136
 
137
+ ### Writing the board
138
+
139
+ Promoting, demoting, and archiving a row all write `.claude/tasks/priority.md`, and this session is the only writer apart from `aitk tasks archive`.
140
+
141
+ - 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.
142
+ - 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.
143
+ - 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.
144
+ - Re-resolve every Plan pointer after anything archives a plan. `claude-docs` moves a plan to `.claude/.tmp/plans-archive/` and rewrites the citation in the task file alone, so a row for a task still on the board keeps pointing into `.claude/plans/` at a file that has moved. Workers running the ship chain on their own branches archive plans this board still cites, and the board reads as correct until a pointer is followed.
145
+ - Read the file back after writing it, since the row that lands is the row a worker acts on.
146
+
114
147
  ## Parallelism
115
148
 
116
149
  No fixed number caps worker tracks. Collision between file sets is what binds, so
@@ -19,6 +19,8 @@ It reports rather than gates, the closer call because a missing section reads mo
19
19
 
20
20
  Length and depth quote their checkpoints from `.claude/standards/context.md`: roughly 150 rendered lines for an entry, roughly 40 for a run no heading breaks. Depth measures the longest such run rather than everything under one `##`, skips fenced blocks so a markdown example does not read as three headings, and exempts a run whose lines are all list items at one indent averaging under 130 characters. The weight condition is what separates a scannable catalog of one-liners from a stack of paragraph-bullets, which reach the same count and read nothing alike.
21
21
 
22
+ Depth exempts a second shape, and the two are excused for opposite reasons. A peer list is already navigable, so a subheading dropped into it splits a set that belongs together. A table is exempt because the remedy does not exist: a heading placed inside one splits the table rather than the run, so a catalog renders as an unbroken stretch by construction and no edit short of rewriting it as a list clears the report. The test is whether the run is a table rather than whether it holds one, so a table with prose either side still reports and a heading breaks it at the seam. A run of piped lines carrying no delimiter row is not a table and reports like any other prose. The table finding above is unaffected, so a catalog silenced here still reports as a candidate for a bullet list, which is the measure whose remedy does exist.
23
+
22
24
  Both checks count rendered lines rather than source lines, wrapping each line at 80 columns and summing the heights. Entries here are authored one line per bullet, so a block of fifteen paragraph-bullets occupies fifteen source lines and renders past sixty, which source counting cannot see. Measuring one checkpoint in each unit would put an entry length beside a run length that mean different things. Their exclusions still differ: the file measure counts fenced blocks and frontmatter, while the run measure skips a fence so an example cannot break the run around it. A reference-heavy entry therefore ranks by its examples, which the length legend states on every run. Runs count blank lines, which the standard leaves open, so a hand reader who drops them lands a line or two lower. Both sections state the width on every run, since a number in rendered lines cannot be reproduced without it.
23
25
 
24
26
  ## Bullet weight and tables
@@ -42,6 +42,34 @@ headlessly, so a call that names its stack or category is unchanged.
42
42
  `aitk snippets install`. Both resolve the target before anything else, so a path
43
43
  that does not exist fails rather than being scaffolded.
44
44
 
45
+ ## Standards selection
46
+
47
+ `aitk standards install --only <names>` takes a comma-separated list and
48
+ defaults to `all`, so a call that omits it installs the whole corpus as before.
49
+ A name resolves with or without its `.md` extension, and one that matches no
50
+ standard fails the run with exit 1 rather than being dropped, because a typo
51
+ would otherwise omit a standard silently and compute the closure over the wrong
52
+ set. `aitk init --standards <selection>` passes the same value through.
53
+
54
+ The selection expands to the standards it depends on, so nothing lands with a
55
+ dangling reference. A citation is a backticked filename in a standard's body,
56
+ resolved case-exactly against the flat `standards/` root, which is what drops a
57
+ citation naming a target's own `.claude/ARCHITECTURE.md` or a bundled standard
58
+ install never copies. Whatever the expansion adds is listed under its own step
59
+ in the output.
60
+
61
+ A citation inside a standard's `Does not govern:` list is a handoff rather than
62
+ a dependency, and the closure stops at it. That entry names a concern a sibling
63
+ owns and this standard does not, so a caller who did not ask for that concern
64
+ does not need the file. Each one is reported under a `Scope handoffs not
65
+ installed` step, naming what to add to `--only` if the project wants it after
66
+ all.
67
+
68
+ That split is what keeps a selection to a slice. Nearly all the citation density
69
+ in the corpus sits inside those scope lists, so following them pulls the whole
70
+ corpus in behind any single name. Following dependencies alone, a single name
71
+ lands between one and three of the fifteen.
72
+
45
73
  ## Governance regen
46
74
 
47
75
  `aitk gov regen` is the one governance verb that runs against the toolkit root,
@@ -97,7 +125,9 @@ exits 1 naming the failures. Passing any flag skips the confirmation prompt,
97
125
  which is what makes it scriptable. `--stack` defaults to `base`, and the default
98
126
  does not read as a passed flag, so a bare `aitk init` installs governance and
99
127
  still prompts. `--skip` takes `wiki`, `standards`, and `governance`, and warns
100
- without aborting on any other value.
128
+ without aborting on any other value. `--standards` defaults to `all` and reaches
129
+ `aitk standards install` only when it names something narrower, so the default
130
+ run spawns the command it always did.
101
131
 
102
132
  ## Unguarded tooling primitives
103
133
 
@@ -101,8 +101,12 @@ AITK_NON_INTERACTIVE=1 aitk standards sync /path/to/project
101
101
  # Copy every standard into a target, overwriting what is there
102
102
  AITK_NON_INTERACTIVE=1 aitk standards install /path/to/project
103
103
 
104
+ # Install a named subset, expanded to the standards it cites
105
+ AITK_NON_INTERACTIVE=1 aitk standards install --only slug /path/to/project
106
+
104
107
  # Bootstrap a project. Any flag suppresses the confirmation prompt
105
108
  AITK_NON_INTERACTIVE=1 aitk init --stack astro --skip wiki /path/to/project
109
+ AITK_NON_INTERACTIVE=1 aitk init --standards design,wireframes /path/to/project
106
110
 
107
111
  # Run every domain sync. The git workflow is refused headlessly, so nothing is pushed
108
112
  AITK_NON_INTERACTIVE=1 aitk sync /path/to/project
@@ -100,7 +100,7 @@ For features on a mature stack, chain the post-plan pipeline in one session. App
100
100
 
101
101
  - Use when the plan is tight and the stack has real verify commands and test coverage
102
102
  - 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
103
- - Review is skipped entirely when the diff is prose-only (every changed file matches `*.md` or `*.txt`). Prose changes are gated by `docs-sync`, `claude-standards-audit`, and pre-push hooks.
103
+ - 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.
104
104
  - 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.
105
105
  - Every stop leaves recoverable state. Fix and resume with `/git-ship`
106
106
  - Skip autoship for auth, migrations, security-sensitive changes, or work where the plan itself is uncertain
@@ -88,7 +88,7 @@ When a new need appears after scaffold, install the one domain without re-runnin
88
88
  - Project-specific rule the toolkit does not ship: invoke `aitk:create-rule`. It scaffolds a rule into `.claude/rules/` with a non-colliding number, and `aitk gov sync` leaves it untouched.
89
89
  - Index.md system for a markdown-heavy folder that emerged: invoke `aitk:setup-indexes`
90
90
  - A snippet preset or category: `aitk snippets install <preset|category|all> <path>`. The argument is required, since the picker refuses headlessly rather than choosing for the caller
91
- - A single standard: `aitk standards install <name> <path>`
91
+ - A single standard: `aitk standards install --only <names> <path>`. The selection expands to the standards it depends on, and stops at a `Does not govern:` handoff, which it reports instead. Omitting the flag installs all of them
92
92
 
93
93
  Per-domain mechanics live in the corresponding `docs/<domain>.md`. The skill body in `claude/skills/<skill>/SKILL.md` covers detection and preview.
94
94
 
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@erclx/aitk",
3
3
  "type": "module",
4
- "version": "0.40.0",
4
+ "version": "0.41.1",
5
5
  "description": "Infrastructure and quality tooling for developer workflows",
6
6
  "license": "MIT",
7
7
  "bin": {
@@ -0,0 +1,37 @@
1
+ Write the pre-compact handoff as orchestrator. Do this before a compaction, because a compaction keeps conclusions and drops the reasoning that produced them, and no other file in the repository carries that reasoning.
2
+
3
+ 1. Resolve the main worktree root with `git worktree list --porcelain | grep -m 1 '^worktree ' | cut -d' ' -f2-`, falling back to `pwd`. Write `.claude/tasks/session.md` under it.
4
+ 2. Write only what a compaction destroys and no other file already carries. The board holds the ordering and what each task waits on, a task file holds its own findings, and a groundwork folder holds its track.
5
+ 3. Use this shape:
6
+
7
+ ```markdown
8
+ ---
9
+ title: Session map
10
+ description: <what the board cannot show, and the date it was written>
11
+ ---
12
+
13
+ # Session map
14
+
15
+ <one line marking the file throwaway and naming the board as the real source>
16
+
17
+ ## State
18
+
19
+ <what is clean, what is running, what is open, and any untracked file that needs committing>
20
+
21
+ ## Decisions taken under delegated authority
22
+
23
+ <each decision and why it went that way, so nobody re-proposes it>
24
+
25
+ ## Mistakes worth not repeating
26
+
27
+ <what went wrong and the rule it yields>
28
+
29
+ ## Standing cautions
30
+
31
+ <commands that lie, tools that measure the wrong tree, and anything unbacked>
32
+ ```
33
+
34
+ 4. Cite a commit, a task, or a file and line for every claim, so the next session can tell a read from a recall.
35
+ 5. Overwrite the previous handoff rather than appending to it. A stale entry read as current is worse than no handoff.
36
+
37
+ Add a section only for content that fits none of the four and would otherwise be lost. Do not restate the board, and do not summarize the work that shipped, because git already carries it.
@@ -408,6 +408,9 @@ function reportDepth(entries: readonly EntryReport[]): void {
408
408
  logInfo(
409
409
  `Fenced blocks are excluded, and so are peer lists averaging under ${PEER_BULLET_CHECKPOINT} characters a bullet.`,
410
410
  )
411
+ logInfo(
412
+ 'A run that is entirely table rows is excluded too, since a heading inside a table splits the table rather than the run.',
413
+ )
411
414
 
412
415
  const over = entries
413
416
  .filter((entry) => entry.longestRun > RUN_CHECKPOINT)
@@ -17,6 +17,8 @@ interface InitOptions {
17
17
  readonly stack: string
18
18
  readonly add?: string
19
19
  readonly snippets: string
20
+ /** Always present: the option falls back to `all`. */
21
+ readonly standards: string
20
22
  readonly skip?: string
21
23
  }
22
24
 
@@ -39,6 +41,7 @@ export function register(program: Command): void {
39
41
  ' aitk init ../my-app',
40
42
  ' aitk init --stack astro --add 260-shadcn ../my-app',
41
43
  ' aitk init --skip governance ../my-app',
44
+ ' aitk init --standards design,wireframes ../my-app',
42
45
  '',
43
46
  ].join('\n'),
44
47
  )
@@ -66,6 +69,7 @@ async function runInit(
66
69
  stack: options.stack,
67
70
  add: options.add,
68
71
  snippets: options.snippets,
72
+ standards: options.standards,
69
73
  skip,
70
74
  }
71
75
 
@@ -3,6 +3,7 @@ import type { Command } from 'commander'
3
3
  import { registerPassThroughVerbs } from '@/commands/pass-through'
4
4
  import { PROJECT_ROOT } from '@/exec'
5
5
  import { createStandardsAdapter, standardsSourceDir } from '@/standards/adapter'
6
+ import { ALL_SELECTION, selectStandards } from '@/standards/closure'
6
7
  import {
7
8
  refreshIndex,
8
9
  STANDARDS_REL,
@@ -11,12 +12,26 @@ import {
11
12
  import { applyInstall, planInstall } from '@/standards/install'
12
13
  import { recordStamp, runDomainSync } from '@/sync/engine'
13
14
  import { resolveTarget } from '@/target'
14
- import { intro, logAdd, logInfo, logStep, logWarn, outro, select } from '@/ui'
15
+ import {
16
+ intro,
17
+ logAdd,
18
+ logError,
19
+ logInfo,
20
+ logStep,
21
+ logWarn,
22
+ outro,
23
+ select,
24
+ } from '@/ui'
15
25
 
16
26
  const GREEN = '\x1b[0;32m'
17
27
  const GREY = '\x1b[0;90m'
18
28
  const NC = '\x1b[0m'
19
29
 
30
+ interface InstallOptions {
31
+ /** Always present: the option falls back to `ALL_SELECTION`. */
32
+ readonly only: string
33
+ }
34
+
20
35
  export function register(program: Command): void {
21
36
  const standards = program
22
37
  .command('standards')
@@ -38,17 +53,36 @@ export function register(program: Command): void {
38
53
 
39
54
  standards
40
55
  .command('install')
41
- .description('Copy all standards into a project (overwrites)')
56
+ .description('Copy standards into a project (overwrites)')
42
57
  .argument('[target]', 'Target directory', '.')
58
+ .option(
59
+ '--only <names>',
60
+ "Comma-separated standard names, or 'all'",
61
+ ALL_SELECTION,
62
+ )
43
63
  .helpOption('-h, --help', 'Show this help message')
44
- .action(async (target: string) => {
45
- process.exitCode = await runInstall(target)
64
+ .addHelpText(
65
+ 'after',
66
+ [
67
+ '',
68
+ 'A selection expands to the standards it cites, so nothing lands with',
69
+ 'a dangling reference.',
70
+ '',
71
+ 'Examples:',
72
+ ' aitk standards install',
73
+ ' aitk standards install --only slug ../my-app',
74
+ ' aitk standards install --only design,wireframes ../my-app',
75
+ '',
76
+ ].join('\n'),
77
+ )
78
+ .action(async (target: string, options: InstallOptions) => {
79
+ process.exitCode = await runInstall(target, options.only)
46
80
  })
47
81
 
48
82
  registerPassThroughVerbs(standards, 'standards', ['list'])
49
83
  }
50
84
 
51
- async function runInstall(target: string): Promise<number> {
85
+ async function runInstall(target: string, selection: string): Promise<number> {
52
86
  intro('aitk standards')
53
87
 
54
88
  const resolved = resolveTarget(target, PROJECT_ROOT)
@@ -58,8 +92,31 @@ async function runInstall(target: string): Promise<number> {
58
92
  const destDir = standardsInstallDir(resolved)
59
93
 
60
94
  logStep('Scanning standards')
61
- const files = planInstall(sourceDir)
62
- for (const file of files) logInfo(join(STANDARDS_REL, file.name))
95
+ const available = planInstall(sourceDir)
96
+ const result = selectStandards(available, selection)
97
+
98
+ if (!result.ok) {
99
+ logError(
100
+ `Standard not found: ${result.unknown.join(', ')}. Run 'aitk standards list' for the catalog.`,
101
+ )
102
+ outro()
103
+ return 1
104
+ }
105
+
106
+ const { files, requested, added, unresolved } = result.selection
107
+ for (const name of requested) logInfo(join(STANDARDS_REL, name))
108
+
109
+ if (added.length > 0) {
110
+ logStep(`Added by citation (${added.length})`)
111
+ for (const name of added) logInfo(join(STANDARDS_REL, name))
112
+ }
113
+
114
+ if (unresolved.length > 0) {
115
+ logStep(`Scope handoffs not installed (${unresolved.length})`)
116
+ for (const name of unresolved) logWarn(name)
117
+ logInfo('Each names a concern these standards do not govern. Add a name')
118
+ logInfo('to --only if the project needs that standard as well.')
119
+ }
63
120
 
64
121
  const shouldInstall = await select({
65
122
  message: `Install ${files.length} standards to ${destDir}?`,
@@ -231,6 +231,36 @@ function isScannablePeerList(run: readonly BodyLine[]): boolean {
231
231
  return characters / items < PEER_BULLET_CHECKPOINT
232
232
  }
233
233
 
234
+ /**
235
+ * Reports whether a run is a table, the second shape the checkpoint cannot fix.
236
+ *
237
+ * The peer list above is exempt because it is already navigable. A table is
238
+ * exempt for the other reason: the remedy does not exist. A heading dropped
239
+ * inside one splits the table into two tables rather than breaking the run, so
240
+ * a catalog renders as an unbroken stretch by construction and no edit short of
241
+ * rewriting it as a list clears the report.
242
+ *
243
+ * Every non-blank line has to be a row. A run holding a table between
244
+ * paragraphs is genuinely mixed, and a heading breaks it at a seam either side,
245
+ * so testing whether the run holds a table would hide the case the checkpoint
246
+ * exists for.
247
+ *
248
+ * A delimiter is required rather than assumed, matching the table scan below. A
249
+ * stack of lines opening with a pipe and no delimiter renders as paragraph text
250
+ * and would otherwise earn the exemption on its punctuation.
251
+ */
252
+ function isTableRun(run: readonly BodyLine[]): boolean {
253
+ let separators = 0
254
+
255
+ for (const line of run) {
256
+ if (line.text.trim() === '') continue
257
+ if (!TABLE_ROW.test(line.text)) return false
258
+ if (TABLE_SEPARATOR.test(line.text)) separators++
259
+ }
260
+
261
+ return separators > 0
262
+ }
263
+
234
264
  /**
235
265
  * Height a source line occupies once wrapped.
236
266
  *
@@ -272,7 +302,7 @@ function longestRun(lines: readonly BodyLine[]): {
272
302
  // two headings rather than a stretch a reader travels, so it never counts.
273
303
  const first = run.find((line) => line.text.trim() !== '')
274
304
 
275
- if (first && !isScannablePeerList(run)) {
305
+ if (first && !isScannablePeerList(run) && !isTableRun(run)) {
276
306
  const height = run.reduce(
277
307
  (sum, line) => sum + renderedHeight(line.text),
278
308
  0,
package/src/init/flags.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import type { Command } from 'commander'
2
2
  import { DEFAULT_STACK, SKIPPABLE_DOMAINS } from '@/init/plan'
3
+ import { ALL_SELECTION } from '@/standards/closure'
3
4
 
4
5
  interface InitOptionSpec {
5
6
  /** The option value key commander stores the parsed value under. */
@@ -32,6 +33,12 @@ export const INIT_OPTIONS: readonly InitOptionSpec[] = [
32
33
  description: "Snippets preset, category, or 'all'",
33
34
  defaultValue: 'essentials',
34
35
  },
36
+ {
37
+ key: 'standards',
38
+ flags: '--standards <selection>',
39
+ description: "Comma-separated standard names, or 'all'",
40
+ defaultValue: ALL_SELECTION,
41
+ },
35
42
  {
36
43
  key: 'skip',
37
44
  flags: '--skip <list>',
@@ -55,9 +62,9 @@ export function applyInitOptions(command: Command): Command {
55
62
 
56
63
  /**
57
64
  * Whether the operator passed any flag, which is what makes the command
58
- * scriptable by suppressing the confirmation prompt. `--snippets` and `--stack`
59
- * both carry defaults, so presence has to be read from where the value came
60
- * from rather than from the value itself.
65
+ * scriptable by suppressing the confirmation prompt. `--stack`, `--snippets`,
66
+ * and `--standards` all carry defaults, so presence has to be read from where
67
+ * the value came from rather than from the value itself.
61
68
  */
62
69
  export function flagsProvided(cmd: Command): boolean {
63
70
  return INIT_OPTIONS.some(
package/src/init/plan.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import { ALL_SELECTION } from '@/standards/closure'
2
+
1
3
  export const SKIPPABLE_DOMAINS = ['wiki', 'standards', 'governance'] as const
2
4
 
3
5
  export type SkippableDomain = (typeof SKIPPABLE_DOMAINS)[number]
@@ -31,6 +33,7 @@ export interface InitFlags {
31
33
  readonly stack?: string
32
34
  readonly add?: string
33
35
  readonly snippets: string
36
+ readonly standards: string
34
37
  readonly skip: SkipPlan
35
38
  }
36
39
 
@@ -106,7 +109,11 @@ export function planInit(flags: InitFlags): InitPlan {
106
109
  }
107
110
 
108
111
  if (!flags.skip.skipped.has('standards')) {
109
- preview.push({ level: 'info', text: 'standards (authoring conventions)' })
112
+ const detail =
113
+ flags.standards === ALL_SELECTION
114
+ ? 'authoring conventions'
115
+ : `${flags.standards}, plus what they cite`
116
+ preview.push({ level: 'info', text: `standards (${detail})` })
110
117
  }
111
118
 
112
119
  preview.push({ level: 'info', text: `snippets (${flags.snippets})` })
package/src/init/steps.ts CHANGED
@@ -1,5 +1,6 @@
1
1
  import { type InitFlags, resolveStack } from '@/init/plan'
2
2
  import type { DomainStep } from '@/init/run'
3
+ import { ALL_SELECTION } from '@/standards/closure'
3
4
 
4
5
  /** Builds the child-process invocation for one domain. */
5
6
  export type RunFactory = (args: readonly string[]) => () => Promise<boolean>
@@ -52,7 +53,7 @@ export function buildSteps(
52
53
  steps.push({
53
54
  kind: 'run',
54
55
  label: 'Standards',
55
- run: child(['standards', 'install', resolved]),
56
+ run: child(standardsArgs(flags.standards, resolved)),
56
57
  })
57
58
  }
58
59
 
@@ -73,6 +74,21 @@ export function buildSteps(
73
74
  return steps
74
75
  }
75
76
 
77
+ /**
78
+ * Builds the `standards install` argv. `all` is left off rather than spelled
79
+ * out, so the default init runs the same command it ran before the flag
80
+ * existed.
81
+ */
82
+ function standardsArgs(selection: string, path: string): string[] {
83
+ const args = ['standards', 'install']
84
+ if (selection !== '' && selection !== ALL_SELECTION) {
85
+ args.push('--only', selection)
86
+ }
87
+ args.push(path)
88
+
89
+ return args
90
+ }
91
+
76
92
  /**
77
93
  * Builds the `gov install` argv. The run and the recovery command a skip prints
78
94
  * come from here both, so the command a caller is told to paste installs what
@@ -0,0 +1,200 @@
1
+ import { readFileSync } from 'node:fs'
2
+ import { basename } from 'node:path'
3
+ import type { StandardsSource } from '@/standards/install'
4
+
5
+ export const ALL_SELECTION = 'all'
6
+
7
+ export interface Citations {
8
+ /** Siblings the body depends on, which the closure follows. */
9
+ readonly cited: readonly string[]
10
+ /** Siblings a `Does not govern:` entry hands off to, which it does not. */
11
+ readonly delegated: readonly string[]
12
+ }
13
+
14
+ export interface StandardsSelection {
15
+ readonly files: readonly StandardsSource[]
16
+ /** What the caller named, in the order the flat root lists it. */
17
+ readonly requested: readonly string[]
18
+ /** What a requested standard cites and the caller did not name. */
19
+ readonly added: readonly string[]
20
+ /** Handoff targets that did not land, so their pointers will not resolve. */
21
+ readonly unresolved: readonly string[]
22
+ }
23
+
24
+ export type SelectionResult =
25
+ | { readonly ok: true; readonly selection: StandardsSelection }
26
+ | { readonly ok: false; readonly unknown: readonly string[] }
27
+
28
+ const CITATION = /`([^`\n]+?\.md)`/g
29
+ const DELEGATION_START = /^Does not govern:/
30
+ const HEADING = /^#{1,6}\s/
31
+
32
+ /** Accepts `prose` and `prose.md` alike, since the catalog lists both spellings. */
33
+ export function normalizeName(raw: string): string {
34
+ const name = raw.trim()
35
+ return name.endsWith('.md') ? name : `${name}.md`
36
+ }
37
+
38
+ export function parseSelection(csv: string): string[] {
39
+ return csv
40
+ .split(',')
41
+ .map((raw) => raw.trim())
42
+ .filter((raw) => raw !== '')
43
+ .map(normalizeName)
44
+ }
45
+
46
+ /**
47
+ * Splits a body at the `Does not govern:` list, which runs to the next heading.
48
+ * Every standard in the flat root carries exactly one, directly under `## Scope`.
49
+ */
50
+ function splitDelegatedScope(body: string): {
51
+ governing: string
52
+ delegated: string
53
+ } {
54
+ const governing: string[] = []
55
+ const delegated: string[] = []
56
+ let inDelegation = false
57
+
58
+ for (const line of body.split('\n')) {
59
+ if (DELEGATION_START.test(line)) inDelegation = true
60
+ else if (inDelegation && HEADING.test(line)) inDelegation = false
61
+
62
+ if (inDelegation) delegated.push(line)
63
+ else governing.push(line)
64
+ }
65
+
66
+ return { governing: governing.join('\n'), delegated: delegated.join('\n') }
67
+ }
68
+
69
+ function matchNames(text: string, available: ReadonlySet<string>): string[] {
70
+ const found = new Set<string>()
71
+
72
+ for (const match of text.matchAll(CITATION)) {
73
+ const token = match[1]
74
+ if (token === undefined) continue
75
+
76
+ const name = basename(token)
77
+ if (available.has(name)) found.add(name)
78
+ }
79
+
80
+ return [...found]
81
+ }
82
+
83
+ /**
84
+ * Reads the sibling standards a body cites, split by whether the citation is a
85
+ * dependency or a handoff. A citation is a backticked token ending in `.md`,
86
+ * which is the only place either relationship is written, so the parse is a
87
+ * heuristic and every candidate is resolved against `available` before it
88
+ * counts. That resolution is what drops a fenced example, a target project's
89
+ * `.claude/ARCHITECTURE.md`, and a bundled standard the flat root does not
90
+ * install, none of which a selection should pull in.
91
+ *
92
+ * A citation inside the `Does not govern:` list is `delegated` rather than
93
+ * `cited`, because that entry says the sibling owns a concern this standard
94
+ * does not. Expanding on it pulls in a file the caller declined by not naming
95
+ * it, and nearly all the corpus density sits in those lists, which is what
96
+ * collapsed every selection into the whole corpus. A name appearing in the list
97
+ * and also outside it stays `cited`, since a real dependency outranks a handoff.
98
+ *
99
+ * Matching is case-exact against the listing rather than a filesystem probe,
100
+ * because a case-insensitive volume would otherwise resolve `SKILL.md` onto
101
+ * `skill.md` and expand a selection on a citation that names a target's own
102
+ * file. The basename is what resolves, so `standards/versioning.md` and a bare
103
+ * `versioning.md` read as the same dependency.
104
+ */
105
+ export function citedStandards(
106
+ body: string,
107
+ available: ReadonlySet<string>,
108
+ ): Citations {
109
+ const { governing, delegated } = splitDelegatedScope(body)
110
+ const cited = matchNames(governing, available)
111
+ const citedSet = new Set(cited)
112
+
113
+ return {
114
+ cited,
115
+ delegated: matchNames(delegated, available).filter(
116
+ (name) => !citedSet.has(name),
117
+ ),
118
+ }
119
+ }
120
+
121
+ /**
122
+ * Expands a selection to the transitive closure of what it cites, so an install
123
+ * cannot land a standard whose citations dangle. `all` and an empty selection
124
+ * both mean every standard, which is what keeps the existing callers unchanged.
125
+ *
126
+ * The closure follows dependencies alone. A `Does not govern:` handoff names a
127
+ * concern the caller declined by not selecting it, so the target stays out and
128
+ * is reported through `unresolved` instead. Following those too pulls the whole
129
+ * corpus in behind any single name.
130
+ *
131
+ * An unrecognized name fails the whole selection rather than being dropped with
132
+ * a warning, unlike `--skip` on `aitk init`. A typo here silently omits a
133
+ * standard the caller asked for, and the closure would then be computed over
134
+ * the wrong set.
135
+ */
136
+ export function selectStandards(
137
+ available: readonly StandardsSource[],
138
+ selection: string,
139
+ ): SelectionResult {
140
+ const byName = new Map(available.map((file) => [file.name, file]))
141
+
142
+ if (selection.trim() === '' || selection.trim() === ALL_SELECTION) {
143
+ return {
144
+ ok: true,
145
+ selection: {
146
+ files: available,
147
+ requested: available.map((file) => file.name),
148
+ added: [],
149
+ unresolved: [],
150
+ },
151
+ }
152
+ }
153
+
154
+ const requested = parseSelection(selection)
155
+ const unknown = requested.filter((name) => !byName.has(name))
156
+ if (unknown.length > 0) return { ok: false, unknown }
157
+
158
+ const names = new Set(byName.keys())
159
+ const resolved = new Set(requested)
160
+ const handoffs = new Set<string>()
161
+ const queue = [...requested]
162
+
163
+ for (let index = 0; index < queue.length; index += 1) {
164
+ const name = queue[index]
165
+ if (name === undefined) continue
166
+
167
+ const file = byName.get(name)
168
+ if (file === undefined) continue
169
+
170
+ const citations = citedStandards(readFileSync(file.path, 'utf8'), names)
171
+ for (const handoff of citations.delegated) handoffs.add(handoff)
172
+
173
+ for (const cited of citations.cited) {
174
+ if (resolved.has(cited)) continue
175
+
176
+ resolved.add(cited)
177
+ queue.push(cited)
178
+ }
179
+ }
180
+
181
+ const requestedSet = new Set(requested)
182
+
183
+ return {
184
+ ok: true,
185
+ selection: {
186
+ files: available.filter((file) => resolved.has(file.name)),
187
+ requested: available
188
+ .filter((file) => requestedSet.has(file.name))
189
+ .map((file) => file.name),
190
+ added: available
191
+ .filter(
192
+ (file) => resolved.has(file.name) && !requestedSet.has(file.name),
193
+ )
194
+ .map((file) => file.name),
195
+ unresolved: available
196
+ .filter((file) => handoffs.has(file.name) && !resolved.has(file.name))
197
+ .map((file) => file.name),
198
+ },
199
+ }
200
+ }
@@ -104,6 +104,7 @@ Only the `development` entry carries this section. It is not a general-purpose h
104
104
  - Past roughly 40 rendered lines with no heading of any level breaking them, add a subheading at the seam. Measure the longest such run rather than everything under one `##`, and exclude fenced code blocks. The number is a checkpoint like the 150 above, not a cap.
105
105
  - Both checkpoints count rendered lines, so wrap each source line at 80 columns and sum the heights. Source lines undercount an entry authored one line per bullet, where a block of fifteen paragraph-bullets occupies fifteen lines and renders past sixty. Counting the two checkpoints in different units would put a file measured one way beside a run measured another.
106
106
  - Exempt a block whose lines are all list items at one level averaging under roughly 130 characters. A flat list of short peers is already navigable, and a subheading dropped into it splits a set that belongs together. Bullet count says nothing on its own, since a catalog of one-liners and a stack of paragraphs reach the same count and read nothing alike, so weight is what decides. Mixing prose with the list, or nesting levels inside it, ends the exemption at any weight.
107
+ - Exempt a block whose lines are all table rows, at any length. The peer list above is exempt because it is already navigable, and a table because the remedy does not exist: a subheading dropped inside one splits the table rather than the run, so no edit short of rewriting it as a list clears the checkpoint. Prose either side of the table ends the exemption, since that block has a seam and a heading breaks it there.
107
108
  - Past roughly 400 characters in one top-level bullet, counting the lines that continue it and excluding any bullet nested under it, check whether the incident that motivated the decision sits beside the decision itself. Keep the current design and the alternative that lost, and move the incident to the change that introduced it, the issue that tracked it, or the research record behind it. The number is a checkpoint like the two above, and a bullet reading well past it means the number is wrong rather than the rule.
108
109
  - Collapse a stack of bullets narrating one subsystem into a single `###` subsection carrying one narrative. Splitting a heavy bullet into three light ones satisfies the checkpoint above and leaves the reader no better off, and subdividing a block does not lighten the bullets inside it, so the two rules answer different defects.
109
110
  - Never cut a `## Decisions` or `## Gotchas` entry to shorten a file. Cut a `## Layout` or `## CLI` section instead.