@gtrabanco/pi-agentic-workflow 0.7.1 → 0.8.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/README.es.md +22 -3
- package/README.md +21 -3
- package/dist/config/schema.js +32 -1
- package/dist/config/types.d.ts +10 -1
- package/dist/config/types.js +2 -0
- package/dist/extension/factory.js +26 -15
- package/dist/extension/index.js +44 -1
- package/dist/routing/dispatch.js +39 -3
- package/dist/routing/types.d.ts +15 -0
- package/dist/routing/types.js +2 -0
- package/dist/settings/console.d.ts +11 -0
- package/dist/settings/console.js +203 -19
- package/dist/settings/picker.d.ts +31 -0
- package/dist/settings/picker.js +93 -0
- package/dist/settings/view.js +4 -1
- package/package.json +13 -8
- package/skills/execute-phase/SKILL.md +6 -4
- package/skills/execute-phase/references/BATCH_AND_PORTABILITY.md +5 -3
- package/skills/execute-phase/references/CLOSEOUT.md +6 -4
- package/skills/execute-phase/references/FOLDING.md +2 -1
- package/skills/execute-phase/references/UNIT_LOOP.md +2 -1
- package/skills/pre-execution-review/SKILL.md +1 -1
- package/skills/pre-execution-review/references/LEDGERS.md +10 -7
- package/skills/pre-execution-review/references/SNAPSHOT.md +2 -2
- package/skills/review-change/SKILL.md +15 -2
- package/skills/review-change/references/OUTPUT_AND_GUARDRAILS.md +5 -4
- package/skills/review-change/references/PERSIST_AND_DECIDE.md +8 -0
- package/skills/review-change/references/REVIEW_PROCESS.md +4 -3
- package/skills/review-plan/SKILL.md +1 -1
- package/skills/review-plan/references/CHECKS.md +1 -1
- package/skills/review-plan/references/OUTPUT.md +2 -2
- package/skills/review-spec/SKILL.md +1 -1
- package/skills/review-spec/references/CHECKS.md +1 -1
- package/skills/review-spec/references/OUTPUT.md +1 -1
- package/skills/ship-roadmap/SKILL.md +2 -2
- package/skills/ship-roadmap/references/ADVANCE.md +1 -3
- package/skills/ship-roadmap/references/MODEL_ROUTING.md +1 -1
- package/skills/workflow-status/SKILL.md +1 -1
- package/skills/workflow-status/references/PRE_EXECUTION.md +1 -1
- package/skills/workflow-status/references/SENSOR_CORE.md +1 -1
package/dist/settings/console.js
CHANGED
|
@@ -8,8 +8,9 @@
|
|
|
8
8
|
// the operator must fix, not noise to clobber), and the project scope is refused
|
|
9
9
|
// while the project is untrusted — the same gate the loader applies (AC13).
|
|
10
10
|
import { loadConfig, configFilePaths } from "../config/load.js";
|
|
11
|
+
import { effectiveRoute } from "../config/merge.js";
|
|
11
12
|
import { parseConfigFile, parseModelReference } from "../config/schema.js";
|
|
12
|
-
import { THINKING_LEVELS, UNAVAILABLE_ROUTE_POLICIES } from "../config/types.js";
|
|
13
|
+
import { MAX_MODEL_CHAIN, THINKING_LEVELS, UNAVAILABLE_ROUTE_POLICIES } from "../config/types.js";
|
|
13
14
|
import { renderMergedConfig, routePath, DEFAULT_ROUTE } from "./view.js";
|
|
14
15
|
/** The console's questions. `test/settings-console.test.mjs` drives the flow
|
|
15
16
|
* through these strings, so renaming one fails the tests that use it rather than
|
|
@@ -27,12 +28,23 @@ export const prompts = {
|
|
|
27
28
|
setDefaultRoute: "Set the default route",
|
|
28
29
|
setOverride: "Set a command override",
|
|
29
30
|
clearOverride: "Clear a command override",
|
|
31
|
+
bulkApply: "Apply one route to several commands",
|
|
32
|
+
bulkClear: "Clear several overrides",
|
|
33
|
+
addAnother: "Add another?",
|
|
30
34
|
policy: "Set the unavailable-route policy",
|
|
31
35
|
save: "Save",
|
|
32
36
|
cancel: "Cancel",
|
|
33
37
|
model: (target) => `Model for ${target}?`,
|
|
34
38
|
modelPicked: (target) => `Which model for ${target}?`,
|
|
35
39
|
thinking: (target) => `Thinking level for ${target}?`,
|
|
40
|
+
fields: "Which fields should change?",
|
|
41
|
+
fieldsBoth: "model and thinking",
|
|
42
|
+
fieldsModel: "model only",
|
|
43
|
+
fieldsThinking: "thinking only",
|
|
44
|
+
chainAction: (target) => `Model chain for ${target}?`,
|
|
45
|
+
chainAppend: "Add a fallback",
|
|
46
|
+
chainRemoveLast: "Remove the last",
|
|
47
|
+
chainDone: "Done",
|
|
36
48
|
};
|
|
37
49
|
const GLOBAL_LABEL = "Global";
|
|
38
50
|
const PROJECT_LABEL = "Project";
|
|
@@ -40,7 +52,10 @@ const TYPED = "Type another reference…";
|
|
|
40
52
|
const INHERIT = "inherit";
|
|
41
53
|
export async function runSettingsConsole(deps) {
|
|
42
54
|
const paths = configFilePaths(deps.agentDir, deps.cwd);
|
|
43
|
-
|
|
55
|
+
const merged = loadConfig({ agentDir: deps.agentDir, cwd: deps.cwd, projectTrusted: deps.projectTrusted, readFile: deps.readFile });
|
|
56
|
+
deps.ui.notify(renderMergedConfig(merged, deps.commands).join("\n"), "info");
|
|
57
|
+
/** The value in force for a target (the merged route, or the default when the target is "the default route"). */
|
|
58
|
+
const currentFor = (target) => effectiveRoute(merged.config, target);
|
|
44
59
|
const opened = await openAScope(deps, paths.global, paths.project);
|
|
45
60
|
if (!opened)
|
|
46
61
|
return { status: "cancelled", edited: false };
|
|
@@ -53,6 +68,8 @@ export async function runSettingsConsole(deps) {
|
|
|
53
68
|
prompts.setDefaultRoute,
|
|
54
69
|
prompts.setOverride,
|
|
55
70
|
prompts.clearOverride,
|
|
71
|
+
prompts.bulkApply,
|
|
72
|
+
prompts.bulkClear,
|
|
56
73
|
prompts.policy,
|
|
57
74
|
...(deps.routing?.inFlight() ? [prompts.undoInFlight] : []),
|
|
58
75
|
prompts.save,
|
|
@@ -67,7 +84,7 @@ export async function runSettingsConsole(deps) {
|
|
|
67
84
|
return { status: "cancelled", edited: dirty(draft, original) ? await discard(deps.ui) : false };
|
|
68
85
|
}
|
|
69
86
|
if (choice === prompts.setDefaultRoute) {
|
|
70
|
-
const edited = await editRoute(deps, DEFAULT_ROUTE);
|
|
87
|
+
const edited = await editRoute(deps, DEFAULT_ROUTE, currentFor(DEFAULT_ROUTE));
|
|
71
88
|
if (edited)
|
|
72
89
|
draft = { ...draft, default: edited };
|
|
73
90
|
continue;
|
|
@@ -76,7 +93,7 @@ export async function runSettingsConsole(deps) {
|
|
|
76
93
|
const name = await pickCommand(deps, commandChoices(deps, draft));
|
|
77
94
|
if (name === undefined)
|
|
78
95
|
continue;
|
|
79
|
-
const route = await editRoute(deps, name);
|
|
96
|
+
const route = await editRoute(deps, name, currentFor(name));
|
|
80
97
|
if (route)
|
|
81
98
|
draft = { ...draft, commands: { ...draft.commands, [name]: route } };
|
|
82
99
|
continue;
|
|
@@ -90,6 +107,35 @@ export async function runSettingsConsole(deps) {
|
|
|
90
107
|
draft = { ...draft, commands: rest };
|
|
91
108
|
continue;
|
|
92
109
|
}
|
|
110
|
+
if (choice === prompts.bulkApply) {
|
|
111
|
+
// One field pass (model via the chain builder + thinking), applied to every
|
|
112
|
+
// selected command. A reference missing from the live registry warns per
|
|
113
|
+
// command but never blocks the write (OB-4 — dispatch's probe stays the
|
|
114
|
+
// authoritative availability gate).
|
|
115
|
+
const selected = await pickCommandsMulti(deps, commandChoices(deps, draft));
|
|
116
|
+
if (selected === undefined || selected.length === 0)
|
|
117
|
+
continue;
|
|
118
|
+
const route = await editRoute(deps, selected[0], currentFor(selected[0]));
|
|
119
|
+
if (route === undefined)
|
|
120
|
+
continue;
|
|
121
|
+
const nextCommands = { ...draft.commands };
|
|
122
|
+
for (const name of selected) {
|
|
123
|
+
nextCommands[name] = route;
|
|
124
|
+
warnMissingModel(deps, name, route.model ?? INHERIT);
|
|
125
|
+
}
|
|
126
|
+
draft = { ...draft, commands: nextCommands };
|
|
127
|
+
continue;
|
|
128
|
+
}
|
|
129
|
+
if (choice === prompts.bulkClear) {
|
|
130
|
+
const names = await pickCommandsMulti(deps, Object.keys(draft.commands ?? {}));
|
|
131
|
+
if (names === undefined || names.length === 0)
|
|
132
|
+
continue;
|
|
133
|
+
const rest = { ...draft.commands };
|
|
134
|
+
for (const name of names)
|
|
135
|
+
delete rest[name];
|
|
136
|
+
draft = { ...draft, commands: rest };
|
|
137
|
+
continue;
|
|
138
|
+
}
|
|
93
139
|
if (choice === prompts.policy) {
|
|
94
140
|
const picked = await deps.ui.select(prompts.policyChoice, [...UNAVAILABLE_ROUTE_POLICIES]);
|
|
95
141
|
if (isPolicy(picked))
|
|
@@ -133,18 +179,54 @@ async function openAScope(deps, globalPath, projectPath) {
|
|
|
133
179
|
}
|
|
134
180
|
}
|
|
135
181
|
/** Ask for a model and a thinking level; `undefined` means nothing changed. */
|
|
136
|
-
async function editRoute(deps, target) {
|
|
137
|
-
const
|
|
138
|
-
if (
|
|
139
|
-
return undefined;
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
182
|
+
async function editRoute(deps, target, current) {
|
|
183
|
+
const fields = await askFields(deps);
|
|
184
|
+
if (fields === undefined)
|
|
185
|
+
return undefined; // nothing marked — the route is left untouched (OB-3)
|
|
186
|
+
// Start from the value in force so only the marked field is replaced; the other
|
|
187
|
+
// keeps its merged value (AC4: changing only model asks no thinking question).
|
|
188
|
+
const route = {
|
|
189
|
+
...(current.model !== undefined ? { model: current.model } : {}),
|
|
190
|
+
...(current.thinking !== undefined ? { thinking: current.thinking } : {}),
|
|
191
|
+
};
|
|
192
|
+
if (fields.model) {
|
|
193
|
+
const model = await askModel(deps, target, current.model);
|
|
194
|
+
if (model === undefined)
|
|
195
|
+
return undefined;
|
|
196
|
+
route.model = model;
|
|
197
|
+
}
|
|
198
|
+
if (fields.thinking) {
|
|
199
|
+
const thinking = await askThinking(deps, target, current.thinking);
|
|
200
|
+
if (thinking === undefined)
|
|
201
|
+
return undefined;
|
|
202
|
+
route.thinking = thinking;
|
|
203
|
+
}
|
|
204
|
+
return route;
|
|
205
|
+
}
|
|
206
|
+
/** Which fields the operator wants to change; `undefined` means none (leave untouched). */
|
|
207
|
+
async function askFields(deps) {
|
|
208
|
+
const choice = await deps.ui.select(prompts.fields, [prompts.fieldsBoth, prompts.fieldsModel, prompts.fieldsThinking]);
|
|
209
|
+
if (choice === prompts.fieldsBoth)
|
|
210
|
+
return { model: true, thinking: true };
|
|
211
|
+
if (choice === prompts.fieldsModel)
|
|
212
|
+
return { model: true, thinking: false };
|
|
213
|
+
if (choice === prompts.fieldsThinking)
|
|
214
|
+
return { model: false, thinking: true };
|
|
215
|
+
return undefined;
|
|
144
216
|
}
|
|
145
|
-
|
|
217
|
+
/** Ask for one model entry; `undefined` means cancelled/skipped, `"inherit"` means the whole-route setting. */
|
|
218
|
+
async function pickModelEntry(deps, target, current) {
|
|
146
219
|
let answer;
|
|
147
|
-
if (deps.models && deps.models.length > 0) {
|
|
220
|
+
if (typeof deps.ui.pick === "function" && deps.models && deps.models.length > 0) {
|
|
221
|
+
// Rich seam: filterable, windowed, preselected to the value in force (OB-1).
|
|
222
|
+
const picked = await deps.ui.pick(prompts.modelPicked(target), [...deps.models, TYPED], {
|
|
223
|
+
initial: typeof current === "string" ? current : undefined,
|
|
224
|
+
});
|
|
225
|
+
answer = typeof picked === "string" ? picked : undefined;
|
|
226
|
+
if (answer === TYPED || answer === undefined)
|
|
227
|
+
answer = await deps.ui.input(prompts.model(target), "provider/modelId or inherit");
|
|
228
|
+
}
|
|
229
|
+
else if (deps.models && deps.models.length > 0) {
|
|
148
230
|
answer = await deps.ui.select(prompts.modelPicked(target), [...deps.models, TYPED]);
|
|
149
231
|
if (answer === TYPED || answer === undefined)
|
|
150
232
|
answer = await deps.ui.input(prompts.model(target), "provider/modelId or inherit");
|
|
@@ -157,17 +239,80 @@ async function askModel(deps, target) {
|
|
|
157
239
|
const value = answer.trim();
|
|
158
240
|
if (value === INHERIT)
|
|
159
241
|
return INHERIT;
|
|
160
|
-
|
|
242
|
+
const parts = parseModelReference(value);
|
|
243
|
+
if (!parts) {
|
|
161
244
|
// Rejected in the operator's terms and in the schema's: the value, and the
|
|
162
245
|
// field path the loader would name for the same mistake in a file (AC5).
|
|
163
246
|
deps.ui.notify(`Rejected: model must be "provider/modelId" or "inherit" — nothing was changed (${routePath(target)}.model).`, "error");
|
|
164
247
|
return undefined;
|
|
165
248
|
}
|
|
166
|
-
|
|
167
|
-
|
|
249
|
+
return `${parts.provider}/${parts.id}`;
|
|
250
|
+
}
|
|
251
|
+
/** Ask for a model; a lone reference is returned as-is, several references are returned as an ordered chain. */
|
|
252
|
+
async function askModel(deps, target, current) {
|
|
253
|
+
// The value in force (OB-3). For a route whose model is already a chain, seed
|
|
254
|
+
// the builder with the current references so the operator sees and edits the
|
|
255
|
+
// chain instead of rebuilding it blind (OB-17 / F2, amendment A1). Fresh and
|
|
256
|
+
// single-string editing keep the existing flow: the picker is preselected to
|
|
257
|
+
// the string value in force.
|
|
258
|
+
const seed = Array.isArray(current) ? [...current] : [];
|
|
259
|
+
if (seed.length > 0) {
|
|
260
|
+
// Already a chain: label the current chain, then let the operator append,
|
|
261
|
+
// trim, or keep it. Done with no change leaves the chain byte-identical.
|
|
262
|
+
deps.ui.notify(`Current ${target} model chain: ${seed.join(" → ")}`, "info");
|
|
263
|
+
return buildModelChain(deps, target, [...seed]);
|
|
264
|
+
}
|
|
265
|
+
const first = await pickModelEntry(deps, target, typeof current === "string" ? current : undefined);
|
|
266
|
+
if (first === undefined)
|
|
267
|
+
return undefined;
|
|
268
|
+
if (first === INHERIT)
|
|
269
|
+
return INHERIT;
|
|
270
|
+
return buildModelChain(deps, target, [first]);
|
|
168
271
|
}
|
|
169
|
-
|
|
170
|
-
|
|
272
|
+
/** Drive the ordered chain builder over the given starting chain; a single entry collapses to a bare reference. */
|
|
273
|
+
async function buildModelChain(deps, target, chain) {
|
|
274
|
+
for (;;) {
|
|
275
|
+
const atCap = chain.length >= MAX_MODEL_CHAIN;
|
|
276
|
+
if (atCap) {
|
|
277
|
+
deps.ui.notify(`A model chain is capped at ${MAX_MODEL_CHAIN} references — remove the last to make room (${routePath(target)}.model).`, "info");
|
|
278
|
+
}
|
|
279
|
+
const action = await deps.ui.select(prompts.chainAction(target), [
|
|
280
|
+
...(atCap ? [] : [prompts.chainAppend]),
|
|
281
|
+
prompts.chainRemoveLast,
|
|
282
|
+
prompts.chainDone,
|
|
283
|
+
]);
|
|
284
|
+
if (action === prompts.chainAppend) {
|
|
285
|
+
const next = await pickModelEntry(deps, target);
|
|
286
|
+
if (next === undefined)
|
|
287
|
+
continue;
|
|
288
|
+
if (next === INHERIT) {
|
|
289
|
+
deps.ui.notify(`Only "provider/modelId" references can be chain entries; "inherit" is a whole-route setting (${routePath(target)}.model).`, "error");
|
|
290
|
+
continue;
|
|
291
|
+
}
|
|
292
|
+
chain.push(next);
|
|
293
|
+
}
|
|
294
|
+
else if (action === prompts.chainRemoveLast) {
|
|
295
|
+
if (chain.length > 1)
|
|
296
|
+
chain.pop();
|
|
297
|
+
}
|
|
298
|
+
else {
|
|
299
|
+
break;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
return chain.length === 1 ? chain[0] : chain;
|
|
303
|
+
}
|
|
304
|
+
async function askThinking(deps, target, current) {
|
|
305
|
+
const options = [...THINKING_LEVELS, INHERIT];
|
|
306
|
+
let answer;
|
|
307
|
+
if (typeof deps.ui.pick === "function") {
|
|
308
|
+
const picked = await deps.ui.pick(prompts.thinking(target), options, {
|
|
309
|
+
initial: typeof current === "string" ? current : undefined,
|
|
310
|
+
});
|
|
311
|
+
answer = typeof picked === "string" ? picked : undefined;
|
|
312
|
+
}
|
|
313
|
+
else {
|
|
314
|
+
answer = await deps.ui.select(prompts.thinking(target), options);
|
|
315
|
+
}
|
|
171
316
|
if (answer === undefined)
|
|
172
317
|
return undefined;
|
|
173
318
|
if (answer === INHERIT || isThinkingLevel(answer))
|
|
@@ -182,6 +327,45 @@ async function pickCommand(deps, options) {
|
|
|
182
327
|
}
|
|
183
328
|
return deps.ui.select(prompts.command, [...options].sort((a, b) => a.localeCompare(b)));
|
|
184
329
|
}
|
|
330
|
+
/** Multi-select command picker over the seam's `multiple` mode; a non-rich UI falls back to repeated single selects. */
|
|
331
|
+
async function pickCommandsMulti(deps, options) {
|
|
332
|
+
if (options.length === 0) {
|
|
333
|
+
deps.ui.notify("There is no command to pick here.", "warning");
|
|
334
|
+
return undefined;
|
|
335
|
+
}
|
|
336
|
+
const sorted = [...options].sort((a, b) => a.localeCompare(b));
|
|
337
|
+
if (typeof deps.ui.pick === "function") {
|
|
338
|
+
const picked = await deps.ui.pick(prompts.command, sorted, { multiple: true });
|
|
339
|
+
if (picked === undefined)
|
|
340
|
+
return undefined;
|
|
341
|
+
return typeof picked === "string" ? [picked] : picked;
|
|
342
|
+
}
|
|
343
|
+
// Non-rich fallback: repeatedly select one candidate until the operator stops.
|
|
344
|
+
const chosen = [];
|
|
345
|
+
for (;;) {
|
|
346
|
+
const remaining = sorted.filter((option) => !chosen.includes(option));
|
|
347
|
+
if (remaining.length === 0)
|
|
348
|
+
break;
|
|
349
|
+
const picked = await deps.ui.select(prompts.command, remaining);
|
|
350
|
+
if (picked === undefined)
|
|
351
|
+
break;
|
|
352
|
+
chosen.push(picked);
|
|
353
|
+
if (!(await deps.ui.confirm(prompts.addAnother, `Picked ${chosen.join(", ")}.`)))
|
|
354
|
+
break;
|
|
355
|
+
}
|
|
356
|
+
return chosen.length > 0 ? chosen : undefined;
|
|
357
|
+
}
|
|
358
|
+
/** Advisory per-command warning when a chosen reference is absent from the live registry (OB-4). */
|
|
359
|
+
function warnMissingModel(deps, target, model) {
|
|
360
|
+
if (!deps.models || deps.models.length === 0)
|
|
361
|
+
return;
|
|
362
|
+
const refs = typeof model === "string" ? [model] : model;
|
|
363
|
+
for (const ref of refs) {
|
|
364
|
+
if (ref !== INHERIT && !deps.models.includes(ref)) {
|
|
365
|
+
deps.ui.notify(`/${target}: ${ref} is not in the live model registry — dispatch will decide availability when the route runs.`, "warning");
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
185
369
|
function commandChoices(deps, draft) {
|
|
186
370
|
return [...new Set([...deps.commands, ...Object.keys(draft.commands ?? {})])];
|
|
187
371
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import { type Component, type SelectItem, type SelectListTheme } from "@earendil-works/pi-tui";
|
|
2
|
+
/** Default window height for the SelectList (the built-in position indicator scales to the list). */
|
|
3
|
+
export declare const PICKER_MAX_VISIBLE = 10;
|
|
4
|
+
/**
|
|
5
|
+
* Token/subsequence, slash-aware reference filter (OB-2).
|
|
6
|
+
*
|
|
7
|
+
* - An empty query returns every reference.
|
|
8
|
+
* - The query is split on whitespace; every token must match for a reference to
|
|
9
|
+
* survive.
|
|
10
|
+
* - A token matched as a *subsequence* (characters in order, not necessarily
|
|
11
|
+
* contiguous): `flash` matches any reference containing `flash`, `a/c`
|
|
12
|
+
* matches `anthropic/claude`.
|
|
13
|
+
* - A token ending in `/` constrains to the provider: `nan/` matches provider
|
|
14
|
+
* `nan` only, never a model id that merely contains `nan`.
|
|
15
|
+
*/
|
|
16
|
+
export declare function filterReferences(query: string, refs: readonly string[]): string[];
|
|
17
|
+
/**
|
|
18
|
+
* Wrap a `SelectList` as a pi-tui `Component`, rebuilding it client-side from
|
|
19
|
+
* `filterReferences` as the operator types so the subsequence/slash-aware
|
|
20
|
+
* filter (OB-2) — not `SelectList`'s own prefix `setFilter` — drives what the
|
|
21
|
+
* window shows. Control keys (arrows, Enter, Escape) forward to the list.
|
|
22
|
+
*/
|
|
23
|
+
export declare function createPickerComponent(options: {
|
|
24
|
+
items: readonly SelectItem[];
|
|
25
|
+
maxVisible: number;
|
|
26
|
+
theme: SelectListTheme;
|
|
27
|
+
onSelect: (value: string) => void;
|
|
28
|
+
onCancel?: () => void;
|
|
29
|
+
}): Component & {
|
|
30
|
+
dispose?(): void;
|
|
31
|
+
};
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
// src/settings/picker.ts
|
|
2
|
+
// The searchable windowed picker primitive (OB-1, OB-2, OB-12, PE-004).
|
|
3
|
+
//
|
|
4
|
+
// Two concerns live here on purpose:
|
|
5
|
+
// - `filterReferences` is the pure, session-free subsequence/slash-aware filter
|
|
6
|
+
// (OB-2). It is unit-tested in `test/picker-filter.test.mjs`.
|
|
7
|
+
// - `createPickerComponent` wraps `@earendil-works/pi-tui`'s `SelectList` as a
|
|
8
|
+
// `Component` so the adapter (extension/index.ts) can drive it through
|
|
9
|
+
// `ctx.ui.custom()` — windowed rendering with a position indicator, typing
|
|
10
|
+
// to narrow the list, selection, and cancel. The Pi-free `SettingsUi.pick`
|
|
11
|
+
// seam stays separated so the filter and the component are exercisable
|
|
12
|
+
// without a live session.
|
|
13
|
+
//
|
|
14
|
+
// The filter is implemented in this package (not imported from Pi): PE-004
|
|
15
|
+
// confirmed pi-coding-agent's `fuzzyFilter` is internal, so the same
|
|
16
|
+
// token/subsequence, slash-aware semantics the acceptance criteria pin live here.
|
|
17
|
+
import { SelectList } from "@earendil-works/pi-tui";
|
|
18
|
+
/** Default window height for the SelectList (the built-in position indicator scales to the list). */
|
|
19
|
+
export const PICKER_MAX_VISIBLE = 10;
|
|
20
|
+
/**
|
|
21
|
+
* Token/subsequence, slash-aware reference filter (OB-2).
|
|
22
|
+
*
|
|
23
|
+
* - An empty query returns every reference.
|
|
24
|
+
* - The query is split on whitespace; every token must match for a reference to
|
|
25
|
+
* survive.
|
|
26
|
+
* - A token matched as a *subsequence* (characters in order, not necessarily
|
|
27
|
+
* contiguous): `flash` matches any reference containing `flash`, `a/c`
|
|
28
|
+
* matches `anthropic/claude`.
|
|
29
|
+
* - A token ending in `/` constrains to the provider: `nan/` matches provider
|
|
30
|
+
* `nan` only, never a model id that merely contains `nan`.
|
|
31
|
+
*/
|
|
32
|
+
export function filterReferences(query, refs) {
|
|
33
|
+
const tokens = query.trim().split(/\s+/u).filter((token) => token !== "");
|
|
34
|
+
if (tokens.length === 0)
|
|
35
|
+
return [...refs];
|
|
36
|
+
return refs.filter((ref) => tokens.every((token) => matchesToken(token, ref)));
|
|
37
|
+
}
|
|
38
|
+
function matchesToken(token, ref) {
|
|
39
|
+
// Slash-aware: a trailing slash makes the token a provider prefix.
|
|
40
|
+
if (token.endsWith("/")) {
|
|
41
|
+
const provider = token.slice(0, -1);
|
|
42
|
+
if (provider === "")
|
|
43
|
+
return true;
|
|
44
|
+
const refProvider = ref.split("/")[0] ?? "";
|
|
45
|
+
return refProvider.startsWith(provider);
|
|
46
|
+
}
|
|
47
|
+
return isSubsequence(token, ref);
|
|
48
|
+
}
|
|
49
|
+
/** True when `needle` appears in `haystack` with its characters in order. */
|
|
50
|
+
function isSubsequence(needle, haystack) {
|
|
51
|
+
let i = 0;
|
|
52
|
+
for (let j = 0; j < haystack.length && i < needle.length; j += 1) {
|
|
53
|
+
if (haystack.charCodeAt(j) === needle.charCodeAt(i))
|
|
54
|
+
i += 1;
|
|
55
|
+
}
|
|
56
|
+
return i === needle.length;
|
|
57
|
+
}
|
|
58
|
+
/**
|
|
59
|
+
* Wrap a `SelectList` as a pi-tui `Component`, rebuilding it client-side from
|
|
60
|
+
* `filterReferences` as the operator types so the subsequence/slash-aware
|
|
61
|
+
* filter (OB-2) — not `SelectList`'s own prefix `setFilter` — drives what the
|
|
62
|
+
* window shows. Control keys (arrows, Enter, Escape) forward to the list.
|
|
63
|
+
*/
|
|
64
|
+
export function createPickerComponent(options) {
|
|
65
|
+
const { items, maxVisible, theme, onSelect, onCancel } = options;
|
|
66
|
+
let filterText = "";
|
|
67
|
+
const byValue = new Map(items.map((item) => [item.value, item]));
|
|
68
|
+
const buildList = () => {
|
|
69
|
+
const visible = filterReferences(filterText, items.map((item) => item.value)).map((value) => byValue.get(value) ?? { value, label: value });
|
|
70
|
+
const list = new SelectList(visible, maxVisible, theme);
|
|
71
|
+
list.onSelect = (item) => onSelect(item.value);
|
|
72
|
+
list.onCancel = onCancel;
|
|
73
|
+
return list;
|
|
74
|
+
};
|
|
75
|
+
let list = buildList();
|
|
76
|
+
return {
|
|
77
|
+
render: (width) => list.render(width),
|
|
78
|
+
invalidate: () => list.invalidate(),
|
|
79
|
+
handleInput(data) {
|
|
80
|
+
// Enter / Ctrl+C / ANSI sequences (arrows) go to the SelectList; any other
|
|
81
|
+
// single character narrows the in-package filter and rebuilds the window.
|
|
82
|
+
if (data === "\r" || data === "\u0003" || data.startsWith("\u001b") || data.length > 1) {
|
|
83
|
+
list.handleInput(data);
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
filterText += data;
|
|
87
|
+
list = buildList();
|
|
88
|
+
},
|
|
89
|
+
dispose: () => {
|
|
90
|
+
// The SelectList holds no resources beyond the rendered lines.
|
|
91
|
+
},
|
|
92
|
+
};
|
|
93
|
+
}
|
package/dist/settings/view.js
CHANGED
|
@@ -34,7 +34,10 @@ function configLines(config, commands) {
|
|
|
34
34
|
];
|
|
35
35
|
}
|
|
36
36
|
function route(value) {
|
|
37
|
-
|
|
37
|
+
// A chain renders its references in order, with the fallback arrow the spec
|
|
38
|
+
// names (OB-14); a single reference and `inherit` keep their existing render.
|
|
39
|
+
const model = Array.isArray(value.model) ? value.model.join(" → ") : value.model;
|
|
40
|
+
return `${model} / ${value.thinking}`;
|
|
38
41
|
}
|
|
39
42
|
function problems(list) {
|
|
40
43
|
if (list.length === 0)
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gtrabanco/pi-agentic-workflow",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "Pi package: canonical agentic-workflow skills, friendly slash commands, and per-command model routing.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"pi-package",
|
|
@@ -36,24 +36,29 @@
|
|
|
36
36
|
"LICENSE"
|
|
37
37
|
],
|
|
38
38
|
"engines": {
|
|
39
|
-
"node": ">=18"
|
|
39
|
+
"node": ">=18",
|
|
40
|
+
"bun": ">=1.4"
|
|
40
41
|
},
|
|
41
42
|
"publishConfig": {
|
|
42
43
|
"access": "public"
|
|
43
44
|
},
|
|
44
45
|
"scripts": {
|
|
45
46
|
"build": "tsc",
|
|
46
|
-
"bundle:skills": "
|
|
47
|
-
"test": "tsc &&
|
|
48
|
-
"
|
|
49
|
-
"
|
|
50
|
-
"
|
|
47
|
+
"bundle:skills": "bun scripts/bundle-skills.mjs",
|
|
48
|
+
"test": "tsc && bun test test/*.test.mjs",
|
|
49
|
+
"test:node": "tsc && node --test test/*.test.mjs",
|
|
50
|
+
"prepublishOnly": "bun run build && bun run test",
|
|
51
|
+
"mutation": "bun scripts/mutation-check.mjs",
|
|
52
|
+
"prepare": "bun run build"
|
|
51
53
|
},
|
|
52
54
|
"peerDependencies": {
|
|
53
55
|
"@earendil-works/pi-coding-agent": ">=0.85.1"
|
|
54
56
|
},
|
|
57
|
+
"dependencies": {
|
|
58
|
+
"@earendil-works/pi-tui": "0.85.1"
|
|
59
|
+
},
|
|
55
60
|
"devDependencies": {
|
|
56
|
-
"@types/node": "
|
|
61
|
+
"@types/node": "24.13.3",
|
|
57
62
|
"typescript": "6"
|
|
58
63
|
}
|
|
59
64
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: execute-phase
|
|
3
3
|
user-invocable: true
|
|
4
|
-
version: 4.4.
|
|
4
|
+
version: 4.4.1
|
|
5
5
|
argument-hint: <NN> [P<k>] | --fix <n> [P<k>] | [--max-attempts N] [--force]
|
|
6
6
|
allowed-tools: [Bash, Read, Edit, Write, MultiEdit]
|
|
7
7
|
author: "Gabriel Trabanco <gtrabanco@users.noreply.github.com>"
|
|
@@ -115,8 +115,9 @@ use its fallback; never skip the workflow step.
|
|
|
115
115
|
Planned by `plan-feature`/`plan-fix`; executes their SPEC. Explicit phases may
|
|
116
116
|
hand off to `review-change` at layer/accumulation/sensitivity checkpoints and must
|
|
117
117
|
at unit end. Unit-loop skips intermediate checkpoints and recommends the manual
|
|
118
|
-
`/
|
|
119
|
-
`review-change` remains the manual
|
|
118
|
+
`/review-change` → `/fold-findings` (only on `REVIEW-FAIL`) → re-run
|
|
119
|
+
`/review-change` path after opening the PR; `review-change` remains the manual
|
|
120
|
+
entry.
|
|
120
121
|
Independent work stays a proposal. A finished unit always opens its PR and flips
|
|
121
122
|
to `done`; `audit-pr` gates merge. Every invocation prints the next step.
|
|
122
123
|
|
|
@@ -127,5 +128,6 @@ to `done`; `audit-pr` gates merge. Every invocation prints the next step.
|
|
|
127
128
|
committed on the correct branch with nothing bundled beyond scope.
|
|
128
129
|
- `git status --porcelain` is empty and an open-PR branch has nothing unpushed.
|
|
129
130
|
- A finished unit is `done`, has an opened PR (URL printed), and recommends the
|
|
130
|
-
mandatory `/
|
|
131
|
+
mandatory `/review-change` end review, then `/fold-findings` (only on
|
|
132
|
+
`REVIEW-FAIL`) and a re-run `/review-change`, with `/audit-pr`
|
|
131
133
|
as the merge gate.
|
|
@@ -9,7 +9,8 @@ The normal omitted-phase command is already the batch:
|
|
|
9
9
|
|
|
10
10
|
It selects every unfinished phase, gates and commits each one, skips
|
|
11
11
|
intermediate review stops, opens the PR, then recommends the manual
|
|
12
|
-
`/
|
|
12
|
+
`/review-change` → `/fold-findings` (only on `REVIEW-FAIL`) → re-run
|
|
13
|
+
`/review-change` path. An explicit `P<n>` keeps the
|
|
13
14
|
atomic/manual path.
|
|
14
15
|
|
|
15
16
|
### Fresh-context driver (recommended for cheap models)
|
|
@@ -20,8 +21,9 @@ slice, and compact receipts. An external driver may implement the same contract:
|
|
|
20
21
|
|
|
21
22
|
1. Call `workflow-status` and choose the next unfinished phase.
|
|
22
23
|
2. Invoke `/execute-phase <unit> P<n>` on the cheap tool-capable tier.
|
|
23
|
-
3. Repeat `CONTINUE`; route `READY_FOR_REVIEW` to `/
|
|
24
|
-
`/
|
|
24
|
+
3. Repeat `CONTINUE`; route `READY_FOR_REVIEW` to `/review-change`, then
|
|
25
|
+
`/fold-findings` (only on `REVIEW-FAIL`), then re-run `/review-change`, on
|
|
26
|
+
the required review/fold tiers.
|
|
25
27
|
4. Stop on `NEEDS_INPUT`, `HALT`, repeated unchanged evidence, or the declared
|
|
26
28
|
attempt budget.
|
|
27
29
|
|
|
@@ -22,9 +22,10 @@ Write `docs/features/<NN>-<slug>/CHECKLIST.md`: schema migration applied (if any
|
|
|
22
22
|
## Review checkpoint & finishing a unit
|
|
23
23
|
|
|
24
24
|
**Independent final review is mandatory — every unit gets one before merge.**
|
|
25
|
-
Recommend the
|
|
26
|
-
|
|
27
|
-
`review-change` remains the
|
|
25
|
+
Recommend the mandatory `/review-change` → `/fold-findings` (only on
|
|
26
|
+
`REVIEW-FAIL`) → re-run `/review-change` path because it preserves fresh review
|
|
27
|
+
contexts while handling bounded corrections; direct `review-change` remains the
|
|
28
|
+
manual entry. Review
|
|
28
29
|
runs in its own turn (hand-off, not composed): a skill's model and
|
|
29
30
|
effort are fixed at turn start, so invoking `review-change` from here would run it at
|
|
30
31
|
execute-phase's `sonnet`/`medium` rather than its own `opus`/`high` — under-powering
|
|
@@ -46,7 +47,8 @@ one).
|
|
|
46
47
|
**Finishing a unit (single-pass, `--fix`, or a feature's final phase): the last step
|
|
47
48
|
is always an open PR.** Mark the unit `done`, commit the flip, push, and `gh pr create`
|
|
48
49
|
(see the mode steps above) — regardless of the review/audit still to come. Then hand
|
|
49
|
-
off to `/
|
|
50
|
+
off to `/review-change` (the mandatory end review) → `/fold-findings` (only
|
|
51
|
+
on `REVIEW-FAIL`) → re-run `/review-change`, which feeds `audit-pr`
|
|
50
52
|
(the merge gate).
|
|
51
53
|
|
|
52
54
|
**Adversarial pass at that mandatory end review.** `review-change` evaluates its
|
|
@@ -41,7 +41,8 @@ Final-phase / single-pass / fix hand-off:
|
|
|
41
41
|
<unit> implemented, gate green, marked done.
|
|
42
42
|
PR opened: <FULL PR URL — always printed here; not every agent shows open PRs>
|
|
43
43
|
Roadmap/fix-index row: done · #<n> (linked and pushed)
|
|
44
|
-
→ Next: /
|
|
44
|
+
→ Next: /review-change on the changed HEAD (the mandatory end review)
|
|
45
|
+
· REVIEW-FAIL → /fold-findings (only on a REVIEW-FAIL), then re-run /review-change (unresolved findings go to triage/replan)
|
|
45
46
|
· clean → /audit-pr (merge gate) → human merges
|
|
46
47
|
· findings → fold fix-now into this PR; independent work remains a proposal until user triage; re-review
|
|
47
48
|
· docs site declared (documentation map has a `Docs site` block) →
|
|
@@ -82,6 +82,7 @@ UNIT LOOP — <unit> COMPLETE
|
|
|
82
82
|
Phases: <n> · Commits: <sha list> · Acceptance: <blob> · Gate: PASS
|
|
83
83
|
PR: <url>
|
|
84
84
|
|
|
85
|
-
→ Next: /
|
|
85
|
+
→ Next: /review-change on the changed HEAD — the mandatory end review (fold-findings only after a REVIEW-FAIL)
|
|
86
|
+
· findings → /fold-findings (only on a REVIEW-FAIL), then re-run /review-change — triage or replan unresolved findings
|
|
86
87
|
· merge gate after REVIEW-PASS → /audit-pr
|
|
87
88
|
```
|
|
@@ -130,17 +130,20 @@ durable ledger its row does not name it for. Cells carry bare values: ` · ` joi
|
|
|
130
130
|
ledgers, ` + ` joins owners, an owner is `<skill>:<column-set>` (the literal
|
|
131
131
|
`human-owner` marks the person, the only authority for an amendment), and a `#`
|
|
132
132
|
line is a directive. A script that writes only generated artifacts names no ledger
|
|
133
|
-
and is out of scope; a copy or shell rewrite stays the reviewer's job.
|
|
133
|
+
and is out of scope; a copy or shell rewrite stays the reviewer's job. Validator
|
|
134
|
+
commands follow the repository's runtime convention: **bun first** (`bun test …`),
|
|
135
|
+
with the same command under `node --test …` when bun is absent — both runtimes
|
|
136
|
+
must pass; the CI node-compat job enforces the fallback.
|
|
134
137
|
|
|
135
138
|
```text
|
|
136
139
|
ledger-ownership@1
|
|
137
140
|
truth-class | ledger | owner | annotator | annotator-token | validator
|
|
138
|
-
review-findings | docs/features/<NN>-<slug>/review-findings.md · docs/fix/<issue>-<topic>/review-findings.md | review-change:finding-rows + review-change:finding-mark + review-change:review-mark + audit-pr:audit-rows + triage-issue:triage-rows + fold-findings:folded-flag | scripts/ledger-provenance.mjs | · fold <sha> + · ticked <sha> + · REOPENED |
|
|
139
|
-
planning-findings | docs/features/<NN>-<slug>/planning-findings.md · docs/fix/<issue>-<topic>/planning-findings.md | review-spec:spec-stage-rows + review-plan:plan-stage-rows + design-feature:product-class-resolutions + plan-feature:plan-class-resolutions + plan-fix:fix-plan-class-resolutions + fold-findings:source-class-resolutions | none | none |
|
|
140
|
-
progress | docs/features/<NN>-<slug>/progress.md · docs/fix/<issue>-<topic>/progress.md | plan-feature-scaffold:create + execute-phase:phase-entries + execute-phase:gate-rejection-traces + review-spec:product-receipt + review-plan:plan-receipt | none | none |
|
|
141
|
-
known-issues | docs/features/<NN>-<slug>/known-issues.md · docs/fix/<issue>-<topic>/known-issues.md | plan-feature-scaffold:create + execute-phase:blocker-entries-and-status | none | none |
|
|
142
|
-
decisions | docs/features/<NN>-<slug>/decisions.md · docs/fix/<issue>-<topic>/decisions.md | plan-feature-scaffold:create + design-feature:product-decisions + plan-feature:engineering-decisions + execute-phase:phase-decisions + human-owner:ratified-verdicts | none | none |
|
|
143
|
-
roadmap | docs/features/ROADMAP.md · docs/fix/README.md | design-feature:idea-or-defined-row + plan-feature-scaffold:planned-row + plan-fix:fix-index-row + execute-phase:status-and-pr-link + ship-roadmap:founding-and-flip + audit-docs:low-risk-row-repair | none | none |
|
|
141
|
+
review-findings | docs/features/<NN>-<slug>/review-findings.md · docs/fix/<issue>-<topic>/review-findings.md | review-change:finding-rows + review-change:finding-mark + review-change:review-mark + audit-pr:audit-rows + triage-issue:triage-rows + fold-findings:folded-flag | scripts/ledger-provenance.mjs | · fold <sha> + · ticked <sha> + · REOPENED | bun test scripts/ledger-provenance.test.mjs
|
|
142
|
+
planning-findings | docs/features/<NN>-<slug>/planning-findings.md · docs/fix/<issue>-<topic>/planning-findings.md | review-spec:spec-stage-rows + review-plan:plan-stage-rows + design-feature:product-class-resolutions + plan-feature:plan-class-resolutions + plan-fix:fix-plan-class-resolutions + fold-findings:source-class-resolutions | none | none | bun test scripts/pre-execution-quality.test.mjs
|
|
143
|
+
progress | docs/features/<NN>-<slug>/progress.md · docs/fix/<issue>-<topic>/progress.md | plan-feature-scaffold:create + execute-phase:phase-entries + execute-phase:gate-rejection-traces + review-spec:product-receipt + review-plan:plan-receipt | none | none | bun test scripts/pre-execution-sensor.test.mjs
|
|
144
|
+
known-issues | docs/features/<NN>-<slug>/known-issues.md · docs/fix/<issue>-<topic>/known-issues.md | plan-feature-scaffold:create + execute-phase:blocker-entries-and-status | none | none | bun test scripts/ledger-ownership.test.mjs
|
|
145
|
+
decisions | docs/features/<NN>-<slug>/decisions.md · docs/fix/<issue>-<topic>/decisions.md | plan-feature-scaffold:create + design-feature:product-decisions + plan-feature:engineering-decisions + execute-phase:phase-decisions + human-owner:ratified-verdicts | none | none | bun test scripts/ledger-ownership.test.mjs
|
|
146
|
+
roadmap | docs/features/ROADMAP.md · docs/fix/README.md | design-feature:idea-or-defined-row + plan-feature-scaffold:planned-row + plan-fix:fix-index-row + execute-phase:status-and-pr-link + ship-roadmap:founding-and-flip + audit-docs:low-risk-row-repair | none | none | bun test scripts/bounded-delivery-loops.test.mjs
|
|
144
147
|
acceptance-manifest | docs/features/<NN>-<slug>/ACCEPTANCE.md · docs/fix/<issue>-<topic>/ACCEPTANCE.md | plan-feature-scaffold:feature-freeze + plan-fix:fix-freeze + human-owner:approved-amendment | none | none | git hash-object docs/features/<NN>-<slug>/ACCEPTANCE.md
|
|
145
148
|
# no-script-writer: SPEC.md · PLAN.md · TASKS.md · CHECKLIST.md · testing.md · architecture-notes.md · planning-evidence.md · planning-obligations.md · delegated-evidence.md
|
|
146
149
|
```
|