continuous-improvement 3.9.2 → 3.11.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 +2 -2
- package/CHANGELOG.md +61 -0
- package/QUICKSTART.md +13 -8
- package/README.md +84 -153
- package/bin/backfill.mjs +0 -0
- package/bin/check-doc-runtime-claims.mjs +0 -0
- package/bin/check-docs-substrings.mjs +73 -0
- package/bin/check-everything-mirror.mjs +0 -0
- package/bin/check-routing-targets.mjs +0 -0
- package/bin/check-scripts-citation-drift.mjs +210 -0
- package/bin/check-skill-count.mjs +110 -0
- package/bin/check-skill-law-tag.mjs +0 -0
- package/bin/check-skill-mirror.mjs +0 -0
- package/bin/check-skill-tiers.mjs +0 -0
- package/bin/check-test-imports-only.mjs +126 -0
- package/bin/check-third-party-shape.mjs +0 -0
- package/bin/companion-preference-status.mjs +231 -0
- package/bin/gateguard-clear.mjs +53 -0
- package/bin/generate-plugin-manifests.mjs +7 -1
- package/bin/harvest-friction.mjs +0 -0
- package/bin/hook-stats.mjs +0 -0
- package/bin/install.mjs +182 -26
- package/bin/lint-transcript.mjs +0 -0
- package/bin/mcp-server.mjs +215 -2
- package/bin/observe.mjs +0 -0
- package/bin/pre-commit-block-strays.sh +0 -0
- package/bin/refresh-third-party.mjs +315 -313
- package/bin/unified-cli.mjs +0 -0
- package/commands/audit.md +34 -0
- package/commands/companion-preference.md +58 -0
- package/commands/discipline.md +14 -0
- package/commands/distill.md +39 -0
- package/commands/goal-check.md +53 -0
- package/commands/grill-me.md +40 -0
- package/commands/grill-with-docs.md +38 -0
- package/commands/handoff.md +42 -0
- package/commands/proceed-with-the-recommendation.md +20 -0
- package/commands/recall.md +49 -0
- package/commands/reconcile.md +47 -0
- package/commands/superpowers.md +2 -2
- package/commands/verify-install.md +55 -0
- package/hooks/companion-preference.mjs +190 -0
- package/hooks/gateguard.mjs +60 -25
- package/hooks/goal-drift-stop.mjs +183 -0
- package/hooks/observe.sh +15 -1
- package/hooks/route-prompt.mjs +180 -0
- package/hooks/route-table.json +35 -0
- package/hooks/three-section-close.mjs +134 -132
- package/lib/cli-anything.mjs +0 -0
- package/lib/compound-engineering.mjs +0 -0
- package/lib/gateguard-state.mjs +54 -2
- package/lib/goal-drift-gate.mjs +50 -0
- package/lib/goal-state.mjs +285 -0
- package/lib/observe-event.mjs +0 -0
- package/lib/plugin-metadata.mjs +126 -12
- package/lib/pm-marketplace.mjs +0 -0
- package/lib/pm-skills.mjs +0 -0
- package/lib/recall-index.mjs +175 -0
- package/lib/resolve-home-dir.mjs +0 -0
- package/lib/skill-distill.mjs +222 -0
- package/lib/skill-tiers.mjs +0 -0
- package/lib/unified-plugin.mjs +0 -0
- package/llms.txt +1 -1
- package/package.json +11 -7
- package/plugins/beginner.json +8 -3
- package/plugins/continuous-improvement/.claude-plugin/marketplace.json +2 -2
- package/plugins/continuous-improvement/.claude-plugin/plugin.json +2 -2
- package/plugins/continuous-improvement/agents/README.md +2 -2
- package/plugins/continuous-improvement/bin/backfill.mjs +0 -0
- package/plugins/continuous-improvement/bin/gateguard-clear.mjs +53 -0
- package/plugins/continuous-improvement/bin/mcp-server.mjs +215 -2
- package/plugins/continuous-improvement/bin/observe.mjs +0 -0
- package/plugins/continuous-improvement/commands/audit.md +34 -0
- package/plugins/continuous-improvement/commands/companion-preference.md +58 -0
- package/plugins/continuous-improvement/commands/discipline.md +14 -0
- package/plugins/continuous-improvement/commands/distill.md +39 -0
- package/plugins/continuous-improvement/commands/goal-check.md +53 -0
- package/plugins/continuous-improvement/commands/grill-me.md +40 -0
- package/plugins/continuous-improvement/commands/grill-with-docs.md +38 -0
- package/plugins/continuous-improvement/commands/handoff.md +42 -0
- package/plugins/continuous-improvement/commands/proceed-with-the-recommendation.md +20 -0
- package/plugins/continuous-improvement/commands/recall.md +49 -0
- package/plugins/continuous-improvement/commands/reconcile.md +47 -0
- package/plugins/continuous-improvement/commands/superpowers.md +2 -2
- package/plugins/continuous-improvement/commands/verify-install.md +55 -0
- package/plugins/continuous-improvement/hooks/companion-preference.mjs +190 -0
- package/plugins/continuous-improvement/hooks/gateguard.mjs +60 -25
- package/plugins/continuous-improvement/hooks/goal-drift-stop.mjs +183 -0
- package/plugins/continuous-improvement/hooks/hooks.json +18 -2
- package/plugins/continuous-improvement/hooks/observe.sh +15 -1
- package/plugins/continuous-improvement/hooks/route-prompt.mjs +180 -0
- package/plugins/continuous-improvement/hooks/route-table.json +35 -0
- package/plugins/continuous-improvement/hooks/three-section-close.mjs +134 -132
- package/plugins/continuous-improvement/lib/gateguard-state.mjs +137 -0
- package/plugins/continuous-improvement/lib/goal-drift-gate.mjs +50 -0
- package/plugins/continuous-improvement/lib/goal-state.mjs +285 -0
- package/plugins/continuous-improvement/lib/observe-event.mjs +0 -0
- package/plugins/continuous-improvement/lib/plugin-metadata.mjs +126 -12
- package/plugins/continuous-improvement/lib/recall-index.mjs +175 -0
- package/plugins/continuous-improvement/lib/resolve-home-dir.mjs +0 -0
- package/plugins/continuous-improvement/lib/skill-distill.mjs +222 -0
- package/plugins/continuous-improvement/skills/README.md +8 -0
- package/plugins/continuous-improvement/skills/audit/SKILL.md +73 -0
- package/plugins/continuous-improvement/skills/deploy-receipt/SKILL.md +13 -17
- package/plugins/continuous-improvement/skills/gateguard/SKILL.md +36 -17
- package/plugins/continuous-improvement/skills/goal-monitor/SKILL.md +81 -0
- package/plugins/continuous-improvement/skills/grill-me/SKILL.md +66 -0
- package/plugins/continuous-improvement/skills/grill-with-docs/SKILL.md +252 -0
- package/plugins/continuous-improvement/skills/handoff/SKILL.md +59 -0
- package/plugins/continuous-improvement/skills/para-memory-files/SKILL.md +1 -1
- package/plugins/continuous-improvement/skills/proceed-with-the-recommendation/SKILL.md +82 -6
- package/plugins/continuous-improvement/skills/ralph/SKILL.md +1 -0
- package/plugins/continuous-improvement/skills/recall/SKILL.md +50 -0
- package/plugins/continuous-improvement/skills/reconcile/SKILL.md +80 -0
- package/plugins/continuous-improvement/skills/recovery-classification/SKILL.md +1 -0
- package/plugins/continuous-improvement/skills/safety-guard/SKILL.md +1 -0
- package/plugins/continuous-improvement/skills/skill-distillation/SKILL.md +57 -0
- package/plugins/continuous-improvement/skills/state-reconciliation/SKILL.md +1 -0
- package/plugins/continuous-improvement/skills/strategic-compact/SKILL.md +1 -0
- package/plugins/continuous-improvement/skills/superpowers/SKILL.md +76 -2
- package/plugins/continuous-improvement/skills/token-budget-advisor/SKILL.md +1 -1
- package/plugins/continuous-improvement/skills/verification-loop/SKILL.md +28 -20
- package/plugins/continuous-improvement/skills/wild-risa-balance/SKILL.md +1 -0
- package/plugins/continuous-improvement/skills/workspace-surface-audit/SKILL.md +1 -1
- package/plugins/continuous-improvement/skills/worktree-safety/SKILL.md +3 -2
- package/plugins/expert.json +27 -2
- package/skills/README.md +3 -0
- package/skills/audit.md +73 -0
- package/skills/deploy-receipt.md +13 -17
- package/skills/gateguard.md +36 -17
- package/skills/goal-monitor.md +81 -0
- package/skills/grill-me.md +66 -0
- package/skills/grill-with-docs.md +252 -0
- package/skills/handoff.md +59 -0
- package/skills/para-memory-files.md +1 -1
- package/skills/proceed-with-the-recommendation.md +82 -6
- package/skills/ralph.md +1 -0
- package/skills/recall.md +50 -0
- package/skills/reconcile.md +80 -0
- package/skills/recovery-classification.md +1 -0
- package/skills/safety-guard.md +1 -0
- package/skills/skill-distillation.md +57 -0
- package/skills/state-reconciliation.md +1 -0
- package/skills/strategic-compact.md +1 -0
- package/skills/superpowers.md +76 -2
- package/skills/token-budget-advisor.md +1 -1
- package/skills/verification-loop.md +28 -20
- package/skills/wild-risa-balance.md +1 -0
- package/skills/workspace-surface-audit.md +1 -1
- package/skills/worktree-safety.md +3 -2
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// goal-drift-stop.mts — Stop hook that scores the turn's activity against the
|
|
3
|
+
// stated goal (task_plan.md ## Goal) and, when the session has DRIFTed on a
|
|
4
|
+
// substantive wrap-up, warns or (opt-in) blocks the close. This is the runtime
|
|
5
|
+
// arm of goal-monitor: it auto-fires the drift check at the Stop phase boundary
|
|
6
|
+
// instead of waiting for someone to run /goal-check.
|
|
7
|
+
//
|
|
8
|
+
// Mode via CLAUDE_GOAL_DRIFT_GATE: "warn" (default) | "block" | "off".
|
|
9
|
+
// - warn : print a one-line drift notice to stderr; never blocks.
|
|
10
|
+
// - block : emit {"decision":"block","reason":...} to re-prompt.
|
|
11
|
+
// - off : no-op.
|
|
12
|
+
// The drift status is persisted to
|
|
13
|
+
// ~/.claude/instincts/<project-hash>/goal-drift-state.json every turn.
|
|
14
|
+
//
|
|
15
|
+
// Project-hash resolution is byte-identical to bin/observe.mts so this hook
|
|
16
|
+
// reads the SAME observations.jsonl the observer writes. Fail-open by
|
|
17
|
+
// construction: any error / missing goal / unreadable observations exits 0 and
|
|
18
|
+
// never blocks. No network. 5s hook budget.
|
|
19
|
+
import { execFileSync } from "node:child_process";
|
|
20
|
+
import { createHash } from "node:crypto";
|
|
21
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
22
|
+
import { join } from "node:path";
|
|
23
|
+
import { evaluateGoalDrift } from "../lib/goal-drift-gate.mjs";
|
|
24
|
+
import { resolveHomeDir } from "../lib/resolve-home-dir.mjs";
|
|
25
|
+
function readStdinSync() {
|
|
26
|
+
try {
|
|
27
|
+
return readFileSync(0, "utf8");
|
|
28
|
+
}
|
|
29
|
+
catch {
|
|
30
|
+
return "";
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
function safeJsonParse(text) {
|
|
34
|
+
try {
|
|
35
|
+
return JSON.parse(text);
|
|
36
|
+
}
|
|
37
|
+
catch {
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function resolveMode() {
|
|
42
|
+
const raw = (process.env.CLAUDE_GOAL_DRIFT_GATE ?? "warn").trim().toLowerCase();
|
|
43
|
+
return raw === "block" || raw === "off" ? raw : "warn";
|
|
44
|
+
}
|
|
45
|
+
// Mirrors bin/observe.mts:resolveProjectRoot — the same basis the observer uses
|
|
46
|
+
// to bucket observations.jsonl, so the hash here resolves to the same file.
|
|
47
|
+
function resolveProjectRoot() {
|
|
48
|
+
const fromEnv = process.env.CLAUDE_PROJECT_DIR;
|
|
49
|
+
if (fromEnv)
|
|
50
|
+
return fromEnv;
|
|
51
|
+
try {
|
|
52
|
+
const root = execFileSync("git", ["rev-parse", "--show-toplevel"], {
|
|
53
|
+
encoding: "utf8",
|
|
54
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
55
|
+
}).trim();
|
|
56
|
+
if (root)
|
|
57
|
+
return root;
|
|
58
|
+
}
|
|
59
|
+
catch {
|
|
60
|
+
// not a git repo
|
|
61
|
+
}
|
|
62
|
+
return "global";
|
|
63
|
+
}
|
|
64
|
+
function projectHash(root) {
|
|
65
|
+
return createHash("sha256").update(root).digest("hex").slice(0, 12);
|
|
66
|
+
}
|
|
67
|
+
function readGoalMarkdown(projectRoot, instinctsProjectDir) {
|
|
68
|
+
const candidates = [join(projectRoot, "task_plan.md"), join(instinctsProjectDir, "goal.md")];
|
|
69
|
+
for (const file of candidates) {
|
|
70
|
+
try {
|
|
71
|
+
if (existsSync(file))
|
|
72
|
+
return readFileSync(file, "utf8");
|
|
73
|
+
}
|
|
74
|
+
catch {
|
|
75
|
+
// try the next candidate
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return "";
|
|
79
|
+
}
|
|
80
|
+
function readObservations(instinctsProjectDir) {
|
|
81
|
+
const file = join(instinctsProjectDir, "observations.jsonl");
|
|
82
|
+
try {
|
|
83
|
+
if (!existsSync(file))
|
|
84
|
+
return [];
|
|
85
|
+
const lines = readFileSync(file, "utf8")
|
|
86
|
+
.split("\n")
|
|
87
|
+
.filter((line) => line.trim().length > 0);
|
|
88
|
+
const out = [];
|
|
89
|
+
for (const line of lines.slice(-100)) {
|
|
90
|
+
const row = safeJsonParse(line);
|
|
91
|
+
if (!row)
|
|
92
|
+
continue;
|
|
93
|
+
out.push({
|
|
94
|
+
ts: typeof row.ts === "string" ? row.ts : "",
|
|
95
|
+
tool: typeof row.tool === "string" ? row.tool : "",
|
|
96
|
+
input_summary: typeof row.input_summary === "string" ? row.input_summary : "",
|
|
97
|
+
output_summary: typeof row.output_summary === "string" ? row.output_summary : "",
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
return out;
|
|
101
|
+
}
|
|
102
|
+
catch {
|
|
103
|
+
return [];
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
function lastAssistantTextLength(transcriptPath) {
|
|
107
|
+
try {
|
|
108
|
+
const lines = readFileSync(transcriptPath, "utf8").split(/\r?\n/);
|
|
109
|
+
for (let i = lines.length - 1; i >= 0; i -= 1) {
|
|
110
|
+
const line = lines[i].trim();
|
|
111
|
+
if (!line)
|
|
112
|
+
continue;
|
|
113
|
+
const obj = safeJsonParse(line);
|
|
114
|
+
if (!obj || obj.type !== "assistant")
|
|
115
|
+
continue;
|
|
116
|
+
const blocks = obj.message?.content;
|
|
117
|
+
if (!Array.isArray(blocks))
|
|
118
|
+
continue;
|
|
119
|
+
const text = blocks
|
|
120
|
+
.filter((b) => b &&
|
|
121
|
+
typeof b === "object" &&
|
|
122
|
+
b.type === "text" &&
|
|
123
|
+
typeof b.text === "string")
|
|
124
|
+
.map((b) => b.text)
|
|
125
|
+
.join("\n");
|
|
126
|
+
if (text.length === 0)
|
|
127
|
+
continue;
|
|
128
|
+
return text.length;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
catch {
|
|
132
|
+
// fall through to 0
|
|
133
|
+
}
|
|
134
|
+
return 0;
|
|
135
|
+
}
|
|
136
|
+
function persistState(instinctsProjectDir, state) {
|
|
137
|
+
try {
|
|
138
|
+
mkdirSync(instinctsProjectDir, { recursive: true });
|
|
139
|
+
writeFileSync(join(instinctsProjectDir, "goal-drift-state.json"), JSON.stringify({ ts: new Date().toISOString(), ...state }) + "\n", "utf8");
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
// state persistence is best-effort; never throw
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
function main() {
|
|
146
|
+
const mode = resolveMode();
|
|
147
|
+
if (mode === "off")
|
|
148
|
+
return;
|
|
149
|
+
const stdin = readStdinSync();
|
|
150
|
+
if (!stdin)
|
|
151
|
+
return;
|
|
152
|
+
const payload = safeJsonParse(stdin);
|
|
153
|
+
if (!payload)
|
|
154
|
+
return;
|
|
155
|
+
const home = resolveHomeDir();
|
|
156
|
+
if (!home)
|
|
157
|
+
return;
|
|
158
|
+
const projectRoot = resolveProjectRoot();
|
|
159
|
+
const instinctsProjectDir = join(home, ".claude", "instincts", projectHash(projectRoot));
|
|
160
|
+
const goalMarkdown = readGoalMarkdown(projectRoot, instinctsProjectDir);
|
|
161
|
+
const observations = readObservations(instinctsProjectDir);
|
|
162
|
+
const textLength = payload.transcript_path && existsSync(payload.transcript_path)
|
|
163
|
+
? lastAssistantTextLength(payload.transcript_path)
|
|
164
|
+
: 0;
|
|
165
|
+
const decision = evaluateGoalDrift({ goalMarkdown, observations, textLength, mode });
|
|
166
|
+
persistState(instinctsProjectDir, {
|
|
167
|
+
status: decision.status,
|
|
168
|
+
score: decision.score,
|
|
169
|
+
action: decision.action,
|
|
170
|
+
});
|
|
171
|
+
if (decision.action === "block") {
|
|
172
|
+
process.stdout.write(JSON.stringify({ decision: "block", reason: decision.reason }) + "\n");
|
|
173
|
+
}
|
|
174
|
+
else if (decision.action === "warn") {
|
|
175
|
+
process.stderr.write(`[continuous-improvement] goal-drift: ${decision.reason}\n`);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
try {
|
|
179
|
+
main();
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
// fail open — never block the session due to a hook bug
|
|
183
|
+
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"description": "Gateguard fact-forcing PreToolUse, observation, session lifecycle,
|
|
2
|
+
"description": "Gateguard fact-forcing PreToolUse, companion-preference enforcement, observation, session lifecycle, 3-section-close discipline, goal-drift Stop gate, and UserPromptSubmit lazy-routing hooks for continuous-improvement.",
|
|
3
3
|
"hooks": {
|
|
4
4
|
"PreToolUse": [
|
|
5
5
|
{
|
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
},
|
|
12
12
|
{
|
|
13
13
|
"type": "command",
|
|
14
|
-
"command": "
|
|
14
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/companion-preference.mjs\"",
|
|
15
15
|
"timeout": 5
|
|
16
16
|
}
|
|
17
17
|
]
|
|
@@ -28,6 +28,17 @@
|
|
|
28
28
|
]
|
|
29
29
|
}
|
|
30
30
|
],
|
|
31
|
+
"UserPromptSubmit": [
|
|
32
|
+
{
|
|
33
|
+
"hooks": [
|
|
34
|
+
{
|
|
35
|
+
"type": "command",
|
|
36
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/route-prompt.mjs\"",
|
|
37
|
+
"timeout": 5
|
|
38
|
+
}
|
|
39
|
+
]
|
|
40
|
+
}
|
|
41
|
+
],
|
|
31
42
|
"SessionStart": [
|
|
32
43
|
{
|
|
33
44
|
"hooks": [
|
|
@@ -57,6 +68,11 @@
|
|
|
57
68
|
"type": "command",
|
|
58
69
|
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/three-section-close.mjs\"",
|
|
59
70
|
"timeout": 5
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
"type": "command",
|
|
74
|
+
"command": "node \"${CLAUDE_PLUGIN_ROOT}/hooks/goal-drift-stop.mjs\"",
|
|
75
|
+
"timeout": 5
|
|
60
76
|
}
|
|
61
77
|
]
|
|
62
78
|
}
|
|
@@ -101,6 +101,20 @@ fi
|
|
|
101
101
|
# ---------------------------------------------------------------------------
|
|
102
102
|
# Compute project hash and name
|
|
103
103
|
# ---------------------------------------------------------------------------
|
|
104
|
+
# Canonicalize MSYS-mounted Windows paths (/d/Path) to native form (D:/Path)
|
|
105
|
+
# so the bash fallback hashes to the same project bucket as the Node observer.
|
|
106
|
+
# Without this, Git Bash on Windows produces /d/Ai/repo from `git rev-parse`
|
|
107
|
+
# while Node's child_process produces D:/Ai/repo for the same physical path,
|
|
108
|
+
# splitting telemetry between two SHA-256 buckets per project.
|
|
109
|
+
# Pattern matches /<single-letter>/... only — Linux/macOS paths like /usr,
|
|
110
|
+
# /home, /Users are unaffected.
|
|
111
|
+
case "$PROJECT_ROOT" in
|
|
112
|
+
/[a-zA-Z]/*)
|
|
113
|
+
drive_letter="$(printf '%s' "$PROJECT_ROOT" | cut -c2 | tr '[:lower:]' '[:upper:]')"
|
|
114
|
+
PROJECT_ROOT="${drive_letter}:$(printf '%s' "$PROJECT_ROOT" | cut -c3-)"
|
|
115
|
+
;;
|
|
116
|
+
esac
|
|
117
|
+
|
|
104
118
|
PROJECT_HASH="$(printf '%s' "$PROJECT_ROOT" | sha256sum | cut -c1-12)"
|
|
105
119
|
PROJECT_NAME="$(basename "${PROJECT_ROOT%.git}")"
|
|
106
120
|
|
|
@@ -119,7 +133,7 @@ if [[ -f "$OBS_FILE" ]]; then
|
|
|
119
133
|
LINE_COUNT="$(wc -l < "$OBS_FILE")"
|
|
120
134
|
if (( LINE_COUNT >= 10000 )); then
|
|
121
135
|
mv "$OBS_FILE" "${PROJECT_DIR}/observations.$(date -u +"%Y-%m-%d-%H%M%S").jsonl"
|
|
122
|
-
|
|
136
|
+
|
|
123
137
|
# Clean up old archives - keep only 10 most recent
|
|
124
138
|
ls -t "${PROJECT_DIR}"/observations.*.jsonl 2>/dev/null | tail -n +11 | xargs -r rm -f
|
|
125
139
|
fi
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Runtime UserPromptSubmit prompt-router hook.
|
|
4
|
+
*
|
|
5
|
+
* Reads the user prompt, matches it against patterns in
|
|
6
|
+
* hooks/route-table.json, and emits a single `<system-reminder>` recommending
|
|
7
|
+
* the matched skill. When no row matches, the hook emits nothing — the prompt
|
|
8
|
+
* passes through untouched.
|
|
9
|
+
*
|
|
10
|
+
* Stdin : JSON { prompt, session_id?, transcript_path?, cwd? }
|
|
11
|
+
* Stdout : JSON { hookSpecificOutput: { hookEventName: "UserPromptSubmit",
|
|
12
|
+
* additionalContext: string } }
|
|
13
|
+
* OR empty when there is no match.
|
|
14
|
+
* Exit : 0 always — routing decisions live in stdout, never in exit code.
|
|
15
|
+
*
|
|
16
|
+
* Fail-open: any unexpected error reading stdin / parsing the route table /
|
|
17
|
+
* compiling a pattern → emit nothing and exit 0. The hook never blocks a
|
|
18
|
+
* prompt on its own bugs.
|
|
19
|
+
*
|
|
20
|
+
* Telemetry: each match appends one JSONL line to
|
|
21
|
+
* ~/.claude/instincts/<project-hash>/route-prompt.jsonl with the row index,
|
|
22
|
+
* skill, and pattern. Telemetry failures never change the decision.
|
|
23
|
+
*
|
|
24
|
+
* Route table schema (hooks/route-table.json):
|
|
25
|
+
* {
|
|
26
|
+
* "rows": [
|
|
27
|
+
* { "pattern": "regex source", "flags": "i", "skill": "skill-name",
|
|
28
|
+
* "reason": "one-line description" }
|
|
29
|
+
* ]
|
|
30
|
+
* }
|
|
31
|
+
* Rows are evaluated in order; first match wins.
|
|
32
|
+
*/
|
|
33
|
+
import { createHash } from "node:crypto";
|
|
34
|
+
import { appendFileSync, existsSync, mkdirSync, readFileSync } from "node:fs";
|
|
35
|
+
import { homedir } from "node:os";
|
|
36
|
+
import { dirname, join } from "node:path";
|
|
37
|
+
import { execFileSync } from "node:child_process";
|
|
38
|
+
import { fileURLToPath } from "node:url";
|
|
39
|
+
function readStdin() {
|
|
40
|
+
try {
|
|
41
|
+
return readFileSync(0, "utf8");
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return "";
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
function resolveHome() {
|
|
48
|
+
return process.env.HOME ?? process.env.USERPROFILE ?? homedir();
|
|
49
|
+
}
|
|
50
|
+
function resolveProjectRoot() {
|
|
51
|
+
const fromEnv = process.env.CLAUDE_PROJECT_DIR;
|
|
52
|
+
if (fromEnv)
|
|
53
|
+
return fromEnv;
|
|
54
|
+
try {
|
|
55
|
+
const root = execFileSync("git", ["rev-parse", "--show-toplevel"], {
|
|
56
|
+
encoding: "utf8",
|
|
57
|
+
stdio: ["ignore", "pipe", "ignore"],
|
|
58
|
+
}).trim();
|
|
59
|
+
if (root)
|
|
60
|
+
return root;
|
|
61
|
+
}
|
|
62
|
+
catch {
|
|
63
|
+
// not in a git repo
|
|
64
|
+
}
|
|
65
|
+
return "global";
|
|
66
|
+
}
|
|
67
|
+
function telemetryPath(home) {
|
|
68
|
+
const hash = createHash("sha256")
|
|
69
|
+
.update(resolveProjectRoot())
|
|
70
|
+
.digest("hex")
|
|
71
|
+
.slice(0, 12);
|
|
72
|
+
return join(home, ".claude", "instincts", hash, "route-prompt.jsonl");
|
|
73
|
+
}
|
|
74
|
+
function writeTelemetry(home, event) {
|
|
75
|
+
try {
|
|
76
|
+
const path = telemetryPath(home);
|
|
77
|
+
const dir = dirname(path);
|
|
78
|
+
if (!existsSync(dir))
|
|
79
|
+
mkdirSync(dir, { recursive: true });
|
|
80
|
+
appendFileSync(path, `${JSON.stringify(event)}\n`);
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
// fail-open
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
function loadRouteTable() {
|
|
87
|
+
// Hook lives at <plugin-root>/hooks/route-prompt.mjs at runtime; the table
|
|
88
|
+
// sits next to it. import.meta.url gives the file:// URL of the compiled
|
|
89
|
+
// .mjs.
|
|
90
|
+
try {
|
|
91
|
+
const here = fileURLToPath(new URL(".", import.meta.url));
|
|
92
|
+
const tablePath = join(here, "route-table.json");
|
|
93
|
+
const raw = readFileSync(tablePath, "utf8");
|
|
94
|
+
const parsed = JSON.parse(raw);
|
|
95
|
+
if (!parsed || !Array.isArray(parsed.rows))
|
|
96
|
+
return null;
|
|
97
|
+
return parsed;
|
|
98
|
+
}
|
|
99
|
+
catch {
|
|
100
|
+
return null;
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
function findMatch(prompt, table) {
|
|
104
|
+
for (let i = 0; i < table.rows.length; i++) {
|
|
105
|
+
const row = table.rows[i];
|
|
106
|
+
if (!row || typeof row.pattern !== "string" || typeof row.skill !== "string") {
|
|
107
|
+
continue;
|
|
108
|
+
}
|
|
109
|
+
let rx;
|
|
110
|
+
try {
|
|
111
|
+
rx = new RegExp(row.pattern, row.flags ?? "");
|
|
112
|
+
}
|
|
113
|
+
catch {
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
if (rx.test(prompt)) {
|
|
117
|
+
return { row, index: i };
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return null;
|
|
121
|
+
}
|
|
122
|
+
function buildContext(row) {
|
|
123
|
+
const reason = row.reason
|
|
124
|
+
? `${row.reason}\nConsider invoking \`/${row.skill}\` now.`
|
|
125
|
+
: `Consider invoking \`/${row.skill}\` now.`;
|
|
126
|
+
return `<system-reminder>\nPrompt-router suggestion: ${reason}\n</system-reminder>`;
|
|
127
|
+
}
|
|
128
|
+
function emit(output) {
|
|
129
|
+
if (output !== null) {
|
|
130
|
+
process.stdout.write(`${JSON.stringify(output)}\n`);
|
|
131
|
+
}
|
|
132
|
+
process.exit(0);
|
|
133
|
+
}
|
|
134
|
+
function main() {
|
|
135
|
+
const raw = readStdin();
|
|
136
|
+
let payload;
|
|
137
|
+
try {
|
|
138
|
+
payload = JSON.parse(raw);
|
|
139
|
+
}
|
|
140
|
+
catch {
|
|
141
|
+
emit(null);
|
|
142
|
+
return;
|
|
143
|
+
}
|
|
144
|
+
const prompt = payload.prompt;
|
|
145
|
+
if (typeof prompt !== "string" || prompt.length === 0) {
|
|
146
|
+
emit(null);
|
|
147
|
+
return;
|
|
148
|
+
}
|
|
149
|
+
const table = loadRouteTable();
|
|
150
|
+
if (!table) {
|
|
151
|
+
emit(null);
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
const match = findMatch(prompt, table);
|
|
155
|
+
const home = resolveHome();
|
|
156
|
+
if (!match) {
|
|
157
|
+
writeTelemetry(home, {
|
|
158
|
+
ts: new Date().toISOString(),
|
|
159
|
+
hook: "route-prompt",
|
|
160
|
+
matched: false,
|
|
161
|
+
});
|
|
162
|
+
emit(null);
|
|
163
|
+
return;
|
|
164
|
+
}
|
|
165
|
+
writeTelemetry(home, {
|
|
166
|
+
ts: new Date().toISOString(),
|
|
167
|
+
hook: "route-prompt",
|
|
168
|
+
matched: true,
|
|
169
|
+
row_index: match.index,
|
|
170
|
+
skill: match.row.skill,
|
|
171
|
+
pattern: match.row.pattern,
|
|
172
|
+
});
|
|
173
|
+
emit({
|
|
174
|
+
hookSpecificOutput: {
|
|
175
|
+
hookEventName: "UserPromptSubmit",
|
|
176
|
+
additionalContext: buildContext(match.row),
|
|
177
|
+
},
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
main();
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
{
|
|
2
|
+
"description": "Declarative prompt-router for the continuous-improvement UserPromptSubmit hook. Each row maps a regex (prompt-side trigger) to a skill the agent should consider invoking. First match wins; rows are evaluated in order. Edit this file to grow the routing table — no code change required.",
|
|
3
|
+
"rows": [
|
|
4
|
+
{
|
|
5
|
+
"pattern": "\\b(write|add|new)\\s+(a\\s+)?(failing\\s+)?test\\b|\\btdd\\b|\\bred[- ]green[- ]refactor\\b",
|
|
6
|
+
"flags": "i",
|
|
7
|
+
"skill": "tdd-workflow",
|
|
8
|
+
"reason": "Prompt mentions writing tests, TDD, or RED-GREEN-REFACTOR."
|
|
9
|
+
},
|
|
10
|
+
{
|
|
11
|
+
"pattern": "\\b(verify|verification|prove\\s+it\\s+works|smallest\\s+check|check\\s+it\\s+passes)\\b",
|
|
12
|
+
"flags": "i",
|
|
13
|
+
"skill": "verification-loop",
|
|
14
|
+
"reason": "Prompt asks for verification before declaring done."
|
|
15
|
+
},
|
|
16
|
+
{
|
|
17
|
+
"pattern": "\\b(plan|blueprint|design\\s+(the\\s+)?approach|step[- ]by[- ]step\\s+plan|roadmap)\\b",
|
|
18
|
+
"flags": "i",
|
|
19
|
+
"skill": "planning-with-files",
|
|
20
|
+
"reason": "Prompt asks for a plan, blueprint, or design before implementation."
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"pattern": "\\b(security\\s+review|threat\\s+model|owasp|vulnerab|auth(entication|orization)\\s+check)\\b",
|
|
24
|
+
"flags": "i",
|
|
25
|
+
"skill": "security-review",
|
|
26
|
+
"reason": "Prompt touches security review, threat modeling, or auth surface."
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
"pattern": "\\b(code\\s+review|review\\s+(my|the|this)\\s+(code|pr|diff|patch)|critique\\s+(the|this)\\s+code)\\b",
|
|
30
|
+
"flags": "i",
|
|
31
|
+
"skill": "code-review",
|
|
32
|
+
"reason": "Prompt asks for a code review on existing changes."
|
|
33
|
+
}
|
|
34
|
+
]
|
|
35
|
+
}
|