@erclx/aitk 3.48.1 → 3.50.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/claude/.claude-plugin/plugin.json +1 -1
- package/claude/skills/claude-autoship/SKILL.md +21 -1
- package/docs/agents/commands.md +4 -1
- package/docs/agents/index.md +2 -0
- package/docs/agents/review-classification.md +77 -0
- package/docs/agents/rule-citations.md +98 -0
- package/docs/ai-workflow.md +4 -0
- package/package.json +1 -1
- package/scripts/core/verify.sh +16 -0
- package/src/autoship/classify.ts +75 -0
- package/src/autoship/paths.ts +51 -0
- package/src/cli.ts +3 -0
- package/src/commands/autoship.ts +129 -0
- package/src/commands/gov.ts +247 -0
- package/src/gov/citations.ts +514 -0
|
@@ -97,12 +97,30 @@ If all UI changes are covered by e2e tests, continue.
|
|
|
97
97
|
|
|
98
98
|
## Step 5: review
|
|
99
99
|
|
|
100
|
-
Classify the diff first. Take the union of `git diff --name-only <base>` and `git ls-files --others --exclude-standard`, resolving `<base>` per Diff baseline
|
|
100
|
+
Classify the diff first. Take the union of `git diff --name-only <base>` and `git ls-files --others --exclude-standard`, resolving `<base>` per Diff baseline, then hand that set to the verb rather than reading it against the list below yourself:
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
aitk autoship classify --json <path>...
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
The verb reads names only and touches git not at all, so the set stays the one this step already computed and no second baseline resolves to disagree with the first. Branch on the record's `decision` rather than on the exit code, which a shell function wrapping `aitk` can flatten to zero.
|
|
107
|
+
|
|
108
|
+
- `skip`. Every path reads as prose and none states agent behavior. Skip review entirely and continue to Step 7.
|
|
109
|
+
- `review`. Invoke `aitk:claude-review`. The record names the `file` that decided it and the `test` it failed, `extension` for a path that is not prose and `behavior-path` for prose that states what an agent does.
|
|
110
|
+
- `refused`, carrying reason `no-changes`. The changed set was empty, so take the stop below.
|
|
111
|
+
|
|
112
|
+
Say in the run which of the two decided, the verb or the written fallback, since a reader otherwise cannot tell a classification from a judgment.
|
|
101
113
|
|
|
102
114
|
An empty list stops the chain: `❌ No changed files to classify. Re-run when the plan has yet to produce its output. When the output is gitignored by design, autoship cannot ship it, so take the work out of the chain.` An empty list satisfies the prose-only test vacuously, so reading it as prose-only routes the branch past review instead of through it.
|
|
103
115
|
|
|
104
116
|
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.
|
|
105
117
|
|
|
118
|
+
### When the verb is absent
|
|
119
|
+
|
|
120
|
+
The verb ships with the CLI and this body ships with the plugin, so a target holding an older binary meets a missing subcommand. Apply the written test by hand there, and say the fallback decided it.
|
|
121
|
+
|
|
122
|
+
Never read an absent subcommand as a skip. Failing open is the exact defect the verb closes, and a shell that answers `command not found` reaching a body that skips on anything other than a `skip` record would ship every branch unreviewed.
|
|
123
|
+
|
|
106
124
|
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`.
|
|
107
125
|
|
|
108
126
|
Behavior paths carry two spellings, the one a surface authors at and the one it reaches a session at, so the rule reads the same in a toolkit and in a project that consumed one:
|
|
@@ -120,6 +138,8 @@ Informational prose is already gated by `docs-sync`, `claude-standards-audit`, a
|
|
|
120
138
|
|
|
121
139
|
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.
|
|
122
140
|
|
|
141
|
+
The verb reads the same set from `src/autoship/paths.ts`, so a path added here belongs there too and a path added there belongs here. Two copies is what the fallback costs, and it stands until a release retires the written half.
|
|
142
|
+
|
|
123
143
|
## Step 6: evaluate findings
|
|
124
144
|
|
|
125
145
|
Skip this step when Step 5 skipped review. Otherwise read `.claude/review/branch/review-<slug>.md` at the main worktree root. Split every finding by origin before parsing the summary line (`X critical, Y should-fix, Z minor`), since the stop exists for a defect the branch inherited rather than for one this run introduced.
|
package/docs/agents/commands.md
CHANGED
|
@@ -55,10 +55,12 @@ Full help: `aitk <command> --help`. Behavior notes for the install and sync verb
|
|
|
55
55
|
| `aitk gov test-order` | Report where an implementation reached history ahead of the test covering it (`--json`) |
|
|
56
56
|
| `aitk gov superseded` | Report where the tree still asserts a value a changed convention no longer produces, keyed on the value and on the family stem behind a templated citation (`--json`) |
|
|
57
57
|
| `aitk gov restated` | Report every instruction the always-loaded file or a rule shares with the seed, a shipped skill body, or another rule, classed and with its anchors named (`--json`) |
|
|
58
|
+
| `aitk gov citations` | Resolve every path a rule cites and every internal frontmatter glob, failing on one reaching nothing (`--json`) |
|
|
58
59
|
| `aitk secrets scan` | Report credential-shaped values in the tree the package ships, keyed on issued values rather than on words (`--json`) |
|
|
59
60
|
| `aitk deps audit` | Report published advisories against the resolved dependency set, refusing rather than reporting clean when the index is unreachable (`--json`) |
|
|
60
61
|
| `aitk labels audit` | Report the labels a changed set earns from the pull request label map and the paths no row reaches (`--json`) |
|
|
61
62
|
| `aitk labels scan` | Fail a pull request whose title or body carries a phase label, sorting a release pull request's tokens as semver rather than as a leak (`--event`, `--json`) |
|
|
63
|
+
| `aitk autoship classify` | Decide whether a changed set needs the review pass, naming the file and the test that decided it (`--json`) |
|
|
62
64
|
| `aitk census [path]` | Report tracked file count, a breakdown by extension, and a line total that skips whatever reads as binary (`--json`) |
|
|
63
65
|
| `aitk audits run` | Run every audit as one set, report per check under one verdict, and compare each count to the recorded baseline (`--json`, `--record`) |
|
|
64
66
|
| `aitk audits list` | List every audit the set runs, with the corpus each reads and whether it gates (`--json`) |
|
|
@@ -96,7 +98,7 @@ Each domain exposes a consistent shape where applicable: `list`, `install`, `syn
|
|
|
96
98
|
| `tooling` | `list`, `sync`, `ref`, `create`, `verify`, `inject`, `prune-gitignore` |
|
|
97
99
|
| `snippets` | `list`, `create` |
|
|
98
100
|
| `standards` | `list`, `audit`, `<name>` |
|
|
99
|
-
| `gov` | `list`, `install`, `sync`, `build`, `regen`, `test-order`, `superseded`
|
|
101
|
+
| `gov` | `list`, `install`, `sync`, `build`, `regen`, `test-order`, `superseded`, `citations` |
|
|
100
102
|
| `claude` | `init`, `sync`, `routing`, `seeds list`, `skills list`, `skills audit`, `skills drift`, `skills reach`, `skills rank`, `setup [dest]` |
|
|
101
103
|
| `demo` | `compile`, `run` |
|
|
102
104
|
| `inventory` | `run` |
|
|
@@ -112,6 +114,7 @@ Each domain exposes a consistent shape where applicable: `list`, `install`, `syn
|
|
|
112
114
|
| `secrets` | `scan` |
|
|
113
115
|
| `deps` | `audit` |
|
|
114
116
|
| `labels` | `audit` |
|
|
117
|
+
| `autoship` | `classify` |
|
|
115
118
|
| `audits` | `run`, `list` |
|
|
116
119
|
|
|
117
120
|
Common patterns:
|
package/docs/agents/index.md
CHANGED
|
@@ -27,7 +27,9 @@ CLI catalog and invocation rules for agents, split by command domain. Start with
|
|
|
27
27
|
- [Overview](overview.md): What this folder covers, the invocation rules every command inherits, and where domain behavior is documented instead
|
|
28
28
|
- [Records](records.md): Validating the session records under .claude/ and the standards corpus, the per-kind checks, the refusal reasons, migrating a record a frontmatter change orphaned, reading each folder's size and growth, backing the folders to a private remote, and which root each kind defaults to
|
|
29
29
|
- [Restated instructions](restated.md): Counting the instructions the always-loaded file and every path-scoped rule share with the seed, the shipped skill bodies, and each other, how a match is decided, the three classes, which surface a later edit starts from, and why the sweep reports rather than gates
|
|
30
|
+
- [Review classification](review-classification.md): Deciding whether a changed set needs the review pass, the two tests it runs, why an empty set refuses rather than skipping, and the written fallback a target on an older binary falls back to
|
|
30
31
|
- [Routing report](routing.md): Reading per CLAUDE.md section how many bullets name a path, what counts as naming one, when a rule counts as covering it, the two refusals, and why the verb reports rather than gates
|
|
32
|
+
- [Rule citations](rule-citations.md): Resolving every path a governance rule cites and every frontmatter glob the internal corpus declares, the three forms a citation is written in, the shapes that look like citations and are not, the two classes where an absent path is correct, why the glob half reads one corpus, and why this one gates
|
|
31
33
|
- [Sandbox](sandbox.md): Scenario routing, the expectation scoring surface, and the coverage census over scenarios and skills
|
|
32
34
|
- [Scripting](scripting.md): The runtime catalogs that replace hardcoded names, what each carries, and a headless invocation per domain
|
|
33
35
|
- [Sessions](sessions.md): Resolving live peer sessions to the worktree and branch each holds, reading which row is the caller, the liveness confidence field, and what each session surface can see
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Review classification
|
|
3
|
+
description: Deciding whether a changed set needs the review pass, the two tests it runs, why an empty set refuses rather than skipping, and the written fallback a target on an older binary falls back to
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Review classification
|
|
7
|
+
|
|
8
|
+
`aitk autoship classify` answers the one decision the ship chain used to make by reading a bulleted list out of a skill body. It takes the names a branch changed and returns whether the review pass can be skipped, which file decided it, and which of the two tests that file failed.
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
aitk autoship classify docs/index.md README.md
|
|
12
|
+
aitk autoship classify --json .claude/skills/deploy-check/SKILL.md
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
| Option | Behavior |
|
|
16
|
+
| -------- | ---------------------------------------------------------- |
|
|
17
|
+
| `--json` | Add a machine-readable record on stdout, keeping the frame |
|
|
18
|
+
|
|
19
|
+
Positional paths are the whole input. The verb never touches git, so it reads no baseline of its own and the caller hands over the set it already computed. A second read here could resolve a different range than the one the chain measured, which is the stale-baseline failure a prior row closed.
|
|
20
|
+
|
|
21
|
+
Under `--json` the record holds stdout alone and the frame still renders on stderr, refusals included, which is the split `output-shape.md` fixes for every mode.
|
|
22
|
+
|
|
23
|
+
## The two tests
|
|
24
|
+
|
|
25
|
+
A branch skips review only when both hold across every changed name:
|
|
26
|
+
|
|
27
|
+
- **Every path reads as prose.** The extension is `.md` or `.txt`, compared lowercased. Anything else is code, a config, or an asset.
|
|
28
|
+
- **No path sits under a behavior path.** Markdown under one of those surfaces states what an agent does, so a change there is a behavior change wearing a prose extension.
|
|
29
|
+
|
|
30
|
+
One failing file sends the whole branch to review. Documentation shipped beside a behavior change does not cancel it.
|
|
31
|
+
|
|
32
|
+
The record names the first file to fail, tested in the caller's own order, and each file is read for its extension before its path. A set failing both tests reports one file and one test, which is all a caller needs to route.
|
|
33
|
+
|
|
34
|
+
## The behavior paths
|
|
35
|
+
|
|
36
|
+
Most carry two spellings, the one a surface authors at and the one it reaches a session at, so the rule reads the same in this toolkit and in a project that consumed one.
|
|
37
|
+
|
|
38
|
+
| Surface | Spellings |
|
|
39
|
+
| ----------------- | ------------------------------------- |
|
|
40
|
+
| Plugin skills | `claude/skills/`, `.claude/skills/` |
|
|
41
|
+
| Governance rules | `governance/rules/`, `.claude/rules/` |
|
|
42
|
+
| Standards | `standards/` |
|
|
43
|
+
| Snippets | `snippets/` |
|
|
44
|
+
| Internal and seed | `internal/`, `tooling/` |
|
|
45
|
+
| Root instructions | `CLAUDE.md` |
|
|
46
|
+
|
|
47
|
+
`standards/` and `snippets/` carry one spelling apiece. Neither installs into a project, reaching a reader by resolution and through the `claude/snippets` symlink respectively, so there is no consumed copy to name.
|
|
48
|
+
|
|
49
|
+
`CLAUDE.md` is matched as a whole path rather than as a prefix, because a prefix reaches nothing that sits in no folder. A nested `docs/CLAUDE.md` stays informational.
|
|
50
|
+
|
|
51
|
+
The set is data at `src/autoship/paths.ts`. That makes it permanently exempt from any later design that folds a machine-parsed list back into the surface citing it, per the machine-parsed clause in `.claude/ARCHITECTURE.md`.
|
|
52
|
+
|
|
53
|
+
## Exit codes and refusals
|
|
54
|
+
|
|
55
|
+
| Code | Meaning |
|
|
56
|
+
| ---- | ------------------------------------------------------------ |
|
|
57
|
+
| `0` | prose-only, so the review pass can be skipped |
|
|
58
|
+
| `1` | refused, with `reason` naming the cause |
|
|
59
|
+
| `2` | review is needed, with `file` and `test` naming what decided |
|
|
60
|
+
|
|
61
|
+
Branch on the record's `decision` rather than on the exit code. A shell function wrapping `aitk` takes its status from whatever it runs last, so every non-zero exit can reach a caller as zero.
|
|
62
|
+
|
|
63
|
+
An empty set refuses with `no-changes` rather than skipping. Both tests are universally quantified, so an empty set satisfies them vacuously, and reading that as prose-only would route a branch past review for having produced no output at all.
|
|
64
|
+
|
|
65
|
+
## Why it is a verb
|
|
66
|
+
|
|
67
|
+
The decision was three sentences in `claude-autoship` Step 5, applied by a session reading a bulleted list of paths. It failed three times. A driven arm on 2026-08-30 staged `.claude/skills/deploy-check/SKILL.md`, which that list names, and the chain skipped review and opened a draft pull request anyway. The rule was correct and the session did not apply it, and the two fixes before that one were each a rewrite of the same prose.
|
|
68
|
+
|
|
69
|
+
A rule a session can talk itself out of moves into a verb. That is the same argument the quiz-order draw in `aitk teach lesson` was decided on: an instruction is a hope where a verb is a check.
|
|
70
|
+
|
|
71
|
+
## The written fallback
|
|
72
|
+
|
|
73
|
+
The verb ships with the CLI and Step 5 ships with the plugin, so a target holding an older binary meets a missing subcommand. The skill body keeps the written list and applies it by hand there, naming which of the two decided the run.
|
|
74
|
+
|
|
75
|
+
The fallback is never a skip. Failing open is the exact defect the verb closes, so a body that skips on anything other than a `skip` record would ship every branch unreviewed the moment the subcommand went absent.
|
|
76
|
+
|
|
77
|
+
Two copies of the path set is what the fallback costs, and it stands until a release retires the written half.
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Rule citations
|
|
3
|
+
description: Resolving every path a governance rule cites and every frontmatter glob the internal corpus declares, the three forms a citation is written in, the shapes that look like citations and are not, the two classes where an absent path is correct, why the glob half reads one corpus, and why this one gates
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Rule citations
|
|
7
|
+
|
|
8
|
+
`aitk gov citations` resolves every path a rule cites and every frontmatter glob the internal corpus declares, naming the ones reaching nothing. It answers a failure no other stage sees: a rule points a reader at a file, the file moves, and nothing reports it until a session opens the path and finds an absence.
|
|
9
|
+
|
|
10
|
+
A glob fails the same way and more quietly. A rule scoped at a directory that moved stops matching, so it never loads again, and a rule that never fires looks exactly like a rule nobody violated.
|
|
11
|
+
|
|
12
|
+
```bash
|
|
13
|
+
aitk gov citations
|
|
14
|
+
aitk gov citations --json
|
|
15
|
+
aitk gov citations --root ../my-app
|
|
16
|
+
```
|
|
17
|
+
|
|
18
|
+
| Option | Behavior |
|
|
19
|
+
| --------------- | ---------------------------------------------------------- |
|
|
20
|
+
| `--root <path>` | Tree to read, defaulting to the current directory |
|
|
21
|
+
| `--json` | Add a machine-readable record on stdout, keeping the frame |
|
|
22
|
+
|
|
23
|
+
Under `--json` the record holds stdout alone and the frame still renders on stderr, refusals included, which is the split `output-shape.md` fixes for every mode.
|
|
24
|
+
|
|
25
|
+
## What the drift check cannot see
|
|
26
|
+
|
|
27
|
+
`bun run check` asserts that an authored rule and its consumed copy agree, and a dead citation passes that assertion because both copies carry it. `governance/rules/claude/561-teach.md` told its reader to open a `references/glossary.md` inside the teach skill that had never existed at that path. The rule shipped, survived a release, and fired during a real teach run before anyone noticed.
|
|
28
|
+
|
|
29
|
+
Two files agreeing is what a drift check measures. Whether either one is right about the tree is a different question, and this is the stage that asks it.
|
|
30
|
+
|
|
31
|
+
## The three forms
|
|
32
|
+
|
|
33
|
+
A citation reaches a reader in one of three shapes, and each resolves against a different root.
|
|
34
|
+
|
|
35
|
+
| Form | Shape | Resolves against |
|
|
36
|
+
| ---------- | ------------------------ | ---------------------------------- |
|
|
37
|
+
| `standard` | `aitk standards <name>` | `standards/<name>.md` |
|
|
38
|
+
| `path` | a backticked path | the root |
|
|
39
|
+
| `sibling` | a bare `<nnn>-<slug>.md` | the folder the citing rule sits in |
|
|
40
|
+
|
|
41
|
+
`standard` is the live form and carries most of the corpus, which is what makes scoping this check to the two path shapes a check over almost nothing.
|
|
42
|
+
|
|
43
|
+
A standard name resolves against the authoring root and nowhere else, matching `standardRoots` in `src/standards/read.ts`, which reads `standards/` at the working root and then the package corpus. The stage refuses a tree holding no rule corpus, so it runs only where those two roots are one directory. `internal/standards/` is deliberately not tried: `aitk standards <name>` never reaches it, so admitting it would pass a citation that refuses for the session opening it, which is a gate failing open.
|
|
44
|
+
|
|
45
|
+
A `path` is anchored on the whole backticked span rather than on a trailing pattern inside it. Cutting `standards/tooling-reference.md` out of `internal/standards/tooling-reference.md` and resolving that against the standards root reports a file that exists as missing, which is a mistake made by hand while measuring this corpus before the stage was written.
|
|
46
|
+
|
|
47
|
+
## What is not a citation
|
|
48
|
+
|
|
49
|
+
A rule body is full of backticked spans carrying a slash, and almost none of them names a file in this tree. The stage declines four shapes outright.
|
|
50
|
+
|
|
51
|
+
- A placeholder or glob segment describes a shape rather than naming a file: `.claude/context/<domain>.md`, `standards/<name>.md`, `app/**/route.ts`, `${CLAUDE_SKILL_DIR}/../../standards/<name>.md`.
|
|
52
|
+
- A bare filename names a convention: `route.ts`, `manifest.toml`, `components.json`, `playwright.config.ts`. A bare name is read as a citation only when it matches a rule filename, which is how `562-session.md` points at `555-tasks.md`.
|
|
53
|
+
- A span carrying no file extension is a folder or a module specifier: `src/pages/`, `next/font`, `try/except`, `react-hooks/set-state-in-effect`, `oven-sh/setup-bun@v2`, `@/lib/utils`.
|
|
54
|
+
- A fenced block displays a path rather than pointing at one.
|
|
55
|
+
|
|
56
|
+
The extension test is what carries most of the separation, and it costs one real path: `claude/standards` is a symlink this stage declines to check because nothing in the span says it is a file. Declining is the right direction for a gate, where a guess that goes wrong fails a push over prose.
|
|
57
|
+
|
|
58
|
+
## Where an absent path is correct
|
|
59
|
+
|
|
60
|
+
Two classes resolve to nothing and are right to. Both are reported by name rather than dropped, so a reader can see what the verdict declined to judge.
|
|
61
|
+
|
|
62
|
+
**Governed.** A rule spelling a path in its own frontmatter `paths:` is naming an artifact a target holds rather than a file here. `governance/rules/claude/560-diagrams.md` declares `.claude/DIAGRAMS.md` and then tells its reader to convert one an older install left behind, so the file is correctly absent from this tree and correctly named in the rule.
|
|
63
|
+
|
|
64
|
+
Only an exact declaration exempts, never a glob match against one. A glob declares a shape, so a body path sitting inside it is still a citation and a stale one is still a defect. A rule scoped at `docs/**` citing a `docs/agents/renamed.md` that moved is exactly the class this stage exists to catch, and matching the glob would excuse it.
|
|
65
|
+
|
|
66
|
+
**Ignored.** A path git ignores is session scratch no clone is expected to hold. `governance/rules/claude/555-tasks.md` cites `.claude/tasks/index.md`, which is real in a live project and absent from a fresh clone and from every linked worktree. Resolving against the filesystem alone would make the verdict depend on which tree the stage ran in, so the unresolved paths go to one batched `git check-ignore` and an ignored one is excused. A read git cannot answer refuses rather than reporting those paths dead.
|
|
67
|
+
|
|
68
|
+
## Why the glob half reads one corpus
|
|
69
|
+
|
|
70
|
+
Bodies are read across both corpora and frontmatter globs across `internal/rules/` alone.
|
|
71
|
+
|
|
72
|
+
A rule under `governance/rules/` installs into a target, and its `paths:` entries name the shape that project holds rather than anything here. Measured over that corpus, 32 of its 72 globs match nothing in this tree and every one of them is correct. `src/pages/**` in the Astro rule cannot be told by pattern from a path this repository might hold, so a check cannot separate the two, and gating them would ship an exemption list the length of the corpus. Nothing reads the shipped globs, and the measurement is the reason rather than a gap left for someone to close.
|
|
73
|
+
|
|
74
|
+
`internal/rules/` ships nowhere. The tree it governs is the tree present, which makes the question answerable, and all 14 of its globs across 7 rules match at the commit this shipped on.
|
|
75
|
+
|
|
76
|
+
What that leaves unreached is a glob that matches real files and still reaches none of the work it was scoped at. `governance/rules/lib/305-e2e-reliability.md` scopes itself at `e2e/*.ts` and `e2e/**/*.ts`, and no probe in this repository is written under `e2e/`, so the rule asking a session to watch a new guard fail never fired for the session writing guards. Resolution is mechanical and reach is a judgment about where the work happens, so only the first is here.
|
|
77
|
+
|
|
78
|
+
## The exemption marker
|
|
79
|
+
|
|
80
|
+
Everything the classifier can separate mechanically is separated there. For the residue, a line carries `aitk-allow-citation: <reason>` on itself or the one directly above, which moves it into the report's `Exempt` section. Only a marker naming a reason counts, since a bare token is a line that meant to say something and did not. This is the `aitk-allow-superseded` shape, and both read the same placement rule through one helper.
|
|
81
|
+
|
|
82
|
+
## The blind spots
|
|
83
|
+
|
|
84
|
+
A citation that resolves and points at the wrong file passes. Resolution is mechanical and correctness is a reading, and only the first is a gate's business.
|
|
85
|
+
|
|
86
|
+
A path written into running prose without backticks is not read at all. Matching one would report every sentence that happens to name a file, and the backticks are what separate a reference a reader follows from a name in a sentence.
|
|
87
|
+
|
|
88
|
+
A folder or a path carrying no extension is declined rather than guessed at, which is stated above as the cost of the test that carries the separation.
|
|
89
|
+
|
|
90
|
+
A glob under `governance/rules/` is not read, and a glob that resolves while reaching none of the work it was scoped at is a reading rather than a resolution. Both are stated above.
|
|
91
|
+
|
|
92
|
+
## Exit codes
|
|
93
|
+
|
|
94
|
+
Exit codes are `0` when every citation resolves or is excused and every glob read matches, `1` for a refusal, and `2` for at least one cited path reaching nothing or one glob matching nothing. It refuses a tree holding neither rule corpus, since a tree with no rules passes each of its zero rules, and a `git check-ignore` read that fails.
|
|
95
|
+
|
|
96
|
+
This gates, and `bun run check` runs it as the `Rule citations` stage. The sibling sweeps report rather than gate because a value appears for reasons unrelated to the convention, so their output is a reading. A path resolving to nothing carries no judgment: either the file is there or the citation is stale, and the two classes where absence is correct are separated before the verdict rather than left for a reader to settle. The corpus is clean at the commit this shipped on, so the gate starts green and stays that way until something breaks.
|
|
97
|
+
|
|
98
|
+
An exit code says nothing about a call made from a session, since a shell profile may wrap the binary in a function taking its status from a later command. Read the JSON record's `citations` array and filter on `status` rather than the exit when a skill consumes this.
|
package/docs/ai-workflow.md
CHANGED
|
@@ -144,6 +144,10 @@ Markdown under one states what an agent does, so a branch touching it reaches re
|
|
|
144
144
|
|
|
145
145
|
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.
|
|
146
146
|
|
|
147
|
+
`aitk autoship classify` answers that decision now, and the chain branches on the record it returns rather than on a session applying the list above. Three runs read past the list while it was prose, the last of them a driven arm that staged a file the list names and shipped a draft pull request with no review. The verb takes the names the chain already computed, so no second diff baseline resolves, and it names the file and the test that decided. [Review classification](agents/review-classification.md) carries the record shape and the exit codes.
|
|
148
|
+
|
|
149
|
+
The list stays written in the skill body as the fallback for a target whose installed CLI predates the verb, since the two ship at different speeds. That fallback is never a skip: failing open is the defect the verb closes, so an absent subcommand routes to review rather than past it.
|
|
150
|
+
|
|
147
151
|
#### Memory in the chain
|
|
148
152
|
|
|
149
153
|
`git-ship` runs its verify gate and then opens on `claude-memory-capture`, which sends what the session learned to the surface that owns it. `autoship` reaches the same step by invoking that skill at its Step 7 rather than restating the order. A fact about a domain carrying an entry in `.claude/context/index.md` is routed to that entry, and `claude-docs` folds it in on the next step, so it ships in the same pull request. Anything no entry owns stays a file in `.claude/memory/`.
|
package/package.json
CHANGED
package/scripts/core/verify.sh
CHANGED
|
@@ -387,6 +387,22 @@ main() {
|
|
|
387
387
|
run_check "cd $PROJECT_ROOT && bun src/cli.ts context audit --citations-only" "A cited context path does not resolve. Run bun src/cli.ts context audit."
|
|
388
388
|
log_info "Context citations resolve"
|
|
389
389
|
|
|
390
|
+
# A rule citing a file that moved fails silently. The consumed-copy drift
|
|
391
|
+
# stage passes an authored rule and its copy that are wrong together, and
|
|
392
|
+
# nothing else resolves the path until a session opens it, which is how
|
|
393
|
+
# `561-teach.md` shipped a `references/glossary.md` that had never existed. A
|
|
394
|
+
# rule whose frontmatter glob names a directory that moved fails the same way,
|
|
395
|
+
# by never firing again.
|
|
396
|
+
#
|
|
397
|
+
# This gates for the reason the stage above gates: a path resolving to
|
|
398
|
+
# nothing carries no judgment. The classes where absence is correct, a path
|
|
399
|
+
# the rule declares in its own frontmatter and one git ignores, are separated
|
|
400
|
+
# inside the verb rather than left as a threshold here. Globs are read under
|
|
401
|
+
# `internal/rules/` alone, since a shipped rule's glob names a target's shape.
|
|
402
|
+
log_step "Rule citations"
|
|
403
|
+
run_check "cd $PROJECT_ROOT && bun src/cli.ts gov citations" "A path a rule cites, or an internal frontmatter glob, does not resolve. Run bun src/cli.ts gov citations."
|
|
404
|
+
log_info "Rule citations resolve"
|
|
405
|
+
|
|
390
406
|
# A banned character, word, or spelling is a fact rather than a threshold, so
|
|
391
407
|
# it fails the push while bullet, paragraph, and depth weight stay advisory
|
|
392
408
|
# for the reason the stage above leaves its own thresholds so.
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import {
|
|
2
|
+
BEHAVIOR_FILES,
|
|
3
|
+
BEHAVIOR_PREFIXES,
|
|
4
|
+
PROSE_EXTENSIONS,
|
|
5
|
+
} from '@/autoship/paths'
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Which of the two tests a file failed.
|
|
9
|
+
*
|
|
10
|
+
* Named apart rather than folded into one boolean, because the repair differs.
|
|
11
|
+
* A non-prose extension means the branch carries code and review is ordinary. A
|
|
12
|
+
* behavior path means prose that states what an agent does, which is the half
|
|
13
|
+
* three sessions read past.
|
|
14
|
+
*/
|
|
15
|
+
export type FailedTest = 'extension' | 'behavior-path'
|
|
16
|
+
|
|
17
|
+
/** Why a classification produced no reading. */
|
|
18
|
+
export type ClassifyRefusal = 'no-changes'
|
|
19
|
+
|
|
20
|
+
export type Classification =
|
|
21
|
+
| { readonly kind: 'skip' }
|
|
22
|
+
| {
|
|
23
|
+
readonly kind: 'review'
|
|
24
|
+
readonly test: FailedTest
|
|
25
|
+
readonly file: string
|
|
26
|
+
}
|
|
27
|
+
| { readonly kind: 'refused'; readonly reason: ClassifyRefusal }
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Prefix-anchored against the folders, matching how `resolveCoverage` reads the
|
|
31
|
+
* label map, and equality against the whole paths a prefix cannot reach.
|
|
32
|
+
*/
|
|
33
|
+
function underBehaviorPath(path: string): boolean {
|
|
34
|
+
if (BEHAVIOR_FILES.some((file) => file === path)) return true
|
|
35
|
+
return BEHAVIOR_PREFIXES.some((prefix) => path.startsWith(prefix))
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function readsAsProse(path: string): boolean {
|
|
39
|
+
const lowered = path.toLowerCase()
|
|
40
|
+
return PROSE_EXTENSIONS.some((extension) => lowered.endsWith(extension))
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Decides whether a branch's changed set skips the review pass.
|
|
45
|
+
*
|
|
46
|
+
* The skip needs both tests to pass: every name reads as prose, and no name
|
|
47
|
+
* sits under a behavior path. One behavior file sends the whole branch to
|
|
48
|
+
* review, since documentation shipped beside a behavior change does not cancel
|
|
49
|
+
* it.
|
|
50
|
+
*
|
|
51
|
+
* An empty set refuses rather than skipping. Both tests are universally
|
|
52
|
+
* quantified, so an empty set satisfies them vacuously, and reading that as
|
|
53
|
+
* prose-only would route a branch past review for having produced no output at
|
|
54
|
+
* all.
|
|
55
|
+
*
|
|
56
|
+
* Names only. The caller hands the set the ship chain already computed, so no
|
|
57
|
+
* second baseline is read here and the stale-baseline half stays closed.
|
|
58
|
+
*/
|
|
59
|
+
export function classifyChanges(paths: readonly string[]): Classification {
|
|
60
|
+
if (paths.length === 0) return { kind: 'refused', reason: 'no-changes' }
|
|
61
|
+
|
|
62
|
+
// First failure in the caller's order wins, and each file is tested for its
|
|
63
|
+
// extension before its path. A set failing both tests reports one file and
|
|
64
|
+
// one test, which is all the branch needs to route.
|
|
65
|
+
for (const path of paths) {
|
|
66
|
+
if (!readsAsProse(path)) {
|
|
67
|
+
return { kind: 'review', test: 'extension', file: path }
|
|
68
|
+
}
|
|
69
|
+
if (underBehaviorPath(path)) {
|
|
70
|
+
return { kind: 'review', test: 'behavior-path', file: path }
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return { kind: 'skip' }
|
|
75
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The surfaces where markdown states what an agent does, and the extensions
|
|
3
|
+
* that read as prose, held as data one command parses.
|
|
4
|
+
*
|
|
5
|
+
* Lifted verbatim in content from the list `claude-autoship/SKILL.md` Step 5
|
|
6
|
+
* carried, which a session was asked to apply by hand. It failed that
|
|
7
|
+
* application three times, so the set moved here and the body now calls a verb
|
|
8
|
+
* that reads it. Being machine-parsed makes it permanently exempt from any
|
|
9
|
+
* later design that folds a standard back into the surface citing it, per the
|
|
10
|
+
* machine-parsed clause in `.claude/ARCHITECTURE.md`.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Path prefixes under which a markdown change is a behavior change.
|
|
15
|
+
*
|
|
16
|
+
* Most carry two spellings, the one a surface authors at and the one it reaches
|
|
17
|
+
* a session at, so the rule reads the same in this toolkit and in a project
|
|
18
|
+
* that consumed one. `standards/` and `snippets/` carry one apiece: neither
|
|
19
|
+
* installs into a project, reaching a reader by resolution and through the
|
|
20
|
+
* `claude/snippets` symlink respectively, so no `.claude/` spelling exists to
|
|
21
|
+
* name.
|
|
22
|
+
*/
|
|
23
|
+
export const BEHAVIOR_PREFIXES = [
|
|
24
|
+
'claude/skills/',
|
|
25
|
+
'.claude/skills/',
|
|
26
|
+
'governance/rules/',
|
|
27
|
+
'.claude/rules/',
|
|
28
|
+
'standards/',
|
|
29
|
+
'snippets/',
|
|
30
|
+
'internal/',
|
|
31
|
+
'tooling/',
|
|
32
|
+
] as const
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Behavior surfaces named as whole paths rather than as prefixes.
|
|
36
|
+
*
|
|
37
|
+
* A path prefix reaches nothing that sits in no folder, which is what the root
|
|
38
|
+
* instruction file is. Matching is equality rather than a suffix test, so a
|
|
39
|
+
* nested `docs/CLAUDE.md` stays informational the way the body's "at the
|
|
40
|
+
* repository root" qualifier says it should.
|
|
41
|
+
*/
|
|
42
|
+
export const BEHAVIOR_FILES = ['CLAUDE.md'] as const
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Extensions a changed file may carry for the branch to read as prose-only.
|
|
46
|
+
*
|
|
47
|
+
* Compared lowercased, so a `README.MD` is not routed to review for its
|
|
48
|
+
* spelling. Everything else is code, a config, or an asset, and one of them
|
|
49
|
+
* sends the whole branch to review.
|
|
50
|
+
*/
|
|
51
|
+
export const PROSE_EXTENSIONS = ['.md', '.txt'] as const
|
package/src/cli.ts
CHANGED
|
@@ -33,6 +33,7 @@ import { register as audits } from '@/commands/audits'
|
|
|
33
33
|
import { register as secrets } from '@/commands/secrets'
|
|
34
34
|
import { register as deps } from '@/commands/deps'
|
|
35
35
|
import { register as labels } from '@/commands/labels'
|
|
36
|
+
import { register as autoship } from '@/commands/autoship'
|
|
36
37
|
import { register as census } from '@/commands/census'
|
|
37
38
|
import { register as targets } from '@/commands/targets'
|
|
38
39
|
import { register as upgrade } from '@/commands/upgrade'
|
|
@@ -80,6 +81,7 @@ function showHelp(): void {
|
|
|
80
81
|
`${GREY}│${NC} secrets [cmd] ${GREY}# Read the shipped tree for credential-shaped values (scan)${NC}`,
|
|
81
82
|
`${GREY}│${NC} deps [cmd] ${GREY}# Read the resolved dependency set for advisories (audit)${NC}`,
|
|
82
83
|
`${GREY}│${NC} labels [cmd] ${GREY}# Read a changed set against the pull request label map (audit)${NC}`,
|
|
84
|
+
`${GREY}│${NC} autoship [cmd] ${GREY}# Decide whether a changed set needs the review pass (classify)${NC}`,
|
|
83
85
|
`${GREY}│${NC} census [path] ${GREY}# Report tracked file count, extension breakdown, and line totals${NC}`,
|
|
84
86
|
`${GREY}│${NC} audits [cmd] ${GREY}# Run every health check as one set (run, list)${NC}`,
|
|
85
87
|
`${GREY}│${NC} upgrade ${GREY}# Reinstall the CLI globally with the manager that installed it${NC}`,
|
|
@@ -181,6 +183,7 @@ worktrees(program)
|
|
|
181
183
|
secrets(program)
|
|
182
184
|
deps(program)
|
|
183
185
|
labels(program)
|
|
186
|
+
autoship(program)
|
|
184
187
|
census(program)
|
|
185
188
|
audits(program)
|
|
186
189
|
upgrade(program)
|
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import type { Command } from 'commander'
|
|
2
|
+
import {
|
|
3
|
+
type ClassifyRefusal,
|
|
4
|
+
type FailedTest,
|
|
5
|
+
classifyChanges,
|
|
6
|
+
} from '@/autoship/classify'
|
|
7
|
+
import { intro, logInfo, logStep, logWarn, outro, plural } from '@/ui'
|
|
8
|
+
|
|
9
|
+
interface ClassifyOptions {
|
|
10
|
+
readonly json?: boolean
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/** What a reader does about the one way the classification produced no reading. */
|
|
14
|
+
const REFUSALS: Record<ClassifyRefusal, string> = {
|
|
15
|
+
'no-changes':
|
|
16
|
+
'No changed files were supplied, so there is no set to classify. Pass the set the ship chain already computed.',
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Why each failed test sends the branch to review, in the reader's terms. */
|
|
20
|
+
const TESTS: Record<FailedTest, string> = {
|
|
21
|
+
extension: 'is not prose, so this branch carries a change review reads',
|
|
22
|
+
'behavior-path':
|
|
23
|
+
'sits under a behavior path, so its prose states what an agent does',
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function register(program: Command): void {
|
|
27
|
+
const autoship = program
|
|
28
|
+
.command('autoship')
|
|
29
|
+
.description('Answer the decisions the ship chain used to make in prose')
|
|
30
|
+
.helpOption('-h, --help', 'Show this help message')
|
|
31
|
+
|
|
32
|
+
autoship
|
|
33
|
+
.command('classify')
|
|
34
|
+
.description('Decide whether a changed set needs the review pass')
|
|
35
|
+
.argument('[paths...]', 'Changed set to classify, as names')
|
|
36
|
+
.helpOption('-h, --help', 'Show this help message')
|
|
37
|
+
.option('--json', 'Add a machine-readable record on stdout')
|
|
38
|
+
.addHelpText(
|
|
39
|
+
'after',
|
|
40
|
+
[
|
|
41
|
+
'',
|
|
42
|
+
'Reads names only and never touches git, so the caller hands over the',
|
|
43
|
+
'set it already computed rather than paying for a second baseline that',
|
|
44
|
+
'can disagree with the first.',
|
|
45
|
+
'',
|
|
46
|
+
'The skip needs both tests to pass. Every name reads as prose, and no',
|
|
47
|
+
'name sits under a behavior path, which is where markdown states what',
|
|
48
|
+
'an agent does. One behavior file sends the whole branch to review,',
|
|
49
|
+
'since documentation shipped beside a behavior change does not cancel',
|
|
50
|
+
'it.',
|
|
51
|
+
'',
|
|
52
|
+
'An empty set refuses rather than skipping. Both tests are universally',
|
|
53
|
+
'quantified, so an empty set satisfies them vacuously, and reading that',
|
|
54
|
+
'as prose-only would route a branch past review for producing no output.',
|
|
55
|
+
'',
|
|
56
|
+
'Exit codes:',
|
|
57
|
+
' 0 prose-only, so the review pass can be skipped',
|
|
58
|
+
' 1 refused, with the reason on stderr or in the JSON record',
|
|
59
|
+
' 2 review is needed, with the file and the test it failed named',
|
|
60
|
+
'',
|
|
61
|
+
'Examples:',
|
|
62
|
+
' aitk autoship classify docs/index.md README.md',
|
|
63
|
+
' aitk autoship classify --json .claude/skills/deploy-check/SKILL.md',
|
|
64
|
+
'',
|
|
65
|
+
].join('\n'),
|
|
66
|
+
)
|
|
67
|
+
.action((paths: string[], opts: ClassifyOptions) => {
|
|
68
|
+
process.exitCode = runClassify(paths, opts)
|
|
69
|
+
})
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function runClassify(paths: string[], opts: ClassifyOptions): number {
|
|
73
|
+
const emitJson = opts.json ?? false
|
|
74
|
+
const result = classifyChanges(paths)
|
|
75
|
+
|
|
76
|
+
intro('aitk autoship classify')
|
|
77
|
+
|
|
78
|
+
// The frame renders on stderr in both modes and the record goes to stdout
|
|
79
|
+
// alone, so an operator reading the terminal sees the refusal rather than a
|
|
80
|
+
// command that appeared to do nothing.
|
|
81
|
+
if (result.kind === 'refused') {
|
|
82
|
+
logStep('Refused')
|
|
83
|
+
logWarn(REFUSALS[result.reason])
|
|
84
|
+
outro()
|
|
85
|
+
|
|
86
|
+
if (emitJson) {
|
|
87
|
+
process.stdout.write(
|
|
88
|
+
`${JSON.stringify({
|
|
89
|
+
decision: 'refused',
|
|
90
|
+
reason: result.reason,
|
|
91
|
+
message: REFUSALS[result.reason],
|
|
92
|
+
})}\n`,
|
|
93
|
+
)
|
|
94
|
+
}
|
|
95
|
+
return 1
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
logStep('Scope')
|
|
99
|
+
logInfo(`${plural(paths.length, 'path')} supplied by the caller`)
|
|
100
|
+
|
|
101
|
+
if (result.kind === 'skip') {
|
|
102
|
+
logStep('Prose only')
|
|
103
|
+
logInfo('every path reads as prose and none states agent behavior')
|
|
104
|
+
outro()
|
|
105
|
+
|
|
106
|
+
if (emitJson) {
|
|
107
|
+
process.stdout.write(
|
|
108
|
+
`${JSON.stringify({ decision: 'skip', changed: paths })}\n`,
|
|
109
|
+
)
|
|
110
|
+
}
|
|
111
|
+
return 0
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
logStep('Review')
|
|
115
|
+
logWarn(`${result.file} ${TESTS[result.test]}.`)
|
|
116
|
+
outro()
|
|
117
|
+
|
|
118
|
+
if (emitJson) {
|
|
119
|
+
process.stdout.write(
|
|
120
|
+
`${JSON.stringify({
|
|
121
|
+
decision: 'review',
|
|
122
|
+
test: result.test,
|
|
123
|
+
file: result.file,
|
|
124
|
+
changed: paths,
|
|
125
|
+
})}\n`,
|
|
126
|
+
)
|
|
127
|
+
}
|
|
128
|
+
return 2
|
|
129
|
+
}
|