@gtrabanco/pi-agentic-workflow 0.15.3 → 0.17.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.md +49 -0
- package/dist/conductor/loop.js +23 -1
- package/dist/conductor/types.d.ts +6 -0
- package/dist/config/defaults.js +11 -0
- package/dist/config/merge.js +93 -1
- package/dist/config/profiles.d.ts +36 -0
- package/dist/config/profiles.js +102 -0
- package/dist/config/recommended.d.ts +21 -0
- package/dist/config/recommended.js +35 -0
- package/dist/config/schema.js +159 -3
- package/dist/config/types.d.ts +52 -0
- package/dist/config/types.js +10 -0
- package/dist/extension/conductor-command.d.ts +2 -5
- package/dist/extension/conductor-command.js +18 -6
- package/dist/extension/factory.d.ts +2 -0
- package/dist/extension/factory.js +10 -3
- package/dist/routing/dispatch.d.ts +9 -2
- package/dist/routing/dispatch.js +115 -46
- package/dist/routing/state.d.ts +26 -0
- package/dist/routing/state.js +67 -1
- package/dist/routing/types.d.ts +7 -0
- package/dist/settings/console.d.ts +7 -0
- package/dist/settings/console.js +214 -35
- package/dist/settings/view.d.ts +3 -1
- package/dist/settings/view.js +8 -1
- package/package.json +1 -1
- package/skills/init-workspace/SKILL.md +14 -2
- package/skills/init-workspace/references/BOOTSTRAP_DISCOVERY.md +2 -1
- package/skills/init-workspace/references/BOOTSTRAP_WRITE.md +13 -7
- package/skills/init-workspace/references/SERENA.md +133 -0
- package/skills/init-workspace/references/UPGRADE.md +10 -2
package/dist/settings/console.js
CHANGED
|
@@ -7,6 +7,11 @@
|
|
|
7
7
|
// a scope whose file does not parse cannot be overwritten (that file is evidence
|
|
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
|
+
//
|
|
11
|
+
// P5 (feature 63): the flow is scope → profile → routes. Every command operates
|
|
12
|
+
// on the active profile's route (for "default" this is top-level; for a named
|
|
13
|
+
// profile it is `profiles.<name>`). The profile defaults to "default", so
|
|
14
|
+
// existing tests that never answer a profile prompt keep working unchanged.
|
|
10
15
|
import { loadConfig, configFilePaths } from "../config/load.js";
|
|
11
16
|
import { effectiveRoute } from "../config/merge.js";
|
|
12
17
|
import { parseConfigFile, parseModelReference } from "../config/schema.js";
|
|
@@ -32,6 +37,13 @@ export const prompts = {
|
|
|
32
37
|
clearOverride: "Clear a command override",
|
|
33
38
|
bulkApply: "Apply one route to several commands",
|
|
34
39
|
bulkClear: "Clear several overrides",
|
|
40
|
+
switchProfile: "Switch the profile being edited",
|
|
41
|
+
rotateProfile: "Rotate the active profile",
|
|
42
|
+
toggleRecommended: "Toggle recommended models",
|
|
43
|
+
profile: "Which profile?",
|
|
44
|
+
newProfile: "Name the new profile",
|
|
45
|
+
profileBuiltIn: (name) => `The "${name}" profile is built into the package and cannot be edited — create a profile to override it.`,
|
|
46
|
+
profileNamed: (name) => `Editing profile "${name}"`,
|
|
35
47
|
addAnother: "Add another?",
|
|
36
48
|
policy: "Set the unavailable-route policy",
|
|
37
49
|
settle: "Set the post-command model keep/restore policy",
|
|
@@ -57,23 +69,27 @@ const INHERIT = "inherit";
|
|
|
57
69
|
export async function runSettingsConsole(deps) {
|
|
58
70
|
const paths = configFilePaths(deps.agentDir, deps.cwd);
|
|
59
71
|
const merged = loadConfig({ agentDir: deps.agentDir, cwd: deps.cwd, projectTrusted: deps.projectTrusted, readFile: deps.readFile });
|
|
60
|
-
deps.ui.notify(renderMergedConfig(merged, deps.commands).
|
|
61
|
-
|
|
62
|
-
|
|
72
|
+
deps.ui.notify(renderMergedConfig(merged, deps.commands, merged.config.profiles?.nan !== undefined || (deps.models ?? []).some((m) => m.startsWith("nan/"))
|
|
73
|
+
? { providerAvailable: (provider) => (deps.models ?? []).some((m) => m.startsWith(`${provider}/`)) }
|
|
74
|
+
: undefined).join("\n"), "info");
|
|
63
75
|
const opened = await openAScope(deps, paths.global, paths.project);
|
|
64
76
|
if (!opened)
|
|
65
77
|
return { status: "cancelled", edited: false };
|
|
66
78
|
const { scope, path, original } = opened;
|
|
67
79
|
let draft = original;
|
|
80
|
+
let active = "default";
|
|
68
81
|
for (;;) {
|
|
69
82
|
// Only offered while a turn actually holds the latch: an option that does
|
|
70
83
|
// nothing is how a console becomes noise.
|
|
71
84
|
const choice = await deps.ui.select(prompts.menu, [
|
|
85
|
+
prompts.switchProfile,
|
|
72
86
|
prompts.setDefaultRoute,
|
|
73
87
|
prompts.setOverride,
|
|
74
88
|
prompts.clearOverride,
|
|
75
89
|
prompts.bulkApply,
|
|
76
90
|
prompts.bulkClear,
|
|
91
|
+
prompts.rotateProfile,
|
|
92
|
+
prompts.toggleRecommended,
|
|
77
93
|
prompts.policy,
|
|
78
94
|
prompts.settle,
|
|
79
95
|
...(deps.routing?.inFlight() ? [prompts.undoInFlight] : []),
|
|
@@ -88,57 +104,172 @@ export async function runSettingsConsole(deps) {
|
|
|
88
104
|
if (choice === undefined || choice === prompts.cancel) {
|
|
89
105
|
return { status: "cancelled", edited: dirty(draft, original) ? await discard(deps.ui) : false };
|
|
90
106
|
}
|
|
107
|
+
// Profile: switch to another profile (scope → profile → routes)
|
|
108
|
+
if (choice === prompts.switchProfile) {
|
|
109
|
+
const picked = await pickProfile(deps, draft, { allowNew: true });
|
|
110
|
+
if (picked === undefined)
|
|
111
|
+
continue;
|
|
112
|
+
// P5: the built-in "nan" profile cannot be edited — create a profile
|
|
113
|
+
// to override it.
|
|
114
|
+
if (picked === "nan" && draft.profiles?.nan === undefined) {
|
|
115
|
+
deps.ui.notify(prompts.profileBuiltIn("nan"), "warning");
|
|
116
|
+
continue;
|
|
117
|
+
}
|
|
118
|
+
if (picked !== "default" && !Object.prototype.hasOwnProperty.call(draft.profiles ?? {}, picked)) {
|
|
119
|
+
draft = { ...draft, profiles: { ...(draft.profiles ?? {}), [picked]: {} } };
|
|
120
|
+
}
|
|
121
|
+
active = picked;
|
|
122
|
+
deps.ui.notify(prompts.profileNamed(active), "info");
|
|
123
|
+
continue;
|
|
124
|
+
}
|
|
125
|
+
// Rotate: move a profile to the front of profileOrder, clear demotion
|
|
126
|
+
if (choice === prompts.rotateProfile) {
|
|
127
|
+
const picked = await pickProfile(deps, draft, { allowNew: false });
|
|
128
|
+
if (picked === undefined)
|
|
129
|
+
continue;
|
|
130
|
+
const base = draft.profileOrder && draft.profileOrder.length > 0 ? draft.profileOrder : ["default"];
|
|
131
|
+
draft = { ...draft, profileOrder: [picked, ...base.filter((n) => n !== picked)] };
|
|
132
|
+
deps.routing?.clearProfileDemotion?.();
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
// Toggle recommended models
|
|
136
|
+
if (choice === prompts.toggleRecommended) {
|
|
137
|
+
const current = draft.recommendedModels ?? merged.config.recommendedModels;
|
|
138
|
+
draft = { ...draft, recommendedModels: !current };
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
// Set default route on the active profile
|
|
91
142
|
if (choice === prompts.setDefaultRoute) {
|
|
92
|
-
const
|
|
93
|
-
|
|
94
|
-
|
|
143
|
+
const target = active === "default" ? DEFAULT_ROUTE : `profile ${active} default`;
|
|
144
|
+
const routeFile = active === "default"
|
|
145
|
+
? (draft.default ?? {})
|
|
146
|
+
: (draft.profiles?.[active]?.default ?? {});
|
|
147
|
+
const currentRoute = {
|
|
148
|
+
model: routeFile.model ?? "inherit",
|
|
149
|
+
thinking: routeFile.thinking ?? "inherit",
|
|
150
|
+
};
|
|
151
|
+
const edited = await editRoute(deps, target, currentRoute);
|
|
152
|
+
if (edited) {
|
|
153
|
+
if (active === "default") {
|
|
154
|
+
draft = { ...draft, default: edited };
|
|
155
|
+
}
|
|
156
|
+
else {
|
|
157
|
+
draft = {
|
|
158
|
+
...draft,
|
|
159
|
+
profiles: { ...(draft.profiles ?? {}), [active]: { ...(draft.profiles?.[active] ?? {}), default: edited } },
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
}
|
|
95
163
|
continue;
|
|
96
164
|
}
|
|
165
|
+
// Set override on active profile's commands
|
|
97
166
|
if (choice === prompts.setOverride) {
|
|
98
|
-
|
|
167
|
+
// Use the full command registry so operators can add new overrides (AC4/OB-3).
|
|
168
|
+
const cmdNames = [...new Set([...deps.commands, ...Object.keys(draft.commands ?? {})])];
|
|
169
|
+
const name = await pickCommand(deps, cmdNames);
|
|
99
170
|
if (name === undefined)
|
|
100
171
|
continue;
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
172
|
+
// The initial value comes from the merged config so the picker shows the
|
|
173
|
+
// value in force, matching the original `currentFor(name)` behavior.
|
|
174
|
+
const effective = effectiveRoute(merged.config, name);
|
|
175
|
+
const routeFile = active === "default"
|
|
176
|
+
? (draft.commands?.[name] ?? {})
|
|
177
|
+
: (draft.profiles?.[active]?.commands?.[name] ?? {});
|
|
178
|
+
const currentRoute = {
|
|
179
|
+
model: typeof routeFile?.model !== "undefined" ? routeFile.model : effective.model,
|
|
180
|
+
thinking: typeof routeFile?.thinking !== "undefined" ? routeFile.thinking : effective.thinking,
|
|
181
|
+
};
|
|
182
|
+
const route = await editRoute(deps, name, currentRoute);
|
|
183
|
+
if (route) {
|
|
184
|
+
if (active === "default") {
|
|
185
|
+
draft = { ...draft, commands: { ...draft.commands, [name]: route } };
|
|
186
|
+
}
|
|
187
|
+
else {
|
|
188
|
+
draft = {
|
|
189
|
+
...draft,
|
|
190
|
+
profiles: {
|
|
191
|
+
...(draft.profiles ?? {}),
|
|
192
|
+
[active]: { ...(draft.profiles?.[active] ?? {}), commands: { ...(draft.profiles?.[active]?.commands ?? {}), [name]: route } },
|
|
193
|
+
},
|
|
194
|
+
};
|
|
195
|
+
}
|
|
196
|
+
}
|
|
104
197
|
continue;
|
|
105
198
|
}
|
|
199
|
+
// Clear override on active profile's commands
|
|
106
200
|
if (choice === prompts.clearOverride) {
|
|
107
|
-
const
|
|
201
|
+
const cmdNames = Object.keys(profileOf(draft, active).commands ?? {});
|
|
202
|
+
const name = await pickCommand(deps, cmdNames);
|
|
108
203
|
if (name === undefined)
|
|
109
204
|
continue;
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
205
|
+
if (active === "default") {
|
|
206
|
+
const rest = { ...draft.commands };
|
|
207
|
+
delete rest[name];
|
|
208
|
+
draft = { ...draft, commands: rest };
|
|
209
|
+
}
|
|
210
|
+
else {
|
|
211
|
+
const profile = draft.profiles?.[active] ?? {};
|
|
212
|
+
const rest = { ...(profile.commands ?? {}) };
|
|
213
|
+
delete rest[name];
|
|
214
|
+
draft = { ...draft, profiles: { ...(draft.profiles ?? {}), [active]: { ...profile, commands: rest } } };
|
|
215
|
+
}
|
|
113
216
|
continue;
|
|
114
217
|
}
|
|
218
|
+
// Bulk apply on active profile's commands
|
|
115
219
|
if (choice === prompts.bulkApply) {
|
|
116
|
-
//
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
// authoritative availability gate).
|
|
120
|
-
const selected = await pickCommandsMulti(deps, commandChoices(deps, draft));
|
|
220
|
+
// Use the full command registry so operators can apply to new commands (AC4/OB-3).
|
|
221
|
+
const cmdNames = [...new Set([...deps.commands, ...Object.keys(draft.commands ?? {})])];
|
|
222
|
+
const selected = await pickCommandsMulti(deps, cmdNames);
|
|
121
223
|
if (selected === undefined || selected.length === 0)
|
|
122
224
|
continue;
|
|
123
|
-
const
|
|
225
|
+
const routeFile = active === "default"
|
|
226
|
+
? (draft.commands?.[selected[0]] ?? {})
|
|
227
|
+
: (draft.profiles?.[active]?.commands?.[selected[0]] ?? {});
|
|
228
|
+
const currentRoute = {
|
|
229
|
+
model: routeFile.model ?? "inherit",
|
|
230
|
+
thinking: routeFile.thinking ?? "inherit",
|
|
231
|
+
};
|
|
232
|
+
const route = await editRoute(deps, selected[0], currentRoute);
|
|
124
233
|
if (route === undefined)
|
|
125
234
|
continue;
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
235
|
+
if (active === "default") {
|
|
236
|
+
const nextCommands = { ...draft.commands };
|
|
237
|
+
for (const name of selected) {
|
|
238
|
+
nextCommands[name] = route;
|
|
239
|
+
warnMissingModel(deps, name, route.model ?? INHERIT);
|
|
240
|
+
}
|
|
241
|
+
draft = { ...draft, commands: nextCommands };
|
|
242
|
+
}
|
|
243
|
+
else {
|
|
244
|
+
const profile = draft.profiles?.[active] ?? {};
|
|
245
|
+
const nextCommands = { ...profile.commands };
|
|
246
|
+
for (const name of selected) {
|
|
247
|
+
nextCommands[name] = route;
|
|
248
|
+
warnMissingModel(deps, name, route.model ?? INHERIT);
|
|
249
|
+
}
|
|
250
|
+
draft = { ...draft, profiles: { ...(draft.profiles ?? {}), [active]: { ...profile, commands: nextCommands } } };
|
|
130
251
|
}
|
|
131
|
-
draft = { ...draft, commands: nextCommands };
|
|
132
252
|
continue;
|
|
133
253
|
}
|
|
254
|
+
// Bulk clear on active profile's commands
|
|
134
255
|
if (choice === prompts.bulkClear) {
|
|
135
|
-
const
|
|
256
|
+
const cmdNames = Object.keys(profileOf(draft, active).commands ?? {});
|
|
257
|
+
const names = await pickCommandsMulti(deps, cmdNames);
|
|
136
258
|
if (names === undefined || names.length === 0)
|
|
137
259
|
continue;
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
260
|
+
if (active === "default") {
|
|
261
|
+
const rest = { ...draft.commands };
|
|
262
|
+
for (const name of names)
|
|
263
|
+
delete rest[name];
|
|
264
|
+
draft = { ...draft, commands: rest };
|
|
265
|
+
}
|
|
266
|
+
else {
|
|
267
|
+
const profile = draft.profiles?.[active] ?? {};
|
|
268
|
+
const rest = { ...(profile.commands ?? {}) };
|
|
269
|
+
for (const name of names)
|
|
270
|
+
delete rest[name];
|
|
271
|
+
draft = { ...draft, profiles: { ...(draft.profiles ?? {}), [active]: { ...profile, commands: rest } } };
|
|
272
|
+
}
|
|
142
273
|
continue;
|
|
143
274
|
}
|
|
144
275
|
if (choice === prompts.policy) {
|
|
@@ -154,7 +285,7 @@ export async function runSettingsConsole(deps) {
|
|
|
154
285
|
continue;
|
|
155
286
|
}
|
|
156
287
|
if (choice === prompts.save) {
|
|
157
|
-
const saved = await saveScope(deps, scope, path, draft);
|
|
288
|
+
const saved = await saveScope(deps, scope, path, draft, merged);
|
|
158
289
|
if (saved)
|
|
159
290
|
return { status: "saved", scope, path, file: clean(draft) };
|
|
160
291
|
continue;
|
|
@@ -167,6 +298,38 @@ export async function runSettingsConsole(deps) {
|
|
|
167
298
|
* without being read, and a scope whose file does not parse is refused so the
|
|
168
299
|
* operator's own file survives (AC10, AC13).
|
|
169
300
|
*/
|
|
301
|
+
// ---------------------------------------------------------------------------
|
|
302
|
+
// Profile helpers (P5, feature 63)
|
|
303
|
+
// ---------------------------------------------------------------------------
|
|
304
|
+
/** The profile file for a named profile or the default. */
|
|
305
|
+
function profileOf(draft, name) {
|
|
306
|
+
if (name === "default")
|
|
307
|
+
return { ...(draft.default ? { default: draft.default } : {}), ...(draft.commands ? { commands: draft.commands } : {}) };
|
|
308
|
+
return draft.profiles?.[name] ?? {};
|
|
309
|
+
}
|
|
310
|
+
/**
|
|
311
|
+
* Pick a profile from the list. `allowNew` adds "Name the new profile" to the
|
|
312
|
+
* options; the input must match a lowercase slug pattern.
|
|
313
|
+
*/
|
|
314
|
+
async function pickProfile(deps, draft, { allowNew }) {
|
|
315
|
+
const names = ["default", ...Object.keys(draft.profiles ?? {}).filter((n) => n !== "default")];
|
|
316
|
+
// "nan" is always listed (built-in unless user-defined)
|
|
317
|
+
if (!names.includes("nan"))
|
|
318
|
+
names.push("nan");
|
|
319
|
+
const options = allowNew ? [...names, prompts.newProfile] : names;
|
|
320
|
+
const picked = await pagedSelect((title, list) => deps.ui.select(title, list), prompts.profile, options);
|
|
321
|
+
if (picked === undefined)
|
|
322
|
+
return undefined;
|
|
323
|
+
if (picked === prompts.newProfile) {
|
|
324
|
+
const name = await deps.ui.input(prompts.newProfile, "lowercase slug");
|
|
325
|
+
if (name === undefined || !/^[a-z0-9][a-z0-9._-]*$/u.test(name.trim())) {
|
|
326
|
+
deps.ui.notify("Rejected: a profile name must be a lowercase slug.", "error");
|
|
327
|
+
return undefined;
|
|
328
|
+
}
|
|
329
|
+
return name.trim();
|
|
330
|
+
}
|
|
331
|
+
return picked;
|
|
332
|
+
}
|
|
170
333
|
async function openAScope(deps, globalPath, projectPath) {
|
|
171
334
|
for (;;) {
|
|
172
335
|
const label = await deps.ui.select(prompts.scope, [GLOBAL_LABEL, PROJECT_LABEL]);
|
|
@@ -439,11 +602,14 @@ function warnMissingModel(deps, target, model) {
|
|
|
439
602
|
}
|
|
440
603
|
}
|
|
441
604
|
}
|
|
442
|
-
function
|
|
443
|
-
return [...new Set([...deps.commands, ...Object.keys(draft.commands ?? {})])];
|
|
444
|
-
}
|
|
445
|
-
async function saveScope(deps, scope, path, draft) {
|
|
605
|
+
async function saveScope(deps, scope, path, draft, merged) {
|
|
446
606
|
const file = clean(draft);
|
|
607
|
+
// P5: materialize recommendedModels when the nan provider is available but
|
|
608
|
+
// the key is absent — the save writes the effective value (AC11).
|
|
609
|
+
const nanAvailable = (deps.models ?? []).some((m) => m.startsWith("nan/"));
|
|
610
|
+
if (file.recommendedModels === undefined && nanAvailable) {
|
|
611
|
+
file.recommendedModels = merged.config.recommendedModels;
|
|
612
|
+
}
|
|
447
613
|
const text = `${JSON.stringify(file, null, 2)}\n`;
|
|
448
614
|
const problems = problemsFor(file, scope);
|
|
449
615
|
if (problems.length > 0) {
|
|
@@ -477,6 +643,8 @@ function describeRouting(file) {
|
|
|
477
643
|
const routes = [
|
|
478
644
|
file.default ? `default: ${file.default.model ?? "inherit"} / ${file.default.thinking ?? "inherit"}` : "default: inherit / inherit",
|
|
479
645
|
...Object.entries(file.commands ?? {}).map(([name, route]) => `${name}: ${route.model ?? "inherit"} / ${route.thinking ?? "inherit"}`),
|
|
646
|
+
...(file.profileOrder && file.profileOrder.length > 0 ? [`profiles: ${file.profileOrder.join(" → ")}`] : []),
|
|
647
|
+
...(file.recommendedModels !== undefined ? [`recommended models: ${file.recommendedModels}`] : []),
|
|
480
648
|
`unavailable: ${file.onUnavailableRoute ?? "stop"}`,
|
|
481
649
|
`settle: ${file.onSettle ?? "keep"}`,
|
|
482
650
|
];
|
|
@@ -504,6 +672,17 @@ function clean(draft) {
|
|
|
504
672
|
file.onUnavailableRoute = draft.onUnavailableRoute;
|
|
505
673
|
if (draft.onSettle)
|
|
506
674
|
file.onSettle = draft.onSettle;
|
|
675
|
+
if (draft.recommendedModels !== undefined)
|
|
676
|
+
file.recommendedModels = draft.recommendedModels;
|
|
677
|
+
if (draft.profiles && Object.keys(draft.profiles).length > 0) {
|
|
678
|
+
file.profiles = structuredClone(draft.profiles);
|
|
679
|
+
}
|
|
680
|
+
if (draft.profileOrder && draft.profileOrder.length > 0) {
|
|
681
|
+
file.profileOrder = [...draft.profileOrder];
|
|
682
|
+
}
|
|
683
|
+
if (draft.profileFallback && Object.keys(draft.profileFallback).length > 0) {
|
|
684
|
+
file.profileFallback = { ...draft.profileFallback };
|
|
685
|
+
}
|
|
507
686
|
return file;
|
|
508
687
|
}
|
|
509
688
|
function parseFile(text, scope) {
|
package/dist/settings/view.d.ts
CHANGED
|
@@ -8,4 +8,6 @@ export declare const DEFAULT_ROUTE = "the default route";
|
|
|
8
8
|
*/
|
|
9
9
|
export declare function routePath(target: string): string;
|
|
10
10
|
/** What the operator reads when the console opens. */
|
|
11
|
-
export declare function renderMergedConfig(loaded: LoadedConfig, commands: readonly string[]
|
|
11
|
+
export declare function renderMergedConfig(loaded: LoadedConfig, commands: readonly string[], opts?: {
|
|
12
|
+
providerAvailable?: (provider: string) => boolean;
|
|
13
|
+
}): string[];
|
package/dist/settings/view.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
// src/settings/view.ts
|
|
2
2
|
// The settings console's display half (AC10, SPEC S4): what is in effect right
|
|
3
3
|
// now, and which files the values came from.
|
|
4
|
+
import { effectiveProfileOrder } from "../config/profiles.js";
|
|
4
5
|
/** The label for the unqualified route, so `default` is never mistaken for a command name. */
|
|
5
6
|
export const DEFAULT_ROUTE = "the default route";
|
|
6
7
|
/**
|
|
@@ -12,9 +13,15 @@ export function routePath(target) {
|
|
|
12
13
|
return target === DEFAULT_ROUTE ? "$.default" : `$.commands.${target}`;
|
|
13
14
|
}
|
|
14
15
|
/** What the operator reads when the console opens. */
|
|
15
|
-
export function renderMergedConfig(loaded, commands) {
|
|
16
|
+
export function renderMergedConfig(loaded, commands, opts) {
|
|
17
|
+
const order = effectiveProfileOrder(loaded.config, {
|
|
18
|
+
providerAvailable: opts?.providerAvailable ?? (() => false),
|
|
19
|
+
});
|
|
16
20
|
return [
|
|
17
21
|
"agentic-workflow routing — what each command runs on right now",
|
|
22
|
+
` profiles: ${order.join(" → ")}`,
|
|
23
|
+
` active profile: ${order[0] ?? "default"}`,
|
|
24
|
+
` recommended models: ${loaded.config.recommendedModels ? "on" : "off"}${loaded.config.recommendedModels && opts?.providerAvailable?.("nan") ? " (built-in nan profile in the chain)" : ""}`,
|
|
18
25
|
...configLines(loaded.config, commands),
|
|
19
26
|
...problems(loaded.problems),
|
|
20
27
|
];
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: init-workspace
|
|
3
3
|
user-invocable: true
|
|
4
|
-
version: 3.0
|
|
4
|
+
version: 3.2.0
|
|
5
5
|
argument-hint: <target-dir>
|
|
6
6
|
author: "Gabriel Trabanco <1969593+gtrabanco@users.noreply.github.com>"
|
|
7
7
|
license: MIT
|
|
@@ -58,7 +58,7 @@ Inspect the target dir (`[target-dir]`, default cwd) before touching anything:
|
|
|
58
58
|
|
|
59
59
|
## Progressive loading — bootstrap or upgrade, never both
|
|
60
60
|
|
|
61
|
-
The reference allowlist is exactly the
|
|
61
|
+
The reference allowlist is exactly the five linked paths below. Never invent or
|
|
62
62
|
read another `references/` path. Step 0 selects exactly one route:
|
|
63
63
|
|
|
64
64
|
**Hard rule for an existing Claude Code, Cursor, Copilot, or OpenCode
|
|
@@ -69,8 +69,12 @@ or portability unless the runtime platform has no named adapter at all.
|
|
|
69
69
|
- Bare/foreign repository: read
|
|
70
70
|
[bootstrap discovery](references/BOOTSTRAP_DISCOVERY.md), complete its
|
|
71
71
|
interview, then read [bootstrap write](references/BOOTSTRAP_WRITE.md).
|
|
72
|
+
Both the discovery and write routes load
|
|
73
|
+
[SERENA language servers](references/SERENA.md) for stack detection.
|
|
72
74
|
- Existing agentic-workflow scaffold: read [upgrade](references/UPGRADE.md)
|
|
73
75
|
only. Its additive-only rule is absolute; do not replay bootstrap.
|
|
76
|
+
The upgrade route loads [SERENA language servers](references/SERENA.md)
|
|
77
|
+
for the additive provisioning step.
|
|
74
78
|
- No named adapter exists for the detected runtime platform: add
|
|
75
79
|
[portability](references/PORTABILITY.md) only for that fallback. Claude Code,
|
|
76
80
|
Cursor, Copilot, and OpenCode are named supported adapters; a missing adapter
|
|
@@ -99,6 +103,12 @@ loads exactly upgrade and skips bootstrap plus portability.
|
|
|
99
103
|
on its own. **The urgency label seeding is additive-only the same way:**
|
|
100
104
|
create a missing `urgent`/`fix-next` label, never touch one that already
|
|
101
105
|
exists (name, color, or description a project already customized).
|
|
106
|
+
- **Serena convention.** `.serena/project.yml` is committed; `project.local.yml`
|
|
107
|
+
and `cache` stay local. Never write `.serena/` config without consent. LSP
|
|
108
|
+
never verifies documents — false positives happen when a file type's server
|
|
109
|
+
is not listed (markdown/yaml/json are never auto-detected), and a listed
|
|
110
|
+
server that fails to start must be removed (one failure disables every
|
|
111
|
+
server in the project). Document verification is a script's job.
|
|
102
112
|
- **Never redefine the urgency label vocabulary here.** `skills/triage-issue
|
|
103
113
|
/SKILL.md` is the sole owner of the `urgent`/`fix-next` names, colors, and
|
|
104
114
|
apply rules — this skill only seeds those two labels into the repo; it never
|
|
@@ -178,6 +188,8 @@ OpenCode adapter does not load this fallback.
|
|
|
178
188
|
listed as a residual when its hook API/dependency was unavailable.
|
|
179
189
|
The adaptive lane conventions (unit document, triage, catalog steps, guards,
|
|
180
190
|
evidence, commit formats) are written into AGENTS.md.
|
|
191
|
+
- The Serena project config is provisioned (or reported as a residual with the
|
|
192
|
+
reason, and the health check ran).
|
|
181
193
|
- **The closing `→ Next:` block is printed** (plus the offer to install the skills):
|
|
182
194
|
|
|
183
195
|
```
|
|
@@ -67,4 +67,5 @@
|
|
|
67
67
|
means Starlight). If no, leave the block commented out —
|
|
68
68
|
then reports NOT-CONFIGURED instead of guessing. Never scaffold the
|
|
69
69
|
website itself.
|
|
70
|
-
- **Naming conventions** and **
|
|
70
|
+
- **Naming conventions**, **MCP servers**, and **Serena language servers** (see
|
|
71
|
+
[SERENA](SERENA.md) for the mapping), if any.
|
|
@@ -21,11 +21,17 @@
|
|
|
21
21
|
optional: an absent document means no project invariants are declared, not
|
|
22
22
|
that the scaffold failed. If retained, add its documentation-map row and
|
|
23
23
|
name the project's explicit architectural-decision authority.
|
|
24
|
-
|
|
24
|
+
7. **Propagate the unit doc template.** Ensure
|
|
25
25
|
`docs/features/_TEMPLATE/SPEC.md` exists with all 13 mandatory sections.
|
|
26
26
|
This is the template used when creating new unit documents.
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
8. **Provision the Serena project config.** Load
|
|
28
|
+
[SERENA](SERENA.md), ask consent, then: (a) append the `.gitignore`
|
|
29
|
+
block additively (skip when already present); (b) write `.serena/project.yml`
|
|
30
|
+
with the `language_servers` list for every detected language (never overwrite
|
|
31
|
+
an existing file — report it); (c) run the health check from SERENA.md and
|
|
32
|
+
record the result. If Serena is not installed, offer install or record as a
|
|
33
|
+
residual.
|
|
34
|
+
9. **Install accepted agent safety hooks.** Keep the canonical policy and
|
|
29
35
|
fullauto wrapper at `.agentic-workflow/hooks/`. Seed the shipped
|
|
30
36
|
path-protection policy at `.agentic-workflow/path-policy.json` and its doc
|
|
31
37
|
page at `.agentic-workflow/path-protection.md`, beside the hooks — the
|
|
@@ -38,10 +44,10 @@
|
|
|
38
44
|
an unknown payload, or an existing customized hook file becomes a residual,
|
|
39
45
|
never a silent overwrite. Direct merges remain blocked; do not create a
|
|
40
46
|
`.automerge` exception.
|
|
41
|
-
|
|
47
|
+
10. **Offer the workflow skills.** Propose installing them:
|
|
42
48
|
`npx skills add gtrabanco/agentic-workflow` (note the SSH/local-path variant if
|
|
43
49
|
the source is private). Don't install without a yes.
|
|
44
|
-
|
|
50
|
+
11. **State that reviews are self-contained; offer optional extras.** The
|
|
45
51
|
workflow ships its **own internal review pack** (`review-code`,
|
|
46
52
|
`review-security`, `review-verify`, `review-debt`, `review-design`,
|
|
47
53
|
`review-a11y`, `review-brand`, `review-perf`, `review-seo`) — it installs
|
|
@@ -51,7 +57,7 @@
|
|
|
51
57
|
under a short "Optional review extras" note so `review-change` and
|
|
52
58
|
`product-audit` run them **in addition** — never as a dependency. Don't
|
|
53
59
|
install anything without a yes.
|
|
54
|
-
|
|
60
|
+
12. **Seed the urgency labels (feature 15, injection-safe urgency).** Create the
|
|
55
61
|
two capability-gated GitHub labels `triage-issue` owns and applies
|
|
56
62
|
(`skills/triage-issue/SKILL.md` is the sole owner of the name/color
|
|
57
63
|
vocabulary — this step only seeds it, never redefines it):
|
|
@@ -64,7 +70,7 @@
|
|
|
64
70
|
forge is unavailable or the user declines forge setup, skip this step and
|
|
65
71
|
list the two labels as a residual for the user to create manually later
|
|
66
72
|
(never fail the whole scaffold on it).
|
|
67
|
-
|
|
73
|
+
13. **Report.** List what was created, which placeholders still need human input,
|
|
68
74
|
the companion skills recorded/installed, the urgency labels seeded (or
|
|
69
75
|
skipped, with reason), and the next step:
|
|
70
76
|
`discover-repository-state` → `/unit-lane` (or `workflow-status`) → catalog
|
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
## Why this step exists
|
|
2
|
+
|
|
3
|
+
LSP-backed navigation and verification must actually work. A project-local
|
|
4
|
+
`.serena/project.yml` that pins the wrong `language_servers` silently disables
|
|
5
|
+
the server, and every symbol call then times out with
|
|
6
|
+
`Language server status: not initialized` (verified 2026-09-24 in this
|
|
7
|
+
repository).
|
|
8
|
+
|
|
9
|
+
## The convention this file owns
|
|
10
|
+
|
|
11
|
+
- `.serena/project.yml` is **committed** to version control (the project's
|
|
12
|
+
config, not a dev-local override).
|
|
13
|
+
- `.serena/project.local.yml` and `.serena/cache/` stay local (tool-internal
|
|
14
|
+
state).
|
|
15
|
+
- The target's `.gitignore` must therefore ignore every `.serena` tree **except
|
|
16
|
+
the root `project.yml`** — the block below enforces that.
|
|
17
|
+
|
|
18
|
+
## What to write into the target .gitignore
|
|
19
|
+
|
|
20
|
+
When the target's `.gitignore` does not already contain the block below, append
|
|
21
|
+
it (additive only — see rules below):
|
|
22
|
+
|
|
23
|
+
```text
|
|
24
|
+
# Ignore every .serena tree (tool-local state); version ONLY the root project
|
|
25
|
+
# config. Serena's own .serena/.gitignore keeps cache and per-dev overrides local.
|
|
26
|
+
**/.serena/
|
|
27
|
+
!.serena/
|
|
28
|
+
.serena/*
|
|
29
|
+
!.serena/project.yml
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
**Rules:**
|
|
33
|
+
|
|
34
|
+
- **Additive only** — append the block when it is absent.
|
|
35
|
+
- If the target already ignores `.serena` wholesale (e.g. `.serena/` without
|
|
36
|
+
an `!` carve-out), **propose the narrowing** and apply it **only with consent**.
|
|
37
|
+
- **Never rewrite unrelated lines.** Only touch the `.serena` block.
|
|
38
|
+
- **Idempotent** — skip when the exact block (all four lines) is already
|
|
39
|
+
present.
|
|
40
|
+
|
|
41
|
+
## Language servers for the detected stack
|
|
42
|
+
|
|
43
|
+
Write language servers in this order: **fixed entries first** (always written,
|
|
44
|
+
regardless of the target), then **detected entries** (from Step 0 signals).
|
|
45
|
+
|
|
46
|
+
### Fixed entries — always written
|
|
47
|
+
|
|
48
|
+
| Entry | Why always |
|
|
49
|
+
|---|---|
|
|
50
|
+
| `markdown` | Every scaffolded project has `docs/` and `.md` files (this workflow's own documentation is Markdown) |
|
|
51
|
+
| `json` | Manifests and tool config (`package.json`, `tsconfig.json`, settings) exist in nearly every stack |
|
|
52
|
+
|
|
53
|
+
### Never auto-detected rule
|
|
54
|
+
|
|
55
|
+
**`markdown`, `yaml`, and `json` are never auto-detected by Serena — they must
|
|
56
|
+
appear in `language_servers` explicitly, or requests for those file types fall
|
|
57
|
+
back to another server and produce false positives** (e.g. TypeScript diagnostics
|
|
58
|
+
on a `.yml`). Every server listed above (and `yaml` when `.yml`/`.yaml` files
|
|
59
|
+
are present) is explicit-only.
|
|
60
|
+
|
|
61
|
+
### Detected entries — append when present
|
|
62
|
+
|
|
63
|
+
| Detected | Entry |
|
|
64
|
+
|---|---|
|
|
65
|
+
| `.yml` / `.yaml` files present (CI workflows, config) | `yaml` |
|
|
66
|
+
| `package.json` / `tsconfig.json` (TypeScript or JavaScript) | `typescript` |
|
|
67
|
+
| `pyproject.toml` / `requirements.txt` | `python` |
|
|
68
|
+
| `go.mod` | `go` |
|
|
69
|
+
| `Cargo.toml` | `rust` |
|
|
70
|
+
| `pom.xml` / `build.gradle` | `java` |
|
|
71
|
+
| `Gemfile` | `ruby` |
|
|
72
|
+
| `composer.json` | `php` |
|
|
73
|
+
| `CMakeLists.txt` / `Makefile` (C/C++) | `cpp` |
|
|
74
|
+
| `.csproj` / `.sln` | `csharp` |
|
|
75
|
+
| shell-dominant repo (`*.sh` prevalent) | `bash` |
|
|
76
|
+
|
|
77
|
+
The authoritative list of valid values is Serena's own `ls_config.py`
|
|
78
|
+
(`LanguageServerId`). An id Serena does not know about is silently ignored by
|
|
79
|
+
Serena — **the health check below is what proves the list is correct.**
|
|
80
|
+
|
|
81
|
+
## Ask first
|
|
82
|
+
|
|
83
|
+
Never write `.serena/` or `.gitignore` without an explicit yes, and report
|
|
84
|
+
exactly what was written. (This matches the skill's existing installation-consent
|
|
85
|
+
rule.)
|
|
86
|
+
|
|
87
|
+
## Health check — fail closed at the claim
|
|
88
|
+
|
|
89
|
+
After activation, confirm **each listed server** initialised by asking Serena
|
|
90
|
+
for the project status and verifying `Language server status: initialized` for
|
|
91
|
+
every language in `language_servers`. If a server did not initialise, the
|
|
92
|
+
check names the offending entry. Removing a failing entry (per the startup
|
|
93
|
+
rule below) is the correct remediation — never leave a dead entry in the file.
|
|
94
|
+
**Never claim LSP evidence for a run whose server never started.**
|
|
95
|
+
|
|
96
|
+
### Startup rule (critical)
|
|
97
|
+
|
|
98
|
+
> **A server that fails to start must not stay listed.** One failing entry
|
|
99
|
+
> disables the whole language-server manager — every symbol call for the
|
|
100
|
+
> project then fails with `Language server manager is not initialized`.
|
|
101
|
+
> After writing the config, the health check must confirm **every** listed
|
|
102
|
+
> server started; any entry that fails is removed from `language_servers`
|
|
103
|
+
> and recorded as a residual with its exact error (e.g. `markdown`: marksman
|
|
104
|
+
> needs the system `libicu` package).
|
|
105
|
+
|
|
106
|
+
## When a server is missing or fails
|
|
107
|
+
|
|
108
|
+
`markdown`, `yaml`, and `json` servers exist but are explicit-only and
|
|
109
|
+
experimental: they must be listed by hand or requests fall back and produce
|
|
110
|
+
false positives.
|
|
111
|
+
|
|
112
|
+
- **Missing (not listed)** → fallback server answers → false positives for that
|
|
113
|
+
file type (e.g. TypeScript diagnostics on a `.yml`).
|
|
114
|
+
- **Failing (listed but broken)** → whole manager down → every symbol call
|
|
115
|
+
fails with `Language server manager is not initialized`. Remediation: remove
|
|
116
|
+
the entry from `language_servers` and record the residual (e.g. `markdown`:
|
|
117
|
+
marksman needs the system `libicu` package).
|
|
118
|
+
|
|
119
|
+
Document verification (schema rules like required sections/columns) stays a
|
|
120
|
+
deterministic script's job regardless — LSP serves navigation and diagnostics,
|
|
121
|
+
not our document grammar. `node`+`npm` are required for json/yaml; `libicu` is
|
|
122
|
+
required for marksman.
|
|
123
|
+
|
|
124
|
+
## Tool install is a different step
|
|
125
|
+
|
|
126
|
+
This file owns the **project config**, not the tool. If Serena is not installed,
|
|
127
|
+
offer the install (ask-first; `uv`/`uvx` preferred, `pipx` fallback) or record
|
|
128
|
+
it as a residual. The install half is tracked by #219.
|
|
129
|
+
|
|
130
|
+
## Degraded path
|
|
131
|
+
|
|
132
|
+
Serena unavailable → warn once, continue, and record the absence in the unit's
|
|
133
|
+
evidence. **Never block a run on it.**
|