claude-code-session-manager 0.121.0 → 0.122.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 (76) hide show
  1. package/dist/assets/{AgentLibrary-FO2ei2wB.js → AgentLibrary-BmJ8hIPw.js} +1 -1
  2. package/dist/assets/{Button-CeIOXUPU.js → Button-BS4kmz4p.js} +1 -1
  3. package/dist/assets/{DataModel-D0p__Hil.js → DataModel-DZHBZrfL.js} +1 -1
  4. package/dist/assets/History-pg00b2Gd.js +2 -0
  5. package/dist/assets/{Hooks-DkTnRHyT.js → Hooks-BTEs0UZY.js} +1 -1
  6. package/dist/assets/{Library-l7KS6pkE.js → Library-DuaJ-VSn.js} +1 -1
  7. package/dist/assets/{ListDetail-DDw8fuRT.js → ListDetail-Cud72s-O.js} +1 -1
  8. package/dist/assets/{McpServers-UUNIxk_3.js → McpServers-Dy-wnG_X.js} +1 -1
  9. package/dist/assets/{Memory-CNYUoQ8C.js → Memory-Bs1T1Jo7.js} +1 -1
  10. package/dist/assets/{Panel-Bb54soc2.js → Panel-8UhLXEOW.js} +1 -1
  11. package/dist/assets/{Permissions-D-dNdV3f.js → Permissions-BfwzvmBS.js} +1 -1
  12. package/dist/assets/Plugins-McjyvvKh.js +2 -0
  13. package/dist/assets/{ProvenanceBadge-DEAkDNjA.js → ProvenanceBadge-D0Ov_6WJ.js} +1 -1
  14. package/dist/assets/{RemoteMachines-xCEgvTGU.js → RemoteMachines-gOlFTvWq.js} +1 -1
  15. package/dist/assets/{SaveBar-BSEs7ecA.js → SaveBar-Cd6hJR6K.js} +1 -1
  16. package/dist/assets/Scheduler-BZXKqYJP.js +16 -0
  17. package/dist/assets/{ScopeSwitcher-DcaFyAV-.js → ScopeSwitcher-C24ysV1K.js} +1 -1
  18. package/dist/assets/{Settings-vcdWy_Jx.js → Settings-DWnN2qxn.js} +1 -1
  19. package/dist/assets/{SkillReferenceGraph-CLrJWTPd.js → SkillReferenceGraph-BsWuiTbw.js} +1 -1
  20. package/dist/assets/{Skills-CYM4rBEC.js → Skills-6pHn6glu.js} +1 -1
  21. package/dist/assets/{SystemPrompt-BWNIu-iY.js → SystemPrompt-DT0LBH-K.js} +1 -1
  22. package/dist/assets/{TagLibrary-CYdwHK6m.js → TagLibrary-DbJ0zbYL.js} +1 -1
  23. package/dist/assets/{TiptapBody-Kt90lHrD.js → TiptapBody-C4kIvIMH.js} +1 -1
  24. package/dist/assets/{Toggle-D1OXWYOz.js → Toggle-pP19zlyk.js} +1 -1
  25. package/dist/assets/{ViewTabs-BeQR3CKh.js → ViewTabs-CpzN8e5I.js} +1 -1
  26. package/dist/assets/{historyTotals-B0qafrRo.js → historyTotals-Caq78bb5.js} +1 -1
  27. package/dist/assets/{index-Bq9uH08R.js → index-Bjtguv6I.js} +396 -396
  28. package/dist/assets/{settingsSchema-0ZksSAPB.js → settingsSchema-CC2ySu3d.js} +1 -1
  29. package/dist/assets/{skillFrontmatter-C0nh8yfa.js → skillFrontmatter-hDhNo8jE.js} +1 -1
  30. package/dist/assets/{useAgentsDirWatch-DPkoBybS.js → useAgentsDirWatch-BuOtY25U.js} +1 -1
  31. package/dist/assets/{useRemotePeers-BDJMUihy.js → useRemotePeers-CNQmRBpt.js} +1 -1
  32. package/dist/index.html +1 -1
  33. package/package.json +1 -1
  34. package/plugins/session-manager-dev/skills/develop/SKILL.md +7 -7
  35. package/plugins/session-manager-dev/skills/develop/standards.md +4 -10
  36. package/scripts/scheduler-mcp-server.cjs +2 -2
  37. package/src/main/build-info.json +4 -4
  38. package/src/main/chatRunner.cjs +13 -37
  39. package/src/main/docEdit.cjs +1 -0
  40. package/src/main/index.cjs +12 -13
  41. package/src/main/ipcSchemas.cjs +2 -2
  42. package/src/main/lib/classifyPromptTicket.cjs +1 -0
  43. package/src/main/lib/claude/effectiveModelInfo.cjs +9 -21
  44. package/src/main/lib/claude/personaToolScope.cjs +7 -1
  45. package/src/main/lib/claude/runClaudeP.cjs +1 -0
  46. package/src/main/lib/epic/delegationReadiness.cjs +75 -8
  47. package/src/main/lib/mcpToolCatalog.cjs +0 -1
  48. package/src/main/lib/persona-macro/shippedPersonaSeeds.cjs +11 -0
  49. package/src/main/lib/prd/prdCreate.cjs +3 -1
  50. package/src/main/lib/prd/prdSizing.cjs +1 -8
  51. package/src/main/lib/runSeeders.cjs +21 -0
  52. package/src/main/lib/scheduler/loopEfficiency.cjs +109 -0
  53. package/src/main/lib/scheduler/queueHistory.cjs +44 -1
  54. package/src/main/lib/scheduler/queueStore.cjs +4 -72
  55. package/src/main/lib/scheduler/rcaReport.cjs +1 -1
  56. package/src/main/lib/scheduler/refireCap.cjs +40 -0
  57. package/src/main/lib/scheduler/reviewNotice.cjs +1 -1
  58. package/src/main/lib/scheduler/schedulerConfig.cjs +1 -2
  59. package/src/main/lib/scheduler/schedulerPaths.cjs +0 -2
  60. package/src/main/lib/scheduler/sessionSlots.cjs +1 -1
  61. package/src/main/lib/telemetry/auditLog.cjs +65 -14
  62. package/src/main/loopEfficiencyHandlers.cjs +160 -0
  63. package/src/main/queueOps.cjs +4 -9
  64. package/src/main/runVerify.cjs +22 -454
  65. package/src/main/scheduler/reviewLadder.cjs +12 -2
  66. package/src/main/scheduler.cjs +103 -177
  67. package/src/main/seedDevPlugin.cjs +49 -22
  68. package/src/main/seedStatus.cjs +3 -2
  69. package/src/main/supervisor.cjs +1 -0
  70. package/src/main/templates/PRD_AUTHORING.md +16 -31
  71. package/src/preload/api.d.ts +26 -6
  72. package/src/preload/index.cjs +1 -0
  73. package/src/seed/agents/dev-lead.md +2 -9
  74. package/dist/assets/History-DVqQ5azN.js +0 -2
  75. package/dist/assets/Plugins-DhcIXRhQ.js +0 -2
  76. package/dist/assets/Scheduler-BIKJIU-C.js +0 -16
