@erclx/aitk 3.21.0 → 3.23.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/docs/agents/audits.md +1 -1
- package/docs/agents/commands.md +52 -52
- package/docs/agents/index.md +1 -1
- package/docs/agents/restated.md +9 -6
- package/package.json +1 -1
- package/src/commands/gov.ts +11 -9
- package/src/gov/restated.ts +142 -16
- package/standards/plan.md +1 -1
- package/standards/tasks.md +3 -1
package/docs/agents/audits.md
CHANGED
|
@@ -26,7 +26,7 @@ Nineteen verbs, listed by `aitk audits list`. Each runs once in its fullest form
|
|
|
26
26
|
|
|
27
27
|
The verbs walk separate trees and share no state, so they run together. Measured on the authoring machine at twelve verbs, a run finished in 0.8 seconds of wall clock against 4.4 seconds of processor, which is under every other stage in `bun run check`. `aitk deps audit` is the one that changes that reading, since it reaches a network rather than a tree and its latency is the index's rather than this machine's.
|
|
28
28
|
|
|
29
|
-
Sixteen of the nineteen read a tree on this disk, the one added by `restated.md` and the one added by `census.md` among them, since the first reads
|
|
29
|
+
Sixteen of the nineteen read a tree on this disk, the one added by `restated.md` and the one added by `census.md` among them, since the first reads four such trees against each other and the second counts every file in one. The two added by `state-scoped-risk.md` read committed state rather than an arriving change, which is the gap every review surface here leaves by construction, and the one added by `label-coverage.md` reads a branch range against a map the project declares.
|
|
30
30
|
|
|
31
31
|
Each is invoked as the CLI the caller is running rather than as a global `aitk`. A globally installed binary resolves to the main checkout no matter which worktree is executing, so the aggregate would measure a tree the branch never touched and report a pass over it.
|
|
32
32
|
|
package/docs/agents/commands.md
CHANGED
|
@@ -9,58 +9,58 @@ Full help: `aitk <command> --help`. Behavior notes for the install and sync verb
|
|
|
9
9
|
|
|
10
10
|
## Project-level
|
|
11
11
|
|
|
12
|
-
| Command | Purpose
|
|
13
|
-
| -------------------------- |
|
|
14
|
-
| `aitk init [path]` | Bootstrap a project with selected toolkit domains
|
|
15
|
-
| `aitk sync [path]` | Sync all installed domains in a target project
|
|
16
|
-
| `aitk sync --check` | Report toolkit drift and the installed version against the newest published (`--json`, `--exit-code`)
|
|
17
|
-
| `aitk sandbox [cat:cmd]` | Run sandbox scenarios (interactive or routed), toolkit-only like the tree it reads
|
|
18
|
-
| `aitk sandbox reset` | Reset sandbox to baseline
|
|
19
|
-
| `aitk sandbox clean` | Wipe the sandbox
|
|
20
|
-
| `aitk sandbox check` | Score a provisioned sandbox against a scenario expectation (`--json` for the verdict)
|
|
21
|
-
| `aitk sandbox coverage` | Report which scenarios declare expectations (`--json`, `--strict`, `--skills`)
|
|
22
|
-
| `aitk indexes regen` | Regenerate `index.md` files from sibling frontmatter
|
|
23
|
-
| `aitk docs [topic]` | Emit toolkit reference docs (`list`, or a topic by name)
|
|
24
|
-
| `aitk design render` | Render `.claude/DESIGN.md` tokens to HTML and CSS
|
|
25
|
-
| `aitk slides render` | Render a `.claude/SLIDES.md` source into a PowerPoint deck
|
|
26
|
-
| `aitk slides list` | List the available slide layouts (`--json` for the catalog)
|
|
27
|
-
| `aitk feedback` | Write toolkit feedback from stdin to `.claude/review/feedback/`, or open a GitHub issue with `--github`
|
|
28
|
-
| `aitk transcripts <url>` | Fetch a YouTube transcript with metadata frontmatter (needs `yt-dlp`)
|
|
29
|
-
| `aitk tasks archive` | Move a shipped task off the board, clear its ordering row, and regenerate the index
|
|
30
|
-
| `aitk tasks pull-request` | Record a pull request number on the task a branch closes, by stem or `--plan` (`--json`)
|
|
31
|
-
| `aitk tasks outcome` | Mark outcomes `[x]` on a task by position, repeating `--close` (`--json`)
|
|
32
|
-
| `aitk tasks validate` | Report board rows whose plan, task file, group, file set, or blocker does not hold (`--json`)
|
|
33
|
-
| `aitk intake list` | Report intake folder counts, or one folder's items, keeping what is unread with `--unread` (`--json`)
|
|
34
|
-
| `aitk intake answer` | Write selections into one cluster's answer slots, repeating `--set <item>=<answer>` (`--json`)
|
|
35
|
-
| `aitk teach list` | Report learning workspaces and the ordinal a new one takes, or what one workspace holds (`--json`)
|
|
36
|
-
| `aitk teach open` | Open a workspace at the next ordinal with its mission, resources, and glossary files (`--json`)
|
|
37
|
-
| `aitk teach resource` | Record sources and leads in a workspace, repeating `--read` or `--lead` as `<title>=<url>` (`--json`)
|
|
38
|
-
| `aitk teach glossary` | Add terms to a workspace glossary alphabetically, repeating `--term <term>=<definition>` (`--json`)
|
|
39
|
-
| `aitk records validate` | Report a session record or a standard against the standard governing it, per kind (`--json`)
|
|
40
|
-
| `aitk records migrate` | Rewrite the records a validate finding names a recoverable transform for (`--write`, `--json`)
|
|
41
|
-
| `aitk records size` | Report what each record folder holds and how much of it is recent, heaviest first (`--json`)
|
|
42
|
-
| `aitk records push` | Commit the nine backed record folders and push them to a private records remote (`--json`)
|
|
43
|
-
| `aitk records pull` | Fetch the records remote and write it back, refusing rather than discarding unpushed records (`--json`)
|
|
44
|
-
| `aitk sessions list` | Resolve live sessions to the worktree and branch each holds, filtered by `--branch` (`--json`)
|
|
45
|
-
| `aitk comments scan` | Measure comment density by language and comment kind, with a trend recomputed from git
|
|
46
|
-
| `aitk context audit` | Report required sections, length, cited paths, reference form, catalog tables, provenance, superseded-decision narration, and index drift
|
|
47
|
-
| `aitk markdown audit` | Fail any markdown path on a banned character, word, or spelling, and report the structural checkpoints
|
|
48
|
-
| `aitk claude skills audit` | Report both skill corpora against the mechanical rules in `standards/skill.md`
|
|
49
|
-
| `aitk claude skills drift` | Name the shipped skill bodies rewritten between a given ref and `HEAD`, and the installed version against the newest published (`--json`)
|
|
50
|
-
| `aitk claude skills reach` | Report the shipped bodies citing a toolkit path no target project receives, exiting 2 on an unqualified one
|
|
51
|
-
| `aitk claude skills rank` | Score the shipped catalog's descriptions against a hand-authored case corpus by TF-IDF cosine similarity, reporting rank-one and top-three
|
|
52
|
-
| `aitk claude routing` | Report per `CLAUDE.md` section how many bullets name a path and how many of those a path-scoped rule already covers (`--json`)
|
|
53
|
-
| `aitk gov test-order` | Report where an implementation reached history ahead of the test covering it (`--json`)
|
|
54
|
-
| `aitk gov superseded` | Report where the tree still asserts a value a changed convention no longer produces, keyed on the value (`--json`)
|
|
55
|
-
| `aitk gov restated` | Report every instruction the always-loaded file shares with the seed
|
|
56
|
-
| `aitk secrets scan` | Report credential-shaped values in the tree the package ships, keyed on issued values rather than on words (`--json`)
|
|
57
|
-
| `aitk deps audit` | Report published advisories against the resolved dependency set, refusing rather than reporting clean when the index is unreachable (`--json`)
|
|
58
|
-
| `aitk labels audit` | Report the labels a changed set earns from the pull request label map and the paths no row reaches (`--json`)
|
|
59
|
-
| `aitk census [path]` | Report tracked file count, a breakdown by extension, and a line total that skips whatever reads as binary (`--json`)
|
|
60
|
-
| `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`)
|
|
61
|
-
| `aitk audits list` | List every audit the set runs, with the corpus each reads and whether it gates (`--json`)
|
|
62
|
-
| `aitk capture [source]` | Render HTML capture sources to PNG, toolkit-only and absent from an installed package
|
|
63
|
-
| `aitk upgrade` | Reinstall the CLI globally with the package manager the install path names (`--json`)
|
|
12
|
+
| Command | Purpose |
|
|
13
|
+
| -------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
14
|
+
| `aitk init [path]` | Bootstrap a project with selected toolkit domains |
|
|
15
|
+
| `aitk sync [path]` | Sync all installed domains in a target project |
|
|
16
|
+
| `aitk sync --check` | Report toolkit drift and the installed version against the newest published (`--json`, `--exit-code`) |
|
|
17
|
+
| `aitk sandbox [cat:cmd]` | Run sandbox scenarios (interactive or routed), toolkit-only like the tree it reads |
|
|
18
|
+
| `aitk sandbox reset` | Reset sandbox to baseline |
|
|
19
|
+
| `aitk sandbox clean` | Wipe the sandbox |
|
|
20
|
+
| `aitk sandbox check` | Score a provisioned sandbox against a scenario expectation (`--json` for the verdict) |
|
|
21
|
+
| `aitk sandbox coverage` | Report which scenarios declare expectations (`--json`, `--strict`, `--skills`) |
|
|
22
|
+
| `aitk indexes regen` | Regenerate `index.md` files from sibling frontmatter |
|
|
23
|
+
| `aitk docs [topic]` | Emit toolkit reference docs (`list`, or a topic by name) |
|
|
24
|
+
| `aitk design render` | Render `.claude/DESIGN.md` tokens to HTML and CSS |
|
|
25
|
+
| `aitk slides render` | Render a `.claude/SLIDES.md` source into a PowerPoint deck |
|
|
26
|
+
| `aitk slides list` | List the available slide layouts (`--json` for the catalog) |
|
|
27
|
+
| `aitk feedback` | Write toolkit feedback from stdin to `.claude/review/feedback/`, or open a GitHub issue with `--github` |
|
|
28
|
+
| `aitk transcripts <url>` | Fetch a YouTube transcript with metadata frontmatter (needs `yt-dlp`) |
|
|
29
|
+
| `aitk tasks archive` | Move a shipped task off the board, clear its ordering row, and regenerate the index |
|
|
30
|
+
| `aitk tasks pull-request` | Record a pull request number on the task a branch closes, by stem or `--plan` (`--json`) |
|
|
31
|
+
| `aitk tasks outcome` | Mark outcomes `[x]` on a task by position, repeating `--close` (`--json`) |
|
|
32
|
+
| `aitk tasks validate` | Report board rows whose plan, task file, group, file set, or blocker does not hold (`--json`) |
|
|
33
|
+
| `aitk intake list` | Report intake folder counts, or one folder's items, keeping what is unread with `--unread` (`--json`) |
|
|
34
|
+
| `aitk intake answer` | Write selections into one cluster's answer slots, repeating `--set <item>=<answer>` (`--json`) |
|
|
35
|
+
| `aitk teach list` | Report learning workspaces and the ordinal a new one takes, or what one workspace holds (`--json`) |
|
|
36
|
+
| `aitk teach open` | Open a workspace at the next ordinal with its mission, resources, and glossary files (`--json`) |
|
|
37
|
+
| `aitk teach resource` | Record sources and leads in a workspace, repeating `--read` or `--lead` as `<title>=<url>` (`--json`) |
|
|
38
|
+
| `aitk teach glossary` | Add terms to a workspace glossary alphabetically, repeating `--term <term>=<definition>` (`--json`) |
|
|
39
|
+
| `aitk records validate` | Report a session record or a standard against the standard governing it, per kind (`--json`) |
|
|
40
|
+
| `aitk records migrate` | Rewrite the records a validate finding names a recoverable transform for (`--write`, `--json`) |
|
|
41
|
+
| `aitk records size` | Report what each record folder holds and how much of it is recent, heaviest first (`--json`) |
|
|
42
|
+
| `aitk records push` | Commit the nine backed record folders and push them to a private records remote (`--json`) |
|
|
43
|
+
| `aitk records pull` | Fetch the records remote and write it back, refusing rather than discarding unpushed records (`--json`) |
|
|
44
|
+
| `aitk sessions list` | Resolve live sessions to the worktree and branch each holds, filtered by `--branch` (`--json`) |
|
|
45
|
+
| `aitk comments scan` | Measure comment density by language and comment kind, with a trend recomputed from git |
|
|
46
|
+
| `aitk context audit` | Report required sections, length, cited paths, reference form, catalog tables, provenance, superseded-decision narration, and index drift |
|
|
47
|
+
| `aitk markdown audit` | Fail any markdown path on a banned character, word, or spelling, and report the structural checkpoints |
|
|
48
|
+
| `aitk claude skills audit` | Report both skill corpora against the mechanical rules in `standards/skill.md` |
|
|
49
|
+
| `aitk claude skills drift` | Name the shipped skill bodies rewritten between a given ref and `HEAD`, and the installed version against the newest published (`--json`) |
|
|
50
|
+
| `aitk claude skills reach` | Report the shipped bodies citing a toolkit path no target project receives, exiting 2 on an unqualified one |
|
|
51
|
+
| `aitk claude skills rank` | Score the shipped catalog's descriptions against a hand-authored case corpus by TF-IDF cosine similarity, reporting rank-one and top-three |
|
|
52
|
+
| `aitk claude routing` | Report per `CLAUDE.md` section how many bullets name a path and how many of those a path-scoped rule already covers (`--json`) |
|
|
53
|
+
| `aitk gov test-order` | Report where an implementation reached history ahead of the test covering it (`--json`) |
|
|
54
|
+
| `aitk gov superseded` | Report where the tree still asserts a value a changed convention no longer produces, keyed on the value (`--json`) |
|
|
55
|
+
| `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`) |
|
|
56
|
+
| `aitk secrets scan` | Report credential-shaped values in the tree the package ships, keyed on issued values rather than on words (`--json`) |
|
|
57
|
+
| `aitk deps audit` | Report published advisories against the resolved dependency set, refusing rather than reporting clean when the index is unreachable (`--json`) |
|
|
58
|
+
| `aitk labels audit` | Report the labels a changed set earns from the pull request label map and the paths no row reaches (`--json`) |
|
|
59
|
+
| `aitk census [path]` | Report tracked file count, a breakdown by extension, and a line total that skips whatever reads as binary (`--json`) |
|
|
60
|
+
| `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`) |
|
|
61
|
+
| `aitk audits list` | List every audit the set runs, with the corpus each reads and whether it gates (`--json`) |
|
|
62
|
+
| `aitk capture [source]` | Render HTML capture sources to PNG, toolkit-only and absent from an installed package |
|
|
63
|
+
| `aitk upgrade` | Reinstall the CLI globally with the package manager the install path names (`--json`) |
|
|
64
64
|
|
|
65
65
|
`aitk demo` is the second browser command and the one that ships, since its purpose is running in a target rather than regenerating what this repository commits. It needs a browser binary the package does not carry, installed once with `bunx playwright install chromium`.
|
|
66
66
|
|
package/docs/agents/index.md
CHANGED
|
@@ -26,7 +26,7 @@ CLI catalog and invocation rules for agents, split by command domain. Start with
|
|
|
26
26
|
- [Output shape](output-shape.md): Two framed shapes every command renders into, how JSON and --names modes keep stdout clean, and the exit discipline that lets piped output drain
|
|
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
|
-
- [Restated instructions](restated.md): Counting the instructions the always-loaded file
|
|
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
30
|
- [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
|
|
31
31
|
- [Sandbox](sandbox.md): Scenario routing, the expectation scoring surface, and the coverage census over scenarios and skills
|
|
32
32
|
- [Scripting](scripting.md): The runtime catalogs that replace hardcoded names, what each carries, and a headless invocation per domain
|
package/docs/agents/restated.md
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
---
|
|
2
2
|
title: Restated instructions
|
|
3
|
-
description: Counting the instructions the always-loaded file
|
|
3
|
+
description: 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
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Restated instructions
|
|
7
7
|
|
|
8
|
-
`aitk gov restated` reports every instruction the always-loaded file states that a
|
|
8
|
+
`aitk gov restated` reports every instruction the always-loaded file or a path-scoped rule states that a further surface states too. It answers a question nothing else here could: one rule was found written in three places, and it was found by accident when a shipped skill deleted a file the rule said to keep.
|
|
9
9
|
|
|
10
10
|
```bash
|
|
11
11
|
aitk gov restated
|
|
@@ -22,14 +22,17 @@ Under `--json` the record holds stdout alone and the frame still renders on stde
|
|
|
22
22
|
|
|
23
23
|
## The corpus it reads
|
|
24
24
|
|
|
25
|
-
|
|
25
|
+
Four surfaces, and they are not read symmetrically. Every top-level bullet in `CLAUDE.md` and in every path-scoped rule is a subject, and every surface below is searched, rules included a second time:
|
|
26
26
|
|
|
27
27
|
- `tooling/claude/seeds/CLAUDE.md`, read as bullets, since the seed carries the same shape as the file it is authored from
|
|
28
28
|
- `claude/skills/*/SKILL.md`, read as every prose line and bullet, since the motivating case was stated in a body as a paragraph rather than a list item
|
|
29
|
+
- `governance/rules/**/*.md`, the authoring root rather than the consumed copy under `.claude/rules/`, read as bullets the same way the seed is
|
|
30
|
+
|
|
31
|
+
A rule reads as both a subject and a candidate, unlike the seed and the shipped bodies. A stack ships a whole rule folder, so a bullet duplicated between two rules reaches a target exactly as a bullet duplicated between the always-loaded file and a rule does, and neither shape is visible from one side alone. A candidate sharing its subject's file is skipped, since two bullets inside one rule sharing anchors are adjacent instructions on one topic rather than the same rule shipped twice, and a pair found from one direction is not reported again from the other.
|
|
29
32
|
|
|
30
33
|
Frontmatter, headings, tables, and fenced blocks are read past. A heading names a section instead of stating a rule, a fenced block is an example whose words belong to the prose around it, and a body's `description` restates that skill's own purpose, so sweeping it would match every subject naming its domain.
|
|
31
34
|
|
|
32
|
-
A rule stated in two skill bodies and never in `CLAUDE.md` is outside this reading.
|
|
35
|
+
A rule stated in two skill bodies and never in `CLAUDE.md` or another rule is outside this reading. A body is searched but never read as a subject, and a subject is what the search runs from.
|
|
33
36
|
|
|
34
37
|
## How a match is decided
|
|
35
38
|
|
|
@@ -63,7 +66,7 @@ Each restatement names where a later edit starts.
|
|
|
63
66
|
|
|
64
67
|
- `claude-md` for a seed match, since the always-loaded file is authored first and the seed carries it to a target
|
|
65
68
|
- `skill-body` where the subject names that skill, which is the content-ownership rule that behavior triggered only when editing one domain belongs to that domain's skill
|
|
66
|
-
- `unknown` everywhere else, which is a first-class answer rather than a gap
|
|
69
|
+
- `unknown` everywhere else, a rule match included, which is a first-class answer rather than a gap
|
|
67
70
|
|
|
68
71
|
The ownership table assigns a cross-domain rule and a domain-triggered one, and reaches nothing stated in a skill body the always-loaded file never names. Guessing there would put a reader on a surface nobody decided.
|
|
69
72
|
|
|
@@ -73,6 +76,6 @@ Exit codes are `0` when no instruction is restated outside a declared mirror, `1
|
|
|
73
76
|
|
|
74
77
|
Nothing wires this into `bun run check` or into a hook. A restatement is legitimate more often than not, and gating a measure whose ordinary result is a finding is what teaches contributors to route around the stage. `aitk gov test-order` and `aitk labels audit` are the siblings.
|
|
75
78
|
|
|
76
|
-
Both refusals are absences rather than breaks. A target holds
|
|
79
|
+
Both refusals are absences rather than breaks. A target holds none of the seed, a shipped skills tree, or a rules tree, so `aitk audits run` reads `no-instructions` and `no-surfaces` as a corpus that is not there rather than a verb that failed.
|
|
77
80
|
|
|
78
81
|
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 record's `counts` rather than the exit when a skill consumes this.
|
package/package.json
CHANGED
package/src/commands/gov.ts
CHANGED
|
@@ -26,6 +26,7 @@ import {
|
|
|
26
26
|
type RestatedRefusal,
|
|
27
27
|
type RestatedReport,
|
|
28
28
|
readRestated,
|
|
29
|
+
RULES_REL as RESTATED_RULES_REL,
|
|
29
30
|
SEED_REL,
|
|
30
31
|
SHIPPED_SKILLS_REL,
|
|
31
32
|
} from '@/gov/restated'
|
|
@@ -97,7 +98,7 @@ interface CountsOptions {
|
|
|
97
98
|
/** What a reader does about each way the sweep produced no reading. */
|
|
98
99
|
const RESTATED_REFUSALS: Record<RestatedRefusal, string> = {
|
|
99
100
|
'no-instructions': `No ${INSTRUCTIONS_REL} here, or it carries no bullet, so there is no instruction corpus to sweep.`,
|
|
100
|
-
'no-surfaces': `
|
|
101
|
+
'no-surfaces': `None of ${SEED_REL}, ${SHIPPED_SKILLS_REL}/, or ${RESTATED_RULES_REL}/ is here, so no further surface exists to match against.`,
|
|
101
102
|
}
|
|
102
103
|
|
|
103
104
|
const COUNTS_REFUSALS: Record<CountsRefusal, string> = {
|
|
@@ -346,11 +347,12 @@ export function register(program: Command): void {
|
|
|
346
347
|
'after',
|
|
347
348
|
[
|
|
348
349
|
'',
|
|
349
|
-
`Matches every bullet in ${INSTRUCTIONS_REL}
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
'
|
|
353
|
-
'
|
|
350
|
+
`Matches every bullet in ${INSTRUCTIONS_REL} and every rule under`,
|
|
351
|
+
`${RESTATED_RULES_REL}/ against ${SEED_REL}, every`,
|
|
352
|
+
`${SHIPPED_SKILLS_REL}/*/SKILL.md body, and every other rule. Matching is`,
|
|
353
|
+
'recall-first, keyed on distinctive tokens two statements share rather',
|
|
354
|
+
'than on a phrase they spell the same way, because the case this exists',
|
|
355
|
+
'for was one rule written three different ways.',
|
|
354
356
|
'',
|
|
355
357
|
'What it separates:',
|
|
356
358
|
' mirror a declared authoring-to-consumed pair, where repeating is the design',
|
|
@@ -361,8 +363,8 @@ export function register(program: Command): void {
|
|
|
361
363
|
'about meaning, so weigh each against the surfaces it names.',
|
|
362
364
|
'',
|
|
363
365
|
'What it does not measure:',
|
|
364
|
-
' a rule stated in two skill bodies and never in the always-loaded file
|
|
365
|
-
' since
|
|
366
|
+
' a rule stated in two skill bodies and never in the always-loaded file',
|
|
367
|
+
' or a path-scoped rule, since a body is searched but never read as a subject',
|
|
366
368
|
'',
|
|
367
369
|
'Exit codes:',
|
|
368
370
|
' 0 no instruction is restated outside a declared mirror',
|
|
@@ -441,7 +443,7 @@ function reportRestated(
|
|
|
441
443
|
// also says how wide the corpus behind it was.
|
|
442
444
|
logStep('Corpus')
|
|
443
445
|
logInfo(
|
|
444
|
-
`${report.corpus.instructions} instruction(s) against ${report.corpus.candidates} statement(s) from the seed
|
|
446
|
+
`${report.corpus.instructions} always-loaded instruction(s) and ${report.corpus.rules} rule file(s) against ${report.corpus.candidates} statement(s) from the seed, ${report.corpus.bodies} shipped body/bodies, and every rule again, in ${root}`,
|
|
445
447
|
)
|
|
446
448
|
logInfo(
|
|
447
449
|
`matched on ${report.matcher.anchors} weighted anchor(s), dropping any token in more than ${report.matcher.common} statements`,
|
package/src/gov/restated.ts
CHANGED
|
@@ -10,6 +10,17 @@ export const SEED_REL = join('tooling', 'claude', 'seeds', 'CLAUDE.md')
|
|
|
10
10
|
/** The shipped plugin bodies, which is where a rule restated in prose lands. */
|
|
11
11
|
export const SHIPPED_SKILLS_REL = join('claude', 'skills')
|
|
12
12
|
|
|
13
|
+
/**
|
|
14
|
+
* Path-scoped rules, read from the authoring root rather than the consumed
|
|
15
|
+
* copy under `.claude/rules/`. A stack takes a whole rule folder, so a bullet
|
|
16
|
+
* landing anywhere under here ships to every target the stack reaches, the
|
|
17
|
+
* same way a bullet in the always-loaded file or the seed does. That is what
|
|
18
|
+
* makes a rule an instruction surface rather than a place a rule is merely
|
|
19
|
+
* quoted, and it is why a bullet moved here from the seed still belongs to
|
|
20
|
+
* the corpus this sweep reads rather than leaving it.
|
|
21
|
+
*/
|
|
22
|
+
export const RULES_REL = join('governance', 'rules')
|
|
23
|
+
|
|
13
24
|
/**
|
|
14
25
|
* Path pairs whose duplication is deliberate and already recorded.
|
|
15
26
|
*
|
|
@@ -168,7 +179,7 @@ export type RestatedRefusal = 'no-instructions' | 'no-surfaces'
|
|
|
168
179
|
export type Restatement = 'mirror' | 'repetition' | 'contradiction'
|
|
169
180
|
|
|
170
181
|
/** Which surface a restatement was found on, before any class is assigned. */
|
|
171
|
-
export type SurfaceKind = 'seed' | 'skill'
|
|
182
|
+
export type SurfaceKind = 'seed' | 'skill' | 'rule'
|
|
172
183
|
|
|
173
184
|
/**
|
|
174
185
|
* Which surface a later edit starts from.
|
|
@@ -209,7 +220,7 @@ export interface RestatedCounts {
|
|
|
209
220
|
readonly contradictions: number
|
|
210
221
|
readonly repetitions: number
|
|
211
222
|
readonly mirrors: number
|
|
212
|
-
/**
|
|
223
|
+
/** Duplicate clusters reaching three statements or more, the title's count. */
|
|
213
224
|
readonly threeSurface: number
|
|
214
225
|
}
|
|
215
226
|
|
|
@@ -220,7 +231,9 @@ export type RestatedReport =
|
|
|
220
231
|
readonly instructions: number
|
|
221
232
|
readonly seed: number
|
|
222
233
|
readonly bodies: number
|
|
223
|
-
/**
|
|
234
|
+
/** Distinct rule files a bullet was read from, counted like `bodies`. */
|
|
235
|
+
readonly rules: number
|
|
236
|
+
/** Statements the three further surfaces offered, which bounds recall. */
|
|
224
237
|
readonly candidates: number
|
|
225
238
|
}
|
|
226
239
|
readonly matcher: {
|
|
@@ -368,6 +381,53 @@ function isMirrorPair(subject: string, surface: string): boolean {
|
|
|
368
381
|
)
|
|
369
382
|
}
|
|
370
383
|
|
|
384
|
+
function find(parent: Map<string, string>, node: string): string {
|
|
385
|
+
const above = parent.get(node)
|
|
386
|
+
if (above === undefined || above === node) return node
|
|
387
|
+
const root = find(parent, above)
|
|
388
|
+
parent.set(node, root)
|
|
389
|
+
return root
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
function union(parent: Map<string, string>, a: string, b: string): void {
|
|
393
|
+
if (!parent.has(a)) parent.set(a, a)
|
|
394
|
+
if (!parent.has(b)) parent.set(b, b)
|
|
395
|
+
const rootA = find(parent, a)
|
|
396
|
+
const rootB = find(parent, b)
|
|
397
|
+
if (rootA !== rootB) parent.set(rootA, rootB)
|
|
398
|
+
}
|
|
399
|
+
|
|
400
|
+
/**
|
|
401
|
+
* How many duplicate clusters reach three member statements or more.
|
|
402
|
+
*
|
|
403
|
+
* A rule sitting on both sides lets a cluster of mutually restating
|
|
404
|
+
* statements fragment across several `RestatedEntry` records, one per member
|
|
405
|
+
* that happens to lead the subject loop, so counting an entry whose own
|
|
406
|
+
* `surfaces.length >= 2` counts the same cluster more than once. Union over
|
|
407
|
+
* every reported pair instead: two statements in one component are one
|
|
408
|
+
* duplicated instruction wherever it was found from, and the entries a
|
|
409
|
+
* reader sees are one view into that same union, never a second source for
|
|
410
|
+
* its total.
|
|
411
|
+
*/
|
|
412
|
+
function countThreeSurfaceClusters(pairs: ReadonlySet<string>): number {
|
|
413
|
+
const parent = new Map<string, string>()
|
|
414
|
+
|
|
415
|
+
for (const key of pairs) {
|
|
416
|
+
const [left, right] = key.split('|')
|
|
417
|
+
union(parent, left, right)
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
const sizes = new Map<string, number>()
|
|
421
|
+
for (const node of parent.keys()) {
|
|
422
|
+
const root = find(parent, node)
|
|
423
|
+
sizes.set(root, (sizes.get(root) ?? 0) + 1)
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
let clusters = 0
|
|
427
|
+
for (const size of sizes.values()) if (size >= 3) clusters += 1
|
|
428
|
+
return clusters
|
|
429
|
+
}
|
|
430
|
+
|
|
371
431
|
/**
|
|
372
432
|
* Bullets at the top level of a markdown file, which is the unit an instruction
|
|
373
433
|
* takes in the always-loaded file and in the seed.
|
|
@@ -398,6 +458,26 @@ function readBullets(root: string, relative: string): Statement[] {
|
|
|
398
458
|
return statements
|
|
399
459
|
}
|
|
400
460
|
|
|
461
|
+
/**
|
|
462
|
+
* Every top-level bullet across every rule file, keyed to the file it came
|
|
463
|
+
* from rather than folded into one corpus. A rule reads as bullets under the
|
|
464
|
+
* same convention the always-loaded file and the seed use, so `readBullets`
|
|
465
|
+
* is reused per file rather than rebuilt for prose, and frontmatter is read
|
|
466
|
+
* past for the same reason it is: no line there starts with `- `.
|
|
467
|
+
*/
|
|
468
|
+
function readRuleFiles(root: string, rulesRoot: string): Statement[] {
|
|
469
|
+
const files = [
|
|
470
|
+
...new Bun.Glob('**/*.md').scanSync({ cwd: rulesRoot, onlyFiles: true }),
|
|
471
|
+
].sort()
|
|
472
|
+
|
|
473
|
+
return files.flatMap((file) =>
|
|
474
|
+
readBullets(
|
|
475
|
+
root,
|
|
476
|
+
`${RULES_REL.replaceAll('\\', '/')}/${file.replaceAll('\\', '/')}`,
|
|
477
|
+
),
|
|
478
|
+
)
|
|
479
|
+
}
|
|
480
|
+
|
|
401
481
|
/**
|
|
402
482
|
* Every prose line and bullet in a shipped body.
|
|
403
483
|
*
|
|
@@ -550,7 +630,8 @@ function classify(
|
|
|
550
630
|
}
|
|
551
631
|
|
|
552
632
|
/**
|
|
553
|
-
* Every instruction
|
|
633
|
+
* Every instruction the always-loaded file or a path-scoped rule states that a
|
|
634
|
+
* further surface also states.
|
|
554
635
|
*
|
|
555
636
|
* Matching is recall-first, keyed on distinctive tokens two statements share
|
|
556
637
|
* rather than on a phrase they spell the same way. The motivating case was one
|
|
@@ -558,6 +639,13 @@ function classify(
|
|
|
558
639
|
* defect the sweep exists for, and a recall-first reading can be narrowed from
|
|
559
640
|
* real output where the reverse cannot.
|
|
560
641
|
*
|
|
642
|
+
* The always-loaded file and every rule each open the search as a subject, and
|
|
643
|
+
* the seed, the shipped bodies, and every rule again close it as a candidate.
|
|
644
|
+
* A rule sits on both sides because a stack ships a whole rule folder, so a
|
|
645
|
+
* bullet duplicated between two rules reaches a target exactly as a bullet
|
|
646
|
+
* duplicated between the always-loaded file and a rule does, and neither shape
|
|
647
|
+
* is visible from one side alone.
|
|
648
|
+
*
|
|
561
649
|
* It reports and never gates. A restatement is legitimate more often than not,
|
|
562
650
|
* so a push failing on one would fail on the ordinary case.
|
|
563
651
|
*/
|
|
@@ -574,14 +662,31 @@ export function readRestated(root: string): RestatedReport {
|
|
|
574
662
|
|
|
575
663
|
const skillsRoot = join(root, SHIPPED_SKILLS_REL)
|
|
576
664
|
const bodies = existsSync(skillsRoot) ? readBodyLines(root, skillsRoot) : []
|
|
577
|
-
|
|
578
665
|
const bodyFiles = new Set(bodies.map((candidate) => candidate.file)).size
|
|
579
|
-
|
|
666
|
+
|
|
667
|
+
const rulesRoot = join(root, RULES_REL)
|
|
668
|
+
const ruleStatements = existsSync(rulesRoot)
|
|
669
|
+
? readRuleFiles(root, rulesRoot)
|
|
670
|
+
: []
|
|
671
|
+
const rules: Candidate[] = ruleStatements.map((statement) => ({
|
|
672
|
+
...statement,
|
|
673
|
+
kind: 'rule' as const,
|
|
674
|
+
}))
|
|
675
|
+
const ruleFiles = new Set(ruleStatements.map((statement) => statement.file))
|
|
676
|
+
.size
|
|
677
|
+
|
|
678
|
+
if (seed.length === 0 && bodies.length === 0 && rules.length === 0) {
|
|
580
679
|
return { kind: 'unreadable', reason: 'no-surfaces' }
|
|
581
680
|
}
|
|
582
681
|
|
|
583
|
-
|
|
584
|
-
|
|
682
|
+
// A rule states a directive directly, the same as the always-loaded file, so
|
|
683
|
+
// it joins the subjects a match is searched from rather than sitting only on
|
|
684
|
+
// the candidate side. Without this, an instruction that moved out of the
|
|
685
|
+
// always-loaded file and into two rules has no subject left carrying it, and
|
|
686
|
+
// the pair the seed move produced would stay invisible to this sweep the
|
|
687
|
+
// same way it did before this corpus widened.
|
|
688
|
+
const subjects = [...instructions, ...rules].map(index)
|
|
689
|
+
const candidates = [...seed, ...bodies, ...rules].map(index)
|
|
585
690
|
const frequency = documentFrequency([subjects, candidates])
|
|
586
691
|
|
|
587
692
|
const distinctive = (analysis: Analysis): Set<string> =>
|
|
@@ -604,13 +709,37 @@ export function readRestated(root: string): RestatedReport {
|
|
|
604
709
|
let contradictions = 0
|
|
605
710
|
let repetitions = 0
|
|
606
711
|
let mirrors = 0
|
|
607
|
-
|
|
712
|
+
|
|
713
|
+
// A rule bullet sits on both sides now, so the pair it forms with another
|
|
714
|
+
// rule bullet would otherwise surface twice: once with each end read as the
|
|
715
|
+
// subject. Recording the pair the first time it is found and skipping it
|
|
716
|
+
// the second keeps one entry per duplicate regardless of which side a
|
|
717
|
+
// reader lands on, the way a seed-versus-body pair never could collide,
|
|
718
|
+
// since only a rule occupies both roles. The same skip is what fragments a
|
|
719
|
+
// cluster of mutually restating rules across several entries, which is why
|
|
720
|
+
// `countThreeSurfaceClusters` reads this set again below rather than the
|
|
721
|
+
// per-entry `surfaces.length` the loop produces.
|
|
722
|
+
const reportedPairs = new Set<string>()
|
|
723
|
+
const pairKey = (a: Statement, b: Statement): string => {
|
|
724
|
+
const left = `${a.file}:${a.line}`
|
|
725
|
+
const right = `${b.file}:${b.line}`
|
|
726
|
+
return left < right ? `${left}|${right}` : `${right}|${left}`
|
|
727
|
+
}
|
|
608
728
|
|
|
609
729
|
for (const subject of subjects) {
|
|
610
730
|
const rare = distinctive(subject.analysis)
|
|
611
731
|
const surfaces: Surface[] = []
|
|
612
732
|
|
|
613
733
|
for (const candidate of rareCandidates) {
|
|
734
|
+
// A second surface is a second file. Two bullets sharing anchors inside
|
|
735
|
+
// one rule are adjacent instructions on one topic rather than the same
|
|
736
|
+
// rule shipped twice, and only a rule can reach this branch at all,
|
|
737
|
+
// since it is the one kind read as both a subject and a candidate.
|
|
738
|
+
if (candidate.statement.file === subject.statement.file) continue
|
|
739
|
+
|
|
740
|
+
const key = pairKey(subject.statement, candidate.statement)
|
|
741
|
+
if (reportedPairs.has(key)) continue
|
|
742
|
+
|
|
614
743
|
const shared = [...candidate.rare]
|
|
615
744
|
.filter((token) => rare.has(token))
|
|
616
745
|
.sort()
|
|
@@ -626,6 +755,7 @@ export function readRestated(root: string): RestatedReport {
|
|
|
626
755
|
)
|
|
627
756
|
|
|
628
757
|
if (weight < ANCHOR_FLOOR) continue
|
|
758
|
+
reportedPairs.add(key)
|
|
629
759
|
|
|
630
760
|
const { restatement, reason } = classify(
|
|
631
761
|
subject.statement,
|
|
@@ -659,22 +789,18 @@ export function readRestated(root: string): RestatedReport {
|
|
|
659
789
|
else repetitions += 1
|
|
660
790
|
}
|
|
661
791
|
|
|
662
|
-
// Every surface counts here, a declared mirror included. The motivating
|
|
663
|
-
// case was the always-loaded file, the seed, and a body, so dropping the
|
|
664
|
-
// mirror would read that exact shape as a rule stated twice. The mirror
|
|
665
|
-
// exclusion is a rule about which class is a finding, not about how far an
|
|
666
|
-
// instruction reached.
|
|
667
|
-
if (surfaces.length >= 2) threeSurface += 1
|
|
668
|
-
|
|
669
792
|
restatements.push({ subject: subject.statement, surfaces })
|
|
670
793
|
}
|
|
671
794
|
|
|
795
|
+
const threeSurface = countThreeSurfaceClusters(reportedPairs)
|
|
796
|
+
|
|
672
797
|
return {
|
|
673
798
|
kind: 'measured',
|
|
674
799
|
corpus: {
|
|
675
800
|
instructions: instructions.length,
|
|
676
801
|
seed: seed.length,
|
|
677
802
|
bodies: bodyFiles,
|
|
803
|
+
rules: ruleFiles,
|
|
678
804
|
candidates: candidates.length,
|
|
679
805
|
},
|
|
680
806
|
matcher: {
|
package/standards/plan.md
CHANGED
|
@@ -123,7 +123,7 @@ This contract inverts the one an intake folder keeps, where an empty slot means
|
|
|
123
123
|
- Write the plan before implementation starts, and treat it as the scope of the run that executes it.
|
|
124
124
|
- Keep every plan at one root. A plan copied into each parallel working tree forks, and the copies answer the same question differently.
|
|
125
125
|
- Amend the plan in place when a decision changes mid-flight. Do not append a second passage narrating the change, which leaves a reader to work out which of two answers is current. An execution-time deviation from a suggestion is one such amendment, and the contract above fixes which line takes it.
|
|
126
|
-
- Move the plan to `.claude/plans/archive/` when the work it describes ships. Never delete it, because the plan is where the rejected alternative is written down and nothing else records it.
|
|
126
|
+
- Move the plan to `.claude/plans/archive/` when the work it describes ships. Never delete it, because the plan is where the rejected alternative is written down and nothing else records it. The archive nests inside `.claude/plans/` rather than sitting beside it as `.claude/plans-archive/`, so an older layout is a flat sibling by that name and a current one is not.
|
|
127
127
|
- Write the plan in the same session that opens the task it serves. The session executing it later inherits reasoning it would otherwise re-derive.
|
|
128
128
|
|
|
129
129
|
## Anti-patterns
|
package/standards/tasks.md
CHANGED
|
@@ -217,7 +217,7 @@ Phase-label format and where labels may appear are governed by `standards/versio
|
|
|
217
217
|
|
|
218
218
|
`Plan:` points at `../plans/feature-<slug>.md` while the task is open. Once the task ships and the plan is archived, it points at `../plans/archive/feature-<slug>.md`, and at `../../plans/archive/feature-<slug>.md` once the task itself is archived a folder deeper. Retarget both halves of the link rather than dropping it, so a completed task still leads to the reasoning behind it.
|
|
219
219
|
|
|
220
|
-
A project that archived plans before the folder nested under `.claude/plans/` holds closed tasks pointing at `../plans-archive/`, or at `../.tmp/plans-archive/` from before the durable records left the scratch tree. Each form resolves against the files it names, so leave those pointers where they are.
|
|
220
|
+
A project that archived plans before the folder nested under `.claude/plans/` holds closed tasks pointing at `../plans-archive/`, or at `../.tmp/plans-archive/` from before the durable records left the scratch tree. Each form resolves against the files it names, so leave those pointers where they are. A named route now moves the folder and retargets its pointers together, but no automation runs it, so an unmigrated project keeps holding the old spelling until someone does, and a task retargeted without its plan moving leads nowhere.
|
|
221
221
|
|
|
222
222
|
One plan per task. A plan cited by two tasks is a misfile rather than a shape to design for, which is why the sweep counts citations before archiving: the count is a guard against the misfile stranding a pointer, not support for the shape.
|
|
223
223
|
|
|
@@ -249,6 +249,8 @@ The line is what lets a merge close its own task. Every merge on `main` is a squ
|
|
|
249
249
|
|
|
250
250
|
Never delete a task file. A shipped task moves to `.claude/tasks/archive/` under its own name, and the live index regenerates without it. `aitk tasks archive` owns the move, the ordering-row removal, and the index regen as one unit.
|
|
251
251
|
|
|
252
|
+
The archive nests inside `.claude/tasks/` rather than sitting beside it as a flat `.claude/task-archive/`. Nesting is what lets a reader tell the two shapes apart on sight: the flat sibling is what a binary predating this convention still writes, so meeting one names an older checkout rather than a second archive to reconcile against this one.
|
|
253
|
+
|
|
252
254
|
Two callers reach that command. The `claude-tasks` skill runs it inside a session, and the `post-merge` hook runs it unattended after a pull that merged the work. Both go through the command rather than moving the file themselves, so the two paths cannot drift into archiving differently. Every gate the command applies refuses with a non-zero exit rather than reporting, because a caller with nobody watching cannot act on a warning.
|
|
253
255
|
|
|
254
256
|
One destination rather than a per-project choice is what lets the move happen without asking. It mirrors the plans archive at `.claude/plans/archive/`, sitting inside the folder it archives the same way, and it inherits the board's own ignore entry rather than needing one of its own. The cost is that an archived task does not appear in diffs, which is the cost the live board already carries.
|