@miraland-labs/conduit-bridge 0.16.41 → 0.16.42
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/driver.js +81 -8
- package/dist/ensure-test-evidence.js +4 -1
- package/dist/execution.js +8 -5
- package/package.json +1 -1
package/dist/driver.js
CHANGED
|
@@ -28,7 +28,7 @@ function deliveryLanguageRule(language) {
|
|
|
28
28
|
return null;
|
|
29
29
|
}
|
|
30
30
|
export const evidenceKinds = ["change", "test", "preview", "research", "documentation"];
|
|
31
|
-
export const agentReportTemplate = '{"outcome": "one-paragraph summary", "changes": ["path from the repository root — what changed"], "verification": ["command — result"], "acceptance_results": [{"criterion": "
|
|
31
|
+
export const agentReportTemplate = '{"outcome": "one-paragraph summary", "changes": ["path from the repository root — what changed"], "verification": ["command — result"], "acceptance_results": [{"criterion": "criterion key, for example c1", "status": "met|not_met|unknown", "unverified_reason": "omit unless status is unknown", "unverified_detail": "one sentence naming what was missing; omit unless status is unknown"}], "evidence": [{"kind": "change|test|preview|research|documentation", "name": "concise evidence name", "uri": "external URL if one exists", "digest": "optional digest", "details": ["observable result"], "acceptance_criteria": ["criterion keys this evidence supports, for example c1"]}], "assumptions": [], "risks": [], "limitations": [], "head_commit": "full sha of your final commit, omit if none"}';
|
|
32
32
|
/**
|
|
33
33
|
* Resolve an operator's ordered tier candidates against the live model list.
|
|
34
34
|
* Picks the first safe candidate the CLI currently offers; without a live list
|
|
@@ -97,8 +97,12 @@ export function buildAssignmentPrompt(context) {
|
|
|
97
97
|
lines.push("", `SCOPE\n${scope.map((item) => `- ${item}`).join("\n")}`);
|
|
98
98
|
if (boundaries?.length)
|
|
99
99
|
lines.push("", `BOUNDARIES — never violate these\n${boundaries.map((item) => `- ${item}`).join("\n")}`);
|
|
100
|
-
if (acceptance?.length)
|
|
101
|
-
|
|
100
|
+
if (acceptance?.length) {
|
|
101
|
+
// Each criterion carries a key. The agent reports the key, not the prose: criteria are long
|
|
102
|
+
// sentences with backticks and quotes, and a retyped sentence made the delivery invalid at
|
|
103
|
+
// "Preparing delivery" although the work was done.
|
|
104
|
+
lines.push("", "ACCEPTANCE CRITERIA — the delivery is judged against these. The key in brackets identifies each one.", ...acceptanceCriteriaLines(acceptance), "", "Name each criterion by its key — \"c1\", not the sentence — in acceptance_results[].criterion and in evidence[].acceptance_criteria. You do not need to retype the criterion text; a retyped sentence that differs makes the whole delivery invalid.", "Start from this report skeleton and change only the statuses, the reasons, and the evidence you map to each key:", acceptanceReportSkeleton(acceptance));
|
|
105
|
+
}
|
|
102
106
|
if (evidence?.length && !spec.instruction) {
|
|
103
107
|
lines.push("", "REQUIRED EVIDENCE");
|
|
104
108
|
for (const kind of evidence) {
|
|
@@ -300,6 +304,53 @@ export function criterionKey(value) {
|
|
|
300
304
|
.replace(/[.\u3002]+$/, "")
|
|
301
305
|
.toLowerCase();
|
|
302
306
|
}
|
|
307
|
+
/**
|
|
308
|
+
* The key of one acceptance criterion: `c1` for the first approved criterion, `c2` for the second.
|
|
309
|
+
*
|
|
310
|
+
* The key is derived from the approved order at prompt time and at parse time. It is not stored:
|
|
311
|
+
* the approved criteria list of the execution epoch is the only source of the order, so a key
|
|
312
|
+
* cannot disagree with the contract it names.
|
|
313
|
+
*/
|
|
314
|
+
export function criterionKeyLabel(index) {
|
|
315
|
+
return `c${index + 1}`;
|
|
316
|
+
}
|
|
317
|
+
/** A criterion reference that is a key: `c1`, `C1`, `[c1]`, or the same with spaces around it. */
|
|
318
|
+
const CRITERION_KEY_REFERENCE = /^\[?\s*c(\d{1,3})\s*\]?$/i;
|
|
319
|
+
/**
|
|
320
|
+
* Resolve one criterion reference from the agent report to the approved criterion text.
|
|
321
|
+
*
|
|
322
|
+
* The reference is the exact text (with the `criterionKey` normalization) or the key. The text is
|
|
323
|
+
* tried first, so a criterion whose own text is "c1" keeps its identity. The result is null when
|
|
324
|
+
* the reference names no approved criterion.
|
|
325
|
+
*/
|
|
326
|
+
export function resolveCriterionReference(value, acceptance) {
|
|
327
|
+
const key = criterionKey(value);
|
|
328
|
+
const byText = acceptance.find((criterion) => criterionKey(criterion) === key);
|
|
329
|
+
if (byText !== undefined)
|
|
330
|
+
return byText;
|
|
331
|
+
const match = CRITERION_KEY_REFERENCE.exec(value.trim());
|
|
332
|
+
if (!match)
|
|
333
|
+
return null;
|
|
334
|
+
return acceptance[Number(match[1]) - 1] ?? null;
|
|
335
|
+
}
|
|
336
|
+
/** True when the reference has the shape of a key. Used to tell an unknown key from free prose. */
|
|
337
|
+
function isCriterionKeyReference(value) {
|
|
338
|
+
return CRITERION_KEY_REFERENCE.test(value.trim());
|
|
339
|
+
}
|
|
340
|
+
/** Render the approved criteria as `[c1] <text>` lines for a prompt. */
|
|
341
|
+
export function acceptanceCriteriaLines(acceptance) {
|
|
342
|
+
return acceptance.map((criterion, index) => `- [${criterionKeyLabel(index)}] ${criterion}`);
|
|
343
|
+
}
|
|
344
|
+
/**
|
|
345
|
+
* The pre-filled acceptance_results skeleton for the prompt: one entry per key, status unknown.
|
|
346
|
+
* The agent changes the statuses and adds the evidence instead of composing the list, because
|
|
347
|
+
* composing it means retyping long criterion prose, and a retyped sentence made the delivery
|
|
348
|
+
* invalid at "Preparing delivery" on most first deliveries.
|
|
349
|
+
*/
|
|
350
|
+
export function acceptanceReportSkeleton(acceptance) {
|
|
351
|
+
const entries = acceptance.map((_, index) => ({ criterion: criterionKeyLabel(index), status: "unknown" }));
|
|
352
|
+
return JSON.stringify({ acceptance_results: entries });
|
|
353
|
+
}
|
|
303
354
|
/** Bound one criterion for an error message. A criterion holds up to 4 000 characters. */
|
|
304
355
|
function quoteCriterion(value) {
|
|
305
356
|
const text = value.trim().replace(/\s+/g, " ");
|
|
@@ -336,13 +387,34 @@ export function parseAgentReport(text, acceptance) {
|
|
|
336
387
|
const at = issue?.path?.length ? ` at ${issue.path.join(".")}` : "";
|
|
337
388
|
throw new Error(`Agent report is invalid${at}: ${issue?.message ?? "unknown validation error"}`);
|
|
338
389
|
}
|
|
339
|
-
|
|
390
|
+
// Map every criterion reference — in the results and in the evidence — back to the approved text
|
|
391
|
+
// before anything else reads it. Downstream (the packet, the reviewer, Today) sees text only; the
|
|
392
|
+
// key is transport. An unknown key is refused here, because a key the agent invented cannot be
|
|
393
|
+
// mapped and silently leaves a criterion unwitnessed.
|
|
394
|
+
const unknownKeys = [];
|
|
395
|
+
const toApprovedText = (value) => {
|
|
396
|
+
const text = resolveCriterionReference(value, acceptance);
|
|
397
|
+
if (text !== null)
|
|
398
|
+
return text;
|
|
399
|
+
if (isCriterionKeyReference(value))
|
|
400
|
+
unknownKeys.push(value.trim());
|
|
401
|
+
return value;
|
|
402
|
+
};
|
|
403
|
+
const acceptanceResults = parsed.data.acceptance_results.map((item) => ({ ...item, criterion: toApprovedText(item.criterion) }));
|
|
404
|
+
const evidence = parsed.data.evidence.map((item) => ({ ...item, acceptance_criteria: item.acceptance_criteria.map(toApprovedText) }));
|
|
405
|
+
if (unknownKeys.length) {
|
|
406
|
+
const valid = acceptance.length
|
|
407
|
+
? acceptance.map((criterion, index) => `${criterionKeyLabel(index)} = ${quoteCriterion(criterion)}`).join("; ")
|
|
408
|
+
: "none — this package has no acceptance criteria";
|
|
409
|
+
throw new Error(`Agent report names a criterion key that does not exist (${[...new Set(unknownKeys)].join(", ")}). The valid keys are: ${valid}`);
|
|
410
|
+
}
|
|
411
|
+
const reportedKeys = acceptanceResults.map((item) => criterionKey(item.criterion));
|
|
340
412
|
const approvedKeys = new Set(acceptance.map(criterionKey));
|
|
341
|
-
const duplicated =
|
|
413
|
+
const duplicated = acceptanceResults
|
|
342
414
|
.filter((item, index) => reportedKeys.indexOf(criterionKey(item.criterion)) !== index)
|
|
343
415
|
.map((item) => item.criterion);
|
|
344
416
|
const missing = acceptance.filter((criterion) => !reportedKeys.includes(criterionKey(criterion)));
|
|
345
|
-
const unapproved =
|
|
417
|
+
const unapproved = acceptanceResults
|
|
346
418
|
.filter((item) => !approvedKeys.has(criterionKey(item.criterion)))
|
|
347
419
|
.map((item) => item.criterion);
|
|
348
420
|
if (missing.length || duplicated.length || unapproved.length) {
|
|
@@ -358,10 +430,11 @@ export function parseAgentReport(text, acceptance) {
|
|
|
358
430
|
// Missing evidence mappings cannot support a "met" claim. Preserve the evidence itself, but
|
|
359
431
|
// downgrade only the unsupported result to unknown so the existing quality loop can assess the
|
|
360
432
|
// completed work instead of throwing the whole implementation away.
|
|
361
|
-
const mappedCriteria = new Set(
|
|
433
|
+
const mappedCriteria = new Set(evidence.flatMap((item) => item.acceptance_criteria.map(criterionKey)));
|
|
362
434
|
return {
|
|
363
435
|
...parsed.data,
|
|
364
|
-
|
|
436
|
+
evidence,
|
|
437
|
+
acceptance_results: acceptanceResults.map((item) => {
|
|
365
438
|
const { unverified_reason: reported, unverified_detail: detail, ...rest } = item;
|
|
366
439
|
const status = item.status === "met" && !mappedCriteria.has(criterionKey(item.criterion))
|
|
367
440
|
? "unknown"
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
*/
|
|
6
6
|
import { execFile } from "node:child_process";
|
|
7
7
|
import { promisify } from "node:util";
|
|
8
|
+
import { resolveCriterionReference } from "./driver.js";
|
|
8
9
|
import { isBoundedVerificationCommand, isRunnableVerificationCommand } from "./execution-class.js";
|
|
9
10
|
const execFileAsync = promisify(execFile);
|
|
10
11
|
const TEST_EVIDENCE_DETAILS_MIN = 32;
|
|
@@ -287,8 +288,10 @@ export function criteriaForTestEvidence(report, command, acceptance = []) {
|
|
|
287
288
|
const mapped = new Set();
|
|
288
289
|
for (const item of sources) {
|
|
289
290
|
for (const criterion of item.acceptance_criteria ?? []) {
|
|
291
|
+
// The agent may name a criterion by its key (c1). The witness carries the approved text, so
|
|
292
|
+
// that a key never reaches the packet and the criterion is not left unwitnessed.
|
|
290
293
|
if (criterion)
|
|
291
|
-
mapped.add(criterion);
|
|
294
|
+
mapped.add(resolveCriterionReference(criterion, acceptance) ?? criterion);
|
|
292
295
|
}
|
|
293
296
|
}
|
|
294
297
|
// A criterion that names the command verbatim is proved by that command's witness, whether or
|
package/dist/execution.js
CHANGED
|
@@ -4,7 +4,7 @@ import { z } from "zod";
|
|
|
4
4
|
import { resolveAgentTimeout } from "./execution-budget.js";
|
|
5
5
|
import { ConduitRequestError } from "./client.js";
|
|
6
6
|
import { redactSecrets, saveDriverOutcome, saveDriverQuota } from "./config.js";
|
|
7
|
-
import { DRIVERS, agentReportTemplate, buildAssignmentPrompt, evidenceKinds, extractAgentReportJsonText, fuelEndpoint, normalizeEvidenceDigest, parseAgentReport, parseJsonObjectCandidate, pickModelCandidate, requireStampedExecutionClass, tierForRisk } from "./driver.js";
|
|
7
|
+
import { DRIVERS, acceptanceCriteriaLines, acceptanceReportSkeleton, agentReportTemplate, buildAssignmentPrompt, evidenceKinds, extractAgentReportJsonText, fuelEndpoint, normalizeEvidenceDigest, parseAgentReport, parseJsonObjectCandidate, pickModelCandidate, requireStampedExecutionClass, tierForRisk } from "./driver.js";
|
|
8
8
|
import { assertClassFloor } from "./execution-class.js";
|
|
9
9
|
import { pickDriverForClaim, recordDriverQuota, resolveDriverFuel, resolveDriverFuelProvenance, supportsReadOnlyDiagnosis, clearDriverOutcome, recordDriverTimeout } from "./drivers.js";
|
|
10
10
|
import { attemptWorktreePath, createAttemptWorktree, proveResumeWorktree, quarantineAttemptWorktree, removeAttemptWorktree, } from "./attempt-worktree.js";
|
|
@@ -1675,8 +1675,8 @@ function buildLandContinuationPrompt(input) {
|
|
|
1675
1675
|
"APPROVED CHANGE SCOPE",
|
|
1676
1676
|
...(scope.length ? scope.map((path) => `- ${path}`) : ["- (none — stay within the plan)"]),
|
|
1677
1677
|
"",
|
|
1678
|
-
"APPROVED ACCEPTANCE CRITERIA",
|
|
1679
|
-
...(input.acceptance.length ? input.acceptance
|
|
1678
|
+
"APPROVED ACCEPTANCE CRITERIA — name each one by its key, not by its text",
|
|
1679
|
+
...(input.acceptance.length ? acceptanceCriteriaLines(input.acceptance) : ["- None"]),
|
|
1680
1680
|
"",
|
|
1681
1681
|
"REQUIRED DELIVERY SHAPE",
|
|
1682
1682
|
agentReportTemplate,
|
|
@@ -1794,8 +1794,11 @@ function buildDeliveryRepairPrompt(parseError, previousReply, acceptance) {
|
|
|
1794
1794
|
"REQUIRED DELIVERY SHAPE",
|
|
1795
1795
|
agentReportTemplate,
|
|
1796
1796
|
"",
|
|
1797
|
-
"APPROVED ACCEPTANCE CRITERIA",
|
|
1798
|
-
...(acceptance.length ? acceptance
|
|
1797
|
+
"APPROVED ACCEPTANCE CRITERIA — name each one by its key, not by its text",
|
|
1798
|
+
...(acceptance.length ? acceptanceCriteriaLines(acceptance) : ["- None"]),
|
|
1799
|
+
...(acceptance.length
|
|
1800
|
+
? ["", "Start from this report skeleton and change only the statuses, the reasons, and the evidence:", acceptanceReportSkeleton(acceptance)]
|
|
1801
|
+
: []),
|
|
1799
1802
|
"",
|
|
1800
1803
|
"PARSE ERROR",
|
|
1801
1804
|
redactSecrets(parseError),
|
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.42",
|
|
4
4
|
"description": "Conduit Bridge CLI \u2014 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": {
|