@aeondave/pi-persona 1.17.0 → 1.17.3

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.
Files changed (109) hide show
  1. package/AGENTS.md +79 -29
  2. package/CHANGELOG.md +264 -0
  3. package/README.md +27 -12
  4. package/agents/architect.md +21 -0
  5. package/agents/balthasar.md +17 -22
  6. package/agents/casper.md +17 -23
  7. package/agents/evidence-verifier.md +31 -16
  8. package/agents/melchior.md +17 -22
  9. package/agents/operator.md +15 -55
  10. package/agents/research.md +14 -48
  11. package/agents/reviewer.md +20 -39
  12. package/agents/scout.md +14 -15
  13. package/agents/verifier.md +15 -16
  14. package/docs/ARCHITECTURE.md +223 -57
  15. package/docs/EXPERIENCE.md +19 -9
  16. package/docs/MONITORS.md +62 -3
  17. package/docs/REFERENCE.md +189 -35
  18. package/docs/SPINE.md +2 -2
  19. package/docs/STRATEGIES.md +23 -8
  20. package/docs/TELEMETRY.md +1 -0
  21. package/flows/deep-review.flow.json +1 -1
  22. package/package.json +9 -8
  23. package/personas/audit.md +28 -17
  24. package/personas/dev.md +30 -32
  25. package/personas/elite.md +223 -221
  26. package/personas/judge.md +25 -19
  27. package/personas/magi.md +26 -29
  28. package/personas/planner.md +31 -24
  29. package/personas/researcher.md +25 -26
  30. package/personas/swarm.md +35 -25
  31. package/personas/verify.md +30 -19
  32. package/src/agents/agent.ts +20 -0
  33. package/src/bridge.ts +3 -2
  34. package/src/core/brief.ts +429 -356
  35. package/src/core/config.ts +16 -11
  36. package/src/core/model-catalog.ts +171 -0
  37. package/src/core/model-tier.ts +89 -0
  38. package/src/core/models.ts +13 -4
  39. package/src/core/monitor-output.ts +85 -0
  40. package/src/core/monitor.ts +34 -2
  41. package/src/core/nudge.ts +176 -23
  42. package/src/core/seed.ts +99 -6
  43. package/src/core/session-runs.ts +87 -0
  44. package/src/core/settings.ts +251 -0
  45. package/src/core/timer.ts +34 -9
  46. package/src/engine/adapter.ts +12 -8
  47. package/src/engine/async.ts +195 -136
  48. package/src/engine/child.ts +1 -1
  49. package/src/engine/host-module.ts +12 -0
  50. package/src/engine/inproc.ts +11 -6
  51. package/src/engine/stream.ts +3 -0
  52. package/src/exocom/install.ts +2 -1
  53. package/src/extension/companion-settings.ts +29 -0
  54. package/src/extension/engine.ts +43 -16
  55. package/src/extension/hooks.ts +106 -36
  56. package/src/extension/persona-model-settings.ts +336 -0
  57. package/src/extension/persona-settings.ts +459 -0
  58. package/src/extension/shared.ts +15 -7
  59. package/src/extension.ts +642 -267
  60. package/src/monitor/archive.ts +235 -0
  61. package/src/monitor/session.ts +21 -1
  62. package/src/orchestration/flow-journal.ts +26 -5
  63. package/src/orchestration/flow-run.ts +38 -13
  64. package/src/orchestration/flow.ts +9 -2
  65. package/src/orchestration/reducers.ts +9 -5
  66. package/src/orchestration/render.ts +1 -1
  67. package/src/orchestration/sdk.ts +17 -3
  68. package/src/orchestration/strategies/compete.ts +5 -1
  69. package/src/orchestration/strategies/council-rounds.ts +2 -0
  70. package/src/orchestration/strategies/critic-loop.ts +3 -0
  71. package/src/orchestration/strategies/debate.ts +2 -0
  72. package/src/orchestration/strategies/fanout.ts +3 -2
  73. package/src/orchestration/strategies/judge.ts +5 -1
  74. package/src/orchestration/strategies/magi.ts +40 -23
  75. package/src/orchestration/strategies/map.ts +13 -3
  76. package/src/orchestration/strategies/pair.ts +2 -0
  77. package/src/orchestration/strategies/pipeline.ts +2 -0
  78. package/src/orchestration/strategies/synthesize.ts +27 -7
  79. package/src/orchestration/strategy.ts +31 -0
  80. package/src/persona/config-store.ts +6 -195
  81. package/src/persona/model-participants.ts +1 -1
  82. package/src/persona/persona.ts +7 -3
  83. package/src/persona/settings-store.ts +636 -0
  84. package/src/telemetry/contract.ts +1 -1
  85. package/src/tools/background-run.ts +151 -0
  86. package/src/tools/council.ts +77 -52
  87. package/src/tools/delegate-tool.ts +315 -285
  88. package/src/tools/delegate.ts +22 -210
  89. package/src/tools/exocom-work.ts +18 -53
  90. package/src/tools/exocom.ts +15 -19
  91. package/src/tools/flow.ts +85 -28
  92. package/src/tools/intercom-tool.ts +108 -118
  93. package/src/tools/intercom.ts +1 -1
  94. package/src/tools/models.ts +19 -9
  95. package/src/tools/monitor.ts +39 -6
  96. package/src/tools/timer.ts +11 -13
  97. package/src/ui/agent-overlay.ts +55 -14
  98. package/src/ui/agent-tree.ts +84 -18
  99. package/src/ui/compact-widget.ts +25 -0
  100. package/src/ui/failure-summary.ts +55 -0
  101. package/src/ui/literal-tool-result.ts +24 -0
  102. package/src/ui/model-picker.ts +158 -81
  103. package/src/ui/persona-settings.ts +328 -0
  104. package/src/ui/presentation.ts +24 -0
  105. package/src/ui/report-body.ts +63 -0
  106. package/src/ui/session-cards.ts +189 -0
  107. package/src/ui/wake-rows.ts +262 -0
  108. package/teams.yaml +5 -1
  109. package/src/persona/state.ts +0 -28
