@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.
- package/.agentic-driver/loop-until-done.defaults.json +5 -0
- package/README.md +315 -33
- package/config/merge-policy.v1.example.json +4 -0
- package/config/relay-authority.v1.example.json +7 -0
- package/config/treehouse.v1.json +4 -0
- package/docs/durable-driver-api.md +23 -0
- package/docs/durable-persistence.md +32 -0
- package/docs/merge-policy.md +45 -0
- package/docs/relay-authority.md +9 -0
- package/extensions/aidr.ts +3 -0
- package/extensions/harvest-lessons.ts +8 -0
- package/extensions/herdr-dispatch.ts +32 -3
- package/extensions/loop-until-done.ts +7 -0
- package/extensions/recovery-evidence.ts +3 -3
- package/extensions/treehouse-recycle.ts +47 -0
- package/janus/README.md +3 -6
- package/janus/janus/config.ts +2 -2
- package/janus/janus/providers.ts +2 -4
- package/janus/tests/providers.test.ts +2 -2
- package/{extensions → lib}/task-store-adapter.ts +1 -5
- package/package.json +161 -7
- package/scripts/enforcement/approval_ledger_pi.js +135 -0
- package/scripts/enforcement/attended_authority_guard.js +113 -0
- package/scripts/enforcement/board_claim_bridge_pi.js +10 -10
- package/scripts/enforcement/durable_assignment_pi.js +488 -0
- package/scripts/enforcement/durable_driver_pi.d.ts +51 -0
- package/scripts/enforcement/durable_driver_pi.js +254 -0
- package/scripts/enforcement/herdr_async_dispatch_pi.js +86 -20
- package/scripts/enforcement/herdr_async_seam_pi.js +90 -3
- package/scripts/enforcement/herdr_communication_pi.js +205 -31
- package/scripts/enforcement/herdr_lifecycle_pi.js +730 -81
- package/scripts/enforcement/linux_microvm_cutover_pi.js +3 -3
- package/scripts/enforcement/linux_microvm_remote_fixture.sh +0 -6
- package/scripts/enforcement/loop_until_done_guide.md +13 -0
- package/scripts/enforcement/loop_until_done_pi.js +214 -0
- package/scripts/enforcement/merge_policy.js +181 -0
- package/scripts/enforcement/outward_effect_pi.js +156 -0
- package/scripts/enforcement/recovery_evidence_review_pi.js +31 -3
- package/scripts/enforcement/storage_owner_lock_pi.js +66 -0
- package/scripts/enforcement/task_board_core_pi.js +13 -100
- package/scripts/enforcement/task_board_hash_pi.js +86 -0
- package/scripts/enforcement/treehouse_recycle_pi.js +145 -0
- package/scripts/harvest_lessons.js +219 -0
- package/scripts/relay/record_approval.mjs +10 -0
- package/scripts/relay/send_request.mjs +26 -0
- package/skills/architect-pi/SKILL.md +44 -0
- package/skills/arena-pi/SKILL.md +46 -0
- package/skills/automate-me-pi/SKILL.md +37 -0
- package/skills/benchmark-checklist-pi/SKILL.md +35 -0
- package/skills/blast-radius-pi/SKILL.md +24 -0
- package/skills/bro-pi/SKILL.md +17 -0
- package/skills/correct-pi/SKILL.md +36 -0
- package/skills/create-verification-skill-pi/LICENSE +21 -0
- package/skills/create-verification-skill-pi/SKILL.md +24 -0
- package/skills/decision-to-spec/SKILL.md +65 -0
- package/skills/figure-it-out-pi/SKILL.md +37 -0
- package/skills/harvest-lessons/SKILL.md +56 -0
- package/skills/harvest-lessons/harvest.example.json +8 -0
- package/skills/how/LICENSE +24 -0
- package/skills/how/WORKFLOW_BOUNDARIES.md +19 -0
- package/skills/how-pi/SKILL.md +33 -0
- package/skills/in-your-own-words-pi/SKILL.md +15 -0
- package/skills/interrogate-pi/SKILL.md +35 -0
- package/skills/ketchup-pi/SKILL.md +21 -0
- package/skills/maintain-verification-skill-pi/SKILL.md +37 -0
- package/skills/make-bot-ui-pi/SKILL.md +33 -0
- package/skills/no-comments-pi/SKILL.md +33 -0
- package/skills/poteto-help-pi/SKILL.md +50 -0
- package/skills/poteto-mode-pi/LICENSE +21 -0
- package/skills/poteto-mode-pi/SKILL.md +41 -0
- package/skills/poteto-mode-pi/playbooks.md +95 -0
- package/skills/principle-attack-the-premise-pi/SKILL.md +17 -0
- package/skills/principle-boundary-discipline-pi/SKILL.md +19 -0
- package/skills/principle-build-the-lever-pi/SKILL.md +19 -0
- package/skills/principle-encode-lessons-in-structure-pi/SKILL.md +17 -0
- package/skills/principle-exhaust-the-design-space-pi/SKILL.md +17 -0
- package/skills/principle-experience-first-pi/SKILL.md +17 -0
- package/skills/principle-explain-the-number-pi/SKILL.md +21 -0
- package/skills/principle-fix-root-causes-pi/SKILL.md +21 -0
- package/skills/principle-foundational-thinking-pi/SKILL.md +21 -0
- package/skills/principle-guard-the-context-window-pi/SKILL.md +19 -0
- package/skills/principle-laziness-protocol-pi/SKILL.md +19 -0
- package/skills/principle-make-operations-idempotent-pi/SKILL.md +21 -0
- package/skills/principle-migrate-callers-then-delete-legacy-apis-pi/SKILL.md +19 -0
- package/skills/principle-minimize-reader-load-pi/SKILL.md +19 -0
- package/skills/principle-model-the-domain-pi/SKILL.md +19 -0
- package/skills/principle-never-block-on-the-human-pi/SKILL.md +19 -0
- package/skills/principle-outcome-oriented-execution-pi/SKILL.md +19 -0
- package/skills/principle-prove-it-works-pi/SKILL.md +19 -0
- package/skills/principle-redesign-from-first-principles-pi/SKILL.md +19 -0
- package/skills/principle-separate-before-serializing-shared-state-pi/SKILL.md +19 -0
- package/skills/principle-sequence-verifiable-units-pi/SKILL.md +19 -0
- package/skills/principle-subtract-before-you-add-pi/SKILL.md +19 -0
- package/skills/principle-test-behavior-not-implementation-pi/SKILL.md +21 -0
- package/skills/principle-type-system-discipline-pi/SKILL.md +19 -0
- package/skills/public-pr-wording/SKILL.md +69 -0
- package/skills/public-pr-wording/pr_wording_config.example.json +22 -0
- package/skills/public-pr-wording/pr_wording_lint.py +249 -0
- package/skills/public-pr-wording/pr_wording_rules.json +60 -0
- package/skills/pulse/SKILL.md +4 -2
- package/skills/recall-pi/SKILL.md +29 -0
- package/skills/reflect-pi/SKILL.md +27 -0
- package/skills/setup-pstack-pi/SKILL.md +23 -0
- package/skills/setup-pstack-pi/assets/pstack-model-roles.v1.example.json +7 -0
- package/skills/sharpen-pi/SKILL.md +19 -0
- package/skills/show-me-your-work-pi/SKILL.md +27 -0
- package/skills/swarm-pi/SKILL.md +27 -0
- package/skills/tasks/SKILL.md +4 -2
- package/skills/tdd-pi/SKILL.md +21 -0
- package/skills/teach-pi/SKILL.md +21 -0
- package/skills/technical-writing-pi/SKILL.md +27 -0
- package/skills/typescript-best-practices-pi/SKILL.md +21 -0
- package/skills/unslop-pi/SKILL.md +27 -0
- package/skills/why-pi/SKILL.md +37 -0
- package/PROVENANCE.md +0 -83
- package/config/herdr-worker-repositories.v1.json +0 -9
- package/docs/codebase-inventory.md +0 -15
- 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
|
-
//
|
|
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
|
-
|
|
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
|
|
1472
|
-
//
|
|
1473
|
-
//
|
|
1474
|
-
// line start
|
|
1475
|
-
//
|
|
1476
|
-
//
|
|
1477
|
-
//
|
|
1478
|
-
//
|
|
1479
|
-
//
|
|
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
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
const range = promptContractRange(text, marker,
|
|
1490
|
-
if (!range || range.end
|
|
1491
|
-
|
|
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" ?
|
|
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
|
|
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
|
-
//
|
|
1765
|
-
|
|
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);
|