@davidorex/pi-context 0.26.0 → 0.28.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. package/CHANGELOG.md +71 -15
  2. package/README.md +71 -18
  3. package/dist/block-api.d.ts +165 -18
  4. package/dist/block-api.d.ts.map +1 -1
  5. package/dist/block-api.js +800 -57
  6. package/dist/block-api.js.map +1 -1
  7. package/dist/citation-rot-scanner.d.ts +80 -0
  8. package/dist/citation-rot-scanner.d.ts.map +1 -0
  9. package/dist/citation-rot-scanner.js +421 -0
  10. package/dist/citation-rot-scanner.js.map +1 -0
  11. package/dist/content-hash.d.ts +25 -0
  12. package/dist/content-hash.d.ts.map +1 -0
  13. package/dist/content-hash.js +62 -0
  14. package/dist/content-hash.js.map +1 -0
  15. package/dist/context-dir.d.ts +126 -1
  16. package/dist/context-dir.d.ts.map +1 -1
  17. package/dist/context-dir.js +208 -4
  18. package/dist/context-dir.js.map +1 -1
  19. package/dist/context-registry.d.ts +122 -0
  20. package/dist/context-registry.d.ts.map +1 -0
  21. package/dist/context-registry.js +189 -0
  22. package/dist/context-registry.js.map +1 -0
  23. package/dist/context-sdk.d.ts +179 -12
  24. package/dist/context-sdk.d.ts.map +1 -1
  25. package/dist/context-sdk.js +536 -101
  26. package/dist/context-sdk.js.map +1 -1
  27. package/dist/context.d.ts +222 -4
  28. package/dist/context.d.ts.map +1 -1
  29. package/dist/context.js +244 -56
  30. package/dist/context.js.map +1 -1
  31. package/dist/git-env.d.ts +2 -0
  32. package/dist/git-env.d.ts.map +1 -0
  33. package/dist/git-env.js +29 -0
  34. package/dist/git-env.js.map +1 -0
  35. package/dist/index.d.ts +124 -2
  36. package/dist/index.d.ts.map +1 -1
  37. package/dist/index.js +440 -1125
  38. package/dist/index.js.map +1 -1
  39. package/dist/lens-view.d.ts.map +1 -1
  40. package/dist/lens-view.js +5 -3
  41. package/dist/lens-view.js.map +1 -1
  42. package/dist/migration-registry-loader.d.ts +121 -0
  43. package/dist/migration-registry-loader.d.ts.map +1 -0
  44. package/dist/migration-registry-loader.js +309 -0
  45. package/dist/migration-registry-loader.js.map +1 -0
  46. package/dist/migrations-store.d.ts +141 -0
  47. package/dist/migrations-store.d.ts.map +1 -0
  48. package/dist/migrations-store.js +206 -0
  49. package/dist/migrations-store.js.map +1 -0
  50. package/dist/object-store.d.ts +26 -0
  51. package/dist/object-store.d.ts.map +1 -0
  52. package/dist/object-store.js +104 -0
  53. package/dist/object-store.js.map +1 -0
  54. package/dist/ops-registry.d.ts +44 -0
  55. package/dist/ops-registry.d.ts.map +1 -0
  56. package/dist/ops-registry.js +1223 -0
  57. package/dist/ops-registry.js.map +1 -0
  58. package/dist/orientation.d.ts +16 -0
  59. package/dist/orientation.d.ts.map +1 -0
  60. package/dist/orientation.js +49 -0
  61. package/dist/orientation.js.map +1 -0
  62. package/dist/promote-item.d.ts +28 -0
  63. package/dist/promote-item.d.ts.map +1 -0
  64. package/dist/promote-item.js +257 -0
  65. package/dist/promote-item.js.map +1 -0
  66. package/dist/read-element.d.ts +129 -0
  67. package/dist/read-element.d.ts.map +1 -0
  68. package/dist/read-element.js +296 -0
  69. package/dist/read-element.js.map +1 -0
  70. package/dist/rename-canonical-id.d.ts.map +1 -1
  71. package/dist/rename-canonical-id.js +22 -13
  72. package/dist/rename-canonical-id.js.map +1 -1
  73. package/dist/roadmap-plan.d.ts.map +1 -1
  74. package/dist/roadmap-plan.js +23 -13
  75. package/dist/roadmap-plan.js.map +1 -1
  76. package/dist/schema-validator.d.ts +1 -0
  77. package/dist/schema-validator.d.ts.map +1 -1
  78. package/dist/schema-validator.js +7 -3
  79. package/dist/schema-validator.js.map +1 -1
  80. package/dist/schema-write.d.ts +78 -4
  81. package/dist/schema-write.d.ts.map +1 -1
  82. package/dist/schema-write.js +297 -34
  83. package/dist/schema-write.js.map +1 -1
  84. package/dist/status-vocab.d.ts +1 -0
  85. package/dist/status-vocab.d.ts.map +1 -1
  86. package/dist/status-vocab.js +4 -0
  87. package/dist/status-vocab.js.map +1 -1
  88. package/dist/truncate.d.ts +34 -0
  89. package/dist/truncate.d.ts.map +1 -0
  90. package/dist/truncate.js +85 -0
  91. package/dist/truncate.js.map +1 -0
  92. package/dist/write-schema-migration-tool.d.ts +46 -0
  93. package/dist/write-schema-migration-tool.d.ts.map +1 -0
  94. package/dist/write-schema-migration-tool.js +115 -0
  95. package/dist/write-schema-migration-tool.js.map +1 -0
  96. package/package.json +35 -2
  97. package/samples/blocks/work-orders.json +3 -0
  98. package/samples/conception.json +22 -3
  99. package/samples/migrations.json +133 -0
  100. package/samples/schemas/context-contracts.schema.json +19 -4
  101. package/samples/schemas/conventions.schema.json +68 -12
  102. package/samples/schemas/decisions.schema.json +125 -28
  103. package/samples/schemas/features.schema.json +140 -26
  104. package/samples/schemas/framework-gaps.schema.json +171 -26
  105. package/samples/schemas/issues.schema.json +86 -13
  106. package/samples/schemas/layer-plans.schema.json +195 -35
  107. package/samples/schemas/phase.schema.json +18 -3
  108. package/samples/schemas/rationale.schema.json +37 -7
  109. package/samples/schemas/requirements.schema.json +77 -10
  110. package/samples/schemas/research.schema.json +169 -36
  111. package/samples/schemas/spec-reviews.schema.json +88 -18
  112. package/samples/schemas/story.schema.json +18 -3
  113. package/samples/schemas/tasks.schema.json +66 -10
  114. package/samples/schemas/verification.schema.json +70 -12
  115. package/samples/schemas/work-orders.schema.json +195 -0
  116. package/schemas/bootstrap.schema.json +18 -6
  117. package/schemas/config.schema.json +58 -26
  118. package/schemas/context-registry.schema.json +46 -0
  119. package/schemas/migrations.schema.json +123 -0
  120. package/schemas/relations.schema.json +41 -10
  121. package/skill-narrative.md +42 -21
  122. package/skills/pi-context/SKILL.md +159 -65
  123. package/skills/pi-context/references/bundled-resources.md +7 -2
