@evoclock/pi-agentic-driver 0.10.0 → 1.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/nudge.defaults.json +11 -0
- package/.githooks/pre-commit +4 -0
- package/README.md +43 -31
- package/config/codebase-inventory.schema.json +23 -0
- package/docs/codebase-inventory.md +15 -0
- package/docs/recovery-evidence-store.md +9 -0
- package/extensions/herdr-dispatch.ts +15 -2
- package/extensions/recovery-evidence.ts +14 -0
- package/extensions/task-store-adapter.ts +101 -0
- package/extensions/tasks.ts +34 -11
- package/package.json +19 -1
- package/scripts/codebase_inventory.py +105 -0
- package/scripts/codebase_inventory_gate.py +48 -0
- package/scripts/enforcement/board_claim_bridge_pi.js +204 -0
- package/scripts/enforcement/herdr_async_dispatch_pi.js +83 -30
- package/scripts/enforcement/herdr_async_seam_pi.js +347 -0
- package/scripts/enforcement/herdr_communication_pi.js +608 -95
- package/scripts/enforcement/herdr_lifecycle_pi.js +266 -13
- package/scripts/enforcement/linux_microvm_remote_fixture.sh +6 -6
- package/scripts/enforcement/native_tui_context.js +16 -0
- package/scripts/enforcement/nudge_classifier_pi.js +176 -0
- package/scripts/enforcement/nudge_config_pi.js +234 -0
- package/scripts/enforcement/nudge_ledger_pi.js +514 -0
- package/scripts/enforcement/nudge_reason_state_pi.js +135 -0
- package/scripts/enforcement/nudge_validate_pi.js +224 -0
- package/scripts/enforcement/pulse_scheduler_pi.js +15 -3
- package/scripts/enforcement/recovery_evidence_review_pi.js +269 -0
- package/scripts/enforcement/recovery_evidence_store_pi.js +170 -0
- package/scripts/enforcement/session_tasks_core_pi.js +14 -0
- package/scripts/enforcement/task_board_core_pi.js +96 -7
|
@@ -0,0 +1,347 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Julen Gamboa <j.a.r.gamboa@gmail.com>
|
|
2
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
3
|
+
|
|
4
|
+
// Cross-repo async transport seam (vogelkop phase #47). A versioned,
|
|
5
|
+
// non-authorizing submit receipt is accepted immediately after a guarded
|
|
6
|
+
// submission; settlement is observed later through separate poll/observe/read
|
|
7
|
+
// operations. Nothing here retries, resends, or re-prompts: one submission is
|
|
8
|
+
// one attempt, and every result is an observation, never authority. Submission
|
|
9
|
+
// handles are process-local, in-memory, and do not survive a driver restart.
|
|
10
|
+
//
|
|
11
|
+
// The blocking journey path in herdr_async_dispatch_pi.js is unchanged; this
|
|
12
|
+
// module adds the split surface on top of the existing herdr-communication
|
|
13
|
+
// trust seam (fixed argv, shell:false, trusted executable, closed adapters).
|
|
14
|
+
|
|
15
|
+
import { createHash } from "node:crypto";
|
|
16
|
+
import {
|
|
17
|
+
executeHerdrCommunication,
|
|
18
|
+
reportMarkersForRole,
|
|
19
|
+
HERDR_COMMUNICATION_SCHEMA,
|
|
20
|
+
} from "./herdr_communication_pi.js";
|
|
21
|
+
|
|
22
|
+
export const ASYNC_DISPATCH_TOOL = "agentic_worker_dispatch_async";
|
|
23
|
+
export const ASYNC_DISPATCH_SCHEMA = "agentic-driver.async-dispatch.v1";
|
|
24
|
+
export const ASYNC_RECEIPT_SCHEMA = "vogelkop.async-transport.receipt.v1";
|
|
25
|
+
export const ASYNC_SUBMIT_STATUS = "submitted";
|
|
26
|
+
export const ASYNC_TERMINAL_STATES = Object.freeze([
|
|
27
|
+
"completed", "exhausted", "review_requested", "failed", "cancelled",
|
|
28
|
+
]);
|
|
29
|
+
export const ASYNC_NON_TERMINAL_STATES = Object.freeze([
|
|
30
|
+
"submitted", "working", "waiting-approval", "role-blocked", "unknown",
|
|
31
|
+
]);
|
|
32
|
+
|
|
33
|
+
const SUBMISSION_TTL_MS = 6 * 60 * 60 * 1000; // 6 hours, bounded memory
|
|
34
|
+
const MAX_ACTIVE_SUBMISSIONS = 64;
|
|
35
|
+
const REGISTRATIONS = new WeakSet();
|
|
36
|
+
const submissions = new Map();
|
|
37
|
+
|
|
38
|
+
function sha256Hex(value) {
|
|
39
|
+
return createHash("sha256").update(value).digest("hex");
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function submissionError(code, message, status = "blocked") {
|
|
43
|
+
const error = new Error(message);
|
|
44
|
+
error.name = "AsyncDispatchError";
|
|
45
|
+
error.code = code;
|
|
46
|
+
error.status = status;
|
|
47
|
+
return error;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function result(details) {
|
|
51
|
+
return { ok: details.ok === true, ...details };
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function failure(action, error) {
|
|
55
|
+
return {
|
|
56
|
+
schema: ASYNC_DISPATCH_SCHEMA,
|
|
57
|
+
ok: false,
|
|
58
|
+
action,
|
|
59
|
+
status: error?.status || "blocked",
|
|
60
|
+
code: error?.code || "async-dispatch-failed",
|
|
61
|
+
error: String(error?.message || error).slice(0, 512),
|
|
62
|
+
nonAuthorizing: true,
|
|
63
|
+
authorityCreated: false,
|
|
64
|
+
persisted: false,
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function baseFields(submission) {
|
|
69
|
+
return {
|
|
70
|
+
schema: ASYNC_DISPATCH_SCHEMA,
|
|
71
|
+
submissionId: submission.submissionId,
|
|
72
|
+
idempotencyKey: submission.idempotencyKey,
|
|
73
|
+
role: submission.role,
|
|
74
|
+
driverSchema: ASYNC_DISPATCH_SCHEMA,
|
|
75
|
+
nonAuthorizing: true,
|
|
76
|
+
authorityCreated: false,
|
|
77
|
+
persisted: false,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function receiptFor(submission) {
|
|
82
|
+
return {
|
|
83
|
+
schema: ASYNC_RECEIPT_SCHEMA,
|
|
84
|
+
submissionId: submission.submissionId,
|
|
85
|
+
driverSchema: ASYNC_DISPATCH_SCHEMA,
|
|
86
|
+
role: submission.role,
|
|
87
|
+
state: ASYNC_SUBMIT_STATUS,
|
|
88
|
+
accepted: submission.phase === "accepted",
|
|
89
|
+
returnedWithoutWaiting: true,
|
|
90
|
+
acceptedAt: submission.acceptedAt,
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function submitResult(submission, { duplicate, inFlight, promptSent }) {
|
|
95
|
+
return result({
|
|
96
|
+
...baseFields(submission),
|
|
97
|
+
ok: true,
|
|
98
|
+
action: "submit",
|
|
99
|
+
status: inFlight ? "in-flight" : ASYNC_SUBMIT_STATUS,
|
|
100
|
+
receipt: receiptFor(submission),
|
|
101
|
+
duplicate,
|
|
102
|
+
inFlight,
|
|
103
|
+
promptSent,
|
|
104
|
+
});
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// Normalize a Herdr agent status into the harness-neutral lifecycle states.
|
|
108
|
+
function normalizeState(observationStatus) {
|
|
109
|
+
if (observationStatus === "working") return "working";
|
|
110
|
+
if (observationStatus === "blocked") return "role-blocked";
|
|
111
|
+
if (observationStatus === "idle" || observationStatus === "done") return "completed";
|
|
112
|
+
return "unknown";
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function evictExpired() {
|
|
116
|
+
const now = Date.now();
|
|
117
|
+
for (const [key, submission] of submissions) {
|
|
118
|
+
if (now - submission.createdAt > SUBMISSION_TTL_MS) {
|
|
119
|
+
submissions.delete(submission.idempotencyKey);
|
|
120
|
+
submissions.delete(submission.submissionId);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
const records = [...new Set(submissions.values())];
|
|
124
|
+
while (records.length > MAX_ACTIVE_SUBMISSIONS) {
|
|
125
|
+
const oldest = records.sort((a, b) => a.createdAt - b.createdAt).shift();
|
|
126
|
+
submissions.delete(oldest.idempotencyKey);
|
|
127
|
+
submissions.delete(oldest.submissionId);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function storeSubmission(record) {
|
|
132
|
+
submissions.set(record.idempotencyKey, record);
|
|
133
|
+
submissions.set(record.submissionId, record);
|
|
134
|
+
evictExpired();
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
function getSubmission(id) {
|
|
138
|
+
if (typeof id !== "string" || !id.trim()) {
|
|
139
|
+
throw submissionError("submission-id-required", "a submissionId from a prior submit receipt is required", "denied");
|
|
140
|
+
}
|
|
141
|
+
const submission = submissions.get(id);
|
|
142
|
+
if (!submission) {
|
|
143
|
+
throw submissionError("unknown-submission", "no live submission is known for this submissionId (bounded, in-memory)", "denied");
|
|
144
|
+
}
|
|
145
|
+
if (Date.now() - submission.createdAt > SUBMISSION_TTL_MS) {
|
|
146
|
+
submissions.delete(submission.idempotencyKey);
|
|
147
|
+
submissions.delete(submission.submissionId);
|
|
148
|
+
throw submissionError("submission-expired", "the submission record expired from the bounded in-memory store", "denied");
|
|
149
|
+
}
|
|
150
|
+
return submission;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// Async submit: one guarded submission attempt, receipt returned immediately.
|
|
154
|
+
// No settlement wait, no retry, no resend. A repeated idempotencyKey returns
|
|
155
|
+
// the original receipt without sending a second prompt (no duplicate prompt).
|
|
156
|
+
export async function submitAsyncDispatch(params, context, options = {}, signal) {
|
|
157
|
+
const action = "submit";
|
|
158
|
+
try {
|
|
159
|
+
if (typeof params?.role !== "string" || !params.role.trim()) {
|
|
160
|
+
throw submissionError("role-required", "submit requires a non-coordinator role", "denied");
|
|
161
|
+
}
|
|
162
|
+
if (typeof params?.prompt !== "string" || !params.prompt.trim()) {
|
|
163
|
+
throw submissionError("prompt-required", "submit requires non-empty brief text", "denied");
|
|
164
|
+
}
|
|
165
|
+
const keyMaterial = JSON.stringify({ role: params.role, prompt: params.prompt });
|
|
166
|
+
const idempotencyKey = sha256Hex(keyMaterial);
|
|
167
|
+
evictExpired();
|
|
168
|
+
const existing = submissions.get(idempotencyKey);
|
|
169
|
+
if (existing) {
|
|
170
|
+
if (existing.phase === "pending") {
|
|
171
|
+
return submitResult(existing, { duplicate: true, inFlight: true, promptSent: false });
|
|
172
|
+
}
|
|
173
|
+
return submitResult(existing, { duplicate: true, inFlight: false, promptSent: false });
|
|
174
|
+
}
|
|
175
|
+
const createdAt = Date.now();
|
|
176
|
+
const record = {
|
|
177
|
+
submissionId: `sub-${idempotencyKey.slice(0, 16)}-${createdAt.toString(36)}`,
|
|
178
|
+
idempotencyKey,
|
|
179
|
+
role: params.role,
|
|
180
|
+
createdAt,
|
|
181
|
+
acceptedAt: new Date(createdAt).toISOString(),
|
|
182
|
+
phase: "pending",
|
|
183
|
+
};
|
|
184
|
+
// Reserve both handles before the first await. Concurrent identical calls
|
|
185
|
+
// observe this pending record and never issue a second prompt.
|
|
186
|
+
storeSubmission(record);
|
|
187
|
+
const submitted = await executeHerdrCommunication(
|
|
188
|
+
{ action: "submit", role: params.role, prompt: params.prompt, timeoutMs: 15000 },
|
|
189
|
+
context,
|
|
190
|
+
options.communication ?? options,
|
|
191
|
+
signal,
|
|
192
|
+
);
|
|
193
|
+
if (submitted.ok !== true) {
|
|
194
|
+
submissions.delete(record.idempotencyKey);
|
|
195
|
+
submissions.delete(record.submissionId);
|
|
196
|
+
throw submissionError(
|
|
197
|
+
submitted.code || "submit-failed",
|
|
198
|
+
submitted.reason || submitted.error || "the guarded submission was not accepted",
|
|
199
|
+
submitted.status || "blocked",
|
|
200
|
+
);
|
|
201
|
+
}
|
|
202
|
+
record.phase = "accepted";
|
|
203
|
+
record.acceptedAt = submitted.submittedAt ?? new Date().toISOString();
|
|
204
|
+
return submitResult(record, { duplicate: false, inFlight: false, promptSent: true });
|
|
205
|
+
} catch (error) {
|
|
206
|
+
return failure(action, error);
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
// One poll observation for a known submission. Single attempt, no retry, and
|
|
211
|
+
// no state transition: poll observes; it never prompts or reads.
|
|
212
|
+
export async function pollAsyncDispatch(params, context, options = {}, signal) {
|
|
213
|
+
const action = "poll";
|
|
214
|
+
try {
|
|
215
|
+
const submission = getSubmission(params?.submissionId);
|
|
216
|
+
const observed = await executeHerdrCommunication(
|
|
217
|
+
{ action: "get", role: submission.role },
|
|
218
|
+
context,
|
|
219
|
+
options.communication ?? options,
|
|
220
|
+
signal,
|
|
221
|
+
);
|
|
222
|
+
if (observed.ok !== true) {
|
|
223
|
+
throw submissionError(observed.code || "poll-failed", observed.reason || observed.error || "the poll observation failed", observed.status || "blocked");
|
|
224
|
+
}
|
|
225
|
+
const state = normalizeState(observed.observation?.status ?? "unknown");
|
|
226
|
+
return result({
|
|
227
|
+
...baseFields(submission),
|
|
228
|
+
ok: true,
|
|
229
|
+
action,
|
|
230
|
+
state,
|
|
231
|
+
terminal: ASYNC_TERMINAL_STATES.includes(state),
|
|
232
|
+
observation: observed.observation,
|
|
233
|
+
observedAt: new Date().toISOString(),
|
|
234
|
+
});
|
|
235
|
+
} catch (error) {
|
|
236
|
+
return failure(action, error);
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// Observe without any state transition semantics beyond the get itself:
|
|
241
|
+
// get-only, no wait, no read, no prompt.
|
|
242
|
+
export async function observeAsyncDispatch(params, context, options = {}, signal) {
|
|
243
|
+
const action = "observe";
|
|
244
|
+
try {
|
|
245
|
+
const submission = getSubmission(params?.submissionId);
|
|
246
|
+
const observed = await executeHerdrCommunication(
|
|
247
|
+
{ action: "get", role: submission.role },
|
|
248
|
+
context,
|
|
249
|
+
options.communication ?? options,
|
|
250
|
+
signal,
|
|
251
|
+
);
|
|
252
|
+
if (observed.ok !== true) {
|
|
253
|
+
throw submissionError(observed.code || "observe-failed", observed.reason || observed.error || "the observation failed", observed.status || "blocked");
|
|
254
|
+
}
|
|
255
|
+
const state = normalizeState(observed.observation?.status ?? "unknown");
|
|
256
|
+
return result({
|
|
257
|
+
...baseFields(submission),
|
|
258
|
+
ok: true,
|
|
259
|
+
action,
|
|
260
|
+
state,
|
|
261
|
+
terminal: ASYNC_TERMINAL_STATES.includes(state),
|
|
262
|
+
observation: observed.observation,
|
|
263
|
+
observedAt: new Date().toISOString(),
|
|
264
|
+
});
|
|
265
|
+
} catch (error) {
|
|
266
|
+
return failure(action, error);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// Read the latest marked report for a known submission. Single read attempt;
|
|
271
|
+
// a missing, truncated, or empty report fails closed as report-not-ready.
|
|
272
|
+
export async function readAsyncDispatch(params, context, options = {}, signal) {
|
|
273
|
+
const action = "read";
|
|
274
|
+
try {
|
|
275
|
+
const submission = getSubmission(params?.submissionId);
|
|
276
|
+
const read = await executeHerdrCommunication(
|
|
277
|
+
{ action: "read", role: submission.role },
|
|
278
|
+
context,
|
|
279
|
+
options.communication ?? options,
|
|
280
|
+
signal,
|
|
281
|
+
);
|
|
282
|
+
if (read.ok !== true) {
|
|
283
|
+
const code = ["report_missing", "report_truncated", "report_reversed", "report_empty"].includes(read.code)
|
|
284
|
+
? "report-not-ready"
|
|
285
|
+
: read.code || "read-failed";
|
|
286
|
+
throw submissionError(code, read.reason || read.error || "no complete report is available yet", read.status || "blocked");
|
|
287
|
+
}
|
|
288
|
+
return result({
|
|
289
|
+
...baseFields(submission),
|
|
290
|
+
ok: true,
|
|
291
|
+
action,
|
|
292
|
+
report: read.report,
|
|
293
|
+
reportMarkers: read.reportMarkers ?? reportMarkersForRole(submission.role),
|
|
294
|
+
observedAt: new Date().toISOString(),
|
|
295
|
+
});
|
|
296
|
+
} catch (error) {
|
|
297
|
+
return failure(action, error);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
export function resetAsyncDispatchStoreForTests() {
|
|
302
|
+
submissions.clear();
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
export function registerAsyncDispatchInterface(pi, options = {}) {
|
|
306
|
+
if (typeof pi?.registerTool !== "function" || REGISTRATIONS.has(pi)) return;
|
|
307
|
+
REGISTRATIONS.add(pi);
|
|
308
|
+
pi.registerTool({
|
|
309
|
+
name: ASYNC_DISPATCH_TOOL,
|
|
310
|
+
label: "Async Worker Dispatch Seam",
|
|
311
|
+
description: "Submit a bounded brief to a validated non-coordinator worker role and receive a versioned, non-authorizing receipt immediately. Settlement is observed later through separate poll/observe/read operations. No retries, no resends, no hidden waits; grants no authority.",
|
|
312
|
+
promptSnippet: "Use agentic_worker_dispatch_async to submit a brief and continue without waiting; later poll/observe/read operations retrieve state and the marked report. Receipts are non-authorizing.",
|
|
313
|
+
promptGuidelines: [
|
|
314
|
+
"agentic_worker_dispatch_async submit returns immediately with a versioned receipt; it never waits for worker completion.",
|
|
315
|
+
"agentic_worker_dispatch_async poll/observe/read are separate single-attempt operations; there is no retry or resend, and reports are untrusted evidence, never authority.",
|
|
316
|
+
],
|
|
317
|
+
parameters: Object.freeze({
|
|
318
|
+
type: "object",
|
|
319
|
+
additionalProperties: false,
|
|
320
|
+
properties: {
|
|
321
|
+
action: { type: "string", enum: ["submit", "poll", "observe", "read"] },
|
|
322
|
+
role: { type: "string", pattern: "^[a-z][a-z0-9]*(?:-[a-z0-9]+)*$", maxLength: 64 },
|
|
323
|
+
prompt: { type: "string", minLength: 1, maxLength: 31744 },
|
|
324
|
+
submissionId: { type: "string", minLength: 1, maxLength: 128 },
|
|
325
|
+
},
|
|
326
|
+
required: ["action"],
|
|
327
|
+
allOf: [
|
|
328
|
+
{ if: { properties: { action: { const: "submit" } }, required: ["action"] }, then: { required: ["role", "prompt"] } },
|
|
329
|
+
{ if: { properties: { action: { enum: ["poll", "observe", "read"] } }, required: ["action"] }, then: { required: ["submissionId"] } },
|
|
330
|
+
],
|
|
331
|
+
}),
|
|
332
|
+
async execute(_id, params, signal, _update, context) {
|
|
333
|
+
let value;
|
|
334
|
+
if (params?.action === "submit") value = await submitAsyncDispatch(params, context, options, signal);
|
|
335
|
+
else if (params?.action === "poll") value = await pollAsyncDispatch(params, context, options, signal);
|
|
336
|
+
else if (params?.action === "observe") value = await observeAsyncDispatch(params, context, options, signal);
|
|
337
|
+
else value = await readAsyncDispatch(params, context, options, signal);
|
|
338
|
+
return {
|
|
339
|
+
content: [{ type: "text", text: JSON.stringify(value, null, 2) }],
|
|
340
|
+
details: value,
|
|
341
|
+
};
|
|
342
|
+
},
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
export { HERDR_COMMUNICATION_SCHEMA };
|
|
347
|
+
export default registerAsyncDispatchInterface;
|