pi-crew 0.1.45 → 0.1.46

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 (198) hide show
  1. package/README.md +5 -5
  2. package/agents/analyst.md +1 -1
  3. package/agents/critic.md +1 -1
  4. package/agents/executor.md +1 -1
  5. package/agents/explorer.md +1 -1
  6. package/agents/planner.md +1 -1
  7. package/agents/reviewer.md +1 -1
  8. package/agents/security-reviewer.md +1 -1
  9. package/agents/test-engineer.md +1 -1
  10. package/agents/verifier.md +1 -1
  11. package/agents/writer.md +1 -1
  12. package/docs/next-upgrade-roadmap.md +733 -0
  13. package/docs/refactor-tasks-phase3.md +394 -394
  14. package/docs/refactor-tasks-phase4.md +564 -564
  15. package/docs/refactor-tasks-phase5.md +402 -402
  16. package/docs/refactor-tasks-phase6.md +662 -662
  17. package/docs/research-awesome-agent-skills-distillation.md +100 -0
  18. package/docs/research-extension-examples.md +297 -297
  19. package/docs/research-extension-system.md +324 -324
  20. package/docs/research-oh-my-pi-distillation.md +322 -0
  21. package/docs/research-optimization-plan.md +548 -548
  22. package/docs/research-phase10-distillation.md +198 -198
  23. package/docs/research-phase11-distillation.md +201 -201
  24. package/docs/research-pi-coding-agent.md +357 -357
  25. package/docs/research-source-pi-crew-reference.md +174 -174
  26. package/docs/runtime-flow.md +148 -148
  27. package/docs/source-runtime-refactor-map.md +107 -83
  28. package/docs/usage.md +3 -3
  29. package/index.ts +6 -6
  30. package/install.mjs +52 -8
  31. package/package.json +1 -1
  32. package/schema.json +2 -1
  33. package/skills/async-worker-recovery/SKILL.md +42 -0
  34. package/skills/context-artifact-hygiene/SKILL.md +52 -0
  35. package/skills/delegation-patterns/SKILL.md +54 -0
  36. package/skills/mailbox-interactive/SKILL.md +40 -0
  37. package/skills/model-routing-context/SKILL.md +39 -0
  38. package/skills/multi-perspective-review/SKILL.md +58 -0
  39. package/skills/observability-reliability/SKILL.md +41 -0
  40. package/skills/ownership-session-security/SKILL.md +41 -0
  41. package/skills/pi-extension-lifecycle/SKILL.md +39 -0
  42. package/skills/requirements-to-task-packet/SKILL.md +63 -0
  43. package/skills/resource-discovery-config/SKILL.md +41 -0
  44. package/skills/runtime-state-reader/SKILL.md +44 -0
  45. package/skills/secure-agent-orchestration-review/SKILL.md +45 -0
  46. package/skills/state-mutation-locking/SKILL.md +42 -0
  47. package/skills/systematic-debugging/SKILL.md +67 -0
  48. package/skills/ui-render-performance/SKILL.md +39 -0
  49. package/skills/verification-before-done/SKILL.md +57 -0
  50. package/skills/worktree-isolation/SKILL.md +39 -0
  51. package/src/agents/agent-serializer.ts +34 -34
  52. package/src/agents/discover-agents.ts +12 -11
  53. package/src/config/config.ts +48 -24
  54. package/src/config/defaults.ts +14 -0
  55. package/src/extension/cross-extension-rpc.ts +82 -82
  56. package/src/extension/project-init.ts +62 -2
  57. package/src/extension/register.ts +11 -9
  58. package/src/extension/registration/commands.ts +32 -25
  59. package/src/extension/registration/compaction-guard.ts +125 -125
  60. package/src/extension/registration/subagent-helpers.ts +8 -0
  61. package/src/extension/registration/subagent-tools.ts +149 -148
  62. package/src/extension/registration/team-tool.ts +8 -6
  63. package/src/extension/run-bundle-schema.ts +89 -89
  64. package/src/extension/run-index.ts +13 -5
  65. package/src/extension/run-maintenance.ts +62 -43
  66. package/src/extension/team-tool/api.ts +25 -8
  67. package/src/extension/team-tool/cancel.ts +33 -4
  68. package/src/extension/team-tool/context.ts +5 -0
  69. package/src/extension/team-tool/handle-settings.ts +188 -188
  70. package/src/extension/team-tool/inspect.ts +41 -41
  71. package/src/extension/team-tool/lifecycle-actions.ts +91 -79
  72. package/src/extension/team-tool/plan.ts +19 -19
  73. package/src/extension/team-tool/respond.ts +37 -17
  74. package/src/extension/team-tool/run.ts +52 -10
  75. package/src/extension/team-tool/status.ts +12 -1
  76. package/src/extension/team-tool-types.ts +2 -0
  77. package/src/extension/team-tool.ts +32 -11
  78. package/src/i18n.ts +184 -184
  79. package/src/observability/event-to-metric.ts +8 -1
  80. package/src/observability/exporters/otlp-exporter.ts +77 -77
  81. package/src/prompt/prompt-runtime.ts +72 -72
  82. package/src/runtime/agent-control.ts +63 -63
  83. package/src/runtime/agent-memory.ts +72 -72
  84. package/src/runtime/agent-observability.ts +114 -114
  85. package/src/runtime/async-marker.ts +26 -26
  86. package/src/runtime/attention-events.ts +28 -28
  87. package/src/runtime/background-runner.ts +59 -53
  88. package/src/runtime/cancellation.ts +51 -0
  89. package/src/runtime/child-pi.ts +457 -444
  90. package/src/runtime/completion-guard.ts +190 -190
  91. package/src/runtime/crash-recovery.ts +1 -0
  92. package/src/runtime/crew-agent-records.ts +38 -6
  93. package/src/runtime/deadletter.ts +1 -0
  94. package/src/runtime/delivery-coordinator.ts +46 -25
  95. package/src/runtime/direct-run.ts +35 -35
  96. package/src/runtime/effectiveness.ts +76 -0
  97. package/src/runtime/foreground-control.ts +82 -82
  98. package/src/runtime/green-contract.ts +46 -46
  99. package/src/runtime/group-join.ts +106 -106
  100. package/src/runtime/heartbeat-gradient.ts +28 -28
  101. package/src/runtime/heartbeat-watcher.ts +124 -124
  102. package/src/runtime/live-agent-control.ts +88 -87
  103. package/src/runtime/live-agent-manager.ts +103 -85
  104. package/src/runtime/live-control-realtime.ts +36 -36
  105. package/src/runtime/live-session-runtime.ts +309 -305
  106. package/src/runtime/manifest-cache.ts +17 -2
  107. package/src/runtime/model-fallback.ts +6 -4
  108. package/src/runtime/parallel-research.ts +44 -44
  109. package/src/runtime/pi-args.ts +18 -3
  110. package/src/runtime/pi-json-output.ts +111 -111
  111. package/src/runtime/policy-engine.ts +79 -79
  112. package/src/runtime/process-status.ts +5 -1
  113. package/src/runtime/progress-event-coalescer.ts +43 -43
  114. package/src/runtime/recovery-recipes.ts +74 -74
  115. package/src/runtime/retry-executor.ts +81 -64
  116. package/src/runtime/role-permission.ts +39 -39
  117. package/src/runtime/runtime-resolver.ts +22 -6
  118. package/src/runtime/session-resources.ts +25 -25
  119. package/src/runtime/session-snapshot.ts +59 -59
  120. package/src/runtime/session-usage.ts +79 -79
  121. package/src/runtime/sidechain-output.ts +29 -29
  122. package/src/runtime/skill-instructions.ts +222 -0
  123. package/src/runtime/stale-reconciler.ts +4 -14
  124. package/src/runtime/subagent-manager.ts +3 -0
  125. package/src/runtime/supervisor-contact.ts +59 -59
  126. package/src/runtime/task-display.ts +38 -38
  127. package/src/runtime/task-output-context.ts +127 -127
  128. package/src/runtime/task-runner/capabilities.ts +78 -0
  129. package/src/runtime/task-runner/live-executor.ts +105 -101
  130. package/src/runtime/task-runner/progress.ts +119 -119
  131. package/src/runtime/task-runner/prompt-builder.ts +3 -1
  132. package/src/runtime/task-runner/prompt-pipeline.ts +64 -0
  133. package/src/runtime/task-runner/result-utils.ts +14 -14
  134. package/src/runtime/task-runner/state-helpers.ts +22 -22
  135. package/src/runtime/task-runner.ts +44 -5
  136. package/src/runtime/team-runner.ts +78 -15
  137. package/src/runtime/worker-heartbeat.ts +21 -21
  138. package/src/runtime/worker-startup.ts +57 -57
  139. package/src/schema/config-schema.ts +1 -0
  140. package/src/schema/team-tool-schema.ts +3 -3
  141. package/src/state/active-run-registry.ts +165 -0
  142. package/src/state/contracts.ts +1 -1
  143. package/src/state/mailbox.ts +44 -4
  144. package/src/state/state-store.ts +8 -1
  145. package/src/state/task-claims.ts +44 -44
  146. package/src/state/types.ts +44 -2
  147. package/src/state/usage.ts +29 -29
  148. package/src/subagents/async-entry.ts +1 -1
  149. package/src/subagents/index.ts +3 -3
  150. package/src/subagents/live/control.ts +1 -1
  151. package/src/subagents/live/manager.ts +1 -1
  152. package/src/subagents/live/realtime.ts +1 -1
  153. package/src/subagents/live/session-runtime.ts +1 -1
  154. package/src/subagents/manager.ts +1 -1
  155. package/src/subagents/spawn.ts +1 -1
  156. package/src/teams/team-config.ts +1 -0
  157. package/src/teams/team-serializer.ts +38 -38
  158. package/src/types/diff.d.ts +18 -18
  159. package/src/ui/crew-footer.ts +101 -101
  160. package/src/ui/crew-select-list.ts +111 -111
  161. package/src/ui/crew-widget.ts +4 -3
  162. package/src/ui/dashboard-panes/metrics-pane.ts +34 -34
  163. package/src/ui/dashboard-panes/progress-pane.ts +2 -0
  164. package/src/ui/dynamic-border.ts +25 -25
  165. package/src/ui/layout-primitives.ts +106 -106
  166. package/src/ui/loaders.ts +158 -158
  167. package/src/ui/render-diff.ts +119 -119
  168. package/src/ui/render-scheduler.ts +143 -143
  169. package/src/ui/run-snapshot-cache.ts +10 -2
  170. package/src/ui/snapshot-types.ts +2 -0
  171. package/src/ui/spinner.ts +17 -17
  172. package/src/ui/status-colors.ts +58 -58
  173. package/src/ui/syntax-highlight.ts +116 -116
  174. package/src/utils/atomic-write.ts +33 -33
  175. package/src/utils/completion-dedupe.ts +63 -63
  176. package/src/utils/frontmatter.ts +68 -68
  177. package/src/utils/git.ts +262 -262
  178. package/src/utils/ids.ts +12 -12
  179. package/src/utils/names.ts +27 -27
  180. package/src/utils/paths.ts +4 -2
  181. package/src/utils/redaction.ts +44 -44
  182. package/src/utils/safe-paths.ts +47 -47
  183. package/src/utils/sleep.ts +32 -32
  184. package/src/workflows/validate-workflow.ts +40 -40
  185. package/src/workflows/workflow-config.ts +1 -0
  186. package/src/worktree/branch-freshness.ts +45 -45
  187. package/teams/default.team.md +12 -12
  188. package/teams/fast-fix.team.md +11 -11
  189. package/teams/implementation.team.md +18 -18
  190. package/teams/parallel-research.team.md +14 -14
  191. package/teams/research.team.md +11 -11
  192. package/teams/review.team.md +12 -12
  193. package/workflows/default.workflow.md +29 -29
  194. package/workflows/fast-fix.workflow.md +22 -22
  195. package/workflows/implementation.workflow.md +38 -38
  196. package/workflows/parallel-research.workflow.md +46 -46
  197. package/workflows/research.workflow.md +22 -22
  198. package/workflows/review.workflow.md +30 -30
