claude-code-session-manager 0.76.0 → 0.78.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 (123) hide show
  1. package/dist/assets/{AgentLibrary-CBx9l4zN.js → AgentLibrary-13pfo8uY.js} +2 -2
  2. package/dist/assets/{DataModel-Bf0EIE_t.js → DataModel-SUyQbFlg.js} +1 -1
  3. package/dist/assets/{History-CpdtWhC8.js → History-2GJMS703.js} +2 -2
  4. package/dist/assets/{Hooks-DyUbMDmg.js → Hooks-DM2nS3RT.js} +2 -2
  5. package/dist/assets/{HostBilko-By-wIpry.js → HostBilko-BLeC-lpp.js} +1 -1
  6. package/dist/assets/{Library-CQmo4QVC.js → Library-BaRkU9m0.js} +1 -1
  7. package/dist/assets/{ListDetail-BQMd6NOm.js → ListDetail-D5scjSKq.js} +1 -1
  8. package/dist/assets/{MarkdownEditor-DEp43FXX.js → MarkdownEditor-B1lgAo9T.js} +1 -1
  9. package/dist/assets/{McpServers-CLarzwqA.js → McpServers-BzyThQSM.js} +1 -1
  10. package/dist/assets/{Memory-B0sCdIy1.js → Memory-7UdaOTtl.js} +6 -6
  11. package/dist/assets/{Panel-BhWPVOCD.js → Panel-JbTMaOPq.js} +1 -1
  12. package/dist/assets/{Permissions-Ddlq8T_O.js → Permissions-UBam0bJG.js} +2 -2
  13. package/dist/assets/{Plugins-D2oA_2Jl.js → Plugins-B3gUDkeb.js} +2 -2
  14. package/dist/assets/{ProvenanceBadge-DgAgavUM.js → ProvenanceBadge-CeHOub7m.js} +1 -1
  15. package/dist/assets/SaveBar-BcvQEq6h.js +1 -0
  16. package/dist/assets/Scheduler-Dc5qiP24.js +14 -0
  17. package/dist/assets/{ScopeSwitcher-C_zWEtIl.js → ScopeSwitcher-BvGQmw4Y.js} +1 -1
  18. package/dist/assets/{Settings-2Vx3X5SI.js → Settings-C2dEFb-v.js} +1 -1
  19. package/dist/assets/{SkillReferenceGraph-BDEUjlTQ.js → SkillReferenceGraph-CIwlosBc.js} +1 -1
  20. package/dist/assets/{Skills-Cmrz_LeN.js → Skills-C0GzzVrQ.js} +2 -2
  21. package/dist/assets/{SystemPrompt-DVA1eYDP.js → SystemPrompt-mtGPK8zo.js} +1 -1
  22. package/dist/assets/TagLibrary-DX54-mpd.js +1 -0
  23. package/dist/assets/{TiptapBody-DmPc3amD.js → TiptapBody-yADC2RWE.js} +1 -1
  24. package/dist/assets/{Toggle-zfd5LJkK.js → Toggle-CRxaCYLI.js} +1 -1
  25. package/dist/assets/{index-DIjnPkRN.css → index-CYhdtisq.css} +1 -1
  26. package/dist/assets/{index-B_4PNh9T.js → index-D6ymGESc.js} +469 -469
  27. package/dist/assets/{settingsSchema-B9es6fdA.js → settingsSchema-TtMvT5Sx.js} +1 -1
  28. package/dist/index.html +2 -2
  29. package/package.json +8 -2
  30. package/plugins/session-manager-dev/skills/develop/standards.md +1 -1
  31. package/scripts/project-pages-logic/dist/logic.cjs +4709 -0
  32. package/scripts/render-project-pages/dist/renderer.cjs +18900 -0
  33. package/scripts/render-project-pages.cjs +70 -0
  34. package/scripts/scheduler-mcp-server.cjs +115 -1
  35. package/scripts/validate-project-pages-summary.cjs +62 -0
  36. package/src/main/__tests__/agentModelResolve.test.cjs +66 -0
  37. package/src/main/__tests__/health-delegation-chain.test.cjs +2 -1
  38. package/src/main/__tests__/prdAgentType.test.cjs +103 -0
  39. package/src/main/__tests__/prdCreate.test.cjs +138 -0
  40. package/src/main/__tests__/prdFrontmatterAgentType.test.cjs +117 -0
  41. package/src/main/__tests__/prdFrontmatterQuietMachine.test.cjs +108 -0
  42. package/src/main/__tests__/projectHomeAdminRoutes.test.cjs +485 -0
  43. package/src/main/__tests__/projectPages.test.cjs +73 -1
  44. package/src/main/__tests__/rcaReport.test.cjs +54 -0
  45. package/src/main/__tests__/runVerify.test.cjs +94 -0
  46. package/src/main/__tests__/scheduler-autofix-select.test.cjs +43 -0
  47. package/src/main/__tests__/scheduler-bash-timeout-env.test.cjs +103 -0
  48. package/src/main/__tests__/scheduler-effective-concurrency.test.cjs +10 -0
  49. package/src/main/__tests__/scheduler-foreign-wip-manifest.test.cjs +78 -0
  50. package/src/main/__tests__/scheduler-inplace-salvage.test.cjs +242 -0
  51. package/src/main/__tests__/scheduler-investigation-prompt.test.cjs +31 -0
  52. package/src/main/__tests__/scheduler-launch-failure.test.cjs +201 -0
  53. package/src/main/__tests__/scheduler-leftover-fields.test.cjs +52 -0
  54. package/src/main/__tests__/scheduler-looks-done.test.cjs +241 -0
  55. package/src/main/__tests__/scheduler-prd-persona-spawn.test.cjs +135 -0
  56. package/src/main/__tests__/scheduler-quiet-machine-lease.test.cjs +222 -0
  57. package/src/main/__tests__/scheduler-reap-dead-running-jobs.test.cjs +147 -0
  58. package/src/main/__tests__/scheduler-shared-tree-guard.test.cjs +212 -0
  59. package/src/main/__tests__/scheduler-worktree-cap-defer.test.cjs +194 -0
  60. package/src/main/__tests__/seedAgentPersonas.test.cjs +75 -14
  61. package/src/main/config.cjs +4 -1
  62. package/src/main/index.cjs +16 -4
  63. package/src/main/ipcSchemas.cjs +53 -0
  64. package/src/main/lib/__tests__/childWithLog.test.cjs +78 -0
  65. package/src/main/lib/__tests__/delegationReadiness.test.cjs +197 -2
  66. package/src/main/lib/__tests__/epicWorktreeMint.test.cjs +4 -2
  67. package/src/main/lib/__tests__/fixChainDepth.test.cjs +40 -0
  68. package/src/main/lib/__tests__/gitWorktree.test.cjs +277 -4
  69. package/src/main/lib/__tests__/gitWorktreeSalvageDelta.test.cjs +153 -0
  70. package/src/main/lib/__tests__/jobWorktree.test.cjs +5 -3
  71. package/src/main/lib/__tests__/landedSinceRun.test.cjs +73 -0
  72. package/src/main/lib/__tests__/launchFailure.test.cjs +220 -0
  73. package/src/main/lib/__tests__/mcpToolCatalog.test.cjs +1 -0
  74. package/src/main/lib/__tests__/opsOwnership.test.cjs +7 -0
  75. package/src/main/lib/__tests__/prdDeclaredPaths.test.cjs +82 -0
  76. package/src/main/lib/__tests__/queueHealth.test.cjs +58 -0
  77. package/src/main/lib/__tests__/quietMachineLease.test.cjs +39 -0
  78. package/src/main/lib/__tests__/reaperHelpers.test.cjs +22 -1
  79. package/src/main/lib/__tests__/schedulerBatchLaunchHold.test.cjs +125 -0
  80. package/src/main/lib/__tests__/schedulerBatchQuietMachine.test.cjs +109 -0
  81. package/src/main/lib/__tests__/schedulerMcpServerHeadlessRefusal.test.cjs +71 -0
  82. package/src/main/lib/__tests__/schedulerMcpServerProjectHome.test.cjs +350 -0
  83. package/src/main/lib/agentModelResolve.cjs +58 -0
  84. package/src/main/lib/childWithLog.cjs +40 -5
  85. package/src/main/lib/claudeBin.cjs +54 -1
  86. package/src/main/lib/definitionOfDone.cjs +3 -2
  87. package/src/main/lib/delegationReadiness.cjs +115 -9
  88. package/src/main/lib/epicWorktreeMint.cjs +5 -2
  89. package/src/main/lib/fixChainDepth.cjs +45 -0
  90. package/src/main/lib/gitWorktree.cjs +464 -19
  91. package/src/main/lib/jobWorktree.cjs +1 -0
  92. package/src/main/lib/landedSinceRun.cjs +55 -0
  93. package/src/main/lib/launchFailure.cjs +357 -0
  94. package/src/main/lib/mcpToolCatalog.cjs +87 -2
  95. package/src/main/lib/opsOwnership.cjs +12 -0
  96. package/src/main/lib/prdAgentType.cjs +84 -0
  97. package/src/main/lib/prdCreate.cjs +57 -1
  98. package/src/main/lib/prdDeclaredPaths.cjs +70 -0
  99. package/src/main/lib/prdFrontmatter.cjs +17 -3
  100. package/src/main/lib/projectHomeAdminRoutes.cjs +402 -0
  101. package/src/main/lib/projectPageSummarySchema.cjs +181 -0
  102. package/src/main/lib/queueHealth.cjs +38 -0
  103. package/src/main/lib/queueStore.cjs +9 -2
  104. package/src/main/lib/quietMachineLease.cjs +48 -0
  105. package/src/main/lib/rcaReport.cjs +53 -3
  106. package/src/main/lib/reaperHelpers.cjs +18 -1
  107. package/src/main/lib/scheduleJobSchema.cjs +31 -0
  108. package/src/main/lib/scheduleJobTransitions.cjs +6 -2
  109. package/src/main/lib/schedulerBatch.cjs +133 -29
  110. package/src/main/lib/schedulerConfig.cjs +19 -0
  111. package/src/main/projectPages.cjs +160 -2
  112. package/src/main/runVerify.cjs +50 -9
  113. package/src/main/scheduler/prdParser.cjs +18 -1
  114. package/src/main/scheduler.cjs +1371 -97
  115. package/src/main/seedAgentPersonas.cjs +62 -21
  116. package/src/main/templates/project-pages-catalog.json +741 -0
  117. package/src/main/templates/project-pages-pipeline.md +417 -0
  118. package/src/preload/api.d.ts +126 -6
  119. package/src/preload/index.cjs +8 -0
  120. package/src/seed/agents/project-home-builder.md +59 -0
  121. package/dist/assets/SaveBar-Qvc4Ek-H.js +0 -1
  122. package/dist/assets/Scheduler-BmYJvNzK.js +0 -14
  123. package/dist/assets/TagLibrary-DYJGAKZu.js +0 -1
