pi-crew 0.9.68 → 0.10.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (217) hide show
  1. package/CHANGELOG.md +124 -0
  2. package/agents/analyst.md +1 -1
  3. package/agents/cold-verifier.md +3 -1
  4. package/agents/critic.md +1 -1
  5. package/agents/executor.md +1 -1
  6. package/agents/explorer.md +1 -1
  7. package/agents/planner.md +1 -1
  8. package/agents/reviewer.md +1 -1
  9. package/agents/security-reviewer.md +1 -1
  10. package/agents/test-engineer.md +1 -1
  11. package/agents/verifier.md +1 -1
  12. package/agents/writer.md +1 -1
  13. package/dist/index.mjs +65838 -60331
  14. package/docs/README.md +2 -0
  15. package/docs/actions-reference.md +31 -0
  16. package/docs/commands-reference.md +17 -6
  17. package/docs/resource-formats.md +13 -0
  18. package/package.json +4 -2
  19. package/schema.json +15 -1
  20. package/scripts/resource-sampler.mjs +36 -2
  21. package/skills/requirements-to-task-packet/SKILL.md +26 -0
  22. package/skills/widget-rendering/SKILL.md +7 -7
  23. package/src/agents/agent-config.ts +2 -1
  24. package/src/agents/discover-agents.ts +23 -14
  25. package/src/config/config-merge.ts +183 -0
  26. package/src/config/config-validation.ts +681 -0
  27. package/src/config/config.ts +22 -864
  28. package/src/config/defaults.ts +35 -1
  29. package/src/config/drift-detector.ts +1 -1
  30. package/src/config/env-vars.ts +691 -0
  31. package/src/config/role-tools.ts +8 -8
  32. package/src/config/sanitize-project-config.ts +172 -0
  33. package/src/config/types.ts +30 -0
  34. package/src/extension/async-notifier.ts +25 -2
  35. package/src/extension/crew-vibes/config.ts +2 -1
  36. package/src/extension/plan-orchestrate.ts +132 -0
  37. package/src/extension/registration/commands/dashboard.ts +158 -0
  38. package/src/extension/registration/commands/index.ts +35 -0
  39. package/src/extension/registration/commands/manage.ts +303 -0
  40. package/src/extension/registration/commands/run.ts +214 -0
  41. package/src/extension/registration/commands/shared.ts +631 -0
  42. package/src/extension/registration/commands/status.ts +60 -0
  43. package/src/extension/registration/commands.ts +13 -1224
  44. package/src/extension/registration/lifecycle-handlers.ts +126 -15
  45. package/src/extension/registration/subagent-tools.ts +218 -9
  46. package/src/extension/rpc-hmac.ts +5 -3
  47. package/src/extension/team-tool/api/heartbeat.ts +47 -10
  48. package/src/extension/team-tool/api/plan-approval.ts +9 -0
  49. package/src/extension/team-tool/api/task-claims.ts +109 -40
  50. package/src/extension/team-tool/cancel.ts +84 -50
  51. package/src/extension/team-tool/dispatch/index.ts +1 -0
  52. package/src/extension/team-tool/dispatch/run.ts +4 -1
  53. package/src/extension/team-tool/doctor.ts +103 -1
  54. package/src/extension/team-tool/orchestrate.ts +66 -1
  55. package/src/extension/team-tool/plans.ts +192 -0
  56. package/src/extension/team-tool/respond.ts +197 -65
  57. package/src/extension/team-tool/run-deadline.ts +16 -1
  58. package/src/extension/team-tool/run-intent.ts +63 -0
  59. package/src/extension/team-tool/run.ts +48 -18
  60. package/src/extension/team-tool/status.ts +84 -26
  61. package/src/extension/team-tool.ts +11 -2
  62. package/src/hooks/registry.ts +1 -6
  63. package/src/i18n.ts +9 -0
  64. package/src/prompt/prompt-runtime.ts +521 -2
  65. package/src/prompt/worker-events-channel.ts +173 -0
  66. package/src/runtime/README.md +8 -8
  67. package/src/runtime/async-runner.ts +7 -3
  68. package/src/runtime/background-runner.ts +42 -14
  69. package/src/runtime/broker/broker-issuer.ts +9 -2
  70. package/src/runtime/broker/crew-broker-tokens.ts +43 -6
  71. package/src/runtime/broker/crew-broker.ts +838 -10
  72. package/src/runtime/broker/wait-status-cache.ts +157 -0
  73. package/src/runtime/budget-enforcement.ts +281 -0
  74. package/src/runtime/child-pi/child-pi-spawn.ts +60 -14
  75. package/src/runtime/child-pi/child-pi-timers.ts +324 -0
  76. package/src/runtime/child-pi/child-pi.ts +97 -201
  77. package/src/runtime/child-pi/mock-fixtures.ts +16 -2
  78. package/src/runtime/crew-agent-records.ts +259 -14
  79. package/src/runtime/delegate-spawn.ts +148 -0
  80. package/src/runtime/deterministic-ast.ts +2 -1
  81. package/src/runtime/dispatch-batch.ts +945 -0
  82. package/src/runtime/finalize-run.ts +557 -0
  83. package/src/runtime/goal-workflow/adaptive-plan.ts +87 -13
  84. package/src/runtime/goal-workflow/dynamic-workflow-runner.ts +3 -2
  85. package/src/runtime/goal-workflow/goal-state-store.ts +1 -1
  86. package/src/runtime/group-join.ts +11 -125
  87. package/src/runtime/live-session/live-session-runtime.ts +26 -1
  88. package/src/runtime/merge-gate.ts +7 -1
  89. package/src/runtime/merge-loop.ts +130 -0
  90. package/src/runtime/model/model-budget-summary.ts +53 -0
  91. package/src/runtime/model/model-fallback.ts +32 -2
  92. package/src/runtime/model/pi-args.ts +10 -0
  93. package/src/runtime/model/provider-extensions.ts +10 -0
  94. package/src/runtime/orphan-worker-registry.ts +1 -1
  95. package/src/runtime/output/output-validator.ts +45 -0
  96. package/src/runtime/parent-guard.ts +3 -1
  97. package/src/runtime/peer-dep.ts +2 -1
  98. package/src/runtime/per-write-validator.ts +0 -5
  99. package/src/runtime/pi-spawn.ts +61 -15
  100. package/src/runtime/plan-approval.ts +125 -0
  101. package/src/runtime/plan-replan.ts +151 -0
  102. package/src/runtime/recovery/checkpoint.ts +0 -18
  103. package/src/runtime/recovery/crash-recovery.ts +95 -46
  104. package/src/runtime/scheduler-context.ts +98 -0
  105. package/src/runtime/scheduling/coalesce-tasks.ts +5 -0
  106. package/src/runtime/scheduling/global-worker-cap.ts +2 -1
  107. package/src/runtime/scheduling/nested-slots.ts +70 -0
  108. package/src/runtime/scheduling/run-coalesced-task-group.ts +64 -13
  109. package/src/runtime/scheduling/task-graph-scheduler.ts +0 -10
  110. package/src/runtime/settings-store.ts +219 -0
  111. package/src/runtime/spawn-policy.ts +217 -0
  112. package/src/runtime/stale-reconciler.ts +87 -6
  113. package/src/runtime/subagent-manager.ts +25 -1
  114. package/src/runtime/task-output-context.ts +230 -9
  115. package/src/runtime/task-packet.ts +23 -1
  116. package/src/runtime/task-runner/child-executor.ts +106 -7
  117. package/src/runtime/task-runner/post-execution.ts +125 -1
  118. package/src/runtime/task-runner/pre-execution.ts +39 -1
  119. package/src/runtime/task-runner/prompt-builder.ts +51 -1
  120. package/src/runtime/task-runner/retrieval-orchestrator.ts +72 -18
  121. package/src/runtime/task-runner/spec-evidence.ts +403 -0
  122. package/src/runtime/task-runner/state-helpers.ts +26 -24
  123. package/src/runtime/task-runner.ts +11 -0
  124. package/src/runtime/team-runner.ts +129 -1671
  125. package/src/runtime/verification/spec-sandbox.ts +255 -0
  126. package/src/runtime/verification/verification-gates.ts +3 -2
  127. package/src/runtime/verification/verification-worktree.ts +2 -1
  128. package/src/runtime/workflow-phase-advance.ts +100 -0
  129. package/src/runtime/workspace-tree.ts +9 -0
  130. package/src/schema/config-schema.ts +65 -25
  131. package/src/schema/sensitive-config-paths.ts +64 -0
  132. package/src/schema/team-tool-schema.ts +13 -3
  133. package/src/state/README.md +4 -10
  134. package/src/state/atomic-write.ts +20 -3
  135. package/src/state/contracts.ts +38 -0
  136. package/src/state/coordination/mailbox.ts +12 -2
  137. package/src/state/event-log/cursor.ts +223 -0
  138. package/src/state/event-log/event-log-rotation.ts +12 -4
  139. package/src/state/event-log/event-log.ts +152 -369
  140. package/src/state/event-log/sequence-cache.ts +373 -0
  141. package/src/state/event-log/worker-atomic-writer.ts +2 -1
  142. package/src/state/stores/active-run-registry.ts +3 -2
  143. package/src/state/stores/manifest-io.ts +237 -0
  144. package/src/state/stores/ownership-map.ts +162 -0
  145. package/src/state/stores/plan-store.ts +241 -0
  146. package/src/state/stores/run-cache.ts +0 -90
  147. package/src/state/stores/spec-store.ts +189 -0
  148. package/src/state/stores/state-store.ts +66 -229
  149. package/src/state/types.ts +197 -0
  150. package/src/ui/dashboard-panes/plan-pane.ts +136 -0
  151. package/src/ui/dashboard-panes/progress-pane.ts +6 -0
  152. package/src/ui/dashboard-panes/transcript-pane.ts +31 -0
  153. package/src/ui/heartbeat-aggregator.ts +9 -1
  154. package/src/ui/keybinding-map.ts +54 -13
  155. package/src/ui/powerbar-publisher.ts +52 -1
  156. package/src/ui/run-dashboard.ts +31 -5
  157. package/src/ui/run-snapshot-cache.ts +57 -30
  158. package/src/ui/snapshot-types.ts +6 -1
  159. package/src/ui/widget/widget-renderer.ts +9 -1
  160. package/src/utils/file-coalescer.ts +0 -4
  161. package/src/utils/fs-errno.ts +66 -0
  162. package/src/utils/fs-watch.ts +1 -1
  163. package/src/utils/internal-error.ts +3 -1
  164. package/src/utils/paths.ts +11 -3
  165. package/src/utils/task-name-generator.ts +1 -8
  166. package/src/workflows/discover-workflows.ts +19 -2
  167. package/src/workflows/validate-workflow.ts +7 -1
  168. package/src/workflows/workflow-config.ts +10 -0
  169. package/src/workflows/workflow-serializer.ts +3 -0
  170. package/src/worktree/worktree-manager.ts +22 -0
  171. package/src/agents/agent-search.ts +0 -98
  172. package/src/benchmark/benchmark-runner.ts +0 -313
  173. package/src/benchmark/feedback-loop.ts +0 -73
  174. package/src/config/resilient-parser.ts +0 -117
  175. package/src/extension/crew-vibes/cat-frames.ts +0 -18
  176. package/src/extension/result-watcher.ts +0 -139
  177. package/src/observability/exporters/prometheus-exporter.ts +0 -54
  178. package/src/observability/metric-retention.ts +0 -64
  179. package/src/runtime/compaction/compaction-summary.ts +0 -278
  180. package/src/runtime/errors/crew-errors.ts +0 -162
  181. package/src/runtime/live-session/intercom-bridge.ts +0 -187
  182. package/src/runtime/loop-gates.ts +0 -128
  183. package/src/runtime/metric-parser.ts +0 -36
  184. package/src/runtime/output/stream-preview.ts +0 -184
  185. package/src/runtime/output/tool-progress.ts +0 -278
  186. package/src/runtime/phase-tracker.ts +0 -385
  187. package/src/runtime/pipeline-runner.ts +0 -523
  188. package/src/runtime/process/process-lifecycle.ts +0 -491
  189. package/src/runtime/recovery/retry-runner.ts +0 -330
  190. package/src/runtime/run-drift.ts +0 -219
  191. package/src/runtime/task-quality.ts +0 -199
  192. package/src/runtime/task-runner/run-projection.ts +0 -128
  193. package/src/runtime/verification/post-checks.ts +0 -142
  194. package/src/state/coordination/schedule.ts +0 -166
  195. package/src/state/event-log/jsonl-writer.ts +0 -115
  196. package/src/state/hook-instinct-bridge.ts +0 -94
  197. package/src/state/hook-integrations.ts +0 -51
  198. package/src/state/session-state-map.ts +0 -51
  199. package/src/state/stores/blob-store.ts +0 -308
  200. package/src/state/stores/instinct-store.ts +0 -275
  201. package/src/state/stores/observation-store.ts +0 -176
  202. package/src/state/tiered-eval.ts +0 -480
  203. package/src/state/types-eval.ts +0 -58
  204. package/src/tools/safe-bash-extension.ts +0 -54
  205. package/src/tools/safe-bash.ts +0 -505
  206. package/src/ui/agent-management-overlay.ts +0 -160
  207. package/src/ui/crew-footer.ts +0 -102
  208. package/src/ui/crew-select-list.ts +0 -114
  209. package/src/ui/dashboard-panes/capability-pane.ts +0 -77
  210. package/src/ui/transcript-entries.ts +0 -256
  211. package/src/utils/conflict-detect.ts +0 -721
  212. package/src/utils/fingerprint.ts +0 -180
  213. package/src/utils/gh-protocol.ts +0 -556
  214. package/src/utils/project-detector.ts +0 -160
  215. package/src/utils/sse-parser.ts +0 -131
  216. package/src/workflows/cost-estimator.ts +0 -34
  217. package/src/workflows/intermediate-store.ts +0 -166
