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.
Files changed (58) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/QUICKSTART.md +1 -1
  3. package/README.md +3 -2
  4. package/bin/check-landing-version.mjs +63 -0
  5. package/bin/check-scripts-citation-drift.mjs +61 -13
  6. package/bin/generate-plugin-manifests.mjs +3 -0
  7. package/bin/install.mjs +19 -11
  8. package/commands/verify-install.md +1 -1
  9. package/hooks/gateguard.mjs +22 -3
  10. package/hooks/query-cost-nudge.mjs +114 -0
  11. package/hooks/typecheck-stop.mjs +2 -1
  12. package/lib/plugin-metadata.mjs +7 -2
  13. package/lib/query-cost-gate.mjs +53 -0
  14. package/package.json +5 -3
  15. package/plugins/beginner.json +1 -1
  16. package/plugins/continuous-improvement/.claude-plugin/marketplace.json +1 -1
  17. package/plugins/continuous-improvement/.claude-plugin/plugin.json +1 -1
  18. package/plugins/continuous-improvement/README.md +1 -0
  19. package/plugins/continuous-improvement/commands/verify-install.md +1 -1
  20. package/plugins/continuous-improvement/hooks/gateguard.mjs +22 -3
  21. package/plugins/continuous-improvement/hooks/hooks.json +6 -1
  22. package/plugins/continuous-improvement/hooks/query-cost-nudge.mjs +114 -0
  23. package/plugins/continuous-improvement/hooks/typecheck-stop.mjs +2 -1
  24. package/plugins/continuous-improvement/lib/plugin-metadata.mjs +7 -2
  25. package/plugins/continuous-improvement/lib/query-cost-gate.mjs +53 -0
  26. package/plugins/continuous-improvement/scripts/README.md +33 -0
  27. package/plugins/continuous-improvement/scripts/detect-deploy-target.sh +66 -0
  28. package/plugins/continuous-improvement/scripts/get-deployed-sha.sh +113 -0
  29. package/plugins/continuous-improvement/scripts/git-state-snapshot.sh +48 -0
  30. package/plugins/continuous-improvement/scripts/resolve-verify-ladder.mjs +241 -0
  31. package/plugins/continuous-improvement/scripts/route-recommendation.mjs +178 -0
  32. package/plugins/continuous-improvement/scripts/route-recommendation.routes.json +213 -0
  33. package/plugins/continuous-improvement/scripts/run-synthetic.mjs +298 -0
  34. package/plugins/continuous-improvement/scripts/scan-past-mistakes.mjs +285 -0
  35. package/plugins/continuous-improvement/skills/deploy-receipt/SKILL.md +2 -2
  36. package/plugins/continuous-improvement/skills/gateguard/SKILL.md +2 -2
  37. package/plugins/continuous-improvement/skills/proceed-with-the-recommendation/SKILL.md +2 -2
  38. package/plugins/continuous-improvement/skills/reconcile/SKILL.md +1 -1
  39. package/plugins/continuous-improvement/skills/verification-loop/SKILL.md +5 -5
  40. package/plugins/continuous-improvement/skills/workspace-surface-audit/SKILL.md +1 -1
  41. package/plugins/continuous-improvement/skills/worktree-safety/SKILL.md +1 -1
  42. package/plugins/expert.json +1 -1
  43. package/scripts/README.md +33 -0
  44. package/scripts/detect-deploy-target.sh +66 -0
  45. package/scripts/get-deployed-sha.sh +113 -0
  46. package/scripts/git-state-snapshot.sh +48 -0
  47. package/scripts/resolve-verify-ladder.mjs +241 -0
  48. package/scripts/route-recommendation.mjs +178 -0
  49. package/scripts/route-recommendation.routes.json +213 -0
  50. package/scripts/run-synthetic.mjs +298 -0
  51. package/scripts/scan-past-mistakes.mjs +285 -0
  52. package/skills/deploy-receipt.md +2 -2
  53. package/skills/gateguard.md +2 -2
  54. package/skills/proceed-with-the-recommendation.md +2 -2
  55. package/skills/reconcile.md +1 -1
  56. package/skills/verification-loop.md +5 -5
  57. package/skills/workspace-surface-audit.md +1 -1
  58. package/skills/worktree-safety.md +1 -1
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "continuous-improvement",
3
- "version": "3.19.0",
3
+ "version": "3.20.4",
4
4
  "description": "Claude Code that gets sharper every session: the persistent-memory and runtime-discipline layer built on the 7 Laws of AI Agent Discipline. It grounds every edit in real facts before it lands and, through the Mulahazah engine, turns each fix into a reusable instinct, so a lesson learned once is applied automatically next time with no re-teaching. Shipped as 27 bundled skills, instinct-aware hooks, an MCP toolset for recall and reflection, and a GitHub Action transcript linter that feeds real work history back into sharper instincts. Beginner: one /plugin install command. Expert: adds MCP tools and session hooks.",