@@ -0,0 +1,336 @@
1
+ /**
2
+ * Interactive editor for per-persona model assignments. This module owns only the settings flow;
3
+ * the extension supplies discovery, the existing participant resolver, and the canonical store.
4
+ */
5
+
6
+ import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
7
+ import type { ModelsRefreshResult } from "@earendil-works/pi-ai";
8
+
9
+ import type { ModelParticipant } from "../persona/model-participants.ts";
10
+ import { compactInlineText } from "../ui/presentation.ts";
11
+ import { ModelPicker, orderModelRefs, type ModelPickerMetadata } from "../ui/model-picker.ts";
12
+
13
+ export interface PersonaModelOwner {
14
+ name: string;
15
+ label: string;
16
+ }
17
+
18
+ export interface PersonaModelProfile {
19
+ name: string;
20
+ label: string;
21
+ }
22
+
23
+ export interface PersonaModelSettingsDeps {
24
+ /** Persona/profile whose saved assignment scope the user is editing. */
25
+ owners: () => readonly PersonaModelOwner[];
26
+ /** Current active persona, used only to mark the first owner option. */
27
+ activeOwner: () => string | undefined;
28
+ /** Named council profiles this owner may inspect; undefined means the owner's own profile. */
29
+ profiles: (owner: string) => readonly PersonaModelProfile[];
30
+ /** Existing participant collector/resolver, including roster and auxiliary actors. */
31
+ participants: (owner: string, profile: string | undefined) => readonly ModelParticipant[];
32
+ /** Current saved assignment map for this owner. */
33
+ saved: (owner: string) => Readonly<Record<string, string>>;
34
+ /** Applies a key patch; null means delete that exact saved assignment. */
35
+ write: (owner: string, patch: Record<string, string | null>) => void;
36
+ /** Session/persona liveness guard supplied by the extension. */
37
+ isCurrent?: (ctx: ExtensionContext) => boolean;
38
+ /** Refresh adapter supplied by the catalog owner to keep derived discovery caches consistent. */
39
+ refreshCatalog?: (signal: AbortSignal) => Promise<ModelsRefreshResult | undefined>;
40
+ }
41
+
42
+ const DONE = "Done";
43
+ const BACK = "Back";
44
+ const REFRESH = "Refresh available model catalog";
45
+
46
+ /**
47
+ * Edit model assignments for a chosen owner without activating it. All changes are staged until
48
+ * the user explicitly chooses Done; canceling any dialog or changing sessions drops the draft.
49
+ */
50
+ export async function openPersonaModelSettings(ctx: ExtensionContext, deps: PersonaModelSettingsDeps): Promise<void> {
51
+ if (!ctx.hasUI) {
52
+ ctx.ui.notify("Persona model settings require an interactive TUI or RPC UI; no assignments changed.", "warning");
53
+ return;
54
+ }
55
+
56
+ const owners = deps.owners();
57
+ if (owners.length === 0) {
58
+ ctx.ui.notify("No personas are installed yet. Run /persona seed, then reopen persona settings.", "info");
59
+ return;
60
+ }
61
+
62
+ const isCurrent = (): boolean => !ctx.signal?.aborted && (deps.isCurrent?.(ctx) ?? true);
63
+ const activeOwner = deps.activeOwner();
64
+ const orderedOwners = [...owners].sort((left, right) => Number(right.name === activeOwner) - Number(left.name === activeOwner));
65
+ const ownerOptions = orderedOwners.map((owner, index) => {
66
+ const active = owner.name === activeOwner ? " · active" : "";
67
+ return `${index + 1}. ${safeInline(owner.label, 70)} (${safeInline(owner.name, 48)})${active}`;
68
+ });
69
+ const ownerChoice = await select(ctx, "Persona model owner", ownerOptions);
70
+ if (ownerChoice === undefined) return;
71
+ if (!isCurrent()) return sessionChanged(ctx);
72
+ const ownerIndex = ownerOptions.indexOf(ownerChoice);
73
+ const owner = orderedOwners[ownerIndex];
74
+ if (!owner) return;
75
+
76
+ const profiles = deps.profiles(owner.name);
77
+ const profileChoices: Array<{ profile: string | undefined; option: string }> = [
78
+ { profile: undefined, option: `1. Current profile (${safeInline(owner.name, 48)})` },
79
+ ...profiles.map((profile, index) => ({
80
+ profile: profile.name,
81
+ option: `${index + 2}. ${safeInline(profile.label, 70)} (${safeInline(profile.name, 48)})`,
82
+ })),
83
+ ];
84
+ const profileChoice = await select(ctx, "Council profile", profileChoices.map((entry) => entry.option));
85
+ if (profileChoice === undefined) return;
86
+ if (!isCurrent()) return sessionChanged(ctx);
87
+ const selectedProfile = profileChoices.find((entry) => entry.option === profileChoice);
88
+ if (!selectedProfile) return;
89
+
90
+ const profile = selectedProfile.profile;
91
+ const staged: Record<string, string | null> = {};
92
+ const getParticipants = (): readonly ModelParticipant[] => deps.participants(owner.name, profile);
93
+
94
+ for (;;) {
95
+ if (!isCurrent()) return sessionChanged(ctx);
96
+ let participants: readonly ModelParticipant[];
97
+ try {
98
+ participants = getParticipants();
99
+ } catch (error) {
100
+ if (!isCurrent()) return sessionChanged(ctx);
101
+ ctx.ui.notify(`Could not resolve model participants (${safeInline(errorMessage(error), 160)}); no assignments were written.`, "error");
102
+ return;
103
+ }
104
+ const saved = deps.saved(owner.name);
105
+ const knownKeys = new Set<string>();
106
+ for (const participant of participants) {
107
+ knownKeys.add(participant.key);
108
+ knownKeys.add(participant.agent);
109
+ }
110
+ const staleKeys = Object.keys(saved).filter((key) => !knownKeys.has(key) && staged[key] === undefined);
111
+ const rows = participants.map((participant, index) => participantRow(participant, saved, staged, index));
112
+ const otherAssignmentsOption = staleKeys.length > 0 ? `Manage other saved assignments (${staleKeys.length})…` : undefined;
113
+ const mainOptions = [...rows.map((row) => row.option), ...(otherAssignmentsOption ? [otherAssignmentsOption] : []), REFRESH, DONE];
114
+ const choice = await select(ctx, `Model settings · ${safeInline(owner.label, 48)}${profile ? ` · ${safeInline(profile, 40)}` : " · current profile"}`, mainOptions);
115
+ if (choice === undefined) return; // cancel discards the entire staged patch
116
+ if (!isCurrent()) return sessionChanged(ctx);
117
+ if (choice === DONE) {
118
+ const patch = Object.fromEntries(Object.entries(staged));
119
+ if (Object.keys(patch).length === 0) return;
120
+ try {
121
+ if (!isCurrent()) return sessionChanged(ctx);
122
+ deps.write(owner.name, patch);
123
+ ctx.ui.notify(`Saved ${Object.keys(patch).length} model assignment change(s) for "${safeInline(owner.name, 48)}"; applies to future runs.`, "info");
124
+ } catch (error) {
125
+ ctx.ui.notify(`Could not save persona model settings (${safeInline(errorMessage(error), 180)}); previous assignments were kept.`, "error");
126
+ }
127
+ return;
128
+ }
129
+ if (choice === REFRESH) {
130
+ if (!(await refreshCatalog(ctx, isCurrent, deps.refreshCatalog))) return;
131
+ continue;
132
+ }
133
+ if (otherAssignmentsOption && choice === otherAssignmentsOption) {
134
+ if (staleKeys.length === 0) continue;
135
+ const staleOptions = staleKeys.map((key, index) => `${index + 1}. ${displayAssignmentKey(key, index)} · ${safeInline(saved[key] ?? "", 90)}`);
136
+ const staleChoice = await select(ctx, "Other saved assignment", staleOptions);
137
+ if (staleChoice === undefined) return;
138
+ if (!isCurrent()) return sessionChanged(ctx);
139
+ const index = staleOptions.indexOf(staleChoice);
140
+ const key = staleKeys[index];
141
+ if (key !== undefined) staged[key] = null;
142
+ continue;
143
+ }
144
+
145
+ const rowIndex = rows.findIndex((row) => row.option === choice);
146
+ const participant = participants[rowIndex];
147
+ if (!participant) continue;
148
+ if (participant.source === "inline") {
149
+ ctx.ui.notify(`"${safeInline(participant.label, 80)}" is pinned inline and is read-only here.`, "info");
150
+ continue;
151
+ }
152
+
153
+ const directExists = effectiveSavedValue(participant.key, saved, staged) !== undefined;
154
+ const sharedExists = participant.key !== participant.agent && effectiveSavedValue(participant.agent, saved, staged) !== undefined;
155
+ const actions = [`Set or change model for "${safeInline(participant.label, 80)}"`];
156
+ if (directExists) actions.push("Reset this participant's saved assignment");
157
+ if (sharedExists) actions.push(`Reset shared agent assignment (${safeInline(participant.agent, 48)})`);
158
+ actions.push(BACK);
159
+ const action = await select(ctx, `Edit participant · ${safeInline(participant.label, 80)}`, actions);
160
+ if (action === undefined) return;
161
+ if (!isCurrent()) return sessionChanged(ctx);
162
+ if (action === BACK) continue;
163
+ if (action === "Reset this participant's saved assignment") {
164
+ staged[participant.key] = null;
165
+ continue;
166
+ }
167
+ if (action.startsWith("Reset shared agent assignment")) {
168
+ staged[participant.agent] = null;
169
+ continue;
170
+ }
171
+ if (action !== actions[0]) continue;
172
+
173
+ const models = availableModels(ctx);
174
+ const modelRefs = orderModelRefs(models.map((model) => `${model.provider}/${model.id}`), ctx.model?.provider);
175
+ if (modelRefs.length === 0) {
176
+ ctx.ui.notify("No authenticated models are available. Use /login, then choose Set or change again; saved assignments were kept.", "warning");
177
+ continue;
178
+ }
179
+ const current = rows[rowIndex]!;
180
+ const modelTitle = compactInlineText(
181
+ `Model for ${participant.label} · current: ${current.model} (${current.source})`,
182
+ { maxChars: 180 },
183
+ );
184
+ let modelChoice: string | undefined;
185
+ try {
186
+ modelChoice = ctx.mode === "tui"
187
+ ? await ctx.ui.custom<string | undefined>(
188
+ (tui, theme, keybindings, done) => new ModelPicker(tui, theme, modelTitle, modelRefs, ctx.model?.provider, done, {
189
+ models,
190
+ ...(current.currentRef !== undefined ? { currentRef: current.currentRef } : {}),
191
+ ...(keybindings ? { keybindings } : {}),
192
+ }),
193
+ { overlay: false },
194
+ )
195
+ : await select(ctx, modelTitle, modelRefs);
196
+ } catch {
197
+ return; // a dismissed/broken picker must not commit partial edits
198
+ }
199
+ if (modelChoice === undefined) return;
200
+ if (!isCurrent()) return sessionChanged(ctx);
201
+ if (modelRefs.includes(modelChoice)) staged[participant.key] = modelChoice;
202
+ else return; // treat an unknown provider/model choice as a cancelled dialog
203
+ }
204
+ }
205
+
206
+ function participantRow(
207
+ participant: ModelParticipant,
208
+ saved: Readonly<Record<string, string>>,
209
+ staged: Readonly<Record<string, string | null>>,
210
+ index: number,
211
+ ): { option: string; model: string; source: string; currentRef?: string } {
212
+ const hasPendingDirect = Object.prototype.hasOwnProperty.call(staged, participant.key);
213
+ const pendingDirect = hasPendingDirect ? staged[participant.key] : undefined;
214
+ const hasPendingShared = participant.key !== participant.agent && Object.prototype.hasOwnProperty.call(staged, participant.agent);
215
+ const pendingShared = hasPendingShared ? staged[participant.agent] : undefined;
216
+ const directSaved = effectiveSavedValue(participant.key, saved, staged);
217
+ const sharedSaved = participant.key !== participant.agent ? effectiveSavedValue(participant.agent, saved, staged) : undefined;
218
+ let model: string;
219
+ let source: string;
220
+ let currentRef: string | undefined;
221
+ if (typeof pendingDirect === "string") {
222
+ model = pendingDirect;
223
+ currentRef = pendingDirect;
224
+ source = "assignment (pending)";
225
+ } else if (pendingDirect === null) {
226
+ if (sharedSaved) {
227
+ model = sharedSaved;
228
+ currentRef = sharedSaved;
229
+ source = "shared assignment (pending fallback)";
230
+ } else {
231
+ model = "resolve lower precedence next run";
232
+ source = "pending reset";
233
+ }
234
+ } else if (typeof pendingShared === "string" && !directSaved && participant.source !== "inline") {
235
+ model = pendingShared;
236
+ currentRef = pendingShared;
237
+ source = "shared assignment (pending)";
238
+ } else if (hasPendingShared && pendingShared === null && !directSaved) {
239
+ model = "resolve lower precedence next run";
240
+ source = "shared reset pending";
241
+ } else {
242
+ model = participant.model ?? "none";
243
+ currentRef = participant.model;
244
+ source = participant.source;
245
+ }
246
+ const sharedNote = !directSaved && sharedSaved
247
+ ? ` · shared ${safeInline(participant.agent, 40)}=${safeInline(sharedSaved, 70)}`
248
+ : hasPendingShared && pendingShared === null && directSaved
249
+ ? ` · shared ${safeInline(participant.agent, 40)} reset pending`
250
+ : "";
251
+ const pendingNote = hasPendingDirect && pendingDirect === null ? " · pending reset"
252
+ : hasPendingShared && pendingShared === null ? " · shared reset pending"
253
+ : "";
254
+ const option = `${index + 1}. ${safeInline(participant.label, 80)} · ${safeInline(model, 90)} (${safeInline(source, 24)})${sharedNote}${pendingNote}`;
255
+ return { option: compactInlineText(option, { maxChars: 200 }), model, source,
256
+ ...(currentRef !== undefined ? { currentRef } : {}) };
257
+ }
258
+
259
+ function effectiveSavedValue(
260
+ key: string,
261
+ saved: Readonly<Record<string, string>>,
262
+ staged: Readonly<Record<string, string | null>>,
263
+ ): string | undefined {
264
+ if (Object.prototype.hasOwnProperty.call(staged, key)) return staged[key] ?? undefined;
265
+ return saved[key];
266
+ }
267
+
268
+ function availableModels(ctx: ExtensionContext): ModelPickerMetadata[] {
269
+ try {
270
+ const unique = new Map<string, ModelPickerMetadata>();
271
+ for (const model of ctx.modelRegistry.getAvailable()) {
272
+ if (typeof model.provider !== "string" || !model.provider.trim() || typeof model.id !== "string" || !model.id.trim()) continue;
273
+ unique.set(`${model.provider}/${model.id}`, model);
274
+ }
275
+ return [...unique.values()];
276
+ } catch {
277
+ return [];
278
+ }
279
+ }
280
+
281
+ async function refreshCatalog(ctx: ExtensionContext, isCurrent: () => boolean, refresh?: PersonaModelSettingsDeps["refreshCatalog"]): Promise<boolean> {
282
+ if (!ctx.modelRegistry.refresh) {
283
+ ctx.ui.notify("This Pi host cannot explicitly refresh the model catalog; existing assignments were kept.", "warning");
284
+ return true;
285
+ }
286
+ const deadline = AbortSignal.timeout(20_000);
287
+ const signal = ctx.signal ? AbortSignal.any([ctx.signal, deadline]) : deadline;
288
+ try {
289
+ const result = await (refresh ? refresh(signal) : ctx.modelRegistry.refresh({ signal }));
290
+ if (ctx.signal?.aborted) {
291
+ ctx.ui.notify("Model catalog refresh was cancelled; saved assignments were kept.", "info");
292
+ return false;
293
+ }
294
+ if (!isCurrent()) {
295
+ sessionChanged(ctx);
296
+ return false;
297
+ }
298
+ if (signal.aborted || result?.aborted) {
299
+ ctx.ui.notify("Model catalog refresh was cancelled or timed out; saved assignments were kept.", "warning");
300
+ return true;
301
+ }
302
+ if (result?.errors.size) {
303
+ const providers = compactInlineText([...result.errors.keys()].join(", "), { maxChars: 120 });
304
+ ctx.ui.notify(`Model catalog refresh incomplete for ${providers}; saved assignments were kept.`, "warning");
305
+ return true;
306
+ }
307
+ ctx.ui.notify("Model catalog refreshed. Choose a participant to edit; existing assignments are unchanged.", "info");
308
+ return true;
309
+ } catch (error) {
310
+ ctx.ui.notify(`Model catalog refresh failed (${safeInline(errorMessage(error), 150)}); saved assignments were kept.`, "warning");
311
+ return true;
312
+ }
313
+ }
314
+
315
+ async function select(ctx: ExtensionContext, title: string, options: string[]): Promise<string | undefined> {
316
+ return ctx.ui.select(title, options);
317
+ }
318
+
319
+ function displayAssignmentKey(key: string, index: number): string {
320
+ // Current keys are agent names or agent#32-hex-digest. Do not surface arbitrary legacy text that
321
+ // might contain an old role prompt; keep a safe stable-looking label for those records instead.
322
+ if (/^[A-Za-z0-9][A-Za-z0-9._-]{0,79}(?:#[a-f0-9]{32})?$/.test(key)) return key;
323
+ return `legacy-key-${index + 1}`;
324
+ }
325
+
326
+ function safeInline(value: string, maxChars: number): string {
327
+ return compactInlineText(value, { maxChars });
328
+ }
329
+
330
+ function sessionChanged(ctx: ExtensionContext): void {
331
+ ctx.ui.notify("Persona model settings: the session changed, the command was cancelled, or the active persona changed; no staged assignments were written.", "warning");
332
+ }
333
+
334
+ function errorMessage(error: unknown): string {
335
+ return error instanceof Error ? error.message : String(error);
336
+ }