pi-crew 0.9.66 → 0.9.67

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 (49) hide show
  1. package/CHANGELOG.md +61 -0
  2. package/README.md +2 -3
  3. package/agents/executor.md +1 -1
  4. package/agents/test-engineer.md +1 -1
  5. package/agents/verifier.md +1 -1
  6. package/dist/index.mjs +4441 -4091
  7. package/package.json +7 -4
  8. package/scripts/analyze-run.mjs +1333 -0
  9. package/scripts/resource-sampler.mjs +482 -0
  10. package/skills/real-test-pi-crew/SKILL.md +10 -9
  11. package/src/config/role-tools.ts +2 -2
  12. package/src/extension/knowledge-injection.ts +17 -0
  13. package/src/extension/pi-api.ts +0 -16
  14. package/src/extension/team-tool/api/agent-control.ts +358 -0
  15. package/src/extension/team-tool/api/handler-context.ts +57 -0
  16. package/src/extension/team-tool/api/heartbeat.ts +75 -0
  17. package/src/extension/team-tool/api/mailbox.ts +242 -0
  18. package/src/extension/team-tool/api/plan-approval.ts +190 -0
  19. package/src/extension/team-tool/api/read.ts +443 -0
  20. package/src/extension/team-tool/api/task-claims.ts +207 -0
  21. package/src/extension/team-tool/api.ts +56 -1301
  22. package/src/extension/team-tool/cancel.ts +15 -4
  23. package/src/extension/team-tool/goal-wrap.ts +2 -2
  24. package/src/extension/team-tool/goal.ts +4 -4
  25. package/src/extension/team-tool/lifecycle-actions.ts +5 -5
  26. package/src/extension/team-tool/parallel-dispatch.ts +2 -2
  27. package/src/extension/team-tool/respond.ts +11 -3
  28. package/src/extension/team-tool/run-intent.ts +357 -0
  29. package/src/extension/team-tool/run.ts +8 -285
  30. package/src/extension/team-tool/status.ts +56 -21
  31. package/src/extension/team-tool.ts +10 -8
  32. package/src/prompt/scratchpad-lifecycle.ts +80 -5
  33. package/src/runtime/child-pi/child-pi-spawn.ts +13 -7
  34. package/src/runtime/child-pi/child-pi.ts +22 -144
  35. package/src/runtime/child-pi/mock-fixtures.ts +171 -0
  36. package/src/runtime/crew-agent-records.ts +18 -1
  37. package/src/runtime/scratchpad/README.md +6 -0
  38. package/src/runtime/scratchpad/guest.ts +54 -5
  39. package/src/runtime/scratchpad/snapshot-hmac.ts +7 -1
  40. package/src/runtime/supervisor-contact.ts +0 -16
  41. package/src/runtime/task-runner/child-executor.ts +1 -0
  42. package/src/runtime/task-runner/state-helpers.ts +9 -1
  43. package/src/runtime/team-runner.ts +39 -2
  44. package/src/state/contracts.ts +3 -0
  45. package/src/state/event-log/event-log.ts +19 -27
  46. package/src/state/gitignore-manager.ts +61 -7
  47. package/src/utils/glob-match.ts +29 -0
  48. package/types/dwf.d.ts +1 -1
  49. package/src/types/new-api-types.ts +0 -35
@@ -10,7 +10,7 @@ import {
10
10
  } from "../../runtime/process/cancellation.ts";
11
11
  import type { TeamToolParamsValue } from "../../schema/team-tool-schema.ts";
12
12
  import { withRunLockSync } from "../../state/coordination/locks.ts";
13
- import { appendEvent } from "../../state/event-log/event-log.ts";
13
+ import { appendEvent, appendEventAsync } from "../../state/event-log/event-log.ts";
14
14
  import { loadRunManifestById, saveRunTasks, updateRunStatus } from "../../state/stores/state-store.ts";
15
15
  import { logInternalError } from "../../utils/internal-error.ts";
16
16
  import { locateRunCwd } from "../team-tool.ts";
