continuous-improvement 3.23.0 → 3.25.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.
Files changed (78) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/CHANGELOG.md +37 -0
  3. package/QUICKSTART.md +19 -20
  4. package/README.md +71 -18
  5. package/SKILL.md +4 -0
  6. package/bin/check-invariant-count.mjs +144 -0
  7. package/bin/check-routing-targets.mjs +74 -4
  8. package/bin/check-test-count.mjs +135 -0
  9. package/bin/companion-preference-status.mjs +2 -5
  10. package/bin/generate-plugin-manifests.mjs +21 -2
  11. package/bin/harvest-friction.mjs +10 -8
  12. package/bin/install.mjs +4 -14
  13. package/bin/mcp-server.mjs +4 -9
  14. package/bin/observe.mjs +3 -3
  15. package/bin/reconcile-instinct-hashes.mjs +226 -0
  16. package/bin/refresh-third-party.mjs +180 -10
  17. package/commands/discipline.md +5 -2
  18. package/commands/reconcile.md +1 -1
  19. package/commands/superpowers.md +2 -2
  20. package/commands/verify-install.md +8 -3
  21. package/hooks/companion-preference.mjs +3 -10
  22. package/hooks/config-guard.mjs +94 -0
  23. package/hooks/gateguard.mjs +39 -39
  24. package/hooks/goal-drift-stop.mjs +2 -2
  25. package/hooks/query-cost-nudge.mjs +2 -2
  26. package/hooks/recall-briefing.mjs +2 -2
  27. package/hooks/route-prompt.mjs +2 -5
  28. package/hooks/session.mjs +2 -2
  29. package/hooks/workflow-distill.mjs +2 -2
  30. package/lib/config-guard-gate.mjs +243 -0
  31. package/lib/destructive-bash.mjs +216 -0
  32. package/lib/gateguard-state.mjs +5 -1
  33. package/lib/plugin-metadata.mjs +12 -1
  34. package/lib/skill-catalog.mjs +169 -0
  35. package/llms.txt +12 -1
  36. package/package.json +5 -3
  37. package/plugins/beginner.json +1 -1
  38. package/plugins/continuous-improvement/.claude-plugin/marketplace.json +2 -2
  39. package/plugins/continuous-improvement/.claude-plugin/plugin.json +2 -2
  40. package/plugins/continuous-improvement/README.md +1 -2
  41. package/plugins/continuous-improvement/bin/mcp-server.mjs +4 -9
  42. package/plugins/continuous-improvement/bin/observe.mjs +3 -3
  43. package/plugins/continuous-improvement/commands/discipline.md +5 -2
  44. package/plugins/continuous-improvement/commands/reconcile.md +1 -1
  45. package/plugins/continuous-improvement/commands/superpowers.md +2 -2
  46. package/plugins/continuous-improvement/commands/verify-install.md +8 -3
  47. package/plugins/continuous-improvement/hooks/companion-preference.mjs +3 -10
  48. package/plugins/continuous-improvement/hooks/config-guard.mjs +94 -0
  49. package/plugins/continuous-improvement/hooks/gateguard.mjs +39 -39
  50. package/plugins/continuous-improvement/hooks/goal-drift-stop.mjs +2 -2
  51. package/plugins/continuous-improvement/hooks/hooks.json +10 -0
  52. package/plugins/continuous-improvement/hooks/query-cost-nudge.mjs +2 -2
  53. package/plugins/continuous-improvement/hooks/recall-briefing.mjs +2 -2
  54. package/plugins/continuous-improvement/hooks/route-prompt.mjs +2 -5
  55. package/plugins/continuous-improvement/hooks/session.mjs +2 -2
  56. package/plugins/continuous-improvement/hooks/workflow-distill.mjs +2 -2
  57. package/plugins/continuous-improvement/lib/config-guard-gate.mjs +243 -0
  58. package/plugins/continuous-improvement/lib/destructive-bash.mjs +216 -0
  59. package/plugins/continuous-improvement/lib/gateguard-state.mjs +5 -1
  60. package/plugins/continuous-improvement/lib/plugin-metadata.mjs +12 -1
  61. package/plugins/continuous-improvement/scripts/route-recommendation.routes.json +2 -2
  62. package/plugins/continuous-improvement/skills/README.md +0 -1
  63. package/plugins/continuous-improvement/skills/continuous-improvement/SKILL.md +4 -0
  64. package/plugins/continuous-improvement/skills/deploy-receipt/SKILL.md +1 -1
  65. package/plugins/continuous-improvement/skills/gateguard/SKILL.md +17 -2
  66. package/plugins/continuous-improvement/skills/proceed-with-the-recommendation/SKILL.md +2 -2
  67. package/plugins/continuous-improvement/skills/reconcile/SKILL.md +0 -1
  68. package/plugins/continuous-improvement/skills/superpowers/SKILL.md +5 -6
  69. package/plugins/expert.json +1 -1
  70. package/scripts/route-recommendation.routes.json +2 -2
  71. package/skills/README.md +1 -2
  72. package/skills/deploy-receipt.md +1 -1
  73. package/skills/gateguard.md +17 -2
  74. package/skills/proceed-with-the-recommendation.md +2 -2
  75. package/skills/reconcile.md +0 -1
  76. package/skills/superpowers.md +5 -6
  77. package/plugins/continuous-improvement/skills/safety-guard/SKILL.md +0 -77
  78. package/skills/safety-guard.md +0 -77