@@ -1,10 +1,13 @@
1
1
  import * as fs from "node:fs";
2
2
  import * as path from "node:path";
3
- import { packageRoot, projectCrewRoot } from "../utils/paths.ts";
3
+ import { configPath as globalConfigPath } from "../config/config.ts";
4
+ import { DEFAULT_UI } from "../config/defaults.ts";
5
+ import { packageRoot, projectCrewRoot, projectPiRoot } from "../utils/paths.ts";
4
6
 
5
7
  export interface ProjectInitOptions {
6
8
  copyBuiltins?: boolean;
7
9
  overwrite?: boolean;
10
+ configScope?: "global" | "project" | "none";
8
11
  }
9
12
 
10
13
  export interface ProjectInitResult {
@@ -13,6 +16,10 @@ export interface ProjectInitResult {
13
16
  skippedFiles: string[];
14
17
  gitignorePath: string;
15
18
  gitignoreUpdated: boolean;
19
+ configPath: string;
20
+ configScope: "global" | "project" | "none";
21
+ configCreated: boolean;
22
+ configSkipped: boolean;
16
23
  }
17
24
 
18
25
  function ensureDir(dir: string, createdDirs: string[]): void {
@@ -24,6 +31,44 @@ function ensureDir(dir: string, createdDirs: string[]): void {
24
31
  }
25
32
  }
26
33
 
34
+ const DEFAULT_PI_CREW_CONFIG = {
35
+ // Keep generated config non-invasive: do not set runtime/limits defaults here.
36
+ // Those are provided by pi-crew internals and should not make a normal workflow block.
37
+ autonomous: {
38
+ enabled: true,
39
+ injectPolicy: true,
40
+ preferAsyncForLongTasks: false,
41
+ allowWorktreeSuggestion: true,
42
+ },
43
+ agents: {
44
+ overrides: {
45
+ explorer: { model: false, thinking: "off" },
46
+ writer: { model: false, thinking: "off" },
47
+ planner: { model: false, thinking: "medium" },
48
+ analyst: { model: false, thinking: "off" },
49
+ critic: { model: false, thinking: "low" },
50
+ executor: { model: false, thinking: "medium" },
51
+ reviewer: { model: false, thinking: "off" },
52
+ "security-reviewer": { model: false, thinking: "medium" },
53
+ "test-engineer": { model: false, thinking: "low" },
54
+ verifier: { model: false, thinking: "off" },
55
+ },
56
+ },
57
+ ui: {
58
+ widgetPlacement: DEFAULT_UI.widgetPlacement,
59
+ widgetMaxLines: DEFAULT_UI.widgetMaxLines,
60
+ powerbar: DEFAULT_UI.powerbar,
61
+ dashboardPlacement: DEFAULT_UI.dashboardPlacement,
62
+ dashboardWidth: DEFAULT_UI.dashboardWidth,
63
+ dashboardLiveRefreshMs: DEFAULT_UI.dashboardLiveRefreshMs,
64
+ autoOpenDashboard: DEFAULT_UI.autoOpenDashboard,
65
+ autoOpenDashboardForForegroundRuns: DEFAULT_UI.autoOpenDashboardForForegroundRuns,
66
+ showModel: DEFAULT_UI.showModel,
67
+ showTokens: DEFAULT_UI.showTokens,
68
+ showTools: DEFAULT_UI.showTools,
69
+ },
70
+ };
71
+
27
72
  function copyBuiltinDir(kind: "agents" | "teams" | "workflows", targetDir: string, overwrite: boolean, copiedFiles: string[], skippedFiles: string[]): void {
28
73
  const sourceDir = path.join(packageRoot(), kind);
29
74
  if (!fs.existsSync(sourceDir)) return;
@@ -50,11 +95,26 @@ export function initializeProject(cwd: string, options: ProjectInitOptions = {})
50
95
  const agentsDir = path.join(crewRoot, "agents");
51
96
  const teamsDir = path.join(crewRoot, "teams");
52
97
  const workflowsDir = path.join(crewRoot, "workflows");
98
+ const configScope = options.configScope ?? "global";
99
+ const configPath = configScope === "project" ? path.join(projectPiRoot(cwd), "pi-crew.json") : configScope === "global" ? globalConfigPath() : "";
53
100
  ensureDir(agentsDir, createdDirs);
54
101
  ensureDir(teamsDir, createdDirs);
55
102
  ensureDir(workflowsDir, createdDirs);
56
103
  ensureDir(path.join(crewRoot, "imports"), createdDirs);
57
104
 
105
+ let configCreated = false;
106
+ let configSkipped = false;
107
+ if (configPath) {
108
+ if (configScope === "project") ensureDir(path.dirname(configPath), createdDirs);
109
+ else fs.mkdirSync(path.dirname(configPath), { recursive: true });
110
+ if (!fs.existsSync(configPath) || options.overwrite === true) {
111
+ fs.writeFileSync(configPath, `${JSON.stringify(DEFAULT_PI_CREW_CONFIG, null, 2)}\n`, "utf-8");
112
+ configCreated = true;
113
+ } else {
114
+ configSkipped = true;
115
+ }
116
+ }
117
+
58
118
  if (options.copyBuiltins) {
59
119
  copyBuiltinDir("agents", agentsDir, options.overwrite === true, copiedFiles, skippedFiles);
60
120
  copyBuiltinDir("teams", teamsDir, options.overwrite === true, copiedFiles, skippedFiles);
@@ -72,5 +132,5 @@ export function initializeProject(cwd: string, options: ProjectInitOptions = {})
72
132
  gitignoreUpdated = true;
73
133
  }
74
134
 
75
- return { createdDirs, copiedFiles, skippedFiles, gitignorePath, gitignoreUpdated };
135
+ return { createdDirs, copiedFiles, skippedFiles, gitignorePath, gitignoreUpdated, configPath, configScope, configCreated, configSkipped };
76
136
  }
@@ -1,6 +1,7 @@
1
1
  import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
2
2
  import * as fs from "node:fs";
3
3
  import * as path from "node:path";
4
+ import { fileURLToPath } from "node:url";
4
5
  import { loadConfig } from "../config/config.ts";
5
6
  import { registerAutonomousPolicy } from "./autonomous-policy.ts";
6
7
  import { startAsyncRunNotifier, stopAsyncRunNotifier, type AsyncNotifierState } from "./async-notifier.ts";
@@ -253,18 +254,18 @@ export function registerPiTeams(pi: ExtensionAPI): void {
253
254
  const openLiveSidebar = (ctx: ExtensionContext, runId: string): void => {
254
255
  const uiConfig = loadConfig(ctx.cwd).config.ui;
255
256
  const autoOpen = uiConfig?.autoOpenDashboard === true;
256
- const foregroundAutoOpen = uiConfig?.autoOpenDashboardForForegroundRuns !== false;
257
- if (!ctx.hasUI || !autoOpen || !foregroundAutoOpen || (uiConfig?.dashboardPlacement ?? "right") !== "right") return;
257
+ const foregroundAutoOpen = uiConfig?.autoOpenDashboardForForegroundRuns ?? DEFAULT_UI.autoOpenDashboardForForegroundRuns;
258
+ if (!ctx.hasUI || !autoOpen || !foregroundAutoOpen || (uiConfig?.dashboardPlacement ?? DEFAULT_UI.dashboardPlacement) !== "right") return;
258
259
  if (liveSidebarRunId === runId) return;
259
260
  liveSidebarRunId = runId;
260
- const widgetPlacement = uiConfig?.widgetPlacement ?? "aboveEditor";
261
+ const widgetPlacement = uiConfig?.widgetPlacement ?? DEFAULT_UI.widgetPlacement;
261
262
  setExtensionWidget(ctx, "pi-crew", undefined, { placement: widgetPlacement });
262
263
  setExtensionWidget(ctx, "pi-crew-active", undefined, { placement: widgetPlacement });
263
264
  widgetState.lastVisibility = "hidden";
264
265
  widgetState.lastPlacement = widgetPlacement;
265
266
  widgetState.lastKey = "pi-crew-active";
266
267
  widgetState.model = undefined;
267
- const width = Math.min(90, Math.max(40, uiConfig?.dashboardWidth ?? 56));
268
+ const width = Math.min(90, Math.max(40, uiConfig?.dashboardWidth ?? DEFAULT_UI.dashboardWidth));
268
269
  void showCustom<undefined>(ctx, (_tui, theme, _keybindings, done) => new LiveRunSidebar({ cwd: ctx.cwd, runId, done, theme, config: uiConfig, snapshotCache: getRunSnapshotCache(ctx.cwd) }), {
269
270
  overlay: true,
270
271
  overlayOptions: { width, minWidth: 40, maxHeight: "100%", anchor: "top-right", offsetX: 0, offsetY: 0, margin: { top: 0, right: 0, bottom: 0, left: 0 }, visible: (termWidth: number) => termWidth >= 100 },
@@ -398,7 +399,7 @@ export function registerPiTeams(pi: ExtensionAPI): void {
398
399
  configureNotifications(ctx);
399
400
  configureObservability(ctx);
400
401
  configureDeliveryCoordinator();
401
- const sessionId = (ctx as unknown as Record<string, unknown>).sessionId;
402
+ const sessionId = ctx.sessionManager?.getSessionId?.() ?? (ctx as unknown as Record<string, unknown>).sessionId;
402
403
  if (typeof sessionId === "string" && sessionId) deliveryCoordinator?.activate(sessionId);
403
404
  tryRegisterSessionCleanup(pi, () => { terminateActiveChildPiProcesses(); cleanupRuntime(); });
404
405
  registerPiCrewPowerbarSegments(pi.events, loadedConfig.config.ui);
@@ -460,7 +461,7 @@ export function registerPiTeams(pi: ExtensionAPI): void {
460
461
  const snapshotCache = lastFrameSnapshotCache ?? getRunSnapshotCache(currentCtx.cwd);
461
462
  const manifests = lastPreloadedManifests.length > 0 ? lastPreloadedManifests : activeCache.list(20);
462
463
  if (liveSidebarRunId) {
463
- const placement = config?.widgetPlacement ?? "aboveEditor";
464
+ const placement = config?.widgetPlacement ?? DEFAULT_UI.widgetPlacement;
464
465
  if (widgetState.lastVisibility !== "hidden" || widgetState.lastPlacement !== placement) {
465
466
  setExtensionWidget(currentCtx, "pi-crew", undefined, { placement });
466
467
  setExtensionWidget(currentCtx, "pi-crew-active", undefined, { placement });
@@ -500,7 +501,7 @@ export function registerPiTeams(pi: ExtensionAPI): void {
500
501
  }
501
502
  };
502
503
 
503
- const fallbackMs = loadedConfig.config.ui?.dashboardLiveRefreshMs ?? 250;
504
+ const fallbackMs = loadedConfig.config.ui?.dashboardLiveRefreshMs ?? DEFAULT_UI.refreshMs;
504
505
  renderScheduler = new RenderScheduler(pi.events, renderTick, {
505
506
  fallbackMs,
506
507
  onInvalidate: () => getRunSnapshotCache(ctx.cwd).invalidate(),
@@ -530,8 +531,9 @@ export function registerPiTeams(pi: ExtensionAPI): void {
530
531
  // Phase 11a: Dynamic resource discovery — inject pi-crew skill paths.
531
532
  try {
532
533
  pi.on("resources_discover", () => {
533
- const skillDir = path.resolve(process.cwd(), "skills");
534
- const extSkillDir = path.resolve(__dirname, "..", "..", "skills");
534
+ const sessionCwd = currentCtx?.cwd ?? process.cwd();
535
+ const skillDir = path.resolve(sessionCwd, "skills");
536
+ const extSkillDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..", "skills");
535
537
  const paths: string[] = [];
536
538
  if (fs.existsSync(extSkillDir)) paths.push(extSkillDir);
537
539
  if (skillDir !== extSkillDir && fs.existsSync(skillDir)) paths.push(skillDir);
@@ -1,6 +1,7 @@
1
1
  import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "@mariozechner/pi-coding-agent";
2
2
  import { loadConfig } from "../../config/config.ts";
3
3
  import { handleTeamTool } from "../team-tool.ts";
4
+ import { withSessionId } from "../team-tool/context.ts";
4
5
  import { piTeamsHelp } from "../help.ts";
5
6
  import { handleTeamManagerCommand } from "../team-manager-command.ts";
6
7
  import { loadRunManifestById } from "../../state/state-store.ts";
@@ -15,6 +16,7 @@ import { MailboxDetailOverlay, type MailboxAction } from "../../ui/overlays/mail
15
16
  import { MailboxComposeOverlay, type MailboxComposeResult } from "../../ui/overlays/mailbox-compose-overlay.ts";
16
17
  import { AgentPickerOverlay } from "../../ui/overlays/agent-picker-overlay.ts";
17
18
  import { dispatchDiagnosticExport, dispatchHealthRecovery, dispatchKillStaleWorkers, dispatchMailboxAck, dispatchMailboxAckAll, dispatchMailboxCompose, dispatchMailboxNudge } from "../../ui/run-action-dispatcher.ts";
19
+ import { DEFAULT_UI } from "../../config/defaults.ts";
18
20
  import { listRecentDiagnostic } from "../../runtime/diagnostic-export.ts";
19
21
  import { commandText, notifyCommandResult, parseRunArgs, parseScalar, pushUnset, setNestedConfig } from "./command-utils.ts";
20
22
  import { openTranscriptViewer, selectAgentTask } from "./viewers.ts";
@@ -78,6 +80,10 @@ function depsNotify(ctx: ExtensionCommandContext, message: string, level: "info"
78
80
  ctx.ui.notify(message, level);
79
81
  }
80
82
 
83
+ function teamCommandContext(ctx: ExtensionCommandContext): ExtensionCommandContext & { sessionId?: string } {
84
+ return withSessionId(ctx);
85
+ }
86
+
81
87
  async function handleHealthDashboardAction(ctx: ExtensionCommandContext, selection: RunDashboardSelection): Promise<void> {
82
88
  const loaded = loadRunManifestById(ctx.cwd, selection.runId);
83
89
  if (!loaded) {
@@ -121,7 +127,7 @@ export function registerTeamCommands(pi: ExtensionAPI, deps: RegisterTeamCommand
121
127
  pi.registerCommand("teams", {
122
128
  description: "List pi-crew teams, workflows, and agents",
123
129
  handler: async (_args: string, ctx: ExtensionCommandContext) => {
124
- const result = await handleTeamTool({ action: "list" }, ctx);
130
+ const result = await handleTeamTool({ action: "list" }, teamCommandContext(ctx));
125
131
  await notifyCommandResult(ctx, commandText(result));
126
132
  },
127
133
  });
@@ -129,7 +135,7 @@ export function registerTeamCommands(pi: ExtensionAPI, deps: RegisterTeamCommand
129
135
  pi.registerCommand("team-run", {
130
136
  description: "Manually start a pi-crew run (agent may also use the team tool autonomously)",
131
137
  handler: async (args: string, ctx: ExtensionCommandContext) => {
132
- const result = await handleTeamTool(parseRunArgs(args), { ...ctx, metricRegistry: deps.getMetricRegistry?.(), startForegroundRun: (runner, runId) => deps.startForegroundRun(ctx as ExtensionContext, runner, runId), onRunStarted: (runId) => deps.openLiveSidebar(ctx as ExtensionContext, runId) });
138
+ const result = await handleTeamTool(parseRunArgs(args), { ...teamCommandContext(ctx), metricRegistry: deps.getMetricRegistry?.(), startForegroundRun: (runner, runId) => deps.startForegroundRun(ctx as ExtensionContext, runner, runId), onRunStarted: (runId) => deps.openLiveSidebar(ctx as ExtensionContext, runId) });
133
139
  await notifyCommandResult(ctx, commandText(result));
134
140
  },
135
141
  });
@@ -146,7 +152,7 @@ export function registerTeamCommands(pi: ExtensionAPI, deps: RegisterTeamCommand
146
152
  ] as const) {
147
153
  pi.registerCommand(name, { description, handler: async (args: string, ctx: ExtensionCommandContext) => {
148
154
  const runId = args.trim() || undefined;
149
- const result = await handleTeamTool({ action, runId }, ctx);
155
+ const result = await handleTeamTool({ action, runId }, teamCommandContext(ctx));
150
156
  await notifyCommandResult(ctx, commandText(result));
151
157
  } });
152
158
  }
@@ -159,7 +165,7 @@ export function registerTeamCommands(pi: ExtensionAPI, deps: RegisterTeamCommand
159
165
  const taskToken = tokens[0] === "--all" ? tokens.shift() : tokens.shift();
160
166
  const taskId = taskToken === "--all" ? undefined : taskToken;
161
167
  const message = tokens.join(" ") || undefined;
162
- const result = await handleTeamTool({ action: "respond", runId, taskId, message }, ctx);
168
+ const result = await handleTeamTool({ action: "respond", runId, taskId, message }, teamCommandContext(ctx));
163
169
  await notifyCommandResult(ctx, commandText(result));
164
170
  },
165
171
  });
@@ -178,19 +184,19 @@ export function registerTeamCommands(pi: ExtensionAPI, deps: RegisterTeamCommand
178
184
  const [key, ...rest] = token.split("=");
179
185
  if (key) config[key] = parseScalar(rest.join("="));
180
186
  }
181
- const result = await handleTeamTool({ action: "api", runId, config }, ctx);
187
+ const result = await handleTeamTool({ action: "api", runId, config }, teamCommandContext(ctx));
182
188
  await notifyCommandResult(ctx, commandText(result));
183
189
  },
184
190
  });
185
191
 
186
192
  pi.registerCommand("team-metrics", { description: "Show pi-crew metrics snapshot: [filter]", handler: async (args: string, ctx: ExtensionCommandContext) => {
187
193
  const filter = args.trim() || undefined;
188
- const result = await handleTeamTool({ action: "api", config: { operation: "metrics-snapshot", filter } }, { ...ctx, metricRegistry: deps.getMetricRegistry?.() });
194
+ const result = await handleTeamTool({ action: "api", config: { operation: "metrics-snapshot", filter } }, { ...teamCommandContext(ctx), metricRegistry: deps.getMetricRegistry?.() });
189
195
  await notifyCommandResult(ctx, commandText(result));
190
196
  } });
191
197
 
192
198
  pi.registerCommand("team-imports", { description: "List imported pi-crew run bundles", handler: async (_args: string, ctx: ExtensionCommandContext) => {
193
- const result = await handleTeamTool({ action: "imports" }, ctx);
199
+ const result = await handleTeamTool({ action: "imports" }, teamCommandContext(ctx));
194
200
  await notifyCommandResult(ctx, commandText(result));
195
201
  } });
196
202
 
@@ -198,7 +204,7 @@ export function registerTeamCommands(pi: ExtensionAPI, deps: RegisterTeamCommand
198
204
  const tokens = args.trim().split(/\s+/).filter(Boolean);
199
205
  const pathArg = tokens.find((token) => !token.startsWith("--"));
200
206
  const scope = tokens.includes("--user") ? "user" : "project";
201
- const result = await handleTeamTool({ action: "import", config: { path: pathArg, scope } }, ctx);
207
+ const result = await handleTeamTool({ action: "import", config: { path: pathArg, scope } }, teamCommandContext(ctx));
202
208
  await notifyCommandResult(ctx, commandText(result));
203
209
  } });
204
210
 
@@ -206,21 +212,21 @@ export function registerTeamCommands(pi: ExtensionAPI, deps: RegisterTeamCommand
206
212
  const tokens = args.trim().split(/\s+/).filter(Boolean);
207
213
  const keepToken = tokens.find((token) => token.startsWith("--keep="));
208
214
  const keep = keepToken ? Number.parseInt(keepToken.slice("--keep=".length), 10) : undefined;
209
- const result = await handleTeamTool({ action: "prune", keep, confirm: tokens.includes("--confirm") }, ctx);
215
+ const result = await handleTeamTool({ action: "prune", keep, confirm: tokens.includes("--confirm") }, teamCommandContext(ctx));
210
216
  await notifyCommandResult(ctx, commandText(result));
211
217
  } });
212
218
 
213
219
  pi.registerCommand("team-forget", { description: "Forget a pi-crew run by deleting its state and artifacts", handler: async (args: string, ctx: ExtensionCommandContext) => {
214
220
  const tokens = args.trim().split(/\s+/).filter(Boolean);
215
221
  const runId = tokens.find((token) => !token.startsWith("--"));
216
- const result = await handleTeamTool({ action: "forget", runId, force: tokens.includes("--force"), confirm: tokens.includes("--confirm") }, ctx);
222
+ const result = await handleTeamTool({ action: "forget", runId, force: tokens.includes("--force"), confirm: tokens.includes("--confirm") }, teamCommandContext(ctx));
217
223
  await notifyCommandResult(ctx, commandText(result));
218
224
  } });
219
225
 
220
226
  pi.registerCommand("team-settings", {
221
227
  description: "View or update pi-crew settings: [list|get <key>|set <key> <value>|unset <key>|path|scope]",
222
228
  handler: async (args: string, ctx: ExtensionCommandContext) => {
223
- const result = await handleTeamTool({ action: "settings", config: { args: args.trim() } }, ctx);
229
+ const result = await handleTeamTool({ action: "settings", config: { args: args.trim() } }, teamCommandContext(ctx));
224
230
  await notifyCommandResult(ctx, commandText(result));
225
231
  },
226
232
  });
@@ -233,18 +239,18 @@ export function registerTeamCommands(pi: ExtensionAPI, deps: RegisterTeamCommand
233
239
  const loaded = selected ? loadRunManifestById(ctx.cwd, selected.runId) : undefined;
234
240
  if (ctx.hasUI && loaded) {
235
241
  const agent = readCrewAgents(loaded.manifest).find((item) => item.taskId === selected?.taskId || item.id === selected?.taskId) ?? readCrewAgents(loaded.manifest)[0];
236
- const resultText = agent?.resultArtifactPath ? commandText(await handleTeamTool({ action: "api", runId: selected?.runId ?? "", config: { operation: "read-agent-output", agentId: agent.taskId, maxBytes: 64_000 } }, ctx)) : "(no result)";
242
+ const resultText = agent?.resultArtifactPath ? commandText(await handleTeamTool({ action: "api", runId: selected?.runId ?? "", config: { operation: "read-agent-output", agentId: agent.taskId, maxBytes: 64_000 } }, teamCommandContext(ctx))) : "(no result)";
237
243
  await ctx.ui.custom<undefined>((_tui, theme, _keybindings, done) => new DurableTextViewer("pi-crew result", `${selected?.runId ?? ""}:${agent?.taskId ?? "unknown"}`, resultText.split(/\r?\n/), theme, done), { overlay: true, overlayOptions: { width: "90%", maxHeight: "85%", anchor: "center" } });
238
244
  return;
239
245
  }
240
- const result = await handleTeamTool({ action: "api", runId, config: { operation: "read-agent-output", agentId: rawTaskId, maxBytes: 64_000 } }, ctx);
246
+ const result = await handleTeamTool({ action: "api", runId, config: { operation: "read-agent-output", agentId: rawTaskId, maxBytes: 64_000 } }, teamCommandContext(ctx));
241
247
  await notifyCommandResult(ctx, commandText(result));
242
248
  } });
243
249
 
244
250
  pi.registerCommand("team-transcript", { description: "Open a pi-crew transcript viewer: <runId> [taskId]", handler: async (args: string, ctx: ExtensionCommandContext) => {
245
251
  const [runId, taskId] = args.trim().split(/\s+/).filter(Boolean);
246
252
  if (await openTranscriptViewer(ctx, runId, taskId)) return;
247
- const result = await handleTeamTool({ action: "api", runId, config: { operation: "read-agent-transcript", agentId: taskId } }, ctx);
253
+ const result = await handleTeamTool({ action: "api", runId, config: { operation: "read-agent-transcript", agentId: taskId } }, teamCommandContext(ctx));
248
254
  await notifyCommandResult(ctx, commandText(result));
249
255
  } });
250
256
 
@@ -252,8 +258,8 @@ export function registerTeamCommands(pi: ExtensionAPI, deps: RegisterTeamCommand
252
258
  for (;;) {
253
259
  const runs = deps.getManifestCache(ctx.cwd).list(50);
254
260
  const uiConfig = loadConfig(ctx.cwd).config.ui;
255
- const rightPanel = uiConfig?.dashboardPlacement !== "center";
256
- const width = rightPanel ? Math.min(90, Math.max(40, uiConfig?.dashboardWidth ?? 56)) : "90%";
261
+ const rightPanel = (uiConfig?.dashboardPlacement ?? DEFAULT_UI.dashboardPlacement) === "right";
262
+ const width = rightPanel ? Math.min(90, Math.max(40, uiConfig?.dashboardWidth ?? DEFAULT_UI.dashboardWidth)) : "90%";
257
263
  const selection = await ctx.ui.custom<RunDashboardSelection | undefined>((_tui, theme, _keybindings, done) => new RunDashboard(runs, done, theme, { placement: rightPanel ? "right" : "center", showModel: uiConfig?.showModel, showTokens: uiConfig?.showTokens, showTools: uiConfig?.showTools, snapshotCache: deps.getRunSnapshotCache?.(ctx.cwd), runProvider: () => deps.getManifestCache(ctx.cwd).list(50), registry: deps.getMetricRegistry?.() }), { overlay: true, overlayOptions: rightPanel ? { width, minWidth: 40, maxHeight: "100%", anchor: "top-right", offsetX: 0, offsetY: 0, margin: { top: 0, right: 0, bottom: 0, left: 0 } } : { width, maxHeight: "90%", anchor: "center", margin: 2 } });
258
264
  if (!selection) return;
259
265
  if (selection.action === "reload") continue;
@@ -273,7 +279,7 @@ export function registerTeamCommands(pi: ExtensionAPI, deps: RegisterTeamCommand
273
279
  continue;
274
280
  }
275
281
  if (selection.action === "agent-transcript" && await openTranscriptViewer(ctx, selection.runId)) continue;
276
- const result = selection.action === "api" ? await handleTeamTool({ action: "api", runId: selection.runId, config: { operation: "read-manifest" } }, ctx) : selection.action === "agents" ? await handleTeamTool({ action: "api", runId: selection.runId, config: { operation: "agent-dashboard" } }, ctx) : selection.action === "mailbox" ? await handleTeamTool({ action: "api", runId: selection.runId, config: { operation: "read-mailbox" } }, ctx) : selection.action === "agent-events" ? await handleTeamTool({ action: "api", runId: selection.runId, config: { operation: "read-agent-events", limit: 50 } }, ctx) : selection.action === "agent-output" ? await handleTeamTool({ action: "api", runId: selection.runId, config: { operation: "read-agent-output", maxBytes: 32_000 } }, ctx) : selection.action === "agent-transcript" ? await handleTeamTool({ action: "api", runId: selection.runId, config: { operation: "read-agent-transcript" } }, ctx) : await handleTeamTool({ action: selection.action, runId: selection.runId }, ctx);
282
+ const result = selection.action === "api" ? await handleTeamTool({ action: "api", runId: selection.runId, config: { operation: "read-manifest" } }, teamCommandContext(ctx)) : selection.action === "agents" ? await handleTeamTool({ action: "api", runId: selection.runId, config: { operation: "agent-dashboard" } }, teamCommandContext(ctx)) : selection.action === "mailbox" ? await handleTeamTool({ action: "api", runId: selection.runId, config: { operation: "read-mailbox" } }, teamCommandContext(ctx)) : selection.action === "agent-events" ? await handleTeamTool({ action: "api", runId: selection.runId, config: { operation: "read-agent-events", limit: 50 } }, teamCommandContext(ctx)) : selection.action === "agent-output" ? await handleTeamTool({ action: "api", runId: selection.runId, config: { operation: "read-agent-output", maxBytes: 32_000 } }, teamCommandContext(ctx)) : selection.action === "agent-transcript" ? await handleTeamTool({ action: "api", runId: selection.runId, config: { operation: "read-agent-transcript" } }, teamCommandContext(ctx)) : await handleTeamTool({ action: selection.action, runId: selection.runId }, teamCommandContext(ctx));
277
283
  await notifyCommandResult(ctx, commandText(result));
278
284
  return;
279
285
  }
@@ -285,14 +291,15 @@ export function registerTeamCommands(pi: ExtensionAPI, deps: RegisterTeamCommand
285
291
  const uiConfig = loadConfig(ctx.cwd).config.ui;
286
292
  const styleArg = tokens.find((t) => t === "cat" || t === "armin");
287
293
  const effectArg = tokens.find((t) => ["random", "none", "typewriter", "scanline", "rain", "fade", "crt", "glitch", "dissolve"].includes(t));
288
- const style = (styleArg as "cat" | "armin" | undefined) ?? uiConfig?.mascotStyle ?? "cat";
289
- const effect = (effectArg as "random" | "none" | "typewriter" | "scanline" | "rain" | "fade" | "crt" | "glitch" | "dissolve" | undefined) ?? uiConfig?.mascotEffect ?? "random";
294
+ const style = (styleArg as "cat" | "armin" | undefined) ?? uiConfig?.mascotStyle ?? DEFAULT_UI.mascotStyle;
295
+ const effect = (effectArg as "random" | "none" | "typewriter" | "scanline" | "rain" | "fade" | "crt" | "glitch" | "dissolve" | undefined) ?? uiConfig?.mascotEffect ?? DEFAULT_UI.mascotEffect;
290
296
  await ctx.ui.custom<undefined>((tui, theme, _keybindings, done) => new AnimatedMascot(theme, () => done(undefined), { frameIntervalMs: style === "armin" ? 33 : 180, autoCloseMs: 7000, requestRender: () => requestRenderTarget(tui), style, effect }), { overlay: true, overlayOptions: { width: style === "armin" ? 48 : 62, maxHeight: "85%", anchor: "center" } });
291
297
  } });
292
298
 
293
- pi.registerCommand("team-init", { description: "Initialize project-local pi-crew directories and gitignore entries", handler: async (args: string, ctx: ExtensionCommandContext) => {
299
+ pi.registerCommand("team-init", { description: "Initialize pi-crew layout and global config. Use --project-config to write .pi/pi-crew.json.", handler: async (args: string, ctx: ExtensionCommandContext) => {
294
300
  const tokens = args.trim().split(/\s+/).filter(Boolean);
295
- const result = await handleTeamTool({ action: "init", config: { copyBuiltins: tokens.includes("--copy-builtins"), overwrite: tokens.includes("--overwrite") } }, ctx);
301
+ const configScope = tokens.includes("--project-config") || tokens.includes("--project") ? "project" : tokens.includes("--no-config") ? "none" : "global";
302
+ const result = await handleTeamTool({ action: "init", config: { copyBuiltins: tokens.includes("--copy-builtins"), overwrite: tokens.includes("--overwrite"), configScope } }, teamCommandContext(ctx));
296
303
  await notifyCommandResult(ctx, commandText(result));
297
304
  } });
298
305
 
@@ -300,14 +307,14 @@ export function registerTeamCommands(pi: ExtensionAPI, deps: RegisterTeamCommand
300
307
  const tokens = args.trim().split(/\s+/).filter(Boolean);
301
308
  const mode = tokens[0]?.toLowerCase();
302
309
  const config = mode === "on" ? { profile: "suggested", enabled: true, injectPolicy: true } : mode === "off" ? { profile: "manual", enabled: false } : mode === "manual" || mode === "suggested" || mode === "assisted" || mode === "aggressive" ? { profile: mode, enabled: mode !== "manual", injectPolicy: mode !== "manual" } : { preferAsyncForLongTasks: tokens.includes("--prefer-async") ? true : undefined, allowWorktreeSuggestion: tokens.includes("--no-worktree-suggest") ? false : undefined };
303
- const result = await handleTeamTool({ action: "autonomy", config }, ctx);
310
+ const result = await handleTeamTool({ action: "autonomy", config }, teamCommandContext(ctx));
304
311
  await notifyCommandResult(ctx, commandText(result));
305
312
  } });
306
313
 
307
314
  pi.registerCommand("team-config", { description: "Show or update pi-crew config. Use key=value [--project] to update.", handler: async (args: string, ctx: ExtensionCommandContext) => {
308
315
  const tokens = args.trim().split(/\s+/).filter(Boolean);
309
316
  if (tokens.length === 0) {
310
- const result = await handleTeamTool({ action: "config" }, ctx);
317
+ const result = await handleTeamTool({ action: "config" }, teamCommandContext(ctx));
311
318
  await notifyCommandResult(ctx, commandText(result));
312
319
  return;
313
320
  }
@@ -324,7 +331,7 @@ export function registerTeamCommands(pi: ExtensionAPI, deps: RegisterTeamCommand
324
331
  if (raw === "unset" || raw === "null") pushUnset(config, key);
325
332
  else setNestedConfig(config, key, parseScalar(raw));
326
333
  }
327
- const result = await handleTeamTool({ action: "config", config }, ctx);
334
+ const result = await handleTeamTool({ action: "config", config }, teamCommandContext(ctx));
328
335
  await notifyCommandResult(ctx, commandText(result));
329
336
  } });
330
337
 
@@ -332,7 +339,7 @@ export function registerTeamCommands(pi: ExtensionAPI, deps: RegisterTeamCommand
332
339
  ["team-validate", "validate", "Validate pi-crew agents, teams, and workflows"],
333
340
  ["team-doctor", "doctor", "Check pi-crew installation and discovery readiness"],
334
341
  ] as const) pi.registerCommand(name, { description, handler: async (_args: string, ctx: ExtensionCommandContext) => {
335
- const result = await handleTeamTool({ action }, ctx);
342
+ const result = await handleTeamTool({ action }, teamCommandContext(ctx));
336
343
  await notifyCommandResult(ctx, commandText(result));
337
344
  } });
338
345
 
@@ -1,125 +1,125 @@
1
- import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
2
- import { listRecentRuns } from "../run-index.ts";
3
- import type { ArtifactDescriptor, TeamRunManifest } from "../../state/types.ts";
4
-
5
- export interface RegisterCompactionGuardOptions {
6
- foregroundControllers: Set<AbortController>;
7
- }
8
-
9
- const TRIGGER_RATIO = 0.75;
10
- const HARD_RATIO = 0.95;
11
- const DEFAULT_CONTEXT_WINDOW = 200_000;
12
- const MAX_ARTIFACT_INDEX_RUNS = 10;
13
- const MAX_ARTIFACT_INDEX_ITEMS = 80;
14
-
15
- function contextWindow(ctx: { model?: { contextWindow?: number } }): number {
16
- const value = ctx.model?.contextWindow;
17
- return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : DEFAULT_CONTEXT_WINDOW;
18
- }
19
-
20
- function usageRatio(ctx: { getContextUsage(): { tokens: number | null } | undefined; model?: { contextWindow?: number } }): number | undefined {
21
- const tokens = ctx.getContextUsage()?.tokens;
22
- if (tokens === null || tokens === undefined || !Number.isFinite(tokens)) return undefined;
23
- return tokens / contextWindow(ctx);
24
- }
25
-
26
- interface CrewArtifactIndexEntry {
27
- runId: string;
28
- status: TeamRunManifest["status"];
29
- team: string;
30
- workflow?: string;
31
- goal: string;
32
- artifact: Pick<ArtifactDescriptor, "kind" | "path" | "producer" | "sizeBytes" | "createdAt">;
33
- }
34
-
35
- function collectCrewArtifactIndex(cwd: string): CrewArtifactIndexEntry[] {
36
- const entries: CrewArtifactIndexEntry[] = [];
37
- for (const run of listRecentRuns(cwd, MAX_ARTIFACT_INDEX_RUNS)) {
38
- for (const artifact of run.artifacts) {
39
- entries.push({
40
- runId: run.runId,
41
- status: run.status,
42
- team: run.team,
43
- workflow: run.workflow,
44
- goal: run.goal,
45
- artifact: {
46
- kind: artifact.kind,
47
- path: artifact.path,
48
- producer: artifact.producer,
49
- sizeBytes: artifact.sizeBytes,
50
- createdAt: artifact.createdAt,
51
- },
52
- });
53
- if (entries.length >= MAX_ARTIFACT_INDEX_ITEMS) return entries;
54
- }
55
- }
56
- return entries;
57
- }
58
-
59
- function formatCrewArtifactIndex(entries: CrewArtifactIndexEntry[]): string {
60
- if (!entries.length) return "";
61
- const lines = ["", "# pi-crew artifact index", "Preserve these run artifact references in the compaction summary:"];
62
- for (const entry of entries) {
63
- lines.push(`- ${entry.artifact.kind}: ${entry.artifact.path} (run=${entry.runId}, status=${entry.status}, team=${entry.team}, workflow=${entry.workflow ?? "none"}, producer=${entry.artifact.producer})`);
64
- }
65
- return lines.join("\n");
66
- }
67
-
68
- export function registerCompactionGuard(pi: ExtensionAPI, options: RegisterCompactionGuardOptions): void {
69
- let pendingCompactReason: string | null = null;
70
- let compactionInProgress = false;
71
-
72
- const startCompact = (ctx: ExtensionContext, reason: string): void => {
73
- if (compactionInProgress) return;
74
- compactionInProgress = true;
75
- const artifactIndex = collectCrewArtifactIndex(ctx.cwd);
76
- if (artifactIndex.length > 0) {
77
- pi.appendEntry("crew:artifact-index", {
78
- reason,
79
- createdAt: new Date().toISOString(),
80
- artifacts: artifactIndex,
81
- });
82
- }
83
- ctx.compact({
84
- customInstructions: `Prioritize keeping pi-crew run state, task results, artifact references, run IDs, and next actions. Keep completed-task detail concise.${formatCrewArtifactIndex(artifactIndex)}`,
85
- onComplete: () => {
86
- compactionInProgress = false;
87
- ctx.ui.notify(reason === "deferred" ? "Deferred compaction completed" : "Auto-compacted context during team run", "info");
88
- },
89
- onError: (error) => {
90
- compactionInProgress = false;
91
- ctx.ui.notify(`${reason === "deferred" ? "Deferred" : "Auto"} compaction failed: ${error.message}`, "error");
92
- },
93
- });
94
- };
95
-
96
- // Phase 1.2: Defer compaction during foreground runs unless context is critically full.
97
- pi.on("session_before_compact", async (_event, ctx) => {
98
- if (options.foregroundControllers.size === 0) return;
99
- const ratio = usageRatio(ctx);
100
- if (ratio !== undefined && ratio >= HARD_RATIO) {
101
- ctx.ui.notify("Compaction allowed despite foreground run: context is critically full", "warning");
102
- return;
103
- }
104
- pendingCompactReason = "deferred-during-foreground-run";
105
- ctx.ui.notify("Compaction deferred: foreground team run in progress", "info");
106
- return { cancel: true };
107
- });
108
-
109
- // Phase 2.1: Proactive compaction with dynamic threshold based on model context window.
110
- pi.on("turn_end", (_event, ctx) => {
111
- if (compactionInProgress) return;
112
- if (options.foregroundControllers.size === 0 && pendingCompactReason) {
113
- pendingCompactReason = null;
114
- startCompact(ctx, "deferred");
115
- return;
116
- }
117
- const ratio = usageRatio(ctx);
118
- if (ratio === undefined || ratio < TRIGGER_RATIO) return;
119
- if (options.foregroundControllers.size > 0) {
120
- pendingCompactReason = "threshold-during-foreground-run";
121
- return;
122
- }
123
- startCompact(ctx, "threshold");
124
- });
125
- }
1
+ import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
2
+ import { listRecentRuns } from "../run-index.ts";
3
+ import type { ArtifactDescriptor, TeamRunManifest } from "../../state/types.ts";
4
+
5
+ export interface RegisterCompactionGuardOptions {
6
+ foregroundControllers: Set<AbortController>;
7
+ }
8
+
9
+ const TRIGGER_RATIO = 0.75;
10
+ const HARD_RATIO = 0.95;
11
+ const DEFAULT_CONTEXT_WINDOW = 200_000;
12
+ const MAX_ARTIFACT_INDEX_RUNS = 10;
13
+ const MAX_ARTIFACT_INDEX_ITEMS = 80;
14
+
15
+ function contextWindow(ctx: { model?: { contextWindow?: number } }): number {
16
+ const value = ctx.model?.contextWindow;
17
+ return typeof value === "number" && Number.isFinite(value) && value > 0 ? value : DEFAULT_CONTEXT_WINDOW;
18
+ }
19
+
20
+ function usageRatio(ctx: { getContextUsage(): { tokens: number | null } | undefined; model?: { contextWindow?: number } }): number | undefined {
21
+ const tokens = ctx.getContextUsage()?.tokens;
22
+ if (tokens === null || tokens === undefined || !Number.isFinite(tokens)) return undefined;
23
+ return tokens / contextWindow(ctx);
24
+ }
25
+
26
+ interface CrewArtifactIndexEntry {
27
+ runId: string;
28
+ status: TeamRunManifest["status"];
29
+ team: string;
30
+ workflow?: string;
31
+ goal: string;
32
+ artifact: Pick<ArtifactDescriptor, "kind" | "path" | "producer" | "sizeBytes" | "createdAt">;
33
+ }
34
+
35
+ function collectCrewArtifactIndex(cwd: string): CrewArtifactIndexEntry[] {
36
+ const entries: CrewArtifactIndexEntry[] = [];
37
+ for (const run of listRecentRuns(cwd, MAX_ARTIFACT_INDEX_RUNS)) {
38
+ for (const artifact of run.artifacts) {
39
+ entries.push({
40
+ runId: run.runId,
41
+ status: run.status,
42
+ team: run.team,
43
+ workflow: run.workflow,
44
+ goal: run.goal,
45
+ artifact: {
46
+ kind: artifact.kind,
47
+ path: artifact.path,
48
+ producer: artifact.producer,
49
+ sizeBytes: artifact.sizeBytes,
50
+ createdAt: artifact.createdAt,
51
+ },
52
+ });
53
+ if (entries.length >= MAX_ARTIFACT_INDEX_ITEMS) return entries;
54
+ }
55
+ }
56
+ return entries;
57
+ }
58
+
59
+ function formatCrewArtifactIndex(entries: CrewArtifactIndexEntry[]): string {
60
+ if (!entries.length) return "";
61
+ const lines = ["", "# pi-crew artifact index", "Preserve these run artifact references in the compaction summary:"];
62
+ for (const entry of entries) {
63
+ lines.push(`- ${entry.artifact.kind}: ${entry.artifact.path} (run=${entry.runId}, status=${entry.status}, team=${entry.team}, workflow=${entry.workflow ?? "none"}, producer=${entry.artifact.producer})`);
64
+ }
65
+ return lines.join("\n");
66
+ }
67
+
68
+ export function registerCompactionGuard(pi: ExtensionAPI, options: RegisterCompactionGuardOptions): void {
69
+ let pendingCompactReason: string | null = null;
70
+ let compactionInProgress = false;
71
+
72
+ const startCompact = (ctx: ExtensionContext, reason: string): void => {
73
+ if (compactionInProgress) return;
74
+ compactionInProgress = true;
75
+ const artifactIndex = collectCrewArtifactIndex(ctx.cwd);
76
+ if (artifactIndex.length > 0) {
77
+ pi.appendEntry("crew:artifact-index", {
78
+ reason,
79
+ createdAt: new Date().toISOString(),
80
+ artifacts: artifactIndex,
81
+ });
82
+ }
83
+ ctx.compact({
84
+ customInstructions: `Prioritize keeping pi-crew run state, task results, artifact references, run IDs, and next actions. Keep completed-task detail concise.${formatCrewArtifactIndex(artifactIndex)}`,
85
+ onComplete: () => {
86
+ compactionInProgress = false;
87
+ ctx.ui.notify(reason === "deferred" ? "Deferred compaction completed" : "Auto-compacted context during team run", "info");
88
+ },
89
+ onError: (error) => {
90
+ compactionInProgress = false;
91
+ ctx.ui.notify(`${reason === "deferred" ? "Deferred" : "Auto"} compaction failed: ${error.message}`, "error");
92
+ },
93
+ });
94
+ };
95
+
96
+ // Phase 1.2: Defer compaction during foreground runs unless context is critically full.
97
+ pi.on("session_before_compact", async (_event, ctx) => {
98
+ if (options.foregroundControllers.size === 0) return;
99
+ const ratio = usageRatio(ctx);
100
+ if (ratio !== undefined && ratio >= HARD_RATIO) {
101
+ ctx.ui.notify("Compaction allowed despite foreground run: context is critically full", "warning");
102
+ return;
103
+ }
104
+ pendingCompactReason = "deferred-during-foreground-run";
105
+ ctx.ui.notify("Compaction deferred: foreground team run in progress", "info");
106
+ return { cancel: true };
107
+ });
108
+
109
+ // Phase 2.1: Proactive compaction with dynamic threshold based on model context window.
110
+ pi.on("turn_end", (_event, ctx) => {
111
+ if (compactionInProgress) return;
112
+ if (options.foregroundControllers.size === 0 && pendingCompactReason) {
113
+ pendingCompactReason = null;
114
+ startCompact(ctx, "deferred");
115
+ return;
116
+ }
117
+ const ratio = usageRatio(ctx);
118
+ if (ratio === undefined || ratio < TRIGGER_RATIO) return;
119
+ if (options.foregroundControllers.size > 0) {
120
+ pendingCompactReason = "threshold-during-foreground-run";
121
+ return;
122
+ }
123
+ startCompact(ctx, "threshold");
124
+ });
125
+ }