@davidorex/pi-context 0.30.0 → 0.32.0

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 (90) hide show
  1. package/CHANGELOG.md +127 -0
  2. package/README.md +27 -11
  3. package/dist/block-api.d.ts +13 -0
  4. package/dist/block-api.d.ts.map +1 -1
  5. package/dist/block-api.js +28 -3
  6. package/dist/block-api.js.map +1 -1
  7. package/dist/content-hash.d.ts +13 -0
  8. package/dist/content-hash.d.ts.map +1 -1
  9. package/dist/content-hash.js +16 -0
  10. package/dist/content-hash.js.map +1 -1
  11. package/dist/context-dir.d.ts +12 -0
  12. package/dist/context-dir.d.ts.map +1 -1
  13. package/dist/context-dir.js +14 -0
  14. package/dist/context-dir.js.map +1 -1
  15. package/dist/context-sdk.d.ts +71 -33
  16. package/dist/context-sdk.d.ts.map +1 -1
  17. package/dist/context-sdk.js +547 -149
  18. package/dist/context-sdk.js.map +1 -1
  19. package/dist/context.d.ts +213 -2
  20. package/dist/context.d.ts.map +1 -1
  21. package/dist/context.js +119 -5
  22. package/dist/context.js.map +1 -1
  23. package/dist/index.d.ts +595 -9
  24. package/dist/index.d.ts.map +1 -1
  25. package/dist/index.js +2225 -55
  26. package/dist/index.js.map +1 -1
  27. package/dist/lens-view.d.ts +0 -5
  28. package/dist/lens-view.d.ts.map +1 -1
  29. package/dist/lens-view.js +43 -1
  30. package/dist/lens-view.js.map +1 -1
  31. package/dist/migration-registry-loader.d.ts +36 -12
  32. package/dist/migration-registry-loader.d.ts.map +1 -1
  33. package/dist/migration-registry-loader.js +79 -17
  34. package/dist/migration-registry-loader.js.map +1 -1
  35. package/dist/migrations-store.d.ts +45 -18
  36. package/dist/migrations-store.d.ts.map +1 -1
  37. package/dist/migrations-store.js +56 -22
  38. package/dist/migrations-store.js.map +1 -1
  39. package/dist/ops-registry.d.ts +16 -0
  40. package/dist/ops-registry.d.ts.map +1 -1
  41. package/dist/ops-registry.js +352 -117
  42. package/dist/ops-registry.js.map +1 -1
  43. package/dist/pending-blocked-store.d.ts +83 -0
  44. package/dist/pending-blocked-store.d.ts.map +1 -0
  45. package/dist/pending-blocked-store.js +93 -0
  46. package/dist/pending-blocked-store.js.map +1 -0
  47. package/dist/promote-item.d.ts.map +1 -1
  48. package/dist/promote-item.js +41 -12
  49. package/dist/promote-item.js.map +1 -1
  50. package/dist/roadmap-plan.d.ts +121 -99
  51. package/dist/roadmap-plan.d.ts.map +1 -1
  52. package/dist/roadmap-plan.js +281 -345
  53. package/dist/roadmap-plan.js.map +1 -1
  54. package/dist/schema-merge.d.ts +26 -0
  55. package/dist/schema-merge.d.ts.map +1 -0
  56. package/dist/schema-merge.js +176 -0
  57. package/dist/schema-merge.js.map +1 -0
  58. package/dist/status-vocab.d.ts +12 -2
  59. package/dist/status-vocab.d.ts.map +1 -1
  60. package/dist/status-vocab.js +14 -1
  61. package/dist/status-vocab.js.map +1 -1
  62. package/package.json +2 -1
  63. package/samples/blocks/milestone.json +3 -0
  64. package/samples/blocks/session-notes.json +1 -0
  65. package/samples/conception.json +358 -15
  66. package/samples/migrations.json +8 -0
  67. package/samples/schemas/context-contracts.schema.json +4 -0
  68. package/samples/schemas/conventions.schema.json +4 -0
  69. package/samples/schemas/decisions.schema.json +4 -0
  70. package/samples/schemas/features.schema.json +4 -0
  71. package/samples/schemas/framework-gaps.schema.json +5 -1
  72. package/samples/schemas/issues.schema.json +30 -2
  73. package/samples/schemas/layer-plans.schema.json +6 -2
  74. package/samples/schemas/milestone.schema.json +79 -0
  75. package/samples/schemas/phase.schema.json +4 -0
  76. package/samples/schemas/rationale.schema.json +4 -0
  77. package/samples/schemas/requirements.schema.json +4 -0
  78. package/samples/schemas/research.schema.json +5 -1
  79. package/samples/schemas/session-notes.schema.json +89 -0
  80. package/samples/schemas/spec-reviews.schema.json +4 -0
  81. package/samples/schemas/story.schema.json +8 -0
  82. package/samples/schemas/tasks.schema.json +4 -0
  83. package/samples/schemas/verification.schema.json +4 -0
  84. package/samples/schemas/work-orders.schema.json +6 -2
  85. package/schemas/config.schema.json +101 -3
  86. package/schemas/migrations.schema.json +25 -0
  87. package/schemas/pending-blocked.schema.json +190 -0
  88. package/skill-narrative.md +14 -10
  89. package/skills/pi-context/SKILL.md +127 -49
  90. package/skills/pi-context/references/bundled-resources.md +7 -2
@@ -19,9 +19,9 @@
19
19
  import path from "node:path";
20
20
  import { Type } from "typebox";
21
21
  import { appendToBlock, appendToNestedArray, nextId, readBlock, readBlockDir, removeFromBlock, removeFromNestedArray, updateItemInBlock, updateNestedArrayItem, upsertItemInBlock, writeBlock, } from "./block-api.js";
22
- import { adoptConception, amendConfigEntry, loadConfig, loadContext } from "./context.js";
22
+ import { adoptConception, amendConfigEntry, loadConfig } from "./context.js";
23
23
  import { BootstrapNotFoundError, schemaPath, tryResolveContextDir } from "./context-dir.js";
24
- import { appendRelationByRef, appendRelationsByRef, completeTask, contextState, currentState, deriveBootstrapState, filterBlockItems, joinBlocks, readBlockItem, readBlockPage, removeRelationByRef, replaceRelationByRef, resolveItemById, resolveItemsByIds, validateContext, } from "./context-sdk.js";
24
+ import { appendRelationByRef, appendRelationsByRef, completeTask, contextState, currentState, deriveBootstrapState, endpointKey, filterBlockItems, joinBlocks, readBlockItem, readBlockPage, removeRelationByRef, replaceRelationByRef, resolveItemById, resolveItemsByIds, validateContext, } from "./context-sdk.js";
25
25
  import { gatherExecutionContext } from "./execution-context.js";
26
26
  // initProject + the switch/list/archive helpers are defined in index.ts (shared
27
27
  // with the /context command handlers + the context-* tools). This is a cyclic
@@ -29,12 +29,12 @@ import { gatherExecutionContext } from "./execution-context.js";
29
29
  // registerAll runs at extension-load time, after both modules' top-level
30
30
  // function bindings exist, and the helpers are only referenced inside op `run`
31
31
  // closures (lazy), never at this module's top level.
32
- import { archiveSubstrate, initProject, listSubstrates, switchAndCreate, switchToExisting, switchToPrevious, } from "./index.js";
33
- import { edgesForLensByName, findReferencesInRepo, validateContextRelations, walkAncestorsByLens, walkLensDescendants, } from "./lens-view.js";
32
+ import { archiveSubstrate, checkStatus, initProject, installContext, listSubstrates, readCatalogSchemaText, resolveBlocked, resolveConflict, switchAndCreate, switchToExisting, switchToPrevious, updateContext, validateBlockItemsAgainstCatalog, } from "./index.js";
33
+ import { edgesForLensByName, findReferencesInRepo, loadLensView, validateContextRelations, walkAncestorsByLens, walkLensDescendants, } from "./lens-view.js";
34
34
  import { promoteItem } from "./promote-item.js";
35
- import { addressInto, renderReadText, structureForRead } from "./read-element.js";
35
+ import { addressInto, pageArray, renderReadText, structureForRead } from "./read-element.js";
36
36
  import { renameCanonicalId } from "./rename-canonical-id.js";
37
- import { listRoadmaps, loadRoadmap, renderRoadmap, validateRoadmaps } from "./roadmap-plan.js";
37
+ import { loadRoadmap, renderRoadmap, validateRoadmap } from "./roadmap-plan.js";
38
38
  import { samplesCatalog } from "./samples-catalog.js";
