claude-code-session-manager 0.86.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.
- package/dist/assets/AgentLibrary-DyLWzZDf.js +3 -0
- package/dist/assets/{DataModel-Q4jhl24R.js → DataModel--mISIJ6h.js} +1 -1
- package/dist/assets/{History-Cj2FejEo.js → History-C2ahUXTg.js} +2 -2
- package/dist/assets/{Hooks-CaelQI6t.js → Hooks-BiC6oyR2.js} +3 -3
- package/dist/assets/{HostBilko--v7cMR8I.js → HostBilko-BPleEOld.js} +1 -1
- package/dist/assets/{Library-DgI9oCCZ.js → Library-Dc8Qst1R.js} +1 -1
- package/dist/assets/{ListDetail-DYUZN-x-.js → ListDetail-DIXh-OLX.js} +1 -1
- package/dist/assets/MarkdownEditor-C90bkLXK.js +1 -0
- package/dist/assets/{McpServers-ypCYURh3.js → McpServers-DqcbLOLZ.js} +2 -2
- package/dist/assets/{Memory-C2qYp-3M.js → Memory-CW62MXlh.js} +4 -4
- package/dist/assets/{Panel-Cj2kw-Zv.js → Panel-Bw1FhRuF.js} +1 -1
- package/dist/assets/Permissions-BcUC-5y8.js +3 -0
- package/dist/assets/{Plugins-C1Vj8_dU.js → Plugins-BnKx9flD.js} +2 -2
- package/dist/assets/{ProvenanceBadge-DczPNM5U.js → ProvenanceBadge-Bw5vNVPT.js} +1 -1
- package/dist/assets/{SaveBar-Cd_7U6Gb.js → SaveBar-CWr0O_w-.js} +1 -1
- package/dist/assets/Scheduler-DYdLuUqq.js +14 -0
- package/dist/assets/{ScopeSwitcher-DVSyI44-.js → ScopeSwitcher-CrBLbg8s.js} +1 -1
- package/dist/assets/Settings-DluB-vN1.js +3 -0
- package/dist/assets/{SkillReferenceGraph-CUv1_Q2c.js → SkillReferenceGraph-CHLSseay.js} +1 -1
- package/dist/assets/{Skills-C_YHkAy-.js → Skills-gNdo_HNK.js} +2 -2
- package/dist/assets/{SystemPrompt-B8R7T9xn.js → SystemPrompt-Cru05-Ia.js} +1 -1
- package/dist/assets/{TagLibrary-dj9YHWyy.js → TagLibrary-DNHY0xou.js} +1 -1
- package/dist/assets/{TiptapBody-DnSBUjHE.js → TiptapBody-I4lmbCgP.js} +1 -1
- package/dist/assets/{Toggle-CjV_BJn6.js → Toggle-bWMHjmRh.js} +1 -1
- package/dist/assets/{index-CDo9xBR9.css → index-DV3PorRY.css} +1 -1
- package/dist/assets/{index-CXFQIPhO.js → index-fc_JjdxL.js} +724 -724
- package/dist/assets/settingsSchema-BfhtZnGD.js +3 -0
- package/dist/index.html +2 -2
- package/package.json +14 -14
- package/plugins/CLAUDE.md +61 -0
- package/plugins/session-manager-dev/.claude-plugin/plugin.json +1 -1
- package/plugins/session-manager-dev/skills/builder/4-manual/SKILL.md +1 -1
- package/plugins/session-manager-dev/skills/ops-sweep/SKILL.md +1 -1
- package/scripts/scheduler-mcp-server.cjs +7 -0
- package/src/main/__tests__/agentModelResolve.test.cjs +100 -9
- package/src/main/__tests__/broadcastCoalescer.test.cjs +18 -0
- package/src/main/__tests__/epicMint.test.cjs +2 -2
- package/src/main/__tests__/health-delegation-chain.test.cjs +2 -1
- package/src/main/__tests__/needsReviewLedger.test.cjs +162 -0
- package/src/main/__tests__/opsErrorLogTelemetryTap.test.cjs +3 -3
- package/src/main/__tests__/pollLoop-dispatch-on-failure.test.cjs +15 -1
- package/src/main/__tests__/prdCreateDisposition.test.cjs +201 -0
- package/src/main/__tests__/prdFrontmatterDisposition.test.cjs +125 -0
- package/src/main/__tests__/prdLocations.test.cjs +100 -2
- package/src/main/__tests__/prdLocationsArchived.test.cjs +43 -1
- package/src/main/__tests__/prdSetDisposition.test.cjs +222 -0
- package/src/main/__tests__/queue-health-verdict.test.cjs +170 -0
- package/src/main/__tests__/queue-starvation-per-project.test.cjs +14 -2
- package/src/main/__tests__/queueHistory.test.cjs +63 -0
- package/src/main/__tests__/reconcileTiming.test.cjs +135 -0
- package/src/main/__tests__/scheduleJobTransitions.test.cjs +101 -1
- package/src/main/__tests__/scheduler-boot-orphans.test.cjs +2 -2
- package/src/main/__tests__/scheduler-broadcast-reconcile.test.cjs +121 -0
- package/src/main/__tests__/scheduler-cross-project-batch.test.cjs +43 -0
- package/src/main/__tests__/scheduler-guard-verdict-autoresolve.test.cjs +344 -0
- package/src/main/__tests__/scheduler-job-budget.test.cjs +172 -0
- package/src/main/__tests__/scheduler-looks-done.test.cjs +93 -3
- package/src/main/__tests__/scheduler-porcelain-rename.test.cjs +164 -0
- package/src/main/__tests__/scheduler-reap-dead-running-jobs.test.cjs +160 -0
- package/src/main/__tests__/scheduler-reaper-helpers-basics.test.cjs +87 -0
- package/src/main/__tests__/scheduler-shared-tree-guard.test.cjs +88 -0
- package/src/main/__tests__/scheduler-starve-escalation.test.cjs +14 -4
- package/src/main/__tests__/scheduler-worktree-cap-defer.test.cjs +14 -0
- package/src/main/chatRunner.cjs +8 -5
- package/src/main/health.cjs +1 -1
- package/src/main/historyAggregator.cjs +5 -0
- package/src/main/index.cjs +95 -44
- package/src/main/ipcSchemas.cjs +47 -0
- package/src/main/lib/__tests__/active-sessions.test.cjs +251 -0
- package/src/main/lib/__tests__/bootSelfHeal.test.cjs +107 -0
- package/src/main/lib/__tests__/delegationReadiness.test.cjs +322 -43
- package/src/main/lib/__tests__/effectiveModelInfo.test.cjs +239 -0
- package/src/main/lib/__tests__/gitWorktree.test.cjs +89 -0
- package/src/main/lib/__tests__/guardShims.test.cjs +151 -0
- package/src/main/lib/__tests__/opsRootAbsoluteCwd.test.cjs +5 -5
- package/src/main/lib/__tests__/prdDisposition.test.cjs +224 -0
- package/src/main/lib/__tests__/reaperHelpers.test.cjs +179 -1
- package/src/main/lib/__tests__/usageCircuit.test.cjs +224 -0
- package/src/main/lib/__tests__/watchdog-helpers.test.cjs +312 -0
- package/src/main/lib/__tests__/watchdog-relaunch.test.cjs +193 -0
- package/{scripts → src/main}/lib/activeSessions.cjs +50 -4
- package/src/main/lib/agentModelResolve.cjs +65 -27
- package/src/main/lib/bootSelfHeal.cjs +88 -0
- package/src/main/lib/delegationReadiness.cjs +290 -225
- package/src/main/lib/effectiveModelInfo.cjs +333 -0
- package/src/main/lib/ephemeralCwd.cjs +1 -1
- package/src/main/lib/epicMint.cjs +3 -3
- package/src/main/lib/gitWorktree.cjs +42 -12
- package/src/main/lib/guardShims.cjs +156 -0
- package/src/main/lib/jobDirtFilter.cjs +7 -2
- package/src/main/lib/launchFailure.cjs +2 -1
- package/src/main/lib/mcpToolCatalog.cjs +4 -1
- package/src/main/lib/needsReviewLedger.cjs +205 -0
- package/src/main/lib/opsErrorLog.cjs +1 -1
- package/src/main/lib/opsOwnership.cjs +1 -1
- package/src/main/lib/prdCreate.cjs +56 -1
- package/src/main/lib/prdDisposition.cjs +199 -0
- package/src/main/lib/prdFrontmatter.cjs +8 -2
- package/src/main/lib/prdLocations.cjs +167 -45
- package/src/main/lib/projectHomeAdminRoutes.cjs +4 -4
- package/src/main/lib/projectPageSummarySchema.cjs +1 -1
- package/src/main/lib/projectRootResolve.cjs +1 -1
- package/src/main/lib/queueHistory.cjs +19 -1
- package/src/main/lib/queueStore.cjs +6 -1
- package/src/main/lib/reaperHelpers.cjs +181 -15
- package/src/main/lib/scheduleJobSchema.cjs +8 -0
- package/src/main/lib/scheduleJobTransitions.cjs +33 -0
- package/src/main/lib/schedulerConfig.cjs +24 -0
- package/src/main/lib/usageCircuit.cjs +159 -0
- package/{scripts → src/main}/lib/watchdogHelpers.cjs +1 -1
- package/src/main/scheduler/prdParser.cjs +13 -0
- package/src/main/scheduler.cjs +1285 -143
- package/src/main/templates/PRD_AUTHORING.md +50 -0
- package/src/main/templates/project-pages-catalog.json +1 -1
- package/src/main/usage.cjs +21 -3
- package/src/preload/api.d.ts +92 -1
- package/src/preload/index.cjs +10 -0
- package/web/README.md +41 -0
- package/{scripts/render-project-pages.cjs → web/project-pages/render.cjs} +4 -4
- package/{scripts/render-project-pages → web/project-pages/renderer}/dist/renderer.cjs +1 -1
- package/{scripts/validate-project-pages-summary.cjs → web/project-pages/validate-summary.cjs} +5 -5
- package/dist/assets/AgentLibrary-DTFL7y8G.js +0 -3
- package/dist/assets/MarkdownEditor-DCIubYWf.js +0 -1
- package/dist/assets/Permissions-BiYZNGYW.js +0 -3
- package/dist/assets/Scheduler-DcLBiJBq.js +0 -14
- package/dist/assets/Settings-Cv-pRyms.js +0 -3
- package/dist/assets/settingsSchema-BJVciriw.js +0 -3
- /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
|
|
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(
|
|
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
|
|
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
|
-
|
|
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:
|
|
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
|
|
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
|
-
|
|
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:
|
|
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
|
|
509
|
-
|
|
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 ${
|
|
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
|
-
|
|
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:
|
|
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 ${
|
|
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
|
|
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
|
-
|
|
874
|
-
expect(
|
|
875
|
-
expect(
|
|
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);
|