@@ -0,0 +1,226 @@
1
+ #!/usr/bin/env node
2
+ /**
3
+ * Merge instinct buckets that are the same project under different path
4
+ * spellings (C:/ vs c:/ vs C:\). observe/gateguard now hash the canonical
5
+ * root; this CLI copies history into that hash and leaves an alias marker
6
+ * on the old dir. Idempotent. Never deletes a directory.
7
+ *
8
+ * Usage:
9
+ * node bin/reconcile-instinct-hashes.mjs --dry-run
10
+ * node bin/reconcile-instinct-hashes.mjs --apply
11
+ * node bin/reconcile-instinct-hashes.mjs --apply --only 3ef4426c6e15 --only 137f2f54ec70
12
+ */
13
+ import { copyFileSync, existsSync, mkdirSync, readFileSync, readdirSync, renameSync, statSync, writeFileSync } from "node:fs";
14
+ import { join } from "node:path";
15
+ import { argv, exit } from "node:process";
16
+ import { canonicalizeProjectRoot, hashProjectRoot, resolveInstinctsRoot } from "../lib/gateguard-state.mjs";
17
+ function countJsonl(path) {
18
+ if (!existsSync(path))
19
+ return 0;
20
+ return readFileSync(path, "utf8").split(/\n/).filter((line) => line.trim() !== "").length;
21
+ }
22
+ function isoNow() {
23
+ return new Date().toISOString().replace(/\.\d{3}Z$/, "Z");
24
+ }
25
+ export function discoverGroups(instinctsRoot) {
26
+ const groups = new Map();
27
+ let entries;
28
+ try {
29
+ entries = readdirSync(instinctsRoot);
30
+ }
31
+ catch {
32
+ return [];
33
+ }
34
+ for (const name of entries) {
35
+ if (name === "global")
36
+ continue;
37
+ const dir = join(instinctsRoot, name);
38
+ try {
39
+ if (!statSync(dir).isDirectory())
40
+ continue;
41
+ }
42
+ catch {
43
+ continue;
44
+ }
45
+ if (existsSync(join(dir, "alias.json")) && !existsSync(join(dir, "observations.jsonl"))) {
46
+ continue;
47
+ }
48
+ const projectPath = join(dir, "project.json");
49
+ if (!existsSync(projectPath))
50
+ continue;
51
+ let project;
52
+ try {
53
+ project = JSON.parse(readFileSync(projectPath, "utf8"));
54
+ }
55
+ catch {
56
+ continue;
57
+ }
58
+ if (!project.root)
59
+ continue;
60
+ const canonicalRoot = canonicalizeProjectRoot(project.root);
61
+ const canonicalHash = hashProjectRoot(canonicalRoot);
62
+ const bucket = {
63
+ hash: name,
64
+ root: project.root,
65
+ name: project.name ?? name,
66
+ rows: countJsonl(join(dir, "observations.jsonl")),
67
+ };
68
+ const existing = groups.get(canonicalHash);
69
+ if (existing) {
70
+ existing.members.push(bucket);
71
+ }
72
+ else {
73
+ groups.set(canonicalHash, {
74
+ canonicalHash,
75
+ canonicalRoot,
76
+ name: project.name ?? name,
77
+ members: [bucket],
78
+ });
79
+ }
80
+ }
81
+ return [...groups.values()].filter((group) => group.members.some((member) => member.hash !== group.canonicalHash && member.rows > 0));
82
+ }
83
+ function parseTs(line) {
84
+ try {
85
+ const parsed = JSON.parse(line);
86
+ const value = Date.parse(parsed.ts ?? "");
87
+ return Number.isFinite(value) ? value : 0;
88
+ }
89
+ catch {
90
+ return 0;
91
+ }
92
+ }
93
+ export function mergeJsonl(sources) {
94
+ const rows = [];
95
+ const seen = new Set();
96
+ for (const file of sources) {
97
+ if (!existsSync(file))
98
+ continue;
99
+ for (const line of readFileSync(file, "utf8").split(/\n/)) {
100
+ const trimmed = line.trim();
101
+ if (!trimmed || seen.has(trimmed))
102
+ continue;
103
+ seen.add(trimmed);
104
+ rows.push(trimmed);
105
+ }
106
+ }
107
+ rows.sort((a, b) => parseTs(a) - parseTs(b) || a.localeCompare(b));
108
+ return rows.length === 0 ? "" : `${rows.join("\n")}\n`;
109
+ }
110
+ export function selectGroups(groups, only) {
111
+ if (only.length === 0)
112
+ return groups;
113
+ const wanted = new Set(only);
114
+ return groups.filter((group) => wanted.has(group.canonicalHash) || group.members.some((member) => wanted.has(member.hash)));
115
+ }
116
+ export function applyGroup(instinctsRoot, group) {
117
+ const destDir = join(instinctsRoot, group.canonicalHash);
118
+ mkdirSync(destDir, { recursive: true });
119
+ const destObs = join(destDir, "observations.jsonl");
120
+ const sources = group.members
121
+ .map((member) => join(instinctsRoot, member.hash, "observations.jsonl"))
122
+ .filter((path) => existsSync(path));
123
+ const merged = mergeJsonl(sources);
124
+ writeFileSync(destObs, merged, "utf8");
125
+ let createdAt = isoNow();
126
+ const destProject = join(destDir, "project.json");
127
+ if (existsSync(destProject)) {
128
+ try {
129
+ const existing = JSON.parse(readFileSync(destProject, "utf8"));
130
+ if (existing.created_at)
131
+ createdAt = existing.created_at;
132
+ }
133
+ catch {
134
+ // rewrite below
135
+ }
136
+ }
137
+ writeFileSync(destProject, `${JSON.stringify({
138
+ id: group.canonicalHash,
139
+ name: group.name,
140
+ root: group.canonicalRoot,
141
+ created_at: createdAt,
142
+ })}\n`, "utf8");
143
+ const aliases = [];
144
+ for (const member of group.members) {
145
+ if (member.hash === group.canonicalHash)
146
+ continue;
147
+ const srcDir = join(instinctsRoot, member.hash);
148
+ try {
149
+ for (const file of readdirSync(srcDir)) {
150
+ if (!file.endsWith(".yaml"))
151
+ continue;
152
+ const dest = join(destDir, file);
153
+ if (!existsSync(dest))
154
+ copyFileSync(join(srcDir, file), dest);
155
+ }
156
+ }
157
+ catch {
158
+ // missing dir is non-fatal
159
+ }
160
+ const srcObs = join(srcDir, "observations.jsonl");
161
+ if (existsSync(srcObs)) {
162
+ renameSync(srcObs, join(srcDir, `observations.migrated-to-${group.canonicalHash}.jsonl`));
163
+ }
164
+ writeFileSync(join(srcDir, "alias.json"), `${JSON.stringify({
165
+ canonical: group.canonicalHash,
166
+ canonical_root: group.canonicalRoot,
167
+ migrated_at: isoNow(),
168
+ rows: member.rows,
169
+ })}\n`, "utf8");
170
+ aliases.push(member.hash);
171
+ }
172
+ const copiedRows = merged === "" ? 0 : merged.trim().split("\n").length;
173
+ return { copiedRows, aliases };
174
+ }
175
+ function printPlan(groups) {
176
+ if (groups.length === 0) {
177
+ console.log("No alias observation buckets to merge.");
178
+ return;
179
+ }
180
+ console.log(`Alias groups: ${groups.length}`);
181
+ for (const group of groups) {
182
+ console.log(` ${group.name} → ${group.canonicalHash} (${group.canonicalRoot})`);
183
+ for (const member of group.members) {
184
+ const mark = member.hash === group.canonicalHash ? "canonical" : "alias";
185
+ console.log(` ${member.hash} ${member.rows} rows ${mark} root=${member.root}`);
186
+ }
187
+ }
188
+ }
189
+ function parseOnly(args) {
190
+ const only = [];
191
+ for (let i = 0; i < args.length; i += 1) {
192
+ if (args[i] === "--only" && args[i + 1]) {
193
+ only.push(args[i + 1]);
194
+ i += 1;
195
+ }
196
+ }
197
+ return only;
198
+ }
199
+ function main() {
200
+ const args = argv.slice(2);
201
+ const apply = args.includes("--apply");
202
+ const dryRun = args.includes("--dry-run") || !apply;
203
+ const instinctsRoot = resolveInstinctsRoot();
204
+ const groups = selectGroups(discoverGroups(instinctsRoot), parseOnly(args));
205
+ printPlan(groups);
206
+ if (dryRun) {
207
+ if (groups.length > 0)
208
+ console.log("\nRe-run with --apply to merge.");
209
+ return;
210
+ }
211
+ for (const group of groups) {
212
+ const result = applyGroup(instinctsRoot, group);
213
+ console.log(`merged ${result.copiedRows} rows into ${group.canonicalHash}; aliases ${result.aliases.join(",") || "(none)"}`);
214
+ }
215
+ }
216
+ const invokedDirectly = argv[1]?.endsWith("reconcile-instinct-hashes.mjs");
217
+ if (invokedDirectly) {
218
+ try {
219
+ main();
220
+ }
221
+ catch (error) {
222
+ const message = error instanceof Error ? error.message : String(error);
223
+ console.error(message);
224
+ exit(1);
225
+ }
226
+ }
@@ -22,7 +22,10 @@
22
22
  * wipe the local path, recreate it, copy the selective surface
