@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.
Files changed (32) 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 +607 -97
  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 +23 -5
  31. package/skills/pulse/SKILL.md +6 -2
  32. package/skills/tasks/SKILL.md +4 -0
@@ -0,0 +1,11 @@
1
+ {
2
+ "schema": "agentic-driver.herdr-nudge-config.v1",
3
+ "revision": 1,
4
+ "nudge": {
5
+ "enabled": false,
6
+ "reasonAttemptWindowPolls": 12,
7
+ "backoffCycles": 2,
8
+ "backoffMaxCycles": 8,
9
+ "nudgeMaxBytes": 2048
10
+ }
11
+ }
@@ -0,0 +1,4 @@
1
+ #!/bin/sh
2
+ # Repository-local gate; preserve any existing hook chain before opting in.
3
+ repo=$(git rev-parse --show-toplevel) || exit 1
4
+ exec python3 "$repo/scripts/codebase_inventory_gate.py" --repo "$repo"
package/README.md CHANGED
@@ -13,7 +13,7 @@
13
13
  <img src="https://img.shields.io/badge/Python-3776AB?style=flat&logo=python&logoColor=white" alt="Python"/>
14
14
  </p>
15
15
 
16
- An agent without guardrails will rewrite code an existing abstraction already
16
+ An agent without guardrails will sometimes rewrite code an existing abstraction already
17
17
  covers. It will ship to the wrong remote. It will lose context and report
18
18
  success without evidence. Pi is deliberately lean by design: the harness is yours to
19
19
  shape. That philosophy is exactly what these extensions practice, they make them mine (and possibly yours too). They take
@@ -21,14 +21,14 @@ some concerns I consider worth addressing, and make them part of the
21
21
  harness.
22
22
 
23
23
  **pi-agentic-driver makes agent work verifiable and controllable. The agent does
24
- the work and each extension makes sure the work can be checked.** Review happens
25
- before code is written. Communication carries reports without asserting authority that isn't granted.
26
- Isolation proofs verify their own cleanup. Sessions survive compaction and
24
+ the work and each extension makes sure the work can be checked.** Reviewing happens
25
+ before code is written. Communication involves reports without asserting authority that isn't granted.
26
+ Isolation proofs exist to verify their own cleanup. Sessions survive compaction and
27
27
  Git operations stay exact, confirmed, and protected.
28
28
 
29
- Every capability passes fixture-based acceptance, native tests, live-session
30
- checks, and independent model review before release. I/we document each
31
- extension's restrictions before release, not after.
29
+ Features have different evidence levels: focused tests and independent review
30
+ support the shipped contracts, while live host-dependent proof is stated only
31
+ where it has actually run. A receipt or worker report is evidence, not approval.
32
32
 
33
33
  Extensions for [Pi](https://github.com/earendil-works/pi-coding-agent):
34
34
  advisory code review, controlled role communication, and governed isolation
@@ -43,24 +43,26 @@ proofs for agentic workflows.
43
43
  | Tool | What it does | Status |
44
44
  |------|--------------|--------|
45
45
  | `code_phage` | Reviews a plan against a stated goal before code is written and advises your agent. | shipped |
46
- | `tasks` | Keeps a session plan close to the work, then lets the owner make selected items durable in `TASKS.md`. | shipped |
47
- | `agentic_herdr_communication` | Exchanges marked reports with worker agents; never grants authority. | shipped |
46
+ | `tasks` | Keeps a priority-ordered session plan, then lets the owner make selected items durable in `TASKS.md`. | shipped |
47
+ | `agentic_herdr_communication` | Submits one prompt and observes delivery and marked reports; never grants authority. | shipped |
48
48
  | `agentic_herdr_spawn_worker` | Starts one Pi worker in a pane or tab, with native confirmation. | shipped |
49
49
  | `agentic_aidr` | A remedy for AI;DR. Reviews writing for clarity, simplicity, brevity, and humanity. | shipped |
50
50
  | `agentic_linux_microvm_cutover` | Runs one job in a throwaway QEMU/KVM virtual machine on a Linux host, with a severity-tiered killswitch that stops escape attempts. | user-enabled, native confirmation |
51
- | `agentic_worker_dispatch` | Runs controlled worker journeys and observes worker liveness. | shipped |
51
+ | `agentic_worker_dispatch` | Runs bounded worker journeys and observes worker liveness; recovery is not activated. | shipped |
52
52
  | `agentic_kanban_board` | Shows the workspace task board: lanes, flags, priorities, dependencies, and which cards can run. | shipped |
53
53
  | `agentic_kanban_board_write` | Adds cards to the board through the trusted writer, which records who authorized the work. | shipped |
54
54
  | `agentic_kanban_board_update` | Moves, closes, flags, edits, or removes cards on the board, always recording who authorized the change. | shipped |
55
55
  | `agentic_kanban_board_dispatch` | Claims an eligible board card for automated contained work and creates its assignment envelope. | shipped |
56
56
  | `agentic_kanban_pulse` | Checks for ready work and available capacity, then starts assignments under the board policy. | shipped |
57
+ | `/agentic-recovery-evidence` | Opens private recovery evidence in the local TUI; it is not a model-callable tool or recovery authorization. | shipped, local review only |
57
58
  | Router | Capacity/policy routing for Board Pulse: seats with scope statements, tunable policy values, and a binding dispatch gate. | shipped |
58
59
  | Janus (bundled service) | Local Jev evaluation service on loopback: semantic ranking, the dispatch-gate judgment, redaction before every external call. See `janus/README.md`. | shipped |
59
60
 
60
- **Status: active development and testing.** Each extension ships only after
61
- it passes fixture-based acceptance, native tests, live-session checks, and
62
- independent model review. You can install released components. Pending
63
- components are listed here for transparency and are not packaged.
61
+ **Status: active development and testing.** The disabled NUDGE classifier and
62
+ private evidence store are packaged foundations, not a running recovery
63
+ ladder. Herdr 0.9.1 has no proven modal-safe nudge channel. No terminal
64
+ `prompt` or `send-keys` fallback is enabled. Pending integrations below are
65
+ not presented as live features.
64
66
 
65
67
  ## Code review and planning
66
68
 
@@ -101,13 +103,13 @@ only, not a runtime dependency).
101
103
 
