claude-code-session-manager 0.92.0 → 0.93.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/DataModel-DDGx3qrn.js +1 -0
- package/dist/assets/{History-BB6wpf02.js → History-CHBjYZyD.js} +2 -2
- package/dist/assets/{Hooks-zQZH56Sv.js → Hooks-DG4c-Rzz.js} +3 -3
- package/dist/assets/{HostBilko-CW0pHoPS.js → HostBilko-DHXcpV2c.js} +1 -1
- package/dist/assets/{Library-DrGdaz9N.js → Library-CN1h5rAE.js} +1 -1
- package/dist/assets/{MarkdownEditor-H1ytN4Tk.js → MarkdownEditor-DmBxrhdn.js} +1 -1
- package/dist/assets/{McpServers-CYSRCI-j.js → McpServers-Beb_HD6I.js} +2 -2
- package/dist/assets/{Memory-zMNpJyRU.js → Memory-Dk4uDCjg.js} +4 -4
- package/dist/assets/{Permissions-Be6cIkPg.js → Permissions-BovobeZw.js} +3 -3
- package/dist/assets/Plugins-DvOf_B62.js +2 -0
- package/dist/assets/ProvenanceBadge-Dutz_4OY.js +1 -0
- package/dist/assets/{SaveBar-xQZ2EBOn.js → SaveBar-CT5u6ZCJ.js} +1 -1
- package/dist/assets/Scheduler-B7GSnZj5.js +16 -0
- package/dist/assets/{ScopeSwitcher-BAfquN6Y.js → ScopeSwitcher-BUYDpz2v.js} +1 -1
- package/dist/assets/Settings-DIRmr3tL.js +3 -0
- package/dist/assets/{SkillReferenceGraph-BI1YtCbB.js → SkillReferenceGraph-CdGr6d3_.js} +1 -1
- package/dist/assets/Skills-BQOheQv4.js +3 -0
- package/dist/assets/SystemPrompt-5LsimKbX.js +1 -0
- package/dist/assets/TagLibrary-DreLsT3n.js +1 -0
- package/dist/assets/{TiptapBody-BptXAVR9.js → TiptapBody-C9kmlBnI.js} +1 -1
- package/dist/assets/{Toggle-BwiClCSI.js → Toggle-BKLCBu9A.js} +1 -1
- package/dist/assets/{index-D3P_jldk.css → index-CKH5Uxik.css} +1 -1
- package/dist/assets/index-Db_SK9rW.js +3079 -0
- package/dist/assets/settingsSchema-B5jBhSUP.js +1 -0
- package/dist/index.html +2 -2
- package/package.json +1 -1
- package/src/main/__tests__/agentEffortResolve.test.cjs +117 -0
- package/src/main/__tests__/agentLibrary.test.cjs +21 -0
- package/src/main/__tests__/agentModelResolve.test.cjs +16 -0
- package/src/main/__tests__/agentOverlayWrite.test.cjs +95 -0
- package/src/main/__tests__/chatRunner-session-flag-retry.test.cjs +44 -0
- package/src/main/__tests__/personaMerge.test.cjs +169 -0
- package/src/main/__tests__/prdCreatePlanId.test.cjs +132 -0
- package/src/main/__tests__/promptSessionsCreateEpicHandler.test.cjs +13 -0
- package/src/main/__tests__/pty-epic-worktree-spawn-cwd.test.cjs +75 -10
- package/src/main/__tests__/scheduler-prd-persona-spawn.test.cjs +47 -2
- package/src/main/agentLibrary.cjs +86 -40
- package/src/main/build-info.json +4 -4
- package/src/main/chatRunner.cjs +5 -0
- package/src/main/index.cjs +10 -0
- package/src/main/ipcSchemas.cjs +12 -0
- package/src/main/lib/__tests__/effectiveModelInfo.test.cjs +10 -5
- package/src/main/lib/__tests__/modelCatalog.test.cjs +202 -0
- package/src/main/lib/agentEffortResolve.cjs +74 -0
- package/src/main/lib/agentModelResolve.cjs +52 -59
- package/src/main/lib/agentPersonaSchema.cjs +3 -0
- package/src/main/lib/effectiveModelInfo.cjs +38 -20
- package/src/main/lib/epicMint.cjs +5 -2
- package/src/main/lib/epicWorktreeMerge.cjs +7 -6
- package/src/main/lib/modelCatalog.cjs +243 -0
- package/src/main/lib/personaMerge.cjs +166 -0
- package/src/main/lib/prdCreate.cjs +23 -5
- package/src/main/lib/prdDisposition.cjs +46 -0
- package/src/main/lib/prdFrontmatter.cjs +6 -2
- package/src/main/lib/promptSessionSchema.cjs +3 -0
- package/src/main/lib/promptSessionsCreateEpic.cjs +5 -3
- package/src/main/pty.cjs +16 -2
- package/src/main/scheduler/prdParser.cjs +2 -0
- package/src/main/scheduler.cjs +21 -5
- package/src/main/templates/PRD_AUTHORING.md +4 -0
- package/src/preload/api.d.ts +53 -2
- package/src/preload/index.cjs +4 -0
- package/dist/assets/AgentLibrary-Bu-1eT_n.js +0 -3
- package/dist/assets/DataModel-Bk9rkJ8x.js +0 -1
- package/dist/assets/ListDetail-D4bxU_-2.js +0 -1
- package/dist/assets/Panel-RMz3Acpu.js +0 -1
- package/dist/assets/Plugins-B8sTWf2K.js +0 -2
- package/dist/assets/ProvenanceBadge-CO-DZN38.js +0 -1
- package/dist/assets/Scheduler-CF3zsJBw.js +0 -16
- package/dist/assets/Settings-uPjmqutA.js +0 -3
- package/dist/assets/Skills-BEy9FDYd.js +0 -3
- package/dist/assets/SystemPrompt-BYkkw-LL.js +0 -1
- package/dist/assets/TagLibrary-Kg3yOC5b.js +0 -1
- package/dist/assets/index-a2MVk8qh.js +0 -3074
- package/dist/assets/settingsSchema-DgxlStF5.js +0 -3
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* prdCreatePlanId.test.cjs — createPrd() stamps a durable `planId` (wave identity):
|
|
3
|
+
* first-ever PRD mints, append inherits the terminal's, new-head mints, explicit
|
|
4
|
+
* multi-dep inherits the lowest-numbered dependency's (diverged ids logged once).
|
|
5
|
+
*
|
|
6
|
+
* Run: timeout 120 npx vitest run src/main/__tests__/prdCreatePlanId.test.cjs
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
'use strict';
|
|
10
|
+
|
|
11
|
+
import { test, expect, beforeEach, afterEach, vi } from 'vitest';
|
|
12
|
+
const fsp = require('node:fs/promises');
|
|
13
|
+
const os = require('node:os');
|
|
14
|
+
const path = require('node:path');
|
|
15
|
+
const config = require('../config.cjs');
|
|
16
|
+
const prdParser = require('../scheduler/prdParser.cjs');
|
|
17
|
+
const { parsePrdFile } = require('../lib/prdFrontmatter.cjs');
|
|
18
|
+
const { createPrd } = require('../lib/prdCreate.cjs');
|
|
19
|
+
const { resolveInheritedPlanId, isValidPlanId } = require('../lib/prdDisposition.cjs');
|
|
20
|
+
|
|
21
|
+
async function mkTmpPrdsDir() {
|
|
22
|
+
const root = await fsp.mkdtemp(path.join(os.tmpdir(), 'sm-planid-create-prd-'));
|
|
23
|
+
config.addAllowedRoot(root);
|
|
24
|
+
const prdsDir = path.join(root, '.claude', 'prds');
|
|
25
|
+
await fsp.mkdir(prdsDir, { recursive: true });
|
|
26
|
+
return prdsDir;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function makeFakeRemote(prdsDir, statusBySlug = {}) {
|
|
30
|
+
return {
|
|
31
|
+
async allocateParallelGroup() { return prdParser.allocateParallelGroup(prdsDir); },
|
|
32
|
+
async readPrd(slug) {
|
|
33
|
+
try { return { ok: true, text: await fsp.readFile(path.join(prdsDir, `${slug}.md`), 'utf8') }; }
|
|
34
|
+
catch (e) { return { ok: false, error: e?.message }; }
|
|
35
|
+
},
|
|
36
|
+
async writePrd(slug, body) {
|
|
37
|
+
await config.writeTextAtomic(path.join(prdsDir, `${slug}.md`), body, { writer: 'scheduler' });
|
|
38
|
+
return { ok: true };
|
|
39
|
+
},
|
|
40
|
+
async listPrds() {
|
|
41
|
+
const prds = [];
|
|
42
|
+
for (const f of await fsp.readdir(prdsDir)) {
|
|
43
|
+
if (!f.endsWith('.md')) continue;
|
|
44
|
+
const slug = f.slice(0, -3);
|
|
45
|
+
const { frontmatter } = parsePrdFile(await fsp.readFile(path.join(prdsDir, f), 'utf8'));
|
|
46
|
+
prds.push({
|
|
47
|
+
slug,
|
|
48
|
+
sourcePromptId: frontmatter.sourcePromptId ?? null,
|
|
49
|
+
dependsOn: frontmatter.dependsOn ?? null,
|
|
50
|
+
planId: frontmatter.planId ?? null,
|
|
51
|
+
status: statusBySlug[slug] ?? null,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
return { prds, total: prds.length, limit: prds.length, offset: 0, hasMore: false };
|
|
55
|
+
},
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function seedPrd(prdsDir, slug, { planId, dependsOn } = {}) {
|
|
60
|
+
const lines = ['---', 'title: seed', 'sourcePromptId: epic-1'];
|
|
61
|
+
if (dependsOn) lines.push(`dependsOn: [${dependsOn.join(', ')}]`);
|
|
62
|
+
if (planId) lines.push(`planId: ${planId}`);
|
|
63
|
+
lines.push('---', '# Goal', '', 'seed', '');
|
|
64
|
+
await fsp.writeFile(path.join(prdsDir, `${slug}.md`), lines.join('\n'));
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const body = (o = {}) => ({
|
|
68
|
+
title: 'Next wave', cwd: os.homedir(), estimateMinutes: 10, goal: 'g',
|
|
69
|
+
acceptanceCriteria: ['a'], implementationNotes: 'n', ...o,
|
|
70
|
+
});
|
|
71
|
+
const planIdOf = async (dir, result) =>
|
|
72
|
+
parsePrdFile(await fsp.readFile(path.join(dir, result.filename), 'utf8')).frontmatter.planId;
|
|
73
|
+
|
|
74
|
+
const originalJobSlug = process.env.SM_SCHEDULER_JOB_SLUG;
|
|
75
|
+
beforeEach(() => { delete process.env.SM_SCHEDULER_JOB_SLUG; });
|
|
76
|
+
afterEach(() => {
|
|
77
|
+
vi.restoreAllMocks();
|
|
78
|
+
if (originalJobSlug === undefined) delete process.env.SM_SCHEDULER_JOB_SLUG;
|
|
79
|
+
else process.env.SM_SCHEDULER_JOB_SLUG = originalJobSlug;
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
test('first-ever PRD in an Epic mints a valid planId', async () => {
|
|
83
|
+
const dir = await mkTmpPrdsDir();
|
|
84
|
+
const r = await createPrd(body({ sourcePromptId: 'epic-1' }), makeFakeRemote(dir));
|
|
85
|
+
expect(r.ok).toBe(true);
|
|
86
|
+
expect(isValidPlanId(await planIdOf(dir, r))).toBe(true);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
test('a caller-supplied planId is ignored (API-owned)', async () => {
|
|
90
|
+
const dir = await mkTmpPrdsDir();
|
|
91
|
+
const r = await createPrd(body({ planId: 'evil-id' }), makeFakeRemote(dir));
|
|
92
|
+
expect(await planIdOf(dir, r)).not.toBe('evil-id');
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
test('append inherits the chain terminal\'s planId', async () => {
|
|
96
|
+
const dir = await mkTmpPrdsDir();
|
|
97
|
+
await seedPrd(dir, '1-tail', { planId: 'pl-abc' });
|
|
98
|
+
const r = await createPrd(body({ sourcePromptId: 'epic-1', disposition: 'append' }), makeFakeRemote(dir, { '1-tail': 'pending' }));
|
|
99
|
+
expect(r.ok).toBe(true);
|
|
100
|
+
expect(await planIdOf(dir, r)).toBe('pl-abc');
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
test('new-head mints a fresh planId, distinct from the existing plan', async () => {
|
|
104
|
+
const dir = await mkTmpPrdsDir();
|
|
105
|
+
await seedPrd(dir, '1-tail', { planId: 'pl-abc' });
|
|
106
|
+
const r = await createPrd(body({ sourcePromptId: 'epic-1', disposition: 'new-head' }), makeFakeRemote(dir, { '1-tail': 'pending' }));
|
|
107
|
+
const id = await planIdOf(dir, r);
|
|
108
|
+
expect(isValidPlanId(id)).toBe(true);
|
|
109
|
+
expect(id).not.toBe('pl-abc');
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
test('explicit multi-dep across two plans inherits the lowest-numbered dependency\'s and warns once', async () => {
|
|
113
|
+
const dir = await mkTmpPrdsDir();
|
|
114
|
+
await seedPrd(dir, '9-late', { planId: 'pl-late' });
|
|
115
|
+
await seedPrd(dir, '2-early', { planId: 'pl-early' });
|
|
116
|
+
const warn = vi.spyOn(console, 'warn').mockImplementation(() => {});
|
|
117
|
+
const r = await createPrd(body({ dependsOn: ['9-late', '2-early'] }), makeFakeRemote(dir));
|
|
118
|
+
expect(r.ok).toBe(true);
|
|
119
|
+
expect(await planIdOf(dir, r)).toBe('pl-early');
|
|
120
|
+
expect(warn.mock.calls.filter((c) => String(c[0]).includes('spans plans')).length).toBe(1);
|
|
121
|
+
});
|
|
122
|
+
|
|
123
|
+
test('deps that predate the stamp fall back to a freshly minted planId', async () => {
|
|
124
|
+
const dir = await mkTmpPrdsDir();
|
|
125
|
+
await seedPrd(dir, '1-legacy');
|
|
126
|
+
const r = await createPrd(body({ dependsOn: ['1-legacy'] }), makeFakeRemote(dir));
|
|
127
|
+
expect(isValidPlanId(await planIdOf(dir, r))).toBe(true);
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
test('resolveInheritedPlanId ignores garbage planIds', () => {
|
|
131
|
+
expect(resolveInheritedPlanId(['1-a'], [{ slug: '1-a', planId: 'bad id,[x]' }]).planId).toBeNull();
|
|
132
|
+
});
|
|
@@ -144,3 +144,16 @@ test('a cwd outside every allowed root is rejected before anything is minted', a
|
|
|
144
144
|
const index = readActiveIndex(unregistered);
|
|
145
145
|
expect(Object.keys(index.sessions)).toHaveLength(0);
|
|
146
146
|
});
|
|
147
|
+
|
|
148
|
+
test('model/effort overrides persist on the minted Epic; without them the record gains no such keys', async () => {
|
|
149
|
+
const cwd = await mkCwd();
|
|
150
|
+
const payload = schemas.promptSessionsCreateEpic.parse({ cwd, goalText: 'Override run', agentType: 'architect', model: 'claude-opus-5', effort: 'max' });
|
|
151
|
+
const withOverride = await createEpicViaIpc(cwd, payload);
|
|
152
|
+
expect(withOverride.session).toMatchObject({ model: 'claude-opus-5', effort: 'max' });
|
|
153
|
+
expect(readActiveIndex(cwd).sessions[withOverride.epicId]).toEqual(withOverride.session);
|
|
154
|
+
expect(PromptSessionSchema.safeParse(withOverride.session).success).toBe(true);
|
|
155
|
+
|
|
156
|
+
const plain = await createEpicViaIpc(cwd, schemas.promptSessionsCreateEpic.parse({ cwd, goalText: 'Plain run', agentType: 'architect' }));
|
|
157
|
+
expect('model' in plain.session).toBe(false);
|
|
158
|
+
expect('effort' in plain.session).toBe(false);
|
|
159
|
+
});
|
|
@@ -175,22 +175,87 @@ test('spawn() opens a merged-worktree Epic in the project cwd (dead worktree dir
|
|
|
175
175
|
manager.kill(sessionId);
|
|
176
176
|
});
|
|
177
177
|
|
|
178
|
-
|
|
179
|
-
const mainCwd = fs.mkdtempSync(path.join(tmpHome, 'sm-pty-closed-'));
|
|
180
|
-
const sessionId = 'epic-session-closed';
|
|
181
|
-
writeActiveIndexWithWorktree(mainCwd, { epicId: 'epic-c', claudeSessionId: sessionId, worktreeDir: mainCwd });
|
|
178
|
+
function setEpic(mainCwd, epicId, patch) {
|
|
182
179
|
const idxPath = path.join(mainCwd, 'session-manager-operations', 'prompt-sessions', 'active-index.json');
|
|
183
180
|
const idx = JSON.parse(fs.readFileSync(idxPath, 'utf8'));
|
|
184
|
-
idx.sessions[
|
|
181
|
+
const rec = idx.sessions[epicId];
|
|
182
|
+
if (patch.status) rec.status = patch.status;
|
|
183
|
+
if (patch.worktreeStatus) rec.worktree.status = patch.worktreeStatus;
|
|
185
184
|
fs.writeFileSync(idxPath, JSON.stringify(idx));
|
|
185
|
+
}
|
|
186
186
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
187
|
+
function captureSpawn() {
|
|
188
|
+
const out = { opts: null, count: 0 };
|
|
189
|
+
nodePty.spawn = (shell, args, opts) => {
|
|
190
|
+
out.opts = opts;
|
|
191
|
+
out.count += 1;
|
|
190
192
|
return fakeProc();
|
|
191
193
|
};
|
|
194
|
+
return out;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function fakeWindow(sent) {
|
|
198
|
+
return {
|
|
199
|
+
isDestroyed: () => false,
|
|
200
|
+
webContents: { isDestroyed: () => false, isCrashed: () => false, send: (channel, payload) => sent.push({ channel, payload }) },
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
test('spawn() opens a completed Epic (merged worktree) as a shell in the project cwd with an informational line', () => {
|
|
205
|
+
const mainCwd = fs.mkdtempSync(path.join(tmpHome, 'sm-pty-closed-'));
|
|
206
|
+
const sessionId = 'epic-session-closed';
|
|
207
|
+
const deadDir = path.join(os.tmpdir(), 'sm-pty-closed-dead-worktree');
|
|
208
|
+
writeActiveIndexWithWorktree(mainCwd, { epicId: 'epic-c', claudeSessionId: sessionId, worktreeDir: deadDir });
|
|
209
|
+
setEpic(mainCwd, 'epic-c', { status: 'completed', worktreeStatus: 'merged' });
|
|
210
|
+
|
|
211
|
+
const cap = captureSpawn();
|
|
212
|
+
const sent = [];
|
|
213
|
+
manager.window = fakeWindow(sent);
|
|
214
|
+
try {
|
|
215
|
+
const result = manager.spawn({ tabId: sessionId, cwd: mainCwd, cols: 80, rows: 24 });
|
|
216
|
+
expect(cap.count).toBe(1);
|
|
217
|
+
expect(cap.opts.cwd).toBe(mainCwd);
|
|
218
|
+
expect(result.pid).toBe(4242);
|
|
219
|
+
expect(result.error).toBeUndefined();
|
|
220
|
+
const info = sent.find((m) => m.channel === `pty:data:${sessionId}`);
|
|
221
|
+
expect(info.payload).toMatch(/Epic is completed/);
|
|
222
|
+
expect(info.payload).toContain(mainCwd);
|
|
223
|
+
} finally {
|
|
224
|
+
manager.window = null;
|
|
225
|
+
manager.kill(sessionId);
|
|
226
|
+
}
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
test('spawn() opens a completed Epic whose worktree dir still exists in that worktree dir', () => {
|
|
230
|
+
const mainCwd = fs.mkdtempSync(path.join(tmpHome, 'sm-pty-closed-live-'));
|
|
231
|
+
const worktreeCwd = fs.mkdtempSync(path.join(os.tmpdir(), 'sm-pty-closed-live-wt-'));
|
|
232
|
+
const sessionId = 'epic-session-closed-live';
|
|
233
|
+
writeActiveIndexWithWorktree(mainCwd, { epicId: 'epic-cl', claudeSessionId: sessionId, worktreeDir: worktreeCwd });
|
|
234
|
+
setEpic(mainCwd, 'epic-cl', { status: 'completed' });
|
|
235
|
+
|
|
236
|
+
const cap = captureSpawn();
|
|
237
|
+
manager.spawn({ tabId: sessionId, cwd: mainCwd, cols: 80, rows: 24 });
|
|
238
|
+
expect(cap.count).toBe(1);
|
|
239
|
+
expect(cap.opts.cwd).toBe(worktreeCwd);
|
|
240
|
+
manager.kill(sessionId);
|
|
241
|
+
fs.rmSync(worktreeCwd, { recursive: true, force: true });
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
test('spawn() still refuses session_unreachable without any node-pty spawn', () => {
|
|
245
|
+
const mainCwd = fs.mkdtempSync(path.join(tmpHome, 'sm-pty-unreachable-'));
|
|
246
|
+
const sessionId = 'epic-session-unreachable';
|
|
247
|
+
const deadDir = path.join(os.tmpdir(), 'sm-pty-unreachable-dead-worktree');
|
|
248
|
+
writeActiveIndexWithWorktree(mainCwd, { epicId: 'epic-u', claudeSessionId: sessionId, worktreeDir: deadDir });
|
|
249
|
+
setEpic(mainCwd, 'epic-u', { worktreeStatus: 'merged' });
|
|
250
|
+
// Transcript exists only under the dead worktree's encoded project dir.
|
|
251
|
+
const enc = deadDir.replace(/[^a-zA-Z0-9]/g, '-');
|
|
252
|
+
const tdir = path.join(tmpHome, '.claude', 'projects', enc);
|
|
253
|
+
fs.mkdirSync(tdir, { recursive: true });
|
|
254
|
+
fs.writeFileSync(path.join(tdir, `${sessionId}.jsonl`), '{"type":"user"}\n');
|
|
255
|
+
|
|
256
|
+
const cap = captureSpawn();
|
|
192
257
|
const result = manager.spawn({ tabId: sessionId, cwd: mainCwd, cols: 80, rows: 24 });
|
|
193
|
-
expect(
|
|
258
|
+
expect(cap.count).toBe(0);
|
|
194
259
|
expect(result.pid).toBeNull();
|
|
195
|
-
expect(result.error).toMatch(/
|
|
260
|
+
expect(result.error).toMatch(/cannot be resumed/);
|
|
196
261
|
});
|
|
@@ -82,8 +82,10 @@ function writePersona(dir, name, frontmatterLines, bodyText) {
|
|
|
82
82
|
fs.writeFileSync(path.join(dir, `${name}.md`), lines.join('\n'));
|
|
83
83
|
}
|
|
84
84
|
|
|
85
|
-
async function runJobWithAgentType({ agentType }) {
|
|
86
|
-
|
|
85
|
+
async function runJobWithAgentType({ agentType, overlay }) {
|
|
86
|
+
// Under tmpHome so config.validatePath's home-dir boundary admits the project overlay.
|
|
87
|
+
const mainCwd = fs.mkdtempSync(path.join(tmpHome, 'sm-prd-persona-main-'));
|
|
88
|
+
if (overlay) writePersona(path.join(mainCwd, '.claude', 'agents'), overlay.name, overlay.frontmatter, overlay.body || '');
|
|
87
89
|
fs.mkdirSync(path.join(tmpHome, '.claude'), { recursive: true });
|
|
88
90
|
const runDir = fs.mkdtempSync(path.join(tmpHome, '.claude', 'sm-prd-persona-run-'));
|
|
89
91
|
const slug = `1115-test-${process.pid}-${Math.floor(Math.random() * 1e6)}`;
|
|
@@ -131,3 +133,46 @@ test('executeJob: a job with no agentType at all runs unaffected (no persona, so
|
|
|
131
133
|
const modelIdx = argv.indexOf('--model');
|
|
132
134
|
expect(argv[modelIdx + 1]).toBe('sonnet');
|
|
133
135
|
});
|
|
136
|
+
|
|
137
|
+
// ---------- persona effort → --effort ----------
|
|
138
|
+
|
|
139
|
+
test('buildClaudeSpawnArgs: effort appends --effort <level>; null/inherit/absent add no token', () => {
|
|
140
|
+
const base = { prompt: 'x', model: 'sonnet', sessionId: 'sid', resume: false };
|
|
141
|
+
const args = buildClaudeSpawnArgs({ ...base, effort: 'high' });
|
|
142
|
+
expect(args[args.indexOf('--effort') + 1]).toBe('high');
|
|
143
|
+
expect(args.indexOf('--model')).toBeGreaterThanOrEqual(0);
|
|
144
|
+
for (const effort of [null, 'inherit', undefined]) {
|
|
145
|
+
expect(buildClaudeSpawnArgs({ ...base, effort })).not.toContain('--effort');
|
|
146
|
+
}
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
test('executeJob: persona effort reaches the child as --effort; inherit yields no --effort token', async () => {
|
|
150
|
+
const globalAgentsDir = path.join(tmpHome, '.claude', 'agents');
|
|
151
|
+
writePersona(globalAgentsDir, 'effort-high', ['model: opus', 'effort: high'], 'body');
|
|
152
|
+
writePersona(globalAgentsDir, 'effort-inherit', ['model: opus', 'effort: inherit'], 'body');
|
|
153
|
+
|
|
154
|
+
const high = await runJobWithAgentType({ agentType: 'effort-high' });
|
|
155
|
+
expect(high.argv[high.argv.indexOf('--effort') + 1]).toBe('high');
|
|
156
|
+
expect(high.argv[high.argv.indexOf('--model') + 1]).toBe('opus');
|
|
157
|
+
|
|
158
|
+
const inherit = await runJobWithAgentType({ agentType: 'effort-inherit' });
|
|
159
|
+
expect(inherit.argv).not.toContain('--effort');
|
|
160
|
+
expect(inherit.argv).not.toContain('inherit');
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// ---------- project overlay MERGE: frontmatter-only overlay patches the runtime fields ----------
|
|
164
|
+
|
|
165
|
+
test('executeJob: a frontmatter-only project overlay changes --model/--effort while the global persona body still reaches --append-system-prompt', async () => {
|
|
166
|
+
const globalAgentsDir = path.join(tmpHome, '.claude', 'agents');
|
|
167
|
+
writePersona(globalAgentsDir, 'dev-lead', ['model: sonnet'], 'You are dev-lead. Global body.');
|
|
168
|
+
|
|
169
|
+
const { result, argv } = await runJobWithAgentType({
|
|
170
|
+
agentType: 'dev-lead',
|
|
171
|
+
overlay: { name: 'dev-lead', frontmatter: ['model: claude-opus-4-6', 'effort: high'], body: '' },
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
expect(result.exitCode).toBe(0);
|
|
175
|
+
expect(argv[argv.indexOf('--model') + 1]).toBe('claude-opus-4-6');
|
|
176
|
+
expect(argv[argv.indexOf('--effort') + 1]).toBe('high');
|
|
177
|
+
expect(argv[argv.indexOf('--append-system-prompt') + 1]).toBe('You are dev-lead. Global body.');
|
|
178
|
+
});
|
|
@@ -22,7 +22,7 @@ const fsSync = require('node:fs');
|
|
|
22
22
|
const path = require('node:path');
|
|
23
23
|
const os = require('node:os');
|
|
24
24
|
const { splitFrontmatter } = require('./lib/prdFrontmatter.cjs');
|
|
25
|
-
const {
|
|
25
|
+
const { resolveMergedPersona, PERSONA_NAME_RE } = require('./lib/personaMerge.cjs');
|
|
26
26
|
const configMgr = require('./config.cjs');
|
|
27
27
|
const sessionsStore = require('./sessionsStore.cjs');
|
|
28
28
|
|
|
@@ -32,9 +32,6 @@ function parseTools(raw) {
|
|
|
32
32
|
return raw.split(',').map((t) => t.trim()).filter(Boolean);
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
/** Filename-safe persona name: lowercase, hyphenated, matches the `.md` files on disk. */
|
|
36
|
-
const PERSONA_NAME_RE = /^[a-z][a-z0-9-]*$/;
|
|
37
|
-
|
|
38
35
|
/** Sentinel `projects:` entry meaning "this agent's Action button shows in every project". */
|
|
39
36
|
const ALL_PROJECTS = '*';
|
|
40
37
|
|
|
@@ -70,11 +67,12 @@ function decodeLine(s) {
|
|
|
70
67
|
* overrides the button's caption (defaults to the persona name). Claude Code
|
|
71
68
|
* ignores all three; only this app reads them.
|
|
72
69
|
*/
|
|
73
|
-
function serializePersona({ name, description, tools, model, color, tags, projects, action, actionLabel, title, body }) {
|
|
70
|
+
function serializePersona({ name, description, tools, model, effort, color, tags, projects, action, actionLabel, title, body }) {
|
|
74
71
|
const lines = ['---', `name: ${name}`];
|
|
75
72
|
if (description) lines.push(`description: ${description}`);
|
|
76
73
|
if (tools && tools.length) lines.push(`tools: ${tools.join(', ')}`);
|
|
77
74
|
if (model && model !== 'inherit') lines.push(`model: ${model}`);
|
|
75
|
+
if (effort && effort !== 'inherit') lines.push(`effort: ${effort}`);
|
|
78
76
|
if (color) lines.push(`color: ${color}`);
|
|
79
77
|
if (tags && tags.length) lines.push(`tags: ${tags.join(', ')}`);
|
|
80
78
|
if (projects && projects.length) lines.push(`projects: ${projects.join(', ')}`);
|
|
@@ -85,10 +83,51 @@ function serializePersona({ name, description, tools, model, color, tags, projec
|
|
|
85
83
|
return lines.join('\n') + (body || '').trim() + '\n';
|
|
86
84
|
}
|
|
87
85
|
|
|
86
|
+
/**
|
|
87
|
+
* Project OVERRIDE write: a frontmatter-only overlay at
|
|
88
|
+
* `<cwd>/.claude/agents/<name>.md` carrying ONLY the overridden `model` /
|
|
89
|
+
* `effort` keys and no body (lib/personaMerge.cjs inherits everything else
|
|
90
|
+
* from the global persona). Keys already in an existing frontmatter-only
|
|
91
|
+
* overlay are preserved; an existing overlay that has a BODY is refused so a
|
|
92
|
+
* full-body overlay is never silently converted. Both keys unset removes the
|
|
93
|
+
* file (same as `removeOverride`) rather than leaving an empty overlay.
|
|
94
|
+
*/
|
|
95
|
+
async function saveOverlay({ name, projectName, model, effort, loadSessions, validatePath, writeTextAtomic }) {
|
|
96
|
+
const projects = await openProjects({ loadSessions });
|
|
97
|
+
const project = projects.find((p) => p.name === projectName);
|
|
98
|
+
if (!project) throw new Error(`project not open: ${projectName}`);
|
|
99
|
+
const target = validatePath(path.join(project.cwd, '.claude', 'agents', `${name}.md`));
|
|
100
|
+
let fm = {};
|
|
101
|
+
try {
|
|
102
|
+
const existing = splitFrontmatter(await fsp.readFile(target, 'utf8'));
|
|
103
|
+
if (existing.body.trim() !== '') {
|
|
104
|
+
throw new Error(`${name} has a full-body override in ${projectName} — edit ${target} directly; it is not converted to a frontmatter-only override`);
|
|
105
|
+
}
|
|
106
|
+
fm = existing.fm;
|
|
107
|
+
} catch (e) {
|
|
108
|
+
if (e.code !== 'ENOENT') throw e;
|
|
109
|
+
}
|
|
110
|
+
const next = { ...fm };
|
|
111
|
+
for (const [k, v] of [['model', model], ['effort', effort]]) {
|
|
112
|
+
if (v && v !== 'inherit') next[k] = v;
|
|
113
|
+
else delete next[k];
|
|
114
|
+
}
|
|
115
|
+
if (Object.keys(next).length === 0) {
|
|
116
|
+
await removeOverride({ name, projectName, loadSessions, validatePath });
|
|
117
|
+
return { ok: true, path: target, removed: true };
|
|
118
|
+
}
|
|
119
|
+
const lines = ['---', ...Object.entries(next).map(([k, v]) => `${k}: ${v}`), '---', ''];
|
|
120
|
+
await writeTextAtomic(target, lines.join('\n'));
|
|
121
|
+
return { ok: true, path: target };
|
|
122
|
+
}
|
|
123
|
+
|
|
88
124
|
/**
|
|
89
125
|
* Writes a global persona `.md` file — `~/.claude/agents/<name>.md`. Pass
|
|
90
126
|
* `originalName` when renaming an existing persona so the old file is
|
|
91
|
-
* removed once the new one is written (no-op if names match).
|
|
127
|
+
* removed once the new one is written (no-op if names match). Pass
|
|
128
|
+
* `projectName` (an open project) to instead write that project's
|
|
129
|
+
* frontmatter-only OVERRIDE of the persona — see saveOverlay; only `model` and
|
|
130
|
+
* `effort` are read in that mode.
|
|
92
131
|
*/
|
|
93
132
|
async function savePersona({
|
|
94
133
|
name,
|
|
@@ -96,6 +135,7 @@ async function savePersona({
|
|
|
96
135
|
description,
|
|
97
136
|
tools,
|
|
98
137
|
model,
|
|
138
|
+
effort,
|
|
99
139
|
color,
|
|
100
140
|
tags,
|
|
101
141
|
projects,
|
|
@@ -103,15 +143,20 @@ async function savePersona({
|
|
|
103
143
|
actionLabel,
|
|
104
144
|
title,
|
|
105
145
|
body,
|
|
146
|
+
projectName,
|
|
106
147
|
globalDir = path.join(os.homedir(), '.claude', 'agents'),
|
|
148
|
+
loadSessions = sessionsStore.load,
|
|
107
149
|
validatePath = configMgr.validatePath,
|
|
108
150
|
writeTextAtomic = configMgr.writeTextAtomic,
|
|
109
151
|
} = {}) {
|
|
110
152
|
if (!PERSONA_NAME_RE.test(name || '')) {
|
|
111
153
|
throw new Error('agent name must be lowercase, hyphenated (e.g. "my-agent")');
|
|
112
154
|
}
|
|
155
|
+
if (projectName) {
|
|
156
|
+
return saveOverlay({ name, projectName, model, effort, loadSessions, validatePath, writeTextAtomic });
|
|
157
|
+
}
|
|
113
158
|
const target = validatePath(path.join(globalDir, `${name}.md`));
|
|
114
|
-
const text = serializePersona({ name, description, tools, model, color, tags, projects, action, actionLabel, title, body });
|
|
159
|
+
const text = serializePersona({ name, description, tools, model, effort, color, tags, projects, action, actionLabel, title, body });
|
|
115
160
|
await writeTextAtomic(target, text);
|
|
116
161
|
if (originalName && originalName !== name) {
|
|
117
162
|
const oldReal = validatePath(path.join(globalDir, `${originalName}.md`));
|
|
@@ -220,6 +265,9 @@ async function listPersonas({
|
|
|
220
265
|
const { fm, body } = splitFrontmatter(text);
|
|
221
266
|
|
|
222
267
|
const overridingProjects = [];
|
|
268
|
+
// Per-project merged view: which frontmatter fields (and whether the body)
|
|
269
|
+
// the overlay actually overrides — from the shared merge, not a boolean.
|
|
270
|
+
const overrideDetails = [];
|
|
223
271
|
for (const p of projects) {
|
|
224
272
|
let overlayReal;
|
|
225
273
|
try {
|
|
@@ -227,7 +275,18 @@ async function listPersonas({
|
|
|
227
275
|
} catch {
|
|
228
276
|
continue; // project cwd outside allowed roots — skip rather than throw
|
|
229
277
|
}
|
|
230
|
-
if (fsSync.existsSync(overlayReal))
|
|
278
|
+
if (!fsSync.existsSync(overlayReal)) continue;
|
|
279
|
+
overridingProjects.push(p.name);
|
|
280
|
+
const merged = resolveMergedPersona(p.cwd, path.basename(file, '.md'), { validatePath, globalDir });
|
|
281
|
+
const fields = merged ? Object.keys(merged.provenance).filter((k) => merged.provenance[k] === 'overlay') : [];
|
|
282
|
+
overrideDetails.push({
|
|
283
|
+
project: p.name,
|
|
284
|
+
fields,
|
|
285
|
+
// The project's value for each overridden key (the UI renders global → project).
|
|
286
|
+
values: merged ? Object.fromEntries(fields.map((k) => [k, merged.fm[k]])) : {},
|
|
287
|
+
bodyOverridden: merged ? merged.bodySource === 'overlay' : false,
|
|
288
|
+
issue: merged ? merged.overlayIssue : null,
|
|
289
|
+
});
|
|
231
290
|
}
|
|
232
291
|
|
|
233
292
|
personas.push({
|
|
@@ -235,6 +294,7 @@ async function listPersonas({
|
|
|
235
294
|
description: fm.description || null,
|
|
236
295
|
tools: parseTools(fm.tools),
|
|
237
296
|
model: fm.model || null,
|
|
297
|
+
effort: fm.effort || null,
|
|
238
298
|
color: fm.color || null,
|
|
239
299
|
tags: parseTools(fm.tags),
|
|
240
300
|
projects: parseTools(fm.projects),
|
|
@@ -244,6 +304,7 @@ async function listPersonas({
|
|
|
244
304
|
path: real,
|
|
245
305
|
body: body.trim(),
|
|
246
306
|
overridingProjects,
|
|
307
|
+
overrideDetails,
|
|
247
308
|
});
|
|
248
309
|
}
|
|
249
310
|
|
|
@@ -251,44 +312,29 @@ async function listPersonas({
|
|
|
251
312
|
}
|
|
252
313
|
|
|
253
314
|
/**
|
|
254
|
-
* Reads a persona
|
|
255
|
-
*
|
|
256
|
-
*
|
|
257
|
-
*
|
|
258
|
-
* `
|
|
259
|
-
*
|
|
260
|
-
*
|
|
261
|
-
*
|
|
315
|
+
* Reads a persona for `epicIntake.ts`'s `agentBody` input and the PRD spawn
|
|
316
|
+
* path, through the shared overlay-over-global MERGE (lib/personaMerge.cjs —
|
|
317
|
+
* see its header for the rules). Returns `{ path, text, fm, body, fromOverlay,
|
|
318
|
+
* bodySource, provenance, overlayIssue }` where `fm`/`body` are the MERGED
|
|
319
|
+
* view, `provenance[key]` says which frontmatter keys came from the overlay,
|
|
320
|
+
* and `text` is the body-source file's raw text (byte-identical to the
|
|
321
|
+
* pre-merge result for a full-body overlay) or a re-serialized merge for a
|
|
322
|
+
* frontmatter-only overlay. `listPersonas` deliberately does NOT apply this —
|
|
323
|
+
* it lists the global directory for the Agent Library nav page. Returns `null`
|
|
324
|
+
* when neither location has the file; frontmatter stripping and the 6000-char
|
|
325
|
+
* cap are epicIntake.ts's / agentModelResolve.cjs's job, not this reader's.
|
|
326
|
+
* (Sync fs inside an async signature: the merge module is shared with the
|
|
327
|
+
* synchronous model resolver.)
|
|
262
328
|
*/
|
|
263
329
|
async function getPersonaBody({
|
|
264
330
|
cwd,
|
|
265
331
|
name,
|
|
266
332
|
validatePath = configMgr.validatePath,
|
|
333
|
+
...deps
|
|
267
334
|
} = {}) {
|
|
268
|
-
//
|
|
269
|
-
//
|
|
270
|
-
|
|
271
|
-
// plain path.join and validatePath's allowed-roots check (which only
|
|
272
|
-
// confirms the resolved path stays under the home dir / an opened
|
|
273
|
-
// project), letting any readable .md under those roots be read and
|
|
274
|
-
// forwarded into the Epic's opening prompt.
|
|
275
|
-
if (!PERSONA_NAME_RE.test(name || '')) return null;
|
|
276
|
-
const { projectPath, globalPath } = resolvePersonaPaths(cwd, name);
|
|
277
|
-
for (const candidate of [projectPath, globalPath]) {
|
|
278
|
-
let real;
|
|
279
|
-
try {
|
|
280
|
-
real = validatePath(candidate);
|
|
281
|
-
} catch {
|
|
282
|
-
continue;
|
|
283
|
-
}
|
|
284
|
-
try {
|
|
285
|
-
const text = await fsp.readFile(real, 'utf8');
|
|
286
|
-
return { path: real, text };
|
|
287
|
-
} catch {
|
|
288
|
-
continue;
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
return null;
|
|
335
|
+
// PERSONA_NAME_RE is enforced inside resolveMergedPersona (the traversal
|
|
336
|
+
// guard for "../../other-project/CLAUDE"-style names).
|
|
337
|
+
return resolveMergedPersona(cwd, name, { ...deps, validatePath });
|
|
292
338
|
}
|
|
293
339
|
|
|
294
340
|
module.exports = {
|
package/src/main/build-info.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
|
-
"version": "0.
|
|
3
|
-
"gitSha": "
|
|
4
|
-
"gitShortSha": "
|
|
2
|
+
"version": "0.93.0",
|
|
3
|
+
"gitSha": "78fa526eb94e044b2c72c736681e956bee653451",
|
|
4
|
+
"gitShortSha": "78fa526",
|
|
5
5
|
"gitBranch": "HEAD",
|
|
6
|
-
"builtAt": "2026-09-
|
|
6
|
+
"builtAt": "2026-09-21T03:09:24.532Z",
|
|
7
7
|
"dirty": false
|
|
8
8
|
}
|
package/src/main/chatRunner.cjs
CHANGED
|
@@ -53,6 +53,7 @@ const { classifyPromptTicket } = require('./lib/classifyPromptTicket.cjs');
|
|
|
53
53
|
const sessionSlots = require('./lib/sessionSlots.cjs');
|
|
54
54
|
const opsErrorLog = require('./lib/opsErrorLog.cjs');
|
|
55
55
|
const agentModelResolve = require('./lib/agentModelResolve.cjs');
|
|
56
|
+
const { resolveEpicEffort, effortArgs } = require('./lib/agentEffortResolve.cjs');
|
|
56
57
|
const { planEpicSpawn } = require('./lib/epicSpawnPlan.cjs');
|
|
57
58
|
const logs = require('./logs.cjs');
|
|
58
59
|
|
|
@@ -546,6 +547,9 @@ function executeRun({ tabId, sessionId, prompt, cwd, resume, silent, onSilentRes
|
|
|
546
547
|
// Epic, no agentType, or the persona has no model/'inherit' — --model
|
|
547
548
|
// must never be left unpinned (CLAUDE.md model-pinning rule).
|
|
548
549
|
const model = agentModelResolve.resolveEpicModel({ cwd, claudeSessionId: sessionId });
|
|
550
|
+
// Persona `effort:` (same overlay-aware persona, agentEffortResolve.cjs);
|
|
551
|
+
// effortArgs() yields NO tokens for inherit/absent/dangling.
|
|
552
|
+
const effortFlags = effortArgs(resolveEpicEffort({ cwd, claudeSessionId: sessionId }).effort);
|
|
549
553
|
|
|
550
554
|
// Main is authoritative about --resume vs --session-id AND the spawn cwd, and it decides
|
|
551
555
|
// them TOGETHER (epicSpawnPlan.cjs): a transcript under an encoding the spawn cwd cannot see
|
|
@@ -579,6 +583,7 @@ function executeRun({ tabId, sessionId, prompt, cwd, resume, silent, onSilentRes
|
|
|
579
583
|
const a = [
|
|
580
584
|
'-p', fullPrompt,
|
|
581
585
|
'--model', model,
|
|
586
|
+
...effortFlags,
|
|
582
587
|
'--dangerously-skip-permissions',
|
|
583
588
|
'--output-format', 'stream-json',
|
|
584
589
|
'--verbose',
|
package/src/main/index.cjs
CHANGED
|
@@ -43,7 +43,9 @@ const epicWorktreeMerge = require('./lib/epicWorktreeMerge.cjs');
|
|
|
43
43
|
const epicWorktreeProjectConfig = require('./lib/epicWorktreeProjectConfig.cjs');
|
|
44
44
|
const agentLibrary = require('./agentLibrary.cjs');
|
|
45
45
|
const agentModelResolve = require('./lib/agentModelResolve.cjs');
|
|
46
|
+
const agentEffortResolve = require('./lib/agentEffortResolve.cjs');
|
|
46
47
|
const { resolveEffectiveModelInfo } = require('./lib/effectiveModelInfo.cjs');
|
|
48
|
+
const { resolveModelCatalog } = require('./lib/modelCatalog.cjs');
|
|
47
49
|
const { checkDelegationReadiness, ensureGuardsInstalled, installPrdWriteGuard, installDestructiveGitGuard, installInlineImplementationGuard, installSelfScheduleGuard } = require('./lib/delegationReadiness.cjs');
|
|
48
50
|
const { resolveProjectContext } = require('./lib/projectRootResolve.cjs');
|
|
49
51
|
const upgradeDrain = require('./lib/upgradeDrain.cjs');
|
|
@@ -565,6 +567,10 @@ ipcMain.handle('agents:get-persona-body', validated(schemas.agentsGetPersonaBody
|
|
|
565
567
|
// is merely supposed to. Never throws (resolveEpicModel's own contract).
|
|
566
568
|
ipcMain.handle('agents:resolve-epic-model', validated(schemas.agentsResolveEpicModel, (payload) =>
|
|
567
569
|
agentModelResolve.resolveEpicModel(payload)));
|
|
570
|
+
// Effort twin of the above (agentEffortResolve.cjs): Terminal appends
|
|
571
|
+
// `--effort <level>` only when `effort` is non-null. Never throws.
|
|
572
|
+
ipcMain.handle('agents:resolve-epic-effort', validated(schemas.agentsResolveEpicEffort, (payload) =>
|
|
573
|
+
agentEffortResolve.resolveEpicEffort(payload)));
|
|
568
574
|
|
|
569
575
|
// "What will this Epic actually run as?" (PRD: effective-model-resolver) —
|
|
570
576
|
// persona alias + evidence-based concrete model id + provenance, for a given
|
|
@@ -572,6 +578,10 @@ ipcMain.handle('agents:resolve-epic-model', validated(schemas.agentsResolveEpicM
|
|
|
572
578
|
ipcMain.handle('agents:resolve-model-info', validated(schemas.agentsResolveModelInfo, (payload) =>
|
|
573
579
|
resolveEffectiveModelInfo(payload)));
|
|
574
580
|
|
|
581
|
+
// Live model/effort catalog (modelCatalog.cjs) — never throws; `force` bypasses its cache.
|
|
582
|
+
ipcMain.handle('models:catalog', validated(schemas.modelsCatalog, (payload) =>
|
|
583
|
+
resolveModelCatalog(payload)));
|
|
584
|
+
|
|
575
585
|
// "Can this project actually delegate?" probe (PRD: delegation-readiness).
|
|
576
586
|
// Structured data over the preconditions for scheduler_create_prd being in an
|
|
577
587
|
// agent's tool list at all — surfacing it in the UI is a sibling PRD.
|
package/src/main/ipcSchemas.cjs
CHANGED
|
@@ -198,6 +198,9 @@ const promptSessionsCreateEpic = z.object({
|
|
|
198
198
|
goalText: z.string().min(1).max(20000),
|
|
199
199
|
tag: EpicTagSchema.optional(),
|
|
200
200
|
agentType: z.string().min(1).max(256).optional(),
|
|
201
|
+
// Per-Epic runtime overrides (New Session card) — absent = use the persona's own value.
|
|
202
|
+
model: z.string().min(1).max(256).optional(),
|
|
203
|
+
effort: z.string().min(1).max(64).optional(),
|
|
201
204
|
source: EpicSourceSchema.optional(),
|
|
202
205
|
// The full first-prompt body + its labeled sections (epicIntake.ts's
|
|
203
206
|
// composeEpicIntake) — both optional, since not every mint caller (e.g. a
|
|
@@ -661,6 +664,12 @@ const agentsResolveModelInfo = z.object({
|
|
|
661
664
|
agentType: z.string().min(1).max(200),
|
|
662
665
|
}).strict();
|
|
663
666
|
|
|
667
|
+
// ──────────────────────────────────────────── Live model/effort catalog (modelCatalog.cjs)
|
|
668
|
+
const modelsCatalog = z.object({
|
|
669
|
+
cwd: z.string().optional(),
|
|
670
|
+
force: z.boolean().optional(),
|
|
671
|
+
}).strict();
|
|
672
|
+
|
|
664
673
|
// ──────────────────────────────────────────── Epic launch model (PRD: unify-epic-model-resolution)
|
|
665
674
|
// Terminal's (EpicTerminalPane.tsx) call into agentModelResolve.cjs's
|
|
666
675
|
// resolveEpicModel — the same launch-model authority chatRunner.cjs's Chat
|
|
@@ -669,6 +678,7 @@ const agentsResolveEpicModel = z.object({
|
|
|
669
678
|
cwd: z.string().min(1).max(4096),
|
|
670
679
|
claudeSessionId: z.string().min(1).max(200),
|
|
671
680
|
}).strict();
|
|
681
|
+
const agentsResolveEpicEffort = agentsResolveEpicModel;
|
|
672
682
|
|
|
673
683
|
// ──────────────────────────────────────────── Project Pages (PRD 932)
|
|
674
684
|
// Same validation split as projectBriefCwd above: real path validation is
|
|
@@ -1146,7 +1156,9 @@ module.exports = {
|
|
|
1146
1156
|
agentsSavePersona,
|
|
1147
1157
|
agentsGetPersonaBody,
|
|
1148
1158
|
agentsResolveModelInfo,
|
|
1159
|
+
modelsCatalog,
|
|
1149
1160
|
agentsResolveEpicModel,
|
|
1161
|
+
agentsResolveEpicEffort,
|
|
1150
1162
|
delegationReadinessCwd,
|
|
1151
1163
|
telemetrySetConfig,
|
|
1152
1164
|
},
|