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.
- package/CHANGELOG.md +222 -0
- package/NOTICE.md +21 -0
- package/README.md +44 -2
- package/agents/analyst.md +1 -1
- package/agents/cold-verifier.md +3 -1
- package/agents/critic.md +1 -1
- package/agents/executor.md +1 -1
- package/agents/explorer.md +1 -1
- package/agents/planner.md +1 -1
- package/agents/reviewer.md +1 -1
- package/agents/security-reviewer.md +1 -1
- package/agents/test-engineer.md +1 -1
- package/agents/verifier.md +1 -1
- package/agents/writer.md +1 -1
- package/dist/index.mjs +68113 -60774
- package/docs/README.md +2 -0
- package/docs/actions-reference.md +31 -0
- package/docs/commands-reference.md +17 -6
- package/docs/resource-formats.md +13 -0
- package/package.json +4 -2
- package/schema.json +503 -91
- package/scripts/resource-sampler.mjs +36 -2
- package/skills/requirements-to-task-packet/SKILL.md +26 -0
- package/skills/widget-rendering/SKILL.md +7 -7
- package/src/agents/agent-config.ts +2 -1
- package/src/agents/discover-agents.ts +23 -14
- package/src/config/config-merge.ts +183 -0
- package/src/config/config-validation.ts +687 -0
- package/src/config/config.ts +22 -864
- package/src/config/defaults.ts +43 -2
- package/src/config/drift-detector.ts +1 -1
- package/src/config/env-vars.ts +691 -0
- package/src/config/role-tools.ts +11 -9
- package/src/config/sanitize-project-config.ts +172 -0
- package/src/config/types.ts +49 -1
- package/src/extension/async-notifier.ts +25 -2
- package/src/extension/crew-cleanup.ts +13 -0
- package/src/extension/crew-vibes/config.ts +2 -1
- package/src/extension/crew-vibes/footer.ts +19 -0
- package/src/extension/crew-vibes/index.ts +11 -1
- package/src/extension/plan-orchestrate.ts +132 -0
- package/src/extension/register.ts +8 -0
- package/src/extension/registration/command-registration.ts +1 -0
- package/src/extension/registration/commands/dashboard.ts +158 -0
- package/src/extension/registration/commands/index.ts +35 -0
- package/src/extension/registration/commands/manage.ts +303 -0
- package/src/extension/registration/commands/run.ts +228 -0
- package/src/extension/registration/commands/shared.ts +639 -0
- package/src/extension/registration/commands/status.ts +60 -0
- package/src/extension/registration/commands.ts +13 -1224
- package/src/extension/registration/foreground-run-controller.ts +10 -2
- package/src/extension/registration/lifecycle-handlers.ts +178 -17
- package/src/extension/registration/runtime-cleanup.ts +23 -5
- package/src/extension/registration/subagent-tools.ts +218 -9
- package/src/extension/registration/team-tool.ts +5 -1
- package/src/extension/registration/ui.ts +5 -4
- package/src/extension/rpc-hmac.ts +5 -3
- package/src/extension/team-tool/api/heartbeat.ts +47 -10
- package/src/extension/team-tool/api/plan-approval.ts +9 -0
- package/src/extension/team-tool/api/task-claims.ts +109 -40
- package/src/extension/team-tool/cancel.ts +84 -50
- package/src/extension/team-tool/dispatch/index.ts +1 -0
- package/src/extension/team-tool/dispatch/run.ts +4 -1
- package/src/extension/team-tool/doctor.ts +103 -1
- package/src/extension/team-tool/orchestrate.ts +66 -1
- package/src/extension/team-tool/plans.ts +192 -0
- package/src/extension/team-tool/respond.ts +197 -65
- package/src/extension/team-tool/run-deadline.ts +35 -3
- package/src/extension/team-tool/run-intent.ts +63 -0
- package/src/extension/team-tool/run.ts +74 -20
- package/src/extension/team-tool/status.ts +84 -26
- package/src/extension/team-tool.ts +11 -2
- package/src/hooks/registry.ts +1 -6
- package/src/i18n.ts +9 -0
- package/src/prompt/prompt-runtime.ts +521 -2
- package/src/prompt/worker-events-channel.ts +173 -0
- package/src/runtime/README.md +8 -8
- package/src/runtime/async-runner.ts +7 -3
- package/src/runtime/background-runner.ts +42 -14
- package/src/runtime/broker/broker-issuer.ts +9 -2
- package/src/runtime/broker/crew-broker-tokens.ts +43 -6
- package/src/runtime/broker/crew-broker.ts +838 -10
- package/src/runtime/broker/wait-status-cache.ts +157 -0
- package/src/runtime/budget-enforcement.ts +281 -0
- package/src/runtime/child-pi/child-pi-constants.ts +8 -0
- package/src/runtime/child-pi/child-pi-spawn.ts +60 -14
- package/src/runtime/child-pi/child-pi-streams.ts +21 -1
- package/src/runtime/child-pi/child-pi-timers.ts +324 -0
- package/src/runtime/child-pi/child-pi.ts +97 -201
- package/src/runtime/child-pi/mock-fixtures.ts +16 -2
- package/src/runtime/crew-agent-records.ts +259 -14
- package/src/runtime/delegate-spawn.ts +148 -0
- package/src/runtime/detached-run-results.ts +90 -0
- package/src/runtime/deterministic-ast.ts +2 -1
- package/src/runtime/dispatch-batch.ts +945 -0
- package/src/runtime/finalize-run.ts +557 -0
- package/src/runtime/goal-workflow/adaptive-plan.ts +116 -15
- package/src/runtime/goal-workflow/dynamic-workflow-runner.ts +8 -3
- package/src/runtime/goal-workflow/goal-state-store.ts +1 -1
- package/src/runtime/group-join.ts +11 -125
- package/src/runtime/live-session/live-session-runtime.ts +26 -1
- package/src/runtime/merge-gate.ts +32 -10
- package/src/runtime/merge-loop.ts +130 -0
- package/src/runtime/model/model-budget-summary.ts +53 -0
- package/src/runtime/model/model-fallback.ts +36 -2
- package/src/runtime/model/pi-args.ts +10 -0
- package/src/runtime/model/provider-extensions.ts +10 -0
- package/src/runtime/orphan-worker-registry.ts +1 -1
- package/src/runtime/output/output-validator.ts +45 -0
- package/src/runtime/parent-guard.ts +3 -1
- package/src/runtime/peer-dep.ts +2 -1
- package/src/runtime/per-write-validator.ts +0 -5
- package/src/runtime/pi-spawn.ts +61 -15
- package/src/runtime/plan-approval.ts +125 -0
- package/src/runtime/plan-replan.ts +151 -0
- package/src/runtime/process-status.ts +16 -1
- package/src/runtime/recovery/checkpoint.ts +0 -18
- package/src/runtime/recovery/crash-recovery.ts +111 -46
- package/src/runtime/run-tracker.ts +77 -10
- package/src/runtime/scheduler-context.ts +98 -0
- package/src/runtime/scheduling/coalesce-tasks.ts +5 -0
- package/src/runtime/scheduling/global-worker-cap.ts +2 -1
- package/src/runtime/scheduling/nested-slots.ts +70 -0
- package/src/runtime/scheduling/run-coalesced-task-group.ts +64 -13
- package/src/runtime/scheduling/task-graph-scheduler.ts +0 -10
- package/src/runtime/settings-store.ts +219 -0
- package/src/runtime/spawn-policy.ts +217 -0
- package/src/runtime/stale-reconciler.ts +87 -6
- package/src/runtime/subagent-manager.ts +25 -1
- package/src/runtime/task-output-context.ts +230 -9
- package/src/runtime/task-packet.ts +23 -1
- package/src/runtime/task-runner/child-executor.ts +106 -7
- package/src/runtime/task-runner/post-execution.ts +125 -1
- package/src/runtime/task-runner/pre-execution.ts +39 -1
- package/src/runtime/task-runner/prompt-builder.ts +51 -1
- package/src/runtime/task-runner/retrieval-orchestrator.ts +72 -18
- package/src/runtime/task-runner/spec-evidence.ts +403 -0
- package/src/runtime/task-runner/state-helpers.ts +26 -24
- package/src/runtime/task-runner.ts +11 -0
- package/src/runtime/team-runner.ts +132 -1673
- package/src/runtime/verification/spec-sandbox.ts +255 -0
- package/src/runtime/verification/verification-gates.ts +3 -2
- package/src/runtime/verification/verification-worktree.ts +2 -1
- package/src/runtime/workflow-phase-advance.ts +100 -0
- package/src/runtime/workspace-tree.ts +9 -0
- package/src/schema/config-schema.ts +66 -26
- package/src/schema/sensitive-config-paths.ts +64 -0
- package/src/schema/team-tool-schema.ts +13 -3
- package/src/state/README.md +4 -10
- package/src/state/atomic-write.ts +20 -3
- package/src/state/contracts.ts +38 -0
- package/src/state/coordination/mailbox.ts +12 -2
- package/src/state/event-log/cursor.ts +223 -0
- package/src/state/event-log/event-log-rotation.ts +12 -4
- package/src/state/event-log/event-log.ts +152 -369
- package/src/state/event-log/sequence-cache.ts +373 -0
- package/src/state/event-log/worker-atomic-writer.ts +2 -1
- package/src/state/stores/active-run-registry.ts +3 -2
- package/src/state/stores/manifest-io.ts +237 -0
- package/src/state/stores/ownership-map.ts +162 -0
- package/src/state/stores/plan-store.ts +241 -0
- package/src/state/stores/run-cache.ts +0 -90
- package/src/state/stores/spec-store.ts +189 -0
- package/src/state/stores/state-store.ts +139 -232
- package/src/state/types.ts +199 -0
- package/src/ui/dashboard-panes/plan-pane.ts +136 -0
- package/src/ui/dashboard-panes/progress-pane.ts +6 -0
- package/src/ui/dashboard-panes/transcript-pane.ts +31 -0
- package/src/ui/dock-footer.ts +49 -0
- package/src/ui/heartbeat-aggregator.ts +9 -1
- package/src/ui/inline-panel/agent-pane.ts +375 -0
- package/src/ui/inline-panel/agent-transcript.ts +338 -0
- package/src/ui/inline-panel/agent-view-overlay.ts +225 -0
- package/src/ui/inline-panel/crew-editor.ts +192 -0
- package/src/ui/inline-panel/index.ts +290 -0
- package/src/ui/inline-panel/panel-rows.ts +37 -0
- package/src/ui/inline-panel/panel-selection.ts +157 -0
- package/src/ui/inline-panel/panel-store.ts +111 -0
- package/src/ui/inline-panel/view-session-store.ts +36 -0
- package/src/ui/keybinding-map.ts +54 -13
- package/src/ui/pi-ui-compat.ts +9 -0
- package/src/ui/powerbar-publisher.ts +52 -1
- package/src/ui/run-dashboard.ts +31 -5
- package/src/ui/run-snapshot-cache.ts +57 -30
- package/src/ui/snapshot-types.ts +6 -1
- package/src/ui/widget/index.ts +176 -22
- package/src/ui/widget/task-list.ts +198 -0
- package/src/ui/widget/widget-formatters.ts +240 -4
- package/src/ui/widget/widget-renderer.ts +243 -38
- package/src/ui/widget/widget-types.ts +11 -0
- package/src/utils/child-process-shield.ts +106 -0
- package/src/utils/file-coalescer.ts +0 -4
- package/src/utils/fs-errno.ts +66 -0
- package/src/utils/fs-watch.ts +1 -1
- package/src/utils/internal-error.ts +3 -1
- package/src/utils/paths.ts +11 -3
- package/src/utils/redaction.ts +7 -0
- package/src/utils/safe-abort.ts +45 -0
- package/src/utils/task-name-generator.ts +1 -8
- package/src/workflows/discover-workflows.ts +20 -2
- package/src/workflows/validate-workflow.ts +7 -1
- package/src/workflows/workflow-config.ts +17 -0
- package/src/workflows/workflow-serializer.ts +3 -0
- package/src/worktree/worktree-manager.ts +22 -0
- package/workflows/default.workflow.md +36 -26
- package/workflows/strict-fast-fix.workflow.md +26 -0
- package/src/agents/agent-search.ts +0 -98
- package/src/benchmark/benchmark-runner.ts +0 -313
- package/src/benchmark/feedback-loop.ts +0 -73
- package/src/config/resilient-parser.ts +0 -117
- package/src/extension/crew-vibes/cat-frames.ts +0 -18
- package/src/extension/result-watcher.ts +0 -139
- package/src/observability/exporters/prometheus-exporter.ts +0 -54
- package/src/observability/metric-retention.ts +0 -64
- package/src/runtime/compaction/compaction-summary.ts +0 -278
- package/src/runtime/errors/crew-errors.ts +0 -162
- package/src/runtime/live-session/intercom-bridge.ts +0 -187
- package/src/runtime/loop-gates.ts +0 -128
- package/src/runtime/metric-parser.ts +0 -36
- package/src/runtime/output/stream-preview.ts +0 -184
- package/src/runtime/output/tool-progress.ts +0 -278
- package/src/runtime/phase-tracker.ts +0 -385
- package/src/runtime/pipeline-runner.ts +0 -523
- package/src/runtime/process/process-lifecycle.ts +0 -491
- package/src/runtime/recovery/retry-runner.ts +0 -330
- package/src/runtime/run-drift.ts +0 -219
- package/src/runtime/task-quality.ts +0 -199
- package/src/runtime/task-runner/run-projection.ts +0 -128
- package/src/runtime/verification/post-checks.ts +0 -142
- package/src/state/coordination/schedule.ts +0 -166
- package/src/state/event-log/jsonl-writer.ts +0 -115
- package/src/state/hook-instinct-bridge.ts +0 -94
- package/src/state/hook-integrations.ts +0 -51
- package/src/state/session-state-map.ts +0 -51
- package/src/state/stores/blob-store.ts +0 -308
- package/src/state/stores/instinct-store.ts +0 -275
- package/src/state/stores/observation-store.ts +0 -176
- package/src/state/tiered-eval.ts +0 -480
- package/src/state/types-eval.ts +0 -58
- package/src/tools/safe-bash-extension.ts +0 -54
- package/src/tools/safe-bash.ts +0 -505
- package/src/ui/agent-management-overlay.ts +0 -160
- package/src/ui/crew-footer.ts +0 -102
- package/src/ui/crew-select-list.ts +0 -114
- package/src/ui/dashboard-panes/capability-pane.ts +0 -77
- package/src/ui/transcript-entries.ts +0 -256
- package/src/utils/conflict-detect.ts +0 -721
- package/src/utils/fingerprint.ts +0 -180
- package/src/utils/gh-protocol.ts +0 -556
- package/src/utils/project-detector.ts +0 -160
- package/src/utils/sse-parser.ts +0 -131
- package/src/workflows/cost-estimator.ts +0 -34
- package/src/workflows/intermediate-store.ts +0 -166
|
@@ -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 {
|
|
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
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
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 ||
|
|
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
|
-
|
|
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
|
-
|
|
602
|
-
|
|
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
|
-
|
|
725
|
-
|
|
726
|
-
|
|
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 =
|
|
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 =
|
|
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")
|