claude-code-session-manager 0.89.0 → 0.91.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 (80) hide show
  1. package/dist/assets/{AgentLibrary-tuj-j1kQ.js → AgentLibrary-zkgdnCnG.js} +1 -1
  2. package/dist/assets/DataModel-CNFdqxrR.js +1 -0
  3. package/dist/assets/{History-DylcasUk.js → History-C-qijfXz.js} +2 -2
  4. package/dist/assets/{Hooks--2xoag9a.js → Hooks-DGxt1nBd.js} +3 -3
  5. package/dist/assets/{HostBilko-oaw8paAh.js → HostBilko-sIYTK-m9.js} +1 -1
  6. package/dist/assets/{Library-DzKA09Iy.js → Library-51Jx0oGl.js} +1 -1
  7. package/dist/assets/{ListDetail-CEdcO80v.js → ListDetail-ByNJU-ie.js} +1 -1
  8. package/dist/assets/MarkdownEditor-Wwcpe9O1.js +1 -0
  9. package/dist/assets/{McpServers-BB7ayW7W.js → McpServers-Bs5xPbTG.js} +2 -2
  10. package/dist/assets/{Memory-BOdYvrL4.js → Memory-BsHGInmd.js} +6 -6
  11. package/dist/assets/{Panel-Yf3SxsRv.js → Panel-ilEDcV0m.js} +1 -1
  12. package/dist/assets/{Permissions-BYCpHNy5.js → Permissions-Djgc3TFk.js} +3 -3
  13. package/dist/assets/{Plugins-B0lXiYqV.js → Plugins-BBojr1mt.js} +2 -2
  14. package/dist/assets/{ProvenanceBadge-T7x8LWcV.js → ProvenanceBadge-Cz1VLnXb.js} +1 -1
  15. package/dist/assets/{SaveBar-BdVb4DbR.js → SaveBar-Cj0wz84u.js} +1 -1
  16. package/dist/assets/Scheduler-D4Ekm93R.js +16 -0
  17. package/dist/assets/{ScopeSwitcher-WVhmNsL5.js → ScopeSwitcher-DA5MqWli.js} +1 -1
  18. package/dist/assets/{Settings-Bt4dVsl6.js → Settings-B1Kfgjvl.js} +2 -2
  19. package/dist/assets/{SkillReferenceGraph-R-0YykV8.js → SkillReferenceGraph-Dew-1uMy.js} +1 -1
  20. package/dist/assets/{Skills-DXV_LdJP.js → Skills-DzuhnXy9.js} +2 -2
  21. package/dist/assets/SystemPrompt-CNcCwEev.js +1 -0
  22. package/dist/assets/{TagLibrary-B9OuqU6M.js → TagLibrary-D0plTZgP.js} +1 -1
  23. package/dist/assets/{TiptapBody-HKKmB0AE.js → TiptapBody-BlmUt7SR.js} +1 -1
  24. package/dist/assets/{Toggle-CBggPonH.js → Toggle-yTfx2R-e.js} +1 -1
  25. package/dist/assets/{index-BHTX4OTc.css → index-CRPJslSA.css} +1 -1
  26. package/dist/assets/{index-BcJqLDiY.js → index-ju-h7BFf.js} +444 -444
  27. package/dist/assets/{settingsSchema-BYE9DVw_.js → settingsSchema-BkcnLBYk.js} +1 -1
  28. package/dist/index.html +2 -2
  29. package/package.json +1 -1
  30. package/scripts/README.md +1 -0
  31. package/src/main/__tests__/bilkoHost-integration.test.cjs +22 -9
  32. package/src/main/__tests__/chatRunner-session-flag-retry.test.cjs +159 -0
  33. package/src/main/__tests__/health-delegation-chain.test.cjs +3 -1
  34. package/src/main/__tests__/pollLoop-dispatch-on-failure.test.cjs +16 -4
  35. package/src/main/__tests__/projectHomeAdminRoutes.test.cjs +12 -0
  36. package/src/main/__tests__/scheduler-default-eligible-heal.test.cjs +10 -3
  37. package/src/main/__tests__/scheduler-looks-done.test.cjs +9 -2
  38. package/src/main/__tests__/scheduler-needs-review-autoresolve.test.cjs +14 -6
  39. package/src/main/__tests__/scheduler-shard-quarantine.test.cjs +2 -2
  40. package/src/main/__tests__/scheduler-stranded-autofix-park.test.cjs +8 -1
  41. package/src/main/__tests__/transcripts-paged-reads.test.cjs +4 -2
  42. package/src/main/__tests__/transcripts-worktree-epic-path.test.cjs +154 -0
  43. package/src/main/__tests__/usageSingleFlight.test.cjs +3 -1
  44. package/src/main/bilkoHost.cjs +6 -7
  45. package/src/main/build-info.json +4 -4
  46. package/src/main/chatRunner.cjs +108 -45
  47. package/src/main/health.cjs +20 -3
  48. package/src/main/index.cjs +1 -1
  49. package/src/main/ipcSchemas.cjs +3 -0
  50. package/src/main/lib/__tests__/epicSpawnCwd.test.cjs +34 -0
  51. package/src/main/lib/__tests__/epicSpawnPlan.test.cjs +117 -0
  52. package/src/main/lib/__tests__/epicTranscriptPath.test.cjs +163 -0
  53. package/src/main/lib/__tests__/localAdminHttp.test.cjs +5 -1
  54. package/src/main/lib/__tests__/schedulerPathsWorktree.test.cjs +40 -1
  55. package/src/main/lib/__tests__/telemetryCountersMetadataColumn.test.cjs +8 -5
  56. package/src/main/lib/effectiveModelInfo.cjs +3 -3
  57. package/src/main/lib/epicDelegationStats.cjs +3 -2
  58. package/src/main/lib/epicSpawnCwd.cjs +36 -5
  59. package/src/main/lib/epicSpawnPlan.cjs +119 -0
  60. package/src/main/lib/epicTranscriptDiagnostic.cjs +79 -0
  61. package/src/main/lib/epicTranscriptPath.cjs +184 -0
  62. package/src/main/lib/gitWorktree.cjs +1 -1
  63. package/src/main/lib/mcpToolCatalog.cjs +6 -1
  64. package/src/main/lib/prdCreate.cjs +27 -1
  65. package/src/main/lib/prdFrontmatter.cjs +16 -5
  66. package/src/main/lib/rcaReport.cjs +27 -3
  67. package/src/main/lib/schedulerPaths.cjs +27 -4
  68. package/src/main/lib/telemetryClient.cjs +11 -4
  69. package/src/main/lib/terminalRunOutcome.cjs +1 -0
  70. package/src/main/pty.cjs +1 -1
  71. package/src/main/runVerify.cjs +89 -1
  72. package/src/main/scheduler.cjs +17 -4
  73. package/src/main/templates/PRD_AUTHORING.md +11 -0
  74. package/src/main/transcripts.cjs +28 -7
  75. package/src/preload/api.d.ts +4 -2
  76. package/src/preload/index.cjs +1 -0
  77. package/dist/assets/DataModel-DOim5SXW.js +0 -1
  78. package/dist/assets/MarkdownEditor-BMyo0F7s.js +0 -1
  79. package/dist/assets/Scheduler-DlOCWioV.js +0 -14
  80. package/dist/assets/SystemPrompt-DiQRo_WF.js +0 -1