5
5
  "keywords": [
6
6
  "claude-code",
@@ -56,10 +56,11 @@
56
56
  "verify:routing-targets": "node bin/check-routing-targets.mjs",
57
57
  "verify:doc-runtime-claims": "node bin/check-doc-runtime-claims.mjs",
58
58
  "verify:test-imports-only": "node bin/check-test-imports-only.mjs",
59
- "verify:scripts-citation-drift": "node bin/check-scripts-citation-drift.mjs",
59
+ "verify:landing-version": "node bin/check-landing-version.mjs",
60
+ "verify:scripts-citation-drift": "node bin/check-scripts-citation-drift.mjs && node bin/check-scripts-citation-drift.mjs plugins/continuous-improvement",
60
61
  "verify:third-party-shape": "node bin/check-third-party-shape.mjs",
61
62
  "verify:tool-count": "node bin/check-tool-count.mjs",
62
- "verify:all": "npm run verify:skill-mirror && npm run verify:skill-tiers && npm run verify:skill-law-tag && npm run verify:skill-count && npm run verify:skill-count-prose && npm run verify:command-count && npm run verify:docs-substrings && npm run verify:everything-mirror && npm run verify:routing-targets && npm run verify:doc-runtime-claims && npm run verify:test-imports-only && npm run verify:scripts-citation-drift && npm run verify:third-party-shape && npm run verify:tool-count && npm run typecheck"
63
+ "verify:all": "npm run verify:skill-mirror && npm run verify:skill-tiers && npm run verify:skill-law-tag && npm run verify:skill-count && npm run verify:skill-count-prose && npm run verify:command-count && npm run verify:docs-substrings && npm run verify:everything-mirror && npm run verify:routing-targets && npm run verify:doc-runtime-claims && npm run verify:test-imports-only && npm run verify:landing-version && npm run verify:scripts-citation-drift && npm run verify:third-party-shape && npm run verify:tool-count && npm run typecheck"
63
64
  },
