continuous-improvement 3.18.0 → 3.20.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 +1 -1
- package/README.md +2 -0
- package/bin/generate-plugin-manifests.mjs +2 -0
- package/hooks/query-cost-nudge.mjs +113 -0
- package/hooks/typecheck-stop.mjs +117 -0
- package/lib/plugin-metadata.mjs +15 -2
- package/lib/query-cost-gate.mjs +53 -0
- package/lib/typecheck-gate.mjs +62 -0
- package/package.json +1 -1
- 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/hooks/hooks.json +11 -1
- package/plugins/continuous-improvement/hooks/query-cost-nudge.mjs +113 -0
- package/plugins/continuous-improvement/hooks/typecheck-stop.mjs +117 -0
- package/plugins/continuous-improvement/lib/plugin-metadata.mjs +15 -2
- package/plugins/continuous-improvement/lib/query-cost-gate.mjs +53 -0
- package/plugins/continuous-improvement/lib/typecheck-gate.mjs +62 -0
- package/plugins/expert.json +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.0",
|
|
12
12
|
"source": "./plugins/continuous-improvement",
|
|
13
13
|
"author": {
|
|
14
14
|
"name": "naimkatiman"
|
package/README.md
CHANGED
|
@@ -174,8 +174,10 @@ The framework has documented operator-level modes that change hook behavior with
|
|
|
174
174
|
|---|---|---|
|
|
175
175
|
| `CLAUDE_THREE_SECTION_CLOSE_DISABLED=1` | `three-section-close.mjs` short-circuits before any enforcement or telemetry. Use when end-of-turn reflection should run as internal thinking rather than visible "What has been done / What is next / Recommendation" sections. Public default unchanged — the rule still fires for everyone else. | bash/zsh: `export CLAUDE_THREE_SECTION_CLOSE_DISABLED=1` in `~/.bashrc` / `~/.zshrc`. PowerShell: `$env:CLAUDE_THREE_SECTION_CLOSE_DISABLED=1` (session) or `[Environment]::SetEnvironmentVariable('CLAUDE_THREE_SECTION_CLOSE_DISABLED','1','User')` (persistent). |
|
|
176
176
|
| `CLAUDE_GOAL_DRIFT_GATE` | `goal-drift-stop.mjs` (a `Stop` hook) scores each turn's activity against the `## Goal` in `task_plan.md` and acts on drift. `warn` (default) prints a one-line stderr notice and never blocks; `block` re-prompts a substantive wrap-up that has drifted off-goal so the goal gates the close; `off` disables it. Reads the same observation feed as Mulahazah; fails open on any error. | bash/zsh: `export CLAUDE_GOAL_DRIFT_GATE=block` in `~/.bashrc` / `~/.zshrc`. PowerShell: `$env:CLAUDE_GOAL_DRIFT_GATE='block'` (session) or `[Environment]::SetEnvironmentVariable('CLAUDE_GOAL_DRIFT_GATE','block','User')` (persistent). |
|
|
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). |
|
|
177
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). |
|
|
178
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). |
|
|
179
181
|
|
|
180
182
|
</details>
|
|
181
183
|
|
|
@@ -156,6 +156,8 @@ async function writePluginBundle() {
|
|
|
156
156
|
copyFileTo(join(REPO_ROOT, "lib", "recall-index.mjs"), join(PLUGIN_BUNDLE_DIR, "lib", "recall-index.mjs")),
|
|
157
157
|
copyFileTo(join(REPO_ROOT, "lib", "recall-briefing.mjs"), join(PLUGIN_BUNDLE_DIR, "lib", "recall-briefing.mjs")),
|
|
158
158
|
copyFileTo(join(REPO_ROOT, "lib", "skill-distill.mjs"), join(PLUGIN_BUNDLE_DIR, "lib", "skill-distill.mjs")),
|
|
159
|
+
copyFileTo(join(REPO_ROOT, "lib", "typecheck-gate.mjs"), join(PLUGIN_BUNDLE_DIR, "lib", "typecheck-gate.mjs")),
|
|
160
|
+
copyFileTo(join(REPO_ROOT, "lib", "query-cost-gate.mjs"), join(PLUGIN_BUNDLE_DIR, "lib", "query-cost-gate.mjs")),
|
|
159
161
|
copyFileTo(join(REPO_ROOT, "LICENSE"), join(PLUGIN_BUNDLE_DIR, "LICENSE")),
|
|
160
162
|
writePluginBundleReadme(),
|
|
161
163
|
]);
|
|
@@ -0,0 +1,113 @@
|
|
|
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
|
+
];
|
|
65
|
+
}
|
|
66
|
+
// Marker key: the sanitized session_id when present (the normal case), else a
|
|
67
|
+
// per-day key so a missing id self-heals daily instead of blocking forever.
|
|
68
|
+
function markerKey(sessionId) {
|
|
69
|
+
const sanitized = (sessionId ?? "").replace(/[^A-Za-z0-9_-]/g, "_").slice(0, 64);
|
|
70
|
+
return sanitized || `day-${new Date().toISOString().slice(0, 10)}`;
|
|
71
|
+
}
|
|
72
|
+
function markerPath(home, projectRoot, sessionId) {
|
|
73
|
+
const hash = createHash("sha256").update(projectRoot).digest("hex").slice(0, 12);
|
|
74
|
+
return join(home, ".claude", "instincts", hash, "query-cost-nudge", `${markerKey(sessionId)}.nudged`);
|
|
75
|
+
}
|
|
76
|
+
function main() {
|
|
77
|
+
if (resolveQueryCostNudge(process.env.CLAUDE_QUERY_COST_NUDGE) === "off")
|
|
78
|
+
return;
|
|
79
|
+
let sessionId;
|
|
80
|
+
try {
|
|
81
|
+
const payload = JSON.parse(readStdin());
|
|
82
|
+
if (typeof payload.session_id === "string")
|
|
83
|
+
sessionId = payload.session_id;
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
// stdin optional — proceed without a session id (per-day dedup)
|
|
87
|
+
}
|
|
88
|
+
const root = resolveProjectRoot();
|
|
89
|
+
if (root === "global")
|
|
90
|
+
return; // no repo → nothing to diff
|
|
91
|
+
const changed = changedQueryPaths(collectChangedFiles(root));
|
|
92
|
+
if (changed.length === 0)
|
|
93
|
+
return;
|
|
94
|
+
const marker = markerPath(resolveHome(), root, sessionId);
|
|
95
|
+
try {
|
|
96
|
+
if (existsSync(marker))
|
|
97
|
+
return; // already nudged this session
|
|
98
|
+
mkdirSync(dirname(marker), { recursive: true });
|
|
99
|
+
writeFileSync(marker, `${new Date().toISOString()}\n`);
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
// if the marker can't be written, nudge anyway (no dedup) rather than stay silent
|
|
103
|
+
}
|
|
104
|
+
process.stdout.write(`${JSON.stringify({
|
|
105
|
+
hookSpecificOutput: { hookEventName: "Stop", additionalContext: buildQueryCostReminder(changed) },
|
|
106
|
+
})}\n`);
|
|
107
|
+
}
|
|
108
|
+
try {
|
|
109
|
+
main();
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
// fail open — never trap a turn on a hook bug
|
|
113
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* typecheck-stop.mts — Stop hook that runs the project typecheck on changed TS
|
|
4
|
+
* files and, when opted in, returns a block decision so the failure re-enters
|
|
5
|
+
* model context and the agent fixes it before ending the turn — in all modes,
|
|
6
|
+
* including headless `-p` runs where Stop still fires (RISA 4 / G4).
|
|
7
|
+
*
|
|
8
|
+
* Ports the proven detection logic of ~/.claude/scripts/typecheck-changed.sh
|
|
9
|
+
* (skip non-TS repos, skip TS repos with no changed TS file, prefer the npm
|
|
10
|
+
* `typecheck` script) and adds the block mechanism of goal-drift-stop.mts.
|
|
11
|
+
*
|
|
12
|
+
* Mode via CLAUDE_TYPECHECK_GATE: "off" (default) | "warn" | "block".
|
|
13
|
+
* off : no-op (the global script's advisory systemMessage stays the default
|
|
14
|
+
* layer; this hook is the opt-in enforcement layer — zero regression).
|
|
15
|
+
* warn : one-line stderr notice; never blocks.
|
|
16
|
+
* block : {"decision":"block","reason":...} to re-prompt the model.
|
|
17
|
+
*
|
|
18
|
+
* Fail-open by construction: any error, missing root, non-TS repo, no changed
|
|
19
|
+
* TS file, no runnable typecheck, or a run that times out exits 0 (allow). No
|
|
20
|
+
* network. The wiring gives this hook a longer timeout than the 5s hooks
|
|
21
|
+
* because `tsc` is slower; on the internal timeout it fails open.
|
|
22
|
+
*/
|
|
23
|
+
import { execFileSync, spawnSync } from "node:child_process";
|
|
24
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
25
|
+
import { join } from "node:path";
|
|
26
|
+
import { decideTypecheckAction, formatTypecheckReason, hasChangedTsFile, parseChangedFiles, pickTypecheckKind, resolveTypecheckMode, } from "../lib/typecheck-gate.mjs";
|
|
27
|
+
const SPAWN_TIMEOUT_MS = 25_000;
|
|
28
|
+
function resolveProjectRoot() {
|
|
29
|
+
const fromEnv = process.env.CLAUDE_PROJECT_DIR;
|
|
30
|
+
if (fromEnv && fromEnv.trim())
|
|
31
|
+
return fromEnv.trim();
|
|
32
|
+
try {
|
|
33
|
+
const root = execFileSync("git", ["rev-parse", "--show-toplevel"], {
|
|
34
|
+
encoding: "utf8",
|
|
35
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
36
|
+
}).trim();
|
|
37
|
+
return root || null;
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function collectChangedFiles(root) {
|
|
44
|
+
const run = (args) => {
|
|
45
|
+
try {
|
|
46
|
+
return execFileSync("git", args, { cwd: root, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return "";
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
const unstaged = run(["diff", "--name-only", "--diff-filter=ACMR"]);
|
|
53
|
+
const staged = run(["diff", "--cached", "--name-only", "--diff-filter=ACMR"]);
|
|
54
|
+
return [...parseChangedFiles(unstaged), ...parseChangedFiles(staged)];
|
|
55
|
+
}
|
|
56
|
+
function hasNpmTypecheckScript(root) {
|
|
57
|
+
try {
|
|
58
|
+
const pkg = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
|
|
59
|
+
return typeof pkg.scripts?.typecheck === "string";
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function localTscPath(root) {
|
|
66
|
+
// node_modules/typescript/bin/tsc is a Node script — invoking it via `node`
|
|
67
|
+
// is cross-platform, unlike node_modules/.bin/tsc (a shell script / .cmd).
|
|
68
|
+
const path = join(root, "node_modules", "typescript", "bin", "tsc");
|
|
69
|
+
return existsSync(path) ? path : null;
|
|
70
|
+
}
|
|
71
|
+
function runTypecheck(root) {
|
|
72
|
+
const tscPath = localTscPath(root);
|
|
73
|
+
const kind = pickTypecheckKind(hasNpmTypecheckScript(root), tscPath !== null);
|
|
74
|
+
if (!kind)
|
|
75
|
+
return { ran: false, rc: 0, output: "" };
|
|
76
|
+
const result = kind === "npm"
|
|
77
|
+
? spawnSync("npm", ["run", "--silent", "typecheck"], {
|
|
78
|
+
cwd: root,
|
|
79
|
+
encoding: "utf8",
|
|
80
|
+
shell: true,
|
|
81
|
+
timeout: SPAWN_TIMEOUT_MS,
|
|
82
|
+
})
|
|
83
|
+
: spawnSync(process.execPath, [tscPath, "--noEmit"], {
|
|
84
|
+
cwd: root,
|
|
85
|
+
encoding: "utf8",
|
|
86
|
+
timeout: SPAWN_TIMEOUT_MS,
|
|
87
|
+
});
|
|
88
|
+
// A timeout or spawn failure leaves error set / status null — inconclusive, so
|
|
89
|
+
// fail open (do not block on a check that never produced a verdict).
|
|
90
|
+
if (result.error || typeof result.status !== "number")
|
|
91
|
+
return { ran: false, rc: 0, output: "" };
|
|
92
|
+
return { ran: true, rc: result.status, output: `${result.stdout ?? ""}${result.stderr ?? ""}` };
|
|
93
|
+
}
|
|
94
|
+
function main() {
|
|
95
|
+
const mode = resolveTypecheckMode(process.env.CLAUDE_TYPECHECK_GATE);
|
|
96
|
+
if (mode === "off")
|
|
97
|
+
return;
|
|
98
|
+
const root = resolveProjectRoot();
|
|
99
|
+
if (!root || !existsSync(join(root, "tsconfig.json")))
|
|
100
|
+
return; // not a TS project
|
|
101
|
+
if (!hasChangedTsFile(collectChangedFiles(root)))
|
|
102
|
+
return; // nothing TS changed — near-zero cost
|
|
103
|
+
const { ran, rc, output } = runTypecheck(root);
|
|
104
|
+
const action = decideTypecheckAction({ mode, ranTypecheck: ran, rc });
|
|
105
|
+
if (action === "block") {
|
|
106
|
+
process.stdout.write(`${JSON.stringify({ decision: "block", reason: formatTypecheckReason(output) })}\n`);
|
|
107
|
+
}
|
|
108
|
+
else if (action === "warn") {
|
|
109
|
+
process.stderr.write(`[continuous-improvement] typecheck: ${formatTypecheckReason(output)}\n`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
try {
|
|
113
|
+
main();
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
// fail open — never trap a turn on a hook bug
|
|
117
|
+
}
|
package/lib/plugin-metadata.mjs
CHANGED
|
@@ -497,6 +497,19 @@ export function getPluginHooksConfig() {
|
|
|
497
497
|
command: "node \"${CLAUDE_PLUGIN_ROOT}/hooks/workflow-distill.mjs\"",
|
|
498
498
|
timeout: 5,
|
|
499
499
|
};
|
|
500
|
+
const typecheckStopCommand = {
|
|
501
|
+
type: "command",
|
|
502
|
+
command: "node \"${CLAUDE_PLUGIN_ROOT}/hooks/typecheck-stop.mjs\"",
|
|
503
|
+
// Longer than the 5s hooks: tsc is slower. Opt-in via CLAUDE_TYPECHECK_GATE
|
|
504
|
+
// (off by default) and near-zero cost when off / no TS file changed; on an
|
|
505
|
+
// internal timeout it fails open (allow) rather than blocking.
|
|
506
|
+
timeout: 30,
|
|
507
|
+
};
|
|
508
|
+
const queryCostNudgeCommand = {
|
|
509
|
+
type: "command",
|
|
510
|
+
command: "node \"${CLAUDE_PLUGIN_ROOT}/hooks/query-cost-nudge.mjs\"",
|
|
511
|
+
timeout: 5,
|
|
512
|
+
};
|
|
500
513
|
const routePromptCommand = {
|
|
501
514
|
type: "command",
|
|
502
515
|
command: "node \"${CLAUDE_PLUGIN_ROOT}/hooks/route-prompt.mjs\"",
|
|
@@ -508,7 +521,7 @@ export function getPluginHooksConfig() {
|
|
|
508
521
|
timeout: 5,
|
|
509
522
|
};
|
|
510
523
|
return {
|
|
511
|
-
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, 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.",
|
|
512
525
|
hooks: {
|
|
513
526
|
// gateguard runs FIRST on PreToolUse so its block decision short-circuits
|
|
514
527
|
// before companion-preference sees the call. companion-preference runs
|
|
@@ -532,7 +545,7 @@ export function getPluginHooksConfig() {
|
|
|
532
545
|
UserPromptSubmit: [{ hooks: [routePromptCommand, recallBriefingCommand] }],
|
|
533
546
|
SessionStart: [{ hooks: [sessionCommand] }],
|
|
534
547
|
SessionEnd: [{ hooks: [sessionCommand] }],
|
|
535
|
-
Stop: [{ hooks: [threeSectionCloseCommand, goalDriftStopCommand, workflowDistillCommand] }],
|
|
548
|
+
Stop: [{ hooks: [threeSectionCloseCommand, goalDriftStopCommand, workflowDistillCommand, typecheckStopCommand, queryCostNudgeCommand] }],
|
|
536
549
|
},
|
|
537
550
|
};
|
|
538
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
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure decision helpers for the typecheck Stop gate (RISA 4 / G4).
|
|
3
|
+
*
|
|
4
|
+
* The hook (src/hooks/typecheck-stop.mts) does the I/O — git, spawning the
|
|
5
|
+
* typecheck, emitting the decision. Everything here is pure and unit-tested so
|
|
6
|
+
* the mode/selection/formatting logic is provable without spawning a compiler.
|
|
7
|
+
*
|
|
8
|
+
* Mode via CLAUDE_TYPECHECK_GATE: "off" (default) | "warn" | "block".
|
|
9
|
+
* - off : no-op. The default. The global ~/.claude typecheck-changed.sh
|
|
10
|
+
* already emits an advisory systemMessage; this hook stays silent
|
|
11
|
+
* until opted in, so there is no double-advisory and no regression.
|
|
12
|
+
* - warn : print a one-line notice to stderr; never blocks.
|
|
13
|
+
* - block : emit {"decision":"block","reason":...} so the failure re-enters
|
|
14
|
+
* model context — the fix headless/autonomous -p loops need.
|
|
15
|
+
*/
|
|
16
|
+
export function resolveTypecheckMode(raw) {
|
|
17
|
+
const value = (raw ?? "").trim().toLowerCase();
|
|
18
|
+
return value === "block" || value === "warn" ? value : "off";
|
|
19
|
+
}
|
|
20
|
+
// Matches the extensions typecheck-changed.sh screens for. `.d.ts` ends in
|
|
21
|
+
// `.ts` so it counts — a changed ambient declaration can still break the build.
|
|
22
|
+
const TS_FILE_RE = /\.(ts|tsx|mts|cts)$/;
|
|
23
|
+
export function hasChangedTsFile(files) {
|
|
24
|
+
return files.some((file) => TS_FILE_RE.test(file.trim()));
|
|
25
|
+
}
|
|
26
|
+
// Split a `git diff --name-only` blob into a clean path list (drops blank lines).
|
|
27
|
+
export function parseChangedFiles(gitOutput) {
|
|
28
|
+
return (gitOutput ?? "")
|
|
29
|
+
.split(/\r?\n/)
|
|
30
|
+
.map((line) => line.trim())
|
|
31
|
+
.filter((line) => line.length > 0);
|
|
32
|
+
}
|
|
33
|
+
// Prefer the project's own `typecheck` npm script (authoritative — it encodes
|
|
34
|
+
// the project's exact flags), else a local tsc, else null (a TS project with no
|
|
35
|
+
// runnable typecheck stays silent rather than guessing).
|
|
36
|
+
export function pickTypecheckKind(hasNpmTypecheckScript, hasLocalTsc) {
|
|
37
|
+
if (hasNpmTypecheckScript)
|
|
38
|
+
return "npm";
|
|
39
|
+
if (hasLocalTsc)
|
|
40
|
+
return "tsc";
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
export function formatTypecheckReason(output, maxLines = 15) {
|
|
44
|
+
const tail = (output ?? "")
|
|
45
|
+
.split(/\r?\n/)
|
|
46
|
+
.filter((line) => line.length > 0)
|
|
47
|
+
.slice(-maxLines)
|
|
48
|
+
.join("\n");
|
|
49
|
+
return `Typecheck FAILED on changed TS files — fix before ending the turn:\n${tail}`;
|
|
50
|
+
}
|
|
51
|
+
// The single decision point. `ranTypecheck` is false when the gate short-circuited
|
|
52
|
+
// (mode off, no TS project, no changed TS file, no runnable typecheck, or the run
|
|
53
|
+
// timed out) — all of which allow. A conclusive non-zero rc maps to the mode.
|
|
54
|
+
export function decideTypecheckAction(input) {
|
|
55
|
+
if (input.mode === "off")
|
|
56
|
+
return "allow";
|
|
57
|
+
if (!input.ranTypecheck)
|
|
58
|
+
return "allow";
|
|
59
|
+
if (input.rc === 0)
|
|
60
|
+
return "allow";
|
|
61
|
+
return input.mode;
|
|
62
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "continuous-improvement",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.20.0",
|
|
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",
|
package/plugins/beginner.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "continuous-improvement",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.20.0",
|
|
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.
|
|
11
|
+
"version": "3.20.0",
|
|
12
12
|
"source": "./",
|
|
13
13
|
"author": {
|
|
14
14
|
"name": "naimkatiman"
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "continuous-improvement",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.20.0",
|
|
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",
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"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, and UserPromptSubmit lazy-routing plus opt-in proactive recall-briefing hooks for continuous-improvement.",
|
|
2
|
+
"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.",
|
|
3
3
|
"hooks": {
|
|
4
4
|
"PreToolUse": [
|
|
5
5
|
{
|
|
@@ -93,6 +93,16 @@
|
|
|
93
93
|
"type": "command",
|
|
94
94
|
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/workflow-distill.mjs\"",
|
|
95
95
|
"timeout": 5
|
|
96
|
+
},
|
|
97
|
+
{
|
|
98
|
+
"type": "command",
|
|
99
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/typecheck-stop.mjs\"",
|
|
100
|
+
"timeout": 30
|
|
101
|
+
},
|
|
102
|
+
{
|
|
103
|
+
"type": "command",
|
|
104
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/query-cost-nudge.mjs\"",
|
|
105
|
+
"timeout": 5
|
|
96
106
|
}
|
|
97
107
|
]
|
|
98
108
|
}
|
|
@@ -0,0 +1,113 @@
|
|
|
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
|
+
];
|
|
65
|
+
}
|
|
66
|
+
// Marker key: the sanitized session_id when present (the normal case), else a
|
|
67
|
+
// per-day key so a missing id self-heals daily instead of blocking forever.
|
|
68
|
+
function markerKey(sessionId) {
|
|
69
|
+
const sanitized = (sessionId ?? "").replace(/[^A-Za-z0-9_-]/g, "_").slice(0, 64);
|
|
70
|
+
return sanitized || `day-${new Date().toISOString().slice(0, 10)}`;
|
|
71
|
+
}
|
|
72
|
+
function markerPath(home, projectRoot, sessionId) {
|
|
73
|
+
const hash = createHash("sha256").update(projectRoot).digest("hex").slice(0, 12);
|
|
74
|
+
return join(home, ".claude", "instincts", hash, "query-cost-nudge", `${markerKey(sessionId)}.nudged`);
|
|
75
|
+
}
|
|
76
|
+
function main() {
|
|
77
|
+
if (resolveQueryCostNudge(process.env.CLAUDE_QUERY_COST_NUDGE) === "off")
|
|
78
|
+
return;
|
|
79
|
+
let sessionId;
|
|
80
|
+
try {
|
|
81
|
+
const payload = JSON.parse(readStdin());
|
|
82
|
+
if (typeof payload.session_id === "string")
|
|
83
|
+
sessionId = payload.session_id;
|
|
84
|
+
}
|
|
85
|
+
catch {
|
|
86
|
+
// stdin optional — proceed without a session id (per-day dedup)
|
|
87
|
+
}
|
|
88
|
+
const root = resolveProjectRoot();
|
|
89
|
+
if (root === "global")
|
|
90
|
+
return; // no repo → nothing to diff
|
|
91
|
+
const changed = changedQueryPaths(collectChangedFiles(root));
|
|
92
|
+
if (changed.length === 0)
|
|
93
|
+
return;
|
|
94
|
+
const marker = markerPath(resolveHome(), root, sessionId);
|
|
95
|
+
try {
|
|
96
|
+
if (existsSync(marker))
|
|
97
|
+
return; // already nudged this session
|
|
98
|
+
mkdirSync(dirname(marker), { recursive: true });
|
|
99
|
+
writeFileSync(marker, `${new Date().toISOString()}\n`);
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
// if the marker can't be written, nudge anyway (no dedup) rather than stay silent
|
|
103
|
+
}
|
|
104
|
+
process.stdout.write(`${JSON.stringify({
|
|
105
|
+
hookSpecificOutput: { hookEventName: "Stop", additionalContext: buildQueryCostReminder(changed) },
|
|
106
|
+
})}\n`);
|
|
107
|
+
}
|
|
108
|
+
try {
|
|
109
|
+
main();
|
|
110
|
+
}
|
|
111
|
+
catch {
|
|
112
|
+
// fail open — never trap a turn on a hook bug
|
|
113
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* typecheck-stop.mts — Stop hook that runs the project typecheck on changed TS
|
|
4
|
+
* files and, when opted in, returns a block decision so the failure re-enters
|
|
5
|
+
* model context and the agent fixes it before ending the turn — in all modes,
|
|
6
|
+
* including headless `-p` runs where Stop still fires (RISA 4 / G4).
|
|
7
|
+
*
|
|
8
|
+
* Ports the proven detection logic of ~/.claude/scripts/typecheck-changed.sh
|
|
9
|
+
* (skip non-TS repos, skip TS repos with no changed TS file, prefer the npm
|
|
10
|
+
* `typecheck` script) and adds the block mechanism of goal-drift-stop.mts.
|
|
11
|
+
*
|
|
12
|
+
* Mode via CLAUDE_TYPECHECK_GATE: "off" (default) | "warn" | "block".
|
|
13
|
+
* off : no-op (the global script's advisory systemMessage stays the default
|
|
14
|
+
* layer; this hook is the opt-in enforcement layer — zero regression).
|
|
15
|
+
* warn : one-line stderr notice; never blocks.
|
|
16
|
+
* block : {"decision":"block","reason":...} to re-prompt the model.
|
|
17
|
+
*
|
|
18
|
+
* Fail-open by construction: any error, missing root, non-TS repo, no changed
|
|
19
|
+
* TS file, no runnable typecheck, or a run that times out exits 0 (allow). No
|
|
20
|
+
* network. The wiring gives this hook a longer timeout than the 5s hooks
|
|
21
|
+
* because `tsc` is slower; on the internal timeout it fails open.
|
|
22
|
+
*/
|
|
23
|
+
import { execFileSync, spawnSync } from "node:child_process";
|
|
24
|
+
import { existsSync, readFileSync } from "node:fs";
|
|
25
|
+
import { join } from "node:path";
|
|
26
|
+
import { decideTypecheckAction, formatTypecheckReason, hasChangedTsFile, parseChangedFiles, pickTypecheckKind, resolveTypecheckMode, } from "../lib/typecheck-gate.mjs";
|
|
27
|
+
const SPAWN_TIMEOUT_MS = 25_000;
|
|
28
|
+
function resolveProjectRoot() {
|
|
29
|
+
const fromEnv = process.env.CLAUDE_PROJECT_DIR;
|
|
30
|
+
if (fromEnv && fromEnv.trim())
|
|
31
|
+
return fromEnv.trim();
|
|
32
|
+
try {
|
|
33
|
+
const root = execFileSync("git", ["rev-parse", "--show-toplevel"], {
|
|
34
|
+
encoding: "utf8",
|
|
35
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
36
|
+
}).trim();
|
|
37
|
+
return root || null;
|
|
38
|
+
}
|
|
39
|
+
catch {
|
|
40
|
+
return null;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
function collectChangedFiles(root) {
|
|
44
|
+
const run = (args) => {
|
|
45
|
+
try {
|
|
46
|
+
return execFileSync("git", args, { cwd: root, encoding: "utf8", stdio: ["ignore", "pipe", "ignore"] });
|
|
47
|
+
}
|
|
48
|
+
catch {
|
|
49
|
+
return "";
|
|
50
|
+
}
|
|
51
|
+
};
|
|
52
|
+
const unstaged = run(["diff", "--name-only", "--diff-filter=ACMR"]);
|
|
53
|
+
const staged = run(["diff", "--cached", "--name-only", "--diff-filter=ACMR"]);
|
|
54
|
+
return [...parseChangedFiles(unstaged), ...parseChangedFiles(staged)];
|
|
55
|
+
}
|
|
56
|
+
function hasNpmTypecheckScript(root) {
|
|
57
|
+
try {
|
|
58
|
+
const pkg = JSON.parse(readFileSync(join(root, "package.json"), "utf8"));
|
|
59
|
+
return typeof pkg.scripts?.typecheck === "string";
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return false;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function localTscPath(root) {
|
|
66
|
+
// node_modules/typescript/bin/tsc is a Node script — invoking it via `node`
|
|
67
|
+
// is cross-platform, unlike node_modules/.bin/tsc (a shell script / .cmd).
|
|
68
|
+
const path = join(root, "node_modules", "typescript", "bin", "tsc");
|
|
69
|
+
return existsSync(path) ? path : null;
|
|
70
|
+
}
|
|
71
|
+
function runTypecheck(root) {
|
|
72
|
+
const tscPath = localTscPath(root);
|
|
73
|
+
const kind = pickTypecheckKind(hasNpmTypecheckScript(root), tscPath !== null);
|
|
74
|
+
if (!kind)
|
|
75
|
+
return { ran: false, rc: 0, output: "" };
|
|
76
|
+
const result = kind === "npm"
|
|
77
|
+
? spawnSync("npm", ["run", "--silent", "typecheck"], {
|
|
78
|
+
cwd: root,
|
|
79
|
+
encoding: "utf8",
|
|
80
|
+
shell: true,
|
|
81
|
+
timeout: SPAWN_TIMEOUT_MS,
|
|
82
|
+
})
|
|
83
|
+
: spawnSync(process.execPath, [tscPath, "--noEmit"], {
|
|
84
|
+
cwd: root,
|
|
85
|
+
encoding: "utf8",
|
|
86
|
+
timeout: SPAWN_TIMEOUT_MS,
|
|
87
|
+
});
|
|
88
|
+
// A timeout or spawn failure leaves error set / status null — inconclusive, so
|
|
89
|
+
// fail open (do not block on a check that never produced a verdict).
|
|
90
|
+
if (result.error || typeof result.status !== "number")
|
|
91
|
+
return { ran: false, rc: 0, output: "" };
|
|
92
|
+
return { ran: true, rc: result.status, output: `${result.stdout ?? ""}${result.stderr ?? ""}` };
|
|
93
|
+
}
|
|
94
|
+
function main() {
|
|
95
|
+
const mode = resolveTypecheckMode(process.env.CLAUDE_TYPECHECK_GATE);
|
|
96
|
+
if (mode === "off")
|
|
97
|
+
return;
|
|
98
|
+
const root = resolveProjectRoot();
|
|
99
|
+
if (!root || !existsSync(join(root, "tsconfig.json")))
|
|
100
|
+
return; // not a TS project
|
|
101
|
+
if (!hasChangedTsFile(collectChangedFiles(root)))
|
|
102
|
+
return; // nothing TS changed — near-zero cost
|
|
103
|
+
const { ran, rc, output } = runTypecheck(root);
|
|
104
|
+
const action = decideTypecheckAction({ mode, ranTypecheck: ran, rc });
|
|
105
|
+
if (action === "block") {
|
|
106
|
+
process.stdout.write(`${JSON.stringify({ decision: "block", reason: formatTypecheckReason(output) })}\n`);
|
|
107
|
+
}
|
|
108
|
+
else if (action === "warn") {
|
|
109
|
+
process.stderr.write(`[continuous-improvement] typecheck: ${formatTypecheckReason(output)}\n`);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
try {
|
|
113
|
+
main();
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
// fail open — never trap a turn on a hook bug
|
|
117
|
+
}
|
|
@@ -497,6 +497,19 @@ export function getPluginHooksConfig() {
|
|
|
497
497
|
command: "node \"${CLAUDE_PLUGIN_ROOT}/hooks/workflow-distill.mjs\"",
|
|
498
498
|
timeout: 5,
|
|
499
499
|
};
|
|
500
|
+
const typecheckStopCommand = {
|
|
501
|
+
type: "command",
|
|
502
|
+
command: "node \"${CLAUDE_PLUGIN_ROOT}/hooks/typecheck-stop.mjs\"",
|
|
503
|
+
// Longer than the 5s hooks: tsc is slower. Opt-in via CLAUDE_TYPECHECK_GATE
|
|
504
|
+
// (off by default) and near-zero cost when off / no TS file changed; on an
|
|
505
|
+
// internal timeout it fails open (allow) rather than blocking.
|
|
506
|
+
timeout: 30,
|
|
507
|
+
};
|
|
508
|
+
const queryCostNudgeCommand = {
|
|
509
|
+
type: "command",
|
|
510
|
+
command: "node \"${CLAUDE_PLUGIN_ROOT}/hooks/query-cost-nudge.mjs\"",
|
|
511
|
+
timeout: 5,
|
|
512
|
+
};
|
|
500
513
|
const routePromptCommand = {
|
|
501
514
|
type: "command",
|
|
502
515
|
command: "node \"${CLAUDE_PLUGIN_ROOT}/hooks/route-prompt.mjs\"",
|
|
@@ -508,7 +521,7 @@ export function getPluginHooksConfig() {
|
|
|
508
521
|
timeout: 5,
|
|
509
522
|
};
|
|
510
523
|
return {
|
|
511
|
-
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, 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.",
|
|
512
525
|
hooks: {
|
|
513
526
|
// gateguard runs FIRST on PreToolUse so its block decision short-circuits
|
|
514
527
|
// before companion-preference sees the call. companion-preference runs
|
|
@@ -532,7 +545,7 @@ export function getPluginHooksConfig() {
|
|
|
532
545
|
UserPromptSubmit: [{ hooks: [routePromptCommand, recallBriefingCommand] }],
|
|
533
546
|
SessionStart: [{ hooks: [sessionCommand] }],
|
|
534
547
|
SessionEnd: [{ hooks: [sessionCommand] }],
|
|
535
|
-
Stop: [{ hooks: [threeSectionCloseCommand, goalDriftStopCommand, workflowDistillCommand] }],
|
|
548
|
+
Stop: [{ hooks: [threeSectionCloseCommand, goalDriftStopCommand, workflowDistillCommand, typecheckStopCommand, queryCostNudgeCommand] }],
|
|
536
549
|
},
|
|
537
550
|
};
|
|
538
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
|
+
}
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure decision helpers for the typecheck Stop gate (RISA 4 / G4).
|
|
3
|
+
*
|
|
4
|
+
* The hook (src/hooks/typecheck-stop.mts) does the I/O — git, spawning the
|
|
5
|
+
* typecheck, emitting the decision. Everything here is pure and unit-tested so
|
|
6
|
+
* the mode/selection/formatting logic is provable without spawning a compiler.
|
|
7
|
+
*
|
|
8
|
+
* Mode via CLAUDE_TYPECHECK_GATE: "off" (default) | "warn" | "block".
|
|
9
|
+
* - off : no-op. The default. The global ~/.claude typecheck-changed.sh
|
|
10
|
+
* already emits an advisory systemMessage; this hook stays silent
|
|
11
|
+
* until opted in, so there is no double-advisory and no regression.
|
|
12
|
+
* - warn : print a one-line notice to stderr; never blocks.
|
|
13
|
+
* - block : emit {"decision":"block","reason":...} so the failure re-enters
|
|
14
|
+
* model context — the fix headless/autonomous -p loops need.
|
|
15
|
+
*/
|
|
16
|
+
export function resolveTypecheckMode(raw) {
|
|
17
|
+
const value = (raw ?? "").trim().toLowerCase();
|
|
18
|
+
return value === "block" || value === "warn" ? value : "off";
|
|
19
|
+
}
|
|
20
|
+
// Matches the extensions typecheck-changed.sh screens for. `.d.ts` ends in
|
|
21
|
+
// `.ts` so it counts — a changed ambient declaration can still break the build.
|
|
22
|
+
const TS_FILE_RE = /\.(ts|tsx|mts|cts)$/;
|
|
23
|
+
export function hasChangedTsFile(files) {
|
|
24
|
+
return files.some((file) => TS_FILE_RE.test(file.trim()));
|
|
25
|
+
}
|
|
26
|
+
// Split a `git diff --name-only` blob into a clean path list (drops blank lines).
|
|
27
|
+
export function parseChangedFiles(gitOutput) {
|
|
28
|
+
return (gitOutput ?? "")
|
|
29
|
+
.split(/\r?\n/)
|
|
30
|
+
.map((line) => line.trim())
|
|
31
|
+
.filter((line) => line.length > 0);
|
|
32
|
+
}
|
|
33
|
+
// Prefer the project's own `typecheck` npm script (authoritative — it encodes
|
|
34
|
+
// the project's exact flags), else a local tsc, else null (a TS project with no
|
|
35
|
+
// runnable typecheck stays silent rather than guessing).
|
|
36
|
+
export function pickTypecheckKind(hasNpmTypecheckScript, hasLocalTsc) {
|
|
37
|
+
if (hasNpmTypecheckScript)
|
|
38
|
+
return "npm";
|
|
39
|
+
if (hasLocalTsc)
|
|
40
|
+
return "tsc";
|
|
41
|
+
return null;
|
|
42
|
+
}
|
|
43
|
+
export function formatTypecheckReason(output, maxLines = 15) {
|
|
44
|
+
const tail = (output ?? "")
|
|
45
|
+
.split(/\r?\n/)
|
|
46
|
+
.filter((line) => line.length > 0)
|
|
47
|
+
.slice(-maxLines)
|
|
48
|
+
.join("\n");
|
|
49
|
+
return `Typecheck FAILED on changed TS files — fix before ending the turn:\n${tail}`;
|
|
50
|
+
}
|
|
51
|
+
// The single decision point. `ranTypecheck` is false when the gate short-circuited
|
|
52
|
+
// (mode off, no TS project, no changed TS file, no runnable typecheck, or the run
|
|
53
|
+
// timed out) — all of which allow. A conclusive non-zero rc maps to the mode.
|
|
54
|
+
export function decideTypecheckAction(input) {
|
|
55
|
+
if (input.mode === "off")
|
|
56
|
+
return "allow";
|
|
57
|
+
if (!input.ranTypecheck)
|
|
58
|
+
return "allow";
|
|
59
|
+
if (input.rc === 0)
|
|
60
|
+
return "allow";
|
|
61
|
+
return input.mode;
|
|
62
|
+
}
|
package/plugins/expert.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "continuous-improvement",
|
|
3
|
-
"version": "3.
|
|
3
|
+
"version": "3.20.0",
|
|
4
4
|
"mode": "expert",
|
|
5
5
|
"description": "Expert mode: tune confidence, manage instincts, and persist plans on disk. Adds safety, token-budget, and strategic-compact skills plus the /learn-eval command so long sessions stay sharp and learnings survive context resets.",
|
|
6
6
|
"tools": [
|