@loucompanion/forge-bridge 0.1.1-dev.40fbc494669b → 0.1.1-dev.5093acd6bece

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.
@@ -3,8 +3,10 @@ import type { WorktreeHandle } from "../worktree/worktree.types.js";
3
3
  export type DevelopCompletionPhase = "implementation" | "self_review" | "validation";
4
4
  export type DebugCompletionPhase = "debug_repro" | "debug_root_cause" | "debug_fix" | "debug_regression";
5
5
  export type ArchitectCompletionPhase = "architect";
6
- export type CompletionPhase = DevelopCompletionPhase | "qa" | DebugCompletionPhase | ArchitectCompletionPhase;
6
+ export type DraftCompletionPhase = "draft";
7
+ export type CompletionPhase = DevelopCompletionPhase | "qa" | DebugCompletionPhase | ArchitectCompletionPhase | DraftCompletionPhase;
7
8
  export type QaCompletionGate = "qa_plan" | "qa_regression" | "qa_api" | "qa_manual_web" | "qa_bug_repro" | "qa_findings" | "qa_verdict";
9
+ export type DraftCompletionGate = "draft_intent" | "draft_repo_context" | "draft_graph_context" | "draft_product_scope" | "draft_work_order_plan" | "draft_critic" | "draft_proposal";
8
10
  export interface CompletionToolRuntime {
9
11
  readonly serverName: string;
10
12
  readonly command: string;
@@ -30,6 +32,7 @@ export interface CompletionToolServerConfig {
30
32
  readonly qaCycle?: boolean;
31
33
  readonly debugCycle?: boolean;
32
34
  readonly architectCycle?: boolean;
35
+ readonly draftCycle?: boolean;
33
36
  }
34
37
  export interface CompletionToolCallResult {
35
38
  readonly content: readonly {
@@ -40,6 +43,7 @@ export interface CompletionToolCallResult {
40
43
  readonly gate: {
41
44
  readonly phase: CompletionPhase;
42
45
  readonly qaGate?: QaCompletionGate;
46
+ readonly draftGate?: DraftCompletionGate;
43
47
  readonly status: string;
44
48
  readonly allowed: boolean;
45
49
  readonly reason: string;
@@ -62,6 +66,7 @@ export interface CompletionArtifactReference {
62
66
  }
63
67
  export declare const debugCompletionPhaseOrder: readonly DebugCompletionPhase[];
64
68
  export declare const qaCompletionGateOrder: readonly QaCompletionGate[];
69
+ export declare const draftCompletionGateOrder: readonly DraftCompletionGate[];
65
70
  export declare function completionPhaseFromDispatch(dispatch: RunOnceDispatchPacket): CompletionPhase;
66
71
  export declare function normalizeCompletionPhase(value: string | null | undefined): CompletionPhase;
67
72
  export declare function usesDevelopCycleCompletion(dispatch: RunOnceDispatchPacket): boolean;
@@ -71,9 +76,11 @@ export declare function usesArchitectCycleCompletion(dispatch: RunOnceDispatchPa
71
76
  export declare function completionToolNamesForPhase(phase: CompletionPhase): readonly string[];
72
77
  export declare function completionToolNamesForQaGate(gate: QaCompletionGate): readonly string[];
73
78
  export declare function completionToolNamesForDebugGate(gate: DebugCompletionPhase): readonly string[];
79
+ export declare function completionToolNamesForDraftGate(gate: DraftCompletionGate): readonly string[];
74
80
  export declare function expectedCompletionToolName(phase: CompletionPhase): string;
75
81
  export declare function expectedQaCompletionToolName(gate: QaCompletionGate): string;
76
82
  export declare function expectedDebugCompletionToolName(gate: DebugCompletionPhase): string;
83
+ export declare function expectedDraftCompletionToolName(gate: DraftCompletionGate): string;
77
84
  export declare function artifactRootFor(worktreePath: string, workItemId: string): string;
78
85
  export declare function artifactRootForOwner(worktreePath: string, ownerType: string, ownerId: string): string;
79
86
  export declare function completionArtifactJsonPath(worktreePath: string, workItemId: string, phase: CompletionPhase, ownerType?: string | null, ownerId?: string | null): string;
@@ -85,20 +92,24 @@ export declare function cursorMcpServerConfig(runtime: CompletionToolRuntime): u
85
92
  export declare function completionPromptForPhase(phase: CompletionPhase): string;
86
93
  export declare function completionPromptForDevelopCycle(phase: CompletionPhase): string;
87
94
  export declare function completionPromptForQaCycle(currentGate?: QaCompletionGate): string;
95
+ export declare function completionPromptForDraftCycle(currentGate?: DraftCompletionGate): string;
88
96
  export declare function completionPromptForDebugCycle(currentGate?: DebugCompletionPhase): string;
89
97
  export declare function completionToolNamesForServer(config: CompletionToolServerConfig): readonly string[];
90
98
  export declare class CompletionToolServerSession {
91
99
  private readonly config;
92
100
  private currentPhase;
93
101
  private currentQaGate;
102
+ private currentDraftGate;
94
103
  private completed;
95
104
  constructor(config: CompletionToolServerConfig);
96
105
  get instructions(): string;
97
106
  toolDefinitions(): readonly unknown[];
98
107
  callTool(toolName: string, input: unknown): Promise<CompletionToolCallResult>;
108
+ private callDraftGateTool;
99
109
  private callDebugGateTool;
100
110
  private callQaGateTool;
101
111
  private advanceAfterPass;
112
+ private advanceAfterDraftGate;
102
113
  private advanceAfterQaGate;
103
114
  private advanceAfterDebugGate;
104
115
  }
@@ -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) {
@@ -335,6 +383,19 @@ Use \`status: "skipped"\` with a reason for gates that are not applicable. If a
335
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\`.
336
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.`;
337
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
+ }
338
399
  export function completionPromptForDebugCycle(currentGate = "debug_repro") {
339
400
  return `Forge completion tools are available through the MCP server named \`${toolServerName}\`.
340
401
  Do not write Debug artifacts by hand. The tool server enforces Debug gate order and starts this run at \`${currentGate}\`.
@@ -347,33 +408,38 @@ Each tool writes a canonical Debug artifact under \`.forge/work-items/{workItemI
347
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.`;
348
409
  }
349
410
  export function completionToolNamesForServer(config) {
350
- return config.debugCycle
351
- ? debugCompletionPhaseOrder.flatMap((gate) => completionToolNamesForDebugGate(gate))
352
- : config.qaCycle
353
- ? qaCompletionGateOrder.flatMap((gate) => completionToolNamesForQaGate(gate))
354
- : config.developCycle
355
- ? completionPhaseOrder.flatMap((phase) => completionToolNamesForPhase(phase))
356
- : config.architectCycle
357
- ? completionToolNamesForPhase("architect")
358
- : completionToolNamesForPhase(config.phase);
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);
359
422
  }
360
423
  export class CompletionToolServerSession {
361
424
  config;
362
425
  currentPhase;
363
426
  currentQaGate = "qa_plan";
427
+ currentDraftGate = "draft_intent";
364
428
  completed = false;
365
429
  constructor(config) {
366
430
  this.config = config;
367
431
  this.currentPhase = config.phase;
368
432
  }
369
433
  get instructions() {
370
- return this.config.debugCycle
371
- ? completionPromptForDebugCycle(asDebugCompletionPhase(this.currentPhase))
372
- : this.config.qaCycle
373
- ? completionPromptForQaCycle(this.currentQaGate)
374
- : this.config.developCycle
375
- ? completionPromptForDevelopCycle(this.currentPhase)
376
- : completionPromptForPhase(this.currentPhase);
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);
377
443
  }
378
444
  toolDefinitions() {
379
445
  return completionToolNamesForServer(this.config).map((name) => toolDefinition(name, phaseFromToolName(name), qaGateFromToolName(name)));
@@ -383,6 +449,9 @@ export class CompletionToolServerSession {
383
449
  if (this.completed) {
384
450
  throw new Error("Forge completion cycle is already complete; no more completion calls are accepted.");
385
451
  }
452
+ if (this.config.draftCycle) {
453
+ return await this.callDraftGateTool(toolName, requestedPhase, input);
454
+ }
386
455
  if (this.config.debugCycle) {
387
456
  return await this.callDebugGateTool(toolName, requestedPhase, input);
388
457
  }
@@ -423,6 +492,30 @@ export class CompletionToolServerSession {
423
492
  }
424
493
  };
425
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
+ }
426
519
  async callDebugGateTool(toolName, requestedPhase, input) {
427
520
  const currentDebugGate = asDebugCompletionPhase(this.currentPhase);
428
521
  if (!isDebugCompletionPhase(requestedPhase)) {
@@ -519,6 +612,16 @@ export class CompletionToolServerSession {
519
612
  this.completed = true;
520
613
  return "dev_test_ready";
521
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
+ }
522
625
  advanceAfterQaGate(gate) {
523
626
  const index = qaCompletionGateOrder.indexOf(gate);
524
627
  const next = index >= 0 ? qaCompletionGateOrder[index + 1] ?? null : null;
@@ -540,6 +643,43 @@ export class CompletionToolServerSession {
540
643
  return null;
541
644
  }
542
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
+ }
543
683
  export async function writeCompletionArtifact(config, toolName, input) {
544
684
  const phase = phaseFromToolName(toolName);
545
685
  if (phase !== config.phase) {
@@ -1037,6 +1177,7 @@ export function parseCompletionMcpArgs(argv) {
1037
1177
  let qaCycle = false;
1038
1178
  let debugCycle = false;
1039
1179
  let architectCycle = false;
1180
+ let draftCycle = false;
1040
1181
  for (let index = 1; index < argv.length; index += 1) {
1041
1182
  const arg = argv[index];
1042
1183
  if (arg === "--worktree") {
@@ -1058,7 +1199,7 @@ export function parseCompletionMcpArgs(argv) {
1058
1199
  const rawPhase = argv[++index];
1059
1200
  const parsedPhase = parseStrictCompletionPhase(rawPhase);
1060
1201
  if (!parsedPhase) {
1061
- 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." };
1062
1203
  }
1063
1204
  phase = parsedPhase;
1064
1205
  }
@@ -1077,6 +1218,9 @@ export function parseCompletionMcpArgs(argv) {
1077
1218
  else if (arg === "--architect-cycle") {
1078
1219
  architectCycle = true;
1079
1220
  }
1221
+ else if (arg === "--draft-cycle") {
1222
+ draftCycle = true;
1223
+ }
1080
1224
  else {
1081
1225
  return { ok: false, error: `Unknown mcp argument: ${arg}` };
1082
1226
  }
@@ -1084,7 +1228,7 @@ export function parseCompletionMcpArgs(argv) {
1084
1228
  if (!worktreePath?.trim() || !workItemId?.trim()) {
1085
1229
  return { ok: false, error: "mcp requires --worktree and --work-item-id." };
1086
1230
  }
1087
- if ([developCycle, qaCycle, debugCycle, architectCycle].filter(Boolean).length > 1) {
1231
+ if ([developCycle, qaCycle, debugCycle, architectCycle, draftCycle].filter(Boolean).length > 1) {
1088
1232
  return { ok: false, error: "mcp accepts only one cycle mode." };
1089
1233
  }
1090
1234
  if (developCycle && !isDevelopCompletionPhase(phase)) {
@@ -1099,6 +1243,9 @@ export function parseCompletionMcpArgs(argv) {
1099
1243
  if (architectCycle && phase !== "architect") {
1100
1244
  return { ok: false, error: "mcp --architect-cycle requires --phase architect." };
1101
1245
  }
1246
+ if (draftCycle && phase !== "draft") {
1247
+ return { ok: false, error: "mcp --draft-cycle requires --phase draft." };
1248
+ }
1102
1249
  return {
1103
1250
  ok: true,
1104
1251
  config: {
@@ -1112,7 +1259,8 @@ export function parseCompletionMcpArgs(argv) {
1112
1259
  developCycle,
1113
1260
  qaCycle,
1114
1261
  debugCycle,
1115
- architectCycle
1262
+ architectCycle,
1263
+ draftCycle
1116
1264
  }
1117
1265
  };
1118
1266
  }
@@ -1250,6 +1398,24 @@ function completionToolNextInstruction(completedPhase, status, nextPhase) {
1250
1398
  }
1251
1399
  return "Validation accepted. Forge has the structured evidence needed to move this work item to QA.";
1252
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
+ }
1253
1419
  function completionToolNextInstructionForQaGate(completedGate, status, nextGate) {
1254
1420
  if (!nextGate) {
1255
1421
  if (status === "blocked") {
@@ -2529,6 +2695,9 @@ function parseStrictCompletionPhase(value) {
2529
2695
  if (normalized === "architect" || normalized === "architect_plan" || normalized === "planning" || normalized === "plan") {
2530
2696
  return "architect";
2531
2697
  }
2698
+ if (normalized === "draft" || normalized === "drafting" || normalized === "draft_work_order") {
2699
+ return "draft";
2700
+ }
2532
2701
  return null;
2533
2702
  }
2534
2703
  function normalizeDebugCompletionPhase(value) {
@@ -2600,6 +2769,9 @@ function phaseFromToolName(toolName) {
2600
2769
  if (normalized === "forge.complete_architect_plan" || normalized === "complete_architect_plan") {
2601
2770
  return "architect";
2602
2771
  }
2772
+ if (draftGateFromToolName(toolName)) {
2773
+ return "draft";
2774
+ }
2603
2775
  const debugGate = debugGateFromToolName(toolName);
2604
2776
  if (debugGate) {
2605
2777
  return debugGate;
@@ -2609,6 +2781,16 @@ function phaseFromToolName(toolName) {
2609
2781
  }
2610
2782
  throw new Error(`Unknown Forge completion tool: ${toolName}`);
2611
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
+ }
2612
2794
  function debugGateFromToolName(toolName) {
2613
2795
  const normalized = toolName.trim().toLowerCase();
2614
2796
  for (const gate of debugCompletionPhaseOrder) {
@@ -9,7 +9,7 @@ import { GitClient } from "../../shared/git/git.js";
9
9
  import { resolveCursorAgentDirectLaunch } from "../../shared/cursor-agent/cursor-agent.launch.js";
10
10
  import { ensureNoSymbolicLinkPathSegments, isPathUnderRoot, sha256File } from "../qa-sidecar-validation.js";
11
11
  import { buildSpecBody, selectLaneCommand } from "../../worktree/artifact-injector.js";
12
- import { codexMcpConfigArgs, completionPhaseFromDispatch, completionPromptForDebugCycle, completionPromptForDevelopCycle, completionPromptForPhase, completionPromptForQaCycle, completionRedactionEnv, expectedCompletionToolName, expectedDebugCompletionToolName, expectedQaCompletionToolName, prepareCompletionToolRuntime, readCompletionArtifact, usesDebugCycleCompletion, usesDevelopCycleCompletion, writeCompletionArtifact, artifactRootFor, artifactRootForOwner, debugCompletionPhaseOrder, qaCompletionGateOrder } from "../phase-completion.js";
12
+ import { codexMcpConfigArgs, completionPhaseFromDispatch, completionPromptForDebugCycle, completionPromptForDevelopCycle, completionPromptForDraftCycle, completionPromptForPhase, completionPromptForQaCycle, completionRedactionEnv, expectedCompletionToolName, expectedDebugCompletionToolName, expectedQaCompletionToolName, prepareCompletionToolRuntime, readCompletionArtifact, usesDebugCycleCompletion, usesDevelopCycleCompletion, writeCompletionArtifact, artifactRootFor, artifactRootForOwner, debugCompletionPhaseOrder, draftCompletionGateOrder, qaCompletionGateOrder } from "../phase-completion.js";
13
13
  import { addUsage, buildProviderUsageEvent, emptyUsage, hasUsage, readUsageNumber } from "./provider-usage.js";
14
14
  const fakeWorkerModel = "server-local-fake-worker";
15
15
  const execFileAsync = promisify(execFile);
@@ -189,17 +189,45 @@ export class ProcessBackedProviderCliService {
189
189
  throw new Error("Draft dispatch context is missing.");
190
190
  }
191
191
  const tempDir = await fs.mkdtemp(path.join(os.tmpdir(), "forge-bridge-draft-"));
192
+ const draftWorktree = {
193
+ repoPath: tempDir,
194
+ worktreePath: tempDir,
195
+ branchName: "draft",
196
+ baseBranch: dispatch.cycleRun.baseBranch || dispatch.repo.defaultBranch || "draft",
197
+ cleanupWorktrees: true
198
+ };
199
+ const metadataRoot = path.join(tempDir, ".forge", "work-items", dispatch.workItem.id);
200
+ await fs.mkdir(metadataRoot, { recursive: true });
201
+ await fs.writeFile(path.join(metadataRoot, "draft-input.json"), JSON.stringify(dispatch.draft, null, 2) + "\n", "utf8");
202
+ await fs.writeFile(path.join(metadataRoot, "spec.md"), buildDraftSpec(dispatch), "utf8");
203
+ const completionTools = await prepareCompletionToolRuntime({ dispatch, worktree: draftWorktree, phase: "draft" });
192
204
  const command = buildProviderCliCommand(dispatch, {
193
205
  worktreePath: tempDir,
194
206
  prompt: buildDraftPrompt(dispatch),
207
+ completionTools,
195
208
  skipGitRepoCheck: true
196
209
  });
210
+ const draftArtifactSlugs = dispatch.artifacts
211
+ .filter((artifact) => artifact.scope === "operational" && ["command", "skill", "hook"].includes(artifact.kind))
212
+ .map((artifact) => `${artifact.kind}:${artifact.slug}`);
213
+ await context.emit({
214
+ role: "runner",
215
+ kind: "checkpoint",
216
+ content: `Draft artifacts injected: ${draftArtifactSlugs.length}${draftArtifactSlugs.length ? ` (${draftArtifactSlugs.join(", ")})` : ""}. Project: ${dispatch.draft.projectName ?? dispatch.repo.name}. Repo: ${dispatch.draft.repoName ?? dispatch.repo.name}.`,
217
+ eventType: "draft_artifacts_injected"
218
+ });
197
219
  await context.emit({
198
220
  role: "runner",
199
221
  kind: "checkpoint",
200
222
  content: `Starting provider CLI Draft: ${formatProviderCliCommand(command)}`,
201
223
  eventType: "provider_cli_started"
202
224
  });
225
+ await context.emit({
226
+ role: "runner",
227
+ kind: "checkpoint",
228
+ content: "Forge Draft completion tools configured for draft_intent -> draft_repo_context -> draft_graph_context -> draft_product_scope -> draft_work_order_plan -> draft_critic -> draft_proposal; provider must call each MCP completion tool before exit.",
229
+ eventType: "completion_tools_configured"
230
+ });
203
231
  const outputState = createProviderOutputState(dispatch.execution.provider);
204
232
  try {
205
233
  const result = await this.processRunner.run({
@@ -217,23 +245,38 @@ export class ProcessBackedProviderCliService {
217
245
  if (result.exitCode !== 0) {
218
246
  throw new Error(`Provider CLI exited with code ${result.exitCode}${summarizeStderr(result.stderr)}.`);
219
247
  }
220
- const content = outputState.finalMessage ?? result.stdout.trim();
221
- if (!content) {
222
- throw new Error("Provider CLI completed without returning a draft.");
248
+ await context.emit({
249
+ role: "runner",
250
+ kind: "checkpoint",
251
+ content: `Provider CLI Draft completed: ${command.command} exit ${result.exitCode}${outputState.sessionLabel ? ` ${outputState.sessionLabel}` : ""}.`,
252
+ eventType: "provider_cli_completed"
253
+ });
254
+ await emitProviderUsageEvent(context.emit, dispatch, outputState);
255
+ await emitDraftGateArtifacts(context.emit, tempDir, dispatch.workItem.id);
256
+ const proposalArtifact = await readCompletionArtifact(tempDir, dispatch.workItem.id, "draft");
257
+ if (!proposalArtifact) {
258
+ throw new Error("Provider CLI Draft completed but did not call forge.complete_draft_proposal.");
259
+ }
260
+ const proposal = parseJsonLine(proposalArtifact.verdictJson) ?? {};
261
+ const draftJson = proposal["draft_json"];
262
+ if (!draftJson || typeof draftJson !== "object" || Array.isArray(draftJson)) {
263
+ throw new Error("Provider CLI Draft completed but draft.proposal.json did not contain draft_json.");
223
264
  }
224
- const json = extractJsonObject(content);
265
+ const json = JSON.stringify(draftJson);
225
266
  await context.emit({
226
267
  role: "assistant",
227
268
  kind: "message",
228
269
  content: json,
229
270
  eventType: "draft_complete"
230
271
  });
231
- await emitProviderUsageEvent(context.emit, dispatch, outputState);
232
272
  return {
233
- summary: "Draft generated through Forge Runner."
273
+ summary: "Draft generated through Forge Draft completion cycle.",
274
+ artifact: proposalArtifact,
275
+ artifacts: [proposalArtifact]
234
276
  };
235
277
  }
236
278
  finally {
279
+ await cleanupCompletionTools(completionTools, context.emit);
237
280
  await fs.rm(tempDir, { recursive: true, force: true });
238
281
  }
239
282
  }
@@ -1900,6 +1943,67 @@ function isFakeWorkerModel(model) {
1900
1943
  function sha256(content) {
1901
1944
  return crypto.createHash("sha256").update(content).digest("hex");
1902
1945
  }
1946
+ function buildDraftSpec(dispatch) {
1947
+ return `# Draft Generation Context
1948
+
1949
+ Project: ${dispatch.draft?.projectName ?? dispatch.repo.name}
1950
+ Repository: ${dispatch.draft?.repoName ?? dispatch.repo.name}
1951
+ Draft kind: ${dispatch.draft?.kind ?? "draft"}
1952
+
1953
+ ## User Prompt
1954
+
1955
+ ${dispatch.draft?.userPrompt ?? ""}
1956
+ `;
1957
+ }
1958
+ async function emitDraftGateArtifacts(emit, worktreePath, workItemId) {
1959
+ const draftArtifactFiles = {
1960
+ draft_intent: "draft.intent.json",
1961
+ draft_repo_context: "draft.repo-context.json",
1962
+ draft_graph_context: "draft.graph-context.json",
1963
+ draft_product_scope: "draft.product-scope.json",
1964
+ draft_work_order_plan: "draft.work-order-plan.json",
1965
+ draft_critic: "draft.critic.json",
1966
+ draft_proposal: "draft.proposal.json"
1967
+ };
1968
+ for (const gate of draftCompletionGateOrder) {
1969
+ const actualPath = path.join(worktreePath, ".forge", "work-items", workItemId, "artifacts", draftArtifactFiles[gate]);
1970
+ if (!await fileExists(actualPath)) {
1971
+ continue;
1972
+ }
1973
+ const relativePath = path.relative(worktreePath, actualPath).split(path.sep).join("/");
1974
+ await emit({
1975
+ role: "tool",
1976
+ kind: "tool_result",
1977
+ content: `Forge Draft completion tool recorded ${gate} artifact ${relativePath}.`,
1978
+ eventType: "completion_tool_artifact_recorded",
1979
+ data: JSON.stringify({ phase: "draft", draft_gate: gate, artifact_relative_paths: [relativePath] })
1980
+ });
1981
+ await emit({
1982
+ role: "hook",
1983
+ kind: "checkpoint",
1984
+ content: `phase_gate: ${gate} artifact accepted with status pass.`,
1985
+ eventType: "phase_gate"
1986
+ });
1987
+ const next = draftCompletionGateOrder[draftCompletionGateOrder.indexOf(gate) + 1];
1988
+ if (next) {
1989
+ await emit({
1990
+ role: "hook",
1991
+ kind: "checkpoint",
1992
+ content: `phase_progression: ${gate} -> ${next}`,
1993
+ eventType: "phase_gate"
1994
+ });
1995
+ }
1996
+ }
1997
+ }
1998
+ async function fileExists(filePath) {
1999
+ try {
2000
+ await fs.access(filePath);
2001
+ return true;
2002
+ }
2003
+ catch {
2004
+ return false;
2005
+ }
2006
+ }
1903
2007
  function buildDraftPrompt(dispatch) {
1904
2008
  const draft = dispatch.draft;
1905
2009
  if (!draft) {
@@ -1912,13 +2016,22 @@ function buildDraftPrompt(dispatch) {
1912
2016
  const artifactSection = draftArtifacts
1913
2017
  ? `\n\n# Injected Draft command/skills/hooks\n\n${draftArtifacts}`
1914
2018
  : "";
1915
- return `${draft.systemPrompt}${artifactSection}
2019
+ const projectSection = [
2020
+ draft.projectName ? `Project: ${draft.projectName}` : null,
2021
+ draft.repoName ? `Repository: ${draft.repoName}` : null
2022
+ ].filter(Boolean).join("\n");
2023
+ const scopedProjectSection = projectSection ? `\n\n# Selected project context\n\n${projectSection}` : "";
2024
+ return `${draft.systemPrompt}${scopedProjectSection}${artifactSection}
1916
2025
 
1917
2026
  Draft kind: ${draft.kind}
1918
2027
 
1919
2028
  ${draft.userPrompt}
1920
2029
 
1921
- Return ONLY the requested JSON object. Do not wrap it in markdown and do not modify files.`;
2030
+ ${completionPromptForDraftCycle("draft_intent")}
2031
+
2032
+ Use the injected command/skills/hooks as lane instructions. You must call each Forge Draft MCP completion tool in order. Do not claim the Draft is complete until forge.complete_draft_proposal accepts the final draft_json.
2033
+
2034
+ Do not modify source files.`;
1922
2035
  }
1923
2036
  function extractJsonObject(content) {
1924
2037
  let trimmed = content.trim();
@@ -23,6 +23,8 @@ export interface DispatchDraftPrompt {
23
23
  readonly kind: string;
24
24
  readonly systemPrompt: string;
25
25
  readonly userPrompt: string;
26
+ readonly projectName?: string | null;
27
+ readonly repoName?: string | null;
26
28
  }
27
29
  export interface DispatchQaBrowserContext {
28
30
  readonly required: boolean;
package/dist/version.d.ts CHANGED
@@ -1 +1 @@
1
- export declare const forgeBridgeVersion = "0.1.1-dev.40fbc494669b";
1
+ export declare const forgeBridgeVersion = "0.1.1-dev.5093acd6bece";
package/dist/version.js CHANGED
@@ -1 +1 @@
1
- export const forgeBridgeVersion = "0.1.1-dev.40fbc494669b";
1
+ export const forgeBridgeVersion = "0.1.1-dev.5093acd6bece";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@loucompanion/forge-bridge",
3
- "version": "0.1.1-dev.40fbc494669b",
3
+ "version": "0.1.1-dev.5093acd6bece",
4
4
  "description": "Forge runner bridge CLI for connecting local developer machines to Forge dispatch.",
5
5
  "license": "UNLICENSED",
6
6
  "type": "module",