pi-crew 0.9.68 → 0.10.2

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 (253) hide show
  1. package/CHANGELOG.md +222 -0
  2. package/NOTICE.md +21 -0
  3. package/README.md +44 -2
  4. package/agents/analyst.md +1 -1
  5. package/agents/cold-verifier.md +3 -1
  6. package/agents/critic.md +1 -1
  7. package/agents/executor.md +1 -1
  8. package/agents/explorer.md +1 -1
  9. package/agents/planner.md +1 -1
  10. package/agents/reviewer.md +1 -1
  11. package/agents/security-reviewer.md +1 -1
  12. package/agents/test-engineer.md +1 -1
  13. package/agents/verifier.md +1 -1
  14. package/agents/writer.md +1 -1
  15. package/dist/index.mjs +68113 -60774
  16. package/docs/README.md +2 -0
  17. package/docs/actions-reference.md +31 -0
  18. package/docs/commands-reference.md +17 -6
  19. package/docs/resource-formats.md +13 -0
  20. package/package.json +4 -2
  21. package/schema.json +503 -91
  22. package/scripts/resource-sampler.mjs +36 -2
  23. package/skills/requirements-to-task-packet/SKILL.md +26 -0
  24. package/skills/widget-rendering/SKILL.md +7 -7
  25. package/src/agents/agent-config.ts +2 -1
  26. package/src/agents/discover-agents.ts +23 -14
  27. package/src/config/config-merge.ts +183 -0
  28. package/src/config/config-validation.ts +687 -0
  29. package/src/config/config.ts +22 -864
  30. package/src/config/defaults.ts +43 -2
  31. package/src/config/drift-detector.ts +1 -1
  32. package/src/config/env-vars.ts +691 -0
  33. package/src/config/role-tools.ts +11 -9
  34. package/src/config/sanitize-project-config.ts +172 -0
  35. package/src/config/types.ts +49 -1
  36. package/src/extension/async-notifier.ts +25 -2
  37. package/src/extension/crew-cleanup.ts +13 -0
  38. package/src/extension/crew-vibes/config.ts +2 -1
  39. package/src/extension/crew-vibes/footer.ts +19 -0
  40. package/src/extension/crew-vibes/index.ts +11 -1
  41. package/src/extension/plan-orchestrate.ts +132 -0
  42. package/src/extension/register.ts +8 -0
  43. package/src/extension/registration/command-registration.ts +1 -0
  44. package/src/extension/registration/commands/dashboard.ts +158 -0
  45. package/src/extension/registration/commands/index.ts +35 -0
  46. package/src/extension/registration/commands/manage.ts +303 -0
  47. package/src/extension/registration/commands/run.ts +228 -0
  48. package/src/extension/registration/commands/shared.ts +639 -0
  49. package/src/extension/registration/commands/status.ts +60 -0
  50. package/src/extension/registration/commands.ts +13 -1224
  51. package/src/extension/registration/foreground-run-controller.ts +10 -2
  52. package/src/extension/registration/lifecycle-handlers.ts +178 -17
  53. package/src/extension/registration/runtime-cleanup.ts +23 -5
  54. package/src/extension/registration/subagent-tools.ts +218 -9
  55. package/src/extension/registration/team-tool.ts +5 -1
  56. package/src/extension/registration/ui.ts +5 -4
  57. package/src/extension/rpc-hmac.ts +5 -3
  58. package/src/extension/team-tool/api/heartbeat.ts +47 -10
  59. package/src/extension/team-tool/api/plan-approval.ts +9 -0
  60. package/src/extension/team-tool/api/task-claims.ts +109 -40
  61. package/src/extension/team-tool/cancel.ts +84 -50
  62. package/src/extension/team-tool/dispatch/index.ts +1 -0
  63. package/src/extension/team-tool/dispatch/run.ts +4 -1
  64. package/src/extension/team-tool/doctor.ts +103 -1
  65. package/src/extension/team-tool/orchestrate.ts +66 -1
  66. package/src/extension/team-tool/plans.ts +192 -0
  67. package/src/extension/team-tool/respond.ts +197 -65
  68. package/src/extension/team-tool/run-deadline.ts +35 -3
  69. package/src/extension/team-tool/run-intent.ts +63 -0
  70. package/src/extension/team-tool/run.ts +74 -20
  71. package/src/extension/team-tool/status.ts +84 -26
  72. package/src/extension/team-tool.ts +11 -2
  73. package/src/hooks/registry.ts +1 -6
  74. package/src/i18n.ts +9 -0
  75. package/src/prompt/prompt-runtime.ts +521 -2
  76. package/src/prompt/worker-events-channel.ts +173 -0
  77. package/src/runtime/README.md +8 -8
  78. package/src/runtime/async-runner.ts +7 -3
  79. package/src/runtime/background-runner.ts +42 -14
  80. package/src/runtime/broker/broker-issuer.ts +9 -2
  81. package/src/runtime/broker/crew-broker-tokens.ts +43 -6
  82. package/src/runtime/broker/crew-broker.ts +838 -10
  83. package/src/runtime/broker/wait-status-cache.ts +157 -0
  84. package/src/runtime/budget-enforcement.ts +281 -0
  85. package/src/runtime/child-pi/child-pi-constants.ts +8 -0
  86. package/src/runtime/child-pi/child-pi-spawn.ts +60 -14
  87. package/src/runtime/child-pi/child-pi-streams.ts +21 -1
  88. package/src/runtime/child-pi/child-pi-timers.ts +324 -0
  89. package/src/runtime/child-pi/child-pi.ts +97 -201
  90. package/src/runtime/child-pi/mock-fixtures.ts +16 -2
  91. package/src/runtime/crew-agent-records.ts +259 -14
  92. package/src/runtime/delegate-spawn.ts +148 -0
  93. package/src/runtime/detached-run-results.ts +90 -0
  94. package/src/runtime/deterministic-ast.ts +2 -1
  95. package/src/runtime/dispatch-batch.ts +945 -0
  96. package/src/runtime/finalize-run.ts +557 -0
  97. package/src/runtime/goal-workflow/adaptive-plan.ts +116 -15
  98. package/src/runtime/goal-workflow/dynamic-workflow-runner.ts +8 -3
  99. package/src/runtime/goal-workflow/goal-state-store.ts +1 -1
  100. package/src/runtime/group-join.ts +11 -125
  101. package/src/runtime/live-session/live-session-runtime.ts +26 -1
  102. package/src/runtime/merge-gate.ts +32 -10
  103. package/src/runtime/merge-loop.ts +130 -0
  104. package/src/runtime/model/model-budget-summary.ts +53 -0
  105. package/src/runtime/model/model-fallback.ts +36 -2
  106. package/src/runtime/model/pi-args.ts +10 -0
  107. package/src/runtime/model/provider-extensions.ts +10 -0
  108. package/src/runtime/orphan-worker-registry.ts +1 -1
  109. package/src/runtime/output/output-validator.ts +45 -0
  110. package/src/runtime/parent-guard.ts +3 -1
  111. package/src/runtime/peer-dep.ts +2 -1
  112. package/src/runtime/per-write-validator.ts +0 -5
  113. package/src/runtime/pi-spawn.ts +61 -15
  114. package/src/runtime/plan-approval.ts +125 -0
  115. package/src/runtime/plan-replan.ts +151 -0
  116. package/src/runtime/process-status.ts +16 -1
  117. package/src/runtime/recovery/checkpoint.ts +0 -18
  118. package/src/runtime/recovery/crash-recovery.ts +111 -46
  119. package/src/runtime/run-tracker.ts +77 -10
  120. package/src/runtime/scheduler-context.ts +98 -0
  121. package/src/runtime/scheduling/coalesce-tasks.ts +5 -0
  122. package/src/runtime/scheduling/global-worker-cap.ts +2 -1
  123. package/src/runtime/scheduling/nested-slots.ts +70 -0
  124. package/src/runtime/scheduling/run-coalesced-task-group.ts +64 -13
  125. package/src/runtime/scheduling/task-graph-scheduler.ts +0 -10
  126. package/src/runtime/settings-store.ts +219 -0
  127. package/src/runtime/spawn-policy.ts +217 -0
  128. package/src/runtime/stale-reconciler.ts +87 -6
  129. package/src/runtime/subagent-manager.ts +25 -1
  130. package/src/runtime/task-output-context.ts +230 -9
  131. package/src/runtime/task-packet.ts +23 -1
  132. package/src/runtime/task-runner/child-executor.ts +106 -7
  133. package/src/runtime/task-runner/post-execution.ts +125 -1
  134. package/src/runtime/task-runner/pre-execution.ts +39 -1
  135. package/src/runtime/task-runner/prompt-builder.ts +51 -1
  136. package/src/runtime/task-runner/retrieval-orchestrator.ts +72 -18
  137. package/src/runtime/task-runner/spec-evidence.ts +403 -0
  138. package/src/runtime/task-runner/state-helpers.ts +26 -24
  139. package/src/runtime/task-runner.ts +11 -0
  140. package/src/runtime/team-runner.ts +132 -1673
  141. package/src/runtime/verification/spec-sandbox.ts +255 -0
  142. package/src/runtime/verification/verification-gates.ts +3 -2
  143. package/src/runtime/verification/verification-worktree.ts +2 -1
  144. package/src/runtime/workflow-phase-advance.ts +100 -0
  145. package/src/runtime/workspace-tree.ts +9 -0
  146. package/src/schema/config-schema.ts +66 -26
  147. package/src/schema/sensitive-config-paths.ts +64 -0
  148. package/src/schema/team-tool-schema.ts +13 -3
  149. package/src/state/README.md +4 -10
  150. package/src/state/atomic-write.ts +20 -3
  151. package/src/state/contracts.ts +38 -0
  152. package/src/state/coordination/mailbox.ts +12 -2
  153. package/src/state/event-log/cursor.ts +223 -0
  154. package/src/state/event-log/event-log-rotation.ts +12 -4
  155. package/src/state/event-log/event-log.ts +152 -369
  156. package/src/state/event-log/sequence-cache.ts +373 -0
  157. package/src/state/event-log/worker-atomic-writer.ts +2 -1
  158. package/src/state/stores/active-run-registry.ts +3 -2
  159. package/src/state/stores/manifest-io.ts +237 -0
  160. package/src/state/stores/ownership-map.ts +162 -0
  161. package/src/state/stores/plan-store.ts +241 -0
  162. package/src/state/stores/run-cache.ts +0 -90
  163. package/src/state/stores/spec-store.ts +189 -0
  164. package/src/state/stores/state-store.ts +139 -232
  165. package/src/state/types.ts +199 -0
  166. package/src/ui/dashboard-panes/plan-pane.ts +136 -0
  167. package/src/ui/dashboard-panes/progress-pane.ts +6 -0
  168. package/src/ui/dashboard-panes/transcript-pane.ts +31 -0
  169. package/src/ui/dock-footer.ts +49 -0
  170. package/src/ui/heartbeat-aggregator.ts +9 -1
  171. package/src/ui/inline-panel/agent-pane.ts +375 -0
  172. package/src/ui/inline-panel/agent-transcript.ts +338 -0
  173. package/src/ui/inline-panel/agent-view-overlay.ts +225 -0
  174. package/src/ui/inline-panel/crew-editor.ts +192 -0
  175. package/src/ui/inline-panel/index.ts +290 -0
  176. package/src/ui/inline-panel/panel-rows.ts +37 -0
  177. package/src/ui/inline-panel/panel-selection.ts +157 -0
  178. package/src/ui/inline-panel/panel-store.ts +111 -0
  179. package/src/ui/inline-panel/view-session-store.ts +36 -0
  180. package/src/ui/keybinding-map.ts +54 -13
  181. package/src/ui/pi-ui-compat.ts +9 -0
  182. package/src/ui/powerbar-publisher.ts +52 -1
  183. package/src/ui/run-dashboard.ts +31 -5
  184. package/src/ui/run-snapshot-cache.ts +57 -30
  185. package/src/ui/snapshot-types.ts +6 -1
  186. package/src/ui/widget/index.ts +176 -22
  187. package/src/ui/widget/task-list.ts +198 -0
  188. package/src/ui/widget/widget-formatters.ts +240 -4
  189. package/src/ui/widget/widget-renderer.ts +243 -38
  190. package/src/ui/widget/widget-types.ts +11 -0
  191. package/src/utils/child-process-shield.ts +106 -0
  192. package/src/utils/file-coalescer.ts +0 -4
  193. package/src/utils/fs-errno.ts +66 -0
  194. package/src/utils/fs-watch.ts +1 -1
  195. package/src/utils/internal-error.ts +3 -1
  196. package/src/utils/paths.ts +11 -3
  197. package/src/utils/redaction.ts +7 -0
  198. package/src/utils/safe-abort.ts +45 -0
  199. package/src/utils/task-name-generator.ts +1 -8
  200. package/src/workflows/discover-workflows.ts +20 -2
  201. package/src/workflows/validate-workflow.ts +7 -1
  202. package/src/workflows/workflow-config.ts +17 -0
  203. package/src/workflows/workflow-serializer.ts +3 -0
  204. package/src/worktree/worktree-manager.ts +22 -0
  205. package/workflows/default.workflow.md +36 -26
  206. package/workflows/strict-fast-fix.workflow.md +26 -0
  207. package/src/agents/agent-search.ts +0 -98
  208. package/src/benchmark/benchmark-runner.ts +0 -313
  209. package/src/benchmark/feedback-loop.ts +0 -73
  210. package/src/config/resilient-parser.ts +0 -117
  211. package/src/extension/crew-vibes/cat-frames.ts +0 -18
  212. package/src/extension/result-watcher.ts +0 -139
  213. package/src/observability/exporters/prometheus-exporter.ts +0 -54
  214. package/src/observability/metric-retention.ts +0 -64
  215. package/src/runtime/compaction/compaction-summary.ts +0 -278
  216. package/src/runtime/errors/crew-errors.ts +0 -162
  217. package/src/runtime/live-session/intercom-bridge.ts +0 -187
  218. package/src/runtime/loop-gates.ts +0 -128
  219. package/src/runtime/metric-parser.ts +0 -36
  220. package/src/runtime/output/stream-preview.ts +0 -184
  221. package/src/runtime/output/tool-progress.ts +0 -278
  222. package/src/runtime/phase-tracker.ts +0 -385
  223. package/src/runtime/pipeline-runner.ts +0 -523
  224. package/src/runtime/process/process-lifecycle.ts +0 -491
  225. package/src/runtime/recovery/retry-runner.ts +0 -330
  226. package/src/runtime/run-drift.ts +0 -219
  227. package/src/runtime/task-quality.ts +0 -199
  228. package/src/runtime/task-runner/run-projection.ts +0 -128
  229. package/src/runtime/verification/post-checks.ts +0 -142
  230. package/src/state/coordination/schedule.ts +0 -166
  231. package/src/state/event-log/jsonl-writer.ts +0 -115
  232. package/src/state/hook-instinct-bridge.ts +0 -94
  233. package/src/state/hook-integrations.ts +0 -51
  234. package/src/state/session-state-map.ts +0 -51
  235. package/src/state/stores/blob-store.ts +0 -308
  236. package/src/state/stores/instinct-store.ts +0 -275
  237. package/src/state/stores/observation-store.ts +0 -176
  238. package/src/state/tiered-eval.ts +0 -480
  239. package/src/state/types-eval.ts +0 -58
  240. package/src/tools/safe-bash-extension.ts +0 -54
  241. package/src/tools/safe-bash.ts +0 -505
  242. package/src/ui/agent-management-overlay.ts +0 -160
  243. package/src/ui/crew-footer.ts +0 -102
  244. package/src/ui/crew-select-list.ts +0 -114
  245. package/src/ui/dashboard-panes/capability-pane.ts +0 -77
  246. package/src/ui/transcript-entries.ts +0 -256
  247. package/src/utils/conflict-detect.ts +0 -721
  248. package/src/utils/fingerprint.ts +0 -180
  249. package/src/utils/gh-protocol.ts +0 -556
  250. package/src/utils/project-detector.ts +0 -160
  251. package/src/utils/sse-parser.ts +0 -131
  252. package/src/workflows/cost-estimator.ts +0 -34
  253. package/src/workflows/intermediate-store.ts +0 -166
