@loucompanion/forge-bridge 0.1.1-dev.32d4dd5874ee → 0.1.1-dev.4f31cdd58cac
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 +32 -0
- package/dist/session/phase-completion.js +360 -79
- package/dist/session/provider-cli/provider-cli.service.d.ts +1 -0
- package/dist/session/provider-cli/provider-cli.service.js +351 -54
- package/dist/session/provider-cli/provider-usage.d.ts +25 -0
- package/dist/session/provider-cli/provider-usage.js +64 -0
- package/dist/session/session.types.d.ts +5 -0
- package/dist/session/session.types.js +11 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/worktree/artifact-injector.js +1 -4
- package/package.json +1 -1
|
@@ -19,6 +19,24 @@ export interface CompletionToolServerConfig {
|
|
|
19
19
|
readonly workItemId: string;
|
|
20
20
|
readonly phase: CompletionPhase;
|
|
21
21
|
readonly producedBy?: string;
|
|
22
|
+
readonly redactionSecrets?: readonly string[];
|
|
23
|
+
readonly developCycle?: boolean;
|
|
24
|
+
}
|
|
25
|
+
export interface CompletionToolCallResult {
|
|
26
|
+
readonly content: readonly {
|
|
27
|
+
readonly type: "text";
|
|
28
|
+
readonly text: string;
|
|
29
|
+
}[];
|
|
30
|
+
readonly structuredContent: CompletionArtifactReference & {
|
|
31
|
+
readonly gate: {
|
|
32
|
+
readonly phase: CompletionPhase;
|
|
33
|
+
readonly status: string;
|
|
34
|
+
readonly allowed: boolean;
|
|
35
|
+
readonly reason: string;
|
|
36
|
+
};
|
|
37
|
+
readonly nextPhase: CompletionPhase | "dev_test_ready";
|
|
38
|
+
readonly nextInstruction: string;
|
|
39
|
+
};
|
|
22
40
|
}
|
|
23
41
|
export interface CompletionArtifactReference {
|
|
24
42
|
readonly phase: CompletionPhase;
|
|
@@ -32,6 +50,7 @@ export interface CompletionArtifactReference {
|
|
|
32
50
|
}
|
|
33
51
|
export declare function completionPhaseFromDispatch(dispatch: RunOnceDispatchPacket): CompletionPhase;
|
|
34
52
|
export declare function normalizeCompletionPhase(value: string | null | undefined): CompletionPhase;
|
|
53
|
+
export declare function usesDevelopCycleCompletion(dispatch: RunOnceDispatchPacket): boolean;
|
|
35
54
|
export declare function completionToolNamesForPhase(phase: CompletionPhase): readonly string[];
|
|
36
55
|
export declare function expectedCompletionToolName(phase: CompletionPhase): string;
|
|
37
56
|
export declare function artifactRootFor(worktreePath: string, workItemId: string): string;
|
|
@@ -42,6 +61,18 @@ export declare function buildCompletionToolServerArgs(config: CompletionToolServ
|
|
|
42
61
|
export declare function codexMcpConfigArgs(runtime: CompletionToolRuntime): readonly string[];
|
|
43
62
|
export declare function cursorMcpServerConfig(runtime: CompletionToolRuntime): unknown;
|
|
44
63
|
export declare function completionPromptForPhase(phase: CompletionPhase): string;
|
|
64
|
+
export declare function completionPromptForDevelopCycle(phase: CompletionPhase): string;
|
|
65
|
+
export declare function completionToolNamesForServer(config: CompletionToolServerConfig): readonly string[];
|
|
66
|
+
export declare class CompletionToolServerSession {
|
|
67
|
+
private readonly config;
|
|
68
|
+
private currentPhase;
|
|
69
|
+
private completed;
|
|
70
|
+
constructor(config: CompletionToolServerConfig);
|
|
71
|
+
get instructions(): string;
|
|
72
|
+
toolDefinitions(): readonly unknown[];
|
|
73
|
+
callTool(toolName: string, input: unknown): Promise<CompletionToolCallResult>;
|
|
74
|
+
private advanceAfterPass;
|
|
75
|
+
}
|
|
45
76
|
export declare function writeCompletionArtifact(config: CompletionToolServerConfig, toolName: string, input: unknown): Promise<CompletionArtifactReference>;
|
|
46
77
|
export declare function readCompletionArtifact(worktreePath: string, workItemId: string, phase: CompletionPhase): Promise<RunnerDispatchArtifact | null>;
|
|
47
78
|
export declare function runCompletionMcpServer(config: CompletionToolServerConfig): Promise<void>;
|
|
@@ -52,3 +83,4 @@ export declare function parseCompletionMcpArgs(argv: readonly string[]): {
|
|
|
52
83
|
readonly ok: false;
|
|
53
84
|
readonly error: string;
|
|
54
85
|
};
|
|
86
|
+
export declare function completionRedactionEnv(secrets: readonly string[]): Readonly<Record<string, string>>;
|
|
@@ -2,7 +2,10 @@ import crypto from "node:crypto";
|
|
|
2
2
|
import fs from "node:fs/promises";
|
|
3
3
|
import path from "node:path";
|
|
4
4
|
import { fileURLToPath } from "node:url";
|
|
5
|
+
import { redactSecrets } from "../shared/redaction/redaction.js";
|
|
5
6
|
const toolServerName = "forge";
|
|
7
|
+
const completionRedactionSecretsEnv = "FORGE_COMPLETION_REDACTION_SECRETS";
|
|
8
|
+
const completionPhaseOrder = ["implementation", "self_review", "validation"];
|
|
6
9
|
const phaseFiles = {
|
|
7
10
|
implementation: {
|
|
8
11
|
jsonFileName: "implementation.completeness.json",
|
|
@@ -30,6 +33,15 @@ export function normalizeCompletionPhase(value) {
|
|
|
30
33
|
}
|
|
31
34
|
return "implementation";
|
|
32
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
|
+
}
|
|
33
45
|
export function completionToolNamesForPhase(phase) {
|
|
34
46
|
const base = phase === "implementation"
|
|
35
47
|
? "complete_implementation"
|
|
@@ -58,7 +70,8 @@ export async function prepareCompletionToolRuntime(options) {
|
|
|
58
70
|
worktreePath,
|
|
59
71
|
workItemId,
|
|
60
72
|
phase,
|
|
61
|
-
producedBy: `${options.dispatch.execution.provider}-cli
|
|
73
|
+
producedBy: `${options.dispatch.execution.provider}-cli`,
|
|
74
|
+
developCycle: usesDevelopCycleCompletion(options.dispatch)
|
|
62
75
|
});
|
|
63
76
|
const runtime = {
|
|
64
77
|
serverName: toolServerName,
|
|
@@ -73,7 +86,7 @@ export async function prepareCompletionToolRuntime(options) {
|
|
|
73
86
|
return await prepareCursorCompletionToolRuntime(worktreePath, runtime);
|
|
74
87
|
}
|
|
75
88
|
export function buildCompletionToolServerArgs(config) {
|
|
76
|
-
|
|
89
|
+
const args = [
|
|
77
90
|
"mcp",
|
|
78
91
|
"--worktree",
|
|
79
92
|
path.resolve(config.worktreePath),
|
|
@@ -84,6 +97,10 @@ export function buildCompletionToolServerArgs(config) {
|
|
|
84
97
|
"--produced-by",
|
|
85
98
|
config.producedBy ?? "forge-completion-tool"
|
|
86
99
|
];
|
|
100
|
+
if (config.developCycle) {
|
|
101
|
+
args.push("--develop-cycle");
|
|
102
|
+
}
|
|
103
|
+
return args;
|
|
87
104
|
}
|
|
88
105
|
export function codexMcpConfigArgs(runtime) {
|
|
89
106
|
const prefix = `mcp_servers.${runtime.serverName}`;
|
|
@@ -114,20 +131,105 @@ Do not write phase artifacts by hand. When the phase is complete, call \`${toolN
|
|
|
114
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.
|
|
115
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.`;
|
|
116
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
|
+
}
|
|
117
217
|
export async function writeCompletionArtifact(config, toolName, input) {
|
|
118
218
|
const phase = phaseFromToolName(toolName);
|
|
119
219
|
if (phase !== config.phase) {
|
|
120
220
|
throw new Error(`Tool ${toolName} is not allowed for phase ${config.phase}.`);
|
|
121
221
|
}
|
|
122
222
|
const payload = asRecord(input, "completion payload");
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
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")
|
|
127
230
|
?? config.producedBy
|
|
128
231
|
?? "forge-completion-tool";
|
|
129
|
-
const
|
|
130
|
-
const artifactWithoutHash = normalizeArtifactPayload(payload, {
|
|
232
|
+
const artifactWithoutHash = normalizeArtifactPayload(redactedPayload, {
|
|
131
233
|
phase,
|
|
132
234
|
workItemId: config.workItemId,
|
|
133
235
|
producedBy,
|
|
@@ -139,7 +241,7 @@ export async function writeCompletionArtifact(config, toolName, input) {
|
|
|
139
241
|
content_hash: contentHash
|
|
140
242
|
};
|
|
141
243
|
const artifactJson = `${JSON.stringify(artifact, null, 2)}\n`;
|
|
142
|
-
const report = reportMarkdown(
|
|
244
|
+
const report = reportMarkdown(redactedPayload, phase, status, producedBy, contentHash);
|
|
143
245
|
const artifactPath = completionArtifactJsonPath(config.worktreePath, config.workItemId, phase);
|
|
144
246
|
const reportPath = completionArtifactReportPath(config.worktreePath, config.workItemId, phase);
|
|
145
247
|
await fs.mkdir(path.dirname(artifactPath), { recursive: true });
|
|
@@ -176,6 +278,7 @@ export async function readCompletionArtifact(worktreePath, workItemId, phase) {
|
|
|
176
278
|
};
|
|
177
279
|
}
|
|
178
280
|
export async function runCompletionMcpServer(config) {
|
|
281
|
+
const session = new CompletionToolServerSession(config);
|
|
179
282
|
process.stdin.setEncoding("utf8");
|
|
180
283
|
let buffer = "";
|
|
181
284
|
for await (const chunk of process.stdin) {
|
|
@@ -185,14 +288,14 @@ export async function runCompletionMcpServer(config) {
|
|
|
185
288
|
const rawLine = buffer.slice(0, newlineIndex).trim();
|
|
186
289
|
buffer = buffer.slice(newlineIndex + 1);
|
|
187
290
|
if (rawLine.length > 0) {
|
|
188
|
-
await handleJsonRpcLine(
|
|
291
|
+
await handleJsonRpcLine(session, rawLine);
|
|
189
292
|
}
|
|
190
293
|
newlineIndex = buffer.indexOf("\n");
|
|
191
294
|
}
|
|
192
295
|
}
|
|
193
296
|
const trailing = buffer.trim();
|
|
194
297
|
if (trailing.length > 0) {
|
|
195
|
-
await handleJsonRpcLine(
|
|
298
|
+
await handleJsonRpcLine(session, trailing);
|
|
196
299
|
}
|
|
197
300
|
}
|
|
198
301
|
export function parseCompletionMcpArgs(argv) {
|
|
@@ -200,6 +303,7 @@ export function parseCompletionMcpArgs(argv) {
|
|
|
200
303
|
let workItemId = null;
|
|
201
304
|
let phase = "implementation";
|
|
202
305
|
let producedBy;
|
|
306
|
+
let developCycle = false;
|
|
203
307
|
for (let index = 1; index < argv.length; index += 1) {
|
|
204
308
|
const arg = argv[index];
|
|
205
309
|
if (arg === "--worktree") {
|
|
@@ -219,6 +323,9 @@ export function parseCompletionMcpArgs(argv) {
|
|
|
219
323
|
else if (arg === "--produced-by") {
|
|
220
324
|
producedBy = argv[++index] ?? undefined;
|
|
221
325
|
}
|
|
326
|
+
else if (arg === "--develop-cycle") {
|
|
327
|
+
developCycle = true;
|
|
328
|
+
}
|
|
222
329
|
else {
|
|
223
330
|
return { ok: false, error: `Unknown mcp argument: ${arg}` };
|
|
224
331
|
}
|
|
@@ -232,10 +339,18 @@ export function parseCompletionMcpArgs(argv) {
|
|
|
232
339
|
worktreePath: path.resolve(worktreePath),
|
|
233
340
|
workItemId,
|
|
234
341
|
phase,
|
|
235
|
-
producedBy
|
|
342
|
+
producedBy,
|
|
343
|
+
redactionSecrets: parseCompletionRedactionSecrets(process.env[completionRedactionSecretsEnv]),
|
|
344
|
+
developCycle
|
|
236
345
|
}
|
|
237
346
|
};
|
|
238
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
|
+
}
|
|
239
354
|
async function prepareCursorCompletionToolRuntime(worktreePath, runtime) {
|
|
240
355
|
const cursorDir = path.join(worktreePath, ".cursor");
|
|
241
356
|
const configPath = path.join(cursorDir, "mcp.json");
|
|
@@ -263,7 +378,7 @@ async function prepareCursorCompletionToolRuntime(worktreePath, runtime) {
|
|
|
263
378
|
}
|
|
264
379
|
};
|
|
265
380
|
}
|
|
266
|
-
async function handleJsonRpcLine(
|
|
381
|
+
async function handleJsonRpcLine(session, rawLine) {
|
|
267
382
|
let message;
|
|
268
383
|
try {
|
|
269
384
|
message = JSON.parse(rawLine);
|
|
@@ -281,14 +396,14 @@ async function handleJsonRpcLine(config, rawLine) {
|
|
|
281
396
|
return;
|
|
282
397
|
}
|
|
283
398
|
try {
|
|
284
|
-
const result = await handleJsonRpcRequest(
|
|
399
|
+
const result = await handleJsonRpcRequest(session, request.method, request.params);
|
|
285
400
|
writeJsonRpc({ jsonrpc: "2.0", id: request.id, result });
|
|
286
401
|
}
|
|
287
402
|
catch (error) {
|
|
288
403
|
writeJsonRpc({ jsonrpc: "2.0", id: request.id, error: rpcError(-32000, errorMessage(error)) });
|
|
289
404
|
}
|
|
290
405
|
}
|
|
291
|
-
async function handleJsonRpcRequest(
|
|
406
|
+
async function handleJsonRpcRequest(session, method, params) {
|
|
292
407
|
if (method === "initialize") {
|
|
293
408
|
const initializeParams = params && typeof params === "object" && !Array.isArray(params)
|
|
294
409
|
? params
|
|
@@ -302,7 +417,7 @@ async function handleJsonRpcRequest(config, method, params) {
|
|
|
302
417
|
name: "forge-completion-tools",
|
|
303
418
|
version: "0.1.0"
|
|
304
419
|
},
|
|
305
|
-
instructions:
|
|
420
|
+
instructions: session.instructions
|
|
306
421
|
};
|
|
307
422
|
}
|
|
308
423
|
if (method === "ping") {
|
|
@@ -310,23 +425,14 @@ async function handleJsonRpcRequest(config, method, params) {
|
|
|
310
425
|
}
|
|
311
426
|
if (method === "tools/list") {
|
|
312
427
|
return {
|
|
313
|
-
tools:
|
|
428
|
+
tools: session.toolDefinitions()
|
|
314
429
|
};
|
|
315
430
|
}
|
|
316
431
|
if (method === "tools/call") {
|
|
317
432
|
const call = asRecord(params, "tools/call params");
|
|
318
433
|
const name = requiredString(call, "name");
|
|
319
434
|
const args = call.arguments ?? {};
|
|
320
|
-
|
|
321
|
-
return {
|
|
322
|
-
content: [
|
|
323
|
-
{
|
|
324
|
-
type: "text",
|
|
325
|
-
text: `Forge recorded ${reference.phase} completion artifact ${path.relative(reference.repoPath, reference.path)} (${reference.status}, ${reference.contentHash}).`
|
|
326
|
-
}
|
|
327
|
-
],
|
|
328
|
-
structuredContent: reference
|
|
329
|
-
};
|
|
435
|
+
return await session.callTool(name, args);
|
|
330
436
|
}
|
|
331
437
|
throw new Error(`Unsupported MCP method: ${method}`);
|
|
332
438
|
}
|
|
@@ -337,6 +443,22 @@ function toolDefinition(name, phase) {
|
|
|
337
443
|
inputSchema: toolInputSchema(phase)
|
|
338
444
|
};
|
|
339
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 Dev Test.";
|
|
461
|
+
}
|
|
340
462
|
function toolDescription(phase) {
|
|
341
463
|
if (phase === "implementation") {
|
|
342
464
|
return "Complete Forge implementation by writing implementation.completeness.json with scoped changed-file, verification, acceptance-criteria, gap, and next-phase evidence.";
|
|
@@ -347,69 +469,213 @@ function toolDescription(phase) {
|
|
|
347
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.";
|
|
348
470
|
}
|
|
349
471
|
function toolInputSchema(phase) {
|
|
350
|
-
const
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
summary: { type: "string" },
|
|
356
|
-
report_markdown: { type: "string" },
|
|
357
|
-
blockers: { type: "array" }
|
|
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."
|
|
358
477
|
},
|
|
359
|
-
|
|
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
|
+
}
|
|
360
489
|
};
|
|
361
490
|
if (phase === "implementation") {
|
|
362
|
-
return {
|
|
363
|
-
...
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
known_gaps: { type: "array" },
|
|
373
|
-
next_phase_context: {}
|
|
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
|
+
})
|
|
374
501
|
},
|
|
375
|
-
|
|
376
|
-
|
|
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.");
|
|
377
545
|
}
|
|
378
546
|
if (phase === "self_review") {
|
|
379
|
-
return {
|
|
380
|
-
...
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
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
|
+
})
|
|
391
559
|
},
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
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
|
+
})
|
|
407
611
|
},
|
|
408
|
-
|
|
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]
|
|
409
630
|
};
|
|
410
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
|
+
}
|
|
411
677
|
function validatePayloadShape(phase, payload) {
|
|
412
|
-
const status = requiredString(payload, "status")
|
|
678
|
+
const status = normalizeCompletionStatus(requiredString(payload, "status"));
|
|
413
679
|
if (!status) {
|
|
414
680
|
throw new Error("status is required.");
|
|
415
681
|
}
|
|
@@ -504,6 +770,21 @@ function parseStrictCompletionPhase(value) {
|
|
|
504
770
|
}
|
|
505
771
|
return null;
|
|
506
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
|
+
}
|
|
507
788
|
function phaseFromToolName(toolName) {
|
|
508
789
|
const normalized = toolName.trim().toLowerCase();
|
|
509
790
|
if (normalized === "forge.complete_implementation" || normalized === "complete_implementation") {
|
|
@@ -11,6 +11,7 @@ export interface ProviderProcessRunRequest {
|
|
|
11
11
|
readonly command: string;
|
|
12
12
|
readonly args: readonly string[];
|
|
13
13
|
readonly cwd: string;
|
|
14
|
+
readonly env?: Readonly<Record<string, string>>;
|
|
14
15
|
}
|
|
15
16
|
export interface ProviderProcessRunResult {
|
|
16
17
|
readonly exitCode: number;
|