@aeondave/pi-persona 1.17.3 → 1.17.4

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 (84) hide show
  1. package/AGENTS.md +44 -36
  2. package/CHANGELOG.md +76 -1
  3. package/README.md +23 -13
  4. package/docs/ARCHITECTURE.md +97 -37
  5. package/docs/COLLABORATION.md +110 -0
  6. package/docs/EXPERIENCE.md +33 -8
  7. package/docs/README.md +12 -0
  8. package/docs/REFERENCE.md +65 -14
  9. package/package.json +12 -6
  10. package/src/agents/agent.ts +6 -0
  11. package/src/bridge.ts +25 -8
  12. package/src/bus/broker/client.ts +119 -21
  13. package/src/bus/broker/host.ts +123 -8
  14. package/src/bus/broker/messages.ts +31 -4
  15. package/src/bus/contact.ts +113 -38
  16. package/src/bus/peers.ts +15 -0
  17. package/src/bus/result-reader.ts +88 -0
  18. package/src/core/brief.ts +14 -4
  19. package/src/core/capabilities.ts +1 -1
  20. package/src/core/leg-thinking.ts +57 -0
  21. package/src/core/ownership.ts +1 -2
  22. package/src/core/pi-compat.ts +5 -12
  23. package/src/core/session-runs.ts +1 -1
  24. package/src/core/types.ts +1 -1
  25. package/src/engine/adapter.ts +83 -19
  26. package/src/engine/async.ts +49 -10
  27. package/src/engine/child.ts +10 -2
  28. package/src/engine/inproc.ts +87 -46
  29. package/src/engine/result-grant.ts +62 -0
  30. package/src/engine/session-tools.ts +6 -0
  31. package/src/engine/stream.ts +6 -0
  32. package/src/engine/worktree.ts +17 -7
  33. package/src/exocom/collaboration-delivery.ts +29 -0
  34. package/src/exocom/collaboration.ts +148 -0
  35. package/src/exocom/envelope.ts +43 -4
  36. package/src/exocom/gate.ts +7 -2
  37. package/src/exocom/install.ts +45 -17
  38. package/src/exocom/ledger.ts +58 -8
  39. package/src/exocom/plane.ts +32 -1
  40. package/src/exocom/profile.ts +40 -0
  41. package/src/exocom/registry.ts +5 -0
  42. package/src/exocom/status.ts +11 -1
  43. package/src/exocom/write-guard.ts +7 -0
  44. package/src/extension/broker-host.ts +61 -9
  45. package/src/extension/engine.ts +20 -21
  46. package/src/extension/hooks.ts +952 -938
  47. package/src/extension/identity.ts +41 -75
  48. package/src/extension/results.ts +136 -0
  49. package/src/extension/shared.ts +17 -0
  50. package/src/extension.ts +2963 -2873
  51. package/src/loader.ts +11 -3
  52. package/src/orchestration/flow-journal.ts +7 -1
  53. package/src/orchestration/flow-run.ts +122 -19
  54. package/src/orchestration/flow.ts +11 -0
  55. package/src/orchestration/roster.ts +71 -23
  56. package/src/orchestration/sdk.ts +44 -6
  57. package/src/orchestration/types.ts +10 -0
  58. package/src/persona/model-participants.ts +20 -7
  59. package/src/persona/orchestrate.ts +2 -0
  60. package/src/persona/persona.ts +1 -1
  61. package/src/results/archive.ts +281 -0
  62. package/src/results/record.ts +166 -0
  63. package/src/results/retrieval.ts +142 -0
  64. package/src/results/types.ts +128 -0
  65. package/src/telemetry/contract.ts +1 -1
  66. package/src/tools/council.ts +40 -23
  67. package/src/tools/delegate-tool.ts +50 -14
  68. package/src/tools/delegate.ts +13 -6
  69. package/src/tools/exocom-collaboration-input.ts +168 -0
  70. package/src/tools/exocom-collaboration.ts +315 -0
  71. package/src/tools/exocom-profile.ts +102 -0
  72. package/src/tools/exocom.ts +20 -3
  73. package/src/tools/flow.ts +24 -8
  74. package/src/tools/intercom-tool.ts +98 -14
  75. package/src/tools/models.ts +15 -0
  76. package/src/tools/monitor.ts +20 -1
  77. package/src/tools/read-result.ts +52 -0
  78. package/src/tools/timer.ts +23 -2
  79. package/src/ui/agent-overlay.ts +11 -3
  80. package/src/ui/agent-tree.ts +43 -1
  81. package/src/ui/literal-tool-result.ts +1 -1
  82. package/src/ui/model-picker.ts +1 -1
  83. package/src/ui/orchestration-launch.ts +49 -0
  84. package/src/ui/session-cards.ts +191 -14