@@ -0,0 +1,111 @@
1
+ /**
2
+ * panel-store.ts — process-wide state shared by the inline panel's two halves.
3
+ *
4
+ * The editor wrapper mutates the cursor; the widget renderer and the transcript
5
+ * pane read it. Neither can own the state (the editor is created by pi's
6
+ * factory, the widget by another), so it lives here as a small observable
7
+ * singleton — the same shape pi-subtask keeps in its closure.
8
+ *
9
+ * Row projection is injected rather than computed here: the widget already owns
10
+ * the manifest/snapshot caches that produce the run list, and duplicating that
11
+ * read on every keypress would put disk I/O on the input path.
12
+ */
13
+
14
+ import type { PanelRow, PanelSelection, PanelTarget } from "./panel-selection.ts";
15
+ import { isAgentSelection } from "./panel-selection.ts";
16
+
17
+ let selection: PanelSelection = null;
18
+ let viewed: PanelTarget | undefined;
19
+ let rowsProvider: (() => PanelRow[]) | undefined;
20
+ const listeners = new Set<() => void>();
21
+
22
+ /** Install the row projection. Called once during panel wiring. */
23
+ export function setPanelRowsProvider(provider: (() => PanelRow[]) | undefined): void {
24
+ rowsProvider = provider;
25
+ }
26
+
27
+ /**
28
+ * Navigable rows, freshly projected on each call.
29
+ *
30
+ * A provider throw must never break input handling — a transient manifest read
31
+ * failure would otherwise make the panel swallow keys — so it degrades to an
32
+ * empty list, which the editor treats as "nothing to navigate".
33
+ */
34
+ export function panelRows(): PanelRow[] {
35
+ if (!rowsProvider) return [];
36
+ try {
37
+ return rowsProvider();
38
+ } catch {
39
+ return [];
40
+ }
41
+ }
42
+
43
+ export function getPanelSelection(): PanelSelection {
44
+ return selection;
45
+ }
46
+
47
+ export function setPanelSelection(next: PanelSelection): void {
48
+ if (next === selection) return;
49
+ if (isAgentSelection(selection) && isAgentSelection(next) && selection.runId === next.runId && selection.taskId === next.taskId) {
50
+ return;
51
+ }
52
+ selection = next;
53
+ notifyPanelChange();
54
+ }
55
+
56
+ export function getViewedAgent(): PanelTarget | undefined {
57
+ return viewed;
58
+ }
59
+
60
+ export function setViewedAgent(next: PanelTarget | undefined): void {
61
+ if (viewed === next) return;
62
+ if (viewed && next && viewed.runId === next.runId && viewed.taskId === next.taskId) return;
63
+ viewed = next;
64
+ notifyPanelChange();
65
+ }
66
+
67
+ /** True while the panel holds the cursor, i.e. keys are not going to the editor. */
68
+ export function isPanelFocused(): boolean {
69
+ return selection !== null;
70
+ }
71
+
72
+ /** What the widget renderer needs, in one read. */
73
+ export function panelDisplayState(): {
74
+ selectedTaskId: string | undefined;
75
+ viewedTaskId: string | undefined;
76
+ focused: boolean;
77
+ } {
78
+ return {
79
+ selectedTaskId: isAgentSelection(selection) ? selection.taskId : undefined,
80
+ viewedTaskId: viewed?.taskId,
81
+ // Cursor-driven: uncapping the row budget while the user is typing
82
+ // into the pane (viewed set, selection null) would jerk the layout on
83
+ // every pane open. The cursor entry is when the full list is needed.
84
+ focused: selection !== null,
85
+ };
86
+ }
87
+
88
+ export function subscribePanelChange(listener: () => void): () => void {
89
+ listeners.add(listener);
90
+ return () => {
91
+ listeners.delete(listener);
92
+ };
93
+ }
94
+
95
+ export function notifyPanelChange(): void {
96
+ for (const listener of [...listeners]) {
97
+ try {
98
+ listener();
99
+ } catch {
100
+ // A failing repaint listener must not stop the others.
101
+ }
102
+ }
103
+ }
104
+
105
+ /** Session teardown / test isolation. */
106
+ export function resetPanelStore(): void {
107
+ selection = null;
108
+ viewed = undefined;
109
+ rowsProvider = undefined;
110
+ listeners.clear();
111
+ }
@@ -0,0 +1,36 @@
1
+ /**
2
+ * view-session-store.ts — session-switch guard for run protection.
3
+ *
4
+ * Agent views are in-document panes (see agent-pane.ts / inline-panel's
5
+ * openPane): viewing an agent NEVER switches, resumes, or tears down a
6
+ * session. This store is the one piece of session-switch machinery that
7
+ * remains, and it protects runs from switches the USER makes (/resume, /new,
8
+ * /fork): a switch tears the current session down via session.abort(), which
9
+ * fires the abort signal of any tool call still in flight — a foreground
10
+ * team run created by that tool is linked to the turn's abort, so ANY switch
11
+ * seconds after a run starts used to kill the run's workers ("Child Pi
12
+ * exited with 143" → run cancelled). While the guard is set, the
13
+ * caller-abort propagation is suppressed (run-deadline.ts).
14
+ *
15
+ * Set by the session_before_switch handler (runs before teardown's abort),
16
+ * cleared on the next session_start (the switch landed) and on reconcile.
17
+ */
18
+
19
+ let sessionSwitchInFlight = false;
20
+
21
+ export function markSessionSwitchInFlight(): void {
22
+ sessionSwitchInFlight = true;
23
+ }
24
+
25
+ export function clearSessionSwitchInFlight(): void {
26
+ sessionSwitchInFlight = false;
27
+ }
28
+
29
+ export function isSessionSwitchInFlight(): boolean {
30
+ return sessionSwitchInFlight;
31
+ }
32
+
33
+ /** Test isolation. */
34
+ export function resetCrewViewSessionState(): void {
35
+ sessionSwitchInFlight = false;
36
+ }
@@ -26,6 +26,7 @@
26
26
  import * as fs from "node:fs";
