claude-code-session-manager 0.122.1 → 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-C2qUX9WU.js → AgentLibrary-DxWN3FSN.js} +2 -2
- package/dist/assets/{Button-ssBzvFha.js → Button-CISLFSKb.js} +1 -1
- package/dist/assets/DataModel-CYPbhGrx.js +1 -0
- package/dist/assets/{History-BRdRp1J_.js → History-Fhouektw.js} +2 -2
- package/dist/assets/{Hooks--hiGPBQy.js → Hooks-COfIcyYV.js} +3 -3
- package/dist/assets/{Library-0XYvkhh2.js → Library-Bt5WOyyu.js} +7 -7
- package/dist/assets/ListDetail-DH7uzYd5.js +1 -0
- package/dist/assets/{McpServers-CCjHSwFm.js → McpServers-CwyVEzMN.js} +2 -2
- package/dist/assets/{Memory-C7RXoVO4.js → Memory-k9TWT9N4.js} +8 -8
- package/dist/assets/{Panel-C92O_aAH.js → Panel-Di8JqCYN.js} +1 -1
- package/dist/assets/{Permissions-DuTJ1FUb.js → Permissions-FFs6Rq1H.js} +3 -3
- package/dist/assets/{Plugins-DSXsP4hM.js → Plugins-uK5vl_RE.js} +2 -2
- package/dist/assets/{ProvenanceBadge-CWdDkU1m.js → ProvenanceBadge-CGDyGDoN.js} +1 -1
- package/dist/assets/{SaveBar-ob2dJJjH.js → SaveBar-Cnwc_XfF.js} +1 -1
- package/dist/assets/{Scheduler-Du4HgPKY.js → Scheduler-aACa6_mC.js} +7 -7
- package/dist/assets/{ScopeSwitcher-Dv_6M_Ci.js → ScopeSwitcher-CoG3CSlC.js} +1 -1
- package/dist/assets/Settings-BG0RM6Di.js +4 -0
- package/dist/assets/{SkillReferenceGraph-Dx1Qs8Y0.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-Cihe5er3.js → TiptapBody-CCHsa8Jv.js} +1 -1
- package/dist/assets/{Toggle-CpYVC8bF.js → Toggle-BUOj4pws.js} +1 -1
- package/dist/assets/{ViewTabs-C3FN6P_C.js → ViewTabs-BdpsL7yr.js} +1 -1
- package/dist/assets/{historyTotals-C4Rs37Cd.js → historyTotals-Dx6ca881.js} +1 -1
- package/dist/assets/{index-Dd7UQabV.css → index-BnKaYERU.css} +1 -1
- package/dist/assets/{index-S-F5jp_h.js → index-CkNGj8FA.js} +151 -151
- package/dist/assets/{settingsSchema-BZcdzHTQ.js → settingsSchema-BZhkpr9e.js} +1 -1
- package/dist/assets/{skillFrontmatter-EfNERt4b.js → skillFrontmatter-N13KGqbs.js} +1 -1
- package/dist/assets/{useAgentsDirWatch-BfbrFvB3.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 -25
- package/scripts/scheduler-mcp-server.cjs +124 -74
- 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 +31 -2
- 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 +205 -32
- 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 +124 -29
- 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-CENoTvc2.js +0 -1
- package/dist/assets/ListDetail-CkKXAM59.js +0 -1
- package/dist/assets/RemoteMachines-BFmbIGpW.js +0 -3
- package/dist/assets/Settings-DMoI0cqB.js +0 -4
- package/dist/assets/Skills-DYWQTQB1.js +0 -2
- package/dist/assets/SystemPrompt-Df0IfOuI.js +0 -1
- package/dist/assets/TagLibrary-BuMsGx-2.js +0 -1
- package/dist/assets/useRemotePeers-CKVakQPc.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
|
@@ -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 };
|
|
@@ -34,6 +34,7 @@ const { isFixPlanSlug } = require('../scheduler/fixPlanSlug.cjs');
|
|
|
34
34
|
const { isIncomplete, resolveChainTerminals, isValidPlanId, mintPlanId, resolveInheritedPlanId } = require('./prdDisposition.cjs');
|
|
35
35
|
const { splitFrontmatter } = require('./prdFrontmatter.cjs');
|
|
36
36
|
const { sizingWarnings } = require('./prdSizing.cjs');
|
|
37
|
+
const telemetryCounters = require('../telemetry/telemetryCounters.cjs');
|
|
37
38
|
const { execGit } = require('../git/gitExec.cjs');
|
|
38
39
|
const { extractRepoPathCandidates, worktreeReachWarnings } = require('./prdWorktreeReach.cjs');
|
|
39
40
|
const { validateGate, validateFiles, renderFilesSection, renderGateSection } = require('./prdGateFiles.cjs');
|
|
@@ -236,7 +237,10 @@ async function createPrd(input, remote) {
|
|
|
236
237
|
}
|
|
237
238
|
}
|
|
238
239
|
|
|
239
|
-
async function createPrdInner(input, remote, ctl) {
|
|
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.
|
|
240
244
|
// cwd is untrusted AND, since this PRD, optional — a caller inside an
|
|
241
245
|
// Epic's git worktree naturally passes its worktree pwd (or omits cwd
|
|
242
246
|
// entirely and relies on originClaudeSessionId/sourcePromptId). Resolve the
|
|
@@ -261,7 +265,7 @@ async function createPrdInner(input, remote, ctl) {
|
|
|
261
265
|
|
|
262
266
|
if (!resolved.cwd) {
|
|
263
267
|
return {
|
|
264
|
-
ok: false,
|
|
268
|
+
ok: false, applied: false,
|
|
265
269
|
status: 400,
|
|
266
270
|
error: 'cwd is required — pass cwd explicitly, or originClaudeSessionId/sourcePromptId that resolves '
|
|
267
271
|
+ 'to an existing, already-approved Epic (create/approve an Epic first)',
|
|
@@ -276,7 +280,7 @@ async function createPrdInner(input, remote, ctl) {
|
|
|
276
280
|
try {
|
|
277
281
|
realCwd = config.validatePath(cwd);
|
|
278
282
|
} catch (e) {
|
|
279
|
-
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'}` };
|
|
280
284
|
}
|
|
281
285
|
// Register this project's cwd as a write-allowed root (config.cjs's
|
|
282
286
|
// validateWrite gate). Chat-only Epics (headless claude -p, no Terminal
|
|
@@ -325,7 +329,7 @@ async function createPrdInner(input, remote, ctl) {
|
|
|
325
329
|
const nonInteractive = Boolean(process.env.SM_SCHEDULER_JOB_SLUG);
|
|
326
330
|
if (!nonInteractive) {
|
|
327
331
|
return {
|
|
328
|
-
ok: false,
|
|
332
|
+
ok: false, applied: false,
|
|
329
333
|
status: 400,
|
|
330
334
|
error: `Epic ${input.sourcePromptId} already has incomplete PRDs — pass disposition: "append" ` +
|
|
331
335
|
'(extend the existing chain behind its current tail) or "new-head" (an independent root, ' +
|
|
@@ -352,7 +356,7 @@ async function createPrdInner(input, remote, ctl) {
|
|
|
352
356
|
const nnMatch = SLUG_HAS_NN_PREFIX_RE.exec(input.slug);
|
|
353
357
|
if (nnMatch) {
|
|
354
358
|
return {
|
|
355
|
-
ok: false,
|
|
359
|
+
ok: false, applied: false,
|
|
356
360
|
status: 400,
|
|
357
361
|
error: `slug "${input.slug}" already starts with a numeric prefix "${nnMatch[1]}-" — the allocator ` +
|
|
358
362
|
`owns the NN- prefix, not the caller. Pass the bare name instead (e.g. "${input.slug.slice(nnMatch[0].length)}").`,
|
|
@@ -362,7 +366,7 @@ async function createPrdInner(input, remote, ctl) {
|
|
|
362
366
|
|
|
363
367
|
const slug = input.slug || deriveSlugFromTitle(input.title);
|
|
364
368
|
if (!slug || !PRD_CREATE_SLUG_RE.test(slug)) {
|
|
365
|
-
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' };
|
|
366
370
|
}
|
|
367
371
|
|
|
368
372
|
// NN allocation is delegated to allocateParallelGroup() (PRD 548) via the
|
|
@@ -377,21 +381,21 @@ async function createPrdInner(input, remote, ctl) {
|
|
|
377
381
|
// checked BEFORE the NN allocation so a refusal burns no number.
|
|
378
382
|
const hasArtifactPaths = Array.isArray(input.artifactPaths) && input.artifactPaths.length > 0;
|
|
379
383
|
if (input.deliverable != null && input.deliverable !== 'artifact') {
|
|
380
|
-
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' };
|
|
381
385
|
}
|
|
382
386
|
if (input.deliverable === 'artifact' && !hasArtifactPaths) {
|
|
383
|
-
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' };
|
|
384
388
|
}
|
|
385
389
|
if (input.artifactPaths != null && input.deliverable !== 'artifact') {
|
|
386
|
-
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' };
|
|
387
391
|
}
|
|
388
392
|
if (hasArtifactPaths) {
|
|
389
393
|
for (const ap of input.artifactPaths) {
|
|
390
394
|
if (typeof ap !== 'string' || !ap.trim() || /[\r\n,\[\]]/.test(ap)) {
|
|
391
|
-
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` };
|
|
392
396
|
}
|
|
393
397
|
if (ap.includes('..')) {
|
|
394
|
-
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 ".."` };
|
|
395
399
|
}
|
|
396
400
|
}
|
|
397
401
|
}
|
|
@@ -415,18 +419,18 @@ async function createPrdInner(input, remote, ctl) {
|
|
|
415
419
|
}
|
|
416
420
|
for (const [fieldLabel, text] of [['goal', input.goal], ['implementation notes', input.implementationNotes]]) {
|
|
417
421
|
const headingError = headingGuardError(fieldLabel, text);
|
|
418
|
-
if (headingError) return { ok: false, status: 400, error: headingError };
|
|
422
|
+
if (headingError) return { ok: false, applied: false, status: 400, error: headingError };
|
|
419
423
|
}
|
|
420
424
|
if (Array.isArray(input.acceptanceCriteria)) {
|
|
421
425
|
for (const entry of input.acceptanceCriteria) {
|
|
422
426
|
const headingError = headingGuardError('acceptance criteria', entry);
|
|
423
|
-
if (headingError) return { ok: false, status: 400, error: headingError };
|
|
427
|
+
if (headingError) return { ok: false, applied: false, status: 400, error: headingError };
|
|
424
428
|
}
|
|
425
429
|
}
|
|
426
430
|
if (Array.isArray(input.outOfScope)) {
|
|
427
431
|
for (const entry of input.outOfScope) {
|
|
428
432
|
const headingError = headingGuardError('out of scope', entry);
|
|
429
|
-
if (headingError) return { ok: false, status: 400, error: headingError };
|
|
433
|
+
if (headingError) return { ok: false, applied: false, status: 400, error: headingError };
|
|
430
434
|
}
|
|
431
435
|
}
|
|
432
436
|
|
|
@@ -439,7 +443,7 @@ async function createPrdInner(input, remote, ctl) {
|
|
|
439
443
|
if (input.gate !== undefined) {
|
|
440
444
|
const gateValidation = validateGate(input.gate);
|
|
441
445
|
if (!gateValidation.ok) {
|
|
442
|
-
return { ok: false, status: 400, error: gateValidation.error };
|
|
446
|
+
return { ok: false, applied: false, status: 400, error: gateValidation.error };
|
|
443
447
|
}
|
|
444
448
|
gateCommands = gateValidation.commands;
|
|
445
449
|
extraWarnings.push(...gateValidation.warnings);
|
|
@@ -450,7 +454,7 @@ async function createPrdInner(input, remote, ctl) {
|
|
|
450
454
|
if (input.files !== undefined) {
|
|
451
455
|
const filesValidation = validateFiles(input.files);
|
|
452
456
|
if (!filesValidation.ok) {
|
|
453
|
-
return { ok: false, status: 400, error: filesValidation.error };
|
|
457
|
+
return { ok: false, applied: false, status: 400, error: filesValidation.error };
|
|
454
458
|
}
|
|
455
459
|
filesList = filesValidation.files;
|
|
456
460
|
} else {
|
|
@@ -463,15 +467,17 @@ async function createPrdInner(input, remote, ctl) {
|
|
|
463
467
|
// Refused before NN allocation so it burns no number. Fix plans are exempt.
|
|
464
468
|
if (input.sourcePromptId && !isFixPlanSlug(`0-${slug}`)) {
|
|
465
469
|
const inFlightKey = `${input.sourcePromptId}\u0000${slug}`;
|
|
466
|
-
if (inFlightCreates.has(inFlightKey)) {
|
|
470
|
+
if (!opts.dryRun && inFlightCreates.has(inFlightKey)) {
|
|
467
471
|
return {
|
|
468
|
-
ok: false,
|
|
472
|
+
ok: false, applied: false,
|
|
469
473
|
status: 409,
|
|
470
474
|
error: `A create for slug "${slug}" in this Epic is still in progress — wait for it to finish, then retry the same call`,
|
|
471
475
|
};
|
|
472
476
|
}
|
|
473
|
-
|
|
474
|
-
|
|
477
|
+
if (!opts.dryRun) {
|
|
478
|
+
inFlightCreates.add(inFlightKey);
|
|
479
|
+
ctl.key = inFlightKey;
|
|
480
|
+
}
|
|
475
481
|
let epicDirs = [];
|
|
476
482
|
try {
|
|
477
483
|
const prdsDir = resolveEpicPrdWriteDir(cwd, input.sourcePromptId);
|
|
@@ -494,6 +500,7 @@ async function createPrdInner(input, remote, ctl) {
|
|
|
494
500
|
} catch { /* non-fatal */ }
|
|
495
501
|
return {
|
|
496
502
|
ok: true,
|
|
503
|
+
applied: true,
|
|
497
504
|
alreadyExisted: true,
|
|
498
505
|
nn: Number((/^(\d+)-/.exec(dup) || [])[1]),
|
|
499
506
|
filename: dup,
|
|
@@ -505,7 +512,7 @@ async function createPrdInner(input, remote, ctl) {
|
|
|
505
512
|
};
|
|
506
513
|
}
|
|
507
514
|
return {
|
|
508
|
-
ok: false,
|
|
515
|
+
ok: false, applied: false,
|
|
509
516
|
status: 409,
|
|
510
517
|
error: `A PRD with slug "${slug}" already exists in this Epic as "${dup}". Its work may already have landed. ` +
|
|
511
518
|
'Use scheduler_update_prd / scheduler_reset_job on it, or pick a distinct slug.',
|
|
@@ -514,7 +521,8 @@ async function createPrdInner(input, remote, ctl) {
|
|
|
514
521
|
}
|
|
515
522
|
}
|
|
516
523
|
|
|
517
|
-
|
|
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);
|
|
518
526
|
const filenameSlug = `${nn}-${slug}`;
|
|
519
527
|
|
|
520
528
|
// Fix-chain depth guard (PRD 1113): refuse a fix-of-a-fix before it's ever
|
|
@@ -526,7 +534,7 @@ async function createPrdInner(input, remote, ctl) {
|
|
|
526
534
|
if (chainDepth > FIX_CHAIN_DEPTH_CAP) {
|
|
527
535
|
const base = baseSlugOf(filenameSlug);
|
|
528
536
|
return {
|
|
529
|
-
ok: false,
|
|
537
|
+
ok: false, applied: false,
|
|
530
538
|
status: 409,
|
|
531
539
|
error: `Fix-chain depth cap exceeded: "${base}" has already failed to close ${chainDepth} time(s) for ` +
|
|
532
540
|
`infrastructure reasons (refusing to write "${filenameSlug}.md" at depth ${chainDepth}). Authoring a ` +
|
|
@@ -549,7 +557,7 @@ async function createPrdInner(input, remote, ctl) {
|
|
|
549
557
|
// — but only when spawnInvestigation authors it, never through this API).
|
|
550
558
|
if (isFixPlanSlug(filenameSlug)) {
|
|
551
559
|
return {
|
|
552
|
-
ok: false,
|
|
560
|
+
ok: false, applied: false,
|
|
553
561
|
status: 400,
|
|
554
562
|
error: `slug "${filenameSlug}" collides with the reserved NN-fix-... fix-plan naming convention ` +
|
|
555
563
|
'(reserved for PRDs the scheduler\'s own investigation loop authors itself). Reword the title/slug ' +
|
|
@@ -563,9 +571,9 @@ async function createPrdInner(input, remote, ctl) {
|
|
|
563
571
|
// no existence guard (by design, it doubles as the edit-in-place
|
|
564
572
|
// path for Scheduler UI PRD edits), so "create" must not silently
|
|
565
573
|
// clobber an existing job's PRD.
|
|
566
|
-
const existing = await remote.readPrd(filenameSlug, input.cwd);
|
|
574
|
+
const existing = opts.dryRun ? null : await remote.readPrd(filenameSlug, input.cwd);
|
|
567
575
|
if (existing?.ok) {
|
|
568
|
-
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` };
|
|
569
577
|
}
|
|
570
578
|
|
|
571
579
|
// Write-time FK check (throw on write, report on read — see
|
|
@@ -578,7 +586,7 @@ async function createPrdInner(input, remote, ctl) {
|
|
|
578
586
|
try {
|
|
579
587
|
await assertAgentTypeWritable(input.cwd, input.agentType);
|
|
580
588
|
} catch (e) {
|
|
581
|
-
return { ok: false, status: 400, error: e?.message ?? 'invalid agentType' };
|
|
589
|
+
return { ok: false, applied: false, status: 400, error: e?.message ?? 'invalid agentType' };
|
|
582
590
|
}
|
|
583
591
|
}
|
|
584
592
|
|
|
@@ -602,13 +610,13 @@ async function createPrdInner(input, remote, ctl) {
|
|
|
602
610
|
}
|
|
603
611
|
if (listing) {
|
|
604
612
|
depRows = listing.prds ?? [];
|
|
605
|
-
const candidateSlugs = depRows.map((p) => p.slug);
|
|
613
|
+
const candidateSlugs = [...depRows.map((p) => p.slug), ...(opts.extraDepSlugs ?? [])];
|
|
606
614
|
for (const dep of input.dependsOn) {
|
|
607
615
|
if (resolveDepSlug(dep, candidateSlugs).length > 0) continue;
|
|
608
616
|
const near = findNearMatches(dep, candidateSlugs);
|
|
609
617
|
const suggestion = near.length ? ` Closest existing slug(s): ${near.join(', ')}.` : '';
|
|
610
618
|
return {
|
|
611
|
-
ok: false,
|
|
619
|
+
ok: false, applied: false,
|
|
612
620
|
status: 400,
|
|
613
621
|
error: `dependsOn entry "${dep}" does not resolve to any existing PRD in this project.${suggestion} ` +
|
|
614
622
|
'Pass the bare name (preferred) or the exact NN-prefixed slug of an existing PRD.',
|
|
@@ -628,6 +636,7 @@ async function createPrdInner(input, remote, ctl) {
|
|
|
628
636
|
console.warn(`[prdCreate] dependsOn spans plans ${inherited.diverged.join(', ')} — inheriting ${planId} (lowest-numbered dependency)`);
|
|
629
637
|
}
|
|
630
638
|
}
|
|
639
|
+
const inheritedPlanId = planId;
|
|
631
640
|
if (!planId) planId = mintPlanId();
|
|
632
641
|
|
|
633
642
|
const body = buildPrdBody({ ...input, planId, gate: gateCommands, files: filesList });
|
|
@@ -647,7 +656,7 @@ async function createPrdInner(input, remote, ctl) {
|
|
|
647
656
|
&& JSON.stringify(resolved.sequence) === JSON.stringify(expectedSequence);
|
|
648
657
|
if (!matches) {
|
|
649
658
|
return {
|
|
650
|
-
ok: false,
|
|
659
|
+
ok: false, applied: false,
|
|
651
660
|
status: 400,
|
|
652
661
|
error: 'The PRD text already holds a gate fence (three backticks + gate), so the scheduler would read '
|
|
653
662
|
+ 'the wrong gate. Remove it from the goal, implementation notes, acceptance criteria, out of scope '
|
|
@@ -656,9 +665,16 @@ async function createPrdInner(input, remote, ctl) {
|
|
|
656
665
|
}
|
|
657
666
|
}
|
|
658
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
|
+
|
|
659
675
|
const writeResult = await remote.writePrd(filenameSlug, body, input.cwd);
|
|
660
676
|
if (!writeResult?.ok) {
|
|
661
|
-
return { ok: false, status: 500, error: writeResult?.error ?? 'write failed' };
|
|
677
|
+
return { ok: false, applied: false, status: 500, error: writeResult?.error ?? 'write failed' };
|
|
662
678
|
}
|
|
663
679
|
|
|
664
680
|
// Trace-back point for "who/what created this PRD" (auditLog.cjs) — every
|
|
@@ -671,6 +687,8 @@ async function createPrdInner(input, remote, ctl) {
|
|
|
671
687
|
sourcePromptId: input.sourcePromptId ?? null,
|
|
672
688
|
sourceTabId: input.sourceTabId ?? null,
|
|
673
689
|
});
|
|
690
|
+
// Anonymous counter: enum-mapped agentType only (telemetryCounters.cjs).
|
|
691
|
+
telemetryCounters.trackPrdCreate({ agentType: input.agentType || DEFAULT_PRD_AGENT_TYPE });
|
|
674
692
|
|
|
675
693
|
// PRD 1446: writePrd() succeeding used to just promise "the next scheduler
|
|
676
694
|
// reconcile pass (~1 minute)" adopts this file — but nothing reliably ran
|
|
@@ -686,7 +704,12 @@ async function createPrdInner(input, remote, ctl) {
|
|
|
686
704
|
let enqueued = false;
|
|
687
705
|
if (typeof remote.requestReconcile === 'function') {
|
|
688
706
|
try {
|
|
689
|
-
|
|
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); }
|
|
690
713
|
if (typeof remote.getJob === 'function') {
|
|
691
714
|
enqueued = Boolean(await remote.getJob(filenameSlug));
|
|
692
715
|
}
|
|
@@ -706,6 +729,7 @@ async function createPrdInner(input, remote, ctl) {
|
|
|
706
729
|
// row before this function returned — it is not a fabricated status.
|
|
707
730
|
return {
|
|
708
731
|
ok: true,
|
|
732
|
+
applied: true,
|
|
709
733
|
nn,
|
|
710
734
|
filename: `${filenameSlug}.md`,
|
|
711
735
|
prdPath: writeResult.path ?? null,
|
|
@@ -728,6 +752,128 @@ async function createPrdInner(input, remote, ctl) {
|
|
|
728
752
|
};
|
|
729
753
|
}
|
|
730
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
|
+
|
|
731
877
|
/**
|
|
732
878
|
* Registers the create-prd admin HTTP route (PRD 689 — moved verbatim out of
|
|
733
879
|
* the former standalone admin HTTP server module's handleRequest, no behavior
|
|
@@ -776,6 +922,31 @@ function registerAdminRoute(adminHttp, remote) {
|
|
|
776
922
|
warnings: result.warnings,
|
|
777
923
|
});
|
|
778
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
|
+
});
|
|
779
950
|
}
|
|
780
951
|
|
|
781
952
|
module.exports = {
|
|
@@ -786,5 +957,7 @@ module.exports = {
|
|
|
786
957
|
buildPrdBody,
|
|
787
958
|
resolveSourcePromptIdFromClaudeSession,
|
|
788
959
|
createPrd,
|
|
960
|
+
createPrdBatch,
|
|
961
|
+
PRD_BATCH_MAX,
|
|
789
962
|
registerAdminRoute,
|
|
790
963
|
};
|
|
@@ -244,6 +244,36 @@ function renderFilesSection(files) {
|
|
|
244
244
|
];
|
|
245
245
|
}
|
|
246
246
|
|
|
247
|
+
/**
|
|
248
|
+
* Inverse of renderFilesSection: read the bullet entries under the `# Files`
|
|
249
|
+
* heading (up to the next top-level heading) back out of a PRD body. Returns
|
|
250
|
+
* [] when there is no such section. Strips only the renderer's `- ` prefix
|
|
251
|
+
* (plus tolerated wrapping backticks); prose lines are ignored.
|
|
252
|
+
*
|
|
253
|
+
* @param {string} body
|
|
254
|
+
* @returns {string[]}
|
|
255
|
+
*/
|
|
256
|
+
function parseFilesSection(body) {
|
|
257
|
+
if (typeof body !== 'string') return [];
|
|
258
|
+
const lines = body.split(/\r?\n/);
|
|
259
|
+
const start = lines.findIndex((l) => /^#\s+Files\s*$/.test(l));
|
|
260
|
+
if (start === -1) return [];
|
|
261
|
+
const out = [];
|
|
262
|
+
let inFence = false;
|
|
263
|
+
for (let i = start + 1; i < lines.length; i++) {
|
|
264
|
+
const line = lines[i];
|
|
265
|
+
if (/^\s*```/.test(line)) { inFence = !inFence; continue; }
|
|
266
|
+
if (!inFence && /^#\s/.test(line)) break;
|
|
267
|
+
const m = line.match(/^[-*]\s+(.+?)\s*$/);
|
|
268
|
+
if (!m) continue;
|
|
269
|
+
let entry = m[1];
|
|
270
|
+
const tick = entry.match(/^`([^`]+)`$/);
|
|
271
|
+
if (tick) entry = tick[1];
|
|
272
|
+
if (entry && !out.includes(entry)) out.push(entry);
|
|
273
|
+
}
|
|
274
|
+
return out;
|
|
275
|
+
}
|
|
276
|
+
|
|
247
277
|
/**
|
|
248
278
|
* Render the `# Gate` body section as an array of lines. `commandsOrNone`
|
|
249
279
|
* must already be validated (validateGate's `commands` output) — either
|
|
@@ -280,5 +310,6 @@ module.exports = {
|
|
|
280
310
|
validateGate,
|
|
281
311
|
validateFiles,
|
|
282
312
|
renderFilesSection,
|
|
313
|
+
parseFilesSection,
|
|
283
314
|
renderGateSection,
|
|
284
315
|
};
|