Files
pi-native-hooks/docs/implementation-plan.md

657 lines
46 KiB
Markdown

# pi-native-hooks implementation plan
## Status
**Approved on 27 August 2026 and implemented as the repository baseline.** Activation remains a separate explicit user-authorized step; the package is not installed and live personal configuration is not created by implementation or tests.
## Purpose
Build one native Pi extension that runs trusted personal policies after relevant workspace mutations. The first policies will:
1. detect TypeScript non-null assertions and dynamic imports with AST-backed ast-grep rules, then append concise steering diagnostics to the mutation execution wave's tool results;
2. track exact required verification commands and prevent Pi from remaining settled while their evidence is missing or stale; and
3. provide an agent-callable, reasoned waiver when verification is intentionally skipped.
The extension will also establish a small toolkit and authoring skill for adding focused policies without duplicating Pi lifecycle, analyzer execution, state, or delivery logic.
## Decisions already made
- Implement a native TypeScript Pi extension with `pi.on(...)`, not `pi-yaml-hooks`, `pi-steering`, or `pi-workflow`.
- Build one standalone repository and Pi package named `pi-native-hooks`, with one extension of the same name.
- Install the package from `/srv/data/code/jb-pi/pi-native-hooks` as a Pi local-path package during development; do not depend on harness-tools link management.
- Load personal hook policies as trusted TypeScript modules from `~/.config/pi-native-hooks/`.
- Use ast-grep rule packs for syntax diagnostics.
- Execute an analyzer once per overlapping mutation wave and analyzer configuration, then fan an immutable normalized result out to every interested policy. Individual policy modules must not spawn AST, lint, or LSP processes through the runtime interface.
- Require exact configured verification commands. A focused, filtered, combined, or otherwise altered command does not satisfy an exact requirement unless separately configured.
- Treat `agent_settled` as Pi's strongest available completion candidate. If armed verification is pending, inject a `followUp` custom message with `triggerTurn: true`.
- Arm verification only after this session observes a relevant mutation or restores an already-armed branch state. A new session treats pre-existing dirty files as its baseline rather than forcing checks during question-only work.
- Accept foreground verification when Pi reports `event.isError === false`. Document the narrow limitation that Pi's public bash result omits the exit code and can be ambiguous if the shell process itself ends through an external signal.
- Allow an agent-callable waiver tool. A waiver must contain a non-empty reason, is scoped to the requirement revisions and outstanding checks it clears, is persisted, and becomes stale after the next relevant mutation.
- Keep command sandboxing, destructive-command policy, permissions, and process isolation out of scope. nono profiles own those controls.
- Treat personal policy modules as executable in-process code with Pi's authority. Load only user-owned configuration from `~/.config/pi-native-hooks`; do not load repository-local policy code.
- Use Node's native erasable-TypeScript support on the Pi-supported Node `>=22.19` runtime. Avoid adding a TypeScript loader dependency unless the Phase 0 proof shows native loading is insufficient.
- Discover policies only at session startup or `/reload`; do not add a file watcher in the MVP.
## Outcomes
The first release is successful when:
- an overlapping parallel mutation wave produces at most one ast-grep process;
- both initial TypeScript policies receive the same normalized analysis snapshot;
- diagnostics identify rule, file, line, guidance, and the supported exception form;
- repeated unchanged findings do not repeatedly consume model context;
- writes made by `write`, `edit`, `bash`, and unknown custom tools are detected through workspace state rather than command-name guesses alone;
- each armed required check is valid only after it fully completes successfully at or after its current required revision;
- a foreground exact `bash` command can provide evidence through Pi's finalized success state;
- Pi is forced into a follow-up turn while required evidence is pending, until checks pass or the agent records a waiver;
- state survives session reload/resume without treating stale evidence as current;
- analyzer failures are visible but do not corrupt successful tool results;
- armed verification uncertainty fails closed to a pending gate;
- the hook-authoring skill can create a typed personal policy and focused fixture using the shared toolkit;
- policy load errors, ownership failures, and analyzer degradation are observable without crashing Pi; and
- the local-path Pi package exposes exactly one `pi-native-hooks` extension and the `native-hook-authoring` skill.
## Boundaries
### In scope
- Pi lifecycle coordination for overlapping tool execution waves, messages, settlement, and session state.
- Git-aware workspace snapshots and changed-path discovery.
- A shared analyzer planner and ast-grep adapter.
- Typed normalized findings and structurally validated diagnostic policy modules.
- Tool-result diagnostic augmentation, deduplication, status, and `/hooks-status` observability.
- Exact foreground `bash` verification evidence.
- Forced completion follow-ups and a reasoned waiver tool.
- Initial personal TypeScript diagnostic policies and verification policies for `pi-native-hooks` and `harness-tools`.
- A package-managed hook-authoring skill, policy type declarations, initializer/doctor commands, and local-path installation documentation.
### Deferred
- Implementations of lint and LSP adapters. The coordinator contract must support them, but the MVP implements only ast-grep.
- Async/background verification adapters. The removed `pi-background-tasks` package is not a runtime dependency or integration target.
- Repository-local or remotely downloaded executable policy modules. Only the fixed personal config root is loaded.
- Automatic inference of verification policy from package scripts.
- Semantic/type-aware TypeScript analysis beyond ast-grep's syntax tree.
- Automatic fixes.
- A universal task-completion protocol for clients other than Pi's `agent_settled` lifecycle.
- Accepting interactive `!`/`!!` commands as verification evidence. Out-of-band changes are detected by workspace snapshots, but required checks must run through an observable tool in the MVP.
### Explicit non-goals
- Shell command filtering or rewriting.
- Destructive-command confirmation.
- Filesystem, network, credential, or process sandboxing.
- Replacing project linters, typecheckers, test runners, or LSP servers.
## Repository and runtime boundaries
- **Repository:** `/srv/data/code/jb-pi/pi-native-hooks`
- **Package and extension:** `pi-native-hooks`
- **Extension entrypoint:** `extensions/pi-native-hooks/index.ts`
- **Personal policy root:** `~/.config/pi-native-hooks/`
- **Installation:** Pi local-path package from `/srv/data/code/jb-pi/pi-native-hooks` during development; registry publication is deferred.
- **Runtime code:** package-owned lifecycle, analyzer, state, delivery, validation, and UI modules.
- **Personal code:** user-owned policy modules and adjacent ast-grep rule files. Personal policies are not committed to this repository.
- **Repository fixtures:** checked-in example policies and test fixtures that mirror the personal module contract without containing the user's live policy set.
The repository may ship an initializer that creates the personal directory, a policy-oriented `tsconfig.json`, and starter modules, but it must perform a dry run and request explicit authorization before writing outside the repository. Package install and personal policy initialization are separate operations.
## Trusted TypeScript policy model
Personal policy modules are arbitrary executable code. Importing one can perform side effects before its export is validated, so schema validation is not a sandbox. Documentation and `/hooks-status` must state that these files run with the same filesystem, process, network, and credential authority as Pi; nono remains responsible for the outer sandbox.
The one-run analyzer guarantee applies to toolkit-managed analyzer keys. The runtime withholds process authority from `PolicyContext`, and the authoring skill requires pure evaluators, but a trusted module can still import `node:child_process` and bypass the toolkit. Hard prevention would require declarative or isolated policies and conflicts with the chosen trusted-TypeScript model.
The loader must:
1. resolve the fixed `<current user's home>/.config/pi-native-hooks` root from the OS user record, without accepting environment or repository overrides;
2. require the root, `sgconfig.yml`, policy files, and rule files to be user-owned regular files/directories, not symlinks, and not writable by group or others;
3. discover only sorted direct-child `.ts` files from the documented `policies/` directory;
4. load each file through native dynamic import with an mtime/content cache token on Node `>=22.19`;
5. document the erasable-TypeScript subset: type-only syntax is supported, while enums, namespaces, parameter properties, JSX, and tsconfig transforms are not;
6. validate the default export structurally after import and reject duplicate IDs;
7. isolate a throwing/invalid module so other policies still load, while surfacing the exact file and error; and
8. reload policies only when Pi recreates the extension on `/reload` or session replacement.
The package exposes `pi-native-hooks/policy` type declarations. An authorized initializer writes a personal `tsconfig.json` with a path mapping to the installed package so policies can use `import type` plus `satisfies` without a runtime package import. Runtime validation remains authoritative because type checking can be skipped or stale.
Suggested personal layout:
```text
~/.config/pi-native-hooks/
├── tsconfig.json
├── sgconfig.yml
├── policies/
│ ├── typescript-no-non-null-assertion.ts
│ ├── typescript-no-dynamic-import.ts
│ ├── pi-native-hooks-verification.ts
│ └── harness-tools-verification.ts
└── ast-grep/
└── rules/
└── typescript/*.yml
```
## Architecture
```text
~/.config/pi-native-hooks/policies/*.ts
|
v
+-----------------------+
| TrustedPolicyLoader | ownership checks + structural validation
+-----------+-----------+
| immutable policy registry
v
Pi lifecycle events
|
v
+----------------------------+
| ExecutionWaveCoordinator | records source order and terminal events,
| | compares pre/post workspace snapshots
+-------------+--------------+
| MutationWave
v
+-----------------------+ one invocation per analyzer key
| AnalysisCoordinator |----------------------------------+
+-----------+-----------+ |
| immutable AnalysisSnapshot v
| +--------------------+
+-------------------------------> | ast-grep adapter |
| +--------------------+
v
+-----------------------+
| Diagnostic policies | evaluation receives data, not process authority
+-----------+-----------+
| Diagnostics
v
+-----------------------+ +------------------------------+
| Result delivery | | Verification tracker |
| dedupe + augmentation | | revisions/evidence/waivers |
+-----------------------+ +--------------+---------------+
|
v
agent_settled follow-up gate
```
### Contract shapes
The implementation should converge on contracts equivalent to these, without treating these names as immutable API:
```ts
interface MutationWave {
readonly id: string;
readonly turnIndex: number;
readonly orderedToolCallIds: readonly string[];
readonly changedPaths: readonly string[];
readonly beforeFingerprint: string;
readonly afterFingerprint: string;
readonly workspaceRevision: number;
readonly toolResults: ReadonlyMap<string, ObservedToolResult>;
}
interface AnalysisRequest {
readonly analyzerKey: string;
readonly paths: readonly string[];
}
interface AnalysisSnapshot {
readonly analyzerKey: string;
readonly workspaceRevision: number;
readonly paths: readonly string[];
readonly findings: readonly Finding[];
readonly durationMs: number;
readonly failure?: AnalyzerFailure;
}
interface DiagnosticPolicy {
readonly kind: "diagnostic";
readonly id: string;
readonly analyzerKeys: readonly string[];
appliesTo(project: ProjectContext, paths: readonly string[]): boolean;
evaluate(
analysis: ReadonlyMap<string, AnalysisSnapshot>,
context: PolicyContext,
): readonly Diagnostic[];
}
interface VerificationPolicy {
readonly kind: "verification";
readonly id: string;
readonly project: ProjectMatcher;
readonly requirements: readonly VerificationRequirement[];
}
interface VerificationRequirement {
readonly id: string;
readonly command: string;
readonly cwd: "project-root";
affects(path: string): boolean;
}
type PersonalPolicy = DiagnosticPolicy | VerificationPolicy;
```
Freeze arrays/maps or expose readonly copies before calling policy modules. Do not expose the process runner, Pi API, session context, or mutable tracker through `PolicyContext`; this makes shared analyzer execution enforceable by construction. This limits what the runtime hands to a policy, but it does not sandbox top-level module code or imports.
## Tool execution wave and mutation lifecycle
Pi preflights parallel sibling calls in assistant source order and runs them concurrently, but sequential tools interleave start and completion. An executed call reaches `tool_result` before `tool_execution_end`; a blocked, invalid, or otherwise immediate call can reach `tool_execution_end` without any `tool_result`. Final tool-result messages are emitted later in assistant source order. The extension must coordinate overlapping execution waves rather than assume one batch per turn or one `tool_result` per start.
1. On `turn_start`, reset turn-local ordering metadata without creating a barrier for the whole turn.
2. On each `tool_execution_start`, add `toolCallId`, tool name, input, and source-order position to the current overlapping wave. If no call remains open, begin a new wave. This naturally treats each sequential execution as its own wave while grouping parallel siblings.
3. Before the first potentially mutating call in a wave executes, capture one workspace baseline. Known read-only built-ins can skip this work; `write`, `edit`, `bash`, and unknown custom tools are potentially mutating. The extension's status and waiver tools are explicitly workspace-read-only.
4. On `tool_result`, record final input, content, details, and `isError`, then mark that call terminal for barrier purposes before waiting. A failed command may have partially mutated files, so failure does not skip comparison.
5. On `tool_execution_end`, mark a call terminal only when no preceding `tool_result` did so. This accounts for blocked and argument-invalid calls without waiting for an event Pi will never emit.
6. When every call in the overlapping wave is terminal, compute one post-wave workspace snapshot. A bounded fallback releases waiters if event accounting becomes inconsistent.
7. If fingerprints differ, calculate changed paths and advance the workspace revision once. For each verification requirement whose `affects(path)` predicate matches, advance its required revision and invalidate its evidence or waiver.
8. Build analyzer requests required by applicable policies. Coalesce requests by `analyzerKey`, union and sort paths, and invoke each key once. ast-grep is the only implemented key in the MVP.
9. Fan immutable snapshots to policy evaluators. Deduplicate and format their diagnostics.
10. Attach one combined diagnostic block to a deterministic carrier with a real `tool_result`, preferably the last source-order potentially mutating result. Fall back to the last source-order participating result when the preferred call completed immediately. Preserve original `content`, `details`, `isError`, and `usage`.
11. Release every waiting handler. Use `turn_end` for cleanup and inconsistency detection, not as the normal analysis point.
An extension defect must not deadlock tool delivery. On timeout or inconsistent events, release handlers, surface a coordinator failure, and conservatively mark armed verification state unknown.
A verification command running in a wave with any other potentially mutating call must not count, even if the final workspace fingerprint is unchanged. The check may have raced a transient mutation and must be rerun in a later stable wave.
## Workspace snapshots
Use repository state, not shell-command regexes, as the primary mutation signal.
### Git workspaces
Capture a deterministic, NUL-safe map for tracked changes, staged changes, deletions, renames, symlinks, submodule markers, and untracked non-ignored files. Include the HEAD object ID and index state. Hash current content only for dirty paths relevant to at least one active policy so a second edit to an already-dirty file changes the fingerprint without hashing the clean repository. If HEAD changes, derive affected paths between the old and new commits. Compare before and after maps to derive paths changed by the execution wave.
Use Git plumbing/status commands through `pi.exec` with argv arrays, timeout, and `ctx.signal`, then strictly parse and cap their results. Ignored build artifacts such as `node_modules` and normal coverage output do not invalidate evidence unless a verification policy explicitly marks them relevant.
### Non-Git or failed snapshots
For successful `write`/`edit`, fall back to their explicit target paths. For `bash` or unknown tools in a non-Git workspace, exact changed paths cannot be proven cheaply; advance armed requirements to `unknown` and fail verification closed. Diagnostics may report that no safe path set was available rather than scanning the entire directory.
### Out-of-band changes
On a new branch state with no native-hooks history, capture the current workspace as an unarmed baseline even when it is already dirty. On restored armed state, compare the persisted project identity and fingerprint during `session_start`; also refresh at `agent_settled`. A later mismatch advances affected requirement revisions before completion gating. This catches user shell commands, editor changes, and background writers without forcing verification in a question-only session merely because files were dirty before it began.
## Analyzer coordination and performance
### One-run invariant
For one mutation execution wave, the analyzer coordinator may execute a given `analyzerKey` zero or one time. Multiple policies, languages, or findings do not create additional processes. Tests must inject a counting fake adapter and prove one call when both initial TypeScript policies apply, including parallel writes to several files.
An analyzer key includes the adapter and effective configuration. If future hooks genuinely require incompatible lint configurations, they use different keys and therefore separate documented invocations.
### ast-grep adapter
Invoke the installed executable once with all applicable rule files and changed paths, using project configuration and JSON stream output, conceptually:
```text
ast-grep scan --config ~/.config/pi-native-hooks/sgconfig.yml \
--json=stream --color=never --max-results <bound> -- <paths...>
```
Normalize JSON into rule ID, severity, message, note, path, source range, and matched text hash. Convert ast-grep's zero-based lines and columns at the rendering seam. Validate every decoded record rather than casting untrusted process output. Bound runtime, stdout, file count, finding count, and rendered diagnostics.
Keep steering rules at warning, info, or hint severity so a match does not become a process failure; the authoring skill rejects error-severity steering rules. Resolve and version-check `ast-grep` lazily once per session. A missing, incompatible, timed-out, nonzero, malformed, or killed analyzer produces an `AnalyzerFailure`; it never produces a false clean snapshot.
### Initial personal syntax policies
The initializer creates these policies and their ast-grep rules only after an approved dry run. The runtime itself ships contract fixtures and examples, not the user's live policy files. Both policies request the shared `ast-grep:personal` analyzer key and filter their own rule IDs from the same immutable snapshot.
1. **`typescript-no-non-null-assertion`**
- Match TypeScript/TSX `non_null_expression` nodes.
- Guidance: prefer a guard, refinement, assertion function, or corrected type.
- Exception: use a rule-specific `ast-grep-ignore` comment with a short rationale when an invariant is intentional.
2. **`typescript-no-dynamic-import`**
- Match dynamic `import(...)` call expressions in TypeScript/TSX.
- Guidance: prefer a static import unless lazy loading, code splitting, an optional dependency, or runtime module selection is intentional.
- Exception: use the same rule-specific suppression with a rationale.
Use separate TypeScript and TSX ast-grep rule definitions where the analyzer requires separate language declarations, but map those rule IDs to one user-facing policy ID. ast-grep's supported form allows a trailing rationale, for example:
```ts
const module = await import(path); // ast-grep-ignore: typescript-no-dynamic-import-ts -- runtime-selected plugin
```
Do not add regex-based parsing of TypeScript syntax. A later policy may validate suppression rationale formatting, but that is not required for the first slice.
### Diagnostic deduplication
Fingerprint a finding from policy ID, analyzer rule ID, canonical path, normalized range, and matched-text hash. Keep the last delivered set per file/content fingerprint. Deliver new or changed findings, drop resolved fingerprints, and avoid repeating an unchanged finding after unrelated tool calls. Cap rendered findings and include a count of omitted items.
## Verification model
### Initial personal verification policies
The authorized initializer creates two verification policy modules:
1. **`pi-native-hooks-verification`** matches Git root plus `package.json` name `pi-native-hooks`.
2. **`harness-tools-verification`** matches Git root plus `package.json` name `harness-tools`.
Both initially require these standalone commands from the matched project root:
```text
npm test
npm run typecheck
```
Each requirement owns a pure `affects(path)` predicate instead of introducing a glob dependency. The `pi-native-hooks` policy arms tests for behavior-bearing changes under `extensions/`, `src/`, `tests/`, `examples/`, and `skills/`, plus package and TypeScript manifests; its typecheck requirement covers TypeScript and package/TypeScript manifests. The harness-tools policy retains the repository-specific `extensions/`, `scripts/`, `services/`, `skills/`, `config/`, and manifest predicates documented in its module. Documentation-only changes outside each policy's declared surfaces do not stale evidence.
Trim command leading/trailing whitespace only. Do not parse shell text or accept `npm test -- ...`, `npm test && npm run typecheck`, aliases, focused scripts, or commands from another working directory. Additional exact forms require an explicit personal policy.
### Evidence lifecycle
Maintain one monotonic workspace revision. Each requirement stores its own `requiredRevision`, which remains absent until an observed path first satisfies its `affects(path)` predicate. Irrelevant later changes may advance the workspace revision without invalidating that requirement.
Each requirement is one of:
- `inactive`: this branch has not observed a relevant mutation;
- `not-run`: armed, with no evidence for its required revision;
- `passed`: the exact command fully succeeded at or after its required revision;
- `failed`: the exact command completed unsuccessfully;
- `stale`: older success exists but a later relevant mutation advanced the required revision;
- `waived`: the agent recorded a reason for this required revision; or
- `unknown`: armed workspace or event state could not be proven.
A requirement becomes valid only when all of these hold:
- it is armed;
- command and `ctx.cwd` exactly match its configured command and repository root;
- execution begins at or after the latest required revision;
- no other potentially mutating call shares its overlapping execution wave;
- no relevant mutation occurs before completion;
- Pi emits a finalized tool result with `event.isError === false`; and
- the evidence records the same project identity and required revision.
Output truncation does not imply partial execution; Pi finalizes the process before emitting the result. Normal nonzero exits, Esc aborts, and timeouts produce `isError: true` and do not count. Pi's public bash result omits the numeric exit code, so the MVP documents one narrow limitation: if the shell process itself ends through an external signal and Pi exposes a successful result, the extension cannot independently disprove completion. This is an operational completion gate, not a security attestation.
### Foreground `bash`
Capture the original command and `ctx.cwd` by `toolCallId`, then compare the finalized input after other extension handlers have run. `event.isError === false` is the success signal exposed by Pi's public finalized bash result. Record evidence only after execution-wave workspace comparison proves no relevant mutation and no potentially mutating sibling raced the check.
## Completion gate and waiver
### Forced follow-up
At every `agent_settled`:
1. refresh workspace state and advance affected requirement revisions if it changed;
2. evaluate only armed verification requirements;
3. update the TUI status; and
4. if any armed requirement is not passed or currently waived, send one custom `pi-native-hooks-verification` message with `{ deliverAs: "followUp", triggerTurn: true }`.
The message lists exact outstanding commands and states that the agent must run them or use the waiver tool with a reason. A per-settlement delivery guard prevents duplicates from concurrent callbacks, but a later settlement with the same unresolved revisions sends another reminder. That repeated behavior is intentional: otherwise Pi could settle on the second attempt without evidence.
A `context` handler removes older `pi-native-hooks-verification` messages from the model-facing copy, retaining only the latest live reminder while the gate is pending and none after it is satisfied. Session history remains auditable without allowing repeated forced turns to accumulate stale guidance in model context.
Pi has no literal task-acceptance event. This mechanism prevents the normal agent loop from remaining idle, but it cannot stop a user from closing Pi or an external client from discarding the session. Document that limitation plainly.
The gate may also fire when an agent settles after asking a question rather than claiming completion. The chosen agent-callable waiver is the escape hatch for intentionally deferred verification.
### Waiver tool
Register `pi_hooks_waive_verification` with one required `reason` string. Runtime validation trims and rejects an empty reason.
The tool:
- lists the exact outstanding requirement IDs it waives;
- records reason, timestamp, project identity, requirement IDs/revisions, and tool call in durable extension state;
- returns a visible summary that verification was skipped rather than passed;
- rejects a no-op request when no armed requirement is outstanding;
- clears the completion gate only for those requirement revisions; and
- becomes stale after the next relevant mutation.
A waiver never creates test evidence and must remain distinguishable in `/hooks-status` and session restoration. Any later path matching the requirement's `affects(path)` predicate advances its required revision and expires that waiver.
## State, observability, and failure policy
### Persistence
Append versioned `pi-native-hooks-state-v1` snapshots for meaningful transitions: policy registry digest, workspace/required revision changes, accepted evidence, failures, waivers, and coordinator uncertainty. Restore the latest valid snapshot from `ctx.sessionManager.getBranch()` on `session_start`, then compare policy digest, project identity, and live workspace fingerprint before trusting it. Reconstruct again on `session_tree` so branch navigation cannot inherit state from an abandoned branch. A previously armed policy that is now missing or fails to load remains `unknown` rather than silently clearing its gate.
Avoid persisting every analyzer finding or reminder. Diagnostic deduplication may be session-local; verification and waiver state must be durable and branch-aware.
### UI and commands
- Set a compact `pi-native-hooks` status showing diagnostic count and verification summary.
- Add `/hooks-status` to show config root health, loaded policy paths/digests, load failures, project identity, workspace revision, analyzer timing, inactive or pending checks, required revisions, last success, active waiver, and last failure.
- Keep model-visible diagnostics concise. Put verbose operational detail in the status command or TUI notification.
- Clear status and release pending barriers during `session_shutdown`.
### Failure policy
| Failure | Behavior |
|---|---|
| Config root absent with no restored armed state | Load an empty inactive registry, show initialization guidance, and do not create files automatically. |
| Config ownership/mode check fails | Import no affected file, identify the unsafe path, and keep previously armed policy state unknown. |
| New policy throws or exports an invalid value | Skip that policy, report its file, and continue valid siblings. |
| Previously armed policy is missing or fails to load | Preserve its outstanding requirement state as unknown until restored or waived; never silently accept completion. |
| ast-grep missing, incompatible, timed out, or malformed | Preserve tool success, append one analyzer warning, show degraded status, and do not claim a clean scan. |
| Policy evaluator throws | Isolate that policy, report its ID, continue siblings, and mark its evaluation degraded. |
| Workspace snapshot cannot prove armed state | Release tool results, mark affected requirements `unknown`, and force follow-up until rerun or waiver. |
| Persistence entry malformed | Ignore that entry, use the latest earlier valid state if available, and surface a warning. |
| Barrier inconsistency or timeout | Release waiters, avoid result corruption, mark verification unknown, and surface coordinator failure. |
| Result augmentation fails | Return the original result unchanged and notify the UI; never convert a successful mutation into a tool error. |
## Proposed repository layout
```text
pi-native-hooks/
├── package.json
├── tsconfig.json
├── README.md
├── extensions/
│ └── pi-native-hooks/
│ └── index.ts
├── src/
│ ├── runtime.ts
│ ├── policy.ts
│ ├── policy-loader.ts
│ ├── analysis.ts
│ ├── execution-wave.ts
│ ├── diagnostics.ts
│ ├── project-context.ts
│ ├── state.ts
│ ├── verification.ts
│ ├── workspace.ts
│ └── analyzers/
│ └── ast-grep.ts
├── tests/
│ ├── runtime.test.ts
│ ├── policy-loader.test.ts
│ ├── execution-wave.test.ts
│ ├── ast-grep.test.ts
│ ├── verification.test.ts
│ └── fixtures/
│ └── personal-config/
├── examples/
│ └── policies/
│ ├── typescript-no-non-null-assertion.ts
│ ├── typescript-no-dynamic-import.ts
│ ├── pi-native-hooks-verification.ts
│ └── harness-tools-verification.ts
├── skills/
│ └── native-hook-authoring/
│ ├── SKILL.md
│ └── references/
│ ├── contracts.md
│ └── testing.md
└── docs/
├── implementation-plan.md
└── adr/
```
Prefer colocated focused tests when a module has substantial behavior. The exact split may be simplified during implementation if files remain single-purpose.
## Delivery slices
After the planning baseline is committed, use a new worktree for implementation. Assign ownership before parallel work. Shared runtime, package manifest, and policy contract files belong to the integration owner; policy authors own only example/fixture policy and rule files. No slice may write `~/.config/pi-native-hooks` or install the Pi package until the explicit activation step in Slice 4.
### Slice 0: standalone package and trusted policy-loader proof
**Goal:** prove the repository/package shape and executable TypeScript policy contract before building the event runtime.
**Work:**
- Add the ESM package manifest, Node `>=22.19` engine, Pi extension/skill declarations, strict TypeScript configuration, and focused test command.
- Define the small `PersonalPolicy` interface and runtime structural validators.
- Implement the fixed-root policy loader behind an injected filesystem/import adapter.
- Prove native dynamic import of erasable `.ts`, type-only imports, deterministic ordering, cache-busted reload, duplicate IDs, top-level throws, invalid exports, ownership/mode rejection, and symlink policy.
- Add checked-in example policies and a fixture personal-config tree. Do not create the live config directory.
- Record the standalone package and trusted-code decision in `docs/adr/`.
**Success criteria:**
- `npm run typecheck` and focused loader tests fully execute.
- Tests run against an isolated fake HOME and never inspect or mutate the real personal config.
- One invalid policy does not hide valid siblings, and every failure identifies its path.
- The loader executes no repository-local policy and accepts no environment override for the config root.
- The Node-native loader limitations are documented and proven on the minimum supported runtime before avoiding a loader dependency.
### Slice 1: execution-wave runtime and shared analyzer seam
**Goal:** establish Pi event and one-run analyzer invariants with fakes.
**Work:**
- Add a fake Pi event harness, execution-wave coordinator, and injected fake workspace/analyzer adapters.
- Model parallel source-order starts, reverse-completion results, sequential waves, and immediate `tool_execution_end` without `tool_result`.
- Implement request coalescing, immutable snapshot fan-out, deterministic result carrier selection, barrier timeout, context-reminder pruning, and shutdown cleanup.
**Success criteria:**
- Two parallel mutations and two interested policies call one fake analyzer exactly once.
- Out-of-order results produce one workspace revision and one diagnostic augmentation.
- A blocked sibling with no `tool_result` cannot deadlock the wave.
- Sequential mutators form separate waves without waiting for future starts.
- Read-only waves invoke no workspace snapshot or analyzer.
- Failure/timeout releases every handler and leaves armed verification unknown rather than deadlocking.
### Slice 2: Git workspace, ast-grep adapter, and diagnostic examples
**Goal:** prove real mutation detection and contextual TypeScript steering.
**Work:**
- Implement Git workspace maps/fingerprints and changed-path calculation.
- Implement the one-process `ast-grep:personal` adapter against the personal `sgconfig.yml` and strict JSON normalization.
- Add TypeScript/TSX example rules and policies for non-null assertions and dynamic imports.
- Add deduplication, bounded rendering, analyzer health/status, and `/hooks-status` policy diagnostics.
- Validate `ast-grep-ignore: <rule-id> -- <reason>` exceptions.
**Success criteria:**
- Real fixture scans detect both patterns and suppress only the named rule.
- One wave touching several TS/TSX files and both policies starts one ast-grep process.
- `write`, `edit`, successful/failed `bash`, untracked, staged, deleted, renamed, symlinked, and already-dirty files change the correct fingerprint.
- Unchanged findings are not repeated after unrelated waves.
- Missing, incompatible, nonzero, timed-out, or malformed ast-grep is visible and never reported as a clean scan.
### Slice 3: verification evidence and settlement gate
**Goal:** keep Pi active until exact current evidence or a current waiver exists.
**Work:**
- Implement project matching, pure path predicates, observed-mutation activation, and example policies for `pi-native-hooks` and `harness-tools`.
- Track exact foreground bash attempts and Pi's finalized success state.
- Persist policy digest plus branch-aware workspace/requirement revisions, evidence, failures, and waivers.
- Handle changed, missing, or failed previously-armed policies without silently accepting completion.
- Implement settlement refresh, forced follow-up delivery, context pruning, status, and waiver tool.
**Success criteria:**
- A new state-free session remains unarmed even when relevant files were already dirty at startup.
- Exact standalone checks satisfy only their matching armed project requirements.
- Focused, altered, combined, failed, aborted, timed-out, wrong-cwd, concurrent, and stale commands do not satisfy the gate.
- A potentially mutating sibling invalidates a racing check even when final content is unchanged.
- `agent_settled` sends one follow-up per pending settlement and none after current evidence or waiver.
- A non-empty waiver is visible, branch-aware, revision-scoped, and invalidated by the next relevant mutation.
- Reload, branch navigation, policy digest change, and out-of-band edits cannot revive stale evidence.
### Slice 4: authoring skill, activation, and independent review
**Goal:** validate authoring and real Pi behavior before normal use.
**Work:**
- Create the package-managed `native-hook-authoring` skill with progressive disclosure for policy creation, revision, validation, and troubleshooting.
- Add `/hooks-init --dry-run` and `/hooks-doctor`; any command that writes personal config requires explicit interactive approval and preserves existing files.
- Run an extension integration scenario with parallel writes, one ast-grep run, diagnostics, stale verification, forced follow-up, successful checks, invalidation, and waiver.
- Run unfiltered `npm run typecheck` and `npm test`.
- Have an agent other than the implementer review standards, trust handling, event races, failure behavior, and this approved plan.
- Only after review and explicit user approval, install the local-path Pi package, initialize selected personal policies, run `/hooks-doctor`, and `/reload`.
**Success criteria:**
- The skill points to the package's policy contract rather than duplicating it.
- A fresh agent can create a fixture policy without adding analyzer execution to its evaluator.
- Dry runs enumerate every external file and package-setting change before mutation.
- Existing personal config is never overwritten; partial initialization rolls back newly created files.
- Tests record actual analyzer invocation counts and full command completion.
- Independent review has no unresolved blockers.
- The activated extension reports exactly one runtime, all intended personal policy digests, and a healthy analyzer after `/reload`.
## Focused test matrix
| Area | Required cases |
|---|---|
| Policy loader | fixed root; native TS; type-only import; sorted files; duplicate IDs; invalid export; top-level throw; owner/mode; symlink; cache-busted reload; changed digest |
| Wave ordering | one/many siblings; source-order starts; reverse completion; sequential waves; immediate end without result; read-only; failed partial mutation; missing terminal timeout; cleanup |
| Analyzer coalescing | two policies same key; many paths; no applicable paths; different future keys; adapter throw/timeout/malformed output |
| ast-grep | TS and TSX positives; static import negative; postfix `!` positive; logical `!` negative; justified suppression; Unicode ranges; result cap |
| Workspace | clean/dirty/staged/untracked/ignored/deleted/renamed/symlinked/binary/already-dirty; non-Git fallback; external edit |
| Delivery | original result fields preserved; one carrier; dedupe; resolved finding; truncation; analyzer warning |
| Foreground evidence | exact success; nonzero; abort; timeout; wrong cwd; focused flag; combined command; mutating sibling with/without net diff; documented external-signal limitation |
| Gate | pre-existing dirty baseline inactive; observed relevant mutation armed; irrelevant mutation unchanged; partial pass; failed; stale; unknown; repeated settlements; no duplicate within settlement; idle follow-up; context pruning |
| Waiver | empty rejection; no-op rejection; scoped requirement revisions; branch persistence; status visibility; later relevant invalidation |
| Session | startup without state; valid active-branch restore; malformed/latest-invalid entry; fork/reload/session-tree; project identity mismatch; fingerprint drift |
| Activation | Pi package manifest; local-path dry-run/install; fake HOME initialization; existing config preservation; partial rollback; doctor; exactly one runtime after reload |
## Performance and safety acceptance
- No analyzer process for a read-only execution wave or when no changed path is relevant.
- At most one invocation per analyzer key per overlapping execution wave, enforced by tests.
- Git work is proportional to dirty/untracked paths, not every clean tracked file.
- Analyzer and snapshot commands have abort signals, timeouts, and output/result caps.
- Policy evaluation exceptions cannot prevent sibling policies or original tool-result delivery.
- Policy modules receive immutable analysis data, but their top-level code remains explicitly trusted and unsandboxed inside Pi's outer sandbox.
- Diagnostics never change a successful tool result into an error.
- Verification never becomes current from incomplete, ambiguous, concurrent, or stale execution.
## Documentation and review artifacts
After approval, implementation should add:
- `docs/adr/0001-standalone-runtime-and-trusted-policies-27AUG2026.md` for repository ownership, native policy loading, shared analyzers, and the completion gate;
- concise user documentation for installation, config trust, supported TypeScript syntax, status, waiver semantics, exact commands, exceptions, and known completion limitations;
- the `native-hook-authoring` skill; and
- a repository-local `docs/todos.md` entry for any reproducible defect discovered while testing the extension on itself.
## Planning validation completed
- Pi 0.84.3 documentation and installed source confirm parallel preflight ordering, concurrent execution, `tool_result` result augmentation, source-order final result messages, and `agent_settled` custom-message follow-ups.
- Installed Pi source also confirms the execution-wave correction: blocked or argument-invalid calls can emit `tool_execution_end` without invoking `tool_result`, while sequential tools do not expose future starts before the current result finishes.
- A throwaway ast-grep 0.45.1 fixture detected both initial TypeScript patterns in one `scan --json=stream` invocation, did not confuse logical negation with a non-null expression, and honored a rule-specific suppression with a trailing rationale.
- The ast-grep fixture confirmed warning findings return exit code zero and JSON source ranges are zero-based, both now reflected in the adapter plan.
- Node 26.7 successfully imported an erasable `.ts` policy containing interfaces, `satisfies`, and a type-only import to an unavailable runtime package. Phase 0 still must repeat the contract on the minimum supported Node 22.19 runtime.
- Planning originally created only this repository's README and documentation. Implementation subsequently added the runtime, tests, examples, and authoring skill. No live personal policy, Pi package setting, or global installation is changed without the separate activation approval.
Independent agent review is deferred because direct subagent launches are currently being repaired. It remains the first execution prerequisite when implementation starts from this repository.
## Approval gate
The user accepted these consequences on 27 August 2026:
1. Every personal `.ts` policy is trusted arbitrary code running in Pi's process; structural validation does not sandbox imports or top-level side effects, and a nonconforming policy can bypass the toolkit's one-run analyzer coordinator.
2. The MVP supports only Node's erasable TypeScript subset for personal modules and reloads policies on `/reload`, not continuously.
3. Pending verification forces a new turn at every settlement, including settlements after the agent asks a question.
4. A state-free session treats pre-existing dirty files as an unarmed baseline; only observed relevant mutations or restored armed state require checks.
5. The agent can waive its own verification requirement, but the reason remains visible and the waiver expires on a relevant mutation.
6. Exact command matching intentionally rejects convenient combined or focused commands.
7. Foreground success trusts Pi's public `isError` state and carries the documented external-signal ambiguity.
8. ast-grep failure degrades steering but does not block file mutation; armed workspace uncertainty does block verification completion.
9. Pi's settlement gate cannot prevent a user or external client from closing the session.
10. Local-path package installation and creation of `~/.config/pi-native-hooks` are separate external mutations requiring approval after tests and review.
User approval is recorded. An independent pre-implementation review was completed before runtime changes; its blockers were addressed by `docs/event-contract.md`, the explicit project identity schema, fail-fast native TypeScript loading, and canonical repository paths.