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