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,7 +1,7 @@
1
1
  import { spawn } from "node:child_process";
2
2
  import * as fs from "node:fs";
3
3
  import type { AgentConfig } from "../../agents/agent-config.ts";
4
- import { DEFAULT_CHILD_PI } from "../../config/defaults.ts";
4
+ import { getCrewEnv } from "../../config/env-vars.ts";
5
5
  import { registerChildProcess, unregisterChildProcess } from "../../extension/crew-cleanup.ts";
6
6
  import type { WorkerExitStatus } from "../../state/types.ts";
7
7
  import { logInternalError } from "../../utils/internal-error.ts";
@@ -14,6 +14,8 @@ import { buildFinalChildPiSpawnOptions, prepareSpawnContext } from "./child-pi-s
14
14
  import { ChildPiSteeringController } from "./child-pi-steering.ts";
15
15
  // Internal helpers for active-child bookkeeping (extracted to child-pi-kill.ts).
16
16
  import { ChildPiLineObserver } from "./child-pi-streams.ts";
17
+ // Phase 2.3: the six timer constructs moved to child-pi-timers.ts (pure motion).
18
+ import { createChildPiTimers } from "./child-pi-timers.ts";
17
19
  import { runMockChildPi } from "./mock-fixtures.ts";
18
20
 
19
21
  // ── Re-exports from child-pi-kill.ts (H-7 decomposition step 2) ──