102
104
  Plans stay close to the work. They do not become board work by accident. The tasks skill gives Pi a familiar session list. It adds an owner-approved way to keep selected tasks in the durable `TASKS.md` board.
103
105
 
104
- - **Keep the working list simple.** Use `TaskCreate`, `TaskGet`, `TaskList`, and `TaskUpdate` as usual; the above-editor widget and footer pill stay live as tasks change.
106
+ - **Keep the working list simple.** Use `TaskCreate`, `TaskGet`, `TaskList`, and `TaskUpdate` as usual. Set an optional P0–P3 session priority; ranked open work appears first, unranked work keeps ID order, and completed work appears last. Priority does not authorize board dispatch. The widget and footer stay live as tasks change.
105
107
  - **Keep the Claude-like view.** `/tasks` provides descriptions, active forms, and internal-task markers without mutating the plan.
106
108
  - **Make durable work deliberate.** `TaskPromote` never runs automatically. It previews the exact cards and destination before the owner confirms.
107
109
  - **Keep authority clear.** The trusted board writer creates and updates `TASKS.md`. The skill does not bypass it.
108
110
  - **Make migration gentle.** Existing task files remain readable. Moving to the driver-owned skill does not strand current plans.
109
111
 
110
- The result is a clean handoff from a session plan to durable work. Selected tasks can survive the session, appear on the shared board, and become available for controlled dispatch when ready.
112
+ The result is a clean handoff from a session plan to durable work. Selected tasks can survive the session, appear on the shared board, and become available for controlled dispatch when ready. A session priority is not copied into a board card without an owner-approved board action.
111
113
 
112
114
  </details>
113
115
 
@@ -276,11 +278,15 @@ across versions rather than pinning one).
276
278
  - **List and observe.** Worker roles are filtered to trusted repositories: a
277
279
  checked-in registry plus canonical-path validation. Unlisted or
278
280
  symlink-escaped repositories are denied.
279
- - **Prompt exactly once.** The tool re-observes the role, sends one approved
280
- prompt with a role-specific report contract, waits for terminal settlement,
281
- and reads exactly one complete marked report. No retry, no target
282
- substitution, no resend on timeout.
283
- - **Wait and read.** The same trust checks apply to partial journeys.
281
+ - **Submit once and observe separately.** A prompt returns an immediate,
282
+ non-authorizing delivery receipt. The `delivery` action can report queued,
283
+ delivered, answered, failed, unknown, or unattributed. A marked answer is
284
+ attributed only to its own delivery boundary and exact correlation line;
285
+ missing or ambiguous evidence is never called success.
286
+ - **Wait and read.** The same trust checks apply to settlement and marked
287
+ reports. Unknown delivery never causes an automatic retry or resend.
288
+ Duplicate submissions within the bounded uncertainty window return the
289
+ existing delivery ID, not another handoff.
284
290
  - **Grant nothing.** Fixed argv, `shell: false`, a pinned executable, the
285
291
  coordinator role class denied. Results come back as untrusted evidence. The
286
292
  tool cannot control panes, start agents, run shells, or create authority.
@@ -383,9 +389,8 @@ starting revision, assigned branch, allowed paths, stopping point, expiry, and
383
389
  interaction profile.
384
390
 
385
391
  Blocked, altered, expired, reused, or unauthorized assignments fail closed.
386
- The journey validates the authenticated envelope before each consequential
387
- step. It runs on the assigned branch and consumes the envelope when the
388
- attempt ends.
392
+ Board Pulse handles board assignment with full route evidence. The session
393
+ worker journey does not claim board cards or mint board envelopes.
389
394
 
390
395
  Consuming an envelope releases the claim. It does not mark the card done.
391
396
 
@@ -579,10 +584,13 @@ end" or "don't wait for me" and the journey runs in autonomous mode. Say
579
584
  "do these two things" and it runs in the normal mode, pausing for you
580
585
  between steps. The mode is recorded in the report either way.
581
586
 
582
- Autonomous journeys can run work you dispatch directly or work claimed from
583
- a task board. Board-managed journeys use the card's authenticated assignment
584
- envelope and the board's automation policy. Direct journeys continue to work
585
- without a board.
587
+ `agentic_worker_dispatch` observes coordinator session tasks through a
588
+ read-only task store; dispatch no longer fails for lack of that store.
589
+ Interactive steps require native confirmation. Autonomous journeys follow
590
+ the session's existing autonomy semantics. Board cards in canonical
591
+ `TASKS.md` are assigned by Board Pulse and `agentic_kanban_board_dispatch`,
592
+ which own the route evidence needed for claims and envelopes. The session
593
+ journey never claims board cards.
586
594
 
587
595
  ### The cast
588
596
 
@@ -652,9 +660,13 @@ silently allowed.
652
660
  notes, governed checkpoint mutation, watchdog handoff, and fresh-session
653
661
  resumption that identifies goal, changed files, checks, and next step
654
662
  without executing anything.
655
- - **evidence ledger.** Deterministic evidence indexing, lossless source
656
- projection, universal checkpoint produce/store/recover, and run-ledger
657
- records with crash and corruption vectors tested.
663
+ - **recovery journey.** A private exact-evidence store and read-only TUI review
664
+ are packaged. Trusted journey authorization, safe handoff, durable unknown-
665
+ delivery lock, and a modal-safe nudge channel are still missing. No recovery
666
+ action is enabled by a stored hash, receipt, or display token.
667
+ - **broader evidence ledger.** Deterministic indexing, lossless projection,
668
+ checkpoint produce/store/recover, and tested multihost reconciliation remain
669
+ planned beyond the bounded private store.
658
670
  - **offline multihost evidence.** Record run evidence on each host while
659
671
  disconnected and reconcile it deterministically on reconnection, with no
660
672
  host as sole authority.
