@loucompanion/forge-bridge 0.1.1-dev.8dcbc6245e51 → 0.1.1-dev.ac486debe210

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.
Files changed (32) hide show
  1. package/dist/bridge/bridge.service.js +7 -1
  2. package/dist/bridge/bridge.types.d.ts +6 -0
  3. package/dist/cli/cli.service.d.ts +27 -2
  4. package/dist/cli/cli.service.js +406 -16
  5. package/dist/cli/local-config.d.ts +3 -0
  6. package/dist/cli/local-config.js +8 -2
  7. package/dist/session/index.d.ts +2 -0
  8. package/dist/session/index.js +1 -0
  9. package/dist/session/internals.d.ts +4 -2
  10. package/dist/session/internals.js +2 -1
  11. package/dist/session/phase-completion.d.ts +86 -0
  12. package/dist/session/phase-completion.js +1018 -0
  13. package/dist/session/provider-cli/index.d.ts +2 -2
  14. package/dist/session/provider-cli/index.js +1 -1
  15. package/dist/session/provider-cli/provider-cli.base.d.ts +2 -2
  16. package/dist/session/provider-cli/provider-cli.service.d.ts +48 -3
  17. package/dist/session/provider-cli/provider-cli.service.js +1226 -38
  18. package/dist/session/provider-cli/provider-cli.types.d.ts +2 -0
  19. package/dist/session/provider-cli/provider-usage.d.ts +25 -0
  20. package/dist/session/provider-cli/provider-usage.js +64 -0
  21. package/dist/session/session.service.js +19 -10
  22. package/dist/session/session.types.d.ts +7 -0
  23. package/dist/session/session.types.js +11 -0
  24. package/dist/shared/cursor-agent/cursor-agent.launch.d.ts +9 -0
  25. package/dist/shared/cursor-agent/cursor-agent.launch.js +66 -0
  26. package/dist/version.d.ts +1 -1
  27. package/dist/version.js +1 -1
  28. package/dist/worktree/artifact-injector.d.ts +1 -1
  29. package/dist/worktree/artifact-injector.js +9 -9
  30. package/dist/worktree/worktree.service.d.ts +2 -0
  31. package/dist/worktree/worktree.service.js +107 -2
  32. package/package.json +1 -1
