@amsterdamdatalabs/enact-extensions 0.1.12 → 0.1.24

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 (147) hide show
  1. package/README.md +11 -12
  2. package/dist/create/enact.js +1 -1
  3. package/dist/create/enact.js.map +1 -1
  4. package/dist/create/index.d.ts +4 -3
  5. package/dist/create/index.d.ts.map +1 -1
  6. package/dist/create/index.js +9 -2
  7. package/dist/create/index.js.map +1 -1
  8. package/dist/index.d.ts +8 -6
  9. package/dist/index.d.ts.map +1 -1
  10. package/dist/index.js +4 -3
  11. package/dist/index.js.map +1 -1
  12. package/dist/install.d.ts +5 -0
  13. package/dist/install.d.ts.map +1 -1
  14. package/dist/install.js +10 -3
  15. package/dist/install.js.map +1 -1
  16. package/dist/internal/agents.d.ts +6 -1
  17. package/dist/internal/agents.d.ts.map +1 -1
  18. package/dist/internal/agents.js +8 -4
  19. package/dist/internal/agents.js.map +1 -1
  20. package/dist/internal/claude.d.ts +24 -0
  21. package/dist/internal/claude.d.ts.map +1 -1
  22. package/dist/internal/claude.js +99 -0
  23. package/dist/internal/claude.js.map +1 -1
  24. package/dist/internal/platform.d.ts +3 -1
  25. package/dist/internal/platform.d.ts.map +1 -1
  26. package/dist/internal/platform.js +7 -1
  27. package/dist/internal/platform.js.map +1 -1
  28. package/dist/internal/types.d.ts +2 -1
  29. package/dist/internal/types.d.ts.map +1 -1
  30. package/dist/principles.d.ts +28 -0
  31. package/dist/principles.d.ts.map +1 -0
  32. package/dist/principles.js +159 -0
  33. package/dist/principles.js.map +1 -0
  34. package/extensions/dev-state/.agents/plugin.json +2 -1
  35. package/extensions/enact-context/.agents/plugin.json +2 -1
  36. package/extensions/enact-context/hooks/hooks.json +0 -10
  37. package/extensions/enact-context/skills/enact-context/SKILL.md +14 -12
  38. package/extensions/enact-context/skills/enact-context/scripts/install.sh +7 -7
  39. package/extensions/enact-core/.agents/plugin.json +2 -1
  40. package/extensions/enact-core/OPERATING-PRINCIPLES.md +7 -0
  41. package/extensions/enact-core/hooks/hooks.json +12 -0
  42. package/extensions/enact-evolve/.agents/plugin.json +47 -0
  43. package/extensions/enact-evolve/agents/evolve-session-analyst.toml +37 -0
  44. package/extensions/enact-evolve/skills/session-analysis/SKILL.md +98 -0
  45. package/extensions/enact-evolve/skills/session-analysis/scripts/run-evolve-analysis.sh +343 -0
  46. package/extensions/enact-factory/.agents/plugin.json +2 -2
  47. package/extensions/enact-factory/agents/architect.toml +9 -5
  48. package/extensions/enact-factory/agents/code-reviewer.toml +9 -5
  49. package/extensions/enact-factory/agents/critic.toml +9 -5
  50. package/extensions/enact-factory/agents/executor.toml +4 -1
  51. package/extensions/enact-factory/agents/explore.toml +4 -1
  52. package/extensions/enact-factory/agents/planner.toml +4 -1
  53. package/extensions/enact-factory/agents/verifier.toml +9 -5
  54. package/extensions/enact-factory/skills/advisor/SKILL.md +82 -0
  55. package/extensions/enact-factory/skills/ai-slop-cleaner/SKILL.md +6 -1
  56. package/extensions/enact-factory/skills/autonomous-runner/SKILL.md +347 -0
  57. package/extensions/enact-factory/skills/azdo-ci-strategy/SKILL.md +42 -15
  58. package/extensions/enact-factory/skills/committee/SKILL.md +80 -0
  59. package/extensions/enact-factory/skills/deep-interview/SKILL.md +9 -13
  60. package/extensions/enact-factory/skills/drive-loop/SKILL.md +161 -31
  61. package/extensions/enact-factory/skills/drive-loop/references/contract-schema.md +26 -6
  62. package/extensions/enact-factory/skills/handoff/SKILL.md +72 -0
  63. package/extensions/enact-factory/skills/hyperplan/SKILL.md +11 -3
  64. package/extensions/enact-factory/skills/looplan/SKILL.md +34 -17
  65. package/extensions/enact-factory/skills/plan/SKILL.md +40 -8
  66. package/extensions/enact-factory/skills/remove-deadcode/SKILL.md +6 -1
  67. package/extensions/enact-factory/skills/research/SKILL.md +14 -4
  68. package/extensions/enact-factory/skills/review/SKILL.md +21 -2
  69. package/extensions/enact-factory/skills/security-research/SKILL.md +5 -2
  70. package/extensions/enact-factory/skills/tdd/SKILL.md +7 -1
  71. package/extensions/enact-factory/skills/testing-strategy/SKILL.md +5 -0
  72. package/extensions/enact-factory/skills/trace/SKILL.md +5 -0
  73. package/extensions/enact-factory/skills/ultraqa/SKILL.md +21 -15
  74. package/extensions/enact-factory/skills/work-with-workitem/SKILL.md +5 -0
  75. package/extensions/enact-factory/skills/workitem-triage/SKILL.md +5 -0
  76. package/extensions/enact-loop/.agents/plugin.json +5 -4
  77. package/extensions/enact-loop/scripts/validate.mjs +123 -0
  78. package/extensions/enact-loop/skills/enact-loop/SKILL.md +189 -30
  79. package/extensions/enact-wiki/.agents/plugin.json +2 -1
  80. package/extensions/net-revenue-management/.agents/plugin.json +2 -1
  81. package/extensions/plugin-dev/.agents/plugin.json +2 -1
  82. package/extensions/plugin-dev/skills/start/SKILL.md +3 -3
  83. package/package.json +1 -1
  84. package/scripts/check-hooks.mjs +5 -5
  85. package/scripts/check-principles.mjs +19 -4
  86. package/scripts/enact-extensions.mjs +237 -90
  87. package/scripts/lib/hooks.mjs +61 -217
  88. package/scripts/lib/migrate-artifacts.mjs +144 -0
  89. package/scripts/lib/principles.mjs +109 -0
  90. package/scripts/lib/provision-mcp.mjs +1 -1
  91. package/scripts/lib/run-install.mjs +72 -2
  92. package/scripts/lib/run-prune.mjs +23 -2
  93. package/scripts/lib/run-sync.mjs +4 -1
  94. package/scripts/postinstall.mjs +6 -6
  95. package/scripts/setup-enact-context.sh +20 -15
  96. package/spec/codex.json +5 -0
  97. package/spec/enact.json +3 -3
  98. package/spec/enact.md +1 -4
  99. package/spec/index.json +1 -1
  100. package/extensions/enact-factory/hooks/hooks.json +0 -14
  101. package/extensions/enact-operator/.agents/plugin.json +0 -56
  102. package/extensions/enact-operator/.app.json +0 -3
  103. package/extensions/enact-operator/.mcp.json +0 -10
  104. package/extensions/enact-operator/_taxonomy.md +0 -86
  105. package/extensions/enact-operator/agents/README.md +0 -5
  106. package/extensions/enact-operator/agents/architect.toml +0 -25
  107. package/extensions/enact-operator/agents/code-reviewer.toml +0 -24
  108. package/extensions/enact-operator/agents/critic.toml +0 -30
  109. package/extensions/enact-operator/agents/executor.toml +0 -24
  110. package/extensions/enact-operator/agents/explore.toml +0 -23
  111. package/extensions/enact-operator/agents/planner.toml +0 -24
  112. package/extensions/enact-operator/agents/verifier.toml +0 -24
  113. package/extensions/enact-operator/docs/skill-variants.md +0 -44
  114. package/extensions/enact-operator/hooks/hooks.json +0 -91
  115. package/extensions/enact-operator/skills/ai-slop-cleaner/SKILL.md +0 -50
  116. package/extensions/enact-operator/skills/analyze/SKILL.md +0 -91
  117. package/extensions/enact-operator/skills/ask/SKILL.md +0 -47
  118. package/extensions/enact-operator/skills/autopilot/SKILL.md +0 -170
  119. package/extensions/enact-operator/skills/autoresearch-goal/SKILL.md +0 -79
  120. package/extensions/enact-operator/skills/cancel/SKILL.md +0 -99
  121. package/extensions/enact-operator/skills/configure-notifications/SKILL.md +0 -77
  122. package/extensions/enact-operator/skills/deep-interview/SKILL.md +0 -80
  123. package/extensions/enact-operator/skills/doctor/SKILL.md +0 -48
  124. package/extensions/enact-operator/skills/hud/SKILL.md +0 -49
  125. package/extensions/enact-operator/skills/hyperplan/SKILL.md +0 -47
  126. package/extensions/enact-operator/skills/plan/SKILL.md +0 -78
  127. package/extensions/enact-operator/skills/ralph/SKILL.md +0 -201
  128. package/extensions/enact-operator/skills/ralph/gemini.md +0 -18
  129. package/extensions/enact-operator/skills/ralplan/SKILL.md +0 -151
  130. package/extensions/enact-operator/skills/remove-deadcode/SKILL.md +0 -45
  131. package/extensions/enact-operator/skills/research/SKILL.md +0 -74
  132. package/extensions/enact-operator/skills/review/SKILL.md +0 -58
  133. package/extensions/enact-operator/skills/security-research/SKILL.md +0 -54
  134. package/extensions/enact-operator/skills/setup/SKILL.md +0 -91
  135. package/extensions/enact-operator/skills/setup/scripts/install.sh +0 -50
  136. package/extensions/enact-operator/skills/skill/SKILL.md +0 -82
  137. package/extensions/enact-operator/skills/tdd/SKILL.md +0 -59
  138. package/extensions/enact-operator/skills/team/SKILL.md +0 -199
  139. package/extensions/enact-operator/skills/trace/SKILL.md +0 -41
  140. package/extensions/enact-operator/skills/ultragoal/SKILL.md +0 -99
  141. package/extensions/enact-operator/skills/ultraqa/SKILL.md +0 -113
  142. package/extensions/enact-operator/skills/ultrawork/SKILL.md +0 -145
  143. package/extensions/enact-operator/skills/ultrawork/planner.md +0 -28
  144. package/extensions/enact-operator/skills/wiki/SKILL.md +0 -41
  145. package/extensions/enact-operator/skills/work-with-workitem/SKILL.md +0 -51
  146. /package/extensions/{enact-operator → enact-evolve}/assets/icon.png +0 -0
  147. /package/extensions/{enact-operator → enact-evolve}/assets/logo.png +0 -0
