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
@@ -5,21 +5,38 @@ import type { TeamRunManifest, TeamTaskState } from "../state/types.ts";
5
5
  import { projectCrewRoot } from "../utils/paths.ts";
6
6
  import { isFinishedRunStatus } from "./process-status.ts";
7
7
 
8
+ export interface RunWaitResult {
9
+ manifest: TeamRunManifest;
10
+ tasks: TeamTaskState[];
11
+ /** True when the waiter was released early by `detachRunPromise` while the
12
+ * run itself keeps executing (see that function). */
13
+ detached?: boolean;
14
+ }
15
+
8
16
  export interface ActiveRunPromise {
9
- promise: Promise<{ manifest: TeamRunManifest; tasks: TeamTaskState[] }>;
10
- resolve: (value: { manifest: TeamRunManifest; tasks: TeamTaskState[] }) => void;
17
+ promise: Promise<RunWaitResult>;
18
+ resolve: (value: RunWaitResult) => void;
11
19
  reject: (reason: unknown) => void;
12
20
  }
13
21
 
14
22
  const activeRunPromises = new Map<string, ActiveRunPromise>();
15
23
 
24
+ /**
25
+ * Runs whose waiter should be released on its next opportunity.
26
+ *
27
+ * A detach request can arrive BEFORE `executeTeamRun` registers its foreground
28
+ * promise (the tool starts the run and calls `waitForRun` immediately, so a
29
+ * slow-starting run puts the waiter on the polling path with no promise to
30
+ * resolve). The flag makes the request path-independent: the polling loop
31
+ * consumes it just like `detachRunPromise` resolves a registered promise.
32
+ */
33
+ const detachRequests = new Set<string>();
34
+
16
35
  export function registerRunPromise(runId: string): ActiveRunPromise {
17
- let resolve!: (value: { manifest: TeamRunManifest; tasks: TeamTaskState[] }) => void;
36
+ detachRequests.delete(runId);
37
+ let resolve!: (value: RunWaitResult) => void;
18
38
  let reject!: (reason: unknown) => void;
19
- const promise = new Promise<{
20
- manifest: TeamRunManifest;
21
- tasks: TeamTaskState[];
22
- }>((res, rej) => {
39
+ const promise = new Promise<RunWaitResult>((res, rej) => {
23
40
  resolve = res;
24
41
  reject = rej;
25
42
  });
@@ -28,7 +45,41 @@ export function registerRunPromise(runId: string): ActiveRunPromise {
28
45
  return entry;
29
46
  }
30
47
 
31
- export function resolveRunPromise(runId: string, result: { manifest: TeamRunManifest; tasks: TeamTaskState[] }): void {
48
+ /**
49
+ * Release a foreground waiter WITHOUT stopping the run.
50
+ *
51
+ * A foreground `team run` keeps the parent pi turn streaming for the whole run,
52
+ * and pi's `switchSession` tears the current session down via
53
+ * `session.abort()` → `waitForIdle()`. Opening an agent view therefore hung
54
+ * silently until the run finished. Detaching resolves the tool's `waitForRun`
55
+ * with the run's current (still-running) state so the tool call returns, the
56
+ * parent turn can settle, and the session switch lands — `executeTeamRun`
57
+ * itself keeps going in this process and the async notifier reports completion.
58
+ *
59
+ * Returns false only when the run's state cannot be read at all. When no
60
+ * foreground promise is registered yet (or the waiter is on the polling path),
61
+ * the request is recorded and consumed by `waitForRun` on its next poll.
62
+ */
63
+ export function detachRunPromise(runId: string, cwd: string): boolean {
64
+ const loaded = loadRunManifestById(cwd, runId);
65
+ if (!loaded) return false;
66
+ const entry = activeRunPromises.get(runId);
67
+ if (entry) {
68
+ activeRunPromises.delete(runId);
69
+ detachRequests.delete(runId);
70
+ entry.resolve({ ...loaded, detached: true });
71
+ return true;
72
+ }
73
+ detachRequests.add(runId);
74
+ return true;
75
+ }
76
+
77
+ /** True while a detach was requested but no waiter has consumed it yet. */
78
+ export function hasPendingRunDetach(runId: string): boolean {
79
+ return detachRequests.has(runId);
80
+ }
81
+
82
+ export function resolveRunPromise(runId: string, result: RunWaitResult): void {
32
83
  const entry = activeRunPromises.get(runId);
33
84
  if (entry) {
34
85
  entry.resolve(result);
@@ -54,15 +105,22 @@ export async function waitForRun(
54
105
  runId: string,
55
106
  cwd: string,
56
107
  options: { timeoutMs?: number; pollIntervalMs?: number } = {},
57
- ): Promise<{ manifest: TeamRunManifest; tasks: TeamTaskState[] }> {
108
+ ): Promise<RunWaitResult> {
58
109
  const { timeoutMs = 300_000, pollIntervalMs = 500 } = options;
59
110
  const deadline = Date.now() + timeoutMs;
60
111
 
112
+ // A detach requested before this waiter existed applies to it (and is
113
+ // consumed here, so it can never leak onto a later waiter for the same run).
114
+ const detachPending = detachRequests.delete(runId);
115
+
61
116
  // Fast path: already terminal on disk
62
117
  const loaded = loadRunManifestById(cwd, runId); // NOTE: no withRunLock - best-effort only; concurrent writes may cause inconsistency;
63
118
  if (loaded && isFinishedRunStatus(loaded.manifest.status)) {
64
119
  return loaded;
65
120
  }
121
+ if (detachPending && loaded) {
122
+ return { ...loaded, detached: true };
123
+ }
66
124
 
67
125
  // Medium path: foreground promise registered in this process
68
126
  const entry = activeRunPromises.get(runId);
@@ -78,9 +136,17 @@ export async function waitForRun(
78
136
  }
79
137
  }
80
138
 
81
- // Slow path: background run — poll with exponential backoff capped at pollIntervalMs
139
+ // Slow path: background run — poll with exponential backoff capped at pollIntervalMs.
140
+ // This path is ALSO taken by a foreground run whose executeTeamRun has not
141
+ // registered its promise yet (the tool calls waitForRun immediately after
142
+ // starting the run), so it must honour a pending detach request too —
143
+ // otherwise opening an agent view can never release the parent turn.
82
144
  let attempt = 0;
83
145
  while (Date.now() < deadline) {
146
+ if (detachRequests.delete(runId)) {
147
+ const current = loadRunManifestById(cwd, runId);
148
+ if (current) return { ...current, detached: true };
149
+ }
84
150
  if (attempt === 0) {
85
151
  // Early exit: if the run directory doesn't exist, don't waste time polling.
86
152
  // Use projectCrewRoot() to honour the .pi/teams/ fallback for .pi-based
@@ -109,6 +175,7 @@ export function hasActiveRunPromise(runId: string): boolean {
109
175
  }
110
176
 
111
177
  export function clearRunPromisesForTest(): void {
178
+ detachRequests.clear();
112
179
  for (const entry of activeRunPromises.values()) {
113
180
  entry.reject(new Error("Cleared by test"));
114
181
  }
@@ -0,0 +1,98 @@
1
+ /**
2
+ * Scheduler context types for the team-run scheduler loop.
3
+ *
4
+ * Extracted from team-runner.ts (2026-08, Phase 2.6 maintainability split —
5
+ * CORE-4 scheduler context, resolves RT-15). Pure type motion: SettledUnit /
6
+ * PendingUnit / SchedulerContext / SchedulerDecision moved verbatim.
7
+ */
8
+
9
+ import type { TeamRunManifest, TeamTaskState } from "../state/types.ts";
10
+ import type { WorkflowConfig } from "../workflows/workflow-config.ts";
11
+ import type { CrewRuntimeKind } from "./crew-agent-runtime.ts";
12
+ import type { BatchConcurrencyDecision } from "./scheduling/concurrency.ts";
13
+ import type { TaskGraphIndex, TaskGraphSchedulerSnapshot } from "./scheduling/task-graph-scheduler.ts";
14
+ import type { ResultArtifactReadCache } from "./task-output-context.ts";
15
+ // ExecuteTeamRunInput is defined in team-runner.ts — import type ONLY so the
16
+ // type-level team-runner↔scheduler-context reference is erased at runtime
17
+ // (no runtime import cycle; team-runner.ts imports these types back).
18
+ import type { ExecuteTeamRunInput } from "./team-runner.ts";
19
+ import type { WorkflowStateMachine } from "./workflow-state.ts";
20
+
21
+ /** RT-12: result shape from a settled dispatch unit (pre-created wrapper). */
22
+ export type SettledUnit = {
23
+ unitKey: string;
24
+ result: { manifest: TeamRunManifest; tasks: TeamTaskState[] } | undefined;
25
+ error: Error | undefined;
26
+ };
27
+
28
+ /**
29
+ * RT-12: in-flight dispatch unit. `wrapped` is a pre-created wrapper promise
30
+ * (try/catch → SettledUnit) so mergeUnitResult can Promise.race without
31
+ * allocating new async closures every loop iteration (O(C) total wrappers
32
+ * instead of O(C×T) churn).
33
+ */
34
+ export type PendingUnit = {
35
+ taskIds: string[];
36
+ promise: Promise<{ manifest: TeamRunManifest; tasks: TeamTaskState[] }>;
37
+ wrapped: Promise<SettledUnit>;
38
+ };
39
+
40
+ // ── CORE-4: SchedulerContext state bag ─────────────────────────────
41
+ // A mutable bag of the closure locals used across executeTeamRunCore.
42
+ // Extracted scheduler functions receive this context and mutate it
43
+ // in-place. This enables incremental extraction of the ~1075-line god
44
+ // function into scheduler/ functions without changing control flow.
45
+
46
+ /**
47
+ * Mutable state shared across the team-run scheduler loop.
48
+ *
49
+ * Fields mirror the closure locals of `executeTeamRunCore`. Extracted
50
+ * scheduler functions mutate these fields in-place; the caller keeps the
51
+ * local variables in sync by assigning back from `ctx` after each call.
52
+ */
53
+ export interface SchedulerContext {
54
+ input: ExecuteTeamRunInput;
55
+ workflow: WorkflowConfig;
56
+ manifest: TeamRunManifest;
57
+ tasks: TeamTaskState[];
58
+ queueIndex: TaskGraphIndex;
59
+ wfMachine: WorkflowStateMachine;
60
+ pendingUnits: Map<string, PendingUnit>;
61
+ /** Task ids ever dispatched (grows monotonically; never removed). Used by
62
+ * terminaliseRunWithDrain to cancel — not skip — tasks that were in-flight
63
+ * even after their dispatch unit settled + left pendingUnits (RT-NEW-2 race). */
64
+ dispatchedTaskIds: Set<string>;
65
+ runController: AbortController;
66
+ runtimeKind: CrewRuntimeKind;
67
+ adaptivePlanInjected: boolean;
68
+ adaptivePlanMissing: boolean;
69
+ /** Outcome of the most recent mergeUnitResult call: the settled unit's
70
+ * taskIds and the merged result object. Read by the post-merge inline
71
+ * logic (cancel-during-exec check + batch summary). Set by extraction 5. */
72
+ settledMerge: { taskIds: string[]; result: { manifest: TeamRunManifest; tasks: TeamTaskState[] } } | null;
73
+ /** R10-1: per-run result-artifact read cache (single instance, created in
74
+ * executeTeamRunCore). Shared by closeout aggregation and — via baseInput
75
+ * in dispatch-batch.ts — dependency-context reads in prepareTaskExecutionContext. */
76
+ resultReadCache: ResultArtifactReadCache;
77
+ }
78
+
79
+ /**
80
+ * Discriminated union representing a scheduler sub-function's decision.
81
+ *
82
+ * - `continue`: proceed to the next phase of the loop body.
83
+ * - `return`: short-circuit the loop and return the given result.
84
+ * - `skip-dispatch`: skip the dispatch phase this iteration (reserved for
85
+ * future extractions).
86
+ */
87
+ export type SchedulerDecision =
88
+ | { kind: "continue" }
89
+ | { kind: "return"; result: { manifest: TeamRunManifest; tasks: TeamTaskState[] } }
90
+ | { kind: "skip-dispatch" }
91
+ | {
92
+ kind: "dispatch";
93
+ batch: TeamTaskState[];
94
+ concurrency: BatchConcurrencyDecision;
95
+ snapshot: TaskGraphSchedulerSnapshot;
96
+ approvalPending: boolean;
97
+ coalesceEnabled: boolean;
98
+ };
@@ -91,6 +91,11 @@ export function planCoalescedGroups(
91
91
  // doesn't compose in a multi-task worker. These flags set up side
92
92
  // effects per-task that can't be combined.
93
93
  if (step.worktree) continue;
94
+ // T4/R6 (round-1 P2): spec-carrying steps never coalesce — the coalesced
95
+ // path bypasses buildTaskPacket/finalizeTaskResult, which would silently
96
+ // drop the snapshot freeze, the SPEC contract prompt block, and the gate.
97
+ if (step.specRefs?.length) continue;
98
+ if (step.specStrict === true) continue;
94
99
  if (step.preStepScript) continue;
95
100
  if (step.verify) continue;
96
101
  if (step.output !== undefined && step.output !== false && step.output !== "") continue;
@@ -22,6 +22,7 @@
22
22
  */
23
23
 
24
24
  import * as os from "node:os";
25
+ import { getCrewEnv } from "../../config/env-vars.ts";
25
26
  import { Semaphore } from "./semaphore.ts";
26
27
 
27
28
  /**
@@ -30,7 +31,7 @@ import { Semaphore } from "./semaphore.ts";
30
31
  * rather than silently disabling the cap.
31
32
  */
32
33
  function resolveCapacity(): number {
33
- const env = process.env.PI_CREW_MAX_WORKERS;
34
+ const env = getCrewEnv("PI_CREW_MAX_WORKERS");
34
35
  if (env !== undefined && env !== "") {
35
36
  const parsed = Number.parseInt(env, 10);
36
37
  if (Number.isFinite(parsed) && parsed > 0) {
@@ -0,0 +1,70 @@
1
+ /**
2
+ * Nested-slot budget for delegate-spawned grandchildren (ADR-5 §2, WP-5 step 3).
3
+ *
4
+ * Grandchildren NEVER ride the global worker semaphore — a waiting parent
5
+ * worker keeps holding its own global slot, so ride-along spawn is the
6
+ * permanent-deadlock shape the repo already records for the judge
7
+ * (`scheduling/global-worker-cap.ts:14-19` MAJ#3). Instead the root-side
8
+ * delegate handler draws from THIS separate budget:
9
+ *
10
+ * max = max(1, floor(globalSem / 2)) (or an explicit config override)
11
+ *
12
+ * FAIL-FAST, NEVER QUEUE: when the budget is exhausted, `tryAcquire` returns
13
+ * false immediately — the caller rejects the delegate with a policy message
14
+ * ("nested spawn budget exhausted; N/M in flight"). No waiting, no queueing.
15
+ *
16
+ * The budget is owned by the single root-side delegate handler process, so an
17
+ * in-memory id-keyed map is the source of truth (no disk state).
18
+ */
19
+
20
+ export interface NestedSlotSnapshot {
21
+ used: number;
22
+ max: number;
23
+ }
24
+
25
+ /** Compute the nested-slot budget from the global worker semaphore. */
26
+ export function defaultNestedSlotBudget(globalWorkerSemaphore: number): number {
27
+ return Math.max(1, Math.floor(globalWorkerSemaphore / 2));
28
+ }
29
+
30
+ export class NestedSlotBudget {
31
+ private readonly held = new Set<string>();
32
+ readonly max: number;
33
+
34
+ constructor(globalWorkerSemaphore: number, override?: number) {
35
+ if (override !== undefined && (!Number.isFinite(override) || override < 1)) {
36
+ throw new TypeError(`nesting.maxSlots must be a positive integer, got ${String(override)}`);
37
+ }
38
+ this.max = override ?? defaultNestedSlotBudget(globalWorkerSemaphore);
39
+ }
40
+
41
+ /**
42
+ * Try to reserve one nested slot for the given grandchild id.
43
+ * Fail-fast: returns false when the budget is exhausted — the caller MUST
44
+ * reject the delegate immediately (never queue).
45
+ */
46
+ tryAcquire(grandchildId: string): boolean {
47
+ if (this.held.has(grandchildId)) return true; // idempotent re-acquire
48
+ if (this.held.size >= this.max) return false;
49
+ this.held.add(grandchildId);
50
+ return true;
51
+ }
52
+
53
+ /** Release the slot held by a finished/dead grandchild. Idempotent. */
54
+ release(grandchildId: string): void {
55
+ this.held.delete(grandchildId);
56
+ }
57
+
58
+ get used(): number {
59
+ return this.held.size;
60
+ }
61
+
62
+ /** Human-facing status line for policy messages: "N/M in flight". */
63
+ get statusLine(): string {
64
+ return `${this.held.size}/${this.max} in flight`;
65
+ }
66
+
67
+ snapshot(): NestedSlotSnapshot {
68
+ return { used: this.held.size, max: this.max };
69
+ }
70
+ }
@@ -2,7 +2,7 @@ import type { AgentConfig } from "../../agents/agent-config.ts";
2
2
  import type { CrewReliabilityConfig, CrewRuntimeConfig } from "../../config/types.ts";
3
3
  import { appendEventAsync } from "../../state/event-log/event-log.ts";
4
4
  import { writeArtifact } from "../../state/stores/artifact-store.ts";
5
- import { saveRunTasksAsync, updateRunStatus } from "../../state/stores/state-store.ts";
5
+ import { loadRunManifestById, saveRunTasksAsync, updateRunStatus } from "../../state/stores/state-store.ts";
6
6
  import type { TeamRunManifest, TeamTaskState } from "../../state/types.ts";
7
7
  import type { WorkflowStep } from "../../workflows/workflow-config.ts";
8
8
  import { createWorkerHeartbeat, touchWorkerHeartbeat } from "../heartbeat/worker-heartbeat.ts";
@@ -115,17 +115,12 @@ export async function runCoalescedTaskGroup(input: CoalescedTaskGroupInput): Pro
115
115
  heartbeatInFlight = true;
116
116
  heartbeatPromise = (async () => {
117
117
  try {
118
- updatedTasks = updatedTasks.map((t) => {
119
- if (!taskIds.includes(t.id)) return t;
120
- // FIND-06 belt-and-suspenders: never replace terminal state or its
121
- // resultArtifact with a heartbeat-only snapshot.
122
- if (t.status === "completed" || t.status === "failed" || t.status === "cancelled") return t;
123
- return {
124
- ...t,
125
- heartbeat: touchWorkerHeartbeat(t.heartbeat ?? createWorkerHeartbeat(t.id), { alive: true }),
126
- };
127
- });
128
- await saveRunTasksAsync(manifest, updatedTasks);
118
+ // R15-3: persist heartbeats for ONLY this group's tasks via a fresh
119
+ // disk read. The old closure `updatedTasks` is a dispatch-time
120
+ // snapshot of the FULL task array — a sibling cancelled on disk after
121
+ // dispatch could be un-cancelled by a late heartbeat save (the map
122
+ // only mutated group tasks, but the SAVE wrote the whole stale array).
123
+ await persistGroupHeartbeats(manifest, taskIds);
129
124
  } catch {
130
125
  // Run may have been pruned mid-dispatch — best-effort only.
131
126
  } finally {
@@ -134,9 +129,12 @@ export async function runCoalescedTaskGroup(input: CoalescedTaskGroupInput): Pro
134
129
  // save was in flight, re-write the (now-terminal) updatedTasks so
135
130
  // our late snapshot cannot leave stale pre-terminal state on disk.
136
131
  // Runs after our own save resolves, so it always lands last.
132
+ // R15-3: the repair now fresh-reads disk and re-applies ONLY the
133
+ // group's terminal state — the old full-array write could resurrect
134
+ // a sibling cancelled on disk (same flaw as the heartbeat save).
137
135
  if (finalWriteStarted) {
138
136
  try {
139
- await saveRunTasksAsync(manifest, updatedTasks);
137
+ await repairGroupTerminalWrite(manifest, taskIds, updatedTasks);
140
138
  } catch {
141
139
  // best-effort repair — same swallow policy as the heartbeat.
142
140
  }
@@ -298,6 +296,59 @@ export async function runCoalescedTaskGroup(input: CoalescedTaskGroupInput): Pro
298
296
  return { manifest: updatedManifest, tasks: updatedTasks, taskIds, rawOutput, success };
299
297
  }
300
298
 
299
+ /**
300
+ * R15-3: heartbeat save scoped to this coalesced group only.
301
+ *
302
+ * Previously the heartbeat wrote the dispatch-time closure `updatedTasks` —
303
+ * the FULL task array including sibling tasks outside this group. A sibling
304
+ * cancelled on disk after dispatch (external cancel / reconciler) could be
305
+ * un-cancelled ("resurrected") by a late heartbeat save, because the map only
306
+ * mutated group tasks but the SAVE wrote the whole stale array. Fix: fresh
307
+ * re-read tasks from disk (loadRunManifestById — the established fresh-read
308
+ * pattern used by team-runner) and write back ONLY the group's own task ids
309
+ * with heartbeats touched; sibling tasks are preserved untouched from the
310
+ * fresh disk read. Keeps the FIND-06 'never replace terminal state' guard.
311
+ */
312
+ async function persistGroupHeartbeats(manifest: TeamRunManifest, taskIds: string[]): Promise<void> {
313
+ const fresh = loadRunManifestById(manifest.cwd, manifest.runId);
314
+ if (!fresh) return; // run pruned mid-dispatch — best-effort only.
315
+ const merged = fresh.tasks.map((t) => {
316
+ if (!taskIds.includes(t.id)) return t; // sibling tasks: preserved untouched.
317
+ // FIND-06 belt-and-suspenders: never replace terminal state or its
318
+ // resultArtifact with a heartbeat-only snapshot.
319
+ if (t.status === "completed" || t.status === "failed" || t.status === "cancelled") return t;
320
+ return {
321
+ ...t,
322
+ heartbeat: touchWorkerHeartbeat(t.heartbeat ?? createWorkerHeartbeat(t.id), { alive: true }),
323
+ };
324
+ });
325
+ await saveRunTasksAsync(manifest, merged);
326
+ }
327
+
328
+ /**
329
+ * R15-3: FIND-06 P1 repair — re-apply the group's terminal state after a late
330
+ * heartbeat save may have landed last. Fresh-reads disk so sibling tasks are
331
+ * preserved untouched; only the group's own terminal state from `updatedTasks`
332
+ * is re-applied, and a disk-terminal group task is never flipped (same
333
+ * belt-and-suspenders guard as the heartbeat).
334
+ */
335
+ async function repairGroupTerminalWrite(manifest: TeamRunManifest, taskIds: string[], updatedTasks: TeamTaskState[]): Promise<void> {
336
+ const fresh = loadRunManifestById(manifest.cwd, manifest.runId);
337
+ if (!fresh) return; // run pruned mid-dispatch — best-effort only.
338
+ const groupById = new Map(updatedTasks.map((t) => [t.id, t] as const));
339
+ const merged = fresh.tasks.map((t) => {
340
+ if (!taskIds.includes(t.id)) return t; // sibling tasks: preserved untouched.
341
+ if (t.status === "completed" || t.status === "failed" || t.status === "cancelled") return t;
342
+ return groupById.get(t.id) ?? t;
343
+ });
344
+ await saveRunTasksAsync(manifest, merged);
345
+ }
346
+
347
+ /** @internal 3.5 R15-3 test export — exercise the heartbeat/repair writes directly. */
348
+ export const __test__persistGroupHeartbeats = persistGroupHeartbeats;
349
+ /** @internal 3.5 R15-3 test export — exercise the terminal repair write directly. */
350
+ export const __test__repairGroupTerminalWrite = repairGroupTerminalWrite;
351
+
301
352
  async function buildCoalescedPrompt(
302
353
  manifest: TeamRunManifest,
303
354
  step: WorkflowStep,
@@ -48,16 +48,6 @@ export function buildTaskGraphIndex(tasks: TeamTaskState[]): TaskGraphIndex {
48
48
  return fresh;
49
49
  }
50
50
 
51
- /** Test/diagnostic helper — invalidate the WeakMap-backed index cache. Not
52
- * used by production code paths; the WeakMap self-invalidates when a tasks
53
- * array goes out of scope. Provided for parity with `clearStablePrefixCache`. */
54
- export function clearTaskGraphIndexCache(): void {
55
- // WeakMap has no `.clear()`; rely on GC. Exposed as a no-op stub so callers
56
- // that import `clearStablePrefixCache` (also a no-op stub for symmetry) can
57
- // adopt a parallel API if needed in the future. Documented as no-op rather
58
- // than removed so the API stays discoverable.
59
- }
60
-
61
51
  function taskById(tasks: TeamTaskState[]): Map<string, TeamTaskState> {
62
52
  return new Map(tasks.map((task) => [task.id, task]));
63
53
  }