---
slug: "davidorex-pi-context"
source_type: "readme"
source_url: "https://cdn.jsdelivr.net/gh/davidorex/pi-project-workflows@main/README.md"
repo: "https://github.com/davidorex/pi-project-workflows"
source_file: "README.md"
branch: "main"
---
# In short

This monorepo is my attempt to direct and produce a controllable environment for working with llm's to build things in code. 

I'm considering renaming it "pi-prometheus-bound." Or maybe just "pi-bound." I currently run it with a script that wholly constrains the main Pi agent to tools given by the extensions, plus read and grep and ls. 

You should (is the intention) be able to create exactly the kind of context substrates you need -- and switch between them, or inter-related them (think git branches, but for substrate shapes in your projects) -- and out of those substrates, construct the on-the-fly agents you need, for the particular purposes you might have.

## Philosophy

> 削斧柯，其则不远
>
> *When cutting wood to make an axe handle, the model is right there in your hand.*

The use of the tool shows you the shape of how to make the version of the tool that you want. Reshape it in-air based on your experience using it.

## Packages

| Package | npm | Description |
|---------|-----|-------------|
| [@davidorex/pi-context](https://github.com/davidorex/pi-project-workflows/tree/HEAD/packages/pi-context/) | `npm:@davidorex/pi-context` | Pi extension. Schema-driven project state: typed JSON blocks with write-time validation, generic block CRUD tools, content-addressed item identity, closure-table relations, and state derived from the substrate. Block types are added by declaring JSON Schemas in the substrate; no code change. |
| [@davidorex/pi-context-cli](https://github.com/davidorex/pi-project-workflows/tree/HEAD/packages/pi-context-cli/) | `npm i -g @davidorex/pi-context-cli` | Standalone CLI (not a Pi extension). Exposes pi-context's op-registry as a `pi-context <op> [flags]` binary; subcommands and flags are derived by reflection over the registry, so they track the op set without per-command code. Calls the same library functions the extension calls; no running Pi instance required. Depends on `@davidorex/pi-context`. |
| [@davidorex/pi-jit-agents](https://github.com/davidorex/pi-project-workflows/tree/HEAD/packages/pi-jit-agents/) | `npm:@davidorex/pi-jit-agents` | Library (not a Pi extension). Loads, compiles, and executes `.agent.yaml` specs in-process, with phantom-tool structured-output enforcement. Consumed by pi-workflows and pi-behavior-monitors. |
| [@davidorex/pi-workflows](https://github.com/davidorex/pi-project-workflows/tree/HEAD/packages/pi-workflows/) | `npm:@davidorex/pi-workflows` | Pi extension. Workflow orchestration from `.workflow.yaml` specs: DAG execution, typed step types, typed data flow between steps, an expression engine, and checkpoint/resume. Output schemas validate the boundary between steps. |
| [@davidorex/pi-behavior-monitors](https://github.com/davidorex/pi-project-workflows/tree/HEAD/packages/pi-behavior-monitors/) | `npm:@davidorex/pi-behavior-monitors` | Pi extension. Monitors that classify agent activity against JSON pattern libraries via side-channel LLM calls, steer corrections, and write structured findings. |
| [@davidorex/pi-agent-dispatch](https://github.com/davidorex/pi-project-workflows/tree/HEAD/packages/pi-agent-dispatch/) | `npm:@davidorex/pi-agent-dispatch` | Pi extension. In-pi orchestrator surface: agent-as-tool dispatch (`call-agent`), capability-grant authoring (`author-tool-grant`), real-check gate (`run-real-checks`), attested commit (`commit-attested`), bounded loop (`run-work-order-loop`), and dynamic composite tools per `config.tool_operations[]`. A per-tool authorization gate at the pi-dispatch layer intercepts write-class tools, prompts via `ctx.ui.confirm`, and stamps the verified operator identity on the write. |
| [@davidorex/pi-project-workflows](https://github.com/davidorex/pi-project-workflows/tree/HEAD/packages/pi-project-workflows/) | `npm:@davidorex/pi-project-workflows` | Meta-package. Bundles the four extensions (pi-context, pi-workflows, pi-behavior-monitors, pi-agent-dispatch) as a single `pi install`; depends on all four and registers each. |

## Quick Start

```bash
# Install all four extensions in any Pi project (one command, via the meta-package)
pi install npm:@davidorex/pi-project-workflows

# Or install the extensions individually
pi install npm:@davidorex/pi-context
pi install npm:@davidorex/pi-workflows
pi install npm:@davidorex/pi-behavior-monitors
pi install npm:@davidorex/pi-agent-dispatch

# Optional: the standalone pi-context CLI (not a Pi extension; no running Pi required)
npm i -g @davidorex/pi-context-cli

# In this monorepo, install the operator pi-context binary as a publish-free
# packed copy of the working tree (no npm link; a repo rebuild can't touch it):
npm run promote:cli

# Initialize project structure
/context init <substrate-dir>  # bootstrap pointer + substrate/schemas dirs only (no config, no schemas, no blocks)
/context accept-all  # adopt the packaged conception (samples/conception.json) as config.json
/context install     # reconciles the substrate against installed_schemas + installed_blocks + installed_agents in config.json
/context check-status  # read-only: report which installed schemas are behind the catalog + the version gap (writes nothing beyond the idempotent ceremony seeds: config migration decls + catalog-implied block-schema chains)
/workflow init       # creates .workflows/ for run state
```

Block kinds reach the substrate only by declaring their names in `config.json`'s `installed_*` arrays (via `/context accept-all` or by hand) and running `/context install`, which copies them from the package-shipped samples catalog (`samples/`). The substrate (config + lenses + closure-table relations) is degree-zero state that defines where the rest lives and how items group into views.

### Substrate-management primitive (`/context switch` family)

A substrate-dir is selected by the `.pi-context.json` pointer (`contextDir` field). `/context switch` is the substrate-management primitive parallel to `git switch`:

```bash
/context switch <existing-dir>    # flip pointer to an existing substrate dir
/context switch -c <new-dir>      # bootstrap a fresh substrate dir + flip pointer in one operation
/context switch -                 # round-trip to previous_contextDir
/context list                     # enumerate substrate dirs with config.json (active marked)
/context archive <dir>            # move a (non-active) substrate dir to archive/
```

Pointer-flip mutations (`context-switch` / `context-archive` / `context-init` / `context-accept-all` and other write-class tools) route through the pi-agent-dispatch auth-gate, which prompts via `ctx.ui.confirm` and stamps the verified operator identity on the substrate write — agent-issued tool calls become human-authorized at the pi-dispatch boundary regardless of caller-supplied writer fields.

This enables the **per-arc substrate engagement pattern**: significant design/spec arcs (multi-step feature work, spec drafting, dependency migrations) get their own `.context-<arc-name>/` substrate dir with bespoke vocabulary (custom block kinds + relation_types + lenses) decomposing the design space. The substrate IS the spec; the markdown form at `analysis/*.md` is source-of-content; the substrate is source-of-structure + cross-references.

### Constrained pi session (`pi-context pi-bound`)

The standalone `pi-context` CLI also provides a process mode that launches a `pi` coding-agent session restricted to the composed pi-extension tool surface:

```bash
pi-context pi-bound [--grant <id>]... [...pi-args]
```

On every launch it registers the extensions into the target dir's `.pi/` (`pi install -l` against the `@davidorex/pi-project-workflows` root), derives the static tool allowlist from the installed packages' generated `skills/*/SKILL.md` (`@davidorex/pi-context` + `@davidorex/pi-project-workflows`), always adds the built-in read-only tools `read,ls,grep,find`, appends the bounded composites declared in the active substrate's `config.tool_operations[]`, then launches `pi --tools <union> ...pi-args`. It runs from the process cwd and reads that dir's `.pi-context.json` for composites (warns, non-fatally, if absent).

- `--grant <id>` (repeatable) — scope the bounded composites to only the named ids (default: all declared).
- Any other token passes through verbatim to `pi` (e.g. `--continue` / `-c` to resume).

This process mode replaces the former `scripts/launch-constrained-pi.sh` launch script. When `/context update` finds a locally-modified schema with irreconcilable conflicts against the catalog, it surfaces the conflict set to the calling agent (it does not spawn a subordinate mergetool); the agent reconciles each conflict and commits it with `/context resolve-conflict`, which writes the resolved body, advances the merge base to the catalog, and registers a known migration chain when the reconciliation advances the schema's version.

## Directory Ownership

After initialization, three directories coexist in a project:

```
.pi/            — Pi platform (agents, skills, settings). Managed by Pi itself.
.pi-context.json          — bootstrap pointer naming the single ACTIVE substrate dir (contextDir)
.pi-context-registry.json — project-root registry enumerating ALL substrates by substrate_id (git-tracked)
<substrate-dir>/ — pi-context. Created by /context init <substrate-dir> (skeleton-only).
  config.json   — substrate bootstrap: root, substrate_id, naming, hierarchy, lenses, installed_*
  relations.json — closure-table edges (created on first authored edge)
  migrations.json — per-substrate schema-version migration registry
  schemas/      — JSON Schema files (empty until /context install reifies declared names)
  objects/      — content-addressed object store, one objects/<content_hash>.json per content version (git-tracked)
  <name>.json   — block data files (each a /context install target or user-authored)
.workflows/     — pi-workflows (run state). Created by /workflow init.
  runs/         — workflow execution state, session logs, outputs
```

`.pi/` is Pi's territory — neither extension writes to it. The substrate directory is tracked in git (substrate, schemas, and blocks are source). `.workflows/` is gitignored (runtime state). `config.json` and `relations.json` always live at the substrate-dir root (the bootstrap-chosen dir, pointer-resolved; they define `root`); everything else lives under `<config.root>/...` and a relocated root reaches every read/write because all path construction routes through `resolveContextDir(cwd)`.

## What Each Extension Provides

### pi-context

**Tool families:** block CRUD (`read/write/append/update/remove-block-item`, top-level + nested), item-level read/query (`read-block-item`, `read-block-page`, `filter-block-items`, `resolve-item(s)-by-id`, `join-blocks`, `find-references`, `walk-ancestors`, `context-walk-descendants`, `context-edges-for-lens`, `context-lens-view`, `gather-execution-context`), substrate writes (`append-relation`, `amend-config`, `write-schema`, `write-schema-migration`, `rename-canonical-id`), content-addressing lifecycle (`promote-item`), discovery/introspection (`read-config`, `read-schema`, `read-samples-catalog`, `read-catalog-schema`, `list-tools`, `context-current-state`, `context-bootstrap-state`), lifecycle/state (`context-status`, `context-validate`, `context-validate-relations`, `context-reconcile`, `complete-task` — the validator ops, `context-roadmap-validate` included, accept optional narrowing filters + `offset`/`limit` pagination bounding the returned `issues[]`: `severity`/`code` on all three, `block` additionally on `context-validate` only (its issues carry a `block` field; the relation/roadmap validators reject `block` as an unknown parameter); `status` always reflects the FULL evaluation, and each op's over-cap boundary refusal names its own parameters), substrate management (`context-init`, `context-accept-all`, `context-install`, `context-switch`, `context-list`, `context-archive`), and the derived roadmap (`context-roadmap-load/render/validate` — a view over authored `milestone_precedes_milestone` edges with per-milestone phase/task rollups) — all writes carry automatic schema validation. Read `packages/pi-context/skills/pi-context/SKILL.md` or call `list-tools` for the current set.

**Item identity + cross-substrate.** Identity-bearing items carry a three-layer identity — a mutable `id` refname, a content-independent `oid` minted once and immutable, a `content_hash` over the item's content projection (persisted to a git-tracked `objects/<content_hash>.json` store), and a `content_parent` version chain. Each substrate's `config.json` carries a `substrate_id`; a project-root `.pi-context-registry.json` enumerates all substrates by `substrate_id` so closure-table edges can point across substrates (a `{kind:"item", oid, substrate_id}` endpoint), resolved/classified by `resolveRef` as active/foreign/dangling/unregistered.

**Commands:**
- `/context init <substrate-dir>` — write the substrate skeleton (bootstrap pointer + dirs; no config, no schemas, no blocks); refuses with loud error when the existing pointer's `contextDir` differs from the caller's argument (points to `/context switch -c <new-dir>` as the correct command for that operation)
- `/context accept-all` — adopt the packaged conception (samples/conception.json) as config.json
- `/context install` — reconcile `<substrate-dir>/` against `installed_schemas` / `installed_blocks` / `installed_agents` declared in `config.json` from the package registry. Populated block data is never overwritten (reported as `preserved`), and empty or absent blocks get the catalog starter. Declared agent specs materialize under `agents/` (output schemas under `agents/schemas/`) as editable project-tier specs, never overwritten even under `--update` and never baselined. Install also base-stamps each as-installed schema body into the object store and records an install baseline (`config.installed_from`: catalog source + per-schema fingerprint of the installed schemas) used for installed-vs-catalog drift detection; the baseline covers schemas only and a re-install on an unchanged substrate is idempotent. (Bringing the installed schema model current is `/context update`, below.)
- `/context check-status` (CLI: `pi-context context-check-status`) — read-only: reports drift between the installed schemas and the catalog (`in-sync` / `catalog-ahead` / `locally-modified` / `both-diverged` / `no-baseline` / `missing-*`), and for each schema behind the catalog (`catalog-ahead` / `both-diverged`) which schema is behind and by what version gap — the baseline → catalog version pair, or a content-only basis when the version string is unchanged; writes nothing beyond the idempotent ceremony seeds of the catalog's `config` migration declarations and the block-schema migration chains implied by each installed schema's catalog starter+schema version pair into `migrations.json` (every substrate-lifecycle ceremony seeds before its first config read, so a substrate missing a catalog-implied chain self-heals on the ceremony)
- `read-catalog-schema` (CLI: `pi-context read-catalog-schema --kind <canonical_id>`) — read-only: fetches and prints the verbatim bundled catalog `*.schema.json` body for a named block kind (the raw JSON Schema — `properties` / `definitions` / `$id`, not the `read-samples-catalog` projection), diffable locally against the installed `<substrate>/schemas/<name>.schema.json` without hunting through `node_modules`. Package-intrinsic; mutates nothing
- `/context update [--dryRun]` (CLI: `pi-context update [--dryRun]`) — bring the installed schema model current with the catalog, routing each schema by drift state: `in-sync` no-op; `catalog-ahead` resync (migration-aware); `locally-modified` / `both-diverged` reconciled by a deterministic 3-way merge of base (the as-installed body in the object store, keyed by the baseline `content_hash`) × ours (installed schema) × theirs (catalog schema) — disjoint edits auto-merge (`required` / `enum` / array-`type` nodes merge as sets), and a schema with irreconcilable per-path conflicts is left unmodified — the conflict set is returned in the op output (under `conflicts`) alongside a readable report, and the calling agent reconciles it then commits via `/context resolve-conflict` (below; no subordinate resolver is spawned). Update also additively propagates catalog-new config-registry entries (`relation_types` / `invariants` / `block_kinds` / `lenses`) absent from the config, preserving user-authored entries and any locally-diverged body of an existing entry (reported under `registryAdditions`). A version-bump `catalog-ahead` resync, or a 3-way merge/resolve-conflict that advances a schema's version, registers the shipped catalog migration chain's declarations into `migrations.json`, reported under `migrationsRegistered` (each `{ schema, from, to }`). A `catalog-ahead` schema whose resync is refused (`blocked`) carries its diagnostic under `blockedDetail` (one entry per blocked schema): the refusal reason — `no-migration-chain` (no shipped chain reaches the catalog version) vs `validation-failed` (the forward-migrated items fail the catalog schema) vs `write-failed` (a non-validation throw at the write boundary, e.g. the block writer's duplicate-item-id guard; the failures entry carries the thrown message, the items were NOT flagged invalid, and no markers or pending-blocked record are produced) — the installed→catalog version pair, and for a validation failure the per-item failures naming the failing item id, field, and constraint; on the CLI's text surface this renders as a readable report below the op output. A live `validation-failed` block also persists a pending-blocked record (`pending-blocked.json`, pinning the target catalog schema + the chain reaching it) consumable by `/context resolve-blocked` (below). A `validation-failed` block additionally gets git-style failure markers written INTO the block file at the offending items (full-line `<<<<<<< BLOCKED …` / `>>>>>>> target: …` sentinels), pinning the pre-marker bytes; the schema and `migrations.json` stay byte-unchanged, and `/context resolve-blocked` strips the markers and re-validates. `--dryRun` predicts the precise per-schema outcome (resync / migrate / block / merge / conflict) by running the forward-migration and re-validation in memory, alongside the per-blocked-schema diagnostic detail, the config-registry entries that would be added, and the migration declarations that would be registered, and writes nothing beyond the idempotent ceremony seeds (the catalog's `config` migration declarations + the catalog-implied block-schema chains; no markers). A run that refuses any schema while applying registry additions or other-schema resyncs/migrations/merges additionally reports the partiality under `partialApplication` (`applied`/`notApplied` mirrors of the result channels + a one-line summary naming what was applied alongside what was refused and why), so a blocked run never reads as a no-op; `--dryRun` reports the predicted partiality in the same shape. On a substrate whose config carries no `substrate_id`, a LIVE update establishes the identity at entry (mint + persist + register, before the first identity-stamping write) so a pre-identity substrate heals on the ceremony instead of refusing, reported under `substrateIdEstablished`; the same entry establishment runs in `install` and `resolve-blocked`, an established identity is never re-minted, and `--dryRun` establishes nothing
- `/context resolve-conflict --schemaName <name> [--schema <reconciled>]` (CLI: `pi-context resolve-conflict --schemaName <name> [--schema <reconciled>]`) — commit the reconciliation of a merge conflict `update` surfaced: writes the reconciled schema body (meta-validated, atomic), advances the merge base for that schema to the catalog, and registers a known catalog migration chain when the reconciliation advances the schema's version, so the next `update` sees the schema as `locally-modified` and its deterministic merge takes the reconciled body (base === theirs → ours), converging with zero conflicts and preserving the resolution (a bare `write-schema` does not advance the base, so `update` would re-report the same conflict). Omit `--schema` to treat the current on-disk body as already reconciled and only advance the base
- `/context resolve-blocked --schemaName <name>` (CLI: `pi-context resolve-blocked --schemaName <name>`) — commit the resolution of a schema `update` blocked. Run after fixing the block's failing items (or widening the local schema): when the block file carries git-style failure markers (written by `update`), it strips the full-line marker sentinels first, then re-validates the corrected block against the pinned target schema from the pending-blocked record, and on pass registers the chain declarations, writes the target schema, advances the block envelope and merge base to the target (so a subsequent `update` converges in-sync instead of re-blocking), and clears the pending entry; on fail it returns the remaining per-item failures and writes nothing (the marker file is left untouched). The commit itself is all-or-nothing: a throw partway through it restores every touched file byte-exact and reports the failure, never a partial commit
- `/context reconcile [--dryRun]` (CLI: `pi-context context-reconcile [--dryRun]`, auth-gated) — converge stored rollup-kind statuses with their derivation (the repair half of the `derived-status` invariant class): `--dryRun` previews the exact delta set; a live run applies it through the validated write path (identity-stamped, envelope-stamped, writer-attested). Never touches authored statuses or prose — authored statuses surface for review through `context-validate`'s signals (among them declared invariant violations, the status-vocabulary warning, the staleness sweep for `stale_conditions`-bearing items); prose is not validated, its review is human
- `/context switch <dir> | -c <new-dir> | -` — substrate-management primitive: flip pointer to existing dir, bootstrap-and-flip in one op, or round-trip to `previous_contextDir`
- `/context list` — enumerate substrate dirs with `config.json` (active marked)
- `/context archive <dir>` — move a (non-active) substrate dir to `archive/`
- `/context view <lensId>` — render a configured lens (groupByLens projection) into the conversation
- `/context lens-curate <lensId>` — surface bin-assignment suggestions for uncategorized items as a follow-up turn
- `/context status` — derived project state (source metrics, test counts, block summaries, git state)
- `/context add-work` — extract structured items from conversation into typed blocks
- `/context validate` — cross-block referential integrity checks
- `/context help` — show available subcommands

**Key concept:** Users define block types by adding JSON Schemas to `<substrate-dir>/schemas/`. Any `<substrate-dir>/*.json` file with a matching schema gets automatic write-time validation. No code changes needed to add new block types.

### pi-workflows

**Tools:** `workflow`, `workflow-list`, `workflow-agents`, `workflow-validate`, `workflow-status`, `workflow-init`

**Commands:**
- `/workflow init` — scaffold `.workflows/` directory
- `/workflow list` — discover and select a workflow to run
- `/workflow run <name>` — execute a workflow (tab-completes with discovered workflow names)
- `/workflow resume <name>` — resume from checkpoint
- `/workflow validate [name]` — validate workflow specs
- `/workflow status` — show workflow vocabulary and discovery
- `/workflow help` — show available subcommands

**Keybindings:** `Ctrl+H` pause, `Ctrl+J` resume

**Key concept:** Workflows are `.workflow.yaml` specs with typed data flow between steps. Each step runs as a subprocess with its own context window. The DAG planner infers parallelism from `${{ steps.X }}` expression references and `context` declarations. Agent steps support `context: [stepName]` to inline prior step narrative text into the dispatch prompt, complementing expression-based structured data flow. The `monitor` step type integrates behavior classification as a verification gate. Bundled agent specs (with their output schemas) ship in the pi-context samples catalog and materialize into a substrate's `agents/` via `/context install`; agent templates ship with pi-jit-agents. A substrate's own `agents/` specs win over the bundled tier, and project `.pi/templates/` override the bundled templates.

### pi-behavior-monitors

**Tools:** `monitors-status`, `monitors-inspect`, `monitors-control`, `monitors-rules`, `monitors-patterns`

**Commands:**
- `/monitors on|off` — enable/disable all monitoring
- `/monitors <name>` — inspect a monitor
- `/monitors <name> rules|patterns|dismiss|reset` — manage monitor state
- `/monitors help` — show available commands

**Programmatic API:** `invokeMonitor(name, context?)` — exported function for synchronous classification without event-handler side effects. Returns `ClassifyResult` directly.

**Key concept:** Monitors are `.monitor.json` specs with Nunjucks classify templates. They observe agent activity via Pi event handlers (`message_end`, `turn_end`, `agent_end`), classify against JSON pattern libraries using side-channel LLM calls, and steer corrections or write structured findings. Verdicts: CLEAN (no issue), FLAG (known pattern), NEW (unknown pattern, optionally learned).

### pi-agent-dispatch

**Tools (static):** `call-agent`, `author-agent-spec`, `author-tool-grant`, `run-real-checks`, `commit-attested`, `run-work-order-loop`, `write-schema-migration`

**Tools (dynamic):** composite Pi tools registered per `config.tool_operations[]` entries (kind-typed: read-files / git-log / grep-paths / command-allowlist). Each closure-binds its `instance_params` and is granted via `--tools <canonical_id>` like any built-in.

**Dispatch-layer event handlers:**
- **auth-gate** (`tool_call` handler) — intercepts canonical write-class tools (`author-agent-spec` / `author-tool-grant` / `commit-attested` / `write-schema` / `write-schema-migration` / `amend-config` / `write-block` / `rename-canonical-id` / `context-init` / `context-accept-all` / `context-install` / `context-switch` / `context-archive` / `workflow-execute` / `workflow-resume` / `workflow-init` / `monitors-control` / `monitors-rules`); refuses non-interactive contexts unconditionally; calls `ctx.ui.confirm` interactively; on confirm, stamps verified operator identity (git config user.email → process.env.USER cascade) onto `event.input.writer`. Substrate attestation reflects actually-confirming-user, not agent-supplied claim.
- **read-truncation-gate** (`tool_result` handler) — hard-refuses pi built-in `read` truncation by replacing `event.content` with single-text-item carrying canonical directive (paginate / grep / sed byte-range as appropriate per `TruncationResult` shape).

**Key concept:** an in-pi orchestrator authors agent specs + composes operation-granular capability grants, dispatches a privileged sub-agent under a bounded grant clamped to the parent's grant at dispatch, runs deterministic real-checks (build / typecheck / test / runtime demo / adversarial probe) as the terminal verdict (no LLM in the gate loop), and commits the agent's per-file changes with an `Attested-by: agent/<id>` footer. `run-work-order-loop` provides bounded iteration with a human-OK gate at iteration boundaries; on a passing real-check its commit routes through the same `commit-attested` auth-gate every other caller of that tool goes through (interactive confirm, or `completed-pending-commit` with no commit attempted in a non-interactive context); a work-order may declare `on_fail: "retry"` so a non-interactive real-check failure retries the bounded loop instead of aborting.

## Development

```bash
# Install dependencies
npm install

# Build all packages (tsc compiles to dist/)
npm run build

# Run all tests
npm test

# Run per-package
npm test -w packages/pi-context
npm test -w packages/pi-workflows
npm test -w packages/pi-behavior-monitors

# Run integration tests (requires pi on PATH, spawns LLM subprocesses)
RUN_INTEGRATION=1 npm test -w packages/pi-workflows

# Lint and format (Biome v2.4.9, scoped to packages/ + scripts/)
npm run lint       # check for lint issues
npm run format     # auto-fix formatting
npm run check      # lint + typecheck

# Clean build artifacts
npm run clean

# Derive project state
npx tsx -e "
  import { contextState } from './packages/pi-context/src/context-sdk.js';
  console.log(JSON.stringify(contextState('.'), null, 2));
"
```

## Architecture

- **Main conversation is the control plane; workflows are subordinate.** Each workflow step runs as a subprocess (`pi --mode json`) with its own context window. The main LLM orchestrates; step agents execute.
- **Agent specs are `.agent.yaml` only** (no `.md` fallback). Compiled to prompts via Nunjucks at dispatch time. Agents declare `inputSchema` for typed input validation at dispatch, `contextBlocks` to inject project block data into templates, and `output.format`/`output.schema` for output validation.
- **`contextBlocks`** — an agent YAML field. Each entry is either a bare block-name string (whole-block injection) or an object `{ name, item?, focus?, depth? }` (per-item or scoped injection). At dispatch time, string entries inject the whole block under `_<name>` (hyphens become underscores). Object entries with `item` resolve the ID via the cross-block resolver and inject under `_<name>_item` (single-entry case) or `_<name>_items` array (multi-entry case for the same name — e.g., three decisions in one contextBlocks array). `depth` controls cross-reference recursion; `focus` carries kind-specific scope hints. Templates access block data via `{{ _conventions.rules }}` or render via the per-item macros under `templates/items/` (e.g., `{% from "items/conventions.md" import render_convention %}{{ render_convention(rule) }}`). Whole-block delegators in `templates/shared/macros.md` map over items. Missing blocks are `null`; templates guard with `{% if _conventions %}`. No substrate directory means injection is skipped entirely. This is how project state flows into agent prompts declaratively.
- **`inputSchema`** — an agent YAML field defining a JSON Schema for the agent's input. Validated at dispatch time before the agent subprocess is spawned. If validation fails, the step fails immediately — no LLM call is made.
- **Per-item macros** (`templates/items/<kind>.md`) — one per block kind, each rendering a single item. Macro names follow canonical singular convention (e.g. `render_decision`, `render_feature`, `render_framework_gap`, `render_convention`); the renderer registry maps each kind to its canonical macro via `CANONICAL_MACRO_NAMES`. Macros are depth-aware: cross-block ID references inline via the `resolve` and `render_recursive` Nunjucks globals when `depth > 0`, fall back to bare-ID emission at `depth = 0`, and produce the named `cycleMarker` / `unrenderedMarker` / `notFoundMarker` sentinels on cycles, missing macros, or unresolved IDs. Budget-annotated fields render through the `enforceBudget` Nunjucks global; warnings surface on `CompiledAgent.budgetWarnings`.
- **Whole-block delegators** (`templates/shared/macros.md`) — thin `for x in data.<key> { render_<kind>_item(x) }` wrappers over the per-item macros, for callers that want to dump a whole block.
- **Shared render-helpers** (`templates/shared/render-helpers.md`) — helper macros for the recursion / optional-array / optional-scalar patterns. Per-item macros import what they need; new block kinds added later get the same recursion behavior without copy-pasting the pattern.
- All resolved via three-tier template search — users override by placing alternate macro files in `.pi/templates/items/<kind>.md` or `.pi/templates/shared/...`.
- **DAG planner infers parallelism** from `${{ steps.X }}` expression references and `context: [stepName]` declarations. Steps without explicit dependencies run sequentially by declaration order.
- **Step context injection** — agent steps with `context: [step1, step2]` get prior step `textOutput` inlined into their dispatch prompt as labeled markdown sections. Complements expression-based structured data flow with narrative text inlining.
- **Monitor step type** — workflows can invoke monitors as verification gates via `monitor: <name>`. CLEAN → completed, FLAG/NEW → failed.
- **Atomic writes** — all block and state persistence uses tmp file + rename for crash safety. State write failure is fatal.
- **Checkpoint/resume** — incomplete runs can be resumed from last completed step. `completion` field controls post-workflow message to main LLM.
- **Three-tier resource search** — project `.pi/` > user `~/.pi/agent/` > package builtin (agents, templates, workflows)
- **Workflow SDK** (`packages/pi-workflows/src/workflow-sdk.ts`) — single queryable surface for the extension's capabilities. All functions derive dynamically from code registries and filesystem. Vocabulary: `stepTypes()`, `filterNames()`, `validationChecks()`. Discovery: `availableAgents()`, `availableWorkflows()`. Contracts: `agentContracts(cwd)` projects each agent's inputSchema, contextBlocks, and output format; `agentsByBlock(cwd, blockName)` finds agents consuming a given block. Validation: `validateWorkflow()` checks 11 dimensions including inputSchema required-key matching, contextBlocks existence, StepType metadata enforcement, and template-input alignment with contextBlocks-injected variables.
- **ESM, TypeScript** compiled via `tsc` to `dist/`. Pi loads compiled JS from each package's `dist/index.js`. Cross-package imports use `.js` extensions for Node16 module resolution.
- **Skill self-install** — each extension copies its `skills/` directory to `~/.pi/agent/skills/` on activation, ensuring skills are discoverable regardless of install method.
- **Pre-commit hook** (husky, `.husky/pre-commit`) runs, in order: `npm run check`, `npm test`, `scripts/check-changelog.ts` (published-surface commits must grow `[Unreleased]`), `scripts/parity-check.ts` (op↔CLI parity), `scripts/check-config-schema.ts` (expand-contract discipline on the bundled config schema), and `scripts/check-comment-citations.ts` (delta-scoped: flags only a canonical_id tracker citation newly introduced in a `.ts`/`.tsx` comment under a package's `src/` tree, never a pre-existing one). **CI** (GitHub Actions) runs check + build + test on Node 22/23.
- All packages use **direct dependencies**. pi-jit-agents depends on pi-context for block-api reads during contextBlocks injection. pi-workflows and pi-behavior-monitors depend on pi-context for block state; consumer migration to adopt pi-jit-agents as their agent runtime is tracked in the jit-agents v2 spec at `analysis/2026-05-30-jit-agents-spec-v2.md` (markdown source-of-content) + `.context-jit-spec-v2/` (substrate source-of-structure: 3 axioms + 10 decisions + 5 concepts + 2 dispatch-modes + 6 v1-supersessions + open-question FGAPs + inter-entity edges + `full-spec-render` composition lens). pi-agent-dispatch depends on pi-context + pi-jit-agents. pi-context has no knowledge of workflows, monitors, jit-agents, or agent-dispatch.

## For LLMs

When working in this repository:

- **Read package READMEs** for detailed API docs: [pi-context](https://github.com/davidorex/pi-project-workflows/blob/HEAD/packages/pi-context/README.md), [pi-context-cli](https://github.com/davidorex/pi-project-workflows/blob/HEAD/packages/pi-context-cli/README.md), [pi-jit-agents](https://github.com/davidorex/pi-project-workflows/blob/HEAD/packages/pi-jit-agents/README.md), [pi-workflows](https://github.com/davidorex/pi-project-workflows/blob/HEAD/packages/pi-workflows/README.md), [pi-behavior-monitors](https://github.com/davidorex/pi-project-workflows/blob/HEAD/packages/pi-behavior-monitors/README.md), [pi-agent-dispatch](https://github.com/davidorex/pi-project-workflows/blob/HEAD/packages/pi-agent-dispatch/README.md), [pi-project-workflows](https://github.com/davidorex/pi-project-workflows/blob/HEAD/packages/pi-project-workflows/README.md)
- **`packages/pi-context/src/context-sdk.ts`** — derived state, block discovery, the `contextState()` function
- **`packages/pi-context/src/block-api.ts`** — block CRUD with schema validation
- **`packages/pi-workflows/src/workflow-sdk.ts`** — vocabulary, discovery, introspection for workflows
- **`packages/pi-workflows/src/workflow-spec.ts`** — YAML parsing and `STEP_TYPES` registry
- **`packages/pi-workflows/src/expression.ts`** — expression evaluator and filter registry
- **`packages/pi-behavior-monitors/index.ts`** — single-file extension: monitors, classification, steering, `invokeMonitor()` export
- **`.project/`** contains this project's own block data (issues, decisions, architecture, inventory) — useful for understanding the extension's development state
- Use `/context status` to see derived metrics. Use `/workflow list` to see available workflows.

## Release

All packages use lockstep versioning — every release bumps all packages to the same version. Run from the repo root:

```bash
npm run release:patch    # bump all packages patch, update CHANGELOGs, commit, tag
npm run release:minor    # bump all packages minor
npm run release:major    # bump all packages major
```

This invokes `scripts/release.mjs`, which: checks for uncommitted changes, bumps versions across all workspaces (via `scripts/bump-versions.js`), stamps `[Unreleased]` CHANGELOG sections with the new version and date, commits, and tags `vX.Y.Z`. The script does not publish or push — after it completes, the human must run `npm publish --workspaces --access public` (requires npm login + OTP) and `git push origin main && git push origin v<version>`.

## License

MIT.