package/dist/index.html CHANGED
@@ -7,10 +7,10 @@
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-BcJqLDiY.js"></script>
10
+ <script type="module" crossorigin src="./assets/index-ju-h7BFf.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
- <link rel="stylesheet" crossorigin href="./assets/index-BHTX4OTc.css">
13
+ <link rel="stylesheet" crossorigin href="./assets/index-CRPJslSA.css">
14
14
  </head>
15
15
  <body class="bg-bg text-fg font-sans antialiased">
16
16
  <div id="root"></div>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "claude-code-session-manager",
3
- "version": "0.89.0",
3
+ "version": "0.91.0",
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",
package/scripts/README.md CHANGED
@@ -10,6 +10,7 @@ scripts/lib was moved into src/main/lib (2026-09); nothing under src/ may requir
10
10
  | --- | --- | --- |
11
11
  | audit-ops-hygiene.cjs | manual; named in `ops-sweep` + `develop` SKILL.md | yes |
12
12
  | check-conditional-hooks.cjs | `npm run lint:hooks` (in `lint`) | no |
13
+ | check-epic-transcripts.cjs | manual read-only diagnostic (`--json`); also surfaced as a non-fatal warning in `npm run health` | no |
13
14
  | check-doc-hierarchy.cjs | `npm run lint:docs` (in `lint`) | no |
14
15
  | check-unregistered-tests.cjs | `npm run lint:unregistered-tests` (in `lint`) | no |
15
16
  | check-unstable-selectors.cjs | `npm run lint:selectors` (in `lint`) | no |
@@ -12,15 +12,12 @@ const bilkoHost = require('../bilkoHost.cjs');
12
12
  function makeProject() {
13
13
  const root = fs.mkdtempSync(path.join(os.tmpdir(), 'bilko-host-'));
14
14
  config.addAllowedRoot(root);
15
- fs.mkdirSync(path.join(root, 'session-manager-operations', 'project-pages', 'output'), { recursive: true });
15
+ fs.mkdirSync(path.join(root, 'session-manager-operations', 'project-pages'), { recursive: true });
16
16
  fs.writeFileSync(
17
17
  path.join(root, 'session-manager-operations', 'project-pages', 'home.html'),
18
18
  '<html><title>Demo</title>Home</html>',
19
19
  );
20
- fs.writeFileSync(
21
- path.join(root, 'session-manager-operations', 'project-pages', 'output', 'feature.html'),
22
- '<html><title>Demo</title>Feature</html>',
23
- );
20
+ fs.writeFileSync(path.join(root, 'feature.html'), '<html><title>Demo</title>Feature</html>');
24
21
  fs.writeFileSync(path.join(root, 'package.json'), JSON.stringify({ name: 'demo-app', version: '1.2.3' }));
25
22
  return root;
26
23
  }
