@miraland-labs/conduit-bridge 0.16.3 → 0.16.4
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/execution.js +275 -99
- package/dist/land-git-reality.js +108 -0
- package/package.json +1 -1
package/dist/execution.js
CHANGED
|
@@ -12,7 +12,8 @@ import { promisify } from "node:util";
|
|
|
12
12
|
import { buildWorkspaceBrief, ensureCommitAvailable, normalizeRepositoryUrl, resolveAttemptStartCommit } from "./brief.js";
|
|
13
13
|
import { ensureDeliveryPullRequest } from "./ensure-pull-request.js";
|
|
14
14
|
import { ensureLandCommit } from "./ensure-land-commit.js";
|
|
15
|
-
import { AgentNoLandCommitError, requiresLandCommit } from "./land-contract.js";
|
|
15
|
+
import { AgentNoLandCommitError, agentNoLandCommitMessage, requiresLandCommit } from "./land-contract.js";
|
|
16
|
+
import { agentClaimsRepositoryWork, claimsVsGitMismatch, isLandTreeEmpty, reportClaimsRepositoryWork, } from "./land-git-reality.js";
|
|
16
17
|
const execFileAsync = promisify(execFile);
|
|
17
18
|
import { captureVerificationFailure, ensureTestEvidence } from "./ensure-test-evidence.js";
|
|
18
19
|
/** Keep a Mac awake only while an assignment is active; display sleep remains allowed. */
|
|
@@ -871,105 +872,251 @@ async function runClaimedAssignment(client, config, driver, workspace, brief, ta
|
|
|
871
872
|
}
|
|
872
873
|
}
|
|
873
874
|
let agentSessionId = result.sessionId ?? undefined;
|
|
874
|
-
let report;
|
|
875
875
|
let reportText = result.resultText ?? "";
|
|
876
|
+
let landContinuationUsed = false;
|
|
877
|
+
if (await claimsVsGitMismatch({
|
|
878
|
+
workspace: attemptWorkspace,
|
|
879
|
+
spec,
|
|
880
|
+
grants,
|
|
881
|
+
agentReply: reportText,
|
|
882
|
+
})) {
|
|
883
|
+
const continuation = await runLandContinuationTurn({
|
|
884
|
+
client,
|
|
885
|
+
taskId,
|
|
886
|
+
attemptId: active.attemptId,
|
|
887
|
+
workspace: attemptWorkspace,
|
|
888
|
+
spec,
|
|
889
|
+
grants,
|
|
890
|
+
driver,
|
|
891
|
+
capabilities: spec.required_capabilities ?? [],
|
|
892
|
+
verificationCommands: attemptBrief?.verification ?? [],
|
|
893
|
+
objective: task.objective,
|
|
894
|
+
selection,
|
|
895
|
+
fuel,
|
|
896
|
+
fuelSource,
|
|
897
|
+
timeoutMs,
|
|
898
|
+
resumeSessionId: agentSessionId,
|
|
899
|
+
reason: "Agent claimed repository work but git shows none on the attempt branch; starting one land-only continuation turn instead of report-only repair.",
|
|
900
|
+
idempotencyKey: `bridge:progress:${active.attemptId}:claims-git-gate`,
|
|
901
|
+
});
|
|
902
|
+
landContinuationUsed = true;
|
|
903
|
+
if (continuation.sessionId) {
|
|
904
|
+
config.sessions = { ...config.sessions, [taskId]: continuation.sessionId };
|
|
905
|
+
agentSessionId = continuation.sessionId;
|
|
906
|
+
}
|
|
907
|
+
reportText = continuation.resultText;
|
|
908
|
+
}
|
|
909
|
+
let report;
|
|
876
910
|
try {
|
|
877
911
|
report = parseAgentReport(reportText, spec.acceptance ?? []);
|
|
878
912
|
}
|
|
879
913
|
catch (error) {
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
for (let repairTurn = 1; repairTurn <= maxRepairs; repairTurn++) {
|
|
889
|
-
const replyDigest = createHash("sha256").update(previousReply).digest("hex");
|
|
890
|
-
await client.attemptRequest(taskId, "progress", {
|
|
891
|
-
phase: "preparing_delivery",
|
|
892
|
-
message: `Delivery envelope invalid (${redactSecrets(parseError)}); starting report-only repair turn ${repairTurn}/${maxRepairs} without rerunning implementation. Original reply: ${previousReply.length} characters, sha256:${replyDigest}.`,
|
|
893
|
-
idempotency_key: `bridge:progress:${active.attemptId}:delivery-repair:${repairTurn}`,
|
|
894
|
-
});
|
|
895
|
-
try {
|
|
896
|
-
repaired = await driver.run({
|
|
897
|
-
prompt: buildDeliveryRepairPrompt(parseError, previousReply, spec.acceptance ?? []),
|
|
914
|
+
const gitEmpty = await isLandTreeEmpty({ workspace: attemptWorkspace, spec, grants });
|
|
915
|
+
const claimsWork = agentClaimsRepositoryWork(reportText);
|
|
916
|
+
if (requiresLandCommit({ grants, spec }) && gitEmpty && claimsWork) {
|
|
917
|
+
if (!landContinuationUsed) {
|
|
918
|
+
const continuation = await runLandContinuationTurn({
|
|
919
|
+
client,
|
|
920
|
+
taskId,
|
|
921
|
+
attemptId: active.attemptId,
|
|
898
922
|
workspace: attemptWorkspace,
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
902
|
-
capabilities: [],
|
|
903
|
-
verificationCommands: [],
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
resumeSessionId,
|
|
907
|
-
timeoutMs,
|
|
908
|
-
model: selection.model,
|
|
923
|
+
spec,
|
|
924
|
+
grants,
|
|
925
|
+
driver,
|
|
926
|
+
capabilities: spec.required_capabilities ?? [],
|
|
927
|
+
verificationCommands: attemptBrief?.verification ?? [],
|
|
928
|
+
objective: task.objective,
|
|
929
|
+
selection,
|
|
909
930
|
fuel,
|
|
910
931
|
fuelSource,
|
|
932
|
+
timeoutMs,
|
|
933
|
+
resumeSessionId: agentSessionId,
|
|
934
|
+
reason: "Agent claimed repository work but git shows none; starting land-only continuation instead of read-only report repair.",
|
|
935
|
+
idempotencyKey: `bridge:progress:${active.attemptId}:claims-git-gate-parse`,
|
|
911
936
|
});
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
923
|
-
|
|
924
|
-
if (repaired.status === "failed") {
|
|
925
|
-
const message = repaired.error ?? "Delivery report repair failed";
|
|
926
|
-
await queueTerminal(client, taskId, { action: "fail", body: { error: message, retryable: retryableAgentFailure(message), idempotency_key: `bridge:delivery-repair-failed:${active.attemptId}` } });
|
|
927
|
-
console.error(`Assignment ${taskId} report-only repair failed: ${redactSecrets(message)}`);
|
|
928
|
-
return;
|
|
929
|
-
}
|
|
930
|
-
previousReply = repaired.resultText ?? "";
|
|
931
|
-
try {
|
|
932
|
-
parsed = parseAgentReport(previousReply, spec.acceptance ?? []);
|
|
933
|
-
break;
|
|
934
|
-
}
|
|
935
|
-
catch (repairError) {
|
|
936
|
-
parseError = repairError instanceof Error ? repairError.message : "Repaired delivery report was invalid";
|
|
937
|
-
if (repairTurn >= maxRepairs) {
|
|
938
|
-
const detail = `Delivery report repair exhausted: ${parseError}`;
|
|
939
|
-
// Implementation finished; only its response envelope is invalid. Preserve the exact
|
|
940
|
-
// tree and identify this narrow condition so the control plane can prepare repair
|
|
941
|
-
// guidance instead of discarding the work and asking the owner to debug JSON.
|
|
942
|
-
retainAttemptWorktree = true;
|
|
937
|
+
landContinuationUsed = true;
|
|
938
|
+
if (continuation.sessionId) {
|
|
939
|
+
config.sessions = { ...config.sessions, [taskId]: continuation.sessionId };
|
|
940
|
+
agentSessionId = continuation.sessionId;
|
|
941
|
+
}
|
|
942
|
+
reportText = continuation.resultText;
|
|
943
|
+
try {
|
|
944
|
+
report = parseAgentReport(reportText, spec.acceptance ?? []);
|
|
945
|
+
}
|
|
946
|
+
catch {
|
|
947
|
+
const base = spec.repository?.base_commit ?? "unknown";
|
|
948
|
+
const classified = classifyFinalizeFailure(agentNoLandCommitMessage(base));
|
|
943
949
|
const response = await queueTerminal(client, taskId, {
|
|
944
950
|
action: "fail",
|
|
945
951
|
body: {
|
|
946
|
-
error:
|
|
947
|
-
retryable:
|
|
948
|
-
|
|
949
|
-
code: "delivery_report_invalid",
|
|
950
|
-
class: "contract",
|
|
951
|
-
disposition: "rework",
|
|
952
|
-
responsible_party: "conduit",
|
|
953
|
-
message: "Conduit could not prepare a valid delivery report from the completed agent run.",
|
|
954
|
-
next_action: "Conductor will prepare bounded recovery guidance. You do not need to edit technical constraints.",
|
|
955
|
-
diagnostic_detail: detail,
|
|
956
|
-
},
|
|
957
|
-
idempotency_key: `bridge:delivery-repair-invalid:${active.attemptId}`,
|
|
952
|
+
error: classified.error,
|
|
953
|
+
retryable: classified.retryable,
|
|
954
|
+
idempotency_key: `bridge:claims-git-gate:${active.attemptId}`,
|
|
958
955
|
},
|
|
959
956
|
});
|
|
960
957
|
retainAttemptWorktree = retainDiagnosticWorktree(response);
|
|
961
|
-
console.error(`Assignment ${taskId}
|
|
962
|
-
|
|
963
|
-
|
|
958
|
+
console.error(`Assignment ${taskId} claims-vs-git gate failed after land continuation: ${redactSecrets(classified.error)}`);
|
|
959
|
+
return;
|
|
960
|
+
}
|
|
961
|
+
}
|
|
962
|
+
else {
|
|
963
|
+
const base = spec.repository?.base_commit ?? "unknown";
|
|
964
|
+
const classified = classifyFinalizeFailure(agentNoLandCommitMessage(base));
|
|
965
|
+
const response = await queueTerminal(client, taskId, {
|
|
966
|
+
action: "fail",
|
|
967
|
+
body: {
|
|
968
|
+
error: classified.error,
|
|
969
|
+
retryable: classified.retryable,
|
|
970
|
+
idempotency_key: `bridge:claims-git-gate:${active.attemptId}`,
|
|
971
|
+
},
|
|
972
|
+
});
|
|
973
|
+
retainAttemptWorktree = retainDiagnosticWorktree(response);
|
|
974
|
+
console.error(`Assignment ${taskId} claims-vs-git gate blocked read-only report repair: ${redactSecrets(classified.error)}`);
|
|
975
|
+
return;
|
|
976
|
+
}
|
|
977
|
+
}
|
|
978
|
+
else {
|
|
979
|
+
// Pump fuel keeps one repair turn. Local-fuel Experimental lanes (Pi et al.) get a second
|
|
980
|
+
// read-only repair — truncated JSON fences are common there and re-implementing is wasteful.
|
|
981
|
+
const maxRepairs = fuelSource === "local" ? 2 : 1;
|
|
982
|
+
let parseError = error instanceof Error ? error.message : "Agent delivery report was invalid";
|
|
983
|
+
let previousReply = reportText;
|
|
984
|
+
let resumeSessionId = result.sessionId ?? undefined;
|
|
985
|
+
let repaired = null;
|
|
986
|
+
let parsed = null;
|
|
987
|
+
for (let repairTurn = 1; repairTurn <= maxRepairs; repairTurn++) {
|
|
988
|
+
const replyDigest = createHash("sha256").update(previousReply).digest("hex");
|
|
989
|
+
await client.attemptRequest(taskId, "progress", {
|
|
990
|
+
phase: "preparing_delivery",
|
|
991
|
+
message: `Delivery envelope invalid (${redactSecrets(parseError)}); starting report-only repair turn ${repairTurn}/${maxRepairs} without rerunning implementation. Original reply: ${previousReply.length} characters, sha256:${replyDigest}.`,
|
|
992
|
+
idempotency_key: `bridge:progress:${active.attemptId}:delivery-repair:${repairTurn}`,
|
|
993
|
+
});
|
|
994
|
+
try {
|
|
995
|
+
repaired = await driver.run({
|
|
996
|
+
prompt: buildDeliveryRepairPrompt(parseError, previousReply, spec.acceptance ?? []),
|
|
997
|
+
workspace: attemptWorkspace,
|
|
998
|
+
// Preserve only existing read authority. No write, test, branch, or push
|
|
999
|
+
// capability is available while the agent repairs the response envelope.
|
|
1000
|
+
grants: grants.filter((grant) => grant === "repo_read"),
|
|
1001
|
+
capabilities: [],
|
|
1002
|
+
verificationCommands: [],
|
|
1003
|
+
// Envelope repair is read-only observe authority regardless of the original class.
|
|
1004
|
+
executionClass: "observe",
|
|
1005
|
+
resumeSessionId,
|
|
1006
|
+
timeoutMs,
|
|
1007
|
+
model: selection.model,
|
|
1008
|
+
fuel,
|
|
1009
|
+
fuelSource,
|
|
1010
|
+
});
|
|
1011
|
+
}
|
|
1012
|
+
catch (repairError) {
|
|
1013
|
+
const message = repairError instanceof Error ? repairError.message : "Delivery report repair failed";
|
|
1014
|
+
await queueTerminal(client, taskId, { action: "fail", body: { error: `Delivery report repair failed: ${message}`, retryable: retryableAgentFailure(message), idempotency_key: `bridge:delivery-repair-failed:${active.attemptId}` } });
|
|
1015
|
+
console.error(`Assignment ${taskId} report-only repair failed: ${redactSecrets(message)}`);
|
|
1016
|
+
return;
|
|
1017
|
+
}
|
|
1018
|
+
if (repaired.sessionId) {
|
|
1019
|
+
config.sessions = { ...config.sessions, [taskId]: repaired.sessionId };
|
|
1020
|
+
resumeSessionId = repaired.sessionId;
|
|
1021
|
+
agentSessionId = repaired.sessionId;
|
|
1022
|
+
}
|
|
1023
|
+
if (repaired.status === "failed") {
|
|
1024
|
+
const message = repaired.error ?? "Delivery report repair failed";
|
|
1025
|
+
await queueTerminal(client, taskId, { action: "fail", body: { error: message, retryable: retryableAgentFailure(message), idempotency_key: `bridge:delivery-repair-failed:${active.attemptId}` } });
|
|
1026
|
+
console.error(`Assignment ${taskId} report-only repair failed: ${redactSecrets(message)}`);
|
|
964
1027
|
return;
|
|
965
1028
|
}
|
|
966
|
-
|
|
1029
|
+
previousReply = repaired.resultText ?? "";
|
|
1030
|
+
try {
|
|
1031
|
+
parsed = parseAgentReport(previousReply, spec.acceptance ?? []);
|
|
1032
|
+
break;
|
|
1033
|
+
}
|
|
1034
|
+
catch (repairError) {
|
|
1035
|
+
parseError = repairError instanceof Error ? repairError.message : "Repaired delivery report was invalid";
|
|
1036
|
+
if (repairTurn >= maxRepairs) {
|
|
1037
|
+
const detail = `Delivery report repair exhausted: ${parseError}`;
|
|
1038
|
+
// Implementation finished; only its response envelope is invalid. Preserve the exact
|
|
1039
|
+
// tree and identify this narrow condition so the control plane can prepare repair
|
|
1040
|
+
// guidance instead of discarding the work and asking the owner to debug JSON.
|
|
1041
|
+
retainAttemptWorktree = true;
|
|
1042
|
+
const response = await queueTerminal(client, taskId, {
|
|
1043
|
+
action: "fail",
|
|
1044
|
+
body: {
|
|
1045
|
+
error: detail,
|
|
1046
|
+
retryable: false,
|
|
1047
|
+
failure: {
|
|
1048
|
+
code: "delivery_report_invalid",
|
|
1049
|
+
class: "contract",
|
|
1050
|
+
disposition: "rework",
|
|
1051
|
+
responsible_party: "conduit",
|
|
1052
|
+
message: "Conduit could not prepare a valid delivery report from the completed agent run.",
|
|
1053
|
+
next_action: "Conductor will prepare bounded recovery guidance. You do not need to edit technical constraints.",
|
|
1054
|
+
diagnostic_detail: detail,
|
|
1055
|
+
},
|
|
1056
|
+
idempotency_key: `bridge:delivery-repair-invalid:${active.attemptId}`,
|
|
1057
|
+
},
|
|
1058
|
+
});
|
|
1059
|
+
retainAttemptWorktree = retainDiagnosticWorktree(response);
|
|
1060
|
+
console.error(`Assignment ${taskId} exhausted its report-only repair: ${redactSecrets(parseError)}`);
|
|
1061
|
+
const replyTail = previousReply.slice(-8_000);
|
|
1062
|
+
console.error(`Assignment ${taskId} repaired reply tail (${previousReply.length} chars total, redacted): ${redactSecrets(replyTail) || "<empty>"}`);
|
|
1063
|
+
return;
|
|
1064
|
+
}
|
|
1065
|
+
console.error(`Assignment ${taskId} report-only repair turn ${repairTurn} still invalid: ${redactSecrets(parseError)}; retrying`);
|
|
1066
|
+
}
|
|
967
1067
|
}
|
|
1068
|
+
if (!parsed)
|
|
1069
|
+
return;
|
|
1070
|
+
reportText = previousReply;
|
|
1071
|
+
report = parsed;
|
|
968
1072
|
}
|
|
969
|
-
|
|
1073
|
+
}
|
|
1074
|
+
if (!landContinuationUsed
|
|
1075
|
+
&& requiresLandCommit({ grants, spec })
|
|
1076
|
+
&& reportClaimsRepositoryWork(report)
|
|
1077
|
+
&& await isLandTreeEmpty({ workspace: attemptWorkspace, spec, grants })) {
|
|
1078
|
+
const continuation = await runLandContinuationTurn({
|
|
1079
|
+
client,
|
|
1080
|
+
taskId,
|
|
1081
|
+
attemptId: active.attemptId,
|
|
1082
|
+
workspace: attemptWorkspace,
|
|
1083
|
+
spec,
|
|
1084
|
+
grants,
|
|
1085
|
+
driver,
|
|
1086
|
+
capabilities: spec.required_capabilities ?? [],
|
|
1087
|
+
verificationCommands: attemptBrief?.verification ?? [],
|
|
1088
|
+
objective: task.objective,
|
|
1089
|
+
selection,
|
|
1090
|
+
fuel,
|
|
1091
|
+
fuelSource,
|
|
1092
|
+
timeoutMs,
|
|
1093
|
+
resumeSessionId: agentSessionId,
|
|
1094
|
+
reason: "Parsed Delivery claims repository work but git shows none; starting land-only continuation.",
|
|
1095
|
+
idempotencyKey: `bridge:progress:${active.attemptId}:claims-git-gate-report`,
|
|
1096
|
+
});
|
|
1097
|
+
landContinuationUsed = true;
|
|
1098
|
+
if (continuation.sessionId) {
|
|
1099
|
+
config.sessions = { ...config.sessions, [taskId]: continuation.sessionId };
|
|
1100
|
+
agentSessionId = continuation.sessionId;
|
|
1101
|
+
}
|
|
1102
|
+
reportText = continuation.resultText;
|
|
1103
|
+
try {
|
|
1104
|
+
report = parseAgentReport(reportText, spec.acceptance ?? []);
|
|
1105
|
+
}
|
|
1106
|
+
catch {
|
|
1107
|
+
const base = spec.repository?.base_commit ?? "unknown";
|
|
1108
|
+
const classified = classifyFinalizeFailure(agentNoLandCommitMessage(base));
|
|
1109
|
+
await queueTerminal(client, taskId, {
|
|
1110
|
+
action: "fail",
|
|
1111
|
+
body: {
|
|
1112
|
+
error: classified.error,
|
|
1113
|
+
retryable: classified.retryable,
|
|
1114
|
+
idempotency_key: `bridge:claims-git-gate-report:${active.attemptId}`,
|
|
1115
|
+
},
|
|
1116
|
+
});
|
|
1117
|
+
console.error(`Assignment ${taskId} claims-vs-git gate failed after parsed-report continuation: ${redactSecrets(classified.error)}`);
|
|
970
1118
|
return;
|
|
971
|
-
|
|
972
|
-
report = parsed;
|
|
1119
|
+
}
|
|
973
1120
|
}
|
|
974
1121
|
try {
|
|
975
1122
|
report = await finalizeRepositoryLand({
|
|
@@ -990,6 +1137,7 @@ async function runClaimedAssignment(client, config, driver, workspace, brief, ta
|
|
|
990
1137
|
fuelSource,
|
|
991
1138
|
timeoutMs,
|
|
992
1139
|
resumeSessionId: agentSessionId,
|
|
1140
|
+
landContinuationUsed,
|
|
993
1141
|
});
|
|
994
1142
|
// Mechanical land path: agent may forget gh; Bridge pushes and the control plane opens the PR.
|
|
995
1143
|
report = await ensureDeliveryPullRequest({
|
|
@@ -1100,6 +1248,37 @@ function buildLandContinuationPrompt(input) {
|
|
|
1100
1248
|
function bindLandHeadCommit(report, headCommit) {
|
|
1101
1249
|
return { ...report, head_commit: headCommit };
|
|
1102
1250
|
}
|
|
1251
|
+
async function runLandContinuationTurn(input) {
|
|
1252
|
+
await input.client.attemptRequest(input.taskId, "progress", {
|
|
1253
|
+
phase: "changing",
|
|
1254
|
+
message: input.reason,
|
|
1255
|
+
idempotency_key: input.idempotencyKey,
|
|
1256
|
+
});
|
|
1257
|
+
const continuation = await input.driver.run({
|
|
1258
|
+
prompt: buildLandContinuationPrompt({
|
|
1259
|
+
objective: input.objective,
|
|
1260
|
+
spec: input.spec,
|
|
1261
|
+
acceptance: input.spec.acceptance ?? [],
|
|
1262
|
+
}),
|
|
1263
|
+
workspace: input.workspace,
|
|
1264
|
+
grants: input.grants,
|
|
1265
|
+
capabilities: input.capabilities,
|
|
1266
|
+
verificationCommands: input.verificationCommands,
|
|
1267
|
+
executionClass: "mutate_repo",
|
|
1268
|
+
resumeSessionId: input.resumeSessionId ?? undefined,
|
|
1269
|
+
timeoutMs: input.timeoutMs,
|
|
1270
|
+
model: input.selection.model ?? undefined,
|
|
1271
|
+
fuel: input.fuel,
|
|
1272
|
+
fuelSource: input.fuelSource,
|
|
1273
|
+
});
|
|
1274
|
+
if (continuation.status === "failed") {
|
|
1275
|
+
throw new AgentNoLandCommitError(input.spec.repository?.base_commit ?? "unknown");
|
|
1276
|
+
}
|
|
1277
|
+
return {
|
|
1278
|
+
resultText: continuation.resultText ?? "",
|
|
1279
|
+
sessionId: continuation.sessionId ?? undefined,
|
|
1280
|
+
};
|
|
1281
|
+
}
|
|
1103
1282
|
async function finalizeRepositoryLand(input) {
|
|
1104
1283
|
if (!requiresLandCommit({ grants: input.grants, spec: input.spec }))
|
|
1105
1284
|
return input.report;
|
|
@@ -1128,34 +1307,31 @@ async function finalizeRepositoryLand(input) {
|
|
|
1128
1307
|
if (!(error instanceof AgentNoLandCommitError))
|
|
1129
1308
|
throw error;
|
|
1130
1309
|
}
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
objective: input.objective,
|
|
1139
|
-
spec: input.spec,
|
|
1140
|
-
acceptance: input.spec.acceptance ?? [],
|
|
1141
|
-
}),
|
|
1310
|
+
if (input.landContinuationUsed) {
|
|
1311
|
+
throw new AgentNoLandCommitError(input.spec.repository?.base_commit ?? "unknown");
|
|
1312
|
+
}
|
|
1313
|
+
const continuation = await runLandContinuationTurn({
|
|
1314
|
+
client: input.client,
|
|
1315
|
+
taskId: input.taskId,
|
|
1316
|
+
attemptId: input.attemptId,
|
|
1142
1317
|
workspace: input.workspace,
|
|
1318
|
+
spec: input.spec,
|
|
1143
1319
|
grants: input.grants,
|
|
1320
|
+
driver: input.driver,
|
|
1144
1321
|
capabilities: input.capabilities,
|
|
1145
1322
|
verificationCommands: input.verificationCommands,
|
|
1146
|
-
|
|
1147
|
-
|
|
1148
|
-
timeoutMs: input.timeoutMs,
|
|
1149
|
-
model: input.selection.model ?? undefined,
|
|
1323
|
+
objective: input.objective,
|
|
1324
|
+
selection: input.selection,
|
|
1150
1325
|
fuel: input.fuel,
|
|
1151
1326
|
fuelSource: input.fuelSource,
|
|
1327
|
+
timeoutMs: input.timeoutMs,
|
|
1328
|
+
resumeSessionId: input.resumeSessionId,
|
|
1329
|
+
reason: "Agent finished without landing repository changes; starting one land-only continuation turn.",
|
|
1330
|
+
idempotencyKey: `bridge:progress:${input.attemptId}:land-continuation`,
|
|
1152
1331
|
});
|
|
1153
|
-
if (continuation.status === "failed") {
|
|
1154
|
-
throw new AgentNoLandCommitError(input.spec.repository?.base_commit ?? "unknown");
|
|
1155
|
-
}
|
|
1156
1332
|
let report = input.report;
|
|
1157
1333
|
try {
|
|
1158
|
-
report = parseAgentReport(continuation.resultText
|
|
1334
|
+
report = parseAgentReport(continuation.resultText, input.spec.acceptance ?? []);
|
|
1159
1335
|
}
|
|
1160
1336
|
catch {
|
|
1161
1337
|
// Land may have succeeded even when the continuation envelope is malformed — git is authoritative.
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Claims vs git — detect when an agent's Delivery narrative claims repository work
|
|
3
|
+
* that git cannot see on the attempt branch.
|
|
4
|
+
*/
|
|
5
|
+
import { execFile } from "node:child_process";
|
|
6
|
+
import { promisify } from "node:util";
|
|
7
|
+
import { commitsAheadOfBase } from "./ensure-pull-request.js";
|
|
8
|
+
import { extractAgentReportJsonText, parseJsonObjectCandidate } from "./driver.js";
|
|
9
|
+
import { filterPathsInScope, requiresLandCommit, } from "./land-contract.js";
|
|
10
|
+
const execFileAsync = promisify(execFile);
|
|
11
|
+
const NO_CHANGE_OUTCOME = /no code changes|no changes were made|working tree clean|did not (?:make|land)|nothing to (?:change|deliver)|no commit was made/i;
|
|
12
|
+
async function listUncommittedPaths(workspace) {
|
|
13
|
+
const { stdout } = await execFileAsync("git", ["-C", workspace, "status", "--porcelain"], {
|
|
14
|
+
timeout: 30_000,
|
|
15
|
+
maxBuffer: 8_000_000,
|
|
16
|
+
});
|
|
17
|
+
const paths = [];
|
|
18
|
+
for (const line of stdout.split("\n")) {
|
|
19
|
+
if (!line.trim())
|
|
20
|
+
continue;
|
|
21
|
+
const entry = line.slice(3).trim();
|
|
22
|
+
if (!entry)
|
|
23
|
+
continue;
|
|
24
|
+
const path = entry.includes(" -> ")
|
|
25
|
+
? entry.slice(entry.lastIndexOf(" -> ") + 4).trim()
|
|
26
|
+
: entry;
|
|
27
|
+
if (path)
|
|
28
|
+
paths.push(path.replace(/^"|"$/g, ""));
|
|
29
|
+
}
|
|
30
|
+
return paths;
|
|
31
|
+
}
|
|
32
|
+
/** True when the land contract applies and git shows no commit and no scoped dirty files. */
|
|
33
|
+
export async function isLandTreeEmpty(input) {
|
|
34
|
+
if (!requiresLandCommit({ grants: input.grants, spec: input.spec }))
|
|
35
|
+
return false;
|
|
36
|
+
const base = input.spec.repository?.base_commit;
|
|
37
|
+
if (!base)
|
|
38
|
+
return false;
|
|
39
|
+
const countAhead = input.countCommitsAhead ?? commitsAheadOfBase;
|
|
40
|
+
const ahead = await countAhead(input.workspace, base);
|
|
41
|
+
if (ahead !== null && ahead > 0)
|
|
42
|
+
return false;
|
|
43
|
+
const pending = filterPathsInScope(await (input.listUncommittedPaths ?? listUncommittedPaths)(input.workspace), input.spec.change_scope ?? []);
|
|
44
|
+
return pending.length === 0;
|
|
45
|
+
}
|
|
46
|
+
function deliveryObjectClaimsWork(raw) {
|
|
47
|
+
if (Array.isArray(raw.changes) && raw.changes.some((entry) => typeof entry === "string" && entry.trim())) {
|
|
48
|
+
return true;
|
|
49
|
+
}
|
|
50
|
+
if (typeof raw.head_commit === "string" && raw.head_commit.trim())
|
|
51
|
+
return true;
|
|
52
|
+
if (Array.isArray(raw.evidence) && raw.evidence.some((entry) => {
|
|
53
|
+
return entry && typeof entry === "object" && entry.kind === "change";
|
|
54
|
+
})) {
|
|
55
|
+
return true;
|
|
56
|
+
}
|
|
57
|
+
if (Array.isArray(raw.acceptance_results) && raw.acceptance_results.some((entry) => {
|
|
58
|
+
return entry && typeof entry === "object" && entry.status === "met";
|
|
59
|
+
})) {
|
|
60
|
+
return true;
|
|
61
|
+
}
|
|
62
|
+
const outcome = typeof raw.outcome === "string" ? raw.outcome : "";
|
|
63
|
+
if (outcome && NO_CHANGE_OUTCOME.test(outcome))
|
|
64
|
+
return false;
|
|
65
|
+
if (outcome.trim()) {
|
|
66
|
+
if (/\b(rewrote|replaced|implemented|added|modified|removed|commit(?:ted)?|fetcher|\.rs\b|\.ts\b)\b/i.test(outcome)) {
|
|
67
|
+
return true;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
/** Best-effort read of the agent reply — used before strict Delivery validation. */
|
|
73
|
+
export function agentClaimsRepositoryWork(text) {
|
|
74
|
+
if (!text.trim())
|
|
75
|
+
return false;
|
|
76
|
+
try {
|
|
77
|
+
const raw = parseJsonObjectCandidate(extractAgentReportJsonText(text));
|
|
78
|
+
if (raw && typeof raw === "object" && !Array.isArray(raw)) {
|
|
79
|
+
return deliveryObjectClaimsWork(raw);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
catch {
|
|
83
|
+
// Fall through — malformed JSON may still prose-claim work in the tail.
|
|
84
|
+
}
|
|
85
|
+
if (NO_CHANGE_OUTCOME.test(text))
|
|
86
|
+
return false;
|
|
87
|
+
return /\b(rewrote|replaced|implemented|fetcher\.rs|head_commit|"changes"\s*:\s*\[)/i.test(text);
|
|
88
|
+
}
|
|
89
|
+
/** Parsed Delivery claims repository work (stricter than free text). */
|
|
90
|
+
export function reportClaimsRepositoryWork(report) {
|
|
91
|
+
if (report.changes.some((entry) => entry.trim()))
|
|
92
|
+
return true;
|
|
93
|
+
if (report.head_commit?.trim())
|
|
94
|
+
return true;
|
|
95
|
+
if (report.evidence.some((entry) => entry.kind === "change"))
|
|
96
|
+
return true;
|
|
97
|
+
if (report.acceptance_results.some((entry) => entry.status === "met"))
|
|
98
|
+
return true;
|
|
99
|
+
if (report.outcome && NO_CHANGE_OUTCOME.test(report.outcome))
|
|
100
|
+
return false;
|
|
101
|
+
return false;
|
|
102
|
+
}
|
|
103
|
+
/** Hard gate: land is required, git is empty, and the agent narrative claims work landed. */
|
|
104
|
+
export async function claimsVsGitMismatch(input) {
|
|
105
|
+
if (!agentClaimsRepositoryWork(input.agentReply))
|
|
106
|
+
return false;
|
|
107
|
+
return isLandTreeEmpty(input);
|
|
108
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@miraland-labs/conduit-bridge",
|
|
3
|
-
"version": "0.16.
|
|
3
|
+
"version": "0.16.4",
|
|
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": {
|