@loucompanion/forge-bridge 0.1.1-dev.af36c7fa102b → 0.1.1-dev.ec3c3e74a648
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/cli/cli.service.js +7 -0
- package/dist/session/phase-completion.d.ts +2 -0
- package/dist/session/phase-completion.js +78 -8
- package/dist/session/provider-cli/provider-cli.service.d.ts +1 -0
- package/dist/session/provider-cli/provider-cli.service.js +334 -71
- package/dist/session/provider-cli/provider-cli.types.d.ts +1 -0
- package/dist/session/session.service.js +3 -1
- package/dist/session/session.types.d.ts +3 -0
- package/dist/session/session.types.js +11 -0
- package/dist/shared/cursor-agent/cursor-agent.launch.d.ts +9 -0
- package/dist/shared/cursor-agent/cursor-agent.launch.js +66 -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
package/dist/cli/cli.service.js
CHANGED
|
@@ -4,6 +4,7 @@ import os from "node:os";
|
|
|
4
4
|
import path from "node:path";
|
|
5
5
|
import { createInterface } from "node:readline/promises";
|
|
6
6
|
import { promisify } from "node:util";
|
|
7
|
+
import { resolveCursorAgentDirectLaunch } from "../shared/cursor-agent/cursor-agent.launch.js";
|
|
7
8
|
import { redactSecrets, redactText } from "../shared/redaction/redaction.js";
|
|
8
9
|
import { defaultBridgeServiceConfig, ForgeBridgeService } from "../bridge/bridge.service.js";
|
|
9
10
|
import { addRepoMapping, applyLocalConfigDefaults, providerStatusesFor, readLocalConfigSync, removeRepoMapping, resolveLocalConfigPath, writeLocalConfig } from "./local-config.js";
|
|
@@ -633,6 +634,12 @@ function cliProbeCommandsFor(provider) {
|
|
|
633
634
|
];
|
|
634
635
|
}
|
|
635
636
|
async function execVersionCommand(probe, timeout = 2000) {
|
|
637
|
+
const directLaunch = process.platform === "win32"
|
|
638
|
+
? await resolveCursorAgentDirectLaunch(probe.command, probe.args)
|
|
639
|
+
: null;
|
|
640
|
+
if (directLaunch) {
|
|
641
|
+
return await execFileAsync(directLaunch.command, [...directLaunch.args], { timeout });
|
|
642
|
+
}
|
|
636
643
|
try {
|
|
637
644
|
return await execFileAsync(probe.command, probe.args, { timeout });
|
|
638
645
|
}
|
|
@@ -19,6 +19,7 @@ export interface CompletionToolServerConfig {
|
|
|
19
19
|
readonly workItemId: string;
|
|
20
20
|
readonly phase: CompletionPhase;
|
|
21
21
|
readonly producedBy?: string;
|
|
22
|
+
readonly redactionSecrets?: readonly string[];
|
|
22
23
|
}
|
|
23
24
|
export interface CompletionArtifactReference {
|
|
24
25
|
readonly phase: CompletionPhase;
|
|
@@ -52,3 +53,4 @@ export declare function parseCompletionMcpArgs(argv: readonly string[]): {
|
|
|
52
53
|
readonly ok: false;
|
|
53
54
|
readonly error: string;
|
|
54
55
|
};
|
|
56
|
+
export declare function completionRedactionEnv(secrets: readonly string[]): Readonly<Record<string, string>>;
|
|
@@ -2,7 +2,9 @@ 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";
|
|
6
8
|
const phaseFiles = {
|
|
7
9
|
implementation: {
|
|
8
10
|
jsonFileName: "implementation.completeness.json",
|
|
@@ -122,12 +124,13 @@ export async function writeCompletionArtifact(config, toolName, input) {
|
|
|
122
124
|
const payload = asRecord(input, "completion payload");
|
|
123
125
|
validatePayloadShape(phase, payload);
|
|
124
126
|
validateOptionalWorkItemId(payload, config.workItemId);
|
|
125
|
-
const
|
|
126
|
-
|
|
127
|
+
const redactedPayload = redactSecrets(payload, config.redactionSecrets ?? []);
|
|
128
|
+
const producedBy = optionalString(redactedPayload, "produced_by")
|
|
129
|
+
?? optionalString(redactedPayload, "producedBy")
|
|
127
130
|
?? config.producedBy
|
|
128
131
|
?? "forge-completion-tool";
|
|
129
|
-
const status = requiredString(
|
|
130
|
-
const artifactWithoutHash = normalizeArtifactPayload(
|
|
132
|
+
const status = requiredString(redactedPayload, "status").trim().toLowerCase();
|
|
133
|
+
const artifactWithoutHash = normalizeArtifactPayload(redactedPayload, {
|
|
131
134
|
phase,
|
|
132
135
|
workItemId: config.workItemId,
|
|
133
136
|
producedBy,
|
|
@@ -139,7 +142,7 @@ export async function writeCompletionArtifact(config, toolName, input) {
|
|
|
139
142
|
content_hash: contentHash
|
|
140
143
|
};
|
|
141
144
|
const artifactJson = `${JSON.stringify(artifact, null, 2)}\n`;
|
|
142
|
-
const report = reportMarkdown(
|
|
145
|
+
const report = reportMarkdown(redactedPayload, phase, status, producedBy, contentHash);
|
|
143
146
|
const artifactPath = completionArtifactJsonPath(config.worktreePath, config.workItemId, phase);
|
|
144
147
|
const reportPath = completionArtifactReportPath(config.worktreePath, config.workItemId, phase);
|
|
145
148
|
await fs.mkdir(path.dirname(artifactPath), { recursive: true });
|
|
@@ -171,7 +174,8 @@ export async function readCompletionArtifact(worktreePath, workItemId, phase) {
|
|
|
171
174
|
verdictJson,
|
|
172
175
|
report,
|
|
173
176
|
producedBy: optionalString(parsed, "produced_by") ?? optionalString(parsed, "producedBy") ?? "forge-completion-tool",
|
|
174
|
-
status: optionalString(parsed, "status") ?? "unknown"
|
|
177
|
+
status: optionalString(parsed, "status") ?? "unknown",
|
|
178
|
+
phase
|
|
175
179
|
};
|
|
176
180
|
}
|
|
177
181
|
export async function runCompletionMcpServer(config) {
|
|
@@ -231,10 +235,17 @@ export function parseCompletionMcpArgs(argv) {
|
|
|
231
235
|
worktreePath: path.resolve(worktreePath),
|
|
232
236
|
workItemId,
|
|
233
237
|
phase,
|
|
234
|
-
producedBy
|
|
238
|
+
producedBy,
|
|
239
|
+
redactionSecrets: parseCompletionRedactionSecrets(process.env[completionRedactionSecretsEnv])
|
|
235
240
|
}
|
|
236
241
|
};
|
|
237
242
|
}
|
|
243
|
+
export function completionRedactionEnv(secrets) {
|
|
244
|
+
const filtered = secrets.filter((secret) => secret.length > 0);
|
|
245
|
+
return filtered.length > 0
|
|
246
|
+
? { [completionRedactionSecretsEnv]: JSON.stringify(filtered) }
|
|
247
|
+
: {};
|
|
248
|
+
}
|
|
238
249
|
async function prepareCursorCompletionToolRuntime(worktreePath, runtime) {
|
|
239
250
|
const cursorDir = path.join(worktreePath, ".cursor");
|
|
240
251
|
const configPath = path.join(cursorDir, "mcp.json");
|
|
@@ -421,10 +432,12 @@ function validatePayloadShape(phase, payload) {
|
|
|
421
432
|
if (phase === "implementation") {
|
|
422
433
|
requireArray(payload, "plan");
|
|
423
434
|
requireArray(payload, "changed_files");
|
|
424
|
-
requireArray(payload, "
|
|
435
|
+
requireArray(payload, "tests_added_or_updated");
|
|
436
|
+
requireArray(payload, "docs_added_or_updated");
|
|
425
437
|
requireArray(payload, "acceptance_criteria");
|
|
426
438
|
requireArray(payload, "known_gaps");
|
|
427
439
|
requireField(payload, "next_phase_context");
|
|
440
|
+
requireImplementationEvidence(payload);
|
|
428
441
|
requireSatisfiedAcceptanceCriteria(payload);
|
|
429
442
|
return;
|
|
430
443
|
}
|
|
@@ -450,6 +463,7 @@ function validatePayloadShape(phase, payload) {
|
|
|
450
463
|
if (payload.ready_for_dev_test !== true) {
|
|
451
464
|
throw new Error("validation pass requires ready_for_dev_test: true.");
|
|
452
465
|
}
|
|
466
|
+
requirePassingCommands(payload);
|
|
453
467
|
requireSatisfiedAcceptanceCriteria(payload);
|
|
454
468
|
}
|
|
455
469
|
function validateOptionalWorkItemId(payload, expectedWorkItemId) {
|
|
@@ -500,6 +514,21 @@ function parseStrictCompletionPhase(value) {
|
|
|
500
514
|
}
|
|
501
515
|
return null;
|
|
502
516
|
}
|
|
517
|
+
function parseCompletionRedactionSecrets(raw) {
|
|
518
|
+
if (!raw?.trim()) {
|
|
519
|
+
return [];
|
|
520
|
+
}
|
|
521
|
+
try {
|
|
522
|
+
const parsed = JSON.parse(raw);
|
|
523
|
+
if (!Array.isArray(parsed)) {
|
|
524
|
+
return [];
|
|
525
|
+
}
|
|
526
|
+
return parsed.filter((value) => typeof value === "string" && value.length > 0);
|
|
527
|
+
}
|
|
528
|
+
catch {
|
|
529
|
+
return [];
|
|
530
|
+
}
|
|
531
|
+
}
|
|
503
532
|
function phaseFromToolName(toolName) {
|
|
504
533
|
const normalized = toolName.trim().toLowerCase();
|
|
505
534
|
if (normalized === "forge.complete_implementation" || normalized === "complete_implementation") {
|
|
@@ -534,6 +563,47 @@ function requireNonPassEvidence(payload) {
|
|
|
534
563
|
throw new Error("non-pass completion requires explicit blockers, findings, failures, or known gaps.");
|
|
535
564
|
}
|
|
536
565
|
}
|
|
566
|
+
function requireImplementationEvidence(payload) {
|
|
567
|
+
const knownGaps = payload.known_gaps;
|
|
568
|
+
const hasKnownGap = Array.isArray(knownGaps) && knownGaps.length > 0;
|
|
569
|
+
for (const key of ["changed_files", "tests_added_or_updated", "docs_added_or_updated"]) {
|
|
570
|
+
const value = payload[key];
|
|
571
|
+
if (Array.isArray(value) && value.length === 0 && !hasKnownGap) {
|
|
572
|
+
throw new Error(`${key} is empty; known_gaps must explain why.`);
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
}
|
|
576
|
+
function requirePassingCommands(payload) {
|
|
577
|
+
const commands = payload.commands;
|
|
578
|
+
if (!Array.isArray(commands)) {
|
|
579
|
+
return;
|
|
580
|
+
}
|
|
581
|
+
if (commands.length === 0) {
|
|
582
|
+
throw new Error("commands must include at least one deterministic validation command.");
|
|
583
|
+
}
|
|
584
|
+
for (const [index, command] of commands.entries()) {
|
|
585
|
+
if (!command || typeof command !== "object" || Array.isArray(command)) {
|
|
586
|
+
throw new Error(`commands[${index}] must be an object.`);
|
|
587
|
+
}
|
|
588
|
+
const record = command;
|
|
589
|
+
if (typeof record.command !== "string" || record.command.trim().length === 0) {
|
|
590
|
+
throw new Error(`commands[${index}].command must be a non-empty string.`);
|
|
591
|
+
}
|
|
592
|
+
if (typeof record.summary !== "string" || record.summary.trim().length === 0) {
|
|
593
|
+
throw new Error(`commands[${index}].summary must be a non-empty string.`);
|
|
594
|
+
}
|
|
595
|
+
if (typeof record.exit_code !== "number" || !Number.isFinite(record.exit_code)) {
|
|
596
|
+
throw new Error(`commands[${index}].exit_code must be a finite number.`);
|
|
597
|
+
}
|
|
598
|
+
if (record.exit_code !== 0) {
|
|
599
|
+
throw new Error(`commands[${index}].exit_code must be 0 when status is pass.`);
|
|
600
|
+
}
|
|
601
|
+
const status = typeof record.status === "string" ? record.status.trim().toLowerCase() : "";
|
|
602
|
+
if (status !== "pass") {
|
|
603
|
+
throw new Error(`commands[${index}].status must be pass.`);
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
}
|
|
537
607
|
function requireArray(payload, key) {
|
|
538
608
|
if (!Array.isArray(payload[key])) {
|
|
539
609
|
throw new Error(`${key} must be an array.`);
|
|
@@ -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;
|
|
@@ -5,8 +5,9 @@ import { execFile, spawn } from "node:child_process";
|
|
|
5
5
|
import readline from "node:readline";
|
|
6
6
|
import { promisify } from "node:util";
|
|
7
7
|
import { GitClient } from "../../shared/git/git.js";
|
|
8
|
+
import { resolveCursorAgentDirectLaunch } from "../../shared/cursor-agent/cursor-agent.launch.js";
|
|
8
9
|
import { buildSpecBody, selectLaneCommand } from "../../worktree/artifact-injector.js";
|
|
9
|
-
import { codexMcpConfigArgs, completionPhaseFromDispatch, completionPromptForPhase, prepareCompletionToolRuntime, readCompletionArtifact } from "../phase-completion.js";
|
|
10
|
+
import { codexMcpConfigArgs, completionPhaseFromDispatch, completionPromptForPhase, completionRedactionEnv, expectedCompletionToolName, prepareCompletionToolRuntime, readCompletionArtifact, writeCompletionArtifact } from "../phase-completion.js";
|
|
10
11
|
const fakeWorkerModel = "server-local-fake-worker";
|
|
11
12
|
const execFileAsync = promisify(execFile);
|
|
12
13
|
export class RoutingProviderCliService {
|
|
@@ -70,16 +71,17 @@ export class ProcessBackedProviderCliService {
|
|
|
70
71
|
content: `Forge completion tools configured for ${phase}; provider must call the MCP completion tool before exit.`,
|
|
71
72
|
eventType: "completion_tools_configured"
|
|
72
73
|
});
|
|
73
|
-
const
|
|
74
|
+
const outputState = createProviderOutputState(dispatch.execution.provider);
|
|
74
75
|
let result;
|
|
75
76
|
try {
|
|
76
77
|
result = await this.processRunner.run({
|
|
77
78
|
command: command.command,
|
|
78
79
|
args: command.args,
|
|
79
|
-
cwd: worktree.worktreePath
|
|
80
|
+
cwd: worktree.worktreePath,
|
|
81
|
+
env: providerRedactionEnv(dispatch)
|
|
80
82
|
}, {
|
|
81
83
|
onStdoutLine: async (line) => {
|
|
82
|
-
const event =
|
|
84
|
+
const event = outputState.consume(line);
|
|
83
85
|
if (event) {
|
|
84
86
|
await context.emit(event);
|
|
85
87
|
}
|
|
@@ -96,7 +98,7 @@ export class ProcessBackedProviderCliService {
|
|
|
96
98
|
await context.emit({
|
|
97
99
|
role: "runner",
|
|
98
100
|
kind: "checkpoint",
|
|
99
|
-
content: `Provider CLI completed: ${command.command} exit ${result.exitCode}${
|
|
101
|
+
content: `Provider CLI completed: ${command.command} exit ${result.exitCode}${outputState.sessionLabel ? ` ${outputState.sessionLabel}` : ""}.`,
|
|
100
102
|
eventType: "provider_cli_completed"
|
|
101
103
|
});
|
|
102
104
|
const committed = await commitWorktreeChanges(this.git, worktree.worktreePath, dispatch.workItem.id);
|
|
@@ -104,12 +106,13 @@ export class ProcessBackedProviderCliService {
|
|
|
104
106
|
if (!completionArtifact) {
|
|
105
107
|
throw new Error(`Provider CLI completed but did not call the Forge ${phase} completion tool.`);
|
|
106
108
|
}
|
|
107
|
-
|
|
109
|
+
await emitCompletionArtifactRecorded(context.emit, worktree.worktreePath, completionArtifact);
|
|
110
|
+
if (outputState.finalMessage) {
|
|
108
111
|
await context.emit({
|
|
109
112
|
role: "assistant",
|
|
110
113
|
kind: "message",
|
|
111
|
-
content:
|
|
112
|
-
eventType:
|
|
114
|
+
content: outputState.finalMessage,
|
|
115
|
+
eventType: phaseCompleteEventType(phase)
|
|
113
116
|
});
|
|
114
117
|
}
|
|
115
118
|
else {
|
|
@@ -117,7 +120,7 @@ export class ProcessBackedProviderCliService {
|
|
|
117
120
|
role: "assistant",
|
|
118
121
|
kind: "message",
|
|
119
122
|
content: `${dispatch.execution.provider} CLI completed for ${worktree.branchName}.`,
|
|
120
|
-
eventType:
|
|
123
|
+
eventType: phaseCompleteEventType(phase)
|
|
121
124
|
});
|
|
122
125
|
}
|
|
123
126
|
return {
|
|
@@ -142,14 +145,14 @@ export class ProcessBackedProviderCliService {
|
|
|
142
145
|
content: `Starting provider CLI QA: ${formatProviderCliCommand(command)}`,
|
|
143
146
|
eventType: "provider_cli_started"
|
|
144
147
|
});
|
|
145
|
-
const
|
|
148
|
+
const outputState = createProviderOutputState(dispatch.execution.provider);
|
|
146
149
|
const result = await this.processRunner.run({
|
|
147
150
|
command: command.command,
|
|
148
151
|
args: command.args,
|
|
149
152
|
cwd: process.cwd()
|
|
150
153
|
}, {
|
|
151
154
|
onStdoutLine: async (line) => {
|
|
152
|
-
const event =
|
|
155
|
+
const event = outputState.consume(line);
|
|
153
156
|
if (event) {
|
|
154
157
|
await emit(event);
|
|
155
158
|
}
|
|
@@ -162,7 +165,7 @@ export class ProcessBackedProviderCliService {
|
|
|
162
165
|
await emit({
|
|
163
166
|
role: "runner",
|
|
164
167
|
kind: "checkpoint",
|
|
165
|
-
content: `Provider CLI QA completed: ${command.command} exit ${result.exitCode}${
|
|
168
|
+
content: `Provider CLI QA completed: ${command.command} exit ${result.exitCode}${outputState.sessionLabel ? ` ${outputState.sessionLabel}` : ""}.`,
|
|
166
169
|
eventType: "provider_cli_completed"
|
|
167
170
|
});
|
|
168
171
|
return {
|
|
@@ -181,14 +184,14 @@ export class ProcessBackedProviderCliService {
|
|
|
181
184
|
content: `Starting provider CLI conflict resolve: ${formatProviderCliCommand(command)} in ${worktree.worktreePath}`,
|
|
182
185
|
eventType: "provider_cli_started"
|
|
183
186
|
});
|
|
184
|
-
const
|
|
187
|
+
const outputState = createProviderOutputState(dispatch.execution.provider);
|
|
185
188
|
const result = await this.processRunner.run({
|
|
186
189
|
command: command.command,
|
|
187
190
|
args: command.args,
|
|
188
191
|
cwd: worktree.worktreePath
|
|
189
192
|
}, {
|
|
190
193
|
onStdoutLine: async (line) => {
|
|
191
|
-
const event =
|
|
194
|
+
const event = outputState.consume(line);
|
|
192
195
|
if (event) {
|
|
193
196
|
await context.emit(event);
|
|
194
197
|
}
|
|
@@ -203,7 +206,7 @@ export class ProcessBackedProviderCliService {
|
|
|
203
206
|
await context.emit({
|
|
204
207
|
role: "assistant",
|
|
205
208
|
kind: "message",
|
|
206
|
-
content:
|
|
209
|
+
content: outputState.finalMessage || `${dispatch.execution.provider} CLI resolved merge conflicts for ${worktree.branchName}.`,
|
|
207
210
|
eventType: "conflict_resolve_complete"
|
|
208
211
|
});
|
|
209
212
|
return {
|
|
@@ -249,27 +252,26 @@ export class DeterministicProviderCliService {
|
|
|
249
252
|
content: `Created bridge changeset at ${changesetPath}`,
|
|
250
253
|
eventType: "changeset"
|
|
251
254
|
});
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
await
|
|
264
|
-
await fs.writeFile(path.join(artifactRoot, "implementation.report.md"), report, "utf8");
|
|
255
|
+
await writeCompletionArtifact({
|
|
256
|
+
worktreePath: worktree.worktreePath,
|
|
257
|
+
workItemId: dispatch.workItem.id,
|
|
258
|
+
phase,
|
|
259
|
+
producedBy: "forge-bridge",
|
|
260
|
+
redactionSecrets: exactRedactionSecrets(dispatch)
|
|
261
|
+
}, expectedCompletionToolName(phase), deterministicCompletionPayload(phase, dispatch, changesetBody));
|
|
262
|
+
const completionArtifact = await readCompletionArtifact(worktree.worktreePath, dispatch.workItem.id, phase);
|
|
263
|
+
if (!completionArtifact) {
|
|
264
|
+
throw new Error(`Deterministic provider did not produce the Forge ${phase} completion artifact.`);
|
|
265
|
+
}
|
|
266
|
+
await emitCompletionArtifactRecorded(context.emit, worktree.worktreePath, completionArtifact);
|
|
265
267
|
await context.emit({
|
|
266
268
|
role: "assistant",
|
|
267
269
|
kind: "message",
|
|
268
|
-
content: "
|
|
269
|
-
eventType:
|
|
270
|
+
content: `${phase.replace(/_/g, " ")} complete. forge-bridge produced deterministic phase artifacts.`,
|
|
271
|
+
eventType: phaseCompleteEventType(phase)
|
|
270
272
|
});
|
|
271
273
|
return {
|
|
272
|
-
summary: `forge-bridge completed ${worktree.branchName}.`,
|
|
274
|
+
summary: `forge-bridge completed ${worktree.branchName} (${phase}).`,
|
|
273
275
|
spec: {
|
|
274
276
|
repoPath: worktree.worktreePath,
|
|
275
277
|
path: specPath,
|
|
@@ -277,14 +279,7 @@ export class DeterministicProviderCliService {
|
|
|
277
279
|
producedBy: "forge-lane-command",
|
|
278
280
|
status: "approved"
|
|
279
281
|
},
|
|
280
|
-
artifact:
|
|
281
|
-
repoPath: worktree.worktreePath,
|
|
282
|
-
path: artifactPath,
|
|
283
|
-
verdictJson,
|
|
284
|
-
report,
|
|
285
|
-
producedBy: "forge-bridge",
|
|
286
|
-
status: "pass"
|
|
287
|
-
}
|
|
282
|
+
artifact: completionArtifact
|
|
288
283
|
};
|
|
289
284
|
}
|
|
290
285
|
async runQa(dispatch, emit) {
|
|
@@ -318,12 +313,114 @@ export class DeterministicProviderCliService {
|
|
|
318
313
|
};
|
|
319
314
|
}
|
|
320
315
|
}
|
|
316
|
+
function phaseCompleteEventType(phase) {
|
|
317
|
+
return phase === "implementation" ? "implementation_complete" : `${phase}_complete`;
|
|
318
|
+
}
|
|
319
|
+
function deterministicCompletionPayload(phase, dispatch, changesetBody) {
|
|
320
|
+
const changesetHash = sha256(changesetBody);
|
|
321
|
+
const criterion = dispatch.workItem.acceptanceCriteria || "Deterministic bridge changeset exists";
|
|
322
|
+
if (phase === "implementation") {
|
|
323
|
+
return {
|
|
324
|
+
status: "pass",
|
|
325
|
+
summary: "forge-bridge produced a deterministic implementation changeset.",
|
|
326
|
+
plan: [{ step: "Create deterministic changeset", status: "done" }],
|
|
327
|
+
changed_files: [{ path: "FORGE_PHASE5_CHANGESET.md", summary: "Recorded deterministic bridge evidence." }],
|
|
328
|
+
tests_added_or_updated: [{ name: "forge_bridge_run_once", evidence: "The bridge committed FORGE_PHASE5_CHANGESET.md." }],
|
|
329
|
+
docs_added_or_updated: [{ path: "FORGE_PHASE5_CHANGESET.md", summary: "Deterministic evidence document created for the run." }],
|
|
330
|
+
acceptance_criteria: [
|
|
331
|
+
{
|
|
332
|
+
criterion,
|
|
333
|
+
evidence: `FORGE_PHASE5_CHANGESET.md was committed with content hash ${changesetHash}.`,
|
|
334
|
+
status: "satisfied"
|
|
335
|
+
}
|
|
336
|
+
],
|
|
337
|
+
known_gaps: [],
|
|
338
|
+
next_phase_context: "Review FORGE_PHASE5_CHANGESET.md and the bridge commit before validation."
|
|
339
|
+
};
|
|
340
|
+
}
|
|
341
|
+
if (phase === "self_review") {
|
|
342
|
+
return {
|
|
343
|
+
status: "pass",
|
|
344
|
+
summary: "forge-bridge fake worker completed deterministic self-review.",
|
|
345
|
+
reviewed_diff: true,
|
|
346
|
+
findings: [],
|
|
347
|
+
security_review: {
|
|
348
|
+
status: "pass",
|
|
349
|
+
notes: "No secret handling, auth, or destructive behavior changes were introduced by the deterministic changeset."
|
|
350
|
+
},
|
|
351
|
+
edge_case_review: {
|
|
352
|
+
status: "pass",
|
|
353
|
+
notes: "The deterministic fake-worker changeset is intentionally narrow and repeatable."
|
|
354
|
+
},
|
|
355
|
+
design_review: {
|
|
356
|
+
status: "not_applicable",
|
|
357
|
+
screenshots: [],
|
|
358
|
+
notes: "No UI changes."
|
|
359
|
+
},
|
|
360
|
+
deterministic_comparison: {
|
|
361
|
+
status: "pass",
|
|
362
|
+
command_or_tool: "git diff --stat HEAD~1..HEAD",
|
|
363
|
+
notes: `Reviewed deterministic changeset with content hash ${changesetHash}.`
|
|
364
|
+
},
|
|
365
|
+
fixes_applied: [],
|
|
366
|
+
remaining_risks: []
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
return {
|
|
370
|
+
status: "pass",
|
|
371
|
+
summary: "forge-bridge fake worker completed deterministic validation.",
|
|
372
|
+
commands: [
|
|
373
|
+
{
|
|
374
|
+
command: "git status --short && git diff --check",
|
|
375
|
+
exit_code: 0,
|
|
376
|
+
status: "pass",
|
|
377
|
+
summary: "Deterministic validation command completed for the fake-worker changeset."
|
|
378
|
+
}
|
|
379
|
+
],
|
|
380
|
+
acceptance_criteria: [
|
|
381
|
+
{
|
|
382
|
+
criterion,
|
|
383
|
+
evidence: `FORGE_PHASE5_CHANGESET.md exists with content hash ${changesetHash}.`,
|
|
384
|
+
status: "satisfied"
|
|
385
|
+
}
|
|
386
|
+
],
|
|
387
|
+
build_status: "pass",
|
|
388
|
+
test_status: "pass",
|
|
389
|
+
lint_status: "not_applicable",
|
|
390
|
+
design_status: "not_applicable",
|
|
391
|
+
drift_checks: [
|
|
392
|
+
{
|
|
393
|
+
command_or_tool: "git diff --check",
|
|
394
|
+
status: "pass",
|
|
395
|
+
summary: "No deterministic drift or whitespace errors were reported for the fake-worker changeset."
|
|
396
|
+
}
|
|
397
|
+
],
|
|
398
|
+
ready_for_dev_test: true
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
async function emitCompletionArtifactRecorded(emit, worktreePath, artifact) {
|
|
402
|
+
const relativePath = path.relative(worktreePath, artifact.path) || path.basename(artifact.path);
|
|
403
|
+
await emit({
|
|
404
|
+
role: "tool",
|
|
405
|
+
kind: "tool_result",
|
|
406
|
+
content: `Forge completion tool recorded ${artifact.phase ?? "unknown"} artifact ${relativePath} (${artifact.status}).`,
|
|
407
|
+
eventType: "completion_tool_artifact_recorded"
|
|
408
|
+
});
|
|
409
|
+
}
|
|
410
|
+
function providerRedactionEnv(dispatch) {
|
|
411
|
+
const env = completionRedactionEnv(exactRedactionSecrets(dispatch));
|
|
412
|
+
return Object.keys(env).length > 0 ? env : undefined;
|
|
413
|
+
}
|
|
414
|
+
function exactRedactionSecrets(dispatch) {
|
|
415
|
+
return dispatch.apiKeySecretGrant?.secret ? [dispatch.apiKeySecretGrant.secret] : [];
|
|
416
|
+
}
|
|
321
417
|
export class StreamingProviderProcessRunner {
|
|
322
418
|
async run(request, handlers = {}, signal) {
|
|
323
419
|
const launch = await resolveProviderProcessLaunch(request.command, request.args);
|
|
324
420
|
return await new Promise((resolve, reject) => {
|
|
325
421
|
const child = spawn(launch.command, launch.args, {
|
|
326
422
|
cwd: request.cwd,
|
|
423
|
+
env: request.env ? { ...process.env, ...request.env } : undefined,
|
|
327
424
|
stdio: ["ignore", "pipe", "pipe"],
|
|
328
425
|
windowsHide: true
|
|
329
426
|
});
|
|
@@ -392,6 +489,10 @@ async function resolveProviderProcessLaunch(command, args) {
|
|
|
392
489
|
if (process.platform !== "win32") {
|
|
393
490
|
return { command, args };
|
|
394
491
|
}
|
|
492
|
+
const cursorDirect = await resolveCursorAgentDirectLaunch(command, args);
|
|
493
|
+
if (cursorDirect) {
|
|
494
|
+
return cursorDirect;
|
|
495
|
+
}
|
|
395
496
|
const resolved = await resolveWindowsCommand(command);
|
|
396
497
|
return resolved ? buildWindowsLaunchCommand(resolved, args) : { command, args };
|
|
397
498
|
}
|
|
@@ -399,10 +500,6 @@ async function resolveWindowsCommand(command) {
|
|
|
399
500
|
if (path.isAbsolute(command)) {
|
|
400
501
|
return command;
|
|
401
502
|
}
|
|
402
|
-
const cursorScript = await resolveCursorAgentPowerShellScript(command);
|
|
403
|
-
if (cursorScript) {
|
|
404
|
-
return cursorScript;
|
|
405
|
-
}
|
|
406
503
|
try {
|
|
407
504
|
const result = await execFileAsync("where.exe", [command], { timeout: 2000 });
|
|
408
505
|
return chooseWindowsCommandCandidate(result.stdout.split(/\r?\n/));
|
|
@@ -411,18 +508,6 @@ async function resolveWindowsCommand(command) {
|
|
|
411
508
|
return null;
|
|
412
509
|
}
|
|
413
510
|
}
|
|
414
|
-
async function resolveCursorAgentPowerShellScript(command) {
|
|
415
|
-
const normalized = command.trim().toLowerCase();
|
|
416
|
-
if (normalized !== "cursor-agent" && normalized !== "agent") {
|
|
417
|
-
return null;
|
|
418
|
-
}
|
|
419
|
-
const localAppData = process.env.LOCALAPPDATA;
|
|
420
|
-
if (!localAppData) {
|
|
421
|
-
return null;
|
|
422
|
-
}
|
|
423
|
-
const script = path.join(localAppData, "cursor-agent", "cursor-agent.ps1");
|
|
424
|
-
return await fileExists(script) ? script : null;
|
|
425
|
-
}
|
|
426
511
|
export function chooseWindowsCommandCandidate(candidates) {
|
|
427
512
|
const normalized = candidates
|
|
428
513
|
.map((candidate) => candidate.trim())
|
|
@@ -519,10 +604,12 @@ export function buildProviderCliCommand(dispatch, run) {
|
|
|
519
604
|
return { command: "codex", args };
|
|
520
605
|
}
|
|
521
606
|
if (normalizedProvider === "cursor") {
|
|
522
|
-
const args = ["--model", model];
|
|
607
|
+
const args = ["--print", "--output-format", "stream-json", "--model", model];
|
|
608
|
+
if (run?.worktreePath) {
|
|
609
|
+
args.push("--workspace", run.worktreePath);
|
|
610
|
+
}
|
|
523
611
|
if (run?.prompt) {
|
|
524
|
-
args.push("--yolo");
|
|
525
|
-
args.push(run.prompt);
|
|
612
|
+
args.push("--yolo", "--approve-mcps", "--trust", run.prompt);
|
|
526
613
|
}
|
|
527
614
|
return { command: "cursor-agent", args };
|
|
528
615
|
}
|
|
@@ -611,27 +698,203 @@ function clip(value, maxLength) {
|
|
|
611
698
|
}
|
|
612
699
|
return `${value.slice(0, maxLength)}...`;
|
|
613
700
|
}
|
|
614
|
-
|
|
615
|
-
|
|
701
|
+
function createProviderOutputState(provider) {
|
|
702
|
+
return provider.trim().toLowerCase() === "cursor"
|
|
703
|
+
? new CursorStreamJsonState()
|
|
704
|
+
: new CodexJsonState();
|
|
705
|
+
}
|
|
706
|
+
class CursorStreamJsonState {
|
|
707
|
+
sessionId = null;
|
|
616
708
|
finalMessage = null;
|
|
617
|
-
|
|
709
|
+
emittedSession = false;
|
|
710
|
+
get sessionLabel() {
|
|
711
|
+
return this.sessionId ? `session ${this.sessionId}` : null;
|
|
712
|
+
}
|
|
618
713
|
consume(line) {
|
|
619
|
-
const
|
|
620
|
-
if (!
|
|
714
|
+
const event = parseJsonLine(line);
|
|
715
|
+
if (!event) {
|
|
621
716
|
return null;
|
|
622
717
|
}
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
718
|
+
const type = stringField(event, "type");
|
|
719
|
+
if (type === "system" && stringField(event, "subtype") === "init") {
|
|
720
|
+
this.sessionId = stringField(event, "session_id");
|
|
721
|
+
if (this.sessionId && !this.emittedSession) {
|
|
722
|
+
this.emittedSession = true;
|
|
723
|
+
return {
|
|
724
|
+
role: "runner",
|
|
725
|
+
kind: "checkpoint",
|
|
726
|
+
content: `Cursor session started: ${this.sessionId}.`,
|
|
727
|
+
eventType: "provider_thread_started"
|
|
728
|
+
};
|
|
729
|
+
}
|
|
626
730
|
}
|
|
627
|
-
|
|
731
|
+
if (type === "assistant") {
|
|
732
|
+
const text = assistantMessageText(event);
|
|
733
|
+
if (text) {
|
|
734
|
+
this.finalMessage = clip(text, 4000);
|
|
735
|
+
return {
|
|
736
|
+
role: "assistant",
|
|
737
|
+
kind: "message",
|
|
738
|
+
content: this.finalMessage,
|
|
739
|
+
eventType: "provider_message"
|
|
740
|
+
};
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
if (type === "tool_call") {
|
|
744
|
+
const subtype = stringField(event, "subtype");
|
|
745
|
+
const toolCall = objectField(event, "tool_call");
|
|
746
|
+
if (subtype === "started") {
|
|
747
|
+
const summary = formatCursorToolCallStarted(toolCall);
|
|
748
|
+
if (summary) {
|
|
749
|
+
return {
|
|
750
|
+
role: "worker",
|
|
751
|
+
kind: "checkpoint",
|
|
752
|
+
content: summary,
|
|
753
|
+
eventType: "provider_tool_call"
|
|
754
|
+
};
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
if (subtype === "completed") {
|
|
758
|
+
const completed = formatCursorToolCallCompleted(toolCall);
|
|
759
|
+
if (completed) {
|
|
760
|
+
return {
|
|
761
|
+
role: "worker",
|
|
762
|
+
kind: completed.kind,
|
|
763
|
+
content: completed.content,
|
|
764
|
+
eventType: completed.kind === "file_edit" ? "provider_file_edit" : "provider_tool_call"
|
|
765
|
+
};
|
|
766
|
+
}
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
if (type === "result" && stringField(event, "subtype") === "success") {
|
|
770
|
+
const result = stringField(event, "result");
|
|
771
|
+
if (result) {
|
|
772
|
+
this.finalMessage = clip(result, 4000);
|
|
773
|
+
}
|
|
774
|
+
}
|
|
775
|
+
if (type === "error" || (type === "result" && event.is_error === true)) {
|
|
776
|
+
return {
|
|
777
|
+
role: "runner",
|
|
778
|
+
kind: "error",
|
|
779
|
+
content: clip(line.trim(), 1000),
|
|
780
|
+
eventType: "provider_cli_error"
|
|
781
|
+
};
|
|
782
|
+
}
|
|
783
|
+
return null;
|
|
784
|
+
}
|
|
785
|
+
}
|
|
786
|
+
function formatCursorToolCallStarted(toolCall) {
|
|
787
|
+
const readArgs = objectField(objectField(toolCall, "readToolCall"), "args");
|
|
788
|
+
const readPath = stringField(readArgs, "path");
|
|
789
|
+
if (readPath) {
|
|
790
|
+
return `Reading ${readPath}`;
|
|
791
|
+
}
|
|
792
|
+
const writeArgs = objectField(objectField(toolCall, "writeToolCall"), "args");
|
|
793
|
+
const writePath = stringField(writeArgs, "path");
|
|
794
|
+
if (writePath) {
|
|
795
|
+
return `Writing ${writePath}`;
|
|
796
|
+
}
|
|
797
|
+
const shellArgs = objectField(objectField(toolCall, "shellToolCall"), "args");
|
|
798
|
+
const command = stringField(shellArgs, "command");
|
|
799
|
+
if (command) {
|
|
800
|
+
return `Shell command started: ${clip(command, 500)}`;
|
|
801
|
+
}
|
|
802
|
+
const fn = objectField(toolCall, "function");
|
|
803
|
+
const name = stringField(fn, "name");
|
|
804
|
+
if (name) {
|
|
805
|
+
return `Tool call started: ${name}`;
|
|
806
|
+
}
|
|
807
|
+
return null;
|
|
808
|
+
}
|
|
809
|
+
function formatCursorToolCallCompleted(toolCall) {
|
|
810
|
+
const writeArgs = objectField(objectField(toolCall, "writeToolCall"), "args");
|
|
811
|
+
const writePath = stringField(writeArgs, "path");
|
|
812
|
+
const writeResult = objectField(objectField(objectField(toolCall, "writeToolCall"), "result"), "success");
|
|
813
|
+
const writtenPath = stringField(writeResult, "path") ?? writePath;
|
|
814
|
+
if (writtenPath) {
|
|
815
|
+
return {
|
|
816
|
+
kind: "file_edit",
|
|
817
|
+
content: `Wrote ${writtenPath}`
|
|
818
|
+
};
|
|
819
|
+
}
|
|
820
|
+
const readArgs = objectField(objectField(toolCall, "readToolCall"), "args");
|
|
821
|
+
const readPath = stringField(readArgs, "path");
|
|
822
|
+
if (readPath) {
|
|
823
|
+
const readResult = objectField(objectField(objectField(toolCall, "readToolCall"), "result"), "success");
|
|
824
|
+
const totalLines = readResult.totalLines;
|
|
825
|
+
const lineSummary = typeof totalLines === "number" ? ` (${totalLines} lines)` : "";
|
|
826
|
+
return {
|
|
827
|
+
kind: "checkpoint",
|
|
828
|
+
content: `Read ${readPath}${lineSummary}`
|
|
829
|
+
};
|
|
830
|
+
}
|
|
831
|
+
const shellArgs = objectField(objectField(toolCall, "shellToolCall"), "args");
|
|
832
|
+
const command = stringField(shellArgs, "command");
|
|
833
|
+
if (command) {
|
|
834
|
+
return {
|
|
835
|
+
kind: "checkpoint",
|
|
836
|
+
content: `Shell command completed: ${clip(command, 500)}`
|
|
837
|
+
};
|
|
838
|
+
}
|
|
839
|
+
const fn = objectField(toolCall, "function");
|
|
840
|
+
const name = stringField(fn, "name");
|
|
841
|
+
if (name) {
|
|
842
|
+
return {
|
|
843
|
+
kind: "checkpoint",
|
|
844
|
+
content: `Tool call completed: ${name}`
|
|
845
|
+
};
|
|
846
|
+
}
|
|
847
|
+
return null;
|
|
848
|
+
}
|
|
849
|
+
function assistantMessageText(event) {
|
|
850
|
+
const message = objectField(event, "message");
|
|
851
|
+
const content = message.content;
|
|
852
|
+
if (!Array.isArray(content)) {
|
|
853
|
+
return null;
|
|
854
|
+
}
|
|
855
|
+
const parts = content
|
|
856
|
+
.map((entry) => {
|
|
857
|
+
if (!entry || typeof entry !== "object") {
|
|
628
858
|
return null;
|
|
629
859
|
}
|
|
630
|
-
|
|
860
|
+
const block = entry;
|
|
861
|
+
return stringField(block, "text");
|
|
862
|
+
})
|
|
863
|
+
.filter((text) => Boolean(text));
|
|
864
|
+
if (parts.length === 0) {
|
|
865
|
+
return null;
|
|
866
|
+
}
|
|
867
|
+
return parts.join("");
|
|
868
|
+
}
|
|
869
|
+
function parseJsonLine(line) {
|
|
870
|
+
const trimmed = line.trim();
|
|
871
|
+
if (!trimmed.startsWith("{")) {
|
|
872
|
+
return null;
|
|
873
|
+
}
|
|
874
|
+
let parsed;
|
|
875
|
+
try {
|
|
876
|
+
parsed = JSON.parse(trimmed);
|
|
877
|
+
}
|
|
878
|
+
catch {
|
|
879
|
+
return null;
|
|
880
|
+
}
|
|
881
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed)
|
|
882
|
+
? parsed
|
|
883
|
+
: null;
|
|
884
|
+
}
|
|
885
|
+
class CodexJsonState {
|
|
886
|
+
threadId = null;
|
|
887
|
+
finalMessage = null;
|
|
888
|
+
emittedThread = false;
|
|
889
|
+
get sessionLabel() {
|
|
890
|
+
return this.threadId ? `thread ${this.threadId}` : null;
|
|
891
|
+
}
|
|
892
|
+
consume(line) {
|
|
893
|
+
const event = parseJsonLine(line);
|
|
894
|
+
if (!event) {
|
|
631
895
|
return null;
|
|
632
896
|
}
|
|
633
|
-
const
|
|
634
|
-
const type = typeof event.type === "string" ? event.type : "";
|
|
897
|
+
const type = stringField(event, "type") ?? "";
|
|
635
898
|
if (type === "thread.started") {
|
|
636
899
|
this.threadId = stringField(event, "thread_id");
|
|
637
900
|
if (this.threadId && !this.emittedThread) {
|
|
@@ -656,7 +919,7 @@ class CodexJsonState {
|
|
|
656
919
|
return {
|
|
657
920
|
role: "runner",
|
|
658
921
|
kind: "error",
|
|
659
|
-
content: clip(
|
|
922
|
+
content: clip(line.trim(), 1000),
|
|
660
923
|
eventType: "provider_cli_error"
|
|
661
924
|
};
|
|
662
925
|
}
|
|
@@ -65,13 +65,15 @@ export class BridgeSessionService {
|
|
|
65
65
|
emit,
|
|
66
66
|
signal
|
|
67
67
|
});
|
|
68
|
+
const phaseArtifacts = run.artifacts?.length ? run.artifacts : run.artifact ? [run.artifact] : [];
|
|
68
69
|
return redactSecrets({
|
|
69
70
|
schemaVersion: 1,
|
|
70
71
|
exitCode: 0,
|
|
71
72
|
summary: run.summary,
|
|
72
73
|
events,
|
|
73
74
|
spec: run.spec ?? null,
|
|
74
|
-
artifact: run.artifact ?? null
|
|
75
|
+
artifact: run.artifact ?? null,
|
|
76
|
+
artifacts: phaseArtifacts
|
|
75
77
|
}, redactionSecrets);
|
|
76
78
|
}
|
|
77
79
|
finally {
|
|
@@ -18,6 +18,7 @@ export interface DispatchResolvedArtifact {
|
|
|
18
18
|
readonly scope: string;
|
|
19
19
|
readonly body: string;
|
|
20
20
|
readonly contentHash: string;
|
|
21
|
+
readonly version: number;
|
|
21
22
|
readonly lane?: string | null;
|
|
22
23
|
readonly event?: string | null;
|
|
23
24
|
}
|
|
@@ -101,6 +102,7 @@ export interface RunnerDispatchArtifact {
|
|
|
101
102
|
readonly report: string;
|
|
102
103
|
readonly producedBy: string;
|
|
103
104
|
readonly status: string;
|
|
105
|
+
readonly phase?: string | null;
|
|
104
106
|
}
|
|
105
107
|
export interface RunOnceResultPacket {
|
|
106
108
|
readonly schemaVersion: 1;
|
|
@@ -109,6 +111,7 @@ export interface RunOnceResultPacket {
|
|
|
109
111
|
readonly events: readonly RunnerDispatchEvent[];
|
|
110
112
|
readonly spec?: RunnerDispatchSpec | null;
|
|
111
113
|
readonly artifact?: RunnerDispatchArtifact | null;
|
|
114
|
+
readonly artifacts?: readonly RunnerDispatchArtifact[] | null;
|
|
112
115
|
}
|
|
113
116
|
export interface RunnerEventLine {
|
|
114
117
|
readonly type: "runner_event";
|
|
@@ -153,6 +153,7 @@ function parseArtifacts(value) {
|
|
|
153
153
|
scope: requiredString(artifact, "scope"),
|
|
154
154
|
body: optionalString(artifact, "body") ?? "",
|
|
155
155
|
contentHash: requiredString(artifact, "contentHash"),
|
|
156
|
+
version: optionalPositiveInteger(artifact, "version") ?? 1,
|
|
156
157
|
lane: optionalString(artifact, "lane"),
|
|
157
158
|
event: optionalString(artifact, "event")
|
|
158
159
|
};
|
|
@@ -188,6 +189,16 @@ function requiredBoolean(source, key) {
|
|
|
188
189
|
}
|
|
189
190
|
return value;
|
|
190
191
|
}
|
|
192
|
+
function optionalPositiveInteger(source, key) {
|
|
193
|
+
const value = source[key];
|
|
194
|
+
if (value === undefined || value === null) {
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
if (typeof value !== "number" || !Number.isInteger(value) || value < 1) {
|
|
198
|
+
throw new Error(`${key} must be a positive integer when provided.`);
|
|
199
|
+
}
|
|
200
|
+
return value;
|
|
201
|
+
}
|
|
191
202
|
function requiredNumber(source, key) {
|
|
192
203
|
const value = source[key];
|
|
193
204
|
if (typeof value !== "number" || !Number.isFinite(value)) {
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export interface CursorAgentLaunch {
|
|
2
|
+
readonly command: string;
|
|
3
|
+
readonly args: readonly string[];
|
|
4
|
+
}
|
|
5
|
+
export declare function isCursorAgentCommand(command: string): boolean;
|
|
6
|
+
export declare function resolveCursorAgentDirectLaunch(command: string, args: readonly string[]): Promise<CursorAgentLaunch | null>;
|
|
7
|
+
export declare function findLatestCursorAgentVersionDir(versionsRoot: string): Promise<string | null>;
|
|
8
|
+
export declare function compareCursorAgentVersionDirNames(left: string, right: string): number;
|
|
9
|
+
export declare function parseCursorAgentVersionSortKey(name: string): number;
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import fs from "node:fs/promises";
|
|
2
|
+
import path from "node:path";
|
|
3
|
+
const cursorAgentVersionDirPattern = /^\d{4}\.\d{1,2}\.\d{1,2}-.+/;
|
|
4
|
+
export function isCursorAgentCommand(command) {
|
|
5
|
+
const normalized = command.trim().toLowerCase();
|
|
6
|
+
return normalized === "cursor-agent" || normalized === "agent";
|
|
7
|
+
}
|
|
8
|
+
export async function resolveCursorAgentDirectLaunch(command, args) {
|
|
9
|
+
if (!isCursorAgentCommand(command)) {
|
|
10
|
+
return null;
|
|
11
|
+
}
|
|
12
|
+
const localAppData = process.env.LOCALAPPDATA;
|
|
13
|
+
if (!localAppData) {
|
|
14
|
+
return null;
|
|
15
|
+
}
|
|
16
|
+
const versionDir = await findLatestCursorAgentVersionDir(path.join(localAppData, "cursor-agent", "versions"));
|
|
17
|
+
if (!versionDir) {
|
|
18
|
+
return null;
|
|
19
|
+
}
|
|
20
|
+
const nodePath = path.join(versionDir, "node.exe");
|
|
21
|
+
const indexPath = path.join(versionDir, "index.js");
|
|
22
|
+
if (!(await fileExists(nodePath)) || !(await fileExists(indexPath))) {
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
return {
|
|
26
|
+
command: nodePath,
|
|
27
|
+
args: [indexPath, ...args]
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
export async function findLatestCursorAgentVersionDir(versionsRoot) {
|
|
31
|
+
let entries;
|
|
32
|
+
try {
|
|
33
|
+
entries = await fs.readdir(versionsRoot);
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
return null;
|
|
37
|
+
}
|
|
38
|
+
const latestName = entries
|
|
39
|
+
.filter((name) => cursorAgentVersionDirPattern.test(name))
|
|
40
|
+
.sort(compareCursorAgentVersionDirNames)
|
|
41
|
+
.at(-1);
|
|
42
|
+
return latestName ? path.join(versionsRoot, latestName) : null;
|
|
43
|
+
}
|
|
44
|
+
export function compareCursorAgentVersionDirNames(left, right) {
|
|
45
|
+
const leftKey = parseCursorAgentVersionSortKey(left);
|
|
46
|
+
const rightKey = parseCursorAgentVersionSortKey(right);
|
|
47
|
+
if (leftKey !== rightKey) {
|
|
48
|
+
return leftKey - rightKey;
|
|
49
|
+
}
|
|
50
|
+
return left.localeCompare(right);
|
|
51
|
+
}
|
|
52
|
+
export function parseCursorAgentVersionSortKey(name) {
|
|
53
|
+
const datePart = name.split("-")[0] ?? "";
|
|
54
|
+
const parts = datePart.split(".");
|
|
55
|
+
if (parts.length !== 3) {
|
|
56
|
+
return 0;
|
|
57
|
+
}
|
|
58
|
+
const year = parts[0] ?? "";
|
|
59
|
+
const month = (parts[1] ?? "").padStart(2, "0");
|
|
60
|
+
const day = (parts[2] ?? "").padStart(2, "0");
|
|
61
|
+
const numeric = Number(`${year}${month}${day}`);
|
|
62
|
+
return Number.isFinite(numeric) ? numeric : 0;
|
|
63
|
+
}
|
|
64
|
+
async function fileExists(inputPath) {
|
|
65
|
+
return fs.stat(inputPath).then((stat) => stat.isFile()).catch(() => false);
|
|
66
|
+
}
|
package/dist/version.d.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export declare const forgeBridgeVersion = "0.1.1-dev.
|
|
1
|
+
export declare const forgeBridgeVersion = "0.1.1-dev.ec3c3e74a648";
|
package/dist/version.js
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export const forgeBridgeVersion = "0.1.1-dev.
|
|
1
|
+
export const forgeBridgeVersion = "0.1.1-dev.ec3c3e74a648";
|
|
@@ -29,10 +29,7 @@ export function selectLaneCommand(artifacts, lane, phase) {
|
|
|
29
29
|
return artifacts.find((artifact) => normalizedArtifactKind(artifact.kind) === "command" && artifact.slug.trim().toLowerCase() === slug);
|
|
30
30
|
}
|
|
31
31
|
export function resolveCommandSlug(lane, phase) {
|
|
32
|
-
const normalizedPhase = phase.trim().toLowerCase();
|
|
33
|
-
if (normalizedPhase === "self_review") {
|
|
34
|
-
return "self-review-prompt";
|
|
35
|
-
}
|
|
32
|
+
const normalizedPhase = phase.trim().toLowerCase().replace(/-/g, "_");
|
|
36
33
|
if (normalizedPhase === "rework") {
|
|
37
34
|
return "rework-prompt";
|
|
38
35
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@loucompanion/forge-bridge",
|
|
3
|
-
"version": "0.1.1-dev.
|
|
3
|
+
"version": "0.1.1-dev.ec3c3e74a648",
|
|
4
4
|
"description": "Forge runner bridge CLI for connecting local developer machines to Forge dispatch.",
|
|
5
5
|
"license": "UNLICENSED",
|
|
6
6
|
"type": "module",
|