@@ -3,7 +3,7 @@ name: pi-context
3
3
  description: >
4
4
  Schema-driven project state management with typed JSON blocks, schema
5
5
  validation, substrate config, lens views, closure-table relations, and
6
- cross-block referential integrity. Use when managing .project/ blocks,
6
+ cross-block referential integrity. Use when managing substrate blocks,
7
7
  scaffolding project structure, installing block kinds from the packaged samples
8
8
  catalog, validating project state, rendering lens views, or adding work items.
9
9
  ---
@@ -31,7 +31,7 @@ Update fields on an item in a project block array. Finds by predicate field matc
31
31
  |-----------|------|----------|-------------|
32
32
  | `block` | string | yes | Block name (e.g., 'issues', 'decisions') |
33
33
  | `arrayKey` | string | yes | Array key in the block |
34
- | `match` | object | yes | Fields to match (e.g., { id: 'issue-123' }) |
34
+ | `match` | object | yes | Fields to match (e.g., { id: 'ISSUE-NNN' }) |
35
35
  | `updates` | object | yes | Fields to update (e.g., { status: 'resolved' }) |
36
36
  </tool>
37
37
 
@@ -48,6 +48,20 @@ Append a closure-table relation (edge: parent, child, relation_type, optional or
48
48
  | `ordinal` | integer | no | Optional sibling-ordering within (parent, relation_type) |
49
49
  </tool>
50
50
 
51
+ <tool name="promote-item">
52
+ 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.
53
+
54
+ *Promote an item into another substrate as a derived copy with a lineage edge*
55
+
56
+ | Parameter | Type | Required | Description |
57
+ |-----------|------|----------|-------------|
58
+ | `source` | string | yes | Source item selector (bare refname / <alias>:<refname>) |
59
+ | `destinationSubstrate` | string | yes | Registered destination substrate alias |
60
+ | `newRefname` | string | no | Explicit destination refname (else allocated from the dest block id pattern) |
61
+ | `dryRun` | boolean | no | Compute the destination without writing any channel |
62
+ | `writer` | object | yes | DispatchContext.writer per pi-context/src/dispatch-context.ts. |
63
+ </tool>
64
+
51
65
  <tool name="append-block-nested-item">
52
66
  Append an item to a nested array on a parent-array item in a project block. Schema validation is automatic.
53
67
 
@@ -57,7 +71,7 @@ Append an item to a nested array on a parent-array item in a project block. Sche
57
71
  |-----------|------|----------|-------------|
58
72
  | `block` | string | yes | Block name (e.g., 'spec-reviews') |
59
73
  | `arrayKey` | string | yes | Parent array key (e.g., 'reviews') |
60
- | `match` | object | yes | Fields to match the parent item (e.g., { id: 'REVIEW-001' }) |
74
+ | `match` | object | yes | Fields to match the parent item (e.g., { id: 'REVIEW-NNN' }) |
61
75
  | `nestedKey` | string | yes | Nested array key on the matched parent (e.g., 'findings') |
62
76
  | `item` | unknown | yes | Item object to append to the nested array — must conform to schema |
63
77
  </tool>
@@ -71,7 +85,7 @@ Update fields on a nested-array item inside a parent-array item in a project blo
71
85
  |-----------|------|----------|-------------|
72
86
  | `block` | string | yes | Block name (e.g., 'spec-reviews') |
73
87
  | `arrayKey` | string | yes | Parent array key (e.g., 'reviews') |
74
- | `match` | object | yes | Fields to match the parent item (e.g., { id: 'REVIEW-001' }) |
88
+ | `match` | object | yes | Fields to match the parent item (e.g., { id: 'REVIEW-NNN' }) |
75
89
  | `nestedKey` | string | yes | Nested array key on the matched parent (e.g., 'findings') |
76
90
  | `nestedMatch` | object | yes | Fields to match the nested item (e.g., { id: 'F-001' }) |
77
91
  | `updates` | object | yes | Fields to update on the nested item (e.g., { state: 'resolved' }) |
@@ -86,7 +100,7 @@ Remove items matching a predicate from a top-level array in a project block. Ide
86
100
  |-----------|------|----------|-------------|
87
101
  | `block` | string | yes | Block name (e.g., 'issues') |
88
102
  | `arrayKey` | string | yes | Top-level array key (e.g., 'issues') |
89
- | `match` | object | yes | Fields to match (e.g., { id: 'issue-123' }) |
103
+ | `match` | object | yes | Fields to match (e.g., { id: 'ISSUE-NNN' }) |
90
104
  </tool>
91
105
 
92
106
  <tool name="remove-block-nested-item">
@@ -98,19 +112,19 @@ Remove items matching a predicate from a nested array on a parent-array item in
98
112
  |-----------|------|----------|-------------|
99
113
  | `block` | string | yes | Block name (e.g., 'spec-reviews') |
100
114
  | `arrayKey` | string | yes | Parent array key (e.g., 'reviews') |
101
- | `match` | object | yes | Fields to match the parent item (e.g., { id: 'REVIEW-001' }) |
115
+ | `match` | object | yes | Fields to match the parent item (e.g., { id: 'REVIEW-NNN' }) |
102
116
  | `nestedKey` | string | yes | Nested array key on the matched parent (e.g., 'findings') |
103
117
  | `nestedMatch` | object | yes | Fields to match the nested items to remove (e.g., { id: 'F-001' }) |
104
118
  </tool>
105
119
 
106
120
  <tool name="read-block-dir">
107
- Enumerate and parse all .json files in a .project/<subdir>/ directory, returned as a sorted array. Missing directories return [].
121
+ Enumerate and parse all .json files in a <substrate-dir>/<subdir>/ directory, returned as a sorted array. Missing directories return [].
108
122
 
109
123
  *Enumerate project block subdirectories (phases, schemas, etc.) as parsed JSON*
110
124
 
111
125
  | Parameter | Type | Required | Description |
112
126
  |-----------|------|----------|-------------|
113
- | `subdir` | string | yes | Subdirectory under .project/ (e.g., 'phases', 'schemas') |
127
+ | `subdir` | string | yes | Subdirectory under the substrate dir (e.g., 'phases', 'schemas') |
114
128
  </tool>
115
129
 
116
130
  <tool name="read-block">
@@ -149,21 +163,28 @@ Validate cross-block referential integrity — check that IDs referenced across
149
163
  </tool>
150
164
 
151
165
  <tool name="read-config">
152
- Read the substrate config.json as structured JSON — vocabulary, lenses, relation_types, status_buckets, display_strings, layers, block_kinds, installed_schemas, installed_blocks.
166
+ 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.
153
167
 
154
168
  *Read project config — vocabulary, lenses, relation_types, status_buckets*
155
169
 
170
+ | Parameter | Type | Required | Description |
171
+ |-----------|------|----------|-------------|
172
+ | `registry` | string | no | Address ONE config registry/map by key (e.g. 'relation_types', 'lenses', 'block_kinds', 'status_buckets') |
173
+ | `id` | string | no | With `registry`: address ONE entry within it by canonical_id |
156
174
  </tool>
157
175
 
158
176
  <tool name="list-tools">
159
- List every tool bound into the current Pi session — name, description, parameter JSON-schema, and source extension plus which tools are currently active. Self-introspection of the agent's own tool surface (all loaded extensions + builtins).
177
+ 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.
160
178
 
161
- *Discover available tools — names, params, descriptions, active set*
179
+ *Discover available tools — compact index, or one tool's full descriptor via `name`*
162
180
 
181
+ | Parameter | Type | Required | Description |
182
+ |-----------|------|----------|-------------|
183
+ | `name` | string | no | Address ONE tool by name → full descriptor (params schema + sourceInfo) |
163
184
  </tool>
164
185
 
165
186
  <tool name="read-samples-catalog">
166
- Enumerate installable sample block kinds (DEC-0037 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.
187
+ 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.
167
188
 
168
189
  *Discover installable sample block kinds — title, shape, relation_types, invariants, lenses*
169
190
 
@@ -173,14 +194,14 @@ Enumerate installable sample block kinds (DEC-0037 packaged view): per kind —
173
194
  </tool>
174
195
 
175
196
  <tool name="context-current-state">
176
- Derive 'where are we + what's next' purely from .project substrate — focus, in-flight tasks, ranked atomic-next actions (open framework-gaps then unblocked planned tasks), and blocked tasks. No writes; nothing hand-stored.
197
+ 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. No writes; nothing hand-stored.
177
198
 
178
199
  *Derive current project state — focus, in-flight, next actions, blocked*
179
200
 
180
201
  </tool>
181
202
 
182
203
  <tool name="context-bootstrap-state">
183
- Derive the substrate bootstrap state for the cwd, purely from the filesystem (DEC-0040): 'no-pointer' | 'no-config' | 'not-installed' | 'ready', plus the resolved contextDir and any declared-but-unmaterialized installed assets. 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 human to run /context start (bootstrap is human-only). No writes.
204
+ Derive the substrate bootstrap state for the cwd, purely from the filesystem: 'no-pointer' | 'no-config' | 'not-installed' | 'ready', plus the resolved contextDir and any declared-but-unmaterialized installed assets. 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.
184
205
 
185
206
  *Derive substrate bootstrap state — no-pointer | no-config | not-installed | ready (never throws pre-bootstrap)*
186
207
 
@@ -214,17 +235,18 @@ Scoped add / replace / remove of ONE entry in ONE config.json registry (block_ki
214
235
  </tool>
215
236
 
216
237
  <tool name="read-schema">
217
- Read a substrate schema by name as parsed JSON. Returns null when the schema file is absent.
238
+ 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.
218
239
 
219
- *Read a block schema as structured JSON*
240
+ *Read a block schema as structured JSON — optionally address one property via `path`*
220
241
 
221
242
  | Parameter | Type | Required | Description |
222
243
  |-----------|------|----------|-------------|
223
244
  | `schemaName` | string | yes | Schema name without extension (e.g., 'tasks', 'decisions', 'issues') |
245
+ | `path` | string | no | Address ONE property by dotted/bracket path (e.g. 'properties.tasks.items.properties.status') |
224
246
  </tool>
225
247
 
226
248
  <tool name="write-schema">
227
- 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. CAVEAT: a 'replace' that changes the schema's version does NOT migrate existing block items read-time validateBlockWithMigration throws a version mismatch until a code-level MigrationFn is registered (no tool surface for that). Registering the block_kind that points at this schema is a separate step (amend-config block_kinds).
249
+ 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).
228
250
 
229
251
  *Create or replace a block-kind JSON Schema (meta-validated, atomic)*
230
252
 
@@ -236,6 +258,22 @@ Create or replace a substrate block-kind JSON Schema. operation 'create' require
236
258
  | `dryRun` | boolean | no | Meta-validate without writing |
237
259
  </tool>
238
260
 
261
+ <tool name="write-schema-migration">
262
+ 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.
263
+
264
+ *Declare a schema version-bump migration (identity or declarative-transform) into migrations.json*
265
+
266
+ | Parameter | Type | Required | Description |
267
+ |-----------|------|----------|-------------|
268
+ | `operation` | string | yes | create | replace | remove |
269
+ | `schemaName` | string | yes | Schema name without extension (e.g., 'tasks'). |
270
+ | `fromVersion` | string | yes | Source schema semver this migration walks forward FROM. |
271
+ | `toVersion` | string | yes | Destination schema semver this migration produces. Must differ from fromVersion. Ignored for operation=remove. |
272
+ | `kind` | string | no | identity | declarative-transform. Required for operation=create/replace; ignored for remove. |
273
+ | `transform` | unknown | no | TransformSpec body — required when kind='declarative-transform'; forbidden when kind='identity'. Accepts a JSON string. |
274
+ | `writer` | object | yes | DispatchContext.writer per pi-context/src/dispatch-context.ts. |
275
+ </tool>
276
+
239
277
  <tool name="context-init">
240
278
  Initialize the substrate dir (bootstrap pointer + dirs only; run accept-all + install to populate).
241
279
 
@@ -243,7 +281,7 @@ Initialize the substrate dir (bootstrap pointer + dirs only; run accept-all + in
243
281
 
244
282
  | Parameter | Type | Required | Description |
245
283
  |-----------|------|----------|-------------|
246
- | `contextDir` | string | yes | Substrate dir name (e.g. .project). Required per DEC-0015 — no default. |
284
+ | `contextDir` | string | yes | Substrate dir name (e.g. .context). Required — no default. |
247
285
  </tool>
248
286
 
249
287
  <tool name="context-accept-all">
@@ -253,6 +291,37 @@ Adopt the canonical packaged conception (samples/conception.json) as this substr
253
291
 
254
292
  </tool>
255
293
 
294
+ <tool name="context-switch">
295
+ 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).
296
+
297
+ *Switch the bootstrap pointer to a different substrate dir*
298
+
299
+ | Parameter | Type | Required | Description |
300
+ |-----------|------|----------|-------------|
301
+ | `target_dir` | string | yes | Substrate dir name to switch to (e.g. '.context'). Required for default + create_new modes; ignored for to_previous mode. |
302
+ | `create_new` | boolean | no | When true, bootstrap target_dir as a fresh substrate 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). |
303
+ | `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. |
304
+ | `writer` | object | no | DispatchContext.writer — stamped by auth-gate on operator confirm; in-body trusts the stamped value. |
305
+ </tool>
306
+
307
+ <tool name="context-list">
308
+ Enumerate top-level dirs under cwd containing a config.json (switchable substrates). Marks the active one with isActive=true. Read-only.
309
+
310
+ *List switchable substrate dirs under cwd*
311
+
312
+ </tool>
313
+
314
+ <tool name="context-archive">
315
+ 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>/.
316
+
317
+ *Archive a non-active substrate dir to archive/<dir>/*
318
+
319
+ | Parameter | Type | Required | Description |
320
+ |-----------|------|----------|-------------|
321
+ | `target_dir` | string | yes | Substrate dir name to archive (e.g. '.project'). Refused if it is the active substrate. |
322
+ | `writer` | object | no | DispatchContext.writer — stamped by auth-gate on operator confirm. |
323
+ </tool>
324
+
256
325
  <tool name="filter-block-items">
257
326
  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.
258
327
 
@@ -267,13 +336,13 @@ Filter the array items of a block by a single-field predicate (eq / neq / in / m
267
336
  </tool>
268
337
 
269
338
  <tool name="resolve-item-by-id">
270
- Look up the block, array key, and item payload for a given ID across all .project/ blocks. 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.
339
+ 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.
271
340
 
272
341
  *Resolve a kind-prefixed ID (DEC-/FEAT-/FGAP-/issue-/REQ-/TASK-/etc.) to its owning block and item*
273
342
 
274
343
  | Parameter | Type | Required | Description |
275
344
  |-----------|------|----------|-------------|
276
- | `id` | string | yes | Kind-prefixed ID, e.g., DEC-0001 / FEAT-001 / FGAP-003 / issue-064 |
345
+ | `id` | string | yes | Kind-prefixed ID, e.g., DEC-NNNN / FEAT-NNN / FGAP-NNN / ISSUE-NNN |
277
346
  </tool>
278
347
 
279
348
  <tool name="read-block-item">
@@ -284,7 +353,7 @@ Read a single item from a named block by its id — returns the item or null. Bl
284
353
  | Parameter | Type | Required | Description |
285
354
  |-----------|------|----------|-------------|
286
355
  | `block` | string | yes | Block name (e.g., 'tasks', 'decisions', 'framework-gaps') |
287
- | `id` | string | yes | Item id within the block (e.g., 'TASK-001') |
356
+ | `id` | string | yes | Item id within the block (e.g., 'TASK-NNN') |
288
357
  </tool>
289
358
 
290
359
  <tool name="read-block-page">
@@ -300,7 +369,7 @@ Paginate a block's items: returns { items, total, hasMore }. offset default 0, l
300
369
  </tool>
301
370
 
302
371
  <tool name="join-blocks">
303
- Join two blocks in one call (FGAP-043). 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.
372
+ 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.
304
373
 
305
374
  *Join two blocks in one call — by relation edge or shared field; returns {left,right[]} pairs*
306
375
 
@@ -389,7 +458,7 @@ Find all closure-table edges incident on an item id (inbound, outbound, or both)
389
458
  </tool>
390
459
 
391
460
  <tool name="gather-execution-context">
392
- 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. Per DEC-0017 substrate primitive serving harness-confined dispatch.
461
+ 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.
393
462
 
394
463
  *Compose ContextBundle for unit + context-contract-declared bundle_relation_types*
395
464
 
@@ -401,7 +470,7 @@ Compose a ContextBundle for a work-unit by reading its context-contract (by unit
401
470
  </tool>
402
471
 
403
472
  <tool name="context-roadmap-load">
404
- Load a roadmap by id and return the materialized RoadmapView (phases, lens-views, status rollup, milestone resolution, scoped phase_depends_on edges, topo-ordered phaseOrder + cycles). Per DEC-0012 phase ordering lives in relations.json with relation_type='phase_depends_on'.
473
+ Load a roadmap by id and return the materialized RoadmapView (phases, lens-views, status rollup, milestone resolution, scoped phase_depends_on edges, topo-ordered phaseOrder + cycles). Phase ordering lives in relations.json with relation_type='phase_depends_on'.
405
474
 
406
475
  *Load a roadmap by id*
407
476
 
@@ -443,23 +512,23 @@ List every roadmap in <config.root>/roadmap.json with id, title, optional status
443
512
  <command name="/context">
444
513
  Context state management
445
514
 
446
- Subcommands: `init`, `install`, `accept-all`, `view`, `lens-curate`, `roadmap-list`, `roadmap-view`, `roadmap-validate`, `status`, `add-work`, `validate`, `help`
515
+ Subcommands: `init`, `switch`, `list`, `archive`, `install`, `accept-all`, `view`, `lens-curate`, `roadmap-list`, `roadmap-view`, `roadmap-validate`, `status`, `add-work`, `validate`, `help`
447
516
  </command>
448
517
 
449
518
  </commands_reference>
450
519
 
451
520
  <events>
452
- `session_start`
521
+ `session_start`, `before_agent_start`, `resources_discover`
453
522
  </events>
454
523
 
455
524
  <bundled_resources>
456
- 9 schemas, 31 samples bundled.
525
+ 11 schemas, 34 samples bundled.
457
526
  See references/bundled-resources.md for full inventory.
458
527
  </bundled_resources>
459
528
 
460
529
  <installable_blocks>
461
530
 
462
- Names valid for the `installed_blocks` array in `.project/config.json`. Install with `/context install <block>`.
531
+ Names valid for the `installed_blocks` array in `<substrate-dir>/config.json`. Install with `/context install <block>`.
463
532
 
464
533
  | Block | Source File |
465
534
  |-------|-------------|
@@ -478,12 +547,13 @@ Names valid for the `installed_blocks` array in `.project/config.json`. Install
478
547
  | `context-contracts` | `samples/blocks/context-contracts.json` |
479
548
  | `phase` | `samples/blocks/phase.json` |
480
549
  | `story` | `samples/blocks/story.json` |
550
+ | `work-orders` | `samples/blocks/work-orders.json` |
481
551
 
482
552
  </installable_blocks>
483
553
 
484
554
  <installable_schemas>
485
555
 
486
- Names valid for the `installed_schemas` array in `.project/config.json`. Schemas back block validation; install with `/context install <schema>`.
556
+ Names valid for the `installed_schemas` array in `<substrate-dir>/config.json`. Schemas back block validation; install with `/context install <schema>`.
487
557
 
488
558
  | Schema | Source File |
489
559
  |--------|-------------|
@@ -502,6 +572,7 @@ Names valid for the `installed_schemas` array in `.project/config.json`. Schemas
502
572
  | `context-contracts` | `samples/schemas/context-contracts.schema.json` |
503
573
  | `phase` | `samples/schemas/phase.schema.json` |
504
574
  | `story` | `samples/schemas/story.schema.json` |
575
+ | `work-orders` | `samples/schemas/work-orders.schema.json` |
505
576
 
506
577
  </installable_schemas>
507
578
 
@@ -511,28 +582,29 @@ Names valid for the `installed_schemas` array in `.project/config.json`. Schemas
511
582
 
512
583
  | Block | Title | Array Key | Item Fields |
513
584
  |-------|-------|-----------|-------------|
514
- | `decisions` | Decisions | `decisions` | id, title, status (string (open|enacted|superseded)), context, decision, options_considered? (array), consequences (array), references? (array), created_by, created_at, enacted_by?, enacted_at? |
515
- | `framework-gaps` | Framework Gaps | `gaps` | id, title, status (string (identified|accepted|in-progress|closed|wontfix)), priority? (string (P0|P1|P2|P3)), package, layer? (string (L1|L2|L3|L4|L5)), description, evidence (array), impact, canonical_vocabulary?, proposed_resolution, created_by, created_at, closed_by?, closed_at? |
516
- | `tasks` | Tasks | `tasks` | id, description, status (string (planned|in-progress|completed|blocked|cancelled)), files? (array), acceptance_criteria? (array), assigned_agent?, notes? |
517
- | `verification` | Verification | `verifications` | id, status (string (passed|failed|partial|skipped)), method (string (command|inspect|test)), evidence?, timestamp?, criteria_results? (array) |
518
- | `issues` | Issues | `issues` | id, title, body, location, status (string (open|resolved|deferred)), category (string (primitive|issue|cleanup|capability|composition)), priority (string (low|medium|high|critical)), package, source? (string (human|agent|monitor|workflow)), resolved_by? |
519
- | `features` | Features | `features` | id, title, status (string (proposed|approved|in-progress|in-review|complete|blocked|cancelled)), layer (string (L1|L2|L3|L4|L5)), description, motivation?, acceptance_criteria (array), created_by, created_at, modified_by?, modified_at?, approved_by?, approved_at? |
520
- | `research` | Research | `research` | id, title, status (string (planned|in-progress|complete|stale|superseded|revised)), layer (string (L1|L2|L3|L4|L5)), type (string (investigative|comparative|empirical|historical|audit|landscape|feasibility|curation)), question, method, scope? (array), findings_summary, findings_document?, grounding? (object), grounded_at?, stale_conditions? (array), citations? (array), conducted_by?, conducted_at?, created_by, created_at, modified_by?, modified_at? |
521
- | `rationale` | Design Rationale | `rationales` | id, title, narrative, phase? (integer) |
522
- | `spec-reviews` | Spec Reviews | `reviews` | id, target, target_revision?, reviewer?, status (string (not-started|in-progress|complete|abandoned)), scope? (array), method?, clean? (boolean), created_by, created_at, completed_at? |
523
- | `layer-plans` | Layer Restructure Plans | `plans` | id, title, status (string (draft|proposed|decided|in-progress|complete|abandoned)), model, description?, layers (array), migration_phases (array), created_by, created_at |
524
- | `requirements` | Requirements | `requirements` | id, description, type (string (functional|non-functional|constraint|integration)), status (string (proposed|accepted|deferred|implemented|verified)), priority (string (must|should|could|wont)), acceptance_criteria? (array), source? (string (human|agent|analysis)) |
525
- | `conventions` | Conventions | `rules` | id, description, enforcement (string (lint|test|review|manual)), severity (string (error|warning|info)) |
526
- | `context-contracts` | Context contracts | `contracts` | id, unit_kind, bundle_relation_types (array), description?, notes?, created_by, created_at, modified_by?, modified_at? |
527
- | `phase` | Phases | `phases` | id, name, intent, goal?, status (string (planned|in-progress|completed)), success_criteria? (array), specs? (array), artifacts_produced? (array) |
528
- | `story` | Stories | `stories` | id, title, status (string (proposed|ready|in-progress|in-review|complete|blocked)), description?, acceptance_criteria? (array), created_by?, created_at?, modified_by?, modified_at? |
585
+ | `decisions` | Decisions | `decisions` | id, title, status (string (open|enacted|superseded)), context, decision, options_considered? (array), consequences (array), references? (array), created_by, created_at, enacted_by?, enacted_at?, oid?, content_hash?, content_parent? |
586
+ | `framework-gaps` | Framework Gaps | `gaps` | id, title, status (string (identified|accepted|in-progress|closed|wontfix|superseded_by)), priority? (string (P0|P1|P2|P3)), package, layer? (string (L1|L2|L3|L4|L5)), description, evidence (array), impact, canonical_vocabulary?, proposed_resolution, related_features? (array), related_decisions? (array), related_issues? (array), created_by, created_at, closed_by?, closed_at?, oid?, content_hash?, content_parent? |
587
+ | `tasks` | Tasks | `tasks` | id, description, status (string (planned|in-progress|completed|blocked|cancelled)), files? (array), acceptance_criteria? (array), assigned_agent?, notes?, oid?, content_hash?, content_parent? |
588
+ | `verification` | Verification | `verifications` | id, status (string (passed|failed|partial|skipped)), method (string (command|inspect|test)), evidence?, timestamp?, criteria_results? (array), oid?, content_hash?, content_parent? |
589
+ | `issues` | Issues | `issues` | id, title, body, location, status (string (open|resolved|deferred)), category (string (primitive|issue|cleanup|capability|composition)), priority (string (low|medium|high|critical)), package, source? (string (human|agent|monitor|workflow)), resolved_by?, oid?, content_hash?, content_parent? |
590
+ | `features` | Features | `features` | id, title, status (string (proposed|approved|in-progress|in-review|complete|blocked|cancelled)), layer (string (L1|L2|L3|L4|L5)), description, motivation?, acceptance_criteria (array), created_by, created_at, modified_by?, modified_at?, approved_by?, approved_at?, oid?, content_hash?, content_parent? |
591
+ | `research` | Research | `research` | id, title, status (string (planned|in-progress|complete|stale|superseded|revised)), layer (string (L1|L2|L3|L4|L5)), type (string (investigative|comparative|empirical|historical|audit|landscape|feasibility|curation)), question, method, scope? (array), findings_summary, findings_document?, grounding? (object), grounded_at?, stale_conditions? (array), citations? (array), conducted_by?, conducted_at?, created_by, created_at, modified_by?, modified_at?, oid?, content_hash?, content_parent? |
592
+ | `rationale` | Design Rationale | `rationales` | id, title, narrative, phase? (integer), oid?, content_hash?, content_parent? |
593
+ | `spec-reviews` | Spec Reviews | `reviews` | id, target, target_revision?, reviewer?, status (string (not-started|in-progress|complete|abandoned)), scope? (array), method?, clean? (boolean), created_by, created_at, completed_at?, oid?, content_hash?, content_parent? |
594
+ | `layer-plans` | Layer Restructure Plans | `plans` | id, title, status (string (draft|proposed|decided|in-progress|complete|abandoned)), model, description?, layers (array), migration_phases (array), created_by, created_at, oid?, content_hash?, content_parent? |
595
+ | `requirements` | Requirements | `requirements` | id, description, type (string (functional|non-functional|constraint|integration)), status (string (proposed|accepted|deferred|implemented|verified)), priority (string (must|should|could|wont)), acceptance_criteria? (array), source? (string (human|agent|analysis)), oid?, content_hash?, content_parent? |
596
+ | `conventions` | Conventions | `rules` | id, description, enforcement (string (lint|test|review|manual)), severity (string (error|warning|info)), oid?, content_hash?, content_parent? |
597
+ | `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? |
598
+ | `phase` | Phases | `phases` | id, name, intent, goal?, status (string (planned|in-progress|completed)), success_criteria? (array), specs? (array), artifacts_produced? (array), oid?, content_hash?, content_parent? |
599
+ | `story` | Stories | `stories` | id, title, status (string (proposed|ready|in-progress|in-review|complete|blocked)), description?, acceptance_criteria? (array), created_by?, created_at?, modified_by?, modified_at?, oid?, content_hash?, content_parent? |
600
+ | `work-orders` | Work Orders | `work_orders` | id, title, status (string (proposed|in-progress|real-check-passed|real-check-failed|completed|cancelled)), target_agent, input_contract (object), context_blocks (array), output_contract (object), scope (object), real_check_criteria (object), description?, created_by?, created_at?, modified_by?, modified_at?, oid?, content_hash?, content_parent? |
529
601
 
530
602
  **Status Enums:**
531
603
 
532
604
  | Block | Field | Values |
533
605
  |-------|-------|--------|
534
606
  | `decisions` | `status` | open, enacted, superseded |
535
- | `framework-gaps` | `status` | identified, accepted, in-progress, closed, wontfix |
607
+ | `framework-gaps` | `status` | identified, accepted, in-progress, closed, wontfix, superseded_by |
536
608
  | `framework-gaps` | `priority` | P0, P1, P2, P3 |
537
609
  | `framework-gaps` | `layer` | L1, L2, L3, L4, L5 |
538
610
  | `tasks` | `status` | planned, in-progress, completed, blocked, cancelled |
@@ -557,23 +629,30 @@ Names valid for the `installed_schemas` array in `.project/config.json`. Schemas
557
629
  | `conventions` | `severity` | error, warning, info |
558
630
  | `phase` | `status` | planned, in-progress, completed |
559
631
  | `story` | `status` | proposed, ready, in-progress, in-review, complete, blocked |
632
+ | `work-orders` | `status` | proposed, in-progress, real-check-passed, real-check-failed, completed, cancelled |
560
633
 
561
634
  </planning_vocabulary>
562
635
 
563
636
  <objective>
564
- pi-context manages structured project state in `.project/` — 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.
637
+ 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.
565
638
  </objective>
566
639
 
567
640
  <block_files>
568
- Blocks are JSON files under the substrate root (e.g., `gaps.json`, `decisions.json`). 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 (DEC-0015). 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.
641
+ 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.
642
+
643
+ 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.
569
644
  </block_files>
570
645
 
646
+ <content_addressing>
647
+ 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.
648
+ </content_addressing>
649
+
571
650
  <schema_validation>
572
651
  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.
573
652
  </schema_validation>
574
653
 
575
654
  <context_init>
576
- `/context init <dir>` creates the substrate skeleton: the `.pi-context.json` bootstrap pointer (declaring the substrate-dir name per DEC-0015) plus the substrate root and its `schemas/` directory. Nothing is imposed — no `config.json`, no schemas, and no starter blocks are written (DEC-0011 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`.
655
+ `/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`.
577
656
  </context_init>
578
657
 
579
658
  <context_accept_all>
@@ -587,17 +666,31 @@ The installable catalog IS the packaged conception (`samples/conception.json`):
587
666
  </context_install>
588
667
 
589
668
  <substrate_config>
590
- `.project/config.json` is the substrate bootstrap. Its `root` field declares where every other block, schema, agent, and template lives — closing the GitHub #3 surface where downstream consumers had to assume `.project/`. `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`.
669
+ `<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`.
591
670
 
592
- `config.json` and `relations.json` are exempt from `root` redirection — they always live at `.project/` because they are the substrate that defines `root`. 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.
671
+ `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.
593
672
 
594
673
  The `loadContext(cwd)` SDK returns an mtime-keyed cached snapshot of `{ config, relations, configMtime, relationsMtime }` for one cwd. Consumers must not mutate.
595
674
  </substrate_config>
596
675
 
676
+ <cross_substrate>
677
+ 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.
678
+
679
+ `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.
680
+
681
+ 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`.
682
+ </cross_substrate>
683
+
684
+ <schema_versioning>
685
+ 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`.
686
+ </schema_versioning>
687
+
597
688
  <lens_views>
598
689
  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`.
599
690
 
600
- Edges live in `.project/relations.json` as a closure table — each row is `{ parent, child, relation_type }`. `parent` is either a canonical id (hierarchy edges) or a lens.bins value (lens edges); disambiguation lives in `validateRelations`.
691
+ 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.
692
+
693
+ 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.)
601
694
 
602
695
  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.
603
696
 
@@ -613,7 +706,9 @@ The lens-view algorithm: `edgesForLens(lens, items, authoredEdges)` returns synt
613
706
  </context_view>
614
707
 
615
708
  <substrate_validation>
616
- `validateRelations(cwd)` (exposed as the `context-validate-relations` tool) checks the closure-table edges in `relations.json` against the config + per-block item snapshots. 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.
709
+ `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.
710
+
711
+ `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.
617
712
 
618
713
  Two 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-walk-descendants` returns the transitive descendant id list from a parent under a given relation_type.
619
714
  </substrate_validation>
@@ -647,21 +742,20 @@ Item-level reads complement whole-block `read-block` (which is all-or-nothing an
647
742
  </duplicate_detection>
648
743
 
649
744
  <context_validate>
650
- Two separate validators address two concerns:
745
+ 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.
651
746
 
652
- `/context validate` (the `context-validate` tool) checks cross-block referential integrity:
653
- - task.phase references a valid phase
654
- - task.depends_on references valid task IDs
655
- - decision.phase references a valid phase
656
- - gap.resolved_by references a valid ID
657
- - requirement.traces_to references valid phase/task IDs
658
- - requirement.depends_on references valid requirement IDs
659
- - verification.target references a valid target ID
660
- - rationale.related_decisions references valid decision IDs
747
+ `/context validate` (the `context-validate` tool → `validateContext`) is the project-wide check. It runs:
748
+ - Source-of-truth drift: when the active `config.substrate_id` is set, the project-root `.pi-context-registry.json` must carry an entry whose dir resolves to the active substrate dir (`substrate_id_unregistered`, `substrate_id_registry_mismatch`).
749
+ - Edge integrity: each edge's `parent`/`child` is 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`); a `relation_type` absent from `config.relation_types[]` errors; when a relation_type declares `source_kinds`/`target_kinds`, an endpoint whose resolved block kind is outside the (non-`*`) declared set errors.
750
+ - Cycle detection: delegated to `validateRelations`; only its `edge_cycle_detected` diagnostics are merged in.
751
+ - Config-declared invariants: `requires-edge` and `status-consistency` classes are enforced generically from `config.invariants[]` DATA no block/status/relation_type vocabulary is hardcoded in source; each invariant's emitted code is its own `inv.id`.
752
+ - Status-vocabulary: an item `status` value with no key in the declared vocabulary is a warning (`status_unknown_value`).
753
+ - Nested id-bearing array: a schema array at nesting depth ≥ 1 whose item shape carries an `id` is a warning to promote it to a top-level entity + membership edge (`nested_id_bearing_array`).
754
+ - Lens validators: every validator registered via `registerLensValidator` is dispatched and its issues merged; a throwing validator surfaces as a warning (`lens_validator_failed:<name>`).
661
755
 
