@davidorex/pi-context 0.26.0 → 0.28.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +71 -15
- package/README.md +71 -18
- package/dist/block-api.d.ts +165 -18
- package/dist/block-api.d.ts.map +1 -1
- package/dist/block-api.js +800 -57
- package/dist/block-api.js.map +1 -1
- package/dist/citation-rot-scanner.d.ts +80 -0
- package/dist/citation-rot-scanner.d.ts.map +1 -0
- package/dist/citation-rot-scanner.js +421 -0
- package/dist/citation-rot-scanner.js.map +1 -0
- package/dist/content-hash.d.ts +25 -0
- package/dist/content-hash.d.ts.map +1 -0
- package/dist/content-hash.js +62 -0
- package/dist/content-hash.js.map +1 -0
- package/dist/context-dir.d.ts +126 -1
- package/dist/context-dir.d.ts.map +1 -1
- package/dist/context-dir.js +208 -4
- package/dist/context-dir.js.map +1 -1
- package/dist/context-registry.d.ts +122 -0
- package/dist/context-registry.d.ts.map +1 -0
- package/dist/context-registry.js +189 -0
- package/dist/context-registry.js.map +1 -0
- package/dist/context-sdk.d.ts +179 -12
- package/dist/context-sdk.d.ts.map +1 -1
- package/dist/context-sdk.js +536 -101
- package/dist/context-sdk.js.map +1 -1
- package/dist/context.d.ts +222 -4
- package/dist/context.d.ts.map +1 -1
- package/dist/context.js +244 -56
- package/dist/context.js.map +1 -1
- package/dist/git-env.d.ts +2 -0
- package/dist/git-env.d.ts.map +1 -0
- package/dist/git-env.js +29 -0
- package/dist/git-env.js.map +1 -0
- package/dist/index.d.ts +124 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +440 -1125
- package/dist/index.js.map +1 -1
- package/dist/lens-view.d.ts.map +1 -1
- package/dist/lens-view.js +5 -3
- package/dist/lens-view.js.map +1 -1
- package/dist/migration-registry-loader.d.ts +121 -0
- package/dist/migration-registry-loader.d.ts.map +1 -0
- package/dist/migration-registry-loader.js +309 -0
- package/dist/migration-registry-loader.js.map +1 -0
- package/dist/migrations-store.d.ts +141 -0
- package/dist/migrations-store.d.ts.map +1 -0
- package/dist/migrations-store.js +206 -0
- package/dist/migrations-store.js.map +1 -0
- package/dist/object-store.d.ts +26 -0
- package/dist/object-store.d.ts.map +1 -0
- package/dist/object-store.js +104 -0
- package/dist/object-store.js.map +1 -0
- package/dist/ops-registry.d.ts +44 -0
- package/dist/ops-registry.d.ts.map +1 -0
- package/dist/ops-registry.js +1223 -0
- package/dist/ops-registry.js.map +1 -0
- package/dist/orientation.d.ts +16 -0
- package/dist/orientation.d.ts.map +1 -0
- package/dist/orientation.js +49 -0
- package/dist/orientation.js.map +1 -0
- package/dist/promote-item.d.ts +28 -0
- package/dist/promote-item.d.ts.map +1 -0
- package/dist/promote-item.js +257 -0
- package/dist/promote-item.js.map +1 -0
- package/dist/read-element.d.ts +129 -0
- package/dist/read-element.d.ts.map +1 -0
- package/dist/read-element.js +296 -0
- package/dist/read-element.js.map +1 -0
- package/dist/rename-canonical-id.d.ts.map +1 -1
- package/dist/rename-canonical-id.js +22 -13
- package/dist/rename-canonical-id.js.map +1 -1
- package/dist/roadmap-plan.d.ts.map +1 -1
- package/dist/roadmap-plan.js +23 -13
- package/dist/roadmap-plan.js.map +1 -1
- package/dist/schema-validator.d.ts +1 -0
- package/dist/schema-validator.d.ts.map +1 -1
- package/dist/schema-validator.js +7 -3
- package/dist/schema-validator.js.map +1 -1
- package/dist/schema-write.d.ts +78 -4
- package/dist/schema-write.d.ts.map +1 -1
- package/dist/schema-write.js +297 -34
- package/dist/schema-write.js.map +1 -1
- package/dist/status-vocab.d.ts +1 -0
- package/dist/status-vocab.d.ts.map +1 -1
- package/dist/status-vocab.js +4 -0
- package/dist/status-vocab.js.map +1 -1
- package/dist/truncate.d.ts +34 -0
- package/dist/truncate.d.ts.map +1 -0
- package/dist/truncate.js +85 -0
- package/dist/truncate.js.map +1 -0
- package/dist/write-schema-migration-tool.d.ts +46 -0
- package/dist/write-schema-migration-tool.d.ts.map +1 -0
- package/dist/write-schema-migration-tool.js +115 -0
- package/dist/write-schema-migration-tool.js.map +1 -0
- package/package.json +35 -2
- package/samples/blocks/work-orders.json +3 -0
- package/samples/conception.json +22 -3
- package/samples/migrations.json +133 -0
- package/samples/schemas/context-contracts.schema.json +19 -4
- package/samples/schemas/conventions.schema.json +68 -12
- package/samples/schemas/decisions.schema.json +125 -28
- package/samples/schemas/features.schema.json +140 -26
- package/samples/schemas/framework-gaps.schema.json +171 -26
- package/samples/schemas/issues.schema.json +86 -13
- package/samples/schemas/layer-plans.schema.json +195 -35
- package/samples/schemas/phase.schema.json +18 -3
- package/samples/schemas/rationale.schema.json +37 -7
- package/samples/schemas/requirements.schema.json +77 -10
- package/samples/schemas/research.schema.json +169 -36
- package/samples/schemas/spec-reviews.schema.json +88 -18
- package/samples/schemas/story.schema.json +18 -3
- package/samples/schemas/tasks.schema.json +66 -10
- package/samples/schemas/verification.schema.json +70 -12
- package/samples/schemas/work-orders.schema.json +195 -0
- package/schemas/bootstrap.schema.json +18 -6
- package/schemas/config.schema.json +58 -26
- package/schemas/context-registry.schema.json +46 -0
- package/schemas/migrations.schema.json +123 -0
- package/schemas/relations.schema.json +41 -10
- package/skill-narrative.md +42 -21
- package/skills/pi-context/SKILL.md +159 -65
- package/skills/pi-context/references/bundled-resources.md +7 -2
|
@@ -0,0 +1,1223 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Op-registry for pi-context — the canonical list of substrate operations,
|
|
3
|
+
* each described once as an OpDefinition and projected onto the in-pi tool
|
|
4
|
+
* surface by registerAll(). This is a behavior-preserving relocation of the
|
|
5
|
+
* 45 inline pi.registerTool({...}) calls that previously lived in the
|
|
6
|
+
* extension factory in index.ts: name/label/description/promptSnippet/
|
|
7
|
+
* parameters are copied verbatim, and each tool's execute body becomes the
|
|
8
|
+
* op's run(cwd, params) with the uniform result wrapper
|
|
9
|
+
* `{ details: undefined, content: [{ type: "text", text: X }] }` reduced to
|
|
10
|
+
* `return X` (registerAll re-applies the wrapper identically for every op).
|
|
11
|
+
*
|
|
12
|
+
* Phase 1 of the pi-context-cli arc (analysis/2026-06-03-pi-context-cli-design-ledger.md):
|
|
13
|
+
* the registry is the single source the auto-tracking CLI will reflect. The
|
|
14
|
+
* authGated / surface fields are carried for that downstream consumer; all
|
|
15
|
+
* current pi-context ops are surface:"use" and authGated is left unset in this
|
|
16
|
+
* phase (the auth-gate at the pi-agent-dispatch layer remains the enforcement
|
|
17
|
+
* point, unchanged by this relocation).
|
|
18
|
+
*/
|
|
19
|
+
import path from "node:path";
|
|
20
|
+
import { Type } from "typebox";
|
|
21
|
+
import { appendToBlock, appendToNestedArray, nextId, readBlock, readBlockDir, removeFromBlock, removeFromNestedArray, updateItemInBlock, updateNestedArrayItem, writeBlock, } from "./block-api.js";
|
|
22
|
+
import { adoptConception, amendConfigEntry, loadConfig, loadContext } from "./context.js";
|
|
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";
|
|
25
|
+
import { gatherExecutionContext } from "./execution-context.js";
|
|
26
|
+
// initProject + the switch/list/archive helpers are defined in index.ts (shared
|
|
27
|
+
// with the /context command handlers + the context-* tools). This is a cyclic
|
|
28
|
+
// import: index.ts imports registerAll from here. The cycle is safe at runtime —
|
|
29
|
+
// registerAll runs at extension-load time, after both modules' top-level
|
|
30
|
+
// function bindings exist, and the helpers are only referenced inside op `run`
|
|
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";
|
|
34
|
+
import { promoteItem } from "./promote-item.js";
|
|
35
|
+
import { addressInto, serializeForRead } from "./read-element.js";
|
|
36
|
+
import { renameCanonicalId } from "./rename-canonical-id.js";
|
|
37
|
+
import { listRoadmaps, loadRoadmap, renderRoadmap, validateRoadmaps } from "./roadmap-plan.js";
|
|
38
|
+
import { samplesCatalog } from "./samples-catalog.js";
|
|
39
|
+
import { readSchema, writeSchemaChecked } from "./schema-write.js";
|
|
40
|
+
import { writeSchemaMigrationExecute } from "./write-schema-migration-tool.js";
|
|
41
|
+
// ── serializeRoadmapView ────────────────────────────────────────────────────
|
|
42
|
+
// Strip non-serializable fields (suggestionTemplate fn, grouped Map) from the
|
|
43
|
+
// embedded LoadedLensView records before tool serialization. Relocated verbatim
|
|
44
|
+
// from the extension factory; consumed by the context-roadmap-load op.
|
|
45
|
+
const serializeRoadmapView = (view) => ({
|
|
46
|
+
roadmap: view.roadmap,
|
|
47
|
+
phases: view.phases.map((pv) => ({
|
|
48
|
+
phase: pv.phase,
|
|
49
|
+
lensView: "error" in pv.lensView
|
|
50
|
+
? pv.lensView
|
|
51
|
+
: {
|
|
52
|
+
lens: pv.lensView.lens,
|
|
53
|
+
items: pv.lensView.items,
|
|
54
|
+
edges: pv.lensView.edges,
|
|
55
|
+
grouped: Object.fromEntries(pv.lensView.grouped),
|
|
56
|
+
uncategorized: pv.lensView.uncategorized,
|
|
57
|
+
},
|
|
58
|
+
status: pv.status,
|
|
59
|
+
...(pv.milestone ? { milestone: pv.milestone } : {}),
|
|
60
|
+
...(pv.milestoneSatisfied !== undefined ? { milestoneSatisfied: pv.milestoneSatisfied } : {}),
|
|
61
|
+
})),
|
|
62
|
+
phaseOrder: view.phaseOrder,
|
|
63
|
+
cycles: view.cycles,
|
|
64
|
+
edges: view.edges,
|
|
65
|
+
});
|
|
66
|
+
export const ops = [
|
|
67
|
+
{
|
|
68
|
+
name: "append-block-item",
|
|
69
|
+
label: "Append Block Item",
|
|
70
|
+
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
|
+
promptSnippet: "Append items to project blocks (issues, decisions, or any user-defined block)",
|
|
72
|
+
parameters: Type.Object({
|
|
73
|
+
block: Type.String({ description: "Block name (e.g., 'issues', 'decisions')" }),
|
|
74
|
+
arrayKey: Type.String({ description: "Array key in the block (e.g., 'issues', 'decisions')" }),
|
|
75
|
+
item: Type.Unknown({ description: "Item object to append — must conform to block schema" }),
|
|
76
|
+
autoId: Type.Optional(Type.Boolean({
|
|
77
|
+
description: "When true and the item has no id, allocate the next id from the block's id pattern",
|
|
78
|
+
})),
|
|
79
|
+
}),
|
|
80
|
+
surface: "use",
|
|
81
|
+
run(cwd, params) {
|
|
82
|
+
// Type.Unknown() params may arrive as JSON strings — parse if needed
|
|
83
|
+
if (typeof params.item === "string") {
|
|
84
|
+
try {
|
|
85
|
+
params.item = JSON.parse(params.item);
|
|
86
|
+
}
|
|
87
|
+
catch {
|
|
88
|
+
throw new Error(`item parameter must be a JSON object, got unparseable string`);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
// Auto-id allocation (FGAP-084 dual-surface twin of file-block-item --auto-id)
|
|
92
|
+
if (params.autoId && params.item && typeof params.item === "object" && !params.item.id) {
|
|
93
|
+
params.item.id = nextId(cwd, params.block);
|
|
94
|
+
}
|
|
95
|
+
// Id-uniqueness is enforced atomically inside appendToBlock's
|
|
96
|
+
// withBlockLock critical section (block-api assertAppendIdUnique) —
|
|
97
|
+
// the single enforcement point. The prior racy readBlock-then-append
|
|
98
|
+
// tool-layer check was removed in favour of that library guard.
|
|
99
|
+
appendToBlock(cwd, params.block, params.arrayKey, params.item);
|
|
100
|
+
const id = params.item?.id ? ` '${params.item.id}'` : "";
|
|
101
|
+
return `Appended item${id} to ${params.block}.${params.arrayKey}`;
|
|
102
|
+
},
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
name: "update-block-item",
|
|
106
|
+
label: "Update Block Item",
|
|
107
|
+
description: "Update fields on an item in a project block array. Finds by predicate field match.",
|
|
108
|
+
promptSnippet: "Update items in project blocks — change status, add details, mark resolved",
|
|
109
|
+
parameters: Type.Object({
|
|
110
|
+
block: Type.String({ description: "Block name (e.g., 'issues', 'decisions')" }),
|
|
111
|
+
arrayKey: Type.String({ description: "Array key in the block" }),
|
|
112
|
+
match: Type.Record(Type.String(), Type.Unknown(), { description: "Fields to match (e.g., { id: 'ISSUE-NNN' })" }),
|
|
113
|
+
updates: Type.Record(Type.String(), Type.Unknown(), {
|
|
114
|
+
description: "Fields to update (e.g., { status: 'resolved' })",
|
|
115
|
+
}),
|
|
116
|
+
}),
|
|
117
|
+
surface: "use",
|
|
118
|
+
run(cwd, params) {
|
|
119
|
+
if (Object.keys(params.updates).length === 0) {
|
|
120
|
+
throw new Error("No fields to update — updates parameter is empty");
|
|
121
|
+
}
|
|
122
|
+
const matchEntries = Object.entries(params.match);
|
|
123
|
+
updateItemInBlock(cwd, params.block, params.arrayKey, (item) => matchEntries.every(([k, v]) => item[k] === v), params.updates);
|
|
124
|
+
const matchDesc = matchEntries.map(([k, v]) => `${k}=${v}`).join(", ");
|
|
125
|
+
return `Updated item (${matchDesc}) in ${params.block}.${params.arrayKey}: ${Object.keys(params.updates).join(", ")}`;
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
{
|
|
129
|
+
name: "append-relation",
|
|
130
|
+
label: "Append Relation",
|
|
131
|
+
description: "Append a closure-table relation (edge: parent, child, relation_type, optional ordinal) to relations.json. " +
|
|
132
|
+
"Shape is AJV-validated; an exact-duplicate edge (same parent+child+relation_type) is a no-op. Reference " +
|
|
133
|
+
"integrity (endpoints resolve, relation_type registered, no cycle) is NOT checked here — run context-validate " +
|
|
134
|
+
"after. Creates relations.json if absent.",
|
|
135
|
+
promptSnippet: "Create a relation/edge between two items (parent→child under a relation_type)",
|
|
136
|
+
parameters: Type.Object({
|
|
137
|
+
parent: Type.String({ description: "Canonical id (or lens bin name) of the parent endpoint" }),
|
|
138
|
+
child: Type.String({ description: "Canonical id of the child endpoint" }),
|
|
139
|
+
relation_type: Type.String({
|
|
140
|
+
description: "Registered relation_type canonical_id / hierarchy edge type / lens id",
|
|
141
|
+
}),
|
|
142
|
+
ordinal: Type.Optional(Type.Integer({ description: "Optional sibling-ordering within (parent, relation_type)" })),
|
|
143
|
+
}),
|
|
144
|
+
surface: "use",
|
|
145
|
+
run(cwd, params) {
|
|
146
|
+
// Cycle-5 porcelain: STRING selectors (bare refname / <alias>:<refname> /
|
|
147
|
+
// lens-bin) are resolved to structured EdgeEndpoints and written via the
|
|
148
|
+
// raw plumbing. The param surface stays string-typed; messaging uses the
|
|
149
|
+
// raw selectors (params.*), not the resolved structured endpoints.
|
|
150
|
+
const { appended } = appendRelationByRef(cwd, {
|
|
151
|
+
parent: params.parent,
|
|
152
|
+
child: params.child,
|
|
153
|
+
relation_type: params.relation_type,
|
|
154
|
+
...(params.ordinal !== undefined ? { ordinal: params.ordinal } : {}),
|
|
155
|
+
});
|
|
156
|
+
const ordinalNote = params.ordinal !== undefined ? ` (ordinal ${params.ordinal})` : "";
|
|
157
|
+
return appended
|
|
158
|
+
? `Appended relation ${params.parent} -[${params.relation_type}]-> ${params.child}${ordinalNote}`
|
|
159
|
+
: `Relation ${params.parent} -[${params.relation_type}]-> ${params.child} already exists — no-op`;
|
|
160
|
+
},
|
|
161
|
+
},
|
|
162
|
+
{
|
|
163
|
+
name: "promote-item",
|
|
164
|
+
label: "Promote Item",
|
|
165
|
+
description: "Promote a substrate item into another (registered) substrate as a NEW content-addressed item, recording the " +
|
|
166
|
+
"'item_derived_from_item' lineage edge in the destination relations.json (parent = the new derived item, child = " +
|
|
167
|
+
"the source, carrying the source content_hash). The destination write-path mints a fresh oid + content_hash + " +
|
|
168
|
+
"content object. When the source block's status enum supports it, the source is marked superseded. Preconditions " +
|
|
169
|
+
"(unresolvable/non-item source, unregistered destination alias, unregistered destination relation_type, refname " +
|
|
170
|
+
"collision) throw. Pass dryRun to compute the destination without writing.",
|
|
171
|
+
promptSnippet: "Promote an item into another substrate as a derived copy with a lineage edge",
|
|
172
|
+
parameters: Type.Object({
|
|
173
|
+
source: Type.String({ description: "Source item selector (bare refname / <alias>:<refname>)" }),
|
|
174
|
+
destinationSubstrate: Type.String({ description: "Registered destination substrate alias" }),
|
|
175
|
+
newRefname: Type.Optional(Type.String({ description: "Explicit destination refname (else allocated from the dest block id pattern)" })),
|
|
176
|
+
dryRun: Type.Optional(Type.Boolean({ description: "Compute the destination without writing any channel" })),
|
|
177
|
+
writer: Type.Object({
|
|
178
|
+
kind: Type.String({ description: "Writer kind discriminator — MUST be 'human'." }),
|
|
179
|
+
user: Type.String({ description: "Human writer identity (e.g. 'davidryan@gmail.com')." }),
|
|
180
|
+
}, { description: "DispatchContext.writer per pi-context/src/dispatch-context.ts." }),
|
|
181
|
+
}),
|
|
182
|
+
surface: "use",
|
|
183
|
+
run(cwd, params) {
|
|
184
|
+
if (!params.writer?.user) {
|
|
185
|
+
throw new Error("promote-item: writer.user is required.");
|
|
186
|
+
}
|
|
187
|
+
const result = promoteItem(cwd, {
|
|
188
|
+
source: params.source,
|
|
189
|
+
destinationSubstrate: params.destinationSubstrate,
|
|
190
|
+
...(params.newRefname !== undefined ? { newRefname: params.newRefname } : {}),
|
|
191
|
+
...(params.dryRun !== undefined ? { dryRun: params.dryRun } : {}),
|
|
192
|
+
}, { writer: { kind: "human", user: params.writer.user } });
|
|
193
|
+
return JSON.stringify(result, null, 2);
|
|
194
|
+
},
|
|
195
|
+
},
|
|
196
|
+
{
|
|
197
|
+
name: "append-block-nested-item",
|
|
198
|
+
label: "Append Block Nested Item",
|
|
199
|
+
description: "Append an item to a nested array on a parent-array item in a project block. Schema validation is automatic.",
|
|
200
|
+
promptSnippet: "Append items to nested arrays inside parent items (e.g., findings inside a review)",
|
|
201
|
+
parameters: Type.Object({
|
|
202
|
+
block: Type.String({ description: "Block name (e.g., 'spec-reviews')" }),
|
|
203
|
+
arrayKey: Type.String({ description: "Parent array key (e.g., 'reviews')" }),
|
|
204
|
+
match: Type.Record(Type.String(), Type.Unknown(), {
|
|
205
|
+
description: "Fields to match the parent item (e.g., { id: 'REVIEW-NNN' })",
|
|
206
|
+
}),
|
|
207
|
+
nestedKey: Type.String({ description: "Nested array key on the matched parent (e.g., 'findings')" }),
|
|
208
|
+
item: Type.Unknown({ description: "Item object to append to the nested array — must conform to schema" }),
|
|
209
|
+
}),
|
|
210
|
+
surface: "use",
|
|
211
|
+
run(cwd, params) {
|
|
212
|
+
if (typeof params.item === "string") {
|
|
213
|
+
try {
|
|
214
|
+
params.item = JSON.parse(params.item);
|
|
215
|
+
}
|
|
216
|
+
catch {
|
|
217
|
+
throw new Error(`item parameter must be a JSON object, got unparseable string`);
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
const matchEntries = Object.entries(params.match);
|
|
221
|
+
const predicate = (i) => matchEntries.every(([k, v]) => i[k] === v);
|
|
222
|
+
appendToNestedArray(cwd, params.block, params.arrayKey, predicate, params.nestedKey, params.item);
|
|
223
|
+
const matchDesc = matchEntries.map(([k, v]) => `${k}=${v}`).join(", ");
|
|
224
|
+
const id = params.item?.id ? ` '${params.item.id}'` : "";
|
|
225
|
+
return `Appended item${id} to ${params.block}.${params.arrayKey}[${matchDesc}].${params.nestedKey}`;
|
|
226
|
+
},
|
|
227
|
+
},
|
|
228
|
+
{
|
|
229
|
+
name: "update-block-nested-item",
|
|
230
|
+
label: "Update Block Nested Item",
|
|
231
|
+
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
|
+
promptSnippet: "Update items inside nested arrays — change finding state, mark resolved",
|
|
233
|
+
parameters: Type.Object({
|
|
234
|
+
block: Type.String({ description: "Block name (e.g., 'spec-reviews')" }),
|
|
235
|
+
arrayKey: Type.String({ description: "Parent array key (e.g., 'reviews')" }),
|
|
236
|
+
match: Type.Record(Type.String(), Type.Unknown(), {
|
|
237
|
+
description: "Fields to match the parent item (e.g., { id: 'REVIEW-NNN' })",
|
|
238
|
+
}),
|
|
239
|
+
nestedKey: Type.String({ description: "Nested array key on the matched parent (e.g., 'findings')" }),
|
|
240
|
+
nestedMatch: Type.Record(Type.String(), Type.Unknown(), {
|
|
241
|
+
description: "Fields to match the nested item (e.g., { id: 'F-001' })",
|
|
242
|
+
}),
|
|
243
|
+
updates: Type.Record(Type.String(), Type.Unknown(), {
|
|
244
|
+
description: "Fields to update on the nested item (e.g., { state: 'resolved' })",
|
|
245
|
+
}),
|
|
246
|
+
}),
|
|
247
|
+
surface: "use",
|
|
248
|
+
run(cwd, params) {
|
|
249
|
+
if (Object.keys(params.updates).length === 0) {
|
|
250
|
+
throw new Error("No fields to update — updates parameter is empty");
|
|
251
|
+
}
|
|
252
|
+
const parentEntries = Object.entries(params.match);
|
|
253
|
+
const nestedEntries = Object.entries(params.nestedMatch);
|
|
254
|
+
const parentPred = (i) => parentEntries.every(([k, v]) => i[k] === v);
|
|
255
|
+
const nestedPred = (i) => nestedEntries.every(([k, v]) => i[k] === v);
|
|
256
|
+
updateNestedArrayItem(cwd, params.block, params.arrayKey, parentPred, params.nestedKey, nestedPred, params.updates);
|
|
257
|
+
const parentDesc = parentEntries.map(([k, v]) => `${k}=${v}`).join(", ");
|
|
258
|
+
const nestedDesc = nestedEntries.map(([k, v]) => `${k}=${v}`).join(", ");
|
|
259
|
+
return `Updated nested item (${nestedDesc}) in ${params.block}.${params.arrayKey}[${parentDesc}].${params.nestedKey}: ${Object.keys(params.updates).join(", ")}`;
|
|
260
|
+
},
|
|
261
|
+
},
|
|
262
|
+
{
|
|
263
|
+
name: "remove-block-item",
|
|
264
|
+
label: "Remove Block Item",
|
|
265
|
+
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
|
+
promptSnippet: "Remove items from project blocks — prune retracted issues, dedupe entries",
|
|
267
|
+
parameters: Type.Object({
|
|
268
|
+
block: Type.String({ description: "Block name (e.g., 'issues')" }),
|
|
269
|
+
arrayKey: Type.String({ description: "Top-level array key (e.g., 'issues')" }),
|
|
270
|
+
match: Type.Record(Type.String(), Type.Unknown(), { description: "Fields to match (e.g., { id: 'ISSUE-NNN' })" }),
|
|
271
|
+
}),
|
|
272
|
+
surface: "use",
|
|
273
|
+
run(cwd, params) {
|
|
274
|
+
const matchEntries = Object.entries(params.match);
|
|
275
|
+
const predicate = (i) => matchEntries.every(([k, v]) => i[k] === v);
|
|
276
|
+
const result = removeFromBlock(cwd, params.block, params.arrayKey, predicate);
|
|
277
|
+
const matchDesc = matchEntries.map(([k, v]) => `${k}=${v}`).join(", ");
|
|
278
|
+
return `Removed ${result.removed} item(s) matching (${matchDesc}) from ${params.block}.${params.arrayKey}`;
|
|
279
|
+
},
|
|
280
|
+
},
|
|
281
|
+
{
|
|
282
|
+
name: "remove-block-nested-item",
|
|
283
|
+
label: "Remove Block Nested Item",
|
|
284
|
+
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
|
+
promptSnippet: "Remove nested items — drop rejected findings, retract nested references",
|
|
286
|
+
parameters: Type.Object({
|
|
287
|
+
block: Type.String({ description: "Block name (e.g., 'spec-reviews')" }),
|
|
288
|
+
arrayKey: Type.String({ description: "Parent array key (e.g., 'reviews')" }),
|
|
289
|
+
match: Type.Record(Type.String(), Type.Unknown(), {
|
|
290
|
+
description: "Fields to match the parent item (e.g., { id: 'REVIEW-NNN' })",
|
|
291
|
+
}),
|
|
292
|
+
nestedKey: Type.String({ description: "Nested array key on the matched parent (e.g., 'findings')" }),
|
|
293
|
+
nestedMatch: Type.Record(Type.String(), Type.Unknown(), {
|
|
294
|
+
description: "Fields to match the nested items to remove (e.g., { id: 'F-001' })",
|
|
295
|
+
}),
|
|
296
|
+
}),
|
|
297
|
+
surface: "use",
|
|
298
|
+
run(cwd, params) {
|
|
299
|
+
const parentEntries = Object.entries(params.match);
|
|
300
|
+
const nestedEntries = Object.entries(params.nestedMatch);
|
|
301
|
+
const parentPred = (i) => parentEntries.every(([k, v]) => i[k] === v);
|
|
302
|
+
const nestedPred = (i) => nestedEntries.every(([k, v]) => i[k] === v);
|
|
303
|
+
const result = removeFromNestedArray(cwd, params.block, params.arrayKey, parentPred, params.nestedKey, nestedPred);
|
|
304
|
+
const parentDesc = parentEntries.map(([k, v]) => `${k}=${v}`).join(", ");
|
|
305
|
+
const nestedDesc = nestedEntries.map(([k, v]) => `${k}=${v}`).join(", ");
|
|
306
|
+
return `Removed ${result.removed} nested item(s) matching (${nestedDesc}) from ${params.block}.${params.arrayKey}[${parentDesc}].${params.nestedKey}`;
|
|
307
|
+
},
|
|
308
|
+
},
|
|
309
|
+
{
|
|
310
|
+
name: "read-block-dir",
|
|
311
|
+
label: "Read Block Dir",
|
|
312
|
+
description: "Enumerate and parse all .json files in a <substrate-dir>/<subdir>/ directory, returned as a sorted array. Missing directories return [].",
|
|
313
|
+
promptSnippet: "Enumerate project block subdirectories (phases, schemas, etc.) as parsed JSON",
|
|
314
|
+
parameters: Type.Object({
|
|
315
|
+
subdir: Type.String({ description: "Subdirectory under the substrate dir (e.g., 'phases', 'schemas')" }),
|
|
316
|
+
}),
|
|
317
|
+
surface: "use",
|
|
318
|
+
run(cwd, params) {
|
|
319
|
+
const result = readBlockDir(cwd, params.subdir);
|
|
320
|
+
const envelope = serializeForRead(result, { label: `<substrate-dir>/${params.subdir}/` });
|
|
321
|
+
return envelope.content;
|
|
322
|
+
},
|
|
323
|
+
},
|
|
324
|
+
{
|
|
325
|
+
name: "read-block",
|
|
326
|
+
label: "Read Block",
|
|
327
|
+
description: "Read a project block file as structured JSON.",
|
|
328
|
+
promptSnippet: "Read a project block as structured JSON",
|
|
329
|
+
parameters: Type.Object({
|
|
330
|
+
block: Type.String({ description: "Block name (e.g., 'issues', 'tasks', 'requirements')" }),
|
|
331
|
+
}),
|
|
332
|
+
surface: "use",
|
|
333
|
+
run(cwd, params) {
|
|
334
|
+
const result = readBlock(cwd, params.block);
|
|
335
|
+
const envelope = serializeForRead(result, {
|
|
336
|
+
label: `<substrate-dir>/${params.block}.json`,
|
|
337
|
+
overCapDirective: {
|
|
338
|
+
tool: "read-block-page",
|
|
339
|
+
params: { block: params.block, offset: 0, limit: 50 },
|
|
340
|
+
hint: "or read-block-item with id=<id>",
|
|
341
|
+
},
|
|
342
|
+
});
|
|
343
|
+
return envelope.content;
|
|
344
|
+
},
|
|
345
|
+
},
|
|
346
|
+
{
|
|
347
|
+
name: "write-block",
|
|
348
|
+
label: "Write Block",
|
|
349
|
+
description: "Write or replace an entire project block with schema validation.",
|
|
350
|
+
promptSnippet: "Write or replace a project block with schema validation",
|
|
351
|
+
parameters: Type.Object({
|
|
352
|
+
block: Type.String({ description: "Block name (e.g., 'project', 'architecture')" }),
|
|
353
|
+
data: Type.Unknown({ description: "Complete block data — must conform to block schema" }),
|
|
354
|
+
}),
|
|
355
|
+
surface: "use",
|
|
356
|
+
authGated: true,
|
|
357
|
+
run(cwd, params) {
|
|
358
|
+
const data = typeof params.data === "string" ? JSON.parse(params.data) : params.data;
|
|
359
|
+
writeBlock(cwd, params.block, data);
|
|
360
|
+
return `Wrote block '${params.block}' successfully`;
|
|
361
|
+
},
|
|
362
|
+
},
|
|
363
|
+
{
|
|
364
|
+
name: "context-status",
|
|
365
|
+
label: "Context Status",
|
|
366
|
+
description: "Get derived context state — source metrics, block summaries, planning lifecycle status.",
|
|
367
|
+
promptSnippet: "Get context state — source metrics, block summaries, planning lifecycle status",
|
|
368
|
+
parameters: Type.Object({}),
|
|
369
|
+
surface: "use",
|
|
370
|
+
run(cwd, _params) {
|
|
371
|
+
const result = contextState(cwd);
|
|
372
|
+
return JSON.stringify(result, null, 2);
|
|
373
|
+
},
|
|
374
|
+
},
|
|
375
|
+
{
|
|
376
|
+
name: "context-validate",
|
|
377
|
+
label: "Context Validate",
|
|
378
|
+
description: "Validate cross-block referential integrity — check that IDs referenced across blocks exist.",
|
|
379
|
+
promptSnippet: "Validate cross-block referential integrity",
|
|
380
|
+
parameters: Type.Object({}),
|
|
381
|
+
surface: "use",
|
|
382
|
+
run(cwd, _params) {
|
|
383
|
+
const result = validateContext(cwd);
|
|
384
|
+
return JSON.stringify(result, null, 2);
|
|
385
|
+
},
|
|
386
|
+
},
|
|
387
|
+
{
|
|
388
|
+
name: "read-config",
|
|
389
|
+
label: "Read Config",
|
|
390
|
+
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
|
+
promptSnippet: "Read project config — vocabulary, lenses, relation_types, status_buckets",
|
|
392
|
+
parameters: Type.Object({
|
|
393
|
+
registry: Type.Optional(Type.String({
|
|
394
|
+
description: "Address ONE config registry/map by key (e.g. 'relation_types', 'lenses', 'block_kinds', 'status_buckets')",
|
|
395
|
+
})),
|
|
396
|
+
id: Type.Optional(Type.String({ description: "With `registry`: address ONE entry within it by canonical_id" })),
|
|
397
|
+
}),
|
|
398
|
+
surface: "use",
|
|
399
|
+
run(cwd, params) {
|
|
400
|
+
const config = loadConfig(cwd);
|
|
401
|
+
const root = tryResolveContextDir(cwd);
|
|
402
|
+
const configPath = root === null ? null : path.join(root, "config.json");
|
|
403
|
+
if (params.registry !== undefined) {
|
|
404
|
+
const reg = addressInto(config, { key: params.registry });
|
|
405
|
+
if (!reg.found) {
|
|
406
|
+
return `read-config: registry not found — ${reg.resolved}`;
|
|
407
|
+
}
|
|
408
|
+
if (params.id !== undefined) {
|
|
409
|
+
const entry = addressInto(reg.value, { id: params.id });
|
|
410
|
+
if (!entry.found) {
|
|
411
|
+
return `read-config: entry not found in ${params.registry} — ${entry.resolved}`;
|
|
412
|
+
}
|
|
413
|
+
const envEntry = serializeForRead(entry.value, { label: `config.${params.registry}.${params.id}` });
|
|
414
|
+
return envEntry.content;
|
|
415
|
+
}
|
|
416
|
+
const envReg = serializeForRead(reg.value, {
|
|
417
|
+
label: `config.${params.registry}`,
|
|
418
|
+
overCapDirective: {
|
|
419
|
+
tool: "read-config",
|
|
420
|
+
params: { registry: params.registry },
|
|
421
|
+
hint: "add id=<entry canonical_id>",
|
|
422
|
+
},
|
|
423
|
+
});
|
|
424
|
+
return envReg.content;
|
|
425
|
+
}
|
|
426
|
+
const result = { config, configPath };
|
|
427
|
+
const envelope = serializeForRead(result, {
|
|
428
|
+
label: configPath ?? "config.json",
|
|
429
|
+
overCapDirective: {
|
|
430
|
+
tool: "read-config",
|
|
431
|
+
hint: "registry=<name> (block_kinds|relation_types|lenses|invariants|…)",
|
|
432
|
+
},
|
|
433
|
+
});
|
|
434
|
+
return envelope.content;
|
|
435
|
+
},
|
|
436
|
+
},
|
|
437
|
+
{
|
|
438
|
+
name: "list-tools",
|
|
439
|
+
label: "List Tools",
|
|
440
|
+
description: "Discover the agent's own tool surface (all loaded extensions + builtins). Default returns a COMPACT index — one line per tool (name · param-count · one-line description) plus the active set — not the full JSON-schemas. Pass `name` to fetch ONE tool's full descriptor (name + description + parameter JSON-schema + sourceInfo). Index-then-detail pattern.",
|
|
441
|
+
promptSnippet: "Discover available tools — compact index, or one tool's full descriptor via `name`",
|
|
442
|
+
parameters: Type.Object({
|
|
443
|
+
name: Type.Optional(Type.String({ description: "Address ONE tool by name → full descriptor (params schema + sourceInfo)" })),
|
|
444
|
+
}),
|
|
445
|
+
// surface: "process" — list-tools closes over the pi-runtime `boundPi`
|
|
446
|
+
// introspection handle (getAllTools/getActiveTools) and throws when that
|
|
447
|
+
// handle is null (any out-of-pi caller). It is therefore the one op the
|
|
448
|
+
// auto-tracking CLI must NOT surface; the CLI partitions on this field
|
|
449
|
+
// (surface === "use") rather than naming list-tools, so the exclusion is
|
|
450
|
+
// data-driven and a future process-only op inherits it.
|
|
451
|
+
surface: "process",
|
|
452
|
+
run(_cwd, params) {
|
|
453
|
+
// Closes over the factory `pi` (the introspection surface lives on
|
|
454
|
+
// ExtensionAPI, not ExtensionContext) — supplied to run via the bound
|
|
455
|
+
// PI handle captured in registerAll. cwd is unused here.
|
|
456
|
+
const piHandle = boundPi;
|
|
457
|
+
if (piHandle === null) {
|
|
458
|
+
throw new Error("list-tools: extension PI handle not bound — registerAll(pi) must run before this op executes");
|
|
459
|
+
}
|
|
460
|
+
const all = piHandle.getAllTools();
|
|
461
|
+
const active = piHandle.getActiveTools();
|
|
462
|
+
if (params.name !== undefined) {
|
|
463
|
+
const hit = addressInto(all, { key: params.name });
|
|
464
|
+
// getAllTools entries are keyed by `.name`, not `.id`/`.canonical_id`;
|
|
465
|
+
// resolve by name explicitly rather than relying on addressInto's id path.
|
|
466
|
+
const tool = hit.found ? hit.value : all.find((t) => t.name === params.name);
|
|
467
|
+
if (tool === undefined) {
|
|
468
|
+
return `list-tools: tool not found — name=${params.name}`;
|
|
469
|
+
}
|
|
470
|
+
const envOne = serializeForRead(tool, { label: `tool ${params.name}` });
|
|
471
|
+
return envOne.content;
|
|
472
|
+
}
|
|
473
|
+
// Default: compact index (FGAP-101) — name + param count + one-line description.
|
|
474
|
+
const index = all.map((t) => {
|
|
475
|
+
const tool = t;
|
|
476
|
+
const paramCount = tool.parameters?.properties && typeof tool.parameters.properties === "object"
|
|
477
|
+
? Object.keys(tool.parameters.properties).length
|
|
478
|
+
: 0;
|
|
479
|
+
const oneLine = (tool.description ?? "").split("\n")[0] ?? "";
|
|
480
|
+
return { name: tool.name, params: paramCount, description: oneLine };
|
|
481
|
+
});
|
|
482
|
+
// The compact index is one line per tool — small enough to serialize whole
|
|
483
|
+
// (no paging); keep the wrapper fields (active/total) on the result object.
|
|
484
|
+
const result = { tools: index, active, total: all.length, activeCount: active.length };
|
|
485
|
+
const envelope = serializeForRead(result, {
|
|
486
|
+
label: "tool index — pass name= for detail",
|
|
487
|
+
overCapDirective: { tool: "list-tools", hint: "name=<tool>" },
|
|
488
|
+
});
|
|
489
|
+
return envelope.content;
|
|
490
|
+
},
|
|
491
|
+
},
|
|
492
|
+
{
|
|
493
|
+
name: "read-samples-catalog",
|
|
494
|
+
label: "Read Samples Catalog",
|
|
495
|
+
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
|
+
promptSnippet: "Discover installable sample block kinds — title, shape, relation_types, invariants, lenses",
|
|
497
|
+
parameters: Type.Object({
|
|
498
|
+
kind: Type.Optional(Type.String({ description: "Filter to one block_kind canonical_id (e.g. 'tasks')" })),
|
|
499
|
+
}),
|
|
500
|
+
surface: "use",
|
|
501
|
+
run(_cwd, params) {
|
|
502
|
+
// Package-intrinsic: the catalog reads the extension's bundled samples
|
|
503
|
+
// directory, not the project substrate — cwd is unused.
|
|
504
|
+
const catalog = samplesCatalog(params.kind ? { kind: params.kind } : undefined);
|
|
505
|
+
const envelope = serializeForRead(catalog, {
|
|
506
|
+
label: params.kind ? `samples kind=${params.kind}` : "samples catalog",
|
|
507
|
+
// Whole catalog → narrow by kind; a single kind has no finer
|
|
508
|
+
// addressing (edge → head-leading marker, no directive).
|
|
509
|
+
...(params.kind ? {} : { overCapDirective: { tool: "read-samples-catalog", hint: "kind=<canonical_id>" } }),
|
|
510
|
+
});
|
|
511
|
+
return envelope.content;
|
|
512
|
+
},
|
|
513
|
+
},
|
|
514
|
+
{
|
|
515
|
+
name: "context-current-state",
|
|
516
|
+
label: "Context Current State",
|
|
517
|
+
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
|
+
promptSnippet: "Derive current project state — focus, in-flight, next actions, blocked",
|
|
519
|
+
parameters: Type.Object({}),
|
|
520
|
+
surface: "use",
|
|
521
|
+
run(cwd, _params) {
|
|
522
|
+
const state = currentState(cwd);
|
|
523
|
+
return JSON.stringify(state, null, 2);
|
|
524
|
+
},
|
|
525
|
+
},
|
|
526
|
+
{
|
|
527
|
+
name: "context-bootstrap-state",
|
|
528
|
+
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)",
|
|
531
|
+
parameters: Type.Object({}),
|
|
532
|
+
surface: "use",
|
|
533
|
+
run(cwd, _params) {
|
|
534
|
+
const status = deriveBootstrapState(cwd);
|
|
535
|
+
return JSON.stringify(status, null, 2);
|
|
536
|
+
},
|
|
537
|
+
},
|
|
538
|
+
{
|
|
539
|
+
name: "rename-canonical-id",
|
|
540
|
+
label: "Rename Canonical Id",
|
|
541
|
+
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
|
+
promptSnippet: "Rename a canonical_id (item/relation_type/lens/layer) across substrate; dryRun to preview",
|
|
543
|
+
parameters: Type.Object({
|
|
544
|
+
kind: Type.String({ description: "One of: item | relation_type | lens | layer" }),
|
|
545
|
+
oldId: Type.String({ description: "Current canonical_id to rename from" }),
|
|
546
|
+
newId: Type.String({ description: "New canonical_id to rename to" }),
|
|
547
|
+
dryRun: Type.Optional(Type.Boolean({ description: "Compute would-change counts without writing" })),
|
|
548
|
+
}),
|
|
549
|
+
surface: "use",
|
|
550
|
+
authGated: true,
|
|
551
|
+
run(cwd, params) {
|
|
552
|
+
const report = renameCanonicalId(cwd, params.kind, params.oldId, params.newId, { dryRun: params.dryRun });
|
|
553
|
+
return JSON.stringify(report, null, 2);
|
|
554
|
+
},
|
|
555
|
+
},
|
|
556
|
+
{
|
|
557
|
+
name: "amend-config",
|
|
558
|
+
label: "Amend Config",
|
|
559
|
+
description: "Scoped add / replace / remove of ONE entry in ONE config.json registry (block_kinds, relation_types, lenses, " +
|
|
560
|
+
"layers, invariants, status_buckets, display_strings, naming, installed_schemas, installed_blocks, hierarchy). " +
|
|
561
|
+
"The whole resulting config is AJV-validated (SHAPE) and op-correctness is enforced (add ⇒ key absent, " +
|
|
562
|
+
"replace/remove ⇒ key present). Cross-registry referential integrity (removing a still-referenced " +
|
|
563
|
+
"relation_type / lens / layer / block_kind) is NOT checked here — run context-validate after. dryRun previews " +
|
|
564
|
+
"without writing.",
|
|
565
|
+
promptSnippet: "Add/replace/remove one entry in a config.json registry (vocabulary, lenses, invariants, status_buckets)",
|
|
566
|
+
parameters: Type.Object({
|
|
567
|
+
registry: Type.String({
|
|
568
|
+
description: "One of: block_kinds | relation_types | lenses | layers | invariants | status_buckets | display_strings | naming | installed_schemas | installed_blocks | hierarchy",
|
|
569
|
+
}),
|
|
570
|
+
operation: Type.String({ description: "add | replace | remove" }),
|
|
571
|
+
key: Type.String({
|
|
572
|
+
description: "Entry key: id for keyed-array (block_kinds/relation_types/lenses/layers/invariants), map key for " +
|
|
573
|
+
"map (status_buckets/display_strings/naming), the string value for string-array " +
|
|
574
|
+
"(installed_schemas/installed_blocks), or a JSON {parent_block, child_block, relation_type} for hierarchy",
|
|
575
|
+
}),
|
|
576
|
+
entry: Type.Optional(Type.Unknown({
|
|
577
|
+
description: "Entry payload: object for keyed-array/hierarchy, string for map value; omit for remove. For keyed-array its id field must equal key; for string-array (when given) it must equal key",
|
|
578
|
+
})),
|
|
579
|
+
dryRun: Type.Optional(Type.Boolean({ description: "Preview the op without writing config.json" })),
|
|
580
|
+
}),
|
|
581
|
+
surface: "use",
|
|
582
|
+
authGated: true,
|
|
583
|
+
run(cwd, params) {
|
|
584
|
+
// Type.Unknown() params may arrive as JSON strings. Parse if possible; on
|
|
585
|
+
// failure KEEP the raw string (valid for map-value registries whose value
|
|
586
|
+
// is a bare string, e.g. naming/display_strings/status_buckets).
|
|
587
|
+
let entry = params.entry;
|
|
588
|
+
if (typeof entry === "string") {
|
|
589
|
+
try {
|
|
590
|
+
entry = JSON.parse(entry);
|
|
591
|
+
}
|
|
592
|
+
catch {
|
|
593
|
+
/* keep raw string — valid for map-value registries */
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
const result = amendConfigEntry(cwd, params.registry, params.operation, params.key, entry, undefined, {
|
|
597
|
+
dryRun: params.dryRun,
|
|
598
|
+
});
|
|
599
|
+
const verb = result.modified ? (params.dryRun ? `would ${result.operation}` : `${result.operation}d`) : "no-op";
|
|
600
|
+
return `amend-config: ${verb} ${result.registry}[${result.key}]`;
|
|
601
|
+
},
|
|
602
|
+
},
|
|
603
|
+
{
|
|
604
|
+
name: "read-schema",
|
|
605
|
+
label: "Read Schema",
|
|
606
|
+
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
|
+
promptSnippet: "Read a block schema as structured JSON — optionally address one property via `path`",
|
|
608
|
+
parameters: Type.Object({
|
|
609
|
+
schemaName: Type.String({
|
|
610
|
+
description: "Schema name without extension (e.g., 'tasks', 'decisions', 'issues')",
|
|
611
|
+
}),
|
|
612
|
+
path: Type.Optional(Type.String({
|
|
613
|
+
description: "Address ONE property by dotted/bracket path (e.g. 'properties.tasks.items.properties.status')",
|
|
614
|
+
})),
|
|
615
|
+
}),
|
|
616
|
+
surface: "use",
|
|
617
|
+
run(cwd, params) {
|
|
618
|
+
const schema = readSchema(cwd, params.schemaName);
|
|
619
|
+
const schemaPathStr = schemaPath(cwd, params.schemaName);
|
|
620
|
+
if (params.path !== undefined) {
|
|
621
|
+
const addr = addressInto(schema, { path: params.path });
|
|
622
|
+
if (!addr.found) {
|
|
623
|
+
return `read-schema: property not found — ${addr.resolved}`;
|
|
624
|
+
}
|
|
625
|
+
const envProp = serializeForRead(addr.value, { label: `${params.schemaName} ${addr.resolved}` });
|
|
626
|
+
return envProp.content;
|
|
627
|
+
}
|
|
628
|
+
const result = { schema, schemaPath: schemaPathStr };
|
|
629
|
+
const envelope = serializeForRead(result, {
|
|
630
|
+
label: schemaPathStr,
|
|
631
|
+
overCapDirective: {
|
|
632
|
+
tool: "read-schema",
|
|
633
|
+
params: { schemaName: params.schemaName },
|
|
634
|
+
hint: "path=<dotted json-path>",
|
|
635
|
+
},
|
|
636
|
+
});
|
|
637
|
+
return envelope.content;
|
|
638
|
+
},
|
|
639
|
+
},
|
|
640
|
+
{
|
|
641
|
+
name: "write-schema",
|
|
642
|
+
label: "Write Schema",
|
|
643
|
+
description: "Create or replace a substrate block-kind JSON Schema. operation 'create' requires the schema absent; " +
|
|
644
|
+
"'replace' requires it present. The body is AJV draft-07 meta-validated before an atomic write. Schema " +
|
|
645
|
+
"version bumps require a companion migration declaration via write-schema-migration; without one, " +
|
|
646
|
+
"read/write of items declaring an older schema_version throws version-mismatch. Registering the block_kind " +
|
|
647
|
+
"that points at this schema is a separate step (amend-config block_kinds).",
|
|
648
|
+
promptSnippet: "Create or replace a block-kind JSON Schema (meta-validated, atomic)",
|
|
649
|
+
parameters: Type.Object({
|
|
650
|
+
operation: Type.String({ description: "create | replace" }),
|
|
651
|
+
schemaName: Type.String({ description: "Schema name without extension (e.g., 'tasks')" }),
|
|
652
|
+
schema: Type.Unknown({
|
|
653
|
+
description: "The whole JSON Schema object (draft-07). Accepts a JSON string.",
|
|
654
|
+
}),
|
|
655
|
+
dryRun: Type.Optional(Type.Boolean({ description: "Meta-validate without writing" })),
|
|
656
|
+
}),
|
|
657
|
+
surface: "use",
|
|
658
|
+
authGated: true,
|
|
659
|
+
run(cwd, params) {
|
|
660
|
+
// Type.Unknown() params may arrive as JSON strings. Parse if possible; on
|
|
661
|
+
// failure KEEP the raw value (meta-validation rejects a non-object body).
|
|
662
|
+
let schema = params.schema;
|
|
663
|
+
if (typeof schema === "string") {
|
|
664
|
+
try {
|
|
665
|
+
schema = JSON.parse(schema);
|
|
666
|
+
}
|
|
667
|
+
catch {
|
|
668
|
+
/* keep raw string — meta-validation will reject a non-object */
|
|
669
|
+
}
|
|
670
|
+
}
|
|
671
|
+
const result = writeSchemaChecked(cwd, params.schemaName, schema, params.operation, undefined, { dryRun: params.dryRun });
|
|
672
|
+
const verb = result.written ? `${result.operation}d` : `would ${result.operation}`;
|
|
673
|
+
return `write-schema: ${verb} schema '${params.schemaName}' at ${result.schemaPath}`;
|
|
674
|
+
},
|
|
675
|
+
},
|
|
676
|
+
{
|
|
677
|
+
name: "write-schema-migration",
|
|
678
|
+
label: "Write Schema Migration",
|
|
679
|
+
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
|
+
promptSnippet: "Declare a schema version-bump migration (identity or declarative-transform) into migrations.json",
|
|
681
|
+
parameters: Type.Object({
|
|
682
|
+
operation: Type.String({ description: "create | replace | remove" }),
|
|
683
|
+
schemaName: Type.String({ description: "Schema name without extension (e.g., 'tasks')." }),
|
|
684
|
+
fromVersion: Type.String({ description: "Source schema semver this migration walks forward FROM." }),
|
|
685
|
+
toVersion: Type.String({
|
|
686
|
+
description: "Destination schema semver this migration produces. Must differ from fromVersion. Ignored for operation=remove.",
|
|
687
|
+
}),
|
|
688
|
+
kind: Type.Optional(Type.String({
|
|
689
|
+
description: "identity | declarative-transform. Required for operation=create/replace; ignored for remove.",
|
|
690
|
+
})),
|
|
691
|
+
transform: Type.Optional(Type.Unknown({
|
|
692
|
+
description: "TransformSpec body — required when kind='declarative-transform'; forbidden when kind='identity'. Accepts a JSON string.",
|
|
693
|
+
})),
|
|
694
|
+
writer: Type.Object({
|
|
695
|
+
kind: Type.String({ description: "Writer kind discriminator — MUST be 'human'." }),
|
|
696
|
+
user: Type.String({ description: "Human writer identity (e.g. 'davidryan@gmail.com')." }),
|
|
697
|
+
}, { description: "DispatchContext.writer per pi-context/src/dispatch-context.ts." }),
|
|
698
|
+
}),
|
|
699
|
+
surface: "use",
|
|
700
|
+
authGated: true,
|
|
701
|
+
async run(cwd, params) {
|
|
702
|
+
const result = await writeSchemaMigrationExecute(cwd, params);
|
|
703
|
+
// writeSchemaMigrationExecute returns the uniform AgentToolResult; the
|
|
704
|
+
// op contract is the text payload, which registerAll re-wraps identically.
|
|
705
|
+
const part = result.content[0];
|
|
706
|
+
return part.type === "text" ? part.text : JSON.stringify(part);
|
|
707
|
+
},
|
|
708
|
+
},
|
|
709
|
+
{
|
|
710
|
+
name: "context-init",
|
|
711
|
+
label: "Context Init",
|
|
712
|
+
description: "Initialize the substrate dir (bootstrap pointer + dirs only; run accept-all + install to populate).",
|
|
713
|
+
promptSnippet: "Initialize the substrate dir (bootstrap pointer + dirs only; run accept-all + install to populate)",
|
|
714
|
+
parameters: Type.Object({
|
|
715
|
+
contextDir: Type.String({
|
|
716
|
+
description: "Substrate dir name (e.g. .context). Required — no default.",
|
|
717
|
+
}),
|
|
718
|
+
}),
|
|
719
|
+
surface: "use",
|
|
720
|
+
authGated: true,
|
|
721
|
+
run(cwd, params) {
|
|
722
|
+
const result = initProject(cwd, params.contextDir);
|
|
723
|
+
return JSON.stringify(result, null, 2);
|
|
724
|
+
},
|
|
725
|
+
},
|
|
726
|
+
{
|
|
727
|
+
name: "context-accept-all",
|
|
728
|
+
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. Idempotent: never overwrites an existing config.",
|
|
730
|
+
promptSnippet: "Adopt the canonical conception as config (accept-all)",
|
|
731
|
+
parameters: Type.Object({}),
|
|
732
|
+
surface: "use",
|
|
733
|
+
authGated: true,
|
|
734
|
+
run(cwd, _params) {
|
|
735
|
+
let result;
|
|
736
|
+
try {
|
|
737
|
+
result = adoptConception(cwd);
|
|
738
|
+
}
|
|
739
|
+
catch (err) {
|
|
740
|
+
if (err instanceof BootstrapNotFoundError) {
|
|
741
|
+
return "substrate not initialized — run context-init first";
|
|
742
|
+
}
|
|
743
|
+
throw err;
|
|
744
|
+
}
|
|
745
|
+
return JSON.stringify(result);
|
|
746
|
+
},
|
|
747
|
+
},
|
|
748
|
+
{
|
|
749
|
+
name: "context-switch",
|
|
750
|
+
label: "Context Switch",
|
|
751
|
+
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
|
+
promptSnippet: "Switch the bootstrap pointer to a different substrate dir",
|
|
753
|
+
parameters: Type.Object({
|
|
754
|
+
target_dir: Type.String({
|
|
755
|
+
description: "Substrate dir name to switch to (e.g. '.context'). Required for default + create_new modes; ignored for to_previous mode.",
|
|
756
|
+
}),
|
|
757
|
+
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).",
|
|
759
|
+
})),
|
|
760
|
+
to_previous: Type.Optional(Type.Boolean({
|
|
761
|
+
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.",
|
|
762
|
+
})),
|
|
763
|
+
writer: Type.Optional(Type.Object({
|
|
764
|
+
kind: Type.String({
|
|
765
|
+
description: "Writer kind discriminator — overwritten by auth-gate to 'human' on confirm.",
|
|
766
|
+
}),
|
|
767
|
+
user: Type.String({
|
|
768
|
+
description: "Writer user — overwritten by auth-gate to the verified terminal-operator identity on confirm.",
|
|
769
|
+
}),
|
|
770
|
+
}, {
|
|
771
|
+
description: "DispatchContext.writer — stamped by auth-gate on operator confirm; in-body trusts the stamped value.",
|
|
772
|
+
})),
|
|
773
|
+
}),
|
|
774
|
+
surface: "use",
|
|
775
|
+
authGated: true,
|
|
776
|
+
run(cwd, params) {
|
|
777
|
+
// The auth-gate stamps event.input.writer to verified identity on
|
|
778
|
+
// confirm; the body trusts the stamped writer (auth-gate is the
|
|
779
|
+
// canonical identity check per FGAP-134 / FGAP-138 model). When the
|
|
780
|
+
// gate is bypassed (e.g., test harness), fall back to 'operator'
|
|
781
|
+
// rather than throwing — the same fallback policy the slash command
|
|
782
|
+
// path uses.
|
|
783
|
+
const writerIdentity = params.writer?.user ?? "operator";
|
|
784
|
+
try {
|
|
785
|
+
if (params.to_previous === true) {
|
|
786
|
+
const { from, to } = switchToPrevious(cwd, writerIdentity);
|
|
787
|
+
return JSON.stringify({ mode: "to_previous", from, to }, null, 2);
|
|
788
|
+
}
|
|
789
|
+
if (params.create_new === true) {
|
|
790
|
+
const { created } = switchAndCreate(cwd, params.target_dir, writerIdentity);
|
|
791
|
+
return JSON.stringify({ mode: "create_new", target_dir: params.target_dir, created }, null, 2);
|
|
792
|
+
}
|
|
793
|
+
switchToExisting(cwd, params.target_dir, writerIdentity);
|
|
794
|
+
return JSON.stringify({ mode: "existing", target_dir: params.target_dir }, null, 2);
|
|
795
|
+
}
|
|
796
|
+
catch (err) {
|
|
797
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
798
|
+
return `context-switch failed: ${msg}`;
|
|
799
|
+
}
|
|
800
|
+
},
|
|
801
|
+
},
|
|
802
|
+
{
|
|
803
|
+
name: "context-list",
|
|
804
|
+
label: "Context List",
|
|
805
|
+
description: "Enumerate top-level dirs under cwd containing a config.json (switchable substrates). Marks the active one with isActive=true. Read-only.",
|
|
806
|
+
promptSnippet: "List switchable substrate dirs under cwd",
|
|
807
|
+
parameters: Type.Object({}),
|
|
808
|
+
surface: "use",
|
|
809
|
+
run(cwd, _params) {
|
|
810
|
+
const subs = listSubstrates(cwd);
|
|
811
|
+
return JSON.stringify(subs, null, 2);
|
|
812
|
+
},
|
|
813
|
+
},
|
|
814
|
+
{
|
|
815
|
+
name: "context-archive",
|
|
816
|
+
label: "Context Archive",
|
|
817
|
+
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
|
+
promptSnippet: "Archive a non-active substrate dir to archive/<dir>/",
|
|
819
|
+
parameters: Type.Object({
|
|
820
|
+
target_dir: Type.String({
|
|
821
|
+
description: "Substrate dir name to archive (e.g. '.project'). Refused if it is the active substrate.",
|
|
822
|
+
}),
|
|
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
|
+
}),
|
|
832
|
+
surface: "use",
|
|
833
|
+
authGated: true,
|
|
834
|
+
run(cwd, params) {
|
|
835
|
+
try {
|
|
836
|
+
const { from, to } = archiveSubstrate(cwd, params.target_dir);
|
|
837
|
+
return JSON.stringify({ from, to }, null, 2);
|
|
838
|
+
}
|
|
839
|
+
catch (err) {
|
|
840
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
841
|
+
return `context-archive failed: ${msg}`;
|
|
842
|
+
}
|
|
843
|
+
},
|
|
844
|
+
},
|
|
845
|
+
{
|
|
846
|
+
name: "filter-block-items",
|
|
847
|
+
label: "Filter Block Items",
|
|
848
|
+
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
|
+
promptSnippet: "Filter a block's items by a predicate — eq / neq / in / matches against a single field",
|
|
850
|
+
parameters: Type.Object({
|
|
851
|
+
block: Type.String({
|
|
852
|
+
description: "Block name (e.g., 'tasks', 'decisions', 'framework-gaps', 'context-contracts')",
|
|
853
|
+
}),
|
|
854
|
+
field: Type.String({ description: "Item field to test (e.g., 'status', 'priority', 'id')" }),
|
|
855
|
+
op: Type.Union([Type.Literal("eq"), Type.Literal("neq"), Type.Literal("in"), Type.Literal("matches")], {
|
|
856
|
+
description: "Comparison operator: eq (===), neq (!==), in (value is array, item[field] in it), matches (regexp test on string)",
|
|
857
|
+
}),
|
|
858
|
+
value: Type.Unknown({
|
|
859
|
+
description: "Comparison value — scalar for eq/neq, array for in, regexp pattern string for matches",
|
|
860
|
+
}),
|
|
861
|
+
}),
|
|
862
|
+
surface: "use",
|
|
863
|
+
run(cwd, params) {
|
|
864
|
+
const result = filterBlockItems(cwd, params.block, {
|
|
865
|
+
field: params.field,
|
|
866
|
+
op: params.op,
|
|
867
|
+
value: params.value,
|
|
868
|
+
});
|
|
869
|
+
const envelope = serializeForRead(result, {
|
|
870
|
+
label: `${params.block} filtered`,
|
|
871
|
+
overCapDirective: { tool: "read-block-page", hint: "or refine the predicate" },
|
|
872
|
+
});
|
|
873
|
+
return envelope.content;
|
|
874
|
+
},
|
|
875
|
+
},
|
|
876
|
+
{
|
|
877
|
+
name: "resolve-item-by-id",
|
|
878
|
+
label: "Resolve Item By Id",
|
|
879
|
+
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
|
+
promptSnippet: "Resolve a kind-prefixed ID (DEC-/FEAT-/FGAP-/issue-/REQ-/TASK-/etc.) to its owning block and item",
|
|
881
|
+
parameters: Type.Object({
|
|
882
|
+
id: Type.String({ description: "Kind-prefixed ID, e.g., DEC-NNNN / FEAT-NNN / FGAP-NNN / ISSUE-NNN" }),
|
|
883
|
+
}),
|
|
884
|
+
surface: "use",
|
|
885
|
+
run(cwd, params) {
|
|
886
|
+
const result = resolveItemById(cwd, params.id);
|
|
887
|
+
return JSON.stringify(result, null, 2);
|
|
888
|
+
},
|
|
889
|
+
},
|
|
890
|
+
{
|
|
891
|
+
name: "read-block-item",
|
|
892
|
+
label: "Read Block Item",
|
|
893
|
+
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
|
+
promptSnippet: "Read one item from a block by id (block-scoped; null if absent)",
|
|
895
|
+
parameters: Type.Object({
|
|
896
|
+
block: Type.String({ description: "Block name (e.g., 'tasks', 'decisions', 'framework-gaps')" }),
|
|
897
|
+
id: Type.String({ description: "Item id within the block (e.g., 'TASK-NNN')" }),
|
|
898
|
+
}),
|
|
899
|
+
surface: "use",
|
|
900
|
+
run(cwd, params) {
|
|
901
|
+
const result = readBlockItem(cwd, params.block, params.id);
|
|
902
|
+
// whole: the item is already the addressed element — don't re-page its
|
|
903
|
+
// intrinsic arrays; preserve the single-item|null output contract.
|
|
904
|
+
const envelope = serializeForRead(result, { whole: true, label: `${params.block} ${params.id}` });
|
|
905
|
+
return envelope.content;
|
|
906
|
+
},
|
|
907
|
+
},
|
|
908
|
+
{
|
|
909
|
+
name: "read-block-page",
|
|
910
|
+
label: "Read Block Page",
|
|
911
|
+
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
|
+
promptSnippet: "Paginate a block's items — offset + limit; returns {items,total,hasMore}",
|
|
913
|
+
parameters: Type.Object({
|
|
914
|
+
block: Type.String({ description: "Block name (e.g., 'framework-gaps', 'decisions', 'issues')" }),
|
|
915
|
+
offset: Type.Optional(Type.Integer({ minimum: 0, description: "Start index (default 0)" })),
|
|
916
|
+
limit: Type.Optional(Type.Integer({ minimum: 1, description: "Max items to return (default 50)" })),
|
|
917
|
+
}),
|
|
918
|
+
surface: "use",
|
|
919
|
+
run(cwd, params) {
|
|
920
|
+
const result = readBlockPage(cwd, params.block, { offset: params.offset, limit: params.limit });
|
|
921
|
+
// whole: readBlockPage ALREADY paged — preserve the {items,total,hasMore}
|
|
922
|
+
// output contract; do not let serializeForRead re-page the items array.
|
|
923
|
+
const envelope = serializeForRead(result, { whole: true, label: `${params.block} page` });
|
|
924
|
+
return envelope.content;
|
|
925
|
+
},
|
|
926
|
+
},
|
|
927
|
+
{
|
|
928
|
+
name: "join-blocks",
|
|
929
|
+
label: "Join Blocks",
|
|
930
|
+
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
|
+
promptSnippet: "Join two blocks in one call — by relation edge or shared field; returns {left,right[]} pairs",
|
|
932
|
+
parameters: Type.Object({
|
|
933
|
+
leftBlock: Type.String({ description: "Left block name (e.g., 'tasks')" }),
|
|
934
|
+
rightBlock: Type.String({ description: "Right block name (e.g., 'verification')" }),
|
|
935
|
+
relationType: Type.Optional(Type.String({ description: "Edge mode: relations.json relation_type" })),
|
|
936
|
+
leftField: Type.Optional(Type.String({ description: "Field mode: left item field" })),
|
|
937
|
+
rightField: Type.Optional(Type.String({ description: "Field mode: right item field" })),
|
|
938
|
+
leftEndpoint: Type.Optional(Type.Union([Type.Literal("parent"), Type.Literal("child")], {
|
|
939
|
+
description: "Edge mode: is the left item the edge parent (default) or child",
|
|
940
|
+
})),
|
|
941
|
+
whereField: Type.Optional(Type.String({ description: "Optional left pre-filter field" })),
|
|
942
|
+
whereOp: Type.Optional(Type.Union([Type.Literal("eq"), Type.Literal("neq"), Type.Literal("in"), Type.Literal("matches")])),
|
|
943
|
+
whereValue: Type.Optional(Type.Unknown({ description: "Optional left pre-filter value" })),
|
|
944
|
+
}),
|
|
945
|
+
surface: "use",
|
|
946
|
+
run(cwd, params) {
|
|
947
|
+
const leftPredicate = params.whereField !== undefined
|
|
948
|
+
? { field: params.whereField, op: params.whereOp ?? "eq", value: params.whereValue }
|
|
949
|
+
: undefined;
|
|
950
|
+
const result = joinBlocks(cwd, {
|
|
951
|
+
leftBlock: params.leftBlock,
|
|
952
|
+
rightBlock: params.rightBlock,
|
|
953
|
+
relationType: params.relationType,
|
|
954
|
+
leftField: params.leftField,
|
|
955
|
+
rightField: params.rightField,
|
|
956
|
+
leftEndpoint: params.leftEndpoint,
|
|
957
|
+
leftPredicate,
|
|
958
|
+
});
|
|
959
|
+
const envelope = serializeForRead(result, {
|
|
960
|
+
label: `${params.leftBlock} ⋈ ${params.rightBlock}`,
|
|
961
|
+
overCapDirective: {
|
|
962
|
+
tool: "join-blocks",
|
|
963
|
+
hint: "refine the relation/field or pre-filter the left block",
|
|
964
|
+
},
|
|
965
|
+
});
|
|
966
|
+
return envelope.content;
|
|
967
|
+
},
|
|
968
|
+
},
|
|
969
|
+
{
|
|
970
|
+
name: "resolve-items-by-id",
|
|
971
|
+
label: "Resolve Items By Id (Bulk)",
|
|
972
|
+
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
|
+
promptSnippet: "Resolve a batch of kind-prefixed ids (DEC-/FGAP-/TASK-/issue-/REQ-/...) in one call",
|
|
974
|
+
parameters: Type.Object({
|
|
975
|
+
ids: Type.Array(Type.String(), {
|
|
976
|
+
description: "Array of kind-prefixed ids (DEC-/FGAP-/TASK-/issue-/REQ-/...) to resolve in one call",
|
|
977
|
+
}),
|
|
978
|
+
}),
|
|
979
|
+
surface: "use",
|
|
980
|
+
run(cwd, params) {
|
|
981
|
+
const resultMap = resolveItemsByIds(cwd, params.ids);
|
|
982
|
+
const obj = {};
|
|
983
|
+
for (const [id, loc] of resultMap)
|
|
984
|
+
obj[id] = loc;
|
|
985
|
+
// whole: an id→location map keyed by arbitrary ids — not a pageable
|
|
986
|
+
// collection; serialize the map verbatim.
|
|
987
|
+
const envelope = serializeForRead(obj, { whole: true, label: "resolved ids" });
|
|
988
|
+
return envelope.content;
|
|
989
|
+
},
|
|
990
|
+
},
|
|
991
|
+
{
|
|
992
|
+
name: "complete-task",
|
|
993
|
+
label: "Complete Task",
|
|
994
|
+
description: "Complete a task with verification gate — requires a passing verification entry targeting the task.",
|
|
995
|
+
promptSnippet: "Complete a task — gates on passing verification before updating status",
|
|
996
|
+
parameters: Type.Object({
|
|
997
|
+
taskId: Type.String({ description: "Task ID to complete" }),
|
|
998
|
+
verificationId: Type.String({
|
|
999
|
+
description: "Verification entry ID (must target this task with status 'passed')",
|
|
1000
|
+
}),
|
|
1001
|
+
}),
|
|
1002
|
+
surface: "use",
|
|
1003
|
+
run(cwd, params) {
|
|
1004
|
+
const result = completeTask(cwd, params.taskId, params.verificationId);
|
|
1005
|
+
return `Task '${result.taskId}' completed (was '${result.previousStatus}'). Verification: ${result.verificationId} (${result.verificationStatus})`;
|
|
1006
|
+
},
|
|
1007
|
+
},
|
|
1008
|
+
{
|
|
1009
|
+
name: "context-validate-relations",
|
|
1010
|
+
label: "Context Validate Relations",
|
|
1011
|
+
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
|
+
promptSnippet: "Validate substrate relations against config + items",
|
|
1013
|
+
parameters: Type.Object({}),
|
|
1014
|
+
surface: "use",
|
|
1015
|
+
run(cwd, _params) {
|
|
1016
|
+
const result = validateContextRelations(cwd);
|
|
1017
|
+
return JSON.stringify(result, null, 2);
|
|
1018
|
+
},
|
|
1019
|
+
},
|
|
1020
|
+
{
|
|
1021
|
+
name: "context-edges-for-lens",
|
|
1022
|
+
label: "Context Edges For Lens",
|
|
1023
|
+
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
|
+
promptSnippet: "Materialize edges for a named lens (auto-derived or hand-curated)",
|
|
1025
|
+
parameters: Type.Object({
|
|
1026
|
+
lensId: Type.String({ description: "Lens id from config.lenses[].id" }),
|
|
1027
|
+
}),
|
|
1028
|
+
surface: "use",
|
|
1029
|
+
run(cwd, params) {
|
|
1030
|
+
const result = edgesForLensByName(cwd, params.lensId);
|
|
1031
|
+
const envelope = serializeForRead(result, { label: `edges for lens ${params.lensId}` });
|
|
1032
|
+
return envelope.content;
|
|
1033
|
+
},
|
|
1034
|
+
},
|
|
1035
|
+
{
|
|
1036
|
+
name: "context-walk-descendants",
|
|
1037
|
+
label: "Context Walk Descendants",
|
|
1038
|
+
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
|
+
promptSnippet: "Walk closure-table descendants under a relation_type",
|
|
1040
|
+
parameters: Type.Object({
|
|
1041
|
+
parentId: Type.String({ description: "Parent id (canonical id or lens bin name)" }),
|
|
1042
|
+
relationType: Type.String({ description: "Relation type from config.relation_types[].canonical_id" }),
|
|
1043
|
+
}),
|
|
1044
|
+
surface: "use",
|
|
1045
|
+
run(cwd, params) {
|
|
1046
|
+
const result = walkLensDescendants(cwd, params.parentId, params.relationType);
|
|
1047
|
+
return JSON.stringify(result, null, 2);
|
|
1048
|
+
},
|
|
1049
|
+
},
|
|
1050
|
+
{
|
|
1051
|
+
name: "walk-ancestors",
|
|
1052
|
+
label: "Walk Ancestors",
|
|
1053
|
+
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
|
+
promptSnippet: "Walk closure-table ancestors under a relation_type",
|
|
1055
|
+
parameters: Type.Object({
|
|
1056
|
+
itemId: Type.String({ description: "Child item id whose ancestors are sought" }),
|
|
1057
|
+
relationType: Type.String({ description: "Relation type from config.relation_types[].canonical_id" }),
|
|
1058
|
+
}),
|
|
1059
|
+
surface: "use",
|
|
1060
|
+
run(cwd, params) {
|
|
1061
|
+
const result = walkAncestorsByLens(cwd, params.itemId, params.relationType);
|
|
1062
|
+
const envelope = serializeForRead(result, { label: `ancestors of ${params.itemId}` });
|
|
1063
|
+
return envelope.content;
|
|
1064
|
+
},
|
|
1065
|
+
},
|
|
1066
|
+
{
|
|
1067
|
+
name: "find-references",
|
|
1068
|
+
label: "Find References",
|
|
1069
|
+
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
|
+
promptSnippet: "Find closure-table edges incident on an item id",
|
|
1071
|
+
parameters: Type.Object({
|
|
1072
|
+
itemId: Type.String({ description: "Item id whose incident edges are sought" }),
|
|
1073
|
+
direction: Type.Optional(Type.Union([Type.Literal("inbound"), Type.Literal("outbound"), Type.Literal("both")], {
|
|
1074
|
+
description: "inbound: edges where child === itemId; outbound: edges where parent === itemId; both: union (default).",
|
|
1075
|
+
})),
|
|
1076
|
+
}),
|
|
1077
|
+
surface: "use",
|
|
1078
|
+
run(cwd, params) {
|
|
1079
|
+
const result = findReferencesInRepo(cwd, params.itemId, params.direction);
|
|
1080
|
+
const envelope = serializeForRead(result, { label: `edges on ${params.itemId}` });
|
|
1081
|
+
return envelope.content;
|
|
1082
|
+
},
|
|
1083
|
+
},
|
|
1084
|
+
{
|
|
1085
|
+
name: "gather-execution-context",
|
|
1086
|
+
label: "Gather Execution Context",
|
|
1087
|
+
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
|
+
promptSnippet: "Compose ContextBundle for unit + context-contract-declared bundle_relation_types",
|
|
1089
|
+
parameters: Type.Object({
|
|
1090
|
+
unitId: Type.String({ description: "Work-unit id (e.g. TASK-NNN / DEC-NNNN / FGAP-NNN)" }),
|
|
1091
|
+
kind: Type.String({
|
|
1092
|
+
description: "Unit-kind type tag (e.g. 'task', 'decision', 'verification') matching a context-contract entry's unit_kind",
|
|
1093
|
+
}),
|
|
1094
|
+
maxDepth: Type.Optional(Type.Integer({
|
|
1095
|
+
minimum: 1,
|
|
1096
|
+
description: "Override per-relation-type max_depth via Math.min against each spec.max_depth",
|
|
1097
|
+
})),
|
|
1098
|
+
}),
|
|
1099
|
+
surface: "use",
|
|
1100
|
+
run(cwd, params) {
|
|
1101
|
+
const result = gatherExecutionContext(cwd, params);
|
|
1102
|
+
// whole: a structured ContextBundle (unit + perRelationType buckets) —
|
|
1103
|
+
// preserve the bundle shape rather than paging any single inner array.
|
|
1104
|
+
const envelope = serializeForRead(result, { whole: true, label: `bundle ${params.unitId}` });
|
|
1105
|
+
return envelope.content;
|
|
1106
|
+
},
|
|
1107
|
+
},
|
|
1108
|
+
{
|
|
1109
|
+
name: "context-roadmap-load",
|
|
1110
|
+
label: "Context: load roadmap",
|
|
1111
|
+
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
|
+
promptSnippet: "Load a roadmap by id",
|
|
1113
|
+
parameters: Type.Object({
|
|
1114
|
+
roadmapId: Type.String({ description: "ROADMAP-NNN id from <config.root>/roadmap.json" }),
|
|
1115
|
+
}),
|
|
1116
|
+
surface: "use",
|
|
1117
|
+
run(cwd, params) {
|
|
1118
|
+
const view = loadRoadmap(cwd, params.roadmapId);
|
|
1119
|
+
if ("error" in view) {
|
|
1120
|
+
const envErr = serializeForRead(view, { whole: true, label: `roadmap ${params.roadmapId} (error)` });
|
|
1121
|
+
return envErr.content;
|
|
1122
|
+
}
|
|
1123
|
+
// whole: a structured RoadmapView (phases + lens-views + rollups) — keep
|
|
1124
|
+
// the view shape intact rather than paging an inner array.
|
|
1125
|
+
const envelope = serializeForRead(serializeRoadmapView(view), {
|
|
1126
|
+
whole: true,
|
|
1127
|
+
label: `roadmap ${params.roadmapId}`,
|
|
1128
|
+
});
|
|
1129
|
+
return envelope.content;
|
|
1130
|
+
},
|
|
1131
|
+
},
|
|
1132
|
+
{
|
|
1133
|
+
name: "context-roadmap-render",
|
|
1134
|
+
label: "Context: render roadmap",
|
|
1135
|
+
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
|
+
promptSnippet: "Render a roadmap as markdown",
|
|
1137
|
+
parameters: Type.Object({
|
|
1138
|
+
roadmapId: Type.String({ description: "ROADMAP-NNN id from <config.root>/roadmap.json" }),
|
|
1139
|
+
}),
|
|
1140
|
+
surface: "use",
|
|
1141
|
+
run(cwd, params) {
|
|
1142
|
+
const view = loadRoadmap(cwd, params.roadmapId);
|
|
1143
|
+
if ("error" in view) {
|
|
1144
|
+
return JSON.stringify(view, null, 2);
|
|
1145
|
+
}
|
|
1146
|
+
const naming = loadContext(cwd).config?.naming;
|
|
1147
|
+
return renderRoadmap(view, naming);
|
|
1148
|
+
},
|
|
1149
|
+
},
|
|
1150
|
+
{
|
|
1151
|
+
name: "context-roadmap-validate",
|
|
1152
|
+
label: "Context: validate roadmap(s)",
|
|
1153
|
+
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
|
+
promptSnippet: "Validate roadmaps",
|
|
1155
|
+
parameters: Type.Object({
|
|
1156
|
+
roadmapId: Type.Optional(Type.String({ description: "Filter to issues matching this roadmap_id (omit for full-project validation)" })),
|
|
1157
|
+
}),
|
|
1158
|
+
surface: "use",
|
|
1159
|
+
run(cwd, params) {
|
|
1160
|
+
const result = validateRoadmaps(cwd);
|
|
1161
|
+
const filtered = params.roadmapId
|
|
1162
|
+
? result.issues.filter((i) => !i.roadmap_id || i.roadmap_id === params.roadmapId)
|
|
1163
|
+
: result.issues;
|
|
1164
|
+
return JSON.stringify({ status: result.status, issues: filtered }, null, 2);
|
|
1165
|
+
},
|
|
1166
|
+
},
|
|
1167
|
+
{
|
|
1168
|
+
name: "context-roadmap-list",
|
|
1169
|
+
label: "Context: list roadmaps",
|
|
1170
|
+
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
|
+
promptSnippet: "List roadmaps",
|
|
1172
|
+
parameters: Type.Object({}),
|
|
1173
|
+
surface: "use",
|
|
1174
|
+
run(cwd, _params) {
|
|
1175
|
+
return JSON.stringify(listRoadmaps(cwd), null, 2);
|
|
1176
|
+
},
|
|
1177
|
+
},
|
|
1178
|
+
];
|
|
1179
|
+
/**
|
|
1180
|
+
* The pi-context-owned set of tool names that require human-authorization at
|
|
1181
|
+
* the pi-agent-dispatch tool_call gate. Derived from the registry's authGated
|
|
1182
|
+
* flags rather than hand-maintained, so a single source — the OpDefinition's
|
|
1183
|
+
* authGated field — names both the CLI's credentialed-confirmation hint and the
|
|
1184
|
+
* gate's aggregated allowlist. pi-agent-dispatch imports this via the `./ops`
|
|
1185
|
+
* subpath and folds it into AUTH_REQUIRED_TOOLS alongside the other packages'
|
|
1186
|
+
* owned sets. The gate at the pi-agent-dispatch layer remains the enforcement
|
|
1187
|
+
* point; this list is the source of pi-context's contribution to it.
|
|
1188
|
+
*/
|
|
1189
|
+
export const gatedTools = ops.filter((o) => o.authGated).map((o) => o.name);
|
|
1190
|
+
/**
|
|
1191
|
+
* The factory PI handle captured at registerAll time. The list-tools op needs
|
|
1192
|
+
* the introspection surface (getAllTools / getActiveTools) which lives on
|
|
1193
|
+
* ExtensionAPI, not on the per-call ExtensionContext. Captured once when the
|
|
1194
|
+
* extension registers its tools; null until then. Module-scoped here so the
|
|
1195
|
+
* op `run` closures (which receive only cwd + params) can reach it without
|
|
1196
|
+
* threading the handle through every signature.
|
|
1197
|
+
*/
|
|
1198
|
+
let boundPi = null;
|
|
1199
|
+
/**
|
|
1200
|
+
* 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, await run, place the
|
|
1202
|
+
* returned string at content[0].text. Behavior-identical to the prior inline
|
|
1203
|
+
* registrations.
|
|
1204
|
+
*/
|
|
1205
|
+
export function registerAll(pi) {
|
|
1206
|
+
boundPi = pi;
|
|
1207
|
+
for (const op of ops) {
|
|
1208
|
+
pi.registerTool({
|
|
1209
|
+
name: op.name,
|
|
1210
|
+
label: op.label,
|
|
1211
|
+
description: op.description,
|
|
1212
|
+
promptSnippet: op.promptSnippet,
|
|
1213
|
+
parameters: op.parameters,
|
|
1214
|
+
async execute(_toolCallId, params, _signal, _onUpdate, ctx) {
|
|
1215
|
+
return {
|
|
1216
|
+
details: undefined,
|
|
1217
|
+
content: [{ type: "text", text: await op.run(ctx.cwd, params) }],
|
|
1218
|
+
};
|
|
1219
|
+
},
|
|
1220
|
+
});
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
//# sourceMappingURL=ops-registry.js.map
|