continuous-improvement 3.9.2 → 3.11.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-plugin/marketplace.json +2 -2
- package/CHANGELOG.md +61 -0
- package/QUICKSTART.md +13 -8
- package/README.md +84 -153
- package/bin/backfill.mjs +0 -0
- package/bin/check-doc-runtime-claims.mjs +0 -0
- package/bin/check-docs-substrings.mjs +73 -0
- package/bin/check-everything-mirror.mjs +0 -0
- package/bin/check-routing-targets.mjs +0 -0
- package/bin/check-scripts-citation-drift.mjs +210 -0
- package/bin/check-skill-count.mjs +110 -0
- package/bin/check-skill-law-tag.mjs +0 -0
- package/bin/check-skill-mirror.mjs +0 -0
- package/bin/check-skill-tiers.mjs +0 -0
- package/bin/check-test-imports-only.mjs +126 -0
- package/bin/check-third-party-shape.mjs +0 -0
- package/bin/companion-preference-status.mjs +231 -0
- package/bin/gateguard-clear.mjs +53 -0
- package/bin/generate-plugin-manifests.mjs +7 -1
- package/bin/harvest-friction.mjs +0 -0
- package/bin/hook-stats.mjs +0 -0
- package/bin/install.mjs +182 -26
- package/bin/lint-transcript.mjs +0 -0
- package/bin/mcp-server.mjs +215 -2
- package/bin/observe.mjs +0 -0
- package/bin/pre-commit-block-strays.sh +0 -0
- package/bin/refresh-third-party.mjs +315 -313
- package/bin/unified-cli.mjs +0 -0
- package/commands/audit.md +34 -0
- package/commands/companion-preference.md +58 -0
- package/commands/discipline.md +14 -0
- package/commands/distill.md +39 -0
- package/commands/goal-check.md +53 -0
- package/commands/grill-me.md +40 -0
- package/commands/grill-with-docs.md +38 -0
- package/commands/handoff.md +42 -0
- package/commands/proceed-with-the-recommendation.md +20 -0
- package/commands/recall.md +49 -0
- package/commands/reconcile.md +47 -0
- package/commands/superpowers.md +2 -2
- package/commands/verify-install.md +55 -0
- package/hooks/companion-preference.mjs +190 -0
- package/hooks/gateguard.mjs +60 -25
- package/hooks/goal-drift-stop.mjs +183 -0
- package/hooks/observe.sh +15 -1
- package/hooks/route-prompt.mjs +180 -0
- package/hooks/route-table.json +35 -0
- package/hooks/three-section-close.mjs +134 -132
- package/lib/cli-anything.mjs +0 -0
- package/lib/compound-engineering.mjs +0 -0
- package/lib/gateguard-state.mjs +54 -2
- package/lib/goal-drift-gate.mjs +50 -0
- package/lib/goal-state.mjs +285 -0
- package/lib/observe-event.mjs +0 -0
- package/lib/plugin-metadata.mjs +126 -12
- package/lib/pm-marketplace.mjs +0 -0
- package/lib/pm-skills.mjs +0 -0
- package/lib/recall-index.mjs +175 -0
- package/lib/resolve-home-dir.mjs +0 -0
- package/lib/skill-distill.mjs +222 -0
- package/lib/skill-tiers.mjs +0 -0
- package/lib/unified-plugin.mjs +0 -0
- package/llms.txt +1 -1
- package/package.json +11 -7
- package/plugins/beginner.json +8 -3
- package/plugins/continuous-improvement/.claude-plugin/marketplace.json +2 -2
- package/plugins/continuous-improvement/.claude-plugin/plugin.json +2 -2
- package/plugins/continuous-improvement/agents/README.md +2 -2
- package/plugins/continuous-improvement/bin/backfill.mjs +0 -0
- package/plugins/continuous-improvement/bin/gateguard-clear.mjs +53 -0
- package/plugins/continuous-improvement/bin/mcp-server.mjs +215 -2
- package/plugins/continuous-improvement/bin/observe.mjs +0 -0
- package/plugins/continuous-improvement/commands/audit.md +34 -0
- package/plugins/continuous-improvement/commands/companion-preference.md +58 -0
- package/plugins/continuous-improvement/commands/discipline.md +14 -0
- package/plugins/continuous-improvement/commands/distill.md +39 -0
- package/plugins/continuous-improvement/commands/goal-check.md +53 -0
- package/plugins/continuous-improvement/commands/grill-me.md +40 -0
- package/plugins/continuous-improvement/commands/grill-with-docs.md +38 -0
- package/plugins/continuous-improvement/commands/handoff.md +42 -0
- package/plugins/continuous-improvement/commands/proceed-with-the-recommendation.md +20 -0
- package/plugins/continuous-improvement/commands/recall.md +49 -0
- package/plugins/continuous-improvement/commands/reconcile.md +47 -0
- package/plugins/continuous-improvement/commands/superpowers.md +2 -2
- package/plugins/continuous-improvement/commands/verify-install.md +55 -0
- package/plugins/continuous-improvement/hooks/companion-preference.mjs +190 -0
- package/plugins/continuous-improvement/hooks/gateguard.mjs +60 -25
- package/plugins/continuous-improvement/hooks/goal-drift-stop.mjs +183 -0
- package/plugins/continuous-improvement/hooks/hooks.json +18 -2
- package/plugins/continuous-improvement/hooks/observe.sh +15 -1
- package/plugins/continuous-improvement/hooks/route-prompt.mjs +180 -0
- package/plugins/continuous-improvement/hooks/route-table.json +35 -0
- package/plugins/continuous-improvement/hooks/three-section-close.mjs +134 -132
- package/plugins/continuous-improvement/lib/gateguard-state.mjs +137 -0
- package/plugins/continuous-improvement/lib/goal-drift-gate.mjs +50 -0
- package/plugins/continuous-improvement/lib/goal-state.mjs +285 -0
- package/plugins/continuous-improvement/lib/observe-event.mjs +0 -0
- package/plugins/continuous-improvement/lib/plugin-metadata.mjs +126 -12
- package/plugins/continuous-improvement/lib/recall-index.mjs +175 -0
- package/plugins/continuous-improvement/lib/resolve-home-dir.mjs +0 -0
- package/plugins/continuous-improvement/lib/skill-distill.mjs +222 -0
- package/plugins/continuous-improvement/skills/README.md +8 -0
- package/plugins/continuous-improvement/skills/audit/SKILL.md +73 -0
- package/plugins/continuous-improvement/skills/deploy-receipt/SKILL.md +13 -17
- package/plugins/continuous-improvement/skills/gateguard/SKILL.md +36 -17
- package/plugins/continuous-improvement/skills/goal-monitor/SKILL.md +81 -0
- package/plugins/continuous-improvement/skills/grill-me/SKILL.md +66 -0
- package/plugins/continuous-improvement/skills/grill-with-docs/SKILL.md +252 -0
- package/plugins/continuous-improvement/skills/handoff/SKILL.md +59 -0
- package/plugins/continuous-improvement/skills/para-memory-files/SKILL.md +1 -1
- package/plugins/continuous-improvement/skills/proceed-with-the-recommendation/SKILL.md +82 -6
- package/plugins/continuous-improvement/skills/ralph/SKILL.md +1 -0
- package/plugins/continuous-improvement/skills/recall/SKILL.md +50 -0
- package/plugins/continuous-improvement/skills/reconcile/SKILL.md +80 -0
- package/plugins/continuous-improvement/skills/recovery-classification/SKILL.md +1 -0
- package/plugins/continuous-improvement/skills/safety-guard/SKILL.md +1 -0
- package/plugins/continuous-improvement/skills/skill-distillation/SKILL.md +57 -0
- package/plugins/continuous-improvement/skills/state-reconciliation/SKILL.md +1 -0
- package/plugins/continuous-improvement/skills/strategic-compact/SKILL.md +1 -0
- package/plugins/continuous-improvement/skills/superpowers/SKILL.md +76 -2
- package/plugins/continuous-improvement/skills/token-budget-advisor/SKILL.md +1 -1
- package/plugins/continuous-improvement/skills/verification-loop/SKILL.md +28 -20
- package/plugins/continuous-improvement/skills/wild-risa-balance/SKILL.md +1 -0
- package/plugins/continuous-improvement/skills/workspace-surface-audit/SKILL.md +1 -1
- package/plugins/continuous-improvement/skills/worktree-safety/SKILL.md +3 -2
- package/plugins/expert.json +27 -2
- package/skills/README.md +3 -0
- package/skills/audit.md +73 -0
- package/skills/deploy-receipt.md +13 -17
- package/skills/gateguard.md +36 -17
- package/skills/goal-monitor.md +81 -0
- package/skills/grill-me.md +66 -0
- package/skills/grill-with-docs.md +252 -0
- package/skills/handoff.md +59 -0
- package/skills/para-memory-files.md +1 -1
- package/skills/proceed-with-the-recommendation.md +82 -6
- package/skills/ralph.md +1 -0
- package/skills/recall.md +50 -0
- package/skills/reconcile.md +80 -0
- package/skills/recovery-classification.md +1 -0
- package/skills/safety-guard.md +1 -0
- package/skills/skill-distillation.md +57 -0
- package/skills/state-reconciliation.md +1 -0
- package/skills/strategic-compact.md +1 -0
- package/skills/superpowers.md +76 -2
- package/skills/token-budget-advisor.md +1 -1
- package/skills/verification-loop.md +28 -20
- package/skills/wild-risa-balance.md +1 -0
- package/skills/workspace-surface-audit.md +1 -1
- package/skills/worktree-safety.md +3 -2
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Scripts-Citation-Drift Invariant Check
|
|
4
|
+
*
|
|
5
|
+
* Locks the inventory-citation contract stated in scripts/README.md:
|
|
6
|
+
*
|
|
7
|
+
* "When a new script lands here, add a row to this table in the same PR
|
|
8
|
+
* and cite the script from at least one skill — otherwise the script is
|
|
9
|
+
* dead code on arrival."
|
|
10
|
+
*
|
|
11
|
+
* Three sides of the contract are enforced as one invariant:
|
|
12
|
+
*
|
|
13
|
+
* Side A — every file in scripts/ (excluding README.md) must appear in the
|
|
14
|
+
* Inventory table's Script column.
|
|
15
|
+
* Side B — for every Inventory row, every `skills/<name>.md` token in the
|
|
16
|
+
* Cited by cell must reference a file whose body contains the
|
|
17
|
+
* literal substring `scripts/<script-filename>` for at least one
|
|
18
|
+
* of the row's scripts.
|
|
19
|
+
* Side C — every skills/*.md that mentions `scripts/<filename>` for an
|
|
20
|
+
* inventoried script must appear in that script's row's Cited by
|
|
21
|
+
* cell.
|
|
22
|
+
*
|
|
23
|
+
* Origin: ongoing composability refactor (PRs #144–#148, #149) extracts
|
|
24
|
+
* shared primitives into scripts/. Without enforcement, the inventory and
|
|
25
|
+
* citations drift the first time a script lands without README + skill
|
|
26
|
+
* plumbing. The lint locks the contract in CI on every PR.
|
|
27
|
+
*
|
|
28
|
+
* Usage:
|
|
29
|
+
* node bin/check-scripts-citation-drift.mjs # Check the current repo
|
|
30
|
+
* node bin/check-scripts-citation-drift.mjs <repo-root> # Check a specific repo root
|
|
31
|
+
*
|
|
32
|
+
* Exit codes:
|
|
33
|
+
* 0 — all three sides reconciled
|
|
34
|
+
* 1 — at least one drift on at least one side
|
|
35
|
+
*/
|
|
36
|
+
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
37
|
+
import { join, posix, sep } from "node:path";
|
|
38
|
+
import { argv, cwd, exit } from "node:process";
|
|
39
|
+
const SCRIPTS_DIR = "scripts";
|
|
40
|
+
const SCRIPTS_README = "scripts/README.md";
|
|
41
|
+
const SKILLS_DIR = "skills";
|
|
42
|
+
const TABLE_HEADER_LINE = "| Script | Purpose | Cited by |";
|
|
43
|
+
export function parseInventoryTable(readmeContent) {
|
|
44
|
+
const lines = readmeContent.split(/\r?\n/);
|
|
45
|
+
const headerIdx = lines.findIndex((l) => l.trim() === TABLE_HEADER_LINE);
|
|
46
|
+
if (headerIdx === -1)
|
|
47
|
+
return [];
|
|
48
|
+
const rows = [];
|
|
49
|
+
for (let i = headerIdx + 2; i < lines.length; i++) {
|
|
50
|
+
const line = lines[i] ?? "";
|
|
51
|
+
const trimmed = line.trim();
|
|
52
|
+
if (!trimmed.startsWith("|"))
|
|
53
|
+
break;
|
|
54
|
+
if (trimmed === "")
|
|
55
|
+
break;
|
|
56
|
+
const cells = trimmed.split("|").slice(1, -1).map((c) => c.trim());
|
|
57
|
+
if (cells.length < 3)
|
|
58
|
+
continue;
|
|
59
|
+
const scriptCell = cells[0] ?? "";
|
|
60
|
+
const citedByCell = cells[2] ?? "";
|
|
61
|
+
const scripts = [...scriptCell.matchAll(/`([^`]+)`/g)].map((m) => m[1] ?? "");
|
|
62
|
+
const skills = [...citedByCell.matchAll(/`(skills\/[^`]+)`/g)].map((m) => m[1] ?? "");
|
|
63
|
+
rows.push({ scripts, skills, raw: trimmed });
|
|
64
|
+
}
|
|
65
|
+
return rows;
|
|
66
|
+
}
|
|
67
|
+
function listScriptFiles(repoRoot) {
|
|
68
|
+
const dir = join(repoRoot, SCRIPTS_DIR);
|
|
69
|
+
if (!existsSync(dir) || !statSync(dir).isDirectory())
|
|
70
|
+
return [];
|
|
71
|
+
return readdirSync(dir)
|
|
72
|
+
.filter((entry) => {
|
|
73
|
+
if (entry === "README.md")
|
|
74
|
+
return false;
|
|
75
|
+
try {
|
|
76
|
+
return statSync(join(dir, entry)).isFile();
|
|
77
|
+
}
|
|
78
|
+
catch {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
})
|
|
82
|
+
.sort();
|
|
83
|
+
}
|
|
84
|
+
function listSkillFiles(repoRoot) {
|
|
85
|
+
const dir = join(repoRoot, SKILLS_DIR);
|
|
86
|
+
if (!existsSync(dir) || !statSync(dir).isDirectory())
|
|
87
|
+
return [];
|
|
88
|
+
return readdirSync(dir)
|
|
89
|
+
.filter((entry) => entry.endsWith(".md"))
|
|
90
|
+
.map((entry) => `${SKILLS_DIR}/${entry}`)
|
|
91
|
+
.sort();
|
|
92
|
+
}
|
|
93
|
+
function toPosix(relPath) {
|
|
94
|
+
return relPath.split(sep).join(posix.sep);
|
|
95
|
+
}
|
|
96
|
+
export function checkRepo(repoRoot) {
|
|
97
|
+
const sideA = [];
|
|
98
|
+
const sideB = [];
|
|
99
|
+
const sideC = [];
|
|
100
|
+
const scriptFiles = listScriptFiles(repoRoot);
|
|
101
|
+
const readmePath = join(repoRoot, SCRIPTS_README);
|
|
102
|
+
const readmeContent = existsSync(readmePath) ? readFileSync(readmePath, "utf8") : "";
|
|
103
|
+
const rows = parseInventoryTable(readmeContent);
|
|
104
|
+
const inventoryScripts = new Set();
|
|
105
|
+
for (const row of rows) {
|
|
106
|
+
for (const s of row.scripts)
|
|
107
|
+
inventoryScripts.add(s);
|
|
108
|
+
}
|
|
109
|
+
for (const file of scriptFiles) {
|
|
110
|
+
if (!inventoryScripts.has(file)) {
|
|
111
|
+
sideA.push(file);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
let citationCount = 0;
|
|
115
|
+
const rowsByScript = new Map();
|
|
116
|
+
for (const row of rows) {
|
|
117
|
+
for (const s of row.scripts) {
|
|
118
|
+
if (!rowsByScript.has(s))
|
|
119
|
+
rowsByScript.set(s, row);
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
for (const row of rows) {
|
|
123
|
+
for (const skillPath of row.skills) {
|
|
124
|
+
citationCount += 1;
|
|
125
|
+
const fullSkill = join(repoRoot, skillPath);
|
|
126
|
+
let skillBody = "";
|
|
127
|
+
try {
|
|
128
|
+
skillBody = readFileSync(fullSkill, "utf8");
|
|
129
|
+
}
|
|
130
|
+
catch {
|
|
131
|
+
sideB.push(`${skillPath} — file does not exist (referenced by row "${row.scripts.join(", ")}")`);
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
const anyCited = row.scripts.some((script) => skillBody.includes(`scripts/${script}`));
|
|
135
|
+
if (!anyCited) {
|
|
136
|
+
const claimedScripts = row.scripts.map((s) => `scripts/${s}`).join(" or ");
|
|
137
|
+
sideB.push(`${skillPath} does not contain "${claimedScripts}" (claimed by row "${row.scripts.join(" + ")}")`);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
const skillFiles = listSkillFiles(repoRoot);
|
|
142
|
+
for (const skillPath of skillFiles) {
|
|
143
|
+
let body = "";
|
|
144
|
+
try {
|
|
145
|
+
body = readFileSync(join(repoRoot, skillPath), "utf8");
|
|
146
|
+
}
|
|
147
|
+
catch {
|
|
148
|
+
continue;
|
|
149
|
+
}
|
|
150
|
+
for (const inventoryScript of inventoryScripts) {
|
|
151
|
+
const needle = `scripts/${inventoryScript}`;
|
|
152
|
+
if (!body.includes(needle))
|
|
153
|
+
continue;
|
|
154
|
+
const row = rowsByScript.get(inventoryScript);
|
|
155
|
+
if (!row)
|
|
156
|
+
continue;
|
|
157
|
+
if (!row.skills.includes(skillPath)) {
|
|
158
|
+
sideC.push(`${skillPath} cites "${needle}" but is not listed in that script's Cited by cell`);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
return {
|
|
163
|
+
sideA,
|
|
164
|
+
sideB,
|
|
165
|
+
sideC,
|
|
166
|
+
scriptCount: scriptFiles.length,
|
|
167
|
+
citationCount,
|
|
168
|
+
};
|
|
169
|
+
}
|
|
170
|
+
function main() {
|
|
171
|
+
const repoRoot = argv[2] ?? cwd();
|
|
172
|
+
const result = checkRepo(repoRoot);
|
|
173
|
+
const totalViolations = result.sideA.length + result.sideB.length + result.sideC.length;
|
|
174
|
+
if (totalViolations === 0) {
|
|
175
|
+
console.log(`OK scripts-citation-drift: ${result.scriptCount} script(s), ${result.citationCount} skill citation(s), all three sides reconciled.`);
|
|
176
|
+
exit(0);
|
|
177
|
+
}
|
|
178
|
+
console.error(`FAIL scripts-citation-drift: ${totalViolations} drift(s) across the inventory-citation contract.`);
|
|
179
|
+
if (result.sideA.length > 0) {
|
|
180
|
+
console.error("");
|
|
181
|
+
console.error("Side A — script files missing from scripts/README.md inventory table:");
|
|
182
|
+
for (const v of result.sideA) {
|
|
183
|
+
console.error(` ${toPosix(v)}`);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
if (result.sideB.length > 0) {
|
|
187
|
+
console.error("");
|
|
188
|
+
console.error("Side B — inventory rows claim a citation the skill body does not contain:");
|
|
189
|
+
for (const v of result.sideB) {
|
|
190
|
+
console.error(` ${toPosix(v)}`);
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
if (result.sideC.length > 0) {
|
|
194
|
+
console.error("");
|
|
195
|
+
console.error("Side C — skill cites a real inventoried script but is not listed in that script's Cited by cell:");
|
|
196
|
+
for (const v of result.sideC) {
|
|
197
|
+
console.error(` ${toPosix(v)}`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
console.error("");
|
|
201
|
+
console.error("Fix: update scripts/README.md inventory (Side A/C) or correct the skill citation (Side B).");
|
|
202
|
+
exit(1);
|
|
203
|
+
}
|
|
204
|
+
const scriptPathFromArgv = argv[1];
|
|
205
|
+
const isMainModule = scriptPathFromArgv !== undefined &&
|
|
206
|
+
(import.meta.url.endsWith(scriptPathFromArgv.replace(/\\/g, "/")) ||
|
|
207
|
+
scriptPathFromArgv.endsWith("check-scripts-citation-drift.mjs"));
|
|
208
|
+
if (isMainModule) {
|
|
209
|
+
main();
|
|
210
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Skill Count Check
|
|
4
|
+
*
|
|
5
|
+
* The plugin's user-facing description strings claim a bundled-skill count
|
|
6
|
+
* ("N bundled skills"). That number is hand-maintained inside
|
|
7
|
+
* src/lib/plugin-metadata.mts (SHARED_PLUGIN_DESCRIPTION) and package.json,
|
|
8
|
+
* and it drifts every time a skill is added without the count being bumped.
|
|
9
|
+
*
|
|
10
|
+
* Origin: the plugin-flow stress-test (2026-05-14) found marketplace.json and
|
|
11
|
+
* plugin.json both claiming "13 enforcement skills" while the bundle shipped
|
|
12
|
+
* 20. This lint locks the claim to the actual source-of-truth count so the
|
|
13
|
+
* marketplace card can never undersell (or oversell) the bundle again.
|
|
14
|
+
*
|
|
15
|
+
* Source of truth: skill directories under `plugins/continuous-improvement/skills/`
|
|
16
|
+
* (one `<name>/SKILL.md` per skill). This is the literal bundled-skill set —
|
|
17
|
+
* `npm run build` mirrors each `skills/<name>.md` standalone source (plus the
|
|
18
|
+
* core skill, whose standalone source is the repo-root `SKILL.md`) into it, and
|
|
19
|
+
* `verify:skill-mirror` already guarantees parity. Counting the bundle is the
|
|
20
|
+
* honest reading of the phrase "N bundled skills".
|
|
21
|
+
*
|
|
22
|
+
* Checked files (each must contain the literal `<count> bundled skills`):
|
|
23
|
+
* - .claude-plugin/marketplace.json
|
|
24
|
+
* - plugins/continuous-improvement/.claude-plugin/plugin.json
|
|
25
|
+
* - package.json
|
|
26
|
+
* - llms.txt
|
|
27
|
+
*
|
|
28
|
+
* Usage:
|
|
29
|
+
* node bin/check-skill-count.mjs # Check the current repo
|
|
30
|
+
* node bin/check-skill-count.mjs <repo-root> # Check a specific repo root
|
|
31
|
+
*
|
|
32
|
+
* Exit codes:
|
|
33
|
+
* 0 — every checked file states the correct bundled-skill count
|
|
34
|
+
* 1 — at least one file states a stale or missing count
|
|
35
|
+
*/
|
|
36
|
+
import { readFileSync, readdirSync, statSync } from "node:fs";
|
|
37
|
+
import { join } from "node:path";
|
|
38
|
+
import { argv, cwd, exit } from "node:process";
|
|
39
|
+
const BUNDLE_SKILLS_DIR = "plugins/continuous-improvement/skills";
|
|
40
|
+
const CHECKED_FILES = [
|
|
41
|
+
".claude-plugin/marketplace.json",
|
|
42
|
+
"plugins/continuous-improvement/.claude-plugin/plugin.json",
|
|
43
|
+
"package.json",
|
|
44
|
+
"llms.txt",
|
|
45
|
+
];
|
|
46
|
+
export function countSkills(repoRoot) {
|
|
47
|
+
const skillsDir = join(repoRoot, BUNDLE_SKILLS_DIR);
|
|
48
|
+
const entries = readdirSync(skillsDir);
|
|
49
|
+
return entries.filter((entry) => {
|
|
50
|
+
try {
|
|
51
|
+
return statSync(join(skillsDir, entry)).isDirectory();
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
return false;
|
|
55
|
+
}
|
|
56
|
+
}).length;
|
|
57
|
+
}
|
|
58
|
+
function checkFile(repoRoot, relPath, expected) {
|
|
59
|
+
let content;
|
|
60
|
+
try {
|
|
61
|
+
content = readFileSync(join(repoRoot, relPath), "utf8");
|
|
62
|
+
}
|
|
63
|
+
catch {
|
|
64
|
+
return { file: relPath, reason: "missing" };
|
|
65
|
+
}
|
|
66
|
+
// Non-digit boundary before the count so "3 bundled skills" does not match
|
|
67
|
+
// inside "13 bundled skills".
|
|
68
|
+
const expectedPattern = new RegExp(`(?<!\\d)${expected} bundled skills`);
|
|
69
|
+
if (expectedPattern.test(content))
|
|
70
|
+
return null;
|
|
71
|
+
const staleMatch = content.match(/(\d+) bundled skills/);
|
|
72
|
+
return {
|
|
73
|
+
file: relPath,
|
|
74
|
+
reason: staleMatch ? "stale" : "missing",
|
|
75
|
+
found: staleMatch ? staleMatch[0] : undefined,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
function main() {
|
|
79
|
+
const repoRoot = argv[2] ?? cwd();
|
|
80
|
+
const expected = countSkills(repoRoot);
|
|
81
|
+
const violations = [];
|
|
82
|
+
for (const file of CHECKED_FILES) {
|
|
83
|
+
const v = checkFile(repoRoot, file, expected);
|
|
84
|
+
if (v)
|
|
85
|
+
violations.push(v);
|
|
86
|
+
}
|
|
87
|
+
if (violations.length === 0) {
|
|
88
|
+
console.log(`OK skill-count: all ${CHECKED_FILES.length} description string(s) state "${expected} bundled skills" (matches skills/ source-of-truth).`);
|
|
89
|
+
exit(0);
|
|
90
|
+
}
|
|
91
|
+
console.error(`FAIL skill-count: ${violations.length} description string(s) do not state the actual bundled-skill count of ${expected}.`);
|
|
92
|
+
console.error(`Source of truth is ${expected} kebab-case *.md file(s) under skills/. Each checked file must contain the literal "${expected} bundled skills".`);
|
|
93
|
+
console.error("");
|
|
94
|
+
for (const v of violations) {
|
|
95
|
+
if (v.reason === "stale") {
|
|
96
|
+
console.error(` ${v.file} — states "${v.found}", expected "${expected} bundled skills"`);
|
|
97
|
+
}
|
|
98
|
+
else {
|
|
99
|
+
console.error(` ${v.file} — no "<count> bundled skills" phrase found`);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
console.error("");
|
|
103
|
+
console.error("Fix: update SHARED_PLUGIN_DESCRIPTION in src/lib/plugin-metadata.mts and the");
|
|
104
|
+
console.error("package.json description, then re-run `npm run build` to regenerate manifests.");
|
|
105
|
+
exit(1);
|
|
106
|
+
}
|
|
107
|
+
const invokedDirectly = argv[1] !== undefined && import.meta.url.endsWith(argv[1].replace(/\\/g, "/"));
|
|
108
|
+
if (invokedDirectly || argv[1]?.endsWith("check-skill-count.mjs")) {
|
|
109
|
+
main();
|
|
110
|
+
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Test-Imports-Only Invariant Check
|
|
4
|
+
*
|
|
5
|
+
* Enforces the test-isolation discipline that emerged from the composability
|
|
6
|
+
* refactor train (PRs #144–#148): every test file's non-`node:*` import must
|
|
7
|
+
* resolve to exactly one of `../bin/<x>.mjs` or `../lib/<x>.mjs`.
|
|
8
|
+
*
|
|
9
|
+
* Catches:
|
|
10
|
+
* - cross-test coupling (one test importing another test or shared helper)
|
|
11
|
+
* - bare npm specifiers leaking into the test surface
|
|
12
|
+
* - reach-through into `hooks/`, `scripts/`, `src/`, or deeper paths under
|
|
13
|
+
* `bin/` or `lib/`
|
|
14
|
+
*
|
|
15
|
+
* Baseline: all 50 `src/test/*.test.mts` files pass today.
|
|
16
|
+
*
|
|
17
|
+
* Usage:
|
|
18
|
+
* node bin/check-test-imports-only.mjs # Check the current repo
|
|
19
|
+
* node bin/check-test-imports-only.mjs <repo-root> # Check a specific repo root
|
|
20
|
+
*
|
|
21
|
+
* Exit codes:
|
|
22
|
+
* 0 — every scanned test file imports only from node:* or ../bin/*.mjs or ../lib/*.mjs
|
|
23
|
+
* 1 — at least one scanned test file imports something outside the allow-list
|
|
24
|
+
*/
|
|
25
|
+
import { existsSync, readFileSync, readdirSync, statSync } from "node:fs";
|
|
26
|
+
import { join, posix, sep } from "node:path";
|
|
27
|
+
import { argv, cwd, exit } from "node:process";
|
|
28
|
+
const TEST_SOURCE_DIR = "src/test";
|
|
29
|
+
const ORPHAN_TESTS = [];
|
|
30
|
+
const ALLOWED_PRODUCTION_PATH = /^\.\.\/(bin|lib)\/[A-Za-z0-9._-]+\.mjs$/;
|
|
31
|
+
export function isAllowedSpecifier(specifier) {
|
|
32
|
+
if (specifier.startsWith("node:"))
|
|
33
|
+
return true;
|
|
34
|
+
if (ALLOWED_PRODUCTION_PATH.test(specifier))
|
|
35
|
+
return true;
|
|
36
|
+
return false;
|
|
37
|
+
}
|
|
38
|
+
export function extractImports(content) {
|
|
39
|
+
const lines = content.split(/\r?\n/);
|
|
40
|
+
const fromClause = /\bfrom\s+["']([^"']+)["']/;
|
|
41
|
+
const importStart = /^\s*import\b/;
|
|
42
|
+
const out = [];
|
|
43
|
+
let inImport = false;
|
|
44
|
+
for (let i = 0; i < lines.length; i++) {
|
|
45
|
+
const line = lines[i] ?? "";
|
|
46
|
+
if (!inImport) {
|
|
47
|
+
if (!importStart.test(line))
|
|
48
|
+
continue;
|
|
49
|
+
const m = fromClause.exec(line);
|
|
50
|
+
if (m && m[1] !== undefined) {
|
|
51
|
+
out.push({ line: i + 1, specifier: m[1] });
|
|
52
|
+
// single-line import resolved; stay in 'searching'
|
|
53
|
+
}
|
|
54
|
+
else {
|
|
55
|
+
inImport = true;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
else {
|
|
59
|
+
const m = fromClause.exec(line);
|
|
60
|
+
if (m && m[1] !== undefined) {
|
|
61
|
+
out.push({ line: i + 1, specifier: m[1] });
|
|
62
|
+
inImport = false;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return out;
|
|
67
|
+
}
|
|
68
|
+
function discoverTestFiles(repoRoot) {
|
|
69
|
+
const found = [];
|
|
70
|
+
const dir = join(repoRoot, TEST_SOURCE_DIR);
|
|
71
|
+
if (existsSync(dir) && statSync(dir).isDirectory()) {
|
|
72
|
+
for (const entry of readdirSync(dir)) {
|
|
73
|
+
if (entry.endsWith(".test.mts")) {
|
|
74
|
+
found.push(join(TEST_SOURCE_DIR, entry));
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
for (const orphan of ORPHAN_TESTS) {
|
|
79
|
+
if (existsSync(join(repoRoot, orphan))) {
|
|
80
|
+
found.push(orphan);
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
return found.sort();
|
|
84
|
+
}
|
|
85
|
+
function toPosix(relPath) {
|
|
86
|
+
return relPath.split(sep).join(posix.sep);
|
|
87
|
+
}
|
|
88
|
+
function main() {
|
|
89
|
+
const repoRoot = argv[2] ?? cwd();
|
|
90
|
+
const files = discoverTestFiles(repoRoot);
|
|
91
|
+
const violations = [];
|
|
92
|
+
let productionImports = 0;
|
|
93
|
+
for (const relPath of files) {
|
|
94
|
+
const content = readFileSync(join(repoRoot, relPath), "utf8");
|
|
95
|
+
const imports = extractImports(content);
|
|
96
|
+
for (const { line, specifier } of imports) {
|
|
97
|
+
if (!isAllowedSpecifier(specifier)) {
|
|
98
|
+
violations.push({ file: toPosix(relPath), line, specifier });
|
|
99
|
+
}
|
|
100
|
+
else if (!specifier.startsWith("node:")) {
|
|
101
|
+
productionImports += 1;
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
if (violations.length === 0) {
|
|
106
|
+
console.log(`OK test-imports-only: ${files.length} test file(s), ${productionImports} production import(s), all within node:* or ../bin/*.mjs or ../lib/*.mjs.`);
|
|
107
|
+
exit(0);
|
|
108
|
+
}
|
|
109
|
+
console.error(`FAIL test-imports-only: ${violations.length} non-node import(s) outside the allow-list.`);
|
|
110
|
+
console.error("");
|
|
111
|
+
for (const v of violations) {
|
|
112
|
+
console.error(` ${v.file}:${v.line} — imports "${v.specifier}"`);
|
|
113
|
+
}
|
|
114
|
+
console.error("");
|
|
115
|
+
console.error("Allow-list: node:* builtins, ../bin/<name>.mjs, ../lib/<name>.mjs.");
|
|
116
|
+
console.error("Fix: replace the offending import with a node:* equivalent, an allowed");
|
|
117
|
+
console.error("production import, or a `spawnSync` of the compiled `.mjs`.");
|
|
118
|
+
exit(1);
|
|
119
|
+
}
|
|
120
|
+
const scriptPathFromArgv = argv[1];
|
|
121
|
+
const isMainModule = scriptPathFromArgv !== undefined &&
|
|
122
|
+
(import.meta.url.endsWith(scriptPathFromArgv.replace(/\\/g, "/")) ||
|
|
123
|
+
scriptPathFromArgv.endsWith("check-test-imports-only.mjs"));
|
|
124
|
+
if (isMainModule) {
|
|
125
|
+
main();
|
|
126
|
+
}
|
|
File without changes
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* /companion-preference status — read & aggregate hook telemetry.
|
|
4
|
+
*
|
|
5
|
+
* Reads ~/.claude/instincts/<project-hash>/companion-preference.jsonl
|
|
6
|
+
* (or an explicit path via --path), groups events by ci_skill within the
|
|
7
|
+
* configured window (default 7 days), and prints a summary report with
|
|
8
|
+
* per-skill totals, action breakdown, and companion_installed%.
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* node bin/companion-preference-status.mjs # last 7 days, human
|
|
12
|
+
* node bin/companion-preference-status.mjs --days 30 # 30-day window
|
|
13
|
+
* node bin/companion-preference-status.mjs --all # whole file, no window
|
|
14
|
+
* node bin/companion-preference-status.mjs --json # machine-readable JSON
|
|
15
|
+
* node bin/companion-preference-status.mjs --path <p> # override JSONL path
|
|
16
|
+
*
|
|
17
|
+
* Exit codes:
|
|
18
|
+
* 0 — report rendered (including the "no telemetry recorded yet" case)
|
|
19
|
+
* 1 — argument parse error or unexpected runtime failure
|
|
20
|
+
*/
|
|
21
|
+
import { execFileSync } from "node:child_process";
|
|
22
|
+
import { createHash } from "node:crypto";
|
|
23
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
24
|
+
import { homedir } from "node:os";
|
|
25
|
+
import { join } from "node:path";
|
|
26
|
+
import { argv, exit, stdout } from "node:process";
|
|
27
|
+
const ACTIONS = [
|
|
28
|
+
"observation",
|
|
29
|
+
"advisory",
|
|
30
|
+
"block",
|
|
31
|
+
"block-not-installed",
|
|
32
|
+
];
|
|
33
|
+
function parseArgs(args) {
|
|
34
|
+
const out = { days: 7, all: false, json: false, path: null };
|
|
35
|
+
for (let i = 0; i < args.length; i += 1) {
|
|
36
|
+
const a = args[i];
|
|
37
|
+
if (a === "--days") {
|
|
38
|
+
const v = Number(args[++i]);
|
|
39
|
+
if (!Number.isFinite(v) || v <= 0) {
|
|
40
|
+
process.stderr.write(`--days requires a positive integer (got ${args[i]})\n`);
|
|
41
|
+
exit(1);
|
|
42
|
+
}
|
|
43
|
+
out.days = v;
|
|
44
|
+
}
|
|
45
|
+
else if (a === "--all") {
|
|
46
|
+
out.all = true;
|
|
47
|
+
out.days = null;
|
|
48
|
+
}
|
|
49
|
+
else if (a === "--json") {
|
|
50
|
+
out.json = true;
|
|
51
|
+
}
|
|
52
|
+
else if (a === "--path") {
|
|
53
|
+
out.path = args[++i] ?? null;
|
|
54
|
+
if (!out.path) {
|
|
55
|
+
process.stderr.write(`--path requires a value\n`);
|
|
56
|
+
exit(1);
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
else if (a === "--help" || a === "-h") {
|
|
60
|
+
printHelp();
|
|
61
|
+
exit(0);
|
|
62
|
+
}
|
|
63
|
+
else if (a !== undefined) {
|
|
64
|
+
process.stderr.write(`unknown argument: ${a}\n`);
|
|
65
|
+
exit(1);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
return out;
|
|
69
|
+
}
|
|
70
|
+
function printHelp() {
|
|
71
|
+
stdout.write([
|
|
72
|
+
"Usage: companion-preference-status [options]",
|
|
73
|
+
"",
|
|
74
|
+
"Options:",
|
|
75
|
+
" --days N Only count events from the last N days (default 7)",
|
|
76
|
+
" --all Ignore the window; count every row in the file",
|
|
77
|
+
" --json Emit a machine-readable JSON report",
|
|
78
|
+
" --path <file> Override the default JSONL path (testing)",
|
|
79
|
+
" -h, --help Print this help",
|
|
80
|
+
"",
|
|
81
|
+
].join("\n"));
|
|
82
|
+
}
|
|
83
|
+
function resolveProjectRoot() {
|
|
84
|
+
const fromEnv = process.env.CLAUDE_PROJECT_DIR;
|
|
85
|
+
if (fromEnv)
|
|
86
|
+
return fromEnv;
|
|
87
|
+
try {
|
|
88
|
+
const root = execFileSync("git", ["rev-parse", "--show-toplevel"], {
|
|
89
|
+
encoding: "utf8",
|
|
90
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
91
|
+
}).trim();
|
|
92
|
+
if (root)
|
|
93
|
+
return root;
|
|
94
|
+
}
|
|
95
|
+
catch {
|
|
96
|
+
// not in a git repo
|
|
97
|
+
}
|
|
98
|
+
return "global";
|
|
99
|
+
}
|
|
100
|
+
function defaultJsonlPath() {
|
|
101
|
+
const home = process.env.HOME ?? process.env.USERPROFILE ?? homedir();
|
|
102
|
+
const hash = createHash("sha256")
|
|
103
|
+
.update(resolveProjectRoot())
|
|
104
|
+
.digest("hex")
|
|
105
|
+
.slice(0, 12);
|
|
106
|
+
return join(home, ".claude", "instincts", hash, "companion-preference.jsonl");
|
|
107
|
+
}
|
|
108
|
+
function parseJsonl(raw) {
|
|
109
|
+
const rows = [];
|
|
110
|
+
let parseErrors = 0;
|
|
111
|
+
for (const line of raw.split(/\r?\n/)) {
|
|
112
|
+
if (line.trim().length === 0)
|
|
113
|
+
continue;
|
|
114
|
+
try {
|
|
115
|
+
rows.push(JSON.parse(line));
|
|
116
|
+
}
|
|
117
|
+
catch {
|
|
118
|
+
parseErrors += 1;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
return { rows, parseErrors };
|
|
122
|
+
}
|
|
123
|
+
function aggregate(rows) {
|
|
124
|
+
const bySkill = {};
|
|
125
|
+
const byMode = {};
|
|
126
|
+
const installedYes = {};
|
|
127
|
+
for (const row of rows) {
|
|
128
|
+
const stats = (bySkill[row.ci_skill] ??= {
|
|
129
|
+
total: 0,
|
|
130
|
+
actions: {},
|
|
131
|
+
installed_pct: null,
|
|
132
|
+
});
|
|
133
|
+
stats.total += 1;
|
|
134
|
+
stats.actions[row.action] = (stats.actions[row.action] ?? 0) + 1;
|
|
135
|
+
if (row.companion_installed) {
|
|
136
|
+
installedYes[row.ci_skill] = (installedYes[row.ci_skill] ?? 0) + 1;
|
|
137
|
+
}
|
|
138
|
+
byMode[row.mode] = (byMode[row.mode] ?? 0) + 1;
|
|
139
|
+
}
|
|
140
|
+
for (const [skill, stats] of Object.entries(bySkill)) {
|
|
141
|
+
stats.installed_pct =
|
|
142
|
+
stats.total > 0
|
|
143
|
+
? Math.round(((installedYes[skill] ?? 0) * 100) / stats.total)
|
|
144
|
+
: null;
|
|
145
|
+
}
|
|
146
|
+
return { bySkill, byMode };
|
|
147
|
+
}
|
|
148
|
+
function pad(s, width) {
|
|
149
|
+
return s.length >= width ? s : s + " ".repeat(width - s.length);
|
|
150
|
+
}
|
|
151
|
+
function rpad(n, width) {
|
|
152
|
+
const s = String(n);
|
|
153
|
+
return s.length >= width ? s : " ".repeat(width - s.length) + s;
|
|
154
|
+
}
|
|
155
|
+
function formatHuman(report) {
|
|
156
|
+
const lines = [];
|
|
157
|
+
const windowLabel = report.window_days === null ? "all-time" : `last ${report.window_days} days`;
|
|
158
|
+
lines.push(`Companion-preference telemetry — ${windowLabel}`);
|
|
159
|
+
lines.push(`File: ${report.file}`);
|
|
160
|
+
if (report.since)
|
|
161
|
+
lines.push(`Window: ${report.since} to ${new Date().toISOString()}`);
|
|
162
|
+
lines.push("");
|
|
163
|
+
if (report.total_events === 0) {
|
|
164
|
+
lines.push("No events in window.");
|
|
165
|
+
}
|
|
166
|
+
else {
|
|
167
|
+
const skillWidth = Math.max(8, ...Object.keys(report.by_skill).map((k) => k.length));
|
|
168
|
+
const header = pad("ci_skill", skillWidth) +
|
|
169
|
+
" " +
|
|
170
|
+
["total", "observation", "advisory", "block", "block-not-installed", "installed%"]
|
|
171
|
+
.map((h) => rpad(h, h.length))
|
|
172
|
+
.join(" ");
|
|
173
|
+
lines.push(header);
|
|
174
|
+
for (const [skill, stats] of Object.entries(report.by_skill).sort((a, b) => b[1].total - a[1].total)) {
|
|
175
|
+
const cells = [
|
|
176
|
+
rpad(stats.total, "total".length),
|
|
177
|
+
rpad(stats.actions.observation ?? 0, "observation".length),
|
|
178
|
+
rpad(stats.actions.advisory ?? 0, "advisory".length),
|
|
179
|
+
rpad(stats.actions.block ?? 0, "block".length),
|
|
180
|
+
rpad(stats.actions["block-not-installed"] ?? 0, "block-not-installed".length),
|
|
181
|
+
rpad(stats.installed_pct === null ? "—" : `${stats.installed_pct}`, "installed%".length),
|
|
182
|
+
].join(" ");
|
|
183
|
+
lines.push(pad(skill, skillWidth) + " " + cells);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
lines.push("");
|
|
187
|
+
const modeEntries = Object.entries(report.by_mode);
|
|
188
|
+
if (modeEntries.length > 0) {
|
|
189
|
+
lines.push(`Mode totals: ${modeEntries.map(([k, v]) => `${k} ${v}`).join(", ")}`);
|
|
190
|
+
}
|
|
191
|
+
lines.push(`Total events: ${report.total_events}`);
|
|
192
|
+
if (report.parse_errors > 0)
|
|
193
|
+
lines.push(`Parse errors (skipped): ${report.parse_errors}`);
|
|
194
|
+
return lines.join("\n") + "\n";
|
|
195
|
+
}
|
|
196
|
+
function main() {
|
|
197
|
+
const args = parseArgs(argv.slice(2));
|
|
198
|
+
const filePath = args.path ?? defaultJsonlPath();
|
|
199
|
+
if (!existsSync(filePath)) {
|
|
200
|
+
stdout.write(`Companion-preference telemetry: no telemetry recorded yet at ${filePath}\n`);
|
|
201
|
+
exit(0);
|
|
202
|
+
}
|
|
203
|
+
const raw = readFileSync(filePath, "utf8");
|
|
204
|
+
const { rows, parseErrors } = parseJsonl(raw);
|
|
205
|
+
const cutoff = args.all || args.days === null
|
|
206
|
+
? null
|
|
207
|
+
: new Date(Date.now() - args.days * 24 * 3600 * 1000).toISOString();
|
|
208
|
+
const filtered = cutoff === null ? rows : rows.filter((r) => r.ts >= cutoff);
|
|
209
|
+
const { bySkill, byMode } = aggregate(filtered);
|
|
210
|
+
const report = {
|
|
211
|
+
file: filePath,
|
|
212
|
+
window_days: args.all ? null : args.days,
|
|
213
|
+
since: cutoff,
|
|
214
|
+
total_events: filtered.length,
|
|
215
|
+
parse_errors: parseErrors,
|
|
216
|
+
by_skill: bySkill,
|
|
217
|
+
by_mode: byMode,
|
|
218
|
+
};
|
|
219
|
+
if (args.json) {
|
|
220
|
+
stdout.write(JSON.stringify(report, null, 2) + "\n");
|
|
221
|
+
}
|
|
222
|
+
else {
|
|
223
|
+
stdout.write(formatHuman(report));
|
|
224
|
+
}
|
|
225
|
+
exit(0);
|
|
226
|
+
}
|
|
227
|
+
const invokedDirectly = argv[1]?.endsWith("companion-preference-status.mjs");
|
|
228
|
+
if (invokedDirectly) {
|
|
229
|
+
main();
|
|
230
|
+
}
|
|
231
|
+
export { ACTIONS, aggregate, parseArgs, parseJsonl, };
|