@aeondave/pi-persona 1.17.1 → 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.
- package/AGENTS.md +59 -28
- package/CHANGELOG.md +234 -0
- package/README.md +16 -12
- package/agents/architect.md +21 -0
- package/agents/balthasar.md +17 -22
- package/agents/casper.md +17 -23
- package/agents/evidence-verifier.md +31 -16
- package/agents/melchior.md +17 -22
- package/agents/operator.md +15 -55
- package/agents/research.md +14 -48
- package/agents/reviewer.md +20 -39
- package/agents/scout.md +14 -15
- package/agents/verifier.md +15 -16
- package/docs/ARCHITECTURE.md +183 -55
- package/docs/EXPERIENCE.md +19 -9
- package/docs/MONITORS.md +62 -3
- package/docs/REFERENCE.md +82 -32
- package/docs/SPINE.md +2 -2
- package/docs/STRATEGIES.md +23 -8
- package/docs/TELEMETRY.md +1 -0
- package/flows/deep-review.flow.json +1 -1
- package/package.json +9 -8
- package/personas/audit.md +28 -17
- package/personas/dev.md +30 -32
- package/personas/elite.md +223 -221
- package/personas/judge.md +25 -19
- package/personas/magi.md +26 -29
- package/personas/planner.md +31 -24
- package/personas/researcher.md +25 -26
- package/personas/swarm.md +35 -25
- package/personas/verify.md +30 -19
- package/src/agents/agent.ts +20 -0
- package/src/bridge.ts +3 -2
- package/src/core/brief.ts +429 -356
- package/src/core/config.ts +15 -10
- package/src/core/model-catalog.ts +171 -0
- package/src/core/model-tier.ts +89 -0
- package/src/core/models.ts +13 -4
- package/src/core/monitor-output.ts +85 -0
- package/src/core/monitor.ts +34 -2
- package/src/core/nudge.ts +176 -23
- package/src/core/seed.ts +99 -6
- package/src/core/session-runs.ts +87 -0
- package/src/core/settings.ts +1 -1
- package/src/core/timer.ts +34 -9
- package/src/engine/adapter.ts +12 -8
- package/src/engine/async.ts +165 -146
- package/src/engine/child.ts +2 -2
- package/src/engine/host-module.ts +12 -0
- package/src/engine/inproc.ts +12 -7
- package/src/engine/stream.ts +4 -20
- package/src/exocom/install.ts +2 -1
- package/src/extension/engine.ts +43 -16
- package/src/extension/hooks.ts +106 -36
- package/src/extension/persona-model-settings.ts +31 -13
- package/src/extension/shared.ts +15 -7
- package/src/extension.ts +505 -241
- package/src/monitor/archive.ts +235 -0
- package/src/monitor/session.ts +21 -1
- package/src/orchestration/flow-journal.ts +26 -5
- package/src/orchestration/flow-run.ts +38 -13
- package/src/orchestration/flow.ts +9 -2
- package/src/orchestration/reducers.ts +9 -5
- package/src/orchestration/render.ts +1 -1
- package/src/orchestration/sdk.ts +17 -5
- package/src/orchestration/strategies/compete.ts +5 -1
- package/src/orchestration/strategies/council-rounds.ts +2 -0
- package/src/orchestration/strategies/critic-loop.ts +3 -0
- package/src/orchestration/strategies/debate.ts +2 -0
- package/src/orchestration/strategies/fanout.ts +3 -2
- package/src/orchestration/strategies/judge.ts +5 -1
- package/src/orchestration/strategies/magi.ts +40 -23
- package/src/orchestration/strategies/map.ts +13 -3
- package/src/orchestration/strategies/pair.ts +2 -0
- package/src/orchestration/strategies/pipeline.ts +2 -0
- package/src/orchestration/strategies/synthesize.ts +27 -7
- package/src/orchestration/strategy.ts +31 -0
- package/src/persona/persona.ts +7 -3
- package/src/telemetry/contract.ts +1 -1
- package/src/tools/background-run.ts +151 -0
- package/src/tools/council.ts +77 -52
- package/src/tools/delegate-tool.ts +310 -280
- package/src/tools/delegate.ts +22 -210
- package/src/tools/exocom-work.ts +18 -53
- package/src/tools/exocom.ts +15 -19
- package/src/tools/flow.ts +85 -28
- package/src/tools/intercom-tool.ts +108 -118
- package/src/tools/intercom.ts +1 -1
- package/src/tools/models.ts +19 -9
- package/src/tools/monitor.ts +39 -6
- package/src/tools/timer.ts +11 -13
- package/src/ui/agent-overlay.ts +55 -14
- package/src/ui/agent-tree.ts +84 -18
- package/src/ui/compact-widget.ts +25 -0
- package/src/ui/failure-summary.ts +55 -0
- package/src/ui/literal-tool-result.ts +24 -0
- package/src/ui/model-picker.ts +158 -81
- package/src/ui/persona-settings.ts +9 -3
- package/src/ui/presentation.ts +24 -0
- package/src/ui/report-body.ts +63 -0
- package/src/ui/session-cards.ts +189 -0
- package/src/ui/wake-rows.ts +262 -0
- package/teams.yaml +5 -1
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
/** Operator-only presentation of existing session payloads. No delivery, storage or tool shells. */
|
|
2
|
+
import type { ExtensionAPI, Theme, ThemeBg, ThemeColor } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import { Text, truncateToWidth, type Component } from "@earendil-works/pi-tui";
|
|
4
|
+
import { formatDuration } from "../core/time.ts";
|
|
5
|
+
import { neutralizeMonitorUnicode } from "../core/monitor-output.ts";
|
|
6
|
+
import { createReportBody } from "./report-body.ts";
|
|
7
|
+
import { MONITOR_LOG_ENTRY, parseMonitorLogRecord, type MonitorLogRecord } from "../monitor/archive.ts";
|
|
8
|
+
import { summarizeFailure } from "./failure-summary.ts";
|
|
9
|
+
import { compactInlineText, compactVisibleText, sanitizeTerminalText, type CompactVisibleTextResult } from "./presentation.ts";
|
|
10
|
+
|
|
11
|
+
type State = "neutral" | "success" | "attention" | "error";
|
|
12
|
+
const COLORS: Record<State, { bg: ThemeBg; fg: ThemeColor }> = {
|
|
13
|
+
neutral: { bg: "customMessageBg", fg: "accent" },
|
|
14
|
+
success: { bg: "toolSuccessBg", fg: "success" },
|
|
15
|
+
attention: { bg: "toolPendingBg", fg: "warning" },
|
|
16
|
+
error: { bg: "toolErrorBg", fg: "error" },
|
|
17
|
+
};
|
|
18
|
+
interface Card {
|
|
19
|
+
title: string;
|
|
20
|
+
state: State;
|
|
21
|
+
full: string;
|
|
22
|
+
preview: (width: number) => Pick<CompactVisibleTextResult, "text" | "truncated">;
|
|
23
|
+
metadata?: string;
|
|
24
|
+
/** Selected only by trusted surface/envelope metadata, never prose keywords. */
|
|
25
|
+
markdown?: boolean;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** All seven surfaces share semantic chrome. Literal paths style at paint time; report bodies
|
|
29
|
+
* use native Markdown's invalidatable layout cache. Pi rebuilds callbacks on transcript invalidation. */
|
|
30
|
+
function createSessionCard(card: Card, expanded: boolean, theme: Theme, detailHint: () => string, outputPad = 0): Component {
|
|
31
|
+
if (expanded && card.markdown) {
|
|
32
|
+
return createReportBody(card.full || "(no output)", theme, {
|
|
33
|
+
padding: outputPad,
|
|
34
|
+
background: COLORS[card.state].bg,
|
|
35
|
+
header: (width) => [
|
|
36
|
+
theme.fg(COLORS[card.state].fg, theme.bold(truncateToWidth(sanitizeTerminalText(card.title), width))),
|
|
37
|
+
...(card.metadata ? [theme.fg("dim", truncateToWidth(card.metadata, width))] : []),
|
|
38
|
+
].join("\n"),
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
return {
|
|
42
|
+
invalidate() { /* Literal Text path has no themed/layout cache. */ },
|
|
43
|
+
render(width) {
|
|
44
|
+
if (width <= 0) return [];
|
|
45
|
+
const padding = Math.min(Math.max(0, Math.floor(outputPad)), Math.max(0, Math.floor((width - 1) / 2)));
|
|
46
|
+
const innerWidth = Math.max(1, width - padding * 2);
|
|
47
|
+
const colors = COLORS[card.state];
|
|
48
|
+
const preview = expanded ? undefined : card.preview(innerWidth);
|
|
49
|
+
const body = expanded ? card.full : preview?.text ?? "(no output)";
|
|
50
|
+
const lines = [
|
|
51
|
+
theme.fg(colors.fg, theme.bold(truncateToWidth(sanitizeTerminalText(card.title), innerWidth))),
|
|
52
|
+
...(card.metadata ? [theme.fg("dim", truncateToWidth(card.metadata, innerWidth))] : []),
|
|
53
|
+
...sanitizeTerminalText(body || "(no output)").split("\n").map((line) => theme.fg("toolOutput", line)),
|
|
54
|
+
...(preview?.truncated ? [theme.fg("dim", truncateToWidth(detailHint(), innerWidth))] : []),
|
|
55
|
+
];
|
|
56
|
+
// Text owns wrapping and full-row padding/background, including embedded foreground resets.
|
|
57
|
+
// A terminal narrower than a single wide glyph cannot display that glyph; clip only there.
|
|
58
|
+
return new Text(lines.join("\n"), padding, 0, (text) => theme.bg(colors.bg, text))
|
|
59
|
+
.render(width).map((line) => truncateToWidth(line, width));
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function preview(full: string, maxLines: number, maxColumns: number): Card["preview"] {
|
|
65
|
+
return (width) => compactVisibleText(full, { maxLines, maxLineChars: Math.min(width, maxColumns) });
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Only this exact runtime-owned aggregate is evidence of settlement. No keyword scan of output,
|
|
69
|
+
* asks, timers, monitor event bodies, or peers. Legacy/unknown follow-ups remain informational. */
|
|
70
|
+
function followUpState(full: string): { state: State; status: string } {
|
|
71
|
+
const first = full.split("\n", 1)[0] ?? "";
|
|
72
|
+
const summary = /^(?:\[pi-persona\] )?(\d+) async runs? settled — (\d+) done, (\d+) failed(?:, (\d+) stopped)?$/.exec(first);
|
|
73
|
+
if (summary) {
|
|
74
|
+
const total = Number(summary[1]);
|
|
75
|
+
const done = Number(summary[2]);
|
|
76
|
+
const failed = Number(summary[3]);
|
|
77
|
+
const stopped = Number(summary[4] ?? 0);
|
|
78
|
+
if ([total, done, failed, stopped].every(Number.isSafeInteger) && total > 0 && total === done + failed + stopped) {
|
|
79
|
+
if (failed > 0) return { state: "error", status: "failed" };
|
|
80
|
+
if (stopped > 0) return { state: "attention", status: "stopped" };
|
|
81
|
+
return { state: "success", status: "complete" };
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
return { state: "neutral", status: "info" };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function monitorState(record: MonitorLogRecord): { state: State; status: string } {
|
|
88
|
+
if (record.outcome === "error") return { state: "error", status: "failed" };
|
|
89
|
+
if (record.outcome === "timeout" || record.outcome === "cancelled") return { state: "attention", status: "stopped" };
|
|
90
|
+
if (record.outcome === "exit") {
|
|
91
|
+
// MonitorManager prepends this line before any untrusted stdout/stderr diagnostics.
|
|
92
|
+
const first = record.text.split("\n", 1)[0] ?? "";
|
|
93
|
+
const exit = /^process exited with exit code (-?\d+)$/.exec(first);
|
|
94
|
+
if (exit) return exit[1] === "0" ? { state: "success", status: "complete" } : { state: "error", status: "failed" };
|
|
95
|
+
if (/^process exited (?:by .+|with exit code -?\d+ \(signal .+\))$/.test(first)) return { state: "attention", status: "stopped" };
|
|
96
|
+
}
|
|
97
|
+
return { state: "neutral", status: "info" };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
interface ResultEntry { label?: string; content?: string; ok?: boolean; stoppedAtGate?: string; failureKind?: string; error?: string }
|
|
101
|
+
interface ContentEntry { content?: string; kind?: "routine" | "stall" }
|
|
102
|
+
interface RunFailureEntry { id: string; label: string; agent: string; model?: string; durationMs?: number; error: string }
|
|
103
|
+
|
|
104
|
+
export interface SessionCardOptions {
|
|
105
|
+
detailHint: () => string;
|
|
106
|
+
/** Existing bounded/fenced runtime formatter, injected to keep UI independent of tool wiring. */
|
|
107
|
+
monitorLog: (record: MonitorLogRecord) => string;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export function registerSessionCards(pi: Pick<ExtensionAPI, "registerMessageRenderer" | "registerEntryRenderer">, options: SessionCardOptions): void {
|
|
111
|
+
const sessionCard = (card: Card, expanded: boolean, theme: Theme, outputPad = 0): Component =>
|
|
112
|
+
createSessionCard(card, expanded, theme, options.detailHint, outputPad);
|
|
113
|
+
for (const customType of ["pi-persona", "exocom_received"] as const) {
|
|
114
|
+
pi.registerMessageRenderer(customType, (message, { expanded, outputPad }, theme) => {
|
|
115
|
+
const raw = typeof message.content === "string" ? message.content
|
|
116
|
+
: message.content.map((part) => part.type === "text" ? part.text : "[image]").join("\n");
|
|
117
|
+
const full = sanitizeTerminalText(raw);
|
|
118
|
+
const { state, status } = customType === "pi-persona" ? followUpState(full) : { state: "neutral" as const, status: "inbound" };
|
|
119
|
+
// Only the validated settlement envelope is a pi-persona report. Unknown is NOT
|
|
120
|
+
// Markdown: monitor stdout/stderr, timers and operational notices share this type.
|
|
121
|
+
const markdown = customType === "exocom_received" || status !== "info";
|
|
122
|
+
return sessionCard({ title: customType === "pi-persona" ? `pi-persona · update · ${status}` : "exocom · inbound", state, full, markdown, preview: preview(full, 4, 100) }, expanded, theme, outputPad);
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
pi.registerEntryRenderer(MONITOR_LOG_ENTRY, (entry, { expanded }, theme) => {
|
|
126
|
+
const record = parseMonitorLogRecord(entry.data);
|
|
127
|
+
if (!record) return undefined;
|
|
128
|
+
const { state, status } = monitorState(record);
|
|
129
|
+
return sessionCard({
|
|
130
|
+
title: `Monitor log ${compactInlineText(record.entry.label)} · ${record.outcome} · ${status}`,
|
|
131
|
+
state, full: sanitizeTerminalText(options.monitorLog(record)),
|
|
132
|
+
preview: (width) => ({ text: `${compactInlineText(neutralizeMonitorUnicode(record.text), { maxChars: Math.min(width, 160) })}\n${compactInlineText(record.entry.id)} · monitor result`, truncated: true }),
|
|
133
|
+
}, expanded, theme);
|
|
134
|
+
});
|
|
135
|
+
pi.registerEntryRenderer<RunFailureEntry>("pi-persona-run-failure", (entry, { expanded }, theme) => {
|
|
136
|
+
const data = entry.data;
|
|
137
|
+
if (!data) return undefined;
|
|
138
|
+
const metadata = [compactInlineText(data.agent, { maxChars: 48 }), ...(data.model ? [compactInlineText(data.model, { maxChars: 96 })] : []), ...(data.durationMs !== undefined ? [formatDuration(data.durationMs)] : [])].join(" · ");
|
|
139
|
+
return sessionCard({
|
|
140
|
+
title: `${compactInlineText(data.label || data.agent, { maxChars: 80 })} · failed`, state: "error", metadata,
|
|
141
|
+
full: `Run ${compactInlineText(data.id)}\n${sanitizeTerminalText(data.error)}`,
|
|
142
|
+
preview: (width) => ({ text: compactVisibleText(summarizeFailure(data.error), { maxLines: 2, maxLineChars: Math.min(width, 100) }).text, truncated: true }),
|
|
143
|
+
}, expanded, theme);
|
|
144
|
+
});
|
|
145
|
+
pi.registerEntryRenderer<ResultEntry>("pi-persona-result", (entry, { expanded }, theme) => {
|
|
146
|
+
const data = entry.data;
|
|
147
|
+
if (!data) return undefined;
|
|
148
|
+
const full = sanitizeTerminalText(data.content || "(no output)");
|
|
149
|
+
const stopped = data.ok !== true && typeof data.stoppedAtGate === "string";
|
|
150
|
+
const state: State = stopped ? "attention" : data.ok === true ? "success" : data.ok === false ? "error" : "neutral";
|
|
151
|
+
const status = stopped ? `stopped · checkpoint ${compactInlineText(data.stoppedAtGate ?? "", { maxChars: 32 })}` : data.ok === true ? "complete" : data.ok === false ? "failed" : "unknown";
|
|
152
|
+
const cause = compactVisibleText([data.failureKind, data.error].filter(Boolean).join(" · "), { maxLines: 1, maxLineChars: 80 }).text;
|
|
153
|
+
return sessionCard({
|
|
154
|
+
title: `${compactInlineText(data.label ?? "", { maxChars: 96 }) || "result"} · ${status}${state === "error" && cause !== "(no output)" ? ` · ${cause}` : ""}`,
|
|
155
|
+
state, full, markdown: true, preview: preview(full, 3, 100),
|
|
156
|
+
}, expanded, theme);
|
|
157
|
+
});
|
|
158
|
+
pi.registerEntryRenderer<ContentEntry>("pi-persona-nudge", (entry, { expanded }, theme) => {
|
|
159
|
+
if (!entry.data) return undefined;
|
|
160
|
+
const full = sanitizeTerminalText(entry.data.content || "⟢ pi-persona · checkpoint");
|
|
161
|
+
const lines = full.split("\n").map((line) => line.trim()).filter(Boolean);
|
|
162
|
+
const header = compactInlineText(lines[0]?.replace(/^⟢\s*/, "") ?? "", { maxChars: 96 }) || "pi-persona · checkpoint";
|
|
163
|
+
const details = lines.slice(1);
|
|
164
|
+
const trigger = details.find((line) => line.startsWith("Trigger:"));
|
|
165
|
+
const action = details.find((line) => /^(?:Action|Verify|Otherwise):/.test(line));
|
|
166
|
+
const selected = [trigger, action].filter((line): line is string => line !== undefined);
|
|
167
|
+
const chosen = selected.length > 0 ? selected : details.slice(0, 2);
|
|
168
|
+
return sessionCard({
|
|
169
|
+
title: header, state: "attention", full,
|
|
170
|
+
preview: (width) => {
|
|
171
|
+
const result = preview(chosen.join("\n"), 2, 100)(width);
|
|
172
|
+
return { text: result.text, truncated: result.truncated || details.some((line) => !chosen.includes(line)) };
|
|
173
|
+
},
|
|
174
|
+
}, expanded, theme);
|
|
175
|
+
});
|
|
176
|
+
pi.registerEntryRenderer<ContentEntry>("pi-persona-status", (entry, { expanded }, theme) => {
|
|
177
|
+
if (!entry.data) return undefined;
|
|
178
|
+
const full = sanitizeTerminalText(entry.data.content || "Async activity · routine check-in");
|
|
179
|
+
const stall = entry.data.kind === "stall";
|
|
180
|
+
const digest = full.split("\n").slice(1).filter((line) => line.trim().length > 0);
|
|
181
|
+
return sessionCard({
|
|
182
|
+
title: stall ? "pi-persona · possible stall" : "pi-persona · async status", state: stall ? "attention" : "neutral", full,
|
|
183
|
+
preview: (width) => {
|
|
184
|
+
const result = preview(digest.slice(0, 2).join("\n") || full, 2, 140)(width);
|
|
185
|
+
return { text: result.text, truncated: result.truncated || digest.length > 2 };
|
|
186
|
+
},
|
|
187
|
+
}, expanded, theme);
|
|
188
|
+
});
|
|
189
|
+
}
|
|
@@ -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,
|
|
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.
|