@@ -11,8 +11,15 @@ import { runPrune } from "./lib/run-prune.mjs";
11
11
  import { runUninstall } from "./lib/run-uninstall.mjs";
12
12
  import { readLedger } from "./lib/ledger.mjs";
13
13
  import { computeOutdated } from "./lib/outdated.mjs";
14
+ import { migrateGlobalArtifacts } from "./lib/migrate-artifacts.mjs";
14
15
  import { startServer, resolveServePort } from "./lib/serve.mjs";
15
- import { registerHook, removeHook, SUPPORTED_SURFACES } from "./lib/hooks.mjs";
16
+ import {
17
+ setupPrinciples,
18
+ removePrinciples,
19
+ checkPrinciples,
20
+ runDoctor,
21
+ SUPPORTED_PRINCIPLES_SURFACES,
22
+ } from "./lib/principles.mjs";
16
23
  import { dirname, join, resolve } from "node:path";
17
24
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
18
25
  import { fileURLToPath } from "node:url";
@@ -36,6 +43,8 @@ Usage:
36
43
  enact-extensions log --json Emit the raw ledger array as JSON
37
44
  enact-extensions outdated Report freshness drift: installed vs canonical source (per plugin+surface)
38
45
  enact-extensions outdated --json Emit JSON array [{name, platform, scope, home, status, installedHash, currentHash, version}]