@@ -0,0 +1,142 @@
1
+ /** Supervisor-facing terminal receipts and the shared fenced page projection. No lifecycle effects. */
2
+ import { fenceUntrusted } from "../core/fence.ts";
3
+ import { readFailure, type ReadReply, type ScopedReader } from "../bus/result-reader.ts";
4
+ import { RESULT_MAX_BODY_CHARS, RESULT_READ_DEFAULT, RESULT_READ_MAX, type ArchiveFailure, type ResultPage, type ResultReadRequest,
5
+ type ResultStorage, type SaveOutcome, type TerminalStatus } from "./types.ts";
6
+ import type { StoredBodyPage } from "./record.ts";
7
+
8
+ export interface ResultReceipt {
9
+ ref?: string;
10
+ storage?: ResultStorage;
11
+ truncated?: boolean;
12
+ warning?: string;
13
+ }
14
+
15
+ export function resultReceipt(outcome: SaveOutcome): ResultReceipt {
16
+ if (!outcome.ok) return { warning: `Terminal report not archived (${outcome.reason}).` };
17
+ return {
18
+ ref: outcome.ref, storage: outcome.storage, truncated: outcome.summary.truncated,
19
+ ...(outcome.warnings.length > 0 ? { warning: `Terminal storage: ${outcome.warnings.join(", ")}.` } : {}),
20
+ };
21
+ }
22
+
23
+ export function resultReceiptHint(receipt: ResultReceipt | undefined): string {
24
+ if (!receipt) return "";
25
+ const reference = receipt.ref
26
+ ? `${receipt.ref} · ${receipt.storage === "session-entry" ? "native session entry accepted (not an fsync guarantee)" : "memory-only; runtime-only, not durable"}. Read with intercom { action:"result", ref:"${receipt.ref}" }.`
27
+ : "";
28
+ return [reference, receipt.truncated ? "Saved report truncated by archive limits." : "", receipt.warning ?? ""].filter(Boolean).join(" ");
29
+ }
30
+
31
+ /** Live reports are not restricted to archive body retention; paging still obeys the same UTF-16 contract. */
32
+ export function pageLiveResult(body: string, offset = 0, limit = RESULT_READ_DEFAULT): StoredBodyPage | undefined {
33
+ const splitsPair = (at: number): boolean => at > 0 && at < body.length
34
+ && /[\uD800-\uDBFF]/.test(body[at - 1]!) && /[\uDC00-\uDFFF]/.test(body[at]!);
35
+ if (!Number.isSafeInteger(offset) || offset < 0 || offset > body.length || splitsPair(offset)
36
+ || !Number.isInteger(limit) || limit < 2 || limit > RESULT_READ_MAX) return undefined;
37
+ let end = Math.min(body.length, offset + limit);
38
+ if (splitsPair(end)) end--;
39
+ return { body: body.slice(offset, end), offset, endOfStoredBody: end === body.length, ...(end < body.length ? { nextOffset: end } : {}) };
40
+ }
41
+
42
+ /** Metadata is caller-owned; report and diagnostic text are always independently fenced. */
43
+ export function projectResultPage(page: StoredBodyPage, error?: string): string {
44
+ const cause = error ? `\nFailure detail:\n${fenceUntrusted(error.slice(0, 2048))}` : "";
45
+ const next = page.nextOffset === undefined ? "end of stored body" : `next offset: ${page.nextOffset}`;
46
+ return `Page offset ${page.offset}; ${page.body.length} UTF-16 units; ${next}.${cause}\n${page.body === "(no output)" ? page.body : fenceUntrusted(page.body)}`;
47
+ }
48
+
49
+ // ── reference handoff (flow reference mode): scoped reads over the terminal archive ──
50
+
51
+ /** The narrow archive face a leg's reader needs — `TerminalResultArchive` satisfies it
52
+ * structurally. Readers never own, hydrate or mutate archive state; they page through it. */
53
+ export interface ResultArchiveView {
54
+ read(request: ResultReadRequest): ResultPage;
55
+ }
56
+
57
+ /** An archive failure reason mapped onto the reader wire's closed error vocabulary. */
58
+ function readFailureFor(reason: ArchiveFailure["reason"]): ReadReply {
59
+ switch (reason) {
60
+ case "invalid":
61
+ return readFailure("invalid");
62
+ case "busy":
63
+ return readFailure("busy");
64
+ default:
65
+ // missing / ambiguous / corrupt / conflict / quota — every identity or durability
66
+ // problem reads to the child as "the ref is not readable here".
67
+ return readFailure("unavailable");
68
+ }
69
+ }
70
+
71
+ /** A scoped reader over the owning session's archive, granted an EXPLICIT ref set (the
72
+ * direct upstream phase needs of THIS leg — nothing else). Owner/capability rechecks at
73
+ * call time are the root's job (the adapter closure); revocation is one-way and final.
74
+ * Reads page the archive's already-stored body; they never rebill, collect live runs, or
75
+ * wake anything. Projection/fencing stays the tool's job (`tools/read-result.ts`). */
76
+ export interface ScopedResultReader extends ScopedReader {
77
+ /** Refs that are not durably readable (missing, unreadable, or memory-only). Empty = healthy. */
78
+ preflight(): { missing: string[] };
79
+ }
80
+
81
+ export function makeScopedResultReader(view: ResultArchiveView, granted: readonly string[]): ScopedResultReader {
82
+ const refs = new Set(granted);
83
+ let revoked = false;
84
+ const page = (request: ResultReadRequest): ReadReply => {
85
+ const result = view.read(request);
86
+ if (!result.ok) return readFailureFor(result.reason);
87
+ const s = result.summary;
88
+ const end = result.offset + result.body.length;
89
+ return {
90
+ ok: true, ref: s.ref, body: result.body, offset: result.offset,
91
+ ...(end < s.storedChars ? { nextOffset: end } : {}),
92
+ storedChars: s.storedChars, originalChars: s.originalChars, truncated: s.truncated,
93
+ endOfStoredBody: result.endOfStoredBody, status: s.status, provenance: s.provenance,
94
+ };
95
+ };
96
+ return {
97
+ async read(request, signal): Promise<ReadReply> {
98
+ if (revoked) return readFailure("revoked");
99
+ if (signal?.aborted) return readFailure("cancelled");
100
+ if (!refs.has(request.ref)) return readFailure("denied");
101
+ return page(request);
102
+ },
103
+ revoke(): void {
104
+ revoked = true;
105
+ },
106
+ preflight(): { missing: string[] } {
107
+ // Reference handoff is a DURABILITY contract: a memory-only record is not a receipt
108
+ // a later process (or a resumed run) could read back, so it does not pass preflight.
109
+ const missing: string[] = [];
110
+ for (const ref of refs) {
111
+ const result = view.read({ ref, offset: 0, limit: 2 });
112
+ if (!result.ok || result.summary.storage !== "session-entry") missing.push(ref);
113
+ }
114
+ return { missing };
115
+ },
116
+ };
117
+ }
118
+
119
+ /** The full stored body of an archived result, paged out to the archive's own ceiling. Used
120
+ * by a reference-mode RESUME to rebuild a done phase's output from the archive (contract 2). */
121
+ export function readArchivedBody(view: ResultArchiveView, ref: string):
122
+ | { ok: true; body: string; status: TerminalStatus; truncated: boolean }
123
+ | { ok: false; error: string } {
124
+ let offset = 0;
125
+ let body = "";
126
+ let status: TerminalStatus | undefined;
127
+ let truncated = false;
128
+ while (true) {
129
+ const page = view.read({ ref, offset, limit: RESULT_READ_MAX });
130
+ if (!page.ok) return { ok: false, error: `${page.reason}: ${page.message}` };
131
+ status = page.summary.status;
132
+ truncated = page.summary.truncated;
133
+ body += page.body;
134
+ if (page.endOfStoredBody || page.nextOffset === undefined) break;
135
+ if (page.nextOffset <= offset || body.length >= RESULT_MAX_BODY_CHARS) {
136
+ return { ok: false, error: `archive paging did not advance past offset ${page.nextOffset}` };
137
+ }
138
+ offset = page.nextOffset;
139
+ }
140
+ if (status === undefined) return { ok: false, error: "archive returned no status" };
141
+ return { ok: true, body, status, truncated };
142
+ }
@@ -0,0 +1,128 @@
1
+ import type { ThinkingLevel } from "../core/types.ts";
2
+
3
+ export const TERMINAL_RESULT_ENTRY = "pi-persona-terminal-result";
4
+ export const RESULT_MAX_ENTRIES = 128;
5
+ /** UTF-8 JSON custom-entry DATA only; the host's JSONL envelope is excluded. */
6
+ export const RESULT_MAX_PAYLOAD_BYTES = 8 * 1024 * 1024;
7
+ export const RESULT_MAX_RECORD_BYTES = 512 * 1024;
8
+ export const RESULT_MAX_BODY_CHARS = 262_144;
9
+ export const RESULT_READ_DEFAULT = 8_000;
10
+ export const RESULT_READ_MAX = 16_000;
11
+ export const RESULT_LIST_DEFAULT = 20;
12
+ export const RESULT_LIST_MAX = 50;
13
+
14
+ export type TerminalStatus = "done" | "failed" | "stopped";
15
+ export type ResultProvenance = "result-output" | "partial-output" | "error";
16
+ export type ResultStorage = "session-entry" | "memory-only";
17
+ export type ResultWarning = "no-session-storage" | "accounting-unavailable" | "invalid-history"
18
+ | "duplicate-history" | "ambiguous-history" | "append-failed" | "body-truncated";
19
+
20
+ /** Cumulative usage only, never a context-window claim or per-token stream. */
21
+ export interface TerminalUsage {
22
+ input: number;
23
+ output: number;
24
+ cacheRead?: number;
25
+ cacheWrite?: number;
26
+ cost?: number;
27
+ turns?: number;
28
+ }
29
+
30
+ /** Trusted caller projection, NOT an arbitrary AgentResult. Unknown fields are rejected.
31
+ * Metadata ceilings (UTF-16): originKey 1024, originRunId/agent 128, label 256,
32
+ * model 512, error 2048. Invalid metadata rejects the save rather than silently changing it. */
33
+ export interface TerminalResultInput {
34
+ /** Immutable activation + run/phase identity. Routing labels are not identities. */
35
+ originKey: string;
36
+ originRunId?: string;
37
+ status: TerminalStatus;
38
+ agent: string;
39
+ label?: string;
40
+ model?: string;
41
+ thinkingRequested?: ThinkingLevel;
42
+ thinkingEffective?: ThinkingLevel;
43
+ usage?: TerminalUsage;
44
+ /** Epoch milliseconds. Omit to stamp a NEW record with the injected clock. */
45
+ createdAt?: number;
46
+ body: string;
47
+ provenance: ResultProvenance;
48
+ error?: string;
49
+ }
50
+
51
+ export interface TerminalResultRecord extends Omit<TerminalResultInput, "createdAt"> {
52
+ version: 1;
53
+ ref: string;
54
+ createdAt: number;
55
+ /** SHA-256 content identity includes the original body, even its discarded suffix.
56
+ * This is collision detection, not authentication of host history. */
57
+ payloadHash: string;
58
+ originalChars: number;
59
+ storedChars: number;
60
+ truncated: boolean;
61
+ }
62
+
63
+ export interface ResultSummary extends Omit<TerminalResultRecord, "body" | "payloadHash"> {
64
+ storage: ResultStorage;
65
+ }
66
+
67
+ export interface ResultAccounting {
68
+ /** Includes unknown/malformed/duplicate history and reserved append attempts. */
69
+ chargedEntries: number;
70
+ /** null means the lifetime baseline is unaccountable; persistence is disabled. */
71
+ chargedPayloadBytes: number | null;
72
+ persistenceDisabled: boolean;
73
+ }
74
+
75
+ export interface ArchiveEvidence {
76
+ accounting: ResultAccounting;
77
+ warnings: ResultWarning[];
78
+ }
79
+
80
+ export interface ArchiveFailure extends ArchiveEvidence {
81
+ ok: false;
82
+ reason: "invalid" | "unavailable" | "conflict" | "ambiguous" | "corrupt" | "quota" | "busy";
83
+ message: string;
84
+ }
85
+
86
+ export type SaveOutcome = ArchiveFailure | (ArchiveEvidence & {
87
+ ok: true;
88
+ ref: string;
89
+ /** session-entry means append returned normally or data came from native history.
90
+ * It is NOT an fsync/crash-survival guarantee. memory-only is never a durable receipt. */
91
+ storage: ResultStorage;
92
+ summary: ResultSummary;
93
+ idempotent: boolean;
94
+ storageError?: string;
95
+ });
96
+
97
+ export interface ResultReadRequest { ref: string; offset?: number; limit?: number }
98
+ export type ResultPage = ArchiveFailure | (ArchiveEvidence & {
99
+ ok: true;
100
+ summary: ResultSummary;
101
+ /** Untrusted text; the integration must apply its shared fence on model delivery. */
102
+ body: string;
103
+ offset: number;
104
+ nextOffset?: number;
105
+ /** Reached the end of the STORED body, not necessarily the original report. */
106
+ endOfStoredBody: boolean;
107
+ });
108
+
109
+ export interface ResultListRequest { cursor?: number; limit?: number }
110
+ export type ResultSummaryPage = ArchiveFailure | (ArchiveEvidence & {
111
+ ok: true;
112
+ results: ResultSummary[];
113
+ /** Opaque-in-practice append ordinal, not a run id or a branch-local index. */
114
+ nextCursor?: number;
115
+ });
116
+
117
+ export interface TerminalResultArchiveDeps {
118
+ /** All branches in ONE owning session file. Never read parent/foreign files.
119
+ * History entries are immutable host data. Hydration is read-only and runs once.
120
+ * Recreate the archive on owner change; adapters must reject stale-owner writes. */
121
+ entries: () => readonly unknown[];
122
+ /** Native custom ENTRY, not a model message. May throw after partially committing. */
123
+ append: (customType: string, data: unknown) => void;
124
+ persistent: boolean;
125
+ /** Must return result:<UUID>. The default uses node:crypto.randomUUID(). */
126
+ makeRef?: () => string;
127
+ clock?: () => number;
128
+ }
@@ -4,7 +4,7 @@ export const TELEMETRY_EVENT_NAME = "pi:telemetry" as const;
4
4
  export const LEGACY_TELEMETRY_EVENT_NAME = "pi-persona:telemetry" as const;
