pi-crew 0.11.16 → 0.11.18

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.
Files changed (45) hide show
  1. package/CHANGELOG.md +228 -0
  2. package/NOTICE.md +9 -0
  3. package/README.md +1 -0
  4. package/dist/index.mjs +517 -516
  5. package/docs/usage.md +66 -0
  6. package/package.json +1 -1
  7. package/skills/real-test-pi-crew/SKILL.md +19 -3
  8. package/src/config/config-validation.ts +26 -1
  9. package/src/config/config.ts +6 -1
  10. package/src/config/env-allowlist-registry.ts +463 -0
  11. package/src/config/env-vars.ts +13 -0
  12. package/src/config/types.ts +36 -0
  13. package/src/extension/registration/team-tool.ts +7 -1
  14. package/src/extension/registration/terminal-status-wiring.ts +155 -2
  15. package/src/extension/run-index.ts +30 -5
  16. package/src/extension/team-tool/context.ts +6 -0
  17. package/src/extension/team-tool/handle-settings.ts +9 -6
  18. package/src/runtime/async-runner.ts +11 -96
  19. package/src/runtime/broker/crew-broker.ts +12 -0
  20. package/src/runtime/child-pi/child-pi-rpc.ts +61 -8
  21. package/src/runtime/child-pi/child-pi-spawn.ts +5 -31
  22. package/src/runtime/live-session/embedded-tier.ts +994 -0
  23. package/src/runtime/live-session/live-session-runtime.ts +223 -11
  24. package/src/runtime/model/pi-args.ts +30 -2
  25. package/src/runtime/model/runtime-policy.ts +45 -0
  26. package/src/runtime/scheduling/nested-slots.ts +26 -0
  27. package/src/runtime/surface/herdr-provider.ts +58 -7
  28. package/src/runtime/task-runner/child-executor.ts +7 -7
  29. package/src/runtime/task-runner/hybrid-tier.ts +439 -0
  30. package/src/runtime/task-runner/live-executor.ts +53 -0
  31. package/src/runtime/task-runner.ts +159 -51
  32. package/src/schema/config-schema.ts +13 -0
  33. package/src/skills/validate.ts +1 -1
  34. package/src/state/contracts.ts +15 -0
  35. package/src/state/stores/state-store.ts +6 -2
  36. package/src/ui/events-state-source.ts +7 -1
  37. package/src/ui/pi-ui-compat.ts +33 -0
  38. package/src/ui/program-status.ts +74 -0
  39. package/src/ui/syntax-highlight.ts +1 -1
  40. package/src/ui/terminal-status.ts +81 -2
  41. package/src/ui/theme-discovery.ts +38 -2
  42. package/src/ui/tool-progress-formatter.ts +17 -1
  43. package/src/utils/env-allowlist.ts +4 -1
  44. package/src/utils/git.ts +6 -71
  45. package/src/utils/paths.ts +18 -3