@@ -303,18 +303,84 @@ async function checkSchedulerMcpLive({ cwd, homeDir, schedulerMcpCheck }) {
303
303
  };
304
304
  }
305
305
 
306
+ const DEV_PLUGIN_MARKETPLACE = 'session-manager';
307
+ const DEV_PLUGIN_REINSTALL_FIX = 'Restart Session Manager to reinstall the session-manager-dev plugin';
308
+
309
+ /** installPath of the dev plugin from installed_plugins.json, or null. */
310
+ function readDevPluginInstallPath(homeDir) {
311
+ const data = readJsonOrSync(path.join(homeDir, '.claude', 'plugins', 'installed_plugins.json'), null);
312
+ const entries = data?.plugins?.[DEV_PLUGIN_ENABLED_KEY];
313
+ if (!Array.isArray(entries)) return null;
314
+ for (const e of entries) {
315
+ if (e && typeof e.installPath === 'string' && e.installPath) return e.installPath;
316
+ }
317
+ return null;
318
+ }
319
+
320
+ /** Source path of the 'session-manager' marketplace from known_marketplaces.json, or null. */
321
+ function readDevMarketplacePath(homeDir) {
322
+ const data = readJsonOrSync(path.join(homeDir, '.claude', 'plugins', 'known_marketplaces.json'), null);
323
+ const m = data?.[DEV_PLUGIN_MARKETPLACE];
324
+ const p = m?.source?.path ?? m?.installLocation;
325
+ return typeof p === 'string' && p ? p : null;
326
+ }
327
+
306
328
  function checkDevPlugin({ homeDir }) {
307
329
  const settings = readJsonOrSync(path.join(homeDir, '.claude', 'settings.json'), null);
308
- const ok = settings?.enabledPlugins?.[DEV_PLUGIN_ENABLED_KEY] === true;
330
+ const enabled = settings?.enabledPlugins?.[DEV_PLUGIN_ENABLED_KEY] === true;
331
+ const base = { id: 'dev-plugin', label: 'session-manager-dev plugin enabled' };
332
+
333
+ if (!enabled) {
334
+ return {
335
+ ...base,
336
+ ok: false,
337
+ detail: `${DEV_PLUGIN_ENABLED_KEY} missing from ~/.claude/settings.json enabledPlugins`,
338
+ fix: 'Enable in Plugins tab: session-manager-dev (Session Manager)',
339
+ };
340
+ }
341
+
342
+ const installPath = readDevPluginInstallPath(homeDir);
343
+ if (!installPath) {
344
+ return {
345
+ ...base,
346
+ ok: false,
347
+ detail: `${DEV_PLUGIN_ENABLED_KEY} enabled but not recorded in ~/.claude/plugins/installed_plugins.json`,
348
+ fix: DEV_PLUGIN_REINSTALL_FIX,
349
+ };
350
+ }
351
+ const skillFile = path.join(installPath, 'skills', 'develop', 'SKILL.md');
352
+ if (!fs.existsSync(skillFile)) {
353
+ return {
354
+ ...base,
355
+ ok: false,
356
+ detail: `${DEV_PLUGIN_ENABLED_KEY} enabled but ${skillFile} is missing`,
357
+ fix: DEV_PLUGIN_REINSTALL_FIX,
358
+ };
359
+ }
360
+
361
+ const marketplacePath = readDevMarketplacePath(homeDir);
362
+ if (!marketplacePath) {
363
+ return {
364
+ ...base,
365
+ ok: false,
366
+ detail: `'${DEV_PLUGIN_MARKETPLACE}' marketplace not registered in ~/.claude/plugins/known_marketplaces.json`,
367
+ fix: DEV_PLUGIN_REINSTALL_FIX,
368
+ };
369
+ }
370
+ if (!fs.existsSync(marketplacePath)) {
371
+ return {
372
+ ...base,
373
+ ok: false,
374
+ detail: `'${DEV_PLUGIN_MARKETPLACE}' marketplace folder ${marketplacePath} no longer exists`,
375
+ fix: DEV_PLUGIN_REINSTALL_FIX,
376
+ };
377
+ }
309
378
 
310
379
  return {
311
- id: 'dev-plugin',
312
- label: 'session-manager-dev plugin enabled',
313
- ok,
314
- detail: ok
315
- ? `${DEV_PLUGIN_ENABLED_KEY} enabled in ~/.claude/settings.json`
316
- : `${DEV_PLUGIN_ENABLED_KEY} missing from ~/.claude/settings.json enabledPlugins`,
317
- fix: ok ? null : 'Enable in Plugins tab: session-manager-dev (Session Manager)',
380
+ ...base,
381
+ ok: true,
382
+ detail: `${DEV_PLUGIN_ENABLED_KEY} enabled in ~/.claude/settings.json and installed on disk`,
383
+ fix: null,
318
384
  };
319
385
  }
