@miraland-labs/conduit-bridge 0.16.24 → 0.16.25

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/client.js CHANGED
@@ -80,6 +80,30 @@ export class ConduitClient {
80
80
  const active = this.attempt(taskId);
81
81
  return this.request(`/runner/v1/tasks/${taskId}/${action}`, { method: "POST", body: JSON.stringify({ ...body, attempt_id: active.attemptId, lease_token: active.leaseToken }) });
82
82
  }
83
+ async listInstructions(taskId, after = "") {
84
+ const active = this.attempt(taskId);
85
+ const query = new URLSearchParams({ attempt_id: active.attemptId, ...(after ? { after } : {}) });
86
+ const data = await this.request(`/runner/v1/tasks/${taskId}/instructions?${query}`);
87
+ return Array.isArray(data.instructions)
88
+ ? data.instructions.flatMap((row) => {
89
+ if (!row || typeof row !== "object")
90
+ return [];
91
+ const instruction = row;
92
+ return typeof instruction.id === "string"
93
+ && typeof instruction.payload_json === "string"
94
+ && typeof instruction.created_at === "string"
95
+ ? [{ id: instruction.id, payload_json: instruction.payload_json, created_at: instruction.created_at }]
96
+ : [];
97
+ })
98
+ : [];
99
+ }
100
+ async acknowledgeInstruction(taskId, instructionId) {
101
+ const active = this.attempt(taskId);
102
+ await this.attemptRequest(taskId, `instructions/${instructionId}/ack`, {
103
+ instruction_id: instructionId,
104
+ idempotency_key: `bridge:instruction-ack:${active.attemptId}:${instructionId}`,
105
+ });
106
+ }
83
107
  async updateAttempt(taskId, patch) {
84
108
  const active = this.attempt(taskId);
85
109
  for (const key of Object.keys(patch)) {
package/dist/execution.js CHANGED
@@ -411,6 +411,79 @@ export async function renewLeases(client, config) {
411
411
  }
412
412
  }
413
413
  }