662
- Returns errors (broken dependency references) and warnings (unresolved cross-references).
756
+ Returns issues with `severity` error/warning; status is `invalid` (any error), `warnings`, or `clean`.
663
757
 
664
- The `context-validate-relations` tool (see `<substrate_validation>`) validates closure-table edges in `relations.json` a separate concern from cross-block ID resolution.
758
+ 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`).
665
759
  </context_validate>
666
760
 
667
761
  <update_check>
@@ -669,7 +763,7 @@ On `session_start`, checks npm registry for newer versions of `@davidorex/pi-pro
669
763
  </update_check>
670
764
 
671
765
  <success_criteria>
672
- - `.project/`, `.project/schemas/`, `.project/phases/`, and `.project/config.json` exist after `/context init`
766
+ - `<substrate-dir>/`, `<substrate-dir>/schemas/`, and the `.pi-context.json` bootstrap pointer exist after `/context init <substrate-dir>` (init is skeleton-only: no `config.json`, no schemas, no blocks until accept-all + install). Phases are not a directory — they live as an in-block array under `phase.json` (plural `phases` key); there is no `phases/` dir.
673
767
  - `installed_schemas` / `installed_blocks` declared in `config.json` are reified by `/context install`; `--update` overwrites
674
768
  - Block writes validate against schemas — invalid data rejected with specific error
