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
@@ -1,68 +0,0 @@
1
- ---
2
- name: propose-epic
3
- description: >-
4
- File work you think should happen as a PROPOSED Epic — an Epic that does not
5
- start until a human presses Approve. Replaces the retired /process-feedback
6
- and /my-feedback intake pipeline: there is no feedback folder, no triage
7
- pass, and no archiving convention, because the proposal IS the work item.
8
- Use whenever you would previously have written a feedback file — for this
9
- project or another one — or whenever the user says "/propose-epic",
10
- "propose this", "file this for later", "suggest we do X", "send feedback to
11
- X". Keywords: propose, proposal, feedback, enhancement request, file for
12
- approval, cross-project request, backlog.
13
- ---
14
-
15
- # propose-epic
16
-
17
- **Role:** the one way an agent asks for work to happen that it is not doing right now.
18
-
19
- You do not implement, and you do not queue anything that runs. You file a **proposed
20
- Epic** and stop. A human approves it in the Epics workspace, at which point its objective
21
- becomes the first prompt of its own claude session — the same start path every hand-created
22
- Epic takes.
23
-
24
- ## Why this replaced the feedback folder
25
-
26
- The old flow was: write a markdown file into `session-manager-operations/feedback/` →
27
- `/process-feedback` later reads it, evaluates it, dedupes it, queues PRDs via `/develop`,
28
- archives the file to `processed/`, and updates a README with lessons. Four moving parts and
29
- ~400 lines of instructions existed to move an idea from "written down" to "being worked on".
30
-
31
- An Epic already is that thing. It has a goal, an intent tag, its own session, its own PRD
32
- directory, and a place in the UI. The only piece it was missing was "don't start yet" —
33
- that is the `proposed` status. So the intake pipeline collapses into one command.
34
-
35
- ## How to file one
36
-
37
- ```bash
38
- node "$SM_ROOT/scripts/propose-epic.cjs" <project-cwd> "<one-line title>" [feature|bug|discussion] <<'BODY'
39
- <the full objective — this is sent verbatim as the first prompt on approval>
40
- BODY
41
- ```
42
-
43
- `$SM_ROOT` is session-manager's own package root. Resolve it once, in this order:
44
- 1. You are working inside the session-manager repo → `.` (use `scripts/propose-epic.cjs`).
45
- 2. Otherwise the installed package — this skill file lives at
46
- `<SM_ROOT>/plugins/session-manager-dev/skills/propose-epic/SKILL.md`, so
47
- `SM_ROOT` is four directories up from this file's own path.
48
-
49
- - **`<project-cwd>`** — the project the work belongs to. Cross-project requests are just a
50
- proposal filed into *that* project's cwd; there is no separate outbound skill.
51
- - **title** — one line. It is what the Epics list shows. Keep it specific: `Scheduler rows
52
- don't show their Epic`, not `Scheduler bug`.
53
- - **body** — write it as an instruction to the agent that will pick it up, not as a report
54
- to a human. Include what you observed, where in the code it lives (file:line if you know
55
- it), and what done looks like. It is the first prompt, so it should be enough to act on.
56
- - Re-proposing the same title into the same project **joins** the existing proposal and
57
- enriches it rather than duplicating it. Say more in the body and re-run; that is the
58
- update mechanism.
59
-
60
- Report back the Epic id and the title you filed. Do not queue PRDs, do not edit code, and
61
- do not ask whether you should file it — filing is the default action, and the approval gate
62
- is the user's control, not your prompt.
63
-
64
- ## When NOT to propose
65
-
66
- - The user asked you to do the work now → do it (or `/develop` it), don't propose it.
67
- - It is a one-line fix you are already in the file for → just fix it.
68
- - Nothing actionable → say so plainly and file nothing.
@@ -1,56 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * propose-epic.cjs — file an Epic that does NOT start.
4
- *
5
- * This is the replacement for the feedback-folder intake. Instead of writing
6
- * a markdown file into `session-manager-operations/feedback/` for a separate
7
- * skill to later parse, triage, dedupe and re-queue, an agent files a
8
- * `proposed` Epic: it appears in the Epics workspace with an Approve & start
9
- * gate, and nothing runs or is spent until a human presses it.
10
- *
11
- * One mechanism replaces the whole intake pipeline — the proposal IS the
12
- * work item, already in the place work is done, already carrying its own
13
- * session and PRD directory.
14
- *
15
- * Usage:
16
- * node scripts/propose-epic.cjs <cwd> "<one-line title>" [feature|bug|discussion]
17
- * # full body (the first prompt sent on approval) is read from stdin;
18
- * # when stdin is empty the title is used as the body.
19
- *
20
- * Prints the Epic id on stdout. Reuse-by-title: re-proposing the same title
21
- * into the same project joins the existing proposal instead of duplicating it.
22
- */
23
- 'use strict';
24
-
25
- const { ensureEpic } = require('../src/main/lib/epicMint.cjs');
26
-
27
- const [cwd, title, tag] = process.argv.slice(2);
28
- if (!cwd || !title) {
29
- process.stderr.write('usage: propose-epic.cjs <cwd> "<title>" [feature|bug|discussion] (body on stdin)\n');
30
- process.exit(1);
31
- }
32
-
33
- function readStdin() {
34
- return new Promise((resolve) => {
35
- if (process.stdin.isTTY) { resolve(''); return; }
36
- let buf = '';
37
- process.stdin.setEncoding('utf8');
38
- process.stdin.on('data', (d) => { buf += d; });
39
- process.stdin.on('end', () => resolve(buf));
40
- process.stdin.on('error', () => resolve(''));
41
- });
42
- }
43
-
44
- (async () => {
45
- const body = (await readStdin()).trim();
46
- const { epicId, created } = await ensureEpic(cwd, {
47
- goalText: title,
48
- tag: tag || 'feature',
49
- status: 'proposed',
50
- reuseByGoal: true,
51
- openingPrompt: body || title,
52
- source: { producer: 'propose-epic' },
53
- });
54
- process.stderr.write(`${created ? 'proposed' : 'joined existing proposal'} epic ${epicId}\n`);
55
- process.stdout.write(`${epicId}\n`);
56
- })();
@@ -1,280 +0,0 @@
1
- /**
2
- * rcaFeedbackHook.test.cjs — unit tests for src/main/lib/rcaFeedbackHook.cjs.
3
- *
4
- * Run: timeout 120 npx vitest run src/main/__tests__/rcaFeedbackHook.test.cjs
5
- */
6
-
7
- 'use strict';
8
-
9
- import { test, expect, beforeEach, afterEach } from 'vitest';
10
- const fs = require('node:fs');
11
- const os = require('node:os');
12
- const path = require('node:path');
13
-
14
- let tmpHome;
15
- let realHome;
16
- let rcaFeedbackHook;
17
-
18
- // config.cjs and rcaFeedbackHook.cjs both bake os.homedir() into top-level
19
- // consts at first require (allowedRoots/WRITE_PREFIXES, SM_REPO_ROOT).
20
- // Node's require cache must be purged per test so each test's tmpHome takes
21
- // effect — vi.resetModules() only resets vitest's ESM graph, not require().
22
- const MODULES_TO_RELOAD = ['../lib/rcaFeedbackHook.cjs', '../config.cjs'];
23
-
24
- function purgeRequireCache() {
25
- for (const m of MODULES_TO_RELOAD) {
26
- try { delete require.cache[require.resolve(m)]; } catch { /* not loaded yet */ }
27
- }
28
- }
29
-
30
- beforeEach(() => {
31
- realHome = process.env.HOME;
32
- tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'rca-feedback-hook-test-'));
33
- process.env.HOME = tmpHome;
34
- delete process.env.SM_RCA_DISABLE;
35
- purgeRequireCache();
36
- rcaFeedbackHook = require('../lib/rcaFeedbackHook.cjs');
37
- // The session-manager-repo fallback destination must exist as a real dir.
38
- fs.mkdirSync(path.join(tmpHome, 'Projects', 'session-manager', 'session-manager-operations', 'feedback'), { recursive: true });
39
- });
40
-
41
- afterEach(() => {
42
- process.env.HOME = realHome;
43
- delete process.env.SM_RCA_DISABLE;
44
- fs.rmSync(tmpHome, { recursive: true, force: true });
45
- purgeRequireCache();
46
- });
47
-
48
- // ─── helpers ──────────────────────────────────────────────────────────────────
49
-
50
- function makeProjectWithInbox(name = 'proj') {
51
- const cwd = path.join(tmpHome, name);
52
- fs.mkdirSync(path.join(cwd, 'session-manager-operations', 'feedback'), { recursive: true });
53
- return cwd;
54
- }
55
-
56
- function makeProjectWithoutInbox(name = 'bare-proj') {
57
- const cwd = path.join(tmpHome, name);
58
- fs.mkdirSync(cwd, { recursive: true });
59
- return cwd;
60
- }
61
-
62
- function writeRun(runDir, slug, { logLines = ['line1', 'line2'], exitCode = 1, durationMs = 1234 } = {}) {
63
- fs.mkdirSync(runDir, { recursive: true });
64
- fs.writeFileSync(path.join(runDir, `${slug}.log`), logLines.join('\n') + '\n');
65
- fs.writeFileSync(path.join(runDir, `${slug}.meta.json`), JSON.stringify({ exitCode, durationMs }));
66
- }
67
-
68
- function writePrd(cwd, slug, { acBody = '- [ ] `timeout 10 echo ok` passes.' } = {}) {
69
- const prdsDir = path.join(cwd, 'session-manager-operations', 'scheduler', 'prds');
70
- fs.mkdirSync(prdsDir, { recursive: true });
71
- const body = ['---', `title: ${slug}`, '---', '', '# Goal', '', 'Do the thing.', '', '# Acceptance criteria', '', acBody, '', '# Out of scope', '', '- N/A'].join('\n');
72
- fs.writeFileSync(path.join(prdsDir, `${slug}.md`), body);
73
- }
74
-
75
- function baseJob(overrides = {}) {
76
- return { slug: 'testslug', runId: '2026-07-24T00-00-00-000Z', exitCode: 1, error: 'something broke', ...overrides };
77
- }
78
-
79
- // ─── dedupe / new-runId ───────────────────────────────────────────────────────
80
-
81
- // ─── proposal filing (replaced the feedback-file inbox) ──────────────────────
82
-
83
- function readSessions(cwd) {
84
- const p = path.join(cwd, 'session-manager-operations', 'prompt-sessions', 'active-index.json');
85
- if (!fs.existsSync(p)) return {};
86
- return JSON.parse(fs.readFileSync(p, 'utf8')).sessions ?? {};
87
- }
88
-
89
- test('fileRcaFeedback: files a PROPOSED Epic carrying the RCA as its opening prompt', async () => {
90
- const cwd = makeProjectWithInbox();
91
- const runDir = path.join(tmpHome, 'run1');
92
- writeRun(runDir, 'testslug');
93
- writePrd(cwd, 'testslug');
94
-
95
- const res = await rcaFeedbackHook.fileRcaFeedback({ job: baseJob({ cwd }), runDir, verdict: 'transcript_errors' });
96
- expect(res.filed).toBe(true);
97
- expect(res.created).toBe(true);
98
-
99
- const sessions = readSessions(cwd);
100
- const epic = sessions[res.epicId];
101
- // Nothing may auto-run: the whole point is a human gate.
102
- expect(epic.status).toBe('proposed');
103
- expect(epic.tag).toBe('bug');
104
- expect(epic.goalText).toContain('testslug');
105
- // Short title for the list, full RCA body for the first prompt.
106
- expect(epic.goalText.split('\n').length).toBe(1);
107
- expect(epic.openingPrompt).toContain('Root cause');
108
- expect(epic.openingPrompt).toContain('prdSlug: testslug');
109
- });
110
-
111
- test('fileRcaFeedback: a re-trigger for the same failure joins the proposal instead of duplicating it', async () => {
112
- const cwd = makeProjectWithInbox();
113
- const runDir = path.join(tmpHome, 'run1');
114
- writeRun(runDir, 'testslug');
115
- writePrd(cwd, 'testslug');
116
- const job = baseJob({ cwd });
117
-
118
- const first = await rcaFeedbackHook.fileRcaFeedback({ job, runDir, verdict: 'transcript_errors' });
119
- const second = await rcaFeedbackHook.fileRcaFeedback({ job, runDir, verdict: 'transcript_errors' });
120
-
121
- expect(first.created).toBe(true);
122
- expect(second.created).toBe(false);
123
- expect(second.epicId).toBe(first.epicId);
124
- expect(Object.keys(readSessions(cwd)).length).toBe(1);
125
- });
126
-
127
- test('fileRcaFeedback: a different verdict for the same slug is its own proposal', async () => {
128
- const cwd = makeProjectWithInbox();
129
- const runDir = path.join(tmpHome, 'run1');
130
- writeRun(runDir, 'testslug');
131
- writePrd(cwd, 'testslug');
132
-
133
- const a = await rcaFeedbackHook.fileRcaFeedback({ job: baseJob({ cwd }), runDir, verdict: 'transcript_errors' });
134
- const b = await rcaFeedbackHook.fileRcaFeedback({ job: baseJob({ cwd }), runDir, verdict: 'pass_no_commit' });
135
-
136
- expect(a.epicId).not.toBe(b.epicId);
137
- expect(Object.keys(readSessions(cwd)).length).toBe(2);
138
- });
139
-
140
- test('fileRcaFeedback: files into the job\'s own project, no feedback dir required', async () => {
141
- const cwd = makeProjectWithoutInbox();
142
- const runDir = path.join(tmpHome, 'run1');
143
- writeRun(runDir, 'testslug');
144
-
145
- const res = await rcaFeedbackHook.fileRcaFeedback({ job: baseJob({ cwd }), runDir, verdict: 'transcript_errors' });
146
- expect(res.filed).toBe(true);
147
- expect(readSessions(cwd)[res.epicId].status).toBe('proposed');
148
- });
149
-
150
- // ─── epic targeting (job.epicId → preferred join target) ───────────────────
151
-
152
- function writeActiveIndex(cwd, sessions) {
153
- const dir = path.join(cwd, 'session-manager-operations', 'prompt-sessions');
154
- fs.mkdirSync(dir, { recursive: true });
155
- const events = {};
156
- for (const id of Object.keys(sessions)) {
157
- events[id] = [{ id: `${id}-e0`, promptSessionId: id, kind: 'prompt', causedByEventId: null, at: new Date(0).toISOString(), text: sessions[id].goalText }];
158
- }
159
- fs.writeFileSync(path.join(dir, 'active-index.json'), JSON.stringify({ sessions, events }));
160
- }
161
-
162
- test('fileRcaFeedback: joins the job\'s known origin Epic when it is still open, and tags source', async () => {
163
- const cwd = makeProjectWithInbox();
164
- const runDir = path.join(tmpHome, 'run1');
165
- writeRun(runDir, 'testslug');
166
- writePrd(cwd, 'testslug');
167
-
168
- writeActiveIndex(cwd, {
169
- 'origin-epic-1': { id: 'origin-epic-1', cwd, goalText: 'Unrelated title text', status: 'proposed', createdAt: new Date(0).toISOString(), completedAt: null },
170
- });
171
-
172
- const res = await rcaFeedbackHook.fileRcaFeedback({
173
- job: baseJob({ cwd, epicId: 'origin-epic-1' }), runDir, verdict: 'transcript_errors',
174
- });
175
-
176
- expect(res.filed).toBe(true);
177
- expect(res.epicId).toBe('origin-epic-1');
178
- expect(res.created).toBe(false);
179
- expect(Object.keys(readSessions(cwd)).length).toBe(1);
180
- });
181
-
182
- test('fileRcaFeedback: does not join a completed origin Epic, falls through to mint', async () => {
183
- const cwd = makeProjectWithInbox();
184
- const runDir = path.join(tmpHome, 'run1');
185
- writeRun(runDir, 'testslug');
186
- writePrd(cwd, 'testslug');
187
-
188
- writeActiveIndex(cwd, {
189
- 'origin-epic-1': { id: 'origin-epic-1', cwd, goalText: 'Unrelated title text', status: 'completed', createdAt: new Date(0).toISOString(), completedAt: new Date(0).toISOString() },
190
- });
191
-
192
- const res = await rcaFeedbackHook.fileRcaFeedback({
193
- job: baseJob({ cwd, epicId: 'origin-epic-1' }), runDir, verdict: 'transcript_errors',
194
- });
195
-
196
- expect(res.filed).toBe(true);
197
- expect(res.epicId).not.toBe('origin-epic-1');
198
- expect(res.created).toBe(true);
199
- const sessions = readSessions(cwd);
200
- expect(sessions[res.epicId].source).toEqual({ producer: 'rca-hook', prdSlug: 'testslug', runId: baseJob().runId });
201
- });
202
-
203
- test('fileRcaFeedback: a nonexistent job.epicId falls through to mint without erroring', async () => {
204
- const cwd = makeProjectWithInbox();
205
- const runDir = path.join(tmpHome, 'run1');
206
- writeRun(runDir, 'testslug');
207
- writePrd(cwd, 'testslug');
208
-
209
- const res = await rcaFeedbackHook.fileRcaFeedback({
210
- job: baseJob({ cwd, epicId: 'does-not-exist' }), runDir, verdict: 'transcript_errors',
211
- });
212
-
213
- expect(res.filed).toBe(true);
214
- expect(res.epicId).not.toBe('does-not-exist');
215
- expect(res.created).toBe(true);
216
- });
217
-
218
- test('fileRcaFeedback: no job.epicId preserves current mint-or-reuseByGoal behavior', async () => {
219
- const cwd = makeProjectWithInbox();
220
- const runDir = path.join(tmpHome, 'run1');
221
- writeRun(runDir, 'testslug');
222
- writePrd(cwd, 'testslug');
223
-
224
- const res = await rcaFeedbackHook.fileRcaFeedback({ job: baseJob({ cwd }), runDir, verdict: 'transcript_errors' });
225
- expect(res.filed).toBe(true);
226
- expect(res.created).toBe(true);
227
- const sessions = readSessions(cwd);
228
- expect(sessions[res.epicId].source).toEqual({ producer: 'rca-hook', prdSlug: 'testslug', runId: baseJob().runId });
229
- });
230
-
231
- test('fileRcaFeedback: rejects a slug containing path-traversal segments', async () => {
232
- const cwd = makeProjectWithInbox();
233
- const runDir = path.join(tmpHome, 'run1');
234
- fs.mkdirSync(runDir, { recursive: true });
235
-
236
- const res = await rcaFeedbackHook.fileRcaFeedback({ job: baseJob({ cwd, slug: '../../etc/passwd' }), runDir, verdict: 'transcript_errors' });
237
- expect(res.filed).toBe(false);
238
- expect(res.reason).toBe('unsafe-slug');
239
- });
240
-
241
- // ─── kill-switch ────────────────────────────────────────────────────────────────
242
-
243
- test('fileRcaFeedback: SM_RCA_DISABLE=1 skips without writing', async () => {
244
- process.env.SM_RCA_DISABLE = '1';
245
- const cwd = makeProjectWithInbox();
246
- const runDir = path.join(tmpHome, 'run1');
247
- writeRun(runDir, 'testslug');
248
- writePrd(cwd, 'testslug');
249
-
250
- const res = await rcaFeedbackHook.fileRcaFeedback({ job: baseJob({ cwd }), runDir, verdict: 'transcript_errors' });
251
- expect(res.filed).toBe(false);
252
- expect(res.reason).toBe('disabled');
253
- expect(fs.readdirSync(path.join(cwd, 'session-manager-operations', 'feedback')).length).toBe(0);
254
- });
255
-
256
- // ─── investigation-text enrichment ──────────────────────────────────────────────
257
-
258
- test('fileRcaFeedback: investigationText enriches the pending proposal in place', async () => {
259
- const cwd = makeProjectWithInbox();
260
- const runDir = path.join(tmpHome, 'run1');
261
- writeRun(runDir, 'testslug');
262
- writePrd(cwd, 'testslug');
263
- const job = baseJob({ cwd });
264
-
265
- const first = await rcaFeedbackHook.fileRcaFeedback({ job, runDir, verdict: 'transcript_errors' });
266
- expect(first.filed).toBe(true);
267
- expect(readSessions(cwd)[first.epicId].openingPrompt).not.toContain('## Investigation analysis');
268
-
269
- const second = await rcaFeedbackHook.fileRcaFeedback({
270
- job, runDir, verdict: 'transcript_errors', investigationText: 'Root cause: forgot to bound the poll loop.',
271
- });
272
- expect(second.epicId).toBe(first.epicId);
273
- expect(second.updated).toBe(true);
274
-
275
- const enriched = readSessions(cwd)[second.epicId];
276
- expect(enriched.openingPrompt).toContain('## Investigation analysis');
277
- expect(enriched.openingPrompt).toContain('forgot to bound the poll loop');
278
- expect(enriched.status).toBe('proposed');
279
- expect(Object.keys(readSessions(cwd)).length).toBe(1);
280
- });