pi-crew 0.9.68 → 0.10.1

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 (217) hide show
  1. package/CHANGELOG.md +124 -0
  2. package/agents/analyst.md +1 -1
  3. package/agents/cold-verifier.md +3 -1
  4. package/agents/critic.md +1 -1
  5. package/agents/executor.md +1 -1
  6. package/agents/explorer.md +1 -1
  7. package/agents/planner.md +1 -1
  8. package/agents/reviewer.md +1 -1
  9. package/agents/security-reviewer.md +1 -1
  10. package/agents/test-engineer.md +1 -1
  11. package/agents/verifier.md +1 -1
  12. package/agents/writer.md +1 -1
  13. package/dist/index.mjs +65838 -60331
  14. package/docs/README.md +2 -0
  15. package/docs/actions-reference.md +31 -0
  16. package/docs/commands-reference.md +17 -6
  17. package/docs/resource-formats.md +13 -0
  18. package/package.json +4 -2
  19. package/schema.json +15 -1
  20. package/scripts/resource-sampler.mjs +36 -2
  21. package/skills/requirements-to-task-packet/SKILL.md +26 -0
  22. package/skills/widget-rendering/SKILL.md +7 -7
  23. package/src/agents/agent-config.ts +2 -1
  24. package/src/agents/discover-agents.ts +23 -14
  25. package/src/config/config-merge.ts +183 -0
  26. package/src/config/config-validation.ts +681 -0
  27. package/src/config/config.ts +22 -864
  28. package/src/config/defaults.ts +35 -1
  29. package/src/config/drift-detector.ts +1 -1
  30. package/src/config/env-vars.ts +691 -0
  31. package/src/config/role-tools.ts +8 -8
  32. package/src/config/sanitize-project-config.ts +172 -0
  33. package/src/config/types.ts +30 -0
  34. package/src/extension/async-notifier.ts +25 -2
  35. package/src/extension/crew-vibes/config.ts +2 -1
  36. package/src/extension/plan-orchestrate.ts +132 -0
  37. package/src/extension/registration/commands/dashboard.ts +158 -0
  38. package/src/extension/registration/commands/index.ts +35 -0
  39. package/src/extension/registration/commands/manage.ts +303 -0
  40. package/src/extension/registration/commands/run.ts +214 -0
  41. package/src/extension/registration/commands/shared.ts +631 -0
  42. package/src/extension/registration/commands/status.ts +60 -0
  43. package/src/extension/registration/commands.ts +13 -1224
  44. package/src/extension/registration/lifecycle-handlers.ts +126 -15
  45. package/src/extension/registration/subagent-tools.ts +218 -9
  46. package/src/extension/rpc-hmac.ts +5 -3
  47. package/src/extension/team-tool/api/heartbeat.ts +47 -10
  48. package/src/extension/team-tool/api/plan-approval.ts +9 -0
  49. package/src/extension/team-tool/api/task-claims.ts +109 -40
  50. package/src/extension/team-tool/cancel.ts +84 -50
  51. package/src/extension/team-tool/dispatch/index.ts +1 -0
  52. package/src/extension/team-tool/dispatch/run.ts +4 -1
  53. package/src/extension/team-tool/doctor.ts +103 -1
  54. package/src/extension/team-tool/orchestrate.ts +66 -1
  55. package/src/extension/team-tool/plans.ts +192 -0
  56. package/src/extension/team-tool/respond.ts +197 -65
  57. package/src/extension/team-tool/run-deadline.ts +16 -1
  58. package/src/extension/team-tool/run-intent.ts +63 -0
  59. package/src/extension/team-tool/run.ts +48 -18
  60. package/src/extension/team-tool/status.ts +84 -26
  61. package/src/extension/team-tool.ts +11 -2
  62. package/src/hooks/registry.ts +1 -6
  63. package/src/i18n.ts +9 -0
  64. package/src/prompt/prompt-runtime.ts +521 -2
  65. package/src/prompt/worker-events-channel.ts +173 -0
  66. package/src/runtime/README.md +8 -8
  67. package/src/runtime/async-runner.ts +7 -3
  68. package/src/runtime/background-runner.ts +42 -14
  69. package/src/runtime/broker/broker-issuer.ts +9 -2
  70. package/src/runtime/broker/crew-broker-tokens.ts +43 -6
  71. package/src/runtime/broker/crew-broker.ts +838 -10
  72. package/src/runtime/broker/wait-status-cache.ts +157 -0
  73. package/src/runtime/budget-enforcement.ts +281 -0
  74. package/src/runtime/child-pi/child-pi-spawn.ts +60 -14
  75. package/src/runtime/child-pi/child-pi-timers.ts +324 -0
  76. package/src/runtime/child-pi/child-pi.ts +97 -201
  77. package/src/runtime/child-pi/mock-fixtures.ts +16 -2
  78. package/src/runtime/crew-agent-records.ts +259 -14
  79. package/src/runtime/delegate-spawn.ts +148 -0
  80. package/src/runtime/deterministic-ast.ts +2 -1
  81. package/src/runtime/dispatch-batch.ts +945 -0
  82. package/src/runtime/finalize-run.ts +557 -0
  83. package/src/runtime/goal-workflow/adaptive-plan.ts +87 -13
  84. package/src/runtime/goal-workflow/dynamic-workflow-runner.ts +3 -2
  85. package/src/runtime/goal-workflow/goal-state-store.ts +1 -1
  86. package/src/runtime/group-join.ts +11 -125
  87. package/src/runtime/live-session/live-session-runtime.ts +26 -1
  88. package/src/runtime/merge-gate.ts +7 -1
  89. package/src/runtime/merge-loop.ts +130 -0
  90. package/src/runtime/model/model-budget-summary.ts +53 -0
  91. package/src/runtime/model/model-fallback.ts +32 -2
  92. package/src/runtime/model/pi-args.ts +10 -0
  93. package/src/runtime/model/provider-extensions.ts +10 -0
  94. package/src/runtime/orphan-worker-registry.ts +1 -1
  95. package/src/runtime/output/output-validator.ts +45 -0
  96. package/src/runtime/parent-guard.ts +3 -1
  97. package/src/runtime/peer-dep.ts +2 -1
  98. package/src/runtime/per-write-validator.ts +0 -5
  99. package/src/runtime/pi-spawn.ts +61 -15
  100. package/src/runtime/plan-approval.ts +125 -0
  101. package/src/runtime/plan-replan.ts +151 -0
  102. package/src/runtime/recovery/checkpoint.ts +0 -18
  103. package/src/runtime/recovery/crash-recovery.ts +95 -46
  104. package/src/runtime/scheduler-context.ts +98 -0
  105. package/src/runtime/scheduling/coalesce-tasks.ts +5 -0
  106. package/src/runtime/scheduling/global-worker-cap.ts +2 -1
  107. package/src/runtime/scheduling/nested-slots.ts +70 -0
  108. package/src/runtime/scheduling/run-coalesced-task-group.ts +64 -13
  109. package/src/runtime/scheduling/task-graph-scheduler.ts +0 -10
  110. package/src/runtime/settings-store.ts +219 -0
  111. package/src/runtime/spawn-policy.ts +217 -0
  112. package/src/runtime/stale-reconciler.ts +87 -6
  113. package/src/runtime/subagent-manager.ts +25 -1
  114. package/src/runtime/task-output-context.ts +230 -9
  115. package/src/runtime/task-packet.ts +23 -1
  116. package/src/runtime/task-runner/child-executor.ts +106 -7
  117. package/src/runtime/task-runner/post-execution.ts +125 -1
  118. package/src/runtime/task-runner/pre-execution.ts +39 -1
  119. package/src/runtime/task-runner/prompt-builder.ts +51 -1
  120. package/src/runtime/task-runner/retrieval-orchestrator.ts +72 -18
  121. package/src/runtime/task-runner/spec-evidence.ts +403 -0
  122. package/src/runtime/task-runner/state-helpers.ts +26 -24
  123. package/src/runtime/task-runner.ts +11 -0
  124. package/src/runtime/team-runner.ts +129 -1671
  125. package/src/runtime/verification/spec-sandbox.ts +255 -0
  126. package/src/runtime/verification/verification-gates.ts +3 -2
  127. package/src/runtime/verification/verification-worktree.ts +2 -1
  128. package/src/runtime/workflow-phase-advance.ts +100 -0
  129. package/src/runtime/workspace-tree.ts +9 -0
  130. package/src/schema/config-schema.ts +65 -25
  131. package/src/schema/sensitive-config-paths.ts +64 -0
  132. package/src/schema/team-tool-schema.ts +13 -3
  133. package/src/state/README.md +4 -10
  134. package/src/state/atomic-write.ts +20 -3
  135. package/src/state/contracts.ts +38 -0
  136. package/src/state/coordination/mailbox.ts +12 -2
  137. package/src/state/event-log/cursor.ts +223 -0
  138. package/src/state/event-log/event-log-rotation.ts +12 -4
  139. package/src/state/event-log/event-log.ts +152 -369
  140. package/src/state/event-log/sequence-cache.ts +373 -0
  141. package/src/state/event-log/worker-atomic-writer.ts +2 -1
  142. package/src/state/stores/active-run-registry.ts +3 -2
  143. package/src/state/stores/manifest-io.ts +237 -0
  144. package/src/state/stores/ownership-map.ts +162 -0
  145. package/src/state/stores/plan-store.ts +241 -0
  146. package/src/state/stores/run-cache.ts +0 -90
  147. package/src/state/stores/spec-store.ts +189 -0
  148. package/src/state/stores/state-store.ts +66 -229
  149. package/src/state/types.ts +197 -0
  150. package/src/ui/dashboard-panes/plan-pane.ts +136 -0
  151. package/src/ui/dashboard-panes/progress-pane.ts +6 -0
  152. package/src/ui/dashboard-panes/transcript-pane.ts +31 -0
  153. package/src/ui/heartbeat-aggregator.ts +9 -1
  154. package/src/ui/keybinding-map.ts +54 -13
  155. package/src/ui/powerbar-publisher.ts +52 -1
  156. package/src/ui/run-dashboard.ts +31 -5
  157. package/src/ui/run-snapshot-cache.ts +57 -30
  158. package/src/ui/snapshot-types.ts +6 -1
  159. package/src/ui/widget/widget-renderer.ts +9 -1
  160. package/src/utils/file-coalescer.ts +0 -4
  161. package/src/utils/fs-errno.ts +66 -0
  162. package/src/utils/fs-watch.ts +1 -1
  163. package/src/utils/internal-error.ts +3 -1
  164. package/src/utils/paths.ts +11 -3
  165. package/src/utils/task-name-generator.ts +1 -8
  166. package/src/workflows/discover-workflows.ts +19 -2
  167. package/src/workflows/validate-workflow.ts +7 -1
  168. package/src/workflows/workflow-config.ts +10 -0
  169. package/src/workflows/workflow-serializer.ts +3 -0
  170. package/src/worktree/worktree-manager.ts +22 -0
  171. package/src/agents/agent-search.ts +0 -98
  172. package/src/benchmark/benchmark-runner.ts +0 -313
  173. package/src/benchmark/feedback-loop.ts +0 -73
  174. package/src/config/resilient-parser.ts +0 -117
  175. package/src/extension/crew-vibes/cat-frames.ts +0 -18
  176. package/src/extension/result-watcher.ts +0 -139
  177. package/src/observability/exporters/prometheus-exporter.ts +0 -54
  178. package/src/observability/metric-retention.ts +0 -64
  179. package/src/runtime/compaction/compaction-summary.ts +0 -278
  180. package/src/runtime/errors/crew-errors.ts +0 -162
  181. package/src/runtime/live-session/intercom-bridge.ts +0 -187
  182. package/src/runtime/loop-gates.ts +0 -128
  183. package/src/runtime/metric-parser.ts +0 -36
  184. package/src/runtime/output/stream-preview.ts +0 -184
  185. package/src/runtime/output/tool-progress.ts +0 -278
  186. package/src/runtime/phase-tracker.ts +0 -385
  187. package/src/runtime/pipeline-runner.ts +0 -523
  188. package/src/runtime/process/process-lifecycle.ts +0 -491
  189. package/src/runtime/recovery/retry-runner.ts +0 -330
  190. package/src/runtime/run-drift.ts +0 -219
  191. package/src/runtime/task-quality.ts +0 -199
  192. package/src/runtime/task-runner/run-projection.ts +0 -128
  193. package/src/runtime/verification/post-checks.ts +0 -142
  194. package/src/state/coordination/schedule.ts +0 -166
  195. package/src/state/event-log/jsonl-writer.ts +0 -115
  196. package/src/state/hook-instinct-bridge.ts +0 -94
  197. package/src/state/hook-integrations.ts +0 -51
  198. package/src/state/session-state-map.ts +0 -51
  199. package/src/state/stores/blob-store.ts +0 -308
  200. package/src/state/stores/instinct-store.ts +0 -275
  201. package/src/state/stores/observation-store.ts +0 -176
  202. package/src/state/tiered-eval.ts +0 -480
  203. package/src/state/types-eval.ts +0 -58
  204. package/src/tools/safe-bash-extension.ts +0 -54
  205. package/src/tools/safe-bash.ts +0 -505
  206. package/src/ui/agent-management-overlay.ts +0 -160
  207. package/src/ui/crew-footer.ts +0 -102
  208. package/src/ui/crew-select-list.ts +0 -114
  209. package/src/ui/dashboard-panes/capability-pane.ts +0 -77
  210. package/src/ui/transcript-entries.ts +0 -256
  211. package/src/utils/conflict-detect.ts +0 -721
  212. package/src/utils/fingerprint.ts +0 -180
  213. package/src/utils/gh-protocol.ts +0 -556
  214. package/src/utils/project-detector.ts +0 -160
  215. package/src/utils/sse-parser.ts +0 -131
  216. package/src/workflows/cost-estimator.ts +0 -34
  217. package/src/workflows/intermediate-store.ts +0 -166
