pi-crew 0.5.1 → 0.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +28 -0
- package/README.md +1 -1
- package/docs/actions-reference.md +87 -0
- package/docs/commands-reference.md +5 -0
- package/docs/pi-crew-bugs.md +6 -0
- package/index.ts +1 -1
- package/package.json +18 -16
- package/src/benchmark/benchmark-runner.ts +245 -0
- package/src/benchmark/feedback-loop.ts +66 -0
- package/src/extension/async-notifier.ts +1 -1
- package/src/extension/autonomous-policy.ts +1 -1
- package/src/extension/cross-extension-rpc.ts +1 -1
- package/src/extension/plan-orchestrate.ts +322 -0
- package/src/extension/register.ts +31 -41
- package/src/extension/registration/command-utils.ts +1 -1
- package/src/extension/registration/commands.ts +1 -1
- package/src/extension/registration/compaction-guard.ts +1 -1
- package/src/extension/registration/subagent-helpers.ts +1 -1
- package/src/extension/registration/subagent-tools.ts +1 -1
- package/src/extension/registration/team-tool.ts +1 -1
- package/src/extension/registration/viewers.ts +1 -1
- package/src/extension/session-summary.ts +1 -1
- package/src/extension/team-manager-command.ts +1 -1
- package/src/extension/team-tool/context.ts +1 -1
- package/src/extension/team-tool/handle-schedule.ts +183 -0
- package/src/extension/team-tool/orchestrate.ts +102 -0
- package/src/extension/team-tool/run.ts +215 -28
- package/src/extension/team-tool.ts +10 -0
- package/src/extension/tool-result.ts +1 -1
- package/src/i18n.ts +1 -1
- package/src/observability/event-to-metric.ts +1 -1
- package/src/prompt/prompt-runtime.ts +1 -1
- package/src/runtime/background-runner.ts +27 -5
- package/src/runtime/crash-recovery.ts +1 -1
- package/src/runtime/crew-hooks.ts +240 -0
- package/src/runtime/custom-tools/irc-tool.ts +1 -1
- package/src/runtime/custom-tools/submit-result-tool.ts +1 -1
- package/src/runtime/diagnostic-export.ts +38 -2
- package/src/runtime/foreground-watchdog.ts +1 -1
- package/src/runtime/live-session-runtime.ts +1 -1
- package/src/runtime/mcp-proxy.ts +1 -1
- package/src/runtime/pi-spawn.ts +20 -4
- package/src/runtime/process-status.ts +15 -2
- package/src/runtime/runtime-resolver.ts +1 -1
- package/src/runtime/session-resources.ts +1 -1
- package/src/runtime/task-runner.ts +31 -1
- package/src/runtime/team-runner.ts +6 -0
- package/src/schema/team-tool-schema.ts +24 -1
- package/src/state/crew-init.ts +56 -38
- package/src/state/decision-ledger.ts +295 -0
- package/src/state/hook-instinct-bridge.ts +90 -0
- package/src/state/hook-integrations.ts +51 -0
- package/src/state/instinct-store.ts +249 -0
- package/src/state/run-metrics.ts +135 -0
- package/src/state/tiered-eval.ts +471 -0
- package/src/state/types-eval.ts +58 -0
- package/src/state/types.ts +3 -0
- package/src/tools/safe-bash-extension.ts +5 -5
- package/src/ui/crew-widget.ts +1 -1
- package/src/ui/pi-ui-compat.ts +1 -1
- package/src/ui/run-action-dispatcher.ts +1 -1
- package/src/ui/tool-render.ts +2 -2
- package/src/utils/project-detector.ts +160 -0
- package/test-bugs-all.mjs +1 -1
- package/skills/.gitkeep +0 -0
- package/skills/REFERENCE.md +0 -136
|
@@ -0,0 +1,183 @@
|
|
|
1
|
+
import * as crypto from "node:crypto";
|
|
2
|
+
import type { PiTeamsToolResult } from "../tool-result.ts";
|
|
3
|
+
import type { TeamToolParamsValue } from "../../schema/team-tool-schema.ts";
|
|
4
|
+
import { result, type TeamContext } from "./context.ts";
|
|
5
|
+
import { humanizeSchedule, nextRunTime, parseSchedule } from "../../runtime/scheduler.ts";
|
|
6
|
+
import { loadConfig } from "../../config/config.ts";
|
|
7
|
+
import { loadCrewSettings, saveCrewSettings } from "../../runtime/settings-store.ts";
|
|
8
|
+
|
|
9
|
+
// Global key for cross-module scheduler access.
|
|
10
|
+
const CREW_SCHEDULER_KEY = Symbol.for("pi-crew:scheduler");
|
|
11
|
+
type SchedulerRef = { add(job: import("../../runtime/scheduler.ts").ScheduledJob): void; list(): import("../../runtime/scheduler.ts").ScheduledJob[] };
|
|
12
|
+
|
|
13
|
+
function getCrewScheduler(): SchedulerRef | undefined {
|
|
14
|
+
return (globalThis as Record<symbol | string, unknown>)[CREW_SCHEDULER_KEY] as SchedulerRef | undefined;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function registerCrewScheduler(scheduler: SchedulerRef): void {
|
|
18
|
+
(globalThis as Record<symbol | string, unknown>)[CREW_SCHEDULER_KEY] = scheduler;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
interface ScheduleParams {
|
|
22
|
+
team?: string;
|
|
23
|
+
goal?: string;
|
|
24
|
+
task?: string;
|
|
25
|
+
cron?: string;
|
|
26
|
+
interval?: number;
|
|
27
|
+
once?: number | string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function buildScheduleSpec(params: ScheduleParams): {
|
|
31
|
+
spec: import("../../runtime/scheduler.ts").ScheduleSpec;
|
|
32
|
+
schedule: string;
|
|
33
|
+
scheduleType: import("../../runtime/scheduler.ts").ScheduleType;
|
|
34
|
+
intervalMs?: number;
|
|
35
|
+
} {
|
|
36
|
+
// Priority: cron > interval > once
|
|
37
|
+
if (params.cron) {
|
|
38
|
+
const parsed = parseSchedule(params.cron);
|
|
39
|
+
if ("error" in parsed) throw new Error(parsed.error);
|
|
40
|
+
return { spec: parsed, schedule: params.cron, scheduleType: "cron" as const };
|
|
41
|
+
}
|
|
42
|
+
if (params.interval !== undefined && params.interval > 0) {
|
|
43
|
+
const specStr = `${params.interval}ms`;
|
|
44
|
+
const spec = parseSchedule(specStr);
|
|
45
|
+
if ("error" in spec) throw new Error(spec.error);
|
|
46
|
+
return { spec, schedule: specStr, scheduleType: "interval" as const, intervalMs: params.interval };
|
|
47
|
+
}
|
|
48
|
+
if (params.once !== undefined) {
|
|
49
|
+
const ts = typeof params.once === "number" ? new Date(params.once).toISOString() : params.once;
|
|
50
|
+
const parsed = parseSchedule(ts);
|
|
51
|
+
if ("error" in parsed) throw new Error(parsed.error);
|
|
52
|
+
return { spec: parsed, schedule: ts, scheduleType: "once" as const };
|
|
53
|
+
}
|
|
54
|
+
throw new Error("schedule requires one of: cron, interval, or once.");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export function handleSchedule(params: TeamToolParamsValue, ctx: TeamContext): PiTeamsToolResult {
|
|
58
|
+
const team = params.team ?? "default";
|
|
59
|
+
const goal = params.goal ?? params.task ?? "";
|
|
60
|
+
if (!goal) return result("Schedule requires goal or task.", { action: "schedule", status: "error" }, true);
|
|
61
|
+
|
|
62
|
+
let specResult: ReturnType<typeof buildScheduleSpec>;
|
|
63
|
+
try {
|
|
64
|
+
specResult = buildScheduleSpec({
|
|
65
|
+
team,
|
|
66
|
+
goal,
|
|
67
|
+
cron: params.cron,
|
|
68
|
+
interval: params.interval,
|
|
69
|
+
once: params.once as ScheduleParams["once"],
|
|
70
|
+
});
|
|
71
|
+
} catch (err) {
|
|
72
|
+
const msg = err instanceof Error ? err.message : String(err);
|
|
73
|
+
return result(msg, { action: "schedule", status: "error" }, true);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const { spec, schedule, scheduleType, intervalMs } = specResult;
|
|
77
|
+
const next = nextRunTime(spec);
|
|
78
|
+
if ("error" in next) return result(next.error, { action: "schedule", status: "error" }, true);
|
|
79
|
+
|
|
80
|
+
// Build the ScheduledJob
|
|
81
|
+
const job: import("../../runtime/scheduler.ts").ScheduledJob = {
|
|
82
|
+
id: crypto.randomUUID(),
|
|
83
|
+
name: `${team}: ${goal.slice(0, 60)}`,
|
|
84
|
+
description: `Scheduled run for team '${team}'`,
|
|
85
|
+
schedule,
|
|
86
|
+
scheduleType,
|
|
87
|
+
intervalMs,
|
|
88
|
+
subagentType: "team",
|
|
89
|
+
prompt: JSON.stringify({ action: "run", team, goal }),
|
|
90
|
+
enabled: true,
|
|
91
|
+
createdAt: new Date().toISOString(),
|
|
92
|
+
nextRun: next.toISOString(),
|
|
93
|
+
runCount: 0,
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
const scheduler = getCrewScheduler();
|
|
97
|
+
if (!scheduler) {
|
|
98
|
+
// Persist even if scheduler isn't running yet — register.ts loads them on startup.
|
|
99
|
+
persistScheduledJob(ctx.cwd, job);
|
|
100
|
+
return result(
|
|
101
|
+
[
|
|
102
|
+
`Scheduled job created (scheduler not yet running — will activate on next session start):`,
|
|
103
|
+
` Job ID: ${job.id}`,
|
|
104
|
+
` Team: ${team}`,
|
|
105
|
+
` Goal: ${goal}`,
|
|
106
|
+
` Schedule: ${humanizeSchedule(spec)}`,
|
|
107
|
+
` Next run: ${next.toISOString()}`,
|
|
108
|
+
].join("\n"),
|
|
109
|
+
{
|
|
110
|
+
action: "schedule",
|
|
111
|
+
status: "ok",
|
|
112
|
+
data: {
|
|
113
|
+
jobId: job.id,
|
|
114
|
+
team,
|
|
115
|
+
goal,
|
|
116
|
+
schedule: humanizeSchedule(spec),
|
|
117
|
+
nextRun: next.toISOString(),
|
|
118
|
+
pending: true,
|
|
119
|
+
},
|
|
120
|
+
},
|
|
121
|
+
);
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
scheduler.add(job);
|
|
125
|
+
persistScheduledJob(ctx.cwd, job);
|
|
126
|
+
|
|
127
|
+
return result(
|
|
128
|
+
[
|
|
129
|
+
`Scheduled job registered.`,
|
|
130
|
+
` Job ID: ${job.id}`,
|
|
131
|
+
` Team: ${team}`,
|
|
132
|
+
` Goal: ${goal}`,
|
|
133
|
+
` Schedule: ${humanizeSchedule(spec)}`,
|
|
134
|
+
` Next run: ${next.toISOString()}`,
|
|
135
|
+
].join("\n"),
|
|
136
|
+
{
|
|
137
|
+
action: "schedule",
|
|
138
|
+
status: "ok",
|
|
139
|
+
data: {
|
|
140
|
+
jobId: job.id,
|
|
141
|
+
team,
|
|
142
|
+
goal,
|
|
143
|
+
schedule: humanizeSchedule(spec),
|
|
144
|
+
nextRun: next.toISOString(),
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
function persistScheduledJob(cwd: string, job: import("../../runtime/scheduler.ts").ScheduledJob): void {
|
|
151
|
+
try {
|
|
152
|
+
const settings = loadCrewSettings(cwd);
|
|
153
|
+
const existingJobs: import("../../runtime/scheduler.ts").ScheduledJob[] = Array.isArray(
|
|
154
|
+
(settings as Record<string, unknown>).scheduledJobs,
|
|
155
|
+
)
|
|
156
|
+
? ((settings as Record<string, unknown>).scheduledJobs as import("../../runtime/scheduler.ts").ScheduledJob[])
|
|
157
|
+
: [];
|
|
158
|
+
saveCrewSettings(
|
|
159
|
+
{ ...settings, scheduledJobs: [...existingJobs, job] } as Parameters<typeof saveCrewSettings>[0],
|
|
160
|
+
cwd,
|
|
161
|
+
);
|
|
162
|
+
} catch {
|
|
163
|
+
/* best-effort persistence */
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export function handleListScheduled(_params: TeamToolParamsValue, ctx: TeamContext): PiTeamsToolResult {
|
|
168
|
+
const scheduler = getCrewScheduler();
|
|
169
|
+
if (!scheduler) return result("Scheduler not running.", { action: "scheduled", status: "error" }, true);
|
|
170
|
+
const jobs = scheduler.list();
|
|
171
|
+
if (jobs.length === 0) return result("No scheduled jobs.", { action: "scheduled", status: "ok" });
|
|
172
|
+
const lines: string[] = [`Scheduled jobs (${jobs.length}):`];
|
|
173
|
+
for (const job of jobs) {
|
|
174
|
+
lines.push(
|
|
175
|
+
` [${job.id}] ${job.name}`,
|
|
176
|
+
` Schedule: ${job.schedule} (${job.scheduleType})`,
|
|
177
|
+
` Enabled: ${job.enabled}`,
|
|
178
|
+
` Next run: ${job.nextRun ?? "(unscheduled)"}`,
|
|
179
|
+
` Runs: ${job.runCount}, Last: ${job.lastRun ?? "(never)"} [${job.lastStatus ?? "?"}]`,
|
|
180
|
+
);
|
|
181
|
+
}
|
|
182
|
+
return result(lines.join("\n"), { action: "scheduled", status: "ok" });
|
|
183
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Handler for `team action='orchestrate' planPath='/path/to/plan.md'`
|
|
3
|
+
*
|
|
4
|
+
* Parses a plan document and outputs agent chain commands.
|
|
5
|
+
*/
|
|
6
|
+
import * as fs from "node:fs";
|
|
7
|
+
import * as path from "node:path";
|
|
8
|
+
import type { TeamToolParamsValue } from "../../schema/team-tool-schema.ts";
|
|
9
|
+
import type { PiTeamsToolResult } from "../tool-result.ts";
|
|
10
|
+
import { result, type TeamContext } from "./context.ts";
|
|
11
|
+
import {
|
|
12
|
+
buildAgentChain,
|
|
13
|
+
formatPlanOverview,
|
|
14
|
+
parsePlanDocument,
|
|
15
|
+
parsePlanDocumentSimple,
|
|
16
|
+
type OrchestratedStep,
|
|
17
|
+
} from "../plan-orchestrate.ts";
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Handle the orchestrate action.
|
|
21
|
+
*
|
|
22
|
+
* Parses a plan document (markdown with `<!-- tag: <tag> -->` comments)
|
|
23
|
+
* and outputs the decomposed agent chain commands.
|
|
24
|
+
*
|
|
25
|
+
* Usage: `team action='orchestrate' planPath='/path/to/plan.md'`
|
|
26
|
+
*/
|
|
27
|
+
export function handleOrchestrate(
|
|
28
|
+
params: TeamToolParamsValue,
|
|
29
|
+
ctx: TeamContext,
|
|
30
|
+
): PiTeamsToolResult {
|
|
31
|
+
const planPath = params.planPath as string | undefined;
|
|
32
|
+
|
|
33
|
+
if (!planPath) {
|
|
34
|
+
return result(
|
|
35
|
+
"orchestrate requires planPath parameter pointing to a markdown plan document.",
|
|
36
|
+
{ action: "orchestrate", status: "error" },
|
|
37
|
+
true,
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Resolve relative paths against ctx.cwd
|
|
42
|
+
const resolvedPath = path.isAbsolute(planPath)
|
|
43
|
+
? planPath
|
|
44
|
+
: path.resolve(ctx.cwd, planPath);
|
|
45
|
+
|
|
46
|
+
if (!fs.existsSync(resolvedPath)) {
|
|
47
|
+
return result(
|
|
48
|
+
`Plan document not found: ${resolvedPath}`,
|
|
49
|
+
{ action: "orchestrate", status: "error" },
|
|
50
|
+
true,
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// Try primary parser
|
|
55
|
+
let steps: OrchestratedStep[] = parsePlanDocument(resolvedPath);
|
|
56
|
+
|
|
57
|
+
// Fallback to simple parser
|
|
58
|
+
if (steps.length === 0) {
|
|
59
|
+
steps = parsePlanDocumentSimple(resolvedPath);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (steps.length === 0) {
|
|
63
|
+
return result(
|
|
64
|
+
`No tagged sections found in plan document: ${resolvedPath}\n\nExpected format: <!-- tag: <tag> --> in markdown sections`,
|
|
65
|
+
{ action: "orchestrate", status: "error" },
|
|
66
|
+
true,
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
// Build overview and commands
|
|
71
|
+
const overview = formatPlanOverview(resolvedPath);
|
|
72
|
+
const commands = buildAgentChain(steps);
|
|
73
|
+
|
|
74
|
+
const outputLines: string[] = [
|
|
75
|
+
`Plan: ${resolvedPath}`,
|
|
76
|
+
`Steps: ${steps.length}`,
|
|
77
|
+
"",
|
|
78
|
+
"# Agent Chain Commands",
|
|
79
|
+
"",
|
|
80
|
+
...commands.map((cmd, i) => `${i + 1}. ${cmd}`),
|
|
81
|
+
"",
|
|
82
|
+
"# Full Overview",
|
|
83
|
+
overview,
|
|
84
|
+
];
|
|
85
|
+
|
|
86
|
+
return result(outputLines.join("\n"), {
|
|
87
|
+
action: "orchestrate",
|
|
88
|
+
status: "ok",
|
|
89
|
+
data: {
|
|
90
|
+
planPath: resolvedPath,
|
|
91
|
+
stepCount: steps.length,
|
|
92
|
+
commands,
|
|
93
|
+
steps: steps.map((sqs) => ({
|
|
94
|
+
stepId: sqs.stepId,
|
|
95
|
+
tag: sqs.tag,
|
|
96
|
+
chain: sqs.chain,
|
|
97
|
+
prompt: sqs.prompt,
|
|
98
|
+
heading: sqs.heading,
|
|
99
|
+
})),
|
|
100
|
+
},
|
|
101
|
+
});
|
|
102
|
+
}
|
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { allAgents, discoverAgents } from "../../agents/discover-agents.ts";
|
|
2
|
-
import { ensureCrewDirectory } from "../../state/crew-init.ts";
|
|
3
2
|
import { allTeams, discoverTeams } from "../../teams/discover-teams.ts";
|
|
4
3
|
import { allWorkflows, discoverWorkflows } from "../../workflows/discover-workflows.ts";
|
|
5
4
|
import { loadConfig } from "../../config/config.ts";
|
|
@@ -28,8 +27,11 @@ import { resolveCrewRuntime, runtimeResolutionState } from "../../runtime/runtim
|
|
|
28
27
|
import { normalizeSkillOverride } from "../../runtime/skill-instructions.ts";
|
|
29
28
|
import { expandParallelResearchWorkflow } from "../../runtime/parallel-research.ts";
|
|
30
29
|
import { checkProcessLiveness, isActiveRunStatus } from "../../runtime/process-status.ts";
|
|
30
|
+
import { waitForRun } from "../../runtime/run-tracker.ts";
|
|
31
31
|
import { hasAsyncStartMarker } from "../../runtime/async-marker.ts";
|
|
32
|
+
import { collectRunMetrics } from "../../state/run-metrics.ts";
|
|
32
33
|
import * as fs from "node:fs";
|
|
34
|
+
import * as path from "node:path";
|
|
33
35
|
import type { PiTeamsToolResult } from "../tool-result.ts";
|
|
34
36
|
import { buildParentContext, result, type TeamContext } from "./context.ts";
|
|
35
37
|
import { effectiveRunConfig } from "./config-patch.ts";
|
|
@@ -76,7 +78,10 @@ export async function handleRun(params: TeamToolParamsValue, ctx: TeamContext):
|
|
|
76
78
|
const intentPrefix = goal.length > 60 ? `${goal.slice(0, 57)}...` : goal;
|
|
77
79
|
|
|
78
80
|
// P0: Ensure .crew directory structure exists before creating any manifests.
|
|
79
|
-
|
|
81
|
+
// Dynamic import to avoid module binding issues in child-process contexts.
|
|
82
|
+
const workingDir = ctx.cwd ?? process.cwd();
|
|
83
|
+
const { ensureCrewDirectory } = await import("../../state/crew-init.ts");
|
|
84
|
+
await ensureCrewDirectory(workingDir);
|
|
80
85
|
|
|
81
86
|
const teams = allTeams(discoverTeams(ctx.cwd));
|
|
82
87
|
const workflows = allWorkflows(discoverWorkflows(ctx.cwd));
|
|
@@ -166,19 +171,110 @@ export async function handleRun(params: TeamToolParamsValue, ctx: TeamContext):
|
|
|
166
171
|
atomicWriteJson(paths.manifestPath, asyncManifest);
|
|
167
172
|
appendEvent(effectiveManifest.eventsPath, { type: "async.spawned", runId: effectiveManifest.runId, data: { pid: spawned.pid, logPath: spawned.logPath } });
|
|
168
173
|
scheduleBackgroundEarlyExitGuard(ctx.cwd, effectiveManifest.runId, spawned.pid, spawned.logPath);
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
174
|
+
// Wait for the async run to complete and return actual results.
|
|
175
|
+
try {
|
|
176
|
+
const completed = await waitForRun(updatedManifest.runId, ctx.cwd, { timeoutMs: 3600000 });
|
|
177
|
+
const metrics = collectRunMetrics(ctx.cwd, completed.manifest.runId);
|
|
178
|
+
const lines: string[] = [
|
|
179
|
+
`pi-crew run ${completed.manifest.status}: ${completed.manifest.runId} (${team.name})`,
|
|
180
|
+
`Goal: ${goal.slice(0, 100)}`,
|
|
181
|
+
];
|
|
182
|
+
if (metrics) {
|
|
183
|
+
lines.push("");
|
|
184
|
+
lines.push(`Metrics: ${metrics.completedCount}/${metrics.taskCount} tasks, ${metrics.totalTokens} tokens, ${metrics.durationMs}ms, consistency=${metrics.consistencyScore}`);
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (completed.tasks.length > 0) {
|
|
188
|
+
// Read run-level summary artifact if present
|
|
189
|
+
let summaryContent: string | undefined;
|
|
190
|
+
const summaryArtifact = completed.manifest.artifacts?.find(
|
|
191
|
+
(a: { kind?: string }) => a.kind === "summary",
|
|
192
|
+
);
|
|
193
|
+
if (summaryArtifact) {
|
|
194
|
+
try {
|
|
195
|
+
const sumPath = path.join(completed.manifest.artifactsRoot, summaryArtifact.path);
|
|
196
|
+
summaryContent = fs.readFileSync(sumPath, "utf-8").trim().slice(0, 4000);
|
|
197
|
+
} catch {
|
|
198
|
+
/* summary unavailable */
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
const taskLines: string[] = [];
|
|
203
|
+
let failedCount = 0;
|
|
204
|
+
const failedIds: string[] = [];
|
|
205
|
+
for (const task of completed.tasks) {
|
|
206
|
+
let resultExcerpt = "";
|
|
207
|
+
if (task.resultArtifact?.path) {
|
|
208
|
+
try {
|
|
209
|
+
const resPath = path.isAbsolute(task.resultArtifact.path)
|
|
210
|
+
? task.resultArtifact.path
|
|
211
|
+
: path.join(completed.manifest.artifactsRoot, task.resultArtifact.path);
|
|
212
|
+
resultExcerpt = fs.readFileSync(resPath, "utf-8").trim().slice(0, 2000);
|
|
213
|
+
} catch {
|
|
214
|
+
resultExcerpt = "(result unavailable)";
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
const shortResult = resultExcerpt.slice(0, 500);
|
|
218
|
+
const statusTag =
|
|
219
|
+
task.status === "completed" ? "✓"
|
|
220
|
+
: task.status === "failed" ? "✗"
|
|
221
|
+
: task.status === "cancelled" ? "⊘"
|
|
222
|
+
: "·";
|
|
223
|
+
taskLines.push(
|
|
224
|
+
`- ${statusTag} ${task.id} [${task.role}]: ${task.status}${shortResult ? " — " + shortResult : ""}${task.error ? ` | Error: ${task.error.slice(0, 200)}` : ""}`,
|
|
225
|
+
);
|
|
226
|
+
if (task.status === "failed" || task.status === "needs_attention") {
|
|
227
|
+
failedCount++;
|
|
228
|
+
failedIds.push(task.id);
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
lines.push("");
|
|
233
|
+
lines.push(`Tasks (${completed.tasks.length}):`);
|
|
234
|
+
lines.push(...taskLines);
|
|
235
|
+
|
|
236
|
+
if (summaryContent) {
|
|
237
|
+
lines.push("");
|
|
238
|
+
lines.push("Summary:");
|
|
239
|
+
lines.push(summaryContent.slice(0, 2000));
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
if (failedCount === 0) {
|
|
243
|
+
lines.push("");
|
|
244
|
+
lines.push("All tasks completed successfully.");
|
|
245
|
+
} else {
|
|
246
|
+
lines.push("");
|
|
247
|
+
lines.push(
|
|
248
|
+
`${failedCount} task(s) failed: ${failedIds.join(", ")}. Consider retrying.`,
|
|
249
|
+
);
|
|
250
|
+
}
|
|
251
|
+
} else {
|
|
252
|
+
lines.push(
|
|
253
|
+
completed.manifest.status === "completed"
|
|
254
|
+
? "Run completed with no task results."
|
|
255
|
+
: `The run ended with status: ${completed.manifest.status}. Check the run artifacts for details.`,
|
|
256
|
+
);
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
const runFailed = completed.manifest.status === "failed" || completed.manifest.status === "blocked";
|
|
260
|
+
return result(lines.join("\n"), { action: "run", status: runFailed ? "error" : "ok", runId: completed.manifest.runId, artifactsRoot: completed.manifest.artifactsRoot }, runFailed);
|
|
261
|
+
} catch (waitError: unknown) {
|
|
262
|
+
const errorMessage = waitError instanceof Error ? waitError.message : String(waitError);
|
|
263
|
+
return result(
|
|
264
|
+
[
|
|
265
|
+
`pi-crew run timed out or failed: ${updatedManifest.runId}`,
|
|
266
|
+
`Team: ${team.name}`,
|
|
267
|
+
`Workflow: ${workflow.name}`,
|
|
268
|
+
`Error: ${errorMessage}`,
|
|
269
|
+
"",
|
|
270
|
+
`Check status with: team status runId=${updatedManifest.runId}`,
|
|
271
|
+
`State: ${updatedManifest.stateRoot}`,
|
|
272
|
+
`Background log: ${spawned.logPath}`,
|
|
273
|
+
].join("\n"),
|
|
274
|
+
{ action: "run", status: "error", runId: updatedManifest.runId, artifactsRoot: updatedManifest.artifactsRoot },
|
|
275
|
+
true,
|
|
276
|
+
);
|
|
277
|
+
}
|
|
182
278
|
}
|
|
183
279
|
|
|
184
280
|
if (runtime.safety === "blocked") {
|
|
@@ -207,19 +303,110 @@ export async function handleRun(params: TeamToolParamsValue, ctx: TeamContext):
|
|
|
207
303
|
unregisterActiveRun(updatedManifest.runId);
|
|
208
304
|
}
|
|
209
305
|
}, updatedManifest.runId);
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
306
|
+
|
|
307
|
+
// Wait for the foreground run to complete and return actual results.
|
|
308
|
+
try {
|
|
309
|
+
const completed = await waitForRun(updatedManifest.runId, ctx.cwd, { timeoutMs: 3600000 });
|
|
310
|
+
const metrics = collectRunMetrics(ctx.cwd, completed.manifest.runId);
|
|
311
|
+
const lines: string[] = [
|
|
312
|
+
`pi-crew run ${completed.manifest.status}: ${completed.manifest.runId} (${team.name})`,
|
|
313
|
+
`Goal: ${goal.slice(0, 100)}`,
|
|
314
|
+
];
|
|
315
|
+
if (metrics) {
|
|
316
|
+
lines.push("");
|
|
317
|
+
lines.push(`Metrics: ${metrics.completedCount}/${metrics.taskCount} tasks, ${metrics.totalTokens} tokens, ${metrics.durationMs}ms, consistency=${metrics.consistencyScore}`);
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
if (completed.tasks.length > 0) {
|
|
321
|
+
// Read run-level summary artifact if present
|
|
322
|
+
let summaryContent: string | undefined;
|
|
323
|
+
const summaryArtifact = completed.manifest.artifacts?.find(
|
|
324
|
+
(a: { kind?: string }) => a.kind === "summary",
|
|
325
|
+
);
|
|
326
|
+
if (summaryArtifact) {
|
|
327
|
+
try {
|
|
328
|
+
const sumPath = path.join(completed.manifest.artifactsRoot, summaryArtifact.path);
|
|
329
|
+
summaryContent = fs.readFileSync(sumPath, "utf-8").trim().slice(0, 4000);
|
|
330
|
+
} catch {
|
|
331
|
+
/* summary unavailable */
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
const taskLines: string[] = [];
|
|
336
|
+
let failedCount = 0;
|
|
337
|
+
const failedIds: string[] = [];
|
|
338
|
+
for (const task of completed.tasks) {
|
|
339
|
+
let resultExcerpt = "";
|
|
340
|
+
if (task.resultArtifact?.path) {
|
|
341
|
+
try {
|
|
342
|
+
const resPath = path.isAbsolute(task.resultArtifact.path)
|
|
343
|
+
? task.resultArtifact.path
|
|
344
|
+
: path.join(completed.manifest.artifactsRoot, task.resultArtifact.path);
|
|
345
|
+
resultExcerpt = fs.readFileSync(resPath, "utf-8").trim().slice(0, 2000);
|
|
346
|
+
} catch {
|
|
347
|
+
resultExcerpt = "(result unavailable)";
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
const shortResult = resultExcerpt.slice(0, 500);
|
|
351
|
+
const statusTag =
|
|
352
|
+
task.status === "completed" ? "✓"
|
|
353
|
+
: task.status === "failed" ? "✗"
|
|
354
|
+
: task.status === "cancelled" ? "⊘"
|
|
355
|
+
: "·";
|
|
356
|
+
taskLines.push(
|
|
357
|
+
`- ${statusTag} ${task.id} [${task.role}]: ${task.status}${shortResult ? " — " + shortResult : ""}${task.error ? ` | Error: ${task.error.slice(0, 200)}` : ""}`,
|
|
358
|
+
);
|
|
359
|
+
if (task.status === "failed" || task.status === "needs_attention") {
|
|
360
|
+
failedCount++;
|
|
361
|
+
failedIds.push(task.id);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
lines.push("");
|
|
366
|
+
lines.push(`Tasks (${completed.tasks.length}):`);
|
|
367
|
+
lines.push(...taskLines);
|
|
368
|
+
|
|
369
|
+
if (summaryContent) {
|
|
370
|
+
lines.push("");
|
|
371
|
+
lines.push("Summary:");
|
|
372
|
+
lines.push(summaryContent.slice(0, 2000));
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
if (failedCount === 0) {
|
|
376
|
+
lines.push("");
|
|
377
|
+
lines.push("All tasks completed successfully.");
|
|
378
|
+
} else {
|
|
379
|
+
lines.push("");
|
|
380
|
+
lines.push(
|
|
381
|
+
`${failedCount} task(s) failed: ${failedIds.join(", ")}. Consider retrying.`,
|
|
382
|
+
);
|
|
383
|
+
}
|
|
384
|
+
} else {
|
|
385
|
+
lines.push(
|
|
386
|
+
completed.manifest.status === "completed"
|
|
387
|
+
? "Run completed with no task results."
|
|
388
|
+
: `The run ended with status: ${completed.manifest.status}. Check the run artifacts for details.`,
|
|
389
|
+
);
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
const runFailed = completed.manifest.status === "failed" || completed.manifest.status === "blocked";
|
|
393
|
+
return result(lines.join("\n"), { action: "run", status: runFailed ? "error" : "ok", runId: completed.manifest.runId, artifactsRoot: completed.manifest.artifactsRoot }, runFailed);
|
|
394
|
+
} catch (waitError: unknown) {
|
|
395
|
+
const errorMessage = waitError instanceof Error ? waitError.message : String(waitError);
|
|
396
|
+
return result(
|
|
397
|
+
[
|
|
398
|
+
`pi-crew run timed out or failed: ${updatedManifest.runId}`,
|
|
399
|
+
`Team: ${team.name}`,
|
|
400
|
+
`Workflow: ${workflow.name}`,
|
|
401
|
+
`Error: ${errorMessage}`,
|
|
402
|
+
"",
|
|
403
|
+
`Check status with: team status runId=${updatedManifest.runId}`,
|
|
404
|
+
`State: ${updatedManifest.stateRoot}`,
|
|
405
|
+
].join("\n"),
|
|
406
|
+
{ action: "run", status: "error", runId: updatedManifest.runId, artifactsRoot: updatedManifest.artifactsRoot },
|
|
407
|
+
true,
|
|
408
|
+
);
|
|
409
|
+
}
|
|
223
410
|
}
|
|
224
411
|
let executed: Awaited<ReturnType<typeof executeTeamRun>>;
|
|
225
412
|
try {
|
|
@@ -165,7 +165,9 @@ import {
|
|
|
165
165
|
import { FileCheckpointStore } from "../runtime/checkpoint.ts";
|
|
166
166
|
import { buildTeamOnboarding } from "./team-onboard.ts";
|
|
167
167
|
import { handleParallel } from "./team-tool/parallel-dispatch.ts";
|
|
168
|
+
import { handleSchedule, handleListScheduled } from "./team-tool/handle-schedule.ts";
|
|
168
169
|
import { handlePlan } from "./team-tool/plan.ts";
|
|
170
|
+
import { handleOrchestrate } from "./team-tool/orchestrate.ts";
|
|
169
171
|
import { handleRespond } from "./team-tool/respond.ts";
|
|
170
172
|
import { handleStatus } from "./team-tool/status.ts";
|
|
171
173
|
|
|
@@ -187,10 +189,12 @@ export {
|
|
|
187
189
|
handlePrune,
|
|
188
190
|
handleWorktrees,
|
|
189
191
|
} from "./team-tool/lifecycle-actions.ts";
|
|
192
|
+
export { handleSchedule } from "./team-tool/handle-schedule.ts";
|
|
190
193
|
export { handlePlan } from "./team-tool/plan.ts";
|
|
191
194
|
export { handleStatus } from "./team-tool/status.ts";
|
|
192
195
|
export type { TeamToolDetails } from "./team-tool-types.ts";
|
|
193
196
|
export { handleRun };
|
|
197
|
+
export { handleOrchestrate } from "./team-tool/orchestrate.ts";
|
|
194
198
|
|
|
195
199
|
export function handleList(
|
|
196
200
|
params: TeamToolParamsValue,
|
|
@@ -1089,6 +1093,8 @@ export async function handleTeamTool(
|
|
|
1089
1093
|
return await handleParallel(params, ctx);
|
|
1090
1094
|
case "plan":
|
|
1091
1095
|
return handlePlan(params, ctx);
|
|
1096
|
+
case "orchestrate":
|
|
1097
|
+
return handleOrchestrate(params, ctx);
|
|
1092
1098
|
case "resume":
|
|
1093
1099
|
return handleResume(params, ctx);
|
|
1094
1100
|
case "create":
|
|
@@ -1147,6 +1153,10 @@ export async function handleTeamTool(
|
|
|
1147
1153
|
return result(`Search failed: ${msg}`, { action: "search", status: "error" }, true);
|
|
1148
1154
|
}
|
|
1149
1155
|
}
|
|
1156
|
+
case "schedule":
|
|
1157
|
+
return handleSchedule(params, ctx);
|
|
1158
|
+
case "scheduled":
|
|
1159
|
+
return handleListScheduled(params, ctx);
|
|
1150
1160
|
case "onboard": {
|
|
1151
1161
|
const team = params.team ?? "default";
|
|
1152
1162
|
const onboarding = buildTeamOnboarding(team, ctx.cwd);
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { AgentToolResult } from "@
|
|
1
|
+
import type { AgentToolResult } from "@earendil-works/pi-agent-core";
|
|
2
2
|
import type { TeamToolDetails } from "./team-tool-types.ts";
|
|
3
3
|
|
|
4
4
|
export type PiTeamsToolResult<TDetails = TeamToolDetails> = AgentToolResult<TDetails> & { isError?: boolean };
|
package/src/i18n.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ExtensionAPI } from "@
|
|
1
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
|
|
3
3
|
export const PI_TEAMS_INHERIT_PROJECT_CONTEXT_ENV = "PI_TEAMS_INHERIT_PROJECT_CONTEXT";
|
|
4
4
|
export const PI_TEAMS_INHERIT_SKILLS_ENV = "PI_TEAMS_INHERIT_SKILLS";
|