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.
Files changed (154) hide show
  1. package/CHANGELOG.md +82 -9
  2. package/README.md +161 -1036
  3. package/agents/verifier.md +18 -7
  4. package/dist/index.mjs +742 -91462
  5. package/docs/README.md +57 -46
  6. package/docs/architecture.md +87 -33
  7. package/docs/commands-reference.md +9 -5
  8. package/docs/troubleshooting.md +3 -2
  9. package/package.json +5 -4
  10. package/schema.json +29 -0
  11. package/skills/real-test-pi-crew/SKILL.md +206 -41
  12. package/src/agents/agent-config.ts +1 -1
  13. package/src/agents/discover-agents.ts +1 -1
  14. package/src/config/config-validation.ts +15 -0
  15. package/src/config/config.ts +47 -13
  16. package/src/config/env-vars.ts +35 -0
  17. package/src/config/types.ts +19 -0
  18. package/src/errors.ts +2 -2
  19. package/src/extension/async-notifier.ts +55 -8
  20. package/src/extension/crew-input-router.ts +9 -2
  21. package/src/extension/help.ts +21 -10
  22. package/src/extension/knowledge-injection.ts +2 -1
  23. package/src/extension/management.ts +8 -3
  24. package/src/extension/notification-sink.ts +17 -0
  25. package/src/extension/plan-orchestrate.ts +19 -3
  26. package/src/extension/registration/command-utils.ts +28 -2
  27. package/src/extension/registration/commands/dashboard.ts +11 -1
  28. package/src/extension/registration/commands/manage.ts +31 -15
  29. package/src/extension/registration/commands/run.ts +24 -2
  30. package/src/extension/registration/commands/shared.ts +34 -10
  31. package/src/extension/registration/commands/status.ts +25 -2
  32. package/src/extension/registration/context-builder.ts +8 -2
  33. package/src/extension/registration/foreground-run-controller.ts +2 -2
  34. package/src/extension/registration/health-notify-policy.ts +100 -0
  35. package/src/extension/registration/lazy-configurers.ts +38 -0
  36. package/src/extension/registration/lifecycle-handlers.ts +107 -40
  37. package/src/extension/registration/lifecycle.ts +77 -10
  38. package/src/extension/registration/observability.ts +125 -53
  39. package/src/extension/registration/registration-types.ts +7 -5
  40. package/src/extension/registration/runtime-cleanup.ts +9 -3
  41. package/src/extension/registration/subagent-helpers.ts +38 -0
  42. package/src/extension/registration/team-tool.ts +9 -2
  43. package/src/extension/registration/wire-cross-extension.ts +30 -0
  44. package/src/extension/run-compare.ts +220 -0
  45. package/src/extension/run-export.ts +37 -5
  46. package/src/extension/run-maintenance.ts +155 -5
  47. package/src/extension/team-manager-command.ts +3 -2
  48. package/src/extension/team-tool/dispatch/index.ts +3 -2
  49. package/src/extension/team-tool/dispatch/manage.ts +5 -2
  50. package/src/extension/team-tool/goal-wrap.ts +7 -0
  51. package/src/extension/team-tool/goal.ts +16 -1
  52. package/src/extension/team-tool/handle-settings.ts +19 -2
  53. package/src/extension/team-tool/health-monitor.ts +21 -7
  54. package/src/extension/team-tool/lifecycle-actions.ts +49 -1
  55. package/src/extension/team-tool/parallel-dispatch.ts +16 -1
  56. package/src/extension/team-tool/plan.ts +10 -0
  57. package/src/extension/team-tool/routing-hint.ts +63 -0
  58. package/src/extension/team-tool/run-intent.ts +1 -1
  59. package/src/extension/team-tool/run.ts +1 -1
  60. package/src/extension/team-tool/status.ts +4 -0
  61. package/src/extension/team-tool.ts +52 -6
  62. package/src/extension/webhook-notify.ts +382 -0
  63. package/src/observability/metric-sink.ts +12 -2
  64. package/src/prompt/prompt-runtime.ts +82 -31
  65. package/src/prompt/worker-events-channel.ts +12 -0
  66. package/src/runtime/README.md +1 -1
  67. package/src/runtime/async-runner.ts +123 -1
  68. package/src/runtime/background-runner.ts +313 -234
  69. package/src/runtime/broker/crew-broker.ts +76 -71
  70. package/src/runtime/broker/delegate/delegate-event.ts +12 -8
  71. package/src/runtime/broker/delegate/shadow-lifecycle.ts +92 -0
  72. package/src/runtime/broker/protocol/connection-state.ts +10 -0
  73. package/src/runtime/broker/protocol/lock-busy.ts +76 -0
  74. package/src/runtime/broker/wait-status-cache.ts +1 -1
  75. package/src/runtime/child-pi/child-pi-timers.ts +1 -1
  76. package/src/runtime/child-pi/mock-fixtures.ts +48 -0
  77. package/src/runtime/crew-agent-records.ts +337 -45
  78. package/src/runtime/deadletter.ts +43 -1
  79. package/src/runtime/delegate-spawn.ts +21 -3
  80. package/src/runtime/deterministic-ast.ts +14 -2
  81. package/src/runtime/dispatch-batch.ts +72 -5
  82. package/src/runtime/goal-workflow/dynamic-workflow-context.ts +78 -58
  83. package/src/runtime/goal-workflow/dynamic-workflow-runner.ts +14 -2
  84. package/src/runtime/goal-workflow/goal-loop-runner.ts +105 -4
  85. package/src/runtime/group-join.ts +11 -3
  86. package/src/runtime/heartbeat/heartbeat-watcher.ts +7 -0
  87. package/src/runtime/live-session/live-session-runtime.ts +1 -1
  88. package/src/runtime/model/model-fallback.ts +21 -1
  89. package/src/runtime/model/pi-args.ts +8 -10
  90. package/src/runtime/model/runtime-warmup.ts +2 -0
  91. package/src/runtime/output/output-validator.ts +10 -5
  92. package/src/runtime/plan-approval.ts +14 -1
  93. package/src/runtime/recovery/crash-recovery.ts +96 -49
  94. package/src/runtime/run-worker.ts +12 -1
  95. package/src/runtime/scheduling/global-worker-cap.ts +13 -6
  96. package/src/runtime/scheduling/run-coalesced-task-group.ts +27 -1
  97. package/src/runtime/scheduling/scheduler.ts +49 -13
  98. package/src/runtime/scheduling/semaphore.ts +148 -20
  99. package/src/runtime/scratchpad/README.md +1 -1
  100. package/src/runtime/scratchpad/protocol.ts +1 -1
  101. package/src/runtime/settings-store.ts +1 -1
  102. package/src/runtime/skill-instructions.ts +22 -0
  103. package/src/runtime/stale-reconciler.ts +85 -13
  104. package/src/runtime/subagent-manager.ts +43 -13
  105. package/src/runtime/task-runner/child-executor.ts +17 -6
  106. package/src/runtime/task-runner/post-execution.ts +44 -14
  107. package/src/runtime/task-runner/pre-execution.ts +26 -2
  108. package/src/runtime/task-runner/prompt-builder.ts +150 -45
  109. package/src/runtime/task-runner/retrieval-orchestrator.ts +36 -14
  110. package/src/runtime/task-runner.ts +28 -1
  111. package/src/runtime/team-runner.ts +42 -2
  112. package/src/runtime/verification/verification-worktree.ts +3 -0
  113. package/src/runtime/workspace-lock.ts +4 -1
  114. package/src/schema/config-schema.ts +14 -0
  115. package/src/schema/team-tool-schema.ts +17 -0
  116. package/src/state/atomic-write.ts +53 -0
  117. package/src/state/contracts.ts +109 -0
  118. package/src/state/coordination/locks.ts +191 -33
  119. package/src/state/coordination/mailbox.ts +147 -17
  120. package/src/state/crew-init.ts +87 -12
  121. package/src/state/event-log/cursor.ts +37 -1
  122. package/src/state/event-log/event-log-rotation.ts +72 -7
  123. package/src/state/stores/active-run-registry.ts +13 -1
  124. package/src/state/stores/state-store.ts +120 -23
  125. package/src/state/types.ts +4 -0
  126. package/src/ui/dashboard-panes/agents-pane.ts +22 -10
  127. package/src/ui/heartbeat-aggregator.ts +34 -0
  128. package/src/ui/inline-panel/index.ts +1 -1
  129. package/src/ui/keybinding-map.ts +22 -4
  130. package/src/ui/live-conversation-overlay.ts +6 -3
  131. package/src/ui/render-diff.ts +19 -6
  132. package/src/ui/run-dashboard.ts +109 -6
  133. package/src/ui/run-snapshot-cache.ts +27 -4
  134. package/src/ui/spinner.ts +26 -2
  135. package/src/ui/tool-progress-formatter.ts +2 -1
  136. package/src/ui/tool-renderers/brief-mode.ts +2 -1
  137. package/src/ui/tool-renderers/index.ts +3 -3
  138. package/src/ui/widget/task-list.ts +8 -5
  139. package/src/ui/widget/widget-model.ts +3 -1
  140. package/src/utils/incremental-reader.ts +11 -3
  141. package/src/utils/paths.ts +94 -12
  142. package/src/utils/project-markers.ts +40 -0
  143. package/src/worktree/worktree-manager.ts +206 -26
  144. package/tsconfig.json +2 -0
  145. package/workflows/distill.workflow.md +3 -3
  146. package/workflows/fast-fix.workflow.md +1 -1
  147. package/workflows/plan-execute.workflow.md +1 -1
  148. package/workflows/review.workflow.md +1 -1
  149. package/workflows/strict-fast-fix.workflow.md +1 -1
  150. package/docs/migration-v0.4-v0.5.md +0 -208
  151. package/docs/runtime-flow.md +0 -148
  152. package/src/ui/card-colors.ts +0 -168
  153. package/src/utils/completion-dedupe.ts +0 -63
  154. 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 is in the current CWD
657
- if (loadRunManifestById(baseCwd, runId)) {
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 (loadRunManifestById(candidate, runId)) {
719
+ if (runUnderOwnRoot(candidate, runId)) {
674
720
  return candidate;
675
721
  }
676
722
  }