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
@@ -1,12 +1,14 @@
1
1
  import { createHash } from "node:crypto";
2
2
  import * as fs from "node:fs";
3
3
  import * as path from "node:path";
4
+ import { getCrewEnv } from "../config/env-vars.ts";
4
5
  import { agentOutputPath, agentsPath, readCrewAgents, readCrewAgentsAsync } from "../runtime/crew-agent-records.ts";
5
6
  import type { CrewAgentRecord } from "../runtime/crew-agent-runtime.ts";
6
7
  import { isActiveRunStatus } from "../runtime/process-status.ts";
7
8
  import type { MailboxMessageStatus } from "../state/coordination/mailbox.ts";
8
9
  import type { TeamEvent } from "../state/event-log/event-log.ts";
9
10
  import { sequencePath } from "../state/event-log/event-log.ts";
11
+ import { loadPlanRecords, planFilePath } from "../state/stores/plan-store.ts";
10
12
  import { loadRunManifestById, loadRunManifestByIdAsync } from "../state/stores/state-store.ts";
11
13
  import type { TeamRunManifest, TeamTaskState } from "../state/types.ts";
12
14
  import { extractDwfPhaseState } from "./dwf-phase-display.ts";
@@ -25,6 +27,12 @@ export interface RunSnapshotCache extends RunSnapshotCacheBase {
25
27
  preloadAllStale(runIds: string[]): Promise<void>;
26
28
  }
27
29
 
30
+ /** WP-7 (R7): the plans slice + Plan pane load only when this flag is set —
31
+ * flag-off keeps the snapshot build byte-identical (zero extra I/O). */
32
+ export function isPlanUiEnabled(): boolean {
33
+ return getCrewEnv("PI_CREW_PLAN_UI") === "1";
34
+ }
35
+
28
36
  const DEFAULT_TTL_MS = 1500;
29
37
  const DEFAULT_MAX_ENTRIES = 24;
30
38
  const DEFAULT_RECENT_EVENTS = 20;
@@ -44,6 +52,8 @@ interface SnapshotStamps {
44
52
  agents: FileStamp;
45
53
  events: FileStamp;
46
54
  mailbox: FileStamp;
55
+ /** WP-7 (R7): present only when PI_CREW_PLAN_UI=1. */
56
+ plans?: FileStamp;
47
57
  }
48
58
 
49
59
  interface CacheEntry {
@@ -163,15 +173,10 @@ function safeAgentOutputPath(manifest: TeamRunManifest, agent: CrewAgentRecord):
163
173
  }
164
174
  }
165
175
 
166
- function outputStamp(manifest: TeamRunManifest, agents: CrewAgentRecord[]): FileStamp {
167
- return combineStamps(agents.map((agent) => stampFile(safeAgentOutputPath(manifest, agent))));
168
- }
169
-
170
- async function outputStampAsync(manifest: TeamRunManifest, agents: CrewAgentRecord[]): Promise<FileStamp> {
171
- return combineStamps(await Promise.all(agents.map((agent) => stampFileAsync(safeAgentOutputPath(manifest, agent)))));
172
- }
173
-
174
- function sameStamp(a: FileStamp, b: FileStamp): boolean {
176
+ function sameStamp(a: FileStamp | undefined, b: FileStamp | undefined): boolean {
177
+ // Optional stamps (WP-7 plans): absent on both sides = unchanged; absent
178
+ // on one side = the flag flipped — force a rebuild.
179
+ if (a === undefined || b === undefined) return a === b;
175
180
  return a.mtimeMs === b.mtimeMs && a.size === b.size;
176
181
  }
177
182
 
@@ -181,19 +186,11 @@ function sameStamps(a: SnapshotStamps, b: SnapshotStamps): boolean {
181
186
  sameStamp(a.tasks, b.tasks) &&
182
187
  sameStamp(a.agents, b.agents) &&
183
188
  sameStamp(a.events, b.events) &&
184
- sameStamp(a.mailbox, b.mailbox)
189
+ sameStamp(a.mailbox, b.mailbox) &&
190
+ sameStamp(a.plans, b.plans)
185
191
  );
186
192
  }
187
193
 
