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
@@ -4,6 +4,7 @@ import { listRecentRuns } from "../extension/run-index.ts";
4
4
  import { readCrewAgents } from "../runtime/crew-agent-records.ts";
5
5
  import { listLiveAgents, listLiveAgentsByWorkspace } from "../runtime/live-session/live-agent-manager.ts";
6
6
  import type { ManifestCache } from "../runtime/manifest-cache.ts";
7
+ import { isPlanApprovalPending } from "../runtime/plan-approval.ts";
7
8
  import { isDisplayActiveRun } from "../runtime/process-status.ts";
8
9
  import type { TeamRunManifest, TeamTaskState } from "../state/types.ts";
9
10
  import { aggregateUsage } from "../state/usage.ts";
@@ -87,6 +88,10 @@ export function registerPiCrewPowerbarSegments(events: EventBus, config?: CrewUi
87
88
  id: "pi-crew-steps",
88
89
  label: "pi-crew workflow steps",
89
90
  });
91
+ safeEmit(events, "powerbar:register-segment", {
92
+ id: "pi-crew-plan",
93
+ label: "pi-crew plan approval",
94
+ });
90
95
  }
91
96
 
92
97
  export function updatePiCrewPowerbar(
@@ -136,11 +141,40 @@ interface ActiveItem {
136
141
  * Build the workflow steps segment showing: ✓explore › →plan › ○execute › ○verify
137
142
  * with the current/active step highlighted using → arrow.
138
143
  */
139
- function buildStepsPayload(active: ActiveItem[], allTasks: TeamTaskState[]): PowerbarPayloadShape {
144
+ export function buildStepsPayload(active: ActiveItem[], allTasks: TeamTaskState[]): PowerbarPayloadShape {
140
145
  if (!active.length) {
141
146
  return { id: "pi-crew-steps" };
142
147
  }
143
148
  const run = active[0]!.run;
149
+ // WP-7 (R7): plan-carrying runs show PLAN PHASES instead of workflow steps —
150
+ // the plan is the actual execution shape (a re-plan revision switches the
151
+ // segment without waiting for workflow steps that no longer apply).
152
+ const plans = active[0]!.snapshot?.plans;
153
+ if (plans && plans.length > 0) {
154
+ const current = plans[plans.length - 1]!;
155
+ const itemById = new Map(current.items.map((i) => [i.id, i]));
156
+ const statusOf = (ids: string[]): "completed" | "running" | "pending" => {
157
+ const items = ids.map((id) => itemById.get(id)?.status).filter(Boolean) as string[];
158
+ if (items.length && items.every((s) => s === "done")) return "completed";
159
+ if (items.some((s) => s === "active")) return "running";
160
+ return "pending";
161
+ };
162
+ const parts = current.phases.map((phase) => {
163
+ const status = phase.status === "done" ? "completed" : statusOf(phase.itemIds);
164
+ const icon = status === "completed" ? "✓" : status === "running" ? "→" : "○";
165
+ const name = phase.title.length > 10 ? `${phase.title.slice(0, 9)}…` : phase.title;
166
+ return `${icon}${name}`;
167
+ });
168
+ if (parts.length) {
169
+ const hasRunning = parts.some((p) => p.startsWith("→"));
170
+ const allComplete = parts.every((p) => p.startsWith("✓"));
171
+ return {
172
+ id: "pi-crew-steps",
173
+ text: `P${current.version} ${parts.join(" › ")}`,
174
+ color: allComplete ? "success" : hasRunning ? "accent" : "dim",
175
+ };
176
+ }
177
+ }
144
178
  const workflowName = run.workflow ?? "default";
145
179
  // Load workflow steps
146
180
  const workflows = allWorkflows(discoverWorkflows(run.cwd));
@@ -206,6 +240,7 @@ class PowerbarPublisher {
206
240
  #lastActiveKey: string | undefined;
207
241
  #lastProgressKey: string | undefined;
208
242
  #lastStepsKey: string | undefined;
243
+ #lastPlanKey: string | undefined;
209
244
  #latestArgs: PowerbarUpdateArgs | null = null;
210
245
  readonly #coalescer: RenderCoalescer;
211
246
 
@@ -279,9 +314,11 @@ class PowerbarPublisher {
279
314
  this.#lastActiveKey = undefined;
280
315
  this.#lastProgressKey = undefined;
281
316
  this.#lastStepsKey = undefined;
317
+ this.#lastPlanKey = undefined;
282
318
  safeEmit(events, "powerbar:update", { id: "pi-crew-active" });
283
319
  safeEmit(events, "powerbar:update", { id: "pi-crew-progress" });
284
320
  safeEmit(events, "powerbar:update", { id: "pi-crew-steps" });
321
+ safeEmit(events, "powerbar:update", { id: "pi-crew-plan" });
285
322
  return;
286
323
  }
287
324
  const agents = active.flatMap((item) => item.agents);
@@ -373,6 +410,12 @@ class PowerbarPublisher {
373
410
  } as const;
374
411
  // Build step progress: "explorer > planner > executor > verifier" with current step highlighted
375
412
  const stepsPayload = buildStepsPayload(active, tasks);
413
+ // WP-3 (H4): plan-approval segment — `plan:pending` while ANY active run is
414
+ // parked awaiting plan approval, clear payload otherwise. `item.run` is
415
+ // always the freshest manifest available (snapshot.manifest when cached).
416
+ const planPayload: PowerbarPayloadShape = active.some((item) => isPlanApprovalPending(item.run))
417
+ ? { id: "pi-crew-plan", text: "plan:pending", color: "warning" }
418
+ : { id: "pi-crew-plan" };
376
419
  // 1.8: dedup per segment using a key over every visible field. Previously
377
420
  // the dedup string only carried text/suffix/running, so changes to `bar`
378
421
  // (progress %) or `color` could be swallowed and stale UI emitted again
@@ -380,6 +423,7 @@ class PowerbarPublisher {
380
423
  const activeKey = powerbarKey(activePayload);
381
424
  const progressKey = powerbarKey(progressPayload);
382
425
  const stepsKey = powerbarKey(stepsPayload);
426
+ const planKey = powerbarKey(planPayload);
383
427
  if (activeKey !== this.#lastActiveKey) {
384
428
  this.#lastActiveKey = activeKey;
385
429
  safeEmit(events, "powerbar:update", activePayload);
@@ -392,6 +436,10 @@ class PowerbarPublisher {
392
436
  this.#lastStepsKey = stepsKey;
393
437
  safeEmit(events, "powerbar:update", stepsPayload);
394
438
  }
439
+ if (planKey !== this.#lastPlanKey) {
440
+ this.#lastPlanKey = planKey;
441
+ safeEmit(events, "powerbar:update", planPayload);
442
+ }
395
443
  // Never call setStatusFallback - crew-widget manages "pi-crew" status with its own widget format
396
444
  // Powerbar only emits events; it does not set status directly
397
445
  }
@@ -431,9 +479,11 @@ class PowerbarPublisher {
431
479
  this.#lastActiveKey = undefined;
432
480
  this.#lastProgressKey = undefined;
433
481
  this.#lastStepsKey = undefined;
482
+ this.#lastPlanKey = undefined;
434
483
  safeEmit(events, "powerbar:update", { id: "pi-crew-active" });
435
484
  safeEmit(events, "powerbar:update", { id: "pi-crew-progress" });
436
485
  safeEmit(events, "powerbar:update", { id: "pi-crew-steps" });
486
+ safeEmit(events, "powerbar:update", { id: "pi-crew-plan" });
437
487
  }
438
488
 
439
489
  /** Reset dedup state on session lifecycle events. */
@@ -441,6 +491,7 @@ class PowerbarPublisher {
441
491
  this.#lastActiveKey = undefined;
442
492
  this.#lastProgressKey = undefined;
443
493
  this.#lastStepsKey = undefined;
494
+ this.#lastPlanKey = undefined;
444
495
  }
445
496
 
446
497
  dispose(): void {
@@ -3,6 +3,7 @@ import type { MetricRegistry } from "../observability/metric-registry.ts";
3
3
  import { readCrewAgents } from "../runtime/crew-agent-records.ts";
4
4
  import type { CrewAgentRecord } from "../runtime/crew-agent-runtime.ts";
5
5
  import { getLiveAgentContextPercent } from "../runtime/live-session/live-agent-manager.ts";
6
+ import { isPlanApprovalPending } from "../runtime/plan-approval.ts";
6
7
  import { isDisplayActiveRun, isLikelyOrphanedActiveRun } from "../runtime/process-status.ts";
7
8
  import type { TeamRunManifest, TeamTaskState, UsageState } from "../state/types.ts";
8
9
  import { aggregateUsage } from "../state/usage.ts";
@@ -16,6 +17,7 @@ import { summarizeTerminalReason } from "./dashboard-panes/cancellation-pane.ts"
16
17
  import { renderHealthPane } from "./dashboard-panes/health-pane.ts";
17
18
  import { renderMailboxPane } from "./dashboard-panes/mailbox-pane.ts";
18
19
  import { renderMetricsPane } from "./dashboard-panes/metrics-pane.ts";
20
+ import { renderPlanPane } from "./dashboard-panes/plan-pane.ts";
19
21
  import { renderProgressPane } from "./dashboard-panes/progress-pane.ts";
20
22
  import { renderTranscriptPane } from "./dashboard-panes/transcript-pane.ts";
21
23
  import { DynamicCrewBorder } from "./dynamic-border.ts";
@@ -75,7 +77,7 @@ export interface RunDashboardOptions {
75
77
  * looking at. Resetting to "agents" on every `new RunDashboard(...)` was a
76
78
  * UX regression.
77
79
  */
78
- let lastActivePane: "agents" | "progress" | "mailbox" | "output" | "health" | "metrics" = "agents";
80
+ let lastActivePane: "agents" | "progress" | "mailbox" | "output" | "health" | "metrics" | "plan" = "agents";
79
81
 
80
82
  export type RunDashboardAction =
81
83
  | "status"
@@ -94,6 +96,8 @@ export type RunDashboardAction =
94
96
  | "health-recovery"
95
97
  | "health-kill-stale"
96
98
  | "health-diagnostic-export"
99
+ | "plan-approve"
100
+ | "plan-deny"
97
101
  | "notifications-dismiss";
98
102
  export interface RunDashboardSelection {
99
103
  runId: string;
@@ -404,7 +408,9 @@ export class RunDashboard implements DashboardComponent {
404
408
  private runScrollOffset = 0;
405
409
  private showFullProgress = false;
406
410
  private showHelp = false;
407
- private activePane: "agents" | "progress" | "mailbox" | "output" | "health" | "metrics" = lastActivePane;
411
+ private activePane: "agents" | "progress" | "mailbox" | "output" | "health" | "metrics" | "plan" = lastActivePane;
412
+ /** WP-7 (R7): pane-scoped revision-diff toggle (X). */
413
+ private planDiff = false;
408
414
  private runs: TeamRunManifest[];
409
415
  private readonly done: (selection: RunDashboardSelection | undefined) => void;
410
416
  private readonly theme: CrewTheme;
@@ -638,7 +644,7 @@ export class RunDashboard implements DashboardComponent {
638
644
  lines.push(
639
645
  border("╭", "╮"),
640
646
  row(
641
- `${fg("accent", "▐")} ${this.theme.bold("pi-crew")} · ${this.runs.length} runs ${fg("dim", "1-6 pane · ↑↓ · Enter · ? help · Esc")}`,
647
+ `${fg("accent", "▐")} ${this.theme.bold("pi-crew")} · ${this.runs.length} runs ${fg("dim", "1-7 pane · ↑↓ · Enter · ? help · Esc")}`,
642
648
  ),
643
649
  sep(),
644
650
  );
@@ -732,7 +738,9 @@ export class RunDashboard implements DashboardComponent {
732
738
  registry: this.options.registry,
733
739
  }),
734
740
  )
735
- : safeRenderPane("transcript", () => renderTranscriptPane(snap))
741
+ : this.activePane === "plan"
742
+ ? safeRenderPane("plan", () => renderPlanPane(snap, { diff: this.planDiff }))
743
+ : safeRenderPane("transcript", () => renderTranscriptPane(snap))
736
744
  : [...readAgentPreview(r, 4, this.options), ...readProgressPreview(r, 2, this.options.snapshotCache)];
737
745
  const filteredPane = paneLines.filter((l) => l && !l.includes("(none)") && l.trim() !== "");
738
746
  if (filteredPane.length > 0) {
@@ -832,6 +840,19 @@ export class RunDashboard implements DashboardComponent {
832
840
  this.done(selectedRunId ? { runId: selectedRunId, action: "status" } : undefined);
833
841
  return;
834
842
  }
843
+ // WP-3 (H4-subset): plan approval keys. Deliberately a DEDICATED branch
844
+ // (not the generic union block below) because plan actions must gate on
845
+ // the selected run actually being parked on a pending approval — a
846
+ // stray keystroke on a non-pending run is a silent no-op (dashboard
847
+ // stays open) instead of closing it via done(undefined).
848
+ if (action === "plan-approve" || action === "plan-deny") {
849
+ const run = selectedRunFromGrouped(this.runs, this.selected, this.options.snapshotCache);
850
+ const manifest = run ? (snapshotFor(run, this.options.snapshotCache)?.manifest ?? run) : undefined;
851
+ if (run && manifest && isPlanApprovalPending(manifest)) {
852
+ this.done({ runId: run.runId, action });
853
+ }
854
+ return;
855
+ }
835
856
  if (
836
857
  action === "summary" ||
837
858
  action === "artifacts" ||
@@ -875,7 +896,12 @@ export class RunDashboard implements DashboardComponent {
875
896
  else if (action === "pane-output") this.activePane = "output";
876
897
  else if (action === "pane-health") this.activePane = "health";
877
898
  else if (action === "pane-metrics") this.activePane = "metrics";
878
- else if (action === "up") this.selected = Math.max(0, this.selected - 1);
899
+ else if (action === "pane-plan") this.activePane = "plan";
900
+ else if (action === "plan-diff") {
901
+ this.planDiff = !this.planDiff;
902
+ this.invalidate();
903
+ return;
904
+ } else if (action === "up") this.selected = Math.max(0, this.selected - 1);
879
905
  else if (action === "down") {
880
906
  const selectableCount = groupedRuns(this.runs, this.options.snapshotCache).filter((row) => row.run).length;
881
907
  this.selected = Math.min(Math.max(0, selectableCount - 1), this.selected + 1);
@@ -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
  }
@@ -5,6 +5,7 @@
5
5
  */
6
6
 
7
7
  import { listLiveAgents } from "../../runtime/live-session/live-agent-manager.ts";
8
+ import { isPlanApprovalStatePending } from "../../runtime/plan-approval.ts";
8
9
  import { isFinishedRunStatus } from "../../runtime/process-status.ts";
9
10
  import { truncate } from "../../utils/visual.ts";
10
11
  import { Box, Text } from "../layout-primitives.ts";
@@ -73,7 +74,14 @@ export function buildWidgetLines(
73
74
  return Number.isFinite(age) && age < maxAgeMs;
74
75
  });
75
76
  const completed = agents.filter((a) => a.status === "completed").length;
76
- const runGlyph = iconForStatus(run.status, { runningGlyph });
77
+ // WP-3 (H4): while a run is parked awaiting plan approval, the spinner
78
+ // glyph is replaced by a `⚠ plan:<last-8 runId>` badge. Plain-unicode ⚠
79
+ // (the same glyph needs_attention already uses) stays legible in no-color
80
+ // mode and is colorized by the shared colorizeStatusGlyphs pass. In-place
81
+ // glyph swap only — no extra line, so the MAX_AGENTS_DISPLAY / maxLines
82
+ // budget and the truncate(width) rule are untouched.
83
+ const planPending = isPlanApprovalStatePending(run.planApproval);
84
+ const runGlyph = planPending ? `⚠ plan:${run.runId.slice(-8)}` : iconForStatus(run.status, { runningGlyph });
77
85
  const isTerminal = isFinishedRunStatus(run.status);
78
86
  // Run progress line. v1–v3 flickered on snapshot.tasks state, v4 was
79
87
  // too minimal (`0/1 agents` only), v5 duplicated the worker activity
@@ -63,10 +63,6 @@ function evictOldestCacheEntry(): void {
63
63
  if (oldestKey !== undefined) readCache.delete(oldestKey);
64
64
  }
65
65
 
66
- export function clearReadCache(): void {
67
- readCache.clear();
68
- }
69
-
70
66
  export function readJsonFileCoalesced<T>(filePath: string, ttlMs: number, read: () => T): T {
71
67
  const now = Date.now();
72
68
  const stat = (() => {
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Fatal-filesystem errno classification (bug-026 sub-issue B).
3
+ *
4
+ * During the 2026-08-15 disk-full window, workers died with generic
5
+ * "unresponsive"/ETIMEDOUT diagnostics — the ENOSPC errno only surfaced
6
+ * inside stderr-tail strings embedded in the E007 ChildTimeout message
7
+ * (`errors.ts` childTimeout appends "Stderr tail: ..."), never as a raw
8
+ * errno object in the parent process. The classifier therefore matches BOTH
9
+ * shapes:
10
+ * - (a) `err.code` (NodeJS.ErrnoException) case-insensitively, and
11
+ * - (b) the error's message text — or any plain string, e.g. a stderr tail.
12
+ *
13
+ * Deliberately NOT placed in state/atomic-write.ts: that module has
14
+ * process.on side effects at module load and imports worker-atomic-writer,
15
+ * and it already throws raw errno errors — classification belongs at catch
16
+ * sites. This module is dependency-free so state/types.ts (type-only) and
17
+ * the team-tool run/doctor surfaces can import it cheaply.
18
+ */
19
+
20
+ export type FatalFsCause = "enospc" | "edquot" | "emfile" | "enfile";
21
+
22
+ const FATAL_FS_CODES: ReadonlySet<string> = new Set(["enospc", "edquot", "emfile", "enfile"]);
23
+
24
+ /** Message-text fallback: catches errno codes embedded in stderr tails /
25
+ * synthesized error messages where no structured `.code` exists. */
26
+ const FATAL_FS_TEXT = /\b(ENOSPC|EDQUOT|EMFILE|ENFILE)\b/;
27
+
28
+ function classifyCode(code: unknown): FatalFsCause | undefined {
29
+ if (typeof code !== "string") return undefined;
30
+ const normalized = code.toLowerCase();
31
+ return FATAL_FS_CODES.has(normalized) ? (normalized as FatalFsCause) : undefined;
32
+ }
33
+
34
+ function classifyText(text: unknown): FatalFsCause | undefined {
35
+ if (typeof text !== "string") return undefined;
36
+ const match = FATAL_FS_TEXT.exec(text);
37
+ return match ? (match[1]?.toLowerCase() as FatalFsCause) : undefined;
38
+ }
39
+
40
+ /**
41
+ * Classify an unknown error (or a raw string, e.g. a child stderr tail) as a
42
+ * fatal filesystem cause. Returns undefined for non-fs errors. Pure and total:
43
+ * never throws on non-object inputs.
44
+ */
45
+ export function classifyFatalFsError(err: unknown): FatalFsCause | undefined {
46
+ if (typeof err === "string") return classifyText(err);
47
+ if (err === null || err === undefined) return undefined;
48
+ const code = classifyCode((err as NodeJS.ErrnoException).code);
49
+ if (code) return code;
50
+ return classifyText((err as { message?: unknown }).message);
51
+ }
52
+
53
+ /**
54
+ * Combined classification for a finished attempt (bug-026 sub-issue B):
55
+ * the assembled attempt error first — a structured errno or a message that
56
+ * embeds one (E007 stderr tails) — then the child's stderr tail. Extracted
57
+ * as a pure seam so the child-executor hook is directly unit-testable.
58
+ */
59
+ export function failureCauseForAttempt(error: string | undefined, stderr: string | undefined): FatalFsCause | undefined {
60
+ return classifyFatalFsError(error) ?? classifyFatalFsError(stderr);
61
+ }
62
+
63
+ /** Human label for a fatal fs cause. */
64
+ export function fsFailureLabel(cause: FatalFsCause): string {
65
+ return cause === "enospc" || cause === "edquot" ? "disk full" : "too many open files";
66
+ }
@@ -10,7 +10,7 @@ import * as fs from "node:fs";
10
10
  * caused a permanent interactive-session busy-loop. The bounded
11
11
  * {@link RunWatcherRegistry} (one non-recursive watcher per ACTIVE run) now
12
12
  * replaces them. Only the two primitives below survive — they are still used by
13
- * manifest-cache, result-watcher, and run-watcher-registry.
13
+ * manifest-cache and run-watcher-registry.
14
14
  */
15
15
 
16
16
  export function closeWatcher(watcher: FSWatcher | null | undefined): void {
@@ -1,7 +1,9 @@
1
+ import { getCrewEnv } from "../config/env-vars.ts";
2
+
1
3
  export function logInternalError(scope: string, error: unknown, details?: string, severity?: "error" | "warn" | "debug"): void {
2
4
  // "error" and "warn" always emit; "debug" (default) is gated behind PI_TEAMS_DEBUG
3
5
  if (!severity || severity === "debug") {
4
- if (!process.env.PI_TEAMS_DEBUG) return;
6
+ if (!getCrewEnv("PI_TEAMS_DEBUG")) return;
5
7
  }
6
8
  const message =
7
9
  error instanceof Error ? error.message : typeof error === "object" && error !== null ? JSON.stringify(error) : String(error);
@@ -2,6 +2,7 @@ import * as fs from "node:fs";
2
2
  import * as os from "node:os";
3
3
  import * as path from "node:path";
4
4
  import { fileURLToPath } from "node:url";
5
+ import { getCrewEnv } from "../config/env-vars.ts";
5
6
 
6
7
  // NEW-P1/NEW-P2 (perf): packageRoot() and userPiRoot() are invariant for a
7
8
  // process lifetime but each call did statSync/readFileSync/lstatSync (10-15+
@@ -55,7 +56,7 @@ export function userPiRoot(): string {
55
56
  // "undefined/.pi/agent" relative to cwd and silently create a junk "undefined/"
56
57
  // tree. Treat the literal "undefined" (and empty) as unset and fall back to
57
58
  // os.homedir().
58
- const rawHome = (process.env.PI_TEAMS_HOME ?? process.env.PI_CREW_HOME)?.trim();
59
+ const rawHome = getCrewEnv("PI_CREW_HOME")?.trim();
59
60
  const home = rawHome && rawHome !== "undefined" ? rawHome : os.homedir();
60
61
  // NEW-P2: memoize but KEY ON HOME. Tests (withIsolatedHome / isolateHome)
61
62
  // change PI_TEAMS_HOME mid-process; an unkeyed cache would leak the first
@@ -203,8 +204,15 @@ export function findRepoRoot(cwd: string): string | undefined {
203
204
  function computeRepoRoot(start: string): string | undefined {
204
205
  let current = start;
205
206
  const root = path.parse(current).root;
206
- const home = path.resolve(os.homedir());
207
- const tempRoot = path.resolve(os.tmpdir());
207
+ // Canonicalize the boundaries to match the walk chain: findRepoRoot
208
+ // resolves the start dir via realpathSync, so every step of the walk is a
209
+ // canonical path. Comparing against the LEXICAL home/tmpdir lets symlinked
210
+ // temp dirs (macOS: /var -> /private/var, TMPDIR=/var/folders/.../T) never
211
+ // match, so the walk escapes the sandbox and can latch onto an unrelated
212
+ // ancestor marker (seen live: GH macOS runners have one) — state then
213
+ // resolves to a repo root far outside the temp tree (bug-029).
214
+ const home = canonicalizePath(path.resolve(os.homedir()));
215
+ const tempRoot = canonicalizePath(path.resolve(os.tmpdir()));
208
216
  while (current !== root) {
209
217
  // Stop walking before checking markers at home or temp root
210
218
  if (current === home || current === tempRoot) return undefined;
@@ -295,7 +295,7 @@ function capitalize(s: string): string {
295
295
  return s.charAt(0).toUpperCase() + s.slice(1);
296
296
  }
297
297
 
298
- let usedNames = new Set<string>();
298
+ const usedNames = new Set<string>();
299
299
 
300
300
  /**
301
301
  * Generate a unique two-word identifier (e.g., "SwiftFalcon").
@@ -328,10 +328,3 @@ export function generateTaskName(): string {
328
328
  usedNames.clear();
329
329
  return generateTaskName();
330
330
  }
331
-
332
- /**
333
- * Reset name generator state (for testing).
334
- */
335
- export function resetTaskNames(): void {
336
- usedNames = new Set<string>();
337
- }