continuous-improvement 3.19.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 +3 -2
- package/bin/check-landing-version.mjs +63 -0
- package/bin/check-scripts-citation-drift.mjs +61 -13
- package/bin/generate-plugin-manifests.mjs +3 -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 +114 -0
- package/hooks/typecheck-stop.mjs +2 -1
- package/lib/plugin-metadata.mjs +7 -2
- package/lib/query-cost-gate.mjs +53 -0
- 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/hooks.json +6 -1
- package/plugins/continuous-improvement/hooks/query-cost-nudge.mjs +114 -0
- package/plugins/continuous-improvement/hooks/typecheck-stop.mjs +2 -1
- package/plugins/continuous-improvement/lib/plugin-metadata.mjs +7 -2
- package/plugins/continuous-improvement/lib/query-cost-gate.mjs +53 -0
- 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.
|
|
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.
|
|
@@ -177,6 +177,7 @@ The framework has documented operator-level modes that change hook behavior with
|
|
|
177
177
|
| `CLAUDE_TYPECHECK_GATE` | `hooks/typecheck-stop.mjs` (a `Stop` hook) runs the project typecheck (the `typecheck` npm script, else a local `tsc --noEmit`) on changed TS files at turn end and feeds a failure back to the model. `off` (default) is a no-op — the global advisory `typecheck-changed.sh` stays the default layer; `warn` prints a one-line stderr notice; `block` re-prompts with the tsc output so a headless/autonomous `-p` loop fixes its own type errors before ending the turn. Skips non-TS repos and turns where no TS file changed; fails open on any error or timeout. | bash/zsh: `export CLAUDE_TYPECHECK_GATE=block` in `~/.bashrc` / `~/.zshrc`. PowerShell: `$env:CLAUDE_TYPECHECK_GATE='block'` (session) or `[Environment]::SetEnvironmentVariable('CLAUDE_TYPECHECK_GATE','block','User')` (persistent). |
|
|
178
178
|
| `CLAUDE_RECALL_BRIEFING=1` | `hooks/recall-briefing.mjs` (a UserPromptSubmit hook) makes episodic memory proactive: on the first substantive prompt of a session it searches this project's past observations (BM25) and injects a one-time `<system-reminder>` with the most relevant prior activity, so the agent reuses a past fix instead of re-deriving it. Opt-in and default off; it is an amplifier, never a gate — it cannot block a prompt and fails open. The `ci_recall` MCP tool stays available for explicit, deeper searches. | bash/zsh: `export CLAUDE_RECALL_BRIEFING=1` in `~/.bashrc` / `~/.zshrc`. PowerShell: `$env:CLAUDE_RECALL_BRIEFING=1` (session) or `[Environment]::SetEnvironmentVariable('CLAUDE_RECALL_BRIEFING','1','User')` (persistent). |
|
|
179
179
|
| `CLAUDE_WORKFLOW_DISTILL_NUDGE=on` | `hooks/workflow-distill.mjs` (a `Stop` hook) closes the orchestration-to-memory loop: when a native Workflow run's output then passed a verify in the same session, it prints a one-line stderr nudge to run the `ci_distill_from_workflow` MCP tool, so an expensive multi-agent run leaves a durable Mulahazah draft instinct instead of evaporating. `on` enables it; default (unset or any other value) is off. Opt-in amplifier, never a gate — it cannot block the Stop, dedupes per run, and fails open. | bash/zsh: `export CLAUDE_WORKFLOW_DISTILL_NUDGE=on` in `~/.bashrc` / `~/.zshrc`. PowerShell: `$env:CLAUDE_WORKFLOW_DISTILL_NUDGE='on'` (session) or `[Environment]::SetEnvironmentVariable('CLAUDE_WORKFLOW_DISTILL_NUDGE','on','User')` (persistent). |
|
|
180
|
+
| `CLAUDE_QUERY_COST_NUDGE=on` | `hooks/query-cost-nudge.mjs` (a `Stop` hook) guards against surprise DB bills: when the working tree has changed DB/query files (`.sql`, `.prisma`, `migrations/`, `/db/`, `schema.*`, `drizzle`) at turn end, it injects a once-per-session `additionalContext` reminder to run a D1-aware cost audit — dispatch the `database-reviewer` agent or check EXPLAIN QUERY PLAN, index coverage, N+1, and D1 `rows_read` billing before finishing. `on` enables it; default (unset) is off. Opt-in amplifier, never a gate; dedupes per session and fails open. | bash/zsh: `export CLAUDE_QUERY_COST_NUDGE=on` in `~/.bashrc` / `~/.zshrc`. PowerShell: `$env:CLAUDE_QUERY_COST_NUDGE='on'` (session) or `[Environment]::SetEnvironmentVariable('CLAUDE_QUERY_COST_NUDGE','on','User')` (persistent). |
|
|
180
181
|
|
|
181
182
|
</details>
|
|
182
183
|
|
|
@@ -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")),
|
|
@@ -157,6 +159,7 @@ async function writePluginBundle() {
|
|
|
157
159
|
copyFileTo(join(REPO_ROOT, "lib", "recall-briefing.mjs"), join(PLUGIN_BUNDLE_DIR, "lib", "recall-briefing.mjs")),
|
|
158
160
|
copyFileTo(join(REPO_ROOT, "lib", "skill-distill.mjs"), join(PLUGIN_BUNDLE_DIR, "lib", "skill-distill.mjs")),
|
|
159
161
|
copyFileTo(join(REPO_ROOT, "lib", "typecheck-gate.mjs"), join(PLUGIN_BUNDLE_DIR, "lib", "typecheck-gate.mjs")),
|
|
162
|
+
copyFileTo(join(REPO_ROOT, "lib", "query-cost-gate.mjs"), join(PLUGIN_BUNDLE_DIR, "lib", "query-cost-gate.mjs")),
|
|
160
163
|
copyFileTo(join(REPO_ROOT, "LICENSE"), join(PLUGIN_BUNDLE_DIR, "LICENSE")),
|
|
161
164
|
writePluginBundleReadme(),
|
|
162
165
|
]);
|
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
|
}
|
|
@@ -0,0 +1,114 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* query-cost-nudge.mts — Stop hook that nudges a D1-aware query-cost audit when
|
|
4
|
+
* the working tree has changed DB/query files at turn end (RISA 5 / G5).
|
|
5
|
+
*
|
|
6
|
+
* The `database-reviewer` agent already carries the cost checklist, but nothing
|
|
7
|
+
* dispatched it on DB edits, so cost regressions (the surprise-D1-bill class)
|
|
8
|
+
* shipped unaudited. This injects a once-per-session reminder via Stop
|
|
9
|
+
* `additionalContext` — a Stop hook (not PostToolUse) because only PreToolUse /
|
|
10
|
+
* UserPromptSubmit / Stop / SubagentStop support additionalContext; PostToolUse
|
|
11
|
+
* could only emit a user-facing systemMessage that never reaches the model.
|
|
12
|
+
*
|
|
13
|
+
* Opt-in via CLAUDE_QUERY_COST_NUDGE=on (default off). Once-per-session dedup
|
|
14
|
+
* keyed on the Stop stdin session_id (falls back to a per-day key) — without it,
|
|
15
|
+
* additionalContext "keeps the turn going" and would re-fire every turn while
|
|
16
|
+
* the DB files stay dirty. Never blocks; fail-open on any error.
|
|
17
|
+
*/
|
|
18
|
+
import { execFileSync } from "node:child_process";
|
|
19
|
+
import { createHash } from "node:crypto";
|
|
20
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
21
|
+
import { homedir } from "node:os";
|
|
22
|
+
import { dirname, join } from "node:path";
|
|
23
|
+
import { buildQueryCostReminder, changedQueryPaths, parseChangedFiles, resolveQueryCostNudge, } from "../lib/query-cost-gate.mjs";
|
|
24
|
+
function readStdin() {
|
|
25
|
+
try {
|
|
26
|
+
return readFileSync(0, "utf8");
|
|
27
|
+
}
|
|
28
|
+
catch {
|
|
29
|
+
return "";
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
function resolveHome() {
|
|
33
|
+
return process.env.HOME || process.env.USERPROFILE || homedir();
|
|
34
|
+
}
|
|
35
|
+
function resolveProjectRoot() {
|
|
36
|
+
const fromEnv = process.env.CLAUDE_PROJECT_DIR;
|
|
37
|
+
if (fromEnv && fromEnv.trim())
|
|
38
|
+
return fromEnv.trim();
|
|
39
|
+
try {
|
|
40
|
+
const root = execFileSync("git", ["rev-parse", "--show-toplevel"], {
|
|
41
|
+
encoding: "utf8",
|
|
42
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
43
|
+
}).trim();
|
|
44
|
+
if (root)
|
|
45
|
+
return root;
|
|
46
|
+
}
|
|
47
|
+
catch {
|
|
48
|
+
// not in a git repo
|
|
49
|
+
}
|
|
50
|
+
return "global";
|
|
51
|
+
}
|
|
52
|
+
function collectChangedFiles(root) {
|
|
53
|
+
const run = (args) => {
|
|
54
|
+
try {
|
|
55
|
+
return execFileSync("git", args, { cwd: root, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
return "";
|
|
59
|
+
}
|
|
60
|
+
};
|
|
61
|
+
return [
|
|
62
|
+
...parseChangedFiles(run(["diff", "--name-only", "--diff-filter=ACMR"])),
|
|
63
|
+
...parseChangedFiles(run(["diff", "--cached", "--name-only", "--diff-filter=ACMR"])),
|
|
64
|
+
...parseChangedFiles(run(["ls-files", "--others", "--exclude-standard"])),
|
|
65
|
+
];
|
|
66
|
+
}
|
|
67
|
+
// Marker key: the sanitized session_id when present (the normal case), else a
|
|
68
|
+
// per-day key so a missing id self-heals daily instead of blocking forever.
|
|
69
|
+
function markerKey(sessionId) {
|
|
70
|
+
const sanitized = (sessionId ?? "").replace(/[^A-Za-z0-9_-]/g, "_").slice(0, 64);
|
|
71
|
+
return sanitized || `day-${new Date().toISOString().slice(0, 10)}`;
|
|
72
|
+
}
|
|
73
|
+
function markerPath(home, projectRoot, sessionId) {
|
|
74
|
+
const hash = createHash("sha256").update(projectRoot).digest("hex").slice(0, 12);
|
|
75
|
+
return join(home, ".claude", "instincts", hash, "query-cost-nudge", `${markerKey(sessionId)}.nudged`);
|
|
76
|
+
}
|
|
77
|
+
function main() {
|
|
78
|
+
if (resolveQueryCostNudge(process.env.CLAUDE_QUERY_COST_NUDGE) === "off")
|
|
79
|
+
return;
|
|
80
|
+
let sessionId;
|
|
81
|
+
try {
|
|
82
|
+
const payload = JSON.parse(readStdin());
|
|
83
|
+
if (typeof payload.session_id === "string")
|
|
84
|
+
sessionId = payload.session_id;
|
|
85
|
+
}
|
|
86
|
+
catch {
|
|
87
|
+
// stdin optional — proceed without a session id (per-day dedup)
|
|
88
|
+
}
|
|
89
|
+
const root = resolveProjectRoot();
|
|
90
|
+
if (root === "global")
|
|
91
|
+
return; // no repo → nothing to diff
|
|
92
|
+
const changed = changedQueryPaths(collectChangedFiles(root));
|
|
93
|
+
if (changed.length === 0)
|
|
94
|
+
return;
|
|
95
|
+
const marker = markerPath(resolveHome(), root, sessionId);
|
|
96
|
+
try {
|
|
97
|
+
if (existsSync(marker))
|
|
98
|
+
return; // already nudged this session
|
|
99
|
+
mkdirSync(dirname(marker), { recursive: true });
|
|
100
|
+
writeFileSync(marker, `${new Date().toISOString()}\n`);
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
// if the marker can't be written, nudge anyway (no dedup) rather than stay silent
|
|
104
|
+
}
|
|
105
|
+
process.stdout.write(`${JSON.stringify({
|
|
106
|
+
hookSpecificOutput: { hookEventName: "Stop", additionalContext: buildQueryCostReminder(changed) },
|
|
107
|
+
})}\n`);
|
|
108
|
+
}
|
|
109
|
+
try {
|
|
110
|
+
main();
|
|
111
|
+
}
|
|
112
|
+
catch {
|
|
113
|
+
// fail open — never trap a turn on a hook bug
|
|
114
|
+
}
|
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/lib/plugin-metadata.mjs
CHANGED
|
@@ -505,6 +505,11 @@ export function getPluginHooksConfig() {
|
|
|
505
505
|
// internal timeout it fails open (allow) rather than blocking.
|
|
506
506
|
timeout: 30,
|
|
507
507
|
};
|
|
508
|
+
const queryCostNudgeCommand = {
|
|
509
|
+
type: "command",
|
|
510
|
+
command: "node \"${CLAUDE_PLUGIN_ROOT}/hooks/query-cost-nudge.mjs\"",
|
|
511
|
+
timeout: 5,
|
|
512
|
+
};
|
|
508
513
|
const routePromptCommand = {
|
|
509
514
|
type: "command",
|
|
510
515
|
command: "node \"${CLAUDE_PLUGIN_ROOT}/hooks/route-prompt.mjs\"",
|
|
@@ -516,7 +521,7 @@ export function getPluginHooksConfig() {
|
|
|
516
521
|
timeout: 5,
|
|
517
522
|
};
|
|
518
523
|
return {
|
|
519
|
-
description: "Gateguard fact-forcing PreToolUse, companion-preference enforcement, observation, session lifecycle, 3-section-close discipline, goal-drift Stop gate, opt-in workflow-distill Stop nudge, opt-in typecheck Stop gate, and UserPromptSubmit lazy-routing plus opt-in proactive recall-briefing hooks for continuous-improvement.",
|
|
524
|
+
description: "Gateguard fact-forcing PreToolUse, companion-preference enforcement, observation, session lifecycle, 3-section-close discipline, goal-drift Stop gate, opt-in workflow-distill Stop nudge, opt-in typecheck Stop gate, opt-in query-cost Stop nudge, and UserPromptSubmit lazy-routing plus opt-in proactive recall-briefing hooks for continuous-improvement.",
|
|
520
525
|
hooks: {
|
|
521
526
|
// gateguard runs FIRST on PreToolUse so its block decision short-circuits
|
|
522
527
|
// before companion-preference sees the call. companion-preference runs
|
|
@@ -540,7 +545,7 @@ export function getPluginHooksConfig() {
|
|
|
540
545
|
UserPromptSubmit: [{ hooks: [routePromptCommand, recallBriefingCommand] }],
|
|
541
546
|
SessionStart: [{ hooks: [sessionCommand] }],
|
|
542
547
|
SessionEnd: [{ hooks: [sessionCommand] }],
|
|
543
|
-
Stop: [{ hooks: [threeSectionCloseCommand, goalDriftStopCommand, workflowDistillCommand, typecheckStopCommand] }],
|
|
548
|
+
Stop: [{ hooks: [threeSectionCloseCommand, goalDriftStopCommand, workflowDistillCommand, typecheckStopCommand, queryCostNudgeCommand] }],
|
|
544
549
|
},
|
|
545
550
|
};
|
|
546
551
|
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure helpers for the query-cost nudge Stop hook (RISA 5 / G5).
|
|
3
|
+
*
|
|
4
|
+
* The hook (src/hooks/query-cost-nudge.mts) does the I/O — git, the per-session
|
|
5
|
+
* dedup marker, emitting the reminder. Everything here is pure and unit-tested.
|
|
6
|
+
*
|
|
7
|
+
* Opt-in via CLAUDE_QUERY_COST_NUDGE=on (default off). When on and the working
|
|
8
|
+
* tree has changed DB/query files at turn end, the hook injects a once-per-session
|
|
9
|
+
* reminder (via Stop `additionalContext`) to run a D1-aware cost audit before
|
|
10
|
+
* finishing — the surprise-D1-bill class the insights flagged. Never blocks.
|
|
11
|
+
*/
|
|
12
|
+
export function resolveQueryCostNudge(raw) {
|
|
13
|
+
return (raw ?? "").trim().toLowerCase() === "on" ? "on" : "off";
|
|
14
|
+
}
|
|
15
|
+
// DB / query source paths. Case-insensitive; the caller passes forward-slash
|
|
16
|
+
// paths straight from `git diff --name-only` (git always uses forward slashes).
|
|
17
|
+
const QUERY_PATH_RES = [
|
|
18
|
+
/\.sql$/i,
|
|
19
|
+
/\.prisma$/i,
|
|
20
|
+
/(^|\/)migrations?\//i,
|
|
21
|
+
/(^|\/)db\//i,
|
|
22
|
+
/(^|\/)schema\.(ts|js|mjs|cjs|cts|mts|prisma|sql)$/i,
|
|
23
|
+
/drizzle/i,
|
|
24
|
+
];
|
|
25
|
+
export function isQueryPath(filePath) {
|
|
26
|
+
const path = (filePath ?? "").replace(/\\/g, "/").trim();
|
|
27
|
+
if (path === "")
|
|
28
|
+
return false;
|
|
29
|
+
return QUERY_PATH_RES.some((re) => re.test(path));
|
|
30
|
+
}
|
|
31
|
+
export function parseChangedFiles(gitOutput) {
|
|
32
|
+
return (gitOutput ?? "")
|
|
33
|
+
.split(/\r?\n/)
|
|
34
|
+
.map((line) => line.trim())
|
|
35
|
+
.filter((line) => line.length > 0);
|
|
36
|
+
}
|
|
37
|
+
export function changedQueryPaths(files) {
|
|
38
|
+
return files.filter((file) => isQueryPath(file));
|
|
39
|
+
}
|
|
40
|
+
export function buildQueryCostReminder(paths) {
|
|
41
|
+
const list = paths.slice(0, 8).map((path) => path.replace(/\\/g, "/")).join(", ");
|
|
42
|
+
return [
|
|
43
|
+
"<system-reminder>",
|
|
44
|
+
`Query-cost check: you changed DB/query file(s) this session (${list}).`,
|
|
45
|
+
"Before finishing, audit cost — dispatch the database-reviewer agent, or check directly:",
|
|
46
|
+
" - EXPLAIN QUERY PLAN each new/changed query; no full table SCAN on a hot path.",
|
|
47
|
+
" - Every WHERE / JOIN / ORDER BY column is index-covered; no N+1 loops.",
|
|
48
|
+
" - D1: rows_read is billed per row SCANNED (not returned) — add covering indexes and cache hot reads (KV) instead of re-querying.",
|
|
49
|
+
" - No unbounded query — add LIMIT / pagination.",
|
|
50
|
+
"One-time per-session reminder (CLAUDE_QUERY_COST_NUDGE).",
|
|
51
|
+
"</system-reminder>",
|
|
52
|
+
].join("\n");
|
|
53
|
+
}
|