pi-crew 0.5.5 → 0.5.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +116 -0
- package/README.md +17 -1
- package/docs/architecture.md +2 -0
- package/docs/migration-v0.4-v0.5.md +19 -2
- package/docs/pi-crew-v0.5.5-audit-fix-plan.md +133 -0
- package/package.json +7 -5
- package/src/benchmark/benchmark-runner.ts +45 -0
- package/src/benchmark/feedback-loop.ts +5 -0
- package/src/config/config.ts +10 -0
- package/src/config/suggestions.ts +8 -0
- package/src/extension/async-notifier.ts +10 -1
- package/src/extension/cross-extension-rpc.ts +1 -1
- package/src/extension/notification-router.ts +18 -0
- package/src/extension/register.ts +13 -17
- package/src/extension/registration/subagent-tools.ts +1 -1
- package/src/extension/team-tool/anchor.ts +201 -0
- package/src/extension/team-tool/api.ts +2 -1
- package/src/extension/team-tool/auto-summarize.ts +154 -0
- package/src/extension/team-tool/run.ts +37 -2
- package/src/extension/team-tool.ts +44 -2
- package/src/hooks/registry.ts +1 -3
- package/src/observability/event-bus.ts +13 -4
- package/src/observability/event-to-metric.ts +0 -2
- package/src/runtime/anchor-manager.ts +473 -0
- package/src/runtime/async-runner.ts +8 -4
- package/src/runtime/auto-summarize.ts +350 -0
- package/src/runtime/background-runner.ts +2 -1
- package/src/runtime/budget-tracker.ts +354 -0
- package/src/runtime/chain-runner.ts +507 -0
- package/src/runtime/child-pi.ts +1 -1
- package/src/runtime/crash-recovery.ts +5 -4
- package/src/runtime/custom-tools/irc-tool.ts +13 -0
- package/src/runtime/custom-tools/submit-result-tool.ts +3 -2
- package/src/runtime/delivery-coordinator.ts +10 -3
- package/src/runtime/dynamic-script-runner.ts +482 -0
- package/src/runtime/handoff-manager.ts +589 -0
- package/src/runtime/hidden-handoff.ts +424 -0
- package/src/runtime/live-agent-manager.ts +20 -4
- package/src/runtime/live-session-runtime.ts +39 -4
- package/src/runtime/manifest-cache.ts +2 -1
- package/src/runtime/model-resolver.ts +16 -4
- package/src/runtime/phase-tracker.ts +373 -0
- package/src/runtime/pipeline-runner.ts +514 -0
- package/src/runtime/retry-runner.ts +354 -0
- package/src/runtime/sandbox.ts +252 -0
- package/src/runtime/scheduler.ts +7 -2
- package/src/runtime/subagent-manager.ts +1 -1
- package/src/runtime/task-graph.ts +11 -1
- package/src/runtime/task-runner.ts +1 -1
- package/src/runtime/team-runner.ts +4 -3
- package/src/schema/team-tool-schema.ts +30 -0
- package/src/skills/discover-skills.ts +5 -0
- package/src/state/active-run-registry.ts +9 -2
- package/src/state/contracts.ts +9 -0
- package/src/state/crew-init.ts +3 -3
- package/src/state/decision-ledger.ts +26 -32
- package/src/state/event-log-rotation.ts +2 -2
- package/src/state/event-log.ts +9 -1
- package/src/state/mailbox.ts +10 -0
- package/src/state/run-cache.ts +18 -8
- package/src/tools/safe-bash-extension.ts +1 -0
- package/src/tools/safe-bash.ts +152 -20
- package/src/ui/overlays/mailbox-detail-overlay.ts +13 -2
- package/src/ui/powerbar-publisher.ts +1 -0
- package/src/ui/transcript-cache.ts +13 -0
- package/src/utils/bm25-search.ts +16 -8
- package/src/utils/env-filter.ts +8 -5
- package/src/utils/redaction.ts +169 -15
- package/src/utils/sse-parser.ts +10 -1
- package/src/worktree/cleanup.ts +6 -1
- package/workflows/chain.workflow.md +252 -0
- package/workflows/pipeline.workflow.md +27 -0
|
@@ -860,7 +860,7 @@ export function registerPiTeams(pi: ExtensionAPI): void {
|
|
|
860
860
|
.then(({ stopWatchdog }) => {
|
|
861
861
|
stopWatchdog(runId);
|
|
862
862
|
})
|
|
863
|
-
.catch(() => {});
|
|
863
|
+
.catch((error) => logInternalError("register.foreground-watchdog", error, `runId=${runId}`));
|
|
864
864
|
}
|
|
865
865
|
const ownerCurrent = isContextCurrent(ctx, ownerGeneration);
|
|
866
866
|
if (ctx.hasUI) {
|
|
@@ -943,9 +943,11 @@ export function registerPiTeams(pi: ExtensionAPI): void {
|
|
|
943
943
|
function getPiEvents():
|
|
944
944
|
| Parameters<typeof registerPiCrewRpc>[0]
|
|
945
945
|
| undefined {
|
|
946
|
-
if (pi && typeof pi === "object" && "events" in pi)
|
|
947
|
-
|
|
948
|
-
|
|
946
|
+
if (pi && typeof pi === "object" && "events" in pi) {
|
|
947
|
+
// pi.events may not be typed in the original pi type, so cast through unknown
|
|
948
|
+
const events = (pi as { events?: Parameters<typeof registerPiCrewRpc>[0] }).events;
|
|
949
|
+
return events;
|
|
950
|
+
}
|
|
949
951
|
return undefined;
|
|
950
952
|
}
|
|
951
953
|
rpcHandle = registerPiCrewRpc(getPiEvents(), () => currentCtx);
|
|
@@ -1499,7 +1501,7 @@ export function registerPiTeams(pi: ExtensionAPI): void {
|
|
|
1499
1501
|
// Health notifications: only warn about genuinely running runs
|
|
1500
1502
|
// Filter to only current session's runs to prevent cross-session notification leakage
|
|
1501
1503
|
const currentSessionGen = sessionGeneration;
|
|
1502
|
-
const currentSessionId = currentCtx ? (currentCtx as
|
|
1504
|
+
const currentSessionId = currentCtx ? (currentCtx as { sessionId?: string }).sessionId : undefined;
|
|
1503
1505
|
const sessionManifests = manifests.filter(
|
|
1504
1506
|
(run) =>
|
|
1505
1507
|
!run.ownerSessionId ||
|
|
@@ -1759,19 +1761,13 @@ export function registerPiTeams(pi: ExtensionAPI): void {
|
|
|
1759
1761
|
// AGENTS.md requires confirm=true for management deletes.
|
|
1760
1762
|
pi.on("tool_call", async (event, ctx) => {
|
|
1761
1763
|
if (event.toolName !== "team") return;
|
|
1762
|
-
const
|
|
1763
|
-
if (!
|
|
1764
|
-
const action
|
|
1765
|
-
|
|
1766
|
-
const destructiveActions = new Set([
|
|
1767
|
-
"delete",
|
|
1768
|
-
"forget",
|
|
1769
|
-
"prune",
|
|
1770
|
-
"cleanup",
|
|
1771
|
-
]);
|
|
1764
|
+
const rawInput = event.input;
|
|
1765
|
+
if (!rawInput || typeof rawInput !== "object") return;
|
|
1766
|
+
const input = rawInput as { action?: unknown; confirm?: unknown; force?: unknown };
|
|
1767
|
+
const action = typeof input.action === "string" ? input.action : undefined;
|
|
1768
|
+
const destructiveActions = new Set(["delete", "forget", "prune", "cleanup"]);
|
|
1772
1769
|
if (!action || !destructiveActions.has(action)) return;
|
|
1773
|
-
const forceBypassesReferenceChecks =
|
|
1774
|
-
action === "delete" && input.force === true;
|
|
1770
|
+
const forceBypassesReferenceChecks = action === "delete" && input.force === true;
|
|
1775
1771
|
if (input.confirm === true || forceBypassesReferenceChecks) return;
|
|
1776
1772
|
return {
|
|
1777
1773
|
block: true,
|
|
@@ -56,7 +56,7 @@ export function registerSubagentTools(pi: ExtensionAPI, subagentManager: Subagen
|
|
|
56
56
|
async execute(_id, params, signal, onUpdate, ctx) {
|
|
57
57
|
// Diagnostic: detect pre-aborted signal before spawn
|
|
58
58
|
if (signal?.aborted) {
|
|
59
|
-
logInternalError("subagent-tools.pre-aborted-signal", undefined, `
|
|
59
|
+
logInternalError("subagent-tools.pre-aborted-signal", undefined, `aborted=true paramsKeys=${Object.keys(params as object).join(",")}`);
|
|
60
60
|
return subagentToolResult("Agent tool signal was already aborted before execution started. This usually means Pi cancelled the tool call before it ran.", { action: "agent", status: "error" }, true);
|
|
61
61
|
}
|
|
62
62
|
const currentRole = currentCrewRole();
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Anchor commands for team tool.
|
|
3
|
+
* Provides set/clear/status commands for anchor points.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { TeamToolParamsValue } from "../../schema/team-tool-schema.ts";
|
|
7
|
+
import type { PiTeamsToolResult } from "../tool-result.ts";
|
|
8
|
+
import { result, type TeamContext } from "./context.ts";
|
|
9
|
+
import {
|
|
10
|
+
AnchorManager,
|
|
11
|
+
createAnchorManager,
|
|
12
|
+
AnchorNotFoundError,
|
|
13
|
+
NoHandoffsError,
|
|
14
|
+
} from "../../runtime/anchor-manager.ts";
|
|
15
|
+
import type { HandoffSummary } from "../../runtime/handoff-manager.ts";
|
|
16
|
+
|
|
17
|
+
// Global anchor manager instance for CLI usage
|
|
18
|
+
let globalAnchorManager: AnchorManager | null = null;
|
|
19
|
+
|
|
20
|
+
function getAnchorManager(): AnchorManager {
|
|
21
|
+
if (!globalAnchorManager) {
|
|
22
|
+
globalAnchorManager = createAnchorManager();
|
|
23
|
+
}
|
|
24
|
+
return globalAnchorManager;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Get the session ID from context or generate a default.
|
|
29
|
+
*/
|
|
30
|
+
function getSessionId(ctx: TeamContext): string {
|
|
31
|
+
return ctx.sessionId ?? "default";
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export function handleAnchorSet(
|
|
35
|
+
params: TeamToolParamsValue,
|
|
36
|
+
ctx: TeamContext,
|
|
37
|
+
): PiTeamsToolResult {
|
|
38
|
+
const manager = getAnchorManager();
|
|
39
|
+
const sessionId = getSessionId(ctx);
|
|
40
|
+
const cfg = params.config ?? {};
|
|
41
|
+
|
|
42
|
+
// Parse context from config
|
|
43
|
+
const context: Record<string, unknown> = {};
|
|
44
|
+
if (cfg.context && typeof cfg.context === "object") {
|
|
45
|
+
Object.assign(context, cfg.context as Record<string, unknown>);
|
|
46
|
+
}
|
|
47
|
+
if (cfg.key) {
|
|
48
|
+
// Single key shorthand
|
|
49
|
+
context.key = cfg.key;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const anchorId = manager.setAnchor(sessionId, context);
|
|
53
|
+
|
|
54
|
+
return result(
|
|
55
|
+
[
|
|
56
|
+
`Anchor set successfully.`,
|
|
57
|
+
`Anchor ID: ${anchorId}`,
|
|
58
|
+
`Session: ${sessionId}`,
|
|
59
|
+
context && Object.keys(context).length > 0
|
|
60
|
+
? `Context: ${JSON.stringify(context)}`
|
|
61
|
+
: "",
|
|
62
|
+
].filter(Boolean).join("\n"),
|
|
63
|
+
{ action: "anchor", status: "ok" },
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export function handleAnchorClear(
|
|
68
|
+
params: TeamToolParamsValue,
|
|
69
|
+
ctx: TeamContext,
|
|
70
|
+
): PiTeamsToolResult {
|
|
71
|
+
const manager = getAnchorManager();
|
|
72
|
+
const sessionId = getSessionId(ctx);
|
|
73
|
+
const cfg = params.config ?? {};
|
|
74
|
+
|
|
75
|
+
let anchorId: string | undefined;
|
|
76
|
+
if (cfg.anchorId) {
|
|
77
|
+
anchorId = cfg.anchorId as string;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
let accumulated: HandoffSummary;
|
|
81
|
+
try {
|
|
82
|
+
if (anchorId) {
|
|
83
|
+
accumulated = manager.clearAnchor(anchorId);
|
|
84
|
+
} else {
|
|
85
|
+
const anchorResult = manager.clearAnchorBySession(sessionId);
|
|
86
|
+
if (!anchorResult) {
|
|
87
|
+
return result(
|
|
88
|
+
"No anchor found for this session.",
|
|
89
|
+
{ action: "anchor", status: "error" },
|
|
90
|
+
true,
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
accumulated = anchorResult;
|
|
94
|
+
}
|
|
95
|
+
} catch (error) {
|
|
96
|
+
if (error instanceof AnchorNotFoundError) {
|
|
97
|
+
return result(
|
|
98
|
+
`Anchor not found: ${error.anchorId}`,
|
|
99
|
+
{ action: "anchor", status: "error" },
|
|
100
|
+
true,
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
if (error instanceof NoHandoffsError) {
|
|
104
|
+
return result(
|
|
105
|
+
"No handoffs have been accumulated to this anchor.",
|
|
106
|
+
{ action: "anchor", status: "error" },
|
|
107
|
+
true,
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
throw error;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return result(
|
|
114
|
+
[
|
|
115
|
+
`Anchor cleared successfully.`,
|
|
116
|
+
`Accumulated summary:`,
|
|
117
|
+
``,
|
|
118
|
+
`Task: ${accumulated.task}`,
|
|
119
|
+
`Outcome: ${accumulated.outcome}`,
|
|
120
|
+
``,
|
|
121
|
+
`Metrics:`,
|
|
122
|
+
` Tokens: ${accumulated.metrics.tokensUsed}`,
|
|
123
|
+
` Duration: ${Math.round(accumulated.metrics.duration / 1000)}s`,
|
|
124
|
+
` Iterations: ${accumulated.metrics.iterations}`,
|
|
125
|
+
` Tools: ${accumulated.metrics.toolsUsed.join(", ") || "(none)"}`,
|
|
126
|
+
``,
|
|
127
|
+
`Files created: ${accumulated.filesCreated.join(", ") || "(none)"}`,
|
|
128
|
+
`Files modified: ${accumulated.filesModified.join(", ") || "(none)"}`,
|
|
129
|
+
`Files deleted: ${accumulated.filesDeleted.join(", ") || "(none)"}`,
|
|
130
|
+
accumulated.decisions.length > 0
|
|
131
|
+
? `\nDecisions:\n${accumulated.decisions.map((d: { rationale: string; outcome: string }) => ` - ${d.rationale}: ${d.outcome}`).join("\n")}`
|
|
132
|
+
: "",
|
|
133
|
+
accumulated.blockers.length > 0
|
|
134
|
+
? `\nBlockers: ${accumulated.blockers.join("; ")}`
|
|
135
|
+
: "",
|
|
136
|
+
accumulated.nextSteps.length > 0
|
|
137
|
+
? `\nNext steps: ${accumulated.nextSteps.join("; ")}`
|
|
138
|
+
: "",
|
|
139
|
+
].filter(Boolean).join("\n"),
|
|
140
|
+
{ action: "anchor", status: "ok" },
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export function handleAnchorStatus(
|
|
145
|
+
params: TeamToolParamsValue,
|
|
146
|
+
ctx: TeamContext,
|
|
147
|
+
): PiTeamsToolResult {
|
|
148
|
+
const manager = getAnchorManager();
|
|
149
|
+
const sessionId = getSessionId(ctx);
|
|
150
|
+
const cfg = params.config ?? {};
|
|
151
|
+
|
|
152
|
+
let anchorId: string | undefined;
|
|
153
|
+
if (cfg.anchorId) {
|
|
154
|
+
anchorId = cfg.anchorId as string;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
let status;
|
|
158
|
+
if (anchorId) {
|
|
159
|
+
status = manager.getAnchorStatus(anchorId);
|
|
160
|
+
} else {
|
|
161
|
+
status = manager.getAnchorStatusBySession(sessionId);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if (!status) {
|
|
165
|
+
return result(
|
|
166
|
+
anchorId
|
|
167
|
+
? `No anchor found with ID: ${anchorId}`
|
|
168
|
+
: `No anchor set for session: ${sessionId}`,
|
|
169
|
+
{ action: "anchor", status: "ok" },
|
|
170
|
+
);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return result(
|
|
174
|
+
[
|
|
175
|
+
`Anchor Status`,
|
|
176
|
+
`─────────────`,
|
|
177
|
+
`Anchor ID: ${status.anchorId}`,
|
|
178
|
+
`Session ID: ${status.sessionId}`,
|
|
179
|
+
`Created: ${new Date(status.createdAt).toISOString()}`,
|
|
180
|
+
`Handoffs: ${status.handoffCount}`,
|
|
181
|
+
`Total tokens: ${status.totalTokens}`,
|
|
182
|
+
`Total duration: ${Math.round(status.totalDuration / 1000)}s`,
|
|
183
|
+
status.context && Object.keys(status.context).length > 0
|
|
184
|
+
? `\nContext: ${JSON.stringify(status.context, null, 2)}`
|
|
185
|
+
: "",
|
|
186
|
+
].filter(Boolean).join("\n"),
|
|
187
|
+
{ action: "anchor", status: "ok" },
|
|
188
|
+
);
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export function handleAnchorAccumulate(
|
|
192
|
+
params: TeamToolParamsValue,
|
|
193
|
+
ctx: TeamContext,
|
|
194
|
+
): PiTeamsToolResult {
|
|
195
|
+
// This would be used to manually accumulate a handoff to the current anchor
|
|
196
|
+
// In practice, this is called internally by HandoffManager when anchor is set
|
|
197
|
+
return result(
|
|
198
|
+
"Use handleAnchorSet to set an anchor, then run tasks normally. Handoffs will be accumulated automatically.",
|
|
199
|
+
{ action: "anchor", status: "ok" },
|
|
200
|
+
);
|
|
201
|
+
}
|
|
@@ -18,6 +18,7 @@ import { readForegroundControlStatus, writeForegroundInterruptRequest } from "..
|
|
|
18
18
|
import { followUpLiveAgent, getLiveAgent, listActiveLiveAgents, resumeLiveAgent, steerLiveAgent, stopLiveAgent } from "../../subagents/live/manager.ts";
|
|
19
19
|
import { appendLiveAgentControlRequest } from "../../subagents/live/control.ts";
|
|
20
20
|
import { liveControlRealtimeMessage, publishLiveControlRealtime } from "../../subagents/live/realtime.ts";
|
|
21
|
+
import { logInternalError } from "../../utils/internal-error.ts";
|
|
21
22
|
import { buildCapabilityInventory } from "../../runtime/capability-inventory.ts";
|
|
22
23
|
import { resolveRealContainedPath } from "../../utils/safe-paths.ts";
|
|
23
24
|
import type { PiTeamsToolResult } from "../tool-result.ts";
|
|
@@ -125,7 +126,7 @@ export async function handleApi(params: TeamToolParamsValue, ctx: TeamContext):
|
|
|
125
126
|
saveRunTasks(manifest, tasks);
|
|
126
127
|
appendEvent(manifest.eventsPath, { type: "plan.cancelled", runId: manifest.runId, taskId: approval.planTaskId, message: "Adaptive implementation plan was cancelled.", metadata: { provenance: "api" } });
|
|
127
128
|
manifest = updateRunStatus(manifest, "cancelled", "Plan approval was cancelled.");
|
|
128
|
-
void terminateLiveAgentsForRun(manifest.runId, "cancelled", appendEvent, manifest.eventsPath).catch(() => {});
|
|
129
|
+
void terminateLiveAgentsForRun(manifest.runId, "cancelled", appendEvent, manifest.eventsPath).catch((error) => logInternalError("team-tool.cancel-plan.terminate", error, `runId=${manifest.runId}`));
|
|
129
130
|
return result(JSON.stringify({ planApproval: manifest.planApproval, cancelledTasks: tasks.filter((task) => task.status === "cancelled").map((task) => task.id) }, null, 2), { action: "api", status: "ok", runId: manifest.runId, artifactsRoot: manifest.artifactsRoot });
|
|
130
131
|
});
|
|
131
132
|
} catch (error) {
|
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Auto-summarize commands for team tool.
|
|
3
|
+
* Provides on/off/status commands for auto-summarization.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
import type { TeamToolParamsValue } from "../../schema/team-tool-schema.ts";
|
|
7
|
+
import type { PiTeamsToolResult } from "../tool-result.ts";
|
|
8
|
+
import { result, type TeamContext } from "./context.ts";
|
|
9
|
+
import {
|
|
10
|
+
AutoSummarizeService,
|
|
11
|
+
createAutoSummarizeService,
|
|
12
|
+
DEFAULT_AUTO_SUMMARIZE_CONFIG,
|
|
13
|
+
} from "../../runtime/auto-summarize.ts";
|
|
14
|
+
|
|
15
|
+
// Global auto-summarize service instance for CLI usage
|
|
16
|
+
let globalAutoSummarize: AutoSummarizeService | null = null;
|
|
17
|
+
|
|
18
|
+
function getAutoSummarize(): AutoSummarizeService {
|
|
19
|
+
if (!globalAutoSummarize) {
|
|
20
|
+
globalAutoSummarize = createAutoSummarizeService();
|
|
21
|
+
}
|
|
22
|
+
return globalAutoSummarize;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function handleAutoSummarizeOn(
|
|
26
|
+
params: TeamToolParamsValue,
|
|
27
|
+
ctx: TeamContext,
|
|
28
|
+
): PiTeamsToolResult {
|
|
29
|
+
const service = getAutoSummarize();
|
|
30
|
+
const cfg = params.config ?? {};
|
|
31
|
+
|
|
32
|
+
// Apply config updates if provided
|
|
33
|
+
if (cfg.threshold !== undefined) {
|
|
34
|
+
const threshold = typeof cfg.threshold === "number" ? cfg.threshold : parseInt(String(cfg.threshold), 10);
|
|
35
|
+
if (!isNaN(threshold) && threshold >= 0) {
|
|
36
|
+
service.setThreshold(threshold);
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (cfg.minTools !== undefined) {
|
|
41
|
+
const minTools = typeof cfg.minTools === "number" ? cfg.minTools : parseInt(String(cfg.minTools), 10);
|
|
42
|
+
if (!isNaN(minTools) && minTools >= 0) {
|
|
43
|
+
service.setMinToolsUsed(minTools);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const previousState = service.isEnabled();
|
|
48
|
+
service.enable();
|
|
49
|
+
const config = service.getConfig();
|
|
50
|
+
|
|
51
|
+
return result(
|
|
52
|
+
[
|
|
53
|
+
`Auto-summarize enabled.`,
|
|
54
|
+
``,
|
|
55
|
+
`Configuration:`,
|
|
56
|
+
` Token threshold: ${config.threshold}`,
|
|
57
|
+
` Min tools: ${config.minToolsUsed}`,
|
|
58
|
+
` Collapse context: ${config.collapseContext}`,
|
|
59
|
+
].join("\n"),
|
|
60
|
+
{ action: "auto-summarize", status: "ok" },
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function handleAutoSummarizeOff(
|
|
65
|
+
params: TeamToolParamsValue,
|
|
66
|
+
ctx: TeamContext,
|
|
67
|
+
): PiTeamsToolResult {
|
|
68
|
+
const service = getAutoSummarize();
|
|
69
|
+
|
|
70
|
+
service.disable();
|
|
71
|
+
|
|
72
|
+
return result(
|
|
73
|
+
"Auto-summarize disabled.",
|
|
74
|
+
{ action: "auto-summarize", status: "ok" },
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function handleAutoSummarizeStatus(
|
|
79
|
+
params: TeamToolParamsValue,
|
|
80
|
+
ctx: TeamContext,
|
|
81
|
+
): PiTeamsToolResult {
|
|
82
|
+
const service = getAutoSummarize();
|
|
83
|
+
const config = service.getConfig();
|
|
84
|
+
const isEnabled = service.isEnabled();
|
|
85
|
+
|
|
86
|
+
return result(
|
|
87
|
+
[
|
|
88
|
+
`Auto-summarize Status`,
|
|
89
|
+
`──────────────────`,
|
|
90
|
+
`Enabled: ${isEnabled ? "Yes" : "No"}`,
|
|
91
|
+
``,
|
|
92
|
+
`Configuration:`,
|
|
93
|
+
` Token threshold: ${config.threshold} (default: ${DEFAULT_AUTO_SUMMARIZE_CONFIG.threshold})`,
|
|
94
|
+
` Min tools used: ${config.minToolsUsed} (default: ${DEFAULT_AUTO_SUMMARIZE_CONFIG.minToolsUsed})`,
|
|
95
|
+
` Collapse context: ${config.collapseContext ? "Yes" : "No"} (default: ${DEFAULT_AUTO_SUMMARIZE_CONFIG.collapseContext ? "Yes" : "No"})`,
|
|
96
|
+
``,
|
|
97
|
+
`Triggers:`,
|
|
98
|
+
` - Token count >= ${config.threshold}`,
|
|
99
|
+
` - Tool count >= ${config.minToolsUsed}`,
|
|
100
|
+
` - High token-to-tool ratio (>1000 tokens/tool with 3+ tools)`,
|
|
101
|
+
].join("\n"),
|
|
102
|
+
{ action: "auto-summarize", status: "ok" },
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export function handleAutoSummarizeConfig(
|
|
107
|
+
params: TeamToolParamsValue,
|
|
108
|
+
ctx: TeamContext,
|
|
109
|
+
): PiTeamsToolResult {
|
|
110
|
+
const service = getAutoSummarize();
|
|
111
|
+
const cfg = params.config ?? {};
|
|
112
|
+
|
|
113
|
+
// Parse config options
|
|
114
|
+
const updates: { threshold?: number; minTools?: number; collapseContext?: boolean } = {};
|
|
115
|
+
|
|
116
|
+
if (cfg.threshold !== undefined) {
|
|
117
|
+
const threshold = typeof cfg.threshold === "number" ? cfg.threshold : parseInt(String(cfg.threshold), 10);
|
|
118
|
+
if (!isNaN(threshold) && threshold >= 0) {
|
|
119
|
+
updates.threshold = threshold;
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
if (cfg.minTools !== undefined) {
|
|
124
|
+
const minTools = typeof cfg.minTools === "number" ? cfg.minTools : parseInt(String(cfg.minTools), 10);
|
|
125
|
+
if (!isNaN(minTools) && minTools >= 0) {
|
|
126
|
+
updates.minTools = minTools;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (cfg.collapseContext !== undefined) {
|
|
131
|
+
updates.collapseContext = Boolean(cfg.collapseContext);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
if (Object.keys(updates).length > 0) {
|
|
135
|
+
service.updateConfig(updates);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
const config = service.getConfig();
|
|
139
|
+
|
|
140
|
+
return result(
|
|
141
|
+
[
|
|
142
|
+
`Auto-summarize configuration updated.`,
|
|
143
|
+
``,
|
|
144
|
+
`Current settings:`,
|
|
145
|
+
` Token threshold: ${config.threshold}`,
|
|
146
|
+
` Min tools used: ${config.minToolsUsed}`,
|
|
147
|
+
` Collapse context: ${config.collapseContext ? "Yes" : "No"}`,
|
|
148
|
+
` Enabled: ${config.enabled ? "Yes" : "No"}`,
|
|
149
|
+
].join("\n"),
|
|
150
|
+
{ action: "auto-summarize", status: "ok" },
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
// Re-export for team-tool.ts
|
|
154
|
+
export { createAutoSummarizeService } from "../../runtime/auto-summarize.ts";
|
|
@@ -8,10 +8,12 @@ import { registerActiveRun, unregisterActiveRun } from "../../state/active-run-r
|
|
|
8
8
|
import { createRunManifest, loadRunManifestById, updateRunStatus } from "../../state/state-store.ts";
|
|
9
9
|
import { atomicWriteJson } from "../../state/atomic-write.ts";
|
|
10
10
|
import { validateWorkflowForTeam } from "../../workflows/validate-workflow.ts";
|
|
11
|
+
import { PipelineRunner, type PipelineWorkflow, type PipelineStage } from "../../runtime/pipeline-runner.ts";
|
|
11
12
|
// Heavy runtime — lazy-loaded to avoid 1.4s import cost at extension registration.
|
|
12
13
|
import type { executeTeamRun as ExecuteTeamRunFn } from "../../runtime/team-runner.ts";
|
|
13
14
|
// eslint-disable-next-line @typescript-eslint/no-unused-vars -- type-only import for TS inference
|
|
14
15
|
const _typeCheck: typeof ExecuteTeamRunFn = null as never as typeof ExecuteTeamRunFn;
|
|
16
|
+
import { logInternalError } from "../../utils/internal-error.ts";
|
|
15
17
|
let _cachedExecuteTeamRun: typeof ExecuteTeamRunFn | undefined;
|
|
16
18
|
async function executeTeamRun(...args: Parameters<typeof ExecuteTeamRunFn>): Promise<Awaited<ReturnType<typeof ExecuteTeamRunFn>>> {
|
|
17
19
|
if (!_cachedExecuteTeamRun) {
|
|
@@ -110,6 +112,39 @@ export async function handleRun(params: TeamToolParamsValue, ctx: TeamContext):
|
|
|
110
112
|
if (!baseWorkflow) return result(`Workflow '${workflowName}' not found.`, { action: "run", status: "error" }, true);
|
|
111
113
|
const workflow = directAgent ? baseWorkflow : expandParallelResearchWorkflow(baseWorkflow, ctx.cwd);
|
|
112
114
|
|
|
115
|
+
// Check if this is a pipeline workflow - special handling for multi-stage execution
|
|
116
|
+
const isPipelineWorkflow = workflowName === "pipeline" && !directAgent;
|
|
117
|
+
if (isPipelineWorkflow) {
|
|
118
|
+
// For pipeline workflows, use PipelineRunner for execution
|
|
119
|
+
const pipelineRunner = new PipelineRunner();
|
|
120
|
+
const pipelineWorkflow: PipelineWorkflow = {
|
|
121
|
+
name: workflow.name,
|
|
122
|
+
description: workflow.description,
|
|
123
|
+
goal,
|
|
124
|
+
stages: workflow.steps.map((step) => ({
|
|
125
|
+
name: step.id,
|
|
126
|
+
team: step.role,
|
|
127
|
+
inputs: step.task,
|
|
128
|
+
usePreviousResults: step.dependsOn && step.dependsOn.length > 0,
|
|
129
|
+
})),
|
|
130
|
+
stopOnError: true,
|
|
131
|
+
defaultMaxConcurrency: workflow.maxConcurrency ?? 5,
|
|
132
|
+
};
|
|
133
|
+
|
|
134
|
+
// For now, show pipeline workflow info - full integration would require
|
|
135
|
+
// connecting PipelineRunner to the actual team execution system
|
|
136
|
+
const stageInfo = pipelineWorkflow.stages.map((s) => `- ${s.name} (${s.team})`).join("\n");
|
|
137
|
+
return result([
|
|
138
|
+
`Pipeline workflow: ${workflow.name}`,
|
|
139
|
+
`Goal: ${goal}`,
|
|
140
|
+
`Stages (${pipelineWorkflow.stages.length}):`,
|
|
141
|
+
stageInfo,
|
|
142
|
+
"",
|
|
143
|
+
"Pipeline execution is available via the PipelineRunner API.",
|
|
144
|
+
"Full CLI integration requires connecting to the team execution system.",
|
|
145
|
+
].join("\n"), { action: "run", status: "ok" }, false);
|
|
146
|
+
}
|
|
147
|
+
|
|
113
148
|
const validationErrors = validateWorkflowForTeam(workflow, team);
|
|
114
149
|
if (validationErrors.length > 0) {
|
|
115
150
|
return result([`Workflow '${workflow.name}' is not valid for team '${team.name}':`, ...validationErrors.map((error) => `- ${error}`)].join("\n"), { action: "run", status: "error" }, true);
|
|
@@ -140,7 +175,7 @@ export async function handleRun(params: TeamToolParamsValue, ctx: TeamContext):
|
|
|
140
175
|
const runtimeResolution = runtimeResolutionState(runtime);
|
|
141
176
|
const executionManifest = { ...updatedManifest, runtimeResolution, runConfig: executedConfig, updatedAt: new Date().toISOString() };
|
|
142
177
|
atomicWriteJson(paths.manifestPath, executionManifest);
|
|
143
|
-
appendEventAsync(executionManifest.eventsPath, { type: "runtime.resolved", runId: executionManifest.runId, message: `Runtime resolved: ${runtime.kind} safety=${runtime.safety}`, data: { runtimeResolution } }).catch(() => {});
|
|
178
|
+
appendEventAsync(executionManifest.eventsPath, { type: "runtime.resolved", runId: executionManifest.runId, message: `Runtime resolved: ${runtime.kind} safety=${runtime.safety}`, data: { runtimeResolution } }).catch((error) => logInternalError("team-tool.run.resolved", error, `runId=${executionManifest.runId}`));
|
|
144
179
|
const runAsync = params.async ?? executedConfig.asyncByDefault ?? false;
|
|
145
180
|
let effectiveRuntime = runtime;
|
|
146
181
|
if (runAsync && runtime.kind === "live-session") {
|
|
@@ -150,7 +185,7 @@ export async function handleRun(params: TeamToolParamsValue, ctx: TeamContext):
|
|
|
150
185
|
const effectiveManifest = effectiveRuntime !== runtime ? { ...executionManifest, runtimeResolution: effectiveRuntimeResolution, updatedAt: new Date().toISOString() } : executionManifest;
|
|
151
186
|
if (effectiveRuntime !== runtime) {
|
|
152
187
|
atomicWriteJson(paths.manifestPath, effectiveManifest);
|
|
153
|
-
appendEventAsync(effectiveManifest.eventsPath, { type: "runtime.resolved", runId: effectiveManifest.runId, message: `Runtime overridden: child-process (async fallback from live-session)`, data: { runtimeResolution: effectiveRuntimeResolution } }).catch(() => {});
|
|
188
|
+
appendEventAsync(effectiveManifest.eventsPath, { type: "runtime.resolved", runId: effectiveManifest.runId, message: `Runtime overridden: child-process (async fallback from live-session)`, data: { runtimeResolution: effectiveRuntimeResolution } }).catch((error) => logInternalError("team-tool.run.override", error, `runId=${effectiveManifest.runId}`));
|
|
154
189
|
}
|
|
155
190
|
if (runAsync) {
|
|
156
191
|
if (effectiveRuntime.safety === "blocked") {
|
|
@@ -170,6 +170,8 @@ import { handlePlan } from "./team-tool/plan.ts";
|
|
|
170
170
|
import { handleOrchestrate } from "./team-tool/orchestrate.ts";
|
|
171
171
|
import { handleRespond } from "./team-tool/respond.ts";
|
|
172
172
|
import { handleStatus } from "./team-tool/status.ts";
|
|
173
|
+
import { handleAnchorSet, handleAnchorClear, handleAnchorStatus, handleAnchorAccumulate } from "./team-tool/anchor.ts";
|
|
174
|
+
import { handleAutoSummarizeOn, handleAutoSummarizeOff, handleAutoSummarizeStatus, handleAutoSummarizeConfig, createAutoSummarizeService } from "./team-tool/auto-summarize.ts";
|
|
173
175
|
|
|
174
176
|
export { handleApi } from "./team-tool/api.ts";
|
|
175
177
|
export { handleRetry } from "./team-tool/cancel.ts";
|
|
@@ -715,7 +717,12 @@ export function handleSteer(
|
|
|
715
717
|
true,
|
|
716
718
|
);
|
|
717
719
|
if (!task.pendingSteers) task.pendingSteers = [];
|
|
718
|
-
|
|
720
|
+
// HIGH-04: Cap pendingSteers array to prevent unbounded memory growth
|
|
721
|
+
const MAX_PENDING_STEERS = 100;
|
|
722
|
+
if (task.pendingSteers.length >= MAX_PENDING_STEERS) {
|
|
723
|
+
task.pendingSteers = task.pendingSteers.slice(-(MAX_PENDING_STEERS - 1));
|
|
724
|
+
}
|
|
725
|
+
task.pendingSteers.push(message);
|
|
719
726
|
saveRunTasks(loaded.manifest, loaded.tasks);
|
|
720
727
|
appendEvent(loaded.manifest.eventsPath, {
|
|
721
728
|
type: "task.steer_queued",
|
|
@@ -871,7 +878,7 @@ export async function handleTeamTool(
|
|
|
871
878
|
ctx: TeamContext,
|
|
872
879
|
): Promise<PiTeamsToolResult> {
|
|
873
880
|
const action = params.action ?? "list";
|
|
874
|
-
switch (action) {
|
|
881
|
+
switch (action as string) {
|
|
875
882
|
case "list":
|
|
876
883
|
return handleList(params, ctx);
|
|
877
884
|
case "get":
|
|
@@ -1157,6 +1164,41 @@ export async function handleTeamTool(
|
|
|
1157
1164
|
return handleSchedule(params, ctx);
|
|
1158
1165
|
case "scheduled":
|
|
1159
1166
|
return handleListScheduled(params, ctx);
|
|
1167
|
+
case "anchor": {
|
|
1168
|
+
const subAction = typeof params.config?.subAction === "string" ? params.config.subAction : "status";
|
|
1169
|
+
switch (subAction) {
|
|
1170
|
+
case "set":
|
|
1171
|
+
return handleAnchorSet(params, ctx);
|
|
1172
|
+
case "clear":
|
|
1173
|
+
return handleAnchorClear(params, ctx);
|
|
1174
|
+
case "accumulate":
|
|
1175
|
+
return handleAnchorAccumulate(params, ctx);
|
|
1176
|
+
default:
|
|
1177
|
+
return handleAnchorStatus(params, ctx);
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
case "auto-summarize":
|
|
1181
|
+
case "auto_boomerang": {
|
|
1182
|
+
const subAction = typeof params.config?.subAction === "string" ? params.config.subAction : ((params.action as string) === "auto_boomerang" ? "toggle" : "status");
|
|
1183
|
+
switch (subAction) {
|
|
1184
|
+
case "on":
|
|
1185
|
+
return handleAutoSummarizeOn(params, ctx);
|
|
1186
|
+
case "off":
|
|
1187
|
+
return handleAutoSummarizeOff(params, ctx);
|
|
1188
|
+
case "config":
|
|
1189
|
+
return handleAutoSummarizeConfig(params, ctx);
|
|
1190
|
+
case "toggle": {
|
|
1191
|
+
const service = createAutoSummarizeService();
|
|
1192
|
+
service.toggle();
|
|
1193
|
+
return result(
|
|
1194
|
+
`Auto-summarize ${service.isEnabled() ? "enabled" : "disabled"}.`,
|
|
1195
|
+
{ action: "auto-summarize", status: "ok" },
|
|
1196
|
+
);
|
|
1197
|
+
}
|
|
1198
|
+
default:
|
|
1199
|
+
return handleAutoSummarizeStatus(params, ctx);
|
|
1200
|
+
}
|
|
1201
|
+
}
|
|
1160
1202
|
case "onboard": {
|
|
1161
1203
|
const team = params.team ?? "default";
|
|
1162
1204
|
const onboarding = buildTeamOnboarding(team, ctx.cwd);
|
package/src/hooks/registry.ts
CHANGED
|
@@ -30,9 +30,7 @@ export async function executeHook(name: HookName, ctx: HookContext): Promise<Hoo
|
|
|
30
30
|
// SECURITY: If ctx contains a workspaceId, filter hooks to only those scoped to
|
|
31
31
|
// this workspace. This prevents globally-registered hooks from operating on runs
|
|
32
32
|
// they weren't designed for.
|
|
33
|
-
const scopedHooks = ctx.workspaceId
|
|
34
|
-
? hooks.filter((h) => !h.workspaceId || h.workspaceId === ctx.workspaceId)
|
|
35
|
-
: hooks;
|
|
33
|
+
const scopedHooks = hooks.filter((h) => !h.workspaceId || h.workspaceId === ctx.workspaceId);
|
|
36
34
|
if (scopedHooks.length === 0) return { hookName: name, outcome: "allow", durationMs: 0 };
|
|
37
35
|
const start = Date.now();
|
|
38
36
|
const diagnostics: string[] = [];
|
|
@@ -19,13 +19,22 @@ type CrewEventListener = (event: CrewEvent) => void;
|
|
|
19
19
|
|
|
20
20
|
class EventBus {
|
|
21
21
|
private listeners = new Map<CrewEventType, Set<CrewEventListener>>();
|
|
22
|
-
private static
|
|
22
|
+
private static _instance?: EventBus;
|
|
23
23
|
|
|
24
24
|
static getInstance(): EventBus {
|
|
25
|
-
if (!EventBus.
|
|
26
|
-
EventBus.
|
|
25
|
+
if (!EventBus._instance) {
|
|
26
|
+
EventBus._instance = new EventBus();
|
|
27
27
|
}
|
|
28
|
-
return EventBus.
|
|
28
|
+
return EventBus._instance;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Dispose of the EventBus instance and clear all listeners.
|
|
33
|
+
* Resets the singleton so a new instance can be created.
|
|
34
|
+
*/
|
|
35
|
+
dispose(): void {
|
|
36
|
+
this.listeners.clear();
|
|
37
|
+
EventBus._instance = undefined;
|
|
29
38
|
}
|
|
30
39
|
|
|
31
40
|
emit(event: CrewEvent): void {
|
|
@@ -32,7 +32,6 @@ export function wireEventToMetrics(events: ExtensionAPI["events"] | undefined, r
|
|
|
32
32
|
const retryAttemptCount = registry.counter("crew.task.retry_attempt_total", "Retry attempts by run and task");
|
|
33
33
|
const deadletterCount = registry.counter("crew.task.deadletter_total", "Deadletter triggers by reason");
|
|
34
34
|
const overflowCount = registry.counter("crew.task.overflow_phase_total", "Overflow recovery phase transitions");
|
|
35
|
-
const waitingCount = registry.counter("crew.task.waiting_total", "Tasks entering waiting state");
|
|
36
35
|
const supervisorContactCount = registry.counter("crew.task.supervisor_contact_total", "Supervisor contact requests by reason");
|
|
37
36
|
registry.gauge("crew.heartbeat.staleness_ms", "Heartbeat elapsed since last seen, milliseconds");
|
|
38
37
|
const runDuration = registry.histogram("crew.run.duration_ms", "Run end-to-end duration, milliseconds", [1000, 5000, 15000, 30000, 60000, 300000, 600000, 1800000]);
|
|
@@ -50,7 +49,6 @@ export function wireEventToMetrics(events: ExtensionAPI["events"] | undefined, r
|
|
|
50
49
|
["crew.task.retry_attempt", (data) => { const item = recordValue(data); taskCount.inc({ status: "retry" }); retryAttemptCount.inc({ runId: stringValue(item.runId, "unknown"), taskId: stringValue(item.taskId, "unknown") }); }],
|
|
51
50
|
["crew.task.deadletter", (data) => { const item = recordValue(data); deadletterCount.inc({ reason: stringValue(item.reason, "unknown") }); }],
|
|
52
51
|
["crew.task.overflow", (data) => { const item = recordValue(data); overflowCount.inc({ phase: stringValue(item.phase, "unknown"), previous_phase: stringValue(item.previousPhase, "none") }); }],
|
|
53
|
-
["task.waiting", (data) => { const item = recordValue(data); waitingCount.inc({ taskId: stringValue(item.taskId, "unknown"), runId: stringValue(item.runId, "unknown") }); }],
|
|
54
52
|
["supervisor.contact", (data) => { const item = recordValue(data); supervisorContactCount.inc({ reason: stringValue(item.reason, "unknown"), taskId: stringValue(item.taskId, "unknown") }); }],
|
|
55
53
|
["crew.subagent.completed", (data) => { const item = recordValue(data); subagentCount.inc({ status: stringValue(item.status, "completed") }); }],
|
|
56
54
|
["crew.subagent.failed", () => subagentCount.inc({ status: "failed" })],
|