claude-code-session-manager 0.88.1 → 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 (35) hide show
  1. package/dist/assets/{AgentLibrary-kQF8_wAw.js → AgentLibrary-tuj-j1kQ.js} +1 -1
  2. package/dist/assets/{DataModel-CY_mMnsr.js → DataModel-DOim5SXW.js} +1 -1
  3. package/dist/assets/{History-Sji2brb0.js → History-DylcasUk.js} +1 -1
  4. package/dist/assets/{Hooks-CtqpO-p5.js → Hooks--2xoag9a.js} +1 -1
  5. package/dist/assets/{HostBilko-DN0ydOCr.js → HostBilko-oaw8paAh.js} +1 -1
  6. package/dist/assets/{Library-CoiLZTKh.js → Library-DzKA09Iy.js} +1 -1
  7. package/dist/assets/{ListDetail-CmqiQ_io.js → ListDetail-CEdcO80v.js} +1 -1
  8. package/dist/assets/{MarkdownEditor-HPoNeKhT.js → MarkdownEditor-BMyo0F7s.js} +1 -1
  9. package/dist/assets/{McpServers-Bj1X3_NT.js → McpServers-BB7ayW7W.js} +1 -1
  10. package/dist/assets/{Memory-1PKjsM3b.js → Memory-BOdYvrL4.js} +1 -1
  11. package/dist/assets/{Panel-I1s6ghPo.js → Panel-Yf3SxsRv.js} +1 -1
  12. package/dist/assets/{Permissions-BhiiZjHQ.js → Permissions-BYCpHNy5.js} +1 -1
  13. package/dist/assets/{Plugins-gv3_fkPX.js → Plugins-B0lXiYqV.js} +2 -2
  14. package/dist/assets/{ProvenanceBadge-BGcgBd8V.js → ProvenanceBadge-T7x8LWcV.js} +1 -1
  15. package/dist/assets/{SaveBar-BGr9eZ4g.js → SaveBar-BdVb4DbR.js} +1 -1
  16. package/dist/assets/{Scheduler-Bdo_awPq.js → Scheduler-DlOCWioV.js} +1 -1
  17. package/dist/assets/{ScopeSwitcher-ukl0qWaA.js → ScopeSwitcher-WVhmNsL5.js} +1 -1
  18. package/dist/assets/{Settings-DJkFj22G.js → Settings-Bt4dVsl6.js} +1 -1
  19. package/dist/assets/{SkillReferenceGraph-0t1Qblp6.js → SkillReferenceGraph-R-0YykV8.js} +1 -1
  20. package/dist/assets/{Skills-CeVdBGE-.js → Skills-DXV_LdJP.js} +1 -1
  21. package/dist/assets/{SystemPrompt-CfKvNHi3.js → SystemPrompt-DiQRo_WF.js} +1 -1
  22. package/dist/assets/{TagLibrary-DAzNaUPd.js → TagLibrary-B9OuqU6M.js} +1 -1
  23. package/dist/assets/{TiptapBody--S_rGXoA.js → TiptapBody-HKKmB0AE.js} +1 -1
  24. package/dist/assets/{Toggle-Dszm0xnp.js → Toggle-CBggPonH.js} +1 -1
  25. package/dist/assets/{index-Dp4Rc--q.js → index-BcJqLDiY.js} +3 -3
  26. package/dist/assets/{settingsSchema-BLbssMYo.js → settingsSchema-BYE9DVw_.js} +1 -1
  27. package/dist/index.html +1 -1
  28. package/package.json +1 -1
  29. package/src/main/__tests__/scheduler-integration-failure-stamp.test.cjs +41 -0
  30. package/src/main/__tests__/scheduler-mechanical-recovery.test.cjs +23 -0
  31. package/src/main/build-info.json +4 -4
  32. package/src/main/lib/__tests__/gitWorktree.test.cjs +172 -0
  33. package/src/main/lib/gitWorktree.cjs +195 -3
  34. package/src/main/lib/rcaReport.cjs +8 -1
  35. package/src/main/scheduler.cjs +89 -14
