pi-crew 0.1.10 → 0.1.12
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/README.md +17 -9
- package/docs/architecture.md +2 -2
- package/docs/usage.md +19 -9
- package/package.json +1 -1
- package/schema.json +10 -0
- package/src/config/config.ts +25 -0
- package/src/extension/help.ts +1 -1
- package/src/extension/register.ts +32 -8
- package/src/extension/run-maintenance.ts +1 -1
- package/src/extension/team-tool.ts +11 -4
- package/src/runtime/child-pi.ts +29 -1
- package/src/runtime/process-status.ts +8 -1
- package/src/runtime/runtime-resolver.ts +7 -8
- package/src/ui/crew-widget.ts +97 -33
- package/src/ui/powerbar-publisher.ts +55 -0
- package/src/ui/run-dashboard.ts +26 -9
- package/src/ui/transcript-viewer.ts +219 -204
package/README.md
CHANGED
|
@@ -32,7 +32,7 @@ Current highlights:
|
|
|
32
32
|
- detached async/background runner
|
|
33
33
|
- stale async PID detection
|
|
34
34
|
- active run summary and async completion notifications in Pi sessions
|
|
35
|
-
-
|
|
35
|
+
- real child Pi worker execution by default, with explicit scaffold/dry-run opt-out
|
|
36
36
|
- child Pi JSON output parsing for final text, usage, and event counts
|
|
37
37
|
- retryable model fallback attempts per task
|
|
38
38
|
- aggregate usage totals in status/summary
|
|
@@ -89,19 +89,21 @@ npm run ci
|
|
|
89
89
|
|
|
90
90
|
## Runtime safety model
|
|
91
91
|
|
|
92
|
-
By default, `run`
|
|
92
|
+
By default, `run` launches each crew task as a separate child Pi process. This matches the subagent model from `pi-subagents`: the parent session orchestrates while worker sessions execute independently and stream durable output back to run state.
|
|
93
93
|
|
|
94
|
-
|
|
94
|
+
Use scaffold/dry-run mode only when you explicitly want prompts/artifacts without launching workers:
|
|
95
95
|
|
|
96
|
-
```
|
|
97
|
-
|
|
96
|
+
```json
|
|
97
|
+
{
|
|
98
|
+
"runtime": { "mode": "scaffold" }
|
|
99
|
+
}
|
|
98
100
|
```
|
|
99
101
|
|
|
100
|
-
or
|
|
102
|
+
or disable workers globally:
|
|
101
103
|
|
|
102
104
|
```json
|
|
103
105
|
{
|
|
104
|
-
"executeWorkers":
|
|
106
|
+
"executeWorkers": false
|
|
105
107
|
}
|
|
106
108
|
```
|
|
107
109
|
|
|
@@ -143,7 +145,7 @@ Supported config:
|
|
|
143
145
|
```json
|
|
144
146
|
{
|
|
145
147
|
"asyncByDefault": false,
|
|
146
|
-
"executeWorkers":
|
|
148
|
+
"executeWorkers": true,
|
|
147
149
|
"notifierIntervalMs": 5000,
|
|
148
150
|
"requireCleanWorktreeLeader": true,
|
|
149
151
|
"autonomous": {
|
|
@@ -163,6 +165,11 @@ Supported config:
|
|
|
163
165
|
"maxRunMinutes": 60,
|
|
164
166
|
"maxRetriesPerTask": 1,
|
|
165
167
|
"heartbeatStaleMs": 60000
|
|
168
|
+
},
|
|
169
|
+
"ui": {
|
|
170
|
+
"widgetPlacement": "aboveEditor",
|
|
171
|
+
"widgetMaxLines": 8,
|
|
172
|
+
"powerbar": true
|
|
166
173
|
}
|
|
167
174
|
}
|
|
168
175
|
```
|
|
@@ -619,7 +626,8 @@ Optional child Pi smoke check is explicit only:
|
|
|
619
626
|
## Environment variables
|
|
620
627
|
|
|
621
628
|
```text
|
|
622
|
-
|
|
629
|
+
PI_CREW_EXECUTE_WORKERS=0 disable child workers and use scaffold/dry-run mode
|
|
630
|
+
PI_TEAMS_EXECUTE_WORKERS=0 legacy disable flag
|
|
623
631
|
PI_TEAMS_MOCK_CHILD_PI=success test/mock child worker success
|
|
624
632
|
PI_TEAMS_MOCK_CHILD_PI=json-success
|
|
625
633
|
PI_TEAMS_MOCK_CHILD_PI=retryable-failure
|
package/docs/architecture.md
CHANGED
|
@@ -32,8 +32,8 @@ Implemented now:
|
|
|
32
32
|
- durable run manifests and task files
|
|
33
33
|
- workflow validation
|
|
34
34
|
- foreground workflow scheduler
|
|
35
|
-
-
|
|
36
|
-
-
|
|
35
|
+
- child Pi task execution by default
|
|
36
|
+
- explicit scaffold/dry-run task execution via `runtime.mode=scaffold` or `executeWorkers=false`
|
|
37
37
|
- safety-first create/update/delete for agents, teams, and workflows
|
|
38
38
|
- backups for update/delete mutations
|
|
39
39
|
- dry-run support for management mutations
|
package/docs/usage.md
CHANGED
|
@@ -19,7 +19,7 @@ Supported fields:
|
|
|
19
19
|
```json
|
|
20
20
|
{
|
|
21
21
|
"asyncByDefault": false,
|
|
22
|
-
"executeWorkers":
|
|
22
|
+
"executeWorkers": true,
|
|
23
23
|
"notifierIntervalMs": 5000,
|
|
24
24
|
"requireCleanWorktreeLeader": true,
|
|
25
25
|
"autonomous": {
|
|
@@ -28,6 +28,11 @@ Supported fields:
|
|
|
28
28
|
"injectPolicy": true,
|
|
29
29
|
"preferAsyncForLongTasks": false,
|
|
30
30
|
"allowWorktreeSuggestion": true
|
|
31
|
+
},
|
|
32
|
+
"ui": {
|
|
33
|
+
"widgetPlacement": "aboveEditor",
|
|
34
|
+
"widgetMaxLines": 8,
|
|
35
|
+
"powerbar": true
|
|
31
36
|
}
|
|
32
37
|
}
|
|
33
38
|
```
|
|
@@ -47,9 +52,9 @@ Then open Pi and run:
|
|
|
47
52
|
/team-autonomy status
|
|
48
53
|
```
|
|
49
54
|
|
|
50
|
-
##
|
|
55
|
+
## Default run: real worker execution
|
|
51
56
|
|
|
52
|
-
By default, `pi-crew`
|
|
57
|
+
By default, `pi-crew` launches each task as a separate child Pi worker process. The parent Pi session orchestrates; workers execute independently and stream output to durable run state.
|
|
53
58
|
|
|
54
59
|
```json
|
|
55
60
|
{
|
|
@@ -59,16 +64,21 @@ By default, `pi-crew` does not launch child workers. It creates a durable run, p
|
|
|
59
64
|
}
|
|
60
65
|
```
|
|
61
66
|
|
|
62
|
-
##
|
|
67
|
+
## Scaffold / dry run
|
|
63
68
|
|
|
64
|
-
|
|
69
|
+
Use scaffold mode only when you want durable prompts/artifacts without launching child workers.
|
|
65
70
|
|
|
66
|
-
```
|
|
67
|
-
|
|
71
|
+
```json
|
|
72
|
+
{
|
|
73
|
+
"action": "run",
|
|
74
|
+
"team": "default",
|
|
75
|
+
"goal": "Plan only",
|
|
76
|
+
"config": {
|
|
77
|
+
"runtime": { "mode": "scaffold" }
|
|
78
|
+
}
|
|
79
|
+
}
|
|
68
80
|
```
|
|
69
81
|
|
|
70
|
-
Then run normally. Each task can spawn a child Pi worker.
|
|
71
|
-
|
|
72
82
|
## Async run
|
|
73
83
|
|
|
74
84
|
```json
|
package/package.json
CHANGED
package/schema.json
CHANGED
|
@@ -78,6 +78,16 @@
|
|
|
78
78
|
"enabled": { "type": "boolean" },
|
|
79
79
|
"needsAttentionAfterMs": { "type": "integer", "minimum": 1 }
|
|
80
80
|
}
|
|
81
|
+
},
|
|
82
|
+
"ui": {
|
|
83
|
+
"type": "object",
|
|
84
|
+
"additionalProperties": false,
|
|
85
|
+
"description": "Pi UI settings for the crew widget, dashboard, and optional powerbar segments.",
|
|
86
|
+
"properties": {
|
|
87
|
+
"widgetPlacement": { "type": "string", "enum": ["aboveEditor", "belowEditor"] },
|
|
88
|
+
"widgetMaxLines": { "type": "integer", "minimum": 1, "maximum": 50 },
|
|
89
|
+
"powerbar": { "type": "boolean" }
|
|
90
|
+
}
|
|
81
91
|
}
|
|
82
92
|
}
|
|
83
93
|
}
|
package/src/config/config.ts
CHANGED
|
@@ -47,6 +47,12 @@ export interface CrewWorktreeConfig {
|
|
|
47
47
|
linkNodeModules?: boolean;
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
export interface CrewUiConfig {
|
|
51
|
+
widgetPlacement?: "aboveEditor" | "belowEditor";
|
|
52
|
+
widgetMaxLines?: number;
|
|
53
|
+
powerbar?: boolean;
|
|
54
|
+
}
|
|
55
|
+
|
|
50
56
|
export interface AgentOverrideConfig {
|
|
51
57
|
disabled?: boolean;
|
|
52
58
|
model?: string | false;
|
|
@@ -71,6 +77,7 @@ export interface PiTeamsConfig {
|
|
|
71
77
|
control?: CrewControlConfig;
|
|
72
78
|
worktree?: CrewWorktreeConfig;
|
|
73
79
|
agents?: CrewAgentsConfig;
|
|
80
|
+
ui?: CrewUiConfig;
|
|
74
81
|
}
|
|
75
82
|
|
|
76
83
|
export interface LoadedPiTeamsConfig {
|
|
@@ -136,6 +143,12 @@ function mergeConfig(base: PiTeamsConfig, override: PiTeamsConfig): PiTeamsConfi
|
|
|
136
143
|
...withoutUndefined((override.worktree ?? {}) as Record<string, unknown>),
|
|
137
144
|
};
|
|
138
145
|
}
|
|
146
|
+
if (base.ui || override.ui) {
|
|
147
|
+
merged.ui = {
|
|
148
|
+
...(base.ui ?? {}),
|
|
149
|
+
...withoutUndefined((override.ui ?? {}) as Record<string, unknown>),
|
|
150
|
+
};
|
|
151
|
+
}
|
|
139
152
|
if (base.agents || override.agents) {
|
|
140
153
|
merged.agents = {
|
|
141
154
|
...(base.agents ?? {}),
|
|
@@ -289,6 +302,17 @@ function parseAgentOverride(value: unknown): AgentOverrideConfig | undefined {
|
|
|
289
302
|
return Object.values(override).some((entry) => entry !== undefined) ? override : undefined;
|
|
290
303
|
}
|
|
291
304
|
|
|
305
|
+
function parseUiConfig(value: unknown): CrewUiConfig | undefined {
|
|
306
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
307
|
+
const obj = value as Record<string, unknown>;
|
|
308
|
+
const ui: CrewUiConfig = {
|
|
309
|
+
widgetPlacement: obj.widgetPlacement === "aboveEditor" || obj.widgetPlacement === "belowEditor" ? obj.widgetPlacement : undefined,
|
|
310
|
+
widgetMaxLines: parsePositiveInteger(obj.widgetMaxLines, 50),
|
|
311
|
+
powerbar: typeof obj.powerbar === "boolean" ? obj.powerbar : undefined,
|
|
312
|
+
};
|
|
313
|
+
return Object.values(ui).some((entry) => entry !== undefined) ? ui : undefined;
|
|
314
|
+
}
|
|
315
|
+
|
|
292
316
|
function parseAgentsConfig(value: unknown): CrewAgentsConfig | undefined {
|
|
293
317
|
if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
|
|
294
318
|
const obj = value as Record<string, unknown>;
|
|
@@ -320,6 +344,7 @@ function parseConfig(raw: unknown): PiTeamsConfig {
|
|
|
320
344
|
control: parseControlConfig(obj.control),
|
|
321
345
|
worktree: parseWorktreeConfig(obj.worktree),
|
|
322
346
|
agents: parseAgentsConfig(obj.agents),
|
|
347
|
+
ui: parseUiConfig(obj.ui),
|
|
323
348
|
};
|
|
324
349
|
}
|
|
325
350
|
|
package/src/extension/help.ts
CHANGED
|
@@ -40,6 +40,6 @@ export function piTeamsHelp(): string {
|
|
|
40
40
|
"- /team-validate",
|
|
41
41
|
"- /team-help",
|
|
42
42
|
"",
|
|
43
|
-
"Real child workers are
|
|
43
|
+
"Real child workers are enabled by default. Use runtime.mode=scaffold or executeWorkers=false only for dry runs.",
|
|
44
44
|
].join("\n");
|
|
45
45
|
}
|
|
@@ -11,9 +11,11 @@ import { listRuns } from "./run-index.ts";
|
|
|
11
11
|
import { RunDashboard, type RunDashboardSelection } from "../ui/run-dashboard.ts";
|
|
12
12
|
import { registerPiCrewRpc, type PiCrewRpcHandle } from "./cross-extension-rpc.ts";
|
|
13
13
|
import { stopCrewWidget, updateCrewWidget, type CrewWidgetState } from "../ui/crew-widget.ts";
|
|
14
|
+
import { clearPiCrewPowerbar, registerPiCrewPowerbarSegments, updatePiCrewPowerbar } from "../ui/powerbar-publisher.ts";
|
|
14
15
|
import { DurableTextViewer, DurableTranscriptViewer } from "../ui/transcript-viewer.ts";
|
|
15
16
|
import { loadRunManifestById } from "../state/state-store.ts";
|
|
16
17
|
import { readCrewAgents } from "../runtime/crew-agent-records.ts";
|
|
18
|
+
import { terminateActiveChildPiProcesses } from "../runtime/child-pi.ts";
|
|
17
19
|
|
|
18
20
|
function parseRunArgs(args: string): TeamToolParamsValue {
|
|
19
21
|
const tokens = args.match(/"[^"]*"|'[^']*'|\S+/g)?.map((token) => token.replace(/^['"]|['"]$/g, "")) ?? [];
|
|
@@ -102,6 +104,7 @@ export function registerPiTeams(pi: ExtensionAPI): void {
|
|
|
102
104
|
let currentCtx: ExtensionContext | undefined;
|
|
103
105
|
let rpcHandle: PiCrewRpcHandle | undefined;
|
|
104
106
|
const widgetState: CrewWidgetState = { frame: 0 };
|
|
107
|
+
const foregroundControllers = new Set<AbortController>();
|
|
105
108
|
registerAutonomousPolicy(pi);
|
|
106
109
|
rpcHandle = registerPiCrewRpc((pi as unknown as { events?: Parameters<typeof registerPiCrewRpc>[0] }).events, () => currentCtx);
|
|
107
110
|
|
|
@@ -109,14 +112,24 @@ export function registerPiTeams(pi: ExtensionAPI): void {
|
|
|
109
112
|
currentCtx = ctx;
|
|
110
113
|
notifyActiveRuns(ctx);
|
|
111
114
|
const loadedConfig = loadConfig(ctx.cwd);
|
|
115
|
+
registerPiCrewPowerbarSegments(pi.events, loadedConfig.config.ui);
|
|
112
116
|
startAsyncRunNotifier(ctx, notifierState, loadedConfig.config.notifierIntervalMs ?? 5000);
|
|
113
|
-
updateCrewWidget(ctx, widgetState);
|
|
114
|
-
|
|
117
|
+
updateCrewWidget(ctx, widgetState, loadedConfig.config.ui);
|
|
118
|
+
updatePiCrewPowerbar(pi.events, ctx.cwd, loadedConfig.config.ui);
|
|
119
|
+
widgetState.interval = setInterval(() => {
|
|
120
|
+
if (!currentCtx) return;
|
|
121
|
+
const config = loadConfig(currentCtx.cwd).config.ui;
|
|
122
|
+
updateCrewWidget(currentCtx, widgetState, config);
|
|
123
|
+
updatePiCrewPowerbar(pi.events, currentCtx.cwd, config);
|
|
124
|
+
}, 1000);
|
|
115
125
|
widgetState.interval.unref?.();
|
|
116
126
|
});
|
|
117
127
|
pi.on("session_shutdown", () => {
|
|
128
|
+
for (const controller of foregroundControllers) controller.abort();
|
|
129
|
+
terminateActiveChildPiProcesses();
|
|
118
130
|
stopAsyncRunNotifier(notifierState);
|
|
119
|
-
stopCrewWidget(currentCtx, widgetState);
|
|
131
|
+
stopCrewWidget(currentCtx, widgetState, currentCtx ? loadConfig(currentCtx.cwd).config.ui : undefined);
|
|
132
|
+
clearPiCrewPowerbar(pi.events);
|
|
120
133
|
currentCtx = undefined;
|
|
121
134
|
rpcHandle?.unsubscribe();
|
|
122
135
|
rpcHandle = undefined;
|
|
@@ -128,10 +141,21 @@ export function registerPiTeams(pi: ExtensionAPI): void {
|
|
|
128
141
|
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.",
|
|
129
142
|
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.",
|
|
130
143
|
parameters: TeamToolParams as never,
|
|
131
|
-
async execute(_id, params,
|
|
132
|
-
const
|
|
133
|
-
|
|
134
|
-
|
|
144
|
+
async execute(_id, params, signal, _onUpdate, ctx) {
|
|
145
|
+
const controller = new AbortController();
|
|
146
|
+
foregroundControllers.add(controller);
|
|
147
|
+
const abort = (): void => controller.abort();
|
|
148
|
+
signal?.addEventListener("abort", abort, { once: true });
|
|
149
|
+
try {
|
|
150
|
+
const output = await handleTeamTool(params as TeamToolParamsValue, { ...ctx, signal: controller.signal });
|
|
151
|
+
const config = loadConfig(ctx.cwd).config.ui;
|
|
152
|
+
updateCrewWidget(ctx, widgetState, config);
|
|
153
|
+
updatePiCrewPowerbar(pi.events, ctx.cwd, config);
|
|
154
|
+
return output;
|
|
155
|
+
} finally {
|
|
156
|
+
signal?.removeEventListener("abort", abort);
|
|
157
|
+
foregroundControllers.delete(controller);
|
|
158
|
+
}
|
|
135
159
|
},
|
|
136
160
|
};
|
|
137
161
|
|
|
@@ -323,7 +347,7 @@ export function registerPiTeams(pi: ExtensionAPI): void {
|
|
|
323
347
|
handler: async (_args: string, ctx: ExtensionCommandContext) => {
|
|
324
348
|
for (;;) {
|
|
325
349
|
const runs = listRuns(ctx.cwd).slice(0, 50);
|
|
326
|
-
const selection = await ctx.ui.custom<RunDashboardSelection | undefined>((_tui,
|
|
350
|
+
const selection = await ctx.ui.custom<RunDashboardSelection | undefined>((_tui, theme, _keybindings, done) => new RunDashboard(runs, done, theme), {
|
|
327
351
|
overlay: true,
|
|
328
352
|
overlayOptions: { width: "90%", maxHeight: "80%", anchor: "center" },
|
|
329
353
|
});
|
|
@@ -12,7 +12,7 @@ function isFinished(run: TeamRunManifest): boolean {
|
|
|
12
12
|
}
|
|
13
13
|
|
|
14
14
|
export function pruneFinishedRuns(cwd: string, keep: number): PruneRunsResult {
|
|
15
|
-
const finished = listRuns(cwd).filter(isFinished).sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
|
|
15
|
+
const finished = listRuns(cwd).filter((run) => run.cwd === cwd && isFinished(run)).sort((a, b) => b.updatedAt.localeCompare(a.updatedAt));
|
|
16
16
|
const kept = finished.slice(0, keep).map((run) => run.runId);
|
|
17
17
|
const removed: string[] = [];
|
|
18
18
|
for (const run of finished.slice(keep)) {
|
|
@@ -56,6 +56,7 @@ type TeamContext = Pick<ExtensionContext, "cwd"> & Partial<Pick<ExtensionContext
|
|
|
56
56
|
modelRegistry?: unknown;
|
|
57
57
|
sessionManager?: { getBranch?: () => unknown[] };
|
|
58
58
|
events?: { emit?: (event: string, data: unknown) => void };
|
|
59
|
+
signal?: AbortSignal;
|
|
59
60
|
};
|
|
60
61
|
|
|
61
62
|
function result(text: string, details: TeamToolDetails, isError = false): PiTeamsToolResult {
|
|
@@ -306,7 +307,7 @@ export async function handleRun(params: TeamToolParamsValue, ctx: TeamContext):
|
|
|
306
307
|
const runtime = await resolveCrewRuntime(effectiveRunConfig(loadedConfig.config, params.config));
|
|
307
308
|
const executeWorkers = runtime.kind === "child-process";
|
|
308
309
|
const executedConfig = effectiveRunConfig(loadedConfig.config, params.config);
|
|
309
|
-
const executed = await executeTeamRun({ manifest: updatedManifest, tasks, team, workflow, agents, executeWorkers, limits: executedConfig.limits, runtime, runtimeConfig: executedConfig.runtime, parentContext: buildParentContext(ctx), parentModel: ctx.model, modelRegistry: ctx.modelRegistry });
|
|
310
|
+
const executed = await executeTeamRun({ manifest: updatedManifest, tasks, team, workflow, agents, executeWorkers, limits: executedConfig.limits, runtime, runtimeConfig: executedConfig.runtime, parentContext: buildParentContext(ctx), parentModel: ctx.model, modelRegistry: ctx.modelRegistry, signal: ctx.signal });
|
|
310
311
|
const text = [
|
|
311
312
|
`Created pi-crew run ${executed.manifest.runId}.`,
|
|
312
313
|
`Team: ${team.name}`,
|
|
@@ -318,10 +319,10 @@ export async function handleRun(params: TeamToolParamsValue, ctx: TeamContext):
|
|
|
318
319
|
"",
|
|
319
320
|
`Runtime: ${runtime.kind}${runtime.fallback ? ` (fallback from ${runtime.requestedMode})` : ""}${runtime.reason ? ` - ${runtime.reason}` : ""}`,
|
|
320
321
|
runtime.kind === "child-process"
|
|
321
|
-
? "Child Pi worker execution
|
|
322
|
+
? "Child Pi worker execution is enabled by default; each task is launched as a separate Pi process. Set runtime.mode=scaffold or executeWorkers=false only for dry runs."
|
|
322
323
|
: runtime.kind === "live-session"
|
|
323
324
|
? "Experimental live-session worker execution was enabled."
|
|
324
|
-
: "Safe scaffold mode: child Pi workers were not launched
|
|
325
|
+
: "Safe scaffold mode: child Pi workers were not launched because runtime.mode=scaffold or executeWorkers=false was configured.",
|
|
325
326
|
].join("\n");
|
|
326
327
|
return result(text, { action: "run", status: executed.manifest.status === "failed" ? "error" : "ok", runId: executed.manifest.runId, artifactsRoot: executed.manifest.artifactsRoot }, executed.manifest.status === "failed");
|
|
327
328
|
}
|
|
@@ -429,7 +430,7 @@ export async function handleResume(params: TeamToolParamsValue, ctx: TeamContext
|
|
|
429
430
|
const loadedConfig = loadConfig(ctx.cwd);
|
|
430
431
|
const runtime = await resolveCrewRuntime(loadedConfig.config);
|
|
431
432
|
const executeWorkers = runtime.kind === "child-process";
|
|
432
|
-
const executed = await executeTeamRun({ manifest: loaded.manifest, tasks: resetTasks, team, workflow, agents: allAgents(discoverAgents(ctx.cwd)), executeWorkers, limits: loadedConfig.config.limits, runtime, runtimeConfig: loadedConfig.config.runtime, parentContext: buildParentContext(ctx), parentModel: ctx.model, modelRegistry: ctx.modelRegistry });
|
|
433
|
+
const executed = await executeTeamRun({ manifest: loaded.manifest, tasks: resetTasks, team, workflow, agents: allAgents(discoverAgents(ctx.cwd)), executeWorkers, limits: loadedConfig.config.limits, runtime, runtimeConfig: loadedConfig.config.runtime, parentContext: buildParentContext(ctx), parentModel: ctx.model, modelRegistry: ctx.modelRegistry, signal: ctx.signal });
|
|
433
434
|
return result([`Resumed run ${executed.manifest.runId}.`, `Status: ${executed.manifest.status}`, `Tasks: ${executed.tasks.length}`, `Artifacts: ${executed.manifest.artifactsRoot}`].join("\n"), { action: "resume", status: executed.manifest.status === "failed" ? "error" : "ok", runId: executed.manifest.runId, artifactsRoot: executed.manifest.artifactsRoot }, executed.manifest.status === "failed");
|
|
434
435
|
});
|
|
435
436
|
}
|
|
@@ -508,6 +509,7 @@ function configPatchFromConfig(config: unknown): PiTeamsConfig {
|
|
|
508
509
|
const runtime = configRecord(cfg.runtime);
|
|
509
510
|
const limits = configRecord(cfg.limits);
|
|
510
511
|
const worktree = configRecord(cfg.worktree);
|
|
512
|
+
const ui = configRecord(cfg.ui);
|
|
511
513
|
return {
|
|
512
514
|
asyncByDefault: typeof cfg.asyncByDefault === "boolean" ? cfg.asyncByDefault : undefined,
|
|
513
515
|
executeWorkers: typeof cfg.executeWorkers === "boolean" ? cfg.executeWorkers : undefined,
|
|
@@ -542,6 +544,11 @@ function configPatchFromConfig(config: unknown): PiTeamsConfig {
|
|
|
542
544
|
enabled: typeof control.enabled === "boolean" ? control.enabled : undefined,
|
|
543
545
|
needsAttentionAfterMs: typeof control.needsAttentionAfterMs === "number" && Number.isInteger(control.needsAttentionAfterMs) && control.needsAttentionAfterMs > 0 ? control.needsAttentionAfterMs : undefined,
|
|
544
546
|
} : undefined,
|
|
547
|
+
ui: Object.keys(ui).length > 0 ? {
|
|
548
|
+
widgetPlacement: ui.widgetPlacement === "aboveEditor" || ui.widgetPlacement === "belowEditor" ? ui.widgetPlacement : undefined,
|
|
549
|
+
widgetMaxLines: typeof ui.widgetMaxLines === "number" && Number.isInteger(ui.widgetMaxLines) && ui.widgetMaxLines > 0 ? ui.widgetMaxLines : undefined,
|
|
550
|
+
powerbar: typeof ui.powerbar === "boolean" ? ui.powerbar : undefined,
|
|
551
|
+
} : undefined,
|
|
545
552
|
};
|
|
546
553
|
}
|
|
547
554
|
|
package/src/runtime/child-pi.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { spawn } from "node:child_process";
|
|
1
|
+
import { spawn, type ChildProcess } from "node:child_process";
|
|
2
2
|
import * as fs from "node:fs";
|
|
3
3
|
import * as path from "node:path";
|
|
4
4
|
import type { AgentConfig } from "../agents/agent-config.ts";
|
|
@@ -8,6 +8,29 @@ import { getPiSpawnCommand } from "./pi-spawn.ts";
|
|
|
8
8
|
const POST_EXIT_STDIO_GUARD_MS = 3000;
|
|
9
9
|
const FINAL_DRAIN_MS = 5000;
|
|
10
10
|
const HARD_KILL_MS = 3000;
|
|
11
|
+
const activeChildProcesses = new Map<number, ChildProcess>();
|
|
12
|
+
|
|
13
|
+
function killProcessTree(pid: number | undefined): void {
|
|
14
|
+
if (!pid || !Number.isInteger(pid) || pid <= 0) return;
|
|
15
|
+
try {
|
|
16
|
+
if (process.platform === "win32") {
|
|
17
|
+
spawn("taskkill", ["/pid", String(pid), "/t", "/f"], { stdio: "ignore", windowsHide: true });
|
|
18
|
+
return;
|
|
19
|
+
}
|
|
20
|
+
try { process.kill(-pid, "SIGTERM"); } catch { process.kill(pid, "SIGTERM"); }
|
|
21
|
+
setTimeout(() => {
|
|
22
|
+
try { process.kill(-pid, "SIGKILL"); } catch { try { process.kill(pid, "SIGKILL"); } catch {} }
|
|
23
|
+
}, HARD_KILL_MS).unref?.();
|
|
24
|
+
} catch {
|
|
25
|
+
// Ignore shutdown races.
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export function terminateActiveChildPiProcesses(): number {
|
|
30
|
+
const pids = [...activeChildProcesses.keys()];
|
|
31
|
+
for (const pid of pids) killProcessTree(pid);
|
|
32
|
+
return pids.length;
|
|
33
|
+
}
|
|
11
34
|
|
|
12
35
|
export interface ChildPiRunInput {
|
|
13
36
|
cwd: string;
|
|
@@ -118,7 +141,9 @@ export async function runChildPi(input: ChildPiRunInput): Promise<ChildPiRunResu
|
|
|
118
141
|
cwd: input.cwd,
|
|
119
142
|
env: { ...process.env, ...built.env },
|
|
120
143
|
stdio: ["ignore", "pipe", "pipe"],
|
|
144
|
+
detached: process.platform !== "win32",
|
|
121
145
|
});
|
|
146
|
+
if (child.pid) activeChildProcesses.set(child.pid, child);
|
|
122
147
|
let stdout = "";
|
|
123
148
|
let stderr = "";
|
|
124
149
|
let settled = false;
|
|
@@ -167,6 +192,7 @@ export async function runChildPi(input: ChildPiRunInput): Promise<ChildPiRunResu
|
|
|
167
192
|
};
|
|
168
193
|
|
|
169
194
|
const abort = (): void => {
|
|
195
|
+
killProcessTree(child.pid);
|
|
170
196
|
try {
|
|
171
197
|
child.kill(process.platform === "win32" ? undefined : "SIGTERM");
|
|
172
198
|
} catch {
|
|
@@ -187,6 +213,7 @@ export async function runChildPi(input: ChildPiRunInput): Promise<ChildPiRunResu
|
|
|
187
213
|
settle({ exitCode: null, stdout, stderr, error: error.message });
|
|
188
214
|
});
|
|
189
215
|
child.on("exit", () => {
|
|
216
|
+
if (child.pid) activeChildProcesses.delete(child.pid);
|
|
190
217
|
childExited = true;
|
|
191
218
|
clearFinalDrainTimers();
|
|
192
219
|
postExitGuard = setTimeout(() => {
|
|
@@ -196,6 +223,7 @@ export async function runChildPi(input: ChildPiRunInput): Promise<ChildPiRunResu
|
|
|
196
223
|
postExitGuard.unref?.();
|
|
197
224
|
});
|
|
198
225
|
child.on("close", (exitCode) => {
|
|
226
|
+
if (child.pid) activeChildProcesses.delete(child.pid);
|
|
199
227
|
settle({ exitCode, stdout, stderr, ...(forcedFinalDrain && !stderr.trim() ? { error: `Child Pi did not exit within ${finalDrainMs}ms after final assistant message; termination was requested.` } : {}) });
|
|
200
228
|
});
|
|
201
229
|
});
|
|
@@ -38,6 +38,13 @@ export function isLikelyOrphanedActiveRun(run: TeamRunManifest, agents: CrewAgen
|
|
|
38
38
|
return agents.every((agent) => agent.status === "queued" && !agent.completedAt && !agent.progress);
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
function hasDurableActiveAgentEvidence(agent: CrewAgentRecord): boolean {
|
|
42
|
+
if (agent.status !== "running" && agent.status !== "queued") return false;
|
|
43
|
+
return Boolean(agent.statusPath || agent.eventsPath || agent.outputPath || agent.progress || agent.toolUses || agent.jsonEvents);
|
|
44
|
+
}
|
|
45
|
+
|
|
41
46
|
export function isDisplayActiveRun(run: TeamRunManifest, agents: CrewAgentRecord[] = [], now = Date.now()): boolean {
|
|
42
|
-
|
|
47
|
+
if (!isActiveRunStatus(run.status) || isLikelyOrphanedActiveRun(run, agents, now)) return false;
|
|
48
|
+
if (agents.length === 0) return true;
|
|
49
|
+
return agents.some(hasDurableActiveAgentEvidence);
|
|
43
50
|
}
|
|
@@ -48,22 +48,21 @@ export async function isLiveSessionRuntimeAvailable(timeoutMs = 1500, env: NodeJ
|
|
|
48
48
|
|
|
49
49
|
export async function resolveCrewRuntime(config: PiTeamsConfig, env: NodeJS.ProcessEnv = process.env): Promise<CrewRuntimeCapabilities> {
|
|
50
50
|
const requestedMode = config.runtime?.mode ?? "auto";
|
|
51
|
-
const
|
|
51
|
+
const workersDisabled = config.executeWorkers === false || env.PI_CREW_EXECUTE_WORKERS === "0" || env.PI_TEAMS_EXECUTE_WORKERS === "0";
|
|
52
52
|
if (requestedMode === "scaffold") return scaffoldCaps(requestedMode);
|
|
53
|
-
if (
|
|
53
|
+
if (workersDisabled) return scaffoldCaps(requestedMode, "Child worker execution disabled by config/env. Set runtime.mode=scaffold or executeWorkers=false only for dry runs.");
|
|
54
|
+
if (requestedMode === "child-process") return childCaps(requestedMode);
|
|
54
55
|
if (requestedMode === "live-session" || (requestedMode === "auto" && config.runtime?.preferLiveSession === true)) {
|
|
55
56
|
const live = await isLiveSessionRuntimeAvailable(1500, env);
|
|
56
57
|
if (live.available) return liveCaps(requestedMode);
|
|
57
58
|
if (requestedMode === "live-session" && config.runtime?.allowChildProcessFallback === false) return { ...scaffoldCaps(requestedMode), available: false, reason: live.reason };
|
|
58
|
-
|
|
59
|
-
return { ...scaffoldCaps(requestedMode), fallback: "scaffold", reason: live.reason };
|
|
59
|
+
return { ...childCaps(requestedMode), fallback: "child-process", reason: live.reason };
|
|
60
60
|
}
|
|
61
|
-
|
|
62
|
-
return scaffoldCaps(requestedMode);
|
|
61
|
+
return childCaps(requestedMode);
|
|
63
62
|
}
|
|
64
63
|
|
|
65
|
-
function scaffoldCaps(requestedMode: CrewRuntimeMode): CrewRuntimeCapabilities {
|
|
66
|
-
return { kind: "scaffold", requestedMode, available: true, steer: false, resume: false, liveToolActivity: false, transcript: false };
|
|
64
|
+
function scaffoldCaps(requestedMode: CrewRuntimeMode, reason?: string): CrewRuntimeCapabilities {
|
|
65
|
+
return { kind: "scaffold", requestedMode, available: true, steer: false, resume: false, liveToolActivity: false, transcript: false, ...(reason ? { reason } : {}) };
|
|
67
66
|
}
|
|
68
67
|
|
|
69
68
|
function childCaps(requestedMode: CrewRuntimeMode, reason?: string): CrewRuntimeCapabilities {
|