@@ -0,0 +1,255 @@
1
+ /**
2
+ * spec-sandbox.ts — hardened re-run sandbox for strict-mode spec checks
3
+ * (ADR-6 §4, WP-6 step 4).
4
+ *
5
+ * Executes ONLY snapshot-frozen acceptance commands (never the live
6
+ * state/specs/<id>.json) under layered isolation:
7
+ * - env: BASE_ALLOWLIST pattern minus credential-carrying keys — NO
8
+ * provider API keys, NO PI_CREW_BROKER_*, NO session tokens (asserted by
9
+ * capturing the child env in tests);
10
+ * - cwd: pinned to the run's workspace root;
11
+ * - resources: `sh -c 'ulimit -v 262144; ulimit -t 30; exec …'`
12
+ * (256 MiB address space, 30 CPU-seconds);
13
+ * - wall-clock: 60s — SIGTERM → 200ms → SIGKILL escalation;
14
+ * - network: `unshare -rn` wrapper on Linux (user-namespace + map-root;
15
+ * plain -n needs CAP_SYS_ADMIN and fails EPERM unprivileged, which would
16
+ * brick every strict re-run).
17
+ *
18
+ * FAIL-CLOSED: wrapper-launch failure fails that acceptance closed — NEVER
19
+ * silently degrades to pass or to running without isolation. macOS has no
20
+ * unshare equivalent → launch-failed there too (platform honesty: the gate
21
+ * emits a loud platform warning; strict re-runs are Linux-only in v1).
22
+ *
23
+ * LEAK DISCIPLINE: results are DIGEST-ONLY. stdout/stderr text never leaves
24
+ * this module — events, logs, and artifacts see exit code / signal / digest /
25
+ * stderr-LENGTH only (error paths included: timeouts, signals, launch
26
+ * failures).
27
+ */
28
+
29
+ import { spawn } from "node:child_process";
30
+ import { createHash } from "node:crypto";
31
+ import { sanitizeEnvSecrets } from "../../utils/env-filter.ts";
32
+ import { BASE_ALLOWLIST } from "../child-pi/child-pi-spawn.ts";
33
+
34
+ /** Config-capped ceilings (ADR-6 §4) — NOT user-facing knobs in v1. The
35
+ * `limits` override on runSpecCheck is an internal test seam. */
36
+ export interface SpecSandboxLimits {
37
+ addressSpaceKb: number; // ulimit -v (256 MiB)
38
+ cpuSeconds: number; // ulimit -t
39
+ wallClockMs: number; // 60s hard wall clock
40
+ sigkillGraceMs: number; // SIGTERM → 200ms → SIGKILL
41
+ maxOutputBytes: number; // stdout buffer cap (digest still computed over capped buffer)
42
+ }
43
+
44
+ export const SPEC_SANDBOX_LIMITS: SpecSandboxLimits = {
45
+ addressSpaceKb: 262_144,
46
+ cpuSeconds: 30,
47
+ wallClockMs: 60_000,
48
+ sigkillGraceMs: 200,
49
+ maxOutputBytes: 4 * 1024 * 1024,
50
+ };
51
+
52
+ /** Credential-shaped keys are scrubbed EVEN IF an allowlist entry matches —
53
+ * belt-and-suspenders against future BASE_ALLOWLIST additions. */
54
+ const CREDENTIAL_KEY_PATTERN = /(API_?KEY|TOKEN|SECRET|PASSWORD|PASSWD|CREDENTIAL|_BEARER|^PI_CREW_BROKER)/i;
55
+
56
+ /** Sandbox env: BASE_ALLOWLIST pattern minus credential-carrying keys, then
57
+ * the generic secret scrubber (deny-list). The scrubbed map IS the result —
58
+ * never merged back into the selection (a merge-back resurrects keys the
59
+ * scrubber dropped). */
60
+ export function buildSpecSandboxEnv(env: NodeJS.ProcessEnv = process.env): Record<string, string> {
61
+ const selected: Record<string, string> = {};
62
+ for (const key of BASE_ALLOWLIST) {
63
+ if (CREDENTIAL_KEY_PATTERN.test(key)) continue;
64
+ const value = env[key];
65
+ if (value !== undefined) selected[key] = value;
66
+ }
67
+ const scrubbed = sanitizeEnvSecrets(selected);
68
+ const result: Record<string, string> = {};
69
+ for (const [key, value] of Object.entries(scrubbed)) {
70
+ if (CREDENTIAL_KEY_PATTERN.test(key)) continue;
71
+ result[key] = value;
72
+ }
73
+ result.FORCE_COLOR = "0";
74
+ return result;
75
+ }
76
+
77
+ export type SpecCheckOutcomeKind =
78
+ | "passed"
79
+ | "digest-mismatch"
80
+ | "exit-mismatch"
81
+ | "output-capped"
82
+ | "timeout"
83
+ | "launch-failed"
84
+ | "network-blocked";
85
+
86
+ /** Digest-only result — raw command output NEVER persists (leak discipline). */
87
+ export interface SpecCheckOutcome {
88
+ outcome: SpecCheckOutcomeKind;
89
+ actualDigest?: string;
90
+ exitCode?: number | null;
91
+ signal?: string;
92
+ durationMs: number;
93
+ stderrLength?: number;
94
+ /** stdout exceeded maxOutputBytes — digest comparison invalid (the authored
95
+ * expectedDigest is over FULL stdout); surfaced so the failure is
96
+ * distinguishable from real tampering (round-1 review). */
97
+ stdoutCapped?: boolean;
98
+ }
99
+
100
+ export interface SpecCheckCommand {
101
+ command: string;
102
+ /** sha-256 hex of raw stdout. Optional — exit-code-only checks omit it. */
103
+ expectedDigest?: string;
104
+ /** Default 0 when neither digest nor exit code is declared. */
105
+ expectedExitCode?: number;
106
+ }
107
+
108
+ /** `unshare -rn` is Linux-only; strict re-runs fail closed elsewhere. */
109
+ export function isSpecSandboxSupported(): boolean {
110
+ return process.platform === "linux";
111
+ }
112
+
113
+ export async function runSpecCheck(
114
+ check: SpecCheckCommand,
115
+ options: { cwd: string; limits?: Partial<SpecSandboxLimits>; wrapperOverride?: string },
116
+ ): Promise<SpecCheckOutcome> {
117
+ const limits = { ...SPEC_SANDBOX_LIMITS, ...options.limits };
118
+ const wrapper = options.wrapperOverride ?? "unshare";
119
+ const start = Date.now();
120
+ if (!isSpecSandboxSupported()) {
121
+ // Platform honesty: macOS/Windows have no unshare equivalent → fail
122
+ // closed, never silently run without isolation.
123
+ return { outcome: "launch-failed", durationMs: Date.now() - start, stderrLength: 0 };
124
+ }
125
+ // `exec` so ulimits apply to the command's own process. The user command
126
+ // rides as $0 (argv, never string-interpolated — arbitrary quoting is safe
127
+ // and compound commands like `a && b` keep their shell semantics).
128
+ const inner = `ulimit -v ${limits.addressSpaceKb}; ulimit -t ${limits.cpuSeconds}; exec sh -c "$0"`;
129
+ return await new Promise<SpecCheckOutcome>((resolve) => {
130
+ const child = spawn(wrapper, ["-rn", "sh", "-c", inner, check.command], {
131
+ cwd: options.cwd,
132
+ env: buildSpecSandboxEnv(),
133
+ stdio: ["ignore", "pipe", "pipe"],
134
+ detached: true, // own process group — group-wide kill reaches grandchildren holding the stdio pipes (CORE-10 pattern)
135
+ });
136
+ let stdout = "";
137
+ let stderrLen = 0;
138
+ let stdoutCapped = false;
139
+ let settled = false;
140
+ let termTimer: NodeJS.Timeout | undefined;
141
+ let killTimer: NodeJS.Timeout | undefined;
142
+ const finish = (outcome: SpecCheckOutcome): void => {
143
+ if (settled) return;
144
+ settled = true;
145
+ if (termTimer) clearTimeout(termTimer);
146
+ if (killTimer) clearTimeout(killTimer);
147
+ resolve(outcome);
148
+ };
149
+ termTimer = setTimeout(() => {
150
+ try {
151
+ if (child.pid) process.kill(-child.pid, "SIGTERM");
152
+ } catch {
153
+ child.kill("SIGTERM");
154
+ }
155
+ }, limits.wallClockMs);
156
+ termTimer.unref();
157
+ killTimer = setTimeout(() => {
158
+ try {
159
+ if (child.pid) process.kill(-child.pid, "SIGKILL");
160
+ } catch {
161
+ child.kill("SIGKILL");
162
+ }
163
+ }, limits.wallClockMs + limits.sigkillGraceMs);
164
+ killTimer.unref();
165
+ child.stdout?.on("data", (chunk: Buffer) => {
166
+ if (stdout.length >= limits.maxOutputBytes) {
167
+ stdoutCapped = true;
168
+ return;
169
+ }
170
+ stdout += chunk.toString("utf8");
171
+ if (stdout.length > limits.maxOutputBytes) {
172
+ stdout = stdout.slice(0, limits.maxOutputBytes);
173
+ stdoutCapped = true;
174
+ }
175
+ });
176
+ child.stderr?.on("data", (chunk: Buffer) => {
177
+ stderrLen += chunk.length;
178
+ });
179
+ child.on("error", (err) => {
180
+ // Wrapper-launch failure (unshare missing/EACCES/EPERM) — fail closed.
181
+ const outcome: SpecCheckOutcome =
182
+ (err as NodeJS.ErrnoException).code === "ENOENT"
183
+ ? { outcome: "launch-failed", durationMs: Date.now() - start, stderrLength: stderrLen }
184
+ : { outcome: "launch-failed", durationMs: Date.now() - start, stderrLength: stderrLen };
185
+ finish(outcome);
186
+ });
187
+ child.on("exit", () => {
188
+ // Survivor kill (round-1): a passing command can background a process
189
+ // that outlives the check. Must run on EXIT, not CLOSE — a survivor
190
+ // holding the stdout pipe delays `close` until it dies, which would
191
+ // make this kill a no-op by construction.
192
+ try {
193
+ if (child.pid) process.kill(-child.pid, "SIGKILL");
194
+ } catch {
195
+ /* group already reaped — fine */
196
+ }
197
+ });
198
+ child.on("close", (code, signal) => {
199
+ const durationMs = Date.now() - start;
200
+ if (signal === "SIGTERM" || signal === "SIGKILL") {
201
+ finish({ outcome: "timeout", signal: signal ?? undefined, durationMs, stderrLength: stderrLen });
202
+ return;
203
+ }
204
+ const actualDigest = createHash("sha256").update(stdout, "utf8").digest("hex");
205
+ const expectedExit = check.expectedExitCode ?? 0;
206
+ if (stdoutCapped && check.expectedDigest !== undefined) {
207
+ // The authored expectedDigest is over FULL stdout — comparing the
208
+ // capped prefix is invalid. Fail with the distinct honest outcome.
209
+ finish({
210
+ outcome: "output-capped",
211
+ actualDigest,
212
+ exitCode: code,
213
+ signal: signal ?? undefined,
214
+ durationMs,
215
+ stderrLength: stderrLen,
216
+ stdoutCapped: true,
217
+ });
218
+ return;
219
+ }
220
+ const exitOk = code === expectedExit;
221
+ const digestOk = check.expectedDigest === undefined || actualDigest === check.expectedDigest;
222
+ if (!exitOk) {
223
+ finish({
224
+ outcome: "exit-mismatch",
225
+ actualDigest: check.expectedDigest !== undefined ? actualDigest : undefined,
226
+ exitCode: code,
227
+ signal: signal ?? undefined,
228
+ durationMs,
229
+ stderrLength: stderrLen,
230
+ });
231
+ return;
232
+ }
233
+ if (!digestOk) {
234
+ finish({
235
+ outcome: "digest-mismatch",
236
+ actualDigest,
237
+ exitCode: code,
238
+ signal: signal ?? undefined,
239
+ durationMs,
240
+ stderrLength: stderrLen,
241
+ });
242
+ return;
243
+ }
244
+ finish({
245
+ outcome: "passed",
246
+ actualDigest: check.expectedDigest !== undefined || stdoutCapped ? actualDigest : undefined,
247
+ exitCode: code,
248
+ signal: signal ?? undefined,
249
+ durationMs,
250
+ stderrLength: stderrLen,
251
+ ...(stdoutCapped ? { stdoutCapped: true } : {}),
252
+ });
253
+ });
254
+ });
255
+ }
@@ -12,6 +12,7 @@
12
12
  import { spawn } from "node:child_process";
