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.
- package/dist/assets/AgentLibrary-DyLWzZDf.js +3 -0
- package/dist/assets/{DataModel-DRH-Ty20.js → DataModel--mISIJ6h.js} +1 -1
- package/dist/assets/{History-CfRhT1Im.js → History-C2ahUXTg.js} +2 -2
- package/dist/assets/{Hooks-wEmh_U6c.js → Hooks-BiC6oyR2.js} +3 -3
- package/dist/assets/{HostBilko-D_t7Rbi7.js → HostBilko-BPleEOld.js} +1 -1
- package/dist/assets/{Library-CpArQ-OJ.js → Library-Dc8Qst1R.js} +1 -1
- package/dist/assets/{ListDetail-pjaKYs84.js → ListDetail-DIXh-OLX.js} +1 -1
- package/dist/assets/MarkdownEditor-C90bkLXK.js +1 -0
- package/dist/assets/{McpServers-ftqaV3kn.js → McpServers-DqcbLOLZ.js} +2 -2
- package/dist/assets/{Memory-ChMWkNd0.js → Memory-CW62MXlh.js} +4 -4
- package/dist/assets/{Panel-D9Kr40Ai.js → Panel-Bw1FhRuF.js} +1 -1
- package/dist/assets/Permissions-BcUC-5y8.js +3 -0
- package/dist/assets/{Plugins-BtChISho.js → Plugins-BnKx9flD.js} +2 -2
- package/dist/assets/{ProvenanceBadge-DBA5EcYy.js → ProvenanceBadge-Bw5vNVPT.js} +1 -1
- package/dist/assets/{SaveBar-I0_dWNTX.js → SaveBar-CWr0O_w-.js} +1 -1
- package/dist/assets/Scheduler-DYdLuUqq.js +14 -0
- package/dist/assets/{ScopeSwitcher-5GTEveb2.js → ScopeSwitcher-CrBLbg8s.js} +1 -1
- package/dist/assets/Settings-DluB-vN1.js +3 -0
- package/dist/assets/{SkillReferenceGraph-DNBFGrYE.js → SkillReferenceGraph-CHLSseay.js} +1 -1
- package/dist/assets/{Skills-DJB6-bBM.js → Skills-gNdo_HNK.js} +2 -2
- package/dist/assets/{SystemPrompt-BiDDrJUA.js → SystemPrompt-Cru05-Ia.js} +1 -1
- package/dist/assets/{TagLibrary-_Wrevtop.js → TagLibrary-DNHY0xou.js} +1 -1
- package/dist/assets/{TiptapBody-OWWXdLRy.js → TiptapBody-I4lmbCgP.js} +1 -1
- package/dist/assets/{Toggle-B122N0HL.js → Toggle-bWMHjmRh.js} +1 -1
- package/dist/assets/{index-CDo9xBR9.css → index-DV3PorRY.css} +1 -1
- package/dist/assets/{index-DhvuQL4C.js → index-fc_JjdxL.js} +724 -724
- package/dist/assets/settingsSchema-BfhtZnGD.js +3 -0
- package/dist/index.html +2 -2
- package/package.json +15 -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__/health-starve-escalation.test.cjs +94 -0
- package/src/main/__tests__/loadGateDetailTick.test.cjs +31 -0
- package/src/main/__tests__/machineProfile.test.cjs +19 -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__/pty-session-open-telemetry.test.cjs +96 -0
- package/src/main/__tests__/queue-health-verdict.test.cjs +170 -0
- package/src/main/__tests__/queue-starvation-per-project.test.cjs +147 -0
- 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-failed-autoreset.test.cjs +121 -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-needs-review-autoresolve.test.cjs +189 -0
- package/src/main/__tests__/scheduler-no-dead-end-status.test.cjs +152 -0
- package/src/main/__tests__/scheduler-porcelain-rename.test.cjs +164 -0
- package/src/main/__tests__/scheduler-quarantine-autoresolve.test.cjs +165 -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 +154 -0
- package/src/main/__tests__/scheduler-worktree-cap-defer.test.cjs +14 -0
- package/src/main/__tests__/telemetryClient.test.cjs +75 -5
- package/src/main/__tests__/telemetrySettings.test.cjs +32 -0
- package/src/main/chatRunner.cjs +8 -5
- package/src/main/health.cjs +76 -3
- 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__/loadGate.test.cjs +103 -2
- 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__/telemetryBoot.test.cjs +11 -0
- 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/loadGate.cjs +23 -1
- package/src/main/lib/machineProfile.cjs +15 -0
- 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/schedulerBatch.cjs +12 -1
- package/src/main/lib/schedulerConfig.cjs +37 -0
- package/src/main/lib/telemetryBoot.cjs +11 -8
- package/src/main/lib/telemetryClient.cjs +44 -2
- package/src/main/lib/telemetrySettings.cjs +20 -3
- package/src/main/lib/usageCircuit.cjs +159 -0
- package/{scripts → src/main}/lib/watchdogHelpers.cjs +1 -1
- package/src/main/pty.cjs +9 -0
- package/src/main/scheduler/prdParser.cjs +13 -0
- package/src/main/scheduler.cjs +1869 -193
- 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-Bkv-HcP1.js +0 -3
- package/dist/assets/MarkdownEditor-Xc141kjj.js +0 -1
- package/dist/assets/Permissions-DKoNVgzj.js +0 -3
- package/dist/assets/Scheduler-CbES7MC8.js +0 -14
- package/dist/assets/Settings-BX3FElXk.js +0 -3
- package/dist/assets/settingsSchema-sGoCTd7J.js +0 -3
- /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
|
|
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)
|
|
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
|
|
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(
|
|
448
|
+
if (typeof h?.command !== 'string' || !h.command.includes(def.guardName)) continue;
|
|
383
449
|
mentioned = true;
|
|
384
|
-
const raw = extractGuardScriptPath(h.command,
|
|
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))
|
|
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 =
|
|
395
|
-
else if (
|
|
396
|
-
else detail =
|
|
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:
|
|
400
|
-
label:
|
|
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
|
|
406
|
-
|
|
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
|
|
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
|
-
*
|
|
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
|
|
430
|
-
*
|
|
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
|
|
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(
|
|
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: `${
|
|
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 ${
|
|
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 (
|
|
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(
|
|
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 ===
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
645
|
-
|
|
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
|
-
|
|
674
|
-
|
|
675
|
-
|
|
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
|
-
|
|
686
|
-
|
|
687
|
-
|
|
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
|
-
|
|
696
|
-
|
|
614
|
+
function checkSelfScheduleGuard({ cwd, homeDir = os.homedir() } = {}) {
|
|
615
|
+
return checkGuard(GUARD_DEFS.selfSchedule, { cwd, homeDir });
|
|
616
|
+
}
|
|
697
617
|
|
|
698
|
-
|
|
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
|
};
|