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.
- package/README.md +2 -2
- package/dist/assets/{TiptapBody-CJDxVF7X.js → TiptapBody-PUx4oZTh.js} +1 -1
- package/dist/assets/index-BPnfPdLW.js +3222 -0
- package/dist/assets/{index-WUc_GHMf.css → index-CPMP2XZ_.css} +1 -1
- package/dist/index.html +2 -2
- package/package.json +1 -2
- package/plugins/session-manager-dev/skills/develop/SKILL.md +7 -6
- package/plugins/session-manager-dev/skills/develop/standards.md +1 -1
- package/plugins/session-manager-dev/skills/explain-to-me/SKILL.md +1 -1
- package/plugins/session-manager-dev/skills/find-opportunity/SKILL.md +1 -1
- package/plugins/session-manager-dev/skills/ops-sweep/SKILL.md +16 -25
- package/plugins/session-manager-dev/skills/project-status/SKILL.md +5 -5
- package/scripts/lib/watchdogHelpers.cjs +0 -434
- package/scripts/mint-epic.cjs +6 -6
- package/src/main/__tests__/activeIndexMerge.test.cjs +2 -2
- package/src/main/__tests__/agentModelResolve.test.cjs +119 -0
- package/src/main/__tests__/classifyTranscriptLine.test.cjs +128 -17
- package/src/main/__tests__/epicContextDigest.test.cjs +100 -0
- package/src/main/__tests__/epicMint.test.cjs +165 -207
- package/src/main/__tests__/epicValidationHook.test.cjs +291 -0
- package/src/main/__tests__/prdMigration.test.cjs +160 -0
- package/src/main/__tests__/projectPages.test.cjs +151 -0
- package/src/main/__tests__/promptSessionEvents.test.cjs +74 -0
- package/src/main/__tests__/promptSessionSchema.test.cjs +101 -0
- package/src/main/__tests__/promptSessionsCreateEpicHandler.test.cjs +139 -0
- package/src/main/__tests__/rcaReport.test.cjs +188 -0
- package/src/main/__tests__/runVerify.test.cjs +109 -6
- package/src/main/__tests__/scheduler-effective-concurrency.test.cjs +32 -12
- package/src/main/__tests__/scheduler-epic-digest.test.cjs +214 -0
- package/src/main/__tests__/scheduler-heal-refusal.test.cjs +61 -0
- package/src/main/__tests__/scheduler-notify-originating-tab.test.cjs +74 -0
- package/src/main/__tests__/scheduler-writeprd-epic-rollback.test.cjs +5 -5
- package/src/main/__tests__/transcripts-doFlush-array.test.cjs +118 -0
- package/src/main/__tests__/transcripts-paged-reads.test.cjs +233 -0
- package/src/main/__tests__/uniquePrdNumbers.test.cjs +7 -2
- package/src/main/chatRunner.cjs +10 -1
- package/src/main/health.cjs +5 -1
- package/src/main/index.cjs +3 -5
- package/src/main/ipcSchemas.cjs +72 -4
- package/src/main/lib/__tests__/schedulerBatchDepends.test.cjs +130 -0
- package/src/main/lib/agentModelResolve.cjs +97 -0
- package/src/main/lib/auditLog.cjs +2 -2
- package/src/main/lib/classifyTranscriptLine.cjs +131 -48
- package/src/main/lib/epicContextDigest.cjs +78 -0
- package/src/main/lib/epicMint.cjs +86 -164
- package/src/main/lib/epicValidationHook.cjs +192 -0
- package/src/main/lib/prdMigration.cjs +76 -5
- package/src/main/lib/promptSessionSchema.cjs +95 -0
- package/src/main/lib/promptSessionsCreateEpic.cjs +70 -0
- package/src/main/lib/{rcaFeedbackHook.cjs → rcaReport.cjs} +47 -114
- package/src/main/lib/schedulerBatch.cjs +70 -111
- package/src/main/lib/schedulerConfig.cjs +0 -1
- package/src/main/otel.cjs +3 -1
- package/src/main/projectPages.cjs +60 -14
- package/src/main/promptSessionEvents.cjs +16 -1
- package/src/main/runVerify.cjs +17 -2
- package/src/main/scheduler.cjs +302 -68
- package/src/main/templates/project-pages-default-home.html +123 -0
- package/src/main/transcripts.cjs +191 -32
- package/src/main/webRemote.cjs +8 -7
- package/src/preload/api.d.ts +100 -57
- package/src/preload/index.cjs +9 -7
- package/dist/assets/index-DCa3QvAi.js +0 -3227
- package/plugins/session-manager-dev/skills/propose-epic/SKILL.md +0 -68
- package/scripts/propose-epic.cjs +0 -56
- package/src/main/__tests__/rcaFeedbackHook.test.cjs +0 -280
- package/src/main/repoAnalyzer.cjs +0 -346
- 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
|
-
*
|
|
5
|
-
*
|
|
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
|
-
*
|
|
22
|
-
*
|
|
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
|
-
|
|
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
|
|
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
|
-
*
|
|
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:
|
|
291
|
-
* A second call for the same
|
|
292
|
-
*
|
|
293
|
-
*
|
|
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
|
-
* @
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
384
|
-
|
|
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
|
|
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
|
-
|
|
393
|
-
|
|
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,
|