claude-code-session-manager 0.88.0 → 0.89.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 (38) hide show
  1. package/dist/assets/{AgentLibrary-BZ6IY2g1.js → AgentLibrary-tuj-j1kQ.js} +1 -1
  2. package/dist/assets/{DataModel-dAoGAODX.js → DataModel-DOim5SXW.js} +1 -1
  3. package/dist/assets/{History-CF157HyX.js → History-DylcasUk.js} +1 -1
  4. package/dist/assets/{Hooks-CCPPipBk.js → Hooks--2xoag9a.js} +1 -1
  5. package/dist/assets/{HostBilko-Dlmz2m06.js → HostBilko-oaw8paAh.js} +1 -1
  6. package/dist/assets/{Library-BaXcboWX.js → Library-DzKA09Iy.js} +1 -1
  7. package/dist/assets/{ListDetail-CoUCzG5S.js → ListDetail-CEdcO80v.js} +1 -1
  8. package/dist/assets/{MarkdownEditor-DTg3JlOT.js → MarkdownEditor-BMyo0F7s.js} +1 -1
  9. package/dist/assets/{McpServers-DdjY1sA3.js → McpServers-BB7ayW7W.js} +1 -1
  10. package/dist/assets/{Memory-BJ3Jlw0C.js → Memory-BOdYvrL4.js} +1 -1
  11. package/dist/assets/{Panel-ygRSgc41.js → Panel-Yf3SxsRv.js} +1 -1
  12. package/dist/assets/{Permissions-m0KAnccF.js → Permissions-BYCpHNy5.js} +1 -1
  13. package/dist/assets/{Plugins-CJRl6B3Q.js → Plugins-B0lXiYqV.js} +2 -2
  14. package/dist/assets/{ProvenanceBadge-w8cnlVvW.js → ProvenanceBadge-T7x8LWcV.js} +1 -1
  15. package/dist/assets/{SaveBar-BpyPcUIk.js → SaveBar-BdVb4DbR.js} +1 -1
  16. package/dist/assets/{Scheduler-D2ppxSfy.js → Scheduler-DlOCWioV.js} +1 -1
  17. package/dist/assets/{ScopeSwitcher-EyuIwt-j.js → ScopeSwitcher-WVhmNsL5.js} +1 -1
  18. package/dist/assets/{Settings-BHSGGFEc.js → Settings-Bt4dVsl6.js} +1 -1
  19. package/dist/assets/{SkillReferenceGraph-BNaZ2JCF.js → SkillReferenceGraph-R-0YykV8.js} +1 -1
  20. package/dist/assets/{Skills-Dl3q75QV.js → Skills-DXV_LdJP.js} +1 -1
  21. package/dist/assets/{SystemPrompt-Bg6F-Cyp.js → SystemPrompt-DiQRo_WF.js} +1 -1
  22. package/dist/assets/{TagLibrary-BSutflCy.js → TagLibrary-B9OuqU6M.js} +1 -1
  23. package/dist/assets/{TiptapBody-CXih4xrc.js → TiptapBody-HKKmB0AE.js} +1 -1
  24. package/dist/assets/{Toggle-2qtUHhA9.js → Toggle-CBggPonH.js} +1 -1
  25. package/dist/assets/{index-B3emy2oI.js → index-BcJqLDiY.js} +3 -3
  26. package/dist/assets/{settingsSchema-CdzxaqwM.js → settingsSchema-BYE9DVw_.js} +1 -1
  27. package/dist/index.html +1 -1
  28. package/package.json +1 -1
  29. package/src/main/__tests__/health-tick-liveness.test.cjs +16 -0
  30. package/src/main/__tests__/scheduler-integration-failure-stamp.test.cjs +41 -0
  31. package/src/main/__tests__/scheduler-mechanical-recovery.test.cjs +23 -0
  32. package/src/main/__tests__/scheduler-stall-per-project.test.cjs +14 -0
  33. package/src/main/build-info.json +4 -4
  34. package/src/main/health.cjs +9 -1
  35. package/src/main/lib/__tests__/gitWorktree.test.cjs +172 -0
  36. package/src/main/lib/gitWorktree.cjs +195 -3
  37. package/src/main/lib/rcaReport.cjs +8 -1
  38. package/src/main/scheduler.cjs +98 -16