@@ -0,0 +1,463 @@
1
+ /**
2
+ * env-allowlist-registry.ts — central env-FORWARDING registry (C-1).
3
+ *
4
+ * Wave A item 1 (upgrade research round 2 §C-1, round 3 §L-lane): the three
5
+ * hand-maintained allowlists (`BACKGROUND_RUNNER_ENV_ALLOWLIST` in
6
+ * src/runtime/async-runner.ts, `BASE_ALLOWLIST` in
7
+ * src/runtime/child-pi/child-pi-spawn.ts, `WINDOWS_ESSENTIAL_ENV_VARS` in
8
+ * src/utils/env-allowlist.ts) drifted independently twice into P2 incidents
9
+ * (PI_CREW_MODEL stripped 2026-09-29; PI_CREW_STATE_BACKEND / CHILD_PI_MODE /
10
+ * CHILD_PI_POOL stripped 2026-10-10, fix 44d16d94). This module is the single
11
+ * place that GENERATES both crew-carrying allowlists from one decision table
12
+ * seeded from the read-side registry (`src/config/env-vars.ts` — CREW_ENV_VARS).
13
+ *
14
+ * BEHAVIOR-PRESERVATION CONTRACT (0.11.17):
15
+ * - The generated lists are byte-identical to the pre-registry literals (pinned
16
+ * by test/unit/config/env-allowlist-registry.test.ts against frozen copies),
17
+ * with ONE documented addition: PI_CREW_TOOLS_MODIFIERS (L2-3 opt-in flag,
18
+ * new control var introduced together with this registry).
19
+ * - Export paths do not move: async-runner.ts and child-pi-spawn.ts keep
20
+ * exporting their arrays (now assigned from the generators); consumers
21
+ * (spec-sandbox.ts, worktree/*, tests) are untouched.
22
+ * - `WINDOWS_ESSENTIAL_ENV_VARS` stays DEFINED in src/utils/env-allowlist.ts:
23
+ * test/unit/utils/env-allowlist.test.ts pins that file as the only src/
24
+ * location allowed to carry the var literals. This registry IMPORTS it as a
25
+ * building block — no literals duplicated here.
26
+ *
27
+ * DECISION MODEL — every CREW_ENV_VARS key MUST carry exactly one decision
28
+ * (fail-fast at module load + unit-test):
29
+ * - `allowlist` — ambient env forwarded via a generated allowlist (which
30
+ * destinations: "background-runner" and/or "child-pi").
31
+ * - `explicit` — forwarded through a dedicated set-only channel (e.g.
32
+ * forwardModelControlEnv for secret-LOOKING names sanitizeEnvSecrets would
33
+ * reject from an allowlist).
34
+ * - `child-write` — WRITTEN into the per-child `built.env` by the spawner
35
+ * (child-pi-spawn.ts / prepareSurfaceSpawn.ts / pi-args.ts env emission);
36
+ * ambient forwarding is neither needed nor wanted.
37
+ * - `host-only` — read only by the host pi-crew process (UI, config load,
38
+ * pre-spawn decisions); spawned processes never read it.
39
+ * - `gap` — KNOWN GAP: the reading code ALSO runs inside the detached
40
+ * background runner (or the worker-argv builder), but the var is
41
+ * deliberately NOT forwarded today — forwarding it would change runtime
42
+ * behavior, which this wave forbids. Gaps are surfaced (see
43
+ * crewEnvForwardingGapReport) for a follow-up wave; the C-1 gate exists so
44
+ * no NEW control var lands unclassified.
45
+ * - `test-only` — docs/test fixtures; never part of any runtime surface.
46
+ */
47
+
48
+ import { WINDOWS_ESSENTIAL_ENV_VARS } from "../utils/env-allowlist.ts";
49
+ import { CREW_ENV_VARS } from "./env-vars.ts";
50
+
51
+ // ── Platform groups (non-crew: OS + toolchain; NOT from CREW_ENV_VARS) ────
52
+
53
+ /** Process-essential vars every spawned process needs (locale/paths/term). */
54
+ export const ESSENTIAL_PROCESS_ENV_VARS: readonly string[] = [
55
+ "PATH",
56
+ "HOME",
57
+ "USER",
58
+ "SHELL",
59
+ "TERM",
60
+ "LANG",
61
+ "LC_ALL",
62
+ "LC_COLLATE",
63
+ "LC_CTYPE",
64
+ "LC_MESSAGES",
65
+ "LC_MONETARY",
66
+ "LC_NUMERIC",
67
+ "LC_TIME",
68
+ "XDG_CONFIG_HOME",
69
+ "XDG_DATA_HOME",
70
+ "XDG_CACHE_HOME",
71
+ "XDG_RUNTIME_DIR",
72
+ ];
73
+
74
+ /** Node/npm toolchain vars (nvm prefixes, npm registry/config). */
75
+ export const NVM_ENV_VARS: readonly string[] = ["NVM_BIN", "NVM_DIR", "NVM_INC"];
76
+ export const NODE_ENV_VARS: readonly string[] = ["NODE_DISABLE_COLORS", "NODE_EXTRA_CA_CERTS"];
77
+ export const NPM_CONFIG_ENV_VARS: readonly string[] = ["NPM_CONFIG_REGISTRY", "NPM_CONFIG_USERCONFIG", "NPM_CONFIG_GLOBALCONFIG"];
78
+
79
+ /**
80
+ * Mux-surface env (2026-08-30 Finding 2): the detached runner needs these to
81
+ * DETECT and REACH the host's tmux/herdr — without them every async run gates
82
+ * to no-mux and stays headless. None are secret-named (sanitizeEnvSecrets
83
+ * would reject secret-looking allowlist entries absent from the env).
84
+ */
85
+ export const MUX_SURFACE_ENV_VARS: readonly string[] = [
86
+ "TMUX",
87
+ "TMUX_PANE",
88
+ "TMUX_TMPDIR",
89
+ "HERDR_ENV",
90
+ "HERDR_SESSION",
91
+ "HERDR_PANE_ID",
92
+ "HERDR_SOCKET_PATH",
93
+ "HERDR_WORKSPACE_ID",
94
+ "HERDR_PING_TIMEOUT_MS",
95
+ ];
96
+
97
+ // ── Crew forwarding decisions (seeded from CREW_ENV_VARS) ─────────────────
98
+
99
+ export type EnvForwardDestination = "background-runner" | "child-pi";
100
+
101
+ export type CrewEnvForwardDecision =
102
+ | { kind: "allowlist"; destinations: readonly EnvForwardDestination[]; reason: string }
103
+ | { kind: "explicit"; channel: string; reason: string }
104
+ | { kind: "child-write"; reason: string }
105
+ | { kind: "host-only"; reason: string }
106
+ | { kind: "gap"; reason: string }
107
+ | { kind: "test-only"; reason: string };
108
+
109
+ const allowlist = (destinations: readonly EnvForwardDestination[], reason: string): CrewEnvForwardDecision => ({
110
+ kind: "allowlist",
111
+ destinations,
112
+ reason,
113
+ });
114
+
115
+ /**
116
+ * The decision table. Keys must be EXACTLY the CREW_ENV_VARS keys (validated at
117
+ * module load): adding a control var to env-vars.ts without a decision here
118
+ * throws on import — the C-1 guard against the "allowlist lagging the control
119
+ * var" incident class (checklist C-8 item 3).
120
+ */
121
+ const CREW_ENV_FORWARDING_DECL: Record<string, CrewEnvForwardDecision> = {
122
+ // — allowlist: forwarded to the detached background runner (+ children) —
123
+ PI_CREW_DEPTH: allowlist(["background-runner", "child-pi"], "depth accounting must survive into the runner AND every child worker"),
124
+ PI_TEAMS_DEPTH: allowlist(["background-runner"], "depth mirror (CREW ?? TEAMS read in the runner)"),
125
+ PI_CREW_MAX_DEPTH: allowlist(["background-runner"], "runner-side depth clamp"),
126
+ PI_TEAMS_MAX_DEPTH: allowlist(["background-runner"], "depth-clamp mirror"),
127
+ PI_CREW_INHERIT_PROJECT_CONTEXT: allowlist(["background-runner"], "runner builds worker prompts — inheritance must match the parent"),
128
+ PI_TEAMS_INHERIT_PROJECT_CONTEXT: allowlist(["background-runner"], "inherit mirror"),
129
+ PI_CREW_INHERIT_SKILLS: allowlist(["background-runner"], "runner builds worker prompts — inheritance must match the parent"),
130
+ PI_TEAMS_INHERIT_SKILLS: allowlist(["background-runner"], "inherit mirror"),
131
+ PI_CREW_PARENT_PID: allowlist(["background-runner"], "runner parent-guard liveness check"),
132
+ PI_TEAMS_PI_BIN: allowlist(["background-runner"], "runner spawns child pi workers — needs the explicit binary path"),
133
+ PI_TEAMS_MOCK_CHILD_PI: allowlist(["background-runner"], "runner-side mock fixture select"),
134
+ PI_CREW_ALLOW_MOCK: allowlist(["background-runner"], "mock gate double-keyed with PI_TEAMS_MOCK_CHILD_PI"),
135
+ PI_CREW_BACKGROUND_RUNNER_ENTRY: allowlist(["background-runner"], "entry guard read by the runner main() itself"),
136
+ PI_CREW_WORKER_ATOMIC_WRITER: allowlist(["background-runner"], "RFC 15 worker-thread atomic writer opt-in (runner side)"),
137
+ PI_TEAMS_WORKER_ATOMIC_WRITER: allowlist(["background-runner"], "atomic-writer mirror"),
138
+ PI_CREW_VERIFICATION_SANITIZE_ENV: allowlist(["background-runner"], "verification commands run inside the runner"),
139
+ PI_TEAMS_VERIFICATION_SANITIZE_ENV: allowlist(["background-runner"], "sanitize mirror"),
140
+ PI_CREW_VERIFICATION_PRESERVE_ENV: allowlist(["background-runner"], "verification commands run inside the runner"),
141
+ PI_TEAMS_VERIFICATION_PRESERVE_ENV: allowlist(["background-runner"], "preserve-env mirror"),
142
+ PI_CREW_VERIFICATION_WORKTREE: allowlist(["background-runner"], "verification worktree sandbox (RFC 16) runs inside the runner"),
143
+ PI_TEAMS_VERIFICATION_WORKTREE: allowlist(["background-runner"], "worktree-sandbox mirror"),
144
+ PI_CREW_BG_REPORT_ON_FATAL: allowlist(
145
+ ["background-runner"],
146
+ "V8 report flags are applied by the parent when BUILDING the runner argv (RFC 17); forwarded for parity/diagnostics",
147
+ ),
148
+ PI_TEAMS_BG_REPORT_ON_FATAL: allowlist(["background-runner"], "report-on-fatal mirror"),
149
+ PI_CREW_MODEL: allowlist(
150
+ ["background-runner"],
151
+ "model routing for manifests without persisted modelContext (2026-09-29 full-battery Finding #1)",
152
+ ),
153
+ PI_CREW_MODEL_FALLBACK_ORDER: allowlist(["background-runner"], "resolveModelFallbackPolicy in the runner"),
154
+ PI_CREW_STATE_BACKEND: allowlist(["background-runner"], "runner OWNS task-state writes (U8 P2 parity — d0cef00f/44d16d94)"),
155
+ PI_CREW_CHILD_PI_MODE: allowlist(["background-runner"], "runner spawns the child-pi workers (U14 parity)"),
156
+ PI_CREW_CHILD_PI_POOL: allowlist(["background-runner"], "rpc worker pool rides along for the same parity reason"),
157
+ PI_CREW_TOOLS_MODIFIERS: allowlist(
158
+ ["background-runner"],
159
+ "L2-3 opt-in: the runner builds worker argv (pi-args) — the flag must reach it. The ONE deliberate content addition shipped with this registry",
160
+ ),
161
+
162
+ // — explicit: set-only forwarding channels (never ambient) —
163
+ PI_CREW_MODEL_REQUIRE_CREDENTIALS: {
164
+ kind: "explicit",
165
+ channel: "forwardModelControlEnv",
166
+ reason: "secret-LOOKING suffix trips sanitizeEnvSecrets on unset envs — copied set-only (async-runner.ts)",
167
+ },
168
+ PI_CREW_PEER_DEP_DIR: {
169
+ kind: "explicit",
170
+ channel: "async-runner childEnv (PEER_DEP_DIR_ENV)",
171
+ reason: "resolved peer-dep dir is computed at spawn time and injected next to the sanitized env",
172
+ },
173
+ PI_CREW_ASYNC_RUN: {
174
+ kind: "explicit",
175
+ channel: "buildBackgroundRunnerEnv",
176
+ reason: "async telemetry marker STAMPED onto the runner env (never ambient; not a surface gate)",
177
+ },
178
+
179
+ // — child-write: written into per-child built.env by the spawner —
180
+ PI_CREW_SCRATCHPAD: { kind: "child-write", reason: "child-pi-spawn dormant gate" },
181
+ PI_CREW_KIND: { kind: "child-write", reason: "child-pi-spawn worker-kind marker" },
182
+ PI_CREW_TASK_ID: { kind: "child-write", reason: "child-pi-spawn per-child task id" },
183
+ PI_CREW_EVENTS_PATH: { kind: "child-write", reason: "child-pi-spawn per-child events log" },
184
+ PI_CREW_ASK_ENABLED: { kind: "child-write", reason: "child-pi-spawn dormant ask gate" },
185
+ PI_CREW_MSG_ENABLED: { kind: "child-write", reason: "child-pi-spawn dormant message gate" },
186
+ PI_CREW_DELEGATE_ENABLED: { kind: "child-write", reason: "child-pi-spawn dormant delegate gate" },
187
+ PI_CREW_STATE_ROOT: { kind: "child-write", reason: "child-pi-spawn mailbox root" },
188
+ PI_CREW_BROKER_RUN_ID: { kind: "child-write", reason: "child-pi-spawn broker handshake" },
189
+ PI_CREW_AGENT_EVENTS_PATH: { kind: "child-write", reason: "prepareSurfaceSpawn recorder log" },
190
+ PI_CREW_BROKER_SOCKET: { kind: "child-write", reason: "child-pi-spawn broker handshake (never ambient — per-run socket)" },
191
+ PI_CREW_BROKER_TOKEN: {
192
+ kind: "child-write",
193
+ reason: "per-task compound broker token via built.env/pipe (F4) — secret, never allowlisted",
194
+ },
195
+ PI_CREW_BROKER_TASK_ID: { kind: "child-write", reason: "child-pi-spawn broker handshake" },
196
+ PI_CREW_STEERING_FILE: { kind: "child-write", reason: "child-pi-spawn per-run steering JSONL" },
197
+ PI_CREW_MAX_OUTPUT: { kind: "child-write", reason: "pi-args emits agent.maxTokens into built.env" },
198
+ PI_CREW_RPC_SECRET: { kind: "child-write", reason: "shared HMAC secret handed to the child at spawn — secret name, never allowlisted" },
199
+ PI_CREW_AUTO_EXIT: { kind: "child-write", reason: "prepareSurfaceSpawn D7 shutdown marker" },
200
+ PI_CREW_SURFACE: { kind: "child-write", reason: "prepareSurfaceSpawn surface kind" },
201
+ PI_CREW_PARENT_START_TIME: { kind: "child-write", reason: "prepareSurfaceSpawn pid-reuse-safe guard input" },
202
+ PI_CREW_ARTIFACTS_ROOT: { kind: "child-write", reason: "child-pi-spawn scratchpad containment root" },
203
+ PI_CREW_SCRATCHPAD_SNAPSHOT: { kind: "child-write", reason: "child-pi-spawn snapshot target" },
204
+ PI_CREW_ATTEMPT: { kind: "child-write", reason: "child-pi-spawn attempt number" },
205
+ PI_CREW_SCRATCHPAD_RESTORE: { kind: "child-write", reason: "child-pi-spawn restore hint" },
206
+ PI_CREW_SCRATCHPAD_RESTORE_MTIME: { kind: "child-write", reason: "child-pi-spawn swap-detection pin" },
207
+ PI_CREW_GUEST: { kind: "child-write", reason: "guest scratchpad self-marker" },
208
+
209
+ // — host-only: read by the host pi-crew process before/during spawn —
210
+ PI_CREW_HOME: { kind: "host-only", reason: "host config/paths resolution" },
211
+ PI_TEAMS_HOME: { kind: "host-only", reason: "host config/paths resolution (mirror)" },
212
+ PI_CREW_ROLE: { kind: "host-only", reason: "role-permission host-side check" },
213
+ PI_TEAMS_ROLE: { kind: "host-only", reason: "role mirror" },
214
+ PI_CREW_ADAPTIVE_REPAIR: { kind: "host-only", reason: "adaptive-plan repair runs pre-spawn in the host" },
215
+ PI_TEAMS_ADAPTIVE_REPAIR: { kind: "host-only", reason: "adaptive-repair mirror" },
216
+ PI_CREW_EXECUTE_WORKERS: { kind: "host-only", reason: "spawn-mode decision made by the host" },
217
+ PI_TEAMS_EXECUTE_WORKERS: { kind: "host-only", reason: "execute-workers mirror" },
218
+ PI_CREW_TEST_ASYNC_INLINE: { kind: "host-only", reason: "async-runner spawn seam decision (test-only, host side)" },
219
+ PI_CREW_ASYNC_EARLY_EXIT_GUARD: { kind: "host-only", reason: "team-tool/run.ts early-exit guard (host spawn path)" },
220
+ PI_CREW_BROKER: { kind: "host-only", reason: "defaults/lifecycle broker enable (host session)" },
221
+ PI_CREW_DEBUG_BUDGET: { kind: "host-only", reason: "token-budget debug log at run submit" },
222
+ PI_CREW_DEBUG_STALE: { kind: "host-only", reason: "stale reconciler forensic gate — may run in any HOST process" },
223
+ PI_CREW_MOCK_LIVE_SESSION: { kind: "host-only", reason: "runtime-resolver mode pick (host)" },
224
+ PI_CREW_SKIP_HOME_CHECK: { kind: "host-only", reason: "config load escape check" },
225
+ PI_CREW_SUPPRESS_RPC_WARNING: { kind: "host-only", reason: "host extension rpc-hmac warning" },
226
+ PI_CREW_PLAN_UI: { kind: "host-only", reason: "host UI pane toggle" },
227
+ PI_CREW_KEYBINDINGS: { kind: "host-only", reason: "host keybinding map" },
228
+ PI_CREW_AUTO_PRUNE_KEEP: { kind: "host-only", reason: "session-start auto-prune (run-maintenance, host)" },
229
+ PI_CREW_AUTO_PRUNE_AGE_FLOOR_HOURS: { kind: "host-only", reason: "session-start auto-prune (host)" },
230
+ PI_CREW_USE_BUNDLE: { kind: "host-only", reason: "root index.ts loader choice (outside src/ gate scope)" },
231
+
232
+ // — gap: read by code that ALSO runs in the detached runner, deliberately
233
+ // NOT forwarded (behavior preservation 0.11.17) — follow-up candidates.
234
+ PI_CREW_PROMPT_BREAKDOWN: { kind: "gap", reason: "prompt-builder runs in the runner for async runs" },
235
+ PI_CREW_PROMPT_SKILLS: { kind: "gap", reason: "prompt-builder runs in the runner for async runs" },
236
+ PI_CREW_BACKGROUND_MODE: { kind: "gap", reason: "child-executor read inside the runner" },
237
+ PI_CREW_DEBUG: { kind: "gap", reason: "background-runner debug logging" },
238
+ PI_TEAMS_DEBUG: { kind: "gap", reason: "internal-error severity gate — every process incl. the runner" },
239
+ PI_CREW_DWF_SCRIPT_TIMEOUT_MS: { kind: "gap", reason: "dynamic-workflow runner executes in-process" },
240
+ PI_CREW_DWF_SKIP_DETERMINISM_CHECK: { kind: "gap", reason: "deterministic-ast check in the DWF path" },
241
+ PI_CREW_TRUST_PROJECT_DWF: { kind: "gap", reason: "project DWF trust gate on the runner path" },
242
+ PI_CREW_HEARTBEAT_PULSE_MS: { kind: "gap", reason: "child-executor liveness pulse (runner)" },
243
+ PI_CREW_INTERRUPT_GUARD_INTERVAL_MS: { kind: "gap", reason: "background-runner interrupt guard" },
244
+ PI_CREW_MAX_RUN_MS: { kind: "gap", reason: "background-runner run ceiling" },
245
+ PI_CREW_MAX_WORKERS: { kind: "gap", reason: "global worker cap — enforced in the runner too" },
246
+ PI_CREW_DISABLE_RESULT_READ_CACHE: { kind: "gap", reason: "task-output-context read cache (runner)" },
247
+ PI_CREW_HANDOFF_BUDGET_TOKENS: { kind: "gap", reason: "dependency-context trim (runner)" },
248
+ PI_CREW_PARENT_GUARD_INTERVAL_MS: { kind: "gap", reason: "parent-guard poll (runner + surface workers)" },
249
+ PI_CREW_SCRATCHPAD_DEMOTE_BASH: { kind: "gap", reason: "agent-config parse on the runner prompt path" },
250
+ PI_CREW_TRUST_PROJECT_AGENT_EXTENSIONS: { kind: "gap", reason: "pi-args reads input.env when the runner builds worker argv" },
251
+ PI_CREW_PERSISTENCE_SKIP_TASKS_FSYNC: { kind: "gap", reason: "config load happens in the runner too" },
252
+ PI_TEAMS_CHILD_RESPONSE_TIMEOUT_MS: { kind: "gap", reason: "child-pi response timeout resolved in the runner" },
253
+ PI_CREW_SESSION_RECOVERY: { kind: "gap", reason: "session-recovery resolve on the runner path" },
254
+ PI_CREW_HERMETIC_WORKERS: { kind: "gap", reason: "pi-args hermetic resolve when the runner builds argv" },
255
+ PI_CREW_WORKER_TRANSPORT: { kind: "gap", reason: "rpc-worker transport seam (runner)" },
256
+ PI_CREW_CLASSIFIER_ENABLED: { kind: "gap", reason: "child-executor retry classifier (runner)" },
257
+ PI_CREW_CLASSIFIER_MODEL: { kind: "gap", reason: "child-executor retry classifier (runner)" },
258
+ PI_CREW_VERIFY_GATE: { kind: "gap", reason: "verify-gate pre-gate before verifier spawns (runner)" },
259
+ PI_CREW_VERIFY_GATE_TIMEOUT_MS: { kind: "gap", reason: "verify-gate per-check timeout (runner)" },
260
+ PI_CREW_RPC_DIALOG_ANSWER: { kind: "gap", reason: "rpc-worker dialog policy (runner)" },
261
+ PI_CREW_RPC_TURN_TIMEOUT_MS: { kind: "gap", reason: "rpc-worker turn timeout (runner)" },
262
+ PI_CREW_MAX_AUTO_FALLBACKS: { kind: "gap", reason: "model-fallback cap resolved in the runner" },
263
+
264
+ // — test-only: docs/test fixtures; never on a runtime surface —
265
+ PI_CREW_SMOKE: { kind: "test-only", reason: "smoke helper" },
266
+ PI_CREW_RUN_PLATFORM_TESTS: { kind: "test-only", reason: "platform test gate" },
267
+ PI_CREW_HARD_KILL_GRACE_MS: { kind: "test-only", reason: "watchdog harness script" },
268
+ PI_CREW_LIVE_MODEL: { kind: "test-only", reason: "live-session test model" },
269
+ PI_CREW_ENABLE_EXPERIMENTAL_LIVE_SESSION: { kind: "test-only", reason: "experimental opt-in (docs)" },
270
+ PI_CREW_RUN_ID: { kind: "test-only", reason: "fixture name; src uses PI_CREW_BROKER_RUN_ID" },
271
+ PI_TEAMS_CONFIG_KEYS: { kind: "test-only", reason: "fixture" },
272
+ PI_TEAMS_MOCK: { kind: "test-only", reason: "fixture" },
273
+ PI_CREW_VERIFICATION: { kind: "test-only", reason: "partial fixture name" },
274
+ PI_TEAMS_VERIFICATION: { kind: "test-only", reason: "partial fixture name" },
275
+ PI_CREW_ATOMIC_WRITER: { kind: "test-only", reason: "superseded by PI_CREW_WORKER_ATOMIC_WRITER" },
276
+ PI_CREW_BROKER_DIAG_UI: { kind: "test-only", reason: "removed" },
277
+ PI_CREW_DEBUG_KILL: { kind: "test-only", reason: "docs/test-only" },
278
+ PI_CREW_DWF_SANDBOX_DRY_RUN: { kind: "test-only", reason: "docs/test-only" },
279
+ PI_CREW_HOOK: { kind: "test-only", reason: "docs/test-only" },
280
+ PI_CREW_POOL_HEALTH: { kind: "test-only", reason: "docs/test-only" },
281
+ PI_CREW_QUIET_PREFLIGHT: { kind: "test-only", reason: "docs/test-only" },
282
+ PI_CREW_SAFE_BASH: { kind: "test-only", reason: "dead" },
283
+ PI_CREW_SCRATCHPAD_EXPERIMENT: { kind: "test-only", reason: "docs/test-only" },
284
+ PI_CREW_SESSION_DEPTH: { kind: "test-only", reason: "legacy name (ADR drift 2026-08-09)" },
285
+ PI_CREW_SIG: { kind: "test-only", reason: "docs/test-only" },
286
+ PI_CREW_SNAPSHOT_HMAC_KEY: { kind: "test-only", reason: "reverted 2026-08-13" },
287
+ PI_CREW_SNAPSHOT_HMAC_STRICT: { kind: "test-only", reason: "reverted 2026-08-13" },
288
+ PI_CREW_TEST_REAL_MODEL: { kind: "test-only", reason: "docs/test-only" },
289
+ PI_CREW_TOOLING_429_NOTE: { kind: "test-only", reason: "docs/test-only" },
290
+ PI_CREW_MAX_OUTPUT_TOKENS: { kind: "test-only", reason: "legacy name; real var PI_CREW_MAX_OUTPUT" },
291
+ PI_CREW_GOAL_OSCILLATION_EMBEDDINGS: { kind: "test-only", reason: "docs/test-only" },
292
+ PI_CREW_OPENAI_API_KEY: { kind: "test-only", reason: "env-filter secret-name EXAMPLE (comment-only)" },
293
+ PI_CREW_SECRET_TOKEN: { kind: "test-only", reason: "env-filter secret-name EXAMPLE (comment-only)" },
294
+ PI_CREW_REFUSE_GATE_PATTERNS: { kind: "test-only", reason: "docs-only" },
295
+ PI_CREW_PROCEED_PATTERNS: { kind: "test-only", reason: "docs-only" },
296
+ PI_CREW_ENFORCEMENT_PATTERNS: { kind: "test-only", reason: "docs-only" },
297
+ PI_CREW_DIR: { kind: "test-only", reason: "isolation fixture" },
298
+ PI_CREW_ROOT: { kind: "test-only", reason: "isolation fixture" },
299
+ PI_CREW_FOO: { kind: "test-only", reason: "isolation fixture" },
300
+ PI_CREW_VAR: { kind: "test-only", reason: "isolation fixture" },
301
+ PI_CREW_X: { kind: "test-only", reason: "isolation fixture" },
302
+ PI_CREW_TEST_SECRET_VALUE: { kind: "test-only", reason: "isolation fixture" },
303
+ PI_TEAMS_BAR: { kind: "test-only", reason: "isolation fixture" },
304
+ };
305
+
306
+ function validateDecisionTable(): void {
307
+ const registryKeys = new Set(Object.keys(CREW_ENV_VARS));
308
+ const decisionKeys = new Set(Object.keys(CREW_ENV_FORWARDING_DECL));
309
+ const unclassified = [...registryKeys].filter((k) => !decisionKeys.has(k));
310
+ if (unclassified.length > 0) {
311
+ throw new Error(`env-allowlist-registry: CREW_ENV_VARS entries without a forwarding decision (C-1): ${unclassified.join(", ")}`);
312
+ }
313
+ const orphan = [...decisionKeys].filter((k) => !registryKeys.has(k));
314
+ if (orphan.length > 0) {
315
+ throw new Error(`env-allowlist-registry: decisions for names NOT in CREW_ENV_VARS (orphans): ${orphan.join(", ")}`);
316
+ }
317
+ }
318
+
319
+ validateDecisionTable();
320
+
321
+ /** Read-only view of the decision table (tests + gap reporting). */
322
+ export function crewEnvForwardingDecisions(): Readonly<Record<string, CrewEnvForwardDecision>> {
323
+ return CREW_ENV_FORWARDING_DECL;
324
+ }
325
+
326
+ /** Names whose decision is `kind` (sorted for stable reporting). */
327
+ export function crewEnvNamesByDecisionKind(kind: CrewEnvForwardDecision["kind"]): string[] {
328
+ return Object.entries(CREW_ENV_FORWARDING_DECL)
329
+ .filter(([, d]) => d.kind === kind)
330
+ .map(([name]) => name)
331
+ .sort();
332
+ }
333
+
334
+ /**
335
+ * Known forwarding gaps (kind "gap"): control vars read by spawned-process
336
+ * code but deliberately NOT forwarded as of 0.11.17. Surfaced for follow-up
337
+ * waves — DO NOT silently forward one (each addition changes runtime env
338
+ * behavior and needs its own parity rationale).
339
+ */
340
+ export function crewEnvForwardingGapReport(): { name: string; reason: string }[] {
341
+ return Object.entries(CREW_ENV_FORWARDING_DECL)
342
+ .filter(([, d]) => d.kind === "gap")
343
+ .map(([name, d]) => ({ name, reason: d.reason }))
344
+ .sort((a, b) => a.name.localeCompare(b.name));
345
+ }
346
+
347
+ /**
348
+ * Validate the FULL ordered group set for one destination. Fail-fast
349
+ * invariants (programming errors, caught by tests at authoring time;
350
+ * runtime never sees these throw on a green test suite):
351
+ * - every name must be a CREW_ENV_VARS key with an `allowlist` decision
352
+ * covering `destination` (no orphan entries — C-1 test),
353
+ * - no duplicates across groups,
354
+ * - every `allowlist`-decision name for `destination` appears in exactly
355
+ * one group (no silently dropped control var — the incident class).
356
+ */
357
+ function validateCrewForwardingOrder(destination: EnvForwardDestination, groups: readonly (readonly string[])[]): void {
358
+ const tagged = new Set(
359
+ Object.entries(CREW_ENV_FORWARDING_DECL)
360
+ .filter(([, d]) => d.kind === "allowlist" && d.destinations.includes(destination))
361
+ .map(([name]) => name),
362
+ );
363
+ const seen = new Set<string>();
364
+ for (const group of groups) {
365
+ for (const name of group) {
366
+ if (seen.has(name)) {
367
+ throw new Error(`env-allowlist-registry: duplicate crew env "${name}" for ${destination}`);
368
+ }
369
+ if (!tagged.has(name)) {
370
+ throw new Error(
371
+ `env-allowlist-registry: "${name}" is in the ${destination} group order but has no allowlist decision for it (orphan or mis-tagged)`,
372
+ );
373
+ }
374
+ seen.add(name);
375
+ }
376
+ }
377
+ for (const name of tagged) {
378
+ if (!seen.has(name)) {
379
+ throw new Error(`env-allowlist-registry: "${name}" is allowlisted for ${destination} but missing from its group order`);
380
+ }
381
+ }
382
+ }
383
+
384
+ // Historical group ORDER (pre-registry literals — do not reorder; the
385
+ // behavior-preservation test pins exact content):
386
+ const BG_CREW_CORE_ORDER = [
387
+ // depth + inheritance + parent-guard + bin/mock gates + entry guard +
388
+ // atomic writer + verification trio + report-on-fatal (pre-mux block).
389
+ "PI_CREW_DEPTH",
390
+ "PI_CREW_MAX_DEPTH",
391
+ "PI_CREW_INHERIT_PROJECT_CONTEXT",
392
+ "PI_CREW_INHERIT_SKILLS",
393
+ "PI_CREW_PARENT_PID",
394
+ "PI_TEAMS_DEPTH",
395
+ "PI_TEAMS_MAX_DEPTH",
396
+ "PI_TEAMS_INHERIT_PROJECT_CONTEXT",
397
+ "PI_TEAMS_INHERIT_SKILLS",
398
+ "PI_TEAMS_PI_BIN",
399
+ "PI_TEAMS_MOCK_CHILD_PI",
400
+ "PI_CREW_ALLOW_MOCK",
401
+ "PI_CREW_BACKGROUND_RUNNER_ENTRY",
402
+ "PI_CREW_WORKER_ATOMIC_WRITER",
403
+ "PI_TEAMS_WORKER_ATOMIC_WRITER",
404
+ "PI_CREW_VERIFICATION_SANITIZE_ENV",
405
+ "PI_TEAMS_VERIFICATION_SANITIZE_ENV",
406
+ "PI_CREW_VERIFICATION_PRESERVE_ENV",
407
+ "PI_TEAMS_VERIFICATION_PRESERVE_ENV",
408
+ "PI_CREW_VERIFICATION_WORKTREE",
409
+ "PI_TEAMS_VERIFICATION_WORKTREE",
410
+ "PI_CREW_BG_REPORT_ON_FATAL",
411
+ "PI_TEAMS_BG_REPORT_ON_FATAL",
412
+ ] as const;
413
+ const BG_CREW_MODEL_ORDER = ["PI_CREW_MODEL", "PI_CREW_MODEL_FALLBACK_ORDER"] as const;
414
+ const BG_CREW_STATE_ORDER = ["PI_CREW_STATE_BACKEND", "PI_CREW_CHILD_PI_MODE", "PI_CREW_CHILD_PI_POOL"] as const;
415
+ const BG_CREW_TOOLS_ORDER = ["PI_CREW_TOOLS_MODIFIERS"] as const;
416
+ const CHILD_CREW_DEPTH_ORDER = ["PI_CREW_DEPTH"] as const;
417
+
418
+ const BG_CREW_GROUPS = [BG_CREW_CORE_ORDER, BG_CREW_MODEL_ORDER, BG_CREW_STATE_ORDER, BG_CREW_TOOLS_ORDER] as const;
419
+
420
+ /**
421
+ * Generate the detached background-runner allowlist (exported AS
422
+ * BACKGROUND_RUNNER_ENV_ALLOWLIST by src/runtime/async-runner.ts).
423
+ * Provider API keys are INTENTIONALLY OMITTED (security review M1): the
424
+ * runner only spawns child Pi workers, which read keys from the Pi config
425
+ * file (not env) — env keys leak into V8 fatal-error reports unredacted.
426
+ * Layout preserves the pre-registry literal order: platform prefix → crew
427
+ * core → mux surface → model → state → tools-modifier (the one addition).
428
+ */
429
+ export function backgroundRunnerEnvAllowlist(): string[] {
430
+ validateCrewForwardingOrder("background-runner", BG_CREW_GROUPS);
431
+ return [
432
+ ...ESSENTIAL_PROCESS_ENV_VARS,
433
+ ...WINDOWS_ESSENTIAL_ENV_VARS,
434
+ ...NVM_ENV_VARS,
435
+ "NODE_PATH",
436
+ ...NODE_ENV_VARS,
437
+ ...NPM_CONFIG_ENV_VARS,
438
+ ...BG_CREW_CORE_ORDER,
439
+ ...MUX_SURFACE_ENV_VARS,
440
+ ...BG_CREW_MODEL_ORDER,
441
+ ...BG_CREW_STATE_ORDER,
442
+ ...BG_CREW_TOOLS_ORDER,
443
+ ];
444
+ }
445
+
446
+ /**
447
+ * Generate the child-Pi base allowlist (exported AS BASE_ALLOWLIST by
448
+ * src/runtime/child-pi/child-pi-spawn.ts). Provider API keys are injected
449
+ * dynamically via buildScopedAllowList() only when a model is assigned
450
+ * (per-task key scoping). No NODE_PATH here — buildChildPiSpawnOptions
451
+ * validates it separately against safe prefixes.
452
+ */
453
+ export function childPiBaseAllowlist(): string[] {
454
+ validateCrewForwardingOrder("child-pi", [CHILD_CREW_DEPTH_ORDER]);
455
+ return [
456
+ ...ESSENTIAL_PROCESS_ENV_VARS,
457
+ ...WINDOWS_ESSENTIAL_ENV_VARS,
458
+ ...NVM_ENV_VARS,
459
+ ...NODE_ENV_VARS,
460
+ ...NPM_CONFIG_ENV_VARS,
461
+ ...CHILD_CREW_DEPTH_ORDER,
462
+ ];
463
+ }
@@ -242,6 +242,10 @@ export const CREW_ENV_VARS: Record<string, CrewEnvVarSpec> = {
242
242
  parser: "boolean",
243
243
  doc: "'1' runs async team runs IN-PROCESS instead of spawning a detached background-runner (test seam; requires PI_CREW_ALLOW_MOCK=1 — see async-runner.ts spawnBackgroundTeamRun)",
244
244
  },