@@ -0,0 +1,1018 @@
1
+ import crypto from "node:crypto";
2
+ import fs from "node:fs/promises";
3
+ import path from "node:path";
4
+ import { fileURLToPath } from "node:url";
5
+ import { redactSecrets } from "../shared/redaction/redaction.js";
6
+ const toolServerName = "forge";
7
+ const completionRedactionSecretsEnv = "FORGE_COMPLETION_REDACTION_SECRETS";
8
+ const completionPhaseOrder = ["implementation", "self_review", "validation"];
9
+ const phaseFiles = {
10
+ implementation: {
11
+ jsonFileName: "implementation.completeness.json",
12
+ reportFileName: "implementation.report.md"
13
+ },
14
+ self_review: {
15
+ jsonFileName: "self-review.report.json",
16
+ reportFileName: "self-review.summary.md"
17
+ },
18
+ validation: {
19
+ jsonFileName: "validation.verdict.json",
20
+ reportFileName: "validation.report.md"
21
+ }
22
+ };
23
+ export function completionPhaseFromDispatch(dispatch) {
24
+ return normalizeCompletionPhase(dispatch.cycleRun.phase || dispatch.execution.lane || dispatch.execution.role);
25
+ }
26
+ export function normalizeCompletionPhase(value) {
27
+ const normalized = (value ?? "").trim().toLowerCase().replace(/-/g, "_");
28
+ if (normalized === "self_review" || normalized === "selfreview" || normalized === "review") {
29
+ return "self_review";
30
+ }
31
+ if (normalized === "validate" || normalized === "validation") {
32
+ return "validation";
33
+ }
34
+ return "implementation";
35
+ }
36
+ export function usesDevelopCycleCompletion(dispatch) {
37
+ const kind = dispatch.cycleRun.kind.trim().toLowerCase();
38
+ const role = dispatch.execution.role.trim().toLowerCase();
39
+ const lane = dispatch.execution.lane.trim().toLowerCase();
40
+ return kind === "develop"
41
+ && role === "develop"
42
+ && lane === "develop"
43
+ && completionPhaseFromDispatch(dispatch) === "implementation";
44
+ }
45
+ export function completionToolNamesForPhase(phase) {
46
+ const base = phase === "implementation"
47
+ ? "complete_implementation"
48
+ : phase === "self_review"
49
+ ? "complete_self_review"
50
+ : "complete_validation";
51
+ return [`forge.${base}`, base];
52
+ }
53
+ export function expectedCompletionToolName(phase) {
54
+ return completionToolNamesForPhase(phase)[0];
55
+ }
56
+ export function artifactRootFor(worktreePath, workItemId) {
57
+ return path.join(worktreePath, ".forge", "work-items", safePathSegment(workItemId, "work item id"), "artifacts");
58
+ }
59
+ export function completionArtifactJsonPath(worktreePath, workItemId, phase) {
60
+ return path.join(artifactRootFor(worktreePath, workItemId), phaseFiles[phase].jsonFileName);
61
+ }
62
+ export function completionArtifactReportPath(worktreePath, workItemId, phase) {
63
+ return path.join(artifactRootFor(worktreePath, workItemId), phaseFiles[phase].reportFileName);
64
+ }
65
+ export async function prepareCompletionToolRuntime(options) {
66
+ const phase = options.phase ?? completionPhaseFromDispatch(options.dispatch);
67
+ const worktreePath = path.resolve(options.worktree.worktreePath);
68
+ const workItemId = options.dispatch.workItem.id;
69
+ const mcpArgs = buildCompletionToolServerArgs({
70
+ worktreePath,
71
+ workItemId,
72
+ phase,
73
+ producedBy: `${options.dispatch.execution.provider}-cli`,
74
+ developCycle: usesDevelopCycleCompletion(options.dispatch)
75
+ });
76
+ const runtime = {
77
+ serverName: toolServerName,
78
+ command: process.execPath,
79
+ args: [forgeBridgeCliBinPath(), ...mcpArgs],
80
+ env: {},
81
+ cleanup: async () => undefined
82
+ };
83
+ if (options.dispatch.execution.provider.trim().toLowerCase() !== "cursor") {
84
+ return runtime;
85
+ }
86
+ return await prepareCursorCompletionToolRuntime(worktreePath, runtime);
87
+ }
88
+ export function buildCompletionToolServerArgs(config) {
89
+ const args = [
90
+ "mcp",
91
+ "--worktree",
92
+ path.resolve(config.worktreePath),
93
+ "--work-item-id",
94
+ config.workItemId,
95
+ "--phase",
96
+ config.phase,
97
+ "--produced-by",
98
+ config.producedBy ?? "forge-completion-tool"
99
+ ];
100
+ if (config.developCycle) {
101
+ args.push("--develop-cycle");
102
+ }
103
+ return args;
104
+ }
105
+ export function codexMcpConfigArgs(runtime) {
106
+ const prefix = `mcp_servers.${runtime.serverName}`;
107
+ const args = [
108
+ "-c",
109
+ `${prefix}.command=${tomlString(runtime.command)}`,
110
+ "-c",
111
+ `${prefix}.args=${tomlStringArray(runtime.args)}`
112
+ ];
113
+ for (const [key, value] of Object.entries(runtime.env)) {
114
+ args.push("-c", `${prefix}.env.${key}=${tomlString(value)}`);
115
+ }
116
+ return args;
117
+ }
118
+ export function cursorMcpServerConfig(runtime) {
119
+ return {
120
+ command: runtime.command,
121
+ args: [...runtime.args],
122
+ env: runtime.env
123
+ };
124
+ }
125
+ export function completionPromptForPhase(phase) {
126
+ const toolName = expectedCompletionToolName(phase);
127
+ const alias = completionToolNamesForPhase(phase).find((name) => name !== toolName);
128
+ const artifactName = phaseFiles[phase].jsonFileName;
129
+ return `Forge completion tools are available through the MCP server named \`${toolServerName}\`.
130
+ Do not write phase artifacts by hand. When the phase is complete, call \`${toolName}(...)\`${alias ? ` (or \`${alias}\` if your client shows unqualified MCP tool names)` : ""} with the required evidence payload.
131
+ That tool writes \`.forge/work-items/{workItemId}/artifacts/${artifactName}\` and returns the artifact reference that Forge will mirror through the existing phase-artifact pipeline.
132
+ If the work cannot pass, call the same tool with a non-pass status and explicit blockers instead of reporting completion in prose only.`;
133
+ }
134
+ export function completionPromptForDevelopCycle(phase) {
135
+ return `Forge completion tools are available through the MCP server named \`${toolServerName}\`.
136
+ Do not write phase artifacts by hand. The tool server enforces phase order and starts this run in \`${phase}\`.
137
+ Complete the dev cycle in order inside this same provider session:
138
+ 1. Call \`forge.complete_implementation(...)\` when implementation evidence is ready.
139
+ 2. Follow the returned self-review instruction, then call \`forge.complete_self_review(...)\`.
140
+ 3. Follow the returned validation instruction, then call \`forge.complete_validation(...)\`.
141
+ Each tool writes the canonical artifact under \`.forge/work-items/{workItemId}/artifacts/\` and returns the next-phase instruction Forge expects you to follow.
142
+ If the current phase cannot pass, call the current phase tool with a non-pass status and explicit blockers instead of reporting completion in prose only.`;
143
+ }
144
+ export function completionToolNamesForServer(config) {
145
+ return config.developCycle
146
+ ? completionPhaseOrder.flatMap((phase) => completionToolNamesForPhase(phase))
147
+ : completionToolNamesForPhase(config.phase);
148
+ }
149
+ export class CompletionToolServerSession {
150
+ config;
151
+ currentPhase;
152
+ completed = false;
153
+ constructor(config) {
154
+ this.config = config;
155
+ this.currentPhase = config.phase;
156
+ }
157
+ get instructions() {
158
+ return this.config.developCycle
159
+ ? completionPromptForDevelopCycle(this.currentPhase)
160
+ : completionPromptForPhase(this.currentPhase);
161
+ }
162
+ toolDefinitions() {
163
+ return completionToolNamesForServer(this.config).map((name) => toolDefinition(name, phaseFromToolName(name)));
164
+ }
165
+ async callTool(toolName, input) {
166
+ const requestedPhase = phaseFromToolName(toolName);
167
+ if (this.completed) {
168
+ throw new Error("Forge develop cycle is already complete; no more phase completion calls are accepted.");
169
+ }
170
+ if (requestedPhase !== this.currentPhase) {
171
+ throw new Error(`Tool ${toolName} is out of order. Current Forge phase is ${this.currentPhase}.`);
172
+ }
173
+ const completedPhase = this.currentPhase;
174
+ const reference = await writeCompletionArtifact({ ...this.config, phase: completedPhase }, toolName, input);
175
+ const status = reference.status.trim().toLowerCase();
176
+ const pass = status === "pass";
177
+ const nextPhase = pass
178
+ ? this.advanceAfterPass(completedPhase)
179
+ : completedPhase;
180
+ const nextInstruction = completionToolNextInstruction(completedPhase, status, nextPhase);
181
+ const reason = pass
182
+ ? `${completedPhase} completion artifact accepted.`
183
+ : `${completedPhase} completion artifact recorded with status ${status}; rework is required before phase progression.`;
184
+ return {
185
+ content: [
186
+ {
187
+ type: "text",
188
+ text: `Forge recorded ${reference.phase} completion artifact ${path.relative(reference.repoPath, reference.path)} (${reference.status}, ${reference.contentHash}). ${nextInstruction}`
189
+ }
190
+ ],
191
+ structuredContent: {
192
+ ...reference,
193
+ gate: {
194
+ phase: completedPhase,
195
+ status,
196
+ allowed: pass,
197
+ reason
198
+ },
199
+ nextPhase,
200
+ nextInstruction
201
+ }
202
+ };
203
+ }
204
+ advanceAfterPass(phase) {
205
+ const next = nextPhaseAfter(phase);
206
+ if (!this.config.developCycle) {
207
+ return next ?? "dev_test_ready";
208
+ }
209
+ if (next) {
210
+ this.currentPhase = next;
211
+ return next;
212
+ }
213
+ this.completed = true;
214
+ return "dev_test_ready";
215
+ }
216
+ }
217
+ export async function writeCompletionArtifact(config, toolName, input) {
218
+ const phase = phaseFromToolName(toolName);
219
+ if (phase !== config.phase) {
220
+ throw new Error(`Tool ${toolName} is not allowed for phase ${config.phase}.`);
221
+ }
222
+ const payload = asRecord(input, "completion payload");
223
+ const status = normalizeCompletionStatus(requiredString(payload, "status"));
224
+ const validationPayload = { ...payload, status };
225
+ validatePayloadShape(phase, validationPayload);
226
+ validateOptionalWorkItemId(validationPayload, config.workItemId);
227
+ const redactedPayload = redactSecrets(validationPayload, config.redactionSecrets ?? []);
228
+ const producedBy = optionalString(redactedPayload, "produced_by")
229
+ ?? optionalString(redactedPayload, "producedBy")
230
+ ?? config.producedBy
231
+ ?? "forge-completion-tool";
232
+ const artifactWithoutHash = normalizeArtifactPayload(redactedPayload, {
233
+ phase,
234
+ workItemId: config.workItemId,
235
+ producedBy,
236
+ status
237
+ });
238
+ const contentHash = sha256(stableStringify(artifactWithoutHash));
239
+ const artifact = {
240
+ ...artifactWithoutHash,
241
+ content_hash: contentHash
242
+ };
243
+ const artifactJson = `${JSON.stringify(artifact, null, 2)}\n`;
244
+ const report = reportMarkdown(redactedPayload, phase, status, producedBy, contentHash);
245
+ const artifactPath = completionArtifactJsonPath(config.worktreePath, config.workItemId, phase);
246
+ const reportPath = completionArtifactReportPath(config.worktreePath, config.workItemId, phase);
247
+ await fs.mkdir(path.dirname(artifactPath), { recursive: true });
248
+ await writeFileAtomic(artifactPath, artifactJson);
249
+ await writeFileAtomic(reportPath, report);
250
+ return {
251
+ phase,
252
+ workItemId: config.workItemId,
253
+ repoPath: path.resolve(config.worktreePath),
254
+ path: artifactPath,
255
+ reportPath,
256
+ contentHash,
257
+ status,
258
+ producedBy
259
+ };
260
+ }
261
+ export async function readCompletionArtifact(worktreePath, workItemId, phase) {
262
+ const artifactPath = completionArtifactJsonPath(worktreePath, workItemId, phase);
263
+ const reportPath = completionArtifactReportPath(worktreePath, workItemId, phase);
264
+ const verdictJson = await readOptionalFile(artifactPath);
265
+ if (verdictJson === null) {
266
+ return null;
267
+ }
268
+ const parsed = parseJsonObject(verdictJson, artifactPath);
269
+ const report = await readOptionalFile(reportPath) ?? fallbackReport(parsed, phase, artifactPath);
270
+ return {
271
+ repoPath: path.resolve(worktreePath),
272
+ path: artifactPath,
273
+ verdictJson,
274
+ report,
275
+ producedBy: optionalString(parsed, "produced_by") ?? optionalString(parsed, "producedBy") ?? "forge-completion-tool",
276
+ status: optionalString(parsed, "status") ?? "unknown",
277
+ phase
278
+ };
279
+ }
280
+ export async function runCompletionMcpServer(config) {
281
+ const session = new CompletionToolServerSession(config);
282
+ process.stdin.setEncoding("utf8");
283
+ let buffer = "";
284
+ for await (const chunk of process.stdin) {
285
+ buffer += chunk;
286
+ let newlineIndex = buffer.indexOf("\n");
287
+ while (newlineIndex >= 0) {
288
+ const rawLine = buffer.slice(0, newlineIndex).trim();
289
+ buffer = buffer.slice(newlineIndex + 1);
290
+ if (rawLine.length > 0) {
291
+ await handleJsonRpcLine(session, rawLine);
292
+ }
293
+ newlineIndex = buffer.indexOf("\n");
294
+ }
295
+ }
296
+ const trailing = buffer.trim();
297
+ if (trailing.length > 0) {
298
+ await handleJsonRpcLine(session, trailing);
299
+ }
300
+ }
301
+ export function parseCompletionMcpArgs(argv) {
302
+ let worktreePath = null;
303
+ let workItemId = null;
304
+ let phase = "implementation";
305
+ let producedBy;
306
+ let developCycle = false;
307
+ for (let index = 1; index < argv.length; index += 1) {
308
+ const arg = argv[index];
309
+ if (arg === "--worktree") {
310
+ worktreePath = argv[++index] ?? null;
311
+ }
312
+ else if (arg === "--work-item-id") {
313
+ workItemId = argv[++index] ?? null;
314
+ }
315
+ else if (arg === "--phase") {
316
+ const rawPhase = argv[++index];
317
+ const parsedPhase = parseStrictCompletionPhase(rawPhase);
318
+ if (!parsedPhase) {
319
+ return { ok: false, error: "mcp --phase must be one of: implementation, self-review, validation." };
320
+ }
321
+ phase = parsedPhase;
322
+ }
323
+ else if (arg === "--produced-by") {
324
+ producedBy = argv[++index] ?? undefined;
325
+ }
326
+ else if (arg === "--develop-cycle") {
327
+ developCycle = true;
328
+ }
329
+ else {
330
+ return { ok: false, error: `Unknown mcp argument: ${arg}` };
331
+ }
332
+ }
333
+ if (!worktreePath?.trim() || !workItemId?.trim()) {
334
+ return { ok: false, error: "mcp requires --worktree and --work-item-id." };
335
+ }
336
+ return {
337
+ ok: true,
338
+ config: {
339
+ worktreePath: path.resolve(worktreePath),
340
+ workItemId,
341
+ phase,
342
+ producedBy,
343
+ redactionSecrets: parseCompletionRedactionSecrets(process.env[completionRedactionSecretsEnv]),
344
+ developCycle
345
+ }
346
+ };
347
+ }
348
+ export function completionRedactionEnv(secrets) {
349
+ const filtered = secrets.filter((secret) => secret.length > 0);
350
+ return filtered.length > 0
351
+ ? { [completionRedactionSecretsEnv]: JSON.stringify(filtered) }
352
+ : {};
353
+ }
354
+ async function prepareCursorCompletionToolRuntime(worktreePath, runtime) {
355
+ const cursorDir = path.join(worktreePath, ".cursor");
356
+ const configPath = path.join(cursorDir, "mcp.json");
357
+ const original = await fs.readFile(configPath, "utf8").then((content) => ({ existed: true, content }), () => ({ existed: false, content: "" }));
358
+ const existing = original.existed ? parseOptionalJsonObject(original.content, configPath) : {};
359
+ const servers = asMutableRecord(existing.mcpServers);
360
+ servers[runtime.serverName] = cursorMcpServerConfig(runtime);
361
+ const next = {
362
+ ...existing,
363
+ mcpServers: servers
364
+ };
365
+ await fs.mkdir(cursorDir, { recursive: true });
366
+ await writeFileAtomic(configPath, `${JSON.stringify(next, null, 2)}\n`);
367
+ return {
368
+ ...runtime,
369
+ cursorMcpConfigPath: configPath,
370
+ cleanup: async () => {
371
+ if (original.existed) {
372
+ await writeFileAtomic(configPath, original.content);
373
+ }
374
+ else {
375
+ await fs.rm(configPath, { force: true });
376
+ await fs.rmdir(cursorDir).catch(() => undefined);
377
+ }
378
+ }
379
+ };
380
+ }
381
+ async function handleJsonRpcLine(session, rawLine) {
382
+ let message;
383
+ try {
384
+ message = JSON.parse(rawLine);
385
+ }
386
+ catch (error) {
387
+ writeJsonRpc({ jsonrpc: "2.0", id: null, error: rpcError(-32700, errorMessage(error)) });
388
+ return;
389
+ }
390
+ const request = asJsonRpcMessage(message);
391
+ if (!request) {
392
+ writeJsonRpc({ jsonrpc: "2.0", id: null, error: rpcError(-32600, "Invalid JSON-RPC message.") });
393
+ return;
394
+ }
395
+ if (request.id === undefined) {
396
+ return;
397
+ }
398
+ try {
399
+ const result = await handleJsonRpcRequest(session, request.method, request.params);
400
+ writeJsonRpc({ jsonrpc: "2.0", id: request.id, result });
401
+ }
402
+ catch (error) {
403
+ writeJsonRpc({ jsonrpc: "2.0", id: request.id, error: rpcError(-32000, errorMessage(error)) });
404
+ }
405
+ }
406
+ async function handleJsonRpcRequest(session, method, params) {
407
+ if (method === "initialize") {
408
+ const initializeParams = params && typeof params === "object" && !Array.isArray(params)
409
+ ? params
410
+ : {};
411
+ return {
412
+ protocolVersion: typeof initializeParams.protocolVersion === "string" ? initializeParams.protocolVersion : "2025-06-18",
413
+ capabilities: {
414
+ tools: {}
415
+ },
416
+ serverInfo: {
417
+ name: "forge-completion-tools",
418
+ version: "0.1.0"
419
+ },
420
+ instructions: session.instructions
421
+ };
422
+ }
423
+ if (method === "ping") {
424
+ return {};
425
+ }
426
+ if (method === "tools/list") {
427
+ return {
428
+ tools: session.toolDefinitions()
429
+ };
430
+ }
431
+ if (method === "tools/call") {
432
+ const call = asRecord(params, "tools/call params");
433
+ const name = requiredString(call, "name");
434
+ const args = call.arguments ?? {};
435
+ return await session.callTool(name, args);
436
+ }
437
+ throw new Error(`Unsupported MCP method: ${method}`);
438
+ }
439
+ function toolDefinition(name, phase) {
440
+ return {
441
+ name,
442
+ description: toolDescription(phase),
443
+ inputSchema: toolInputSchema(phase)
444
+ };
445
+ }
446
+ function nextPhaseAfter(phase) {
447
+ const index = completionPhaseOrder.indexOf(phase);
448
+ return index >= 0 ? completionPhaseOrder[index + 1] ?? null : null;
449
+ }
450
+ function completionToolNextInstruction(completedPhase, status, nextPhase) {
451
+ if (status !== "pass") {
452
+ return `Stay in ${completedPhase}, address the recorded blockers, then call ${expectedCompletionToolName(completedPhase)} again.`;
453
+ }
454
+ if (nextPhase === "self_review") {
455
+ return "Implementation accepted. Begin self-review: inspect the diff, apply safe fixes, and call forge.complete_self_review with review evidence.";
456
+ }
457
+ if (nextPhase === "validation") {
458
+ return "Self-review accepted. Begin validation: run deterministic checks, prove acceptance criteria, and call forge.complete_validation with command evidence.";
459
+ }
460
+ return "Validation accepted. Forge has the structured evidence needed to move this work item to QA.";
461
+ }
462
+ function toolDescription(phase) {
463
+ if (phase === "implementation") {
464
+ return "Complete Forge implementation by writing implementation.completeness.json with scoped changed-file, verification, acceptance-criteria, gap, and next-phase evidence.";
465
+ }
466
+ if (phase === "self_review") {
467
+ return "Complete Forge self-review by writing self-review.report.json with diff review, findings, security, edge-case, design, deterministic-comparison, fixes, and remaining-risk evidence.";
468
+ }
469
+ return "Complete Forge validation by writing validation.verdict.json with command, build/test/lint/design, drift, acceptance-criteria, and ready-for-dev-test evidence.";
470
+ }
471
+ function toolInputSchema(phase) {
472
+ const baseProperties = {
473
+ status: {
474
+ type: "string",
475
+ enum: ["pass", "needs_rework", "fail", "failed"],
476
+ description: "Use pass only when the phase evidence satisfies Forge's gate. Use needs_rework/fail/failed with explicit blockers or findings otherwise."
477
+ },
478
+ summary: { type: "string", description: "Concise phase summary." },
479
+ report_markdown: { type: "string", description: "Optional human-readable report. Forge will generate a fallback report if omitted." },
480
+ blockers: {
481
+ type: "array",
482
+ description: "Required for non-pass status unless findings, failures, known_gaps, remaining_risks, or commands provide explicit evidence.",
483
+ items: evidenceObjectSchema(["reason"], {
484
+ reason: { type: "string" },
485
+ owner: { type: "string" },
486
+ next_step: { type: "string" }
487
+ })
488
+ }
489
+ };
490
+ if (phase === "implementation") {
491
+ return objectToolSchema({
492
+ ...baseProperties,
493
+ plan: {
494
+ type: "array",
495
+ description: "Required for pass. Ordered implementation steps with status.",
496
+ items: evidenceObjectSchema(["step", "status"], {
497
+ step: { type: "string" },
498
+ status: { type: "string" },
499
+ evidence: { type: "string" }
500
+ })
501
+ },
502
+ changed_files: {
503
+ type: "array",
504
+ description: "Required for pass. Must be non-empty unless known_gaps explains why.",
505
+ items: evidenceObjectSchema(["path", "summary"], {
506
+ path: { type: "string" },
507
+ summary: { type: "string" }
508
+ })
509
+ },
510
+ tests_added_or_updated: {
511
+ type: "array",
512
+ description: "Required for pass. Use an object explaining why no tests were needed for docs/config-only work if empty would be ambiguous.",
513
+ items: evidenceObjectSchema(["path", "summary"], {
514
+ path: { type: "string" },
515
+ summary: { type: "string" }
516
+ })
517
+ },
518
+ docs_added_or_updated: {
519
+ type: "array",
520
+ description: "Required for pass. Use an object explaining why no docs changed when appropriate.",
521
+ items: evidenceObjectSchema(["path", "summary"], {
522
+ path: { type: "string" },
523
+ summary: { type: "string" }
524
+ })
525
+ },
526
+ verification: {
527
+ type: "array",
528
+ description: "Implementation verification evidence. Objects should include command, exit_code, status, and summary when command-based.",
529
+ items: commandEvidenceSchema()
530
+ },
531
+ acceptance_criteria: acceptanceCriteriaSchema(),
532
+ known_gaps: {
533
+ type: "array",
534
+ description: "Required for pass, even when empty. If changed_files/tests/docs arrays are empty, explain why here.",
535
+ items: evidenceObjectSchema(["summary"], {
536
+ summary: { type: "string" },
537
+ impact: { type: "string" },
538
+ follow_up: { type: "string" }
539
+ })
540
+ },
541
+ next_phase_context: {
542
+ description: "Required for pass. Context the self-review phase should carry forward."
543
+ }
544
+ }, ["status", "summary"], "For status pass, include plan, changed_files, tests_added_or_updated, docs_added_or_updated, acceptance_criteria, known_gaps, and next_phase_context. Acceptance criteria entries must use status: satisfied plus evidence.");
545
+ }
546
+ if (phase === "self_review") {
547
+ return objectToolSchema({
548
+ ...baseProperties,
549
+ reviewed_diff: { type: "boolean", description: "Required for pass. True after reviewing the implementation diff." },
550
+ findings: {
551
+ type: "array",
552
+ description: "Required for pass, even when empty. Include finding objects for issues discovered during review.",
553
+ items: evidenceObjectSchema(["severity", "summary", "status"], {
554
+ severity: { type: "string" },
555
+ summary: { type: "string" },
556
+ status: { type: "string" },
557
+ file: { type: "string" }
558
+ })
559
+ },
560
+ security_review: reviewSectionSchema(),
561
+ edge_case_review: reviewSectionSchema(),
562
+ design_review: {
563
+ ...reviewSectionSchema(),
564
+ properties: {
565
+ ...reviewSectionProperties(),
566
+ screenshots: { type: "array", items: evidenceObjectSchema(["path"], { path: { type: "string" }, summary: { type: "string" } }) }
567
+ }
568
+ },
569
+ deterministic_comparison: {
570
+ type: "object",
571
+ description: "Required for pass. Evidence that the reviewed diff matches the intended deterministic change.",
572
+ additionalProperties: true,
573
+ properties: {
574
+ status: { type: "string" },
575
+ command_or_tool: { type: "string" },
576
+ notes: { type: "string" }
577
+ }
578
+ },
579
+ fixes_applied: {
580
+ type: "array",
581
+ description: "Required for pass, even when empty.",
582
+ items: evidenceObjectSchema(["summary"], { summary: { type: "string" }, path: { type: "string" } })
583
+ },
584
+ remaining_risks: {
585
+ type: "array",
586
+ description: "Required for pass, even when empty.",
587
+ items: evidenceObjectSchema(["summary"], { summary: { type: "string" }, mitigation: { type: "string" } })
588
+ }
589
+ }, ["status", "summary"], "For status pass, include reviewed_diff, findings, security_review, edge_case_review, design_review, deterministic_comparison, fixes_applied, and remaining_risks.");
590
+ }
591
+ return objectToolSchema({
592
+ ...baseProperties,
593
+ commands: {
594
+ type: "array",
595
+ description: "Required for pass. Include at least one deterministic validation command with exit_code: 0 and status: pass.",
596
+ items: commandEvidenceSchema()
597
+ },
598
+ acceptance_criteria: acceptanceCriteriaSchema(),
599
+ build_status: { type: "string", description: "Required for pass. pass/fail/needs_rework/not_applicable." },
600
+ test_status: { type: "string", description: "Required for pass. pass/fail/needs_rework/not_applicable." },
601
+ lint_status: { type: "string", description: "Required for pass. pass/fail/needs_rework/not_applicable." },
602
+ design_status: { type: "string", description: "Required for pass. pass/fail/needs_rework/not_applicable." },
603
+ drift_checks: {
604
+ type: "array",
605
+ description: "Required for pass. Deterministic checks for unintended drift.",
606
+ items: evidenceObjectSchema(["command_or_tool", "status", "summary"], {
607
+ command_or_tool: { type: "string" },
608
+ status: { type: "string" },
609
+ summary: { type: "string" }
610
+ })
611
+ },
612
+ ready_for_dev_test: { type: "boolean", description: "Required for pass and must be true." }
613
+ }, ["status", "summary"], "For status pass, commands must include at least one passing deterministic command, acceptance criteria entries must use status: satisfied plus evidence, and ready_for_dev_test must be true.");
614
+ }
615
+ function objectToolSchema(properties, required, description) {
616
+ return {
617
+ type: "object",
618
+ additionalProperties: true,
619
+ description,
620
+ properties,
621
+ required: [...required]
622
+ };
623
+ }
624
+ function evidenceObjectSchema(required, properties) {
625
+ return {
626
+ type: "object",
627
+ additionalProperties: true,
628
+ properties,
629
+ required: [...required]
630
+ };
631
+ }
632
+ function commandEvidenceSchema() {
633
+ return evidenceObjectSchema(["command", "exit_code", "status", "summary"], {
634
+ command: { type: "string" },
635
+ exit_code: { type: "number" },
636
+ status: { type: "string" },
637
+ summary: { type: "string" }
638
+ });
639
+ }
640
+ function acceptanceCriteriaSchema() {
641
+ return {
642
+ type: "array",
643
+ description: "Required for pass. Every entry must be an object with status: satisfied and non-empty evidence.",
644
+ items: evidenceObjectSchema(["criterion", "evidence", "status"], {
645
+ criterion: { type: "string" },
646
+ evidence: { type: "string" },
647
+ status: { type: "string", enum: ["satisfied"] }
648
+ })
649
+ };
650
+ }
651
+ function reviewSectionSchema() {
652
+ return {
653
+ type: "object",
654
+ description: "Required for pass. Use status pass/fail/needs_rework/not_applicable plus notes.",
655
+ additionalProperties: true,
656
+ properties: reviewSectionProperties()
657
+ };
658
+ }
659
+ function reviewSectionProperties() {
660
+ return {
661
+ status: { type: "string" },
662
+ notes: { type: "string" }
663
+ };
664
+ }
665
+ function normalizeCompletionStatus(value) {
666
+ const normalized = value.trim().toLowerCase().replace(/-/g, "_");
667
+ const canonical = normalized === "blocked" || normalized === "needsrework"
668
+ ? "needs_rework"
669
+ : normalized === "failed"
670
+ ? "fail"
671
+ : normalized;
672
+ if (canonical !== "pass" && canonical !== "fail" && canonical !== "needs_rework") {
673
+ throw new Error("status must be pass, fail, or needs_rework.");
674
+ }
675
+ return canonical;
676
+ }
677
+ function validatePayloadShape(phase, payload) {
678
+ const status = normalizeCompletionStatus(requiredString(payload, "status"));
679
+ if (!status) {
680
+ throw new Error("status is required.");
681
+ }
682
+ const pass = status === "pass";
683
+ requireField(payload, "summary");
684
+ if (!pass) {
685
+ requireNonPassEvidence(payload);
686
+ return;
687
+ }
688
+ if (phase === "implementation") {
689
+ requireArray(payload, "plan");
690
+ requireArray(payload, "changed_files");
691
+ requireArray(payload, "tests_added_or_updated");
692
+ requireArray(payload, "docs_added_or_updated");
693
+ requireArray(payload, "acceptance_criteria");
694
+ requireArray(payload, "known_gaps");
695
+ requireField(payload, "next_phase_context");
696
+ requireImplementationEvidence(payload);
697
+ requireSatisfiedAcceptanceCriteria(payload);
698
+ return;
699
+ }
700
+ if (phase === "self_review") {
701
+ requireBoolean(payload, "reviewed_diff");
702
+ requireArray(payload, "findings");
703
+ requireObject(payload, "security_review");
704
+ requireObject(payload, "edge_case_review");
705
+ requireObject(payload, "design_review");
706
+ requireObject(payload, "deterministic_comparison");
707
+ requireArray(payload, "fixes_applied");
708
+ requireArray(payload, "remaining_risks");
709
+ return;
710
+ }
711
+ requireArray(payload, "commands");
712
+ requireArray(payload, "acceptance_criteria");
713
+ requireField(payload, "build_status");
714
+ requireField(payload, "test_status");
715
+ requireField(payload, "lint_status");
716
+ requireField(payload, "design_status");
717
+ requireArray(payload, "drift_checks");
718
+ requireBoolean(payload, "ready_for_dev_test");
719
+ if (payload.ready_for_dev_test !== true) {
720
+ throw new Error("validation pass requires ready_for_dev_test: true.");
721
+ }
722
+ requirePassingCommands(payload);
723
+ requireSatisfiedAcceptanceCriteria(payload);
724
+ }
725
+ function validateOptionalWorkItemId(payload, expectedWorkItemId) {
726
+ const actual = optionalString(payload, "work_item_id") ?? optionalString(payload, "workItemId");
727
+ if (actual && actual !== expectedWorkItemId) {
728
+ throw new Error(`completion payload work item ${actual} does not match dispatch work item ${expectedWorkItemId}.`);
729
+ }
730
+ }
731
+ function normalizeArtifactPayload(payload, defaults) {
732
+ const normalized = stripUndefined({ ...payload });
733
+ delete normalized.report_markdown;
734
+ delete normalized.content_hash;
735
+ delete normalized.contentHash;
736
+ normalized.schema_version = numberOrDefault(normalized.schema_version, 1);
737
+ normalized.phase = defaults.phase;
738
+ normalized.work_item_id = defaults.workItemId;
739
+ normalized.status = defaults.status;
740
+ normalized.produced_by = defaults.producedBy;
741
+ return normalized;
742
+ }
743
+ function reportMarkdown(payload, phase, status, producedBy, contentHash) {
744
+ const explicit = optionalString(payload, "report_markdown");
745
+ if (explicit) {
746
+ return explicit.endsWith("\n") ? explicit : `${explicit}\n`;
747
+ }
748
+ const summary = optionalString(payload, "summary") ?? "No summary provided.";
749
+ return `# Forge ${phase.replace(/_/g, " ")} completion
750
+
751
+ Status: ${status}
752
+ Produced by: ${producedBy}
753
+ Content hash: ${contentHash}
754
+
755
+ ## Summary
756
+
757
+ ${summary}
758
+ `;
759
+ }
760
+ function parseStrictCompletionPhase(value) {
761
+ const normalized = (value ?? "").trim().toLowerCase().replace(/-/g, "_");
762
+ if (normalized === "implementation" || normalized === "implement" || normalized === "develop") {
763
+ return "implementation";
764
+ }
765
+ if (normalized === "self_review" || normalized === "selfreview" || normalized === "review") {
766
+ return "self_review";
767
+ }
768
+ if (normalized === "validate" || normalized === "validation") {
769
+ return "validation";
770
+ }
771
+ return null;
772
+ }
773
+ function parseCompletionRedactionSecrets(raw) {
774
+ if (!raw?.trim()) {
775
+ return [];
776
+ }
777
+ try {
778
+ const parsed = JSON.parse(raw);
779
+ if (!Array.isArray(parsed)) {
780
+ return [];
781
+ }
782
+ return parsed.filter((value) => typeof value === "string" && value.length > 0);
783
+ }
784
+ catch {
785
+ return [];
786
+ }
787
+ }
788
+ function phaseFromToolName(toolName) {
789
+ const normalized = toolName.trim().toLowerCase();
790
+ if (normalized === "forge.complete_implementation" || normalized === "complete_implementation") {
791
+ return "implementation";
792
+ }
793
+ if (normalized === "forge.complete_self_review" || normalized === "complete_self_review") {
794
+ return "self_review";
795
+ }
796
+ if (normalized === "forge.complete_validation" || normalized === "complete_validation") {
797
+ return "validation";
798
+ }
799
+ throw new Error(`Unknown Forge completion tool: ${toolName}`);
800
+ }
801
+ function requireField(payload, key) {
802
+ if (payload[key] === undefined || payload[key] === null || (typeof payload[key] === "string" && payload[key].trim().length === 0)) {
803
+ throw new Error(`${key} is required.`);
804
+ }
805
+ }
806
+ function requireNonPassEvidence(payload) {
807
+ const evidenceKeys = ["blockers", "known_gaps", "findings", "remaining_risks", "commands", "failures"];
808
+ const hasEvidence = evidenceKeys.some((key) => {
809
+ const value = payload[key];
810
+ if (typeof value === "string") {
811
+ return value.trim().length > 0;
812
+ }
813
+ if (Array.isArray(value)) {
814
+ return value.length > 0;
815
+ }
816
+ return Boolean(value && typeof value === "object");
817
+ });
818
+ if (!hasEvidence) {
819
+ throw new Error("non-pass completion requires explicit blockers, findings, failures, or known gaps.");
820
+ }
821
+ }
822
+ function requireImplementationEvidence(payload) {
823
+ const knownGaps = payload.known_gaps;
824
+ const hasKnownGap = Array.isArray(knownGaps) && knownGaps.length > 0;
825
+ for (const key of ["changed_files", "tests_added_or_updated", "docs_added_or_updated"]) {
826
+ const value = payload[key];
827
+ if (Array.isArray(value) && value.length === 0 && !hasKnownGap) {
828
+ throw new Error(`${key} is empty; known_gaps must explain why.`);
829
+ }
830
+ }
831
+ }
832
+ function requirePassingCommands(payload) {
833
+ const commands = payload.commands;
834
+ if (!Array.isArray(commands)) {
835
+ return;
836
+ }
837
+ if (commands.length === 0) {
838
+ throw new Error("commands must include at least one deterministic validation command.");
839
+ }
840
+ for (const [index, command] of commands.entries()) {
841
+ if (!command || typeof command !== "object" || Array.isArray(command)) {
842
+ throw new Error(`commands[${index}] must be an object.`);
843
+ }
844
+ const record = command;
845
+ if (typeof record.command !== "string" || record.command.trim().length === 0) {
846
+ throw new Error(`commands[${index}].command must be a non-empty string.`);
847
+ }
848
+ if (typeof record.summary !== "string" || record.summary.trim().length === 0) {
849
+ throw new Error(`commands[${index}].summary must be a non-empty string.`);
850
+ }
851
+ if (typeof record.exit_code !== "number" || !Number.isFinite(record.exit_code)) {
852
+ throw new Error(`commands[${index}].exit_code must be a finite number.`);
853
+ }
854
+ if (record.exit_code !== 0) {
855
+ throw new Error(`commands[${index}].exit_code must be 0 when status is pass.`);
856
+ }
857
+ const status = typeof record.status === "string" ? record.status.trim().toLowerCase() : "";
858
+ if (status !== "pass") {
859
+ throw new Error(`commands[${index}].status must be pass.`);
860
+ }
861
+ }
862
+ }
863
+ function requireArray(payload, key) {
864
+ if (!Array.isArray(payload[key])) {
865
+ throw new Error(`${key} must be an array.`);
866
+ }
867
+ }
868
+ function requireObject(payload, key) {
869
+ const value = payload[key];
870
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
871
+ throw new Error(`${key} must be an object.`);
872
+ }
873
+ }
874
+ function requireBoolean(payload, key) {
875
+ if (typeof payload[key] !== "boolean") {
876
+ throw new Error(`${key} must be a boolean.`);
877
+ }
878
+ }
879
+ function requireSatisfiedAcceptanceCriteria(payload) {
880
+ const criteria = payload.acceptance_criteria;
881
+ if (!Array.isArray(criteria)) {
882
+ return;
883
+ }
884
+ for (const [index, criterion] of criteria.entries()) {
885
+ if (!criterion || typeof criterion !== "object" || Array.isArray(criterion)) {
886
+ throw new Error(`acceptance_criteria[${index}] must be an object.`);
887
+ }
888
+ const record = criterion;
889
+ const status = optionalString(record, "status")?.toLowerCase();
890
+ const evidence = optionalString(record, "evidence");
891
+ if (status !== "satisfied" || !evidence) {
892
+ throw new Error(`acceptance_criteria[${index}] must be satisfied with evidence for status pass.`);
893
+ }
894
+ }
895
+ }
896
+ function fallbackReport(parsed, phase, artifactPath) {
897
+ return `# Forge ${phase.replace(/_/g, " ")} completion
898
+
899
+ Artifact: ${artifactPath}
900
+ Status: ${optionalString(parsed, "status") ?? "unknown"}
901
+ `;
902
+ }
903
+ function parseJsonObject(content, label) {
904
+ try {
905
+ return asRecord(JSON.parse(content), label);
906
+ }
907
+ catch (error) {
908
+ throw new Error(`${label} is not valid JSON: ${errorMessage(error)}`);
909
+ }
910
+ }
911
+ function parseOptionalJsonObject(content, label) {
912
+ if (!content.trim()) {
913
+ return {};
914
+ }
915
+ return parseJsonObject(content, label);
916
+ }
917
+ function asRecord(value, label) {
918
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
919
+ throw new Error(`${label} must be an object.`);
920
+ }
921
+ return value;
922
+ }
923
+ function asMutableRecord(value) {
924
+ return value && typeof value === "object" && !Array.isArray(value)
925
+ ? { ...value }
926
+ : {};
927
+ }
928
+ function requiredString(source, key) {
929
+ const value = source[key];
930
+ if (typeof value !== "string" || value.trim().length === 0) {
931
+ throw new Error(`${key} must be a non-empty string.`);
932
+ }
933
+ return value;
934
+ }
935
+ function optionalString(source, key) {
936
+ const value = source[key];
937
+ return typeof value === "string" && value.trim().length > 0 ? value : null;
938
+ }
939
+ function numberOrDefault(value, fallback) {
940
+ return typeof value === "number" && Number.isFinite(value) ? value : fallback;
941
+ }
942
+ function stripUndefined(value) {
943
+ return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== undefined));
944
+ }
945
+ function safePathSegment(value, label) {
946
+ if (!value.trim() || value.includes("/") || value.includes("\\") || value === "." || value === "..") {
947
+ throw new Error(`${label} is not a safe path segment.`);
948
+ }
949
+ return value;
950
+ }
951
+ async function readOptionalFile(filePath) {
952
+ return await fs.readFile(filePath, "utf8").catch((error) => {
953
+ if (isNodeError(error) && error.code === "ENOENT") {
954
+ return null;
955
+ }
956
+ throw error;
957
+ });
958
+ }
959
+ async function writeFileAtomic(filePath, content) {
960
+ await fs.mkdir(path.dirname(filePath), { recursive: true });
961
+ const tempPath = path.join(path.dirname(filePath), `.${path.basename(filePath)}.${process.pid}.${Date.now()}.tmp`);
962
+ await fs.writeFile(tempPath, content, "utf8");
963
+ await fs.rename(tempPath, filePath);
964
+ }
965
+ function stableStringify(value) {
966
+ return JSON.stringify(sortJson(value));
967
+ }
968
+ function sortJson(value) {
969
+ if (Array.isArray(value)) {
970
+ return value.map(sortJson);
971
+ }
972
+ if (value && typeof value === "object") {
973
+ return Object.fromEntries(Object.entries(value)
974
+ .filter(([, entry]) => entry !== undefined)
975
+ .sort(([left], [right]) => left.localeCompare(right))
976
+ .map(([key, entry]) => [key, sortJson(entry)]));
977
+ }
978
+ return value;
979
+ }
980
+ function sha256(content) {
981
+ return crypto.createHash("sha256").update(content).digest("hex");
982
+ }
983
+ function forgeBridgeCliBinPath() {
984
+ return fileURLToPath(new URL("../cli/bin.js", import.meta.url));
985
+ }
986
+ function tomlString(value) {
987
+ return JSON.stringify(value);
988
+ }
989
+ function tomlStringArray(values) {
990
+ return `[${values.map(tomlString).join(", ")}]`;
991
+ }
992
+ function writeJsonRpc(message) {
993
+ process.stdout.write(`${JSON.stringify(message)}\n`);
994
+ }
995
+ function rpcError(code, message) {
996
+ return { code, message };
997
+ }
998
+ function asJsonRpcMessage(value) {
999
+ if (!value || typeof value !== "object" || Array.isArray(value)) {
1000
+ return null;
1001
+ }
1002
+ const message = value;
1003
+ if (message.jsonrpc !== "2.0" || typeof message.method !== "string") {
1004
+ return null;
1005
+ }
1006
+ const id = message.id;
1007
+ return {
1008
+ id: typeof id === "string" || typeof id === "number" || id === null ? id : undefined,
1009
+ method: message.method,
1010
+ params: message.params
1011
+ };
1012
+ }
1013
+ function errorMessage(error) {
1014
+ return error instanceof Error ? error.message : String(error);
1015
+ }
1016
+ function isNodeError(error) {
1017
+ return error instanceof Error && "code" in error;
1018
+ }