pi-crew 0.6.3 → 0.7.1

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 (56) hide show
  1. package/CHANGELOG.md +84 -0
  2. package/README.md +80 -1
  3. package/docs/ui-optimization-plan.md +447 -0
  4. package/package.json +2 -1
  5. package/src/extension/knowledge-injection.ts +71 -0
  6. package/src/extension/pi-api.ts +47 -0
  7. package/src/extension/register.ts +32 -1
  8. package/src/extension/registration/commands.ts +65 -1
  9. package/src/extension/registration/compaction-guard.ts +154 -14
  10. package/src/extension/registration/subagent-tools.ts +8 -3
  11. package/src/extension/registration/team-tool.ts +18 -11
  12. package/src/extension/team-tool/handle-settings.ts +57 -0
  13. package/src/extension/team-tool/inspect.ts +4 -1
  14. package/src/extension/team-tool/plan.ts +8 -1
  15. package/src/extension/team-tool/run.ts +4 -3
  16. package/src/extension/team-tool-types.ts +2 -0
  17. package/src/runtime/intercom-bridge.ts +5 -1
  18. package/src/runtime/replace.ts +555 -0
  19. package/src/runtime/resilient-edit.ts +166 -0
  20. package/src/runtime/single-agent-compose.ts +87 -0
  21. package/src/runtime/team-runner.ts +23 -6
  22. package/src/schema/team-tool-schema.ts +6 -0
  23. package/src/state/session-state-map.ts +51 -0
  24. package/src/state/usage.ts +73 -0
  25. package/src/ui/card-colors.ts +126 -0
  26. package/src/ui/deploy-bundled-themes.ts +71 -0
  27. package/src/ui/powerbar-publisher.ts +1 -1
  28. package/src/ui/render-diff.ts +37 -3
  29. package/src/ui/settings-overlay.ts +70 -7
  30. package/src/ui/status-colors.ts +5 -1
  31. package/src/ui/syntax-highlight.ts +42 -23
  32. package/src/ui/theme-adapter.ts +80 -1
  33. package/src/ui/theme-discovery.ts +188 -0
  34. package/src/ui/tool-progress-formatter.ts +9 -5
  35. package/src/ui/tool-render.ts +4 -0
  36. package/src/ui/tool-renderers/brief-mode.ts +207 -0
  37. package/src/ui/tool-renderers/index.ts +640 -0
  38. package/src/ui/widget/index.ts +224 -0
  39. package/src/ui/widget/widget-formatters.ts +148 -0
  40. package/src/ui/widget/widget-model.ts +90 -0
  41. package/src/ui/widget/widget-renderer.ts +130 -0
  42. package/src/ui/widget/widget-types.ts +37 -0
  43. package/src/utils/guards.ts +110 -0
  44. package/themes/crew-catppuccin-latte.json +96 -0
  45. package/themes/crew-catppuccin-mocha.json +93 -0
  46. package/themes/crew-dark.json +90 -0
  47. package/themes/crew-dracula.json +83 -0
  48. package/themes/crew-gruvbox-dark.json +83 -0
  49. package/themes/crew-gruvbox-light.json +90 -0
  50. package/themes/crew-nord.json +85 -0
  51. package/themes/crew-one-dark.json +89 -0
  52. package/themes/crew-solarized-dark.json +90 -0
  53. package/themes/crew-solarized-light.json +92 -0
  54. package/themes/crew-tokyo-night.json +85 -0
  55. package/src/runtime/budget-tracker.ts +0 -354
  56. package/src/state/memory-store.ts +0 -244