@@ -4797,6 +4797,25 @@ As the LAST LINE of your final result text, emit exactly one of:
4797
4797
  Print PASS only once the commit above has actually landed.`;
4798
4798
  }
4799
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
+
4800
4819
  /**
4801
4820
  * Mechanical recovery (PRD 1130). isFixPlanBeyondDepthCap (below) is the
4802
4821
  * ONLY gate on re-investigating a fix-plan job at investigationDepth >= 2 —
@@ -4809,14 +4828,21 @@ Print PASS only once the commit above has actually landed.`;
4809
4828
  * auto-fix it authors no plan and spawns no model; it is pure git.
4810
4829
  *
4811
4830
  * The closed set of mechanically-resolvable verdicts starts at exactly
4812
- * 'worktree_integration_failed': PRD 1125 already taught integrateBranch to
4813
- * parse git's "would be overwritten by merge" stderr, verify the blocking
4814
- * paths are byte-identical to the branch, discard the proven duplicates, and
4815
- * retry the merge once. A job parked with this verdict has its `sm-job/
4816
- * <slug>` branch preserved (integrateJobBranch never deletes the branch on
4817
- * failure — see cleanupJobWorktree's `keepBranch: !integration.ok`), so a
4818
- * plain re-call of integrateBranch against that same branch inherits PRD
4819
- * 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`).
4820
4846
  *
4821
4847
  * Bounded to exactly one attempt via job.mechanicalRecoveryAttempted,
4822
4848
  * stamped in the SAME mutate as the outcome (performMechanicalRecovery,
@@ -4828,11 +4854,41 @@ Print PASS only once the commit above has actually landed.`;
4828
4854
  */
4829
4855
  const MECHANICALLY_RESOLVABLE_VERDICTS = new Set(['worktree_integration_failed']);
4830
4856
 
4831
- 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) {
4832
4887
  if (process.env.SM_MECHANICAL_RECOVERY_DISABLE === '1') return null;
4833
4888
  if (!job || job.status !== 'needs_review') return null;
4834
4889
  if (!MECHANICALLY_RESOLVABLE_VERDICTS.has(job.verifierVerdict)) return null;
4835
4890
  if (job.mechanicalRecoveryAttempted === true) return null;
4891
+ if (isMechanicalRecoveryFutile(job, currentHeadSha)) return null;
4836
4892
  const cwd = job.cwd || DEFAULT_PROJECT_CWD;
4837
4893
  return { slug: job.slug, cwd, branch: jobWorktree.branchNameFor(job.slug), carriedPaths: job.carriedPaths || [] };
4838
4894
  }
@@ -6044,7 +6100,7 @@ async function spawnInvestigation(failedJob, runDir, { deadChild = null } = {})
6044
6100
  // Mechanical recovery (PRD 1130): same first-refusal treatment — a job
6045
6101
  // eligible for a pure-git retry must never also get a cold-read fix-plan
6046
6102
  // PRD authored in the same pass.