27
27
  import * as path from "node:path";
28
28
  import { type KeyId, matchesKey } from "@earendil-works/pi-tui";
29
+ import { getCrewEnv } from "../config/env-vars.ts";
29
30
  import { keyOf } from "./key-utils.ts";
30
31
 
31
32
  export const DASHBOARD_KEYS = {
@@ -52,6 +53,7 @@ export const DASHBOARD_KEYS = {
52
53
  output: ["4"],
53
54
  health: ["5"],
54
55
  metrics: ["6"],
56
+ plan: ["7"],
55
57
  },
56
58
  navigation: { up: ["k", "up"], down: ["j", "down"] },
57
59
  mailbox: {
@@ -63,6 +65,7 @@ export const DASHBOARD_KEYS = {
63
65
  openDetail: ["\r", "\n"],
64
66
  },
65
67
  health: { recovery: ["R"], killStale: ["K"], diagnosticExport: ["D"] },
68
+ plan: { approve: ["A"], deny: ["n"], diff: ["X"] },
66
69
  notification: { dismissAll: ["H"] },
67
70
  } as const;
68
71
 
@@ -70,7 +73,7 @@ export const DASHBOARD_KEYS = {
70
73
  * Pane identifiers that can scope a binding. `undefined` means the binding
71
74
  * fires in every pane.
72
75
  */
73
- export type ActivePane = "agents" | "progress" | "mailbox" | "output" | "health" | "metrics";
76
+ export type ActivePane = "agents" | "progress" | "mailbox" | "output" | "health" | "metrics" | "plan";
74
77
 
75
78
  /**
76
79
  * A single keybinding: the keys that trigger it, the action it produces, and
@@ -81,8 +84,8 @@ export type ActivePane = "agents" | "progress" | "mailbox" | "output" | "health"
81
84
  export interface KeyBinding {
82
85
  readonly keys: readonly string[];
83
86
  readonly action: DashboardKeyAction;
84
- /** When set, the binding only fires when `activePane === pane`. */
85
- readonly pane?: ActivePane;
87
+ /** When set, the binding fires only in these pane(s). */
88
+ readonly pane?: PaneScope;
86
89
  }
87
90
 
88
91
  export type DashboardKeyAction =
@@ -106,12 +109,16 @@ export type DashboardKeyAction =
106
109
  | "pane-output"
107
110
  | "pane-health"
108
111
  | "pane-metrics"
112
+ | "pane-plan"
113
+ | "plan-diff"
109
114
  | "up"
110
115
  | "down"
111
116
  | "mailbox-detail"
112
117
  | "health-recovery"
113
118
  | "health-kill-stale"
114
119
  | "health-diagnostic-export"
120
+ | "plan-approve"
121
+ | "plan-deny"
115
122
  | "notifications-dismiss";
116
123
 
117
124
  /**
@@ -125,14 +132,17 @@ export type DashboardKeyAction =
125
132
  * 2. `mailbox-detail` (\r, \n) is pane-scoped to mailbox and MUST precede
126
133
  * `select` (which also binds \r, \n) so Enter opens the detail instead of
127
134
  * triggering select while in the mailbox pane.
128
- * 3. `health-*` are pane-scoped to health.
135
+ * 3. `health-*` and `plan-*` are pane-scoped (health / progress).
129
136
  * 4. `notifications-dismiss` (H) is global.
130
137
  * 5. `select`, then the root actions, pane switches, and navigation.
131
138
  *
132
139
  * NOTE: mailbox action keys A/N/C/P/X (ack/nudge/compose/preview/ackAll) are
133
- * intentionally NOT in this table. They live in `DASHBOARD_KEYS.mailbox` for
134
- * reservation but are handled by the mailbox overlay's own `handleInput`,
135
- * not by the dashboard dispatch. Adding them here would change behavior.
140
+ * intentionally NOT dispatched for the mailbox pane by this table. They live
141
+ * in `DASHBOARD_KEYS.mailbox` for reservation but are handled by the mailbox
142
+ * overlay's own `handleInput`, not by the dashboard dispatch. The `plan`
143
+ * group reuses uppercase "A" (approve) pane-scoped to "progress" — it never
144
+ * fires while the mailbox pane (or the mailbox-detail overlay) owns input,
145
+ * so `mailbox.ack` behavior is unchanged.
136
146
  */
137
147
  const DEFAULT_BINDINGS: readonly KeyBinding[] = [
138
148
  { keys: DASHBOARD_KEYS.close, action: "close" },
@@ -157,6 +167,22 @@ const DEFAULT_BINDINGS: readonly KeyBinding[] = [
157
167
  action: "health-diagnostic-export",
158
168
  pane: "health",
159
169
  },
170
+ {
171
+ keys: DASHBOARD_KEYS.plan.approve,
172
+ action: "plan-approve",
173
+ // WP-7: shared by the progress banner and the Plan pane (pane 7).
174
+ pane: ["progress", "plan"],
175
+ },
176
+ {
177
+ keys: DASHBOARD_KEYS.plan.deny,
178
+ action: "plan-deny",
179
+ pane: ["progress", "plan"],
180
+ },
181
+ {
182
+ keys: DASHBOARD_KEYS.plan.diff,
183
+ action: "plan-diff",
184
+ pane: "plan",
185
+ },
160
186
  {
161
187
  keys: DASHBOARD_KEYS.notification.dismissAll,
162
188
  action: "notifications-dismiss",
@@ -179,6 +205,7 @@ const DEFAULT_BINDINGS: readonly KeyBinding[] = [
179
205
  { keys: DASHBOARD_KEYS.pane.output, action: "pane-output" },
180
206
  { keys: DASHBOARD_KEYS.pane.health, action: "pane-health" },
181
207
  { keys: DASHBOARD_KEYS.pane.metrics, action: "pane-metrics" },
208
+ { keys: DASHBOARD_KEYS.pane.plan, action: "pane-plan" },
182
209
  { keys: DASHBOARD_KEYS.navigation.up, action: "up" },
183
210
  { keys: DASHBOARD_KEYS.navigation.down, action: "down" },
184
211
  ];
@@ -202,6 +229,7 @@ const KEY_RESERVED = new Set<string>([
202
229
  ...Object.values(DASHBOARD_KEYS.navigation).flat(),
203
230
  ...Object.values(DASHBOARD_KEYS.mailbox).flat(),
204
231
  ...Object.values(DASHBOARD_KEYS.health).flat(),
232
+ ...Object.values(DASHBOARD_KEYS.plan).flat(),
205
233
  ...Object.values(DASHBOARD_KEYS.notification).flat(),
206
234
  ]);
207
235
 
@@ -253,9 +281,22 @@ function parseKeybindingOverride(raw: unknown): KeybindingOverride {
253
281
  * bindings fire at once (so a shared key is genuinely ambiguous). Global
254
282
  * (`undefined`) matches anything; two different concrete panes never overlap.
255
283
  */
256
- function paneScopesCompatible(a: ActivePane | undefined, b: ActivePane | undefined): boolean {
284
+ /** Pane scopes — a binding may fire in ONE pane, SEVERAL panes (WP-7: plan
285
+ * approval keys are shared by progress + plan panes), or every pane
286
+ * (undefined). */
287
+ export type PaneScope = ActivePane | readonly ActivePane[];
288
+
289
+ function paneScopeMatches(scope: PaneScope | undefined, pane: ActivePane | undefined): boolean {
290
+ if (scope === undefined) return true;
291
+ if (Array.isArray(scope)) return pane !== undefined && scope.includes(pane);
292
+ return scope === pane;
293
+ }
294
+
295
+ function paneScopesCompatible(a: PaneScope | undefined, b: PaneScope | undefined): boolean {
257
296
  if (a === undefined || b === undefined) return true;
258
- return a === b;
297
+ const as = Array.isArray(a) ? a : [a];
298
+ const bs = Array.isArray(b) ? b : [b];
299
+ return as.some((x) => bs.includes(x));
259
300
  }
260
301
 
261
302
  interface EffectiveBindingsResult {
@@ -311,7 +352,7 @@ function readConfigKeybindings(cwd: string): KeybindingOverride {
311
352
 
312
353
  /** Read the `PI_CREW_KEYBINDINGS` env var (JSON object string). */
313
354
  function readEnvKeybindings(): KeybindingOverride {
314
- const raw = process.env[KEYBINDINGS_ENV];
355
+ const raw = getCrewEnv(KEYBINDINGS_ENV);
315
356
  if (!raw) return {};
316
357
  try {
317
358
  return parseKeybindingOverride(JSON.parse(raw));
@@ -344,7 +385,7 @@ let _overrideWarnings: readonly string[] = [];
344
385
  * mtime, cwd); a single `statSync` per call detects on-disk config changes.
345
386
  */
346
387
  function getEffectiveBindings(cwd: string = process.cwd()): readonly KeyBinding[] {
347
- const envRaw = process.env[KEYBINDINGS_ENV];
388
+ const envRaw = getCrewEnv(KEYBINDINGS_ENV);
348
389
  const configMtime = configKeybindingsMtime(cwd);
349
390
  if (_effectiveCache && _effectiveCache.env === envRaw && _effectiveCache.configMtime === configMtime && _effectiveCache.cwd === cwd) {
350
391
  return _effectiveCache.bindings;
@@ -402,7 +443,7 @@ export function dashboardActionForKey(data: string, activePane?: ActivePane): Da
402
443
  // Pass 1 — exact string match (case-sensitive). Handles literal ASCII
403
444
  // keystrokes ('d', 'D', 'q', 'S', …) and preserves their distinct meanings.
404
445
  for (const binding of BINDINGS) {
405
- if (binding.pane !== undefined && binding.pane !== activePane) continue;
446
+ if (!paneScopeMatches(binding.pane, activePane)) continue;
406
447
  if (binding.keys.includes(data)) return binding.action;
407
448
  }
408
449
  // Pass 2 — terminal-aware match for escape sequences / canonical KeyIds.
@@ -411,7 +452,7 @@ export function dashboardActionForKey(data: string, activePane?: ActivePane): Da
411
452
  // legacy CSI, app-cursor-mode, and Kitty-protocol variants uniformly.
412
453
  const key = keyOf(data);
413
454
  for (const binding of BINDINGS) {
414
- if (binding.pane !== undefined && binding.pane !== activePane) continue;
455
+ if (!paneScopeMatches(binding.pane, activePane)) continue;
415
456
  for (const candidate of binding.keys) {
416
457
  if (key === candidate) return binding.action;
417
458
  if (matchesKey(data, candidate as KeyId)) return binding.action;
@@ -46,6 +46,15 @@ export function setExtensionWidget(
46
46
  ctx.ui.setWidget(key, content as never, widgetOptions as WidgetOptions);
47
47
  }
48
48
 
49
+ /**
50
+ * Map the crew widget placement onto pi's `WidgetPlacement`. `"bottom"` is a
51
+ * crew-only placement (rendered inside the crew-vibes footer, below the quota
52
+ * lines); when a raw widget slot is involved it falls back to `belowEditor`.
53
+ */
54
+ export function toPiWidgetPlacement(placement: "aboveEditor" | "belowEditor" | "bottom"): "aboveEditor" | "belowEditor" {
55
+ return placement === "bottom" ? "belowEditor" : placement;
56
+ }
57
+
49
58
  type FooterFactory = (tui: unknown, theme: unknown, footerData: unknown) => unknown;
50
59
 
51
60
  /** Install a custom footer component, or pass `undefined` to restore pi's built-in footer.
@@ -4,6 +4,7 @@ import { listRecentRuns } from "../extension/run-index.ts";
4
4
  import { readCrewAgents } from "../runtime/crew-agent-records.ts";
5
5
  import { listLiveAgents, listLiveAgentsByWorkspace } from "../runtime/live-session/live-agent-manager.ts";
6
6
  import type { ManifestCache } from "../runtime/manifest-cache.ts";
7
+ import { isPlanApprovalPending } from "../runtime/plan-approval.ts";
7
8
  import { isDisplayActiveRun } from "../runtime/process-status.ts";
8
9
  import type { TeamRunManifest, TeamTaskState } from "../state/types.ts";
9
10
  import { aggregateUsage } from "../state/usage.ts";
@@ -87,6 +88,10 @@ export function registerPiCrewPowerbarSegments(events: EventBus, config?: CrewUi
87
88
  id: "pi-crew-steps",
88
89
  label: "pi-crew workflow steps",
89
90
  });
91
+ safeEmit(events, "powerbar:register-segment", {
92
+ id: "pi-crew-plan",
93
+ label: "pi-crew plan approval",
94
+ });
90
95
  }
91
96
 
92
97
  export function updatePiCrewPowerbar(
@@ -136,11 +141,40 @@ interface ActiveItem {
136
141
  * Build the workflow steps segment showing: ✓explore › →plan › ○execute › ○verify
137
142
  * with the current/active step highlighted using → arrow.
138
143
  */
139
- function buildStepsPayload(active: ActiveItem[], allTasks: TeamTaskState[]): PowerbarPayloadShape {
144
+ export function buildStepsPayload(active: ActiveItem[], allTasks: TeamTaskState[]): PowerbarPayloadShape {
140
145
  if (!active.length) {
141
146
  return { id: "pi-crew-steps" };
142
147
  }
143
148
  const run = active[0]!.run;
149
+ // WP-7 (R7): plan-carrying runs show PLAN PHASES instead of workflow steps —
150
+ // the plan is the actual execution shape (a re-plan revision switches the
151
+ // segment without waiting for workflow steps that no longer apply).
152
+ const plans = active[0]!.snapshot?.plans;
153
+ if (plans && plans.length > 0) {
154
+ const current = plans[plans.length - 1]!;
155
+ const itemById = new Map(current.items.map((i) => [i.id, i]));
156
+ const statusOf = (ids: string[]): "completed" | "running" | "pending" => {
157
+ const items = ids.map((id) => itemById.get(id)?.status).filter(Boolean) as string[];
158
+ if (items.length && items.every((s) => s === "done")) return "completed";
159
+ if (items.some((s) => s === "active")) return "running";
160
+ return "pending";
161
+ };
162
+ const parts = current.phases.map((phase) => {
163
+ const status = phase.status === "done" ? "completed" : statusOf(phase.itemIds);
164
+ const icon = status === "completed" ? "✓" : status === "running" ? "→" : "○";
165
+ const name = phase.title.length > 10 ? `${phase.title.slice(0, 9)}…` : phase.title;
166
+ return `${icon}${name}`;
167
+ });
168
+ if (parts.length) {
169
+ const hasRunning = parts.some((p) => p.startsWith("→"));
170
+ const allComplete = parts.every((p) => p.startsWith("✓"));
171
+ return {
172
+ id: "pi-crew-steps",
173
+ text: `P${current.version} ${parts.join(" › ")}`,
174
+ color: allComplete ? "success" : hasRunning ? "accent" : "dim",
175
+ };
176
+ }
177
+ }
144
178
  const workflowName = run.workflow ?? "default";
145
179
  // Load workflow steps
146
180
  const workflows = allWorkflows(discoverWorkflows(run.cwd));
@@ -206,6 +240,7 @@ class PowerbarPublisher {
206
240
  #lastActiveKey: string | undefined;
207
241
  #lastProgressKey: string | undefined;
208
242
  #lastStepsKey: string | undefined;
243
+ #lastPlanKey: string | undefined;
209
244
  #latestArgs: PowerbarUpdateArgs | null = null;
210
245
  readonly #coalescer: RenderCoalescer;
211
246
 
@@ -279,9 +314,11 @@ class PowerbarPublisher {
279
314
  this.#lastActiveKey = undefined;
280
315
  this.#lastProgressKey = undefined;
281
316
  this.#lastStepsKey = undefined;
317
+ this.#lastPlanKey = undefined;
282
318
  safeEmit(events, "powerbar:update", { id: "pi-crew-active" });
283
319
  safeEmit(events, "powerbar:update", { id: "pi-crew-progress" });
284
320
  safeEmit(events, "powerbar:update", { id: "pi-crew-steps" });
321
+ safeEmit(events, "powerbar:update", { id: "pi-crew-plan" });
285
322
  return;
286
323
  }
287
324
  const agents = active.flatMap((item) => item.agents);
@@ -373,6 +410,12 @@ class PowerbarPublisher {
373
410
  } as const;
374
411
  // Build step progress: "explorer > planner > executor > verifier" with current step highlighted
375
412
  const stepsPayload = buildStepsPayload(active, tasks);
413
+ // WP-3 (H4): plan-approval segment — `plan:pending` while ANY active run is
414
+ // parked awaiting plan approval, clear payload otherwise. `item.run` is
415
+ // always the freshest manifest available (snapshot.manifest when cached).
416
+ const planPayload: PowerbarPayloadShape = active.some((item) => isPlanApprovalPending(item.run))
417
+ ? { id: "pi-crew-plan", text: "plan:pending", color: "warning" }
418
+ : { id: "pi-crew-plan" };
376
419
  // 1.8: dedup per segment using a key over every visible field. Previously
377
420
  // the dedup string only carried text/suffix/running, so changes to `bar`
378
421
  // (progress %) or `color` could be swallowed and stale UI emitted again
@@ -380,6 +423,7 @@ class PowerbarPublisher {
380
423
  const activeKey = powerbarKey(activePayload);
381
424
  const progressKey = powerbarKey(progressPayload);
382
425
  const stepsKey = powerbarKey(stepsPayload);
426
+ const planKey = powerbarKey(planPayload);
383
427
  if (activeKey !== this.#lastActiveKey) {
384
428
  this.#lastActiveKey = activeKey;
385
429
  safeEmit(events, "powerbar:update", activePayload);
@@ -392,6 +436,10 @@ class PowerbarPublisher {
392
436
  this.#lastStepsKey = stepsKey;
393
437
  safeEmit(events, "powerbar:update", stepsPayload);
394
438
  }
439
+ if (planKey !== this.#lastPlanKey) {
440
+ this.#lastPlanKey = planKey;
441
+ safeEmit(events, "powerbar:update", planPayload);
442
+ }
395
443
  // Never call setStatusFallback - crew-widget manages "pi-crew" status with its own widget format
396
444
  // Powerbar only emits events; it does not set status directly
397
445
  }
@@ -431,9 +479,11 @@ class PowerbarPublisher {
431
479
  this.#lastActiveKey = undefined;
432
480
  this.#lastProgressKey = undefined;
433
481
  this.#lastStepsKey = undefined;
482
+ this.#lastPlanKey = undefined;
434
483
  safeEmit(events, "powerbar:update", { id: "pi-crew-active" });
435
484
  safeEmit(events, "powerbar:update", { id: "pi-crew-progress" });
436
485
  safeEmit(events, "powerbar:update", { id: "pi-crew-steps" });
486
+ safeEmit(events, "powerbar:update", { id: "pi-crew-plan" });
437
487
  }
438
488
 
439
489
  /** Reset dedup state on session lifecycle events. */
@@ -441,6 +491,7 @@ class PowerbarPublisher {
441
491
  this.#lastActiveKey = undefined;
442
492
  this.#lastProgressKey = undefined;
443
493
  this.#lastStepsKey = undefined;
494
+ this.#lastPlanKey = undefined;
444
495
  }
445
496
 
446
497
  dispose(): void {
@@ -3,6 +3,7 @@ import type { MetricRegistry } from "../observability/metric-registry.ts";
3
3
  import { readCrewAgents } from "../runtime/crew-agent-records.ts";
4
4
  import type { CrewAgentRecord } from "../runtime/crew-agent-runtime.ts";
5
5
  import { getLiveAgentContextPercent } from "../runtime/live-session/live-agent-manager.ts";
6
+ import { isPlanApprovalPending } from "../runtime/plan-approval.ts";
6
7
  import { isDisplayActiveRun, isLikelyOrphanedActiveRun } from "../runtime/process-status.ts";
7
8
  import type { TeamRunManifest, TeamTaskState, UsageState } from "../state/types.ts";
8
9
  import { aggregateUsage } from "../state/usage.ts";
@@ -16,6 +17,7 @@ import { summarizeTerminalReason } from "./dashboard-panes/cancellation-pane.ts"
16
17
  import { renderHealthPane } from "./dashboard-panes/health-pane.ts";
17
18
  import { renderMailboxPane } from "./dashboard-panes/mailbox-pane.ts";
18
19
  import { renderMetricsPane } from "./dashboard-panes/metrics-pane.ts";
20
+ import { renderPlanPane } from "./dashboard-panes/plan-pane.ts";
19
21
  import { renderProgressPane } from "./dashboard-panes/progress-pane.ts";
20
22
  import { renderTranscriptPane } from "./dashboard-panes/transcript-pane.ts";
21
23
  import { DynamicCrewBorder } from "./dynamic-border.ts";
@@ -75,7 +77,7 @@ export interface RunDashboardOptions {
75
77
  * looking at. Resetting to "agents" on every `new RunDashboard(...)` was a
76
78
  * UX regression.
77
79
  */
78
- let lastActivePane: "agents" | "progress" | "mailbox" | "output" | "health" | "metrics" = "agents";
80
+ let lastActivePane: "agents" | "progress" | "mailbox" | "output" | "health" | "metrics" | "plan" = "agents";
79
81
 
80
82
  export type RunDashboardAction =
81
83
  | "status"
@@ -94,6 +96,8 @@ export type RunDashboardAction =
94
96
  | "health-recovery"
95
97
  | "health-kill-stale"
96
98
  | "health-diagnostic-export"
99
+ | "plan-approve"
100
+ | "plan-deny"
97
101
  | "notifications-dismiss";
98
102
  export interface RunDashboardSelection {
99
103
  runId: string;
@@ -404,7 +408,9 @@ export class RunDashboard implements DashboardComponent {
404
408
  private runScrollOffset = 0;
405
409
  private showFullProgress = false;
406
410
  private showHelp = false;
407
- private activePane: "agents" | "progress" | "mailbox" | "output" | "health" | "metrics" = lastActivePane;
411
+ private activePane: "agents" | "progress" | "mailbox" | "output" | "health" | "metrics" | "plan" = lastActivePane;
412
+ /** WP-7 (R7): pane-scoped revision-diff toggle (X). */
413
+ private planDiff = false;
408
414
  private runs: TeamRunManifest[];
409
415
  private readonly done: (selection: RunDashboardSelection | undefined) => void;
410
416
  private readonly theme: CrewTheme;
@@ -638,7 +644,7 @@ export class RunDashboard implements DashboardComponent {
638
644
  lines.push(
639
645
  border("╭", "╮"),
640
646
  row(
641
- `${fg("accent", "▐")} ${this.theme.bold("pi-crew")} · ${this.runs.length} runs ${fg("dim", "1-6 pane · ↑↓ · Enter · ? help · Esc")}`,
647
+ `${fg("accent", "▐")} ${this.theme.bold("pi-crew")} · ${this.runs.length} runs ${fg("dim", "1-7 pane · ↑↓ · Enter · ? help · Esc")}`,
642
648
  ),
643
649
  sep(),
644
650
  );
@@ -732,7 +738,9 @@ export class RunDashboard implements DashboardComponent {
732
738
  registry: this.options.registry,
733
739
  }),
734
740
  )
735
- : safeRenderPane("transcript", () => renderTranscriptPane(snap))
741
+ : this.activePane === "plan"
742
+ ? safeRenderPane("plan", () => renderPlanPane(snap, { diff: this.planDiff }))
743
+ : safeRenderPane("transcript", () => renderTranscriptPane(snap))
736
744
  : [...readAgentPreview(r, 4, this.options), ...readProgressPreview(r, 2, this.options.snapshotCache)];
737
745
  const filteredPane = paneLines.filter((l) => l && !l.includes("(none)") && l.trim() !== "");
738
746
  if (filteredPane.length > 0) {
@@ -832,6 +840,19 @@ export class RunDashboard implements DashboardComponent {
832
840
  this.done(selectedRunId ? { runId: selectedRunId, action: "status" } : undefined);
833
841
  return;
834
842
  }
843
+ // WP-3 (H4-subset): plan approval keys. Deliberately a DEDICATED branch
844
+ // (not the generic union block below) because plan actions must gate on
845
+ // the selected run actually being parked on a pending approval — a
846
+ // stray keystroke on a non-pending run is a silent no-op (dashboard
847
+ // stays open) instead of closing it via done(undefined).
848
+ if (action === "plan-approve" || action === "plan-deny") {
849
+ const run = selectedRunFromGrouped(this.runs, this.selected, this.options.snapshotCache);
850
+ const manifest = run ? (snapshotFor(run, this.options.snapshotCache)?.manifest ?? run) : undefined;
851
+ if (run && manifest && isPlanApprovalPending(manifest)) {
852
+ this.done({ runId: run.runId, action });
853
+ }
854
+ return;
855
+ }
835
856
  if (
836
857
  action === "summary" ||
837
858
  action === "artifacts" ||
@@ -875,7 +896,12 @@ export class RunDashboard implements DashboardComponent {
875
896
  else if (action === "pane-output") this.activePane = "output";
876
897
  else if (action === "pane-health") this.activePane = "health";
877
898
  else if (action === "pane-metrics") this.activePane = "metrics";
878
- else if (action === "up") this.selected = Math.max(0, this.selected - 1);
899
+ else if (action === "pane-plan") this.activePane = "plan";
900
+ else if (action === "plan-diff") {
901
+ this.planDiff = !this.planDiff;
902
+ this.invalidate();
903
+ return;
904
+ } else if (action === "up") this.selected = Math.max(0, this.selected - 1);
879
905
  else if (action === "down") {
880
906
  const selectableCount = groupedRuns(this.runs, this.options.snapshotCache).filter((row) => row.run).length;
881
907
  this.selected = Math.min(Math.max(0, selectableCount - 1), this.selected + 1);