5
5
  export const LEGACY_TELEMETRY_VERSION = 1 as const;
6
6
  export const TELEMETRY_PRODUCER_ID = "pi-persona" as const;
7
- export const TELEMETRY_PRODUCER_VERSION = "1.17.3" as const;
7
+ export const TELEMETRY_PRODUCER_VERSION = "1.17.4" as const;
8
8
 
9
9
  export type InstanceStatus = "active" | "idle" | "stopped" | "stale" | (string & {});
10
10
  export type AgentKind = "subagent" | "delegate" | "council" | "flow" | "phase" | (string & {});
@@ -2,9 +2,10 @@
2
2
  import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
3
3
  import { Type } from "typebox";
4
4
  import { fenceUntrusted } from "../core/fence.ts";
5
- import { expandDetailHint, failureDetails, formatCouncilCallLabel, isInline } from "../extension/shared.ts";
5
+ import { resultReceiptHint } from "../results/retrieval.ts";
6
+ import { expandDetailHint, failureDetails, formatCouncilCallLabel, isInline, toastCause } from "../extension/shared.ts";
6
7
  import { knownParams, missingRequiredParams, strategyCatalog } from "../orchestration/strategy.ts";
7
- import { memberBaseLabel, type RosterMember } from "../orchestration/roster.ts";
8
+ import { memberBaseLabel, parseRuntimeRosterMember, type RosterMember } from "../orchestration/roster.ts";
8
9
  import { formatCouncilResult, humanizeAggregateResult } from "../orchestration/render.ts";
