continuous-improvement 3.19.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.
@@ -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.19.0",
11
+ "version": "3.20.0",
12
12
  "source": "./plugins/continuous-improvement",
13
13
  "author": {
14
14
  "name": "naimkatiman"
package/README.md CHANGED
@@ -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
 
@@ -157,6 +157,7 @@ async function writePluginBundle() {
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
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")),
160
161
  copyFileTo(join(REPO_ROOT, "LICENSE"), join(PLUGIN_BUNDLE_DIR, "LICENSE")),
161
162
  writePluginBundleReadme(),
162
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
+ }
@@ -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
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "continuous-improvement",
3
- "version": "3.19.0",
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",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "continuous-improvement",
3
- "version": "3.19.0",
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.19.0",
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.19.0",
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, opt-in typecheck Stop gate, 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
  {
@@ -98,6 +98,11 @@
98
98
  "type": "command",
99
99
  "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/typecheck-stop.mjs\"",
100
100
  "timeout": 30
101
+ },
102
+ {
103
+ "type": "command",
104
+ "command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/query-cost-nudge.mjs\"",
105
+ "timeout": 5
101
106
  }
102
107
  ]
103
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
+ }
@@ -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
+ }
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "continuous-improvement",
3
- "version": "3.19.0",
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": [