39
39
  import { readSchema, writeSchemaChecked } from "./schema-write.js";
40
40
  import { truncateHead } from "./truncate.js";
@@ -115,37 +115,15 @@ export function boundedJsonOutput(r) {
115
115
  const { over, totalBytes } = overReadCap(s);
116
116
  return over ? { data: null, truncated: true, totalBytes, complete: false } : r.json;
117
117
  }
118
- // ── serializeRoadmapView ────────────────────────────────────────────────────
119
- // Strip non-serializable fields (suggestionTemplate fn, grouped Map) from the
120
- // embedded LoadedLensView records before tool serialization. Relocated verbatim
121
- // from the extension factory; consumed by the context-roadmap-load op.
122
- const serializeRoadmapView = (view) => ({
123
- roadmap: view.roadmap,
124
- phases: view.phases.map((pv) => ({
125
- phase: pv.phase,
126
- lensView: "error" in pv.lensView
127
- ? pv.lensView
128
- : {
129
- lens: pv.lensView.lens,
130
- items: pv.lensView.items,
131
- edges: pv.lensView.edges,
132
- grouped: Object.fromEntries(pv.lensView.grouped),
133
- uncategorized: pv.lensView.uncategorized,
134
- },
135
- status: pv.status,
136
- ...(pv.milestone ? { milestone: pv.milestone } : {}),
137
- ...(pv.milestoneSatisfied !== undefined ? { milestoneSatisfied: pv.milestoneSatisfied } : {}),
138
- })),
139
- phaseOrder: view.phaseOrder,
140
- cycles: view.cycles,
141
- edges: view.edges,
142
- });
143
118
  export const ops = [
144
119
  {
145
120
  name: "append-block-item",
146
121
  label: "Append Block Item",
147
122
  description: "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.",
148
123
  promptSnippet: "Append items to project blocks (issues, decisions, or any user-defined block)",
124
+ examples: [
125
+ `pi-context append-block-item --block framework-gaps --arrayKey gaps --autoId true --item @/tmp/fgap.json --writer '{"kind":"human","user":"you@example.com"}' --json`,
126
+ ],
149
127
  parameters: Type.Object({
150
128
  block: Type.String({ description: "Block name (e.g., 'issues', 'decisions')" }),
151
129
  arrayKey: Type.String({ description: "Array key in the block (e.g., 'issues', 'decisions')" }),
@@ -183,6 +161,9 @@ export const ops = [
183
161
  label: "Update Block Item",
184
162
  description: "Update fields on an item in a project block array. Finds by predicate field match.",
185
163
  promptSnippet: "Update items in project blocks — change status, add details, mark resolved",
164
+ examples: [
165
+ `pi-context update-block-item --block tasks --arrayKey tasks --match '{"id":"TASK-001"}' --updates '{"status":"in-progress"}' --writer '{"kind":"human","user":"you@example.com"}' --json`,
166
+ ],
186
167
  parameters: Type.Object({
187
168
  block: Type.String({ description: "Block name (e.g., 'issues', 'decisions')" }),
188
169
  arrayKey: Type.String({ description: "Array key in the block" }),
@@ -205,14 +186,30 @@ export const ops = [
205
186
  {
206
187
  name: "append-relation",
207
188
  label: "Append Relation",
208
- description: "Append a closure-table relation (edge: parent, child, relation_type, optional ordinal) to relations.json. " +
209
- "Shape is AJV-validated; an exact-duplicate edge (same parent+child+relation_type) is a no-op. Reference " +
210
- "integrity (endpoints resolve, relation_type registered, no cycle) is NOT checked here run context-validate " +
211
- "after. Creates relations.json if absent.",
212
- promptSnippet: "Create a relation/edge between two items (parentchild under a relation_type)",
189
+ description: "Append a closure-table relation (edge: relation_type, optional ordinal) to relations.json. Orient the edge with " +
190
+ "EITHER raw --parent/--child OR the role-typed --primary/--counter (which maps to parent/child via the relation's " +
191
+ "declared role_direction); the two pairs are mutually exclusive. A bare --parent/--child append of a relation that " +
192
+ "is BOTH role-bearing and orientation-ambiguous (its source/target kinds overlap) is rejected — re-issue with " +
193
+ "--primary/--counter. Shape is AJV-validated; an exact-duplicate edge (same parent+child+relation_type) is a no-op. " +
194
+ "Reference integrity (endpoints resolve, relation_type registered, no cycle) is NOT checked here — run " +
195
+ "context-validate after. Creates relations.json if absent.",
196
+ promptSnippet: "Create a relation/edge between two items (raw --parent/--child, or role-typed --primary/--counter mapped via role_direction)",
197
+ examples: [
198
+ `pi-context append-relation --parent VER-001 --child TASK-001 --relation_type verification_verifies_item --writer '{"kind":"human","user":"you@example.com"}' --json`,
199
+ ],
213
200
  parameters: Type.Object({
214
- parent: Type.String({ description: "Canonical id (or lens bin name) of the parent endpoint" }),
215
- child: Type.String({ description: "Canonical id of the child endpoint" }),
201
+ parent: Type.Optional(Type.String({
202
+ description: "Parent-endpoint selector (canonical id / <alias>:<refname> / lens bin) — RAW orientation. Mutually exclusive with --primary/--counter.",
203
+ })),
204
+ child: Type.Optional(Type.String({
205
+ description: "Child-endpoint selector — RAW orientation. Mutually exclusive with --primary/--counter.",
206
+ })),
207
+ primary: Type.Optional(Type.String({
208
+ description: "Selector of the endpoint holding the relation's PRIMARY semantic role (ROLE-TYPED orientation; mapped to parent/child via the relation's declared role_direction). Requires --counter; the relation_type must declare role_direction.",
209
+ })),
210
+ counter: Type.Optional(Type.String({
211
+ description: "Selector of the endpoint holding the relation's COUNTER role (ROLE-TYPED orientation). Requires --primary.",
212
+ })),
216
213
  relation_type: Type.String({
217
214
  description: "Registered relation_type canonical_id / hierarchy edge type / lens id",
218
215
  }),
@@ -222,26 +219,32 @@ export const ops = [
222
219
  surface: "use",
223
220
  run(cwd, params, ctx) {
224
221
  // Cycle-5 porcelain: STRING selectors (bare refname / <alias>:<refname> /
225
- // lens-bin) are resolved to structured EdgeEndpoints and written via the
226
- // raw plumbing. The param surface stays string-typed; messaging uses the
227
- // raw selectors (params.*), not the resolved structured endpoints. Under
228
- // dryRun the byRef fn validates the prospective relations + dedup-checks
229
- // without writing (TASK-010 shared preview path).
230
- const { appended } = appendRelationByRef(cwd, {
231
- parent: params.parent,
232
- child: params.child,
222
+ // lens-bin) are resolved to structured EdgeEndpoints and written via the raw
223
+ // plumbing. The append accepts EITHER raw --parent/--child OR the role-typed
224
+ // --primary/--counter form (FGAP-113); messaging renders the RESOLVED stored
225
+ // orientation (endpointKey of the returned edge), so a role-typed call reports
226
+ // the parent/child it actually filed. Under dryRun the byRef fn validates the
227
+ // prospective relations + dedup-checks without writing (TASK-010 shared
228
+ // preview path).
229
+ const { appended, edge } = appendRelationByRef(cwd, {
230
+ ...(params.parent !== undefined ? { parent: params.parent } : {}),
231
+ ...(params.child !== undefined ? { child: params.child } : {}),
232
+ ...(params.primary !== undefined ? { primary: params.primary } : {}),
233
+ ...(params.counter !== undefined ? { counter: params.counter } : {}),
233
234
  relation_type: params.relation_type,
234
235
  ...(params.ordinal !== undefined ? { ordinal: params.ordinal } : {}),
235
236
  }, ctx, { dryRun: params.dryRun });
237
+ const from = endpointKey(edge.parent);
238
+ const to = endpointKey(edge.child);
236
239
  const ordinalNote = params.ordinal !== undefined ? ` (ordinal ${params.ordinal})` : "";
237
240
  if (params.dryRun) {
238
241
  return appended
239
- ? `would append relation ${params.parent} -[${params.relation_type}]-> ${params.child}${ordinalNote}`
240
- : `would no-op (duplicate): relation ${params.parent} -[${params.relation_type}]-> ${params.child}`;
242
+ ? `would append relation ${from} -[${params.relation_type}]-> ${to}${ordinalNote}`
243
+ : `would no-op (duplicate): relation ${from} -[${params.relation_type}]-> ${to}`;
241
244
  }
242
245
  return appended
243
- ? `Appended relation ${params.parent} -[${params.relation_type}]-> ${params.child}${ordinalNote}`
244
- : `Relation ${params.parent} -[${params.relation_type}]-> ${params.child} already exists — no-op`;
246
+ ? `Appended relation ${from} -[${params.relation_type}]-> ${to}${ordinalNote}`
247
+ : `Relation ${from} -[${params.relation_type}]-> ${to} already exists — no-op`;
245
248
  },
246
249
  },
247
250
  {
@@ -252,6 +255,9 @@ export const ops = [
252
255
  "inverse of append-relation (ordinal is NOT part of identity). An absent edge is an idempotent no-op. " +
253
256
  "Reference integrity is NOT checked here — run context-validate after if the removal changes resolvability.",
254
257
  promptSnippet: "Remove a relation/edge between two items (the inverse of append-relation)",
258
+ examples: [
259
+ `pi-context remove-relation --parent VER-001 --child TASK-001 --relation_type verification_verifies_item --writer '{"kind":"human","user":"you@example.com"}' --json`,
260
+ ],
255
261
  parameters: Type.Object({
256
262
  parent: Type.String({ description: "Canonical id (or lens bin name) of the parent endpoint" }),
257
263
  child: Type.String({ description: "Canonical id of the child endpoint" }),
@@ -284,8 +290,13 @@ export const ops = [
284
290
  description: "Atomically replace one closure-table relation with another in a SINGLE write (no half-state: the old edge and " +
285
291
  "the new edge never coexist on disk). The old edge is matched on the (parent, child, relation_type) dedup identity; " +
286
292
  "the new edge is written with its optional ordinal. If the old edge is absent the call is effectively an append of " +
287
- "the new edge. Reference integrity is NOT checked here run context-validate after.",
293
+ "the new edge. This op takes RAW parent/child (old + new) and BYPASSES the write-time orientation gate that " +
294
+ "append-relation applies — it writes the endpoints verbatim, so it is the affordance for re-orienting an existing " +
295
+ "edge; reference integrity is NOT checked here — run context-validate after.",
288
296
  promptSnippet: "Atomically swap one relation/edge for another in a single write",
297
+ examples: [
298
+ `pi-context replace-relation --old_parent TASK-001 --old_child DEC-0001 --old_relation_type task_informed_by_decision --parent TASK-001 --child DEC-0002 --relation_type task_informed_by_decision --writer '{"kind":"human","user":"you@example.com"}' --json`,
299
+ ],
289
300
  parameters: Type.Object({
290
301
  old_parent: Type.String({ description: "Parent endpoint selector of the edge to remove" }),
291
302
  old_child: Type.String({ description: "Child endpoint selector of the edge to remove" }),
@@ -340,14 +351,20 @@ export const ops = [
340
351
  {
341
352
  name: "append-relations",
342
353
  label: "Append Relations (bulk)",
343
- description: "Append MANY closure-table relations to relations.json in a single write. Each edge is an object " +
344
- "{ parent, child, relation_type, ordinal? }. Per-(parent, child, relation_type) duplicates are skipped (against " +
345
- "on-disk edges AND earlier edges in the same batch). Returns appended/skipped counts. Reference integrity is NOT " +
346
- "checked here run context-validate after. Creates relations.json if absent.",
347
- promptSnippet: "Create many relations/edges between items in one write",
354
+ description: "Append MANY closure-table relations to relations.json in a single write. Each edge is an object with " +
355
+ "{ relation_type, ordinal? } plus EITHER a raw { parent, child } pair OR the role-typed { primary, counter } pair " +
356
+ "(mapped to parent/child via the relation's declared role_direction); the two pairs are mutually exclusive per edge, " +
357
+ "and a bare { parent, child } for an orientation-ambiguous role-bearing relation rejects the whole batch before any " +
358
+ "write. Per-(parent, child, relation_type) duplicates are skipped (against on-disk edges AND earlier edges in the " +
359
+ "same batch). Returns appended/skipped counts. Reference integrity is NOT checked here — run context-validate " +
360
+ "after. Creates relations.json if absent.",
361
+ promptSnippet: "Create many relations/edges between items in one write (raw or role-typed per edge)",
362
+ examples: [
363
+ `pi-context append-relations --edges '[{"parent":"FEAT-008","child":"TASK-042","relation_type":"feature_decomposed_into_task"}]' --writer '{"kind":"human","user":"you@example.com"}' --json`,
364
+ ],
348
365
  parameters: Type.Object({
349
366
  edges: Type.Unknown({
350
- description: "JSON array of { parent, child, relation_type, ordinal? } selector objects (parent/child are id/lens-bin selectors)",
367
+ description: "JSON array of edge objects. Each edge is { relation_type, ordinal? } plus EITHER a raw { parent, child } pair OR the role-typed { primary, counter } pair (mapped to parent/child via the relation's declared role_direction); the two orientation pairs are mutually exclusive per edge. Selectors are id / <alias>:<refname> / lens-bin.",
351
368
  }),
352
369
  dryRun: Type.Optional(Type.Boolean({ description: "Preview without writing relations.json" })),
353
370
  }),
@@ -364,12 +381,16 @@ export const ops = [
364
381
  }
365
382
  }
366
383
  if (!Array.isArray(edges)) {
367
- throw new Error(`edges parameter must be a JSON array of { parent, child, relation_type, ordinal? } objects`);
384
+ throw new Error(`edges parameter must be a JSON array of relation edge objects`);
368
385
  }
369
386
  // Under dryRun the byRef fn replays the on-disk + in-batch dedup and
370
387
  // validates the prospective relations without writing (TASK-010 shared
371
- // preview path).
372
- const { appended, skipped } = appendRelationsByRef(cwd, edges, ctx, { dryRun: params.dryRun });
388
+ // preview path). Each edge accepts raw {parent,child} or role-typed
389
+ // {primary,counter} (FGAP-113); orientation + the ambiguous-bare-append
390
+ // reject are applied inside appendRelationsByRef before any write.
391
+ const { appended, skipped } = appendRelationsByRef(cwd, edges, ctx, {
392
+ dryRun: params.dryRun,
393
+ });
373
394
  return params.dryRun
374
395
  ? `would append ${appended}, skip ${skipped} (duplicates)`
375
396
  : `appended ${appended}, skipped ${skipped} (duplicates)`;
@@ -382,6 +403,9 @@ export const ops = [
382
403
  "REPLACED (full-shape replacement, not shallow-merge — use update-block-item for merge); otherwise the item is " +
383
404
  "appended. Schema validation is automatic. idField defaults to 'id'.",
384
405
  promptSnippet: "Append-or-replace a full block item by id (replacement, not merge)",
406
+ examples: [
407
+ `pi-context upsert-block-item --block tasks --arrayKey tasks --item @/tmp/task.json --writer '{"kind":"human","user":"you@example.com"}' --json`,
408
+ ],
385
409
  parameters: Type.Object({
386
410
  block: Type.String({ description: "Block name (e.g., 'issues', 'decisions')" }),
387
411
  arrayKey: Type.String({ description: "Array key in the block (e.g., 'issues', 'decisions')" }),
@@ -423,6 +447,9 @@ export const ops = [
423
447
  "(unresolvable/non-item source, unregistered destination alias, unregistered destination relation_type, refname " +
424
448
  "collision) throw. Pass dryRun to compute the destination without writing.",
425
449
  promptSnippet: "Promote an item into another substrate as a derived copy with a lineage edge",
450
+ examples: [
451
+ `pi-context promote-item --source DEC-0001 --destinationSubstrate .context --writer '{"kind":"human","user":"you@example.com"}' --json`,
452
+ ],
426
453
  parameters: Type.Object({
427
454
  source: Type.String({ description: "Source item selector (bare refname / <alias>:<refname>)" }),
428
455
  destinationSubstrate: Type.String({ description: "Registered destination substrate alias" }),
@@ -461,6 +488,9 @@ export const ops = [
461
488
  label: "Append Block Nested Item",
462
489
  description: "Append an item to a nested array on a parent-array item in a project block. Schema validation is automatic.",
463
490
  promptSnippet: "Append items to nested arrays inside parent items (e.g., findings inside a review)",
491
+ examples: [
492
+ `pi-context append-block-nested-item --block spec-reviews --arrayKey reviews --match '{"id":"REVIEW-001"}' --nestedKey findings --item @/tmp/finding.json --writer '{"kind":"human","user":"you@example.com"}' --json`,
493
+ ],
464
494
  parameters: Type.Object({
465
495
  block: Type.String({ description: "Block name (e.g., 'spec-reviews')" }),
466
496
  arrayKey: Type.String({ description: "Parent array key (e.g., 'reviews')" }),
@@ -493,6 +523,9 @@ export const ops = [
493
523
  label: "Update Block Nested Item",
494
524
  description: "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).",
495
525
  promptSnippet: "Update items inside nested arrays — change finding state, mark resolved",
526
+ examples: [
527
+ `pi-context update-block-nested-item --block spec-reviews --arrayKey reviews --match '{"id":"REVIEW-001"}' --nestedKey findings --nestedMatch '{"id":"F-001"}' --updates '{"state":"resolved"}' --writer '{"kind":"human","user":"you@example.com"}' --json`,
528
+ ],
496
529
  parameters: Type.Object({
497
530
  block: Type.String({ description: "Block name (e.g., 'spec-reviews')" }),
498
531
  arrayKey: Type.String({ description: "Parent array key (e.g., 'reviews')" }),
@@ -527,6 +560,9 @@ export const ops = [
527
560
  label: "Remove Block Item",
528
561
  description: "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.",
529
562
  promptSnippet: "Remove items from project blocks — prune retracted issues, dedupe entries",
563
+ examples: [
564
+ `pi-context remove-block-item --block issues --arrayKey issues --match '{"id":"ISSUE-001"}' --writer '{"kind":"human","user":"you@example.com"}' --json`,
565
+ ],
530
566
  parameters: Type.Object({
531
567
  block: Type.String({ description: "Block name (e.g., 'issues')" }),
532
568
  arrayKey: Type.String({ description: "Top-level array key (e.g., 'issues')" }),
@@ -546,6 +582,9 @@ export const ops = [
546
582
  label: "Remove Block Nested Item",
547
583
  description: "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.",
548
584
  promptSnippet: "Remove nested items — drop rejected findings, retract nested references",
585
+ examples: [
586
+ `pi-context remove-block-nested-item --block spec-reviews --arrayKey reviews --match '{"id":"REVIEW-001"}' --nestedKey findings --nestedMatch '{"id":"F-001"}' --writer '{"kind":"human","user":"you@example.com"}' --json`,
587
+ ],
549
588
  parameters: Type.Object({
550
589
  block: Type.String({ description: "Block name (e.g., 'spec-reviews')" }),
551
590
  arrayKey: Type.String({ description: "Parent array key (e.g., 'reviews')" }),
@@ -574,6 +613,7 @@ export const ops = [
574
613
  label: "Read Block Dir",
575
614
  description: "Enumerate and parse all .json files in a <substrate-dir>/<subdir>/ directory, returned as a sorted array. Missing directories return [].",
576
615
  promptSnippet: "Enumerate project block subdirectories (phases, schemas, etc.) as parsed JSON",
616
+ examples: [`pi-context read-block-dir --subdir phases --json`],
577
617
  parameters: Type.Object({
578
618
  subdir: Type.String({ description: "Subdirectory under the substrate dir (e.g., 'phases', 'schemas')" }),
579
619
  }),
@@ -589,6 +629,7 @@ export const ops = [
589
629
  label: "Read Block",
590
630
  description: "Read a project block file as structured JSON.",
591
631
  promptSnippet: "Read a project block as structured JSON",
632
+ examples: [`pi-context read-block --block tasks --json`],
592
633
  parameters: Type.Object({
593
634
  block: Type.String({ description: "Block name (e.g., 'issues', 'tasks', 'requirements')" }),
594
635
  }),
@@ -611,6 +652,9 @@ export const ops = [
611
652
  label: "Write Block",
612
653
  description: "Write or replace an entire project block with schema validation.",
613
654
  promptSnippet: "Write or replace a project block with schema validation",
655
+ examples: [
656
+ `pi-context write-block --block architecture --data @/tmp/architecture.json --writer '{"kind":"human","user":"you@example.com"}' --json`,
657
+ ],
614
658
  parameters: Type.Object({
615
659
  block: Type.String({ description: "Block name (e.g., 'project', 'architecture')" }),
616
660
  data: Type.Unknown({ description: "Complete block data — must conform to block schema" }),
@@ -628,6 +672,7 @@ export const ops = [
628
672
  label: "Context Status",
629
673
  description: "Get derived context state — source metrics, block summaries, planning lifecycle status.",
630
674
  promptSnippet: "Get context state — source metrics, block summaries, planning lifecycle status",
675
+ examples: [`pi-context context-status --json`],
631
676
  parameters: Type.Object({}),
632
677
  surface: "use",
633
678
  run(cwd, _params) {
@@ -635,11 +680,24 @@ export const ops = [
635
680
  return { json: result };
636
681
  },
637
682
  },
683
+ {
684
+ name: "context-check-status",
685
+ label: "Context Check Status",
686
+ description: "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. Like every substrate-lifecycle ceremony it seeds the catalog's config migration declarations into migrations.json (idempotent) before its first config read, so a version-lagging legacy substrate is diagnosable; beyond that seed it writes nothing.",
687
+ promptSnippet: "Report installed-vs-catalog schema drift + the version gap for behind schemas (read-only)",
688
+ examples: [`pi-context context-check-status --json`],
689
+ parameters: Type.Object({}),
690
+ surface: "use",
691
+ run(cwd, _params) {
692
+ return { json: checkStatus(cwd) };
693
+ },
694
+ },
638
695
  {
639
696
  name: "context-validate",
640
697
  label: "Context Validate",
641
698
  description: "Validate cross-block referential integrity — check that IDs referenced across blocks exist.",
642
699
  promptSnippet: "Validate cross-block referential integrity",
700
+ examples: [`pi-context context-validate --json`],
643
701
  parameters: Type.Object({}),
644
702
  surface: "use",
645
703
  run(cwd, _params) {
@@ -652,6 +710,7 @@ export const ops = [
652
710
  label: "Read Config",
653
711
  description: "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.",
654
712
  promptSnippet: "Read project config — vocabulary, lenses, relation_types, status_buckets",
713
+ examples: [`pi-context read-config --registry block_kinds --id tasks --json`],
655
714
  parameters: Type.Object({
656
715
  registry: Type.Optional(Type.String({
657
716
  description: "Address ONE config registry/map by key (e.g. 'relation_types', 'lenses', 'block_kinds', 'status_buckets')",
@@ -673,10 +732,14 @@ export const ops = [
673
732
  if (!entry.found) {
674
733
  return `read-config: entry not found in ${params.registry} — ${entry.resolved}`;
675
734
  }
676
- const read = structureForRead(entry.value, { label: `config.${params.registry}.${params.id}` });
735
+ const read = structureForRead(entry.value, {
736
+ whole: true,
737
+ label: `config.${params.registry}.${params.id}`,
738
+ });
677
739
  return { read };
678
740
  }
679
741
  const read = structureForRead(reg.value, {
742
+ whole: true,
680
743
  label: `config.${params.registry}`,
681
744
  overCapDirective: {
682
745
  tool: "read-config",
@@ -730,7 +793,7 @@ export const ops = [
730
793
  if (tool === undefined) {
731
794
  return `list-tools: tool not found — name=${params.name}`;
732
795
  }
733
- const read = structureForRead(tool, { label: `tool ${params.name}` });
796
+ const read = structureForRead(tool, { whole: true, label: `tool ${params.name}` });
734
797
  return { read };
735
798
  }
736
799
  // Default: compact index (FGAP-101) — name + param count + one-line description.
@@ -757,6 +820,7 @@ export const ops = [
757
820
  label: "Read Samples Catalog",
758
821
  description: "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.",
759
822
  promptSnippet: "Discover installable sample block kinds — title, shape, relation_types, invariants, lenses",
823
+ examples: [`pi-context read-samples-catalog --kind tasks --json`],
760
824
  parameters: Type.Object({
761
825
  kind: Type.Optional(Type.String({ description: "Filter to one block_kind canonical_id (e.g. 'tasks')" })),
762
826
  }),
@@ -768,17 +832,48 @@ export const ops = [
768
832
  const read = structureForRead(catalog, {
769
833
  label: params.kind ? `samples kind=${params.kind}` : "samples catalog",
770
834
  // Whole catalog → narrow by kind; a single kind has no finer
771
- // addressing (edge → head-leading marker, no directive).
772
- ...(params.kind ? {} : { overCapDirective: { tool: "read-samples-catalog", hint: "kind=<canonical_id>" } }),
835
+ // addressing (edge → head-leading marker, no directive). The
836
+ // single-kind read is an addressed node return it whole (capped),
837
+ // never a 50-item page of an incidental array child.
838
+ ...(params.kind
839
+ ? { whole: true }
840
+ : { overCapDirective: { tool: "read-samples-catalog", hint: "kind=<canonical_id>" } }),
773
841
  });
774
842
  return { read };
775
843
  },
776
844
  },
845
+ {
846
+ name: "read-catalog-schema",
847
+ label: "Read Catalog Schema",
848
+ description: "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.",
849
+ promptSnippet: "Fetch and print the verbatim catalog schema body for a named block kind (raw JSON Schema, diffable locally)",
850
+ examples: ["pi-context read-catalog-schema --kind tasks", "pi-context read-catalog-schema --kind tasks --json"],
851
+ parameters: Type.Object({
852
+ kind: Type.String({ description: "Catalog block_kind canonical_id (e.g. 'tasks')" }),
853
+ }),
854
+ surface: "use",
855
+ // The catalog schema file carries its own trailing newline (`}\n`); emit the file
856
+ // bytes exactly — preserving that single newline, appending none — so
857
+ // `read-catalog-schema --kind <k> | diff <installed> -` shows no phantom line when
858
+ // content matches (the pre-flag defect was the print path appending a second
859
+ // newline, doubling it to `}\n\n`).
860
+ verbatimText: true,
861
+ run(_cwd, params) {
862
+ // Package-intrinsic: reads the extension's bundled catalog schema file,
863
+ // not the project substrate — cwd is unused (like read-samples-catalog).
864
+ // Returns the RAW TEXT bytes as a prose-string OpResult so the verbatim
865
+ // catalog body prints as-is (renderOpResultText) and rides the --json
866
+ // envelope as a string; no {json}/{read} wrap that would re-serialize and
867
+ // alter the bytes the operator diffs.
868
+ return readCatalogSchemaText(params.kind).text;
869
+ },
870
+ },
777
871
  {
778
872
  name: "context-current-state",
779
873
  label: "Context Current State",
780
- description: "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.",
781
- promptSnippet: "Derive current project state — focus, in-flight, next actions, blocked",
874
+ description: "Derive 'where are we + what's next' purely from the substrate — focus, in-flight items, ranked atomic-next actions, blocked items, and milestone rollups. Every facet derives from the config-declared `state_derivation` registry: which block kinds + status bucket count as in-flight, the focus fallback kind + bucket, the ordered cross-kind next-actions push order with per-entry ranking (a named field + ordered value list, e.g. gap priority P0..P3) or topo ordering over the blocking-relation graph, the relation_types whose edges contribute blockers (the stock set being `task_depends_on_task` dependencies + `task_gated_by_item` gates), the membership rollups (e.g. milestones over `phase_positioned_in_milestone`) with their complete/incomplete status strings, and the next-actions head-size cap. A blocked item's dependency/gate target that has not reached the complete bucket is reported in blockedBy and held out of nextActions; a target reaching its complete status releases it. A substrate whose config declares no `state_derivation` reports focus 'state-derivation not configured' with empty arrays. No writes; nothing hand-stored.",
875
+ promptSnippet: "Derive current project state from the config-declared state_derivation registry — focus, in-flight, ranked next actions, blocked, milestone rollups",
876
+ examples: [`pi-context context-current-state --json`],
782
877
  parameters: Type.Object({}),
783
878
  surface: "use",
784
879
  run(cwd, _params) {
@@ -791,6 +886,7 @@ export const ops = [
791
886
  label: "Context Bootstrap State",
792
887
  description: "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.",
793
888
  promptSnippet: "Derive substrate bootstrap state — no-pointer | no-config | skeleton | not-installed | ready (never throws pre-bootstrap)",
889
+ examples: [`pi-context context-bootstrap-state --json`],
794
890
  parameters: Type.Object({}),
795
891
  surface: "use",
796
892
  run(cwd, _params) {
@@ -803,6 +899,7 @@ export const ops = [
803
899
  label: "Rename Canonical Id",
804
900
  description: "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.",
805
901
  promptSnippet: "Rename a canonical_id (item/relation_type/lens/layer) across substrate; dryRun to preview",
902
+ examples: [`pi-context rename-canonical-id --kind item --oldId TASK-001 --newId TASK-100 --dryRun true --json`],
806
903
  parameters: Type.Object({
807
904
  kind: Type.String({ description: "One of: item | relation_type | lens | layer" }),
808
905
  oldId: Type.String({ description: "Current canonical_id to rename from" }),
@@ -826,6 +923,9 @@ export const ops = [
826
923
  "relation_type / lens / layer / block_kind) is NOT checked here — run context-validate after. dryRun previews " +
827
924
  "without writing.",
828
925
  promptSnippet: "Add/replace/remove one entry in a config.json registry (vocabulary, lenses, invariants, status_buckets)",
926
+ examples: [
927
+ `pi-context amend-config --registry relation_types --operation add --key task_blocks_task --entry @/tmp/relation-type.json --writer '{"kind":"human","user":"you@example.com"}' --json`,
928
+ ],
829
929
  parameters: Type.Object({
830
930
  registry: Type.String({
831
931
  description: "One of: block_kinds | relation_types | lenses | layers | invariants | status_buckets | display_strings | naming | installed_schemas | installed_blocks | hierarchy",
@@ -869,6 +969,7 @@ export const ops = [
869
969
  label: "Read Schema",
870
970
  description: "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.",
871
971
  promptSnippet: "Read a block schema as structured JSON — optionally address one property via `path`",
972
+ examples: [`pi-context read-schema --schemaName framework-gaps --path properties.gaps.items.required --json`],
872
973
  parameters: Type.Object({
873
974
  schemaName: Type.String({
874
975
  description: "Schema name without extension (e.g., 'tasks', 'decisions', 'issues')",
@@ -886,7 +987,7 @@ export const ops = [
886
987
  if (!addr.found) {
887
988
  return `read-schema: property not found — ${addr.resolved}`;
888
989
  }
889
- const read = structureForRead(addr.value, { label: `${params.schemaName} ${addr.resolved}` });
990
+ const read = structureForRead(addr.value, { whole: true, label: `${params.schemaName} ${addr.resolved}` });
890
991
  return { read };
891
992
  }
892
993
  const result = { schema, schemaPath: schemaPathStr };
@@ -910,6 +1011,9 @@ export const ops = [
910
1011
  "read/write of items declaring an older schema_version throws version-mismatch. Registering the block_kind " +
911
1012
  "that points at this schema is a separate step (amend-config block_kinds).",
912
1013
  promptSnippet: "Create or replace a block-kind JSON Schema (meta-validated, atomic)",
1014
+ examples: [
1015
+ `pi-context write-schema --operation create --schemaName tasks --schema @/tmp/tasks.schema.json --writer '{"kind":"human","user":"you@example.com"}' --json`,
1016
+ ],
913
1017
  parameters: Type.Object({
914
1018
  operation: Type.String({ description: "create | replace" }),
915
1019
  schemaName: Type.String({ description: "Schema name without extension (e.g., 'tasks')" }),
@@ -937,11 +1041,52 @@ export const ops = [
937
1041
  return `write-schema: ${verb} schema '${params.schemaName}' at ${result.schemaPath}`;
938
1042
  },
939
1043
  },
1044
+ {
1045
+ name: "resolve-conflict",
1046
+ label: "Resolve Schema Conflict",
1047
+ description: "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.",
1048
+ promptSnippet: "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)",
1049
+ examples: [
1050
+ `pi-context resolve-conflict --schemaName tasks --schema @/tmp/tasks.reconciled.json --writer '{"kind":"human","user":"you@example.com"}' --json`,
1051
+ ],
1052
+ parameters: Type.Object({
1053
+ schemaName: Type.String({ description: "Schema name without extension (e.g., 'tasks')" }),
1054
+ schema: Type.Optional(Type.Unknown({
1055
+ description: "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.",
1056
+ })),
1057
+ }),
1058
+ surface: "use",
1059
+ authGated: true,
1060
+ run(cwd, params, ctx) {
1061
+ const result = resolveConflict(cwd, params.schemaName, params.schema, ctx);
1062
+ return { json: result };
1063
+ },
1064
+ },
1065
+ {
1066
+ name: "resolve-blocked",
1067
+ label: "Resolve Blocked",
1068
+ description: "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. The commit is all-or-nothing: a throw partway through it restores every touched file byte-exact — migrations.json, the installed schema, the block file, config.json, and the pending record — and reports the failure, never a partial commit. On a substrate whose config carries no substrate_id, resolve-blocked establishes the identity at entry (mints, persists, registers) before the commit's stamping write and reports it under substrateIdEstablished.",
1069
+ promptSnippet: "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)",
1070
+ examples: [`pi-context resolve-blocked --schemaName tasks --yes --json`],
1071
+ parameters: Type.Object({
1072
+ schemaName: Type.String({
1073
+ description: "Schema name with a pending-blocked entry (from update's blocked report)",
1074
+ }),
1075
+ }),
1076
+ surface: "use",
1077
+ authGated: true,
1078
+ run(cwd, params, ctx) {
1079
+ return { json: resolveBlocked(cwd, params.schemaName, ctx) };
1080
+ },
1081
+ },
940
1082
  {
941
1083
  name: "write-schema-migration",
942
1084
  label: "Write Schema Migration",
943
- description: "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.",
1085
+ description: "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/map_each operations on dotted JSON paths; map_each addresses an array — table mode maps each string element through a lookup (unmatched elements become {relation_type, item_endpoint} with parent/child fallback), set-on-each mode sets a field on every object element. 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.",
944
1086
  promptSnippet: "Declare a schema version-bump migration (identity or declarative-transform) into migrations.json",
1087
+ examples: [
1088
+ `pi-context write-schema-migration --operation create --schemaName tasks --fromVersion 1.0.0 --toVersion 1.1.0 --kind identity --writer '{"kind":"human","user":"you@example.com"}' --json`,
1089
+ ],
945
1090
  parameters: Type.Object({
946
1091
  operation: Type.String({ description: "create | replace | remove" }),
947
1092
  schemaName: Type.String({ description: "Schema name without extension (e.g., 'tasks')." }),
@@ -975,6 +1120,7 @@ export const ops = [
975
1120
  label: "Context Init",
976
1121
  description: "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).",
977
1122
  promptSnippet: "Initialize the substrate dir (bootstrap pointer + dirs + skeleton config; onward via accept-all OR amend-config/edit)",
1123
+ examples: [`pi-context context-init --contextDir .context --json`],
978
1124
  parameters: Type.Object({
979
1125
  contextDir: Type.String({
980
1126
  description: "Substrate dir name (e.g. .context). Required — no default.",
@@ -992,6 +1138,7 @@ export const ops = [
992
1138
  label: "Accept-All Conception",
993
1139
  description: "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.",
994
1140
  promptSnippet: "Adopt the canonical conception as config (accept-all)",
1141
+ examples: [`pi-context context-accept-all --json`],
995
1142
  parameters: Type.Object({}),
996
1143
  surface: "use",
997
1144
  authGated: true,
@@ -1009,11 +1156,65 @@ export const ops = [
1009
1156
  return { json: result };
1010
1157
  },
1011
1158
  },
1159
+ {
1160
+ name: "context-install",
1161
+ label: "Context Install",
1162
+ description: "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. On a substrate whose config carries no substrate_id, install establishes the identity at entry (mints, persists to config.json, registers in the project registry) and reports it under substrateIdEstablished; an established identity is never re-minted.",
1163
+ promptSnippet: "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)",
1164
+ examples: ["pi-context context-install --json", "pi-context context-install --update true --json"],
1165
+ parameters: Type.Object({
1166
+ update: Type.Optional(Type.Boolean({
1167
+ description: "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.",
1168
+ })),
1169
+ }),
1170
+ surface: "use",
1171
+ authGated: true,
1172
+ run(cwd, params) {
1173
+ const result = installContext(cwd, { overwrite: params.update === true });
1174
+ if (result.error)
1175
+ return result.error;
1176
+ return { json: result };
1177
+ },
1178
+ },
1179
+ {
1180
+ name: "update",
1181
+ label: "Update Installed Model",
1182
+ description: "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) vs write-failed (a non-validation throw at the write boundary, e.g. the block writer's duplicate-item-id guard; the failures entry carries the thrown message, the items were NOT flagged invalid, and no markers or pending-blocked record are produced) — the installed -> catalog version pair, and for a validation failure the per-item failures naming the failing item id, field, and constraint. 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 beyond the idempotent ceremony seed of the catalog's config migration declarations into migrations.json (every substrate-lifecycle ceremony seeds at entry, before its first config read, so a version-lagging legacy substrate heals instead of throwing). 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. Because update applies per-component (a blocked schema rolls back only itself; the additive registry propagation writes regardless), a run that refuses any schema while applying registry additions or other-schema resyncs/migrations/merges reports the partiality under partialApplication — applied and notApplied channel mirrors plus a one-line summary naming what was applied alongside what was refused and why — so a blocked run never reads as a no-op; dryRun reports the predicted partiality in the same shape. On a substrate whose config carries no substrate_id, a LIVE update establishes the identity at entry (mints, persists to config.json, registers in the project registry) before its first identity-stamping write — so a pre-identity substrate heals on the ceremony instead of refusing — and reports it under substrateIdEstablished; an established identity is never re-minted, and dryRun (no stamping writes) establishes nothing.",
1183
+ promptSnippet: "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 (no-migration-chain / validation-failed / write-failed for a non-validation write-boundary throw), version pair, per-item failures — and a validation-failed block 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; a run that refuses any schema while applying registry additions or other-schema updates surfaces the partiality under partialApplication with a one-line summary, so a blocked run never reads as a no-op)",
1184
+ examples: [`pi-context update --dryRun true --json`],
1185
+ parameters: Type.Object({
1186
+ dryRun: Type.Optional(Type.Boolean({ description: "Preview the per-schema action plan without writing anything." })),
1187
+ }),
1188
+ surface: "use",
1189
+ run(cwd, params) {
1190
+ const result = updateContext(cwd, { dryRun: params.dryRun === true });
1191
+ if (result.error)
1192
+ return result.error;
1193
+ return { json: result };
1194
+ },
1195
+ },
1196
+ {
1197
+ name: "validate-block-items",
1198
+ label: "Validate Block Items",
1199
+ description: "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.",
1200
+ promptSnippet: "Validate a block's items against the catalog schema version — returns the per-item failures (item id, field, constraint) without writing",
1201
+ examples: [`pi-context validate-block-items --block tasks --json`],
1202
+ parameters: Type.Object({
1203
+ block: Type.String({ description: "Block name (e.g. 'tasks')" }),
1204
+ }),
1205
+ surface: "use",
1206
+ run(cwd, params) {
1207
+ return { json: validateBlockItemsAgainstCatalog(cwd, params.block) };
1208
+ },
1209
+ },
1012
1210
  {
1013
1211
  name: "context-switch",
1014
1212
  label: "Context Switch",
1015
1213
  description: "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).",
1016
1214
  promptSnippet: "Switch the bootstrap pointer to a different substrate dir",
1215
+ examples: [
1216
+ `pi-context context-switch --target_dir .context --writer '{"kind":"human","user":"you@example.com"}' --json`,
1217
+ ],
1017
1218
  parameters: Type.Object({
1018
1219
  target_dir: Type.String({
1019
1220
  description: "Substrate dir name to switch to (e.g. '.context'). Required for default + create_new modes; ignored for to_previous mode.",
@@ -1068,6 +1269,7 @@ export const ops = [
1068
1269
  label: "Context List",
1069
1270
  description: "Enumerate top-level dirs under cwd containing a config.json (switchable substrates). Marks the active one with isActive=true. Read-only.",
1070
1271
  promptSnippet: "List switchable substrate dirs under cwd",
1272
+ examples: [`pi-context context-list --json`],
1071
1273
  parameters: Type.Object({}),
1072
1274
  surface: "use",
1073
1275
  run(cwd, _params) {
@@ -1080,6 +1282,9 @@ export const ops = [
1080
1282
  label: "Context Archive",
1081
1283
  description: "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>/.",
1082
1284
  promptSnippet: "Archive a non-active substrate dir to archive/<dir>/",
1285
+ examples: [
1286
+ `pi-context context-archive --target_dir .context-old --writer '{"kind":"human","user":"you@example.com"}' --json`,
1287
+ ],
1083
1288
  parameters: Type.Object({
1084
1289
  target_dir: Type.String({
1085
1290
  description: "Substrate dir name to archive (e.g. '.project'). Refused if it is the active substrate.",
@@ -1103,6 +1308,7 @@ export const ops = [
1103
1308
  label: "Filter Block Items",
1104
1309
  description: "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.",
1105
1310
  promptSnippet: "Filter a block's items by a predicate — eq / neq / in / matches against a single field",
1311
+ examples: [`pi-context filter-block-items --block framework-gaps --field status --op eq --value '"open"' --json`],
1106
1312
  parameters: Type.Object({
1107
1313
  block: Type.String({
1108
1314
  description: "Block name (e.g., 'tasks', 'decisions', 'framework-gaps', 'context-contracts')",
@@ -1134,6 +1340,7 @@ export const ops = [
1134
1340
  label: "Resolve Item By Id",
1135
1341
  description: "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.",
1136
1342
  promptSnippet: "Resolve a kind-prefixed ID (DEC-/FEAT-/FGAP-/issue-/REQ-/TASK-/etc.) to its owning block and item",
1343
+ examples: [`pi-context resolve-item-by-id --id TASK-001 --json`],
1137
1344
  parameters: Type.Object({
1138
1345
  id: Type.String({ description: "Kind-prefixed ID, e.g., DEC-NNNN / FEAT-NNN / FGAP-NNN / ISSUE-NNN" }),
1139
1346
  }),
@@ -1158,6 +1365,7 @@ export const ops = [
1158
1365
  label: "Read Block Item",
1159
1366
  description: "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.",
1160
1367
  promptSnippet: "Read one item from a block by id (block-scoped; null if absent)",
1368
+ examples: [`pi-context read-block-item --block tasks --id TASK-001 --json`],
1161
1369
  parameters: Type.Object({
1162
1370
  block: Type.String({ description: "Block name (e.g., 'tasks', 'decisions', 'framework-gaps')" }),
1163
1371
  id: Type.String({ description: "Item id within the block (e.g., 'TASK-NNN')" }),
@@ -1176,6 +1384,7 @@ export const ops = [
1176
1384
  label: "Read Block Page",
1177
1385
  description: "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.",
1178
1386
  promptSnippet: "Paginate a block's items — offset + limit; returns {items,total,hasMore}",
1387
+ examples: [`pi-context read-block-page --block tasks --limit 20 --json`],
1179
1388
  parameters: Type.Object({
1180
1389
  block: Type.String({ description: "Block name (e.g., 'framework-gaps', 'decisions', 'issues')" }),
1181
1390
  offset: Type.Optional(Type.Integer({ minimum: 0, description: "Start index (default 0)" })),
@@ -1195,6 +1404,9 @@ export const ops = [
1195
1404
  label: "Join Blocks",
1196
1405
  description: "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.",
1197
1406
  promptSnippet: "Join two blocks in one call — by relation edge or shared field; returns {left,right[]} pairs",
1407
+ examples: [
1408
+ `pi-context join-blocks --leftBlock tasks --rightBlock verification --relationType verification_verifies_item --leftEndpoint child --json`,
1409
+ ],
1198
1410
  parameters: Type.Object({
1199
1411
  leftBlock: Type.String({ description: "Left block name (e.g., 'tasks')" }),
1200
1412
  rightBlock: Type.String({ description: "Right block name (e.g., 'verification')" }),
@@ -1237,6 +1449,7 @@ export const ops = [
1237
1449
  label: "Resolve Items By Id (Bulk)",
1238
1450
  description: "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.",
1239
1451
  promptSnippet: "Resolve a batch of kind-prefixed ids (DEC-/FGAP-/TASK-/issue-/REQ-/...) in one call",
1452
+ examples: [`pi-context resolve-items-by-id --ids '["TASK-001","DEC-0001","FGAP-042"]' --json`],
1240
1453
  parameters: Type.Object({
1241
1454
  ids: Type.Array(Type.String(), {
1242
1455
  description: "Array of kind-prefixed ids (DEC-/FGAP-/TASK-/issue-/REQ-/...) to resolve in one call",
@@ -1259,6 +1472,9 @@ export const ops = [
1259
1472
  label: "Complete Task",
1260
1473
  description: "Complete a task with verification gate — requires a passing verification entry targeting the task.",
1261
1474
  promptSnippet: "Complete a task — gates on passing verification before updating status",
1475
+ examples: [
1476
+ `pi-context complete-task --taskId TASK-001 --verificationId VER-001 --writer '{"kind":"human","user":"you@example.com"}' --json`,
1477
+ ],
1262
1478
  parameters: Type.Object({
1263
1479
  taskId: Type.String({ description: "Task ID to complete" }),
1264
1480
  verificationId: Type.String({
@@ -1276,6 +1492,7 @@ export const ops = [
1276
1492
  label: "Context Validate Relations",
1277
1493
  description: "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.",
1278
1494
  promptSnippet: "Validate substrate relations against config + items",
1495
+ examples: [`pi-context context-validate-relations --json`],
1279
1496
  parameters: Type.Object({}),
1280
1497
  surface: "use",
1281
1498
  run(cwd, _params) {
@@ -1288,6 +1505,7 @@ export const ops = [
1288
1505
  label: "Context Edges For Lens",
1289
1506
  description: "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.",
1290
1507
  promptSnippet: "Materialize edges for a named lens (auto-derived or hand-curated)",
1508
+ examples: [`pi-context context-edges-for-lens --lensId feature-decomposition --json`],
1291
1509
  parameters: Type.Object({
1292
1510
  lensId: Type.String({ description: "Lens id from config.lenses[].id" }),
1293
1511
  }),
@@ -1298,11 +1516,51 @@ export const ops = [
1298
1516
  return { read };
1299
1517
  },
1300
1518
  },
1519
+ {
1520
+ name: "context-lens-view",
1521
+ label: "Context Lens View",
1522
+ description: "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.",
1523
+ promptSnippet: "Project a config-declared lens as a binned item-view — bin->count summary, or one bin's items paged",
1524
+ examples: [
1525
+ `pi-context context-lens-view --lensId gaps-by-status --json`,
1526
+ `pi-context context-lens-view --lensId gaps-by-status --bin identified --limit 20 --json`,
1527
+ ],
1528
+ parameters: Type.Object({
1529
+ lensId: Type.String({ description: "Lens id from config.lenses[].id" }),
1530
+ bin: Type.Optional(Type.String({ description: "Return this bin's items paged; omit for a bin->count summary" })),
1531
+ offset: Type.Optional(Type.Integer({ minimum: 0, description: "Per-bin page start index (default 0)" })),
1532
+ limit: Type.Optional(Type.Integer({ minimum: 1, description: "Per-bin page size (default 50)" })),
1533
+ }),
1534
+ surface: "use",
1535
+ run(cwd, params) {
1536
+ const view = loadLensView(cwd, params.lensId);
1537
+ if ("error" in view)
1538
+ throw new Error(view.error); // unknown lens / no-config -> non-zero exit
1539
+ if (params.bin !== undefined) {
1540
+ const items = view.grouped.get(params.bin);
1541
+ if (items === undefined)
1542
+ throw new Error(`Bin '${params.bin}' not declared on lens '${params.lensId}'. Bins: ${view.lens.bins.join(", ")}`);
1543
+ const page = pageArray(items, { offset: params.offset, limit: params.limit });
1544
+ return { read: structureForRead(page, { whole: true, label: `lens ${params.lensId} bin ${params.bin}` }) };
1545
+ }
1546
+ const summary = {
1547
+ lens: view.lens.id,
1548
+ kind: view.lens.kind ?? "target",
1549
+ bins: Object.fromEntries(view.lens.bins.map((b) => [b, (view.grouped.get(b) ?? []).length])),
1550
+ uncategorized: view.uncategorized.length,
1551
+ total: view.items.length,
1552
+ };
1553
+ return { read: structureForRead(summary, { whole: true, label: `lens ${params.lensId} bins` }) };
1554
+ },
1555
+ },
1301
1556
  {
1302
1557
  name: "context-walk-descendants",
1303
1558
  label: "Context Walk Descendants",
1304
- description: "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).",
1559
+ description: "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). For a DISJOINT-kind relation, querying from the wrong (target-kind) end THROWS naming walk-ancestors instead of silently returning []; same-kind / wildcard relations return [] honestly.",
1305
1560
  promptSnippet: "Walk closure-table descendants under a relation_type",
1561
+ examples: [
1562
+ `pi-context context-walk-descendants --parentId FEAT-008 --relationType feature_decomposed_into_task --json`,
1563
+ ],
1306
1564
  parameters: Type.Object({
1307
1565
  parentId: Type.String({ description: "Parent id (canonical id or lens bin name)" }),
1308
1566
  relationType: Type.String({ description: "Relation type from config.relation_types[].canonical_id" }),
@@ -1316,8 +1574,9 @@ export const ops = [
1316
1574
  {
1317
1575
  name: "walk-ancestors",
1318
1576
  label: "Walk Ancestors",
1319
- description: "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).",
1577
+ description: "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). For a DISJOINT-kind relation, querying from the wrong (source-kind) end THROWS naming context-walk-descendants instead of silently returning []; same-kind / wildcard relations return [] honestly.",
1320
1578
  promptSnippet: "Walk closure-table ancestors under a relation_type",
1579
+ examples: [`pi-context walk-ancestors --itemId TASK-042 --relationType feature_decomposed_into_task --json`],
1321
1580
  parameters: Type.Object({
1322
1581
  itemId: Type.String({ description: "Child item id whose ancestors are sought" }),
1323
1582
  relationType: Type.String({ description: "Relation type from config.relation_types[].canonical_id" }),
@@ -1334,6 +1593,7 @@ export const ops = [
1334
1593
  label: "Find References",
1335
1594
  description: "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.",
1336
1595
  promptSnippet: "Find closure-table edges incident on an item id",
1596
+ examples: [`pi-context find-references --itemId TASK-001 --direction both --json`],
1337
1597
  parameters: Type.Object({
1338
1598
  itemId: Type.String({ description: "Item id whose incident edges are sought" }),
1339
1599
  direction: Type.Optional(Type.Union([Type.Literal("inbound"), Type.Literal("outbound"), Type.Literal("both")], {
@@ -1352,6 +1612,7 @@ export const ops = [
1352
1612
  label: "Gather Execution Context",
1353
1613
  description: "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.",
1354
1614
  promptSnippet: "Compose ContextBundle for unit + context-contract-declared bundle_relation_types",
1615
+ examples: [`pi-context gather-execution-context --unitId TASK-001 --kind task --json`],
1355
1616
  parameters: Type.Object({
1356
1617
  unitId: Type.String({ description: "Work-unit id (e.g. TASK-NNN / DEC-NNNN / FGAP-NNN)" }),
1357
1618
  kind: Type.String({
@@ -1374,71 +1635,45 @@ export const ops = [
1374
1635
  {
1375
1636
  name: "context-roadmap-load",
1376
1637
  label: "Context: load roadmap",
1377
- description: "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'.",
1378
- promptSnippet: "Load a roadmap by id",
1379
- parameters: Type.Object({
1380
- roadmapId: Type.String({ description: "ROADMAP-NNN id from <config.root>/roadmap.json" }),
1381
- }),
1638
+ description: "Load the derived roadmap view over the milestone_precedes_milestone DAG: milestone-block items topo-ordered by the authored precedes edges (order + cycles), each milestone carrying its derived status/phaseCount (currentState's milestone rollup), its member phases (parents of phase_positioned_in_milestone edges), each phase's tasks (parents of task_positioned_in_phase edges), and per-phase + per-milestone status rollups. Adjacency comes strictly from the authored edges — never inferred from order. Zero milestones is a valid empty view.",
1639
+ promptSnippet: "Load the derived milestone roadmap view",
1640
+ examples: [`pi-context context-roadmap-load --json`],
1641
+ parameters: Type.Object({}),
1382
1642
  surface: "use",
1383
- run(cwd, params) {
1384
- const view = loadRoadmap(cwd, params.roadmapId);
1385
- if ("error" in view) {
1386
- const read = structureForRead(view, { whole: true, label: `roadmap ${params.roadmapId} (error)` });
1387
- return { read };
1388
- }
1389
- // whole: a structured RoadmapView (phases + lens-views + rollups) — keep
1390
- // the view shape intact rather than paging an inner array.
1391
- const read = structureForRead(serializeRoadmapView(view), {
1392
- whole: true,
1393
- label: `roadmap ${params.roadmapId}`,
1394
- });
1643
+ run(cwd, _params) {
1644
+ const view = loadRoadmap(cwd);
1645
+ // whole: a structured MilestoneRoadmapView (milestones + rollups + edges)
1646
+ // keep the view shape intact rather than paging an inner array.
1647
+ const read = structureForRead(view, { whole: true, label: "roadmap (derived)" });
1395
1648
  return { read };
1396
1649
  },
1397
1650
  },
1398
1651
  {
1399
1652
  name: "context-roadmap-render",
1400
1653
  label: "Context: render roadmap",
1401
- description: "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, milestone resolution, exit criteria. NO mermaid / graph syntax: per-phase **Depends on:** lines come strictly from authored phase_depends_on edges scoped to in-roadmap phases.",
1402
- promptSnippet: "Render a roadmap as markdown",
1403
- parameters: Type.Object({
1404
- roadmapId: Type.String({ description: "ROADMAP-NNN id from <config.root>/roadmap.json" }),
1405
- }),
1654
+ description: "Render the derived roadmap as pure-textual markdown — milestone order list (topo over the authored milestone_precedes_milestone edges), per-milestone sections with **Preceded by:** adjacency lines sourced strictly from those edges (alphabetically sorted; '—' when none), per-milestone rollup counts, and per-phase task tables. Cycle participants surface under a separate heading with a Cycles-detected line. NO mermaid / graph syntax; adjacency is never inferred from order consecutive pairs.",
1655
+ promptSnippet: "Render the derived milestone roadmap as markdown",
1656
+ examples: [`pi-context context-roadmap-render`],
1657
+ parameters: Type.Object({}),
1406
1658
  surface: "use",
1407
- run(cwd, params) {
1408
- const view = loadRoadmap(cwd, params.roadmapId);
1659
+ run(cwd, _params) {
1660
+ const view = loadRoadmap(cwd);
1409
1661
  if ("error" in view) {
1410
1662
  return { json: view };
1411
1663
  }
1412
- const naming = loadContext(cwd).config?.naming;
1413
- return renderRoadmap(view, naming);
1664
+ return renderRoadmap(view);
1414
1665
  },
1415
1666
  },
1416
1667
  {
1417
1668
  name: "context-roadmap-validate",
1418
- label: "Context: validate roadmap(s)",
1419
- description: "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_evidence_block_missing, roadmap_milestone_query_invalid, roadmap_status_unknown_value. Display strings flow through config.display_strings (pi-context divergence). Optional roadmapId filter restricts issue list to a single roadmap.",
1420
- promptSnippet: "Validate roadmaps",
1421
- parameters: Type.Object({
1422
- roadmapId: Type.Optional(Type.String({ description: "Filter to issues matching this roadmap_id (omit for full-project validation)" })),
1423
- }),
1424
- surface: "use",
1425
- run(cwd, params) {
1426
- const result = validateRoadmaps(cwd);
1427
- const filtered = params.roadmapId
1428
- ? result.issues.filter((i) => !i.roadmap_id || i.roadmap_id === params.roadmapId)
1429
- : result.issues;
1430
- return { json: { status: result.status, issues: filtered } };
1431
- },
1432
- },
1433
- {
1434
- name: "context-roadmap-list",
1435
- label: "Context: list roadmaps",
1436
- description: "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).",
1437
- promptSnippet: "List roadmaps",
1669
+ label: "Context: validate roadmap",
1670
+ description: "Validate the derived roadmap over the milestone_precedes_milestone edges. Error codes: roadmap_precedes_endpoint_missing (a precedes-edge endpoint that is not a milestone-block item), roadmap_milestone_cycle (a cycle in the precedes graph), roadmap_milestone_missing (a phase_positioned_in_milestone edge whose child is not a known milestone). Warning: roadmap_status_unknown_value (a member phase whose task rollup buckets unknown with items present). Info: roadmap_milestone_isolated (a milestone with zero precedes edges while others are ordered) — info never affects status: invalid iff any error-code issue, warnings iff any warning-code issue, else clean. Display strings flow through config.display_strings (pi-context divergence).",
1671
+ promptSnippet: "Validate the derived milestone roadmap",
1672
+ examples: [`pi-context context-roadmap-validate --json`],
1438
1673
  parameters: Type.Object({}),
1439
1674
  surface: "use",
1440
1675
  run(cwd, _params) {
1441
- return { json: listRoadmaps(cwd) };
1676
+ return { json: validateRoadmap(cwd) };
1442
1677
  },
1443
1678
  },
1444
1679
  ];