@evoclock/pi-agentic-driver 1.1.1 → 2.0.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.
Files changed (118) hide show
  1. package/.agentic-driver/loop-until-done.defaults.json +5 -0
  2. package/README.md +315 -33
  3. package/config/merge-policy.v1.example.json +4 -0
  4. package/config/relay-authority.v1.example.json +7 -0
  5. package/config/treehouse.v1.json +4 -0
  6. package/docs/durable-driver-api.md +23 -0
  7. package/docs/durable-persistence.md +32 -0
  8. package/docs/merge-policy.md +45 -0
  9. package/docs/relay-authority.md +9 -0
  10. package/extensions/aidr.ts +3 -0
  11. package/extensions/harvest-lessons.ts +8 -0
  12. package/extensions/herdr-dispatch.ts +32 -3
  13. package/extensions/loop-until-done.ts +7 -0
  14. package/extensions/recovery-evidence.ts +3 -3
  15. package/extensions/treehouse-recycle.ts +47 -0
  16. package/janus/README.md +3 -6
  17. package/janus/janus/config.ts +2 -2
  18. package/janus/janus/providers.ts +2 -4
  19. package/janus/tests/providers.test.ts +2 -2
  20. package/{extensions → lib}/task-store-adapter.ts +1 -5
  21. package/package.json +161 -7
  22. package/scripts/enforcement/approval_ledger_pi.js +135 -0
  23. package/scripts/enforcement/attended_authority_guard.js +113 -0
  24. package/scripts/enforcement/board_claim_bridge_pi.js +10 -10
  25. package/scripts/enforcement/durable_assignment_pi.js +488 -0
  26. package/scripts/enforcement/durable_driver_pi.d.ts +51 -0
  27. package/scripts/enforcement/durable_driver_pi.js +254 -0
  28. package/scripts/enforcement/herdr_async_dispatch_pi.js +86 -20
  29. package/scripts/enforcement/herdr_async_seam_pi.js +90 -3
  30. package/scripts/enforcement/herdr_communication_pi.js +205 -31
  31. package/scripts/enforcement/herdr_lifecycle_pi.js +730 -81
  32. package/scripts/enforcement/linux_microvm_cutover_pi.js +3 -3
  33. package/scripts/enforcement/linux_microvm_remote_fixture.sh +0 -6
  34. package/scripts/enforcement/loop_until_done_guide.md +13 -0
  35. package/scripts/enforcement/loop_until_done_pi.js +214 -0
  36. package/scripts/enforcement/merge_policy.js +181 -0
  37. package/scripts/enforcement/outward_effect_pi.js +156 -0
  38. package/scripts/enforcement/recovery_evidence_review_pi.js +31 -3
  39. package/scripts/enforcement/storage_owner_lock_pi.js +66 -0
  40. package/scripts/enforcement/task_board_core_pi.js +13 -100
  41. package/scripts/enforcement/task_board_hash_pi.js +86 -0
  42. package/scripts/enforcement/treehouse_recycle_pi.js +145 -0
  43. package/scripts/harvest_lessons.js +219 -0
  44. package/scripts/relay/record_approval.mjs +10 -0
  45. package/scripts/relay/send_request.mjs +26 -0
  46. package/skills/architect-pi/SKILL.md +44 -0
  47. package/skills/arena-pi/SKILL.md +46 -0
  48. package/skills/automate-me-pi/SKILL.md +37 -0
  49. package/skills/benchmark-checklist-pi/SKILL.md +35 -0
  50. package/skills/blast-radius-pi/SKILL.md +24 -0
  51. package/skills/bro-pi/SKILL.md +17 -0
  52. package/skills/correct-pi/SKILL.md +36 -0
  53. package/skills/create-verification-skill-pi/LICENSE +21 -0
  54. package/skills/create-verification-skill-pi/SKILL.md +24 -0
  55. package/skills/decision-to-spec/SKILL.md +65 -0
  56. package/skills/figure-it-out-pi/SKILL.md +37 -0
  57. package/skills/harvest-lessons/SKILL.md +56 -0
  58. package/skills/harvest-lessons/harvest.example.json +8 -0
  59. package/skills/how/LICENSE +24 -0
  60. package/skills/how/WORKFLOW_BOUNDARIES.md +19 -0
  61. package/skills/how-pi/SKILL.md +33 -0
  62. package/skills/in-your-own-words-pi/SKILL.md +15 -0
  63. package/skills/interrogate-pi/SKILL.md +35 -0
  64. package/skills/ketchup-pi/SKILL.md +21 -0
  65. package/skills/maintain-verification-skill-pi/SKILL.md +37 -0
  66. package/skills/make-bot-ui-pi/SKILL.md +33 -0
  67. package/skills/no-comments-pi/SKILL.md +33 -0
  68. package/skills/poteto-help-pi/SKILL.md +50 -0
  69. package/skills/poteto-mode-pi/LICENSE +21 -0
  70. package/skills/poteto-mode-pi/SKILL.md +41 -0
  71. package/skills/poteto-mode-pi/playbooks.md +95 -0
  72. package/skills/principle-attack-the-premise-pi/SKILL.md +17 -0
  73. package/skills/principle-boundary-discipline-pi/SKILL.md +19 -0
  74. package/skills/principle-build-the-lever-pi/SKILL.md +19 -0
  75. package/skills/principle-encode-lessons-in-structure-pi/SKILL.md +17 -0
  76. package/skills/principle-exhaust-the-design-space-pi/SKILL.md +17 -0
  77. package/skills/principle-experience-first-pi/SKILL.md +17 -0
  78. package/skills/principle-explain-the-number-pi/SKILL.md +21 -0
  79. package/skills/principle-fix-root-causes-pi/SKILL.md +21 -0
  80. package/skills/principle-foundational-thinking-pi/SKILL.md +21 -0
  81. package/skills/principle-guard-the-context-window-pi/SKILL.md +19 -0
  82. package/skills/principle-laziness-protocol-pi/SKILL.md +19 -0
  83. package/skills/principle-make-operations-idempotent-pi/SKILL.md +21 -0
  84. package/skills/principle-migrate-callers-then-delete-legacy-apis-pi/SKILL.md +19 -0
  85. package/skills/principle-minimize-reader-load-pi/SKILL.md +19 -0
  86. package/skills/principle-model-the-domain-pi/SKILL.md +19 -0
  87. package/skills/principle-never-block-on-the-human-pi/SKILL.md +19 -0
  88. package/skills/principle-outcome-oriented-execution-pi/SKILL.md +19 -0
  89. package/skills/principle-prove-it-works-pi/SKILL.md +19 -0
  90. package/skills/principle-redesign-from-first-principles-pi/SKILL.md +19 -0
  91. package/skills/principle-separate-before-serializing-shared-state-pi/SKILL.md +19 -0
  92. package/skills/principle-sequence-verifiable-units-pi/SKILL.md +19 -0
  93. package/skills/principle-subtract-before-you-add-pi/SKILL.md +19 -0
  94. package/skills/principle-test-behavior-not-implementation-pi/SKILL.md +21 -0
  95. package/skills/principle-type-system-discipline-pi/SKILL.md +19 -0
  96. package/skills/public-pr-wording/SKILL.md +69 -0
  97. package/skills/public-pr-wording/pr_wording_config.example.json +22 -0
  98. package/skills/public-pr-wording/pr_wording_lint.py +249 -0
  99. package/skills/public-pr-wording/pr_wording_rules.json +60 -0
  100. package/skills/pulse/SKILL.md +4 -2
  101. package/skills/recall-pi/SKILL.md +29 -0
  102. package/skills/reflect-pi/SKILL.md +27 -0
  103. package/skills/setup-pstack-pi/SKILL.md +23 -0
  104. package/skills/setup-pstack-pi/assets/pstack-model-roles.v1.example.json +7 -0
  105. package/skills/sharpen-pi/SKILL.md +19 -0
  106. package/skills/show-me-your-work-pi/SKILL.md +27 -0
  107. package/skills/swarm-pi/SKILL.md +27 -0
  108. package/skills/tasks/SKILL.md +4 -2
  109. package/skills/tdd-pi/SKILL.md +21 -0
  110. package/skills/teach-pi/SKILL.md +21 -0
  111. package/skills/technical-writing-pi/SKILL.md +27 -0
  112. package/skills/typescript-best-practices-pi/SKILL.md +21 -0
  113. package/skills/unslop-pi/SKILL.md +27 -0
  114. package/skills/why-pi/SKILL.md +37 -0
  115. package/PROVENANCE.md +0 -83
  116. package/config/herdr-worker-repositories.v1.json +0 -9
  117. package/docs/codebase-inventory.md +0 -15
  118. package/docs/recovery-evidence-store.md +0 -9
