@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 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": "exact criterion text", "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": ["exact criterion text supported by this evidence"]}], "assumptions": [], "risks": [], "limitations": [], "head_commit": "full sha of your final commit, omit if none"}';
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
- lines.push("", `ACCEPTANCE CRITERIA the delivery is judged against these\n${acceptance.map((item) => `- ${item}`).join("\n")}`);
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
- const reportedKeys = parsed.data.acceptance_results.map((item) => criterionKey(item.criterion));
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 = parsed.data.acceptance_results
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 = parsed.data.acceptance_results
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(parsed.data.evidence.flatMap((item) => item.acceptance_criteria.map(criterionKey)));
433
+ const mappedCriteria = new Set(evidence.flatMap((item) => item.acceptance_criteria.map(criterionKey)));
362
434
  return {
363
435
  ...parsed.data,
364
- acceptance_results: parsed.data.acceptance_results.map((item) => {
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.map((criterion) => `- ${criterion}`) : ["- None"]),
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.map((criterion) => `- ${criterion}`) : ["- None"]),
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.41",
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": {