@@ -0,0 +1,71 @@
1
+ /**
2
+ * knowledge-injection.ts — Project knowledge that accumulates across runs (O4).
3
+ *
4
+ * ROADMAP Phase 1 / T1.3 ("downsized memory"): a deliberately minimal
5
+ * replacement for the deleted 244-LOC MemoryStore. Crews and the main
6
+ * session read `.crew/knowledge.md` and have it injected into the system
7
+ * prompt, so pi-crew "remembers" project context across runs.
8
+ *
9
+ * Philosophy (Round 6 stress-test): radically downsized. Just a Markdown
10
+ * file the user can edit, surfaced into every run. No vector DB, no
11
+ * embeddings, no graph. Simple = trustworthy.
12
+ */
13
+ import * as fs from "node:fs";
14
+ import * as path from "node:path";
15
+ import type { BeforeAgentStartEvent, ExtensionAPI } from "./pi-api.ts";
16
+ import { projectCrewRoot } from "../utils/paths.ts";
17
+
18
+ /** The knowledge file, relative to the project crew root. */
19
+ export const KNOWLEDGE_FILENAME = "knowledge.md";
20
+ /** Cap injected knowledge to avoid unbounded system-prompt growth. */
21
+ const MAX_KNOWLEDGE_BYTES = 16_000;
22
+
23
+ /** Resolve the knowledge file path for a cwd (may not exist yet). */
24
+ export function knowledgePath(cwd: string): string {
25
+ return path.join(projectCrewRoot(cwd), KNOWLEDGE_FILENAME);
26
+ }
27
+
28
+ /** Read knowledge content, truncated to a safe size. "" if absent/empty. */
29
+ export function readKnowledge(cwd: string): string {
30
+ try {
31
+ const p = knowledgePath(cwd);
32
+ if (!fs.existsSync(p)) return "";
33
+ let content = fs.readFileSync(p, "utf8").trim();
34
+ if (content.length > MAX_KNOWLEDGE_BYTES) {
35
+ content = `${content.slice(0, MAX_KNOWLEDGE_BYTES)}\n\n<!-- knowledge.md truncated at ${MAX_KNOWLEDGE_BYTES} bytes -->`;
36
+ }
37
+ return content;
38
+ } catch {
39
+ return "";
40
+ }
41
+ }
42
+
43
+ /** Build the injected prompt fragment (empty if no knowledge). */
44
+ export function buildKnowledgeFragment(cwd: string): string {
45
+ const content = readKnowledge(cwd);
46
+ if (!content) return "";
47
+ return [
48
+ "",
49
+ "# Project knowledge (from .crew/knowledge.md)",
50
+ "The following project knowledge was captured by pi-crew from prior runs.",
51
+ "Use it to avoid repeating past mistakes and to respect project conventions.",
52
+ "You may update .crew/knowledge.md when you learn something durable.",
53
+ "",
54
+ content,
55
+ ].join("\n");
56
+ }
57
+
58
+ /**
59
+ * Register the knowledge-injection hook. Appends project knowledge to the
60
+ * system prompt on every agent start (main session + each crew worker,
61
+ * since workers are child Pi processes that also fire before_agent_start).
62
+ */
63
+ export function registerKnowledgeInjection(pi: ExtensionAPI): void {
64
+ pi.on("before_agent_start", (event: BeforeAgentStartEvent) => {
65
+ const options = (event as BeforeAgentStartEvent & { systemPromptOptions?: { cwd?: unknown } }).systemPromptOptions ?? {};
66
+ const cwd = typeof options.cwd === "string" ? options.cwd : process.cwd();
67
+ const fragment = buildKnowledgeFragment(cwd);
68
+ if (!fragment) return undefined;
69
+ return { systemPrompt: `${event.systemPrompt}${fragment}` };
70
+ });
71
+ }
@@ -0,0 +1,47 @@
1
+ /**
2
+ * pi-api.ts — pi-crew's stable seam against the Pi extension API.
3
+ *
4
+ * PURPOSE (roadmap Phase 0 / Pillar 4 "Pi-Native, Protocol-Aware"):
5
+ * Centralize every symbol pi-crew imports from
6
+ * `@earendil-works/pi-coding-agent` so that:
7
+ * 1. The coupling surface is auditable in ONE file (8 symbols today).
8
+ * 2. A Pi API rename/restructure requires updating only this seam.
9
+ * 3. New code imports from here ("./pi-api") rather than the raw package,
10
+ * establishing the indirection that hedges Pi-coupling risk.
11
+ *
12
+ * COUpling SURFACE (kept intentionally small — public extension API only):
13
+ * - ExtensionAPI the registration entry point (pi)
14
+ * - ExtensionContext per-event/session context (ctx)
15
+ * - ExtensionCommandContext slash-command context
16
+ * - ToolDefinition tool schema type
17
+ * - defineTool tool factory
18
+ * - createBashTool built-in bash tool (for custom tooling)
19
+ * - AgentSessionEvent session event type
20
+ * - BeforeAgentStartEvent pre-turn system-prompt hook event
21
+ *
22
+ * NEW code should `import { ExtensionAPI, ExtensionContext } from "./pi-api"`.
23
+ * Existing files may keep their direct imports; migrate opportunistically
24
+ * (no big-bang refactor — see roadmap Phase 0 T0.3/T0.4 notes).
25
+ *
26
+ * Note: these are TYPE-level re-exports (erased at runtime). Runtime coupling
27
+ * to Pi is via the `pi` and `ctx` objects passed by Pi's loader — that is
28
+ * unavoidable and correct (pi-crew IS a Pi extension). This seam documents
29
+ * and centralizes the type-level surface.
30
+ */
31
+ export type {
32
+ ExtensionAPI,
33
+ ExtensionContext,
34
+ ExtensionCommandContext,
35
+ ToolDefinition,
36
+ AgentSessionEvent,
37
+ BeforeAgentStartEvent,
38
+ } from "@earendil-works/pi-coding-agent";
39
+
40
+ export { defineTool, createBashTool } from "@earendil-works/pi-coding-agent";
41
+
42
+ /**
43
+ * The Pi package version pi-crew was built against. Used for diagnostics
44
+ * and to surface version-drift if Pi upgrades introduce breaking changes.
45
+ * Update this when bumping the pi-coding-agent dependency.
46
+ */
47
+ export const BUILT_AGAINST_PI_VERSION = "0.79.3";
@@ -17,6 +17,7 @@ import {
17
17
  stopAsyncRunNotifier,
18
18
  } from "./async-notifier.ts";
