pi-crew 0.11.1 → 0.11.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 +39 -9
- package/README.md +161 -1036
- package/agents/verifier.md +18 -7
- package/dist/index.mjs +744 -91462
- package/docs/README.md +57 -46
- package/docs/architecture.md +87 -33
- package/docs/commands-reference.md +9 -5
- package/docs/troubleshooting.md +3 -2
- package/package.json +1 -3
- package/schema.json +29 -0
- package/skills/real-test-pi-crew/SKILL.md +193 -36
- package/src/agents/agent-config.ts +1 -1
- package/src/agents/discover-agents.ts +1 -1
- package/src/config/config-validation.ts +15 -0
- package/src/config/config.ts +47 -13
- package/src/config/env-vars.ts +35 -0
- package/src/config/types.ts +19 -0
- package/src/errors.ts +2 -2
- package/src/extension/async-notifier.ts +23 -0
- package/src/extension/help.ts +21 -10
- package/src/extension/knowledge-injection.ts +2 -1
- package/src/extension/management.ts +8 -3
- package/src/extension/notification-sink.ts +17 -0
- package/src/extension/registration/command-utils.ts +28 -2
- package/src/extension/registration/commands/dashboard.ts +11 -1
- package/src/extension/registration/commands/manage.ts +31 -15
- package/src/extension/registration/commands/run.ts +24 -2
- package/src/extension/registration/commands/shared.ts +23 -0
- package/src/extension/registration/commands/status.ts +25 -2
- package/src/extension/registration/context-builder.ts +8 -2
- package/src/extension/registration/health-notify-policy.ts +100 -0
- package/src/extension/registration/lazy-configurers.ts +35 -0
- package/src/extension/registration/lifecycle-handlers.ts +91 -30
- package/src/extension/registration/lifecycle.ts +75 -10
- package/src/extension/registration/observability.ts +98 -35
- package/src/extension/registration/registration-types.ts +7 -5
- package/src/extension/registration/runtime-cleanup.ts +9 -3
- package/src/extension/registration/subagent-helpers.ts +38 -0
- package/src/extension/registration/wire-cross-extension.ts +28 -0
- package/src/extension/run-compare.ts +220 -0
- package/src/extension/run-export.ts +37 -5
- package/src/extension/run-maintenance.ts +155 -5
- package/src/extension/team-tool/dispatch/index.ts +3 -2
- package/src/extension/team-tool/dispatch/manage.ts +5 -2
- package/src/extension/team-tool/goal.ts +4 -1
- package/src/extension/team-tool/handle-settings.ts +19 -2
- package/src/extension/team-tool/health-monitor.ts +21 -7
- package/src/extension/team-tool/lifecycle-actions.ts +49 -1
- package/src/extension/team-tool/plan.ts +10 -0
- package/src/extension/team-tool/routing-hint.ts +63 -0
- package/src/extension/team-tool/status.ts +4 -0
- package/src/extension/team-tool.ts +52 -6
- package/src/extension/webhook-notify.ts +382 -0
- package/src/observability/metric-sink.ts +12 -2
- package/src/prompt/prompt-runtime.ts +82 -31
- package/src/prompt/worker-events-channel.ts +12 -0
- package/src/runtime/README.md +1 -1
- package/src/runtime/async-runner.ts +87 -1
- package/src/runtime/background-runner.ts +313 -234
- package/src/runtime/broker/crew-broker.ts +17 -11
- package/src/runtime/broker/delegate/shadow-lifecycle.ts +92 -0
- package/src/runtime/broker/wait-status-cache.ts +1 -1
- package/src/runtime/child-pi/child-pi-timers.ts +1 -1
- package/src/runtime/child-pi/mock-fixtures.ts +48 -0
- package/src/runtime/crew-agent-records.ts +337 -45
- package/src/runtime/deadletter.ts +43 -1
- package/src/runtime/delegate-spawn.ts +5 -1
- package/src/runtime/dispatch-batch.ts +72 -5
- package/src/runtime/goal-workflow/goal-loop-runner.ts +73 -4
- package/src/runtime/heartbeat/heartbeat-watcher.ts +7 -0
- package/src/runtime/model/model-fallback.ts +21 -1
- package/src/runtime/model/pi-args.ts +8 -10
- package/src/runtime/recovery/crash-recovery.ts +25 -1
- package/src/runtime/run-worker.ts +12 -1
- package/src/runtime/scheduling/global-worker-cap.ts +13 -6
- package/src/runtime/scheduling/run-coalesced-task-group.ts +27 -1
- package/src/runtime/scheduling/scheduler.ts +49 -13
- package/src/runtime/scheduling/semaphore.ts +148 -20
- package/src/runtime/scratchpad/README.md +1 -1
- package/src/runtime/scratchpad/protocol.ts +1 -1
- package/src/runtime/settings-store.ts +1 -1
- package/src/runtime/skill-instructions.ts +22 -0
- package/src/runtime/stale-reconciler.ts +85 -13
- package/src/runtime/task-runner/pre-execution.ts +26 -2
- package/src/runtime/task-runner/prompt-builder.ts +142 -45
- package/src/runtime/task-runner.ts +21 -1
- package/src/runtime/team-runner.ts +38 -1
- package/src/runtime/workspace-lock.ts +4 -1
- package/src/schema/config-schema.ts +14 -0
- package/src/schema/team-tool-schema.ts +17 -0
- package/src/state/atomic-write.ts +53 -0
- package/src/state/contracts.ts +109 -0
- package/src/state/coordination/locks.ts +191 -33
- package/src/state/coordination/mailbox.ts +140 -15
- package/src/state/crew-init.ts +87 -12
- package/src/state/event-log/cursor.ts +37 -1
- package/src/state/event-log/event-log-rotation.ts +72 -7
- package/src/state/stores/active-run-registry.ts +13 -1
- package/src/state/stores/state-store.ts +112 -22
- package/src/state/types.ts +4 -0
- package/src/ui/dashboard-panes/agents-pane.ts +11 -2
- package/src/ui/heartbeat-aggregator.ts +34 -0
- package/src/ui/keybinding-map.ts +22 -4
- package/src/ui/live-conversation-overlay.ts +6 -3
- package/src/ui/run-dashboard.ts +98 -5
- package/src/ui/run-snapshot-cache.ts +18 -1
- package/src/ui/spinner.ts +26 -2
- package/src/ui/tool-progress-formatter.ts +2 -1
- package/src/ui/tool-renderers/brief-mode.ts +2 -1
- package/src/ui/tool-renderers/index.ts +3 -3
- package/src/utils/incremental-reader.ts +11 -3
- package/src/utils/paths.ts +94 -12
- package/src/utils/project-markers.ts +40 -0
- package/src/worktree/worktree-manager.ts +206 -26
- package/workflows/distill.workflow.md +3 -3
- package/workflows/fast-fix.workflow.md +1 -1
- package/workflows/plan-execute.workflow.md +1 -1
- package/workflows/review.workflow.md +1 -1
- package/workflows/strict-fast-fix.workflow.md +1 -1
- package/docs/migration-v0.4-v0.5.md +0 -208
- package/docs/runtime-flow.md +0 -148
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
3
4
|
import { allAgents, discoverAgents } from "../agents/discover-agents.ts";
|
|
4
5
|
import { loadConfig } from "../config/config.ts";
|
|
5
6
|
import { getCrewEnv } from "../config/env-vars.ts";
|
|
@@ -419,6 +420,251 @@ function runCleanup(
|
|
|
419
420
|
// Used by the module-level catch to signal that finally should call process.exit(1).
|
|
420
421
|
let exitDueToRejection = false;
|
|
421
422
|
|
|
423
|
+
export interface ExecuteBackgroundRunOptions {
|
|
424
|
+
/** Abort signal — the detached runner passes its controller's signal (SIGINT/SIGTERM/watchdog); the inline test seam passes none. */
|
|
425
|
+
signal?: AbortSignal;
|
|
426
|
+
/**
|
|
427
|
+
* Set PI_CREW_BACKGROUND_MODE=1 (detached-runner observability marker).
|
|
428
|
+
* SKIPPED by the in-process test seam: it must not pollute the host test
|
|
429
|
+
* process env (task-runner routes events by this flag).
|
|
430
|
+
*/
|
|
431
|
+
markBackgroundMode?: boolean;
|
|
432
|
+
/**
|
|
433
|
+
* Progress log sink. Defaults to console.log — the detached runner's
|
|
434
|
+
* console is redirected to background.log by main(); the inline test seam
|
|
435
|
+
* appends to background.log directly.
|
|
436
|
+
*/
|
|
437
|
+
log?: (message: string) => void;
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
/**
|
|
441
|
+
* The background-run EXECUTION CORE shared by two hosts:
|
|
442
|
+
* 1. the detached runner process (main() below), and
|
|
443
|
+
* 2. the in-process async test seam (async-runner.ts spawnBackgroundTeamRun
|
|
444
|
+
* with PI_CREW_TEST_ASYNC_INLINE=1 + PI_CREW_ALLOW_MOCK=1).
|
|
445
|
+
*
|
|
446
|
+
* Contains NO process-level side effects beyond the run itself: no console
|
|
447
|
+
* redirect, no signal handlers, no watchdog/keepalive/parent-guard, no exit
|
|
448
|
+
* codes. Hosts own those. This is why the seam kills the Windows Defender
|
|
449
|
+
* first-spawn stall (no second process to come up inside a test deadline)
|
|
450
|
+
* and the orphan-tmpdir leak (the run dies with the test process — nothing
|
|
451
|
+
* detached survives to write state after the test's rmSync).
|
|
452
|
+
*/
|
|
453
|
+
export async function executeBackgroundRun(
|
|
454
|
+
input: { manifest: TeamRunManifest; tasks: TeamTaskState[] },
|
|
455
|
+
opts: ExecuteBackgroundRunOptions = {},
|
|
456
|
+
): Promise<{ manifest: TeamRunManifest; tasks: TeamTaskState[] }> {
|
|
457
|
+
const log = opts.log ?? ((message: string) => console.log(message));
|
|
458
|
+
let manifest = input.manifest;
|
|
459
|
+
let tasks = input.tasks;
|
|
460
|
+
const cwd = manifest.cwd;
|
|
461
|
+
// Inline seam passes no signal — a never-aborted dummy is behaviorally
|
|
462
|
+
// identical ("no cancellation source") for the goal-loop/dwf/team-run
|
|
463
|
+
// inputs below that require a non-optional AbortSignal.
|
|
464
|
+
const signal: AbortSignal = opts.signal ?? new AbortController().signal;
|
|
465
|
+
debugLog(`[background-runner] about to call discoverAgents`);
|
|
466
|
+
const agents = allAgents(discoverAgents(cwd));
|
|
467
|
+
debugLog(`[background-runner] discoverAgents done, ${agents.length} agents`);
|
|
468
|
+
// Round 27 (BUG 2): openSync returned an fd that was never closed → FD
|
|
469
|
+
// leak per background runner startup. Close it in a finally (matches the
|
|
470
|
+
// canonical pattern in checkpoint.ts:83 and event-log.ts:582).
|
|
471
|
+
try {
|
|
472
|
+
const fd = fs.openSync(manifest.eventsPath, "a");
|
|
473
|
+
try {
|
|
474
|
+
fs.fsyncSync(fd);
|
|
475
|
+
} finally {
|
|
476
|
+
try {
|
|
477
|
+
fs.closeSync(fd);
|
|
478
|
+
} catch {
|
|
479
|
+
/* best-effort */
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
} catch {
|
|
483
|
+
/* best-effort */
|
|
484
|
+
} // FORCE flush so we see this before death
|
|
485
|
+
// Fix round-4 CRITICAL: goal-loop and dynamic-workflow manifests use SYNTHETIC
|
|
486
|
+
// team/workflow names not in discoverTeams/discoverWorkflows. The team+workflow
|
|
487
|
+
// lookup below would throw "Team not found" BEFORE the runKind switch, making the
|
|
488
|
+
// goal feature unreachable from background. Short-circuit the new runKinds here.
|
|
489
|
+
if (opts.markBackgroundMode) process.env.PI_CREW_BACKGROUND_MODE = "1";
|
|
490
|
+
let earlyResult: { manifest: TeamRunManifest; tasks: TeamTaskState[] } | undefined;
|
|
491
|
+
let result: { manifest: TeamRunManifest; tasks: TeamTaskState[] } | undefined;
|
|
492
|
+
if (manifest.runKind === "goal-loop" || manifest.runKind === "dynamic-workflow") {
|
|
493
|
+
debugLog(`[background-runner] short-circuiting ${manifest.runKind} (synthetic team/workflow)`);
|
|
494
|
+
if (manifest.runKind === "goal-loop") {
|
|
495
|
+
// LAZY: defer dynamic import of ./goal-loop-runner.ts to its call site.
|
|
496
|
+
const { runGoalLoop } = await import("./goal-workflow/goal-loop-runner.ts");
|
|
497
|
+
// LAZY: defer dynamic import of ./goal-state-store.ts to its call site.
|
|
498
|
+
const { GoalStore } = await import("./goal-workflow/goal-state-store.ts");
|
|
499
|
+
// LAZY: defer dynamic import of ../agents/discover-agents.ts to its call site.
|
|
500
|
+
const { discoverAgents, allAgents } = await import("../agents/discover-agents.ts");
|
|
501
|
+
const store = new GoalStore(manifest.cwd);
|
|
502
|
+
const goalState = store.load(manifest.runId);
|
|
503
|
+
if (!goalState) throw new Error(`runKind="goal-loop" but GoalLoopState '${manifest.runId}' not found (cwd=${manifest.cwd})`);
|
|
504
|
+
const goalResult = await runGoalLoop({
|
|
505
|
+
goalState,
|
|
506
|
+
manifest,
|
|
507
|
+
signal,
|
|
508
|
+
deps: {
|
|
509
|
+
discoverAgents: (c: string) => allAgents(discoverAgents(c)),
|
|
510
|
+
},
|
|
511
|
+
});
|
|
512
|
+
// Fix P1-1 + round-6 #5: persist terminal status reflecting the goal's actual outcome,
|
|
513
|
+
// not a blanket 'completed'. Map goal state → manifest status.
|
|
514
|
+
const goalStatusToRunStatus: Record<string, TeamRunManifest["status"]> = {
|
|
515
|
+
achieved: "completed",
|
|
516
|
+
max_turns: "completed",
|
|
517
|
+
budget_exceeded: "completed",
|
|
518
|
+
blocked: "blocked",
|
|
519
|
+
cancelled: "cancelled",
|
|
520
|
+
paused: "blocked",
|
|
521
|
+
running: "running",
|
|
522
|
+
};
|
|
523
|
+
const runStatus = goalStatusToRunStatus[goalResult.goalState.state] ?? "completed";
|
|
524
|
+
const finalGoalManifest: TeamRunManifest = {
|
|
525
|
+
...goalResult.manifest,
|
|
526
|
+
status: runStatus,
|
|
527
|
+
updatedAt: new Date().toISOString(),
|
|
528
|
+
};
|
|
529
|
+
await saveRunManifestAsync(finalGoalManifest);
|
|
530
|
+
earlyResult = {
|
|
531
|
+
manifest: finalGoalManifest,
|
|
532
|
+
tasks: goalResult.tasks,
|
|
533
|
+
};
|
|
534
|
+
} else {
|
|
535
|
+
// LAZY: defer dynamic import of ./dynamic-workflow-runner.ts to its call site.
|
|
536
|
+
const { runDynamicWorkflow } = await import("./goal-workflow/dynamic-workflow-runner.ts");
|
|
537
|
+
// LAZY: defer dynamic import of ../workflows/discover-workflows.ts to its call site.
|
|
538
|
+
const { allWorkflows, discoverWorkflows } = await import("../workflows/discover-workflows.ts");
|
|
539
|
+
const wf = allWorkflows(discoverWorkflows(manifest.cwd)).find((w) => w.name === manifest.workflow);
|
|
540
|
+
if (wf?.runtime !== "dynamic" || !wf.dynamicScript)
|
|
541
|
+
throw new Error(`runKind="dynamic-workflow" but workflow '${manifest.workflow}' is not dynamic (runId=${manifest.runId})`);
|
|
542
|
+
const dwfResult = await runDynamicWorkflow({
|
|
543
|
+
manifest,
|
|
544
|
+
workflow: wf as import("../workflows/workflow-config.ts").DynamicWorkflowConfig,
|
|
545
|
+
signal,
|
|
546
|
+
tokenBudget: wf.maxTokenBudget,
|
|
547
|
+
});
|
|
548
|
+
await saveRunManifestAsync(dwfResult.manifest);
|
|
549
|
+
earlyResult = dwfResult;
|
|
550
|
+
}
|
|
551
|
+
log(`[background-runner] ${manifest.runKind} returned, status=${earlyResult.manifest.status}`);
|
|
552
|
+
result = earlyResult;
|
|
553
|
+
}
|
|
554
|
+
if (!earlyResult) {
|
|
555
|
+
debugLog(`[background-runner] calling directTeamAndWorkflowFromRun`);
|
|
556
|
+
const direct = directTeamAndWorkflowFromRun(manifest, tasks, agents);
|
|
557
|
+
debugLog(`[background-runner] direct done, finding team`);
|
|
558
|
+
const team = direct?.team ?? allTeams(discoverTeams(cwd)).find((candidate) => candidate.name === manifest.team);
|
|
559
|
+
if (!team) throw new Error(`Team '${manifest.team}' not found.`);
|
|
560
|
+
debugLog(`[background-runner] team=${team.name}, finding workflow`);
|
|
561
|
+
const baseWorkflow =
|
|
562
|
+
direct?.workflow ?? allWorkflows(discoverWorkflows(cwd)).find((candidate) => candidate.name === manifest.workflow);
|
|
563
|
+
if (!baseWorkflow) throw new Error(`Workflow '${manifest.workflow ?? ""}' not found.`);
|
|
564
|
+
debugLog(`[background-runner] workflow=${baseWorkflow.name}`);
|
|
565
|
+
const workflow = expandParallelResearchWorkflow(baseWorkflow, cwd);
|
|
566
|
+
debugLog(`[background-runner] loading config`);
|
|
567
|
+
const loadedConfig = loadConfig(cwd);
|
|
568
|
+
const runConfig =
|
|
569
|
+
manifest.runConfig && typeof manifest.runConfig === "object" && !Array.isArray(manifest.runConfig)
|
|
570
|
+
? (manifest.runConfig as typeof loadedConfig.config)
|
|
571
|
+
: loadedConfig.config;
|
|
572
|
+
const runtime = manifest.runtimeResolution
|
|
573
|
+
? {
|
|
574
|
+
kind: manifest.runtimeResolution.kind,
|
|
575
|
+
requestedMode: manifest.runtimeResolution.requestedMode,
|
|
576
|
+
available: manifest.runtimeResolution.available,
|
|
577
|
+
fallback: manifest.runtimeResolution.fallback,
|
|
578
|
+
steer: manifest.runtimeResolution.kind === "live-session",
|
|
579
|
+
resume: manifest.runtimeResolution.kind === "live-session",
|
|
580
|
+
liveToolActivity: manifest.runtimeResolution.kind === "live-session",
|
|
581
|
+
transcript: manifest.runtimeResolution.kind !== "scaffold",
|
|
582
|
+
reason: manifest.runtimeResolution.reason,
|
|
583
|
+
safety: manifest.runtimeResolution.safety,
|
|
584
|
+
}
|
|
585
|
+
: await resolveCrewRuntime(runConfig);
|
|
586
|
+
const runtimeResolution = manifest.runtimeResolution ?? runtimeResolutionState(runtime);
|
|
587
|
+
manifest = {
|
|
588
|
+
...manifest,
|
|
589
|
+
runtimeResolution,
|
|
590
|
+
runConfig,
|
|
591
|
+
updatedAt: new Date().toISOString(),
|
|
592
|
+
};
|
|
593
|
+
await saveRunManifestAsync(manifest);
|
|
594
|
+
appendEventBuffered(manifest.eventsPath, {
|
|
595
|
+
type: "runtime.resolved",
|
|
596
|
+
runId: manifest.runId,
|
|
597
|
+
message: `Runtime resolved: ${runtime.kind} safety=${runtime.safety}`,
|
|
598
|
+
data: { runtimeResolution, async: true },
|
|
599
|
+
}).catch((e) => logInternalError("background-runner.buffered", e, "type=runtime.resolved"));
|
|
600
|
+
if (runtime.safety === "blocked")
|
|
601
|
+
throw new Error(runtime.reason ?? "Child worker execution is disabled; refusing to create no-op scaffold subagents.");
|
|
602
|
+
const executeWorkers = runtime.kind !== "scaffold";
|
|
603
|
+
// Use ownerSessionId for workspaceId to ensure agents are only visible to the session that spawned them.
|
|
604
|
+
// manifest.cwd would cause cross-session visibility since all sessions share the same project directory.
|
|
605
|
+
// Mark this as background mode so task-runner writes events to background.log for debugging.
|
|
606
|
+
if (opts.markBackgroundMode) process.env.PI_CREW_BACKGROUND_MODE = "1";
|
|
607
|
+
// BUG #17: Keep-alive interval (NOT unref'd) prevents event loop from exiting
|
|
608
|
+
// during jiti compilation of team-runner.ts. Without this, the event loop
|
|
609
|
+
// can drain when import() blocks, causing the process to exit prematurely.
|
|
610
|
+
// NOTE: abortController is already created above (before heartbeat/interrupt guard start)
|
|
611
|
+
// so it is available here and its signal is passed through to executeTeamRun → child-pi.
|
|
612
|
+
|
|
613
|
+
debugLog(`[background-runner] dispatching runKind=${manifest.runKind ?? "team-run"}`);
|
|
614
|
+
try {
|
|
615
|
+
// Fix round-4: goal-loop/dynamic-workflow handled by the short-circuit above.
|
|
616
|
+
// This switch now only carries the traditional team-run path.
|
|
617
|
+
switch (manifest.runKind ?? "team-run") {
|
|
618
|
+
default: {
|
|
619
|
+
// Existing "team-run" path — unchanged behavior.
|
|
620
|
+
// Forward budget fields from manifest (set by team-tool/run.ts
|
|
621
|
+
// from params.budgetTotal/etc.) so the team-runner's
|
|
622
|
+
// checkPerTaskBudget guard actually arms.
|
|
623
|
+
result = await executeTeamRun({
|
|
624
|
+
manifest,
|
|
625
|
+
tasks,
|
|
626
|
+
team,
|
|
627
|
+
workflow,
|
|
628
|
+
agents,
|
|
629
|
+
executeWorkers,
|
|
630
|
+
limits: runConfig.limits,
|
|
631
|
+
runtime,
|
|
632
|
+
runtimeConfig: runConfig.runtime,
|
|
633
|
+
skillOverride: manifest.skillOverride,
|
|
634
|
+
// Restore the caller's model routing inputs (see RunModelContext):
|
|
635
|
+
// this process has no ExtensionContext, so without these the
|
|
636
|
+
// `model=` override and the inherited session model are lost and
|
|
637
|
+
// every worker falls back to the first models.json entry.
|
|
638
|
+
...restoredModelRouting(manifest),
|
|
639
|
+
reliability: runConfig.reliability,
|
|
640
|
+
workspaceId: manifest.ownerSessionId ?? manifest.cwd,
|
|
641
|
+
signal: opts.signal,
|
|
642
|
+
...(manifest.budgetTotal !== undefined ? { budgetTotal: manifest.budgetTotal } : {}),
|
|
643
|
+
...(manifest.budgetWarning !== undefined ? { budgetWarning: manifest.budgetWarning } : {}),
|
|
644
|
+
...(manifest.budgetAbort !== undefined ? { budgetAbort: manifest.budgetAbort } : {}),
|
|
645
|
+
...(manifest.budgetUnlimited !== undefined ? { budgetUnlimited: manifest.budgetUnlimited } : {}),
|
|
646
|
+
});
|
|
647
|
+
break;
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
log(`[background-runner] executeTeamRun returned, status=${result.manifest.status}`);
|
|
651
|
+
} catch (execError) {
|
|
652
|
+
log(`[background-runner] executeTeamRun THREW: ${errorMessage(execError)}`);
|
|
653
|
+
log(`[background-runner] stack: ${execError instanceof Error ? execError.stack : "N/A"}`);
|
|
654
|
+
throw execError;
|
|
655
|
+
}
|
|
656
|
+
} // close if (!earlyResult) — team-run setup+execute done; earlyResult path skips to here
|
|
657
|
+
manifest = result!.manifest;
|
|
658
|
+
tasks = result!.tasks;
|
|
659
|
+
appendEventBuffered(manifest.eventsPath, {
|
|
660
|
+
type: "async.completed",
|
|
661
|
+
runId: manifest.runId,
|
|
662
|
+
data: { status: manifest.status, tasks: tasks.length },
|
|
663
|
+
}).catch((e) => logInternalError("background-runner.buffered", e, "type=async.completed"));
|
|
664
|
+
log(`[background-runner] async.completed written, status=${manifest.status}`);
|
|
665
|
+
return { manifest, tasks };
|
|
666
|
+
}
|
|
667
|
+
|
|
422
668
|
async function main(): Promise<void> {
|
|
423
669
|
// FIX: Store logFd so it can be closed on exit to prevent file descriptor leak
|
|
424
670
|
let logFd: number | undefined;
|
|
@@ -754,209 +1000,9 @@ async function main(): Promise<void> {
|
|
|
754
1000
|
}, MAX_BACKGROUND_RUN_MS);
|
|
755
1001
|
|
|
756
1002
|
try {
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
// Round 27 (BUG 2): openSync returned an fd that was never closed → FD
|
|
761
|
-
// leak per background runner startup. Close it in a finally (matches the
|
|
762
|
-
// canonical pattern in checkpoint.ts:83 and event-log.ts:582).
|
|
763
|
-
try {
|
|
764
|
-
const fd = fs.openSync(manifest.eventsPath, "a");
|
|
765
|
-
try {
|
|
766
|
-
fs.fsyncSync(fd);
|
|
767
|
-
} finally {
|
|
768
|
-
try {
|
|
769
|
-
fs.closeSync(fd);
|
|
770
|
-
} catch {
|
|
771
|
-
/* best-effort */
|
|
772
|
-
}
|
|
773
|
-
}
|
|
774
|
-
} catch {
|
|
775
|
-
/* best-effort */
|
|
776
|
-
} // FORCE flush so we see this before death
|
|
777
|
-
// Fix round-4 CRITICAL: goal-loop and dynamic-workflow manifests use SYNTHETIC
|
|
778
|
-
// team/workflow names not in discoverTeams/discoverWorkflows. The team+workflow
|
|
779
|
-
// lookup below would throw "Team not found" BEFORE the runKind switch, making the
|
|
780
|
-
// goal feature unreachable from background. Short-circuit the new runKinds here.
|
|
781
|
-
process.env.PI_CREW_BACKGROUND_MODE = "1";
|
|
782
|
-
let earlyResult: { manifest: TeamRunManifest; tasks: TeamTaskState[] } | undefined;
|
|
783
|
-
let result: { manifest: TeamRunManifest; tasks: TeamTaskState[] } | undefined;
|
|
784
|
-
if (manifest.runKind === "goal-loop" || manifest.runKind === "dynamic-workflow") {
|
|
785
|
-
debugLog(`[background-runner] short-circuiting ${manifest.runKind} (synthetic team/workflow)`);
|
|
786
|
-
if (manifest.runKind === "goal-loop") {
|
|
787
|
-
// LAZY: defer dynamic import of ./goal-loop-runner.ts to its call site.
|
|
788
|
-
const { runGoalLoop } = await import("./goal-workflow/goal-loop-runner.ts");
|
|
789
|
-
// LAZY: defer dynamic import of ./goal-state-store.ts to its call site.
|
|
790
|
-
const { GoalStore } = await import("./goal-workflow/goal-state-store.ts");
|
|
791
|
-
// LAZY: defer dynamic import of ../agents/discover-agents.ts to its call site.
|
|
792
|
-
const { discoverAgents, allAgents } = await import("../agents/discover-agents.ts");
|
|
793
|
-
const store = new GoalStore(manifest.cwd);
|
|
794
|
-
const goalState = store.load(manifest.runId);
|
|
795
|
-
if (!goalState)
|
|
796
|
-
throw new Error(`runKind="goal-loop" but GoalLoopState '${manifest.runId}' not found (cwd=${manifest.cwd})`);
|
|
797
|
-
const goalResult = await runGoalLoop({
|
|
798
|
-
goalState,
|
|
799
|
-
manifest,
|
|
800
|
-
signal: abortController.signal,
|
|
801
|
-
deps: {
|
|
802
|
-
discoverAgents: (c: string) => allAgents(discoverAgents(c)),
|
|
803
|
-
},
|
|
804
|
-
});
|
|
805
|
-
// Fix P1-1 + round-6 #5: persist terminal status reflecting the goal's actual outcome,
|
|
806
|
-
// not a blanket 'completed'. Map goal state → manifest status.
|
|
807
|
-
const goalStatusToRunStatus: Record<string, TeamRunManifest["status"]> = {
|
|
808
|
-
achieved: "completed",
|
|
809
|
-
max_turns: "completed",
|
|
810
|
-
budget_exceeded: "completed",
|
|
811
|
-
blocked: "blocked",
|
|
812
|
-
cancelled: "cancelled",
|
|
813
|
-
paused: "blocked",
|
|
814
|
-
running: "running",
|
|
815
|
-
};
|
|
816
|
-
const runStatus = goalStatusToRunStatus[goalResult.goalState.state] ?? "completed";
|
|
817
|
-
const finalGoalManifest: TeamRunManifest = {
|
|
818
|
-
...goalResult.manifest,
|
|
819
|
-
status: runStatus,
|
|
820
|
-
updatedAt: new Date().toISOString(),
|
|
821
|
-
};
|
|
822
|
-
await saveRunManifestAsync(finalGoalManifest);
|
|
823
|
-
earlyResult = {
|
|
824
|
-
manifest: finalGoalManifest,
|
|
825
|
-
tasks: goalResult.tasks,
|
|
826
|
-
};
|
|
827
|
-
} else {
|
|
828
|
-
// LAZY: defer dynamic import of ./dynamic-workflow-runner.ts to its call site.
|
|
829
|
-
const { runDynamicWorkflow } = await import("./goal-workflow/dynamic-workflow-runner.ts");
|
|
830
|
-
// LAZY: defer dynamic import of ../workflows/discover-workflows.ts to its call site.
|
|
831
|
-
const { allWorkflows, discoverWorkflows } = await import("../workflows/discover-workflows.ts");
|
|
832
|
-
const wf = allWorkflows(discoverWorkflows(manifest.cwd)).find((w) => w.name === manifest.workflow);
|
|
833
|
-
if (wf?.runtime !== "dynamic" || !wf.dynamicScript)
|
|
834
|
-
throw new Error(
|
|
835
|
-
`runKind="dynamic-workflow" but workflow '${manifest.workflow}' is not dynamic (runId=${manifest.runId})`,
|
|
836
|
-
);
|
|
837
|
-
const dwfResult = await runDynamicWorkflow({
|
|
838
|
-
manifest,
|
|
839
|
-
workflow: wf as import("../workflows/workflow-config.ts").DynamicWorkflowConfig,
|
|
840
|
-
signal: abortController.signal,
|
|
841
|
-
tokenBudget: wf.maxTokenBudget,
|
|
842
|
-
});
|
|
843
|
-
await saveRunManifestAsync(dwfResult.manifest);
|
|
844
|
-
earlyResult = dwfResult;
|
|
845
|
-
}
|
|
846
|
-
console.log(`[background-runner] ${manifest.runKind} returned, status=${earlyResult.manifest.status}`);
|
|
847
|
-
result = earlyResult;
|
|
848
|
-
}
|
|
849
|
-
if (!earlyResult) {
|
|
850
|
-
debugLog(`[background-runner] calling directTeamAndWorkflowFromRun`);
|
|
851
|
-
const direct = directTeamAndWorkflowFromRun(manifest, tasks, agents);
|
|
852
|
-
debugLog(`[background-runner] direct done, finding team`);
|
|
853
|
-
const team = direct?.team ?? allTeams(discoverTeams(cwd)).find((candidate) => candidate.name === manifest.team);
|
|
854
|
-
if (!team) throw new Error(`Team '${manifest.team}' not found.`);
|
|
855
|
-
debugLog(`[background-runner] team=${team.name}, finding workflow`);
|
|
856
|
-
const baseWorkflow =
|
|
857
|
-
direct?.workflow ?? allWorkflows(discoverWorkflows(cwd)).find((candidate) => candidate.name === manifest.workflow);
|
|
858
|
-
if (!baseWorkflow) throw new Error(`Workflow '${manifest.workflow ?? ""}' not found.`);
|
|
859
|
-
debugLog(`[background-runner] workflow=${baseWorkflow.name}`);
|
|
860
|
-
const workflow = expandParallelResearchWorkflow(baseWorkflow, cwd);
|
|
861
|
-
debugLog(`[background-runner] loading config`);
|
|
862
|
-
const loadedConfig = loadConfig(cwd);
|
|
863
|
-
const runConfig =
|
|
864
|
-
manifest.runConfig && typeof manifest.runConfig === "object" && !Array.isArray(manifest.runConfig)
|
|
865
|
-
? (manifest.runConfig as typeof loadedConfig.config)
|
|
866
|
-
: loadedConfig.config;
|
|
867
|
-
const runtime = manifest.runtimeResolution
|
|
868
|
-
? {
|
|
869
|
-
kind: manifest.runtimeResolution.kind,
|
|
870
|
-
requestedMode: manifest.runtimeResolution.requestedMode,
|
|
871
|
-
available: manifest.runtimeResolution.available,
|
|
872
|
-
fallback: manifest.runtimeResolution.fallback,
|
|
873
|
-
steer: manifest.runtimeResolution.kind === "live-session",
|
|
874
|
-
resume: manifest.runtimeResolution.kind === "live-session",
|
|
875
|
-
liveToolActivity: manifest.runtimeResolution.kind === "live-session",
|
|
876
|
-
transcript: manifest.runtimeResolution.kind !== "scaffold",
|
|
877
|
-
reason: manifest.runtimeResolution.reason,
|
|
878
|
-
safety: manifest.runtimeResolution.safety,
|
|
879
|
-
}
|
|
880
|
-
: await resolveCrewRuntime(runConfig);
|
|
881
|
-
const runtimeResolution = manifest.runtimeResolution ?? runtimeResolutionState(runtime);
|
|
882
|
-
manifest = {
|
|
883
|
-
...manifest,
|
|
884
|
-
runtimeResolution,
|
|
885
|
-
runConfig,
|
|
886
|
-
updatedAt: new Date().toISOString(),
|
|
887
|
-
};
|
|
888
|
-
await saveRunManifestAsync(manifest);
|
|
889
|
-
appendEventBuffered(manifest.eventsPath, {
|
|
890
|
-
type: "runtime.resolved",
|
|
891
|
-
runId: manifest.runId,
|
|
892
|
-
message: `Runtime resolved: ${runtime.kind} safety=${runtime.safety}`,
|
|
893
|
-
data: { runtimeResolution, async: true },
|
|
894
|
-
}).catch((e) => logInternalError("background-runner.buffered", e, "type=runtime.resolved"));
|
|
895
|
-
if (runtime.safety === "blocked")
|
|
896
|
-
throw new Error(runtime.reason ?? "Child worker execution is disabled; refusing to create no-op scaffold subagents.");
|
|
897
|
-
const executeWorkers = runtime.kind !== "scaffold";
|
|
898
|
-
// Use ownerSessionId for workspaceId to ensure agents are only visible to the session that spawned them.
|
|
899
|
-
// manifest.cwd would cause cross-session visibility since all sessions share the same project directory.
|
|
900
|
-
// Mark this as background mode so task-runner writes events to background.log for debugging.
|
|
901
|
-
process.env.PI_CREW_BACKGROUND_MODE = "1";
|
|
902
|
-
// BUG #17: Keep-alive interval (NOT unref'd) prevents event loop from exiting
|
|
903
|
-
// during jiti compilation of team-runner.ts. Without this, the event loop
|
|
904
|
-
// can drain when import() blocks, causing the process to exit prematurely.
|
|
905
|
-
// NOTE: abortController is already created above (before heartbeat/interrupt guard start)
|
|
906
|
-
// so it is available here and its signal is passed through to executeTeamRun → child-pi.
|
|
907
|
-
|
|
908
|
-
debugLog(`[background-runner] dispatching runKind=${manifest.runKind ?? "team-run"}`);
|
|
909
|
-
try {
|
|
910
|
-
// Fix round-4: goal-loop/dynamic-workflow handled by the short-circuit above.
|
|
911
|
-
// This switch now only carries the traditional team-run path.
|
|
912
|
-
switch (manifest.runKind ?? "team-run") {
|
|
913
|
-
default: {
|
|
914
|
-
// Existing "team-run" path — unchanged behavior.
|
|
915
|
-
// Forward budget fields from manifest (set by team-tool/run.ts
|
|
916
|
-
// from params.budgetTotal/etc.) so the team-runner's
|
|
917
|
-
// checkPerTaskBudget guard actually arms.
|
|
918
|
-
result = await executeTeamRun({
|
|
919
|
-
manifest,
|
|
920
|
-
tasks,
|
|
921
|
-
team,
|
|
922
|
-
workflow,
|
|
923
|
-
agents,
|
|
924
|
-
executeWorkers,
|
|
925
|
-
limits: runConfig.limits,
|
|
926
|
-
runtime,
|
|
927
|
-
runtimeConfig: runConfig.runtime,
|
|
928
|
-
skillOverride: manifest.skillOverride,
|
|
929
|
-
// Restore the caller's model routing inputs (see RunModelContext):
|
|
930
|
-
// this process has no ExtensionContext, so without these the
|
|
931
|
-
// `model=` override and the inherited session model are lost and
|
|
932
|
-
// every worker falls back to the first models.json entry.
|
|
933
|
-
...restoredModelRouting(manifest),
|
|
934
|
-
reliability: runConfig.reliability,
|
|
935
|
-
workspaceId: manifest.ownerSessionId ?? manifest.cwd,
|
|
936
|
-
signal: abortController.signal,
|
|
937
|
-
...(manifest.budgetTotal !== undefined ? { budgetTotal: manifest.budgetTotal } : {}),
|
|
938
|
-
...(manifest.budgetWarning !== undefined ? { budgetWarning: manifest.budgetWarning } : {}),
|
|
939
|
-
...(manifest.budgetAbort !== undefined ? { budgetAbort: manifest.budgetAbort } : {}),
|
|
940
|
-
...(manifest.budgetUnlimited !== undefined ? { budgetUnlimited: manifest.budgetUnlimited } : {}),
|
|
941
|
-
});
|
|
942
|
-
break;
|
|
943
|
-
}
|
|
944
|
-
}
|
|
945
|
-
console.log(`[background-runner] executeTeamRun returned, status=${result.manifest.status}`);
|
|
946
|
-
} catch (execError) {
|
|
947
|
-
console.log(`[background-runner] executeTeamRun THREW: ${errorMessage(execError)}`);
|
|
948
|
-
console.log(`[background-runner] stack: ${execError instanceof Error ? execError.stack : "N/A"}`);
|
|
949
|
-
throw execError;
|
|
950
|
-
}
|
|
951
|
-
} // close if (!earlyResult) — team-run setup+execute done; earlyResult path skips to here
|
|
952
|
-
manifest = result!.manifest;
|
|
953
|
-
tasks = result!.tasks;
|
|
954
|
-
appendEventBuffered(manifest.eventsPath, {
|
|
955
|
-
type: "async.completed",
|
|
956
|
-
runId: manifest.runId,
|
|
957
|
-
data: { status: manifest.status, tasks: tasks.length },
|
|
958
|
-
}).catch((e) => logInternalError("background-runner.buffered", e, "type=async.completed"));
|
|
959
|
-
console.log(`[background-runner] async.completed written, status=${manifest.status}`);
|
|
1003
|
+
const executed = await executeBackgroundRun({ manifest, tasks }, { signal: abortController.signal });
|
|
1004
|
+
manifest = executed.manifest;
|
|
1005
|
+
tasks = executed.tasks;
|
|
960
1006
|
if (manifest.status === "failed" || manifest.status === "cancelled" || manifest.status === "blocked") process.exitCode = 1;
|
|
961
1007
|
} catch (error) {
|
|
962
1008
|
// Terminate live agents on failure too — agents are done when the run fails
|
|
@@ -1030,39 +1076,72 @@ async function main(): Promise<void> {
|
|
|
1030
1076
|
// guard didn't fire), .catch() ran instead of the finally block doing the exit.
|
|
1031
1077
|
// New pattern: move await main() inside main() itself, wrapped in try/catch that
|
|
1032
1078
|
// sets exitDueToRejection so the finally block exits with code 1 after cleanup.
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
// notifier/foreground detects the failure immediately, and set exitCode=1
|
|
1046
|
-
// so the process exits non-zero.
|
|
1079
|
+
/**
|
|
1080
|
+
* Entry-point guard: run main() ONLY when this module is the process entry
|
|
1081
|
+
* (detached runner spawned by async-runner, or direct CLI invocation by
|
|
1082
|
+
* integration tests). A plain import — notably the IN-PROCESS async test seam
|
|
1083
|
+
* (async-runner.ts lazy-imports executeBackgroundRun) — must not run a team.
|
|
1084
|
+
* Belt-and-braces: the explicit PI_CREW_BACKGROUND_RUNNER_ENTRY env marker
|
|
1085
|
+
* (set by the async-runner spawn, allowlisted) OR the classic argv[1] check
|
|
1086
|
+
* (keeps direct `node background-runner.ts --cwd ... --run-id ...` spawns in
|
|
1087
|
+
* tests working without the env).
|
|
1088
|
+
*/
|
|
1089
|
+
function shouldRunMainAsEntry(): boolean {
|
|
1090
|
+
if (getCrewEnv("PI_CREW_BACKGROUND_RUNNER_ENTRY") === "1") return true;
|
|
1047
1091
|
try {
|
|
1048
|
-
const
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
|
|
1055
|
-
|
|
1056
|
-
|
|
1057
|
-
|
|
1092
|
+
const argv1 = process.argv[1];
|
|
1093
|
+
if (!argv1) return false;
|
|
1094
|
+
const a = path.resolve(argv1);
|
|
1095
|
+
const b = path.resolve(fileURLToPath(import.meta.url));
|
|
1096
|
+
const eq = (x: string, y: string): boolean => (process.platform === "win32" ? x.toLowerCase() === y.toLowerCase() : x === b);
|
|
1097
|
+
if (eq(a, b)) return true;
|
|
1098
|
+
try {
|
|
1099
|
+
const ra = fs.realpathSync(a);
|
|
1100
|
+
const rb = fs.realpathSync(b);
|
|
1101
|
+
return process.platform === "win32" ? ra.toLowerCase() === rb.toLowerCase() : ra === rb;
|
|
1102
|
+
} catch {
|
|
1103
|
+
return false;
|
|
1058
1104
|
}
|
|
1059
1105
|
} catch {
|
|
1060
|
-
|
|
1106
|
+
return false;
|
|
1107
|
+
}
|
|
1108
|
+
}
|
|
1109
|
+
|
|
1110
|
+
if (shouldRunMainAsEntry()) {
|
|
1111
|
+
try {
|
|
1112
|
+
await main();
|
|
1113
|
+
} catch (err) {
|
|
1114
|
+
console.error(`[background-runner] DEBUG: main() uncaught: ${errorMessage(err)}`);
|
|
1115
|
+
// FIX Issue #1: Set the flag so the finally block's runCleanup() call
|
|
1116
|
+
// will trigger process.exit(1) after cleanup completes. Previously this
|
|
1117
|
+
// called process.exit(1) directly, bypassing the finally block and leaving
|
|
1118
|
+
// orphaned child processes.
|
|
1119
|
+
exitDueToRejection = true;
|
|
1120
|
+
// RT-3 FIX: Startup failures (lock-fail, missing manifest, pre-try throws)
|
|
1121
|
+
// previously wrote NO event and left exitCode at 0. The run stayed 'queued'
|
|
1122
|
+
// until the stale reconciler reaped it. Now write async.failed so the
|
|
1123
|
+
// notifier/foreground detects the failure immediately, and set exitCode=1
|
|
1124
|
+
// so the process exits non-zero.
|
|
1125
|
+
try {
|
|
1126
|
+
const mCwd = argValue("--cwd");
|
|
1127
|
+
const mRunId = argValue("--run-id");
|
|
1128
|
+
if (mCwd && mRunId) {
|
|
1129
|
+
const mEventsPath = createRunPaths(mCwd, mRunId).eventsPath;
|
|
1130
|
+
appendEventBuffered(mEventsPath, {
|
|
1131
|
+
type: "async.failed",
|
|
1132
|
+
runId: mRunId,
|
|
1133
|
+
message: errorMessage(err),
|
|
1134
|
+
data: { stack: err instanceof Error ? err.stack : undefined },
|
|
1135
|
+
}).catch((e) => logInternalError("background-runner.buffered", e, "type=async.failed source=catch"));
|
|
1136
|
+
}
|
|
1137
|
+
} catch {
|
|
1138
|
+
/* best-effort — don't let event-write failure mask the original error */
|
|
1139
|
+
}
|
|
1140
|
+
process.exitCode = 1;
|
|
1141
|
+
// FIX: Call stopParentGuard directly here as a safety net in case the
|
|
1142
|
+
// finally block (which calls runCleanup→stopParentGuard) does not complete.
|
|
1143
|
+
// This ensures the parent guard is stopped in ALL exit paths: normal
|
|
1144
|
+
// completion, unhandled rejection, and fatal errors.
|
|
1145
|
+
stopParentGuard();
|
|
1061
1146
|
}
|
|
1062
|
-
process.exitCode = 1;
|
|
1063
|
-
// FIX: Call stopParentGuard directly here as a safety net in case the
|
|
1064
|
-
// finally block (which calls runCleanup→stopParentGuard) does not complete.
|
|
1065
|
-
// This ensures the parent guard is stopped in ALL exit paths: normal
|
|
1066
|
-
// completion, unhandled rejection, and fatal errors.
|
|
1067
|
-
stopParentGuard();
|
|
1068
1147
|
}
|
|
@@ -41,6 +41,7 @@ import { NestedSlotBudget } from "../scheduling/nested-slots.ts";
|
|
|
41
41
|
import { evaluateDelegateAdmission } from "../spawn-policy.ts";
|
|
42
42
|
import { type BrokerToken, BrokerTokenRegistry } from "./crew-broker-tokens.ts";
|
|
43
43
|
import { recordDelegateEvent } from "./delegate/delegate-event.ts";
|
|
44
|
+
import { promoteShadowToRunning } from "./delegate/shadow-lifecycle.ts";
|
|
44
45
|
import { fanoutMailboxMessage } from "./mailbox-observer/mailbox-fanout.ts";
|
|
45
46
|
import type { CrewBrokerOptions, ServerConnection } from "./protocol/connection-state.ts";
|
|
46
47
|
import { handleEventsSince } from "./protocol/events-replay.ts";
|
|
@@ -1449,6 +1450,10 @@ export class CrewBroker {
|
|
|
1449
1450
|
});
|
|
1450
1451
|
return { code: "bad-params" as const, message: `delegate: parent task '${parentTaskId}' is ${task.status}, not running` };
|
|
1451
1452
|
}
|
|
1453
|
+
// RR-012 F03: authoritative execution cwd = the parent task's cwd from
|
|
1454
|
+
// this locked fresh read — ONE source for overlap check, shadow record
|
|
1455
|
+
// and spawner input; broker cwd stays for manifest lookup only.
|
|
1456
|
+
const executionCwd = task.cwd;
|
|
1452
1457
|
const catalog = this.options.modelCatalog?.();
|
|
1453
1458
|
// S3 fail-closed: a DEFINED loader yielding undefined is a loader failure.
|
|
1454
1459
|
const effectiveCatalog = this.options.modelCatalog !== undefined ? (catalog ?? []) : undefined;
|
|
@@ -1459,7 +1464,7 @@ export class CrewBroker {
|
|
|
1459
1464
|
t.id !== parentTaskId &&
|
|
1460
1465
|
t.status === "running" &&
|
|
1461
1466
|
(t.role === "executor" || t.role === "test-engineer") &&
|
|
1462
|
-
t.cwd ===
|
|
1467
|
+
t.cwd === executionCwd,
|
|
1463
1468
|
).length;
|
|
1464
1469
|
const decision = evaluateDelegateAdmission({
|
|
1465
1470
|
maxDepth: this.options.nestingMaxDepth ?? resolveCrewMaxDepth(undefined), // config knob > env-clamped 1..10, default 4 (D8; ADR-5 §3)
|
|
@@ -1551,19 +1556,19 @@ export class CrewBroker {
|
|
|
1551
1556
|
agent: "delegate",
|
|
1552
1557
|
title: `delegate: ${(requested as { description?: string }).description ?? subId}`,
|
|
1553
1558
|
status: "queued",
|
|
1554
|
-
cwd:
|
|
1559
|
+
cwd: executionCwd,
|
|
1555
1560
|
dependsOn: [],
|
|
1556
1561
|
depth: decision.childDepth ?? 2,
|
|
1557
1562
|
startedAt: new Date().toISOString(),
|
|
1558
1563
|
} satisfies TeamTaskState,
|
|
1559
1564
|
]);
|
|
1560
|
-
return { code: "ok" as const, decision, reserved };
|
|
1565
|
+
return { code: "ok" as const, decision, reserved, executionCwd };
|
|
1561
1566
|
});
|
|
1562
1567
|
if (admissionOutcome.code !== "ok") {
|
|
1563
1568
|
this.sendError(conn, id, admissionOutcome.code, admissionOutcome.message);
|
|
1564
1569
|
return;
|
|
1565
1570
|
}
|
|
1566
|
-
const { decision, reserved } = admissionOutcome;
|
|
1571
|
+
const { decision, reserved, executionCwd } = admissionOutcome;
|
|
1567
1572
|
this.recordDelegateEvent(loaded.manifest, "delegate.admitted", parentTaskId, {
|
|
1568
1573
|
subId,
|
|
1569
1574
|
childDepth: decision.childDepth,
|
|
@@ -1585,7 +1590,7 @@ export class CrewBroker {
|
|
|
1585
1590
|
let outcome: GrandchildSpawnResult;
|
|
1586
1591
|
try {
|
|
1587
1592
|
outcome = await spawner({
|
|
1588
|
-
cwd,
|
|
1593
|
+
cwd: executionCwd,
|
|
1589
1594
|
runId,
|
|
1590
1595
|
parentTaskId,
|
|
1591
1596
|
subId,
|
|
@@ -1597,6 +1602,10 @@ export class CrewBroker {
|
|
|
1597
1602
|
...(requested.maxTurns !== undefined ? { maxTurns: requested.maxTurns } : {}),
|
|
1598
1603
|
timeoutSec: decision.timeoutSec ?? 900,
|
|
1599
1604
|
depthOverride: decision.childDepth ?? 2,
|
|
1605
|
+
// RR-012 F16: promote shadow queued→running once the process exists.
|
|
1606
|
+
onSpawn: (pid) => {
|
|
1607
|
+
if (pid !== null) promoteShadowToRunning(cwd, runId, subId);
|
|
1608
|
+
},
|
|
1600
1609
|
});
|
|
1601
1610
|
} catch (err) {
|
|
1602
1611
|
outcome = { ok: false, resultText: `delegate spawn failed: ${(err as Error).message}` };
|
|
@@ -1649,12 +1658,9 @@ export class CrewBroker {
|
|
|
1649
1658
|
});
|
|
1650
1659
|
}
|
|
1651
1660
|
}
|
|
1652
|
-
// S1#1:
|
|
1653
|
-
//
|
|
1654
|
-
//
|
|
1655
|
-
// shadow "queued" forever.
|
|
1656
|
-
// S1#1: flip the shadow task to terminal so the subId record does not
|
|
1657
|
-
// linger as "queued" in team status views.
|
|
1661
|
+
// S1#1 (verifier N1): the terminal flip is UNCONDITIONAL — never depends
|
|
1662
|
+
// on a reservation. RR-012 F16: onSpawn promotes the record to "running"
|
|
1663
|
+
// during execution; this flip terminalizes every settle path.
|
|
1658
1664
|
saveRunTasks(
|
|
1659
1665
|
latest.manifest,
|
|
1660
1666
|
tasksToWrite.map((t) =>
|