pi-crew 0.11.1 → 0.11.4
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 +82 -9
- package/README.md +161 -1036
- package/agents/verifier.md +18 -7
- package/dist/index.mjs +742 -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 +5 -4
- package/schema.json +29 -0
- package/skills/real-test-pi-crew/SKILL.md +206 -41
- 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 +55 -8
- package/src/extension/crew-input-router.ts +9 -2
- 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/plan-orchestrate.ts +19 -3
- 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 +34 -10
- package/src/extension/registration/commands/status.ts +25 -2
- package/src/extension/registration/context-builder.ts +8 -2
- package/src/extension/registration/foreground-run-controller.ts +2 -2
- package/src/extension/registration/health-notify-policy.ts +100 -0
- package/src/extension/registration/lazy-configurers.ts +38 -0
- package/src/extension/registration/lifecycle-handlers.ts +107 -40
- package/src/extension/registration/lifecycle.ts +77 -10
- package/src/extension/registration/observability.ts +125 -53
- 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/team-tool.ts +9 -2
- package/src/extension/registration/wire-cross-extension.ts +30 -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-manager-command.ts +3 -2
- 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-wrap.ts +7 -0
- package/src/extension/team-tool/goal.ts +16 -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/parallel-dispatch.ts +16 -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/run-intent.ts +1 -1
- package/src/extension/team-tool/run.ts +1 -1
- 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 +123 -1
- package/src/runtime/background-runner.ts +313 -234
- package/src/runtime/broker/crew-broker.ts +76 -71
- package/src/runtime/broker/delegate/delegate-event.ts +12 -8
- package/src/runtime/broker/delegate/shadow-lifecycle.ts +92 -0
- package/src/runtime/broker/protocol/connection-state.ts +10 -0
- package/src/runtime/broker/protocol/lock-busy.ts +76 -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 +21 -3
- package/src/runtime/deterministic-ast.ts +14 -2
- package/src/runtime/dispatch-batch.ts +72 -5
- package/src/runtime/goal-workflow/dynamic-workflow-context.ts +78 -58
- package/src/runtime/goal-workflow/dynamic-workflow-runner.ts +14 -2
- package/src/runtime/goal-workflow/goal-loop-runner.ts +105 -4
- package/src/runtime/group-join.ts +11 -3
- package/src/runtime/heartbeat/heartbeat-watcher.ts +7 -0
- package/src/runtime/live-session/live-session-runtime.ts +1 -1
- package/src/runtime/model/model-fallback.ts +21 -1
- package/src/runtime/model/pi-args.ts +8 -10
- package/src/runtime/model/runtime-warmup.ts +2 -0
- package/src/runtime/output/output-validator.ts +10 -5
- package/src/runtime/plan-approval.ts +14 -1
- package/src/runtime/recovery/crash-recovery.ts +96 -49
- 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/subagent-manager.ts +43 -13
- package/src/runtime/task-runner/child-executor.ts +17 -6
- package/src/runtime/task-runner/post-execution.ts +44 -14
- package/src/runtime/task-runner/pre-execution.ts +26 -2
- package/src/runtime/task-runner/prompt-builder.ts +150 -45
- package/src/runtime/task-runner/retrieval-orchestrator.ts +36 -14
- package/src/runtime/task-runner.ts +28 -1
- package/src/runtime/team-runner.ts +42 -2
- package/src/runtime/verification/verification-worktree.ts +3 -0
- 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 +147 -17
- 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 +120 -23
- package/src/state/types.ts +4 -0
- package/src/ui/dashboard-panes/agents-pane.ts +22 -10
- package/src/ui/heartbeat-aggregator.ts +34 -0
- package/src/ui/inline-panel/index.ts +1 -1
- package/src/ui/keybinding-map.ts +22 -4
- package/src/ui/live-conversation-overlay.ts +6 -3
- package/src/ui/render-diff.ts +19 -6
- package/src/ui/run-dashboard.ts +109 -6
- package/src/ui/run-snapshot-cache.ts +27 -4
- 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/ui/widget/task-list.ts +8 -5
- package/src/ui/widget/widget-model.ts +3 -1
- 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/tsconfig.json +2 -0
- 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
- package/src/ui/card-colors.ts +0 -168
- package/src/utils/completion-dedupe.ts +0 -63
- package/src/utils/resolve-shell.ts +0 -36
|
@@ -10,6 +10,7 @@ import { discoverAgents } from "../../agents/discover-agents.ts";
|
|
|
10
10
|
import { loadConfig } from "../../config/config.ts";
|
|
11
11
|
import { spawnBackgroundTeamRun } from "../../runtime/async-runner.ts";
|
|
12
12
|
import { resolveCrewRuntime } from "../../runtime/model/runtime-resolver.ts";
|
|
13
|
+
import { captureRunModelContext } from "../../runtime/model/session-model.ts";
|
|
13
14
|
import type { TeamToolParamsValue } from "../../schema/team-tool-schema.ts";
|
|
14
15
|
import { appendEventAsync } from "../../state/event-log/event-log.ts";
|
|
15
16
|
import { createRunManifest } from "../../state/stores/state-store.ts";
|
|
@@ -66,13 +67,23 @@ export async function handleParallel(params: TeamToolParamsValue, ctx: TeamConte
|
|
|
66
67
|
|
|
67
68
|
const runtime = await resolveCrewRuntime(config.config);
|
|
68
69
|
|
|
70
|
+
// Finding #1 (full battery 2026-09-29): parallel-dispatched runs re-enter
|
|
71
|
+
// through the detached background-runner, which has no ExtensionContext.
|
|
72
|
+
// Without a persisted modelContext, restoredModelRouting() returns {} and
|
|
73
|
+
// every worker silently falls back to the DEFAULT chain (live: 3 runs died
|
|
74
|
+
// on minimax/MiniMax-M3 402 while the parent session ran zai/glm-5.3).
|
|
75
|
+
// Captured ONCE here (in the parent session's process) so resolveParentModel()
|
|
76
|
+
// sees the live session model; undefined when nothing is worth persisting
|
|
77
|
+
// (older manifests stay byte-identical).
|
|
78
|
+
const modelContext = captureRunModelContext(ctx, typeof params.model === "string" ? params.model : undefined);
|
|
79
|
+
|
|
69
80
|
const launched: Array<{ runId: string; goal: string; agent: string }> = [];
|
|
70
81
|
const errors: Array<{ goal: string; error: string }> = [];
|
|
71
82
|
|
|
72
83
|
// C1: Enforce concurrency limit with batched spawning
|
|
73
84
|
for (let batchStart = 0; batchStart < tasksParam.length; batchStart += concurrency) {
|
|
74
85
|
const batch = tasksParam.slice(batchStart, batchStart + concurrency);
|
|
75
|
-
const batchPromises = batch.map((task) => spawnSingleTask(task, ctx, allAgentsList, team, workflow, runtime));
|
|
86
|
+
const batchPromises = batch.map((task) => spawnSingleTask(task, ctx, allAgentsList, team, workflow, runtime, modelContext));
|
|
76
87
|
const batchResults = await Promise.allSettled(batchPromises);
|
|
77
88
|
for (const res of batchResults) {
|
|
78
89
|
if (res.status === "fulfilled" && res.value.ok) {
|
|
@@ -121,6 +132,7 @@ async function spawnSingleTask(
|
|
|
121
132
|
team: TeamConfig,
|
|
122
133
|
workflow: WorkflowConfig,
|
|
123
134
|
runtime: { available: boolean; kind: string },
|
|
135
|
+
modelContext: ReturnType<typeof captureRunModelContext>,
|
|
124
136
|
): Promise<SpawnResult> {
|
|
125
137
|
try {
|
|
126
138
|
const taskRec = task as Record<string, unknown>;
|
|
@@ -163,6 +175,9 @@ async function spawnSingleTask(
|
|
|
163
175
|
// skipped and any session can cancel/retry parallel-dispatched runs.
|
|
164
176
|
ownerSessionId: ctx.sessionId,
|
|
165
177
|
runKind: "team-run",
|
|
178
|
+
// Finding #1: persist the parent session's model routing so the detached
|
|
179
|
+
// runner restores it instead of default-chaining (see handleParallel).
|
|
180
|
+
modelContext,
|
|
166
181
|
});
|
|
167
182
|
|
|
168
183
|
await appendEventAsync(created.manifest.eventsPath, {
|
|
@@ -5,6 +5,7 @@ import { allWorkflows, discoverWorkflows } from "../../workflows/discover-workfl
|
|
|
5
5
|
import { validateWorkflowForTeam } from "../../workflows/validate-workflow.ts";
|
|
6
6
|
import type { PiTeamsToolResult } from "../tool-result.ts";
|
|
7
7
|
import { result, type TeamContext } from "./context.ts";
|
|
8
|
+
import { resolveRoutingHint } from "./routing-hint.ts";
|
|
8
9
|
|
|
9
10
|
export function handlePlan(params: TeamToolParamsValue, ctx: TeamContext): PiTeamsToolResult {
|
|
10
11
|
const teamName = params.team ?? "default";
|
|
@@ -21,6 +22,12 @@ export function handlePlan(params: TeamToolParamsValue, ctx: TeamContext): PiTea
|
|
|
21
22
|
true,
|
|
22
23
|
);
|
|
23
24
|
const goal = params.goal ?? params.task ?? "(not provided)";
|
|
25
|
+
// DP-04: small-goal routing hint (suggest-only by default; never overrides
|
|
26
|
+
// an explicit team/workflow choice). See routing-hint.ts for the rationale.
|
|
27
|
+
const routingHint = resolveRoutingHint({
|
|
28
|
+
goal,
|
|
29
|
+
explicitOverride: params.team !== undefined || params.workflow !== undefined,
|
|
30
|
+
});
|
|
24
31
|
// ROADMAP T2.2: single-agent composition mode (cliff hedge).
|
|
25
32
|
if (params.singleAgent) {
|
|
26
33
|
const composed = composeSingleAgentPrompt(workflow, goal);
|
|
@@ -46,5 +53,8 @@ export function handlePlan(params: TeamToolParamsValue, ctx: TeamContext): PiTea
|
|
|
46
53
|
`${index + 1}. ${step.id} [${step.role}]${step.dependsOn?.length ? ` after ${step.dependsOn.join(", ")}` : ""}`,
|
|
47
54
|
),
|
|
48
55
|
];
|
|
56
|
+
if (routingHint.suggested && routingHint.message) {
|
|
57
|
+
lines.push("", routingHint.message);
|
|
58
|
+
}
|
|
49
59
|
return result(lines.join("\n"), { action: "plan", status: "ok" });
|
|
50
60
|
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DP-04 (2026-09-22): small-goal routing hint.
|
|
3
|
+
*
|
|
4
|
+
* Measured: a trivial goal through the default team burns 7.6–9k tokens just on
|
|
5
|
+
* the 3 context-setup steps (assess → execute → verify). The engine ALREADY
|
|
6
|
+
* supports `singleAgent: true` (handlePlan → composeSingleAgentPrompt); nothing
|
|
7
|
+
* suggested it for small goals.
|
|
8
|
+
*
|
|
9
|
+
* This module decides the hint. It is deliberately PURE and explainable — no
|
|
10
|
+
* ML, no hidden state — so the routing decision can be asserted in tests and
|
|
11
|
+
* audited from the plan output. Default mode is "suggest" (visible nudge, the
|
|
12
|
+
* caller/agent still decides), NOT a silent switch: this project has been
|
|
13
|
+
* burned by silent routing changes before (the auto-boomerang aliasing trap),
|
|
14
|
+
* so the decision must be visible.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
export type SmallGoalRoutingMode = "off" | "suggest" | "auto";
|
|
18
|
+
|
|
19
|
+
/** Goals at or below this many whitespace tokens are considered "small". */
|
|
20
|
+
export const SMALL_GOAL_TOKEN_THRESHOLD = 40;
|
|
21
|
+
|
|
22
|
+
export interface RoutingHintInput {
|
|
23
|
+
goal: string;
|
|
24
|
+
/** True when the caller supplied an explicit team and/or workflow override. */
|
|
25
|
+
explicitOverride: boolean;
|
|
26
|
+
/** Configured mode; defaults to "suggest". */
|
|
27
|
+
mode?: SmallGoalRoutingMode;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface RoutingHint {
|
|
31
|
+
/** Whether a cheaper single-agent composition is advised. */
|
|
32
|
+
suggested: boolean;
|
|
33
|
+
/** Machine-readable reason (empty when not suggested). */
|
|
34
|
+
reason?: string;
|
|
35
|
+
/** Human-readable, shown in the plan output. */
|
|
36
|
+
message?: string;
|
|
37
|
+
/** Rough token saving estimate for the plan text (informational). */
|
|
38
|
+
estimatedSavings?: string;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** Cheap, deterministic token count (whitespace split) — no tokenizer dep. */
|
|
42
|
+
export function goalTokenCount(goal: string): number {
|
|
43
|
+
const trimmed = goal.trim();
|
|
44
|
+
if (trimmed.length === 0) return 0;
|
|
45
|
+
return trimmed.split(/\s+/).length;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export function resolveRoutingHint(input: RoutingHintInput): RoutingHint {
|
|
49
|
+
const mode = input.mode ?? "suggest";
|
|
50
|
+
if (mode === "off") return { suggested: false };
|
|
51
|
+
// Never second-guess an explicit team/workflow choice.
|
|
52
|
+
if (input.explicitOverride) return { suggested: false };
|
|
53
|
+
const tokens = goalTokenCount(input.goal);
|
|
54
|
+
if (tokens === 0 || tokens > SMALL_GOAL_TOKEN_THRESHOLD) return { suggested: false };
|
|
55
|
+
return {
|
|
56
|
+
suggested: true,
|
|
57
|
+
reason: `goal is small (${tokens} tokens ≤ ${SMALL_GOAL_TOKEN_THRESHOLD})`,
|
|
58
|
+
message:
|
|
59
|
+
`Hint: this goal is small (${tokens} tokens). The default team runs 3 context-setup phases ` +
|
|
60
|
+
`(~8k tokens overhead) before real work — consider \`singleAgent: true\` (or a 2-step chain) to skip them.`,
|
|
61
|
+
estimatedSavings: "~5-6k tokens",
|
|
62
|
+
};
|
|
63
|
+
}
|
|
@@ -73,7 +73,7 @@ const MAX_ANALYSIS_BYTES = 100_000;
|
|
|
73
73
|
var crewInitPromise: Promise<typeof import("../../state/crew-init.ts")> | undefined;
|
|
74
74
|
function loadCrewInit(): Promise<typeof import("../../state/crew-init.ts")> {
|
|
75
75
|
if (!crewInitPromise) {
|
|
76
|
-
crewInitPromise = import("../../state/crew-init.ts");
|
|
76
|
+
crewInitPromise = import("../../state/crew-init.ts"); // LAZY: defer crew-init until first run intent
|
|
77
77
|
}
|
|
78
78
|
return crewInitPromise;
|
|
79
79
|
}
|
|
@@ -61,7 +61,7 @@ import type { RuntimeResolutionState, TeamRunManifest, TeamTaskState } from "../
|
|
|
61
61
|
var crewInitPromise: Promise<typeof import("../../state/crew-init.ts")> | undefined;
|
|
62
62
|
function loadCrewInit(): Promise<typeof import("../../state/crew-init.ts")> {
|
|
63
63
|
if (!crewInitPromise) {
|
|
64
|
-
crewInitPromise = import("../../state/crew-init.ts");
|
|
64
|
+
crewInitPromise = import("../../state/crew-init.ts"); // LAZY: defer crew-init until first run intent
|
|
65
65
|
}
|
|
66
66
|
return crewInitPromise;
|
|
67
67
|
}
|
|
@@ -2,6 +2,7 @@ import { loadConfig } from "../../config/config.ts";
|
|
|
2
2
|
import { applyAttentionState, formatActivityAge, resolveCrewControlConfig } from "../../runtime/agent-control.ts";
|
|
3
3
|
import { extractCommandTrace } from "../../runtime/command-trace.ts";
|
|
4
4
|
import { readCrewAgents } from "../../runtime/crew-agent-records.ts";
|
|
5
|
+
import { deadletterStatusLine } from "../../runtime/deadletter.ts";
|
|
5
6
|
import { evaluateRunEffectiveness } from "../../runtime/effectiveness.ts";
|
|
6
7
|
import { computePhaseProgress } from "../../runtime/phase-progress.ts";
|
|
7
8
|
import { checkProcessLiveness, isActiveRunStatus } from "../../runtime/process-status.ts";
|
|
@@ -150,6 +151,7 @@ export function handleStatus(params: TeamToolParamsValue, ctx: TeamContext): PiT
|
|
|
150
151
|
}
|
|
151
152
|
const counts = new Map<string, number>();
|
|
152
153
|
for (const task of tasks) counts.set(task.status, (counts.get(task.status) ?? 0) + 1);
|
|
154
|
+
const deadletterLine = deadletterStatusLine(manifest);
|
|
153
155
|
const phaseProgress = computePhaseProgress(tasks);
|
|
154
156
|
// PERF (2026-08-24): intentionally NOT passing `limit` here — readEventsCursor's
|
|
155
157
|
// limit is a HEAD cap (oldest-first slice for streaming pagination), not a tail
|
|
@@ -267,6 +269,8 @@ export function handleStatus(params: TeamToolParamsValue, ctx: TeamContext): PiT
|
|
|
267
269
|
)
|
|
268
270
|
: ["- (none)"]),
|
|
269
271
|
`Task counts: ${[...counts.entries()].map(([status, count]) => `${status}=${count}`).join(", ") || "none"}`,
|
|
272
|
+
// US-003 AC-3: surface exhausted-retry failures — only when entries exist.
|
|
273
|
+
...(deadletterLine ? [deadletterLine] : []),
|
|
270
274
|
"Effectiveness:",
|
|
271
275
|
`- observable=${effectiveness.observable}/${Math.max(1, effectiveness.completed)} completed tasks`,
|
|
272
276
|
`- workerExecution=${effectiveness.workerExecution} guard=${effectiveness.guardMode} severity=${effectiveness.severity}`,
|
|
@@ -3,6 +3,7 @@ import * as path from "node:path";
|
|
|
3
3
|
import type { AgentConfig } from "../agents/agent-config.ts";
|
|
4
4
|
import { allAgents, discoverAgents, listDynamicAgents, registerDynamicAgent, unregisterDynamicAgent } from "../agents/discover-agents.ts";
|
|
5
5
|
import { loadConfig } from "../config/config.ts";
|
|
6
|
+
import { DEFAULT_PATHS } from "../config/defaults.ts";
|
|
6
7
|
import { getCrewEnv } from "../config/env-vars.ts";
|
|
7
8
|
// Heavy runtime — lazy-loaded to avoid 1.4s import cost at extension registration.
|
|
8
9
|
// executeTeamRun is only called when a team run actually executes.
|
|
@@ -17,6 +18,7 @@ import { loadRunManifestById, saveRunManifestAsync, saveRunTasks, updateRunStatu
|
|
|
17
18
|
import type { ArtifactDescriptor, TeamRunManifest, TeamTaskState } from "../state/types.ts";
|
|
18
19
|
import { allTeams, discoverTeams } from "../teams/discover-teams.ts";
|
|
19
20
|
import { logInternalError } from "../utils/internal-error.ts";
|
|
21
|
+
import { findRepoRoot, projectCrewRoot, userCrewRoot } from "../utils/paths.ts";
|
|
20
22
|
import { resolveRealContainedPath } from "../utils/safe-paths.ts";
|
|
21
23
|
import { allWorkflows, discoverWorkflows } from "../workflows/discover-workflows.ts";
|
|
22
24
|
import { listRecentRuns } from "./run-index.ts";
|
|
@@ -191,9 +193,9 @@ function artifactKey(artifact: ArtifactDescriptor): string {
|
|
|
191
193
|
* passes) but handlers expect real numbers (TeamToolParamsValue types them as
|
|
192
194
|
* number). This converts the string form back to a number — and treats "" as
|
|
193
195
|
* unset (deleted) — before any domain router reads the param. */
|
|
194
|
-
const LOOSE_NUMERIC_PARAM_KEYS = ["interval", "budgetWarning", "budgetAbort", "tokenBudget", "replyDeadline"] as const;
|
|
196
|
+
const LOOSE_NUMERIC_PARAM_KEYS = ["interval", "budgetWarning", "budgetAbort", "tokenBudget", "replyDeadline", "budgetTotal"] as const;
|
|
195
197
|
|
|
196
|
-
function normalizeLooseNumericFields(params: TeamToolParamsValue): TeamToolParamsValue {
|
|
198
|
+
export function normalizeLooseNumericFields(params: TeamToolParamsValue): TeamToolParamsValue {
|
|
197
199
|
let mutated = false;
|
|
198
200
|
const out: Record<string, unknown> = { ...params };
|
|
199
201
|
for (const key of LOOSE_NUMERIC_PARAM_KEYS) {
|
|
@@ -400,7 +402,10 @@ export async function handleResume(params: TeamToolParamsValue, ctx: TeamContext
|
|
|
400
402
|
data: { runtimeResolution, action: "resume" },
|
|
401
403
|
});
|
|
402
404
|
if (runtime.safety === "blocked") {
|
|
403
|
-
const runningManifest = updateRunStatus(runtimeManifest, "running", "Checking worker runtime availability before resume."
|
|
405
|
+
const runningManifest = updateRunStatus(runtimeManifest, "running", "Checking worker runtime availability before resume.", {
|
|
406
|
+
// Resume is the ONE legitimate terminal-exit flow (finding 8 write guard).
|
|
407
|
+
allowTerminalExit: true,
|
|
408
|
+
});
|
|
404
409
|
const blocked = updateRunStatus(
|
|
405
410
|
runningManifest,
|
|
406
411
|
"blocked",
|
|
@@ -511,6 +516,8 @@ export async function handleResume(params: TeamToolParamsValue, ctx: TeamContext
|
|
|
511
516
|
reliability: decision.executedConfig.reliability,
|
|
512
517
|
metricRegistry: ctx.metricRegistry,
|
|
513
518
|
workspaceId: ctx.sessionId ?? ctx.cwd,
|
|
519
|
+
// Finding 8: resume is the legitimate terminal-exit flow.
|
|
520
|
+
isResume: true,
|
|
514
521
|
});
|
|
515
522
|
return result(
|
|
516
523
|
[
|
|
@@ -652,9 +659,48 @@ export function locateRunCwd(runId: string, baseCwd: string): string | undefined
|
|
|
652
659
|
* - Skips hidden entries (starting with `.`) unless they look like run directories
|
|
653
660
|
* (e.g. .crew, .pi, .tmp-crew-runs)
|
|
654
661
|
*/
|
|
662
|
+
/**
|
|
663
|
+
* F-L1 companion (2026-09-23 correction): a hit counts when the resolved
|
|
664
|
+
* stateRoot sits under EITHER root that `list` unions (run-index
|
|
665
|
+
* `scopedRunRoots` = userCrewRoot + projectCrewRoot), because this function
|
|
666
|
+
* answers "which cwd can read this run's state" — not "was it created here".
|
|
667
|
+
*
|
|
668
|
+
* History: the first F-L1 fix (5d3a3d0f) restricted hits to the candidate
|
|
669
|
+
* cwd's PRIMARY root. That kept `locateRunCwd` from claiming a run created in
|
|
670
|
+
* a sibling, but it also made every by-ID handler (status/events/summary/
|
|
671
|
+
* artifacts/worktrees/api/plans/respond/cancel) reject runs that `list` shows
|
|
672
|
+
* from the user root: `list` unions both roots, `loadRunManifestById` resolves
|
|
673
|
+
* both roots, yet `locateRunCwd` returned undefined → "Run not found" for
|
|
674
|
+
* runs the dashboard had just listed. Measured live: 10/10 user-root runs
|
|
675
|
+
* listed by `action='list'` failed `action='status'`.
|
|
676
|
+
*
|
|
677
|
+
* Sibling isolation is preserved WITHOUT the primary-only restriction: a run
|
|
678
|
+
* living under a sibling's PROJECT root (…/sibling/.crew/state/runs/<id>) is
|
|
679
|
+
* not under the candidate cwd's project root nor the user root, so it still
|
|
680
|
+
* does not match (pinned by locate-run-cwd.test.ts "sibling directory").
|
|
681
|
+
*/
|
|
682
|
+
function runUnderListedRoot(cwd: string, runId: string): boolean {
|
|
683
|
+
const loaded = loadRunManifestById(cwd, runId);
|
|
684
|
+
if (!loaded) return false;
|
|
685
|
+
const roots = findRepoRoot(cwd) ? [projectCrewRoot(cwd), userCrewRoot()] : [userCrewRoot()];
|
|
686
|
+
return roots.some((root) => loaded.manifest.stateRoot === path.join(root, DEFAULT_PATHS.state.runsSubdir, runId));
|
|
687
|
+
}
|
|
688
|
+
|
|
689
|
+
/**
|
|
690
|
+
* Scan-path variant: a CHILD directory may claim the run only when the run's
|
|
691
|
+
* state actually lives under that child's OWN crew root (the nested-child
|
|
692
|
+
* case this scan exists for). Without this, a shared user-root run would be
|
|
693
|
+
* "claimed" by whichever child happened to be scanned first.
|
|
694
|
+
*/
|
|
695
|
+
function runUnderOwnRoot(cwd: string, runId: string): boolean {
|
|
696
|
+
const loaded = loadRunManifestById(cwd, runId);
|
|
697
|
+
if (!loaded) return false;
|
|
698
|
+
return loaded.manifest.stateRoot === path.join(projectCrewRoot(cwd), DEFAULT_PATHS.state.runsSubdir, runId);
|
|
699
|
+
}
|
|
700
|
+
|
|
655
701
|
export function locateRunCwdUncached(runId: string, baseCwd: string): string | undefined {
|
|
656
|
-
// Fast path: run
|
|
657
|
-
if (
|
|
702
|
+
// Fast path: run resolves under one of the roots `list` shows (see helper note).
|
|
703
|
+
if (runUnderListedRoot(baseCwd, runId)) {
|
|
658
704
|
return baseCwd;
|
|
659
705
|
}
|
|
660
706
|
|
|
@@ -670,7 +716,7 @@ export function locateRunCwdUncached(runId: string, baseCwd: string): string | u
|
|
|
670
716
|
if (!entry.name.startsWith(".crew") && !entry.name.startsWith(".pi") && !entry.name.startsWith(".tmp-crew")) continue;
|
|
671
717
|
}
|
|
672
718
|
const candidate = path.join(baseCwd, entry.name);
|
|
673
|
-
if (
|
|
719
|
+
if (runUnderOwnRoot(candidate, runId)) {
|
|
674
720
|
return candidate;
|
|
675
721
|
}
|
|
676
722
|
}
|