
Pi Context
- 1 installs
- 25 repo stars
- Updated July 24, 2026
- davidorex/pi-project-workflows
Manages schema-driven project state with typed JSON blocks, closure-table relations, lens views, and cross-block referential integrity validation.
About
Provides schema-driven project state management using typed JSON blocks, schema validation, closure-table relations and lens views with referential integrity checks. A developer uses it when scaffolding project structure, adding work items, or validating and rendering project state.
- Append and update block items with automatic AJV schema validation
- Closure-table relations with a separate context-validate integrity gate
Pi Context by the numbers
- 1 all-time installs (skills.sh)
- Ranked #2,479 of 3,282 Productivity & Planning skills by installs in the Skillselion catalog
- Data as of Jul 29, 2026 (Skillselion catalog sync)
npx skills add https://github.com/davidorex/pi-project-workflows --skill pi-contextAdd your badge
Show developers this skill is listed on Skillselion. Paste this into your README.
| Installs | 1 |
|---|---|
| repo stars | ★ 25 |
| Last updated | July 24, 2026 |
| Repository | davidorex/pi-project-workflows ↗ |
What it does
Manages schema-driven project state with typed JSON blocks, closure-table relations, lens views, and cross-block referential integrity validation.
Files
<tools_reference> <tool name="append-block-item"> Append an item to an array in a project block file. Schema validation is automatic. Set autoId:true to allocate the next id from the block's id pattern when the item has no id.
Append items to project blocks (issues, decisions, or any user-defined block)
| Parameter | Type | Required | Description |
|---|---|---|---|
block | string | yes | Block name (e.g., 'issues', 'decisions') |
arrayKey | string | yes | Array key in the block (e.g., 'issues', 'decisions') |
item | unknown | yes | Item object to append — must conform to block schema |
autoId | boolean | no | When true and the item has no id, allocate the next id from the block's id pattern |
</tool>
<tool name="update-block-item"> Update fields on an item in a project block array. Finds by predicate field match.
Update items in project blocks — change status, add details, mark resolved
| Parameter | Type | Required | Description |
|---|---|---|---|
block | string | yes | Block name (e.g., 'issues', 'decisions') |
arrayKey | string | yes | Array key in the block |
match | object | yes | Fields to match (e.g., { id: 'ISSUE-NNN' }) |
updates | object | yes | Fields to update (e.g., { status: 'resolved' }) |
</tool>
<tool name="append-relation"> Append a closure-table relation (edge: parent, child, relation_type, optional ordinal) to relations.json. Shape is AJV-validated; an exact-duplicate edge (same parent+child+relation_type) is a no-op. Reference integrity (endpoints resolve, relation_type registered, no cycle) is NOT checked here — run context-validate after. Creates relations.json if absent.
Create a relation/edge between two items (parent→child under a relation_type)
| Parameter | Type | Required | Description |
|---|---|---|---|
parent | string | yes | Canonical id (or lens bin name) of the parent endpoint |
child | string | yes | Canonical id of the child endpoint |
relation_type | string | yes | Registered relation_type canonical_id / hierarchy edge type / lens id |
ordinal | integer | no | Optional sibling-ordering within (parent, relation_type) |
dryRun | boolean | no | Preview without writing relations.json |
</tool>
<tool name="remove-relation"> Remove the single closure-table relation (edge) matching parent+child+relation_type from relations.json. Matches on the SAME (parent, child, relation_type) dedup identity append-relation uses, so it is the symmetric inverse of append-relation (ordinal is NOT part of identity). An absent edge is an idempotent no-op. Reference integrity is NOT checked here — run context-validate after if the removal changes resolvability.
Remove a relation/edge between two items (the inverse of append-relation)
| Parameter | Type | Required | Description |
|---|---|---|---|
parent | string | yes | Canonical id (or lens bin name) of the parent endpoint |
child | string | yes | Canonical id of the child endpoint |
relation_type | string | yes | Registered relation_type canonical_id / hierarchy edge type / lens id |
dryRun | boolean | no | Preview without writing relations.json |
</tool>
<tool name="replace-relation"> Atomically replace one closure-table relation with another in a SINGLE write (no half-state: the old edge and the new edge never coexist on disk). The old edge is matched on the (parent, child, relation_type) dedup identity; the new edge is written with its optional ordinal. If the old edge is absent the call is effectively an append of the new edge. Reference integrity is NOT checked here — run context-validate after.
Atomically swap one relation/edge for another in a single write
| Parameter | Type | Required | Description |
|---|---|---|---|
old_parent | string | yes | Parent endpoint selector of the edge to remove |
old_child | string | yes | Child endpoint selector of the edge to remove |
old_relation_type | string | yes | relation_type of the edge to remove |
parent | string | yes | Parent endpoint selector of the replacement edge |
child | string | yes | Child endpoint selector of the replacement edge |
relation_type | string | yes | relation_type of the replacement edge |
ordinal | integer | no | Optional sibling-ordering within (parent, relation_type) for the new edge |
dryRun | boolean | no | Preview without writing relations.json |
</tool>
<tool name="append-relations"> Append MANY closure-table relations to relations.json in a single write. Each edge is an object { parent, child, relation_type, ordinal? }. Per-(parent, child, relation_type) duplicates are skipped (against on-disk edges AND earlier edges in the same batch). Returns appended/skipped counts. Reference integrity is NOT checked here — run context-validate after. Creates relations.json if absent.
Create many relations/edges between items in one write
| Parameter | Type | Required | Description |
|---|---|---|---|
edges | unknown | yes | JSON array of { parent, child, relation_type, ordinal? } selector objects (parent/child are id/lens-bin selectors) |
dryRun | boolean | no | Preview without writing relations.json |
</tool>
<tool name="upsert-block-item"> Append-or-replace an item in a project block array by id: if an item with the same idField value exists it is REPLACED (full-shape replacement, not shallow-merge — use update-block-item for merge); otherwise the item is appended. Schema validation is automatic. idField defaults to 'id'.
Append-or-replace a full block item by id (replacement, not merge)
| Parameter | Type | Required | Description |
|---|---|---|---|
block | string | yes | Block name (e.g., 'issues', 'decisions') |
arrayKey | string | yes | Array key in the block (e.g., 'issues', 'decisions') |
item | unknown | yes | Full item object to upsert — must conform to block schema |
idField | string | no | Field used as the upsert key (default 'id') |
dryRun | boolean | no | Preview the upsert without writing |
</tool>
<tool name="promote-item"> Promote a substrate item into another (registered) substrate as a NEW content-addressed item, recording the 'item_derived_from_item' lineage edge in the destination relations.json (parent = the new derived item, child = the source, carrying the source content_hash). The destination write-path mints a fresh oid + content_hash + content object. When the source block's status enum supports it, the source is marked superseded. Preconditions (unresolvable/non-item source, unregistered destination alias, unregistered destination relation_type, refname collision) throw. Pass dryRun to compute the destination without writing.
Promote an item into another substrate as a derived copy with a lineage edge
| Parameter | Type | Required | Description |
|---|---|---|---|
source | string | yes | Source item selector (bare refname / <alias>:<refname>) |
destinationSubstrate | string | yes | Registered destination substrate alias |
newRefname | string | no | Explicit destination refname (else allocated from the dest block id pattern) |
dryRun | boolean | no | Compute the destination without writing any channel |
writer | object | yes | DispatchContext.writer per pi-context/src/dispatch-context.ts. |
</tool>
<tool name="append-block-nested-item"> Append an item to a nested array on a parent-array item in a project block. Schema validation is automatic.
Append items to nested arrays inside parent items (e.g., findings inside a review)
| Parameter | Type | Required | Description |
|---|---|---|---|
block | string | yes | Block name (e.g., 'spec-reviews') |
arrayKey | string | yes | Parent array key (e.g., 'reviews') |
match | object | yes | Fields to match the parent item (e.g., { id: 'REVIEW-NNN' }) |
nestedKey | string | yes | Nested array key on the matched parent (e.g., 'findings') |
item | unknown | yes | Item object to append to the nested array — must conform to schema |
</tool>
<tool name="update-block-nested-item"> Update fields on a nested-array item inside a parent-array item in a project block. Finds parent and nested by predicate field match. Throws on parent or nested miss (mirrors update-block-item semantics).
Update items inside nested arrays — change finding state, mark resolved
| Parameter | Type | Required | Description |
|---|---|---|---|
block | string | yes | Block name (e.g., 'spec-reviews') |
arrayKey | string | yes | Parent array key (e.g., 'reviews') |
match | object | yes | Fields to match the parent item (e.g., { id: 'REVIEW-NNN' }) |
nestedKey | string | yes | Nested array key on the matched parent (e.g., 'findings') |
nestedMatch | object | yes | Fields to match the nested item (e.g., { id: 'F-001' }) |
updates | object | yes | Fields to update on the nested item (e.g., { state: 'resolved' }) |
</tool>
<tool name="remove-block-item"> Remove items matching a predicate from a top-level array in a project block. Idempotent — returns { removed: 0 } on no match without throwing. Schema validation runs after removal.
Remove items from project blocks — prune retracted issues, dedupe entries
| Parameter | Type | Required | Description |
|---|---|---|---|
block | string | yes | Block name (e.g., 'issues') |
arrayKey | string | yes | Top-level array key (e.g., 'issues') |
match | object | yes | Fields to match (e.g., { id: 'ISSUE-NNN' }) |
</tool>
<tool name="remove-block-nested-item"> Remove items matching a predicate from a nested array on a parent-array item in a project block. Throws on parent miss; returns { removed: 0 } on nested miss without throwing.
Remove nested items — drop rejected findings, retract nested references
| Parameter | Type | Required | Description |
|---|---|---|---|
block | string | yes | Block name (e.g., 'spec-reviews') |
arrayKey | string | yes | Parent array key (e.g., 'reviews') |
match | object | yes | Fields to match the parent item (e.g., { id: 'REVIEW-NNN' }) |
nestedKey | string | yes | Nested array key on the matched parent (e.g., 'findings') |
nestedMatch | object | yes | Fields to match the nested items to remove (e.g., { id: 'F-001' }) |
</tool>
<tool name="read-block-dir"> Enumerate and parse all .json files in a <substrate-dir>/<subdir>/ directory, returned as a sorted array. Missing directories return [].
Enumerate project block subdirectories (phases, schemas, etc.) as parsed JSON
| Parameter | Type | Required | Description |
|---|---|---|---|
subdir | string | yes | Subdirectory under the substrate dir (e.g., 'phases', 'schemas') |
</tool>
<tool name="read-block"> Read a project block file as structured JSON.
Read a project block as structured JSON
| Parameter | Type | Required | Description |
|---|---|---|---|
block | string | yes | Block name (e.g., 'issues', 'tasks', 'requirements') |
</tool>
<tool name="write-block"> Write or replace an entire project block with schema validation.
Write or replace a project block with schema validation
| Parameter | Type | Required | Description |
|---|---|---|---|
block | string | yes | Block name (e.g., 'project', 'architecture') |
data | unknown | yes | Complete block data — must conform to block schema |
</tool>
<tool name="context-status"> Get derived context state — source metrics, block summaries, planning lifecycle status.
Get context state — source metrics, block summaries, planning lifecycle status
</tool>
<tool name="context-check-status"> Read-only installed-vs-catalog schema drift report — per installed schema the drift state, the baseline and catalog versions, and for behind schemas (catalog-ahead / both-diverged) the version delta (baseline -> catalog) or the content-only basis when the version string is unchanged. The front of the check-status -> update --dryRun -> update sequence; writes nothing.
Report installed-vs-catalog schema drift + the version gap for behind schemas (read-only)
</tool>
<tool name="context-validate"> Validate cross-block referential integrity — check that IDs referenced across blocks exist.
Validate cross-block referential integrity
</tool>
<tool name="read-config"> Read the substrate config.json as structured JSON — vocabulary, lenses, relation_types, status_buckets, display_strings, layers, block_kinds, installed_schemas, installed_blocks. Address ONE registry/map via registry (e.g. relation_types) and ONE entry within it via id (canonical_id) instead of reading the whole config.
Read project config — vocabulary, lenses, relation_types, status_buckets
| Parameter | Type | Required | Description |
|---|---|---|---|
registry | string | no | Address ONE config registry/map by key (e.g. 'relation_types', 'lenses', 'block_kinds', 'status_buckets') |
id | string | no | With registry: address ONE entry within it by canonical_id |
</tool>
<tool name="list-tools"> Discover the agent's own tool surface (all loaded extensions + builtins). Default returns a COMPACT index — one line per tool (name · param-count · one-line description) plus the active set — not the full JSON-schemas. Pass name to fetch ONE tool's full descriptor (name + description + parameter JSON-schema + sourceInfo). Index-then-detail pattern.
Discover available tools — compact index, or one tool's full descriptor via `name`
| Parameter | Type | Required | Description |
|---|---|---|---|
name | string | no | Address ONE tool by name → full descriptor (params schema + sourceInfo) |
</tool>
<tool name="read-samples-catalog"> Enumerate installable sample block kinds (packaged view): per kind — title, description, item shape, applicable relation_types (as source/target), invariants, lenses — plus top-level relation_type/lens/invariant/layer/status_bucket registries. Package-intrinsic: reads the extension's bundled samples catalog, independent of any project. Optional kind returns one packaged kind.
Discover installable sample block kinds — title, shape, relation_types, invariants, lenses
| Parameter | Type | Required | Description |
|---|---|---|---|
kind | string | no | Filter to one block_kind canonical_id (e.g. 'tasks') |
</tool>
<tool name="read-catalog-schema"> Fetch and print the verbatim catalog schema body (raw JSON Schema: properties/definitions/$id) for a named block kind — diffable locally against the installed <substrate>/schemas/<name>.schema.json without touching node_modules. Read-only; the projection-returning sibling is read-samples-catalog.
Fetch and print the verbatim catalog schema body for a named block kind (raw JSON Schema, diffable locally)
| Parameter | Type | Required | Description |
|---|---|---|---|
kind | string | yes | Catalog block_kind canonical_id (e.g. 'tasks') |
</tool>
<tool name="context-current-state"> Derive 'where are we + what's next' purely from the substrate — focus, in-flight tasks, ranked atomic-next actions (open framework-gaps then unblocked planned tasks), and blocked tasks. Task readiness honors both task_depends_on_task dependencies and task_gated_by_item gates: a planned task whose dependency or gate target has not reached the complete bucket is reported blocked (the blocker id in blockedBy) and held out of nextActions; a gate target of any kind (gap/decision/feature/task) reaching its complete status releases the gate. No writes; nothing hand-stored.
Derive current project state — focus, in-flight, next actions, dependency- and gate-aware blocked
</tool>
<tool name="context-bootstrap-state"> Derive the substrate bootstrap state for the cwd, purely from the filesystem: 'no-pointer' | 'no-config' | 'skeleton' | 'not-installed' | 'ready', plus the resolved contextDir and any declared-but-unmaterialized installed assets. Bootstrap (/context init or /context switch -c <new-dir>) now writes a minimal schema-valid config empty of vocabulary, so a freshly-bootstrapped substrate lands at 'skeleton' — onward via /context accept-all (adopt the packaged catalog, then /context install) OR amend-config / edit (build a custom vocabulary). Unlike every other tool, this NEVER throws on an un-bootstrapped substrate — it returns 'no-pointer' so you can detect a fresh substrate and tell the user to run /context init <substrate-dir> → /context accept-all → /context install (bootstrap requires user authorization via interactive confirmation). No writes.
Derive substrate bootstrap state — no-pointer | no-config | skeleton | not-installed | ready (never throws pre-bootstrap)
</tool>
<tool name="rename-canonical-id"> Rename a canonical_id (kind: item | relation_type | lens | layer) from oldId to newId across all substrate surfaces that carry it as DATA — item home block + relations.json edges, or the relevant config registries. Out-of-substrate occurrences (analysis MDs, git history) are REPORTED, never rewritten. block_kind renames are unsupported (filesystem cascade). Use dryRun to preview the would-change counts without writing.
Rename a canonical_id (item/relation_type/lens/layer) across substrate; dryRun to preview
| Parameter | Type | Required | Description |
|---|---|---|---|
kind | string | yes | One of: item |
oldId | string | yes | Current canonical_id to rename from |
newId | string | yes | New canonical_id to rename to |
dryRun | boolean | no | Compute would-change counts without writing |
</tool>
<tool name="amend-config"> Scoped add / replace / remove of ONE entry in ONE config.json registry (block_kinds, relation_types, lenses, layers, invariants, status_buckets, display_strings, naming, installed_schemas, installed_blocks, hierarchy). The whole resulting config is AJV-validated (SHAPE) and op-correctness is enforced (add ⇒ key absent, replace/remove ⇒ key present). Cross-registry referential integrity (removing a still-referenced relation_type / lens / layer / block_kind) is NOT checked here — run context-validate after. dryRun previews without writing.
Add/replace/remove one entry in a config.json registry (vocabulary, lenses, invariants, status_buckets)
| Parameter | Type | Required | Description |
|---|---|---|---|
registry | string | yes | One of: block_kinds |
operation | string | yes | add |
key | string | yes | Entry key: id for keyed-array (block_kinds/relation_types/lenses/layers/invariants), map key for map (status_buckets/display_strings/naming), the string value for string-array (installed_schemas/installed_blocks), or a JSON {parent_block, child_block, relation_type} for hierarchy |
entry | unknown | no | Entry payload: object for keyed-array/hierarchy, string for map value; omit for remove. For keyed-array its id field must equal key; for string-array (when given) it must equal key |
dryRun | boolean | no | Preview the op without writing config.json |
</tool>
<tool name="read-schema"> Read a substrate schema by name as parsed JSON. Returns null when the schema file is absent. Address ONE property via path (dotted/bracket, e.g. properties.tasks.items.properties.status) instead of reading the whole schema.
Read a block schema as structured JSON — optionally address one property via `path`
| Parameter | Type | Required | Description |
|---|---|---|---|
schemaName | string | yes | Schema name without extension (e.g., 'tasks', 'decisions', 'issues') |
path | string | no | Address ONE property by dotted/bracket path (e.g. 'properties.tasks.items.properties.status') |
</tool>
<tool name="write-schema"> Create or replace a substrate block-kind JSON Schema. operation 'create' requires the schema absent; 'replace' requires it present. The body is AJV draft-07 meta-validated before an atomic write. Schema version bumps require a companion migration declaration via write-schema-migration; without one, read/write of items declaring an older schema_version throws version-mismatch. Registering the block_kind that points at this schema is a separate step (amend-config block_kinds).
Create or replace a block-kind JSON Schema (meta-validated, atomic)
| Parameter | Type | Required | Description |
|---|---|---|---|
operation | string | yes | create |
schemaName | string | yes | Schema name without extension (e.g., 'tasks') |
schema | unknown | yes | The whole JSON Schema object (draft-07). Accepts a JSON string. |
dryRun | boolean | no | Meta-validate without writing |
</tool>
<tool name="resolve-conflict"> Commit the reconciliation of a schema merge conflict surfaced by update. Run this AFTER reconciling a both-diverged conflict update reported: it writes the reconciled schema body (meta-validated, atomic, operation 'replace') AND advances the merge base for that schema to the packaged catalog body. Advancing the base is the step a bare write-schema lacks — without it, update's 3-way merge re-derives the SAME conflict on every subsequent run because the base never moves off the original pre-conflict body. With the base advanced to the catalog, the next update sees the schema as locally-modified (base === catalog ≠ your body) and the deterministic merge takes your reconciled body (base === theirs → ours) — auto-merging with zero conflicts and preserving your resolution. If schema is omitted, the current on-disk schema is treated as already reconciled and only the base is advanced. The calling agent runs this; no subordinate resolver is spawned.
Commit a reconciled schema conflict: write the resolved body + advance the merge base to the catalog so update stops re-reporting it (run after reconciling an update conflict)
| Parameter | Type | Required | Description |
|---|---|---|---|
schemaName | string | yes | Schema name without extension (e.g., 'tasks') |
schema | unknown | no | The reconciled schema body R (whole JSON Schema object, draft-07; accepts a JSON string). If omitted, the current on-disk schema is treated as already reconciled and only the merge base is advanced. |
</tool>
<tool name="resolve-blocked"> Commit the resolution of a blocked schema surfaced by update. Run AFTER fixing the block's items (or widening the local schema): when the block file carries git-style failure markers (written by update), strips the full-line marker sentinels first, then re-validates the corrected block against the pinned target schema from the pending-blocked record; on pass registers the migration chain, writes the target schema, advances the merge base to the target (so a subsequent update converges instead of re-blocking), and clears the pending entry; on fail reports the remaining per-item failures and writes nothing.
Commit a blocked schema's resolution: strip any git-style failure markers, re-validate the corrected block against the pinned target, then write the target schema + advance the base + clear the pending record (run after fixing the items update reported blocked)
| Parameter | Type | Required | Description |
|---|---|---|---|
schemaName | string | yes | Schema name with a pending-blocked entry (from update's blocked report) |
</tool>
<tool name="write-schema-migration"> Declare a schema version-bump migration into substrate (migrations.json). operation 'create' appends a new declaration; 'replace' overwrites an existing declaration matched by (schemaName, fromVersion); 'remove' drops a declaration. kind='identity' asserts the bump is shape-compatible (no data transform); kind='declarative-transform' carries a TransformSpec of rename/set/delete/coerce operations on dotted JSON paths. The loaded MigrationRegistry resolves the recorded edge at next read/write so block items declaring an older schema_version walk forward without process restart. Requires user authorization via interactive confirmation at the pi-dispatch auth-gate; on confirm, the verified terminal-operator identity is stamped as writer.
Declare a schema version-bump migration (identity or declarative-transform) into migrations.json
| Parameter | Type | Required | Description |
|---|---|---|---|
operation | string | yes | create |
schemaName | string | yes | Schema name without extension (e.g., 'tasks'). |
fromVersion | string | yes | Source schema semver this migration walks forward FROM. |
toVersion | string | yes | Destination schema semver this migration produces. Must differ from fromVersion. Ignored for operation=remove. |
kind | string | no | identity |
transform | unknown | no | TransformSpec body — required when kind='declarative-transform'; forbidden when kind='identity'. Accepts a JSON string. |
writer | object | yes | DispatchContext.writer per pi-context/src/dispatch-context.ts. |
</tool>
<tool name="context-init"> Initialize the substrate dir: bootstrap pointer + dirs + a minimal schema-valid SKELETON config empty of vocabulary. Lands at the 'skeleton' bootstrap state — onward via accept-all (adopt the packaged catalog, then install) OR amend-config / edit (build a custom vocabulary).
Initialize the substrate dir (bootstrap pointer + dirs + skeleton config; onward via accept-all OR amend-config/edit)
| Parameter | Type | Required | Description |
|---|---|---|---|
contextDir | string | yes | Substrate dir name (e.g. .context). Required — no default. |
</tool>
<tool name="context-accept-all"> Adopt the canonical packaged conception (samples/conception.json) as this substrate's config.json (accept-all). Writes config only — run install after. Skeleton-aware: overwrites a SKELETON config (the empty-of-vocabulary config init / switch -c writes) but never a POPULATED one.
Adopt the canonical conception as config (accept-all)
</tool>
<tool name="context-install"> Install (materialize) the schemas and starter blocks declared in config.json's installed_schemas / installed_blocks from the package samples catalog. Default skip-if-exists (installed files never overwritten without --update); populated block data is always preserved (even with --update); empty or absent blocks get the catalog starter. Records the install baseline (config.installed_from: catalog source + per-schema fingerprint) for installed-vs-catalog drift detection (schemas only). A re-install on an unchanged substrate is idempotent.
Install declared schemas + starter blocks from the samples catalog (skip-if-exists; --update re-syncs schemas + replaces empty blocks; records the config.installed_from baseline)
| Parameter | Type | Required | Description |
|---|---|---|---|
update | boolean | no | When true, re-sync existing installed schemas (migration-aware) and replace empty blocks with the catalog starter; populated block data is never overwritten. When false (default), skip existing files. |
</tool>
<tool name="update"> Bring the installed substrate model (schemas) current with the packaged catalog. Per installed schema, consults the read-only drift check and routes by state: an already-current (in-sync) schema is a no-op; a schema the package shipped a newer version of (catalog-ahead) is re-synced through the migration-aware path; a schema edited locally (locally-modified / both-diverged) is reconciled by a deterministic 3-way merge of base (the as-installed body in the object store, keyed by the recorded baseline content_hash) × ours (the installed schema) × theirs (the catalog schema) — disjoint edits auto-merge so both the user's and the catalog's changes survive (required / enum / array-valued 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 resolve-conflict — which writes the reconciled body AND advances the merge base to the catalog so update stops re-reporting it (no subordinate resolver is spawned); undecidable / absent schemas (no-baseline / missing-catalog / missing-installed) are reported, not touched. Update also additively propagates catalog-new config-registry entries (relation_types / invariants / block_kinds / lenses) that are absent from the substrate config, preserving every user-authored entry and any locally-diverged body of an existing entry (additive-only — present entries are never overwritten). Update reports, under migrationsRegistered, the migration declarations a version-bump resync registers into migrations.json (each as schema / from / to). A blocked (refused) catalog-ahead schema additionally carries its diagnostic detail 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) — the installed -> catalog version pair, and for a validation failure the per-item failures naming the failing item id, field, and constraint. A live blocked resync also persists a pending-blocked record (pinning the target catalog schema + the chain reaching it) consumable by resolve-blocked, which commits the resolution once the block's items are fixed. Pass dryRun to preview the per-schema action plan; dryRun predicts the precise per-schema catalog-ahead outcome (resync / migrate / block / merge / conflict) by running the forward-migration + re-validation in memory, the per-blocked-schema diagnostic detail, the config-registry entries that would be added, AND the migration declarations that would be registered, writing nothing. When a catalog-ahead resync is blocked because the block's items fail the catalog schema (validation-failed), update inscribes git-style failure markers INTO the block file at the offending items (full-line <<<<<<< BLOCKED … / >>>>>>> target: … sentinels), pinning the pre-marker bytes so resolve-blocked can strip the markers and re-validate; the schema and migrations.json stay byte-unchanged. A dryRun preview writes no markers.
Update the installed schema model from the catalog (3-way merges locally-modified schemas, preserving non-conflicting edits; conflicts → returned in the op output + a report for the calling agent to reconcile and commit via resolve-conflict; a blocked resync carries blockedDetail — reason, version pair, per-item failures — and persists a pending-blocked record (target catalog schema + the chain reaching it) resolved via resolve-blocked once the block's items are fixed; a validation-failed block is marked in place with git-style failure markers (recoverable; stripped + re-validated by resolve-blocked); --dry-run predicts the precise per-schema outcome — resync / migrate / block / merge / conflict — via in-memory forward-migration + re-validation, writing nothing)
| Parameter | Type | Required | Description |
|---|---|---|---|
dryRun | boolean | no | Preview the per-schema action plan without writing anything. |
</tool>
<tool name="validate-block-items"> Validate a block's items against the catalog schema version — returns the per-item failures (item id, field, constraint) without writing. Resolves the block's catalog block_kind, loads the installed block, forward-migrates its items in memory through the shipped chain when the block lags the catalog version (a fresh registry; never warms the project's cache), and validates against the catalog schema body. Returns block / from (the block's declared version) / to (the catalog version) / valid / failures[] (each: itemId — the failing item's id when the instancePath resolves to one — instancePath, keyword, message). Read-only: never overwrites the schema, the block, or migrations.json. An unknown block or a missing installed block file throws.
Validate a block's items against the catalog schema version — returns the per-item failures (item id, field, constraint) without writing
| Parameter | Type | Required | Description |
|---|---|---|---|
block | string | yes | Block name (e.g. 'tasks') |
</tool>
<tool name="context-switch"> Flip the bootstrap pointer to a different substrate dir (parallel to git switch). Default: flip to an existing substrate at target_dir (requires config.json present). create_new=true: bootstrap a fresh substrate at target_dir AND flip in one operation. to_previous=true: flip back to the pointer's previous_contextDir (target_dir ignored).
Switch the bootstrap pointer to a different substrate dir
| Parameter | Type | Required | Description |
|---|---|---|---|
target_dir | string | yes | Substrate dir name to switch to (e.g. '.context'). Required for default + create_new modes; ignored for to_previous mode. |
create_new | boolean | no | When true, bootstrap target_dir as a fresh substrate (dirs + a minimal schema-valid SKELETON config empty of vocabulary — onward via accept-all OR amend/edit) AND flip the pointer in one operation (parallel to 'git switch -c <branch>'). Default false (flip to existing substrate; fails if target_dir lacks config.json). |
to_previous | boolean | no | When true, flip the pointer back to its previous_contextDir (parallel to 'git switch -'). Requires the pointer to carry a previous_contextDir (a prior switch must have populated it). When true, target_dir is ignored. |
writer | object | no | DispatchContext.writer — stamped by auth-gate on operator confirm; in-body trusts the stamped value. |
</tool>
<tool name="context-list"> Enumerate top-level dirs under cwd containing a config.json (switchable substrates). Marks the active one with isActive=true. Read-only.
List switchable substrate dirs under cwd
</tool>
<tool name="context-archive"> Move a non-active substrate dir to archive/<dir>/. Refuses to archive the active substrate (the dir the bootstrap pointer currently names) or to clobber an existing archive/<dir>/.
Archive a non-active substrate dir to archive/<dir>/
| Parameter | Type | Required | Description |
|---|---|---|---|
target_dir | string | yes | Substrate dir name to archive (e.g. '.project'). Refused if it is the active substrate. |
</tool>
<tool name="filter-block-items"> Filter the array items of a block by a single-field predicate (eq / neq / in / matches). Discovers the single top-level array property in the block; items missing the predicate field are never matched. Wraps the canonical readBlock + caller-side filter into one queryable surface; never mutates the block.
Filter a block's items by a predicate — eq / neq / in / matches against a single field
| Parameter | Type | Required | Description |
|---|---|---|---|
block | string | yes | Block name (e.g., 'tasks', 'decisions', 'framework-gaps', 'context-contracts') |
field | string | yes | Item field to test (e.g., 'status', 'priority', 'id') |
op | unknown | yes | Comparison operator: eq (===), neq (!==), in (value is array, item[field] in it), matches (regexp test on string) |
value | unknown | yes | Comparison value — scalar for eq/neq, array for in, regexp pattern string for matches |
</tool>
<tool name="resolve-item-by-id"> Look up the block, array key, and item payload for a given ID across all blocks in the substrate dir. Returns null when no item matches. Mirrors the resolveItemById SDK function and shares its prefix-vs-block invariant — IDs whose prefix maps to a known block but live elsewhere throw at index-build time.
Resolve a kind-prefixed ID (DEC-/FEAT-/FGAP-/issue-/REQ-/TASK-/etc.) to its owning block and item
| Parameter | Type | Required | Description |
|---|---|---|---|
id | string | yes | Kind-prefixed ID, e.g., DEC-NNNN / FEAT-NNN / FGAP-NNN / ISSUE-NNN |
</tool>
<tool name="read-block-item"> Read a single item from a named block by its id — returns the item or null. Block-scoped (unlike resolve-item-by-id, which searches all blocks by kind-prefixed id). Avoids fetching a whole large block to get one item.
Read one item from a block by id (block-scoped; null if absent)
| Parameter | Type | Required | Description |
|---|---|---|---|
block | string | yes | Block name (e.g., 'tasks', 'decisions', 'framework-gaps') |
id | string | yes | Item id within the block (e.g., 'TASK-NNN') |
</tool>
<tool name="read-block-page"> Paginate a block's items: returns { items, total, hasMore }. offset default 0, limit default 50. Use for blocks too large to fetch whole (past the 50KB read-block cap). total is the full item count; hasMore signals another page.
Paginate a block's items — offset + limit; returns {items,total,hasMore}
| Parameter | Type | Required | Description |
|---|---|---|---|
block | string | yes | Block name (e.g., 'framework-gaps', 'decisions', 'issues') |
offset | integer | no | Start index (default 0) |
limit | integer | no | Max items to return (default 50) |
</tool>
<tool name="join-blocks"> Join two blocks in one call. EDGE mode: pass relationType — pairs left items with right-block items connected by that relations.json edge (leftEndpoint parent|child, default parent). FIELD mode: pass leftField+rightField — pairs where left[leftField] === right[rightField]. Optional left pre-filter via where{Field,Op,Value}. Returns [{left, right:[]}] (right always an array; one-to-many). Use instead of N+1 read-block + resolve calls.
Join two blocks in one call — by relation edge or shared field; returns {left,right[]} pairs
| Parameter | Type | Required | Description |
|---|---|---|---|
leftBlock | string | yes | Left block name (e.g., 'tasks') |
rightBlock | string | yes | Right block name (e.g., 'verification') |
relationType | string | no | Edge mode: relations.json relation_type |
leftField | string | no | Field mode: left item field |
rightField | string | no | Field mode: right item field |
leftEndpoint | unknown | no | Edge mode: is the left item the edge parent (default) or child |
whereField | string | no | Optional left pre-filter field |
whereOp | unknown | no | |
whereValue | unknown | no | Optional left pre-filter value |
</tool>
<tool name="resolve-items-by-id"> Bulk variant of resolve-item-by-id — resolve N kind-prefixed ids against a single buildIdIndex traversal. Returns an object mapping each input id to its ItemLocation (block / arrayKey / item) or null when not found. Coexists with the singular resolve-item-by-id tool; bulk collapses the N×singular-call pattern for callers resolving multiple ids in one render pass.
Resolve a batch of kind-prefixed ids (DEC-/FGAP-/TASK-/issue-/REQ-/...) in one call
| Parameter | Type | Required | Description |
|---|---|---|---|
ids | array | yes | Array of kind-prefixed ids (DEC-/FGAP-/TASK-/issue-/REQ-/...) to resolve in one call |
</tool>
<tool name="complete-task"> Complete a task with verification gate — requires a passing verification entry targeting the task.
Complete a task — gates on passing verification before updating status
| Parameter | Type | Required | Description |
|---|---|---|---|
taskId | string | yes | Task ID to complete |
verificationId | string | yes | Verification entry ID (must target this task with status 'passed') |
</tool>
<tool name="context-validate-relations"> Validate substrate relations.json edges against config-declared lenses + hierarchy + relation_types and the cross-block id index. Returns SubstrateValidationResult with status (clean/warnings/invalid) and per-issue diagnostics.
Validate substrate relations against config + items
</tool>
<tool name="context-edges-for-lens"> Materialize the Edge[] for a named lens — synthetic edges from derived_from_field for auto-derived lenses; authored edges filtered by relation_type for hand-curated lenses; unioned items from composition members for kind=composition lenses.
Materialize edges for a named lens (auto-derived or hand-curated)
| Parameter | Type | Required | Description |
|---|---|---|---|
lensId | string | yes | Lens id from config.lenses[].id |
</tool>
<tool name="context-lens-view"> Project a config-declared lens (config.lenses[]) as a binned item-view. Without --bin, a bin->count summary (always under the read cap). With --bin, that bin's items paged by --offset/--limit. Serves target, composition, and hand-curated lenses.
Project a config-declared lens as a binned item-view — bin->count summary, or one bin's items paged
| Parameter | Type | Required | Description |
|---|---|---|---|
lensId | string | yes | Lens id from config.lenses[].id |
bin | string | no | Return this bin's items paged; omit for a bin->count summary |
offset | integer | no | Per-bin page start index (default 0) |
limit | integer | no | Per-bin page size (default 50) |
</tool>
<tool name="context-walk-descendants"> Walk closure-table descendants of a parent id under a given relation_type. Returns string[] of descendant ids (may be empty if no children or relations.json absent).
Walk closure-table descendants under a relation_type
| Parameter | Type | Required | Description |
|---|---|---|---|
parentId | string | yes | Parent id (canonical id or lens bin name) |
relationType | string | yes | Relation type from config.relation_types[].canonical_id |
</tool>
<tool name="walk-ancestors"> Walk closure-table ancestors of an item id under a given relation_type — reverse-direction counterpart to context-walk-descendants. Returns string[] of ancestor ids (may be empty if no parents or relations.json absent).
Walk closure-table ancestors under a relation_type
| Parameter | Type | Required | Description |
|---|---|---|---|
itemId | string | yes | Child item id whose ancestors are sought |
relationType | string | yes | Relation type from config.relation_types[].canonical_id |
</tool>
<tool name="find-references"> Find all closure-table edges incident on an item id (inbound, outbound, or both). Returns Edge[] preserving relation_type + ordinal per record — edge-level view, not the id-chain projection that walk-ancestors / context-walk-descendants emit.
Find closure-table edges incident on an item id
| Parameter | Type | Required | Description |
|---|---|---|---|
itemId | string | yes | Item id whose incident edges are sought |
direction | unknown | no | inbound: edges where child === itemId; outbound: edges where parent === itemId; both: union (default). |
</tool>
<tool name="gather-execution-context"> Compose a ContextBundle for a work-unit by reading its context-contract (by unit_kind) and walking declared relation_types bidirectionally per direction semantic. Returns unit + perRelationType buckets of resolved items + traversal_depth + scoped_at. Substrate primitive serving harness-confined dispatch.
Compose ContextBundle for unit + context-contract-declared bundle_relation_types
| Parameter | Type | Required | Description |
|---|---|---|---|
unitId | string | yes | Work-unit id (e.g. TASK-NNN / DEC-NNNN / FGAP-NNN) |
kind | string | yes | Unit-kind type tag (e.g. 'task', 'decision', 'verification') matching a context-contract entry's unit_kind |
maxDepth | integer | no | Override per-relation-type max_depth via Math.min against each spec.max_depth |
</tool>
<tool name="context-roadmap-load"> Load a roadmap by id and return the materialized RoadmapView (phases, lens-views, status rollup, resolved milestone, scoped phase_depends_on edges, topo-ordered phaseOrder + cycles). A phase's milestone is a MILE- milestone-block id; its satisfaction reads that milestone's derived reached (a pure phase-rollup over the parent phases of its phase_positioned_in_milestone edges, in which the phase is the parent and the milestone the child), not inline evidence. Phase ordering lives in relations.json with relation_type='phase_depends_on'.
Load a roadmap by id
| Parameter | Type | Required | Description |
|---|---|---|---|
roadmapId | string | yes | ROADMAP-NNN id from <config.root>/roadmap.json |
</tool>
<tool name="context-roadmap-render"> Render a roadmap by id as pure-textual markdown — phase order list, per-phase adjacency lines (sourced from view.edges, alphabetically sorted), status rollup counts, resolved milestone (MILE- block id + name + derived reached|planned phase-rollup), exit criteria. NO mermaid / graph syntax: per-phase Depends on: lines come strictly from authored phase_depends_on edges scoped to in-roadmap phases.
Render a roadmap as markdown
| Parameter | Type | Required | Description |
|---|---|---|---|
roadmapId | string | yes | ROADMAP-NNN id from <config.root>/roadmap.json |
</tool>
<tool name="context-roadmap-validate"> Validate every roadmap × phase × milestone in <config.root>/roadmap.json. Codes: roadmap_lens_missing, roadmap_phase_dep_missing, roadmap_phase_cycle, roadmap_composition_cycle, roadmap_milestone_missing, roadmap_status_unknown_value. A phase's milestone is a MILE- milestone-block id; roadmap_milestone_missing fires when the referenced id is absent from the milestone block. Display strings flow through config.display_strings (pi-context divergence). Optional roadmapId filter restricts issue list to a single roadmap.
Validate roadmaps
| Parameter | Type | Required | Description |
|---|---|---|---|
roadmapId | string | no | Filter to issues matching this roadmap_id (omit for full-project validation) |
</tool>
<tool name="context-roadmap-list"> List every roadmap in <config.root>/roadmap.json with id, title, optional status, and phase count. Returns [] when roadmap.json absent (opt-in block; absence is the truthful answer).
List roadmaps
</tool>
</tools_reference>
<commands_reference> <command name="/context"> Context state management
Subcommands: init, switch, list, archive, install, check-status, accept-all, view, lens-curate, roadmap-list, roadmap-view, roadmap-validate, status, add-work, validate, help </command>
</commands_reference>
<events> session_start, before_agent_start, resources_discover </events>
<bundled_resources> 12 schemas, 36 samples bundled. See references/bundled-resources.md for full inventory. </bundled_resources>
<installable_blocks>
Names valid for the installed_blocks array in <substrate-dir>/config.json. Install with /context install <block>.
| Block | Source File |
|---|---|
decisions | samples/blocks/decisions.json |
framework-gaps | samples/blocks/framework-gaps.json |
tasks | samples/blocks/tasks.json |
verification | samples/blocks/verification.json |
issues | samples/blocks/issues.json |
features | samples/blocks/features.json |
research | samples/blocks/research.json |
rationale | samples/blocks/rationale.json |
spec-reviews | samples/blocks/spec-reviews.json |
layer-plans | samples/blocks/layer-plans.json |
requirements | samples/blocks/requirements.json |
conventions | samples/blocks/conventions.json |
context-contracts | samples/blocks/context-contracts.json |
phase | samples/blocks/phase.json |
story | samples/blocks/story.json |
milestone | samples/blocks/milestone.json |
work-orders | samples/blocks/work-orders.json |
</installable_blocks>
<installable_schemas>
Names valid for the installed_schemas array in <substrate-dir>/config.json. Schemas back block validation; install with /context install <schema>.
| Schema | Source File |
|---|---|
decisions | samples/schemas/decisions.schema.json |
framework-gaps | samples/schemas/framework-gaps.schema.json |
tasks | samples/schemas/tasks.schema.json |
verification | samples/schemas/verification.schema.json |
issues | samples/schemas/issues.schema.json |
features | samples/schemas/features.schema.json |
research | samples/schemas/research.schema.json |
rationale | samples/schemas/rationale.schema.json |
spec-reviews | samples/schemas/spec-reviews.schema.json |
layer-plans | samples/schemas/layer-plans.schema.json |
requirements | samples/schemas/requirements.schema.json |
conventions | samples/schemas/conventions.schema.json |
context-contracts | samples/schemas/context-contracts.schema.json |
phase | samples/schemas/phase.schema.json |
story | samples/schemas/story.schema.json |
milestone | samples/schemas/milestone.schema.json |
work-orders | samples/schemas/work-orders.schema.json |
</installable_schemas>
<planning_vocabulary>
Block Types:
| Block | Title | Array Key | Item Fields |
|---|---|---|---|
decisions | Decisions | decisions | id, title, status (string (open |
framework-gaps | Framework Gaps | gaps | id, title, status (string (identified |
tasks | Tasks | tasks | id, description, status (string (planned |
verification | Verification | verifications | id, status (string (passed |
issues | Issues | issues | id, title, body, location, status (string (open |
features | Features | features | id, title, status (string (proposed |
research | Research | research | id, title, status (string (planned |
rationale | Design Rationale | rationales | id, title, narrative, phase? (integer), oid?, content_hash?, content_parent? |
spec-reviews | Spec Reviews | reviews | id, target, target_revision?, reviewer?, status (string (not-started |
layer-plans | Layer Restructure Plans | plans | id, title, status (string (draft |
requirements | Requirements | requirements | id, description, type (string (functional |
conventions | Conventions | rules | id, description, enforcement (string (lint |
context-contracts | Context contracts | contracts | id, unit_kind, bundle_relation_types (array), description?, notes?, created_by, created_at, modified_by?, modified_at?, oid?, content_hash?, content_parent? |
phase | Phases | phases | id, name, intent, goal?, status (string (planned |
story | Stories | stories | id, title, status (string (proposed |
milestone | Milestones | milestones | id, name, status (string (planned |
work-orders | Work Orders | work_orders | id, title, status (string (proposed |
Status Enums:
| Block | Field | Values |
|---|---|---|
decisions | status | open, enacted, superseded |
framework-gaps | status | identified, accepted, in-progress, closed, wontfix, superseded_by |
framework-gaps | priority | P0, P1, P2, P3 |
framework-gaps | layer | L1, L2, L3, L4, L5 |
tasks | status | planned, in-progress, completed, blocked, cancelled |
verification | status | passed, failed, partial, skipped |
verification | method | command, inspect, test |
issues | status | open, resolved, deferred |
issues | category | primitive, issue, cleanup, capability, composition |
issues | priority | low, medium, high, critical |
issues | source | human, agent, monitor, workflow |
features | status | proposed, approved, in-progress, in-review, complete, blocked, cancelled |
features | layer | L1, L2, L3, L4, L5 |
research | status | planned, in-progress, complete, stale, superseded, revised |
research | layer | L1, L2, L3, L4, L5 |
research | type | investigative, comparative, empirical, historical, audit, landscape, feasibility, curation |
spec-reviews | status | not-started, in-progress, complete, abandoned |
layer-plans | status | draft, proposed, decided, in-progress, complete, abandoned |
requirements | type | functional, non-functional, constraint, integration |
requirements | status | proposed, accepted, deferred, implemented, verified |
requirements | priority | must, should, could, wont |
requirements | source | human, agent, analysis |
conventions | enforcement | lint, test, review, manual |
conventions | severity | error, warning, info |
phase | status | planned, in-progress, completed |
story | status | proposed, ready, in-progress, in-review, complete, blocked |
milestone | status | planned, reached |
work-orders | status | proposed, in-progress, real-check-passed, real-check-failed, completed, cancelled |
</planning_vocabulary>
<objective> pi-context manages structured project state in the substrate directory — a directory of JSON block files validated against schemas. The substrate (config + lenses + closure-table relations) is degree-zero state that defines where the rest lives and how items group into views. </objective>
<block_files> Blocks are JSON files under the substrate root (one per block kind, each an array of items). Each block has a corresponding schema in <root>/schemas/. When you write to a block via the tools, the data is validated against its schema before persisting. Writes are atomic (tmp file + rename) and serialised per block via withBlockLock. The substrate root is the dir chosen at init (recorded in the .pi-context.json bootstrap pointer) and written to config.json's root field by /context accept-all; the framework ships no default substrate-dir name. block-api routes through resolveContextDir(cwd) — which resolves config.root when set and otherwise falls back to the pointer — so a relocated root reaches every read/write site.
Each item in an identity-bearing block carries a three-layer identity, not a refname alone: id (the mutable human label, e.g. a kind-prefixed refname), oid (a content-independent 32-hex identity minted once at the item's birth and immutable thereafter, salted by the substrate's substrate_id), content_hash (a SHA-256 over the item's content projection — the item minus its metadata fields — so identical content deduplicates), and content_parent (the prior version's content_hash, forming a per-item version chain that advances only when content actually changed). A write carrying a different incoming oid is rejected. Identity stamping is a no-op unless the block's array subschema declares all three identity fields, scoping content-addressing to canonical schemas and leaving bespoke/test schemas untouched. </block_files>
<content_addressing> On a stamping write the item's content projection (a shallow copy with the metadata fields removed) is persisted to <substrate-dir>/objects/<content_hash>.json — a content-addressed object store (idempotent, atomic tmp+rename; identical content yields a byte-identical file). The store is git-tracked: it is the integrity/version store, and gitignoring it would lose pinning. Object persistence is deferred until after the whole block clears schema validation, so a validation failure never orphans an object. The metadata fields excluded from the content hash are the mandatory floor {id, oid, content_hash, content_parent} (never hashable, never pullable into the hash by an override) plus a discretionary set (the author fields and closed_by/closed_at); a schema's item subschema may redefine the discretionary set via x-identity.metadata_fields, but the floor is always unioned in. </content_addressing>
<schema_validation> Every block write validates against <root>/schemas/<blockname>.schema.json. If the schema file doesn't exist, writes proceed without validation. Validation errors include the specific JSON Schema violations. </schema_validation>
<context_init> /context init <substrate-dir> creates the substrate skeleton: the .pi-context.json bootstrap pointer (declaring the chosen substrate-dir name) plus the substrate root and its schemas/ directory. Nothing is imposed — no config.json, no schemas, and no starter blocks are written (ship-no-defaults). Idempotent: re-running preserves existing dirs. Populate the substrate next with /context accept-all (adopt the canonical conception) followed by /context install. </context_init>
<context_accept_all> /context accept-all adopts the package's canonical packaged conception (samples/conception.json) as the substrate's config.json — the full vocabulary (block_kinds, relation_types, lenses, invariants) plus the installed_schemas / installed_blocks manifest — with root set to the actual substrate dir. It writes config.json only (run /context install after to materialize the schemas + starter blocks) and is idempotent: it never overwrites an existing config.json (offer, don't impose). This is the accept-all path; per-entry step-through curation is a separate surface. </context_accept_all>
<context_install> /context install (reflected CLI op: pi-context context-install [--update], authGated) reconciles the substrate against the installed_schemas and installed_blocks lists declared in config.json. The slash command and the CLI op run the same install engine — the op's --update maps to the same migration-aware overwrite the slash command's --update does. For each declared name it copies the matching asset from the package-shipped samples catalog (samples/schemas/ for schemas, samples/blocks/ for starter blocks) into the substrate. Default behavior is skip-if-exists (preserves user edits). Pass --update to re-sync installed SCHEMAS through the migration registry: a same-version body change (or a non-versioned schema) is a verbatim re-sync (reported resynced); a schema version bump forward-migrates the populated block's items through the shipped catalog migration chain and re-validates them against the new schema (reported migrated); a bump with no safe migration — no shipped chain reaching the catalog version, or items that would fail the new schema — is refused, leaving BOTH the installed schema file AND the block file byte-unchanged (reported blocked). Preview drift first with /context check-status. Populated block data is never overwritten — a block holding items is preserved (reported as preserved) regardless of --update, while empty or absent blocks receive the catalog starter. Sources missing from the catalog are reported as notFound. Empty install lists are not an error — the result is a clean no-op message instructing the user to edit config.json. Install also records an install baseline in config.installed_from: the catalog source (catalog = pi-context name@version, catalog_version = conception schema_version), a baseline timestamp, and a per-schema fingerprint (assets[canonical_id] = content hash + the schema's own version) of the installed SCHEMAS — the basis for installed-vs-catalog drift detection. The baseline covers schemas only (blocks are user data). A re-install on an unchanged substrate is idempotent: the existing baseline is preserved verbatim (timestamp included) so config.json stays byte-identical. /context check-status is a separate read-only command that previews drift between the installed schemas and the catalog: it compares each installed schema against its recorded baseline, the catalog's current schema, and the currently-installed file, and reports in-sync / catalog-ahead (the package shipped a newer schema) / locally-modified (the installed file was edited) / both-diverged / no-baseline / missing-catalog / missing-installed. For each schema behind the catalog (catalog-ahead / both-diverged) it additionally surfaces which schema is behind and by what version gap — behind plus a version_delta carrying the baseline → catalog version pair (a declared version bump) or a content-only basis when the catalog body moved with the version string unchanged. It writes nothing. The reflected CLI op is pi-context context-check-status --json.
The installable catalog IS the packaged conception (samples/conception.json): its block_kinds enumerate the available kinds, each carrying its schema (samples/schemas/) and starter block (samples/blocks/). The generated installable-catalog table below lists the authoritative names — declare any subset in installed_* and run /context install, or take the whole conception via /context accept-all. To inspect a catalog schema body itself, pi-context read-catalog-schema --kind <canonical_id> fetches and prints the verbatim bundled *.schema.json (the raw JSON Schema — properties / definitions / $id, distinct from the read-samples-catalog summary projection), so after /context check-status flags a schema behind the catalog you can diff that body locally against the installed <substrate>/schemas/<name>.schema.json without hunting through node_modules. It is read-only and package-intrinsic (reads the bundled catalog, mutates nothing). </context_install>
<substrate_config> <substrate-dir>/config.json is the substrate bootstrap. Its root field declares where every other block, schema, agent, and template lives — consumers resolve that dir via the .pi-context.json pointer plus config.root, never by assuming a fixed directory name. Its substrate_id field is the per-substrate root identity (pattern sub- followed by 16 hex), minted once and immutable on disk; it salts oid minting and identifies the substrate in the project-root registry. naming aliases canonical block ids to display names (used by /context view rendering). hierarchy declares legal closure-table edges (parent block → child block via relation_type). lenses declares named projections over a target block. installed_schemas / installed_blocks are the install manifest consumed by /context install. installed_from is the optional install baseline /context install records — the catalog source plus a per-schema content fingerprint of the installed schemas — for installed-vs-catalog drift detection.
A fresh substrate adopted via /context accept-all carries advisory (severity-warning) convention-articulation invariants: every decision, feature, and task should carry an item_governed_by_convention edge to a convention it follows, or an item_acknowledges_missing_convention edge to a missing-convention gap. context-validate reports an artifact that articulates neither as a warning (it does not error), so the advice surfaces without blocking writes; satisfy it by adding one of those two edges (append-relation) when filing or amending the artifact.
config.json and relations.json are exempt from config.root redirection — they always live at the substrate-dir root (the dir chosen at bootstrap, resolved via the .pi-context.json pointer, suggested .context) because they are the substrate that defines root. The substrate-dir root is whatever was chosen at bootstrap, not necessarily .project. All other state lives under <config.root>/... per resolveContextDir(cwd). The package ships their schemas in schemas/ (config.schema.json, relations.schema.json) and resolves them via three-tier search: project override > user override > package-shipped.
The loadContext(cwd) SDK returns an mtime-keyed cached snapshot of { config, relations, configMtime, relationsMtime } for one cwd. Consumers must not mutate. </substrate_config>
<cross_substrate> A project can carry multiple substrates. The .pi-context.json bootstrap pointer names the single ACTIVE substrate dir; a separate project-root, git-tracked .pi-context-registry.json enumerates ALL substrates — a version plus substrates: { <substrate_id>: { dir, aliases[] } }. resolveSubstrateDir(cwd, substrate_id) and resolveAlias(cwd, alias) look up the registry and return null on a clean miss.
resolveRef(cwd, ref) classifies any endpoint into one of four statuses: active — resolved in the active substrate's index (a bare oid/refname, or any lens_bin endpoint, which is always active without an item lookup); foreign — a structured endpoint carrying a substrate_id, or an <alias>:<refname> string whose alias is registered, resolved in the foreign substrate's index; dangling — a locator naming a registered substrate where the oid/refname is absent; unregistered — a substrate_id or alias the registry does not carry. A string with a NON-leading : (the parse gates on colon > 0) is first attempted as an <alias>:<refname> parse; a leading-colon string is not alias-parsed.
Source-of-truth-drift invariant: validateContext requires the active config.substrate_id to have a registry entry whose dir resolves to the active substrate. A mismatch yields substrate_id_registry_mismatch; a missing entry yields substrate_id_unregistered. </cross_substrate>
<schema_versioning> Schemas are draft-07 JSON-Schema, one per block kind, under <substrate-dir>/schemas/. Package-shipped substrate-singleton schemas carry a pi-context://schemas/<name> $id plus a version. <substrate-dir>/migrations.json is the per-substrate schema-version migration registry. A schema version bump REQUIRES a companion migration declared via the write-schema-migration tool; without one, reading or writing an item that declares an older schema_version throws a version mismatch. Migration kinds are identity (shape-compatible, no transform) or declarative-transform (a TransformSpec of rename/set/delete/coerce on dotted paths). The loaded registry resolves the migration edge at the next read/write, so items walk forward without a process restart. A block:<name> reference resolves to <contextDir>/schemas/<name>.schema.json. </schema_versioning>
<lens_views> Lenses are named projections over a target block. A lens declares id, target (block name), relation_type, derived_from_field (optional — synthesizes edges from a per-item field instead of requiring authored edges), bins (named groupings), and render_uncategorized.
Edges live in <substrate-dir>/relations.json as a closure table — each row is { parent, child, relation_type, ordinal? }. relation_type is a lens id, a hierarchy edge type, or a registered relation_types[].canonical_id; ordinal orders siblings within (parent, relation_type). Endpoints (both parent and child) are dual-form: a legacy string (a canonical id, a lens bin name, or an <alias>:<refname> cross-substrate sentinel; disambiguation lives in validateRelations), OR a structured item endpoint { kind: "item", oid, refname?, substrate_id?, content_hash? } where a present substrate_id marks a foreign endpoint and content_hash is carried for drift detection, OR a structured lens-bin endpoint { kind: "lens_bin", bin } — a virtual parent that never resolves to an item.
The single-form rule: ALL inter-item relationships are closure-table edges. Embedded nested id-bearing arrays and FK-as-field are forbidden — a nested id-bearing array in a schema is flagged nested_id_bearing_array by validateContext with the remediation "promote to a top-level entity + membership edge". Containment is a membership edge carrying ordinal; the nested id-bearing array → top-level entity block + ordinal-bearing membership edges promotion is performed by the canonicalizer (the context-dir-migration canonicalizeSubstrate machinery, run as a repo-side migration script under scripts/migration/ — not a packaged pi-context tool). (Distinct from the promote-item tool, which is a cross-substrate derivation: it promotes a substrate item INTO another registered substrate as a new content-addressed item, recording an item_derived_from_item lineage edge in the destination.)
The lens-view algorithm: edgesForLens(lens, items, authoredEdges) returns synthetic edges (when derived_from_field is set) or filtered authored edges (otherwise). groupByLens(items, lens, lensEdges) produces a Map<binName, ItemRecord[]>. walkDescendants(parentId, relationType, edges) traverses the closure table from any parent.
/context view <lensId> loads the lens via loadLensView(cwd, lensId), runs groupByLens, and renders the result as markdown headings + bullet lines (id + status + title) into the conversation via renderLensView. lens.render_uncategorized: false omits the uncategorized bucket. </lens_views>
<context_lens_curate> /context lens-curate <lensId> walks items in the lens's target block that have no edge in any declared bin and surfaces bin-assignment suggestions (would-be relations.json edge appends) as a follow-up turn. The LLM reads the suggestions and persists the chosen edges via append-block-item against relations.json. The command itself does not write — curation is a follow-up-turn pattern so the model decides which suggestions to enact. </context_lens_curate>
<context_view> /context view <lensId> renders a configured lens as markdown into the conversation. Bins become headings, items become bullet lines (id + status + title where present). naming aliases from config.json are honored for the target block name. Errors (missing config, unknown lens, unreadable target block, no array property in target) surface via ctx.ui.notify with severity error. </context_view>
<substrate_validation> validateRelations(cwd) (exposed as the context-validate-relations tool) checks the closure-table edges in relations.json against the config + per-block item snapshots, with the resolveRef hook classifying foreign endpoints. Diagnostics codes: edge_unknown_relation_type, edge_parent_not_in_bins, edge_unresolved_parent, edge_unresolved_child, edge_parent_wrong_block, edge_child_wrong_block, edge_cycle_detected. Returns { status: "clean" | "warnings" | "invalid", issues[] } where each issue carries the offending edge or cycle path.
validateContext(cwd) (the context-validate tool) layers the registry/identity invariants over cross-block referential integrity: substrate_id_unregistered and substrate_id_registry_mismatch (the source-of-truth-drift guard on the active substrate), edge_endpoint_dangling and edge_endpoint_unregistered (a structured endpoint naming a registered-but-absent or unregistered substrate), and nested_id_bearing_array (a schema embedding an id-bearing array instead of using a membership edge). Config-declared invariants[] and registered lens-validators are checked in the same pass.
Three derived substrate tools complement validation: context-edges-for-lens returns the materialized Edge[] for a named lens (synthetic from derived_from_field or filtered authored edges); context-lens-view projects a config-declared lens as a binned item-view — a bin→count summary, or one bin's items paged by offset/limit; context-walk-descendants returns the transitive descendant id list from a parent under a given relation_type. </substrate_validation>
<block_item_reads> Item-level reads complement whole-block read-block (which is all-or-nothing and caps at the 50KB read limit): read-block-item returns one item from a named block by its id (block-scoped — null if absent; distinct from resolve-item-by-id, which searches every block by kind-prefixed id). read-block-page paginates a block too large to fetch whole — { items, total, hasMore } where total is the full item count and pagination uses offset/limit (defaults 0/50). join-blocks — one-call cross-block join, EDGE mode (relations.json relation_type, leftEndpoint parent|child) or FIELD mode (shared field value), optional left pre-filter; returns {left, right[]} pairs; replaces N+1 read+resolve. </block_item_reads>
<context_status> /context status derives project state dynamically from the filesystem:
- Source file count and line count (
.tsfiles excluding tests) - Test count and test file count
- Schema count, block count, phase count
- Block summaries with array item counts and status distributions
- Requirements summary (total, by status, by priority) — from requirements.json (if installed)
- Tasks summary (total, by status) — from tasks.json (if installed)
- Domain entry count — from domain.json (if installed)
- Verification summary (total, passed, failed) — from verification.json (if installed)
- Handoff presence — whether handoff.json exists
- Recent git commits
- Current phase detection
</context_status>
<context_add_work> /context add-work discovers appendable blocks (blocks with array schemas), reads their schemas, and sends a structured instruction to the LLM to extract items from the conversation into the typed blocks. This is a follow-up message that triggers the LLM to use the append-block-item tool. </context_add_work>
<duplicate_detection> append-block-item checks for duplicate items by id field before appending. If an item with the same id already exists in the target array, it returns a message instead of appending. </duplicate_detection>
<context_validate> Two surfaces address related concerns. Cross-block referential integrity is EDGE-based: there are no per-block inline-FK field checks (no task.phase, task.depends_on, decision.phase, gap.resolved_by, requirement.traces_to/depends_on, verification.target, rationale.related_decisions scans) — relations.json closure-table edges are the sole reference surface.
/context validate (the context-validate tool → validateContext) is the project-wide check. It runs:
- Source-of-truth drift: when the active
config.substrate_idis set, the project-root.pi-context-registry.jsonmust carry an entry whose dir resolves to the active substrate dir (substrate_id_unregistered,substrate_id_registry_mismatch). - Edge integrity: each edge's
parent/childis classified via the unified id index across substrates — an endpoint naming an unregistered substrate alias/id errors (edge_endpoint_unregistered), one that resolves to no item errors (edge_endpoint_dangling); arelation_typeabsent fromconfig.relation_types[]errors; when a relation_type declaressource_kinds/target_kinds, an endpoint whose resolved block kind is outside the (non-*) declared set errors. - Cycle detection: delegated to
validateRelations; only itsedge_cycle_detecteddiagnostics are merged in. - Config-declared invariants:
requires-edgeandstatus-consistencyclasses are enforced generically fromconfig.invariants[]DATA — no block/status/relation_type vocabulary is hardcoded in source; each invariant's emitted code is its owninv.id. - Status-vocabulary: an item
statusvalue with no key in the declared vocabulary is a warning (status_unknown_value). - Nested id-bearing array: a schema array at nesting depth ≥ 1 whose item shape carries an
idis a warning to promote it to a top-level entity + membership edge (nested_id_bearing_array). - Lens validators: every validator registered via
registerLensValidatoris dispatched and its issues merged; a throwing validator surfaces as a warning (lens_validator_failed:<name>).
Returns issues with severity error/warning; status is invalid (any error), warnings, or clean.
The lower-level context-validate-relations tool (→ validateRelations, see <substrate_validation>) checks the closure-table edges in relations.json in isolation: unregistered relation_type (edge_unknown_relation_type), lens-bin parent not among a lens's bins (edge_parent_not_in_bins), unresolved/wrong-block parent or child (edge_unresolved_parent, edge_unresolved_child, edge_parent_wrong_block, edge_child_wrong_block), and cycles (edge_cycle_detected). </context_validate>
<update_check> On session_start, checks npm registry for newer versions of @davidorex/pi-project-workflows and notifies via UI if an update is available. Non-blocking — failures are silently ignored. </update_check>
<success_criteria>
<substrate-dir>/,<substrate-dir>/schemas/, and the.pi-context.jsonbootstrap pointer exist after/context init <substrate-dir>(init is skeleton-only: noconfig.json, no schemas, no blocks until accept-all + install). Phases are not a directory — they live as an in-block array underphase.json(pluralphaseskey); there is nophases/dir.installed_schemas/installed_blocksdeclared inconfig.jsonare reified by/context install;--updatere-syncs installed schemas through the migration registry (forward-migrate block items on a version bump, or refuse and leave unchanged when items can't be safely migrated), but populated block data is never overwritten (preserved) — only empty or absent blocks receive the catalog starter- Block writes validate against schemas — invalid data rejected with specific error
/context statusreturns current derived state without errors/context validatereturns no errors for well-formed cross-block referencescontext-validate-relationsreturns no errors for a well-formedrelations.json/context view <lensId>renders the configured projection;/context lens-curate <lensId>surfaces suggestions for uncategorized itemsappend-block-itemrejects duplicate IDs- Schema customizations (field additions, enum changes) take effect on next write
- A relocated
config.rootreaches every read/write because all path construction routes throughresolveContextDir(cwd)
</success_criteria>
Generated from source by `scripts/generate-skills.js` — do not edit by hand.
Bundled Resources
schemas/ (12 files)
schemas/bootstrap.schema.jsonschemas/config.schema.jsonschemas/context-registry.schema.jsonschemas/layer.schema.jsonschemas/migrations.schema.jsonschemas/pending-blocked.schema.jsonschemas/priority.schema.jsonschemas/relations.schema.jsonschemas/severity.schema.jsonschemas/source.schema.jsonschemas/status.schema.jsonschemas/verification-method.schema.json
samples/ (36 files)
samples/blocks/context-contracts.jsonsamples/blocks/conventions.jsonsamples/blocks/decisions.jsonsamples/blocks/features.jsonsamples/blocks/framework-gaps.jsonsamples/blocks/issues.jsonsamples/blocks/layer-plans.jsonsamples/blocks/milestone.jsonsamples/blocks/phase.jsonsamples/blocks/rationale.jsonsamples/blocks/requirements.jsonsamples/blocks/research.jsonsamples/blocks/spec-reviews.jsonsamples/blocks/story.jsonsamples/blocks/tasks.jsonsamples/blocks/verification.jsonsamples/blocks/work-orders.jsonsamples/conception.jsonsamples/migrations.jsonsamples/schemas/context-contracts.schema.jsonsamples/schemas/conventions.schema.jsonsamples/schemas/decisions.schema.jsonsamples/schemas/features.schema.jsonsamples/schemas/framework-gaps.schema.jsonsamples/schemas/issues.schema.jsonsamples/schemas/layer-plans.schema.jsonsamples/schemas/milestone.schema.jsonsamples/schemas/phase.schema.jsonsamples/schemas/rationale.schema.jsonsamples/schemas/requirements.schema.jsonsamples/schemas/research.schema.jsonsamples/schemas/spec-reviews.schema.jsonsamples/schemas/story.schema.jsonsamples/schemas/tasks.schema.jsonsamples/schemas/verification.schema.jsonsamples/schemas/work-orders.schema.json