@loucompanion/forge-bridge 0.1.1-dev.cada6731ecc7 → 0.1.1-dev.d71662695396
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/session/phase-completion.d.ts +12 -1
- package/dist/session/phase-completion.js +482 -22
- package/dist/session/provider-cli/provider-cli.service.d.ts +7 -0
- package/dist/session/provider-cli/provider-cli.service.js +296 -24
- package/dist/session/session.types.d.ts +4 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/worktree/artifact-injector.js +39 -0
- package/package.json +1 -1
|
@@ -22,6 +22,33 @@ export const qaCompletionGateOrder = [
|
|
|
22
22
|
"qa_findings",
|
|
23
23
|
"qa_verdict"
|
|
24
24
|
];
|
|
25
|
+
export const draftCompletionGateOrder = [
|
|
26
|
+
"draft_intent",
|
|
27
|
+
"draft_repo_context",
|
|
28
|
+
"draft_graph_context",
|
|
29
|
+
"draft_product_scope",
|
|
30
|
+
"draft_work_order_plan",
|
|
31
|
+
"draft_critic",
|
|
32
|
+
"draft_proposal"
|
|
33
|
+
];
|
|
34
|
+
const draftGateFileNames = {
|
|
35
|
+
draft_intent: "draft.intent.json",
|
|
36
|
+
draft_repo_context: "draft.repo-context.json",
|
|
37
|
+
draft_graph_context: "draft.graph-context.json",
|
|
38
|
+
draft_product_scope: "draft.product-scope.json",
|
|
39
|
+
draft_work_order_plan: "draft.work-order-plan.json",
|
|
40
|
+
draft_critic: "draft.critic.json",
|
|
41
|
+
draft_proposal: "draft.proposal.json"
|
|
42
|
+
};
|
|
43
|
+
const draftGateToolNames = {
|
|
44
|
+
draft_intent: "complete_draft_intent",
|
|
45
|
+
draft_repo_context: "complete_draft_repo_context",
|
|
46
|
+
draft_graph_context: "complete_draft_graph_context",
|
|
47
|
+
draft_product_scope: "complete_draft_product_scope",
|
|
48
|
+
draft_work_order_plan: "complete_draft_work_order_plan",
|
|
49
|
+
draft_critic: "complete_draft_critic",
|
|
50
|
+
draft_proposal: "complete_draft_proposal"
|
|
51
|
+
};
|
|
25
52
|
const qaGateFileNames = {
|
|
26
53
|
qa_plan: "qa.plan.json",
|
|
27
54
|
qa_regression: "qa.regression.json",
|
|
@@ -95,6 +122,10 @@ const phaseFiles = {
|
|
|
95
122
|
architect: {
|
|
96
123
|
jsonFileName: "architect.verdict.json",
|
|
97
124
|
reportFileName: "architect.report.md"
|
|
125
|
+
},
|
|
126
|
+
draft: {
|
|
127
|
+
jsonFileName: "draft.proposal.json",
|
|
128
|
+
reportFileName: "draft.report.md"
|
|
98
129
|
}
|
|
99
130
|
};
|
|
100
131
|
export function completionPhaseFromDispatch(dispatch) {
|
|
@@ -118,6 +149,9 @@ export function normalizeCompletionPhase(value) {
|
|
|
118
149
|
if (normalized === "architect" || normalized === "architect_plan" || normalized === "planning" || normalized === "plan") {
|
|
119
150
|
return "architect";
|
|
120
151
|
}
|
|
152
|
+
if (normalized === "draft" || normalized === "drafting" || normalized === "draft_work_order") {
|
|
153
|
+
return "draft";
|
|
154
|
+
}
|
|
121
155
|
return "implementation";
|
|
122
156
|
}
|
|
123
157
|
export function usesDevelopCycleCompletion(dispatch) {
|
|
@@ -154,6 +188,9 @@ export function usesArchitectCycleCompletion(dispatch) {
|
|
|
154
188
|
return kind === "architect" || role === "architect" || lane === "architect";
|
|
155
189
|
}
|
|
156
190
|
export function completionToolNamesForPhase(phase) {
|
|
191
|
+
if (phase === "draft") {
|
|
192
|
+
return completionToolNamesForDraftGate("draft_proposal");
|
|
193
|
+
}
|
|
157
194
|
if (isDebugCompletionPhase(phase)) {
|
|
158
195
|
return completionToolNamesForDebugGate(phase);
|
|
159
196
|
}
|
|
@@ -176,6 +213,10 @@ export function completionToolNamesForDebugGate(gate) {
|
|
|
176
213
|
const base = debugGateToolNames[gate];
|
|
177
214
|
return [`forge.${base}`, base];
|
|
178
215
|
}
|
|
216
|
+
export function completionToolNamesForDraftGate(gate) {
|
|
217
|
+
const base = draftGateToolNames[gate];
|
|
218
|
+
return [`forge.${base}`, base];
|
|
219
|
+
}
|
|
179
220
|
export function expectedCompletionToolName(phase) {
|
|
180
221
|
return completionToolNamesForPhase(phase)[0];
|
|
181
222
|
}
|
|
@@ -185,6 +226,9 @@ export function expectedQaCompletionToolName(gate) {
|
|
|
185
226
|
export function expectedDebugCompletionToolName(gate) {
|
|
186
227
|
return completionToolNamesForDebugGate(gate)[0];
|
|
187
228
|
}
|
|
229
|
+
export function expectedDraftCompletionToolName(gate) {
|
|
230
|
+
return completionToolNamesForDraftGate(gate)[0];
|
|
231
|
+
}
|
|
188
232
|
export function artifactRootFor(worktreePath, workItemId) {
|
|
189
233
|
return path.join(worktreePath, ".forge", "work-items", safePathSegment(workItemId, "work item id"), "artifacts");
|
|
190
234
|
}
|
|
@@ -219,7 +263,8 @@ export async function prepareCompletionToolRuntime(options) {
|
|
|
219
263
|
developCycle: usesDevelopCycleCompletion(options.dispatch),
|
|
220
264
|
qaCycle: usesQaCycleCompletion(options.dispatch),
|
|
221
265
|
debugCycle: usesDebugCycleCompletion(options.dispatch),
|
|
222
|
-
architectCycle: usesArchitectCycleCompletion(options.dispatch)
|
|
266
|
+
architectCycle: usesArchitectCycleCompletion(options.dispatch),
|
|
267
|
+
draftCycle: options.dispatch.execution.lane.trim().toLowerCase() === "draft" || options.dispatch.cycleRun.kind.trim().toLowerCase() === "draft"
|
|
223
268
|
});
|
|
224
269
|
const runtime = {
|
|
225
270
|
serverName: toolServerName,
|
|
@@ -261,6 +306,9 @@ export function buildCompletionToolServerArgs(config) {
|
|
|
261
306
|
if (config.architectCycle) {
|
|
262
307
|
args.push("--architect-cycle");
|
|
263
308
|
}
|
|
309
|
+
if (config.draftCycle) {
|
|
310
|
+
args.push("--draft-cycle");
|
|
311
|
+
}
|
|
264
312
|
return args;
|
|
265
313
|
}
|
|
266
314
|
export function codexMcpConfigArgs(runtime) {
|
|
@@ -300,6 +348,7 @@ Do not write Architect artifacts by hand. When the plan is complete, call \`${to
|
|
|
300
348
|
That tool writes \`.forge/work-items/{workItemId}/artifacts/implementation.plan.json\`, \`qa.plan.json\`, \`${artifactName}\`, and \`architect.report.md\`.
|
|
301
349
|
The payload must put \`phases\`, \`tasks\`, \`dependencies\`, and \`phase_gates\` as top-level fields inside \`implementation_plan\`, for example:
|
|
302
350
|
\`{ "status": "pass", "summary": "...", "implementation_plan": { "schema_version": 1, "plan_id": "plan-...", "phases": [{ "id": "phase-1", "tasks": ["task-1"], "gate": "gate-1" }], "tasks": [{ "id": "task-1", "tests": [{ "command": "npm test" }] }], "dependencies": [], "phase_gates": [{ "id": "gate-1", "phase": "phase-1" }] }, "qa_plan": { "schema_version": 1, "final_qa": [{ "id": "qa-1", "coverage": "..." }] }, "report_markdown": "# Architect Report" }\`.
|
|
351
|
+
\`implementation_plan.tasks\` are Develop child tasks only: each task must require a focused repo/product implementation change. Put parent-story QA, sign-off, child-count/materialization inventory, parent plan artifact checks, and final integrated acceptance proof in \`qa_plan.final_qa\`, not as child Develop tasks.
|
|
303
352
|
Dependency entries use canonical \`before\` and \`after\` task ids, for example \`{ "before": "task-1", "after": "task-2" }\`. Forge accepts \`from\`/\`to\` only as aliases and normalizes them before writing artifacts.
|
|
304
353
|
Use \`status: "pass"\` for a valid pending-approval plan, \`needs_revision\` when story/design input must change, or \`blocked\` with a concrete blocker.
|
|
305
354
|
Forge will mirror only the tool-written Architect artifacts; reporting the plan in prose is not enough.`;
|
|
@@ -334,6 +383,19 @@ Use \`status: "skipped"\` with a reason for gates that are not applicable. If a
|
|
|
334
383
|
The final verdict tool writes the canonical QA bundle under \`.forge/work-items/{workItemId}/artifacts/\`: \`qa.plan.json\`, \`qa.evidence.json\`, \`qa.findings.json\`, \`qa.verdict.json\`, and \`qa.report.md\`.
|
|
335
384
|
The legacy \`forge.complete_qa\` one-shot tool is not available in normal QA-cycle mode. Reporting QA completion in prose is not enough.`;
|
|
336
385
|
}
|
|
386
|
+
export function completionPromptForDraftCycle(currentGate = "draft_intent") {
|
|
387
|
+
return `Forge completion tools are available through the MCP server named \`${toolServerName}\`.
|
|
388
|
+
Do not finish Draft by prose or raw JSON. The tool server enforces Draft gate order and starts at \`${currentGate}\`.
|
|
389
|
+
Complete the Draft cycle in order inside this same provider session:
|
|
390
|
+
1. Call \`forge.complete_draft_intent(...)\` after interpreting user intent and non-goals.
|
|
391
|
+
2. Follow the returned repo-context instruction, inspect selected project/repo assumptions, then call \`forge.complete_draft_repo_context(...)\`.
|
|
392
|
+
3. Follow the returned graph-context instruction, account for DAG/dependency placement, then call \`forge.complete_draft_graph_context(...)\`.
|
|
393
|
+
4. Follow the returned product-scope instruction, choose product scope for the selected project, then call \`forge.complete_draft_product_scope(...)\`.
|
|
394
|
+
5. Follow the returned work-order-plan instruction, draft Work Order fields and candidate feature boundaries, then call \`forge.complete_draft_work_order_plan(...)\`.
|
|
395
|
+
6. Follow the returned critic instruction, critique literalism, wrong-project scope, and decomposition quality, then call \`forge.complete_draft_critic(...)\`.
|
|
396
|
+
7. Follow the returned proposal instruction, then call \`forge.complete_draft_proposal(...)\` with the final Work Order JSON in \`draft_json\`.
|
|
397
|
+
The final proposal tool writes \`draft.proposal.json\` under \`.forge/work-items/{workItemId}/artifacts/\`. Reporting Draft completion in prose is not enough.`;
|
|
398
|
+
}
|
|
337
399
|
export function completionPromptForDebugCycle(currentGate = "debug_repro") {
|
|
338
400
|
return `Forge completion tools are available through the MCP server named \`${toolServerName}\`.
|
|
339
401
|
Do not write Debug artifacts by hand. The tool server enforces Debug gate order and starts this run at \`${currentGate}\`.
|
|
@@ -346,33 +408,38 @@ Each tool writes a canonical Debug artifact under \`.forge/work-items/{workItemI
|
|
|
346
408
|
Use a non-pass status with explicit blockers when the current Debug gate cannot pass. Reporting Debug completion in prose is not enough; the relevant MCP completion tool is the transition request.`;
|
|
347
409
|
}
|
|
348
410
|
export function completionToolNamesForServer(config) {
|
|
349
|
-
return config.
|
|
350
|
-
?
|
|
351
|
-
: config.
|
|
352
|
-
?
|
|
353
|
-
: config.
|
|
354
|
-
?
|
|
355
|
-
: config.
|
|
356
|
-
? completionToolNamesForPhase(
|
|
357
|
-
:
|
|
411
|
+
return config.draftCycle
|
|
412
|
+
? draftCompletionGateOrder.flatMap((gate) => completionToolNamesForDraftGate(gate))
|
|
413
|
+
: config.debugCycle
|
|
414
|
+
? debugCompletionPhaseOrder.flatMap((gate) => completionToolNamesForDebugGate(gate))
|
|
415
|
+
: config.qaCycle
|
|
416
|
+
? qaCompletionGateOrder.flatMap((gate) => completionToolNamesForQaGate(gate))
|
|
417
|
+
: config.developCycle
|
|
418
|
+
? completionPhaseOrder.flatMap((phase) => completionToolNamesForPhase(phase))
|
|
419
|
+
: config.architectCycle
|
|
420
|
+
? completionToolNamesForPhase("architect")
|
|
421
|
+
: completionToolNamesForPhase(config.phase);
|
|
358
422
|
}
|
|
359
423
|
export class CompletionToolServerSession {
|
|
360
424
|
config;
|
|
361
425
|
currentPhase;
|
|
362
426
|
currentQaGate = "qa_plan";
|
|
427
|
+
currentDraftGate = "draft_intent";
|
|
363
428
|
completed = false;
|
|
364
429
|
constructor(config) {
|
|
365
430
|
this.config = config;
|
|
366
431
|
this.currentPhase = config.phase;
|
|
367
432
|
}
|
|
368
433
|
get instructions() {
|
|
369
|
-
return this.config.
|
|
370
|
-
?
|
|
371
|
-
: this.config.
|
|
372
|
-
?
|
|
373
|
-
: this.config.
|
|
374
|
-
?
|
|
375
|
-
:
|
|
434
|
+
return this.config.draftCycle
|
|
435
|
+
? completionPromptForDraftCycle(this.currentDraftGate)
|
|
436
|
+
: this.config.debugCycle
|
|
437
|
+
? completionPromptForDebugCycle(asDebugCompletionPhase(this.currentPhase))
|
|
438
|
+
: this.config.qaCycle
|
|
439
|
+
? completionPromptForQaCycle(this.currentQaGate)
|
|
440
|
+
: this.config.developCycle
|
|
441
|
+
? completionPromptForDevelopCycle(this.currentPhase)
|
|
442
|
+
: completionPromptForPhase(this.currentPhase);
|
|
376
443
|
}
|
|
377
444
|
toolDefinitions() {
|
|
378
445
|
return completionToolNamesForServer(this.config).map((name) => toolDefinition(name, phaseFromToolName(name), qaGateFromToolName(name)));
|
|
@@ -382,6 +449,9 @@ export class CompletionToolServerSession {
|
|
|
382
449
|
if (this.completed) {
|
|
383
450
|
throw new Error("Forge completion cycle is already complete; no more completion calls are accepted.");
|
|
384
451
|
}
|
|
452
|
+
if (this.config.draftCycle) {
|
|
453
|
+
return await this.callDraftGateTool(toolName, requestedPhase, input);
|
|
454
|
+
}
|
|
385
455
|
if (this.config.debugCycle) {
|
|
386
456
|
return await this.callDebugGateTool(toolName, requestedPhase, input);
|
|
387
457
|
}
|
|
@@ -422,6 +492,30 @@ export class CompletionToolServerSession {
|
|
|
422
492
|
}
|
|
423
493
|
};
|
|
424
494
|
}
|
|
495
|
+
async callDraftGateTool(toolName, requestedPhase, input) {
|
|
496
|
+
const requestedGate = draftGateFromToolName(toolName);
|
|
497
|
+
if (requestedPhase !== "draft" || !requestedGate) {
|
|
498
|
+
throw new Error(`Tool ${toolName} is not available in Draft-cycle mode. Current Forge Draft gate is ${this.currentDraftGate}.`);
|
|
499
|
+
}
|
|
500
|
+
if (requestedGate !== this.currentDraftGate) {
|
|
501
|
+
throw new Error(`Tool ${toolName} is out of order. Current Forge Draft gate is ${this.currentDraftGate}.`);
|
|
502
|
+
}
|
|
503
|
+
const completedGate = this.currentDraftGate;
|
|
504
|
+
const reference = await writeDraftGateCompletionArtifact(this.config, completedGate, toolName, input);
|
|
505
|
+
const status = reference.status.trim().toLowerCase();
|
|
506
|
+
const nextGate = this.advanceAfterDraftGate(completedGate);
|
|
507
|
+
const nextPhase = nextGate ? "draft" : "sign_off_ready";
|
|
508
|
+
const nextInstruction = completionToolNextInstructionForDraftGate(completedGate, status, nextGate);
|
|
509
|
+
return {
|
|
510
|
+
content: [{ type: "text", text: `Forge recorded ${completedGate} artifact ${path.relative(reference.repoPath, reference.path)} (${reference.status}, ${reference.contentHash}). ${nextInstruction}` }],
|
|
511
|
+
structuredContent: {
|
|
512
|
+
...reference,
|
|
513
|
+
gate: { phase: "draft", draftGate: completedGate, status, allowed: status !== "blocked", reason: nextGate ? `${completedGate} artifact accepted; continue to ${nextGate}.` : `Draft proposal artifact accepted with status ${status}.` },
|
|
514
|
+
nextPhase,
|
|
515
|
+
nextInstruction
|
|
516
|
+
}
|
|
517
|
+
};
|
|
518
|
+
}
|
|
425
519
|
async callDebugGateTool(toolName, requestedPhase, input) {
|
|
426
520
|
const currentDebugGate = asDebugCompletionPhase(this.currentPhase);
|
|
427
521
|
if (!isDebugCompletionPhase(requestedPhase)) {
|
|
@@ -518,6 +612,16 @@ export class CompletionToolServerSession {
|
|
|
518
612
|
this.completed = true;
|
|
519
613
|
return "dev_test_ready";
|
|
520
614
|
}
|
|
615
|
+
advanceAfterDraftGate(gate) {
|
|
616
|
+
const index = draftCompletionGateOrder.indexOf(gate);
|
|
617
|
+
const next = index >= 0 ? draftCompletionGateOrder[index + 1] ?? null : null;
|
|
618
|
+
if (next) {
|
|
619
|
+
this.currentDraftGate = next;
|
|
620
|
+
return next;
|
|
621
|
+
}
|
|
622
|
+
this.completed = true;
|
|
623
|
+
return null;
|
|
624
|
+
}
|
|
521
625
|
advanceAfterQaGate(gate) {
|
|
522
626
|
const index = qaCompletionGateOrder.indexOf(gate);
|
|
523
627
|
const next = index >= 0 ? qaCompletionGateOrder[index + 1] ?? null : null;
|
|
@@ -539,6 +643,43 @@ export class CompletionToolServerSession {
|
|
|
539
643
|
return null;
|
|
540
644
|
}
|
|
541
645
|
}
|
|
646
|
+
async function writeDraftGateCompletionArtifact(config, gate, toolName, input) {
|
|
647
|
+
const payload = asRecord(input, "Draft completion payload");
|
|
648
|
+
const status = normalizeDraftStatus(optionalString(payload, "status") ?? "pass");
|
|
649
|
+
if (gate === "draft_proposal") {
|
|
650
|
+
const draftJson = asRecord(payload["draft_json"], "draft_json");
|
|
651
|
+
for (const key of ["title", "summary", "objective", "planningPacket", "decompositionContext"]) {
|
|
652
|
+
if (typeof draftJson[key] !== "string" || draftJson[key].trim().length === 0) {
|
|
653
|
+
throw new Error(`draft_json.${key}: expected non-empty string`);
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
}
|
|
657
|
+
const redactedPayload = redactSecrets({ ...payload, status, phase: "draft", gate, tool_name: toolName }, config.redactionSecrets ?? []);
|
|
658
|
+
const root = artifactRootFor(config.worktreePath, config.workItemId);
|
|
659
|
+
await fs.mkdir(root, { recursive: true });
|
|
660
|
+
const artifactPath = path.join(root, draftGateFileNames[gate]);
|
|
661
|
+
const reportPath = gate === "draft_proposal" ? path.join(root, phaseFiles.draft.reportFileName) : path.join(root, `${gate}.md`);
|
|
662
|
+
const verdictJson = jsonText(redactedPayload);
|
|
663
|
+
const report = optionalString(redactedPayload, "report_markdown") ?? `# ${gate.replace(/_/g, " ")}\n\n${optionalString(redactedPayload, "summary") ?? "Draft gate recorded."}\n`;
|
|
664
|
+
await writeFileAtomic(artifactPath, verdictJson);
|
|
665
|
+
await writeFileAtomic(reportPath, report);
|
|
666
|
+
return {
|
|
667
|
+
phase: "draft",
|
|
668
|
+
workItemId: config.workItemId,
|
|
669
|
+
ownerType: config.ownerType,
|
|
670
|
+
ownerId: config.ownerId,
|
|
671
|
+
repoPath: path.resolve(config.worktreePath),
|
|
672
|
+
path: artifactPath,
|
|
673
|
+
reportPath,
|
|
674
|
+
contentHash: sha256(verdictJson),
|
|
675
|
+
status,
|
|
676
|
+
producedBy: config.producedBy ?? "forge-completion-tool"
|
|
677
|
+
};
|
|
678
|
+
}
|
|
679
|
+
function normalizeDraftStatus(value) {
|
|
680
|
+
const normalized = value.trim().toLowerCase();
|
|
681
|
+
return normalized === "blocked" || normalized === "needs_revision" || normalized === "needs_rework" ? normalized : "pass";
|
|
682
|
+
}
|
|
542
683
|
export async function writeCompletionArtifact(config, toolName, input) {
|
|
543
684
|
const phase = phaseFromToolName(toolName);
|
|
544
685
|
if (phase !== config.phase) {
|
|
@@ -550,7 +691,10 @@ export async function writeCompletionArtifact(config, toolName, input) {
|
|
|
550
691
|
}
|
|
551
692
|
const payload = asRecord(input, "completion payload");
|
|
552
693
|
const status = normalizeCompletionStatus(requiredString(payload, "status"), phase);
|
|
553
|
-
|
|
694
|
+
let validationPayload = { ...payload, status };
|
|
695
|
+
if (config.qaCycle && qaGate === "qa_verdict") {
|
|
696
|
+
validationPayload = await synthesizeQaCycleVerdictPayload(config, validationPayload, status);
|
|
697
|
+
}
|
|
554
698
|
validatePayloadShape(phase, validationPayload);
|
|
555
699
|
validateOptionalWorkItemId(validationPayload, config.workItemId);
|
|
556
700
|
const redactedPayload = redactSecrets(validationPayload, config.redactionSecrets ?? []);
|
|
@@ -793,6 +937,133 @@ async function writeQaCompletionArtifact(config, payload, status, producedBy) {
|
|
|
793
937
|
producedBy
|
|
794
938
|
};
|
|
795
939
|
}
|
|
940
|
+
async function synthesizeQaCycleVerdictPayload(config, payload, status) {
|
|
941
|
+
const qaPlan = await readQaGateArtifact(config, "qa_plan");
|
|
942
|
+
const qaRegression = await readQaGateArtifact(config, "qa_regression");
|
|
943
|
+
const qaApi = await readQaGateArtifact(config, "qa_api");
|
|
944
|
+
const qaManualWeb = await readQaGateArtifact(config, "qa_manual_web");
|
|
945
|
+
const qaBugRepro = await readQaGateArtifact(config, "qa_bug_repro");
|
|
946
|
+
const qaFindings = await readQaGateArtifact(config, "qa_findings");
|
|
947
|
+
const evidenceGates = [qaRegression, qaApi, qaManualWeb, qaBugRepro];
|
|
948
|
+
const payloadEvidence = qaEvidenceSource(payload);
|
|
949
|
+
const merged = { ...payload };
|
|
950
|
+
const checklist = qaStringArray(payload, "checklist");
|
|
951
|
+
if (checklist.length === 0) {
|
|
952
|
+
const plannedChecklist = qaStringArray(qaPlan, "checklist");
|
|
953
|
+
merged.checklist = plannedChecklist.length > 0 ? plannedChecklist : defaultQaChecklist(status);
|
|
954
|
+
}
|
|
955
|
+
const testedAcceptanceCriteria = qaStringArray(payload, "tested_acceptance_criteria");
|
|
956
|
+
if (testedAcceptanceCriteria.length === 0) {
|
|
957
|
+
const plannedAcceptanceCriteria = qaStringArray(qaPlan, "acceptance_criteria");
|
|
958
|
+
const payloadAcceptanceCriteria = qaStringArray(payload, "acceptance_criteria");
|
|
959
|
+
const acceptanceCriteria = plannedAcceptanceCriteria.length > 0 ? plannedAcceptanceCriteria : payloadAcceptanceCriteria;
|
|
960
|
+
if (acceptanceCriteria.length > 0) {
|
|
961
|
+
merged.tested_acceptance_criteria = acceptanceCriteria;
|
|
962
|
+
}
|
|
963
|
+
}
|
|
964
|
+
const payloadCommands = qaArray(payload, "commands", payloadEvidence);
|
|
965
|
+
const inheritedCommands = evidenceGates.flatMap((gate) => qaArray(gate, "commands"));
|
|
966
|
+
const inheritedDiagnosticCommandNotes = status === "pass"
|
|
967
|
+
? qaNonPassingCommandNotes(inheritedCommands)
|
|
968
|
+
: [];
|
|
969
|
+
merged.commands = dedupeUnknownArray([
|
|
970
|
+
...payloadCommands,
|
|
971
|
+
...(status === "pass" ? inheritedCommands.filter(isQaPassingCommandEvidence) : inheritedCommands)
|
|
972
|
+
]);
|
|
973
|
+
merged.browser_checks = dedupeUnknownArray([
|
|
974
|
+
...qaArray(payload, "browser_checks", payloadEvidence),
|
|
975
|
+
...evidenceGates.flatMap((gate) => qaArray(gate, "browser_checks"))
|
|
976
|
+
]);
|
|
977
|
+
merged.sidecars = dedupeUnknownArray([
|
|
978
|
+
...qaArray(payload, "sidecars", payloadEvidence),
|
|
979
|
+
...evidenceGates.flatMap((gate) => qaArray(gate, "sidecars"))
|
|
980
|
+
]);
|
|
981
|
+
const notes = dedupeStrings([
|
|
982
|
+
optionalString(payload, "notes"),
|
|
983
|
+
optionalString(payloadEvidence, "notes"),
|
|
984
|
+
...evidenceGates.map((gate) => optionalString(gate, "notes")),
|
|
985
|
+
...inheritedDiagnosticCommandNotes
|
|
986
|
+
]);
|
|
987
|
+
if (notes.length > 0) {
|
|
988
|
+
merged.notes = notes.join("\n");
|
|
989
|
+
}
|
|
990
|
+
merged.findings = mergeQaObjectArraysById(qaObjectArray(payload, "findings"), qaObjectArray(qaFindings, "findings"));
|
|
991
|
+
merged.candidate_bugs = mergeQaObjectArraysById(qaObjectArray(payload, "candidate_bugs"), qaObjectArray(qaFindings, "candidate_bugs"));
|
|
992
|
+
return merged;
|
|
993
|
+
}
|
|
994
|
+
async function readQaGateArtifact(config, gate) {
|
|
995
|
+
const ownerFields = artifactOwnerFields(config);
|
|
996
|
+
const artifactRoot = ownerFields.owner_type && ownerFields.owner_id
|
|
997
|
+
? artifactRootForOwner(config.worktreePath, ownerFields.owner_type, ownerFields.owner_id)
|
|
998
|
+
: artifactRootFor(config.worktreePath, config.workItemId);
|
|
999
|
+
const artifactPath = path.join(artifactRoot, qaGateFileNames[gate]);
|
|
1000
|
+
const content = await readOptionalFile(artifactPath);
|
|
1001
|
+
return content === null ? {} : parseOptionalJsonObject(content, qaGateFileNames[gate]);
|
|
1002
|
+
}
|
|
1003
|
+
function dedupeUnknownArray(values) {
|
|
1004
|
+
const seen = new Set();
|
|
1005
|
+
const deduped = [];
|
|
1006
|
+
for (const value of values) {
|
|
1007
|
+
const key = stableStringify(value);
|
|
1008
|
+
if (seen.has(key)) {
|
|
1009
|
+
continue;
|
|
1010
|
+
}
|
|
1011
|
+
seen.add(key);
|
|
1012
|
+
deduped.push(value);
|
|
1013
|
+
}
|
|
1014
|
+
return deduped;
|
|
1015
|
+
}
|
|
1016
|
+
function isQaPassingCommandEvidence(value) {
|
|
1017
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
1018
|
+
return false;
|
|
1019
|
+
}
|
|
1020
|
+
const record = value;
|
|
1021
|
+
return record.exit_code === 0 && normalizedString(record.status) === "pass";
|
|
1022
|
+
}
|
|
1023
|
+
function qaNonPassingCommandNotes(values) {
|
|
1024
|
+
return values
|
|
1025
|
+
.filter((value) => !isQaPassingCommandEvidence(value))
|
|
1026
|
+
.map((value, index) => {
|
|
1027
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) {
|
|
1028
|
+
return `QA diagnostic command ${index + 1} was omitted from pass evidence because it was not a structured passing command.`;
|
|
1029
|
+
}
|
|
1030
|
+
const record = value;
|
|
1031
|
+
const command = optionalString(record, "command") ?? "<missing command>";
|
|
1032
|
+
const status = optionalString(record, "status") ?? "<missing status>";
|
|
1033
|
+
const exitCode = typeof record.exit_code === "number" && Number.isFinite(record.exit_code)
|
|
1034
|
+
? String(record.exit_code)
|
|
1035
|
+
: "<missing exit_code>";
|
|
1036
|
+
const summary = optionalString(record, "summary");
|
|
1037
|
+
return `QA diagnostic command omitted from pass evidence: ${command} (status ${status}, exit_code ${exitCode})${summary ? ` — ${summary}` : ""}.`;
|
|
1038
|
+
});
|
|
1039
|
+
}
|
|
1040
|
+
function mergeQaObjectArraysById(primary, fallback) {
|
|
1041
|
+
const seen = new Set();
|
|
1042
|
+
const merged = [];
|
|
1043
|
+
for (const value of [...primary, ...fallback]) {
|
|
1044
|
+
const id = optionalString(value, "id");
|
|
1045
|
+
const key = id ? `id:${id}` : stableStringify(value);
|
|
1046
|
+
if (seen.has(key)) {
|
|
1047
|
+
continue;
|
|
1048
|
+
}
|
|
1049
|
+
seen.add(key);
|
|
1050
|
+
merged.push(value);
|
|
1051
|
+
}
|
|
1052
|
+
return merged;
|
|
1053
|
+
}
|
|
1054
|
+
function dedupeStrings(values) {
|
|
1055
|
+
const seen = new Set();
|
|
1056
|
+
const deduped = [];
|
|
1057
|
+
for (const value of values) {
|
|
1058
|
+
const trimmed = value?.trim();
|
|
1059
|
+
if (!trimmed || seen.has(trimmed)) {
|
|
1060
|
+
continue;
|
|
1061
|
+
}
|
|
1062
|
+
seen.add(trimmed);
|
|
1063
|
+
deduped.push(trimmed);
|
|
1064
|
+
}
|
|
1065
|
+
return deduped;
|
|
1066
|
+
}
|
|
796
1067
|
async function validateQaSidecarReferences(worktreePath, workItemId, sidecars, ownerType, ownerId) {
|
|
797
1068
|
if (sidecars.length === 0) {
|
|
798
1069
|
return [];
|
|
@@ -855,10 +1126,19 @@ export async function readCompletionArtifact(worktreePath, workItemId, phase, ow
|
|
|
855
1126
|
}
|
|
856
1127
|
const parsed = parseJsonObject(verdictJson, artifactPath);
|
|
857
1128
|
const report = await readOptionalFile(reportPath) ?? fallbackReport(parsed, phase, artifactPath);
|
|
1129
|
+
const artifactRoot = path.dirname(artifactPath);
|
|
1130
|
+
const implementationPlanJson = phase === "architect"
|
|
1131
|
+
? await readOptionalFile(path.join(artifactRoot, "implementation.plan.json"))
|
|
1132
|
+
: null;
|
|
1133
|
+
const qaPlanJson = phase === "architect"
|
|
1134
|
+
? await readOptionalFile(path.join(artifactRoot, "qa.plan.json"))
|
|
1135
|
+
: null;
|
|
858
1136
|
return {
|
|
859
1137
|
repoPath: path.resolve(worktreePath),
|
|
860
1138
|
path: artifactPath,
|
|
861
1139
|
verdictJson,
|
|
1140
|
+
implementationPlanJson,
|
|
1141
|
+
qaPlanJson,
|
|
862
1142
|
report,
|
|
863
1143
|
producedBy: optionalString(parsed, "produced_by") ?? optionalString(parsed, "producedBy") ?? "forge-completion-tool",
|
|
864
1144
|
status: optionalString(parsed, "status") ?? "unknown",
|
|
@@ -897,6 +1177,7 @@ export function parseCompletionMcpArgs(argv) {
|
|
|
897
1177
|
let qaCycle = false;
|
|
898
1178
|
let debugCycle = false;
|
|
899
1179
|
let architectCycle = false;
|
|
1180
|
+
let draftCycle = false;
|
|
900
1181
|
for (let index = 1; index < argv.length; index += 1) {
|
|
901
1182
|
const arg = argv[index];
|
|
902
1183
|
if (arg === "--worktree") {
|
|
@@ -918,7 +1199,7 @@ export function parseCompletionMcpArgs(argv) {
|
|
|
918
1199
|
const rawPhase = argv[++index];
|
|
919
1200
|
const parsedPhase = parseStrictCompletionPhase(rawPhase);
|
|
920
1201
|
if (!parsedPhase) {
|
|
921
|
-
return { ok: false, error: "mcp --phase must be one of: implementation, self-review, validation, qa, architect, debug-repro, debug-root-cause, debug-fix, debug-regression." };
|
|
1202
|
+
return { ok: false, error: "mcp --phase must be one of: implementation, self-review, validation, qa, architect, draft, debug-repro, debug-root-cause, debug-fix, debug-regression." };
|
|
922
1203
|
}
|
|
923
1204
|
phase = parsedPhase;
|
|
924
1205
|
}
|
|
@@ -937,6 +1218,9 @@ export function parseCompletionMcpArgs(argv) {
|
|
|
937
1218
|
else if (arg === "--architect-cycle") {
|
|
938
1219
|
architectCycle = true;
|
|
939
1220
|
}
|
|
1221
|
+
else if (arg === "--draft-cycle") {
|
|
1222
|
+
draftCycle = true;
|
|
1223
|
+
}
|
|
940
1224
|
else {
|
|
941
1225
|
return { ok: false, error: `Unknown mcp argument: ${arg}` };
|
|
942
1226
|
}
|
|
@@ -944,7 +1228,7 @@ export function parseCompletionMcpArgs(argv) {
|
|
|
944
1228
|
if (!worktreePath?.trim() || !workItemId?.trim()) {
|
|
945
1229
|
return { ok: false, error: "mcp requires --worktree and --work-item-id." };
|
|
946
1230
|
}
|
|
947
|
-
if ([developCycle, qaCycle, debugCycle, architectCycle].filter(Boolean).length > 1) {
|
|
1231
|
+
if ([developCycle, qaCycle, debugCycle, architectCycle, draftCycle].filter(Boolean).length > 1) {
|
|
948
1232
|
return { ok: false, error: "mcp accepts only one cycle mode." };
|
|
949
1233
|
}
|
|
950
1234
|
if (developCycle && !isDevelopCompletionPhase(phase)) {
|
|
@@ -959,6 +1243,9 @@ export function parseCompletionMcpArgs(argv) {
|
|
|
959
1243
|
if (architectCycle && phase !== "architect") {
|
|
960
1244
|
return { ok: false, error: "mcp --architect-cycle requires --phase architect." };
|
|
961
1245
|
}
|
|
1246
|
+
if (draftCycle && phase !== "draft") {
|
|
1247
|
+
return { ok: false, error: "mcp --draft-cycle requires --phase draft." };
|
|
1248
|
+
}
|
|
962
1249
|
return {
|
|
963
1250
|
ok: true,
|
|
964
1251
|
config: {
|
|
@@ -972,7 +1259,8 @@ export function parseCompletionMcpArgs(argv) {
|
|
|
972
1259
|
developCycle,
|
|
973
1260
|
qaCycle,
|
|
974
1261
|
debugCycle,
|
|
975
|
-
architectCycle
|
|
1262
|
+
architectCycle,
|
|
1263
|
+
draftCycle
|
|
976
1264
|
}
|
|
977
1265
|
};
|
|
978
1266
|
}
|
|
@@ -1110,6 +1398,24 @@ function completionToolNextInstruction(completedPhase, status, nextPhase) {
|
|
|
1110
1398
|
}
|
|
1111
1399
|
return "Validation accepted. Forge has the structured evidence needed to move this work item to QA.";
|
|
1112
1400
|
}
|
|
1401
|
+
function completionToolNextInstructionForDraftGate(completedGate, status, nextGate) {
|
|
1402
|
+
if (status === "blocked") {
|
|
1403
|
+
return `Stay in ${completedGate}; record the blocker and retry the same Draft gate when unblocked.`;
|
|
1404
|
+
}
|
|
1405
|
+
if (!nextGate) {
|
|
1406
|
+
return "Draft proposal accepted. Forge has the structured Work Order draft JSON needed to populate the form.";
|
|
1407
|
+
}
|
|
1408
|
+
const instructions = {
|
|
1409
|
+
draft_intent: "Capture intent/non-goals, then call forge.complete_draft_intent.",
|
|
1410
|
+
draft_repo_context: "Inspect selected repo/project assumptions, then call forge.complete_draft_repo_context.",
|
|
1411
|
+
draft_graph_context: "Account for DAG dependencies and placement, then call forge.complete_draft_graph_context.",
|
|
1412
|
+
draft_product_scope: "Choose product scope for the selected project, then call forge.complete_draft_product_scope.",
|
|
1413
|
+
draft_work_order_plan: "Draft Work Order fields and candidate Feature boundaries, then call forge.complete_draft_work_order_plan.",
|
|
1414
|
+
draft_critic: "Critique literalism/wrong-project scope/decomposition risk, then call forge.complete_draft_critic.",
|
|
1415
|
+
draft_proposal: "Submit final draft_json through forge.complete_draft_proposal."
|
|
1416
|
+
};
|
|
1417
|
+
return `${completedGate} accepted. Continue to ${nextGate}. ${instructions[nextGate]}`;
|
|
1418
|
+
}
|
|
1113
1419
|
function completionToolNextInstructionForQaGate(completedGate, status, nextGate) {
|
|
1114
1420
|
if (!nextGate) {
|
|
1115
1421
|
if (status === "blocked") {
|
|
@@ -1425,7 +1731,7 @@ function toolInputSchema(phase, qaGate = null) {
|
|
|
1425
1731
|
},
|
|
1426
1732
|
tasks: {
|
|
1427
1733
|
type: "array",
|
|
1428
|
-
description: "Required task list. Each task must include id and a non-empty tests array.",
|
|
1734
|
+
description: "Required Develop child task list. Each task must include id and a non-empty tests array, and must require a focused repo/product implementation change. Do not put parent-story QA/sign-off, child-count/materialization inventory, parent plan artifact checks, or final acceptance proof here; those belong in qa_plan.final_qa.",
|
|
1429
1735
|
items: { type: "object", additionalProperties: true }
|
|
1430
1736
|
},
|
|
1431
1737
|
dependencies: {
|
|
@@ -1881,6 +2187,7 @@ function validateQaGatePayloadShape(gate, payload, status) {
|
|
|
1881
2187
|
if (commands.length === 0) {
|
|
1882
2188
|
throw new Error(`${qaGateToolNames[gate]} pass requires commands with at least one entry.`);
|
|
1883
2189
|
}
|
|
2190
|
+
validateQaGatePassingCommands(commands);
|
|
1884
2191
|
}
|
|
1885
2192
|
if (gate === "qa_manual_web" && status === "pass") {
|
|
1886
2193
|
const browserChecks = qaArray(payload, "browser_checks");
|
|
@@ -1890,6 +2197,24 @@ function validateQaGatePayloadShape(gate, payload, status) {
|
|
|
1890
2197
|
}
|
|
1891
2198
|
}
|
|
1892
2199
|
}
|
|
2200
|
+
function validateQaGatePassingCommands(commands) {
|
|
2201
|
+
for (const [index, command] of commands.entries()) {
|
|
2202
|
+
requireEvidenceRecord(command, `commands[${index}]`);
|
|
2203
|
+
const record = command;
|
|
2204
|
+
requireNonEmptyStringRecord(record, "command", `commands[${index}].`);
|
|
2205
|
+
requireNonEmptyStringRecord(record, "status", `commands[${index}].`);
|
|
2206
|
+
requireNonEmptyStringRecord(record, "summary", `commands[${index}].`);
|
|
2207
|
+
if (typeof record.exit_code !== "number" || !Number.isFinite(record.exit_code)) {
|
|
2208
|
+
throw new Error(`commands[${index}].exit_code is required.`);
|
|
2209
|
+
}
|
|
2210
|
+
if (record.exit_code !== 0) {
|
|
2211
|
+
throw new Error(`commands[${index}].exit_code must be 0 when QA gate status is pass.`);
|
|
2212
|
+
}
|
|
2213
|
+
if (normalizedString(record.status) !== "pass") {
|
|
2214
|
+
throw new Error(`commands[${index}].status must be pass when QA gate status is pass.`);
|
|
2215
|
+
}
|
|
2216
|
+
}
|
|
2217
|
+
}
|
|
1893
2218
|
function validateArchitectPayloadShape(payload, status) {
|
|
1894
2219
|
if (status !== "pass") {
|
|
1895
2220
|
requireArchitectNonPassEvidence(payload);
|
|
@@ -1918,6 +2243,7 @@ function validateArchitectPayloadShape(payload, status) {
|
|
|
1918
2243
|
throw new Error(`implementation_plan.tasks: duplicate task id '${taskId}'.`);
|
|
1919
2244
|
}
|
|
1920
2245
|
taskIds.add(taskId);
|
|
2246
|
+
validateArchitectTaskDevelopScope(taskRecord, taskId);
|
|
1921
2247
|
requireArchitectArray(taskRecord, "tests", payload, { path: `implementation_plan.tasks[${optionalString(taskRecord, "id") ?? "?"}].tests` });
|
|
1922
2248
|
if (arrayLength(taskRecord, "tests") === 0) {
|
|
1923
2249
|
throw new Error(`implementation_plan.tasks[${optionalString(taskRecord, "id") ?? "?"}].tests: expected non-empty array; correction: include task-specific verification.`);
|
|
@@ -1930,6 +2256,124 @@ function validateArchitectPayloadShape(payload, status) {
|
|
|
1930
2256
|
throw new Error("qa_plan.final_qa: expected non-empty array; correction: include final QA coverage for the completed plan.");
|
|
1931
2257
|
}
|
|
1932
2258
|
}
|
|
2259
|
+
function validateArchitectTaskDevelopScope(task, taskId) {
|
|
2260
|
+
const explicitScope = firstOptionalString(task, ["execution_scope", "scope", "task_scope", "task_type", "lane", "kind", "type"]);
|
|
2261
|
+
if (isStoryQaScope(explicitScope)) {
|
|
2262
|
+
throw new Error(`implementation_plan.tasks[${taskId}]: story QA/sign-off work must not be materialized as a Develop child task; correction: move this coverage to qa_plan.final_qa and keep implementation_plan.tasks limited to repo/product changes.`);
|
|
2263
|
+
}
|
|
2264
|
+
if (looksLikeStoryLevelVerificationTask(architectTaskScopeText(task))) {
|
|
2265
|
+
throw new Error(`implementation_plan.tasks[${taskId}]: parent-story materialization or sign-off verification belongs in qa_plan.final_qa, not implementation_plan.tasks; correction: keep child tasks focused on implementation work and put child-count/parent-plan artifact checks in final QA coverage.`);
|
|
2266
|
+
}
|
|
2267
|
+
}
|
|
2268
|
+
function firstOptionalString(record, keys) {
|
|
2269
|
+
for (const key of keys) {
|
|
2270
|
+
const value = optionalString(record, key);
|
|
2271
|
+
if (value) {
|
|
2272
|
+
return value;
|
|
2273
|
+
}
|
|
2274
|
+
}
|
|
2275
|
+
return null;
|
|
2276
|
+
}
|
|
2277
|
+
function isStoryQaScope(value) {
|
|
2278
|
+
if (!value) {
|
|
2279
|
+
return false;
|
|
2280
|
+
}
|
|
2281
|
+
const normalized = value.trim().toLowerCase().replace(/[\s-]+/g, "_");
|
|
2282
|
+
return [
|
|
2283
|
+
"qa",
|
|
2284
|
+
"story_qa",
|
|
2285
|
+
"final_qa",
|
|
2286
|
+
"sign_off",
|
|
2287
|
+
"signoff",
|
|
2288
|
+
"story_sign_off",
|
|
2289
|
+
"story_verification",
|
|
2290
|
+
"story_validation"
|
|
2291
|
+
].includes(normalized);
|
|
2292
|
+
}
|
|
2293
|
+
function architectTaskScopeText(task) {
|
|
2294
|
+
const parts = [];
|
|
2295
|
+
for (const key of ["id", "title", "summary", "phase", "phase_id"]) {
|
|
2296
|
+
appendOptionalString(parts, task, key);
|
|
2297
|
+
}
|
|
2298
|
+
for (const key of ["acceptance_criteria", "expected_paths", "touched_surfaces", "implementation_notes"]) {
|
|
2299
|
+
appendOptionalStringArray(parts, task, key);
|
|
2300
|
+
}
|
|
2301
|
+
const tests = task.tests;
|
|
2302
|
+
if (Array.isArray(tests)) {
|
|
2303
|
+
for (const test of tests) {
|
|
2304
|
+
if (!test || typeof test !== "object" || Array.isArray(test)) {
|
|
2305
|
+
continue;
|
|
2306
|
+
}
|
|
2307
|
+
const record = test;
|
|
2308
|
+
for (const key of ["id", "command", "purpose", "target", "expected"]) {
|
|
2309
|
+
appendOptionalString(parts, record, key);
|
|
2310
|
+
}
|
|
2311
|
+
appendOptionalStringArray(parts, record, "steps");
|
|
2312
|
+
}
|
|
2313
|
+
}
|
|
2314
|
+
return parts.join(" ");
|
|
2315
|
+
}
|
|
2316
|
+
function appendOptionalString(parts, record, key) {
|
|
2317
|
+
const value = optionalString(record, key);
|
|
2318
|
+
if (value) {
|
|
2319
|
+
parts.push(value);
|
|
2320
|
+
}
|
|
2321
|
+
}
|
|
2322
|
+
function appendOptionalStringArray(parts, record, key) {
|
|
2323
|
+
const value = record[key];
|
|
2324
|
+
if (!Array.isArray(value)) {
|
|
2325
|
+
return;
|
|
2326
|
+
}
|
|
2327
|
+
for (const entry of value) {
|
|
2328
|
+
if (typeof entry === "string" && entry.trim().length > 0) {
|
|
2329
|
+
parts.push(entry.trim());
|
|
2330
|
+
}
|
|
2331
|
+
}
|
|
2332
|
+
}
|
|
2333
|
+
function looksLikeStoryLevelVerificationTask(text) {
|
|
2334
|
+
if (!text.trim()) {
|
|
2335
|
+
return false;
|
|
2336
|
+
}
|
|
2337
|
+
const normalized = text.toLowerCase();
|
|
2338
|
+
const verificationWords = [
|
|
2339
|
+
"verify",
|
|
2340
|
+
"verification",
|
|
2341
|
+
"validate",
|
|
2342
|
+
"validation",
|
|
2343
|
+
"inspect",
|
|
2344
|
+
"audit",
|
|
2345
|
+
"check",
|
|
2346
|
+
"confirm",
|
|
2347
|
+
"prove",
|
|
2348
|
+
"evidence",
|
|
2349
|
+
"sign-off",
|
|
2350
|
+
"sign off"
|
|
2351
|
+
];
|
|
2352
|
+
if (!verificationWords.some((word) => normalized.includes(word))) {
|
|
2353
|
+
return false;
|
|
2354
|
+
}
|
|
2355
|
+
return [
|
|
2356
|
+
"materialization",
|
|
2357
|
+
"materialized child",
|
|
2358
|
+
"child inventory",
|
|
2359
|
+
"child work-item",
|
|
2360
|
+
"child work item",
|
|
2361
|
+
"created task count",
|
|
2362
|
+
"created_task_count",
|
|
2363
|
+
"parent plan",
|
|
2364
|
+
"parent story",
|
|
2365
|
+
"story sign-off",
|
|
2366
|
+
"story sign off",
|
|
2367
|
+
".forge/work-items",
|
|
2368
|
+
".forge\\work-items",
|
|
2369
|
+
".forge/stories",
|
|
2370
|
+
".forge\\stories",
|
|
2371
|
+
"implementation.plan.json",
|
|
2372
|
+
"qa.plan.json",
|
|
2373
|
+
"architect plan artifact",
|
|
2374
|
+
"child spec"
|
|
2375
|
+
].some((needle) => normalized.includes(needle));
|
|
2376
|
+
}
|
|
1933
2377
|
function validateAndNormalizeArchitectDependencies(implementationPlan, taskIds) {
|
|
1934
2378
|
const dependencies = implementationPlan.dependencies;
|
|
1935
2379
|
const edges = new Map();
|
|
@@ -2251,6 +2695,9 @@ function parseStrictCompletionPhase(value) {
|
|
|
2251
2695
|
if (normalized === "architect" || normalized === "architect_plan" || normalized === "planning" || normalized === "plan") {
|
|
2252
2696
|
return "architect";
|
|
2253
2697
|
}
|
|
2698
|
+
if (normalized === "draft" || normalized === "drafting" || normalized === "draft_work_order") {
|
|
2699
|
+
return "draft";
|
|
2700
|
+
}
|
|
2254
2701
|
return null;
|
|
2255
2702
|
}
|
|
2256
2703
|
function normalizeDebugCompletionPhase(value) {
|
|
@@ -2322,6 +2769,9 @@ function phaseFromToolName(toolName) {
|
|
|
2322
2769
|
if (normalized === "forge.complete_architect_plan" || normalized === "complete_architect_plan") {
|
|
2323
2770
|
return "architect";
|
|
2324
2771
|
}
|
|
2772
|
+
if (draftGateFromToolName(toolName)) {
|
|
2773
|
+
return "draft";
|
|
2774
|
+
}
|
|
2325
2775
|
const debugGate = debugGateFromToolName(toolName);
|
|
2326
2776
|
if (debugGate) {
|
|
2327
2777
|
return debugGate;
|
|
@@ -2331,6 +2781,16 @@ function phaseFromToolName(toolName) {
|
|
|
2331
2781
|
}
|
|
2332
2782
|
throw new Error(`Unknown Forge completion tool: ${toolName}`);
|
|
2333
2783
|
}
|
|
2784
|
+
function draftGateFromToolName(toolName) {
|
|
2785
|
+
const normalized = toolName.trim().toLowerCase();
|
|
2786
|
+
for (const gate of draftCompletionGateOrder) {
|
|
2787
|
+
const base = draftGateToolNames[gate];
|
|
2788
|
+
if (normalized === `forge.${base}` || normalized === base) {
|
|
2789
|
+
return gate;
|
|
2790
|
+
}
|
|
2791
|
+
}
|
|
2792
|
+
return null;
|
|
2793
|
+
}
|
|
2334
2794
|
function debugGateFromToolName(toolName) {
|
|
2335
2795
|
const normalized = toolName.trim().toLowerCase();
|
|
2336
2796
|
for (const gate of debugCompletionPhaseOrder) {
|