46
+ enact-extensions migrate-artifacts [--dry-run] [--json]
47
+ Rename legacy factory/loop markdown artifacts to YYMMDDHH-<slug>.md
39
48
  enact-extensions update <name> Refresh an outdated plugin on every surface it is installed on (ledger-driven)
40
49
  enact-extensions update --all Refresh EVERY outdated plugin/surface
41
50
  enact-extensions update <name> --dry-run Show what would be refreshed; change nothing
@@ -68,17 +77,14 @@ Usage:
68
77
  enact-extensions uninstall [path|name] --platform all Uninstall from every plugin platform
69
78
  enact-extensions uninstall [path|name] --platform claude,cursor Uninstall from a subset of platforms
70
79
  enact-extensions serve [--port N] [--prod] [--host 127.0.0.1] [--open] Start the localhost management UI + API (dev 43217 / prod 53217)
71
- enact-extensions hooks [setup] [--surfaces <list>|--all] [--remove] [--local]
72
- Register (or remove) the session-start drift-check hook
73
- for each named surface (claude, codex, cursor, enact).
74
-
75
- Hooks command:
76
- enact-extensions hooks setup --surfaces claude,codex Register the drift hook for Claude and Codex
77
- enact-extensions hooks setup --all Register for all surfaces (claude, codex, cursor, enact)
78
- enact-extensions hooks setup --surfaces claude --remove Remove the drift hook from Claude
79
- enact-extensions hooks setup --all --remove Remove the drift hook from all surfaces
80
- enact-extensions hooks setup --surfaces claude --local Register using project-local home (under cwd)
81
- enact-extensions hooks setup Interactive surface picker (TTY only)
80
+ enact-extensions principles setup [--surfaces <list>|--all] [--remove] [--local]
81
+ Inject (or remove) the enact-core operating-principles block
82
+ into host agent files (CLAUDE.md / AGENTS.md).
83
+ enact-extensions principles check [--surfaces <list>|--all] [--local] [--json]
84
+ Report principles block drift per surface (present/missing/stale).
85
+ Use principles setup or doctor --fix to remediate. Not npm run check:principles.
86
+ enact-extensions doctor [--fix] [--surfaces <list>|--all] [--local] [--json]
87
+ Check operating-principles block drift per surface; --fix applies blocks.
82
88
  enact-extensions -v | --version Print version and exit
