@loucompanion/forge-bridge 0.1.3-dev.fbe15023cdfe → 0.1.4-dev.41da3780c451
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/README.md +88 -2
- package/dist/bridge/bridge-chat.types.d.ts +111 -0
- package/dist/bridge/bridge-chat.types.js +401 -0
- package/dist/bridge/bridge.base.d.ts +4 -1
- package/dist/bridge/bridge.service.d.ts +72 -3
- package/dist/bridge/bridge.service.js +1563 -152
- package/dist/bridge/bridge.types.d.ts +46 -1
- package/dist/bridge/dispatch-result-outbox.d.ts +25 -0
- package/dist/bridge/dispatch-result-outbox.js +62 -0
- package/dist/bridge/dispatch-result-worker.d.ts +2 -0
- package/dist/bridge/dispatch-result-worker.js +122 -0
- package/dist/bridge/internals.d.ts +2 -0
- package/dist/bridge/internals.js +1 -0
- package/dist/bridge/readiness-result-outbox.d.ts +21 -0
- package/dist/bridge/readiness-result-outbox.js +54 -0
- package/dist/bridge/readiness-result-worker.d.ts +2 -0
- package/dist/bridge/readiness-result-worker.js +121 -0
- package/dist/bridge/runner-host-capabilities.d.ts +15 -0
- package/dist/bridge/runner-host-capabilities.js +93 -0
- package/dist/cleanup/cleanup.service.d.ts +1 -0
- package/dist/cleanup/cleanup.service.js +70 -9
- package/dist/cli/cli.help.js +2 -2
- package/dist/cli/cli.service.d.ts +14 -6
- package/dist/cli/cli.service.js +75 -16
- package/dist/cli/local-config.d.ts +1 -0
- package/dist/cli/local-config.js +4 -0
- package/dist/runtime/aspnet-runtime-prebuild.d.ts +36 -0
- package/dist/runtime/aspnet-runtime-prebuild.js +341 -0
- package/dist/runtime/attempt-database-broker.d.ts +89 -0
- package/dist/runtime/attempt-database-broker.js +390 -0
- package/dist/runtime/attempt-node-runtime-launcher.d.ts +2 -1
- package/dist/runtime/attempt-node-runtime-launcher.js +57 -18
- package/dist/runtime/attempt-path-operation.d.ts +21 -0
- package/dist/runtime/attempt-path-operation.js +94 -0
- package/dist/runtime/attempt-private-directory.d.ts +6 -0
- package/dist/runtime/attempt-private-directory.js +54 -0
- package/dist/runtime/attempt-process-supervisor.d.ts +11 -0
- package/dist/runtime/attempt-process-supervisor.js +70 -22
- package/dist/runtime/canonical-source-file.d.ts +3 -0
- package/dist/runtime/canonical-source-file.js +54 -0
- package/dist/runtime/docker-engine-probe.d.ts +90 -0
- package/dist/runtime/docker-engine-probe.js +333 -0
- package/dist/runtime/generated-output-reset.d.ts +10 -0
- package/dist/runtime/generated-output-reset.js +62 -8
- package/dist/runtime/host-database-capability-boundary.d.ts +16 -0
- package/dist/runtime/host-database-capability-boundary.js +62 -0
- package/dist/runtime/host-diagnostics.d.ts +11 -0
- package/dist/runtime/host-diagnostics.js +192 -0
- package/dist/runtime/host-execution-root.d.ts +4 -0
- package/dist/runtime/host-execution-root.js +39 -0
- package/dist/runtime/host-listener-inspector.d.ts +8 -1
- package/dist/runtime/host-listener-inspector.js +121 -60
- package/dist/runtime/host-runtime-contract.d.ts +12 -1
- package/dist/runtime/host-runtime-contract.js +168 -55
- package/dist/runtime/host-runtime-lease.d.ts +92 -8
- package/dist/runtime/host-runtime-lease.js +1141 -114
- package/dist/runtime/host-runtime-promotion.d.ts +12 -3
- package/dist/runtime/host-runtime-promotion.js +59 -12
- package/dist/runtime/memory-capacity.d.ts +70 -0
- package/dist/runtime/memory-capacity.js +123 -0
- package/dist/runtime/native-test-postgres-fallback.d.ts +25 -0
- package/dist/runtime/native-test-postgres-fallback.js +91 -0
- package/dist/runtime/node-host-runtime-adapter.d.ts +3 -0
- package/dist/runtime/node-host-runtime-adapter.js +29 -11
- package/dist/runtime/pinned-directory-process.d.ts +9 -0
- package/dist/runtime/pinned-directory-process.js +61 -0
- package/dist/runtime/pinned-source-directory.d.ts +17 -0
- package/dist/runtime/pinned-source-directory.js +94 -0
- package/dist/runtime/playwright-host-runtime-adapter.d.ts +1 -0
- package/dist/runtime/playwright-host-runtime-adapter.js +53 -5
- package/dist/runtime/prepared-runtime-identity.d.ts +6 -0
- package/dist/runtime/prepared-runtime-identity.js +218 -0
- package/dist/runtime/prepared-runtime-readiness.d.ts +30 -0
- package/dist/runtime/prepared-runtime-readiness.js +285 -0
- package/dist/runtime/runtime-isolation-lease.js +4 -1
- package/dist/runtime/workspace-resource-estimator.d.ts +17 -0
- package/dist/runtime/workspace-resource-estimator.js +174 -0
- package/dist/sandbox/preflight.d.ts +31 -1
- package/dist/sandbox/preflight.js +237 -51
- package/dist/sandbox/repository-lock.d.ts +2 -1
- package/dist/sandbox/repository-lock.js +19 -9
- package/dist/sandbox/runtime.d.ts +6 -1
- package/dist/sandbox/runtime.js +137 -107
- package/dist/sandbox/sandbox.types.d.ts +7 -0
- package/dist/sandbox/workspace.service.js +2 -0
- package/dist/session/managed-attempt-tools.d.ts +17 -1
- package/dist/session/managed-attempt-tools.js +489 -34
- package/dist/session/managed-build-coordinator.d.ts +5 -0
- package/dist/session/managed-build-coordinator.js +106 -0
- package/dist/session/managed-build-receipt.d.ts +21 -0
- package/dist/session/managed-build-receipt.js +337 -0
- package/dist/session/managed-verification-timeouts.d.ts +8 -0
- package/dist/session/managed-verification-timeouts.js +17 -0
- package/dist/session/phase-completion.artifacts.d.ts +17 -0
- package/dist/session/phase-completion.artifacts.js +224 -22
- package/dist/session/phase-completion.d.ts +11 -1
- package/dist/session/phase-completion.helpers.d.ts +5 -0
- package/dist/session/phase-completion.helpers.js +30 -7
- package/dist/session/phase-completion.js +14 -3
- package/dist/session/phase-completion.prompts.js +17 -18
- package/dist/session/phase-completion.runtime.d.ts +3 -1
- package/dist/session/phase-completion.runtime.js +126 -17
- package/dist/session/phase-completion.schemas.js +49 -2
- package/dist/session/phase-completion.validation.d.ts +1 -0
- package/dist/session/phase-completion.validation.js +111 -21
- package/dist/session/pre-publication-validation.d.ts +75 -0
- package/dist/session/pre-publication-validation.js +661 -0
- package/dist/session/provider-cli/bridge-cli.conformance-fixtures.d.ts +72 -0
- package/dist/session/provider-cli/bridge-cli.conformance-fixtures.js +462 -0
- package/dist/session/provider-cli/ci-repair-readiness.d.ts +8 -0
- package/dist/session/provider-cli/ci-repair-readiness.js +23 -3
- package/dist/session/provider-cli/provider-cli.artifacts.d.ts +4 -2
- package/dist/session/provider-cli/provider-cli.artifacts.js +47 -13
- package/dist/session/provider-cli/provider-cli.deterministic.js +34 -2
- package/dist/session/provider-cli/provider-cli.process.d.ts +51 -0
- package/dist/session/provider-cli/provider-cli.process.js +422 -22
- package/dist/session/provider-cli/provider-cli.prompts.d.ts +5 -0
- package/dist/session/provider-cli/provider-cli.prompts.js +136 -16
- package/dist/session/provider-cli/provider-cli.repair-epoch.d.ts +6 -0
- package/dist/session/provider-cli/provider-cli.repair-epoch.js +11 -0
- package/dist/session/provider-cli/provider-cli.runtime.d.ts +14 -1
- package/dist/session/provider-cli/provider-cli.runtime.js +54 -3
- package/dist/session/provider-cli/provider-cli.service.d.ts +30 -5
- package/dist/session/provider-cli/provider-cli.service.js +773 -69
- package/dist/session/provider-cli/provider-cli.types.d.ts +2 -0
- package/dist/session/provider-cli/provider-output-state.d.ts +10 -0
- package/dist/session/provider-cli/provider-output-state.js +361 -17
- package/dist/session/provider-cli/provider-usage.d.ts +7 -0
- package/dist/session/provider-cli/provider-usage.js +28 -5
- package/dist/session/session-redaction.js +434 -9
- package/dist/session/session.service.js +145 -23
- package/dist/session/session.types.d.ts +147 -2
- package/dist/session/session.types.js +587 -7
- package/dist/session/unreferenced-exports.d.ts +12 -0
- package/dist/session/unreferenced-exports.js +59 -0
- package/dist/shared/format/format-bytes.d.ts +1 -0
- package/dist/shared/format/format-bytes.js +9 -0
- package/dist/shared/git/git.js +2 -1
- package/dist/shared/path/path.d.ts +1 -0
- package/dist/shared/path/path.js +18 -0
- package/dist/shared/process/dotnet-toolchain.d.ts +17 -0
- package/dist/shared/process/dotnet-toolchain.js +307 -0
- package/dist/shared/process/host-process-recovery.js +3 -2
- package/dist/shared/process/host-process-supervisor.d.ts +2 -0
- package/dist/shared/process/host-process-supervisor.js +60 -11
- package/dist/shared/process/process.d.ts +2 -0
- package/dist/shared/process/process.js +12 -7
- package/dist/shared/provider-capabilities.d.ts +3 -0
- package/dist/shared/provider-capabilities.js +85 -0
- package/dist/shared/redaction/redaction.d.ts +1 -0
- package/dist/shared/redaction/redaction.js +39 -3
- package/dist/shared/time/monotonic.d.ts +1 -0
- package/dist/shared/time/monotonic.js +8 -0
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/worktree/dependency-warmer.d.ts +41 -9
- package/dist/worktree/dependency-warmer.js +497 -22
- package/dist/worktree/index.d.ts +1 -1
- package/dist/worktree/story-qa-reference-hydrator.d.ts +4 -0
- package/dist/worktree/story-qa-reference-hydrator.js +253 -0
- package/dist/worktree/worktree.service.d.ts +7 -0
- package/dist/worktree/worktree.service.js +253 -21
- package/dist/worktree/worktree.types.d.ts +7 -1
- package/package.json +7 -5
- package/runtime/windows-job-supervisor.ps1 +380 -14
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
1
2
|
import fs from "node:fs/promises";
|
|
2
3
|
import path from "node:path";
|
|
3
4
|
import { redactSecrets } from "../shared/redaction/redaction.js";
|
|
@@ -12,8 +13,11 @@ export async function writeDecomposeGateCompletionArtifact(config, gate, toolNam
|
|
|
12
13
|
validateDecomposeProposalJson(proposalJson);
|
|
13
14
|
}
|
|
14
15
|
const redactedPayload = redactSecrets({ ...payload, status, phase: "decompose", gate, tool_name: toolName }, config.redactionSecrets ?? []);
|
|
16
|
+
assertStructuralFieldsUnchanged(payload, redactedPayload);
|
|
17
|
+
if (gate === "decompose_proposal") {
|
|
18
|
+
validatePostRedaction(() => validateDecomposeProposalJson(asRecord(redactedPayload["proposal_json"] ?? redactedPayload["decomposition_json"], "proposal_json")));
|
|
19
|
+
}
|
|
15
20
|
const root = artifactRootFor(config.worktreePath, config.workItemId);
|
|
16
|
-
await fs.mkdir(root, { recursive: true });
|
|
17
21
|
const artifactPath = path.join(root, decomposeGateFileNames[gate]);
|
|
18
22
|
const reportPath = gate === "decompose_proposal" ? path.join(root, phaseFiles.decompose.reportFileName) : path.join(root, `${gate}.md`);
|
|
19
23
|
const verdictJson = jsonText(redactedPayload);
|
|
@@ -21,8 +25,10 @@ export async function writeDecomposeGateCompletionArtifact(config, gate, toolNam
|
|
|
21
25
|
|
|
22
26
|
${optionalString(redactedPayload, "summary") ?? "Decompose gate recorded."}
|
|
23
27
|
`;
|
|
24
|
-
await
|
|
25
|
-
|
|
28
|
+
await publishArtifactBundleAtomic(root, [
|
|
29
|
+
{ fileName: path.basename(reportPath), content: report },
|
|
30
|
+
{ fileName: path.basename(artifactPath), content: verdictJson }
|
|
31
|
+
], path.basename(artifactPath));
|
|
26
32
|
return {
|
|
27
33
|
phase: "decompose",
|
|
28
34
|
workItemId: config.workItemId,
|
|
@@ -44,14 +50,19 @@ export async function writeDraftGateCompletionArtifact(config, gate, toolName, i
|
|
|
44
50
|
validateDraftProposalJson(draftJson);
|
|
45
51
|
}
|
|
46
52
|
const redactedPayload = redactSecrets({ ...payload, status, phase: "draft", gate, tool_name: toolName }, config.redactionSecrets ?? []);
|
|
53
|
+
assertStructuralFieldsUnchanged(payload, redactedPayload);
|
|
54
|
+
if (gate === "draft_proposal") {
|
|
55
|
+
validatePostRedaction(() => validateDraftProposalJson(asRecord(redactedPayload["draft_json"], "draft_json")));
|
|
56
|
+
}
|
|
47
57
|
const root = artifactRootFor(config.worktreePath, config.workItemId);
|
|
48
|
-
await fs.mkdir(root, { recursive: true });
|
|
49
58
|
const artifactPath = path.join(root, draftGateFileNames[gate]);
|
|
50
59
|
const reportPath = gate === "draft_proposal" ? path.join(root, phaseFiles.draft.reportFileName) : path.join(root, `${gate}.md`);
|
|
51
60
|
const verdictJson = jsonText(redactedPayload);
|
|
52
61
|
const report = optionalString(redactedPayload, "report_markdown") ?? `# ${gate.replace(/_/g, " ")}\n\n${optionalString(redactedPayload, "summary") ?? "Draft gate recorded."}\n`;
|
|
53
|
-
await
|
|
54
|
-
|
|
62
|
+
await publishArtifactBundleAtomic(root, [
|
|
63
|
+
{ fileName: path.basename(reportPath), content: report },
|
|
64
|
+
{ fileName: path.basename(artifactPath), content: verdictJson }
|
|
65
|
+
], path.basename(artifactPath));
|
|
55
66
|
return {
|
|
56
67
|
phase: "draft",
|
|
57
68
|
workItemId: config.workItemId,
|
|
@@ -148,6 +159,11 @@ export async function writeCompletionArtifact(config, toolName, input) {
|
|
|
148
159
|
validatePayloadShape(phase, validationPayload);
|
|
149
160
|
validateOptionalWorkItemId(validationPayload, config.workItemId);
|
|
150
161
|
const redactedPayload = redactSecrets(validationPayload, config.redactionSecrets ?? []);
|
|
162
|
+
assertStructuralFieldsUnchanged(validationPayload, redactedPayload);
|
|
163
|
+
validatePostRedaction(() => {
|
|
164
|
+
validatePayloadShape(phase, redactedPayload);
|
|
165
|
+
validateOptionalWorkItemId(redactedPayload, config.workItemId);
|
|
166
|
+
});
|
|
151
167
|
const producedBy = optionalString(redactedPayload, "produced_by")
|
|
152
168
|
?? optionalString(redactedPayload, "producedBy")
|
|
153
169
|
?? config.producedBy
|
|
@@ -175,9 +191,10 @@ export async function writeCompletionArtifact(config, toolName, input) {
|
|
|
175
191
|
const report = reportMarkdown(redactedPayload, phase, status, producedBy, contentHash);
|
|
176
192
|
const artifactPath = completionArtifactJsonPath(config.worktreePath, config.workItemId, phase);
|
|
177
193
|
const reportPath = completionArtifactReportPath(config.worktreePath, config.workItemId, phase);
|
|
178
|
-
await
|
|
179
|
-
|
|
180
|
-
|
|
194
|
+
await publishArtifactBundleAtomic(path.dirname(artifactPath), [
|
|
195
|
+
{ fileName: path.basename(reportPath), content: report },
|
|
196
|
+
{ fileName: path.basename(artifactPath), content: artifactJson }
|
|
197
|
+
], path.basename(artifactPath));
|
|
181
198
|
return {
|
|
182
199
|
phase,
|
|
183
200
|
workItemId: config.workItemId,
|
|
@@ -215,11 +232,12 @@ export async function writeArchitectCompletionArtifact(config, payload, status,
|
|
|
215
232
|
...artifactWithoutHash,
|
|
216
233
|
content_hash: contentHash
|
|
217
234
|
};
|
|
218
|
-
await
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
235
|
+
await publishArtifactBundleAtomic(artifactRoot, [
|
|
236
|
+
{ fileName: path.basename(implementationPlanPath), content: jsonText(implementationPlan) },
|
|
237
|
+
{ fileName: path.basename(qaPlanPath), content: jsonText(qaPlan) },
|
|
238
|
+
{ fileName: path.basename(reportPath), content: `${report.trim()}\n` },
|
|
239
|
+
{ fileName: path.basename(verdictPath), content: jsonText(verdict) }
|
|
240
|
+
], path.basename(verdictPath));
|
|
223
241
|
return {
|
|
224
242
|
phase: "architect",
|
|
225
243
|
workItemId: config.workItemId,
|
|
@@ -246,6 +264,16 @@ export async function writeQaGateCompletionArtifact(config, gate, input) {
|
|
|
246
264
|
status,
|
|
247
265
|
...(validatedSidecars ? { sidecars: validatedSidecars } : {})
|
|
248
266
|
}, config.redactionSecrets ?? []);
|
|
267
|
+
assertStructuralFieldsUnchanged(payload, redactedPayload);
|
|
268
|
+
validatePostRedaction(() => {
|
|
269
|
+
validateQaGatePayloadShape(gate, redactedPayload, status);
|
|
270
|
+
validateOptionalWorkItemId(redactedPayload, config.workItemId);
|
|
271
|
+
});
|
|
272
|
+
if (gate === "qa_manual_web") {
|
|
273
|
+
await validateQaSidecarReferences(config.worktreePath, config.workItemId, qaArray(redactedPayload, "sidecars", qaEvidenceSource(redactedPayload)), ownerFields.owner_type, ownerFields.owner_id).catch((error) => {
|
|
274
|
+
throw postRedactionValidationError(error);
|
|
275
|
+
});
|
|
276
|
+
}
|
|
249
277
|
const producedBy = optionalString(redactedPayload, "produced_by")
|
|
250
278
|
?? optionalString(redactedPayload, "producedBy")
|
|
251
279
|
?? config.producedBy
|
|
@@ -269,7 +297,6 @@ export async function writeQaGateCompletionArtifact(config, gate, input) {
|
|
|
269
297
|
...artifactWithoutHash,
|
|
270
298
|
content_hash: contentHash
|
|
271
299
|
};
|
|
272
|
-
await fs.mkdir(artifactRoot, { recursive: true });
|
|
273
300
|
await writeFileAtomic(artifactPath, jsonText(artifact));
|
|
274
301
|
return {
|
|
275
302
|
phase: "qa",
|
|
@@ -378,12 +405,13 @@ export async function writeQaCompletionArtifact(config, payload, status, produce
|
|
|
378
405
|
content_hash: contentHash
|
|
379
406
|
});
|
|
380
407
|
const finalReport = reportMarkdown(payload, "qa", status, producedBy, contentHash);
|
|
381
|
-
await
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
408
|
+
await publishArtifactBundleAtomic(artifactRoot, [
|
|
409
|
+
{ fileName: path.basename(planPath), content: planJson },
|
|
410
|
+
{ fileName: path.basename(evidencePath), content: evidenceJson },
|
|
411
|
+
{ fileName: path.basename(findingsPath), content: findingsJson },
|
|
412
|
+
{ fileName: path.basename(reportPath), content: finalReport },
|
|
413
|
+
{ fileName: path.basename(verdictPath), content: verdictJson }
|
|
414
|
+
], path.basename(verdictPath));
|
|
387
415
|
return {
|
|
388
416
|
phase: "qa",
|
|
389
417
|
workItemId: config.workItemId,
|
|
@@ -553,6 +581,7 @@ async function validateQaSidecarReferences(worktreePath, workItemId, sidecars, o
|
|
|
553
581
|
: artifactRootFor(worktreePath, workItemId);
|
|
554
582
|
const qaSidecarRoot = path.resolve(artifactRoot, "qa");
|
|
555
583
|
const validated = [];
|
|
584
|
+
const validatedByPath = new Map();
|
|
556
585
|
for (const [index, sidecar] of sidecars.entries()) {
|
|
557
586
|
if (!sidecar || typeof sidecar !== "object" || Array.isArray(sidecar)) {
|
|
558
587
|
throw new Error(`sidecars[${index}] must be an object.`);
|
|
@@ -588,9 +617,19 @@ async function validateQaSidecarReferences(worktreePath, workItemId, sidecars, o
|
|
|
588
617
|
if (actualHash !== normalizeSha256ContentHash(contentHash)) {
|
|
589
618
|
throw new Error(`sidecars[${index}].content_hash does not match the referenced file.`);
|
|
590
619
|
}
|
|
620
|
+
const canonicalPath = path.relative(worktreePath, fullPath).split(path.sep).join("/");
|
|
621
|
+
const duplicateKey = canonicalPath.toLowerCase();
|
|
622
|
+
const existing = validatedByPath.get(duplicateKey);
|
|
623
|
+
if (existing) {
|
|
624
|
+
if (existing.contentHash !== actualHash || existing.bytes !== stat.size) {
|
|
625
|
+
throw new Error(`sidecars[${index}] conflicts with another payload for ${canonicalPath}.`);
|
|
626
|
+
}
|
|
627
|
+
continue;
|
|
628
|
+
}
|
|
629
|
+
validatedByPath.set(duplicateKey, { contentHash: actualHash, bytes: stat.size });
|
|
591
630
|
validated.push({
|
|
592
631
|
...record,
|
|
593
|
-
path:
|
|
632
|
+
path: canonicalPath,
|
|
594
633
|
content_hash: actualHash,
|
|
595
634
|
bytes: stat.size
|
|
596
635
|
});
|
|
@@ -625,3 +664,166 @@ export async function readCompletionArtifact(worktreePath, workItemId, phase, ow
|
|
|
625
664
|
phase
|
|
626
665
|
};
|
|
627
666
|
}
|
|
667
|
+
/**
|
|
668
|
+
* Stages a complete phase bundle outside the live artifact directory, then
|
|
669
|
+
* publishes its canonical verdict/marker last. Consumers treat the marker as
|
|
670
|
+
* the bundle's commit point, so they can observe either the old complete bundle
|
|
671
|
+
* or the new complete bundle, never a partially written set. A failed or
|
|
672
|
+
* cancelled publication rolls every touched file back before rejecting.
|
|
673
|
+
*/
|
|
674
|
+
export async function publishArtifactBundleAtomic(artifactRoot, files, commitFileName, options = {}) {
|
|
675
|
+
validateArtifactBundleFiles(files, commitFileName);
|
|
676
|
+
throwIfPublicationCancelled(options.signal);
|
|
677
|
+
const parent = path.dirname(artifactRoot);
|
|
678
|
+
const transactionId = `${process.pid}.${crypto.randomUUID()}`;
|
|
679
|
+
const stagingRoot = path.join(parent, `.${path.basename(artifactRoot)}.${transactionId}.publish`);
|
|
680
|
+
const backupRoot = path.join(stagingRoot, ".previous");
|
|
681
|
+
const orderedFiles = [
|
|
682
|
+
...files.filter((file) => file.fileName !== commitFileName),
|
|
683
|
+
files.find((file) => file.fileName === commitFileName)
|
|
684
|
+
];
|
|
685
|
+
const backedUp = new Set();
|
|
686
|
+
const published = new Set();
|
|
687
|
+
try {
|
|
688
|
+
await fs.mkdir(parent, { recursive: true });
|
|
689
|
+
await fs.mkdir(stagingRoot, { recursive: false });
|
|
690
|
+
for (const file of orderedFiles) {
|
|
691
|
+
throwIfPublicationCancelled(options.signal);
|
|
692
|
+
const handle = await fs.open(path.join(stagingRoot, file.fileName), "wx");
|
|
693
|
+
try {
|
|
694
|
+
await handle.writeFile(file.content, { encoding: "utf8", signal: options.signal });
|
|
695
|
+
await handle.sync();
|
|
696
|
+
}
|
|
697
|
+
finally {
|
|
698
|
+
await handle.close();
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
throwIfPublicationCancelled(options.signal);
|
|
702
|
+
await fs.mkdir(artifactRoot, { recursive: true });
|
|
703
|
+
await fs.mkdir(backupRoot, { recursive: true });
|
|
704
|
+
// Hide the previous commit marker before changing any supporting file.
|
|
705
|
+
for (const fileName of [commitFileName, ...orderedFiles.map((file) => file.fileName).filter((name) => name !== commitFileName)]) {
|
|
706
|
+
const livePath = path.join(artifactRoot, fileName);
|
|
707
|
+
try {
|
|
708
|
+
await fs.rename(livePath, path.join(backupRoot, fileName));
|
|
709
|
+
backedUp.add(fileName);
|
|
710
|
+
}
|
|
711
|
+
catch (error) {
|
|
712
|
+
if (!isNodeError(error) || error.code !== "ENOENT") {
|
|
713
|
+
throw error;
|
|
714
|
+
}
|
|
715
|
+
}
|
|
716
|
+
}
|
|
717
|
+
for (const [index, file] of orderedFiles.entries()) {
|
|
718
|
+
throwIfPublicationCancelled(options.signal);
|
|
719
|
+
await options.beforePublishFile?.(file.fileName, index);
|
|
720
|
+
await fs.rename(path.join(stagingRoot, file.fileName), path.join(artifactRoot, file.fileName));
|
|
721
|
+
published.add(file.fileName);
|
|
722
|
+
}
|
|
723
|
+
}
|
|
724
|
+
catch (error) {
|
|
725
|
+
const rollbackErrors = [];
|
|
726
|
+
for (const file of [...orderedFiles].reverse()) {
|
|
727
|
+
if (!published.has(file.fileName))
|
|
728
|
+
continue;
|
|
729
|
+
await fs.rm(path.join(artifactRoot, file.fileName), { force: true }).catch(() => {
|
|
730
|
+
rollbackErrors.push(file.fileName);
|
|
731
|
+
});
|
|
732
|
+
}
|
|
733
|
+
// Restore the previous marker last for the same observable commit boundary.
|
|
734
|
+
const restoreOrder = [
|
|
735
|
+
...orderedFiles.filter((file) => file.fileName !== commitFileName),
|
|
736
|
+
orderedFiles.find((file) => file.fileName === commitFileName)
|
|
737
|
+
];
|
|
738
|
+
for (const file of restoreOrder) {
|
|
739
|
+
if (!backedUp.has(file.fileName))
|
|
740
|
+
continue;
|
|
741
|
+
await fs.rename(path.join(backupRoot, file.fileName), path.join(artifactRoot, file.fileName)).catch(() => {
|
|
742
|
+
rollbackErrors.push(file.fileName);
|
|
743
|
+
});
|
|
744
|
+
}
|
|
745
|
+
if (rollbackErrors.length === 0
|
|
746
|
+
&& error instanceof Error
|
|
747
|
+
&& error.message.startsWith("artifact_atomic_publish_cancelled:")) {
|
|
748
|
+
throw error;
|
|
749
|
+
}
|
|
750
|
+
const suffix = rollbackErrors.length > 0 ? " Rollback did not restore every bundle file." : "";
|
|
751
|
+
throw new Error(`artifact_atomic_publish_failure: completion artifact bundle was not published.${suffix}`, { cause: error });
|
|
752
|
+
}
|
|
753
|
+
finally {
|
|
754
|
+
await fs.rm(stagingRoot, { recursive: true, force: true }).catch(() => undefined);
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
function validateArtifactBundleFiles(files, commitFileName) {
|
|
758
|
+
if (files.length === 0 || !files.some((file) => file.fileName === commitFileName)) {
|
|
759
|
+
throw new Error("artifact_atomic_publish_failure: bundle commit file is missing.");
|
|
760
|
+
}
|
|
761
|
+
const names = new Set();
|
|
762
|
+
for (const file of files) {
|
|
763
|
+
if (!file.fileName || path.basename(file.fileName) !== file.fileName || names.has(file.fileName)) {
|
|
764
|
+
throw new Error("artifact_atomic_publish_failure: bundle file names must be unique safe base names.");
|
|
765
|
+
}
|
|
766
|
+
names.add(file.fileName);
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
function throwIfPublicationCancelled(signal) {
|
|
770
|
+
if (signal?.aborted) {
|
|
771
|
+
throw new Error("artifact_atomic_publish_cancelled: completion artifact bundle publication was cancelled.");
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
function validatePostRedaction(validate) {
|
|
775
|
+
try {
|
|
776
|
+
validate();
|
|
777
|
+
}
|
|
778
|
+
catch (error) {
|
|
779
|
+
throw postRedactionValidationError(error);
|
|
780
|
+
}
|
|
781
|
+
}
|
|
782
|
+
function postRedactionValidationError(error) {
|
|
783
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
784
|
+
return new Error(`artifact_post_redaction_validation_failed: ${message}`, { cause: error });
|
|
785
|
+
}
|
|
786
|
+
function assertStructuralFieldsUnchanged(original, redacted) {
|
|
787
|
+
if (structuralFieldFingerprint(original) !== structuralFieldFingerprint(redacted)) {
|
|
788
|
+
// This stable code is intentionally safe for telemetry: it does not include
|
|
789
|
+
// the structural value or any candidate secret.
|
|
790
|
+
throw new Error("artifact_redaction_changed_structural_field: redaction changed a structural identifier; the bundle was not published.");
|
|
791
|
+
}
|
|
792
|
+
}
|
|
793
|
+
function structuralFieldFingerprint(value, key = "", result = []) {
|
|
794
|
+
if (Array.isArray(value)) {
|
|
795
|
+
if (isStructuralFieldKey(key) && value.every((item) => item === null || typeof item !== "object")) {
|
|
796
|
+
result.push(stableStringify(value));
|
|
797
|
+
}
|
|
798
|
+
else {
|
|
799
|
+
for (const item of value)
|
|
800
|
+
structuralFieldFingerprint(item, key, result);
|
|
801
|
+
}
|
|
802
|
+
return result.join("\n");
|
|
803
|
+
}
|
|
804
|
+
if (!value || typeof value !== "object") {
|
|
805
|
+
if (isStructuralFieldKey(key))
|
|
806
|
+
result.push(stableStringify(value));
|
|
807
|
+
return result.join("\n");
|
|
808
|
+
}
|
|
809
|
+
for (const [childKey, child] of Object.entries(value)) {
|
|
810
|
+
if (isStructuralFieldKey(childKey)) {
|
|
811
|
+
result.push(`${childKey}:${stableStringify(child)}`);
|
|
812
|
+
}
|
|
813
|
+
else {
|
|
814
|
+
structuralFieldFingerprint(child, childKey, result);
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
return result.join("\n");
|
|
818
|
+
}
|
|
819
|
+
function isStructuralFieldKey(key) {
|
|
820
|
+
return key === "id"
|
|
821
|
+
|| key === "before"
|
|
822
|
+
|| key === "after"
|
|
823
|
+
|| key === "from"
|
|
824
|
+
|| key === "to"
|
|
825
|
+
|| key === "tasks"
|
|
826
|
+
|| key === "phases"
|
|
827
|
+
|| /(?:^|_)(?:id|ids)$/i.test(key)
|
|
828
|
+
|| /(?:Id|Ids)$/.test(key);
|
|
829
|
+
}
|
|
@@ -10,7 +10,8 @@ export type ArchitectCompletionPhase = "architect";
|
|
|
10
10
|
export type DraftCompletionPhase = "draft";
|
|
11
11
|
export type DecomposeCompletionPhase = "decompose";
|
|
12
12
|
export type CiRepairCompletionPhase = "ci_repair";
|
|
13
|
-
export type
|
|
13
|
+
export type CommentRepairCompletionPhase = "comment_repair";
|
|
14
|
+
export type CompletionPhase = DevelopCompletionPhase | "qa" | DebugCompletionPhase | ArchitectCompletionPhase | DraftCompletionPhase | DecomposeCompletionPhase | CiRepairCompletionPhase | CommentRepairCompletionPhase;
|
|
14
15
|
export type QaCompletionGate = "qa_plan" | "qa_regression" | "qa_api" | "qa_manual_web" | "qa_bug_repro" | "qa_findings" | "qa_verdict";
|
|
15
16
|
export type DraftCompletionGate = "draft_intent" | "draft_repo_context" | "draft_graph_context" | "draft_product_scope" | "draft_work_order_plan" | "draft_critic" | "draft_proposal";
|
|
16
17
|
export type DecomposeCompletionGate = "decompose_subject_intake" | "decompose_context_review" | "decompose_breakdown_strategy" | "decompose_child_items" | "decompose_dependency_graph" | "decompose_critic" | "decompose_proposal";
|
|
@@ -20,6 +21,7 @@ export interface CompletionToolRuntime {
|
|
|
20
21
|
readonly args: readonly string[];
|
|
21
22
|
readonly env: Readonly<Record<string, string>>;
|
|
22
23
|
readonly cursorMcpConfigPath?: string | null;
|
|
24
|
+
readonly claudeMcpConfigPath?: string | null;
|
|
23
25
|
cleanup(): Promise<void>;
|
|
24
26
|
}
|
|
25
27
|
export interface CompletionToolRuntimeOptions {
|
|
@@ -49,6 +51,13 @@ export interface CompletionToolCallResult {
|
|
|
49
51
|
readonly text: string;
|
|
50
52
|
}[];
|
|
51
53
|
readonly structuredContent: CompletionArtifactReference & {
|
|
54
|
+
readonly artifact_written: true;
|
|
55
|
+
readonly artifact_receipt: {
|
|
56
|
+
readonly schema_version: 1;
|
|
57
|
+
readonly path: string;
|
|
58
|
+
readonly report_path: string;
|
|
59
|
+
readonly content_hash: string;
|
|
60
|
+
};
|
|
52
61
|
readonly gate: {
|
|
53
62
|
readonly phase: CompletionPhase;
|
|
54
63
|
readonly qaGate?: QaCompletionGate;
|
|
@@ -100,6 +109,7 @@ export declare const qaFindingScopes: Set<string>;
|
|
|
100
109
|
export declare const qaCandidateBugActions: Set<string>;
|
|
101
110
|
export declare const qaScopeSurfaces: Set<string>;
|
|
102
111
|
export declare const qaScopeEvidence: Set<string>;
|
|
112
|
+
export declare const qaScopeCapabilities: Set<string>;
|
|
103
113
|
export declare const qaScopeSources: Set<string>;
|
|
104
114
|
export declare const qaScopeAuthorities: Set<string>;
|
|
105
115
|
export declare const qaScopeConfidence: Set<string>;
|
|
@@ -33,6 +33,11 @@ export declare function requireField(payload: Record<string, unknown>, key: stri
|
|
|
33
33
|
export declare function requireNonPassEvidence(payload: Record<string, unknown>): void;
|
|
34
34
|
export declare function requireImplementationEvidence(payload: Record<string, unknown>): void;
|
|
35
35
|
export declare function requirePassingCommands(payload: Record<string, unknown>): void;
|
|
36
|
+
export declare class CompletionPayloadValidationError extends Error {
|
|
37
|
+
readonly issues: readonly string[];
|
|
38
|
+
constructor(issues: readonly string[]);
|
|
39
|
+
}
|
|
40
|
+
export declare function throwCompletionPayloadIssues(issues: readonly string[]): void;
|
|
36
41
|
export declare function requireArray(payload: Record<string, unknown>, key: string): void;
|
|
37
42
|
export declare function requireNonEmptyArray(payload: Record<string, unknown>, key: string): void;
|
|
38
43
|
export declare function arrayLength(payload: Record<string, unknown>, key: string): number;
|
|
@@ -88,6 +88,9 @@ export function parseStrictCompletionPhase(value) {
|
|
|
88
88
|
if (normalized === "ci_repair" || normalized === "pr_checks" || normalized === "repair_pr_checks") {
|
|
89
89
|
return "ci_repair";
|
|
90
90
|
}
|
|
91
|
+
if (normalized === "comment_repair" || normalized === "pr_comments" || normalized === "repair_pr_comments") {
|
|
92
|
+
return "comment_repair";
|
|
93
|
+
}
|
|
91
94
|
return null;
|
|
92
95
|
}
|
|
93
96
|
export function normalizeDebugCompletionPhase(value) {
|
|
@@ -162,6 +165,9 @@ export function phaseFromToolName(toolName) {
|
|
|
162
165
|
if (normalized === "forge.complete_ci_repair" || normalized === "complete_ci_repair") {
|
|
163
166
|
return "ci_repair";
|
|
164
167
|
}
|
|
168
|
+
if (normalized === "forge.complete_comment_repair" || normalized === "complete_comment_repair") {
|
|
169
|
+
return "comment_repair";
|
|
170
|
+
}
|
|
165
171
|
if (draftGateFromToolName(toolName)) {
|
|
166
172
|
return "draft";
|
|
167
173
|
}
|
|
@@ -262,28 +268,45 @@ export function requirePassingCommands(payload) {
|
|
|
262
268
|
if (commands.length === 0) {
|
|
263
269
|
throw new Error("commands must include at least one deterministic validation command.");
|
|
264
270
|
}
|
|
271
|
+
const issues = [];
|
|
265
272
|
for (const [index, command] of commands.entries()) {
|
|
266
273
|
if (!command || typeof command !== "object" || Array.isArray(command)) {
|
|
267
|
-
|
|
274
|
+
issues.push(`commands[${index}] must be an object.`);
|
|
275
|
+
continue;
|
|
268
276
|
}
|
|
269
277
|
const record = command;
|
|
270
278
|
if (typeof record.command !== "string" || record.command.trim().length === 0) {
|
|
271
|
-
|
|
279
|
+
issues.push(`commands[${index}].command must be a non-empty string.`);
|
|
272
280
|
}
|
|
273
281
|
if (typeof record.summary !== "string" || record.summary.trim().length === 0) {
|
|
274
|
-
|
|
282
|
+
issues.push(`commands[${index}].summary must be a non-empty string.`);
|
|
275
283
|
}
|
|
276
284
|
if (typeof record.exit_code !== "number" || !Number.isFinite(record.exit_code)) {
|
|
277
|
-
|
|
285
|
+
issues.push(`commands[${index}].exit_code must be a finite number.`);
|
|
278
286
|
}
|
|
279
|
-
if (record.exit_code !== 0) {
|
|
280
|
-
|
|
287
|
+
else if (record.exit_code !== 0) {
|
|
288
|
+
issues.push(`commands[${index}].exit_code must be 0 when status is pass.`);
|
|
281
289
|
}
|
|
282
290
|
const status = typeof record.status === "string" ? record.status.trim().toLowerCase() : "";
|
|
283
291
|
if (status !== "pass") {
|
|
284
|
-
|
|
292
|
+
issues.push(`commands[${index}].status must be pass.`);
|
|
285
293
|
}
|
|
286
294
|
}
|
|
295
|
+
throwCompletionPayloadIssues(issues);
|
|
296
|
+
}
|
|
297
|
+
export class CompletionPayloadValidationError extends Error {
|
|
298
|
+
issues;
|
|
299
|
+
constructor(issues) {
|
|
300
|
+
super(`completion_payload_invalid: ${issues.length} correction${issues.length === 1 ? "" : "s"} required; no artifact was written.\n`
|
|
301
|
+
+ issues.map(issue => `- ${issue}`).join("\n")
|
|
302
|
+
+ "\nRepair every listed field and resubmit this gate once.");
|
|
303
|
+
this.issues = issues;
|
|
304
|
+
this.name = "CompletionPayloadValidationError";
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
export function throwCompletionPayloadIssues(issues) {
|
|
308
|
+
if (issues.length > 0)
|
|
309
|
+
throw new CompletionPayloadValidationError(issues);
|
|
287
310
|
}
|
|
288
311
|
export function requireArray(payload, key) {
|
|
289
312
|
if (!Array.isArray(payload[key])) {
|
|
@@ -133,6 +133,7 @@ export const qaScopeEvidence = new Set([
|
|
|
133
133
|
"migration_check",
|
|
134
134
|
"config_check"
|
|
135
135
|
]);
|
|
136
|
+
export const qaScopeCapabilities = new Set(["docker_engine"]);
|
|
136
137
|
export const qaScopeSources = new Set(["user", "draft", "decompose", "architect", "heuristic", "system"]);
|
|
137
138
|
export const qaScopeAuthorities = new Set(["user", "decompose", "architect", "system"]);
|
|
138
139
|
export const qaScopeConfidence = new Set(["explicit", "inferred"]);
|
|
@@ -140,6 +141,7 @@ export const qaScopeFields = new Set([
|
|
|
140
141
|
"schema_version",
|
|
141
142
|
"surface",
|
|
142
143
|
"required_evidence",
|
|
144
|
+
"required_capabilities",
|
|
143
145
|
"not_applicable",
|
|
144
146
|
"browser_required",
|
|
145
147
|
"api_required",
|
|
@@ -199,6 +201,10 @@ export const phaseFiles = {
|
|
|
199
201
|
ci_repair: {
|
|
200
202
|
jsonFileName: "ci-repair.handoff.json",
|
|
201
203
|
reportFileName: "ci-repair.report.md"
|
|
204
|
+
},
|
|
205
|
+
comment_repair: {
|
|
206
|
+
jsonFileName: "comment-repair.handoff.json",
|
|
207
|
+
reportFileName: "comment-repair.report.md"
|
|
202
208
|
}
|
|
203
209
|
};
|
|
204
210
|
export function completionPhaseFromDispatch(dispatch) {
|
|
@@ -231,6 +237,9 @@ export function normalizeCompletionPhase(value) {
|
|
|
231
237
|
if (normalized === "ci_repair" || normalized === "pr_checks" || normalized === "repair_pr_checks") {
|
|
232
238
|
return "ci_repair";
|
|
233
239
|
}
|
|
240
|
+
if (normalized === "comment_repair" || normalized === "pr_comments" || normalized === "repair_pr_comments") {
|
|
241
|
+
return "comment_repair";
|
|
242
|
+
}
|
|
234
243
|
return "implementation";
|
|
235
244
|
}
|
|
236
245
|
export function usesDevelopCycleCompletion(dispatch) {
|
|
@@ -284,9 +293,11 @@ export function completionToolNamesForPhase(phase) {
|
|
|
284
293
|
? "complete_validation"
|
|
285
294
|
: phase === "ci_repair"
|
|
286
295
|
? "complete_ci_repair"
|
|
287
|
-
: phase === "
|
|
288
|
-
? "
|
|
289
|
-
: "
|
|
296
|
+
: phase === "comment_repair"
|
|
297
|
+
? "complete_comment_repair"
|
|
298
|
+
: phase === "architect"
|
|
299
|
+
? "complete_architect_plan"
|
|
300
|
+
: "complete_qa";
|
|
290
301
|
return [`forge.${base}`, base];
|
|
291
302
|
}
|
|
292
303
|
export function completionToolNamesForQaGate(gate) {
|
|
@@ -17,8 +17,9 @@ Forge will mirror only the tool-written QA artifacts; reporting QA completion in
|
|
|
17
17
|
Do not write Architect artifacts by hand. When the plan is complete, call \`${toolName}(...)\`${alias ? ` (or \`${alias}\` if your client shows unqualified MCP tool names)` : ""} with the full structured Architect planning payload.
|
|
18
18
|
That tool writes \`.forge/work-items/{workItemId}/artifacts/implementation.plan.json\`, \`qa.plan.json\`, \`${artifactName}\`, and \`architect.report.md\`.
|
|
19
19
|
The payload must put \`phases\`, \`tasks\`, \`dependencies\`, and \`phase_gates\` as top-level fields inside \`implementation_plan\`, for example:
|
|
20
|
-
\`{ "status": "pass", "summary": "...", "implementation_plan": { "schema_version": 1, "plan_id": "plan-...", "phases": [{ "id": "phase-1", "tasks": ["task-1"], "gate": "gate-1" }], "tasks": [{ "id": "task-1", "verification_scope": { "schema_version": 1, "surface": ["domain_contract"], "required_evidence": ["unit_tests"], "not_applicable": ["browser_manual", "api_http"], "browser_required": false, "api_required": false, "rationale": "Backend-only child task." }, "tests": [{ "command": "npm test" }] }], "dependencies": [], "phase_gates": [{ "id": "gate-1", "phase": "phase-1" }] }, "qa_plan": { "schema_version": 1, "final_qa": [{ "id": "qa-1", "coverage": "..." }] }, "report_markdown": "# Architect Report" }\`.
|
|
20
|
+
\`{ "status": "pass", "summary": "...", "implementation_plan": { "schema_version": 1, "plan_id": "plan-...", "phases": [{ "id": "phase-1", "tasks": ["task-1"], "gate": "gate-1" }], "tasks": [{ "id": "task-1", "verification_scope": { "schema_version": 1, "surface": ["domain_contract"], "required_evidence": ["unit_tests"], "required_capabilities": [], "not_applicable": ["browser_manual", "api_http"], "browser_required": false, "api_required": false, "rationale": "Backend-only child task." }, "tests": [{ "command": "npm test" }] }], "dependencies": [], "phase_gates": [{ "id": "gate-1", "phase": "phase-1" }] }, "qa_plan": { "schema_version": 1, "final_qa": [{ "id": "qa-1", "coverage": "..." }] }, "report_markdown": "# Architect Report" }\`.
|
|
21
21
|
\`implementation_plan.tasks\` are Develop child tasks only: each task should include \`verification_scope\` describing task-level QA evidence, and must require a focused repo/product implementation change. Put parent-story QA, sign-off, child-count/materialization inventory, parent plan artifact checks, and final integrated acceptance proof in \`qa_plan.final_qa\`, not as child Develop tasks.
|
|
22
|
+
When a task's implementation or validation starts containers or otherwise needs the host Docker daemon, include \`"required_capabilities": ["docker_engine"]\` in its verification scope. Do not encode a database, image, or Docker version as a runner capability; the repository owns those versions.
|
|
22
23
|
Dependency entries use canonical \`before\` and \`after\` task ids, for example \`{ "before": "task-1", "after": "task-2" }\`. Forge accepts \`from\`/\`to\` only as aliases and normalizes them before writing artifacts.
|
|
23
24
|
Use \`status: "pass"\` for a valid pending-approval plan, \`needs_revision\` when story/design input must change, or \`blocked\` with a concrete blocker.
|
|
24
25
|
Forge will mirror only the tool-written Architect artifacts; reporting the plan in prose is not enough.`;
|
|
@@ -58,7 +59,8 @@ Complete the QA cycle in order inside this same provider session. Start long, in
|
|
|
58
59
|
6. Follow the returned findings instruction, then call \`forge.complete_qa_findings(...)\`.
|
|
59
60
|
7. Follow the returned verdict instruction, then call \`forge.complete_qa_verdict(...)\`.
|
|
60
61
|
Use \`status: "skipped"\` with a reason for gates that are not applicable. If a gate is blocked, keep moving through later gates as skipped due to the blocker and finish with a blocked verdict.
|
|
61
|
-
On a managed host,
|
|
62
|
+
On a managed host, use focused checks plus the relevant runner-authored \`forge.start_managed_verification\` profiles for normal Story QA; shard slow tiers when broader evidence is justified. Reserve the \`dotnet_solution\` profile for shared test/runtime infrastructure, Feature/PR integration, or an explicit QA-plan requirement. Use \`forge.start_managed_command\` only for other commands expected to exceed two minutes. Continue independent QA work, then call \`forge.wait_managed_command\` for the durable terminal receipt; if it returns \`command_continues=true\`, continue useful work or wait again after \`retry_after_ms\`. Do not poll \`forge.get_managed_command\`. Include the terminal \`managed_command_id\` in command evidence. Forge records live progress outside the provider transcript.
|
|
63
|
+
For a negative assertion, make the command itself exit 0 when the forbidden condition is absent. In particular, raw \`rg\` exit 1 means no matches but cannot be submitted as passing evidence; wrap it so no match exits 0, a match fails, and tool errors remain failures. Before every completion call, ensure passing command records have \`exit_code: 0\`, \`status: "pass"\`, and a summary. A rejected call writes no artifact; repair every issue returned by Forge and resubmit the same gate once without probing the filesystem.
|
|
62
64
|
The final verdict tool writes the canonical QA bundle under \`${artifactRoot}/\`: \`qa.plan.json\`, \`qa.evidence.json\`, \`qa.findings.json\`, \`qa.verdict.json\`, and \`qa.report.md\`.
|
|
63
65
|
Include \`policy_warnings\` in the final verdict for non-blocking Forge policy or scope mismatches that should remain visible after QA.
|
|
64
66
|
The legacy \`forge.complete_qa\` one-shot tool is not available in normal QA-cycle mode. Reporting QA completion in prose is not enough.`;
|
|
@@ -90,7 +92,7 @@ Required order:
|
|
|
90
92
|
5. Follow the returned dependency-graph instruction, validate keys/dependencies/order, then call \`forge.complete_decompose_dependency_graph(...)\`.
|
|
91
93
|
6. Follow the returned critic instruction, critique scope, DAG safety, manual reconciliation risk, and missing acceptance detail, then call \`forge.complete_decompose_critic(...)\`.
|
|
92
94
|
7. Follow the returned proposal instruction, then call \`forge.complete_decompose_proposal(...)\` with final JSON in \`proposal_json\`.
|
|
93
|
-
The final proposal tool writes \`decompose.proposal.json\` under \`.forge/work-items/{workItemId}/artifacts/\`. The proposal_json object must contain summary, optional blockedReason, and candidates. Work Order-to-Feature candidates may include \`estimated_qa_scope\`; Feature-to-Story candidates should include \`qa_scope\` with schema_version 1 plus scheduleIndex, waveIndex, expectedOutput, handoffSummary, and dependencyReason for dependency handoff.`;
|
|
95
|
+
The final proposal tool writes \`decompose.proposal.json\` under \`.forge/work-items/{workItemId}/artifacts/\`. The proposal_json object must contain summary, optional blockedReason, and candidates. Work Order-to-Feature candidates may include \`estimated_qa_scope\`; Feature-to-Story candidates should include \`qa_scope\` with schema_version 1 plus scheduleIndex, waveIndex, expectedOutput, handoffSummary, and dependencyReason for dependency handoff. Set \`qa_scope.required_capabilities\` to \`["docker_engine"]\` when implementation or validation starts containers or needs a Docker daemon; repository-specific service and image versions do not belong in the runner capability name.`;
|
|
94
96
|
}
|
|
95
97
|
export function completionPromptForDebugCycle(currentGate = "debug_repro") {
|
|
96
98
|
return `Forge completion tools are available through the MCP server named \`${toolServerName}\`.
|
|
@@ -116,13 +118,7 @@ export function nextDebugPhaseAfter(phase) {
|
|
|
116
118
|
}
|
|
117
119
|
export function completionToolNextInstruction(completedPhase, status, nextPhase) {
|
|
118
120
|
if (completedPhase === "qa") {
|
|
119
|
-
|
|
120
|
-
return "QA blocked. Forge recorded the blocker reason and will surface the run for human attention.";
|
|
121
|
-
}
|
|
122
|
-
if (status === "needs_rework") {
|
|
123
|
-
return "QA findings recorded. Forge has the structured evidence needed to route this work back for implementation rework.";
|
|
124
|
-
}
|
|
125
|
-
return "QA accepted. Forge has the structured evidence needed to move this work item to Sign-Off.";
|
|
121
|
+
return qaLocalCompletionInstruction(status);
|
|
126
122
|
}
|
|
127
123
|
if (completedPhase === "ci_repair") {
|
|
128
124
|
return status === "pass"
|
|
@@ -164,13 +160,7 @@ export function completionToolNextInstructionForDraftGate(completedGate, status,
|
|
|
164
160
|
}
|
|
165
161
|
export function completionToolNextInstructionForQaGate(completedGate, status, nextGate) {
|
|
166
162
|
if (!nextGate) {
|
|
167
|
-
|
|
168
|
-
return "QA blocked. Forge recorded the blocker reason and will surface the run for human attention.";
|
|
169
|
-
}
|
|
170
|
-
if (status === "needs_rework") {
|
|
171
|
-
return "QA findings recorded. Forge has the structured evidence needed to route this work back for implementation rework.";
|
|
172
|
-
}
|
|
173
|
-
return "QA accepted. Forge has the structured evidence needed to move this work item to Sign-Off.";
|
|
163
|
+
return qaLocalCompletionInstruction(status);
|
|
174
164
|
}
|
|
175
165
|
const skipped = status === "skipped";
|
|
176
166
|
const blocked = status === "blocked";
|
|
@@ -181,6 +171,15 @@ export function completionToolNextInstructionForQaGate(completedGate, status, ne
|
|
|
181
171
|
: `${completedGate} accepted. Continue to ${nextGate}.`;
|
|
182
172
|
return `${prefix} ${qaGateInstruction(nextGate)}`;
|
|
183
173
|
}
|
|
174
|
+
function qaLocalCompletionInstruction(status) {
|
|
175
|
+
if (status === "blocked") {
|
|
176
|
+
return "QA blocker recorded locally. API ingestion and validation are still pending before Forge can surface the run for human attention.";
|
|
177
|
+
}
|
|
178
|
+
if (status === "needs_rework") {
|
|
179
|
+
return "QA findings recorded locally. API ingestion and validation are still pending before Forge can route implementation rework.";
|
|
180
|
+
}
|
|
181
|
+
return "QA bundle recorded locally. API ingestion and validation are still pending; this receipt does not authorize Sign-Off.";
|
|
182
|
+
}
|
|
184
183
|
export function completionToolNextInstructionForDecomposeGate(completedGate, status, nextGate) {
|
|
185
184
|
if (status !== "pass") {
|
|
186
185
|
return `Stay in ${completedGate}, address the recorded blockers/rework, then call ${expectedDecomposeCompletionToolName(completedGate)} again.`;
|
|
@@ -217,7 +216,7 @@ export function completionToolNextInstructionForDebugGate(completedGate, status,
|
|
|
217
216
|
function qaGateInstruction(gate) {
|
|
218
217
|
switch (gate) {
|
|
219
218
|
case "qa_regression":
|
|
220
|
-
return "Run or explicitly skip applicable regression/build/lint/typecheck/e2e checks, then call forge.complete_qa_regression.
|
|
219
|
+
return "Run or explicitly skip applicable regression/build/lint/typecheck/e2e checks, then call forge.complete_qa_regression. For normal Story QA, prefer focused checks and runner-authored forge.start_managed_verification tier profiles; reserve dotnet_solution for shared infrastructure, Feature/PR integration, or an explicit plan requirement. Use forge.start_managed_command only for other commands expected to exceed two minutes, continue independent QA, then call forge.wait_managed_command and include its terminal managed_command_id in command evidence. If the wait returns command_continues=true, continue useful work or wait again after retry_after_ms; do not poll get_managed_command. Normalize negative assertions so an expected no-match exits 0. A reproduced failure outside the current diff and acceptance criteria may remain non-blocking only when the current item has independent passing evidence: preserve it as an out_of_scope_failure with candidate_bug_id, scope_evidence, acceptance_criteria_independent: true, and a failing focused repro command/exit code. Never classify an unexplained, in-scope, or infrastructure failure this way.";
|
|
221
220
|
case "qa_api":
|
|
222
221
|
return "Run or explicitly skip applicable API/backend contract checks, then call forge.complete_qa_api.";
|
|
223
222
|
case "qa_manual_web":
|
|
@@ -4,6 +4,7 @@ export declare function prepareCompletionToolRuntime(options: CompletionToolRunt
|
|
|
4
4
|
export declare function buildCompletionToolServerArgs(config: CompletionToolServerConfig): readonly string[];
|
|
5
5
|
export declare function codexMcpConfigArgs(runtime: CompletionToolRuntime): readonly string[];
|
|
6
6
|
export declare function cursorMcpServerConfig(runtime: CompletionToolRuntime): unknown;
|
|
7
|
+
export declare function claudeMcpServerConfig(runtime: CompletionToolRuntime): unknown;
|
|
7
8
|
export declare function completionToolNamesForServer(config: CompletionToolServerConfig): readonly string[];
|
|
8
9
|
export declare class CompletionToolServerSession {
|
|
9
10
|
private readonly config;
|
|
@@ -27,7 +28,7 @@ export declare class CompletionToolServerSession {
|
|
|
27
28
|
private advanceAfterQaGate;
|
|
28
29
|
private advanceAfterDebugGate;
|
|
29
30
|
}
|
|
30
|
-
export declare function completionToolRuntimeEnvironment(runtimeEnvironment?: Readonly<Record<string, string
|
|
31
|
+
export declare function completionToolRuntimeEnvironment(runtimeEnvironment?: Readonly<Record<string, string>>, nativeTestPostgresFallbackPath?: string | null): Readonly<Record<string, string>>;
|
|
31
32
|
/**
|
|
32
33
|
* A solution-wide Forge test is long enough that a generic provider shell can
|
|
33
34
|
* expire while VSTest is still healthy. On managed host attempts, only the
|
|
@@ -45,3 +46,4 @@ export declare function parseCompletionMcpArgs(argv: readonly string[]): {
|
|
|
45
46
|
readonly error: string;
|
|
46
47
|
};
|
|
47
48
|
export declare function completionRedactionEnv(secrets: readonly string[]): Readonly<Record<string, string>>;
|
|
49
|
+
export declare function completionToolRejectedResult(toolName: string, error: unknown): Readonly<Record<string, unknown>>;
|