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
@@ -13,6 +13,8 @@ const os = require('node:os');
13
13
  const path = require('node:path');
14
14
  const {
15
15
  checkDelegationReadiness,
16
+ ensureGuardsInstalled,
17
+ clearGuardsInstalledCache,
16
18
  installPrdWriteGuard,
17
19
  installDestructiveGitGuard,
18
20
  installInlineImplementationGuard,
@@ -21,17 +23,28 @@ const {
21
23
  PRD_WRITE_GUARD_SCRIPT,
22
24
  DESTRUCTIVE_GIT_GUARD_SCRIPT,
23
25
  INLINE_IMPLEMENTATION_GUARD_SCRIPT,
26
+ SELF_SCHEDULE_GUARD_SCRIPT,
24
27
  } = require('../delegationReadiness.cjs');
28
+ const { todayFile: opsErrorLogTodayFile } = require('../opsErrorLog.cjs');
29
+ const {
30
+ writeGuardShims,
31
+ shimPath: guardShimPath,
32
+ hooksDir: guardHooksDir,
33
+ pointerPath: guardPointerPath,
34
+ APP_ROOT: GUARD_APP_ROOT,
35
+ } = require('../guardShims.cjs');
25
36
 
26
37
  const REQUIRED_TOOLS = ['scheduler_create_prd', 'session_manager_help'];
27
38
 
28
39
  beforeEach(() => {
29
40
  clearLiveProbeCache();
41
+ clearGuardsInstalledCache();
30
42
  });
31
43
 
32
44
  const tmpDirs = [];
33
45
  afterEach(async () => {
34
46
  clearLiveProbeCache();
47
+ clearGuardsInstalledCache();
35
48
  while (tmpDirs.length) {
36
49
  const d = tmpDirs.pop();
37
50
  await fsp.rm(d, { recursive: true, force: true });
@@ -108,6 +121,10 @@ async function makeGreenFixtures() {
108
121
  matcher: 'Bash',
109
122
  hooks: [{ type: 'command', command: `node ${DESTRUCTIVE_GIT_GUARD_SCRIPT}` }],
110
123
  },
124
+ {
125
+ matcher: 'ScheduleWakeup|CronCreate|Task|Agent',
126
+ hooks: [{ type: 'command', command: `node ${SELF_SCHEDULE_GUARD_SCRIPT}` }],
127
+ },
111
128
  ],
112
129
  },
113
130
  });
@@ -115,12 +132,12 @@ async function makeGreenFixtures() {
115
132
  return { homeDir, cwd, scriptPath };
116
133
  }
117
134
 