83
89
 
84
90
  Options:
@@ -198,6 +204,8 @@ function parseArgs(argv) {
198
204
  options.surfaces = argv[++i].split(",").map((s) => s.trim()).filter(Boolean);
199
205
  } else if (arg === "--remove") {
200
206
  options.remove = true;
207
+ } else if (arg === "--fix") {
208
+ options.fix = true;
201
209
  } else if (arg === "-h" || arg === "--help") {
202
210
  positional.push("help");
203
211
  } else if (!arg.startsWith("-")) {
@@ -227,83 +235,81 @@ if (!command || command === "help") {
227
235
  // Surfaces: claude, codex, cursor, enact
228
236
  // Registration is idempotent and reversible.
229
237
  // ---------------------------------------------------------------------------
230
- if (command === "hooks") {
231
- // Sub-command: `hooks setup` or just `hooks` (treat bare `hooks` as `hooks setup`).
232
- // path is positional[1] — could be "setup" or undefined.
238
+ // ---------------------------------------------------------------------------
239
+ // `principles` command — inject or check operating-principles blocks in host files.
240
+ //
241
+ // Surfaces: claude, codex, cursor, enact
242
+ // ---------------------------------------------------------------------------
243
+ if (command === "principles") {
233
244
  const subCommand = path ?? "setup";
234
245
 
235
- if (subCommand !== "setup") {
236
- process.stderr.write(`[enact-extensions hooks] Unknown sub-command: ${subCommand}. Did you mean 'setup'?\n`);
246
+ if (subCommand !== "setup" && subCommand !== "check") {
247
+ process.stderr.write(
248
+ `[enact-extensions principles] Unknown sub-command: ${subCommand}. Use 'setup' or 'check'.\n`,
249
+ );
237
250
  process.exit(1);
238
251
  }
239
252
 
240
253
  const isRemove = options.remove ?? false;
241
254
  const isLocal = options.scope === "local";
242
255
  const cwd = processCwd();
256
+ const surfacesList = SUPPORTED_PRINCIPLES_SURFACES;
243
257
 
244
- // Determine which surfaces to act on.
245
258
  let surfaces = [];
246
259
  if (options.all) {
247
- surfaces = [...SUPPORTED_SURFACES];
260
+ surfaces = [...surfacesList];
248
261
  } else if (options.surfaces && options.surfaces.length > 0) {
249
- // Validate provided surface names.
250
- const invalid = options.surfaces.filter((s) => !SUPPORTED_SURFACES.includes(s));
262
+ const invalid = options.surfaces.filter((s) => !surfacesList.includes(s));
251
263
  if (invalid.length > 0) {
252
- process.stderr.write(`[enact-extensions hooks] Unknown surface(s): ${invalid.join(", ")}. Valid: ${SUPPORTED_SURFACES.join(", ")}\n`);
264
+ process.stderr.write(
265
+ `[enact-extensions principles] Unknown surface(s): ${invalid.join(", ")}. Valid: ${surfacesList.join(", ")}\n`,
266
+ );
253
267
  process.exit(1);
254
268
  }
255
269
  surfaces = options.surfaces;
256
- } else {
257
- // No surfaces specified — check if TTY for interactive picker.
258
- if (process.stdin.isTTY) {
259
- // Interactive picker via node:readline.
260
- const { createInterface } = await import("node:readline");
261
- const rl = createInterface({ input: process.stdin, output: process.stdout });
270
+ } else if (subCommand === "setup" && process.stdin.isTTY) {
271
+ const { createInterface } = await import("node:readline");
272
+ const rl = createInterface({ input: process.stdin, output: process.stdout });
273
+ const question = (prompt) => new Promise((resolve) => rl.question(prompt, resolve));
262
274
 
263
- const question = (prompt) => new Promise((resolve) => rl.question(prompt, resolve));
275
+ process.stdout.write(`\nenact-extensions principles setup — interactive surface picker\n`);
276
+ process.stdout.write(`Available surfaces: ${surfacesList.map((s, i) => `${i + 1}. ${s}`).join(", ")}\n`);
277
+ process.stdout.write(`Enter surface names or numbers (comma-separated), or 'all':\n`);
264
278
 
265
- process.stdout.write(`\nenact-extensions hooks setup — interactive surface picker\n`);
266
- process.stdout.write(`Available surfaces: ${SUPPORTED_SURFACES.map((s, i) => `${i + 1}. ${s}`).join(", ")}\n`);
267
- process.stdout.write(`Enter surface names or numbers (comma-separated), or 'all':\n`);
279
+ const answer = await question("> ");
280
+ rl.close();
268
281
 
269
- const answer = await question("> ");
270
- rl.close();
282
+ if (answer.trim().toLowerCase() === "all") {
283
+ surfaces = [...surfacesList];
284
+ } else {
285
+ surfaces = answer
286
+ .split(",")
287
+ .map((s) => s.trim())
288
+ .filter(Boolean)
289
+ .map((s) => {
290
+ const idx = parseInt(s, 10);
291
+ if (!isNaN(idx) && idx >= 1 && idx <= surfacesList.length) {
292
+ return surfacesList[idx - 1];
293
+ }
294
+ return s.toLowerCase();
295
+ })
296
+ .filter((s) => surfacesList.includes(s));
271
297
 
272
- if (answer.trim().toLowerCase() === "all") {
273
- surfaces = [...SUPPORTED_SURFACES];
274
- } else {
275
- surfaces = answer
276
- .split(",")
277
- .map((s) => s.trim())
278
- .filter(Boolean)
279
- .map((s) => {
280
- // Allow numeric selection.
281
- const idx = parseInt(s, 10);
282
- if (!isNaN(idx) && idx >= 1 && idx <= SUPPORTED_SURFACES.length) {
283
- return SUPPORTED_SURFACES[idx - 1];
284
- }
285
- return s.toLowerCase();
286
- })
287
- .filter((s) => SUPPORTED_SURFACES.includes(s));
288
-
289
- if (surfaces.length === 0) {
290
- process.stderr.write(`[enact-extensions hooks] No valid surfaces selected. Exiting.\n`);
291
- process.exit(0);
292
- }
298
+ if (surfaces.length === 0) {
299
+ process.stderr.write(`[enact-extensions principles] No valid surfaces selected. Exiting.\n`);
300
+ process.exit(0);
293
301
  }
294
- } else {
295
- // Non-TTY, no surface flags → print notice and exit 0.
296
- process.stdout.write(
297
- `[enact-extensions hooks] No surface specified. Run with --surfaces or --all:\n` +
298
- ` enact-extensions hooks setup --surfaces claude,codex\n` +
299
- ` enact-extensions hooks setup --all\n`,
300
- );
301
- process.exit(0);
302
302
  }
303
+ } else {
304
+ process.stdout.write(
305
+ `[enact-extensions principles] No surface specified. Run with --surfaces or --all:\n` +
306
+ ` enact-extensions principles ${subCommand} --surfaces claude,codex\n` +
307
+ ` enact-extensions principles ${subCommand} --all\n`,
308
+ );
309
+ process.exit(0);
303
310
  }
304
311
 
305
- // Execute registration or removal for each surface.
306
- const hookOpts = {
312
+ const principleOpts = {
307
313
  claudeHome: options.claudeHome,
308
314
  codexHome: options.codexHome,
309
315
  cursorHome: options.cursorHome,
@@ -312,37 +318,126 @@ if (command === "hooks") {
312
318
  cwd,
313
319
  };
314
320
 
321
+ let exitCode = 0;
322
+
323
+ if (subCommand === "check") {
324
+ const rows = [];
325
+ for (const surface of surfaces) {
326
+ try {
327
+ const res = checkPrinciples(surface, principleOpts);
328
+ rows.push(res);
329
+ if (!options.json) {
330
+ process.stdout.write(
331
+ ` [principles] ${surface}: ${res.status} → ${res.path}\n`,
332
+ );
333
+ }
334
+ } catch (err) {
335
+ const message = err instanceof Error ? err.message : String(err);
336
+ rows.push({ surface, path: "", status: "error", error: message });
337
+ process.stderr.write(` [principles] ${surface}: error — ${message}\n`);
338
+ }
339
+ }
340
+
341
+ if (options.json) {
342
+ console.log(JSON.stringify(rows, null, 2));
343
+ }
344
+
345
+ process.exit(0);
346
+ }
347
+
315
348
  for (const surface of surfaces) {
316
- let res;
317
- if (isRemove) {
318
- res = removeHook(surface, hookOpts);
319
- } else {
320
- res = registerHook(surface, hookOpts);
349
+ try {
350
+ const res = isRemove
351
+ ? removePrinciples(surface, principleOpts)
352
+ : setupPrinciples(surface, principleOpts);
353
+
354
+ if (res.result === "applied") {
355
+ process.stdout.write(
356
+ ` [principles] ${surface}: applied (${res.action}) → ${res.path}\n`,
357
+ );
358
+ } else if (res.result === "already_present") {
359
+ process.stdout.write(
360
+ ` [principles] ${surface}: already present (noop) → ${res.path}\n`,
361
+ );
362
+ } else if (res.result === "removed") {
363
+ process.stdout.write(` [principles] ${surface}: removed → ${res.path}\n`);
364
+ } else if (res.result === "not_found") {
365
+ process.stdout.write(
366
+ ` [principles] ${surface}: not found (nothing to remove) → ${res.path}\n`,
367
+ );
368
+ }
369
+ } catch (err) {
370
+ exitCode = 1;
371
+ const message = err instanceof Error ? err.message : String(err);
372
+ process.stderr.write(` [principles] ${surface}: error — ${message}\n`);
321
373
  }
374
+ }
322
375
 
323
- // Print per-surface result.
324
- const status = res.result;
325
- const location = res.location ? ` → ${res.location}` : "";
326
- const note = res.note ? ` (${res.note})` : "";
327
-
328
- if (status === "registered") {
329
- process.stdout.write(` [hooks] ${surface}: registered${location}\n`);
330
- } else if (status === "already_registered") {
331
- process.stdout.write(` [hooks] ${surface}: already registered (skipped)${location}\n`);
332
- } else if (status === "removed") {
333
- process.stdout.write(` [hooks] ${surface}: removed${location}\n`);
334
- } else if (status === "not_found") {
335
- process.stdout.write(` [hooks] ${surface}: not found (nothing to remove)${location}\n`);
336
- } else {
337
- process.stdout.write(` [hooks] ${surface}: skipped${note}\n`);
376
+ process.exit(exitCode);
377
+ }
378
+
379
+ // ---------------------------------------------------------------------------
380
+ // `doctor` command — check (and optionally fix) operating-principles drift.
381
+ //
382
+ // Surfaces: claude, codex, cursor, enact (defaults to all when omitted)
383
+ // ---------------------------------------------------------------------------
384
+ if (command === "doctor") {
385
+ const isFix = options.fix ?? false;
386
+ const isLocal = options.scope === "local";
387
+ const cwd = processCwd();
388
+ const surfacesList = SUPPORTED_PRINCIPLES_SURFACES;
389
+
390
+ let surfaces = [];
391
+ if (options.all || !options.surfaces || options.surfaces.length === 0) {
392
+ surfaces = [...surfacesList];
393
+ } else {
394
+ const invalid = options.surfaces.filter((s) => !surfacesList.includes(s));
395
+ if (invalid.length > 0) {
396
+ process.stderr.write(
397
+ `[enact-extensions doctor] Unknown surface(s): ${invalid.join(", ")}. Valid: ${surfacesList.join(", ")}\n`,
398
+ );
399
+ process.exit(1);
338
400
  }
401
+ surfaces = options.surfaces;
402
+ }
403
+
404
+ const principleOpts = {
405
+ claudeHome: options.claudeHome,
406
+ codexHome: options.codexHome,
407
+ cursorHome: options.cursorHome,
408
+ enactHome: options.enactHome,
409
+ local: isLocal,
410
+ cwd,
411
+ fix: isFix,
412
+ };
413
+
414
+ const { rows, exitCode } = runDoctor(surfaces, principleOpts);
339
415
 
340
- if (res.note && status !== "skipped") {
341
- process.stderr.write(` [hooks] ${surface} note: ${res.note}\n`);
416
+ if (!options.json) {
417
+ for (const row of rows) {
418
+ if (row.error) {
419
+ process.stderr.write(` [doctor] ${row.surface}: error — ${row.error}\n`);
420
+ continue;
421
+ }
422
+ if (isFix) {
423
+ const note = row.result === "already_present"
424
+ ? "already present (noop)"
425
+ : `fixed (${row.action ?? row.result})`;
426
+ process.stdout.write(` [doctor] ${row.surface}: ${row.status} — ${note} → ${row.path}\n`);
427
+ } else {
428
+ process.stdout.write(` [doctor] ${row.surface}: ${row.status} → ${row.path}\n`);
429
+ }
342
430
  }
431
+ if (!isFix && exitCode !== 0) {
432
+ process.stderr.write(
433
+ `[enact-extensions doctor] Drift detected. Fix with: enact-extensions doctor --fix --all\n`,
434
+ );
435
+ }
436
+ } else {
437
+ console.log(JSON.stringify(rows, null, 2));
343
438
  }
344
439
 
345
- process.exit(0);
440
+ process.exit(exitCode);
346
441
  }
347
442
 
348
443
  // ---------------------------------------------------------------------------
@@ -615,6 +710,39 @@ if (command === "outdated") {
615
710
  }
616
711
 
617
712
  // ---------------------------------------------------------------------------
713
+ // `migrate-artifacts` command — rename legacy global markdown artifacts.
714
+ if (command === "migrate-artifacts") {
715
+ try {
716
+ const report = migrateGlobalArtifacts({ dryRun: Boolean(options.dryRun) });
717
+ if (options.json) {
718
+ process.stdout.write(`${JSON.stringify(report, null, 2)}\n`);
719
+ } else {
720
+ const factoryRenamed = report.factory.flatMap((entry) => entry.renamed);
721
+ const loopRenamed = report.loop.flatMap((entry) => entry.renamed);
722
+ if (factoryRenamed.length === 0 && loopRenamed.length === 0) {
723
+ process.stdout.write("✓ no legacy markdown artifacts to migrate\n");
724
+ }
725
+ for (const { from, to } of factoryRenamed) {
726
+ process.stdout.write(`✓ factory ${from} → ${to}\n`);
727
+ }
728
+ for (const { from, to } of loopRenamed) {
729
+ process.stdout.write(`✓ loop ${from} → ${to}\n`);
730
+ }
731
+ if (report.fallbacks.length > 0) {
732
+ process.stderr.write(
733
+ `· ${report.fallbacks.length} ISO-date rename(s) used hour fallback — see --json for details\n`,
734
+ );
735
+ }
736
+ }
737
+ process.exit(0);
738
+ } catch (err) {
739
+ process.stderr.write(
740
+ `[enact-extensions migrate-artifacts] Error: ${err instanceof Error ? err.message : String(err)}\n`,
741
+ );
742
+ process.exit(1);
743
+ }
744
+ }
745
+
618
746
  // `update` command — refresh outdated installed plugins, driven by the ledger.
619
747
  //
620
748
  // Re-installs each OUTDATED (plugin, surface, home) to the EXACT surface + home
@@ -803,6 +931,9 @@ if (command === "serve") {
803
931
  // `hook` command — execute a hook handler by name, reading JSON from stdin.
804
932
  //
805
933
  // Currently supported sub-commands:
934
+ // enact-extensions hook session-start
935
+ // Runs the advisory drift-check and prints a single line only when plugin
936
+ // surfaces are outdated.
806
937
  // enact-extensions hook user-prompt-submit
807
938
  // Reads a UserPromptSubmit hook payload from stdin, extracts the prompt,
808
939
  // builds the skill catalog from the bundled extensions dir, routes the
@@ -816,10 +947,10 @@ if (command === "serve") {
816
947
  if (command === "hook") {
817
948
  const subCommand = path; // positional[1]
818
949
 
819
- if (subCommand !== "user-prompt-submit") {
950
+ if (subCommand !== "user-prompt-submit" && subCommand !== "session-start") {
820
951
  process.stderr.write(
821
952
  `[enact-extensions hook] Unknown sub-command: ${subCommand ?? "(none)"}. ` +
822
- `Supported: user-prompt-submit\n`,
953
+ `Supported: session-start, user-prompt-submit\n`,
823
954
  );
824
955
  process.stdout.write(JSON.stringify({ continue: true }) + "\n");
825
956
  process.exit(0);
@@ -827,6 +958,22 @@ if (command === "hook") {
827
958
 
828
959
  // Async IIFE: stdin reading and dynamic import require async context.
829
960
  (async () => {
961
+ if (subCommand === "session-start") {
962
+ try {
963
+ const { computeOutdated } = await import("./lib/outdated.mjs");
964
+ const outdated = (computeOutdated() ?? []).filter((entry) => entry?.status === "outdated");
965
+ if (outdated.length > 0) {
966
+ const pairs = outdated.map((entry) => `${entry.name}/${entry.platform}`).join(", ");
967
+ process.stdout.write(
968
+ `[enact-extensions] ${outdated.length} plugin surface${outdated.length === 1 ? "" : "s"} outdated: ${pairs}. Run: enact-extensions update --all\n`,
969
+ );
970
+ }
971
+ } catch {
972
+ // Fail-silent for session-start hooks.
973
+ }
974
+ process.exit(0);
975
+ }
976
+
830
977
  // Read stdin to get the hook payload JSON.
831
978
  let payload;
832
979
  try {