claude-code-session-manager 0.57.1 → 0.59.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 (68) hide show
  1. package/README.md +2 -2
  2. package/dist/assets/{TiptapBody-CJDxVF7X.js → TiptapBody-PUx4oZTh.js} +1 -1
  3. package/dist/assets/index-BPnfPdLW.js +3222 -0
  4. package/dist/assets/{index-WUc_GHMf.css → index-CPMP2XZ_.css} +1 -1
  5. package/dist/index.html +2 -2
  6. package/package.json +1 -2
  7. package/plugins/session-manager-dev/skills/develop/SKILL.md +7 -6
  8. package/plugins/session-manager-dev/skills/develop/standards.md +1 -1
  9. package/plugins/session-manager-dev/skills/explain-to-me/SKILL.md +1 -1
  10. package/plugins/session-manager-dev/skills/find-opportunity/SKILL.md +1 -1
  11. package/plugins/session-manager-dev/skills/ops-sweep/SKILL.md +16 -25
  12. package/plugins/session-manager-dev/skills/project-status/SKILL.md +5 -5
  13. package/scripts/lib/watchdogHelpers.cjs +0 -434
  14. package/scripts/mint-epic.cjs +6 -6
  15. package/src/main/__tests__/activeIndexMerge.test.cjs +2 -2
  16. package/src/main/__tests__/agentModelResolve.test.cjs +119 -0
  17. package/src/main/__tests__/classifyTranscriptLine.test.cjs +128 -17
  18. package/src/main/__tests__/epicContextDigest.test.cjs +100 -0
  19. package/src/main/__tests__/epicMint.test.cjs +165 -207
  20. package/src/main/__tests__/epicValidationHook.test.cjs +291 -0
  21. package/src/main/__tests__/prdMigration.test.cjs +160 -0
  22. package/src/main/__tests__/projectPages.test.cjs +151 -0
  23. package/src/main/__tests__/promptSessionEvents.test.cjs +74 -0
  24. package/src/main/__tests__/promptSessionSchema.test.cjs +101 -0
  25. package/src/main/__tests__/promptSessionsCreateEpicHandler.test.cjs +139 -0
  26. package/src/main/__tests__/rcaReport.test.cjs +188 -0
  27. package/src/main/__tests__/runVerify.test.cjs +109 -6
  28. package/src/main/__tests__/scheduler-effective-concurrency.test.cjs +32 -12
  29. package/src/main/__tests__/scheduler-epic-digest.test.cjs +214 -0
  30. package/src/main/__tests__/scheduler-heal-refusal.test.cjs +61 -0
  31. package/src/main/__tests__/scheduler-notify-originating-tab.test.cjs +74 -0
  32. package/src/main/__tests__/scheduler-writeprd-epic-rollback.test.cjs +5 -5
  33. package/src/main/__tests__/transcripts-doFlush-array.test.cjs +118 -0
  34. package/src/main/__tests__/transcripts-paged-reads.test.cjs +233 -0
  35. package/src/main/__tests__/uniquePrdNumbers.test.cjs +7 -2
  36. package/src/main/chatRunner.cjs +10 -1
  37. package/src/main/health.cjs +5 -1
  38. package/src/main/index.cjs +3 -5
  39. package/src/main/ipcSchemas.cjs +72 -4
  40. package/src/main/lib/__tests__/schedulerBatchDepends.test.cjs +130 -0
  41. package/src/main/lib/agentModelResolve.cjs +97 -0
  42. package/src/main/lib/auditLog.cjs +2 -2
  43. package/src/main/lib/classifyTranscriptLine.cjs +131 -48
  44. package/src/main/lib/epicContextDigest.cjs +78 -0
  45. package/src/main/lib/epicMint.cjs +86 -164
  46. package/src/main/lib/epicValidationHook.cjs +192 -0
  47. package/src/main/lib/prdMigration.cjs +76 -5
  48. package/src/main/lib/promptSessionSchema.cjs +95 -0
  49. package/src/main/lib/promptSessionsCreateEpic.cjs +70 -0
  50. package/src/main/lib/{rcaFeedbackHook.cjs → rcaReport.cjs} +47 -114
  51. package/src/main/lib/schedulerBatch.cjs +70 -111
  52. package/src/main/lib/schedulerConfig.cjs +0 -1
  53. package/src/main/otel.cjs +3 -1
  54. package/src/main/projectPages.cjs +60 -14
  55. package/src/main/promptSessionEvents.cjs +16 -1
  56. package/src/main/runVerify.cjs +17 -2
  57. package/src/main/scheduler.cjs +302 -68
  58. package/src/main/templates/project-pages-default-home.html +123 -0
  59. package/src/main/transcripts.cjs +191 -32
  60. package/src/main/webRemote.cjs +8 -7
  61. package/src/preload/api.d.ts +100 -57
  62. package/src/preload/index.cjs +9 -7
  63. package/dist/assets/index-DCa3QvAi.js +0 -3227
  64. package/plugins/session-manager-dev/skills/propose-epic/SKILL.md +0 -68
  65. package/scripts/propose-epic.cjs +0 -56
  66. package/src/main/__tests__/rcaFeedbackHook.test.cjs +0 -280
  67. package/src/main/repoAnalyzer.cjs +0 -346
  68. package/src/main/search.cjs +0 -332