118
- test('all eight checks pass on a fully-configured project', async () => {
135
+ test('all nine checks pass on a fully-configured project', async () => {
119
136
  const { homeDir, cwd } = await makeGreenFixtures();
120
137
  const result = await checkDelegationReadiness({ cwd, homeDir });
121
138
 
122
139
  expect(result.ok).toBe(true);
123
- expect(result.checks).toHaveLength(8);
140
+ expect(result.checks).toHaveLength(9);
124
141
  expect(result.checks.every((c) => c.ok)).toBe(true);
125
142
  expect(result.checks.map((c) => c.id)).toEqual([
126
143
  'scheduler-mcp',
@@ -131,6 +148,7 @@ test('all eight checks pass on a fully-configured project', async () => {
131
148
  'prd-write-guard',
132
149
  'destructive-git-guard',
133
150
  'inline-implementation-guard',
151
+ 'self-schedule-guard',
134
152
  ]);
135
153
  }, 15_000);
136
154
 
@@ -340,43 +358,130 @@ test('scheduler-mcp-live: memoizes the probe per registration signature within t
340
358
 
341
359
  // ─────────────────────────────── installPrdWriteGuard (unchanged behavior)
342
360
 
343
- test('installPrdWriteGuard: writes the canonical ABSOLUTE-path entry and turns the check green', async () => {
361
+ test('installPrdWriteGuard: writes the canonical STABLE-SHIM-path entry and turns the check green', async () => {
344
362
  const { homeDir, cwd } = await makeGreenFixtures();
345
363
  await fsp.rm(path.join(cwd, '.claude', 'settings.json'));
346
364
 
347
- const r = await installPrdWriteGuard({ cwd });
365
+ const r = await installPrdWriteGuard({ cwd, homeDir });
348
366
  expect(r.ok).toBe(true);
349
367
  expect(r.action).toBe('installed');
350
- expect(r.command).toBe(`node ${PRD_WRITE_GUARD_SCRIPT}`);
368
+ const shimCommand = `node ${guardShimPath('guard-prd-writes.cjs', homeDir)}`;
369
+ expect(r.command).toBe(shimCommand);
370
+ // The shim path survives app upgrades; it must never be the raw app-root script.
371
+ expect(r.command).not.toBe(`node ${PRD_WRITE_GUARD_SCRIPT}`);
351
372
 
352
373
  const written = JSON.parse(fs.readFileSync(path.join(cwd, '.claude', 'settings.json'), 'utf8'));
353
374
  expect(written.hooks.PreToolUse).toEqual([
354
- { matcher: 'Write|Edit|NotebookEdit', hooks: [{ type: 'command', command: `node ${PRD_WRITE_GUARD_SCRIPT}` }] },
375
+ { matcher: 'Write|Edit|NotebookEdit', hooks: [{ type: 'command', command: shimCommand }] },
355
376
  ]);
356
377
  const result = await checkDelegationReadiness({ cwd, homeDir });
357
378
  expect(result.checks.find((c) => c.id === 'prd-write-guard').ok).toBe(true);
358
379
  }, 15_000);
359
380
 
360
381
  test('installPrdWriteGuard: is idempotent — a healthy guard is a no-op', async () => {
361
- const { cwd } = await makeGreenFixtures();
362
- await installPrdWriteGuard({ cwd });
382
+ const { homeDir, cwd } = await makeGreenFixtures();
383
+ await installPrdWriteGuard({ cwd, homeDir });
363
384
  const before = fs.readFileSync(path.join(cwd, '.claude', 'settings.json'), 'utf8');
364
385
 
365
- const again = await installPrdWriteGuard({ cwd });
386
+ const again = await installPrdWriteGuard({ cwd, homeDir });
366
387
  expect(again.action).toBe('already-installed');
367
388
  expect(fs.readFileSync(path.join(cwd, '.claude', 'settings.json'), 'utf8')).toBe(before);
368
389
  });
369
390
 
370
391
  test('installPrdWriteGuard: refuses on unparseable settings rather than discarding them', async () => {
371
- const { cwd } = await makeGreenFixtures();
392
+ const { homeDir, cwd } = await makeGreenFixtures();
372
393
  await fsp.writeFile(path.join(cwd, '.claude', 'settings.json'), '{ not valid json', 'utf8');
373
394
 
374
- const r = await installPrdWriteGuard({ cwd });
395
+ const r = await installPrdWriteGuard({ cwd, homeDir });
375
396
  expect(r.ok).toBe(false);
376
397
  expect(r.action).toBe('error');
377
398
  expect(fs.readFileSync(path.join(cwd, '.claude', 'settings.json'), 'utf8')).toBe('{ not valid json');
378
399
  });
379
400
 
401
+ // ─────────────────────────────── stable-shim indirection (guardShims.cjs)
402
+
403
+ test('installPrdWriteGuard: repairs a hook pointing at a dead ephemeral _npx cache path to the stable shim path', async () => {
404
+ const { homeDir, cwd } = await makeGreenFixtures();
405
+ // Shaped exactly like the decay this PRD closes: an absolute path under a
406
+ // version-scoped npx cache dir that no longer exists on this machine.
407
+ const deadNpxPath = path.join(
408
+ homeDir, '.npm', '_npx', '5346543b21849140', 'node_modules',
409
+ 'claude-code-session-manager', 'scripts', 'hooks', 'guard-prd-writes.cjs',
410
+ );
411
+ await writeJson(path.join(cwd, '.claude', 'settings.json'), {
412
+ hooks: { PreToolUse: [{ matcher: 'Write|Edit|NotebookEdit', hooks: [{ type: 'command', command: `node ${deadNpxPath}` }] }] },
413
+ });
414
+
415
+ const before = await checkDelegationReadiness({ cwd, homeDir });
416
+ expect(before.checks.find((c) => c.id === 'prd-write-guard').ok).toBe(false);
417
+
418
+ const r = await installPrdWriteGuard({ cwd, homeDir });
419
+ expect(r.ok).toBe(true);
420
+ expect(r.action).toBe('repaired');
421
+ const shimCommand = `node ${guardShimPath('guard-prd-writes.cjs', homeDir)}`;
422
+ expect(r.command).toBe(shimCommand);
423
+
424
+ const written = JSON.parse(fs.readFileSync(path.join(cwd, '.claude', 'settings.json'), 'utf8'));
425
+ const writeMatchers = written.hooks.PreToolUse.filter((m) => m.matcher === 'Write|Edit|NotebookEdit');
426
+ expect(writeMatchers).toHaveLength(1);
427
+ expect(writeMatchers[0].hooks).toEqual([{ type: 'command', command: shimCommand }]);
428
+
429
+ const after = await checkDelegationReadiness({ cwd, homeDir });
430
+ expect(after.checks.find((c) => c.id === 'prd-write-guard').ok).toBe(true);
431
+ }, 15_000);
432
+
433
+ test('checkPrdWriteGuard: reports red — not a false green — when an installed shim\'s pointer has decayed (one hop downstream of the hook command)', async () => {
434
+ const { homeDir, cwd } = await makeGreenFixtures();
435
+ // makeGreenFixtures seeds a hook pointing DIRECTLY at the app-root script —
436
+ // already healthy, so installPrdWriteGuard would short-circuit as
437
+ // 'already-installed' without ever switching the command to the shim.
438
+ // Remove it first so the install genuinely writes the shim-path command.
439
+ await fsp.rm(path.join(cwd, '.claude', 'settings.json'));
440
+ const r = await installPrdWriteGuard({ cwd, homeDir });
441
+ expect(r.ok).toBe(true);
442
+ expect(r.command).toBe(`node ${guardShimPath('guard-prd-writes.cjs', homeDir)}`);
443
+ const healthy = await checkDelegationReadiness({ cwd, homeDir });
444
+ expect(healthy.checks.find((c) => c.id === 'prd-write-guard').ok).toBe(true);
445
+
446
+ // Corrupt ONLY the pointer — the shim FILE at the hook's command path never
447
+ // moves, which is exactly why checking just its existence isn't enough.
448
+ await writeJson(guardPointerPath(homeDir), { appRoot: path.join(homeDir, 'deleted-app-root') });
449
+
450
+ const decayed = await checkDelegationReadiness({ cwd, homeDir });
451
+ const check = decayed.checks.find((c) => c.id === 'prd-write-guard');
452
+ expect(check.ok).toBe(false);
453
+ expect(check.detail).toMatch(/pointer has decayed/);
454
+ }, 15_000);
455
+
456
+ test('checkPrdWriteGuard: accepts BOTH a direct app-root path and the stable shim path as healthy', async () => {
457
+ const { homeDir, cwd } = await makeGreenFixtures(); // direct app-root path, already healthy
458
+ const directResult = await checkDelegationReadiness({ cwd, homeDir });
459
+ expect(directResult.checks.find((c) => c.id === 'prd-write-guard').ok).toBe(true);
460
+
461
+ const shimResult = await writeGuardShims({ homeDir });
462
+ expect(shimResult.ok).toBe(true);
463
+ await writeJson(path.join(cwd, '.claude', 'settings.json'), {
464
+ hooks: { PreToolUse: [{ matcher: 'Write|Edit|NotebookEdit', hooks: [{ type: 'command', command: `node ${guardShimPath('guard-prd-writes.cjs', homeDir)}` }] }] },
465
+ });
466
+ const viaShim = await checkDelegationReadiness({ cwd, homeDir });
467
+ expect(viaShim.checks.find((c) => c.id === 'prd-write-guard').ok).toBe(true);
468
+ }, 15_000);
469
+
470
+ test('installPrdWriteGuard: refuses with ok:false/action:error when the guard shim cannot be written, never writing a hook pointing at a missing shim', async () => {
471
+ const { cwd } = await makeGreenFixtures();
472
+ await fsp.rm(path.join(cwd, '.claude', 'settings.json'));
473
+ const base = await mkTmp('sm-delegation-unwritable-');
474
+ const blocker = path.join(base, 'blocker-file');
475
+ await fsp.writeFile(blocker, 'x', 'utf8');
476
+ const unwritableHomeDir = path.join(blocker, 'home'); // parent segment is a FILE -> mkdir fails
477
+
478
+ const r = await installPrdWriteGuard({ cwd, homeDir: unwritableHomeDir });
479
+ expect(r.ok).toBe(false);
480
+ expect(r.action).toBe('error');
481
+ expect(r.error).toBeTruthy();
482
+ expect(fs.existsSync(path.join(cwd, '.claude', 'settings.json'))).toBe(false);
483
+ }, 15_000);
484
+
380
485
  // The guard script is not just referenced — it is EXERCISED here, so the
381
486
  // "certifies a config that has never been proven to run" gap is closed by a
382
487
  // real end-to-end run of the exact command installPrdWriteGuard writes.
@@ -410,6 +515,10 @@ test('destructive-git-guard: fails independently when the hook is missing', asyn
410
515
  { type: 'command', command: `node ${INLINE_IMPLEMENTATION_GUARD_SCRIPT}` },
411
516
  ],
412
517
  },
518
+ {
519
+ matcher: 'ScheduleWakeup|CronCreate|Task|Agent',
520
+ hooks: [{ type: 'command', command: `node ${SELF_SCHEDULE_GUARD_SCRIPT}` }],
521
+ },
413
522
  ],
414
523
  },
415
524
  });
@@ -445,7 +554,7 @@ test('destructive-git-guard: fails when the hook names a script that does not ex
445
554
  expect(check.detail).toMatch(/does not exist/);
446
555
  }, 15_000);
447
556
 
448
- test('installDestructiveGitGuard: writes the canonical ABSOLUTE-path entry and turns the check green', async () => {
557
+ test('installDestructiveGitGuard: writes the canonical STABLE-SHIM-path entry and turns the check green', async () => {
449
558
  const { homeDir, cwd } = await makeGreenFixtures();
450
559
  await writeJson(path.join(cwd, '.claude', 'settings.json'), {
451
560
  hooks: {
@@ -458,10 +567,12 @@ test('installDestructiveGitGuard: writes the canonical ABSOLUTE-path entry and t
458
567
  },
459
568
  });
460
569
 
461
- const r = await installDestructiveGitGuard({ cwd });
570
+ const r = await installDestructiveGitGuard({ cwd, homeDir });
462
571
  expect(r.ok).toBe(true);
463
572
  expect(r.action).toBe('installed');
464
- expect(r.command).toBe(`node ${DESTRUCTIVE_GIT_GUARD_SCRIPT}`);
573
+ const shimCommand = `node ${guardShimPath('guard-destructive-git.cjs', homeDir)}`;
574
+ expect(r.command).toBe(shimCommand);
575
+ expect(r.command).not.toBe(`node ${DESTRUCTIVE_GIT_GUARD_SCRIPT}`);
465
576
 
466
577
  const written = JSON.parse(fs.readFileSync(path.join(cwd, '.claude', 'settings.json'), 'utf8'));
467
578
  // The pre-existing Write|Edit|NotebookEdit matcher (and its hook) survives untouched.
@@ -469,7 +580,7 @@ test('installDestructiveGitGuard: writes the canonical ABSOLUTE-path entry and t
469
580
  { matcher: 'Write|Edit|NotebookEdit', hooks: [{ type: 'command', command: `node ${PRD_WRITE_GUARD_SCRIPT}` }] },
470
581
  );
471
582
  expect(written.hooks.PreToolUse).toContainEqual(
472
- { matcher: 'Bash', hooks: [{ type: 'command', command: `node ${DESTRUCTIVE_GIT_GUARD_SCRIPT}` }] },
583
+ { matcher: 'Bash', hooks: [{ type: 'command', command: shimCommand }] },
473
584
  );
474
585
 
475
586
  const result = await checkDelegationReadiness({ cwd, homeDir });
@@ -499,14 +610,15 @@ test('installDestructiveGitGuard: flips destructive-git-guard FAIL -> PASS on a
499
610
  detail: `no guard-destructive-git PreToolUse hook in ${cwd}/.claude/settings.json`,
500
611
  });
501
612
 
502
- const first = await installDestructiveGitGuard({ cwd });
613
+ const first = await installDestructiveGitGuard({ cwd, homeDir });
503
614
  expect(first).toMatchObject({ ok: true, action: 'installed', settingsPath });
504
615
 
505
616
  const written = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
506
617
  const bash = written.hooks.PreToolUse.filter((m) => m.matcher === 'Bash');
507
618
  expect(bash).toHaveLength(1);
508
- // By reference to THIS repo's absolute script — never a copy inside the target cwd.
509
- expect(bash[0].hooks).toEqual([{ type: 'command', command: `node ${DESTRUCTIVE_GIT_GUARD_SCRIPT}` }]);
619
+ // By reference to the stable shim — never a copy inside the target cwd.
620
+ const shimCommand = `node ${guardShimPath('guard-destructive-git.cjs', homeDir)}`;
621
+ expect(bash[0].hooks).toEqual([{ type: 'command', command: shimCommand }]);
510
622
  expect(path.isAbsolute(DESTRUCTIVE_GIT_GUARD_SCRIPT)).toBe(true);
511
623
  expect(DESTRUCTIVE_GIT_GUARD_SCRIPT.startsWith(cwd + path.sep)).toBe(false);
512
624
  expect(fs.existsSync(path.join(cwd, 'scripts'))).toBe(false);
@@ -517,22 +629,22 @@ test('installDestructiveGitGuard: flips destructive-git-guard FAIL -> PASS on a
517
629
 
518
630
  // Second press: no-op, byte-identical file.
519
631
  const snapshot = fs.readFileSync(settingsPath, 'utf8');
520
- const second = await installDestructiveGitGuard({ cwd });
632
+ const second = await installDestructiveGitGuard({ cwd, homeDir });
521
633
  expect(second).toMatchObject({ ok: true, action: 'already-installed', settingsPath });
522
634
  expect(fs.readFileSync(settingsPath, 'utf8')).toBe(snapshot);
523
635
  }, 15_000);
524
636
 
525
637
  test('installDestructiveGitGuard: is idempotent — a healthy guard is a no-op', async () => {
526
- const { cwd } = await makeGreenFixtures();
638
+ const { homeDir, cwd } = await makeGreenFixtures();
527
639
  const before = fs.readFileSync(path.join(cwd, '.claude', 'settings.json'), 'utf8');
528
640
 
529
- const again = await installDestructiveGitGuard({ cwd });
641
+ const again = await installDestructiveGitGuard({ cwd, homeDir });
530
642
  expect(again.action).toBe('already-installed');
531
643
  expect(fs.readFileSync(path.join(cwd, '.claude', 'settings.json'), 'utf8')).toBe(before);
532
644
  });
533
645
 
534
646
  test('installDestructiveGitGuard: repairs a broken entry in place rather than duplicating it', async () => {
535
- const { cwd } = await makeGreenFixtures();
647
+ const { homeDir, cwd } = await makeGreenFixtures();
536
648
  await writeJson(path.join(cwd, '.claude', 'settings.json'), {
537
649
  hooks: {
538
650
  PreToolUse: [
@@ -548,20 +660,20 @@ test('installDestructiveGitGuard: repairs a broken entry in place rather than du
548
660
  },
549
661
  });
550
662
 
551
- const r = await installDestructiveGitGuard({ cwd });
663
+ const r = await installDestructiveGitGuard({ cwd, homeDir });
552
664
  expect(r.action).toBe('repaired');
553
665
 
554
666
  const written = JSON.parse(fs.readFileSync(path.join(cwd, '.claude', 'settings.json'), 'utf8'));
555
667
  const bashMatchers = written.hooks.PreToolUse.filter((m) => m.matcher === 'Bash');
556
668
  expect(bashMatchers).toHaveLength(1);
557
- expect(bashMatchers[0].hooks).toEqual([{ type: 'command', command: `node ${DESTRUCTIVE_GIT_GUARD_SCRIPT}` }]);
669
+ expect(bashMatchers[0].hooks).toEqual([{ type: 'command', command: `node ${guardShimPath('guard-destructive-git.cjs', homeDir)}` }]);
558
670
  });
559
671
 
560
672
  test('installDestructiveGitGuard: refuses on unparseable settings rather than discarding them', async () => {
561
- const { cwd } = await makeGreenFixtures();
673
+ const { homeDir, cwd } = await makeGreenFixtures();
562
674
  await fsp.writeFile(path.join(cwd, '.claude', 'settings.json'), '{ not valid json', 'utf8');
563
675
 
564
- const r = await installDestructiveGitGuard({ cwd });
676
+ const r = await installDestructiveGitGuard({ cwd, homeDir });
565
677
  expect(r.ok).toBe(false);
566
678
  expect(r.action).toBe('error');
567
679
  expect(fs.readFileSync(path.join(cwd, '.claude', 'settings.json'), 'utf8')).toBe('{ not valid json');
@@ -602,6 +714,10 @@ test('inline-implementation-guard: fails independently when the hook is missing
602
714
  matcher: 'Bash',
603
715
  hooks: [{ type: 'command', command: `node ${DESTRUCTIVE_GIT_GUARD_SCRIPT}` }],
604
716
  },
717
+ {
718
+ matcher: 'ScheduleWakeup|CronCreate|Task|Agent',
719
+ hooks: [{ type: 'command', command: `node ${SELF_SCHEDULE_GUARD_SCRIPT}` }],
720
+ },
605
721
  ],
606
722
  },
607
723
  });
@@ -676,8 +792,9 @@ test('installInlineImplementationGuard: appends into the EXISTING Write|Edit|Not
676
792
  fixAction: 'install-inline-implementation-guard',
677
793
  });
678
794
 
679
- const r = await installInlineImplementationGuard({ cwd });
680
- expect(r).toMatchObject({ ok: true, action: 'installed', settingsPath, command: `node ${INLINE_IMPLEMENTATION_GUARD_SCRIPT}` });
795
+ const r = await installInlineImplementationGuard({ cwd, homeDir });
796
+ const inlineShimCommand = `node ${guardShimPath('guard-inline-implementation.cjs', homeDir)}`;
797
+ expect(r).toMatchObject({ ok: true, action: 'installed', settingsPath, command: inlineShimCommand });
681
798
 
682
799
  const written = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
683
800
  // Exactly ONE Write|Edit|NotebookEdit matcher entry, now carrying BOTH commands.
@@ -685,7 +802,7 @@ test('installInlineImplementationGuard: appends into the EXISTING Write|Edit|Not
685
802
  expect(writeMatchers).toHaveLength(1);
686
803
  expect(writeMatchers[0].hooks).toEqual([
687
804
  { type: 'command', command: `node ${PRD_WRITE_GUARD_SCRIPT}` },
688
- { type: 'command', command: `node ${INLINE_IMPLEMENTATION_GUARD_SCRIPT}` },
805
+ { type: 'command', command: inlineShimCommand },
689
806
  ]);
690
807
  // The unrelated Bash matcher and every unrelated top-level key survive untouched.
691
808
  expect(written.hooks.PreToolUse).toContainEqual(
@@ -700,7 +817,7 @@ test('installInlineImplementationGuard: appends into the EXISTING Write|Edit|Not
700
817
  }, 15_000);
701
818
 
702
819
  test('installInlineImplementationGuard: is idempotent — running it twice leaves exactly one entry, byte-identical file on the second press', async () => {
703
- const { cwd } = await makeGreenFixtures();
820
+ const { homeDir, cwd } = await makeGreenFixtures();
704
821
  await writeJson(path.join(cwd, '.claude', 'settings.json'), {
705
822
  hooks: {
706
823
  PreToolUse: [
@@ -709,11 +826,11 @@ test('installInlineImplementationGuard: is idempotent — running it twice leave
709
826
  },
710
827
  });
711
828
 
712
- const first = await installInlineImplementationGuard({ cwd });
829
+ const first = await installInlineImplementationGuard({ cwd, homeDir });
713
830
  expect(first.action).toBe('installed');
714
831
  const snapshot = fs.readFileSync(path.join(cwd, '.claude', 'settings.json'), 'utf8');
715
832
 
716
- const second = await installInlineImplementationGuard({ cwd });
833
+ const second = await installInlineImplementationGuard({ cwd, homeDir });
717
834
  expect(second.action).toBe('already-installed');
718
835
  expect(fs.readFileSync(path.join(cwd, '.claude', 'settings.json'), 'utf8')).toBe(snapshot);
719
836
 
@@ -724,7 +841,7 @@ test('installInlineImplementationGuard: is idempotent — running it twice leave
724
841
  });
725
842
 
726
843
  test('installInlineImplementationGuard: repairs an entry pointing at a now-nonexistent path in place rather than appending a duplicate', async () => {
727
- const { cwd } = await makeGreenFixtures();
844
+ const { homeDir, cwd } = await makeGreenFixtures();
728
845
  const settingsPath = path.join(cwd, '.claude', 'settings.json');
729
846
  await writeJson(settingsPath, {
730
847
  hooks: {
@@ -740,7 +857,7 @@ test('installInlineImplementationGuard: repairs an entry pointing at a now-nonex
740
857
  },
741
858
  });
742
859
 
743
- const r = await installInlineImplementationGuard({ cwd });
860
+ const r = await installInlineImplementationGuard({ cwd, homeDir });
744
861
  expect(r.action).toBe('repaired');
745
862
 
746
863
  const written = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
@@ -748,14 +865,14 @@ test('installInlineImplementationGuard: repairs an entry pointing at a now-nonex
748
865
  expect(writeMatchers).toHaveLength(1);
749
866
  const inlineHooks = writeMatchers[0].hooks.filter((h) => h.command.includes('guard-inline-implementation'));
750
867
  expect(inlineHooks).toHaveLength(1);
751
- expect(inlineHooks[0].command).toBe(`node ${INLINE_IMPLEMENTATION_GUARD_SCRIPT}`);
868
+ expect(inlineHooks[0].command).toBe(`node ${guardShimPath('guard-inline-implementation.cjs', homeDir)}`);
752
869
  });
753
870
 
754
871
  test('installInlineImplementationGuard: refuses on unparseable settings rather than discarding them', async () => {
755
- const { cwd } = await makeGreenFixtures();
872
+ const { homeDir, cwd } = await makeGreenFixtures();
756
873
  await fsp.writeFile(path.join(cwd, '.claude', 'settings.json'), '{ not valid json', 'utf8');
757
874
 
758
- const r = await installInlineImplementationGuard({ cwd });
875
+ const r = await installInlineImplementationGuard({ cwd, homeDir });
759
876
  expect(r.ok).toBe(false);
760
877
  expect(r.action).toBe('error');
761
878
  expect(fs.readFileSync(path.join(cwd, '.claude', 'settings.json'), 'utf8')).toBe('{ not valid json');
@@ -863,15 +980,177 @@ test('installInlineImplementationGuard: refuses when its guard script is missing
863
980
  });
864
981
  });
865
982
 
866
- test('installInlineImplementationGuard: uses the ABSOLUTE reference path, never a vendored copy', async () => {
867
- const { cwd } = await makeGreenFixtures();
983
+ test('installInlineImplementationGuard: uses the ABSOLUTE stable-shim path, never a vendored copy — and the shim itself resolves by REFERENCE, not a copy', async () => {
984
+ const { homeDir, cwd } = await makeGreenFixtures();
868
985
  await writeJson(path.join(cwd, '.claude', 'settings.json'), {
869
986
  hooks: { PreToolUse: [{ matcher: 'Write|Edit|NotebookEdit', hooks: [{ type: 'command', command: `node ${PRD_WRITE_GUARD_SCRIPT}` }] }] },
870
987
  });
871
988
 
872
- const r = await installInlineImplementationGuard({ cwd });
873
- expect(path.isAbsolute(INLINE_IMPLEMENTATION_GUARD_SCRIPT)).toBe(true);
874
- expect(r.command).toContain(INLINE_IMPLEMENTATION_GUARD_SCRIPT);
875
- expect(INLINE_IMPLEMENTATION_GUARD_SCRIPT.startsWith(cwd + path.sep)).toBe(false);
989
+ const r = await installInlineImplementationGuard({ cwd, homeDir });
990
+ const shimFile = guardShimPath('guard-inline-implementation.cjs', homeDir);
991
+ expect(path.isAbsolute(shimFile)).toBe(true);
992
+ expect(r.command).toBe(`node ${shimFile}`);
993
+ expect(shimFile.startsWith(cwd + path.sep)).toBe(false);
876
994
  expect(fs.existsSync(path.join(cwd, 'scripts'))).toBe(false);
995
+
996
+ // The shim is not a vendored copy of the real guard — it's a pointer-file
997
+ // indirection that names the real script's absolute path, which still
998
+ // lives only inside this repo.
999
+ const pointer = JSON.parse(fs.readFileSync(guardPointerPath(homeDir), 'utf8'));
1000
+ expect(pointer.appRoot).toBe(GUARD_APP_ROOT);
1001
+ const shimSource = fs.readFileSync(shimFile, 'utf8');
1002
+ expect(shimSource).toContain('guard-inline-implementation.cjs');
1003
+ expect(shimSource).not.toContain(INLINE_IMPLEMENTATION_GUARD_SCRIPT);
877
1004
  });
1005
+
1006
+ // ─────────────────────────────── ensureGuardsInstalled (self-heal — PRD: guards-auto-install)
1007
+ //
1008
+ // The "Fix It does nothing" incident: installPrdWriteGuard/installDestructiveGitGuard/
1009
+ // installInlineImplementationGuard above were only ever reachable from a human
1010
+ // pressing the New Session card's Fix It button, so every project started red
1011
+ // forever. ensureGuardsInstalled is the self-heal entry point invoked from the
1012
+ // app:delegation-readiness IPC handler BEFORE checkDelegationReadiness runs.
1013
+
1014
+ /** Fabricates a linked git worktree pointing at `main`, without shelling out to git. */
1015
+ async function makeLinkedWorktreeFixture(mk) {
1016
+ const main = await mk('sm-ensure-guards-main-');
1017
+ fs.mkdirSync(path.join(main, '.git'), { recursive: true });
1018
+ const worktree = await mk('sm-ensure-guards-worktree-');
1019
+ const worktreeName = 'sm-epic-testfixture';
1020
+ const worktreeGitFile = path.join(worktree, '.git');
1021
+ const adminDir = path.join(main, '.git', 'worktrees', worktreeName);
1022
+ fs.mkdirSync(adminDir, { recursive: true });
1023
+ // Round-trip back-reference real `git worktree add` writes — required by
1024
+ // worktreeMainRootOf's verification (see opsRootAbsoluteCwd.test.cjs).
1025
+ fs.writeFileSync(path.join(adminDir, 'gitdir'), `${worktreeGitFile}\n`, 'utf8');
1026
+ fs.writeFileSync(worktreeGitFile, `gitdir: ${adminDir}\n`, 'utf8');
1027
+ return { main, worktree };
1028
+ }
1029
+
1030
+ test('ensureGuardsInstalled: installs all four guards into a fresh project with no .claude/settings.json', async () => {
1031
+ const cwd = await mkTmp('sm-ensure-guards-fresh-');
1032
+ const homeDir = await mkTmp('sm-ensure-guards-fresh-home-');
1033
+
1034
+ const result = await ensureGuardsInstalled(cwd, { homeDir });
1035
+ expect(result.ok).toBe(true);
1036
+ expect(result.root).toBe(cwd);
1037
+ expect(result.guards['prd-write-guard'].action).toBe('installed');
1038
+ expect(result.guards['destructive-git-guard'].action).toBe('installed');
1039
+ expect(result.guards['inline-implementation-guard'].action).toBe('installed');
1040
+ expect(result.guards['self-schedule-guard'].action).toBe('installed');
1041
+
1042
+ const readiness = await checkDelegationReadiness({ cwd, homeDir });
1043
+ expect(readiness.checks.find((c) => c.id === 'prd-write-guard').ok).toBe(true);
1044
+ expect(readiness.checks.find((c) => c.id === 'destructive-git-guard').ok).toBe(true);
1045
+ expect(readiness.checks.find((c) => c.id === 'inline-implementation-guard').ok).toBe(true);
1046
+ expect(readiness.checks.find((c) => c.id === 'self-schedule-guard').ok).toBe(true);
1047
+ }, 15_000);
1048
+
1049
+ test('ensureGuardsInstalled: a second call against an already-healthy project performs no write (memoized per cwd)', async () => {
1050
+ const cwd = await mkTmp('sm-ensure-guards-idempotent-');
1051
+ const homeDir = await mkTmp('sm-ensure-guards-idempotent-home-');
1052
+ const settingsPath = path.join(cwd, '.claude', 'settings.json');
1053
+
1054
+ const first = await ensureGuardsInstalled(cwd, { homeDir });
1055
+ expect(first.ok).toBe(true);
1056
+ const before = fs.statSync(settingsPath).mtimeMs;
1057
+
1058
+ const second = await ensureGuardsInstalled(cwd, { homeDir });
1059
+ expect(second).toBe(first); // memoized — same resolved outcome, no re-run
1060
+ const after = fs.statSync(settingsPath).mtimeMs;
1061
+ expect(after).toBe(before);
1062
+ }, 15_000);
1063
+
1064
+ test('ensureGuardsInstalled: malformed settings.json does not throw and leaves the guard row red with its manual fixAction intact', async () => {
1065
+ const cwd = await mkTmp('sm-ensure-guards-malformed-');
1066
+ const homeDir = await mkTmp('sm-ensure-guards-malformed-home-');
1067
+ const settingsPath = path.join(cwd, '.claude', 'settings.json');
1068
+ await fsp.mkdir(path.dirname(settingsPath), { recursive: true });
1069
+ await fsp.writeFile(settingsPath, '{ not valid json', 'utf8');
1070
+
1071
+ const result = await ensureGuardsInstalled(cwd, { homeDir });
1072
+ expect(result.ok).toBe(false);
1073
+ expect(result.guards['prd-write-guard']).toMatchObject({ ok: false, action: 'error' });
1074
+ // never silently discarded the human's unparseable file
1075
+ expect(fs.readFileSync(settingsPath, 'utf8')).toBe('{ not valid json');
1076
+
1077
+ const readiness = await checkDelegationReadiness({ cwd, homeDir });
1078
+ const check = readiness.checks.find((c) => c.id === 'prd-write-guard');
1079
+ expect(check.ok).toBe(false);
1080
+ expect(check.fixAction).toBe('install-prd-write-guard');
1081
+ }, 15_000);
1082
+
1083
+ test('ensureGuardsInstalled: a failed attempt is NOT permanently cached — a later call retries once the underlying problem is fixed', async () => {
1084
+ const cwd = await mkTmp('sm-ensure-guards-retry-');
1085
+ const homeDir = await mkTmp('sm-ensure-guards-retry-home-');
1086
+ const settingsPath = path.join(cwd, '.claude', 'settings.json');
1087
+ await fsp.mkdir(path.dirname(settingsPath), { recursive: true });
1088
+ await fsp.writeFile(settingsPath, '{ not valid json', 'utf8');
1089
+
1090
+ const first = await ensureGuardsInstalled(cwd, { homeDir });
1091
+ expect(first.ok).toBe(false);
1092
+
1093
+ // Fixed by hand, WITHOUT an app restart / cache clear.
1094
+ await fsp.writeFile(settingsPath, '{}', 'utf8');
1095
+
1096
+ const second = await ensureGuardsInstalled(cwd, { homeDir });
1097
+ expect(second.ok).toBe(true);
1098
+ expect(second.guards['prd-write-guard'].action).toBe('installed');
1099
+ }, 15_000);
1100
+
1101
+ test('ensureGuardsInstalled: a nonexistent cwd never throws, is reported ok:false, and never resurrects the deleted folder', async () => {
1102
+ const homeDir = await mkTmp('sm-ensure-guards-missing-home-');
1103
+ const missingCwd = path.join(os.tmpdir(), `sm-ensure-guards-does-not-exist-${Date.now()}`, 'nested', 'project');
1104
+
1105
+ const result = await ensureGuardsInstalled(missingCwd, { homeDir });
1106
+ expect(result.ok).toBe(false);
1107
+ expect(result.error).toMatch(/does not exist on disk/);
1108
+ // Never `mkdir -p`s a deleted project back into existence as a side effect.
1109
+ expect(fs.existsSync(missingCwd)).toBe(false);
1110
+ }, 15_000);
1111
+
1112
+ test('ensureGuardsInstalled: preserves a pre-existing unrelated PreToolUse matcher and hook', async () => {
1113
+ const cwd = await mkTmp('sm-ensure-guards-unrelated-');
1114
+ const homeDir = await mkTmp('sm-ensure-guards-unrelated-home-');
1115
+ const settingsPath = path.join(cwd, '.claude', 'settings.json');
1116
+ const unrelatedMatcher = { matcher: 'SomeOtherTool', hooks: [{ type: 'command', command: 'node /some/unrelated-hook.cjs' }] };
1117
+ await writeJson(settingsPath, { someUnrelatedTopLevelKey: 'keep-me', hooks: { PreToolUse: [unrelatedMatcher] } });
1118
+
1119
+ const result = await ensureGuardsInstalled(cwd, { homeDir });
1120
+ expect(result.ok).toBe(true);
1121
+
1122
+ const written = JSON.parse(fs.readFileSync(settingsPath, 'utf8'));
1123
+ expect(written.someUnrelatedTopLevelKey).toBe('keep-me');
1124
+ expect(written.hooks.PreToolUse).toContainEqual(unrelatedMatcher);
1125
+ }, 15_000);
1126
+
1127
+ test('ensureGuardsInstalled: a worktree cwd installs into the MAIN tree root, never the ephemeral worktree', async () => {
1128
+ const { main, worktree } = await makeLinkedWorktreeFixture(mkTmp);
1129
+ const homeDir = await mkTmp('sm-ensure-guards-worktree-home-');
1130
+
1131
+ const result = await ensureGuardsInstalled(worktree, { homeDir });
1132
+ expect(result.ok).toBe(true);
1133
+ expect(result.root).toBe(main);
1134
+
1135
+ expect(fs.existsSync(path.join(main, '.claude', 'settings.json'))).toBe(true);
1136
+ expect(fs.existsSync(path.join(worktree, '.claude', 'settings.json'))).toBe(false);
1137
+
1138
+ const readiness = await checkDelegationReadiness({ cwd: main, homeDir });
1139
+ expect(readiness.checks.find((c) => c.id === 'prd-write-guard').ok).toBe(true);
1140
+ }, 15_000);
1141
+
1142
+ test('ensureGuardsInstalled: records every auto-install attempt in the ops error log under scope "delegationReadiness"', async () => {
1143
+ const cwd = await mkTmp('sm-ensure-guards-logged-');
1144
+ const homeDir = await mkTmp('sm-ensure-guards-logged-home-');
1145
+
1146
+ await ensureGuardsInstalled(cwd, { homeDir });
1147
+
1148
+ const logFile = opsErrorLogTodayFile(cwd);
1149
+ const lines = fs.readFileSync(logFile, 'utf8').trim().split('\n').map((l) => JSON.parse(l));
1150
+ const delegationLines = lines.filter((l) => l.scope === 'delegationReadiness');
1151
+ expect(delegationLines.map((l) => l.meta?.guard)).toEqual(
1152
+ expect.arrayContaining(['prd-write-guard', 'destructive-git-guard', 'inline-implementation-guard', 'self-schedule-guard']),
1153
+ );
1154
+ expect(delegationLines.every((l) => l.level === 'info')).toBe(true);
1155
+ expect(delegationLines.every((l) => l.meta?.action === 'installed')).toBe(true);
1156
+ }, 15_000);