19
19
  import { registerAutonomousPolicy } from "./autonomous-policy.ts";
20
+ import { registerKnowledgeInjection } from "./knowledge-injection.ts";
20
21
  import { registerCleanupHandler } from "./crew-cleanup.ts";
21
22
  import type { ScheduledJob } from "../runtime/scheduler.ts";
22
23
  import { clearHooksScoped } from "../hooks/registry.ts";
@@ -61,8 +62,9 @@ import {
61
62
  type CrewWidgetState,
62
63
  stopCrewWidget,
63
64
  updateCrewWidget,
64
- } from "../ui/crew-widget.ts";
65
+ } from "../ui/widget/index.ts";
65
66
  import { summarizeHeartbeats } from "../ui/heartbeat-aggregator.ts";
67
+ import { deployBundledThemes } from "../ui/deploy-bundled-themes.ts";
66
68
  import {
67
69
  requestRender,
68
70
  setExtensionWidget,
@@ -187,6 +189,9 @@ export function registerPiTeams(pi: ExtensionAPI): void {
187
189
  const disposeI18n = initI18n(pi);
188
190
  resetTimings();
189
191
  time("register:start");
192
+ // Deploy bundled themes (crew-dark, crew-dracula, etc.) to ~/.pi/agent/themes/
193
+ // so Pi's theme loader discovers them. Best-effort, idempotent.
194
+ deployBundledThemes();
190
195
  const globalStore = globalThis as Record<string | symbol, unknown>;
191
196
  const runtimeCleanupStoreKey = Symbol("__piCrewRuntimeCleanup");
192
197
  const previousRuntimeCleanup = globalStore[runtimeCleanupStoreKey];
@@ -981,6 +986,8 @@ export function registerPiTeams(pi: ExtensionAPI): void {
981
986
  };
982
987
  time("register.policy");
983
988
  registerAutonomousPolicy(pi);
989
+ registerKnowledgeInjection(pi);
990
+ time("register.knowledge");
984
991
  time("register.rpc");
985
992
  function getPiEvents():
986
993
  | Parameters<typeof registerPiCrewRpc>[0]
@@ -1194,6 +1201,17 @@ export function registerPiTeams(pi: ExtensionAPI): void {
1194
1201
 
1195
1202
  pi.on("session_start", (_event, ctx) => {
1196
1203
  runArtifactCleanup(ctx.cwd);
1204
+
1205
+ // Restore brief mode state from session entries
1206
+ try {
1207
+ const entries = ctx.sessionManager?.getEntries?.();
1208
+ if (entries) {
1209
+ import("../ui/tool-renderers/brief-mode.ts").then(({ restoreBriefState }) => {
1210
+ restoreBriefState(entries);
1211
+ }).catch(() => {/* non-critical */});
1212
+ }
1213
+ } catch { /* non-critical */ }
1214
+
1197
1215
  time("register.session-start");
1198
1216
  cleanedUp = false;
1199
1217
  sessionGeneration++;
@@ -1974,6 +1992,19 @@ export function registerPiTeams(pi: ExtensionAPI): void {
1974
1992
 
1975
1993
  registerCleanupHandler(pi);
1976
1994
 
1995
+
1996
+ // Resilient edit (Phase 2): OPT-IN via CREW_RESILIENT_EDIT=1.
1997
+ // Wraps the native edit tool with the cascading replace() fallback so that
1998
+ // slightly-wrong oldText (indentation drift, whitespace) still matches.
1999
+ // Auto-disables if pi-diff is loaded (it has its own replace integration).
2000
+ if (process.env.CREW_RESILIENT_EDIT === "1") {
2001
+ import("../runtime/resilient-edit.ts")
2002
+ .then(({ wrapEditWithResilientReplace }) => {
2003
+ wrapEditWithResilientReplace(pi);
2004
+ })
2005
+ .catch(() => { /* non-critical */ });
2006
+ }
2007
+
1977
2008
  registerTeamCommands(pi, {
1978
2009
  startForegroundRun,
1979
2010
  abortForegroundRun,
@@ -364,13 +364,52 @@ export function registerTeamCommands(pi: ExtensionAPI, deps: RegisterTeamCommand
364
364
  } catch (error) {
365
365
  ctx.ui.notify(`Failed to save: ${error instanceof Error ? error.message : String(error)}`, "error");
366
366
  }
367
- }, () => done(undefined));
367
+ }, () => done(undefined), async (action: string, value: unknown) => {
368
+ // Action callbacks (Pi theme switch) write to a different store
369
+ // than pi-crew config (e.g. ~/.pi/agent/settings.json).
370
+ try {
371
+ if (action === "piTheme" && typeof value === "string") {
372
+ // Live theme switch: ctx.ui.setTheme() swaps the global theme,
373
+ // persists it to settings.json, and triggers a UI redraw — no
374
+ // restart needed. Falls back to file-write + restart hint if
375
+ // the live API is unavailable (e.g. non-TUI mode).
376
+ if (typeof ctx.ui.setTheme === "function") {
377
+ const res = ctx.ui.setTheme(value);
378
+ if (res.success) {
379
+ ctx.ui.notify(`Theme: ${value} (applied live)`, "info");
380
+ } else {
381
+ const { setPiTheme } = await import("../../ui/theme-discovery.ts");
382
+ setPiTheme(value);
383
+ ctx.ui.notify(`Theme saved as '${value}' but failed to apply: ${res.error ?? "unknown"}. Restart Pi.`, "warning");
384
+ }
385
+ } else {
386
+ const { setPiTheme } = await import("../../ui/theme-discovery.ts");
387
+ setPiTheme(value);
388
+ ctx.ui.notify(`Pi theme set to '${value}'. Restart Pi to apply.`, "info");
389
+ }
390
+ }
391
+ } catch (error) {
392
+ ctx.ui.notify(`Failed: ${error instanceof Error ? error.message : String(error)}`, "error");
393
+ }
394
+ });
368
395
  return overlay;
369
396
  }, { overlay: true, overlayOptions: { width: "90%", maxHeight: "85%", anchor: "center" } });
370
397
  return;
371
398
  }
372
399
  const result = await handleTeamTool({ action: "settings", config: { args: args.trim() } }, teamCommandContext(ctx));
373
400
  await notifyCommandResult(ctx, commandText(result));
401
+ // Live-switch hook: when the text subcommand 'theme <name>' succeeds,
402
+ // also apply the theme live via ctx.ui.setTheme() (no restart). The
403
+ // handler above only writes to settings.json.
404
+ const trimmed = args.trim();
405
+ if (trimmed.startsWith("theme ")) {
406
+ const themeName = trimmed.slice(6).trim();
407
+ if (themeName && typeof ctx.ui.setTheme === "function") {
408
+ const res = ctx.ui.setTheme(themeName);
409
+ if (res.success) ctx.ui.notify(`Theme: ${themeName} (applied live)`, "info");
410
+ else ctx.ui.notify(`Saved but live-switch failed: ${res.error ?? "unknown"}. Restart Pi.`, "warning");
411
+ }
412
+ }
374
413
  },
