pi-crew 0.1.30 → 0.1.32
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 +9 -0
- package/README.md +80 -27
- 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/architecture.md +173 -164
- 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 -0
- package/docs/research-extension-system.md +324 -0
- package/docs/research-optimization-plan.md +548 -0
- package/docs/research-pi-coding-agent.md +357 -0
- package/docs/resource-formats.md +4 -3
- package/docs/runtime-flow.md +148 -148
- package/docs/source-runtime-refactor-map.md +83 -83
- package/docs/usage.md +3 -3
- package/index.ts +6 -6
- package/package.json +1 -1
- package/schema.json +53 -1
- package/skills/git-master/SKILL.md +24 -19
- package/skills/read-only-explorer/SKILL.md +26 -21
- package/skills/safe-bash/SKILL.md +21 -16
- package/skills/task-packet/SKILL.md +28 -23
- package/skills/verify-evidence/SKILL.md +27 -22
- package/src/agents/agent-serializer.ts +34 -34
- package/src/agents/discover-agents.ts +102 -102
- package/src/config/config.ts +14 -1
- package/src/config/defaults.ts +3 -2
- package/src/extension/cross-extension-rpc.ts +82 -82
- package/src/extension/import-index.ts +4 -3
- package/src/extension/management.ts +2 -2
- package/src/extension/project-init.ts +9 -7
- package/src/extension/register.ts +63 -0
- package/src/extension/registration/artifact-cleanup.ts +15 -14
- package/src/extension/registration/commands.ts +208 -208
- package/src/extension/registration/compaction-guard.ts +125 -0
- package/src/extension/registration/subagent-tools.ts +27 -8
- package/src/extension/registration/team-tool.ts +61 -44
- package/src/extension/result-watcher.ts +98 -98
- package/src/extension/run-import.ts +4 -4
- package/src/extension/run-index.ts +14 -14
- package/src/extension/run-maintenance.ts +24 -24
- package/src/extension/team-tool/api.ts +3 -0
- package/src/extension/team-tool/cancel.ts +31 -31
- package/src/extension/team-tool/doctor.ts +179 -178
- 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/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 +113 -113
- package/src/runtime/async-marker.ts +26 -26
- package/src/runtime/background-runner.ts +53 -53
- package/src/runtime/crew-agent-runtime.ts +58 -58
- 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/live-agent-control.ts +78 -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 +212 -214
- 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/pi-spawn.ts +96 -96
- 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/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 +29 -2
- package/src/runtime/task-display.ts +38 -38
- package/src/runtime/task-output-context.ts +106 -106
- package/src/runtime/task-packet.ts +84 -84
- package/src/runtime/task-runner/live-executor.ts +98 -98
- package/src/runtime/task-runner/progress.ts +111 -111
- package/src/runtime/task-runner/prompt-builder.ts +72 -72
- package/src/runtime/task-runner/result-utils.ts +14 -14
- package/src/runtime/task-runner/state-helpers.ts +22 -22
- package/src/runtime/worker-heartbeat.ts +21 -21
- package/src/runtime/worker-startup.ts +57 -57
- package/src/schema/config-schema.ts +12 -0
- package/src/schema/team-tool-schema.ts +100 -100
- package/src/state/artifact-store.ts +108 -108
- package/src/state/contracts.ts +105 -105
- package/src/state/jsonl-writer.ts +77 -77
- package/src/state/state-store.ts +8 -9
- package/src/state/task-claims.ts +42 -42
- package/src/state/types.ts +180 -180
- 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 +2 -2
- package/src/teams/team-serializer.ts +36 -36
- package/src/types/diff.d.ts +18 -0
- package/src/ui/crew-footer.ts +101 -101
- package/src/ui/crew-select-list.ts +111 -111
- package/src/ui/crew-widget.ts +285 -285
- 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/powerbar-publisher.ts +94 -94
- package/src/ui/render-diff.ts +119 -119
- package/src/ui/run-dashboard.ts +372 -372
- package/src/ui/status-colors.ts +54 -54
- package/src/ui/syntax-highlight.ts +116 -116
- package/src/ui/transcript-viewer.ts +302 -302
- 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 +34 -7
- 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 +2 -2
- package/src/workflows/validate-workflow.ts +40 -40
- package/src/worktree/branch-freshness.ts +45 -45
- package/src/worktree/cleanup.ts +71 -69
- package/src/worktree/worktree-manager.ts +3 -1
- 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,44 +1,61 @@
|
|
|
1
|
-
import type { ExtensionAPI, ExtensionContext, ToolDefinition } from "@mariozechner/pi-coding-agent";
|
|
2
|
-
import { loadConfig } from "../../config/config.ts";
|
|
3
|
-
import { TeamToolParams, type TeamToolParamsValue } from "../../schema/team-tool-schema.ts";
|
|
4
|
-
import type { CrewWidgetState } from "../../ui/crew-widget.ts";
|
|
5
|
-
import { updateCrewWidget } from "../../ui/crew-widget.ts";
|
|
6
|
-
import { updatePiCrewPowerbar } from "../../ui/powerbar-publisher.ts";
|
|
7
|
-
import type { createManifestCache } from "../../runtime/manifest-cache.ts";
|
|
8
|
-
import { handleTeamTool } from "../team-tool.ts";
|
|
9
|
-
|
|
10
|
-
export interface RegisterTeamToolDeps {
|
|
11
|
-
foregroundControllers: Set<AbortController>;
|
|
12
|
-
startForegroundRun: (ctx: ExtensionContext, runner: (signal?: AbortSignal) => Promise<void>, runId?: string) => void;
|
|
13
|
-
openLiveSidebar: (ctx: ExtensionContext, runId: string) => void;
|
|
14
|
-
getManifestCache: (cwd: string) => ReturnType<typeof createManifestCache>;
|
|
15
|
-
widgetState: CrewWidgetState;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export function registerTeamTool(pi: ExtensionAPI, deps: RegisterTeamToolDeps): void {
|
|
19
|
-
const tool: ToolDefinition = {
|
|
20
|
-
name: "team",
|
|
21
|
-
label: "Team",
|
|
22
|
-
description: "Coordinate Pi teams. Use proactively for complex multi-file work, planning, implementation, tests, reviews, security audits, research, async/background runs, and worktree-isolated execution. Use action='recommend' when unsure which team/workflow to choose. Destructive actions require explicit user confirmation.",
|
|
23
|
-
promptSnippet: "Use the team tool proactively for coordinated multi-agent work. If unsure, call { action: 'recommend', goal } first, then run or plan with the suggested team/workflow.",
|
|
24
|
-
parameters: TeamToolParams as never,
|
|
25
|
-
async execute(_id, params, signal, _onUpdate, ctx) {
|
|
26
|
-
const controller = new AbortController();
|
|
27
|
-
deps.foregroundControllers.add(controller);
|
|
28
|
-
const abort = (): void => controller.abort();
|
|
29
|
-
signal?.addEventListener("abort", abort, { once: true });
|
|
30
|
-
try {
|
|
31
|
-
const
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
1
|
+
import type { ExtensionAPI, ExtensionContext, ToolDefinition } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import { loadConfig } from "../../config/config.ts";
|
|
3
|
+
import { TeamToolParams, type TeamToolParamsValue } from "../../schema/team-tool-schema.ts";
|
|
4
|
+
import type { CrewWidgetState } from "../../ui/crew-widget.ts";
|
|
5
|
+
import { updateCrewWidget } from "../../ui/crew-widget.ts";
|
|
6
|
+
import { updatePiCrewPowerbar } from "../../ui/powerbar-publisher.ts";
|
|
7
|
+
import type { createManifestCache } from "../../runtime/manifest-cache.ts";
|
|
8
|
+
import { handleTeamTool } from "../team-tool.ts";
|
|
9
|
+
|
|
10
|
+
export interface RegisterTeamToolDeps {
|
|
11
|
+
foregroundControllers: Set<AbortController>;
|
|
12
|
+
startForegroundRun: (ctx: ExtensionContext, runner: (signal?: AbortSignal) => Promise<void>, runId?: string) => void;
|
|
13
|
+
openLiveSidebar: (ctx: ExtensionContext, runId: string) => void;
|
|
14
|
+
getManifestCache: (cwd: string) => ReturnType<typeof createManifestCache>;
|
|
15
|
+
widgetState: CrewWidgetState;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export function registerTeamTool(pi: ExtensionAPI, deps: RegisterTeamToolDeps): void {
|
|
19
|
+
const tool: ToolDefinition = {
|
|
20
|
+
name: "team",
|
|
21
|
+
label: "Team",
|
|
22
|
+
description: "Coordinate Pi teams. Use proactively for complex multi-file work, planning, implementation, tests, reviews, security audits, research, async/background runs, and worktree-isolated execution. Use action='recommend' when unsure which team/workflow to choose. Destructive actions require explicit user confirmation.",
|
|
23
|
+
promptSnippet: "Use the team tool proactively for coordinated multi-agent work. If unsure, call { action: 'recommend', goal } first, then run or plan with the suggested team/workflow.",
|
|
24
|
+
parameters: TeamToolParams as never,
|
|
25
|
+
async execute(_id, params, signal, _onUpdate, ctx) {
|
|
26
|
+
const controller = new AbortController();
|
|
27
|
+
deps.foregroundControllers.add(controller);
|
|
28
|
+
const abort = (): void => controller.abort();
|
|
29
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
30
|
+
try {
|
|
31
|
+
const resolved = params as TeamToolParamsValue;
|
|
32
|
+
// Phase 1.5: Auto-set session name from team run context
|
|
33
|
+
if (resolved.action === "run" && resolved.goal && !pi.getSessionName()) {
|
|
34
|
+
const runLabel = resolved.team ?? resolved.agent ?? "direct";
|
|
35
|
+
pi.setSessionName(`pi-crew: ${runLabel}/${resolved.workflow ?? "default"} — ${resolved.goal.slice(0, 60)}`);
|
|
36
|
+
}
|
|
37
|
+
const output = await handleTeamTool(resolved, { ...ctx, signal: controller.signal, startForegroundRun: (runner, runId) => deps.startForegroundRun(ctx, runner, runId), onRunStarted: (runId) => deps.openLiveSidebar(ctx, runId) });
|
|
38
|
+
if (resolved.action === "run") {
|
|
39
|
+
pi.appendEntry("crew:run-started", {
|
|
40
|
+
runId: output.details?.runId,
|
|
41
|
+
team: resolved.team,
|
|
42
|
+
workflow: resolved.workflow,
|
|
43
|
+
agent: resolved.agent,
|
|
44
|
+
goal: resolved.goal,
|
|
45
|
+
status: output.details?.status,
|
|
46
|
+
timestamp: Date.now(),
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
const config = loadConfig(ctx.cwd).config.ui;
|
|
50
|
+
const cache = deps.getManifestCache(ctx.cwd);
|
|
51
|
+
updateCrewWidget(ctx, deps.widgetState, config, cache);
|
|
52
|
+
updatePiCrewPowerbar(pi.events, ctx.cwd, config, cache);
|
|
53
|
+
return output;
|
|
54
|
+
} finally {
|
|
55
|
+
signal?.removeEventListener("abort", abort);
|
|
56
|
+
deps.foregroundControllers.delete(controller);
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
pi.registerTool(tool);
|
|
61
|
+
}
|
|
@@ -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
|
+
}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import { assertRunBundle } from "./run-bundle-schema.ts";
|
|
4
|
-
import {
|
|
4
|
+
import { projectCrewRoot, userCrewRoot } from "../utils/paths.ts";
|
|
5
|
+
import { DEFAULT_PATHS } from "../config/defaults.ts";
|
|
5
6
|
|
|
6
7
|
export interface ImportedRunBundleInfo {
|
|
7
8
|
runId: string;
|
|
@@ -11,9 +12,8 @@ export interface ImportedRunBundleInfo {
|
|
|
11
12
|
}
|
|
12
13
|
|
|
13
14
|
function importRoot(cwd: string, scope: "project" | "user"): string {
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
: path.join(userPiRoot(), "extensions", "pi-crew", "imports");
|
|
15
|
+
const base = scope === "project" ? projectCrewRoot(cwd) : userCrewRoot();
|
|
16
|
+
return path.join(base, DEFAULT_PATHS.state.importsSubdir);
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
export function importRunBundle(cwd: string, bundlePath: string, scope: "project" | "user" = "project"): ImportedRunBundleInfo {
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
3
|
import type { TeamRunManifest } from "../state/types.ts";
|
|
4
|
-
import {
|
|
4
|
+
import { DEFAULT_PATHS } from "../config/defaults.ts";
|
|
5
|
+
import { findRepoRoot, projectCrewRoot, userCrewRoot } from "../utils/paths.ts";
|
|
5
6
|
|
|
6
7
|
function readManifest(filePath: string): TeamRunManifest | undefined {
|
|
7
8
|
try {
|
|
@@ -12,33 +13,32 @@ function readManifest(filePath: string): TeamRunManifest | undefined {
|
|
|
12
13
|
}
|
|
13
14
|
|
|
14
15
|
function collectRuns(root: string, maxEntries?: number): TeamRunManifest[] {
|
|
15
|
-
const runsRoot = path.join(root,
|
|
16
|
+
const runsRoot = path.join(root, DEFAULT_PATHS.state.runsSubdir);
|
|
16
17
|
if (!fs.existsSync(runsRoot)) return [];
|
|
17
18
|
const entries = fs.readdirSync(runsRoot).sort((a, b) => b.localeCompare(a));
|
|
18
19
|
const selected = maxEntries !== undefined ? entries.slice(0, Math.max(0, maxEntries)) : entries;
|
|
19
20
|
return selected
|
|
20
|
-
.map((entry) => readManifest(path.join(runsRoot, entry,
|
|
21
|
+
.map((entry) => readManifest(path.join(runsRoot, entry, DEFAULT_PATHS.state.manifestFile)))
|
|
21
22
|
.filter((manifest): manifest is TeamRunManifest => manifest !== undefined);
|
|
22
23
|
}
|
|
23
24
|
|
|
24
|
-
function mergeRuns(
|
|
25
|
+
function mergeRuns(runSets: TeamRunManifest[][], max?: number): TeamRunManifest[] {
|
|
25
26
|
const byId = new Map<string, TeamRunManifest>();
|
|
26
|
-
for (const run of
|
|
27
|
+
for (const runs of runSets) for (const run of runs) byId.set(run.runId, run);
|
|
27
28
|
const sorted = [...byId.values()].sort((a, b) => b.createdAt.localeCompare(a.createdAt));
|
|
28
29
|
return max !== undefined ? sorted.slice(0, Math.max(0, max)) : sorted;
|
|
29
30
|
}
|
|
30
31
|
|
|
32
|
+
function scopedRunRoots(cwd: string): string[] {
|
|
33
|
+
return [findRepoRoot(cwd) ? projectCrewRoot(cwd) : userCrewRoot()];
|
|
34
|
+
}
|
|
35
|
+
|
|
31
36
|
export function listRuns(cwd: string): TeamRunManifest[] {
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
collectRuns(path.join(projectPiRoot(cwd), "teams")),
|
|
35
|
-
);
|
|
37
|
+
const roots = scopedRunRoots(cwd);
|
|
38
|
+
return mergeRuns(roots.map((root) => collectRuns(root)));
|
|
36
39
|
}
|
|
37
40
|
|
|
38
41
|
export function listRecentRuns(cwd: string, max = 20): TeamRunManifest[] {
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
collectRuns(path.join(projectPiRoot(cwd), "teams"), max),
|
|
42
|
-
max,
|
|
43
|
-
);
|
|
42
|
+
const roots = scopedRunRoots(cwd);
|
|
43
|
+
return mergeRuns(roots.map((root) => collectRuns(root, max)), max);
|
|
44
44
|
}
|
|
@@ -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
|
+
}
|
|
@@ -116,6 +116,7 @@ export async function handleApi(params: TeamToolParamsValue, ctx: TeamContext):
|
|
|
116
116
|
const messageText = typeof cfg.message === "string" && cfg.message.trim() ? cfg.message.trim() : "Please report your current status, blocker, or smallest next step.";
|
|
117
117
|
const message = appendMailboxMessage(loaded.manifest, { direction: "inbox", from: "leader", to: agent.taskId, taskId: agent.taskId, body: messageText });
|
|
118
118
|
appendEvent(loaded.manifest.eventsPath, { type: "agent.nudged", runId: loaded.manifest.runId, taskId: agent.taskId, message: messageText, data: { agentId: agent.id, mailboxMessageId: message.id } });
|
|
119
|
+
ctx.events?.emit?.("crew.mailbox.message", { runId: loaded.manifest.runId, id: message.id, direction: message.direction, from: message.from, to: message.to, taskId: message.taskId, source: "nudge-agent" });
|
|
119
120
|
return result(JSON.stringify({ agentId: agent.id, mailboxMessage: message }, null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
|
|
120
121
|
}
|
|
121
122
|
if (operation === "list-live-agents") {
|
|
@@ -170,6 +171,7 @@ export async function handleApi(params: TeamToolParamsValue, ctx: TeamContext):
|
|
|
170
171
|
return withRunLockSync(loaded.manifest, () => {
|
|
171
172
|
const message = appendMailboxMessage(loaded.manifest, { direction, from, to, body, taskId });
|
|
172
173
|
appendEvent(loaded.manifest.eventsPath, { type: "mailbox.message", runId: loaded.manifest.runId, data: { id: message.id, direction, from, to } });
|
|
174
|
+
ctx.events?.emit?.("crew.mailbox.message", { runId: loaded.manifest.runId, id: message.id, direction, from, to, taskId, source: "send-message" });
|
|
173
175
|
return result(JSON.stringify(message, null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
|
|
174
176
|
});
|
|
175
177
|
} catch (error) {
|
|
@@ -184,6 +186,7 @@ export async function handleApi(params: TeamToolParamsValue, ctx: TeamContext):
|
|
|
184
186
|
return withRunLockSync(loaded.manifest, () => {
|
|
185
187
|
const delivery = acknowledgeMailboxMessage(loaded.manifest, messageId);
|
|
186
188
|
appendEvent(loaded.manifest.eventsPath, { type: "mailbox.acknowledged", runId: loaded.manifest.runId, data: { messageId } });
|
|
189
|
+
ctx.events?.emit?.("crew.mailbox.acknowledged", { runId: loaded.manifest.runId, messageId, delivery });
|
|
187
190
|
return result(JSON.stringify(delivery, null, 2), { action: "api", status: "ok", runId: loaded.manifest.runId, artifactsRoot: loaded.manifest.artifactsRoot });
|
|
188
191
|
});
|
|
189
192
|
} catch (error) {
|
|
@@ -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
|
+
}
|