@erclx/aitk 0.24.0 → 0.26.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-address-review/SKILL.md +3 -3
- package/claude/skills/claude-autoship/SKILL.md +1 -1
- package/claude/skills/claude-diagram/SKILL.md +1 -1
- package/claude/skills/claude-docs/SKILL.md +1 -1
- package/claude/skills/claude-memory-review/SKILL.md +1 -1
- package/claude/skills/claude-pr-review/SKILL.md +1 -1
- package/claude/skills/claude-review/SKILL.md +1 -1
- package/claude/skills/claude-seed-sync/SKILL.md +1 -1
- package/claude/skills/claude-standards-audit/SKILL.md +27 -11
- package/claude/skills/claude-ui-test/SKILL.md +1 -1
- package/claude/skills/claude-ux-audit/SKILL.md +1 -1
- package/claude/skills/claude-worktree/SKILL.md +1 -1
- package/claude/skills/git-branch/references/branch.md +1 -1
- package/claude/skills/git-followup/SKILL.md +1 -1
- package/claude/skills/git-issue/SKILL.md +1 -1
- package/claude/skills/git-pr/SKILL.md +1 -1
- package/claude/skills/git-pr/references/branch.md +1 -1
- package/claude/skills/git-split/SKILL.md +1 -1
- package/claude/skills/git-split/references/branch.md +1 -1
- package/claude/skills/setup-init/SKILL.md +1 -1
- package/docs/agents.md +20 -4
- package/docs/target-projects.md +5 -3
- package/package.json +1 -1
- package/scripts/standards/list.sh +24 -2
- package/src/commands/sync.ts +5 -1
- package/src/sync/check.ts +6 -0
- package/src/sync/engine.ts +73 -3
- package/src/sync/history.ts +123 -0
- package/standards/bundled/branch.md +1 -1
- package/standards/index.md +2 -0
- package/standards/prose.md +2 -28
- package/standards/publish.md +40 -0
- package/standards/skill.md +2 -13
- package/standards/slug.md +29 -0
- package/standards/standard.md +3 -1
|
@@ -73,9 +73,9 @@ bullet per finding, each opening with the bolded finding identifier.
|
|
|
73
73
|
Close the body with `🤖 Addressed by Claude Code` on its own line so the reply
|
|
74
74
|
reads as an independent machine pass, not a human sign-off.
|
|
75
75
|
|
|
76
|
-
Before posting, run the
|
|
77
|
-
against the reply, or `${CLAUDE_SKILL_DIR}/../../standards/
|
|
78
|
-
project does not have it. The hook skips `.claude/.tmp/`, so this scan is the
|
|
76
|
+
Before posting, run the scan in `.claude/standards/publish.md`
|
|
77
|
+
against the reply, or `${CLAUDE_SKILL_DIR}/../../standards/publish.md` when
|
|
78
|
+
the project does not have it. The hook skips `.claude/.tmp/`, so this scan is the
|
|
79
79
|
only gate on the published reply. Post it to the PR:
|
|
80
80
|
|
|
81
81
|
```bash
|
|
@@ -11,7 +11,7 @@ Chain the post-plan pipeline in a single run. Every step has a stop condition. S
|
|
|
11
11
|
## Guards
|
|
12
12
|
|
|
13
13
|
- All `.claude/plans/` and `.claude/review/` reads resolve at the main worktree root, not the current worktree. See Worktrees in `CLAUDE.md`.
|
|
14
|
-
- Derive `<slug>` per
|
|
14
|
+
- Derive `<slug>` per `.claude/standards/slug.md`, or `${CLAUDE_SKILL_DIR}/../../standards/slug.md` when the project does not have it. This skill takes the stop rather than the `latest` fallback, since it commits and opens a pull request. If empty, stop: `❌ Detached HEAD. Checkout the feature branch first.`
|
|
15
15
|
- If `.claude/plans/feature-<slug>.md` does not exist at the main worktree root, stop: `❌ No approved plan at .claude/plans/feature-<slug>.md. Run /claude-feature first.`
|
|
16
16
|
- If the working tree has uncommitted changes unrelated to the plan, stop: `❌ Uncommitted changes outside the plan. Commit or stash before autoshipping.`
|
|
17
17
|
|
|
@@ -80,7 +80,7 @@ The code paths the explanation cites are what the `claude-docs` sweep watches fo
|
|
|
80
80
|
|
|
81
81
|
Quote node labels containing spaces or special characters with double quotes (`A["Web shell"]`). Avoid parentheses inside labels, they break some renderers. Use `<br/>` for line breaks inside labels.
|
|
82
82
|
|
|
83
|
-
Run the
|
|
83
|
+
Run the scan in `.claude/standards/publish.md` against the whole file, including inside mermaid `subgraph` labels and node text, or `${CLAUDE_SKILL_DIR}/../../standards/publish.md` when the project does not have it. The file lands in the repository rather than on a remote, so the scan's destination scoping leaves the character checks and not the phase-label one. A colon also works as a replacement inside a label. The pedagogical voice the diagram standard asks for is a yield on voice alone and buys no exemption from these bans.
|
|
84
84
|
|
|
85
85
|
Nothing enforces the ban inside the diagram. The standards-audit hook toggles on a fence and skips every line within it, so a label carrying an em dash passes silently while the same character in the explanation paragraph below is caught. Check the labels by reading them. This is the one place in the file where the author is the only gate.
|
|
86
86
|
|
|
@@ -237,7 +237,7 @@ A plan can serve more than one task, and archiving on the first task to close st
|
|
|
237
237
|
|
|
238
238
|
Write the retarget as a markdown link, `Plan: [feature-<slug>](../.tmp/plans-archive/feature-<slug>.md)`, updating both halves so the text and the target stay in step. This branch is the only writer that produces a `Plan:` line nobody authored by hand, so a retarget that emits a bare path converts every task to the old form as it closes and drifts the board back to two shapes on its own.
|
|
239
239
|
|
|
240
|
-
**Reviews.** Derive `<slug>` per
|
|
240
|
+
**Reviews.** Derive `<slug>` per `.claude/standards/slug.md`, or `${CLAUDE_SKILL_DIR}/../../standards/slug.md` when the project does not have it. Fall back to `latest` on an empty result. If `.claude/review/review-<slug>.md` exists, delete it. `claude-review` writes with this convention. Do not sweep any other `review-*.md` file.
|
|
241
241
|
|
|
242
242
|
Do not sweep `ui-checklist-*.md` (pending human verification) or `ux-audit-*.md` (standalone deliverable).
|
|
243
243
|
|
|
@@ -78,7 +78,7 @@ Rules that resist crisp one-line phrasing default to **Delete** over promote. Ne
|
|
|
78
78
|
|
|
79
79
|
### Step 4: write the proposal to the review file
|
|
80
80
|
|
|
81
|
-
Derive `<slug>` per
|
|
81
|
+
Derive `<slug>` per `.claude/standards/slug.md`, or `${CLAUDE_SKILL_DIR}/../../standards/slug.md` when the project does not have it. Fall back to `latest` on an empty result.
|
|
82
82
|
|
|
83
83
|
Write the full proposal to `.claude/review/memory-review-<slug>.md` at the main worktree root. Do not print it inline.
|
|
84
84
|
|
|
@@ -131,7 +131,7 @@ The `What is right` section is optional, capped at three bullets, and included o
|
|
|
131
131
|
|
|
132
132
|
Close the body with `🤖 Reviewed by Claude Code` on its own line so the review reads as an independent machine pass, not a human sign-off.
|
|
133
133
|
|
|
134
|
-
Before posting, run the
|
|
134
|
+
Before posting, run the scan in `.claude/standards/publish.md` against the body, or `${CLAUDE_SKILL_DIR}/../../standards/publish.md` when the project does not have it. The hook skips `.claude/.tmp/`, so this scan is the only gate on the published comment. A finding phrased against an internal phase label is what the label half of the scan catches here.
|
|
135
135
|
|
|
136
136
|
```bash
|
|
137
137
|
gh pr review <number> --comment --body-file .claude/.tmp/pr-review/body-<number>-<short-sha>.md
|
|
@@ -118,7 +118,7 @@ If nothing is wrong, use: `✅ No findings.`
|
|
|
118
118
|
|
|
119
119
|
### Persist
|
|
120
120
|
|
|
121
|
-
Derive `<slug>` per
|
|
121
|
+
Derive `<slug>` per `.claude/standards/slug.md`, or `${CLAUDE_SKILL_DIR}/../../standards/slug.md` when the project does not have it. Fall back to `latest` on an empty result.
|
|
122
122
|
|
|
123
123
|
Write the full report directly to `.claude/review/review-<slug>.md` at the main worktree root, not the current worktree. See Worktrees in `CLAUDE.md`. Create the directory if it does not exist. Always overwrite.
|
|
124
124
|
|
|
@@ -52,7 +52,7 @@ Before proposing an Update, grep the target section for the seed text's keywords
|
|
|
52
52
|
|
|
53
53
|
## Step 4: write the proposal to the review file
|
|
54
54
|
|
|
55
|
-
Derive `<slug>` per
|
|
55
|
+
Derive `<slug>` per `.claude/standards/slug.md`, or `${CLAUDE_SKILL_DIR}/../../standards/slug.md` when the project does not have it. Fall back to `latest` on an empty result.
|
|
56
56
|
|
|
57
57
|
Write the full proposal to `.claude/review/seed-audit-<slug>.md` at the main worktree root. Do not print the proposal inline.
|
|
58
58
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: claude-standards-audit
|
|
3
|
-
description: Audits changed markdown files against
|
|
3
|
+
description: Audits changed markdown files against every authoring standard that declares jurisdiction over their paths and reports violations without fixing. Reads the standards catalog to map each file, greps for banned tokens, and groups findings by file. Use when asked to "audit prose", "audit standards", "check standards", "standards audit", or after editing markdown where standards compliance matters. Do NOT fix violations. Reporting only.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# Claude standards audit
|
|
@@ -35,17 +35,33 @@ Get the changed file list, substituting `git diff HEAD --name-only` when the bas
|
|
|
35
35
|
git diff <base> HEAD --name-only
|
|
36
36
|
```
|
|
37
37
|
|
|
38
|
-
Filter to markdown (`.md`)
|
|
38
|
+
Filter to markdown (`.md`), then drop what the project does not hand-author:
|
|
39
|
+
|
|
40
|
+
- `index.md`, unless its frontmatter carries `auto: false`
|
|
41
|
+
- Any file in a gitignored directory
|
|
42
|
+
- Any file sitting in a skill's bundled reference folder whose frontmatter names the consumers a generator copied it out to
|
|
43
|
+
|
|
44
|
+
The last rule keeps a fan-out from multiplying one edit into a finding per copy. The source is the file to audit and the only one an author can fix, because the next regen overwrites every copy.
|
|
45
|
+
|
|
46
|
+
It takes both halves. A generator copies frontmatter verbatim, so the field alone matches the source as well and drops the one file the rule means to keep. The location alone matches a reference the skill author wrote by hand, which carries no such field and is governed like the rest of the folder.
|
|
39
47
|
|
|
40
48
|
## Step 2: map files to standards
|
|
41
49
|
|
|
42
|
-
|
|
50
|
+
Read the mapping rather than holding it here. Every standard declares the paths it governs, so a standard added later joins this audit with no edit to this body:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
aitk standards list --json
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Each entry carries `appliesTo`, the paths its `## Scope` statement declares. Match every changed file against every entry:
|
|
57
|
+
|
|
58
|
+
- `*` matches every changed markdown file
|
|
59
|
+
- An entry ending in `/` matches when the file path contains it at a path-segment boundary
|
|
60
|
+
- Any other entry matches when the file path ends with it at a path-segment boundary
|
|
43
61
|
|
|
44
|
-
|
|
45
|
-
- `SKILL.md` under `.claude/skills/` or `claude/skills/`: also `.claude/standards/skill.md`
|
|
46
|
-
- `README.md` at any level: also `.claude/standards/readme.md`
|
|
62
|
+
Add any standard in `.claude/standards/` the catalog did not list, and derive every declaration the same way when `aitk` is unavailable: the backticked paths in the first sentence under `## Scope`, or `*` when that sentence says the standard governs an attribute. Skip `index.md`, which is generated from the others and declares nothing. Read a standard from `${CLAUDE_SKILL_DIR}/../../standards/` when the project does not have it.
|
|
47
63
|
|
|
48
|
-
|
|
64
|
+
An entry whose `appliesTo` is empty declared nothing this can read. Report it as a finding against that standard's own file in Step 4 and audit the rest. A standard dropped in silence is the same miss this mapping exists to remove, one level up.
|
|
49
65
|
|
|
50
66
|
Every mapping names a changed markdown file, which is the only thing Step 1 produces. Text that never lands in the tree, such as a branch name or a pull request body, is checked by the skill that publishes it rather than here.
|
|
51
67
|
|
|
@@ -60,14 +76,14 @@ Every changed markdown file gets the prose pattern pass, since `.claude/standard
|
|
|
60
76
|
|
|
61
77
|
## Step 4: report
|
|
62
78
|
|
|
63
|
-
Group findings by file with line references. Use this shape:
|
|
79
|
+
Group findings by file with line references, naming the standard each one comes from. One file is one fix pass, and the standard is what the reader opens to settle a finding they disagree with. Use this shape:
|
|
64
80
|
|
|
65
81
|
```markdown
|
|
66
82
|
path/to/file.md
|
|
67
83
|
|
|
68
|
-
- L12: em dash in prose
|
|
69
|
-
- L34: semicolon used to join clauses
|
|
70
|
-
- L67:
|
|
84
|
+
- L12: `prose.md`, em dash in prose
|
|
85
|
+
- L34: `prose.md`, semicolon used to join clauses
|
|
86
|
+
- L67: `context.md`, decision entry names no rejected alternative
|
|
71
87
|
```
|
|
72
88
|
|
|
73
89
|
If clean, respond with `✅ No violations.`
|
|
@@ -57,7 +57,7 @@ If all changes are automatable, skip the manual checklist:
|
|
|
57
57
|
|
|
58
58
|
### Persist the checklist
|
|
59
59
|
|
|
60
|
-
Derive `<slug>` per
|
|
60
|
+
Derive `<slug>` per `.claude/standards/slug.md`, or `${CLAUDE_SKILL_DIR}/../../standards/slug.md` when the project does not have it. Fall back to `latest` on an empty result.
|
|
61
61
|
|
|
62
62
|
When a manual checklist is produced, write it directly to `.claude/review/ui-checklist-<slug>.md` at the main worktree root, not the current worktree. See Worktrees in `CLAUDE.md`. Create the directory if it does not exist. Always overwrite.
|
|
63
63
|
|
|
@@ -58,7 +58,7 @@ If nothing is wrong, use: `✅ No observations.`
|
|
|
58
58
|
|
|
59
59
|
### Persist
|
|
60
60
|
|
|
61
|
-
Derive `<slug>` per
|
|
61
|
+
Derive `<slug>` per `.claude/standards/slug.md`, or `${CLAUDE_SKILL_DIR}/../../standards/slug.md` when the project does not have it. Fall back to `latest` on an empty result.
|
|
62
62
|
|
|
63
63
|
Write the full report directly to `.claude/review/ux-audit-<slug>.md` at the main worktree root, not the current worktree. See Worktrees in `CLAUDE.md`. Create the directory if it does not exist. Always overwrite.
|
|
64
64
|
|
|
@@ -34,7 +34,7 @@ The directory test separates the defect from a genuinely bare repository, which
|
|
|
34
34
|
|
|
35
35
|
Try each source in order. Stop at the first match.
|
|
36
36
|
|
|
37
|
-
1. **Plan matched to current branch.** Derive `<slug>` per
|
|
37
|
+
1. **Plan matched to current branch.** Derive `<slug>` per `.claude/standards/slug.md`, or `${CLAUDE_SKILL_DIR}/../../standards/slug.md` when the project does not have it. An empty result falls through to the next source here rather than becoming `latest`, since the slug is one candidate among several. If `<main-root>/.claude/plans/feature-<slug>.md` exists, use `<slug>`.
|
|
38
38
|
2. **Single plan file.** List `<main-root>/.claude/plans/feature-*.md`. If exactly one match, derive `<slug>` from the filename.
|
|
39
39
|
3. **Multiple plan files, no branch match.** Ask the user which plan. Show the candidate slugs as a numbered list. Do not pick.
|
|
40
40
|
4. **Current branch.** When no plan exists, use the `<slug>` from step 1 if it is not `main` or `master`.
|
|
@@ -15,7 +15,7 @@ Does not govern:
|
|
|
15
15
|
- Commit subject format, which shares the type vocabulary: `commit.md`
|
|
16
16
|
- Pull request title and body: `pr.md`
|
|
17
17
|
- Whether a phase label may appear in a branch name: `versioning.md`
|
|
18
|
-
- Deriving
|
|
18
|
+
- Deriving a slug from a branch name for use in an output filename: `slug.md`
|
|
19
19
|
|
|
20
20
|
## Format
|
|
21
21
|
|
|
@@ -24,7 +24,7 @@ its own reply, so skip the comment in step 6. The push and body sync still run.
|
|
|
24
24
|
3. Invoke `aitk:git-commit` to generate one conventional commit from the staged diff
|
|
25
25
|
4. Run `git push` to the tracking branch
|
|
26
26
|
5. Check for existing review comments: `gh api 'repos/{owner}/{repo}/pulls/<number>/comments' --jq 'length'`, resolving `<number>` from `gh pr view --json number`.
|
|
27
|
-
6. When invoked with `reply-owned`, skip this step's comment: the caller posts the reply. Otherwise, if the count is above zero, the followup addresses review feedback: post a one-line summary of the fix with `gh pr comment --body`, first running the
|
|
27
|
+
6. When invoked with `reply-owned`, skip this step's comment: the caller posts the reply. Otherwise, if the count is above zero, the followup addresses review feedback: post a one-line summary of the fix with `gh pr comment --body`, first running the scan in `.claude/standards/publish.md` against it, or `${CLAUDE_SKILL_DIR}/../../standards/publish.md` when the project does not have it, since the hook does not see an inline comment body. If it is zero, run `gh pr view --json url,title,body` and update the body with `gh pr edit --body` when the new commit changes scope, and the title with `gh pr edit --title` when the scope shifted enough to make it inaccurate.
|
|
28
28
|
|
|
29
29
|
## After completion
|
|
30
30
|
|
|
@@ -40,7 +40,7 @@ After outputting the preview, execute the final command immediately. Claude Code
|
|
|
40
40
|
|
|
41
41
|
### Pre-publish scan
|
|
42
42
|
|
|
43
|
-
Before running the final command, run the
|
|
43
|
+
Before running the final command, run the scan in `.claude/standards/publish.md` against the title and body, or `${CLAUDE_SKILL_DIR}/../../standards/publish.md` when the project does not have it. The title and body go straight to the remote with nothing checking them on the way, so this scan is the only gate. It covers the phase-label check as well as the characters, since both go to a reader who has no task board. It applies on top of the banned phrases in `${CLAUDE_SKILL_DIR}/references/issue.md`.
|
|
44
44
|
|
|
45
45
|
### Final command
|
|
46
46
|
|
|
@@ -69,7 +69,7 @@ Leave a box unchecked only for the human-only cases the reference defines, and n
|
|
|
69
69
|
|
|
70
70
|
### Pre-publish scan
|
|
71
71
|
|
|
72
|
-
Before running the final command, run the
|
|
72
|
+
Before running the final command, run the scan in `.claude/standards/publish.md` against the PR title and body, or `${CLAUDE_SKILL_DIR}/../../standards/publish.md` when the project does not have it. The title and body go straight to the remote with nothing checking them on the way, so this scan is the only gate. It covers the phase-label check as well as the characters, since both go to a reader who has no task board. It applies on top of the banned phrases in `${CLAUDE_SKILL_DIR}/references/pr.md`.
|
|
73
73
|
|
|
74
74
|
### Final command
|
|
75
75
|
|
|
@@ -15,7 +15,7 @@ Does not govern:
|
|
|
15
15
|
- Commit subject format, which shares the type vocabulary: `commit.md`
|
|
16
16
|
- Pull request title and body: `pr.md`
|
|
17
17
|
- Whether a phase label may appear in a branch name: `versioning.md`
|
|
18
|
-
- Deriving
|
|
18
|
+
- Deriving a slug from a branch name for use in an output filename: `slug.md`
|
|
19
19
|
|
|
20
20
|
## Format
|
|
21
21
|
|
|
@@ -69,7 +69,7 @@ If Mode is Independent, append this line to the preview:
|
|
|
69
69
|
|
|
70
70
|
After outputting the preview, execute the final commands immediately. Claude Code's tool permission dialog is the confirmation gate. Do not wait for user input.
|
|
71
71
|
|
|
72
|
-
Before running them, run the
|
|
72
|
+
Before running them, run the scan in `.claude/standards/publish.md` against every title and body, or `${CLAUDE_SKILL_DIR}/../../standards/publish.md` when the project does not have it. Each body stages under `.claude/.tmp/` and leaves through `gh`, so the hook sees neither and this scan is the only gate.
|
|
73
73
|
|
|
74
74
|
### Final commands
|
|
75
75
|
|
|
@@ -15,7 +15,7 @@ Does not govern:
|
|
|
15
15
|
- Commit subject format, which shares the type vocabulary: `commit.md`
|
|
16
16
|
- Pull request title and body: `pr.md`
|
|
17
17
|
- Whether a phase label may appear in a branch name: `versioning.md`
|
|
18
|
-
- Deriving
|
|
18
|
+
- Deriving a slug from a branch name for use in an output filename: `slug.md`
|
|
19
19
|
|
|
20
20
|
## Format
|
|
21
21
|
|
|
@@ -63,7 +63,7 @@ Before executing, output:
|
|
|
63
63
|
|
|
64
64
|
## Execute
|
|
65
65
|
|
|
66
|
-
Run the chain in order. Each step's permission dialog is the confirmation gate. Run from the target project's current directory.
|
|
66
|
+
Run the chain in order, starting immediately after the preview. Each step's permission dialog is the confirmation gate. Do not pause for additional confirmation. Run from the target project's current directory.
|
|
67
67
|
|
|
68
68
|
Step 1: `aitk init` installs base tooling, claude seeds, governance rules, standards, snippets, and wiki.
|
|
69
69
|
|
package/docs/agents.md
CHANGED
|
@@ -163,8 +163,16 @@ matches what the toolkit installed, `customized` when the project edited it,
|
|
|
163
163
|
when the project authored it, or `drifted` when no stamp covers it. Use `--json`
|
|
164
164
|
for the machine-readable report and `--exit-code` to fail a CI job. Orphaned
|
|
165
165
|
files are excluded from that exit code, since a project-authored rule never
|
|
166
|
-
converges. Attribution
|
|
167
|
-
writes.
|
|
166
|
+
converges. Attribution reads `.claude/aitk.json`, which every install and sync
|
|
167
|
+
writes.
|
|
168
|
+
|
|
169
|
+
A target installed before stamping shipped has no such file, and the report
|
|
170
|
+
falls back to the toolkit's own git history. Installed content matching any
|
|
171
|
+
version that history ever published proves the file is untouched, so it reports
|
|
172
|
+
`stale` naming the commit it came from, and content matching no published
|
|
173
|
+
version stays `drifted`. A toolkit reached outside a git clone, which is what a
|
|
174
|
+
registry install is, cannot run that fallback and reports
|
|
175
|
+
`historyUnavailable` alongside the unattributed files.
|
|
168
176
|
|
|
169
177
|
Each domain carries its own toolkit anchor in that file, so syncing one domain
|
|
170
178
|
never advances the revision another measures from, and each reports the upstream
|
|
@@ -238,7 +246,7 @@ The record carries every scenario with the arms that declare, plus `totalScenari
|
|
|
238
246
|
|
|
239
247
|
`--skills` answers what the scenario count cannot, which is whether anything can fail a given skill. It adds `skills`, `totalSkills`, `asserted`, `shouldBeAsserted`, `exempt`, `staleExemptions`, and `supersededExemptions` to the record, and keeps the scenario view rather than replacing it. The two denominators disagree on purpose: an armed scenario under `infra/` or `tooling/` exercises a CLI domain and pairs with no skill at all.
|
|
240
248
|
|
|
241
|
-
A skill pairs to a scenario by filename, `<category>-<command>` first and bare `<command>` second, so `claude/setup-init.sh` reaches the `setup-init` skill. `should-be-asserted` is the default and is the
|
|
249
|
+
A skill pairs to a scenario by filename, `<category>-<command>` first and bare `<command>` second, so `claude/setup-init.sh` reaches the `setup-init` skill. `should-be-asserted` is the default rather than a queue to drain, and which of those skills earns an arm is a project decision the census does not make. `exempt` means no arm should be written and holds only with a reason, declared in `scripts/sandbox/exempt.toml` and limited to a harness limit the checker cannot reach past or a skill that writes no artifact. An armed arm outranks an exemption. An exemption naming no shipped skill, or naming one an arm now asserts, exits 1 without `--strict`. Each armed arm reports as `<category>:<command>/<arm>`, so two same-named arms under different scenarios stay distinct.
|
|
242
250
|
|
|
243
251
|
`scripts/sandbox/run.sh` calls this after a headless run and merges the verdict into the envelope it prints. It also writes that merged record to `.claude/.tmp/sandbox-runs/<target>-<arm>-<timestamp>.json` with a `writes` array appended, and logs the path on stderr. Both fields are what a later re-score needs, since `--envelope` and `--writes` read files the run deletes on exit. Stdout carries the same bytes it did before the record existed.
|
|
244
252
|
|
|
@@ -403,7 +411,7 @@ Use these to discover what's available instead of hardcoding names.
|
|
|
403
411
|
| ------------------------------- | -------------------------------------------- |
|
|
404
412
|
| `aitk tooling list --json` | Stacks, extends chain, dep and script counts |
|
|
405
413
|
| `aitk snippets list --json` | Presets and categories with their slugs |
|
|
406
|
-
| `aitk standards list --json` | Standards docs
|
|
414
|
+
| `aitk standards list --json` | Standards docs and the paths each governs |
|
|
407
415
|
| `aitk gov list --json` | Governance stacks and rule sets |
|
|
408
416
|
| `aitk claude seeds list --json` | Seed doc sources with content |
|
|
409
417
|
| `aitk docs list --json` | Consumer docs plus per-domain context |
|
|
@@ -412,6 +420,14 @@ Every catalog serializes through `JSON.stringify`, so a name carrying a quote
|
|
|
412
420
|
emits valid JSON. `aitk tooling list` and `aitk snippets list` previously built
|
|
413
421
|
their output with `printf` and no escaping.
|
|
414
422
|
|
|
423
|
+
`aitk standards list` carries `appliesTo` per standard, the paths that standard's
|
|
424
|
+
`## Scope` statement declares. It holds the backticked paths from the first
|
|
425
|
+
sentence of the statement, the single entry `*` for a standard governing an
|
|
426
|
+
attribute rather than a document type, and an empty array when the statement
|
|
427
|
+
declares nothing a parser can read. A consumer mapping a file to its governing
|
|
428
|
+
standards reads this rather than holding a table of its own, and reports an empty
|
|
429
|
+
array rather than skipping the standard behind it.
|
|
430
|
+
|
|
415
431
|
`aitk claude seeds list` reads the same plan `aitk claude init` applies, so the
|
|
416
432
|
listing and the install cannot disagree. It now reports
|
|
417
433
|
`.claude/context/index.md`, which `init` has always installed and the listing
|
package/docs/target-projects.md
CHANGED
|
@@ -77,7 +77,7 @@ Run `aitk tooling list --json` and `aitk gov list --json` to see the current cat
|
|
|
77
77
|
`governance`, `standards`, and `wiki` are skippable:
|
|
78
78
|
|
|
79
79
|
- `--skip governance`: leave `.claude/rules/` empty. Standards still install, so `.claude/standards/prose.md` lands with nothing pointing at it and no coding standard loads on a file match. The preview names any `--add` rules the skip drops, and the run prints the `aitk gov install <stack> <path>` command to add rules afterward, carrying those extras so one paste restores what the skip declined.
|
|
80
|
-
- `--skip standards`: leave standards out. The governance rules still reference `.claude/standards/`, so their authority lines resolve to nothing. Toolkit skills are unaffected, since each falls back to the copy in its own plugin root. That fallback now carries runtime behavior rather than reference prose alone, because the pre-publish scan and the branch-slug transform
|
|
80
|
+
- `--skip standards`: leave standards out. The governance rules still reference `.claude/standards/`, so their authority lines resolve to nothing. Toolkit skills are unaffected, since each falls back to the copy in its own plugin root. That fallback now carries runtime behavior rather than reference prose alone, because the pre-publish scan and the branch-slug transform each have a standard of their own, `publish.md` and `slug.md`, cited by the skills that run them.
|
|
81
81
|
- `--skip wiki`: skip the `.claude/wiki/` scaffold. A target that already carries a root `wiki/` keeps it, since the verb reports that folder rather than migrating it.
|
|
82
82
|
|
|
83
83
|
## Add a domain later
|
|
@@ -100,7 +100,9 @@ When the toolkit updates, target projects pull changes per domain. There is one
|
|
|
100
100
|
|
|
101
101
|
`aitk sync --check <path>` reports what has drifted without writing anything. It splits each difference by cause, which is the question that decides what to do next. A `stale` file still matches what the toolkit installed, so the update is mechanical. A `customized` file carries local edits, so taking the upstream version is a decision and `aitk:claude-seed-sync` is the tool for it. A `stranded` file sits where an older toolkit installed it and the toolkit has since moved, which is what `aitk:migration-standards` handles.
|
|
102
102
|
|
|
103
|
-
That attribution comes from `.claude/aitk.json`, a stamp every install and sync writes recording a hash per installed file. Each domain holds its own toolkit commit, so syncing governance today does not move the revision standards measures against, and each domain reports the upstream commits touching its own source path.
|
|
103
|
+
That attribution comes from `.claude/aitk.json`, a stamp every install and sync writes recording a hash per installed file. Each domain holds its own toolkit commit, so syncing governance today does not move the revision standards measures against, and each domain reports the upstream commits touching its own source path. Running any sync stamps that domain, and the report names the ones still unstamped.
|
|
104
|
+
|
|
105
|
+
A project that has never synced under a toolkit new enough to write a stamp falls back to the toolkit's own git history. Installed content matching any version that history published proves the file untouched, so it reports `stale` naming the commit it came from, and content matching no published version stays `drifted`. That fallback needs the toolkit as a git checkout. Installed from the registry it ships source without history, and the report says attribution was unavailable rather than reading every file as a local edit.
|
|
104
106
|
|
|
105
107
|
Add `--json` for the machine-readable report, and `--exit-code` to fail a CI job when a target falls behind. Files the project authored itself never count toward that exit code.
|
|
106
108
|
|
|
@@ -110,7 +112,7 @@ Tooling is not covered by the stamp. Reconcile those configs with `aitk tooling
|
|
|
110
112
|
|
|
111
113
|
`aitk sync <path>` runs every installed domain's sync in sequence. Safe to run on a cadence. It never touches user-owned seed files. Governance rules in `.claude/rules/`, tooling configs, and reference docs refresh in place. Stale `.claude/GOV.md` from earlier installs is removed.
|
|
112
114
|
|
|
113
|
-
Standards are the exception inside that run, and the stamp narrows it. A standard the project customized is reported and left alone rather than overwritten. A standard still matching what was installed carries no local edits to lose, so a headless run updates it.
|
|
115
|
+
Standards are the exception inside that run, and the stamp narrows it. A standard the project customized is reported and left alone rather than overwritten. A standard still matching what was installed carries no local edits to lose, so a headless run updates it. An unstamped project reaches the same split through the history fallback, so a headless run updates every standard it can prove untouched and refuses while any file resists attribution. To take the upstream version of a customized file, run `aitk standards sync <path>` interactively, or use `aitk:claude-seed-sync` below to merge section by section.
|
|
114
116
|
|
|
115
117
|
### Targeted
|
|
116
118
|
|
package/package.json
CHANGED
|
@@ -28,6 +28,26 @@ json_escape() {
|
|
|
28
28
|
printf '%s' "$s"
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
# Emits the paths a standard's `## Scope` statement declares, as a JSON array.
|
|
32
|
+
# The declaration is prose, so the contract is narrow on purpose: backticked
|
|
33
|
+
# paths in the first sentence of the statement, or `*` for an attribute standard,
|
|
34
|
+
# which names no path and says so in the same statement. An empty array means the
|
|
35
|
+
# statement did not parse, which consumers report rather than skip.
|
|
36
|
+
read_applies_to() {
|
|
37
|
+
local file="$1"
|
|
38
|
+
local scope first paths
|
|
39
|
+
|
|
40
|
+
scope=$(awk '/^## Scope[[:space:]]*$/ {found = 1; next} found && NF {print; exit}' "$file")
|
|
41
|
+
first="${scope%%. *}"
|
|
42
|
+
paths=$(printf '%s' "$first" | grep -o '`[^`]*`' | tr -d '`' || true)
|
|
43
|
+
|
|
44
|
+
if [ -z "$paths" ] && [[ "$scope" == *"attribute standard"* ]]; then
|
|
45
|
+
paths="*"
|
|
46
|
+
fi
|
|
47
|
+
|
|
48
|
+
printf '%s\n' "$paths" | jq -Rnc '[inputs | select(length > 0)]'
|
|
49
|
+
}
|
|
50
|
+
|
|
31
51
|
list_text() {
|
|
32
52
|
log_step "Standards"
|
|
33
53
|
local file name title
|
|
@@ -41,13 +61,14 @@ list_text() {
|
|
|
41
61
|
|
|
42
62
|
list_json() {
|
|
43
63
|
local first=1
|
|
44
|
-
local file name title target
|
|
64
|
+
local file name title target applies_to
|
|
45
65
|
printf '['
|
|
46
66
|
while IFS= read -r file; do
|
|
47
67
|
name=$(basename "$file" .md)
|
|
48
68
|
[ "$name" = "index" ] && continue
|
|
49
69
|
title=$(read_frontmatter_field "$file" "description")
|
|
50
70
|
target=".claude/standards/$(basename "$file")"
|
|
71
|
+
applies_to=$(read_applies_to "$file")
|
|
51
72
|
if [ "$first" -eq 0 ]; then
|
|
52
73
|
printf ','
|
|
53
74
|
fi
|
|
@@ -55,8 +76,9 @@ list_json() {
|
|
|
55
76
|
--arg name "$name" \
|
|
56
77
|
--arg description "$title" \
|
|
57
78
|
--arg target "$target" \
|
|
79
|
+
--argjson appliesTo "$applies_to" \
|
|
58
80
|
--rawfile content "$file" \
|
|
59
|
-
'{name: $name, description: $description, target: $target, content: $content}'
|
|
81
|
+
'{name: $name, description: $description, target: $target, appliesTo: $appliesTo, content: $content}'
|
|
60
82
|
first=0
|
|
61
83
|
done < <(find "$STANDARDS_DIR" -maxdepth 1 -type f -name "*.md" | sort)
|
|
62
84
|
printf ']'
|
package/src/commands/sync.ts
CHANGED
|
@@ -99,7 +99,11 @@ function renderCheck(report: CheckReport): void {
|
|
|
99
99
|
logStep(domain.domain)
|
|
100
100
|
|
|
101
101
|
if (domain.commit === undefined) {
|
|
102
|
-
logWarn(
|
|
102
|
+
logWarn(
|
|
103
|
+
domain.historyUnavailable
|
|
104
|
+
? 'Not stamped, and this toolkit has no git history. Drift below is reported unattributed.'
|
|
105
|
+
: 'Not stamped. Drift below is attributed from toolkit history.',
|
|
106
|
+
)
|
|
103
107
|
} else {
|
|
104
108
|
logInfo(`Synced from ${domain.commit} on ${domain.syncedAt}`)
|
|
105
109
|
}
|
package/src/sync/check.ts
CHANGED
|
@@ -54,6 +54,11 @@ export interface DomainReport {
|
|
|
54
54
|
readonly counts: StateCounts
|
|
55
55
|
readonly entries: readonly ScanEntry[]
|
|
56
56
|
readonly upstream: readonly UpstreamCommit[]
|
|
57
|
+
/**
|
|
58
|
+
* Separates a toolkit that could not attribute from one that attributed and
|
|
59
|
+
* found a local edit. Only the first is a capability the install lacks.
|
|
60
|
+
*/
|
|
61
|
+
readonly historyUnavailable: boolean
|
|
57
62
|
}
|
|
58
63
|
|
|
59
64
|
export interface UpstreamCommit {
|
|
@@ -145,6 +150,7 @@ async function buildDomainReport(
|
|
|
145
150
|
syncedAt: record?.syncedAt,
|
|
146
151
|
counts: countStates(plan.entries),
|
|
147
152
|
entries: plan.entries,
|
|
153
|
+
historyUnavailable: plan.historyUnavailable,
|
|
148
154
|
upstream:
|
|
149
155
|
since === undefined
|
|
150
156
|
? []
|
package/src/sync/engine.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { existsSync, readFileSync } from 'node:fs'
|
|
|
2
2
|
import { rm } from 'node:fs/promises'
|
|
3
3
|
import { isAbsolute, relative, resolve } from 'node:path'
|
|
4
4
|
import { copyPreservingMode } from '@/copy'
|
|
5
|
+
import { findInstalledOrigin, readHistoryIndex } from '@/sync/history'
|
|
5
6
|
import {
|
|
6
7
|
type DomainHashes,
|
|
7
8
|
hashFile,
|
|
@@ -74,12 +75,16 @@ export type EntryState =
|
|
|
74
75
|
export interface ScanEntry {
|
|
75
76
|
readonly state: EntryState
|
|
76
77
|
readonly rel: string
|
|
78
|
+
/** Toolkit revision this file's content came from, when history proved it. */
|
|
79
|
+
readonly since?: string
|
|
77
80
|
}
|
|
78
81
|
|
|
79
82
|
export interface SyncPlan {
|
|
80
83
|
readonly entries: readonly ScanEntry[]
|
|
81
84
|
readonly retired: readonly RetiredSurface[]
|
|
82
85
|
readonly changes: readonly SyncChange[]
|
|
86
|
+
/** Set when a file needed history to attribute it and this toolkit has none. */
|
|
87
|
+
readonly historyUnavailable: boolean
|
|
83
88
|
}
|
|
84
89
|
|
|
85
90
|
/**
|
|
@@ -156,6 +161,7 @@ export function listInstalled(root: string, target: string): InstalledFile[] {
|
|
|
156
161
|
export function planSync(adapter: SyncAdapter, target: string): SyncPlan {
|
|
157
162
|
const entries: ScanEntry[] = []
|
|
158
163
|
const changes: SyncChange[] = []
|
|
164
|
+
const unattributed: UnattributedFile[] = []
|
|
159
165
|
|
|
160
166
|
const hashes = stampedHashes(readStamp(target), adapter.stamp?.domain)
|
|
161
167
|
const walked = new Set<string>()
|
|
@@ -176,7 +182,12 @@ export function planSync(adapter: SyncAdapter, target: string): SyncPlan {
|
|
|
176
182
|
continue
|
|
177
183
|
}
|
|
178
184
|
|
|
179
|
-
|
|
185
|
+
const state = attribute(hashes, file)
|
|
186
|
+
if (state === 'drifted') {
|
|
187
|
+
unattributed.push({ index: entries.length, source, path: file.path })
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
entries.push({ state, rel: file.rel })
|
|
180
191
|
changes.push({
|
|
181
192
|
kind: 'copy',
|
|
182
193
|
source,
|
|
@@ -185,6 +196,8 @@ export function planSync(adapter: SyncAdapter, target: string): SyncPlan {
|
|
|
185
196
|
})
|
|
186
197
|
}
|
|
187
198
|
|
|
199
|
+
const historyUnavailable = recoverAttribution(adapter, entries, unattributed)
|
|
200
|
+
|
|
188
201
|
entries.push(...strandedByRelocation(target, hashes, walked))
|
|
189
202
|
|
|
190
203
|
const retired = adapter.collectRetired?.(target) ?? []
|
|
@@ -192,7 +205,53 @@ export function planSync(adapter: SyncAdapter, target: string): SyncPlan {
|
|
|
192
205
|
changes.push({ kind: 'delete', dest: surface.path, rel: surface.rel })
|
|
193
206
|
}
|
|
194
207
|
|
|
195
|
-
return { entries, retired, changes }
|
|
208
|
+
return { entries, retired, changes, historyUnavailable }
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
interface UnattributedFile {
|
|
212
|
+
readonly index: number
|
|
213
|
+
readonly source: string
|
|
214
|
+
readonly path: string
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Second pass over the files the stamp could not attribute, which is every file
|
|
219
|
+
* in a target installed before stamping shipped. Matching the installed content
|
|
220
|
+
* against the toolkit's own history recovers the fact a stamp would have held,
|
|
221
|
+
* and a file matching no published version stays unattributed.
|
|
222
|
+
*
|
|
223
|
+
* Runs as one git call for the whole domain rather than one per file, and only
|
|
224
|
+
* when the first pass left something to attribute. Reports whether history was
|
|
225
|
+
* readable at all, so a registry install can say why it fell short instead of
|
|
226
|
+
* reporting every file as a local edit.
|
|
227
|
+
*/
|
|
228
|
+
function recoverAttribution(
|
|
229
|
+
adapter: SyncAdapter,
|
|
230
|
+
entries: ScanEntry[],
|
|
231
|
+
unattributed: readonly UnattributedFile[],
|
|
232
|
+
): boolean {
|
|
233
|
+
const toolkitRoot = adapter.stamp?.toolkitRoot
|
|
234
|
+
if (toolkitRoot === undefined || unattributed.length === 0) return false
|
|
235
|
+
|
|
236
|
+
const index = readHistoryIndex(
|
|
237
|
+
toolkitRoot,
|
|
238
|
+
unattributed.map((file) => relative(toolkitRoot, file.source)),
|
|
239
|
+
)
|
|
240
|
+
|
|
241
|
+
if (index === undefined) return true
|
|
242
|
+
|
|
243
|
+
for (const file of unattributed) {
|
|
244
|
+
const since = findInstalledOrigin(
|
|
245
|
+
index,
|
|
246
|
+
relative(toolkitRoot, file.source),
|
|
247
|
+
file.path,
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
if (since === undefined) continue
|
|
251
|
+
entries[file.index] = { ...entries[file.index], state: 'stale', since }
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
return false
|
|
196
255
|
}
|
|
197
256
|
|
|
198
257
|
export async function applyChanges(
|
|
@@ -314,7 +373,12 @@ function report(adapter: SyncAdapter, plan: SyncPlan): void {
|
|
|
314
373
|
for (const entry of plan.entries) {
|
|
315
374
|
if (entry.state === 'matching') logInfo(entry.rel)
|
|
316
375
|
else if (entry.state === 'drifted') logWarn(entry.rel)
|
|
317
|
-
else if (entry.state === 'stale')
|
|
376
|
+
else if (entry.state === 'stale')
|
|
377
|
+
logWarn(
|
|
378
|
+
entry.since === undefined
|
|
379
|
+
? `${entry.rel} (toolkit updated)`
|
|
380
|
+
: `${entry.rel} (toolkit updated since ${entry.since.slice(0, 7)})`,
|
|
381
|
+
)
|
|
318
382
|
else if (entry.state === 'customized')
|
|
319
383
|
logWarn(`${entry.rel} (locally customized)`)
|
|
320
384
|
else if (entry.state === 'stranded')
|
|
@@ -325,6 +389,12 @@ function report(adapter: SyncAdapter, plan: SyncPlan): void {
|
|
|
325
389
|
for (const surface of plan.retired) {
|
|
326
390
|
logWarn(surface.notice)
|
|
327
391
|
}
|
|
392
|
+
|
|
393
|
+
if (plan.historyUnavailable) {
|
|
394
|
+
logWarn(
|
|
395
|
+
'Attribution unavailable: this toolkit has no git history to match against.',
|
|
396
|
+
)
|
|
397
|
+
}
|
|
328
398
|
}
|
|
329
399
|
|
|
330
400
|
function sameContent(left: string, right: string): boolean {
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import { createHash } from 'node:crypto'
|
|
2
|
+
import { readFileSync } from 'node:fs'
|
|
3
|
+
import { sep } from 'node:path'
|
|
4
|
+
import { execaSync } from 'execa'
|
|
5
|
+
import { gitEnv } from '@/git-env'
|
|
6
|
+
|
|
7
|
+
/** Blob SHA to the newest commit whose post-image for that path held it. */
|
|
8
|
+
export type PathHistory = ReadonlyMap<string, string>
|
|
9
|
+
|
|
10
|
+
/** Source path relative to the toolkit root, to that path's known blobs. */
|
|
11
|
+
export type HistoryIndex = ReadonlyMap<string, PathHistory>
|
|
12
|
+
|
|
13
|
+
const EMPTY_BLOB = '0000000000000000000000000000000000000000'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Git's own object name for a file's contents: sha1 over `blob <len>\0` and the
|
|
17
|
+
* bytes. Computed here rather than shelled out to `git hash-object`, which would
|
|
18
|
+
* cost a process per file inside the attribution pass.
|
|
19
|
+
*
|
|
20
|
+
* A repository normalizing line endings on checkout stores a blob the working
|
|
21
|
+
* tree never holds, so this returns a name that matches no historical version.
|
|
22
|
+
* The caller treats an unmatched file as unattributed, which is the safe label.
|
|
23
|
+
*/
|
|
24
|
+
export function gitBlobHash(content: Buffer): string {
|
|
25
|
+
const header = Buffer.from(`blob ${content.length}\0`, 'utf8')
|
|
26
|
+
return createHash('sha1').update(header).update(content).digest('hex')
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Reads `--raw` log output into blob-to-commit maps, one per path.
|
|
31
|
+
*
|
|
32
|
+
* Reverse-chronological input means the first commit naming a blob is the
|
|
33
|
+
* newest that produced it, so an existing key is never overwritten. Content
|
|
34
|
+
* reverted and restored therefore reports the recent commit rather than the
|
|
35
|
+
* original, which is the one an operator can act on.
|
|
36
|
+
*/
|
|
37
|
+
export function parseRawLog(output: string): HistoryIndex {
|
|
38
|
+
const index = new Map<string, Map<string, string>>()
|
|
39
|
+
let commit = ''
|
|
40
|
+
|
|
41
|
+
for (const line of output.split('\n')) {
|
|
42
|
+
if (line === '') continue
|
|
43
|
+
|
|
44
|
+
if (!line.startsWith(':')) {
|
|
45
|
+
commit = line.trim()
|
|
46
|
+
continue
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const [meta, path] = line.split('\t')
|
|
50
|
+
if (path === undefined || commit === '') continue
|
|
51
|
+
|
|
52
|
+
const blob = meta.trim().split(/\s+/)[3]
|
|
53
|
+
if (blob === undefined || blob === EMPTY_BLOB) continue
|
|
54
|
+
|
|
55
|
+
const blobs = index.get(path) ?? new Map<string, string>()
|
|
56
|
+
if (!blobs.has(blob)) blobs.set(blob, commit)
|
|
57
|
+
index.set(path, blobs)
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return index
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Every historical version of the given toolkit-relative paths, or `undefined`
|
|
65
|
+
* when this toolkit has no history to read. A package installed from the
|
|
66
|
+
* registry ships source without `.git`, so absence is an ordinary state rather
|
|
67
|
+
* than a failure, and the caller degrades to the unattributed path.
|
|
68
|
+
*
|
|
69
|
+
* Renames are disabled so a raw line always carries exactly one path. Detection
|
|
70
|
+
* would emit a two-path form that the parser would read as an unknown blob.
|
|
71
|
+
*/
|
|
72
|
+
export function readHistoryIndex(
|
|
73
|
+
toolkitRoot: string,
|
|
74
|
+
paths: readonly string[],
|
|
75
|
+
): HistoryIndex | undefined {
|
|
76
|
+
if (paths.length === 0) return new Map()
|
|
77
|
+
|
|
78
|
+
const result = execaSync(
|
|
79
|
+
'git',
|
|
80
|
+
[
|
|
81
|
+
'-C',
|
|
82
|
+
toolkitRoot,
|
|
83
|
+
'log',
|
|
84
|
+
'--all',
|
|
85
|
+
'--raw',
|
|
86
|
+
'--no-renames',
|
|
87
|
+
'--no-abbrev',
|
|
88
|
+
'--relative',
|
|
89
|
+
'--format=%H',
|
|
90
|
+
'--',
|
|
91
|
+
...paths.map(toRepoPath),
|
|
92
|
+
],
|
|
93
|
+
{ reject: false, env: gitEnv(), extendEnv: false },
|
|
94
|
+
)
|
|
95
|
+
|
|
96
|
+
return result.exitCode === 0 ? parseRawLog(result.stdout) : undefined
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Git prints and accepts posix separators on every platform, so a Windows
|
|
101
|
+
* caller's `relative()` output has to be converted before it can match a path
|
|
102
|
+
* the log emitted.
|
|
103
|
+
*/
|
|
104
|
+
function toRepoPath(path: string): string {
|
|
105
|
+
return path.split(sep).join('/')
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* The commit whose version of `sourceRel` matches what sits at `installedPath`.
|
|
110
|
+
* A match proves the file is untouched since it was installed, so the toolkit is
|
|
111
|
+
* what moved. No match means the content matches nothing this toolkit ever
|
|
112
|
+
* published, which is a local edit and stays unattributed.
|
|
113
|
+
*/
|
|
114
|
+
export function findInstalledOrigin(
|
|
115
|
+
index: HistoryIndex,
|
|
116
|
+
sourceRel: string,
|
|
117
|
+
installedPath: string,
|
|
118
|
+
): string | undefined {
|
|
119
|
+
const blobs = index.get(toRepoPath(sourceRel))
|
|
120
|
+
if (blobs === undefined) return undefined
|
|
121
|
+
|
|
122
|
+
return blobs.get(gitBlobHash(readFileSync(installedPath)))
|
|
123
|
+
}
|
|
@@ -15,7 +15,7 @@ Does not govern:
|
|
|
15
15
|
- Commit subject format, which shares the type vocabulary: `commit.md`
|
|
16
16
|
- Pull request title and body: `pr.md`
|
|
17
17
|
- Whether a phase label may appear in a branch name: `versioning.md`
|
|
18
|
-
- Deriving
|
|
18
|
+
- Deriving a slug from a branch name for use in an output filename: `slug.md`
|
|
19
19
|
|
|
20
20
|
## Format
|
|
21
21
|
|
package/standards/index.md
CHANGED
|
@@ -12,10 +12,12 @@ Reference docs for consistent authoring across the toolkit and target projects.
|
|
|
12
12
|
- [Design reference](design.md): Shape and content rules for .claude/DESIGN.md
|
|
13
13
|
- [Diagram reference](diagrams.md): Shape and content rules for .claude/diagrams/<kind>.md files
|
|
14
14
|
- [Prose reference](prose.md): Voice, structure, formatting, and language rules for reference markdown
|
|
15
|
+
- [Publish reference](publish.md): Scan run against finished text leaving through a channel no automated check covers
|
|
15
16
|
- [Readme reference](readme.md): Readme voice, structure, and content conventions
|
|
16
17
|
- [Requirements reference](requirements.md): Shape and content rules for .claude/REQUIREMENTS.md
|
|
17
18
|
- [Governance rule reference](rule.md): Rule frontmatter, body shape, and voice for .claude/rules files
|
|
18
19
|
- [Claude skill reference](skill.md): Claude skill structure and authoring rules
|
|
20
|
+
- [Slug reference](slug.md): Transform from a git branch name to a slug, and the three responses to an empty result
|
|
19
21
|
- [Standard reference](standard.md): Shape and content rules for authoring a standard
|
|
20
22
|
- [Tasks reference](tasks.md): Folder layout, filename convention, and content rules for .claude/tasks/
|
|
21
23
|
- [Versioning reference](versioning.md): Phase label vs semver discipline across tasks, PRs, reviews, issues, commits, and tags
|
package/standards/prose.md
CHANGED
|
@@ -5,7 +5,7 @@ description: Voice, structure, formatting, and language rules for reference mark
|
|
|
5
5
|
|
|
6
6
|
# Prose reference
|
|
7
7
|
|
|
8
|
-
Applies to markdown reference docs, READMEs, and inline documentation in repos. It is the default voice for `.md` files and yields to any surface with its own voice, such as blogs, emails, changelogs, or commit messages. It also yields wherever another standard states the voice for the surface it governs, which is how a surface claims the exemption without this file having to name it. The yield covers voice alone. Punctuation, formatting, and language rules below stay in force on every surface, including the surfaces no automated check reaches
|
|
8
|
+
Applies to markdown reference docs, READMEs, and inline documentation in repos. It is the default voice for `.md` files and yields to any surface with its own voice, such as blogs, emails, changelogs, or commit messages. It also yields wherever another standard states the voice for the surface it governs, which is how a surface claims the exemption without this file having to name it. The yield covers voice alone. Punctuation, formatting, and language rules below stay in force on every surface, including the surfaces no automated check reaches.
|
|
9
9
|
|
|
10
10
|
## Scope
|
|
11
11
|
|
|
@@ -16,6 +16,7 @@ Does not govern:
|
|
|
16
16
|
- What sections a document has, or what belongs in each: the standard for that document type
|
|
17
17
|
- Which frontmatter fields a document carries, which is that standard's own subject. This file governs the wording of a `title` and a `description` and nothing else about them.
|
|
18
18
|
- Phase-label and semver discipline: `versioning.md`
|
|
19
|
+
- The scan that applies these bans to finished text on its way out: `publish.md`
|
|
19
20
|
- Code style and language conventions, which are governance rules rather than a standard
|
|
20
21
|
|
|
21
22
|
## Voice
|
|
@@ -85,33 +86,6 @@ Does not govern:
|
|
|
85
86
|
- Do not address the reader as a participant (`Let's`, `Here's`, `Here are`). State the content directly.
|
|
86
87
|
- Commit to a position. Do not hedge in clusters (`It might be worth considering`) or use false balance (`While X is true, Y is also important`). Recommend, or state the tradeoff.
|
|
87
88
|
|
|
88
|
-
## Pre-publish scan
|
|
89
|
-
|
|
90
|
-
Wherever text leaves through a channel no automated check covers, the author is the only gate and runs this scan. Text sent to another service, written to a path the project's checks exclude, and text inside a fenced block are the usual cases. The surface that publishes the text is what knows which gap applies, so it names its own rather than reading one here.
|
|
91
|
-
|
|
92
|
-
Run the scan as an explicit step against the finished text. Having read this file before drafting does not cover it, because the check has to happen after the text exists.
|
|
93
|
-
|
|
94
|
-
### Banned characters
|
|
95
|
-
|
|
96
|
-
Scan the drafted text and rewrite each occurrence:
|
|
97
|
-
|
|
98
|
-
- `—` (em dash): split into two sentences, or use a comma
|
|
99
|
-
- `;` (semicolon): split into two sentences
|
|
100
|
-
|
|
101
|
-
Restructure the sentence rather than substituting the character. A semicolon swapped for a period leaves both clauses in the order the semicolon chose, which is the shape the ban exists to remove.
|
|
102
|
-
|
|
103
|
-
### Phase labels
|
|
104
|
-
|
|
105
|
-
The versioning standard beside this file holds the label rule and the table of surfaces. Read it at scan time rather than working the format from memory.
|
|
106
|
-
|
|
107
|
-
Scope this check by destination. Text published to a remote takes it. Text scanned on its way into the repository, where the reader has the task board, takes the character checks alone.
|
|
108
|
-
|
|
109
|
-
### An unreadable source
|
|
110
|
-
|
|
111
|
-
Stop and name the source when one this scan reaches for cannot be read. Do not scan what resolved and report the result.
|
|
112
|
-
|
|
113
|
-
A run that covers half its sources and says nothing is worse than one that visibly did not happen, because the surfaces running this scan are the ones that describe themselves as the only gate. A clean result from a half-run scan is read as coverage.
|
|
114
|
-
|
|
115
89
|
## Frontmatter descriptions
|
|
116
90
|
|
|
117
91
|
When frontmatter carries a short `title` or `description` used for catalog display:
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Publish reference
|
|
3
|
+
description: Scan run against finished text leaving through a channel no automated check covers
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Publish reference
|
|
7
|
+
|
|
8
|
+
## Scope
|
|
9
|
+
|
|
10
|
+
Governs the scan an author runs against finished text on its way out, and the responses available when a source it reaches for cannot be read. It is an attribute standard rather than a document-type one, so it applies to any text leaving through a channel no automated check covers.
|
|
11
|
+
|
|
12
|
+
Does not govern:
|
|
13
|
+
|
|
14
|
+
- Which characters are banned, and the voice and formatting the text is written in: `prose.md`
|
|
15
|
+
- The phase-label rule and the table of surfaces each namespace may appear on: `versioning.md`
|
|
16
|
+
- Which gap a given surface has, and what it publishes through, which that surface names for itself
|
|
17
|
+
|
|
18
|
+
## When it runs
|
|
19
|
+
|
|
20
|
+
Wherever text leaves through a channel no automated check covers, the author is the only gate and runs this scan. Text sent to another service, written to a path the project's checks exclude, and text inside a fenced block are the usual cases. The surface that publishes the text is what knows which gap applies, so it names its own rather than reading one here.
|
|
21
|
+
|
|
22
|
+
Run the scan as an explicit step against the finished text. Having read the underlying rules before drafting does not cover it, because the check has to happen after the text exists.
|
|
23
|
+
|
|
24
|
+
## Banned characters
|
|
25
|
+
|
|
26
|
+
`prose.md` holds the character bans. Read it at scan time rather than working them from memory, then scan the drafted text and rewrite each occurrence.
|
|
27
|
+
|
|
28
|
+
Restructure the sentence rather than substituting the character. A semicolon swapped for a period leaves both clauses in the order the semicolon chose, which is the shape the ban exists to remove.
|
|
29
|
+
|
|
30
|
+
## Phase labels
|
|
31
|
+
|
|
32
|
+
`versioning.md` holds the label rule and the table of surfaces. Read it at scan time rather than working the format from memory.
|
|
33
|
+
|
|
34
|
+
Scope this check by destination. Text published to a remote takes it. Text scanned on its way into the repository, where the reader has the task board, takes the character checks alone.
|
|
35
|
+
|
|
36
|
+
## An unreadable source
|
|
37
|
+
|
|
38
|
+
Stop and name the source when one this scan reaches for cannot be read. Do not scan what resolved and report the result.
|
|
39
|
+
|
|
40
|
+
A run that covers half its sources and says nothing is worse than one that visibly did not happen, because the surfaces running this scan are the ones that describe themselves as the only gate. A clean result from a half-run scan is read as coverage.
|
package/standards/skill.md
CHANGED
|
@@ -11,13 +11,14 @@ Skills give Claude Code domain-specific constraints and rules inline, so it can
|
|
|
11
11
|
|
|
12
12
|
## Scope
|
|
13
13
|
|
|
14
|
-
Governs a skill folder as one artifact: `SKILL.md`, its optional sibling `REQUIREMENT.md`, and the bundled
|
|
14
|
+
Governs a skill folder under `skills/` as one artifact: `SKILL.md`, its optional sibling `REQUIREMENT.md`, and the bundled reference, script, and asset folders beside them.
|
|
15
15
|
|
|
16
16
|
Does not govern:
|
|
17
17
|
|
|
18
18
|
- Path-scoped coding rules, which load on a file match rather than on a request match: `rule.md`
|
|
19
19
|
- Single-purpose chat prompts carrying no frontmatter, references, or scripts: `snippets.md`
|
|
20
20
|
- Voice, punctuation, and formatting in a skill body: `prose.md`
|
|
21
|
+
- The transform from a branch name to a slug a skill carries in a filename: `slug.md`
|
|
21
22
|
- The domain conventions a skill cites, each of which belongs to the standard that owns it
|
|
22
23
|
|
|
23
24
|
## Skill types
|
|
@@ -184,18 +185,6 @@ Without this skill, a session <observed failure>, <observed failure>.
|
|
|
184
185
|
- When a skill gathers user input or pre-seeds a template, attach a concrete proposed default to every question, derived from project context. Accept "use defaults" as a bulk-confirm.
|
|
185
186
|
- Separate correctness axes (routing, sourcing, escalation, decline) from shape axes (line count, formatting, variant sprawl) when tuning a skill. Tighten only on correctness regressions. Do not convert soft caps to hard caps for aesthetic drift when correctness passes.
|
|
186
187
|
|
|
187
|
-
### Deriving the branch slug
|
|
188
|
-
|
|
189
|
-
Run `git branch --show-current` and replace every `/` with `-`. The result is `<slug>`. Anything reading a branch-derived name uses this transform, so two skills cannot spell it differently.
|
|
190
|
-
|
|
191
|
-
A skill that persists output under `.claude/` carries the slug in the filename, which is what keeps parallel worktrees from overwriting each other's output.
|
|
192
|
-
|
|
193
|
-
The empty result is a detached HEAD, and the skill picks one of three responses rather than inheriting a default. State the choice in the body, since the transform is shared and this is not.
|
|
194
|
-
|
|
195
|
-
- Fall back to `latest`, so a read-only pass still writes somewhere predictable
|
|
196
|
-
- Stop, when the skill commits or opens a pull request. There is no branch to put the work on, so `latest` would bury the problem instead of reporting it. State the stop in the skill's guards.
|
|
197
|
-
- Fall through to the next source, when the slug is one candidate among several rather than the name of an output file
|
|
198
|
-
|
|
199
188
|
## Scripts
|
|
200
189
|
|
|
201
190
|
- Use `scripts/` for operations that must be deterministic or repetitive
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
---
|
|
2
|
+
title: Slug reference
|
|
3
|
+
description: Transform from a git branch name to a slug, and the three responses to an empty result
|
|
4
|
+
---
|
|
5
|
+
|
|
6
|
+
# Slug reference
|
|
7
|
+
|
|
8
|
+
## Scope
|
|
9
|
+
|
|
10
|
+
Governs the transform from a git branch name to the slug a surface carries in a derived name, and the responses available when the transform returns empty. It is an attribute standard rather than a document-type one, so it applies wherever a branch-derived name is produced.
|
|
11
|
+
|
|
12
|
+
Does not govern:
|
|
13
|
+
|
|
14
|
+
- The format of the branch name the transform reads: `branch.md`
|
|
15
|
+
- Where a surface writes what it names with the slug, and what goes in it, which is that surface's own subject
|
|
16
|
+
|
|
17
|
+
## The transform
|
|
18
|
+
|
|
19
|
+
Run `git branch --show-current` and replace every `/` with `-`. The result is `<slug>`. Anything reading a branch-derived name uses this transform, so two surfaces cannot spell it differently.
|
|
20
|
+
|
|
21
|
+
A surface that persists output to a shared folder carries the slug in the filename, which is what keeps parallel worktrees from overwriting each other's output.
|
|
22
|
+
|
|
23
|
+
## An empty result
|
|
24
|
+
|
|
25
|
+
The empty result is a detached HEAD, and the surface picks one of three responses rather than inheriting a default. State the choice where the transform is cited, since the transform is shared and this is not.
|
|
26
|
+
|
|
27
|
+
- Fall back to `latest`, so a read-only pass still writes somewhere predictable
|
|
28
|
+
- Stop, when the surface commits or opens a pull request. There is no branch to put the work on, so `latest` would bury the problem instead of reporting it. State the stop in the surface's guards.
|
|
29
|
+
- Fall through to the next source, when the slug is one candidate among several rather than the name of an output file
|
package/standards/standard.md
CHANGED
|
@@ -13,7 +13,7 @@ A standard is a target-facing authoring convention for one document type, or for
|
|
|
13
13
|
|
|
14
14
|
## Scope
|
|
15
15
|
|
|
16
|
-
Governs
|
|
16
|
+
Governs each authored standard under `standards/`: its stated jurisdiction, success criterion, frontmatter, structure, and rule phrasing.
|
|
17
17
|
|
|
18
18
|
Does not govern:
|
|
19
19
|
|
|
@@ -38,6 +38,8 @@ A standard failing these questions is non-conforming even when it satisfies ever
|
|
|
38
38
|
- Govern one document type per standard, or one attribute across every document. Split unrelated conventions into separate files.
|
|
39
39
|
- Open with a `## Scope` section stating what the standard governs and what it does not, placed above the shape rules. A standard that specifies shape exhaustively and jurisdiction nowhere cannot refuse a rule, so the rule with no obvious owner lands in whichever standard sits nearest.
|
|
40
40
|
- Write it as one line naming the artifact or attribute and where it applies, then a `Does not govern:` list. Give each entry the excluded concern and the owner it goes to. Name a sibling standard by bare filename, since standards install as siblings, and name the surface instead where the owner is one, such as a coding rule, a project policy, or the code.
|
|
41
|
+
- Put the governed path in backticks inside the first sentence of that line, anchored deep enough to resolve from a project root. A bare folder name matches a same-named folder elsewhere in the tree, and a path held back until a later sentence sits beside every other path the statement mentions, including the ones it excludes.
|
|
42
|
+
- Say in that same sentence when the standard governs an attribute rather than a document type. An attribute is written across every document and has no path to name, so the silence reads as an omission unless the sentence claims it.
|
|
41
43
|
- Cut an entry that names no owner at all. It is either excluding something nothing was going to claim, or it is a content exclusion, which the rule below sends to the shape rules instead.
|
|
42
44
|
- Declare a boundary from both sides. A yield, an exemption, or a handoff stated in one standard alone is never checked against the standard on the other side of it, which is how two files come to claim the same rule or neither does.
|
|
43
45
|
- Separate a jurisdiction exclusion from a content exclusion. The first names a concern another standard owns and belongs in `## Scope`. The second names what does not belong inside the document and stays with the shape rules. Merging them puts a boundary claim where no sibling will read it.
|