claude-code-session-manager 0.35.9 → 0.35.10

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/index.html CHANGED
@@ -7,7 +7,7 @@
7
7
  <link rel="preconnect" href="https://fonts.googleapis.com">
8
8
  <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
9
9
  <link href="https://fonts.googleapis.com/css2?family=Newsreader:ital,opsz,wght@0,6..72,400;0,6..72,500;0,6..72,600;0,6..72,700;1,6..72,400&family=Geist:wght@300;400;500;600;700&family=IBM+Plex+Mono:wght@400;500;600&display=swap" rel="stylesheet">
10
- <script type="module" crossorigin src="./assets/index-B4PkQh71.js"></script>
10
+ <script type="module" crossorigin src="./assets/index-B1hI9l9p.js"></script>
11
11
  <link rel="modulepreload" crossorigin href="./assets/monaco-editor-BW5C4Iv1.js">
12
12
  <link rel="stylesheet" crossorigin href="./assets/monaco-editor-BTnBOi8r.css">
13
13
  <link rel="stylesheet" crossorigin href="./assets/index-C7NyYuXu.css">
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-code-session-manager",
3
- "version": "0.35.9",
3
+ "version": "0.35.10",
4
4
  "description": "Local cockpit for the Claude Code CLI — multi-tab terminal, full config surface, scheduler, voice dictation, and live observability.",
5
5
  "type": "module",
6
6
  "main": "src/main/index.cjs",
@@ -903,6 +903,25 @@ test('pass_no_commit: SCHEDULER_VERDICT: PASS but no commit landed → needs_rev
903
903
  }
904
904
  });
905
905
 
906
+ test('pass_no_commit: fix-plan job (slug ^\\d+-fix-) with PASS but no commit is exempt → clean', async () => {
907
+ const tmp = makeTmpDir();
908
+ try {
909
+ const slug = '523-fix-bounded-fix-plan-retry';
910
+ writeLog(tmp, slug, noOpRunEvents('Verification complete — no code gap exists.\nSCHEDULER_VERDICT: PASS'));
911
+ const prdPath = writePrd(tmp, slug, '# Fix: bounded fix-plan retry');
912
+ const verdict = await verifyRun({
913
+ runDir: tmp,
914
+ prdPath,
915
+ queueEntry: { slug, status: 'running' },
916
+ allJobs: [],
917
+ committedDuringRun: false,
918
+ });
919
+ assert.equal(verdict.verdict, 'clean', `expected clean (fix-plan exemption), got ${verdict.verdict}: ${verdict.reason}`);
920
+ } finally {
921
+ rmdir(tmp);
922
+ }
923
+ });
924
+
906
925
  // (e) halt + sentinel PASS → still halt (override must not apply to halt)