@@ -0,0 +1,23 @@
1
+ {
2
+ "$schema": "https://json-schema.org/draft/2020-12/schema",
3
+ "$id": "https://github.com/evoclock/pi-agentic-driver/blob/main/config/codebase-inventory.schema.json",
4
+ "title": "Codebase inventory file record v1",
5
+ "description": "One compact JSON object per tracked code file; JSONL lines sorted by repository-relative path. Static extraction is best-effort; this is a search aid, not an authorization source.",
6
+ "type": "object",
7
+ "additionalProperties": false,
8
+ "required": ["path", "subdir", "filename", "blob_sha", "loc", "size_bytes", "language", "functions", "classes", "dependencies", "exported_symbols"],
9
+ "properties": {
10
+ "path": {"type": "string", "minLength": 1, "description": "Repository-relative POSIX path"},
11
+ "subdir": {"type": "string"},
12
+ "filename": {"type": "string", "minLength": 1},
13
+ "blob_sha": {"type": "string", "pattern": "^[a-f0-9]{40}$", "description": "Git SHA-1 blob ID for the source bytes, even in SHA-256 repositories this v1 field denotes the SHA-1 blob computation"},
14
+ "loc": {"type": "integer", "minimum": 0},
15
+ "size_bytes": {"type": "integer", "minimum": 0},
16
+ "language": {"enum": ["python", "shell", "javascript", "typescript"]},
17
+ "functions": {"type": "array", "items": {"$ref": "#/$defs/named"}, "uniqueItems": true},
18
+ "classes": {"type": "array", "items": {"$ref": "#/$defs/named"}, "uniqueItems": true},
19
+ "dependencies": {"type": "array", "items": {"type": "string"}, "uniqueItems": true},
20
+ "exported_symbols": {"type": "array", "items": {"type": "string"}, "uniqueItems": true}
21
+ },
22
+ "$defs": {"named": {"type": "object", "required": ["name"], "additionalProperties": false, "properties": {"name": {"type": "string", "minLength": 1}}}}
23
+ }
@@ -0,0 +1,15 @@
1
+ # Codebase inventory
2
+
3
+ `pipeline_output/codebase_inventory.jsonl` is a searchable list of tracked code files. Before adding a module or function, search it for a filename or `"name":"symbol"`, then read any match. It is a discovery aid, not evidence that code is correct or safe.
4
+
5
+ ## Provenance and schema
6
+
7
+ The earlier Claude Code `cc_pre_commit_verify_hook.py` in `agent-infra` called `pre_commit_verify.py`, which called `codebase_inventory_gate.py` and refreshed the inventory on commit. The original Python/shell-only generator missed JavaScript repositories. A later `agentic-driver/scripts/codebase_inventory.py` added JS/TS recognition and richer per-file records. Among inspected existing inventories, `local-assist-model` has the most records (1,524 files), while `agentic-driver` has the most complete *fields* per record (path, filename, blob hash, lines, bytes, language, functions, classes, dependencies, exports). This repository's documented v1 subset follows the latter format and is defined in `config/codebase-inventory.schema.json`. The compact line layout intentionally supports exact `grep '"name":"symbol"'` lookups. Static JS/TS symbol detection is approximate and never executes source; Python uses `ast`, shell uses bounded patterns. No function bodies, secret-bearing values, or docstrings are copied into the inventory.
8
+
9
+ Every JSONL line is a file record sorted by repository-relative path. Its `blob_sha` is the SHA-1 Git blob ID of the working-tree file, allowing drift checks. Only Git-indexed Python, shell, JS, JSX, TS and TSX files (including `.mjs`, `.cjs`, `.mts` and Python/shell shebang scripts) are scanned. Generated/untracked files are not scanned. Empty symbol lists do not mean that a file is inert; dynamic exports can evade static patterns.
10
+
11
+ ## Refresh and commit gate
12
+
13
+ Run `python3 scripts/codebase_inventory.py` to regenerate the inventory. Review and stage both the code and `pipeline_output/codebase_inventory.jsonl`. `python3 scripts/codebase_inventory.py --check` compares without writing. `python3 scripts/codebase_inventory_gate.py` refreshes and blocks if inventoried source has unstaged edits or if the regenerated bytes differ from the staged inventory. It does not stage changes for you.
14
+
15
+ The repository ships an **opt-in Git pre-commit hook** at `.githooks/pre-commit` (no `cc_` prefix). If this repo has no existing hooks path, enable it with `git config --local core.hooksPath .githooks` and make the hook executable. **Do not replace an existing hook chain:** call `python3 scripts/codebase_inventory_gate.py` from that chain instead. Hook configuration is local and is not changed automatically by this package. No hook can intercept commits made with Git hooks disabled; this is a workflow freshness check, not a security boundary. The gate does not install dependencies or run code from inventoried files.
@@ -0,0 +1,9 @@
1
+ # Recovery evidence: store contract (Phase 2, first slice)
2
+
3
+ The driver owns the NUDGE ledger and its evidence format. A host supplies a canonical, owner-private local directory: Vogelkop will choose its app-data root and provide a review UI; the Pi host will supply its own trusted root. Neither host may replace the driver's evidence checks or treat a digest as dispatch authority. No default path is selected, and no current journey calls this module.
4
+
5
+ `scripts/enforcement/recovery_evidence_store_pi.js` stores exact, schema-validated NUDGE ledger entries in a dedicated SQLite file. The caller MUST persist a `requested` entry **before** attempting a nudge or replacement. The caller later records one terminal result; the store retains both immutable phases, links the terminal to the requested digest, and checks the SHA-256 hash, byte count, entry identity, phase, and predecessor when reading. A reference contains `{schema, journeyId, entryId, phase, sha256, bytes}`. It is a retrieval and integrity pointer, **not** proof of authorization, tamper-proof authentication, or evidence that an action was delivered. The owner-private file contains exact message text; it must not be sent unredacted to Jev or printed in a public report.
6
+
7
+ The owner-provided root must be absolute, canonical, non-symlinked, and private (mode 0700). The DB file must be regular, non-symlinked, and private (0600). Read-only open never creates storage. Writes serialize in SQLite transactions and fail on duplicates, invalid entries, per-record size, total evidence-byte quota, or record-count quota. No automatic eviction or deletion API exists; a full store blocks **new recovery actions** until the owner reviews capacity. A failed terminal write **after** an attempted action means the caller must retain its unknown-delivery identity lock and must not resend or replace blindly. This module does not implement that journey lock.
8
+
9
+ There is deliberately no retention timer yet. A host review/acknowledgment and retention policy must be specified before any deletion feature. A lost store cannot be reconstructed from its hash. An attacker able to rewrite both DB contents and hashes can forge non-authorizing evidence; filesystem permissions are the local privacy boundary, not a substitute for a signed audit service. The current 32 KiB journey report, its truncation behavior, Jev selection, Vogelkop UI, and N1 Herdr transport remain unchanged. Before journey wiring, add host-provided root and read-access control, receipt pointers with mandatory pinned fields, crash/ambiguous-delivery tests, and a verified owner review route.
@@ -1,7 +1,9 @@
1
1
  // SPDX-FileCopyrightText: 2026 Julen Gamboa <j.a.r.gamboa@gmail.com>