@@ -42,7 +39,7 @@ test('addDocument then prepareBundle writes a sub-path document at the expected
42
39
  cwd: root,
43
40
  subpath: 'special-doc/01',
44
41
  title: 'Feature deep-dive',
45
- source: { kind: 'project-page-lens', lens: 'feature' },
42
+ source: { kind: 'file', path: 'feature.html' },
46
43
  });
47
44
  const result = await bilkoHost.prepareBundle({ cwd: root, slug: 'demo-app' });
48
45
  assert.equal(
@@ -53,6 +50,22 @@ test('addDocument then prepareBundle writes a sub-path document at the expected
53
50
  fs.rmSync(root, { recursive: true, force: true });
54
51
  });
55
52
 
53
+ test('a non-home project-page-lens document throws the retired-lens error (no output/ fallback)', async () => {
54
+ const root = makeProject();
55
+ await bilkoHost.prepareBundle({ cwd: root, slug: 'demo-app' });
56
+ await bilkoHost.addDocument({
57
+ cwd: root,
58
+ subpath: 'special-doc/01',
59
+ title: 'Feature deep-dive',
60
+ source: { kind: 'project-page-lens', lens: 'feature' },
61
+ });
62
+ await assert.rejects(
63
+ () => bilkoHost.prepareBundle({ cwd: root, slug: 'demo-app' }),
64
+ /retired "feature" Project Page lens; only the single home\.html page is generated now/,
65
+ );
66
+ fs.rmSync(root, { recursive: true, force: true });
67
+ });
68
+
56
69
  test('removeDocument + prepareBundle wholesale-rebuilds dist/, leaving no trace of the removed file', async () => {
57
70
  const root = makeProject();
58
71
  await bilkoHost.prepareBundle({ cwd: root, slug: 'demo-app' });
@@ -60,7 +73,7 @@ test('removeDocument + prepareBundle wholesale-rebuilds dist/, leaving no trace
60
73
  cwd: root,
61
74
  subpath: 'special-doc/01',
62
75
  title: 'Feature deep-dive',
63
- source: { kind: 'project-page-lens', lens: 'feature' },
76
+ source: { kind: 'file', path: 'feature.html' },
64
77
  });
65
78
  await bilkoHost.prepareBundle({ cwd: root, slug: 'demo-app' });
66
79
  const docId = added.documents.find((d) => d.subpath === 'special-doc/01').id;
@@ -95,10 +108,10 @@ test('addDocument refuses a duplicate sub-path', async () => {
95
108
  cwd: root,
96
109
  subpath: 'doc',
97
110
  title: 'A',
98
- source: { kind: 'project-page-lens', lens: 'feature' },
111
+ source: { kind: 'file', path: 'feature.html' },
99
112
  });
100
113
  await assert.rejects(
101
- () => bilkoHost.addDocument({ cwd: root, subpath: 'doc', title: 'B', source: { kind: 'project-page-lens', lens: 'feature' } }),
114
+ () => bilkoHost.addDocument({ cwd: root, subpath: 'doc', title: 'B', source: { kind: 'file', path: 'feature.html' } }),
102
115
  /already uses subpath/,
103
116
  );
104
117
  fs.rmSync(root, { recursive: true, force: true });
@@ -0,0 +1,159 @@
1
+ /**
2
+ * chatRunner-session-flag-retry.test.cjs — main is authoritative about
3
+ * --session-id vs --resume, and a CLI flag rejection is retried exactly once
4
+ * with the other flag without losing the turn.
5
+ *
6
+ * Run: timeout 300 npx vitest run src/main/__tests__/chatRunner-session-flag-retry.test.cjs
7
+ */
8
+
9
+ 'use strict';
10
+
11
+ import { test, expect, beforeAll, afterAll, afterEach } from 'vitest';
12
+ const fs = require('node:fs');
13
+ const os = require('node:os');
14
+ const path = require('node:path');
15
+
16
+ let tmpHome;
17
+ let originalHome;
18
+ let cr;
19
+ let sent;
20
+
21
+ beforeAll(() => {
22
+ originalHome = process.env.HOME;
23
+ tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'sm-chatrunner-flagretry-home-'));
24
+ process.env.HOME = tmpHome;
25
+ cr = require('../chatRunner.cjs');
26
+ });
27
+
28
+ afterAll(() => {
29
+ process.env.HOME = originalHome;
30
+ fs.rmSync(tmpHome, { recursive: true, force: true });
31
+ });
32
+
33
+ afterEach(() => {
34
+ delete process.env.SM_CLAUDE_BIN;
35
+ cr.__resetQueueForTests();
36
+ cr.attachWindow(null);
37
+ });
38
+
39
+ function attachRecorder() {
40
+ sent = [];
41
+ cr.attachWindow({
42
+ isDestroyed: () => false,
43
+ webContents: { isDestroyed: () => false, send: (channel, payload) => sent.push({ channel, payload }) },
44
+ });
45
+ }
46
+
47
+ // Stub claude: logs its argv (one JSON line per spawn) to logFile. Invocation 1
48
+ // fails with `firstStderr` (optionally after emitting assistant text); later
49
+ // invocations succeed.
50
+ function writeStub(logFile, { firstStderr, textFirst }) {
51
+ const stubPath = path.join(os.tmpdir(), `sm-flagretry-stub-${process.pid}-${Math.floor(Math.random() * 1e9)}.cjs`);
52
+ const body = `
53
+ const fs = require('fs');
54
+ const log = ${JSON.stringify(logFile)};
55
+ fs.appendFileSync(log, JSON.stringify(process.argv.slice(2)) + '\\n');
56
+ const n = fs.readFileSync(log, 'utf8').trim().split('\\n').length;
57
+ if (n === 1) {
58
+ ${textFirst ? "process.stdout.write(JSON.stringify({ type: 'assistant', message: { content: [{ type: 'text', text: 'partial' }] } }) + '\\n');" : ''}
59
+ process.stderr.write(${JSON.stringify(firstStderr)});
60
+ process.exit(1);
61
+ }
62
+ process.stdout.write(JSON.stringify({ type: 'result', subtype: 'success', result: 'ok' }) + '\\n');
63
+ process.exit(0);
64
+ `;
65
+ fs.writeFileSync(stubPath, `#!${process.execPath}\n${body}\n`, { mode: 0o755 });
66
+ return stubPath;
67
+ }
68
+
69
+ async function waitFor(pred, timeoutMs = 8000) {
70
+ const start = Date.now();
71
+ while (!pred()) {
72
+ if (Date.now() - start > timeoutMs) throw new Error('timed out');
73
+ await new Promise((r) => setTimeout(r, 25));
74
+ }
75
+ }
76
+
77
+ const TERMINAL = ['chat:run:complete', 'chat:run:needs-input', 'chat:run:error'];
78
+ let curTab;
79
+ const terminals = () => sent.filter((e) => TERMINAL.includes(e.channel) && e.payload.tabId === curTab);
80
+ const spawns = (logFile) => fs.readFileSync(logFile, 'utf8').trim().split('\n').map((l) => JSON.parse(l));
81
+
82
+ test('in-use error on --session-id retries once with --resume; one terminal event; lane freed', async () => {
83
+ attachRecorder();
84
+ const cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'sm-flagretry-cwd-'));
85
+ const logFile = path.join(cwd, 'argv.log');
86
+ const sessionId = `flag-sess-${process.pid}`;
87
+ process.env.SM_CLAUDE_BIN = writeStub(logFile, { firstStderr: `Error: Session ID ${sessionId} is already in use.\n` });
88
+ try {
89
+ curTab = 'tab-a';
90
+ cr.run({ tabId: 'tab-a', sessionId, prompt: 'hello', cwd, resume: false });
91
+ await waitFor(() => terminals().length > 0);
92
+ await new Promise((r) => setTimeout(r, 100));
93
+
94
+ const argvs = spawns(logFile);
95
+ expect(argvs).toHaveLength(2);
96
+ expect(argvs[0]).toContain('--session-id');
97
+ expect(argvs[1]).toContain('--resume');
98
+ expect(argvs[1]).not.toContain('--session-id');
99
+ expect(argvs[1][argvs[1].indexOf('--model') + 1]).toBe(argvs[0][argvs[0].indexOf('--model') + 1]);
100
+ expect(argvs[1][1]).toBe(argvs[0][1]);
101
+
102
+ expect(terminals()).toHaveLength(1);
103
+ expect(terminals()[0].channel).toBe('chat:run:complete');
104
+ expect(sent.some((e) => e.channel === 'chat:run:notice')).toBe(true);
105
+
106
+ // Lane freed: the same tab accepts a new run immediately (not queued).
107
+ const again = cr.run({ tabId: 'tab-a', sessionId, prompt: 'again', cwd, resume: true });
108
+ expect(again.queued).toBe(false);
109
+ await cr.cancel('tab-a');
110
+ } finally {
111
+ fs.rmSync(cwd, { recursive: true, force: true });
112
+ }
113
+ });
114
+
115
+ test('no retry when the failed attempt already streamed assistant text', async () => {
116
+ attachRecorder();
117
+ const cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'sm-flagretry-cwd-'));
118
+ const logFile = path.join(cwd, 'argv.log');
119
+ const sessionId = `flag-sess2-${process.pid}`;
120
+ process.env.SM_CLAUDE_BIN = writeStub(logFile, {
121
+ firstStderr: `Error: Session ID ${sessionId} is already in use.\n`,
122
+ textFirst: true,
123
+ });
124
+ try {
125
+ curTab = 'tab-b';
126
+ cr.run({ tabId: 'tab-b', sessionId, prompt: 'hello', cwd, resume: false });
127
+ await waitFor(() => terminals().length > 0);
128
+ await new Promise((r) => setTimeout(r, 100));
129
+
130
+ expect(spawns(logFile)).toHaveLength(1);
131
+ expect(terminals()).toHaveLength(1);
132
+ expect(terminals()[0].channel).toBe('chat:run:error');
133
+ expect(terminals()[0].payload.message).toMatch(/already in use/);
134
+ } finally {
135
+ fs.rmSync(cwd, { recursive: true, force: true });
136
+ }
137
+ });
138
+
139
+ test('"No conversation found" on --resume retries once with --session-id (main overrides a wrong renderer flag only via transcript)', async () => {
140
+ attachRecorder();
141
+ const cwd = fs.mkdtempSync(path.join(os.tmpdir(), 'sm-flagretry-cwd-'));
142
+ const logFile = path.join(cwd, 'argv.log');
143
+ const sessionId = `flag-sess3-${process.pid}`;
144
+ process.env.SM_CLAUDE_BIN = writeStub(logFile, { firstStderr: `No conversation found with session ID: ${sessionId}\n` });
145
+ try {
146
+ // No transcript exists anywhere → main computes resume=false even though
147
+ // the renderer said true, so the first attempt is --session-id and the
148
+ // stub's failure is the wrong-direction message: no retry, error surfaces.
149
+ curTab = 'tab-c';
150
+ cr.run({ tabId: 'tab-c', sessionId, prompt: 'hello', cwd, resume: true });
151
+ await waitFor(() => terminals().length > 0);
152
+ const argvs = spawns(logFile);
153
+ expect(argvs).toHaveLength(1);
154
+ expect(argvs[0]).toContain('--session-id');
155
+ expect(terminals()[0].channel).toBe('chat:run:error');
156
+ } finally {
157
+ fs.rmSync(cwd, { recursive: true, force: true });
158
+ }
159
+ });
@@ -94,7 +94,9 @@ test('end-to-end: check() reports delegation_chain for this repo, expected to pa
94
94
  process.env.SM_ALLOW_LIVE_ROOT_READS = '1';
95
95
  let status;
96
96
  try {
97
- status = await check();
97
+ // skipTypecheck: this test asserts on delegation_chain only; the tsc shell-out was
98
+ // ~15s of the ~16s idle runtime and starved to the 90s budget under CPU load.
99
+ status = await check({ skipTypecheck: true });
98
100
  } finally {
99
101
  delete process.env.SM_ALLOW_LIVE_ROOT_READS;
100
102
  process.env.HOME = sandboxHome;
@@ -135,13 +135,19 @@ test('pollLoop still attempts a dispatch after a transient billing failure, usin
135
135
  await scheduler.writeQueue({ jobs: [], config: {}, paused: null });
136
136
 
137
137
  await seedOnePendingJob();
138
- expect(await lastDispatchAttemptAt()).toBeNull();
138
+ // Baseline, not `toBeNull()`: under full-suite load a straggler tick from an
139
+ // earlier poll/test (spawnJob's completion tick, a slot-release pump) can
140
+ // stamp between the reset above and here, so the stamp is not guaranteed
141
+ // null. What the test proves is that THIS poll+flush advances the stamp.
142
+ const before = await lastDispatchAttemptAt();
139
143
 
140
144
  process.env.SM_MOCK_BILLING_KIND = 'transient';
141
145
  await scheduler.pollLoop();
142
146
  await flushPendingTick();
143
147
 
144
- expect(await lastDispatchAttemptAt()).not.toBeNull();
148
+ const after = await lastDispatchAttemptAt();
149
+ expect(after).not.toBeNull();
150
+ expect(after).not.toBe(before);
145
151
  });
146
152
 
147
153
  test('pollLoop still attempts a dispatch after the billing poll throws, using the degraded budget (not utilization 0)', async () => {
@@ -152,7 +158,11 @@ test('pollLoop still attempts a dispatch after the billing poll throws, using th
152
158
  await scheduler.writeQueue({ jobs: [], config: {}, paused: null });
153
159
 
154
160
  await seedOnePendingJob();
155
- expect(await lastDispatchAttemptAt()).toBeNull();
161
+ // Baseline, not `toBeNull()`: under full-suite load a straggler tick from an
162
+ // earlier poll/test (spawnJob's completion tick, a slot-release pump) can
163
+ // stamp between the reset above and here, so the stamp is not guaranteed
164
+ // null. What the test proves is that THIS poll+flush advances the stamp.
165
+ const before = await lastDispatchAttemptAt();
156
166
 
157
167
  delete process.env.SM_E2E;
158
168
  delete process.env.SM_MOCK_BILLING_KIND;
@@ -160,5 +170,7 @@ test('pollLoop still attempts a dispatch after the billing poll throws, using th
160
170
  await scheduler.pollLoop();
161
171
  await flushPendingTick();
162
172
 
163
- expect(await lastDispatchAttemptAt()).not.toBeNull();
173
+ const after = await lastDispatchAttemptAt();
174
+ expect(after).not.toBeNull();
175
+ expect(after).not.toBe(before);
164
176
  });
@@ -163,3 +163,15 @@ test('bad JSON, missing/relative cwd, non-project cwd, and unknown keys are 400s
163
163
  expect(res.status).toBe(400);
164
164
  expect(res.body.error).toContain('not a Session Manager project');
165
165
  });
166
+
167
+ test('one-file contract: project-pages holds exactly home.html after one write and after a regenerate', async () => {
168
+ const cwd = await mkProjectCwd();
169
+ const adminHttp = makeFakeAdminHttp();
170
+ registerAdminRoute(adminHttp);
171
+ const pagesDir = path.join(fs.realpathSync(cwd), 'session-manager-operations', 'project-pages');
172
+ await adminHttp.call('POST', '/admin/project-home/write', { body: { cwd, html: '<p>first</p>' } });
173
+ expect(fs.readdirSync(pagesDir)).toEqual(['home.html']);
174
+ await adminHttp.call('POST', '/admin/project-home/write', { body: { cwd, html: '<p>second, different</p>' } });
175
+ expect(fs.readdirSync(pagesDir)).toEqual(['home.html']);
176
+ expect(fs.readFileSync(path.join(pagesDir, 'home.html'), 'utf8')).toBe('<p>second, different</p>');
177
+ });
@@ -19,6 +19,13 @@ const path = require('node:path');
19
19
 
20
20
  process.env.HOME = fs.mkdtempSync(path.join(os.tmpdir(), 'default-eligible-heal-test-'));
21
21
 
22
+ // Fixture cwd must be a REAL, writable dir: transitionJob out of needs_review
23
+ // fire-and-forgets a history append under <cwd>/session-manager-operations. A fake
24
+ // path made it fail EACCES AFTER the sync test returned, so its console.error hit
25
+ // vitest mid worker-close ("Closing rpc while onUserConsoleLog was pending").
26
+ const PROJECT_CWD = path.join(process.env.HOME, 'project');
27
+ fs.mkdirSync(PROJECT_CWD, { recursive: true });
28
+
22
29
  const {
23
30
  isRescanCandidate,
24
31
  isTranscriptRescannable,
@@ -39,7 +46,7 @@ const {
39
46
  } = require('../scheduler.cjs');
40
47
  const { evaluateBlockingParkHealth } = require('../health.cjs');
41
48
 
42
- const row = (o = {}) => ({ slug: '10-x', status: 'needs_review', runId: 'r1', cwd: '/p', ...o });
49
+ const row = (o = {}) => ({ slug: '10-x', status: 'needs_review', runId: 'r1', cwd: PROJECT_CWD, ...o });
43
50
 
44
51
  test('shared_tree_reverted is a rescan candidate without being on any list', () => {
45
52
  assert.equal(RESCANNABLE_VERDICTS.has('shared_tree_reverted'), false);
@@ -142,8 +149,8 @@ test('auto-resolve ladder is capped: requeue attempts stop at NEEDS_REVIEW_RESOL
142
149
  describe('evaluateBlockingParkHealth', () => {
143
150
  const now = Date.parse('2026-09-18T20:00:00Z');
144
151
  const parkedAgo = (ms) => [{ to: 'needs_review', at: new Date(now - ms).toISOString() }];
145
- const park = (ms, o = {}) => ({ slug: '5-park', status: 'needs_review', cwd: '/p', verifierVerdict: 'shared_tree_reverted', statusHistory: parkedAgo(ms), ...o });
146
- const dep = (slug, dependsOn) => ({ slug, status: 'pending', cwd: '/p', dependsOn });
152
+ const park = (ms, o = {}) => ({ slug: '5-park', status: 'needs_review', cwd: PROJECT_CWD, verifierVerdict: 'shared_tree_reverted', statusHistory: parkedAgo(ms), ...o });
153
+ const dep = (slug, dependsOn) => ({ slug, status: 'pending', cwd: PROJECT_CWD, dependsOn });
147
154
 
148
155
  test('parked past one interval with pending dependents (transitive) is non-green, naming row + count', () => {
149
156
  const r = evaluateBlockingParkHealth([park(REVERIFY_INTERVAL_MS + 1000), dep('6-a', ['5-park']), dep('7-b', ['6-a']), dep('8-c', [])], now);
@@ -210,7 +210,10 @@ test('needs_review with an in-window commit heals exactly as today (regression
210
210
  '- [ ] `npm test` passes',
211
211
  ].join('\n'));
212
212
 
213
- const startedAt = new Date().toISOString();
213
+ // Window opens a minute back: the only commit here is initRepo's, and git's --since
214
+ // compares whole seconds, so `new Date()` (taken AFTER initRepo) drops it out of the
215
+ // window whenever the clock crosses a second boundary under load.
216
+ const startedAt = new Date(Date.now() - 60_000).toISOString();
214
217
  const queuePath = writeProjectQueue(projectCwd, [
215
218
  {
216
219
  slug: '20-clean-feature',
@@ -266,7 +269,6 @@ test('computeLooksDone: no declared paths on the PRD → null, never fabricates
266
269
  test('needs_review row with autoFixAttempted:true is never also stamped looksDone (PRD 1136 — mutually exclusive)', async () => {
267
270
  const projectCwd = path.join(tmpHome, 'proj-already-fix-planned');
268
271
  initRepo(projectCwd);
269
- registerActiveProject(projectCwd, 'proj-already-fix-planned-slug');
270
272
  writePrd(projectCwd, '30-example', [
271
273
  '# Implementation notes',
272
274
  'Edit `src/thing.js`.',
@@ -301,6 +303,11 @@ test('needs_review row with autoFixAttempted:true is never also stamped looksDon
301
303
  JSON.stringify({ type: 'result', subtype: 'error_during_execution', is_error: true, result: 'still broken' }),
302
304
  ]);
303
305
 
306
+ // Register LAST: the project only becomes discoverable once its PRD and queue row both
307
+ // exist, so a straggling background reconcile from an earlier test can't see the PRD
308
+ // without its row and mint a 'quarantined' adopt row over this needs_review one.
309
+ registerActiveProject(projectCwd, 'proj-already-fix-planned-slug');
310
+
304
311
  commitFile(projectCwd, 'src/thing.js', 'hello', 'later commit touching the declared path');
305
312
 
306
313
  await reverifyNeedsReview();
@@ -26,6 +26,14 @@ const path = require('node:path');
26
26
  const tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'needs-review-autoresolve-test-'));
27
27
  process.env.HOME = tmpHome;
28
28
 
29
+ // Fixture cwd must be a REAL, writable dir: every transitionJob out of
30
+ // needs_review fire-and-forgets a history append under <cwd>/session-manager-operations.
31
+ // A fake path ('/home/user/project') made that append fail with EACCES *after*
32
+ // the synchronous test returned, so its console.error landed while vitest was
33
+ // closing the worker ("Closing rpc while onUserConsoleLog was pending" -> exit 1).
34
+ const PROJECT_CWD = path.join(tmpHome, 'project');
35
+ fs.mkdirSync(PROJECT_CWD, { recursive: true });
36
+
29
37
  const {
30
38
  selectExhaustedNeedsReviewTargets,
31
39
  applyNeedsReviewAutoResolve,
@@ -41,7 +49,7 @@ const THRESHOLD_MS = 30 * MIN_MS;
41
49
  function exhaustedJob(overrides = {}) {
42
50
  return {
43
51
  slug: 'exhausted-row',
44
- cwd: '/home/user/project',
52
+ cwd: PROJECT_CWD,
45
53
  status: 'needs_review',
46
54
  autoFixAttempted: true,
47
55
  autoFixOutcome: 'no-plan',
@@ -153,16 +161,16 @@ test('applyNeedsReviewAutoResolve race-guards against a row that moved off needs
153
161
  test('a 3-row dependsOn chain drains: the exhausted middle row auto-skips and the third row becomes eligible', () => {
154
162
  const now = Date.now();
155
163
  const rowA = {
156
- slug: 'a-first', cwd: '/home/user/project', status: 'completed',
164
+ slug: 'a-first', cwd: PROJECT_CWD, status: 'completed',
157
165
  };
158
166
  const rowB = exhaustedJob({
159
167
  slug: 'b-middle',
160
- cwd: '/home/user/project',
168
+ cwd: PROJECT_CWD,
161
169
  exhaustedResolveAttempts: NEEDS_REVIEW_RESOLVE_CAP,
162
170
  dependsOn: ['a-first'],
163
171
  });
164
172
  const rowC = {
165
- slug: 'c-last', cwd: '/home/user/project', status: 'pending', dependsOn: ['b-middle'],
173
+ slug: 'c-last', cwd: PROJECT_CWD, status: 'pending', dependsOn: ['b-middle'],
166
174
  };
167
175
  const jobs = [rowA, rowB, rowC];
168
176
 
@@ -182,8 +190,8 @@ test('a 3-row dependsOn chain drains: the exhausted middle row auto-skips and th
182
190
  });
183
191
 
184
192
  test('a generic (non-auto-resolved) skipped dep still blocks forever — the carve-out is narrow', () => {
185
- const rowB = { slug: 'b-middle', cwd: '/home/user/project', status: 'skipped' }; // no needsReviewAutoResolvedSkip marker
186
- const rowC = { slug: 'c-last', cwd: '/home/user/project', status: 'pending', dependsOn: ['b-middle'] };
193
+ const rowB = { slug: 'b-middle', cwd: PROJECT_CWD, status: 'skipped' }; // no needsReviewAutoResolvedSkip marker
194
+ const rowC = { slug: 'c-last', cwd: PROJECT_CWD, status: 'pending', dependsOn: ['b-middle'] };
187
195
  const { batch } = pickForProject([rowB, rowC], new Set(), 5);
188
196
  assert.equal(batch.length, 0, 'a PRD-source-vanished skip must remain a permanent block, unaffected by this carve-out');
189
197
  });
@@ -103,8 +103,8 @@ test('tickQueue dispatches the healthy project while the torn project is skipped
103
103
  expect(fs.existsSync(path.join(torn.cwd, 'ran-here.marker'))).toBe(false);
104
104
  expect(fs.readFileSync(torn.queueFile).equals(before)).toBe(true);
105
105
 
106
- const health = await require('../health.cjs').check();
106
+ const health = await require('../health.cjs').check({ skipTypecheck: true });
107
107
  expect(health.components.scheduler_queue.ok).toBe(false);
108
108
  expect(health.components.scheduler_queue.quarantinedCwds).toEqual([torn.cwd]);
109
109
  expect(health.issues.some((i) => i.includes(torn.cwd))).toBe(true);
110
- }, 120_000); // health.check() alone can take tens of seconds
110
+ }, 120_000); // skipTypecheck: this asserts scheduler_queue only; tsc was the tens of seconds
@@ -38,6 +38,13 @@ const { execFileSync } = require('node:child_process');
38
38
  const tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'stranded-autofix-park-test-'));
39
39
  process.env.HOME = tmpHome;
40
40
 
41
+ // Fixture cwd must be a REAL, writable dir: transitionJob out of needs_review
42
+ // fire-and-forgets a history append under <cwd>/session-manager-operations. A fake
43
+ // path made it fail EACCES AFTER the sync test returned, so its console.error hit
44
+ // vitest mid worker-close ("Closing rpc while onUserConsoleLog was pending").
45
+ const PROJECT_CWD = path.join(tmpHome, 'project');
46
+ fs.mkdirSync(PROJECT_CWD, { recursive: true });
47
+
41
48
  const {
42
49
  isStrandedAutoFixPark,
43
50
  isExhaustedAutoFix,
@@ -72,7 +79,7 @@ function registerActiveProject(cwd, slug) {
72
79
  function strandedJob(overrides = {}) {
73
80
  return {
74
81
  slug: '1218-fo-01-move-scripts-lib-into-src-main-lib',
75
- cwd: '/home/user/project',
82
+ cwd: PROJECT_CWD,
76
83
  status: 'needs_review',
77
84
  exitCode: 0,
78
85
  verifierVerdict: 'transcript_errors',
@@ -137,7 +137,9 @@ test('inode-change rotation invalidates and rebuilds the index — no stale offs
137
137
  // The chokidar watcher on the live sub picks up the rotation and rebuilds
138
138
  // the index via readDelta's inode-change reset — poll (bounded) rather
139
139
  // than assume a fixed debounce delay.
140
- const deadline = Date.now() + 5000;
140
+ // 30s ceiling, not 5s: the watcher event is starved under heavy CPU contention; the loop
141
+ // exits the instant the condition holds so idle cost is unchanged.
142
+ const deadline = Date.now() + 30000;
141
143
  while (Date.now() < deadline) {
142
144
  page = await pageEvents(tabId, 0, 0);
143
145
  if (page.totalLines === 1 && page.events[0]?.data === 'new-a') break;
@@ -146,7 +148,7 @@ test('inode-change rotation invalidates and rebuilds the index — no stale offs
146
148
  expect(page.totalLines).toBe(1);
147
149
  expect(page.events[0].data).toBe('new-a');
148
150
  closeTab(tabId);
149
- }, 10000);
151
+ }, 40000);
150
152
 
151
153
  test('a partial trailing line is not indexed until it completes', async () => {
152
154
  const sessionId = 'sess-partial';