245
+ PI_CREW_ASYNC_RUN: {
246
+ name: "PI_CREW_ASYNC_RUN",
247
+ doc: "async telemetry marker STAMPED onto the detached runner env by buildBackgroundRunnerEnv (async-runner.ts), read by resolve-surface.ts SurfaceGateEnvSnapshot — NOT a surface gate (registry entry added with the C-1 env-allowlist registry 2026-10-11 — was a read/write site missing from this registry)",
248
+ },
245
249
  PI_CREW_BACKGROUND_RUNNER_ENTRY: {
246
250
  name: "PI_CREW_BACKGROUND_RUNNER_ENTRY",
247
251
  parser: "boolean",
@@ -433,6 +437,10 @@ export const CREW_ENV_VARS: Record<string, CrewEnvVarSpec> = {
433
437
  name: "PI_CREW_MSG_ENABLED",
434
438
  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)",
435
439
  },
440
+ PI_CREW_DELEGATE_ENABLED: {
441
+ name: "PI_CREW_DELEGATE_ENABLED",
442
+ doc: "'1' enables the worker-side delegate tool — dormant-until-env gate (written UNCONDITIONALLY by child-pi-spawn.ts, read by prompt-runtime.ts; registry entry added with the C-1 env-allowlist registry 2026-10-11 — was a read/write site missing from this registry)",
443
+ },
436
444
  PI_CREW_STATE_ROOT: {
437
445
  name: "PI_CREW_STATE_ROOT",
438
446
  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)",
