@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.
- package/claude/.claude-plugin/plugin.json +1 -1
- package/claude/skills/claude-autoship/REQUIREMENT.md +5 -2
- package/claude/skills/claude-autoship/SKILL.md +16 -3
- package/claude/skills/claude-orchestrate/REQUIREMENT.md +12 -0
- package/claude/skills/claude-orchestrate/SKILL.md +35 -2
- package/docs/agents/context-audit-checks.md +2 -0
- package/docs/agents/install-and-sync.md +31 -1
- package/docs/agents/scripting.md +4 -0
- package/docs/ai-workflow.md +1 -1
- package/docs/target-projects.md +1 -1
- package/package.json +1 -1
- package/snippets/claude/orchestrator-handoff.md +37 -0
- package/src/commands/context.ts +3 -0
- package/src/commands/init.ts +4 -0
- package/src/commands/standards.ts +64 -7
- package/src/context/audit.ts +31 -1
- package/src/init/flags.ts +10 -3
- package/src/init/plan.ts +8 -1
- package/src/init/steps.ts +17 -1
- package/src/standards/closure.ts +200 -0
- package/standards/context.md +1 -0
|
@@ -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.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
package/docs/agents/scripting.md
CHANGED
|
@@ -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
|
package/docs/ai-workflow.md
CHANGED
|
@@ -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
|
|
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
|
package/docs/target-projects.md
CHANGED
|
@@ -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 <
|
|
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
|
@@ -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.
|
package/src/commands/context.ts
CHANGED
|
@@ -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)
|
package/src/commands/init.ts
CHANGED
|
@@ -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 {
|
|
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
|
|
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
|
-
.
|
|
45
|
-
|
|
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
|
|
62
|
-
|
|
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}?`,
|
package/src/context/audit.ts
CHANGED
|
@@ -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. `--
|
|
59
|
-
*
|
|
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
|
-
|
|
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(
|
|
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
|
+
}
|
package/standards/context.md
CHANGED
|
@@ -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.
|