claude-code-session-manager 0.85.0 → 0.87.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 (147) hide show
  1. package/dist/assets/AgentLibrary-DyLWzZDf.js +3 -0
  2. package/dist/assets/{DataModel-DRH-Ty20.js → DataModel--mISIJ6h.js} +1 -1
  3. package/dist/assets/{History-CfRhT1Im.js → History-C2ahUXTg.js} +2 -2
  4. package/dist/assets/{Hooks-wEmh_U6c.js → Hooks-BiC6oyR2.js} +3 -3
  5. package/dist/assets/{HostBilko-D_t7Rbi7.js → HostBilko-BPleEOld.js} +1 -1
  6. package/dist/assets/{Library-CpArQ-OJ.js → Library-Dc8Qst1R.js} +1 -1
  7. package/dist/assets/{ListDetail-pjaKYs84.js → ListDetail-DIXh-OLX.js} +1 -1
  8. package/dist/assets/MarkdownEditor-C90bkLXK.js +1 -0
  9. package/dist/assets/{McpServers-ftqaV3kn.js → McpServers-DqcbLOLZ.js} +2 -2
  10. package/dist/assets/{Memory-ChMWkNd0.js → Memory-CW62MXlh.js} +4 -4
  11. package/dist/assets/{Panel-D9Kr40Ai.js → Panel-Bw1FhRuF.js} +1 -1
  12. package/dist/assets/Permissions-BcUC-5y8.js +3 -0
  13. package/dist/assets/{Plugins-BtChISho.js → Plugins-BnKx9flD.js} +2 -2
  14. package/dist/assets/{ProvenanceBadge-DBA5EcYy.js → ProvenanceBadge-Bw5vNVPT.js} +1 -1
  15. package/dist/assets/{SaveBar-I0_dWNTX.js → SaveBar-CWr0O_w-.js} +1 -1
  16. package/dist/assets/Scheduler-DYdLuUqq.js +14 -0
  17. package/dist/assets/{ScopeSwitcher-5GTEveb2.js → ScopeSwitcher-CrBLbg8s.js} +1 -1
  18. package/dist/assets/Settings-DluB-vN1.js +3 -0
  19. package/dist/assets/{SkillReferenceGraph-DNBFGrYE.js → SkillReferenceGraph-CHLSseay.js} +1 -1
  20. package/dist/assets/{Skills-DJB6-bBM.js → Skills-gNdo_HNK.js} +2 -2
  21. package/dist/assets/{SystemPrompt-BiDDrJUA.js → SystemPrompt-Cru05-Ia.js} +1 -1
  22. package/dist/assets/{TagLibrary-_Wrevtop.js → TagLibrary-DNHY0xou.js} +1 -1
  23. package/dist/assets/{TiptapBody-OWWXdLRy.js → TiptapBody-I4lmbCgP.js} +1 -1
  24. package/dist/assets/{Toggle-B122N0HL.js → Toggle-bWMHjmRh.js} +1 -1
  25. package/dist/assets/{index-CDo9xBR9.css → index-DV3PorRY.css} +1 -1
  26. package/dist/assets/{index-DhvuQL4C.js → index-fc_JjdxL.js} +724 -724
  27. package/dist/assets/settingsSchema-BfhtZnGD.js +3 -0
  28. package/dist/index.html +2 -2
  29. package/package.json +15 -14
  30. package/plugins/CLAUDE.md +61 -0
  31. package/plugins/session-manager-dev/.claude-plugin/plugin.json +1 -1
  32. package/plugins/session-manager-dev/skills/builder/4-manual/SKILL.md +1 -1
  33. package/plugins/session-manager-dev/skills/ops-sweep/SKILL.md +1 -1
  34. package/scripts/scheduler-mcp-server.cjs +7 -0
  35. package/src/main/__tests__/agentModelResolve.test.cjs +100 -9
  36. package/src/main/__tests__/broadcastCoalescer.test.cjs +18 -0
  37. package/src/main/__tests__/epicMint.test.cjs +2 -2
  38. package/src/main/__tests__/health-delegation-chain.test.cjs +2 -1
  39. package/src/main/__tests__/health-starve-escalation.test.cjs +94 -0
  40. package/src/main/__tests__/loadGateDetailTick.test.cjs +31 -0
  41. package/src/main/__tests__/machineProfile.test.cjs +19 -1
  42. package/src/main/__tests__/needsReviewLedger.test.cjs +162 -0
  43. package/src/main/__tests__/opsErrorLogTelemetryTap.test.cjs +3 -3
  44. package/src/main/__tests__/pollLoop-dispatch-on-failure.test.cjs +15 -1
  45. package/src/main/__tests__/prdCreateDisposition.test.cjs +201 -0
  46. package/src/main/__tests__/prdFrontmatterDisposition.test.cjs +125 -0
  47. package/src/main/__tests__/prdLocations.test.cjs +100 -2
  48. package/src/main/__tests__/prdLocationsArchived.test.cjs +43 -1
  49. package/src/main/__tests__/prdSetDisposition.test.cjs +222 -0
  50. package/src/main/__tests__/pty-session-open-telemetry.test.cjs +96 -0
  51. package/src/main/__tests__/queue-health-verdict.test.cjs +170 -0
  52. package/src/main/__tests__/queue-starvation-per-project.test.cjs +147 -0
  53. package/src/main/__tests__/queueHistory.test.cjs +63 -0
  54. package/src/main/__tests__/reconcileTiming.test.cjs +135 -0
  55. package/src/main/__tests__/scheduleJobTransitions.test.cjs +101 -1
  56. package/src/main/__tests__/scheduler-boot-orphans.test.cjs +2 -2
  57. package/src/main/__tests__/scheduler-broadcast-reconcile.test.cjs +121 -0
  58. package/src/main/__tests__/scheduler-cross-project-batch.test.cjs +43 -0
  59. package/src/main/__tests__/scheduler-failed-autoreset.test.cjs +121 -0
  60. package/src/main/__tests__/scheduler-guard-verdict-autoresolve.test.cjs +344 -0
  61. package/src/main/__tests__/scheduler-job-budget.test.cjs +172 -0
  62. package/src/main/__tests__/scheduler-looks-done.test.cjs +93 -3
  63. package/src/main/__tests__/scheduler-needs-review-autoresolve.test.cjs +189 -0
  64. package/src/main/__tests__/scheduler-no-dead-end-status.test.cjs +152 -0
  65. package/src/main/__tests__/scheduler-porcelain-rename.test.cjs +164 -0
  66. package/src/main/__tests__/scheduler-quarantine-autoresolve.test.cjs +165 -0
  67. package/src/main/__tests__/scheduler-reap-dead-running-jobs.test.cjs +160 -0
  68. package/src/main/__tests__/scheduler-reaper-helpers-basics.test.cjs +87 -0
  69. package/src/main/__tests__/scheduler-shared-tree-guard.test.cjs +88 -0
  70. package/src/main/__tests__/scheduler-starve-escalation.test.cjs +154 -0
  71. package/src/main/__tests__/scheduler-worktree-cap-defer.test.cjs +14 -0
  72. package/src/main/__tests__/telemetryClient.test.cjs +75 -5
  73. package/src/main/__tests__/telemetrySettings.test.cjs +32 -0
  74. package/src/main/chatRunner.cjs +8 -5
  75. package/src/main/health.cjs +76 -3
  76. package/src/main/historyAggregator.cjs +5 -0
  77. package/src/main/index.cjs +95 -44
  78. package/src/main/ipcSchemas.cjs +47 -0
  79. package/src/main/lib/__tests__/active-sessions.test.cjs +251 -0
  80. package/src/main/lib/__tests__/bootSelfHeal.test.cjs +107 -0
  81. package/src/main/lib/__tests__/delegationReadiness.test.cjs +322 -43
  82. package/src/main/lib/__tests__/effectiveModelInfo.test.cjs +239 -0
  83. package/src/main/lib/__tests__/gitWorktree.test.cjs +89 -0
  84. package/src/main/lib/__tests__/guardShims.test.cjs +151 -0
  85. package/src/main/lib/__tests__/loadGate.test.cjs +103 -2
  86. package/src/main/lib/__tests__/opsRootAbsoluteCwd.test.cjs +5 -5
  87. package/src/main/lib/__tests__/prdDisposition.test.cjs +224 -0
  88. package/src/main/lib/__tests__/reaperHelpers.test.cjs +179 -1
  89. package/src/main/lib/__tests__/telemetryBoot.test.cjs +11 -0
  90. package/src/main/lib/__tests__/usageCircuit.test.cjs +224 -0
  91. package/src/main/lib/__tests__/watchdog-helpers.test.cjs +312 -0
  92. package/src/main/lib/__tests__/watchdog-relaunch.test.cjs +193 -0
  93. package/{scripts → src/main}/lib/activeSessions.cjs +50 -4
  94. package/src/main/lib/agentModelResolve.cjs +65 -27
  95. package/src/main/lib/bootSelfHeal.cjs +88 -0
  96. package/src/main/lib/delegationReadiness.cjs +290 -225
  97. package/src/main/lib/effectiveModelInfo.cjs +333 -0
  98. package/src/main/lib/ephemeralCwd.cjs +1 -1
  99. package/src/main/lib/epicMint.cjs +3 -3
  100. package/src/main/lib/gitWorktree.cjs +42 -12
  101. package/src/main/lib/guardShims.cjs +156 -0
  102. package/src/main/lib/jobDirtFilter.cjs +7 -2
  103. package/src/main/lib/launchFailure.cjs +2 -1
  104. package/src/main/lib/loadGate.cjs +23 -1
  105. package/src/main/lib/machineProfile.cjs +15 -0
  106. package/src/main/lib/mcpToolCatalog.cjs +4 -1
  107. package/src/main/lib/needsReviewLedger.cjs +205 -0
  108. package/src/main/lib/opsErrorLog.cjs +1 -1
  109. package/src/main/lib/opsOwnership.cjs +1 -1
  110. package/src/main/lib/prdCreate.cjs +56 -1
  111. package/src/main/lib/prdDisposition.cjs +199 -0
  112. package/src/main/lib/prdFrontmatter.cjs +8 -2
  113. package/src/main/lib/prdLocations.cjs +167 -45
  114. package/src/main/lib/projectHomeAdminRoutes.cjs +4 -4
  115. package/src/main/lib/projectPageSummarySchema.cjs +1 -1
  116. package/src/main/lib/projectRootResolve.cjs +1 -1
  117. package/src/main/lib/queueHistory.cjs +19 -1
  118. package/src/main/lib/queueStore.cjs +6 -1
  119. package/src/main/lib/reaperHelpers.cjs +181 -15
  120. package/src/main/lib/scheduleJobSchema.cjs +8 -0
  121. package/src/main/lib/scheduleJobTransitions.cjs +33 -0
  122. package/src/main/lib/schedulerBatch.cjs +12 -1
  123. package/src/main/lib/schedulerConfig.cjs +37 -0
  124. package/src/main/lib/telemetryBoot.cjs +11 -8
  125. package/src/main/lib/telemetryClient.cjs +44 -2
  126. package/src/main/lib/telemetrySettings.cjs +20 -3
  127. package/src/main/lib/usageCircuit.cjs +159 -0
  128. package/{scripts → src/main}/lib/watchdogHelpers.cjs +1 -1
  129. package/src/main/pty.cjs +9 -0
  130. package/src/main/scheduler/prdParser.cjs +13 -0
  131. package/src/main/scheduler.cjs +1869 -193
  132. package/src/main/templates/PRD_AUTHORING.md +50 -0
  133. package/src/main/templates/project-pages-catalog.json +1 -1
  134. package/src/main/usage.cjs +21 -3
  135. package/src/preload/api.d.ts +92 -1
  136. package/src/preload/index.cjs +10 -0
  137. package/web/README.md +41 -0
  138. package/{scripts/render-project-pages.cjs → web/project-pages/render.cjs} +4 -4
  139. package/{scripts/render-project-pages → web/project-pages/renderer}/dist/renderer.cjs +1 -1
  140. package/{scripts/validate-project-pages-summary.cjs → web/project-pages/validate-summary.cjs} +5 -5
  141. package/dist/assets/AgentLibrary-Bkv-HcP1.js +0 -3
  142. package/dist/assets/MarkdownEditor-Xc141kjj.js +0 -1
  143. package/dist/assets/Permissions-DKoNVgzj.js +0 -3
  144. package/dist/assets/Scheduler-CbES7MC8.js +0 -14
  145. package/dist/assets/Settings-BX3FElXk.js +0 -3
  146. package/dist/assets/settingsSchema-sGoCTd7J.js +0 -3
  147. /package/{scripts/project-pages-logic → web/project-pages/logic}/dist/logic.cjs +0 -0
