claude-code-session-manager 0.122.0 → 0.123.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-BmJ8hIPw.js → AgentLibrary-DxWN3FSN.js} +2 -2
- package/dist/assets/{Button-BS4kmz4p.js → Button-CISLFSKb.js} +1 -1
- package/dist/assets/DataModel-CYPbhGrx.js +1 -0
- package/dist/assets/{History-pg00b2Gd.js → History-Fhouektw.js} +2 -2
- package/dist/assets/{Hooks-BTEs0UZY.js → Hooks-COfIcyYV.js} +3 -3
- package/dist/assets/{Library-DuaJ-VSn.js → Library-Bt5WOyyu.js} +7 -7
- package/dist/assets/ListDetail-DH7uzYd5.js +1 -0
- package/dist/assets/{McpServers-Dy-wnG_X.js → McpServers-CwyVEzMN.js} +2 -2
- package/dist/assets/{Memory-Bs1T1Jo7.js → Memory-k9TWT9N4.js} +8 -8
- package/dist/assets/{Panel-8UhLXEOW.js → Panel-Di8JqCYN.js} +1 -1
- package/dist/assets/{Permissions-BfwzvmBS.js → Permissions-FFs6Rq1H.js} +3 -3
- package/dist/assets/{Plugins-McjyvvKh.js → Plugins-uK5vl_RE.js} +2 -2
- package/dist/assets/{ProvenanceBadge-D0Ov_6WJ.js → ProvenanceBadge-CGDyGDoN.js} +1 -1
- package/dist/assets/{SaveBar-Cd6hJR6K.js → SaveBar-Cnwc_XfF.js} +1 -1
- package/dist/assets/{Scheduler-BZXKqYJP.js → Scheduler-aACa6_mC.js} +7 -7
- package/dist/assets/{ScopeSwitcher-C24ysV1K.js → ScopeSwitcher-CoG3CSlC.js} +1 -1
- package/dist/assets/Settings-BG0RM6Di.js +4 -0
- package/dist/assets/{SkillReferenceGraph-BsWuiTbw.js → SkillReferenceGraph-COffGu_A.js} +1 -1
- package/dist/assets/Skills--qQpFjjf.js +2 -0
- package/dist/assets/SystemPrompt-Cr9RRSiZ.js +1 -0
- package/dist/assets/TagLibrary-D_ZbXwBv.js +1 -0
- package/dist/assets/{TiptapBody-C4kIvIMH.js → TiptapBody-CCHsa8Jv.js} +1 -1
- package/dist/assets/{Toggle-pP19zlyk.js → Toggle-BUOj4pws.js} +1 -1
- package/dist/assets/{ViewTabs-CpzN8e5I.js → ViewTabs-BdpsL7yr.js} +1 -1
- package/dist/assets/{historyTotals-Caq78bb5.js → historyTotals-Dx6ca881.js} +1 -1
- package/dist/assets/{index-Dd7UQabV.css → index-BnKaYERU.css} +1 -1
- package/dist/assets/{index-Bjtguv6I.js → index-CkNGj8FA.js} +151 -151
- package/dist/assets/{settingsSchema-CC2ySu3d.js → settingsSchema-BZhkpr9e.js} +1 -1
- package/dist/assets/{skillFrontmatter-hDhNo8jE.js → skillFrontmatter-N13KGqbs.js} +1 -1
- package/dist/assets/{useAgentsDirWatch-BuOtY25U.js → useAgentsDirWatch-De9UsbDt.js} +1 -1
- package/dist/index.html +2 -2
- package/package.json +1 -1
- package/plugins/session-manager-dev/skills/develop/SKILL.md +29 -24
- package/scripts/scheduler-mcp-server.cjs +142 -78
- package/src/main/agentLibrary.cjs +8 -3
- package/src/main/build-info.json +4 -4
- package/src/main/crashDiagnostics.cjs +24 -0
- package/src/main/historyAggregator.cjs +41 -7
- package/src/main/index.cjs +13 -3
- package/src/main/ipcSchemas.cjs +30 -22
- package/src/main/lib/activeSessions.cjs +93 -32
- package/src/main/lib/epic/delegationReadiness.cjs +2 -0
- package/src/main/lib/epic/delegationSelfHeal.cjs +77 -0
- package/src/main/lib/epic/epicMint.cjs +1 -1
- package/src/main/lib/epic/epicStandingInstructions.cjs +73 -0
- package/src/main/lib/epic/guardSpawnSettings.cjs +76 -0
- package/src/main/lib/feedback-remote/crossProjectFeedback.cjs +9 -2
- package/src/main/lib/feedback-remote/remoteClientSessions.cjs +3 -11
- package/src/main/lib/feedback-remote/remoteE2eCrypto.cjs +1 -1
- package/src/main/lib/git/gitWorktree.cjs +9 -4
- package/src/main/lib/git/worktreeDeps.cjs +13 -2
- package/src/main/lib/git/worktreeSetup.cjs +212 -0
- package/src/main/lib/mcpToolCatalog.cjs +36 -9
- package/src/main/lib/persona-macro/agentPersonaSchema.cjs +4 -0
- package/src/main/lib/persona-macro/shippedPersonaSeeds.cjs +23 -0
- package/src/main/lib/prd/prdAgentType.cjs +28 -0
- package/src/main/lib/prd/prdBodyPatch.cjs +138 -0
- package/src/main/lib/prd/prdCreate.cjs +245 -27
- package/src/main/lib/prd/prdGateFiles.cjs +31 -0
- package/src/main/lib/scheduler/definitionOfDone.cjs +24 -3
- package/src/main/lib/scheduler/queueStore.cjs +5 -4
- package/src/main/lib/scheduler/scheduleJobSchema.cjs +1 -0
- package/src/main/lib/scheduler/schedulerBatch.cjs +43 -5
- package/src/main/lib/scheduler/schedulerPaths.cjs +2 -0
- package/src/main/lib/scheduler/watchdogHelpers.cjs +114 -22
- package/src/main/lib/telemetry/mainHeapSnapshot.cjs +69 -0
- package/src/main/lib/telemetry/telemetryBoot.cjs +25 -3
- package/src/main/lib/telemetry/telemetryClient.cjs +12 -0
- package/src/main/lib/telemetry/telemetryCounters.cjs +25 -2
- package/src/main/pty.cjs +0 -53
- package/src/main/runVerify.cjs +38 -11
- package/src/main/scheduler/prdParser.cjs +3 -0
- package/src/main/scheduler/prdPaths.cjs +48 -2
- package/src/main/scheduler.cjs +135 -31
- package/src/main/seedDevPlugin.cjs +1 -1
- package/src/main/seedSchedulerMcp.cjs +1 -1
- package/src/main/templates/PRD_AUTHORING.md +8 -4
- package/src/main/webRemote.cjs +38 -132
- package/src/preload/api.d.ts +4 -52
- package/src/preload/index.cjs +0 -24
- package/src/seed/agents/dev-lead.md +2 -1
- package/src/seed/agents/validator.md +2 -1
- package/dist/assets/DataModel-DZHBZrfL.js +0 -1
- package/dist/assets/ListDetail-Cud72s-O.js +0 -1
- package/dist/assets/RemoteMachines-gOlFTvWq.js +0 -3
- package/dist/assets/Settings-DWnN2qxn.js +0 -4
- package/dist/assets/Skills-6pHn6glu.js +0 -2
- package/dist/assets/SystemPrompt-DT0LBH-K.js +0 -1
- package/dist/assets/TagLibrary-DbJ0zbYL.js +0 -1
- package/dist/assets/useRemotePeers-CNQmRBpt.js +0 -1
- package/src/main/lib/feedback-remote/relayClientAuth.cjs +0 -133
- package/src/main/remoteClient.cjs +0 -334
- package/src/main/remoteClientIpc.cjs +0 -133
|
@@ -15,6 +15,8 @@
|
|
|
15
15
|
|
|
16
16
|
const { personaFileExists, resolvePersonaPaths } = require('../epic/epicMint.cjs');
|
|
17
17
|
const { listPersonas } = require('../../agentLibrary.cjs');
|
|
18
|
+
const { splitFrontmatter } = require('./prdFrontmatter.cjs');
|
|
19
|
+
const fs = require('node:fs');
|
|
18
20
|
|
|
19
21
|
/** PRD execution's documented default persona (dev-lead.md's own frontmatter role). */
|
|
20
22
|
const DEFAULT_PRD_AGENT_TYPE = 'dev-lead';
|
|
@@ -77,8 +79,34 @@ async function assertAgentTypeWritable(cwd, agentType, deps = {}) {
|
|
|
77
79
|
);
|
|
78
80
|
}
|
|
79
81
|
|
|
82
|
+
/**
|
|
83
|
+
* resolvePersonaDefaultEstimate(cwd, agentType, deps) — the persona's
|
|
84
|
+
* optional `defaultEstimateMinutes` frontmatter (integer 1..60), project
|
|
85
|
+
* overlay before global. Returns a number or null; never throws (missing,
|
|
86
|
+
* malformed or out-of-range → null). Uncached sync read: called once per
|
|
87
|
+
* job spawn.
|
|
88
|
+
*/
|
|
89
|
+
function resolvePersonaDefaultEstimate(cwd, agentType, deps = {}) {
|
|
90
|
+
try {
|
|
91
|
+
if (!cwd || typeof agentType !== 'string' || !/^[a-z0-9][a-z0-9-]*$/.test(agentType)) return null;
|
|
92
|
+
const { projectPath, globalPath } = resolvePersonaPaths(cwd, agentType, deps);
|
|
93
|
+
for (const file of [projectPath, globalPath]) {
|
|
94
|
+
let text;
|
|
95
|
+
try { text = fs.readFileSync(file, 'utf8'); } catch { continue; }
|
|
96
|
+
const raw = splitFrontmatter(text).fm.defaultEstimateMinutes;
|
|
97
|
+
if (raw === undefined) continue;
|
|
98
|
+
const str = String(raw).trim();
|
|
99
|
+
if (!/^\d+$/.test(str)) continue;
|
|
100
|
+
const n = Number(str);
|
|
101
|
+
if (n >= 1 && n <= 60) return n;
|
|
102
|
+
}
|
|
103
|
+
} catch { /* never throws */ }
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
|
|
80
107
|
module.exports = {
|
|
81
108
|
DEFAULT_PRD_AGENT_TYPE,
|
|
109
|
+
resolvePersonaDefaultEstimate,
|
|
82
110
|
assertAgentTypeWritable,
|
|
83
111
|
reportDanglingAgentTypeOnce,
|
|
84
112
|
};
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
/**
|
|
3
|
+
* prdBodyPatch.cjs — pure structured patch of a PRD body. Replaces only the
|
|
4
|
+
* `# Gate`, `# Files` and/or `# Acceptance criteria` sections, validating and
|
|
5
|
+
* rendering gate/files with the same prdGateFiles.cjs functions createPrd uses.
|
|
6
|
+
* No I/O.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
const { validateGate, validateFiles, renderFilesSection, renderGateSection } = require('./prdGateFiles.cjs');
|
|
10
|
+
const { resolveGate, parseChain, isNoneGate } = require('../scheduler/definitionOfDone.cjs');
|
|
11
|
+
|
|
12
|
+
const H_AC = '# Acceptance criteria';
|
|
13
|
+
const H_FILES = '# Files';
|
|
14
|
+
const H_GATE = '# Gate';
|
|
15
|
+
const H_NOTES = '# Implementation notes';
|
|
16
|
+
const H_STANDARDS = '## Engineering standards';
|
|
17
|
+
|
|
18
|
+
/** Locate a heading's span [start, end) in lines, ignoring fenced code. */
|
|
19
|
+
function findSection(lines, heading) {
|
|
20
|
+
let inFence = false;
|
|
21
|
+
let start = -1;
|
|
22
|
+
for (let i = 0; i < lines.length; i++) {
|
|
23
|
+
const line = lines[i];
|
|
24
|
+
if (/^\s*```/.test(line)) { inFence = !inFence; continue; }
|
|
25
|
+
if (inFence) continue;
|
|
26
|
+
if (start === -1) {
|
|
27
|
+
if (line === heading) start = i;
|
|
28
|
+
} else if (/^#{1,2} /.test(line)) {
|
|
29
|
+
return { start, end: i };
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
return start === -1 ? null : { start, end: lines.length };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function findHeadingIndex(lines, heading) {
|
|
36
|
+
const s = findSection(lines, heading);
|
|
37
|
+
return s ? s.start : -1;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function replaceSection(lines, heading, rendered, insertBeforeHeadings) {
|
|
41
|
+
const span = findSection(lines, heading);
|
|
42
|
+
if (span) {
|
|
43
|
+
lines.splice(span.start, span.end - span.start, ...rendered);
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
let at = lines.length;
|
|
47
|
+
for (const h of insertBeforeHeadings) {
|
|
48
|
+
const idx = findHeadingIndex(lines, h);
|
|
49
|
+
if (idx !== -1) { at = idx; break; }
|
|
50
|
+
}
|
|
51
|
+
lines.splice(at, 0, ...rendered);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function gateSignature(body) {
|
|
55
|
+
const r = resolveGate(body);
|
|
56
|
+
return JSON.stringify([r.source, r.sequence]);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* @param {string} body
|
|
61
|
+
* @param {{gate?: string[], files?: string[], acceptanceCriteria?: string[]}} patch
|
|
62
|
+
* @returns {{ok:true, body:string, warnings:string[]}|{ok:false, error:string}}
|
|
63
|
+
*/
|
|
64
|
+
function applyStructuredPrdPatch(body, patch) {
|
|
65
|
+
if (typeof body !== 'string') return { ok: false, error: 'body must be a string.' };
|
|
66
|
+
if (!patch || typeof patch !== 'object') return { ok: false, error: 'patch must be an object.' };
|
|
67
|
+
const hasGate = patch.gate !== undefined;
|
|
68
|
+
const hasFiles = patch.files !== undefined;
|
|
69
|
+
const hasAc = patch.acceptanceCriteria !== undefined;
|
|
70
|
+
if (!hasGate && !hasFiles && !hasAc) {
|
|
71
|
+
return { ok: false, error: 'patch must name at least one of gate, files, acceptanceCriteria.' };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
let warnings = [];
|
|
75
|
+
let gateCommands = null;
|
|
76
|
+
let filesList = null;
|
|
77
|
+
let acLines = null;
|
|
78
|
+
|
|
79
|
+
if (hasGate) {
|
|
80
|
+
const g = validateGate(patch.gate);
|
|
81
|
+
if (!g.ok) return { ok: false, error: g.error };
|
|
82
|
+
gateCommands = g.commands;
|
|
83
|
+
warnings = warnings.concat(g.warnings || []);
|
|
84
|
+
}
|
|
85
|
+
if (hasFiles) {
|
|
86
|
+
const f = validateFiles(patch.files);
|
|
87
|
+
if (!f.ok) return { ok: false, error: f.error };
|
|
88
|
+
filesList = f.files;
|
|
89
|
+
}
|
|
90
|
+
if (hasAc) {
|
|
91
|
+
const ac = patch.acceptanceCriteria;
|
|
92
|
+
if (!Array.isArray(ac) || ac.length === 0) {
|
|
93
|
+
return { ok: false, error: 'acceptanceCriteria must be a non-empty array of strings.' };
|
|
94
|
+
}
|
|
95
|
+
acLines = [];
|
|
96
|
+
for (let i = 0; i < ac.length; i++) {
|
|
97
|
+
if (typeof ac[i] !== 'string' || ac[i].trim() === '') {
|
|
98
|
+
return { ok: false, error: `acceptanceCriteria entry ${i + 1}: must be a non-empty string.` };
|
|
99
|
+
}
|
|
100
|
+
if (/[\r\n]/.test(ac[i])) {
|
|
101
|
+
return { ok: false, error: `acceptanceCriteria entry ${i + 1}: must not contain a newline.` };
|
|
102
|
+
}
|
|
103
|
+
acLines.push(`- [ ] ${ac[i].trim()}`);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const lines = body.split('\n');
|
|
108
|
+
if (hasAc) {
|
|
109
|
+
if (!findSection(lines, H_AC)) return { ok: false, error: `body has no "${H_AC}" section to patch.` };
|
|
110
|
+
replaceSection(lines, H_AC, [H_AC, '', ...acLines, ''], []);
|
|
111
|
+
}
|
|
112
|
+
if (hasFiles) {
|
|
113
|
+
replaceSection(lines, H_FILES, renderFilesSection(filesList), [H_NOTES, H_STANDARDS]);
|
|
114
|
+
}
|
|
115
|
+
if (hasGate) {
|
|
116
|
+
replaceSection(lines, H_GATE, renderGateSection(gateCommands), [H_STANDARDS]);
|
|
117
|
+
}
|
|
118
|
+
const next = lines.join('\n');
|
|
119
|
+
|
|
120
|
+
// Read-back with the scheduler's own reader, as createPrd does.
|
|
121
|
+
if (hasGate) {
|
|
122
|
+
const expectedSource = gateCommands.length === 1 && isNoneGate(gateCommands[0]) ? 'none' : 'explicit';
|
|
123
|
+
const expectedSequence = expectedSource === 'none' ? [] : gateCommands.flatMap(parseChain);
|
|
124
|
+
const resolved = resolveGate(next);
|
|
125
|
+
if (resolved.source !== expectedSource || JSON.stringify(resolved.sequence) !== JSON.stringify(expectedSequence)) {
|
|
126
|
+
return {
|
|
127
|
+
ok: false,
|
|
128
|
+
error: 'The PRD text holds another gate fence (three backticks + gate) outside the # Gate section, so the scheduler would read the wrong gate. Remove it and retry.',
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
} else if (gateSignature(next) !== gateSignature(body)) {
|
|
132
|
+
return { ok: false, error: 'The patch would change how the scheduler reads the PRD gate (stray gate fence in the new text).' };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return { ok: true, body: next, warnings };
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
module.exports = { applyStructuredPrdPatch };
|
|
@@ -32,7 +32,9 @@ const { resolveDepSlug, findNearMatches } = require('./depSlugResolve.cjs');
|
|
|
32
32
|
const { resolveEpicPrdWriteDir } = require('./prdLocations.cjs');
|
|
33
33
|
const { isFixPlanSlug } = require('../scheduler/fixPlanSlug.cjs');
|
|
34
34
|
const { isIncomplete, resolveChainTerminals, isValidPlanId, mintPlanId, resolveInheritedPlanId } = require('./prdDisposition.cjs');
|
|
35
|
+
const { splitFrontmatter } = require('./prdFrontmatter.cjs');
|
|
35
36
|
const { sizingWarnings } = require('./prdSizing.cjs');
|
|
37
|
+
const telemetryCounters = require('../telemetry/telemetryCounters.cjs');
|
|
36
38
|
const { execGit } = require('../git/gitExec.cjs');
|
|
37
39
|
const { extractRepoPathCandidates, worktreeReachWarnings } = require('./prdWorktreeReach.cjs');
|
|
38
40
|
const { validateGate, validateFiles, renderFilesSection, renderGateSection } = require('./prdGateFiles.cjs');
|
|
@@ -224,7 +226,21 @@ async function worktreeReachAdvisory(input, cwd) {
|
|
|
224
226
|
* on failure — `status` is the HTTP status code the caller should map errors to
|
|
225
227
|
* (400/409/500), left to the caller since only the HTTP route needs it.
|
|
226
228
|
*/
|
|
229
|
+
const inFlightCreates = new Set();
|
|
230
|
+
|
|
227
231
|
async function createPrd(input, remote) {
|
|
232
|
+
const ctl = { key: null };
|
|
233
|
+
try {
|
|
234
|
+
return await createPrdInner(input, remote, ctl);
|
|
235
|
+
} finally {
|
|
236
|
+
if (ctl.key) inFlightCreates.delete(ctl.key);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
async function createPrdInner(input, remote, ctl, opts = {}) {
|
|
241
|
+
// opts.dryRun: run every check, allocate/write nothing, return the prepared
|
|
242
|
+
// entry (createPrdBatch's validate phase). opts.extraDepSlugs: bare slugs of
|
|
243
|
+
// sibling batch entries, accepted as dependsOn targets.
|
|
228
244
|
// cwd is untrusted AND, since this PRD, optional — a caller inside an
|
|
229
245
|
// Epic's git worktree naturally passes its worktree pwd (or omits cwd
|
|
230
246
|
// entirely and relies on originClaudeSessionId/sourcePromptId). Resolve the
|
|
@@ -249,7 +265,7 @@ async function createPrd(input, remote) {
|
|
|
249
265
|
|
|
250
266
|
if (!resolved.cwd) {
|
|
251
267
|
return {
|
|
252
|
-
ok: false,
|
|
268
|
+
ok: false, applied: false,
|
|
253
269
|
status: 400,
|
|
254
270
|
error: 'cwd is required — pass cwd explicitly, or originClaudeSessionId/sourcePromptId that resolves '
|
|
255
271
|
+ 'to an existing, already-approved Epic (create/approve an Epic first)',
|
|
@@ -264,7 +280,7 @@ async function createPrd(input, remote) {
|
|
|
264
280
|
try {
|
|
265
281
|
realCwd = config.validatePath(cwd);
|
|
266
282
|
} catch (e) {
|
|
267
|
-
return { ok: false, status: 400, error: `cwd rejected: ${e?.message ?? 'outside allowed roots'}` };
|
|
283
|
+
return { ok: false, applied: false, status: 400, error: `cwd rejected: ${e?.message ?? 'outside allowed roots'}` };
|
|
268
284
|
}
|
|
269
285
|
// Register this project's cwd as a write-allowed root (config.cjs's
|
|
270
286
|
// validateWrite gate). Chat-only Epics (headless claude -p, no Terminal
|
|
@@ -313,7 +329,7 @@ async function createPrd(input, remote) {
|
|
|
313
329
|
const nonInteractive = Boolean(process.env.SM_SCHEDULER_JOB_SLUG);
|
|
314
330
|
if (!nonInteractive) {
|
|
315
331
|
return {
|
|
316
|
-
ok: false,
|
|
332
|
+
ok: false, applied: false,
|
|
317
333
|
status: 400,
|
|
318
334
|
error: `Epic ${input.sourcePromptId} already has incomplete PRDs — pass disposition: "append" ` +
|
|
319
335
|
'(extend the existing chain behind its current tail) or "new-head" (an independent root, ' +
|
|
@@ -340,7 +356,7 @@ async function createPrd(input, remote) {
|
|
|
340
356
|
const nnMatch = SLUG_HAS_NN_PREFIX_RE.exec(input.slug);
|
|
341
357
|
if (nnMatch) {
|
|
342
358
|
return {
|
|
343
|
-
ok: false,
|
|
359
|
+
ok: false, applied: false,
|
|
344
360
|
status: 400,
|
|
345
361
|
error: `slug "${input.slug}" already starts with a numeric prefix "${nnMatch[1]}-" — the allocator ` +
|
|
346
362
|
`owns the NN- prefix, not the caller. Pass the bare name instead (e.g. "${input.slug.slice(nnMatch[0].length)}").`,
|
|
@@ -350,7 +366,7 @@ async function createPrd(input, remote) {
|
|
|
350
366
|
|
|
351
367
|
const slug = input.slug || deriveSlugFromTitle(input.title);
|
|
352
368
|
if (!slug || !PRD_CREATE_SLUG_RE.test(slug)) {
|
|
353
|
-
return { ok: false, status: 400, error: 'could not derive a valid kebab-case slug from title; supply "slug" explicitly' };
|
|
369
|
+
return { ok: false, applied: false, status: 400, error: 'could not derive a valid kebab-case slug from title; supply "slug" explicitly' };
|
|
354
370
|
}
|
|
355
371
|
|
|
356
372
|
// NN allocation is delegated to allocateParallelGroup() (PRD 548) via the
|
|
@@ -365,21 +381,21 @@ async function createPrd(input, remote) {
|
|
|
365
381
|
// checked BEFORE the NN allocation so a refusal burns no number.
|
|
366
382
|
const hasArtifactPaths = Array.isArray(input.artifactPaths) && input.artifactPaths.length > 0;
|
|
367
383
|
if (input.deliverable != null && input.deliverable !== 'artifact') {
|
|
368
|
-
return { ok: false, status: 400, error: 'deliverable must be exactly "artifact" when given' };
|
|
384
|
+
return { ok: false, applied: false, status: 400, error: 'deliverable must be exactly "artifact" when given' };
|
|
369
385
|
}
|
|
370
386
|
if (input.deliverable === 'artifact' && !hasArtifactPaths) {
|
|
371
|
-
return { ok: false, status: 400, error: 'deliverable: "artifact" requires a non-empty artifactPaths — name every git-excluded file this PRD produces' };
|
|
387
|
+
return { ok: false, applied: false, status: 400, error: 'deliverable: "artifact" requires a non-empty artifactPaths — name every git-excluded file this PRD produces' };
|
|
372
388
|
}
|
|
373
389
|
if (input.artifactPaths != null && input.deliverable !== 'artifact') {
|
|
374
|
-
return { ok: false, status: 400, error: 'artifactPaths requires deliverable: "artifact" — artifactPaths given without it' };
|
|
390
|
+
return { ok: false, applied: false, status: 400, error: 'artifactPaths requires deliverable: "artifact" — artifactPaths given without it' };
|
|
375
391
|
}
|
|
376
392
|
if (hasArtifactPaths) {
|
|
377
393
|
for (const ap of input.artifactPaths) {
|
|
378
394
|
if (typeof ap !== 'string' || !ap.trim() || /[\r\n,\[\]]/.test(ap)) {
|
|
379
|
-
return { ok: false, status: 400, error: `artifactPaths entry ${JSON.stringify(ap)} must be a non-empty single-line path without commas or brackets` };
|
|
395
|
+
return { ok: false, applied: false, status: 400, error: `artifactPaths entry ${JSON.stringify(ap)} must be a non-empty single-line path without commas or brackets` };
|
|
380
396
|
}
|
|
381
397
|
if (ap.includes('..')) {
|
|
382
|
-
return { ok: false, status: 400, error: `artifactPaths entry "${ap}" must not contain ".."` };
|
|
398
|
+
return { ok: false, applied: false, status: 400, error: `artifactPaths entry "${ap}" must not contain ".."` };
|
|
383
399
|
}
|
|
384
400
|
}
|
|
385
401
|
}
|
|
@@ -403,18 +419,18 @@ async function createPrd(input, remote) {
|
|
|
403
419
|
}
|
|
404
420
|
for (const [fieldLabel, text] of [['goal', input.goal], ['implementation notes', input.implementationNotes]]) {
|
|
405
421
|
const headingError = headingGuardError(fieldLabel, text);
|
|
406
|
-
if (headingError) return { ok: false, status: 400, error: headingError };
|
|
422
|
+
if (headingError) return { ok: false, applied: false, status: 400, error: headingError };
|
|
407
423
|
}
|
|
408
424
|
if (Array.isArray(input.acceptanceCriteria)) {
|
|
409
425
|
for (const entry of input.acceptanceCriteria) {
|
|
410
426
|
const headingError = headingGuardError('acceptance criteria', entry);
|
|
411
|
-
if (headingError) return { ok: false, status: 400, error: headingError };
|
|
427
|
+
if (headingError) return { ok: false, applied: false, status: 400, error: headingError };
|
|
412
428
|
}
|
|
413
429
|
}
|
|
414
430
|
if (Array.isArray(input.outOfScope)) {
|
|
415
431
|
for (const entry of input.outOfScope) {
|
|
416
432
|
const headingError = headingGuardError('out of scope', entry);
|
|
417
|
-
if (headingError) return { ok: false, status: 400, error: headingError };
|
|
433
|
+
if (headingError) return { ok: false, applied: false, status: 400, error: headingError };
|
|
418
434
|
}
|
|
419
435
|
}
|
|
420
436
|
|
|
@@ -427,7 +443,7 @@ async function createPrd(input, remote) {
|
|
|
427
443
|
if (input.gate !== undefined) {
|
|
428
444
|
const gateValidation = validateGate(input.gate);
|
|
429
445
|
if (!gateValidation.ok) {
|
|
430
|
-
return { ok: false, status: 400, error: gateValidation.error };
|
|
446
|
+
return { ok: false, applied: false, status: 400, error: gateValidation.error };
|
|
431
447
|
}
|
|
432
448
|
gateCommands = gateValidation.commands;
|
|
433
449
|
extraWarnings.push(...gateValidation.warnings);
|
|
@@ -438,7 +454,7 @@ async function createPrd(input, remote) {
|
|
|
438
454
|
if (input.files !== undefined) {
|
|
439
455
|
const filesValidation = validateFiles(input.files);
|
|
440
456
|
if (!filesValidation.ok) {
|
|
441
|
-
return { ok: false, status: 400, error: filesValidation.error };
|
|
457
|
+
return { ok: false, applied: false, status: 400, error: filesValidation.error };
|
|
442
458
|
}
|
|
443
459
|
filesList = filesValidation.files;
|
|
444
460
|
} else {
|
|
@@ -450,6 +466,18 @@ async function createPrd(input, remote) {
|
|
|
450
466
|
// Finished rows leave queue.json, so an author can re-issue a landed PRD under a fresh NN.
|
|
451
467
|
// Refused before NN allocation so it burns no number. Fix plans are exempt.
|
|
452
468
|
if (input.sourcePromptId && !isFixPlanSlug(`0-${slug}`)) {
|
|
469
|
+
const inFlightKey = `${input.sourcePromptId}\u0000${slug}`;
|
|
470
|
+
if (!opts.dryRun && inFlightCreates.has(inFlightKey)) {
|
|
471
|
+
return {
|
|
472
|
+
ok: false, applied: false,
|
|
473
|
+
status: 409,
|
|
474
|
+
error: `A create for slug "${slug}" in this Epic is still in progress — wait for it to finish, then retry the same call`,
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
if (!opts.dryRun) {
|
|
478
|
+
inFlightCreates.add(inFlightKey);
|
|
479
|
+
ctl.key = inFlightKey;
|
|
480
|
+
}
|
|
453
481
|
let epicDirs = [];
|
|
454
482
|
try {
|
|
455
483
|
const prdsDir = resolveEpicPrdWriteDir(cwd, input.sourcePromptId);
|
|
@@ -460,8 +488,31 @@ async function createPrd(input, remote) {
|
|
|
460
488
|
try { names = await fsp.readdir(dir); } catch { /* missing dir counts as empty */ }
|
|
461
489
|
const dup = names.find((n) => n.endsWith('.md') && n.slice(0, -3).replace(/^\d+-/, '') === slug);
|
|
462
490
|
if (dup) {
|
|
491
|
+
let existingTitle = null;
|
|
492
|
+
try {
|
|
493
|
+
existingTitle = splitFrontmatter(await fsp.readFile(path.join(dir, dup), 'utf8')).fm.title ?? null;
|
|
494
|
+
} catch { /* unreadable — treat as different */ }
|
|
495
|
+
if (existingTitle !== null && String(existingTitle).trim() === String(input.title ?? '').trim()) {
|
|
496
|
+
const existingSlug = dup.slice(0, -3);
|
|
497
|
+
let enqueuedExisting = false;
|
|
498
|
+
try {
|
|
499
|
+
if (typeof remote.getJob === 'function') enqueuedExisting = Boolean(await remote.getJob(existingSlug));
|
|
500
|
+
} catch { /* non-fatal */ }
|
|
501
|
+
return {
|
|
502
|
+
ok: true,
|
|
503
|
+
applied: true,
|
|
504
|
+
alreadyExisted: true,
|
|
505
|
+
nn: Number((/^(\d+)-/.exec(dup) || [])[1]),
|
|
506
|
+
filename: dup,
|
|
507
|
+
prdPath: path.join(dir, dup),
|
|
508
|
+
epicId: input.sourcePromptId,
|
|
509
|
+
enqueued: enqueuedExisting,
|
|
510
|
+
note: 'PRD was not written again: this Epic already has that file with the same title, which usually means an earlier call\'s response was lost, so treat it as created.',
|
|
511
|
+
warnings: [],
|
|
512
|
+
};
|
|
513
|
+
}
|
|
463
514
|
return {
|
|
464
|
-
ok: false,
|
|
515
|
+
ok: false, applied: false,
|
|
465
516
|
status: 409,
|
|
466
517
|
error: `A PRD with slug "${slug}" already exists in this Epic as "${dup}". Its work may already have landed. ` +
|
|
467
518
|
'Use scheduler_update_prd / scheduler_reset_job on it, or pick a distinct slug.',
|
|
@@ -470,7 +521,8 @@ async function createPrd(input, remote) {
|
|
|
470
521
|
}
|
|
471
522
|
}
|
|
472
523
|
|
|
473
|
-
|
|
524
|
+
// Dry run burns no number: a placeholder keeps the filename-shaped guards below meaningful.
|
|
525
|
+
const nn = opts.dryRun ? 9999999 : await remote.allocateParallelGroup(input.cwd);
|
|
474
526
|
const filenameSlug = `${nn}-${slug}`;
|
|
475
527
|
|
|
476
528
|
// Fix-chain depth guard (PRD 1113): refuse a fix-of-a-fix before it's ever
|
|
@@ -482,7 +534,7 @@ async function createPrd(input, remote) {
|
|
|
482
534
|
if (chainDepth > FIX_CHAIN_DEPTH_CAP) {
|
|
483
535
|
const base = baseSlugOf(filenameSlug);
|
|
484
536
|
return {
|
|
485
|
-
ok: false,
|
|
537
|
+
ok: false, applied: false,
|
|
486
538
|
status: 409,
|
|
487
539
|
error: `Fix-chain depth cap exceeded: "${base}" has already failed to close ${chainDepth} time(s) for ` +
|
|
488
540
|
`infrastructure reasons (refusing to write "${filenameSlug}.md" at depth ${chainDepth}). Authoring a ` +
|
|
@@ -505,7 +557,7 @@ async function createPrd(input, remote) {
|
|
|
505
557
|
// — but only when spawnInvestigation authors it, never through this API).
|
|
506
558
|
if (isFixPlanSlug(filenameSlug)) {
|
|
507
559
|
return {
|
|
508
|
-
ok: false,
|
|
560
|
+
ok: false, applied: false,
|
|
509
561
|
status: 400,
|
|
510
562
|
error: `slug "${filenameSlug}" collides with the reserved NN-fix-... fix-plan naming convention ` +
|
|
511
563
|
'(reserved for PRDs the scheduler\'s own investigation loop authors itself). Reword the title/slug ' +
|
|
@@ -519,9 +571,9 @@ async function createPrd(input, remote) {
|
|
|
519
571
|
// no existence guard (by design, it doubles as the edit-in-place
|
|
520
572
|
// path for Scheduler UI PRD edits), so "create" must not silently
|
|
521
573
|
// clobber an existing job's PRD.
|
|
522
|
-
const existing = await remote.readPrd(filenameSlug, input.cwd);
|
|
574
|
+
const existing = opts.dryRun ? null : await remote.readPrd(filenameSlug, input.cwd);
|
|
523
575
|
if (existing?.ok) {
|
|
524
|
-
return { ok: false, status: 409, error: `PRD already exists: ${filenameSlug}.md` };
|
|
576
|
+
return { ok: false, applied: false, status: 409, error: `PRD already exists: ${filenameSlug}.md` };
|
|
525
577
|
}
|
|
526
578
|
|
|
527
579
|
// Write-time FK check (throw on write, report on read — see
|
|
@@ -534,7 +586,7 @@ async function createPrd(input, remote) {
|
|
|
534
586
|
try {
|
|
535
587
|
await assertAgentTypeWritable(input.cwd, input.agentType);
|
|
536
588
|
} catch (e) {
|
|
537
|
-
return { ok: false, status: 400, error: e?.message ?? 'invalid agentType' };
|
|
589
|
+
return { ok: false, applied: false, status: 400, error: e?.message ?? 'invalid agentType' };
|
|
538
590
|
}
|
|
539
591
|
}
|
|
540
592
|
|
|
@@ -558,13 +610,13 @@ async function createPrd(input, remote) {
|
|
|
558
610
|
}
|
|
559
611
|
if (listing) {
|
|
560
612
|
depRows = listing.prds ?? [];
|
|
561
|
-
const candidateSlugs = depRows.map((p) => p.slug);
|
|
613
|
+
const candidateSlugs = [...depRows.map((p) => p.slug), ...(opts.extraDepSlugs ?? [])];
|
|
562
614
|
for (const dep of input.dependsOn) {
|
|
563
615
|
if (resolveDepSlug(dep, candidateSlugs).length > 0) continue;
|
|
564
616
|
const near = findNearMatches(dep, candidateSlugs);
|
|
565
617
|
const suggestion = near.length ? ` Closest existing slug(s): ${near.join(', ')}.` : '';
|
|
566
618
|
return {
|
|
567
|
-
ok: false,
|
|
619
|
+
ok: false, applied: false,
|
|
568
620
|
status: 400,
|
|
569
621
|
error: `dependsOn entry "${dep}" does not resolve to any existing PRD in this project.${suggestion} ` +
|
|
570
622
|
'Pass the bare name (preferred) or the exact NN-prefixed slug of an existing PRD.',
|
|
@@ -584,6 +636,7 @@ async function createPrd(input, remote) {
|
|
|
584
636
|
console.warn(`[prdCreate] dependsOn spans plans ${inherited.diverged.join(', ')} — inheriting ${planId} (lowest-numbered dependency)`);
|
|
585
637
|
}
|
|
586
638
|
}
|
|
639
|
+
const inheritedPlanId = planId;
|
|
587
640
|
if (!planId) planId = mintPlanId();
|
|
588
641
|
|
|
589
642
|
const body = buildPrdBody({ ...input, planId, gate: gateCommands, files: filesList });
|
|
@@ -603,7 +656,7 @@ async function createPrd(input, remote) {
|
|
|
603
656
|
&& JSON.stringify(resolved.sequence) === JSON.stringify(expectedSequence);
|
|
604
657
|
if (!matches) {
|
|
605
658
|
return {
|
|
606
|
-
ok: false,
|
|
659
|
+
ok: false, applied: false,
|
|
607
660
|
status: 400,
|
|
608
661
|
error: 'The PRD text already holds a gate fence (three backticks + gate), so the scheduler would read '
|
|
609
662
|
+ 'the wrong gate. Remove it from the goal, implementation notes, acceptance criteria, out of scope '
|
|
@@ -612,9 +665,16 @@ async function createPrd(input, remote) {
|
|
|
612
665
|
}
|
|
613
666
|
}
|
|
614
667
|
|
|
668
|
+
if (opts.dryRun) {
|
|
669
|
+
return {
|
|
670
|
+
ok: true, dryRun: true, slug, input, gateCommands, filesList,
|
|
671
|
+
inheritedPlanId, warnings: [...extraWarnings, ...sizingWarnings(input)],
|
|
672
|
+
};
|
|
673
|
+
}
|
|
674
|
+
|
|
615
675
|
const writeResult = await remote.writePrd(filenameSlug, body, input.cwd);
|
|
616
676
|
if (!writeResult?.ok) {
|
|
617
|
-
return { ok: false, status: 500, error: writeResult?.error ?? 'write failed' };
|
|
677
|
+
return { ok: false, applied: false, status: 500, error: writeResult?.error ?? 'write failed' };
|
|
618
678
|
}
|
|
619
679
|
|
|
620
680
|
// Trace-back point for "who/what created this PRD" (auditLog.cjs) — every
|
|
@@ -627,6 +687,8 @@ async function createPrd(input, remote) {
|
|
|
627
687
|
sourcePromptId: input.sourcePromptId ?? null,
|
|
628
688
|
sourceTabId: input.sourceTabId ?? null,
|
|
629
689
|
});
|
|
690
|
+
// Anonymous counter: enum-mapped agentType only (telemetryCounters.cjs).
|
|
691
|
+
telemetryCounters.trackPrdCreate({ agentType: input.agentType || DEFAULT_PRD_AGENT_TYPE });
|
|
630
692
|
|
|
631
693
|
// PRD 1446: writePrd() succeeding used to just promise "the next scheduler
|
|
632
694
|
// reconcile pass (~1 minute)" adopts this file — but nothing reliably ran
|
|
@@ -642,7 +704,12 @@ async function createPrd(input, remote) {
|
|
|
642
704
|
let enqueued = false;
|
|
643
705
|
if (typeof remote.requestReconcile === 'function') {
|
|
644
706
|
try {
|
|
645
|
-
|
|
707
|
+
let timer;
|
|
708
|
+
const reconcile = Promise.resolve().then(() => remote.requestReconcile());
|
|
709
|
+
// Late rejection (after the race is lost) must never be unhandled.
|
|
710
|
+
reconcile.catch((e) => console.warn(`[prdCreate] requestReconcile failed for ${filenameSlug} (non-fatal): ${e?.message ?? e}`));
|
|
711
|
+
const bound = new Promise((resolve) => { timer = setTimeout(resolve, 2000); });
|
|
712
|
+
try { await Promise.race([reconcile, bound]); } finally { clearTimeout(timer); }
|
|
646
713
|
if (typeof remote.getJob === 'function') {
|
|
647
714
|
enqueued = Boolean(await remote.getJob(filenameSlug));
|
|
648
715
|
}
|
|
@@ -662,6 +729,7 @@ async function createPrd(input, remote) {
|
|
|
662
729
|
// row before this function returned — it is not a fabricated status.
|
|
663
730
|
return {
|
|
664
731
|
ok: true,
|
|
732
|
+
applied: true,
|
|
665
733
|
nn,
|
|
666
734
|
filename: `${filenameSlug}.md`,
|
|
667
735
|
prdPath: writeResult.path ?? null,
|
|
@@ -684,6 +752,128 @@ async function createPrd(input, remote) {
|
|
|
684
752
|
};
|
|
685
753
|
}
|
|
686
754
|
|
|
755
|
+
const PRD_BATCH_MAX = 40;
|
|
756
|
+
|
|
757
|
+
/**
|
|
758
|
+
* Create several PRDs all-or-nothing. Every entry goes through createPrd's own
|
|
759
|
+
* validation (dry run: no NN, no file); `dependsOn` may also name other entries
|
|
760
|
+
* of the batch by bare slug, earlier or later. Only when every entry validates
|
|
761
|
+
* and the intra-batch graph is acyclic are NNs allocated and files written; a
|
|
762
|
+
* write failure unlinks what this call already wrote. Allocated NNs are never
|
|
763
|
+
* reissued (the allocator's high-water only moves up), so a rollback leaves a
|
|
764
|
+
* gap, not an inconsistency.
|
|
765
|
+
*/
|
|
766
|
+
async function createPrdBatch(inputs, remote, opts = {}) {
|
|
767
|
+
const fail = (errors) => ({ ok: false, applied: false, errors });
|
|
768
|
+
if (!Array.isArray(inputs) || inputs.length === 0) return fail([{ index: -1, slug: null, error: 'batch needs at least one entry' }]);
|
|
769
|
+
if (inputs.length > PRD_BATCH_MAX) return fail([{ index: -1, slug: null, error: `batch has ${inputs.length} entries; the maximum is ${PRD_BATCH_MAX}` }]);
|
|
770
|
+
|
|
771
|
+
const envelope = {};
|
|
772
|
+
for (const k of ['cwd', 'originClaudeSessionId', 'originProjectRoot', 'sourcePromptId']) {
|
|
773
|
+
if (opts[k] != null) envelope[k] = opts[k];
|
|
774
|
+
}
|
|
775
|
+
const entries = inputs.map((e) => ({ ...envelope, ...e }));
|
|
776
|
+
|
|
777
|
+
// Pass 1: slugs (cheap, so duplicates / cycles are reported before any I/O).
|
|
778
|
+
const slugs = entries.map((e) => e.slug || deriveSlugFromTitle(e.title));
|
|
779
|
+
const errors = [];
|
|
780
|
+
const firstIndex = new Map();
|
|
781
|
+
slugs.forEach((s, i) => {
|
|
782
|
+
if (!s) return;
|
|
783
|
+
if (firstIndex.has(s)) errors.push({ index: i, slug: s, error: `duplicate slug "${s}" in batch (also entry ${firstIndex.get(s)})` });
|
|
784
|
+
else firstIndex.set(s, i);
|
|
785
|
+
});
|
|
786
|
+
if (errors.length) return fail(errors);
|
|
787
|
+
const batchSlugs = slugs.filter(Boolean);
|
|
788
|
+
|
|
789
|
+
// Pass 2: per-entry validation through createPrd's own checks.
|
|
790
|
+
const prepared = [];
|
|
791
|
+
for (let i = 0; i < entries.length; i++) {
|
|
792
|
+
const r = await createPrdInner(entries[i], remote, { key: null }, { dryRun: true, extraDepSlugs: batchSlugs });
|
|
793
|
+
if (!r.ok) { errors.push({ index: i, slug: slugs[i] ?? null, error: r.error }); continue; }
|
|
794
|
+
if (r.alreadyExisted) { errors.push({ index: i, slug: slugs[i], error: `slug "${slugs[i]}" already exists in this Epic as "${r.filename}"` }); continue; }
|
|
795
|
+
prepared.push(r);
|
|
796
|
+
}
|
|
797
|
+
if (errors.length) return fail(errors);
|
|
798
|
+
const sameTarget = prepared.every((p) => p.input.cwd === prepared[0].input.cwd && (p.input.sourcePromptId ?? null) === (prepared[0].input.sourcePromptId ?? null));
|
|
799
|
+
if (!sameTarget) return fail([{ index: -1, slug: null, error: 'all batch entries must resolve to the same project and Epic' }]);
|
|
800
|
+
|
|
801
|
+
// Cycle detection: DFS over intra-batch dependsOn edges (bare-slug match).
|
|
802
|
+
const edges = prepared.map((p) => (p.input.dependsOn ?? []).filter((d) => firstIndex.has(d)).map((d) => firstIndex.get(d)));
|
|
803
|
+
const state = new Array(prepared.length).fill(0);
|
|
804
|
+
let cycle = null;
|
|
805
|
+
const visit = (n, trail) => {
|
|
806
|
+
if (cycle) return;
|
|
807
|
+
state[n] = 1;
|
|
808
|
+
for (const m of edges[n]) {
|
|
809
|
+
if (state[m] === 1) { cycle = [...trail.slice(trail.indexOf(m)), n, m].map((x) => slugs[x]); return; }
|
|
810
|
+
if (state[m] === 0) visit(m, [...trail, n]);
|
|
811
|
+
if (cycle) return;
|
|
812
|
+
}
|
|
813
|
+
state[n] = 2;
|
|
814
|
+
};
|
|
815
|
+
for (let i = 0; i < prepared.length && !cycle; i++) if (state[i] === 0) visit(i, []);
|
|
816
|
+
if (cycle) {
|
|
817
|
+
const at = firstIndex.get(cycle[0]);
|
|
818
|
+
return fail([{ index: at, slug: cycle[0], error: `dependsOn cycle inside batch: ${cycle.join(' -> ')}` }]);
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
// One planId for the whole batch unless an entry inherits one from existing deps.
|
|
822
|
+
const planId = prepared.map((p) => p.inheritedPlanId).find(Boolean) || mintPlanId();
|
|
823
|
+
|
|
824
|
+
// Write phase: allocate + write in input order; unlink on any failure.
|
|
825
|
+
const written = [];
|
|
826
|
+
const results = [];
|
|
827
|
+
const rollback = async () => {
|
|
828
|
+
for (const w of written) {
|
|
829
|
+
try {
|
|
830
|
+
if (w.path) await fsp.unlink(w.path);
|
|
831
|
+
else if (typeof remote.deletePrd === 'function') await remote.deletePrd(w.filenameSlug, w.cwd);
|
|
832
|
+
} catch (e) { console.warn(`[prdCreate] batch rollback could not remove ${w.filenameSlug}: ${e?.message ?? e}`); }
|
|
833
|
+
}
|
|
834
|
+
};
|
|
835
|
+
for (let i = 0; i < prepared.length; i++) {
|
|
836
|
+
const p = prepared[i];
|
|
837
|
+
let nn; let filenameSlug;
|
|
838
|
+
try {
|
|
839
|
+
nn = await remote.allocateParallelGroup(p.input.cwd);
|
|
840
|
+
filenameSlug = `${nn}-${p.slug}`;
|
|
841
|
+
const body = buildPrdBody({ ...p.input, planId, gate: p.gateCommands, files: p.filesList });
|
|
842
|
+
const w = await remote.writePrd(filenameSlug, body, p.input.cwd);
|
|
843
|
+
if (!w?.ok) throw new Error(w?.error ?? 'write failed');
|
|
844
|
+
written.push({ filenameSlug, path: w.path ?? null, cwd: p.input.cwd });
|
|
845
|
+
results.push({ slug: p.slug, nn, filename: `${filenameSlug}.md`, warnings: p.warnings });
|
|
846
|
+
} catch (e) {
|
|
847
|
+
await rollback();
|
|
848
|
+
return fail([{ index: i, slug: p.slug, error: `write failed (batch rolled back): ${e?.message ?? e}` }]);
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
for (let i = 0; i < prepared.length; i++) {
|
|
853
|
+
const p = prepared[i];
|
|
854
|
+
appendAuditEvent('prd_create', {
|
|
855
|
+
cwd: p.input.cwd, slug: `${results[i].nn}-${p.slug}`, title: p.input.title,
|
|
856
|
+
sourcePromptId: p.input.sourcePromptId ?? null, sourceTabId: p.input.sourceTabId ?? null, batch: true,
|
|
857
|
+
});
|
|
858
|
+
telemetryCounters.trackPrdCreate({ agentType: p.input.agentType || DEFAULT_PRD_AGENT_TYPE });
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
// One bounded reconcile after the last write (best-effort, as in createPrd).
|
|
862
|
+
if (typeof remote.requestReconcile === 'function') {
|
|
863
|
+
let timer;
|
|
864
|
+
try {
|
|
865
|
+
const reconcile = Promise.resolve().then(() => remote.requestReconcile());
|
|
866
|
+
reconcile.catch((e) => console.warn(`[prdCreate] batch requestReconcile failed (non-fatal): ${e?.message ?? e}`));
|
|
867
|
+
const bound = new Promise((resolve) => { timer = setTimeout(resolve, 2000); });
|
|
868
|
+
await Promise.race([reconcile, bound]);
|
|
869
|
+
} catch (e) {
|
|
870
|
+
console.warn(`[prdCreate] batch requestReconcile failed (non-fatal): ${e?.message ?? e}`);
|
|
871
|
+
} finally { clearTimeout(timer); }
|
|
872
|
+
}
|
|
873
|
+
|
|
874
|
+
return { ok: true, applied: true, prds: results };
|
|
875
|
+
}
|
|
876
|
+
|
|
687
877
|
/**
|
|
688
878
|
* Registers the create-prd admin HTTP route (PRD 689 — moved verbatim out of
|
|
689
879
|
* the former standalone admin HTTP server module's handleRequest, no behavior
|
|
@@ -720,6 +910,7 @@ function registerAdminRoute(adminHttp, remote) {
|
|
|
720
910
|
// Mirror createPrd()'s own honest shape verbatim — never re-add a
|
|
721
911
|
// 'status' field here. See createPrd's return above for why.
|
|
722
912
|
sendJson(res, 200, {
|
|
913
|
+
...(result.alreadyExisted ? { alreadyExisted: true } : {}),
|
|
723
914
|
nn: result.nn,
|
|
724
915
|
filename: result.filename,
|
|
725
916
|
prdPath: result.prdPath ?? null,
|
|
@@ -731,6 +922,31 @@ function registerAdminRoute(adminHttp, remote) {
|
|
|
731
922
|
warnings: result.warnings,
|
|
732
923
|
});
|
|
733
924
|
});
|
|
925
|
+
|
|
926
|
+
adminHttp.registerRoute('POST', '/admin/scheduler/create-prds', async (req, res) => {
|
|
927
|
+
const raw = await readBody(req);
|
|
928
|
+
let parsed;
|
|
929
|
+
try {
|
|
930
|
+
parsed = raw ? JSON.parse(raw) : {};
|
|
931
|
+
} catch {
|
|
932
|
+
sendJson(res, 400, { ok: false, applied: false, error: 'invalid JSON body' });
|
|
933
|
+
return;
|
|
934
|
+
}
|
|
935
|
+
let input;
|
|
936
|
+
try {
|
|
937
|
+
input = schemas.adminCreatePrds.parse(parsed);
|
|
938
|
+
} catch (e) {
|
|
939
|
+
sendJson(res, 400, { ok: false, applied: false, error: 'invalid PRD batch payload', details: e?.issues ?? e?.message });
|
|
940
|
+
return;
|
|
941
|
+
}
|
|
942
|
+
const { prds, ...envelope } = input;
|
|
943
|
+
const result = await createPrdBatch(prds, remote, envelope);
|
|
944
|
+
if (!result.ok) {
|
|
945
|
+
sendJson(res, 400, { ok: false, applied: false, errors: result.errors });
|
|
946
|
+
return;
|
|
947
|
+
}
|
|
948
|
+
sendJson(res, 200, { ok: true, applied: true, prds: result.prds });
|
|
949
|
+
});
|
|
734
950
|
}
|
|
735
951
|
|
|
736
952
|
module.exports = {
|
|
@@ -741,5 +957,7 @@ module.exports = {
|
|
|
741
957
|
buildPrdBody,
|
|
742
958
|
resolveSourcePromptIdFromClaudeSession,
|
|
743
959
|
createPrd,
|
|
960
|
+
createPrdBatch,
|
|
961
|
+
PRD_BATCH_MAX,
|
|
744
962
|
registerAdminRoute,
|
|
745
963
|
};
|