675
769
  - `/context status` returns current derived state without errors
@@ -1,10 +1,12 @@
1
1
  # Bundled Resources
2
2
 
3
- ## schemas/ (9 files)
3
+ ## schemas/ (11 files)
4
4
 
5
5
  - `schemas/bootstrap.schema.json`
6
6
  - `schemas/config.schema.json`
7
+ - `schemas/context-registry.schema.json`
7
8
  - `schemas/layer.schema.json`
9
+ - `schemas/migrations.schema.json`
8
10
  - `schemas/priority.schema.json`
9
11
  - `schemas/relations.schema.json`
10
12
  - `schemas/severity.schema.json`
@@ -12,7 +14,7 @@
12
14
  - `schemas/status.schema.json`
13
15
  - `schemas/verification-method.schema.json`
14
16
 
15
- ## samples/ (31 files)
17
+ ## samples/ (34 files)
16
18
 
17
19
  - `samples/blocks/context-contracts.json`
18
20
  - `samples/blocks/conventions.json`
@@ -29,7 +31,9 @@
29
31
  - `samples/blocks/story.json`
30
32
  - `samples/blocks/tasks.json`
31
33
  - `samples/blocks/verification.json`
34
+ - `samples/blocks/work-orders.json`
32
35
  - `samples/conception.json`
36
+ - `samples/migrations.json`
33
37
  - `samples/schemas/context-contracts.schema.json`
34
38
  - `samples/schemas/conventions.schema.json`
35
39
  - `samples/schemas/decisions.schema.json`
@@ -45,3 +49,4 @@
45
49
  - `samples/schemas/story.schema.json`
46
50
  - `samples/schemas/tasks.schema.json`
47
51
  - `samples/schemas/verification.schema.json`
52
+ - `samples/schemas/work-orders.schema.json`