@@ -455,6 +463,11 @@ export const CREW_ENV_VARS: Record<string, CrewEnvVarSpec> = {
455
463
  parser: "boolean",
456
464
  doc: "R3-19/D5 hermetic worker spawn override: '1' forces --no-extensions on worker argv, '0' restores the old ambient-stack parity (discovered/configured/built-in extensions load); beats BuildPiWorkerArgsInput.hermeticWorkers / runtime.hermeticWorkers; default ON (pi-args.ts:resolveHermeticWorkers). Explicit -e prompt-runtime + agent extensions + --skill flags are unaffected (cli.md:186-191)",
457
465
  },
466
+ PI_CREW_TOOLS_MODIFIERS: {
467
+ name: "PI_CREW_TOOLS_MODIFIERS",
468
+ parser: "boolean",
469
+ doc: "L2-3 (pi 1.1.0 `+name`/`-name` --tools modifier syntax) OPT-IN, default OFF: '1' emits a declared agent tool list as --tools +a,+b (default toolset PLUS the declarations) instead of the legacy replacement allowlist, but ONLY when the probed host piVersion >= 1.1.0 (PI_TOOLS_MODIFIER_FLOOR, pi-args.ts); below the floor or unknown version falls back to the legacy syntax (pi's strict parser + getToolListError must never see modifiers on a pre-1.1.0 host). Additive semantics: the worker keeps the FULL default tool surface (incl. runtime-registered control tools), so CONTROL_TOOLS are not merged in modifier mode (pi-args.ts)",
470
+ },
458
471
  PI_CREW_STATE_BACKEND: {
459
472
  name: "PI_CREW_STATE_BACKEND",
460
473
  doc: "U8 (upgrade-spec 2026-10-09) run-state store backend select: 'sqlite' stores manifest+tasks+worker-status in <runId>/state.sqlite via node:sqlite with ONE transaction per batch write (all-or-nothing; manifest.json/tasks.json stay written as read-side mirrors for legacy consumers — loads read the db); unset or any other value keeps the default JSON file backend. events.jsonl audit spine unchanged in both modes (state-store.ts / sqlite-run-state.ts isSqliteStateBackend)",
@@ -141,6 +141,42 @@ export interface CrewRuntimeConfig {
141
141
  /** Default runtime for roles not in isolatedRoles. Default: "live-session" (uses live-session). */
142
142
  defaultRuntime?: "live-session" | "child-process";
143
143
  };
144
+ /**
145
+ * U11 (hybrid 2-tier worker model, upgrade spec 2026-10-11): per-role
146
+ * opt-in to the EMBEDDED tier — in-process live-session workers (the U4
147
+ * stack) — with subprocess as the automatic fallback tier.
148
+ *
149
+ * DEFAULT-OFF: subprocess stays the ONLY tier unless a role explicitly
150
+ * opts in via `roles`. With the block absent/disabled, tier routing is a
151
+ * byte-identical no-op (no imports, no events, no budget reads).
152
+ *
153
+ * Routing guards (resolveTaskRuntimeKind): isolatedRoles and the
154
+ * depth>0 child-process guard ALWAYS beat embedded opt-in — an embedded
155
+ * session never nests inside a pi-crew worker and a crash-isolated role
156
+ * never leaves the subprocess tier. Not sensitive — behavior pick, not
157
+ * authority.
158
+ */
159
+ embeddedWorkers?: {
160
+ /** Global gate for the embedded tier. Default false (tier inert). */
161
+ enabled?: boolean;
162
+ /** Roles that opt into the embedded tier. Default: [] (nobody). */
163
+ roles?: string[];
164
+ /**
165
+ * Ceiling on concurrently in-flight embedded sessions. When exhausted,
166
+ * new routed tasks fail-fast back to the subprocess tier (never queue
167
+ * on this ceiling — they queue only on the SHARED global worker budget
168
+ * once rerouted). Blast-radius bound (spec U11 risk #2 mitigation:
169
+ * "limit N embedded đồng thời"). Default: max(1, floor(worker-cap/2)).
170
+ */
171
+ maxConcurrent?: number;
172
+ /**
173
+ * Counted embedded faults per task before that task reroutes to the
174
+ * subprocess tier for all subsequent attempts (sticky, persisted per
175
+ * task in task.diagnostics). Aborts are NOT faults. Default 2
176
+ * (spec: "embedded worker fault 2 lần → hạ cấp xuống subprocess").
177
+ */
178
+ faultThreshold?: number;
179
+ };
144
180
  /** Mark certain bash commands as excludeFromContext to reduce context tokens. Default: false */
145
181
  excludeContextBash?: boolean;
146
182
  /**
@@ -323,7 +323,8 @@ interface TeamToolProgressBinder {
323
323
  stop: () => void;
324
324
  }
325
325
 
326
- function startTeamToolProgressBinder(onUpdate: OnUpdate | undefined): TeamToolProgressBinder {
326
+ // Exported for the FIX-ROUND MAJOR 2 regression test (durationMs plumbing).
327
+ export function startTeamToolProgressBinder(onUpdate: OnUpdate | undefined): TeamToolProgressBinder {
327
328
  if (!onUpdate) {
328
329
  return { attach: () => undefined, stop: () => undefined };
329
330
  }
@@ -356,6 +357,11 @@ function startTeamToolProgressBinder(onUpdate: OnUpdate | undefined): TeamToolPr
356
357
  status: loaded.manifest.status,
357
358
  runId,
358
359
  startedAt,
360
+ // FIX-ROUND MAJOR 2 (review 0.11.17): plumb the producer-measured
361
+ // duration so the formatter consumes a real durationMs instead of
362
+ // always self-timing from startedAt (B-3 path was built but no
363
+ // streaming binder fed it).
364
+ durationMs: Date.now() - startedAt,
359
365
  manifest: loaded.manifest,
360
366
  tasks: loaded.tasks,
361
367
  agents,