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
@@ -168,6 +168,38 @@ test('isMachineReportDue: true when lastMachineReportAt is more than 30 days bef
168
168
  expect(telemetrySettings.isMachineReportDue(cfg, { now, appVersion: '0.81.0' })).toBe(true);
169
169
  });
170
170
 
171
+ test('save() merges a partial patch onto a fresh disk read — concurrent savers do not clobber each other', async () => {
172
+ const home = await mkHome();
173
+ const telemetrySettings = await freshModule(home);
174
+ const base = await telemetrySettings.load(); // mints + persists installId
175
+ expect(base.installId).not.toBe('');
176
+
177
+ // Two independent writers, each stamping a DIFFERENT field, racing each
178
+ // other — mirrors telemetryBoot's install-report stamp landing around the
179
+ // same time as telemetryClient's own daily-flush stamp. Neither holds the
180
+ // other's snapshot; each passes only the field it intends to change.
181
+ await Promise.all([
182
+ telemetrySettings.save({ lastMachineReportAt: '2026-01-01T00:00:00.000Z', lastMachineReportVersion: '2.0.0' }),
183
+ telemetrySettings.save({ lastDailyFlushAt: '2026-01-01T01:00:00.000Z' }),
184
+ ]);
185
+
186
+ const onDisk = await telemetrySettings.load();
187
+ expect(onDisk.lastMachineReportAt).toBe('2026-01-01T00:00:00.000Z');
188
+ expect(onDisk.lastMachineReportVersion).toBe('2.0.0');
189
+ expect(onDisk.lastDailyFlushAt).toBe('2026-01-01T01:00:00.000Z');
190
+ // Neither save touched installId/enabled/endpoint — they must survive too.
191
+ expect(onDisk.installId).toBe(base.installId);
192
+ expect(onDisk.enabled).toBe(base.enabled);
193
+ expect(onDisk.endpoint).toBe(base.endpoint);
194
+ });
195
+
196
+ test('save() rejects an unknown key without touching disk', async () => {
197
+ const home = await mkHome();
198
+ const telemetrySettings = await freshModule(home);
199
+ await telemetrySettings.load();
200
+ await expect(telemetrySettings.save({ notARealField: 'x' })).rejects.toThrow('Invalid telemetry config');
201
+ });
202
+
171
203
  test('isMachineReportDue: false when same version and reported less than 30 days ago', async () => {
172
204
  const home = await mkHome();
173
205
  const telemetrySettings = await freshModule(home);
@@ -530,11 +530,14 @@ function executeRun({ tabId, sessionId, prompt, cwd, resume, silent, onSilentRes
530
530
  const fullPrompt = STOP_SIGNAL_INSTRUCTION + CHAT_MODE_TRUTH_INSTRUCTION + prompt;
531
531
 
532
532
  // sessionId is the Epic's claudeSessionId for an Epic-backed tab (see
533
- // comment above childEnv) — resolve that Epic's agentType persona model
534
- // so Chat and Terminal views of the same Epic launch with the same
535
- // model. Never throws; falls back to the hardcoded 'sonnet' literal when
536
- // there's no Epic, no agentType, or the persona has no model/'inherit' —
537
- // --model must never be left unpinned (CLAUDE.md model-pinning rule).
533
+ // comment above childEnv) — resolve that Epic's agentType persona model,
534
+ // project-overlay-then-global (agentModelResolve.cjs's
535
+ // readOverlayAwarePersonaModel). Terminal (EpicTerminalPane.tsx) calls
536
+ // this SAME function via the agents:resolve-epic-model IPC, so the two
537
+ // views of one Epic agree by construction, not by convention. Never
538
+ // throws; falls back to the hardcoded 'sonnet' literal when there's no
539
+ // Epic, no agentType, or the persona has no model/'inherit' — --model
540
+ // must never be left unpinned (CLAUDE.md model-pinning rule).
538
541
  const model = agentModelResolve.resolveEpicModel({ cwd, claudeSessionId: sessionId });
539
542
 
540
543
  // Build argv as an array — no shell: true, no string interpolation
@@ -16,9 +16,11 @@ const { checkDelegationReadiness } = require('./lib/delegationReadiness.cjs');
16
16
  const { resolvePrdsDirs } = require('./lib/prdLocations.cjs');
17
17
  const { migratePrds } = require('./lib/prdMigration.cjs');
18
18
  const queueStore = require('./lib/queueStore.cjs');
19
- const { computeStallSummary, SCHEDULER_STATE_PATH, FAILURE_STREAK_WARN_THRESHOLD, classifyQueueStarvation } = require('./scheduler.cjs');
19
+ const { computeStallSummary, SCHEDULER_STATE_PATH, FAILURE_STREAK_WARN_THRESHOLD, classifyQueueStarvation, STARVE_ESCALATION_MS } = require('./scheduler.cjs');
20
+ const { findStarvedProjects } = require('./lib/schedulerBatch.cjs');
21
+ const { AUDIT_LOG_PATH } = require('./lib/auditLog.cjs');
20
22
  const { DEFAULT_RUNS_DIR, computeReport, isRetentionEnabled, liveKeysFromJobs } = require('./lib/runLogRetention.cjs');
21
- const { allProjectCwds } = require('../../scripts/lib/activeSessions.cjs');
23
+ const { allProjectCwds } = require('./lib/activeSessions.cjs');
22
24
 
23
25
  const MAX_LOG_AGE_MS = 5 * 60_000; // 5 min — warn if no logs this old
24
26
  const PROJECT_ROOT = path.resolve(__dirname, '../..');
@@ -458,6 +460,63 @@ function evaluateQueueDispatchHealth(queueState, runningCount, now, thresholdMs
458
460
  };
459
461
  }
460
462
 
463
+ /**
464
+ * latestStarveEscalationReasons(auditLogPath) → { [cwd]: holdReason }
465
+ *
466
+ * health.cjs runs as its own cold process (`npm run health`), so it has no
467
+ * access to the live scheduler's in-memory `lastTick` — the durable
468
+ * audit-log.jsonl trail (auditLog.cjs) is the only place the hold reason a
469
+ * 'project_starve_escalated' event already recorded (scheduler.cjs's
470
+ * runStarveEscalationSweep) survives to be read from here. Reads the SAME
471
+ * recorded value rather than re-deriving it; the last record per cwd wins.
472
+ * Missing/unreadable log → {} (no reasons known yet), never a throw.
473
+ */
474
+ function latestStarveEscalationReasons(auditLogPath) {
475
+ let lines;
476
+ try {
477
+ lines = fs.readFileSync(auditLogPath, 'utf8').split('\n').filter(Boolean);
478
+ } catch {
479
+ return {};
480
+ }
481
+ const byCwd = {};
482
+ for (const line of lines) {
483
+ let rec;
484
+ try {
485
+ rec = JSON.parse(line);
486
+ } catch {
487
+ continue;
488
+ }
489
+ if (rec?.kind === 'project_starve_escalated' && rec.cwd) byCwd[rec.cwd] = rec.holdReason ?? 'unknown';
490
+ }
491
+ return byCwd;
492
+ }
493
+
494
+ /**
495
+ * evaluateStarveEscalationHealth(jobs, now, thresholdMs, escalationReasons)
496
+ * → { ok, projects?, message? }
497
+ *
498
+ * Pure over its inputs. Reuses findStarvedProjects — the exact per-cwd
499
+ * STARVED verdict the live escalation (scheduler.cjs) acts on — so health
500
+ * reports the identical set of starved-past-threshold projects, never a
501
+ * re-derived one. `escalationReasons` supplies the hold reason per cwd (see
502
+ * latestStarveEscalationReasons); a cwd with no recorded reason yet reports
503
+ * 'unknown' rather than failing.
504
+ */
505
+ function evaluateStarveEscalationHealth(jobs, now, thresholdMs, escalationReasons = {}) {
506
+ const starved = findStarvedProjects(jobs, now, thresholdMs);
507
+ if (starved.length === 0) return { ok: true };
508
+ const projects = starved.map((sp) => ({
509
+ cwd: sp.cwd,
510
+ pendingCount: sp.pendingCount,
511
+ ageMs: sp.ageMs,
512
+ holdReason: escalationReasons[sp.cwd] ?? 'unknown',
513
+ }));
514
+ const message = `Project(s) starved past the ${Math.round(thresholdMs / 60_000)}m escalation threshold: ${
515
+ projects.map((p) => `${p.cwd} (${Math.round(p.ageMs / 60_000)}m, ${p.pendingCount} pending, hold=${p.holdReason})`).join('; ')
516
+ }`;
517
+ return { ok: false, projects, message };
518
+ }
519
+
461
520
  function loadUsagePollerState(statePath) {
462
521
  let raw;
463
522
  try {
@@ -640,6 +699,18 @@ async function check() {
640
699
  status.issues.push(`Queue dispatch: ${status.components.queue_dispatch.message}`);
641
700
  }
642
701
 
702
+ // Per-project starve escalation (bounded consequence for project_starved
703
+ // — see runStarveEscalationSweep in scheduler.cjs). Distinct from
704
+ // queue_dispatch above: that's machine-wide dispatch liveness, this is
705
+ // "has any ONE project been starved past the LATER escalation threshold",
706
+ // the exact condition the 2026-09-12 19h Bilko starve went unreported by.
707
+ status.components.project_starve_escalation = evaluateStarveEscalationHealth(
708
+ queueState.jobs, now, STARVE_ESCALATION_MS, latestStarveEscalationReasons(AUDIT_LOG_PATH),
709
+ );
710
+ if (!status.components.project_starve_escalation.ok) {
711
+ status.issues.push(status.components.project_starve_escalation.message);
712
+ }
713
+
643
714
  // Worktree cap blocking every dispatchable pending job with nothing
644
715
  // running (see evaluateWorktreeCapBlocked's header) — a distinct
645
716
  // condition from generic tick/dispatch staleness above, since a leaked
@@ -877,7 +948,7 @@ async function check() {
877
948
  // Critical: nodejs, config dir, typescript, build artifact, test infrastructure.
878
949
  // Non-fatal: scheduler/transcripts dirs may not exist on fresh install.
879
950
  // Informational: app log age (shows if app is running, but not blocking).
880
- const criticalComponents = ['nodejs', 'config_dir', 'typescript', 'build_artifact', 'test_infrastructure', 'scheduler_queue', 'queue_dispatch', 'usage_poller', 'prd_migration', 'claude_md_budget', 'delegation_chain'];
951
+ const criticalComponents = ['nodejs', 'config_dir', 'typescript', 'build_artifact', 'test_infrastructure', 'scheduler_queue', 'queue_dispatch', 'project_starve_escalation', 'usage_poller', 'prd_migration', 'claude_md_budget', 'delegation_chain'];
881
952
  status.ok = criticalComponents.every((c) => status.components[c]?.ok !== false);
882
953
 
883
954
  status.elapsedMs = Date.now() - start;
@@ -909,6 +980,8 @@ module.exports = {
909
980
  evaluateUsagePollerHealth,
910
981
  loadUsagePollerState,
911
982
  evaluateQueueDispatchHealth,
983
+ evaluateStarveEscalationHealth,
984
+ latestStarveEscalationReasons,
912
985
  DISPATCH_STALL_THRESHOLD_MS,
913
986
  TICK_STALL_THRESHOLD_MS,
914
987
  HEARTBEAT_STALE_MS,
@@ -21,10 +21,14 @@ const CACHE_MAX = 50_000;
21
21
  // tokens are priced far below input because they're served from
22
22
  // Anthropic's prompt cache, not re-processed — this is what makes the
23
23
  // cache-savings figure in the dashboard real money, not a vanity stat.
24
+ // fable: Claude Fable 5.1 (claude-fable-5-1) — $10/$50 per MTok input/output,
25
+ // cache reads at $0.25/MTok (the `claude-api` skill's model reference and its
26
+ // Claude Fable 5.1 migration notes, both cached 2026-06-24).
24
27
  const MODEL_PRICING = {
25
28
  opus: { i: 15, o: 75, c: 1.5 },
26
29
  sonnet: { i: 3, o: 15, c: 0.3 },
27
30
  haiku: { i: 0.8, o: 4, c: 0.08 },
31
+ fable: { i: 10, o: 50, c: 0.25 },
28
32
  };
29
33
  const DEFAULT_PRICING_KEY = 'sonnet'; // fallback for unrecognized model ids
30
34
 
@@ -40,6 +44,7 @@ function resolvePricingKey(modelId) {
40
44
  if (id.includes('opus')) return { key: 'opus', estimated: false };
41
45
  if (id.includes('sonnet')) return { key: 'sonnet', estimated: false };
42
46
  if (id.includes('haiku')) return { key: 'haiku', estimated: false };
47
+ if (id.includes('fable')) return { key: 'fable', estimated: false };
43
48
  return { key: DEFAULT_PRICING_KEY, estimated: true };
44
49
  }
45
50
 
@@ -37,7 +37,11 @@ const epicWorktreeMint = require('./lib/epicWorktreeMint.cjs');
37
37
  const epicWorktreeMerge = require('./lib/epicWorktreeMerge.cjs');
38
38
  const epicWorktreeProjectConfig = require('./lib/epicWorktreeProjectConfig.cjs');
39
39
  const agentLibrary = require('./agentLibrary.cjs');
40
- const { checkDelegationReadiness, installPrdWriteGuard, installDestructiveGitGuard, installInlineImplementationGuard } = require('./lib/delegationReadiness.cjs');
40
+ const agentModelResolve = require('./lib/agentModelResolve.cjs');
41
+ const { resolveEffectiveModelInfo } = require('./lib/effectiveModelInfo.cjs');
42
+ const { checkDelegationReadiness, ensureGuardsInstalled, installPrdWriteGuard, installDestructiveGitGuard, installInlineImplementationGuard } = require('./lib/delegationReadiness.cjs');
43
+ const { resolveProjectContext } = require('./lib/projectRootResolve.cjs');
44
+ const { writeGuardShims } = require('./lib/guardShims.cjs');
41
45
  const { MCP_TOOL_CATALOG, MCP_RECIPES } = require('./lib/mcpToolCatalog.cjs');
42
46
  const { sendIfAlive } = require('./lib/sendToRenderer.cjs');
43
47
  const { resolveBuildTarget } = require('./lib/buildTarget.cjs');
@@ -70,7 +74,7 @@ const telemetryConsent = require('./lib/telemetryConsent.cjs');
70
74
  const { registerHistoryAggregatorHandlers, finalizeClosedDays, refreshIntradayToday } = require('./historyAggregator.cjs');
71
75
  const runLogRetention = require('./lib/runLogRetention.cjs');
72
76
  const { registerHistoryDashboardHandlers } = require('./historyDashboard.cjs');
73
- const { tryAcquireLock, releaseLock, DEFAULT_LOCK_PATH } = require('../../scripts/lib/watchdogHelpers.cjs');
77
+ const { tryAcquireLock, releaseLock, DEFAULT_LOCK_PATH } = require('./lib/watchdogHelpers.cjs');
74
78
  const schedulerConfig = require('./lib/schedulerConfig.cjs');
75
79
  const memoryTool = require('./memoryTool.cjs');
76
80
  const { registerMemoryAggregateIpc } = require('./memoryAggregate.cjs');
@@ -90,7 +94,8 @@ const { checkInsideHome, assertInsideHome } = require('./lib/insideHome.cjs');
90
94
  const { openInEditor, openFileInEditor, openInFinder, openInTerminal } = require('./lib/openExternalApp.cjs');
91
95
  const { rebuildActiveIndex } = require('./lib/activeIndexRebuild.cjs');
92
96
  const { activeIndexPath: promptSessionsActiveIndexPath } = require('./lib/epicMint.cjs');
93
- const { allProjectCwds } = require('../../scripts/lib/activeSessions.cjs');
97
+ const { allProjectCwds } = require('./lib/activeSessions.cjs');
98
+ const { scheduleBootSelfHeal } = require('./lib/bootSelfHeal.cjs');
94
99
 
95
100
  let mainWindow = null;
96
101
  let rebooting = false;
@@ -520,12 +525,78 @@ ipcMain.handle('agents:remove-override', async (_e, payload) => {
520
525
  ipcMain.handle('agents:get-persona-body', validated(schemas.agentsGetPersonaBody, (payload) =>
521
526
  agentLibrary.getPersonaBody(payload)));
522
527
 
528
+ // "What will this Epic's launch actually use for --model?" — the SAME
529
+ // resolver chatRunner.cjs's Chat-view launch calls in-process
530
+ // (agentModelResolve.cjs's resolveEpicModel), reached here so Terminal's
531
+ // launch (EpicTerminalPane.tsx, a renderer module with no fs access) agrees
532
+ // with Chat by calling the identical function rather than a second one that
533
+ // is merely supposed to. Never throws (resolveEpicModel's own contract).
534
+ ipcMain.handle('agents:resolve-epic-model', validated(schemas.agentsResolveEpicModel, (payload) =>
535
+ agentModelResolve.resolveEpicModel(payload)));
536
+
537
+ // "What will this Epic actually run as?" (PRD: effective-model-resolver) —
538
+ // persona alias + evidence-based concrete model id + provenance, for a given
539
+ // (cwd, agentType). Read-only observer; never affects what a launch resolves.
540
+ ipcMain.handle('agents:resolve-model-info', validated(schemas.agentsResolveModelInfo, (payload) =>
541
+ resolveEffectiveModelInfo(payload)));
542
+
523
543
  // "Can this project actually delegate?" probe (PRD: delegation-readiness).
524
544
  // Structured data over the preconditions for scheduler_create_prd being in an
525
545
  // agent's tool list at all — surfacing it in the UI is a sibling PRD.
526
- ipcMain.handle('app:delegation-readiness', validated(schemas.delegationReadinessCwd, async (payload) =>
527
- checkDelegationReadiness(payload)
528
- ));
546
+ //
547
+ // Self-heals BEFORE computing the checks (PRD: guards-auto-install) — Fix It
548
+ // used to be the only call site for the three guard installers, so every
549
+ // project the human opened started red and demanded three manual clicks.
550
+ // ensureGuardsInstalled never rejects on its own, but it is still awaited
551
+ // inside a try/catch here so a future change to it can never turn this into
552
+ // a rejected IPC handler.
553
+ //
554
+ // `cwd` is normalized to its real project root via the SAME resolver
555
+ // prdAdminRoutes.cjs's session_manager_help readiness route already uses
556
+ // (projectRootResolve.cjs's resolveProjectContext) before either the install
557
+ // or the read-back check runs, so a worktree/ops-internal cwd can't install
558
+ // into one path while the banner reads another.
559
+ ipcMain.handle('app:delegation-readiness', validated(schemas.delegationReadinessCwd, async (payload) => {
560
+ const resolved = resolveProjectContext({ cwd: payload.cwd });
561
+ const cwd = resolved.cwd || payload.cwd;
562
+ try {
563
+ await ensureGuardsInstalled(cwd);
564
+ } catch { /* auto-install must never block the readiness probe itself */ }
565
+ return checkDelegationReadiness({ ...payload, cwd });
566
+ }));
567
+
568
+ // Every guard-install attempt is logged from MAIN, not the renderer — a
569
+ // press that never reaches this process (e.g. a renderer talking to an
570
+ // older main with no handler for the channel, Electron's own "No handler
571
+ // registered for ..." rejection) leaves no line here, which is exactly what
572
+ // makes it distinguishable from a press that reached main and failed. Uses
573
+ // opsErrorLog.appendError (the sole writer of the `logs` ops namespace) so
574
+ // the record lands in the project's own
575
+ // session-manager-operations/logs/errors-<date>.jsonl, same shape as every
576
+ // other structured error line.
577
+ function logGuardInstallAttempt(guard, cwd, outcome) {
578
+ const opsErrorLog = require('./lib/opsErrorLog.cjs');
579
+ opsErrorLog.appendError({
580
+ cwd,
581
+ scope: 'delegationReadinessGuardInstall',
582
+ level: outcome.ok ? 'info' : 'error',
583
+ message: outcome.ok
584
+ ? `${guard} install ${outcome.action}`
585
+ : `${guard} install failed: ${outcome.error || 'unknown error'}`,
586
+ meta: { guard, ...outcome },
587
+ });
588
+ }
589
+
590
+ async function handleGuardInstall(guard, installFn, payload) {
591
+ try {
592
+ const result = await installFn(payload);
593
+ logGuardInstallAttempt(guard, payload.cwd, result);
594
+ return result;
595
+ } catch (err) {
596
+ logGuardInstallAttempt(guard, payload.cwd, { ok: false, action: 'error', error: err?.message ?? String(err) });
597
+ throw err;
598
+ }
599
+ }
529
600
 
530
601
  // The three readiness checks with a sanctioned one-press install. Session
531
602
  // Manager standardizes on the REFERENCE approach (absolute path to this
@@ -536,13 +607,13 @@ ipcMain.handle('app:delegation-readiness', validated(schemas.delegationReadiness
536
607
  // header. inline-implementation-guard shares prd-write-guard's matcher, so
537
608
  // its installer appends into that SAME matcher's `hooks` array instead.
538
609
  ipcMain.handle('app:install-prd-write-guard', validated(schemas.delegationReadinessCwd, (payload) =>
539
- installPrdWriteGuard(payload)
610
+ handleGuardInstall('prd-write-guard', installPrdWriteGuard, payload)
540
611
  ));
541
612
  ipcMain.handle('app:install-destructive-git-guard', validated(schemas.delegationReadinessCwd, (payload) =>
542
- installDestructiveGitGuard(payload)
613
+ handleGuardInstall('destructive-git-guard', installDestructiveGitGuard, payload)
543
614
  ));
544
615
  ipcMain.handle('app:install-inline-implementation-guard', validated(schemas.delegationReadinessCwd, (payload) =>
545
- installInlineImplementationGuard(payload)
616
+ handleGuardInstall('inline-implementation-guard', installInlineImplementationGuard, payload)
546
617
  ));
547
618
 
548
619
  ipcMain.handle('app:engage-rules-path', () => process.env.SESSION_MANAGER_ENGAGE_RULES || null);
@@ -1274,42 +1345,22 @@ app.whenReady().then(async () => {
1274
1345
  seedSchedulerMcp({ logger: console, writeLog: logs.writeLine }).catch((e) => {
1275
1346
  logs.writeLine({ scope: 'seed-scheduler-mcp', level: 'error', message: 'seed failed', meta: { error: e?.message } });
1276
1347
  });
1348
+ // Rewrite the stable guard-hook shims (~/.claude/session-manager/hooks/) to
1349
+ // point at THIS running app's root, on every boot. An already-installed
1350
+ // project hook resolves through the shim at invocation time, so an app
1351
+ // upgrade (a new npx hash dir) just follows along with no re-install — see
1352
+ // guardShims.cjs's header.
1353
+ writeGuardShims().catch((e) => {
1354
+ logs.writeLine({ scope: 'guard-shims', level: 'error', message: 'writeGuardShims failed', meta: { error: e?.message } });
1355
+ });
1277
1356
  // Epic index self-heal: a project whose active-index.json is MISSING or
1278
1357
  // fails to parse has lost every open Epic's status unless the per-Epic
1279
- // status mirror (epicStatusMirror.cjs) can rebuild it. Runs once per boot,
1280
- // fire-and-forget, and only touches a project whose index reads unclean —
1281
- // a clean index is never rewritten here. See activeIndexRebuild.cjs.
1282
- try {
1283
- for (const projectCwd of allProjectCwds()) {
1284
- const indexPath = promptSessionsActiveIndexPath(projectCwd);
1285
- let unclean = false;
1286
- let parseError = null;
1287
- if (!fs.existsSync(indexPath)) {
1288
- unclean = true;
1289
- } else {
1290
- try {
1291
- JSON.parse(fs.readFileSync(indexPath, 'utf8'));
1292
- } catch (e) {
1293
- unclean = true;
1294
- parseError = e.message;
1295
- }
1296
- }
1297
- if (!unclean) continue;
1298
- try {
1299
- const { rows, skipped } = rebuildActiveIndex(projectCwd);
1300
- logs.writeLine({
1301
- scope: 'active-index-rebuild',
1302
- level: 'info',
1303
- message: 'rebuilt active-index.json from status mirrors',
1304
- meta: { cwd: projectCwd, parseError, restored: rows.length, skipped: skipped.length },
1305
- });
1306
- } catch (e) {
1307
- logs.writeLine({ scope: 'active-index-rebuild', level: 'error', message: 'rebuild failed', meta: { cwd: projectCwd, error: e?.message } });
1308
- }
1309
- }
1310
- } catch (e) {
1311
- logs.writeLine({ scope: 'active-index-rebuild', level: 'error', message: 'boot sweep failed', meta: { error: e?.message } });
1312
- }
1358
+ // status mirror (epicStatusMirror.cjs) can rebuild it. Runs once per boot
1359
+ // and only touches a project whose index reads unclean — a clean index is
1360
+ // never rewritten. Deferred until the window paints (did-finish-load) so
1361
+ // the ~270ms allProjectCwds() scan doesn't compete with the renderer's
1362
+ // first IPC round trips for the event loop — see bootSelfHeal.cjs.
1363
+ scheduleBootSelfHeal(mainWindow, { fs, allProjectCwds, promptSessionsActiveIndexPath, rebuildActiveIndex, logs });
1313
1364
  // History rollup finalize pass: deferred 30s past boot so it never competes
1314
1365
  // with first-paint, fire-and-forget (cron/offline refresh is PRD 651 — this
1315
1366
  // is just the one-shot in-app pass).
@@ -1340,7 +1391,7 @@ app.whenReady().then(async () => {
1340
1391
  }, 30_000);
1341
1392
  // Keep TODAY's rollup line current so the History dashboard never needs to
1342
1393
  // fall back to a live transcript scan. Shares the same O_EXCL lock file as
1343
- // the external watchdog's finalize pass (scripts/lib/watchdogHelpers.cjs)
1394
+ // the external watchdog's finalize pass (src/main/lib/watchdogHelpers.cjs)
1344
1395
  // so the two never interleave writes to the rollup; a contended lock just
1345
1396
  // means this tick's refresh is skipped (harmless — the next tick retries).
1346
1397
  const intradayTimer = setInterval(runIntradayRefresh, schedulerConfig.HISTORY_INTRADAY_REFRESH_MS);
@@ -320,6 +320,11 @@ const PrdAgentTypeSchema = z.string().regex(PERSONA_NAME_RE, 'agent name must be
320
320
  // call site since update also allows an explicit empty array to CLEAR the
321
321
  // dependency, which create has no reason to accept.
322
322
  const DepSlugSchema = z.string().min(1).max(160).regex(/^[A-Za-z0-9][\w.-]*$/);
323
+ // Wave-authoring decision (scheduler wave-disposition PRD), shared between
324
+ // schedulerCreatePrd (authoring time) and adminPrdFrontmatterPatch (the
325
+ // after-the-fact change path) so both can never disagree on what a valid
326
+ // disposition value is.
327
+ const PrdDispositionSchema = z.enum(['append', 'new-head']);
323
328
  const schedulerCreatePrd = z.object({
324
329
  title: z.string().min(1).max(200).regex(NO_NEWLINE_RE, 'must not contain newlines'),
325
330
  // Optional (PRD: worktree-cwd Epic-lookup hazard) — when omitted, prdCreate.cjs's
@@ -340,6 +345,12 @@ const schedulerCreatePrd = z.object({
340
345
  // Explicit ordering (PRD 832): slugs that must complete before this PRD
341
346
  // becomes eligible. Written to frontmatter as `dependsOn: [a, b]`.
342
347
  dependsOn: z.array(DepSlugSchema).max(20).optional(),
348
+ // Explicit wave-authoring decision (scheduler wave-disposition PRD) — only
349
+ // meaningful (and only stamped) when this PRD joins an Epic that already
350
+ // has incomplete PRDs and lands as a wave root (no dependsOn of its own);
351
+ // createPrd() requires it in that situation for an interactive caller and
352
+ // defaults to 'append' (logged as a default) for a non-interactive one.
353
+ disposition: PrdDispositionSchema.optional(),
343
354
  // An EXISTING Epic's promptSessionId (PRD 748) — NOT a PromptTicket.id.
344
355
  // ensureEpic() (epicMint.cjs) joins the Epic whose active-index.json
345
356
  // sessions key literally equals this value; the renderer only ever sends
@@ -387,6 +398,19 @@ const schedulerCreatePrd = z.object({
387
398
  quietMachine: z.boolean().optional(),
388
399
  });
389
400
 
401
+ // Renderer-facing counterpart to adminPrdFrontmatterPatch's dependsOn/
402
+ // disposition patch (scheduler wave-disposition PRD) — backs the Scheduler
403
+ // UI's "change disposition" action. `dependsOn` is the caller's already-
404
+ // computed target (from the SAME backlog tree the UI renders — see
405
+ // lib/backlogTree.ts); the main-process handler only validates the rewrite
406
+ // is safe (prdDisposition.cjs), never re-derives "the" terminal itself.
407
+ const scheduleSetPrdDisposition = z.object({
408
+ slug: z.string().regex(SCHEDULE_SLUG_RE),
409
+ cwd: z.string().min(1).max(4096).optional(),
410
+ disposition: PrdDispositionSchema,
411
+ dependsOn: z.array(DepSlugSchema).max(20).optional(),
412
+ });
413
+
390
414
  // Bulk archive: slug list, capped to limit unbounded retag/archive payloads.
391
415
  // Optional cwd narrows every slug's directory search to one project (same
392
416
  // role as scheduler_get_prd/scheduler_update_prd's cwd hint) — omitted, the
@@ -469,6 +493,11 @@ const adminPrdFrontmatterPatch = z.object({
469
493
  // (depSlugResolve.cjs) scheduler_create_prd uses, so update and create
470
494
  // can never disagree about what a dependsOn entry resolves to.
471
495
  dependsOn: z.array(DepSlugSchema).max(20).optional(),
496
+ // Patchable after creation too — the Scheduler UI's "change disposition"
497
+ // action (promote a wave to its own head, or attach it behind another
498
+ // chain) rewrites this alongside dependsOn via the same
499
+ // schedule:set-prd-disposition path (see prdDisposition.cjs).
500
+ disposition: PrdDispositionSchema.optional(),
472
501
  });
473
502
 
474
503
  const adminUpdatePrd = z.object({
@@ -623,6 +652,21 @@ const agentsGetPersonaBody = z.object({
623
652
  name: z.string().min(1).max(200),
624
653
  }).strict();
625
654
 
655
+ // ──────────────────────────────────────────── Effective model info (PRD: what-will-this-run-as)
656
+ const agentsResolveModelInfo = z.object({
657
+ cwd: z.string().min(1).max(4096),
658
+ agentType: z.string().min(1).max(200),
659
+ }).strict();
660
+
661
+ // ──────────────────────────────────────────── Epic launch model (PRD: unify-epic-model-resolution)
662
+ // Terminal's (EpicTerminalPane.tsx) call into agentModelResolve.cjs's
663
+ // resolveEpicModel — the same launch-model authority chatRunner.cjs's Chat
664
+ // path calls in-process, reached here over IPC since the renderer has no fs.
665
+ const agentsResolveEpicModel = z.object({
666
+ cwd: z.string().min(1).max(4096),
667
+ claudeSessionId: z.string().min(1).max(200),
668
+ }).strict();
669
+
626
670
  // ──────────────────────────────────────────── Project Pages (PRD 932)
627
671
  // Same validation split as projectBriefCwd above: real path validation is
628
672
  // config.cjs's validatePath at first fs access in projectPages.cjs.
@@ -1037,6 +1081,7 @@ module.exports = {
1037
1081
  scheduleReadLog,
1038
1082
  scheduleWritePrd,
1039
1083
  schedulerCreatePrd,
1084
+ scheduleSetPrdDisposition,
1040
1085
  scheduleArchivePrd,
1041
1086
  scheduleRetagPrd,
1042
1087
  adminListPrdsQuery,
@@ -1108,6 +1153,8 @@ module.exports = {
1108
1153
  exchangesList,
1109
1154
  agentsSavePersona,
1110
1155
  agentsGetPersonaBody,
1156
+ agentsResolveModelInfo,
1157
+ agentsResolveEpicModel,
1111
1158
  delegationReadinessCwd,
1112
1159
  telemetrySetConfig,
1113
1160
  },