@@ -1,6 +1,7 @@
1
1
  import type { CrewAgentProgress } from "../runtime/crew-agent-runtime.ts";
2
2
  import type { WorkerHeartbeatState } from "../runtime/heartbeat/worker-heartbeat.ts";
3
3
  import type { CrashClass } from "../runtime/recovery/crash-classification.ts";
4
+ import type { FatalFsCause } from "../utils/fs-errno.ts";
4
5
  import type { TeamRunStatus, TeamTaskStatus } from "./contracts.ts";
5
6
  import type { TaskClaimState } from "./coordination/task-claims.ts";
6
7
  import type { CoherenceMark, RolloutEntry } from "./decision-ledger.ts";
@@ -55,6 +56,73 @@ export interface TaskOutputSchema {
55
56
  example?: string;
56
57
  }
57
58
 
59
+ export type SpecPriority = "must" | "should" | "could";
60
+
61
+ /** T4/R6 (ADR-6 §1): workspace-level spec record — state/specs/<id>.json.
62
+ * Revision machinery mirrors ADR-4 §1 PlanRecord (append-only revision list,
63
+ * copy-forward linkage, stable requirement/acceptance ids). */
64
+ export interface SpecRecord {
65
+ id: string;
66
+ version: number;
67
+ revisionOf?: number;
68
+ title: string;
69
+ requirements: Array<{ id: string; text: string; priority: SpecPriority }>;
70
+ acceptance: Array<{
71
+ id: string;
72
+ requirementId: string;
73
+ /** Free-text description of what counts as evidence (non-strict). */
74
+ check: string;
75
+ /** Strict mode (ADR-6 §4): machine-checkable form. */
76
+ command?: string;
77
+ expectedDigest?: string;
78
+ expectedExitCode?: number;
79
+ idempotent?: boolean;
80
+ }>;
81
+ source: { kind: "manual" | "generated"; by?: string; from?: string };
82
+ /** INFORMATIONAL copy of the store-mint provenance sidecar — the strict
83
+ * gate NEVER trusts this field alone (ADR-6 §4 provenance enforcement). */
84
+ trusted?: boolean;
85
+ }
86
+
87
+ /** Immutable per-task freeze (ADR-6 §1): embedded into the TaskPacket at
88
+ * dispatch; the strict gate executes ONLY snapshot-frozen commands. */
89
+ export interface SpecSnapshotItem {
90
+ requirement: SpecRecord["requirements"][number];
91
+ acceptance: SpecRecord["acceptance"][number];
92
+ }
93
+
94
+ export interface SpecSnapshot {
95
+ specId: string;
96
+ version: number;
97
+ frozenAt: string;
98
+ /** Provenance v2 (ADR-6 §4 + erratum): trust decided ONCE at freeze from
99
+ * the user-store digest sidecar; the strict gate reads only this frozen
100
+ * bit — post-freeze mint/delete cannot affect a running task. */
101
+ trustedAtFreeze: boolean;
102
+ items: SpecSnapshotItem[];
103
+ }
104
+
105
+ /** T4/R6 (ADR-6 §3): non-strict coverage-gate report persisted on the task.
106
+ * Non-strict NEVER blocks — gaps surface as the `unverified` badge only. */
107
+ export interface SpecGateResult {
108
+ /** "coverage" = non-strict (§3); "strict" arrives with the §4 sandbox. */
109
+ mode: "coverage" | "strict";
110
+ /** false for spec-less tasks — gate not applicable (regression guard). */
111
+ applicable: boolean;
112
+ footerPresent: boolean;
113
+ /** Cited ids in citation order (duplicates preserved). */
114
+ citedIds: string[];
115
+ /** Must-acceptance ids NOT cited — mechanically-detectable gap. */
116
+ missingMustIds: string[];
117
+ /** Cited ids that exist in no snapshot — fabrication signal (§2). */
118
+ unknownIds: string[];
119
+ /** Set ONLY on mechanically-detectable gaps (missing footer / missing
120
+ * must-ids / unknown ids). Full-coverage fabrication passes with NO badge. */
121
+ badge?: "unverified";
122
+ /** acceptanceId → one-line evidence text (verifier advisory input, §5). */
123
+ evidence: Record<string, string>;
124
+ }
125
+
58
126
  export interface TaskPacket {
59
127
  objective: string;
60
128
  scope: TaskScope;
@@ -70,6 +138,17 @@ export interface TaskPacket {
70
138
  expectedArtifacts: string[];
71
139
  verification: VerificationContract;
72
140
  outputSchema?: TaskOutputSchema;
141
+ /** T4/R6 (ADR-6): workspace spec ids this task is held to (frozen below). */
142
+ specRefs?: string[];
143
+ /** T4/R6 (ADR-6 §7): strict mode — coverage AND machine-check (§4). */
144
+ specStrict?: boolean;
145
+ /** Declared specRefs that resolved to NO record at freeze (typo / not
146
+ * imported / corrupted). Non-strict: unverified badge. Strict: the gate
147
+ * fails — a strict workflow must never silently degrade to ungated. */
148
+ unresolvedSpecRefs?: string[];
149
+ /** Frozen snapshots embedded at dispatch — later spec edits never rewrite
150
+ * what a running task was held to. */
151
+ specSnapshots?: SpecSnapshot[];
73
152
  }
74
153
 
75
154
  export type PolicyDecisionAction = "retry" | "reassign" | "escalate" | "block" | "notify" | "cleanup" | "closeout" | "fail";
@@ -164,6 +243,61 @@ export interface PlanApprovalState {
164
243
  }
165
244
 
166
245
  export type CrewActivityState = "active" | "active_long_running" | "needs_attention" | "stale";
246
+
247
+ /** T2/R4 (ADR-4 docs/decisions/2026-08-17-plan-object.md §1): status vocabulary
248
+ * shared by plan phases and items. `dropped` marks items removed by a re-plan
249
+ * revision (kept for traceability + diff, never re-dispatched). */
250
+ export type PlanItemStatus = "pending" | "active" | "done" | "dropped";
251
+
252
+ export interface PlanPhaseRecord {
253
+ id: string;
254
+ title: string;
255
+ itemIds: string[];
256
+ status: PlanItemStatus;
257
+ }
258
+
259
+ export interface PlanItemRecord {
260
+ /** Stable across revisions (ADR-4 §3 producer contract) — the scheduler
261
+ * copies carried-over linkage forward at revision switch. */
262
+ id: string;
263
+ /** External reference (e.g. tagged section id in the source plan doc). */
264
+ ref?: string;
265
+ title: string;
266
+ /** Scheduler-owned (single writer, inside the run lock — ADR-4 §3).
267
+ * Producers NEVER set taskIds. */
268
+ taskIds: string[];
269
+ /** R6/T4 forward hook; T2 writes it empty (ADR-4 §1). */
270
+ specIds: string[];
271
+ acceptance: string[];
272
+ status: PlanItemStatus;
273
+ }
274
+
275
+ /** Plan-record side of the approval gate. Vocabulary note (ADR-4 §8): the
276
+ * manifest side keeps `PlanApprovalState` ("cancelled" for deny); the record
277
+ * side uses "rejected" — `plans reject` dual-writes both. */
278
+ export interface PlanApprovalRecord {
279
+ status: "pending" | "approved" | "rejected";
280
+ by?: string;
281
+ at: string;
282
+ planVersion: number;
283
+ }
284
+
285
+ /** One revision in the append-only list persisted at
286
+ * `<stateRoot>/plans/plans.json` (plan-store.ts). History is never mutated in
287
+ * place — EXCEPT the current revision's `items[].taskIds`, the scheduler's
288
+ * single-writer linkage field (ADR-4 §3). */
289
+ export interface PlanRecord {
290
+ id: string;
291
+ runId: string;
292
+ version: number;
293
+ revisionOf?: { id: string; version: number };
294
+ title: string;
295
+ phases: PlanPhaseRecord[];
296
+ items: PlanItemRecord[];
297
+ approval?: PlanApprovalRecord;
298
+ createdAt: string;
299
+ authorTaskId?: string;
300
+ }
167
301
  export type CrewAttentionReason = "idle" | "tool_failures" | "completion_guard" | "heartbeat_stale" | "plan_approval_pending";
168
302
 
169
303
  export interface CrewAttentionEventData {
@@ -226,7 +360,24 @@ export interface TeamRunManifest {
226
360
  eventsPath: string;
227
361
  artifacts: ArtifactDescriptor[];
228
362
  async?: AsyncRunState;
363
+ /** @deprecated-plan-pointer T2/R4 (ADR-4 §2): dual-write era — PlanRecord at
364
+ * `<stateRoot>/plans/plans.json` is authoritative; this field is kept (never
365
+ * dropped) as the pre-v2 fallback and legacy UI surface. Deprecate-only. */
229
366
  planApproval?: PlanApprovalState;
367
+ /** T2/R4 (ADR-4 §2): pointer to the CURRENT plan revision in
368
+ * `<stateRoot>/plans/plans.json`. Plan-record-first readers fall back to
369
+ * `planApproval` above when absent or no record exists (dual-read migration;
370
+ * the manifest field is deprecated, never dropped). */
371
+ plan?: { id: string; version: number };
372
+ /** WP-2/R2 (ADR-0 item 3): run-level park pointer while a worker is blocked
373
+ * in the `ask` tool. Purely additive coordination state — `status` above is
374
+ * NEVER flipped to express waiting (the run stays "running": registry entry,
375
+ * sidebar visibility and live-executor.isCurrent() all preserved). */
376
+ waitState?: {
377
+ taskId: string;
378
+ questionId: string;
379
+ askedAt: string;
380
+ };
230
381
  /** Pi session that created the run, when available. Used to prevent cross-session destructive actions. */
231
382
  ownerSessionId?: string;
232
383
  /** pi-crew skill override selected when the run was created. false disables injected skill instructions. */
@@ -422,6 +573,10 @@ export interface TeamTaskState {
422
573
  jsonEvents?: number;
423
574
  agentProgress?: CrewAgentProgress;
424
575
  error?: string;
576
+ /** Fatal fs failure cause (bug-026 sub-issue B): set when the failure was
577
+ * classified as enospc/edquot/emfile/enfile so operators see "failed
578
+ * (disk full)" instead of a generic timeout diagnostic. */
579
+ failureCause?: FatalFsCause;
425
580
  claim?: TaskClaimState;
426
581
  heartbeat?: WorkerHeartbeatState;
427
582
  checkpoint?: TaskCheckpointState;
@@ -430,11 +585,35 @@ export interface TeamTaskState {
430
585
  terminalEvidence?: OperationTerminalEvidence[];
431
586
  taskPacket?: TaskPacket;
432
587
  verification?: VerificationEvidence;
588
+ /** T4/R6 (ADR-6 §3): SPEC-EVIDENCE coverage report. Present only when the
589
+ task carried specSnapshots; non-strict gaps show as badge:"unverified". */
590
+ specGate?: SpecGateResult;
433
591
  graph?: TaskGraphNode;
434
592
  adaptive?: {
435
593
  phase: string;
436
594
  task: string;
437
595
  };
596
+ /** T2/R4 (ADR-4 §3): the plan item this task implements. Set by producers
597
+ * when they create tasks from PlanRecord items; the scheduler reads it at
598
+ * dispatch to link `items[].taskIds` (single writer, run-locked). */
599
+ planItem?: string;
600
+ /** T2/R4 (ADR-4 §4): set when the wrap-up advisory was delivered because the
601
+ * item was dropped by a re-plan (soft cancel; exactly-once across ticks).
602
+ * Doubles as the terminal marker "cancelled-by-replan". */
603
+ replanDroppedAt?: string;
604
+ /** T3/R5 (ADR-5 §3): delegation depth of THIS task. Workers are depth 1;
605
+ * delegate-spawned grandchildren are depth 2+. The spawn policy computes
606
+ * a grandchild's depth from this record field — NEVER from the requesting
607
+ * worker's env or self-report (design §7 rev-2 P0-2). Additive: absent on
608
+ * pre-v2 records (readers treat absent as depth 1). */
609
+ depth?: number;
610
+ /** T3/R5 (ADR-5 §5): per-task budget allocation for delegate accounting.
611
+ * `tokensGranted` is reserved at delegate admission; `tokensSpent` rolls up
612
+ * grandchild usage as events arrive (single writer, run lock). Additive. */
613
+ allocation?: {
614
+ tokensGranted: number;
615
+ tokensSpent: number;
616
+ };
438
617
  policy?: {
439
618
  retryCount?: number;
440
619
  lastDecision?: PolicyDecision;
@@ -457,6 +636,24 @@ export interface TeamTaskState {
457
636
  /** Steering messages queued before the task's session was ready.
458
637
  * Delivered when the session initializes (mirrors pi-subagents3 pendingSteers pattern). */
459
638
  pendingSteers?: string[];
639
+
640
+ /** WP-2/R2 (ADR-0 docs/decisions/2026-08-17-waiting-producer-ask.md item 3):
641
+ * park marker set while the worker is blocked in the `ask` tool awaiting a
642
+ * leader answer. Purely additive — `status` carries "waiting" for the whole
643
+ * park and the parked tool's terminal report flips it back via the normal
644
+ * task lifecycle. */
645
+ waiting?: {
646
+ /** Correlation id (randomUUID) — matches manifest.waitState.questionId and
647
+ * the mailbox `kind:"response"` entry carrying the answer. */
648
+ questionId: string;
649
+ /** ISO timestamp when the park was accepted (broker wait.request). */
650
+ askedAt: string;
651
+ /** Ms-epoch answer deadline. Server-clamped root-side by the broker to
652
+ * now + min(timeoutSec, 3600) — worker-controlled values never exceed 1h. */
653
+ deadline: number;
654
+ /** Optional answer choices the worker presented with the question. */
655
+ options?: string[];
656
+ };
460
657
  }
461
658
 
462
659
  export interface ControlReservation {
@@ -0,0 +1,136 @@
1
+ /**
2
+ * plan-pane.ts — dashboard pane 7 "Plan" (WP-7 / R7, H4).
3
+ *
4
+ * Tree: phase → item → tasks, with per-item progress derived from linked
5
+ * tasks (deriveItemProgress) and a depth badge on grandchild tasks (depth>1,
6
+ * T3/R5). Approval pending → `A approve · n deny` hint line (same actions the
7
+ * progress pane exposes — plan-approve/plan-deny are pane-scoped to BOTH
8
+ * panes in keybinding-map). `X` toggles the multi-revision diff view
9
+ * (pane-scoped; V collides with root liveConversation, e with root events —
10
+ * X verified free).
11
+ *
12
+ * Flag-off: the dashboard never mounts this pane (key 7 still switches, but
13
+ * the snapshot carries no plans slice — the pane degrades to a hint line).
14
+ * Uncolored by design, mirroring the other dashboard panes.
15
+ */
16
+
17
+ import { isPlanApprovalPending } from "../../runtime/plan-approval.ts";
18
+ import { deriveItemProgress } from "../../state/stores/plan-store.ts";
19
+ import type { PlanItemStatus, TeamTaskState } from "../../state/types.ts";
20
+ import type { RunUiSnapshot } from "../snapshot-types.ts";
21
+
22
+ const ITEM_GLYPH: Record<PlanItemStatus, string> = {
23
+ pending: "○",
24
+ active: "▸",
25
+ done: "✓",
26
+ dropped: "✗",
27
+ };
28
+
29
+ const TASK_GLYPH: Record<string, string> = {
30
+ queued: "○",
31
+ running: "▸",
32
+ waiting: "◷",
33
+ needs_attention: "⚠",
34
+ completed: "✓",
35
+ failed: "✗",
36
+ cancelled: "⊘",
37
+ skipped: "·",
38
+ };
39
+
40
+ function taskLine(task: TeamTaskState, indent: string): string {
41
+ const glyph = TASK_GLYPH[task.status] ?? "?";
42
+ // T3/R5 depth badge: grandchildren (depth 2+) surface explicitly so the
43
+ // tree shows delegation nesting without another pane.
44
+ const depth = typeof task.depth === "number" && task.depth > 1 ? ` d${task.depth}` : "";
45
+ const role = task.displayName ?? task.role;
46
+ return `${indent}${glyph} ${task.id}${depth} ${role} [${task.status}]`;
47
+ }
48
+
49
+ export interface PlanPaneOptions {
50
+ /** X-toggled multi-revision diff view (current vs previous revision). */
51
+ diff?: boolean;
52
+ }
53
+
54
+ /** Item-level diff between the current revision and its predecessor. */
55
+ export function planRevisionDiff(snapshot: RunUiSnapshot): string[] {
56
+ const records = snapshot.plans ?? [];
57
+ // Current = highest-version record in the SNAPSHOT slice (in-memory truth —
58
+ // the pane must not re-read disk for what it already has).
59
+ const current = records.length ? records.reduce((a, b) => (b.version > a.version ? b : a)) : undefined;
60
+ if (!current) return ["Plan diff: no plan records"];
61
+ const prevVersion = current.revisionOf?.version;
62
+ const previous = prevVersion !== undefined ? records.find((r) => r.version === prevVersion) : undefined;
63
+ if (!previous) {
64
+ return [`Plan diff: v${current.version} has no prior revision`];
65
+ }
66
+ const prevItems = new Map(previous.items.map((i) => [i.id, i]));
67
+ const lines = [`Plan diff: v${previous.version} → v${current.version}`];
68
+ for (const item of current.items) {
69
+ const before = prevItems.get(item.id);
70
+ if (!before) {
71
+ lines.push(` + ${item.id} ${item.title} [${item.status}]`);
72
+ continue;
73
+ }
74
+ if (before.status !== item.status || before.taskIds.length !== item.taskIds.length) {
75
+ lines.push(
76
+ ` ~ ${item.id} ${item.title} [${before.status}→${item.status} · ${before.taskIds.length}→${item.taskIds.length} tasks]`,
77
+ );
78
+ }
79
+ prevItems.delete(item.id);
80
+ }
81
+ for (const dropped of prevItems.values()) {
82
+ lines.push(` - ${dropped.id} ${dropped.title} (dropped in v${current.version})`);
83
+ }
84
+ return lines;
85
+ }
86
+
87
+ export function renderPlanPane(snapshot: RunUiSnapshot, options: PlanPaneOptions = {}): string[] {
88
+ const records = snapshot.plans;
89
+ if (!records || records.length === 0) {
90
+ // Flag-off or plan-less run — one honest line, no I/O.
91
+ return ["Plan pane: no plan records (PI_CREW_PLAN_UI=1; plan-producing runs only)"];
92
+ }
93
+ if (options.diff) return planRevisionDiff(snapshot);
94
+
95
+ // Highest-version record in the snapshot slice (no disk re-read).
96
+ const current = records.reduce((a, b) => (b.version > a.version ? b : a));
97
+ if (!current) return ["Plan pane: no plan records"];
98
+ const progress = deriveItemProgress(current, snapshot.tasks);
99
+ const tasksById = new Map(snapshot.tasks.map((t) => [t.id, t]));
100
+ const itemById = new Map(current.items.map((i) => [i.id, i]));
101
+
102
+ const pending = isPlanApprovalPending(snapshot.manifest);
103
+ const header = `Plan pane: ${current.title} @v${current.version} (${current.phases.length} phases · ${current.items.length} items)`;
104
+ const approval = pending ? ["⚠ plan approval pending — A approve · n deny"] : [];
105
+
106
+ const lines: string[] = [header, ...approval];
107
+ for (const phase of current.phases) {
108
+ const glyph = ITEM_GLYPH[phase.status] ?? "?";
109
+ lines.push(`${glyph} ${phase.title}`);
110
+ for (const itemId of phase.itemIds) {
111
+ const item = itemById.get(itemId);
112
+ if (!item) continue;
113
+ const p = progress.get(itemId);
114
+ const counts = p ? ` ${p.done}/${p.total}${p.failed ? ` ✗${p.failed}` : ""}${p.running ? ` ▸${p.running}` : ""}` : "";
115
+ const droppedTag = item.status === "dropped" ? " ✗ dropped" : "";
116
+ lines.push(` ${ITEM_GLYPH[item.status] ?? "?"} ${item.title}${counts}${droppedTag}`);
117
+ for (const taskId of item.taskIds) {
118
+ const task = tasksById.get(taskId);
119
+ if (task) lines.push(taskLine(task, " "));
120
+ }
121
+ }
122
+ }
123
+ // Items not linked to any phase (producer-free-format plans) — still visible.
124
+ const phased = new Set(current.phases.flatMap((p) => p.itemIds));
125
+ const orphans = current.items.filter((i) => !phased.has(i.id));
126
+ if (orphans.length) {
127
+ lines.push("(unphased)");
128
+ for (const item of orphans) {
129
+ const p = progress.get(item.id);
130
+ const droppedTag = item.status === "dropped" ? " ✗ dropped" : "";
131
+ lines.push(` ${ITEM_GLYPH[item.status] ?? "?"} ${item.title}${p ? ` ${p.done}/${p.total}` : ""}${droppedTag}`);
132
+ }
133
+ }
134
+ lines.push("X revision diff");
135
+ return lines;
136
+ }
@@ -1,4 +1,5 @@
1
1
  import { computePhaseProgress, formatPhaseProgressLine } from "../../runtime/phase-progress.ts";
2
+ import { isPlanApprovalPending } from "../../runtime/plan-approval.ts";
2
3
  import { renderDwfPhaseLines } from "../dwf-phase-display.ts";
3
4
  import type { RunUiSnapshot } from "../snapshot-types.ts";
4
5
 
@@ -23,8 +24,13 @@ export function renderProgressPane(snapshot: RunUiSnapshot | undefined): string[
23
24
  // DWF logical phases (round-15 P1-4): derived from dwf.phase_* events.
24
25
  // Null/absent for non-DWF runs → zero visible change.
25
26
  const dwfPhaseLines = snapshot.dwfPhaseState ? renderDwfPhaseLines(snapshot.dwfPhaseState) : [];
27
+ // WP-3 (H4-subset): plan-approval gate banner. Mirrors the health-pane
28
+ // hint pattern — plain foreground text, no color codes (pane output is
29
+ // uncolored by design). One line while the run is parked on approval.
30
+ const planBanner = isPlanApprovalPending(snapshot.manifest) ? ["⚠ plan approval pending — A approve / n deny"] : [];
26
31
  return [
27
32
  `Progress pane: ${progress.completed}/${progress.total} completed · running=${progress.running} queued=${progress.queued} failed=${progress.failed}`,
33
+ ...planBanner,
28
34
  ...dwfPhaseLines,
29
35
  ...phaseHeader,
30
36
  ...cancellationLine,
@@ -1,9 +1,40 @@
1
1
  import type { RunUiSnapshot } from "../snapshot-types.ts";
2
2
 
3
+ /**
4
+ * Transcript pane (pane 4). WP-8 (R8): per-attempt model transparency —
5
+ * tasks that burned fallback attempts show which model each attempt used
6
+ * (`requested ✓` / `failed ✗ → next`), newest first, capped to the pane
7
+ * budget. Rendering stays uncolored (pane convention); status glyphs are
8
+ * colorized by the shared colorizeStatusGlyphs pass.
9
+ */
10
+
11
+ const ATTEMPT_SUMMARY_TASKS = 3;
12
+
13
+ function modelAttemptLines(snapshot: RunUiSnapshot): string[] {
14
+ const withAttempts = snapshot.tasks
15
+ .filter((task) => (task.modelAttempts?.length ?? 0) > 0)
16
+ .slice(-ATTEMPT_SUMMARY_TASKS)
17
+ .reverse();
18
+ if (!withAttempts.length) return [];
19
+ const lines = ["model attempts (newest first):"];
20
+ for (const task of withAttempts) {
21
+ const attempts = (task.modelAttempts ?? [])
22
+ .map(
23
+ (attempt) =>
24
+ `${attempt.model} ${attempt.success ? "✓" : `✗${attempt.exitCode !== undefined ? `(${attempt.exitCode})` : ""}`}`,
25
+ )
26
+ .join(" → ");
27
+ const resolved = task.modelRouting?.resolved ? ` · resolved ${task.modelRouting.resolved}` : "";
28
+ lines.push(` ${task.id} (${task.role})${resolved}: ${attempts}`);
29
+ }
30
+ return lines;
31
+ }
32
+
3
33
  export function renderTranscriptPane(snapshot: RunUiSnapshot | undefined): string[] {
4
34
  if (!snapshot) return ["Output pane: snapshot unavailable"];
5
35
  return [
6
36
  `Output pane: ${snapshot.recentOutputLines.length} recent lines · press v for transcript viewer · o for raw output`,
37
+ ...modelAttemptLines(snapshot),
7
38
  ...snapshot.recentOutputLines.slice(-12).map((line) => `⎿ ${line}`),
8
39
  ...(snapshot.recentOutputLines.length ? [] : ["No recent output"]),
9
40
  ];
@@ -1,5 +1,6 @@
1
1
  import type { MetricRegistry } from "../observability/metric-registry.ts";
2
2
  import { classifyHeartbeat, heartbeatAgeMs } from "../runtime/heartbeat/heartbeat-gradient.ts";
3
+ import { isTerminalRunStatus } from "../state/contracts.ts";
3
4
  import type { TeamTaskState } from "../state/types.ts";
4
5
  import type { RunUiSnapshot } from "./snapshot-types.ts";
5
6
 
@@ -45,8 +46,15 @@ export function summarizeHeartbeats(snapshot: RunUiSnapshot, opts: HeartbeatSumm
45
46
  worstStaleMs: 0,
46
47
  gradient: { healthy: 0, warn: 0, stale: 0, dead: 0 },
47
48
  };
49
+ // bug-026 sub-issue C: a terminal run must never report dead/missing/stale
50
+ // workers. isActiveTask below already skips terminal tasks (the primary
51
+ // task-level gate — locked in by regression tests), but a stale snapshot can
52
+ // carry "running" task statuses that lag the run manifest's terminal
53
+ // transition. Defense-in-depth: skip ALL task counting when the run manifest
54
+ // itself is terminal. Runs with a non-terminal manifest are unaffected.
55
+ const runTerminal = isTerminalRunStatus(snapshot.manifest.status);
48
56
  for (const task of snapshot.tasks) {
49
- if (!isActiveTask(task)) continue;
57
+ if (runTerminal || !isActiveTask(task)) continue;
50
58
  const heartbeat = task.heartbeat;
51
59
  if (!heartbeat) {
52
60
  summary.missing += 1;
@@ -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;