9
10
  import type { FailureKind } from "../orchestration/types.ts";
10
11
  import type { AgentResult } from "../orchestration/types.ts";
@@ -13,6 +14,7 @@ import type { OrchestrationGrammar, Persona } from "../persona/persona.ts";
13
14
  import type { PersonaController } from "../persona/controller.ts";
14
15
  import { Text } from "@earendil-works/pi-tui";
15
16
  import { createReportBody } from "../ui/report-body.ts";
17
+ import { orchestrationLaunchLines, type OrchestrationLaunchSnapshot } from "../ui/orchestration-launch.ts";
16
18
  import { compactInlineText, compactVisibleText, sanitizeTerminalText } from "../ui/presentation.ts";
17
19
  import { toolUsageField } from "../ui/usage.ts";
18
20
  import { emptyUsage } from "../engine/stream.ts";
@@ -24,6 +26,7 @@ export interface CouncilToolDeps {
24
26
  set lastCtx(value: ExtensionContext | undefined);
25
27
  controller: PersonaController;
26
28
  personas: Persona[];
29
+ launchSnapshot(ctx: ExtensionContext, orch: OrchestrationGrammar): OrchestrationLaunchSnapshot;
27
30
  runStrategyVisible(
28
31
  ctx: ExtensionContext,
29
32
  orch: OrchestrationGrammar,
@@ -60,7 +63,7 @@ export function registerCouncilTool(pi: ExtensionAPI, d: CouncilToolDeps): void
60
63
  members: Type.Optional(
61
64
  Type.Array(Type.Unknown(), {
62
65
  description:
63
- 'Ephemeral panel members for THIS call — each an installed agent name ("scout") or an inline { agent, role?, model?, skills? } specialisation. They win `roster` and the profile\'s own members for this call only: no team file, persona file, or inherited model/tool is touched. Omit to use the named roster.',
66
+ 'Ephemeral panel members for THIS call — each an installed agent name ("scout") or an inline { agent, role?, model?, thinking?, skills?, tools?, isolation?, mcp? } specialisation. thinking is per-leg requested effort (off/minimal/low/medium/high/xhigh/max); Pi clamps it to model capabilities, overriding the agent and shared child effort defaults. They win `roster` and the profile\'s own members for this call only: no team file, persona file, or inherited model/tool is touched. Omit to use the named roster.',
64
67
  }),
65
68
  ),
66
69
  params: Type.Optional(
@@ -113,6 +116,7 @@ ${strategyCatalog()}`,
113
116
  }
114
117
  const { strategy, roster, members, params: mergedParams, persona } = resolved.value;
115
118
  const panel = panelLabel({ roster, members });
119
+ let launch: OrchestrationLaunchSnapshot | undefined;
116
120
  try {
117
121
  // Fully persona-driven: a persona's `council:` block picks the strategy, roster,
118
122
  // and params — a new ensemble (more members, supermajority, multi-round) needs no
@@ -127,7 +131,9 @@ ${strategyCatalog()}`,
127
131
  if (unknown.length > 0) {
128
132
  const note = `council: ignoring unknown param(s) [${unknown.join(", ")}] for "${strategy}" — known: ${Object.keys(schema).join(", ") || "(none)"}`;
129
133
  if (process.env.PI_PERSONA_DEBUG) process.stderr.write(`[pi-persona] ${note}\n`);
130
- ctx.ui.notify(note, "warning");
134
+ // The toast is bounded/sanitized for the operator; the model-facing paramNote below
135
+ // keeps the raw note byte-identical (payloads are not presentation).
136
+ ctx.ui.notify(toastCause(note), "warning");
131
137
  paramNote = `\n\n(${note})`;
132
138
  }
133
139
  }
@@ -135,6 +141,14 @@ ${strategyCatalog()}`,
135
141
  // Ephemeral members ride the SAME grammar path as a named team (see
136
142
  // `resolveOrchestrationMembers`) — nothing about the profile is inherited.
137
143
  if (members) orch.members = members;
144
+ try {
145
+ const snapshot = d.launchSnapshot(ctx, orch);
146
+ launch = { ...snapshot, participants: snapshot.participants.map((p) => ({ ...p })) };
147
+ } catch {
148
+ // A display lookup must not change launch admission; its failure is visible, not
149
+ // replaced with guessed models or an apparently successful empty panel.
150
+ launch = { participants: [], unavailable: true };
151
+ }
138
152
  // The same launch `delegate` uses: ONE tracked run. Interactive sessions get a receipt now and
139
153
  // the ruling as ONE completion follow-up (the idle-gated wake, fenced and bounded); headless
140
154
  // sessions join the run before returning, since nothing would deliver a follow-up there.
@@ -146,7 +160,7 @@ ${strategyCatalog()}`,
146
160
  const names = compactInlineText(`${strategy} · ${panel}`, { maxChars: 120 });
147
161
  return {
148
162
  content: [{ type: "text", text: `${launchReceipt("council", id, names, "with the ruling")}${paramNote}` }],
149
- details: { runId: id, strategy, roster: panel, persona },
163
+ details: { runId: id, strategy, roster: panel, persona, launch },
150
164
  isError: false,
151
165
  };
152
166
  }
@@ -155,7 +169,7 @@ ${strategyCatalog()}`,
155
169
  const result = run?.result;
156
170
  if (!result) {
157
171
  const message = run?.error ?? run?.status ?? "the run record is gone";
158
- return { content: [{ type: "text", text: `council failed: ${message}` }], details: failureDetails({ error: message, strategy, roster: panel }), isError: true };
172
+ return { content: [{ type: "text", text: `council failed: ${message}` }], details: failureDetails({ error: message, strategy, roster: panel, ...(launch ? { launch } : {}) }), isError: true };
159
173
  }
160
174
  const s = (result.structured ?? {}) as {
161
175
  headline?: string;
@@ -167,6 +181,7 @@ ${strategyCatalog()}`,
167
181
  };