414
+ /**
415
+ * Poll while the headless CLI owns the worktree, then hand owner guidance to its next session turn.
416
+ * Driver stdin is intentionally closed after the initial prompt, so pretending a mid-process write
417
+ * is portable would silently lose guidance on most lanes. The first safe common checkpoint is the
418
+ * driver's completed turn, before Bridge validates or submits its delivery.
419
+ */
420
+ function pollCheckpointInstructions(client, taskId, state, intervalMs = 15_000) {
421
+ let stopped = false;
422
+ let inFlight = null;
423
+ const pending = new Map();
424
+ const poll = async () => {
425
+ const rows = await client.listInstructions(taskId, state.cursor);
426
+ for (const row of rows) {
427
+ state.cursor = `${row.created_at}|${row.id}`;
428
+ if (state.seen.has(row.id))
429
+ continue;
430
+ state.seen.add(row.id);
431
+ const parsed = parseCheckpointInstruction(row);
432
+ if (parsed && state.acceptedCount < 10 && state.acceptedCharacters + parsed.message.length <= 40_000) {
433
+ pending.set(parsed.id, parsed);
434
+ state.acceptedCount += 1;
435
+ state.acceptedCharacters += parsed.message.length;
436
+ }
437
+ else if (parsed) {
438
+ console.error(`Instruction ${parsed.id} exceeded the checkpoint guidance bound and was not acknowledged.`);
439
+ }
440
+ }
441
+ };
442
+ const runPoll = () => {
443
+ if (stopped || inFlight)
444
+ return;
445
+ inFlight = poll()
446
+ .catch((error) => console.error(`Instruction poll failed: ${redactSecrets(error instanceof Error ? error.message : "unknown")}`))
447
+ .finally(() => { inFlight = null; });
448
+ };
449
+ runPoll();
450
+ const timer = setInterval(runPoll, intervalMs);
451
+ timer.unref?.();
452
+ return {
453
+ stop: async () => {
454
+ stopped = true;
455
+ clearInterval(timer);
456
+ await inFlight;
457
+ await poll().catch((error) => console.error(`Final instruction poll failed: ${redactSecrets(error instanceof Error ? error.message : "unknown")}`));
458
+ return [...pending.values()];
459
+ },
460
+ };
461
+ }
462
+ function parseCheckpointInstruction(row) {
463
+ try {
464
+ const payload = JSON.parse(row.payload_json);
465
+ if (payload.changes_scope === true) {
466
+ console.error(`Refusing scope-changing instruction ${row.id}; revise the approved initiative contract instead.`);
467
+ return null;
468
+ }
469
+ const message = typeof payload.message === "string" ? payload.message.trim() : "";
470
+ return message ? { id: row.id, message: message.slice(0, 20_000), createdAt: row.created_at } : null;
471
+ }
472
+ catch {
473
+ return null;
474
+ }
475
+ }
476
+ function checkpointInstructionPrompt(instructions) {
477
+ return [
478
+ "OWNER INSTRUCTIONS AT CHECKPOINT",
479
+ "These messages refine the current run only. They do not amend approved scope, boundaries, acceptance criteria, or grants.",
480
+ ...instructions.map((instruction) => `- ${instruction.message}`),
481
+ "",
482
+ "Apply the guidance within the approved contract, re-check the resulting work, then return a complete final report.",
483
+ "Return only one fenced ```json object with exactly this shape:",
484
+ agentReportTemplate,
485
+ ].join("\n");
486
+ }
414
487
  function resolveAttemptDriver(config, active, fallback) {
415
488
  if (active.driverId && DRIVERS[active.driverId])
416
489
  return DRIVERS[active.driverId];
@@ -983,7 +1056,7 @@ async function runClaimedAssignment(client, config, driver, workspace, brief, ta
983
1056
  // turn already "finished" makes the agent reply conversationally without the report block.
984
1057
  const resumeSessionId = options.forceResumeSessionId
985
1058
  ?? (reworkFeedback && task.repair_mode !== "briefed" ? config.sessions?.[taskId] : undefined);
986
- const result = await driver.run({
1059
+ const runInput = {
987
1060
  prompt,
988
1061
  workspace: attemptWorkspace,
989
1062
  grants,
@@ -997,7 +1070,40 @@ async function runClaimedAssignment(client, config, driver, workspace, brief, ta
997
1070
  model: selection.model,
998
1071
  fuel,
999
1072
  fuelSource,
1000
- });
1073
+ };
1074
+ let result;
1075
+ let currentRunInput = runInput;
1076
+ const instructionPollState = {
1077
+ cursor: "",
1078
+ seen: new Set(),
1079
+ acceptedCount: 0,
1080
+ acceptedCharacters: 0,
1081
+ };
1082
+ let instructionsInPrompt = [];
1083
+ for (let round = 0;; round += 1) {
1084
+ const instructionPoll = diagnosis ? null : pollCheckpointInstructions(client, taskId, instructionPollState);
1085
+ let polled = [];
1086
+ try {
1087
+ result = await driver.run(currentRunInput);
1088
+ }
1089
+ finally {
1090
+ polled = await instructionPoll?.stop() ?? polled;
1091
+ }
1092
+ if (instructionsInPrompt.length > 0) {
1093
+ await Promise.all(instructionsInPrompt.map((instruction) => client.acknowledgeInstruction(taskId, instruction.id).catch((error) => console.error(`Instruction acknowledgement failed: ${redactSecrets(error instanceof Error ? error.message : "unknown")}`))));
1094
+ }
1095
+ // One initial turn plus at most two bounded checkpoint turns. Guidance arriving after that
1096
+ // remains unacknowledged rather than creating an unbounded owner-driven agent loop.
1097
+ if (diagnosis || result.status !== "completed" || polled.length === 0 || round >= 2)
1098
+ break;
1099
+ instructionsInPrompt = polled;
1100
+ const followup = checkpointInstructionPrompt(instructionsInPrompt);
1101
+ currentRunInput = {
1102
+ ...runInput,
1103
+ prompt: result.sessionId ? followup : `${prompt}\n\n${followup}`,
1104
+ resumeSessionId: result.sessionId ?? undefined,
1105
+ };
1106
+ }
1001
1107
  if (result.sessionId)
1002
1108
  config.sessions = { ...config.sessions, [taskId]: result.sessionId };
1003
1109
  await learnDriverFuel(config, driver.name, fuelSource, result);
@@ -1,5 +1,5 @@
1
1
  import { spawnSync } from "node:child_process";
2
- import { existsSync } from "node:fs";
2
+ import { existsSync, readFileSync } from "node:fs";
3
3
  import { join } from "node:path";
4
4
  function defaultRunner(command, args, workspace) {
5
5
  const result = spawnSync(command, args, {
@@ -10,17 +10,64 @@ function defaultRunner(command, args, workspace) {
10
10
  });
11
11
  return { status: result.status, ...(result.error ? { error: result.error } : {}) };
12
12
  }
13
- /** Install locked JavaScript dependencies before a Conduit-managed checkout advertises readiness. */
13
+ /** Install dependencies with the repository's own committed package-manager identity. */
14
14
  export function bootstrapManagedWorkspace(workspace, run = defaultRunner) {
15
- if (!existsSync(join(workspace, "package.json")))
15
+ const packagePath = join(workspace, "package.json");
16
+ if (!existsSync(packagePath))
16
17
  return "not_applicable";
17
- if (!existsSync(join(workspace, "package-lock.json"))) {
18
- throw new Error("managed_workspace_bootstrap_package_lock_required");
19
- }
20
- const result = run("npm", ["ci", "--include=optional", "--no-audit", "--no-fund"], workspace);
18
+ const install = lockedInstall(workspace, packagePath);
19
+ const result = run(install.command, install.args, workspace);
21
20
  if (result.error)
22
- throw new Error(`managed_workspace_bootstrap_failed:${result.error.message}`);
21
+ throw new Error(`managed_workspace_bootstrap_failed:${install.id}:${result.error.message}`);
23
22
  if (result.status !== 0)
24
- throw new Error(`managed_workspace_bootstrap_failed:exit_${result.status ?? "unknown"}`);
23
+ throw new Error(`managed_workspace_bootstrap_failed:${install.id}:exit_${result.status ?? "unknown"}`);
25
24
  return "installed";
26
25
  }
26
+ function lockedInstall(workspace, packagePath) {
27
+ const declared = declaredPackageManager(packagePath);
28
+ const managers = [
29
+ existsSync(join(workspace, "package-lock.json")) || existsSync(join(workspace, "npm-shrinkwrap.json")) ? "npm" : null,
30
+ existsSync(join(workspace, "pnpm-lock.yaml")) ? "pnpm" : null,
31
+ existsSync(join(workspace, "yarn.lock")) ? "yarn" : null,
32
+ existsSync(join(workspace, "bun.lock")) || existsSync(join(workspace, "bun.lockb")) ? "bun" : null,
33
+ ].filter((manager) => manager !== null);
34
+ if (managers.length === 0)
35
+ throw new Error("managed_workspace_bootstrap_lockfile_required");
36
+ if (declared && !managers.includes(declared)) {
37
+ throw new Error(`managed_workspace_bootstrap_package_manager_lockfile_mismatch:${declared}:${managers.join(",")}`);
38
+ }
39
+ const selected = declared ?? (managers.length === 1 ? managers[0] : null);
40
+ if (!selected)
41
+ throw new Error(`managed_workspace_bootstrap_lockfile_ambiguous:${managers.join(",")}`);
42
+ if (selected === "npm")
43
+ return { id: selected, command: "npm", args: ["ci", "--include=optional", "--no-audit", "--no-fund"] };
44
+ if (selected === "pnpm")
45
+ return { id: selected, command: "pnpm", args: ["install", "--frozen-lockfile"] };
46
+ if (selected === "bun")
47
+ return { id: selected, command: "bun", args: ["install", "--frozen-lockfile"] };
48
+ const modernYarn = existsSync(join(workspace, ".yarnrc.yml")) || declaredPackageManagerMajor(packagePath, "yarn") >= 2;
49
+ return { id: selected, command: "yarn", args: ["install", modernYarn ? "--immutable" : "--frozen-lockfile"] };
50
+ }
51
+ function declaredPackageManager(packagePath) {
52
+ try {
53
+ const parsed = JSON.parse(readFileSync(packagePath, "utf8"));
54
+ const match = typeof parsed.packageManager === "string" ? /^(npm|pnpm|yarn|bun)@/.exec(parsed.packageManager.trim()) : null;
55
+ const id = match?.[1];
56
+ return id === "npm" || id === "pnpm" || id === "yarn" || id === "bun" ? id : null;
57
+ }
58
+ catch {
59
+ return null;
60
+ }
61
+ }
62
+ function declaredPackageManagerMajor(packagePath, manager) {
63
+ try {
64
+ const parsed = JSON.parse(readFileSync(packagePath, "utf8"));
65
+ const match = typeof parsed.packageManager === "string"
66
+ ? new RegExp(`^${manager}@(\\d+)`).exec(parsed.packageManager.trim())
67
+ : null;
68
+ return match ? Number(match[1]) : 0;
69
+ }
70
+ catch {
71
+ return 0;
72
+ }
73
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@miraland-labs/conduit-bridge",
3
- "version": "0.16.24",
3
+ "version": "0.16.25",
4
4
  "description": "Conduit Bridge CLI — join, connect, disconnect, multi-driver lanes, and run Claude Code / Codex / Cursor / OpenCode / Pi / Kiro / Antigravity / Grok Build agents for a Conduit organization",
5
5
  "type": "module",
6
6
  "bin": {