2
2
  // SPDX-License-Identifier: AGPL-3.0-only
3
3
 
4
- export default async function herdrDispatchPi(pi) {
4
+ import { createTaskStore } from "./task-store-adapter.ts";
5
+
6
+ async function herdrDispatchCore(pi) {
5
7
  const dispatch = await import(new URL("../scripts/enforcement/herdr_async_dispatch_pi.js", import.meta.url).href);
6
8
  const lifecycle = await import(new URL("../scripts/enforcement/herdr_lifecycle_pi.js", import.meta.url).href);
7
9
 
@@ -23,5 +25,16 @@ export default async function herdrDispatchPi(pi) {
23
25
  signal,
24
26
  );
25
27
 
26
- return dispatch.registerWorkerDispatchInterface(pi, { spawnReplacement });
28
+ // A fresh read-only session view per journey; registration remains keyed
29
+ // on the real Pi API identity for the enforcement module's dedupe guard.
30
+ return dispatch.registerWorkerDispatchInterface(pi, { spawnReplacement, taskStoreFactory: createTaskStore });
31
+ }
32
+
33
+ // Async transport seam (vogelkop phase #47): submit returns a versioned,
34
+ // non-authorizing receipt immediately; poll/observe/read are separate
35
+ // single-attempt operations. No retries, no resends, no authority.
36
+ export default async function herdrDispatchPi(pi) {
37
+ await herdrDispatchCore(pi);
38
+ const asyncSeam = await import(new URL("../scripts/enforcement/herdr_async_seam_pi.js", import.meta.url).href);
39
+ return asyncSeam.registerAsyncDispatchInterface(pi);
27
40
  }
@@ -0,0 +1,14 @@
1
+ // SPDX-FileCopyrightText: 2026 Julen Gamboa <j.a.r.gamboa@gmail.com>
2
+ // SPDX-License-Identifier: AGPL-3.0-only
3
+
4
+ // Thin extension wrapper for the owner-only recovery-evidence review command.
5
+ // Deliberately registers no model-callable tool: the full evidence surface is
6
+ // the interactive native TUI overlay only. The registration is async (the
7
+ // trusted root is resolved and snapshotted); Pi awaits an asynchronous
8
+ // default factory before startup continues (extensions.md), so no return
9
+ // value or extra await is needed here.
10
+ import { registerRecoveryEvidenceReview } from "../scripts/enforcement/recovery_evidence_review_pi.js";
11
+
12
+ export default async function registerRecoveryEvidenceReviewExtension(pi: any) {
13
+ await registerRecoveryEvidenceReview(pi);
14
+ }
@@ -0,0 +1,101 @@
1
+ // SPDX-FileCopyrightText: 2026 Julen Gamboa <j.a.r.gamboa@gmail.com>
2
+ // SPDX-License-Identifier: AGPL-3.0-only
3
+
4
+ import {
5
+ TASK_STATE_ENTRY, resolveTaskListId, tasksMirrorPath, loadSnapshotFile, validateSnapshot,
6
+ sessionTaskDisplayId, PROVENANCE_CAPTURE_STATUSES,
7
+ } from "../scripts/enforcement/session_tasks_core_pi.js";
8
+ import { createHash } from "node:crypto";
9
+
10
+ // Match TaskList's branch-first/high-water-mark observation in tasks.ts.
11
+ // The session mirror is a fallback; neither reader changes either source.
12
+ function observeSessionTasks(ctx) {
13
+ const id = resolveTaskListId({ session: ctx?.sessionManager?.getSessionId?.() ?? null });
14
+ let branch = null;
15
+ try {
16
+ const entries = ctx?.sessionManager?.getBranch?.() ?? [];
17
+ const snapshots = entries.filter((entry) => entry?.type === "custom" && entry?.customType === TASK_STATE_ENTRY);
18
+ const candidate = snapshots.at(-1)?.data;
19
+ if (validateSnapshot(candidate)) branch = candidate;
20
+ } catch { /* disk mirror remains available */ }
21
+ const disk = id ? loadSnapshotFile(tasksMirrorPath(id)) : null;
22
+ const fromDisk = (Number(disk?.highWaterMark) || 0) > (Number(branch?.highWaterMark) || 0);
23
+ const snapshot = fromDisk ? disk : branch ?? disk;
24
+ return { tasks: snapshot?.tasks ?? [], taskListId: id, source: fromDisk || (!branch && disk) ? "disk" : branch ? "branch" : "none" };
25
+ }
26
+
27
+ export function sessionTasks(ctx) {
28
+ return observeSessionTasks(ctx).tasks;
29
+ }
30
+
31
+ // Audit evidence only. A task-list environment override may select a different
32
+ // mirror; never attribute that mirror to the host session identity.
33
+ export function captureSessionTaskProvenance(ctx) {
34
+ const hostSessionId = ctx?.sessionManager?.getSessionId?.();
35
+ const observation = observeSessionTasks(ctx);
36
+ const source = { hostSessionId: typeof hostSessionId === "string" && hostSessionId.trim() ? hostSessionId : null,
37
+ taskListId: observation.taskListId, kind: observation.source };
38
+ if (!source.hostSessionId) return { status: "host-session-unavailable", source, tasks: [] };
39
+ if (source.taskListId !== source.hostSessionId) return { status: "task-list-mismatch", source, tasks: [] };
40
+ if (source.kind === "none") return { status: "source-unavailable", source, tasks: [] };
41
+ const capturedAt = new Date().toISOString();
42
+ const tasks = observation.tasks.map(({ id, subject, description }) => {
43
+ const material = { id, title: subject ?? null, description: description ?? null, capturedAt };
44
+ const canonical = JSON.stringify({ capturedAt, description: material.description, id, title: material.title });
45
+ return Object.freeze({ material: Object.freeze(material), digest: createHash("sha256").update(canonical, "utf8").digest("hex") });
46
+ });
47
+ return Object.freeze({ status: "captured", source: Object.freeze(source), tasks: Object.freeze(tasks) });
48
+ }
49
+
50
+ // Model-visible projection: task identity, deterministic digest, and source
51
+ // only. Raw titles and descriptions can carry private planning text or
52
+ // secrets, so they never leave the local capture used for revalidation.
53
+ export function projectSessionTaskProvenance(capture) {
54
+ const rawStatus = capture?.status;
55
+ const status = typeof rawStatus === "string" && PROVENANCE_CAPTURE_STATUSES.includes(rawStatus)
56
+ ? rawStatus
57
+ : "source-unavailable";
58
+ const tasks = Array.isArray(capture?.tasks) && status === "captured"
59
+ ? capture.tasks.map((entry) => ({ taskDisplayId: sessionTaskDisplayId(entry?.material?.id), digest: typeof entry?.digest === "string" ? entry.digest : null }))
60
+ : [];
61
+ return Object.freeze({
62
+ status,
63
+ source: capture?.source ? Object.freeze({
64
+ hostSessionId: capture.source.hostSessionId ?? null,
65
+ taskListId: capture.source.taskListId ?? null,
66
+ kind: capture.source.kind ?? "none",
67
+ }) : null,
68
+ tasks: Object.freeze(tasks),
69
+ });
70
+ }
71
+
72
+ export function revalidateSessionTaskProvenance(ctx, capture, taskId) {
73
+ if (capture?.status !== "captured") return { status: "capture-unavailable" };
74
+ const current = captureSessionTaskProvenance(ctx);
75
+ if (current.status !== "captured") return { status: current.status };
76
+ if (current.source.hostSessionId !== capture.source.hostSessionId || current.source.taskListId !== capture.source.taskListId
77
+ || current.source.kind !== capture.source.kind)
78
+ return { status: "identity-mismatch" };
79
+ const original = capture.tasks.find((entry) => entry.material.id === taskId);
80
+ const observed = current.tasks.find((entry) => entry.material.id === taskId);
81
+ if (!original || !observed) return { status: "task-missing" };
82
+ return { status: original.material.title === observed.material.title
83
+ && original.material.description === observed.material.description ? "matched" : "content-drift" };
84
+ }
85
+
86
+ // One instance per journey: observeAdvance only skips an already-dispatched
87
+ // head in this journey. It never updates TaskList or persists a completion.
88
+ export function createTaskStore(ctx) {
89
+ const skipped = new Set();
90
+ return {
91
+ list() {
92
+ return sessionTasks(ctx).filter((task) => !skipped.has(task.id)).map((task) => ({
93
+ id: task.id, status: task.status, blockedBy: [...(task.blockedBy ?? [])], owner: task.owner ?? null,
94
+ }));
95
+ },
96
+ observeAdvance(id) { skipped.add(id); },
97
+ captureProvenance() { return captureSessionTaskProvenance(ctx); },
98
+ projectProvenance(capture) { return projectSessionTaskProvenance(capture); },
99
+ revalidateProvenance(capture, id) { return revalidateSessionTaskProvenance(ctx, capture, id); },
100
+ };
101
+ }
@@ -26,7 +26,7 @@ import {
26
26
  buildPromotionPreview, buildBatchPromotionPreview,
27
27
  reconcileDependencies, applyDependencyEdges,
28
28
  promotionIdempotencyKey, forwardLinkFor, findExistingPromotedCard,
29
- resolveCallerOrigin, canPromote, hasTasksCapability,
29
+ resolveCallerOrigin, canPromote, hasTasksCapability, SESSION_TASK_PRIORITIES,
30
30
  } from "../scripts/enforcement/session_tasks_core_pi.js";
31
31
  import { writeCard, updateCard, parseBoard } from "../scripts/enforcement/task_board_core_pi.js";
32
32
 
@@ -69,6 +69,15 @@ export default async function tasksPi(pi) {
69
69
  const TASK_ICONS = Object.freeze({ pending: "▫", in_progress: "▪", completed: "✓" });
70
70
 
71
71
  const isVisible = (task) => task?.metadata?._internal !== true;
72
+ // Presentation only: priority never grants board claim or dispatch authority.
73
+ const orderedVisibleTasks = () => tasks.filter(isVisible).sort((a, b) => {
74
+ if (a.status === "completed" && b.status !== "completed") return 1;
75
+ if (b.status === "completed" && a.status !== "completed") return -1;
76
+ const rankA = SESSION_TASK_PRIORITIES.indexOf(a.priority);
77
+ const rankB = SESSION_TASK_PRIORITIES.indexOf(b.priority);
78
+ if (rankA !== rankB) return (rankA < 0 ? 4 : rankA) - (rankB < 0 ? 4 : rankB);
79
+ return Number(a.id) - Number(b.id) || a.id.localeCompare(b.id);
80
+ });
72
81
  const unresolvedTaskIds = () => new Set(
73
82
  tasks.filter((task) => task.status !== "completed").map((task) => task.id),
74
83
  );
@@ -77,6 +86,7 @@ export default async function tasksPi(pi) {
77
86
  const parts = [
78
87
  `${TASK_ICONS[task.status] ?? "?"} #${task.id}`,
79
88
  `[${task.status}]`,
89
+ ...(task.priority ? [`[${task.priority}]`] : []),
80
90
  task.subject,
81
91
  ];
82
92
  if (task.owner) parts.push(`(${task.owner})`);
@@ -88,7 +98,7 @@ export default async function tasksPi(pi) {
88
98
  };
89
99
 
90
100
  const renderTaskListLineForLLM = (task) => {
91
- const parts = [`#${task.id}`, `[${task.status}]`, task.subject];
101
+ const parts = [`#${task.id}`, `[${task.status}]`, ...(task.priority ? [`[${task.priority}]`] : []), task.subject];
92
102
  if (task.owner) parts.push(`(${task.owner})`);
93
103
  let line = parts.join(" ");
94
104
  const unresolved = unresolvedTaskIds();
@@ -109,6 +119,7 @@ export default async function tasksPi(pi) {
109
119
  const parts = [
110
120
  TASK_ICONS[task.status] ?? "?",
111
121
  `[${task.status}]`,
122
+ ...(task.priority ? [`[${task.priority}]`] : []),
112
123
  task.subject,
113
124
  ];
114
125
  if (task.owner) parts.push(`(${task.owner})`);
@@ -136,7 +147,7 @@ export default async function tasksPi(pi) {
136
147
  return;
137
148
  }
138
149
 
139
- const visible = tasks.filter(isVisible);
150
+ const visible = orderedVisibleTasks();
140
151
  try {
141
152
  if (typeof uiHost.ui.setWidget === "function") {
142
153
  if (visible.length === 0) {
@@ -150,9 +161,7 @@ export default async function tasksPi(pi) {
150
161
  const header = `Tasks ${counts.pending} pending · ${counts.in_progress} in progress · ${counts.completed} done`;
151
162
  const unresolved = unresolvedTaskIds();
152
163
  const theme = uiHost.ui.theme;
153
- const rows = [...visible]
154
- .sort((a, b) => Number(a.id) - Number(b.id) || a.id.localeCompare(b.id))
155
- .map((task) => renderWidgetTaskLine(theme, task, unresolved));
164
+ const rows = visible.map((task) => renderWidgetTaskLine(theme, task, unresolved));
156
165
  uiHost.ui.setWidget(WIDGET_KEY, [themed(theme, "fg", ["dim", header], header), ...rows], { placement: "aboveEditor" });
157
166
  }
158
167
  }
@@ -239,10 +248,14 @@ export default async function tasksPi(pi) {
239
248
  description: { type: "string", description: "What needs to be done" },
240
249
  activeForm: { type: "string", description: "Present continuous form shown in spinner when in_progress (e.g., \"Running tests\")" },
241
250
  metadata: { type: "object", description: "Arbitrary metadata to attach to the task", additionalProperties: true, properties: {} },
251
+ priority: { type: "string", enum: ["P0", "P1", "P2", "P3"], description: "Optional session-list display priority; does not affect board dispatch." },
242
252
  },
243
253
  required: ["subject", "description"],
244
254
  },
245
255
  async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
256
+ if (params.priority !== undefined && !SESSION_TASK_PRIORITIES.includes(params.priority)) {
257
+ return failResult("invalid-priority", "priority must be P0, P1, P2, or P3");
258
+ }
246
259
  highWaterMark += 1;
247
260
  let task = {
248
261
  id: String(highWaterMark),
@@ -253,6 +266,7 @@ export default async function tasksPi(pi) {
253
266
  blockedBy: [],
254
267
  ...(params.activeForm !== undefined ? { activeForm: params.activeForm } : {}),
255
268
  ...(params.metadata !== undefined ? { metadata: params.metadata } : {}),
269
+ ...(params.priority !== undefined ? { priority: params.priority } : {}),
256
270
  };
257
271
  task = stampOrigin(task);
258
272
  tasks.push(task);
@@ -273,7 +287,7 @@ export default async function tasksPi(pi) {
273
287
  async execute(_toolCallId, params) {
274
288
  const task = findTask(params.taskId);
275
289
  if (!task) return { content: [{ type: "text", text: "Task not found" }], details: { task: null } };
276
- const lines = [`Task #${task.id}: ${task.subject}`, `Status: ${task.status}`, `Description: ${task.description}`];
290
+ const lines = [`Task #${task.id}: ${task.subject}`, `Status: ${task.status}`, ...(task.priority ? [`Priority: ${task.priority}`] : []), `Description: ${task.description}`];
277
291
  if (task.blockedBy.length > 0) lines.push(`Blocked by: ${task.blockedBy.map((id) => `#${id}`).join(", ")}`);
278
292
  if (task.blocks.length > 0) lines.push(`Blocks: ${task.blocks.map((id) => `#${id}`).join(", ")}`);
279
293
  // picc-tasks v0.2.0 TaskGet details carry the restricted task shape
@@ -299,16 +313,17 @@ export default async function tasksPi(pi) {
299
313
  pi.registerTool({
300
314
  name: "TaskList",
301
315
  label: "TaskList",
302
- description: "Use this tool to list all tasks in the task list.\n\n## When to Use This Tool\n\n- To see what tasks are available to work on (status: 'pending', no owner, not blocked)\n- To check overall progress on the project\n- To find tasks that are blocked and need dependencies resolved\n- After completing a task, to check for newly unblocked work or claim the next available task\n- **Prefer working on tasks in ID order** (lowest ID first) when multiple tasks are available, as earlier tasks often set up context for later ones\n\n## Output\n\nReturns a summary of each task:\n- **id**: Task identifier (use with TaskGet, TaskUpdate)\n- **subject**: Brief description of the task\n- **status**: 'pending', 'in_progress', or 'completed'\n- **owner**: Agent ID if assigned, empty if available\n- **blockedBy**: List of open task IDs that must be resolved first (tasks with blockedBy cannot be claimed until dependencies resolve)\n\nUse TaskGet with a specific task ID to view full details including description and comments.",
316
+ description: "Use this tool to list all tasks in the task list.\n\n## When to Use This Tool\n\n- To see what tasks are available to work on (status: 'pending', no owner, not blocked)\n- To check overall progress on the project\n- To find tasks that are blocked and need dependencies resolved\n- After completing a task, to check for newly unblocked work or claim the next available task\n- Choose unblocked work by explicit P0–P3 priority, then stable ID order. Unranked tasks follow ranked tasks; completed tasks display last. Session priority never grants board or dispatch authority.\n\n## Output\n\nReturns a summary of each task:\n- **id**: Task identifier (use with TaskGet, TaskUpdate)\n- **subject**: Brief description of the task\n- **status**: 'pending', 'in_progress', or 'completed'\n- **priority**: P0–P3 when set; absent means unranked\n- **owner**: Agent ID if assigned, empty if available\n- **blockedBy**: List of open task IDs that must be resolved first (tasks with blockedBy cannot be claimed until dependencies resolve)\n\nUse TaskGet with a specific task ID to view full details including description and comments.",
303
317
  parameters: { type: "object", additionalProperties: false, properties: {} },
304
318
  async execute() {
305
- const visible = tasks.filter(isVisible);
319
+ const visible = orderedVisibleTasks();
306
320
  if (visible.length === 0) return { content: [{ type: "text", text: "No tasks found" }], details: { tasks: [] } };
307
321
  const unresolved = unresolvedTaskIds();
308
322
  const list = visible.map((t) => ({
309
323
  id: t.id,
310
324
  subject: t.subject,
311
325
  status: t.status,
326
+ ...(t.priority !== undefined ? { priority: t.priority } : {}),
312
327
  ...(t.owner !== undefined ? { owner: t.owner } : {}),
313
328
  blockedBy: t.blockedBy.filter((id) => unresolved.has(id)),
314
329
  }));
@@ -321,7 +336,7 @@ export default async function tasksPi(pi) {
321
336
  pi.registerTool({
322
337
  name: "TaskUpdate",
323
338
  label: "TaskUpdate",
324
- description: "Use this tool to update a task in the task list.\n\n## When to Use This Tool\n\n**Mark tasks as resolved:**\n- When you have completed the work described in a task\n- When a task is no longer needed or has been superseded\n- IMPORTANT: Always mark your assigned tasks as resolved when you finish them\n- After resolving, call TaskList to find your next task\n\n- ONLY mark a task as completed when you have FULLY accomplished it\n- If you encounter errors, blockers, or cannot finish, keep the task as in_progress\n- When blocked, create a new task describing what needs to be resolved\n- Never mark a task as completed if:\n - Tests are failing\n - Implementation is partial\n - You encountered unresolved errors\n - You couldn't find necessary files or dependencies\n\n**Delete tasks:**\n- When a task is no longer relevant or was created in error\n- Setting status to `deleted` permanently removes the task\n\n**Update task details:**\n- When requirements change or become clearer\n- When establishing dependencies between tasks\n\n## Fields You Can Update\n\n- **status**: The task status (see Status Workflow below)\n- **subject**: Change the task title (imperative form, e.g., \"Run tests\")\n- **description**: Change the task description\n- **activeForm**: Present continuous form shown in spinner when in_progress (e.g., \"Running tests\")\n- **owner**: Change the task owner (agent name)\n- **metadata**: Merge metadata keys into the task (set a key to null to delete it)\n- **addBlocks**: Mark tasks that cannot start until this one completes\n- **addBlockedBy**: Mark tasks that must complete before this one can start\n\n## Status Workflow\n\nStatus progresses: `pending` → `in_progress` → `completed`\n\nUse `deleted` to permanently remove a task.\n\n## Staleness\n\nMake sure to read a task's latest state using `TaskGet` before updating it.\n\n## Examples\n\nMark task as in progress when starting work:\n```json\n{\"taskId\": \"1\", \"status\": \"in_progress\"}\n```\n\nMark task as completed after finishing work:\n```json\n{\"taskId\": \"1\", \"status\": \"completed\"}\n```\n\nDelete a task:\n```json\n{\"taskId\": \"1\", \"status\": \"deleted\"}\n```\n\nClaim a task by setting owner:\n```json\n{\"taskId\": \"1\", \"owner\": \"my-name\"}\n```\n\nSet up task dependencies:\n```json\n{\"taskId\": \"2\", \"addBlockedBy\": [\"1\"]}\n```",
339
+ description: "Use this tool to update a task in the task list.\n\n## When to Use This Tool\n\n**Mark tasks as resolved:**\n- When you have completed the work described in a task\n- When a task is no longer needed or has been superseded\n- IMPORTANT: Always mark your assigned tasks as resolved when you finish them\n- After resolving, call TaskList to find your next task\n\n- ONLY mark a task as completed when you have FULLY accomplished it\n- If you encounter errors, blockers, or cannot finish, keep the task as in_progress\n- When blocked, create a new task describing what needs to be resolved\n- Never mark a task as completed if:\n - Tests are failing\n - Implementation is partial\n - You encountered unresolved errors\n - You couldn't find necessary files or dependencies\n\n**Delete tasks:**\n- When a task is no longer relevant or was created in error\n- Setting status to `deleted` permanently removes the task\n\n**Update task details:**\n- When requirements change or become clearer\n- When establishing dependencies between tasks\n\n## Fields You Can Update\n\n- **status**: The task status (see Status Workflow below)\n- **subject**: Change the task title (imperative form, e.g., \"Run tests\")\n- **description**: Change the task description\n- **activeForm**: Present continuous form shown in spinner when in_progress (e.g., \"Running tests\")\n- **owner**: Change the task owner (agent name)\n- **priority**: Set P0–P3, or null to clear session-list priority\n- **metadata**: Merge metadata keys into the task (set a key to null to delete it)\n- **addBlocks**: Mark tasks that cannot start until this one completes\n- **addBlockedBy**: Mark tasks that must complete before this one can start\n\n## Status Workflow\n\nStatus progresses: `pending` → `in_progress` → `completed`\n\nUse `deleted` to permanently remove a task.\n\n## Staleness\n\nMake sure to read a task's latest state using `TaskGet` before updating it.\n\n## Examples\n\nMark task as in progress when starting work:\n```json\n{\"taskId\": \"1\", \"status\": \"in_progress\"}\n```\n\nMark task as completed after finishing work:\n```json\n{\"taskId\": \"1\", \"status\": \"completed\"}\n```\n\nDelete a task:\n```json\n{\"taskId\": \"1\", \"status\": \"deleted\"}\n```\n\nClaim a task by setting owner:\n```json\n{\"taskId\": \"1\", \"owner\": \"my-name\"}\n```\n\nSet up task dependencies:\n```json\n{\"taskId\": \"2\", \"addBlockedBy\": [\"1\"]}\n```",
325
340
  parameters: {
326
341
  type: "object",
327
342
  additionalProperties: false,
@@ -331,6 +346,7 @@ export default async function tasksPi(pi) {
331
346
  description: { type: "string", description: "New description for the task" },
332
347
  activeForm: { type: "string", description: "Present continuous form shown in spinner when in_progress (e.g., \"Running tests\")" },
333
348
  owner: { type: "string", description: "New owner for the task" },
349
+ priority: { type: ["string", "null"], enum: ["P0", "P1", "P2", "P3", null], description: "Set P0–P3 display priority, or null to clear. No board authority." },
334
350
  metadata: { type: "object", description: "Metadata keys to merge into the task. Set a key to null to delete it.", additionalProperties: true, properties: {} },
335
351
  status: { type: "string", enum: ["pending", "in_progress", "completed", "deleted"], description: "New status for the task" },
336
352
  addBlocks: { type: "array", items: { type: "string" }, description: "Task IDs that this task blocks" },
@@ -344,12 +360,19 @@ export default async function tasksPi(pi) {
344
360
  return { content: [{ type: "text", text: "Task not found" }], details: { success: false, taskId: params.taskId, error: "Task not found", updatedFields: [] } };
345
361
  }
346
362
  const task = tasks[idx];
363
+ if (params.priority !== undefined && params.priority !== null && !SESSION_TASK_PRIORITIES.includes(params.priority)) {
364
+ return failResult("invalid-priority", "priority must be P0, P1, P2, P3, or null to clear");
365
+ }
347
366
  const updatedFields = [];
348
367
  let statusChange;
349
368
  if (params.subject !== undefined && params.subject !== task.subject) { task.subject = params.subject; updatedFields.push("subject"); }
350
369
  if (params.description !== undefined && params.description !== task.description) { task.description = params.description; updatedFields.push("description"); }
351
370
  if (params.activeForm !== undefined && params.activeForm !== task.activeForm) { task.activeForm = params.activeForm; updatedFields.push("activeForm"); }
352
371
  if (params.owner !== undefined && params.owner !== task.owner) { task.owner = params.owner; updatedFields.push("owner"); }
372
+ if (params.priority !== undefined && params.priority !== task.priority) {
373
+ if (params.priority === null) delete task.priority; else task.priority = params.priority;
374
+ updatedFields.push("priority");
375
+ }
353
376
  if (params.metadata !== undefined) {
354
377
  const merged = { ...(task.metadata ?? {}) };
355
378
  for (const [k, v] of Object.entries(params.metadata)) {
@@ -692,7 +715,7 @@ export default async function tasksPi(pi) {
692
715
  return;
693
716
  }
694
717
  const blocks = [];
695
- for (const task of tasks) {
718
+ for (const task of [...orderedVisibleTasks(), ...tasks.filter((task) => !isVisible(task))]) {
696
719
  const internal = task.metadata?._internal === true ? " [internal]" : "";
697
720
  blocks.push(`${renderTaskListLine(task)}${internal}`);
698
721
  blocks.push(` ${task.description}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@evoclock/pi-agentic-driver",
3
- "version": "0.10.1",
3
+ "version": "1.1.0",
4
4
  "description": "Guardrail extensions for Agentic Driver: advisory review, bounded Herdr communication, and guarded worker lifecycle.",
5
5
  "type": "module",
6
6
  "license": "AGPL-3.0-only",
@@ -42,6 +42,7 @@
42
42
  "extensions/code-phage.js",
43
43
  "extensions/herdr-communication.ts",
44
44
  "extensions/herdr-dispatch.ts",
45
+ "extensions/task-store-adapter.ts",
45
46
  "extensions/task-board.ts",
46
47
  "extensions/pulse.ts",
47
48
  "lib/adapters/diff-scope.mjs",
@@ -54,6 +55,18 @@
54
55
  "lib/typescript_ast_metrics.mjs",
55
56
  "scripts/enforcement/herdr_communication_pi.js",
56
57
  "scripts/enforcement/herdr_async_dispatch_pi.js",
58
+ "scripts/enforcement/herdr_async_seam_pi.js",
59
+ "scripts/enforcement/board_claim_bridge_pi.js",
60
+ "scripts/enforcement/nudge_classifier_pi.js",
61
+ "scripts/enforcement/nudge_config_pi.js",
62
+ "scripts/enforcement/nudge_ledger_pi.js",
63
+ "scripts/enforcement/recovery_evidence_store_pi.js",
64
+ "extensions/recovery-evidence.ts",
65
+ "scripts/enforcement/recovery_evidence_review_pi.js",
66
+ "docs/recovery-evidence-store.md",
67
+ "scripts/enforcement/nudge_reason_state_pi.js",
68
+ "scripts/enforcement/nudge_validate_pi.js",
69
+ ".agentic-driver/nudge.defaults.json",
57
70
  "README.md",
58
71
  "LICENSE",
59
72
  "extensions/herdr-lifecycle.ts",
@@ -69,6 +82,11 @@
69
82
  "PROVENANCE.md",
70
83
  "extensions/aidr.ts",
71
84
  "scripts/aidr_writing_review.js",
85
+ "scripts/codebase_inventory.py",
86
+ "scripts/codebase_inventory_gate.py",
87
+ "config/codebase-inventory.schema.json",
88
+ "docs/codebase-inventory.md",
89
+ ".githooks/pre-commit",
72
90
  "templates/AGENTS.md",
73
91
  "scripts/enforcement/task_board_core_pi.js",
74
92
  "scripts/enforcement/pulse_core_pi.js",