46 KiB
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:
- 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;
- track exact required verification commands and prevent Pi from remaining settled while their evidence is missing or stale; and
- 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(...), notpi-yaml-hooks,pi-steering, orworkflow. - 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-hooksas 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_settledas Pi's strongest available completion candidate. If armed verification is pending, inject afollowUpcustom message withtriggerTurn: 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.19runtime. 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
bashcommand 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-hooksextension and thenative-hook-authoringskill.
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-statusobservability. - Exact foreground
bashverification evidence. - Forced completion follow-ups and a reasoned waiver tool.
- Initial personal TypeScript diagnostic policies and verification policies for
pi-native-hooksandharness-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-taskspackage 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_settledlifecycle. - 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-hooksduring 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:
- resolve the fixed
<current user's home>/.config/pi-native-hooksroot from the OS user record, without accepting environment or repository overrides; - 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; - discover only sorted direct-child
.tsfiles from the documentedpolicies/directory; - load each file through native dynamic import with an mtime/content cache token on Node
>=22.19; - document the erasable-TypeScript subset: type-only syntax is supported, while enums, namespaces, parameter properties, JSX, and tsconfig transforms are not;
- validate the default export structurally after import and reject duplicate IDs;
- isolate a throwing/invalid module so other policies still load, while surfacing the exact file and error; and
- reload policies only when Pi recreates the extension on
/reloador session replacement.
The package exposes @_jb/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:
~/.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
~/.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:
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.
- On
turn_start, reset turn-local ordering metadata without creating a barrier for the whole turn. - On each
tool_execution_start, addtoolCallId, 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. - 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. - On
tool_result, record final input, content, details, andisError, then mark that call terminal for barrier purposes before waiting. A failed command may have partially mutated files, so failure does not skip comparison. - On
tool_execution_end, mark a call terminal only when no precedingtool_resultdid so. This accounts for blocked and argument-invalid calls without waiting for an event Pi will never emit. - 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.
- 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. - 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. - Fan immutable snapshots to policy evaluators. Deduplicate and format their diagnostics.
- 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 originalcontent,details,isError, andusage. - Release every waiting handler. Use
turn_endfor 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:
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.
-
typescript-no-non-null-assertion- Match TypeScript/TSX
non_null_expressionnodes. - Guidance: prefer a guard, refinement, assertion function, or corrected type.
- Exception: use a rule-specific
ast-grep-ignorecomment with a short rationale when an invariant is intentional.
- Match TypeScript/TSX
-
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.
- Match dynamic
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:
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:
pi-native-hooks-verificationmatches Git root pluspackage.jsonnamepi-native-hooks.harness-tools-verificationmatches Git root pluspackage.jsonnameharness-tools.
Both initially require these standalone commands from the matched project root:
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; orunknown: 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.cwdexactly 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:
- refresh workspace state and advance affected requirement revisions if it changed;
- evaluate only armed verification requirements;
- update the TUI status; and
- if any armed requirement is not passed or currently waived, send one custom
pi-native-hooks-verificationmessage 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-hooksstatus showing diagnostic count and verification summary. - Add
/hooks-statusto 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
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.19engine, Pi extension/skill declarations, strict TypeScript configuration, and focused test command. - Define the small
PersonalPolicyinterface 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 typecheckand 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_endwithouttool_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_resultcannot 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:personaladapter against the personalsgconfig.ymland 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-statuspolicy 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/failedbash, 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-hooksandharness-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_settledsends 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-authoringskill with progressive disclosure for policy creation, revision, validation, and troubleshooting. - Add
/hooks-init --dry-runand/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 typecheckandnpm 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.mdfor 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-authoringskill; and - a repository-local
docs/todos.mdentry 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_resultresult augmentation, source-order final result messages, andagent_settledcustom-message follow-ups. - Installed Pi source also confirms the execution-wave correction: blocked or argument-invalid calls can emit
tool_execution_endwithout invokingtool_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=streaminvocation, 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
.tspolicy 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:
- Every personal
.tspolicy 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. - The MVP supports only Node's erasable TypeScript subset for personal modules and reloads policies on
/reload, not continuously. - Pending verification forces a new turn at every settlement, including settlements after the agent asks a question.
- A state-free session treats pre-existing dirty files as an unarmed baseline; only observed relevant mutations or restored armed state require checks.
- The agent can waive its own verification requirement, but the reason remains visible and the waiver expires on a relevant mutation.
- Exact command matching intentionally rejects convenient combined or focused commands.
- Foreground success trusts Pi's public
isErrorstate and carries the documented external-signal ambiguity. - ast-grep failure degrades steering but does not block file mutation; armed workspace uncertainty does block verification completion.
- Pi's settlement gate cannot prevent a user or external client from closing the session.
- Local-path package installation and creation of
~/.config/pi-native-hooksare 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.