@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.
Files changed (30) hide show
  1. package/.agentic-driver/nudge.defaults.json +11 -0
  2. package/.githooks/pre-commit +4 -0
  3. package/README.md +43 -31
  4. package/config/codebase-inventory.schema.json +23 -0
  5. package/docs/codebase-inventory.md +15 -0
  6. package/docs/recovery-evidence-store.md +9 -0
  7. package/extensions/herdr-dispatch.ts +15 -2
  8. package/extensions/recovery-evidence.ts +14 -0
  9. package/extensions/task-store-adapter.ts +101 -0
  10. package/extensions/tasks.ts +34 -11
  11. package/package.json +19 -1
  12. package/scripts/codebase_inventory.py +105 -0
  13. package/scripts/codebase_inventory_gate.py +48 -0
  14. package/scripts/enforcement/board_claim_bridge_pi.js +204 -0
  15. package/scripts/enforcement/herdr_async_dispatch_pi.js +83 -30
  16. package/scripts/enforcement/herdr_async_seam_pi.js +347 -0
  17. package/scripts/enforcement/herdr_communication_pi.js +608 -95
  18. package/scripts/enforcement/herdr_lifecycle_pi.js +266 -13
  19. package/scripts/enforcement/linux_microvm_remote_fixture.sh +6 -6
  20. package/scripts/enforcement/native_tui_context.js +16 -0
  21. package/scripts/enforcement/nudge_classifier_pi.js +176 -0
  22. package/scripts/enforcement/nudge_config_pi.js +234 -0
  23. package/scripts/enforcement/nudge_ledger_pi.js +514 -0
  24. package/scripts/enforcement/nudge_reason_state_pi.js +135 -0
  25. package/scripts/enforcement/nudge_validate_pi.js +224 -0
  26. package/scripts/enforcement/pulse_scheduler_pi.js +15 -3
  27. package/scripts/enforcement/recovery_evidence_review_pi.js +269 -0
  28. package/scripts/enforcement/recovery_evidence_store_pi.js +170 -0
  29. package/scripts/enforcement/session_tasks_core_pi.js +14 -0
  30. package/scripts/enforcement/task_board_core_pi.js +96 -7
@@ -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
+ }