pi-crew 0.7.5 → 0.7.6
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 +51 -0
- package/README.md +11 -11
- package/docs/commands-reference.md +14 -10
- package/docs/troubleshooting.md +131 -0
- package/docs/usage.md +9 -4
- package/package.json +1 -1
- package/src/config/config.ts +11 -4
- package/src/extension/action-suggestions.ts +71 -0
- package/src/extension/context-status-injection.ts +32 -1
- package/src/extension/register.ts +71 -65
- package/src/extension/team-tool/api.ts +3 -2
- package/src/extension/team-tool/cancel.ts +5 -4
- package/src/extension/team-tool/explain.ts +2 -1
- package/src/extension/team-tool/failure-patterns.ts +124 -0
- package/src/extension/team-tool/inspect.ts +10 -6
- package/src/extension/team-tool/lifecycle-actions.ts +5 -4
- package/src/extension/team-tool/respond.ts +4 -3
- package/src/extension/team-tool/run-not-found.ts +54 -0
- package/src/extension/team-tool/run.ts +26 -4
- package/src/extension/team-tool/status.ts +58 -4
- package/src/extension/team-tool.ts +5 -3
- package/src/runtime/async-runner.ts +7 -0
- package/src/runtime/background-runner.ts +7 -1
- package/src/runtime/chain-parser.ts +13 -5
- package/src/runtime/checkpoint.ts +13 -1
- package/src/runtime/child-pi.ts +9 -1
- package/src/runtime/live-session-runtime.ts +15 -1
- package/src/runtime/parent-guard.ts +2 -2
- package/src/runtime/stale-reconciler.ts +8 -3
- package/src/runtime/task-runner.ts +10 -1
- package/src/runtime/team-runner.ts +19 -2
- package/src/runtime/verification-gates.ts +21 -1
- package/src/schema/team-tool-schema.ts +9 -0
- package/src/state/blob-store.ts +12 -10
- package/src/state/event-log-rotation.ts +114 -93
- package/src/state/event-log.ts +79 -20
- package/src/state/health-store.ts +6 -1
- package/src/state/locks.ts +66 -16
- package/src/state/state-store.ts +14 -1
- package/src/ui/card-colors.ts +7 -3
- package/src/ui/dashboard-panes/agents-pane.ts +15 -2
- package/src/ui/live-duration.ts +58 -0
- package/src/ui/tool-render.ts +7 -11
- package/src/ui/tool-renderers/index.ts +6 -3
- package/src/ui/widget/widget-formatters.ts +2 -13
- package/src/utils/fs-watch.ts +11 -60
- package/src/utils/run-watcher-registry.ts +164 -0
- package/src/workflows/discover-workflows.ts +2 -1
- package/src/workflows/workflow-config.ts +5 -0
- package/src/runtime/dynamic-script-runner.ts +0 -497
- package/src/runtime/sandbox.ts +0 -335
|
@@ -24,6 +24,7 @@ import { resolveRealContainedPath } from "../../utils/safe-paths.ts";
|
|
|
24
24
|
import type { PiTeamsToolResult } from "../tool-result.ts";
|
|
25
25
|
import { locateRunCwd } from "../team-tool.ts";
|
|
26
26
|
import { configRecord, result, type TeamContext } from "./context.ts";
|
|
27
|
+
import { RUN_NOT_FOUND_HINT } from "./run-not-found.ts";
|
|
27
28
|
|
|
28
29
|
export function globMatch(value: string, pattern: string): boolean {
|
|
29
30
|
// Prevent ReDoS: reject excessively long patterns
|
|
@@ -91,9 +92,9 @@ export async function handleApi(params: TeamToolParamsValue, ctx: TeamContext):
|
|
|
91
92
|
}
|
|
92
93
|
if (!params.runId) return result("API requires runId.", { action: "api", status: "error" }, true);
|
|
93
94
|
const runCwd = locateRunCwd(params.runId, ctx.cwd);
|
|
94
|
-
if (!runCwd) return result(`Run '${params.runId}' not found
|
|
95
|
+
if (!runCwd) return result(`Run '${params.runId}' not found.${RUN_NOT_FOUND_HINT}`, { action: "api", status: "error" }, true);
|
|
95
96
|
const loaded = loadRunManifestById(runCwd, params.runId); // NOTE: no withRunLock - best-effort only; concurrent writes may cause inconsistency
|
|
96
|
-
if (!loaded) return result(`Run '${params.runId}' not found
|
|
97
|
+
if (!loaded) return result(`Run '${params.runId}' not found.${RUN_NOT_FOUND_HINT}`, { action: "api", status: "error" }, true);
|
|
97
98
|
if (operation === "read-manifest") {
|
|
98
99
|
return result(JSON.stringify(loaded.manifest, null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
|
|
99
100
|
}
|
|
@@ -12,6 +12,7 @@ import { executeHook, appendHookEvent } from "../../hooks/registry.ts";
|
|
|
12
12
|
import type { PiTeamsToolResult } from "../tool-result.ts";
|
|
13
13
|
import { locateRunCwd } from "../team-tool.ts";
|
|
14
14
|
import { result, type TeamContext } from "./context.ts";
|
|
15
|
+
import { RUN_NOT_FOUND_HINT } from "./run-not-found.ts";
|
|
15
16
|
import { enforceDestructiveIntent, intentFromConfig } from "./intent-policy.ts";
|
|
16
17
|
import { invalidateSnapshot, type CacheControlDeps } from "./cache-control.ts";
|
|
17
18
|
|
|
@@ -80,9 +81,9 @@ function cancelReasonFromParams(params: TeamToolParamsValue): CancellationReason
|
|
|
80
81
|
export async function handleRetry(params: TeamToolParamsValue, ctx: TeamContext, deps?: CacheControlDeps): Promise<PiTeamsToolResult> {
|
|
81
82
|
if (!params.runId) return result("Retry requires runId.", { action: "retry", status: "error" }, true);
|
|
82
83
|
const runCwd = locateRunCwd(params.runId, ctx.cwd);
|
|
83
|
-
if (!runCwd) return result(`Run '${params.runId}' not found
|
|
84
|
+
if (!runCwd) return result(`Run '${params.runId}' not found.${RUN_NOT_FOUND_HINT}`, { action: "retry", status: "error" }, true);
|
|
84
85
|
const loaded = loadRunManifestById(runCwd, params.runId); // NOTE: no withRunLock - best-effort only; concurrent writes may cause inconsistency
|
|
85
|
-
if (!loaded) return result(`Run '${params.runId}' not found
|
|
86
|
+
if (!loaded) return result(`Run '${params.runId}' not found.${RUN_NOT_FOUND_HINT}`, { action: "retry", status: "error" }, true);
|
|
86
87
|
|
|
87
88
|
// Pre-lock ownership check: reject foreign-owned runs unless force is set
|
|
88
89
|
const foreignRun = typeof loaded.manifest.ownerSessionId === "string" && loaded.manifest.ownerSessionId !== ctx.sessionId;
|
|
@@ -145,9 +146,9 @@ export async function handleCancel(params: TeamToolParamsValue, ctx: TeamContext
|
|
|
145
146
|
if (intentError) return intentError;
|
|
146
147
|
if (!params.runId) return result("Cancel requires runId.", { action: "cancel", status: "error" }, true);
|
|
147
148
|
const runCwd = locateRunCwd(params.runId, ctx.cwd);
|
|
148
|
-
if (!runCwd) return result(`Run '${params.runId}' not found
|
|
149
|
+
if (!runCwd) return result(`Run '${params.runId}' not found.${RUN_NOT_FOUND_HINT}`, { action: "cancel", status: "error" }, true);
|
|
149
150
|
const loaded = loadRunManifestById(runCwd, params.runId); // NOTE: no withRunLock - best-effort only; concurrent writes may cause inconsistency
|
|
150
|
-
if (!loaded) return result(`Run '${params.runId}' not found
|
|
151
|
+
if (!loaded) return result(`Run '${params.runId}' not found.${RUN_NOT_FOUND_HINT}`, { action: "cancel", status: "error" }, true);
|
|
151
152
|
|
|
152
153
|
// Pre-lock ownership check: reject foreign-owned runs unless force is set
|
|
153
154
|
const preCheck = abortOwned(loaded.manifest.runId, undefined, ctx, params.force);
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
|
+
import { RUN_NOT_FOUND_HINT } from "./run-not-found.ts";
|
|
2
3
|
import * as path from "node:path";
|
|
3
4
|
import { loadRunManifestById } from "../../state/state-store.ts";
|
|
4
5
|
import type { TeamRunManifest, TeamTaskState } from "../../state/types.ts";
|
|
@@ -211,7 +212,7 @@ export function handleExplain(params: {
|
|
|
211
212
|
|
|
212
213
|
const loaded = loadRunManifestById(cwd, params.runId); // NOTE: no withRunLock - best-effort only; concurrent writes may cause inconsistency
|
|
213
214
|
if (!loaded) {
|
|
214
|
-
return result(`Run '${params.runId}' not found
|
|
215
|
+
return result(`Run '${params.runId}' not found.${RUN_NOT_FOUND_HINT}`, { action: "explain", status: "error" }, true);
|
|
215
216
|
}
|
|
216
217
|
|
|
217
218
|
const { manifest, tasks } = loaded;
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* failure-patterns.ts — Group failed tasks by error similarity (Round 17 BS-4).
|
|
3
|
+
*
|
|
4
|
+
* Before this, a run with 8 failed tasks surfaced 8 separate raw error
|
|
5
|
+
* strings. The user had to mentally group them ("5 of these say 'model
|
|
6
|
+
* routing fallback failed'"). This module detects common failure patterns
|
|
7
|
+
* so `summary` can say "5 of 8 failures share root cause: X".
|
|
8
|
+
*
|
|
9
|
+
* Grouping strategy (cheap, deterministic, no ML):
|
|
10
|
+
* 1. Normalize: lowercase, collapse whitespace, strip task ids / run ids /
|
|
11
|
+
* absolute paths / numbers → a canonical "signature".
|
|
12
|
+
* 2. Bucket by signature. Buckets with >1 member are "common patterns".
|
|
13
|
+
* 3. Sort by frequency desc.
|
|
14
|
+
*
|
|
15
|
+
* Conservative: only buckets with >=2 members count as a pattern (a single
|
|
16
|
+
* failure is just itself). Returns [] when there are no repeated signatures.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
export interface FailurePattern {
|
|
20
|
+
/** Canonical error signature used for grouping. */
|
|
21
|
+
signature: string;
|
|
22
|
+
/** A representative original error (the shortest variant) for display. */
|
|
23
|
+
representative: string;
|
|
24
|
+
/** Task ids that hit this pattern. */
|
|
25
|
+
taskIds: string[];
|
|
26
|
+
/** Count of failures in this bucket (== taskIds.length). */
|
|
27
|
+
count: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export interface FailurePatternInput {
|
|
31
|
+
id: string;
|
|
32
|
+
status: string;
|
|
33
|
+
error?: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Normalize an error string into a grouping signature.
|
|
38
|
+
* Exported for unit testing.
|
|
39
|
+
*/
|
|
40
|
+
export function normalizeErrorSignature(error: string | undefined): string {
|
|
41
|
+
if (!error) return "(no error detail)";
|
|
42
|
+
let s = error.toLowerCase();
|
|
43
|
+
// Strip run ids (team_YYYYMMDDHHMMSS_xxxxxxxxxxxxxxxx)
|
|
44
|
+
s = s.replace(/team_\d{8,}_[a-z0-9]{12,}/g, "<run>");
|
|
45
|
+
// Strip task ids (01_explore, adaptive-03-executor, etc.)
|
|
46
|
+
s = s.replace(/\b(adaptive-)?\d{2,}[a-z0-9_-]+/g, "<task>");
|
|
47
|
+
// Strip absolute paths
|
|
48
|
+
s = s.replace(/\/(?:home|users|tmp|var|opt|root)[^\s'"]*/g, "<path>");
|
|
49
|
+
// Strip numbers (line numbers, counts, pids, ms durations)
|
|
50
|
+
s = s.replace(/\b\d+\b/g, "N");
|
|
51
|
+
// Collapse whitespace
|
|
52
|
+
s = s.replace(/\s+/g, " ").trim();
|
|
53
|
+
return s || "(no error detail)";
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Group failed tasks by error-pattern similarity. Only groups with >=2
|
|
58
|
+
* members are returned (singletons are not "patterns"). Sorted by count desc.
|
|
59
|
+
*
|
|
60
|
+
* @param tasks the run's tasks (any with status 'failed'/'cancelled' are
|
|
61
|
+
* considered failures for aggregation purposes).
|
|
62
|
+
*/
|
|
63
|
+
export function aggregateFailurePatterns(tasks: FailurePatternInput[]): FailurePattern[] {
|
|
64
|
+
const failed = tasks.filter(
|
|
65
|
+
(t) => t.status === "failed" || t.status === "cancelled",
|
|
66
|
+
);
|
|
67
|
+
if (failed.length === 0) return [];
|
|
68
|
+
const buckets = new Map<string, FailurePattern>();
|
|
69
|
+
for (const t of failed) {
|
|
70
|
+
const signature = normalizeErrorSignature(t.error);
|
|
71
|
+
const existing = buckets.get(signature);
|
|
72
|
+
if (existing) {
|
|
73
|
+
existing.taskIds.push(t.id);
|
|
74
|
+
existing.count += 1;
|
|
75
|
+
// Keep the shortest non-empty variant as representative (most readable).
|
|
76
|
+
if (t.error && (!existing.representative || t.error.length < existing.representative.length)) {
|
|
77
|
+
existing.representative = t.error;
|
|
78
|
+
}
|
|
79
|
+
} else {
|
|
80
|
+
buckets.set(signature, {
|
|
81
|
+
signature,
|
|
82
|
+
representative: t.error ?? "(no error detail)",
|
|
83
|
+
taskIds: [t.id],
|
|
84
|
+
count: 1,
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
// Only patterns with >=2 members (repeated root causes).
|
|
89
|
+
return [...buckets.values()]
|
|
90
|
+
.filter((b) => b.count >= 2)
|
|
91
|
+
.sort((a, b) => b.count - a.count);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Render failure patterns as human-readable lines for the `summary` action.
|
|
96
|
+
* Returns [] when there are no repeated patterns (so the caller can omit the
|
|
97
|
+
* section entirely).
|
|
98
|
+
*
|
|
99
|
+
* Example output:
|
|
100
|
+
* Common failure patterns (3 of 5 failures share 2 root causes):
|
|
101
|
+
* - [×3] model routing fallback failed: all 2 candidates exhausted
|
|
102
|
+
* tasks: 02_exec, 03_exec, 04_exec
|
|
103
|
+
* - [×2] EPERM: operation not permitted, rename
|
|
104
|
+
* tasks: 05_exec, 06_exec
|
|
105
|
+
*/
|
|
106
|
+
export function formatFailurePatterns(tasks: FailurePatternInput[]): string[] {
|
|
107
|
+
const patterns = aggregateFailurePatterns(tasks);
|
|
108
|
+
if (patterns.length === 0) return [];
|
|
109
|
+
const failedCount = tasks.filter(
|
|
110
|
+
(t) => t.status === "failed" || t.status === "cancelled",
|
|
111
|
+
).length;
|
|
112
|
+
const groupedCount = patterns.reduce((sum, p) => sum + p.count, 0);
|
|
113
|
+
const lines = [
|
|
114
|
+
`Common failure patterns (${groupedCount} of ${failedCount} failures share ${patterns.length} root cause${patterns.length === 1 ? "" : "s"}):`,
|
|
115
|
+
];
|
|
116
|
+
for (const p of patterns) {
|
|
117
|
+
const rep = p.representative.length > 100 ? `${p.representative.slice(0, 99)}…` : p.representative;
|
|
118
|
+
lines.push(`- [×${p.count}] ${rep}`);
|
|
119
|
+
const shown = p.taskIds.slice(0, 6);
|
|
120
|
+
const more = p.taskIds.length > 6 ? `, +${p.taskIds.length - 6} more` : "";
|
|
121
|
+
lines.push(` tasks: ${shown.join(", ")}${more}`);
|
|
122
|
+
}
|
|
123
|
+
return lines;
|
|
124
|
+
}
|
|
@@ -5,13 +5,15 @@ import { aggregateUsage, formatUsage, formatCostReport } from "../../state/usage
|
|
|
5
5
|
import type { PiTeamsToolResult } from "../tool-result.ts";
|
|
6
6
|
import { locateRunCwd } from "../team-tool.ts";
|
|
7
7
|
import { result, type TeamContext } from "./context.ts";
|
|
8
|
+
import { RUN_NOT_FOUND_HINT } from "./run-not-found.ts";
|
|
9
|
+
import { formatFailurePatterns } from "./failure-patterns.ts";
|
|
8
10
|
|
|
9
11
|
export function handleEvents(params: TeamToolParamsValue, ctx: TeamContext): PiTeamsToolResult {
|
|
10
12
|
if (!params.runId) return result("Events requires runId.", { action: "events", status: "error" }, true);
|
|
11
13
|
const runCwd = locateRunCwd(params.runId, ctx.cwd);
|
|
12
|
-
if (!runCwd) return result(`Run '${params.runId}' not found
|
|
14
|
+
if (!runCwd) return result(`Run '${params.runId}' not found.${RUN_NOT_FOUND_HINT}`, { action: "events", status: "error" }, true);
|
|
13
15
|
const loaded = loadRunManifestById(runCwd, params.runId); // NOTE: no withRunLock - best-effort only; concurrent writes may cause inconsistency
|
|
14
|
-
if (!loaded) return result(`Run '${params.runId}' not found
|
|
16
|
+
if (!loaded) return result(`Run '${params.runId}' not found.${RUN_NOT_FOUND_HINT}`, { action: "events", status: "error" }, true);
|
|
15
17
|
const events = readEvents(loaded.manifest.eventsPath);
|
|
16
18
|
const lines = [`Events for ${loaded.manifest.runId}:`, ...(events.length ? events.map((event) => `${event.time} ${event.type}${event.taskId ? ` ${event.taskId}` : ""}${event.message ? `: ${event.message}` : ""}${event.data ? ` ${JSON.stringify(event.data)}` : ""}`) : ["(none)"])];
|
|
17
19
|
return result(lines.join("\n"), { action: "events", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
|
|
@@ -20,9 +22,9 @@ export function handleEvents(params: TeamToolParamsValue, ctx: TeamContext): PiT
|
|
|
20
22
|
export function handleArtifacts(params: TeamToolParamsValue, ctx: TeamContext): PiTeamsToolResult {
|
|
21
23
|
if (!params.runId) return result("Artifacts requires runId.", { action: "artifacts", status: "error" }, true);
|
|
22
24
|
const runCwd = locateRunCwd(params.runId, ctx.cwd);
|
|
23
|
-
if (!runCwd) return result(`Run '${params.runId}' not found
|
|
25
|
+
if (!runCwd) return result(`Run '${params.runId}' not found.${RUN_NOT_FOUND_HINT}`, { action: "artifacts", status: "error" }, true);
|
|
24
26
|
const loaded = loadRunManifestById(runCwd, params.runId); // NOTE: no withRunLock - best-effort only; concurrent writes may cause inconsistency
|
|
25
|
-
if (!loaded) return result(`Run '${params.runId}' not found
|
|
27
|
+
if (!loaded) return result(`Run '${params.runId}' not found.${RUN_NOT_FOUND_HINT}`, { action: "artifacts", status: "error" }, true);
|
|
26
28
|
const lines = [`Artifacts for ${loaded.manifest.runId}:`, ...(loaded.manifest.artifacts.length ? loaded.manifest.artifacts.map((artifact) => `- ${artifact.kind}: ${artifact.path}${artifact.sizeBytes !== undefined ? ` (${artifact.sizeBytes} bytes)` : ""}${artifact.contentHash ? ` sha256=${artifact.contentHash.slice(0, 12)}` : ""}`) : ["- (none)"])];
|
|
27
29
|
return result(lines.join("\n"), { action: "artifacts", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
|
|
28
30
|
}
|
|
@@ -30,10 +32,11 @@ export function handleArtifacts(params: TeamToolParamsValue, ctx: TeamContext):
|
|
|
30
32
|
export function handleSummary(params: TeamToolParamsValue, ctx: TeamContext): PiTeamsToolResult {
|
|
31
33
|
if (!params.runId) return result("Summary requires runId.", { action: "summary", status: "error" }, true);
|
|
32
34
|
const runCwd = locateRunCwd(params.runId, ctx.cwd);
|
|
33
|
-
if (!runCwd) return result(`Run '${params.runId}' not found
|
|
35
|
+
if (!runCwd) return result(`Run '${params.runId}' not found.${RUN_NOT_FOUND_HINT}`, { action: "summary", status: "error" }, true);
|
|
34
36
|
const loaded = loadRunManifestById(runCwd, params.runId); // NOTE: no withRunLock - best-effort only; concurrent writes may cause inconsistency
|
|
35
|
-
if (!loaded) return result(`Run '${params.runId}' not found
|
|
37
|
+
if (!loaded) return result(`Run '${params.runId}' not found.${RUN_NOT_FOUND_HINT}`, { action: "summary", status: "error" }, true);
|
|
36
38
|
const usage = aggregateUsage(loaded.tasks);
|
|
39
|
+
const failurePatternLines = formatFailurePatterns(loaded.tasks);
|
|
37
40
|
const lines = [
|
|
38
41
|
`Summary for ${loaded.manifest.runId}`,
|
|
39
42
|
`Status: ${loaded.manifest.status}`,
|
|
@@ -43,6 +46,7 @@ export function handleSummary(params: TeamToolParamsValue, ctx: TeamContext): Pi
|
|
|
43
46
|
`Usage: ${formatUsage(usage)}`,
|
|
44
47
|
"",
|
|
45
48
|
formatCostReport(loaded.tasks),
|
|
49
|
+
...(failurePatternLines.length > 0 ? ["", ...failurePatternLines] : []),
|
|
46
50
|
"",
|
|
47
51
|
"Tasks:",
|
|
48
52
|
...loaded.tasks.map((task) => `- ${task.id}: ${task.status} (${task.role} -> ${task.agent})${task.error ? ` - ${task.error}` : ""}`),
|
|
@@ -9,6 +9,7 @@ import { importRunBundle } from "../run-import.ts";
|
|
|
9
9
|
import { pruneFinishedRuns } from "../run-maintenance.ts";
|
|
10
10
|
import type { PiTeamsToolResult } from "../tool-result.ts";
|
|
11
11
|
import { configRecord, result, type TeamContext } from "./context.ts";
|
|
12
|
+
import { RUN_NOT_FOUND_HINT } from "./run-not-found.ts";
|
|
12
13
|
import { enforceDestructiveIntent, intentFromConfig } from "./intent-policy.ts";
|
|
13
14
|
import { executeHook, appendHookEvent } from "../../hooks/registry.ts";
|
|
14
15
|
import { resolveRealContainedPath } from "../../utils/safe-paths.ts";
|
|
@@ -18,7 +19,7 @@ import * as path from "node:path";
|
|
|
18
19
|
export function handleWorktrees(params: TeamToolParamsValue, ctx: TeamContext): PiTeamsToolResult {
|
|
19
20
|
if (!params.runId) return result("Worktrees requires runId.", { action: "worktrees", status: "error" }, true);
|
|
20
21
|
const loaded = loadRunManifestById(ctx.cwd, params.runId); // NOTE: no withRunLock - best-effort only; concurrent writes may cause inconsistency
|
|
21
|
-
if (!loaded) return result(`Run '${params.runId}' not found
|
|
22
|
+
if (!loaded) return result(`Run '${params.runId}' not found.${RUN_NOT_FOUND_HINT}`, { action: "worktrees", status: "error" }, true);
|
|
22
23
|
const withWorktrees = loaded.tasks.filter((task) => task.worktree);
|
|
23
24
|
const lines = [`Worktrees for ${loaded.manifest.runId}:`, ...(withWorktrees.length ? withWorktrees.map((task) => `- ${task.id}: ${task.worktree!.path} branch=${task.worktree!.branch} reused=${task.worktree!.reused ? "true" : "false"}`) : ["- (none)"])];
|
|
24
25
|
return result(lines.join("\n"), { action: "worktrees", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
|
|
@@ -47,7 +48,7 @@ export function handleImport(params: TeamToolParamsValue, ctx: TeamContext): PiT
|
|
|
47
48
|
export async function handleExport(params: TeamToolParamsValue, ctx: TeamContext): Promise<PiTeamsToolResult> {
|
|
48
49
|
if (!params.runId) return result("Export requires runId.", { action: "export", status: "error" }, true);
|
|
49
50
|
const loaded = loadRunManifestById(ctx.cwd, params.runId); // NOTE: no withRunLock - best-effort only; concurrent writes may cause inconsistency
|
|
50
|
-
if (!loaded) return result(`Run '${params.runId}' not found
|
|
51
|
+
if (!loaded) return result(`Run '${params.runId}' not found.${RUN_NOT_FOUND_HINT}`, { action: "export", status: "error" }, true);
|
|
51
52
|
|
|
52
53
|
// SECURITY: Ownership check — only the owner session may export a run.
|
|
53
54
|
// Foreign-run export requires confirm: true (explicit user intent).
|
|
@@ -96,7 +97,7 @@ export async function handleForget(params: TeamToolParamsValue, ctx: TeamContext
|
|
|
96
97
|
if (!params.runId) return result("Forget requires runId.", { action: "forget", status: "error" }, true);
|
|
97
98
|
if (!params.confirm) return result("forget requires confirm: true.", { action: "forget", status: "error" }, true);
|
|
98
99
|
const loaded = loadRunManifestById(ctx.cwd, params.runId); // NOTE: no withRunLock - best-effort only; concurrent writes may cause inconsistency
|
|
99
|
-
if (!loaded) return result(`Run '${params.runId}' not found
|
|
100
|
+
if (!loaded) return result(`Run '${params.runId}' not found.${RUN_NOT_FOUND_HINT}`, { action: "forget", status: "error" }, true);
|
|
100
101
|
|
|
101
102
|
// Ownership check — prevent cross-session deletion unless force is set
|
|
102
103
|
const foreignRun = typeof loaded.manifest.ownerSessionId === "string" && loaded.manifest.ownerSessionId !== ctx.sessionId;
|
|
@@ -126,7 +127,7 @@ export async function handleCleanup(params: TeamToolParamsValue, ctx: TeamContex
|
|
|
126
127
|
if (intentError) return intentError;
|
|
127
128
|
if (!params.runId) return result("Cleanup requires runId.", { action: "cleanup", status: "error" }, true);
|
|
128
129
|
const loaded = loadRunManifestById(ctx.cwd, params.runId); // NOTE: no withRunLock - best-effort only; concurrent writes may cause inconsistency
|
|
129
|
-
if (!loaded) return result(`Run '${params.runId}' not found
|
|
130
|
+
if (!loaded) return result(`Run '${params.runId}' not found.${RUN_NOT_FOUND_HINT}`, { action: "cleanup", status: "error" }, true);
|
|
130
131
|
|
|
131
132
|
// Ownership check — prevent cross-session worktree cleanup unless force is set
|
|
132
133
|
const foreignRun = typeof loaded.manifest.ownerSessionId === "string" && loaded.manifest.ownerSessionId !== ctx.sessionId;
|
|
@@ -8,6 +8,7 @@ import { logInternalError } from "../../utils/internal-error.ts";
|
|
|
8
8
|
import type { PiTeamsToolResult } from "../tool-result.ts";
|
|
9
9
|
import { locateRunCwd } from "../team-tool.ts";
|
|
10
10
|
import { result, type TeamContext } from "./context.ts";
|
|
11
|
+
import { RUN_NOT_FOUND_HINT } from "./run-not-found.ts";
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* Handle `respond` action: send a message to a waiting (interactive) task.
|
|
@@ -19,13 +20,13 @@ export function handleRespond(params: TeamToolParamsValue, ctx: TeamContext): Pi
|
|
|
19
20
|
if (!params.message && !params.taskId) return result("Respond requires taskId and/or message.", { action: "respond", status: "error" }, true);
|
|
20
21
|
|
|
21
22
|
const runCwd = locateRunCwd(params.runId, ctx.cwd);
|
|
22
|
-
if (!runCwd) return result(`Run '${params.runId}' not found
|
|
23
|
+
if (!runCwd) return result(`Run '${params.runId}' not found.${RUN_NOT_FOUND_HINT}`, { action: "respond", status: "error" }, true);
|
|
23
24
|
const loaded = loadRunManifestById(runCwd, params.runId); // NOTE: no withRunLock - best-effort only; concurrent writes may cause inconsistency
|
|
24
|
-
if (!loaded) return result(`Run '${params.runId}' not found
|
|
25
|
+
if (!loaded) return result(`Run '${params.runId}' not found.${RUN_NOT_FOUND_HINT}`, { action: "respond", status: "error" }, true);
|
|
25
26
|
|
|
26
27
|
return withRunLockSync(loaded.manifest, () => {
|
|
27
28
|
const fresh = loadRunManifestById(loaded.manifest.cwd, params.runId!); // NOTE: inside withRunLockSync - consistent read
|
|
28
|
-
if (!fresh) return result(`Run '${params.runId}' not found
|
|
29
|
+
if (!fresh) return result(`Run '${params.runId}' not found.${RUN_NOT_FOUND_HINT}`, { action: "respond", status: "error" }, true);
|
|
29
30
|
const foreignRun = typeof fresh.manifest.ownerSessionId === "string" && fresh.manifest.ownerSessionId !== ctx.sessionId;
|
|
30
31
|
if (foreignRun && !params.force) return result(`Run ${fresh.manifest.runId} belongs to another session. Use force: true to override.`, { action: "respond", status: "error", runId: fresh.manifest.runId }, true);
|
|
31
32
|
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* run-not-found.ts — Centralized "Run not found" error helper (DX: F2).
|
|
3
|
+
*
|
|
4
|
+
* Round 16 DX audit found that a stale/typo'd runId hits a blank
|
|
5
|
+
* "Run '<id>' not found." wall in 8+ handlers (status, resume, steer, export,
|
|
6
|
+
* forget, cleanup, invalidate, worktrees, events, artifacts). The run IDs are
|
|
7
|
+
* long (`team_20260615173318_b9c8fe49a74e0760`), so typos/truncation are
|
|
8
|
+
* near-certain for new users — yet `team list` (which shows recent runs) is
|
|
9
|
+
* never suggested.
|
|
10
|
+
*
|
|
11
|
+
* This module centralizes the message + recovery hint so every handler stays
|
|
12
|
+
* consistent and the hint never drifts.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { result, type TeamContext } from "./context.ts";
|
|
16
|
+
import type { TeamToolDetails } from "../team-tool-types.ts";
|
|
17
|
+
|
|
18
|
+
/** Recovery hint appended to every "Run not found" message. */
|
|
19
|
+
export const RUN_NOT_FOUND_HINT =
|
|
20
|
+
"\n\nTip: run action='list' to see recent runs and their IDs.";
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Build the standard "Run not found" error result with a recovery hint.
|
|
24
|
+
*
|
|
25
|
+
* @param runId the (missing/typo'd) run id the caller passed
|
|
26
|
+
* @param action the action that was attempted (for the details.action field)
|
|
27
|
+
*/
|
|
28
|
+
export function runNotFound(runId: string, action: string): ReturnType<typeof result> {
|
|
29
|
+
return result(
|
|
30
|
+
`Run '${runId}' not found.${RUN_NOT_FOUND_HINT}`,
|
|
31
|
+
{ action, status: "error" } satisfies TeamToolDetails,
|
|
32
|
+
true,
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Helper: resolve a runId to its cwd, returning a runNotFound() result when
|
|
38
|
+
* missing. Reduces the boilerplate `locateRunCwd → if (!runCwd) return ...`
|
|
39
|
+
* duplicated across handlers.
|
|
40
|
+
*/
|
|
41
|
+
export function resolveRunOrNotFound(
|
|
42
|
+
runId: string,
|
|
43
|
+
action: string,
|
|
44
|
+
cwd: string,
|
|
45
|
+
locate: (runId: string, cwd: string) => string | undefined,
|
|
46
|
+
): { kind: "found"; runCwd: string } | { kind: "notfound"; result: ReturnType<typeof result> } {
|
|
47
|
+
const runCwd = locate(runId, cwd);
|
|
48
|
+
if (!runCwd) return { kind: "notfound", result: runNotFound(runId, action) };
|
|
49
|
+
return { kind: "found", runCwd };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// Re-export TeamContext so callers importing this helper don't need a second
|
|
53
|
+
// import line — keeps the diff in each handler to a single import swap.
|
|
54
|
+
export type { TeamContext };
|
|
@@ -184,13 +184,17 @@ export async function handleRun(params: TeamToolParamsValue, ctx: TeamContext):
|
|
|
184
184
|
// connecting PipelineRunner to the actual team execution system
|
|
185
185
|
const stageInfo = pipelineWorkflow.stages.map((s) => `- ${s.name} (${s.team})`).join("\n");
|
|
186
186
|
return result([
|
|
187
|
-
`Pipeline workflow
|
|
187
|
+
`Pipeline workflow '${workflow.name}' is not yet wired into the team execution system.`,
|
|
188
188
|
`Goal: ${goal}`,
|
|
189
|
-
`
|
|
189
|
+
`Defined stages (${pipelineWorkflow.stages.length}):`,
|
|
190
190
|
stageInfo,
|
|
191
191
|
"",
|
|
192
|
-
"
|
|
193
|
-
"
|
|
192
|
+
"To actually run work right now, use a supported workflow instead:",
|
|
193
|
+
" - action='run' workflow='default' (explore → plan → execute → verify)",
|
|
194
|
+
" - action='run' workflow='implementation' (adaptive, parallel specialists)",
|
|
195
|
+
" - action='run' workflow='research' (explore → analyze → write)",
|
|
196
|
+
"",
|
|
197
|
+
"Run action='list' resource='workflow' to see all available workflows.",
|
|
194
198
|
].join("\n"), { action: "run", status: "ok" }, false);
|
|
195
199
|
}
|
|
196
200
|
|
|
@@ -219,6 +223,24 @@ export async function handleRun(params: TeamToolParamsValue, ctx: TeamContext):
|
|
|
219
223
|
registerActiveRun(updatedManifest);
|
|
220
224
|
|
|
221
225
|
const loadedConfig = loadConfig(resolvedCtx.cwd);
|
|
226
|
+
// DX (Round 16 F4): surface config errors/warnings instead of silently
|
|
227
|
+
// proceeding with defaults. Non-blocking: emit a config.warning event so
|
|
228
|
+
// it shows in the run timeline and status, and log it. A malformed config
|
|
229
|
+
// (bad JSON / wrong types) should not be a silent no-op — doctor/config
|
|
230
|
+
// actions already surface these; run should too.
|
|
231
|
+
const configIssues = [
|
|
232
|
+
...(loadedConfig.error ? [`Config error: ${loadedConfig.error}`] : []),
|
|
233
|
+
...(loadedConfig.warnings ?? []),
|
|
234
|
+
];
|
|
235
|
+
if (configIssues.length > 0) {
|
|
236
|
+
void appendEventAsync(updatedManifest.eventsPath, {
|
|
237
|
+
type: "config.warning",
|
|
238
|
+
runId: updatedManifest.runId,
|
|
239
|
+
message: `Loaded config from ${loadedConfig.path || "(defaults)"} with ${configIssues.length} issue(s): ${configIssues.join("; ")}`,
|
|
240
|
+
data: { error: loadedConfig.error, warnings: loadedConfig.warnings, path: loadedConfig.path },
|
|
241
|
+
}).catch((error) => logInternalError("team-tool.run.configWarning", error, `runId=${updatedManifest.runId}`));
|
|
242
|
+
logInternalError("team-tool.run.configWarning", new Error(`config issues: ${configIssues.join("; ")}`), `runId=${updatedManifest.runId} path=${loadedConfig.path ?? "(defaults)"}`);
|
|
243
|
+
}
|
|
222
244
|
const executedConfig = effectiveRunConfig(loadedConfig.config, params.config);
|
|
223
245
|
const runtime = await resolveCrewRuntime(executedConfig);
|
|
224
246
|
const runtimeResolution = runtimeResolutionState(runtime);
|
|
@@ -3,24 +3,31 @@ import type { TeamToolParamsValue } from "../../schema/team-tool-schema.ts";
|
|
|
3
3
|
import { appendEvent, readEvents } from "../../state/event-log.ts";
|
|
4
4
|
import { readDeliveryState, readMailbox } from "../../state/mailbox.ts";
|
|
5
5
|
import { loadRunManifestById, updateRunStatus, saveRunTasks } from "../../state/state-store.ts";
|
|
6
|
-
import { aggregateUsage, formatUsage } from "../../state/usage.ts";
|
|
6
|
+
import { aggregateUsage, formatUsage, formatCost } from "../../state/usage.ts";
|
|
7
7
|
import { applyAttentionState, formatActivityAge, resolveCrewControlConfig } from "../../runtime/agent-control.ts";
|
|
8
8
|
import { readCrewAgents } from "../../runtime/crew-agent-records.ts";
|
|
9
9
|
import { checkProcessLiveness, isActiveRunStatus } from "../../runtime/process-status.ts";
|
|
10
10
|
import { formatTaskGraphLines, waitingReason } from "../../runtime/task-display.ts";
|
|
11
|
+
import { computePhaseProgress } from "../../runtime/phase-progress.ts";
|
|
12
|
+
import { formatDuration } from "../../ui/tool-render.ts";
|
|
11
13
|
import { verifyTaskCompletion } from "../../runtime/completion-guard.ts";
|
|
12
14
|
import { evaluateRunEffectiveness } from "../../runtime/effectiveness.ts";
|
|
13
15
|
import type { PiTeamsToolResult } from "../tool-result.ts";
|
|
14
16
|
import { locateRunCwd } from "../team-tool.ts";
|
|
15
17
|
import { result, type TeamContext } from "./context.ts";
|
|
18
|
+
import { RUN_NOT_FOUND_HINT } from "./run-not-found.ts";
|
|
16
19
|
|
|
17
20
|
export function handleStatus(params: TeamToolParamsValue, ctx: TeamContext): PiTeamsToolResult {
|
|
18
21
|
if (!params.runId) return result("Status requires runId.", { action: "status", status: "error" }, true);
|
|
19
22
|
const runCwd = locateRunCwd(params.runId, ctx.cwd);
|
|
20
|
-
if (!runCwd) return result(`Run '${params.runId}' not found
|
|
23
|
+
if (!runCwd) return result(`Run '${params.runId}' not found.${RUN_NOT_FOUND_HINT}`, { action: "status", status: "error" }, true);
|
|
21
24
|
const loaded = loadRunManifestById(runCwd, params.runId); // NOTE: no withRunLock - best-effort only; concurrent writes may cause inconsistency
|
|
22
|
-
if (!loaded) return result(`Run '${params.runId}' not found
|
|
25
|
+
if (!loaded) return result(`Run '${params.runId}' not found.${RUN_NOT_FOUND_HINT}`, { action: "status", status: "error" }, true);
|
|
23
26
|
let { manifest, tasks } = loaded;
|
|
27
|
+
// DX (Round 16 F3): compact status mode. Default = full (backward compatible).
|
|
28
|
+
// details=false gives a tight summary (status, goal, counts, failed/attention
|
|
29
|
+
// errors) for quick checks without 40 lines of dense key=value noise.
|
|
30
|
+
const fullDetails = params.details !== false;
|
|
24
31
|
let asyncLivenessLine: string | undefined;
|
|
25
32
|
if (manifest.async) {
|
|
26
33
|
const asyncState = manifest.async;
|
|
@@ -35,6 +42,7 @@ export function handleStatus(params: TeamToolParamsValue, ctx: TeamContext): PiT
|
|
|
35
42
|
}
|
|
36
43
|
const counts = new Map<string, number>();
|
|
37
44
|
for (const task of tasks) counts.set(task.status, (counts.get(task.status) ?? 0) + 1);
|
|
45
|
+
const phaseProgress = computePhaseProgress(tasks);
|
|
38
46
|
const allEvents = readEvents(manifest.eventsPath);
|
|
39
47
|
const events = allEvents.slice(-8);
|
|
40
48
|
const attentionByTask = new Map(allEvents.filter((event) => event.type === "task.attention" && event.taskId).map((event) => [event.taskId!, event]));
|
|
@@ -62,12 +70,13 @@ export function handleStatus(params: TeamToolParamsValue, ctx: TeamContext): PiT
|
|
|
62
70
|
const activeAgents = crewAgents.filter((agent) => agent.status === "running");
|
|
63
71
|
const completedAgents = crewAgents.filter((agent) => agent.status !== "running");
|
|
64
72
|
const waitingTasks = tasks.filter((task) => task.status === "queued" || task.status === "waiting");
|
|
65
|
-
const agentLine = (agent: typeof crewAgents[number]): string => `- ${agent.id} [${agent.status}] ${agent.role} -> ${agent.agent} runtime=${agent.runtime}${agent.model ? ` model=${agent.model}` : ""}${agent.usage ? ` usage=${formatUsage(agent.usage)}` : ""}${agent.progress?.activityState ? ` activityState=${agent.progress.activityState}` : ""}${formatActivityAge(agent) ? ` activity=${formatActivityAge(agent)}` : ""}${agent.progress?.currentTool ? ` tool=${agent.progress.currentTool}` : ""}${agent.toolUses ? ` tools=${agent.toolUses}` : ""}${!agent.usage && agent.progress?.tokens ? ` tokens=${agent.progress.tokens}` : ""}${agent.progress?.turns ? ` turns=${agent.progress.turns}` : ""}${agent.jsonEvents !== undefined ? ` jsonEvents=${agent.jsonEvents}` : ""}${agent.outputPath ? ` output=${agent.outputPath}` : ""}${agent.transcriptPath ? ` transcript=${agent.transcriptPath}` : ""}${agent.statusPath ? ` status=${agent.statusPath}` : ""}${agent.error ? ` error=${agent.error}` : ""}`;
|
|
73
|
+
const agentLine = (agent: typeof crewAgents[number]): string => `- ${agent.id} [${agent.status}] ${agent.role} -> ${agent.agent} runtime=${agent.runtime}${agent.model ? ` model=${agent.model}` : ""}${agent.usage ? ` usage=${formatUsage(agent.usage)}` : ""}${agent.usage?.cost ? ` cost=${formatCost(agent.usage.cost)}` : ""}${agent.progress?.activityState ? ` activityState=${agent.progress.activityState}` : ""}${formatActivityAge(agent) ? ` activity=${formatActivityAge(agent)}` : ""}${agent.progress?.currentTool ? ` tool=${agent.progress.currentTool}` : ""}${agent.toolUses ? ` tools=${agent.toolUses}` : ""}${!agent.usage && agent.progress?.tokens ? ` tokens=${agent.progress.tokens}` : ""}${agent.progress?.turns ? ` turns=${agent.progress.turns}` : ""}${agent.jsonEvents !== undefined ? ` jsonEvents=${agent.jsonEvents}` : ""}${agent.outputPath ? ` output=${agent.outputPath}` : ""}${agent.transcriptPath ? ` transcript=${agent.transcriptPath}` : ""}${agent.statusPath ? ` status=${agent.statusPath}` : ""}${agent.error ? ` error=${agent.error}` : ""}`;
|
|
66
74
|
const lines = [
|
|
67
75
|
`Run: ${manifest.runId}`,
|
|
68
76
|
`Team: ${manifest.team}`,
|
|
69
77
|
`Workflow: ${manifest.workflow ?? "(none)"}`,
|
|
70
78
|
`Status: ${manifest.status}`,
|
|
79
|
+
`Progress: ${phaseProgress.overallPercentage}% (~${formatDuration(phaseProgress.estimatedRemainingMs)} remaining)`,
|
|
71
80
|
`Workspace mode: ${manifest.workspaceMode}`,
|
|
72
81
|
...(manifest.runtimeResolution ? [`Runtime: ${manifest.runtimeResolution.kind}`, `Runtime safety: ${manifest.runtimeResolution.safety}`, `Runtime requested: ${manifest.runtimeResolution.requestedMode}${manifest.runtimeResolution.reason ? ` (${manifest.runtimeResolution.reason})` : ""}`] : []),
|
|
73
82
|
`Goal: ${manifest.goal}`,
|
|
@@ -109,5 +118,50 @@ export function handleStatus(params: TeamToolParamsValue, ctx: TeamContext): PiT
|
|
|
109
118
|
"Recent events:",
|
|
110
119
|
...(events.length ? events.map((event) => `- ${event.time} ${event.type}${event.taskId ? ` ${event.taskId}` : ""}${event.message ? `: ${event.message}` : ""}`) : ["- (none)"]),
|
|
111
120
|
];
|
|
121
|
+
if (!fullDetails) {
|
|
122
|
+
return result(
|
|
123
|
+
buildCompactStatus(manifest, tasks, counts, asyncLivenessLine, phaseProgress).join("\n"),
|
|
124
|
+
{ action: "status", status: "ok", runId: manifest.runId, artifactsRoot: manifest.artifactsRoot, intent: `status ${manifest.runId}: ${manifest.status} (compact)` },
|
|
125
|
+
);
|
|
126
|
+
}
|
|
112
127
|
return result(lines.join("\n"), { action: "status", status: "ok", runId: manifest.runId, artifactsRoot: manifest.artifactsRoot, intent: `status ${manifest.runId}: ${manifest.status}` });
|
|
113
128
|
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Compact status builder (DX: Round 16 F3). A tight summary for quick checks:
|
|
132
|
+
* identity, status, goal, task counts, and ONLY failed / attention task
|
|
133
|
+
* errors — not the 40-line dense dump. Invoked when params.details === false.
|
|
134
|
+
*
|
|
135
|
+
* Exported for unit testing.
|
|
136
|
+
*/
|
|
137
|
+
export function buildCompactStatus(
|
|
138
|
+
manifest: { runId: string; team: string; workflow?: string; status: string; goal: string; workspaceMode?: string },
|
|
139
|
+
tasks: Array<{ id: string; status: string; role: string; agent: string; error?: string }>,
|
|
140
|
+
counts: Map<string, number>,
|
|
141
|
+
asyncLivenessLine?: string,
|
|
142
|
+
progress?: { overallPercentage: number; estimatedRemainingMs: number },
|
|
143
|
+
): string[] {
|
|
144
|
+
const failedOrAttention = tasks.filter(
|
|
145
|
+
(t) =>
|
|
146
|
+
t.status === "failed" ||
|
|
147
|
+
t.status === "needs_attention" ||
|
|
148
|
+
t.status === "cancelled",
|
|
149
|
+
);
|
|
150
|
+
const lines = [
|
|
151
|
+
`Run: ${manifest.runId}`,
|
|
152
|
+
`Team: ${manifest.team}${manifest.workflow ? ` (${manifest.workflow})` : ""}`,
|
|
153
|
+
`Status: ${manifest.status}`,
|
|
154
|
+
...(progress ? [`Progress: ${progress.overallPercentage}% (~${formatDuration(progress.estimatedRemainingMs)} remaining)`] : []),
|
|
155
|
+
`Goal: ${manifest.goal}`,
|
|
156
|
+
...(asyncLivenessLine ? [asyncLivenessLine] : []),
|
|
157
|
+
`Tasks: ${[...counts.entries()].map(([s, c]) => `${s}=${c}`).join(", ") || "none"}`,
|
|
158
|
+
];
|
|
159
|
+
if (failedOrAttention.length > 0) {
|
|
160
|
+
lines.push("Issues:");
|
|
161
|
+
for (const t of failedOrAttention) {
|
|
162
|
+
lines.push(`- ${t.id} [${t.status}] ${t.role}: ${t.error ?? "(no error detail)"}`);
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
lines.push("Tip: pass details=true for full output (task graph, agents, effectiveness, events).");
|
|
166
|
+
return lines;
|
|
167
|
+
}
|
|
@@ -156,6 +156,8 @@ import { handleParallel } from "./team-tool/parallel-dispatch.ts";
|
|
|
156
156
|
import { handlePlan } from "./team-tool/plan.ts";
|
|
157
157
|
import { handleRespond } from "./team-tool/respond.ts";
|
|
158
158
|
import { handleStatus } from "./team-tool/status.ts";
|
|
159
|
+
import { RUN_NOT_FOUND_HINT } from "./team-tool/run-not-found.ts";
|
|
160
|
+
import { formatActionSuggestion } from "./action-suggestions.ts";
|
|
159
161
|
|
|
160
162
|
export { handleApi } from "./team-tool/api.ts";
|
|
161
163
|
export { handleRetry } from "./team-tool/cancel.ts";
|
|
@@ -459,14 +461,14 @@ export async function handleResume(
|
|
|
459
461
|
const runCwd = locateRunCwd(params.runId, ctx.cwd);
|
|
460
462
|
if (!runCwd)
|
|
461
463
|
return result(
|
|
462
|
-
`Run '${params.runId}' not found
|
|
464
|
+
`Run '${params.runId}' not found.${RUN_NOT_FOUND_HINT}`,
|
|
463
465
|
{ action: "resume", status: "error" },
|
|
464
466
|
true,
|
|
465
467
|
);
|
|
466
468
|
const loaded = loadRunManifestById(runCwd, params.runId); // NOTE: no withRunLock - best-effort only; concurrent writes may cause inconsistency
|
|
467
469
|
if (!loaded)
|
|
468
470
|
return result(
|
|
469
|
-
`Run '${params.runId}' not found
|
|
471
|
+
`Run '${params.runId}' not found.${RUN_NOT_FOUND_HINT}`,
|
|
470
472
|
{ action: "resume", status: "error" },
|
|
471
473
|
true,
|
|
472
474
|
);
|
|
@@ -1347,7 +1349,7 @@ export async function handleTeamTool(
|
|
|
1347
1349
|
}
|
|
1348
1350
|
default:
|
|
1349
1351
|
return result(
|
|
1350
|
-
`Unknown action: ${action}`,
|
|
1352
|
+
`Unknown action: ${action}${formatActionSuggestion(String(action))}`,
|
|
1351
1353
|
{ action: "unknown", status: "error" },
|
|
1352
1354
|
true,
|
|
1353
1355
|
);
|
|
@@ -231,6 +231,13 @@ export async function spawnBackgroundTeamRun(manifest: TeamRunManifest): Promise
|
|
|
231
231
|
windowsHide: true,
|
|
232
232
|
} as unknown as Parameters<typeof spawn>[2];
|
|
233
233
|
const child = spawn(process.execPath, command.args, spawnOpts);
|
|
234
|
+
// Round 27 (BUG 3): the piped stdout/stderr are NEVER read or destroyed →
|
|
235
|
+
// 2 FDs leak per background spawn, and if the child writes >64KB (pipe
|
|
236
|
+
// buffer) it blocks forever (nobody drains the pipe) → background runner
|
|
237
|
+
// hangs. The background runner redirects its own console to a file, so we
|
|
238
|
+
// don't need this output — destroy the read ends immediately.
|
|
239
|
+
child.stdout?.destroy();
|
|
240
|
+
child.stderr?.destroy();
|
|
234
241
|
child.on("error", (error: Error) => {
|
|
235
242
|
logInternalError("async-runner.spawn", error, `pid=${child.pid ?? "unknown"}`);
|
|
236
243
|
});
|
|
@@ -525,7 +525,13 @@ async function main(): Promise<void> {
|
|
|
525
525
|
const agents = allAgents(discoverAgents(cwd));
|
|
526
526
|
debugLog(`[background-runner] discoverAgents done, ${agents.length} agents`,
|
|
527
527
|
);
|
|
528
|
-
|
|
528
|
+
// Round 27 (BUG 2): openSync returned an fd that was never closed → FD
|
|
529
|
+
// leak per background runner startup. Close it in a finally (matches the
|
|
530
|
+
// canonical pattern in checkpoint.ts:83 and event-log.ts:582).
|
|
531
|
+
try {
|
|
532
|
+
const fd = fs.openSync(manifest.eventsPath, "a");
|
|
533
|
+
try { fs.fsyncSync(fd); } finally { try { fs.closeSync(fd); } catch { /* best-effort */ } }
|
|
534
|
+
} catch { /* best-effort */ } // FORCE flush so we see this before death
|
|
529
535
|
debugLog(`[background-runner] calling directTeamAndWorkflowFromRun`,
|
|
530
536
|
);
|
|
531
537
|
const direct = directTeamAndWorkflowFromRun(manifest, tasks, agents);
|