907
926
  test('halt result + sentinel PASS + committedDuringRun:true → still halt', async () => {
908
927
  const tmp = makeTmpDir();
@@ -8,7 +8,10 @@
8
8
 
9
9
  const { test } = require('node:test');
10
10
  const assert = require('node:assert/strict');
11
- const { selectAutoFixTargets, isUnresolvableNeedsReview, isRescanCandidate } = require('../scheduler.cjs');
11
+ const {
12
+ selectAutoFixTargets, isUnresolvableNeedsReview, isRescanCandidate,
13
+ healTargetForFix, isFixPlanBeyondDepthCap, MAX_INVESTIGATION_DEPTH,
14
+ } = require('../scheduler.cjs');
12
15
 
13
16
  const noSiblingOnDisk = () => false;
14
17
  const noRunDir = () => null;
@@ -150,4 +153,64 @@ test('isRescanCandidate: needs_review + runId + uncommitted_changes → false (g
150
153
  assert.strictEqual(isRescanCandidate(job), false);
151
154
  });
152
155
 
156
+ test('isRescanCandidate: needs_review + runId + pass_no_commit → true (fix-plan exemption can flip the verdict on rescan)', () => {
157
+ const job = makeJob({ verifierVerdict: 'pass_no_commit' });
158
+ assert.strictEqual(isRescanCandidate(job), true);
159
+ });
160
+
161
+ // ---- investigationDepth bound (fix-plan-of-a-fix-plan recursion cap) ----
162
+
163
+ test('depth-1 (ordinary, no investigationDepth field) needs_review job is eligible', () => {
164
+ const jobs = [makeJob({ slug: '05-my-feature' })];
165
+ const result = selectAutoFixTargets(jobs, { fixSlugExists: noSiblingOnDisk });
166
+ assert.strictEqual(result.length, 1);
167
+ });
168
+
169
+ test('fix-plan job with investigationDepth: 2 is now ALSO eligible', () => {
170
+ const jobs = [makeJob({ slug: '05-fix-foo', investigationDepth: 2 })];
171
+ const result = selectAutoFixTargets(jobs, { fixSlugExists: noSiblingOnDisk });
172
+ assert.strictEqual(result.length, 1);
173
+ });
174
+
175
+ test('fix-plan job with investigationDepth: 3 is NOT eligible (bound holds)', () => {
176
+ const jobs = [makeJob({ slug: '05-fix-foo', investigationDepth: 3 })];
177
+ const result = selectAutoFixTargets(jobs, { fixSlugExists: noSiblingOnDisk });
178
+ assert.strictEqual(result.length, 0);
179
+ });
180
+
181
+ test('legacy fix-plan job with no investigationDepth field defaults to excluded (back-compat)', () => {
182
+ const jobs = [makeJob({ slug: '05-fix-foo' })];
183
+ const result = selectAutoFixTargets(jobs, { fixSlugExists: noSiblingOnDisk });
184
+ assert.strictEqual(result.length, 0);
185
+ });
186
+
187
+ test('isFixPlanBeyondDepthCap: non-fix-plan slug is never capped regardless of depth', () => {
188
+ assert.strictEqual(isFixPlanBeyondDepthCap('05-my-feature', 99), false);
189
+ });
190
+
191
+ test('isFixPlanBeyondDepthCap: fix-plan at depth 1 or 2 is not capped, depth 3 is', () => {
192
+ assert.strictEqual(isFixPlanBeyondDepthCap('05-fix-foo', 1), false);
193
+ assert.strictEqual(isFixPlanBeyondDepthCap('05-fix-foo', MAX_INVESTIGATION_DEPTH), false);
194
+ assert.strictEqual(isFixPlanBeyondDepthCap('05-fix-foo', MAX_INVESTIGATION_DEPTH + 1), true);
195
+ });
196
+
197
+ test('healTargetForFix: depth-2 fix-of-a-fix slug resolves back to the depth-1 fix job (521-fix-recorder-export-footer-redesign)', () => {
198
+ // Depth-1 fix job: originally authored to fix job 521, itself in needs_review.
199
+ const depth1FixJob = {
200
+ slug: '521-fix-recorder-export-footer-redesign',
201
+ status: 'needs_review',
202
+ parallelGroup: 521,
203
+ investigationDepth: 2,
204
+ };
205
+ const jobs = [depth1FixJob];
206
+ // spawnInvestigation's fix-slug formula: baseSlug strips only the leading
207
+ // numeric prefix (not "fix-"), so investigating this depth-1 fix job
208
+ // produces a depth-2 fix slug of the form "<group>-fix-fix-<rest>".
209
+ const baseSlug = depth1FixJob.slug.replace(/^\d+-/, '');
210
+ const depth2FixSlug = `${String(depth1FixJob.parallelGroup).padStart(2, '0')}-fix-${baseSlug}`;
211
+ assert.strictEqual(depth2FixSlug, '521-fix-fix-recorder-export-footer-redesign');
212
+ const resolved = healTargetForFix(depth2FixSlug, jobs);
213
+ assert.strictEqual(resolved, depth1FixJob);
214
+ });
215
+
153
216
  console.log('scheduler-autofix-select tests: PASS');
@@ -636,7 +636,20 @@ async function verifyRun({ runDir, prdPath, queueEntry, allJobs = [], committedD
636
636
  // silently accepting a bare sentinel as proof of work done. Mutually
637
637
  // exclusive with the no_verdict_sentinel case above (sentinel === null
638
638
  // vs sentinel === 'pass'), kept as a separate sibling check for clarity.
639
- if (sentinel === 'pass' && !committedDuringRun) {
639
+ //
640
+ // EXEMPTION: fix-plan jobs (slug matches ^\d+-fix-) are investigations —
641
+ // "I checked, the original work already landed correctly, nothing to
642
+ // change" is a legitimate, correct, common outcome for them, not a
643
+ // silent no-op bug. Applying this check to fix-plan jobs the same way as
644
+ // original feature/bugfix PRDs produced a false-positive cascade
645
+ // (2026-07-12: 523-fix-bounded-fix-plan-retry re-verified PRD 523, found
646
+ // it already fully implemented and committed, correctly declined to make
647
+ // a no-op commit, and printed a truthful PASS — but still got flagged
648
+ // needs_review). Original PRDs are expected to build something new, so a
649
+ // bare PASS with no commit from one is still a strong silent-failure
650
+ // signal and stays covered by this check.
651
+ const isFixPlanJob = /^\d+-fix-/.test(queueEntry?.slug || '');
652
+ if (sentinel === 'pass' && !committedDuringRun && !isFixPlanJob) {
640
653
  issues.push({
641
654
  verdict: 'pass_no_commit',
642
655
  reason: 'SCHEDULER_VERDICT: PASS but no commit landed during the run window — the run claims success but produced no code change',
@@ -616,7 +616,7 @@ async function reconcile(state) {
616
616
  }
617
617
  for (const [slug, p] of onDisk) {
618
618
  if (seen.has(slug)) continue;
619
- next.push({
619
+ const entry = {
620
620
  slug,
621
621
  title: p.title,
622
622
  cwd: p.cwd,
@@ -629,7 +629,16 @@ async function reconcile(state) {
629
629
  finishedAt: null,
630
630
  exitCode: null,
631
631
  error: null,
632
- });
632
+ };
633
+ // Newly-discovered fix-plan PRD: stamp its investigationDepth relative to
634
+ // the original job it heals, so selectAutoFixTargets/spawnInvestigation
635
+ // can bound the fix-of-a-fix recursion (see MAX_INVESTIGATION_DEPTH).
636
+ // Non-fix-plan jobs get no explicit field — they read as depth 1 via `?? 1`.
637
+ if (isFixPlanSlug(slug)) {
638
+ const parent = healTargetForFix(slug, state.jobs);
639
+ entry.investigationDepth = parent ? (parent.investigationDepth ?? 1) + 1 : 2;
640
+ }
641
+ next.push(entry);
633
642
  }
634
643
  state.jobs = next.sort((a, b) => b.slug.localeCompare(a.slug));
635
644
  return state;
@@ -1244,8 +1253,8 @@ DO NOT attempt the fix. ONLY write the file. When the file exists, exit immediat
1244
1253
  * Skipped if the failed job is itself a fix-plan (avoids infinite recursion).
1245
1254
  */
1246
1255
  async function spawnInvestigation(failedJob, runDir) {
1247
- if (isFixPlanSlug(failedJob.slug)) {
1248
- console.log(`[scheduler] skip investigation: ${failedJob.slug} is itself a fix plan`);
1256
+ if (isFixPlanBeyondDepthCap(failedJob.slug, failedJob.investigationDepth)) {
1257
+ console.log(`[scheduler] skip investigation: ${failedJob.slug} is a fix plan at/beyond depth cap (depth=${failedJob.investigationDepth ?? 'none'})`);
1249
1258
  return { deferred: false };
1250
1259
  }
1251
1260
  if (investigationsInFlight >= MAX_CONCURRENT_INVESTIGATIONS) {
@@ -1929,8 +1938,35 @@ function selectHistoryJobs(jobs, limit) {
1929
1938
  // commit-detection, which committedInWindow() can fix retroactively (e.g. the
1930
1939
  // git-log --all scan added for missed non-HEAD commits) — rescanning lets a
1931
1940
  // job whose commit is now correctly detected clear on its own instead of
1932
- // staying stuck in needs_review forever.
1933
- const RESCANNABLE_VERDICTS = new Set(['transcript_errors', 'verify_unavailable', 'no_verdict_sentinel']);
1941
+ // staying stuck in needs_review forever. 'pass_no_commit' is included because
1942
+ // verifyRun now exempts fix-plan jobs (slug ^\d+-fix-) from that check — a
1943
+ // fix-plan job flagged before that exemption shipped gets a genuinely
1944
+ // different verdict on rescan (2026-07-12: false-positive cascade where
1945
+ // investigation jobs correctly found "nothing to fix" but were flagged
1946
+ // anyway). For non-fix-plan jobs the exemption never applies, so rescanning
1947
+ // their pass_no_commit verdict is a harmless no-op (same facts, same verdict).
1948
+ const RESCANNABLE_VERDICTS = new Set(['transcript_errors', 'verify_unavailable', 'no_verdict_sentinel', 'pass_no_commit']);
1949
+
1950
+ // Bounds fix-plan recursion: depth 1 = the original job, depth 2 = its fix
1951
+ // (gets exactly one follow-up investigation if it also lands in
1952
+ // needs_review), depth 3+ (a fix-of-a-fix-of-a-fix) is excluded. Shared by
1953
+ // selectAutoFixTargets and spawnInvestigation so both call sites agree on
1954
+ // one threshold.
1955
+ const MAX_INVESTIGATION_DEPTH = 2;
1956
+
1957
+ /**
1958
+ * True when a fix-plan job's investigationDepth is at or past the recursion
1959
+ * cap and it must be excluded from auto-fix eligibility. A fix-plan job with
1960
+ * no recorded investigationDepth (a job already in the queue before this
1961
+ * depth tracking shipped) is treated as excluded too, preserving the
1962
+ * pre-existing blanket-exclusion behavior for legacy jobs — no retroactive
1963
+ * migration. Non-fix-plan slugs are never capped here. Exported for tests.
1964
+ */
1965
+ function isFixPlanBeyondDepthCap(slug, investigationDepth) {
1966
+ if (!isFixPlanSlug(slug)) return false;
1967
+ if (investigationDepth == null) return true;
1968
+ return investigationDepth >= MAX_INVESTIGATION_DEPTH + 1;
1969
+ }
1934
1970
 
1935
1971
  /**
1936
1972
  * Backfill a job's missing runId by scanning RUNS_DIR for a run directory
@@ -2019,7 +2055,7 @@ function selectAutoFixTargets(jobs, { fixSlugExists, resolveJobRunId = resolveRu
2019
2055
  if (job.status !== 'needs_review') return false;
2020
2056
  const runId = job.runId || resolveJobRunId(job);
2021
2057
  if (!runId) return false;
2022
- if (isFixPlanSlug(job.slug)) return false;
2058
+ if (isFixPlanBeyondDepthCap(job.slug, job.investigationDepth)) return false;
2023
2059
  if (job.autoFixAttempted) {
2024
2060
  if (job.autoFixOutcome !== 'no-plan') return false;
2025
2061
  if ((job.autoFixRetries ?? 0) >= 1) return false;
@@ -2665,4 +2701,4 @@ const remote = {
2665
2701
  },
2666
2702
  };
2667
2703
 
2668
- module.exports = { registerScheduleHandlers, attachWindow, init, ROOT, PRDS_DIR, selectHistoryJobs, parsePorcelain, FINISH_PROTOCOL, remote, pickNextBatch, pickForProject, reapDeadRunningJobs, pollRecoveryClearSource, memoryLimitedBatchSize, availableForJobs, reverifyNeedsReview, isRescanCandidate, isPromotableOriginal, selectAutoFixTargets, resolveRunId, isUnresolvableNeedsReview, healTargetForFix, buildInvestigationPrompt, committedInWindow };
2704
+ module.exports = { registerScheduleHandlers, attachWindow, init, ROOT, PRDS_DIR, selectHistoryJobs, parsePorcelain, FINISH_PROTOCOL, remote, pickNextBatch, pickForProject, reapDeadRunningJobs, pollRecoveryClearSource, memoryLimitedBatchSize, availableForJobs, reverifyNeedsReview, isRescanCandidate, isPromotableOriginal, selectAutoFixTargets, resolveRunId, isUnresolvableNeedsReview, healTargetForFix, buildInvestigationPrompt, committedInWindow, isFixPlanSlug, isFixPlanBeyondDepthCap, MAX_INVESTIGATION_DEPTH };