@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,262 @@
1
+ import { stripTerminalControls } from "../core/fence.ts";
2
+ import type { MonitorEntry, MonitorEvent, MonitorOutput } from "../core/monitor.ts";
3
+ import { MonitorOutputTail } from "../core/monitor-output.ts";
4
+ import type { TimerEntry } from "../core/timer.ts";
5
+ import type { AddNodeInput, AgentNodePatch, AgentNodeStatus } from "./agent-tree.ts";
6
+
7
+ /**
8
+ * Wake-source rows — the pure mapping from monitor/timer lifecycle callbacks to agent-tree rows.
9
+ *
10
+ * Monitors and timers are background wake sources, not agents, but the operator should see them
11
+ * beside the sub-agents: what is armed, when it is due, what it has produced, why it ended. This
12
+ * module turns each lifecycle callback into an `AddNodeInput` / `AgentNodePatch` and nothing more:
13
+ * no tree, no clock, no Pi imports. Every timestamp comes from the entry or event itself, so the
14
+ * output is deterministic. Monitor labels, details and metadata are sanitized and bounded;
15
+ * live output adds a single-line progress preview without becoming a semantic event. Anything
16
+ * long — command, timer message, event history and bounded stdout/stderr tails — lives in
17
+ * `output`, which only the F9 drill-in shows.
18
+ */
19
+
20
+ /** Longest `command: …` description line (args may total 24k chars). */
21
+ const COMMAND_LINE_MAX = 2_000;
22
+ /** Longest first-line excerpt of one monitor event kept in the drill-in. */
23
+ const EVENT_TEXT_MAX = 1_000;
24
+ /** Longest first-line summary of a non-output terminal event, used as the row detail. */
25
+ const SUMMARY_MAX = 120;
26
+ /** Longest timer label shown on a row (the scheduler bounds it too; this projection does not trust that). */
27
+ const TIMER_LABEL_MAX = 120;
28
+ /** Event lines the drill-in remembers per monitor; older ones fall off the front. */
29
+ const MAX_EVENT_LINES = 40;
30
+ /** The manager's status line for a clean exit; diagnostics (stdout/stderr tails) follow on later lines. */
31
+ const CLEAN_EXIT = /\bexit code 0\b/;
32
+
33
+ /** Tree id of a wake-source row: `monitor:<id>` / `timer:<id>` (never collides with `async:`/strategy ids). */
34
+ export function wakeNodeId(kind: "monitor" | "timer", id: string): string {
35
+ return `${kind}:${id}`;
36
+ }
37
+
38
+ export interface WakeRowChange {
39
+ id: string;
40
+ patch: AgentNodePatch;
41
+ }
42
+
43
+ export interface WakeRowEvent extends WakeRowChange {
44
+ /** true when this event ended the watch (the row must settle and leave). */
45
+ settled: boolean;
46
+ }
47
+
48
+ /** ISO-8601 for a display line; "?" for a non-finite or out-of-range instant instead of throwing. */
49
+ function iso(ms: number): string {
50
+ if (!Number.isFinite(ms)) return "?";
51
+ const date = new Date(ms);
52
+ return Number.isFinite(date.getTime()) ? date.toISOString() : "?";
53
+ }
54
+
55
+ /** At most `max` code units, ending in "…" when clipped; never cuts an astral character in half. */
56
+ function clamp(text: string, max: number): string {
57
+ if (text.length <= max) return text;
58
+ let end = max - 1;
59
+ const last = text.charCodeAt(end - 1);
60
+ if (last >= 0xd800 && last <= 0xdbff) end -= 1;
61
+ return `${text.slice(0, end)}…`;
62
+ }
63
+
64
+ function firstLine(text: string): string {
65
+ const newline = text.indexOf("\n");
66
+ const line = newline < 0 ? text : text.slice(0, newline);
67
+ return line.endsWith("\r") ? line.slice(0, -1) : line;
68
+ }
69
+
70
+ /**
71
+ * One self-contained display line: terminal controls stripped, any line break that survives
72
+ * (`stripTerminalControls` maps a bare `\r` to `\n`) collapsed to a single visible marker.
73
+ *
74
+ * Done per stored line, NOT at render time, on purpose. `renderMonitorOutput` joins every stored line
75
+ * into one buffer and the F9 overlay sanitises only that joined buffer, after the join. A child
76
+ * line carrying a bare `\r` would then render as two lines, the second able to pose as a genuine
77
+ * `[<iso>] …` event line (a forged event), and an unterminated escape in one line could swallow the
78
+ * genuine lines after it. Each line must already be inert before it is joined with its neighbours.
79
+ */
80
+ function oneLine(text: string): string {
81
+ return stripTerminalControls(text).replace(/[\u200e\u200f\u202a-\u202e\u2066-\u2069]/g, "").replace(/[\n\u2028\u2029]+/g, " ⏎ ").replace(/\t/g, " ").trim();
82
+ }
83
+
84
+ /** What a monitor remembers between its arming and its end. */
85
+ interface MonitorState {
86
+ displayId: string;
87
+ mode: MonitorEntry["mode"];
88
+ stdout: MonitorOutputTail;
89
+ stderr: MonitorOutputTail;
90
+ preview: string;
91
+ latestStream: MonitorOutput["stream"];
92
+ /** One dirty bit per admitted monitor, never a queue of raw chunks. */
93
+ outputPending: boolean;
94
+ /** Description lines above the events counter (mode, command, cwd, timing). */
95
+ base: string[];
96
+ maxEvents: number;
97
+ /** Whether the description carries an `events:` counter line (output mode). */
98
+ counted: boolean;
99
+ /** Output events seen so far. */
100
+ events: number;
101
+ /** The last `MAX_EVENT_LINES` event lines, oldest first. */
102
+ lines: string[];
103
+ }
104
+
105
+ function renderMonitorOutput(state: MonitorState): string {
106
+ const counter = state.counted || state.events > 0 ? [`events: ${state.events}/${state.maxEvents}`] : [];
107
+ const tails: string[] = [];
108
+ for (const stream of ["stdout", "stderr"] as const) {
109
+ const text = state[stream].text.trimEnd();
110
+ // Quote every untrusted line: output must not impersonate the metadata or event history.
111
+ if (text) tails.push(`${stream} tail:\n> ${text.replace(/\n/g, "\n> ")}`);
112
+ }
113
+ return [...state.base, ...counter, ...state.lines, ...tails].join("\n");
114
+ }
115
+
116
+ /** Preview construction, like the drill-in, belongs to publication rather than raw ingestion. */
117
+ function refreshMonitorPreview(state: MonitorState): void {
118
+ if (!state.outputPending) return;
119
+ const other = state.latestStream === "stdout" ? "stderr" : "stdout";
120
+ for (const stream of [state.latestStream, other] as const) {
121
+ const preview = clamp(state[stream].preview, SUMMARY_MAX);
122
+ if (preview) {
123
+ state.preview = `${stream}: ${preview}`;
124
+ break;
125
+ }
126
+ }
127
+ state.outputPending = false;
128
+ }
129
+
130
+ function monitorDetail(state: MonitorState): string {
131
+ const counter = state.counted || state.events > 0 ? ` · ${state.events}/${state.maxEvents} events` : "";
132
+ return `${state.displayId} · ${state.mode}${counter}${state.preview ? ` · ${state.preview}` : ""}`;
133
+ }
134
+
135
+ function terminalStatus(event: MonitorEvent): AgentNodeStatus {
136
+ if (event.kind === "output") return "done";
137
+ // Only the manager's status line decides: a failed child's stdout/stderr tail can say anything.
138
+ if (event.kind === "exit") return CLEAN_EXIT.test(firstLine(event.text)) ? "done" : "failed";
139
+ return "failed";
140
+ }
141
+
142
+ export class WakeRows {
143
+ private readonly monitors = new Map<string, MonitorState>();
144
+
145
+ monitorArmed(entry: MonitorEntry): AddNodeInput {
146
+ const counted = entry.mode === "output";
147
+ const displayId = clamp(oneLine(entry.id), SUMMARY_MAX);
148
+ const state: MonitorState = {
149
+ displayId,
150
+ mode: entry.mode,
151
+ stdout: new MonitorOutputTail(),
152
+ stderr: new MonitorOutputTail(),
153
+ preview: "",
154
+ latestStream: "stdout",
155
+ outputPending: false,
156
+ base: [
157
+ `${displayId} · ${entry.mode} mode`,
158
+ clamp(oneLine(`command: ${[entry.command, ...entry.args].join(" ")}`), COMMAND_LINE_MAX),
159
+ clamp(oneLine(`cwd: ${entry.cwd}`), COMMAND_LINE_MAX),
160
+ `armed ${iso(entry.startedAtEpochMs)} · deadline ${iso(entry.expiresAtEpochMs)}`,
161
+ ],
162
+ maxEvents: entry.maxEvents,
163
+ counted,
164
+ events: 0,
165
+ lines: [],
166
+ };
167
+ this.monitors.set(entry.id, state);
168
+ return {
169
+ id: wakeNodeId("monitor", entry.id),
170
+ label: clamp(oneLine(entry.label), SUMMARY_MAX),
171
+ kind: "monitor",
172
+ status: "running",
173
+ detail: `${displayId} · ${entry.mode}`,
174
+ startedAt: entry.startedAtEpochMs,
175
+ lastAdvanceAt: entry.startedAtEpochMs,
176
+ dueAt: entry.expiresAtEpochMs,
177
+ output: renderMonitorOutput(state),
178
+ };
179
+ }
180
+
181
+ /** Feed bounded tails immediately; no full-tail reads, tree patches or semantic credit per chunk. */
182
+ monitorOutput(output: MonitorOutput): boolean {
183
+ const state = this.monitors.get(output.id);
184
+ if (!state) return false;
185
+ state[output.stream].append(output.text);
186
+ state.latestStream = output.stream;
187
+ state.outputPending = true;
188
+ return true;
189
+ }
190
+
191
+ /** Called by the existing UI frame BEFORE composing the widget, once per dirty monitor. */
192
+ flushMonitorOutput(): WakeRowChange[] {
193
+ const changes: WakeRowChange[] = [];
194
+ for (const [id, state] of this.monitors) {
195
+ if (!state.outputPending) continue;
196
+ refreshMonitorPreview(state);
197
+ changes.push({ id: wakeNodeId("monitor", id), patch: { detail: monitorDetail(state), output: renderMonitorOutput(state) } });
198
+ }
199
+ return changes;
200
+ }
201
+
202
+ /** Shutdown owns the final archive capture, not another UI frame. Late output is ignored. */
203
+ clearMonitors(): void {
204
+ this.monitors.clear();
205
+ }
206
+
207
+ monitorEvent(event: MonitorEvent): WakeRowEvent {
208
+ const state = this.monitors.get(event.id);
209
+ const head = clamp(oneLine(firstLine(event.text)), EVENT_TEXT_MAX);
210
+ let observed = 1;
211
+ if (state) {
212
+ refreshMonitorPreview(state);
213
+ if (event.kind === "output") state.events += 1;
214
+ observed = state.events;
215
+ state.lines.push(`[${iso(event.atEpochMs)}] ${head}`);
216
+ if (state.lines.length > MAX_EVENT_LINES) state.lines.splice(0, state.lines.length - MAX_EVENT_LINES);
217
+ }
218
+ const max = state ? String(state.maxEvents) : "?";
219
+ const displayId = state?.displayId ?? clamp(oneLine(event.id), SUMMARY_MAX);
220
+
221
+ const patch: AgentNodePatch = { lastAdvanceAt: event.atEpochMs };
222
+ if (event.terminal) {
223
+ patch.status = terminalStatus(event);
224
+ patch.detail = `${displayId} · ${event.kind === "output" ? `${observed}/${max} events · watch ended` : clamp(head, SUMMARY_MAX)}`;
225
+ } else if (event.kind === "output") {
226
+ patch.detail = state?.preview ? monitorDetail(state) : `${displayId} · output · ${observed}/${max} events`;
227
+ }
228
+ if (state) patch.output = renderMonitorOutput(state);
229
+ if (event.terminal) this.monitors.delete(event.id);
230
+ return { id: wakeNodeId("monitor", event.id), patch, settled: event.terminal };
231
+ }
232
+
233
+ monitorRemoved(entry: MonitorEntry): WakeRowChange {
234
+ const state = this.monitors.get(entry.id);
235
+ const output = state?.outputPending ? renderMonitorOutput(state) : undefined;
236
+ this.monitors.delete(entry.id);
237
+ return { id: wakeNodeId("monitor", entry.id), patch: { status: "stopped", detail: `${clamp(oneLine(entry.id), SUMMARY_MAX)} · cancelled`, ...(output !== undefined ? { output } : {}) } };
238
+ }
239
+
240
+ timerArmed(entry: TimerEntry): AddNodeInput {
241
+ return {
242
+ id: wakeNodeId("timer", entry.id),
243
+ label: clamp(entry.label, TIMER_LABEL_MAX),
244
+ kind: "timer",
245
+ status: "running",
246
+ // A timer armed without a label is already labelled by its id; do not repeat it.
247
+ ...(entry.label !== entry.id ? { detail: entry.id } : {}),
248
+ startedAt: entry.armedAtEpochMs,
249
+ lastAdvanceAt: entry.armedAtEpochMs,
250
+ dueAt: entry.fireAtEpochMs,
251
+ output: `${entry.id} · fires ${iso(entry.fireAtEpochMs)} (armed ${iso(entry.armedAtEpochMs)})\nmessage: ${entry.message}`,
252
+ };
253
+ }
254
+
255
+ timerFired(entry: TimerEntry): WakeRowChange {
256
+ return { id: wakeNodeId("timer", entry.id), patch: { status: "done", detail: `${entry.id} · fired` } };
257
+ }
258
+
259
+ timerCancelled(entry: TimerEntry): WakeRowChange {
260
+ return { id: wakeNodeId("timer", entry.id), patch: { status: "stopped", detail: `${entry.id} · cancelled` } };
261
+ }
262
+ }
package/teams.yaml CHANGED
@@ -4,15 +4,19 @@
4
4
  # not one .md per perspective). Referenced by a persona's `orchestration.roster` / `council.roster`.
