claude-code-session-manager 0.57.0 → 0.58.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/dist/assets/{TiptapBody-B1llI4h-.js → TiptapBody-BEBLdJl_.js} +1 -1
- package/dist/assets/index-DEQzGYa6.js +3227 -0
- package/dist/assets/{index-WUc_GHMf.css → index-DwUffaDq.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__/epicContextDigest.test.cjs +100 -0
- package/src/main/__tests__/epicMint.test.cjs +165 -207
- package/src/main/__tests__/promptSessionSchema.test.cjs +101 -0
- package/src/main/__tests__/promptSessionsCreateEpicHandler.test.cjs +139 -0
- package/src/main/__tests__/queueHistory.test.cjs +14 -0
- package/src/main/__tests__/rcaReport.test.cjs +188 -0
- package/src/main/__tests__/runVerify.test.cjs +109 -6
- package/src/main/__tests__/scheduler-epic-digest.test.cjs +214 -0
- package/src/main/__tests__/scheduler-writeprd-epic-rollback.test.cjs +5 -5
- package/src/main/chatRunner.cjs +10 -1
- package/src/main/index.cjs +2 -4
- package/src/main/ipcSchemas.cjs +47 -2
- package/src/main/lib/agentModelResolve.cjs +97 -0
- package/src/main/lib/auditLog.cjs +2 -2
- package/src/main/lib/epicContextDigest.cjs +78 -0
- package/src/main/lib/epicMint.cjs +81 -164
- package/src/main/lib/promptSessionSchema.cjs +79 -0
- package/src/main/lib/promptSessionsCreateEpic.cjs +68 -0
- package/src/main/lib/queueHistory.cjs +14 -2
- package/src/main/lib/{rcaFeedbackHook.cjs → rcaReport.cjs} +47 -114
- package/src/main/runVerify.cjs +17 -2
- package/src/main/scheduler.cjs +79 -24
- package/src/preload/api.d.ts +25 -48
- package/src/preload/index.cjs +6 -7
- package/dist/assets/index-DdVGnaCd.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
|
@@ -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.
|
package/scripts/propose-epic.cjs
DELETED
|
@@ -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
|
-
});
|