pi-crew 0.11.0 → 0.11.2
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 +155 -9
- package/README.md +161 -1037
- package/agents/verifier.md +18 -7
- package/dist/index.mjs +744 -90644
- package/docs/README.md +57 -46
- package/docs/architecture.md +87 -33
- package/docs/commands-reference.md +9 -5
- package/docs/troubleshooting.md +3 -2
- package/package.json +1 -3
- package/schema.json +39 -0
- package/skills/real-test-pi-crew/REPORT-TEMPLATE.md +2 -0
- package/skills/real-test-pi-crew/SKILL.md +371 -34
- package/src/agents/agent-config.ts +1 -1
- package/src/agents/discover-agents.ts +1 -1
- package/src/config/config-validation.ts +15 -2
- package/src/config/config.ts +47 -13
- package/src/config/defaults.ts +0 -1
- package/src/config/env-vars.ts +35 -0
- package/src/config/types.ts +19 -5
- package/src/errors.ts +2 -2
- package/src/extension/async-notifier.ts +23 -0
- package/src/extension/crew-vibes/config.ts +0 -21
- package/src/extension/crew-vibes/index.ts +0 -2
- package/src/extension/crew-vibes/render.ts +1 -50
- package/src/extension/help.ts +21 -12
- package/src/extension/knowledge-injection.ts +2 -1
- package/src/extension/management.ts +8 -3
- package/src/extension/notification-sink.ts +17 -0
- package/src/extension/register.ts +7 -2
- package/src/extension/registration/command-utils.ts +28 -2
- package/src/extension/registration/commands/dashboard.ts +11 -1
- package/src/extension/registration/commands/manage.ts +36 -19
- package/src/extension/registration/commands/run.ts +24 -2
- package/src/extension/registration/commands/shared.ts +23 -1
- package/src/extension/registration/commands/status.ts +25 -2
- package/src/extension/registration/context-builder.ts +8 -2
- package/src/extension/registration/health-notify-policy.ts +100 -0
- package/src/extension/registration/lazy-configurers.ts +35 -0
- package/src/extension/registration/lifecycle-handlers.ts +91 -30
- package/src/extension/registration/lifecycle.ts +75 -10
- package/src/extension/registration/observability.ts +98 -35
- package/src/extension/registration/registration-types.ts +7 -5
- package/src/extension/registration/runtime-cleanup.ts +9 -3
- package/src/extension/registration/subagent-helpers.ts +38 -0
- package/src/extension/registration/subagent-tools.ts +16 -6
- package/src/extension/registration/team-tool.ts +10 -3
- package/src/extension/registration/terminal-status-wiring.ts +172 -0
- package/src/extension/registration/viewers.ts +6 -0
- package/src/extension/registration/wire-cross-extension.ts +28 -0
- package/src/extension/run-compare.ts +220 -0
- package/src/extension/run-export.ts +37 -5
- package/src/extension/run-maintenance.ts +155 -5
- package/src/extension/team-tool/dispatch/index.ts +3 -2
- package/src/extension/team-tool/dispatch/manage.ts +5 -2
- package/src/extension/team-tool/goal.ts +4 -1
- package/src/extension/team-tool/handle-settings.ts +33 -4
- package/src/extension/team-tool/health-monitor.ts +21 -7
- package/src/extension/team-tool/lifecycle-actions.ts +49 -1
- package/src/extension/team-tool/plan.ts +10 -0
- package/src/extension/team-tool/routing-hint.ts +63 -0
- package/src/extension/team-tool/status.ts +4 -0
- package/src/extension/team-tool.ts +52 -6
- package/src/extension/webhook-notify.ts +382 -0
- package/src/observability/metric-sink.ts +12 -2
- package/src/prompt/prompt-runtime.ts +82 -31
- package/src/prompt/worker-events-channel.ts +12 -0
- package/src/runtime/README.md +1 -1
- package/src/runtime/async-runner.ts +87 -1
- package/src/runtime/background-runner.ts +313 -234
- package/src/runtime/broker/crew-broker.ts +17 -11
- package/src/runtime/broker/delegate/shadow-lifecycle.ts +92 -0
- package/src/runtime/broker/wait-status-cache.ts +1 -1
- package/src/runtime/child-pi/child-pi-timers.ts +1 -1
- package/src/runtime/child-pi/mock-fixtures.ts +48 -0
- package/src/runtime/crew-agent-records.ts +337 -45
- package/src/runtime/deadletter.ts +43 -1
- package/src/runtime/delegate-spawn.ts +5 -1
- package/src/runtime/dispatch-batch.ts +72 -5
- package/src/runtime/goal-workflow/goal-loop-runner.ts +73 -4
- package/src/runtime/heartbeat/heartbeat-watcher.ts +7 -0
- package/src/runtime/model/model-fallback.ts +21 -1
- package/src/runtime/model/pi-args.ts +8 -10
- package/src/runtime/recovery/crash-recovery.ts +25 -1
- package/src/runtime/run-worker.ts +12 -1
- package/src/runtime/scheduling/global-worker-cap.ts +13 -6
- package/src/runtime/scheduling/run-coalesced-task-group.ts +27 -1
- package/src/runtime/scheduling/scheduler.ts +49 -13
- package/src/runtime/scheduling/semaphore.ts +148 -20
- package/src/runtime/scratchpad/README.md +1 -1
- package/src/runtime/scratchpad/protocol.ts +1 -1
- package/src/runtime/settings-store.ts +1 -1
- package/src/runtime/skill-instructions.ts +22 -0
- package/src/runtime/stale-reconciler.ts +85 -13
- package/src/runtime/task-display.ts +1 -1
- package/src/runtime/task-runner/pre-execution.ts +26 -2
- package/src/runtime/task-runner/prompt-builder.ts +142 -45
- package/src/runtime/task-runner.ts +21 -1
- package/src/runtime/team-runner.ts +38 -1
- package/src/runtime/workspace-lock.ts +4 -1
- package/src/schema/config-schema.ts +18 -0
- package/src/schema/team-tool-schema.ts +17 -0
- package/src/state/atomic-write.ts +53 -0
- package/src/state/contracts.ts +109 -0
- package/src/state/coordination/locks.ts +191 -33
- package/src/state/coordination/mailbox.ts +140 -15
- package/src/state/crew-init.ts +87 -12
- package/src/state/event-log/cursor.ts +37 -1
- package/src/state/event-log/event-log-rotation.ts +72 -7
- package/src/state/stores/active-run-registry.ts +13 -1
- package/src/state/stores/state-store.ts +112 -22
- package/src/state/types.ts +4 -0
- package/src/ui/adaptive-card.ts +65 -0
- package/src/ui/agents-jobs-browser.ts +70 -64
- package/src/ui/card-colors.ts +36 -7
- package/src/ui/dashboard-panes/agents-pane.ts +55 -14
- package/src/ui/dashboard-panes/cancellation-pane.ts +0 -42
- package/src/ui/dashboard-panes/health-pane.ts +7 -5
- package/src/ui/dashboard-panes/mailbox-pane.ts +22 -6
- package/src/ui/dashboard-panes/metrics-pane.ts +15 -7
- package/src/ui/dashboard-panes/pane-theme.ts +21 -0
- package/src/ui/dashboard-panes/plan-pane.ts +63 -30
- package/src/ui/dashboard-panes/progress-pane.ts +3 -2
- package/src/ui/dashboard-panes/schedules-pane.ts +44 -21
- package/src/ui/dashboard-panes/transcript-pane.ts +11 -5
- package/src/ui/dwf-phase-display.ts +3 -20
- package/src/ui/format-helpers.ts +22 -0
- package/src/ui/heartbeat-aggregator.ts +34 -0
- package/src/ui/inline-panel/crew-editor.ts +13 -3
- package/src/ui/inline-panel/index.ts +60 -4
- package/src/ui/keybinding-map.ts +251 -35
- package/src/ui/live-conversation-overlay.ts +180 -47
- package/src/ui/live-run-sidebar.ts +134 -55
- package/src/ui/mascot.ts +32 -16
- package/src/ui/overlays/agent-picker-overlay.ts +81 -26
- package/src/ui/overlays/confirm-overlay.ts +55 -29
- package/src/ui/overlays/help-overlay.ts +108 -53
- package/src/ui/overlays/mailbox-compose-overlay.ts +89 -50
- package/src/ui/overlays/mailbox-detail-overlay.ts +137 -57
- package/src/ui/powerbar-publisher.ts +0 -1
- package/src/ui/rail.ts +333 -0
- package/src/ui/run-dashboard.ts +193 -79
- package/src/ui/run-snapshot-cache.ts +18 -1
- package/src/ui/settings-overlay.ts +81 -39
- package/src/ui/spinner.ts +26 -2
- package/src/ui/terminal-status.ts +7 -1
- package/src/ui/theme-adapter.ts +0 -45
- package/src/ui/theme-discovery.ts +12 -6
- package/src/ui/tool-progress-formatter.ts +128 -9
- package/src/ui/tool-renderers/brief-mode.ts +10 -67
- package/src/ui/tool-renderers/index.ts +374 -523
- package/src/ui/transcript-viewer.ts +30 -12
- package/src/ui/widget/index.ts +32 -52
- package/src/ui/widget/task-list.ts +64 -32
- package/src/ui/widget/widget-formatters.ts +3 -402
- package/src/ui/widget/widget-model.ts +28 -7
- package/src/ui/widget/widget-renderer.ts +201 -128
- package/src/ui/widget/widget-types.ts +0 -2
- package/src/utils/incremental-reader.ts +11 -3
- package/src/utils/paths.ts +94 -12
- package/src/utils/project-markers.ts +40 -0
- package/src/utils/visual.ts +0 -4
- package/src/worktree/worktree-manager.ts +206 -26
- package/workflows/distill.workflow.md +3 -3
- package/workflows/fast-fix.workflow.md +1 -1
- package/workflows/plan-execute.workflow.md +1 -1
- package/workflows/review.workflow.md +1 -1
- package/workflows/strict-fast-fix.workflow.md +1 -1
- package/docs/migration-v0.4-v0.5.md +0 -208
- package/docs/runtime-flow.md +0 -148
- package/src/extension/crew-vibes/figures.ts +0 -22
- package/src/extension/crew-vibes/font-detect.ts +0 -71
- package/src/ui/dynamic-border.ts +0 -35
- package/src/ui/loaders.ts +0 -6
- package/src/ui/overlay-stack.ts +0 -148
|
@@ -105,12 +105,22 @@ export function registerDashboardCommands(pi: ExtensionAPI, deps: RegisterTeamCo
|
|
|
105
105
|
pi.registerCommand("team-dashboard", {
|
|
106
106
|
description: "Open a pi-crew run dashboard overlay",
|
|
107
107
|
handler: async (_args: string, ctx: ExtensionCommandContext) => {
|
|
108
|
+
// W4 (slash-commands fix spec): headless invocation previously fell
|
|
109
|
+
// through to openTeamDashboard's silent `if (!ctx.hasUI) return;`
|
|
110
|
+
// no-op — notify + return here instead so headless users get an
|
|
111
|
+
// actionable pointer instead of silence.
|
|
112
|
+
if (!ctx.hasUI) {
|
|
113
|
+
ctx.ui.notify("team-dashboard needs a UI session — headless runs can use /team-status instead.", "info");
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
108
116
|
await openTeamDashboard(ctx);
|
|
109
117
|
},
|
|
110
118
|
});
|
|
111
119
|
|
|
112
120
|
pi.registerCommand("team-mascot", {
|
|
113
|
-
|
|
121
|
+
// W4: cosmetic command is a documented silent no-op without a UI
|
|
122
|
+
// session (handler's `if (!ctx.hasUI) return;` stays).
|
|
123
|
+
description: "Show an animated mascot splash (UI session only)",
|
|
114
124
|
handler: async (args: string, ctx: ExtensionCommandContext) => {
|
|
115
125
|
if (!ctx.hasUI) return;
|
|
116
126
|
const tokens = args.trim().split(/\s+/).filter(Boolean);
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import * as fs from "node:fs";
|
|
2
2
|
import * as path from "node:path";
|
|
3
|
+
import { fileURLToPath } from "node:url";
|
|
3
4
|
import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
4
5
|
import { getBuiltinTemplates, instantiateTemplate, listTemplates } from "../../../skills/skill-templates.ts";
|
|
5
6
|
import { atomicWriteFile } from "../../../state/atomic-write.ts";
|
|
@@ -9,6 +10,29 @@ import { commandText, notifyCommandResult, parseScalar, pushUnset, setNestedConf
|
|
|
9
10
|
import type { RegisterTeamCommandsDeps } from "./shared.ts";
|
|
10
11
|
import { handleTeamTool, openTeamSettingsOverlay, teamCommandContext } from "./shared.ts";
|
|
11
12
|
|
|
13
|
+
/**
|
|
14
|
+
* Resolve the user-level skills directory (<pi-crew package root>/skills).
|
|
15
|
+
*
|
|
16
|
+
* ESM-safe replacement for the former `require.resolve("../../../../package.json",
|
|
17
|
+
* { paths: [__dirname] })`: neither `require` nor `__dirname` exists under
|
|
18
|
+
* strip-types source loading or inside the esbuild ESM bundle (dist/index.mjs).
|
|
19
|
+
* Anchoring on `import.meta.url` and walking up to the nearest package.json
|
|
20
|
+
* yields the package root under BOTH layouts — src/.../commands/manage.ts and
|
|
21
|
+
* dist/index.mjs sit at different depths, so a fixed relative hop count cannot
|
|
22
|
+
* serve both.
|
|
23
|
+
*/
|
|
24
|
+
export function resolveUserSkillsDir(): string {
|
|
25
|
+
let dir = fileURLToPath(new URL(".", import.meta.url));
|
|
26
|
+
for (;;) {
|
|
27
|
+
if (fs.existsSync(path.join(dir, "package.json"))) return path.join(dir, "skills");
|
|
28
|
+
const parent = path.dirname(dir);
|
|
29
|
+
if (parent === dir) {
|
|
30
|
+
throw new Error(`pi-crew: cannot locate package root (no package.json above ${dir})`);
|
|
31
|
+
}
|
|
32
|
+
dir = parent;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
12
36
|
export function registerManageCommands(pi: ExtensionAPI, deps: RegisterTeamCommandsDeps): void {
|
|
13
37
|
pi.registerCommand("team-prune", {
|
|
14
38
|
description: "Prune old finished pi-crew runs, keeping the newest N",
|
|
@@ -210,16 +234,20 @@ export function registerManageCommands(pi: ExtensionAPI, deps: RegisterTeamComma
|
|
|
210
234
|
return idx === -1 ? [s, ""] : [s.slice(0, idx), s.slice(idx + 1)];
|
|
211
235
|
});
|
|
212
236
|
const templateId = tokens.find((t) => !t.startsWith("--") && !t.includes("="));
|
|
237
|
+
const availableTemplateIds = listTemplates().map((t) => t.id);
|
|
213
238
|
if (!templateId) {
|
|
214
239
|
await notifyCommandResult(
|
|
215
240
|
ctx,
|
|
216
|
-
|
|
241
|
+
`Usage: /skill-create <template-id> [--var key=value...] [--project]\nAvailable templates: ${availableTemplateIds.join(", ")}`,
|
|
217
242
|
);
|
|
218
243
|
return;
|
|
219
244
|
}
|
|
220
245
|
const template = getBuiltinTemplates().find((t) => t.id === templateId);
|
|
221
246
|
if (!template) {
|
|
222
|
-
await notifyCommandResult(
|
|
247
|
+
await notifyCommandResult(
|
|
248
|
+
ctx,
|
|
249
|
+
`Unknown template '${templateId}'.\nUsage: /skill-create <template-id> [--var key=value...] [--project]\nAvailable templates: ${availableTemplateIds.join(", ")}`,
|
|
250
|
+
);
|
|
223
251
|
return;
|
|
224
252
|
}
|
|
225
253
|
const variables: Record<string, string> = {};
|
|
@@ -249,19 +277,7 @@ export function registerManageCommands(pi: ExtensionAPI, deps: RegisterTeamComma
|
|
|
249
277
|
await notifyCommandResult(ctx, error instanceof Error ? error.message : String(error));
|
|
250
278
|
return;
|
|
251
279
|
}
|
|
252
|
-
const skillsDir = path.resolve(
|
|
253
|
-
cwd,
|
|
254
|
-
useProject
|
|
255
|
-
? "skills"
|
|
256
|
-
: path.join(
|
|
257
|
-
path.dirname(
|
|
258
|
-
require.resolve("../../../../package.json", {
|
|
259
|
-
paths: [__dirname],
|
|
260
|
-
}),
|
|
261
|
-
),
|
|
262
|
-
"skills",
|
|
263
|
-
),
|
|
264
|
-
);
|
|
280
|
+
const skillsDir = useProject ? path.resolve(cwd, "skills") : resolveUserSkillsDir();
|
|
265
281
|
const skillDir = path.join(skillsDir, template.id);
|
|
266
282
|
const skillPath = path.join(skillDir, "SKILL.md");
|
|
267
283
|
try {
|
|
@@ -278,22 +294,23 @@ export function registerManageCommands(pi: ExtensionAPI, deps: RegisterTeamComma
|
|
|
278
294
|
});
|
|
279
295
|
|
|
280
296
|
pi.registerCommand("crew-brief", {
|
|
281
|
-
description:
|
|
297
|
+
description:
|
|
298
|
+
"Toggle brief mode for crew `team`/`agent` tool output (native read/bash/edit/write/find/grep/ls output is unaffected): on | off | status",
|
|
282
299
|
handler: async (args: string, ctx: ExtensionCommandContext) => {
|
|
283
300
|
// LAZY: defer dynamic import of ../../ui/tool-renderers/brief-mode.ts to its call site. Multi-line form breaks scripts/check-lazy-imports.mjs (which does `lines[lineNum - 2]`), so keep destructuring + await import on one line and place this LAZY marker directly above.
|
|
284
|
-
const { isBrief, setBrief, BRIEF_ENTRY_TYPE
|
|
301
|
+
const { isBrief, setBrief, BRIEF_ENTRY_TYPE } = await import("../../../ui/tool-renderers/brief-mode.ts");
|
|
285
302
|
const trimmed = args.trim();
|
|
286
303
|
|
|
287
304
|
if (trimmed === "on") {
|
|
288
305
|
setBrief(true);
|
|
289
306
|
pi.appendEntry(BRIEF_ENTRY_TYPE, { enabled: true });
|
|
290
|
-
ctx.ui.notify("Brief mode: on — tool output will show compact summaries", "info");
|
|
307
|
+
ctx.ui.notify("Brief mode: on — crew team/agent tool output will show compact summaries", "info");
|
|
291
308
|
return;
|
|
292
309
|
}
|
|
293
310
|
if (trimmed === "off") {
|
|
294
311
|
setBrief(false);
|
|
295
312
|
pi.appendEntry(BRIEF_ENTRY_TYPE, { enabled: false });
|
|
296
|
-
ctx.ui.notify("Brief mode: off — full tool output restored", "info");
|
|
313
|
+
ctx.ui.notify("Brief mode: off — full crew team/agent tool output restored", "info");
|
|
297
314
|
return;
|
|
298
315
|
}
|
|
299
316
|
// status (default)
|
|
@@ -109,6 +109,10 @@ export function registerRunCommands(pi: ExtensionAPI, deps: RegisterTeamCommands
|
|
|
109
109
|
const taskToken = tokens[0] === "--all" ? tokens.shift() : tokens.shift();
|
|
110
110
|
const taskId = taskToken === "--all" ? undefined : taskToken;
|
|
111
111
|
const message = tokens.join(" ") || undefined;
|
|
112
|
+
if (!runId || !taskToken || !message) {
|
|
113
|
+
await notifyCommandResult(ctx, "Usage: /team-respond <runId> <taskId|--all> <message>…");
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
112
116
|
const result = await handleTeamTool({ action: "respond", runId, taskId, message }, teamCommandContext(ctx));
|
|
113
117
|
await notifyCommandResult(ctx, commandText(result));
|
|
114
118
|
},
|
|
@@ -163,7 +167,19 @@ export function registerRunCommands(pi: ExtensionAPI, deps: RegisterTeamCommands
|
|
|
163
167
|
|
|
164
168
|
pi.registerCommand("team-goal", {
|
|
165
169
|
description:
|
|
166
|
-
"Autonomous goal loop control: [start|status|pause|resume|stop|step|clear] [goalId] [--objective=...] [--evaluatorModel=...] [--maxTurns=N]",
|
|
170
|
+
"Autonomous goal loop control (defaults to status): [start|status|pause|resume|stop|cancel|reset|step|clear] [goalId] [--objective=...] [--evaluatorModel=...] [--maxTurns=N]",
|
|
171
|
+
// Suggest the stop-aliases only while completing the FIRST argument:
|
|
172
|
+
// pi hands `getArgumentCompletions` the whole argument text (there is no
|
|
173
|
+
// argument-index parameter), so any whitespace in it means the cursor is
|
|
174
|
+
// already past arg 1 — nothing useful to suggest for goalId/flags.
|
|
175
|
+
getArgumentCompletions: (argumentPrefix: string) => {
|
|
176
|
+
if (argumentPrefix.includes(" ")) return [];
|
|
177
|
+
const prefix = argumentPrefix.trim();
|
|
178
|
+
return [
|
|
179
|
+
{ value: "cancel", label: "cancel", description: "stop the goal loop (alias of stop)" },
|
|
180
|
+
{ value: "reset", label: "reset", description: "stop the goal loop (alias of stop)" },
|
|
181
|
+
].filter((item) => item.value.startsWith(prefix));
|
|
182
|
+
},
|
|
167
183
|
handler: async (args: string, ctx: ExtensionCommandContext) => {
|
|
168
184
|
const tokens = args.trim().split(/\s+/).filter(Boolean);
|
|
169
185
|
const knownSubs = new Set(["start", "status", "pause", "resume", "stop", "step", "clear", "cancel", "reset"]);
|
|
@@ -203,7 +219,13 @@ export function registerRunCommands(pi: ExtensionAPI, deps: RegisterTeamCommands
|
|
|
203
219
|
metricRegistry: deps.getMetricRegistry?.(),
|
|
204
220
|
},
|
|
205
221
|
);
|
|
206
|
-
|
|
222
|
+
const text = commandText(result);
|
|
223
|
+
const trimmed = text.trim();
|
|
224
|
+
const hint =
|
|
225
|
+
!trimmed || trimmed === "[]"
|
|
226
|
+
? "\nNo metrics yet — observability may be disabled for this team. Set `observability: true` in the team frontmatter to enable."
|
|
227
|
+
: "";
|
|
228
|
+
await notifyCommandResult(ctx, text + hint);
|
|
207
229
|
},
|
|
208
230
|
});
|
|
209
231
|
|
|
@@ -531,7 +531,6 @@ export async function openTeamDashboard(ctx: ExtensionContext): Promise<void> {
|
|
|
531
531
|
const selection = await cmdCtx.ui.custom<RunDashboardSelection | undefined>(
|
|
532
532
|
(tui, theme, _keybindings, done) =>
|
|
533
533
|
new RunDashboard(runs, done, theme, {
|
|
534
|
-
placement: rightPanel ? "right" : "center",
|
|
535
534
|
showModel: uiConfig?.showModel,
|
|
536
535
|
showTokens: uiConfig?.showTokens,
|
|
537
536
|
showTools: uiConfig?.showTools,
|
|
@@ -577,6 +576,29 @@ export async function openTeamDashboard(ctx: ExtensionContext): Promise<void> {
|
|
|
577
576
|
deps.getRunSnapshotCache?.(cmdCtx.cwd).invalidate(selection.runId);
|
|
578
577
|
continue;
|
|
579
578
|
}
|
|
579
|
+
// US-020: dashboard `x` cancel — dispatches the EXISTING cancel channel
|
|
580
|
+
// (control-domain handleCancel). The 2-step dashboard gate is the user's
|
|
581
|
+
// explicit intent; `intent` satisfies requireIntentForDestructiveActions
|
|
582
|
+
// when that policy is on. Dashboard reopens on continue (selection kept).
|
|
583
|
+
if (selection.action === "cancel" && selection.runId) {
|
|
584
|
+
// F4 pattern: surface backend rejections at error level, truncate long text.
|
|
585
|
+
const cancelResult = await handleTeamTool(
|
|
586
|
+
{
|
|
587
|
+
action: "cancel",
|
|
588
|
+
runId: selection.runId,
|
|
589
|
+
config: { intent: "dashboard cancel keystroke (2-step confirm)" },
|
|
590
|
+
},
|
|
591
|
+
teamCommandContext(cmdCtx),
|
|
592
|
+
);
|
|
593
|
+
const cancelText = commandText(cancelResult);
|
|
594
|
+
depsNotify(
|
|
595
|
+
cmdCtx,
|
|
596
|
+
cancelText.length > 800 ? `${cancelText.slice(0, 797)}...` : cancelText,
|
|
597
|
+
cancelResult.isError ? "error" : "info",
|
|
598
|
+
);
|
|
599
|
+
deps.getRunSnapshotCache?.(cmdCtx.cwd).invalidate(selection.runId);
|
|
600
|
+
continue;
|
|
601
|
+
}
|
|
580
602
|
if (selection.action === "plan-approve" || selection.action === "plan-deny") {
|
|
581
603
|
await handlePlanDashboardAction(cmdCtx, selection);
|
|
582
604
|
deps.getRunSnapshotCache?.(cmdCtx.cwd).invalidate(selection.runId);
|
|
@@ -1,10 +1,28 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { loadRunManifestById } from "../../../state/stores/state-store.ts";
|
|
2
3
|
import { suggestRunIds } from "../../command-completions.ts";
|
|
3
4
|
import { piTeamsHelp } from "../../help.ts";
|
|
4
|
-
import { commandText, notifyCommandResult } from "../command-utils.ts";
|
|
5
|
+
import { commandText, NOTIFY_TEXT_CAP, notifyCommandResult } from "../command-utils.ts";
|
|
5
6
|
import type { RegisterTeamCommandsDeps } from "./shared.ts";
|
|
6
7
|
import { handleTeamTool, teamCommandContext } from "./shared.ts";
|
|
7
8
|
|
|
9
|
+
/**
|
|
10
|
+
* W5 (slash-commands fix spec): `/team-events` pointer. When the events
|
|
11
|
+
* listing is about to be clipped by the 800-char notification cap, resolve
|
|
12
|
+
* the run's on-disk events log so the message can end with a reachable path.
|
|
13
|
+
* Derived from the SAME source the `events` team action reads
|
|
14
|
+
* (inspect.ts): locateRunCwd + loadRunManifestById → manifest.eventsPath.
|
|
15
|
+
* The dynamic team-tool import is free here — the awaited handleTeamTool
|
|
16
|
+
* call above it already loaded that module into the ESM cache.
|
|
17
|
+
*/
|
|
18
|
+
async function eventsLogFooter(runId: string | undefined, text: string, cwd: string): Promise<string | undefined> {
|
|
19
|
+
if (!runId || text.length <= NOTIFY_TEXT_CAP) return undefined;
|
|
20
|
+
const { locateRunCwd } = await import("../../team-tool.ts"); // LAZY: defer team-tool chain to call site (handleTeamTool already cached it).
|
|
21
|
+
const runCwd = locateRunCwd(runId, cwd);
|
|
22
|
+
const manifest = runCwd ? loadRunManifestById(runCwd, runId)?.manifest : undefined;
|
|
23
|
+
return manifest ? ` Full log: ${manifest.eventsPath}` : undefined;
|
|
24
|
+
}
|
|
25
|
+
|
|
8
26
|
export function registerStatusCommands(pi: ExtensionAPI, deps: RegisterTeamCommandsDeps): void {
|
|
9
27
|
pi.registerCommand("teams", {
|
|
10
28
|
description: "List pi-crew teams, workflows, and agents",
|
|
@@ -34,7 +52,12 @@ export function registerStatusCommands(pi: ExtensionAPI, deps: RegisterTeamComma
|
|
|
34
52
|
getRunSnapshotCache: deps.getRunSnapshotCache,
|
|
35
53
|
},
|
|
36
54
|
);
|
|
37
|
-
|
|
55
|
+
const text = commandText(result);
|
|
56
|
+
await notifyCommandResult(ctx, text, {
|
|
57
|
+
// W5: only `team-events` output is a truncatable on-disk log —
|
|
58
|
+
// point at the full file when the listing gets clipped.
|
|
59
|
+
truncatedFooter: action === "events" ? await eventsLogFooter(runId, text, ctx.cwd) : undefined,
|
|
60
|
+
});
|
|
38
61
|
},
|
|
39
62
|
});
|
|
40
63
|
}
|
|
@@ -58,6 +58,7 @@ export function buildRegistrationContext(pi: ExtensionAPI): RegistrationContext
|
|
|
58
58
|
autoRepairTimer: undefined,
|
|
59
59
|
tempReconcileTimer: undefined,
|
|
60
60
|
otlpExporter: undefined,
|
|
61
|
+
initPromise: undefined,
|
|
61
62
|
} as ObservabilityState,
|
|
62
63
|
lifecycleState: {
|
|
63
64
|
notifierStarted: false,
|
|
@@ -121,7 +122,12 @@ export function buildRegistrationContext(pi: ExtensionAPI): RegistrationContext
|
|
|
121
122
|
};
|
|
122
123
|
|
|
123
124
|
ctx.getManifestCache = (cwd: string) => {
|
|
124
|
-
|
|
125
|
+
// F13 (RR-018): a disposed cache pair must be recreated EVEN WHEN the cwd
|
|
126
|
+
// is unchanged. Session cleanup disposes the caches but used to leave
|
|
127
|
+
// `cacheCwd` pointing at them, so same-cwd access returned the disposed
|
|
128
|
+
// instance (no disposed flag existed). isDisposed() makes the pair's
|
|
129
|
+
// liveness part of the swap check.
|
|
130
|
+
if (ctx.manifestCache && ctx.cacheCwd === cwd && !ctx.runSnapshotCache.isDisposed()) return ctx.manifestCache;
|
|
125
131
|
if (ctx.manifestCache) ctx.manifestCache.dispose();
|
|
126
132
|
if (ctx.runSnapshotCache) ctx.runSnapshotCache.dispose?.();
|
|
127
133
|
ctx.cacheCwd = cwd;
|
|
@@ -130,7 +136,7 @@ export function buildRegistrationContext(pi: ExtensionAPI): RegistrationContext
|
|
|
130
136
|
return ctx.manifestCache;
|
|
131
137
|
};
|
|
132
138
|
ctx.getRunSnapshotCache = (cwd: string) => {
|
|
133
|
-
if (ctx.cacheCwd !== cwd) ctx.getManifestCache(cwd);
|
|
139
|
+
if (ctx.cacheCwd !== cwd || ctx.runSnapshotCache.isDisposed()) ctx.getManifestCache(cwd);
|
|
134
140
|
return ctx.runSnapshotCache;
|
|
135
141
|
};
|
|
136
142
|
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* health-notify-policy.ts — FINDING 6 (2026-09-23 battery): ambient
|
|
3
|
+
* notification re-delivery storms.
|
|
4
|
+
*
|
|
5
|
+
* Observed live: a zombie-run leak made the health monitor re-fire the same
|
|
6
|
+
* `recovery_missing_heartbeat` notification every cooldown window (5 min) for
|
|
7
|
+
* hours. Each fire queued a follow-up message on the host session while the
|
|
8
|
+
* parent was busy; the host drains follow-ups ONE PER TURN BOUNDARY
|
|
9
|
+
* (followUpMode "one-at-a-time"), so the queued duplicates kept dripping into
|
|
10
|
+
* the parent conversation for ~5 hours AFTER the runs were deleted and the
|
|
11
|
+
* monitor had written its `clear` entries.
|
|
12
|
+
*
|
|
13
|
+
* Policy fixes (both sides of the storm):
|
|
14
|
+
* 1. BOUNDED RE-FIRE — a given notification key may fire at most
|
|
15
|
+
* MAX_HEALTH_NOTIFY_FIRES times unless the underlying fingerprint changes
|
|
16
|
+
* (a different dead/missing/task-count shape is a NEW situation and gets a
|
|
17
|
+
* fresh budget). Previously the 5-min cooldown re-armed forever.
|
|
18
|
+
* 2. RESET-ON-CLEAR — the clear path resets the fire budget, so a genuine
|
|
19
|
+
* recurrence after a resolved incident notifies again (up to the cap).
|
|
20
|
+
*
|
|
21
|
+
* The host-queue purge of already-queued copies is handled separately
|
|
22
|
+
* (purgeQueuedAmbientNotifications in subagent-helpers.ts) — this module only
|
|
23
|
+
* stops pi-crew from FEEDING the queue.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
/** Maximum notifications emitted per key before a fingerprint change. */
|
|
27
|
+
export const MAX_HEALTH_NOTIFY_FIRES = 3;
|
|
28
|
+
|
|
29
|
+
export interface AutoRecoveryEntry {
|
|
30
|
+
insertedAt: number;
|
|
31
|
+
lastAccessAt: number;
|
|
32
|
+
/** Times this key has fired under the current fingerprint. */
|
|
33
|
+
fires?: number;
|
|
34
|
+
/** Shape of the situation last notified (see healthNotifyFingerprint). */
|
|
35
|
+
fingerprint?: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface HealthNotifyState {
|
|
39
|
+
/** Map keyed `${kind}_${runId}` — typically ctx.autoRecoveryLast. */
|
|
40
|
+
entries: Map<string, AutoRecoveryEntry>;
|
|
41
|
+
/** LRU cap — typically ctx.AUTO_RECOVERY_LAST_MAX_ENTRIES. */
|
|
42
|
+
maxEntries: number;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Compact fingerprint of the health situation: same counts = same situation. */
|
|
46
|
+
export function healthNotifyFingerprint(summary: { dead: number; missing: number }, totalTasks: number): string {
|
|
47
|
+
return `${summary.dead}/${summary.missing}/${totalTasks}`;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Decide whether a health notification may fire now, and record the decision.
|
|
52
|
+
*
|
|
53
|
+
* Fires when: (a) never fired before, (b) cooldown elapsed AND the same
|
|
54
|
+
* fingerprint still has budget, or (c) the fingerprint CHANGED (new situation
|
|
55
|
+
* — fresh budget). Blocks within the cooldown window and after the budget is
|
|
56
|
+
* exhausted. Evicts oldest-access entries at the cap (LRU, mirrors the
|
|
57
|
+
* previous inline behavior).
|
|
58
|
+
*/
|
|
59
|
+
export function recordHealthNotifyDecision(
|
|
60
|
+
state: HealthNotifyState,
|
|
61
|
+
key: string,
|
|
62
|
+
fingerprint: string,
|
|
63
|
+
now: number,
|
|
64
|
+
cooldownMs = 5 * 60_000,
|
|
65
|
+
maxFires = MAX_HEALTH_NOTIFY_FIRES,
|
|
66
|
+
): boolean {
|
|
67
|
+
const previous = state.entries.get(key);
|
|
68
|
+
if (previous !== undefined && now - previous.lastAccessAt < cooldownMs) return false;
|
|
69
|
+
if (previous !== undefined && previous.fires !== undefined && previous.fires >= maxFires && previous.fingerprint === fingerprint) {
|
|
70
|
+
// Budget exhausted for an unchanged situation. Still refresh access so
|
|
71
|
+
// LRU ordering reflects interest in this key.
|
|
72
|
+
previous.lastAccessAt = now;
|
|
73
|
+
return false;
|
|
74
|
+
}
|
|
75
|
+
while (state.entries.size >= state.maxEntries) {
|
|
76
|
+
let oldestKey: string | undefined;
|
|
77
|
+
let oldestAccess = Infinity;
|
|
78
|
+
for (const [k, v] of state.entries) {
|
|
79
|
+
if (v.lastAccessAt < oldestAccess) {
|
|
80
|
+
oldestAccess = v.lastAccessAt;
|
|
81
|
+
oldestKey = k;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
if (oldestKey === undefined) break;
|
|
85
|
+
state.entries.delete(oldestKey);
|
|
86
|
+
}
|
|
87
|
+
const fires = previous?.fingerprint === fingerprint ? (previous.fires ?? 0) + 1 : 1;
|
|
88
|
+
state.entries.set(key, {
|
|
89
|
+
insertedAt: previous?.insertedAt ?? now,
|
|
90
|
+
lastAccessAt: now,
|
|
91
|
+
fires,
|
|
92
|
+
fingerprint,
|
|
93
|
+
});
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Reset a key's fire budget (clear path: resolved incident gets fresh budget). */
|
|
98
|
+
export function resetHealthNotifyEntry(state: HealthNotifyState, key: string): void {
|
|
99
|
+
state.entries.delete(key);
|
|
100
|
+
}
|
|
@@ -24,6 +24,7 @@ import { reconcileAllStaleRuns } from "../../runtime/recovery/crash-recovery.ts"
|
|
|
24
24
|
import { reconcileOrphanedTempWorkspaces } from "../../runtime/stale-reconciler.ts";
|
|
25
25
|
import { requestPowerbarUpdate } from "../../ui/powerbar-publisher.ts";
|
|
26
26
|
import { logInternalError } from "../../utils/internal-error.ts";
|
|
27
|
+
import { extractSessionId } from "../../utils/session-utils.ts";
|
|
27
28
|
import type { RegistrationContext } from "./registration-types.ts";
|
|
28
29
|
import { sendAgentWakeUp, sendFollowUp } from "./subagent-helpers.ts";
|
|
29
30
|
|
|
@@ -48,6 +49,32 @@ export function installLazyConfigurers(pi: ExtensionAPI, ctx: RegistrationContex
|
|
|
48
49
|
ctx.configureDeliveryCoordinator = (): void => {
|
|
49
50
|
void configureDeliveryCoordinatorImpl(pi, ctx);
|
|
50
51
|
};
|
|
52
|
+
installTurnReconcileHook(pi, ctx);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* F12 (RR-018): register the before_agent_start reconcile hook EXACTLY ONCE
|
|
57
|
+
* for the extension lifetime. The callback resolves the CURRENT session
|
|
58
|
+
* (`ctx.currentCtx`) at fire time instead of capturing the configuring
|
|
59
|
+
* session's ExtensionContext, so a stale session's hook can never re-activate,
|
|
60
|
+
* double-reconcile a turn, or flip the shared manifest-cache cwd back to an
|
|
61
|
+
* old session's directory. (There is no pi.off unregister surface, so the old
|
|
62
|
+
* per-session registration accumulated one dead hook per session switch.)
|
|
63
|
+
*/
|
|
64
|
+
function installTurnReconcileHook(pi: ExtensionAPI, ctx: RegistrationContext): void {
|
|
65
|
+
try {
|
|
66
|
+
pi.on?.("before_agent_start", () => {
|
|
67
|
+
const current = ctx.currentCtx;
|
|
68
|
+
if (!current || ctx.cleanedUp) return;
|
|
69
|
+
try {
|
|
70
|
+
reconcileAllStaleRuns(current.cwd, ctx.getManifestCache(current.cwd), undefined, extractSessionId(current));
|
|
71
|
+
} catch (error) {
|
|
72
|
+
logInternalError("register.autoRepair.turnHook", error);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
} catch {
|
|
76
|
+
/* older Pi without before_agent_start — rely on the interval alone */
|
|
77
|
+
}
|
|
51
78
|
}
|
|
52
79
|
|
|
53
80
|
/**
|
|
@@ -58,6 +85,9 @@ export function installLazyConfigurers(pi: ExtensionAPI, ctx: RegistrationContex
|
|
|
58
85
|
* `ctx.currentCtx` and caches.
|
|
59
86
|
*/
|
|
60
87
|
async function configureNotificationsImpl(pi: ExtensionAPI, ctx: RegistrationContext, extCtx: ExtensionContext): Promise<void> {
|
|
88
|
+
// F11: capture the owning generation BEFORE the first await — the deps
|
|
89
|
+
// closure below re-checks it after lifecycle.ts's lazy imports resolve.
|
|
90
|
+
const ownerGeneration = ctx.sessionGeneration;
|
|
61
91
|
try {
|
|
62
92
|
// LAZY: registration/lifecycle is heavy (notification-router + sink)
|
|
63
93
|
const lifecycleModule = await import("./lifecycle.ts");
|
|
@@ -68,6 +98,7 @@ async function configureNotificationsImpl(pi: ExtensionAPI, ctx: RegistrationCon
|
|
|
68
98
|
getManifestCache: ctx.getManifestCache,
|
|
69
99
|
getRunSnapshotCache: ctx.getRunSnapshotCache,
|
|
70
100
|
requestPowerbarUpdate,
|
|
101
|
+
isOwnerSession: () => !ctx.cleanedUp && ctx.sessionGeneration === ownerGeneration,
|
|
71
102
|
});
|
|
72
103
|
} catch (error) {
|
|
73
104
|
logInternalError("register.configureNotifications", error);
|
|
@@ -90,6 +121,8 @@ async function configureObservabilityImpl(pi: ExtensionAPI, ctx: RegistrationCon
|
|
|
90
121
|
getManifestCache: ctx.getManifestCache,
|
|
91
122
|
notifyOperator: ctx.notifyOperator,
|
|
92
123
|
isCleanedUp: () => ctx.cleanedUp,
|
|
124
|
+
// F11: reuse the existing sessionGeneration counter — never a parallel one.
|
|
125
|
+
getSessionGeneration: () => ctx.sessionGeneration,
|
|
93
126
|
reconcileStaleRuns: (cwd, cache, currentSessionId) => reconcileAllStaleRuns(cwd, cache, undefined, currentSessionId),
|
|
94
127
|
reconcileOrphanedTempWorkspaces: (now, opts) => reconcileOrphanedTempWorkspaces(now, opts),
|
|
95
128
|
cleanupOrphanTempDirs,
|
|
@@ -108,6 +141,7 @@ async function configureObservabilityImpl(pi: ExtensionAPI, ctx: RegistrationCon
|
|
|
108
141
|
* No dynamic import needed — lifecycle is already loaded by configureNotifications.
|
|
109
142
|
*/
|
|
110
143
|
async function configureDeliveryCoordinatorImpl(pi: ExtensionAPI, ctx: RegistrationContext): Promise<void> {
|
|
144
|
+
const ownerGeneration = ctx.sessionGeneration;
|
|
111
145
|
try {
|
|
112
146
|
// LAZY: lifecycle.ts may not be loaded yet if configureNotifications hasn't fired.
|
|
113
147
|
const lifecycleModule = await import("./lifecycle.ts");
|
|
@@ -117,6 +151,7 @@ async function configureDeliveryCoordinatorImpl(pi: ExtensionAPI, ctx: Registrat
|
|
|
117
151
|
notifyOperator: ctx.notifyOperator,
|
|
118
152
|
sendFollowUp,
|
|
119
153
|
sendAgentWakeUp,
|
|
154
|
+
isOwnerSession: () => !ctx.cleanedUp && ctx.sessionGeneration === ownerGeneration,
|
|
120
155
|
});
|
|
121
156
|
} catch (error) {
|
|
122
157
|
logInternalError("register.configureDeliveryCoordinator", error);
|