5
5
  # Project/user copies override.
6
6
 
7
- # One `reviewer` agent, three complementary lenses — the role focuses each parallel pass
7
+ # One `reviewer` agent, four complementary lenses — the role focuses each parallel pass
8
8
  # (skills load the rest). Add `model: <provider/id>` to a member for cross-model diversity.
9
9
  # Pin cheap models on WORKER members here; leave the SUPERVISOR on the session model (a weak
10
10
  # supervisor is the least able to act on its delegation reflex).
11
11
  review:
12
+ - { agent: reviewer, role: "Focus ONLY on the CORRECTNESS lens: wrong logic, off-by-one, races, null/undefined, unhandled errors, broken invariants, wrong API or contract use." }
12
13
  - { agent: reviewer, role: "Focus ONLY on the SECURITY lens: unsafe input flows, access control, sensitive data handling, unsafe sinks, input validation." }
13
14
  - { agent: reviewer, role: "Focus ONLY on the PERFORMANCE lens: algorithmic cost on hot paths, needless allocations/copies, repeated or N+1 I/O, unbounded work." }
14
15
  - { agent: reviewer, role: "Focus ONLY on the TESTS lens: untested branches and edge/error cases, brittle or mock-asserting tests, missing regression pins." }
15
16
 
17
+ # design (critic-loop): `architect` drafts ONE plan, `reviewer` critiques it, revised until nothing must-fix remains.
18
+ design: [architect, reviewer]
19
+
16
20
  repair: [operator, verifier]
