continuous-improvement 3.19.0 → 3.20.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/marketplace.json +1 -1
- package/QUICKSTART.md +1 -1
- package/README.md +3 -2
- package/bin/check-landing-version.mjs +63 -0
- package/bin/check-scripts-citation-drift.mjs +61 -13
- package/bin/generate-plugin-manifests.mjs +3 -0
- package/bin/install.mjs +19 -11
- package/commands/verify-install.md +1 -1
- package/hooks/gateguard.mjs +22 -3
- package/hooks/query-cost-nudge.mjs +114 -0
- package/hooks/typecheck-stop.mjs +2 -1
- package/lib/plugin-metadata.mjs +7 -2
- package/lib/query-cost-gate.mjs +53 -0
- package/package.json +5 -3
- package/plugins/beginner.json +1 -1
- package/plugins/continuous-improvement/.claude-plugin/marketplace.json +1 -1
- package/plugins/continuous-improvement/.claude-plugin/plugin.json +1 -1
- package/plugins/continuous-improvement/README.md +1 -0
- package/plugins/continuous-improvement/commands/verify-install.md +1 -1
- package/plugins/continuous-improvement/hooks/gateguard.mjs +22 -3
- package/plugins/continuous-improvement/hooks/hooks.json +6 -1
- package/plugins/continuous-improvement/hooks/query-cost-nudge.mjs +114 -0
- package/plugins/continuous-improvement/hooks/typecheck-stop.mjs +2 -1
- package/plugins/continuous-improvement/lib/plugin-metadata.mjs +7 -2
- package/plugins/continuous-improvement/lib/query-cost-gate.mjs +53 -0
- package/plugins/continuous-improvement/scripts/README.md +33 -0
- package/plugins/continuous-improvement/scripts/detect-deploy-target.sh +66 -0
- package/plugins/continuous-improvement/scripts/get-deployed-sha.sh +113 -0
- package/plugins/continuous-improvement/scripts/git-state-snapshot.sh +48 -0
- package/plugins/continuous-improvement/scripts/resolve-verify-ladder.mjs +241 -0
- package/plugins/continuous-improvement/scripts/route-recommendation.mjs +178 -0
- package/plugins/continuous-improvement/scripts/route-recommendation.routes.json +213 -0
- package/plugins/continuous-improvement/scripts/run-synthetic.mjs +298 -0
- package/plugins/continuous-improvement/scripts/scan-past-mistakes.mjs +285 -0
- package/plugins/continuous-improvement/skills/deploy-receipt/SKILL.md +2 -2
- package/plugins/continuous-improvement/skills/gateguard/SKILL.md +2 -2
- package/plugins/continuous-improvement/skills/proceed-with-the-recommendation/SKILL.md +2 -2
- package/plugins/continuous-improvement/skills/reconcile/SKILL.md +1 -1
- package/plugins/continuous-improvement/skills/verification-loop/SKILL.md +5 -5
- package/plugins/continuous-improvement/skills/workspace-surface-audit/SKILL.md +1 -1
- package/plugins/continuous-improvement/skills/worktree-safety/SKILL.md +1 -1
- package/plugins/expert.json +1 -1
- package/scripts/README.md +33 -0
- package/scripts/detect-deploy-target.sh +66 -0
- package/scripts/get-deployed-sha.sh +113 -0
- package/scripts/git-state-snapshot.sh +48 -0
- package/scripts/resolve-verify-ladder.mjs +241 -0
- package/scripts/route-recommendation.mjs +178 -0
- package/scripts/route-recommendation.routes.json +213 -0
- package/scripts/run-synthetic.mjs +298 -0
- package/scripts/scan-past-mistakes.mjs +285 -0
- package/skills/deploy-receipt.md +2 -2
- package/skills/gateguard.md +2 -2
- package/skills/proceed-with-the-recommendation.md +2 -2
- package/skills/reconcile.md +1 -1
- package/skills/verification-loop.md +5 -5
- package/skills/workspace-surface-audit.md +1 -1
- package/skills/worktree-safety.md +1 -1
|
@@ -0,0 +1,298 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* Phase 9 Synthetic-Checks Runner
|
|
4
|
+
*
|
|
5
|
+
* Executes every *.synthetic.{sh,mjs,ts,py} file in --dir (default
|
|
6
|
+
* synthetic-checks/) in lexical order. Injects the documented env vars
|
|
7
|
+
* (BASE_URL, BASELINE_URL, EXPECTED_SHA, DEPLOY_BRANCH, RECEIPT_TIMESTAMP),
|
|
8
|
+
* captures stdout + stderr + exit per check, aggregates.
|
|
9
|
+
*
|
|
10
|
+
* Pure aggregator: required-env enforcement lives in the synthetic checks
|
|
11
|
+
* themselves (a check exits 2 when its own input contract is unmet, per
|
|
12
|
+
* synthetic-checks/README.md). The runner forwards env, propagates exit
|
|
13
|
+
* categories, and prints the operator-facing report.
|
|
14
|
+
*
|
|
15
|
+
* Contract: synthetic-checks/README.md
|
|
16
|
+
* Skill: skills/verification-loop.md (Phase 9 — production-vs-baseline diff)
|
|
17
|
+
*
|
|
18
|
+
* Exit codes:
|
|
19
|
+
* 0 — every recognized check exited 0, OR directory empty/absent
|
|
20
|
+
* 1 — at least one check exited non-zero (drift) or hit the wall-clock cap
|
|
21
|
+
* 2 — at least one check exited 2 (config error) AND zero drift
|
|
22
|
+
* 3 — runner-level usage error (bad flag, --timeout not a positive int)
|
|
23
|
+
*
|
|
24
|
+
* Flags:
|
|
25
|
+
* --dir <path> synthetic-checks/ Directory to walk
|
|
26
|
+
* --timeout <sec> 60 Per-check wall-clock cap
|
|
27
|
+
* --fail-fast off Halt on first drift or timeout
|
|
28
|
+
* (config_error does not halt — gate
|
|
29
|
+
* did not run is not the same as drift)
|
|
30
|
+
* --json off Emit machine-readable summary
|
|
31
|
+
* --show-command off Print interpreter map, do not execute
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
import { existsSync, readdirSync, statSync } from "node:fs";
|
|
35
|
+
import { extname, join } from "node:path";
|
|
36
|
+
import { spawnSync } from "node:child_process";
|
|
37
|
+
import { argv, env, exit, stderr, stdout } from "node:process";
|
|
38
|
+
import { parseArgs } from "node:util";
|
|
39
|
+
|
|
40
|
+
const INTERPRETER_MAP = {
|
|
41
|
+
".sh": "bash",
|
|
42
|
+
".mjs": "node",
|
|
43
|
+
".ts": "tsx",
|
|
44
|
+
".py": "python",
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
const FORWARDED_ENV = [
|
|
48
|
+
"BASE_URL",
|
|
49
|
+
"BASELINE_URL",
|
|
50
|
+
"EXPECTED_SHA",
|
|
51
|
+
"DEPLOY_BRANCH",
|
|
52
|
+
"RECEIPT_TIMESTAMP",
|
|
53
|
+
];
|
|
54
|
+
|
|
55
|
+
const SYNTHETIC_FILE_RE = /\.synthetic\.[A-Za-z0-9]+$/;
|
|
56
|
+
|
|
57
|
+
function parseFlags() {
|
|
58
|
+
try {
|
|
59
|
+
const { values } = parseArgs({
|
|
60
|
+
args: argv.slice(2),
|
|
61
|
+
options: {
|
|
62
|
+
dir: { type: "string", default: "synthetic-checks/" },
|
|
63
|
+
timeout: { type: "string", default: "60" },
|
|
64
|
+
"fail-fast": { type: "boolean", default: false },
|
|
65
|
+
json: { type: "boolean", default: false },
|
|
66
|
+
"show-command": { type: "boolean", default: false },
|
|
67
|
+
},
|
|
68
|
+
strict: true,
|
|
69
|
+
});
|
|
70
|
+
return values;
|
|
71
|
+
} catch (err) {
|
|
72
|
+
stderr.write(`usage error: ${err.message}\n`);
|
|
73
|
+
stderr.write("Run with no arguments to see defaults; --dir/--timeout/--fail-fast/--json/--show-command are the supported flags.\n");
|
|
74
|
+
exit(3);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function showCommand(dir) {
|
|
79
|
+
stdout.write(`Phase 9 runner — interpreter map (dry run for ${dir}):\n`);
|
|
80
|
+
for (const [ext, bin] of Object.entries(INTERPRETER_MAP)) {
|
|
81
|
+
stdout.write(` ${ext} -> ${bin}\n`);
|
|
82
|
+
}
|
|
83
|
+
stdout.write("\nForwarded env vars (runner -> child):\n");
|
|
84
|
+
for (const name of FORWARDED_ENV) {
|
|
85
|
+
const present = env[name] !== undefined ? "set" : "unset";
|
|
86
|
+
stdout.write(` ${name} (${present})\n`);
|
|
87
|
+
}
|
|
88
|
+
stdout.write("\nNo checks were executed. Re-run without --show-command to invoke.\n");
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function listChecks(dir) {
|
|
92
|
+
if (!existsSync(dir)) return null;
|
|
93
|
+
try {
|
|
94
|
+
if (!statSync(dir).isDirectory()) return null;
|
|
95
|
+
} catch {
|
|
96
|
+
return null;
|
|
97
|
+
}
|
|
98
|
+
return readdirSync(dir)
|
|
99
|
+
.filter((name) => SYNTHETIC_FILE_RE.test(name))
|
|
100
|
+
.sort();
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function runOne(filePath, timeoutMs) {
|
|
104
|
+
const ext = extname(filePath);
|
|
105
|
+
const interpreter = INTERPRETER_MAP[ext];
|
|
106
|
+
if (!interpreter) {
|
|
107
|
+
return {
|
|
108
|
+
status: "skipped — unknown extension",
|
|
109
|
+
exitCode: null,
|
|
110
|
+
stdout: "",
|
|
111
|
+
stderr: `unknown extension: ${ext}`,
|
|
112
|
+
durationMs: 0,
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
const childEnv = { PATH: env.PATH ?? "" };
|
|
116
|
+
for (const name of FORWARDED_ENV) {
|
|
117
|
+
childEnv[name] = env[name] ?? "";
|
|
118
|
+
}
|
|
119
|
+
const start = Date.now();
|
|
120
|
+
const result = spawnSync(interpreter, [filePath], {
|
|
121
|
+
env: childEnv,
|
|
122
|
+
encoding: "utf8",
|
|
123
|
+
timeout: timeoutMs,
|
|
124
|
+
killSignal: "SIGKILL",
|
|
125
|
+
});
|
|
126
|
+
const durationMs = Date.now() - start;
|
|
127
|
+
|
|
128
|
+
if (result.error && result.error.code === "ENOENT") {
|
|
129
|
+
return {
|
|
130
|
+
status: "skipped — interpreter not on PATH",
|
|
131
|
+
exitCode: null,
|
|
132
|
+
stdout: "",
|
|
133
|
+
stderr: `interpreter '${interpreter}' not on PATH`,
|
|
134
|
+
durationMs,
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// Cross-platform timeout detection: POSIX returns a signal name; Windows
|
|
139
|
+
// sets result.error.code to "ETIMEDOUT" with result.signal sometimes null.
|
|
140
|
+
const timedOut =
|
|
141
|
+
result.signal === "SIGKILL" ||
|
|
142
|
+
result.signal === "SIGTERM" ||
|
|
143
|
+
result.error?.code === "ETIMEDOUT";
|
|
144
|
+
if (timedOut) {
|
|
145
|
+
return {
|
|
146
|
+
status: "timeout",
|
|
147
|
+
exitCode: 124,
|
|
148
|
+
stdout: result.stdout ?? "",
|
|
149
|
+
stderr: result.stderr ?? "",
|
|
150
|
+
durationMs,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const exitCode = result.status ?? -1;
|
|
155
|
+
let status;
|
|
156
|
+
if (exitCode === 0) status = "pass";
|
|
157
|
+
else if (exitCode === 2) status = "config_error";
|
|
158
|
+
else status = "fail";
|
|
159
|
+
|
|
160
|
+
return {
|
|
161
|
+
status,
|
|
162
|
+
exitCode,
|
|
163
|
+
stdout: result.stdout ?? "",
|
|
164
|
+
stderr: result.stderr ?? "",
|
|
165
|
+
durationMs,
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
function main() {
|
|
170
|
+
const args = parseFlags();
|
|
171
|
+
const dir = args.dir;
|
|
172
|
+
|
|
173
|
+
if (args["show-command"]) {
|
|
174
|
+
showCommand(dir);
|
|
175
|
+
exit(0);
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
const timeoutSec = Number.parseInt(args.timeout, 10);
|
|
179
|
+
if (Number.isNaN(timeoutSec) || timeoutSec <= 0) {
|
|
180
|
+
stderr.write(`usage error: --timeout must be a positive integer (got "${args.timeout}")\n`);
|
|
181
|
+
exit(3);
|
|
182
|
+
}
|
|
183
|
+
const timeoutMs = timeoutSec * 1000;
|
|
184
|
+
|
|
185
|
+
const files = listChecks(dir);
|
|
186
|
+
|
|
187
|
+
if (files === null) {
|
|
188
|
+
const note = `skipped — directory not found: ${dir}`;
|
|
189
|
+
if (args.json) {
|
|
190
|
+
stdout.write(JSON.stringify({
|
|
191
|
+
dir,
|
|
192
|
+
checks: [],
|
|
193
|
+
summary: { pass: 0, fail: 0, configError: 0, skipped: 0, total: 0 },
|
|
194
|
+
note,
|
|
195
|
+
}, null, 2) + "\n");
|
|
196
|
+
} else {
|
|
197
|
+
stdout.write(`Phase 9 runner: ${note}\n`);
|
|
198
|
+
}
|
|
199
|
+
exit(0);
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
if (files.length === 0) {
|
|
203
|
+
const note = `skipped — 0 check(s) in ${dir}`;
|
|
204
|
+
if (args.json) {
|
|
205
|
+
stdout.write(JSON.stringify({
|
|
206
|
+
dir,
|
|
207
|
+
checks: [],
|
|
208
|
+
summary: { pass: 0, fail: 0, configError: 0, skipped: 0, total: 0 },
|
|
209
|
+
note,
|
|
210
|
+
}, null, 2) + "\n");
|
|
211
|
+
} else {
|
|
212
|
+
stdout.write(`Phase 9 runner: ${note}\n`);
|
|
213
|
+
}
|
|
214
|
+
exit(0);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
const results = [];
|
|
218
|
+
let halt = false;
|
|
219
|
+
for (const name of files) {
|
|
220
|
+
const ext = extname(name);
|
|
221
|
+
if (!INTERPRETER_MAP[ext]) {
|
|
222
|
+
results.push({
|
|
223
|
+
file: name,
|
|
224
|
+
status: "skipped — unknown extension",
|
|
225
|
+
exitCode: null,
|
|
226
|
+
stdout: "",
|
|
227
|
+
stderr: `unknown extension: ${ext}`,
|
|
228
|
+
durationMs: 0,
|
|
229
|
+
});
|
|
230
|
+
continue;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
if (halt) {
|
|
234
|
+
results.push({
|
|
235
|
+
file: name,
|
|
236
|
+
status: "skipped — fail_fast",
|
|
237
|
+
exitCode: null,
|
|
238
|
+
stdout: "",
|
|
239
|
+
stderr: "",
|
|
240
|
+
durationMs: 0,
|
|
241
|
+
});
|
|
242
|
+
continue;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const r = runOne(join(dir, name), timeoutMs);
|
|
246
|
+
results.push({ file: name, ...r });
|
|
247
|
+
|
|
248
|
+
if (args["fail-fast"] && (r.status === "fail" || r.status === "timeout")) {
|
|
249
|
+
halt = true;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
let pass = 0, fail = 0, configError = 0, skipped = 0;
|
|
254
|
+
for (const r of results) {
|
|
255
|
+
if (r.status === "pass") pass += 1;
|
|
256
|
+
else if (r.status === "fail" || r.status === "timeout") fail += 1;
|
|
257
|
+
else if (r.status === "config_error") configError += 1;
|
|
258
|
+
else if (typeof r.status === "string" && r.status.startsWith("skipped")) skipped += 1;
|
|
259
|
+
}
|
|
260
|
+
const summary = { pass, fail, configError, skipped, total: results.length };
|
|
261
|
+
|
|
262
|
+
let exitCode;
|
|
263
|
+
if (fail > 0) exitCode = 1;
|
|
264
|
+
else if (configError > 0) exitCode = 2;
|
|
265
|
+
else exitCode = 0;
|
|
266
|
+
|
|
267
|
+
if (args.json) {
|
|
268
|
+
stdout.write(JSON.stringify({ dir, checks: results, summary }, null, 2) + "\n");
|
|
269
|
+
} else {
|
|
270
|
+
stdout.write(`Phase 9 runner: ${results.length} check(s) in ${dir}\n`);
|
|
271
|
+
for (const r of results) {
|
|
272
|
+
const label = r.status === "pass"
|
|
273
|
+
? "PASS"
|
|
274
|
+
: r.status === "fail"
|
|
275
|
+
? "FAIL"
|
|
276
|
+
: r.status === "config_error"
|
|
277
|
+
? "CONFIG"
|
|
278
|
+
: r.status === "timeout"
|
|
279
|
+
? "TIMEOUT"
|
|
280
|
+
: (typeof r.status === "string" && r.status.startsWith("skipped"))
|
|
281
|
+
? "SKIP"
|
|
282
|
+
: "UNKNOWN";
|
|
283
|
+
stdout.write(` ${label.padEnd(8)} ${r.file}\n`);
|
|
284
|
+
if (r.status === "fail" || r.status === "timeout" || r.status === "config_error") {
|
|
285
|
+
if (r.stdout) stdout.write(r.stdout.split("\n").map((l) => " " + l).join("\n") + "\n");
|
|
286
|
+
if (r.stderr) stderr.write(r.stderr.split("\n").map((l) => " " + l).join("\n") + "\n");
|
|
287
|
+
}
|
|
288
|
+
if (typeof r.status === "string" && r.status.startsWith("skipped")) {
|
|
289
|
+
stderr.write(` ${r.status}${r.stderr ? `: ${r.stderr}` : ""}\n`);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
stdout.write(`\nSummary: ${pass} pass, ${fail} fail, ${configError} config-error, ${skipped} skipped (${results.length} total)\n`);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
exit(exitCode);
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
main();
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
// scripts/scan-past-mistakes.mjs
|
|
3
|
+
//
|
|
4
|
+
// Scan the three past-mistake surfaces named in Phase 0 P-MAG of
|
|
5
|
+
// `proceed-with-the-recommendation` and surface their content as a
|
|
6
|
+
// machine-readable list, so the skill's most-skipped phase becomes
|
|
7
|
+
// mechanically detectable: if the scan never runs, no output is emitted.
|
|
8
|
+
//
|
|
9
|
+
// Surfaces (all three are scanned every invocation):
|
|
10
|
+
// 1. observations.jsonl — JSONL lines whose type (or legacy event field)
|
|
11
|
+
// is "failure" or "correction". Last N (default 10) returned.
|
|
12
|
+
// 2. memory feedback — every feedback_*.md file in the project's
|
|
13
|
+
// auto-memory directory whose frontmatter declares `type: feedback`.
|
|
14
|
+
// 3. CLAUDE.md — rows extracted from a "## Past Mistakes" table
|
|
15
|
+
// in the project's CLAUDE.md (markdown table, first column = date).
|
|
16
|
+
//
|
|
17
|
+
// Defaults derive each path from the project root (positional arg; default
|
|
18
|
+
// cwd). All three can be overridden explicitly via flags for testing or
|
|
19
|
+
// non-default layouts.
|
|
20
|
+
//
|
|
21
|
+
// Usage:
|
|
22
|
+
// node scripts/scan-past-mistakes.mjs # cwd default
|
|
23
|
+
// node scripts/scan-past-mistakes.mjs <repo-root> # explicit root
|
|
24
|
+
// node scripts/scan-past-mistakes.mjs --json # JSON output
|
|
25
|
+
// node scripts/scan-past-mistakes.mjs \
|
|
26
|
+
// --observations <path> --memory-dir <dir> --claude-md <path>
|
|
27
|
+
// node scripts/scan-past-mistakes.mjs --max-observations 25
|
|
28
|
+
//
|
|
29
|
+
// Active-in-scope assessment is the LLM's job, not the script's. The script
|
|
30
|
+
// surfaces raw entries with citations; the skill body annotates each line
|
|
31
|
+
// with "Active in current scope: yes|no" based on the current task.
|
|
32
|
+
//
|
|
33
|
+
// Cited by:
|
|
34
|
+
// - skills/proceed-with-the-recommendation.md Phase 0 Rule 1
|
|
35
|
+
|
|
36
|
+
import { existsSync, readFileSync, readdirSync } from "node:fs";
|
|
37
|
+
import { homedir } from "node:os";
|
|
38
|
+
import { join } from "node:path";
|
|
39
|
+
import { argv, cwd, exit, stdout } from "node:process";
|
|
40
|
+
|
|
41
|
+
function parseArgs() {
|
|
42
|
+
const args = argv.slice(2);
|
|
43
|
+
const out = {
|
|
44
|
+
json: false,
|
|
45
|
+
root: cwd(),
|
|
46
|
+
observations: null,
|
|
47
|
+
memoryDir: null,
|
|
48
|
+
claudeMd: null,
|
|
49
|
+
maxObservations: 10,
|
|
50
|
+
};
|
|
51
|
+
for (let i = 0; i < args.length; i++) {
|
|
52
|
+
const a = args[i];
|
|
53
|
+
if (a === "--json") {
|
|
54
|
+
out.json = true;
|
|
55
|
+
} else if (a === "--observations") {
|
|
56
|
+
out.observations = args[++i];
|
|
57
|
+
} else if (a === "--memory-dir") {
|
|
58
|
+
out.memoryDir = args[++i];
|
|
59
|
+
} else if (a === "--claude-md") {
|
|
60
|
+
out.claudeMd = args[++i];
|
|
61
|
+
} else if (a === "--max-observations") {
|
|
62
|
+
out.maxObservations = parseInt(args[++i], 10) || 10;
|
|
63
|
+
} else if (a === "-h" || a === "--help") {
|
|
64
|
+
stdout.write(
|
|
65
|
+
"usage: scan-past-mistakes.mjs [--json] [<repo-root>]\n" +
|
|
66
|
+
" [--observations <path>] [--memory-dir <dir>] [--claude-md <path>]\n" +
|
|
67
|
+
" [--max-observations <n>]\n",
|
|
68
|
+
);
|
|
69
|
+
exit(0);
|
|
70
|
+
} else {
|
|
71
|
+
out.root = a;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
return out;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Map a project root to the auto-memory subdirectory name. Lowercases the
|
|
78
|
+
// Windows drive letter and replaces path separators with dashes, matching
|
|
79
|
+
// the convention used by `~/.claude/projects/<hash>/memory/` and
|
|
80
|
+
// `~/.claude/instincts/<hash>/`. On POSIX paths the leading slash becomes
|
|
81
|
+
// a leading dash, which is fine — the host has the same path on both
|
|
82
|
+
// surfaces and the resolved file existence is what's checked, not the
|
|
83
|
+
// hash format itself.
|
|
84
|
+
function projectHash(root) {
|
|
85
|
+
let p = root;
|
|
86
|
+
p = p.replace(/^([A-Za-z]):/, (_m, d) => d.toLowerCase() + ":");
|
|
87
|
+
p = p.replace(/:[\\/]/, "--");
|
|
88
|
+
p = p.replace(/[\\/]/g, "-");
|
|
89
|
+
return p;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function defaultObservationsPath(root) {
|
|
93
|
+
return join(homedir(), ".claude", "instincts", projectHash(root), "observations.jsonl");
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function defaultMemoryDir(root) {
|
|
97
|
+
return join(homedir(), ".claude", "projects", projectHash(root), "memory");
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function defaultClaudeMdPath(root) {
|
|
101
|
+
return join(root, "CLAUDE.md");
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function scanObservations(path, maxN) {
|
|
105
|
+
if (!path || !existsSync(path)) return [];
|
|
106
|
+
const raw = readFileSync(path, "utf8");
|
|
107
|
+
const lines = raw.split(/\r?\n/);
|
|
108
|
+
const matches = [];
|
|
109
|
+
for (let i = 0; i < lines.length; i++) {
|
|
110
|
+
const line = lines[i].trim();
|
|
111
|
+
if (!line) continue;
|
|
112
|
+
let obj;
|
|
113
|
+
try {
|
|
114
|
+
obj = JSON.parse(line);
|
|
115
|
+
} catch {
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
// Support both the current `type` field and the legacy `event` field
|
|
119
|
+
// (per feedback_observer_field_name_bug — pre-2026-05-06T00:38Z rows).
|
|
120
|
+
const t = obj.type ?? obj.event;
|
|
121
|
+
if (t !== "failure" && t !== "correction") continue;
|
|
122
|
+
const summary =
|
|
123
|
+
obj.summary ??
|
|
124
|
+
obj.output_summary ??
|
|
125
|
+
(typeof obj.tool_response === "string"
|
|
126
|
+
? obj.tool_response
|
|
127
|
+
: JSON.stringify(obj).slice(0, 240));
|
|
128
|
+
matches.push({
|
|
129
|
+
line: i + 1,
|
|
130
|
+
ts: obj.ts ?? obj.timestamp ?? null,
|
|
131
|
+
type: t,
|
|
132
|
+
summary,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
// Last N (chronologically — JSONL is append-only).
|
|
136
|
+
return matches.slice(-maxN);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function parseFrontmatter(content) {
|
|
140
|
+
const m = /^---\r?\n([\s\S]*?)\r?\n---/.exec(content);
|
|
141
|
+
if (!m) return null;
|
|
142
|
+
const fm = {};
|
|
143
|
+
for (const line of m[1].split(/\r?\n/)) {
|
|
144
|
+
const kv = /^([A-Za-z][A-Za-z0-9_-]*):\s*(.*)$/.exec(line);
|
|
145
|
+
if (!kv) continue;
|
|
146
|
+
let value = kv[2].trim();
|
|
147
|
+
if (
|
|
148
|
+
(value.startsWith('"') && value.endsWith('"')) ||
|
|
149
|
+
(value.startsWith("'") && value.endsWith("'"))
|
|
150
|
+
) {
|
|
151
|
+
value = value.slice(1, -1);
|
|
152
|
+
}
|
|
153
|
+
fm[kv[1]] = value;
|
|
154
|
+
}
|
|
155
|
+
return fm;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function scanFeedbackMemories(dir) {
|
|
159
|
+
if (!dir || !existsSync(dir)) return [];
|
|
160
|
+
let entries;
|
|
161
|
+
try {
|
|
162
|
+
entries = readdirSync(dir);
|
|
163
|
+
} catch {
|
|
164
|
+
return [];
|
|
165
|
+
}
|
|
166
|
+
const out = [];
|
|
167
|
+
for (const file of entries) {
|
|
168
|
+
if (!file.startsWith("feedback_") || !file.endsWith(".md")) continue;
|
|
169
|
+
let content;
|
|
170
|
+
try {
|
|
171
|
+
content = readFileSync(join(dir, file), "utf8");
|
|
172
|
+
} catch {
|
|
173
|
+
continue;
|
|
174
|
+
}
|
|
175
|
+
const fm = parseFrontmatter(content);
|
|
176
|
+
if (!fm) continue;
|
|
177
|
+
// Only surface entries explicitly typed as feedback.
|
|
178
|
+
if (fm.type && fm.type !== "feedback") continue;
|
|
179
|
+
out.push({
|
|
180
|
+
file,
|
|
181
|
+
name: fm.name ?? null,
|
|
182
|
+
description: fm.description ?? null,
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
out.sort((a, b) => a.file.localeCompare(b.file));
|
|
186
|
+
return out;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
function scanClaudeMdPastMistakes(path) {
|
|
190
|
+
if (!path || !existsSync(path)) return [];
|
|
191
|
+
const content = readFileSync(path, "utf8");
|
|
192
|
+
const headingIdx = content.search(/^##\s+Past Mistakes\s*$/m);
|
|
193
|
+
if (headingIdx === -1) return [];
|
|
194
|
+
|
|
195
|
+
// Slice from the heading to the start of the next ## heading (or EOF).
|
|
196
|
+
const fromHeading = content.slice(headingIdx);
|
|
197
|
+
const nextSectionRel = fromHeading.slice(2).search(/^##\s/m);
|
|
198
|
+
const section = nextSectionRel === -1 ? fromHeading : fromHeading.slice(0, nextSectionRel + 2);
|
|
199
|
+
|
|
200
|
+
const out = [];
|
|
201
|
+
for (const rawLine of section.split(/\r?\n/)) {
|
|
202
|
+
const line = rawLine.trim();
|
|
203
|
+
// Skip blank, heading, and the markdown table divider.
|
|
204
|
+
if (!line || line.startsWith("#")) continue;
|
|
205
|
+
if (/^\|[\s|:-]+\|$/.test(line)) continue;
|
|
206
|
+
if (!line.startsWith("|") || !line.endsWith("|")) continue;
|
|
207
|
+
|
|
208
|
+
// Parse pipe-delimited cells; strip leading/trailing pipe.
|
|
209
|
+
const inner = line.slice(1, -1);
|
|
210
|
+
const cells = inner.split("|").map((c) => c.trim());
|
|
211
|
+
if (cells.length < 2) continue;
|
|
212
|
+
|
|
213
|
+
// Header detection: literal "Date" first cell.
|
|
214
|
+
if (cells[0].toLowerCase() === "date") continue;
|
|
215
|
+
if (/^[-: ]+$/.test(cells[0])) continue;
|
|
216
|
+
|
|
217
|
+
out.push({
|
|
218
|
+
date: cells[0],
|
|
219
|
+
mistake: cells[1] ?? "",
|
|
220
|
+
lesson: cells[2] ?? null,
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
return out;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function pretty(scan) {
|
|
227
|
+
const total = scan.observations.length + scan.feedback.length + scan.claude_md.length;
|
|
228
|
+
if (total === 0) return "No prior mistakes recorded — proceed.\n";
|
|
229
|
+
|
|
230
|
+
const out = [`Past mistakes scanned: ${total} found across 3 surfaces.`, ""];
|
|
231
|
+
|
|
232
|
+
if (scan.observations.length > 0) {
|
|
233
|
+
out.push(`== observations.jsonl (${scan.observations.length}) ==`);
|
|
234
|
+
for (const o of scan.observations) {
|
|
235
|
+
const ts = o.ts ? `${o.ts} ` : "";
|
|
236
|
+
out.push(` [line ${o.line}] ${ts}${o.type}: ${o.summary}`);
|
|
237
|
+
}
|
|
238
|
+
out.push("");
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
if (scan.feedback.length > 0) {
|
|
242
|
+
out.push(`== feedback memories (${scan.feedback.length}) ==`);
|
|
243
|
+
for (const f of scan.feedback) {
|
|
244
|
+
out.push(` [${f.file}] ${f.name ?? ""}`);
|
|
245
|
+
if (f.description) out.push(` — ${f.description}`);
|
|
246
|
+
}
|
|
247
|
+
out.push("");
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (scan.claude_md.length > 0) {
|
|
251
|
+
out.push(`== CLAUDE.md Past Mistakes (${scan.claude_md.length}) ==`);
|
|
252
|
+
for (const e of scan.claude_md) {
|
|
253
|
+
out.push(` [${e.date}] ${e.mistake}`);
|
|
254
|
+
if (e.lesson) out.push(` Lesson: ${e.lesson}`);
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return out.join("\n") + "\n";
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function main() {
|
|
262
|
+
const args = parseArgs();
|
|
263
|
+
const obsPath = args.observations ?? defaultObservationsPath(args.root);
|
|
264
|
+
const memDir = args.memoryDir ?? defaultMemoryDir(args.root);
|
|
265
|
+
const claudeMd = args.claudeMd ?? defaultClaudeMdPath(args.root);
|
|
266
|
+
|
|
267
|
+
const scan = {
|
|
268
|
+
observations: scanObservations(obsPath, args.maxObservations),
|
|
269
|
+
feedback: scanFeedbackMemories(memDir),
|
|
270
|
+
claude_md: scanClaudeMdPastMistakes(claudeMd),
|
|
271
|
+
sources: {
|
|
272
|
+
observations: obsPath,
|
|
273
|
+
memory_dir: memDir,
|
|
274
|
+
claude_md: claudeMd,
|
|
275
|
+
},
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
if (args.json) {
|
|
279
|
+
stdout.write(JSON.stringify(scan, null, 2) + "\n");
|
|
280
|
+
} else {
|
|
281
|
+
stdout.write(pretty(scan));
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
main();
|
|
@@ -21,7 +21,7 @@ This skill defines the receipt that closes that gap, without modifying the vendo
|
|
|
21
21
|
Activate when ALL of the following are true:
|
|
22
22
|
|
|
23
23
|
1. A merge into the deploy branch (typically `main` or `master`) has just landed
|
|
24
|
-
2.
|
|
24
|
+
2. `bash "${CLAUDE_PLUGIN_ROOT}/scripts/detect-deploy-target.sh"` (source: `scripts/detect-deploy-target.sh`) returns a value other than `none` at the repo root. The script encodes the full file-marker table — `railway.toml` / `railway.json` → `railway`, `wrangler.toml` / `wrangler.jsonc` → `cloudflare`, `vercel.json` / `.vercel/` → `vercel`, `netlify.toml` → `netlify`, `fly.toml` → `fly`, `app.yaml` → `appengine`, `apprunner.yaml` → `apprunner`, `.github/workflows/*.yml` with a `deploy:` job → `gha-deploy`. First match wins, in that order. The script is the source of truth; the file list above is documentation
|
|
25
25
|
3. `finishing-a-development-branch` has reported "merged" — not "PR opened", not "review pending"
|
|
26
26
|
|
|
27
27
|
Do NOT activate when:
|
|
@@ -45,7 +45,7 @@ The skill is provider-aware but never hardcodes a specific API key or token shap
|
|
|
45
45
|
|
|
46
46
|
### Route A — Provider CLI (preferred when authenticated)
|
|
47
47
|
|
|
48
|
-
The CLI is the highest-fidelity source. Run
|
|
48
|
+
The CLI is the highest-fidelity source. Run `bash "${CLAUDE_PLUGIN_ROOT}/scripts/get-deployed-sha.sh" <provider>` (source: `scripts/get-deployed-sha.sh`) — the script owns the per-provider pipeline (CLI + jq filter) and prints just the SHA on stdout. Inspect the pipeline shape without executing via `bash "${CLAUDE_PLUGIN_ROOT}/scripts/get-deployed-sha.sh" --show-command <provider>`.
|
|
49
49
|
|
|
50
50
|
Provider-to-pipeline map (cited from the script, not redefined here):
|
|
51
51
|
|
|
@@ -97,7 +97,7 @@ A second Claude/Codex/Maulana session can be running on the same host and the sa
|
|
|
97
97
|
|
|
98
98
|
**On the first Edit / Write / mutating Bash of a session:**
|
|
99
99
|
|
|
100
|
-
Run
|
|
100
|
+
Run `bash "${CLAUDE_PLUGIN_ROOT}/scripts/git-state-snapshot.sh"` (source: `scripts/git-state-snapshot.sh`) and quote its JSON envelope verbatim. Example output:
|
|
101
101
|
|
|
102
102
|
```
|
|
103
103
|
{"head":"966ce51","upstream":"966ce51","dirty":0,"root":"/path/to/repo","branch":"main"}
|
|
@@ -115,7 +115,7 @@ If `upstream` is `"none"` or `branch` is `"detached"`, say so explicitly. Do not
|
|
|
115
115
|
|
|
116
116
|
**On every subsequent Edit / Write / mutating Bash, before allowing the action:**
|
|
117
117
|
|
|
118
|
-
Re-run
|
|
118
|
+
Re-run `bash "${CLAUDE_PLUGIN_ROOT}/scripts/git-state-snapshot.sh"` (source: `scripts/git-state-snapshot.sh`) and diff against the baseline:
|
|
119
119
|
|
|
120
120
|
1. `head` — has it advanced past your baseline without your commits?
|
|
121
121
|
2. `upstream` — did upstream move while you worked?
|
|
@@ -135,7 +135,7 @@ Before research begins, the skill must read its own track record. The instinct s
|
|
|
135
135
|
|
|
136
136
|
### Rule 1 — Acknowledge before context (right context from the beginning)
|
|
137
137
|
|
|
138
|
-
Run
|
|
138
|
+
Run `node "${CLAUDE_PLUGIN_ROOT}/scripts/scan-past-mistakes.mjs"` (source: `scripts/scan-past-mistakes.mjs`) at the project root. Scan three surfaces in one pass and surface every entry with a citation:
|
|
139
139
|
|
|
140
140
|
- `~/.claude/instincts/<project-hash>/observations.jsonl` — last N (default 10) entries with `type: failure` or `correction` (legacy `event` field also matched for pre-2026-05-06 rows)
|
|
141
141
|
- `~/.claude/projects/<project-hash>/memory/feedback_*.md` — every file whose frontmatter declares `type: feedback`; the canonical home of the operator's named corrections (e.g. `feedback_past_mistake_gate.md`, `feedback_no_git_add_all_on_windows.md`)
|
|
@@ -202,7 +202,7 @@ For each item in the ORIGINAL order:
|
|
|
202
202
|
|
|
203
203
|
### Routing Table (with Inline Fallbacks)
|
|
204
204
|
|
|
205
|
-
Run
|
|
205
|
+
Run `node "${CLAUDE_PLUGIN_ROOT}/scripts/route-recommendation.mjs" "<item>"` (source: `scripts/route-recommendation.mjs`) to match a single recommendation item to its preferred chain + inline fallback. Default mode prints the matched row; `--json` for programmatic consumption; `--list` enumerates every row. The programmatic source of truth is `scripts/route-recommendation.routes.json` — the table below is the human-readable documentation that mirrors it. If they drift, the routes.json file wins.
|
|
206
206
|
|
|
207
207
|
Rows whose **Preferred skill** is not bundled with the `continuous-improvement` plugin carry a `(Reference behavior — does not require <skill>.)` marker on the fallback cell. The marker makes the soft-dependency contract visible at point of use: the inline fallback is fully self-contained and runs without that skill installed. Rows whose preferred skill ships with the plugin (`ralph`, `tdd-workflow`, `continuous-improvement`) carry no marker — the dedicated skill is always available.
|
|
208
208
|
|
|
@@ -40,7 +40,7 @@ When another session/loop may be active, do not assume the tree is yours:
|
|
|
40
40
|
- An in-progress `MERGE_HEAD` / `rebase-merge` you did not start means another actor is mid-operation. Do not "help" by editing conflicted files — wait, or hand off.
|
|
41
41
|
- Re-read the current branch immediately before any mutation; if it shifted since your snapshot, re-survey from the top.
|
|
42
42
|
- If `.git/index` keeps changing while you are idle, a writer is active. Pause and surface it rather than racing.
|
|
43
|
-
- If `gateguard` is installed, its Parallel-Actor Gate already captured this baseline on the session's first mutation (
|
|
43
|
+
- If `gateguard` is installed, its Parallel-Actor Gate already captured this baseline on the session's first mutation by running `bash "${CLAUDE_PLUGIN_ROOT}/scripts/git-state-snapshot.sh"` (source: `scripts/git-state-snapshot.sh`) and divergence-checks every later mutation — `reconcile` complements that gate, it does not replace it. Without gateguard, run the snapshot above yourself.
|
|
44
44
|
|
|
45
45
|
## Classify, Then Act
|
|
46
46
|
|