@@ -1865,7 +1865,12 @@ function computeStallSummary(state) {
1865
1865
  byProject[key].invalid = (byProject[key].invalid || 0) + 1;
1866
1866
  }
1867
1867
  const total = jobs.length + invalidJobs.length;
1868
- const stalled = total > 0 && running === 0 && pending === 0 && !state?.paused;
1868
+ // A drained queue (every row completed/skipped) is idle, not stalled — a
1869
+ // stall needs at least one parked problem row (failed/needs_review/
1870
+ // quarantined/invalid) that is waiting on someone.
1871
+ const isProblemStatus = (st) => st !== 'completed' && st !== 'skipped';
1872
+ const stalled = total > 0 && running === 0 && pending === 0 && !state?.paused
1873
+ && (invalidJobs.length > 0 || jobs.some((j) => isProblemStatus(j.status)));
1869
1874
  for (const key of Object.keys(byProject)) {
1870
1875
  const counts = byProject[key];
1871
1876
  const projRunning = counts.running || 0;
@@ -1873,7 +1878,9 @@ function computeStallSummary(state) {
1873
1878
  const projTotal = Object.keys(counts)
1874
1879
  .filter((k) => k !== 'stalled')
1875
1880
  .reduce((sum, k) => sum + counts[k], 0);
1876
- counts.stalled = projTotal > 0 && projRunning === 0 && projPending === 0 && !state?.paused;
1881
+ const projProblem = Object.keys(counts)
1882
+ .some((k) => k !== 'stalled' && k !== 'completed' && k !== 'skipped' && counts[k] > 0);
1883
+ counts.stalled = projTotal > 0 && projRunning === 0 && projPending === 0 && !state?.paused && projProblem;
1877
1884
  }
1878
1885
  return { stalled, total, running, pending, byProject };
1879
1886
  }
@@ -4790,6 +4797,25 @@ As the LAST LINE of your final result text, emit exactly one of:
4790
4797
  Print PASS only once the commit above has actually landed.`;
4791
4798
  }
4792
4799
 
4800
+ /**
4801
+ * Stamps integrateBranch's failure subtype (`failureKind`) and conflicted
4802
+ * paths onto a job row at finalize; clears both when there is no failure so
4803
+ * a re-run never carries a stale subtype. Plain JSON values only.
4804
+ */
4805
+ function stampIntegrationFailure(row, integration) {
4806
+ if (integration && integration.failureKind) {
4807
+ row.integrationFailureKind = integration.failureKind;
4808
+ if (Array.isArray(integration.conflictedPaths)) row.integrationConflictPaths = integration.conflictedPaths;
4809
+ else delete row.integrationConflictPaths;
4810
+ if (integration.baseHeadSha) row.integrationBaseHeadSha = integration.baseHeadSha;
4811
+ else delete row.integrationBaseHeadSha;
4812
+ } else {
4813
+ delete row.integrationFailureKind;
4814
+ delete row.integrationConflictPaths;
4815
+ delete row.integrationBaseHeadSha;
4816
+ }
4817
+ }
4818
+
4793
4819
  /**
4794
4820
  * Mechanical recovery (PRD 1130). isFixPlanBeyondDepthCap (below) is the
4795
4821
  * ONLY gate on re-investigating a fix-plan job at investigationDepth >= 2 —
@@ -4802,14 +4828,21 @@ Print PASS only once the commit above has actually landed.`;
4802
4828
  * auto-fix it authors no plan and spawns no model; it is pure git.
4803
4829
  *
4804
4830
  * The closed set of mechanically-resolvable verdicts starts at exactly
