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.
Files changed (66) hide show
  1. package/CHANGELOG.md +28 -0
  2. package/README.md +1 -1
  3. package/docs/actions-reference.md +87 -0
  4. package/docs/commands-reference.md +5 -0
  5. package/docs/pi-crew-bugs.md +6 -0
  6. package/index.ts +1 -1
  7. package/package.json +18 -16
  8. package/src/benchmark/benchmark-runner.ts +245 -0
  9. package/src/benchmark/feedback-loop.ts +66 -0
  10. package/src/extension/async-notifier.ts +1 -1
  11. package/src/extension/autonomous-policy.ts +1 -1
  12. package/src/extension/cross-extension-rpc.ts +1 -1
  13. package/src/extension/plan-orchestrate.ts +322 -0
  14. package/src/extension/register.ts +31 -41
  15. package/src/extension/registration/command-utils.ts +1 -1
  16. package/src/extension/registration/commands.ts +1 -1
  17. package/src/extension/registration/compaction-guard.ts +1 -1
  18. package/src/extension/registration/subagent-helpers.ts +1 -1
  19. package/src/extension/registration/subagent-tools.ts +1 -1
  20. package/src/extension/registration/team-tool.ts +1 -1
  21. package/src/extension/registration/viewers.ts +1 -1
  22. package/src/extension/session-summary.ts +1 -1
  23. package/src/extension/team-manager-command.ts +1 -1
  24. package/src/extension/team-tool/context.ts +1 -1
  25. package/src/extension/team-tool/handle-schedule.ts +183 -0
  26. package/src/extension/team-tool/orchestrate.ts +102 -0
  27. package/src/extension/team-tool/run.ts +215 -28
  28. package/src/extension/team-tool.ts +10 -0
  29. package/src/extension/tool-result.ts +1 -1
  30. package/src/i18n.ts +1 -1
  31. package/src/observability/event-to-metric.ts +1 -1
  32. package/src/prompt/prompt-runtime.ts +1 -1
  33. package/src/runtime/background-runner.ts +27 -5
  34. package/src/runtime/crash-recovery.ts +1 -1
  35. package/src/runtime/crew-hooks.ts +240 -0
  36. package/src/runtime/custom-tools/irc-tool.ts +1 -1
  37. package/src/runtime/custom-tools/submit-result-tool.ts +1 -1
  38. package/src/runtime/diagnostic-export.ts +38 -2
  39. package/src/runtime/foreground-watchdog.ts +1 -1
  40. package/src/runtime/live-session-runtime.ts +1 -1
  41. package/src/runtime/mcp-proxy.ts +1 -1
  42. package/src/runtime/pi-spawn.ts +20 -4
  43. package/src/runtime/process-status.ts +15 -2
  44. package/src/runtime/runtime-resolver.ts +1 -1
  45. package/src/runtime/session-resources.ts +1 -1
  46. package/src/runtime/task-runner.ts +31 -1
  47. package/src/runtime/team-runner.ts +6 -0
  48. package/src/schema/team-tool-schema.ts +24 -1
  49. package/src/state/crew-init.ts +56 -38
  50. package/src/state/decision-ledger.ts +295 -0
  51. package/src/state/hook-instinct-bridge.ts +90 -0
  52. package/src/state/hook-integrations.ts +51 -0
  53. package/src/state/instinct-store.ts +249 -0
  54. package/src/state/run-metrics.ts +135 -0
  55. package/src/state/tiered-eval.ts +471 -0
  56. package/src/state/types-eval.ts +58 -0
  57. package/src/state/types.ts +3 -0
  58. package/src/tools/safe-bash-extension.ts +5 -5
  59. package/src/ui/crew-widget.ts +1 -1
  60. package/src/ui/pi-ui-compat.ts +1 -1
  61. package/src/ui/run-action-dispatcher.ts +1 -1
  62. package/src/ui/tool-render.ts +2 -2
  63. package/src/utils/project-detector.ts +160 -0
  64. package/test-bugs-all.mjs +1 -1
  65. package/skills/.gitkeep +0 -0
  66. 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
- await ensureCrewDirectory(ctx.cwd);
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
- const text = [
170
- `Started async pi-crew run ${updatedManifest.runId}.`,
171
- `Team: ${team.name}`,
172
- `Workflow: ${workflow.name}`,
173
- `Status: ${updatedManifest.status}`,
174
- `Tasks: ${tasks.length}`,
175
- `State: ${updatedManifest.stateRoot}`,
176
- `Artifacts: ${updatedManifest.artifactsRoot}`,
177
- `Background log: ${spawned.logPath}`,
178
- "",
179
- `Check status with: team status runId=${updatedManifest.runId}`,
180
- ].join("\n");
181
- return result(text, { action: "run", status: "ok", runId: updatedManifest.runId, artifactsRoot: updatedManifest.artifactsRoot, intent: `running ${team.name}: ${intentPrefix}` });
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
- const text = [
211
- `Started foreground pi-crew run ${updatedManifest.runId}.`,
212
- `Team: ${team.name}`,
213
- `Workflow: ${workflow.name}`,
214
- "Status: running",
215
- `Tasks: ${tasks.length}`,
216
- `Runtime: ${runtime.kind}`,
217
- `State: ${updatedManifest.stateRoot}`,
218
- `Artifacts: ${updatedManifest.artifactsRoot}`,
219
- "",
220
- "The run continues in this Pi session without blocking the chat. It will be interrupted on session shutdown. Use /team-dashboard or /team-status to watch it.",
221
- ].join("\n");
222
- return result(text, { action: "run", status: "ok", runId: updatedManifest.runId, artifactsRoot: updatedManifest.artifactsRoot, intent: `running ${team.name}: ${intentPrefix}` });
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 "@mariozechner/pi-agent-core";
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 "@mariozechner/pi-coding-agent";
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
 
3
3
  type Params = Record<string, string | number>;
4
4
 
@@ -1,4 +1,4 @@
1
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
1
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
2
2
  import type { MetricRegistry } from "./metric-registry.ts";
3
3
 
4
4
  function recordValue(value: unknown): Record<string, unknown> {
@@ -1,4 +1,4 @@
1
- import type { ExtensionAPI } from "@mariozechner/pi-coding-agent";
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";