64
65
  "files": [
65
66
  ".claude-plugin/",
@@ -74,6 +75,7 @@
74
75
  "hooks/",
75
76
  "commands/",
76
77
  "skills/",
78
+ "scripts/",
77
79
  "templates/",
78
80
  "plugins/",
79
81
  "instinct-packs/"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "continuous-improvement",
3
- "version": "3.19.0",
3
+ "version": "3.20.4",
4
4
  "mode": "beginner",
5
5
  "description": "Beginner mode: see what your agent learned, list its instincts, and request a session reflection. Bundles three grounding skills (gateguard, tdd-workflow, verification-loop) so research, memory, tests, and verification happen by default — every edit starts from facts, not guesses.",
6
6
  "tools": [
@@ -8,7 +8,7 @@
8
8
  {
9
9
  "name": "continuous-improvement",
10
10
  "description": "The persistent-memory and runtime-discipline layer for Claude Code. It remembers the corrections you already gave, grounds every edit in real facts before it lands, and — through the Mulahazah engine — turns each fix into a reusable instinct, so a lesson learned once is applied automatically next time with no re-teaching. Built on the 7 Laws of AI Agent Discipline (research, plan, verify, reflect, learn) and shipped as 27 bundled skills, instinct-aware hooks, an MCP toolset for recall and reflection, and a GitHub Action transcript linter that feeds real work history back into sharper instincts.",
11
- "version": "3.19.0",
11
+ "version": "3.20.4",
12
12
  "source": "./",
13
13
  "author": {
14
14
  "name": "naimkatiman"
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "continuous-improvement",
3
- "version": "3.19.0",
3
+ "version": "3.20.4",
4
4
  "description": "The persistent-memory and runtime-discipline layer for Claude Code. It remembers the corrections you already gave, grounds every edit in real facts before it lands, and — through the Mulahazah engine — turns each fix into a reusable instinct, so a lesson learned once is applied automatically next time with no re-teaching. Built on the 7 Laws of AI Agent Discipline (research, plan, verify, reflect, learn) and shipped as 27 bundled skills, instinct-aware hooks, an MCP toolset for recall and reflection, and a GitHub Action transcript linter that feeds real work history back into sharper instincts.",
5
5
  "author": {
6
6
  "name": "naimkatiman",
@@ -10,6 +10,7 @@ Included surfaces:
10
10
  - `commands/`
11
11
  - `agents/` — `code-reviewer`, `security-auditor`, `test-engineer` personas (auto-discovered Claude Code subagents; pattern from addy/agent-skills)
12
12
  - `hooks/`
13
+ - `scripts/`
13
14
  - `bin/mcp-server.mjs`
14
15
  - `bin/observe.mjs`
15
16
  - `bin/backfill.mjs`
@@ -41,7 +41,7 @@ The observation hook appends one row per tool call to
41
41
 
42
42
  - If it exists and has at least one row — capture is recording. Record `observe: ✓`.
43
43
  - If it is missing or empty — record `observe: ✗ (observation hook not recording —
44
- on Windows confirm Git Bash / WSL is installed, then re-run the installer)`.
44
+ on Windows confirm Git Bash is installed before WSL Bash on PATH, then re-run the installer)`.
45
45
 
46
46
  ## Report
47
47
 
@@ -210,6 +210,16 @@ function buildMutatingFileReason(toolName, filePaths, stateFilePath) {
210
210
  " `_gateguard_facts_presented: true`; Claude Code's strict schema rejects that, so use A or B.)",
211
211
  ].join("\n");
212
212
  }
213
+ function isGitBraceSelector(value) {
214
+ const selector = value.trim();
215
+ if (/^(?:u|upstream|push|-?\d+)$/i.test(selector))
216
+ return true;
217
+ if (/^(?:now|today|yesterday|tomorrow|noon|midnight|tea)$/i.test(selector))
218
+ return true;
219
+ if (/^\d{4}-\d{1,2}-\d{1,2}(?:[ T].*)?$/.test(selector))
220
+ return true;
221
+ return !selector.startsWith("#") && /(?:^|[.\s])ago$/i.test(selector);
222
+ }
213
223
  function findUnquotedBraceRef(command) {
214
224
  let quote = null;
215
225
  for (let i = 0; i < command.length; i++) {
@@ -224,17 +234,26 @@ function findUnquotedBraceRef(command) {
224
234
  continue;
225
235
  }
226
236
  if (ch === "@" && command[i + 1] === "{") {
237
+ const braceEnd = command.indexOf("}", i + 2);
238
+ if (braceEnd === -1)
239
+ continue;
240
+ const selector = command.slice(i + 2, braceEnd);
241
+ // Deny only recognized Git selector grammar. PowerShell hashtables can
242
+ // contain comments, quoted braces, or arbitrary key expressions.
243
+ if (!isGitBraceSelector(selector)) {
244
+ i = braceEnd;
245
+ continue;
246
+ }
227
247
  // Expand to the whitespace-delimited word that carries this @{ ref, then
228
248
  // single-quote that whole word in the suggested fix.
229
249
  let wordStart = i;
230
250
  while (wordStart > 0 && !/\s/.test(command[wordStart - 1]))
231
251
  wordStart--;
232
- let wordEnd = i;
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 braceEnd = command.indexOf("}", i);
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
  }
@@ -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,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
+ }
@@ -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
- return [...parseChangedFiles(unstaged), ...parseChangedFiles(staged)];
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 {
@@ -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
+ }
@@ -0,0 +1,33 @@
1
+ # `scripts/` — deterministic primitives skills cite
2
+
3
+ This directory holds small, hand-authored scripts that skills (and hooks) cite instead of restating fixed operations inline. The motivation is the skills audit's "deterministic vs non-deterministic" axis: when a step inside a skill is the same operation every time (a fixed git command, a regex parse, a path lookup), it does not belong in the LLM-driven part of the skill — it belongs here, where the script runs the same way every invocation, costs no tokens, and stays in one place so multiple skills can share it.
4
+
5
+ ## Conventions
6
+
7
+ - **Hand-authored, no `.mts` source.** Files in `scripts/` are not part of the `tsc` build pipeline (see [`CLAUDE.md`](../CLAUDE.md) → "Build pipeline"). The pipeline owns `bin/`, `lib/`, `test/`. `scripts/` is the bare-metal home for shell scripts and one-off Node utilities skills cite directly.
8
+ - **One concern per script.** Each script does one thing and prints either machine-readable output (JSON envelope, single value) or a stable fenced block. Skills consume the output; they do not re-derive it.
9
+ - **Cross-platform shape.** Bash scripts run via Git Bash on Windows, native bash on macOS/Linux. Tests for bash scripts skip when bash is not on PATH (see `src/test/hook.test.mts` for the established skip pattern).
10
+ - **Cite from skills.** When a skill needs a primitive, the skill body cites the script path (`scripts/<name>.<ext>`) and quotes the expected output shape once. The skill does not restate the script's inner mechanics.
11
+
12
+ ## Inventory
13
+
14
+ | Script | Purpose | Cited by |
15
+ |---|---|---|
16
+ | `git-state-snapshot.sh` | JSON envelope `{head, upstream, dirty, root, branch}` for the current git working tree | `skills/gateguard.md` (Parallel-Actor Gate), `skills/worktree-safety.md` (Root + branch), `skills/workspace-surface-audit.md` (Environment Grain — parallel-actor row), `skills/reconcile.md` (Detect a Concurrent Writer) |
17
+ | `detect-deploy-target.sh` | Detect the auto-deploy provider for the repo at the current working directory (or first arg). Prints one of `railway`/`cloudflare`/`vercel`/`netlify`/`fly`/`appengine`/`apprunner`/`gha-deploy`/`none`. Always exits 0. | `skills/verification-loop.md` (Phase 8 deploy-receipt handoff), `skills/deploy-receipt.md` (When to Activate gate) |
18
+ | `get-deployed-sha.sh` | Per-provider deployed-SHA extraction. Default mode runs the CLI; `--show-command <provider>` prints the pipeline shape without executing (useful for citation, dry-run, tests). | `skills/verification-loop.md` (Phase 8), `skills/deploy-receipt.md` (Route A — provider CLI extraction) |
19
+ | `resolve-verify-ladder.mjs` | Resolve the per-project verification ladder for Phase 0 of `verification-loop`. Encodes the four-step priority — manifest > package.json sniff > per-language toolchain > ask-operator. Default mode prints the fenced block the skill displays; `--json` mode emits a JSON object for machine consumption. | `skills/verification-loop.md` (Phase 0) |
20
+ | `scan-past-mistakes.mjs` | Scan the three Past-Mistake Acknowledgment Gate surfaces — `~/.claude/instincts/<hash>/observations.jsonl` (last N failure/correction rows), `~/.claude/projects/<hash>/memory/feedback_*.md`, and `<root>/CLAUDE.md` "## Past Mistakes" table. Active-in-scope judgment is the LLM's job; the script provides quotes + citations only. | `skills/proceed-with-the-recommendation.md` (Phase 0 Rule 1) |
21
+ | `route-recommendation.mjs` + `route-recommendation.routes.json` | Match a recommendation item to its preferred-skill chain + inline fallback from the Phase 3 routing table (29 rows). Data file is the programmatic source of truth; the skill's table is documentation that mirrors it. Default mode prints a match block; `--json` for machine consumption; `--list` enumerates all rows. | `skills/proceed-with-the-recommendation.md` (Phase 3 routing table) |
22
+ | `run-synthetic.mjs` | Phase 9 runner: invoke every `*.synthetic.{sh,mjs,ts,py}` in `synthetic-checks/`, inject `BASE_URL`/`BASELINE_URL`/`EXPECTED_SHA`/`DEPLOY_BRANCH`/`RECEIPT_TIMESTAMP` (unset vars pass through as `""`), capture stdout/stderr/exit per check, aggregate. Default mode prints the report block; `--json` for machine consumption; `--fail-fast` halts on first drift or timeout; `--timeout <sec>` per-check wall-clock cap; `--show-command` dry-run. Exit 0 all-pass, 1 drift, 2 config error, 3 usage. | `skills/verification-loop.md` (Phase 9 — production-vs-baseline diff) |
23
+
24
+ When a new script lands here, add a row to this table in the same PR and cite the script from at least one skill — otherwise the script is dead code on arrival.
25
+
26
+ ## Relationship to other locations
27
+
28
+ - `bin/` — generated CLI entrypoints (`.mjs` from `src/bin/*.mts`). Do not hand-edit; see [`CLAUDE.md`](../CLAUDE.md) → "Build pipeline".
29
+ - `lib/` — generated library code (`.mjs` from `src/lib/*.mts`). Same rule.
30
+ - `hooks/` — generated PreToolUse / PostToolUse / Stop hooks (`.mjs` from `src/hooks/*.mts`). Wired in `plugins/continuous-improvement/hooks/hooks.json`.
31
+ - `scripts/` — this directory. Hand-authored primitives skills cite.
32
+
33
+ When deciding where a new piece of code belongs: if it implements a runtime hook the harness will call, it goes in `src/hooks/`. If it's a verification or CLI entrypoint the plugin or `npm run` invokes, it goes in `src/bin/`. If it's reusable logic shared between those, it goes in `src/lib/`. If it's a small primitive a skill body cites by path (and the operator or harness runs ad hoc), it goes here.
@@ -0,0 +1,66 @@
1
+ #!/usr/bin/env bash
2
+ # scripts/detect-deploy-target.sh
3
+ #
4
+ # Detect the auto-deploy provider for the repo rooted at the current working
5
+ # directory (or the first argument, if supplied). Composability primitive used
6
+ # by skills and hooks that need to know "does this repo auto-deploy, and if so
7
+ # from where?" without each restating the file-marker table.
8
+ #
9
+ # Output: one of
10
+ # railway | cloudflare | vercel | netlify | fly | appengine | apprunner |
11
+ # gha-deploy | none
12
+ #
13
+ # Resolution priority (first match wins):
14
+ # 1. railway.toml | railway.json → railway
15
+ # 2. wrangler.toml | wrangler.jsonc → cloudflare
16
+ # 3. vercel.json | .vercel/ → vercel
17
+ # 4. netlify.toml → netlify
18
+ # 5. fly.toml → fly
19
+ # 6. app.yaml → appengine
20
+ # 7. apprunner.yaml → apprunner
21
+ # 8. .github/workflows/*.{yml,yaml} containing "deploy:" job → gha-deploy
22
+ # 9. nothing matched → none
23
+ #
24
+ # Always exits 0. `none` is a valid result, not an error condition.
25
+ #
26
+ # Cited by:
27
+ # - skills/verification-loop.md Phase 8 (deploy-receipt handoff trigger)
28
+ # - skills/deploy-receipt.md "When to Activate" gate
29
+
30
+ set -u
31
+
32
+ ROOT="${1:-$PWD}"
33
+
34
+ emit() {
35
+ printf '%s\n' "$1"
36
+ exit 0
37
+ }
38
+
39
+ # Order is the contract — earlier rows shadow later ones when multiple
40
+ # markers exist in the same repo.
41
+ [ -f "$ROOT/railway.toml" ] && emit railway
42
+ [ -f "$ROOT/railway.json" ] && emit railway
43
+ [ -f "$ROOT/wrangler.toml" ] && emit cloudflare
44
+ [ -f "$ROOT/wrangler.jsonc" ] && emit cloudflare
45
+ [ -f "$ROOT/vercel.json" ] && emit vercel
46
+ [ -d "$ROOT/.vercel" ] && emit vercel
47
+ [ -f "$ROOT/netlify.toml" ] && emit netlify
48
+ [ -f "$ROOT/fly.toml" ] && emit fly
49
+ [ -f "$ROOT/app.yaml" ] && emit appengine
50
+ [ -f "$ROOT/apprunner.yaml" ] && emit apprunner
51
+
52
+ # GitHub Actions deploy workflow: scan .github/workflows/*.{yml,yaml} for a
53
+ # job whose key is literally `deploy:`. Cheap and conservative — false
54
+ # positives (a non-deploy job named "deploy") are unlikely; false negatives
55
+ # (a deploy job named something else) require the repo to declare the
56
+ # provider via one of the file markers above instead.
57
+ if [ -d "$ROOT/.github/workflows" ]; then
58
+ if grep -lE '^[[:space:]]*deploy:[[:space:]]*$' \
59
+ "$ROOT/.github/workflows/"*.yml \
60
+ "$ROOT/.github/workflows/"*.yaml 2>/dev/null \
61
+ | head -n 1 | grep -q .; then
62
+ emit gha-deploy
63
+ fi
64
+ fi
65
+
66
+ emit none
@@ -0,0 +1,113 @@
1
+ #!/usr/bin/env bash
2
+ # scripts/get-deployed-sha.sh
3
+ #
4
+ # Print the currently-deployed commit SHA for an auto-deploy provider, or
5
+ # print the CLI command shape (with `--show-command`) without executing it.
6
+ # Composability primitive that owns the per-provider CLI knowledge so skills
7
+ # can cite one path instead of restating the 5-provider extraction table.
8
+ #
9
+ # Usage:
10
+ # bash scripts/get-deployed-sha.sh <provider>
11
+ # bash scripts/get-deployed-sha.sh --show-command <provider>
12
+ #
13
+ # Providers: railway | cloudflare | vercel | netlify | fly
14
+ #
15
+ # Default mode: runs the provider CLI, pipes through jq, prints the SHA on
16
+ # stdout. Requires the CLI to be installed and authenticated; exits 3 with a
17
+ # clear error if the CLI is missing.
18
+ #
19
+ # --show-command mode: prints the command pipeline that would run, without
20
+ # executing it. Useful for skill citations, dry-runs, and tests that should
21
+ # not require live CLI auth.
22
+ #
23
+ # Exit codes:
24
+ # 0 — SHA printed (default mode) or command printed (--show-command mode)
25
+ # 2 — missing or unknown provider (usage error)
26
+ # 3 — required CLI not installed (default mode only)
27
+ # non-zero — CLI failure (passed through)
28
+ #
29
+ # Cited by:
30
+ # - skills/verification-loop.md Phase 8 (deploy-receipt handoff trigger)
31
+ # - skills/deploy-receipt.md Route A (provider CLI extraction)
32
+
33
+ set -u
34
+
35
+ SHOW_COMMAND=false
36
+ PROVIDER=""
37
+
38
+ while [ $# -gt 0 ]; do
39
+ case "$1" in
40
+ --show-command)
41
+ SHOW_COMMAND=true
42
+ shift
43
+ ;;
44
+ -h|--help)
45
+ sed -n '2,/^$/p' "$0" >&2
46
+ exit 0
47
+ ;;
48
+ *)
49
+ if [ -z "$PROVIDER" ]; then
50
+ PROVIDER="$1"
51
+ else
52
+ printf 'usage: get-deployed-sha.sh [--show-command] <provider>\n' >&2
53
+ exit 2
54
+ fi
55
+ shift
56
+ ;;
57
+ esac
58
+ done
59
+
60
+ if [ -z "$PROVIDER" ]; then
61
+ printf 'usage: get-deployed-sha.sh [--show-command] <provider>\n' >&2
62
+ exit 2
63
+ fi
64
+
65
+ # Single source of truth for the per-provider command pipeline. Each value is
66
+ # the literal pipeline that would run; the jq filter extracts the SHA.
67
+ case "$PROVIDER" in
68
+ railway)
69
+ CMD='railway status --json | jq -r .deployments[0].meta.commitHash'
70
+ CLI=railway
71
+ ;;
72
+ cloudflare)
73
+ CMD='wrangler deployments list --json | jq -r .[0].metadata.deployment_trigger.metadata.commit_hash'
74
+ CLI=wrangler
75
+ ;;
76
+ vercel)
77
+ CMD='vercel inspect "$(vercel ls --json | jq -r .[0].url)" --json | jq -r .gitSource.sha'
78
+ CLI=vercel
79
+ ;;
80
+ netlify)
81
+ CMD='netlify api listSiteDeploys --data="{\"site_id\":\"$NETLIFY_SITE_ID\"}" | jq -r .[0].commit_ref'
82
+ CLI=netlify
83
+ ;;
84
+ fly)
85
+ CMD='fly releases --json | jq -r .[0].commit_sha'
86
+ CLI=fly
87
+ ;;
88
+ *)
89
+ printf 'unknown or unsupported provider: %s\n' "$PROVIDER" >&2
90
+ printf 'supported: railway | cloudflare | vercel | netlify | fly\n' >&2
91
+ exit 2
92
+ ;;
93
+ esac
94
+
95
+ if [ "$SHOW_COMMAND" = "true" ]; then
96
+ printf '%s\n' "$CMD"
97
+ exit 0
98
+ fi
99
+
100
+ if ! command -v "$CLI" >/dev/null 2>&1; then
101
+ printf 'required CLI "%s" not installed for provider "%s"\n' "$CLI" "$PROVIDER" >&2
102
+ exit 3
103
+ fi
104
+
105
+ if ! command -v jq >/dev/null 2>&1; then
106
+ printf 'required CLI "jq" not installed\n' >&2
107
+ exit 3
108
+ fi
109
+
110
+ # Execute the pipeline. eval is intentional — the per-provider CMD contains
111
+ # pipes and command substitution that need shell interpretation. CMDs are
112
+ # sourced from the literal table above, not user input.
113
+ eval "$CMD"
@@ -0,0 +1,48 @@
1
+ #!/usr/bin/env bash
2
+ # scripts/git-state-snapshot.sh
3
+ #
4
+ # Emit a single-line JSON envelope describing the current git working-tree
5
+ # state. Composability primitive used by skills that need to baseline or check
6
+ # git state without each restating the same 3-command triple.
7
+ #
8
+ # Fields:
9
+ # - head: short SHA returned by `git rev-parse --short HEAD`
10
+ # - upstream: short SHA of `@{u}` if the branch tracks an upstream, else "none"
11
+ # - dirty: integer count of lines from `git status --porcelain` (0 == clean)
12
+ # - root: absolute path from `git rev-parse --show-toplevel`
13
+ # - branch: `git symbolic-ref --short HEAD`, else "detached"
14
+ #
15
+ # Outside a git repository the script prints `{"error":"not-a-git-repo"}` and
16
+ # exits 1. All other failures are treated as a non-git-repo condition rather
17
+ # than emitting a partial envelope.
18
+ #
19
+ # Cited by:
20
+ # - skills/gateguard.md (Parallel-Actor Gate baseline + divergence)
21
+ # - skills/worktree-safety.md (Root + branch alignment)
22
+ # - skills/workspace-surface-audit.md (Environment Grain — parallel-actor row)
23
+
24
+ set -u
25
+
26
+ head=$(git rev-parse --short HEAD 2>/dev/null) || {
27
+ printf '{"error":"not-a-git-repo"}\n'
28
+ exit 1
29
+ }
30
+
31
+ if upstream=$(git rev-parse --short '@{u}' 2>/dev/null); then
32
+ upstream_field=$(printf '"%s"' "$upstream")
33
+ else
34
+ upstream_field='"none"'
35
+ fi
36
+
37
+ dirty=$(git status --porcelain 2>/dev/null | wc -l | tr -d ' ')
38
+
39
+ root=$(git rev-parse --show-toplevel 2>/dev/null || printf 'unknown')
40
+
41
+ if branch=$(git symbolic-ref --short HEAD 2>/dev/null); then
42
+ branch_field=$(printf '"%s"' "$branch")
43
+ else
44
+ branch_field='"detached"'
45
+ fi
46
+
47
+ printf '{"head":"%s","upstream":%s,"dirty":%s,"root":"%s","branch":%s}\n' \
48
+ "$head" "$upstream_field" "$dirty" "$root" "$branch_field"