@@ -171,8 +173,25 @@ export interface ChildPiRunInput {
171
173
  * credentials (today's behavior — file paths remain authoritative).
172
174
  * The production wiring passes a closure that delegates to the
173
175
  * session's `CrewBrokerLifecycleController.issueForChild`.
176
+ *
177
+ * ADR-5 §4: the third parameter carries the child's depth for
178
+ * delegate-spawned grandchildren — the issuer gates token minting on
179
+ * `childDepth < resolved maxDepth` (see BrokerIssuer).
174
180
  */
175
- brokerIssuer?: (runId: string, taskId?: string) => Promise<{ socketPath: string; token: string } | undefined>;
181
+ brokerIssuer?: (runId: string, taskId?: string, childDepth?: number) => Promise<{ socketPath: string; token: string } | undefined>;
182
+ /**
183
+ * ADR-5 §3 (governed nesting): explicit child depth for delegate-spawned
184
+ * grandchildren. The spawn policy computes this from the PARENT TASK
185
+ * RECORD (task.depth) — never from this process's env (the root has depth
186
+ * 0, so env-derived depth would wrongly be 1). Expressed as the parent's
187
+ * depth in the base env so the existing `parentDepth + 1` spawn math and
188
+ * the `checkCrewDepth` gate both see the parent's true depth. Undefined =
189
+ * normal worker spawn (unchanged env-derived behavior).
190
+ */
191
+ depthOverride?: number;
192
+ /** Base env for depth computation (defaults to process.env). Advanced —
193
+ * used with depthOverride by the root-side delegate handler. */
194
+ env?: NodeJS.ProcessEnv;
176
195
  }
177
196
 
178
197
  export interface ChildPiRunResult {
@@ -248,7 +267,20 @@ export async function runChildPi(input: ChildPiRunInput): Promise<ChildPiRunResu
248
267
  input.inheritContext === true && input.parentContext
249
268
  ? `${input.parentContext}\n\n---\n# Child Worker Task\n${input.task}`
250
269
  : input.task;
251
- const depth = checkCrewDepth(input.maxDepth);
270
+ // ADR-5 §3 depthOverride: delegate-spawned grandchildren carry their depth
271
+ // from the parent task RECORD. Expressed as the parent's depth in the base
272
+ // env so BOTH the depth gate and the spawn env builder (parentDepth + 1)
273
+ // see the parent's true depth — the root process's env (depth 0) is never
274
+ // consulted for grandchildren.
275
+ const depthEnv =
276
+ input.depthOverride !== undefined
277
+ ? {
278
+ ...(input.env ?? process.env),
279
+ PI_CREW_DEPTH: String(input.depthOverride - 1),
280
+ PI_TEAMS_DEPTH: String(input.depthOverride - 1),
281
+ }
282
+ : undefined;
283
+ const depth = checkCrewDepth(input.maxDepth, depthEnv);
252
284
  if (depth.blocked)
253
285
  return {
254
286
  exitCode: 1,
@@ -271,7 +303,10 @@ export async function runChildPi(input: ChildPiRunInput): Promise<ChildPiRunResu
271
303
  const brokerIssuer = input.brokerIssuer ?? getActiveBrokerIssuer();
272
304
  if (!brokerSpawn && brokerIssuer && input.runId) {
273
305
  try {
274
- brokerSpawn = await brokerIssuer(input.runId, input.agentId);
306
+ // ADR-5 §4: thread the child depth so the issuer can contain broker
307
+ // credentials at depths that may not delegate (depth-2 at default
308
+ // maxDepth=2 gets NO socket/token — env containment AC).
309
+ brokerSpawn = await brokerIssuer(input.runId, input.agentId, input.depthOverride);
275
310
  } catch (error) {
276
311
  // H8 (2026-08-10): surface the silent degradation. Previously this
277
312
  // swallowed ALL issuer failures (token-rotation race, broker socket
@@ -288,7 +323,7 @@ export async function runChildPi(input: ChildPiRunInput): Promise<ChildPiRunResu
288
323
  brokerSpawn = undefined;
289
324
  }
290
325
  }
291
- const spawnPrep = prepareSpawnContext(brokerSpawn ? { ...input, brokerSpawn } : input, effectiveTask);
326
+ const spawnPrep = prepareSpawnContext(brokerSpawn ? { ...input, brokerSpawn } : input, effectiveTask, depthEnv);
292
327
  if (spawnPrep.kind === "aborted") return spawnPrep.result;
293
328
  const { spawnSpec, mergedEnv, tempDir, builtEnv } = spawnPrep.ctx;
294
329
  try {
@@ -328,9 +363,6 @@ export async function runChildPi(input: ChildPiRunInput): Promise<ChildPiRunResu
328
363
  let settled = false;
329
364
  let childExited = false;
330
365
  let postExitGuardCleanup: (() => void) | undefined;
331
- let finalDrainTimer: NodeJS.Timeout | undefined;
332
- let hardKillTimer: NodeJS.Timeout | undefined;
333
- let noResponseTimer: NodeJS.Timeout | undefined;
334
366
  const finalDrainMs = input.finalDrainMs ?? FINAL_DRAIN_MS;
335
367
  const hardKillMs = input.hardKillMs ?? HARD_KILL_MS;
336
368
  // Phase-0 diagnostic (HB-003a): track the final-drain race that produces
@@ -353,7 +385,7 @@ export async function runChildPi(input: ChildPiRunInput): Promise<ChildPiRunResu
353
385
  // built-in default.
354
386
  const RESPONSE_TIMEOUT_MIN_MS = 1_000;
355
387
  const RESPONSE_TIMEOUT_MAX_MS = 3_600_000;
356
- const responseTimeoutEnv = Number.parseInt(process.env.PI_TEAMS_CHILD_RESPONSE_TIMEOUT_MS ?? "", 10);
388
+ const responseTimeoutEnv = Number.parseInt(getCrewEnv("PI_TEAMS_CHILD_RESPONSE_TIMEOUT_MS") ?? "", 10);
357
389
  const envInRange =
358
390
  Number.isFinite(responseTimeoutEnv) &&
359
391
  responseTimeoutEnv >= RESPONSE_TIMEOUT_MIN_MS &&
@@ -382,82 +414,52 @@ export async function runChildPi(input: ChildPiRunInput): Promise<ChildPiRunResu
382
414
  input.signal?.addEventListener("abort", onParentAbort, {
383
415
  once: true,
384
416
  });
385
- const restartNoResponseTimer = (): void => {
386
- if (responseTimeoutMs <= 0) return;
387
- if (noResponseTimer) clearTimeout(noResponseTimer);
388
- noResponseTimer = setTimeout(() => {
389
- responseTimeoutHit = true;
390
- // P0-1: snapshot the bounded stderr accumulator once for this timer fire.
391
- const stderr = stderrTail.value();
392
- // Capture stderr at timeout moment for debugging
393
- // SEC-1: redact secrets before embedding in lifecycle event so
394
- // worker-emitted secrets (API keys etc.) don't bypass redaction.
395
- const timeoutStderr = redactStderrExcerpt(stderr, 1024); // Last 1KB of stderr (redacted, SEC-1)
396
- input.onLifecycleEvent?.({
397
- type: "response_timeout",
398
- pid: child.pid,
399
- error: `No output for ${responseTimeoutMs}ms`,
400
- ts: new Date().toISOString(),
401
- stderr: timeoutStderr || undefined,
402
- });
403
- killProcessTree(child.pid, child);
404
- try {
405
- child.kill(process.platform === "win32" ? undefined : "SIGTERM");
406
- } catch (error) {
407
- logInternalError("child-pi.response-timeout-term", error, `pid=${child.pid}`);
408
- }
409
- // #3 hardening: if the child never exits (zombie) and neither the
410
- // 'exit' nor 'close' event ever fires, the promise would hang forever.
411
- // SIGKILL fires ~3s after SIGTERM via hardKillTimer in killProcessPid,
412
- // but on platforms where SIGKILL also fails (e.g. permission issues),
413
- // add a bounded safety settle so the promise always resolves. Using
414
- // hardKillMs + 2s as the safety window: enough for SIGKILL to work
415
- // normally, but forces settle if the process is truly immortal.
416
- // NOTE: we do NOT clear hardKillTimer here (that would defeat its purpose);
417
- // we intentionally add a parallel safety path.
418
- const SAFETY_SETTLE_MS = HARD_KILL_MS + 2000;
419
- const safetyTimer = setTimeout(() => {
420
- if (settled || childExited) return;
421
- logInternalError(
422
- "child-pi.settle-safety-fired",
423
- new Error(`Child did not exit within ${SAFETY_SETTLE_MS}ms of kill; forcing settle`),
424
- `pid=${child.pid}, responseTimeoutMs=${responseTimeoutMs}`,
425
- );
426
- // Verify the child is still alive before forcing settle.
427
- // If it somehow exited between childExited=false and here, the
428
- // settled/childExited guard prevents double-settle (harmless but noisy).
429
- try {
430
- process.kill(child.pid!, 0);
431
- // Child still alive — force settle with timeout error.
432
- const timeoutErr = `Child Pi produced no new output for ${responseTimeoutMs}ms; killed but did not exit within ${SAFETY_SETTLE_MS}ms (possible zombie).`;
433
- void settle({
434
- exitCode: null,
435
- stdout: stdoutTail.value(),
436
- stderr: stderrTail.value(),
437
- error: timeoutErr,
438
- exitStatus: {
439
- exitCode: null,
440
- cancelled: abortRequested,
441
- timedOut: true,
442
- killed: hardKilled,
443
- cleanupErrors,
444
- finalDrainMs,
445
- crashClass: "timeout",
446
- },
447
- });
448
- } catch {
449
- // ESRCH / EPERM — child is already gone. The 'exit'/'close' handler
450
- // will fire shortly (or already fired in a race). Let it settle normally.
451
- }
452
- }, SAFETY_SETTLE_MS);
453
- safetyTimer.unref();
454
- }, responseTimeoutMs);
455
- noResponseTimer.unref();
456
- };
457
- const clearNoResponseTimer = (): void => {
458
- if (noResponseTimer) clearTimeout(noResponseTimer);
459
- noResponseTimer = undefined;
460
- };
417
+ // Phase 2.3: the six timer constructs (noResponseTimer, finalDrainTimer,
418
+ // hardKillTimer, safetyTimer, cancelHardKill, pollHandle) live in
419
+ // child-pi-timers.ts. Mutable flags are shared via getters/setters so
420
+ // the timer callbacks observe the same values as the handlers below.
421
+ // The methods are destructured so the call sites keep their original
422
+ // source shape (HB-003a source-contract test asserts the steering guard
423
+ // directly precedes `restartNoResponseTimer()`).
424
+ const {
425
+ restartNoResponseTimer,
426
+ clearNoResponseTimer,
427
+ clearFinalDrainTimers,
428
+ armFinalDrain,
429
+ hasFinalDrainTimer,
430
+ armCancelHardKill,
431
+ clearAll,
432
+ } = createChildPiTimers({
433
+ child,
434
+ input,
435
+ responseTimeoutMs,
436
+ finalDrainMs,
437
+ hardKillMs,
438
+ stdoutTail,
439
+ stderrTail,
440
+ cleanupErrors,
441
+ getSettle: () => settle,
442
+ redactStderrExcerpt,
443
+ state: {
444
+ getSettled: () => settled,
445
+ getChildExited: () => childExited,
446
+ setResponseTimeoutHit: (value) => {
447
+ responseTimeoutHit = value;
448
+ },
449
+ getHardKilled: () => hardKilled,
450
+ setHardKilled: (value) => {
451
+ hardKilled = value;
452
+ },
453
+ setForcedFinalDrain: (value) => {
454
+ forcedFinalDrain = value;
455
+ },
456
+ getLastStdoutActivityMonotonicMs: () => lastStdoutActivityMonotonicMs,
457
+ setFinalDrainFiredMonotonicMs: (value) => {
458
+ finalDrainFiredMonotonicMs = value;
459
+ },
460
+ getAbortRequested: () => abortRequested,
461
+ },
462
+ });
461
463
  restartNoResponseTimer();
462
464
  const lineObserver = new ChildPiLineObserver({
463
465
  ...input,
@@ -483,114 +485,13 @@ export async function runChildPi(input: ChildPiRunInput): Promise<ChildPiRunResu
483
485
  // regardless of what onJsonEvent does.
484
486
  lastStdoutActivityMonotonicMs = performance.now();
485
487
  input.onJsonEvent?.(event);
486
- if (!isFinalAssistantEvent(event) || childExited || settled || finalDrainTimer) return;
488
+ if (!isFinalAssistantEvent(event) || childExited || settled || hasFinalDrainTimer()) return;
487
489
  finalAssistantEventMonotonicMs = performance.now();
488
490
  finalDrainArmed = true; // Phase-0 diagnostic: track that a drain timer was created.
489
- // F12: alongside the 5 s ceiling timer, start a polling watcher
490
- // that fires the drain early if stdout goes quiet for `quietMs`
491
- // after the final assistant event. Heavy children that emit a
492
- // stopReason=stop message_end and then sit idle will exit in
493
- // ~quietMs (default 800 ms) instead of up to up to 5 s. unref() so
494
- // the poller never holds the event loop on shutdown.
495
- // NOTE: The polling watcher is NOT explicitly cleared on process exit.
496
- // This is safe because: (1) it's unref()'d, so it won't prevent exit;
497
- // (2) the `settled || childExited` guard at the top prevents firing
498
- // after the child has exited; (3) sending SIGTERM to an already-
499
- // exiting process is harmless. The `finalDrainQuietMs` config allows
500
- // disabling this behavior (set >= finalDrainMs, e.g., 10000).
501
- const quietMs = input.finalDrainQuietMs ?? DEFAULT_CHILD_PI.finalDrainQuietMs;
502
- if (quietMs < (input.finalDrainMs ?? DEFAULT_CHILD_PI.finalDrainMs)) {
503
- const pollHandle = setInterval(() => {
504
- if (settled || childExited) {
505
- clearInterval(pollHandle);
506
- pollHandle.unref();
507
- return;
508
- }
509
- const sinceLast = performance.now() - lastStdoutActivityMonotonicMs;
510
- if (sinceLast >= quietMs) {
511
- clearInterval(pollHandle);
512
- pollHandle.unref();
513
- // Trigger the same drain path as the 5 s timer:
514
- // mark forced, fire final_drain lifecycle, SIGTERM.
515
- forcedFinalDrain = true;
516
- finalDrainFiredMonotonicMs = performance.now();
517
- input.onLifecycleEvent?.({
518
- type: "final_drain",
519
- pid: child.pid,
520
- ts: new Date().toISOString(),
521
- reason: "stdout-quiet",
522
- });
523
- try {
524
- child.kill(process.platform === "win32" ? undefined : "SIGTERM");
525
- } catch (error) {
526
- logInternalError("child-pi.quiet-drain-term", error, `pid=${child.pid}`);
527
- }
528
- // Mark for hard kill fallback so the existing timer is
529
- // still reaped if it ever fires later.
530
- hardKillTimer = setTimeout(() => {
531
- if (settled || childExited) return;
532
- try {
533
- hardKilled = true;
534
- input.onLifecycleEvent?.({
535
- type: "hard_kill",
536
- pid: child.pid,
537
- ts: new Date().toISOString(),
538
- });
539
- child.kill(process.platform === "win32" ? undefined : "SIGKILL");
540
- } catch (error) {
541
- logInternalError("child-pi.quiet-drain-hard-kill", error, `pid=${child.pid}`);
542
- }
543
- }, hardKillMs);
544
- hardKillTimer.unref();
545
- // Cancel the 5 s ceiling so we don't double-fire.
546
- if (finalDrainTimer) {
547
- clearTimeout(finalDrainTimer);
548
- finalDrainTimer = undefined;
549
- }
550
- }
551
- }, 200);
552
- pollHandle.unref();
553
- }
554
- finalDrainTimer = setTimeout(() => {
555
- if (settled || childExited) return;
556
- forcedFinalDrain = true;
557
- finalDrainFiredMonotonicMs = performance.now(); // Phase-0 diagnostic: race timing.
558
- input.onLifecycleEvent?.({
559
- type: "final_drain",
560
- pid: child.pid,
561
- ts: new Date().toISOString(),
562
- });
563
- try {
564
- child.kill(process.platform === "win32" ? undefined : "SIGTERM");
565
- } catch (error) {
566
- logInternalError("child-pi.final-drain-term", error, `pid=${child.pid}`);
567
- }
568
- hardKillTimer = setTimeout(() => {
569
- if (settled || childExited) return;
570
- try {
571
- hardKilled = true;
572
- input.onLifecycleEvent?.({
573
- type: "hard_kill",
574
- pid: child.pid,
575
- ts: new Date().toISOString(),
576
- });
577
- child.kill(process.platform === "win32" ? undefined : "SIGKILL");
578
- } catch (error) {
579
- logInternalError("child-pi.final-drain-kill", error, `pid=${child.pid}`);
580
- }
581
- }, hardKillMs);
582
- hardKillTimer.unref();
583
- }, finalDrainMs);
584
- finalDrainTimer.unref();
491
+ armFinalDrain();
585
492
  },
586
493
  });
587
494
 
588
- const clearFinalDrainTimers = (): void => {
589
- if (finalDrainTimer) clearTimeout(finalDrainTimer);
590
- if (hardKillTimer) clearTimeout(hardKillTimer);
591
- finalDrainTimer = undefined;
592
- hardKillTimer = undefined;
593
- };
594
495
  const clearPostExitGuard = (): void => {
595
496
  if (postExitGuardCleanup) {
596
497
  postExitGuardCleanup();
@@ -598,8 +499,10 @@ export async function runChildPi(input: ChildPiRunInput): Promise<ChildPiRunResu
598
499
  }
599
500
  };
600
501
  const clearChildPiTimeouts = (): void => {
601
- clearNoResponseTimer();
602
- clearFinalDrainTimers();
502
+ // R6-F1: clearAll() covers all six timer constructs
503
+ // (incl. cancelHardKill — previously a local const inside abort()
504
+ // that leaked past settle on short-lived children).
505
+ clearAll();
603
506
  clearPostExitGuard();
604
507
  };
605
508
 
@@ -720,17 +623,10 @@ export async function runChildPi(input: ChildPiRunInput): Promise<ChildPiRunResu
720
623
  // 3.5 — fast-escalate to SIGKILL within 200ms on explicit cancel
721
624
  // so /team-cancel completes round-trip well under the operator
722
625
  // expectation. The standard finalDrainMs / HARD_KILL_MS paths
723
- // are for graceful drain, not user-initiated cancel.
724
- const cancelHardKill = setTimeout(() => {
725
- if (settled || childExited) return;
726
- try {
727
- hardKilled = true;
728
- child.kill(process.platform === "win32" ? undefined : "SIGKILL");
729
- } catch (error) {
730
- logInternalError("child-pi.cancel-fast-kill", error, `pid=${child.pid}`);
731
- }
732
- }, 200);
733
- cancelHardKill.unref();
626
+ // are for graceful drain, not user-initiated cancel. R6-F1: the
627
+ // timer handle is owned by child-pi-timers.ts and cleared by
628
+ // clearAll() on settle (was previously an unreachable local const).
629
+ armCancelHardKill();
734
630
  };
735
631
 
736
632
  input.signal?.addEventListener("abort", abort, { once: true });
@@ -13,6 +13,7 @@
13
13
  import * as fs from "node:fs";
14
14
  import * as os from "node:os";
15
15
  import * as path from "node:path";
16
+ import { getCrewEnv } from "../../config/env-vars.ts";
16
17
  import { atomicWriteFile } from "../../state/atomic-write.ts";
17
18
  import { logInternalError } from "../../utils/internal-error.ts";
18
19
  import type { ChildPiRunInput, ChildPiRunResult } from "./child-pi.ts";
@@ -32,12 +33,12 @@ export async function runMockChildPi(
32
33
  effectiveTask: string,
33
34
  observe: (input: ChildPiRunInput, text: string) => Promise<void>,
34
35
  ): Promise<ChildPiRunResult | undefined> {
35
- const mock = process.env.PI_TEAMS_MOCK_CHILD_PI;
36
+ const mock = getCrewEnv("PI_TEAMS_MOCK_CHILD_PI");
36
37
  if (!mock) return undefined;
37
38
 
38
39
  // SECURITY (Issue #2): see module docstring — PI_CREW_ALLOW_MOCK is only
39
40
  // checked in the parent process scope; it is never passed to children.
40
- const allowMock = process.env.PI_CREW_ALLOW_MOCK === "1" || process.env.PI_CREW_ALLOW_MOCK === "true";
41
+ const allowMock = getCrewEnv("PI_CREW_ALLOW_MOCK") === "1" || getCrewEnv("PI_CREW_ALLOW_MOCK") === "true";
41
42
  if (!allowMock) {
42
43
  return {
43
44
  exitCode: 1,
@@ -54,6 +55,19 @@ export async function runMockChildPi(
54
55
  return { exitCode: 0, stdout, stderr: "" };
55
56
  }
56
57
 
58
+ if (mock === "json-slow-success") {
59
+ // T1/WP-1 (mid-run steer test): same JSON event shape as json-success,
60
+ // but sleeps briefly BEFORE emitting so the task stays in `running` — the
61
+ // steering window the steer tool's T-S1 guard must permit. Bounded
62
+ // (default 1500ms, cap 5000ms) via PI_TEAMS_MOCK_STEER_WINDOW_MS.
63
+ const windowMs = Number(getCrewEnv("PI_TEAMS_MOCK_STEER_WINDOW_MS") ?? "1500");
64
+ await new Promise((resolve) => setTimeout(resolve, Math.min(windowMs, 5000)));
65
+ const text = `[MOCK] JSON success for ${input.agent.name}`;
66
+ const stdout = `${JSON.stringify({ type: "message", message: { role: "assistant", content: [{ type: "text", text }] } })}\n${JSON.stringify({ type: "message_end", usage: { input: 10, output: 5, cost: 0.001, turns: 1 } })}\n`;
67
+ await observe(input, stdout);
68
+ return { exitCode: 0, stdout, stderr: "" };
69
+ }
70
+
57
71
  if (mock === "json-success" || mock === "adaptive-plan") {
58
72
  const text =
59
73
  mock === "adaptive-plan" && effectiveTask.includes("ADAPTIVE_PLAN_JSON_START")