pi-crew 0.9.67 → 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 (222) hide show
  1. package/CHANGELOG.md +172 -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 +65847 -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 +9 -6
  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 +36 -14
  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/scratchpad/README.md +10 -5
  111. package/src/runtime/scratchpad/guest.ts +103 -2
  112. package/src/runtime/scratchpad/protocol.ts +13 -1
  113. package/src/runtime/scratchpad/transform.ts +206 -12
  114. package/src/runtime/settings-store.ts +219 -0
  115. package/src/runtime/spawn-policy.ts +217 -0
  116. package/src/runtime/stale-reconciler.ts +87 -6
  117. package/src/runtime/subagent-manager.ts +25 -1
  118. package/src/runtime/task-output-context.ts +230 -9
  119. package/src/runtime/task-packet.ts +23 -1
  120. package/src/runtime/task-runner/child-executor.ts +106 -7
  121. package/src/runtime/task-runner/post-execution.ts +125 -1
  122. package/src/runtime/task-runner/pre-execution.ts +39 -1
  123. package/src/runtime/task-runner/prompt-builder.ts +51 -1
  124. package/src/runtime/task-runner/retrieval-orchestrator.ts +72 -18
  125. package/src/runtime/task-runner/spec-evidence.ts +403 -0
  126. package/src/runtime/task-runner/state-helpers.ts +26 -24
  127. package/src/runtime/task-runner.ts +11 -0
  128. package/src/runtime/team-runner.ts +129 -1671
  129. package/src/runtime/verification/spec-sandbox.ts +255 -0
  130. package/src/runtime/verification/verification-gates.ts +3 -2
  131. package/src/runtime/verification/verification-worktree.ts +2 -1
  132. package/src/runtime/workflow-phase-advance.ts +100 -0
  133. package/src/runtime/workspace-tree.ts +9 -0
  134. package/src/schema/config-schema.ts +65 -25
  135. package/src/schema/sensitive-config-paths.ts +64 -0
  136. package/src/schema/team-tool-schema.ts +13 -3
  137. package/src/state/README.md +4 -10
  138. package/src/state/atomic-write.ts +20 -3
  139. package/src/state/contracts.ts +38 -0
  140. package/src/state/coordination/mailbox.ts +12 -2
  141. package/src/state/event-log/cursor.ts +223 -0
  142. package/src/state/event-log/event-log-rotation.ts +12 -4
  143. package/src/state/event-log/event-log.ts +152 -369
  144. package/src/state/event-log/sequence-cache.ts +373 -0
  145. package/src/state/event-log/worker-atomic-writer.ts +2 -1
  146. package/src/state/stores/active-run-registry.ts +3 -2
  147. package/src/state/stores/manifest-io.ts +237 -0
  148. package/src/state/stores/ownership-map.ts +162 -0
  149. package/src/state/stores/plan-store.ts +241 -0
  150. package/src/state/stores/run-cache.ts +0 -90
  151. package/src/state/stores/spec-store.ts +189 -0
  152. package/src/state/stores/state-store.ts +66 -229
  153. package/src/state/types.ts +197 -0
  154. package/src/ui/dashboard-panes/plan-pane.ts +136 -0
  155. package/src/ui/dashboard-panes/progress-pane.ts +6 -0
  156. package/src/ui/dashboard-panes/transcript-pane.ts +31 -0
  157. package/src/ui/heartbeat-aggregator.ts +9 -1
  158. package/src/ui/keybinding-map.ts +54 -13
  159. package/src/ui/powerbar-publisher.ts +52 -1
  160. package/src/ui/run-dashboard.ts +31 -5
  161. package/src/ui/run-snapshot-cache.ts +57 -30
  162. package/src/ui/snapshot-types.ts +6 -1
  163. package/src/ui/widget/widget-renderer.ts +9 -1
  164. package/src/utils/file-coalescer.ts +0 -4
  165. package/src/utils/fs-errno.ts +66 -0
  166. package/src/utils/fs-watch.ts +1 -1
  167. package/src/utils/internal-error.ts +3 -1
  168. package/src/utils/paths.ts +11 -3
  169. package/src/utils/task-name-generator.ts +1 -8
  170. package/src/workflows/discover-workflows.ts +19 -2
  171. package/src/workflows/validate-workflow.ts +7 -1
  172. package/src/workflows/workflow-config.ts +10 -0
  173. package/src/workflows/workflow-serializer.ts +3 -0
  174. package/src/worktree/worktree-manager.ts +22 -0
  175. package/src/agents/agent-search.ts +0 -98
  176. package/src/benchmark/benchmark-runner.ts +0 -313
  177. package/src/benchmark/feedback-loop.ts +0 -73
  178. package/src/config/resilient-parser.ts +0 -117
  179. package/src/extension/crew-vibes/cat-frames.ts +0 -18
  180. package/src/extension/result-watcher.ts +0 -139
  181. package/src/observability/exporters/prometheus-exporter.ts +0 -54
  182. package/src/observability/metric-retention.ts +0 -64
  183. package/src/runtime/compaction/compaction-summary.ts +0 -278
  184. package/src/runtime/errors/crew-errors.ts +0 -162
  185. package/src/runtime/live-session/intercom-bridge.ts +0 -187
  186. package/src/runtime/loop-gates.ts +0 -128
  187. package/src/runtime/metric-parser.ts +0 -36
  188. package/src/runtime/output/stream-preview.ts +0 -184
  189. package/src/runtime/output/tool-progress.ts +0 -278
  190. package/src/runtime/phase-tracker.ts +0 -385
  191. package/src/runtime/pipeline-runner.ts +0 -523
  192. package/src/runtime/process/process-lifecycle.ts +0 -491
  193. package/src/runtime/recovery/retry-runner.ts +0 -330
  194. package/src/runtime/run-drift.ts +0 -219
  195. package/src/runtime/scratchpad/snapshot-hmac.ts +0 -167
  196. package/src/runtime/task-quality.ts +0 -199
  197. package/src/runtime/task-runner/run-projection.ts +0 -128
  198. package/src/runtime/verification/post-checks.ts +0 -142
  199. package/src/state/coordination/schedule.ts +0 -166
  200. package/src/state/event-log/jsonl-writer.ts +0 -115
  201. package/src/state/hook-instinct-bridge.ts +0 -94
  202. package/src/state/hook-integrations.ts +0 -51
  203. package/src/state/session-state-map.ts +0 -51
  204. package/src/state/stores/blob-store.ts +0 -308
  205. package/src/state/stores/instinct-store.ts +0 -275
  206. package/src/state/stores/observation-store.ts +0 -176
  207. package/src/state/tiered-eval.ts +0 -480
  208. package/src/state/types-eval.ts +0 -58
  209. package/src/tools/safe-bash-extension.ts +0 -54
  210. package/src/tools/safe-bash.ts +0 -505
  211. package/src/ui/agent-management-overlay.ts +0 -160
  212. package/src/ui/crew-footer.ts +0 -102
  213. package/src/ui/crew-select-list.ts +0 -114
  214. package/src/ui/dashboard-panes/capability-pane.ts +0 -77
  215. package/src/ui/transcript-entries.ts +0 -256
  216. package/src/utils/conflict-detect.ts +0 -721
  217. package/src/utils/fingerprint.ts +0 -180
  218. package/src/utils/gh-protocol.ts +0 -556
  219. package/src/utils/project-detector.ts +0 -160
  220. package/src/utils/sse-parser.ts +0 -131
  221. package/src/workflows/cost-estimator.ts +0 -34
  222. 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
  );