pi-crew 0.9.68 → 0.10.2

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 (253) hide show
  1. package/CHANGELOG.md +222 -0
  2. package/NOTICE.md +21 -0
  3. package/README.md +44 -2
  4. package/agents/analyst.md +1 -1
  5. package/agents/cold-verifier.md +3 -1
  6. package/agents/critic.md +1 -1
  7. package/agents/executor.md +1 -1
  8. package/agents/explorer.md +1 -1
  9. package/agents/planner.md +1 -1
  10. package/agents/reviewer.md +1 -1
  11. package/agents/security-reviewer.md +1 -1
  12. package/agents/test-engineer.md +1 -1
  13. package/agents/verifier.md +1 -1
  14. package/agents/writer.md +1 -1
  15. package/dist/index.mjs +68113 -60774
  16. package/docs/README.md +2 -0
  17. package/docs/actions-reference.md +31 -0
  18. package/docs/commands-reference.md +17 -6
  19. package/docs/resource-formats.md +13 -0
  20. package/package.json +4 -2
  21. package/schema.json +503 -91
  22. package/scripts/resource-sampler.mjs +36 -2
  23. package/skills/requirements-to-task-packet/SKILL.md +26 -0
  24. package/skills/widget-rendering/SKILL.md +7 -7
  25. package/src/agents/agent-config.ts +2 -1
  26. package/src/agents/discover-agents.ts +23 -14
  27. package/src/config/config-merge.ts +183 -0
  28. package/src/config/config-validation.ts +687 -0
  29. package/src/config/config.ts +22 -864
  30. package/src/config/defaults.ts +43 -2
  31. package/src/config/drift-detector.ts +1 -1
  32. package/src/config/env-vars.ts +691 -0
  33. package/src/config/role-tools.ts +11 -9
  34. package/src/config/sanitize-project-config.ts +172 -0
  35. package/src/config/types.ts +49 -1
  36. package/src/extension/async-notifier.ts +25 -2
  37. package/src/extension/crew-cleanup.ts +13 -0
  38. package/src/extension/crew-vibes/config.ts +2 -1
  39. package/src/extension/crew-vibes/footer.ts +19 -0
  40. package/src/extension/crew-vibes/index.ts +11 -1
  41. package/src/extension/plan-orchestrate.ts +132 -0
  42. package/src/extension/register.ts +8 -0
  43. package/src/extension/registration/command-registration.ts +1 -0
  44. package/src/extension/registration/commands/dashboard.ts +158 -0
  45. package/src/extension/registration/commands/index.ts +35 -0
  46. package/src/extension/registration/commands/manage.ts +303 -0
  47. package/src/extension/registration/commands/run.ts +228 -0
  48. package/src/extension/registration/commands/shared.ts +639 -0
  49. package/src/extension/registration/commands/status.ts +60 -0
  50. package/src/extension/registration/commands.ts +13 -1224
  51. package/src/extension/registration/foreground-run-controller.ts +10 -2
  52. package/src/extension/registration/lifecycle-handlers.ts +178 -17
  53. package/src/extension/registration/runtime-cleanup.ts +23 -5
  54. package/src/extension/registration/subagent-tools.ts +218 -9
  55. package/src/extension/registration/team-tool.ts +5 -1
  56. package/src/extension/registration/ui.ts +5 -4
  57. package/src/extension/rpc-hmac.ts +5 -3
  58. package/src/extension/team-tool/api/heartbeat.ts +47 -10
  59. package/src/extension/team-tool/api/plan-approval.ts +9 -0
  60. package/src/extension/team-tool/api/task-claims.ts +109 -40
  61. package/src/extension/team-tool/cancel.ts +84 -50
  62. package/src/extension/team-tool/dispatch/index.ts +1 -0
  63. package/src/extension/team-tool/dispatch/run.ts +4 -1
  64. package/src/extension/team-tool/doctor.ts +103 -1
  65. package/src/extension/team-tool/orchestrate.ts +66 -1
  66. package/src/extension/team-tool/plans.ts +192 -0
  67. package/src/extension/team-tool/respond.ts +197 -65
  68. package/src/extension/team-tool/run-deadline.ts +35 -3
  69. package/src/extension/team-tool/run-intent.ts +63 -0
  70. package/src/extension/team-tool/run.ts +74 -20
  71. package/src/extension/team-tool/status.ts +84 -26
  72. package/src/extension/team-tool.ts +11 -2
  73. package/src/hooks/registry.ts +1 -6
  74. package/src/i18n.ts +9 -0
  75. package/src/prompt/prompt-runtime.ts +521 -2
  76. package/src/prompt/worker-events-channel.ts +173 -0
  77. package/src/runtime/README.md +8 -8
  78. package/src/runtime/async-runner.ts +7 -3
  79. package/src/runtime/background-runner.ts +42 -14
  80. package/src/runtime/broker/broker-issuer.ts +9 -2
  81. package/src/runtime/broker/crew-broker-tokens.ts +43 -6
  82. package/src/runtime/broker/crew-broker.ts +838 -10
  83. package/src/runtime/broker/wait-status-cache.ts +157 -0
  84. package/src/runtime/budget-enforcement.ts +281 -0
  85. package/src/runtime/child-pi/child-pi-constants.ts +8 -0
  86. package/src/runtime/child-pi/child-pi-spawn.ts +60 -14
  87. package/src/runtime/child-pi/child-pi-streams.ts +21 -1
  88. package/src/runtime/child-pi/child-pi-timers.ts +324 -0
  89. package/src/runtime/child-pi/child-pi.ts +97 -201
  90. package/src/runtime/child-pi/mock-fixtures.ts +16 -2
  91. package/src/runtime/crew-agent-records.ts +259 -14
  92. package/src/runtime/delegate-spawn.ts +148 -0
  93. package/src/runtime/detached-run-results.ts +90 -0
  94. package/src/runtime/deterministic-ast.ts +2 -1
  95. package/src/runtime/dispatch-batch.ts +945 -0
  96. package/src/runtime/finalize-run.ts +557 -0
  97. package/src/runtime/goal-workflow/adaptive-plan.ts +116 -15
  98. package/src/runtime/goal-workflow/dynamic-workflow-runner.ts +8 -3
  99. package/src/runtime/goal-workflow/goal-state-store.ts +1 -1
  100. package/src/runtime/group-join.ts +11 -125
  101. package/src/runtime/live-session/live-session-runtime.ts +26 -1
  102. package/src/runtime/merge-gate.ts +32 -10
  103. package/src/runtime/merge-loop.ts +130 -0
  104. package/src/runtime/model/model-budget-summary.ts +53 -0
  105. package/src/runtime/model/model-fallback.ts +36 -2
  106. package/src/runtime/model/pi-args.ts +10 -0
  107. package/src/runtime/model/provider-extensions.ts +10 -0
  108. package/src/runtime/orphan-worker-registry.ts +1 -1
  109. package/src/runtime/output/output-validator.ts +45 -0
  110. package/src/runtime/parent-guard.ts +3 -1
  111. package/src/runtime/peer-dep.ts +2 -1
  112. package/src/runtime/per-write-validator.ts +0 -5
  113. package/src/runtime/pi-spawn.ts +61 -15
  114. package/src/runtime/plan-approval.ts +125 -0
  115. package/src/runtime/plan-replan.ts +151 -0
  116. package/src/runtime/process-status.ts +16 -1
  117. package/src/runtime/recovery/checkpoint.ts +0 -18
  118. package/src/runtime/recovery/crash-recovery.ts +111 -46
  119. package/src/runtime/run-tracker.ts +77 -10
  120. package/src/runtime/scheduler-context.ts +98 -0
  121. package/src/runtime/scheduling/coalesce-tasks.ts +5 -0
  122. package/src/runtime/scheduling/global-worker-cap.ts +2 -1
  123. package/src/runtime/scheduling/nested-slots.ts +70 -0
  124. package/src/runtime/scheduling/run-coalesced-task-group.ts +64 -13
  125. package/src/runtime/scheduling/task-graph-scheduler.ts +0 -10
  126. package/src/runtime/settings-store.ts +219 -0
  127. package/src/runtime/spawn-policy.ts +217 -0
  128. package/src/runtime/stale-reconciler.ts +87 -6
  129. package/src/runtime/subagent-manager.ts +25 -1
  130. package/src/runtime/task-output-context.ts +230 -9
  131. package/src/runtime/task-packet.ts +23 -1
  132. package/src/runtime/task-runner/child-executor.ts +106 -7
  133. package/src/runtime/task-runner/post-execution.ts +125 -1
  134. package/src/runtime/task-runner/pre-execution.ts +39 -1
  135. package/src/runtime/task-runner/prompt-builder.ts +51 -1
  136. package/src/runtime/task-runner/retrieval-orchestrator.ts +72 -18
  137. package/src/runtime/task-runner/spec-evidence.ts +403 -0
  138. package/src/runtime/task-runner/state-helpers.ts +26 -24
  139. package/src/runtime/task-runner.ts +11 -0
  140. package/src/runtime/team-runner.ts +132 -1673
  141. package/src/runtime/verification/spec-sandbox.ts +255 -0
  142. package/src/runtime/verification/verification-gates.ts +3 -2
  143. package/src/runtime/verification/verification-worktree.ts +2 -1
  144. package/src/runtime/workflow-phase-advance.ts +100 -0
  145. package/src/runtime/workspace-tree.ts +9 -0
  146. package/src/schema/config-schema.ts +66 -26
  147. package/src/schema/sensitive-config-paths.ts +64 -0
  148. package/src/schema/team-tool-schema.ts +13 -3
  149. package/src/state/README.md +4 -10
  150. package/src/state/atomic-write.ts +20 -3
  151. package/src/state/contracts.ts +38 -0
  152. package/src/state/coordination/mailbox.ts +12 -2
  153. package/src/state/event-log/cursor.ts +223 -0
  154. package/src/state/event-log/event-log-rotation.ts +12 -4
  155. package/src/state/event-log/event-log.ts +152 -369
  156. package/src/state/event-log/sequence-cache.ts +373 -0
  157. package/src/state/event-log/worker-atomic-writer.ts +2 -1
  158. package/src/state/stores/active-run-registry.ts +3 -2
  159. package/src/state/stores/manifest-io.ts +237 -0
  160. package/src/state/stores/ownership-map.ts +162 -0
  161. package/src/state/stores/plan-store.ts +241 -0
  162. package/src/state/stores/run-cache.ts +0 -90
  163. package/src/state/stores/spec-store.ts +189 -0
  164. package/src/state/stores/state-store.ts +139 -232
  165. package/src/state/types.ts +199 -0
  166. package/src/ui/dashboard-panes/plan-pane.ts +136 -0
  167. package/src/ui/dashboard-panes/progress-pane.ts +6 -0
  168. package/src/ui/dashboard-panes/transcript-pane.ts +31 -0
  169. package/src/ui/dock-footer.ts +49 -0
  170. package/src/ui/heartbeat-aggregator.ts +9 -1
  171. package/src/ui/inline-panel/agent-pane.ts +375 -0
  172. package/src/ui/inline-panel/agent-transcript.ts +338 -0
  173. package/src/ui/inline-panel/agent-view-overlay.ts +225 -0
  174. package/src/ui/inline-panel/crew-editor.ts +192 -0
  175. package/src/ui/inline-panel/index.ts +290 -0
  176. package/src/ui/inline-panel/panel-rows.ts +37 -0
  177. package/src/ui/inline-panel/panel-selection.ts +157 -0
  178. package/src/ui/inline-panel/panel-store.ts +111 -0
  179. package/src/ui/inline-panel/view-session-store.ts +36 -0
  180. package/src/ui/keybinding-map.ts +54 -13
  181. package/src/ui/pi-ui-compat.ts +9 -0
  182. package/src/ui/powerbar-publisher.ts +52 -1
  183. package/src/ui/run-dashboard.ts +31 -5
  184. package/src/ui/run-snapshot-cache.ts +57 -30
  185. package/src/ui/snapshot-types.ts +6 -1
  186. package/src/ui/widget/index.ts +176 -22
  187. package/src/ui/widget/task-list.ts +198 -0
  188. package/src/ui/widget/widget-formatters.ts +240 -4
  189. package/src/ui/widget/widget-renderer.ts +243 -38
  190. package/src/ui/widget/widget-types.ts +11 -0
  191. package/src/utils/child-process-shield.ts +106 -0
  192. package/src/utils/file-coalescer.ts +0 -4
  193. package/src/utils/fs-errno.ts +66 -0
  194. package/src/utils/fs-watch.ts +1 -1
  195. package/src/utils/internal-error.ts +3 -1
  196. package/src/utils/paths.ts +11 -3
  197. package/src/utils/redaction.ts +7 -0
  198. package/src/utils/safe-abort.ts +45 -0
  199. package/src/utils/task-name-generator.ts +1 -8
  200. package/src/workflows/discover-workflows.ts +20 -2
  201. package/src/workflows/validate-workflow.ts +7 -1
  202. package/src/workflows/workflow-config.ts +17 -0
  203. package/src/workflows/workflow-serializer.ts +3 -0
  204. package/src/worktree/worktree-manager.ts +22 -0
  205. package/workflows/default.workflow.md +36 -26
  206. package/workflows/strict-fast-fix.workflow.md +26 -0
  207. package/src/agents/agent-search.ts +0 -98
  208. package/src/benchmark/benchmark-runner.ts +0 -313
  209. package/src/benchmark/feedback-loop.ts +0 -73
  210. package/src/config/resilient-parser.ts +0 -117
  211. package/src/extension/crew-vibes/cat-frames.ts +0 -18
  212. package/src/extension/result-watcher.ts +0 -139
  213. package/src/observability/exporters/prometheus-exporter.ts +0 -54
  214. package/src/observability/metric-retention.ts +0 -64
  215. package/src/runtime/compaction/compaction-summary.ts +0 -278
  216. package/src/runtime/errors/crew-errors.ts +0 -162
  217. package/src/runtime/live-session/intercom-bridge.ts +0 -187
  218. package/src/runtime/loop-gates.ts +0 -128
  219. package/src/runtime/metric-parser.ts +0 -36
  220. package/src/runtime/output/stream-preview.ts +0 -184
  221. package/src/runtime/output/tool-progress.ts +0 -278
  222. package/src/runtime/phase-tracker.ts +0 -385
  223. package/src/runtime/pipeline-runner.ts +0 -523
  224. package/src/runtime/process/process-lifecycle.ts +0 -491
  225. package/src/runtime/recovery/retry-runner.ts +0 -330
  226. package/src/runtime/run-drift.ts +0 -219
  227. package/src/runtime/task-quality.ts +0 -199
  228. package/src/runtime/task-runner/run-projection.ts +0 -128
  229. package/src/runtime/verification/post-checks.ts +0 -142
  230. package/src/state/coordination/schedule.ts +0 -166
  231. package/src/state/event-log/jsonl-writer.ts +0 -115
  232. package/src/state/hook-instinct-bridge.ts +0 -94
  233. package/src/state/hook-integrations.ts +0 -51
  234. package/src/state/session-state-map.ts +0 -51
  235. package/src/state/stores/blob-store.ts +0 -308
  236. package/src/state/stores/instinct-store.ts +0 -275
  237. package/src/state/stores/observation-store.ts +0 -176
  238. package/src/state/tiered-eval.ts +0 -480
  239. package/src/state/types-eval.ts +0 -58
  240. package/src/tools/safe-bash-extension.ts +0 -54
  241. package/src/tools/safe-bash.ts +0 -505
  242. package/src/ui/agent-management-overlay.ts +0 -160
  243. package/src/ui/crew-footer.ts +0 -102
  244. package/src/ui/crew-select-list.ts +0 -114
  245. package/src/ui/dashboard-panes/capability-pane.ts +0 -77
  246. package/src/ui/transcript-entries.ts +0 -256
  247. package/src/utils/conflict-detect.ts +0 -721
  248. package/src/utils/fingerprint.ts +0 -180
  249. package/src/utils/gh-protocol.ts +0 -556
  250. package/src/utils/project-detector.ts +0 -160
  251. package/src/utils/sse-parser.ts +0 -131
  252. package/src/workflows/cost-estimator.ts +0 -34
  253. 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 },
@@ -258,7 +282,7 @@ export const PiTeamsOtlpConfigSchema = Type.Object(
258
282
 
259
283
  export const PiTeamsUiConfigSchema = Type.Object(
260
284
  {
261
- widgetPlacement: Type.Optional(Type.Union([Type.Literal("aboveEditor"), Type.Literal("belowEditor")])),
285
+ widgetPlacement: Type.Optional(Type.Union([Type.Literal("aboveEditor"), Type.Literal("belowEditor"), Type.Literal("bottom")])),
262
286
  widgetMaxLines: Type.Optional(Type.Integer({ minimum: 1, maximum: 50 })),
263
287
  powerbar: Type.Optional(Type.Boolean()),
264
288
  dashboardPlacement: Type.Optional(Type.Union([Type.Literal("center"), Type.Literal("right")])),
@@ -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
  );