@@ -219,7 +219,7 @@ function boundedString(value, field, maxBytes = MAX_PROCESS_OUTPUT_BYTES) {
219
219
  return value;
220
220
  }
221
221
 
222
- function expectedRepository(context) {
222
+ export function expectedRepository(context) {
223
223
  const value = context?.repository ?? context?.cwd;
224
224
  if (typeof value !== "string" || !value.trim()) {
225
225
  throw communicationError("repository_unavailable", "the current repository is unavailable");
@@ -303,7 +303,7 @@ function checkedRegistryPath(primary) {
303
303
  // Registry lookup order (coordinator decision): the session repository's
304
304
  // own config first; when missing (ENOENT only) the active Pi profile's
305
305
  // shared config under {PI_CODING_AGENT_DIR}/config/ (fallback
306
- // ~/.pi/agent/config/). When neither exists, return the repo-local path so
306
+ // the installed agent config directory). When neither exists, return the repo-local path so
307
307
  // the caller's primary-only fallback applies exactly as before.
308
308
  const profileDir = process.env.PI_CODING_AGENT_DIR;
309
309
  const base = typeof profileDir === "string" && profileDir.trim()
@@ -1257,10 +1257,81 @@ export function extractLatestHerdrReport(text, role) {
1257
1257
  // ---------------------------------------------------------------------------
1258
1258
 
1259
1259
  const deliveries = new Map();
1260
+ let offlineDeliveries = new WeakMap();
1260
1261
  let deliverySeq = 0;
1261
1262
 
1263
+ function deliveryRecords(options) {
1264
+ if (!options.offlinePersistence) return deliveries;
1265
+ if (!offlineDeliveries.has(options.offlinePersistence)) offlineDeliveries.set(options.offlinePersistence, new Map());
1266
+ return offlineDeliveries.get(options.offlinePersistence);
1267
+ }
1268
+
1269
+ function requireOfflinePersistence(options) {
1270
+ const store = options.offlinePersistence;
1271
+ if (!store) return;
1272
+ if (store.schema !== "agentic-driver.offline-seam-store.v1" || typeof options.runProcess !== "function" ||
1273
+ ["get", "reserve", "update"].some((name) => typeof store[name] !== "function")) {
1274
+ throw communicationError("offline_persistence_required", "persistence requires the offline store and injected process adapter", "denied");
1275
+ }
1276
+ }
1277
+
1278
+ async function deliverPersistedPrompt(operation, request, context, options, signal, repositories) {
1279
+ const repository = expectedRepository(context);
1280
+ const digest = createHash("sha256").update(JSON.stringify({ repository, role: request.role, prompt: request.prompt })).digest("hex");
1281
+ const requestId = options.offlineRequestId ?? digest;
1282
+ if (typeof requestId !== "string" || !/^[a-z0-9-]{1,128}$/.test(requestId)) throw communicationError("offline_request_invalid", "bounded offline request identity required", "denied");
1283
+ const identity = createHash("sha256").update(`${repository}\u0000${requestId}`).digest("hex");
1284
+ // Retain the existing correlation grammar; the suffix is opaque, not a clock.
1285
+ const id = `dlv-${identity.slice(0, 16)}-${identity.slice(16, 32)}`;
1286
+ // Validate the complete framed brief before leaving a persistent tombstone.
1287
+ promptWithReportRequirement(request.role, request.prompt, id);
1288
+ const store = options.offlinePersistence;
1289
+ const reserved = await store.reserve("delivery", { id, digest, repository, role: request.role, createdAt: Date.now(), state: "queued" });
1290
+ if (!reserved.created) {
1291
+ const accepted = ["delivered", "answered"].includes(reserved.record.state);
1292
+ return { schema: HERDR_COMMUNICATION_SCHEMA, ok: accepted, status: accepted ? "accepted" : "refused", operation,
1293
+ deliveryId: id, role: request.role, deliveryState: reserved.record.state, acceptedAt: reserved.record.acceptedAt,
1294
+ duplicate: true, promptSent: false, held: !accepted, persisted: true, nonAuthorizing: true, authorityCreated: false };
1295
+ }
1296
+ await options.offlineBoundary?.("after-delivery-intent", { deliveryId: id });
1297
+ const sent = await deliverPrompt(operation, request, context, { ...options, offlineDeliveryId: id }, signal, repositories);
1298
+ await options.offlineBoundary?.("after-delivery-send", { deliveryId: id });
1299
+ const latest = await store.get("delivery", id);
1300
+ const patch = { state: latest.state === "held" ? "held" : sent.deliveryState ?? "failed" };
1301
+ if (sent.acceptedAt) patch.acceptedAt = sent.acceptedAt;
1302
+ if (sent.code) patch.code = sent.code;
1303
+ const committed = await store.update("delivery", id, digest, patch);
1304
+ await options.offlineBoundary?.("after-delivery-ack", { deliveryId: id });
1305
+ return { ...sent, ...(committed.state === "held" ? { ok: false, status: "refused", held: true, deliveryState: "unknown" } : {}), deliveryId: id, persisted: true };
1306
+ }
1307
+
1308
+ async function queryPersistedDelivery(request, context, options, signal, repositories) {
1309
+ const store = options.offlinePersistence;
1310
+ const saved = await store.get("delivery", request.deliveryId);
1311
+ if (!saved) return deliveryRefusal("delivery", communicationError("delivery_unknown", "no persisted delivery is known"));
1312
+ if (saved.repository !== expectedRepository(context)) throw communicationError("repository_mismatch", "delivery belongs to another repository", "denied");
1313
+ requireRole(saved.role);
1314
+ if (saved.state === "held") return deliverySnapshot({ ...saved, deliveryId: saved.id, state: "unknown" }, { held: true, code: saved.code ?? "delivery-held", persisted: true });
1315
+ if (saved.state === "answered" && saved.proof === "snapshot-backed-v1") {
1316
+ return deliverySnapshot({ ...saved, deliveryId: saved.id }, { report: saved.report, reportMarkers: reportMarkersForRole(saved.role), answeredAt: saved.answeredAt, persisted: true });
1317
+ }
1318
+ if (!deliveryRecords(options).has(saved.id)) {
1319
+ // Neither terminal snapshots nor framed echoes are restored from disk.
1320
+ // Original proof lost means hold, even if the role is now idle/done.
1321
+ if (saved.state !== "failed" && saved.state !== "held") await store.update("delivery", saved.id, saved.digest, { state: "held", code: "attribution-lost" });
1322
+ return deliverySnapshot({ ...saved, deliveryId: saved.id, state: saved.state === "failed" ? "failed" : "unknown" },
1323
+ { held: saved.state !== "failed", code: saved.state === "failed" ? saved.code : "attribution-lost", persisted: true });
1324
+ }
1325
+ const observed = await queryDelivery(request, context, options, signal, repositories);
1326
+ if (observed.deliveryState === "answered") {
1327
+ await store.update("delivery", saved.id, saved.digest, { state: "answered", report: observed.report, answeredAt: observed.answeredAt, proof: "snapshot-backed-v1" });
1328
+ }
1329
+ return { ...observed, persisted: true };
1330
+ }
1331
+
1262
1332
  export function resetHerdrDeliveriesForTests() {
1263
1333
  deliveries.clear();
1334
+ offlineDeliveries = new WeakMap();
1264
1335
  }
1265
1336
 
1266
1337
  function evictDeliveries() {
@@ -1280,12 +1351,12 @@ function stateChangeSeq(value) {
1280
1351
  return Number.isSafeInteger(seq) && seq >= 0 ? seq : undefined;
1281
1352
  }
1282
1353
 
1283
- function createDelivery(role, prompt, pre, seqBefore) {
1284
- evictDeliveries();
1354
+ function createDelivery(role, prompt, pre, seqBefore, options = {}) {
1355
+ if (!options.offlinePersistence) evictDeliveries();
1285
1356
  const createdAt = Date.now();
1286
1357
  const seq = (deliverySeq += 1);
1287
1358
  const digest = createHash("sha256").update(`${role}\u0000${createdAt}\u0000${seq}`).digest("hex");
1288
- const deliveryId = `dlv-${digest.slice(0, 16)}-${createdAt.toString(36)}`;
1359
+ const deliveryId = options.offlineDeliveryId ?? `dlv-${digest.slice(0, 16)}-${createdAt.toString(36)}`;
1289
1360
  const sentPrompt = promptWithReportRequirement(role, prompt, deliveryId);
1290
1361
  const record = {
1291
1362
  deliveryId,
@@ -1304,7 +1375,7 @@ function createDelivery(role, prompt, pre, seqBefore) {
1304
1375
  code: undefined,
1305
1376
  reason: undefined,
1306
1377
  };
1307
- deliveries.set(record.deliveryId, record);
1378
+ deliveryRecords(options).set(record.deliveryId, record);
1308
1379
  return record;
1309
1380
  }
1310
1381
 
@@ -1370,7 +1441,7 @@ async function deliverPrompt(operation, request, context, options, signal, repos
1370
1441
  // unconfirmed returns the existing delivery instead of re-handing off.
1371
1442
  // After the window, or once the prior delivery is confirmed
1372
1443
  // (delivered/answered/failed), a repeat is a genuinely new delivery.
1373
- const duplicate = findUnconfirmedDelivery(role, request.prompt);
1444
+ const duplicate = options.offlinePersistence ? undefined : findUnconfirmedDelivery(role, request.prompt);
1374
1445
  if (duplicate) {
1375
1446
  if (duplicate.state === "queued") {
1376
1447
  return {
@@ -1419,7 +1490,11 @@ async function deliverPrompt(operation, request, context, options, signal, repos
1419
1490
  } catch {
1420
1491
  pre = undefined;
1421
1492
  }
1422
- record = createDelivery(role, request.prompt, pre, stateChangeSeq(current));
1493
+ record = createDelivery(role, request.prompt, pre, stateChangeSeq(current), options);
1494
+ if (options.offlinePersistence) {
1495
+ const latest = await options.offlinePersistence.get("delivery", record.deliveryId);
1496
+ if (latest?.state !== "queued") throw communicationError("delivery_held", "persisted delivery is held; no handoff started");
1497
+ }
1423
1498
  // Handoff: `agent prompt` without --wait returns as soon as the CLI
1424
1499
  // accepts the brief. The acceptance window is internal
1425
1500
  // (COMMAND_TIMEOUT_MS); timeoutMs never schedules a model round-trip.
@@ -1468,28 +1543,88 @@ async function deliverPrompt(operation, request, context, options, signal, repos
1468
1543
  }
1469
1544
  }
1470
1545
 
1471
- // The per-handoff echo boundary. A boundary exists only where the FULL
1472
- // framed prompt — caller text, contract line, marker template, this
1473
- // delivery's correlation line, and the instruction line — is echoed at a
1474
- // line start and parses as one echoed contract unit. A bare or partially
1475
- // fabricated "Delivery:" line in live terminal text mints no boundary, and
1476
- // a delivery without a pre-delivery snapshot is unattributed before any
1477
- // boundary is ever searched. The correlation line is not an authenticator:
1478
- // terminal text stays untrusted evidence, and no authority is ever derived
1479
- // from it.
1546
+ // The per-handoff echo boundary. A boundary exists only where a FULL
1547
+ // echoed contract unit is observable in the snapshot: the caller's prompt
1548
+ // text (bounded terminal normalization), the contract line at
1549
+ // a line start (bounded horizontal whitespace only), the marker template
1550
+ // (with the same horizontal padding and right-hand box-border tolerance
1551
+ // marker parsing already applies — Herdr's recent-unwrapped snapshot pads
1552
+ // standalone marker lines to the terminal width and can retain Pi's `│`
1553
+ // border, so a byte-exact sentPrompt echo must not be required), this
1554
+ // delivery's correlation line, and the instruction line. A bare or partially
1555
+ // fabricated "Delivery:" line mints no boundary, and a delivery without a
1556
+ // pre-delivery snapshot is unattributed before any boundary is ever
1557
+ // searched. The correlation line is not an authenticator: terminal text
1558
+ // stays untrusted evidence, and no authority is ever derived from it.
1559
+ // A framed echo renders the caller's prompt text immediately before the
1560
+ // contract line. Compare its complete, bounded line sequence so embedded
1561
+ // contract text in the caller prompt cannot truncate the expected text.
1562
+ // Preserve caller whitespace and line breaks; allow only bounded extra
1563
+ // horizontal terminal padding (and its optional right-hand box border) after
1564
+ // each exact caller line. A contract-plus-tail echo with a missing caller line
1565
+ // is a partial echo.
1566
+ function echoedCallerPromptPrecedes(text, range, callerPrompt) {
1567
+ const expectedLines = callerPrompt.replace(/\r\n/g, "\n").split("\n");
1568
+ const window = Math.min(
1569
+ MAX_PROCESS_OUTPUT_BYTES,
1570
+ Buffer.byteLength(callerPrompt, "utf8")
1571
+ + expectedLines.length * (MAX_MARKER_HORIZONTAL_WHITESPACE + 8) + 8,
1572
+ );
1573
+ const from = Math.max(0, range.start - window);
1574
+ const beforeContract = text.slice(from, range.start).replace(/\r\n/g, "\n");
1575
+ if (!beforeContract.endsWith("\n")) return false;
1576
+ const observedLines = beforeContract.slice(0, -1).split("\n");
1577
+ if (observedLines.length < expectedLines.length) return false;
1578
+ const callerLines = observedLines.slice(-expectedLines.length);
1579
+ return expectedLines.every((expected, index) => {
1580
+ const observed = callerLines[index];
1581
+ if (!observed.startsWith(expected)) return false;
1582
+ let padding = observed.slice(expected.length);
1583
+ if (padding.endsWith("│")) padding = padding.slice(0, -1);
1584
+ return Buffer.byteLength(padding, "utf8") <= MAX_MARKER_HORIZONTAL_WHITESPACE
1585
+ && /^[ \t]*$/.test(padding);
1586
+ });
1587
+ }
1588
+
1589
+ function handoffEchoTailEnd(text, from, correlationLine) {
1590
+ // Each padded line may consume up to MAX_MARKER_HORIZONTAL_WHITESPACE
1591
+ // bytes; the window must span three framing lines plus the literal tail.
1592
+ const window = text.slice(from, from + MAX_MARKER_HORIZONTAL_WHITESPACE * 3 + correlationLine.length + 128);
1593
+ const match = new RegExp(
1594
+ "^[ \\t]*(?:│)?\\r?\\n[ \\t]*(?:│)?" + correlationLine + "[ \\t│]*\\r?\\n"
1595
+ + "[ \\t]*(?:│)?Begin your report with the Delivery line above, exactly as written\\.[ \\t│]*(?:\\r?\\n|$)",
1596
+ ).exec(window);
1597
+ return match ? from + match[0].length : undefined;
1598
+ }
1599
+
1480
1600
  function handoffBoundaryIn(text, record) {
1481
1601
  const marker = reportMarkersForRole(record.role);
1482
- const starts = lineStarts(text);
1483
- let from = 0;
1484
- while (true) {
1485
- const start = text.indexOf(record.sentPrompt, from);
1486
- if (start < 0) return undefined;
1487
- from = start + 1;
1488
- if (!starts.includes(start)) continue;
1489
- const range = promptContractRange(text, marker, start + record.sentPrompt.length, { boundEnd: true });
1490
- if (!range || range.end !== start + record.sentPrompt.length) continue;
1491
- return { start, end: range.end };
1602
+ const correlationLine = `Delivery: ${record.deliveryId}`;
1603
+ for (const occurrence of allMarkerOccurrences(text, true, marker)) {
1604
+ if (occurrence.marker !== marker.close) continue;
1605
+ // promptContractRange validates the echoed contract structure tolerantly
1606
+ // (contract line, both markers, bounded size, no foreign markers) and
1607
+ // already tolerates horizontal padding and the box border between the
1608
+ // markers themselves.
1609
+ const range = promptContractRange(text, marker, occurrence.index);
1610
+ if (!range || range.end <= occurrence.index) continue;
1611
+ // The echoed contract must begin at a line start, exactly as the
1612
+ // byte-exact boundary it replaces did.
1613
+ const lineStart = text.lastIndexOf("\n", range.start - 1) + 1;
1614
+ const leading = text.slice(lineStart, range.start);
1615
+ if (Buffer.byteLength(leading, "utf8") > MAX_MARKER_HORIZONTAL_WHITESPACE || !/^[ \\t]*$/.test(leading)) continue;
1616
+ // The tail must self-identify THIS handoff by its unique correlation
1617
+ // line, with the same padding/border tolerance as marker lines.
1618
+ const end = handoffEchoTailEnd(text, occurrence.end, correlationLine);
1619
+ if (end === undefined) continue;
1620
+ // The caller's own prompt text must be echoed immediately before the
1621
+ // contract line under the same bounded terminal normalization. A bare
1622
+ // contract-plus-tail echo is a fabricated partial echo and mints no
1623
+ // boundary even when it quotes this delivery's correlation line.
1624
+ if (!echoedCallerPromptPrecedes(text, range, record.prompt)) continue;
1625
+ return { start: range.start, end };
1492
1626
  }
1627
+ return undefined;
1493
1628
  }
1494
1629
 
1495
1630
  function reportRegionsAfter(text, role, fromIndex) {
@@ -1542,7 +1677,7 @@ function regionBody(text, role, region, correlationLine) {
1542
1677
  }
1543
1678
 
1544
1679
  async function queryDelivery(request, context, options, signal, repositories) {
1545
- const record = typeof request.deliveryId === "string" ? deliveries.get(request.deliveryId) : undefined;
1680
+ const record = typeof request.deliveryId === "string" ? deliveryRecords(options).get(request.deliveryId) : undefined;
1546
1681
  if (!record) {
1547
1682
  return deliveryRefusal("delivery", communicationError("delivery_unknown", "no live delivery record is known for this deliveryId (bounded, in-memory)"));
1548
1683
  }
@@ -1644,6 +1779,36 @@ async function queryDelivery(request, context, options, signal, repositories) {
1644
1779
  // Legacy blocking exchange (prompt → wait → provenanced report read). It is
1645
1780
  // no longer reachable from the tool surface; the dispatch journey keeps its
1646
1781
  // single-exchange semantics through this entry point unchanged.
1782
+ export async function inspectHerdrJourneyMarker(role, requestId, context, options = {}, signal) {
1783
+ requireRole(role);
1784
+ if (typeof requestId !== "string" || !/^[a-zA-Z0-9_-]{1,100}:[1-9][0-9]{0,2}$/.test(requestId)) {
1785
+ throw communicationError("journey-id-invalid", "bounded journey request ID required", "denied");
1786
+ }
1787
+ const repositories = trustedRepositories(expectedRepository(context));
1788
+ const current = await invokeHerdr("get", { action: "get", role }, context, options, signal);
1789
+ publicAgentObservation(extractAgent(current, "agent_info"), role, repositories);
1790
+ const history = readText(await invokeHerdr("read", { action: "read", role, readLines: MAX_REPORT_READ_LINES }, context, options, signal));
1791
+ // Absence is never evidence that an asynchronous pane send did not land.
1792
+ const lines = history.split("\n");
1793
+ const tag = `[journey:${requestId}]`;
1794
+ const occurrences = lines.filter((line) => line.trim() === tag).length;
1795
+ if (occurrences !== 1) return { present: false };
1796
+ const from = history.indexOf(tag) + tag.length;
1797
+ const tail = history.slice(from);
1798
+ // One report only, with no later journey prompt. An unrelated or ambiguous
1799
+ // pane exchange is not attributable merely because it is the latest text.
1800
+ if (/^\[journey:[^\]]+\]$/m.test(tail)) return { present: true };
1801
+ const markers = reportMarkersForRole(role);
1802
+ const relevant = allMarkerOccurrences(tail, true, markers)
1803
+ .filter((item) => (item.marker === markers.open || item.marker === markers.close)
1804
+ && !isPromptContractMarker(history, { index: item.index + from, end: item.end + from }, markers));
1805
+ if (relevant.length !== 2 || relevant[0].marker !== markers.open || relevant[1].marker !== markers.close) {
1806
+ return { present: true };
1807
+ }
1808
+ try { return { present: true, report: extractReportFromSegment(history, role, from) }; }
1809
+ catch { return { present: true }; }
1810
+ }
1811
+
1647
1812
  export async function executeHerdrPromptExchange(params, context, options = {}, signal) {
1648
1813
  let request;
1649
1814
  const operation = "prompt";
@@ -1742,6 +1907,10 @@ export async function executeHerdrCommunication(params, context, options = {}, s
1742
1907
  try {
1743
1908
  request = validateParams(params);
1744
1909
  operation = request.action;
1910
+ requireOfflinePersistence(options);
1911
+ if (options.offlinePersistence && !["prompt", "submit", "delivery"].includes(operation)) {
1912
+ throw communicationError("offline_delivery_required", "offline persistence requires a delivery-bound operation; role-keyed fallback is refused", "denied");
1913
+ }
1745
1914
  const repository = expectedRepository(context);
1746
1915
  const repositories = trustedRepositories(repository);
1747
1916
  if (operation === "list") {
@@ -1758,11 +1927,16 @@ export async function executeHerdrCommunication(params, context, options = {}, s
1758
1927
  // return an explicit receipt immediately. No model round-trip happens
1759
1928
  // on this path; settlement is observed through the `delivery` action,
1760
1929
  // get, wait, and read.
1761
- return await deliverPrompt(operation, request, context, options, signal, repositories);
1930
+ return options.offlinePersistence
1931
+ ? await deliverPersistedPrompt(operation, request, context, options, signal, repositories)
1932
+ : await deliverPrompt(operation, request, context, options, signal, repositories);
1762
1933
  }
1763
1934
  if (operation === "delivery") {
1764
- // Read-only, non-authorizing per-delivery state query.
1765
- return await queryDelivery(request, context, options, signal, repositories);
1935
+ // Non-authorizing observation. Offline mode may persist proof or an
1936
+ // attribution-loss hold, but never sends; default mode is unchanged.
1937
+ return options.offlinePersistence
1938
+ ? await queryPersistedDelivery(request, context, options, signal, repositories)
1939
+ : await queryDelivery(request, context, options, signal, repositories);
1766
1940
  }
1767
1941
  if (operation === "wait") {
1768
1942
  const raw = await invokeHerdr(operation, request, context, options, signal);