pi-crew 0.10.2 → 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 +249 -0
- package/dist/index.mjs +98 -307
- package/package.json +2 -1
- package/schema.json +11 -0
- 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 +40 -1
- package/src/config/config.ts +28 -6
- package/src/config/defaults.ts +35 -10
- package/src/config/env-vars.ts +27 -2
- package/src/config/types.ts +36 -0
- package/src/extension/registration/lifecycle-handlers.ts +40 -9
- package/src/extension/registration/team-tool.ts +53 -5
- 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/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-spawn.ts +23 -9
- package/src/runtime/child-pi/child-pi-streams.ts +9 -1
- package/src/runtime/child-pi/child-pi.ts +353 -5
- package/src/runtime/crew-agent-records.ts +13 -1
- 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/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/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/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 +101 -0
- package/src/schema/config-schema.ts +24 -0
- 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/state-store.ts +98 -6
- package/src/state/types.ts +51 -0
- package/src/ui/inline-panel/agent-pane.ts +3 -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 +32 -8
- package/src/utils/visual.ts +43 -0
- package/src/worktree/worktree-manager.ts +65 -4
|
@@ -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;
|
|
@@ -676,6 +714,7 @@ export function parseConfig(raw: unknown): PiTeamsConfig {
|
|
|
676
714
|
otlp: parseOtlpConfig(obj.otlp),
|
|
677
715
|
ui: parseUiConfig(obj.ui),
|
|
678
716
|
broker: parseBrokerConfig(obj.broker),
|
|
717
|
+
persistence: parsePersistenceConfig(obj.persistence),
|
|
679
718
|
};
|
|
680
719
|
}
|
|
681
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
|
@@ -194,24 +194,49 @@ export const DEFAULT_BROKER = {
|
|
|
194
194
|
maxFrameBytes: 262144,
|
|
195
195
|
outboundQueueCap: 256,
|
|
196
196
|
/** WP-2/R2 (ADR-0 docs/decisions/2026-08-17-waiting-producer-ask item 7):
|
|
197
|
-
* capability gate for the broker's `wait.*` methods.
|
|
198
|
-
*
|
|
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,
|
|
199
203
|
* wait.request/wait.resolve are rejected with a policy-disabled error
|
|
200
204
|
* AND a policy.action event in events.jsonl (never silent). */
|
|
201
|
-
waitMethodsEnabled:
|
|
205
|
+
waitMethodsEnabled: true,
|
|
202
206
|
} as const;
|
|
203
207
|
|
|
204
208
|
/**
|
|
205
209
|
* Governed-nesting defaults (ADR-5 docs/decisions/2026-08-17-governed-nesting.md §10).
|
|
206
|
-
* `enabled` is DEFAULT
|
|
207
|
-
* security sign-off)
|
|
208
|
-
*
|
|
209
|
-
*
|
|
210
|
-
*
|
|
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)).
|
|
211
220
|
*/
|
|
212
221
|
export const DEFAULT_NESTING = {
|
|
213
|
-
enabled:
|
|
214
|
-
|
|
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,
|
|
215
240
|
} as const;
|
|
216
241
|
|
|
217
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/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
|
}
|
|
@@ -313,6 +328,27 @@ export interface PiTeamsConfig {
|
|
|
313
328
|
* `delegate.rejected` event (never silent).
|
|
314
329
|
*/
|
|
315
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;
|
|
316
352
|
}
|
|
317
353
|
|
|
318
354
|
/** Governed-nesting config (ADR-5). */
|
|
@@ -21,7 +21,7 @@ import { loadConfig } from "../../config/config.ts";
|
|
|
21
21
|
import { DEFAULT_UI } from "../../config/defaults.ts";
|
|
22
22
|
import { getCrewEnv } from "../../config/env-vars.ts";
|
|
23
23
|
import { pruneFinishedRuns, pruneUserLevelRuns } from "../../extension/run-maintenance.ts";
|
|
24
|
-
import { type BrokerSpawnCredentials, setActiveBrokerIssuer } from "../../runtime/broker/broker-issuer.ts";
|
|
24
|
+
import { type BrokerSpawnCredentials, setActiveBrokerIssuer, setActiveBrokerRevoker } from "../../runtime/broker/broker-issuer.ts";
|
|
25
25
|
import { CrewBroker } from "../../runtime/broker/crew-broker.ts";
|
|
26
26
|
import { terminateActiveChildPiProcesses } from "../../runtime/child-pi/child-pi.ts";
|
|
27
27
|
import { forgetDetachedRun, hasDetachedRuns, peekFinishedDetachedRunResults } from "../../runtime/detached-run-results.ts";
|
|
@@ -681,8 +681,13 @@ function setupRenderLoop(
|
|
|
681
681
|
// file just changed on disk, so force a fresh snapshot while keeping
|
|
682
682
|
// the entry populated — deleting it left a window where the widget's
|
|
683
683
|
// `get()` returned undefined and dropped the run to "(loading…)".
|
|
684
|
+
// PERF (2026-08-24): route through the coalesced ASYNC refresh —
|
|
685
|
+
// fs.watch can fire many times per second and the sync rebuild
|
|
686
|
+
// blocked the UI event loop. The entry stays populated until the
|
|
687
|
+
// async rebuild re-sets it in place; the render schedule below
|
|
688
|
+
// repaints while it lands.
|
|
684
689
|
try {
|
|
685
|
-
ctx.getRunSnapshotCache(ctx.currentCtx?.cwd ?? process.cwd()).
|
|
690
|
+
ctx.getRunSnapshotCache(ctx.currentCtx?.cwd ?? process.cwd()).scheduleRefresh(runId);
|
|
686
691
|
} catch (error) {
|
|
687
692
|
logInternalError("register.runWatcher.refresh", error, runId);
|
|
688
693
|
}
|
|
@@ -930,8 +935,11 @@ function setupRenderLoop(
|
|
|
930
935
|
// FLICKER FIX: rebuild-in-place instead of deleting the entry (see
|
|
931
936
|
// onRunChange above). A hard delete left `get()` returning undefined for
|
|
932
937
|
// a frame, dropping the run to "(loading…)" and causing visible flicker.
|
|
938
|
+
// PERF (2026-08-24): coalesced ASYNC refresh — fs.watch can fire many
|
|
939
|
+
// times per second and the sync rebuild blocked the UI event loop; the
|
|
940
|
+
// render schedule below repaints while the rebuild lands.
|
|
933
941
|
try {
|
|
934
|
-
ctx.getRunSnapshotCache(ctx.currentCtx?.cwd ?? process.cwd()).
|
|
942
|
+
ctx.getRunSnapshotCache(ctx.currentCtx?.cwd ?? process.cwd()).scheduleRefresh(runId);
|
|
935
943
|
} catch (error) {
|
|
936
944
|
logInternalError("register.crewRunWatcher.refresh", error, runId);
|
|
937
945
|
}
|
|
@@ -1077,10 +1085,11 @@ export function installCrewBrokerLifecycleController(_pi: ExtensionAPI, _ctx: Re
|
|
|
1077
1085
|
// config.broker.waitMethodsEnabled a dead knob and the ADR-0
|
|
1078
1086
|
// "then true" flip a silent no-op. Fail-closed when unset.
|
|
1079
1087
|
waitMethodsEnabled: cfg?.waitMethodsEnabled ?? false,
|
|
1080
|
-
// T3/R5 (ADR-5 §10): governed-nesting capability gate —
|
|
1081
|
-
//
|
|
1082
|
-
// only). Nested-slot sizing +
|
|
1083
|
-
//
|
|
1088
|
+
// T3/R5 (ADR-5 §10): governed-nesting capability gate — default-on
|
|
1089
|
+
// since D8 (loadConfig layers DEFAULT_NESTING.enabled=true; sensitive
|
|
1090
|
+
// flag, so only USER config may flip it). Nested-slot sizing +
|
|
1091
|
+
// admission-time model catalog (ADR-5 §7 — the production wiring
|
|
1092
|
+
// MUST supply it) + workspace gate mirror.
|
|
1084
1093
|
nestingEnabled: nestingCfg?.nesting?.enabled ?? false,
|
|
1085
1094
|
...(nestingCfg?.nesting?.maxSlots !== undefined ? { nestingMaxSlots: nestingCfg.nesting.maxSlots } : {}),
|
|
1086
1095
|
...(nestingCfg?.nesting?.maxDepth !== undefined ? { nestingMaxDepth: nestingCfg.nesting.maxDepth } : {}),
|
|
@@ -1120,8 +1129,8 @@ export function installCrewBrokerLifecycleController(_pi: ExtensionAPI, _ctx: Re
|
|
|
1120
1129
|
if (!effectiveEnabled()) return undefined;
|
|
1121
1130
|
// ADR-5 §4 (governed nesting): tokens are minted ONLY for children that
|
|
1122
1131
|
// may themselves delegate — childDepth < resolved maxDepth. At the default
|
|
1123
|
-
// maxDepth=
|
|
1124
|
-
// (env containment: no PI_CREW_BROKER_SOCKET/TOKEN at depth
|
|
1132
|
+
// maxDepth=4 a delegate-spawned depth-4 grandchild gets NO credentials
|
|
1133
|
+
// (env containment: no PI_CREW_BROKER_SOCKET/TOKEN at the cap depth; identity
|
|
1125
1134
|
// routing via PI_CREW_BROKER_RUN_ID/TASK_ID is threaded unconditionally
|
|
1126
1135
|
// elsewhere). Undefined childDepth = legacy worker spawn (depth 1).
|
|
1127
1136
|
if (childDepth !== undefined && childDepth >= resolveCrewMaxDepth(undefined)) return undefined;
|
|
@@ -1136,6 +1145,27 @@ export function installCrewBrokerLifecycleController(_pi: ExtensionAPI, _ctx: Re
|
|
|
1136
1145
|
}
|
|
1137
1146
|
};
|
|
1138
1147
|
|
|
1148
|
+
// MuxSurface A1 (spec §7 D3 step 2): publish the revoker alongside the
|
|
1149
|
+
// issuer — team-runner's degrade controller calls it when a pane is lost.
|
|
1150
|
+
// Best-effort and self-gated: no broker bound (never started / session
|
|
1151
|
+
// switched) means nothing to revoke; re-issue after respawn mints fresh (T10).
|
|
1152
|
+
const revokeForTask = (taskId: string): void => {
|
|
1153
|
+
if (!taskId || typeof taskId !== "string") return;
|
|
1154
|
+
if (!isRootSession(process.env)) return;
|
|
1155
|
+
if (!broker || brokerSessionId !== cachedSessionId) return;
|
|
1156
|
+
try {
|
|
1157
|
+
broker.revokeTaskToken(taskId);
|
|
1158
|
+
} catch (error) {
|
|
1159
|
+
logInternalError(
|
|
1160
|
+
"broker.revoke-for-task",
|
|
1161
|
+
error instanceof Error ? error : new Error(String(error)),
|
|
1162
|
+
`taskId=${taskId}`,
|
|
1163
|
+
"warn",
|
|
1164
|
+
);
|
|
1165
|
+
}
|
|
1166
|
+
};
|
|
1167
|
+
setActiveBrokerRevoker(revokeForTask);
|
|
1168
|
+
|
|
1139
1169
|
// Publish this issuer as the process-local active issuer so runChildPi can
|
|
1140
1170
|
// default `brokerIssuer` without the registration context being threaded
|
|
1141
1171
|
// through every runner call site. The issuer self-gates (root + flag), so
|
|
@@ -1146,6 +1176,7 @@ export function installCrewBrokerLifecycleController(_pi: ExtensionAPI, _ctx: Re
|
|
|
1146
1176
|
issueForChild,
|
|
1147
1177
|
stop: async () => {
|
|
1148
1178
|
setActiveBrokerIssuer(undefined);
|
|
1179
|
+
setActiveBrokerRevoker(undefined);
|
|
1149
1180
|
if (broker) {
|
|
1150
1181
|
try {
|
|
1151
1182
|
await broker.stop();
|
|
@@ -147,6 +147,52 @@ export function detectUnrecognizedParams(schema: TObject, params: unknown): stri
|
|
|
147
147
|
].join("\n");
|
|
148
148
|
}
|
|
149
149
|
|
|
150
|
+
// PERF (2026-08-24): cheap read actions (status/summary/events/get/list) re-rendered
|
|
151
|
+
// the widget + powerbar after every call; the event bus and render tick already
|
|
152
|
+
// reflect run-state changes. Refresh only on actions that mutate run state
|
|
153
|
+
// outside the watched files. Names mirror the five domain dispatcher tables in
|
|
154
|
+
// src/schema/team-tool-schema.ts (RUN/STATUS/CONTROL/MANAGE/AUTOMATE). Actions
|
|
155
|
+
// whose effect depends on sub-params (api: approve-plan/agent-control/mailbox;
|
|
156
|
+
// plans: approve/reject; anchor/auto-summarize/auto_boomerang: set/clear/toggle;
|
|
157
|
+
// config/autonomy: show vs write) are included — a redundant refresh on their
|
|
158
|
+
// read sub-actions is cheap, a missed refresh after a write leaves stale UI.
|
|
159
|
+
const MUTATING_ACTIONS = new Set([
|
|
160
|
+
// run domain
|
|
161
|
+
"run",
|
|
162
|
+
"parallel",
|
|
163
|
+
"orchestrate",
|
|
164
|
+
"resume",
|
|
165
|
+
"retry",
|
|
166
|
+
"steer",
|
|
167
|
+
"goal",
|
|
168
|
+
"plans",
|
|
169
|
+
// control domain
|
|
170
|
+
"cancel",
|
|
171
|
+
"invalidate",
|
|
172
|
+
"respond",
|
|
173
|
+
"cleanup",
|
|
174
|
+
"prune",
|
|
175
|
+
"forget",
|
|
176
|
+
// manage domain
|
|
177
|
+
"create",
|
|
178
|
+
"update",
|
|
179
|
+
"delete",
|
|
180
|
+
"init",
|
|
181
|
+
"config",
|
|
182
|
+
"autonomy",
|
|
183
|
+
"settings",
|
|
184
|
+
"workflow-create",
|
|
185
|
+
"workflow-save",
|
|
186
|
+
"workflow-delete",
|
|
187
|
+
"import",
|
|
188
|
+
// automate domain
|
|
189
|
+
"schedule",
|
|
190
|
+
"anchor",
|
|
191
|
+
"auto-summarize",
|
|
192
|
+
"auto_boomerang",
|
|
193
|
+
"api",
|
|
194
|
+
]);
|
|
195
|
+
|
|
150
196
|
export function registerTeamTool(pi: ExtensionAPI, deps: RegisterTeamToolDeps): void {
|
|
151
197
|
const tool: ToolDefinition = {
|
|
152
198
|
name: "team",
|
|
@@ -229,11 +275,13 @@ export function registerTeamTool(pi: ExtensionAPI, deps: RegisterTeamToolDeps):
|
|
|
229
275
|
timestamp: Date.now(),
|
|
230
276
|
});
|
|
231
277
|
}
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
278
|
+
if (MUTATING_ACTIONS.has(resolved.action ?? "list")) {
|
|
279
|
+
const config = loadConfig(toolCtx.cwd).config.ui;
|
|
280
|
+
const cache = deps.getManifestCache(toolCtx.cwd);
|
|
281
|
+
const snapshotCache = deps.getRunSnapshotCache?.(toolCtx.cwd);
|
|
282
|
+
updateCrewWidget(toolCtx, deps.widgetState, config, cache, snapshotCache);
|
|
283
|
+
updatePiCrewPowerbar(pi.events, toolCtx.cwd, config, cache, snapshotCache, toolCtx);
|
|
284
|
+
}
|
|
237
285
|
return output;
|
|
238
286
|
} finally {
|
|
239
287
|
signal?.removeEventListener("abort", abort);
|