pi-crew 0.9.41 → 0.9.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 +253 -0
- package/README.md +14 -0
- package/dist/build-meta.json +6450 -12419
- package/dist/index.mjs +57998 -68417
- package/dist/index.mjs.map +4 -4
- package/docs/migration/atomic-write-v2-migration.md +2 -0
- package/docs/superpowers/plans/2026-06-09-fallow-patterns-adoption.md +2 -0
- package/package.json +4 -2
- package/scripts/README.md +6 -0
- package/scripts/build-bundle.mjs +2 -1
- package/scripts/check-all-skills.ts +3 -3
- package/scripts/dev-runner.mjs +106 -0
- package/scripts/postinstall.mjs +6 -2
- package/scripts/verify-skill.ts +2 -2
- package/src/agents/agent-config.ts +1 -1
- package/src/config/config.ts +20 -1
- package/src/config/drift-detector.ts +97 -15
- package/src/config/resilient-parser.ts +4 -4
- package/src/config/types.ts +0 -2
- package/src/extension/crew-cleanup.ts +17 -3
- package/src/extension/crew-vibes/config.ts +1 -1
- package/src/extension/crew-vibes/font-detect.ts +16 -3
- package/src/extension/crew-vibes/index.ts +1 -1
- package/src/extension/cross-extension-rpc.ts +26 -9
- package/src/extension/management.ts +6 -5
- package/src/extension/register.ts +58 -1573
- package/src/extension/registration/command-registration.ts +57 -0
- package/src/extension/registration/commands.ts +7 -2
- package/src/extension/registration/context-builder.ts +131 -0
- package/src/extension/registration/crash-recovery-cache.ts +54 -0
- package/src/extension/registration/foreground-run-controller.ts +238 -0
- package/src/extension/registration/hook-registration.ts +119 -0
- package/src/extension/registration/lazy-configurers.ts +124 -0
- package/src/extension/registration/lifecycle-handlers.ts +731 -0
- package/src/extension/registration/observability.ts +33 -4
- package/src/extension/registration/registration-types.ts +149 -0
- package/src/extension/registration/runtime-cleanup.ts +230 -0
- package/src/extension/registration/subagent-manager-setup.ts +211 -0
- package/src/extension/registration/tool-registration.ts +50 -0
- package/src/extension/registration/wire-cross-extension.ts +37 -0
- package/src/extension/rpc-hmac.ts +21 -1
- package/src/extension/run-import.ts +3 -3
- package/src/extension/team-tool/auto-summarize.ts +4 -4
- package/src/extension/team-tool/cancel.ts +5 -5
- package/src/extension/team-tool/chain-executor.ts +5 -4
- package/src/extension/team-tool/context.ts +11 -1
- package/src/extension/team-tool/doctor.ts +2 -1
- package/src/extension/team-tool/goal-wrap.ts +2 -2
- package/src/extension/team-tool/goal.ts +1 -1
- package/src/extension/team-tool/handle-schedule.ts +25 -36
- package/src/extension/team-tool/orchestrate.ts +0 -1
- package/src/extension/team-tool/parallel-dispatch.ts +5 -0
- package/src/extension/team-tool/run.ts +220 -214
- package/src/extension/team-tool/workflow-manage.ts +1 -1
- package/src/extension/team-tool.ts +69 -16
- package/src/hooks/registry.ts +7 -2
- package/src/observability/correlation.ts +4 -3
- package/src/observability/event-bus.ts +10 -1
- package/src/observability/event-to-metric.ts +8 -7
- package/src/observability/exporters/adapter.ts +6 -9
- package/src/observability/exporters/otlp-exporter.ts +81 -0
- package/src/observability/metric-retention.ts +4 -0
- package/src/observability/metric-sink.ts +14 -4
- package/src/prompt/prompt-runtime.ts +15 -0
- package/src/runtime/adaptive-plan.ts +1 -1
- package/src/runtime/async-runner.ts +13 -4
- package/src/runtime/background-runner.ts +32 -23
- package/src/runtime/capability-inventory.ts +1 -1
- package/src/runtime/chain-runner.ts +5 -5
- package/src/runtime/checkpoint.ts +6 -16
- package/src/runtime/child-pi-constants.ts +42 -0
- package/src/runtime/child-pi-kill.ts +180 -0
- package/src/runtime/child-pi-spawn.ts +234 -0
- package/src/runtime/child-pi-steering.ts +128 -0
- package/src/runtime/child-pi-streams.ts +296 -0
- package/src/runtime/child-pi-transcript.ts +169 -0
- package/src/runtime/child-pi.ts +80 -822
- package/src/runtime/coalesce-tasks.ts +1 -1
- package/src/runtime/compact-stages/tail-capture-stage.ts +1 -1
- package/src/runtime/crash-recovery.ts +63 -47
- package/src/runtime/delivery-coordinator.ts +1 -1
- package/src/runtime/deterministic-ast.ts +2 -2
- package/src/runtime/diagnostic-export.ts +2 -1
- package/src/runtime/dwf-state-store.ts +5 -0
- package/src/runtime/dynamic-workflow-context.ts +78 -18
- package/src/runtime/dynamic-workflow-runner.ts +18 -2
- package/src/runtime/foreground-control.ts +3 -2
- package/src/runtime/goal-evaluator.ts +59 -27
- package/src/runtime/goal-loop-runner.ts +46 -13
- package/src/runtime/goal-state-store.ts +1 -1
- package/src/runtime/handoff-manager.ts +0 -2
- package/src/runtime/intercom-bridge.ts +0 -2
- package/src/runtime/live-agent-manager.ts +30 -5
- package/src/runtime/live-extension-bridge.ts +0 -2
- package/src/runtime/live-session-runtime.ts +65 -57
- package/src/runtime/manifest-cache.ts +59 -0
- package/src/runtime/mcp-proxy.ts +1 -1
- package/src/runtime/orphan-worker-registry.ts +3 -3
- package/src/runtime/output-validator.ts +1 -1
- package/src/runtime/path-overlap.ts +1 -1
- package/src/runtime/pi-args.ts +3 -3
- package/src/runtime/pi-json-output.ts +4 -3
- package/src/runtime/pi-spawn.ts +7 -1
- package/src/runtime/pipeline-runner.ts +1 -4
- package/src/runtime/post-checks.ts +0 -1
- package/src/runtime/retry-runner.ts +1 -1
- package/src/runtime/run-coalesced-task-group.ts +18 -23
- package/src/runtime/scheduler.ts +2 -0
- package/src/runtime/session-resources.ts +1 -1
- package/src/runtime/settings-store.ts +31 -1
- package/src/runtime/skill-effectiveness.ts +2 -2
- package/src/runtime/stale-reconciler.ts +23 -53
- package/src/runtime/subagent-manager.ts +19 -3
- package/src/runtime/task-output-context.ts +2 -1
- package/src/runtime/task-runner/prompt-builder.ts +3 -3
- package/src/runtime/task-runner/run-projection.ts +3 -1
- package/src/runtime/task-runner/state-helpers.ts +13 -1
- package/src/runtime/task-runner.ts +31 -6
- package/src/runtime/team-runner.ts +8 -2
- package/src/runtime/verification-gates.ts +54 -5
- package/src/schema/config-schema.ts +19 -1
- package/src/schema/team-tool-schema.ts +2 -0
- package/src/skills/discover-skills.ts +25 -15
- package/src/state/active-run-registry.ts +0 -1
- package/src/state/atomic-write.ts +37 -11
- package/src/state/blob-store.ts +3 -9
- package/src/state/contracts.ts +5 -1
- package/src/state/crew-init.ts +3 -2
- package/src/state/decision-ledger.ts +2 -2
- package/src/state/event-log-rotation.ts +1 -1
- package/src/state/event-log.ts +4 -3
- package/src/state/gitignore-manager.ts +2 -1
- package/src/state/health-store.ts +5 -2
- package/src/state/instinct-store.ts +11 -2
- package/src/state/locks.ts +132 -49
- package/src/state/mailbox.ts +3 -4
- package/src/state/observation-store.ts +1 -13
- package/src/state/run-cache.ts +12 -12
- package/src/state/run-graph.ts +2 -1
- package/src/state/run-metrics.ts +1 -1
- package/src/state/schedule.ts +7 -1
- package/src/state/state-store.ts +9 -3
- package/src/state/tiered-eval.ts +1 -1
- package/src/state/worker-atomic-writer.ts +1 -0
- package/src/ui/dynamic-border.ts +13 -3
- package/src/ui/live-conversation-overlay.ts +7 -27
- package/src/ui/live-duration.ts +2 -2
- package/src/ui/live-run-sidebar.ts +56 -32
- package/src/ui/mascot.ts +19 -13
- package/src/ui/powerbar-publisher.ts +244 -178
- package/src/ui/render-coalescer.ts +44 -3
- package/src/ui/render-diff.ts +2 -2
- package/src/ui/run-dashboard.ts +122 -21
- package/src/ui/run-event-bus.ts +66 -0
- package/src/ui/run-snapshot-cache.ts +84 -81
- package/src/ui/settings-overlay.ts +3 -3
- package/src/ui/status-colors.ts +24 -0
- package/src/ui/tool-progress-formatter.ts +1 -1
- package/src/ui/tool-render.ts +3 -3
- package/src/ui/transcript-cache.ts +5 -10
- package/src/ui/transcript-entries.ts +68 -72
- package/src/ui/transcript-viewer.ts +54 -14
- package/src/ui/widget/index.ts +26 -15
- package/src/ui/widget/widget-model.ts +18 -9
- package/src/ui/widget/widget-renderer.ts +2 -5
- package/src/utils/fingerprint.ts +1 -1
- package/src/utils/paths.ts +1 -1
- package/src/utils/project-detector.ts +1 -1
- package/src/worktree/worktree-manager.ts +47 -19
- package/src/state/atomic-write-v2.ts +0 -108
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Command registration installer for pi-crew.
|
|
3
|
+
*
|
|
4
|
+
* Single entry point that wires every `pi.registerCommand(...)` call
|
|
5
|
+
* site in the extension. Today that means one heavy call:
|
|
6
|
+
* • `registerTeamCommands` — registers every `/crew-*`, `/team-*`,
|
|
7
|
+
* `/teams`, and friends. The full set lives in
|
|
8
|
+
* `registration/commands.ts` (already extracted); this file is
|
|
9
|
+
* the orchestrator-level wrapper that calls it with the right deps.
|
|
10
|
+
*/
|
|
11
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
12
|
+
import { loadConfig } from "../../config/config.ts";
|
|
13
|
+
import { updatePiCrewPowerbar } from "../../ui/powerbar-publisher.ts";
|
|
14
|
+
import { updateCrewWidget } from "../../ui/widget/index.ts";
|
|
15
|
+
import { registerTeamCommands } from "./commands.ts";
|
|
16
|
+
import type { RegistrationContext } from "./registration-types.ts";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Register all pi-crew slash commands on the ExtensionAPI.
|
|
20
|
+
*
|
|
21
|
+
* The dismiss-notifications callback (used by commands like
|
|
22
|
+
* `/crew-notifications dismiss`) needs to refresh both the widget and
|
|
23
|
+
* the powerbar with the new count, so we read `currentCtx` + `widgetState`
|
|
24
|
+
* lazily off the context.
|
|
25
|
+
*/
|
|
26
|
+
export function registerPiCommands(pi: ExtensionAPI, ctx: RegistrationContext): void {
|
|
27
|
+
registerTeamCommands(pi, {
|
|
28
|
+
startForegroundRun: ctx.startForegroundRun,
|
|
29
|
+
abortForegroundRun: ctx.abortForegroundRun,
|
|
30
|
+
openLiveSidebar: ctx.openLiveSidebar,
|
|
31
|
+
getManifestCache: ctx.getManifestCache,
|
|
32
|
+
getRunSnapshotCache: ctx.getRunSnapshotCache,
|
|
33
|
+
getMetricRegistry: () => ctx.observabilityState.metricRegistry,
|
|
34
|
+
dismissNotifications: () => {
|
|
35
|
+
ctx.widgetState.notificationCount = 0;
|
|
36
|
+
if (ctx.currentCtx) {
|
|
37
|
+
const uiConfig = loadConfig(ctx.currentCtx.cwd).config.ui;
|
|
38
|
+
updateCrewWidget(
|
|
39
|
+
ctx.currentCtx,
|
|
40
|
+
ctx.widgetState,
|
|
41
|
+
uiConfig,
|
|
42
|
+
ctx.getManifestCache(ctx.currentCtx.cwd),
|
|
43
|
+
ctx.getRunSnapshotCache(ctx.currentCtx.cwd),
|
|
44
|
+
);
|
|
45
|
+
updatePiCrewPowerbar(
|
|
46
|
+
pi.events,
|
|
47
|
+
ctx.currentCtx.cwd,
|
|
48
|
+
uiConfig,
|
|
49
|
+
ctx.getManifestCache(ctx.currentCtx.cwd),
|
|
50
|
+
ctx.getRunSnapshotCache(ctx.currentCtx.cwd),
|
|
51
|
+
ctx.currentCtx,
|
|
52
|
+
0,
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ExtensionAPI, ExtensionCommandContext, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { loadConfig } from "../../config/config.ts";
|
|
3
|
+
import { atomicWriteFile } from "../../state/atomic-write.ts";
|
|
3
4
|
// Lazy-loaded: team-tool.ts pulls in entire runtime chain (1.4s+).
|
|
4
5
|
import type { handleTeamTool as HandleTeamToolFn } from "../team-tool.ts";
|
|
5
6
|
|
|
@@ -1113,7 +1114,11 @@ export function registerTeamCommands(pi: ExtensionAPI, deps: RegisterTeamCommand
|
|
|
1113
1114
|
const useProject = tokens.includes("--project");
|
|
1114
1115
|
const varEntries = tokens
|
|
1115
1116
|
.filter((t) => t.startsWith("--var=") || t.startsWith("--var "))
|
|
1116
|
-
.map((t)
|
|
1117
|
+
.map((t): [string, string] => {
|
|
1118
|
+
const s = t.replace(/^--var(?:\s+|=)/, "");
|
|
1119
|
+
const idx = s.indexOf("=");
|
|
1120
|
+
return idx === -1 ? [s, ""] : [s.slice(0, idx), s.slice(idx + 1)];
|
|
1121
|
+
});
|
|
1117
1122
|
const templateId = tokens.find((t) => !t.startsWith("--") && !t.includes("="));
|
|
1118
1123
|
if (!templateId) {
|
|
1119
1124
|
await notifyCommandResult(
|
|
@@ -1171,7 +1176,7 @@ export function registerTeamCommands(pi: ExtensionAPI, deps: RegisterTeamCommand
|
|
|
1171
1176
|
const skillPath = path.join(skillDir, "SKILL.md");
|
|
1172
1177
|
try {
|
|
1173
1178
|
fs.mkdirSync(skillDir, { recursive: true });
|
|
1174
|
-
|
|
1179
|
+
atomicWriteFile(skillPath, instantiated.content);
|
|
1175
1180
|
await notifyCommandResult(
|
|
1176
1181
|
ctx,
|
|
1177
1182
|
`Created skill '${template.id}' at:\n${skillPath}\n\n${instantiated.content.slice(0, 200)}...`,
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* RegistrationContext builder.
|
|
3
|
+
*
|
|
4
|
+
* Pure construction of the mutable state bag passed to every install*
|
|
5
|
+
* helper under `./registration/`. Kept in its own file because the
|
|
6
|
+
* initialization is verbose (every field of the orchestrator's state
|
|
7
|
+
* is initialized here) and bundling it into `register.ts` would push
|
|
8
|
+
* that file past its size budget.
|
|
9
|
+
*/
|
|
10
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
11
|
+
import { loadConfig } from "../../config/config.ts";
|
|
12
|
+
import { initI18n } from "../../i18n.ts";
|
|
13
|
+
import { BatchBarrier } from "../../runtime/batch-barrier.ts";
|
|
14
|
+
import { createManifestCache } from "../../runtime/manifest-cache.ts";
|
|
15
|
+
import { createRunSnapshotCache } from "../../ui/run-snapshot-cache.ts";
|
|
16
|
+
import type { CrewWidgetState } from "../../ui/widget/index.ts";
|
|
17
|
+
import { logInternalError } from "../../utils/internal-error.ts";
|
|
18
|
+
import type { AsyncNotifierState } from "../async-notifier.ts";
|
|
19
|
+
import type { NotificationDescriptor } from "../notification-router.ts";
|
|
20
|
+
import type { LifecycleState } from "./lifecycle.ts";
|
|
21
|
+
import type { ObservabilityState } from "./observability.ts";
|
|
22
|
+
import type { RegistrationContext } from "./registration-types.ts";
|
|
23
|
+
import { sendFollowUp } from "./subagent-helpers.ts";
|
|
24
|
+
import type { UiState } from "./ui.ts";
|
|
25
|
+
|
|
26
|
+
/** Module-level slot for `disposeI18n` so the cleanup helpers can find it. */
|
|
27
|
+
const RUNTIME_CLEANUP_STORE_KEY = Symbol("__piCrewRuntimeCleanup");
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Build the mutable RegistrationContext. All state + bound closures
|
|
31
|
+
* that the extracted modules need are hung off this object.
|
|
32
|
+
*
|
|
33
|
+
* The shape is built once with placeholder closures for fields that the
|
|
34
|
+
* install* helpers under registration/ will rebind. Two of them
|
|
35
|
+
* (`notifyOperator`, `getManifestCache`/`getRunSnapshotCache`) are
|
|
36
|
+
* constructed inline because they only depend on the ctx itself, not
|
|
37
|
+
* on later install steps.
|
|
38
|
+
*/
|
|
39
|
+
export function buildRegistrationContext(pi: ExtensionAPI): RegistrationContext {
|
|
40
|
+
const ctx: RegistrationContext = {
|
|
41
|
+
pi,
|
|
42
|
+
currentCtx: undefined,
|
|
43
|
+
sessionGeneration: 0,
|
|
44
|
+
cleanedUp: false,
|
|
45
|
+
rpcHandle: undefined,
|
|
46
|
+
manifestCache: createManifestCache(process.cwd()),
|
|
47
|
+
runSnapshotCache: createRunSnapshotCache(process.cwd()),
|
|
48
|
+
cacheCwd: process.cwd(),
|
|
49
|
+
getManifestCache: undefined as never,
|
|
50
|
+
getRunSnapshotCache: undefined as never,
|
|
51
|
+
widgetState: { frame: 0 } as CrewWidgetState,
|
|
52
|
+
uiState: { liveSidebarRunId: undefined, dashboardOpened: false } as UiState,
|
|
53
|
+
observabilityState: {
|
|
54
|
+
metricRegistry: undefined,
|
|
55
|
+
eventMetricSub: undefined,
|
|
56
|
+
metricSink: undefined,
|
|
57
|
+
heartbeatWatcher: undefined,
|
|
58
|
+
autoRepairTimer: undefined,
|
|
59
|
+
tempReconcileTimer: undefined,
|
|
60
|
+
otlpExporter: undefined,
|
|
61
|
+
} as ObservabilityState,
|
|
62
|
+
lifecycleState: {
|
|
63
|
+
notifierStarted: false,
|
|
64
|
+
notificationSink: undefined,
|
|
65
|
+
notificationRouter: undefined,
|
|
66
|
+
deliveryCoordinator: undefined,
|
|
67
|
+
overflowTracker: undefined,
|
|
68
|
+
} as LifecycleState,
|
|
69
|
+
notifierState: { seenFinishedRunIds: new Set() } as AsyncNotifierState,
|
|
70
|
+
foregroundControllers: new Map(),
|
|
71
|
+
foregroundTeamRunControllers: new Map(),
|
|
72
|
+
renderScheduler: undefined,
|
|
73
|
+
renderSchedulerUnsubscribers: [],
|
|
74
|
+
terminalStatus: undefined,
|
|
75
|
+
terminalStatusActive: false,
|
|
76
|
+
crewScheduler: undefined,
|
|
77
|
+
preloadTimer: undefined,
|
|
78
|
+
crewRunWatchers: undefined,
|
|
79
|
+
userCrewWatchers: undefined,
|
|
80
|
+
subagentManager: undefined as never,
|
|
81
|
+
batchBarrier: new BatchBarrier(),
|
|
82
|
+
crewAutocompleteRegistered: false,
|
|
83
|
+
autoRecoveryLast: new Map(),
|
|
84
|
+
AUTO_RECOVERY_LAST_MAX_ENTRIES: 1000,
|
|
85
|
+
disposeI18n: initI18n(pi),
|
|
86
|
+
globalStore: globalThis as Record<string | symbol, unknown>,
|
|
87
|
+
runtimeCleanupStoreKey: RUNTIME_CLEANUP_STORE_KEY,
|
|
88
|
+
captureSessionGeneration: () => ctx.sessionGeneration,
|
|
89
|
+
isOwnerSessionCurrent: (gen) => !ctx.cleanedUp && (gen === undefined || gen === ctx.sessionGeneration),
|
|
90
|
+
isContextCurrent: (c, gen) => !ctx.cleanedUp && ctx.currentCtx === c && ctx.sessionGeneration === gen,
|
|
91
|
+
telemetryEnabled: () => loadConfig(ctx.currentCtx?.cwd ?? process.cwd()).config.telemetry?.enabled !== false,
|
|
92
|
+
notifyOperator: undefined as never,
|
|
93
|
+
cleanupSessionResourcesOnly: () => {},
|
|
94
|
+
cleanupRuntime: () => {},
|
|
95
|
+
disposeRenderSchedulerSubscriptions: () => {},
|
|
96
|
+
stopSessionBoundSubagents: () => {},
|
|
97
|
+
configureNotifications: () => {},
|
|
98
|
+
configureObservability: () => {},
|
|
99
|
+
configureDeliveryCoordinator: () => {},
|
|
100
|
+
importCrashRecovery: undefined as never,
|
|
101
|
+
purgeStaleActiveRunIndexSyncIfLoaded: () => {},
|
|
102
|
+
startForegroundRun: undefined as never,
|
|
103
|
+
abortForegroundRun: () => false,
|
|
104
|
+
openLiveSidebar: () => {},
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
ctx.notifyOperator = (notification: NotificationDescriptor): void => {
|
|
108
|
+
try {
|
|
109
|
+
ctx.lifecycleState.notificationRouter?.enqueue(notification);
|
|
110
|
+
} catch (error) {
|
|
111
|
+
logInternalError("register.notification", error);
|
|
112
|
+
void sendFollowUp(pi, [notification.title, notification.body].filter((line): line is string => Boolean(line)).join("\n"));
|
|
113
|
+
}
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
ctx.getManifestCache = (cwd: string) => {
|
|
117
|
+
if (ctx.manifestCache && ctx.cacheCwd === cwd) return ctx.manifestCache;
|
|
118
|
+
if (ctx.manifestCache) ctx.manifestCache.dispose();
|
|
119
|
+
if (ctx.runSnapshotCache) ctx.runSnapshotCache.dispose?.();
|
|
120
|
+
ctx.cacheCwd = cwd;
|
|
121
|
+
ctx.manifestCache = createManifestCache(cwd);
|
|
122
|
+
ctx.runSnapshotCache = createRunSnapshotCache(cwd);
|
|
123
|
+
return ctx.manifestCache;
|
|
124
|
+
};
|
|
125
|
+
ctx.getRunSnapshotCache = (cwd: string) => {
|
|
126
|
+
if (ctx.cacheCwd !== cwd) ctx.getManifestCache(cwd);
|
|
127
|
+
return ctx.runSnapshotCache;
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
return ctx;
|
|
131
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Crash-recovery lazy import + sync purge helper.
|
|
3
|
+
*
|
|
4
|
+
* The session_start handler needs `cancelOrphanedRuns` and
|
|
5
|
+
* `purgeStaleActiveRunIndex` from runtime/crash-recovery.ts, but that
|
|
6
|
+
* module is ~14 KB and only relevant on the orphan-cleanup path.
|
|
7
|
+
*
|
|
8
|
+
* `importCrashRecovery` defers the actual `import()` until first use,
|
|
9
|
+
* caches the result on a module-level slot, and `purgeStaleActiveRunIndexSyncIfLoaded`
|
|
10
|
+
* fires the purge only if the cache has been populated (cleanup runs
|
|
11
|
+
* synchronously and may not have triggered the lazy import yet).
|
|
12
|
+
*
|
|
13
|
+
* Module-level cache is intentional: it survives across multiple
|
|
14
|
+
* `registerPiTeams` calls during dev hot-reload, so we don't reload
|
|
15
|
+
* the heavy crash-recovery module on every reload.
|
|
16
|
+
*/
|
|
17
|
+
import type {
|
|
18
|
+
cancelOrphanedRuns as CancelOrphanedRunsFn,
|
|
19
|
+
detectInterruptedRuns as DetectInterruptedRunsFn,
|
|
20
|
+
purgeStaleActiveRunIndex as PurgeStaleActiveRunIndexFn,
|
|
21
|
+
} from "../../runtime/crash-recovery.ts";
|
|
22
|
+
import { logInternalError } from "../../utils/internal-error.ts";
|
|
23
|
+
|
|
24
|
+
export type CrashRecoveryCache = {
|
|
25
|
+
cancelOrphanedRuns: typeof CancelOrphanedRunsFn;
|
|
26
|
+
detectInterruptedRuns: typeof DetectInterruptedRunsFn;
|
|
27
|
+
purgeStaleActiveRunIndex: typeof PurgeStaleActiveRunIndexFn;
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
let _cachedCrashRecovery: CrashRecoveryCache | undefined;
|
|
31
|
+
|
|
32
|
+
/** Lazy-import crash-recovery. Cached after first call. */
|
|
33
|
+
export async function importCrashRecovery(): Promise<CrashRecoveryCache> {
|
|
34
|
+
if (!_cachedCrashRecovery) {
|
|
35
|
+
// LAZY: defer crash-recovery (~14 KB) until session_start cleanup runs.
|
|
36
|
+
const mod = await import("../../runtime/crash-recovery.ts");
|
|
37
|
+
_cachedCrashRecovery = {
|
|
38
|
+
cancelOrphanedRuns: mod.cancelOrphanedRuns,
|
|
39
|
+
detectInterruptedRuns: mod.detectInterruptedRuns,
|
|
40
|
+
purgeStaleActiveRunIndex: mod.purgeStaleActiveRunIndex,
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
return _cachedCrashRecovery;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Sync purge-if-loaded helper used by cleanup functions. */
|
|
47
|
+
export function purgeStaleActiveRunIndexSyncIfLoaded(): void {
|
|
48
|
+
if (!_cachedCrashRecovery) return;
|
|
49
|
+
try {
|
|
50
|
+
_cachedCrashRecovery.purgeStaleActiveRunIndex();
|
|
51
|
+
} catch (error) {
|
|
52
|
+
logInternalError("register.cleanupRuntime.purgeStale", error);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Foreground run controller for pi-crew.
|
|
3
|
+
*
|
|
4
|
+
* Owns the three helpers consumed by the team-tool / subagent-tools /
|
|
5
|
+
* team-commands modules:
|
|
6
|
+
* • `startForegroundRun` — spawn a foreground team run with watchdog,
|
|
7
|
+
* abort signal, and completion-handling (status notification + widget
|
|
8
|
+
* refresh + crew.run.* event emission).
|
|
9
|
+
* • `abortForegroundRun` — abort a foreground team run by runId.
|
|
10
|
+
* • `openLiveSidebar` — open the per-run live sidebar UI overlay.
|
|
11
|
+
*
|
|
12
|
+
* These three are conceptually one unit (all manipulate the same
|
|
13
|
+
* foreground-team-run controllers + live sidebar state), so they share
|
|
14
|
+
* a single file.
|
|
15
|
+
*/
|
|
16
|
+
import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
17
|
+
import { loadConfig } from "../../config/config.ts";
|
|
18
|
+
import { loadRunManifestById, updateRunStatus } from "../../state/state-store.ts";
|
|
19
|
+
import { setWorkingIndicator } from "../../ui/pi-ui-compat.ts";
|
|
20
|
+
import { requestPowerbarUpdate } from "../../ui/powerbar-publisher.ts";
|
|
21
|
+
import { updateCrewWidget } from "../../ui/widget/index.ts";
|
|
22
|
+
import { logInternalError } from "../../utils/internal-error.ts";
|
|
23
|
+
import type { RegistrationContext } from "./registration-types.ts";
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Install the foreground-run controller helpers into the registration
|
|
27
|
+
* context. Re-call safe — each call rebinds `ctx.startForegroundRun`,
|
|
28
|
+
* `ctx.abortForegroundRun`, and `ctx.openLiveSidebar` to fresh closures
|
|
29
|
+
* (the underlying state lives on `ctx`).
|
|
30
|
+
*/
|
|
31
|
+
export function installForegroundRunController(pi: ExtensionAPI, ctx: RegistrationContext): void {
|
|
32
|
+
ctx.openLiveSidebar = (extCtx, runId) => {
|
|
33
|
+
void openLiveSidebarImpl(pi, ctx, extCtx, runId);
|
|
34
|
+
};
|
|
35
|
+
ctx.startForegroundRun = (extCtx, runner, runId) => startForegroundRunImpl(pi, ctx, extCtx, runner, runId);
|
|
36
|
+
ctx.abortForegroundRun = (runId) => {
|
|
37
|
+
const controller = ctx.foregroundTeamRunControllers.get(runId);
|
|
38
|
+
if (!controller) return false;
|
|
39
|
+
controller.abort();
|
|
40
|
+
return true;
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Open the live sidebar for a specific run.
|
|
46
|
+
*
|
|
47
|
+
* Defers the actual install to registration/ui.ts via a lazy dynamic
|
|
48
|
+
* import — only invoked when a sidebar is actually requested, never at
|
|
49
|
+
* module load.
|
|
50
|
+
*/
|
|
51
|
+
async function openLiveSidebarImpl(
|
|
52
|
+
pi: ExtensionAPI,
|
|
53
|
+
ctx: RegistrationContext,
|
|
54
|
+
extensionCtx: ExtensionContext,
|
|
55
|
+
runId: string,
|
|
56
|
+
): Promise<void> {
|
|
57
|
+
// LAZY: registration/ui pulls in transcript-viewer + heavy UI modules.
|
|
58
|
+
const uiModule = await import("./ui.ts");
|
|
59
|
+
await uiModule.installLiveSidebar(extensionCtx, runId, ctx.uiState, {
|
|
60
|
+
pi,
|
|
61
|
+
widgetState: ctx.widgetState,
|
|
62
|
+
getManifestCache: ctx.getManifestCache,
|
|
63
|
+
getRunSnapshotCache: ctx.getRunSnapshotCache,
|
|
64
|
+
isCleanedUp: () => ctx.cleanedUp,
|
|
65
|
+
getCurrentCtx: () => ctx.currentCtx,
|
|
66
|
+
});
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Spawn a foreground team run.
|
|
71
|
+
*
|
|
72
|
+
* The runner executes on the next macrotask (`setImmediate`) so this
|
|
73
|
+
* function returns synchronously to Pi. The runner receives an AbortSignal
|
|
74
|
+
* that fires when:
|
|
75
|
+
* • `abortForegroundRun(runId)` is called (e.g., from a tool cancel),
|
|
76
|
+
* • `cleanupRuntime()` fires (session shutdown with reason=quit/reload).
|
|
77
|
+
*
|
|
78
|
+
* A foreground-watchdog is started per runId to surface hung runs to the
|
|
79
|
+
* assistant. The .finally block handles: abort propagation, working-message
|
|
80
|
+
* clearing, status notification, run-completed session entry, and crew.run.*
|
|
81
|
+
* event emission.
|
|
82
|
+
*/
|
|
83
|
+
function startForegroundRunImpl(
|
|
84
|
+
pi: ExtensionAPI,
|
|
85
|
+
ctx: RegistrationContext,
|
|
86
|
+
extensionCtx: ExtensionContext,
|
|
87
|
+
runner: (signal?: AbortSignal) => Promise<void>,
|
|
88
|
+
runId?: string,
|
|
89
|
+
): void {
|
|
90
|
+
const ownerGeneration = ctx.captureSessionGeneration();
|
|
91
|
+
const controller = new AbortController();
|
|
92
|
+
const key = runId ?? Symbol();
|
|
93
|
+
ctx.foregroundTeamRunControllers.set(key, controller);
|
|
94
|
+
if (extensionCtx.hasUI) {
|
|
95
|
+
setWorkingIndicator(extensionCtx, {
|
|
96
|
+
frames: ["⣾", "⣽", "⣻", "⢿", "⡿", "⣟", "⣯", "⣷"],
|
|
97
|
+
intervalMs: 80,
|
|
98
|
+
});
|
|
99
|
+
extensionCtx.ui.setWorkingMessage(runId ? `pi-crew foreground run ${runId}...` : "pi-crew foreground run...");
|
|
100
|
+
}
|
|
101
|
+
if (runId) {
|
|
102
|
+
void import("../../runtime/foreground-watchdog.ts")
|
|
103
|
+
.then(({ startForegroundWatchdog }) => {
|
|
104
|
+
startForegroundWatchdog({ pi, cwd: extensionCtx.cwd, runId });
|
|
105
|
+
})
|
|
106
|
+
.catch((error) => {
|
|
107
|
+
logInternalError("register.foreground-watchdog-import", error);
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
setImmediate(() => {
|
|
111
|
+
void runner(controller.signal)
|
|
112
|
+
.catch((error) => {
|
|
113
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
114
|
+
if (runId) {
|
|
115
|
+
try {
|
|
116
|
+
const loaded = loadRunManifestById(extensionCtx.cwd, runId);
|
|
117
|
+
if (
|
|
118
|
+
loaded &&
|
|
119
|
+
loaded.manifest.status !== "completed" &&
|
|
120
|
+
loaded.manifest.status !== "failed" &&
|
|
121
|
+
loaded.manifest.status !== "cancelled" &&
|
|
122
|
+
loaded.manifest.status !== "blocked"
|
|
123
|
+
)
|
|
124
|
+
updateRunStatus(loaded.manifest, "failed", message);
|
|
125
|
+
} catch (statusError) {
|
|
126
|
+
logInternalError("register.foreground-run-failure", statusError, `runId=${runId}`);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
if (ctx.isContextCurrent(extensionCtx, ownerGeneration)) {
|
|
130
|
+
extensionCtx.ui.notify(`pi-crew foreground run failed: ${message}`, "error");
|
|
131
|
+
} else {
|
|
132
|
+
logInternalError("register.foreground-run-failure", error, `runId=${runId} context disposed`);
|
|
133
|
+
}
|
|
134
|
+
})
|
|
135
|
+
.finally(() => {
|
|
136
|
+
ctx.foregroundTeamRunControllers.delete(key);
|
|
137
|
+
if (runId) {
|
|
138
|
+
void import("../../runtime/foreground-watchdog.ts")
|
|
139
|
+
.then(({ stopWatchdog }) => {
|
|
140
|
+
stopWatchdog(runId);
|
|
141
|
+
})
|
|
142
|
+
.catch((error) => logInternalError("register.foreground-watchdog", error, `runId=${runId}`));
|
|
143
|
+
}
|
|
144
|
+
const ownerCurrent = ctx.isContextCurrent(extensionCtx, ownerGeneration);
|
|
145
|
+
if (extensionCtx.hasUI) {
|
|
146
|
+
try {
|
|
147
|
+
setWorkingIndicator(extensionCtx);
|
|
148
|
+
extensionCtx.ui.setWorkingMessage();
|
|
149
|
+
} catch {
|
|
150
|
+
/* ignore */
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
if (ownerCurrent && runId) {
|
|
154
|
+
const loaded = loadRunManifestById(extensionCtx.cwd, runId);
|
|
155
|
+
const status = loaded?.manifest.status ?? "finished";
|
|
156
|
+
const level = status === "failed" || status === "blocked" ? "error" : status === "cancelled" ? "warning" : "info";
|
|
157
|
+
extensionCtx.ui.notify(
|
|
158
|
+
`pi-crew run ${runId} ${status}. Use /team-summary ${runId} or /team-status ${runId}.`,
|
|
159
|
+
level as "info" | "warning" | "error",
|
|
160
|
+
);
|
|
161
|
+
pi.appendEntry("crew:run-completed", {
|
|
162
|
+
runId,
|
|
163
|
+
team: loaded?.manifest.team,
|
|
164
|
+
workflow: loaded?.manifest.workflow,
|
|
165
|
+
goal: loaded?.manifest.goal,
|
|
166
|
+
status,
|
|
167
|
+
taskCount: loaded?.tasks.length,
|
|
168
|
+
durationMs: loaded?.manifest.createdAt ? Date.now() - new Date(loaded.manifest.createdAt).getTime() : 0,
|
|
169
|
+
timestamp: Date.now(),
|
|
170
|
+
});
|
|
171
|
+
const eventType =
|
|
172
|
+
status === "completed"
|
|
173
|
+
? "crew.run.completed"
|
|
174
|
+
: status === "failed" || status === "blocked"
|
|
175
|
+
? "crew.run.failed"
|
|
176
|
+
: status === "cancelled"
|
|
177
|
+
? "crew.run.cancelled"
|
|
178
|
+
: undefined;
|
|
179
|
+
if (eventType) {
|
|
180
|
+
pi.events?.emit?.(eventType, {
|
|
181
|
+
runId,
|
|
182
|
+
team: loaded?.manifest.team,
|
|
183
|
+
workflow: loaded?.manifest.workflow,
|
|
184
|
+
status,
|
|
185
|
+
taskCount: loaded?.tasks.length,
|
|
186
|
+
goal: loaded?.manifest.goal,
|
|
187
|
+
durationMs: loaded?.manifest.createdAt ? Date.now() - new Date(loaded.manifest.createdAt).getTime() : 0,
|
|
188
|
+
});
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
// Emit per-task events so task-level metrics (crew.task.count,
|
|
192
|
+
// crew.task.duration_ms, crew.task.tokens_total) record actual values.
|
|
193
|
+
const terminalTaskStatuses = new Set(["completed", "failed", "needs_attention"]);
|
|
194
|
+
for (const task of loaded?.tasks ?? []) {
|
|
195
|
+
if (!terminalTaskStatuses.has(task.status)) continue;
|
|
196
|
+
const taskEventType =
|
|
197
|
+
task.status === "completed"
|
|
198
|
+
? "crew.task.completed"
|
|
199
|
+
: task.status === "failed"
|
|
200
|
+
? "crew.task.failed"
|
|
201
|
+
: "crew.task.needs_attention";
|
|
202
|
+
pi.events?.emit?.(taskEventType, {
|
|
203
|
+
runId,
|
|
204
|
+
team: loaded?.manifest.team,
|
|
205
|
+
workflow: loaded?.manifest.workflow,
|
|
206
|
+
taskId: task.id,
|
|
207
|
+
taskCount: loaded?.tasks.length,
|
|
208
|
+
role: task.role,
|
|
209
|
+
durationMs:
|
|
210
|
+
task.startedAt && task.finishedAt
|
|
211
|
+
? new Date(task.finishedAt).getTime() - new Date(task.startedAt).getTime()
|
|
212
|
+
: 0,
|
|
213
|
+
tokens: (task.usage?.input ?? 0) + (task.usage?.output ?? 0),
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
if (ownerCurrent && ctx.currentCtx) {
|
|
218
|
+
const config = loadConfig(ctx.currentCtx.cwd).config.ui;
|
|
219
|
+
updateCrewWidget(
|
|
220
|
+
ctx.currentCtx,
|
|
221
|
+
ctx.widgetState,
|
|
222
|
+
config,
|
|
223
|
+
ctx.getManifestCache(ctx.currentCtx.cwd),
|
|
224
|
+
ctx.getRunSnapshotCache(ctx.currentCtx.cwd),
|
|
225
|
+
);
|
|
226
|
+
requestPowerbarUpdate(
|
|
227
|
+
pi.events,
|
|
228
|
+
ctx.currentCtx.cwd,
|
|
229
|
+
config,
|
|
230
|
+
ctx.getManifestCache(ctx.currentCtx.cwd),
|
|
231
|
+
ctx.getRunSnapshotCache(ctx.currentCtx.cwd),
|
|
232
|
+
ctx.currentCtx,
|
|
233
|
+
ctx.widgetState.notificationCount ?? 0,
|
|
234
|
+
);
|
|
235
|
+
}
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generic event-hook installer for pi-crew.
|
|
3
|
+
*
|
|
4
|
+
* Owns the non-lifecycle `pi.on(...)` subscribers:
|
|
5
|
+
* • `tool_call` — destructive-team-action permission gate.
|
|
6
|
+
* • `tool_result` — per-write validation (catches malformed config at
|
|
7
|
+
* write time, not at next load).
|
|
8
|
+
* • `resources_discover` — injects pi-crew skill paths.
|
|
9
|
+
*
|
|
10
|
+
* Session-lifecycle hooks (`session_start`, `session_shutdown`,
|
|
11
|
+
* `session_before_switch`) live in `lifecycle-handlers.ts` because they
|
|
12
|
+
* are heavy and own the deferred-cleanup + scheduler + render-loop setup.
|
|
13
|
+
*
|
|
14
|
+
* Compaction-guard hooks live in `compaction-guard.ts` because they were
|
|
15
|
+
* already extracted; this file does NOT re-register them.
|
|
16
|
+
*/
|
|
17
|
+
import * as fs from "node:fs";
|
|
18
|
+
import * as path from "node:path";
|
|
19
|
+
import { fileURLToPath } from "node:url";
|
|
20
|
+
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
21
|
+
import { asRecord, loadConfig } from "../../config/config.ts";
|
|
22
|
+
import { buildValidationBlocker, extractPathFromInput, validateWrittenFile } from "../../runtime/per-write-validator.ts";
|
|
23
|
+
import { resolveContainedPath } from "../../utils/safe-paths.ts";
|
|
24
|
+
import { shouldBlockDestructiveTeamAction } from "../team-tool/destructive-gate.ts";
|
|
25
|
+
import type { RegistrationContext } from "./registration-types.ts";
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Register all non-lifecycle event hooks on the ExtensionAPI.
|
|
29
|
+
*
|
|
30
|
+
* Safe to call once during `registerPiTeams`. Does not stack idempotency
|
|
31
|
+
* guards — these hooks are intentionally re-registered on reload (the
|
|
32
|
+
* orchestrator's `previousRuntimeCleanup` handles cleanup of derived state).
|
|
33
|
+
*/
|
|
34
|
+
export function installPiHooks(pi: ExtensionAPI, ctx: RegistrationContext): void {
|
|
35
|
+
installResourcesDiscoverHook(pi, ctx);
|
|
36
|
+
installToolCallHook(pi, ctx);
|
|
37
|
+
installToolResultHook(pi, ctx);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Phase 11a: Dynamic resource discovery — inject pi-crew skill paths.
|
|
42
|
+
*
|
|
43
|
+
* Older Pi versions without `resources_discover` are tolerated (try/catch
|
|
44
|
+
* around pi.on; missing support is a no-op).
|
|
45
|
+
*/
|
|
46
|
+
function installResourcesDiscoverHook(pi: ExtensionAPI, ctx: RegistrationContext): void {
|
|
47
|
+
try {
|
|
48
|
+
pi.on("resources_discover", () => {
|
|
49
|
+
const sessionCwd = ctx.currentCtx?.cwd ?? process.cwd();
|
|
50
|
+
const skillDir = path.resolve(sessionCwd, "skills");
|
|
51
|
+
const extSkillDir = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..", "..", "skills");
|
|
52
|
+
const paths: string[] = [];
|
|
53
|
+
if (fs.existsSync(extSkillDir)) paths.push(extSkillDir);
|
|
54
|
+
if (skillDir !== extSkillDir && fs.existsSync(skillDir)) {
|
|
55
|
+
// Validate skillDir is within sessionCwd to prevent path traversal
|
|
56
|
+
try {
|
|
57
|
+
resolveContainedPath(sessionCwd, "skills");
|
|
58
|
+
paths.push(skillDir);
|
|
59
|
+
} catch {
|
|
60
|
+
// skillDir outside sessionCwd boundary — skip
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return paths.length > 0 ? { skillPaths: paths } : {};
|
|
64
|
+
});
|
|
65
|
+
} catch {
|
|
66
|
+
/* older Pi without resources_discover */
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Phase 1.4: Permission gate for destructive team actions.
|
|
72
|
+
*
|
|
73
|
+
* AGENTS.md requires `confirm: true` for management deletes. We enforce it
|
|
74
|
+
* at the tool_call boundary so the user gets a clear error rather than
|
|
75
|
+
* executing a destructive action silently.
|
|
76
|
+
*/
|
|
77
|
+
function installToolCallHook(_pi: ExtensionAPI, _ctx: RegistrationContext): void {
|
|
78
|
+
_pi.on("tool_call", async (event) => {
|
|
79
|
+
if (event.toolName !== "team") return;
|
|
80
|
+
const rawInput = event.input;
|
|
81
|
+
if (!rawInput || typeof rawInput !== "object") return;
|
|
82
|
+
const input = asRecord(rawInput);
|
|
83
|
+
if (!input) return;
|
|
84
|
+
const action = typeof input.action === "string" ? input.action : undefined;
|
|
85
|
+
const reason = shouldBlockDestructiveTeamAction(action, input);
|
|
86
|
+
if (!reason) return;
|
|
87
|
+
return {
|
|
88
|
+
block: true as const,
|
|
89
|
+
reason,
|
|
90
|
+
};
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* T5 (v0.8.5): per-write validation. On write/edit, run a zero-cost
|
|
96
|
+
* SYNCHRONOUS validator (v1: JSON.parse) and append a 🔴 blocker to the
|
|
97
|
+
* tool result on failure — catches malformed config the moment it's
|
|
98
|
+
* written, not at the next load. Latency-safe by construction: no process
|
|
99
|
+
* spawn, one disk read ONLY for validated extensions, dedup'd by content.
|
|
100
|
+
* Toggle via runtime.reliability.perWriteValidation (default true).
|
|
101
|
+
* Process-spawning validators (.js/.sh/.py) are a future opt-in.
|
|
102
|
+
*/
|
|
103
|
+
function installToolResultHook(_pi: ExtensionAPI, _ctx: RegistrationContext): void {
|
|
104
|
+
_pi.on("tool_result", (event, ctx) => {
|
|
105
|
+
try {
|
|
106
|
+
if (event.toolName !== "write" && event.toolName !== "edit") return;
|
|
107
|
+
if (loadConfig(ctx.cwd).config.reliability?.perWriteValidation === false) return;
|
|
108
|
+
const filePath = extractPathFromInput(event.input);
|
|
109
|
+
if (!filePath) return;
|
|
110
|
+
const result = validateWrittenFile(filePath);
|
|
111
|
+
if (!result || result.ok) return;
|
|
112
|
+
return {
|
|
113
|
+
content: [...event.content, buildValidationBlocker(filePath, result.error ?? "validation failed")],
|
|
114
|
+
};
|
|
115
|
+
} catch {
|
|
116
|
+
// best-effort: never break a tool result
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
}
|