continuous-improvement 3.20.0 → 3.20.4
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 +1 -1
- package/QUICKSTART.md +1 -1
- package/README.md +2 -2
- package/bin/check-landing-version.mjs +63 -0
- package/bin/check-scripts-citation-drift.mjs +61 -13
- package/bin/generate-plugin-manifests.mjs +2 -0
- package/bin/install.mjs +19 -11
- package/commands/verify-install.md +1 -1
- package/hooks/gateguard.mjs +22 -3
- package/hooks/query-cost-nudge.mjs +1 -0
- package/hooks/typecheck-stop.mjs +2 -1
- package/package.json +5 -3
- package/plugins/beginner.json +1 -1
- package/plugins/continuous-improvement/.claude-plugin/marketplace.json +1 -1
- package/plugins/continuous-improvement/.claude-plugin/plugin.json +1 -1
- package/plugins/continuous-improvement/README.md +1 -0
- package/plugins/continuous-improvement/commands/verify-install.md +1 -1
- package/plugins/continuous-improvement/hooks/gateguard.mjs +22 -3
- package/plugins/continuous-improvement/hooks/query-cost-nudge.mjs +1 -0
- package/plugins/continuous-improvement/hooks/typecheck-stop.mjs +2 -1
- package/plugins/continuous-improvement/scripts/README.md +33 -0
- package/plugins/continuous-improvement/scripts/detect-deploy-target.sh +66 -0
- package/plugins/continuous-improvement/scripts/get-deployed-sha.sh +113 -0
- package/plugins/continuous-improvement/scripts/git-state-snapshot.sh +48 -0
- package/plugins/continuous-improvement/scripts/resolve-verify-ladder.mjs +241 -0
- package/plugins/continuous-improvement/scripts/route-recommendation.mjs +178 -0
- package/plugins/continuous-improvement/scripts/route-recommendation.routes.json +213 -0
- package/plugins/continuous-improvement/scripts/run-synthetic.mjs +298 -0
- package/plugins/continuous-improvement/scripts/scan-past-mistakes.mjs +285 -0
- package/plugins/continuous-improvement/skills/deploy-receipt/SKILL.md +2 -2
- package/plugins/continuous-improvement/skills/gateguard/SKILL.md +2 -2
- package/plugins/continuous-improvement/skills/proceed-with-the-recommendation/SKILL.md +2 -2
- package/plugins/continuous-improvement/skills/reconcile/SKILL.md +1 -1
- package/plugins/continuous-improvement/skills/verification-loop/SKILL.md +5 -5
- package/plugins/continuous-improvement/skills/workspace-surface-audit/SKILL.md +1 -1
- package/plugins/continuous-improvement/skills/worktree-safety/SKILL.md +1 -1
- package/plugins/expert.json +1 -1
- package/scripts/README.md +33 -0
- package/scripts/detect-deploy-target.sh +66 -0
- package/scripts/get-deployed-sha.sh +113 -0
- package/scripts/git-state-snapshot.sh +48 -0
- package/scripts/resolve-verify-ladder.mjs +241 -0
- package/scripts/route-recommendation.mjs +178 -0
- package/scripts/route-recommendation.routes.json +213 -0
- package/scripts/run-synthetic.mjs +298 -0
- package/scripts/scan-past-mistakes.mjs +285 -0
- package/skills/deploy-receipt.md +2 -2
- package/skills/gateguard.md +2 -2
- package/skills/proceed-with-the-recommendation.md +2 -2
- package/skills/reconcile.md +1 -1
- package/skills/verification-loop.md +5 -5
- package/skills/workspace-surface-audit.md +1 -1
- package/skills/worktree-safety.md +1 -1
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
{
|
|
9
9
|
"name": "continuous-improvement",
|
|
10
10
|
"description": "The persistent-memory and runtime-discipline layer for Claude Code. It remembers the corrections you already gave, grounds every edit in real facts before it lands, and — through the Mulahazah engine — turns each fix into a reusable instinct, so a lesson learned once is applied automatically next time with no re-teaching. Built on the 7 Laws of AI Agent Discipline (research, plan, verify, reflect, learn) and shipped as 27 bundled skills, instinct-aware hooks, an MCP toolset for recall and reflection, and a GitHub Action transcript linter that feeds real work history back into sharper instincts.",
|
|
11
|
-
"version": "3.20.
|
|
11
|
+
"version": "3.20.4",
|
|
12
12
|
"source": "./plugins/continuous-improvement",
|
|
13
13
|
"author": {
|
|
14
14
|
"name": "naimkatiman"
|
package/QUICKSTART.md
CHANGED
|
@@ -139,7 +139,7 @@ npx continuous-improvement install --mode expert
|
|
|
139
139
|
npx continuous-improvement install --pack react # optional: react | python | go | meta
|
|
140
140
|
```
|
|
141
141
|
|
|
142
|
-
Preconditions: Node 18 / 20 / 22, plus
|
|
142
|
+
Preconditions: Node 18 / 20 / 22, plus Git Bash on Windows (`hooks/observe.sh` is a Bash script). WSL Bash launched from native Windows cannot resolve the `C:/...` hook paths written by the installer. See [README.md § Expert](README.md#expert--adds-mcp-server-observation-hooks-and-instinct-packs) for the full preconditions and troubleshooting matrix.
|
|
143
143
|
|
|
144
144
|
Verify with `/dashboard` — you should see instinct health and observation count.
|
|
145
145
|
|
package/README.md
CHANGED
|
@@ -137,7 +137,7 @@ V1 honest limitations: the runtime gate is honor-system once the agent flips `_g
|
|
|
137
137
|
|
|
138
138
|
Pick this if you want the MCP tools (19 of them, including `ci_plan_init` / `ci_plan_status` for `task_plan.md`-style planning), the session hooks that feed Mulahazah, and starter packs.
|
|
139
139
|
|
|
140
|
-
Preconditions: Node 18 / 20 / 22, plus
|
|
140
|
+
Preconditions: Node 18 / 20 / 22, plus Git Bash on Windows (`hooks/observe.sh` is a Bash script). WSL Bash launched from native Windows cannot resolve the `C:/...` hook paths written by the installer. **`jq` is no longer required**: as of v3.6.0, `observe.sh` prefers the Node observer (`bin/observe.mjs`) which writes the rich event schema natively without external dependencies. The bash thin-schema path is kept as a two-phase shim, so legacy installs that have not re-run `npx continuous-improvement install` since v3.5.x will still degrade silently without `jq` (`winget install jqlang.jq` on Windows, `brew install jq` on macOS, `apt install jq` on Debian/Ubuntu) — re-running the installer is the cleaner fix and removes the dependency entirely. See [CHANGELOG.md](CHANGELOG.md) `[3.6.0]` for the migration details.
|
|
141
141
|
|
|
142
142
|
```bash
|
|
143
143
|
npx continuous-improvement install --mode expert
|
|
@@ -158,7 +158,7 @@ Three failures account for nearly every install support thread. Try them in orde
|
|
|
158
158
|
| Symptom | Real cause | Fix |
|
|
159
159
|
|---|---|---|
|
|
160
160
|
| `/discipline` says "command not recognized" right after `/plugin install` | Slash commands load on session start; the marketplace did pick the plugin up | Quit and reopen Claude Code, then run `/discipline` again |
|
|
161
|
-
| Expert mode hooks never fire on Windows |
|
|
161
|
+
| Expert mode hooks never fire on Windows | WSL Bash cannot resolve the native `C:/...` hook paths | Install Git Bash, ensure its `bin` directory precedes `C:\Windows\System32` on PATH, then re-run `npx continuous-improvement install --mode expert` |
|
|
162
162
|
| `/plugin marketplace add ...` returned nothing visible | Marketplace add was silent; the plugin is not yet selected | Run `/plugin install continuous-improvement@continuous-improvement` to select and activate it |
|
|
163
163
|
|
|
164
164
|
If none of those apply, paste the output of `npx continuous-improvement install` into a GitHub issue — that surface logs every step.
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { resolve } from "node:path";
|
|
4
|
+
import { argv, cwd } from "node:process";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
const VERSION_RE = "([0-9]+\\.[0-9]+\\.[0-9]+)";
|
|
7
|
+
function firstMatch(content, pattern) {
|
|
8
|
+
return pattern.exec(content)?.[1] ?? null;
|
|
9
|
+
}
|
|
10
|
+
export function checkLandingVersion(repoRoot) {
|
|
11
|
+
const packagePath = resolve(repoRoot, "package.json");
|
|
12
|
+
const landingPath = resolve(repoRoot, "docs", "landing", "index.html");
|
|
13
|
+
const packageJson = JSON.parse(readFileSync(packagePath, "utf8"));
|
|
14
|
+
const packageVersion = typeof packageJson.version === "string" ? packageJson.version : "";
|
|
15
|
+
const landing = readFileSync(landingPath, "utf8");
|
|
16
|
+
const markers = {
|
|
17
|
+
badge: firstMatch(landing, new RegExp(`<span\\b[^>]*class=["'][^"']*\\bver\\b[^"']*["'][^>]*>\\s*v${VERSION_RE}\\s*</span>`, "i")),
|
|
18
|
+
hero: firstMatch(landing, new RegExp(`<span\\b[^>]*class=["'][^"']*\\bkicker\\b[^"']*["'][^>]*>[\\s\\S]*?\\bREV\\s+${VERSION_RE}[\\s\\S]*?</span>`, "i")),
|
|
19
|
+
current: firstMatch(landing, new RegExp(`<span\\b[^>]*class=["'][^"']*\\bv\\b[^"']*["'][^>]*>\\s*v${VERSION_RE}\\s*</span>\\s*<span\\b[^>]*class=["'][^"']*\\bk\\b[^"']*["'][^>]*>\\s*Current rev\\s*</span>`, "i")),
|
|
20
|
+
footer: firstMatch(landing, new RegExp(`<span\\b[^>]*class=["'][^"']*\\bfoot-doc\\b[^"']*["'][^>]*>[\\s\\S]*?\\bREV\\s+${VERSION_RE}[\\s\\S]*?</span>`, "i")),
|
|
21
|
+
};
|
|
22
|
+
const errors = [];
|
|
23
|
+
if (!/^[0-9]+\.[0-9]+\.[0-9]+$/.test(packageVersion)) {
|
|
24
|
+
errors.push(`package.json has an invalid version: ${packageVersion || "<missing>"}`);
|
|
25
|
+
}
|
|
26
|
+
const missing = Object.entries(markers)
|
|
27
|
+
.filter(([, version]) => version === null)
|
|
28
|
+
.map(([name]) => name);
|
|
29
|
+
if (missing.length > 0) {
|
|
30
|
+
errors.push(`landing release marker(s) missing: ${missing.join(", ")}`);
|
|
31
|
+
}
|
|
32
|
+
const landingVersions = [...new Set(Object.values(markers).filter((value) => value !== null))];
|
|
33
|
+
if (landingVersions.length > 1) {
|
|
34
|
+
errors.push(`landing release markers are mixed: ${landingVersions.join(", ")}`);
|
|
35
|
+
}
|
|
36
|
+
for (const [name, version] of Object.entries(markers)) {
|
|
37
|
+
if (version !== null && version !== packageVersion) {
|
|
38
|
+
errors.push(`${name} marker is ${version}; package.json is ${packageVersion}`);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
return { ok: errors.length === 0, packageVersion, markers, errors };
|
|
42
|
+
}
|
|
43
|
+
function main() {
|
|
44
|
+
const repoRoot = argv[2] ?? cwd();
|
|
45
|
+
try {
|
|
46
|
+
const result = checkLandingVersion(repoRoot);
|
|
47
|
+
if (result.ok) {
|
|
48
|
+
console.log(`OK landing-version: all 4 markers match package.json ${result.packageVersion}.`);
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
console.error(`FAIL landing-version: ${result.errors.join("; ")}`);
|
|
52
|
+
process.exitCode = 1;
|
|
53
|
+
}
|
|
54
|
+
catch (error) {
|
|
55
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
56
|
+
console.error(`FAIL landing-version: ${message}`);
|
|
57
|
+
process.exitCode = 1;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
const entryPath = argv[1] ? resolve(argv[1]) : "";
|
|
61
|
+
if (entryPath === fileURLToPath(import.meta.url)) {
|
|
62
|
+
main();
|
|
63
|
+
}
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
* Three sides of the contract are enforced as one invariant:
|
|
12
12
|
*
|
|
13
13
|
* Side A — every file in scripts/ (excluding README.md) must appear in the
|
|
14
|
-
* Inventory table
|
|
14
|
+
* Inventory table, and every inventoried helper must exist on disk.
|
|
15
15
|
* Side B — for every Inventory row, every `skills/<name>.md` token in the
|
|
16
16
|
* Cited by cell must reference a file whose body contains the
|
|
17
17
|
* literal substring `scripts/<script-filename>` for at least one
|
|
@@ -85,30 +85,67 @@ function listSkillFiles(repoRoot) {
|
|
|
85
85
|
const dir = join(repoRoot, SKILLS_DIR);
|
|
86
86
|
if (!existsSync(dir) || !statSync(dir).isDirectory())
|
|
87
87
|
return [];
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
.
|
|
91
|
-
|
|
88
|
+
const skills = [];
|
|
89
|
+
for (const entry of readdirSync(dir, { withFileTypes: true })) {
|
|
90
|
+
if (entry.isFile() && entry.name.endsWith(".md")) {
|
|
91
|
+
skills.push(`${SKILLS_DIR}/${entry.name}`);
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (entry.isDirectory() &&
|
|
95
|
+
existsSync(join(dir, entry.name, "SKILL.md"))) {
|
|
96
|
+
skills.push(`${SKILLS_DIR}/${entry.name}.md`);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return skills.sort();
|
|
100
|
+
}
|
|
101
|
+
function resolveSkillFile(repoRoot, skillPath) {
|
|
102
|
+
const flatPath = join(repoRoot, skillPath);
|
|
103
|
+
if (existsSync(flatPath))
|
|
104
|
+
return flatPath;
|
|
105
|
+
const bundledSkill = /^skills\/([^/]+)\.md$/.exec(skillPath);
|
|
106
|
+
if (bundledSkill?.[1]) {
|
|
107
|
+
return join(repoRoot, SKILLS_DIR, bundledSkill[1], "SKILL.md");
|
|
108
|
+
}
|
|
109
|
+
return flatPath;
|
|
92
110
|
}
|
|
93
111
|
function toPosix(relPath) {
|
|
94
112
|
return relPath.split(sep).join(posix.sep);
|
|
95
113
|
}
|
|
96
114
|
export function checkRepo(repoRoot) {
|
|
115
|
+
const structure = [];
|
|
97
116
|
const sideA = [];
|
|
98
117
|
const sideB = [];
|
|
99
118
|
const sideC = [];
|
|
100
|
-
const
|
|
119
|
+
const scriptsPath = join(repoRoot, SCRIPTS_DIR);
|
|
101
120
|
const readmePath = join(repoRoot, SCRIPTS_README);
|
|
102
|
-
const
|
|
121
|
+
const scriptsDirectoryExists = existsSync(scriptsPath) && statSync(scriptsPath).isDirectory();
|
|
122
|
+
const readmeExists = scriptsDirectoryExists && existsSync(readmePath);
|
|
123
|
+
const readmeContent = readmeExists ? readFileSync(readmePath, "utf8") : "";
|
|
103
124
|
const rows = parseInventoryTable(readmeContent);
|
|
125
|
+
if (!scriptsDirectoryExists) {
|
|
126
|
+
structure.push("scripts/ directory is missing");
|
|
127
|
+
}
|
|
128
|
+
else if (!readmeExists) {
|
|
129
|
+
structure.push("scripts/README.md inventory is missing");
|
|
130
|
+
}
|
|
131
|
+
else if (rows.length === 0) {
|
|
132
|
+
structure.push("scripts/README.md inventory has no rows");
|
|
133
|
+
}
|
|
134
|
+
const scriptFiles = listScriptFiles(repoRoot);
|
|
104
135
|
const inventoryScripts = new Set();
|
|
105
136
|
for (const row of rows) {
|
|
106
137
|
for (const s of row.scripts)
|
|
107
138
|
inventoryScripts.add(s);
|
|
108
139
|
}
|
|
140
|
+
const scriptFileSet = new Set(scriptFiles);
|
|
109
141
|
for (const file of scriptFiles) {
|
|
110
142
|
if (!inventoryScripts.has(file)) {
|
|
111
|
-
sideA.push(file);
|
|
143
|
+
sideA.push(`${file} — file is missing from the inventory`);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
for (const inventoryScript of inventoryScripts) {
|
|
147
|
+
if (!scriptFileSet.has(inventoryScript)) {
|
|
148
|
+
sideA.push(`${inventoryScript} — inventoried helper file is missing`);
|
|
112
149
|
}
|
|
113
150
|
}
|
|
114
151
|
let citationCount = 0;
|
|
@@ -122,7 +159,7 @@ export function checkRepo(repoRoot) {
|
|
|
122
159
|
for (const row of rows) {
|
|
123
160
|
for (const skillPath of row.skills) {
|
|
124
161
|
citationCount += 1;
|
|
125
|
-
const fullSkill =
|
|
162
|
+
const fullSkill = resolveSkillFile(repoRoot, skillPath);
|
|
126
163
|
let skillBody = "";
|
|
127
164
|
try {
|
|
128
165
|
skillBody = readFileSync(fullSkill, "utf8");
|
|
@@ -142,7 +179,7 @@ export function checkRepo(repoRoot) {
|
|
|
142
179
|
for (const skillPath of skillFiles) {
|
|
143
180
|
let body = "";
|
|
144
181
|
try {
|
|
145
|
-
body = readFileSync(
|
|
182
|
+
body = readFileSync(resolveSkillFile(repoRoot, skillPath), "utf8");
|
|
146
183
|
}
|
|
147
184
|
catch {
|
|
148
185
|
continue;
|
|
@@ -160,6 +197,7 @@ export function checkRepo(repoRoot) {
|
|
|
160
197
|
}
|
|
161
198
|
}
|
|
162
199
|
return {
|
|
200
|
+
structure,
|
|
163
201
|
sideA,
|
|
164
202
|
sideB,
|
|
165
203
|
sideC,
|
|
@@ -170,15 +208,25 @@ export function checkRepo(repoRoot) {
|
|
|
170
208
|
function main() {
|
|
171
209
|
const repoRoot = argv[2] ?? cwd();
|
|
172
210
|
const result = checkRepo(repoRoot);
|
|
173
|
-
const totalViolations = result.
|
|
211
|
+
const totalViolations = result.structure.length +
|
|
212
|
+
result.sideA.length +
|
|
213
|
+
result.sideB.length +
|
|
214
|
+
result.sideC.length;
|
|
174
215
|
if (totalViolations === 0) {
|
|
175
216
|
console.log(`OK scripts-citation-drift: ${result.scriptCount} script(s), ${result.citationCount} skill citation(s), all three sides reconciled.`);
|
|
176
217
|
exit(0);
|
|
177
218
|
}
|
|
178
219
|
console.error(`FAIL scripts-citation-drift: ${totalViolations} drift(s) across the inventory-citation contract.`);
|
|
220
|
+
if (result.structure.length > 0) {
|
|
221
|
+
console.error("");
|
|
222
|
+
console.error("Contract structure:");
|
|
223
|
+
for (const v of result.structure) {
|
|
224
|
+
console.error(` ${toPosix(v)}`);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
179
227
|
if (result.sideA.length > 0) {
|
|
180
228
|
console.error("");
|
|
181
|
-
console.error("Side A —
|
|
229
|
+
console.error("Side A — scripts/ files and inventory rows do not match:");
|
|
182
230
|
for (const v of result.sideA) {
|
|
183
231
|
console.error(` ${toPosix(v)}`);
|
|
184
232
|
}
|
|
@@ -198,7 +246,7 @@ function main() {
|
|
|
198
246
|
}
|
|
199
247
|
}
|
|
200
248
|
console.error("");
|
|
201
|
-
console.error("Fix:
|
|
249
|
+
console.error("Fix: restore scripts/ and its inventory, update Side A/C, or correct the Side B skill citation.");
|
|
202
250
|
exit(1);
|
|
203
251
|
}
|
|
204
252
|
const scriptPathFromArgv = argv[1];
|
|
@@ -82,6 +82,7 @@ async function writePluginBundleReadme() {
|
|
|
82
82
|
"- `commands/`",
|
|
83
83
|
"- `agents/` — `code-reviewer`, `security-auditor`, `test-engineer` personas (auto-discovered Claude Code subagents; pattern from addy/agent-skills)",
|
|
84
84
|
"- `hooks/`",
|
|
85
|
+
"- `scripts/`",
|
|
85
86
|
"- `bin/mcp-server.mjs`",
|
|
86
87
|
"- `bin/observe.mjs`",
|
|
87
88
|
"- `bin/backfill.mjs`",
|
|
@@ -140,6 +141,7 @@ async function writePluginBundle() {
|
|
|
140
141
|
copyDirectory(join(REPO_ROOT, "commands"), join(PLUGIN_BUNDLE_DIR, "commands")),
|
|
141
142
|
copyDirectory(join(REPO_ROOT, "agents"), join(PLUGIN_BUNDLE_DIR, "agents")),
|
|
142
143
|
copyDirectory(join(REPO_ROOT, "hooks"), join(PLUGIN_BUNDLE_DIR, "hooks")),
|
|
144
|
+
copyDirectory(join(REPO_ROOT, "scripts"), join(PLUGIN_BUNDLE_DIR, "scripts")),
|
|
143
145
|
copyDirectory(join(REPO_ROOT, "instinct-packs"), join(PLUGIN_BUNDLE_DIR, "instinct-packs")),
|
|
144
146
|
copyDirectory(join(REPO_ROOT, "templates", "planning-with-files"), join(PLUGIN_BUNDLE_DIR, "templates", "planning-with-files")),
|
|
145
147
|
copyFileTo(join(REPO_ROOT, "bin", "mcp-server.mjs"), join(PLUGIN_BUNDLE_DIR, "bin", "mcp-server.mjs")),
|
package/bin/install.mjs
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
* npx continuous-improvement install --uninstall # remove everything
|
|
11
11
|
*/
|
|
12
12
|
import { chmodSync, copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, rmSync, statSync, writeFileSync, } from "node:fs";
|
|
13
|
-
import { execSync } from "node:child_process";
|
|
13
|
+
import { execFileSync as runFileSync, execSync } from "node:child_process";
|
|
14
14
|
import { homedir } from "node:os";
|
|
15
15
|
import { dirname, join } from "node:path";
|
|
16
16
|
import { fileURLToPath } from "node:url";
|
|
@@ -84,20 +84,25 @@ function readJsonFile(filePath) {
|
|
|
84
84
|
return null;
|
|
85
85
|
}
|
|
86
86
|
}
|
|
87
|
-
// The observation hooks
|
|
88
|
-
//
|
|
89
|
-
//
|
|
90
|
-
//
|
|
91
|
-
function
|
|
87
|
+
// The observation hooks are Bash scripts, and the native Windows settings use
|
|
88
|
+
// forward-slash drive paths such as C:/Users/... . Git Bash resolves that form;
|
|
89
|
+
// WSL's C:\Windows\System32\bash.exe does not. Probe the real packaged hook path
|
|
90
|
+
// instead of accepting any executable that happens to answer `bash --version`.
|
|
91
|
+
function assertBashCanReadHookOnWindows() {
|
|
92
92
|
if (process.platform !== "win32")
|
|
93
93
|
return;
|
|
94
|
+
const hookSource = toBashPath(join(REPO_ROOT, "hooks", "observe.sh"));
|
|
94
95
|
try {
|
|
95
|
-
|
|
96
|
+
runFileSync("bash", ["-c", 'test -r "$CONTINUOUS_IMPROVEMENT_HOOK_SOURCE"'], {
|
|
97
|
+
env: { ...process.env, CONTINUOUS_IMPROVEMENT_HOOK_SOURCE: hookSource },
|
|
98
|
+
stdio: "ignore",
|
|
99
|
+
});
|
|
96
100
|
}
|
|
97
101
|
catch {
|
|
98
|
-
console.error(
|
|
99
|
-
"
|
|
100
|
-
"reopen your shell
|
|
102
|
+
console.error(` ✗ Install refused: Bash on PATH cannot read the Windows hook path ${hookSource}. ` +
|
|
103
|
+
"Install Git Bash and ensure its bin directory comes before " +
|
|
104
|
+
"C:\\Windows\\System32 on PATH, then reopen your shell and re-run. " +
|
|
105
|
+
"See README > Troubleshooting install.");
|
|
101
106
|
process.exit(1);
|
|
102
107
|
}
|
|
103
108
|
}
|
|
@@ -612,6 +617,10 @@ function installNonClaudeTargets(targetIds) {
|
|
|
612
617
|
for (const note of notes)
|
|
613
618
|
console.log(` ℹ ${note}`);
|
|
614
619
|
}
|
|
620
|
+
// A mixed target install can write non-Claude rule files below. Validate the
|
|
621
|
+
// Claude hook runtime first so an incompatible Bash cannot leave a partial install.
|
|
622
|
+
if (requestedTargets.includes("claude"))
|
|
623
|
+
assertBashCanReadHookOnWindows();
|
|
615
624
|
const nonClaudeTargets = requestedTargets.filter((targetId) => targetId !== "claude");
|
|
616
625
|
if (nonClaudeTargets.length > 0) {
|
|
617
626
|
console.log("\ncontinuous-improvement multi-platform install\n");
|
|
@@ -625,7 +634,6 @@ console.log(`
|
|
|
625
634
|
continuous-improvement (mode: ${INSTALL_MODE})
|
|
626
635
|
Research → Plan → Execute → Verify → Reflect → Learn → Iterate
|
|
627
636
|
`);
|
|
628
|
-
assertBashAvailableOnWindows();
|
|
629
637
|
warnOnMarketplaceCollision();
|
|
630
638
|
console.log("Installing to Claude Code...\n");
|
|
631
639
|
const installed = installSkill() ? 1 : 0;
|
|
@@ -41,7 +41,7 @@ The observation hook appends one row per tool call to
|
|
|
41
41
|
|
|
42
42
|
- If it exists and has at least one row — capture is recording. Record `observe: ✓`.
|
|
43
43
|
- If it is missing or empty — record `observe: ✗ (observation hook not recording —
|
|
44
|
-
on Windows confirm Git Bash
|
|
44
|
+
on Windows confirm Git Bash is installed before WSL Bash on PATH, then re-run the installer)`.
|
|
45
45
|
|
|
46
46
|
## Report
|
|
47
47
|
|
package/hooks/gateguard.mjs
CHANGED
|
@@ -210,6 +210,16 @@ function buildMutatingFileReason(toolName, filePaths, stateFilePath) {
|
|
|
210
210
|
" `_gateguard_facts_presented: true`; Claude Code's strict schema rejects that, so use A or B.)",
|
|
211
211
|
].join("\n");
|
|
212
212
|
}
|
|
213
|
+
function isGitBraceSelector(value) {
|
|
214
|
+
const selector = value.trim();
|
|
215
|
+
if (/^(?:u|upstream|push|-?\d+)$/i.test(selector))
|
|
216
|
+
return true;
|
|
217
|
+
if (/^(?:now|today|yesterday|tomorrow|noon|midnight|tea)$/i.test(selector))
|
|
218
|
+
return true;
|
|
219
|
+
if (/^\d{4}-\d{1,2}-\d{1,2}(?:[ T].*)?$/.test(selector))
|
|
220
|
+
return true;
|
|
221
|
+
return !selector.startsWith("#") && /(?:^|[.\s])ago$/i.test(selector);
|
|
222
|
+
}
|
|
213
223
|
function findUnquotedBraceRef(command) {
|
|
214
224
|
let quote = null;
|
|
215
225
|
for (let i = 0; i < command.length; i++) {
|
|
@@ -224,17 +234,26 @@ function findUnquotedBraceRef(command) {
|
|
|
224
234
|
continue;
|
|
225
235
|
}
|
|
226
236
|
if (ch === "@" && command[i + 1] === "{") {
|
|
237
|
+
const braceEnd = command.indexOf("}", i + 2);
|
|
238
|
+
if (braceEnd === -1)
|
|
239
|
+
continue;
|
|
240
|
+
const selector = command.slice(i + 2, braceEnd);
|
|
241
|
+
// Deny only recognized Git selector grammar. PowerShell hashtables can
|
|
242
|
+
// contain comments, quoted braces, or arbitrary key expressions.
|
|
243
|
+
if (!isGitBraceSelector(selector)) {
|
|
244
|
+
i = braceEnd;
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
227
247
|
// Expand to the whitespace-delimited word that carries this @{ ref, then
|
|
228
248
|
// single-quote that whole word in the suggested fix.
|
|
229
249
|
let wordStart = i;
|
|
230
250
|
while (wordStart > 0 && !/\s/.test(command[wordStart - 1]))
|
|
231
251
|
wordStart--;
|
|
232
|
-
let wordEnd =
|
|
252
|
+
let wordEnd = braceEnd + 1;
|
|
233
253
|
while (wordEnd < command.length && !/\s/.test(command[wordEnd]))
|
|
234
254
|
wordEnd++;
|
|
235
255
|
const word = command.slice(wordStart, wordEnd);
|
|
236
|
-
const
|
|
237
|
-
const ref = braceEnd === -1 ? command.slice(i, wordEnd) : command.slice(i, braceEnd + 1);
|
|
256
|
+
const ref = command.slice(i, braceEnd + 1);
|
|
238
257
|
const fixed = `${command.slice(0, wordStart)}'${word}'${command.slice(wordEnd)}`;
|
|
239
258
|
return { ref, fixed };
|
|
240
259
|
}
|
|
@@ -61,6 +61,7 @@ function collectChangedFiles(root) {
|
|
|
61
61
|
return [
|
|
62
62
|
...parseChangedFiles(run(["diff", "--name-only", "--diff-filter=ACMR"])),
|
|
63
63
|
...parseChangedFiles(run(["diff", "--cached", "--name-only", "--diff-filter=ACMR"])),
|
|
64
|
+
...parseChangedFiles(run(["ls-files", "--others", "--exclude-standard"])),
|
|
64
65
|
];
|
|
65
66
|
}
|
|
66
67
|
// Marker key: the sanitized session_id when present (the normal case), else a
|
package/hooks/typecheck-stop.mjs
CHANGED
|
@@ -51,7 +51,8 @@ function collectChangedFiles(root) {
|
|
|
51
51
|
};
|
|
52
52
|
const unstaged = run(["diff", "--name-only", "--diff-filter=ACMR"]);
|
|
53
53
|
const staged = run(["diff", "--cached", "--name-only", "--diff-filter=ACMR"]);
|
|
54
|
-
|
|
54
|
+
const untracked = run(["ls-files", "--others", "--exclude-standard"]);
|
|
55
|
+
return [...parseChangedFiles(unstaged), ...parseChangedFiles(staged), ...parseChangedFiles(untracked)];
|
|
55
56
|
}
|
|
56
57
|
function hasNpmTypecheckScript(root) {
|
|
57
58
|
try {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "continuous-improvement",
|
|
3
|
-
"version": "3.20.
|
|
3
|
+
"version": "3.20.4",
|
|
4
4
|
"description": "Claude Code that gets sharper every session: the persistent-memory and runtime-discipline layer built on the 7 Laws of AI Agent Discipline. It grounds every edit in real facts before it lands and, through the Mulahazah engine, turns each fix into a reusable instinct, so a lesson learned once is applied automatically next time with no re-teaching. Shipped as 27 bundled skills, instinct-aware hooks, an MCP toolset for recall and reflection, and a GitHub Action transcript linter that feeds real work history back into sharper instincts. Beginner: one /plugin install command. Expert: adds MCP tools and session hooks.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"claude-code",
|
|
@@ -56,10 +56,11 @@
|
|
|
56
56
|
"verify:routing-targets": "node bin/check-routing-targets.mjs",
|
|
57
57
|
"verify:doc-runtime-claims": "node bin/check-doc-runtime-claims.mjs",
|
|
58
58
|
"verify:test-imports-only": "node bin/check-test-imports-only.mjs",
|
|
59
|
-
"verify:
|
|
59
|
+
"verify:landing-version": "node bin/check-landing-version.mjs",
|
|
60
|
+
"verify:scripts-citation-drift": "node bin/check-scripts-citation-drift.mjs && node bin/check-scripts-citation-drift.mjs plugins/continuous-improvement",
|
|
60
61
|
"verify:third-party-shape": "node bin/check-third-party-shape.mjs",
|
|
61
62
|
"verify:tool-count": "node bin/check-tool-count.mjs",
|
|
62
|
-
"verify:all": "npm run verify:skill-mirror && npm run verify:skill-tiers && npm run verify:skill-law-tag && npm run verify:skill-count && npm run verify:skill-count-prose && npm run verify:command-count && npm run verify:docs-substrings && npm run verify:everything-mirror && npm run verify:routing-targets && npm run verify:doc-runtime-claims && npm run verify:test-imports-only && npm run verify:scripts-citation-drift && npm run verify:third-party-shape && npm run verify:tool-count && npm run typecheck"
|
|
63
|
+
"verify:all": "npm run verify:skill-mirror && npm run verify:skill-tiers && npm run verify:skill-law-tag && npm run verify:skill-count && npm run verify:skill-count-prose && npm run verify:command-count && npm run verify:docs-substrings && npm run verify:everything-mirror && npm run verify:routing-targets && npm run verify:doc-runtime-claims && npm run verify:test-imports-only && npm run verify:landing-version && npm run verify:scripts-citation-drift && npm run verify:third-party-shape && npm run verify:tool-count && npm run typecheck"
|
|
63
64
|
},
|
|
64
65
|
"files": [
|
|
65
66
|
".claude-plugin/",
|
|
@@ -74,6 +75,7 @@
|
|
|
74
75
|
"hooks/",
|
|
75
76
|
"commands/",
|
|
76
77
|
"skills/",
|
|
78
|
+
"scripts/",
|
|
77
79
|
"templates/",
|
|
78
80
|
"plugins/",
|
|
79
81
|
"instinct-packs/"
|
package/plugins/beginner.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "continuous-improvement",
|
|
3
|
-
"version": "3.20.
|
|
3
|
+
"version": "3.20.4",
|
|
4
4
|
"mode": "beginner",
|
|
5
5
|
"description": "Beginner mode: see what your agent learned, list its instincts, and request a session reflection. Bundles three grounding skills (gateguard, tdd-workflow, verification-loop) so research, memory, tests, and verification happen by default — every edit starts from facts, not guesses.",
|
|
6
6
|
"tools": [
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
{
|
|
9
9
|
"name": "continuous-improvement",
|
|
10
10
|
"description": "The persistent-memory and runtime-discipline layer for Claude Code. It remembers the corrections you already gave, grounds every edit in real facts before it lands, and — through the Mulahazah engine — turns each fix into a reusable instinct, so a lesson learned once is applied automatically next time with no re-teaching. Built on the 7 Laws of AI Agent Discipline (research, plan, verify, reflect, learn) and shipped as 27 bundled skills, instinct-aware hooks, an MCP toolset for recall and reflection, and a GitHub Action transcript linter that feeds real work history back into sharper instincts.",
|
|
11
|
-
"version": "3.20.
|
|
11
|
+
"version": "3.20.4",
|
|
12
12
|
"source": "./",
|
|
13
13
|
"author": {
|
|
14
14
|
"name": "naimkatiman"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "continuous-improvement",
|
|
3
|
-
"version": "3.20.
|
|
3
|
+
"version": "3.20.4",
|
|
4
4
|
"description": "The persistent-memory and runtime-discipline layer for Claude Code. It remembers the corrections you already gave, grounds every edit in real facts before it lands, and — through the Mulahazah engine — turns each fix into a reusable instinct, so a lesson learned once is applied automatically next time with no re-teaching. Built on the 7 Laws of AI Agent Discipline (research, plan, verify, reflect, learn) and shipped as 27 bundled skills, instinct-aware hooks, an MCP toolset for recall and reflection, and a GitHub Action transcript linter that feeds real work history back into sharper instincts.",
|
|
5
5
|
"author": {
|
|
6
6
|
"name": "naimkatiman",
|
|
@@ -10,6 +10,7 @@ Included surfaces:
|
|
|
10
10
|
- `commands/`
|
|
11
11
|
- `agents/` — `code-reviewer`, `security-auditor`, `test-engineer` personas (auto-discovered Claude Code subagents; pattern from addy/agent-skills)
|
|
12
12
|
- `hooks/`
|
|
13
|
+
- `scripts/`
|
|
13
14
|
- `bin/mcp-server.mjs`
|
|
14
15
|
- `bin/observe.mjs`
|
|
15
16
|
- `bin/backfill.mjs`
|
|
@@ -41,7 +41,7 @@ The observation hook appends one row per tool call to
|
|
|
41
41
|
|
|
42
42
|
- If it exists and has at least one row — capture is recording. Record `observe: ✓`.
|
|
43
43
|
- If it is missing or empty — record `observe: ✗ (observation hook not recording —
|
|
44
|
-
on Windows confirm Git Bash
|
|
44
|
+
on Windows confirm Git Bash is installed before WSL Bash on PATH, then re-run the installer)`.
|
|
45
45
|
|
|
46
46
|
## Report
|
|
47
47
|
|
|
@@ -210,6 +210,16 @@ function buildMutatingFileReason(toolName, filePaths, stateFilePath) {
|
|
|
210
210
|
" `_gateguard_facts_presented: true`; Claude Code's strict schema rejects that, so use A or B.)",
|
|
211
211
|
].join("\n");
|
|
212
212
|
}
|
|
213
|
+
function isGitBraceSelector(value) {
|
|
214
|
+
const selector = value.trim();
|
|
215
|
+
if (/^(?:u|upstream|push|-?\d+)$/i.test(selector))
|
|
216
|
+
return true;
|
|
217
|
+
if (/^(?:now|today|yesterday|tomorrow|noon|midnight|tea)$/i.test(selector))
|
|
218
|
+
return true;
|
|
219
|
+
if (/^\d{4}-\d{1,2}-\d{1,2}(?:[ T].*)?$/.test(selector))
|
|
220
|
+
return true;
|
|
221
|
+
return !selector.startsWith("#") && /(?:^|[.\s])ago$/i.test(selector);
|
|
222
|
+
}
|
|
213
223
|
function findUnquotedBraceRef(command) {
|
|
214
224
|
let quote = null;
|
|
215
225
|
for (let i = 0; i < command.length; i++) {
|
|
@@ -224,17 +234,26 @@ function findUnquotedBraceRef(command) {
|
|
|
224
234
|
continue;
|
|
225
235
|
}
|
|
226
236
|
if (ch === "@" && command[i + 1] === "{") {
|
|
237
|
+
const braceEnd = command.indexOf("}", i + 2);
|
|
238
|
+
if (braceEnd === -1)
|
|
239
|
+
continue;
|
|
240
|
+
const selector = command.slice(i + 2, braceEnd);
|
|
241
|
+
// Deny only recognized Git selector grammar. PowerShell hashtables can
|
|
242
|
+
// contain comments, quoted braces, or arbitrary key expressions.
|
|
243
|
+
if (!isGitBraceSelector(selector)) {
|
|
244
|
+
i = braceEnd;
|
|
245
|
+
continue;
|
|
246
|
+
}
|
|
227
247
|
// Expand to the whitespace-delimited word that carries this @{ ref, then
|
|
228
248
|
// single-quote that whole word in the suggested fix.
|
|
229
249
|
let wordStart = i;
|
|
230
250
|
while (wordStart > 0 && !/\s/.test(command[wordStart - 1]))
|
|
231
251
|
wordStart--;
|
|
232
|
-
let wordEnd =
|
|
252
|
+
let wordEnd = braceEnd + 1;
|
|
233
253
|
while (wordEnd < command.length && !/\s/.test(command[wordEnd]))
|
|
234
254
|
wordEnd++;
|
|
235
255
|
const word = command.slice(wordStart, wordEnd);
|
|
236
|
-
const
|
|
237
|
-
const ref = braceEnd === -1 ? command.slice(i, wordEnd) : command.slice(i, braceEnd + 1);
|
|
256
|
+
const ref = command.slice(i, braceEnd + 1);
|
|
238
257
|
const fixed = `${command.slice(0, wordStart)}'${word}'${command.slice(wordEnd)}`;
|
|
239
258
|
return { ref, fixed };
|
|
240
259
|
}
|
|
@@ -61,6 +61,7 @@ function collectChangedFiles(root) {
|
|
|
61
61
|
return [
|
|
62
62
|
...parseChangedFiles(run(["diff", "--name-only", "--diff-filter=ACMR"])),
|
|
63
63
|
...parseChangedFiles(run(["diff", "--cached", "--name-only", "--diff-filter=ACMR"])),
|
|
64
|
+
...parseChangedFiles(run(["ls-files", "--others", "--exclude-standard"])),
|
|
64
65
|
];
|
|
65
66
|
}
|
|
66
67
|
// Marker key: the sanitized session_id when present (the normal case), else a
|
|
@@ -51,7 +51,8 @@ function collectChangedFiles(root) {
|
|
|
51
51
|
};
|
|
52
52
|
const unstaged = run(["diff", "--name-only", "--diff-filter=ACMR"]);
|
|
53
53
|
const staged = run(["diff", "--cached", "--name-only", "--diff-filter=ACMR"]);
|
|
54
|
-
|
|
54
|
+
const untracked = run(["ls-files", "--others", "--exclude-standard"]);
|
|
55
|
+
return [...parseChangedFiles(unstaged), ...parseChangedFiles(staged), ...parseChangedFiles(untracked)];
|
|
55
56
|
}
|
|
56
57
|
function hasNpmTypecheckScript(root) {
|
|
57
58
|
try {
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# `scripts/` — deterministic primitives skills cite
|
|
2
|
+
|
|
3
|
+
This directory holds small, hand-authored scripts that skills (and hooks) cite instead of restating fixed operations inline. The motivation is the skills audit's "deterministic vs non-deterministic" axis: when a step inside a skill is the same operation every time (a fixed git command, a regex parse, a path lookup), it does not belong in the LLM-driven part of the skill — it belongs here, where the script runs the same way every invocation, costs no tokens, and stays in one place so multiple skills can share it.
|
|
4
|
+
|
|
5
|
+
## Conventions
|
|
6
|
+
|
|
7
|
+
- **Hand-authored, no `.mts` source.** Files in `scripts/` are not part of the `tsc` build pipeline (see [`CLAUDE.md`](../CLAUDE.md) → "Build pipeline"). The pipeline owns `bin/`, `lib/`, `test/`. `scripts/` is the bare-metal home for shell scripts and one-off Node utilities skills cite directly.
|
|
8
|
+
- **One concern per script.** Each script does one thing and prints either machine-readable output (JSON envelope, single value) or a stable fenced block. Skills consume the output; they do not re-derive it.
|
|
9
|
+
- **Cross-platform shape.** Bash scripts run via Git Bash on Windows, native bash on macOS/Linux. Tests for bash scripts skip when bash is not on PATH (see `src/test/hook.test.mts` for the established skip pattern).
|
|
10
|
+
- **Cite from skills.** When a skill needs a primitive, the skill body cites the script path (`scripts/<name>.<ext>`) and quotes the expected output shape once. The skill does not restate the script's inner mechanics.
|
|
11
|
+
|
|
12
|
+
## Inventory
|
|
13
|
+
|
|
14
|
+
| Script | Purpose | Cited by |
|
|
15
|
+
|---|---|---|
|
|
16
|
+
| `git-state-snapshot.sh` | JSON envelope `{head, upstream, dirty, root, branch}` for the current git working tree | `skills/gateguard.md` (Parallel-Actor Gate), `skills/worktree-safety.md` (Root + branch), `skills/workspace-surface-audit.md` (Environment Grain — parallel-actor row), `skills/reconcile.md` (Detect a Concurrent Writer) |
|
|
17
|
+
| `detect-deploy-target.sh` | Detect the auto-deploy provider for the repo at the current working directory (or first arg). Prints one of `railway`/`cloudflare`/`vercel`/`netlify`/`fly`/`appengine`/`apprunner`/`gha-deploy`/`none`. Always exits 0. | `skills/verification-loop.md` (Phase 8 deploy-receipt handoff), `skills/deploy-receipt.md` (When to Activate gate) |
|
|
18
|
+
| `get-deployed-sha.sh` | Per-provider deployed-SHA extraction. Default mode runs the CLI; `--show-command <provider>` prints the pipeline shape without executing (useful for citation, dry-run, tests). | `skills/verification-loop.md` (Phase 8), `skills/deploy-receipt.md` (Route A — provider CLI extraction) |
|
|
19
|
+
| `resolve-verify-ladder.mjs` | Resolve the per-project verification ladder for Phase 0 of `verification-loop`. Encodes the four-step priority — manifest > package.json sniff > per-language toolchain > ask-operator. Default mode prints the fenced block the skill displays; `--json` mode emits a JSON object for machine consumption. | `skills/verification-loop.md` (Phase 0) |
|
|
20
|
+
| `scan-past-mistakes.mjs` | Scan the three Past-Mistake Acknowledgment Gate surfaces — `~/.claude/instincts/<hash>/observations.jsonl` (last N failure/correction rows), `~/.claude/projects/<hash>/memory/feedback_*.md`, and `<root>/CLAUDE.md` "## Past Mistakes" table. Active-in-scope judgment is the LLM's job; the script provides quotes + citations only. | `skills/proceed-with-the-recommendation.md` (Phase 0 Rule 1) |
|
|
21
|
+
| `route-recommendation.mjs` + `route-recommendation.routes.json` | Match a recommendation item to its preferred-skill chain + inline fallback from the Phase 3 routing table (29 rows). Data file is the programmatic source of truth; the skill's table is documentation that mirrors it. Default mode prints a match block; `--json` for machine consumption; `--list` enumerates all rows. | `skills/proceed-with-the-recommendation.md` (Phase 3 routing table) |
|
|
22
|
+
| `run-synthetic.mjs` | Phase 9 runner: invoke every `*.synthetic.{sh,mjs,ts,py}` in `synthetic-checks/`, inject `BASE_URL`/`BASELINE_URL`/`EXPECTED_SHA`/`DEPLOY_BRANCH`/`RECEIPT_TIMESTAMP` (unset vars pass through as `""`), capture stdout/stderr/exit per check, aggregate. Default mode prints the report block; `--json` for machine consumption; `--fail-fast` halts on first drift or timeout; `--timeout <sec>` per-check wall-clock cap; `--show-command` dry-run. Exit 0 all-pass, 1 drift, 2 config error, 3 usage. | `skills/verification-loop.md` (Phase 9 — production-vs-baseline diff) |
|
|
23
|
+
|
|
24
|
+
When a new script lands here, add a row to this table in the same PR and cite the script from at least one skill — otherwise the script is dead code on arrival.
|
|
25
|
+
|
|
26
|
+
## Relationship to other locations
|
|
27
|
+
|
|
28
|
+
- `bin/` — generated CLI entrypoints (`.mjs` from `src/bin/*.mts`). Do not hand-edit; see [`CLAUDE.md`](../CLAUDE.md) → "Build pipeline".
|
|
29
|
+
- `lib/` — generated library code (`.mjs` from `src/lib/*.mts`). Same rule.
|
|
30
|
+
- `hooks/` — generated PreToolUse / PostToolUse / Stop hooks (`.mjs` from `src/hooks/*.mts`). Wired in `plugins/continuous-improvement/hooks/hooks.json`.
|
|
31
|
+
- `scripts/` — this directory. Hand-authored primitives skills cite.
|
|
32
|
+
|
|
33
|
+
When deciding where a new piece of code belongs: if it implements a runtime hook the harness will call, it goes in `src/hooks/`. If it's a verification or CLI entrypoint the plugin or `npm run` invokes, it goes in `src/bin/`. If it's reusable logic shared between those, it goes in `src/lib/`. If it's a small primitive a skill body cites by path (and the operator or harness runs ad hoc), it goes here.
|