continuous-improvement 3.23.0 → 3.25.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 +37 -0
- package/QUICKSTART.md +19 -20
- package/README.md +71 -18
- package/SKILL.md +4 -0
- package/bin/check-invariant-count.mjs +144 -0
- package/bin/check-routing-targets.mjs +74 -4
- package/bin/check-test-count.mjs +135 -0
- package/bin/companion-preference-status.mjs +2 -5
- package/bin/generate-plugin-manifests.mjs +21 -2
- package/bin/harvest-friction.mjs +10 -8
- package/bin/install.mjs +4 -14
- package/bin/mcp-server.mjs +4 -9
- package/bin/observe.mjs +3 -3
- package/bin/reconcile-instinct-hashes.mjs +226 -0
- package/bin/refresh-third-party.mjs +180 -10
- package/commands/discipline.md +5 -2
- package/commands/reconcile.md +1 -1
- package/commands/superpowers.md +2 -2
- package/commands/verify-install.md +8 -3
- package/hooks/companion-preference.mjs +3 -10
- package/hooks/config-guard.mjs +94 -0
- package/hooks/gateguard.mjs +39 -39
- package/hooks/goal-drift-stop.mjs +2 -2
- package/hooks/query-cost-nudge.mjs +2 -2
- package/hooks/recall-briefing.mjs +2 -2
- package/hooks/route-prompt.mjs +2 -5
- package/hooks/session.mjs +2 -2
- package/hooks/workflow-distill.mjs +2 -2
- package/lib/config-guard-gate.mjs +243 -0
- package/lib/destructive-bash.mjs +216 -0
- package/lib/gateguard-state.mjs +5 -1
- package/lib/plugin-metadata.mjs +12 -1
- package/lib/skill-catalog.mjs +169 -0
- package/llms.txt +12 -1
- package/package.json +5 -3
- package/plugins/beginner.json +1 -1
- package/plugins/continuous-improvement/.claude-plugin/marketplace.json +2 -2
- package/plugins/continuous-improvement/.claude-plugin/plugin.json +2 -2
- package/plugins/continuous-improvement/README.md +1 -2
- package/plugins/continuous-improvement/bin/mcp-server.mjs +4 -9
- package/plugins/continuous-improvement/bin/observe.mjs +3 -3
- package/plugins/continuous-improvement/commands/discipline.md +5 -2
- package/plugins/continuous-improvement/commands/reconcile.md +1 -1
- package/plugins/continuous-improvement/commands/superpowers.md +2 -2
- package/plugins/continuous-improvement/commands/verify-install.md +8 -3
- package/plugins/continuous-improvement/hooks/companion-preference.mjs +3 -10
- package/plugins/continuous-improvement/hooks/config-guard.mjs +94 -0
- package/plugins/continuous-improvement/hooks/gateguard.mjs +39 -39
- package/plugins/continuous-improvement/hooks/goal-drift-stop.mjs +2 -2
- package/plugins/continuous-improvement/hooks/hooks.json +10 -0
- package/plugins/continuous-improvement/hooks/query-cost-nudge.mjs +2 -2
- package/plugins/continuous-improvement/hooks/recall-briefing.mjs +2 -2
- package/plugins/continuous-improvement/hooks/route-prompt.mjs +2 -5
- package/plugins/continuous-improvement/hooks/session.mjs +2 -2
- package/plugins/continuous-improvement/hooks/workflow-distill.mjs +2 -2
- package/plugins/continuous-improvement/lib/config-guard-gate.mjs +243 -0
- package/plugins/continuous-improvement/lib/destructive-bash.mjs +216 -0
- package/plugins/continuous-improvement/lib/gateguard-state.mjs +5 -1
- package/plugins/continuous-improvement/lib/plugin-metadata.mjs +12 -1
- package/plugins/continuous-improvement/scripts/route-recommendation.routes.json +2 -2
- package/plugins/continuous-improvement/skills/README.md +0 -1
- package/plugins/continuous-improvement/skills/continuous-improvement/SKILL.md +4 -0
- package/plugins/continuous-improvement/skills/deploy-receipt/SKILL.md +1 -1
- package/plugins/continuous-improvement/skills/gateguard/SKILL.md +17 -2
- package/plugins/continuous-improvement/skills/proceed-with-the-recommendation/SKILL.md +2 -2
- package/plugins/continuous-improvement/skills/reconcile/SKILL.md +0 -1
- package/plugins/continuous-improvement/skills/superpowers/SKILL.md +5 -6
- package/plugins/expert.json +1 -1
- package/scripts/route-recommendation.routes.json +2 -2
- package/skills/README.md +1 -2
- package/skills/deploy-receipt.md +1 -1
- package/skills/gateguard.md +17 -2
- package/skills/proceed-with-the-recommendation.md +2 -2
- package/skills/reconcile.md +0 -1
- package/skills/superpowers.md +5 -6
- package/plugins/continuous-improvement/skills/safety-guard/SKILL.md +0 -77
- package/skills/safety-guard.md +0 -77
|
@@ -8,6 +8,13 @@
|
|
|
8
8
|
* (a) ships bundled at plugins/continuous-improvement/skills/<name>/SKILL.md, or
|
|
9
9
|
* (b) is declared in the root-level optional-companions.json file.
|
|
10
10
|
*
|
|
11
|
+
* And, for the prefixes we vendor (oh-my-claudecode, superpowers, agent-skills,
|
|
12
|
+
* ruflo-swarm), that every declared target actually EXISTS in its snapshot under
|
|
13
|
+
* third-party/<snapshot>/skills/<name>/. Declaring a target only asserted intent;
|
|
14
|
+
* nothing checked the destination. oh-my-claudecode:ultrawork was declared after
|
|
15
|
+
* upstream deleted it, and oh-my-claudecode:retrospective was declared for months
|
|
16
|
+
* having never existed upstream at all — both with this check green.
|
|
17
|
+
*
|
|
11
18
|
* Catches: a routing-table row that names a skill the bundle does not ship and
|
|
12
19
|
* the maintainer has not declared as an optional companion. Without this gate,
|
|
13
20
|
* such drift only surfaces at runtime when the orchestrator routes to a target
|
|
@@ -20,15 +27,60 @@
|
|
|
20
27
|
*
|
|
21
28
|
* Exit codes:
|
|
22
29
|
* 0 — every routing target is accounted for (bundled or optional-declared)
|
|
23
|
-
* 1 — at least one routing target is unaccounted for
|
|
30
|
+
* 1 — at least one routing target is unaccounted for, or a declared vendored
|
|
31
|
+
* target has no directory in its snapshot
|
|
24
32
|
*/
|
|
25
|
-
import { readdirSync, readFileSync, statSync } from "node:fs";
|
|
33
|
+
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
26
34
|
import { join } from "node:path";
|
|
27
35
|
import { argv, cwd, exit } from "node:process";
|
|
28
36
|
const ORCHESTRATOR_SKILL_PATH = "skills/proceed-with-the-recommendation.md";
|
|
29
37
|
const OPTIONAL_COMPANIONS_PATH = "optional-companions.json";
|
|
30
38
|
const PLUGIN_SKILLS_DIR = "plugins/continuous-improvement/skills";
|
|
31
39
|
const SECTION_HEADER = "### Routing Table (with Inline Fallbacks)";
|
|
40
|
+
/**
|
|
41
|
+
* Routing-target prefix -> vendored snapshot directory under third-party/.
|
|
42
|
+
*
|
|
43
|
+
* Declaring a target in optional-companions.json only asserted that we MEANT to
|
|
44
|
+
* route somewhere; nothing checked the destination existed. So when
|
|
45
|
+
* oh-my-claudecode 5.x deleted `ultrawork`, this check stayed green while the
|
|
46
|
+
* routing table named a skill upstream no longer ships (#308, fixed in #309) —
|
|
47
|
+
* and `oh-my-claudecode:retrospective` had been declared for months without ever
|
|
48
|
+
* existing upstream at all. For prefixes we vendor, the snapshot is ground truth.
|
|
49
|
+
*
|
|
50
|
+
* Prefixes NOT listed here (host built-ins, `frontend-design:`, `commit-commands:`)
|
|
51
|
+
* are deliberately unchecked: we have no local copy to check them against, and
|
|
52
|
+
* guessing would make this fail closed on things it cannot see.
|
|
53
|
+
*/
|
|
54
|
+
export const VENDORED_PREFIXES = {
|
|
55
|
+
"oh-my-claudecode": "oh-my-claudecode",
|
|
56
|
+
superpowers: "superpowers",
|
|
57
|
+
"agent-skills": "addy-agent-skills",
|
|
58
|
+
"ruflo-swarm": "ruflo-swarm",
|
|
59
|
+
};
|
|
60
|
+
/** Repo-relative skill path a vendored target must resolve to, or null if unvendored. */
|
|
61
|
+
export function resolveVendoredSkillPath(target) {
|
|
62
|
+
const idx = target.indexOf(":");
|
|
63
|
+
if (idx < 0)
|
|
64
|
+
return null;
|
|
65
|
+
const dir = VENDORED_PREFIXES[target.slice(0, idx)];
|
|
66
|
+
if (!dir)
|
|
67
|
+
return null;
|
|
68
|
+
// Everything after the FIRST colon is the skill name, colons included.
|
|
69
|
+
return `third-party/${dir}/skills/${target.slice(idx + 1)}`;
|
|
70
|
+
}
|
|
71
|
+
/** Vendored targets with no directory in their snapshot. */
|
|
72
|
+
export function findMissingVendoredTargets(repoRoot, targets) {
|
|
73
|
+
const missing = [];
|
|
74
|
+
for (const target of targets) {
|
|
75
|
+
const rel = resolveVendoredSkillPath(target);
|
|
76
|
+
if (rel === null)
|
|
77
|
+
continue;
|
|
78
|
+
const abs = join(repoRoot, rel);
|
|
79
|
+
if (!existsSync(abs))
|
|
80
|
+
missing.push({ target, expectedPath: abs });
|
|
81
|
+
}
|
|
82
|
+
return missing;
|
|
83
|
+
}
|
|
32
84
|
export function discoverBundledSkills(repoRoot) {
|
|
33
85
|
const dir = join(repoRoot, PLUGIN_SKILLS_DIR);
|
|
34
86
|
let entries;
|
|
@@ -119,19 +171,37 @@ export function checkRoutingTargets(repoRoot) {
|
|
|
119
171
|
continue;
|
|
120
172
|
drifts.push(t);
|
|
121
173
|
}
|
|
174
|
+
// Declared is not the same as existing: for prefixes we vendor, the snapshot
|
|
175
|
+
// is ground truth. Checks the declared companion set, so a target that is
|
|
176
|
+
// declared but never referenced is still caught.
|
|
177
|
+
const missingVendored = findMissingVendoredTargets(repoRoot, optional);
|
|
122
178
|
return {
|
|
123
179
|
targets,
|
|
124
180
|
drifts,
|
|
125
181
|
bundledCount: bundled.size,
|
|
126
182
|
optionalCount: optional.size,
|
|
183
|
+
missingVendored,
|
|
127
184
|
};
|
|
128
185
|
}
|
|
129
186
|
function main() {
|
|
130
187
|
const repoRoot = argv[2] ?? cwd();
|
|
131
|
-
const { targets, drifts, bundledCount, optionalCount } = checkRoutingTargets(repoRoot);
|
|
188
|
+
const { targets, drifts, bundledCount, optionalCount, missingVendored } = checkRoutingTargets(repoRoot);
|
|
189
|
+
if (missingVendored.length > 0) {
|
|
190
|
+
console.error(`FAIL routing-targets: ${missingVendored.length} declared target(s) do not exist in the vendored snapshot.\n`);
|
|
191
|
+
for (const m of missingVendored) {
|
|
192
|
+
console.error(` - "${m.target}"`);
|
|
193
|
+
console.error(` Declared in ${OPTIONAL_COMPANIONS_PATH}, but no directory at:`);
|
|
194
|
+
console.error(` ${m.expectedPath}`);
|
|
195
|
+
}
|
|
196
|
+
console.error(`\nEither upstream removed or renamed the skill (retarget the routing rows and this ` +
|
|
197
|
+
`declaration), or the snapshot is stale (refresh it with ` +
|
|
198
|
+
`'node bin/refresh-third-party.mjs <name>'). Declaring a target does not make it exist.`);
|
|
199
|
+
exit(1);
|
|
200
|
+
}
|
|
132
201
|
if (drifts.length === 0) {
|
|
133
202
|
console.log(`OK routing-targets: all ${targets.length} routing target(s) accounted for ` +
|
|
134
|
-
`(${bundledCount} bundled skill(s), ${optionalCount} optional companion(s) declared
|
|
203
|
+
`(${bundledCount} bundled skill(s), ${optionalCount} optional companion(s) declared, ` +
|
|
204
|
+
`every vendored target present in its snapshot).`);
|
|
135
205
|
exit(0);
|
|
136
206
|
}
|
|
137
207
|
console.error(`FAIL routing-targets: ${drifts.length} unaccounted target(s) in ${ORCHESTRATOR_SKILL_PATH}.\n`);
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Test Count Check
|
|
4
|
+
*
|
|
5
|
+
* The landing page states the size of the test suite in two places: the spec
|
|
6
|
+
* strip ("1,271 / Tests in the suite") and the verification-loop panel
|
|
7
|
+
* ("tests PASS 1271/1271"). Both are hand-maintained and both drifted
|
|
8
|
+
* twice in a single day on 2026-09-06 — 1,137 to 1,249 to 1,271 — because every
|
|
9
|
+
* PR that adds a test moves the real number and nothing guarded the claim. The
|
|
10
|
+
* skill and command counts are guarded (check-skill-count, check-command-count);
|
|
11
|
+
* this closes the same hole for tests.
|
|
12
|
+
*
|
|
13
|
+
* Unlike skills and commands, the real number cannot be derived from the
|
|
14
|
+
* filesystem: 45 test files generate cases in loops, so counting `it(` / `test(`
|
|
15
|
+
* declarations reports 1,066 against an actual 1,271. Node's runner has no
|
|
16
|
+
* collect-without-running mode either — filtering by name pattern reports the
|
|
17
|
+
* file count, not the test count. The only honest source is an actual run.
|
|
18
|
+
*
|
|
19
|
+
* So the check runs in two layers, and the docs are the source of truth:
|
|
20
|
+
*
|
|
21
|
+
* 1. Locally, inside `verify:all`, it asserts that every surface states the
|
|
22
|
+
* SAME number. Fast, no suite run, and it catches the two-surfaces-disagree
|
|
23
|
+
* case that a single-number edit produces.
|
|
24
|
+
* 2. In CI, after `npm test` has already run, `--actual-from <file>` parses
|
|
25
|
+
* the runner's "tests N" summary out of the captured output and asserts the
|
|
26
|
+
* claimed number equals reality. No second suite run.
|
|
27
|
+
*
|
|
28
|
+
* A claim that drifts from reality fails CI; two claims that drift apart fail
|
|
29
|
+
* locally. Fail-closed: a missing surface is a violation, not a silent pass.
|
|
30
|
+
*
|
|
31
|
+
* Usage:
|
|
32
|
+
* node bin/check-test-count.mjs # Claims agree with each other
|
|
33
|
+
* node bin/check-test-count.mjs --actual-from out.txt # ...and with an actual run
|
|
34
|
+
* node bin/check-test-count.mjs <repo-root> [--actual-from out.txt]
|
|
35
|
+
*
|
|
36
|
+
* Exit codes:
|
|
37
|
+
* 0 — every stated test count agrees (and matches the run, when supplied)
|
|
38
|
+
* 1 — a surface is missing, surfaces disagree, or a claim is stale
|
|
39
|
+
*/
|
|
40
|
+
import { readFileSync } from "node:fs";
|
|
41
|
+
import { join } from "node:path";
|
|
42
|
+
import { argv, cwd, exit } from "node:process";
|
|
43
|
+
const LANDING = join("docs", "landing", "index.html");
|
|
44
|
+
/** The spec strip cell: <span class="v">1,271</span><span class="k">Tests in the suite</span> */
|
|
45
|
+
const STRIP_RE = /<span\b[^>]*class=["'][^"']*\bv\b[^"']*["'][^>]*>\s*([\d,]+)\s*<\/span>\s*<span\b[^>]*class=["'][^"']*\bk\b[^"']*["'][^>]*>\s*Tests in the suite\s*<\/span>/gi;
|
|
46
|
+
/** The verification-loop panel line: tests PASS 1271/1271 */
|
|
47
|
+
const LADDER_RE = /^tests\s+PASS\s+([\d,]+)\/([\d,]+)\s*$/gim;
|
|
48
|
+
/** The node test runner summary: "ℹ tests 1271" */
|
|
49
|
+
const ACTUAL_RE = /^\W*tests\s+(\d+)\s*$/im;
|
|
50
|
+
const toNumber = (raw) => Number(raw.replace(/,/g, ""));
|
|
51
|
+
export function extractClaims(html) {
|
|
52
|
+
const strip = [...html.matchAll(STRIP_RE)].map((m) => toNumber(m[1]));
|
|
53
|
+
const ladder = [...html.matchAll(LADDER_RE)].flatMap((m) => [toNumber(m[1]), toNumber(m[2])]);
|
|
54
|
+
return { strip, ladder };
|
|
55
|
+
}
|
|
56
|
+
export function parseActualCount(runOutput) {
|
|
57
|
+
const m = ACTUAL_RE.exec(runOutput);
|
|
58
|
+
return m ? Number(m[1]) : null;
|
|
59
|
+
}
|
|
60
|
+
export function findViolations(claims, actual) {
|
|
61
|
+
const violations = [];
|
|
62
|
+
if (claims.strip.length === 0) {
|
|
63
|
+
violations.push(`the spec strip states no test count (expected a <span class="v">N</span> cell labelled "Tests in the suite").`);
|
|
64
|
+
}
|
|
65
|
+
if (claims.ladder.length === 0) {
|
|
66
|
+
violations.push(`the verification-loop ladder states no test count (expected a "tests PASS N/N" line).`);
|
|
67
|
+
}
|
|
68
|
+
if (violations.length > 0)
|
|
69
|
+
return violations;
|
|
70
|
+
const stated = [...claims.strip, ...claims.ladder];
|
|
71
|
+
const distinct = [...new Set(stated)].sort((a, b) => a - b);
|
|
72
|
+
if (distinct.length > 1) {
|
|
73
|
+
// Surfaces disagree with each other. Comparing a contested number to the run
|
|
74
|
+
// would emit a second, derivative violation; fix the disagreement first.
|
|
75
|
+
return [`the stated test counts disagree with each other: ${distinct.join(" vs ")}.`];
|
|
76
|
+
}
|
|
77
|
+
const claimed = distinct[0];
|
|
78
|
+
if (actual !== null && actual !== claimed) {
|
|
79
|
+
violations.push(`every surface states ${claimed} tests but the suite actually ran ${actual}.`);
|
|
80
|
+
}
|
|
81
|
+
return violations;
|
|
82
|
+
}
|
|
83
|
+
function main() {
|
|
84
|
+
const args = argv.slice(2);
|
|
85
|
+
const actualIdx = args.indexOf("--actual-from");
|
|
86
|
+
const actualPath = actualIdx === -1 ? null : args[actualIdx + 1];
|
|
87
|
+
if (actualIdx !== -1 && !actualPath) {
|
|
88
|
+
console.error("FAIL test-count: --actual-from needs a path to a captured test run.");
|
|
89
|
+
exit(1);
|
|
90
|
+
}
|
|
91
|
+
const repoRoot = args.find((a) => !a.startsWith("--") && a !== actualPath) ?? cwd();
|
|
92
|
+
let html;
|
|
93
|
+
try {
|
|
94
|
+
html = readFileSync(join(repoRoot, LANDING), "utf8");
|
|
95
|
+
}
|
|
96
|
+
catch {
|
|
97
|
+
console.error(`FAIL test-count: cannot read ${LANDING} at ${repoRoot}.`);
|
|
98
|
+
exit(1);
|
|
99
|
+
}
|
|
100
|
+
let actual = null;
|
|
101
|
+
if (actualPath) {
|
|
102
|
+
let runOutput;
|
|
103
|
+
try {
|
|
104
|
+
runOutput = readFileSync(actualPath, "utf8");
|
|
105
|
+
}
|
|
106
|
+
catch {
|
|
107
|
+
console.error(`FAIL test-count: cannot read the captured test run at ${actualPath}.`);
|
|
108
|
+
exit(1);
|
|
109
|
+
}
|
|
110
|
+
actual = parseActualCount(runOutput);
|
|
111
|
+
if (actual === null) {
|
|
112
|
+
console.error(`FAIL test-count: ${actualPath} has no "tests N" summary line, so the claim cannot be checked against a real run.`);
|
|
113
|
+
exit(1);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
const claims = extractClaims(html);
|
|
117
|
+
const violations = findViolations(claims, actual);
|
|
118
|
+
if (violations.length === 0) {
|
|
119
|
+
const claimed = [...claims.strip, ...claims.ladder][0];
|
|
120
|
+
const against = actual === null ? "each other" : `each other and the actual run`;
|
|
121
|
+
console.log(`OK test-count: all ${claims.strip.length + claims.ladder.length} claim(s) state ${claimed} tests, matching ${against}.`);
|
|
122
|
+
exit(0);
|
|
123
|
+
}
|
|
124
|
+
console.error(`FAIL test-count: ${violations.length} issue(s) with the stated test count.`);
|
|
125
|
+
console.error("");
|
|
126
|
+
for (const v of violations)
|
|
127
|
+
console.error(` ${v}`);
|
|
128
|
+
console.error("");
|
|
129
|
+
console.error(`Fix: run the suite, take the "tests N" total, and update both surfaces in ${LANDING} (the spec strip and the verification-loop panel).`);
|
|
130
|
+
exit(1);
|
|
131
|
+
}
|
|
132
|
+
const invokedDirectly = argv[1] !== undefined && import.meta.url.endsWith(argv[1].replace(/\\/g, "/"));
|
|
133
|
+
if (invokedDirectly || argv[1]?.endsWith("check-test-count.mjs")) {
|
|
134
|
+
main();
|
|
135
|
+
}
|
|
@@ -19,9 +19,9 @@
|
|
|
19
19
|
* 1 — argument parse error or unexpected runtime failure
|
|
20
20
|
*/
|
|
21
21
|
import { execFileSync } from "node:child_process";
|
|
22
|
-
import { createHash } from "node:crypto";
|
|
23
22
|
import { existsSync, readFileSync } from "node:fs";
|
|
24
23
|
import { homedir } from "node:os";
|
|
24
|
+
import { hashProjectRoot } from "../lib/gateguard-state.mjs";
|
|
25
25
|
import { join } from "node:path";
|
|
26
26
|
import { argv, exit, stdout } from "node:process";
|
|
27
27
|
const ACTIONS = [
|
|
@@ -99,10 +99,7 @@ function resolveProjectRoot() {
|
|
|
99
99
|
}
|
|
100
100
|
function defaultJsonlPath() {
|
|
101
101
|
const home = process.env.HOME ?? process.env.USERPROFILE ?? homedir();
|
|
102
|
-
const hash =
|
|
103
|
-
.update(resolveProjectRoot())
|
|
104
|
-
.digest("hex")
|
|
105
|
-
.slice(0, 12);
|
|
102
|
+
const hash = hashProjectRoot(resolveProjectRoot());
|
|
106
103
|
return join(home, ".claude", "instincts", hash, "companion-preference.jsonl");
|
|
107
104
|
}
|
|
108
105
|
function parseJsonl(raw) {
|
|
@@ -4,6 +4,7 @@ import { basename, dirname, join } from "node:path";
|
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
5
|
import { PACKAGE_NAME, PLUGIN_MODES, getPluginHooksConfig, getClaudePluginManifest, getClaudePluginMarketplaceManifest, getClaudeRepoMarketplaceManifest, getPluginManifest, } from "../lib/plugin-metadata.mjs";
|
|
6
6
|
import { normalizeTier, parseSkillFrontmatter, renderBundledSkillsReadme, } from "../lib/skill-tiers.mjs";
|
|
7
|
+
import { enforcementOf, lawOf, renderSkillCatalogHtml, } from "../lib/skill-catalog.mjs";
|
|
7
8
|
const REPO_ROOT = join(dirname(fileURLToPath(import.meta.url)), "..");
|
|
8
9
|
const PLUGINS_DIR = join(REPO_ROOT, "plugins");
|
|
9
10
|
const PLUGIN_BUNDLE_DIR = join(PLUGINS_DIR, PACKAGE_NAME);
|
|
@@ -67,6 +68,23 @@ async function writeBundledSkills() {
|
|
|
67
68
|
});
|
|
68
69
|
}
|
|
69
70
|
await writeFile(join(targetSkillsDir, "README.md"), renderBundledSkillsReadme(skillsForReadme));
|
|
71
|
+
await writeSkillCatalogHtml(skillsForReadme);
|
|
72
|
+
}
|
|
73
|
+
/**
|
|
74
|
+
* Generate `docs/skill-catalog.html` from the same frontmatter that feeds the
|
|
75
|
+
* bundled README, so the catalog's Enforcement badges can never drift from the
|
|
76
|
+
* shipped file set. Guarded by `verify:generated`.
|
|
77
|
+
*/
|
|
78
|
+
async function writeSkillCatalogHtml(skills) {
|
|
79
|
+
const commandNames = new Set((await readdir(join(REPO_ROOT, "commands")))
|
|
80
|
+
.filter((f) => f.endsWith(".md") && f !== "README.md")
|
|
81
|
+
.map((f) => basename(f, ".md")));
|
|
82
|
+
const catalog = skills.map((skill) => ({
|
|
83
|
+
...skill,
|
|
84
|
+
...enforcementOf(skill.name, commandNames),
|
|
85
|
+
law: lawOf(skill.description),
|
|
86
|
+
}));
|
|
87
|
+
await writeFile(join(REPO_ROOT, "docs", "skill-catalog.html"), renderSkillCatalogHtml(catalog));
|
|
70
88
|
}
|
|
71
89
|
async function writePluginBundleReadme() {
|
|
72
90
|
const readme = [
|
|
@@ -108,8 +126,7 @@ async function writePluginBundleReadme() {
|
|
|
108
126
|
"- `tdd-workflow` — RED/GREEN/REFACTOR + 80% coverage gate",
|
|
109
127
|
"- `workspace-surface-audit` — environment + capability audit",
|
|
110
128
|
"- Tier-1/Tier-2 enforcement skills (`gateguard`, `verification-loop`,",
|
|
111
|
-
" `
|
|
112
|
-
" `strategic-compact`, `wild-risa-balance`)",
|
|
129
|
+
" `token-budget-advisor`, `strategic-compact`, `wild-risa-balance`)",
|
|
113
130
|
"",
|
|
114
131
|
"**Optional companions the orchestrator routes to (install separately if",
|
|
115
132
|
"you want the dedicated skill instead of the inline fallback):**",
|
|
@@ -151,12 +168,14 @@ async function writePluginBundle() {
|
|
|
151
168
|
copyFileTo(join(REPO_ROOT, "lib", "git-state.mjs"), join(PLUGIN_BUNDLE_DIR, "lib", "git-state.mjs")),
|
|
152
169
|
copyFileTo(join(REPO_ROOT, "bin", "gateguard-clear.mjs"), join(PLUGIN_BUNDLE_DIR, "bin", "gateguard-clear.mjs")),
|
|
153
170
|
copyFileTo(join(REPO_ROOT, "lib", "gateguard-state.mjs"), join(PLUGIN_BUNDLE_DIR, "lib", "gateguard-state.mjs")),
|
|
171
|
+
copyFileTo(join(REPO_ROOT, "lib", "destructive-bash.mjs"), join(PLUGIN_BUNDLE_DIR, "lib", "destructive-bash.mjs")),
|
|
154
172
|
copyFileTo(join(REPO_ROOT, "lib", "plugin-metadata.mjs"), join(PLUGIN_BUNDLE_DIR, "lib", "plugin-metadata.mjs")),
|
|
155
173
|
copyFileTo(join(REPO_ROOT, "lib", "resolve-home-dir.mjs"), join(PLUGIN_BUNDLE_DIR, "lib", "resolve-home-dir.mjs")),
|
|
156
174
|
copyFileTo(join(REPO_ROOT, "lib", "observe-event.mjs"), join(PLUGIN_BUNDLE_DIR, "lib", "observe-event.mjs")),
|
|
157
175
|
copyFileTo(join(REPO_ROOT, "lib", "goal-state.mjs"), join(PLUGIN_BUNDLE_DIR, "lib", "goal-state.mjs")),
|
|
158
176
|
copyFileTo(join(REPO_ROOT, "lib", "goal-drift-gate.mjs"), join(PLUGIN_BUNDLE_DIR, "lib", "goal-drift-gate.mjs")),
|
|
159
177
|
copyFileTo(join(REPO_ROOT, "lib", "hook-pack-gate.mjs"), join(PLUGIN_BUNDLE_DIR, "lib", "hook-pack-gate.mjs")),
|
|
178
|
+
copyFileTo(join(REPO_ROOT, "lib", "config-guard-gate.mjs"), join(PLUGIN_BUNDLE_DIR, "lib", "config-guard-gate.mjs")),
|
|
160
179
|
copyFileTo(join(REPO_ROOT, "lib", "recall-index.mjs"), join(PLUGIN_BUNDLE_DIR, "lib", "recall-index.mjs")),
|
|
161
180
|
copyFileTo(join(REPO_ROOT, "lib", "recall-briefing.mjs"), join(PLUGIN_BUNDLE_DIR, "lib", "recall-briefing.mjs")),
|
|
162
181
|
copyFileTo(join(REPO_ROOT, "lib", "skill-distill.mjs"), join(PLUGIN_BUNDLE_DIR, "lib", "skill-distill.mjs")),
|
package/bin/harvest-friction.mjs
CHANGED
|
@@ -33,7 +33,8 @@ import { createHash } from "node:crypto";
|
|
|
33
33
|
import { appendFileSync, existsSync, readFileSync } from "node:fs";
|
|
34
34
|
import { homedir } from "node:os";
|
|
35
35
|
import { join } from "node:path";
|
|
36
|
-
import { argv,
|
|
36
|
+
import { argv, exit } from "node:process";
|
|
37
|
+
import { hashProjectRoot, resolveProjectRoot } from "../lib/gateguard-state.mjs";
|
|
37
38
|
const SUMMARY_TRUNCATE = 120;
|
|
38
39
|
const DEDUP_KEY_INPUT_TRUNCATE = 120;
|
|
39
40
|
/**
|
|
@@ -74,10 +75,13 @@ export function classifyObservation(row) {
|
|
|
74
75
|
return build("env_issue", tool, summary, row.ts);
|
|
75
76
|
}
|
|
76
77
|
// permission_block — harness, sandbox, or OS denied the action.
|
|
78
|
+
// Do not match the substring "sandbox" alone: a plan file named
|
|
79
|
+
// slow-regime-gate-sandbox.md is not a denial (2026-08-22 false positive).
|
|
77
80
|
if (/permission denied/i.test(out) ||
|
|
78
81
|
/harness[- ]blocked/i.test(out) ||
|
|
79
|
-
/
|
|
80
|
-
|
|
82
|
+
/operation not permitted/i.test(out) ||
|
|
83
|
+
/\bblocked by (?:the )?sandbox\b/i.test(out) ||
|
|
84
|
+
/\bsandbox(?:ed)?[- ](?:denied|blocked|restrict(?:ed|ion)|violation)\b/i.test(out)) {
|
|
81
85
|
return build("permission_block", tool, summary, row.ts);
|
|
82
86
|
}
|
|
83
87
|
// wrong_approach — agent acted on stale state. The "file changed since last
|
|
@@ -219,11 +223,9 @@ export function harvest(observationsPath, instinctsPath) {
|
|
|
219
223
|
};
|
|
220
224
|
}
|
|
221
225
|
function defaultProjectHash() {
|
|
222
|
-
//
|
|
223
|
-
//
|
|
224
|
-
|
|
225
|
-
const root = cwd();
|
|
226
|
-
return createHash("sha256").update(root).digest("hex").slice(0, 12);
|
|
226
|
+
// Same root + canonical hash as observe/gateguard so `harvest-friction` from
|
|
227
|
+
// this repo reads the bucket the hook writes (C:/ vs c:/ vs cwd backslashes).
|
|
228
|
+
return hashProjectRoot(resolveProjectRoot());
|
|
227
229
|
}
|
|
228
230
|
function main() {
|
|
229
231
|
const args = argv.slice(2);
|
package/bin/install.mjs
CHANGED
|
@@ -10,12 +10,11 @@
|
|
|
10
10
|
* npx continuous-improvement install --uninstall # remove everything
|
|
11
11
|
*/
|
|
12
12
|
import { chmodSync, copyFileSync, existsSync, lstatSync, mkdtempSync, mkdirSync, readFileSync, realpathSync, readdirSync, renameSync, rmSync, statSync, writeFileSync, } from "node:fs";
|
|
13
|
-
import { execSync } from "node:child_process";
|
|
14
13
|
import { homedir } from "node:os";
|
|
15
14
|
import { dirname, isAbsolute, join, relative, sep } from "node:path";
|
|
16
15
|
import { fileURLToPath } from "node:url";
|
|
17
|
-
import { createHash } from "node:crypto";
|
|
18
16
|
import { PACKAGE_NAME, VERSION, getToolNames } from "../lib/plugin-metadata.mjs";
|
|
17
|
+
import { hashProjectRoot, resolveProjectRoot } from "../lib/gateguard-state.mjs";
|
|
19
18
|
import { TARGET_IDS, planTargetWrites, resolveTargets } from "../lib/install-targets.mjs";
|
|
20
19
|
import { evaluateUpdateCheck, fetchLatestNpmVersion, isThrottled, pendingNotice, } from "../lib/version-check.mjs";
|
|
21
20
|
const __filename = fileURLToPath(import.meta.url);
|
|
@@ -674,19 +673,10 @@ Examples:
|
|
|
674
673
|
`);
|
|
675
674
|
}
|
|
676
675
|
function getProjectHashSync() {
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
// file literally named /dev/null); ignore stderr via stdio instead.
|
|
680
|
-
const root = execSync("git rev-parse --show-toplevel", {
|
|
681
|
-
encoding: "utf8",
|
|
682
|
-
stdio: ["ignore", "pipe", "ignore"],
|
|
683
|
-
}).trim();
|
|
684
|
-
const hash = createHash("sha256").update(root).digest("hex").slice(0, 12);
|
|
685
|
-
return { root, hash };
|
|
686
|
-
}
|
|
687
|
-
catch {
|
|
676
|
+
const root = resolveProjectRoot();
|
|
677
|
+
if (root === "global")
|
|
688
678
|
return { root: "global", hash: "global" };
|
|
689
|
-
}
|
|
679
|
+
return { root, hash: hashProjectRoot(root) };
|
|
690
680
|
}
|
|
691
681
|
const args = process.argv.slice(2);
|
|
692
682
|
const command = args[0];
|
package/bin/mcp-server.mjs
CHANGED
|
@@ -16,12 +16,11 @@ import { homedir } from "node:os";
|
|
|
16
16
|
import { basename, dirname, join, resolve } from "node:path";
|
|
17
17
|
import { createInterface } from "node:readline";
|
|
18
18
|
import { fileURLToPath } from "node:url";
|
|
19
|
-
import { createHash } from "node:crypto";
|
|
20
19
|
import { PACKAGE_NAME, VERSION, getToolDefinitions, isPluginMode, } from "../lib/plugin-metadata.mjs";
|
|
21
20
|
import { formatDriftReport, parseGoalFromPlan, scoreObservations, } from "../lib/goal-state.mjs";
|
|
22
21
|
import { buildIndex, formatRecallHits, parseSince, query as queryRecall, } from "../lib/recall-index.mjs";
|
|
23
22
|
import { draftFromCandidate, draftFromWorkflowRun, extractTrajectories, findCandidates, formatCandidates, serializeDraft, workflowRunFromObservations, } from "../lib/skill-distill.mjs";
|
|
24
|
-
import { MAX_CLEARED_FILES, canonicalizeFileKey, clearFiles, resolveInstinctsRoot, resolveSessionDir, } from "../lib/gateguard-state.mjs";
|
|
23
|
+
import { MAX_CLEARED_FILES, canonicalizeFileKey, clearFiles, hashProjectRoot, resolveInstinctsRoot, resolveProjectRoot, resolveSessionDir, } from "../lib/gateguard-state.mjs";
|
|
25
24
|
function getHomeDir() {
|
|
26
25
|
return process.env.HOME || process.env.USERPROFILE || homedir();
|
|
27
26
|
}
|
|
@@ -93,14 +92,10 @@ const modeIndex = args.indexOf("--mode");
|
|
|
93
92
|
const requestedMode = args[modeIndex + 1];
|
|
94
93
|
const MODE = isPluginMode(requestedMode) ? requestedMode : "beginner";
|
|
95
94
|
function getProjectHash() {
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
const hash = createHash("sha256").update(root).digest("hex").slice(0, 12);
|
|
99
|
-
return { root, hash, name: basename(root) };
|
|
100
|
-
}
|
|
101
|
-
catch {
|
|
95
|
+
const root = resolveProjectRoot();
|
|
96
|
+
if (root === "global")
|
|
102
97
|
return { root: "global", hash: "global", name: "global" };
|
|
103
|
-
}
|
|
98
|
+
return { root, hash: hashProjectRoot(root), name: basename(root) };
|
|
104
99
|
}
|
|
105
100
|
function readInstincts(projectHash) {
|
|
106
101
|
const instincts = [];
|
package/bin/observe.mjs
CHANGED
|
@@ -16,10 +16,10 @@
|
|
|
16
16
|
//
|
|
17
17
|
// See docs/plans/2026-05-05-node-observer-rich-schema.md for the full design.
|
|
18
18
|
import { execFileSync } from "node:child_process";
|
|
19
|
-
import { createHash } from "node:crypto";
|
|
20
19
|
import { appendFileSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, statSync, unlinkSync, writeFileSync } from "node:fs";
|
|
21
20
|
import { homedir } from "node:os";
|
|
22
21
|
import { basename, join } from "node:path";
|
|
22
|
+
import { canonicalizeProjectRoot, hashProjectRoot } from "../lib/gateguard-state.mjs";
|
|
23
23
|
import { parseHookPayload, summariseInput, summariseOutput } from "../lib/observe-event.mjs";
|
|
24
24
|
const ROTATION_LINE_THRESHOLD = 10_000;
|
|
25
25
|
const ARCHIVE_RETENTION = 10;
|
|
@@ -40,8 +40,8 @@ function runObserver() {
|
|
|
40
40
|
const payload = parseHookPayload(raw);
|
|
41
41
|
if (!payload)
|
|
42
42
|
return;
|
|
43
|
-
const projectRoot = resolveProjectRoot();
|
|
44
|
-
const projectHash =
|
|
43
|
+
const projectRoot = canonicalizeProjectRoot(resolveProjectRoot());
|
|
44
|
+
const projectHash = hashProjectRoot(projectRoot);
|
|
45
45
|
const projectName = basename(projectRoot.replace(/\.git$/, ""));
|
|
46
46
|
const instinctsDir = join(getHomeDir(), ".claude", "instincts");
|
|
47
47
|
const projectDir = join(instinctsDir, projectHash);
|