23
23
  * verbatim, strip every CLAUDE.md inside the snapshot, print a diff
24
24
  * stat. Aborts if the local snapshot path has uncommitted changes
25
- * unless --force is passed.
25
+ * unless --force is passed, and aborts BEFORE the wipe if upstream
26
+ * removed a skill this repo still routes to, unless --allow-stale-refs
27
+ * is passed. The two flags are separate on purpose: a dirty-tree
28
+ * refresh must not silently disable the stale-reference gate.
26
29
  *
27
30
  * node bin/refresh-third-party.mjs --all
28
31
  * node bin/refresh-third-party.mjs --all --check
@@ -38,8 +41,9 @@
38
41
  */
39
42
  import { spawnSync } from "node:child_process";
40
43
  import { cp, mkdir, readFile, rm, stat, writeFile } from "node:fs/promises";
44
+ import { readdirSync, readFileSync, statSync } from "node:fs";
41
45
  import { tmpdir } from "node:os";
42
- import { dirname, join, resolve } from "node:path";
46
+ import { dirname, join, relative, resolve } from "node:path";
43
47
  import { argv, exit, stderr, stdout } from "node:process";
44
48
  import { fileURLToPath } from "node:url";
45
49
  const SCRIPT_DIR = dirname(fileURLToPath(import.meta.url));
