pi-crew 0.1.41 → 0.1.44
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 +47 -0
- package/README.md +51 -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-phase10-distillation.md +199 -0
- package/docs/research-phase11-distillation.md +201 -0
- package/docs/research-pi-coding-agent.md +357 -357
- package/docs/research-source-pi-crew-reference.md +174 -174
- package/docs/runtime-flow.md +148 -148
- package/docs/source-runtime-refactor-map.md +83 -83
- package/index.ts +6 -6
- package/package.json +1 -1
- package/src/agents/agent-serializer.ts +34 -34
- package/src/agents/discover-agents.ts +5 -4
- package/src/config/config.ts +28 -4
- package/src/extension/cross-extension-rpc.ts +82 -82
- package/src/extension/management.ts +37 -8
- package/src/extension/notification-router.ts +2 -2
- package/src/extension/register.ts +130 -8
- package/src/extension/registration/commands.ts +11 -9
- package/src/extension/registration/compaction-guard.ts +125 -125
- package/src/extension/registration/subagent-tools.ts +28 -19
- package/src/extension/registration/team-tool.ts +2 -1
- package/src/extension/result-watcher.ts +4 -4
- package/src/extension/run-bundle-schema.ts +8 -4
- package/src/extension/run-import.ts +4 -0
- package/src/extension/run-index.ts +23 -1
- package/src/extension/run-maintenance.ts +43 -24
- package/src/extension/team-tool/api.ts +2 -2
- package/src/extension/team-tool/cancel.ts +76 -4
- package/src/extension/team-tool/context.ts +1 -0
- package/src/extension/team-tool/doctor.ts +8 -1
- package/src/extension/team-tool/handle-settings.ts +188 -0
- 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/respond.ts +67 -0
- package/src/extension/team-tool/run.ts +6 -4
- package/src/extension/team-tool/status.ts +99 -93
- package/src/extension/team-tool-types.ts +4 -0
- package/src/extension/team-tool.ts +5 -1
- package/src/i18n.ts +184 -0
- package/src/observability/correlation.ts +2 -2
- package/src/observability/event-to-metric.ts +10 -3
- package/src/observability/exporters/adapter.ts +7 -1
- package/src/observability/exporters/otlp-exporter.ts +14 -2
- package/src/observability/exporters/prometheus-exporter.ts +9 -2
- package/src/observability/metric-registry.ts +18 -3
- package/src/observability/metric-retention.ts +11 -3
- package/src/observability/metric-sink.ts +9 -4
- package/src/observability/metrics-primitives.ts +4 -3
- package/src/prompt/prompt-runtime.ts +72 -68
- package/src/runtime/agent-control.ts +63 -63
- package/src/runtime/agent-memory.ts +72 -72
- package/src/runtime/agent-observability.ts +114 -114
- package/src/runtime/async-marker.ts +26 -26
- package/src/runtime/attention-events.ts +28 -23
- package/src/runtime/background-runner.ts +53 -53
- package/src/runtime/child-pi.ts +4 -4
- package/src/runtime/completion-guard.ts +95 -4
- package/src/runtime/concurrency.ts +1 -1
- package/src/runtime/crash-recovery.ts +32 -1
- package/src/runtime/crew-agent-runtime.ts +59 -58
- package/src/runtime/deadletter.ts +14 -4
- package/src/runtime/delivery-coordinator.ts +143 -0
- 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 +106 -106
- package/src/runtime/heartbeat-gradient.ts +28 -28
- package/src/runtime/heartbeat-watcher.ts +48 -4
- package/src/runtime/live-agent-control.ts +87 -87
- 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 +305 -305
- package/src/runtime/manifest-cache.ts +2 -2
- package/src/runtime/model-fallback.ts +272 -261
- package/src/runtime/overflow-recovery.ts +157 -0
- package/src/runtime/parallel-research.ts +44 -44
- package/src/runtime/parallel-utils.ts +1 -1
- package/src/runtime/pi-json-output.ts +111 -111
- package/src/runtime/policy-engine.ts +79 -78
- package/src/runtime/post-exit-stdio-guard.ts +2 -2
- 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 +5 -0
- package/src/runtime/role-permission.ts +39 -39
- package/src/runtime/runtime-resolver.ts +1 -1
- package/src/runtime/session-resources.ts +25 -0
- package/src/runtime/session-snapshot.ts +59 -0
- package/src/runtime/session-usage.ts +79 -79
- package/src/runtime/sidechain-output.ts +29 -29
- package/src/runtime/stale-reconciler.ts +179 -0
- package/src/runtime/subagent-manager.ts +3 -3
- package/src/runtime/supervisor-contact.ts +59 -0
- package/src/runtime/task-display.ts +38 -38
- package/src/runtime/task-output-context.ts +127 -127
- package/src/runtime/task-runner/live-executor.ts +101 -101
- package/src/runtime/task-runner/progress.ts +119 -111
- package/src/runtime/task-runner/result-utils.ts +14 -14
- package/src/runtime/task-runner/state-helpers.ts +22 -22
- package/src/runtime/task-runner.ts +14 -0
- package/src/runtime/team-runner.ts +9 -10
- package/src/runtime/worker-heartbeat.ts +21 -21
- package/src/runtime/worker-startup.ts +57 -57
- package/src/schema/config-schema.ts +2 -1
- package/src/schema/team-tool-schema.ts +115 -109
- package/src/state/artifact-store.ts +4 -2
- package/src/state/atomic-write.ts +12 -4
- package/src/state/contracts.ts +109 -105
- package/src/state/event-log.ts +3 -4
- package/src/state/jsonl-writer.ts +4 -1
- package/src/state/locks.ts +9 -1
- package/src/state/task-claims.ts +44 -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 +2 -2
- package/src/teams/team-serializer.ts +38 -38
- 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/crew-widget.ts +5 -4
- 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/live-run-sidebar.ts +1 -1
- package/src/ui/loaders.ts +158 -158
- package/src/ui/mascot.ts +3 -2
- package/src/ui/powerbar-publisher.ts +7 -6
- package/src/ui/render-diff.ts +119 -119
- package/src/ui/render-scheduler.ts +54 -14
- package/src/ui/run-dashboard.ts +39 -11
- package/src/ui/run-snapshot-cache.ts +336 -36
- package/src/ui/spinner.ts +17 -17
- package/src/ui/status-colors.ts +58 -54
- package/src/ui/syntax-highlight.ts +116 -116
- package/src/ui/theme-adapter.ts +1 -1
- package/src/ui/transcript-viewer.ts +7 -2
- package/src/utils/atomic-write.ts +33 -0
- package/src/utils/completion-dedupe.ts +63 -63
- package/src/utils/file-coalescer.ts +5 -3
- package/src/utils/frontmatter.ts +68 -36
- package/src/utils/git.ts +262 -262
- package/src/utils/ids.ts +12 -12
- package/src/utils/internal-error.ts +1 -1
- package/src/utils/names.ts +27 -26
- package/src/utils/paths.ts +1 -1
- package/src/utils/redaction.ts +44 -41
- package/src/utils/safe-paths.ts +47 -34
- package/src/utils/sleep.ts +2 -2
- package/src/utils/timings.ts +2 -0
- package/src/utils/visual.ts +9 -1
- package/src/workflows/discover-workflows.ts +4 -1
- package/src/workflows/validate-workflow.ts +40 -40
- package/src/worktree/branch-freshness.ts +45 -45
- package/src/worktree/worktree-manager.ts +6 -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
package/src/config/config.ts
CHANGED
|
@@ -80,6 +80,7 @@ export interface AgentOverrideConfig {
|
|
|
80
80
|
fallbackModels?: string[] | false;
|
|
81
81
|
thinking?: string | false;
|
|
82
82
|
tools?: string[] | false;
|
|
83
|
+
skills?: string[] | false;
|
|
83
84
|
}
|
|
84
85
|
|
|
85
86
|
export interface CrewAgentsConfig {
|
|
@@ -189,6 +190,14 @@ export function projectConfigPath(cwd: string): string {
|
|
|
189
190
|
return path.join(projectCrewRoot(cwd), "config.json");
|
|
190
191
|
}
|
|
191
192
|
|
|
193
|
+
/**
|
|
194
|
+
* Alternative project config path: `.pi/pi-crew.json` in the project root.
|
|
195
|
+
* This is a convenience path alongside the standard `config.json` in crewRoot.
|
|
196
|
+
*/
|
|
197
|
+
export function projectPiCrewJsonPath(cwd: string): string {
|
|
198
|
+
return path.join(cwd, ".pi", "pi-crew.json");
|
|
199
|
+
}
|
|
200
|
+
|
|
192
201
|
function withoutUndefined<T extends Record<string, unknown>>(value: T): Partial<T> {
|
|
193
202
|
return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== undefined)) as Partial<T>;
|
|
194
203
|
}
|
|
@@ -331,7 +340,7 @@ function mergeConfig(base: PiTeamsConfig, override: PiTeamsConfig): PiTeamsConfi
|
|
|
331
340
|
...withoutUndefined((override.agents ?? {}) as Record<string, unknown>),
|
|
332
341
|
overrides: {
|
|
333
342
|
...(base.agents?.overrides ?? {}),
|
|
334
|
-
...(override.agents?.overrides ?? {}),
|
|
343
|
+
...withoutUndefined((override.agents?.overrides ?? {}) as Record<string, unknown>) as Record<string, AgentOverrideConfig>,
|
|
335
344
|
},
|
|
336
345
|
};
|
|
337
346
|
}
|
|
@@ -421,7 +430,7 @@ function parseStringList(value: unknown): string[] | undefined {
|
|
|
421
430
|
|
|
422
431
|
function parseStringArrayOrFalse(value: unknown): string[] | false | undefined {
|
|
423
432
|
if (value === false) return false;
|
|
424
|
-
if (typeof value === "string") return parseStringList(value.split(","));
|
|
433
|
+
if (typeof value === "string") return value.trim() === "" ? [] : parseStringList(value.split(","));
|
|
425
434
|
return parseStringList(value);
|
|
426
435
|
}
|
|
427
436
|
|
|
@@ -536,6 +545,7 @@ function parseAgentOverride(value: unknown): AgentOverrideConfig | undefined {
|
|
|
536
545
|
fallbackModels: parseStringArrayOrFalse(obj.fallbackModels),
|
|
537
546
|
thinking: parseWithSchema(Type.Union([Type.String(), Type.Literal(false)]), obj.thinking),
|
|
538
547
|
tools: parseStringArrayOrFalse(obj.tools),
|
|
548
|
+
skills: parseStringArrayOrFalse(obj.skills),
|
|
539
549
|
};
|
|
540
550
|
return Object.values(override).some((entry) => entry !== undefined) ? override : undefined;
|
|
541
551
|
}
|
|
@@ -649,9 +659,14 @@ function parseReliabilityConfig(value: unknown): CrewReliabilityConfig | undefin
|
|
|
649
659
|
function parseOtlpConfig(value: unknown): CrewOtlpConfig | undefined {
|
|
650
660
|
const obj = asRecord(value);
|
|
651
661
|
if (!obj) return undefined;
|
|
652
|
-
const headers: Record<string, string> =
|
|
662
|
+
const headers: Record<string, string> = Object.create(null);
|
|
653
663
|
const rawHeaders = asRecord(obj.headers);
|
|
654
|
-
if (rawHeaders) for (const [key, entry] of Object.entries(rawHeaders))
|
|
664
|
+
if (rawHeaders) for (const [key, entry] of Object.entries(rawHeaders)) {
|
|
665
|
+
if (typeof entry !== "string") continue;
|
|
666
|
+
// Prevent prototype pollution via __proto__ / constructor / prototype keys.
|
|
667
|
+
if (key === "__proto__" || key === "constructor" || key === "prototype") continue;
|
|
668
|
+
headers[key] = entry;
|
|
669
|
+
}
|
|
655
670
|
const otlp: CrewOtlpConfig = {
|
|
656
671
|
enabled: parseWithSchema(Type.Boolean(), obj.enabled),
|
|
657
672
|
endpoint: parseWithSchema(Type.String({ minLength: 1 }), obj.endpoint),
|
|
@@ -726,6 +741,15 @@ export function loadConfig(cwd?: string): LoadedPiTeamsConfig {
|
|
|
726
741
|
const projectSafeConfig = sanitizeProjectConfig(projectPath, config, projectConfig.config);
|
|
727
742
|
warnings.push(...projectConfig.warnings.map((warning) => `${projectPath}: ${warning}`), ...projectSafeConfig.warnings);
|
|
728
743
|
config = mergeConfig(config, projectSafeConfig.config);
|
|
744
|
+
// Also load .pi/pi-crew.json from project root if it exists
|
|
745
|
+
const piCrewJsonPath = projectPiCrewJsonPath(cwd);
|
|
746
|
+
if (fs.existsSync(piCrewJsonPath)) {
|
|
747
|
+
const piCrewJsonConfig = parseConfigWithWarnings(readConfigRecord(piCrewJsonPath));
|
|
748
|
+
const piCrewJsonSafeConfig = sanitizeProjectConfig(piCrewJsonPath, config, piCrewJsonConfig.config);
|
|
749
|
+
warnings.push(...piCrewJsonConfig.warnings.map((warning) => `${piCrewJsonPath}: ${warning}`), ...piCrewJsonSafeConfig.warnings);
|
|
750
|
+
config = mergeConfig(config, piCrewJsonSafeConfig.config);
|
|
751
|
+
paths.push(piCrewJsonPath);
|
|
752
|
+
}
|
|
729
753
|
}
|
|
730
754
|
return { path: filePath, paths, config, warnings: warnings.length > 0 ? warnings : undefined };
|
|
731
755
|
} catch (error) {
|
|
@@ -1,82 +1,82 @@
|
|
|
1
|
-
import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
2
|
-
import type { TeamToolParamsValue } from "../schema/team-tool-schema.ts";
|
|
3
|
-
import { handleTeamTool } from "./team-tool.ts";
|
|
4
|
-
import { parseLiveControlRealtimeMessage, publishLiveControlRealtime } from "../runtime/live-control-realtime.ts";
|
|
5
|
-
|
|
6
|
-
export interface EventBusLike {
|
|
7
|
-
on(event: string, handler: (data: unknown) => void): (() => void) | void;
|
|
8
|
-
emit(event: string, data: unknown): void;
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export type RpcReply<T = unknown> = { success: true; data?: T } | { success: false; error: string };
|
|
12
|
-
export const PI_CREW_RPC_VERSION = 1;
|
|
13
|
-
|
|
14
|
-
export interface PiCrewRpcHandle {
|
|
15
|
-
unsubscribe(): void;
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
function requestId(raw: unknown): string | undefined {
|
|
19
|
-
return raw && typeof raw === "object" && !Array.isArray(raw) && typeof (raw as { requestId?: unknown }).requestId === "string" ? (raw as { requestId: string }).requestId : undefined;
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
function reply(events: EventBusLike, channel: string, id: string | undefined, payload: RpcReply): void {
|
|
23
|
-
if (!id) return;
|
|
24
|
-
events.emit(`${channel}:reply:${id}`, payload);
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
function textOf(result: Awaited<ReturnType<typeof handleTeamTool>>): string {
|
|
28
|
-
return result.content?.map((item) => item.type === "text" ? item.text : "").join("\n") ?? "";
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
function on(events: EventBusLike, channel: string, handler: (raw: unknown) => void): () => void {
|
|
32
|
-
const unsub = events.on(channel, handler);
|
|
33
|
-
return typeof unsub === "function" ? unsub : () => {};
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
export function registerPiCrewRpc(events: EventBusLike | undefined, getCtx: () => ExtensionContext | undefined): PiCrewRpcHandle | undefined {
|
|
37
|
-
if (!events) return undefined;
|
|
38
|
-
const unsubs = [
|
|
39
|
-
on(events, "pi-crew:rpc:ping", (raw) => reply(events, "pi-crew:rpc:ping", requestId(raw), { success: true, data: { version: PI_CREW_RPC_VERSION } })),
|
|
40
|
-
on(events, "pi-crew:rpc:run", async (raw) => {
|
|
41
|
-
const id = requestId(raw);
|
|
42
|
-
try {
|
|
43
|
-
const ctx = getCtx();
|
|
44
|
-
if (!ctx) throw new Error("No active pi-crew session context.");
|
|
45
|
-
const params: TeamToolParamsValue = raw && typeof raw === "object" && !Array.isArray(raw) ? { ...(raw as object), action: "run" } as TeamToolParamsValue : { action: "run" };
|
|
46
|
-
const result = await handleTeamTool(params, ctx);
|
|
47
|
-
reply(events, "pi-crew:rpc:run", id, result.isError ? { success: false, error: textOf(result) } : { success: true, data: result.details });
|
|
48
|
-
} catch (error) {
|
|
49
|
-
reply(events, "pi-crew:rpc:run", id, { success: false, error: error instanceof Error ? error.message : String(error) });
|
|
50
|
-
}
|
|
51
|
-
}),
|
|
52
|
-
on(events, "pi-crew:rpc:status", async (raw) => {
|
|
53
|
-
const id = requestId(raw);
|
|
54
|
-
try {
|
|
55
|
-
const ctx = getCtx();
|
|
56
|
-
if (!ctx) throw new Error("No active pi-crew session context.");
|
|
57
|
-
const runId = raw && typeof raw === "object" && !Array.isArray(raw) ? (raw as { runId?: string }).runId : undefined;
|
|
58
|
-
const result = await handleTeamTool({ action: "status", runId }, ctx);
|
|
59
|
-
reply(events, "pi-crew:rpc:status", id, result.isError ? { success: false, error: textOf(result) } : { success: true, data: { text: textOf(result), details: result.details } });
|
|
60
|
-
} catch (error) {
|
|
61
|
-
reply(events, "pi-crew:rpc:status", id, { success: false, error: error instanceof Error ? error.message : String(error) });
|
|
62
|
-
}
|
|
63
|
-
}),
|
|
64
|
-
on(events, "pi-crew:live-control", (raw) => {
|
|
65
|
-
const request = parseLiveControlRealtimeMessage(raw);
|
|
66
|
-
if (request) publishLiveControlRealtime(request);
|
|
67
|
-
}),
|
|
68
|
-
on(events, "pi-crew:rpc:live-control", async (raw) => {
|
|
69
|
-
const id = requestId(raw);
|
|
70
|
-
try {
|
|
71
|
-
const ctx = getCtx();
|
|
72
|
-
if (!ctx) throw new Error("No active pi-crew session context.");
|
|
73
|
-
const obj = raw && typeof raw === "object" && !Array.isArray(raw) ? raw as Record<string, unknown> : {};
|
|
74
|
-
const result = await handleTeamTool({ action: "api", runId: typeof obj.runId === "string" ? obj.runId : undefined, config: { operation: typeof obj.operation === "string" ? obj.operation : "steer-agent", agentId: obj.agentId, message: obj.message, prompt: obj.prompt } }, ctx);
|
|
75
|
-
reply(events, "pi-crew:rpc:live-control", id, result.isError ? { success: false, error: textOf(result) } : { success: true, data: { text: textOf(result), details: result.details } });
|
|
76
|
-
} catch (error) {
|
|
77
|
-
reply(events, "pi-crew:rpc:live-control", id, { success: false, error: error instanceof Error ? error.message : String(error) });
|
|
78
|
-
}
|
|
79
|
-
}),
|
|
80
|
-
];
|
|
81
|
-
return { unsubscribe: () => unsubs.forEach((unsub) => unsub()) };
|
|
82
|
-
}
|
|
1
|
+
import type { ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import type { TeamToolParamsValue } from "../schema/team-tool-schema.ts";
|
|
3
|
+
import { handleTeamTool } from "./team-tool.ts";
|
|
4
|
+
import { parseLiveControlRealtimeMessage, publishLiveControlRealtime } from "../runtime/live-control-realtime.ts";
|
|
5
|
+
|
|
6
|
+
export interface EventBusLike {
|
|
7
|
+
on(event: string, handler: (data: unknown) => void): (() => void) | void;
|
|
8
|
+
emit(event: string, data: unknown): void;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export type RpcReply<T = unknown> = { success: true; data?: T } | { success: false; error: string };
|
|
12
|
+
export const PI_CREW_RPC_VERSION = 1;
|
|
13
|
+
|
|
14
|
+
export interface PiCrewRpcHandle {
|
|
15
|
+
unsubscribe(): void;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function requestId(raw: unknown): string | undefined {
|
|
19
|
+
return raw && typeof raw === "object" && !Array.isArray(raw) && typeof (raw as { requestId?: unknown }).requestId === "string" ? (raw as { requestId: string }).requestId : undefined;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function reply(events: EventBusLike, channel: string, id: string | undefined, payload: RpcReply): void {
|
|
23
|
+
if (!id) return;
|
|
24
|
+
events.emit(`${channel}:reply:${id}`, payload);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function textOf(result: Awaited<ReturnType<typeof handleTeamTool>>): string {
|
|
28
|
+
return result.content?.map((item) => item.type === "text" ? item.text : "").join("\n") ?? "";
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function on(events: EventBusLike, channel: string, handler: (raw: unknown) => void): () => void {
|
|
32
|
+
const unsub = events.on(channel, handler);
|
|
33
|
+
return typeof unsub === "function" ? unsub : () => {};
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function registerPiCrewRpc(events: EventBusLike | undefined, getCtx: () => ExtensionContext | undefined): PiCrewRpcHandle | undefined {
|
|
37
|
+
if (!events) return undefined;
|
|
38
|
+
const unsubs = [
|
|
39
|
+
on(events, "pi-crew:rpc:ping", (raw) => reply(events, "pi-crew:rpc:ping", requestId(raw), { success: true, data: { version: PI_CREW_RPC_VERSION } })),
|
|
40
|
+
on(events, "pi-crew:rpc:run", async (raw) => {
|
|
41
|
+
const id = requestId(raw);
|
|
42
|
+
try {
|
|
43
|
+
const ctx = getCtx();
|
|
44
|
+
if (!ctx) throw new Error("No active pi-crew session context.");
|
|
45
|
+
const params: TeamToolParamsValue = raw && typeof raw === "object" && !Array.isArray(raw) ? { ...(raw as object), action: "run" } as TeamToolParamsValue : { action: "run" };
|
|
46
|
+
const result = await handleTeamTool(params, ctx);
|
|
47
|
+
reply(events, "pi-crew:rpc:run", id, result.isError ? { success: false, error: textOf(result) } : { success: true, data: result.details });
|
|
48
|
+
} catch (error) {
|
|
49
|
+
reply(events, "pi-crew:rpc:run", id, { success: false, error: error instanceof Error ? error.message : String(error) });
|
|
50
|
+
}
|
|
51
|
+
}),
|
|
52
|
+
on(events, "pi-crew:rpc:status", async (raw) => {
|
|
53
|
+
const id = requestId(raw);
|
|
54
|
+
try {
|
|
55
|
+
const ctx = getCtx();
|
|
56
|
+
if (!ctx) throw new Error("No active pi-crew session context.");
|
|
57
|
+
const runId = raw && typeof raw === "object" && !Array.isArray(raw) ? (raw as { runId?: string }).runId : undefined;
|
|
58
|
+
const result = await handleTeamTool({ action: "status", runId }, ctx);
|
|
59
|
+
reply(events, "pi-crew:rpc:status", id, result.isError ? { success: false, error: textOf(result) } : { success: true, data: { text: textOf(result), details: result.details } });
|
|
60
|
+
} catch (error) {
|
|
61
|
+
reply(events, "pi-crew:rpc:status", id, { success: false, error: error instanceof Error ? error.message : String(error) });
|
|
62
|
+
}
|
|
63
|
+
}),
|
|
64
|
+
on(events, "pi-crew:live-control", (raw) => {
|
|
65
|
+
const request = parseLiveControlRealtimeMessage(raw);
|
|
66
|
+
if (request) publishLiveControlRealtime(request);
|
|
67
|
+
}),
|
|
68
|
+
on(events, "pi-crew:rpc:live-control", async (raw) => {
|
|
69
|
+
const id = requestId(raw);
|
|
70
|
+
try {
|
|
71
|
+
const ctx = getCtx();
|
|
72
|
+
if (!ctx) throw new Error("No active pi-crew session context.");
|
|
73
|
+
const obj = raw && typeof raw === "object" && !Array.isArray(raw) ? raw as Record<string, unknown> : {};
|
|
74
|
+
const result = await handleTeamTool({ action: "api", runId: typeof obj.runId === "string" ? obj.runId : undefined, config: { operation: typeof obj.operation === "string" ? obj.operation : "steer-agent", agentId: obj.agentId, message: obj.message, prompt: obj.prompt } }, ctx);
|
|
75
|
+
reply(events, "pi-crew:rpc:live-control", id, result.isError ? { success: false, error: textOf(result) } : { success: true, data: { text: textOf(result), details: result.details } });
|
|
76
|
+
} catch (error) {
|
|
77
|
+
reply(events, "pi-crew:rpc:live-control", id, { success: false, error: error instanceof Error ? error.message : String(error) });
|
|
78
|
+
}
|
|
79
|
+
}),
|
|
80
|
+
];
|
|
81
|
+
return { unsubscribe: () => unsubs.forEach((unsub) => unsub()) };
|
|
82
|
+
}
|
|
@@ -41,7 +41,11 @@ function extensionFor(resource: "agent" | "team" | "workflow"): string {
|
|
|
41
41
|
}
|
|
42
42
|
|
|
43
43
|
function backupFile(filePath: string): string {
|
|
44
|
-
|
|
44
|
+
// Include milliseconds and a short random suffix to prevent collision
|
|
45
|
+
// when multiple backups happen within the same second.
|
|
46
|
+
const ts = new Date().toISOString().replace(/[-:.TZ]/g, "");
|
|
47
|
+
const random = Math.random().toString(36).slice(2, 6);
|
|
48
|
+
const backupPath = `${filePath}.bak-${ts.slice(0, 17)}-${random}`;
|
|
45
49
|
fs.copyFileSync(filePath, backupPath);
|
|
46
50
|
return backupPath;
|
|
47
51
|
}
|
|
@@ -83,7 +87,7 @@ function parseRoles(value: unknown): { roles?: TeamRole[]; error?: string } {
|
|
|
83
87
|
agent: sanitizeName(agent.value!),
|
|
84
88
|
description: typeof obj.description === "string" ? obj.description.trim() : undefined,
|
|
85
89
|
model: typeof obj.model === "string" ? obj.model.trim() : undefined,
|
|
86
|
-
maxConcurrency: typeof obj.maxConcurrency === "number" && Number.isInteger(obj.maxConcurrency) ? obj.maxConcurrency : undefined,
|
|
90
|
+
maxConcurrency: typeof obj.maxConcurrency === "number" && Number.isInteger(obj.maxConcurrency) && obj.maxConcurrency > 0 ? obj.maxConcurrency : undefined,
|
|
87
91
|
});
|
|
88
92
|
}
|
|
89
93
|
return { roles };
|
|
@@ -131,6 +135,8 @@ function findResource(ctx: ManagementContext, resource: "agent" | "team" | "work
|
|
|
131
135
|
return allWorkflows(discoverWorkflows(ctx.cwd)).filter(sourceMatches);
|
|
132
136
|
}
|
|
133
137
|
|
|
138
|
+
// Note: only checks agent→team references and defaultWorkflow. Does not detect
|
|
139
|
+
// workflow-step→agent/team references or team name in workflow metadata.
|
|
134
140
|
function findReferences(ctx: ManagementContext, resource: "agent" | "team" | "workflow", name: string): string[] {
|
|
135
141
|
const refs: string[] = [];
|
|
136
142
|
if (resource === "agent") {
|
|
@@ -231,7 +237,7 @@ export function handleCreate(params: TeamToolParamsValue, ctx: ManagementContext
|
|
|
231
237
|
roles: parsedRoles.roles!,
|
|
232
238
|
defaultWorkflow: typeof cfg.defaultWorkflow === "string" ? sanitizeName(cfg.defaultWorkflow) : undefined,
|
|
233
239
|
workspaceMode: cfg.workspaceMode === "worktree" ? "worktree" : "single",
|
|
234
|
-
maxConcurrency: typeof cfg.maxConcurrency === "number" && Number.isInteger(cfg.maxConcurrency) ? cfg.maxConcurrency : undefined,
|
|
240
|
+
maxConcurrency: typeof cfg.maxConcurrency === "number" && Number.isInteger(cfg.maxConcurrency) && cfg.maxConcurrency > 0 ? cfg.maxConcurrency : undefined,
|
|
235
241
|
routing: parseRouting(cfg),
|
|
236
242
|
});
|
|
237
243
|
} else {
|
|
@@ -248,7 +254,11 @@ export function handleCreate(params: TeamToolParamsValue, ctx: ManagementContext
|
|
|
248
254
|
}
|
|
249
255
|
|
|
250
256
|
if (params.dryRun) return result(`[dry-run] Would create ${params.resource} '${name}' at ${filePath}:\n\n${content}`);
|
|
251
|
-
|
|
257
|
+
try {
|
|
258
|
+
fs.writeFileSync(filePath, content, "utf-8");
|
|
259
|
+
} catch (writeError) {
|
|
260
|
+
return result(`Failed to create ${params.resource}: ${writeError instanceof Error ? writeError.message : String(writeError)}`, "error", true);
|
|
261
|
+
}
|
|
252
262
|
return result(`Created ${params.resource} '${name}' at ${filePath}.`);
|
|
253
263
|
}
|
|
254
264
|
|
|
@@ -301,7 +311,7 @@ export function handleUpdate(params: TeamToolParamsValue, ctx: ManagementContext
|
|
|
301
311
|
roles,
|
|
302
312
|
defaultWorkflow: hasOwn(cfg, "defaultWorkflow") ? (typeof cfg.defaultWorkflow === "string" ? sanitizeName(cfg.defaultWorkflow) : undefined) : team.defaultWorkflow,
|
|
303
313
|
workspaceMode: cfg.workspaceMode === "worktree" ? "worktree" : cfg.workspaceMode === "single" ? "single" : team.workspaceMode,
|
|
304
|
-
maxConcurrency: typeof cfg.maxConcurrency === "number" && Number.isInteger(cfg.maxConcurrency) ? cfg.maxConcurrency : team.maxConcurrency,
|
|
314
|
+
maxConcurrency: typeof cfg.maxConcurrency === "number" && Number.isInteger(cfg.maxConcurrency) && cfg.maxConcurrency > 0 ? cfg.maxConcurrency : team.maxConcurrency,
|
|
305
315
|
routing: parseRouting(cfg, team.routing),
|
|
306
316
|
});
|
|
307
317
|
} else {
|
|
@@ -327,8 +337,23 @@ export function handleUpdate(params: TeamToolParamsValue, ctx: ManagementContext
|
|
|
327
337
|
return result([`[dry-run] Would update ${params.resource} at ${current.filePath}:`, "", content, ...(referenceUpdates.length ? ["", "Would update references in:", ...referenceUpdates.map((filePath) => `- ${filePath}`)] : [])].join("\n"));
|
|
328
338
|
}
|
|
329
339
|
const backupPath = backupFile(current.filePath);
|
|
330
|
-
|
|
331
|
-
|
|
340
|
+
try {
|
|
341
|
+
if (nextPath !== current.filePath) {
|
|
342
|
+
try {
|
|
343
|
+
fs.renameSync(current.filePath, nextPath);
|
|
344
|
+
} catch (renameError) {
|
|
345
|
+
if ((renameError as NodeJS.ErrnoException).code === "EXDEV") {
|
|
346
|
+
fs.copyFileSync(current.filePath, nextPath);
|
|
347
|
+
fs.unlinkSync(current.filePath);
|
|
348
|
+
} else {
|
|
349
|
+
throw renameError;
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
fs.writeFileSync(nextPath, content, "utf-8");
|
|
354
|
+
} catch (updateError) {
|
|
355
|
+
return result(`Failed to update ${params.resource}: ${updateError instanceof Error ? updateError.message : String(updateError)}`, "error", true);
|
|
356
|
+
}
|
|
332
357
|
const updatedRefs = params.updateReferences ? updateReferencesForRename(ctx, params.resource!, current.name, nextName, source, false) : [];
|
|
333
358
|
return result([`Updated ${params.resource} at ${nextPath}. Backup: ${backupPath}.`, ...(updatedRefs.length ? ["Updated references:", ...updatedRefs.map((filePath) => `- ${filePath}`)] : [])].join("\n"));
|
|
334
359
|
}
|
|
@@ -343,6 +368,10 @@ export function handleDelete(params: TeamToolParamsValue, ctx: ManagementContext
|
|
|
343
368
|
}
|
|
344
369
|
if (params.dryRun) return result(`[dry-run] Would delete ${params.resource} at ${resolved.resource!.filePath}.${refs.length ? `\nReferences:\n${refs.map((ref) => `- ${ref}`).join("\n")}` : ""}`);
|
|
345
370
|
const backupPath = backupFile(resolved.resource!.filePath);
|
|
346
|
-
|
|
371
|
+
try {
|
|
372
|
+
fs.unlinkSync(resolved.resource!.filePath);
|
|
373
|
+
} catch (deleteError) {
|
|
374
|
+
return result(`Failed to delete ${params.resource}: ${deleteError instanceof Error ? deleteError.message : String(deleteError)}`, "error", true);
|
|
375
|
+
}
|
|
347
376
|
return result(`Deleted ${params.resource} at ${resolved.resource!.filePath}. Backup: ${backupPath}.`);
|
|
348
377
|
}
|
|
@@ -66,8 +66,8 @@ export class NotificationRouter {
|
|
|
66
66
|
const withTime = { ...notification, timestamp: notification.timestamp ?? now };
|
|
67
67
|
try {
|
|
68
68
|
this.opts.sink?.(withTime);
|
|
69
|
-
} catch {
|
|
70
|
-
|
|
69
|
+
} catch (sinkError) {
|
|
70
|
+
process.stderr.write(`[pi-crew] notification-sink: ${sinkError instanceof Error ? sinkError.message : String(sinkError)}\n`);
|
|
71
71
|
}
|
|
72
72
|
const filter = this.opts.severityFilter ?? DEFAULT_SEVERITY_FILTER;
|
|
73
73
|
if (!filter.includes(withTime.severity)) return false;
|
|
@@ -1,4 +1,6 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionContext } from "@mariozechner/pi-coding-agent";
|
|
2
|
+
import * as fs from "node:fs";
|
|
3
|
+
import * as path from "node:path";
|
|
2
4
|
import { loadConfig } from "../config/config.ts";
|
|
3
5
|
import { registerAutonomousPolicy } from "./autonomous-policy.ts";
|
|
4
6
|
import { startAsyncRunNotifier, stopAsyncRunNotifier, type AsyncNotifierState } from "./async-notifier.ts";
|
|
@@ -8,6 +10,7 @@ import { registerPiCrewRpc, type PiCrewRpcHandle } from "./cross-extension-rpc.t
|
|
|
8
10
|
import { stopCrewWidget, updateCrewWidget, type CrewWidgetState } from "../ui/crew-widget.ts";
|
|
9
11
|
import { clearPiCrewPowerbar, registerPiCrewPowerbarSegments, updatePiCrewPowerbar } from "../ui/powerbar-publisher.ts";
|
|
10
12
|
import { loadRunManifestById, updateRunStatus } from "../state/state-store.ts";
|
|
13
|
+
import type { TeamRunManifest } from "../state/types.ts";
|
|
11
14
|
import { terminateActiveChildPiProcesses } from "../subagents/spawn.ts";
|
|
12
15
|
import { SubagentManager } from "../subagents/manager.ts";
|
|
13
16
|
import { __test__subagentSpawnParams, sendAgentWakeUp, sendFollowUp } from "./registration/subagent-helpers.ts";
|
|
@@ -34,10 +37,15 @@ import { OTLPExporter } from "../observability/exporters/otlp-exporter.ts";
|
|
|
34
37
|
import { HeartbeatWatcher } from "../runtime/heartbeat-watcher.ts";
|
|
35
38
|
import { appendDeadletter } from "../runtime/deadletter.ts";
|
|
36
39
|
import { detectInterruptedRuns } from "../runtime/crash-recovery.ts";
|
|
40
|
+
import { DeliveryCoordinator } from "../runtime/delivery-coordinator.ts";
|
|
41
|
+
import { OverflowRecoveryTracker } from "../runtime/overflow-recovery.ts";
|
|
42
|
+
import { tryRegisterSessionCleanup } from "../runtime/session-resources.ts";
|
|
43
|
+
import { initI18n } from "../i18n.ts";
|
|
37
44
|
|
|
38
45
|
export { __test__subagentSpawnParams };
|
|
39
46
|
|
|
40
47
|
export function registerPiTeams(pi: ExtensionAPI): void {
|
|
48
|
+
const disposeI18n = initI18n(pi);
|
|
41
49
|
resetTimings();
|
|
42
50
|
time("register:start");
|
|
43
51
|
const globalStore = globalThis as Record<string, unknown>;
|
|
@@ -81,6 +89,8 @@ export function registerPiTeams(pi: ExtensionAPI): void {
|
|
|
81
89
|
let metricSink: MetricSink | undefined;
|
|
82
90
|
let heartbeatWatcher: HeartbeatWatcher | undefined;
|
|
83
91
|
let otlpExporter: OTLPExporter | undefined;
|
|
92
|
+
let deliveryCoordinator: DeliveryCoordinator | undefined;
|
|
93
|
+
let overflowTracker: OverflowRecoveryTracker | undefined;
|
|
84
94
|
const configureNotifications = (ctx: ExtensionContext): void => {
|
|
85
95
|
notificationRouter?.dispose();
|
|
86
96
|
notificationSink?.dispose();
|
|
@@ -146,6 +156,28 @@ export function registerPiTeams(pi: ExtensionAPI): void {
|
|
|
146
156
|
}
|
|
147
157
|
};
|
|
148
158
|
const autoRecoveryLast = new Map<string, number>();
|
|
159
|
+
const configureDeliveryCoordinator = (): void => {
|
|
160
|
+
deliveryCoordinator?.dispose();
|
|
161
|
+
deliveryCoordinator = undefined;
|
|
162
|
+
overflowTracker?.dispose();
|
|
163
|
+
overflowTracker = undefined;
|
|
164
|
+
deliveryCoordinator = new DeliveryCoordinator({
|
|
165
|
+
emit: (event, data) => { pi.events?.emit?.(event, data); },
|
|
166
|
+
sendFollowUp: (title, body) => { sendFollowUp(pi, [title, body].filter((line): line is string => Boolean(line)).join("\n")); },
|
|
167
|
+
sendWakeUp: (message) => { sendAgentWakeUp(pi, message); },
|
|
168
|
+
});
|
|
169
|
+
overflowTracker = new OverflowRecoveryTracker({
|
|
170
|
+
onPhaseChange: (state, previousPhase) => {
|
|
171
|
+
if (metricRegistry) {
|
|
172
|
+
metricRegistry.counter("crew.task.overflow_recovery_total", "Overflow recovery phase transitions").inc({ phase: state.phase, previous_phase: previousPhase });
|
|
173
|
+
}
|
|
174
|
+
pi.events?.emit?.("crew.task.overflow", { runId: state.runId, taskId: state.taskId, phase: state.phase, previousPhase });
|
|
175
|
+
},
|
|
176
|
+
onTimeout: (state) => {
|
|
177
|
+
notifyOperator({ id: `overflow_timeout_${state.taskId}`, severity: "warning", source: "overflow-recovery", runId: state.runId, title: `Task ${state.taskId} overflow recovery timed out`, body: `Phase: ${state.phase}, compaction_count: ${state.compactionCount}, retry_count: ${state.retryCount}. The task may be stuck.` });
|
|
178
|
+
},
|
|
179
|
+
});
|
|
180
|
+
};
|
|
149
181
|
const notifyOperator = (notification: NotificationDescriptor): void => {
|
|
150
182
|
try {
|
|
151
183
|
notificationRouter?.enqueue(notification);
|
|
@@ -205,6 +237,7 @@ export function registerPiTeams(pi: ExtensionAPI): void {
|
|
|
205
237
|
const foregroundControllers = new Set<AbortController>();
|
|
206
238
|
let liveSidebarRunId: string | undefined;
|
|
207
239
|
let renderScheduler: RenderScheduler | undefined;
|
|
240
|
+
let preloadTimer: ReturnType<typeof setTimeout> | undefined;
|
|
208
241
|
const stopSessionBoundSubagents = (): void => {
|
|
209
242
|
for (const controller of foregroundControllers) controller.abort();
|
|
210
243
|
foregroundControllers.clear();
|
|
@@ -312,6 +345,7 @@ export function registerPiTeams(pi: ExtensionAPI): void {
|
|
|
312
345
|
const cleanupRuntime = (): void => {
|
|
313
346
|
if (cleanedUp) return;
|
|
314
347
|
cleanedUp = true;
|
|
348
|
+
if (preloadTimer) { clearTimeout(preloadTimer); preloadTimer = undefined; }
|
|
315
349
|
stopSessionBoundSubagents();
|
|
316
350
|
stopAsyncRunNotifier(notifierState);
|
|
317
351
|
stopCrewWidget(currentCtx, widgetState, currentCtx ? loadConfig(currentCtx.cwd).config.ui : undefined);
|
|
@@ -326,6 +360,10 @@ export function registerPiTeams(pi: ExtensionAPI): void {
|
|
|
326
360
|
eventMetricSub = undefined;
|
|
327
361
|
otlpExporter = undefined;
|
|
328
362
|
metricRegistry = undefined;
|
|
363
|
+
deliveryCoordinator?.dispose();
|
|
364
|
+
overflowTracker?.dispose();
|
|
365
|
+
deliveryCoordinator = undefined;
|
|
366
|
+
overflowTracker = undefined;
|
|
329
367
|
manifestCache.dispose();
|
|
330
368
|
runSnapshotCache.dispose?.();
|
|
331
369
|
renderScheduler?.dispose();
|
|
@@ -337,6 +375,7 @@ export function registerPiTeams(pi: ExtensionAPI): void {
|
|
|
337
375
|
notificationSink = undefined;
|
|
338
376
|
rpcHandle?.unsubscribe();
|
|
339
377
|
rpcHandle = undefined;
|
|
378
|
+
disposeI18n();
|
|
340
379
|
sessionGeneration += 1;
|
|
341
380
|
currentCtx = undefined;
|
|
342
381
|
if (globalStore[runtimeCleanupStoreKey] === cleanupRuntime) delete globalStore[runtimeCleanupStoreKey];
|
|
@@ -357,16 +396,68 @@ export function registerPiTeams(pi: ExtensionAPI): void {
|
|
|
357
396
|
autoRecoveryLast.clear();
|
|
358
397
|
configureNotifications(ctx);
|
|
359
398
|
configureObservability(ctx);
|
|
399
|
+
configureDeliveryCoordinator();
|
|
400
|
+
const sessionId = (ctx as unknown as Record<string, unknown>).sessionId;
|
|
401
|
+
if (typeof sessionId === "string" && sessionId) deliveryCoordinator?.activate(sessionId);
|
|
402
|
+
tryRegisterSessionCleanup(pi, () => { terminateActiveChildPiProcesses(); cleanupRuntime(); });
|
|
360
403
|
registerPiCrewPowerbarSegments(pi.events, loadedConfig.config.ui);
|
|
361
404
|
startAsyncRunNotifier(ctx, notifierState, loadedConfig.config.notifierIntervalMs ?? DEFAULT_UI.notifierIntervalMs, { generation: ownerGeneration, isCurrent: (generation) => generation === sessionGeneration && currentCtx === ctx && !cleanedUp });
|
|
362
405
|
const cache = getManifestCache(ctx.cwd);
|
|
363
406
|
updateCrewWidget(ctx, widgetState, loadedConfig.config.ui, cache, getRunSnapshotCache(ctx.cwd));
|
|
364
407
|
updatePiCrewPowerbar(pi.events, ctx.cwd, loadedConfig.config.ui, cache, getRunSnapshotCache(ctx.cwd), ctx, widgetState.notificationCount ?? 0);
|
|
365
408
|
renderScheduler?.dispose();
|
|
409
|
+
// Phase 12: Async preloading — renderTick reads only a pre-computed frame
|
|
410
|
+
// from memory (zero fs I/O). Background preload refreshes the frame async.
|
|
411
|
+
let preloading = false;
|
|
412
|
+
|
|
413
|
+
let lastPreloadedConfig: ReturnType<typeof loadConfig> | undefined;
|
|
414
|
+
let lastPreloadedManifests: TeamRunManifest[] = [];
|
|
415
|
+
let lastFrameManifestCache: ReturnType<typeof createManifestCache> | undefined;
|
|
416
|
+
let lastFrameSnapshotCache: ReturnType<typeof createRunSnapshotCache> | undefined;
|
|
417
|
+
|
|
418
|
+
const buildFrame = async (): Promise<boolean> => {
|
|
419
|
+
if (!currentCtx) return false;
|
|
420
|
+
lastPreloadedConfig = loadConfig(currentCtx.cwd);
|
|
421
|
+
lastFrameManifestCache = getManifestCache(currentCtx.cwd);
|
|
422
|
+
lastFrameSnapshotCache = getRunSnapshotCache(currentCtx.cwd);
|
|
423
|
+
const manifests = lastFrameManifestCache.list(20);
|
|
424
|
+
lastPreloadedManifests = manifests;
|
|
425
|
+
const runIds = manifests.map((r) => r.runId);
|
|
426
|
+
await lastFrameSnapshotCache.preloadAllStale(runIds);
|
|
427
|
+
return true;
|
|
428
|
+
};
|
|
429
|
+
|
|
430
|
+
const backgroundPreload = (): void => {
|
|
431
|
+
if (!currentCtx || preloading) return;
|
|
432
|
+
preloading = true;
|
|
433
|
+
buildFrame()
|
|
434
|
+
.then((ok) => {
|
|
435
|
+
preloading = false;
|
|
436
|
+
if (ok) renderScheduler?.schedule();
|
|
437
|
+
})
|
|
438
|
+
.catch((error: unknown) => {
|
|
439
|
+
preloading = false;
|
|
440
|
+
logInternalError("register.backgroundPreload", error);
|
|
441
|
+
});
|
|
442
|
+
};
|
|
443
|
+
|
|
444
|
+
const startPreloadLoop = (intervalMs: number): void => {
|
|
445
|
+
if (preloadTimer) clearTimeout(preloadTimer);
|
|
446
|
+
const tick = (): void => {
|
|
447
|
+
backgroundPreload();
|
|
448
|
+
preloadTimer = setTimeout(tick, intervalMs);
|
|
449
|
+
preloadTimer.unref();
|
|
450
|
+
};
|
|
451
|
+
preloadTimer = setTimeout(tick, intervalMs);
|
|
452
|
+
preloadTimer.unref();
|
|
453
|
+
};
|
|
454
|
+
|
|
366
455
|
const renderTick = (): void => {
|
|
367
456
|
if (!currentCtx) return;
|
|
368
|
-
const config =
|
|
369
|
-
const activeCache = getManifestCache(currentCtx.cwd);
|
|
457
|
+
const config = lastPreloadedConfig?.config.ui;
|
|
458
|
+
const activeCache = lastFrameManifestCache ?? getManifestCache(currentCtx.cwd);
|
|
459
|
+
const snapshotCache = lastFrameSnapshotCache ?? getRunSnapshotCache(currentCtx.cwd);
|
|
460
|
+
const manifests = lastPreloadedManifests.length > 0 ? lastPreloadedManifests : activeCache.list(20);
|
|
370
461
|
if (liveSidebarRunId) {
|
|
371
462
|
const placement = config?.widgetPlacement ?? "aboveEditor";
|
|
372
463
|
if (widgetState.lastVisibility !== "hidden" || widgetState.lastPlacement !== placement) {
|
|
@@ -379,13 +470,18 @@ export function registerPiTeams(pi: ExtensionAPI): void {
|
|
|
379
470
|
}
|
|
380
471
|
requestRender(currentCtx);
|
|
381
472
|
} else {
|
|
382
|
-
updateCrewWidget(currentCtx, widgetState, config, activeCache,
|
|
473
|
+
updateCrewWidget(currentCtx, widgetState, config, activeCache, snapshotCache, manifests);
|
|
383
474
|
}
|
|
384
|
-
updatePiCrewPowerbar(pi.events, currentCtx.cwd, config, activeCache,
|
|
475
|
+
updatePiCrewPowerbar(pi.events, currentCtx.cwd, config, activeCache, snapshotCache, currentCtx, widgetState.notificationCount ?? 0, manifests);
|
|
476
|
+
// Health notifications: only warn about genuinely running runs
|
|
385
477
|
const now = Date.now();
|
|
386
|
-
for (const run of
|
|
478
|
+
for (const run of manifests) {
|
|
479
|
+
if (run.status !== "running") continue;
|
|
387
480
|
try {
|
|
388
|
-
const snapshot =
|
|
481
|
+
const snapshot = snapshotCache.get(run.runId);
|
|
482
|
+
if (!snapshot) continue;
|
|
483
|
+
// Skip if snapshot shows run already completed/failed (stale cache)
|
|
484
|
+
if (snapshot.manifest.status !== "running") continue;
|
|
389
485
|
const summary = summarizeHeartbeats(snapshot, { now });
|
|
390
486
|
const maybeNotifyHealth = (kind: string, count: number, title: string, body: string): void => {
|
|
391
487
|
if (count <= 0) return;
|
|
@@ -402,18 +498,40 @@ export function registerPiTeams(pi: ExtensionAPI): void {
|
|
|
402
498
|
}
|
|
403
499
|
}
|
|
404
500
|
};
|
|
501
|
+
|
|
502
|
+
const fallbackMs = loadedConfig.config.ui?.dashboardLiveRefreshMs ?? 250;
|
|
405
503
|
renderScheduler = new RenderScheduler(pi.events, renderTick, {
|
|
406
|
-
fallbackMs
|
|
504
|
+
fallbackMs,
|
|
407
505
|
onInvalidate: () => getRunSnapshotCache(ctx.cwd).invalidate(),
|
|
408
506
|
});
|
|
507
|
+
// Start async preload loop — refreshes snapshot cache in background
|
|
508
|
+
startPreloadLoop(fallbackMs);
|
|
409
509
|
});
|
|
410
510
|
pi.on("session_before_switch", () => {
|
|
411
511
|
sessionGeneration++;
|
|
512
|
+
// Phase 11b: Capture state before session switch
|
|
513
|
+
const pendingCount = deliveryCoordinator?.getPendingCount() ?? 0;
|
|
514
|
+
if (pendingCount > 0) {
|
|
515
|
+
logInternalError("register.session-before-switch", `Switching session with ${pendingCount} pending deliveries`);
|
|
516
|
+
}
|
|
517
|
+
deliveryCoordinator?.deactivate();
|
|
412
518
|
stopAsyncRunNotifier(notifierState);
|
|
413
519
|
stopSessionBoundSubagents();
|
|
414
520
|
});
|
|
415
521
|
pi.on("session_shutdown", () => cleanupRuntime());
|
|
416
522
|
|
|
523
|
+
// Phase 11a: Dynamic resource discovery — inject pi-crew skill paths.
|
|
524
|
+
try {
|
|
525
|
+
pi.on("resources_discover", () => {
|
|
526
|
+
const skillDir = path.resolve(process.cwd(), "skills");
|
|
527
|
+
const extSkillDir = path.resolve(__dirname, "..", "..", "skills");
|
|
528
|
+
const paths: string[] = [];
|
|
529
|
+
if (fs.existsSync(extSkillDir)) paths.push(extSkillDir);
|
|
530
|
+
if (skillDir !== extSkillDir && fs.existsSync(skillDir)) paths.push(skillDir);
|
|
531
|
+
return paths.length > 0 ? { skillPaths: paths } : {};
|
|
532
|
+
});
|
|
533
|
+
} catch { /* older Pi without resources_discover */ }
|
|
534
|
+
|
|
417
535
|
registerCompactionGuard(pi, { foregroundControllers });
|
|
418
536
|
|
|
419
537
|
// Phase 1.4: Permission gate for destructive team actions.
|
|
@@ -432,7 +550,11 @@ export function registerPiTeams(pi: ExtensionAPI): void {
|
|
|
432
550
|
};
|
|
433
551
|
});
|
|
434
552
|
|
|
435
|
-
registerTeamTool(pi, { foregroundControllers, startForegroundRun, openLiveSidebar, getManifestCache, getRunSnapshotCache, getMetricRegistry: () => metricRegistry, widgetState
|
|
553
|
+
registerTeamTool(pi, { foregroundControllers, startForegroundRun, openLiveSidebar, getManifestCache, getRunSnapshotCache, getMetricRegistry: () => metricRegistry, widgetState, onJsonEvent: (taskId, runId, event) => {
|
|
554
|
+
const record = event as Record<string, unknown>;
|
|
555
|
+
const eventType = typeof record.type === "string" ? record.type : undefined;
|
|
556
|
+
if (eventType) overflowTracker?.feedEvent(taskId, runId, eventType);
|
|
557
|
+
} });
|
|
436
558
|
registerSubagentTools(pi, subagentManager, { ownerSessionGeneration: captureSessionGeneration });
|
|
437
559
|
time("register.tools");
|
|
438
560
|
|
|
@@ -74,6 +74,7 @@ async function handleMailboxDashboardAction(ctx: ExtensionCommandContext, runId:
|
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
function depsNotify(ctx: ExtensionCommandContext, message: string, level: "info" | "warning" | "error"): void {
|
|
77
|
+
if (!ctx.hasUI) return;
|
|
77
78
|
ctx.ui.notify(message, level);
|
|
78
79
|
}
|
|
79
80
|
|
|
@@ -200,14 +201,15 @@ export function registerTeamCommands(pi: ExtensionAPI, deps: RegisterTeamCommand
|
|
|
200
201
|
await notifyCommandResult(ctx, commandText(result));
|
|
201
202
|
} });
|
|
202
203
|
|
|
203
|
-
pi.registerCommand("team-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
204
|
+
pi.registerCommand("team-settings", {
|
|
205
|
+
description: "View or update pi-crew settings: [list|get <key>|set <key> <value>|unset <key>|path|scope]",
|
|
206
|
+
handler: async (args: string, ctx: ExtensionCommandContext) => {
|
|
207
|
+
const result = await handleTeamTool({ action: "settings", config: { args: args.trim() } }, ctx);
|
|
208
|
+
await notifyCommandResult(ctx, commandText(result));
|
|
209
|
+
},
|
|
210
|
+
});
|
|
209
211
|
|
|
210
|
-
pi.registerCommand("team-
|
|
212
|
+
pi.registerCommand("team-cleanup", { description: "Open a simple pi-crew interactive manager", handler: handleTeamManagerCommand });
|
|
211
213
|
|
|
212
214
|
pi.registerCommand("team-result", { description: "Open a pi-crew agent result viewer: <runId> [taskId]", handler: async (args: string, ctx: ExtensionCommandContext) => {
|
|
213
215
|
const [runId, rawTaskId] = args.trim().split(/\s+/).filter(Boolean);
|
|
@@ -215,8 +217,8 @@ export function registerTeamCommands(pi: ExtensionAPI, deps: RegisterTeamCommand
|
|
|
215
217
|
const loaded = selected ? loadRunManifestById(ctx.cwd, selected.runId) : undefined;
|
|
216
218
|
if (ctx.hasUI && loaded) {
|
|
217
219
|
const agent = readCrewAgents(loaded.manifest).find((item) => item.taskId === selected?.taskId || item.id === selected?.taskId) ?? readCrewAgents(loaded.manifest)[0];
|
|
218
|
-
const
|
|
219
|
-
await ctx.ui.custom<undefined>((_tui, theme, _keybindings, done) => new DurableTextViewer("pi-crew result", `${selected
|
|
220
|
+
const resultText = agent?.resultArtifactPath ? commandText(await handleTeamTool({ action: "api", runId: selected?.runId ?? "", config: { operation: "read-agent-output", agentId: agent.taskId, maxBytes: 64_000 } }, ctx)) : "(no result)";
|
|
221
|
+
await ctx.ui.custom<undefined>((_tui, theme, _keybindings, done) => new DurableTextViewer("pi-crew result", `${selected?.runId ?? ""}:${agent?.taskId ?? "unknown"}`, resultText.split(/\r?\n/), theme, done), { overlay: true, overlayOptions: { width: "90%", maxHeight: "85%", anchor: "center" } });
|
|
220
222
|
return;
|
|
221
223
|
}
|
|
222
224
|
const result = await handleTeamTool({ action: "api", runId, config: { operation: "read-agent-output", agentId: rawTaskId, maxBytes: 64_000 } }, ctx);
|