@@ -0,0 +1,95 @@
1
+ /**
2
+ * Canonical runtime schema for a PromptSession (Epic) record, mirroring the
3
+ * TS `PromptSession` interface at src/renderer/state/promptSessions.ts:29-76
4
+ * field-for-field. Two independent code paths hand-construct this same
5
+ * record shape — the renderer's createPromptSession and this main-process
6
+ * module's ensureEpic mint branch (epicMint.cjs) — kept in sync only by doc
7
+ * comments today. This schema is the single source of truth to validate
8
+ * against, closing that drift risk on the main-process/CLI/scheduler side.
9
+ *
10
+ * Plain CJS, no Electron dependency, so it's requirable from epicMint.cjs
11
+ * and (in a later PRD) ipcSchemas.cjs's IPC boundary.
12
+ */
13
+ 'use strict';
14
+
15
+ const { z } = require('zod');
16
+
17
+ // Mirrors EpicTag (src/renderer/lib/tagLibrary.ts:10) — the Epic-level
18
+ // mission tag, NOT the same value set as ipcSchemas.cjs's scheduleRetagPrd
19
+ // tag enum (~line 349), which only covers 5 of these 6 values ('feature',
20
+ // 'bug', 'discussion', 'build', 'project-home-builder') and is missing
21
+ // 'bilko-host-publisher' — the two enums describe related but distinct
22
+ // concepts (PRD-scheduling tag vs. Epic-mission tag), so this is its own
23
+ // definition rather than an import of that one.
24
+ const EpicTagSchema = z.enum([
25
+ 'feature',
26
+ 'bug',
27
+ 'discussion',
28
+ 'build',
29
+ 'project-home-builder',
30
+ 'bilko-host-publisher',
31
+ ]);
32
+
33
+ // Mirrors EpicSource (src/renderer/state/promptSessions.ts:22-27).
34
+ const EpicSourceSchema = z.object({
35
+ producer: z.enum(['new-epic-ui', 'scheduler-dispatch']),
36
+ prdSlug: z.string().optional(),
37
+ runId: z.string().optional(),
38
+ sourceTabId: z.string().optional(),
39
+ });
40
+
41
+ // Mirrors EpicIntakeSection (src/renderer/lib/epicIntake.ts) — the labeled
42
+ // slices composeEpicIntake emits alongside the flat `openingPrompt`, kept as
43
+ // data so the Epic's first turn can render a structured AIM briefing card
44
+ // instead of regex-parsing the flat string back apart.
45
+ const EpicIntakeSectionSchema = z.object({
46
+ kind: z.enum(['actor', 'injection', 'input', 'mission', 'goal', 'reference']),
47
+ label: z.string(),
48
+ text: z.string(),
49
+ source: z.string().optional(),
50
+ });
51
+
52
+ // Mirrors PromptSession (src/renderer/state/promptSessions.ts:29-76).
53
+ const PromptSessionSchema = z.object({
54
+ id: z.string(),
55
+ cwd: z.string(),
56
+ goalText: z.string(),
57
+ claudeSessionId: z.string(),
58
+ status: z.enum(['proposed', 'active', 'completed']),
59
+ createdAt: z.string(),
60
+ completedAt: z.string().nullable(),
61
+ resumedFromId: z.string().nullable().optional(),
62
+ tag: EpicTagSchema.optional(),
63
+ openingPrompt: z.string().nullable().optional(),
64
+ source: EpicSourceSchema.optional(),
65
+ agentType: z.string().optional(),
66
+ // Absent on Epics minted before this field existed — those fall back to
67
+ // rendering the flat `openingPrompt` as a single block (see
68
+ // ChatTranscriptTurn.tsx's EpicIntakeCard).
69
+ sections: z.array(EpicIntakeSectionSchema).optional(),
70
+ });
71
+
72
+ /**
73
+ * Throws a clear, descriptive error (not a raw ZodError dump) when `session`
74
+ * doesn't match PromptSessionSchema. Callers that want fail-closed
75
+ * enforcement before persisting a session object call this immediately
76
+ * before their write.
77
+ */
78
+ function assertValidPromptSession(session) {
79
+ const result = PromptSessionSchema.safeParse(session);
80
+ if (!result.success) {
81
+ const issues = result.error.issues
82
+ .map((issue) => `${issue.path.join('.') || '(root)'}: ${issue.message}`)
83
+ .join('; ');
84
+ throw new Error(`assertValidPromptSession: invalid PromptSession shape — ${issues}`);
85
+ }
86
+ return result.data;
87
+ }
88
+
89
+ module.exports = {
90
+ PromptSessionSchema,
91
+ EpicSourceSchema,
92
+ EpicTagSchema,
93
+ EpicIntakeSectionSchema,
94
+ assertValidPromptSession,
95
+ };
@@ -0,0 +1,70 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * promptSessionsCreateEpic.cjs — IPC entry point letting the renderer mint a
5
+ * new Epic through main's own ensureEpic() instead of hand-constructing a
6
+ * PromptSession record itself (state/promptSessions.ts's createPromptSession).
7
+ *
8
+ * Step 1 of a two-PRD chain (PRD 953 here; PRD 954 routes createPromptSession
9
+ * through it): purely additive today — nothing calls this handler yet. Kept
10
+ * as a sibling module (mirrors lib/activeIndexMerge.cjs, which does the same
11
+ * "own file, own register*Handlers export, wired from index.cjs" shape for
12
+ * the neighboring active-index merge IPC) rather than growing epicMint.cjs
13
+ * itself, since this is IPC-registration plumbing, not Epic-minting logic.
14
+ */
15
+
16
+ const { ensureEpic, readActiveIndex, MINT_AUTHORITY_NEW_EPIC_UI } = require('./epicMint.cjs');
17
+ const { validatePath } = require('../config.cjs');
18
+
19
+ /**
20
+ * createEpicViaIpc(cwd, { goalText, tag?, agentType?, source? }) → Promise<{ epicId, session }>
21
+ *
22
+ * THE one Epic-creation path (epicMint.cjs's SINGLE-CREATOR LAW): this handler
23
+ * holds `MINT_AUTHORITY_NEW_EPIC_UI`, the token that unlocks ensureEpic's mint
24
+ * branch, because it is what the New Epic card calls when a human presses it.
25
+ * Nothing else may hold it.
26
+ *
27
+ * Always mints a brand-new 'proposed' Epic — never joins an existing one — and
28
+ * returns the just-written record read back from the active-index.json on disk, so the
29
+ * response is guaranteed byte-identical to what a subsequent hydrate would
30
+ * see — never a second, independently re-derived copy of the session shape.
31
+ *
32
+ * `ensureEpic`'s writes go through epicMint.cjs's own raw `fs` calls, not
33
+ * config.cjs's writeJson/validateWrite — fine for its other callers
34
+ * (the scheduler's join-only dispatch), which derives `cwd` from a
35
+ * project the app already knows about, but this function is reachable from
36
+ * the renderer over IPC with an arbitrary `cwd` string. Explicitly running
37
+ * that `cwd` through config.cjs's own `validatePath` (allowedRoots = home dir
38
+ * + every cwd a TAB has opened) closes the gap so this handler can't be used
39
+ * to make main write session-manager-operations/ into an arbitrary
40
+ * filesystem location — the same boundary every other renderer-facing path
41
+ * IPC (config:read-json/write-json, lib/activeIndexMerge.cjs's own merge) is
42
+ * already required to pass through.
43
+ */
44
+ async function createEpicViaIpc(cwd, { goalText, tag, agentType, source, openingPrompt, sections } = {}) {
45
+ validatePath(cwd);
46
+ const { epicId } = await ensureEpic(cwd, {
47
+ goalText,
48
+ tag,
49
+ agentType,
50
+ source,
51
+ openingPrompt,
52
+ sections,
53
+ mintAuthority: MINT_AUTHORITY_NEW_EPIC_UI,
54
+ status: 'proposed',
55
+ });
56
+ const index = readActiveIndex(cwd);
57
+ return { epicId, session: index.sessions[epicId] };
58
+ }
59
+
60
+ function registerPromptSessionsCreateEpicHandlers() {
61
+ const { ipcMain } = require('electron');
62
+ const { schemas: s, validated: v } = require('../ipcSchemas.cjs');
63
+ ipcMain.handle(
64
+ 'promptSessions:create-epic',
65
+ v(s.promptSessionsCreateEpic, ({ cwd, goalText, tag, agentType, source, openingPrompt, sections }) =>
66
+ createEpicViaIpc(cwd, { goalText, tag, agentType, source, openingPrompt, sections })),
67
+ );
68
+ }
69
+
70
+ module.exports = { createEpicViaIpc, registerPromptSessionsCreateEpicHandlers };
@@ -1,13 +1,21 @@
1
1
  'use strict';
2
2
 
3
3
  /**
4
- * rcaFeedbackHook.cjs — files a deterministic Root Cause Analysis feedback
5
- * item when a scheduler job is parked in `needs_review`.
4
+ * rcaReport.cjs — writes a deterministic Root Cause Analysis report into the
5
+ * run directory when a scheduler job is parked in `needs_review`.
6
6
  *
7
7
  * Mirrors the dodDrainHook.cjs pattern: fire-and-forget, idempotent,
8
8
  * kill-switched, never throws to the caller. Unlike the DoD gate this fires
9
9
  * per-job at the needs_review transition, not at queue drain.
10
10
  *
11
+ * This module creates NOTHING but a markdown file next to the run's own log.
12
+ * It used to file a `proposed` Epic per parked job; Epics are now created in
13
+ * exactly one place — the human pressing New Epic (see epicMint.cjs's
14
+ * SINGLE-CREATOR LAW). Surfacing the parked job back to a human is the
15
+ * scheduler's job (notifyNeedsReview → appendResponseEventIfKnown), which
16
+ * routes it into the Epic that AUTHORED the PRD rather than opening a
17
+ * sibling Epic about it.
18
+ *
11
19
  * No LLM calls live here — the RCA body is assembled deterministically from
12
20
  * the run's meta.json, log tail, and the original PRD's acceptance-criteria
13
21
  * section. When an Opus auto-fix investigation later runs (spawnInvestigation
@@ -17,34 +25,17 @@
17
25
  *
18
26
  * Kill-switch: SM_RCA_DISABLE=1 (mirrors SM_DOD_DISABLE / SM_SUPERVISOR_DISABLE).
19
27
  *
20
- * Electron-free by design (no require('electron')) so tests stay hermetic.
21
- * config.cjs itself requires('electron'), but that resolves to a harmless
22
- * string (not the electron module) when required outside the Electron
23
- * runtime, and this lib never touches ipcMain — only the atomic-write /
24
- * path-validation helpers.
28
+ * Electron-free by design (no require('electron')) so tests stay hermetic —
29
+ * it only reads run artifacts and writes one markdown file into the run dir
30
+ * the caller already owns.
25
31
  */
26
32
 
27
33
  const fs = require('node:fs');
28
- const os = require('node:os');
29
34
  const path = require('node:path');
30
- const config = require('../config.cjs');
31
- const { ensureEpic, findJoinableEpic } = require('./epicMint.cjs');
32
35
  const { splitFrontmatter } = require('./prdFrontmatter.cjs');
33
36
  const { readTail } = require('./fileTail.cjs');
34
37
  const { resolvePrdWriteDir } = require('./prdLocations.cjs');
35
38
 
36
- // The one project this hook always knows how to reach, used as the fallback
37
- // destination when a job's own cwd has no feedback inbox.
38
- const SM_REPO_ROOT = path.join(os.homedir(), 'Projects', 'session-manager');
39
-
40
- // NEVER write here: ~/.claude/session-manager/feedback/ is the scheduler's
41
- // auto-PRD intake (src/main/scheduler.cjs's periodic feedback sweep) — it
42
- // converts anything dropped there into a scheduled PRD automatically. Filing
43
- // an RCA there would create an RCA → auto-PRD → run → needs_review → RCA
44
- // cycle with no human in the loop. Both real destinations below are project
45
- // feedback INBOXES (consumed by /process-feedback), which is a different,
46
- // human/agent-triaged path.
47
-
48
39
  // ─── VERDICT_LABELS — deliberate duplicate ──────────────────────────────────
49
40
  // Mirrors src/renderer/components/tabs/scheduler/sched-primitives.tsx:123-131.
50
41
  // The main process cannot import renderer TSX, so this vocabulary is kept in
@@ -197,41 +188,12 @@ function extractAcceptanceCriteria(prdBody) {
197
188
  return text || null;
198
189
  }
199
190
 
200
- // ─── Destination resolution ──────────────────────────────────────────────────
201
-
202
- /**
203
- * Resolve which PROJECT the proposal is filed into.
204
- * 1. the job's own cwd — Epics live in every project, so this is almost
205
- * always the answer.
206
- * 2. else session-manager's own repo, naming the target project in the body.
207
- *
208
- * The old version probed for a `session-manager-operations/feedback/`
209
- * directory and fell back when it was missing; with proposals there is
210
- * nothing to probe for — the Epic store is created on demand.
211
- */
212
- function resolveDestination(job) {
213
- const jobCwd = job?.cwd;
214
- if (jobCwd) {
215
- return { cwd: jobCwd, allowlistRoot: jobCwd, targetProjectNote: null };
216
- }
217
- return {
218
- cwd: SM_REPO_ROOT,
219
- allowlistRoot: SM_REPO_ROOT,
220
- targetProjectNote: "Filed into session-manager's own project (job had no cwd on record).",
221
- };
222
- }
223
-
224
- /** One-line title for the proposal — goalText is what the Epics list shows,
225
- * so it must stay short; the full RCA markdown rides in openingPrompt. */
226
- function rcaProposalTitle(job, verdict) {
191
+ /** One-line summary of the parked job — used as the report's title and as the
192
+ * text the scheduler surfaces back into the Epic that authored the PRD. */
193
+ function rcaSummaryLine(job, verdict) {
227
194
  return `PRD ${job?.slug ?? 'unknown'} needs review: ${humanVerdict(verdict)}`;
228
195
  }
229
196
 
230
- function shortRunId(runId) {
231
- const cleaned = String(runId ?? '').replace(/[^a-zA-Z0-9]/g, '');
232
- return cleaned.slice(0, 12) || 'norun';
233
- }
234
-
235
197
  function lastNLines(text, n) {
236
198
  const lines = (text || '').split('\n');
237
199
  return lines.slice(-n).join('\n');
@@ -261,7 +223,7 @@ function readPrdBody(cwd, slug) {
261
223
  * Assemble the deterministic RCA markdown body (frontmatter + sections).
262
224
  * Pure — no I/O, no LLM.
263
225
  */
264
- function buildRcaMarkdown({ job, verdict, meta, logTail, acText, failureClass, investigationText, targetProjectNote }) {
226
+ function buildRcaMarkdown({ job, verdict, meta, logTail, acText, failureClass, investigationText }) {
265
227
  const title = `Root cause: ${job.slug} → needs_review (${humanVerdict(verdict)})`;
266
228
  const fm = ['---', `title: ${title}`, 'source: scheduler-rca', 'type: rca', 'severity: normal', `prdSlug: ${job.slug}`, `runId: ${job.runId}`, `verdict: ${verdict}`, '---'].join('\n');
267
229
 
@@ -277,21 +239,21 @@ function buildRcaMarkdown({ job, verdict, meta, logTail, acText, failureClass, i
277
239
  `# The PRD's acceptance criteria\n\n${acText || '(acceptance criteria not found — original PRD may have been archived or removed)'}`,
278
240
  `# Likely failure class\n\n**${failureClass}**\n\nPrevention: ${prevention}`,
279
241
  ];
280
- if (targetProjectNote) sections.push(`# Note\n\n${targetProjectNote}`);
281
242
  if (investigationText) sections.push(`## Investigation analysis\n\n${investigationText.trim()}`);
282
243
 
283
244
  return `${fm}\n\n${sections.join('\n\n')}\n`;
284
245
  }
285
246
 
286
247
  /**
287
- * File (or update) a deterministic RCA feedback item for a needs_review job.
248
+ * Write (or refresh) the deterministic RCA report for a needs_review job,
249
+ * as `<runDir>/root-cause-<slug>.md` — next to the run's own log, the same
250
+ * place the definition-of-done gate writes its report (dodDrainHook.cjs).
288
251
  * Never throws — logs one line on success/skip/error and always resolves.
289
252
  *
290
- * Idempotency: the destination filename is deterministic per (slug, runId).
291
- * A second call for the same (slug, runId) with no investigationText is a
292
- * no-op; a second call WITH investigationText overwrites the same file to
293
- * add/replace the "Investigation analysis" section rather than filing a
294
- * duplicate. A different runId (new run of the same slug) files a new RCA.
253
+ * Idempotency: one report per run directory per slug, and a run dir is
254
+ * per-(runId). A second call for the same run overwrites it in place, which
255
+ * is exactly what the investigation-enrichment pass wants (it re-renders the
256
+ * same body plus an "Investigation analysis" section) — never a duplicate.
295
257
  *
296
258
  * @param {object} opts
297
259
  * @param {{slug: string, cwd?: string, runId: string, exitCode?: number, error?: string, epicId?: string}} opts.job
@@ -300,10 +262,9 @@ function buildRcaMarkdown({ job, verdict, meta, logTail, acText, failureClass, i
300
262
  * @param {Array} [opts.annotations] Non-blocking verifier annotations (currently unused
301
263
  * in the body beyond being accepted for future use).
302
264
  * @param {string} [opts.investigationText] <RCA> block text from an Opus investigation.
303
- * @param {Date} [opts.now] Override for the date segment of the filename (testing).
304
- * @returns {Promise<{filed: boolean, path?: string, updated?: boolean, reason?: string}>}
265
+ * @returns {Promise<{filed: boolean, path?: string, summary?: string, failureClass?: string, reason?: string}>}
305
266
  */
306
- async function fileRcaFeedback({ job, runDir, verdict, annotations, investigationText, now } = {}) {
267
+ async function writeRcaReport({ job, runDir, verdict, annotations, investigationText } = {}) {
307
268
  try {
308
269
  if (process.env.SM_RCA_DISABLE === '1') {
309
270
  console.log('[rca] skip: SM_RCA_DISABLE=1');
@@ -327,7 +288,10 @@ async function fileRcaFeedback({ job, runDir, verdict, annotations, investigatio
327
288
  return { filed: false, reason: 'no-verdict' };
328
289
  }
329
290
 
330
- const dest = resolveDestination(job);
291
+ if (!runDir) {
292
+ console.log(`[rca] skip: no run directory for ${job.slug}`);
293
+ return { filed: false, reason: 'no-run-dir' };
294
+ }
331
295
 
332
296
  const meta = readRunMeta(runDir, job.slug);
333
297
  const logPath = runDir ? path.join(runDir, `${job.slug}.log`) : null;
@@ -336,67 +300,36 @@ async function fileRcaFeedback({ job, runDir, verdict, annotations, investigatio
336
300
  const failureClass = classifyFailure({ verdict, logTail });
337
301
 
338
302
  const markdown = buildRcaMarkdown({
339
- job, verdict, meta, logTail, acText, failureClass, investigationText,
340
- targetProjectNote: dest.targetProjectNote, annotations,
341
- });
342
-
343
- // File it as a PROPOSED Epic rather than a markdown file in a feedback
344
- // inbox. The proposal lands directly in the Epics workspace behind an
345
- // Approve & start gate: nothing runs and nothing is spent until a human
346
- // approves it. This replaced the feedback-folder intake (and the
347
- // process-feedback triage pass that existed only to read it back out) —
348
- // the proposal IS the work item, already carrying its own session and
349
- // PRD directory. reuseByGoal makes a re-trigger for the same failure
350
- // join the existing proposal instead of duplicating it, which is what
351
- // the old live/processed existence checks were for.
352
- config.addAllowedRoot(dest.allowlistRoot);
353
- const title = rcaProposalTitle(job, verdict);
354
-
355
- // job.epicId (present on jobs authored after Epic-gating landed; absent
356
- // on legacy PRDs) is the known origin Epic this failing job belonged to.
357
- // Passing it straight through as ensureEpic's `epicId` would hit its
358
- // explicitEpicId join-only branch, which joins unconditionally — even a
359
- // 'completed' Epic. findJoinableEpic's preferEpicId strategy is the one
360
- // that only joins when the Epic is still open ('proposed'/'active'), so
361
- // resolve it here and only forward it when that strategy actually fires
362
- // — otherwise fall through to the existing reuseByGoal/mint behavior.
363
- // Once forwarded, ensureEpic joins it unconditionally by id (no second
364
- // status check) — the status gate lives entirely in this resolve step.
365
- let preferredEpicId;
366
- if (typeof job.epicId === 'string' && job.epicId) {
367
- const joinable = findJoinableEpic(dest.cwd, { goalText: title, preferEpicId: job.epicId, status: 'proposed' });
368
- if (joinable && joinable.matchedBy === 'preferEpicId') {
369
- preferredEpicId = joinable.epicId;
370
- }
371
- }
372
-
373
- const { epicId, created } = await ensureEpic(dest.cwd, {
374
- goalText: title,
375
- tag: 'bug',
376
- status: 'proposed',
377
- reuseByGoal: true,
378
- openingPrompt: markdown,
379
- epicId: preferredEpicId,
380
- source: { producer: 'rca-hook', prdSlug: job.slug, runId: job.runId },
303
+ job, verdict, meta, logTail, acText, failureClass, investigationText, annotations,
381
304
  });
382
305
 
383
- console.log(`[rca] ${created ? 'proposed' : 'joined'} epic ${epicId} for ${job.slug}`);
384
- return { filed: true, epicId, created, updated: !created };
306
+ // Raw fs (not config.cjs's writeTextAtomic) for the same reason the rest
307
+ // of this lib uses it: the run directory lives under
308
+ // ~/.claude/session-manager/scheduled-plans/runs/, which is Session-Manager
309
+ // runtime state, not an OWNERS namespace under a project's operations root.
310
+ const reportPath = path.join(runDir, `root-cause-${job.slug}.md`);
311
+ fs.mkdirSync(runDir, { recursive: true });
312
+ const tmp = `${reportPath}.tmp-${process.pid}`;
313
+ fs.writeFileSync(tmp, markdown);
314
+ fs.renameSync(tmp, reportPath);
315
+
316
+ const summary = rcaSummaryLine(job, verdict);
317
+ console.log(`[rca] wrote ${reportPath} (${failureClass})`);
318
+ return { filed: true, path: reportPath, summary, failureClass };
385
319
  } catch (e) {
386
- console.error('[rca] error filing RCA feedback', e?.message ?? String(e));
320
+ console.error('[rca] error writing RCA report', e?.message ?? String(e));
387
321
  return { filed: false, reason: 'error', error: e?.message ?? String(e) };
388
322
  }
389
323
  }
390
324
 
391
325
  module.exports = {
392
- fileRcaFeedback,
393
- rcaProposalTitle,
326
+ writeRcaReport,
327
+ rcaSummaryLine,
394
328
  // Pure helpers, exported for unit tests.
395
329
  humanVerdict,
396
330
  classifyFailure,
397
331
  extractAcceptanceCriteria,
398
332
  extractRcaBlock,
399
- resolveDestination,
400
333
  buildRcaMarkdown,
401
334
  FAILURE_CLASSES,
402
335
  VERDICT_LABELS,