@miraland-labs/conduit-bridge 0.12.3 → 0.13.0
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 +17 -9
- package/dist/attempt-worktree.js +8 -0
- package/dist/client.js +2 -0
- package/dist/driver.js +13 -2
- package/dist/drivers.js +16 -3
- package/dist/ensure-test-evidence.js +55 -0
- package/dist/execution-class.js +8 -2
- package/dist/execution.js +430 -70
- package/dist/preflight.js +3 -2
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
Local Bridge CLI for [Conduit](https://github.com/miralandlabs/conduit). Connects a computer to one organization, claims work, and drives a local agent.
|
|
4
4
|
|
|
5
|
-
**Package version:** `0.
|
|
5
|
+
**Package version:** `0.12.3` — `ops connect` asks which org unless `--organization` is passed (does **not** silently reuse a leftover `CONDUIT_ORG` from `ops.env`); `ops install --workspace/--repo` persists declared intent without hand-editing the file; stamped **ExecutionClass** required; heartbeat **protocol 2 + preflight**. Prefer the Connect UI **join** command when switching organizations.
|
|
6
6
|
|
|
7
7
|
## Prerequisites
|
|
8
8
|
|
|
@@ -18,19 +18,27 @@ Local Bridge CLI for [Conduit](https://github.com/miralandlabs/conduit). Connect
|
|
|
18
18
|
|
|
19
19
|
## Easy path (all platforms)
|
|
20
20
|
|
|
21
|
-
|
|
21
|
+
**Preferred:** copy **join** from Conduit → Computers (includes `--organization` for the org you are signed into), approve, then:
|
|
22
|
+
|
|
23
|
+
```bash
|
|
24
|
+
npx @miraland-labs/conduit-bridge@latest ops install \
|
|
25
|
+
--workspace /path/to/repo \
|
|
26
|
+
--repo https://github.com/org/repo
|
|
27
|
+
npx @miraland-labs/conduit-bridge@latest ops status
|
|
28
|
+
```
|
|
29
|
+
|
|
30
|
+
**Alternate** (optional `ops.env` for URL/workspace defaults):
|
|
22
31
|
|
|
23
32
|
```bash
|
|
24
|
-
# 1) Config once
|
|
25
33
|
mkdir -p ~/.config/conduit
|
|
26
34
|
# Windows: mkdir %USERPROFILE%\.config\conduit
|
|
27
35
|
npx @miraland-labs/conduit-bridge@latest init-ops
|
|
28
36
|
cp ~/conduit/ops/env.example ~/.config/conduit/ops.env
|
|
29
|
-
# edit
|
|
37
|
+
# edit: CONDUIT_URL; leave CONDUIT_ORG unset (or commented) so connect asks
|
|
30
38
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
npx @miraland-labs/conduit-bridge@latest ops install
|
|
39
|
+
npx @miraland-labs/conduit-bridge@latest ops connect # asks "Join which org?"
|
|
40
|
+
# or: ops connect --organization your-org-slug
|
|
41
|
+
npx @miraland-labs/conduit-bridge@latest ops install --workspace /path/to/repo
|
|
34
42
|
npx @miraland-labs/conduit-bridge@latest ops status
|
|
35
43
|
```
|
|
36
44
|
|
|
@@ -39,7 +47,7 @@ Later:
|
|
|
39
47
|
```bash
|
|
40
48
|
npx @miraland-labs/conduit-bridge@latest ops online cursor
|
|
41
49
|
npx @miraland-labs/conduit-bridge@latest ops offline cursor
|
|
42
|
-
npx @miraland-labs/conduit-bridge@latest ops disconnect
|
|
50
|
+
npx @miraland-labs/conduit-bridge@latest ops disconnect # then join again to change organizations
|
|
43
51
|
```
|
|
44
52
|
|
|
45
53
|
Optional local shortcuts after `init-ops` (same verbs):
|
|
@@ -51,7 +59,7 @@ Optional local shortcuts after `init-ops` (same verbs):
|
|
|
51
59
|
|
|
52
60
|
**Windows note:** `ops install` brings lanes online and prints a `runner --workspace …` command to keep open (no LaunchAgent). macOS/Linux install a background service.
|
|
53
61
|
|
|
54
|
-
`ops.env` keys: `CONDUIT_URL`, `CONDUIT_ORG` (optional), `CONDUIT_WORKSPACE`, `CONDUIT_REPO` (optional), `CONDUIT_DRIVERS` (default: auto-detect installed agents), `CONDUIT_ROLES` (default `implement research review`). Values expand `$HOME`, `%USERPROFILE%`, and `~`.
|
|
62
|
+
`ops.env` keys: `CONDUIT_URL`, `CONDUIT_ORG` (optional; leave unset for interactive org prompt on `ops connect`), `CONDUIT_WORKSPACE`, `CONDUIT_REPO` (optional), `CONDUIT_DRIVERS` (default: auto-detect installed agents), `CONDUIT_ROLES` (default `implement research review`). Values expand `$HOME`, `%USERPROFILE%`, and `~`. Prefer `ops install --workspace/--repo` over hand-editing workspace keys.
|
|
55
63
|
|
|
56
64
|
## Advanced CLI
|
|
57
65
|
|
package/dist/attempt-worktree.js
CHANGED
|
@@ -56,6 +56,14 @@ export async function createAttemptWorktree(input) {
|
|
|
56
56
|
return path;
|
|
57
57
|
}
|
|
58
58
|
export async function removeAttemptWorktree(sourceWorkspace, worktreePath) {
|
|
59
|
+
// Settled repair releases are returned on every assignment poll until the local tree is gone.
|
|
60
|
+
// Make that at-least-once signal cheap and idempotent instead of invoking Git on a missing path.
|
|
61
|
+
try {
|
|
62
|
+
await access(worktreePath, constants.F_OK);
|
|
63
|
+
}
|
|
64
|
+
catch {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
59
67
|
try {
|
|
60
68
|
await execFileAsync("git", ["-C", sourceWorkspace, "worktree", "remove", "--force", worktreePath], { timeout: 60_000, maxBuffer: 2_000_000 });
|
|
61
69
|
}
|
package/dist/client.js
CHANGED
|
@@ -48,6 +48,8 @@ export class ConduitClient {
|
|
|
48
48
|
leaseExpiresAt: String(data.lease_expires_at),
|
|
49
49
|
phase: "claimed",
|
|
50
50
|
...(extras?.driverId ? { driverId: extras.driverId } : {}),
|
|
51
|
+
...(extras?.executionKind ? { executionKind: extras.executionKind } : {}),
|
|
52
|
+
...(extras?.sourceAttemptId ? { sourceAttemptId: extras.sourceAttemptId } : {}),
|
|
51
53
|
};
|
|
52
54
|
this.config.activeAttempts[taskId] = active;
|
|
53
55
|
await this.persistRuntime(this.config);
|
package/dist/driver.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { spawn } from "node:child_process";
|
|
2
2
|
import { existsSync } from "node:fs";
|
|
3
|
-
import { mkdir, readFile, rm, writeFile } from "node:fs/promises";
|
|
3
|
+
import { mkdir, readFile, rm, rmdir, writeFile } from "node:fs/promises";
|
|
4
4
|
import { join } from "node:path";
|
|
5
5
|
import { z } from "zod";
|
|
6
6
|
import { deniedCommands, executionClassPromptRules, parsePiJsonl, projectClaude, projectCoarseMode, projectCodex, projectCursor, projectKiroTools, projectPi, requireStampedExecutionClass, } from "./execution-class.js";
|
|
@@ -334,6 +334,7 @@ export const claudeCodeDriver = {
|
|
|
334
334
|
const projected = projectClaude(executionClass, {
|
|
335
335
|
grants: input.grants,
|
|
336
336
|
verificationCommands: input.verificationCommands,
|
|
337
|
+
diagnosis: input.workRole === "diagnose",
|
|
337
338
|
});
|
|
338
339
|
// Fail closed: omitting --allowedTools would leave only the deny-list and
|
|
339
340
|
// broaden the agent beyond the grant contract (privileged grants map to none).
|
|
@@ -456,7 +457,10 @@ export const codexDriver = {
|
|
|
456
457
|
};
|
|
457
458
|
}
|
|
458
459
|
const executionClass = resolveRunClass(input);
|
|
459
|
-
const projected = projectCodex(executionClass, {
|
|
460
|
+
const projected = projectCodex(executionClass, {
|
|
461
|
+
capabilities: input.capabilities,
|
|
462
|
+
diagnosis: input.workRole === "diagnose",
|
|
463
|
+
});
|
|
460
464
|
// Prompt via stdin avoids ARG_MAX limits on large assignment contracts.
|
|
461
465
|
const { code, stdout, stderr } = await execute(input.executable ?? resolveCodexExecutable(), codexExecArgs({ ...input, networkAccess: projected.networkAccess }, projected.sandbox), input.workspace, input.timeoutMs ?? 20 * 60_000, fuelSource === "conduit" ? input.fuel : undefined, fuelSource, input.prompt);
|
|
462
466
|
const parsed = parseCodexJsonl(stdout);
|
|
@@ -588,6 +592,7 @@ export const cursorDriver = {
|
|
|
588
592
|
grants: input.grants,
|
|
589
593
|
verificationCommands: input.verificationCommands,
|
|
590
594
|
capabilities: input.capabilities ?? [],
|
|
595
|
+
diagnosis: input.workRole === "diagnose",
|
|
591
596
|
});
|
|
592
597
|
const configured = await withCursorPermissions(input.workspace, { allow: projected.allow, deny: projected.deny }, () => execute(executable, cursorRunArgs({ ...input, trustWorkspace, executionClass, force: projected.force }), input.workspace, input.timeoutMs ?? 20 * 60_000, undefined, "local"));
|
|
593
598
|
const { code, stdout, stderr } = configured;
|
|
@@ -602,6 +607,7 @@ export const cursorDriver = {
|
|
|
602
607
|
async function withCursorPermissions(workspace, permissions, run) {
|
|
603
608
|
const configDir = join(workspace, ".cursor");
|
|
604
609
|
const configPath = join(configDir, "cli.json");
|
|
610
|
+
const configDirExisted = existsSync(configDir);
|
|
605
611
|
let previous = null;
|
|
606
612
|
try {
|
|
607
613
|
previous = await readFile(configPath, "utf8");
|
|
@@ -621,6 +627,11 @@ async function withCursorPermissions(workspace, permissions, run) {
|
|
|
621
627
|
await rm(configPath, { force: true });
|
|
622
628
|
else
|
|
623
629
|
await writeFile(configPath, previous, "utf8");
|
|
630
|
+
// Do not leave control-plane permission scaffolding in a worktree that had no Cursor config.
|
|
631
|
+
// rmdir removes only an empty directory, preserving anything the operator already had or the
|
|
632
|
+
// CLI legitimately created during the run.
|
|
633
|
+
if (!configDirExisted)
|
|
634
|
+
await rmdir(configDir).catch(() => undefined);
|
|
624
635
|
}
|
|
625
636
|
}
|
|
626
637
|
/**
|
package/dist/drivers.js
CHANGED
|
@@ -16,6 +16,12 @@ const LABEL_TO_DRIVER = {
|
|
|
16
16
|
Antigravity: "antigravity",
|
|
17
17
|
};
|
|
18
18
|
const LOCAL_FUEL_ONLY_DRIVERS = new Set(["cursor", "pi", "kiro", "antigravity"]);
|
|
19
|
+
/**
|
|
20
|
+
* Drivers that can combine repository reads with bounded verification while denying source edits.
|
|
21
|
+
* Coarse build/bash modes are not enough for Invariant 23: a diagnosis carries `test_run`, but it
|
|
22
|
+
* must never acquire `repo_write` as an implementation detail of the selected lane.
|
|
23
|
+
*/
|
|
24
|
+
const READ_ONLY_DIAGNOSIS_DRIVERS = new Set(["claude-code", "codex", "cursor"]);
|
|
19
25
|
export function isSupportedDriverId(id) {
|
|
20
26
|
return Object.prototype.hasOwnProperty.call(DRIVERS, id);
|
|
21
27
|
}
|
|
@@ -25,6 +31,9 @@ export function driverLabel(id) {
|
|
|
25
31
|
export function localFuelOnlyDriver(id) {
|
|
26
32
|
return LOCAL_FUEL_ONLY_DRIVERS.has(id);
|
|
27
33
|
}
|
|
34
|
+
export function supportsReadOnlyDiagnosis(id) {
|
|
35
|
+
return READ_ONLY_DIAGNOSIS_DRIVERS.has(id);
|
|
36
|
+
}
|
|
28
37
|
/** Map PATH probe labels to unique driver ids. */
|
|
29
38
|
export function driverIdsFromDetectedLabels(labels) {
|
|
30
39
|
const ids = new Set();
|
|
@@ -148,9 +157,13 @@ export function driversHeartbeatReport(config, activeAttempts, processOnlineIds)
|
|
|
148
157
|
* Pick an online driver for a new claim: least loaded among online (shared machine pool).
|
|
149
158
|
* `processOnlineIds` overrides config online lanes (tests / harness).
|
|
150
159
|
*/
|
|
151
|
-
export function pickDriverForClaim(config, processOnlineIds) {
|
|
152
|
-
const
|
|
153
|
-
|
|
160
|
+
export function pickDriverForClaim(config, processOnlineIds, eligible = () => true) {
|
|
161
|
+
const candidates = processOnlineIds === undefined || processOnlineIds === null
|
|
162
|
+
? onlineDriverIds(config)
|
|
163
|
+
: processOnlineIds;
|
|
164
|
+
const online = candidates
|
|
165
|
+
.filter((id) => isSupportedDriverId(id))
|
|
166
|
+
.filter(eligible);
|
|
154
167
|
if (!online.length)
|
|
155
168
|
return null;
|
|
156
169
|
const load = new Map();
|
|
@@ -30,6 +30,61 @@ export function pickVerificationCommand(commands) {
|
|
|
30
30
|
function argvForBoundedCommand(command) {
|
|
31
31
|
return command.trim().split(/\s+/);
|
|
32
32
|
}
|
|
33
|
+
/**
|
|
34
|
+
* Why an agent failure needs its own verification run.
|
|
35
|
+
*
|
|
36
|
+
* When a driver run fails, `result.error` is `stderr || resultText` — and a coding agent runs its
|
|
37
|
+
* own verification *inside its tool loop*, so that output never reaches Bridge's stderr. Production
|
|
38
|
+
* proved the consequence: the failure reason reaching Conduit was a bare git SHA or a chunk of the
|
|
39
|
+
* agent's echoed source, and Conductor had nothing to diagnose from.
|
|
40
|
+
*
|
|
41
|
+
* Running the project's own bounded verification command in the attempt worktree recovers the real
|
|
42
|
+
* compiler/test output. Read-only by construction: the same bounded command list Bridge already
|
|
43
|
+
* trusts for test evidence, in the workspace the failed attempt left behind.
|
|
44
|
+
*/
|
|
45
|
+
/**
|
|
46
|
+
* Bounded commands to try for failure diagnosis. Unlike `pickVerificationCommand` (used for
|
|
47
|
+
* delivery test evidence), do **not** prefer `npm run test` alone: a green test suite must not
|
|
48
|
+
* hide a failed typecheck/lint/build that the agent broke.
|
|
49
|
+
*/
|
|
50
|
+
export function diagnosisVerificationCommands(commands) {
|
|
51
|
+
const bounded = commands
|
|
52
|
+
.map((command) => command.trim())
|
|
53
|
+
.filter((command) => command && isBoundedVerificationCommand(command));
|
|
54
|
+
const nonTest = bounded.filter((command) => !isPreferentialTestCommand(command));
|
|
55
|
+
const tests = bounded.filter((command) => isPreferentialTestCommand(command));
|
|
56
|
+
return [...nonTest, ...tests];
|
|
57
|
+
}
|
|
58
|
+
export async function captureVerificationFailure(input) {
|
|
59
|
+
const commands = diagnosisVerificationCommands(input.verificationCommands);
|
|
60
|
+
if (commands.length === 0)
|
|
61
|
+
return null;
|
|
62
|
+
const run = input.runCommand ?? defaultRunCommand;
|
|
63
|
+
// Try each bounded command until one fails. First non-zero wins (typecheck before test when both exist).
|
|
64
|
+
for (const command of commands) {
|
|
65
|
+
let result;
|
|
66
|
+
try {
|
|
67
|
+
result = await run(command, input.workspace);
|
|
68
|
+
}
|
|
69
|
+
catch {
|
|
70
|
+
// Diagnosis is best-effort: try the next command rather than failing the attempt again.
|
|
71
|
+
continue;
|
|
72
|
+
}
|
|
73
|
+
if (result.code === 0)
|
|
74
|
+
continue;
|
|
75
|
+
const lines = [
|
|
76
|
+
`$ ${command}`,
|
|
77
|
+
...(result.stdout.trim() ? result.stdout.trim().split("\n") : []),
|
|
78
|
+
...(result.stderr.trim() ? result.stderr.trim().split("\n") : []),
|
|
79
|
+
`exit ${result.code}`,
|
|
80
|
+
].map((line) => line.slice(0, 4_000)).slice(0, 120);
|
|
81
|
+
const text = lines.join("\n").slice(0, 12_000);
|
|
82
|
+
if (text.trim())
|
|
83
|
+
return text;
|
|
84
|
+
}
|
|
85
|
+
// Every bounded command passed (or none could start): agent failed for another reason.
|
|
86
|
+
return null;
|
|
87
|
+
}
|
|
33
88
|
async function defaultRunCommand(command, workspace) {
|
|
34
89
|
const argv = argvForBoundedCommand(command);
|
|
35
90
|
const bin = argv[0];
|
package/dist/execution-class.js
CHANGED
|
@@ -126,8 +126,13 @@ export function projectCursor(executionClass, input) {
|
|
|
126
126
|
? landCommands.map((command) => `Shell(${command})`)
|
|
127
127
|
: []),
|
|
128
128
|
];
|
|
129
|
-
if (executionClass === "observe" || executionClass === "observe_network") {
|
|
129
|
+
if (executionClass === "observe" || executionClass === "observe_network" || input.diagnosis) {
|
|
130
130
|
deny.push("Shell(*)", "Write(**)");
|
|
131
|
+
if (input.diagnosis) {
|
|
132
|
+
// Re-open only the bounded verification commands below. Diagnosis may run checks, never an
|
|
133
|
+
// arbitrary shell or Cursor's write tool.
|
|
134
|
+
deny.splice(deny.indexOf("Shell(*)"), 1);
|
|
135
|
+
}
|
|
131
136
|
}
|
|
132
137
|
const allow = [];
|
|
133
138
|
if (allowsExternalFetch(executionClass, capabilities))
|
|
@@ -175,6 +180,7 @@ export function projectClaude(executionClass, input) {
|
|
|
175
180
|
return {
|
|
176
181
|
allowedTools: [
|
|
177
182
|
"Read",
|
|
183
|
+
...(input.diagnosis ? ["Glob", "Grep"] : []),
|
|
178
184
|
...verificationCommands.filter(isBoundedVerificationCommand).map((command) => `Bash(${command}:*)`),
|
|
179
185
|
...fetchTools,
|
|
180
186
|
],
|
|
@@ -207,7 +213,7 @@ export function projectCodex(executionClass, input) {
|
|
|
207
213
|
const networkAccess = allowsExternalFetch(executionClass, capabilities);
|
|
208
214
|
// Read-only is the sandbox for the one class that may not write; everything else needs the
|
|
209
215
|
// workspace. Network is the shared predicate, so Codex cannot drift from Cursor and Claude again.
|
|
210
|
-
if (executionClass === "observe")
|
|
216
|
+
if (executionClass === "observe" || input.diagnosis)
|
|
211
217
|
return { sandbox: "read-only", networkAccess };
|
|
212
218
|
return { sandbox: "workspace-write", networkAccess };
|
|
213
219
|
}
|
package/dist/execution.js
CHANGED
|
@@ -2,13 +2,13 @@ import { createHash } from "node:crypto";
|
|
|
2
2
|
import { z } from "zod";
|
|
3
3
|
import { ConduitRequestError } from "./client.js";
|
|
4
4
|
import { redactSecrets } from "./config.js";
|
|
5
|
-
import { DRIVERS, agentReportTemplate, buildAssignmentPrompt, evidenceKinds, fuelEndpoint, normalizeEvidenceDigest, parseAgentReport, pickModelCandidate, requireStampedExecutionClass, tierForRisk } from "./driver.js";
|
|
5
|
+
import { DRIVERS, agentReportTemplate, buildAssignmentPrompt, evidenceKinds, extractAgentReportJsonText, fuelEndpoint, normalizeEvidenceDigest, parseAgentReport, parseJsonObjectCandidate, pickModelCandidate, requireStampedExecutionClass, tierForRisk } from "./driver.js";
|
|
6
6
|
import { assertClassFloor } from "./execution-class.js";
|
|
7
|
-
import { pickDriverForClaim, resolveDriverFuel } from "./drivers.js";
|
|
7
|
+
import { pickDriverForClaim, resolveDriverFuel, supportsReadOnlyDiagnosis } from "./drivers.js";
|
|
8
8
|
import { attemptWorktreePath, createAttemptWorktree, proveResumeWorktree, quarantineAttemptWorktree, removeAttemptWorktree, } from "./attempt-worktree.js";
|
|
9
9
|
import { buildWorkspaceBrief, ensureCommitAvailable, normalizeRepositoryUrl, resolveAttemptStartCommit } from "./brief.js";
|
|
10
10
|
import { ensureDeliveryPullRequest } from "./ensure-pull-request.js";
|
|
11
|
-
import { ensureTestEvidence } from "./ensure-test-evidence.js";
|
|
11
|
+
import { captureVerificationFailure, ensureTestEvidence } from "./ensure-test-evidence.js";
|
|
12
12
|
/** Feedback text for changes_requested summaries (plain string or `{ feedback }`). */
|
|
13
13
|
function changesRequestedFeedback(summary) {
|
|
14
14
|
if (!summary)
|
|
@@ -62,10 +62,13 @@ const assignmentSchema = z.object({
|
|
|
62
62
|
id: z.string().uuid(),
|
|
63
63
|
attempt_id: z.string().uuid(),
|
|
64
64
|
execution_mode: z.enum(["agent", "human"]).optional().default("agent"),
|
|
65
|
+
execution_kind: z.enum(["authoring", "diagnosis"]).optional().default("authoring"),
|
|
66
|
+
source_attempt_id: z.string().uuid().nullable().optional().default(null),
|
|
65
67
|
repository_fingerprint: z.string().nullable().optional().default(null),
|
|
66
68
|
requested_base_commit: z.string().nullable().optional().default(null),
|
|
67
69
|
claimed_head: z.string().nullable().optional().default(null),
|
|
68
70
|
});
|
|
71
|
+
const releasedWorktreeAttemptIdsSchema = z.array(z.string().uuid()).max(50);
|
|
69
72
|
const taskDetailSchema = z.object({
|
|
70
73
|
objective: z.string(),
|
|
71
74
|
project_id: z.string().uuid(),
|
|
@@ -74,6 +77,8 @@ const taskDetailSchema = z.object({
|
|
|
74
77
|
delivery_state: z.string(),
|
|
75
78
|
delivery_summary: z.string().nullable(),
|
|
76
79
|
execution_mode: z.enum(["agent", "human"]).optional().default("agent"),
|
|
80
|
+
execution_kind: z.enum(["authoring", "diagnosis"]).optional().default("authoring"),
|
|
81
|
+
source_attempt_id: z.string().uuid().nullable().optional().default(null),
|
|
77
82
|
});
|
|
78
83
|
const taskSpecSchema = z.object({
|
|
79
84
|
goal: z.string().optional(), scope: z.array(z.string()).optional(), boundaries: z.array(z.string()).optional(),
|
|
@@ -94,6 +99,7 @@ const executionContractSchema = z.object({
|
|
|
94
99
|
repository_fingerprint: z.string().nullable().optional().default(null),
|
|
95
100
|
requested_base_commit: z.string().nullable().optional().default(null),
|
|
96
101
|
claimed_head: z.string().nullable().optional().default(null),
|
|
102
|
+
source_attempt_id: z.string().uuid().nullable().optional().default(null),
|
|
97
103
|
});
|
|
98
104
|
const workPackageSchema = z.object({
|
|
99
105
|
work_role: z.string().nullable().optional(),
|
|
@@ -111,8 +117,104 @@ const workPackageSchema = z.object({
|
|
|
111
117
|
}).optional(),
|
|
112
118
|
decisions: z.array(z.object({ question: z.string(), decision: z.string() })).optional(),
|
|
113
119
|
rework_feedback: z.string().nullable().optional(),
|
|
120
|
+
diagnostic_context: z.object({
|
|
121
|
+
source_attempt_id: z.string().uuid(),
|
|
122
|
+
failure_output: z.string().min(1).max(12_000),
|
|
123
|
+
}).optional(),
|
|
114
124
|
working_language: z.enum(["en", "zh"]).optional(),
|
|
115
125
|
}).nullable().optional();
|
|
126
|
+
const diagnosticRepairBriefSchema = z.object({
|
|
127
|
+
root_cause: z.string().trim().min(1).max(2_000),
|
|
128
|
+
wrong_approach: z.string().trim().max(2_000).default(""),
|
|
129
|
+
fix_direction: z.string().trim().min(1).max(4_000),
|
|
130
|
+
do_not_touch: z.array(z.string().trim().min(1).max(400)).max(10).default([]),
|
|
131
|
+
confident: z.boolean().default(true),
|
|
132
|
+
});
|
|
133
|
+
function diagnosticText(value, max) {
|
|
134
|
+
if (typeof value === "string")
|
|
135
|
+
return value.trim().slice(0, max);
|
|
136
|
+
if (!Array.isArray(value))
|
|
137
|
+
return undefined;
|
|
138
|
+
const parts = value.filter((item) => typeof item === "string")
|
|
139
|
+
.map((item) => item.trim()).filter(Boolean);
|
|
140
|
+
if (!parts.length)
|
|
141
|
+
return undefined;
|
|
142
|
+
return (parts.length === 1 ? parts[0] : parts.map((part, index) => `${index + 1}. ${part}`).join("\n"))
|
|
143
|
+
.slice(0, max);
|
|
144
|
+
}
|
|
145
|
+
/** Parse the diagnostic agent's final JSON with the same field-shape tolerance as the CP. */
|
|
146
|
+
export function parseDiagnosticRepairBrief(text) {
|
|
147
|
+
let raw = parseJsonObjectCandidate(extractAgentReportJsonText(text));
|
|
148
|
+
if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
|
|
149
|
+
throw new Error("Diagnostic agent did not emit a RepairBrief object");
|
|
150
|
+
}
|
|
151
|
+
let obj = { ...raw };
|
|
152
|
+
const keys = Object.keys(obj);
|
|
153
|
+
if (keys.length === 1 && obj[keys[0]] && typeof obj[keys[0]] === "object"
|
|
154
|
+
&& !Array.isArray(obj[keys[0]]) && "fix_direction" in obj[keys[0]]) {
|
|
155
|
+
obj = { ...obj[keys[0]] };
|
|
156
|
+
}
|
|
157
|
+
for (const [field, max] of [["root_cause", 2_000], ["wrong_approach", 2_000], ["fix_direction", 4_000]]) {
|
|
158
|
+
const value = diagnosticText(obj[field], max);
|
|
159
|
+
if (value !== undefined)
|
|
160
|
+
obj[field] = value;
|
|
161
|
+
}
|
|
162
|
+
if (obj.wrong_approach == null)
|
|
163
|
+
obj.wrong_approach = "";
|
|
164
|
+
obj.do_not_touch = Array.isArray(obj.do_not_touch)
|
|
165
|
+
? obj.do_not_touch.filter((item) => typeof item === "string")
|
|
166
|
+
.map((item) => item.trim().slice(0, 400)).filter(Boolean).slice(0, 10)
|
|
167
|
+
: typeof obj.do_not_touch === "string" && obj.do_not_touch.trim()
|
|
168
|
+
? [obj.do_not_touch.trim().slice(0, 400)]
|
|
169
|
+
: [];
|
|
170
|
+
if (typeof obj.confident === "string") {
|
|
171
|
+
const token = obj.confident.trim().toLowerCase();
|
|
172
|
+
if (token === "true" || token === "yes")
|
|
173
|
+
obj.confident = true;
|
|
174
|
+
else if (token === "false" || token === "no")
|
|
175
|
+
obj.confident = false;
|
|
176
|
+
}
|
|
177
|
+
const parsed = diagnosticRepairBriefSchema.safeParse(obj);
|
|
178
|
+
if (!parsed.success) {
|
|
179
|
+
const issue = parsed.error.issues[0];
|
|
180
|
+
throw new Error(`Diagnostic RepairBrief is invalid${issue?.path.length ? ` at ${issue.path.join(".")}` : ""}: ${issue?.message ?? "unknown shape"}`);
|
|
181
|
+
}
|
|
182
|
+
return parsed.data;
|
|
183
|
+
}
|
|
184
|
+
/** Prompt for Option C: IDE-depth understanding in the retained tree, with write authority absent. */
|
|
185
|
+
export function buildDiagnosticPrompt(input) {
|
|
186
|
+
const pkg = input.workPackage;
|
|
187
|
+
const goal = pkg?.goal ?? input.spec.goal ?? input.objective;
|
|
188
|
+
const scope = pkg?.scope?.length ? pkg.scope : input.spec.scope ?? [];
|
|
189
|
+
const boundaries = pkg?.boundaries?.length ? pkg.boundaries : input.spec.boundaries ?? [];
|
|
190
|
+
const acceptance = pkg?.acceptance?.length ? pkg.acceptance : input.spec.acceptance ?? [];
|
|
191
|
+
const changeScope = pkg?.change_scope?.length ? pkg.change_scope : input.spec.change_scope ?? [];
|
|
192
|
+
const decisions = pkg?.decisions ?? [];
|
|
193
|
+
return [
|
|
194
|
+
"You are Conductor's read-only diagnosis lane. Diagnose the exact failed authoring run in the current retained worktree.",
|
|
195
|
+
"Inspect the actual code, git diff, nearby callers, and relevant tests. Run only the supplied project verification commands when useful.",
|
|
196
|
+
"You have repo_read and test_run only. Do not edit, create, delete, format, commit, branch, push, install, or change any file.",
|
|
197
|
+
"Do not propose owner-authored compiler constraints and do not return a Delivery Packet. Your only deliverable is a grounded RepairBrief for the next authoring agent.",
|
|
198
|
+
"The RepairBrief crosses to the control plane: name files, symbols, and fix steps, but do not quote secrets or copy file bodies into it.",
|
|
199
|
+
"If the worktree does not support a grounded diagnosis, set confident to false; never guess.",
|
|
200
|
+
"",
|
|
201
|
+
`TASK ${input.taskId}`,
|
|
202
|
+
`GOAL\n${goal}`,
|
|
203
|
+
...(scope.length ? ["", `APPROVED SCOPE\n${scope.map((item) => `- ${item}`).join("\n")}`] : []),
|
|
204
|
+
...(boundaries.length ? ["", `BOUNDARIES\n${boundaries.map((item) => `- ${item}`).join("\n")}`] : []),
|
|
205
|
+
...(acceptance.length ? ["", `APPROVED ACCEPTANCE CRITERIA\n${acceptance.map((item) => `- ${item}`).join("\n")}`] : []),
|
|
206
|
+
...(changeScope.length ? ["", `APPROVED CHANGE SCOPE\n${changeScope.map((item) => `- ${item}`).join("\n")}`] : []),
|
|
207
|
+
...(decisions.length ? ["", `OWNER DECISIONS\n${decisions.map((item) => `- ${item.question}: ${item.decision}`).join("\n")}`] : []),
|
|
208
|
+
...(input.verificationCommands.length ? ["", `BOUNDED VERIFICATION COMMANDS\n${input.verificationCommands.map((item) => `- ${item}`).join("\n")}`] : []),
|
|
209
|
+
"",
|
|
210
|
+
"WITNESSED FAILURE OUTPUT",
|
|
211
|
+
redactSecrets(input.failureOutput),
|
|
212
|
+
"",
|
|
213
|
+
"Return only one fenced ```json object with exactly this shape:",
|
|
214
|
+
'{"root_cause":"specific cause grounded in files/symbols","wrong_approach":"what the failed run did wrong","fix_direction":"ordered fix instructions for the authoring agent","do_not_touch":["out-of-scope paths or behavior"],"confident":true}',
|
|
215
|
+
"root_cause, wrong_approach, and fix_direction are strings, not arrays. Close the fence and add no prose after it.",
|
|
216
|
+
].join("\n");
|
|
217
|
+
}
|
|
116
218
|
export async function renewLeases(client, config) {
|
|
117
219
|
for (const active of Object.values(config.activeAttempts)) {
|
|
118
220
|
if (Date.parse(active.leaseExpiresAt) - Date.now() < 120_000) {
|
|
@@ -142,6 +244,26 @@ export async function recoverActiveAttempt(client, config, driver, workspace, br
|
|
|
142
244
|
const active = taskId ? config.activeAttempts[taskId] : Object.values(config.activeAttempts)[0];
|
|
143
245
|
if (!active)
|
|
144
246
|
return false;
|
|
247
|
+
// A terminal payload is already complete and replay-safe. Replaying it must not depend on the
|
|
248
|
+
// driver that produced it still being installed or online after a Bridge restart.
|
|
249
|
+
if (active.phase === "terminal_pending") {
|
|
250
|
+
const response = await flushTerminal(client, active.taskId);
|
|
251
|
+
const diagnosis = active.executionKind === "diagnosis";
|
|
252
|
+
const worktreeOwner = diagnosis ? active.sourceAttemptId : active.attemptId;
|
|
253
|
+
const worktree = active.worktreePath ?? (worktreeOwner ? attemptWorktreePath(workspace, worktreeOwner) : "");
|
|
254
|
+
if (diagnosis && !retainDiagnosticWorktree(response) && worktree) {
|
|
255
|
+
await removeAttemptWorktree(workspace, worktree).catch(() => undefined);
|
|
256
|
+
}
|
|
257
|
+
else if (!diagnosis && response.retain_worktree !== true && worktree) {
|
|
258
|
+
if (active.terminal?.action === "fail") {
|
|
259
|
+
await quarantineAttemptWorktree(workspace, worktree, active.attemptId).catch(() => removeAttemptWorktree(workspace, worktree).catch(() => undefined));
|
|
260
|
+
}
|
|
261
|
+
else {
|
|
262
|
+
await removeAttemptWorktree(workspace, worktree).catch(() => undefined);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
return true;
|
|
266
|
+
}
|
|
145
267
|
const laneDriver = resolveAttemptDriver(config, active, driver);
|
|
146
268
|
if (!laneDriver) {
|
|
147
269
|
console.error(`No driver for attempt ${active.attemptId} (driverId=${active.driverId ?? "unset"})`);
|
|
@@ -153,10 +275,12 @@ export async function recoverActiveAttempt(client, config, driver, workspace, br
|
|
|
153
275
|
await client.updateAttempt(active.taskId, { driverId: assigned });
|
|
154
276
|
}
|
|
155
277
|
if (active.phase === "agent_running") {
|
|
156
|
-
const
|
|
278
|
+
const diagnosis = active.executionKind === "diagnosis";
|
|
279
|
+
const worktreeOwner = diagnosis ? active.sourceAttemptId : active.attemptId;
|
|
280
|
+
const worktree = active.worktreePath ?? (worktreeOwner ? attemptWorktreePath(workspace, worktreeOwner) : "");
|
|
157
281
|
const sessionId = config.sessions?.[active.taskId]?.trim() || "";
|
|
158
282
|
// F-07 safe resume: only when session + worktree identity are both proven. Otherwise interrupt.
|
|
159
|
-
if (sessionId && await proveResumeWorktree(workspace,
|
|
283
|
+
if (worktreeOwner && sessionId && await proveResumeWorktree(workspace, worktreeOwner, worktree)) {
|
|
160
284
|
console.log(`Resuming interrupted attempt ${active.attemptId} with proven worktree and session.`);
|
|
161
285
|
await runClaimedAssignment(client, config, laneDriver, workspace, brief, active.taskId, timeoutMs, supervision, {
|
|
162
286
|
existingWorktree: worktree,
|
|
@@ -164,22 +288,29 @@ export async function recoverActiveAttempt(client, config, driver, workspace, br
|
|
|
164
288
|
});
|
|
165
289
|
return true;
|
|
166
290
|
}
|
|
167
|
-
await
|
|
168
|
-
await client.updateAttempt(active.taskId, { worktreePath: undefined });
|
|
169
|
-
await queueTerminal(client, active.taskId, {
|
|
291
|
+
const response = await queueTerminal(client, active.taskId, {
|
|
170
292
|
action: "fail",
|
|
171
|
-
body: {
|
|
293
|
+
body: {
|
|
294
|
+
error: diagnosis
|
|
295
|
+
? "Bridge restarted during read-only diagnosis; Conduit may retry diagnosis safely."
|
|
296
|
+
: "Bridge restarted during agent execution; Conduit may retry this work safely.",
|
|
297
|
+
idempotency_key: `bridge:restart:${active.attemptId}`,
|
|
298
|
+
},
|
|
172
299
|
});
|
|
300
|
+
if (diagnosis && !retainDiagnosticWorktree(response) && worktree) {
|
|
301
|
+
await removeAttemptWorktree(workspace, worktree).catch(() => undefined);
|
|
302
|
+
}
|
|
303
|
+
else if (!diagnosis && response.retain_worktree !== true && worktree) {
|
|
304
|
+
// Do not quarantine before the terminal response: Conduit may need this exact tree for a
|
|
305
|
+
// pinned diagnosis even when Bridge restarted before it could preserve the agent session.
|
|
306
|
+
await quarantineAttemptWorktree(workspace, worktree, active.attemptId).catch(() => removeAttemptWorktree(workspace, worktree).catch(() => undefined));
|
|
307
|
+
}
|
|
173
308
|
return true;
|
|
174
309
|
}
|
|
175
310
|
if (client.attempt(active.taskId).phase === "agent_finished") {
|
|
176
311
|
await submitFinishedDelivery(client, active.taskId);
|
|
177
312
|
return true;
|
|
178
313
|
}
|
|
179
|
-
if (client.attempt(active.taskId).phase === "terminal_pending") {
|
|
180
|
-
await flushTerminal(client, active.taskId);
|
|
181
|
-
return true;
|
|
182
|
-
}
|
|
183
314
|
await runClaimedAssignment(client, config, laneDriver, workspace, brief, active.taskId, timeoutMs, supervision);
|
|
184
315
|
return true;
|
|
185
316
|
}
|
|
@@ -199,8 +330,10 @@ export async function pumpExecutionSlots(client, config, workspace, brief, timeo
|
|
|
199
330
|
if (running.has(id))
|
|
200
331
|
continue;
|
|
201
332
|
const active = config.activeAttempts[id];
|
|
202
|
-
const laneDriver =
|
|
203
|
-
|
|
333
|
+
const laneDriver = active.phase === "terminal_pending"
|
|
334
|
+
? fallbackDriver
|
|
335
|
+
: resolveAttemptDriver(config, active, fallbackDriver);
|
|
336
|
+
if (!laneDriver && active.phase !== "terminal_pending") {
|
|
204
337
|
console.error(`Slot recovery skipped for ${id}: no driver lane`);
|
|
205
338
|
continue;
|
|
206
339
|
}
|
|
@@ -228,9 +361,12 @@ export async function pumpExecutionSlots(client, config, workspace, brief, timeo
|
|
|
228
361
|
}
|
|
229
362
|
if (!laneDriver || !driverId)
|
|
230
363
|
break;
|
|
231
|
-
const
|
|
232
|
-
if (!
|
|
364
|
+
const claimed = await claimNextAssignment(client, config, workspace, brief, driverId, processOnlineIds);
|
|
365
|
+
if (!claimed)
|
|
233
366
|
break;
|
|
367
|
+
const taskId = claimed.taskId;
|
|
368
|
+
driverId = claimed.driverId ?? driverId;
|
|
369
|
+
laneDriver = driverId ? DRIVERS[driverId] ?? laneDriver : laneDriver;
|
|
234
370
|
progressed = true;
|
|
235
371
|
console.log(`Executing ${taskId} via ${laneDriver.name}`);
|
|
236
372
|
const slot = runClaimedAssignment(client, config, laneDriver, workspace, brief, taskId, timeoutMs, supervision)
|
|
@@ -244,10 +380,17 @@ export async function pumpExecutionSlots(client, config, workspace, brief, timeo
|
|
|
244
380
|
return progressed;
|
|
245
381
|
}
|
|
246
382
|
/** Claim one assignment when under capacity; does not start the agent (multi-slot pump does). */
|
|
247
|
-
export async function claimNextAssignment(client, config, workspace, brief, driverId) {
|
|
383
|
+
export async function claimNextAssignment(client, config, workspace, brief, driverId, processOnlineIds) {
|
|
248
384
|
if (Object.keys(config.activeAttempts).length >= config.leaseCapacity)
|
|
249
385
|
return null;
|
|
250
386
|
const data = await client.request("/runner/v1/assignments");
|
|
387
|
+
const releasedWorktrees = releasedWorktreeAttemptIdsSchema.parse(data.released_worktree_attempt_ids ?? []);
|
|
388
|
+
for (const attemptId of releasedWorktrees) {
|
|
389
|
+
const worktree = attemptWorktreePath(workspace, attemptId);
|
|
390
|
+
await removeAttemptWorktree(workspace, worktree)
|
|
391
|
+
.then(() => console.log(`Released settled diagnostic worktree ${worktree}`))
|
|
392
|
+
.catch((error) => console.error(`Diagnostic worktree release failed: ${redactSecrets(error instanceof Error ? error.message : "unknown")}`));
|
|
393
|
+
}
|
|
251
394
|
const assignments = z.array(assignmentSchema).parse(data.assignments ?? []);
|
|
252
395
|
const assignment = assignments[0];
|
|
253
396
|
if (!assignment)
|
|
@@ -256,18 +399,25 @@ export async function claimNextAssignment(client, config, workspace, brief, driv
|
|
|
256
399
|
console.log(`Human takeover assignment ${assignment.id} — use Bridge MCP tools to claim and submit (agent runner skips).`);
|
|
257
400
|
return null;
|
|
258
401
|
}
|
|
402
|
+
let selectedDriverId = driverId ?? null;
|
|
403
|
+
if (assignment.execution_kind === "diagnosis" && selectedDriverId && !supportsReadOnlyDiagnosis(selectedDriverId)) {
|
|
404
|
+
// Prefer a lane that can honour repo_read + test_run without silently widening to repo_write.
|
|
405
|
+
// If none is online, claim on the selected lane so runClaimedAssignment can create a durable,
|
|
406
|
+
// owner-safe Hold and refund the diagnostic slot instead of leaving an expiring dispatch loop.
|
|
407
|
+
selectedDriverId = pickDriverForClaim(config, processOnlineIds, supportsReadOnlyDiagnosis) ?? selectedDriverId;
|
|
408
|
+
}
|
|
259
409
|
const liveBrief = await buildWorkspaceBrief(workspace).catch(() => brief);
|
|
260
410
|
const liveRepository = liveBrief?.repository ? normalizeRepositoryUrl(liveBrief.repository) : null;
|
|
261
411
|
let rejection = null;
|
|
262
412
|
if (assignment.repository_fingerprint && liveRepository !== assignment.repository_fingerprint)
|
|
263
413
|
rejection = "workspace_repository_mismatch";
|
|
264
|
-
else if (assignment.repository_fingerprint && assignment.claimed_head && liveBrief?.base_commit !== assignment.claimed_head) {
|
|
414
|
+
else if (assignment.execution_kind === "authoring" && assignment.repository_fingerprint && assignment.claimed_head && liveBrief?.base_commit !== assignment.claimed_head) {
|
|
265
415
|
// Rework / advanced base may claim a head that differs from the source checkout HEAD.
|
|
266
416
|
const reachable = await ensureCommitAvailable(workspace, assignment.claimed_head).catch(() => false);
|
|
267
417
|
if (!reachable)
|
|
268
418
|
rejection = "workspace_head_changed";
|
|
269
419
|
}
|
|
270
|
-
if (!rejection && assignment.requested_base_commit && assignment.claimed_head) {
|
|
420
|
+
if (!rejection && assignment.execution_kind === "authoring" && assignment.requested_base_commit && assignment.claimed_head) {
|
|
271
421
|
const startHead = await resolveAttemptStartCommit(workspace, assignment.requested_base_commit, assignment.claimed_head);
|
|
272
422
|
if (!startHead)
|
|
273
423
|
rejection = "base_not_ancestor";
|
|
@@ -280,17 +430,21 @@ export async function claimNextAssignment(client, config, workspace, brief, driv
|
|
|
280
430
|
console.error(`Assignment ${assignment.id} rejected before claim: ${rejection}`);
|
|
281
431
|
return null;
|
|
282
432
|
}
|
|
283
|
-
console.log(`Claiming assignment ${assignment.id} (attempt ${assignment.attempt_id})`);
|
|
284
|
-
await client.claim(assignment.id, assignment.attempt_id,
|
|
285
|
-
|
|
433
|
+
console.log(`Claiming ${assignment.execution_kind} assignment ${assignment.id} (attempt ${assignment.attempt_id})`);
|
|
434
|
+
await client.claim(assignment.id, assignment.attempt_id, {
|
|
435
|
+
...(selectedDriverId ? { driverId: selectedDriverId } : {}),
|
|
436
|
+
executionKind: assignment.execution_kind,
|
|
437
|
+
...(assignment.source_attempt_id ? { sourceAttemptId: assignment.source_attempt_id } : {}),
|
|
438
|
+
});
|
|
439
|
+
return { taskId: assignment.id, driverId: selectedDriverId };
|
|
286
440
|
}
|
|
287
441
|
export async function executeNextAssignment(client, config, driver, workspace, brief, timeoutMs, supervision) {
|
|
288
442
|
if (Object.keys(config.activeAttempts).length)
|
|
289
443
|
return recoverActiveAttempt(client, config, driver, workspace, brief, timeoutMs, supervision);
|
|
290
|
-
const
|
|
291
|
-
if (!
|
|
444
|
+
const claimed = await claimNextAssignment(client, config, workspace, brief);
|
|
445
|
+
if (!claimed)
|
|
292
446
|
return false;
|
|
293
|
-
await runClaimedAssignment(client, config, driver, workspace, brief, taskId, timeoutMs, supervision);
|
|
447
|
+
await runClaimedAssignment(client, config, driver, workspace, brief, claimed.taskId, timeoutMs, supervision);
|
|
294
448
|
return true;
|
|
295
449
|
}
|
|
296
450
|
async function runClaimedAssignment(client, config, driver, workspace, brief, taskId, timeoutMs, supervision, options = {}) {
|
|
@@ -299,12 +453,21 @@ async function runClaimedAssignment(client, config, driver, workspace, brief, ta
|
|
|
299
453
|
const task = taskDetailSchema.parse(detail.task);
|
|
300
454
|
const executionContract = executionContractSchema.parse(detail.execution_contract ?? {});
|
|
301
455
|
const workPackage = workPackageSchema.parse(detail.work_package) ?? null;
|
|
456
|
+
const executionKind = task.execution_kind;
|
|
457
|
+
const diagnosis = executionKind === "diagnosis";
|
|
458
|
+
const sourceAttemptId = task.source_attempt_id ?? executionContract.source_attempt_id ?? active.sourceAttemptId ?? null;
|
|
459
|
+
if (active.executionKind !== executionKind || active.sourceAttemptId !== (sourceAttemptId ?? undefined)) {
|
|
460
|
+
await client.updateAttempt(taskId, {
|
|
461
|
+
executionKind,
|
|
462
|
+
sourceAttemptId: sourceAttemptId ?? undefined,
|
|
463
|
+
});
|
|
464
|
+
}
|
|
302
465
|
const parsedSpec = parseTaskSpec(task.spec_json);
|
|
303
466
|
const deliverable = workPackage?.deliverable ?? parsedSpec.deliverable;
|
|
304
467
|
const spec = { ...parsedSpec, deliverable };
|
|
305
|
-
const artifactDelivery = deliverable === "artifact";
|
|
468
|
+
const artifactDelivery = !diagnosis && deliverable === "artifact";
|
|
306
469
|
const grants = z.array(z.string()).parse(task.grants_json ? JSON.parse(task.grants_json) : []);
|
|
307
|
-
const reworkFeedback = task.delivery_state === "changes_requested" ? changesRequestedFeedback(task.delivery_summary) : null;
|
|
470
|
+
const reworkFeedback = !diagnosis && task.delivery_state === "changes_requested" ? changesRequestedFeedback(task.delivery_summary) : null;
|
|
308
471
|
// Recompile current state at claim time — earlier packages may have moved the repo.
|
|
309
472
|
const liveBrief = await buildWorkspaceBrief(workspace).catch(() => brief);
|
|
310
473
|
const liveRepository = liveBrief?.repository ? normalizeRepositoryUrl(liveBrief.repository) : null;
|
|
@@ -313,37 +476,83 @@ async function runClaimedAssignment(client, config, driver, workspace, brief, ta
|
|
|
313
476
|
console.error(`Assignment ${taskId} preflight failed: Workspace repository changed after dispatch`);
|
|
314
477
|
return;
|
|
315
478
|
}
|
|
316
|
-
const startCommit =
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
479
|
+
const startCommit = executionContract.claimed_head ?? liveBrief?.base_commit ?? null;
|
|
480
|
+
let worktreeStart = startCommit ?? "retained-worktree";
|
|
481
|
+
let attemptWorkspace;
|
|
482
|
+
let deliverySubmitted = false;
|
|
483
|
+
// A diagnostic terminal defaults to retention. If its lease disappears, deleting the only copy
|
|
484
|
+
// of the failed code would make the durable retry mechanically impossible.
|
|
485
|
+
let retainAttemptWorktree = diagnosis;
|
|
486
|
+
if (diagnosis) {
|
|
487
|
+
if (!sourceAttemptId) {
|
|
488
|
+
await queueTerminal(client, taskId, {
|
|
489
|
+
action: "fail",
|
|
490
|
+
body: {
|
|
491
|
+
failure: {
|
|
492
|
+
code: "repair_diagnosis_source_missing",
|
|
493
|
+
class: "platform",
|
|
494
|
+
disposition: "stop",
|
|
495
|
+
responsible_party: "conduit",
|
|
496
|
+
message: "Conductor could not locate the failed run for read-only diagnosis.",
|
|
497
|
+
next_action: "No compiler constraints are needed from the owner; inspect Conduit repair history before retrying the task.",
|
|
498
|
+
diagnostic_detail: "Diagnostic assignment omitted source_attempt_id.",
|
|
499
|
+
},
|
|
500
|
+
error: "Diagnostic assignment omitted source_attempt_id",
|
|
501
|
+
retryable: false,
|
|
502
|
+
idempotency_key: `bridge:diagnosis-source-missing:${active.attemptId}`,
|
|
503
|
+
},
|
|
504
|
+
});
|
|
329
505
|
return;
|
|
330
506
|
}
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
507
|
+
const retained = options.existingWorktree ?? attemptWorktreePath(workspace, sourceAttemptId);
|
|
508
|
+
if (!(await proveResumeWorktree(workspace, sourceAttemptId, retained))) {
|
|
509
|
+
await queueTerminal(client, taskId, {
|
|
510
|
+
action: "fail",
|
|
511
|
+
body: {
|
|
512
|
+
failure: {
|
|
513
|
+
code: "repair_diagnosis_worktree_missing",
|
|
514
|
+
class: "platform",
|
|
515
|
+
disposition: "stop",
|
|
516
|
+
responsible_party: "conduit",
|
|
517
|
+
message: "Conductor could not access the retained failed run for read-only diagnosis.",
|
|
518
|
+
next_action: "No compiler constraints are needed from the owner; inspect Conduit repair history before retrying the task.",
|
|
519
|
+
diagnostic_detail: `Retained worktree for source attempt ${sourceAttemptId} is unavailable.`,
|
|
520
|
+
},
|
|
521
|
+
error: "Retained failed worktree is unavailable for read-only diagnosis",
|
|
522
|
+
retryable: false,
|
|
523
|
+
idempotency_key: `bridge:diagnosis-worktree-missing:${active.attemptId}`,
|
|
524
|
+
},
|
|
525
|
+
});
|
|
338
526
|
return;
|
|
339
527
|
}
|
|
528
|
+
attemptWorkspace = retained;
|
|
529
|
+
worktreeStart = executionContract.claimed_head ?? "retained-worktree";
|
|
530
|
+
await client.updateAttempt(taskId, { worktreePath: attemptWorkspace, executionKind, sourceAttemptId });
|
|
340
531
|
}
|
|
341
|
-
|
|
342
|
-
let deliverySubmitted = false;
|
|
343
|
-
if (options.existingWorktree) {
|
|
532
|
+
else if (options.existingWorktree) {
|
|
344
533
|
attemptWorkspace = options.existingWorktree;
|
|
345
534
|
}
|
|
346
535
|
else {
|
|
536
|
+
if (!startCommit) {
|
|
537
|
+
await queueTerminal(client, taskId, { action: "fail", body: { error: "No start commit for attempt worktree", retryable: true, idempotency_key: `bridge:no-start-commit:${active.attemptId}` } });
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
if (executionContract.requested_base_commit) {
|
|
541
|
+
const resolved = await resolveAttemptStartCommit(workspace, executionContract.requested_base_commit, startCommit);
|
|
542
|
+
if (!resolved) {
|
|
543
|
+
await queueTerminal(client, taskId, { action: "fail", body: { error: "Required base commit is not available in this workspace", retryable: true, idempotency_key: `bridge:base-not-ancestor:${active.attemptId}` } });
|
|
544
|
+
return;
|
|
545
|
+
}
|
|
546
|
+
worktreeStart = resolved;
|
|
547
|
+
}
|
|
548
|
+
else if (liveBrief?.base_commit && liveBrief.base_commit !== startCommit) {
|
|
549
|
+
const reachable = await ensureCommitAvailable(workspace, startCommit).catch(() => false);
|
|
550
|
+
if (!reachable) {
|
|
551
|
+
await queueTerminal(client, taskId, { action: "fail", body: { error: "Delivered head is not available in this workspace", retryable: true, idempotency_key: `bridge:workspace-mismatch:${active.attemptId}` } });
|
|
552
|
+
console.error(`Assignment ${taskId} preflight failed: Delivered head is not available in this workspace`);
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
}
|
|
347
556
|
try {
|
|
348
557
|
attemptWorkspace = await createAttemptWorktree({
|
|
349
558
|
sourceWorkspace: workspace,
|
|
@@ -362,6 +571,16 @@ async function runClaimedAssignment(client, config, driver, workspace, brief, ta
|
|
|
362
571
|
}
|
|
363
572
|
await client.updateAttempt(taskId, { worktreePath: attemptWorkspace });
|
|
364
573
|
}
|
|
574
|
+
// Diagnosis reads manifests and verification commands from the failed tree itself. The source
|
|
575
|
+
// checkout may be clean but stale relative to edits that caused the failure.
|
|
576
|
+
const attemptBrief = diagnosis
|
|
577
|
+
? await buildWorkspaceBrief(attemptWorkspace).catch(() => liveBrief)
|
|
578
|
+
: liveBrief;
|
|
579
|
+
const removeReleasedDiagnosticWorktree = async (response) => {
|
|
580
|
+
if (diagnosis && !retainDiagnosticWorktree(response)) {
|
|
581
|
+
await removeAttemptWorktree(workspace, attemptWorkspace).catch(() => undefined);
|
|
582
|
+
}
|
|
583
|
+
};
|
|
365
584
|
// Stamped class is the source of truth (Bridge 0.11+). Do not re-derive from grants.
|
|
366
585
|
let executionClass;
|
|
367
586
|
try {
|
|
@@ -372,10 +591,11 @@ async function runClaimedAssignment(client, config, driver, workspace, brief, ta
|
|
|
372
591
|
console.error(`Assignment ${taskId}: ${detail}`);
|
|
373
592
|
// Non-retryable: control plane restamps on dispatch/claim. Infinite retry here burned path-4
|
|
374
593
|
// attempt budget when a pre-inversion task row still lacked the stamp.
|
|
375
|
-
await queueTerminal(client, taskId, {
|
|
594
|
+
const response = await queueTerminal(client, taskId, {
|
|
376
595
|
action: "fail",
|
|
377
596
|
body: { error: detail, retryable: false, idempotency_key: `bridge:fail:${active.attemptId}:missing-class` },
|
|
378
597
|
});
|
|
598
|
+
await removeReleasedDiagnosticWorktree(response);
|
|
379
599
|
return;
|
|
380
600
|
}
|
|
381
601
|
// An artifact deliverable that is not publish_artifact cannot write/publish — fail closed.
|
|
@@ -393,19 +613,68 @@ async function runClaimedAssignment(client, config, driver, workspace, brief, ta
|
|
|
393
613
|
if (!floor.ok) {
|
|
394
614
|
const detail = `Class floor unmet: ${floor.reason}`;
|
|
395
615
|
console.error(`Assignment ${taskId}: ${detail}`);
|
|
396
|
-
await queueTerminal(client, taskId, {
|
|
616
|
+
const response = await queueTerminal(client, taskId, {
|
|
397
617
|
action: "fail",
|
|
398
618
|
body: { error: detail, retryable: false, idempotency_key: `bridge:fail:${active.attemptId}:class-floor` },
|
|
399
619
|
});
|
|
620
|
+
await removeReleasedDiagnosticWorktree(response);
|
|
621
|
+
return;
|
|
622
|
+
}
|
|
623
|
+
const builtInDriverId = Object.entries(DRIVERS).find(([, candidate]) => candidate === driver)?.[0] ?? null;
|
|
624
|
+
if (diagnosis && builtInDriverId && !supportsReadOnlyDiagnosis(builtInDriverId)) {
|
|
625
|
+
const detail = `${driver.name} cannot combine bounded test execution with enforced read-only repository access.`;
|
|
626
|
+
const response = await queueTerminal(client, taskId, {
|
|
627
|
+
action: "fail",
|
|
628
|
+
body: {
|
|
629
|
+
failure: {
|
|
630
|
+
code: "repair_diagnosis_driver_read_only_unsupported",
|
|
631
|
+
class: "environment",
|
|
632
|
+
disposition: "hold",
|
|
633
|
+
responsible_party: "computer_operator",
|
|
634
|
+
message: "Conductor is waiting for a diagnostic lane that can enforce read-only access.",
|
|
635
|
+
next_action: "Bring a Claude Code, Codex, or Cursor lane online on this computer, then Recheck. Do not author compiler constraints for the agent.",
|
|
636
|
+
diagnostic_detail: detail,
|
|
637
|
+
},
|
|
638
|
+
error: detail,
|
|
639
|
+
retryable: false,
|
|
640
|
+
idempotency_key: `bridge:diagnosis-driver-authority:${active.attemptId}`,
|
|
641
|
+
},
|
|
642
|
+
});
|
|
643
|
+
await removeReleasedDiagnosticWorktree(response);
|
|
644
|
+
console.error(`Assignment ${taskId} diagnosis held: ${detail}`);
|
|
645
|
+
return;
|
|
646
|
+
}
|
|
647
|
+
const diagnosticFailure = workPackage?.diagnostic_context?.failure_output;
|
|
648
|
+
if (diagnosis && !diagnosticFailure) {
|
|
649
|
+
const response = await queueTerminal(client, taskId, {
|
|
650
|
+
action: "fail",
|
|
651
|
+
body: {
|
|
652
|
+
error: "Diagnostic assignment is missing witnessed failure output",
|
|
653
|
+
retryable: true,
|
|
654
|
+
idempotency_key: `bridge:diagnosis-context-missing:${active.attemptId}`,
|
|
655
|
+
},
|
|
656
|
+
});
|
|
657
|
+
await removeReleasedDiagnosticWorktree(response);
|
|
400
658
|
return;
|
|
401
659
|
}
|
|
402
|
-
const prompt =
|
|
660
|
+
const prompt = diagnosis
|
|
661
|
+
? buildDiagnosticPrompt({
|
|
662
|
+
taskId,
|
|
663
|
+
objective: task.objective,
|
|
664
|
+
spec,
|
|
665
|
+
workPackage,
|
|
666
|
+
failureOutput: diagnosticFailure,
|
|
667
|
+
verificationCommands: attemptBrief?.verification ?? [],
|
|
668
|
+
})
|
|
669
|
+
: buildAssignmentPrompt({ taskId, objective: task.objective, spec, grants, workspace: attemptWorkspace, currentHead: worktreeStart, reworkFeedback, workPackage, verificationCommands: attemptBrief?.verification ?? [], executionClass });
|
|
403
670
|
const resuming = Boolean(options.forceResumeSessionId);
|
|
404
671
|
await client.attemptRequest(taskId, "progress", {
|
|
405
|
-
phase: "changing",
|
|
672
|
+
phase: diagnosis ? "inspecting" : "changing",
|
|
406
673
|
message: resuming
|
|
407
674
|
? `Resuming ${driver.name} after Bridge restart with proven session and worktree.`
|
|
408
|
-
:
|
|
675
|
+
: diagnosis
|
|
676
|
+
? `Starting ${driver.name} read-only diagnosis in the retained failed worktree.`
|
|
677
|
+
: `Starting ${driver.name} for this assignment${reworkFeedback ? " with review feedback" : ""}.`,
|
|
409
678
|
idempotency_key: resuming ? `bridge:progress:${active.attemptId}:resume` : `bridge:progress:${active.attemptId}:start`,
|
|
410
679
|
});
|
|
411
680
|
const driverId = active.driverId
|
|
@@ -433,7 +702,14 @@ async function runClaimedAssignment(client, config, driver, workspace, brief, ta
|
|
|
433
702
|
: `Started ${driver.name}${selection.model ? ` on ${selection.model}` : " on its CLI default"}.`,
|
|
434
703
|
idempotency_key: resuming ? `bridge:progress:${active.attemptId}:agent-resume` : `bridge:progress:${active.attemptId}:agent-start`,
|
|
435
704
|
});
|
|
436
|
-
await client.updateAttempt(taskId, {
|
|
705
|
+
await client.updateAttempt(taskId, {
|
|
706
|
+
phase: "agent_running",
|
|
707
|
+
fuelMode: fuelSource,
|
|
708
|
+
worktreePath: attemptWorkspace,
|
|
709
|
+
driverId,
|
|
710
|
+
executionKind,
|
|
711
|
+
sourceAttemptId: sourceAttemptId ?? undefined,
|
|
712
|
+
});
|
|
437
713
|
const renewTimer = setInterval(() => { void renewLeases(client, config).catch((error) => console.error(`Lease renewal failed: ${redactSecrets(error instanceof Error ? error.message : "unknown")}`)); }, 60_000);
|
|
438
714
|
let heartbeatRunning = false;
|
|
439
715
|
const heartbeatTimer = supervision ? setInterval(() => {
|
|
@@ -455,9 +731,9 @@ async function runClaimedAssignment(client, config, driver, workspace, brief, ta
|
|
|
455
731
|
workspace: attemptWorkspace,
|
|
456
732
|
grants,
|
|
457
733
|
capabilities: spec.required_capabilities ?? [],
|
|
458
|
-
verificationCommands:
|
|
734
|
+
verificationCommands: attemptBrief?.verification ?? [],
|
|
459
735
|
deliverable,
|
|
460
|
-
workRole: workPackage?.work_role ?? spec.work_role,
|
|
736
|
+
workRole: diagnosis ? "diagnose" : workPackage?.work_role ?? spec.work_role,
|
|
461
737
|
executionClass,
|
|
462
738
|
resumeSessionId,
|
|
463
739
|
timeoutMs,
|
|
@@ -467,9 +743,73 @@ async function runClaimedAssignment(client, config, driver, workspace, brief, ta
|
|
|
467
743
|
});
|
|
468
744
|
if (result.sessionId)
|
|
469
745
|
config.sessions = { ...config.sessions, [taskId]: result.sessionId };
|
|
746
|
+
if (diagnosis) {
|
|
747
|
+
if (result.status === "failed") {
|
|
748
|
+
const message = result.error ?? "Read-only diagnosis failed";
|
|
749
|
+
const response = await queueTerminal(client, taskId, {
|
|
750
|
+
action: "fail",
|
|
751
|
+
body: {
|
|
752
|
+
error: message,
|
|
753
|
+
retryable: retryableAgentFailure(message),
|
|
754
|
+
idempotency_key: `bridge:diagnosis-fail:${active.attemptId}`,
|
|
755
|
+
},
|
|
756
|
+
});
|
|
757
|
+
retainAttemptWorktree = retainDiagnosticWorktree(response);
|
|
758
|
+
console.error(`Assignment ${taskId} diagnosis failed: ${redactSecrets(message)}`);
|
|
759
|
+
return;
|
|
760
|
+
}
|
|
761
|
+
let repairBrief;
|
|
762
|
+
try {
|
|
763
|
+
repairBrief = parseDiagnosticRepairBrief(result.resultText ?? "");
|
|
764
|
+
}
|
|
765
|
+
catch (error) {
|
|
766
|
+
const message = error instanceof Error ? error.message : "Diagnostic RepairBrief was invalid";
|
|
767
|
+
const response = await queueTerminal(client, taskId, {
|
|
768
|
+
action: "fail",
|
|
769
|
+
body: {
|
|
770
|
+
error: message,
|
|
771
|
+
retryable: true,
|
|
772
|
+
idempotency_key: `bridge:diagnosis-invalid:${active.attemptId}`,
|
|
773
|
+
},
|
|
774
|
+
});
|
|
775
|
+
retainAttemptWorktree = retainDiagnosticWorktree(response);
|
|
776
|
+
console.error(`Assignment ${taskId} diagnosis returned no usable brief: ${redactSecrets(message)}`);
|
|
777
|
+
return;
|
|
778
|
+
}
|
|
779
|
+
const response = await queueTerminal(client, taskId, {
|
|
780
|
+
action: "complete",
|
|
781
|
+
body: {
|
|
782
|
+
repair_brief: repairBrief,
|
|
783
|
+
idempotency_key: `bridge:diagnosis-complete:${active.attemptId}`,
|
|
784
|
+
},
|
|
785
|
+
});
|
|
786
|
+
retainAttemptWorktree = retainDiagnosticWorktree(response);
|
|
787
|
+
console.log(`Assignment ${taskId} returned a grounded Conductor RepairBrief.`);
|
|
788
|
+
return;
|
|
789
|
+
}
|
|
470
790
|
if (result.status === "failed") {
|
|
471
|
-
const
|
|
472
|
-
|
|
791
|
+
const agentMessage = result.error ?? "Agent execution failed";
|
|
792
|
+
// The agent verifies inside its own tool loop, so its compiler/test output never reaches
|
|
793
|
+
// result.error — production saw a bare git SHA and echoed source arrive as the "failure
|
|
794
|
+
// reason", leaving Conductor nothing to diagnose. Re-run the project's bounded verification
|
|
795
|
+
// in the attempt worktree to recover the real errors. Best-effort: keep the agent's own
|
|
796
|
+
// message when there is no command, the command cannot run, or the tree actually verifies.
|
|
797
|
+
const verificationDetail = grants.includes("test_run")
|
|
798
|
+
? await captureVerificationFailure({
|
|
799
|
+
workspace: attemptWorkspace,
|
|
800
|
+
verificationCommands: attemptBrief?.verification ?? [],
|
|
801
|
+
})
|
|
802
|
+
: null;
|
|
803
|
+
const message = verificationDetail
|
|
804
|
+
? `Verification failed after the agent run.\n${verificationDetail}`
|
|
805
|
+
: agentMessage;
|
|
806
|
+
// Retryability stays keyed to what the agent reported; a recovered verification log describes
|
|
807
|
+
// the same run and must not silently reclassify an unretryable failure.
|
|
808
|
+
// Keep the tree until the replay-safe terminal response explicitly says whether diagnosis was
|
|
809
|
+
// queued. A network fault here must not delete the evidence before Bridge can replay terminal.
|
|
810
|
+
retainAttemptWorktree = true;
|
|
811
|
+
const response = await queueTerminal(client, taskId, { action: "fail", body: { error: message, retryable: retryableAgentFailure(agentMessage), idempotency_key: `bridge:fail:${active.attemptId}` } });
|
|
812
|
+
retainAttemptWorktree = response.retain_worktree === true;
|
|
473
813
|
console.error(`Assignment ${taskId} failed: ${redactSecrets(message)}`);
|
|
474
814
|
return;
|
|
475
815
|
}
|
|
@@ -569,16 +909,25 @@ async function runClaimedAssignment(client, config, driver, workspace, brief, ta
|
|
|
569
909
|
report,
|
|
570
910
|
spec,
|
|
571
911
|
grants,
|
|
572
|
-
verificationCommands:
|
|
912
|
+
verificationCommands: attemptBrief?.verification ?? [],
|
|
573
913
|
});
|
|
574
914
|
validateDeliveryReport(report, spec, grants);
|
|
575
915
|
}
|
|
576
916
|
catch (error) {
|
|
577
917
|
const message = error instanceof Error ? error.message : "Agent delivery report was invalid";
|
|
578
|
-
//
|
|
579
|
-
//
|
|
580
|
-
|
|
581
|
-
|
|
918
|
+
// Bridge's own bounded verification can reject an otherwise completed agent run here. That is
|
|
919
|
+
// the same verify-class pre-delivery failure as the driver-failed path above: preserve the
|
|
920
|
+
// exact tree and send the witness marker that arms Conductor diagnosis. Treating it as a
|
|
921
|
+
// delivery-envelope defect would stop before Invariant 23 ever ran.
|
|
922
|
+
const verificationFailed = /^Agent report: Verification failed \(/.test(message);
|
|
923
|
+
const classified = verificationFailed
|
|
924
|
+
? { retryable: true, error: `Verification failed after the agent run.\n${message}` }
|
|
925
|
+
// classifyFinalizeFailure: forge transport → retryable; contract/environment/unknown → fail
|
|
926
|
+
// closed (unknown prefixed as "Bridge finalize interrupted", not laundered as contract).
|
|
927
|
+
: classifyFinalizeFailure(message);
|
|
928
|
+
if (verificationFailed)
|
|
929
|
+
retainAttemptWorktree = true;
|
|
930
|
+
const response = await queueTerminal(client, taskId, {
|
|
582
931
|
action: "fail",
|
|
583
932
|
body: {
|
|
584
933
|
error: classified.error,
|
|
@@ -586,6 +935,8 @@ async function runClaimedAssignment(client, config, driver, workspace, brief, ta
|
|
|
586
935
|
idempotency_key: `bridge:invalid-delivery:${active.attemptId}`,
|
|
587
936
|
},
|
|
588
937
|
});
|
|
938
|
+
if (verificationFailed)
|
|
939
|
+
retainAttemptWorktree = response.retain_worktree === true;
|
|
589
940
|
console.error(`Assignment ${taskId} could not produce a valid Delivery: ${redactSecrets(classified.error)}`);
|
|
590
941
|
const replyTail = reportText.slice(-8_000);
|
|
591
942
|
console.error(`Assignment ${taskId} agent reply tail (${reportText.length} chars total, redacted): ${redactSecrets(replyTail) || "<empty>"}`);
|
|
@@ -600,7 +951,10 @@ async function runClaimedAssignment(client, config, driver, workspace, brief, ta
|
|
|
600
951
|
clearInterval(renewTimer);
|
|
601
952
|
if (heartbeatTimer)
|
|
602
953
|
clearInterval(heartbeatTimer);
|
|
603
|
-
if (
|
|
954
|
+
if (retainAttemptWorktree) {
|
|
955
|
+
console.log(`Retained failed-attempt worktree for Conductor diagnosis at ${attemptWorkspace}`);
|
|
956
|
+
}
|
|
957
|
+
else if (artifactDelivery && !deliverySubmitted) {
|
|
604
958
|
console.error(`Artifact delivery was not submitted; retained generated files at ${attemptWorkspace}`);
|
|
605
959
|
}
|
|
606
960
|
else {
|
|
@@ -869,22 +1223,28 @@ function pathMatchesScope(path, scope) {
|
|
|
869
1223
|
}
|
|
870
1224
|
return path === normalized;
|
|
871
1225
|
}
|
|
1226
|
+
function retainDiagnosticWorktree(response) {
|
|
1227
|
+
// Lease expiry is reconciled by the CP into another diagnostic attempt. The old terminal cannot
|
|
1228
|
+
// receive that decision, so fail safe by retaining the only copy of the failed code.
|
|
1229
|
+
return response.retain_worktree === true || response.status === "invalid_lease";
|
|
1230
|
+
}
|
|
872
1231
|
async function queueTerminal(client, taskId, terminal) {
|
|
873
1232
|
await client.updateAttempt(taskId, { phase: "terminal_pending", terminal });
|
|
874
|
-
|
|
1233
|
+
return flushTerminal(client, taskId);
|
|
875
1234
|
}
|
|
876
1235
|
export async function flushTerminal(client, taskId) {
|
|
877
1236
|
const active = client.attempt(taskId);
|
|
878
1237
|
if (!active.terminal)
|
|
879
1238
|
throw new Error("Pending terminal operation is missing its replay payload");
|
|
880
1239
|
try {
|
|
881
|
-
await client.attemptRequest(taskId, active.terminal.action, active.terminal.body);
|
|
1240
|
+
const response = await client.attemptRequest(taskId, active.terminal.action, active.terminal.body);
|
|
882
1241
|
await client.clearAttempt(taskId);
|
|
1242
|
+
return response;
|
|
883
1243
|
}
|
|
884
1244
|
catch (error) {
|
|
885
1245
|
if (error instanceof ConduitRequestError && error.code === "invalid_lease") {
|
|
886
1246
|
await client.clearAttempt(taskId);
|
|
887
|
-
return;
|
|
1247
|
+
return { accepted: false, status: "invalid_lease" };
|
|
888
1248
|
}
|
|
889
1249
|
throw error;
|
|
890
1250
|
}
|
package/dist/preflight.js
CHANGED
|
@@ -3,7 +3,8 @@ import { promisify } from "node:util";
|
|
|
3
3
|
import { buildWorkspaceBrief, normalizeRepositoryUrl } from "./brief.js";
|
|
4
4
|
import { hasAntigravityLogin, hasClaudeLogin, hasOpenAiLogin, hasOpenCodeLogin, resolveCodexExecutable, } from "./driver.js";
|
|
5
5
|
import { localFuelOnlyDriver, onlineDriverIds, resolveDriverFuel } from "./drivers.js";
|
|
6
|
-
|
|
6
|
+
/** Protocol 3: read-only diagnosis can reuse a retained failed-attempt worktree. */
|
|
7
|
+
export const BRIDGE_PROTOCOL_VERSION = 3;
|
|
7
8
|
const execFileAsync = promisify(execFile);
|
|
8
9
|
async function defaultCommandRunner(command, args, cwd) {
|
|
9
10
|
try {
|
|
@@ -110,7 +111,7 @@ export async function cachedBridgePreflight(input) {
|
|
|
110
111
|
cached = { key, at: Date.now(), report };
|
|
111
112
|
return report;
|
|
112
113
|
}
|
|
113
|
-
/** Heartbeat-only / missing workspace still reports protocol
|
|
114
|
+
/** Heartbeat-only / missing workspace still reports the current protocol preflight (not ready). */
|
|
114
115
|
export function unavailableWorkspacePreflight(input) {
|
|
115
116
|
const online = input.processOnlineIds?.length ? input.processOnlineIds : onlineDriverIds(input.config);
|
|
116
117
|
const issues = [];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@miraland-labs/conduit-bridge",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"description": "Conduit Bridge CLI — join, connect, disconnect, multi-driver lanes, and run Claude Code / Codex / Cursor / OpenCode / Pi / Kiro / Antigravity agents for a Conduit organization",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|