168
182
  const headline = s.headline ?? (typeof s.count === "number" ? `${s.count} member results` : s.status ?? "");
169
183
  const details = {
184
+ launch,
170
185
  ok: result.ok,
171
186
  headline,
172
187
  status: s.status,
@@ -179,32 +194,31 @@ ${strategyCatalog()}`,
179
194
  persona,
180
195
  ...(result.error ? { error: result.error } : {}),
181
196
  ...(result.failureKind ? { failureKind: result.failureKind } : {}),
197
+ ...(run?.resultReceipt ? { resultReceipt: { ...run.resultReceipt } } : {}),
182
198
  };
183
199
  return {
184
200
  // The ruling is sub-agent (council member) text — fence it like every other
185
201
  // path that hands sub-agent output to the supervisor.
186
202
  // A run stopped before it convened has no ruling, only its cause: show that, not an empty fence.
187
- content: [{ type: "text", text: `${fenceUntrusted(result.output || result.error || "")}${paramNote}${d.drainBusBlock()}` }],
203
+ content: [{ type: "text", text: `${fenceUntrusted(result.output || result.error || "")}${run?.resultReceipt ? `\n\n${resultReceiptHint(run.resultReceipt)}` : ""}${paramNote}${d.drainBusBlock()}` }],
188
204
  details: result.ok ? details : failureDetails(details),
189
205
  isError: !result.ok,
190
206
  ...toolUsageField(usage),
191
207
  };
192
208
  } catch (err) {
193
209
  const message = err instanceof Error ? err.message : String(err);
194
- return { content: [{ type: "text", text: `council failed: ${message}` }], details: failureDetails({ error: message, strategy, roster: panel }), isError: true };
210
+ return { content: [{ type: "text", text: `council failed: ${message}` }], details: failureDetails({ error: message, strategy, roster: panel, ...(launch ? { launch } : {}) }), isError: true };
195
211
  }
196
212
  },
197
213
  renderCall(args, theme) {
198
- const resolved = resolveCouncilInvocation(d.personas, d.controller.activePersona, {
199
- persona: args.persona,
200
- strategy: args.strategy,
201
- roster: args.roster,
202
- members: args.members,
203
- params: args.params as Record<string, unknown> | undefined,
204
- });
205
- const strategy = resolved.ok ? resolved.value.strategy : (args.strategy ?? "?");
206
- // With inline members there is no named team to name — label the panel itself.
207
- const roster = resolved.ok ? panelLabel(resolved.value) : (args.roster ?? args.persona ?? "?");
214
+ // Native Pi draws the call before its saved result. Keep this title args-only and
215
+ // neutral about omitted defaults; the receipt below carries the resolved snapshot.
216
+ const members = Array.isArray(args.members) ? args.members.flatMap((raw) => {
217
+ const parsed = parseRuntimeRosterMember(raw);
218
+ return parsed.ok ? [parsed.member] : [];
219
+ }) : [];
220
+ const strategy = args.strategy ?? "profile defaults";
221
+ const roster = members?.length ? panelLabel({ roster: "", members }) : args.roster ?? args.persona ?? "profile roster";
208
222
  return new Text(theme.fg("toolTitle", theme.bold(formatCouncilCallLabel(strategy, roster))), 0, 0);
209
223
  },
210
224
  renderResult(result, { expanded }, theme, context) {
@@ -220,6 +234,7 @@ ${strategyCatalog()}`,
220
234
  error?: string;
221
235
  failureKind?: FailureKind;
222
236
  runId?: string;
237
+ launch?: OrchestrationLaunchSnapshot;
223
238
  };