@@ -22,6 +22,8 @@ const os = require('node:os');
22
22
  const { spawn } = require('node:child_process');
23
23
  const { writeJson, addAllowedRoot } = require('../config.cjs');
24
24
  const { cleanChildEnv, pathWithUserBins } = require('./cleanEnv.cjs');
25
+ const { shimPath: guardShimPath, resolveShimTarget, ensureGuardShimsOrError } = require('./guardShims.cjs');
26
+ const { resolveProjectRoot } = require('./opsOwnership.cjs');
25
27
 
26
28
  const SCHEDULER_MCP_NAME = 'session-manager-scheduler';
27
29
  const DEV_PLUGIN_ENABLED_KEY = 'session-manager-dev@session-manager';
@@ -32,6 +34,8 @@ const DESTRUCTIVE_GIT_GUARD_SCRIPT = path.resolve(__dirname, '..', '..', '..', '
32
34
  const DESTRUCTIVE_GIT_GUARD_MATCHER = 'Bash';
33
35
  const INLINE_IMPLEMENTATION_GUARD_SCRIPT = path.resolve(__dirname, '..', '..', '..', 'scripts', 'hooks', 'guard-inline-implementation.cjs');
34
36
  const INLINE_IMPLEMENTATION_GUARD_MATCHER = 'Write|Edit|NotebookEdit';
37
+ const SELF_SCHEDULE_GUARD_SCRIPT = path.resolve(__dirname, '..', '..', '..', 'scripts', 'hooks', 'guard-self-schedule.cjs');
38
+ const SELF_SCHEDULE_GUARD_MATCHER = 'ScheduleWakeup|CronCreate|Task|Agent';
35
39
  const LIVE_PROBE_TIMEOUT_MS = 10_000;
36
40
  const LIVE_PROBE_TTL_MS = 60_000;
37
41
  const REQUIRED_LIVE_TOOLS = ['scheduler_create_prd', 'session_manager_help'];
@@ -358,63 +362,131 @@ function extractGuardScriptPath(command, scriptBasename) {
358
362
  return match[1] || match[2] || match[3] || null;
359
363
  }
360
364
 
365
+ /**
366
+ * Per-guard config for the three sanctioned PreToolUse guards. `checkGuard`/
367
+ * `installGuard` below are the ONE implementation of the "resolve the hook
368
+ * command's script, validate a stable shim's pointer, merge/repair into
369
+ * .claude/settings.json" logic that used to be copy-pasted three times (one
370
+ * per guard) — a change to that logic (e.g. a new decay case) used to need
371
+ * three edits kept in lockstep by hand. `checkPrdWriteGuard`/`installPrdWriteGuard`/
372
+ * etc. below are thin, name-preserving wrappers so every existing caller/test/
373
+ * export keeps working unchanged.
374
+ */
375
+ const GUARD_DEFS = {
376
+ prdWrite: {
377
+ id: 'prd-write-guard',
378
+ label: 'PRD-write guard hook installed',
379
+ guardName: 'guard-prd-writes',
380
+ scriptBasename: 'guard-prd-writes.cjs',
381
+ script: PRD_WRITE_GUARD_SCRIPT,
382
+ matcher: PRD_WRITE_GUARD_MATCHER,
383
+ fixActionId: 'install-prd-write-guard',
384
+ nudge: false,
385
+ },
386
+ destructiveGit: {
387
+ id: 'destructive-git-guard',
388
+ label: 'Destructive-git guard hook installed',
389
+ guardName: 'guard-destructive-git',
390
+ scriptBasename: 'guard-destructive-git.cjs',
391
+ script: DESTRUCTIVE_GIT_GUARD_SCRIPT,
392
+ matcher: DESTRUCTIVE_GIT_GUARD_MATCHER,
393
+ fixActionId: 'install-destructive-git-guard',
394
+ nudge: false,
395
+ },
396
+ inlineImplementation: {
397
+ id: 'inline-implementation-guard',
398
+ label: 'Inline-implementation guard hook installed',
399
+ guardName: 'guard-inline-implementation',
400
+ scriptBasename: 'guard-inline-implementation.cjs',
401
+ script: INLINE_IMPLEMENTATION_GUARD_SCRIPT,
402
+ matcher: INLINE_IMPLEMENTATION_GUARD_MATCHER,
403
+ fixActionId: 'install-inline-implementation-guard',
404
+ // Deliberately a NUDGE that fails OPEN twice over (see
405
+ // guard-inline-implementation.cjs's header), not an ownership law like
406
+ // guard-prd-writes — label/detail/fix text must not imply a hard gate.
407
+ nudge: true,
408
+ },
409
+ selfSchedule: {
410
+ id: 'self-schedule-guard',
411
+ label: 'Self-schedule guard hook installed',
412
+ guardName: 'guard-self-schedule',
413
+ scriptBasename: 'guard-self-schedule.cjs',
414
+ script: SELF_SCHEDULE_GUARD_SCRIPT,
415
+ matcher: SELF_SCHEDULE_GUARD_MATCHER,
416
+ fixActionId: 'install-self-schedule-guard',
417
+ nudge: false,
418
+ },
419
+ };
420
+
361
421
  /**
362
422
  * `ok` must mean "a hook that will actually run", not "the settings file
363
- * mentions guard-prd-writes somewhere".
423
+ * mentions this guard somewhere".
364
424
  *
365
425
  * The failure mode this guards against: a PreToolUse command that exits
366
426
  * non-zero WITHOUT exit code 2 is a non-blocking error, so a hook pointing at
367
427
  * a script that doesn't exist in this project silently guards nothing — while
368
428
  * a substring match would flip this check green. That is strictly worse than
369
429
  * an honest red. So we resolve the command's script path (relative resolves
370
- * against the project cwd, which is what the harness runs the hook with) and
371
- * require the file to exist on disk.
430
+ * against the project cwd, which is what the harness runs the hook with),
431
+ * require the file to exist on disk, AND — when that file is itself a stable
432
+ * shim (guardShims.cjs), require its pointer to resolve to a real script too.
433
+ * Without that second step, a shim whose `app-root.json` has decayed (moved
434
+ * repo, hand-edited, deleted) would report green forever: the shim FILE never
435
+ * moves, only its pointer does — this is the same "silently guards nothing
436
+ * while reporting green" failure mode, one hop downstream of the hook command.
372
437
  */
373
- function checkPrdWriteGuard({ cwd }) {
438
+ function checkGuard(def, { cwd, homeDir = os.homedir() }) {
374
439
  const settings = readJsonSafe(path.join(cwd, '.claude', 'settings.json'), null);
375
440
  const preToolUse = Array.isArray(settings?.hooks?.PreToolUse) ? settings.hooks.PreToolUse : [];
376
441
 
377
442
  let mentioned = false;
378
443
  let resolvedScript = null;
444
+ let decayError = null;
379
445
  for (const matcher of preToolUse) {
380
446
  if (!Array.isArray(matcher?.hooks)) continue;
381
447
  for (const h of matcher.hooks) {
382
- if (typeof h?.command !== 'string' || !h.command.includes('guard-prd-writes')) continue;
448
+ if (typeof h?.command !== 'string' || !h.command.includes(def.guardName)) continue;
383
449
  mentioned = true;
384
- const raw = extractGuardScriptPath(h.command, 'guard-prd-writes.cjs');
450
+ const raw = extractGuardScriptPath(h.command, def.scriptBasename);
385
451
  if (!raw) continue;
386
452
  const abs = path.isAbsolute(raw) ? raw : path.resolve(cwd, raw);
387
- if (fs.existsSync(abs)) { resolvedScript = abs; break; }
453
+ if (!fs.existsSync(abs)) continue;
454
+ try {
455
+ resolveShimTarget(abs); // throws only when `abs` IS a shim with a decayed pointer
456
+ resolvedScript = abs;
457
+ break;
458
+ } catch (err) {
459
+ decayError = err.message;
460
+ }
388
461
  }
389
462
  if (resolvedScript) break;
390
463
  }
391
464
 
392
465
  const ok = !!resolvedScript;
393
466
  let detail;
394
- if (ok) detail = `guard-prd-writes PreToolUse hook found in ${cwd}/.claude/settings.json, resolving to ${resolvedScript}`;
395
- else if (mentioned) detail = `guard-prd-writes PreToolUse hook in ${cwd}/.claude/settings.json names a script that does not exist — it would silently guard nothing`;
396
- else detail = `no guard-prd-writes PreToolUse hook in ${cwd}/.claude/settings.json`;
467
+ if (ok) detail = `${def.guardName} PreToolUse hook found in ${cwd}/.claude/settings.json, resolving to ${resolvedScript}`;
468
+ else if (decayError) detail = `${def.guardName} PreToolUse hook in ${cwd}/.claude/settings.json points at a stable shim whose pointer has decayed — it would silently guard nothing: ${decayError}`;
469
+ else if (mentioned) detail = `${def.guardName} PreToolUse hook in ${cwd}/.claude/settings.json names a script that does not exist — it would silently guard nothing`;
470
+ else detail = `no ${def.guardName} PreToolUse hook in ${cwd}/.claude/settings.json${def.nudge ? ' — this is a nudge, not a hard gate' : ''}`;
397
471
 
398
472
  return {
399
- id: 'prd-write-guard',
400
- label: 'PRD-write guard hook installed',
473
+ id: def.id,
474
+ label: def.label,
401
475
  ok,
402
476
  detail,
403
477
  fix: ok
404
478
  ? null
405
- : `Add a PreToolUse hook to ${cwd}/.claude/settings.json matching Write|Edit|NotebookEdit that runs node ${PRD_WRITE_GUARD_SCRIPT}`,
406
- // The one check with a sanctioned one-press install (installPrdWriteGuard).
407
- fixAction: ok ? null : 'install-prd-write-guard',
479
+ : `Add a PreToolUse hook to ${cwd}/.claude/settings.json matching ${def.matcher} that runs node ${guardShimPath(def.scriptBasename, homeDir)}${def.nudge ? ' (a nudge, not a hard gate)' : ''}`,
480
+ fixAction: ok ? null : def.fixActionId,
408
481
  };
409
482
  }
410
483
 
411
-
412
484
  /**
413
- * Install the PRD-write guard into `<cwd>/.claude/settings.json`.
485
+ * Install `def`'s guard into `<cwd>/.claude/settings.json`.
414
486
  *
415
487
  * ── The standardized approach is REFERENCE, not vendor ────────────────────
416
- * The hook command points at session-manager's own copy of
417
- * scripts/hooks/guard-prd-writes.cjs by ABSOLUTE path. We deliberately do NOT
488
+ * The hook command points at session-manager's own copy of the guard script
489
+ * by ABSOLUTE path (via the stable shim, see below). We deliberately do NOT
418
490
  * copy the script into the adopting repo:
419
491
  *
420
492
  * - Vendoring drifts. It already did, with exactly one adopter
@@ -426,15 +498,29 @@ function checkPrdWriteGuard({ cwd }) {
426
498
  * absolute path is the entire dependency.
427
499
  *
428
500
  * The liability of reference — a silently-no-op hook if the script ever moves
429
- * — is closed by checkPrdWriteGuard() above, which resolves the command's
430
- * script path and fails the check when the file is gone.
501
+ * — is closed by checkGuard() above, which resolves the command's script path
502
+ * and fails the check when the file is gone.
431
503
  *
432
504
  * Merges into any existing hooks block rather than clobbering it: other
433
505
  * PreToolUse matchers, other hooks under the same matcher, and every unrelated
434
506
  * settings key are preserved. Idempotent — a healthy guard is a no-op, and a
435
- * guard entry pointing at a missing script is repaired in place.
507
+ * guard entry pointing at a missing script is repaired in place. Two guards
508
+ * sharing the same matcher string (prd-write and inline-implementation both
509
+ * use `Write|Edit|NotebookEdit`) fall into the SAME PreToolUse entry's hooks
510
+ * array automatically — nothing here special-cases that, it falls out of
511
+ * "find the matcher, else push a new one".
512
+ *
513
+ * ── Stable shim, not the raw app path ─────────────────────────────────────
514
+ * The hook command written below points at
515
+ * `~/.claude/session-manager/hooks/<guard>.cjs` (guardShims.cjs), a tiny shim
516
+ * that re-requires the real script through a pointer file rewritten on every
517
+ * boot — not at `def.script` directly. Run via
518
+ * `npx claude-code-session-manager@latest`, `def.script` resolves under an
519
+ * ephemeral `~/.npm/_npx/<hash>/...` directory that the next `npx ...@latest`
520
+ * or npm cache prune deletes — the shim survives every app upgrade because
521
+ * only its pointer file changes, never its own path.
436
522
  */
437
- async function installPrdWriteGuard({ cwd }) {
523
+ async function installGuard(def, { cwd, homeDir = os.homedir(), skipShimEnsure = false }) {
438
524
  const settingsPath = path.join(cwd, '.claude', 'settings.json');
439
525
  // config.cjs's validateWrite only permits `<registered project root>/.claude/**`,
440
526
  // and a project the user is merely *pointing* the New Epic dialog at may
@@ -448,20 +534,27 @@ async function installPrdWriteGuard({ cwd }) {
448
534
  // the human has in there.
449
535
  return { ok: false, action: 'error', error: `${settingsPath} is not valid JSON — fix it by hand before installing the guard` };
450
536
  }
451
- if (!fs.existsSync(PRD_WRITE_GUARD_SCRIPT)) {
537
+ if (!fs.existsSync(def.script)) {
452
538
  // Writing a hook that points at a script that doesn't exist would report
453
539
  // installed while silently guarding nothing (a non-zero-without-2 exit is
454
540
  // a non-blocking error to the harness) — refuse instead of lying.
455
- return { ok: false, action: 'error', error: `${PRD_WRITE_GUARD_SCRIPT} does not exist — cannot install a guard hook that points at a missing script` };
541
+ return { ok: false, action: 'error', error: `${def.script} does not exist — cannot install a guard hook that points at a missing script` };
542
+ }
543
+ // Same refusal principle as above: never write a hook command pointing at
544
+ // a shim that does not exist on disk. Skipped when the caller (installAllGuards)
545
+ // already ensured it once for this whole pass — see that function's comment.
546
+ if (!skipShimEnsure) {
547
+ const shimError = await ensureGuardShimsOrError(homeDir);
548
+ if (shimError) return shimError;
456
549
  }
457
550
  const settings = (existing && typeof existing === 'object' && !Array.isArray(existing)) ? existing : {};
458
551
 
459
- const command = `node ${PRD_WRITE_GUARD_SCRIPT}`;
552
+ const command = `node ${guardShimPath(def.scriptBasename, homeDir)}`;
460
553
  const hooks = (settings.hooks && typeof settings.hooks === 'object') ? settings.hooks : {};
461
554
  const preToolUse = Array.isArray(hooks.PreToolUse) ? hooks.PreToolUse.slice() : [];
462
555
 
463
556
  // Already healthy? Nothing to do.
464
- if (checkPrdWriteGuard({ cwd }).ok) {
557
+ if (checkGuard(def, { cwd, homeDir }).ok) {
465
558
  return { ok: true, action: 'already-installed', settingsPath, command };
466
559
  }
467
560
 
@@ -470,7 +563,7 @@ async function installPrdWriteGuard({ cwd }) {
470
563
  for (const matcher of preToolUse) {
471
564
  if (!Array.isArray(matcher?.hooks)) continue;
472
565
  for (const h of matcher.hooks) {
473
- if (typeof h?.command === 'string' && h.command.includes('guard-prd-writes')) {
566
+ if (typeof h?.command === 'string' && h.command.includes(def.guardName)) {
474
567
  h.command = command;
475
568
  h.type = 'command';
476
569
  repaired = true;
@@ -479,12 +572,12 @@ async function installPrdWriteGuard({ cwd }) {
479
572
  }
480
573
 
481
574
  if (!repaired) {
482
- const target = preToolUse.find((m) => m?.matcher === PRD_WRITE_GUARD_MATCHER);
575
+ const target = preToolUse.find((m) => m?.matcher === def.matcher);
483
576
  if (target) {
484
577
  target.hooks = Array.isArray(target.hooks) ? target.hooks : [];
485
578
  target.hooks.push({ type: 'command', command });
486
579
  } else {
487
- preToolUse.push({ matcher: PRD_WRITE_GUARD_MATCHER, hooks: [{ type: 'command', command }] });
580
+ preToolUse.push({ matcher: def.matcher, hooks: [{ type: 'command', command }] });
488
581
  }
489
582
  }
490
583
 
@@ -494,208 +587,36 @@ async function installPrdWriteGuard({ cwd }) {
494
587
  return { ok: true, action: repaired ? 'repaired' : 'installed', settingsPath, command };
495
588
  }
496
589
 
497
- /**
498
- * `ok` must mean "a hook that will actually run", not "the settings file
499
- * mentions guard-destructive-git somewhere" — same rationale as
500
- * checkPrdWriteGuard above (a PreToolUse command pointing at a script that
501
- * doesn't exist in this project exits non-zero WITHOUT code 2, a
502
- * non-blocking error that silently guards nothing).
503
- */
504
- function checkDestructiveGitGuard({ cwd }) {
505
- const settings = readJsonSafe(path.join(cwd, '.claude', 'settings.json'), null);
506
- const preToolUse = Array.isArray(settings?.hooks?.PreToolUse) ? settings.hooks.PreToolUse : [];
507
-
508
- let mentioned = false;
509
- let resolvedScript = null;
510
- for (const matcher of preToolUse) {
511
- if (!Array.isArray(matcher?.hooks)) continue;
512
- for (const h of matcher.hooks) {
513
- if (typeof h?.command !== 'string' || !h.command.includes('guard-destructive-git')) continue;
514
- mentioned = true;
515
- const raw = extractGuardScriptPath(h.command, 'guard-destructive-git.cjs');
516
- if (!raw) continue;
517
- const abs = path.isAbsolute(raw) ? raw : path.resolve(cwd, raw);
518
- if (fs.existsSync(abs)) { resolvedScript = abs; break; }
519
- }
520
- if (resolvedScript) break;
521
- }
522
-
523
- const ok = !!resolvedScript;
524
- let detail;
525
- if (ok) detail = `guard-destructive-git PreToolUse hook found in ${cwd}/.claude/settings.json, resolving to ${resolvedScript}`;
526
- else if (mentioned) detail = `guard-destructive-git PreToolUse hook in ${cwd}/.claude/settings.json names a script that does not exist — it would silently guard nothing`;
527
- else detail = `no guard-destructive-git PreToolUse hook in ${cwd}/.claude/settings.json`;
528
-
529
- return {
530
- id: 'destructive-git-guard',
531
- label: 'Destructive-git guard hook installed',
532
- ok,
533
- detail,
534
- fix: ok
535
- ? null
536
- : `Add a PreToolUse hook to ${cwd}/.claude/settings.json matching Bash that runs node ${DESTRUCTIVE_GIT_GUARD_SCRIPT}`,
537
- fixAction: ok ? null : 'install-destructive-git-guard',
538
- };
590
+ function checkPrdWriteGuard({ cwd, homeDir = os.homedir() } = {}) {
591
+ return checkGuard(GUARD_DEFS.prdWrite, { cwd, homeDir });
539
592
  }
540
593
 
541
- /**
542
- * Install the destructive-git guard into `<cwd>/.claude/settings.json`.
543
- * Same reference-not-vendor rationale, and the same merge-into-existing-
544
- * hooks-block / repair-in-place / idempotent behaviour as installPrdWriteGuard
545
- * above — see that function's header. The one difference is the matcher
546
- * (`Bash`, not `Write|Edit|NotebookEdit`), so this lands as its own
547
- * `PreToolUse` entry rather than sharing installPrdWriteGuard's.
548
- */
549
- async function installDestructiveGitGuard({ cwd }) {
550
- const settingsPath = path.join(cwd, '.claude', 'settings.json');
551
- addAllowedRoot(cwd);
552
-
553
- const existing = fs.existsSync(settingsPath) ? readJsonSafe(settingsPath, undefined) : {};
554
- if (existing === undefined) {
555
- return { ok: false, action: 'error', error: `${settingsPath} is not valid JSON — fix it by hand before installing the guard` };
556
- }
557
- if (!fs.existsSync(DESTRUCTIVE_GIT_GUARD_SCRIPT)) {
558
- return { ok: false, action: 'error', error: `${DESTRUCTIVE_GIT_GUARD_SCRIPT} does not exist — cannot install a guard hook that points at a missing script` };
559
- }
560
- const settings = (existing && typeof existing === 'object' && !Array.isArray(existing)) ? existing : {};
561
-
562
- const command = `node ${DESTRUCTIVE_GIT_GUARD_SCRIPT}`;
563
- const hooks = (settings.hooks && typeof settings.hooks === 'object') ? settings.hooks : {};
564
- const preToolUse = Array.isArray(hooks.PreToolUse) ? hooks.PreToolUse.slice() : [];
565
-
566
- if (checkDestructiveGitGuard({ cwd }).ok) {
567
- return { ok: true, action: 'already-installed', settingsPath, command };
568
- }
569
-
570
- let repaired = false;
571
- for (const matcher of preToolUse) {
572
- if (!Array.isArray(matcher?.hooks)) continue;
573
- for (const h of matcher.hooks) {
574
- if (typeof h?.command === 'string' && h.command.includes('guard-destructive-git')) {
575
- h.command = command;
576
- h.type = 'command';
577
- repaired = true;
578
- }
579
- }
580
- }
581
-
582
- if (!repaired) {
583
- const target = preToolUse.find((m) => m?.matcher === DESTRUCTIVE_GIT_GUARD_MATCHER);
584
- if (target) {
585
- target.hooks = Array.isArray(target.hooks) ? target.hooks : [];
586
- target.hooks.push({ type: 'command', command });
587
- } else {
588
- preToolUse.push({ matcher: DESTRUCTIVE_GIT_GUARD_MATCHER, hooks: [{ type: 'command', command }] });
589
- }
590
- }
591
-
592
- const next = { ...settings, hooks: { ...hooks, PreToolUse: preToolUse } };
593
- await writeJson(settingsPath, next);
594
-
595
- return { ok: true, action: repaired ? 'repaired' : 'installed', settingsPath, command };
594
+ async function installPrdWriteGuard({ cwd, homeDir = os.homedir(), skipShimEnsure = false } = {}) {
595
+ return installGuard(GUARD_DEFS.prdWrite, { cwd, homeDir, skipShimEnsure });
596
596
  }
597
597
 
598
- /**
599
- * `ok` must mean "a hook that will actually run", not "the settings file
600
- * mentions guard-inline-implementation somewhere" — same rationale as
601
- * checkPrdWriteGuard above. Note this guard is deliberately a NUDGE that
602
- * fails OPEN twice over (see guard-inline-implementation.cjs's header), not
603
- * an ownership law like guard-prd-writes — this check's label/detail must
604
- * not imply it's a hard gate.
605
- */
606
- function checkInlineImplementationGuard({ cwd }) {
607
- const settings = readJsonSafe(path.join(cwd, '.claude', 'settings.json'), null);
608
- const preToolUse = Array.isArray(settings?.hooks?.PreToolUse) ? settings.hooks.PreToolUse : [];
609
-
610
- let mentioned = false;
611
- let resolvedScript = null;
612
- for (const matcher of preToolUse) {
613
- if (!Array.isArray(matcher?.hooks)) continue;
614
- for (const h of matcher.hooks) {
615
- if (typeof h?.command !== 'string' || !h.command.includes('guard-inline-implementation')) continue;
616
- mentioned = true;
617
- const raw = extractGuardScriptPath(h.command, 'guard-inline-implementation.cjs');
618
- if (!raw) continue;
619
- const abs = path.isAbsolute(raw) ? raw : path.resolve(cwd, raw);
620
- if (fs.existsSync(abs)) { resolvedScript = abs; break; }
621
- }
622
- if (resolvedScript) break;
623
- }
624
-
625
- const ok = !!resolvedScript;
626
- let detail;
627
- if (ok) detail = `guard-inline-implementation PreToolUse hook found in ${cwd}/.claude/settings.json, resolving to ${resolvedScript}`;
628
- else if (mentioned) detail = `guard-inline-implementation PreToolUse hook in ${cwd}/.claude/settings.json names a script that does not exist — it would silently guard nothing`;
629
- else detail = `no guard-inline-implementation PreToolUse hook in ${cwd}/.claude/settings.json — this is a nudge, not a hard gate`;
630
-
631
- return {
632
- id: 'inline-implementation-guard',
633
- label: 'Inline-implementation guard hook installed',
634
- ok,
635
- detail,
636
- fix: ok
637
- ? null
638
- : `Add a PreToolUse hook to ${cwd}/.claude/settings.json matching Write|Edit|NotebookEdit that runs node ${INLINE_IMPLEMENTATION_GUARD_SCRIPT} (a nudge, not a hard gate)`,
639
- fixAction: ok ? null : 'install-inline-implementation-guard',
640
- };
598
+ function checkDestructiveGitGuard({ cwd, homeDir = os.homedir() } = {}) {
599
+ return checkGuard(GUARD_DEFS.destructiveGit, { cwd, homeDir });
641
600
  }
642
601
 
643
- /**
644
- * Install the inline-implementation guard into `<cwd>/.claude/settings.json`.
645
- * Same reference-not-vendor rationale and merge/repair/idempotence behaviour
646
- * as installPrdWriteGuard above — see that function's header. Unlike
647
- * installDestructiveGitGuard, this guard shares installPrdWriteGuard's
648
- * matcher (`Write|Edit|NotebookEdit`), so this appends into that SAME
649
- * matcher's `hooks` array when it already exists, rather than pushing a new
650
- * matcher entry.
651
- */
652
- async function installInlineImplementationGuard({ cwd }) {
653
- const settingsPath = path.join(cwd, '.claude', 'settings.json');
654
- addAllowedRoot(cwd);
655
-
656
- const existing = fs.existsSync(settingsPath) ? readJsonSafe(settingsPath, undefined) : {};
657
- if (existing === undefined) {
658
- return { ok: false, action: 'error', error: `${settingsPath} is not valid JSON — fix it by hand before installing the guard` };
659
- }
660
- if (!fs.existsSync(INLINE_IMPLEMENTATION_GUARD_SCRIPT)) {
661
- return { ok: false, action: 'error', error: `${INLINE_IMPLEMENTATION_GUARD_SCRIPT} does not exist — cannot install a guard hook that points at a missing script` };
662
- }
663
- const settings = (existing && typeof existing === 'object' && !Array.isArray(existing)) ? existing : {};
664
-
665
- const command = `node ${INLINE_IMPLEMENTATION_GUARD_SCRIPT}`;
666
- const hooks = (settings.hooks && typeof settings.hooks === 'object') ? settings.hooks : {};
667
- const preToolUse = Array.isArray(hooks.PreToolUse) ? hooks.PreToolUse.slice() : [];
668
-
669
- if (checkInlineImplementationGuard({ cwd }).ok) {
670
- return { ok: true, action: 'already-installed', settingsPath, command };
671
- }
602
+ async function installDestructiveGitGuard({ cwd, homeDir = os.homedir(), skipShimEnsure = false } = {}) {
603
+ return installGuard(GUARD_DEFS.destructiveGit, { cwd, homeDir, skipShimEnsure });
604
+ }
672
605
 
673
- let repaired = false;
674
- for (const matcher of preToolUse) {
675
- if (!Array.isArray(matcher?.hooks)) continue;
676
- for (const h of matcher.hooks) {
677
- if (typeof h?.command === 'string' && h.command.includes('guard-inline-implementation')) {
678
- h.command = command;
679
- h.type = 'command';
680
- repaired = true;
681
- }
682
- }
683
- }
606
+ function checkInlineImplementationGuard({ cwd, homeDir = os.homedir() } = {}) {
607
+ return checkGuard(GUARD_DEFS.inlineImplementation, { cwd, homeDir });
608
+ }
684
609
 
685
- if (!repaired) {
686
- const target = preToolUse.find((m) => m?.matcher === INLINE_IMPLEMENTATION_GUARD_MATCHER);
687
- if (target) {
688
- target.hooks = Array.isArray(target.hooks) ? target.hooks : [];
689
- target.hooks.push({ type: 'command', command });
690
- } else {
691
- preToolUse.push({ matcher: INLINE_IMPLEMENTATION_GUARD_MATCHER, hooks: [{ type: 'command', command }] });
692
- }
693
- }
610
+ async function installInlineImplementationGuard({ cwd, homeDir = os.homedir(), skipShimEnsure = false } = {}) {
611
+ return installGuard(GUARD_DEFS.inlineImplementation, { cwd, homeDir, skipShimEnsure });
612
+ }
694
613
 
695
- const next = { ...settings, hooks: { ...hooks, PreToolUse: preToolUse } };
696
- await writeJson(settingsPath, next);
614
+ function checkSelfScheduleGuard({ cwd, homeDir = os.homedir() } = {}) {
615
+ return checkGuard(GUARD_DEFS.selfSchedule, { cwd, homeDir });
616
+ }
697
617
 
698
- return { ok: true, action: repaired ? 'repaired' : 'installed', settingsPath, command };
618
+ async function installSelfScheduleGuard({ cwd, homeDir = os.homedir(), skipShimEnsure = false } = {}) {
619
+ return installGuard(GUARD_DEFS.selfSchedule, { cwd, homeDir, skipShimEnsure });
699
620
  }
700
621
 
701
622
  /**
@@ -720,9 +641,10 @@ async function checkDelegationReadiness({ cwd, homeDir = os.homedir() }) {
720
641
  checkSchedulerMcpProjectDuplicate({ cwd, homeDir }),
721
642
  checkDevPlugin({ homeDir }),
722
643
  checkAgentPersonas({ cwd, homeDir }),
723
- checkPrdWriteGuard({ cwd }),
724
- checkDestructiveGitGuard({ cwd }),
725
- checkInlineImplementationGuard({ cwd }),
644
+ checkPrdWriteGuard({ cwd, homeDir }),
645
+ checkDestructiveGitGuard({ cwd, homeDir }),
646
+ checkInlineImplementationGuard({ cwd, homeDir }),
647
+ checkSelfScheduleGuard({ cwd, homeDir }),
726
648
  ].map((c) => ({ fixAction: null, warn: false, ...c }));
727
649
 
728
650
  return {
@@ -731,11 +653,152 @@ async function checkDelegationReadiness({ cwd, homeDir = os.homedir() }) {
731
653
  };
732
654
  }
733
655
 
656
+ /**
657
+ * Every auto-install attempt is logged from HERE, not just the manual
658
+ * Fix-It path in index.cjs's logGuardInstallAttempt — `scope: 'delegationReadiness'`
659
+ * is the distinct vocabulary this PRD's acceptance criteria names, so an
660
+ * auto-heal is never confused with a human pressing Fix It
661
+ * (scope: 'delegationReadinessGuardInstall'). Never throws — a logging
662
+ * failure must not prevent the guard install it's reporting on.
663
+ */
664
+ function logAutoInstallEvent({ cwd, guard, action, error }) {
665
+ try {
666
+ const opsErrorLog = require('./opsErrorLog.cjs');
667
+ opsErrorLog.appendError({
668
+ cwd,
669
+ scope: 'delegationReadiness',
670
+ level: action === 'error' ? 'error' : 'info',
671
+ message: `auto-install ${guard}: ${action}`,
672
+ meta: { guard, action, ...(error ? { error } : {}) },
673
+ });
674
+ } catch { /* logging must never break the caller */ }
675
+ }
676
+
677
+ const GUARDS = [
678
+ { id: 'prd-write-guard', install: installPrdWriteGuard },
679
+ { id: 'destructive-git-guard', install: installDestructiveGitGuard },
680
+ { id: 'inline-implementation-guard', install: installInlineImplementationGuard },
681
+ { id: 'self-schedule-guard', install: installSelfScheduleGuard },
682
+ ];
683
+
684
+ // resolved project root -> Promise<{ ok, root, guards }>, kept for the app's
685
+ // whole lifetime — a project that already passed all three guard checks
686
+ // once never gets re-probed/re-written on a later cwd-change/keystroke.
687
+ const guardsInstalledCache = new Map();
688
+
689
+ /** Test-only: forces the next ensureGuardsInstalled call to actually run. */
690
+ function clearGuardsInstalledCache() {
691
+ guardsInstalledCache.clear();
692
+ }
693
+
694
+ async function installAllGuards(root, homeDir) {
695
+ // Ensure the shim pointer + all three shims ONCE per pass, up front, rather
696
+ // than once per guard below — each install*Guard rewrites the SAME pointer
697
+ // file + shim files via writeTextAtomic (no content-equality skip), so
698
+ // calling it 3x here was 3x the atomic writes for identical content. The
699
+ // per-guard installers still ensure it themselves when called individually
700
+ // (the manual "Fix it" path installs one guard at a time and never goes
701
+ // through installAllGuards), and this call still refreshes the pointer to
702
+ // the current app root exactly as before an app upgrade requires.
703
+ const shimError = await ensureGuardShimsOrError(homeDir);
704
+ const guards = {};
705
+ for (const { id, install } of GUARDS) {
706
+ let outcome;
707
+ try {
708
+ outcome = shimError ? shimError : await install({ cwd: root, homeDir, skipShimEnsure: true });
709
+ } catch (err) {
710
+ outcome = { ok: false, action: 'error', error: err?.message ?? String(err) };
711
+ }
712
+ guards[id] = outcome;
713
+ logAutoInstallEvent({
714
+ cwd: root,
715
+ guard: id,
716
+ action: outcome.ok ? outcome.action : 'error',
717
+ error: outcome.ok ? undefined : outcome.error,
718
+ });
719
+ }
720
+ return { ok: Object.values(guards).every((g) => g.ok), root, guards };
721
+ }
722
+
723
+ /**
724
+ * Silently, idempotently installs the three sanctioned guards for whatever
725
+ * real project `cwd` belongs to — the fix for "Fix It is the only call site,
726
+ * so every project starts red forever." Never throws (every failure mode —
727
+ * a bad/ephemeral/nonexistent cwd, an unwritable project, malformed JSON —
728
+ * resolves to a logged, ok:false outcome instead), so it is always safe to
729
+ * `await` directly in front of checkDelegationReadiness.
730
+ *
731
+ * `cwd` is resolved to its real project root via opsOwnership's
732
+ * resolveProjectRoot (same normalization the ops-write path already trusts)
733
+ * BEFORE anything is cached or written, so a job/Epic worktree cwd installs
734
+ * into the actual project's `.claude/settings.json`, never a copy that gets
735
+ * deleted with the worktree.
736
+ */
737
+ async function ensureGuardsInstalled(cwd, { homeDir = os.homedir() } = {}) {
738
+ let root;
739
+ try {
740
+ root = resolveProjectRoot(cwd);
741
+ } catch (err) {
742
+ logAutoInstallEvent({ cwd, guard: 'project-root', action: 'error', error: err?.message ?? String(err) });
743
+ return { ok: false, root: null, guards: {}, error: err?.message ?? String(err) };
744
+ }
745
+
746
+ const cacheKey = `${root}${homeDir}`;
747
+ const cached = guardsInstalledCache.get(cacheKey);
748
+ if (cached) return cached;
749
+
750
+ // A deleted/renamed project folder must never come back to life as a
751
+ // side effect of probing it — installPrdWriteGuard/etc. happily
752
+ // `mkdir -p` their target, which would otherwise resurrect a phantom
753
+ // `<root>/.claude/` for a project that no longer exists on disk. Refuse
754
+ // before any write is attempted, and log via console (not the ops error
755
+ // log — there is no legitimate ops root left to write into).
756
+ let rootStat;
757
+ try {
758
+ rootStat = fs.statSync(root);
759
+ } catch {
760
+ rootStat = null;
761
+ }
762
+ if (!rootStat || !rootStat.isDirectory()) {
763
+ const message = `project root does not exist on disk: ${root}`;
764
+ console.warn(`[delegationReadiness] ensureGuardsInstalled: ${message}`);
765
+ // Not cached — same "only a success is worth remembering" rule as the
766
+ // installAllGuards path below, so a worktree that hasn't materialized
767
+ // yet (or a project folder restored after a delete) gets retried on the
768
+ // next probe instead of being permanently marked ok:false.
769
+ return { ok: false, root, guards: {}, error: message };
770
+ }
771
+
772
+ const promise = installAllGuards(root, homeDir).then((result) => {
773
+ // The cache's whole purpose (see the comment above guardsInstalledCache)
774
+ // is to skip re-probing a project that already SUCCEEDED — not to lock in
775
+ // a failed first attempt (malformed settings.json, a transient write
776
+ // error) so the automatic self-heal can never retry even after the
777
+ // underlying problem is fixed. installAllGuards never throws for an
778
+ // ordinary per-guard failure (see its own try/catch), so this ok:false
779
+ // path — not the .catch below — is the one that actually fires for the
780
+ // realistic failure modes; only a genuine success is worth remembering
781
+ // for the app's lifetime.
782
+ if (!result.ok) guardsInstalledCache.delete(cacheKey);
783
+ return result;
784
+ }).catch((err) => {
785
+ guardsInstalledCache.delete(cacheKey); // don't poison the cache on an unexpected throw
786
+ const message = err?.message ?? String(err);
787
+ logAutoInstallEvent({ cwd: root, guard: 'ensure-guards-installed', action: 'error', error: message });
788
+ return { ok: false, root, guards: {}, error: message };
789
+ });
790
+ guardsInstalledCache.set(cacheKey, promise);
791
+ return promise;
792
+ }
793
+
734
794
  module.exports = {
735
795
  checkDelegationReadiness,
796
+ ensureGuardsInstalled,
797
+ clearGuardsInstalledCache,
736
798
  installPrdWriteGuard,
737
799
  installDestructiveGitGuard,
738
800
  installInlineImplementationGuard,
801
+ installSelfScheduleGuard,
739
802
  probeSchedulerMcpLive,
740
803
  clearLiveProbeCache,
741
804
  PRD_WRITE_GUARD_SCRIPT,
@@ -744,4 +807,6 @@ module.exports = {
744
807
  DESTRUCTIVE_GIT_GUARD_MATCHER,
745
808
  INLINE_IMPLEMENTATION_GUARD_SCRIPT,
746
809
  INLINE_IMPLEMENTATION_GUARD_MATCHER,
810
+ SELF_SCHEDULE_GUARD_SCRIPT,
811
+ SELF_SCHEDULE_GUARD_MATCHER,
747
812
  };