17
21
  # Evidence assurance: execute one bounded leg, then independently replay its proof. Used by Elite's
18
22
  # on-demand critic-loop council; unlike the software verifier, this checks objective/scope/artifacts.
@@ -1,28 +0,0 @@
1
- /**
2
- * Persona persistence — remember the last explicitly-selected persona and restore
3
- * it on the next start. Mirrors pi-subagents-persona: the state file lives in the
4
- * global persona folder (`<agentDir>/persona/state.json`); session-start restore
5
- * only READS it, only user gestures (f8 / /persona) write it. Best-effort: any
6
- * I/O failure is swallowed (persistence must never break a session).
7
- */
8
-
9
- import * as fs from "node:fs";
10
- import * as path from "node:path";
11
-
12
- export function readLastPersona(stateFile: string): string | undefined {
13
- try {
14
- const data = JSON.parse(fs.readFileSync(stateFile, "utf-8")) as { lastPersona?: unknown };
15
- return typeof data.lastPersona === "string" && data.lastPersona ? data.lastPersona : undefined;
16
- } catch {
17
- return undefined;
18
- }
19
- }
20
-
21
- export function writeLastPersona(stateFile: string, name: string | undefined): void {
22
- try {
23
- fs.mkdirSync(path.dirname(stateFile), { recursive: true });
24
- fs.writeFileSync(stateFile, `${JSON.stringify({ lastPersona: name ?? null }, null, 2)}\n`);
25
- } catch {
26
- /* best-effort */
27
- }
28
- }