@evoclock/pi-agentic-driver 0.10.1 → 1.1.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 +607 -97
- 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 +23 -5
- package/skills/pulse/SKILL.md +6 -2
- package/skills/tasks/SKILL.md +4 -0
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
// SPDX-FileCopyrightText: 2026 Julen Gamboa <j.a.r.gamboa@gmail.com>
|
|
2
|
+
// SPDX-License-Identifier: AGPL-3.0-only
|
|
3
|
+
|
|
4
|
+
import { NUDGE_STUCK_REASONS } from "./nudge_classifier_pi.js";
|
|
5
|
+
import {
|
|
6
|
+
TRUSTED_HERDR_EXECUTABLE,
|
|
7
|
+
resolveTrustedHerdrExecutable,
|
|
8
|
+
trustedHerdrExecutableRealPath,
|
|
9
|
+
} from "./herdr_communication_pi.js";
|
|
10
|
+
|
|
11
|
+
// NUDGE §7 config schema and validation (SPEC_TASK43_NUDGE.md v3.3.1,
|
|
12
|
+
// rule 30). Pure module: schema constants, the closed validator, and the
|
|
13
|
+
// runtime-merged-config loader shape. The `nudge validate` CLI lives in
|
|
14
|
+
// nudge_validate_pi.js; this module holds everything it checks so tests can
|
|
15
|
+
// exercise the exact runtime rules without spawning a process.
|
|
16
|
+
|
|
17
|
+
export const NUDGE_CONFIG_SCHEMA = "agentic-driver.herdr-nudge-config.v1";
|
|
18
|
+
export const NUDGE_CONFIG_REVISION = 1;
|
|
19
|
+
|
|
20
|
+
// All keys are REQUIRED; exactKeys closed (§7). `stallPolls` lives solely in
|
|
21
|
+
// #41's dispatch config — duplicate ownership here is invalid.
|
|
22
|
+
export const NUDGE_REQUIRED_KEYS = Object.freeze([
|
|
23
|
+
"enabled",
|
|
24
|
+
"reasonAttemptWindowPolls",
|
|
25
|
+
"backoffCycles",
|
|
26
|
+
"backoffMaxCycles",
|
|
27
|
+
"nudgeMaxBytes",
|
|
28
|
+
]);
|
|
29
|
+
|
|
30
|
+
// Removed count-cap keys (v3.3): the budget is the closed reason taxonomy ×
|
|
31
|
+
// ladder depth × observation window × same-reason replacement loop guard,
|
|
32
|
+
// never a numeric cap. Their presence fails exact-key validation.
|
|
33
|
+
export const NUDGE_REMOVED_KEYS = Object.freeze([
|
|
34
|
+
"maxNudgesPerJourney",
|
|
35
|
+
"notifyAfterNudges",
|
|
36
|
+
"replaceAfterNudges",
|
|
37
|
+
"maxReplacementsPerJourney",
|
|
38
|
+
]);
|
|
39
|
+
|
|
40
|
+
// MAX_PROMPT_BYTES is owned by herdr communication (communication line 42);
|
|
41
|
+
// nudgeMaxBytes is bounded by it.
|
|
42
|
+
export const HERDR_MAX_PROMPT_BYTES = 32 * 1024;
|
|
43
|
+
|
|
44
|
+
// Re-export the classifier-owned closed taxonomy for config consumers.
|
|
45
|
+
export { NUDGE_STUCK_REASONS };
|
|
46
|
+
const CONFIG_SUPPORTED_STUCK_REASONS = Object.freeze([
|
|
47
|
+
"blocked-modal", "working-sequence-frozen", "status-unknown", "done-report-unavailable",
|
|
48
|
+
]);
|
|
49
|
+
|
|
50
|
+
function isNonNegativeSafeInteger(value) {
|
|
51
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 0;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function isPositiveSafeInteger(value) {
|
|
55
|
+
return typeof value === "number" && Number.isSafeInteger(value) && value >= 1;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Validate the merged nudge config exactly as the runtime would consume it.
|
|
59
|
+
// Returns {ok, errors, operational}. `operational` is false when the schema
|
|
60
|
+
// is valid but the required N1 upstream capability is not yet verified
|
|
61
|
+
// (§7: a valid schema with a missing required N1 capability is reported
|
|
62
|
+
// non-operational and exits nonzero).
|
|
63
|
+
export function validateNudgeConfig(config, { upstreamCapabilityVerified = null, maxPollsPerSubmit = null, stallPolls = null } = {}) {
|
|
64
|
+
const errors = [];
|
|
65
|
+
if (config === null || typeof config !== "object" || Array.isArray(config)) {
|
|
66
|
+
return { ok: false, operational: false, errors: ["config: must be an object"] };
|
|
67
|
+
}
|
|
68
|
+
if (config.schema !== NUDGE_CONFIG_SCHEMA) {
|
|
69
|
+
errors.push(`schema: must be "${NUDGE_CONFIG_SCHEMA}" (got ${JSON.stringify(config.schema ?? null)})`);
|
|
70
|
+
}
|
|
71
|
+
if (config.revision !== NUDGE_CONFIG_REVISION) {
|
|
72
|
+
errors.push(`revision: must be ${NUDGE_CONFIG_REVISION} (got ${JSON.stringify(config.revision ?? null)})`);
|
|
73
|
+
}
|
|
74
|
+
const nudge = config.nudge;
|
|
75
|
+
if (nudge === null || typeof nudge !== "object" || Array.isArray(nudge)) {
|
|
76
|
+
errors.push("nudge: REQUIRED section missing — " + NUDGE_REQUIRED_KEYS.join(", ") + " must be set (fail closed)");
|
|
77
|
+
return { ok: false, operational: false, errors };
|
|
78
|
+
}
|
|
79
|
+
// exactKeys closed: every REQUIRED key present, no unknown key, and the
|
|
80
|
+
// removed count-cap keys are rejected by name so the operator sees why.
|
|
81
|
+
for (const key of NUDGE_REQUIRED_KEYS) {
|
|
82
|
+
if (nudge[key] === undefined) {
|
|
83
|
+
errors.push(`nudge.${key}: REQUIRED key absent (fail closed)`);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
for (const key of Object.keys(nudge)) {
|
|
87
|
+
if (!NUDGE_REQUIRED_KEYS.includes(key)) {
|
|
88
|
+
const removed = NUDGE_REMOVED_KEYS.includes(key);
|
|
89
|
+
errors.push(`nudge.${key}: ${removed
|
|
90
|
+
? "removed count-cap key — the budget is the closed reason taxonomy × ladder depth × observation window × same-reason replacement loop guard, never a numeric cap (NUDGE §7)"
|
|
91
|
+
: "key outside the closed nudge config shape"}`);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const enabled = nudge.enabled;
|
|
96
|
+
if (enabled !== undefined && typeof enabled !== "boolean") {
|
|
97
|
+
errors.push("nudge.enabled: must be a boolean");
|
|
98
|
+
}
|
|
99
|
+
const window = nudge.reasonAttemptWindowPolls;
|
|
100
|
+
if (window !== undefined && !isPositiveSafeInteger(window)) {
|
|
101
|
+
errors.push("nudge.reasonAttemptWindowPolls: must be a positive safe integer (the bounded observation window for one persistent reason)");
|
|
102
|
+
}
|
|
103
|
+
const backoffCycles = nudge.backoffCycles;
|
|
104
|
+
if (backoffCycles !== undefined && !isNonNegativeSafeInteger(backoffCycles)) {
|
|
105
|
+
errors.push("nudge.backoffCycles: must be a safe integer >= 0");
|
|
106
|
+
}
|
|
107
|
+
const backoffMaxCycles = nudge.backoffMaxCycles;
|
|
108
|
+
if (backoffMaxCycles !== undefined && !isPositiveSafeInteger(backoffMaxCycles)) {
|
|
109
|
+
errors.push("nudge.backoffMaxCycles: must be a positive safe integer");
|
|
110
|
+
}
|
|
111
|
+
if (backoffCycles !== undefined && backoffMaxCycles !== undefined
|
|
112
|
+
&& isNonNegativeSafeInteger(backoffCycles) && isPositiveSafeInteger(backoffMaxCycles)
|
|
113
|
+
&& backoffCycles > backoffMaxCycles) {
|
|
114
|
+
errors.push("nudge.backoffCycles: must be <= nudge.backoffMaxCycles");
|
|
115
|
+
}
|
|
116
|
+
const nudgeMaxBytes = nudge.nudgeMaxBytes;
|
|
117
|
+
if (nudgeMaxBytes !== undefined) {
|
|
118
|
+
if (!isPositiveSafeInteger(nudgeMaxBytes)) {
|
|
119
|
+
errors.push("nudge.nudgeMaxBytes: must be a positive safe integer");
|
|
120
|
+
} else if (nudgeMaxBytes > HERDR_MAX_PROMPT_BYTES) {
|
|
121
|
+
errors.push(`nudge.nudgeMaxBytes: must be <= MAX_PROMPT_BYTES (${HERDR_MAX_PROMPT_BYTES})`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Cross-surface checks (§7 + SEAM §7): the poll bound owns the outer hard
|
|
126
|
+
// bound; the nudge window must fit inside it with room for at least one
|
|
127
|
+
// backoff/observation cycle. stallPolls must have exactly one owner.
|
|
128
|
+
if (maxPollsPerSubmit !== null) {
|
|
129
|
+
if (!isPositiveSafeInteger(maxPollsPerSubmit)) {
|
|
130
|
+
errors.push("cross-surface: maxPollsPerSubmit must be a positive safe integer when supplied");
|
|
131
|
+
} else if (isPositiveSafeInteger(window) && window > maxPollsPerSubmit) {
|
|
132
|
+
errors.push(`cross-surface: nudge.reasonAttemptWindowPolls (${window}) must be <= dispatch poll.maxPollsPerSubmit (${maxPollsPerSubmit})`);
|
|
133
|
+
} else if (isPositiveSafeInteger(window) && window >= maxPollsPerSubmit) {
|
|
134
|
+
errors.push("cross-surface: the observation window leaves no room for even one backoff/observation cycle inside maxPollsPerSubmit");
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (stallPolls !== null && !isPositiveSafeInteger(stallPolls)) {
|
|
138
|
+
errors.push("cross-surface: stallPolls must be a positive safe integer when supplied");
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
// Cross-module pin: config supports exactly the classifier-owned reasons.
|
|
142
|
+
const unsupportedReasons = NUDGE_STUCK_REASONS.filter(
|
|
143
|
+
(reason) => !CONFIG_SUPPORTED_STUCK_REASONS.includes(reason),
|
|
144
|
+
);
|
|
145
|
+
const missingReasons = CONFIG_SUPPORTED_STUCK_REASONS.filter(
|
|
146
|
+
(reason) => !NUDGE_STUCK_REASONS.includes(reason),
|
|
147
|
+
);
|
|
148
|
+
if (unsupportedReasons.length > 0 || missingReasons.length > 0) {
|
|
149
|
+
errors.push(`reason enum: classifier/config taxonomy mismatch (unsupported=${unsupportedReasons.join(",") || "none"}; missing=${missingReasons.join(",") || "none"})`);
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
// Operational readiness: a valid schema with a missing required N1
|
|
153
|
+
// capability is reported non-operational (§7). The upstream herdr nudge
|
|
154
|
+
// verb has not shipped, so the default is non-operational until the
|
|
155
|
+
// capability probe and modal-isolation fixtures pass.
|
|
156
|
+
const operational = errors.length === 0
|
|
157
|
+
&& (upstreamCapabilityVerified === null ? false : upstreamCapabilityVerified === true)
|
|
158
|
+
&& enabled === true;
|
|
159
|
+
|
|
160
|
+
return { ok: errors.length === 0, operational, errors };
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
// ---------------------------------------------------------------------------
|
|
164
|
+
// 8. Upstream nudge-capability probe seam (NUDGE §3, §7; SEAM Part 2.11).
|
|
165
|
+
// The real `herdr agent nudge` verb arrives with the upstream capability.
|
|
166
|
+
// Until then this probe fails closed: it reports verified=false unless the
|
|
167
|
+
// transport itself proves the capability. `nudge validate` (and the
|
|
168
|
+
// runtime enablement path) consume this single seam — there is no second
|
|
169
|
+
// place where the capability verdict is invented. The manual
|
|
170
|
+
// `upstreamCapabilityVerified` input is a caller assertion, never a
|
|
171
|
+
// substitute for this probe.
|
|
172
|
+
// ---------------------------------------------------------------------
|
|
173
|
+
// The probe runs the trusted herdr binary (never PATH-resolved) with the
|
|
174
|
+
// nudge help verb and checks the pinned capability marker in the output.
|
|
175
|
+
// Any failure — missing binary, untrusted/symlinked path, non-zero exit,
|
|
176
|
+
// missing marker — means the capability is NOT verified. The executable
|
|
177
|
+
// goes through the SAME realpath/Cellar trust validation the runtime uses
|
|
178
|
+
// (herdr_communication_pi.js), so the read-only probe can never execute an
|
|
179
|
+
// untrusted path. A `runProcess` seam may be injected for tests (a fake
|
|
180
|
+
// process needs no binary); production probing uses the trusted executable.
|
|
181
|
+
// ---------------------------------------------------------------------------
|
|
182
|
+
|
|
183
|
+
export const NUDGE_CAPABILITY_PROBE_MARKER = "agent nudge";
|
|
184
|
+
|
|
185
|
+
// Resolve and trust-validate the executable the probe will run. The production
|
|
186
|
+
// constant is resolved through the runtime's own entry point (platform gate +
|
|
187
|
+
// realpath/Cellar trust). A caller-supplied path (tests) receives the exact
|
|
188
|
+
// same realpath/regular-file/executable/Cellar validation, so an untrusted or
|
|
189
|
+
// symlinked executable fails closed just as it would in production.
|
|
190
|
+
function trustedProbeExecutable(candidate) {
|
|
191
|
+
if (candidate === TRUSTED_HERDR_EXECUTABLE) return resolveTrustedHerdrExecutable({});
|
|
192
|
+
trustedHerdrExecutableRealPath(candidate);
|
|
193
|
+
return candidate;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export async function probeNudgeUpstreamCapability({ runProcess = null, executable = null } = {}) {
|
|
197
|
+
// No probe seam and no trusted binary: the capability is unverified. This
|
|
198
|
+
// is the expected state until the upstream capability ships.
|
|
199
|
+
const probeRun = typeof runProcess === "function" ? runProcess : null;
|
|
200
|
+
const probeExecutable = typeof executable === "string" && executable.trim() ? executable : null;
|
|
201
|
+
if (probeRun === null && probeExecutable === null) {
|
|
202
|
+
return { verified: false, reason: "no nudge-capability probe seam or trusted executable is available; the upstream herdr nudge verb is not verified" };
|
|
203
|
+
}
|
|
204
|
+
// Fail closed BEFORE any execution when the executable does not pass the
|
|
205
|
+
// runtime's trusted-executable validation.
|
|
206
|
+
let trustedExecutable = null;
|
|
207
|
+
if (probeRun === null) {
|
|
208
|
+
try {
|
|
209
|
+
trustedExecutable = trustedProbeExecutable(probeExecutable);
|
|
210
|
+
} catch (error) {
|
|
211
|
+
return { verified: false, reason: `the nudge-capability probe refused an untrusted executable: ${String(error?.message || error).slice(0, 200)}` };
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
const { execFileSync } = await import("node:child_process");
|
|
215
|
+
const argv = ["agent", "nudge", "--help"];
|
|
216
|
+
let stdout = "";
|
|
217
|
+
try {
|
|
218
|
+
if (probeRun !== null) {
|
|
219
|
+
const result = await probeRun({ argv, shell: false });
|
|
220
|
+
if (!result || result.code !== 0) {
|
|
221
|
+
return { verified: false, reason: `the nudge-capability probe exited non-zero (${result?.code ?? "unknown"})` };
|
|
222
|
+
}
|
|
223
|
+
stdout = String(result.stdout ?? "");
|
|
224
|
+
} else {
|
|
225
|
+
stdout = execFileSync(trustedExecutable, argv, { encoding: "utf8", timeout: 5_000, shell: false });
|
|
226
|
+
}
|
|
227
|
+
} catch (error) {
|
|
228
|
+
return { verified: false, reason: `the nudge-capability probe failed: ${String(error?.message || error).slice(0, 200)}` };
|
|
229
|
+
}
|
|
230
|
+
if (!stdout.includes(NUDGE_CAPABILITY_PROBE_MARKER)) {
|
|
231
|
+
return { verified: false, reason: "the trusted herdr binary does not advertise the agent nudge verb (capability marker absent)" };
|
|
232
|
+
}
|
|
233
|
+
return { verified: true, reason: "the trusted herdr binary advertises the agent nudge verb" };
|
|
234
|
+
}
|