@davidorex/pi-context 0.29.0 → 0.31.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.
- package/CHANGELOG.md +88 -0
- package/README.md +48 -21
- package/dist/block-api.d.ts +25 -3
- package/dist/block-api.d.ts.map +1 -1
- package/dist/block-api.js +17 -6
- package/dist/block-api.js.map +1 -1
- package/dist/content-hash.d.ts +13 -0
- package/dist/content-hash.d.ts.map +1 -1
- package/dist/content-hash.js +16 -0
- package/dist/content-hash.js.map +1 -1
- package/dist/context-dir.d.ts +12 -0
- package/dist/context-dir.d.ts.map +1 -1
- package/dist/context-dir.js +14 -0
- package/dist/context-dir.js.map +1 -1
- package/dist/context-sdk.d.ts +99 -4
- package/dist/context-sdk.d.ts.map +1 -1
- package/dist/context-sdk.js +183 -13
- package/dist/context-sdk.js.map +1 -1
- package/dist/context.d.ts +103 -0
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +85 -0
- package/dist/context.js.map +1 -1
- package/dist/index.d.ts +534 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1879 -20
- package/dist/index.js.map +1 -1
- package/dist/migration-registry-loader.d.ts +16 -0
- package/dist/migration-registry-loader.d.ts.map +1 -1
- package/dist/migration-registry-loader.js +33 -0
- package/dist/migration-registry-loader.js.map +1 -1
- package/dist/ops-registry.d.ts +196 -4
- package/dist/ops-registry.d.ts.map +1 -1
- package/dist/ops-registry.js +795 -110
- package/dist/ops-registry.js.map +1 -1
- package/dist/pending-blocked-store.d.ts +83 -0
- package/dist/pending-blocked-store.d.ts.map +1 -0
- package/dist/pending-blocked-store.js +93 -0
- package/dist/pending-blocked-store.js.map +1 -0
- package/dist/read-element.d.ts +48 -0
- package/dist/read-element.d.ts.map +1 -1
- package/dist/read-element.js +88 -30
- package/dist/read-element.js.map +1 -1
- package/dist/schema-merge.d.ts +26 -0
- package/dist/schema-merge.d.ts.map +1 -0
- package/dist/schema-merge.js +176 -0
- package/dist/schema-merge.js.map +1 -0
- package/dist/write-schema-migration-tool.d.ts +2 -1
- package/dist/write-schema-migration-tool.d.ts.map +1 -1
- package/dist/write-schema-migration-tool.js +13 -10
- package/dist/write-schema-migration-tool.js.map +1 -1
- package/package.json +2 -1
- package/samples/conception.json +50 -0
- package/samples/schemas/framework-gaps.schema.json +1 -1
- package/samples/schemas/issues.schema.json +2 -2
- package/samples/schemas/layer-plans.schema.json +2 -2
- package/samples/schemas/research.schema.json +1 -1
- package/samples/schemas/work-orders.schema.json +2 -2
- package/schemas/config.schema.json +25 -1
- package/schemas/pending-blocked.schema.json +190 -0
- package/skill-narrative.md +7 -5
- package/skills/pi-context/SKILL.md +146 -8
- package/skills/pi-context/references/bundled-resources.md +2 -1
package/dist/ops-registry.js
CHANGED
|
@@ -18,10 +18,10 @@
|
|
|
18
18
|
*/
|
|
19
19
|
import path from "node:path";
|
|
20
20
|
import { Type } from "typebox";
|
|
21
|
-
import { appendToBlock, appendToNestedArray, nextId, readBlock, readBlockDir, removeFromBlock, removeFromNestedArray, updateItemInBlock, updateNestedArrayItem, writeBlock, } from "./block-api.js";
|
|
21
|
+
import { appendToBlock, appendToNestedArray, nextId, readBlock, readBlockDir, removeFromBlock, removeFromNestedArray, updateItemInBlock, updateNestedArrayItem, upsertItemInBlock, writeBlock, } from "./block-api.js";
|
|
22
22
|
import { adoptConception, amendConfigEntry, loadConfig, loadContext } from "./context.js";
|
|
23
23
|
import { BootstrapNotFoundError, schemaPath, tryResolveContextDir } from "./context-dir.js";
|
|
24
|
-
import { appendRelationByRef, completeTask, contextState, currentState, deriveBootstrapState, filterBlockItems, joinBlocks, readBlockItem, readBlockPage, resolveItemById, resolveItemsByIds, validateContext, } from "./context-sdk.js";
|
|
24
|
+
import { appendRelationByRef, appendRelationsByRef, completeTask, contextState, currentState, deriveBootstrapState, 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,15 +29,92 @@ 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,
|
|
35
|
+
import { addressInto, pageArray, renderReadText, structureForRead } from "./read-element.js";
|
|
36
36
|
import { renameCanonicalId } from "./rename-canonical-id.js";
|
|
37
37
|
import { listRoadmaps, loadRoadmap, renderRoadmap, validateRoadmaps } from "./roadmap-plan.js";
|
|
38
38
|
import { samplesCatalog } from "./samples-catalog.js";
|
|
39
39
|
import { readSchema, writeSchemaChecked } from "./schema-write.js";
|
|
40
|
+
import { truncateHead } from "./truncate.js";
|
|
40
41
|
import { writeSchemaMigrationExecute } from "./write-schema-migration-tool.js";
|
|
42
|
+
/**
|
|
43
|
+
* Collapse an {@link OpResult} to the text the default CLI surface + the in-pi
|
|
44
|
+
* Pi-tool surface emit. This reproduces, byte-for-byte, what each op's `run`
|
|
45
|
+
* returned before the TASK-012 split: prose → itself; `{json}` →
|
|
46
|
+
* `JSON.stringify(x, null, 2)`; `{read}` → `renderReadText` (== the old
|
|
47
|
+
* `serializeForRead().content`).
|
|
48
|
+
*/
|
|
49
|
+
/**
|
|
50
|
+
* The unbypassable output-boundary cap (TASK-013 / FGAP-015). The 50KB read cap
|
|
51
|
+
* (`DEFAULT_MAX_BYTES` + `truncateHead`) previously lived ONLY in the `{read}`
|
|
52
|
+
* channel (structureForRead / renderReadText); the prose `string` and `{json}`
|
|
53
|
+
* channels emitted unbounded. A `{json}` op embedding substrate content (e.g.
|
|
54
|
+
* resolve-item-by-id, promote-item) therefore leaked that content uncapped on
|
|
55
|
+
* BOTH surfaces — the CLI `--json` `output` and the shared text renderer used by
|
|
56
|
+
* the default CLI surface AND the in-pi Pi-tool surface. These two helpers move
|
|
57
|
+
* the cap to the emission boundary so it fires for EVERY channel regardless of
|
|
58
|
+
* which op shape produced the value.
|
|
59
|
+
*
|
|
60
|
+
* `{read}` is already fail-closed at structureForRead (over-cap → data null +
|
|
61
|
+
* tiny metadata / refusal text), so both helpers pass it through untouched — it
|
|
62
|
+
* is never double-handled here.
|
|
63
|
+
*/
|
|
64
|
+
/** True when `s` exceeds the 50KB read cap (shared byte-count/threshold logic). */
|
|
65
|
+
function overReadCap(s) {
|
|
66
|
+
const totalBytes = Buffer.byteLength(s, "utf-8");
|
|
67
|
+
return { over: truncateHead(s).truncated, totalBytes };
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* REFUSAL prose for an over-cap `{json}` or prose `string` result — no narrowing
|
|
71
|
+
* tool/addressing is available at this boundary (unlike `{read}`'s
|
|
72
|
+
* overCapDirective), so this mirrors renderReadText's REFUSAL wording without a
|
|
73
|
+
* tool name and returns NO payload body.
|
|
74
|
+
*/
|
|
75
|
+
function overCapRefusalText(totalBytes) {
|
|
76
|
+
return (`⚠️ OUTPUT REFUSED — this result is ${totalBytes} bytes, over the 50KB read cap. ` +
|
|
77
|
+
`Nothing was returned (a partial read would mislead). Narrow your read.`);
|
|
78
|
+
}
|
|
79
|
+
/**
|
|
80
|
+
* Collapse an {@link OpResult} to the text the default CLI surface + the in-pi
|
|
81
|
+
* Pi-tool surface emit, NOW BOUNDED at the 50KB read cap (TASK-013 / FGAP-015).
|
|
82
|
+
* `{read}` → renderReadText (already capped); prose `string` → itself when under
|
|
83
|
+
* cap, else the REFUSAL prose; `{json}` → `JSON.stringify(x, null, 2)` when under
|
|
84
|
+
* cap, else the REFUSAL prose (no partial body).
|
|
85
|
+
*/
|
|
86
|
+
export function renderOpResultText(r) {
|
|
87
|
+
if (typeof r === "string") {
|
|
88
|
+
const { over, totalBytes } = overReadCap(r);
|
|
89
|
+
return over ? overCapRefusalText(totalBytes) : r;
|
|
90
|
+
}
|
|
91
|
+
if ("read" in r)
|
|
92
|
+
return renderReadText(r.read);
|
|
93
|
+
const s = JSON.stringify(r.json, null, 2);
|
|
94
|
+
const { over, totalBytes } = overReadCap(s);
|
|
95
|
+
return over ? overCapRefusalText(totalBytes) : s;
|
|
96
|
+
}
|
|
97
|
+
/**
|
|
98
|
+
* The JSON VALUE for the CLI `--json` envelope `output` field, NOW BOUNDED at the
|
|
99
|
+
* 50KB read cap (TASK-013 / FGAP-015). Prose `string` → itself when under cap,
|
|
100
|
+
* else the REFUSAL string; `{read}` → its ReadStructured (already fail-closed —
|
|
101
|
+
* serializes tiny on over-cap); `{json}` → the raw value when under cap, else a
|
|
102
|
+
* fail-closed envelope that MIRRORS {@link ReadStructured}'s over-cap shape
|
|
103
|
+
* (`{ data: null, truncated: true, totalBytes, complete: false }`) so `--json`
|
|
104
|
+
* consumers see one uniform fail-closed envelope across `{read}` and bounded
|
|
105
|
+
* `{json}`. No partial payload is ever emitted past the cap.
|
|
106
|
+
*/
|
|
107
|
+
export function boundedJsonOutput(r) {
|
|
108
|
+
if (typeof r === "string") {
|
|
109
|
+
const { over, totalBytes } = overReadCap(r);
|
|
110
|
+
return over ? overCapRefusalText(totalBytes) : r;
|
|
111
|
+
}
|
|
112
|
+
if ("read" in r)
|
|
113
|
+
return r.read;
|
|
114
|
+
const s = JSON.stringify(r.json, null, 2);
|
|
115
|
+
const { over, totalBytes } = overReadCap(s);
|
|
116
|
+
return over ? { data: null, truncated: true, totalBytes, complete: false } : r.json;
|
|
117
|
+
}
|
|
41
118
|
// ── serializeRoadmapView ────────────────────────────────────────────────────
|
|
42
119
|
// Strip non-serializable fields (suggestionTemplate fn, grouped Map) from the
|
|
43
120
|
// embedded LoadedLensView records before tool serialization. Relocated verbatim
|
|
@@ -69,6 +146,9 @@ export const ops = [
|
|
|
69
146
|
label: "Append Block Item",
|
|
70
147
|
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.",
|
|
71
148
|
promptSnippet: "Append items to project blocks (issues, decisions, or any user-defined block)",
|
|
149
|
+
examples: [
|
|
150
|
+
`pi-context append-block-item --block framework-gaps --arrayKey gaps --autoId true --item @/tmp/fgap.json --writer '{"kind":"human","user":"you@example.com"}' --json`,
|
|
151
|
+
],
|
|
72
152
|
parameters: Type.Object({
|
|
73
153
|
block: Type.String({ description: "Block name (e.g., 'issues', 'decisions')" }),
|
|
74
154
|
arrayKey: Type.String({ description: "Array key in the block (e.g., 'issues', 'decisions')" }),
|
|
@@ -78,7 +158,7 @@ export const ops = [
|
|
|
78
158
|
})),
|
|
79
159
|
}),
|
|
80
160
|
surface: "use",
|
|
81
|
-
run(cwd, params) {
|
|
161
|
+
run(cwd, params, ctx) {
|
|
82
162
|
// Type.Unknown() params may arrive as JSON strings — parse if needed
|
|
83
163
|
if (typeof params.item === "string") {
|
|
84
164
|
try {
|
|
@@ -96,7 +176,7 @@ export const ops = [
|
|
|
96
176
|
// withBlockLock critical section (block-api assertAppendIdUnique) —
|
|
97
177
|
// the single enforcement point. The prior racy readBlock-then-append
|
|
98
178
|
// tool-layer check was removed in favour of that library guard.
|
|
99
|
-
appendToBlock(cwd, params.block, params.arrayKey, params.item);
|
|
179
|
+
appendToBlock(cwd, params.block, params.arrayKey, params.item, ctx);
|
|
100
180
|
const id = params.item?.id ? ` '${params.item.id}'` : "";
|
|
101
181
|
return `Appended item${id} to ${params.block}.${params.arrayKey}`;
|
|
102
182
|
},
|
|
@@ -106,6 +186,9 @@ export const ops = [
|
|
|
106
186
|
label: "Update Block Item",
|
|
107
187
|
description: "Update fields on an item in a project block array. Finds by predicate field match.",
|
|
108
188
|
promptSnippet: "Update items in project blocks — change status, add details, mark resolved",
|
|
189
|
+
examples: [
|
|
190
|
+
`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`,
|
|
191
|
+
],
|
|
109
192
|
parameters: Type.Object({
|
|
110
193
|
block: Type.String({ description: "Block name (e.g., 'issues', 'decisions')" }),
|
|
111
194
|
arrayKey: Type.String({ description: "Array key in the block" }),
|
|
@@ -115,12 +198,12 @@ export const ops = [
|
|
|
115
198
|
}),
|
|
116
199
|
}),
|
|
117
200
|
surface: "use",
|
|
118
|
-
run(cwd, params) {
|
|
201
|
+
run(cwd, params, ctx) {
|
|
119
202
|
if (Object.keys(params.updates).length === 0) {
|
|
120
203
|
throw new Error("No fields to update — updates parameter is empty");
|
|
121
204
|
}
|
|
122
205
|
const matchEntries = Object.entries(params.match);
|
|
123
|
-
updateItemInBlock(cwd, params.block, params.arrayKey, (item) => matchEntries.every(([k, v]) => item[k] === v), params.updates);
|
|
206
|
+
updateItemInBlock(cwd, params.block, params.arrayKey, (item) => matchEntries.every(([k, v]) => item[k] === v), params.updates, ctx);
|
|
124
207
|
const matchDesc = matchEntries.map(([k, v]) => `${k}=${v}`).join(", ");
|
|
125
208
|
return `Updated item (${matchDesc}) in ${params.block}.${params.arrayKey}: ${Object.keys(params.updates).join(", ")}`;
|
|
126
209
|
},
|
|
@@ -133,6 +216,9 @@ export const ops = [
|
|
|
133
216
|
"integrity (endpoints resolve, relation_type registered, no cycle) is NOT checked here — run context-validate " +
|
|
134
217
|
"after. Creates relations.json if absent.",
|
|
135
218
|
promptSnippet: "Create a relation/edge between two items (parent→child under a relation_type)",
|
|
219
|
+
examples: [
|
|
220
|
+
`pi-context append-relation --parent VER-001 --child TASK-001 --relation_type verification_verifies_item --writer '{"kind":"human","user":"you@example.com"}' --json`,
|
|
221
|
+
],
|
|
136
222
|
parameters: Type.Object({
|
|
137
223
|
parent: Type.String({ description: "Canonical id (or lens bin name) of the parent endpoint" }),
|
|
138
224
|
child: Type.String({ description: "Canonical id of the child endpoint" }),
|
|
@@ -140,25 +226,214 @@ export const ops = [
|
|
|
140
226
|
description: "Registered relation_type canonical_id / hierarchy edge type / lens id",
|
|
141
227
|
}),
|
|
142
228
|
ordinal: Type.Optional(Type.Integer({ description: "Optional sibling-ordering within (parent, relation_type)" })),
|
|
229
|
+
dryRun: Type.Optional(Type.Boolean({ description: "Preview without writing relations.json" })),
|
|
143
230
|
}),
|
|
144
231
|
surface: "use",
|
|
145
|
-
run(cwd, params) {
|
|
232
|
+
run(cwd, params, ctx) {
|
|
146
233
|
// Cycle-5 porcelain: STRING selectors (bare refname / <alias>:<refname> /
|
|
147
234
|
// lens-bin) are resolved to structured EdgeEndpoints and written via the
|
|
148
235
|
// raw plumbing. The param surface stays string-typed; messaging uses the
|
|
149
|
-
// raw selectors (params.*), not the resolved structured endpoints.
|
|
236
|
+
// raw selectors (params.*), not the resolved structured endpoints. Under
|
|
237
|
+
// dryRun the byRef fn validates the prospective relations + dedup-checks
|
|
238
|
+
// without writing (TASK-010 shared preview path).
|
|
150
239
|
const { appended } = appendRelationByRef(cwd, {
|
|
151
240
|
parent: params.parent,
|
|
152
241
|
child: params.child,
|
|
153
242
|
relation_type: params.relation_type,
|
|
154
243
|
...(params.ordinal !== undefined ? { ordinal: params.ordinal } : {}),
|
|
155
|
-
});
|
|
244
|
+
}, ctx, { dryRun: params.dryRun });
|
|
156
245
|
const ordinalNote = params.ordinal !== undefined ? ` (ordinal ${params.ordinal})` : "";
|
|
246
|
+
if (params.dryRun) {
|
|
247
|
+
return appended
|
|
248
|
+
? `would append relation ${params.parent} -[${params.relation_type}]-> ${params.child}${ordinalNote}`
|
|
249
|
+
: `would no-op (duplicate): relation ${params.parent} -[${params.relation_type}]-> ${params.child}`;
|
|
250
|
+
}
|
|
157
251
|
return appended
|
|
158
252
|
? `Appended relation ${params.parent} -[${params.relation_type}]-> ${params.child}${ordinalNote}`
|
|
159
253
|
: `Relation ${params.parent} -[${params.relation_type}]-> ${params.child} already exists — no-op`;
|
|
160
254
|
},
|
|
161
255
|
},
|
|
256
|
+
{
|
|
257
|
+
name: "remove-relation",
|
|
258
|
+
label: "Remove Relation",
|
|
259
|
+
description: "Remove the single closure-table relation (edge) matching parent+child+relation_type from relations.json. " +
|
|
260
|
+
"Matches on the SAME (parent, child, relation_type) dedup identity append-relation uses, so it is the symmetric " +
|
|
261
|
+
"inverse of append-relation (ordinal is NOT part of identity). An absent edge is an idempotent no-op. " +
|
|
262
|
+
"Reference integrity is NOT checked here — run context-validate after if the removal changes resolvability.",
|
|
263
|
+
promptSnippet: "Remove a relation/edge between two items (the inverse of append-relation)",
|
|
264
|
+
examples: [
|
|
265
|
+
`pi-context remove-relation --parent VER-001 --child TASK-001 --relation_type verification_verifies_item --writer '{"kind":"human","user":"you@example.com"}' --json`,
|
|
266
|
+
],
|
|
267
|
+
parameters: Type.Object({
|
|
268
|
+
parent: Type.String({ description: "Canonical id (or lens bin name) of the parent endpoint" }),
|
|
269
|
+
child: Type.String({ description: "Canonical id of the child endpoint" }),
|
|
270
|
+
relation_type: Type.String({
|
|
271
|
+
description: "Registered relation_type canonical_id / hierarchy edge type / lens id",
|
|
272
|
+
}),
|
|
273
|
+
dryRun: Type.Optional(Type.Boolean({ description: "Preview without writing relations.json" })),
|
|
274
|
+
}),
|
|
275
|
+
surface: "use",
|
|
276
|
+
run(cwd, params, ctx) {
|
|
277
|
+
// Cycle-5 porcelain: STRING selectors are resolved to structured
|
|
278
|
+
// EdgeEndpoints, then matched on the identityKey dedup identity. Messaging
|
|
279
|
+
// uses the raw selectors (params.*), not the resolved structured endpoints.
|
|
280
|
+
// Under dryRun the byRef fn validates the prospective post-removal
|
|
281
|
+
// relations + match-checks without writing (TASK-010 shared preview path).
|
|
282
|
+
const { removed } = removeRelationByRef(cwd, { parent: params.parent, child: params.child, relation_type: params.relation_type }, ctx, { dryRun: params.dryRun });
|
|
283
|
+
if (params.dryRun) {
|
|
284
|
+
return removed
|
|
285
|
+
? `would remove relation ${params.parent} -[${params.relation_type}]-> ${params.child}`
|
|
286
|
+
: `would no-op (no matching relation): ${params.parent} -[${params.relation_type}]-> ${params.child}`;
|
|
287
|
+
}
|
|
288
|
+
return removed
|
|
289
|
+
? `Removed relation ${params.parent} -[${params.relation_type}]-> ${params.child}`
|
|
290
|
+
: `Relation ${params.parent} -[${params.relation_type}]-> ${params.child} — no matching relation — no-op`;
|
|
291
|
+
},
|
|
292
|
+
},
|
|
293
|
+
{
|
|
294
|
+
name: "replace-relation",
|
|
295
|
+
label: "Replace Relation",
|
|
296
|
+
description: "Atomically replace one closure-table relation with another in a SINGLE write (no half-state: the old edge and " +
|
|
297
|
+
"the new edge never coexist on disk). The old edge is matched on the (parent, child, relation_type) dedup identity; " +
|
|
298
|
+
"the new edge is written with its optional ordinal. If the old edge is absent the call is effectively an append of " +
|
|
299
|
+
"the new edge. Reference integrity is NOT checked here — run context-validate after.",
|
|
300
|
+
promptSnippet: "Atomically swap one relation/edge for another in a single write",
|
|
301
|
+
examples: [
|
|
302
|
+
`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`,
|
|
303
|
+
],
|
|
304
|
+
parameters: Type.Object({
|
|
305
|
+
old_parent: Type.String({ description: "Parent endpoint selector of the edge to remove" }),
|
|
306
|
+
old_child: Type.String({ description: "Child endpoint selector of the edge to remove" }),
|
|
307
|
+
old_relation_type: Type.String({ description: "relation_type of the edge to remove" }),
|
|
308
|
+
parent: Type.String({ description: "Parent endpoint selector of the replacement edge" }),
|
|
309
|
+
child: Type.String({ description: "Child endpoint selector of the replacement edge" }),
|
|
310
|
+
relation_type: Type.String({ description: "relation_type of the replacement edge" }),
|
|
311
|
+
ordinal: Type.Optional(Type.Integer({ description: "Optional sibling-ordering within (parent, relation_type) for the new edge" })),
|
|
312
|
+
dryRun: Type.Optional(Type.Boolean({ description: "Preview without writing relations.json" })),
|
|
313
|
+
}),
|
|
314
|
+
surface: "use",
|
|
315
|
+
run(cwd, params, ctx) {
|
|
316
|
+
// Under dryRun the byRef fn validates the prospective post-replace
|
|
317
|
+
// relations and computes the same removed/replaced would-decisions
|
|
318
|
+
// without writing (TASK-010 shared preview path).
|
|
319
|
+
const { replaced, removed } = replaceRelationByRef(cwd, {
|
|
320
|
+
old: { parent: params.old_parent, child: params.old_child, relation_type: params.old_relation_type },
|
|
321
|
+
new: {
|
|
322
|
+
parent: params.parent,
|
|
323
|
+
child: params.child,
|
|
324
|
+
relation_type: params.relation_type,
|
|
325
|
+
...(params.ordinal !== undefined ? { ordinal: params.ordinal } : {}),
|
|
326
|
+
},
|
|
327
|
+
}, ctx, { dryRun: params.dryRun });
|
|
328
|
+
const ordinalNote = params.ordinal !== undefined ? ` (ordinal ${params.ordinal})` : "";
|
|
329
|
+
const oldDesc = `${params.old_parent} -[${params.old_relation_type}]-> ${params.old_child}`;
|
|
330
|
+
const newDesc = `${params.parent} -[${params.relation_type}]-> ${params.child}${ordinalNote}`;
|
|
331
|
+
if (params.dryRun) {
|
|
332
|
+
if (!removed && !replaced) {
|
|
333
|
+
return `would no-op — old edge ${oldDesc} absent and new edge ${newDesc} already present`;
|
|
334
|
+
}
|
|
335
|
+
if (!removed) {
|
|
336
|
+
return `would append new relation ${newDesc} (old ${oldDesc} absent)`;
|
|
337
|
+
}
|
|
338
|
+
if (!replaced) {
|
|
339
|
+
return `would remove relation ${oldDesc}; new relation ${newDesc} already present (no duplicate written)`;
|
|
340
|
+
}
|
|
341
|
+
return `would replace relation ${oldDesc} with ${newDesc}`;
|
|
342
|
+
}
|
|
343
|
+
if (!removed && !replaced) {
|
|
344
|
+
return `Replace relation no-op — old edge ${oldDesc} absent and new edge ${newDesc} already present`;
|
|
345
|
+
}
|
|
346
|
+
if (!removed) {
|
|
347
|
+
return `Old relation ${oldDesc} absent — appended new relation ${newDesc}`;
|
|
348
|
+
}
|
|
349
|
+
if (!replaced) {
|
|
350
|
+
return `Removed relation ${oldDesc}; new relation ${newDesc} already present (no duplicate written)`;
|
|
351
|
+
}
|
|
352
|
+
return `Replaced relation ${oldDesc} with ${newDesc}`;
|
|
353
|
+
},
|
|
354
|
+
},
|
|
355
|
+
{
|
|
356
|
+
name: "append-relations",
|
|
357
|
+
label: "Append Relations (bulk)",
|
|
358
|
+
description: "Append MANY closure-table relations to relations.json in a single write. Each edge is an object " +
|
|
359
|
+
"{ parent, child, relation_type, ordinal? }. Per-(parent, child, relation_type) duplicates are skipped (against " +
|
|
360
|
+
"on-disk edges AND earlier edges in the same batch). Returns appended/skipped counts. Reference integrity is NOT " +
|
|
361
|
+
"checked here — run context-validate after. Creates relations.json if absent.",
|
|
362
|
+
promptSnippet: "Create many relations/edges between items in one write",
|
|
363
|
+
examples: [
|
|
364
|
+
`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`,
|
|
365
|
+
],
|
|
366
|
+
parameters: Type.Object({
|
|
367
|
+
edges: Type.Unknown({
|
|
368
|
+
description: "JSON array of { parent, child, relation_type, ordinal? } selector objects (parent/child are id/lens-bin selectors)",
|
|
369
|
+
}),
|
|
370
|
+
dryRun: Type.Optional(Type.Boolean({ description: "Preview without writing relations.json" })),
|
|
371
|
+
}),
|
|
372
|
+
surface: "use",
|
|
373
|
+
run(cwd, params, ctx) {
|
|
374
|
+
// Type.Unknown() params may arrive as JSON strings — parse if needed.
|
|
375
|
+
let edges = params.edges;
|
|
376
|
+
if (typeof edges === "string") {
|
|
377
|
+
try {
|
|
378
|
+
edges = JSON.parse(edges);
|
|
379
|
+
}
|
|
380
|
+
catch {
|
|
381
|
+
throw new Error(`edges parameter must be a JSON array, got unparseable string`);
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
if (!Array.isArray(edges)) {
|
|
385
|
+
throw new Error(`edges parameter must be a JSON array of { parent, child, relation_type, ordinal? } objects`);
|
|
386
|
+
}
|
|
387
|
+
// Under dryRun the byRef fn replays the on-disk + in-batch dedup and
|
|
388
|
+
// validates the prospective relations without writing (TASK-010 shared
|
|
389
|
+
// preview path).
|
|
390
|
+
const { appended, skipped } = appendRelationsByRef(cwd, edges, ctx, { dryRun: params.dryRun });
|
|
391
|
+
return params.dryRun
|
|
392
|
+
? `would append ${appended}, skip ${skipped} (duplicates)`
|
|
393
|
+
: `appended ${appended}, skipped ${skipped} (duplicates)`;
|
|
394
|
+
},
|
|
395
|
+
},
|
|
396
|
+
{
|
|
397
|
+
name: "upsert-block-item",
|
|
398
|
+
label: "Upsert Block Item",
|
|
399
|
+
description: "Append-or-replace an item in a project block array by id: if an item with the same idField value exists it is " +
|
|
400
|
+
"REPLACED (full-shape replacement, not shallow-merge — use update-block-item for merge); otherwise the item is " +
|
|
401
|
+
"appended. Schema validation is automatic. idField defaults to 'id'.",
|
|
402
|
+
promptSnippet: "Append-or-replace a full block item by id (replacement, not merge)",
|
|
403
|
+
examples: [
|
|
404
|
+
`pi-context upsert-block-item --block tasks --arrayKey tasks --item @/tmp/task.json --writer '{"kind":"human","user":"you@example.com"}' --json`,
|
|
405
|
+
],
|
|
406
|
+
parameters: Type.Object({
|
|
407
|
+
block: Type.String({ description: "Block name (e.g., 'issues', 'decisions')" }),
|
|
408
|
+
arrayKey: Type.String({ description: "Array key in the block (e.g., 'issues', 'decisions')" }),
|
|
409
|
+
item: Type.Unknown({ description: "Full item object to upsert — must conform to block schema" }),
|
|
410
|
+
idField: Type.Optional(Type.String({ description: "Field used as the upsert key (default 'id')" })),
|
|
411
|
+
dryRun: Type.Optional(Type.Boolean({ description: "Preview the upsert without writing" })),
|
|
412
|
+
}),
|
|
413
|
+
surface: "use",
|
|
414
|
+
run(cwd, params, ctx) {
|
|
415
|
+
// Type.Unknown() params may arrive as JSON strings — parse if needed.
|
|
416
|
+
if (typeof params.item === "string") {
|
|
417
|
+
try {
|
|
418
|
+
params.item = JSON.parse(params.item);
|
|
419
|
+
}
|
|
420
|
+
catch {
|
|
421
|
+
throw new Error(`item parameter must be a JSON object, got unparseable string`);
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
const idField = params.idField ?? "id";
|
|
425
|
+
// Under dryRun upsertItemInBlock computes mode + builds + validates the prospective
|
|
426
|
+
// whole block, writing nothing (TASK-011 shared preview path).
|
|
427
|
+
const { mode } = upsertItemInBlock(cwd, params.block, params.arrayKey, params.item, idField, ctx, {
|
|
428
|
+
dryRun: params.dryRun,
|
|
429
|
+
});
|
|
430
|
+
const idVal = params.item?.[idField];
|
|
431
|
+
const idDesc = idVal !== undefined ? ` '${idVal}'` : "";
|
|
432
|
+
return params.dryRun
|
|
433
|
+
? `would upsert item${idDesc} (${mode}) in ${params.block}.${params.arrayKey}`
|
|
434
|
+
: `Upserted item${idDesc} (${mode}) to ${params.block}.${params.arrayKey}`;
|
|
435
|
+
},
|
|
436
|
+
},
|
|
162
437
|
{
|
|
163
438
|
name: "promote-item",
|
|
164
439
|
label: "Promote Item",
|
|
@@ -169,6 +444,9 @@ export const ops = [
|
|
|
169
444
|
"(unresolvable/non-item source, unregistered destination alias, unregistered destination relation_type, refname " +
|
|
170
445
|
"collision) throw. Pass dryRun to compute the destination without writing.",
|
|
171
446
|
promptSnippet: "Promote an item into another substrate as a derived copy with a lineage edge",
|
|
447
|
+
examples: [
|
|
448
|
+
`pi-context promote-item --source DEC-0001 --destinationSubstrate .context --writer '{"kind":"human","user":"you@example.com"}' --json`,
|
|
449
|
+
],
|
|
172
450
|
parameters: Type.Object({
|
|
173
451
|
source: Type.String({ description: "Source item selector (bare refname / <alias>:<refname>)" }),
|
|
174
452
|
destinationSubstrate: Type.String({ description: "Registered destination substrate alias" }),
|
|
@@ -180,17 +458,26 @@ export const ops = [
|
|
|
180
458
|
}, { description: "DispatchContext.writer per pi-context/src/dispatch-context.ts." }),
|
|
181
459
|
}),
|
|
182
460
|
surface: "use",
|
|
183
|
-
run(cwd, params) {
|
|
184
|
-
|
|
185
|
-
|
|
461
|
+
run(cwd, params, ctx) {
|
|
462
|
+
// The DispatchContext now arrives via the op contract — registerAll
|
|
463
|
+
// (in-pi) builds it from the auth-gate-stamped `params.writer`, and the
|
|
464
|
+
// CLI builds it from its resolved identity. The schema `writer` field is
|
|
465
|
+
// retained (the in-pi auth-gate stamps it), but lineage attestation reads
|
|
466
|
+
// the contract ctx, not params.writer.
|
|
467
|
+
if (!ctx?.writer) {
|
|
468
|
+
throw new Error("promote-item: a DispatchContext writer is required.");
|
|
186
469
|
}
|
|
187
470
|
const result = promoteItem(cwd, {
|
|
188
471
|
source: params.source,
|
|
189
472
|
destinationSubstrate: params.destinationSubstrate,
|
|
190
473
|
...(params.newRefname !== undefined ? { newRefname: params.newRefname } : {}),
|
|
191
474
|
...(params.dryRun !== undefined ? { dryRun: params.dryRun } : {}),
|
|
192
|
-
},
|
|
193
|
-
|
|
475
|
+
}, ctx);
|
|
476
|
+
// TASK-013 / FGAP-015: route through {read} so the embedded
|
|
477
|
+
// ResolvedRef.loc.item is bounded at the 50KB cap; over-cap fails closed
|
|
478
|
+
// with metadata. Under-cap text stays the same JSON (renderReadText
|
|
479
|
+
// under-cap returns JSON.stringify(serialized, null, 2) with no footer).
|
|
480
|
+
return { read: structureForRead(result, { whole: true, label: "promote-item result" }) };
|
|
194
481
|
},
|
|
195
482
|
},
|
|
196
483
|
{
|
|
@@ -198,6 +485,9 @@ export const ops = [
|
|
|
198
485
|
label: "Append Block Nested Item",
|
|
199
486
|
description: "Append an item to a nested array on a parent-array item in a project block. Schema validation is automatic.",
|
|
200
487
|
promptSnippet: "Append items to nested arrays inside parent items (e.g., findings inside a review)",
|
|
488
|
+
examples: [
|
|
489
|
+
`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`,
|
|
490
|
+
],
|
|
201
491
|
parameters: Type.Object({
|
|
202
492
|
block: Type.String({ description: "Block name (e.g., 'spec-reviews')" }),
|
|
203
493
|
arrayKey: Type.String({ description: "Parent array key (e.g., 'reviews')" }),
|
|
@@ -208,7 +498,7 @@ export const ops = [
|
|
|
208
498
|
item: Type.Unknown({ description: "Item object to append to the nested array — must conform to schema" }),
|
|
209
499
|
}),
|
|
210
500
|
surface: "use",
|
|
211
|
-
run(cwd, params) {
|
|
501
|
+
run(cwd, params, ctx) {
|
|
212
502
|
if (typeof params.item === "string") {
|
|
213
503
|
try {
|
|
214
504
|
params.item = JSON.parse(params.item);
|
|
@@ -219,7 +509,7 @@ export const ops = [
|
|
|
219
509
|
}
|
|
220
510
|
const matchEntries = Object.entries(params.match);
|
|
221
511
|
const predicate = (i) => matchEntries.every(([k, v]) => i[k] === v);
|
|
222
|
-
appendToNestedArray(cwd, params.block, params.arrayKey, predicate, params.nestedKey, params.item);
|
|
512
|
+
appendToNestedArray(cwd, params.block, params.arrayKey, predicate, params.nestedKey, params.item, ctx);
|
|
223
513
|
const matchDesc = matchEntries.map(([k, v]) => `${k}=${v}`).join(", ");
|
|
224
514
|
const id = params.item?.id ? ` '${params.item.id}'` : "";
|
|
225
515
|
return `Appended item${id} to ${params.block}.${params.arrayKey}[${matchDesc}].${params.nestedKey}`;
|
|
@@ -230,6 +520,9 @@ export const ops = [
|
|
|
230
520
|
label: "Update Block Nested Item",
|
|
231
521
|
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).",
|
|
232
522
|
promptSnippet: "Update items inside nested arrays — change finding state, mark resolved",
|
|
523
|
+
examples: [
|
|
524
|
+
`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`,
|
|
525
|
+
],
|
|
233
526
|
parameters: Type.Object({
|
|
234
527
|
block: Type.String({ description: "Block name (e.g., 'spec-reviews')" }),
|
|
235
528
|
arrayKey: Type.String({ description: "Parent array key (e.g., 'reviews')" }),
|
|
@@ -245,7 +538,7 @@ export const ops = [
|
|
|
245
538
|
}),
|
|
246
539
|
}),
|
|
247
540
|
surface: "use",
|
|
248
|
-
run(cwd, params) {
|
|
541
|
+
run(cwd, params, ctx) {
|
|
249
542
|
if (Object.keys(params.updates).length === 0) {
|
|
250
543
|
throw new Error("No fields to update — updates parameter is empty");
|
|
251
544
|
}
|
|
@@ -253,7 +546,7 @@ export const ops = [
|
|
|
253
546
|
const nestedEntries = Object.entries(params.nestedMatch);
|
|
254
547
|
const parentPred = (i) => parentEntries.every(([k, v]) => i[k] === v);
|
|
255
548
|
const nestedPred = (i) => nestedEntries.every(([k, v]) => i[k] === v);
|
|
256
|
-
updateNestedArrayItem(cwd, params.block, params.arrayKey, parentPred, params.nestedKey, nestedPred, params.updates);
|
|
549
|
+
updateNestedArrayItem(cwd, params.block, params.arrayKey, parentPred, params.nestedKey, nestedPred, params.updates, ctx);
|
|
257
550
|
const parentDesc = parentEntries.map(([k, v]) => `${k}=${v}`).join(", ");
|
|
258
551
|
const nestedDesc = nestedEntries.map(([k, v]) => `${k}=${v}`).join(", ");
|
|
259
552
|
return `Updated nested item (${nestedDesc}) in ${params.block}.${params.arrayKey}[${parentDesc}].${params.nestedKey}: ${Object.keys(params.updates).join(", ")}`;
|
|
@@ -264,16 +557,19 @@ export const ops = [
|
|
|
264
557
|
label: "Remove Block Item",
|
|
265
558
|
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.",
|
|
266
559
|
promptSnippet: "Remove items from project blocks — prune retracted issues, dedupe entries",
|
|
560
|
+
examples: [
|
|
561
|
+
`pi-context remove-block-item --block issues --arrayKey issues --match '{"id":"ISSUE-001"}' --writer '{"kind":"human","user":"you@example.com"}' --json`,
|
|
562
|
+
],
|
|
267
563
|
parameters: Type.Object({
|
|
268
564
|
block: Type.String({ description: "Block name (e.g., 'issues')" }),
|
|
269
565
|
arrayKey: Type.String({ description: "Top-level array key (e.g., 'issues')" }),
|
|
270
566
|
match: Type.Record(Type.String(), Type.Unknown(), { description: "Fields to match (e.g., { id: 'ISSUE-NNN' })" }),
|
|
271
567
|
}),
|
|
272
568
|
surface: "use",
|
|
273
|
-
run(cwd, params) {
|
|
569
|
+
run(cwd, params, ctx) {
|
|
274
570
|
const matchEntries = Object.entries(params.match);
|
|
275
571
|
const predicate = (i) => matchEntries.every(([k, v]) => i[k] === v);
|
|
276
|
-
const result = removeFromBlock(cwd, params.block, params.arrayKey, predicate);
|
|
572
|
+
const result = removeFromBlock(cwd, params.block, params.arrayKey, predicate, ctx);
|
|
277
573
|
const matchDesc = matchEntries.map(([k, v]) => `${k}=${v}`).join(", ");
|
|
278
574
|
return `Removed ${result.removed} item(s) matching (${matchDesc}) from ${params.block}.${params.arrayKey}`;
|
|
279
575
|
},
|
|
@@ -283,6 +579,9 @@ export const ops = [
|
|
|
283
579
|
label: "Remove Block Nested Item",
|
|
284
580
|
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.",
|
|
285
581
|
promptSnippet: "Remove nested items — drop rejected findings, retract nested references",
|
|
582
|
+
examples: [
|
|
583
|
+
`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`,
|
|
584
|
+
],
|
|
286
585
|
parameters: Type.Object({
|
|
287
586
|
block: Type.String({ description: "Block name (e.g., 'spec-reviews')" }),
|
|
288
587
|
arrayKey: Type.String({ description: "Parent array key (e.g., 'reviews')" }),
|
|
@@ -295,12 +594,12 @@ export const ops = [
|
|
|
295
594
|
}),
|
|
296
595
|
}),
|
|
297
596
|
surface: "use",
|
|
298
|
-
run(cwd, params) {
|
|
597
|
+
run(cwd, params, ctx) {
|
|
299
598
|
const parentEntries = Object.entries(params.match);
|
|
300
599
|
const nestedEntries = Object.entries(params.nestedMatch);
|
|
301
600
|
const parentPred = (i) => parentEntries.every(([k, v]) => i[k] === v);
|
|
302
601
|
const nestedPred = (i) => nestedEntries.every(([k, v]) => i[k] === v);
|
|
303
|
-
const result = removeFromNestedArray(cwd, params.block, params.arrayKey, parentPred, params.nestedKey, nestedPred);
|
|
602
|
+
const result = removeFromNestedArray(cwd, params.block, params.arrayKey, parentPred, params.nestedKey, nestedPred, ctx);
|
|
304
603
|
const parentDesc = parentEntries.map(([k, v]) => `${k}=${v}`).join(", ");
|
|
305
604
|
const nestedDesc = nestedEntries.map(([k, v]) => `${k}=${v}`).join(", ");
|
|
306
605
|
return `Removed ${result.removed} nested item(s) matching (${nestedDesc}) from ${params.block}.${params.arrayKey}[${parentDesc}].${params.nestedKey}`;
|
|
@@ -311,14 +610,15 @@ export const ops = [
|
|
|
311
610
|
label: "Read Block Dir",
|
|
312
611
|
description: "Enumerate and parse all .json files in a <substrate-dir>/<subdir>/ directory, returned as a sorted array. Missing directories return [].",
|
|
313
612
|
promptSnippet: "Enumerate project block subdirectories (phases, schemas, etc.) as parsed JSON",
|
|
613
|
+
examples: [`pi-context read-block-dir --subdir phases --json`],
|
|
314
614
|
parameters: Type.Object({
|
|
315
615
|
subdir: Type.String({ description: "Subdirectory under the substrate dir (e.g., 'phases', 'schemas')" }),
|
|
316
616
|
}),
|
|
317
617
|
surface: "use",
|
|
318
618
|
run(cwd, params) {
|
|
319
619
|
const result = readBlockDir(cwd, params.subdir);
|
|
320
|
-
const
|
|
321
|
-
return
|
|
620
|
+
const read = structureForRead(result, { label: `<substrate-dir>/${params.subdir}/` });
|
|
621
|
+
return { read };
|
|
322
622
|
},
|
|
323
623
|
},
|
|
324
624
|
{
|
|
@@ -326,13 +626,14 @@ export const ops = [
|
|
|
326
626
|
label: "Read Block",
|
|
327
627
|
description: "Read a project block file as structured JSON.",
|
|
328
628
|
promptSnippet: "Read a project block as structured JSON",
|
|
629
|
+
examples: [`pi-context read-block --block tasks --json`],
|
|
329
630
|
parameters: Type.Object({
|
|
330
631
|
block: Type.String({ description: "Block name (e.g., 'issues', 'tasks', 'requirements')" }),
|
|
331
632
|
}),
|
|
332
633
|
surface: "use",
|
|
333
634
|
run(cwd, params) {
|
|
334
635
|
const result = readBlock(cwd, params.block);
|
|
335
|
-
const
|
|
636
|
+
const read = structureForRead(result, {
|
|
336
637
|
label: `<substrate-dir>/${params.block}.json`,
|
|
337
638
|
overCapDirective: {
|
|
338
639
|
tool: "read-block-page",
|
|
@@ -340,7 +641,7 @@ export const ops = [
|
|
|
340
641
|
hint: "or read-block-item with id=<id>",
|
|
341
642
|
},
|
|
342
643
|
});
|
|
343
|
-
return
|
|
644
|
+
return { read };
|
|
344
645
|
},
|
|
345
646
|
},
|
|
346
647
|
{
|
|
@@ -348,15 +649,18 @@ export const ops = [
|
|
|
348
649
|
label: "Write Block",
|
|
349
650
|
description: "Write or replace an entire project block with schema validation.",
|
|
350
651
|
promptSnippet: "Write or replace a project block with schema validation",
|
|
652
|
+
examples: [
|
|
653
|
+
`pi-context write-block --block architecture --data @/tmp/architecture.json --writer '{"kind":"human","user":"you@example.com"}' --json`,
|
|
654
|
+
],
|
|
351
655
|
parameters: Type.Object({
|
|
352
656
|
block: Type.String({ description: "Block name (e.g., 'project', 'architecture')" }),
|
|
353
657
|
data: Type.Unknown({ description: "Complete block data — must conform to block schema" }),
|
|
354
658
|
}),
|
|
355
659
|
surface: "use",
|
|
356
660
|
authGated: true,
|
|
357
|
-
run(cwd, params) {
|
|
661
|
+
run(cwd, params, ctx) {
|
|
358
662
|
const data = typeof params.data === "string" ? JSON.parse(params.data) : params.data;
|
|
359
|
-
writeBlock(cwd, params.block, data);
|
|
663
|
+
writeBlock(cwd, params.block, data, ctx);
|
|
360
664
|
return `Wrote block '${params.block}' successfully`;
|
|
361
665
|
},
|
|
362
666
|
},
|
|
@@ -365,11 +669,24 @@ export const ops = [
|
|
|
365
669
|
label: "Context Status",
|
|
366
670
|
description: "Get derived context state — source metrics, block summaries, planning lifecycle status.",
|
|
367
671
|
promptSnippet: "Get context state — source metrics, block summaries, planning lifecycle status",
|
|
672
|
+
examples: [`pi-context context-status --json`],
|
|
368
673
|
parameters: Type.Object({}),
|
|
369
674
|
surface: "use",
|
|
370
675
|
run(cwd, _params) {
|
|
371
676
|
const result = contextState(cwd);
|
|
372
|
-
return
|
|
677
|
+
return { json: result };
|
|
678
|
+
},
|
|
679
|
+
},
|
|
680
|
+
{
|
|
681
|
+
name: "context-check-status",
|
|
682
|
+
label: "Context Check Status",
|
|
683
|
+
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; writes nothing.",
|
|
684
|
+
promptSnippet: "Report installed-vs-catalog schema drift + the version gap for behind schemas (read-only)",
|
|
685
|
+
examples: [`pi-context context-check-status --json`],
|
|
686
|
+
parameters: Type.Object({}),
|
|
687
|
+
surface: "use",
|
|
688
|
+
run(cwd, _params) {
|
|
689
|
+
return { json: checkStatus(cwd) };
|
|
373
690
|
},
|
|
374
691
|
},
|
|
375
692
|
{
|
|
@@ -377,11 +694,12 @@ export const ops = [
|
|
|
377
694
|
label: "Context Validate",
|
|
378
695
|
description: "Validate cross-block referential integrity — check that IDs referenced across blocks exist.",
|
|
379
696
|
promptSnippet: "Validate cross-block referential integrity",
|
|
697
|
+
examples: [`pi-context context-validate --json`],
|
|
380
698
|
parameters: Type.Object({}),
|
|
381
699
|
surface: "use",
|
|
382
700
|
run(cwd, _params) {
|
|
383
701
|
const result = validateContext(cwd);
|
|
384
|
-
return
|
|
702
|
+
return { json: result };
|
|
385
703
|
},
|
|
386
704
|
},
|
|
387
705
|
{
|
|
@@ -389,6 +707,7 @@ export const ops = [
|
|
|
389
707
|
label: "Read Config",
|
|
390
708
|
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.",
|
|
391
709
|
promptSnippet: "Read project config — vocabulary, lenses, relation_types, status_buckets",
|
|
710
|
+
examples: [`pi-context read-config --registry block_kinds --id tasks --json`],
|
|
392
711
|
parameters: Type.Object({
|
|
393
712
|
registry: Type.Optional(Type.String({
|
|
394
713
|
description: "Address ONE config registry/map by key (e.g. 'relation_types', 'lenses', 'block_kinds', 'status_buckets')",
|
|
@@ -410,10 +729,14 @@ export const ops = [
|
|
|
410
729
|
if (!entry.found) {
|
|
411
730
|
return `read-config: entry not found in ${params.registry} — ${entry.resolved}`;
|
|
412
731
|
}
|
|
413
|
-
const
|
|
414
|
-
|
|
732
|
+
const read = structureForRead(entry.value, {
|
|
733
|
+
whole: true,
|
|
734
|
+
label: `config.${params.registry}.${params.id}`,
|
|
735
|
+
});
|
|
736
|
+
return { read };
|
|
415
737
|
}
|
|
416
|
-
const
|
|
738
|
+
const read = structureForRead(reg.value, {
|
|
739
|
+
whole: true,
|
|
417
740
|
label: `config.${params.registry}`,
|
|
418
741
|
overCapDirective: {
|
|
419
742
|
tool: "read-config",
|
|
@@ -421,17 +744,17 @@ export const ops = [
|
|
|
421
744
|
hint: "add id=<entry canonical_id>",
|
|
422
745
|
},
|
|
423
746
|
});
|
|
424
|
-
return
|
|
747
|
+
return { read };
|
|
425
748
|
}
|
|
426
749
|
const result = { config, configPath };
|
|
427
|
-
const
|
|
750
|
+
const read = structureForRead(result, {
|
|
428
751
|
label: configPath ?? "config.json",
|
|
429
752
|
overCapDirective: {
|
|
430
753
|
tool: "read-config",
|
|
431
754
|
hint: "registry=<name> (block_kinds|relation_types|lenses|invariants|…)",
|
|
432
755
|
},
|
|
433
756
|
});
|
|
434
|
-
return
|
|
757
|
+
return { read };
|
|
435
758
|
},
|
|
436
759
|
},
|
|
437
760
|
{
|
|
@@ -467,8 +790,8 @@ export const ops = [
|
|
|
467
790
|
if (tool === undefined) {
|
|
468
791
|
return `list-tools: tool not found — name=${params.name}`;
|
|
469
792
|
}
|
|
470
|
-
const
|
|
471
|
-
return
|
|
793
|
+
const read = structureForRead(tool, { whole: true, label: `tool ${params.name}` });
|
|
794
|
+
return { read };
|
|
472
795
|
}
|
|
473
796
|
// Default: compact index (FGAP-101) — name + param count + one-line description.
|
|
474
797
|
const index = all.map((t) => {
|
|
@@ -482,11 +805,11 @@ export const ops = [
|
|
|
482
805
|
// The compact index is one line per tool — small enough to serialize whole
|
|
483
806
|
// (no paging); keep the wrapper fields (active/total) on the result object.
|
|
484
807
|
const result = { tools: index, active, total: all.length, activeCount: active.length };
|
|
485
|
-
const
|
|
808
|
+
const read = structureForRead(result, {
|
|
486
809
|
label: "tool index — pass name= for detail",
|
|
487
810
|
overCapDirective: { tool: "list-tools", hint: "name=<tool>" },
|
|
488
811
|
});
|
|
489
|
-
return
|
|
812
|
+
return { read };
|
|
490
813
|
},
|
|
491
814
|
},
|
|
492
815
|
{
|
|
@@ -494,6 +817,7 @@ export const ops = [
|
|
|
494
817
|
label: "Read Samples Catalog",
|
|
495
818
|
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.",
|
|
496
819
|
promptSnippet: "Discover installable sample block kinds — title, shape, relation_types, invariants, lenses",
|
|
820
|
+
examples: [`pi-context read-samples-catalog --kind tasks --json`],
|
|
497
821
|
parameters: Type.Object({
|
|
498
822
|
kind: Type.Optional(Type.String({ description: "Filter to one block_kind canonical_id (e.g. 'tasks')" })),
|
|
499
823
|
}),
|
|
@@ -502,13 +826,43 @@ export const ops = [
|
|
|
502
826
|
// Package-intrinsic: the catalog reads the extension's bundled samples
|
|
503
827
|
// directory, not the project substrate — cwd is unused.
|
|
504
828
|
const catalog = samplesCatalog(params.kind ? { kind: params.kind } : undefined);
|
|
505
|
-
const
|
|
829
|
+
const read = structureForRead(catalog, {
|
|
506
830
|
label: params.kind ? `samples kind=${params.kind}` : "samples catalog",
|
|
507
831
|
// Whole catalog → narrow by kind; a single kind has no finer
|
|
508
|
-
// addressing (edge → head-leading marker, no directive).
|
|
509
|
-
|
|
832
|
+
// addressing (edge → head-leading marker, no directive). The
|
|
833
|
+
// single-kind read is an addressed node — return it whole (capped),
|
|
834
|
+
// never a 50-item page of an incidental array child.
|
|
835
|
+
...(params.kind
|
|
836
|
+
? { whole: true }
|
|
837
|
+
: { overCapDirective: { tool: "read-samples-catalog", hint: "kind=<canonical_id>" } }),
|
|
510
838
|
});
|
|
511
|
-
return
|
|
839
|
+
return { read };
|
|
840
|
+
},
|
|
841
|
+
},
|
|
842
|
+
{
|
|
843
|
+
name: "read-catalog-schema",
|
|
844
|
+
label: "Read Catalog Schema",
|
|
845
|
+
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.",
|
|
846
|
+
promptSnippet: "Fetch and print the verbatim catalog schema body for a named block kind (raw JSON Schema, diffable locally)",
|
|
847
|
+
examples: ["pi-context read-catalog-schema --kind tasks", "pi-context read-catalog-schema --kind tasks --json"],
|
|
848
|
+
parameters: Type.Object({
|
|
849
|
+
kind: Type.String({ description: "Catalog block_kind canonical_id (e.g. 'tasks')" }),
|
|
850
|
+
}),
|
|
851
|
+
surface: "use",
|
|
852
|
+
// The catalog schema file carries its own trailing newline (`}\n`); emit the file
|
|
853
|
+
// bytes exactly — preserving that single newline, appending none — so
|
|
854
|
+
// `read-catalog-schema --kind <k> | diff <installed> -` shows no phantom line when
|
|
855
|
+
// content matches (the pre-flag defect was the print path appending a second
|
|
856
|
+
// newline, doubling it to `}\n\n`).
|
|
857
|
+
verbatimText: true,
|
|
858
|
+
run(_cwd, params) {
|
|
859
|
+
// Package-intrinsic: reads the extension's bundled catalog schema file,
|
|
860
|
+
// not the project substrate — cwd is unused (like read-samples-catalog).
|
|
861
|
+
// Returns the RAW TEXT bytes as a prose-string OpResult so the verbatim
|
|
862
|
+
// catalog body prints as-is (renderOpResultText) and rides the --json
|
|
863
|
+
// envelope as a string; no {json}/{read} wrap that would re-serialize and
|
|
864
|
+
// alter the bytes the operator diffs.
|
|
865
|
+
return readCatalogSchemaText(params.kind).text;
|
|
512
866
|
},
|
|
513
867
|
},
|
|
514
868
|
{
|
|
@@ -516,11 +870,12 @@ export const ops = [
|
|
|
516
870
|
label: "Context Current State",
|
|
517
871
|
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.",
|
|
518
872
|
promptSnippet: "Derive current project state — focus, in-flight, next actions, blocked",
|
|
873
|
+
examples: [`pi-context context-current-state --json`],
|
|
519
874
|
parameters: Type.Object({}),
|
|
520
875
|
surface: "use",
|
|
521
876
|
run(cwd, _params) {
|
|
522
877
|
const state = currentState(cwd);
|
|
523
|
-
return
|
|
878
|
+
return { json: state };
|
|
524
879
|
},
|
|
525
880
|
},
|
|
526
881
|
{
|
|
@@ -528,11 +883,12 @@ export const ops = [
|
|
|
528
883
|
label: "Context Bootstrap State",
|
|
529
884
|
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.",
|
|
530
885
|
promptSnippet: "Derive substrate bootstrap state — no-pointer | no-config | skeleton | not-installed | ready (never throws pre-bootstrap)",
|
|
886
|
+
examples: [`pi-context context-bootstrap-state --json`],
|
|
531
887
|
parameters: Type.Object({}),
|
|
532
888
|
surface: "use",
|
|
533
889
|
run(cwd, _params) {
|
|
534
890
|
const status = deriveBootstrapState(cwd);
|
|
535
|
-
return
|
|
891
|
+
return { json: status };
|
|
536
892
|
},
|
|
537
893
|
},
|
|
538
894
|
{
|
|
@@ -540,6 +896,7 @@ export const ops = [
|
|
|
540
896
|
label: "Rename Canonical Id",
|
|
541
897
|
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.",
|
|
542
898
|
promptSnippet: "Rename a canonical_id (item/relation_type/lens/layer) across substrate; dryRun to preview",
|
|
899
|
+
examples: [`pi-context rename-canonical-id --kind item --oldId TASK-001 --newId TASK-100 --dryRun true --json`],
|
|
543
900
|
parameters: Type.Object({
|
|
544
901
|
kind: Type.String({ description: "One of: item | relation_type | lens | layer" }),
|
|
545
902
|
oldId: Type.String({ description: "Current canonical_id to rename from" }),
|
|
@@ -550,7 +907,7 @@ export const ops = [
|
|
|
550
907
|
authGated: true,
|
|
551
908
|
run(cwd, params) {
|
|
552
909
|
const report = renameCanonicalId(cwd, params.kind, params.oldId, params.newId, { dryRun: params.dryRun });
|
|
553
|
-
return
|
|
910
|
+
return { json: report };
|
|
554
911
|
},
|
|
555
912
|
},
|
|
556
913
|
{
|
|
@@ -563,6 +920,9 @@ export const ops = [
|
|
|
563
920
|
"relation_type / lens / layer / block_kind) is NOT checked here — run context-validate after. dryRun previews " +
|
|
564
921
|
"without writing.",
|
|
565
922
|
promptSnippet: "Add/replace/remove one entry in a config.json registry (vocabulary, lenses, invariants, status_buckets)",
|
|
923
|
+
examples: [
|
|
924
|
+
`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`,
|
|
925
|
+
],
|
|
566
926
|
parameters: Type.Object({
|
|
567
927
|
registry: Type.String({
|
|
568
928
|
description: "One of: block_kinds | relation_types | lenses | layers | invariants | status_buckets | display_strings | naming | installed_schemas | installed_blocks | hierarchy",
|
|
@@ -580,7 +940,7 @@ export const ops = [
|
|
|
580
940
|
}),
|
|
581
941
|
surface: "use",
|
|
582
942
|
authGated: true,
|
|
583
|
-
run(cwd, params) {
|
|
943
|
+
run(cwd, params, ctx) {
|
|
584
944
|
// Type.Unknown() params may arrive as JSON strings. Parse if possible; on
|
|
585
945
|
// failure KEEP the raw string (valid for map-value registries whose value
|
|
586
946
|
// is a bare string, e.g. naming/display_strings/status_buckets).
|
|
@@ -593,10 +953,11 @@ export const ops = [
|
|
|
593
953
|
/* keep raw string — valid for map-value registries */
|
|
594
954
|
}
|
|
595
955
|
}
|
|
596
|
-
const result = amendConfigEntry(cwd, params.registry, params.operation, params.key, entry,
|
|
956
|
+
const result = amendConfigEntry(cwd, params.registry, params.operation, params.key, entry, ctx, {
|
|
597
957
|
dryRun: params.dryRun,
|
|
598
958
|
});
|
|
599
|
-
const
|
|
959
|
+
const pastTense = result.operation === "add" ? "added" : `${result.operation}d`;
|
|
960
|
+
const verb = result.modified ? (params.dryRun ? `would ${result.operation}` : pastTense) : "no-op";
|
|
600
961
|
return `amend-config: ${verb} ${result.registry}[${result.key}]`;
|
|
601
962
|
},
|
|
602
963
|
},
|
|
@@ -605,6 +966,7 @@ export const ops = [
|
|
|
605
966
|
label: "Read Schema",
|
|
606
967
|
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.",
|
|
607
968
|
promptSnippet: "Read a block schema as structured JSON — optionally address one property via `path`",
|
|
969
|
+
examples: [`pi-context read-schema --schemaName framework-gaps --path properties.gaps.items.required --json`],
|
|
608
970
|
parameters: Type.Object({
|
|
609
971
|
schemaName: Type.String({
|
|
610
972
|
description: "Schema name without extension (e.g., 'tasks', 'decisions', 'issues')",
|
|
@@ -622,11 +984,11 @@ export const ops = [
|
|
|
622
984
|
if (!addr.found) {
|
|
623
985
|
return `read-schema: property not found — ${addr.resolved}`;
|
|
624
986
|
}
|
|
625
|
-
const
|
|
626
|
-
return
|
|
987
|
+
const read = structureForRead(addr.value, { whole: true, label: `${params.schemaName} ${addr.resolved}` });
|
|
988
|
+
return { read };
|
|
627
989
|
}
|
|
628
990
|
const result = { schema, schemaPath: schemaPathStr };
|
|
629
|
-
const
|
|
991
|
+
const read = structureForRead(result, {
|
|
630
992
|
label: schemaPathStr,
|
|
631
993
|
overCapDirective: {
|
|
632
994
|
tool: "read-schema",
|
|
@@ -634,7 +996,7 @@ export const ops = [
|
|
|
634
996
|
hint: "path=<dotted json-path>",
|
|
635
997
|
},
|
|
636
998
|
});
|
|
637
|
-
return
|
|
999
|
+
return { read };
|
|
638
1000
|
},
|
|
639
1001
|
},
|
|
640
1002
|
{
|
|
@@ -646,6 +1008,9 @@ export const ops = [
|
|
|
646
1008
|
"read/write of items declaring an older schema_version throws version-mismatch. Registering the block_kind " +
|
|
647
1009
|
"that points at this schema is a separate step (amend-config block_kinds).",
|
|
648
1010
|
promptSnippet: "Create or replace a block-kind JSON Schema (meta-validated, atomic)",
|
|
1011
|
+
examples: [
|
|
1012
|
+
`pi-context write-schema --operation create --schemaName tasks --schema @/tmp/tasks.schema.json --writer '{"kind":"human","user":"you@example.com"}' --json`,
|
|
1013
|
+
],
|
|
649
1014
|
parameters: Type.Object({
|
|
650
1015
|
operation: Type.String({ description: "create | replace" }),
|
|
651
1016
|
schemaName: Type.String({ description: "Schema name without extension (e.g., 'tasks')" }),
|
|
@@ -656,7 +1021,7 @@ export const ops = [
|
|
|
656
1021
|
}),
|
|
657
1022
|
surface: "use",
|
|
658
1023
|
authGated: true,
|
|
659
|
-
run(cwd, params) {
|
|
1024
|
+
run(cwd, params, ctx) {
|
|
660
1025
|
// Type.Unknown() params may arrive as JSON strings. Parse if possible; on
|
|
661
1026
|
// failure KEEP the raw value (meta-validation rejects a non-object body).
|
|
662
1027
|
let schema = params.schema;
|
|
@@ -668,16 +1033,57 @@ export const ops = [
|
|
|
668
1033
|
/* keep raw string — meta-validation will reject a non-object */
|
|
669
1034
|
}
|
|
670
1035
|
}
|
|
671
|
-
const result = writeSchemaChecked(cwd, params.schemaName, schema, params.operation,
|
|
1036
|
+
const result = writeSchemaChecked(cwd, params.schemaName, schema, params.operation, ctx, { dryRun: params.dryRun });
|
|
672
1037
|
const verb = result.written ? `${result.operation}d` : `would ${result.operation}`;
|
|
673
1038
|
return `write-schema: ${verb} schema '${params.schemaName}' at ${result.schemaPath}`;
|
|
674
1039
|
},
|
|
675
1040
|
},
|
|
1041
|
+
{
|
|
1042
|
+
name: "resolve-conflict",
|
|
1043
|
+
label: "Resolve Schema Conflict",
|
|
1044
|
+
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.",
|
|
1045
|
+
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)",
|
|
1046
|
+
examples: [
|
|
1047
|
+
`pi-context resolve-conflict --schemaName tasks --schema @/tmp/tasks.reconciled.json --writer '{"kind":"human","user":"you@example.com"}' --json`,
|
|
1048
|
+
],
|
|
1049
|
+
parameters: Type.Object({
|
|
1050
|
+
schemaName: Type.String({ description: "Schema name without extension (e.g., 'tasks')" }),
|
|
1051
|
+
schema: Type.Optional(Type.Unknown({
|
|
1052
|
+
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.",
|
|
1053
|
+
})),
|
|
1054
|
+
}),
|
|
1055
|
+
surface: "use",
|
|
1056
|
+
authGated: true,
|
|
1057
|
+
run(cwd, params, ctx) {
|
|
1058
|
+
const result = resolveConflict(cwd, params.schemaName, params.schema, ctx);
|
|
1059
|
+
return { json: result };
|
|
1060
|
+
},
|
|
1061
|
+
},
|
|
1062
|
+
{
|
|
1063
|
+
name: "resolve-blocked",
|
|
1064
|
+
label: "Resolve Blocked",
|
|
1065
|
+
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.",
|
|
1066
|
+
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)",
|
|
1067
|
+
examples: [`pi-context resolve-blocked --schemaName tasks --yes --json`],
|
|
1068
|
+
parameters: Type.Object({
|
|
1069
|
+
schemaName: Type.String({
|
|
1070
|
+
description: "Schema name with a pending-blocked entry (from update's blocked report)",
|
|
1071
|
+
}),
|
|
1072
|
+
}),
|
|
1073
|
+
surface: "use",
|
|
1074
|
+
authGated: true,
|
|
1075
|
+
run(cwd, params, ctx) {
|
|
1076
|
+
return { json: resolveBlocked(cwd, params.schemaName, ctx) };
|
|
1077
|
+
},
|
|
1078
|
+
},
|
|
676
1079
|
{
|
|
677
1080
|
name: "write-schema-migration",
|
|
678
1081
|
label: "Write Schema Migration",
|
|
679
1082
|
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.",
|
|
680
1083
|
promptSnippet: "Declare a schema version-bump migration (identity or declarative-transform) into migrations.json",
|
|
1084
|
+
examples: [
|
|
1085
|
+
`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`,
|
|
1086
|
+
],
|
|
681
1087
|
parameters: Type.Object({
|
|
682
1088
|
operation: Type.String({ description: "create | replace | remove" }),
|
|
683
1089
|
schemaName: Type.String({ description: "Schema name without extension (e.g., 'tasks')." }),
|
|
@@ -698,8 +1104,8 @@ export const ops = [
|
|
|
698
1104
|
}),
|
|
699
1105
|
surface: "use",
|
|
700
1106
|
authGated: true,
|
|
701
|
-
async run(cwd, params) {
|
|
702
|
-
const result = await writeSchemaMigrationExecute(cwd, params);
|
|
1107
|
+
async run(cwd, params, ctx) {
|
|
1108
|
+
const result = await writeSchemaMigrationExecute(cwd, params, ctx);
|
|
703
1109
|
// writeSchemaMigrationExecute returns the uniform AgentToolResult; the
|
|
704
1110
|
// op contract is the text payload, which registerAll re-wraps identically.
|
|
705
1111
|
const part = result.content[0];
|
|
@@ -711,6 +1117,7 @@ export const ops = [
|
|
|
711
1117
|
label: "Context Init",
|
|
712
1118
|
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).",
|
|
713
1119
|
promptSnippet: "Initialize the substrate dir (bootstrap pointer + dirs + skeleton config; onward via accept-all OR amend-config/edit)",
|
|
1120
|
+
examples: [`pi-context context-init --contextDir .context --json`],
|
|
714
1121
|
parameters: Type.Object({
|
|
715
1122
|
contextDir: Type.String({
|
|
716
1123
|
description: "Substrate dir name (e.g. .context). Required — no default.",
|
|
@@ -720,7 +1127,7 @@ export const ops = [
|
|
|
720
1127
|
authGated: true,
|
|
721
1128
|
run(cwd, params) {
|
|
722
1129
|
const result = initProject(cwd, params.contextDir);
|
|
723
|
-
return
|
|
1130
|
+
return { json: result };
|
|
724
1131
|
},
|
|
725
1132
|
},
|
|
726
1133
|
{
|
|
@@ -728,6 +1135,7 @@ export const ops = [
|
|
|
728
1135
|
label: "Accept-All Conception",
|
|
729
1136
|
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.",
|
|
730
1137
|
promptSnippet: "Adopt the canonical conception as config (accept-all)",
|
|
1138
|
+
examples: [`pi-context context-accept-all --json`],
|
|
731
1139
|
parameters: Type.Object({}),
|
|
732
1140
|
surface: "use",
|
|
733
1141
|
authGated: true,
|
|
@@ -742,7 +1150,58 @@ export const ops = [
|
|
|
742
1150
|
}
|
|
743
1151
|
throw err;
|
|
744
1152
|
}
|
|
745
|
-
return
|
|
1153
|
+
return { json: result };
|
|
1154
|
+
},
|
|
1155
|
+
},
|
|
1156
|
+
{
|
|
1157
|
+
name: "context-install",
|
|
1158
|
+
label: "Context Install",
|
|
1159
|
+
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.",
|
|
1160
|
+
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)",
|
|
1161
|
+
examples: ["pi-context context-install --json", "pi-context context-install --update true --json"],
|
|
1162
|
+
parameters: Type.Object({
|
|
1163
|
+
update: Type.Optional(Type.Boolean({
|
|
1164
|
+
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.",
|
|
1165
|
+
})),
|
|
1166
|
+
}),
|
|
1167
|
+
surface: "use",
|
|
1168
|
+
authGated: true,
|
|
1169
|
+
run(cwd, params) {
|
|
1170
|
+
const result = installContext(cwd, { overwrite: params.update === true });
|
|
1171
|
+
if (result.error)
|
|
1172
|
+
return result.error;
|
|
1173
|
+
return { json: result };
|
|
1174
|
+
},
|
|
1175
|
+
},
|
|
1176
|
+
{
|
|
1177
|
+
name: "update",
|
|
1178
|
+
label: "Update Installed Model",
|
|
1179
|
+
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) — the installed -> catalog version pair, and for a validation failure the per-item failures naming the failing item id, field, and constraint. A live blocked resync also persists a pending-blocked record (pinning the target catalog schema + the chain reaching it) consumable by resolve-blocked, which commits the resolution once the block's items are fixed. Pass dryRun to preview the per-schema action plan; dryRun predicts the precise per-schema catalog-ahead outcome (resync / migrate / block / merge / conflict) by running the forward-migration + re-validation in memory, the per-blocked-schema diagnostic detail, the config-registry entries that would be added, AND the migration declarations that would be registered, writing nothing. When a catalog-ahead resync is blocked because the block's items fail the catalog schema (validation-failed), update inscribes git-style failure markers INTO the block file at the offending items (full-line `<<<<<<< BLOCKED …` / `>>>>>>> target: …` sentinels), pinning the pre-marker bytes so resolve-blocked can strip the markers and re-validate; the schema and migrations.json stay byte-unchanged. A dryRun preview writes no markers.",
|
|
1180
|
+
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, version pair, per-item failures — and persists a pending-blocked record (target catalog schema + the chain reaching it) resolved via resolve-blocked once the block's items are fixed; a validation-failed block is marked in place with git-style failure markers (recoverable; stripped + re-validated by resolve-blocked); --dry-run predicts the precise per-schema outcome — resync / migrate / block / merge / conflict — via in-memory forward-migration + re-validation, writing nothing)",
|
|
1181
|
+
examples: [`pi-context update --dryRun true --json`],
|
|
1182
|
+
parameters: Type.Object({
|
|
1183
|
+
dryRun: Type.Optional(Type.Boolean({ description: "Preview the per-schema action plan without writing anything." })),
|
|
1184
|
+
}),
|
|
1185
|
+
surface: "use",
|
|
1186
|
+
run(cwd, params) {
|
|
1187
|
+
const result = updateContext(cwd, { dryRun: params.dryRun === true });
|
|
1188
|
+
if (result.error)
|
|
1189
|
+
return result.error;
|
|
1190
|
+
return { json: result };
|
|
1191
|
+
},
|
|
1192
|
+
},
|
|
1193
|
+
{
|
|
1194
|
+
name: "validate-block-items",
|
|
1195
|
+
label: "Validate Block Items",
|
|
1196
|
+
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.",
|
|
1197
|
+
promptSnippet: "Validate a block's items against the catalog schema version — returns the per-item failures (item id, field, constraint) without writing",
|
|
1198
|
+
examples: [`pi-context validate-block-items --block tasks --json`],
|
|
1199
|
+
parameters: Type.Object({
|
|
1200
|
+
block: Type.String({ description: "Block name (e.g. 'tasks')" }),
|
|
1201
|
+
}),
|
|
1202
|
+
surface: "use",
|
|
1203
|
+
run(cwd, params) {
|
|
1204
|
+
return { json: validateBlockItemsAgainstCatalog(cwd, params.block) };
|
|
746
1205
|
},
|
|
747
1206
|
},
|
|
748
1207
|
{
|
|
@@ -750,6 +1209,9 @@ export const ops = [
|
|
|
750
1209
|
label: "Context Switch",
|
|
751
1210
|
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).",
|
|
752
1211
|
promptSnippet: "Switch the bootstrap pointer to a different substrate dir",
|
|
1212
|
+
examples: [
|
|
1213
|
+
`pi-context context-switch --target_dir .context --writer '{"kind":"human","user":"you@example.com"}' --json`,
|
|
1214
|
+
],
|
|
753
1215
|
parameters: Type.Object({
|
|
754
1216
|
target_dir: Type.String({
|
|
755
1217
|
description: "Substrate dir name to switch to (e.g. '.context'). Required for default + create_new modes; ignored for to_previous mode.",
|
|
@@ -784,14 +1246,14 @@ export const ops = [
|
|
|
784
1246
|
try {
|
|
785
1247
|
if (params.to_previous === true) {
|
|
786
1248
|
const { from, to } = switchToPrevious(cwd, writerIdentity);
|
|
787
|
-
return
|
|
1249
|
+
return { json: { mode: "to_previous", from, to } };
|
|
788
1250
|
}
|
|
789
1251
|
if (params.create_new === true) {
|
|
790
1252
|
const { created } = switchAndCreate(cwd, params.target_dir, writerIdentity);
|
|
791
|
-
return
|
|
1253
|
+
return { json: { mode: "create_new", target_dir: params.target_dir, created } };
|
|
792
1254
|
}
|
|
793
1255
|
switchToExisting(cwd, params.target_dir, writerIdentity);
|
|
794
|
-
return
|
|
1256
|
+
return { json: { mode: "existing", target_dir: params.target_dir } };
|
|
795
1257
|
}
|
|
796
1258
|
catch (err) {
|
|
797
1259
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -804,11 +1266,12 @@ export const ops = [
|
|
|
804
1266
|
label: "Context List",
|
|
805
1267
|
description: "Enumerate top-level dirs under cwd containing a config.json (switchable substrates). Marks the active one with isActive=true. Read-only.",
|
|
806
1268
|
promptSnippet: "List switchable substrate dirs under cwd",
|
|
1269
|
+
examples: [`pi-context context-list --json`],
|
|
807
1270
|
parameters: Type.Object({}),
|
|
808
1271
|
surface: "use",
|
|
809
1272
|
run(cwd, _params) {
|
|
810
1273
|
const subs = listSubstrates(cwd);
|
|
811
|
-
return
|
|
1274
|
+
return { json: subs };
|
|
812
1275
|
},
|
|
813
1276
|
},
|
|
814
1277
|
{
|
|
@@ -816,25 +1279,20 @@ export const ops = [
|
|
|
816
1279
|
label: "Context Archive",
|
|
817
1280
|
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>/.",
|
|
818
1281
|
promptSnippet: "Archive a non-active substrate dir to archive/<dir>/",
|
|
1282
|
+
examples: [
|
|
1283
|
+
`pi-context context-archive --target_dir .context-old --writer '{"kind":"human","user":"you@example.com"}' --json`,
|
|
1284
|
+
],
|
|
819
1285
|
parameters: Type.Object({
|
|
820
1286
|
target_dir: Type.String({
|
|
821
1287
|
description: "Substrate dir name to archive (e.g. '.project'). Refused if it is the active substrate.",
|
|
822
1288
|
}),
|
|
823
|
-
writer: Type.Optional(Type.Object({
|
|
824
|
-
kind: Type.String({
|
|
825
|
-
description: "Writer kind discriminator — overwritten by auth-gate to 'human' on confirm.",
|
|
826
|
-
}),
|
|
827
|
-
user: Type.String({
|
|
828
|
-
description: "Writer user — overwritten by auth-gate to the verified terminal-operator identity on confirm.",
|
|
829
|
-
}),
|
|
830
|
-
}, { description: "DispatchContext.writer — stamped by auth-gate on operator confirm." })),
|
|
831
1289
|
}),
|
|
832
1290
|
surface: "use",
|
|
833
1291
|
authGated: true,
|
|
834
1292
|
run(cwd, params) {
|
|
835
1293
|
try {
|
|
836
1294
|
const { from, to } = archiveSubstrate(cwd, params.target_dir);
|
|
837
|
-
return
|
|
1295
|
+
return { json: { from, to } };
|
|
838
1296
|
}
|
|
839
1297
|
catch (err) {
|
|
840
1298
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -847,6 +1305,7 @@ export const ops = [
|
|
|
847
1305
|
label: "Filter Block Items",
|
|
848
1306
|
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.",
|
|
849
1307
|
promptSnippet: "Filter a block's items by a predicate — eq / neq / in / matches against a single field",
|
|
1308
|
+
examples: [`pi-context filter-block-items --block framework-gaps --field status --op eq --value '"open"' --json`],
|
|
850
1309
|
parameters: Type.Object({
|
|
851
1310
|
block: Type.String({
|
|
852
1311
|
description: "Block name (e.g., 'tasks', 'decisions', 'framework-gaps', 'context-contracts')",
|
|
@@ -866,11 +1325,11 @@ export const ops = [
|
|
|
866
1325
|
op: params.op,
|
|
867
1326
|
value: params.value,
|
|
868
1327
|
});
|
|
869
|
-
const
|
|
1328
|
+
const read = structureForRead(result, {
|
|
870
1329
|
label: `${params.block} filtered`,
|
|
871
1330
|
overCapDirective: { tool: "read-block-page", hint: "or refine the predicate" },
|
|
872
1331
|
});
|
|
873
|
-
return
|
|
1332
|
+
return { read };
|
|
874
1333
|
},
|
|
875
1334
|
},
|
|
876
1335
|
{
|
|
@@ -878,13 +1337,24 @@ export const ops = [
|
|
|
878
1337
|
label: "Resolve Item By Id",
|
|
879
1338
|
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.",
|
|
880
1339
|
promptSnippet: "Resolve a kind-prefixed ID (DEC-/FEAT-/FGAP-/issue-/REQ-/TASK-/etc.) to its owning block and item",
|
|
1340
|
+
examples: [`pi-context resolve-item-by-id --id TASK-001 --json`],
|
|
881
1341
|
parameters: Type.Object({
|
|
882
1342
|
id: Type.String({ description: "Kind-prefixed ID, e.g., DEC-NNNN / FEAT-NNN / FGAP-NNN / ISSUE-NNN" }),
|
|
883
1343
|
}),
|
|
884
1344
|
surface: "use",
|
|
885
1345
|
run(cwd, params) {
|
|
886
1346
|
const result = resolveItemById(cwd, params.id);
|
|
887
|
-
|
|
1347
|
+
// TASK-013 / FGAP-015: route through {read} so the embedded full
|
|
1348
|
+
// ItemLocation is bounded at the 50KB cap and over-cap fails closed with
|
|
1349
|
+
// a narrowing directive (mirrors read-block-item). `result` is
|
|
1350
|
+
// ItemLocation | null — structureForRead handles both.
|
|
1351
|
+
return {
|
|
1352
|
+
read: structureForRead(result, {
|
|
1353
|
+
whole: true,
|
|
1354
|
+
label: `resolve ${params.id}`,
|
|
1355
|
+
overCapDirective: { tool: "read-block-item", hint: "narrow to one block" },
|
|
1356
|
+
}),
|
|
1357
|
+
};
|
|
888
1358
|
},
|
|
889
1359
|
},
|
|
890
1360
|
{
|
|
@@ -892,6 +1362,7 @@ export const ops = [
|
|
|
892
1362
|
label: "Read Block Item",
|
|
893
1363
|
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.",
|
|
894
1364
|
promptSnippet: "Read one item from a block by id (block-scoped; null if absent)",
|
|
1365
|
+
examples: [`pi-context read-block-item --block tasks --id TASK-001 --json`],
|
|
895
1366
|
parameters: Type.Object({
|
|
896
1367
|
block: Type.String({ description: "Block name (e.g., 'tasks', 'decisions', 'framework-gaps')" }),
|
|
897
1368
|
id: Type.String({ description: "Item id within the block (e.g., 'TASK-NNN')" }),
|
|
@@ -901,8 +1372,8 @@ export const ops = [
|
|
|
901
1372
|
const result = readBlockItem(cwd, params.block, params.id);
|
|
902
1373
|
// whole: the item is already the addressed element — don't re-page its
|
|
903
1374
|
// intrinsic arrays; preserve the single-item|null output contract.
|
|
904
|
-
const
|
|
905
|
-
return
|
|
1375
|
+
const read = structureForRead(result, { whole: true, label: `${params.block} ${params.id}` });
|
|
1376
|
+
return { read };
|
|
906
1377
|
},
|
|
907
1378
|
},
|
|
908
1379
|
{
|
|
@@ -910,6 +1381,7 @@ export const ops = [
|
|
|
910
1381
|
label: "Read Block Page",
|
|
911
1382
|
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.",
|
|
912
1383
|
promptSnippet: "Paginate a block's items — offset + limit; returns {items,total,hasMore}",
|
|
1384
|
+
examples: [`pi-context read-block-page --block tasks --limit 20 --json`],
|
|
913
1385
|
parameters: Type.Object({
|
|
914
1386
|
block: Type.String({ description: "Block name (e.g., 'framework-gaps', 'decisions', 'issues')" }),
|
|
915
1387
|
offset: Type.Optional(Type.Integer({ minimum: 0, description: "Start index (default 0)" })),
|
|
@@ -920,8 +1392,8 @@ export const ops = [
|
|
|
920
1392
|
const result = readBlockPage(cwd, params.block, { offset: params.offset, limit: params.limit });
|
|
921
1393
|
// whole: readBlockPage ALREADY paged — preserve the {items,total,hasMore}
|
|
922
1394
|
// output contract; do not let serializeForRead re-page the items array.
|
|
923
|
-
const
|
|
924
|
-
return
|
|
1395
|
+
const read = structureForRead(result, { whole: true, label: `${params.block} page` });
|
|
1396
|
+
return { read };
|
|
925
1397
|
},
|
|
926
1398
|
},
|
|
927
1399
|
{
|
|
@@ -929,6 +1401,9 @@ export const ops = [
|
|
|
929
1401
|
label: "Join Blocks",
|
|
930
1402
|
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.",
|
|
931
1403
|
promptSnippet: "Join two blocks in one call — by relation edge or shared field; returns {left,right[]} pairs",
|
|
1404
|
+
examples: [
|
|
1405
|
+
`pi-context join-blocks --leftBlock tasks --rightBlock verification --relationType verification_verifies_item --leftEndpoint child --json`,
|
|
1406
|
+
],
|
|
932
1407
|
parameters: Type.Object({
|
|
933
1408
|
leftBlock: Type.String({ description: "Left block name (e.g., 'tasks')" }),
|
|
934
1409
|
rightBlock: Type.String({ description: "Right block name (e.g., 'verification')" }),
|
|
@@ -956,14 +1431,14 @@ export const ops = [
|
|
|
956
1431
|
leftEndpoint: params.leftEndpoint,
|
|
957
1432
|
leftPredicate,
|
|
958
1433
|
});
|
|
959
|
-
const
|
|
1434
|
+
const read = structureForRead(result, {
|
|
960
1435
|
label: `${params.leftBlock} ⋈ ${params.rightBlock}`,
|
|
961
1436
|
overCapDirective: {
|
|
962
1437
|
tool: "join-blocks",
|
|
963
1438
|
hint: "refine the relation/field or pre-filter the left block",
|
|
964
1439
|
},
|
|
965
1440
|
});
|
|
966
|
-
return
|
|
1441
|
+
return { read };
|
|
967
1442
|
},
|
|
968
1443
|
},
|
|
969
1444
|
{
|
|
@@ -971,6 +1446,7 @@ export const ops = [
|
|
|
971
1446
|
label: "Resolve Items By Id (Bulk)",
|
|
972
1447
|
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.",
|
|
973
1448
|
promptSnippet: "Resolve a batch of kind-prefixed ids (DEC-/FGAP-/TASK-/issue-/REQ-/...) in one call",
|
|
1449
|
+
examples: [`pi-context resolve-items-by-id --ids '["TASK-001","DEC-0001","FGAP-042"]' --json`],
|
|
974
1450
|
parameters: Type.Object({
|
|
975
1451
|
ids: Type.Array(Type.String(), {
|
|
976
1452
|
description: "Array of kind-prefixed ids (DEC-/FGAP-/TASK-/issue-/REQ-/...) to resolve in one call",
|
|
@@ -984,8 +1460,8 @@ export const ops = [
|
|
|
984
1460
|
obj[id] = loc;
|
|
985
1461
|
// whole: an id→location map keyed by arbitrary ids — not a pageable
|
|
986
1462
|
// collection; serialize the map verbatim.
|
|
987
|
-
const
|
|
988
|
-
return
|
|
1463
|
+
const read = structureForRead(obj, { whole: true, label: "resolved ids" });
|
|
1464
|
+
return { read };
|
|
989
1465
|
},
|
|
990
1466
|
},
|
|
991
1467
|
{
|
|
@@ -993,6 +1469,9 @@ export const ops = [
|
|
|
993
1469
|
label: "Complete Task",
|
|
994
1470
|
description: "Complete a task with verification gate — requires a passing verification entry targeting the task.",
|
|
995
1471
|
promptSnippet: "Complete a task — gates on passing verification before updating status",
|
|
1472
|
+
examples: [
|
|
1473
|
+
`pi-context complete-task --taskId TASK-001 --verificationId VER-001 --writer '{"kind":"human","user":"you@example.com"}' --json`,
|
|
1474
|
+
],
|
|
996
1475
|
parameters: Type.Object({
|
|
997
1476
|
taskId: Type.String({ description: "Task ID to complete" }),
|
|
998
1477
|
verificationId: Type.String({
|
|
@@ -1000,8 +1479,8 @@ export const ops = [
|
|
|
1000
1479
|
}),
|
|
1001
1480
|
}),
|
|
1002
1481
|
surface: "use",
|
|
1003
|
-
run(cwd, params) {
|
|
1004
|
-
const result = completeTask(cwd, params.taskId, params.verificationId);
|
|
1482
|
+
run(cwd, params, ctx) {
|
|
1483
|
+
const result = completeTask(cwd, params.taskId, params.verificationId, ctx);
|
|
1005
1484
|
return `Task '${result.taskId}' completed (was '${result.previousStatus}'). Verification: ${result.verificationId} (${result.verificationStatus})`;
|
|
1006
1485
|
},
|
|
1007
1486
|
},
|
|
@@ -1010,11 +1489,12 @@ export const ops = [
|
|
|
1010
1489
|
label: "Context Validate Relations",
|
|
1011
1490
|
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.",
|
|
1012
1491
|
promptSnippet: "Validate substrate relations against config + items",
|
|
1492
|
+
examples: [`pi-context context-validate-relations --json`],
|
|
1013
1493
|
parameters: Type.Object({}),
|
|
1014
1494
|
surface: "use",
|
|
1015
1495
|
run(cwd, _params) {
|
|
1016
1496
|
const result = validateContextRelations(cwd);
|
|
1017
|
-
return
|
|
1497
|
+
return { json: result };
|
|
1018
1498
|
},
|
|
1019
1499
|
},
|
|
1020
1500
|
{
|
|
@@ -1022,14 +1502,52 @@ export const ops = [
|
|
|
1022
1502
|
label: "Context Edges For Lens",
|
|
1023
1503
|
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.",
|
|
1024
1504
|
promptSnippet: "Materialize edges for a named lens (auto-derived or hand-curated)",
|
|
1505
|
+
examples: [`pi-context context-edges-for-lens --lensId feature-decomposition --json`],
|
|
1025
1506
|
parameters: Type.Object({
|
|
1026
1507
|
lensId: Type.String({ description: "Lens id from config.lenses[].id" }),
|
|
1027
1508
|
}),
|
|
1028
1509
|
surface: "use",
|
|
1029
1510
|
run(cwd, params) {
|
|
1030
1511
|
const result = edgesForLensByName(cwd, params.lensId);
|
|
1031
|
-
const
|
|
1032
|
-
return
|
|
1512
|
+
const read = structureForRead(result, { label: `edges for lens ${params.lensId}` });
|
|
1513
|
+
return { read };
|
|
1514
|
+
},
|
|
1515
|
+
},
|
|
1516
|
+
{
|
|
1517
|
+
name: "context-lens-view",
|
|
1518
|
+
label: "Context Lens View",
|
|
1519
|
+
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.",
|
|
1520
|
+
promptSnippet: "Project a config-declared lens as a binned item-view — bin->count summary, or one bin's items paged",
|
|
1521
|
+
examples: [
|
|
1522
|
+
`pi-context context-lens-view --lensId gaps-by-status --json`,
|
|
1523
|
+
`pi-context context-lens-view --lensId gaps-by-status --bin identified --limit 20 --json`,
|
|
1524
|
+
],
|
|
1525
|
+
parameters: Type.Object({
|
|
1526
|
+
lensId: Type.String({ description: "Lens id from config.lenses[].id" }),
|
|
1527
|
+
bin: Type.Optional(Type.String({ description: "Return this bin's items paged; omit for a bin->count summary" })),
|
|
1528
|
+
offset: Type.Optional(Type.Integer({ minimum: 0, description: "Per-bin page start index (default 0)" })),
|
|
1529
|
+
limit: Type.Optional(Type.Integer({ minimum: 1, description: "Per-bin page size (default 50)" })),
|
|
1530
|
+
}),
|
|
1531
|
+
surface: "use",
|
|
1532
|
+
run(cwd, params) {
|
|
1533
|
+
const view = loadLensView(cwd, params.lensId);
|
|
1534
|
+
if ("error" in view)
|
|
1535
|
+
throw new Error(view.error); // unknown lens / no-config -> non-zero exit
|
|
1536
|
+
if (params.bin !== undefined) {
|
|
1537
|
+
const items = view.grouped.get(params.bin);
|
|
1538
|
+
if (items === undefined)
|
|
1539
|
+
throw new Error(`Bin '${params.bin}' not declared on lens '${params.lensId}'. Bins: ${view.lens.bins.join(", ")}`);
|
|
1540
|
+
const page = pageArray(items, { offset: params.offset, limit: params.limit });
|
|
1541
|
+
return { read: structureForRead(page, { whole: true, label: `lens ${params.lensId} bin ${params.bin}` }) };
|
|
1542
|
+
}
|
|
1543
|
+
const summary = {
|
|
1544
|
+
lens: view.lens.id,
|
|
1545
|
+
kind: view.lens.kind ?? "target",
|
|
1546
|
+
bins: Object.fromEntries(view.lens.bins.map((b) => [b, (view.grouped.get(b) ?? []).length])),
|
|
1547
|
+
uncategorized: view.uncategorized.length,
|
|
1548
|
+
total: view.items.length,
|
|
1549
|
+
};
|
|
1550
|
+
return { read: structureForRead(summary, { whole: true, label: `lens ${params.lensId} bins` }) };
|
|
1033
1551
|
},
|
|
1034
1552
|
},
|
|
1035
1553
|
{
|
|
@@ -1037,6 +1555,9 @@ export const ops = [
|
|
|
1037
1555
|
label: "Context Walk Descendants",
|
|
1038
1556
|
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).",
|
|
1039
1557
|
promptSnippet: "Walk closure-table descendants under a relation_type",
|
|
1558
|
+
examples: [
|
|
1559
|
+
`pi-context context-walk-descendants --parentId FEAT-008 --relationType feature_decomposed_into_task --json`,
|
|
1560
|
+
],
|
|
1040
1561
|
parameters: Type.Object({
|
|
1041
1562
|
parentId: Type.String({ description: "Parent id (canonical id or lens bin name)" }),
|
|
1042
1563
|
relationType: Type.String({ description: "Relation type from config.relation_types[].canonical_id" }),
|
|
@@ -1044,7 +1565,7 @@ export const ops = [
|
|
|
1044
1565
|
surface: "use",
|
|
1045
1566
|
run(cwd, params) {
|
|
1046
1567
|
const result = walkLensDescendants(cwd, params.parentId, params.relationType);
|
|
1047
|
-
return
|
|
1568
|
+
return { json: result };
|
|
1048
1569
|
},
|
|
1049
1570
|
},
|
|
1050
1571
|
{
|
|
@@ -1052,6 +1573,7 @@ export const ops = [
|
|
|
1052
1573
|
label: "Walk Ancestors",
|
|
1053
1574
|
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).",
|
|
1054
1575
|
promptSnippet: "Walk closure-table ancestors under a relation_type",
|
|
1576
|
+
examples: [`pi-context walk-ancestors --itemId TASK-042 --relationType feature_decomposed_into_task --json`],
|
|
1055
1577
|
parameters: Type.Object({
|
|
1056
1578
|
itemId: Type.String({ description: "Child item id whose ancestors are sought" }),
|
|
1057
1579
|
relationType: Type.String({ description: "Relation type from config.relation_types[].canonical_id" }),
|
|
@@ -1059,8 +1581,8 @@ export const ops = [
|
|
|
1059
1581
|
surface: "use",
|
|
1060
1582
|
run(cwd, params) {
|
|
1061
1583
|
const result = walkAncestorsByLens(cwd, params.itemId, params.relationType);
|
|
1062
|
-
const
|
|
1063
|
-
return
|
|
1584
|
+
const read = structureForRead(result, { label: `ancestors of ${params.itemId}` });
|
|
1585
|
+
return { read };
|
|
1064
1586
|
},
|
|
1065
1587
|
},
|
|
1066
1588
|
{
|
|
@@ -1068,6 +1590,7 @@ export const ops = [
|
|
|
1068
1590
|
label: "Find References",
|
|
1069
1591
|
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.",
|
|
1070
1592
|
promptSnippet: "Find closure-table edges incident on an item id",
|
|
1593
|
+
examples: [`pi-context find-references --itemId TASK-001 --direction both --json`],
|
|
1071
1594
|
parameters: Type.Object({
|
|
1072
1595
|
itemId: Type.String({ description: "Item id whose incident edges are sought" }),
|
|
1073
1596
|
direction: Type.Optional(Type.Union([Type.Literal("inbound"), Type.Literal("outbound"), Type.Literal("both")], {
|
|
@@ -1077,8 +1600,8 @@ export const ops = [
|
|
|
1077
1600
|
surface: "use",
|
|
1078
1601
|
run(cwd, params) {
|
|
1079
1602
|
const result = findReferencesInRepo(cwd, params.itemId, params.direction);
|
|
1080
|
-
const
|
|
1081
|
-
return
|
|
1603
|
+
const read = structureForRead(result, { label: `edges on ${params.itemId}` });
|
|
1604
|
+
return { read };
|
|
1082
1605
|
},
|
|
1083
1606
|
},
|
|
1084
1607
|
{
|
|
@@ -1086,6 +1609,7 @@ export const ops = [
|
|
|
1086
1609
|
label: "Gather Execution Context",
|
|
1087
1610
|
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.",
|
|
1088
1611
|
promptSnippet: "Compose ContextBundle for unit + context-contract-declared bundle_relation_types",
|
|
1612
|
+
examples: [`pi-context gather-execution-context --unitId TASK-001 --kind task --json`],
|
|
1089
1613
|
parameters: Type.Object({
|
|
1090
1614
|
unitId: Type.String({ description: "Work-unit id (e.g. TASK-NNN / DEC-NNNN / FGAP-NNN)" }),
|
|
1091
1615
|
kind: Type.String({
|
|
@@ -1101,8 +1625,8 @@ export const ops = [
|
|
|
1101
1625
|
const result = gatherExecutionContext(cwd, params);
|
|
1102
1626
|
// whole: a structured ContextBundle (unit + perRelationType buckets) —
|
|
1103
1627
|
// preserve the bundle shape rather than paging any single inner array.
|
|
1104
|
-
const
|
|
1105
|
-
return
|
|
1628
|
+
const read = structureForRead(result, { whole: true, label: `bundle ${params.unitId}` });
|
|
1629
|
+
return { read };
|
|
1106
1630
|
},
|
|
1107
1631
|
},
|
|
1108
1632
|
{
|
|
@@ -1110,6 +1634,7 @@ export const ops = [
|
|
|
1110
1634
|
label: "Context: load roadmap",
|
|
1111
1635
|
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'.",
|
|
1112
1636
|
promptSnippet: "Load a roadmap by id",
|
|
1637
|
+
examples: [`pi-context context-roadmap-load --roadmapId ROADMAP-001 --json`],
|
|
1113
1638
|
parameters: Type.Object({
|
|
1114
1639
|
roadmapId: Type.String({ description: "ROADMAP-NNN id from <config.root>/roadmap.json" }),
|
|
1115
1640
|
}),
|
|
@@ -1117,16 +1642,16 @@ export const ops = [
|
|
|
1117
1642
|
run(cwd, params) {
|
|
1118
1643
|
const view = loadRoadmap(cwd, params.roadmapId);
|
|
1119
1644
|
if ("error" in view) {
|
|
1120
|
-
const
|
|
1121
|
-
return
|
|
1645
|
+
const read = structureForRead(view, { whole: true, label: `roadmap ${params.roadmapId} (error)` });
|
|
1646
|
+
return { read };
|
|
1122
1647
|
}
|
|
1123
1648
|
// whole: a structured RoadmapView (phases + lens-views + rollups) — keep
|
|
1124
1649
|
// the view shape intact rather than paging an inner array.
|
|
1125
|
-
const
|
|
1650
|
+
const read = structureForRead(serializeRoadmapView(view), {
|
|
1126
1651
|
whole: true,
|
|
1127
1652
|
label: `roadmap ${params.roadmapId}`,
|
|
1128
1653
|
});
|
|
1129
|
-
return
|
|
1654
|
+
return { read };
|
|
1130
1655
|
},
|
|
1131
1656
|
},
|
|
1132
1657
|
{
|
|
@@ -1134,6 +1659,7 @@ export const ops = [
|
|
|
1134
1659
|
label: "Context: render roadmap",
|
|
1135
1660
|
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.",
|
|
1136
1661
|
promptSnippet: "Render a roadmap as markdown",
|
|
1662
|
+
examples: [`pi-context context-roadmap-render --roadmapId ROADMAP-001`],
|
|
1137
1663
|
parameters: Type.Object({
|
|
1138
1664
|
roadmapId: Type.String({ description: "ROADMAP-NNN id from <config.root>/roadmap.json" }),
|
|
1139
1665
|
}),
|
|
@@ -1141,7 +1667,7 @@ export const ops = [
|
|
|
1141
1667
|
run(cwd, params) {
|
|
1142
1668
|
const view = loadRoadmap(cwd, params.roadmapId);
|
|
1143
1669
|
if ("error" in view) {
|
|
1144
|
-
return
|
|
1670
|
+
return { json: view };
|
|
1145
1671
|
}
|
|
1146
1672
|
const naming = loadContext(cwd).config?.naming;
|
|
1147
1673
|
return renderRoadmap(view, naming);
|
|
@@ -1152,6 +1678,7 @@ export const ops = [
|
|
|
1152
1678
|
label: "Context: validate roadmap(s)",
|
|
1153
1679
|
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.",
|
|
1154
1680
|
promptSnippet: "Validate roadmaps",
|
|
1681
|
+
examples: [`pi-context context-roadmap-validate --roadmapId ROADMAP-001 --json`],
|
|
1155
1682
|
parameters: Type.Object({
|
|
1156
1683
|
roadmapId: Type.Optional(Type.String({ description: "Filter to issues matching this roadmap_id (omit for full-project validation)" })),
|
|
1157
1684
|
}),
|
|
@@ -1161,7 +1688,7 @@ export const ops = [
|
|
|
1161
1688
|
const filtered = params.roadmapId
|
|
1162
1689
|
? result.issues.filter((i) => !i.roadmap_id || i.roadmap_id === params.roadmapId)
|
|
1163
1690
|
: result.issues;
|
|
1164
|
-
return
|
|
1691
|
+
return { json: { status: result.status, issues: filtered } };
|
|
1165
1692
|
},
|
|
1166
1693
|
},
|
|
1167
1694
|
{
|
|
@@ -1169,10 +1696,11 @@ export const ops = [
|
|
|
1169
1696
|
label: "Context: list roadmaps",
|
|
1170
1697
|
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).",
|
|
1171
1698
|
promptSnippet: "List roadmaps",
|
|
1699
|
+
examples: [`pi-context context-roadmap-list --json`],
|
|
1172
1700
|
parameters: Type.Object({}),
|
|
1173
1701
|
surface: "use",
|
|
1174
1702
|
run(cwd, _params) {
|
|
1175
|
-
return
|
|
1703
|
+
return { json: listRoadmaps(cwd) };
|
|
1176
1704
|
},
|
|
1177
1705
|
},
|
|
1178
1706
|
];
|
|
@@ -1187,6 +1715,137 @@ export const ops = [
|
|
|
1187
1715
|
* point; this list is the source of pi-context's contribution to it.
|
|
1188
1716
|
*/
|
|
1189
1717
|
export const gatedTools = ops.filter((o) => o.authGated).map((o) => o.name);
|
|
1718
|
+
/**
|
|
1719
|
+
* The FGAP-009 non-exposure allowlist: every library write function that is
|
|
1720
|
+
* deliberately NOT op-backed, with the reason it is withheld. This is the
|
|
1721
|
+
* closure contract γ (TASK-008) WILL consume: γ's parity test — not yet written
|
|
1722
|
+
* (no executable parity test exists in β; β defines the contract, γ implements
|
|
1723
|
+
* the test against it) — WILL assert that EVERY library writer is either op-backed
|
|
1724
|
+
* (appears in {@link ops}, directly or transitively) OR named here, so a
|
|
1725
|
+
* newly-added library writer with neither an op nor an allowlist entry will fail
|
|
1726
|
+
* that test, keeping the op surface and the library write surface in lockstep.
|
|
1727
|
+
*
|
|
1728
|
+
* The `*ForDir` twins of op-backed writers (e.g. `appendRelationForDir`,
|
|
1729
|
+
* `writeRelationsForDir`, `upsertItemInBlockForDir`, `appendToBlockForDir`, …)
|
|
1730
|
+
* are NOT enumerated individually: each is the dir-targeted internal twin of a
|
|
1731
|
+
* cwd-form writer that IS op-backed, and is covered by that cwd-form op (the op
|
|
1732
|
+
* resolves the active substrate dir then delegates to the same shared
|
|
1733
|
+
* typed-file primitive the `*ForDir` twin calls). The contract is that γ's
|
|
1734
|
+
* parity test (TASK-008, not yet written) WILL treat a `*ForDir` writer as
|
|
1735
|
+
* covered when its cwd-form sibling is covered.
|
|
1736
|
+
*/
|
|
1737
|
+
export const INTENTIONALLY_UNEXPOSED_WRITERS = [
|
|
1738
|
+
{ libraryFn: "writeConfig", safeOp: "amend-config", reason: "scoped guarded config mutation" },
|
|
1739
|
+
{ libraryFn: "writeSchema", safeOp: "write-schema", reason: "raw bypasses the create/replace + migration check" },
|
|
1740
|
+
{ libraryFn: "updateSchema", safeOp: "write-schema", reason: "no mutator-scripting surface" },
|
|
1741
|
+
{ libraryFn: "writeBootstrapPointer", safeOp: "context-init", reason: "raw bypasses target validation" },
|
|
1742
|
+
{ libraryFn: "flipBootstrapPointer", safeOp: "context-switch", reason: "raw splits the safe switch" },
|
|
1743
|
+
{ libraryFn: "writeRegistry", reason: "internal; registry writes flow through registerSubstrate callers" },
|
|
1744
|
+
{
|
|
1745
|
+
libraryFn: "registerSubstrate",
|
|
1746
|
+
reason: "manual/foreign registration is the clone arc (DEC-0002); normal paths auto-register",
|
|
1747
|
+
},
|
|
1748
|
+
{
|
|
1749
|
+
libraryFn: "rollbackBlockFiles",
|
|
1750
|
+
reason: "workflow-executor transactional rollback (graduated-failure undo); internal recovery path with no operator-facing op by design",
|
|
1751
|
+
},
|
|
1752
|
+
];
|
|
1753
|
+
/**
|
|
1754
|
+
* The five mutually-exhaustive ways a library write function is COVERED by the
|
|
1755
|
+
* FGAP-009 op-surface ↔ library-write-surface parity contract. A writer that
|
|
1756
|
+
* matches NONE of these is a silent gap that γ's (TASK-008) parity test — once
|
|
1757
|
+
* written — MUST fail on. Coverage is the DISJUNCTION over these classes — a
|
|
1758
|
+
* writer needs ANY one, not all.
|
|
1759
|
+
*/
|
|
1760
|
+
export var CoverageClass;
|
|
1761
|
+
(function (CoverageClass) {
|
|
1762
|
+
/** An op's `run` calls the writer directly (e.g. `append-block-item` → `appendToBlock`). */
|
|
1763
|
+
CoverageClass["OpBackedDirect"] = "op-backed-direct";
|
|
1764
|
+
/**
|
|
1765
|
+
* An op's `run` reaches the writer TRANSITIVELY — through any helper / wrapper
|
|
1766
|
+
* chain, not just a direct call. Two sub-shapes both land here:
|
|
1767
|
+
* - `*ByRef` / SDK relation porcelain: the `remove-relation` / `replace-relation`
|
|
1768
|
+
* / `append-relations` ops call `removeRelationByRef` / `replaceRelationByRef`
|
|
1769
|
+
* / `appendRelationsByRef`, which call `writeRelations`.
|
|
1770
|
+
* - init / switch → internal-helper chains: `context-init` → `initProject` →
|
|
1771
|
+
* `writeSkeletonConfig`; `context-switch` → `switchToExisting` /
|
|
1772
|
+
* `switchAndCreate` → `reconcileActiveSubstrateRegistration`. The writer is
|
|
1773
|
+
* not a `*ByRef` porcelain and is not allowlisted, but it IS reachable from an
|
|
1774
|
+
* op's `run` via a helper the op calls.
|
|
1775
|
+
* Coverage condition: reachable from some op's `run` via any helper/wrapper chain.
|
|
1776
|
+
*/
|
|
1777
|
+
CoverageClass["OpBackedTransitive"] = "op-backed-transitive";
|
|
1778
|
+
/**
|
|
1779
|
+
* A `*ForDir` dir-targeted twin of a covered cwd-form writer (e.g.
|
|
1780
|
+
* `appendToBlockForDir` is the twin of the op-backed `appendToBlock`). Covered
|
|
1781
|
+
* by its cwd-form sibling — both delegate to the same shared typed-file
|
|
1782
|
+
* primitive; the cwd-form op resolves the active dir then calls it.
|
|
1783
|
+
*/
|
|
1784
|
+
CoverageClass["ForDirTwin"] = "for-dir-twin";
|
|
1785
|
+
/**
|
|
1786
|
+
* On {@link INTENTIONALLY_UNEXPOSED_WRITERS}: a raw write deliberately given NO
|
|
1787
|
+
* direct op (a scoped op supersedes it, or it is internal/foreign-only).
|
|
1788
|
+
*/
|
|
1789
|
+
CoverageClass["IntentionallyUnexposed"] = "intentionally-unexposed";
|
|
1790
|
+
/**
|
|
1791
|
+
* A block-api internal primitive below the op layer — the `*TypedFile` read/
|
|
1792
|
+
* write layer (`readTypedFile` / `writeTypedFile`), `prepareItemIdentityForWrite`,
|
|
1793
|
+
* and the identity / content-hash helpers. Never op-backed by design; the ops
|
|
1794
|
+
* compose over these.
|
|
1795
|
+
*/
|
|
1796
|
+
CoverageClass["InternalPrimitive"] = "internal-primitive";
|
|
1797
|
+
})(CoverageClass || (CoverageClass = {}));
|
|
1798
|
+
/**
|
|
1799
|
+
* The FGAP-009 coverage RULE, made explicit so γ (TASK-008) will import the
|
|
1800
|
+
* contract rather than re-derive it. A library write function is COVERED iff it
|
|
1801
|
+
* matches ANY clause below (the disjunction); a writer matching none is a silent
|
|
1802
|
+
* gap that γ's parity test — when written — MUST fail on. β fixes the contract
|
|
1803
|
+
* here; no executable parity test exists yet (that is γ).
|
|
1804
|
+
*
|
|
1805
|
+
* Why `writeConfig` is allowlisted but `writeRelations` is NOT — the distinction
|
|
1806
|
+
* a strict name-parity reading mis-saw as inconsistent:
|
|
1807
|
+
* - `writeConfig` has NO direct wholesale-config op. The scoped surface is
|
|
1808
|
+
* `amend-config` (one-entry-in-one-registry add/replace/remove, AJV-validated),
|
|
1809
|
+
* which deliberately does NOT expose a raw whole-config overwrite. So
|
|
1810
|
+
* `writeConfig` is `intentionally-unexposed` (the scoped op supersedes the raw
|
|
1811
|
+
* writer; a raw wholesale overwrite is withheld by design).
|
|
1812
|
+
* - `writeRelations` IS reached — transitively — by the relation ops:
|
|
1813
|
+
* `remove-relation` / `replace-relation` / `append-relations` call
|
|
1814
|
+
* `removeRelationByRef` / `replaceRelationByRef` / `appendRelationsByRef`, each
|
|
1815
|
+
* of which calls `writeRelations`. It is therefore `op-backed-transitive`
|
|
1816
|
+
* and needs NO allowlist entry. The asymmetry is real and correct: one writer
|
|
1817
|
+
* has an op path (via a helper/wrapper chain), the other does not.
|
|
1818
|
+
*
|
|
1819
|
+
* The `op-backed-transitive` clause covers BOTH the `*ByRef` relation porcelain
|
|
1820
|
+
* AND the init/switch → internal-helper chains: `writeSkeletonConfig` (reached
|
|
1821
|
+
* via `context-init` → `initProject`) and `reconcileActiveSubstrateRegistration`
|
|
1822
|
+
* (reached via `context-switch` → `switchToExisting` / `switchAndCreate`) are
|
|
1823
|
+
* neither `*ByRef` porcelain nor allowlisted, yet each is reachable from an op's
|
|
1824
|
+
* `run` through a helper that op calls — so each classifies cleanly as
|
|
1825
|
+
* `op-backed-transitive`, not as a gap.
|
|
1826
|
+
*/
|
|
1827
|
+
export const OP_COVERAGE_RULE = [
|
|
1828
|
+
{
|
|
1829
|
+
coverageClass: CoverageClass.OpBackedDirect,
|
|
1830
|
+
test: "an op's run() calls the writer directly",
|
|
1831
|
+
},
|
|
1832
|
+
{
|
|
1833
|
+
coverageClass: CoverageClass.OpBackedTransitive,
|
|
1834
|
+
test: "reachable from some op's run() via any helper/wrapper chain — the *ByRef / SDK relation porcelain (writeRelations via removeRelationByRef / replaceRelationByRef / appendRelationsByRef) OR an init/switch → internal-helper chain (writeSkeletonConfig via context-init → initProject; reconcileActiveSubstrateRegistration via context-switch → switchToExisting / switchAndCreate)",
|
|
1835
|
+
},
|
|
1836
|
+
{
|
|
1837
|
+
coverageClass: CoverageClass.ForDirTwin,
|
|
1838
|
+
test: "a *ForDir twin of a covered cwd-form writer (covered by its cwd-form sibling)",
|
|
1839
|
+
},
|
|
1840
|
+
{
|
|
1841
|
+
coverageClass: CoverageClass.IntentionallyUnexposed,
|
|
1842
|
+
test: "named on INTENTIONALLY_UNEXPOSED_WRITERS — a raw bypass with no direct op",
|
|
1843
|
+
},
|
|
1844
|
+
{
|
|
1845
|
+
coverageClass: CoverageClass.InternalPrimitive,
|
|
1846
|
+
test: "a block-api internal primitive below the op layer (the *TypedFile layer, prepareItemIdentityForWrite, identity / content-hash helpers)",
|
|
1847
|
+
},
|
|
1848
|
+
];
|
|
1190
1849
|
/**
|
|
1191
1850
|
* The factory PI handle captured at registerAll time. The list-tools op needs
|
|
1192
1851
|
* the introspection surface (getAllTools / getActiveTools) which lives on
|
|
@@ -1196,11 +1855,36 @@ export const gatedTools = ops.filter((o) => o.authGated).map((o) => o.name);
|
|
|
1196
1855
|
* threading the handle through every signature.
|
|
1197
1856
|
*/
|
|
1198
1857
|
let boundPi = null;
|
|
1858
|
+
/**
|
|
1859
|
+
* Build the DispatchContext threaded into an op's `run` from the in-pi tool
|
|
1860
|
+
* execute boundary (registerAll). Two derivation branches:
|
|
1861
|
+
*
|
|
1862
|
+
* - When `params.writer.user` is a non-empty string — the shape the
|
|
1863
|
+
* pi-agent-dispatch auth-gate stamps onto authGated op params on operator
|
|
1864
|
+
* confirm — the writer is a human identity. (The smuggle-ops promote-item /
|
|
1865
|
+
* write-schema-migration / context-switch carry a `writer` schema field
|
|
1866
|
+
* precisely so the gate has somewhere to stamp; this converts that field
|
|
1867
|
+
* into the contract ctx the op now consumes via its 3rd `run` arg.)
|
|
1868
|
+
* - Otherwise the writer is the running agent, identified by the active
|
|
1869
|
+
* model's id (`ExtensionContext.model.id`); falls back to "pi-agent" when
|
|
1870
|
+
* no model (or no id) is resolvable.
|
|
1871
|
+
*
|
|
1872
|
+
* Exported for unit testing — the two branches are asserted directly against
|
|
1873
|
+
* synthetic params + a minimal ExtensionContext-shaped object.
|
|
1874
|
+
*/
|
|
1875
|
+
export function buildDispatchContextFromExecute(params, extCtx) {
|
|
1876
|
+
const writerUser = params?.writer?.user;
|
|
1877
|
+
if (typeof writerUser === "string" && writerUser.length > 0) {
|
|
1878
|
+
return { writer: { kind: "human", user: writerUser } };
|
|
1879
|
+
}
|
|
1880
|
+
const modelId = extCtx.model?.id;
|
|
1881
|
+
return { writer: { kind: "agent", agent_id: modelId && modelId.length > 0 ? modelId : "pi-agent" } };
|
|
1882
|
+
}
|
|
1199
1883
|
/**
|
|
1200
1884
|
* Register every op in `ops` as a pi tool. Each tool's execute body is the
|
|
1201
|
-
* uniform wrapper around the op's run(): coerce params,
|
|
1202
|
-
*
|
|
1203
|
-
*
|
|
1885
|
+
* uniform wrapper around the op's run(): coerce params, build the attestation
|
|
1886
|
+
* DispatchContext from the auth-gate-stamped writer (human) or the running
|
|
1887
|
+
* model (agent), await run, place the returned string at content[0].text.
|
|
1204
1888
|
*/
|
|
1205
1889
|
export function registerAll(pi) {
|
|
1206
1890
|
boundPi = pi;
|
|
@@ -1212,9 +1896,10 @@ export function registerAll(pi) {
|
|
|
1212
1896
|
promptSnippet: op.promptSnippet,
|
|
1213
1897
|
parameters: op.parameters,
|
|
1214
1898
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
1899
|
+
const dctx = buildDispatchContextFromExecute(params, ctx);
|
|
1215
1900
|
return {
|
|
1216
1901
|
details: undefined,
|
|
1217
|
-
content: [{ type: "text", text: await op.run(ctx.cwd, params) }],
|
|
1902
|
+
content: [{ type: "text", text: renderOpResultText(await op.run(ctx.cwd, params, dctx)) }],
|
|
1218
1903
|
};
|
|
1219
1904
|
},
|
|
1220
1905
|
});
|