4805
- * 'worktree_integration_failed': PRD 1125 already taught integrateBranch to
4806
- * parse git's "would be overwritten by merge" stderr, verify the blocking
4807
- * paths are byte-identical to the branch, discard the proven duplicates, and
4808
- * retry the merge once. A job parked with this verdict has its `sm-job/
4809
- * <slug>` branch preserved (integrateJobBranch never deletes the branch on
4810
- * failure — see cleanupJobWorktree's `keepBranch: !integration.ok`), so a
4811
- * plain re-call of integrateBranch against that same branch inherits PRD
4812
- * 1125's auto-resolution for free — no re-implementation needed here.
4831
+ * 'worktree_integration_failed', which covers two subtypes (job.
4832
+ * integrationFailureKind): `blocking_paths` — PRD 1125 taught integrateBranch
4833
+ * to parse git's "would be overwritten by merge" stderr, verify the blocking
4834
+ * paths are byte-identical to the branch, discard the proven duplicates and
4835
+ * retry once, so a re-call can genuinely succeed once the dirty files are
4836
+ * gone — and `content_conflict`, which PRD 1125 explicitly does NOT handle
4837
+ * (parseBlockingMergePaths returns null for it). Only pure-addition text
4838
+ * conflicts are auto-resolved (mc-02, gitWorktree.cjs, kill switch
4839
+ * SM_PURE_ADDITION_MERGE_DISABLE=1) — and that already ran inside the
4840
+ * original failed integration. Re-calling integrateBranch for a content
4841
+ * conflict that survived it, against an unmoved base HEAD, fails
4842
+ * deterministically, so the selector skips it (WITHOUT spending the one
4843
+ * attempt) until HEAD moves — a sibling merge may make the retry succeed.
4844
+ * A job parked with this verdict has its `sm-job/<slug>` branch preserved
4845
+ * (cleanupJobWorktree's `keepBranch: !integration.ok`).
4813
4846
  *
4814
4847
  * Bounded to exactly one attempt via job.mechanicalRecoveryAttempted,
4815
4848
  * stamped in the SAME mutate as the outcome (performMechanicalRecovery,
@@ -4821,11 +4854,41 @@ Print PASS only once the commit above has actually landed.`;
4821
4854
  */
4822
4855
  const MECHANICALLY_RESOLVABLE_VERDICTS = new Set(['worktree_integration_failed']);
4823
4856
 
4824
- function selectMechanicalRecoveryTarget(job) {
4857
+ /**
4858
+ * True when a retry is provably futile: a content_conflict whose base HEAD is
4859
+ * the same as at failure time. Unknown state (no stamp, null current HEAD)
4860
+ * is NOT futile — falls back to attempting the retry.
4861
+ */
4862
+ function isMechanicalRecoveryFutile(job, currentHeadSha) {
4863
+ return job.integrationFailureKind === 'content_conflict'
4864
+ && !!job.integrationBaseHeadSha
4865
+ && !!currentHeadSha
4866
+ && job.integrationBaseHeadSha === currentHeadSha;
4867
+ }
4868
+
4869
+ /**
4870
+ * Resolves the current HEAD once per distinct cwd for a pass. Returns a
4871
+ * `(job) => sha|null` lookup over `jobs`; the memo lives only as long as the
4872
+ * returned closure. O(distinct cwds) git calls, not O(jobs).
4873
+ */
4874
+ async function resolveHeadShasForPass(jobs) {
4875
+ const byCwd = new Map();
4876
+ for (const j of jobs || []) {
4877
+ if (!j || j.status !== 'needs_review' || j.integrationFailureKind !== 'content_conflict') continue;
4878
+ const cwd = j.cwd || DEFAULT_PROJECT_CWD;
4879
+ if (!byCwd.has(cwd)) byCwd.set(cwd, gitHead(cwd));
4880
+ }
4881
+ const resolved = new Map();
4882
+ for (const [cwd, p] of byCwd) resolved.set(cwd, await p);
4883
+ return (j) => (j && resolved.get(j.cwd || DEFAULT_PROJECT_CWD)) || null;
4884
+ }
4885
+
4886
+ function selectMechanicalRecoveryTarget(job, currentHeadSha = null) {
4825
4887
  if (process.env.SM_MECHANICAL_RECOVERY_DISABLE === '1') return null;
4826
4888
  if (!job || job.status !== 'needs_review') return null;
4827
4889
  if (!MECHANICALLY_RESOLVABLE_VERDICTS.has(job.verifierVerdict)) return null;
4828
4890
  if (job.mechanicalRecoveryAttempted === true) return null;
4891
+ if (isMechanicalRecoveryFutile(job, currentHeadSha)) return null;
4829
4892
  const cwd = job.cwd || DEFAULT_PROJECT_CWD;
4830
4893
  return { slug: job.slug, cwd, branch: jobWorktree.branchNameFor(job.slug), carriedPaths: job.carriedPaths || [] };
4831
4894
  }
@@ -6037,7 +6100,7 @@ async function spawnInvestigation(failedJob, runDir, { deadChild = null } = {})
6037
6100
  // Mechanical recovery (PRD 1130): same first-refusal treatment — a job
6038
6101
  // eligible for a pure-git retry must never also get a cold-read fix-plan
6039
6102
  // PRD authored in the same pass.
6040
- if (selectMechanicalRecoveryTarget(failedJob)) {
6103
+ if (selectMechanicalRecoveryTarget(failedJob, failedJob.integrationFailureKind === 'content_conflict' ? await gitHead(failedJob.cwd || DEFAULT_PROJECT_CWD) : null)) {
6041
6104
  console.log(`[scheduler] skip investigation: ${failedJob.slug} is mechanical-recovery eligible`);
6042
6105
  return { deferred: false };
6043
6106
  }
@@ -6852,6 +6915,7 @@ async function spawnJob(job, runId, runDir, defaultCwd, resumeTarget = null) {
6852
6915
  let res;
6853
6916
  let worktreeLeftoverDirty = [];
6854
6917
  let worktreeIntegrationFailure = null;
6918
+ let worktreeIntegrationDetail = null;
6855
6919
  // Set only when integrateJobBranch's stderr-parsing auto-resolve fired
6856
6920
  // (PRD 1125) — surfaced on the job row so the Queue UI can say the merge
6857
6921
  // self-healed rather than silently looking like an ordinary merge.
@@ -6912,6 +6976,7 @@ async function spawnJob(job, runId, runDir, defaultCwd, resumeTarget = null) {
6912
6976
  }
6913
6977
  if (!integration.ok) {
6914
6978
  worktreeIntegrationFailure = integration.reason;
6979
+ worktreeIntegrationDetail = integration;
6915
6980
  console.error(`[scheduler] ${job.slug}: worktree branch integration FAILED (${integration.reason}) — branch ${worktree.branch} preserved in ${guardCwd} for manual recovery`);
6916
6981
  } else if (integration.integrated) {
6917
6982
  console.log(`[scheduler] ${job.slug}: worktree branch ${worktree.branch} integrated into ${guardCwd}${integration.mergeCommit ? ' (merge commit)' : ' (fast-forward)'}`);
@@ -7198,7 +7263,9 @@ async function spawnJob(job, runId, runDir, defaultCwd, resumeTarget = null) {
7198
7263
  if (worktreeIntegrationFailure) {
7199
7264
  verifyResult = {
7200
7265
  verdict: 'worktree_integration_failed',
7201
- reason: `worktree branch integration failed: ${worktreeIntegrationFailure} — branch preserved for manual merge`,
7266
+ reason: worktreeIntegrationDetail && worktreeIntegrationDetail.failureKind === 'content_conflict'
7267
+ ? `Integration blocked by a content conflict in ${(worktreeIntegrationDetail.conflictedPaths || []).join(', ') || 'unknown paths'} — branch ${jobWorktree.branchNameFor(job.slug)} preserved; needs a manual merge.`
7268
+ : `worktree branch integration failed: ${worktreeIntegrationFailure} — branch preserved for manual merge`,
7202
7269
  downgradeTo: 'needs_review',
7203
7270
  };
7204
7271
  }
@@ -7313,6 +7380,9 @@ async function spawnJob(job, runId, runDir, defaultCwd, resumeTarget = null) {
7313
7380
  let mechanicalRecoveryTarget = null;
7314
7381
  let terminalNotifySnapshot = null;
7315
7382
  const newlyCompletedPrds = [];
7383
+ // Base HEAD for the mechanical-recovery futility check — read here, never inside mutate() (no I/O).
7384
+ const finalizeHeadSha = worktreeIntegrationDetail && worktreeIntegrationDetail.failureKind === 'content_conflict'
7385
+ ? await gitHead(guardCwd) : null;
7316
7386
  await mutate((s) => {
7317
7387
  const i2 = s.jobs.findIndex((x) => x.slug === job.slug);
7318
7388
  // A job already moved off 'running' by someone else (namely
@@ -7504,6 +7574,7 @@ async function spawnJob(job, runId, runDir, defaultCwd, resumeTarget = null) {
7504
7574
  } else if (!['blocked_by_foreign_wip_streak', 'budget_exceeded'].includes(s.jobs[i2].verifierVerdict)) {
7505
7575
  delete s.jobs[i2].verifierVerdict;
7506
7576
  }
7577
+ stampIntegrationFailure(s.jobs[i2], worktreeIntegrationFailure ? worktreeIntegrationDetail : null);
7507
7578
  transitionJob(s.jobs[i2], effectiveStatus, { reason: finalizeReason, source: 'spawnJob:finalize' });
7508
7579
  s.jobs[i2].finishedAt = new Date().toISOString();
7509
7580
  s.jobs[i2].exitCode = res.exitCode;
@@ -7613,7 +7684,7 @@ async function spawnJob(job, runId, runDir, defaultCwd, resumeTarget = null) {
7613
7684
  // depth-cap check, so it must never fall through to either.
7614
7685
  // Snapshot only (no I/O inside mutate()); the actual git retry
7615
7686
  // happens outside mutate(), below.
7616
- const mTarget = selectMechanicalRecoveryTarget(s.jobs[i2]);
7687
+ const mTarget = selectMechanicalRecoveryTarget(s.jobs[i2], finalizeHeadSha);
7617
7688
  if (mTarget) {
7618
7689
  mechanicalRecoveryJob = { ...s.jobs[i2] };
7619
7690
  mechanicalRecoveryTarget = mTarget;
@@ -10821,9 +10892,17 @@ async function reverifyNeedsReview() {
10821
10892
  // check in spawnJob missed because the app restarted in between). Depth
10822
10893
  // never disqualifies it, so it runs regardless of investigationDepth.
10823
10894
  {
10895
+ const headFor = await resolveHeadShasForPass(queueForResumeAndAutofix.jobs);
10824
10896
  for (const job of queueForResumeAndAutofix.jobs) {
10825
- const target = selectMechanicalRecoveryTarget(job);
10826
- if (!target) continue;
10897
+ const head = headFor(job);
10898
+ const target = selectMechanicalRecoveryTarget(job, head);
10899
+ if (!target) {
10900
+ if (job.status === 'needs_review' && job.mechanicalRecoveryAttempted !== true
10901
+ && MECHANICALLY_RESOLVABLE_VERDICTS.has(job.verifierVerdict) && isMechanicalRecoveryFutile(job, head)) {
10902
+ console.log(`[scheduler] mechanical-recovery: skip ${job.slug} — content conflict, base HEAD unchanged since failure`);
10903
+ }
10904
+ continue;
10905
+ }
10827
10906
  console.log(`[scheduler] mechanical-recovery: needs_review ${job.slug} → re-integrating ${target.branch}`);
10828
10907
  performMechanicalRecovery(job, target).catch((e) => {
10829
10908
  console.error('[scheduler] performMechanicalRecovery error', job.slug, e);
@@ -12591,6 +12670,9 @@ module.exports = {
12591
12670
  buildClaudeSpawnArgs,
12592
12671
  spawnResumeRecovery,
12593
12672
  selectMechanicalRecoveryTarget,
12673
+ isMechanicalRecoveryFutile,
12674
+ resolveHeadShasForPass,
12675
+ stampIntegrationFailure,
12594
12676
  performMechanicalRecovery,
12595
12677
  MECHANICALLY_RESOLVABLE_VERDICTS,
12596
12678
  selectLeftoverQuarantineTarget,