188
- function readTasks(tasksPath: string): TeamTaskState[] {
189
- try {
190
- const parsed = JSON.parse(fs.readFileSync(tasksPath, "utf-8")) as unknown;
191
- return Array.isArray(parsed) ? (parsed as TeamTaskState[]) : [];
192
- } catch {
193
- throw new Error(`Failed to parse tasks at ${tasksPath}`);
194
- }
195
- }
196
-
197
194
  /** Tail-read JSONL lines from a file, returning parsed objects (limited). */
198
195
  function tailJsonlLines<T>(filePath: string, limit: number, parse: (line: string) => T | undefined): T[] {
199
196
  if (limit <= 0) return [];
@@ -711,6 +708,21 @@ function computeSliceSignatures(input: Omit<RunUiSnapshot, "signature" | "fetche
711
708
  event.data?.reason,
712
709
  ]),
713
710
  ),
711
+ ...(input.plans
712
+ ? {
713
+ // WP-7 (R7): plan writes (revision append / approval flip / item
714
+ // linkage) must invalidate the Plan pane. Plans live outside the
715
+ // stamped files' content — the slice hashes the records directly.
716
+ plans: hash(
717
+ input.plans.map((record) => [
718
+ record.version,
719
+ record.approval?.status,
720
+ record.items.map((item) => [item.id, item.status, item.taskIds]),
721
+ record.phases.map((p) => [p.id, p.status]),
722
+ ]),
723
+ ),
724
+ }
725
+ : {}),
714
726
  };
715
727
  }
716
728
 