6047
- if (selectMechanicalRecoveryTarget(failedJob)) {
6103
+ if (selectMechanicalRecoveryTarget(failedJob, failedJob.integrationFailureKind === 'content_conflict' ? await gitHead(failedJob.cwd || DEFAULT_PROJECT_CWD) : null)) {
6048
6104
  console.log(`[scheduler] skip investigation: ${failedJob.slug} is mechanical-recovery eligible`);
6049
6105
  return { deferred: false };
6050
6106
  }
@@ -6859,6 +6915,7 @@ async function spawnJob(job, runId, runDir, defaultCwd, resumeTarget = null) {
6859
6915
  let res;
6860
6916
  let worktreeLeftoverDirty = [];
6861
6917
  let worktreeIntegrationFailure = null;
6918
+ let worktreeIntegrationDetail = null;
6862
6919
  // Set only when integrateJobBranch's stderr-parsing auto-resolve fired
6863
6920
  // (PRD 1125) — surfaced on the job row so the Queue UI can say the merge
6864
6921
  // self-healed rather than silently looking like an ordinary merge.
@@ -6919,6 +6976,7 @@ async function spawnJob(job, runId, runDir, defaultCwd, resumeTarget = null) {
6919
6976
  }
6920
6977
  if (!integration.ok) {
6921
6978
  worktreeIntegrationFailure = integration.reason;
6979
+ worktreeIntegrationDetail = integration;
6922
6980
  console.error(`[scheduler] ${job.slug}: worktree branch integration FAILED (${integration.reason}) — branch ${worktree.branch} preserved in ${guardCwd} for manual recovery`);
6923
6981
  } else if (integration.integrated) {
6924
6982
  console.log(`[scheduler] ${job.slug}: worktree branch ${worktree.branch} integrated into ${guardCwd}${integration.mergeCommit ? ' (merge commit)' : ' (fast-forward)'}`);
@@ -7205,7 +7263,9 @@ async function spawnJob(job, runId, runDir, defaultCwd, resumeTarget = null) {
7205
7263
  if (worktreeIntegrationFailure) {
7206
7264
  verifyResult = {
7207
7265
  verdict: 'worktree_integration_failed',
7208
- 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`,
7209
7269
  downgradeTo: 'needs_review',
7210
7270
  };
7211
7271
  }
@@ -7320,6 +7380,9 @@ async function spawnJob(job, runId, runDir, defaultCwd, resumeTarget = null) {
7320
7380
  let mechanicalRecoveryTarget = null;
7321
7381
  let terminalNotifySnapshot = null;
7322
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;
7323
7386
  await mutate((s) => {
7324
7387
  const i2 = s.jobs.findIndex((x) => x.slug === job.slug);
7325
7388
  // A job already moved off 'running' by someone else (namely
@@ -7511,6 +7574,7 @@ async function spawnJob(job, runId, runDir, defaultCwd, resumeTarget = null) {
7511
7574
  } else if (!['blocked_by_foreign_wip_streak', 'budget_exceeded'].includes(s.jobs[i2].verifierVerdict)) {
7512
7575
  delete s.jobs[i2].verifierVerdict;
7513
7576
  }
7577
+ stampIntegrationFailure(s.jobs[i2], worktreeIntegrationFailure ? worktreeIntegrationDetail : null);
7514
7578
  transitionJob(s.jobs[i2], effectiveStatus, { reason: finalizeReason, source: 'spawnJob:finalize' });
7515
7579
  s.jobs[i2].finishedAt = new Date().toISOString();
7516
7580
  s.jobs[i2].exitCode = res.exitCode;
@@ -7620,7 +7684,7 @@ async function spawnJob(job, runId, runDir, defaultCwd, resumeTarget = null) {
7620
7684
  // depth-cap check, so it must never fall through to either.
7621
7685
  // Snapshot only (no I/O inside mutate()); the actual git retry
7622
7686
  // happens outside mutate(), below.
7623
- const mTarget = selectMechanicalRecoveryTarget(s.jobs[i2]);
7687
+ const mTarget = selectMechanicalRecoveryTarget(s.jobs[i2], finalizeHeadSha);
7624
7688
  if (mTarget) {
7625
7689
  mechanicalRecoveryJob = { ...s.jobs[i2] };
7626
7690
  mechanicalRecoveryTarget = mTarget;
@@ -10828,9 +10892,17 @@ async function reverifyNeedsReview() {
10828
10892
  // check in spawnJob missed because the app restarted in between). Depth
10829
10893
  // never disqualifies it, so it runs regardless of investigationDepth.
10830
10894
  {
10895
+ const headFor = await resolveHeadShasForPass(queueForResumeAndAutofix.jobs);
10831
10896
  for (const job of queueForResumeAndAutofix.jobs) {
10832
- const target = selectMechanicalRecoveryTarget(job);
10833
- 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
+ }
10834
10906
  console.log(`[scheduler] mechanical-recovery: needs_review ${job.slug} → re-integrating ${target.branch}`);
10835
10907
  performMechanicalRecovery(job, target).catch((e) => {
10836
10908
  console.error('[scheduler] performMechanicalRecovery error', job.slug, e);
@@ -12598,6 +12670,9 @@ module.exports = {
12598
12670
  buildClaudeSpawnArgs,
12599
12671
  spawnResumeRecovery,
12600
12672
  selectMechanicalRecoveryTarget,
12673
+ isMechanicalRecoveryFutile,
12674
+ resolveHeadShasForPass,
12675
+ stampIntegrationFailure,
12601
12676
  performMechanicalRecovery,
12602
12677
  MECHANICALLY_RESOLVABLE_VERDICTS,
12603
12678
  selectLeftoverQuarantineTarget,