375
414
  })
376
415
 
@@ -571,6 +610,31 @@ export function registerTeamCommands(pi: ExtensionAPI, deps: RegisterTeamCommand
571
610
  pi.registerCommand("team-help", { description: "Show pi-crew command help", handler: async (_args: string, ctx: ExtensionCommandContext) => {
572
611
  await notifyCommandResult(ctx, piTeamsHelp());
573
612
  } });
613
+
614
+ // Brief mode command — toggle compact tool output display
615
+ pi.registerCommand("crew-brief", {
616
+ description: "Toggle brief tool output mode: on | off | status",
617
+ handler: async (args: string, ctx: ExtensionCommandContext) => {
618
+ const { isBrief, setBrief, BRIEF_ENTRY_TYPE, makeBriefEntry } = await import("../../ui/tool-renderers/brief-mode.ts");
619
+ const trimmed = args.trim();
620
+
621
+ if (trimmed === "on") {
622
+ setBrief(true);
623
+ pi.appendEntry(BRIEF_ENTRY_TYPE, { enabled: true });
624
+ ctx.ui.notify("Brief mode: on — tool output will show compact summaries", "info");
625
+ return;
626
+ }
627
+ if (trimmed === "off") {
628
+ setBrief(false);
629
+ pi.appendEntry(BRIEF_ENTRY_TYPE, { enabled: false });
630
+ ctx.ui.notify("Brief mode: off — full tool output restored", "info");
631
+ return;
632
+ }
633
+ // status (default)
634
+ ctx.ui.notify(`Brief mode: ${isBrief() ? "on" : "off"}`, "info");
635
+ },
636
+ });
637
+
574
638
  time("register.commands");
575
639
  printTimings();
576
640
  }
