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
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* scheduler-epic-digest.test.cjs — PRD 958: prepend the Epic context digest
|
|
3
|
+
* (PRD 950's epicContextDigest.cjs) to a job's prompt when job.epicId
|
|
4
|
+
* resolves to a known Epic (same resolution resolveOriginSessionId already
|
|
5
|
+
* performs). Additive only: the PRD's own body + FINISH_PROTOCOL must remain
|
|
6
|
+
* byte-for-byte unchanged, and a digest build failure must never block
|
|
7
|
+
* dispatch.
|
|
8
|
+
*
|
|
9
|
+
* Drives the REAL spawn path via SM_CLAUDE_BIN pointing at a stub process
|
|
10
|
+
* that captures the '-p' prompt argv to a file, so the assertions inspect
|
|
11
|
+
* exactly what would have been sent to `claude -p`.
|
|
12
|
+
*
|
|
13
|
+
* Run: timeout 300 npx vitest run src/main/__tests__/scheduler-epic-digest.test.cjs
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
'use strict';
|
|
17
|
+
|
|
18
|
+
import { test, expect, beforeAll, afterAll, afterEach } from 'vitest';
|
|
19
|
+
const fs = require('node:fs');
|
|
20
|
+
const os = require('node:os');
|
|
21
|
+
const path = require('node:path');
|
|
22
|
+
|
|
23
|
+
let tmpHome;
|
|
24
|
+
let originalHome;
|
|
25
|
+
let originalClaudeBin;
|
|
26
|
+
let executeJob;
|
|
27
|
+
let FINISH_PROTOCOL;
|
|
28
|
+
let buildContextDigest;
|
|
29
|
+
|
|
30
|
+
beforeAll(() => {
|
|
31
|
+
originalHome = process.env.HOME;
|
|
32
|
+
originalClaudeBin = process.env.SM_CLAUDE_BIN;
|
|
33
|
+
tmpHome = fs.mkdtempSync(path.join(os.tmpdir(), 'sm-epic-digest-'));
|
|
34
|
+
process.env.HOME = tmpHome;
|
|
35
|
+
|
|
36
|
+
({ executeJob, FINISH_PROTOCOL } = require('../scheduler.cjs'));
|
|
37
|
+
({ buildContextDigest } = require('../lib/epicContextDigest.cjs'));
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
afterAll(() => {
|
|
41
|
+
process.env.HOME = originalHome;
|
|
42
|
+
if (originalClaudeBin === undefined) delete process.env.SM_CLAUDE_BIN;
|
|
43
|
+
else process.env.SM_CLAUDE_BIN = originalClaudeBin;
|
|
44
|
+
fs.rmSync(tmpHome, { recursive: true, force: true });
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
afterEach(() => {
|
|
48
|
+
delete process.env.SM_CLAUDE_BIN;
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
// Stub `claude` binary: captures the '-p' prompt argv to `capturePath`, then
|
|
52
|
+
// emits one stream-json result line and exits 0.
|
|
53
|
+
function writeClaudeStub(capturePath) {
|
|
54
|
+
const stubPath = path.join(os.tmpdir(), `sm-claude-stub-${process.pid}-${Math.floor(Math.random() * 1e9)}.cjs`);
|
|
55
|
+
const body = `
|
|
56
|
+
const fs = require('fs');
|
|
57
|
+
const idx = process.argv.indexOf('-p');
|
|
58
|
+
const prompt = idx >= 0 ? process.argv[idx + 1] : '';
|
|
59
|
+
fs.writeFileSync(${JSON.stringify(capturePath)}, prompt, 'utf8');
|
|
60
|
+
process.stdout.write(JSON.stringify({ type: 'result', subtype: 'success', result: 'ok' }) + '\\n');
|
|
61
|
+
process.exit(0);
|
|
62
|
+
`;
|
|
63
|
+
fs.writeFileSync(stubPath, `#!${process.execPath}\n${body}\n`, { mode: 0o755 });
|
|
64
|
+
return stubPath;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function setupProject(prefix) {
|
|
68
|
+
const projectCwd = fs.mkdtempSync(path.join(os.tmpdir(), `${prefix}-project-`));
|
|
69
|
+
fs.mkdirSync(path.join(tmpHome, '.claude'), { recursive: true });
|
|
70
|
+
const runDir = fs.mkdtempSync(path.join(tmpHome, '.claude', `${prefix}-run-`));
|
|
71
|
+
return { projectCwd, runDir };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function writeLegacyPrd(projectCwd, slug, bodyText) {
|
|
75
|
+
const prdsDir = path.join(projectCwd, 'session-manager-operations', 'scheduler', 'prds');
|
|
76
|
+
fs.mkdirSync(prdsDir, { recursive: true });
|
|
77
|
+
fs.writeFileSync(path.join(prdsDir, `${slug}.md`), bodyText, 'utf8');
|
|
78
|
+
return path.join(prdsDir, `${slug}.md`);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function writeActiveIndex(projectCwd, sessions) {
|
|
82
|
+
const idxPath = path.join(projectCwd, 'session-manager-operations', 'prompt-sessions', 'active-index.json');
|
|
83
|
+
fs.mkdirSync(path.dirname(idxPath), { recursive: true });
|
|
84
|
+
fs.writeFileSync(idxPath, JSON.stringify({ sessions, events: {} }, null, 2), 'utf8');
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
test('prepends the Epic context digest when job.epicId resolves to a known Epic', async () => {
|
|
88
|
+
const { projectCwd, runDir } = setupProject('sm-epic-digest-hit');
|
|
89
|
+
try {
|
|
90
|
+
const epicId = 'epic-hit-1';
|
|
91
|
+
writeActiveIndex(projectCwd, {
|
|
92
|
+
[epicId]: { claudeSessionId: 'sess-abc-123', goalText: 'Ship the digest feature.' },
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
const slug = `958-test-hit-${process.pid}-${Math.floor(Math.random() * 1e6)}`;
|
|
96
|
+
const bodyText = 'Do the thing described in this PRD body.';
|
|
97
|
+
writeLegacyPrd(projectCwd, slug, bodyText);
|
|
98
|
+
|
|
99
|
+
const capturePath = path.join(runDir, 'captured-prompt.txt');
|
|
100
|
+
process.env.SM_CLAUDE_BIN = writeClaudeStub(capturePath);
|
|
101
|
+
|
|
102
|
+
const job = { slug, cwd: projectCwd, epicId };
|
|
103
|
+
const result = await executeJob(job, runDir, projectCwd, () => Promise.resolve());
|
|
104
|
+
|
|
105
|
+
expect(result.exitCode).toBe(0);
|
|
106
|
+
const capturedPrompt = fs.readFileSync(capturePath, 'utf8');
|
|
107
|
+
|
|
108
|
+
const digestText = await buildContextDigest({ cwd: projectCwd, epicId });
|
|
109
|
+
expect(digestText.length).toBeGreaterThan(0);
|
|
110
|
+
expect(capturedPrompt.startsWith(digestText)).toBe(true);
|
|
111
|
+
expect(capturedPrompt.endsWith(bodyText + FINISH_PROTOCOL)).toBe(true);
|
|
112
|
+
|
|
113
|
+
// The PRD source on disk must never be rewritten with the digest merged in.
|
|
114
|
+
const onDiskPrd = fs.readFileSync(path.join(projectCwd, 'session-manager-operations', 'scheduler', 'prds', `${slug}.md`), 'utf8');
|
|
115
|
+
expect(onDiskPrd).toBe(bodyText);
|
|
116
|
+
|
|
117
|
+
const metaPath = path.join(runDir, `${slug}.meta.json`);
|
|
118
|
+
const meta = JSON.parse(fs.readFileSync(metaPath, 'utf8'));
|
|
119
|
+
expect(meta.contextDigestApplied).toBe(true);
|
|
120
|
+
expect(meta.originSessionId).toBe('sess-abc-123');
|
|
121
|
+
} finally {
|
|
122
|
+
fs.rmSync(projectCwd, { recursive: true, force: true });
|
|
123
|
+
fs.rmSync(runDir, { recursive: true, force: true });
|
|
124
|
+
}
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
test('is a silent no-op when job.epicId does not resolve to a known Epic', async () => {
|
|
128
|
+
const { projectCwd, runDir } = setupProject('sm-epic-digest-miss');
|
|
129
|
+
try {
|
|
130
|
+
const slug = `958-test-miss-${process.pid}-${Math.floor(Math.random() * 1e6)}`;
|
|
131
|
+
const bodyText = 'Do the thing described in this PRD body, unaccompanied.';
|
|
132
|
+
writeLegacyPrd(projectCwd, slug, bodyText);
|
|
133
|
+
|
|
134
|
+
const capturePath = path.join(runDir, 'captured-prompt.txt');
|
|
135
|
+
process.env.SM_CLAUDE_BIN = writeClaudeStub(capturePath);
|
|
136
|
+
|
|
137
|
+
// epicId is absent entirely — no active-index.json even exists for this project.
|
|
138
|
+
const job = { slug, cwd: projectCwd };
|
|
139
|
+
const result = await executeJob(job, runDir, projectCwd, () => Promise.resolve());
|
|
140
|
+
|
|
141
|
+
expect(result.exitCode).toBe(0);
|
|
142
|
+
const capturedPrompt = fs.readFileSync(capturePath, 'utf8');
|
|
143
|
+
expect(capturedPrompt).toBe(bodyText + FINISH_PROTOCOL);
|
|
144
|
+
|
|
145
|
+
const metaPath = path.join(runDir, `${slug}.meta.json`);
|
|
146
|
+
const meta = JSON.parse(fs.readFileSync(metaPath, 'utf8'));
|
|
147
|
+
expect(meta.contextDigestApplied).toBe(false);
|
|
148
|
+
expect(meta.originSessionId).toBe(null);
|
|
149
|
+
} finally {
|
|
150
|
+
fs.rmSync(projectCwd, { recursive: true, force: true });
|
|
151
|
+
fs.rmSync(runDir, { recursive: true, force: true });
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
test('a digest build failure is caught and never blocks dispatch', async () => {
|
|
156
|
+
const { projectCwd, runDir } = setupProject('sm-epic-digest-fail');
|
|
157
|
+
try {
|
|
158
|
+
const epicId = 'epic-fail-1';
|
|
159
|
+
writeActiveIndex(projectCwd, {
|
|
160
|
+
[epicId]: { claudeSessionId: 'sess-def-456', goalText: 'This Epic exists and resolves fine.' },
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
const slug = `958-test-fail-${process.pid}-${Math.floor(Math.random() * 1e6)}`;
|
|
164
|
+
const bodyText = 'Do the thing even if the digest build blows up.';
|
|
165
|
+
writeLegacyPrd(projectCwd, slug, bodyText);
|
|
166
|
+
|
|
167
|
+
const capturePath = path.join(runDir, 'captured-prompt.txt');
|
|
168
|
+
process.env.SM_CLAUDE_BIN = writeClaudeStub(capturePath);
|
|
169
|
+
|
|
170
|
+
// Stub epicContextDigest.cjs's export to reject, then force scheduler.cjs
|
|
171
|
+
// to re-require it so it picks up the stub (mirrors exchanges.test.cjs's
|
|
172
|
+
// summarize.cjs stubbing pattern) — only scheduler.cjs's own cache entry
|
|
173
|
+
// is cleared, not its whole dependency tree.
|
|
174
|
+
const digestPath = require.resolve('../lib/epicContextDigest.cjs');
|
|
175
|
+
const realDigestModule = require.cache[digestPath];
|
|
176
|
+
require.cache[digestPath] = {
|
|
177
|
+
id: digestPath,
|
|
178
|
+
filename: digestPath,
|
|
179
|
+
loaded: true,
|
|
180
|
+
exports: {
|
|
181
|
+
buildContextDigest: async () => {
|
|
182
|
+
throw new Error('boom: digest build exploded');
|
|
183
|
+
},
|
|
184
|
+
},
|
|
185
|
+
};
|
|
186
|
+
const schedulerPath = require.resolve('../scheduler.cjs');
|
|
187
|
+
delete require.cache[schedulerPath];
|
|
188
|
+
const freshScheduler = require('../scheduler.cjs');
|
|
189
|
+
|
|
190
|
+
try {
|
|
191
|
+
const job = { slug, cwd: projectCwd, epicId };
|
|
192
|
+
const result = await freshScheduler.executeJob(job, runDir, projectCwd, () => Promise.resolve());
|
|
193
|
+
|
|
194
|
+
expect(result.exitCode).toBe(0);
|
|
195
|
+
const capturedPrompt = fs.readFileSync(capturePath, 'utf8');
|
|
196
|
+
// Digest failed to build — prompt falls back to the un-prefixed PRD body.
|
|
197
|
+
expect(capturedPrompt).toBe(bodyText + FINISH_PROTOCOL);
|
|
198
|
+
|
|
199
|
+
const metaPath = path.join(runDir, `${slug}.meta.json`);
|
|
200
|
+
const meta = JSON.parse(fs.readFileSync(metaPath, 'utf8'));
|
|
201
|
+
expect(meta.contextDigestApplied).toBe(false);
|
|
202
|
+
expect(meta.originSessionId).toBe('sess-def-456');
|
|
203
|
+
} finally {
|
|
204
|
+
// Restore real modules for any later test in this file/process.
|
|
205
|
+
if (realDigestModule) require.cache[digestPath] = realDigestModule;
|
|
206
|
+
else delete require.cache[digestPath];
|
|
207
|
+
delete require.cache[schedulerPath];
|
|
208
|
+
({ executeJob, FINISH_PROTOCOL } = require('../scheduler.cjs'));
|
|
209
|
+
}
|
|
210
|
+
} finally {
|
|
211
|
+
fs.rmSync(projectCwd, { recursive: true, force: true });
|
|
212
|
+
fs.rmSync(runDir, { recursive: true, force: true });
|
|
213
|
+
}
|
|
214
|
+
});
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* scheduler-writeprd-epic-rollback.test.cjs — remote.writePrd may only JOIN
|
|
3
3
|
* an existing, already-human-approved Epic (via `sourcePromptId`); it must
|
|
4
|
-
* never mint a new one (
|
|
4
|
+
* never mint a new one (no mintAuthority, see epicMint.cjs's ensureEpic).
|
|
5
5
|
* A write with no resolvable Epic fails outright with zero residual
|
|
6
6
|
* active-index.json entries — no orphaned Epic can ever be left behind by
|
|
7
7
|
* this path because none is ever created by it.
|
|
@@ -19,7 +19,7 @@ const fs = require('node:fs');
|
|
|
19
19
|
const os = require('node:os');
|
|
20
20
|
const path = require('node:path');
|
|
21
21
|
const { remote } = require('../scheduler.cjs');
|
|
22
|
-
const { ensureEpic, readActiveIndex } = require('../lib/epicMint.cjs');
|
|
22
|
+
const { ensureEpic, readActiveIndex, MINT_AUTHORITY_NEW_EPIC_UI } = require('../lib/epicMint.cjs');
|
|
23
23
|
const config = require('../config.cjs');
|
|
24
24
|
|
|
25
25
|
const tmpDirs = [];
|
|
@@ -61,7 +61,7 @@ test('writePrd: no sourcePromptId refuses to write (no Epic to join) and mints n
|
|
|
61
61
|
test('writePrd: invalid-slug failure when joining a pre-existing Epic leaves that Epic untouched', async () => {
|
|
62
62
|
const cwd = makeFixtureCwd();
|
|
63
63
|
|
|
64
|
-
const preexisting = await ensureEpic(cwd, { goalText: 'pre-existing epic goal' });
|
|
64
|
+
const preexisting = await ensureEpic(cwd, { goalText: 'pre-existing epic goal', mintAuthority: MINT_AUTHORITY_NEW_EPIC_UI });
|
|
65
65
|
expect(preexisting.created).toBe(true);
|
|
66
66
|
|
|
67
67
|
const beforeIndex = readActiveIndex(cwd);
|
|
@@ -81,7 +81,7 @@ test('writePrd: invalid-slug failure when joining a pre-existing Epic leaves tha
|
|
|
81
81
|
|
|
82
82
|
// Provenance (PRD 905): writePrd's ensureEpic call passes
|
|
83
83
|
// source: { producer: 'scheduler-dispatch', prdSlug }, but this call is
|
|
84
|
-
// join-only (
|
|
84
|
+
// join-only (no mintAuthority, explicit epicId) — ensureEpic's
|
|
85
85
|
// explicitEpicId branch returns before ever reading `source`, so a
|
|
86
86
|
// successful join must NOT write/alter the joined Epic's `source` field.
|
|
87
87
|
test('writePrd: successful join does not write a source field onto the pre-existing Epic (join-only no-op)', async () => {
|
|
@@ -92,7 +92,7 @@ test('writePrd: successful join does not write a source field onto the pre-exist
|
|
|
92
92
|
tmpDirs.push(cwd);
|
|
93
93
|
config.addAllowedRoot(cwd);
|
|
94
94
|
|
|
95
|
-
const preexisting = await ensureEpic(cwd, { goalText: 'pre-existing epic goal for join' });
|
|
95
|
+
const preexisting = await ensureEpic(cwd, { goalText: 'pre-existing epic goal for join', mintAuthority: MINT_AUTHORITY_NEW_EPIC_UI });
|
|
96
96
|
expect(preexisting.created).toBe(true);
|
|
97
97
|
|
|
98
98
|
const body = `---\ntitle: Joins the existing epic\nsourcePromptId: ${preexisting.epicId}\n---\n\nbody text\n`;
|
package/src/main/chatRunner.cjs
CHANGED
|
@@ -52,6 +52,7 @@ const { extractJson } = require('./lib/extractJson.cjs');
|
|
|
52
52
|
const { classifyPromptTicket } = require('./lib/classifyPromptTicket.cjs');
|
|
53
53
|
const sessionSlots = require('./lib/sessionSlots.cjs');
|
|
54
54
|
const opsErrorLog = require('./lib/opsErrorLog.cjs');
|
|
55
|
+
const agentModelResolve = require('./lib/agentModelResolve.cjs');
|
|
55
56
|
|
|
56
57
|
// ─── Stop-signal protocol ──────────────────────────────────────────────────
|
|
57
58
|
// Single source of truth for the sentinel and parser. The renderer (PRD 320)
|
|
@@ -522,10 +523,18 @@ function executeRun({ tabId, sessionId, prompt, cwd, resume, silent, onSilentRes
|
|
|
522
523
|
// instruction to every prompt
|
|
523
524
|
const fullPrompt = STOP_SIGNAL_INSTRUCTION + CHAT_MODE_TRUTH_INSTRUCTION + prompt;
|
|
524
525
|
|
|
526
|
+
// sessionId is the Epic's claudeSessionId for an Epic-backed tab (see
|
|
527
|
+
// comment above childEnv) — resolve that Epic's agentType persona model
|
|
528
|
+
// so Chat and Terminal views of the same Epic launch with the same
|
|
529
|
+
// model. Never throws; falls back to the hardcoded 'sonnet' literal when
|
|
530
|
+
// there's no Epic, no agentType, or the persona has no model/'inherit' —
|
|
531
|
+
// --model must never be left unpinned (CLAUDE.md model-pinning rule).
|
|
532
|
+
const model = agentModelResolve.resolveEpicModel({ cwd, claudeSessionId: sessionId });
|
|
533
|
+
|
|
525
534
|
// Build argv as an array — no shell: true, no string interpolation
|
|
526
535
|
const args = [
|
|
527
536
|
'-p', fullPrompt,
|
|
528
|
-
'--model',
|
|
537
|
+
'--model', model,
|
|
529
538
|
'--dangerously-skip-permissions',
|
|
530
539
|
'--output-format', 'stream-json',
|
|
531
540
|
'--verbose',
|
package/src/main/index.cjs
CHANGED
|
@@ -32,6 +32,7 @@ const { appendAuditEvent } = require('./lib/auditLog.cjs');
|
|
|
32
32
|
const chatRunner = require('./chatRunner.cjs');
|
|
33
33
|
const promptSessionEvents = require('./promptSessionEvents.cjs');
|
|
34
34
|
const activeIndexMerge = require('./lib/activeIndexMerge.cjs');
|
|
35
|
+
const promptSessionsCreateEpic = require('./lib/promptSessionsCreateEpic.cjs');
|
|
35
36
|
const agentLibrary = require('./agentLibrary.cjs');
|
|
36
37
|
const { sendIfAlive } = require('./lib/sendToRenderer.cjs');
|
|
37
38
|
const { resolveBuildTarget } = require('./lib/buildTarget.cjs');
|
|
@@ -68,8 +69,6 @@ const agentMemory = require('./agentMemory.cjs');
|
|
|
68
69
|
const git = require('./git.cjs');
|
|
69
70
|
const filesIpc = require('./files.cjs');
|
|
70
71
|
const { registerDocEditHandlers, attachWindow: attachDocEditWindow } = require('./docEdit.cjs');
|
|
71
|
-
const searchIpc = require('./search.cjs');
|
|
72
|
-
const repoAnalyzer = require('./repoAnalyzer.cjs');
|
|
73
72
|
const webRemote = require('./webRemote.cjs');
|
|
74
73
|
const { listExchanges } = require('./exchanges.cjs');
|
|
75
74
|
const { resolveClaudeBin } = require('./lib/claudeBin.cjs');
|
|
@@ -819,6 +818,7 @@ registerHistoryDashboardHandlers();
|
|
|
819
818
|
pluginInstall.registerPluginInstallHandlers();
|
|
820
819
|
memoryTool.registerMemoryHandlers();
|
|
821
820
|
activeIndexMerge.registerActiveIndexMergeHandlers();
|
|
821
|
+
promptSessionsCreateEpic.registerPromptSessionsCreateEpicHandlers();
|
|
822
822
|
registerMemoryAggregateIpc();
|
|
823
823
|
registerProjectBriefIpc();
|
|
824
824
|
registerProjectPagesIpc();
|
|
@@ -841,8 +841,6 @@ agentMemory.registerAgentMemoryHandlers();
|
|
|
841
841
|
git.register(ipcMain);
|
|
842
842
|
filesIpc.registerFilesHandlers();
|
|
843
843
|
registerDocEditHandlers();
|
|
844
|
-
searchIpc.registerSearchHandlers();
|
|
845
|
-
repoAnalyzer.register(ipcMain);
|
|
846
844
|
webRemote.registerRemoteHandlers();
|
|
847
845
|
chatRunner.registerChatHandlers();
|
|
848
846
|
|
package/src/main/ipcSchemas.cjs
CHANGED
|
@@ -8,6 +8,7 @@
|
|
|
8
8
|
const { z } = require('zod');
|
|
9
9
|
const os = require('node:os');
|
|
10
10
|
const path = require('node:path');
|
|
11
|
+
const { PromptSessionSchema, EpicTagSchema, EpicSourceSchema } = require('./lib/promptSessionSchema.cjs');
|
|
11
12
|
|
|
12
13
|
// ──────────────────────────────────────────── PTY
|
|
13
14
|
const ptySpawn = z.object({
|
|
@@ -229,9 +230,35 @@ const configParseImports = z.object({ path: z.string().min(1).max(4096) });
|
|
|
229
230
|
// Real epic ids are `${slugify(goalText)}-${uuid8}` (epicMint.cjs) — max 64
|
|
230
231
|
// mirrors slugify's 48-char cap plus the "-" + 8 hex chars suffix, rounded up.
|
|
231
232
|
const PROMPT_SESSION_ID_RE = /^(?!__proto__$|constructor$|prototype$)[A-Za-z0-9_-]{1,64}$/;
|
|
233
|
+
|
|
234
|
+
// Per-ROW validation, not per-BATCH. persistActiveIndex (state/promptSessions.ts)
|
|
235
|
+
// sends every open Epic for a cwd in ONE call, so validating the map with a
|
|
236
|
+
// single parse meant one malformed/legacy row rejected the whole payload —
|
|
237
|
+
// every other legitimate Epic for that cwd silently stopped persisting (the
|
|
238
|
+
// renderer only logs the failure). Dropping just the offending row matches how
|
|
239
|
+
// the rest of this pipeline already fails: per-id (activeIndexMerge.cjs's
|
|
240
|
+
// tombstone/resurrection handling), never per-batch.
|
|
241
|
+
const dropInvalidSessions = (raw) => {
|
|
242
|
+
if (!raw || typeof raw !== 'object' || Array.isArray(raw)) return raw;
|
|
243
|
+
const kept = Object.create(null);
|
|
244
|
+
for (const id of Object.keys(raw)) {
|
|
245
|
+
if (!PROMPT_SESSION_ID_RE.test(id)) {
|
|
246
|
+
console.warn(`[ipc] merge-active-index: dropping session with invalid id ${JSON.stringify(id)}`);
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
const parsed = PromptSessionSchema.safeParse(raw[id]);
|
|
250
|
+
if (!parsed.success) {
|
|
251
|
+
console.warn(`[ipc] merge-active-index: dropping malformed session ${id} — ${parsed.error.issues.map((i) => `${i.path.join('.')}: ${i.message}`).join('; ')}`);
|
|
252
|
+
continue;
|
|
253
|
+
}
|
|
254
|
+
kept[id] = raw[id];
|
|
255
|
+
}
|
|
256
|
+
return kept;
|
|
257
|
+
};
|
|
258
|
+
|
|
232
259
|
const promptSessionsMergeActiveIndex = z.object({
|
|
233
260
|
cwd: z.string().min(1).max(4096),
|
|
234
|
-
sessions: z.record(z.string().regex(PROMPT_SESSION_ID_RE),
|
|
261
|
+
sessions: z.preprocess(dropInvalidSessions, z.record(z.string().regex(PROMPT_SESSION_ID_RE), PromptSessionSchema)).refine(
|
|
235
262
|
(v) => Object.keys(v).length <= 2000,
|
|
236
263
|
{ message: 'too many sessions in one merge payload' },
|
|
237
264
|
),
|
|
@@ -247,6 +274,19 @@ const promptSessionsMergeActiveIndex = z.object({
|
|
|
247
274
|
source: z.literal('epics'),
|
|
248
275
|
});
|
|
249
276
|
|
|
277
|
+
// Renderer entry point for main's ensureEpic() (lib/promptSessionsCreateEpic.cjs)
|
|
278
|
+
// — additive step 1 of a two-PRD chain; unused by the app until the next PRD
|
|
279
|
+
// routes state/promptSessions.ts's createPromptSession through it. `tag`/
|
|
280
|
+
// `source` reuse promptSessionSchema.cjs's own enums rather than retyping
|
|
281
|
+
// the value lists a third time.
|
|
282
|
+
const promptSessionsCreateEpic = z.object({
|
|
283
|
+
cwd: z.string().min(1).max(4096),
|
|
284
|
+
goalText: z.string().min(1).max(20000),
|
|
285
|
+
tag: EpicTagSchema.optional(),
|
|
286
|
+
agentType: z.string().min(1).max(256).optional(),
|
|
287
|
+
source: EpicSourceSchema.optional(),
|
|
288
|
+
});
|
|
289
|
+
|
|
250
290
|
// ──────────────────────────────────────────── Sessions
|
|
251
291
|
const sessionsPayload = z.object({
|
|
252
292
|
tabs: z.array(z.object({
|
|
@@ -345,7 +385,11 @@ const schedulerCreatePrd = z.object({
|
|
|
345
385
|
// forgot to pass sourcePromptId explicitly.
|
|
346
386
|
originClaudeSessionId: z.string().min(1).max(128).regex(NO_NEWLINE_RE, 'must not contain newlines').optional(),
|
|
347
387
|
// User-selected Feature/Bug tag (PRD 774) carried from the originating
|
|
348
|
-
// PromptTicket — deterministic, never LLM-classified.
|
|
388
|
+
// PromptTicket — deterministic, never LLM-classified. NOT the same value
|
|
389
|
+
// set as PromptSession's Epic-mission `tag` (lib/promptSessionSchema.cjs's
|
|
390
|
+
// EpicTagSchema) — that one also includes 'bilko-host-publisher'. Keep
|
|
391
|
+
// these two enums in sync deliberately if their meanings ever converge;
|
|
392
|
+
// don't assume editing one updates the other.
|
|
349
393
|
tag: z.enum(['feature', 'bug', 'discussion', 'build', 'project-home-builder']).optional(),
|
|
350
394
|
});
|
|
351
395
|
|
|
@@ -971,6 +1015,7 @@ module.exports = {
|
|
|
971
1015
|
promptSessionTranscriptAppend,
|
|
972
1016
|
promptSessionTranscriptRead,
|
|
973
1017
|
promptSessionsMergeActiveIndex,
|
|
1018
|
+
promptSessionsCreateEpic,
|
|
974
1019
|
auditLogAppend,
|
|
975
1020
|
agentMemoryList,
|
|
976
1021
|
agentMemoryGet,
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* agentModelResolve.cjs — resolves the `--model` an Epic's session should
|
|
5
|
+
* launch a headless `claude -p` (or Terminal `claude`) process with: its
|
|
6
|
+
* agentType persona's own `model` field (Agent Library) when the Epic has
|
|
7
|
+
* one set and it isn't 'inherit', else `fallbackModel`.
|
|
8
|
+
*
|
|
9
|
+
* Extracted from PRD agent-model-default-terminal (which wired this for the
|
|
10
|
+
* Terminal-view launch in EpicTerminalPane.tsx) so chatRunner.cjs's headless
|
|
11
|
+
* Chat-view launch resolves the SAME persona-derived model — Chat and
|
|
12
|
+
* Terminal are two VIEWS over one Epic session (CLAUDE.md) and must agree on
|
|
13
|
+
* which model that session launches with.
|
|
14
|
+
*
|
|
15
|
+
* Plain Node module (sync fs) — no Electron deps, mirrors epicMint.cjs's
|
|
16
|
+
* readActiveIndex so this can be called from chatRunner.cjs's synchronous
|
|
17
|
+
* executeRun() without restructuring it into an async flow.
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
const fs = require('node:fs');
|
|
21
|
+
const path = require('node:path');
|
|
22
|
+
const os = require('node:os');
|
|
23
|
+
const { splitFrontmatter } = require('./prdFrontmatter.cjs');
|
|
24
|
+
const { readActiveIndex } = require('./epicMint.cjs');
|
|
25
|
+
const configMgr = require('../config.cjs');
|
|
26
|
+
|
|
27
|
+
/** The hardcoded floor every branch of resolveEpicModel falls back to — --model must never be omitted (CLAUDE.md "Automation model pinning"). */
|
|
28
|
+
const FALLBACK_MODEL = 'sonnet';
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Looks up the agentType an Epic (PromptSession) was created with, by the
|
|
32
|
+
* Epic's own claudeSessionId — the same domain-model join `prdCreate.cjs`'s
|
|
33
|
+
* resolveSourcePromptIdFromClaudeSession uses (Epic:claude-session is 1:1).
|
|
34
|
+
* Returns null if no matching Epic is found or it has no agentType set.
|
|
35
|
+
*/
|
|
36
|
+
function findAgentTypeByClaudeSessionId(cwd, claudeSessionId, deps = {}) {
|
|
37
|
+
if (!cwd || !claudeSessionId) return null;
|
|
38
|
+
const load = deps.readActiveIndex || readActiveIndex;
|
|
39
|
+
const { sessions } = load(cwd);
|
|
40
|
+
for (const session of Object.values(sessions)) {
|
|
41
|
+
if (session && session.claudeSessionId === claudeSessionId) return session.agentType || null;
|
|
42
|
+
}
|
|
43
|
+
return null;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Reads a global agent persona's `model` frontmatter field by name
|
|
48
|
+
* (`~/.claude/agents/<agentType>.md`), sync. Returns null on any miss
|
|
49
|
+
* (no such file, unreadable, no `model` key) — never throws.
|
|
50
|
+
*/
|
|
51
|
+
function readPersonaModel(agentType, deps = {}) {
|
|
52
|
+
if (!agentType) return null;
|
|
53
|
+
const globalDir = deps.globalDir || path.join(os.homedir(), '.claude', 'agents');
|
|
54
|
+
const validatePath = deps.validatePath || configMgr.validatePath;
|
|
55
|
+
let real;
|
|
56
|
+
try {
|
|
57
|
+
real = validatePath(path.join(globalDir, `${agentType}.md`));
|
|
58
|
+
} catch {
|
|
59
|
+
return null;
|
|
60
|
+
}
|
|
61
|
+
let text;
|
|
62
|
+
try {
|
|
63
|
+
text = fs.readFileSync(real, 'utf8');
|
|
64
|
+
} catch {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
const { fm } = splitFrontmatter(text);
|
|
68
|
+
return fm.model || null;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Resolves the `--model` value for an Epic-backed session launch. Never
|
|
73
|
+
* throws — any lookup failure (missing Epic, missing persona, unreadable
|
|
74
|
+
* file) falls back to `fallbackModel` (default: 'sonnet', the same literal
|
|
75
|
+
* every claude -p call site pins per CLAUDE.md's model-pinning rule).
|
|
76
|
+
*
|
|
77
|
+
* @param {{ cwd: string, claudeSessionId: string, fallbackModel?: string, deps?: object }} opts
|
|
78
|
+
* @returns {string}
|
|
79
|
+
*/
|
|
80
|
+
function resolveEpicModel({ cwd, claudeSessionId, fallbackModel = FALLBACK_MODEL, deps = {} } = {}) {
|
|
81
|
+
try {
|
|
82
|
+
const agentType = findAgentTypeByClaudeSessionId(cwd, claudeSessionId, deps);
|
|
83
|
+
if (!agentType) return fallbackModel;
|
|
84
|
+
const model = readPersonaModel(agentType, deps);
|
|
85
|
+
if (!model || model === 'inherit') return fallbackModel;
|
|
86
|
+
return model;
|
|
87
|
+
} catch {
|
|
88
|
+
return fallbackModel;
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
module.exports = {
|
|
93
|
+
FALLBACK_MODEL,
|
|
94
|
+
resolveEpicModel,
|
|
95
|
+
findAgentTypeByClaudeSessionId,
|
|
96
|
+
readPersonaModel,
|
|
97
|
+
};
|
|
@@ -8,10 +8,10 @@
|
|
|
8
8
|
* survive even if the project's ops root doesn't exist yet.
|
|
9
9
|
*
|
|
10
10
|
* Best-effort only: a logging failure must never block the mint/create it is
|
|
11
|
-
* observing (same posture as
|
|
11
|
+
* observing (same posture as rcaReport.cjs / dodDrainHook.cjs).
|
|
12
12
|
*
|
|
13
13
|
* Plain Node module (no Electron deps) so scripts/mint-epic.cjs and
|
|
14
|
-
* scripts/
|
|
14
|
+
* scripts/mint-epic.cjs (which runs outside Electron) can require it too.
|
|
15
15
|
*/
|
|
16
16
|
'use strict';
|
|
17
17
|
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* epicContextDigest.cjs — condensed, real (never fabricated) digest of an
|
|
5
|
+
* Epic's own session, for later injection into a scheduled PRD job's opening
|
|
6
|
+
* prompt (wiring is a separate, dependent PRD — this module is groundwork
|
|
7
|
+
* only). Composes the Epic's goalText (active-index.json, via epicMint.cjs's
|
|
8
|
+
* readActiveIndex) with its most-recent turns (the JSONL transcript, via
|
|
9
|
+
* promptSessionTranscript.cjs's readTurns) — never reimplements either read.
|
|
10
|
+
*
|
|
11
|
+
* Plain Node module (no Electron deps), matching sessionSlots.cjs's and
|
|
12
|
+
* promptSessionTranscript.cjs's own posture, so it is callable from
|
|
13
|
+
* scheduler.cjs.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const { readTurns } = require('../promptSessionTranscript.cjs');
|
|
17
|
+
const { readActiveIndex } = require('./epicMint.cjs');
|
|
18
|
+
|
|
19
|
+
const DEFAULT_MAX_CHARS = 4000;
|
|
20
|
+
const TURN_LIMIT = 40;
|
|
21
|
+
|
|
22
|
+
function formatTurn(turn) {
|
|
23
|
+
return `[${turn.role}] ${turn.text}`;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* buildContextDigest({ cwd, epicId, maxChars }) → Promise<string>
|
|
28
|
+
*
|
|
29
|
+
* Best-effort: every internal call is wrapped so a failure here can never
|
|
30
|
+
* propagate to a caller — returns '' on any error, missing epicId, an epicId
|
|
31
|
+
* absent from the cwd's active-index.json, or an empty transcript.
|
|
32
|
+
*/
|
|
33
|
+
async function buildContextDigest({ cwd, epicId, maxChars } = {}) {
|
|
34
|
+
try {
|
|
35
|
+
if (!cwd || !epicId) return '';
|
|
36
|
+
const limit = typeof maxChars === 'number' && maxChars > 0 ? maxChars : DEFAULT_MAX_CHARS;
|
|
37
|
+
|
|
38
|
+
const index = readActiveIndex(cwd);
|
|
39
|
+
if (!Object.prototype.hasOwnProperty.call(index.sessions, epicId)) return '';
|
|
40
|
+
const session = index.sessions[epicId];
|
|
41
|
+
const goalText = session && typeof session.goalText === 'string' ? session.goalText.trim() : '';
|
|
42
|
+
|
|
43
|
+
const turns = await readTurns(cwd, epicId, { limit: TURN_LIMIT });
|
|
44
|
+
|
|
45
|
+
const header = goalText;
|
|
46
|
+
const headerChars = header ? header.length + 1 : 0; // +1 for trailing newline before turns
|
|
47
|
+
|
|
48
|
+
// Drop whole turns from the oldest first until the joined digest fits —
|
|
49
|
+
// never a hard mid-string slice that could cut a turn's text in half.
|
|
50
|
+
let kept = turns.slice();
|
|
51
|
+
while (kept.length > 0) {
|
|
52
|
+
const lines = kept.map(formatTurn);
|
|
53
|
+
const bodyChars = lines.length > 0 ? lines.join('\n').length : 0;
|
|
54
|
+
const total = headerChars + bodyChars;
|
|
55
|
+
if (total <= limit) break;
|
|
56
|
+
kept = kept.slice(1);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const parts = [];
|
|
60
|
+
if (header) parts.push(header);
|
|
61
|
+
if (kept.length > 0) parts.push(kept.map(formatTurn).join('\n'));
|
|
62
|
+
|
|
63
|
+
let digest = parts.join('\n');
|
|
64
|
+
if (digest.length > limit) {
|
|
65
|
+
// Header alone (no turns fit) can still exceed maxChars — clip the
|
|
66
|
+
// header itself rather than exceeding the caller's bound.
|
|
67
|
+
digest = digest.slice(0, limit);
|
|
68
|
+
}
|
|
69
|
+
return digest;
|
|
70
|
+
} catch (e) {
|
|
71
|
+
console.error('[epicContextDigest] buildContextDigest error', cwd, epicId, e);
|
|
72
|
+
return '';
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
module.exports = {
|
|
77
|
+
buildContextDigest,
|
|
78
|
+
};
|