@@ -0,0 +1,55 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * landedSinceRun.cjs — widened, path-scoped commit evidence for the reverify
5
+ * self-heal pass (PRD 1102).
6
+ *
7
+ * committedInWindow (scheduler.cjs) only sees commits inside
8
+ * [startedAt, finishedAt+60s] — a commit that lands later (a retry, a
9
+ * sibling run, a human) is invisible to it. landedSinceRun has no upper
10
+ * bound, but narrows the OTHER way that committedInWindow is dangerously
11
+ * broad: it is scoped to paths the PRD itself declares, so an unrelated
12
+ * commit elsewhere in the repo is not credited to this job (see
13
+ * scheduler.cjs's healRefusalReason for why repo-wide, unscoped evidence is
14
+ * not attribution).
15
+ *
16
+ * Pure git wrapper — no fetch, no scheduler state. Callers that need remote
17
+ * commits visible (e.g. a job that committed in a since-removed worktree)
18
+ * must call scheduler.cjs's fetchAllRefs(cwd) first, same as
19
+ * committedInWindow's own callers do.
20
+ */
21
+
22
+ const { execFile } = require('node:child_process');
23
+
24
+ const LANDED_SINCE_RUN_TIMEOUT_MS = 10_000;
25
+
26
+ /**
27
+ * Commits in `cwd` since `sinceIso` (no upper bound) that touch any of
28
+ * `paths`. Never throws — git-unavailable, a non-repo cwd, or an empty
29
+ * `paths` list all resolve to `[]` rather than fabricating evidence.
30
+ *
31
+ * @param {string} cwd
32
+ * @param {string} sinceIso
33
+ * @param {string[]} paths
34
+ * @param {{ timeoutMs?: number }} [opts]
35
+ * @returns {Promise<string[]>} full commit SHAs, newest first
36
+ */
37
+ function landedSinceRun(cwd, sinceIso, paths, { timeoutMs = LANDED_SINCE_RUN_TIMEOUT_MS } = {}) {
38
+ return new Promise((resolve) => {
39
+ if (!cwd || !sinceIso || !Array.isArray(paths) || paths.length === 0) {
40
+ resolve([]);
41
+ return;
42
+ }
43
+ execFile(
44
+ 'git',
45
+ ['-C', cwd, 'log', '--all', `--since=${sinceIso}`, '--format=%H', '--', ...paths],
46
+ { timeout: timeoutMs, windowsHide: true },
47
+ (err, stdout) => {
48
+ if (err) { resolve([]); return; }
49
+ resolve(String(stdout || '').trim().split('\n').filter(Boolean));
50
+ },
51
+ );
52
+ });
53
+ }
54
+
55
+ module.exports = { landedSinceRun, LANDED_SINCE_RUN_TIMEOUT_MS };
@@ -0,0 +1,357 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * launchFailure.cjs — detects a headless `claude -p` run that NEVER RAN, and
5
+ * models the per-persona launch circuit breaker the scheduler routes on.
6
+ *
7
+ * Incident (GitHub issue #11, 2026-09-02, macOS): the installed Claude CLI
8
+ * sent `thinking.type.enabled` to a model that only accepts
9
+ * `thinking.type.adaptive`. The API answered HTTP 400 on the very first
10
+ * request, so every job did zero turns, spent zero output tokens, wrote zero
11
+ * files, exited 1 in ~25 s — and was recorded as `failed` with `error: null`,
12
+ * indistinguishable from a real implementation failure. The auto-fix
13
+ * investigation probe then launched with the same CLI and died the same way.
14
+ * 12 of 41 transcripts in one project over a month were this signature.
15
+ *
16
+ * Two facts this module makes first-class:
17
+ *
18
+ * 1. A NON-RUN is not a failure of the PRD. `classifyLaunchFailure` is
19
+ * deliberately narrow — it only fires when the transcript's `result`
20
+ * event shows no real turn (num_turns ≤ 1 AND output_tokens = 0) AND the
21
+ * result text carries the CLI's literal `API Error:` prefix. Anything
22
+ * that did a turn, or failed without the API marker, is somebody else's
23
+ * classification (rate-limit, network, transient, verifier).
24
+ *
25
+ * 2. The environment is broken, not the job — so the scheduler must stop
26
+ * re-dispatching identical launches (each one is a wasted 25 s + a
27
+ * misleading `failed` row + a doomed investigation) while still
28
+ * self-healing the moment the environment is fixed. That is a circuit
29
+ * breaker keyed by the launch persona (`agentType` → model): closed
30
+ * (normal) → open (blocked, exponential backoff) → half-open (exactly one
31
+ * probe job goes through) → closed again on a real turn. A CLI version
32
+ * change (the actual fix for the incident: `claude update`) short-circuits
33
+ * the backoff so the queue resumes on the next tick, not the next hour.
34
+ *
35
+ * Pure and Electron-free: every function here takes plain values (a parsed
36
+ * result event, a block record, `now`) so the whole state machine is
37
+ * unit-testable without a spawn. scheduler.cjs owns the I/O around it.
38
+ */
39
+
40
+ const fs = require('node:fs');
41
+ const path = require('node:path');
42
+ const { readTail } = require('./fileTail.cjs');
43
+
44
+ const LAUNCH_FAILURE_KINDS = Object.freeze({
45
+ /** HTTP 400 naming a thinking/effort/config parameter the model rejects — the issue-#11 signature. */
46
+ MODEL_CONFIG_REJECTED: 'model_config_rejected',
47
+ /** Any other HTTP 400 on the first request (malformed request body, unsupported flag combo). */
48
+ BAD_REQUEST: 'bad_request',
49
+ /** HTTP 401/403 — the CLI's credentials are missing, expired, or lack access to the model. */
50
+ AUTH_FAILED: 'auth_failed',
51
+ /** HTTP 404 that names the model — the pinned `--model` does not exist for this account/CLI. */
52
+ MODEL_NOT_FOUND: 'model_not_found',
53
+ /** HTTP 5xx / 529 / "Overloaded" — the API itself is unavailable right now. */
54
+ API_OVERLOADED: 'api_overloaded',
55
+ /** Any other first-request API error. */
56
+ API_ERROR: 'api_error',
57
+ });
58
+
59
+ /** Tail bytes scanned for the `result` event — same budget classifyRunOutcome uses. */
60
+ const RESULT_TAIL_BYTES = 65536;
61
+
62
+ /**
63
+ * Parse the LAST `{"type":"result",...}` stream-json event out of a log tail.
64
+ * Returns a flat, typed summary or null when no result event is present
65
+ * (the process died before the harness could emit one — that is
66
+ * reaperHelpers' `no_result`, not a launch failure).
67
+ */
68
+ function parseResultEvent(text) {
69
+ if (!text) return null;
70
+ let last = null;
71
+ for (const line of String(text).split('\n')) {
72
+ const t = line.trim();
73
+ if (!t.startsWith('{') || !t.includes('"type":"result"')) continue;
74
+ try {
75
+ const obj = JSON.parse(t);
76
+ if (obj && obj.type === 'result') last = obj;
77
+ } catch { /* partial line at the tail boundary */ }
78
+ }
79
+ if (!last) return null;
80
+ const usage = last.usage && typeof last.usage === 'object' ? last.usage : {};
81
+ const num = (v) => (typeof v === 'number' && Number.isFinite(v) ? v : null);
82
+ return {
83
+ subtype: typeof last.subtype === 'string' ? last.subtype : '',
84
+ isError: last.is_error === true,
85
+ numTurns: num(last.num_turns),
86
+ outputTokens: num(usage.output_tokens),
87
+ inputTokens: num(usage.input_tokens),
88
+ apiErrorStatus: num(last.api_error_status),
89
+ totalCostUsd: num(last.total_cost_usd),
90
+ durationMs: num(last.duration_ms),
91
+ terminalReason: typeof last.terminal_reason === 'string' ? last.terminal_reason : null,
92
+ resultText: typeof last.result === 'string' ? last.result : '',
93
+ };
94
+ }
95
+
96
+ function readResultEvent(logPath) {
97
+ try {
98
+ return parseResultEvent(readTail(logPath, RESULT_TAIL_BYTES));
99
+ } catch {
100
+ return null;
101
+ }
102
+ }
103
+
104
+ /**
105
+ * Pull the human-readable message out of the CLI's `API Error: <status> <json>`
106
+ * text. The body nests unpredictably (`{"error":{"message":...}}`, or
107
+ * `{"detail":{"error":"<json string with message>"}}` as in issue #11), so
108
+ * this walks any `message`/`error` chain it can parse and falls back to the
109
+ * raw text, bounded.
110
+ */
111
+ function extractApiMessage(text) {
112
+ const raw = String(text || '').trim();
113
+ const jsonStart = raw.indexOf('{');
114
+ if (jsonStart >= 0) {
115
+ let node;
116
+ try { node = JSON.parse(raw.slice(jsonStart)); } catch { node = null; }
117
+ let depth = 0;
118
+ while (node && depth < 6) {
119
+ depth += 1;
120
+ if (typeof node === 'string') {
121
+ const s = node.trim();
122
+ if (s.startsWith('{')) {
123
+ try { node = JSON.parse(s); continue; } catch { /* not JSON — it's the message */ }
124
+ }
125
+ return s.slice(0, 400);
126
+ }
127
+ if (typeof node !== 'object') break;
128
+ if (typeof node.message === 'string') return node.message.slice(0, 400);
129
+ node = node.error ?? node.detail ?? null;
130
+ }
131
+ }
132
+ return raw.slice(0, 400);
133
+ }
134
+
135
+ /**
136
+ * classifyLaunchFailure(result) → null | { kind, httpStatus, message }
137
+ *
138
+ * `result` is parseResultEvent()'s output. Returns null for every run that
139
+ * did real work (or failed for a reason that is not a first-request API
140
+ * rejection) — the narrowness is the point; see the module header.
141
+ * HTTP 429 is excluded: the rate-limit pause path owns it.
142
+ */
143
+ function classifyLaunchFailure(result) {
144
+ if (!result) return null;
145
+ const numTurns = result.numTurns ?? 0;
146
+ const outputTokens = result.outputTokens ?? 0;
147
+ if (numTurns > 1 || outputTokens > 0) return null;
148
+ const text = result.resultText || '';
149
+ const marker = /API Error:?\s*(\d{3})?/i.exec(text);
150
+ if (!marker && !(result.isError && result.apiErrorStatus)) return null;
151
+ const httpStatus = (marker && marker[1] ? Number(marker[1]) : null) ?? result.apiErrorStatus ?? null;
152
+ if (httpStatus === 429) return null;
153
+ const message = extractApiMessage(text.replace(/^.*?API Error:?\s*(\d{3})?\s*/i, '')) || text.slice(0, 400);
154
+ let kind;
155
+ if (httpStatus === 400) {
156
+ kind = /thinking|not supported for this model|output_config|effort/i.test(text)
157
+ ? LAUNCH_FAILURE_KINDS.MODEL_CONFIG_REJECTED
158
+ : LAUNCH_FAILURE_KINDS.BAD_REQUEST;
159
+ } else if (httpStatus === 401 || httpStatus === 403) {
160
+ kind = LAUNCH_FAILURE_KINDS.AUTH_FAILED;
161
+ } else if (httpStatus === 404 && /model/i.test(text)) {
162
+ kind = LAUNCH_FAILURE_KINDS.MODEL_NOT_FOUND;
163
+ } else if ((httpStatus !== null && httpStatus >= 500) || /overloaded/i.test(text)) {
164
+ kind = LAUNCH_FAILURE_KINDS.API_OVERLOADED;
165
+ } else {
166
+ kind = LAUNCH_FAILURE_KINDS.API_ERROR;
167
+ }
168
+ return { kind, httpStatus, message };
169
+ }
170
+
171
+ /** Did this run get at least one real model turn? (The half-open probe's "close the breaker" evidence.) */
172
+ function resultShowsRealTurn(result) {
173
+ if (!result) return false;
174
+ return (result.numTurns ?? 0) > 1 || (result.outputTokens ?? 0) > 0;
175
+ }
176
+
177
+ // ─── Circuit breaker ────────────────────────────────────────────────────────
178
+
179
+ /** After this many consecutive failed probes the block stays open until the CLI version changes or a human resets it. */
180
+ const LAUNCH_BLOCK_MAX_ATTEMPTS = 8;
181
+ /** A probe that has not reported back in this long is presumed dead; the next tick may probe again. */
182
+ const LAUNCH_PROBE_STALE_MS = 30 * 60_000;
183
+
184
+ const BACKOFF_BASE_MS = {
185
+ [LAUNCH_FAILURE_KINDS.MODEL_CONFIG_REJECTED]: 5 * 60_000,
186
+ [LAUNCH_FAILURE_KINDS.BAD_REQUEST]: 5 * 60_000,
187
+ [LAUNCH_FAILURE_KINDS.AUTH_FAILED]: 5 * 60_000,
188
+ [LAUNCH_FAILURE_KINDS.MODEL_NOT_FOUND]: 10 * 60_000,
189
+ [LAUNCH_FAILURE_KINDS.API_OVERLOADED]: 60_000,
190
+ [LAUNCH_FAILURE_KINDS.API_ERROR]: 2 * 60_000,
191
+ };
192
+ const BACKOFF_CAP_MS = 60 * 60_000;
193
+
194
+ /** Exponential backoff for the Nth consecutive failure (attempts ≥ 1), capped at one hour. */
195
+ function backoffMsFor(kind, attempts) {
196
+ const base = BACKOFF_BASE_MS[kind] ?? BACKOFF_BASE_MS[LAUNCH_FAILURE_KINDS.API_ERROR];
197
+ const n = Math.max(0, (attempts ?? 1) - 1);
198
+ return Math.min(BACKOFF_CAP_MS, base * 2 ** n);
199
+ }
200
+
201
+ /**
202
+ * Environment the scheduler applies to a launch as a DEGRADED-MODE
203
+ * mitigation for a kind, or null when there is none. For the issue-#11
204
+ * signature the CLI's `MAX_THINKING_TOKENS=0` switches extended thinking off
205
+ * entirely, so an older CLI stops sending the rejected `thinking` block at
206
+ * all — the run proceeds without thinking rather than not at all. Harmless
207
+ * if the CLI ignores it (the probe simply fails again and the backoff holds).
208
+ */
209
+ function mitigationEnvFor(kind) {
210
+ if (kind === LAUNCH_FAILURE_KINDS.MODEL_CONFIG_REJECTED) return { MAX_THINKING_TOKENS: '0' };
211
+ return null;
212
+ }
213
+
214
+ /** Circuit-breaker key: the launch persona, because `agentType` is what selects the `--model`. */
215
+ function launchBlockKeyFor(job) {
216
+ return (job && typeof job.agentType === 'string' && job.agentType) || 'default';
217
+ }
218
+
219
+ /** Operator-facing explanation + the one action that clears the condition. */
220
+ function launchFailureHint(kind, { claudeVersion, mitigationInForce = false } = {}) {
221
+ const ver = claudeVersion ? `installed Claude CLI ${claudeVersion}` : 'installed Claude CLI';
222
+ switch (kind) {
223
+ case LAUNCH_FAILURE_KINDS.MODEL_CONFIG_REJECTED:
224
+ return mitigationInForce
225
+ ? `The ${ver} sends a thinking parameter this model rejects, and disabling thinking (MAX_THINKING_TOKENS=0) did not get past it. Update the CLI (\`claude update\` or \`npm i -g @anthropic-ai/claude-code@latest\`); the queue resumes automatically when the version changes.`
226
+ : `The ${ver} sends a thinking parameter this model rejects (HTTP 400 on the first request — no work was attempted). Update the CLI (\`claude update\` or \`npm i -g @anthropic-ai/claude-code@latest\`); until then jobs re-probe with thinking disabled, and the queue resumes automatically when the version changes.`;
227
+ case LAUNCH_FAILURE_KINDS.AUTH_FAILED:
228
+ return `The API rejected the CLI's credentials (HTTP 401/403) before any work started. Run \`claude login\` (or check the model is enabled for this account), then press Retry now.`;
229
+ case LAUNCH_FAILURE_KINDS.MODEL_NOT_FOUND:
230
+ return `The pinned --model does not exist for the ${ver} / this account (HTTP 404). Fix the persona's \`model:\` in the Agent Library or update the CLI, then press Retry now.`;
231
+ case LAUNCH_FAILURE_KINDS.API_OVERLOADED:
232
+ return 'The API is overloaded or unavailable (HTTP 5xx/529). Nothing is wrong with the PRD; the scheduler re-probes with backoff and resumes on its own.';
233
+ case LAUNCH_FAILURE_KINDS.BAD_REQUEST:
234
+ return `The API rejected the launch request (HTTP 400) before any work started. Check the ${ver} against the pinned model, then press Retry now.`;
235
+ default:
236
+ return 'The first API request of the run failed before any work started. The scheduler re-probes with backoff; press Retry now to probe immediately.';
237
+ }
238
+ }
239
+
240
+ /**
241
+ * armLaunchBlock(prev, failure) → block
242
+ *
243
+ * Opens (or re-opens with a longer backoff) the breaker for one key after a
244
+ * launch failure. `prev` is the existing block for the key or null; a
245
+ * different `kind` than before restarts the attempt count, the same kind
246
+ * escalates it. Beyond LAUNCH_BLOCK_MAX_ATTEMPTS `until` becomes null:
247
+ * blocked indefinitely — only a CLI version change or a human Retry clears
248
+ * it, and the UI says so.
249
+ */
250
+ function armLaunchBlock(prev, { kind, httpStatus, message, now, claudeVersion, slug, runId, mitigationApplied = false }) {
251
+ const sameKind = prev && prev.kind === kind;
252
+ const attempts = sameKind ? (prev.attempts ?? 0) + 1 : 1;
253
+ const exhausted = attempts >= LAUNCH_BLOCK_MAX_ATTEMPTS;
254
+ const mitigationEnv = mitigationEnvFor(kind);
255
+ return {
256
+ kind,
257
+ httpStatus: httpStatus ?? null,
258
+ message: String(message || '').slice(0, 400),
259
+ hint: launchFailureHint(kind, { claudeVersion, mitigationInForce: mitigationApplied }),
260
+ since: sameKind && prev.since ? prev.since : new Date(now).toISOString(),
261
+ lastAt: new Date(now).toISOString(),
262
+ until: exhausted ? null : new Date(now + backoffMsFor(kind, attempts)).toISOString(),
263
+ attempts,
264
+ exhausted,
265
+ claudeVersion: claudeVersion ?? null,
266
+ lastSlug: slug ?? null,
267
+ lastRunId: runId ?? null,
268
+ mitigationEnv,
269
+ mitigationApplied,
270
+ probing: null,
271
+ };
272
+ }
273
+
274
+ /**
275
+ * evaluateLaunchGate(block, { now, claudeVersion }) →
276
+ * { state: 'open' | 'blocked' | 'probe', reason }
277
+ *
278
+ * 'open' — no block, or the CLI version changed since it was armed (the
279
+ * caller should drop the block: the environment was replaced).
280
+ * 'blocked' — inside the backoff window, exhausted, or a probe is already in
281
+ * flight. `reason` is the row-level hold text.
282
+ * 'probe' — backoff elapsed: let exactly ONE job through as the probe.
283
+ */
284
+ function evaluateLaunchGate(block, { now, claudeVersion } = {}) {
285
+ if (!block) return { state: 'open', reason: null };
286
+ if (claudeVersion && block.claudeVersion && claudeVersion !== block.claudeVersion) {
287
+ return { state: 'open', reason: `cli-version-changed (${block.claudeVersion} → ${claudeVersion})` };
288
+ }
289
+ const t = typeof now === 'number' ? now : Date.now();
290
+ if (block.probing && block.probing.at) {
291
+ const age = t - Date.parse(block.probing.at);
292
+ if (Number.isFinite(age) && age >= 0 && age < LAUNCH_PROBE_STALE_MS) {
293
+ return { state: 'blocked', reason: `launch blocked (${block.kind}) — probe ${block.probing.slug} in flight` };
294
+ }
295
+ }
296
+ if (block.until === null || block.until === undefined) {
297
+ return { state: 'blocked', reason: `launch blocked (${block.kind}) after ${block.attempts} failed probe(s) — ${block.hint}` };
298
+ }
299
+ const until = Date.parse(block.until);
300
+ if (Number.isFinite(until) && t < until) {
301
+ const mins = Math.max(1, Math.round((until - t) / 60_000));
302
+ return { state: 'blocked', reason: `launch blocked (${block.kind}) — re-probe in ${mins} min. ${block.hint}` };
303
+ }
304
+ return { state: 'probe', reason: `launch probe (${block.kind}) — attempt ${(block.attempts ?? 0) + 1}` };
305
+ }
306
+
307
+ /**
308
+ * Terminal-reason taxonomy for a finalized job row (issue #11 list A2). A
309
+ * closed set so operators never have to open a transcript to tell a
310
+ * non-start from an implementation failure from a verifier downgrade.
311
+ */
312
+ function deriveTerminalReason({ effectiveStatus, exitCode, verifyResult, sigtermOverride, worktreeIntegrationFailure }) {
313
+ if (worktreeIntegrationFailure) return 'worktree_integration_failed';
314
+ if (effectiveStatus === 'completed') return 'completed';
315
+ if (sigtermOverride) return 'signal_kill_with_commit';
316
+ if (exitCode === 143 || exitCode === 137) return 'signal_kill';
317
+ if (typeof exitCode === 'number' && exitCode !== 0) return `impl_failed:exit_${exitCode}`;
318
+ if (effectiveStatus === 'needs_review' && verifyResult && verifyResult.verdict) return `verifier:${verifyResult.verdict}`;
319
+ return effectiveStatus || 'unknown';
320
+ }
321
+
322
+ /**
323
+ * Write the per-run `<slug>.outcome.json` sidecar (issue #11 list B5): the
324
+ * handful of numbers that make fleet health computable without parsing
325
+ * transcripts. Best-effort, never throws.
326
+ */
327
+ function writeOutcomeSidecar(runDir, slug, outcome) {
328
+ if (!runDir || !slug) return null;
329
+ const p = path.join(runDir, `${slug}.outcome.json`);
330
+ try {
331
+ const tmp = `${p}.tmp`;
332
+ fs.writeFileSync(tmp, JSON.stringify({ slug, writtenAt: new Date().toISOString(), ...outcome }, null, 2));
333
+ fs.renameSync(tmp, p);
334
+ return p;
335
+ } catch {
336
+ return null;
337
+ }
338
+ }
339
+
340
+ module.exports = {
341
+ LAUNCH_FAILURE_KINDS,
342
+ LAUNCH_BLOCK_MAX_ATTEMPTS,
343
+ LAUNCH_PROBE_STALE_MS,
344
+ parseResultEvent,
345
+ readResultEvent,
346
+ extractApiMessage,
347
+ classifyLaunchFailure,
348
+ resultShowsRealTurn,
349
+ backoffMsFor,
350
+ mitigationEnvFor,
351
+ launchBlockKeyFor,
352
+ launchFailureHint,
353
+ armLaunchBlock,
354
+ evaluateLaunchGate,
355
+ deriveTerminalReason,
356
+ writeOutcomeSidecar,
357
+ };
@@ -23,7 +23,7 @@ const { z } = require('zod');
23
23
 
24
24
  const CatalogEntrySchema = z.object({
25
25
  name: z.string().min(1),
26
- group: z.enum(['scheduler', 'chat', 'feedback', 'help']),
26
+ group: z.enum(['scheduler', 'chat', 'feedback', 'help', 'project-home']),
27
27
  purpose: z.string().min(1),
28
28
  whenToUse: z.string().min(1),
29
29
  whenNotToUse: z.string().min(1),
@@ -71,7 +71,11 @@ const MCP_TOOL_CATALOG = [
71
71
  + '`cwd` is OPTIONAL when called from inside an Epic session (chat or terminal, '
72
72
  + 'including from inside that Epic\'s own git worktree pwd): the server resolves '
73
73
  + 'the real project from the calling session\'s sourcePromptId/originClaudeSessionId, '
74
- + 'never from a worktree\'s own possibly-stale active-index.json snapshot.',
74
+ + 'never from a worktree\'s own possibly-stale active-index.json snapshot. '
75
+ + '`tag` and `agentType` are two DIFFERENT, independent fields — `tag` is WHAT KIND '
76
+ + 'of work this PRD is (feature/bug/build/...); `agentType` is WHO executes it (a '
77
+ + 'persona name, e.g. "dev-lead"). `agentType` defaults to "dev-lead" when omitted, '
78
+ + 'and is rejected at write time if it does not name a real persona file.',
75
79
  whenToUse: 'Use whenever new work should be queued into an already-approved Epic — this is the /develop path.',
76
80
  whenNotToUse: 'TWO DISTINCT FAILURE MODES if this tool is not usable — do not conflate them: '
77
81
  + '(a) this tool call is PRESENT in your tool list but ERRORS as app-not-running / admin '
@@ -213,6 +217,68 @@ const MCP_TOOL_CATALOG = [
213
217
  + 'the other project has "been fixed" or "is working on it". Report only that the proposal was '
214
218
  + 'delivered.',
215
219
  },
220
+ {
221
+ name: 'project_home_get_contract',
222
+ group: 'project-home',
223
+ purpose: "Fetch the self-sufficient contract for generating this project's Project Home pages via the "
224
+ + "session-manager app's admin API — the protocol steps, the ProjectPageSummary/ProjectPagePicks JSON "
225
+ + 'schemas, the component catalog (all 5 lenses: home, marketing, feature, architecture, brief), the '
226
+ + 'absolute output paths, and the full pipeline spec text. Nothing in the response requires reading '
227
+ + "session-manager's own repo — this is what makes generation work on a machine with only the npm "
228
+ + 'package installed.',
229
+ whenToUse: 'Call this FIRST, before composing any ProjectPageSummary content or picking variants, whenever '
230
+ + 'a project-home-builder session starts generating Project Home pages for a project.',
231
+ whenNotToUse: 'Do not begin composing a ProjectPageSummary or choosing variants before calling this — it '
232
+ + 'must be the FIRST call a project-home-builder session makes; every other project_home_* tool assumes '
233
+ + "you already have this response's schema/catalog/paths in hand.",
234
+ exampleArgs: { cwd: '/home/bilko/Projects/session-manager' },
235
+ notes: "cwd is optional — defaults to the calling session's own project root (SM_PROJECT_ROOT or process.cwd()) when omitted.",
236
+ },
237
+ {
238
+ name: 'project_home_validate_summary',
239
+ group: 'project-home',
240
+ purpose: 'Validate a composed ProjectPageSummary against the schema from project_home_get_contract via the '
241
+ + "session-manager app's admin API, without writing anything to disk.",
242
+ whenToUse: 'Use after composing (or editing) a ProjectPageSummary and before calling project_home_render — '
243
+ + 'fix every returned {field, message} error and re-validate until valid:true.',
244
+ whenNotToUse: 'Do not skip straight to project_home_render on an unvalidated summary — render does '
245
+ + 're-validate server-side and rejects with no writes on failure, but catching errors here first saves a '
246
+ + 'round trip and gives per-field detail sooner.',
247
+ exampleArgs: { cwd: '/home/bilko/Projects/session-manager', summary: { title: 'Example Project' } },
248
+ notes: "cwd is optional — defaults to the calling session's own project root (SM_PROJECT_ROOT or process.cwd()) when omitted.",
249
+ },
250
+ {
251
+ name: 'project_home_render',
252
+ group: 'project-home',
253
+ purpose: 'Render and write all 5 Project Home lenses (home, marketing, feature, architecture, brief) plus '
254
+ + "summary.json/picks.json/manifest.json via the session-manager app's admin API — the only write path "
255
+ + 'for Project Home generation.',
256
+ whenToUse: 'Use once project_home_validate_summary reports valid:true and a variant has been picked for '
257
+ + "every lens/slot in the contract's catalog.",
258
+ whenNotToUse: 'Must NOT be used to fabricate summary content — every field of the summary passed here must '
259
+ + 'trace to something concrete about the real project (an Epic goal, a source file/dir, a convention, a '
260
+ + 'git log entry). This tool re-validates the summary server-side and rejects with no writes on schema '
261
+ + 'failure, but it only checks shape, never truthfulness.',
262
+ exampleArgs: {
263
+ cwd: '/home/bilko/Projects/session-manager',
264
+ summary: { title: 'Example Project' },
265
+ picks: { home: { hero: 'variant-a' } },
266
+ },
267
+ notes: "cwd is optional — defaults to the calling session's own project root (SM_PROJECT_ROOT or process.cwd()) when omitted.",
268
+ },
269
+ {
270
+ name: 'project_home_status',
271
+ group: 'project-home',
272
+ purpose: "Report what already exists for this project's Project Home generation via the session-manager "
273
+ + "app's admin API — summary.json/picks.json/each lens file's existence + mtime, and manifest.json's "
274
+ + 'generatedAt.',
275
+ whenToUse: 'Use before starting generation to see prior state, and again after project_home_render to '
276
+ + 'confirm the new files landed.',
277
+ whenNotToUse: 'Do not treat an all-absent result as an error — a project that has never generated a '
278
+ + 'Project Home page yet still returns a well-formed status with every exists:false rather than failing.',
279
+ exampleArgs: { cwd: '/home/bilko/Projects/session-manager' },
280
+ notes: "cwd is optional — defaults to the calling session's own project root (SM_PROJECT_ROOT or process.cwd()) when omitted.",
281
+ },
216
282
  {
217
283
  name: 'session_manager_help',
218
284
  group: 'help',
@@ -263,6 +329,25 @@ const MCP_RECIPES = [
263
329
  'The call only delivers a PROPOSED session in the receiving project — nothing runs until a human there presses Approve & start; there is no reply channel.',
264
330
  ],
265
331
  },
332
+ {
333
+ id: 'generate-project-home',
334
+ title: "Generate a project's Project Home pages",
335
+ steps: [
336
+ "Call project_home_get_contract to get the protocol, ProjectPageSummary/ProjectPagePicks schemas, "
337
+ + 'component catalog, absolute output paths, and pipeline spec — the FIRST call any '
338
+ + 'project-home-builder session makes.',
339
+ 'Author a ProjectPageSummary matching the contract\'s summarySchema — every field must trace to '
340
+ + 'something concrete about the real project; never fabricate content.',
341
+ 'Call project_home_validate_summary with { cwd, summary } and fix every {field, message} error it '
342
+ + 'returns, re-validating until valid:true.',
343
+ "For each lens and slot in the contract's catalog, choose exactly one variant id by judging the "
344
+ + 'candidate variants against the summary you composed, assembling a ProjectPagePicks object '
345
+ + '(lensId -> slotId -> variantId).',
346
+ 'Call project_home_render with { cwd, summary, picks } to render all 5 lenses and write '
347
+ + 'summary.json/picks.json/output files.',
348
+ "Call project_home_status to confirm the new files landed and read manifest.json's generatedAt.",
349
+ ],
350
+ },
266
351
  ];
267
352
 
268
353
  for (const entry of MCP_TOOL_CATALOG) {
@@ -48,6 +48,18 @@ const OWNERS = Object.freeze({
48
48
  'scheduler': 'scheduler',
49
49
  // Project Home owns the synthesized Brief (generate + hand-edit).
50
50
  'project-brief': 'project-home',
51
+ // Project Home also owns the app-side render path for Project Pages
52
+ // (summary.json/picks.json/output/* written via the
53
+ // /admin/project-home/render route, config.cjs's writeJson). This is a
54
+ // NARROWER claim than "project-home owns everything under this folder":
55
+ // the SAME folder is also written directly by a project-home-builder
56
+ // Epic's own Write tool, which never goes through config.cjs and so is
57
+ // NOT governed by this table at all (see
58
+ // session-manager-operations/project-pages/README.md). Declaring the
59
+ // namespace here only closes the fail-closed gap for the app's own write
60
+ // path — it does not claim exclusivity over the agent's Write-tool calls,
61
+ // which remain outside this law by construction.
62
+ 'project-pages': 'project-home',
51
63
  // Structured per-tab error log lines (JSONL), tagged for tracing/analysis.
52
64
  'logs': 'logs',
53
65
  // Host on Bilko.run tab's deterministic bundle prep (dist/index.html +
@@ -0,0 +1,84 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * prdAgentType.cjs — the PRD-frontmatter `agentType` FK, mirroring
5
+ * agentModelResolve.cjs's/epicMint.cjs's established convention for the
6
+ * Epic-level `agentType` FK: THROW ON WRITE, REPORT ON READ.
7
+ *
8
+ * A PRD's `agentType` names the persona (`~/.claude/agents/<name>.md`, or a
9
+ * project overlay at `<cwd>/.claude/agents/<name>.md`) that should execute
10
+ * it — distinct from `tag` (workTypeLibrary.cjs), which is the WORK TYPE.
11
+ * Reuses epicMint.cjs's personaFileExists/resolvePersonaPaths rather than a
12
+ * second persona-file reader, and agentLibrary.cjs's listPersonas() for the
13
+ * "available personas" name list surfaced in the write-time rejection error.
14
+ */
15
+
16
+ const { personaFileExists, resolvePersonaPaths } = require('./epicMint.cjs');
17
+ const { listPersonas } = require('../agentLibrary.cjs');
18
+
19
+ /** PRD execution's documented default persona (dev-lead.md's own frontmatter role). */
20
+ const DEFAULT_PRD_AGENT_TYPE = 'dev-lead';
21
+
22
+ // READ side of the FK — logged at most once per (cwd, agentType) so a hot
23
+ // reconcile/list pass re-parsing the same on-disk PRD every tick doesn't
24
+ // flood opsErrorLog's daily file. Mirrors agentModelResolve.cjs's
25
+ // loggedDanglingPersonas set.
26
+ const loggedDanglingAgentTypes = new Set();
27
+
28
+ /**
29
+ * Read-time tolerance check: a PRD on disk whose persona was renamed or
30
+ * deleted after the PRD was written must still load. Never throws — logs at
31
+ * most once via opsErrorLog when `agentType` is set but unresolvable.
32
+ */
33
+ function reportDanglingAgentTypeOnce(cwd, agentType, deps = {}) {
34
+ if (!cwd || !agentType) return;
35
+ try {
36
+ const checkPersonaExists = deps.personaExists || personaFileExists;
37
+ if (checkPersonaExists(cwd, agentType, deps)) return;
38
+ const key = `${cwd}::${agentType}`;
39
+ if (loggedDanglingAgentTypes.has(key)) return;
40
+ loggedDanglingAgentTypes.add(key);
41
+ // Required lazily, same rationale as agentModelResolve.cjs's
42
+ // logDanglingPersonaOnce: pay for this leaf module only on the (rare)
43
+ // miss path.
44
+ const { appendError } = require('./opsErrorLog.cjs');
45
+ appendError({
46
+ cwd,
47
+ scope: 'prdAgentType',
48
+ level: 'warn',
49
+ message: `PRD agentType '${agentType}' has no resolvable persona file — PRD still loads`,
50
+ meta: { agentType },
51
+ });
52
+ } catch { /* logging must never break PRD loading */ }
53
+ }
54
+
55
+ /**
56
+ * Write-time FK check: throws when `agentType` is supplied but doesn't
57
+ * resolve to a readable persona file, naming the available personas (from
58
+ * the Agent Library) so the caller can pick a real one. A no-op when
59
+ * `agentType` is falsy — the caller is expected to default it separately
60
+ * (buildPrdBody's DEFAULT_PRD_AGENT_TYPE fill-in).
61
+ */
62
+ async function assertAgentTypeWritable(cwd, agentType, deps = {}) {
63
+ if (!agentType) return;
64
+ const checkPersonaExists = deps.personaExists || personaFileExists;
65
+ if (checkPersonaExists(cwd, agentType, deps)) return;
66
+
67
+ const load = deps.listPersonas || listPersonas;
68
+ let available = [];
69
+ try {
70
+ available = (await load()).map((p) => p.name).filter(Boolean).sort();
71
+ } catch { /* best-effort — still throw below even if listing failed */ }
72
+
73
+ const { projectPath, globalPath } = resolvePersonaPaths(cwd, agentType, deps);
74
+ throw new Error(
75
+ `agentType '${agentType}' does not resolve to a readable persona file — checked ${projectPath} and `
76
+ + `${globalPath}. Available personas: ${available.length ? available.join(', ') : '(none found)'}`,
77
+ );
78
+ }
79
+
80
+ module.exports = {
81
+ DEFAULT_PRD_AGENT_TYPE,
82
+ assertAgentTypeWritable,
83
+ reportDanglingAgentTypeOnce,
84
+ };