pi-crew 0.1.37 → 0.1.39
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/AGENTS.md +1 -1
- package/CHANGELOG.md +27 -0
- package/README.md +5 -0
- package/agents/analyst.md +11 -11
- package/agents/critic.md +11 -11
- package/agents/executor.md +11 -11
- package/agents/explorer.md +11 -11
- package/agents/planner.md +11 -11
- package/agents/reviewer.md +11 -11
- package/agents/security-reviewer.md +11 -11
- package/agents/test-engineer.md +11 -11
- package/agents/verifier.md +11 -11
- package/agents/writer.md +11 -11
- package/docs/refactor-tasks-phase3.md +394 -394
- package/docs/refactor-tasks-phase4.md +564 -564
- package/docs/refactor-tasks-phase5.md +402 -402
- package/docs/refactor-tasks-phase6.md +662 -662
- package/docs/research-extension-examples.md +297 -297
- package/docs/research-extension-system.md +324 -324
- package/docs/research-optimization-plan.md +548 -548
- package/docs/research-pi-coding-agent.md +357 -357
- package/docs/research-source-pi-crew-reference.md +174 -174
- package/docs/resource-formats.md +10 -8
- package/docs/runtime-flow.md +148 -148
- package/docs/source-runtime-refactor-map.md +83 -83
- package/docs/usage.md +6 -0
- package/index.ts +6 -6
- package/package.json +3 -3
- package/schema.json +2 -2
- package/src/agents/agent-serializer.ts +34 -34
- package/src/config/config.ts +8 -4
- package/src/extension/cross-extension-rpc.ts +82 -82
- package/src/extension/import-index.ts +18 -2
- package/src/extension/register.ts +11 -1
- package/src/extension/registration/compaction-guard.ts +125 -125
- package/src/extension/registration/subagent-helpers.ts +30 -6
- package/src/extension/registration/subagent-tools.ts +8 -3
- package/src/extension/result-watcher.ts +98 -98
- package/src/extension/run-import.ts +12 -2
- package/src/extension/run-index.ts +12 -2
- package/src/extension/run-maintenance.ts +24 -24
- package/src/extension/team-tool/api.ts +54 -14
- package/src/extension/team-tool/cancel.ts +31 -31
- package/src/extension/team-tool/doctor.ts +179 -179
- package/src/extension/team-tool/inspect.ts +41 -41
- package/src/extension/team-tool/lifecycle-actions.ts +79 -79
- package/src/extension/team-tool/plan.ts +19 -19
- package/src/extension/team-tool/status.ts +73 -73
- package/src/observability/correlation.ts +35 -35
- package/src/observability/event-to-metric.ts +54 -54
- package/src/observability/exporters/adapter.ts +24 -24
- package/src/observability/exporters/otlp-exporter.ts +65 -65
- package/src/observability/exporters/prometheus-exporter.ts +47 -47
- package/src/observability/metric-registry.ts +72 -72
- package/src/observability/metric-retention.ts +46 -46
- package/src/observability/metric-sink.ts +51 -51
- package/src/observability/metrics-primitives.ts +166 -166
- package/src/prompt/prompt-runtime.ts +68 -68
- package/src/runtime/agent-control.ts +64 -64
- package/src/runtime/agent-memory.ts +72 -72
- package/src/runtime/agent-observability.ts +114 -113
- package/src/runtime/async-marker.ts +26 -26
- package/src/runtime/background-runner.ts +53 -53
- package/src/runtime/crash-recovery.ts +56 -56
- package/src/runtime/crew-agent-records.ts +54 -9
- package/src/runtime/crew-agent-runtime.ts +58 -58
- package/src/runtime/deadletter.ts +36 -36
- package/src/runtime/direct-run.ts +35 -35
- package/src/runtime/foreground-control.ts +82 -82
- package/src/runtime/green-contract.ts +46 -46
- package/src/runtime/group-join.ts +88 -88
- package/src/runtime/heartbeat-gradient.ts +28 -28
- package/src/runtime/heartbeat-watcher.ts +80 -80
- package/src/runtime/live-agent-control.ts +87 -78
- package/src/runtime/live-agent-manager.ts +85 -85
- package/src/runtime/live-control-realtime.ts +36 -36
- package/src/runtime/live-session-runtime.ts +299 -299
- package/src/runtime/manifest-cache.ts +248 -212
- package/src/runtime/model-fallback.ts +261 -261
- package/src/runtime/parallel-research.ts +44 -44
- package/src/runtime/parallel-utils.ts +99 -99
- package/src/runtime/pi-json-output.ts +111 -111
- package/src/runtime/policy-engine.ts +78 -78
- package/src/runtime/post-exit-stdio-guard.ts +86 -86
- package/src/runtime/process-status.ts +56 -56
- package/src/runtime/progress-event-coalescer.ts +43 -43
- package/src/runtime/recovery-recipes.ts +74 -74
- package/src/runtime/retry-executor.ts +59 -59
- package/src/runtime/role-permission.ts +39 -39
- package/src/runtime/session-usage.ts +79 -79
- package/src/runtime/sidechain-output.ts +28 -28
- package/src/runtime/subagent-manager.ts +80 -12
- package/src/runtime/task-display.ts +38 -38
- package/src/runtime/task-output-context.ts +127 -106
- package/src/runtime/task-runner/live-executor.ts +98 -98
- package/src/runtime/task-runner/progress.ts +111 -111
- package/src/runtime/task-runner/result-utils.ts +14 -14
- package/src/runtime/task-runner/state-helpers.ts +22 -22
- package/src/runtime/team-runner.ts +1 -1
- package/src/runtime/worker-heartbeat.ts +21 -21
- package/src/runtime/worker-startup.ts +57 -57
- package/src/schema/config-schema.ts +21 -21
- package/src/schema/team-tool-schema.ts +100 -100
- package/src/state/artifact-store.ts +122 -108
- package/src/state/contracts.ts +105 -105
- package/src/state/jsonl-writer.ts +77 -77
- package/src/state/mailbox.ts +67 -22
- package/src/state/state-store.ts +36 -5
- package/src/state/task-claims.ts +42 -42
- package/src/state/usage.ts +29 -29
- package/src/subagents/async-entry.ts +1 -1
- package/src/subagents/index.ts +3 -3
- package/src/subagents/live/control.ts +1 -1
- package/src/subagents/live/manager.ts +1 -1
- package/src/subagents/live/realtime.ts +1 -1
- package/src/subagents/live/session-runtime.ts +1 -1
- package/src/subagents/manager.ts +1 -1
- package/src/subagents/spawn.ts +1 -1
- package/src/teams/discover-teams.ts +27 -5
- package/src/teams/team-serializer.ts +38 -36
- package/src/types/diff.d.ts +18 -18
- package/src/ui/crew-footer.ts +101 -101
- package/src/ui/crew-select-list.ts +111 -111
- package/src/ui/dashboard-panes/metrics-pane.ts +34 -34
- package/src/ui/dynamic-border.ts +25 -25
- package/src/ui/layout-primitives.ts +106 -106
- package/src/ui/loaders.ts +158 -158
- package/src/ui/mascot.ts +441 -441
- package/src/ui/render-diff.ts +119 -119
- package/src/ui/run-dashboard.ts +5 -2
- package/src/ui/run-snapshot-cache.ts +19 -8
- package/src/ui/spinner.ts +17 -17
- package/src/ui/status-colors.ts +54 -54
- package/src/ui/syntax-highlight.ts +116 -116
- package/src/ui/transcript-viewer.ts +15 -1
- package/src/utils/completion-dedupe.ts +63 -63
- package/src/utils/file-coalescer.ts +84 -84
- package/src/utils/frontmatter.ts +36 -36
- package/src/utils/fs-watch.ts +31 -31
- package/src/utils/git.ts +262 -262
- package/src/utils/ids.ts +12 -12
- package/src/utils/names.ts +26 -26
- package/src/utils/paths.ts +3 -2
- package/src/utils/safe-paths.ts +34 -0
- package/src/utils/sleep.ts +32 -32
- package/src/utils/timings.ts +31 -31
- package/src/utils/visual.ts +159 -159
- package/src/workflows/discover-workflows.ts +30 -3
- package/src/workflows/validate-workflow.ts +40 -40
- package/src/worktree/branch-freshness.ts +45 -45
- package/teams/default.team.md +12 -12
- package/teams/fast-fix.team.md +11 -11
- package/teams/implementation.team.md +18 -18
- package/teams/parallel-research.team.md +14 -14
- package/teams/research.team.md +11 -11
- package/teams/review.team.md +12 -12
- package/workflows/default.workflow.md +29 -29
- package/workflows/fast-fix.workflow.md +22 -22
- package/workflows/implementation.workflow.md +38 -38
- package/workflows/parallel-research.workflow.md +46 -46
- package/workflows/research.workflow.md +22 -22
- package/workflows/review.workflow.md +30 -30
|
@@ -1,98 +1,98 @@
|
|
|
1
|
-
import * as fs from "node:fs";
|
|
2
|
-
import * as path from "node:path";
|
|
3
|
-
import { buildCompletionKey, getGlobalSeenMap, markSeenWithTtl } from "../utils/completion-dedupe.ts";
|
|
4
|
-
import { closeWatcher, watchWithErrorHandler } from "../utils/fs-watch.ts";
|
|
5
|
-
import { createFileCoalescer } from "../utils/file-coalescer.ts";
|
|
6
|
-
import { logInternalError } from "../utils/internal-error.ts";
|
|
7
|
-
|
|
8
|
-
export interface ResultWatcherEvents {
|
|
9
|
-
emit(event: string, data: unknown): void;
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
export interface ResultWatcherHandle {
|
|
13
|
-
start(): void;
|
|
14
|
-
prime(): void;
|
|
15
|
-
stop(): void;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
interface ResultWatcherDependencies {
|
|
19
|
-
watch?: typeof watchWithErrorHandler;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export interface ResultWatcherOptions extends ResultWatcherDependencies {
|
|
23
|
-
eventName?: string;
|
|
24
|
-
completionTtlMs?: number;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
const RESULT_WATCHER_RESTART_MS = 3000;
|
|
28
|
-
|
|
29
|
-
function readJson(filePath: string): unknown | undefined {
|
|
30
|
-
try {
|
|
31
|
-
return JSON.parse(fs.readFileSync(filePath, "utf-8")) as unknown;
|
|
32
|
-
} catch (error) {
|
|
33
|
-
logInternalError("result-watcher.parse", error, `filePath=${filePath}`);
|
|
34
|
-
return undefined;
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
export function createResultWatcher(events: ResultWatcherEvents, resultsDir: string, eventNameOrOptions: string | ResultWatcherOptions = "pi-crew:run-result"): ResultWatcherHandle {
|
|
39
|
-
const options: ResultWatcherOptions = typeof eventNameOrOptions === "string" ? { eventName: eventNameOrOptions } : eventNameOrOptions;
|
|
40
|
-
const eventName = options.eventName ?? "pi-crew:run-result";
|
|
41
|
-
const completionTtlMs = options.completionTtlMs ?? 5 * 60_000;
|
|
42
|
-
const watch = options.watch ?? watchWithErrorHandler;
|
|
43
|
-
const seen = getGlobalSeenMap("pi-crew.result-watcher");
|
|
44
|
-
let watcher: fs.FSWatcher | null | undefined;
|
|
45
|
-
let restartTimer: ReturnType<typeof setTimeout> | undefined;
|
|
46
|
-
const coalescer = createFileCoalescer((file) => {
|
|
47
|
-
const filePath = path.join(resultsDir, file);
|
|
48
|
-
if (!file.endsWith(".json") || !fs.existsSync(filePath)) return;
|
|
49
|
-
const payload = readJson(filePath);
|
|
50
|
-
if (payload !== undefined) {
|
|
51
|
-
const key = buildCompletionKey(payload as Record<string, unknown>, `file:${file}`);
|
|
52
|
-
if (!markSeenWithTtl(seen, key, Date.now(), completionTtlMs)) {
|
|
53
|
-
events.emit(eventName, payload);
|
|
54
|
-
}
|
|
55
|
-
}
|
|
56
|
-
try {
|
|
57
|
-
fs.unlinkSync(filePath);
|
|
58
|
-
} catch (error) {
|
|
59
|
-
logInternalError("result-watcher.unlink", error, `filePath=${filePath}`);
|
|
60
|
-
}
|
|
61
|
-
}, 50);
|
|
62
|
-
const scheduleRestart = () => {
|
|
63
|
-
if (restartTimer) clearTimeout(restartTimer);
|
|
64
|
-
restartTimer = setTimeout(() => {
|
|
65
|
-
restartTimer = undefined;
|
|
66
|
-
try {
|
|
67
|
-
fs.mkdirSync(resultsDir, { recursive: true });
|
|
68
|
-
handle.start();
|
|
69
|
-
} catch (error) {
|
|
70
|
-
logInternalError("result-watcher.restart", error, `resultsDir=${resultsDir}`);
|
|
71
|
-
}
|
|
72
|
-
}, RESULT_WATCHER_RESTART_MS);
|
|
73
|
-
restartTimer.unref?.();
|
|
74
|
-
};
|
|
75
|
-
const handle: ResultWatcherHandle = {
|
|
76
|
-
start() {
|
|
77
|
-
fs.mkdirSync(resultsDir, { recursive: true });
|
|
78
|
-
if (watcher) closeWatcher(watcher);
|
|
79
|
-
watcher = watch(resultsDir, (event, fileName) => {
|
|
80
|
-
if (event !== "rename" || !fileName) return;
|
|
81
|
-
coalescer.schedule(fileName.toString());
|
|
82
|
-
}, scheduleRestart);
|
|
83
|
-
watcher?.unref?.();
|
|
84
|
-
},
|
|
85
|
-
prime() {
|
|
86
|
-
if (!fs.existsSync(resultsDir)) return;
|
|
87
|
-
for (const file of fs.readdirSync(resultsDir).filter((entry) => entry.endsWith(".json"))) coalescer.schedule(file, 0);
|
|
88
|
-
},
|
|
89
|
-
stop() {
|
|
90
|
-
if (restartTimer) clearTimeout(restartTimer);
|
|
91
|
-
restartTimer = undefined;
|
|
92
|
-
closeWatcher(watcher);
|
|
93
|
-
watcher = undefined;
|
|
94
|
-
coalescer.clear();
|
|
95
|
-
},
|
|
96
|
-
};
|
|
97
|
-
return handle;
|
|
98
|
-
}
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import * as path from "node:path";
|
|
3
|
+
import { buildCompletionKey, getGlobalSeenMap, markSeenWithTtl } from "../utils/completion-dedupe.ts";
|
|
4
|
+
import { closeWatcher, watchWithErrorHandler } from "../utils/fs-watch.ts";
|
|
5
|
+
import { createFileCoalescer } from "../utils/file-coalescer.ts";
|
|
6
|
+
import { logInternalError } from "../utils/internal-error.ts";
|
|
7
|
+
|
|
8
|
+
export interface ResultWatcherEvents {
|
|
9
|
+
emit(event: string, data: unknown): void;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export interface ResultWatcherHandle {
|
|
13
|
+
start(): void;
|
|
14
|
+
prime(): void;
|
|
15
|
+
stop(): void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface ResultWatcherDependencies {
|
|
19
|
+
watch?: typeof watchWithErrorHandler;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export interface ResultWatcherOptions extends ResultWatcherDependencies {
|
|
23
|
+
eventName?: string;
|
|
24
|
+
completionTtlMs?: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const RESULT_WATCHER_RESTART_MS = 3000;
|
|
28
|
+
|
|
29
|
+
function readJson(filePath: string): unknown | undefined {
|
|
30
|
+
try {
|
|
31
|
+
return JSON.parse(fs.readFileSync(filePath, "utf-8")) as unknown;
|
|
32
|
+
} catch (error) {
|
|
33
|
+
logInternalError("result-watcher.parse", error, `filePath=${filePath}`);
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export function createResultWatcher(events: ResultWatcherEvents, resultsDir: string, eventNameOrOptions: string | ResultWatcherOptions = "pi-crew:run-result"): ResultWatcherHandle {
|
|
39
|
+
const options: ResultWatcherOptions = typeof eventNameOrOptions === "string" ? { eventName: eventNameOrOptions } : eventNameOrOptions;
|
|
40
|
+
const eventName = options.eventName ?? "pi-crew:run-result";
|
|
41
|
+
const completionTtlMs = options.completionTtlMs ?? 5 * 60_000;
|
|
42
|
+
const watch = options.watch ?? watchWithErrorHandler;
|
|
43
|
+
const seen = getGlobalSeenMap("pi-crew.result-watcher");
|
|
44
|
+
let watcher: fs.FSWatcher | null | undefined;
|
|
45
|
+
let restartTimer: ReturnType<typeof setTimeout> | undefined;
|
|
46
|
+
const coalescer = createFileCoalescer((file) => {
|
|
47
|
+
const filePath = path.join(resultsDir, file);
|
|
48
|
+
if (!file.endsWith(".json") || !fs.existsSync(filePath)) return;
|
|
49
|
+
const payload = readJson(filePath);
|
|
50
|
+
if (payload !== undefined) {
|
|
51
|
+
const key = buildCompletionKey(payload as Record<string, unknown>, `file:${file}`);
|
|
52
|
+
if (!markSeenWithTtl(seen, key, Date.now(), completionTtlMs)) {
|
|
53
|
+
events.emit(eventName, payload);
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
try {
|
|
57
|
+
fs.unlinkSync(filePath);
|
|
58
|
+
} catch (error) {
|
|
59
|
+
logInternalError("result-watcher.unlink", error, `filePath=${filePath}`);
|
|
60
|
+
}
|
|
61
|
+
}, 50);
|
|
62
|
+
const scheduleRestart = () => {
|
|
63
|
+
if (restartTimer) clearTimeout(restartTimer);
|
|
64
|
+
restartTimer = setTimeout(() => {
|
|
65
|
+
restartTimer = undefined;
|
|
66
|
+
try {
|
|
67
|
+
fs.mkdirSync(resultsDir, { recursive: true });
|
|
68
|
+
handle.start();
|
|
69
|
+
} catch (error) {
|
|
70
|
+
logInternalError("result-watcher.restart", error, `resultsDir=${resultsDir}`);
|
|
71
|
+
}
|
|
72
|
+
}, RESULT_WATCHER_RESTART_MS);
|
|
73
|
+
restartTimer.unref?.();
|
|
74
|
+
};
|
|
75
|
+
const handle: ResultWatcherHandle = {
|
|
76
|
+
start() {
|
|
77
|
+
fs.mkdirSync(resultsDir, { recursive: true });
|
|
78
|
+
if (watcher) closeWatcher(watcher);
|
|
79
|
+
watcher = watch(resultsDir, (event, fileName) => {
|
|
80
|
+
if (event !== "rename" || !fileName) return;
|
|
81
|
+
coalescer.schedule(fileName.toString());
|
|
82
|
+
}, scheduleRestart);
|
|
83
|
+
watcher?.unref?.();
|
|
84
|
+
},
|
|
85
|
+
prime() {
|
|
86
|
+
if (!fs.existsSync(resultsDir)) return;
|
|
87
|
+
for (const file of fs.readdirSync(resultsDir).filter((entry) => entry.endsWith(".json"))) coalescer.schedule(file, 0);
|
|
88
|
+
},
|
|
89
|
+
stop() {
|
|
90
|
+
if (restartTimer) clearTimeout(restartTimer);
|
|
91
|
+
restartTimer = undefined;
|
|
92
|
+
closeWatcher(watcher);
|
|
93
|
+
watcher = undefined;
|
|
94
|
+
coalescer.clear();
|
|
95
|
+
},
|
|
96
|
+
};
|
|
97
|
+
return handle;
|
|
98
|
+
}
|
|
@@ -3,6 +3,7 @@ import * as path from "node:path";
|
|
|
3
3
|
import { assertRunBundle } from "./run-bundle-schema.ts";
|
|
4
4
|
import { projectCrewRoot, userCrewRoot } from "../utils/paths.ts";
|
|
5
5
|
import { DEFAULT_PATHS } from "../config/defaults.ts";
|
|
6
|
+
import { assertSafePathId, resolveContainedRelativePath, resolveRealContainedPath } from "../utils/safe-paths.ts";
|
|
6
7
|
|
|
7
8
|
export interface ImportedRunBundleInfo {
|
|
8
9
|
runId: string;
|
|
@@ -20,12 +21,21 @@ export function importRunBundle(cwd: string, bundlePath: string, scope: "project
|
|
|
20
21
|
const resolvedPath = path.isAbsolute(bundlePath) ? bundlePath : path.resolve(cwd, bundlePath);
|
|
21
22
|
const raw = JSON.parse(fs.readFileSync(resolvedPath, "utf-8")) as unknown;
|
|
22
23
|
assertRunBundle(raw);
|
|
23
|
-
const runId = raw.manifest.runId;
|
|
24
|
+
const runId = assertSafePathId("runId", raw.manifest.runId);
|
|
24
25
|
const importedAt = new Date().toISOString();
|
|
25
|
-
const
|
|
26
|
+
const importsRoot = importRoot(cwd, scope);
|
|
27
|
+
fs.mkdirSync(importsRoot, { recursive: true });
|
|
28
|
+
if (fs.lstatSync(importsRoot).isSymbolicLink()) throw new Error(`Invalid import root: ${importsRoot}`);
|
|
29
|
+
resolveRealContainedPath(path.dirname(importsRoot), path.basename(importsRoot));
|
|
30
|
+
const root = resolveContainedRelativePath(importsRoot, runId, "runId");
|
|
26
31
|
fs.mkdirSync(root, { recursive: true });
|
|
32
|
+
if (fs.lstatSync(root).isSymbolicLink()) throw new Error(`Invalid import directory: ${root}`);
|
|
33
|
+
resolveRealContainedPath(importsRoot, runId);
|
|
27
34
|
const targetJson = path.join(root, "run-export.json");
|
|
28
35
|
const targetSummary = path.join(root, "README.md");
|
|
36
|
+
for (const target of [targetJson, targetSummary]) {
|
|
37
|
+
if (fs.existsSync(target) && fs.lstatSync(target).isSymbolicLink()) throw new Error(`Invalid import target: ${target}`);
|
|
38
|
+
}
|
|
29
39
|
fs.writeFileSync(targetJson, `${JSON.stringify({ ...raw, importedAt, importedFrom: resolvedPath }, null, 2)}\n`, "utf-8");
|
|
30
40
|
fs.writeFileSync(targetSummary, [
|
|
31
41
|
`# Imported pi-crew run ${runId}`,
|
|
@@ -3,6 +3,7 @@ import * as path from "node:path";
|
|
|
3
3
|
import type { TeamRunManifest } from "../state/types.ts";
|
|
4
4
|
import { DEFAULT_PATHS } from "../config/defaults.ts";
|
|
5
5
|
import { findRepoRoot, projectCrewRoot, userCrewRoot } from "../utils/paths.ts";
|
|
6
|
+
import { isSafePathId, resolveRealContainedPath } from "../utils/safe-paths.ts";
|
|
6
7
|
|
|
7
8
|
function readManifest(filePath: string): TeamRunManifest | undefined {
|
|
8
9
|
try {
|
|
@@ -15,10 +16,19 @@ function readManifest(filePath: string): TeamRunManifest | undefined {
|
|
|
15
16
|
function collectRuns(root: string, maxEntries?: number): TeamRunManifest[] {
|
|
16
17
|
const runsRoot = path.join(root, DEFAULT_PATHS.state.runsSubdir);
|
|
17
18
|
if (!fs.existsSync(runsRoot)) return [];
|
|
18
|
-
const entries = fs.readdirSync(runsRoot
|
|
19
|
+
const entries = fs.readdirSync(runsRoot, { withFileTypes: true })
|
|
20
|
+
.filter((entry) => entry.isDirectory() && isSafePathId(entry.name))
|
|
21
|
+
.map((entry) => entry.name)
|
|
22
|
+
.sort((a, b) => b.localeCompare(a));
|
|
19
23
|
const selected = maxEntries !== undefined ? entries.slice(0, Math.max(0, maxEntries)) : entries;
|
|
20
24
|
return selected
|
|
21
|
-
.map((entry) =>
|
|
25
|
+
.map((entry) => {
|
|
26
|
+
try {
|
|
27
|
+
return readManifest(path.join(resolveRealContainedPath(runsRoot, entry), DEFAULT_PATHS.state.manifestFile));
|
|
28
|
+
} catch {
|
|
29
|
+
return undefined;
|
|
30
|
+
}
|
|
31
|
+
})
|
|
22
32
|
.filter((manifest): manifest is TeamRunManifest => manifest !== undefined);
|
|
23
33
|
}
|
|
24
34
|
|
|
@@ -1,24 +1,24 @@
|
|
|
1
|
-
import * as fs from "node:fs";
|
|
2
|
-
import type { TeamRunManifest } from "../state/types.ts";
|
|
3
|
-
import { listRuns } from "./run-index.ts";
|
|
4
|
-
|
|
5
|
-
export interface PruneRunsResult {
|
|
6
|
-
kept: string[];
|
|
7
|
-
removed: string[];
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
function isFinished(run: TeamRunManifest): boolean {
|
|
11
|
-
return run.status === "completed" || run.status === "failed" || run.status === "cancelled" || run.status === "blocked";
|
|
12
|
-
}
|
|
13
|
-
|
|
14
|
-
export function pruneFinishedRuns(cwd: string, keep: number): PruneRunsResult {
|
|
15
|
-
const finished = listRuns(cwd).filter((run) => run.cwd === cwd && isFinished(run)).sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
|
|
16
|
-
const kept = finished.slice(0, keep).map((run) => run.runId);
|
|
17
|
-
const removed: string[] = [];
|
|
18
|
-
for (const run of finished.slice(keep)) {
|
|
19
|
-
fs.rmSync(run.stateRoot, { recursive: true, force: true });
|
|
20
|
-
fs.rmSync(run.artifactsRoot, { recursive: true, force: true });
|
|
21
|
-
removed.push(run.runId);
|
|
22
|
-
}
|
|
23
|
-
return { kept, removed };
|
|
24
|
-
}
|
|
1
|
+
import * as fs from "node:fs";
|
|
2
|
+
import type { TeamRunManifest } from "../state/types.ts";
|
|
3
|
+
import { listRuns } from "./run-index.ts";
|
|
4
|
+
|
|
5
|
+
export interface PruneRunsResult {
|
|
6
|
+
kept: string[];
|
|
7
|
+
removed: string[];
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function isFinished(run: TeamRunManifest): boolean {
|
|
11
|
+
return run.status === "completed" || run.status === "failed" || run.status === "cancelled" || run.status === "blocked";
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function pruneFinishedRuns(cwd: string, keep: number): PruneRunsResult {
|
|
15
|
+
const finished = listRuns(cwd).filter((run) => run.cwd === cwd && isFinished(run)).sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
|
|
16
|
+
const kept = finished.slice(0, keep).map((run) => run.runId);
|
|
17
|
+
const removed: string[] = [];
|
|
18
|
+
for (const run of finished.slice(keep)) {
|
|
19
|
+
fs.rmSync(run.stateRoot, { recursive: true, force: true });
|
|
20
|
+
fs.rmSync(run.artifactsRoot, { recursive: true, force: true });
|
|
21
|
+
removed.push(run.runId);
|
|
22
|
+
}
|
|
23
|
+
return { kept, removed };
|
|
24
|
+
}
|
|
@@ -10,12 +10,13 @@ import { appendEvent, readEvents, readEventsCursor } from "../../state/event-log
|
|
|
10
10
|
import { resolveCrewRuntime } from "../../runtime/runtime-resolver.ts";
|
|
11
11
|
import { probeLiveSessionRuntime } from "../../subagents/live/session-runtime.ts";
|
|
12
12
|
import { touchWorkerHeartbeat } from "../../runtime/worker-heartbeat.ts";
|
|
13
|
-
import {
|
|
13
|
+
import { agentOutputPath, readCrewAgentEventsCursor, readCrewAgentStatus, readCrewAgents } from "../../runtime/crew-agent-records.ts";
|
|
14
14
|
import { buildAgentDashboard, readAgentOutput } from "../../runtime/agent-observability.ts";
|
|
15
15
|
import { readForegroundControlStatus, writeForegroundInterruptRequest } from "../../runtime/foreground-control.ts";
|
|
16
|
-
import { listLiveAgents, resumeLiveAgent, steerLiveAgent, stopLiveAgent } from "../../subagents/live/manager.ts";
|
|
16
|
+
import { getLiveAgent, listLiveAgents, resumeLiveAgent, steerLiveAgent, stopLiveAgent } from "../../subagents/live/manager.ts";
|
|
17
17
|
import { appendLiveAgentControlRequest } from "../../subagents/live/control.ts";
|
|
18
18
|
import { liveControlRealtimeMessage, publishLiveControlRealtime } from "../../subagents/live/realtime.ts";
|
|
19
|
+
import { resolveRealContainedPath } from "../../utils/safe-paths.ts";
|
|
19
20
|
import type { PiTeamsToolResult } from "../tool-result.ts";
|
|
20
21
|
import { configRecord, result, type TeamContext } from "./context.ts";
|
|
21
22
|
|
|
@@ -24,6 +25,26 @@ function globMatch(value: string, pattern: string): boolean {
|
|
|
24
25
|
return new RegExp(`^${escaped}$`).test(value);
|
|
25
26
|
}
|
|
26
27
|
|
|
28
|
+
function safeReadContainedFile(baseDir: string, filePath: string | undefined): string | undefined {
|
|
29
|
+
if (!filePath) return undefined;
|
|
30
|
+
let safePath: string;
|
|
31
|
+
try {
|
|
32
|
+
safePath = resolveRealContainedPath(baseDir, filePath);
|
|
33
|
+
} catch {
|
|
34
|
+
return undefined;
|
|
35
|
+
}
|
|
36
|
+
return fs.existsSync(safePath) ? fs.readFileSync(safePath, "utf-8") : undefined;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function safeContainedPath(baseDir: string, filePath: string | undefined): string | undefined {
|
|
40
|
+
if (!filePath) return undefined;
|
|
41
|
+
try {
|
|
42
|
+
return resolveRealContainedPath(baseDir, filePath);
|
|
43
|
+
} catch {
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
27
48
|
function snapshotHasRunId(snapshot: { values?: unknown }, runId: string): boolean {
|
|
28
49
|
const values = Array.isArray(snapshot.values) ? snapshot.values : [];
|
|
29
50
|
return values.some((value) => {
|
|
@@ -85,7 +106,7 @@ export async function handleApi(params: TeamToolParamsValue, ctx: TeamContext):
|
|
|
85
106
|
const agent = readCrewAgents(loaded.manifest).find((item) => item.id === agentId || item.taskId === agentId);
|
|
86
107
|
if (!agent) return result("API get-agent-result requires config.agentId matching an agent id or task id.", { action: "api", status: "error", runId: loaded.manifest.runId }, true);
|
|
87
108
|
const task = loaded.tasks.find((item) => item.id === agent.taskId);
|
|
88
|
-
const text =
|
|
109
|
+
const text = safeReadContainedFile(loaded.manifest.artifactsRoot, task?.resultArtifact?.path) ?? JSON.stringify(agent, null, 2);
|
|
89
110
|
return result(text, { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
|
|
90
111
|
}
|
|
91
112
|
if (operation === "read-agent-status") {
|
|
@@ -102,9 +123,8 @@ export async function handleApi(params: TeamToolParamsValue, ctx: TeamContext):
|
|
|
102
123
|
if (!agent) return result("API read-agent-events requires config.agentId matching an agent id or task id, or at least one agent in the run.", { action: "api", status: "error", runId: loaded.manifest.runId }, true);
|
|
103
124
|
const sinceSeq = typeof cfg.sinceSeq === "number" ? cfg.sinceSeq : undefined;
|
|
104
125
|
const limit = typeof cfg.limit === "number" ? cfg.limit : undefined;
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
: { path: agentEventsPath(loaded.manifest, agent.taskId), events: readCrewAgentEvents(loaded.manifest, agent.taskId) };
|
|
126
|
+
const cursorPayload = readCrewAgentEventsCursor(loaded.manifest, agent.taskId, { sinceSeq, limit });
|
|
127
|
+
const payload = sinceSeq !== undefined || limit !== undefined ? cursorPayload : { path: cursorPayload.path, events: cursorPayload.events };
|
|
108
128
|
return result(JSON.stringify(payload, null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
|
|
109
129
|
}
|
|
110
130
|
if (operation === "read-agent-transcript") {
|
|
@@ -112,8 +132,12 @@ export async function handleApi(params: TeamToolParamsValue, ctx: TeamContext):
|
|
|
112
132
|
const agents = readCrewAgents(loaded.manifest);
|
|
113
133
|
const agent = agentId ? agents.find((item) => item.id === agentId || item.taskId === agentId) : agents[0];
|
|
114
134
|
if (!agent) return result("API read-agent-transcript requires config.agentId matching an agent id or task id, or at least one agent in the run.", { action: "api", status: "error", runId: loaded.manifest.runId }, true);
|
|
115
|
-
const
|
|
116
|
-
const
|
|
135
|
+
const artifactTranscriptPath = safeContainedPath(loaded.manifest.artifactsRoot, agent.transcriptPath);
|
|
136
|
+
const fallbackPath = agentOutputPath(loaded.manifest, agent.taskId);
|
|
137
|
+
const artifactText = artifactTranscriptPath ? safeReadContainedFile(loaded.manifest.artifactsRoot, artifactTranscriptPath) ?? "" : "";
|
|
138
|
+
const fallbackText = artifactText ? "" : safeReadContainedFile(loaded.manifest.stateRoot, fallbackPath) ?? "";
|
|
139
|
+
const transcriptPath = artifactText ? artifactTranscriptPath : fallbackPath;
|
|
140
|
+
const text = artifactText || fallbackText;
|
|
117
141
|
return result(text || `(no transcript at ${transcriptPath})`, { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
|
|
118
142
|
}
|
|
119
143
|
if (operation === "read-agent-output") {
|
|
@@ -153,6 +177,8 @@ export async function handleApi(params: TeamToolParamsValue, ctx: TeamContext):
|
|
|
153
177
|
const message = typeof cfg.message === "string" && cfg.message.trim() ? cfg.message.trim() : undefined;
|
|
154
178
|
const prompt = typeof cfg.prompt === "string" && cfg.prompt.trim() ? cfg.prompt.trim() : message;
|
|
155
179
|
try {
|
|
180
|
+
const live = getLiveAgent(agentId);
|
|
181
|
+
if (live && live.runId !== loaded.manifest.runId) return result(`Live agent '${agentId}' does not belong to run ${loaded.manifest.runId}.`, { action: "api", status: "error", runId: loaded.manifest.runId }, true);
|
|
156
182
|
if (operation === "steer-agent") return result(JSON.stringify(await steerLiveAgent(agentId, message ?? "Please report current status and wrap up if possible."), null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
|
|
157
183
|
if (operation === "resume-agent") {
|
|
158
184
|
if (!prompt) return result("API resume-agent requires config.prompt or config.message.", { action: "api", status: "error", runId: loaded.manifest.runId }, true);
|
|
@@ -165,18 +191,31 @@ export async function handleApi(params: TeamToolParamsValue, ctx: TeamContext):
|
|
|
165
191
|
const err = error instanceof Error ? error.message : String(error);
|
|
166
192
|
return result(err, { action: "api", status: "error", runId: loaded.manifest.runId }, true);
|
|
167
193
|
}
|
|
194
|
+
const task = loaded.tasks.find((item) => item.id === agent.taskId);
|
|
195
|
+
if (!task) return result(`API ${operation} agent '${agentId}' does not match a run task.`, { action: "api", status: "error", runId: loaded.manifest.runId }, true);
|
|
168
196
|
if (operation === "resume-agent" && !prompt) return result("API resume-agent requires config.prompt or config.message.", { action: "api", status: "error", runId: loaded.manifest.runId }, true);
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
197
|
+
try {
|
|
198
|
+
const request = appendLiveAgentControlRequest(loaded.manifest, { taskId: task.id, agentId: agent.id, operation: operation === "resume-agent" ? "resume" : operation === "steer-agent" ? "steer" : "stop", message: operation === "resume-agent" ? prompt : message });
|
|
199
|
+
publishLiveControlRealtime(request);
|
|
200
|
+
ctx.events?.emit?.("pi-crew:live-control", liveControlRealtimeMessage(request));
|
|
201
|
+
appendEvent(loaded.manifest.eventsPath, { type: "agent.control.queued", runId: loaded.manifest.runId, taskId: agent.taskId, message: `Queued ${request.operation} control request for live agent.`, data: { request, realtime: true } });
|
|
202
|
+
return result(JSON.stringify({ queued: true, request }, null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
|
|
203
|
+
} catch (queueError) {
|
|
204
|
+
const message = queueError instanceof Error ? queueError.message : String(queueError);
|
|
205
|
+
return result(message, { action: "api", status: "error", runId: loaded.manifest.runId }, true);
|
|
206
|
+
}
|
|
174
207
|
}
|
|
175
208
|
}
|
|
176
209
|
if (operation === "read-mailbox") {
|
|
177
210
|
const direction = cfg.direction === "inbox" || cfg.direction === "outbox" ? cfg.direction as MailboxDirection : undefined;
|
|
178
211
|
const taskId = typeof cfg.taskId === "string" ? cfg.taskId : undefined;
|
|
179
|
-
|
|
212
|
+
if (taskId && !loaded.tasks.some((task) => task.id === taskId)) return result(`API read-mailbox taskId '${taskId}' does not match a run task.`, { action: "api", status: "error", runId: loaded.manifest.runId }, true);
|
|
213
|
+
try {
|
|
214
|
+
return result(JSON.stringify(readMailbox(loaded.manifest, direction, taskId), null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
|
|
215
|
+
} catch (error) {
|
|
216
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
217
|
+
return result(message, { action: "api", status: "error", runId: loaded.manifest.runId }, true);
|
|
218
|
+
}
|
|
180
219
|
}
|
|
181
220
|
if (operation === "validate-mailbox") {
|
|
182
221
|
const report = validateMailbox(loaded.manifest, { repair: cfg.repair === true });
|
|
@@ -192,6 +231,7 @@ export async function handleApi(params: TeamToolParamsValue, ctx: TeamContext):
|
|
|
192
231
|
const body = typeof cfg.body === "string" && cfg.body.trim() ? cfg.body : undefined;
|
|
193
232
|
const taskId = typeof cfg.taskId === "string" && cfg.taskId.trim() ? cfg.taskId.trim() : undefined;
|
|
194
233
|
if (!body) return result("API send-message requires config.body.", { action: "api", status: "error", runId: loaded.manifest.runId }, true);
|
|
234
|
+
if (taskId && !loaded.tasks.some((task) => task.id === taskId)) return result(`API send-message taskId '${taskId}' does not match a run task.`, { action: "api", status: "error", runId: loaded.manifest.runId }, true);
|
|
195
235
|
try {
|
|
196
236
|
return withRunLockSync(loaded.manifest, () => {
|
|
197
237
|
const message = appendMailboxMessage(loaded.manifest, { direction, from, to, body, taskId });
|
|
@@ -1,31 +1,31 @@
|
|
|
1
|
-
import type { TeamToolParamsValue } from "../../schema/team-tool-schema.ts";
|
|
2
|
-
import { withRunLockSync } from "../../state/locks.ts";
|
|
3
|
-
import { loadRunManifestById, saveRunTasks, updateRunStatus } from "../../state/state-store.ts";
|
|
4
|
-
import { saveCrewAgents, recordFromTask } from "../../runtime/crew-agent-records.ts";
|
|
5
|
-
import { writeForegroundInterruptRequest } from "../../runtime/foreground-control.ts";
|
|
6
|
-
import { logInternalError } from "../../utils/internal-error.ts";
|
|
7
|
-
import type { PiTeamsToolResult } from "../tool-result.ts";
|
|
8
|
-
import { result, type TeamContext } from "./context.ts";
|
|
9
|
-
|
|
10
|
-
export function handleCancel(params: TeamToolParamsValue, ctx: TeamContext): PiTeamsToolResult {
|
|
11
|
-
if (!params.runId) return result("Cancel requires runId.", { action: "cancel", status: "error" }, true);
|
|
12
|
-
const loaded = loadRunManifestById(ctx.cwd, params.runId);
|
|
13
|
-
if (!loaded) return result(`Run '${params.runId}' not found.`, { action: "cancel", status: "error" }, true);
|
|
14
|
-
return withRunLockSync(loaded.manifest, () => {
|
|
15
|
-
if (loaded.manifest.status === "completed" && !params.force) return result(`Run ${loaded.manifest.runId} is already completed; nothing to cancel. Use force: true to mark it cancelled anyway.`, { action: "cancel", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
|
|
16
|
-
const tasks = loaded.tasks.map((task) => task.status === "queued" || task.status === "running" ? { ...task, status: "cancelled" as const, finishedAt: new Date().toISOString(), error: "Run cancelled by user request." } : task);
|
|
17
|
-
saveRunTasks(loaded.manifest, tasks);
|
|
18
|
-
try {
|
|
19
|
-
saveCrewAgents(loaded.manifest, tasks.map((task) => recordFromTask(loaded.manifest, task, "child-process")));
|
|
20
|
-
} catch (error) {
|
|
21
|
-
logInternalError("team-tool.handleCancel.crewAgents", error, `runId=${loaded.manifest.runId}`);
|
|
22
|
-
}
|
|
23
|
-
try {
|
|
24
|
-
writeForegroundInterruptRequest(loaded.manifest, "Run cancelled by user request.");
|
|
25
|
-
} catch (error) {
|
|
26
|
-
logInternalError("team-tool.handleCancel.interruptRequest", error, `runId=${loaded.manifest.runId}`);
|
|
27
|
-
}
|
|
28
|
-
const updated = updateRunStatus(loaded.manifest, "cancelled", "Run cancelled by user request. Already-finished worker processes are not retroactively changed.");
|
|
29
|
-
return result(`Cancelled run ${updated.runId}.`, { action: "cancel", status: "ok", runId: updated.runId, artifactsRoot: updated.artifactsRoot });
|
|
30
|
-
});
|
|
31
|
-
}
|
|
1
|
+
import type { TeamToolParamsValue } from "../../schema/team-tool-schema.ts";
|
|
2
|
+
import { withRunLockSync } from "../../state/locks.ts";
|
|
3
|
+
import { loadRunManifestById, saveRunTasks, updateRunStatus } from "../../state/state-store.ts";
|
|
4
|
+
import { saveCrewAgents, recordFromTask } from "../../runtime/crew-agent-records.ts";
|
|
5
|
+
import { writeForegroundInterruptRequest } from "../../runtime/foreground-control.ts";
|
|
6
|
+
import { logInternalError } from "../../utils/internal-error.ts";
|
|
7
|
+
import type { PiTeamsToolResult } from "../tool-result.ts";
|
|
8
|
+
import { result, type TeamContext } from "./context.ts";
|
|
9
|
+
|
|
10
|
+
export function handleCancel(params: TeamToolParamsValue, ctx: TeamContext): PiTeamsToolResult {
|
|
11
|
+
if (!params.runId) return result("Cancel requires runId.", { action: "cancel", status: "error" }, true);
|
|
12
|
+
const loaded = loadRunManifestById(ctx.cwd, params.runId);
|
|
13
|
+
if (!loaded) return result(`Run '${params.runId}' not found.`, { action: "cancel", status: "error" }, true);
|
|
14
|
+
return withRunLockSync(loaded.manifest, () => {
|
|
15
|
+
if (loaded.manifest.status === "completed" && !params.force) return result(`Run ${loaded.manifest.runId} is already completed; nothing to cancel. Use force: true to mark it cancelled anyway.`, { action: "cancel", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
|
|
16
|
+
const tasks = loaded.tasks.map((task) => task.status === "queued" || task.status === "running" ? { ...task, status: "cancelled" as const, finishedAt: new Date().toISOString(), error: "Run cancelled by user request." } : task);
|
|
17
|
+
saveRunTasks(loaded.manifest, tasks);
|
|
18
|
+
try {
|
|
19
|
+
saveCrewAgents(loaded.manifest, tasks.map((task) => recordFromTask(loaded.manifest, task, "child-process")));
|
|
20
|
+
} catch (error) {
|
|
21
|
+
logInternalError("team-tool.handleCancel.crewAgents", error, `runId=${loaded.manifest.runId}`);
|
|
22
|
+
}
|
|
23
|
+
try {
|
|
24
|
+
writeForegroundInterruptRequest(loaded.manifest, "Run cancelled by user request.");
|
|
25
|
+
} catch (error) {
|
|
26
|
+
logInternalError("team-tool.handleCancel.interruptRequest", error, `runId=${loaded.manifest.runId}`);
|
|
27
|
+
}
|
|
28
|
+
const updated = updateRunStatus(loaded.manifest, "cancelled", "Run cancelled by user request. Already-finished worker processes are not retroactively changed.");
|
|
29
|
+
return result(`Cancelled run ${updated.runId}.`, { action: "cancel", status: "ok", runId: updated.runId, artifactsRoot: updated.artifactsRoot });
|
|
30
|
+
});
|
|
31
|
+
}
|