@@ -198,12 +198,21 @@ export async function handleRetry(params: TeamToolParamsValue, ctx: TeamContext,
198
198
 
199
199
  const retriedTaskIds = [...retriedIds];
200
200
  for (const taskId of retriedTaskIds) {
201
- appendEvent(loaded.manifest.eventsPath, {
201
+ // H1 (2026-08-10): inside a sync run-lock callback — cannot await;
202
+ // fire-and-forget async. task.retried is informational; the queued
203
+ // status is the authoritative record in tasks.json.
204
+ void appendEventAsync(loaded.manifest.eventsPath, {
202
205
  type: "task.retried",
203
206
  runId: loaded.manifest.runId,
204
207
  taskId,
205
208
  message: `Task ${taskId} queued for retry.`,
206
- });
209
+ }).catch((error) =>
210
+ logInternalError(
211
+ "cancel.retry-event",
212
+ error instanceof Error ? error : new Error(String(error)),
213
+ `runId=${loaded.manifest.runId}`,
214
+ ),
215
+ );
207
216
  }
208
217
 
209
218
  if (deps) invalidateSnapshot(loaded.manifest.runId, runCwd, deps);
@@ -267,7 +276,9 @@ export async function handleCancel(params: TeamToolParamsValue, ctx: TeamContext
267
276
  if (asyncPid !== undefined && asyncPid > 0) {
268
277
  try {
269
278
  killProcessPid(asyncPid);
270
- appendEvent(loaded.manifest.eventsPath, {
279
+ // H1 (2026-08-10): informational event in async context — await the
280
+ // async lock path (non-blocking event loop, ~1ms vs ~14ms sync).
281
+ await appendEventAsync(loaded.manifest.eventsPath, {
271
282
  type: "async.kill_requested",
272
283
  runId: loaded.manifest.runId,
273
284
  message: "Sent SIGTERM to background runner process.",
@@ -24,7 +24,7 @@ import { GoalStore } from "../../runtime/goal-workflow/goal-state-store.ts";
24
24
  import { snapshotManifests } from "../../runtime/verification/verification-integrity.ts";
25
25
  import type { TeamToolParamsValue } from "../../schema/team-tool-schema.ts";
26
26
  import { atomicWriteJson } from "../../state/atomic-write.ts";
27
- import { appendEvent } from "../../state/event-log/event-log.ts";
27
+ import { appendEventAsync } from "../../state/event-log/event-log.ts";
28
28
  import { createRunPaths, saveRunManifestAsync } from "../../state/stores/state-store.ts";
29
29
  import type { GoalLoopState, TeamRunManifest } from "../../state/types.ts";
30
30
  import { logInternalError } from "../../utils/internal-error.ts";
@@ -269,7 +269,7 @@ export async function startGoalWrappedRun(
269
269
  runKind: "goal-loop",
270
270
  };
271
271
  await saveRunManifestAsync(goalLoopManifest);
272
- appendEvent(paths.eventsPath, {
272
+ await appendEventAsync(paths.eventsPath, {
273
273
  type: "goal.loop_start",
274
274
  runId: goalId,
275
275
  data: {
@@ -19,7 +19,7 @@ import { GoalStore } from "../../runtime/goal-workflow/goal-state-store.ts";
19
19
  import { snapshotManifests } from "../../runtime/verification/verification-integrity.ts";
20
20
  import { isWorkspaceBusy } from "../../runtime/workspace-lock.ts";
21
21
  import type { TeamToolParamsValue } from "../../schema/team-tool-schema.ts";
22
- import { appendEvent } from "../../state/event-log/event-log.ts";
22
+ import { appendEventAsync } from "../../state/event-log/event-log.ts";
23
23
  import { createRunPaths, saveRunManifestAsync } from "../../state/stores/state-store.ts";
24
24
  import type { GoalLoopState, GoalLoopStatus, TeamRunManifest } from "../../state/types.ts";
25
25
  import { logInternalError } from "../../utils/internal-error.ts";
@@ -193,7 +193,7 @@ async function handleStart(input: GoalSubActionInput): Promise<ReturnType<typeof
193
193
  runKind: "goal-loop",
194
194
  };
195
195
  await saveRunManifestAsync(goalLoopManifest);
196
- appendEvent(paths.eventsPath, {
196
+ await appendEventAsync(paths.eventsPath, {
197
197
  type: "goal.loop_start",
198
198
  runId: goalId,
199
199
  data: {
@@ -438,7 +438,7 @@ async function handleResume(input: GoalSubActionInput): Promise<ReturnType<typeo
438
438
  if (hint) {
439
439
  withHint = store.patch(goalId, { nextTurnFeedback: hint }, eventsPath) ?? updated;
440
440
  }
441
- appendEvent(eventsPath, {
441
+ await appendEventAsync(eventsPath, {
442
442
  type: "goal.resumed",
443
443
  runId: goalId,
444
444
  data: { goalId, fromState: existing.state, hint: hint?.slice(0, 200) },
@@ -487,7 +487,7 @@ async function handleResume(input: GoalSubActionInput): Promise<ReturnType<typeo
487
487
  // 'goal resume' (which requires paused/stuck). Leaving it at 'running' with no process made
488
488
  // the goal un-resumable — the user had to pause-then-resume as a workaround.
489
489
  store.compareAndSetStatus(goalId, "running", existing.state, eventsPath);
490
- appendEvent(eventsPath, {
490
+ await appendEventAsync(eventsPath, {
491
491
  type: "goal.resume_spawn_failed",
492
492
  runId: goalId,
493
493
  data: { goalId, error: msg, rolledBackTo: existing.state },
@@ -5,7 +5,7 @@ import { appendHookEvent, executeHook } from "../../hooks/registry.ts";
5
5
  import { killProcessPid } from "../../runtime/child-pi/child-pi.ts";
6
6
  import { terminateLiveAgentsForRun } from "../../runtime/live-session/live-agent-manager.ts";
7
7
  import type { TeamToolParamsValue } from "../../schema/team-tool-schema.ts";
8
- import { appendEvent } from "../../state/event-log/event-log.ts";
8
+ import { appendEvent, appendEventAsync } from "../../state/event-log/event-log.ts";
9
9
  import { loadRunManifestById } from "../../state/stores/state-store.ts";
10
10
  import { logInternalError } from "../../utils/internal-error.ts";
11
11
  import { projectCrewRoot, userCrewRoot, userPiRoot } from "../../utils/paths.ts";
@@ -128,7 +128,7 @@ export async function handleExport(params: TeamToolParamsValue, ctx: TeamContext
128
128
  }
129
129
 
130
130
  const exported = exportRunBundle(loaded.manifest, loaded.tasks);
131
- appendEvent(loaded.manifest.eventsPath, {
131
+ await appendEventAsync(loaded.manifest.eventsPath, {
132
132
  type: "run.exported",
133
133
  runId: loaded.manifest.runId,
134
134
  data: exported,
@@ -256,7 +256,7 @@ export async function handleForget(params: TeamToolParamsValue, ctx: TeamContext
256
256
  true,
257
257
  );
258
258
  const intent = intentFromConfig(params.config);
259
- appendEvent(loaded.manifest.eventsPath, {
259
+ await appendEventAsync(loaded.manifest.eventsPath, {
260
260
  type: "run.forget_requested",
261
261
  runId: loaded.manifest.runId,
262
262
  message: "Run state and artifacts are being forgotten.",
@@ -276,7 +276,7 @@ export async function handleForget(params: TeamToolParamsValue, ctx: TeamContext
276
276
  if (asyncPid !== undefined && asyncPid > 0) {
277
277
  try {
278
278
  killProcessPid(asyncPid);
279
- appendEvent(loaded.manifest.eventsPath, {
279
+ await appendEventAsync(loaded.manifest.eventsPath, {
280
280
  type: "async.kill_requested",
281
281
  runId: loaded.manifest.runId,
282
282
  message: "Sent SIGTERM to background runner process (forget).",
@@ -655,7 +655,7 @@ async function handleRunCleanup(params: TeamToolParamsValue, ctx: TeamContext):
655
655
  signal: ctx.signal,
656
656
  });
657
657
  const intent = intentFromConfig(params.config);
658
- appendEvent(loaded.manifest.eventsPath, {
658
+ await appendEventAsync(loaded.manifest.eventsPath, {
659
659
  type: "worktree.cleanup",
660
660
  runId: loaded.manifest.runId,
661
661
  data: {
@@ -11,7 +11,7 @@ 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
13
  import type { TeamToolParamsValue } from "../../schema/team-tool-schema.ts";
14
- import { appendEvent } from "../../state/event-log/event-log.ts";
14
+ import { appendEventAsync } from "../../state/event-log/event-log.ts";
15
15
  import { createRunManifest } from "../../state/stores/state-store.ts";
16
16
  import { discoverTeams } from "../../teams/discover-teams.ts";
17
17
  import type { TeamConfig } from "../../teams/team-config.ts";
@@ -165,7 +165,7 @@ async function spawnSingleTask(
165
165
  runKind: "team-run",
166
166
  });
167
167
 
168
- appendEvent(created.manifest.eventsPath, {
168
+ await appendEventAsync(created.manifest.eventsPath, {
169
169
  type: "run.started",
170
170
  runId: created.manifest.runId,
171
171
  message: `Parallel task: ${goal}`,
@@ -2,7 +2,7 @@ import { readCrewAgents, recordFromTask, saveCrewAgents } from "../../runtime/cr
2
2
  import type { TeamToolParamsValue } from "../../schema/team-tool-schema.ts";
3
3
  import { withRunLockSync } from "../../state/coordination/locks.ts";
4
4
  import { appendMailboxMessage, updateMailboxMessageReply } from "../../state/coordination/mailbox.ts";
5
- import { appendEvent } from "../../state/event-log/event-log.ts";
5
+ import { appendEventAsync } from "../../state/event-log/event-log.ts";
6
6
  import { loadRunManifestById, saveRunTasks, updateRunStatus } from "../../state/stores/state-store.ts";
7
7
  import { logInternalError } from "../../utils/internal-error.ts";
8
8
  import { locateRunCwd } from "../team-tool.ts";
@@ -133,13 +133,21 @@ export function handleRespond(params: TeamToolParamsValue, ctx: TeamContext): Pi
133
133
  manifest = updateRunStatus(manifest, "running", `Resumed ${resumed.size} waiting task(s).`);
134
134
  }
135
135
  for (const taskId of resumed) {
136
- appendEvent(manifest.eventsPath, {
136
+ // H1 (2026-08-10): handleRespond is a SYNC function inside a sync
137
+ // run-lock callback — cannot await; fire-and-forget async.
138
+ void appendEventAsync(manifest.eventsPath, {
137
139
  type: "task.resumed",
138
140
  runId: manifest.runId,
139
141
  taskId,
140
142
  message: message || "Task re-queued after respond.",
141
143
  data: { mailboxIds },
142
- });
144
+ }).catch((error) =>
145
+ logInternalError(
146
+ "respond.resumed-event",
147
+ error instanceof Error ? error : new Error(String(error)),
148
+ `runId=${manifest.runId}`,
149
+ ),
150
+ );
143
151
  }
144
152
  try {
145
153
  const existingRuntimes = new Map(readCrewAgents(fresh.manifest).map((a) => [a.taskId, a.runtime]));
@@ -0,0 +1,357 @@
1
+ /**
2
+ * Run-intent validation phase (H3 phase 4).
3
+ *
4
+ * Extracted from `handleRun` (src/extension/team-tool/run.ts) on 2026-08-10.
5
+ * Validates the tool params → resolved team/workflow/agent + goal + analysis,
6
+ * returning either a validated `RunIntent` (for materialization) or an error
7
+ * result. Behaviour is byte-identical to the inline block it replaces —
8
+ * including the exact error-precedence order the run tests assert on.
9
+ *
10
+ * The chain dispatch stays in `handleRun` because it recurses via a lazy
11
+ * injected handleRun reference (run.ts ↔ chain-dispatch.ts import cycle).
12
+ */
13
+
14
+ import * as fs from "node:fs";
15
+ import { allAgents, discoverAgents } from "../../agents/discover-agents.ts";
16
+ import { loadConfig } from "../../config/config.ts";
17
+ import { sanitizeTaskText } from "../../runtime/task-packet.ts";
18
+ import type { TeamToolParamsValue } from "../../schema/team-tool-schema.ts";
19
+ import { allTeams, discoverTeams } from "../../teams/discover-teams.ts";
20
+ import type { TeamConfig } from "../../teams/team-config.ts";
21
+ import { errorMessage } from "../../utils/guards.ts";
22
+ import { logInternalError } from "../../utils/internal-error.ts";
23
+ import { resolveRealContainedPath } from "../../utils/safe-paths.ts";
24
+ import { allWorkflows, discoverWorkflows } from "../../workflows/discover-workflows.ts";
25
+ import type { WorkflowConfig } from "../../workflows/workflow-config.ts";
26
+ import { assertCleanLeaderAsync, findGitRootAsync } from "../../worktree/worktree-manager.ts";
27
+ import type { PiTeamsToolResult } from "../tool-result.ts";
28
+ import type { TeamContext } from "./context.ts";
29
+ import { result } from "./context.ts";
30
+ import { isGoalWrapEnabled, shouldGoalWrap, startGoalWrappedRun } from "./goal-wrap.ts";
31
+ import { paramRequired } from "./param-error.ts";
32
+
33
+ /** Cap for inline/path analysis content (mirrors the schema maxLength). */
34
+ const MAX_ANALYSIS_BYTES = 100_000;
35
+
36
+ /**
37
+ * Module-scoped latch for the crew-init dynamic import (moved verbatim from
38
+ * run.ts — see the comment there for the jiti TDZ race it guards).
39
+ */
40
+ var crewInitPromise: Promise<typeof import("../../state/crew-init.ts")> | undefined;
41
+ function loadCrewInit(): Promise<typeof import("../../state/crew-init.ts")> {
42
+ if (!crewInitPromise) {
43
+ crewInitPromise = import("../../state/crew-init.ts");
44
+ }
45
+ return crewInitPromise;
46
+ }
47
+
48
+ /**
49
+ * Resolve analysis text from inline or file. Mutual exclusivity mirrors the
50
+ * `budgetTotal`/`budgetUnlimited` pattern (cold-review #2 blocking fix).
51
+ */
52
+ function resolveAnalysisText(
53
+ params: TeamToolParamsValue,
54
+ cwd: string,
55
+ ): { text?: string; error?: string; source: "inline" | "path" | "none" } {
56
+ const hasInline = typeof params.analysis === "string" && params.analysis.length > 0;
57
+ const hasPath = typeof params.analysisPath === "string" && params.analysisPath.length > 0;
58
+
59
+ if (hasInline && hasPath) {
60
+ return {
61
+ error: "`analysis` and `analysisPath` are mutually exclusive. Set exactly one.",
62
+ source: "none",
63
+ };
64
+ }
65
+ if (!hasInline && !hasPath) return { source: "none" };
66
+
67
+ if (hasPath) {
68
+ let resolved: string;
69
+ try {
70
+ resolved = resolveRealContainedPath(cwd, params.analysisPath as string);
71
+ } catch {
72
+ return {
73
+ error: `analysisPath must be within project directory: ${params.analysisPath}`,
74
+ source: "none",
75
+ };
76
+ }
77
+ if (!fs.existsSync(resolved)) {
78
+ return {
79
+ error: `Analysis file not found: ${resolved}`,
80
+ source: "none",
81
+ };
82
+ }
83
+ // Size cap BEFORE reading: mirror the inline schema cap (maxLength 100_000)
84
+ // so a large file can't blow up worker prompts via the sharedReads channel.
85
+ const { size } = fs.statSync(resolved);
86
+ if (size > MAX_ANALYSIS_BYTES) {
87
+ return {
88
+ error: `Analysis file too large: ${size} bytes (max ${MAX_ANALYSIS_BYTES}). Trim the analysis or pass a summary inline.`,
89
+ source: "none",
90
+ };
91
+ }
92
+ const raw = fs.readFileSync(resolved, "utf-8");
93
+ const sanitized = sanitizeTaskText(raw);
94
+ if (!sanitized) return { source: "none" };
95
+ return { text: sanitized, source: "path" };
96
+ }
97
+
98
+ // hasInline
99
+ const sanitized = sanitizeTaskText(params.analysis as string);
100
+ if (!sanitized) return { source: "none" };
101
+ return { text: sanitized, source: "inline" };
102
+ }
103
+
104
+ /** The validated run intent produced by {@link validateRunIntent}. */
105
+ export interface RunIntent {
106
+ goal: string;
107
+ intentPrefix: string;
108
+ /** cwd after the git-root auto-correction for worktree mode. */
109
+ resolvedCtx: TeamContext;
110
+ directAgent: boolean;
111
+ team: TeamConfig;
112
+ workflow: WorkflowConfig;
113
+ /** All discovered agents (needed by the execution phase for executeTeamRun). */
114
+ agents: ReturnType<typeof allAgents>;
115
+ analysisParam: { text?: string; error?: string; source: "inline" | "path" | "none" };
116
+ isDynamicWorkflow: boolean;
117
+ /** params.runKind only when the workflow is dynamic (else undefined). */
118
+ effectiveRunKind: TeamToolParamsValue["runKind"];
119
+ /** normalizeSkillOverride result — matches TeamRunManifest.skillOverride. */
120
+ skillOverride: false | string[] | undefined;
121
+ }
122
+
123
+ /**
124
+ * Validate the run params into a {@link RunIntent}.
125
+ *
126
+ * Error-precedence order (asserted by run tests, do not reorder):
127
+ * goal/params → crew-init → worktree precondition → agent → team →
128
+ * workflow → analysis → workflow-validation. Non-validation branches
129
+ * (chain dispatch, goal-wrap delegation) are handled inside.
130
+ */
131
+ export async function validateRunIntent(
132
+ params: TeamToolParamsValue,
133
+ ctx: TeamContext,
134
+ ): Promise<{ kind: "ok"; intent: RunIntent } | { kind: "error"; result: PiTeamsToolResult }> {
135
+ const goal = params.goal ?? params.task;
136
+ if (!goal)
137
+ return {
138
+ kind: "error",
139
+ result: result(
140
+ paramRequired("run", "goal or task", "{ action: 'run', goal: '<what to achieve>' }"),
141
+ { action: "run", status: "error" },
142
+ true,
143
+ ),
144
+ };
145
+ const intentPrefix = goal.length > 60 ? `${goal.slice(0, 57)}...` : goal;
146
+
147
+ // P0: Ensure .crew directory structure exists before creating any manifests.
148
+ // Latch shared across concurrent `team` tool calls (see loadCrewInit).
149
+ const workingDir = ctx.cwd ?? process.cwd();
150
+ const { ensureCrewDirectory } = await loadCrewInit();
151
+ await ensureCrewDirectory(workingDir);
152
+
153
+ // WORKTREE FIX: If worktree mode is needed but cwd is not a git repo,
154
+ // auto-correct to the nearest git repo root.
155
+ let resolvedCtx = ctx;
156
+ if (workingDir) {
157
+ try {
158
+ const gitRoot = await findGitRootAsync(workingDir);
159
+ if (gitRoot && gitRoot !== workingDir) {
160
+ resolvedCtx = { ...ctx, cwd: gitRoot };
161
+ }
162
+ } catch {
163
+ // cwd is not in a git repo — validate below if worktree mode is needed
164
+ }
165
+ }
166
+
167
+ // WORKTREE PRECONDITION CHECK: validate git repo exists and is clean
168
+ // BEFORE creating the run manifest.
169
+ if (params.workspaceMode === "worktree") {
170
+ let gitRoot: string | undefined;
171
+ try {
172
+ gitRoot = await findGitRootAsync(resolvedCtx.cwd);
173
+ } catch {
174
+ // not a git repo
175
+ }
176
+ if (!gitRoot) {
177
+ return {
178
+ kind: "error",
179
+ result: result(
180
+ `Worktree mode requires a git repository. '${resolvedCtx.cwd}' is not inside a git repo.\nUse workspaceMode: 'single' or run from a git repository.`,
181
+ { action: "run", status: "error" },
182
+ true,
183
+ ),
184
+ };
185
+ }
186
+ // Check if clean leader is required (can be disabled via config)
187
+ const preCheckConfig = loadConfig(resolvedCtx.cwd);
188
+ if (preCheckConfig.config.requireCleanWorktreeLeader !== false) {
189
+ try {
190
+ await assertCleanLeaderAsync(gitRoot);
191
+ } catch (err) {
192
+ const msg = errorMessage(err);
193
+ return {
194
+ kind: "error",
195
+ result: result(
196
+ `${msg}\nCommit or stash changes before using worktree mode, or use workspaceMode: 'single'.`,
197
+ { action: "run", status: "error" },
198
+ true,
199
+ ),
200
+ };
201
+ }
202
+ }
203
+ }
204
+
205
+ const teams = allTeams(discoverTeams(resolvedCtx.cwd));
206
+ const workflows = allWorkflows(discoverWorkflows(resolvedCtx.cwd));
207
+ const agents = allAgents(discoverAgents(resolvedCtx.cwd));
208
+ const directAgent = params.agent ? agents.find((item) => item.name === params.agent) : undefined;
209
+ if (params.agent && !directAgent)
210
+ return { kind: "error", result: result(`Agent '${params.agent}' not found.`, { action: "run", status: "error" }, true) };
211
+ const teamName = params.team ?? "default";
212
+ const team = directAgent
213
+ ? {
214
+ name: `direct-${directAgent.name}`,
215
+ description: `Direct subagent run for ${directAgent.name}`,
216
+ source: "builtin" as const,
217
+ filePath: "<generated>",
218
+ roles: [
219
+ {
220
+ name: params.role ?? "agent",
221
+ agent: directAgent.name,
222
+ description: directAgent.description,
223
+ },
224
+ ],
225
+ defaultWorkflow: "direct-agent",
226
+ workspaceMode: params.workspaceMode,
227
+ }
228
+ : teams.find((item) => item.name === teamName);
229
+ if (!team) return { kind: "error", result: result(`Team '${teamName}' not found.`, { action: "run", status: "error" }, true) };
230
+ // BUG-44 (github #44): `chain` is a dispatcher-only workflow — see the
231
+ // comment in run.ts; chain steps forwarding params.workflow fall back to
232
+ // the team's default workflow.
233
+ const workflowName = directAgent
234
+ ? "direct-agent"
235
+ : params.workflow === "chain" && !params.chain
236
+ ? (team.defaultWorkflow ?? "default")
237
+ : (params.workflow ?? team.defaultWorkflow ?? "default");
238
+ const baseWorkflow = directAgent
239
+ ? {
240
+ name: "direct-agent",
241
+ description: `Direct task for ${directAgent.name}`,
242
+ source: "builtin" as const,
243
+ filePath: "<generated>",
244
+ steps: [
245
+ {
246
+ id: "01_agent",
247
+ role: params.role ?? "agent",
248
+ task: "{goal}",
249
+ model: params.model,
250
+ reads: params.analysis || params.analysisPath ? ["analysis.md"] : undefined,
251
+ },
252
+ ],
253
+ }
254
+ : workflows.find((item) => item.name === workflowName);
255
+ if (!baseWorkflow)
256
+ return { kind: "error", result: result(`Workflow '${workflowName}' not found.`, { action: "run", status: "error" }, true) };
257
+
258
+ // ANALYSIS CHANNEL (round-X Y1): resolve analysis text BEFORE
259
+ // createRunManifest so validation errors fail-fast (no orphan run state).
260
+ const analysisParam = resolveAnalysisText(params, resolvedCtx.cwd);
261
+ if (analysisParam.error) return { kind: "error", result: result(analysisParam.error, { action: "run", status: "error" }, true) };
262
+
263
+ // LAZY: dodge the jiti ESM/CJS interop TDZ race on the static `import { expandParallelResearchWorkflow }` (issue #28, RFC 17). Multi-line form breaks scripts/check-lazy-imports.mjs.
264
+ const { expandParallelResearchWorkflow: expandParallelResearch } = await import("../../runtime/scheduling/parallel-research.ts");
265
+ const workflow = directAgent ? baseWorkflow : expandParallelResearch(baseWorkflow, resolvedCtx.cwd);
266
+ const isDynamicWorkflow =
267
+ !directAgent && (workflow as import("../../workflows/workflow-config.ts").WorkflowConfig).runtime === "dynamic";
268
+ if (params.runKind !== undefined && !isDynamicWorkflow) {
269
+ logInternalError(
270
+ "team-tool.run.runKindIgnored",
271
+ new Error(`Ignoring runKind='${params.runKind}' because workflow '${workflow.name}' is not dynamic.`),
272
+ undefined,
273
+ "warn",
274
+ );
275
+ }
276
+
277
+ // PREFLIGHT (advisory only, since v0.9.15) — informational notes per the
278
+ // rule in .crew/knowledge.md "pi-crew USAGE THRESHOLD RULE". Never blocks.
279
+ if (!directAgent) {
280
+ // LAZY: defer preflight-validator import until a team run requests it.
281
+ const { validateWorkflowUsage } = await import("../../workflows/preflight-validator.ts");
282
+ const preflight = validateWorkflowUsage(workflow, {
283
+ force: params.force === true,
284
+ });
285
+ const icon = preflight.level === "warn" ? "⚠️ " : preflight.level === "note" ? "ℹ️ " : "";
286
+ const tag = preflight.level.toUpperCase();
287
+ console.warn(`${icon}[team-tool.preflight] ${tag}: ${preflight.message} (workflow=${workflow.name})`);
288
+ if (preflight.suggestion) {
289
+ console.warn(`[team-tool.preflight] → ${preflight.suggestion}`);
290
+ }
291
+ }
292
+
293
+ // RFC v0.5 vision: goal-wrap. If .crew/config.json has
294
+ // goalWrap[workflow.name].enabled=true, route to a goal loop where this
295
+ // workflow runs as the worker turn. Only for eligible builtins. When
296
+ // goal-wrap is unsafe for this workflow we fall through (never block the
297
+ // run the user asked for).
298
+ if (!directAgent && workflow.source === "builtin" && isGoalWrapEnabled(resolvedCtx.cwd, workflow.name)) {
299
+ const decision = shouldGoalWrap(resolvedCtx.cwd, workflow);
300
+ if (decision.enabled) {
301
+ if (analysisParam.text) {
302
+ console.warn(
303
+ `[team-tool.run] analysis param is ignored by goal-wrapped run (workflow=${workflow.name}). The analysis artifact will not be written.`,
304
+ );
305
+ }
306
+ return {
307
+ kind: "error",
308
+ result: await startGoalWrappedRun(params, ctx, workflow, goal),
309
+ };
310
+ }
311
+ if (decision.message) {
312
+ logInternalError(
313
+ "team-tool.run.goalWrapBypassed",
314
+ new Error(decision.message),
315
+ `workflow=${workflow.name} reason=${decision.reason}`,
316
+ );
317
+ }
318
+ }
319
+
320
+ // LAZY: dodge the jiti ESM/CJS interop TDZ race (issue #28, RFC 17).
321
+ const { validateWorkflowForTeam: validateWorkflow } = await import("../../workflows/validate-workflow.ts");
322
+ const validationErrors = validateWorkflow(workflow, team);
323
+ if (validationErrors.length > 0) {
324
+ return {
325
+ kind: "error",
326
+ result: result(
327
+ [
328
+ `Workflow '${workflow.name}' is not valid for team '${team.name}':`,
329
+ ...validationErrors.map((error) => `- ${error}`),
330
+ ].join("\n"),
331
+ { action: "run", status: "error" },
332
+ true,
333
+ ),
334
+ };
335
+ }
336
+
337
+ // LAZY: dodge the jiti ESM/CJS interop TDZ race (issue #28, RFC 17).
338
+ const { normalizeSkillOverride: normalizeSkill } = await import("../../runtime/skill-instructions.ts");
339
+ const skillOverride = normalizeSkill(params.skill);
340
+
341
+ return {
342
+ kind: "ok",
343
+ intent: {
344
+ goal,
345
+ intentPrefix,
346
+ resolvedCtx,
347
+ directAgent: !!directAgent,
348
+ team,
349
+ workflow,
350
+ agents,
351
+ analysisParam,
352
+ isDynamicWorkflow,
353
+ effectiveRunKind: isDynamicWorkflow ? params.runKind : undefined,
354
+ skillOverride,
355
+ },
356
+ };
357
+ }