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.
- package/CHANGELOG.md +61 -0
- package/README.md +2 -3
- package/agents/executor.md +1 -1
- package/agents/test-engineer.md +1 -1
- package/agents/verifier.md +1 -1
- package/dist/index.mjs +4441 -4091
- package/package.json +7 -4
- package/scripts/analyze-run.mjs +1333 -0
- package/scripts/resource-sampler.mjs +482 -0
- package/skills/real-test-pi-crew/SKILL.md +10 -9
- package/src/config/role-tools.ts +2 -2
- package/src/extension/knowledge-injection.ts +17 -0
- package/src/extension/pi-api.ts +0 -16
- package/src/extension/team-tool/api/agent-control.ts +358 -0
- package/src/extension/team-tool/api/handler-context.ts +57 -0
- package/src/extension/team-tool/api/heartbeat.ts +75 -0
- package/src/extension/team-tool/api/mailbox.ts +242 -0
- package/src/extension/team-tool/api/plan-approval.ts +190 -0
- package/src/extension/team-tool/api/read.ts +443 -0
- package/src/extension/team-tool/api/task-claims.ts +207 -0
- package/src/extension/team-tool/api.ts +56 -1301
- package/src/extension/team-tool/cancel.ts +15 -4
- package/src/extension/team-tool/goal-wrap.ts +2 -2
- package/src/extension/team-tool/goal.ts +4 -4
- package/src/extension/team-tool/lifecycle-actions.ts +5 -5
- package/src/extension/team-tool/parallel-dispatch.ts +2 -2
- package/src/extension/team-tool/respond.ts +11 -3
- package/src/extension/team-tool/run-intent.ts +357 -0
- package/src/extension/team-tool/run.ts +8 -285
- package/src/extension/team-tool/status.ts +56 -21
- package/src/extension/team-tool.ts +10 -8
- package/src/prompt/scratchpad-lifecycle.ts +80 -5
- package/src/runtime/child-pi/child-pi-spawn.ts +13 -7
- package/src/runtime/child-pi/child-pi.ts +22 -144
- package/src/runtime/child-pi/mock-fixtures.ts +171 -0
- package/src/runtime/crew-agent-records.ts +18 -1
- package/src/runtime/scratchpad/README.md +6 -0
- package/src/runtime/scratchpad/guest.ts +54 -5
- package/src/runtime/scratchpad/snapshot-hmac.ts +7 -1
- package/src/runtime/supervisor-contact.ts +0 -16
- package/src/runtime/task-runner/child-executor.ts +1 -0
- package/src/runtime/task-runner/state-helpers.ts +9 -1
- package/src/runtime/team-runner.ts +39 -2
- package/src/state/contracts.ts +3 -0
- package/src/state/event-log/event-log.ts +19 -27
- package/src/state/gitignore-manager.ts +61 -7
- package/src/utils/glob-match.ts +29 -0
- package/types/dwf.d.ts +1 -1
- 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
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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
|
+
}
|