@@ -723,7 +735,21 @@ function signatureFor(
723
735
  const digest = createHash("sha256");
724
736
  digest.update(
725
737
  JSON.stringify({
726
- run: [input.manifest.runId, input.manifest.status, input.manifest.updatedAt, input.manifest.artifacts.length],
738
+ // WP-3 (H4): planApproval.status is a RUN-level field surfaced by the
739
+ // widget badge / progress banner / powerbar segment — pending→approved
740
+ // must flip the signature so per-run render caches (run-dashboard keys
741
+ // on snapshot.signature) invalidate. `undefined` serializes as null,
742
+ // keeping the array shape stable for older manifests without the field.
743
+ run: [
744
+ input.manifest.runId,
745
+ input.manifest.status,
746
+ input.manifest.updatedAt,
747
+ input.manifest.artifacts.length,
748
+ input.manifest.planApproval?.status,
749
+ // T2/R4 (ADR-4 §2): a re-plan (revision switch) or record-side
750
+ // approval flip must invalidate per-run render caches too.
751
+ input.manifest.plan?.version ?? null,
752
+ ],
727
753
  tasks: sliceSignatures.tasks,
728
754
  agents: sliceSignatures.agents,
729
755
  progress: input.progress,
@@ -731,6 +757,7 @@ function signatureFor(
731
757
  mailbox: input.mailbox,
732
758
  groupJoins: input.groupJoins,
733
759
  events: sliceSignatures.events,
760
+ ...(sliceSignatures.plans ? { plans: sliceSignatures.plans } : {}),
734
761
  cancellationReason: input.cancellationReason,
735
762
  dwfPhaseState: input.dwfPhaseState,
736
763
  output: input.recentOutputLines,
@@ -744,16 +771,8 @@ function signatureFor(
744
771
  }
745
772
  }
746
773
 
747
- /**
748
- * 1.6 / 1.7 — compute one short hash per logical slice of the snapshot so
749
- * dashboard panes / widget can short-circuit when their slice hasn't moved.
750
- * The slice contents must mirror what `signatureFor` packs into each branch.
751
- */
752
- function sliceSignaturesFor(sliceSignatures: SliceSignatures): SliceSignatures {
753
- return sliceSignatures;
754
- }
755
-
756
774
  function stampsFor(manifest: TeamRunManifest, _agents: CrewAgentRecord[]): SnapshotStamps {
775
+ // WP-7: optional plans stamp — one extra statSync, flag-gated.
757
776
  // 1.4: use events sequence file instead of stat-ing the events log directly.
758
777
  // 1.5: drop per-agent output.log stamping; rely on event-bus invalidation
759
778
  // (`crew.subagent.*` and stream events) and on agents.json mtime which
@@ -765,6 +784,7 @@ function stampsFor(manifest: TeamRunManifest, _agents: CrewAgentRecord[]): Snaps
765
784
  agents: stampFile(agentsPath(manifest)),
766
785
  events: eventsStamp(manifest.eventsPath),
767
786
  mailbox: mailboxStamp(manifest),
787
+ ...(isPlanUiEnabled() ? { plans: stampFile(planFilePath(manifest)) } : {}),
768
788
  };
769
789
  }
770
790
 
@@ -834,7 +854,12 @@ export function createRunSnapshotCache(cwd: string, options: RunSnapshotCacheOpt
834
854
  let tasks: TeamTaskState[];
835
855
  let agents: CrewAgentRecord[];
836
856
  try {
837
- tasks = readTasks(loaded.manifest.tasksPath);
857
+ // R10-4 (docs/refactor-plan.review.md §ROUND 10): sync/async parity.
858
+ // loadRunManifestById already returns tasks validated against the
859
+ // current tasks.json (mtime+size+generation check in state-store),
860
+ // so the old readTasks() re-read only doubled tasks.json I/O per
861
+ // sync rebuild. buildAsync() has always used loaded.tasks.
862
+ tasks = loaded.tasks;
838
863
  agents = readCrewAgents(loaded.manifest);
839
864
  } catch {
840
865
  if (previous) return previous;
@@ -857,6 +882,7 @@ export function createRunSnapshotCache(cwd: string, options: RunSnapshotCacheOpt
857
882
  dwfPhaseState: extractDwfPhaseState(recentEvents),
858
883
  recentEvents,
859
884
  recentOutputLines: recentOutputLines(loaded.manifest, agents, recentOutputLimit),
885
+ ...(isPlanUiEnabled() ? { plans: loadPlanRecords(loaded.manifest) } : {}),
860
886
  };
861
887
  const stamps = stampsFor(loaded.manifest, agents);
862
888
  const sliceSignatures = computeSliceSignatures(base);
@@ -915,6 +941,7 @@ export function createRunSnapshotCache(cwd: string, options: RunSnapshotCacheOpt
915
941
  dwfPhaseState: extractDwfPhaseState(recentEvents),
916
942
  recentEvents,
917
943
  recentOutputLines: recentOutput,
944
+ ...(isPlanUiEnabled() ? { plans: loadPlanRecords(loaded.manifest) } : {}),
918
945
  };
919
946
  const stamps = await stampsForAsync(loaded.manifest, agents);
920
947
  const sliceSignatures = computeSliceSignatures(base);
@@ -1,6 +1,6 @@
1
1
  import type { CrewAgentRecord } from "../runtime/crew-agent-runtime.ts";
2
2
  import type { TeamEvent } from "../state/event-log/event-log.ts";
3
- import type { TeamRunManifest, TeamTaskState } from "../state/types.ts";
3
+ import type { PlanRecord, TeamRunManifest, TeamTaskState } from "../state/types.ts";
4
4
  import type { DwfPhaseState } from "./dwf-phase-display.ts";
5
5
 
6
6
  export interface RunUiProgress {
@@ -64,6 +64,8 @@ export interface RunUiSnapshot {
64
64
  mailbox: string;
65
65
  progress: string;
66
66
  events: string;
67
+ /** WP-7 (R7): present only when the plans slice is loaded (flag on). */
68
+ plans?: string;
67
69
  };
68
70
  manifest: TeamRunManifest;
69
71
  tasks: TeamTaskState[];
@@ -76,6 +78,9 @@ export interface RunUiSnapshot {
76
78
  cancellationReason?: string;
77
79
  /** DWF phase state derived from `recentEvents`. Null/absent for non-DWF runs. */
78
80
  dwfPhaseState?: DwfPhaseState | null;
81
+ /** WP-7 (R7): plan records for the Plan pane — populated ONLY when
82
+ * PI_CREW_PLAN_UI=1 (flag-off → field absent, zero extra I/O). */
83
+ plans?: PlanRecord[];
79
84
  recentEvents: TeamEvent[];
80
85
  recentOutputLines: string[];
81
86
  }
@@ -11,6 +11,9 @@ import { DEFAULT_UI } from "../../config/defaults.ts";
11
11
  import type { ManifestCache } from "../../runtime/manifest-cache.ts";
12
12
  import type { TeamRunManifest } from "../../state/types.ts";
13
13
  import { truncate } from "../../utils/visual.ts";
14
+ import { isFooterDockSinkActive, setFooterDockProvider } from "../dock-footer.ts";
15
+ import { panelRowsFromRuns } from "../inline-panel/panel-rows.ts";
16
+ import { panelDisplayState, setPanelRowsProvider, subscribePanelChange } from "../inline-panel/panel-store.ts";
14
17
  import { requestRender, requestRenderTarget, setExtensionWidget } from "../pi-ui-compat.ts";
15
18
  import type { OverlaySchedulerHandle } from "../shared-overlay-scheduler.ts";
16
19
  import { registerOverlayScheduler } from "../shared-overlay-scheduler.ts";
@@ -18,6 +21,7 @@ import type { RunSnapshotCache } from "../snapshot-types.ts";
18
21
  import { spinnerBucket, spinnerFrame } from "../spinner.ts";
19
22
  import type { CrewTheme } from "../theme-adapter.ts";
20
23
  import { asCrewTheme, subscribeThemeChange } from "../theme-adapter.ts";
24
+ import { buildTaskListLines } from "./task-list.ts";
21
25
  import { activeWidgetRuns, statusSummary } from "./widget-model.ts";
22
26
  import { buildWidgetLines, colorWidgetLine, DEFAULT_WIDGET_WIDTH, renderLines } from "./widget-renderer.ts";
23
27
  import type { CrewWidgetModel, CrewWidgetState, WidgetRun } from "./widget-types.ts";
@@ -62,6 +66,8 @@ export {
62
66
  const MAX_LINES_DEFAULT = DEFAULT_UI.widgetMaxLines;
63
67
  const LEGACY_WIDGET_KEY = "pi-crew";
64
68
  const WIDGET_KEY = "pi-crew-active";
69
+ /** The run's plan progress, painted ABOVE the editor (task-list.ts). */
70
+ const TASKS_WIDGET_KEY = "pi-crew-tasks";
65
71
  const STATUS_KEY = "pi-crew";
66
72
 
67
73
  /**
@@ -78,11 +84,12 @@ const SIGNATURE_CACHE_TTL_MS = 100;
78
84
  // next invalidate; a mid-run resize could briefly paint a frame at the old
79
85
  // width. We register ONE debounced process-level listener (guarded so it never
80
86
  // accumulates across widget reinstalls) that busts the active widget's cache
81
- // and pokes Pi to repaint at the new width. The listener references a
82
- // module-level `activeResizeTarget` (the most-recently-mounted widget) rather
83
- // than a specific instance, so replaced widgets do not leak listeners.
87
+ // and pokes Pi to repaint at the new width. The listener references the
88
+ // module-level `activeResizeTargets` set (every mounted widget) rather than
89
+ // specific instances, so replaced widgets do not leak listeners.
84
90
  let resizeListenerInstalled = false;
85
- let activeResizeTarget: { invalidate(): void; requestRepaint(): void } | undefined;
91
+ /** All mounted widgets (dock + task list) — every one gets resize-busted. */
92
+ const activeResizeTargets = new Set<{ invalidate(): void; requestRepaint(): void }>();
86
93
  let resizeTimer: ReturnType<typeof setTimeout> | undefined;
87
94
 
88
95
  /**
@@ -95,8 +102,10 @@ const onResize = (): void => {
95
102
  // Debounce (~120ms) so a drag-resize doesn't thrash renders.
96
103
  resizeTimer = setTimeout(() => {
97
104
  resizeTimer = undefined;
98
- activeResizeTarget?.invalidate();
99
- activeResizeTarget?.requestRepaint();
105
+ for (const target of activeResizeTargets) {
106
+ target.invalidate();
107
+ target.requestRepaint();
108
+ }
100
109
  }, 120);
101
110
  };
102
111
 
@@ -134,7 +143,7 @@ export function uninstallResizeListener(): void {
134
143
  clearTimeout(resizeTimer);
135
144
  resizeTimer = undefined;
136
145
  }
137
- activeResizeTarget = undefined;
146
+ activeResizeTargets.clear();
138
147
  process.off("SIGWINCH", onResize);
139
148
  // Windows has no SIGWINCH; Node emits "resize" on stdout instead.
140
149
  // Remove via whichever API is present. The listener was only registered
@@ -159,6 +168,8 @@ interface WidgetComponent extends CrewComponent {}
159
168
  class CrewWidgetComponent implements WidgetComponent {
160
169
  private readonly model: CrewWidgetModel;
161
170
  private theme: CrewTheme;
171
+ /** Which surface this instance paints: the agent dock, or the task list. */
172
+ private readonly variant: "dock" | "tasks";
162
173
  private cacheSignature = "";
163
174
  /** C4 — invalidate-on-write cache for the buildSignature() result. */
164
175
  private cachedBuildSignature = "";
@@ -169,10 +180,12 @@ class CrewWidgetComponent implements WidgetComponent {
169
180
  private cachedTheme: CrewTheme;
170
181
  private readonly tui: unknown;
171
182
  private readonly unsubscribeTheme: () => void;
183
+ private readonly unsubscribePanel: () => void;
172
184
  private readonly schedulerHandle: OverlaySchedulerHandle;
173
185
 
174
- constructor(model: CrewWidgetModel, themeLike: unknown, tui?: unknown) {
186
+ constructor(model: CrewWidgetModel, themeLike: unknown, tui?: unknown, variant: "dock" | "tasks" = "dock") {
175
187
  this.model = model;
188
+ this.variant = variant;
176
189
  this.theme = asCrewTheme(themeLike);
177
190
  this.cachedTheme = this.theme;
178
191
  this.tui = tui;
@@ -180,9 +193,16 @@ class CrewWidgetComponent implements WidgetComponent {
180
193
  // terminal-resize listener is installed. On a resize the cached width
181
194
  // goes stale; busting the cache + requesting a repaint refreshes the
182
195
  // widget at the new width without waiting for the next event tick (T-2).
183
- activeResizeTarget = this;
196
+ activeResizeTargets.add(this);
184
197
  installResizeListener();
185
198
  this.unsubscribeTheme = subscribeThemeChange(themeLike, () => this.invalidate());
199
+ // Cursor movement is a keypress, not a run event, so it never reaches the
200
+ // shared scheduler. Repaint directly instead of waiting for the next host
201
+ // tick — a lagging cursor reads as a dropped keystroke.
202
+ this.unsubscribePanel = subscribePanelChange(() => {
203
+ this.invalidate();
204
+ this.requestRepaint();
205
+ });
186
206
  // 1.10 (UI-P1-1): route run:state / worker:lifecycle / ui:invalidate
187
207
  // through a RenderScheduler (debounce + fallback) instead of three
188
208
  // direct runEventBus.onChannel subscriptions. With 3 overlays
@@ -265,8 +285,9 @@ class CrewWidgetComponent implements WidgetComponent {
265
285
 
266
286
  dispose(): void {
267
287
  this.unsubscribeTheme();
288
+ this.unsubscribePanel();
268
289
  this.schedulerHandle.dispose();
269
- if (activeResizeTarget === this) activeResizeTarget = undefined;
290
+ activeResizeTargets.delete(this);
270
291
  }
271
292
 
272
293
  render(width: number): string[] {
@@ -295,7 +316,35 @@ class CrewWidgetComponent implements WidgetComponent {
295
316
  const signature = `${sigBase}:${this.model.notificationCount ?? 0}`;
296
317
  const runningGlyph = spinnerFrame("widget-header");
297
318
 
298
- if (this.cacheSignature !== signature || width !== this.cachedWidth || this.cachedTheme !== this.theme) {
319
+ // Task-list variant: the run's plan above the editor (task-list.ts).
320
+ // No panel state, no spinner glyph — the list changes only on task
321
+ // transitions, which the run signature already covers.
322
+ if (this.variant === "tasks") {
323
+ if (this.cacheSignature !== signature || width !== this.cachedWidth || this.cachedTheme !== this.theme) {
324
+ this.cachedBaseLines = buildTaskListLines(runs, width);
325
+ this.cachedLines = this.colorize(this.cachedBaseLines, width);
326
+ this.cachedWidth = width;
327
+ this.cachedTheme = this.theme;
328
+ this.cacheSignature = signature;
329
+ }
330
+ if (runs.length === 0) {
331
+ this.invalidate();
332
+ return [];
333
+ }
334
+ return this.cachedLines.map((line) => truncate(line, width));
335
+ }
336
+
337
+ // Panel cursor/pane state is part of the rendered output, so it belongs in
338
+ // the cache key — otherwise moving the cursor would not repaint.
339
+ const panel = panelDisplayState();
340
+ const signatureWithPanel = `${signature}|panel:${panel.selectedTaskId ?? ""}/${panel.viewedTaskId ?? ""}/${panel.focused ? 1 : 0}`;
341
+
342
+ // The spinner-frame swap only belongs on the LEGACY header, whose line 0
343
+ // already starts with a glyph position (`<frame> Crew agents …`). The
344
+ // compact dock's line 0 is the HINT text ("agents (N) — ↓ to select"):
345
+ // swapping would visibly eat its first character on every frame.
346
+ const compactDock = this.model.rowStyle === "compact";
347
+ if (this.cacheSignature !== signatureWithPanel || width !== this.cachedWidth || this.cachedTheme !== this.theme) {
299
348
  this.cachedBaseLines = buildWidgetLines(
300
349
  this.model.cwd,
301
350
  0,
@@ -303,14 +352,15 @@ class CrewWidgetComponent implements WidgetComponent {
303
352
  runs,
304
353
  this.model.notificationCount ?? 0,
305
354
  width,
355
+ { rowStyle: this.model.rowStyle, ...panel },
306
356
  ).map((line, index) => {
307
- if (index === 0 && line.length > 0) return `${runningGlyph}${line.slice(1)}`;
357
+ if (!compactDock && index === 0 && line.length > 0) return `${runningGlyph}${line.slice(1)}`;
308
358
  return line;
309
359
  });
310
360
  this.cachedLines = this.colorize(this.cachedBaseLines, width);
311
361
  this.cachedWidth = width;
312
362
  this.cachedTheme = this.theme;
313
- this.cacheSignature = signature;
363
+ this.cacheSignature = signatureWithPanel;
314
364
  }
315
365
 
316
366
  if (runs.length === 0) {
@@ -323,12 +373,43 @@ class CrewWidgetComponent implements WidgetComponent {
323
373
  return [];
324
374
  }
325
375
 
326
- const updatedHeader = `${runningGlyph}${this.cachedBaseLines[0]?.slice(1) ?? ""}`;
327
- this.cachedLines[0] = truncate(colorWidgetLine(updatedHeader, 0, this.theme), width);
376
+ if (!compactDock) {
377
+ const updatedHeader = `${runningGlyph}${this.cachedBaseLines[0]?.slice(1) ?? ""}`;
378
+ this.cachedLines[0] = truncate(colorWidgetLine(updatedHeader, 0, this.theme), width);
379
+ }
328
380
  return this.cachedLines.map((line) => truncate(line, width));
329
381
  }
330
382
  }
331
383
 
384
+ // ── Footer dock host (widgetPlacement: "bottom") ──────────────────────
385
+
386
+ /**
387
+ * Dock host for `widgetPlacement: "bottom"`. Keeps a single CrewWidgetComponent
388
+ * (theme-less → raw lines, no ANSI) per session and feeds its render output to
389
+ * the crew-vibes footer through the dock-footer registry. The footer colors the
390
+ * lines with ITS OWN theme so the dock matches the footer context. All caching,
391
+ * event wiring (panel changes, run events, resize) lives in the wrapped
392
+ * component; the footer is re-rendered by pi on every host repaint.
393
+ */
394
+ class FooterDockHost {
395
+ private component: CrewWidgetComponent | undefined;
396
+ private readonly model: CrewWidgetModel;
397
+
398
+ constructor(model: CrewWidgetModel) {
399
+ this.model = model;
400
+ }
401
+
402
+ render(width: number): string[] {
403
+ if (!this.component) this.component = new CrewWidgetComponent(this.model, undefined, undefined);
404
+ return this.component.render(width);
405
+ }
406
+
407
+ dispose(): void {
408
+ this.component?.dispose();
409
+ this.component = undefined;
410
+ }
411
+ }
412
+
332
413
  // ── Re-export listLiveAgents for buildSignature ───────────────────────
333
414
 
334
415
  import { listLiveAgents } from "../../runtime/live-session/live-agent-manager.ts";
@@ -355,20 +436,39 @@ export function updateCrewWidget(
355
436
  }
356
437
 
357
438
  const runs = activeWidgetRuns(ctx.cwd, manifestCache, snapshotCache, preloadedManifests, workspaceId);
358
- const lines = buildWidgetLines(ctx.cwd, state.frame, maxLines, runs, state.notificationCount ?? 0, getRenderWidth());
439
+ const rowStyle = config?.widgetRowStyle ?? DEFAULT_UI.widgetRowStyle;
440
+ // The inline panel navigates the same run list the widget paints, and this is
441
+ // the only place already holding the manifest/snapshot caches — so the row
442
+ // projection is registered here instead of re-reading state on every keypress.
443
+ setPanelRowsProvider(() => panelRowsFromRuns(activeWidgetRuns(ctx.cwd, manifestCache, snapshotCache, preloadedManifests, workspaceId)));
444
+ const lines = buildWidgetLines(ctx.cwd, state.frame, maxLines, runs, state.notificationCount ?? 0, getRenderWidth(), {
445
+ rowStyle,
446
+ ...panelDisplayState(),
447
+ });
359
448
  const placement = config?.widgetPlacement ?? DEFAULT_UI.widgetPlacement;
449
+ // `bottom` is not a pi widget slot: the dock then renders inside the
450
+ // crew-vibes footer (dock-footer registry). pi's slot calls always use a
451
+ // real slot so legacy-clear/installs stay on maps pi understands.
452
+ const bottomMode = placement === "bottom";
453
+ const dockInFooter = bottomMode && isFooterDockSinkActive();
454
+ const piPlacement: "aboveEditor" | "belowEditor" = bottomMode ? "belowEditor" : placement;
360
455
 
361
456
  ctx.ui.setStatus(STATUS_KEY, lines.length ? statusSummary(runs) : undefined);
362
457
 
363
458
  const shouldClearLegacy = state.legacyCleared !== true || state.lastPlacement !== placement;
364
459
  if (shouldClearLegacy) {
365
- setExtensionWidget(ctx, LEGACY_WIDGET_KEY, undefined, { placement });
460
+ setExtensionWidget(ctx, LEGACY_WIDGET_KEY, undefined, { placement: piPlacement });
366
461
  state.legacyCleared = true;
367
462
  }
368
463
 
369
464
  if (!lines.length) {
370
465
  if (state.lastVisibility !== "hidden" || state.lastPlacement !== placement) {
371
- setExtensionWidget(ctx, WIDGET_KEY, undefined, { placement });
466
+ setExtensionWidget(ctx, WIDGET_KEY, undefined, { placement: piPlacement });
467
+ setExtensionWidget(ctx, TASKS_WIDGET_KEY, undefined, { placement: "aboveEditor" });
468
+ state.lastTasksVisibility = "hidden";
469
+ state.footerDock?.dispose();
470
+ state.footerDock = undefined;
471
+ setFooterDockProvider(undefined);
372
472
  state.lastVisibility = "hidden";
373
473
  state.lastPlacement = placement;
374
474
  state.lastKey = WIDGET_KEY;
@@ -398,6 +498,7 @@ export function updateCrewWidget(
398
498
  snapshotCache,
399
499
  preloadManifests: preloadedManifests,
400
500
  workspaceId,
501
+ rowStyle,
401
502
  };
402
503
  else {
403
504
  state.model.cwd = ctx.cwd;
@@ -408,12 +509,33 @@ export function updateCrewWidget(
408
509
  state.model.snapshotCache = snapshotCache;
409
510
  state.model.preloadManifests = preloadedManifests;
410
511
  state.model.workspaceId = workspaceId;
512
+ state.model.rowStyle = rowStyle;
411
513
  }
412
514
 
413
- if (needsWidgetInstall) {
515
+ if (dockInFooter) {
516
+ // Keep pi's widget slot free: the crew-vibes footer paints the dock at
517
+ // the very bottom, below the quota/meter lines. A widget-slot install
518
+ // from a PREVIOUS placement (or a sink that was just enabled) must be
519
+ // removed first.
520
+ if (needsWidgetInstall && state.lastKey === WIDGET_KEY) {
521
+ setExtensionWidget(ctx, WIDGET_KEY, undefined, { placement: piPlacement });
522
+ }
523
+ if (!state.footerDock) state.footerDock = new FooterDockHost(state.model);
524
+ setFooterDockProvider((width) => state.footerDock!.render(width));
525
+ } else {
526
+ // Widget-slot path (aboveEditor/belowEditor, or no footer sink for
527
+ // "bottom"): ensure any stale footer dock is detached first.
528
+ if (state.footerDock) {
529
+ state.footerDock.dispose();
530
+ state.footerDock = undefined;
531
+ }
532
+ setFooterDockProvider(undefined);
533
+ }
534
+
535
+ if (needsWidgetInstall && !dockInFooter) {
414
536
  const model = state.model;
415
537
  setExtensionWidget(ctx, WIDGET_KEY, ((_tui: unknown, theme: unknown) => new CrewWidgetComponent(model, theme, _tui)) as never, {
416
- placement,
538
+ placement: piPlacement,
417
539
  persist: true,
418
540
  });
419
541
  state.lastVisibility = "visible";
@@ -421,6 +543,32 @@ export function updateCrewWidget(
421
543
  state.lastKey = WIDGET_KEY;
422
544
  state.lastMaxLines = maxLines;
423
545
  state.lastCwd = ctx.cwd;
546
+ } else if (dockInFooter) {
547
+ state.lastVisibility = "visible";
548
+ state.lastPlacement = placement;
549
+ state.lastKey = WIDGET_KEY;
550
+ state.lastMaxLines = maxLines;
551
+ state.lastCwd = ctx.cwd;
552
+ }
553
+
554
+ // Task list (aboveEditor): the run's plan progress — Claude Code / droid
555
+ // style, independent of the dock's placement (so it also shows when the
556
+ // dock renders in the crew-vibes footer). Installed once while any
557
+ // display-active run exists; the component paints nothing until a run
558
+ // carries a tasks slice.
559
+ const tasksVisible = runs.length > 0 && Boolean(state.model);
560
+ if (tasksVisible && state.lastTasksVisibility !== "visible") {
561
+ const model = state.model;
562
+ setExtensionWidget(
563
+ ctx,
564
+ TASKS_WIDGET_KEY,
565
+ ((_tui: unknown, theme: unknown) => new CrewWidgetComponent(model, theme, _tui, "tasks")) as never,
566
+ { placement: "aboveEditor" },
567
+ );
568
+ state.lastTasksVisibility = "visible";
569
+ } else if (!tasksVisible && state.lastTasksVisibility === "visible") {
570
+ setExtensionWidget(ctx, TASKS_WIDGET_KEY, undefined, { placement: "aboveEditor" });
571
+ state.lastTasksVisibility = "hidden";
424
572
  }
425
573
 
426
574
  requestRender(ctx);
@@ -436,9 +584,15 @@ export function stopCrewWidget(
436
584
  uninstallResizeListener();
437
585
  if (ctx?.hasUI) {
438
586
  const placement = config?.widgetPlacement ?? DEFAULT_UI.widgetPlacement;
587
+ const piPlacement: "aboveEditor" | "belowEditor" = placement === "bottom" ? "belowEditor" : placement;
439
588
  ctx.ui.setStatus(STATUS_KEY, undefined);
440
- setExtensionWidget(ctx, LEGACY_WIDGET_KEY, undefined, { placement });
441
- setExtensionWidget(ctx, WIDGET_KEY, undefined, { placement });
589
+ setExtensionWidget(ctx, LEGACY_WIDGET_KEY, undefined, { placement: piPlacement });
590
+ setExtensionWidget(ctx, WIDGET_KEY, undefined, { placement: piPlacement });
591
+ setExtensionWidget(ctx, TASKS_WIDGET_KEY, undefined, { placement: "aboveEditor" });
592
+ state.lastTasksVisibility = "hidden";
593
+ state.footerDock?.dispose();
594
+ state.footerDock = undefined;
595
+ setFooterDockProvider(undefined);
442
596
  state.lastVisibility = "hidden";
443
597
  state.lastPlacement = placement;
444
598
  state.lastKey = WIDGET_KEY;