13
13
  import * as fs from "node:fs";
14
14
  import * as path from "node:path";
15
+ import { getCrewEnv } from "../../config/env-vars.ts";
15
16
  import { writeArtifact } from "../../state/stores/artifact-store.ts";
16
17
  import type { GreenLevel, VerificationCommandResult, VerificationContract } from "../../state/types.ts";
17
18
  import { WINDOWS_ESSENTIAL_ENV_VARS } from "../../utils/env-allowlist.ts";
@@ -66,7 +67,7 @@ const VERIFICATION_ENV_ALLOWLIST: readonly string[] = [
66
67
  /** Whether env sanitization for verification is enabled (opt-out: enabled by default). */
67
68
  export function isVerificationEnvSanitizeEnabled(): boolean {
68
69
  // Opt-out: explicitly disabled via env var set to '0'.
69
- if (process.env.PI_CREW_VERIFICATION_SANITIZE_ENV === "0" || process.env.PI_TEAMS_VERIFICATION_SANITIZE_ENV === "0") {
70
+ if (getCrewEnv("PI_CREW_VERIFICATION_SANITIZE_ENV") === "0" || getCrewEnv("PI_TEAMS_VERIFICATION_SANITIZE_ENV") === "0") {
70
71
  return false;
71
72
  }
72
73
  return true;
@@ -82,7 +83,7 @@ function buildVerificationEnv(): Record<string, string> {
82
83
  if (!isVerificationEnvSanitizeEnabled()) {
83
84
  return { ...process.env, FORCE_COLOR: "0" };
84
85
  }
85
- const preserveRaw = process.env.PI_CREW_VERIFICATION_PRESERVE_ENV ?? process.env.PI_TEAMS_VERIFICATION_PRESERVE_ENV ?? "";
86
+ const preserveRaw = getCrewEnv("PI_CREW_VERIFICATION_PRESERVE_ENV") ?? "";
86
87
  const preserve = preserveRaw
87
88
  .split(",")
88
89
  .map((s) => s.trim())
@@ -25,6 +25,7 @@ import { execFileSync } from "node:child_process";
25
25
  import * as fs from "node:fs";
26
26
  import * as os from "node:os";
27
27
  import * as path from "node:path";
28
+ import { getCrewEnv } from "../../config/env-vars.ts";
28
29
 
29
30
  export interface VerificationWorktree {
30
31
  /** Absolute path to the pristine worktree directory. */
@@ -37,7 +38,7 @@ export interface VerificationWorktree {
37
38
 
38
39
  /** Whether the worktree sandbox is enabled (env var opt-in). */
39
40
  export function isWorktreeSandboxEnabled(): boolean {
40
- const v = process.env.PI_CREW_VERIFICATION_WORKTREE ?? process.env.PI_TEAMS_VERIFICATION_WORKTREE;
41
+ const v = getCrewEnv("PI_CREW_VERIFICATION_WORKTREE");
41
42
  return v === "1" || v === "true";
42
43
  }
43
44
 
@@ -0,0 +1,100 @@
1
+ /**
2
+ * Workflow-phase advancement for the team-run scheduler loop.
3
+ *
4
+ * Extracted from team-runner.ts (2026-08, Phase 2.6 maintainability split —
5
+ * CORE-4 extraction 6). Pure code motion: advanceWorkflowPhases moved
6
+ * verbatim.
7
+ */
8
+ import { appendEventAsync } from "../state/event-log/event-log.ts";
9
+ import type { SchedulerContext } from "./scheduler-context.ts";
10
+ import { type PhaseGuardContext, transitionPhase } from "./workflow-state.ts";
11
+
12
+ /**
13
+ * CORE-4 extraction 6: advance workflow phases whose tasks are all in
14
+ * terminal state.
15
+ *
16
+ * Iterates phases starting at `ctx.wfMachine.currentPhaseIndex`; for each phase
17
+ * whose tasks are all terminal, determines the transition status (failed if
18
+ * any task failed/cancelled, else completed), applies the phase transition,
19
+ * emits `workflow.phase_completed`/`workflow.phase_failed`/
20
+ * `workflow.phase_guard_blocked` events, and advances `currentPhaseIndex`.
21
+ *
22
+ * Reads `ctx.tasks`, `ctx.manifest` (read-only). Mutates `ctx.wfMachine`
23
+ * in-place (phase status + currentPhaseIndex). The caller syncs the local
24
+ * `wfMachine` from ctx after the call.
25
+ *
26
+ * @param ctx The scheduler context.
27
+ */
28
+ export async function advanceWorkflowPhases(ctx: SchedulerContext): Promise<void> {
29
+ let wfMachine = ctx.wfMachine;
30
+ const tasks = ctx.tasks;
31
+ const manifest = ctx.manifest;
32
+ // Advance workflow phases whose tasks are all in terminal state
33
+ const terminalStatuses = new Set(["completed", "failed", "skipped", "cancelled", "needs_attention"]);
34
+ const phaseTaskMap = new Map<string, string[]>();
35
+ for (const task of tasks) {
36
+ if (!task.stepId) continue;
37
+ const existing = phaseTaskMap.get(task.stepId) ?? [];
38
+ existing.push(task.id);
39
+ phaseTaskMap.set(task.stepId, existing);
40
+ }
41
+ // W5-4: by-id map once for the phase loop (was O(phases × phaseTasks × tasks)).
42
+ const taskById = new Map(tasks.map((t) => [t.id, t] as const));
43
+ for (let pi = wfMachine.currentPhaseIndex; pi < wfMachine.phases.length; pi++) {
44
+ const phase = wfMachine.phases[pi]!;
45
+ const phaseTaskIds = phaseTaskMap.get(phase.name) ?? [];
46
+ if (phaseTaskIds.length === 0) continue;
47
+ const allTerminal = phaseTaskIds.every((taskId) => {
48
+ const task = taskById.get(taskId);
49
+ return task ? terminalStatuses.has(task.status) : false;
50
+ });
51
+ if (!allTerminal) break;
52
+ if (phase.status !== "completed" && phase.status !== "failed" && phase.status !== "skipped") {
53
+ const completedArtifacts = manifest.artifacts.filter((a) => a.kind === "result" || a.kind === "summary").map((a) => a.path);
54
+ const previousPhaseStatus = pi > 0 ? (wfMachine.phases[pi - 1]?.status ?? "pending") : "completed";
55
+ const wfContext: PhaseGuardContext = {
56
+ completedArtifacts,
57
+ previousPhaseStatus,
58
+ taskResults: tasks
59
+ .filter((t) => t.status === "completed" || t.status === "needs_attention")
60
+ .map((t) => ({
61
+ taskId: t.id,
62
+ status: t.status,
63
+ outputPath: t.resultArtifact?.path,
64
+ })),
65
+ };
66
+ // Determine phase transition status based on individual task outcomes
67
+ const phaseTasks = phaseTaskIds
68
+ .map((taskId) => taskById.get(taskId))
69
+ .filter((t): t is NonNullable<typeof t> => t !== undefined);
70
+ const hasFailedOrCancelled = phaseTasks.some((t) => t.status === "failed" || t.status === "cancelled");
71
+ const phaseStatus = hasFailedOrCancelled ? "failed" : "completed";
72
+ const transition = transitionPhase(wfMachine, pi, phaseStatus, wfContext);
73
+ wfMachine = transition.machine;
74
+ if (transition.guardResult && !transition.guardResult.allowed) {
75
+ await appendEventAsync(manifest.eventsPath, {
76
+ type: "workflow.phase_guard_blocked",
77
+ runId: manifest.runId,
78
+ message: `Workflow phase '${phase.name}' guard blocked: ${transition.guardResult.reason ?? "unknown"}`,
79
+ data: {
80
+ phaseIndex: pi,
81
+ phaseName: phase.name,
82
+ reason: transition.guardResult.reason,
83
+ },
84
+ });
85
+ break;
86
+ }
87
+ await appendEventAsync(manifest.eventsPath, {
88
+ type: phaseStatus === "failed" ? "workflow.phase_failed" : "workflow.phase_completed",
89
+ runId: manifest.runId,
90
+ message: `Workflow phase '${phase.name}' ${phaseStatus}.`,
91
+ data: { phaseIndex: pi, phaseStatus },
92
+ });
93
+ }
94
+ wfMachine = { ...wfMachine, currentPhaseIndex: pi + 1 };
95
+ }
96
+ ctx.wfMachine = wfMachine;
97
+ }
98
+
99
+ /** @internal 1.9(b) test export — exercise advanceWorkflowPhases directly. */
100
+ export const __test__advanceWorkflowPhases = advanceWorkflowPhases;
@@ -262,6 +262,11 @@ const emptyResult = (rootPath: string): WorkspaceTree => ({
262
262
  * long active runs while eliminating redundant walks.
263
263
  */
264
264
  const TREE_CACHE_TTL_MS = 30_000;
265
+ // R5-L3 (Round 5 LOW-3): FIFO cap at insertion (mirrors knowledgeCache in
266
+ // knowledge-injection.ts). Expired entries are only overwritten on re-access,
267
+ // so distinct cwd/option keys would otherwise accumulate for the process
268
+ // lifetime. A miss just re-walks the tree — same as a TTL expiry.
269
+ const TREE_CACHE_MAX_ENTRIES = 64;
265
270
  interface CachedTree {
266
271
  tree: WorkspaceTree;
267
272
  expiresAt: number;
@@ -305,6 +310,10 @@ export async function buildWorkspaceTree(cwd: string, options?: WorkspaceTreeOpt
305
310
  tree: result,
306
311
  expiresAt: Date.now() + TREE_CACHE_TTL_MS,
307
312
  });
313
+ if (treeCache.size > TREE_CACHE_MAX_ENTRIES) {
314
+ const oldest = treeCache.keys().next().value;
315
+ if (oldest !== undefined) treeCache.delete(oldest);
316
+ }
308
317
  return result;
309
318
  } catch {
310
319
  return emptyResult(rootPath);
@@ -17,12 +17,21 @@ export const PiTeamsAutonomyProfileSchema = Type.Union([
17
17
 
18
18
  export const PiTeamsAutonomousConfigSchema = Type.Object(
19
19
  {
20
- profile: Type.Optional(PiTeamsAutonomyProfileSchema),
21
- enabled: Type.Optional(Type.Boolean()),
22
- injectPolicy: Type.Optional(Type.Boolean()),
23
- preferAsyncForLongTasks: Type.Optional(Type.Boolean()),
24
- allowWorktreeSuggestion: Type.Optional(Type.Boolean()),
25
- magicKeywords: Type.Optional(Type.Record(Type.String({ minLength: 1 }), Type.Array(Type.String({ minLength: 1 })))),
20
+ profile: Type.Optional(
21
+ Type.Union([Type.Literal("manual"), Type.Literal("suggested"), Type.Literal("assisted"), Type.Literal("aggressive")], {
22
+ sensitive: true,
23
+ }),
24
+ ),
25
+ enabled: Type.Optional(Type.Boolean({ sensitive: true })),
26
+ injectPolicy: Type.Optional(Type.Boolean({ sensitive: true })),
27
+ preferAsyncForLongTasks: Type.Optional(Type.Boolean({ sensitive: true })),
28
+ allowWorktreeSuggestion: Type.Optional(Type.Boolean({ sensitive: true })),
29
+ // S19-5 (Wave 1A): magicKeywords alone flips effective autonomous mode on
30
+ // (effectiveAutonomousConfig defaults profile "suggested" when only
31
+ // magicKeywords is present) — untrusted project config must not set it.
32
+ magicKeywords: Type.Optional(
33
+ Type.Record(Type.String({ minLength: 1 }), Type.Array(Type.String({ minLength: 1 })), { sensitive: true }),
34
+ ),
26
35
  },
27
36
  { additionalProperties: false },
28
37
  );
@@ -56,14 +65,16 @@ export const PiTeamsModelFallbackConfigSchema = Type.Object(
56
65
  export const PiTeamsRuntimeConfigSchema = Type.Object(
57
66
  {
58
67
  mode: Type.Optional(
59
- Type.Union([Type.Literal("auto"), Type.Literal("scaffold"), Type.Literal("child-process"), Type.Literal("live-session")]),
68
+ Type.Union([Type.Literal("auto"), Type.Literal("scaffold"), Type.Literal("child-process"), Type.Literal("live-session")], {
69
+ sensitive: true,
70
+ }),
60
71
  ),
61
- preferLiveSession: Type.Optional(Type.Boolean()),
62
- allowChildProcessFallback: Type.Optional(Type.Boolean()),
72
+ preferLiveSession: Type.Optional(Type.Boolean({ sensitive: true })),
73
+ allowChildProcessFallback: Type.Optional(Type.Boolean({ sensitive: true })),
63
74
  maxTurns: Type.Optional(Type.Integer({ minimum: 1 })),
64
75
  graceTurns: Type.Optional(Type.Integer({ minimum: 1 })),
65
76
  taskTimeoutMs: Type.Optional(Type.Integer({ minimum: 1 })),
66
- inheritContext: Type.Optional(Type.Boolean()),
77
+ inheritContext: Type.Optional(Type.Boolean({ sensitive: true })),
67
78
  promptMode: Type.Optional(Type.Union([Type.Literal("replace"), Type.Literal("append")])),
68
79
  groupJoin: Type.Optional(Type.Union([Type.Literal("off"), Type.Literal("group"), Type.Literal("smart")])),
69
80
  groupJoinAckTimeoutMs: Type.Optional(Type.Integer({ minimum: 1 })),
@@ -83,14 +94,14 @@ export const PiTeamsRuntimeConfigSchema = Type.Object(
83
94
  ),
84
95
  ),
85
96
  excludeContextBash: Type.Optional(Type.Boolean()),
86
- agentExtensions: Type.Optional(Type.Array(Type.String({ minLength: 1 }))),
97
+ agentExtensions: Type.Optional(Type.Array(Type.String({ minLength: 1 }), { sensitive: true })),
87
98
  isolationPolicy: Type.Optional(
88
99
  Type.Object(
89
100
  {
90
101
  isolatedRoles: Type.Optional(Type.Array(Type.String({ minLength: 1 }))),
91
102
  defaultRuntime: Type.Optional(Type.Union([Type.Literal("live-session"), Type.Literal("child-process")])),
92
103
  },
93
- { additionalProperties: false },
104
+ { additionalProperties: false, sensitive: true },
94
105
  ),
95
106
  ),
96
107
  modelFallback: Type.Optional(PiTeamsModelFallbackConfigSchema),
@@ -102,16 +113,19 @@ export const PiTeamsControlConfigSchema = Type.Object(
102
113
  {
103
114
  enabled: Type.Optional(Type.Boolean()),
104
115
  needsAttentionAfterMs: Type.Optional(Type.Integer({ minimum: 1 })),
116
+ // F19-3 (Round 19 parity): read at agent-control.ts with defaults 3/10.
117
+ consecutiveFailureThreshold: Type.Optional(Type.Integer({ minimum: 1 })),
118
+ longRunningMinutes: Type.Optional(Type.Integer({ minimum: 1 })),
105
119
  },
106
120
  { additionalProperties: false },
107
121
  );
108
122
 
109
123
  export const PiTeamsWorktreeConfigSchema = Type.Object(
110
124
  {
111
- setupHook: Type.Optional(Type.String({ minLength: 1 })),
125
+ setupHook: Type.Optional(Type.String({ minLength: 1, sensitive: true })),
112
126
  setupHookTimeoutMs: Type.Optional(Type.Integer({ minimum: 1 })),
113
127
  linkNodeModules: Type.Optional(Type.Boolean()),
114
- seedPaths: Type.Optional(Type.Array(Type.String({ minLength: 1 }))),
128
+ seedPaths: Type.Optional(Type.Array(Type.String({ minLength: 1 }), { sensitive: true })),
115
129
  },
116
130
  { additionalProperties: false },
117
131
  );
@@ -144,7 +158,11 @@ export const GoalWrapWorkflowConfigSchema = Type.Object(
144
158
  { additionalProperties: false },
145
159
  );
146
160
 
147
- export const PiTeamsGoalWrapConfigSchema = Type.Record(Type.String({ minLength: 1 }), GoalWrapWorkflowConfigSchema);
161
+ // S19-2 (Wave 1A): the whole goalWrap subtree is user-only — a project config
162
+ // must not enable silent auto-wrap, pick the evaluator model, or set
163
+ // budgetUnlimited (unbounded provider spend). Marking the Record is terminal:
164
+ // the walk collapses the subtree to the single top-level path `goalWrap`.
165
+ export const PiTeamsGoalWrapConfigSchema = Type.Record(Type.String({ minLength: 1 }), GoalWrapWorkflowConfigSchema, { sensitive: true });
148
166
 
149
167
  export const AgentOverrideSchema = Type.Object(
150
168
  {
@@ -160,8 +178,8 @@ export const AgentOverrideSchema = Type.Object(
160
178
 
161
179
  export const PiTeamsAgentsConfigSchema = Type.Object(
162
180
  {
163
- disableBuiltins: Type.Optional(Type.Boolean()),
164
- overrides: Type.Optional(Type.Record(Type.String({ minLength: 1 }), AgentOverrideSchema)),
181
+ disableBuiltins: Type.Optional(Type.Boolean({ sensitive: true })),
182
+ overrides: Type.Optional(Type.Record(Type.String({ minLength: 1 }), AgentOverrideSchema, { sensitive: true })),
165
183
  },
166
184
  { additionalProperties: false },
167
185
  );
@@ -169,8 +187,8 @@ export const PiTeamsAgentsConfigSchema = Type.Object(
169
187
  export const PiTeamsToolsConfigSchema = Type.Object(
170
188
  {
171
189
  enableClaudeStyleAliases: Type.Optional(Type.Boolean()),
172
- enableSteer: Type.Optional(Type.Boolean()),
173
- terminateOnForeground: Type.Optional(Type.Boolean()),
190
+ enableSteer: Type.Optional(Type.Boolean({ sensitive: true })),
191
+ terminateOnForeground: Type.Optional(Type.Boolean({ sensitive: true })),
174
192
  },
175
193
  { additionalProperties: false },
176
194
  );
@@ -184,8 +202,8 @@ export const PiTeamsTelemetryConfigSchema = Type.Object(
184
202
 
185
203
  export const PiTeamsPolicyConfigSchema = Type.Object(
186
204
  {
187
- requireIntentForDestructiveActions: Type.Optional(Type.Boolean()),
188
- disabledCapabilities: Type.Optional(Type.Array(Type.String())),
205
+ requireIntentForDestructiveActions: Type.Optional(Type.Boolean({ sensitive: true })),
206
+ disabledCapabilities: Type.Optional(Type.Array(Type.String(), { sensitive: true })),
189
207
  },
190
208
  { additionalProperties: false },
191
209
  );
@@ -224,6 +242,9 @@ export const PiTeamsReliabilityConfigSchema = Type.Object(
224
242
  jitterRatio: Type.Optional(Type.Number({ minimum: 0, maximum: 1 })),
225
243
  exponentialFactor: Type.Optional(Type.Number({ minimum: 1, maximum: 5 })),
226
244
  retryableErrors: Type.Optional(Type.Array(Type.String({ minLength: 1 }))),
245
+ // F19-2 (Round 19 parity): parsed at config-validation.ts but
246
+ // additionalProperties:false previously rejected it at the schema level.
247
+ maxTotalSpawns: Type.Optional(Type.Integer({ minimum: 0 })),
227
248
  },
228
249
  { additionalProperties: false },
229
250
  ),
@@ -248,9 +269,12 @@ export const PiTeamsOtlpConfigSchema = Type.Object(
248
269
  minLength: 1,
249
270
  maxLength: 2048,
250
271
  pattern: "^https?://",
272
+ sensitive: true,
251
273
  }),
252
274
  ),
253
- headers: Type.Optional(Type.Record(Type.String({ minLength: 1, maxLength: 256 }), Type.String({ maxLength: 4096 }))),
275
+ headers: Type.Optional(
276
+ Type.Record(Type.String({ minLength: 1, maxLength: 256 }), Type.String({ maxLength: 4096 }), { sensitive: true }),
277
+ ),
254
278
  intervalMs: Type.Optional(Type.Integer({ minimum: 5000 })),
255
279
  },
256
280
  { additionalProperties: false },
@@ -298,16 +322,31 @@ export const CrewBrokerConfigSchema = Type.Object(
298
322
  pathHashLen: Type.Optional(Type.Integer({ minimum: 4, maximum: 32 })),
299
323
  maxFrameBytes: Type.Optional(Type.Integer({ minimum: 1024, maximum: 1_048_576 })),
300
324
  outboundQueueCap: Type.Optional(Type.Integer({ minimum: 32, maximum: 4096 })),
325
+ /** WP-2/R2 (ADR-0 2026-08-17-waiting-producer-ask item 7): gate for the
326
+ * broker wait.* methods. Default false — fail-closed. */
327
+ waitMethodsEnabled: Type.Optional(Type.Boolean()),
328
+ },
329
+ { additionalProperties: false },
330
+ );
331
+
332
+ export const PiTeamsNestingConfigSchema = Type.Object(
333
+ {
334
+ // Privilege-raising flag (spawns grandchildren): project-level config must
335
+ // not be able to enable it — sensitive → user config only (ADR-5 §12 posture,
336
+ // same treatment as autonomous.enabled / broker gates).
337
+ enabled: Type.Optional(Type.Boolean({ sensitive: true })),
338
+ maxSlots: Type.Optional(Type.Integer({ minimum: 1, maximum: 64 })),
339
+ maxDepth: Type.Optional(Type.Integer({ minimum: 1, maximum: 10 })),
301
340
  },
302
341
  { additionalProperties: false },
303
342
  );
304
343
 
305
344
  export const PiTeamsConfigSchema = Type.Object(
306
345
  {
307
- asyncByDefault: Type.Optional(Type.Boolean()),
308
- executeWorkers: Type.Optional(Type.Boolean()),
346
+ asyncByDefault: Type.Optional(Type.Boolean({ sensitive: true })),
347
+ executeWorkers: Type.Optional(Type.Boolean({ sensitive: true })),
309
348
  notifierIntervalMs: Type.Optional(Type.Number({ minimum: 1000 })),
310
- requireCleanWorktreeLeader: Type.Optional(Type.Boolean()),
349
+ requireCleanWorktreeLeader: Type.Optional(Type.Boolean({ sensitive: true })),
311
350
  ignoreMethod: Type.Optional(Type.Union([Type.Literal("gitignore"), Type.Literal("exclude")])),
312
351
  autonomous: Type.Optional(PiTeamsAutonomousConfigSchema),
313
352
  limits: Type.Optional(PiTeamsLimitsConfigSchema),
@@ -325,6 +364,7 @@ export const PiTeamsConfigSchema = Type.Object(
325
364
  otlp: Type.Optional(PiTeamsOtlpConfigSchema),
326
365
  ui: Type.Optional(PiTeamsUiConfigSchema),
327
366
  broker: Type.Optional(CrewBrokerConfigSchema),
367
+ nesting: Type.Optional(PiTeamsNestingConfigSchema),
328
368
  },
329
369
  { additionalProperties: false },
330
370
  );