@@ -12,6 +12,8 @@ const HARD_RATIO = 0.95;
12
12
  const DEFAULT_CONTEXT_WINDOW = 200_000;
13
13
  const MAX_ARTIFACT_INDEX_RUNS = 10;
14
14
  const MAX_ARTIFACT_INDEX_ITEMS = 80;
15
+ /** Run statuses that mean the run is still in-flight and may need resuming. */
16
+ const IN_FLIGHT_RUN_STATUSES = new Set(["queued", "planning", "running"]);
15
17
 
16
18
  function contextWindow(ctx: { model?: { contextWindow?: number } }): number {
17
19
  const value = ctx.model?.contextWindow;
@@ -66,6 +68,99 @@ function formatCrewArtifactIndex(entries: CrewArtifactIndexEntry[]): string {
66
68
  return lines.join("\n");
67
69
  }
68
70
 
71
+ /**
72
+ * Collect in-flight (non-terminal) crew runs that must be resumable after
73
+ * compaction. These are runs the agent was actively working on or awaiting.
74
+ */
75
+ export function collectInFlightRuns(cwd: string): TeamRunManifest[] {
76
+ return listRecentRuns(cwd, MAX_ARTIFACT_INDEX_RUNS).filter((run) =>
77
+ IN_FLIGHT_RUN_STATUSES.has(run.status),
78
+ );
79
+ }
80
+
81
+ /**
82
+ * Build an explicit resume directive that survives compaction. This is the
83
+ * core of O10 (compaction resilience): after compaction, the agent MUST know
84
+ * what crew tasks were in-flight and how to continue them.
85
+ */
86
+ function formatResumeDirective(runs: TeamRunManifest[]): string {
87
+ if (!runs.length) return "";
88
+ const lines = [
89
+ "",
90
+ "# pi-crew in-flight task resume directive (CRITICAL — do not drop)",
91
+ "The following pi-crew runs were in progress when the context was compacted.",
92
+ "After compaction, you MUST continue these tasks — do NOT consider them finished.",
93
+ ];
94
+ for (const run of runs) {
95
+ const wf = run.workflow ? `, workflow=${run.workflow}` : "";
96
+ lines.push(
97
+ `- runId=${run.runId} (status=${run.status}, team=${run.team}${wf}): ${run.goal}`,
98
+ );
99
+ }
100
+ lines.push("");
101
+ lines.push("To resume: call the `team` tool with action='status' to check progress, then");
102
+ lines.push("action='wait' (to join a still-running task) or action='summary' / action='get'");
103
+ lines.push("to retrieve results. If a task was mid-execution and the worker is still alive,");
104
+ lines.push("it continues independently — just re-attach. Do not restart completed work.");
105
+ return lines.join("\n");
106
+ }
107
+
108
+ /**
109
+ * Build a short continuation prompt for sendUserMessage. This is what actually
110
+ * makes the agent resume after compaction — Pi's threshold compaction does NOT
111
+ * auto-retry (it stops and waits for user input). By injecting this as a user
112
+ * message that triggers a turn, the agent automatically continues the in-flight
113
+ * crew task instead of stalling until the user types "continue".
114
+ */
115
+ export function buildContinuationPrompt(runs: TeamRunManifest[]): string {
116
+ if (!runs.length) return "";
117
+ const lines = [
118
+ "[pi-crew] Context was compacted while crew tasks were still in-flight. Continue the work — do not wait for me.",
119
+ ];
120
+ for (const run of runs) {
121
+ const wf = run.workflow ? `, workflow=${run.workflow}` : "";
122
+ lines.push(`- runId=${run.runId} (status=${run.status}, team=${run.team}${wf}): ${run.goal}`);
123
+ }
124
+ lines.push("");
125
+ lines.push("Resume: call `team` with action='status' to check progress, then action='wait' (join a running task), action='summary', or action='get' as appropriate. If a worker is still alive it continues independently — just re-attach. Do NOT restart completed work.");
126
+ return lines.join("\n");
127
+ }
128
+
129
+ /**
130
+ * Trigger automatic agent continuation after compaction. Fire-and-forget the
131
+ * promise — never block the compaction flow. The sendUserMessage type is
132
+ * declared `void` but the runtime returns a Promise (it triggers an agent turn).
133
+ */
134
+ export function triggerContinuation(pi: ExtensionAPI, ctx: ExtensionContext, runs: TeamRunManifest[]): void {
135
+ if (!runs.length) return;
136
+ const prompt = buildContinuationPrompt(runs);
137
+ try {
138
+ const result = pi.sendUserMessage(prompt) as unknown;
139
+ Promise.resolve(result).catch(() => {
140
+ // best-effort: if continuation fails, at least notify
141
+ try {
142
+ ctx.ui.notify("pi-crew: auto-continuation after compaction failed — use team status to resume manually.", "warning");
143
+ } catch {
144
+ // swallow
145
+ }
146
+ });
147
+ } catch {
148
+ // best-effort
149
+ }
150
+ }
151
+
152
+ /** Combined customInstructions injected into proactive compaction summaries. */
153
+ function buildCompactionInstructions(cwd: string): string {
154
+ const artifactIndex = collectCrewArtifactIndex(cwd);
155
+ const inFlight = collectInFlightRuns(cwd);
156
+ const parts = [
157
+ "Prioritize keeping pi-crew run state, task results, artifact references, run IDs, and next actions. Keep completed-task detail concise.",
158
+ ];
159
+ if (artifactIndex.length > 0) parts.push(formatCrewArtifactIndex(artifactIndex));
160
+ if (inFlight.length > 0) parts.push(formatResumeDirective(inFlight));
161
+ return parts.join("\n");
162
+ }
163
+
69
164
  export function registerCompactionGuard(pi: ExtensionAPI, options: RegisterCompactionGuardOptions): void {
70
165
  let pendingCompactReason: string | null = null;
71
166
  let compactionInProgress = false;
@@ -73,18 +168,32 @@ export function registerCompactionGuard(pi: ExtensionAPI, options: RegisterCompa
73
168
  const startCompact = (ctx: ExtensionContext, reason: string): void => {
74
169
  if (compactionInProgress) return;
75
170
  compactionInProgress = true;
76
- const artifactIndex = collectCrewArtifactIndex(ctx.cwd);
77
- if (artifactIndex.length > 0) {
78
- pi.appendEntry("crew:artifact-index", {
171
+ const customInstructions = buildCompactionInstructions(ctx.cwd);
172
+ // Append a durable resume entry so it appears in the post-compaction
173
+ // context regardless of how summarization treats customInstructions.
174
+ const inFlight = collectInFlightRuns(ctx.cwd);
175
+ if (inFlight.length > 0) {
176
+ pi.appendEntry("crew:resume-directive", {
79
177
  reason,
80
178
  createdAt: new Date().toISOString(),
81
- artifacts: artifactIndex,
179
+ runs: inFlight.map((r) => ({
180
+ runId: r.runId,
181
+ status: r.status,
182
+ team: r.team,
183
+ workflow: r.workflow,
184
+ goal: r.goal,
185
+ })),
82
186
  });
83
187
  }
84
188
  ctx.compact({
85
- customInstructions: `Prioritize keeping pi-crew run state, task results, artifact references, run IDs, and next actions. Keep completed-task detail concise.${formatCrewArtifactIndex(artifactIndex)}`,
189
+ customInstructions,
86
190
  onComplete: () => {
87
191
  compactionInProgress = false;
192
+ // O10 FIX: Pi's threshold compaction does NOT auto-retry — it
193
+ // stops and waits for user input. Trigger automatic
194
+ // continuation so the agent resumes the in-flight crew task.
195
+ const runs = collectInFlightRuns(ctx.cwd);
196
+ triggerContinuation(pi, ctx, runs);
88
197
  ctx.ui.notify(reason === "deferred" ? "Deferred compaction completed" : "Auto-compacted context during team run", "info");
89
198
  },
90
199
  onError: (error) => {
@@ -94,17 +203,49 @@ export function registerCompactionGuard(pi: ExtensionAPI, options: RegisterCompa
94
203
  });
95
204
  };
96
205
 
97
- // Phase 1.2: Always allow compaction to proceed.
98
- // Previously this deferred compaction during foreground runs, but that prevented
99
- // context-mode's PreCompact hook from running (which needs compaction to build resume snapshots).
100
- // pi-crew state is preserved via customInstructions and artifactIndex appended above.
101
- pi.on("session_before_compact", async (_event, ctx) => {
102
- // Always allow compaction - context-mode needs it for resume snapshots
206
+ // Allow compaction to proceed. pi-crew state is preserved via the
207
+ // customInstructions + resume-directive entry appended in startCompact,
208
+ // and re-injected post-compaction by the session_compact handler below.
209
+ pi.on("session_before_compact", async (_event, _ctx) => {
103
210
  return;
104
211
  });
105
212
 
106
- // Phase 2.1: Proactive compaction with dynamic threshold.
107
- // Only trigger compaction when context usage >=75%, or when deferred from a previous high-usage turn.
213
+ // O10: After ANY compaction (proactive OR reactive/Pi-triggered), detect
214
+ // in-flight crew runs and trigger automatic continuation. This is the
215
+ // critical fix: Pi's threshold compaction does NOT auto-retry — it stops
216
+ // and waits for user input. By injecting a continuation user message that
217
+ // triggers a turn, the agent automatically resumes the in-flight crew task.
218
+ // This covers the common case where Pi auto-compacts without going through
219
+ // our proactive startCompact path.
220
+ pi.on("session_compact", (_event, ctx) => {
221
+ try {
222
+ const inFlight = collectInFlightRuns(ctx.cwd);
223
+ if (inFlight.length === 0) return;
224
+ // Re-append the resume directive entry for durable record.
225
+ pi.appendEntry("crew:resume-directive", {
226
+ reason: "post-compaction-continuation",
227
+ createdAt: new Date().toISOString(),
228
+ runs: inFlight.map((r) => ({
229
+ runId: r.runId,
230
+ status: r.status,
231
+ team: r.team,
232
+ workflow: r.workflow,
233
+ goal: r.goal,
234
+ })),
235
+ });
236
+ ctx.ui.notify(
237
+ `Context compacted. ${inFlight.length} pi-crew run(s) still in-flight — auto-resuming.`,
238
+ "info",
239
+ );
240
+ // THE FIX: trigger automatic continuation. Without this, Pi stops
241
+ // after threshold compaction and the user must type "continue".
242
+ triggerContinuation(pi, ctx, inFlight);
243
+ } catch {
244
+ // best-effort: never block compaction completion
245
+ }
246
+ });
247
+
248
+ // Proactive compaction with dynamic threshold.
108
249
  pi.on("turn_end", (_event, ctx) => {
109
250
  if (compactionInProgress) return;
110
251
  const hasActiveForeground = options.foregroundControllers.size > 0 || options.foregroundTeamRunControllers.size > 0;
@@ -112,7 +253,6 @@ export function registerCompactionGuard(pi: ExtensionAPI, options: RegisterCompa
112
253
  // If deferred compaction is pending and foreground just ended, check if still needed
113
254
  if (!hasActiveForeground && pendingCompactReason) {
114
255
  pendingCompactReason = null;
115
- // Only compact if ratio still >= TRIGGER_RATIO, otherwise skip
116
256
  if (ratio === undefined || ratio < TRIGGER_RATIO) return;
117
257
  startCompact(ctx, "deferred");
118
258
  return;
@@ -22,7 +22,8 @@ import { t } from "../../i18n.ts";
22
22
  import { loadRunManifestById } from "../../state/state-store.ts";
23
23
  import { readCrewAgents } from "../../runtime/crew-agent-records.ts";
24
24
  import { formatCompactToolProgress } from "../../ui/tool-progress-formatter.ts";
25
- import { renderAgentToolCall, renderAgentToolResult } from "../../ui/tool-render.ts";
25
+ import { Text } from "@earendil-works/pi-tui";
26
+ import { agentToolRenderer } from "../../ui/tool-renderers/index.ts";
26
27
 
27
28
  const TOOL_PROGRESS_TICK_MS = 1000;
28
29
 
@@ -98,11 +99,15 @@ export function registerSubagentTools(pi: ExtensionAPI, subagentManager: Subagen
98
99
  },
99
100
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
100
101
  renderCall(args: any, theme: any, context: any): any {
101
- return renderAgentToolCall(args, theme, context);
102
+ return agentToolRenderer.renderCall(args, theme, context);
102
103
  },
103
104
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
104
105
  renderResult(result: any, options: any, theme: any, context: any): any {
105
- return renderAgentToolResult(result, options, theme, context);
106
+ try {
107
+ return agentToolRenderer.renderResult(result, options, theme, context);
108
+ } catch (e: any) {
109
+ return new Text('agent-err: ' + (e?.message ?? 'unknown'), 0, 0);
110
+ }
106
111
  },
107
112
  };
108
113
 
@@ -1,15 +1,16 @@
1
- import * as fs from "node:fs";
1
+ import { statSync } from "node:fs";
2
2
  import type { ExtensionAPI, ExtensionContext, ToolDefinition } from "@earendil-works/pi-coding-agent";
3
3
  import { loadConfig } from "../../config/config.ts";
4
4
  import { TeamToolParams, type TeamToolParamsValue } from "../../schema/team-tool-schema.ts";
5
- import type { CrewWidgetState } from "../../ui/crew-widget.ts";
6
- import { updateCrewWidget } from "../../ui/crew-widget.ts";
5
+ import type { CrewWidgetState } from "../../ui/widget/index.ts";
6
+ import { updateCrewWidget } from "../../ui/widget/index.ts";
7
7
  import { updatePiCrewPowerbar } from "../../ui/powerbar-publisher.ts";
8
8
  import type { createManifestCache } from "../../runtime/manifest-cache.ts";
9
9
  import type { createRunSnapshotCache } from "../../ui/run-snapshot-cache.ts";
10
10
  import type { MetricRegistry } from "../../observability/metric-registry.ts";
11
11
  import { resolveRealContainedPath } from "../../utils/safe-paths.ts";
12
- import { renderTeamToolCall, renderTeamToolResult } from "../../ui/tool-render.ts";
12
+ import { Text } from "@earendil-works/pi-tui";
13
+ import { teamToolRenderer, statusIcon } from "../../ui/tool-renderers/index.ts";
13
14
  // Team tool handler — lazy-loaded because team-tool.ts imports many modules
14
15
  import type { handleTeamTool as HandleTeamToolFn } from "../team-tool.ts";
15
16
  let _cachedHandleTeamTool: typeof HandleTeamToolFn | undefined;
@@ -48,7 +49,7 @@ export function resolveCwdOverride(baseCwd: string, override: string | undefined
48
49
  if (!override) return { ok: true, cwd: baseCwd };
49
50
  try {
50
51
  const resolved = resolveRealContainedPath(baseCwd, override);
51
- const stat = fs.statSync(resolved);
52
+ const stat = statSync(resolved);
52
53
  if (!stat.isDirectory()) return { ok: false, error: `cwd override is not a directory: ${resolved}` };
53
54
  return { ok: true, cwd: resolved };
54
55
  } catch (error) {
@@ -107,11 +108,15 @@ export function registerTeamTool(pi: ExtensionAPI, deps: RegisterTeamToolDeps):
107
108
  },
108
109
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
109
110
  renderCall(args: any, theme: any, context: any): any {
110
- return renderTeamToolCall(args, theme, context);
111
+ return teamToolRenderer.renderCall(args, theme, context);
111
112
  },
112
113
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
113
114
  renderResult(result: any, options: any, theme: any, context: any): any {
114
- return renderTeamToolResult(result, options, theme, context);
115
+ try {
116
+ return teamToolRenderer.renderResult(result, options, theme, context);
117
+ } catch {
118
+ return new Text(statusIcon("completed", theme) + " done", 0, 0);
119
+ }
115
120
  },
116
121
  };
117
122
  pi.registerTool(tool);
@@ -133,13 +138,15 @@ function startTeamToolProgressBinder(onUpdate: OnUpdate | undefined): TeamToolPr
133
138
  try {
134
139
  if (!cwd || !runId) {
135
140
  const elapsed = Math.max(0, Math.round((Date.now() - startedAt) / 1000));
136
- onUpdate({ content: [{ type: "text", text: `team status=starting elapsed=${elapsed}s` }] });
141
+ const msg = `team status=starting elapsed=${elapsed}s`;
142
+ onUpdate({ content: [{ type: "text", text: msg }] });
137
143
  return;
138
144
  }
139
- const loaded = loadRunManifestById(cwd, runId); // NOTE: no withRunLock - best-effort only; concurrent writes may cause inconsistency
145
+ const loaded = loadRunManifestById(cwd, runId);
140
146
  if (!loaded) {
141
147
  const elapsed = Math.max(0, Math.round((Date.now() - startedAt) / 1000));
142
- onUpdate({ content: [{ type: "text", text: `team run=${runId} elapsed=${elapsed}s (manifest pending)` }] });
148
+ const msg = `team run=${runId} elapsed=${elapsed}s (manifest pending)`;
149
+ onUpdate({ content: [{ type: "text", text: msg }] });
143
150
  return;
144
151
  }
145
152
  let agents;
@@ -162,7 +169,7 @@ function startTeamToolProgressBinder(onUpdate: OnUpdate | undefined): TeamToolPr
162
169
  const timer = setInterval(tick, TEAM_TOOL_PROGRESS_TICK_MS);
163
170
  if (typeof timer.unref === "function") timer.unref();
164
171
  return {
165
- attach: (boundCwd, boundRunId) => { cwd = boundCwd; runId = boundRunId; tick(); },
172
+ attach: (boundCwd: string, boundRunId: string) => { cwd = boundCwd; runId = boundRunId; tick(); },
166
173
  stop: () => clearInterval(timer),
167
174
  };
168
175
  }