320
386
 
@@ -795,6 +861,7 @@ async function ensureGuardsInstalled(cwd, { homeDir = os.homedir() } = {}) {
795
861
  }
796
862
 
797
863
  module.exports = {
864
+ checkDevPlugin,
798
865
  checkDelegationReadiness,
799
866
  ensureGuardsInstalled,
800
867
  logAutoInstallEvent,
@@ -127,7 +127,6 @@ const MCP_TOOL_CATALOG = [
127
127
  exampleArgs: {
128
128
  title: 'Add unit tests for the retry backoff helper',
129
129
  cwd: '/home/bilko/Projects/session-manager',
130
- estimateMinutes: 10,
131
130
  goal: 'Cover retryWithBackoff.cjs edge cases (zero retries, max-delay clamp) that currently have no test.',
132
131
  acceptanceCriteria: ['New test file exercises zero-retry and max-delay-clamp cases', 'Typecheck passes', 'Unit tests pass'],
133
132
  implementationNotes: 'See src/main/lib/retryWithBackoff.cjs and its existing __tests__ sibling for the pattern to extend.',
@@ -124,6 +124,17 @@ const SHIPPED_PERSONA_SEEDS = deepFreeze({
124
124
  tools: "Read, Grep, Glob, Bash, Edit, Write",
125
125
  },
126
126
  },
127
+ {
128
+ commit: "2129verify",
129
+ bodySha256: "82b93c1da9a5e3b7dfcfadd1b4689d988a4bd19de2d897ebd3f3158969d741fc",
130
+ fm: {
131
+ description: "Executes exactly one already-scoped PRD at a time, headless, start to finish — reads the PRD's Goal/Acceptance Criteria/Implementation notes and standards.md, implements it, verifies against its own AC, and reports. Has no visibility into the overall plan — that's architect's job. This is the default persona a scheduled PRD runs as: the scheduler resolves a PRD's `agentType` frontmatter field (default `dev-lead`) to this file and launches the headless executor AS this persona via `--append-system-prompt`.",
132
+ model: "sonnet",
133
+ effort: "medium",
134
+ title: "Engineering — Software Engineer",
135
+ tools: "Read, Grep, Glob, Bash, Edit, Write",
136
+ },
137
+ },
127
138
  ],
128
139
  "project-home-builder": [
129
140
  {
@@ -88,7 +88,9 @@ function buildPrdBody(input) {
88
88
  // No `parallelGroup` frontmatter key (PRD_AUTHORING.md §5) — the NN-
89
89
  // filename prefix is the single source of truth for grouping; adding a
90
90
  // second one here would let the two drift out of sync.
91
- const fmLines = ['---', `title: ${title}`, `cwd: ${cwd}`, `estimateMinutes: ${estimateMinutes}`];
91
+ const fmLines = ['---', `title: ${title}`, `cwd: ${cwd}`];
92
+ // Optional: only a finite positive integer is written; absent => budget's 45-min floor.
93
+ if (Number.isInteger(estimateMinutes) && estimateMinutes > 0) fmLines.push(`estimateMinutes: ${estimateMinutes}`);
92
94
  // Provenance stamp (PRD-authoring lockdown): every PRD written through this
93
95
  // function — the sole in-process implementation behind both the admin
94
96
  // HTTP create-prd route and the renderer's chat:create-prd IPC handler —
@@ -11,7 +11,6 @@
11
11
  'use strict';
12
12
 
13
13
  const SIZING_LIMITS = {
14
- estimateMinutes: 15,
15
14
  acLines: 8,
16
15
  acLineChars: 400,
17
16
  bodyChars: 7000,
@@ -30,18 +29,12 @@ const TIMEOUT_CMD_RE = /\btimeout\s+\d+/gi;
30
29
  * treated as empty/absent rather than thrown on, since this is advisory only.
31
30
  */
32
31
  function sizingWarnings(input) {
33
- const { estimateMinutes, goal, implementationNotes } = input || {};
32
+ const { goal, implementationNotes } = input || {};
34
33
  const acceptanceCriteria = Array.isArray(input?.acceptanceCriteria) ? input.acceptanceCriteria : [];
35
34
  const acLines = acceptanceCriteria.filter((line) => typeof line === 'string');
36
35
 
37
36
  const warnings = [];
38
37
 
39
- if (typeof estimateMinutes === 'number' && estimateMinutes > SIZING_LIMITS.estimateMinutes) {
40
- warnings.push(
41
- `estimateMinutes (${estimateMinutes}) exceeds the ${SIZING_LIMITS.estimateMinutes}-minute sizing limit — split this PRD into smaller work items.`,
42
- );
43
- }
44
-
45
38
  if (acceptanceCriteria.length > SIZING_LIMITS.acLines) {
46
39
  warnings.push(
47
40
  `acceptanceCriteria has ${acceptanceCriteria.length} lines, exceeding the ${SIZING_LIMITS.acLines}-line sizing limit — split into smaller, single-purpose PRDs.`,
@@ -0,0 +1,21 @@
1
+ // Runs first-boot seeders one at a time. `claude plugin install` and
2
+ // `claude mcp add --scope user` both rewrite Claude's config files; run
3
+ // concurrently, one write can drop the other's entry after its marker says done.
4
+ // A failing step is logged and never skips the steps after it.
5
+
6
+ /**
7
+ * @param {Array<{ name: string, run: () => Promise<unknown> }>} steps
8
+ * @returns {Promise<void>}
9
+ */
10
+ async function runSeedersSerially(steps) {
11
+ for (const step of steps) {
12
+ try {
13
+ await step.run();
14
+ } catch (e) {
15
+ const message = e instanceof Error ? e.message : String(e);
16
+ console.error(`[seed] ${step.name} failed: ${message}`);
17
+ }
18
+ }
19
+ }
20
+
21
+ module.exports = { runSeedersSerially };
@@ -0,0 +1,109 @@
1
+ /**
2
+ * loopEfficiency.cjs — pure calculator for the scheduler's real throughput
3
+ * metric: validated PRDs per unit of usage, per project.
4
+ *
5
+ * Why: the scheduler is bound by the account's usage window, not by slots, so
6
+ * "verified PRDs per usage %" (and $ per verified PRD) is what matters. Also
7
+ * makes the "wasted runs <= 5%" target in scheduler-operations.md computable.
8
+ *
9
+ * Pure: no I/O, no Date.now(), no mutation of inputs.
10
+ * Complexity O(R + V + P log P): one pass over runs, one over validations,
11
+ * then a sort of the P project rows.
12
+ */
13
+
14
+ 'use strict';
15
+
16
+ const path = require('path');
17
+ const { COMPLETED_EQUIVALENT_VERDICTS } = require('./terminalRunOutcome.cjs');
18
+
19
+ /** @param {unknown} t @param {number} fromMs @param {number} toMs */
20
+ function inWindow(t, fromMs, toMs) {
21
+ return typeof t === 'number' && Number.isFinite(t) && t >= fromMs && t < toMs;
22
+ }
23
+
24
+ /** @param {unknown} slug */
25
+ function normSlug(slug) {
26
+ return String(slug == null ? '' : slug).replace(/^\d+-/, '');
27
+ }
28
+
29
+ /**
30
+ * @param {{
31
+ * runs?: Array<{ cwd: string, slug: string, startedAtMs: number, costUsd?: number, verdict: string | null }>,
32
+ * validations?: Array<{ cwd: string, slug: string, verdict: 'VERIFIED' | 'REFUTED', atMs: number }>,
33
+ * fromMs: number,
34
+ * toMs: number,
35
+ * weeklyUtilizationPct?: number | null,
36
+ * }} input
37
+ */
38
+ function computeLoopEfficiency({ runs, validations, fromMs, toMs, weeklyUtilizationPct }) {
39
+ /** @type {Map<string, { cwd: string, runs: number, wastedRuns: number, executorUsd: number, last: Map<string, { atMs: number, verdict: string }> }>} */
40
+ const byCwd = new Map();
41
+ const bucket = (/** @type {string} */ cwd) => {
42
+ let b = byCwd.get(cwd);
43
+ if (!b) {
44
+ b = { cwd, runs: 0, wastedRuns: 0, executorUsd: 0, last: new Map() };
45
+ byCwd.set(cwd, b);
46
+ }
47
+ return b;
48
+ };
49
+
50
+ for (const r of runs || []) {
51
+ if (!r || !inWindow(r.startedAtMs, fromMs, toMs)) continue;
52
+ const b = bucket(String(r.cwd));
53
+ b.runs += 1;
54
+ const v = r.verdict;
55
+ if (v != null && v !== 'clean' && !COMPLETED_EQUIVALENT_VERDICTS.has(v)) b.wastedRuns += 1;
56
+ const cost = Number(r.costUsd);
57
+ if (Number.isFinite(cost)) b.executorUsd += cost;
58
+ }
59
+
60
+ for (const v of validations || []) {
61
+ if (!v || !inWindow(v.atMs, fromMs, toMs)) continue;
62
+ const b = bucket(String(v.cwd));
63
+ const key = normSlug(v.slug);
64
+ const prev = b.last.get(key);
65
+ if (!prev || v.atMs >= prev.atMs) b.last.set(key, { atMs: v.atMs, verdict: v.verdict });
66
+ }
67
+
68
+ const rows = [];
69
+ const totals = { runs: 0, wastedRuns: 0, verified: 0, refuted: 0, executorUsd: 0, wastedRunRate: 0 };
70
+ for (const b of byCwd.values()) {
71
+ let verified = 0;
72
+ let refuted = 0;
73
+ for (const { verdict } of b.last.values()) {
74
+ if (verdict === 'VERIFIED') verified += 1;
75
+ else if (verdict === 'REFUTED') refuted += 1;
76
+ }
77
+ rows.push({
78
+ cwd: b.cwd,
79
+ project: path.basename(b.cwd),
80
+ runs: b.runs,
81
+ wastedRuns: b.wastedRuns,
82
+ verified,
83
+ refuted,
84
+ executorUsd: b.executorUsd,
85
+ usdPerVerified: verified > 0 ? b.executorUsd / verified : null,
86
+ });
87
+ totals.runs += b.runs;
88
+ totals.wastedRuns += b.wastedRuns;
89
+ totals.verified += verified;
90
+ totals.refuted += refuted;
91
+ totals.executorUsd += b.executorUsd;
92
+ }
93
+ totals.wastedRunRate = totals.runs > 0 ? totals.wastedRuns / totals.runs : 0;
94
+
95
+ rows.sort((a, b) => b.executorUsd - a.executorUsd || (a.project < b.project ? -1 : a.project > b.project ? 1 : 0));
96
+
97
+ const pct = typeof weeklyUtilizationPct === 'number' ? weeklyUtilizationPct : NaN;
98
+ const hasPct = Number.isFinite(pct) && pct > 0;
99
+ return {
100
+ fromMs,
101
+ toMs,
102
+ rows,
103
+ totals,
104
+ weeklyUtilizationPct: weeklyUtilizationPct == null ? null : weeklyUtilizationPct,
105
+ verifiedPerUsagePct: hasPct ? totals.verified / pct : null,
106
+ };
107
+ }
108
+
109
+ module.exports = { computeLoopEfficiency };
@@ -11,7 +11,8 @@
11
11
  // disk. appendHistory/readHistory own the actual fs I/O.
12
12
 
13
13
  const path = require('path');
14
- const fsp = require('fs').promises;
14
+ const fs = require('fs');
15
+ const fsp = fs.promises;
15
16
  const { HISTORY_RETENTION_MS } = require('./schedulerConfig.cjs');
16
17
  const { projectHistoryPath, stateCwds } = require('./queueStore.cjs');
17
18
  const { queueHistoryPath } = require('./schedulerPaths.cjs');
@@ -288,6 +289,47 @@ async function readHistoryFile(target, cap) {
288
289
  }
289
290
  }
290
291
 
292
+ /**
293
+ * Sync bounded tail read of one history file: newest-first parsed rows, at
294
+ * most `maxRows` (clamped [1,500]) drawn from at most the last `maxBytes`
295
+ * bytes (default 1 MB). Missing/unreadable file -> []. Never throws.
296
+ */
297
+ function readHistoryFileTailSync(target, { maxRows = 500, maxBytes = 1024 * 1024 } = {}) {
298
+ const cap = Math.max(1, Math.min(500, Math.floor(maxRows) || 500));
299
+ let fd;
300
+ try {
301
+ fd = fs.openSync(target, 'r');
302
+ const { size } = fs.fstatSync(fd);
303
+ if (size === 0) return [];
304
+ const length = Math.min(Math.max(1, maxBytes), size);
305
+ const start = size - length;
306
+ const buf = Buffer.alloc(length);
307
+ fs.readSync(fd, buf, 0, length, start);
308
+ let text = buf.toString('utf8');
309
+ if (start > 0) {
310
+ // Chunk may begin mid-line; drop the partial first fragment.
311
+ const firstNl = text.indexOf('\n');
312
+ text = firstNl === -1 ? '' : text.slice(firstNl + 1);
313
+ }
314
+ const lines = text.split('\n').map((l) => l.trim()).filter(Boolean).slice(-cap);
315
+ const parsed = [];
316
+ for (const line of lines) {
317
+ try {
318
+ parsed.push(JSON.parse(line));
319
+ } catch {
320
+ // corrupt/partial line — skip
321
+ }
322
+ }
323
+ return parsed.reverse();
324
+ } catch {
325
+ return [];
326
+ } finally {
327
+ if (fd !== undefined) {
328
+ try { fs.closeSync(fd); } catch { /* ignore */ }
329
+ }
330
+ }
331
+ }
332
+
291
333
  // mtime-gated cache, same convention as scheduler/prdParser.cjs's dirCache —
292
334
  // repeated reconcile() calls between appends hit the cache instead of
293
335
  // re-reading/re-parsing a JSONL file that only grows over time.
@@ -467,6 +509,7 @@ module.exports = {
467
509
  appendHistory,
468
510
  slimHistoryRow,
469
511
  readHistory,
512
+ readHistoryFileTailSync,
470
513
  historyTerminalBySlug,
471
514
  completedSlugsForCwd,
472
515
  };
@@ -11,7 +11,7 @@
11
11
  * - Per-project history: `<cwd>/session-manager-operations/scheduler/state/history.jsonl`
12
12
  * (owned by queueHistory.cjs, path resolved here)
13
13
  * - Machine runtime state: `~/.claude/session-manager/scheduler-machine.json`
14
- * (config, paused/rate-limit, scheduledFor, lastRunAt — these are
14
+ * (config, paused/rate-limit, lastRunAt — these are
15
15
  * Session-Manager runtime concerns, like the sessionSlots pool, not any
16
16
  * one project's data. Run logs under scheduled-plans/runs/ stay
17
17
  * machine-local for the same reason: they're execution artifacts of this
@@ -230,7 +230,6 @@ function bustCwdCache() {
230
230
  function shapeMachine(data) {
231
231
  return {
232
232
  config: data.config || {},
233
- scheduledFor: data.scheduledFor ?? null,
234
233
  lastRunAt: data.lastRunAt ?? null,
235
234
  // Distinct from lastRunAt (stamped only when tickQueue actually launches a
236
235
  // job): this is stamped every time tickQueue gets far enough to evaluate
@@ -451,7 +450,7 @@ function shapeJobs(raw, file) {
451
450
  }
452
451
 
453
452
  /**
454
- * readMergedSync(opts?) → { config, jobs, scheduledFor, lastRunAt, paused,
453
+ * readMergedSync(opts?) → { config, jobs, lastRunAt, paused,
455
454
  * unreadable?, unreadablePath?, unreadableCwds, sourceCwds }.
456
455
  *
457
456
  * `unreadable` is reserved for the machine-runtime file alone (a machine-wide
@@ -462,7 +461,7 @@ function shapeJobs(raw, file) {
462
461
  * writeSplit can persist "this project now has zero jobs".
463
462
  */
464
463
  function readMergedSync(opts) {
465
- const out = { config: {}, jobs: [], scheduledFor: null, lastRunAt: null, lastDispatchAttemptAt: null, paused: null, drain: null, launchBlocks: {}, launchMitigations: {}, invalidJobs: [], unreadableCwds: [] };
464
+ const out = { config: {}, jobs: [], lastRunAt: null, lastDispatchAttemptAt: null, paused: null, drain: null, launchBlocks: {}, launchMitigations: {}, invalidJobs: [], unreadableCwds: [] };
466
465
  const sourceCwds = [];
467
466
  const machine = loadMachineStateSync();
468
467
  if (machine.shaped) {
@@ -496,7 +495,7 @@ function readMergedSync(opts) {
496
495
 
497
496
  /** Async twin of readMergedSync for IPC hot paths. */
498
497
  async function readMerged(opts) {
499
- const out = { config: {}, jobs: [], scheduledFor: null, lastRunAt: null, lastDispatchAttemptAt: null, paused: null, drain: null, launchBlocks: {}, launchMitigations: {}, invalidJobs: [], unreadableCwds: [] };
498
+ const out = { config: {}, jobs: [], lastRunAt: null, lastDispatchAttemptAt: null, paused: null, drain: null, launchBlocks: {}, launchMitigations: {}, invalidJobs: [], unreadableCwds: [] };
500
499
  const sourceCwds = [];
501
500
  const machine = await loadMachineState();
502
501
  if (machine.shaped) {
@@ -547,7 +546,6 @@ function defineSources(state, sourceCwds) {
547
546
  async function writeSplit(state, defaultCwd) {
548
547
  await writeJsonAtomic(schedulerPaths.machineStatePath(), {
549
548
  config: state.config,
550
- scheduledFor: state.scheduledFor ?? null,
551
549
  lastRunAt: state.lastRunAt ?? null,
552
550
  lastDispatchAttemptAt: state.lastDispatchAttemptAt ?? null,
553
551
  paused: state.paused ?? null,
@@ -581,70 +579,6 @@ async function writeSplit(state, defaultCwd) {
581
579
  bustCwdCache();
582
580
  }
583
581
 
584
- // ---------- legacy migration ----------
585
-
586
- /**
587
- * migrateLegacyGlobalQueue(defaultCwd) — one-time boot split of the retired
588
- * global queue.json into per-project shards. Shard rows win over legacy rows
589
- * with the same slug (the shard is newer by construction). The legacy file is
590
- * renamed to `queue.json.retired-<epoch>` so a rollback can recover it but no
591
- * reader ever consults it again. Machine fields (config/paused/...) migrate
592
- * only when no machine file exists yet. Idempotent: no legacy file → no-op.
593
- */
594
- async function migrateLegacyGlobalQueue(defaultCwd) {
595
- let raw;
596
- try {
597
- raw = await fsp.readFile(schedulerPaths.legacyQueuePath(), 'utf8');
598
- } catch {
599
- return { migrated: false };
600
- }
601
- let legacy;
602
- try {
603
- legacy = JSON.parse(raw);
604
- } catch (e) {
605
- console.error(`[queueStore] legacy queue.json unparseable — leaving in place: ${e?.message}`);
606
- return { migrated: false, error: e?.message };
607
- }
608
-
609
- if (!fs.existsSync(schedulerPaths.machineStatePath())) {
610
- await writeJsonAtomic(schedulerPaths.machineStatePath(), {
611
- config: legacy.config || {},
612
- scheduledFor: legacy.scheduledFor ?? null,
613
- lastRunAt: legacy.lastRunAt ?? null,
614
- paused: legacy.paused ?? null,
615
- });
616
- }
617
-
618
- const legacyJobs = Array.isArray(legacy.jobs) ? legacy.jobs : [];
619
- const byCwd = new Map();
620
- for (const job of legacyJobs) {
621
- const cwd = job.cwd || defaultCwd;
622
- if (!cwd) continue;
623
- if (!byCwd.has(cwd)) byCwd.set(cwd, []);
624
- byCwd.get(cwd).push(job);
625
- }
626
- let moved = 0;
627
- for (const [cwd, jobs] of byCwd) {
628
- const file = queuePathOrSkip(cwd, 'legacy split');
629
- if (!file) continue;
630
- let existing = [];
631
- try { existing = shapeJobs(await fsp.readFile(file, 'utf8'), file).jobs; } catch { /* fresh shard */ }
632
- const have = new Set(existing.map((j) => j.slug));
633
- const merged = [...existing, ...jobs.filter((j) => !have.has(j.slug))];
634
- try {
635
- await writeJsonAtomic(file, { jobs: merged });
636
- moved += merged.length - existing.length;
637
- } catch (e) {
638
- console.error(`[queueStore] legacy split: failed to write ${file}: ${e?.message}`);
639
- return { migrated: false, error: e?.message };
640
- }
641
- }
642
-
643
- await fsp.rename(schedulerPaths.legacyQueuePath(), `${schedulerPaths.legacyQueuePath()}.retired-${Date.now()}`);
644
- bustCwdCache();
645
- return { migrated: true, moved, projects: byCwd.size };
646
- }
647
-
648
582
  module.exports = {
649
583
  STATE_SUBPATH,
650
584
  projectStateDir,
@@ -656,7 +590,6 @@ module.exports = {
656
590
  readMerged,
657
591
  readMergedSync,
658
592
  writeSplit,
659
- migrateLegacyGlobalQueue,
660
593
  writeJsonAtomic,
661
594
  writeJsonAtomicSync,
662
595
  findLongestValidJsonPrefix,
@@ -664,4 +597,3 @@ module.exports = {
664
597
 
665
598
  // Lazy path getters (SM_SCHEDULER_HOME resolved at read, never at require).
666
599
  Object.defineProperty(module.exports, 'MACHINE_STATE_PATH', { get: schedulerPaths.machineStatePath, enumerable: true });
667
- Object.defineProperty(module.exports, 'LEGACY_QUEUE_PATH', { get: schedulerPaths.legacyQueuePath, enumerable: true });
@@ -12,7 +12,7 @@
12
12
  * It used to file a `proposed` Epic per parked job; Epics are now created in
13
13
  * exactly one place — the human pressing New Epic (see epicMint.cjs's
14
14
  * SINGLE-CREATOR LAW). Surfacing the parked job back to a human is the
15
- * scheduler's job (notifyNeedsReview → appendResponseEventIfKnown), which
15
+ * scheduler's job (flushDueReviewNotices → appendResponseEventIfKnown), which
16
16
  * routes it into the Epic that AUTHORED the PRD rather than opening a
17
17
  * sibling Epic about it.
18
18
  *
@@ -0,0 +1,40 @@
1
+ /**
2
+ * refireCap.cjs — pure decision for a job the post-run verifier downgraded
3
+ * back to `pending` (verdict `halt` / `deps_unmet`).
4
+ *
5
+ * Why: scheduler.cjs re-fired such jobs with no counter, so any verifier
6
+ * disagreement re-ran a billed `claude -p` forever (Bilko-Launch-Kit,
7
+ * 2026-10-11: 125 runs, 846 turns, $28 in 3.5 hours). This helper decides,
8
+ * from the job's consecutive-downgrade count, whether to re-fire or park.
9
+ *
10
+ * Pure: no I/O, no mutation of inputs. Complexity O(1).
11
+ */
12
+
13
+ 'use strict';
14
+
15
+ /** Max consecutive post-run downgrades that are still re-fired. */
16
+ const MAX_CONSECUTIVE_VERIFY_REFIRES = 2;
17
+
18
+ /**
19
+ * @param {{ verifyRefireCount?: unknown }} job
20
+ * @param {{ verdict?: string, reason?: string }} verifyResult
21
+ * @param {{ cap?: number }} [opts]
22
+ * @returns {{ action: 'refire', nextCount: number }
23
+ * | { action: 'park', nextCount: number, verdict: 'refire_capped', reason: string }}
24
+ */
25
+ function decideVerifyDowngrade(job, verifyResult, { cap = MAX_CONSECUTIVE_VERIFY_REFIRES } = {}) {
26
+ const raw = job && job.verifyRefireCount;
27
+ const count = Number.isInteger(raw) && raw > 0 ? raw : 0;
28
+ const nextCount = count + 1;
29
+ const effectiveCap = Math.max(0, cap);
30
+ if (nextCount <= effectiveCap) return { action: 'refire', nextCount };
31
+ const vr = verifyResult || {};
32
+ return {
33
+ action: 'park',
34
+ nextCount,
35
+ verdict: 'refire_capped',
36
+ reason: `re-fire cap: ${nextCount} consecutive post-run downgrades (${vr.verdict}: ${vr.reason})`,
37
+ };
38
+ }
39
+
40
+ module.exports = { MAX_CONSECUTIVE_VERIFY_REFIRES, decideVerifyDowngrade };
@@ -5,7 +5,7 @@
5
5
  * notice (this PRD).
6
6
  *
7
7
  * Before this module, a park into `needs_review` told the authoring Epic
8
- * immediately (scheduler.cjs's notifyNeedsReview), BEFORE the scheduler's own
8
+ * immediately at park time, BEFORE the scheduler's own
9
9
  * recovery ladder (mechanical re-integration, resume, quarantine, auto-fix,
10
10
  * gate re-run, bounded auto-resolve) ever got a chance to run. Most parks
11
11
  * heal on their own, so that immediate message was usually a false alarm.
@@ -78,8 +78,7 @@ module.exports = {
78
78
  // this age is escalated: warn-logged naming project + slug + age, and
79
79
  // surfaced distinctly on Home (see homeNeedsYou.ts's matching constant)
80
80
  // so it cannot be stranded indefinitely with nothing looking at it — see
81
- // findStaleQuarantinedJobs in scheduler.cjs. Overridable via
82
- // SM_QUARANTINE_ESCALATE_HOURS for testing/tuning.
81
+ // findStaleQuarantinedJobs in scheduler.cjs.
83
82
  QUARANTINE_ESCALATE_MS: 24 * 60 * 60_000,
84
83
 
85
84
  // A project with pending work, nothing of its own running, and an oldest
@@ -104,7 +104,6 @@ function scheduledPlansRoot() { return assertNotLiveRoot(path.join(schedulerHome
104
104
  function runsDir() { return assertNotLiveRoot(path.join(scheduledPlansRoot(), 'runs'), 'runsDir'); }
105
105
  function prdsRoot() { return assertNotLiveRoot(path.join(scheduledPlansRoot(), 'prds'), 'prdsRoot'); }
106
106
  function machineStatePath() { return assertNotLiveRoot(path.join(schedulerHome(), 'scheduler-machine.json'), 'machineStatePath'); }
107
- function legacyQueuePath() { return assertNotLiveRoot(path.join(scheduledPlansRoot(), 'queue.json'), 'legacyQueuePath'); }
108
107
  function schedulerStatePath() { return assertNotLiveRoot(path.join(schedulerHome(), 'scheduler-state.json'), 'schedulerStatePath'); }
109
108
  function heartbeatPath() { return assertNotLiveRoot(path.join(schedulerHome(), 'scheduler-heartbeat.log'), 'heartbeatPath'); }
110
109
  function sessionSlotsConfigPath() { return assertNotLiveRoot(path.join(schedulerHome(), 'session-slots-config.json'), 'sessionSlotsConfigPath'); }
@@ -193,7 +192,6 @@ module.exports = {
193
192
  runsDir,
194
193
  prdsRoot,
195
194
  machineStatePath,
196
- legacyQueuePath,
197
195
  schedulerStatePath,
198
196
  heartbeatPath,
199
197
  sessionSlotsConfigPath,
@@ -32,7 +32,7 @@ const atomicFs = require('../util/atomicFs.cjs');
32
32
  const { isProvablyDead, DEFAULT_GRACE_MS } = require('./reservationExpiry.cjs');
33
33
 
34
34
  const MIN_SLOTS = 0;
35
- const MAX_SLOTS = 10;
35
+ const MAX_SLOTS = 32;
36
36
  const DEFAULT_SLOTS = 5;
37
37
 
38
38