@davidorex/pi-context 0.28.1 → 0.30.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 +32 -0
- package/README.md +28 -16
- package/dist/block-api.d.ts +12 -3
- package/dist/block-api.d.ts.map +1 -1
- package/dist/block-api.js +16 -5
- package/dist/block-api.js.map +1 -1
- package/dist/context-sdk.d.ts +111 -8
- package/dist/context-sdk.d.ts.map +1 -1
- package/dist/context-sdk.js +191 -15
- 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 +171 -15
- package/dist/context.js.map +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +42 -7
- package/dist/index.js.map +1 -1
- package/dist/ops-registry.d.ts +180 -4
- package/dist/ops-registry.d.ts.map +1 -1
- package/dist/ops-registry.js +535 -112
- package/dist/ops-registry.js.map +1 -1
- 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/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 +1 -1
- package/skills/pi-context/SKILL.md +62 -7
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
|
|
@@ -32,12 +32,89 @@ import { gatherExecutionContext } from "./execution-context.js";
|
|
|
32
32
|
import { archiveSubstrate, initProject, listSubstrates, switchAndCreate, switchToExisting, switchToPrevious, } from "./index.js";
|
|
33
33
|
import { edgesForLensByName, findReferencesInRepo, validateContextRelations, walkAncestorsByLens, walkLensDescendants, } from "./lens-view.js";
|
|
34
34
|
import { promoteItem } from "./promote-item.js";
|
|
35
|
-
import { addressInto,
|
|
35
|
+
import { addressInto, 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
|
|
@@ -78,7 +155,7 @@ export const ops = [
|
|
|
78
155
|
})),
|
|
79
156
|
}),
|
|
80
157
|
surface: "use",
|
|
81
|
-
run(cwd, params) {
|
|
158
|
+
run(cwd, params, ctx) {
|
|
82
159
|
// Type.Unknown() params may arrive as JSON strings — parse if needed
|
|
83
160
|
if (typeof params.item === "string") {
|
|
84
161
|
try {
|
|
@@ -96,7 +173,7 @@ export const ops = [
|
|
|
96
173
|
// withBlockLock critical section (block-api assertAppendIdUnique) —
|
|
97
174
|
// the single enforcement point. The prior racy readBlock-then-append
|
|
98
175
|
// tool-layer check was removed in favour of that library guard.
|
|
99
|
-
appendToBlock(cwd, params.block, params.arrayKey, params.item);
|
|
176
|
+
appendToBlock(cwd, params.block, params.arrayKey, params.item, ctx);
|
|
100
177
|
const id = params.item?.id ? ` '${params.item.id}'` : "";
|
|
101
178
|
return `Appended item${id} to ${params.block}.${params.arrayKey}`;
|
|
102
179
|
},
|
|
@@ -115,12 +192,12 @@ export const ops = [
|
|
|
115
192
|
}),
|
|
116
193
|
}),
|
|
117
194
|
surface: "use",
|
|
118
|
-
run(cwd, params) {
|
|
195
|
+
run(cwd, params, ctx) {
|
|
119
196
|
if (Object.keys(params.updates).length === 0) {
|
|
120
197
|
throw new Error("No fields to update — updates parameter is empty");
|
|
121
198
|
}
|
|
122
199
|
const matchEntries = Object.entries(params.match);
|
|
123
|
-
updateItemInBlock(cwd, params.block, params.arrayKey, (item) => matchEntries.every(([k, v]) => item[k] === v), params.updates);
|
|
200
|
+
updateItemInBlock(cwd, params.block, params.arrayKey, (item) => matchEntries.every(([k, v]) => item[k] === v), params.updates, ctx);
|
|
124
201
|
const matchDesc = matchEntries.map(([k, v]) => `${k}=${v}`).join(", ");
|
|
125
202
|
return `Updated item (${matchDesc}) in ${params.block}.${params.arrayKey}: ${Object.keys(params.updates).join(", ")}`;
|
|
126
203
|
},
|
|
@@ -140,25 +217,202 @@ export const ops = [
|
|
|
140
217
|
description: "Registered relation_type canonical_id / hierarchy edge type / lens id",
|
|
141
218
|
}),
|
|
142
219
|
ordinal: Type.Optional(Type.Integer({ description: "Optional sibling-ordering within (parent, relation_type)" })),
|
|
220
|
+
dryRun: Type.Optional(Type.Boolean({ description: "Preview without writing relations.json" })),
|
|
143
221
|
}),
|
|
144
222
|
surface: "use",
|
|
145
|
-
run(cwd, params) {
|
|
223
|
+
run(cwd, params, ctx) {
|
|
146
224
|
// Cycle-5 porcelain: STRING selectors (bare refname / <alias>:<refname> /
|
|
147
225
|
// lens-bin) are resolved to structured EdgeEndpoints and written via the
|
|
148
226
|
// raw plumbing. The param surface stays string-typed; messaging uses the
|
|
149
|
-
// raw selectors (params.*), not the resolved structured endpoints.
|
|
227
|
+
// raw selectors (params.*), not the resolved structured endpoints. Under
|
|
228
|
+
// dryRun the byRef fn validates the prospective relations + dedup-checks
|
|
229
|
+
// without writing (TASK-010 shared preview path).
|
|
150
230
|
const { appended } = appendRelationByRef(cwd, {
|
|
151
231
|
parent: params.parent,
|
|
152
232
|
child: params.child,
|
|
153
233
|
relation_type: params.relation_type,
|
|
154
234
|
...(params.ordinal !== undefined ? { ordinal: params.ordinal } : {}),
|
|
155
|
-
});
|
|
235
|
+
}, ctx, { dryRun: params.dryRun });
|
|
156
236
|
const ordinalNote = params.ordinal !== undefined ? ` (ordinal ${params.ordinal})` : "";
|
|
237
|
+
if (params.dryRun) {
|
|
238
|
+
return appended
|
|
239
|
+
? `would append relation ${params.parent} -[${params.relation_type}]-> ${params.child}${ordinalNote}`
|
|
240
|
+
: `would no-op (duplicate): relation ${params.parent} -[${params.relation_type}]-> ${params.child}`;
|
|
241
|
+
}
|
|
157
242
|
return appended
|
|
158
243
|
? `Appended relation ${params.parent} -[${params.relation_type}]-> ${params.child}${ordinalNote}`
|
|
159
244
|
: `Relation ${params.parent} -[${params.relation_type}]-> ${params.child} already exists — no-op`;
|
|
160
245
|
},
|
|
161
246
|
},
|
|
247
|
+
{
|
|
248
|
+
name: "remove-relation",
|
|
249
|
+
label: "Remove Relation",
|
|
250
|
+
description: "Remove the single closure-table relation (edge) matching parent+child+relation_type from relations.json. " +
|
|
251
|
+
"Matches on the SAME (parent, child, relation_type) dedup identity append-relation uses, so it is the symmetric " +
|
|
252
|
+
"inverse of append-relation (ordinal is NOT part of identity). An absent edge is an idempotent no-op. " +
|
|
253
|
+
"Reference integrity is NOT checked here — run context-validate after if the removal changes resolvability.",
|
|
254
|
+
promptSnippet: "Remove a relation/edge between two items (the inverse of append-relation)",
|
|
255
|
+
parameters: Type.Object({
|
|
256
|
+
parent: Type.String({ description: "Canonical id (or lens bin name) of the parent endpoint" }),
|
|
257
|
+
child: Type.String({ description: "Canonical id of the child endpoint" }),
|
|
258
|
+
relation_type: Type.String({
|
|
259
|
+
description: "Registered relation_type canonical_id / hierarchy edge type / lens id",
|
|
260
|
+
}),
|
|
261
|
+
dryRun: Type.Optional(Type.Boolean({ description: "Preview without writing relations.json" })),
|
|
262
|
+
}),
|
|
263
|
+
surface: "use",
|
|
264
|
+
run(cwd, params, ctx) {
|
|
265
|
+
// Cycle-5 porcelain: STRING selectors are resolved to structured
|
|
266
|
+
// EdgeEndpoints, then matched on the identityKey dedup identity. Messaging
|
|
267
|
+
// uses the raw selectors (params.*), not the resolved structured endpoints.
|
|
268
|
+
// Under dryRun the byRef fn validates the prospective post-removal
|
|
269
|
+
// relations + match-checks without writing (TASK-010 shared preview path).
|
|
270
|
+
const { removed } = removeRelationByRef(cwd, { parent: params.parent, child: params.child, relation_type: params.relation_type }, ctx, { dryRun: params.dryRun });
|
|
271
|
+
if (params.dryRun) {
|
|
272
|
+
return removed
|
|
273
|
+
? `would remove relation ${params.parent} -[${params.relation_type}]-> ${params.child}`
|
|
274
|
+
: `would no-op (no matching relation): ${params.parent} -[${params.relation_type}]-> ${params.child}`;
|
|
275
|
+
}
|
|
276
|
+
return removed
|
|
277
|
+
? `Removed relation ${params.parent} -[${params.relation_type}]-> ${params.child}`
|
|
278
|
+
: `Relation ${params.parent} -[${params.relation_type}]-> ${params.child} — no matching relation — no-op`;
|
|
279
|
+
},
|
|
280
|
+
},
|
|
281
|
+
{
|
|
282
|
+
name: "replace-relation",
|
|
283
|
+
label: "Replace Relation",
|
|
284
|
+
description: "Atomically replace one closure-table relation with another in a SINGLE write (no half-state: the old edge and " +
|
|
285
|
+
"the new edge never coexist on disk). The old edge is matched on the (parent, child, relation_type) dedup identity; " +
|
|
286
|
+
"the new edge is written with its optional ordinal. If the old edge is absent the call is effectively an append of " +
|
|
287
|
+
"the new edge. Reference integrity is NOT checked here — run context-validate after.",
|
|
288
|
+
promptSnippet: "Atomically swap one relation/edge for another in a single write",
|
|
289
|
+
parameters: Type.Object({
|
|
290
|
+
old_parent: Type.String({ description: "Parent endpoint selector of the edge to remove" }),
|
|
291
|
+
old_child: Type.String({ description: "Child endpoint selector of the edge to remove" }),
|
|
292
|
+
old_relation_type: Type.String({ description: "relation_type of the edge to remove" }),
|
|
293
|
+
parent: Type.String({ description: "Parent endpoint selector of the replacement edge" }),
|
|
294
|
+
child: Type.String({ description: "Child endpoint selector of the replacement edge" }),
|
|
295
|
+
relation_type: Type.String({ description: "relation_type of the replacement edge" }),
|
|
296
|
+
ordinal: Type.Optional(Type.Integer({ description: "Optional sibling-ordering within (parent, relation_type) for the new edge" })),
|
|
297
|
+
dryRun: Type.Optional(Type.Boolean({ description: "Preview without writing relations.json" })),
|
|
298
|
+
}),
|
|
299
|
+
surface: "use",
|
|
300
|
+
run(cwd, params, ctx) {
|
|
301
|
+
// Under dryRun the byRef fn validates the prospective post-replace
|
|
302
|
+
// relations and computes the same removed/replaced would-decisions
|
|
303
|
+
// without writing (TASK-010 shared preview path).
|
|
304
|
+
const { replaced, removed } = replaceRelationByRef(cwd, {
|
|
305
|
+
old: { parent: params.old_parent, child: params.old_child, relation_type: params.old_relation_type },
|
|
306
|
+
new: {
|
|
307
|
+
parent: params.parent,
|
|
308
|
+
child: params.child,
|
|
309
|
+
relation_type: params.relation_type,
|
|
310
|
+
...(params.ordinal !== undefined ? { ordinal: params.ordinal } : {}),
|
|
311
|
+
},
|
|
312
|
+
}, ctx, { dryRun: params.dryRun });
|
|
313
|
+
const ordinalNote = params.ordinal !== undefined ? ` (ordinal ${params.ordinal})` : "";
|
|
314
|
+
const oldDesc = `${params.old_parent} -[${params.old_relation_type}]-> ${params.old_child}`;
|
|
315
|
+
const newDesc = `${params.parent} -[${params.relation_type}]-> ${params.child}${ordinalNote}`;
|
|
316
|
+
if (params.dryRun) {
|
|
317
|
+
if (!removed && !replaced) {
|
|
318
|
+
return `would no-op — old edge ${oldDesc} absent and new edge ${newDesc} already present`;
|
|
319
|
+
}
|
|
320
|
+
if (!removed) {
|
|
321
|
+
return `would append new relation ${newDesc} (old ${oldDesc} absent)`;
|
|
322
|
+
}
|
|
323
|
+
if (!replaced) {
|
|
324
|
+
return `would remove relation ${oldDesc}; new relation ${newDesc} already present (no duplicate written)`;
|
|
325
|
+
}
|
|
326
|
+
return `would replace relation ${oldDesc} with ${newDesc}`;
|
|
327
|
+
}
|
|
328
|
+
if (!removed && !replaced) {
|
|
329
|
+
return `Replace relation no-op — old edge ${oldDesc} absent and new edge ${newDesc} already present`;
|
|
330
|
+
}
|
|
331
|
+
if (!removed) {
|
|
332
|
+
return `Old relation ${oldDesc} absent — appended new relation ${newDesc}`;
|
|
333
|
+
}
|
|
334
|
+
if (!replaced) {
|
|
335
|
+
return `Removed relation ${oldDesc}; new relation ${newDesc} already present (no duplicate written)`;
|
|
336
|
+
}
|
|
337
|
+
return `Replaced relation ${oldDesc} with ${newDesc}`;
|
|
338
|
+
},
|
|
339
|
+
},
|
|
340
|
+
{
|
|
341
|
+
name: "append-relations",
|
|
342
|
+
label: "Append Relations (bulk)",
|
|
343
|
+
description: "Append MANY closure-table relations to relations.json in a single write. Each edge is an object " +
|
|
344
|
+
"{ parent, child, relation_type, ordinal? }. Per-(parent, child, relation_type) duplicates are skipped (against " +
|
|
345
|
+
"on-disk edges AND earlier edges in the same batch). Returns appended/skipped counts. Reference integrity is NOT " +
|
|
346
|
+
"checked here — run context-validate after. Creates relations.json if absent.",
|
|
347
|
+
promptSnippet: "Create many relations/edges between items in one write",
|
|
348
|
+
parameters: Type.Object({
|
|
349
|
+
edges: Type.Unknown({
|
|
350
|
+
description: "JSON array of { parent, child, relation_type, ordinal? } selector objects (parent/child are id/lens-bin selectors)",
|
|
351
|
+
}),
|
|
352
|
+
dryRun: Type.Optional(Type.Boolean({ description: "Preview without writing relations.json" })),
|
|
353
|
+
}),
|
|
354
|
+
surface: "use",
|
|
355
|
+
run(cwd, params, ctx) {
|
|
356
|
+
// Type.Unknown() params may arrive as JSON strings — parse if needed.
|
|
357
|
+
let edges = params.edges;
|
|
358
|
+
if (typeof edges === "string") {
|
|
359
|
+
try {
|
|
360
|
+
edges = JSON.parse(edges);
|
|
361
|
+
}
|
|
362
|
+
catch {
|
|
363
|
+
throw new Error(`edges parameter must be a JSON array, got unparseable string`);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
if (!Array.isArray(edges)) {
|
|
367
|
+
throw new Error(`edges parameter must be a JSON array of { parent, child, relation_type, ordinal? } objects`);
|
|
368
|
+
}
|
|
369
|
+
// Under dryRun the byRef fn replays the on-disk + in-batch dedup and
|
|
370
|
+
// validates the prospective relations without writing (TASK-010 shared
|
|
371
|
+
// preview path).
|
|
372
|
+
const { appended, skipped } = appendRelationsByRef(cwd, edges, ctx, { dryRun: params.dryRun });
|
|
373
|
+
return params.dryRun
|
|
374
|
+
? `would append ${appended}, skip ${skipped} (duplicates)`
|
|
375
|
+
: `appended ${appended}, skipped ${skipped} (duplicates)`;
|
|
376
|
+
},
|
|
377
|
+
},
|
|
378
|
+
{
|
|
379
|
+
name: "upsert-block-item",
|
|
380
|
+
label: "Upsert Block Item",
|
|
381
|
+
description: "Append-or-replace an item in a project block array by id: if an item with the same idField value exists it is " +
|
|
382
|
+
"REPLACED (full-shape replacement, not shallow-merge — use update-block-item for merge); otherwise the item is " +
|
|
383
|
+
"appended. Schema validation is automatic. idField defaults to 'id'.",
|
|
384
|
+
promptSnippet: "Append-or-replace a full block item by id (replacement, not merge)",
|
|
385
|
+
parameters: Type.Object({
|
|
386
|
+
block: Type.String({ description: "Block name (e.g., 'issues', 'decisions')" }),
|
|
387
|
+
arrayKey: Type.String({ description: "Array key in the block (e.g., 'issues', 'decisions')" }),
|
|
388
|
+
item: Type.Unknown({ description: "Full item object to upsert — must conform to block schema" }),
|
|
389
|
+
idField: Type.Optional(Type.String({ description: "Field used as the upsert key (default 'id')" })),
|
|
390
|
+
dryRun: Type.Optional(Type.Boolean({ description: "Preview the upsert without writing" })),
|
|
391
|
+
}),
|
|
392
|
+
surface: "use",
|
|
393
|
+
run(cwd, params, ctx) {
|
|
394
|
+
// Type.Unknown() params may arrive as JSON strings — parse if needed.
|
|
395
|
+
if (typeof params.item === "string") {
|
|
396
|
+
try {
|
|
397
|
+
params.item = JSON.parse(params.item);
|
|
398
|
+
}
|
|
399
|
+
catch {
|
|
400
|
+
throw new Error(`item parameter must be a JSON object, got unparseable string`);
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
const idField = params.idField ?? "id";
|
|
404
|
+
// Under dryRun upsertItemInBlock computes mode + builds + validates the prospective
|
|
405
|
+
// whole block, writing nothing (TASK-011 shared preview path).
|
|
406
|
+
const { mode } = upsertItemInBlock(cwd, params.block, params.arrayKey, params.item, idField, ctx, {
|
|
407
|
+
dryRun: params.dryRun,
|
|
408
|
+
});
|
|
409
|
+
const idVal = params.item?.[idField];
|
|
410
|
+
const idDesc = idVal !== undefined ? ` '${idVal}'` : "";
|
|
411
|
+
return params.dryRun
|
|
412
|
+
? `would upsert item${idDesc} (${mode}) in ${params.block}.${params.arrayKey}`
|
|
413
|
+
: `Upserted item${idDesc} (${mode}) to ${params.block}.${params.arrayKey}`;
|
|
414
|
+
},
|
|
415
|
+
},
|
|
162
416
|
{
|
|
163
417
|
name: "promote-item",
|
|
164
418
|
label: "Promote Item",
|
|
@@ -180,17 +434,26 @@ export const ops = [
|
|
|
180
434
|
}, { description: "DispatchContext.writer per pi-context/src/dispatch-context.ts." }),
|
|
181
435
|
}),
|
|
182
436
|
surface: "use",
|
|
183
|
-
run(cwd, params) {
|
|
184
|
-
|
|
185
|
-
|
|
437
|
+
run(cwd, params, ctx) {
|
|
438
|
+
// The DispatchContext now arrives via the op contract — registerAll
|
|
439
|
+
// (in-pi) builds it from the auth-gate-stamped `params.writer`, and the
|
|
440
|
+
// CLI builds it from its resolved identity. The schema `writer` field is
|
|
441
|
+
// retained (the in-pi auth-gate stamps it), but lineage attestation reads
|
|
442
|
+
// the contract ctx, not params.writer.
|
|
443
|
+
if (!ctx?.writer) {
|
|
444
|
+
throw new Error("promote-item: a DispatchContext writer is required.");
|
|
186
445
|
}
|
|
187
446
|
const result = promoteItem(cwd, {
|
|
188
447
|
source: params.source,
|
|
189
448
|
destinationSubstrate: params.destinationSubstrate,
|
|
190
449
|
...(params.newRefname !== undefined ? { newRefname: params.newRefname } : {}),
|
|
191
450
|
...(params.dryRun !== undefined ? { dryRun: params.dryRun } : {}),
|
|
192
|
-
},
|
|
193
|
-
|
|
451
|
+
}, ctx);
|
|
452
|
+
// TASK-013 / FGAP-015: route through {read} so the embedded
|
|
453
|
+
// ResolvedRef.loc.item is bounded at the 50KB cap; over-cap fails closed
|
|
454
|
+
// with metadata. Under-cap text stays the same JSON (renderReadText
|
|
455
|
+
// under-cap returns JSON.stringify(serialized, null, 2) with no footer).
|
|
456
|
+
return { read: structureForRead(result, { whole: true, label: "promote-item result" }) };
|
|
194
457
|
},
|
|
195
458
|
},
|
|
196
459
|
{
|
|
@@ -208,7 +471,7 @@ export const ops = [
|
|
|
208
471
|
item: Type.Unknown({ description: "Item object to append to the nested array — must conform to schema" }),
|
|
209
472
|
}),
|
|
210
473
|
surface: "use",
|
|
211
|
-
run(cwd, params) {
|
|
474
|
+
run(cwd, params, ctx) {
|
|
212
475
|
if (typeof params.item === "string") {
|
|
213
476
|
try {
|
|
214
477
|
params.item = JSON.parse(params.item);
|
|
@@ -219,7 +482,7 @@ export const ops = [
|
|
|
219
482
|
}
|
|
220
483
|
const matchEntries = Object.entries(params.match);
|
|
221
484
|
const predicate = (i) => matchEntries.every(([k, v]) => i[k] === v);
|
|
222
|
-
appendToNestedArray(cwd, params.block, params.arrayKey, predicate, params.nestedKey, params.item);
|
|
485
|
+
appendToNestedArray(cwd, params.block, params.arrayKey, predicate, params.nestedKey, params.item, ctx);
|
|
223
486
|
const matchDesc = matchEntries.map(([k, v]) => `${k}=${v}`).join(", ");
|
|
224
487
|
const id = params.item?.id ? ` '${params.item.id}'` : "";
|
|
225
488
|
return `Appended item${id} to ${params.block}.${params.arrayKey}[${matchDesc}].${params.nestedKey}`;
|
|
@@ -245,7 +508,7 @@ export const ops = [
|
|
|
245
508
|
}),
|
|
246
509
|
}),
|
|
247
510
|
surface: "use",
|
|
248
|
-
run(cwd, params) {
|
|
511
|
+
run(cwd, params, ctx) {
|
|
249
512
|
if (Object.keys(params.updates).length === 0) {
|
|
250
513
|
throw new Error("No fields to update — updates parameter is empty");
|
|
251
514
|
}
|
|
@@ -253,7 +516,7 @@ export const ops = [
|
|
|
253
516
|
const nestedEntries = Object.entries(params.nestedMatch);
|
|
254
517
|
const parentPred = (i) => parentEntries.every(([k, v]) => i[k] === v);
|
|
255
518
|
const nestedPred = (i) => nestedEntries.every(([k, v]) => i[k] === v);
|
|
256
|
-
updateNestedArrayItem(cwd, params.block, params.arrayKey, parentPred, params.nestedKey, nestedPred, params.updates);
|
|
519
|
+
updateNestedArrayItem(cwd, params.block, params.arrayKey, parentPred, params.nestedKey, nestedPred, params.updates, ctx);
|
|
257
520
|
const parentDesc = parentEntries.map(([k, v]) => `${k}=${v}`).join(", ");
|
|
258
521
|
const nestedDesc = nestedEntries.map(([k, v]) => `${k}=${v}`).join(", ");
|
|
259
522
|
return `Updated nested item (${nestedDesc}) in ${params.block}.${params.arrayKey}[${parentDesc}].${params.nestedKey}: ${Object.keys(params.updates).join(", ")}`;
|
|
@@ -270,10 +533,10 @@ export const ops = [
|
|
|
270
533
|
match: Type.Record(Type.String(), Type.Unknown(), { description: "Fields to match (e.g., { id: 'ISSUE-NNN' })" }),
|
|
271
534
|
}),
|
|
272
535
|
surface: "use",
|
|
273
|
-
run(cwd, params) {
|
|
536
|
+
run(cwd, params, ctx) {
|
|
274
537
|
const matchEntries = Object.entries(params.match);
|
|
275
538
|
const predicate = (i) => matchEntries.every(([k, v]) => i[k] === v);
|
|
276
|
-
const result = removeFromBlock(cwd, params.block, params.arrayKey, predicate);
|
|
539
|
+
const result = removeFromBlock(cwd, params.block, params.arrayKey, predicate, ctx);
|
|
277
540
|
const matchDesc = matchEntries.map(([k, v]) => `${k}=${v}`).join(", ");
|
|
278
541
|
return `Removed ${result.removed} item(s) matching (${matchDesc}) from ${params.block}.${params.arrayKey}`;
|
|
279
542
|
},
|
|
@@ -295,12 +558,12 @@ export const ops = [
|
|
|
295
558
|
}),
|
|
296
559
|
}),
|
|
297
560
|
surface: "use",
|
|
298
|
-
run(cwd, params) {
|
|
561
|
+
run(cwd, params, ctx) {
|
|
299
562
|
const parentEntries = Object.entries(params.match);
|
|
300
563
|
const nestedEntries = Object.entries(params.nestedMatch);
|
|
301
564
|
const parentPred = (i) => parentEntries.every(([k, v]) => i[k] === v);
|
|
302
565
|
const nestedPred = (i) => nestedEntries.every(([k, v]) => i[k] === v);
|
|
303
|
-
const result = removeFromNestedArray(cwd, params.block, params.arrayKey, parentPred, params.nestedKey, nestedPred);
|
|
566
|
+
const result = removeFromNestedArray(cwd, params.block, params.arrayKey, parentPred, params.nestedKey, nestedPred, ctx);
|
|
304
567
|
const parentDesc = parentEntries.map(([k, v]) => `${k}=${v}`).join(", ");
|
|
305
568
|
const nestedDesc = nestedEntries.map(([k, v]) => `${k}=${v}`).join(", ");
|
|
306
569
|
return `Removed ${result.removed} nested item(s) matching (${nestedDesc}) from ${params.block}.${params.arrayKey}[${parentDesc}].${params.nestedKey}`;
|
|
@@ -317,8 +580,8 @@ export const ops = [
|
|
|
317
580
|
surface: "use",
|
|
318
581
|
run(cwd, params) {
|
|
319
582
|
const result = readBlockDir(cwd, params.subdir);
|
|
320
|
-
const
|
|
321
|
-
return
|
|
583
|
+
const read = structureForRead(result, { label: `<substrate-dir>/${params.subdir}/` });
|
|
584
|
+
return { read };
|
|
322
585
|
},
|
|
323
586
|
},
|
|
324
587
|
{
|
|
@@ -332,7 +595,7 @@ export const ops = [
|
|
|
332
595
|
surface: "use",
|
|
333
596
|
run(cwd, params) {
|
|
334
597
|
const result = readBlock(cwd, params.block);
|
|
335
|
-
const
|
|
598
|
+
const read = structureForRead(result, {
|
|
336
599
|
label: `<substrate-dir>/${params.block}.json`,
|
|
337
600
|
overCapDirective: {
|
|
338
601
|
tool: "read-block-page",
|
|
@@ -340,7 +603,7 @@ export const ops = [
|
|
|
340
603
|
hint: "or read-block-item with id=<id>",
|
|
341
604
|
},
|
|
342
605
|
});
|
|
343
|
-
return
|
|
606
|
+
return { read };
|
|
344
607
|
},
|
|
345
608
|
},
|
|
346
609
|
{
|
|
@@ -354,9 +617,9 @@ export const ops = [
|
|
|
354
617
|
}),
|
|
355
618
|
surface: "use",
|
|
356
619
|
authGated: true,
|
|
357
|
-
run(cwd, params) {
|
|
620
|
+
run(cwd, params, ctx) {
|
|
358
621
|
const data = typeof params.data === "string" ? JSON.parse(params.data) : params.data;
|
|
359
|
-
writeBlock(cwd, params.block, data);
|
|
622
|
+
writeBlock(cwd, params.block, data, ctx);
|
|
360
623
|
return `Wrote block '${params.block}' successfully`;
|
|
361
624
|
},
|
|
362
625
|
},
|
|
@@ -369,7 +632,7 @@ export const ops = [
|
|
|
369
632
|
surface: "use",
|
|
370
633
|
run(cwd, _params) {
|
|
371
634
|
const result = contextState(cwd);
|
|
372
|
-
return
|
|
635
|
+
return { json: result };
|
|
373
636
|
},
|
|
374
637
|
},
|
|
375
638
|
{
|
|
@@ -381,7 +644,7 @@ export const ops = [
|
|
|
381
644
|
surface: "use",
|
|
382
645
|
run(cwd, _params) {
|
|
383
646
|
const result = validateContext(cwd);
|
|
384
|
-
return
|
|
647
|
+
return { json: result };
|
|
385
648
|
},
|
|
386
649
|
},
|
|
387
650
|
{
|
|
@@ -410,10 +673,10 @@ export const ops = [
|
|
|
410
673
|
if (!entry.found) {
|
|
411
674
|
return `read-config: entry not found in ${params.registry} — ${entry.resolved}`;
|
|
412
675
|
}
|
|
413
|
-
const
|
|
414
|
-
return
|
|
676
|
+
const read = structureForRead(entry.value, { label: `config.${params.registry}.${params.id}` });
|
|
677
|
+
return { read };
|
|
415
678
|
}
|
|
416
|
-
const
|
|
679
|
+
const read = structureForRead(reg.value, {
|
|
417
680
|
label: `config.${params.registry}`,
|
|
418
681
|
overCapDirective: {
|
|
419
682
|
tool: "read-config",
|
|
@@ -421,17 +684,17 @@ export const ops = [
|
|
|
421
684
|
hint: "add id=<entry canonical_id>",
|
|
422
685
|
},
|
|
423
686
|
});
|
|
424
|
-
return
|
|
687
|
+
return { read };
|
|
425
688
|
}
|
|
426
689
|
const result = { config, configPath };
|
|
427
|
-
const
|
|
690
|
+
const read = structureForRead(result, {
|
|
428
691
|
label: configPath ?? "config.json",
|
|
429
692
|
overCapDirective: {
|
|
430
693
|
tool: "read-config",
|
|
431
694
|
hint: "registry=<name> (block_kinds|relation_types|lenses|invariants|…)",
|
|
432
695
|
},
|
|
433
696
|
});
|
|
434
|
-
return
|
|
697
|
+
return { read };
|
|
435
698
|
},
|
|
436
699
|
},
|
|
437
700
|
{
|
|
@@ -467,8 +730,8 @@ export const ops = [
|
|
|
467
730
|
if (tool === undefined) {
|
|
468
731
|
return `list-tools: tool not found — name=${params.name}`;
|
|
469
732
|
}
|
|
470
|
-
const
|
|
471
|
-
return
|
|
733
|
+
const read = structureForRead(tool, { label: `tool ${params.name}` });
|
|
734
|
+
return { read };
|
|
472
735
|
}
|
|
473
736
|
// Default: compact index (FGAP-101) — name + param count + one-line description.
|
|
474
737
|
const index = all.map((t) => {
|
|
@@ -482,11 +745,11 @@ export const ops = [
|
|
|
482
745
|
// The compact index is one line per tool — small enough to serialize whole
|
|
483
746
|
// (no paging); keep the wrapper fields (active/total) on the result object.
|
|
484
747
|
const result = { tools: index, active, total: all.length, activeCount: active.length };
|
|
485
|
-
const
|
|
748
|
+
const read = structureForRead(result, {
|
|
486
749
|
label: "tool index — pass name= for detail",
|
|
487
750
|
overCapDirective: { tool: "list-tools", hint: "name=<tool>" },
|
|
488
751
|
});
|
|
489
|
-
return
|
|
752
|
+
return { read };
|
|
490
753
|
},
|
|
491
754
|
},
|
|
492
755
|
{
|
|
@@ -502,13 +765,13 @@ export const ops = [
|
|
|
502
765
|
// Package-intrinsic: the catalog reads the extension's bundled samples
|
|
503
766
|
// directory, not the project substrate — cwd is unused.
|
|
504
767
|
const catalog = samplesCatalog(params.kind ? { kind: params.kind } : undefined);
|
|
505
|
-
const
|
|
768
|
+
const read = structureForRead(catalog, {
|
|
506
769
|
label: params.kind ? `samples kind=${params.kind}` : "samples catalog",
|
|
507
770
|
// Whole catalog → narrow by kind; a single kind has no finer
|
|
508
771
|
// addressing (edge → head-leading marker, no directive).
|
|
509
772
|
...(params.kind ? {} : { overCapDirective: { tool: "read-samples-catalog", hint: "kind=<canonical_id>" } }),
|
|
510
773
|
});
|
|
511
|
-
return
|
|
774
|
+
return { read };
|
|
512
775
|
},
|
|
513
776
|
},
|
|
514
777
|
{
|
|
@@ -520,19 +783,19 @@ export const ops = [
|
|
|
520
783
|
surface: "use",
|
|
521
784
|
run(cwd, _params) {
|
|
522
785
|
const state = currentState(cwd);
|
|
523
|
-
return
|
|
786
|
+
return { json: state };
|
|
524
787
|
},
|
|
525
788
|
},
|
|
526
789
|
{
|
|
527
790
|
name: "context-bootstrap-state",
|
|
528
791
|
label: "Context Bootstrap State",
|
|
529
|
-
description: "Derive the substrate bootstrap state for the cwd, purely from the filesystem: 'no-pointer' | 'no-config' | 'not-installed' | 'ready', plus the resolved contextDir and any declared-but-unmaterialized installed assets. Unlike every other tool, this NEVER throws on an un-bootstrapped substrate — it returns 'no-pointer' so you can detect a fresh substrate and tell the user to run /context init <substrate-dir> → /context accept-all → /context install (bootstrap requires user authorization via interactive confirmation). No writes.",
|
|
530
|
-
promptSnippet: "Derive substrate bootstrap state — no-pointer | no-config | not-installed | ready (never throws pre-bootstrap)",
|
|
792
|
+
description: "Derive the substrate bootstrap state for the cwd, purely from the filesystem: 'no-pointer' | 'no-config' | 'skeleton' | 'not-installed' | 'ready', plus the resolved contextDir and any declared-but-unmaterialized installed assets. Bootstrap (/context init or /context switch -c <new-dir>) now writes a minimal schema-valid config empty of vocabulary, so a freshly-bootstrapped substrate lands at 'skeleton' — onward via /context accept-all (adopt the packaged catalog, then /context install) OR amend-config / edit (build a custom vocabulary). Unlike every other tool, this NEVER throws on an un-bootstrapped substrate — it returns 'no-pointer' so you can detect a fresh substrate and tell the user to run /context init <substrate-dir> → /context accept-all → /context install (bootstrap requires user authorization via interactive confirmation). No writes.",
|
|
793
|
+
promptSnippet: "Derive substrate bootstrap state — no-pointer | no-config | skeleton | not-installed | ready (never throws pre-bootstrap)",
|
|
531
794
|
parameters: Type.Object({}),
|
|
532
795
|
surface: "use",
|
|
533
796
|
run(cwd, _params) {
|
|
534
797
|
const status = deriveBootstrapState(cwd);
|
|
535
|
-
return
|
|
798
|
+
return { json: status };
|
|
536
799
|
},
|
|
537
800
|
},
|
|
538
801
|
{
|
|
@@ -550,7 +813,7 @@ export const ops = [
|
|
|
550
813
|
authGated: true,
|
|
551
814
|
run(cwd, params) {
|
|
552
815
|
const report = renameCanonicalId(cwd, params.kind, params.oldId, params.newId, { dryRun: params.dryRun });
|
|
553
|
-
return
|
|
816
|
+
return { json: report };
|
|
554
817
|
},
|
|
555
818
|
},
|
|
556
819
|
{
|
|
@@ -580,7 +843,7 @@ export const ops = [
|
|
|
580
843
|
}),
|
|
581
844
|
surface: "use",
|
|
582
845
|
authGated: true,
|
|
583
|
-
run(cwd, params) {
|
|
846
|
+
run(cwd, params, ctx) {
|
|
584
847
|
// Type.Unknown() params may arrive as JSON strings. Parse if possible; on
|
|
585
848
|
// failure KEEP the raw string (valid for map-value registries whose value
|
|
586
849
|
// is a bare string, e.g. naming/display_strings/status_buckets).
|
|
@@ -593,10 +856,11 @@ export const ops = [
|
|
|
593
856
|
/* keep raw string — valid for map-value registries */
|
|
594
857
|
}
|
|
595
858
|
}
|
|
596
|
-
const result = amendConfigEntry(cwd, params.registry, params.operation, params.key, entry,
|
|
859
|
+
const result = amendConfigEntry(cwd, params.registry, params.operation, params.key, entry, ctx, {
|
|
597
860
|
dryRun: params.dryRun,
|
|
598
861
|
});
|
|
599
|
-
const
|
|
862
|
+
const pastTense = result.operation === "add" ? "added" : `${result.operation}d`;
|
|
863
|
+
const verb = result.modified ? (params.dryRun ? `would ${result.operation}` : pastTense) : "no-op";
|
|
600
864
|
return `amend-config: ${verb} ${result.registry}[${result.key}]`;
|
|
601
865
|
},
|
|
602
866
|
},
|
|
@@ -622,11 +886,11 @@ export const ops = [
|
|
|
622
886
|
if (!addr.found) {
|
|
623
887
|
return `read-schema: property not found — ${addr.resolved}`;
|
|
624
888
|
}
|
|
625
|
-
const
|
|
626
|
-
return
|
|
889
|
+
const read = structureForRead(addr.value, { label: `${params.schemaName} ${addr.resolved}` });
|
|
890
|
+
return { read };
|
|
627
891
|
}
|
|
628
892
|
const result = { schema, schemaPath: schemaPathStr };
|
|
629
|
-
const
|
|
893
|
+
const read = structureForRead(result, {
|
|
630
894
|
label: schemaPathStr,
|
|
631
895
|
overCapDirective: {
|
|
632
896
|
tool: "read-schema",
|
|
@@ -634,7 +898,7 @@ export const ops = [
|
|
|
634
898
|
hint: "path=<dotted json-path>",
|
|
635
899
|
},
|
|
636
900
|
});
|
|
637
|
-
return
|
|
901
|
+
return { read };
|
|
638
902
|
},
|
|
639
903
|
},
|
|
640
904
|
{
|
|
@@ -656,7 +920,7 @@ export const ops = [
|
|
|
656
920
|
}),
|
|
657
921
|
surface: "use",
|
|
658
922
|
authGated: true,
|
|
659
|
-
run(cwd, params) {
|
|
923
|
+
run(cwd, params, ctx) {
|
|
660
924
|
// Type.Unknown() params may arrive as JSON strings. Parse if possible; on
|
|
661
925
|
// failure KEEP the raw value (meta-validation rejects a non-object body).
|
|
662
926
|
let schema = params.schema;
|
|
@@ -668,7 +932,7 @@ export const ops = [
|
|
|
668
932
|
/* keep raw string — meta-validation will reject a non-object */
|
|
669
933
|
}
|
|
670
934
|
}
|
|
671
|
-
const result = writeSchemaChecked(cwd, params.schemaName, schema, params.operation,
|
|
935
|
+
const result = writeSchemaChecked(cwd, params.schemaName, schema, params.operation, ctx, { dryRun: params.dryRun });
|
|
672
936
|
const verb = result.written ? `${result.operation}d` : `would ${result.operation}`;
|
|
673
937
|
return `write-schema: ${verb} schema '${params.schemaName}' at ${result.schemaPath}`;
|
|
674
938
|
},
|
|
@@ -698,8 +962,8 @@ export const ops = [
|
|
|
698
962
|
}),
|
|
699
963
|
surface: "use",
|
|
700
964
|
authGated: true,
|
|
701
|
-
async run(cwd, params) {
|
|
702
|
-
const result = await writeSchemaMigrationExecute(cwd, params);
|
|
965
|
+
async run(cwd, params, ctx) {
|
|
966
|
+
const result = await writeSchemaMigrationExecute(cwd, params, ctx);
|
|
703
967
|
// writeSchemaMigrationExecute returns the uniform AgentToolResult; the
|
|
704
968
|
// op contract is the text payload, which registerAll re-wraps identically.
|
|
705
969
|
const part = result.content[0];
|
|
@@ -709,8 +973,8 @@ export const ops = [
|
|
|
709
973
|
{
|
|
710
974
|
name: "context-init",
|
|
711
975
|
label: "Context Init",
|
|
712
|
-
description: "Initialize the substrate dir
|
|
713
|
-
promptSnippet: "Initialize the substrate dir (bootstrap pointer + dirs
|
|
976
|
+
description: "Initialize the substrate dir: bootstrap pointer + dirs + a minimal schema-valid SKELETON config empty of vocabulary. Lands at the 'skeleton' bootstrap state — onward via accept-all (adopt the packaged catalog, then install) OR amend-config / edit (build a custom vocabulary).",
|
|
977
|
+
promptSnippet: "Initialize the substrate dir (bootstrap pointer + dirs + skeleton config; onward via accept-all OR amend-config/edit)",
|
|
714
978
|
parameters: Type.Object({
|
|
715
979
|
contextDir: Type.String({
|
|
716
980
|
description: "Substrate dir name (e.g. .context). Required — no default.",
|
|
@@ -720,13 +984,13 @@ export const ops = [
|
|
|
720
984
|
authGated: true,
|
|
721
985
|
run(cwd, params) {
|
|
722
986
|
const result = initProject(cwd, params.contextDir);
|
|
723
|
-
return
|
|
987
|
+
return { json: result };
|
|
724
988
|
},
|
|
725
989
|
},
|
|
726
990
|
{
|
|
727
991
|
name: "context-accept-all",
|
|
728
992
|
label: "Accept-All Conception",
|
|
729
|
-
description: "Adopt the canonical packaged conception (samples/conception.json) as this substrate's config.json (accept-all). Writes config only — run install after.
|
|
993
|
+
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
994
|
promptSnippet: "Adopt the canonical conception as config (accept-all)",
|
|
731
995
|
parameters: Type.Object({}),
|
|
732
996
|
surface: "use",
|
|
@@ -742,7 +1006,7 @@ export const ops = [
|
|
|
742
1006
|
}
|
|
743
1007
|
throw err;
|
|
744
1008
|
}
|
|
745
|
-
return
|
|
1009
|
+
return { json: result };
|
|
746
1010
|
},
|
|
747
1011
|
},
|
|
748
1012
|
{
|
|
@@ -755,7 +1019,7 @@ export const ops = [
|
|
|
755
1019
|
description: "Substrate dir name to switch to (e.g. '.context'). Required for default + create_new modes; ignored for to_previous mode.",
|
|
756
1020
|
}),
|
|
757
1021
|
create_new: Type.Optional(Type.Boolean({
|
|
758
|
-
description: "When true, bootstrap target_dir as a fresh substrate AND flip the pointer in one operation (parallel to 'git switch -c <branch>'). Default false (flip to existing substrate; fails if target_dir lacks config.json).",
|
|
1022
|
+
description: "When true, bootstrap target_dir as a fresh substrate (dirs + a minimal schema-valid SKELETON config empty of vocabulary — onward via accept-all OR amend/edit) AND flip the pointer in one operation (parallel to 'git switch -c <branch>'). Default false (flip to existing substrate; fails if target_dir lacks config.json).",
|
|
759
1023
|
})),
|
|
760
1024
|
to_previous: Type.Optional(Type.Boolean({
|
|
761
1025
|
description: "When true, flip the pointer back to its previous_contextDir (parallel to 'git switch -'). Requires the pointer to carry a previous_contextDir (a prior switch must have populated it). When true, target_dir is ignored.",
|
|
@@ -784,14 +1048,14 @@ export const ops = [
|
|
|
784
1048
|
try {
|
|
785
1049
|
if (params.to_previous === true) {
|
|
786
1050
|
const { from, to } = switchToPrevious(cwd, writerIdentity);
|
|
787
|
-
return
|
|
1051
|
+
return { json: { mode: "to_previous", from, to } };
|
|
788
1052
|
}
|
|
789
1053
|
if (params.create_new === true) {
|
|
790
1054
|
const { created } = switchAndCreate(cwd, params.target_dir, writerIdentity);
|
|
791
|
-
return
|
|
1055
|
+
return { json: { mode: "create_new", target_dir: params.target_dir, created } };
|
|
792
1056
|
}
|
|
793
1057
|
switchToExisting(cwd, params.target_dir, writerIdentity);
|
|
794
|
-
return
|
|
1058
|
+
return { json: { mode: "existing", target_dir: params.target_dir } };
|
|
795
1059
|
}
|
|
796
1060
|
catch (err) {
|
|
797
1061
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -808,7 +1072,7 @@ export const ops = [
|
|
|
808
1072
|
surface: "use",
|
|
809
1073
|
run(cwd, _params) {
|
|
810
1074
|
const subs = listSubstrates(cwd);
|
|
811
|
-
return
|
|
1075
|
+
return { json: subs };
|
|
812
1076
|
},
|
|
813
1077
|
},
|
|
814
1078
|
{
|
|
@@ -820,21 +1084,13 @@ export const ops = [
|
|
|
820
1084
|
target_dir: Type.String({
|
|
821
1085
|
description: "Substrate dir name to archive (e.g. '.project'). Refused if it is the active substrate.",
|
|
822
1086
|
}),
|
|
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
1087
|
}),
|
|
832
1088
|
surface: "use",
|
|
833
1089
|
authGated: true,
|
|
834
1090
|
run(cwd, params) {
|
|
835
1091
|
try {
|
|
836
1092
|
const { from, to } = archiveSubstrate(cwd, params.target_dir);
|
|
837
|
-
return
|
|
1093
|
+
return { json: { from, to } };
|
|
838
1094
|
}
|
|
839
1095
|
catch (err) {
|
|
840
1096
|
const msg = err instanceof Error ? err.message : String(err);
|
|
@@ -866,11 +1122,11 @@ export const ops = [
|
|
|
866
1122
|
op: params.op,
|
|
867
1123
|
value: params.value,
|
|
868
1124
|
});
|
|
869
|
-
const
|
|
1125
|
+
const read = structureForRead(result, {
|
|
870
1126
|
label: `${params.block} filtered`,
|
|
871
1127
|
overCapDirective: { tool: "read-block-page", hint: "or refine the predicate" },
|
|
872
1128
|
});
|
|
873
|
-
return
|
|
1129
|
+
return { read };
|
|
874
1130
|
},
|
|
875
1131
|
},
|
|
876
1132
|
{
|
|
@@ -884,7 +1140,17 @@ export const ops = [
|
|
|
884
1140
|
surface: "use",
|
|
885
1141
|
run(cwd, params) {
|
|
886
1142
|
const result = resolveItemById(cwd, params.id);
|
|
887
|
-
|
|
1143
|
+
// TASK-013 / FGAP-015: route through {read} so the embedded full
|
|
1144
|
+
// ItemLocation is bounded at the 50KB cap and over-cap fails closed with
|
|
1145
|
+
// a narrowing directive (mirrors read-block-item). `result` is
|
|
1146
|
+
// ItemLocation | null — structureForRead handles both.
|
|
1147
|
+
return {
|
|
1148
|
+
read: structureForRead(result, {
|
|
1149
|
+
whole: true,
|
|
1150
|
+
label: `resolve ${params.id}`,
|
|
1151
|
+
overCapDirective: { tool: "read-block-item", hint: "narrow to one block" },
|
|
1152
|
+
}),
|
|
1153
|
+
};
|
|
888
1154
|
},
|
|
889
1155
|
},
|
|
890
1156
|
{
|
|
@@ -901,8 +1167,8 @@ export const ops = [
|
|
|
901
1167
|
const result = readBlockItem(cwd, params.block, params.id);
|
|
902
1168
|
// whole: the item is already the addressed element — don't re-page its
|
|
903
1169
|
// intrinsic arrays; preserve the single-item|null output contract.
|
|
904
|
-
const
|
|
905
|
-
return
|
|
1170
|
+
const read = structureForRead(result, { whole: true, label: `${params.block} ${params.id}` });
|
|
1171
|
+
return { read };
|
|
906
1172
|
},
|
|
907
1173
|
},
|
|
908
1174
|
{
|
|
@@ -920,8 +1186,8 @@ export const ops = [
|
|
|
920
1186
|
const result = readBlockPage(cwd, params.block, { offset: params.offset, limit: params.limit });
|
|
921
1187
|
// whole: readBlockPage ALREADY paged — preserve the {items,total,hasMore}
|
|
922
1188
|
// output contract; do not let serializeForRead re-page the items array.
|
|
923
|
-
const
|
|
924
|
-
return
|
|
1189
|
+
const read = structureForRead(result, { whole: true, label: `${params.block} page` });
|
|
1190
|
+
return { read };
|
|
925
1191
|
},
|
|
926
1192
|
},
|
|
927
1193
|
{
|
|
@@ -956,14 +1222,14 @@ export const ops = [
|
|
|
956
1222
|
leftEndpoint: params.leftEndpoint,
|
|
957
1223
|
leftPredicate,
|
|
958
1224
|
});
|
|
959
|
-
const
|
|
1225
|
+
const read = structureForRead(result, {
|
|
960
1226
|
label: `${params.leftBlock} ⋈ ${params.rightBlock}`,
|
|
961
1227
|
overCapDirective: {
|
|
962
1228
|
tool: "join-blocks",
|
|
963
1229
|
hint: "refine the relation/field or pre-filter the left block",
|
|
964
1230
|
},
|
|
965
1231
|
});
|
|
966
|
-
return
|
|
1232
|
+
return { read };
|
|
967
1233
|
},
|
|
968
1234
|
},
|
|
969
1235
|
{
|
|
@@ -984,8 +1250,8 @@ export const ops = [
|
|
|
984
1250
|
obj[id] = loc;
|
|
985
1251
|
// whole: an id→location map keyed by arbitrary ids — not a pageable
|
|
986
1252
|
// collection; serialize the map verbatim.
|
|
987
|
-
const
|
|
988
|
-
return
|
|
1253
|
+
const read = structureForRead(obj, { whole: true, label: "resolved ids" });
|
|
1254
|
+
return { read };
|
|
989
1255
|
},
|
|
990
1256
|
},
|
|
991
1257
|
{
|
|
@@ -1000,8 +1266,8 @@ export const ops = [
|
|
|
1000
1266
|
}),
|
|
1001
1267
|
}),
|
|
1002
1268
|
surface: "use",
|
|
1003
|
-
run(cwd, params) {
|
|
1004
|
-
const result = completeTask(cwd, params.taskId, params.verificationId);
|
|
1269
|
+
run(cwd, params, ctx) {
|
|
1270
|
+
const result = completeTask(cwd, params.taskId, params.verificationId, ctx);
|
|
1005
1271
|
return `Task '${result.taskId}' completed (was '${result.previousStatus}'). Verification: ${result.verificationId} (${result.verificationStatus})`;
|
|
1006
1272
|
},
|
|
1007
1273
|
},
|
|
@@ -1014,7 +1280,7 @@ export const ops = [
|
|
|
1014
1280
|
surface: "use",
|
|
1015
1281
|
run(cwd, _params) {
|
|
1016
1282
|
const result = validateContextRelations(cwd);
|
|
1017
|
-
return
|
|
1283
|
+
return { json: result };
|
|
1018
1284
|
},
|
|
1019
1285
|
},
|
|
1020
1286
|
{
|
|
@@ -1028,8 +1294,8 @@ export const ops = [
|
|
|
1028
1294
|
surface: "use",
|
|
1029
1295
|
run(cwd, params) {
|
|
1030
1296
|
const result = edgesForLensByName(cwd, params.lensId);
|
|
1031
|
-
const
|
|
1032
|
-
return
|
|
1297
|
+
const read = structureForRead(result, { label: `edges for lens ${params.lensId}` });
|
|
1298
|
+
return { read };
|
|
1033
1299
|
},
|
|
1034
1300
|
},
|
|
1035
1301
|
{
|
|
@@ -1044,7 +1310,7 @@ export const ops = [
|
|
|
1044
1310
|
surface: "use",
|
|
1045
1311
|
run(cwd, params) {
|
|
1046
1312
|
const result = walkLensDescendants(cwd, params.parentId, params.relationType);
|
|
1047
|
-
return
|
|
1313
|
+
return { json: result };
|
|
1048
1314
|
},
|
|
1049
1315
|
},
|
|
1050
1316
|
{
|
|
@@ -1059,8 +1325,8 @@ export const ops = [
|
|
|
1059
1325
|
surface: "use",
|
|
1060
1326
|
run(cwd, params) {
|
|
1061
1327
|
const result = walkAncestorsByLens(cwd, params.itemId, params.relationType);
|
|
1062
|
-
const
|
|
1063
|
-
return
|
|
1328
|
+
const read = structureForRead(result, { label: `ancestors of ${params.itemId}` });
|
|
1329
|
+
return { read };
|
|
1064
1330
|
},
|
|
1065
1331
|
},
|
|
1066
1332
|
{
|
|
@@ -1077,8 +1343,8 @@ export const ops = [
|
|
|
1077
1343
|
surface: "use",
|
|
1078
1344
|
run(cwd, params) {
|
|
1079
1345
|
const result = findReferencesInRepo(cwd, params.itemId, params.direction);
|
|
1080
|
-
const
|
|
1081
|
-
return
|
|
1346
|
+
const read = structureForRead(result, { label: `edges on ${params.itemId}` });
|
|
1347
|
+
return { read };
|
|
1082
1348
|
},
|
|
1083
1349
|
},
|
|
1084
1350
|
{
|
|
@@ -1101,8 +1367,8 @@ export const ops = [
|
|
|
1101
1367
|
const result = gatherExecutionContext(cwd, params);
|
|
1102
1368
|
// whole: a structured ContextBundle (unit + perRelationType buckets) —
|
|
1103
1369
|
// preserve the bundle shape rather than paging any single inner array.
|
|
1104
|
-
const
|
|
1105
|
-
return
|
|
1370
|
+
const read = structureForRead(result, { whole: true, label: `bundle ${params.unitId}` });
|
|
1371
|
+
return { read };
|
|
1106
1372
|
},
|
|
1107
1373
|
},
|
|
1108
1374
|
{
|
|
@@ -1117,16 +1383,16 @@ export const ops = [
|
|
|
1117
1383
|
run(cwd, params) {
|
|
1118
1384
|
const view = loadRoadmap(cwd, params.roadmapId);
|
|
1119
1385
|
if ("error" in view) {
|
|
1120
|
-
const
|
|
1121
|
-
return
|
|
1386
|
+
const read = structureForRead(view, { whole: true, label: `roadmap ${params.roadmapId} (error)` });
|
|
1387
|
+
return { read };
|
|
1122
1388
|
}
|
|
1123
1389
|
// whole: a structured RoadmapView (phases + lens-views + rollups) — keep
|
|
1124
1390
|
// the view shape intact rather than paging an inner array.
|
|
1125
|
-
const
|
|
1391
|
+
const read = structureForRead(serializeRoadmapView(view), {
|
|
1126
1392
|
whole: true,
|
|
1127
1393
|
label: `roadmap ${params.roadmapId}`,
|
|
1128
1394
|
});
|
|
1129
|
-
return
|
|
1395
|
+
return { read };
|
|
1130
1396
|
},
|
|
1131
1397
|
},
|
|
1132
1398
|
{
|
|
@@ -1141,7 +1407,7 @@ export const ops = [
|
|
|
1141
1407
|
run(cwd, params) {
|
|
1142
1408
|
const view = loadRoadmap(cwd, params.roadmapId);
|
|
1143
1409
|
if ("error" in view) {
|
|
1144
|
-
return
|
|
1410
|
+
return { json: view };
|
|
1145
1411
|
}
|
|
1146
1412
|
const naming = loadContext(cwd).config?.naming;
|
|
1147
1413
|
return renderRoadmap(view, naming);
|
|
@@ -1161,7 +1427,7 @@ export const ops = [
|
|
|
1161
1427
|
const filtered = params.roadmapId
|
|
1162
1428
|
? result.issues.filter((i) => !i.roadmap_id || i.roadmap_id === params.roadmapId)
|
|
1163
1429
|
: result.issues;
|
|
1164
|
-
return
|
|
1430
|
+
return { json: { status: result.status, issues: filtered } };
|
|
1165
1431
|
},
|
|
1166
1432
|
},
|
|
1167
1433
|
{
|
|
@@ -1172,7 +1438,7 @@ export const ops = [
|
|
|
1172
1438
|
parameters: Type.Object({}),
|
|
1173
1439
|
surface: "use",
|
|
1174
1440
|
run(cwd, _params) {
|
|
1175
|
-
return
|
|
1441
|
+
return { json: listRoadmaps(cwd) };
|
|
1176
1442
|
},
|
|
1177
1443
|
},
|
|
1178
1444
|
];
|
|
@@ -1187,6 +1453,137 @@ export const ops = [
|
|
|
1187
1453
|
* point; this list is the source of pi-context's contribution to it.
|
|
1188
1454
|
*/
|
|
1189
1455
|
export const gatedTools = ops.filter((o) => o.authGated).map((o) => o.name);
|
|
1456
|
+
/**
|
|
1457
|
+
* The FGAP-009 non-exposure allowlist: every library write function that is
|
|
1458
|
+
* deliberately NOT op-backed, with the reason it is withheld. This is the
|
|
1459
|
+
* closure contract γ (TASK-008) WILL consume: γ's parity test — not yet written
|
|
1460
|
+
* (no executable parity test exists in β; β defines the contract, γ implements
|
|
1461
|
+
* the test against it) — WILL assert that EVERY library writer is either op-backed
|
|
1462
|
+
* (appears in {@link ops}, directly or transitively) OR named here, so a
|
|
1463
|
+
* newly-added library writer with neither an op nor an allowlist entry will fail
|
|
1464
|
+
* that test, keeping the op surface and the library write surface in lockstep.
|
|
1465
|
+
*
|
|
1466
|
+
* The `*ForDir` twins of op-backed writers (e.g. `appendRelationForDir`,
|
|
1467
|
+
* `writeRelationsForDir`, `upsertItemInBlockForDir`, `appendToBlockForDir`, …)
|
|
1468
|
+
* are NOT enumerated individually: each is the dir-targeted internal twin of a
|
|
1469
|
+
* cwd-form writer that IS op-backed, and is covered by that cwd-form op (the op
|
|
1470
|
+
* resolves the active substrate dir then delegates to the same shared
|
|
1471
|
+
* typed-file primitive the `*ForDir` twin calls). The contract is that γ's
|
|
1472
|
+
* parity test (TASK-008, not yet written) WILL treat a `*ForDir` writer as
|
|
1473
|
+
* covered when its cwd-form sibling is covered.
|
|
1474
|
+
*/
|
|
1475
|
+
export const INTENTIONALLY_UNEXPOSED_WRITERS = [
|
|
1476
|
+
{ libraryFn: "writeConfig", safeOp: "amend-config", reason: "scoped guarded config mutation" },
|
|
1477
|
+
{ libraryFn: "writeSchema", safeOp: "write-schema", reason: "raw bypasses the create/replace + migration check" },
|
|
1478
|
+
{ libraryFn: "updateSchema", safeOp: "write-schema", reason: "no mutator-scripting surface" },
|
|
1479
|
+
{ libraryFn: "writeBootstrapPointer", safeOp: "context-init", reason: "raw bypasses target validation" },
|
|
1480
|
+
{ libraryFn: "flipBootstrapPointer", safeOp: "context-switch", reason: "raw splits the safe switch" },
|
|
1481
|
+
{ libraryFn: "writeRegistry", reason: "internal; registry writes flow through registerSubstrate callers" },
|
|
1482
|
+
{
|
|
1483
|
+
libraryFn: "registerSubstrate",
|
|
1484
|
+
reason: "manual/foreign registration is the clone arc (DEC-0002); normal paths auto-register",
|
|
1485
|
+
},
|
|
1486
|
+
{
|
|
1487
|
+
libraryFn: "rollbackBlockFiles",
|
|
1488
|
+
reason: "workflow-executor transactional rollback (graduated-failure undo); internal recovery path with no operator-facing op by design",
|
|
1489
|
+
},
|
|
1490
|
+
];
|
|
1491
|
+
/**
|
|
1492
|
+
* The five mutually-exhaustive ways a library write function is COVERED by the
|
|
1493
|
+
* FGAP-009 op-surface ↔ library-write-surface parity contract. A writer that
|
|
1494
|
+
* matches NONE of these is a silent gap that γ's (TASK-008) parity test — once
|
|
1495
|
+
* written — MUST fail on. Coverage is the DISJUNCTION over these classes — a
|
|
1496
|
+
* writer needs ANY one, not all.
|
|
1497
|
+
*/
|
|
1498
|
+
export var CoverageClass;
|
|
1499
|
+
(function (CoverageClass) {
|
|
1500
|
+
/** An op's `run` calls the writer directly (e.g. `append-block-item` → `appendToBlock`). */
|
|
1501
|
+
CoverageClass["OpBackedDirect"] = "op-backed-direct";
|
|
1502
|
+
/**
|
|
1503
|
+
* An op's `run` reaches the writer TRANSITIVELY — through any helper / wrapper
|
|
1504
|
+
* chain, not just a direct call. Two sub-shapes both land here:
|
|
1505
|
+
* - `*ByRef` / SDK relation porcelain: the `remove-relation` / `replace-relation`
|
|
1506
|
+
* / `append-relations` ops call `removeRelationByRef` / `replaceRelationByRef`
|
|
1507
|
+
* / `appendRelationsByRef`, which call `writeRelations`.
|
|
1508
|
+
* - init / switch → internal-helper chains: `context-init` → `initProject` →
|
|
1509
|
+
* `writeSkeletonConfig`; `context-switch` → `switchToExisting` /
|
|
1510
|
+
* `switchAndCreate` → `reconcileActiveSubstrateRegistration`. The writer is
|
|
1511
|
+
* not a `*ByRef` porcelain and is not allowlisted, but it IS reachable from an
|
|
1512
|
+
* op's `run` via a helper the op calls.
|
|
1513
|
+
* Coverage condition: reachable from some op's `run` via any helper/wrapper chain.
|
|
1514
|
+
*/
|
|
1515
|
+
CoverageClass["OpBackedTransitive"] = "op-backed-transitive";
|
|
1516
|
+
/**
|
|
1517
|
+
* A `*ForDir` dir-targeted twin of a covered cwd-form writer (e.g.
|
|
1518
|
+
* `appendToBlockForDir` is the twin of the op-backed `appendToBlock`). Covered
|
|
1519
|
+
* by its cwd-form sibling — both delegate to the same shared typed-file
|
|
1520
|
+
* primitive; the cwd-form op resolves the active dir then calls it.
|
|
1521
|
+
*/
|
|
1522
|
+
CoverageClass["ForDirTwin"] = "for-dir-twin";
|
|
1523
|
+
/**
|
|
1524
|
+
* On {@link INTENTIONALLY_UNEXPOSED_WRITERS}: a raw write deliberately given NO
|
|
1525
|
+
* direct op (a scoped op supersedes it, or it is internal/foreign-only).
|
|
1526
|
+
*/
|
|
1527
|
+
CoverageClass["IntentionallyUnexposed"] = "intentionally-unexposed";
|
|
1528
|
+
/**
|
|
1529
|
+
* A block-api internal primitive below the op layer — the `*TypedFile` read/
|
|
1530
|
+
* write layer (`readTypedFile` / `writeTypedFile`), `prepareItemIdentityForWrite`,
|
|
1531
|
+
* and the identity / content-hash helpers. Never op-backed by design; the ops
|
|
1532
|
+
* compose over these.
|
|
1533
|
+
*/
|
|
1534
|
+
CoverageClass["InternalPrimitive"] = "internal-primitive";
|
|
1535
|
+
})(CoverageClass || (CoverageClass = {}));
|
|
1536
|
+
/**
|
|
1537
|
+
* The FGAP-009 coverage RULE, made explicit so γ (TASK-008) will import the
|
|
1538
|
+
* contract rather than re-derive it. A library write function is COVERED iff it
|
|
1539
|
+
* matches ANY clause below (the disjunction); a writer matching none is a silent
|
|
1540
|
+
* gap that γ's parity test — when written — MUST fail on. β fixes the contract
|
|
1541
|
+
* here; no executable parity test exists yet (that is γ).
|
|
1542
|
+
*
|
|
1543
|
+
* Why `writeConfig` is allowlisted but `writeRelations` is NOT — the distinction
|
|
1544
|
+
* a strict name-parity reading mis-saw as inconsistent:
|
|
1545
|
+
* - `writeConfig` has NO direct wholesale-config op. The scoped surface is
|
|
1546
|
+
* `amend-config` (one-entry-in-one-registry add/replace/remove, AJV-validated),
|
|
1547
|
+
* which deliberately does NOT expose a raw whole-config overwrite. So
|
|
1548
|
+
* `writeConfig` is `intentionally-unexposed` (the scoped op supersedes the raw
|
|
1549
|
+
* writer; a raw wholesale overwrite is withheld by design).
|
|
1550
|
+
* - `writeRelations` IS reached — transitively — by the relation ops:
|
|
1551
|
+
* `remove-relation` / `replace-relation` / `append-relations` call
|
|
1552
|
+
* `removeRelationByRef` / `replaceRelationByRef` / `appendRelationsByRef`, each
|
|
1553
|
+
* of which calls `writeRelations`. It is therefore `op-backed-transitive`
|
|
1554
|
+
* and needs NO allowlist entry. The asymmetry is real and correct: one writer
|
|
1555
|
+
* has an op path (via a helper/wrapper chain), the other does not.
|
|
1556
|
+
*
|
|
1557
|
+
* The `op-backed-transitive` clause covers BOTH the `*ByRef` relation porcelain
|
|
1558
|
+
* AND the init/switch → internal-helper chains: `writeSkeletonConfig` (reached
|
|
1559
|
+
* via `context-init` → `initProject`) and `reconcileActiveSubstrateRegistration`
|
|
1560
|
+
* (reached via `context-switch` → `switchToExisting` / `switchAndCreate`) are
|
|
1561
|
+
* neither `*ByRef` porcelain nor allowlisted, yet each is reachable from an op's
|
|
1562
|
+
* `run` through a helper that op calls — so each classifies cleanly as
|
|
1563
|
+
* `op-backed-transitive`, not as a gap.
|
|
1564
|
+
*/
|
|
1565
|
+
export const OP_COVERAGE_RULE = [
|
|
1566
|
+
{
|
|
1567
|
+
coverageClass: CoverageClass.OpBackedDirect,
|
|
1568
|
+
test: "an op's run() calls the writer directly",
|
|
1569
|
+
},
|
|
1570
|
+
{
|
|
1571
|
+
coverageClass: CoverageClass.OpBackedTransitive,
|
|
1572
|
+
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)",
|
|
1573
|
+
},
|
|
1574
|
+
{
|
|
1575
|
+
coverageClass: CoverageClass.ForDirTwin,
|
|
1576
|
+
test: "a *ForDir twin of a covered cwd-form writer (covered by its cwd-form sibling)",
|
|
1577
|
+
},
|
|
1578
|
+
{
|
|
1579
|
+
coverageClass: CoverageClass.IntentionallyUnexposed,
|
|
1580
|
+
test: "named on INTENTIONALLY_UNEXPOSED_WRITERS — a raw bypass with no direct op",
|
|
1581
|
+
},
|
|
1582
|
+
{
|
|
1583
|
+
coverageClass: CoverageClass.InternalPrimitive,
|
|
1584
|
+
test: "a block-api internal primitive below the op layer (the *TypedFile layer, prepareItemIdentityForWrite, identity / content-hash helpers)",
|
|
1585
|
+
},
|
|
1586
|
+
];
|
|
1190
1587
|
/**
|
|
1191
1588
|
* The factory PI handle captured at registerAll time. The list-tools op needs
|
|
1192
1589
|
* the introspection surface (getAllTools / getActiveTools) which lives on
|
|
@@ -1196,11 +1593,36 @@ export const gatedTools = ops.filter((o) => o.authGated).map((o) => o.name);
|
|
|
1196
1593
|
* threading the handle through every signature.
|
|
1197
1594
|
*/
|
|
1198
1595
|
let boundPi = null;
|
|
1596
|
+
/**
|
|
1597
|
+
* Build the DispatchContext threaded into an op's `run` from the in-pi tool
|
|
1598
|
+
* execute boundary (registerAll). Two derivation branches:
|
|
1599
|
+
*
|
|
1600
|
+
* - When `params.writer.user` is a non-empty string — the shape the
|
|
1601
|
+
* pi-agent-dispatch auth-gate stamps onto authGated op params on operator
|
|
1602
|
+
* confirm — the writer is a human identity. (The smuggle-ops promote-item /
|
|
1603
|
+
* write-schema-migration / context-switch carry a `writer` schema field
|
|
1604
|
+
* precisely so the gate has somewhere to stamp; this converts that field
|
|
1605
|
+
* into the contract ctx the op now consumes via its 3rd `run` arg.)
|
|
1606
|
+
* - Otherwise the writer is the running agent, identified by the active
|
|
1607
|
+
* model's id (`ExtensionContext.model.id`); falls back to "pi-agent" when
|
|
1608
|
+
* no model (or no id) is resolvable.
|
|
1609
|
+
*
|
|
1610
|
+
* Exported for unit testing — the two branches are asserted directly against
|
|
1611
|
+
* synthetic params + a minimal ExtensionContext-shaped object.
|
|
1612
|
+
*/
|
|
1613
|
+
export function buildDispatchContextFromExecute(params, extCtx) {
|
|
1614
|
+
const writerUser = params?.writer?.user;
|
|
1615
|
+
if (typeof writerUser === "string" && writerUser.length > 0) {
|
|
1616
|
+
return { writer: { kind: "human", user: writerUser } };
|
|
1617
|
+
}
|
|
1618
|
+
const modelId = extCtx.model?.id;
|
|
1619
|
+
return { writer: { kind: "agent", agent_id: modelId && modelId.length > 0 ? modelId : "pi-agent" } };
|
|
1620
|
+
}
|
|
1199
1621
|
/**
|
|
1200
1622
|
* 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
|
-
*
|
|
1623
|
+
* uniform wrapper around the op's run(): coerce params, build the attestation
|
|
1624
|
+
* DispatchContext from the auth-gate-stamped writer (human) or the running
|
|
1625
|
+
* model (agent), await run, place the returned string at content[0].text.
|
|
1204
1626
|
*/
|
|
1205
1627
|
export function registerAll(pi) {
|
|
1206
1628
|
boundPi = pi;
|
|
@@ -1212,9 +1634,10 @@ export function registerAll(pi) {
|
|
|
1212
1634
|
promptSnippet: op.promptSnippet,
|
|
1213
1635
|
parameters: op.parameters,
|
|
1214
1636
|
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
1637
|
+
const dctx = buildDispatchContextFromExecute(params, ctx);
|
|
1215
1638
|
return {
|
|
1216
1639
|
details: undefined,
|
|
1217
|
-
content: [{ type: "text", text: await op.run(ctx.cwd, params) }],
|
|
1640
|
+
content: [{ type: "text", text: renderOpResultText(await op.run(ctx.cwd, params, dctx)) }],
|
|
1218
1641
|
};
|
|
1219
1642
|
},
|
|
1220
1643
|
});
|