claude-code-session-manager 0.75.3 → 0.77.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/{AgentLibrary-CzQqcObq.js → AgentLibrary-B2ie8bbw.js} +2 -2
- package/dist/assets/{DataModel-Bj_WlLz8.js → DataModel-BIJPYw32.js} +1 -1
- package/dist/assets/{History-DnSi_OHm.js → History-CeY6dk9S.js} +2 -2
- package/dist/assets/{Hooks-0BB0dp3S.js → Hooks-BFH2ocKg.js} +2 -2
- package/dist/assets/{HostBilko-DHpwwsLQ.js → HostBilko-36gj9wLz.js} +1 -1
- package/dist/assets/{Library-CaJVqVvi.js → Library-C-hBct39.js} +1 -1
- package/dist/assets/{ListDetail-C1W2HmC2.js → ListDetail-CNq64VWV.js} +1 -1
- package/dist/assets/{MarkdownEditor-5Ob9FW3z.js → MarkdownEditor-Bh3qt5-1.js} +1 -1
- package/dist/assets/{McpServers-JxCSfm1S.js → McpServers-DpGN0oyz.js} +1 -1
- package/dist/assets/{Memory-BDeqlqwH.js → Memory-D59hUjC4.js} +6 -6
- package/dist/assets/{Panel-Dh9ZHuEj.js → Panel-DCgbaoci.js} +1 -1
- package/dist/assets/{Permissions-DXy-CbEY.js → Permissions-DAmQ0DYV.js} +2 -2
- package/dist/assets/{Plugins-_n1Iuc8T.js → Plugins-Dyfgn6Is.js} +2 -2
- package/dist/assets/{ProvenanceBadge-BP_evfxE.js → ProvenanceBadge-BiYhPO1U.js} +1 -1
- package/dist/assets/SaveBar-RV7B6sOh.js +1 -0
- package/dist/assets/Scheduler-BPaNqx1b.js +14 -0
- package/dist/assets/{ScopeSwitcher-CAWzM6RI.js → ScopeSwitcher-P4mdLGNU.js} +1 -1
- package/dist/assets/{Settings-DRRozLyT.js → Settings-BL4vf5aX.js} +1 -1
- package/dist/assets/{SkillReferenceGraph-DGHDWlz4.js → SkillReferenceGraph-BRBDyi1_.js} +1 -1
- package/dist/assets/{Skills-D8L66eiX.js → Skills-BV08gDUH.js} +2 -2
- package/dist/assets/{SystemPrompt-CYtUsonD.js → SystemPrompt-CLftSsDw.js} +1 -1
- package/dist/assets/TagLibrary-Bp8jGsd5.js +1 -0
- package/dist/assets/{TiptapBody-B2hRgbPE.js → TiptapBody-jCpuB6E5.js} +1 -1
- package/dist/assets/{Toggle-BTwsbxam.js → Toggle-D2paA1xf.js} +1 -1
- package/dist/assets/{index-DijufvkJ.js → index-BDRSqBl3.js} +704 -704
- package/dist/assets/{index-CMLnzdZC.css → index-CYhdtisq.css} +1 -1
- package/dist/assets/{settingsSchema-D6wzxAi6.js → settingsSchema-6IOLjZZN.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +8 -2
- package/plugins/session-manager-dev/skills/develop/standards.md +1 -1
- package/scripts/lib/activeSessions.cjs +116 -6
- package/scripts/project-pages-logic/dist/logic.cjs +4709 -0
- package/scripts/render-project-pages/dist/renderer.cjs +18900 -0
- package/scripts/render-project-pages.cjs +70 -0
- package/scripts/scheduler-mcp-server.cjs +269 -96
- package/scripts/validate-project-pages-summary.cjs +62 -0
- package/src/main/__tests__/agentModelResolve.test.cjs +66 -0
- package/src/main/__tests__/epicStatusMirror.test.cjs +110 -0
- package/src/main/__tests__/health-delegation-chain.test.cjs +106 -0
- package/src/main/__tests__/prdAdminRoutes.test.cjs +295 -0
- package/src/main/__tests__/prdAgentType.test.cjs +103 -0
- package/src/main/__tests__/prdCreate.test.cjs +247 -0
- package/src/main/__tests__/prdFrontmatterAgentType.test.cjs +117 -0
- package/src/main/__tests__/prdFrontmatterQuietMachine.test.cjs +108 -0
- package/src/main/__tests__/projectHomeAdminRoutes.test.cjs +485 -0
- package/src/main/__tests__/projectPages.test.cjs +73 -1
- package/src/main/__tests__/rcaReport.test.cjs +54 -0
- package/src/main/__tests__/runVerify.test.cjs +94 -0
- package/src/main/__tests__/scheduler-autofix-select.test.cjs +58 -3
- package/src/main/__tests__/scheduler-bash-timeout-env.test.cjs +103 -0
- package/src/main/__tests__/scheduler-commit-guard-noop.test.cjs +41 -0
- package/src/main/__tests__/scheduler-effective-concurrency.test.cjs +10 -0
- package/src/main/__tests__/scheduler-foreign-wip-manifest.test.cjs +78 -0
- package/src/main/__tests__/scheduler-inplace-salvage.test.cjs +242 -0
- package/src/main/__tests__/scheduler-investigation-prompt.test.cjs +31 -0
- package/src/main/__tests__/scheduler-launch-failure.test.cjs +201 -0
- package/src/main/__tests__/scheduler-leftover-fields.test.cjs +52 -0
- package/src/main/__tests__/scheduler-looks-done.test.cjs +241 -0
- package/src/main/__tests__/scheduler-prd-persona-spawn.test.cjs +135 -0
- package/src/main/__tests__/scheduler-quiet-machine-lease.test.cjs +222 -0
- package/src/main/__tests__/scheduler-reap-dead-running-jobs.test.cjs +207 -1
- package/src/main/__tests__/scheduler-shared-tree-guard.test.cjs +212 -0
- package/src/main/__tests__/scheduler-stranded-investigation.test.cjs +185 -0
- package/src/main/__tests__/scheduler-worktree-cap-defer.test.cjs +194 -0
- package/src/main/__tests__/seedAgentPersonas.test.cjs +75 -14
- package/src/main/__tests__/seedSchedulerMcp.test.cjs +66 -0
- package/src/main/__tests__/uniquePrdNumbers.test.cjs +14 -5
- package/src/main/bilkoHost.cjs +4 -3
- package/src/main/chatRunner.cjs +6 -1
- package/src/main/config.cjs +25 -33
- package/src/main/health.cjs +153 -2
- package/src/main/index.cjs +64 -5
- package/src/main/ipcSchemas.cjs +69 -1
- package/src/main/lib/__tests__/activeIndexRebuild.test.cjs +179 -0
- package/src/main/lib/__tests__/childWithLog.test.cjs +141 -0
- package/src/main/lib/__tests__/delegationReadiness.test.cjs +391 -42
- package/src/main/lib/__tests__/ephemeralCwd.test.cjs +91 -0
- package/src/main/lib/__tests__/epicWorktreeMint.test.cjs +5 -3
- package/src/main/lib/__tests__/fixChainDepth.test.cjs +40 -0
- package/src/main/lib/__tests__/gitWorktree.test.cjs +290 -5
- package/src/main/lib/__tests__/gitWorktreeSalvage.test.cjs +107 -0
- package/src/main/lib/__tests__/gitWorktreeSalvageDelta.test.cjs +153 -0
- package/src/main/lib/__tests__/jobWorktree.test.cjs +6 -4
- package/src/main/lib/__tests__/landedSinceRun.test.cjs +73 -0
- package/src/main/lib/__tests__/launchFailure.test.cjs +220 -0
- package/src/main/lib/__tests__/loadGate.test.cjs +159 -0
- package/src/main/lib/__tests__/mcpToolCatalog.test.cjs +102 -0
- package/src/main/lib/__tests__/opsOwnership.test.cjs +7 -0
- package/src/main/lib/__tests__/opsRootAbsoluteCwd.test.cjs +151 -0
- package/src/main/lib/__tests__/opsRootResolve.test.cjs +149 -0
- package/src/main/lib/__tests__/prdDeclaredPaths.test.cjs +82 -0
- package/src/main/lib/__tests__/projectRootResolve.test.cjs +148 -0
- package/src/main/lib/__tests__/queueHealth.test.cjs +58 -0
- package/src/main/lib/__tests__/quietMachineLease.test.cjs +39 -0
- package/src/main/lib/__tests__/reaperHelpers.test.cjs +133 -0
- package/src/main/lib/__tests__/schedulerBatchDepends.test.cjs +19 -9
- package/src/main/lib/__tests__/schedulerBatchFairness.test.cjs +213 -0
- package/src/main/lib/__tests__/schedulerBatchLaunchHold.test.cjs +125 -0
- package/src/main/lib/__tests__/schedulerBatchProjectCap.test.cjs +127 -0
- package/src/main/lib/__tests__/schedulerBatchQuietMachine.test.cjs +109 -0
- package/src/main/lib/__tests__/schedulerMcpServerHeadlessRefusal.test.cjs +71 -0
- package/src/main/lib/__tests__/schedulerMcpServerHelp.test.cjs +217 -0
- package/src/main/lib/__tests__/schedulerMcpServerProjectHome.test.cjs +350 -0
- package/src/main/lib/activeIndexMerge.cjs +15 -0
- package/src/main/lib/activeIndexRebuild.cjs +133 -0
- package/src/main/lib/agentModelResolve.cjs +58 -0
- package/src/main/lib/buildTarget.cjs +3 -2
- package/src/main/lib/childWithLog.cjs +69 -2
- package/src/main/lib/claudeBin.cjs +54 -1
- package/src/main/lib/crossProjectFeedback.cjs +8 -1
- package/src/main/lib/definitionOfDone.cjs +3 -2
- package/src/main/lib/delegationReadiness.cjs +514 -26
- package/src/main/lib/ephemeralCwd.cjs +78 -0
- package/src/main/lib/epicDelegationStats.cjs +2 -1
- package/src/main/lib/epicMint.cjs +17 -1
- package/src/main/lib/epicStatusMirror.cjs +95 -0
- package/src/main/lib/epicValidationHook.cjs +2 -1
- package/src/main/lib/epicWorktreeMint.cjs +5 -2
- package/src/main/lib/fixChainDepth.cjs +45 -0
- package/src/main/lib/gitWorktree.cjs +520 -21
- package/src/main/lib/jobWorktree.cjs +2 -0
- package/src/main/lib/landedSinceRun.cjs +55 -0
- package/src/main/lib/launchFailure.cjs +357 -0
- package/src/main/lib/loadGate.cjs +134 -0
- package/src/main/lib/mcpToolCatalog.cjs +370 -0
- package/src/main/lib/opsErrorLog.cjs +12 -1
- package/src/main/lib/opsOwnership.cjs +106 -0
- package/src/main/lib/prdAdminRoutes.cjs +43 -3
- package/src/main/lib/prdAgentType.cjs +84 -0
- package/src/main/lib/prdCreate.cjs +103 -15
- package/src/main/lib/prdDeclaredPaths.cjs +70 -0
- package/src/main/lib/prdFrontmatter.cjs +17 -3
- package/src/main/lib/prdLocations.cjs +13 -6
- package/src/main/lib/projectHomeAdminRoutes.cjs +402 -0
- package/src/main/lib/projectPageSummarySchema.cjs +181 -0
- package/src/main/lib/projectRootResolve.cjs +134 -0
- package/src/main/lib/promptSessionSchema.cjs +7 -0
- package/src/main/lib/queueHealth.cjs +38 -0
- package/src/main/lib/queueStore.cjs +40 -7
- package/src/main/lib/quietMachineLease.cjs +48 -0
- package/src/main/lib/rcaReport.cjs +54 -4
- package/src/main/lib/reaperHelpers.cjs +64 -1
- package/src/main/lib/scheduleJobSchema.cjs +31 -0
- package/src/main/lib/scheduleJobTransitions.cjs +6 -2
- package/src/main/lib/schedulerBatch.cjs +301 -55
- package/src/main/lib/schedulerConfig.cjs +99 -0
- package/src/main/projectBrief.cjs +3 -2
- package/src/main/projectPages.cjs +162 -3
- package/src/main/promptSessionTranscript.cjs +0 -0
- package/src/main/pty.cjs +5 -0
- package/src/main/queueOps.cjs +15 -8
- package/src/main/runVerify.cjs +50 -9
- package/src/main/scheduler/prdParser.cjs +18 -1
- package/src/main/scheduler.cjs +1701 -130
- package/src/main/seedAgentPersonas.cjs +62 -21
- package/src/main/seedSchedulerMcp.cjs +58 -4
- package/src/main/templates/project-pages-catalog.json +741 -0
- package/src/main/templates/project-pages-pipeline.md +417 -0
- package/src/preload/api.d.ts +187 -3
- package/src/preload/index.cjs +9 -0
- package/src/seed/agents/project-home-builder.md +59 -0
- package/dist/assets/SaveBar-D-gCUx4n.js +0 -1
- package/dist/assets/Scheduler-Bpd4OGju.js +0 -14
- package/dist/assets/TagLibrary-E5CLeuVk.js +0 -1
|
@@ -0,0 +1,350 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* schedulerMcpServerProjectHome.test.cjs — the four project_home_* MCP
|
|
3
|
+
* tools (PRD: project-home-mcp-tools) that wrap PRD 1089's admin routes.
|
|
4
|
+
* Covers: presence in TOOLS, catalog-composed descriptions, correct
|
|
5
|
+
* route/payload dispatch via a stubbed admin HTTP server, cwd defaulting to
|
|
6
|
+
* SM_PROJECT_ROOT/process.cwd() when omitted, the app-not-running failure
|
|
7
|
+
* shape, and that project_home_get_contract's composed response is
|
|
8
|
+
* self-sufficient (mentions all 5 lens ids, no repo-relative path).
|
|
9
|
+
*
|
|
10
|
+
* Same HOME-repointing pattern as schedulerMcpServerHelp.test.cjs — see that
|
|
11
|
+
* file's header for why.
|
|
12
|
+
*
|
|
13
|
+
* Run: timeout 120 npx vitest run src/main/lib/__tests__/schedulerMcpServerProjectHome.test.cjs
|
|
14
|
+
*/
|
|
15
|
+
'use strict';
|
|
16
|
+
|
|
17
|
+
import { test, expect, afterEach } from 'vitest';
|
|
18
|
+
const fsp = require('node:fs/promises');
|
|
19
|
+
const http = require('node:http');
|
|
20
|
+
const os = require('node:os');
|
|
21
|
+
const path = require('node:path');
|
|
22
|
+
|
|
23
|
+
const SERVER_PATH = path.join(__dirname, '../../../../scripts/scheduler-mcp-server.cjs');
|
|
24
|
+
const { MCP_TOOL_CATALOG, composeDescription } = require('../mcpToolCatalog.cjs');
|
|
25
|
+
|
|
26
|
+
const PROJECT_HOME_TOOL_NAMES = [
|
|
27
|
+
'project_home_get_contract',
|
|
28
|
+
'project_home_validate_summary',
|
|
29
|
+
'project_home_render',
|
|
30
|
+
'project_home_status',
|
|
31
|
+
];
|
|
32
|
+
|
|
33
|
+
const tmpDirs = [];
|
|
34
|
+
const servers = [];
|
|
35
|
+
let originalHome;
|
|
36
|
+
let originalProjectRoot;
|
|
37
|
+
|
|
38
|
+
afterEach(async () => {
|
|
39
|
+
if (originalHome !== undefined) {
|
|
40
|
+
process.env.HOME = originalHome;
|
|
41
|
+
originalHome = undefined;
|
|
42
|
+
}
|
|
43
|
+
if (originalProjectRoot !== undefined) {
|
|
44
|
+
if (originalProjectRoot === null) delete process.env.SM_PROJECT_ROOT;
|
|
45
|
+
else process.env.SM_PROJECT_ROOT = originalProjectRoot;
|
|
46
|
+
originalProjectRoot = undefined;
|
|
47
|
+
}
|
|
48
|
+
delete require.cache[require.resolve(SERVER_PATH)];
|
|
49
|
+
while (servers.length) {
|
|
50
|
+
const s = servers.pop();
|
|
51
|
+
await new Promise((resolve) => s.close(resolve));
|
|
52
|
+
}
|
|
53
|
+
while (tmpDirs.length) {
|
|
54
|
+
const d = tmpDirs.pop();
|
|
55
|
+
await fsp.rm(d, { recursive: true, force: true });
|
|
56
|
+
}
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
async function mkTmp() {
|
|
60
|
+
const d = await fsp.mkdtemp(path.join(os.tmpdir(), 'sm-mcp-project-home-'));
|
|
61
|
+
tmpDirs.push(d);
|
|
62
|
+
return d;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** Starts a fake admin HTTP server that records every request and answers with `body`. */
|
|
66
|
+
function startFakeAdminServer(token, body) {
|
|
67
|
+
const requests = [];
|
|
68
|
+
return new Promise((resolve) => {
|
|
69
|
+
const server = http.createServer((req, res) => {
|
|
70
|
+
const chunks = [];
|
|
71
|
+
req.on('data', (c) => chunks.push(c));
|
|
72
|
+
req.on('end', () => {
|
|
73
|
+
if (req.headers.authorization !== `Bearer ${token}`) {
|
|
74
|
+
res.writeHead(401, { 'Content-Type': 'application/json' });
|
|
75
|
+
res.end(JSON.stringify({ ok: false, error: 'bad token' }));
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
const raw = Buffer.concat(chunks).toString('utf8');
|
|
79
|
+
requests.push({
|
|
80
|
+
method: req.method,
|
|
81
|
+
url: req.url,
|
|
82
|
+
body: raw ? JSON.parse(raw) : undefined,
|
|
83
|
+
});
|
|
84
|
+
res.writeHead(200, { 'Content-Type': 'application/json' });
|
|
85
|
+
res.end(JSON.stringify(body));
|
|
86
|
+
});
|
|
87
|
+
});
|
|
88
|
+
server.listen(0, '127.0.0.1', () => resolve({ server, requests }));
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function requireServerWithHome(homeDir) {
|
|
93
|
+
originalHome = process.env.HOME;
|
|
94
|
+
process.env.HOME = homeDir;
|
|
95
|
+
delete require.cache[require.resolve(SERVER_PATH)];
|
|
96
|
+
const mod = require(SERVER_PATH);
|
|
97
|
+
process.env.HOME = originalHome;
|
|
98
|
+
originalHome = undefined;
|
|
99
|
+
return mod;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
async function writeAdminConfig(homeDir, { port, token }) {
|
|
103
|
+
const tokenPath = path.join(homeDir, '.claude', 'session-manager', 'admin-api.json');
|
|
104
|
+
await fsp.mkdir(path.dirname(tokenPath), { recursive: true });
|
|
105
|
+
await fsp.writeFile(tokenPath, JSON.stringify({ port, token }), 'utf8');
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function callTool(handleCallTool, name, args) {
|
|
109
|
+
return handleCallTool({ params: { name, arguments: args } });
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
test('all four project_home_* tools are present in TOOLS with correct input schemas', async () => {
|
|
113
|
+
const homeDir = await mkTmp();
|
|
114
|
+
const { TOOLS } = requireServerWithHome(homeDir);
|
|
115
|
+
for (const name of PROJECT_HOME_TOOL_NAMES) {
|
|
116
|
+
const tool = TOOLS.find((t) => t.name === name);
|
|
117
|
+
expect(tool).toBeTruthy();
|
|
118
|
+
expect(tool.inputSchema.type).toBe('object');
|
|
119
|
+
expect(tool.inputSchema.properties.cwd).toBeTruthy();
|
|
120
|
+
// cwd is never required on any of the four
|
|
121
|
+
expect(tool.inputSchema.required ?? []).not.toContain('cwd');
|
|
122
|
+
}
|
|
123
|
+
const validate = TOOLS.find((t) => t.name === 'project_home_validate_summary');
|
|
124
|
+
expect(validate.inputSchema.required).toContain('summary');
|
|
125
|
+
const render = TOOLS.find((t) => t.name === 'project_home_render');
|
|
126
|
+
expect(render.inputSchema.required).toEqual(expect.arrayContaining(['summary', 'picks']));
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
test.each(PROJECT_HOME_TOOL_NAMES)('%s description equals the catalog-composed string', async (name) => {
|
|
130
|
+
const homeDir = await mkTmp();
|
|
131
|
+
const { TOOLS } = requireServerWithHome(homeDir);
|
|
132
|
+
const tool = TOOLS.find((t) => t.name === name);
|
|
133
|
+
const entry = MCP_TOOL_CATALOG.find((e) => e.name === name);
|
|
134
|
+
expect(entry).toBeTruthy();
|
|
135
|
+
expect(entry.group).toBe('project-home');
|
|
136
|
+
expect(tool.description).toBe(composeDescription(entry));
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test('project_home_get_contract dispatches GET /admin/project-home/contract with cwd query param', async () => {
|
|
140
|
+
const homeDir = await mkTmp();
|
|
141
|
+
const token = 'test-token';
|
|
142
|
+
const { server, requests } = await startFakeAdminServer(token, {
|
|
143
|
+
ok: true,
|
|
144
|
+
protocol: ['step one'],
|
|
145
|
+
catalog: { lenses: [{ id: 'home' }, { id: 'marketing' }, { id: 'feature' }, { id: 'architecture' }, { id: 'brief' }] },
|
|
146
|
+
paths: { summaryPath: '/abs/summary.json', picksPath: '/abs/picks.json', outputDir: '/abs/output' },
|
|
147
|
+
spec: { text: 'spec text', path: '/abs/spec.md' },
|
|
148
|
+
});
|
|
149
|
+
servers.push(server);
|
|
150
|
+
await writeAdminConfig(homeDir, { port: server.address().port, token });
|
|
151
|
+
|
|
152
|
+
const { handleCallTool } = requireServerWithHome(homeDir);
|
|
153
|
+
const result = await callTool(handleCallTool, 'project_home_get_contract', { cwd: '/home/bilko/Projects/session-manager' });
|
|
154
|
+
expect(result.isError).toBeFalsy();
|
|
155
|
+
expect(requests).toHaveLength(1);
|
|
156
|
+
expect(requests[0].method).toBe('GET');
|
|
157
|
+
expect(requests[0].url).toBe('/admin/project-home/contract?cwd=%2Fhome%2Fbilko%2FProjects%2Fsession-manager');
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
test('project_home_get_contract composed response mentions all 5 lens ids and no repo-relative path', async () => {
|
|
161
|
+
const homeDir = await mkTmp();
|
|
162
|
+
const token = 'test-token';
|
|
163
|
+
const { server } = await startFakeAdminServer(token, {
|
|
164
|
+
ok: true,
|
|
165
|
+
protocol: [
|
|
166
|
+
'Call GET /admin/project-home/status?cwd=<abs> first.',
|
|
167
|
+
'Compose a ProjectPageSummary matching summarySchema.',
|
|
168
|
+
'For each lens in catalog.lenses, pick a variant per slot.',
|
|
169
|
+
'POST /admin/project-home/validate-summary with {cwd, summary}.',
|
|
170
|
+
'POST /admin/project-home/render with {cwd, summary, picks}.',
|
|
171
|
+
],
|
|
172
|
+
summarySchema: { type: 'object' },
|
|
173
|
+
picksSchema: { type: 'object' },
|
|
174
|
+
catalog: {
|
|
175
|
+
lenses: [
|
|
176
|
+
{ id: 'home', slots: [] },
|
|
177
|
+
{ id: 'marketing', slots: [] },
|
|
178
|
+
{ id: 'feature', slots: [] },
|
|
179
|
+
{ id: 'architecture', slots: [] },
|
|
180
|
+
{ id: 'brief', slots: [] },
|
|
181
|
+
],
|
|
182
|
+
},
|
|
183
|
+
paths: { summaryPath: '/abs/project-pages/summary.json', picksPath: '/abs/project-pages/picks.json', outputDir: '/abs/project-pages/output' },
|
|
184
|
+
spec: { text: 'This project uses absolute paths only, never repo-relative ones.', path: '/abs/project-pages-pipeline.md' },
|
|
185
|
+
});
|
|
186
|
+
servers.push(server);
|
|
187
|
+
await writeAdminConfig(homeDir, { port: server.address().port, token });
|
|
188
|
+
|
|
189
|
+
const { handleCallTool } = requireServerWithHome(homeDir);
|
|
190
|
+
const result = await callTool(handleCallTool, 'project_home_get_contract', {});
|
|
191
|
+
expect(result.isError).toBeFalsy();
|
|
192
|
+
const text = result.content[0].text;
|
|
193
|
+
for (const lens of ['home', 'marketing', 'feature', 'architecture', 'brief']) {
|
|
194
|
+
expect(text).toContain(lens);
|
|
195
|
+
}
|
|
196
|
+
expect(text).not.toMatch(/session-manager-operations\/architecture\//);
|
|
197
|
+
expect(text).not.toMatch(/\.claude\/agents\//);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
test('project_home_validate_summary dispatches POST /admin/project-home/validate-summary with cwd+summary', async () => {
|
|
201
|
+
const homeDir = await mkTmp();
|
|
202
|
+
const token = 'test-token';
|
|
203
|
+
const { server, requests } = await startFakeAdminServer(token, { ok: true, valid: true, errors: [] });
|
|
204
|
+
servers.push(server);
|
|
205
|
+
await writeAdminConfig(homeDir, { port: server.address().port, token });
|
|
206
|
+
|
|
207
|
+
const { handleCallTool } = requireServerWithHome(homeDir);
|
|
208
|
+
const summary = { title: 'Example Project' };
|
|
209
|
+
const result = await callTool(handleCallTool, 'project_home_validate_summary', {
|
|
210
|
+
cwd: '/home/bilko/Projects/session-manager',
|
|
211
|
+
summary,
|
|
212
|
+
});
|
|
213
|
+
expect(result.isError).toBeFalsy();
|
|
214
|
+
expect(requests).toHaveLength(1);
|
|
215
|
+
expect(requests[0].method).toBe('POST');
|
|
216
|
+
expect(requests[0].url).toBe('/admin/project-home/validate-summary');
|
|
217
|
+
expect(requests[0].body).toEqual({ cwd: '/home/bilko/Projects/session-manager', summary });
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
test('project_home_validate_summary surfaces per-field errors from the route, not a generic failure', async () => {
|
|
221
|
+
const homeDir = await mkTmp();
|
|
222
|
+
const token = 'test-token';
|
|
223
|
+
const { server } = await startFakeAdminServer(token, {
|
|
224
|
+
ok: true,
|
|
225
|
+
valid: false,
|
|
226
|
+
errors: [{ field: 'quotes[0].attribution', message: 'quotes[0].attribution must not be a placeholder value' }],
|
|
227
|
+
});
|
|
228
|
+
servers.push(server);
|
|
229
|
+
await writeAdminConfig(homeDir, { port: server.address().port, token });
|
|
230
|
+
|
|
231
|
+
const { handleCallTool } = requireServerWithHome(homeDir);
|
|
232
|
+
const result = await callTool(handleCallTool, 'project_home_validate_summary', {
|
|
233
|
+
cwd: '/home/bilko/Projects/session-manager',
|
|
234
|
+
summary: { title: 'x', quotes: [{ attribution: 'TODO' }] },
|
|
235
|
+
});
|
|
236
|
+
expect(result.isError).toBeFalsy();
|
|
237
|
+
const body = JSON.parse(result.content[0].text);
|
|
238
|
+
expect(body.valid).toBe(false);
|
|
239
|
+
expect(body.errors).toEqual([
|
|
240
|
+
{ field: 'quotes[0].attribution', message: 'quotes[0].attribution must not be a placeholder value' },
|
|
241
|
+
]);
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
test('project_home_validate_summary requires summary', async () => {
|
|
245
|
+
const homeDir = await mkTmp();
|
|
246
|
+
const { handleCallTool } = requireServerWithHome(homeDir);
|
|
247
|
+
const result = await callTool(handleCallTool, 'project_home_validate_summary', { cwd: '/x' });
|
|
248
|
+
expect(result.isError).toBe(true);
|
|
249
|
+
expect(result.content[0].text).toContain('summary');
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
test('project_home_render dispatches POST /admin/project-home/render with cwd+summary+picks', async () => {
|
|
253
|
+
const homeDir = await mkTmp();
|
|
254
|
+
const token = 'test-token';
|
|
255
|
+
const { server, requests } = await startFakeAdminServer(token, {
|
|
256
|
+
ok: true,
|
|
257
|
+
filesWritten: ['/abs/summary.json'],
|
|
258
|
+
generatedAt: '2026-09-01T00:00:00.000Z',
|
|
259
|
+
});
|
|
260
|
+
servers.push(server);
|
|
261
|
+
await writeAdminConfig(homeDir, { port: server.address().port, token });
|
|
262
|
+
|
|
263
|
+
const { handleCallTool } = requireServerWithHome(homeDir);
|
|
264
|
+
const summary = { title: 'Example Project' };
|
|
265
|
+
const picks = { home: { hero: 'variant-a' } };
|
|
266
|
+
const result = await callTool(handleCallTool, 'project_home_render', {
|
|
267
|
+
cwd: '/home/bilko/Projects/session-manager',
|
|
268
|
+
summary,
|
|
269
|
+
picks,
|
|
270
|
+
});
|
|
271
|
+
expect(result.isError).toBeFalsy();
|
|
272
|
+
expect(requests).toHaveLength(1);
|
|
273
|
+
expect(requests[0].method).toBe('POST');
|
|
274
|
+
expect(requests[0].url).toBe('/admin/project-home/render');
|
|
275
|
+
expect(requests[0].body).toEqual({ cwd: '/home/bilko/Projects/session-manager', summary, picks });
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
test('project_home_render requires both summary and picks', async () => {
|
|
279
|
+
const homeDir = await mkTmp();
|
|
280
|
+
const { handleCallTool } = requireServerWithHome(homeDir);
|
|
281
|
+
|
|
282
|
+
const missingSummary = await callTool(handleCallTool, 'project_home_render', { picks: {} });
|
|
283
|
+
expect(missingSummary.isError).toBe(true);
|
|
284
|
+
expect(missingSummary.content[0].text).toContain('summary');
|
|
285
|
+
|
|
286
|
+
const missingPicks = await callTool(handleCallTool, 'project_home_render', { summary: {} });
|
|
287
|
+
expect(missingPicks.isError).toBe(true);
|
|
288
|
+
expect(missingPicks.content[0].text).toContain('picks');
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
test('project_home_status dispatches GET /admin/project-home/status with cwd query param', async () => {
|
|
292
|
+
const homeDir = await mkTmp();
|
|
293
|
+
const token = 'test-token';
|
|
294
|
+
const { server, requests } = await startFakeAdminServer(token, {
|
|
295
|
+
ok: true,
|
|
296
|
+
summary: { exists: false, mtimeMs: null },
|
|
297
|
+
picks: { exists: false, mtimeMs: null },
|
|
298
|
+
output: {
|
|
299
|
+
home: { exists: false, mtimeMs: null },
|
|
300
|
+
marketing: { exists: false, mtimeMs: null },
|
|
301
|
+
feature: { exists: false, mtimeMs: null },
|
|
302
|
+
architecture: { exists: false, mtimeMs: null },
|
|
303
|
+
brief: { exists: false, mtimeMs: null },
|
|
304
|
+
},
|
|
305
|
+
manifest: { exists: false, generatedAt: null },
|
|
306
|
+
});
|
|
307
|
+
servers.push(server);
|
|
308
|
+
await writeAdminConfig(homeDir, { port: server.address().port, token });
|
|
309
|
+
|
|
310
|
+
const { handleCallTool } = requireServerWithHome(homeDir);
|
|
311
|
+
const result = await callTool(handleCallTool, 'project_home_status', { cwd: '/home/bilko/Projects/session-manager' });
|
|
312
|
+
expect(result.isError).toBeFalsy();
|
|
313
|
+
expect(requests).toHaveLength(1);
|
|
314
|
+
expect(requests[0].method).toBe('GET');
|
|
315
|
+
expect(requests[0].url).toBe('/admin/project-home/status?cwd=%2Fhome%2Fbilko%2FProjects%2Fsession-manager');
|
|
316
|
+
|
|
317
|
+
// Never-generated project: well-formed all-absent result, not an error.
|
|
318
|
+
const body = JSON.parse(result.content[0].text);
|
|
319
|
+
expect(body.ok).toBe(true);
|
|
320
|
+
expect(body.summary.exists).toBe(false);
|
|
321
|
+
expect(body.manifest.exists).toBe(false);
|
|
322
|
+
expect(body.output.brief.exists).toBe(false);
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
test.each(PROJECT_HOME_TOOL_NAMES)('%s defaults cwd to SM_PROJECT_ROOT when omitted', async (name) => {
|
|
326
|
+
const homeDir = await mkTmp();
|
|
327
|
+
const token = 'test-token';
|
|
328
|
+
const { server, requests } = await startFakeAdminServer(token, { ok: true, valid: true, errors: [] });
|
|
329
|
+
servers.push(server);
|
|
330
|
+
await writeAdminConfig(homeDir, { port: server.address().port, token });
|
|
331
|
+
|
|
332
|
+
originalProjectRoot = process.env.SM_PROJECT_ROOT ?? null;
|
|
333
|
+
process.env.SM_PROJECT_ROOT = '/home/bilko/Projects/session-manager/session-manager-operations/scheduler/epics/some-epic/worktree';
|
|
334
|
+
|
|
335
|
+
const { handleCallTool } = requireServerWithHome(homeDir);
|
|
336
|
+
const args = name === 'project_home_render' ? { summary: {}, picks: {} } : name === 'project_home_validate_summary' ? { summary: {} } : {};
|
|
337
|
+
await callTool(handleCallTool, name, args);
|
|
338
|
+
expect(requests).toHaveLength(1);
|
|
339
|
+
const encoded = requests[0].body?.cwd ?? new URLSearchParams(requests[0].url.split('?')[1]).get('cwd');
|
|
340
|
+
expect(encoded).toBe(process.env.SM_PROJECT_ROOT);
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
test.each(PROJECT_HOME_TOOL_NAMES)('%s returns the app-not-running error when the admin API is unreachable', async (name) => {
|
|
344
|
+
const homeDir = await mkTmp();
|
|
345
|
+
const { handleCallTool, NOT_RUNNING_ERROR } = requireServerWithHome(homeDir);
|
|
346
|
+
const args = name === 'project_home_render' ? { summary: {}, picks: {} } : name === 'project_home_validate_summary' ? { summary: {} } : {};
|
|
347
|
+
const result = await callTool(handleCallTool, name, args);
|
|
348
|
+
expect(result.isError).toBe(true);
|
|
349
|
+
expect(result.content[0].text).toBe(NOT_RUNNING_ERROR);
|
|
350
|
+
});
|
|
@@ -36,6 +36,7 @@
|
|
|
36
36
|
const path = require('path');
|
|
37
37
|
const { activeIndexPath, withPathLock } = require('./epicMint.cjs');
|
|
38
38
|
const config = require('../config.cjs');
|
|
39
|
+
const { mirrorEpicStatus } = require('./epicStatusMirror.cjs');
|
|
39
40
|
|
|
40
41
|
function hasOwn(obj, key) {
|
|
41
42
|
return Object.prototype.hasOwnProperty.call(obj, key);
|
|
@@ -160,6 +161,20 @@ async function mergeActiveIndex(cwd, { sessions: rawMemorySessions = {}, events:
|
|
|
160
161
|
// payload, so a compromised/forged renderer call can't claim a
|
|
161
162
|
// different writer id.
|
|
162
163
|
await config.writeJson(filePath, index, { writer: 'epics' });
|
|
164
|
+
// Status mirror (epicStatusMirror.cjs) — this IPC handler is the
|
|
165
|
+
// renderer-driven persist path (persistActiveIndex), the write site for
|
|
166
|
+
// both the 'proposed'->'active' start transition (approveProposed) and
|
|
167
|
+
// every other in-place session mutation. Mirrors every surviving row so
|
|
168
|
+
// a lost/clobbered active-index.json can be reconstructed from these
|
|
169
|
+
// files (activeIndexRebuild.cjs). Best-effort per row: one bad write must
|
|
170
|
+
// not undo the index write that already landed above.
|
|
171
|
+
for (const [id, sess] of Object.entries(mergedSessions)) {
|
|
172
|
+
try {
|
|
173
|
+
mirrorEpicStatus(cwd, id, { session: sess, status: sess.status });
|
|
174
|
+
} catch (e) {
|
|
175
|
+
console.warn(`[activeIndexMerge] mirrorEpicStatus failed for ${id}`, e);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
163
178
|
return { sessions: mergedSessions, events: mergedEvents };
|
|
164
179
|
});
|
|
165
180
|
}
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* activeIndexRebuild.cjs — reconstructs `active-index.json` from the
|
|
5
|
+
* per-Epic status mirrors `epicStatusMirror.cjs` writes onto `prompt-
|
|
6
|
+
* sessions/<epicId>.json`. This is the recovery path for the incident the
|
|
7
|
+
* mirror exists to survive: a lost, clobbered, git-reverted, or
|
|
8
|
+
* worktree-snapshotted index that would otherwise silently erase every open
|
|
9
|
+
* Epic it held with no way to get them back.
|
|
10
|
+
*
|
|
11
|
+
* Rebuild NEVER mints — SINGLE-CREATOR LAW (epicMint.cjs) is untouched
|
|
12
|
+
* because no new Epic id is ever created here, only rows for ids that
|
|
13
|
+
* already have a mirrored file re-materialized. It also never resurrects a
|
|
14
|
+
* tombstoned or archived Epic (see the two "never resurrect" guards below) —
|
|
15
|
+
* a human's delete/complete decision always wins.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const fs = require('node:fs');
|
|
19
|
+
const path = require('node:path');
|
|
20
|
+
const { activeIndexPath, readActiveIndex, writeActiveIndex, withPathLock } = require('./epicMint.cjs');
|
|
21
|
+
|
|
22
|
+
function promptSessionsDir(cwd) {
|
|
23
|
+
const { opsPath } = require('./opsOwnership.cjs');
|
|
24
|
+
return opsPath(cwd, 'prompt-sessions');
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function hasOwn(obj, key) {
|
|
28
|
+
return Object.prototype.hasOwnProperty.call(obj, key);
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function listMirrorFiles(dir) {
|
|
32
|
+
let entries;
|
|
33
|
+
try {
|
|
34
|
+
entries = fs.readdirSync(dir);
|
|
35
|
+
} catch {
|
|
36
|
+
return [];
|
|
37
|
+
}
|
|
38
|
+
return entries.filter((f) => f.endsWith('.json') && f !== 'active-index.json');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* rebuildActiveIndex(cwd, { dryRun? }) → { rows, skipped, reasons }
|
|
43
|
+
*
|
|
44
|
+
* `rows` — reconstructed PromptSession objects, one per mirrored file that
|
|
45
|
+
* is a live (proposed/active), non-tombstoned, non-archived Epic. These
|
|
46
|
+
* become the new `sessions` map (dryRun: false is the default — pass
|
|
47
|
+
* `{ dryRun: true }` to preview without writing).
|
|
48
|
+
* `skipped` — file names not turned into a row.
|
|
49
|
+
* `reasons` — `{ [fileName]: humanReadableReason }`, one entry per skipped
|
|
50
|
+
* file. A file is never guessed at: no mirrored `status` is a skip, not a
|
|
51
|
+
* fabricated 'unknown' row (see scripts/mirror-epic-status.cjs for the
|
|
52
|
+
* only place that vocabulary is deliberately introduced, for the
|
|
53
|
+
* migration's own back-fill bookkeeping).
|
|
54
|
+
*
|
|
55
|
+
* `events` and `tombstones` from the existing on-disk index (if any) are
|
|
56
|
+
* carried through completely untouched — this function only ever
|
|
57
|
+
* reconstructs `sessions`.
|
|
58
|
+
*/
|
|
59
|
+
function rebuildActiveIndex(cwd, { dryRun = false } = {}) {
|
|
60
|
+
if (!cwd || typeof cwd !== 'string') throw new Error('rebuildActiveIndex: cwd is required');
|
|
61
|
+
cwd = path.resolve(cwd);
|
|
62
|
+
return withPathLock(activeIndexPath(cwd), () => {
|
|
63
|
+
const dir = promptSessionsDir(cwd);
|
|
64
|
+
const existing = readActiveIndex(cwd);
|
|
65
|
+
const tombstones = existing.tombstones || {};
|
|
66
|
+
const files = listMirrorFiles(dir);
|
|
67
|
+
|
|
68
|
+
const rows = [];
|
|
69
|
+
const skipped = [];
|
|
70
|
+
const reasons = {};
|
|
71
|
+
|
|
72
|
+
for (const file of files) {
|
|
73
|
+
const id = file.slice(0, -'.json'.length);
|
|
74
|
+
const full = path.join(dir, file);
|
|
75
|
+
let parsed;
|
|
76
|
+
try {
|
|
77
|
+
parsed = JSON.parse(fs.readFileSync(full, 'utf8'));
|
|
78
|
+
} catch (e) {
|
|
79
|
+
skipped.push(file);
|
|
80
|
+
reasons[file] = `unreadable: ${e.message}`;
|
|
81
|
+
continue;
|
|
82
|
+
}
|
|
83
|
+
if (!parsed || typeof parsed !== 'object' || Array.isArray(parsed)) {
|
|
84
|
+
skipped.push(file);
|
|
85
|
+
reasons[file] = 'unreadable: not a JSON object';
|
|
86
|
+
continue;
|
|
87
|
+
}
|
|
88
|
+
// Never resurrected — a tombstoned id stays out even if its file says
|
|
89
|
+
// 'active' (see activeIndexMerge.cjs's header on why tombstones are
|
|
90
|
+
// never auto-cleared).
|
|
91
|
+
if (hasOwn(tombstones, id)) {
|
|
92
|
+
skipped.push(file);
|
|
93
|
+
reasons[file] = 'tombstoned — never resurrected by a rebuild';
|
|
94
|
+
continue;
|
|
95
|
+
}
|
|
96
|
+
if (!parsed.status) {
|
|
97
|
+
skipped.push(file);
|
|
98
|
+
reasons[file] = 'no status mirror on this file — never guessed at';
|
|
99
|
+
continue;
|
|
100
|
+
}
|
|
101
|
+
// A completed archive (markCompleted's PromptSessionArchive, or any
|
|
102
|
+
// mirror written after archival) is never restored as a live row.
|
|
103
|
+
if (parsed.archivedAt) {
|
|
104
|
+
skipped.push(file);
|
|
105
|
+
reasons[file] = 'archivedAt is set — completed Epic, not a live row';
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
108
|
+
if (parsed.status !== 'proposed' && parsed.status !== 'active') {
|
|
109
|
+
skipped.push(file);
|
|
110
|
+
reasons[file] = `status '${parsed.status}' is not a live row`;
|
|
111
|
+
continue;
|
|
112
|
+
}
|
|
113
|
+
// Drop mirror-only bookkeeping (`indexedAt`) — it is not part of the
|
|
114
|
+
// PromptSession shape active-index.json's `sessions` map holds.
|
|
115
|
+
const { indexedAt, archivedAt, ...row } = parsed;
|
|
116
|
+
rows.push({ ...row, id, cwd: parsed.cwd || cwd });
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (!dryRun) {
|
|
120
|
+
const sessions = {};
|
|
121
|
+
for (const row of rows) sessions[row.id] = row;
|
|
122
|
+
writeActiveIndex(cwd, {
|
|
123
|
+
sessions,
|
|
124
|
+
events: existing.events || {},
|
|
125
|
+
tombstones,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return { rows, skipped, reasons };
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
module.exports = { rebuildActiveIndex };
|
|
@@ -120,9 +120,67 @@ function resolveEpicModel({ cwd, claudeSessionId, fallbackModel = FALLBACK_MODEL
|
|
|
120
120
|
}
|
|
121
121
|
}
|
|
122
122
|
|
|
123
|
+
/** Mirrors epicIntake.ts's AGENT_BODY_CHAR_CAP — a runaway persona file must
|
|
124
|
+
* not be able to dominate a spawned job's system prompt either. Duplicated
|
|
125
|
+
* rather than imported: epicIntake.ts is a renderer module (no ES modules
|
|
126
|
+
* in main). */
|
|
127
|
+
const AGENT_BODY_CHAR_CAP = 6000;
|
|
128
|
+
|
|
129
|
+
/** Trims a persona's frontmatter-stripped body and caps it at
|
|
130
|
+
* AGENT_BODY_CHAR_CAP, appending a truncation notice naming `personaPath`
|
|
131
|
+
* — same shape as epicIntake.ts's buildPersonaBodyText. Returns null for an
|
|
132
|
+
* empty body. */
|
|
133
|
+
function buildPersonaBodyText(rawBody, personaPath) {
|
|
134
|
+
const body = (rawBody || '').trim();
|
|
135
|
+
if (!body) return null;
|
|
136
|
+
if (body.length <= AGENT_BODY_CHAR_CAP) return body;
|
|
137
|
+
const truncated = body.slice(0, AGENT_BODY_CHAR_CAP);
|
|
138
|
+
const pathLabel = personaPath || 'the persona file';
|
|
139
|
+
return `${truncated}\n\n[Truncated — ${pathLabel} exceeds ${AGENT_BODY_CHAR_CAP} characters; see the file for the full body.]`;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* Resolves the persona system-prompt + model a scheduled PRD's `agentType`
|
|
144
|
+
* should launch its headless `claude -p` spawn with. Never throws — a
|
|
145
|
+
* missing agentType, or one that no longer resolves to a readable persona
|
|
146
|
+
* file (renamed/deleted), falls back to `{ systemPrompt: null, model:
|
|
147
|
+
* fallbackModel, personaPath: null }` and logs once via prdAgentType.cjs's
|
|
148
|
+
* reportDanglingAgentTypeOnce (the PRD-frontmatter FK's own dangling-read
|
|
149
|
+
* convention) — a dangling persona must never park or fail the job.
|
|
150
|
+
*
|
|
151
|
+
* Reuses agentLibrary.cjs's getPersonaBody (project-overlay-then-global
|
|
152
|
+
* read, same precedence resolvePersonaPaths documents) rather than a second
|
|
153
|
+
* persona-file reader, and this module's own frontmatter-stripped-body cap
|
|
154
|
+
* (mirroring epicIntake.ts's Epic-path treatment) so the PRD path gets the
|
|
155
|
+
* same runaway-file protection.
|
|
156
|
+
*
|
|
157
|
+
* @param {{ cwd: string, agentType: string|null, fallbackModel?: string, deps?: object }} opts
|
|
158
|
+
* @returns {Promise<{ model: string, systemPrompt: string|null, personaPath: string|null }>}
|
|
159
|
+
*/
|
|
160
|
+
async function resolvePrdPersonaForSpawn({ cwd, agentType, fallbackModel = FALLBACK_MODEL, deps = {} } = {}) {
|
|
161
|
+
const miss = { model: fallbackModel, systemPrompt: null, personaPath: null };
|
|
162
|
+
if (!agentType) return miss;
|
|
163
|
+
try {
|
|
164
|
+
const getBody = deps.getPersonaBody || require('../agentLibrary.cjs').getPersonaBody;
|
|
165
|
+
const persona = await getBody({ cwd, name: agentType });
|
|
166
|
+
if (!persona) {
|
|
167
|
+
const reportOnce = deps.reportDanglingAgentTypeOnce || require('./prdAgentType.cjs').reportDanglingAgentTypeOnce;
|
|
168
|
+
reportOnce(cwd, agentType, deps);
|
|
169
|
+
return miss;
|
|
170
|
+
}
|
|
171
|
+
const { fm, body } = splitFrontmatter(persona.text);
|
|
172
|
+
const model = fm.model && fm.model !== 'inherit' ? fm.model : fallbackModel;
|
|
173
|
+
const systemPrompt = buildPersonaBodyText(body, persona.path);
|
|
174
|
+
return { model, systemPrompt, personaPath: persona.path };
|
|
175
|
+
} catch {
|
|
176
|
+
return miss;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
123
180
|
module.exports = {
|
|
124
181
|
FALLBACK_MODEL,
|
|
125
182
|
resolveEpicModel,
|
|
126
183
|
findAgentTypeByClaudeSessionId,
|
|
127
184
|
readPersonaModel,
|
|
185
|
+
resolvePrdPersonaForSpawn,
|
|
128
186
|
};
|
|
@@ -30,7 +30,7 @@
|
|
|
30
30
|
const fs = require('node:fs');
|
|
31
31
|
const path = require('node:path');
|
|
32
32
|
|
|
33
|
-
const BUILD_TARGET_SUBPATH = ['
|
|
33
|
+
const BUILD_TARGET_SUBPATH = ['architecture', 'build-target.json']; // joined under opsOwnership.opsPath
|
|
34
34
|
|
|
35
35
|
function readJson(filePath) {
|
|
36
36
|
try {
|
|
@@ -48,7 +48,8 @@ function readJson(filePath) {
|
|
|
48
48
|
function resolveBuildTarget(cwd) {
|
|
49
49
|
if (!cwd || typeof cwd !== 'string') return null;
|
|
50
50
|
|
|
51
|
-
const
|
|
51
|
+
const { opsPath } = require('./opsOwnership.cjs');
|
|
52
|
+
const configPath = opsPath(cwd, ...BUILD_TARGET_SUBPATH);
|
|
52
53
|
const explicit = readJson(configPath);
|
|
53
54
|
if (explicit && typeof explicit === 'object' && !Array.isArray(explicit) && typeof explicit.packageName === 'string') {
|
|
54
55
|
return explicit;
|