224
239
  const first = result.content[0];
225
240
  const failed = (context?.isError ?? result.isError) || d.ok === false || !!d.error;
@@ -227,27 +242,29 @@ ${strategyCatalog()}`,
227
242
  // Interactive launch receipt: the ruling arrives later as the completion follow-up.
228
243
  const launched = failed ? theme.fg("error", theme.bold("council failed"))
229
244
  : `${theme.fg("toolTitle", theme.bold("council "))}${theme.fg("accent", compactInlineText(`launched ${d.runId} · ${d.strategy ?? "?"} · ${d.roster ?? "?"}`, { maxChars: 120 }))}`;
230
- return new Text(expanded && first?.type === "text" ? `${launched}\n${theme.fg("toolOutput", sanitizeTerminalText(first.text))}` : launched, 0, 0);
245
+ const metadata = orchestrationLaunchLines(d.launch, expanded).join("\n");
246
+ return new Text([launched, theme.fg("dim", metadata), expanded && first?.type === "text" ? theme.fg("toolOutput", sanitizeTerminalText(first.text)) : ""].filter(Boolean).join("\n"), 0, 0);
231
247
  }
232
248
  const body = sanitizeTerminalText(d.body || (first && first.type === "text" ? first.text : ""));
249
+ const metadata = d.launch ? `\n\nLaunch request (Pi may clamp effort):\n${orchestrationLaunchLines(d.launch, expanded).join("\n")}` : "";
233
250
  if (failed) {
234
251
  const cause = compactInlineText([d.failureKind, d.error].filter((part): part is string => !!part).join(" · "), { maxChars: 160 });
235
252
  const title = () => theme.fg("error", theme.bold(`council failed${cause ? ` · ${cause}` : ""}`));
236
253
  if (expanded) {
237
254
  // Only a completed council retains a ruling body; preflight/host errors are literal.
238
- if (typeof d.body === "string") return createReportBody(body || "(no ruling)", theme, { header: title }, context?.lastComponent);
239
- return new Text(`${title()}\n${theme.fg("toolOutput", body || "(no ruling)")}`, 0, 0);
255
+ if (typeof d.body === "string") return createReportBody(`${body || "(no ruling)"}${metadata}`, theme, { header: title }, context?.lastComponent);
256
+ return new Text(`${title()}\n${theme.fg("toolOutput", body || "(no ruling)")}${theme.fg("dim", metadata)}`, 0, 0);
240
257
  }
241
258
  const preview = compactVisibleText(body || "(no ruling)", { maxLines: 3, maxLineChars: 100 });
242
259
  const hint = preview.truncated ? `\n${theme.fg("dim", expandDetailHint())}` : "";
243
- return new Text(`${title()}\n${theme.fg("toolOutput", preview.text)}${hint}`, 0, 0);
260
+ return new Text(`${title()}\n${theme.fg("toolOutput", preview.text)}${hint}${theme.fg("dim", metadata)}`, 0, 0);
244
261
  }
245
262
  const text = formatCouncilResult(
246
263
  { headline: d.headline, status: d.status, tally: d.tally, usedFallback: d.usedFallback, body },
247
264
  expanded,
248
265
  expandDetailHint(),
249
266
  );
250
- return expanded ? createReportBody(text, theme, {}, context?.lastComponent) : new Text(theme.fg("accent", text), 0, 0);
267
+ return expanded ? createReportBody(`${text}${metadata}`, theme, {}, context?.lastComponent) : new Text(`${theme.fg("accent", text)}${theme.fg("dim", metadata)}`, 0, 0);
251
268
  },
252
269
  });
253
270
  }