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.
- package/CHANGELOG.md +124 -0
- package/agents/analyst.md +1 -1
- package/agents/cold-verifier.md +3 -1
- package/agents/critic.md +1 -1
- package/agents/executor.md +1 -1
- package/agents/explorer.md +1 -1
- package/agents/planner.md +1 -1
- package/agents/reviewer.md +1 -1
- package/agents/security-reviewer.md +1 -1
- package/agents/test-engineer.md +1 -1
- package/agents/verifier.md +1 -1
- package/agents/writer.md +1 -1
- package/dist/index.mjs +65838 -60331
- package/docs/README.md +2 -0
- package/docs/actions-reference.md +31 -0
- package/docs/commands-reference.md +17 -6
- package/docs/resource-formats.md +13 -0
- package/package.json +4 -2
- package/schema.json +15 -1
- package/scripts/resource-sampler.mjs +36 -2
- package/skills/requirements-to-task-packet/SKILL.md +26 -0
- package/skills/widget-rendering/SKILL.md +7 -7
- package/src/agents/agent-config.ts +2 -1
- package/src/agents/discover-agents.ts +23 -14
- package/src/config/config-merge.ts +183 -0
- package/src/config/config-validation.ts +681 -0
- package/src/config/config.ts +22 -864
- package/src/config/defaults.ts +35 -1
- package/src/config/drift-detector.ts +1 -1
- package/src/config/env-vars.ts +691 -0
- package/src/config/role-tools.ts +8 -8
- package/src/config/sanitize-project-config.ts +172 -0
- package/src/config/types.ts +30 -0
- package/src/extension/async-notifier.ts +25 -2
- package/src/extension/crew-vibes/config.ts +2 -1
- package/src/extension/plan-orchestrate.ts +132 -0
- package/src/extension/registration/commands/dashboard.ts +158 -0
- package/src/extension/registration/commands/index.ts +35 -0
- package/src/extension/registration/commands/manage.ts +303 -0
- package/src/extension/registration/commands/run.ts +214 -0
- package/src/extension/registration/commands/shared.ts +631 -0
- package/src/extension/registration/commands/status.ts +60 -0
- package/src/extension/registration/commands.ts +13 -1224
- package/src/extension/registration/lifecycle-handlers.ts +126 -15
- package/src/extension/registration/subagent-tools.ts +218 -9
- package/src/extension/rpc-hmac.ts +5 -3
- package/src/extension/team-tool/api/heartbeat.ts +47 -10
- package/src/extension/team-tool/api/plan-approval.ts +9 -0
- package/src/extension/team-tool/api/task-claims.ts +109 -40
- package/src/extension/team-tool/cancel.ts +84 -50
- package/src/extension/team-tool/dispatch/index.ts +1 -0
- package/src/extension/team-tool/dispatch/run.ts +4 -1
- package/src/extension/team-tool/doctor.ts +103 -1
- package/src/extension/team-tool/orchestrate.ts +66 -1
- package/src/extension/team-tool/plans.ts +192 -0
- package/src/extension/team-tool/respond.ts +197 -65
- package/src/extension/team-tool/run-deadline.ts +16 -1
- package/src/extension/team-tool/run-intent.ts +63 -0
- package/src/extension/team-tool/run.ts +48 -18
- package/src/extension/team-tool/status.ts +84 -26
- package/src/extension/team-tool.ts +11 -2
- package/src/hooks/registry.ts +1 -6
- package/src/i18n.ts +9 -0
- package/src/prompt/prompt-runtime.ts +521 -2
- package/src/prompt/worker-events-channel.ts +173 -0
- package/src/runtime/README.md +8 -8
- package/src/runtime/async-runner.ts +7 -3
- package/src/runtime/background-runner.ts +42 -14
- package/src/runtime/broker/broker-issuer.ts +9 -2
- package/src/runtime/broker/crew-broker-tokens.ts +43 -6
- package/src/runtime/broker/crew-broker.ts +838 -10
- package/src/runtime/broker/wait-status-cache.ts +157 -0
- package/src/runtime/budget-enforcement.ts +281 -0
- package/src/runtime/child-pi/child-pi-spawn.ts +60 -14
- package/src/runtime/child-pi/child-pi-timers.ts +324 -0
- package/src/runtime/child-pi/child-pi.ts +97 -201
- package/src/runtime/child-pi/mock-fixtures.ts +16 -2
- package/src/runtime/crew-agent-records.ts +259 -14
- package/src/runtime/delegate-spawn.ts +148 -0
- package/src/runtime/deterministic-ast.ts +2 -1
- package/src/runtime/dispatch-batch.ts +945 -0
- package/src/runtime/finalize-run.ts +557 -0
- package/src/runtime/goal-workflow/adaptive-plan.ts +87 -13
- package/src/runtime/goal-workflow/dynamic-workflow-runner.ts +3 -2
- package/src/runtime/goal-workflow/goal-state-store.ts +1 -1
- package/src/runtime/group-join.ts +11 -125
- package/src/runtime/live-session/live-session-runtime.ts +26 -1
- package/src/runtime/merge-gate.ts +7 -1
- package/src/runtime/merge-loop.ts +130 -0
- package/src/runtime/model/model-budget-summary.ts +53 -0
- package/src/runtime/model/model-fallback.ts +32 -2
- package/src/runtime/model/pi-args.ts +10 -0
- package/src/runtime/model/provider-extensions.ts +10 -0
- package/src/runtime/orphan-worker-registry.ts +1 -1
- package/src/runtime/output/output-validator.ts +45 -0
- package/src/runtime/parent-guard.ts +3 -1
- package/src/runtime/peer-dep.ts +2 -1
- package/src/runtime/per-write-validator.ts +0 -5
- package/src/runtime/pi-spawn.ts +61 -15
- package/src/runtime/plan-approval.ts +125 -0
- package/src/runtime/plan-replan.ts +151 -0
- package/src/runtime/recovery/checkpoint.ts +0 -18
- package/src/runtime/recovery/crash-recovery.ts +95 -46
- package/src/runtime/scheduler-context.ts +98 -0
- package/src/runtime/scheduling/coalesce-tasks.ts +5 -0
- package/src/runtime/scheduling/global-worker-cap.ts +2 -1
- package/src/runtime/scheduling/nested-slots.ts +70 -0
- package/src/runtime/scheduling/run-coalesced-task-group.ts +64 -13
- package/src/runtime/scheduling/task-graph-scheduler.ts +0 -10
- package/src/runtime/settings-store.ts +219 -0
- package/src/runtime/spawn-policy.ts +217 -0
- package/src/runtime/stale-reconciler.ts +87 -6
- package/src/runtime/subagent-manager.ts +25 -1
- package/src/runtime/task-output-context.ts +230 -9
- package/src/runtime/task-packet.ts +23 -1
- package/src/runtime/task-runner/child-executor.ts +106 -7
- package/src/runtime/task-runner/post-execution.ts +125 -1
- package/src/runtime/task-runner/pre-execution.ts +39 -1
- package/src/runtime/task-runner/prompt-builder.ts +51 -1
- package/src/runtime/task-runner/retrieval-orchestrator.ts +72 -18
- package/src/runtime/task-runner/spec-evidence.ts +403 -0
- package/src/runtime/task-runner/state-helpers.ts +26 -24
- package/src/runtime/task-runner.ts +11 -0
- package/src/runtime/team-runner.ts +129 -1671
- package/src/runtime/verification/spec-sandbox.ts +255 -0
- package/src/runtime/verification/verification-gates.ts +3 -2
- package/src/runtime/verification/verification-worktree.ts +2 -1
- package/src/runtime/workflow-phase-advance.ts +100 -0
- package/src/runtime/workspace-tree.ts +9 -0
- package/src/schema/config-schema.ts +65 -25
- package/src/schema/sensitive-config-paths.ts +64 -0
- package/src/schema/team-tool-schema.ts +13 -3
- package/src/state/README.md +4 -10
- package/src/state/atomic-write.ts +20 -3
- package/src/state/contracts.ts +38 -0
- package/src/state/coordination/mailbox.ts +12 -2
- package/src/state/event-log/cursor.ts +223 -0
- package/src/state/event-log/event-log-rotation.ts +12 -4
- package/src/state/event-log/event-log.ts +152 -369
- package/src/state/event-log/sequence-cache.ts +373 -0
- package/src/state/event-log/worker-atomic-writer.ts +2 -1
- package/src/state/stores/active-run-registry.ts +3 -2
- package/src/state/stores/manifest-io.ts +237 -0
- package/src/state/stores/ownership-map.ts +162 -0
- package/src/state/stores/plan-store.ts +241 -0
- package/src/state/stores/run-cache.ts +0 -90
- package/src/state/stores/spec-store.ts +189 -0
- package/src/state/stores/state-store.ts +66 -229
- package/src/state/types.ts +197 -0
- package/src/ui/dashboard-panes/plan-pane.ts +136 -0
- package/src/ui/dashboard-panes/progress-pane.ts +6 -0
- package/src/ui/dashboard-panes/transcript-pane.ts +31 -0
- package/src/ui/heartbeat-aggregator.ts +9 -1
- package/src/ui/keybinding-map.ts +54 -13
- package/src/ui/powerbar-publisher.ts +52 -1
- package/src/ui/run-dashboard.ts +31 -5
- package/src/ui/run-snapshot-cache.ts +57 -30
- package/src/ui/snapshot-types.ts +6 -1
- package/src/ui/widget/widget-renderer.ts +9 -1
- package/src/utils/file-coalescer.ts +0 -4
- package/src/utils/fs-errno.ts +66 -0
- package/src/utils/fs-watch.ts +1 -1
- package/src/utils/internal-error.ts +3 -1
- package/src/utils/paths.ts +11 -3
- package/src/utils/task-name-generator.ts +1 -8
- package/src/workflows/discover-workflows.ts +19 -2
- package/src/workflows/validate-workflow.ts +7 -1
- package/src/workflows/workflow-config.ts +10 -0
- package/src/workflows/workflow-serializer.ts +3 -0
- package/src/worktree/worktree-manager.ts +22 -0
- package/src/agents/agent-search.ts +0 -98
- package/src/benchmark/benchmark-runner.ts +0 -313
- package/src/benchmark/feedback-loop.ts +0 -73
- package/src/config/resilient-parser.ts +0 -117
- package/src/extension/crew-vibes/cat-frames.ts +0 -18
- package/src/extension/result-watcher.ts +0 -139
- package/src/observability/exporters/prometheus-exporter.ts +0 -54
- package/src/observability/metric-retention.ts +0 -64
- package/src/runtime/compaction/compaction-summary.ts +0 -278
- package/src/runtime/errors/crew-errors.ts +0 -162
- package/src/runtime/live-session/intercom-bridge.ts +0 -187
- package/src/runtime/loop-gates.ts +0 -128
- package/src/runtime/metric-parser.ts +0 -36
- package/src/runtime/output/stream-preview.ts +0 -184
- package/src/runtime/output/tool-progress.ts +0 -278
- package/src/runtime/phase-tracker.ts +0 -385
- package/src/runtime/pipeline-runner.ts +0 -523
- package/src/runtime/process/process-lifecycle.ts +0 -491
- package/src/runtime/recovery/retry-runner.ts +0 -330
- package/src/runtime/run-drift.ts +0 -219
- package/src/runtime/task-quality.ts +0 -199
- package/src/runtime/task-runner/run-projection.ts +0 -128
- package/src/runtime/verification/post-checks.ts +0 -142
- package/src/state/coordination/schedule.ts +0 -166
- package/src/state/event-log/jsonl-writer.ts +0 -115
- package/src/state/hook-instinct-bridge.ts +0 -94
- package/src/state/hook-integrations.ts +0 -51
- package/src/state/session-state-map.ts +0 -51
- package/src/state/stores/blob-store.ts +0 -308
- package/src/state/stores/instinct-store.ts +0 -275
- package/src/state/stores/observation-store.ts +0 -176
- package/src/state/tiered-eval.ts +0 -480
- package/src/state/types-eval.ts +0 -58
- package/src/tools/safe-bash-extension.ts +0 -54
- package/src/tools/safe-bash.ts +0 -505
- package/src/ui/agent-management-overlay.ts +0 -160
- package/src/ui/crew-footer.ts +0 -102
- package/src/ui/crew-select-list.ts +0 -114
- package/src/ui/dashboard-panes/capability-pane.ts +0 -77
- package/src/ui/transcript-entries.ts +0 -256
- package/src/utils/conflict-detect.ts +0 -721
- package/src/utils/fingerprint.ts +0 -180
- package/src/utils/gh-protocol.ts +0 -556
- package/src/utils/project-detector.ts +0 -160
- package/src/utils/sse-parser.ts +0 -131
- package/src/workflows/cost-estimator.ts +0 -34
- package/src/workflows/intermediate-store.ts +0 -166
package/src/state/types.ts
CHANGED
|
@@ -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;
|
package/src/ui/keybinding-map.ts
CHANGED
|
@@ -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
|
|
85
|
-
readonly pane?:
|
|
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
|
|
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
|
|
134
|
-
* reservation but are handled by the mailbox
|
|
135
|
-
* not by the dashboard dispatch.
|
|
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
|
-
|
|
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
|
-
|
|
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 =
|
|
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 =
|
|
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
|
|
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
|
|
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;
|