@loucompanion/forge-bridge 0.1.3-dev.b6be5e7f9668 → 0.1.3-dev.bf78fdcaa623
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 +44 -0
- package/dist/bridge/bridge.base.d.ts +5 -1
- package/dist/bridge/bridge.service.d.ts +36 -2
- package/dist/bridge/bridge.service.js +588 -18
- package/dist/bridge/bridge.types.d.ts +114 -1
- package/dist/cli/cli.help.d.ts +2 -0
- package/dist/cli/cli.help.js +183 -0
- package/dist/cli/cli.service.d.ts +12 -0
- package/dist/cli/cli.service.js +176 -184
- package/dist/cli/local-config.d.ts +2 -0
- package/dist/cli/local-config.js +6 -2
- package/dist/sandbox/command-runner.d.ts +4 -0
- package/dist/sandbox/command-runner.js +47 -0
- package/dist/sandbox/contract.d.ts +5 -0
- package/dist/sandbox/contract.js +276 -0
- package/dist/sandbox/event-budget.d.ts +11 -0
- package/dist/sandbox/event-budget.js +27 -0
- package/dist/sandbox/image-policy.d.ts +33 -0
- package/dist/sandbox/image-policy.js +245 -0
- package/dist/sandbox/index.d.ts +5 -0
- package/dist/sandbox/index.js +5 -0
- package/dist/sandbox/lease-watchdog.d.ts +18 -0
- package/dist/sandbox/lease-watchdog.js +67 -0
- package/dist/sandbox/preflight.d.ts +36 -0
- package/dist/sandbox/preflight.js +239 -0
- package/dist/sandbox/process.d.ts +20 -0
- package/dist/sandbox/process.js +124 -0
- package/dist/sandbox/provider-auth/codex.d.ts +26 -0
- package/dist/sandbox/provider-auth/codex.js +212 -0
- package/dist/sandbox/repository-lock.d.ts +5 -0
- package/dist/sandbox/repository-lock.js +83 -0
- package/dist/sandbox/runtime.d.ts +216 -0
- package/dist/sandbox/runtime.js +1658 -0
- package/dist/sandbox/sandbox.types.d.ts +83 -0
- package/dist/sandbox/sandbox.types.js +1 -0
- package/dist/sandbox/worker-supervisor.d.ts +13 -0
- package/dist/sandbox/worker-supervisor.js +36 -0
- package/dist/sandbox/worker.d.ts +1 -0
- package/dist/sandbox/worker.js +228 -0
- package/dist/sandbox/workspace.service.d.ts +12 -0
- package/dist/sandbox/workspace.service.js +49 -0
- package/dist/session/phase-completion.artifacts.d.ts +10 -0
- package/dist/session/phase-completion.artifacts.js +620 -0
- package/dist/session/phase-completion.d.ts +32 -44
- package/dist/session/phase-completion.helpers.d.ts +112 -0
- package/dist/session/phase-completion.helpers.js +660 -0
- package/dist/session/phase-completion.js +28 -3485
- package/dist/session/phase-completion.prompts.d.ts +14 -0
- package/dist/session/phase-completion.prompts.js +220 -0
- package/dist/session/phase-completion.runtime.d.ts +36 -0
- package/dist/session/phase-completion.runtime.js +609 -0
- package/dist/session/phase-completion.schemas.d.ts +3 -0
- package/dist/session/phase-completion.schemas.js +707 -0
- package/dist/session/phase-completion.validation.d.ts +34 -0
- package/dist/session/phase-completion.validation.js +724 -0
- package/dist/session/provider-cli/provider-cli.artifacts.d.ts +93 -0
- package/dist/session/provider-cli/provider-cli.artifacts.js +754 -0
- package/dist/session/provider-cli/provider-cli.base.d.ts +1 -0
- package/dist/session/provider-cli/provider-cli.deterministic-payloads.d.ts +12 -0
- package/dist/session/provider-cli/provider-cli.deterministic-payloads.js +279 -0
- package/dist/session/provider-cli/provider-cli.deterministic.d.ts +14 -0
- package/dist/session/provider-cli/provider-cli.deterministic.js +329 -0
- package/dist/session/provider-cli/provider-cli.process.d.ts +32 -0
- package/dist/session/provider-cli/provider-cli.process.js +287 -0
- package/dist/session/provider-cli/provider-cli.prompts.d.ts +44 -0
- package/dist/session/provider-cli/provider-cli.prompts.js +528 -0
- package/dist/session/provider-cli/provider-cli.qa-sidecars.d.ts +11 -0
- package/dist/session/provider-cli/provider-cli.qa-sidecars.js +65 -0
- package/dist/session/provider-cli/provider-cli.runtime.d.ts +9 -0
- package/dist/session/provider-cli/provider-cli.runtime.js +40 -0
- package/dist/session/provider-cli/provider-cli.service.d.ts +9 -53
- package/dist/session/provider-cli/provider-cli.service.js +161 -2527
- package/dist/session/provider-cli/provider-cli.types.d.ts +1 -0
- package/dist/session/provider-cli/provider-output-state.d.ts +20 -0
- package/dist/session/provider-cli/provider-output-state.js +445 -0
- package/dist/session/session.service.js +34 -24
- package/dist/session/session.types.d.ts +106 -1
- package/dist/session/session.types.js +248 -3
- package/dist/shared/process/windows-launch.d.ts +8 -0
- package/dist/shared/process/windows-launch.js +41 -0
- package/dist/shared/provider-capabilities.d.ts +1 -0
- package/dist/shared/provider-capabilities.js +3 -0
- package/dist/shared/redaction/redaction.js +4 -2
- package/dist/version.d.ts +1 -1
- package/dist/version.js +1 -1
- package/dist/worktree/artifact-injector.js +39 -6
- package/dist/worktree/dependency-warmer.d.ts +11 -0
- package/dist/worktree/dependency-warmer.js +45 -1
- package/dist/worktree/worktree.service.d.ts +2 -1
- package/dist/worktree/worktree.service.js +32 -12
- package/package.json +4 -2
|
@@ -2,6 +2,7 @@ import type { ProviderCliDraftContext, ProviderCliRunContext, ProviderCliRunResu
|
|
|
2
2
|
export interface ProviderCliService {
|
|
3
3
|
runImplementation(context: ProviderCliRunContext): Promise<ProviderCliRunResult>;
|
|
4
4
|
runArchitect(context: ProviderCliRunContext): Promise<ProviderCliRunResult>;
|
|
5
|
+
runCiRepair(context: ProviderCliRunContext): Promise<ProviderCliRunResult>;
|
|
5
6
|
runConflictResolve(context: ProviderCliRunContext): Promise<ProviderCliRunResult>;
|
|
6
7
|
runQa(context: ProviderCliRunContext): Promise<ProviderCliRunResult>;
|
|
7
8
|
runDraft(context: ProviderCliDraftContext): Promise<ProviderCliRunResult>;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { RunOnceDispatchPacket } from "../session.types.js";
|
|
2
|
+
import type { CompletionPhase, QaCompletionGate } from "../phase-completion.js";
|
|
3
|
+
export declare function deterministicCompletionPayload(phase: CompletionPhase, dispatch: RunOnceDispatchPacket, changesetBody: string): Record<string, unknown>;
|
|
4
|
+
export declare function deterministicArchitectPayload(dispatch: RunOnceDispatchPacket): Record<string, unknown>;
|
|
5
|
+
export declare function deterministicQaGatePayload(options: {
|
|
6
|
+
readonly gate: QaCompletionGate;
|
|
7
|
+
readonly criterion: string;
|
|
8
|
+
readonly summary: string;
|
|
9
|
+
readonly missingLaunchTarget: boolean;
|
|
10
|
+
readonly browserChecks: readonly Record<string, unknown>[];
|
|
11
|
+
readonly sidecars: readonly Record<string, unknown>[];
|
|
12
|
+
}): Record<string, unknown>;
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
import crypto from "node:crypto";
|
|
2
|
+
export function deterministicCompletionPayload(phase, dispatch, changesetBody) {
|
|
3
|
+
const changesetHash = sha256(changesetBody);
|
|
4
|
+
const criterion = dispatch.workItem.acceptanceCriteria || "Deterministic bridge changeset exists";
|
|
5
|
+
if (phase === "implementation") {
|
|
6
|
+
return {
|
|
7
|
+
status: "pass",
|
|
8
|
+
summary: "forge-bridge produced a deterministic implementation changeset.",
|
|
9
|
+
plan: [{ step: "Create deterministic changeset", status: "done" }],
|
|
10
|
+
changed_files: [{ path: "FORGE_PHASE5_CHANGESET.md", summary: "Recorded deterministic bridge evidence." }],
|
|
11
|
+
tests_added_or_updated: [{ path: "FORGE_PHASE5_CHANGESET.md", summary: "The deterministic bridge committed FORGE_PHASE5_CHANGESET.md as implementation evidence." }],
|
|
12
|
+
docs_added_or_updated: [{ path: "FORGE_PHASE5_CHANGESET.md", summary: "Deterministic evidence document created for the run." }],
|
|
13
|
+
acceptance_criteria: [
|
|
14
|
+
{
|
|
15
|
+
criterion,
|
|
16
|
+
evidence: `FORGE_PHASE5_CHANGESET.md was committed with content hash ${changesetHash}.`,
|
|
17
|
+
status: "satisfied"
|
|
18
|
+
}
|
|
19
|
+
],
|
|
20
|
+
known_gaps: [],
|
|
21
|
+
next_phase_context: "Review FORGE_PHASE5_CHANGESET.md and the bridge commit before validation."
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
if (phase === "debug_repro") {
|
|
25
|
+
return {
|
|
26
|
+
status: "pass",
|
|
27
|
+
summary: "forge-bridge reproduced the bug deterministically.",
|
|
28
|
+
repro_steps: ["Run the deterministic fake-worker debug reproduction."],
|
|
29
|
+
observed: "The configured bug scenario is reproducible in deterministic bridge mode.",
|
|
30
|
+
expected: "The bug scenario should be fixed before QA handoff.",
|
|
31
|
+
evidence: [{ kind: "changeset", path: "FORGE_PHASE5_CHANGESET.md", content_hash: changesetHash }]
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
if (phase === "debug_root_cause") {
|
|
35
|
+
return {
|
|
36
|
+
status: "pass",
|
|
37
|
+
summary: "forge-bridge isolated a deterministic root cause.",
|
|
38
|
+
root_cause: "The deterministic fake-worker bug is represented by the generated changeset evidence.",
|
|
39
|
+
evidence: [{ kind: "changeset", path: "FORGE_PHASE5_CHANGESET.md", content_hash: changesetHash }],
|
|
40
|
+
eliminated_alternatives: [{ summary: "No alternate deterministic fake-worker path was implicated." }],
|
|
41
|
+
risk_area: "forge-bridge deterministic debug lane"
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
if (phase === "debug_fix") {
|
|
45
|
+
return {
|
|
46
|
+
status: "pass",
|
|
47
|
+
summary: "forge-bridge applied a deterministic debug fix.",
|
|
48
|
+
changed_files: [{ path: "FORGE_PHASE5_CHANGESET.md", summary: "Recorded deterministic debug fix evidence." }],
|
|
49
|
+
tests_added_or_updated: [{ path: "FORGE_PHASE5_CHANGESET.md", summary: "Deterministic debug evidence doubles as fake-worker regression coverage." }],
|
|
50
|
+
remaining_risks: [],
|
|
51
|
+
resolved_findings: (dispatch.qaRework?.findingIds ?? []).map((qaId) => ({
|
|
52
|
+
qa_id: qaId,
|
|
53
|
+
status: "resolved",
|
|
54
|
+
summary: `Resolved deterministic QA finding ${qaId}.`,
|
|
55
|
+
evidence: { path: "FORGE_PHASE5_CHANGESET.md", content_hash: changesetHash },
|
|
56
|
+
changed_files: [{ path: "FORGE_PHASE5_CHANGESET.md", summary: "Recorded deterministic debug fix evidence." }],
|
|
57
|
+
tests_added_or_updated: [{ path: "FORGE_PHASE5_CHANGESET.md", summary: "Deterministic fake-worker regression evidence." }],
|
|
58
|
+
regression_evidence: [{ command: "forge-bridge deterministic debug regression", status: "pass" }]
|
|
59
|
+
}))
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
if (phase === "debug_regression") {
|
|
63
|
+
return {
|
|
64
|
+
status: "pass",
|
|
65
|
+
summary: "forge-bridge proved the deterministic debug regression is QA-ready.",
|
|
66
|
+
commands: [{ command: "forge-bridge deterministic debug regression", exit_code: 0, status: "pass", summary: "Deterministic debug regression completed." }],
|
|
67
|
+
required_regressions: ["Original deterministic repro no longer fails."],
|
|
68
|
+
build_status: "not_applicable",
|
|
69
|
+
test_status: "pass",
|
|
70
|
+
repro_negative: true,
|
|
71
|
+
qa_ready: true
|
|
72
|
+
};
|
|
73
|
+
}
|
|
74
|
+
if (phase === "self_review") {
|
|
75
|
+
return {
|
|
76
|
+
status: "pass",
|
|
77
|
+
summary: "forge-bridge fake worker completed deterministic self-review.",
|
|
78
|
+
reviewed_diff: true,
|
|
79
|
+
findings: [],
|
|
80
|
+
security_review: {
|
|
81
|
+
status: "pass",
|
|
82
|
+
notes: "No secret handling, auth, or destructive behavior changes were introduced by the deterministic changeset."
|
|
83
|
+
},
|
|
84
|
+
edge_case_review: {
|
|
85
|
+
status: "pass",
|
|
86
|
+
notes: "The deterministic fake-worker changeset is intentionally narrow and repeatable."
|
|
87
|
+
},
|
|
88
|
+
design_review: {
|
|
89
|
+
status: "not_applicable",
|
|
90
|
+
screenshots: [],
|
|
91
|
+
notes: "No UI changes."
|
|
92
|
+
},
|
|
93
|
+
deterministic_comparison: {
|
|
94
|
+
status: "pass",
|
|
95
|
+
command_or_tool: "git diff --stat HEAD~1..HEAD",
|
|
96
|
+
notes: `Reviewed deterministic changeset with content hash ${changesetHash}.`
|
|
97
|
+
},
|
|
98
|
+
fixes_applied: [],
|
|
99
|
+
remaining_risks: []
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
return {
|
|
103
|
+
status: "pass",
|
|
104
|
+
summary: "forge-bridge fake worker completed deterministic validation.",
|
|
105
|
+
commands: [
|
|
106
|
+
{
|
|
107
|
+
command: "git status --short && git diff --check",
|
|
108
|
+
exit_code: 0,
|
|
109
|
+
status: "pass",
|
|
110
|
+
summary: "Deterministic validation command completed for the fake-worker changeset."
|
|
111
|
+
}
|
|
112
|
+
],
|
|
113
|
+
acceptance_criteria: [
|
|
114
|
+
{
|
|
115
|
+
criterion,
|
|
116
|
+
evidence: `FORGE_PHASE5_CHANGESET.md exists with content hash ${changesetHash}.`,
|
|
117
|
+
status: "satisfied"
|
|
118
|
+
}
|
|
119
|
+
],
|
|
120
|
+
build_status: "pass",
|
|
121
|
+
test_status: "pass",
|
|
122
|
+
lint_status: "not_applicable",
|
|
123
|
+
design_status: "not_applicable",
|
|
124
|
+
drift_checks: [
|
|
125
|
+
{
|
|
126
|
+
command_or_tool: "git diff --check",
|
|
127
|
+
status: "pass",
|
|
128
|
+
summary: "No deterministic drift or whitespace errors were reported for the fake-worker changeset."
|
|
129
|
+
}
|
|
130
|
+
],
|
|
131
|
+
ready_for_dev_test: true
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
export function deterministicArchitectPayload(dispatch) {
|
|
135
|
+
const planId = `architect-${dispatch.workItem.id.replace(/-/g, "")}`;
|
|
136
|
+
return {
|
|
137
|
+
status: "pass",
|
|
138
|
+
summary: "forge-bridge produced a deterministic Architect plan.",
|
|
139
|
+
implementation_plan: {
|
|
140
|
+
schema_version: 1,
|
|
141
|
+
plan_id: planId,
|
|
142
|
+
story_id: dispatch.workItem.id,
|
|
143
|
+
phases: [
|
|
144
|
+
{ id: "phase-1", name: "Foundation", tasks: ["task-1"], gate: "phase-1-gate" }
|
|
145
|
+
],
|
|
146
|
+
tasks: [
|
|
147
|
+
{
|
|
148
|
+
id: "task-1",
|
|
149
|
+
title: `Implement ${dispatch.workItem.title}`,
|
|
150
|
+
phase: "phase-1",
|
|
151
|
+
summary: "Deterministic bridge Architect task proving MCP artifact contract persistence.",
|
|
152
|
+
acceptance_criteria: [dispatch.workItem.acceptanceCriteria || `Preserve story intent for ${dispatch.workItem.title}`],
|
|
153
|
+
tests: [
|
|
154
|
+
{ command: "dotnet test Forge.sln --filter Architect", purpose: "Architect bridge contract regression" }
|
|
155
|
+
]
|
|
156
|
+
}
|
|
157
|
+
],
|
|
158
|
+
dependencies: [],
|
|
159
|
+
phase_gates: [
|
|
160
|
+
{ id: "phase-1-gate", phase: "phase-1", checks: ["Task tests pass", "Artifacts validate"] }
|
|
161
|
+
]
|
|
162
|
+
},
|
|
163
|
+
qa_plan: {
|
|
164
|
+
schema_version: 1,
|
|
165
|
+
plan_id: planId,
|
|
166
|
+
final_qa: [
|
|
167
|
+
{ id: "qa-1", coverage: `Verify Architect plan artifacts and pending approval status for ${dispatch.workItem.title}` }
|
|
168
|
+
]
|
|
169
|
+
},
|
|
170
|
+
report_markdown: `# Architect Report\n\nWork item: ${dispatch.workItem.id}\nTitle: ${dispatch.workItem.title}\n\nVerdict: pass\n\nDeterministic bridge Architect plan produced for pending approval.\n`
|
|
171
|
+
};
|
|
172
|
+
}
|
|
173
|
+
export function deterministicQaGatePayload(options) {
|
|
174
|
+
const blocked = options.missingLaunchTarget;
|
|
175
|
+
const skippedBecauseBlocked = blocked
|
|
176
|
+
? {
|
|
177
|
+
status: "skipped",
|
|
178
|
+
summary: "Skipped because browser QA was blocked by missing launch target.",
|
|
179
|
+
reason: "Upstream browser QA blocker prevents meaningful downstream evidence."
|
|
180
|
+
}
|
|
181
|
+
: null;
|
|
182
|
+
switch (options.gate) {
|
|
183
|
+
case "qa_plan":
|
|
184
|
+
return {
|
|
185
|
+
status: "pass",
|
|
186
|
+
summary: "Deterministic QA plan recorded.",
|
|
187
|
+
checklist: [
|
|
188
|
+
"Review the validated work item against acceptance criteria.",
|
|
189
|
+
"Run deterministic regression evidence.",
|
|
190
|
+
"Confirm no in-scope QA findings remain."
|
|
191
|
+
],
|
|
192
|
+
acceptance_criteria: [options.criterion]
|
|
193
|
+
};
|
|
194
|
+
case "qa_regression":
|
|
195
|
+
return {
|
|
196
|
+
status: "pass",
|
|
197
|
+
summary: "Deterministic regression QA completed.",
|
|
198
|
+
commands: [
|
|
199
|
+
{
|
|
200
|
+
command: "forge-bridge deterministic QA",
|
|
201
|
+
exit_code: 0,
|
|
202
|
+
status: "pass",
|
|
203
|
+
summary: "Deterministic regression evidence completed."
|
|
204
|
+
}
|
|
205
|
+
],
|
|
206
|
+
notes: "Regression gate passed in deterministic fake-worker mode."
|
|
207
|
+
};
|
|
208
|
+
case "qa_api":
|
|
209
|
+
return {
|
|
210
|
+
status: "skipped",
|
|
211
|
+
summary: "No API/backend surface was identified for deterministic QA.",
|
|
212
|
+
reason: "The deterministic fake-worker changeset does not exercise API/backend behavior."
|
|
213
|
+
};
|
|
214
|
+
case "qa_manual_web":
|
|
215
|
+
return blocked
|
|
216
|
+
? {
|
|
217
|
+
status: "blocked",
|
|
218
|
+
summary: "Browser QA could not run because no app launch target was provided.",
|
|
219
|
+
blocker_reason: "missing_app_launch_target"
|
|
220
|
+
}
|
|
221
|
+
: options.browserChecks.length > 0
|
|
222
|
+
? {
|
|
223
|
+
status: "pass",
|
|
224
|
+
summary: "Deterministic browser QA exercised the configured launch target.",
|
|
225
|
+
browser_checks: options.browserChecks,
|
|
226
|
+
sidecars: options.sidecars,
|
|
227
|
+
notes: "Browser sidecar evidence recorded."
|
|
228
|
+
}
|
|
229
|
+
: {
|
|
230
|
+
status: "skipped",
|
|
231
|
+
summary: "Manual web QA was not required for this deterministic run.",
|
|
232
|
+
reason: "Forge did not require browser QA and no launch target was necessary."
|
|
233
|
+
};
|
|
234
|
+
case "qa_bug_repro":
|
|
235
|
+
return skippedBecauseBlocked ?? {
|
|
236
|
+
status: "skipped",
|
|
237
|
+
summary: "No failed QA observation required bug reproduction.",
|
|
238
|
+
reason: "No defects or candidate bugs were observed."
|
|
239
|
+
};
|
|
240
|
+
case "qa_findings":
|
|
241
|
+
return skippedBecauseBlocked ?? {
|
|
242
|
+
status: "pass",
|
|
243
|
+
summary: "No QA findings or candidate bugs were identified.",
|
|
244
|
+
findings: [],
|
|
245
|
+
candidate_bugs: []
|
|
246
|
+
};
|
|
247
|
+
case "qa_verdict":
|
|
248
|
+
return {
|
|
249
|
+
status: blocked ? "blocked" : "pass",
|
|
250
|
+
summary: options.summary,
|
|
251
|
+
checklist: [
|
|
252
|
+
"Review the validated work item against acceptance criteria.",
|
|
253
|
+
"Run deterministic regression evidence.",
|
|
254
|
+
blocked ? "Record missing browser launch target blocker." : "Confirm no in-scope QA findings remain."
|
|
255
|
+
],
|
|
256
|
+
tested_acceptance_criteria: blocked ? [] : [options.criterion],
|
|
257
|
+
commands: blocked
|
|
258
|
+
? []
|
|
259
|
+
: [
|
|
260
|
+
{
|
|
261
|
+
command: "forge-bridge deterministic QA",
|
|
262
|
+
exit_code: 0,
|
|
263
|
+
status: "pass",
|
|
264
|
+
summary: "Deterministic QA command completed."
|
|
265
|
+
}
|
|
266
|
+
],
|
|
267
|
+
browser_checks: blocked ? [] : options.browserChecks,
|
|
268
|
+
sidecars: blocked ? [] : options.sidecars,
|
|
269
|
+
notes: options.summary,
|
|
270
|
+
findings: [],
|
|
271
|
+
candidate_bugs: [],
|
|
272
|
+
blocker_reason: blocked ? "missing_app_launch_target" : null,
|
|
273
|
+
report_markdown: `# QA report\n\n${options.summary}\n`
|
|
274
|
+
};
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
function sha256(content) {
|
|
278
|
+
return crypto.createHash("sha256").update(content).digest("hex");
|
|
279
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { GitClient } from "../../shared/git/git.js";
|
|
2
|
+
import type { ProviderCliService } from "./provider-cli.base.js";
|
|
3
|
+
import type { ProviderCliDraftContext, ProviderCliRunContext, ProviderCliRunResult } from "./provider-cli.types.js";
|
|
4
|
+
export declare class DeterministicProviderCliService implements ProviderCliService {
|
|
5
|
+
private readonly git;
|
|
6
|
+
constructor(git?: GitClient);
|
|
7
|
+
runDraft(context: ProviderCliDraftContext): Promise<ProviderCliRunResult>;
|
|
8
|
+
runImplementation(context: ProviderCliRunContext): Promise<ProviderCliRunResult>;
|
|
9
|
+
runArchitect(context: ProviderCliRunContext): Promise<ProviderCliRunResult>;
|
|
10
|
+
runQa(context: ProviderCliRunContext): Promise<ProviderCliRunResult>;
|
|
11
|
+
runCiRepair(context: ProviderCliRunContext): Promise<ProviderCliRunResult>;
|
|
12
|
+
runConflictResolve(context: ProviderCliRunContext): Promise<ProviderCliRunResult>;
|
|
13
|
+
private runPullRequestRepair;
|
|
14
|
+
}
|
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
import { GitClient } from "../../shared/git/git.js";
|
|
4
|
+
import { buildSpecBody, selectLaneCommand } from "../../worktree/artifact-injector.js";
|
|
5
|
+
import { completionPhaseFromDispatch, debugCompletionPhaseOrder, expectedCompletionToolName, expectedDebugCompletionToolName, expectedQaCompletionToolName, qaCompletionGateOrder, readCompletionArtifact, usesDebugCycleCompletion, usesDevelopCycleCompletion, writeCompletionArtifact } from "../phase-completion.js";
|
|
6
|
+
import { emitArchitectStructuredEvents, emitCompletionArtifactRecorded, emitQaArtifactRecorded, emitQaGateAccepted, emitQaStructuredEvents, isDebugCompletionPhaseForProvider, nextQaGate, phaseCompleteEventType, qaArtifactRootForDispatch, readQaCompletionArtifactBundle, readQaGateArtifact } from "./provider-cli.artifacts.js";
|
|
7
|
+
import { deterministicBrowserCheck, deterministicBrowserSidecar, hasLaunchTarget, writeQaSidecarFiles } from "./provider-cli.qa-sidecars.js";
|
|
8
|
+
import { buildProviderCliCommand, formatProviderCliCommand, isPullRequestCheckRepairDispatch, isPullRequestCommentsDispatch, singleLine } from "./provider-cli.prompts.js";
|
|
9
|
+
import { estimatedDeterministicUsage, exactRedactionSecrets } from "./provider-cli.runtime.js";
|
|
10
|
+
import { buildProviderUsageEvent } from "./provider-usage.js";
|
|
11
|
+
import { deterministicArchitectPayload, deterministicCompletionPayload, deterministicQaGatePayload } from "./provider-cli.deterministic-payloads.js";
|
|
12
|
+
export class DeterministicProviderCliService {
|
|
13
|
+
git;
|
|
14
|
+
constructor(git = new GitClient()) {
|
|
15
|
+
this.git = git;
|
|
16
|
+
}
|
|
17
|
+
async runDraft(context) {
|
|
18
|
+
const content = JSON.stringify(deterministicDraftPayload(context.dispatch));
|
|
19
|
+
await context.emit({
|
|
20
|
+
role: "assistant",
|
|
21
|
+
kind: "message",
|
|
22
|
+
content,
|
|
23
|
+
eventType: "draft_complete"
|
|
24
|
+
});
|
|
25
|
+
await context.emit(buildProviderUsageEvent(context.dispatch, estimatedDeterministicUsage()));
|
|
26
|
+
return { summary: "forge-bridge generated deterministic draft." };
|
|
27
|
+
}
|
|
28
|
+
async runImplementation(context) {
|
|
29
|
+
const { dispatch, worktree } = context;
|
|
30
|
+
const phase = completionPhaseFromDispatch(dispatch);
|
|
31
|
+
const developCycle = usesDevelopCycleCompletion(dispatch);
|
|
32
|
+
const debugCycle = usesDebugCycleCompletion(dispatch);
|
|
33
|
+
const providerCommand = buildProviderCliCommand(dispatch);
|
|
34
|
+
const providerCommandText = formatProviderCliCommand(providerCommand);
|
|
35
|
+
const metadataRoot = path.join(worktree.worktreePath, ".forge", "work-items", dispatch.workItem.id);
|
|
36
|
+
const artifactRoot = path.join(metadataRoot, "artifacts");
|
|
37
|
+
await fs.mkdir(artifactRoot, { recursive: true });
|
|
38
|
+
const laneCommand = selectLaneCommand(dispatch.artifacts, dispatch.execution.lane, dispatch.cycleRun.phase);
|
|
39
|
+
if (!laneCommand) {
|
|
40
|
+
throw new Error(`Lane command for lane '${dispatch.execution.lane}' phase '${dispatch.cycleRun.phase}' was not resolved.`);
|
|
41
|
+
}
|
|
42
|
+
const specBody = `${buildSpecBody(laneCommand, dispatch.workItem, phase)}
|
|
43
|
+
## Execution
|
|
44
|
+
|
|
45
|
+
- Provider: ${singleLine(dispatch.execution.provider)}
|
|
46
|
+
- Model: ${singleLine(dispatch.execution.model)}
|
|
47
|
+
- Command: ${providerCommandText}
|
|
48
|
+
`;
|
|
49
|
+
const specPath = path.join(metadataRoot, "spec.md");
|
|
50
|
+
await fs.writeFile(specPath, specBody, "utf8");
|
|
51
|
+
await fs.writeFile(path.join(metadataRoot, "spec.meta.json"), JSON.stringify({ schema_version: 1, status: "approved", produced_by: "forge-lane-command" }) + "\n", "utf8");
|
|
52
|
+
const changesetPath = path.join(worktree.worktreePath, "FORGE_PHASE5_CHANGESET.md");
|
|
53
|
+
const changesetBody = `# Forge Phase 5 bridge changeset\n\nWork item: ${dispatch.workItem.id}\nTitle: ${dispatch.workItem.title}\nBranch: ${worktree.branchName}\nProvider: ${singleLine(dispatch.execution.provider)}\nModel: ${singleLine(dispatch.execution.model)}\nCommand: ${providerCommandText}\n\nThis deterministic file is written by forge-bridge run-once through the shared session/worktree service path.\n`;
|
|
54
|
+
await fs.writeFile(changesetPath, changesetBody, "utf8");
|
|
55
|
+
await this.git.run(worktree.worktreePath, ["add", "FORGE_PHASE5_CHANGESET.md"]);
|
|
56
|
+
await this.git.run(worktree.worktreePath, ["commit", "--allow-empty", "-m", `forge bridge changeset for ${dispatch.workItem.id.slice(0, 8)}`]);
|
|
57
|
+
await context.emit({
|
|
58
|
+
role: "worker",
|
|
59
|
+
kind: "file_edit",
|
|
60
|
+
content: `Created bridge changeset at ${changesetPath}`,
|
|
61
|
+
eventType: "changeset"
|
|
62
|
+
});
|
|
63
|
+
const phases = debugCycle
|
|
64
|
+
? debugCompletionPhaseOrder
|
|
65
|
+
: developCycle
|
|
66
|
+
? ["implementation", "self_review", "validation"]
|
|
67
|
+
: [phase];
|
|
68
|
+
const completionArtifacts = [];
|
|
69
|
+
for (const completionPhase of phases) {
|
|
70
|
+
await writeCompletionArtifact({
|
|
71
|
+
worktreePath: worktree.worktreePath,
|
|
72
|
+
workItemId: dispatch.workItem.id,
|
|
73
|
+
phase: completionPhase,
|
|
74
|
+
producedBy: "forge-bridge",
|
|
75
|
+
redactionSecrets: exactRedactionSecrets(dispatch)
|
|
76
|
+
}, isDebugCompletionPhaseForProvider(completionPhase)
|
|
77
|
+
? expectedDebugCompletionToolName(completionPhase)
|
|
78
|
+
: expectedCompletionToolName(completionPhase), deterministicCompletionPayload(completionPhase, dispatch, changesetBody));
|
|
79
|
+
const completionArtifact = await readCompletionArtifact(worktree.worktreePath, dispatch.workItem.id, completionPhase);
|
|
80
|
+
if (!completionArtifact) {
|
|
81
|
+
throw new Error(`Deterministic provider did not produce the Forge ${completionPhase} completion artifact.`);
|
|
82
|
+
}
|
|
83
|
+
completionArtifacts.push(completionArtifact);
|
|
84
|
+
await emitCompletionArtifactRecorded(context.emit, worktree.worktreePath, completionArtifact);
|
|
85
|
+
await context.emit({
|
|
86
|
+
role: "assistant",
|
|
87
|
+
kind: "message",
|
|
88
|
+
content: `${completionPhase.replace(/_/g, " ")} complete. forge-bridge produced deterministic phase artifacts.`,
|
|
89
|
+
eventType: phaseCompleteEventType(completionPhase)
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
await context.emit(buildProviderUsageEvent(dispatch, estimatedDeterministicUsage()));
|
|
93
|
+
return {
|
|
94
|
+
summary: `forge-bridge completed ${worktree.branchName} (${debugCycle ? "debug cycle" : developCycle ? "develop cycle" : phase}).`,
|
|
95
|
+
spec: {
|
|
96
|
+
repoPath: worktree.worktreePath,
|
|
97
|
+
path: specPath,
|
|
98
|
+
body: specBody,
|
|
99
|
+
producedBy: "forge-lane-command",
|
|
100
|
+
status: "approved"
|
|
101
|
+
},
|
|
102
|
+
artifact: completionArtifacts[0],
|
|
103
|
+
artifacts: completionArtifacts
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
async runArchitect(context) {
|
|
107
|
+
const { dispatch, worktree } = context;
|
|
108
|
+
const metadataRoot = path.join(worktree.worktreePath, ".forge", "work-items", dispatch.workItem.id);
|
|
109
|
+
await fs.mkdir(metadataRoot, { recursive: true });
|
|
110
|
+
const specBody = `---\ntitle: "${dispatch.workItem.title}"\nstatus: pending_approval\nproduced_by: forge-bridge\n---\n\n# ${dispatch.workItem.title}\n\nArchitect plan pending approval.\n`;
|
|
111
|
+
const specPath = path.join(metadataRoot, "spec.md");
|
|
112
|
+
await fs.writeFile(specPath, specBody, "utf8");
|
|
113
|
+
await fs.writeFile(path.join(metadataRoot, "spec.meta.json"), JSON.stringify({ schema_version: 1, status: "pending_approval", produced_by: "forge-bridge" }) + "\n", "utf8");
|
|
114
|
+
await writeCompletionArtifact({
|
|
115
|
+
worktreePath: worktree.worktreePath,
|
|
116
|
+
workItemId: dispatch.workItem.id,
|
|
117
|
+
phase: "architect",
|
|
118
|
+
producedBy: "forge-bridge",
|
|
119
|
+
redactionSecrets: exactRedactionSecrets(dispatch),
|
|
120
|
+
architectCycle: true
|
|
121
|
+
}, expectedCompletionToolName("architect"), deterministicArchitectPayload(dispatch));
|
|
122
|
+
const artifact = await readCompletionArtifact(worktree.worktreePath, dispatch.workItem.id, "architect");
|
|
123
|
+
if (!artifact) {
|
|
124
|
+
throw new Error("Deterministic provider did not produce the Forge Architect completion artifact.");
|
|
125
|
+
}
|
|
126
|
+
await emitCompletionArtifactRecorded(context.emit, worktree.worktreePath, artifact);
|
|
127
|
+
await emitArchitectStructuredEvents(context.emit, worktree.worktreePath, dispatch.workItem.id, artifact);
|
|
128
|
+
await context.emit({
|
|
129
|
+
role: "assistant",
|
|
130
|
+
kind: "message",
|
|
131
|
+
content: "Architect plan complete. forge-bridge produced deterministic planning artifacts.",
|
|
132
|
+
eventType: "architect_complete"
|
|
133
|
+
});
|
|
134
|
+
return {
|
|
135
|
+
summary: "forge-bridge completed Architect plan (deterministic).",
|
|
136
|
+
spec: {
|
|
137
|
+
repoPath: worktree.worktreePath,
|
|
138
|
+
path: specPath,
|
|
139
|
+
body: specBody,
|
|
140
|
+
producedBy: "forge-bridge",
|
|
141
|
+
status: "pending_approval"
|
|
142
|
+
},
|
|
143
|
+
artifact,
|
|
144
|
+
artifacts: [artifact]
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
async runQa(context) {
|
|
148
|
+
const { dispatch, worktree } = context;
|
|
149
|
+
await context.emit({
|
|
150
|
+
role: "worker",
|
|
151
|
+
kind: "message",
|
|
152
|
+
content: `Planning ${dispatch.execution.provider} QA checks through forge-bridge.`,
|
|
153
|
+
eventType: "qa_plan"
|
|
154
|
+
});
|
|
155
|
+
const qaBrowser = dispatch.qaBrowser;
|
|
156
|
+
const launchTarget = qaBrowser?.launchTarget ?? null;
|
|
157
|
+
const missingLaunchTarget = qaBrowser?.required === true && !hasLaunchTarget(launchTarget);
|
|
158
|
+
const browserSidecar = !missingLaunchTarget && qaBrowser?.required === true && launchTarget
|
|
159
|
+
? deterministicBrowserSidecar(dispatch.workItem.id, launchTarget, dispatch.cycleRun.ownerType, dispatch.cycleRun.ownerId)
|
|
160
|
+
: null;
|
|
161
|
+
const browserChecks = browserSidecar && launchTarget
|
|
162
|
+
? [deterministicBrowserCheck(launchTarget, browserSidecar.relativePath)]
|
|
163
|
+
: [];
|
|
164
|
+
const sidecars = await writeQaSidecarFiles(worktree.worktreePath, browserSidecar ? [browserSidecar] : []);
|
|
165
|
+
const summary = missingLaunchTarget
|
|
166
|
+
? "QA blocked because browser testing is required but no app launch target was provided."
|
|
167
|
+
: "All checks passed. forge-bridge QA completed.";
|
|
168
|
+
const criterion = dispatch.workItem.acceptanceCriteria.trim() || "Forge QA evidence exists";
|
|
169
|
+
for (const gate of qaCompletionGateOrder) {
|
|
170
|
+
await writeCompletionArtifact({
|
|
171
|
+
worktreePath: worktree.worktreePath,
|
|
172
|
+
workItemId: dispatch.workItem.id,
|
|
173
|
+
phase: "qa",
|
|
174
|
+
producedBy: "forge-bridge",
|
|
175
|
+
redactionSecrets: exactRedactionSecrets(dispatch),
|
|
176
|
+
qaCycle: true,
|
|
177
|
+
ownerType: dispatch.cycleRun.ownerType ?? undefined,
|
|
178
|
+
ownerId: dispatch.cycleRun.ownerId ?? undefined
|
|
179
|
+
}, expectedQaCompletionToolName(gate), deterministicQaGatePayload({
|
|
180
|
+
gate,
|
|
181
|
+
criterion,
|
|
182
|
+
summary,
|
|
183
|
+
missingLaunchTarget,
|
|
184
|
+
browserChecks,
|
|
185
|
+
sidecars
|
|
186
|
+
}));
|
|
187
|
+
const gateArtifact = await readQaGateArtifact(qaArtifactRootForDispatch(worktree.worktreePath, dispatch.workItem.id, dispatch), gate);
|
|
188
|
+
if (gateArtifact) {
|
|
189
|
+
await emitQaGateAccepted(context.emit, worktree.worktreePath, gateArtifact, nextQaGate(gate));
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
const qaArtifact = await readQaCompletionArtifactBundle(worktree.worktreePath, dispatch.workItem.id, dispatch.cycleRun.ownerType, dispatch.cycleRun.ownerId);
|
|
193
|
+
if (!qaArtifact) {
|
|
194
|
+
throw new Error("Deterministic QA did not produce the complete durable QA artifact bundle.");
|
|
195
|
+
}
|
|
196
|
+
await emitQaArtifactRecorded(context.emit, worktree.worktreePath, qaArtifact);
|
|
197
|
+
await emitQaStructuredEvents(context.emit, qaArtifact);
|
|
198
|
+
await context.emit(buildProviderUsageEvent(dispatch, estimatedDeterministicUsage()));
|
|
199
|
+
return {
|
|
200
|
+
summary: missingLaunchTarget ? "forge-bridge QA blocked." : "forge-bridge QA passed.",
|
|
201
|
+
artifact: qaArtifact,
|
|
202
|
+
artifacts: [qaArtifact]
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
async runCiRepair(context) {
|
|
206
|
+
return await this.runPullRequestRepair(context, "ci_repair");
|
|
207
|
+
}
|
|
208
|
+
async runConflictResolve(context) {
|
|
209
|
+
return await this.runPullRequestRepair(context, "conflict_resolve");
|
|
210
|
+
}
|
|
211
|
+
async runPullRequestRepair(context, route) {
|
|
212
|
+
const { dispatch, worktree } = context;
|
|
213
|
+
const repairChecks = route === "ci_repair" || isPullRequestCheckRepairDispatch(dispatch);
|
|
214
|
+
const addressComments = isPullRequestCommentsDispatch(dispatch);
|
|
215
|
+
await this.git.run(worktree.worktreePath, ["commit", "--allow-empty", "-m", `forge bridge ${repairChecks ? "ci repair" : addressComments ? "pr comments" : "conflict resolve"} for ${dispatch.workItem.id.slice(0, 8)}`]);
|
|
216
|
+
await context.emit(buildProviderUsageEvent(dispatch, estimatedDeterministicUsage()));
|
|
217
|
+
await context.emit({
|
|
218
|
+
role: "assistant",
|
|
219
|
+
kind: "message",
|
|
220
|
+
content: repairChecks
|
|
221
|
+
? `Deterministic CI repair completed for ${worktree.branchName}.`
|
|
222
|
+
: addressComments
|
|
223
|
+
? `Deterministic PR comment addressing completed for ${worktree.branchName}.`
|
|
224
|
+
: `Deterministic conflict resolve completed for ${worktree.branchName}.`,
|
|
225
|
+
eventType: repairChecks ? "pr_checks_complete" : addressComments ? "pr_comments_complete" : "conflict_resolve_complete"
|
|
226
|
+
});
|
|
227
|
+
return {
|
|
228
|
+
summary: repairChecks
|
|
229
|
+
? `forge-bridge repaired PR checks for ${worktree.branchName}.`
|
|
230
|
+
: addressComments
|
|
231
|
+
? `forge-bridge addressed PR comments for ${worktree.branchName}.`
|
|
232
|
+
: `forge-bridge resolved conflicts for ${worktree.branchName}.`
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
function deterministicDraftPayload(dispatch) {
|
|
237
|
+
const kind = dispatch.draft?.kind ?? "work_item";
|
|
238
|
+
if (kind === "work_order_decomposition") {
|
|
239
|
+
return {
|
|
240
|
+
summary: "Fake decompose proposed a Feature DAG for the Work Order.",
|
|
241
|
+
blockedReason: "",
|
|
242
|
+
candidates: [
|
|
243
|
+
{
|
|
244
|
+
origin: "agent",
|
|
245
|
+
key: "intake-submission",
|
|
246
|
+
title: "Customer intake submission",
|
|
247
|
+
summary: "Collect, validate, de-duplicate, and confirm incoming customer requests.",
|
|
248
|
+
planningPacket: "# Customer intake submission\n\nPlan Stories for request form validation, attachment support, duplicate prevention, and submission confirmation.",
|
|
249
|
+
sortOrder: 10,
|
|
250
|
+
dependsOn: []
|
|
251
|
+
},
|
|
252
|
+
{
|
|
253
|
+
origin: "agent",
|
|
254
|
+
key: "review-queue",
|
|
255
|
+
title: "Reviewer triage queue",
|
|
256
|
+
summary: "Provide reviewer triage, assignment, notes, status transitions, and audit trail.",
|
|
257
|
+
planningPacket: "# Reviewer triage queue\n\nPlan Stories for queue filtering, assignment, reviewer notes, status changes, and immutable audit trail.",
|
|
258
|
+
sortOrder: 20,
|
|
259
|
+
dependsOn: ["intake-submission"]
|
|
260
|
+
}
|
|
261
|
+
]
|
|
262
|
+
};
|
|
263
|
+
}
|
|
264
|
+
if (kind === "feature_decomposition") {
|
|
265
|
+
return {
|
|
266
|
+
summary: "Fake decompose proposed a Story DAG for the Feature.",
|
|
267
|
+
blockedReason: "",
|
|
268
|
+
candidates: [
|
|
269
|
+
{
|
|
270
|
+
origin: "agent",
|
|
271
|
+
key: "foundation",
|
|
272
|
+
title: "Build the feature foundation",
|
|
273
|
+
description: "Create the persistence, API, and UI shell needed by the feature.",
|
|
274
|
+
acceptanceCriteria: "Scenario: Foundation exists\nGiven the Feature is selected\nWhen the foundation is delivered\nThen downstream Stories can build on stable contracts",
|
|
275
|
+
points: 3,
|
|
276
|
+
sortOrder: 10,
|
|
277
|
+
dependsOn: []
|
|
278
|
+
},
|
|
279
|
+
{
|
|
280
|
+
origin: "agent",
|
|
281
|
+
key: "workflow-behavior",
|
|
282
|
+
title: "Implement the primary workflow behavior",
|
|
283
|
+
description: "Implement the main user-visible behavior for the feature after the foundation is ready.",
|
|
284
|
+
acceptanceCriteria: "Scenario: Workflow behavior works\nGiven the foundation Story is complete\nWhen the user performs the primary workflow\nThen the expected state change and feedback are visible",
|
|
285
|
+
points: 5,
|
|
286
|
+
sortOrder: 20,
|
|
287
|
+
dependsOn: ["foundation"]
|
|
288
|
+
},
|
|
289
|
+
{
|
|
290
|
+
origin: "agent",
|
|
291
|
+
key: "audit-and-tests",
|
|
292
|
+
title: "Add audit coverage and regression tests",
|
|
293
|
+
description: "Add audit/evidence behavior and automated regression coverage for the feature.",
|
|
294
|
+
acceptanceCriteria: "Scenario: Feature is verifiable\nGiven the workflow behavior is complete\nWhen validation runs\nThen audit evidence and tests cover the expected outcomes",
|
|
295
|
+
points: 3,
|
|
296
|
+
sortOrder: 30,
|
|
297
|
+
dependsOn: ["workflow-behavior"]
|
|
298
|
+
}
|
|
299
|
+
]
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
if (kind === "work_order") {
|
|
303
|
+
return {
|
|
304
|
+
title: "Deterministic runner Work Order draft",
|
|
305
|
+
summary: "Forge Runner generated this deterministic Work Order draft.",
|
|
306
|
+
objective: "Validate runner-backed Draft generation.",
|
|
307
|
+
planningPacket: "Runner-backed draft generation returned a Work Order JSON payload.",
|
|
308
|
+
decompositionContext: "Use this deterministic fixture only in bridge tests.",
|
|
309
|
+
assumptions: ["Forge Runner dispatch is available."],
|
|
310
|
+
warnings: []
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
if (kind === "story") {
|
|
314
|
+
return {
|
|
315
|
+
title: "Deterministic runner Story draft",
|
|
316
|
+
description: "Forge Runner generated this deterministic Story draft.",
|
|
317
|
+
acceptanceCriteria: "Given runner-backed Draft generation, when Forge requests a Story draft, then JSON is returned.",
|
|
318
|
+
points: 3
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
return {
|
|
322
|
+
title: "Deterministic runner Work Item draft",
|
|
323
|
+
description: "Forge Runner generated this deterministic Work Item draft.",
|
|
324
|
+
acceptanceCriteria: "Given runner-backed Draft generation, when Forge requests a draft, then JSON is returned.",
|
|
325
|
+
reproSteps: "Run Draft with a Forge Runner provider selected.",
|
|
326
|
+
systemInfo: "forge-bridge deterministic provider",
|
|
327
|
+
points: 1
|
|
328
|
+
};
|
|
329
|
+
}
|