pi-crew 0.10.1 → 0.10.3
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 +347 -0
- package/NOTICE.md +21 -0
- package/README.md +44 -2
- package/agents/executor.md +1 -1
- package/agents/test-engineer.md +1 -1
- package/dist/index.mjs +2555 -932
- package/package.json +2 -1
- package/schema.json +503 -94
- package/skills/real-test-pi-crew/REPORT-TEMPLATE.md +6 -2
- package/skills/real-test-pi-crew/SKILL.md +278 -79
- package/src/config/config-merge.ts +11 -1
- package/src/config/config-validation.ts +47 -2
- package/src/config/config.ts +28 -6
- package/src/config/defaults.ts +43 -11
- package/src/config/env-vars.ts +27 -2
- package/src/config/role-tools.ts +4 -2
- package/src/config/types.ts +55 -1
- package/src/extension/crew-cleanup.ts +13 -0
- package/src/extension/crew-vibes/footer.ts +19 -0
- package/src/extension/crew-vibes/index.ts +11 -1
- package/src/extension/register.ts +8 -0
- package/src/extension/registration/command-registration.ts +1 -0
- package/src/extension/registration/commands/run.ts +15 -1
- package/src/extension/registration/commands/shared.ts +8 -0
- package/src/extension/registration/foreground-run-controller.ts +10 -2
- package/src/extension/registration/lifecycle-handlers.ts +92 -11
- package/src/extension/registration/runtime-cleanup.ts +23 -5
- package/src/extension/registration/team-tool.ts +58 -6
- package/src/extension/registration/ui.ts +5 -4
- package/src/extension/team-tool/doctor.ts +364 -7
- package/src/extension/team-tool/handle-settings.ts +19 -0
- package/src/extension/team-tool/inspect.ts +10 -2
- package/src/extension/team-tool/run-deadline.ts +20 -3
- package/src/extension/team-tool/run.ts +26 -2
- package/src/extension/team-tool/status.ts +7 -0
- package/src/extension/team-tool.ts +35 -2
- package/src/hooks/registry.ts +59 -56
- package/src/prompt/inbox-poll.ts +90 -0
- package/src/prompt/message-tool.ts +166 -0
- package/src/prompt/prompt-runtime.ts +201 -18
- package/src/prompt/surface-worker.ts +720 -0
- package/src/prompt/worker-events-channel.ts +49 -3
- package/src/runtime/async-runner.ts +29 -1
- package/src/runtime/background-runner.ts +13 -7
- package/src/runtime/broker/broker-issuer.ts +27 -2
- package/src/runtime/broker/crew-broker-tokens.ts +56 -4
- package/src/runtime/broker/crew-broker.ts +261 -41
- package/src/runtime/child-pi/child-pi-constants.ts +8 -0
- package/src/runtime/child-pi/child-pi-spawn.ts +23 -9
- package/src/runtime/child-pi/child-pi-streams.ts +30 -2
- package/src/runtime/child-pi/child-pi.ts +353 -5
- package/src/runtime/crew-agent-records.ts +13 -1
- package/src/runtime/detached-run-results.ts +90 -0
- package/src/runtime/dispatch-batch.ts +12 -1
- package/src/runtime/event-log-tail-source.ts +374 -0
- package/src/runtime/finalize-run.ts +4 -0
- package/src/runtime/goal-workflow/adaptive-plan.ts +30 -3
- package/src/runtime/goal-workflow/dynamic-workflow-runner.ts +5 -1
- package/src/runtime/live-session/live-agent-manager.ts +34 -1
- package/src/runtime/live-session/live-control-realtime.ts +10 -0
- package/src/runtime/live-session/live-session-runtime.ts +47 -27
- package/src/runtime/manifest-cache.ts +128 -17
- package/src/runtime/merge-gate.ts +25 -9
- package/src/runtime/model/model-fallback.ts +7 -3
- package/src/runtime/model/pi-args.ts +54 -65
- package/src/runtime/output/sidechain-output.ts +61 -6
- package/src/runtime/process/proc-stat.ts +46 -0
- package/src/runtime/process/zombie-scanner.ts +32 -19
- package/src/runtime/process-status.ts +16 -1
- package/src/runtime/recovery/crash-recovery.ts +16 -0
- package/src/runtime/run-tracker.ts +77 -10
- package/src/runtime/spawn-policy.ts +27 -41
- package/src/runtime/surface/degrade.ts +776 -0
- package/src/runtime/surface/herdr-provider.ts +546 -0
- package/src/runtime/surface/launch-script.ts +172 -0
- package/src/runtime/surface/resolve-surface.ts +274 -0
- package/src/runtime/surface/surface-provider.ts +129 -0
- package/src/runtime/surface/surface-spawn.ts +475 -0
- package/src/runtime/surface/tmux-provider.ts +400 -0
- package/src/runtime/task-runner/child-executor.ts +47 -0
- package/src/runtime/task-runner/post-execution.ts +57 -2
- package/src/runtime/task-runner/prompt-builder.ts +1 -0
- package/src/runtime/task-runner/retrieval-orchestrator.ts +191 -56
- package/src/runtime/task-runner/state-helpers.ts +54 -30
- package/src/runtime/task-runner.ts +4 -2
- package/src/runtime/team-runner.ts +104 -2
- package/src/schema/config-schema.ts +25 -1
- package/src/state/atomic-write.ts +219 -40
- package/src/state/coordination/locks.ts +7 -5
- package/src/state/coordination/mailbox.ts +56 -10
- package/src/state/event-log/cursor.ts +413 -23
- package/src/state/event-log/event-log.ts +120 -113
- package/src/state/event-log/sequence-cache.ts +21 -3
- package/src/state/stores/plan-store.ts +1 -1
- package/src/state/stores/state-store.ts +171 -9
- package/src/state/types.ts +53 -0
- package/src/ui/dock-footer.ts +49 -0
- package/src/ui/inline-panel/agent-pane.ts +378 -0
- package/src/ui/inline-panel/agent-transcript.ts +338 -0
- package/src/ui/inline-panel/agent-view-overlay.ts +225 -0
- package/src/ui/inline-panel/crew-editor.ts +192 -0
- package/src/ui/inline-panel/index.ts +290 -0
- package/src/ui/inline-panel/panel-rows.ts +37 -0
- package/src/ui/inline-panel/panel-selection.ts +157 -0
- package/src/ui/inline-panel/panel-store.ts +111 -0
- package/src/ui/inline-panel/view-session-store.ts +36 -0
- package/src/ui/pi-ui-compat.ts +9 -0
- package/src/ui/render-diff.ts +16 -8
- package/src/ui/run-dashboard.ts +87 -42
- package/src/ui/run-event-bus.ts +10 -1
- package/src/ui/run-snapshot-cache.ts +83 -35
- package/src/ui/transcript-cache.ts +101 -13
- package/src/ui/transcript-viewer.ts +92 -24
- package/src/ui/widget/index.ts +203 -25
- package/src/ui/widget/task-list.ts +198 -0
- package/src/ui/widget/widget-formatters.ts +240 -4
- package/src/ui/widget/widget-renderer.ts +234 -37
- package/src/ui/widget/widget-types.ts +11 -0
- package/src/utils/child-process-shield.ts +106 -0
- package/src/utils/redaction.ts +7 -0
- package/src/utils/safe-abort.ts +45 -0
- package/src/utils/visual.ts +43 -0
- package/src/workflows/discover-workflows.ts +1 -0
- package/src/workflows/workflow-config.ts +7 -0
- package/src/worktree/worktree-manager.ts +65 -4
- package/workflows/default.workflow.md +36 -26
- package/workflows/strict-fast-fix.workflow.md +26 -0
|
@@ -31,7 +31,8 @@ export function mergeConfig(base: PiTeamsConfig, override: PiTeamsConfig): PiTea
|
|
|
31
31
|
}
|
|
32
32
|
if (base.nesting || override.nesting) {
|
|
33
33
|
// ADR-5: per-key user-wins deep merge — a partial user-side nesting block
|
|
34
|
-
// (e.g. just maxSlots) must not erase DEFAULT_NESTING.enabled
|
|
34
|
+
// (e.g. just maxSlots) must not erase DEFAULT_NESTING.enabled (true since
|
|
35
|
+
// the D8 flip; explicit user false still wins as the kill switch).
|
|
35
36
|
merged.nesting = {
|
|
36
37
|
...(base.nesting ?? {}),
|
|
37
38
|
...withoutUndefined((override.nesting ?? {}) as Record<string, unknown>),
|
|
@@ -59,6 +60,15 @@ export function mergeConfig(base: PiTeamsConfig, override: PiTeamsConfig): PiTea
|
|
|
59
60
|
...withoutUndefined((override.runtime?.modelFallback ?? {}) as Record<string, unknown>),
|
|
60
61
|
};
|
|
61
62
|
}
|
|
63
|
+
// Spec v0.7 §8.1 (Task 6): same per-key treatment for the surface
|
|
64
|
+
// block — a user surface with only visibleAgents must not wholesale-
|
|
65
|
+
// erase a project surface.mode (the WP-2/R2 bug class).
|
|
66
|
+
if (base.runtime?.surface || override.runtime?.surface) {
|
|
67
|
+
merged.runtime.surface = {
|
|
68
|
+
...(base.runtime?.surface ?? {}),
|
|
69
|
+
...withoutUndefined((override.runtime?.surface ?? {}) as Record<string, unknown>),
|
|
70
|
+
};
|
|
71
|
+
}
|
|
62
72
|
}
|
|
63
73
|
if (base.control || override.control) {
|
|
64
74
|
merged.control = {
|
|
@@ -23,6 +23,7 @@ import type {
|
|
|
23
23
|
CrewUiConfig,
|
|
24
24
|
CrewWorktreeConfig,
|
|
25
25
|
GoalWrapWorkflowConfig,
|
|
26
|
+
PersistenceConfig,
|
|
26
27
|
PiTeamsAutonomousConfig,
|
|
27
28
|
PiTeamsAutonomyProfile,
|
|
28
29
|
PiTeamsConfig,
|
|
@@ -155,7 +156,13 @@ function parseProfile(value: unknown): PiTeamsAutonomyProfile | undefined {
|
|
|
155
156
|
|
|
156
157
|
function parseStringList(value: unknown): string[] | undefined {
|
|
157
158
|
const items = parseWithSchema(Type.Array(Type.String()), value);
|
|
158
|
-
if (!items
|
|
159
|
+
if (!items) return undefined;
|
|
160
|
+
// F3 (real-test 2026-08-30-post-tab-layout-live, Finding 3): an explicit
|
|
161
|
+
// empty array is a VALUE ("nobody"/"none"), not "unset" — dropping it here
|
|
162
|
+
// made `team-settings set <key> []` a no-op that silently kept the previous
|
|
163
|
+
// list on disk. Whitespace-only entries below still collapse to undefined
|
|
164
|
+
// (unchanged legacy behavior).
|
|
165
|
+
if (items.length === 0) return [];
|
|
159
166
|
const normalized = items.map((entry) => entry.trim()).filter((entry) => entry.length > 0);
|
|
160
167
|
return normalized.length > 0 ? normalized : undefined;
|
|
161
168
|
}
|
|
@@ -281,6 +288,24 @@ function parseIsolationPolicy(value: unknown): CrewRuntimeConfig["isolationPolic
|
|
|
281
288
|
};
|
|
282
289
|
}
|
|
283
290
|
|
|
291
|
+
/** Mux-surface policy (spec v0.7 §8.1) — mirrors the isolationPolicy parse
|
|
292
|
+
* shape: unknown literals are dropped field-wise (never thrown), and an
|
|
293
|
+
* all-invalid block collapses to undefined. */
|
|
294
|
+
function parseSurfacePolicy(value: unknown): CrewRuntimeConfig["surface"] | undefined {
|
|
295
|
+
const obj = asRecord(value);
|
|
296
|
+
if (!obj) return undefined;
|
|
297
|
+
const mode = parseWithSchema(
|
|
298
|
+
Type.Union([Type.Literal("auto"), Type.Literal("tmux"), Type.Literal("herdr"), Type.Literal("off")]),
|
|
299
|
+
obj.mode,
|
|
300
|
+
);
|
|
301
|
+
const visibleAgents = parseStringList(obj.visibleAgents);
|
|
302
|
+
if (mode === undefined && visibleAgents === undefined) return undefined;
|
|
303
|
+
return {
|
|
304
|
+
...(mode !== undefined ? { mode } : {}),
|
|
305
|
+
...(visibleAgents !== undefined ? { visibleAgents } : {}),
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
|
|
284
309
|
/**
|
|
285
310
|
* F19-1 (Round 19 parity): runtime.modelFallback was declared in types.ts and the
|
|
286
311
|
* schema (PiTeamsModelFallbackConfigSchema) but parseRuntimeConfig never emitted
|
|
@@ -338,6 +363,7 @@ function parseRuntimeConfig(value: unknown): CrewRuntimeConfig | undefined {
|
|
|
338
363
|
excludeContextBash: parseWithSchema(Type.Boolean(), obj.excludeContextBash),
|
|
339
364
|
agentExtensions: parseStringList(obj.agentExtensions),
|
|
340
365
|
isolationPolicy: parseIsolationPolicy(obj.isolationPolicy),
|
|
366
|
+
surface: parseSurfacePolicy(obj.surface),
|
|
341
367
|
modelFallback: parseModelFallbackConfig(obj.modelFallback),
|
|
342
368
|
};
|
|
343
369
|
return Object.values(runtime).some((entry) => entry !== undefined) ? runtime : undefined;
|
|
@@ -384,6 +410,18 @@ function parseBrokerConfig(value: unknown): CrewBrokerConfig | undefined {
|
|
|
384
410
|
return Object.values(broker).some((entry) => entry !== undefined) ? broker : undefined;
|
|
385
411
|
}
|
|
386
412
|
|
|
413
|
+
/** PERF round 2, Task 3: state-layer persistence parser. Opt-in only;
|
|
414
|
+
* the default (false) is layered in loadConfig via DEFAULT_PERSISTENCE,
|
|
415
|
+
* so a missing block here means "not set", never "false" by default. */
|
|
416
|
+
function parsePersistenceConfig(value: unknown): PersistenceConfig | undefined {
|
|
417
|
+
const obj = asRecord(value);
|
|
418
|
+
if (!obj) return undefined;
|
|
419
|
+
const persistence: PersistenceConfig = {
|
|
420
|
+
skipTasksFsync: parseWithSchema(Type.Boolean(), obj.skipTasksFsync),
|
|
421
|
+
};
|
|
422
|
+
return Object.values(persistence).some((entry) => entry !== undefined) ? persistence : undefined;
|
|
423
|
+
}
|
|
424
|
+
|
|
387
425
|
function parseWorktreeConfig(value: unknown): CrewWorktreeConfig | undefined {
|
|
388
426
|
const obj = asRecord(value);
|
|
389
427
|
if (!obj) return undefined;
|
|
@@ -456,11 +494,17 @@ function parseAgentOverride(value: unknown): AgentOverrideConfig | undefined {
|
|
|
456
494
|
function parseUiConfig(value: unknown): CrewUiConfig | undefined {
|
|
457
495
|
const obj = asRecord(value);
|
|
458
496
|
if (!obj) return undefined;
|
|
459
|
-
const rawWidgetPlacement = parseWithSchema(
|
|
497
|
+
const rawWidgetPlacement = parseWithSchema(
|
|
498
|
+
Type.Union([Type.Literal("aboveEditor"), Type.Literal("belowEditor"), Type.Literal("bottom")]),
|
|
499
|
+
obj.widgetPlacement,
|
|
500
|
+
);
|
|
460
501
|
const rawDashboardPlacement = parseWithSchema(Type.Union([Type.Literal("center"), Type.Literal("right")]), obj.dashboardPlacement);
|
|
502
|
+
const rawRowStyle = parseWithSchema(Type.Union([Type.Literal("compact"), Type.Literal("detailed")]), obj.widgetRowStyle);
|
|
461
503
|
const ui: CrewUiConfig = {
|
|
462
504
|
widgetPlacement: rawWidgetPlacement,
|
|
463
505
|
widgetMaxLines: parsePositiveInteger(obj.widgetMaxLines, 50),
|
|
506
|
+
widgetRowStyle: rawRowStyle,
|
|
507
|
+
inlinePanel: parseWithSchema(Type.Boolean(), obj.inlinePanel),
|
|
464
508
|
powerbar: parseWithSchema(Type.Boolean(), obj.powerbar),
|
|
465
509
|
dashboardPlacement: rawDashboardPlacement,
|
|
466
510
|
dashboardWidth: parseIntegerInRange(obj.dashboardWidth, 32, 120),
|
|
@@ -670,6 +714,7 @@ export function parseConfig(raw: unknown): PiTeamsConfig {
|
|
|
670
714
|
otlp: parseOtlpConfig(obj.otlp),
|
|
671
715
|
ui: parseUiConfig(obj.ui),
|
|
672
716
|
broker: parseBrokerConfig(obj.broker),
|
|
717
|
+
persistence: parsePersistenceConfig(obj.persistence),
|
|
673
718
|
};
|
|
674
719
|
}
|
|
675
720
|
|
package/src/config/config.ts
CHANGED
|
@@ -7,8 +7,8 @@ import { logInternalError } from "../utils/internal-error.ts";
|
|
|
7
7
|
import { projectCrewRoot, projectPiRoot } from "../utils/paths.ts";
|
|
8
8
|
import { mergeConfig } from "./config-merge.ts";
|
|
9
9
|
import { parseConfig, parseConfigWithWarnings } from "./config-validation.ts";
|
|
10
|
-
import { DEFAULT_BROKER, DEFAULT_NESTING, resolveBrokerEnvOverride } from "./defaults.ts";
|
|
11
|
-
import { getCrewEnv } from "./env-vars.ts";
|
|
10
|
+
import { DEFAULT_BROKER, DEFAULT_NESTING, DEFAULT_PERSISTENCE, resolveBrokerEnvOverride } from "./defaults.ts";
|
|
11
|
+
import { getCrewEnv, getCrewEnvBool } from "./env-vars.ts";
|
|
12
12
|
import { sanitizeProjectConfig } from "./sanitize-project-config.ts";
|
|
13
13
|
|
|
14
14
|
// 2.9: interface types extracted to ./types.ts; re-export for back-compat.
|
|
@@ -45,6 +45,7 @@ import type {
|
|
|
45
45
|
CrewBrokerConfig,
|
|
46
46
|
CrewNestingConfig,
|
|
47
47
|
LoadedPiTeamsConfig,
|
|
48
|
+
PersistenceConfig,
|
|
48
49
|
PiTeamsAutonomousConfig,
|
|
49
50
|
PiTeamsConfig,
|
|
50
51
|
SavedPiTeamsConfig,
|
|
@@ -225,8 +226,9 @@ export function projectPiCrewJsonPath(cwd: string): string {
|
|
|
225
226
|
* config block, or default.
|
|
226
227
|
*/
|
|
227
228
|
|
|
228
|
-
/** ADR-5 §10: layer DEFAULT_NESTING under any user-set keys — the
|
|
229
|
-
*
|
|
229
|
+
/** ADR-5 §10: layer DEFAULT_NESTING under any user-set keys — the default-on
|
|
230
|
+
* enabled=true (D8 flip) must hold when no nesting block exists, and an
|
|
231
|
+
* explicit user `enabled: false` still wins as the kill switch. */
|
|
230
232
|
function applyNestingDefaults(parsed: CrewNestingConfig | undefined): CrewNestingConfig {
|
|
231
233
|
return { ...DEFAULT_NESTING, ...parsed };
|
|
232
234
|
}
|
|
@@ -235,6 +237,21 @@ function applyBrokerEnvOverrideAndDefaults(parsed: CrewBrokerConfig | undefined)
|
|
|
235
237
|
return { ...DEFAULT_BROKER, ...envOverridden };
|
|
236
238
|
}
|
|
237
239
|
|
|
240
|
+
/**
|
|
241
|
+
* Resolve the persistence section (perf round 2, Task 3).
|
|
242
|
+
* Precedence: env `PI_CREW_PERSISTENCE_SKIP_TASKS_FSYNC` ("1"/"true") BEATS a
|
|
243
|
+
* config `persistence.skipTasksFsync`; config beats the DEFAULT_PERSISTENCE
|
|
244
|
+
* default (false). Mirrors resolveBrokerEnvOverride's env-beats-config shape.
|
|
245
|
+
*/
|
|
246
|
+
function resolvePersistenceEnvOverrideAndDefaults(parsed: PersistenceConfig | undefined): PersistenceConfig {
|
|
247
|
+
const envOverride = getCrewEnvBool("PI_CREW_PERSISTENCE_SKIP_TASKS_FSYNC");
|
|
248
|
+
return {
|
|
249
|
+
...DEFAULT_PERSISTENCE,
|
|
250
|
+
...parsed,
|
|
251
|
+
...(envOverride === undefined ? {} : { skipTasksFsync: envOverride }),
|
|
252
|
+
};
|
|
253
|
+
}
|
|
254
|
+
|
|
238
255
|
function unsetPath(record: Record<string, unknown>, dottedPath: string): void {
|
|
239
256
|
const parts = dottedPath.split(".").filter(Boolean);
|
|
240
257
|
if (parts.length === 0) return;
|
|
@@ -377,15 +394,20 @@ export function loadConfig(cwd?: string): LoadedPiTeamsConfig {
|
|
|
377
394
|
// the enabled flag even when no broker block is configured.
|
|
378
395
|
broker: applyBrokerEnvOverrideAndDefaults(config.broker),
|
|
379
396
|
nesting: applyNestingDefaults(config.nesting),
|
|
397
|
+
// PERF round 2, Task 3: env beats config, config beats the default.
|
|
398
|
+
persistence: resolvePersistenceEnvOverrideAndDefaults(config.persistence),
|
|
380
399
|
},
|
|
381
400
|
warnings: warnings.length > 0 ? warnings : undefined,
|
|
382
401
|
};
|
|
402
|
+
// PERF (2026-08-24): readCacheMtimes stats up to 4 files per call and ran
|
|
403
|
+
// twice back-to-back here (guard + store). Compute once.
|
|
404
|
+
const storeMtimes = readCacheMtimes(cacheParts);
|
|
383
405
|
// Only cache when at least one of the watched paths exists — this avoids
|
|
384
406
|
// pinning stale empty results when a user later creates one of these files
|
|
385
407
|
// in the cache window. mtime stat below picks up the new file (it appears
|
|
386
408
|
// in currentMtimes but not in cached.mtimes) and triggers a re-parse.
|
|
387
|
-
if (Object.keys(
|
|
388
|
-
setConfigCache(cacheKey, result,
|
|
409
|
+
if (Object.keys(storeMtimes).length > 0) {
|
|
410
|
+
setConfigCache(cacheKey, result, storeMtimes);
|
|
389
411
|
}
|
|
390
412
|
return result;
|
|
391
413
|
}
|
package/src/config/defaults.ts
CHANGED
|
@@ -129,8 +129,15 @@ export const DEFAULT_UI = {
|
|
|
129
129
|
refreshMs: 1000,
|
|
130
130
|
notifierIntervalMs: 5000,
|
|
131
131
|
widgetDefaultFrameMs: 1000,
|
|
132
|
-
|
|
132
|
+
// The crew widget (agent rows + hint + main row) docks BELOW the editor,
|
|
133
|
+
// matching pi-subtask's subtask panel — the transcript pane separately
|
|
134
|
+
// mounts above the editor, exactly like pi-subtask's fork view.
|
|
135
|
+
// The dock renders at the very bottom of the screen, below the crew-vibes
|
|
136
|
+
// quota/meter footer (falls back to belowEditor when no footer sink exists).
|
|
137
|
+
widgetPlacement: "bottom" as const,
|
|
133
138
|
widgetMaxLines: 8,
|
|
139
|
+
widgetRowStyle: "compact" as const,
|
|
140
|
+
inlinePanel: true,
|
|
134
141
|
powerbar: true,
|
|
135
142
|
dashboardPlacement: "center" as const,
|
|
136
143
|
dashboardWidth: 72,
|
|
@@ -187,24 +194,49 @@ export const DEFAULT_BROKER = {
|
|
|
187
194
|
maxFrameBytes: 262144,
|
|
188
195
|
outboundQueueCap: 256,
|
|
189
196
|
/** WP-2/R2 (ADR-0 docs/decisions/2026-08-17-waiting-producer-ask item 7):
|
|
190
|
-
* capability gate for the broker's `wait.*` methods.
|
|
191
|
-
*
|
|
197
|
+
* capability gate for the broker's `wait.*` methods. FLIPPED TO TRUE
|
|
198
|
+
* (2026-08-26): WP-2 completed and its battery landed, so the ADR-0
|
|
199
|
+
* "then flipped to true" step is now done — ask works out of the box.
|
|
200
|
+
* (Before this flip, every production ask was rejected policy-disabled
|
|
201
|
+
* whenever no user config existed — the feature slept silently.) Set
|
|
202
|
+
* `broker.waitMethodsEnabled: false` to re-close. While false,
|
|
192
203
|
* wait.request/wait.resolve are rejected with a policy-disabled error
|
|
193
204
|
* AND a policy.action event in events.jsonl (never silent). */
|
|
194
|
-
waitMethodsEnabled:
|
|
205
|
+
waitMethodsEnabled: true,
|
|
195
206
|
} as const;
|
|
196
207
|
|
|
197
208
|
/**
|
|
198
209
|
* Governed-nesting defaults (ADR-5 docs/decisions/2026-08-17-governed-nesting.md §10).
|
|
199
|
-
* `enabled` is DEFAULT
|
|
200
|
-
* security sign-off)
|
|
201
|
-
*
|
|
202
|
-
*
|
|
203
|
-
*
|
|
210
|
+
* `enabled` is DEFAULT TRUE since D8 (spec v0.7, 2026-08-26): the WP-5
|
|
211
|
+
* completion gate (B3 battery + security sign-off) passed and Task 3 opened
|
|
212
|
+
* the `delegate` role gate for every role, so nested spawning — child creates
|
|
213
|
+
* child — is on out of the box. The security border moved to the depth cap
|
|
214
|
+
* (maxDepth 4) + the nested-slot budget; `nesting.enabled: false` in USER
|
|
215
|
+
* config (sensitive — project config cannot flip it) closes the surface, and
|
|
216
|
+
* while closed `delegate` rejects with a structured policy message plus a
|
|
217
|
+
* `delegate.rejected` event in events.jsonl (never silent). `maxSlots` default
|
|
218
|
+
* is computed at the spawn policy from the global worker semaphore:
|
|
219
|
+
* max(1, floor(globalSem/2)).
|
|
204
220
|
*/
|
|
205
221
|
export const DEFAULT_NESTING = {
|
|
206
|
-
enabled:
|
|
207
|
-
|
|
222
|
+
enabled: true,
|
|
223
|
+
// D8 (spec v0.7): nested spawning open — child creates child. Kept in
|
|
224
|
+
// lockstep with DEFAULT_MAX_CREW_DEPTH (pi-args) so the broker-admission
|
|
225
|
+
// depth gate and the spawn-side cap can never disagree.
|
|
226
|
+
maxDepth: 4,
|
|
227
|
+
} as const;
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* State-layer persistence defaults (perf round 2, Task 3).
|
|
231
|
+
* `skipTasksFsync` is DEFAULT FALSE — opt-in only. Because the flag is
|
|
232
|
+
* consumed at the save site in the state layer, `loadConfig` must READ it
|
|
233
|
+
* (not default-on-borrow): the flag affects the cost of every non-terminal
|
|
234
|
+
* tasks checkpoint, so it must never silently flip from "fsync" to "no
|
|
235
|
+
* fsync" due to a config default alone. Env `PI_CREW_PERSISTENCE_SKIP_TASKS_FSYNC`
|
|
236
|
+
* ("1"/"true") beats config; config beats this default.
|
|
237
|
+
*/
|
|
238
|
+
export const DEFAULT_PERSISTENCE = {
|
|
239
|
+
skipTasksFsync: false,
|
|
208
240
|
} as const;
|
|
209
241
|
|
|
210
242
|
/**
|
package/src/config/env-vars.ts
CHANGED
|
@@ -270,8 +270,8 @@ export const CREW_ENV_VARS: Record<string, CrewEnvVarSpec> = {
|
|
|
270
270
|
PI_CREW_INTERRUPT_GUARD_INTERVAL_MS: {
|
|
271
271
|
name: "PI_CREW_INTERRUPT_GUARD_INTERVAL_MS",
|
|
272
272
|
parser: "int",
|
|
273
|
-
default:
|
|
274
|
-
doc: "interrupt-guard poll interval; default
|
|
273
|
+
default: 1000,
|
|
274
|
+
doc: "interrupt-guard poll interval; default 1000ms (background-runner.ts:217)",
|
|
275
275
|
},
|
|
276
276
|
PI_CREW_MAX_RUN_MS: {
|
|
277
277
|
name: "PI_CREW_MAX_RUN_MS",
|
|
@@ -323,6 +323,11 @@ export const CREW_ENV_VARS: Record<string, CrewEnvVarSpec> = {
|
|
|
323
323
|
name: "PI_CREW_PLAN_UI",
|
|
324
324
|
doc: "'1' enables the Plan dashboard pane (7) + plans snapshot slice (WP-7/R7)",
|
|
325
325
|
},
|
|
326
|
+
PI_CREW_PERSISTENCE_SKIP_TASKS_FSYNC: {
|
|
327
|
+
name: "PI_CREW_PERSISTENCE_SKIP_TASKS_FSYNC",
|
|
328
|
+
parser: "boolean",
|
|
329
|
+
doc: "'1'/'true' writes non-terminal tasks checkpoints best-effort (no fsync; 50ms coalesce kept, only durability skipped); terminal transitions stay full (defaults.ts, config.ts persistence.skipTasksFsync)",
|
|
330
|
+
},
|
|
326
331
|
PI_CREW_TRUST_PROJECT_DWF: {
|
|
327
332
|
name: "PI_CREW_TRUST_PROJECT_DWF",
|
|
328
333
|
doc: "'1' allows project-sourced .dwf.ts workflows (dynamic-workflow-runner.ts:154)",
|
|
@@ -390,6 +395,10 @@ export const CREW_ENV_VARS: Record<string, CrewEnvVarSpec> = {
|
|
|
390
395
|
name: "PI_CREW_ASK_ENABLED",
|
|
391
396
|
doc: "'1' enables the worker-side ask tool — dormant-until-env gate (written UNCONDITIONALLY by child-pi-spawn.ts, read by prompt-runtime.ts per ADR-0 WP-2 item 2)",
|
|
392
397
|
},
|
|
398
|
+
PI_CREW_MSG_ENABLED: {
|
|
399
|
+
name: "PI_CREW_MSG_ENABLED",
|
|
400
|
+
doc: "'1' enables the worker-side message tool (D9/§15.2) — dormant-until-env gate (written UNCONDITIONALLY by child-pi-spawn.ts, read by prompt-runtime.ts)",
|
|
401
|
+
},
|
|
393
402
|
PI_CREW_STATE_ROOT: {
|
|
394
403
|
name: "PI_CREW_STATE_ROOT",
|
|
395
404
|
doc: "run stateRoot for the ask-tool mailbox poll (<stateRoot>/mailbox; written by child-pi-spawn.ts, read by prompt-runtime.ts per ADR-0 WP-2 item 2)",
|
|
@@ -398,6 +407,22 @@ export const CREW_ENV_VARS: Record<string, CrewEnvVarSpec> = {
|
|
|
398
407
|
name: "PI_CREW_BROKER_RUN_ID",
|
|
399
408
|
doc: "broker run id; aliases PI_CREW_RUN_ID (scratchpad-lifecycle.ts:81, crew-broker-child.ts:51)",
|
|
400
409
|
},
|
|
410
|
+
PI_CREW_AGENT_EVENTS_PATH: {
|
|
411
|
+
name: "PI_CREW_AGENT_EVENTS_PATH",
|
|
412
|
+
doc: "per-agent events log (<stateRoot>/agents/<taskId>/events.jsonl) written by the surface worker recorder (surface-worker.ts; derived by prepareSurfaceSpawn)",
|
|
413
|
+
},
|
|
414
|
+
PI_CREW_AUTO_EXIT: {
|
|
415
|
+
name: "PI_CREW_AUTO_EXIT",
|
|
416
|
+
doc: "'1' → the worker shuts its session down after the final settled turn — spec §5.2 D7 (written by prepareSurfaceSpawn, read by surface-worker.ts)",
|
|
417
|
+
},
|
|
418
|
+
PI_CREW_SURFACE: {
|
|
419
|
+
name: "PI_CREW_SURFACE",
|
|
420
|
+
doc: "surface provider kind for this worker ('tmux'|'herdr') — arms the worker-side recorder/parent-guard (written by prepareSurfaceSpawn.ts:214, read by surface-worker.ts)",
|
|
421
|
+
},
|
|
422
|
+
PI_CREW_PARENT_START_TIME: {
|
|
423
|
+
name: "PI_CREW_PARENT_START_TIME",
|
|
424
|
+
doc: "parent starttime ticks (/proc/<pid>/stat field 22) captured at spawn — pid-reuse-safe parent-guard comparison (written by prepareSurfaceSpawn.ts:223, read by surface-worker.ts)",
|
|
425
|
+
},
|
|
401
426
|
PI_CREW_ARTIFACTS_ROOT: {
|
|
402
427
|
name: "PI_CREW_ARTIFACTS_ROOT",
|
|
403
428
|
doc: "artifacts root for scratchpad containment (scratchpad-lifecycle.ts:228/281)",
|
package/src/config/role-tools.ts
CHANGED
|
@@ -90,9 +90,11 @@ export const ROLE_TOOL_CONFIGS: Record<string, RoleToolConfig> = {
|
|
|
90
90
|
scratchpad: true,
|
|
91
91
|
},
|
|
92
92
|
|
|
93
|
-
// Test Engineer - Can write tests (F1: hyphenated key)
|
|
93
|
+
// Test Engineer - Can write tests (F1: hyphenated key). Tool list mirrors
|
|
94
|
+
// agents/test-engineer.md frontmatter (T3 binding, commit c25dfbe2):
|
|
95
|
+
// search tools + delegate for spawning depth-1 helpers.
|
|
94
96
|
"test-engineer": {
|
|
95
|
-
tools: ["read", "edit", "write", "bash", "ls", "scratchpad", "ask"],
|
|
97
|
+
tools: ["read", "edit", "write", "bash", "ls", "glob", "grep", "find", "scratchpad", "ask", "delegate"],
|
|
96
98
|
excludeTools: ["web"],
|
|
97
99
|
// Phase 1 scratchpad: build/run test suites with state across cells.
|
|
98
100
|
scratchpad: true,
|
package/src/config/types.ts
CHANGED
|
@@ -85,6 +85,21 @@ export interface CrewRuntimeConfig {
|
|
|
85
85
|
};
|
|
86
86
|
/** Mark certain bash commands as excludeFromContext to reduce context tokens. Default: false */
|
|
87
87
|
excludeContextBash?: boolean;
|
|
88
|
+
/**
|
|
89
|
+
* Mux-surface policy (mux-surface spec v0.7 §8.1): WHERE worker processes
|
|
90
|
+
* live — a pane in tmux/herdr or headless child processes. Surface only
|
|
91
|
+
* picks the process home; scheduler, broker, and state-on-disk are
|
|
92
|
+
* unaffected. Not sensitive — any config tier may set it.
|
|
93
|
+
*/
|
|
94
|
+
surface?: {
|
|
95
|
+
/** auto = detect tmux/herdr and use panes when present, else headless.
|
|
96
|
+
* "tmux"/"herdr" force a backend (detect fail → headless + warning
|
|
97
|
+
* event, never a throw). "off" disables panes entirely. Default: "auto". */
|
|
98
|
+
mode?: "auto" | "tmux" | "herdr" | "off";
|
|
99
|
+
/** Exact-match agent/role names that get a surface pane; ["*"] = all.
|
|
100
|
+
* Default: [] in A1 (surface visible to nobody until opted in). */
|
|
101
|
+
visibleAgents?: string[];
|
|
102
|
+
};
|
|
88
103
|
/** Subagent model fallback policy: auto-tail ordering, cap, credential filtering, default model. */
|
|
89
104
|
modelFallback?: CrewModelFallbackConfig;
|
|
90
105
|
}
|
|
@@ -143,8 +158,26 @@ export interface GoalWrapWorkflowConfig {
|
|
|
143
158
|
}
|
|
144
159
|
|
|
145
160
|
export interface CrewUiConfig {
|
|
146
|
-
|
|
161
|
+
/**
|
|
162
|
+
* Where the crew dock (agents list) renders:
|
|
163
|
+
* - `aboveEditor` / `belowEditor`: pi's widget slots around the prompt;
|
|
164
|
+
* - `bottom`: inside the crew-vibes footer, BELOW the quota/meter lines
|
|
165
|
+
* (the very bottom of the screen). Falls back to `belowEditor` when
|
|
166
|
+
* crew-vibes is not active (no footer sink).
|
|
167
|
+
*/
|
|
168
|
+
widgetPlacement?: "aboveEditor" | "belowEditor" | "bottom";
|
|
147
169
|
widgetMaxLines?: number;
|
|
170
|
+
/**
|
|
171
|
+
* Per-agent row layout in the widget. `compact` is one width-budgeted line
|
|
172
|
+
* per agent; `detailed` keeps the two-line tree (name row + activity row).
|
|
173
|
+
*/
|
|
174
|
+
widgetRowStyle?: "compact" | "detailed";
|
|
175
|
+
/**
|
|
176
|
+
* Keyboard-navigable agent rows under the prompt (`↓` from an empty prompt).
|
|
177
|
+
* Requires owning pi's editor component, so it yields to any other extension
|
|
178
|
+
* that already installed a custom editor.
|
|
179
|
+
*/
|
|
180
|
+
inlinePanel?: boolean;
|
|
148
181
|
powerbar?: boolean;
|
|
149
182
|
dashboardPlacement?: "center" | "right";
|
|
150
183
|
dashboardWidth?: number;
|
|
@@ -295,6 +328,27 @@ export interface PiTeamsConfig {
|
|
|
295
328
|
* `delegate.rejected` event (never silent).
|
|
296
329
|
*/
|
|
297
330
|
nesting?: CrewNestingConfig;
|
|
331
|
+
/** State-layer persistence knobs (perf round 2, Task 3). Opt-in only. */
|
|
332
|
+
persistence?: PersistenceConfig;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/** State-layer persistence knobs (perf round 2, Task 3). */
|
|
336
|
+
export interface PersistenceConfig {
|
|
337
|
+
/**
|
|
338
|
+
* Opt-in (default `false`): write non-terminal tasks checkpoints with
|
|
339
|
+
* `durability:"best-effort"` (no fsync) while KEEPING the 50ms coalesced
|
|
340
|
+
* write grouping — only the durability of the flush changes, not its
|
|
341
|
+
* timing. tasks.json is fully reconstructible from the fsync'd event log,
|
|
342
|
+
* so a crash loses at most the tail of an in-flight checkpoint and
|
|
343
|
+
* recovers from events.jsonl. Terminal task transitions (completed/failed/
|
|
344
|
+
* cancelled/needs_attention/skipped) ALWAYS stay full-durability
|
|
345
|
+
* regardless of this flag.
|
|
346
|
+
* Set via env `PI_CREW_PERSISTENCE_SKIP_TASKS_FSYNC` (`"1"`/`"true"`) or
|
|
347
|
+
* config `persistence.skipTasksFsync`. Env beats config; default false.
|
|
348
|
+
* @see src/state/stores/state-store.ts saveRunTasksCoalesced
|
|
349
|
+
* @see src/runtime/task-runner/state-helpers.ts persistSingleTaskUpdate
|
|
350
|
+
*/
|
|
351
|
+
skipTasksFsync?: boolean;
|
|
298
352
|
}
|
|
299
353
|
|
|
300
354
|
/** Governed-nesting config (ADR-5). */
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
2
2
|
import { cleanupAllTrackedTempDirs } from "../runtime/model/pi-args.ts";
|
|
3
|
+
import { isSessionSwitchInFlight } from "../ui/inline-panel/view-session-store.ts";
|
|
3
4
|
import { logInternalError } from "../utils/internal-error.ts";
|
|
4
5
|
|
|
5
6
|
// NOTE: globalProgressTracker import kept for documentation but not directly used
|
|
@@ -67,6 +68,18 @@ export function registerCleanupHandler(pi: ExtensionAPI, opts?: { disposeTermina
|
|
|
67
68
|
console.log("[pi-crew] Session shutdown - cleaning up resources");
|
|
68
69
|
|
|
69
70
|
try {
|
|
71
|
+
// Navigational switches (pi's own resume/new/fork) tear the session
|
|
72
|
+
// down on purpose — the user is only LOOKING at a different pi
|
|
73
|
+
// session; foreground team runs + their workers share this process
|
|
74
|
+
// and must keep running (P0: "jumping into any session stops the
|
|
75
|
+
// run" — exit 143 signature). Killing them here bypasses child-pi's
|
|
76
|
+
// abort flags, so the worker exit reads as an "unexpected
|
|
77
|
+
// code=143 signal=null" and the run is cancelled. Same contract as
|
|
78
|
+
// stopSessionBoundSubagents and the run-deadline caller-abort
|
|
79
|
+
// suppression; on REAL shutdowns (quit/reload) the flag is unset
|
|
80
|
+
// and this cleanup still drains the registry, as does the
|
|
81
|
+
// process-exit hook (tryRegisterSessionCleanup) when pi dies.
|
|
82
|
+
if (isSessionSwitchInFlight()) return;
|
|
70
83
|
// Kill all child-pi processes
|
|
71
84
|
await cleanupChildProcesses();
|
|
72
85
|
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { isAbsolute, relative, resolve, sep } from "node:path";
|
|
2
2
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import { getFooterDockProvider } from "../../ui/dock-footer.ts";
|
|
3
4
|
import { requestRenderTarget } from "../../ui/pi-ui-compat.ts";
|
|
4
5
|
import { asCrewTheme, type CrewTheme } from "../../ui/theme-adapter.ts";
|
|
6
|
+
import { colorWidgetLine, renderLines } from "../../ui/widget/widget-renderer.ts";
|
|
5
7
|
import { truncateToWidth, visibleWidth } from "../../utils/visual.ts";
|
|
6
8
|
import type { CrewVibesConfig } from "./config.ts";
|
|
7
9
|
import type { ProviderUsage } from "./provider-usage.ts";
|
|
@@ -280,11 +282,28 @@ class CrewVibesFooter implements FooterComponent {
|
|
|
280
282
|
return [truncateToWidth(cap, width, "…"), this.rightAlign(quota, width)];
|
|
281
283
|
}
|
|
282
284
|
|
|
285
|
+
/** Dock lines registered by the crew widget (`widgetPlacement: "bottom"`):
|
|
286
|
+
* painted as the very last block of the footer, below the quota/meters.
|
|
287
|
+
* The provider returns RAW lines; we color them with this footer's theme
|
|
288
|
+
* (same per-line palette the widget would have used) and truncate to the
|
|
289
|
+
* real render width. */
|
|
290
|
+
private buildDockLines(width: number): string[] {
|
|
291
|
+
const provider = getFooterDockProvider();
|
|
292
|
+
if (!provider) return [];
|
|
293
|
+
const raw = provider(width);
|
|
294
|
+
if (raw.length === 0) return [];
|
|
295
|
+
return renderLines(
|
|
296
|
+
raw.map((line, index) => colorWidgetLine(line, index, this.theme)),
|
|
297
|
+
width,
|
|
298
|
+
).map((line) => truncateToWidth(line, width, "…"));
|
|
299
|
+
}
|
|
300
|
+
|
|
283
301
|
render(width: number): string[] {
|
|
284
302
|
const lines = [this.buildPwdLine(width), this.buildStatsLine(width)];
|
|
285
303
|
const statusLine = this.buildStatusLine(width);
|
|
286
304
|
if (statusLine) lines.push(statusLine);
|
|
287
305
|
lines.push(...this.buildMeterLines(width));
|
|
306
|
+
lines.push(...this.buildDockLines(width));
|
|
288
307
|
return lines;
|
|
289
308
|
}
|
|
290
309
|
}
|
|
@@ -19,6 +19,7 @@ function safeUiCall(scope: string, fn: () => void): void {
|
|
|
19
19
|
}
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
+
import { setFooterDockSinkActive } from "../../ui/dock-footer.ts";
|
|
22
23
|
import { type CrewVibesConfig, loadConfig, saveConfig } from "./config.ts";
|
|
23
24
|
import { intervalForSpeed } from "./figures.ts";
|
|
24
25
|
import { type CrewVibesFooterSource, createCrewVibesFooter } from "./footer.ts";
|
|
@@ -91,11 +92,18 @@ export function registerCrewVibes(pi: ExtensionAPI): void {
|
|
|
91
92
|
function installFooter(ctx: ExtensionContext): void {
|
|
92
93
|
if (!ctx?.hasUI) return;
|
|
93
94
|
if (!metersActive()) {
|
|
94
|
-
safeUiCall("clear-footer", () =>
|
|
95
|
+
safeUiCall("clear-footer", () => {
|
|
96
|
+
// The crew dock can no longer render at the very bottom — the
|
|
97
|
+
// widget falls back to pi's belowEditor slot automatically.
|
|
98
|
+
setFooterDockSinkActive(false);
|
|
99
|
+
setFooter(ctx, undefined);
|
|
100
|
+
});
|
|
95
101
|
return;
|
|
96
102
|
}
|
|
97
103
|
safeUiCall("install-footer", () => {
|
|
98
104
|
setFooter(ctx, (tui, theme, footerData) => createCrewVibesFooter({ tui, theme, footerData, ctx, source: footerSource }));
|
|
105
|
+
// The footer can now host the crew dock below the quota lines.
|
|
106
|
+
setFooterDockSinkActive(true);
|
|
99
107
|
requestRender(ctx);
|
|
100
108
|
});
|
|
101
109
|
}
|
|
@@ -245,6 +253,7 @@ export function registerCrewVibes(pi: ExtensionAPI): void {
|
|
|
245
253
|
stopFooterTimer();
|
|
246
254
|
stopCapacityTimer();
|
|
247
255
|
stopProviderTimer();
|
|
256
|
+
setFooterDockSinkActive(false);
|
|
248
257
|
setFooter(ctx, undefined);
|
|
249
258
|
clearVibesStatus(ctx);
|
|
250
259
|
return;
|
|
@@ -370,6 +379,7 @@ export function registerCrewVibes(pi: ExtensionAPI): void {
|
|
|
370
379
|
stopFooterTimer();
|
|
371
380
|
stopCapacityTimer();
|
|
372
381
|
stopProviderTimer();
|
|
382
|
+
setFooterDockSinkActive(false);
|
|
373
383
|
setFooter(ctx, undefined);
|
|
374
384
|
clearVibesStatus(ctx);
|
|
375
385
|
});
|
|
@@ -21,6 +21,7 @@ import { loadConfig } from "../config/config.ts";
|
|
|
21
21
|
import { startRuntimeWarmup } from "../runtime/model/runtime-warmup.ts";
|
|
22
22
|
import { primePeerDep } from "../runtime/peer-dep.ts";
|
|
23
23
|
import { deployBundledThemes } from "../ui/deploy-bundled-themes.ts";
|
|
24
|
+
import { installChildProcessAbortShield } from "../utils/child-process-shield.ts";
|
|
24
25
|
import { resetTimings, time } from "../utils/timings.ts";
|
|
25
26
|
import { registerAutonomousPolicy } from "./autonomous-policy.ts";
|
|
26
27
|
import { registerContextStatusInjection } from "./context-status-injection.ts";
|
|
@@ -52,6 +53,13 @@ export function registerPiTeams(pi: ExtensionAPI): void {
|
|
|
52
53
|
resetTimings();
|
|
53
54
|
time("register:start");
|
|
54
55
|
|
|
56
|
+
// MUST run before any team run can start: guarantees every signal-spawned
|
|
57
|
+
// child in the process has an 'error' listener so Node's abort-after-exit
|
|
58
|
+
// AbortError dispatches instead of being rethrown on the next tick as an
|
|
59
|
+
// uncaughtException that kills pi (see child-process-shield.ts for the
|
|
60
|
+
// full crash chain — /crew-view mid-run crash).
|
|
61
|
+
installChildProcessAbortShield();
|
|
62
|
+
|
|
55
63
|
startRuntimeWarmup();
|
|
56
64
|
primePeerDep().catch(() => undefined);
|
|
57
65
|
deployBundledThemes();
|
|
@@ -31,6 +31,7 @@ export function registerPiCommands(pi: ExtensionAPI, ctx: RegistrationContext):
|
|
|
31
31
|
getManifestCache: ctx.getManifestCache,
|
|
32
32
|
getRunSnapshotCache: ctx.getRunSnapshotCache,
|
|
33
33
|
getMetricRegistry: () => ctx.observabilityState.metricRegistry,
|
|
34
|
+
getCurrentSessionFile: () => ctx.currentCtx?.sessionManager?.getSessionFile?.(),
|
|
34
35
|
uiState: ctx.uiState,
|
|
35
36
|
dismissNotifications: () => {
|
|
36
37
|
ctx.widgetState.notificationCount = 0;
|
|
@@ -17,7 +17,21 @@ export function registerRunCommands(pi: ExtensionAPI, deps: RegisterTeamCommands
|
|
|
17
17
|
abortForegroundRun: deps.abortForegroundRun,
|
|
18
18
|
onRunStarted: undefined,
|
|
19
19
|
});
|
|
20
|
-
|
|
20
|
+
// A foreground run can span a session switch (e.g. the user opens
|
|
21
|
+
// an agent view mid-run and the run finishes there). The captured
|
|
22
|
+
// ctx is stale after the switch: ANY access to it (even reading a
|
|
23
|
+
// field) throws pi's "This extension ctx is stale ..." error, which
|
|
24
|
+
// pi then prints into whichever session is active. Probe
|
|
25
|
+
// defensively and skip the notification when the session moved on
|
|
26
|
+
// (the result is still visible via /team-status).
|
|
27
|
+
try {
|
|
28
|
+
const capturedFile = ctx.sessionManager?.getSessionFile?.();
|
|
29
|
+
const currentFile = deps.getCurrentSessionFile?.();
|
|
30
|
+
if (capturedFile && currentFile && capturedFile !== currentFile) return;
|
|
31
|
+
await notifyCommandResult(ctx, commandText(result));
|
|
32
|
+
} catch {
|
|
33
|
+
// Stale ctx during the probe or notify — nothing to do here.
|
|
34
|
+
}
|
|
21
35
|
},
|
|
22
36
|
});
|
|
23
37
|
|
|
@@ -80,6 +80,14 @@ export interface RegisterTeamCommandsDeps {
|
|
|
80
80
|
getMetricRegistry?: () => MetricRegistry | undefined;
|
|
81
81
|
uiState?: UiState;
|
|
82
82
|
dismissNotifications?: () => void;
|
|
83
|
+
/**
|
|
84
|
+
* The session file of the CURRENT session (RegistrationContext.currentCtx).
|
|
85
|
+
* Commands whose handler can span a session switch — team-run awaits the
|
|
86
|
+
* whole foreground run — use this after the await to detect that the
|
|
87
|
+
* captured ctx is stale and skip notifications that pi would drop with a
|
|
88
|
+
* "ctx is stale" error.
|
|
89
|
+
*/
|
|
90
|
+
getCurrentSessionFile?: () => string | undefined;
|
|
83
91
|
}
|
|
84
92
|
|
|
85
93
|
// Lazy-loaded UI module cache — avoids importing 900ms+ of UI at Pi startup.
|
|
@@ -20,6 +20,7 @@ import { setWorkingIndicator } from "../../ui/pi-ui-compat.ts";
|
|
|
20
20
|
import { requestPowerbarUpdate } from "../../ui/powerbar-publisher.ts";
|
|
21
21
|
import { updateCrewWidget } from "../../ui/widget/index.ts";
|
|
22
22
|
import { logInternalError } from "../../utils/internal-error.ts";
|
|
23
|
+
import { safeAbort } from "../../utils/safe-abort.ts";
|
|
23
24
|
import type { RegistrationContext } from "./registration-types.ts";
|
|
24
25
|
|
|
25
26
|
/**
|
|
@@ -36,7 +37,10 @@ export function installForegroundRunController(pi: ExtensionAPI, ctx: Registrati
|
|
|
36
37
|
ctx.abortForegroundRun = (runId) => {
|
|
37
38
|
const controller = ctx.foregroundTeamRunControllers.get(runId);
|
|
38
39
|
if (!controller) return false;
|
|
39
|
-
|
|
40
|
+
// safe-abort: cancel can race a completed run whose spawned children
|
|
41
|
+
// already exited — Node's child_process signal listener would throw
|
|
42
|
+
// AbortError out of abort() and kill the host (uncaughtException).
|
|
43
|
+
safeAbort(controller, "fg-run.abort-by-run-id");
|
|
40
44
|
return true;
|
|
41
45
|
};
|
|
42
46
|
}
|
|
@@ -142,7 +146,11 @@ function startForegroundRunImpl(
|
|
|
142
146
|
.catch((error) => logInternalError("register.foreground-watchdog", error, `runId=${runId}`));
|
|
143
147
|
}
|
|
144
148
|
const ownerCurrent = ctx.isContextCurrent(extensionCtx, ownerGeneration);
|
|
145
|
-
|
|
149
|
+
// The owner ctx can go stale while the run is in flight (e.g. the
|
|
150
|
+
// user opened an agent session view via /crew-view and the run
|
|
151
|
+
// finishes there): the `hasUI` getter THROWS on a stale ctx, so
|
|
152
|
+
// it must only be touched for the still-current owner.
|
|
153
|
+
if (ownerCurrent && extensionCtx.hasUI) {
|
|
146
154
|
try {
|
|
147
155
|
setWorkingIndicator(extensionCtx);
|
|
148
156
|
extensionCtx.ui.setWorkingMessage();
|