@@ -56,6 +60,7 @@ const SNAPSHOTS = [
56
60
  manifestHeading: "oh-my-claudecode",
57
61
  upstream: "https://github.com/Yeachan-Heo/oh-my-claudecode.git",
58
62
  localPath: "third-party/oh-my-claudecode",
63
+ routingPrefix: "oh-my-claudecode",
59
64
  selectiveDirs: [
60
65
  "agents",
61
66
  "skills",
@@ -94,6 +99,7 @@ const SNAPSHOTS = [
94
99
  manifestHeading: "obra/superpowers",
95
100
  upstream: "https://github.com/obra/superpowers.git",
96
101
  localPath: "third-party/superpowers",
102
+ routingPrefix: "superpowers",
97
103
  selectiveDirs: ["skills", "hooks", "docs", "assets", ".claude-plugin"],
98
104
  selectiveFiles: [
99
105
  "LICENSE",
@@ -116,7 +122,7 @@ function usage() {
116
122
  "Usage:",
117
123
  " node bin/refresh-third-party.mjs --list",
118
124
  " node bin/refresh-third-party.mjs <name> --check",
119
- " node bin/refresh-third-party.mjs <name> [--force]",
125
+ " node bin/refresh-third-party.mjs <name> [--force] [--allow-stale-refs]",
120
126
  " node bin/refresh-third-party.mjs --all [--check] [--force]",
121
127
  " node bin/refresh-third-party.mjs --help",
122
128
  "",
@@ -198,6 +204,7 @@ function localPathDirty(relPath) {
198
204
  }
199
205
  async function deleteClaudeMdRecursive(root) {
200
206
  // Node-only equivalent of `find <root> -name CLAUDE.md -type f -delete`.
207
+ // (helpers for our own in-snapshot annotations are defined below)
201
208
  const { readdir } = await import("node:fs/promises");
202
209
  const stack = [root];
203
210
  let deleted = 0;
@@ -239,7 +246,130 @@ async function checkOne(snapshot) {
239
246
  await rm(dir, { recursive: true, force: true });
240
247
  }
241
248
  }
242
- async function refreshOne(snapshot, { force }) {
249
+ /**
250
+ * Flat-source files and directories that can name a companion skill. Deliberately
251
+ * excludes `third-party/` — scanning the snapshot being replaced would match the
252
+ * very copy we are about to overwrite and report every removal as still-referenced.
253
+ * The `plugins/` tree is excluded too: it is a build mirror of these files.
254
+ */
255
+ export const REFERENCE_SOURCES = [
256
+ "optional-companions.json",
257
+ "skills",
258
+ "commands",
259
+ "scripts",
260
+ join("src", "hooks"),
261
+ ];
262
+ /** Skill directory names inside a snapshot, sorted. Empty if there is no skills/ dir. */
263
+ export function listSkillDirs(snapshotRoot) {
264
+ try {
265
+ return readdirSync(join(snapshotRoot, "skills"), { withFileTypes: true })
266
+ .filter((e) => e.isDirectory())
267
+ .map((e) => e.name)
268
+ .sort();
269
+ }
270
+ catch {
271
+ return [];
272
+ }
273
+ }
274
+ /** Skills the old snapshot had that the incoming one does not. */
275
+ export function findRemovedSkills(oldSkills, newSkills) {
276
+ const incoming = new Set(newSkills);
277
+ return oldSkills.filter((s) => !incoming.has(s));
278
+ }
279
+ function walkFiles(abs, out) {
280
+ let entries;
281
+ try {
282
+ entries = readdirSync(abs, { withFileTypes: true });
283
+ }
284
+ catch {
285
+ return;
286
+ }
287
+ for (const e of entries) {
288
+ const full = join(abs, e.name);
289
+ if (e.isDirectory())
290
+ walkFiles(full, out);
291
+ else
292
+ out.push(full);
293
+ }
294
+ }
295
+ /**
296
+ * Removed skills that our own source still routes to, with the files naming them.
297
+ *
298
+ * This is the check that turns a refresh into a loud failure instead of a silent
299
+ * one: when OMC 5.x deleted `ultrawork`, the snapshot became honest while our
300
+ * routing table kept pointing at it, and every gate stayed green. Matching is on
301
+ * the prefixed `<plugin>:<skill>` form with a boundary, so a bare English word
302
+ * ("release", "review") in prose is not a false positive and `omc:ultrawork-plus`
303
+ * does not match `omc:ultrawork`.
304
+ */
305
+ export function findStaleReferences(repoRoot, routingPrefix, removedSkills) {
306
+ if (removedSkills.length === 0)
307
+ return [];
308
+ const files = [];
309
+ for (const rel of REFERENCE_SOURCES) {
310
+ const abs = join(repoRoot, rel);
311
+ let isDir = false;
312
+ try {
313
+ isDir = statSync(abs).isDirectory();
314
+ }
315
+ catch {
316
+ continue;
317
+ }
318
+ if (isDir)
319
+ walkFiles(abs, files);
320
+ else
321
+ files.push(abs);
322
+ }
323
+ const stale = [];
324
+ for (const skill of removedSkills) {
325
+ const re = new RegExp(`${escapeRegExp(routingPrefix)}:${escapeRegExp(skill)}(?![A-Za-z0-9_-])`);
326
+ const hits = [];
327
+ for (const file of files) {
328
+ let text;
329
+ try {
330
+ text = readFileSync(file, "utf8");
331
+ }
332
+ catch {
333
+ continue;
334
+ }
335
+ if (re.test(text))
336
+ hits.push(relative(repoRoot, file));
337
+ }
338
+ if (hits.length > 0)
339
+ stale.push({ skill, files: hits.sort() });
340
+ }
341
+ return stale;
342
+ }
343
+ function escapeRegExp(s) {
344
+ return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
345
+ }
346
+ /**
347
+ * Files inside a snapshot directory that we author, not upstream. The refresh
348
+ * wipes the directory wholesale, so these have to be carried across it by hand.
349
+ * `OUR_NOTES.md` is the drift radar the vendoring contract requires;
350
+ * `.fork-only-skills.txt` is the allowlist the Skills Drift Check subtracts.
351
+ */
352
+ export const OUR_FILES = ["OUR_NOTES.md", ".fork-only-skills.txt"];
353
+ /** Read whichever of OUR_FILES exist under `localAbs`. Absent files are skipped. */
354
+ export async function readOurFiles(localAbs) {
355
+ const saved = new Map();
356
+ for (const name of OUR_FILES) {
357
+ try {
358
+ saved.set(name, await readFile(join(localAbs, name)));
359
+ }
360
+ catch {
361
+ // Not every snapshot carries every one of these; absence is normal.
362
+ }
363
+ }
364
+ return saved;
365
+ }
366
+ /** Write the captured files back under `localAbs`. Returns how many were restored. */
367
+ export async function restoreOurFiles(localAbs, saved) {
368
+ for (const [name, body] of saved)
369
+ await writeFile(join(localAbs, name), body);
370
+ return saved.size;
371
+ }
372
+ async function refreshOne(snapshot, { force, allowStaleRefs }) {
243
373
  const pinned = await readPinnedSha(snapshot);
244
374
  const localAbs = join(REPO_ROOT, snapshot.localPath);
245
375
  if (!force && (await pathExists(localAbs))) {
@@ -254,6 +384,31 @@ async function refreshOne(snapshot, { force }) {
254
384
  throw new Error(`[${snapshot.name}] upstream HEAD ${headSha} != pinned ${pinned}; ` +
255
385
  `bump the pinned SHA in third-party/MANIFEST.md first`);
256
386
  }
387
+ // Refuse to land a snapshot that would make our own routing table lie.
388
+ // When OMC 5.x deleted `ultrawork`, refreshing made the snapshot honest while
389
+ // optional-companions.json, two skills/superpowers.md tables and the
390
+ // companion-preference OVERRIDES map kept naming it — and every gate stayed
391
+ // green, because nothing compared the two. Checked BEFORE the wipe, so a
392
+ // failure leaves the snapshot untouched and the operator retargets first.
393
+ const removedSkills = findRemovedSkills(listSkillDirs(localAbs), listSkillDirs(dir));
394
+ const staleRefs = findStaleReferences(REPO_ROOT, snapshot.routingPrefix, removedSkills);
395
+ if (staleRefs.length > 0) {
396
+ const detail = staleRefs
397
+ .map((r) => ` ${snapshot.routingPrefix}:${r.skill}\n ${r.files.join("\n ")}`)
398
+ .join("\n");
399
+ const message = `[${snapshot.name}] upstream removed ${staleRefs.length} skill(s) this repo still routes to:\n${detail}\n` +
400
+ ` Retarget or drop these references first, then re-run the refresh. ` +
401
+ `Pass --allow-stale-refs to land the snapshot anyway and fix them afterwards.`;
402
+ if (!allowStaleRefs)
403
+ throw new Error(message);
404
+ err(` WARNING: ${message}`);
405
+ }
406
+ // Our own annotations live inside the snapshot directory, so the wipe below
407
+ // destroys them along with the upstream copy. That silently deleted
408
+ // OUR_NOTES.md on both the superpowers (#304) and oh-my-claudecode (#308)
409
+ // refreshes — the drift radar, removed by the tool whose drift it records.
410
+ // Capture before the wipe, put back after the copy.
411
+ const ourFiles = await readOurFiles(localAbs);
257
412
  // Wipe + recreate destination.
258
413
  await rm(localAbs, { recursive: true, force: true });
259
414
  await mkdir(localAbs, { recursive: true });
@@ -279,6 +434,10 @@ async function refreshOne(snapshot, { force }) {
279
434
  }
280
435
  // Strip every CLAUDE.md (auto-loads as session context).
281
436
  const stripped = await deleteClaudeMdRecursive(localAbs);
437
+ // Put our annotations back. After the CLAUDE.md strip, so a snapshot that
438
+ // ever carries an OUR_* named CLAUDE.md is not re-deleted; before the diff
439
+ // stat, so the printed diff reflects what actually landed on disk.
440
+ const preserved = await restoreOurFiles(localAbs, ourFiles);
282
441
  // Defense in depth: forcibly delete excludePostCopy paths from the
283
442
  // local snapshot, regardless of whether they were copied. Guards
284
443
  // against silent regressions if selectiveDirs is later edited to
@@ -322,7 +481,7 @@ async function refreshOne(snapshot, { force }) {
322
481
  log(` upstream : ${snapshot.upstream}`);
323
482
  log(` pinned SHA : ${pinned}`);
324
483
  log(` upstream HEAD: ${headSha}`);
325
- log(` status : updated (${copiedDirs} dirs, ${copiedFiles} files, ${stripped} CLAUDE.md stripped, ${excluded} excluded paths removed, ${patchedKeys} json keys patched)`);
484
+ log(` status : updated (${copiedDirs} dirs, ${copiedFiles} files, ${stripped} CLAUDE.md stripped, ${preserved} of ours preserved, ${excluded} excluded paths removed, ${patchedKeys} json keys patched)`);
326
485
  if (String(diffStat.stdout).trim()) {
327
486
  log(" diff --stat :");
328
487
  for (const line of String(diffStat.stdout).trimEnd().split(/\r?\n/)) {
@@ -368,6 +527,10 @@ async function main() {
368
527
  exit(0);
369
528
  }
370
529
  const force = args.includes("--force");
530
+ // Deliberately NOT --force: that means "my tree is dirty, proceed", and a
531
+ // routine dirty-tree refresh must not silently switch off the stale-reference
532
+ // gate as a side effect.
533
+ const allowStaleRefs = args.includes("--allow-stale-refs");
371
534
  const check = args.includes("--check");
372
535
  const all = args.includes("--all");
373
536
  const positional = args.filter((a) => !a.startsWith("--"));
@@ -397,7 +560,7 @@ async function main() {
397
560
  allUpToDate = false;
398
561
  }
399
562
  else {
400
- await refreshOne(snap, { force });
563
+ await refreshOne(snap, { force, allowStaleRefs });
401
564
  }
402
565
  }
403
566
  catch (e) {
@@ -412,7 +575,14 @@ async function main() {
412
575
  exit(1);
413
576
  exit(0);
414
577
  }
415
- main().catch((e) => {
416
- err(`fatal: ${e.stack || e.message}`);
417
- exit(1);
418
- });
578
+ // Only run the CLI when invoked as one. Without this, importing the module to
579
+ // test its helpers would start a refresh. Same shape as the other bin/ scripts:
580
+ // the endsWith fallback covers Windows, where argv[1] is a backslash path and
581
+ // never equals the forward-slash file:// URL.
582
+ const invokedDirectly = argv[1] !== undefined && import.meta.url.endsWith(argv[1].replace(/\\/g, "/"));
583
+ if (invokedDirectly || argv[1]?.endsWith("refresh-third-party.mjs")) {
584
+ main().catch((e) => {
585
+ err(`fatal: ${e.stack || e.message}`);
586
+ exit(1);
587
+ });
588
+ }
@@ -19,6 +19,8 @@ Print this card and check yourself against each law.
19
19
  | 6 | **Iterate One Change** | Am I changing one thing at a time? | "And also..." |
20
20
  | 7 | **Learn From Every Session** | Did I capture this as an instinct? | "Next time I'll..." |
21
21
 
22
+ Read the seven as three moments around every act. **Before** (Laws 1, 2): set the terms. **During** (Laws 3, 6): watch yourself. **After** (Laws 4, 5, 7): settle the account and carry it forward. Every check is an audit you run on yourself; every red flag is you hoping instead. The sentence this comes from, and its sources: `docs/philosophy.md`.
23
+
22
24
  ## Operator Stakes
23
25
 
24
26
  The Laws above are the *how*. These five principles are the *why*: code ships from your account, the incident lands on your pager, the bill hits your budget. Each one pairs with the Law that prevents it from going wrong.
@@ -31,7 +33,7 @@ The Laws above are the *how*. These five principles are the *why*: code ships fr
31
33
  | 4 | **Problem framing** | Builds the websocket chat the ticket asked for | Finds out users wanted faster support replies, not chat | 1 |
32
34
  | 5 | **Constraints management** | Calls the $0.02/image model on every upload | Does the math, adds client-side validation + caching + cheaper triage model | 2 |
33
35
 
34
- Code is a liability, not an asset. Speed without these five turns into someone else's incident at 3am — except the someone is you.
36
+ Code is a liability, not an asset. Speed without these five turns into someone else's incident at 3am — except the someone is you. The other half of the why is not fear: the session ends and the context is gone, so the only work that survives is what you verified and wrote down for the one who comes after, whether that is tomorrow's session or the engineer who inherits the repo.
35
37
 
36
38
  ## Goal-Driven Execution maps onto the Laws
37
39
 
@@ -61,5 +63,6 @@ Before saying "Done", verify ALL:
61
63
  - [ ] I checked the **actual** result (not assumed)
62
64
  - [ ] Build passes
63
65
  - [ ] I can explain the change in one sentence
66
+ - [ ] For each item above I checked, not hoped
64
67
 
65
- If you're skipping a step, that's the step you need most.
68
+ If you're skipping a step, that's the step you need most. The step you skip is the one you are hoping through.
@@ -95,7 +95,7 @@ git branch -d <type>/<slug> # delete the merged feature branch (safe
95
95
  ## Pairs with
96
96
 
97
97
  - **`reconcile`** skill — the discipline this command runs.
98
- - **`gateguard`** / **`safety-guard`** runtime + destructive-op guardrails.
98
+ - **`gateguard`** the runtime gate for mutating tool calls and destructive shell.
99
99
  - **`recall`** — recall whether the same git op failed here before.
100
100
  - **`audit`** — the loop that often produces the fix `/reconcile` then ships.
101
101
  - **`/ship`** — the TDD-gated single-defect variant; `commit-commands:commit-push-pr` is the external-plugin equivalent of the commit → PR tail.
@@ -13,11 +13,11 @@ The 7 Laws define *what* discipline must be applied. `/superpowers` decides *whi
13
13
 
14
14
  | Source | Where it lives | Examples of what it routes to |
15
15
  |---|---|---|
16
- | `continuous-improvement` (this plugin) | bundled — always present | `gateguard` (Law 1), `tdd-workflow` (Law 3+4), `verification-loop` (Law 4), `wild-risa-balance` (Law 2), `safety-guard` (Law 3), `proceed-with-the-recommendation` (all 7), `ralph` (Law 6), `workspace-surface-audit` (Law 1) |
16
+ | `continuous-improvement` (this plugin) | bundled — always present | `gateguard` (Law 1), `tdd-workflow` (Law 3+4), `verification-loop` (Law 4), `wild-risa-balance` (Law 2), `proceed-with-the-recommendation` (all 7), `ralph` (Law 6), `workspace-surface-audit` (Law 1) |
17
17
  | `obra/superpowers` (Jesse Vincent) | vendored at `third-party/superpowers/`, pinned SHA `f2cbfbe` (v5.1.0) | `superpowers:brainstorming`, `:writing-plans`, `:executing-plans`, `:test-driven-development`, `:systematic-debugging`, `:requesting-code-review`, `:receiving-code-review`, `:verification-before-completion`, `:dispatching-parallel-agents`, `:using-git-worktrees`, `:finishing-a-development-branch`, `:subagent-driven-development`, `:writing-skills`, `:using-superpowers` |
18
18
  | `addyosmani/agent-skills` | vendored at `third-party/addy-agent-skills/`, pinned SHA `742dca5` (v1.0.0) | `spec-driven-development`, `source-driven-development`, `context-engineering`, `idea-refine`, `incremental-implementation`, `code-review-and-quality`, `code-simplification`, `security-and-hardening`, `debugging-and-error-recovery`, `performance-optimization`, `api-and-interface-design`, `frontend-ui-engineering`, `browser-testing-with-devtools`, `ci-cd-and-automation`, `deprecation-and-migration`, `documentation-and-adrs`, `git-workflow-and-versioning`, `planning-and-task-breakdown`, `shipping-and-launch` |
19
19
  | `ruflo-swarm` (ruvnet) | vendored at `third-party/ruflo-swarm/`, pinned SHA `addb5cd` (v0.2.0) | `swarm-init`, `monitor-stream`; `swarm_*` and `agent_*` MCP tools; `/swarm`, `/watch` |
20
- | `oh-my-claudecode` (Yeachan-Heo) | vendored at `third-party/oh-my-claudecode/`, pinned SHA `aacde3e` (v4.13.6) | 38 skills + 19 agents — `release`, `ultrawork`, `ultraqa`, `team`, `trace`, `visual-verdict`, `debug`, `deep-dive`, `deep-interview`, `autopilot`, `autoresearch`, plus a separate `ralph` (overlaps with our `/ralph` — see "Distinct variants" below) |
20
+ | `oh-my-claudecode` (Yeachan-Heo) | vendored at `third-party/oh-my-claudecode/`, pinned SHA `4820f56` (v5.3.0) | 37 skills + 19 agents — `release`, `ultragoal`, `launch`, `team`, `trace`, `visual-verdict`, `debug`, `review`, `research`, `deep-interview`, `autopilot`, `autoresearch`, plus a separate `ralph` (overlaps with our `/ralph` — see "Distinct variants" below) |
21
21
 
22
22
  PM coverage (product-management skills) lives **outside** this marketplace. If you need it, install [`phuryn/pm-skills`](https://github.com/phuryn/pm-skills) separately via Claude Code's host marketplace; the dispatcher names it as a routing target without pre-resolving the namespace. See [docs/THIRD_PARTY.md § Routing in /superpowers](../docs/THIRD_PARTY.md#routing-in-superpowers).
23
23
 
@@ -29,9 +29,14 @@ with the text `probe`) **without presenting any research first**.
29
29
 
30
30
  - If the hook **blocks** the write with a fact-list reason — the runtime layer is
31
31
  wired. Record `gateguard: ✓`. Do not retry the write; the block is the pass.
32
- - If the write **goes through** with no pause — the hook did not load. Record
33
- `gateguard: (hooks/gateguard.mjs not wired see README Troubleshooting install)`.
34
- Delete the probe file if it was created.
32
+ - If the write **goes through** with no pause — either the hook did not load, or
33
+ `CI_GATEGUARD_EXCLUDE` is set to a fragment that matches the probe path (a
34
+ catch-all such as `/` or `.` matches every path and switches the file gate off;
35
+ the hook prints a one-line stderr notice when an exclusion fires). Run
36
+ `echo "$CI_GATEGUARD_EXCLUDE"` first. If it is empty, record
37
+ `gateguard: ✗ (hooks/gateguard.mjs not wired — see README → Troubleshooting install)`;
38
+ if it is set, record `gateguard: ✗ (excluded by CI_GATEGUARD_EXCLUDE=<value>; unset it or
39
+ narrow the fragment)`. Delete the probe file if it was created.
35
40
 
36
41
  ## Check 3 — observation capture recording
37
42
 
@@ -30,10 +30,10 @@
30
30
  * suite or a follow-up audit walks the table against this map.
31
31
  */
32
32
  import { execFileSync } from "node:child_process";
33
- import { createHash } from "node:crypto";
34
33
  import { appendFileSync, existsSync, mkdirSync, readFileSync } from "node:fs";
35
34
  import { homedir } from "node:os";
36
35
  import { join } from "node:path";
36
+ import { hashProjectRoot } from "../lib/gateguard-state.mjs";
37
37
  const OVERRIDES = {
38
38
  "tdd-workflow": {
39
39
  companion: "superpowers:test-driven-development",
@@ -48,11 +48,7 @@ const OVERRIDES = {
48
48
  plugin: "agent-skills",
49
49
  },
50
50
  ralph: {
51
- companion: "oh-my-claudecode:ultrawork",
52
- plugin: "oh-my-claudecode",
53
- },
54
- "learn-eval": {
55
- companion: "oh-my-claudecode:retrospective",
51
+ companion: "oh-my-claudecode:ultragoal",
56
52
  plugin: "oh-my-claudecode",
57
53
  },
58
54
  };
@@ -125,10 +121,7 @@ function resolveProjectRoot() {
125
121
  return "global";
126
122
  }
127
123
  function telemetryPath(home) {
128
- const hash = createHash("sha256")
129
- .update(resolveProjectRoot())
130
- .digest("hex")
131
- .slice(0, 12);
124
+ const hash = hashProjectRoot(resolveProjectRoot());
132
125
  return join(home, ".claude", "instincts", hash, "companion-preference.jsonl");
133
126
  }
134
127
  /**