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,687 @@
1
+ import { type Static, type TSchema, Type } from "@sinclair/typebox";
2
+ import { Value } from "@sinclair/typebox/value";
3
+ import { PiTeamsAutonomyProfileSchema, PiTeamsConfigSchema } from "../schema/config-schema.ts";
4
+ import { logInternalError } from "../utils/internal-error.ts";
5
+ import { suggestConfigKey } from "./suggestions.ts";
6
+ import type {
7
+ AgentOverrideConfig,
8
+ ConfigValidationResult,
9
+ CrewAgentsConfig,
10
+ CrewBrokerConfig,
11
+ CrewControlConfig,
12
+ CrewLimitsConfig,
13
+ CrewNestingConfig,
14
+ CrewNotificationsConfig,
15
+ CrewObservabilityConfig,
16
+ CrewOtlpConfig,
17
+ CrewPolicyConfig,
18
+ CrewReliabilityConfig,
19
+ CrewRetryPolicyConfig,
20
+ CrewRuntimeConfig,
21
+ CrewTelemetryConfig,
22
+ CrewToolsConfig,
23
+ CrewUiConfig,
24
+ CrewWorktreeConfig,
25
+ GoalWrapWorkflowConfig,
26
+ PiTeamsAutonomousConfig,
27
+ PiTeamsAutonomyProfile,
28
+ PiTeamsConfig,
29
+ } from "./types.ts";
30
+
31
+ function errorPathFromValidation(error: unknown): string {
32
+ if (error && typeof error === "object") {
33
+ if (typeof (error as { path?: unknown }).path === "string") return (error as { path: string }).path;
34
+ if (typeof (error as { instancePath?: unknown }).instancePath === "string") return (error as { instancePath: string }).instancePath;
35
+ if (
36
+ typeof (error as { keyword?: unknown }).keyword === "string" &&
37
+ typeof (error as { schemaPath?: unknown }).schemaPath === "string"
38
+ )
39
+ return (error as { schemaPath: string }).schemaPath;
40
+ }
41
+ return "config";
42
+ }
43
+
44
+ /** Known top-level config keys from the schema — used for fuzzy suggestions. */
45
+ const KNOWN_TOP_LEVEL_KEYS = Object.keys(PiTeamsConfigSchema.properties ?? {}) as string[];
46
+
47
+ function validateConfigWithWarnings(raw: unknown): string[] {
48
+ if (!Value.Check(PiTeamsConfigSchema, raw)) {
49
+ return [...Value.Errors(PiTeamsConfigSchema, raw)].map((error) => {
50
+ const path = errorPathFromValidation(error);
51
+ const message = (error as { message?: unknown }).message ?? "invalid value";
52
+ // Enhance "additionalProperties" errors with fuzzy suggestions
53
+ if ((error as { keyword?: unknown }).keyword === "additionalProperties") {
54
+ const offendingKey = path.split("/").pop() ?? path;
55
+ const suggestion = suggestConfigKey(offendingKey, KNOWN_TOP_LEVEL_KEYS);
56
+ if (suggestion) return `${path}: ${message} (did you mean '${suggestion}'?)`;
57
+ }
58
+ return `${path}: ${message}`;
59
+ });
60
+ }
61
+ return [];
62
+ }
63
+
64
+ const LIMIT_CEILINGS = {
65
+ maxConcurrentWorkers: 1024,
66
+ maxTaskDepth: 100,
67
+ maxChildrenPerTask: 1000,
68
+ maxRunMinutes: 1440,
69
+ maxRetriesPerTask: 100,
70
+ maxTasksPerRun: 10_000,
71
+ heartbeatStaleMs: 24 * 60 * 60 * 1000,
72
+ runtimeMaxTurns: 10_000,
73
+ runtimeGraceTurns: 1_000,
74
+ // RT-NEW-1: taskTimeoutMs is in MILLISECONDS — it must NOT reuse runtimeMaxTurns
75
+ // (10_000 turns), which capped the effective timeout at 10s and silently disabled
76
+ // any larger value (e.g. 300_000 = 5min) via parsePositiveInteger returning undefined.
77
+ runtimeTaskTimeoutMs: 24 * 60 * 60 * 1000,
78
+ nestingMaxSlots: 64,
79
+ nestingMaxDepth: 10,
80
+ } as const;
81
+
82
+ /**
83
+ * Keys that could allow prototype pollution if merged into plain objects.
84
+ * NOTE: This set is comprehensive for ES2023 and earlier. When upgrading JavaScript
85
+ * versions, verify whether new dangerous Object.prototype properties have been added
86
+ * that could enable prototype pollution attacks.
87
+ */
88
+ export const DANGEROUS_OBJECT_KEYS = new Set([
89
+ "__proto__",
90
+ "constructor",
91
+ "prototype",
92
+ "hasOwnProperty",
93
+ "toString",
94
+ "valueOf",
95
+ "isPrototypeOf",
96
+ "propertyIsEnumerable",
97
+ "toLocaleString",
98
+ "__defineGetter__",
99
+ "__defineSetter__",
100
+ "__lookupGetter__",
101
+ "__lookupSetter__",
102
+ ]);
103
+
104
+ /**
105
+ * Strips dangerous Object.prototype keys from an object.
106
+ * Returns a new object built with Object.create(null) to prevent
107
+ * prototype pollution attacks.
108
+ */
109
+ function sanitizeObject(obj: Record<string, unknown>): Record<string, unknown> {
110
+ const sanitized: Record<string, unknown> = Object.create(null);
111
+ for (const [key, value] of Object.entries(obj)) {
112
+ // Case-insensitive check to catch __Proto__, CONSTRUCTOR, etc.
113
+ const lowerKey = key.toLowerCase();
114
+ if (DANGEROUS_OBJECT_KEYS.has(lowerKey)) continue;
115
+ if (value && typeof value === "object" && !Array.isArray(value)) {
116
+ sanitized[key] = sanitizeObject(value as Record<string, unknown>);
117
+ } else {
118
+ sanitized[key] = value;
119
+ }
120
+ }
121
+ return sanitized;
122
+ }
123
+
124
+ export function asRecord(value: unknown): Record<string, unknown> | undefined {
125
+ if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
126
+ // Defensive: create a sanitized copy to prevent prototype pollution.
127
+ // Uses Object.create(null) so the result has no prototype chain.
128
+ // WARNING: The returned object has no prototype methods (no hasOwnProperty,
129
+ // toString, etc.). Use Object.hasOwn(obj, key) or
130
+ // Object.prototype.hasOwnProperty.call(obj, key) for property checks.
131
+ return sanitizeObject(value as Record<string, unknown>);
132
+ }
133
+
134
+ function parseWithSchema<T extends TSchema>(schema: T, value: unknown, context?: string): Static<T> | undefined {
135
+ if (!Value.Check(schema, value)) {
136
+ if (context) {
137
+ logInternalError("config.parseWithSchema", undefined, `${context}: schema validation failed`);
138
+ }
139
+ return undefined;
140
+ }
141
+ return Value.Decode(schema, value);
142
+ }
143
+
144
+ function parseIntegerInRange(value: unknown, minimum = 1, maximum = Number.MAX_SAFE_INTEGER): number | undefined {
145
+ return parseWithSchema(Type.Integer({ minimum, maximum }), value);
146
+ }
147
+
148
+ function parsePositiveInteger(value: unknown, max = Number.MAX_SAFE_INTEGER): number | undefined {
149
+ return parseIntegerInRange(value, 1, max);
150
+ }
151
+
152
+ function parseProfile(value: unknown): PiTeamsAutonomyProfile | undefined {
153
+ return parseWithSchema(PiTeamsAutonomyProfileSchema, value);
154
+ }
155
+
156
+ function parseStringList(value: unknown): string[] | undefined {
157
+ const items = parseWithSchema(Type.Array(Type.String()), value);
158
+ if (!items || items.length === 0) return undefined;
159
+ const normalized = items.map((entry) => entry.trim()).filter((entry) => entry.length > 0);
160
+ return normalized.length > 0 ? normalized : undefined;
161
+ }
162
+
163
+ function parseStringArrayOrFalse(value: unknown): string[] | false | undefined {
164
+ if (value === false) return false;
165
+ if (typeof value === "string") return value.trim() === "" ? [] : parseStringList(value.split(","));
166
+ return parseStringList(value);
167
+ }
168
+
169
+ export function effectiveAutonomousConfig(
170
+ config: PiTeamsAutonomousConfig | undefined,
171
+ ): Required<Pick<PiTeamsAutonomousConfig, "profile" | "enabled" | "injectPolicy" | "preferAsyncForLongTasks" | "allowWorktreeSuggestion">> &
172
+ Pick<PiTeamsAutonomousConfig, "magicKeywords"> {
173
+ const profile = config?.enabled === false ? "manual" : (config?.profile ?? "suggested");
174
+ const profileDefaults: Record<
175
+ PiTeamsAutonomyProfile,
176
+ {
177
+ enabled: boolean;
178
+ injectPolicy: boolean;
179
+ preferAsyncForLongTasks: boolean;
180
+ allowWorktreeSuggestion: boolean;
181
+ }
182
+ > = {
183
+ manual: {
184
+ enabled: false,
185
+ injectPolicy: false,
186
+ preferAsyncForLongTasks: false,
187
+ allowWorktreeSuggestion: false,
188
+ },
189
+ suggested: {
190
+ enabled: true,
191
+ injectPolicy: true,
192
+ preferAsyncForLongTasks: false,
193
+ allowWorktreeSuggestion: true,
194
+ },
195
+ assisted: {
196
+ enabled: true,
197
+ injectPolicy: true,
198
+ preferAsyncForLongTasks: true,
199
+ allowWorktreeSuggestion: true,
200
+ },
201
+ aggressive: {
202
+ enabled: true,
203
+ injectPolicy: true,
204
+ preferAsyncForLongTasks: true,
205
+ allowWorktreeSuggestion: true,
206
+ },
207
+ };
208
+ const defaults = profileDefaults[profile];
209
+ return {
210
+ profile,
211
+ enabled: config?.enabled ?? defaults.enabled,
212
+ injectPolicy: config?.injectPolicy ?? defaults.injectPolicy,
213
+ preferAsyncForLongTasks: config?.preferAsyncForLongTasks ?? defaults.preferAsyncForLongTasks,
214
+ allowWorktreeSuggestion: config?.allowWorktreeSuggestion ?? defaults.allowWorktreeSuggestion,
215
+ magicKeywords: config?.magicKeywords,
216
+ };
217
+ }
218
+
219
+ function parseStringArrayRecord(value: unknown): Record<string, string[]> | undefined {
220
+ const record = parseWithSchema(Type.Record(Type.String({ minLength: 1 }), Type.Array(Type.String())), value);
221
+ if (!record) return undefined;
222
+ const result: Record<string, string[]> = {};
223
+ for (const [key, rawValues] of Object.entries(record)) {
224
+ const parsed = parseStringList(rawValues);
225
+ if (parsed && parsed.length > 0) result[key] = parsed;
226
+ }
227
+ return Object.keys(result).length > 0 ? result : undefined;
228
+ }
229
+
230
+ function parseAutonomousConfig(value: unknown): PiTeamsAutonomousConfig | undefined {
231
+ const obj = asRecord(value);
232
+ if (!obj) return undefined;
233
+ const config: PiTeamsAutonomousConfig = {
234
+ profile: parseProfile(obj.profile),
235
+ enabled: parseWithSchema(Type.Boolean(), obj.enabled),
236
+ injectPolicy: parseWithSchema(Type.Boolean(), obj.injectPolicy),
237
+ preferAsyncForLongTasks: parseWithSchema(Type.Boolean(), obj.preferAsyncForLongTasks),
238
+ allowWorktreeSuggestion: parseWithSchema(Type.Boolean(), obj.allowWorktreeSuggestion),
239
+ magicKeywords: parseStringArrayRecord(obj.magicKeywords),
240
+ };
241
+ return Object.values(config).some((entry) => entry !== undefined) ? config : undefined;
242
+ }
243
+
244
+ function parseLimitsConfig(value: unknown): CrewLimitsConfig | undefined {
245
+ const obj = asRecord(value);
246
+ if (!obj) return undefined;
247
+ const limits: CrewLimitsConfig = {
248
+ maxConcurrentWorkers: parsePositiveInteger(obj.maxConcurrentWorkers, LIMIT_CEILINGS.maxConcurrentWorkers),
249
+ allowUnboundedConcurrency: parseWithSchema(Type.Boolean(), obj.allowUnboundedConcurrency),
250
+ maxTaskDepth: parsePositiveInteger(obj.maxTaskDepth, LIMIT_CEILINGS.maxTaskDepth),
251
+ maxChildrenPerTask: parsePositiveInteger(obj.maxChildrenPerTask, LIMIT_CEILINGS.maxChildrenPerTask),
252
+ maxRunMinutes: parsePositiveInteger(obj.maxRunMinutes, LIMIT_CEILINGS.maxRunMinutes),
253
+ maxRetriesPerTask: parsePositiveInteger(obj.maxRetriesPerTask, LIMIT_CEILINGS.maxRetriesPerTask),
254
+ maxTasksPerRun: parsePositiveInteger(obj.maxTasksPerRun, LIMIT_CEILINGS.maxTasksPerRun),
255
+ heartbeatStaleMs: parsePositiveInteger(obj.heartbeatStaleMs, LIMIT_CEILINGS.heartbeatStaleMs),
256
+ serializeOnPathOverlap: parseWithSchema(Type.Boolean(), obj.serializeOnPathOverlap),
257
+ };
258
+ return Object.values(limits).some((entry) => entry !== undefined) ? limits : undefined;
259
+ }
260
+
261
+ function parseNestingConfig(value: unknown): CrewNestingConfig | undefined {
262
+ const obj = asRecord(value);
263
+ if (!obj) return undefined;
264
+ const nesting: CrewNestingConfig = {
265
+ enabled: parseWithSchema(Type.Boolean(), obj.enabled),
266
+ maxSlots: parsePositiveInteger(obj.maxSlots, LIMIT_CEILINGS.nestingMaxSlots),
267
+ maxDepth: parsePositiveInteger(obj.maxDepth, LIMIT_CEILINGS.nestingMaxDepth),
268
+ };
269
+ return Object.values(nesting).some((entry) => entry !== undefined) ? nesting : undefined;
270
+ }
271
+
272
+ function parseIsolationPolicy(value: unknown): CrewRuntimeConfig["isolationPolicy"] | undefined {
273
+ const obj = asRecord(value);
274
+ if (!obj) return undefined;
275
+ const isolatedRoles = parseStringList(obj.isolatedRoles);
276
+ const defaultRuntime = parseWithSchema(Type.Union([Type.Literal("live-session"), Type.Literal("child-process")]), obj.defaultRuntime);
277
+ if (isolatedRoles === undefined && defaultRuntime === undefined) return undefined;
278
+ return {
279
+ ...(isolatedRoles !== undefined ? { isolatedRoles } : {}),
280
+ ...(defaultRuntime !== undefined ? { defaultRuntime } : {}),
281
+ };
282
+ }
283
+
284
+ /**
285
+ * F19-1 (Round 19 parity): runtime.modelFallback was declared in types.ts and the
286
+ * schema (PiTeamsModelFallbackConfigSchema) but parseRuntimeConfig never emitted
287
+ * it, so user config was silently dropped. Mirrors the schema field-for-field.
288
+ */
289
+ function parseModelFallbackConfig(value: unknown): CrewRuntimeConfig["modelFallback"] {
290
+ const obj = asRecord(value);
291
+ if (!obj) return undefined;
292
+ const modelFallback: NonNullable<CrewRuntimeConfig["modelFallback"]> = {
293
+ maxAutoFallbacks: parseWithSchema(Type.Integer({ minimum: 0 }), obj.maxAutoFallbacks),
294
+ order: parseWithSchema(Type.Union([Type.Literal("parentFirst"), Type.Literal("asIs")]), obj.order),
295
+ requireCredentials: parseWithSchema(Type.Boolean(), obj.requireCredentials),
296
+ quotaAwareOrdering: parseWithSchema(Type.Boolean(), obj.quotaAwareOrdering),
297
+ defaultSubagentModel: parseWithSchema(Type.String({ minLength: 1 }), obj.defaultSubagentModel),
298
+ };
299
+ return Object.values(modelFallback).some((entry) => entry !== undefined) ? modelFallback : undefined;
300
+ }
301
+
302
+ function parseRuntimeConfig(value: unknown): CrewRuntimeConfig | undefined {
303
+ const obj = asRecord(value);
304
+ if (!obj) return { inheritContext: true } as CrewRuntimeConfig;
305
+ const runtime: CrewRuntimeConfig = {
306
+ mode: parseWithSchema(
307
+ Type.Union([Type.Literal("auto"), Type.Literal("scaffold"), Type.Literal("child-process"), Type.Literal("live-session")]),
308
+ obj.mode,
309
+ ),
310
+ preferLiveSession: parseWithSchema(Type.Boolean(), obj.preferLiveSession),
311
+ allowChildProcessFallback: parseWithSchema(Type.Boolean(), obj.allowChildProcessFallback),
312
+ maxTurns: parsePositiveInteger(obj.maxTurns, LIMIT_CEILINGS.runtimeMaxTurns),
313
+ graceTurns: parsePositiveInteger(obj.graceTurns, LIMIT_CEILINGS.runtimeGraceTurns),
314
+ taskTimeoutMs: parsePositiveInteger(obj.taskTimeoutMs, LIMIT_CEILINGS.runtimeTaskTimeoutMs),
315
+ inheritContext: parseWithSchema(Type.Boolean(), obj.inheritContext) ?? true,
316
+ promptMode: parseWithSchema(Type.Union([Type.Literal("replace"), Type.Literal("append")]), obj.promptMode),
317
+ groupJoin: parseWithSchema(Type.Union([Type.Literal("off"), Type.Literal("group"), Type.Literal("smart")]), obj.groupJoin),
318
+ groupJoinAckTimeoutMs: parsePositiveInteger(obj.groupJoinAckTimeoutMs, 86_400_000),
319
+ requirePlanApproval: parseWithSchema(Type.Boolean(), obj.requirePlanApproval),
320
+ completionMutationGuard: parseWithSchema(
321
+ Type.Union([Type.Literal("off"), Type.Literal("warn"), Type.Literal("fail")]),
322
+ obj.completionMutationGuard,
323
+ ),
324
+ effectivenessGuard: parseWithSchema(
325
+ Type.Union([Type.Literal("off"), Type.Literal("warn"), Type.Literal("block"), Type.Literal("fail")]),
326
+ obj.effectivenessGuard,
327
+ ),
328
+ yield: (() => {
329
+ const y = asRecord(obj.yield);
330
+ if (!y) return undefined;
331
+ const parsed: NonNullable<CrewRuntimeConfig["yield"]> = {
332
+ enabled: parseWithSchema(Type.Boolean(), y.enabled),
333
+ maxReminders: parseWithSchema(Type.Integer({ minimum: 0 }), y.maxReminders),
334
+ reminderPrompt: parseWithSchema(Type.String({ maxLength: 1000 }), y.reminderPrompt),
335
+ };
336
+ return Object.values(parsed).some((v) => v !== undefined) ? parsed : undefined;
337
+ })(),
338
+ excludeContextBash: parseWithSchema(Type.Boolean(), obj.excludeContextBash),
339
+ agentExtensions: parseStringList(obj.agentExtensions),
340
+ isolationPolicy: parseIsolationPolicy(obj.isolationPolicy),
341
+ modelFallback: parseModelFallbackConfig(obj.modelFallback),
342
+ };
343
+ return Object.values(runtime).some((entry) => entry !== undefined) ? runtime : undefined;
344
+ }
345
+
346
+ function parseControlConfig(value: unknown): CrewControlConfig | undefined {
347
+ const obj = asRecord(value);
348
+ if (!obj) return undefined;
349
+ const control: CrewControlConfig = {
350
+ enabled: parseWithSchema(Type.Boolean(), obj.enabled),
351
+ needsAttentionAfterMs: parsePositiveInteger(obj.needsAttentionAfterMs),
352
+ // F19-3 (Round 19 parity): read at agent-control.ts with defaults 3/10 —
353
+ // emit undefined when unset so the read-site defaults stay authoritative.
354
+ consecutiveFailureThreshold: parsePositiveInteger(obj.consecutiveFailureThreshold),
355
+ longRunningMinutes: parsePositiveInteger(obj.longRunningMinutes),
356
+ };
357
+ return Object.values(control).some((entry) => entry !== undefined) ? control : undefined;
358
+ }
359
+
360
+ /**
361
+ * Phase 0 broker parser. Returns `undefined` only when input is not an object;
362
+ * otherwise returns the broker config (with only defined fields populated)
363
+ * so the caller can layer defaults on top.
364
+ */
365
+ function parseBrokerConfig(value: unknown): CrewBrokerConfig | undefined {
366
+ const obj = asRecord(value);
367
+ if (!obj) return undefined;
368
+ // Use the exact schema bounds (4..32 / 1024..1048576 / 32..4096). The
369
+ // previous version used parsePositiveInteger(value, default) which clamps
370
+ // the UPPER bound to the default — effectively pathHashLen was capped
371
+ // at 8, much narrower than the schema advertises.
372
+ const broker: CrewBrokerConfig = {
373
+ enabled: parseWithSchema(Type.Boolean(), obj.enabled),
374
+ pathHashLen: parseIntegerInRange(obj.pathHashLen, 4, 32),
375
+ maxFrameBytes: parseIntegerInRange(obj.maxFrameBytes, 1024, 1_048_576),
376
+ outboundQueueCap: parseIntegerInRange(obj.outboundQueueCap, 32, 4096),
377
+ // WP-2/R2 fix (B1 battery 2026-08-18): the hand-rolled whitelist here
378
+ // missed this field — config-schema.ts + DEFAULT_BROKER + the CrewBroker
379
+ // ctor all honored it, but parseConfig silently DROPPED it from every
380
+ // config file, so `broker.waitMethodsEnabled: true` in a workspace
381
+ // config never reached the broker (live ask rejected policy-disabled).
382
+ waitMethodsEnabled: parseWithSchema(Type.Boolean(), obj.waitMethodsEnabled),
383
+ };
384
+ return Object.values(broker).some((entry) => entry !== undefined) ? broker : undefined;
385
+ }
386
+
387
+ function parseWorktreeConfig(value: unknown): CrewWorktreeConfig | undefined {
388
+ const obj = asRecord(value);
389
+ if (!obj) return undefined;
390
+ const rawSetupHook = parseWithSchema(Type.String(), obj.setupHook);
391
+ const setupHook = rawSetupHook?.trim();
392
+ const worktree: CrewWorktreeConfig = {
393
+ setupHook: setupHook ? setupHook : undefined,
394
+ setupHookTimeoutMs: parsePositiveInteger(obj.setupHookTimeoutMs, 300_000),
395
+ linkNodeModules: parseWithSchema(Type.Boolean(), obj.linkNodeModules),
396
+ // C6: seedPaths was declared in the type + schema but never parsed here, so
397
+ // loadedConfig.config.worktree?.seedPaths was always undefined -> the global
398
+ // worktree seed overlay (worktree-manager.ts) silently never applied.
399
+ seedPaths: parseStringList(obj.seedPaths),
400
+ };
401
+ return Object.values(worktree).some((entry) => entry !== undefined) ? worktree : undefined;
402
+ }
403
+
404
+ /** Parse goalWrap config (RFC v0.5 vision: apply goal completion-guarantee to builtins). */
405
+ function parseGoalWrapConfig(value: unknown): Record<string, GoalWrapWorkflowConfig> | undefined {
406
+ const obj = asRecord(value);
407
+ if (!obj) return undefined;
408
+ const result: Record<string, GoalWrapWorkflowConfig> = {};
409
+ let hasAny = false;
410
+ for (const [workflowName, entry] of Object.entries(obj)) {
411
+ const entryObj = asRecord(entry);
412
+ if (!entryObj) continue;
413
+ const parsed: GoalWrapWorkflowConfig = {
414
+ enabled: parseWithSchema(Type.Boolean(), entryObj.enabled),
415
+ maxTurns: parseWithSchema(Type.Integer({ minimum: 1, maximum: 50 }), entryObj.maxTurns),
416
+ evaluatorModel: parseWithSchema(Type.String({ minLength: 1 }), entryObj.evaluatorModel),
417
+ budgetTotal: parseWithSchema(Type.Integer({ minimum: 1000 }), entryObj.budgetTotal),
418
+ budgetUnlimited: parseWithSchema(Type.Boolean(), entryObj.budgetUnlimited),
419
+ };
420
+ // Parse verification sub-object.
421
+ const verObj = asRecord(entryObj.verification);
422
+ if (verObj) {
423
+ const commands = Array.isArray(verObj.commands)
424
+ ? verObj.commands.filter((c): c is string => typeof c === "string" && c.length > 0)
425
+ : undefined;
426
+ const mode = verObj.mode === "text-only" ? ("text-only" as const) : undefined;
427
+ if (commands || mode) {
428
+ parsed.verification = {
429
+ ...(commands ? { commands } : { commands: [] }),
430
+ ...(mode ? { mode } : {}),
431
+ };
432
+ }
433
+ }
434
+ if (Object.values(parsed).some((v) => v !== undefined)) {
435
+ result[workflowName] = parsed;
436
+ hasAny = true;
437
+ }
438
+ }
439
+ return hasAny ? result : undefined;
440
+ }
441
+
442
+ function parseAgentOverride(value: unknown): AgentOverrideConfig | undefined {
443
+ const obj = asRecord(value);
444
+ if (!obj) return undefined;
445
+ const override: AgentOverrideConfig = {
446
+ disabled: parseWithSchema(Type.Boolean(), obj.disabled),
447
+ model: parseWithSchema(Type.Union([Type.String(), Type.Literal(false)]), obj.model),
448
+ fallbackModels: parseStringArrayOrFalse(obj.fallbackModels),
449
+ thinking: parseWithSchema(Type.Union([Type.String(), Type.Literal(false)]), obj.thinking),
450
+ tools: parseStringArrayOrFalse(obj.tools),
451
+ skills: parseStringArrayOrFalse(obj.skills),
452
+ };
453
+ return Object.values(override).some((entry) => entry !== undefined) ? override : undefined;
454
+ }
455
+
456
+ function parseUiConfig(value: unknown): CrewUiConfig | undefined {
457
+ const obj = asRecord(value);
458
+ if (!obj) return undefined;
459
+ const rawWidgetPlacement = parseWithSchema(
460
+ Type.Union([Type.Literal("aboveEditor"), Type.Literal("belowEditor"), Type.Literal("bottom")]),
461
+ obj.widgetPlacement,
462
+ );
463
+ const rawDashboardPlacement = parseWithSchema(Type.Union([Type.Literal("center"), Type.Literal("right")]), obj.dashboardPlacement);
464
+ const rawRowStyle = parseWithSchema(Type.Union([Type.Literal("compact"), Type.Literal("detailed")]), obj.widgetRowStyle);
465
+ const ui: CrewUiConfig = {
466
+ widgetPlacement: rawWidgetPlacement,
467
+ widgetMaxLines: parsePositiveInteger(obj.widgetMaxLines, 50),
468
+ widgetRowStyle: rawRowStyle,
469
+ inlinePanel: parseWithSchema(Type.Boolean(), obj.inlinePanel),
470
+ powerbar: parseWithSchema(Type.Boolean(), obj.powerbar),
471
+ dashboardPlacement: rawDashboardPlacement,
472
+ dashboardWidth: parseIntegerInRange(obj.dashboardWidth, 32, 120),
473
+ dashboardLiveRefreshMs: parseIntegerInRange(obj.dashboardLiveRefreshMs, 250, 60_000),
474
+ autoOpenDashboard: parseWithSchema(Type.Boolean(), obj.autoOpenDashboard),
475
+ autoOpenDashboardForForegroundRuns: parseWithSchema(Type.Boolean(), obj.autoOpenDashboardForForegroundRuns),
476
+ autoCloseDashboardMs: parseWithSchema(Type.Integer({ minimum: 0 }), obj.autoCloseDashboardMs),
477
+ showModel: parseWithSchema(Type.Boolean(), obj.showModel),
478
+ showTokens: parseWithSchema(Type.Boolean(), obj.showTokens),
479
+ showTools: parseWithSchema(Type.Boolean(), obj.showTools),
480
+ transcriptTailBytes: parseIntegerInRange(obj.transcriptTailBytes, 1024, 50 * 1024 * 1024),
481
+ mascotStyle: parseWithSchema(Type.Union([Type.Literal("cat"), Type.Literal("armin")]), obj.mascotStyle),
482
+ mascotEffect: parseWithSchema(
483
+ Type.Union([
484
+ Type.Literal("random"),
485
+ Type.Literal("none"),
486
+ Type.Literal("typewriter"),
487
+ Type.Literal("scanline"),
488
+ Type.Literal("rain"),
489
+ Type.Literal("fade"),
490
+ Type.Literal("crt"),
491
+ Type.Literal("glitch"),
492
+ Type.Literal("dissolve"),
493
+ ]),
494
+ obj.mascotEffect,
495
+ ),
496
+ };
497
+ return Object.values(ui).some((entry) => entry !== undefined) ? ui : undefined;
498
+ }
499
+
500
+ function parseAgentsConfig(value: unknown): CrewAgentsConfig | undefined {
501
+ const obj = asRecord(value);
502
+ if (!obj) return undefined;
503
+ const overrides: Record<string, AgentOverrideConfig> = {};
504
+ if (obj.overrides && typeof obj.overrides === "object" && !Array.isArray(obj.overrides)) {
505
+ for (const [name, rawOverride] of Object.entries(obj.overrides as Record<string, unknown>)) {
506
+ const parsed = parseAgentOverride(rawOverride);
507
+ if (parsed && name.trim()) overrides[name.trim()] = parsed;
508
+ }
509
+ }
510
+ const agents: CrewAgentsConfig = {
511
+ disableBuiltins: parseWithSchema(Type.Boolean(), obj.disableBuiltins),
512
+ overrides: Object.keys(overrides).length > 0 ? overrides : undefined,
513
+ };
514
+ return Object.values(agents).some((entry) => entry !== undefined) ? agents : undefined;
515
+ }
516
+
517
+ function parseToolsConfig(value: unknown): CrewToolsConfig | undefined {
518
+ const obj = asRecord(value);
519
+ if (!obj) return undefined;
520
+ const tools: CrewToolsConfig = {
521
+ enableClaudeStyleAliases: parseWithSchema(Type.Boolean(), obj.enableClaudeStyleAliases),
522
+ enableSteer: parseWithSchema(Type.Boolean(), obj.enableSteer),
523
+ terminateOnForeground: parseWithSchema(Type.Boolean(), obj.terminateOnForeground),
524
+ };
525
+ return Object.values(tools).some((entry) => entry !== undefined) ? tools : undefined;
526
+ }
527
+
528
+ function parseTelemetryConfig(value: unknown): CrewTelemetryConfig | undefined {
529
+ const obj = asRecord(value);
530
+ if (!obj) return undefined;
531
+ const telemetry: CrewTelemetryConfig = {
532
+ enabled: parseWithSchema(Type.Boolean(), obj.enabled),
533
+ };
534
+ return Object.values(telemetry).some((entry) => entry !== undefined) ? telemetry : undefined;
535
+ }
536
+
537
+ function parsePolicyConfig(value: unknown): CrewPolicyConfig | undefined {
538
+ const obj = asRecord(value);
539
+ if (!obj) return undefined;
540
+ const policy: CrewPolicyConfig = {
541
+ requireIntentForDestructiveActions: parseWithSchema(Type.Boolean(), obj.requireIntentForDestructiveActions),
542
+ disabledCapabilities: parseWithSchema(Type.Array(Type.String()), obj.disabledCapabilities),
543
+ };
544
+ return Object.values(policy).some((entry) => entry !== undefined) ? policy : undefined;
545
+ }
546
+
547
+ function parseNotificationsConfig(value: unknown): CrewNotificationsConfig | undefined {
548
+ const obj = asRecord(value);
549
+ if (!obj) return undefined;
550
+ const notifications: CrewNotificationsConfig = {
551
+ enabled: parseWithSchema(Type.Boolean(), obj.enabled),
552
+ severityFilter: parseWithSchema(
553
+ Type.Array(Type.Union([Type.Literal("info"), Type.Literal("warning"), Type.Literal("error"), Type.Literal("critical")])),
554
+ obj.severityFilter,
555
+ ),
556
+ // F19-5 (Wave 1A): minimum 1000 aligns with PiTeamsNotificationsConfigSchema
557
+ // (config-schema.ts:208); values <1000 now parse to undefined like other
558
+ // bound violations (schema has no upper bound, so the old parser-only 24h
559
+ // ceiling is gone — schema wins).
560
+ dedupWindowMs: parseWithSchema(Type.Integer({ minimum: 1000 }), obj.dedupWindowMs),
561
+ batchWindowMs: parseWithSchema(Type.Integer({ minimum: 0, maximum: 60_000 }), obj.batchWindowMs),
562
+ quietHours: parseWithSchema(Type.String({ pattern: "^\\d{2}:\\d{2}-\\d{2}:\\d{2}$" }), obj.quietHours),
563
+ sinkRetentionDays: parsePositiveInteger(obj.sinkRetentionDays, 90),
564
+ };
565
+ return Object.values(notifications).some((entry) => entry !== undefined) ? notifications : undefined;
566
+ }
567
+
568
+ function parseObservabilityConfig(value: unknown): CrewObservabilityConfig | undefined {
569
+ const obj = asRecord(value);
570
+ if (!obj) return undefined;
571
+ const observability: CrewObservabilityConfig = {
572
+ enabled: parseWithSchema(Type.Boolean(), obj.enabled),
573
+ pollIntervalMs: parseWithSchema(Type.Integer({ minimum: 1000, maximum: 60_000 }), obj.pollIntervalMs),
574
+ // F19-5 (Wave 1A): 365 -> 90 aligns the parser ceiling with
575
+ // PiTeamsObservabilityConfigSchema max 90 (config-schema.ts:220).
576
+ metricRetentionDays: parsePositiveInteger(obj.metricRetentionDays, 90),
577
+ };
578
+ return Object.values(observability).some((entry) => entry !== undefined) ? observability : undefined;
579
+ }
580
+
581
+ function parseReliabilityConfig(value: unknown): CrewReliabilityConfig | undefined {
582
+ const obj = asRecord(value);
583
+ if (!obj) return undefined;
584
+ const retryObj = asRecord(obj.retryPolicy);
585
+ const retryPolicy: CrewRetryPolicyConfig | undefined = retryObj
586
+ ? {
587
+ maxAttempts: parsePositiveInteger(retryObj.maxAttempts, 10),
588
+ backoffMs: parseWithSchema(Type.Integer({ minimum: 100, maximum: 60_000 }), retryObj.backoffMs),
589
+ jitterRatio: parseWithSchema(Type.Number({ minimum: 0, maximum: 1 }), retryObj.jitterRatio),
590
+ exponentialFactor: parseWithSchema(Type.Number({ minimum: 1, maximum: 5 }), retryObj.exponentialFactor),
591
+ retryableErrors: parseStringList(retryObj.retryableErrors),
592
+ maxTotalSpawns: parsePositiveInteger(retryObj.maxTotalSpawns),
593
+ }
594
+ : undefined;
595
+ const reliability: CrewReliabilityConfig = {
596
+ autoRetry: parseWithSchema(Type.Boolean(), obj.autoRetry),
597
+ retryPolicy: retryPolicy && Object.values(retryPolicy).some((entry) => entry !== undefined) ? retryPolicy : undefined,
598
+ autoRecover: parseWithSchema(Type.Boolean(), obj.autoRecover),
599
+ deadletterThreshold: parsePositiveInteger(obj.deadletterThreshold),
600
+ cleanupOrphanedTempDirs: parseWithSchema(Type.Boolean(), obj.cleanupOrphanedTempDirs),
601
+ autoRepairIntervalMs: parseWithSchema(Type.Integer({ minimum: 0 }), obj.autoRepairIntervalMs),
602
+ forcePreflight: parseWithSchema(Type.Boolean(), obj.forcePreflight),
603
+ ambientStatusInjection: parseWithSchema(Type.Boolean(), obj.ambientStatusInjection),
604
+ perWriteValidation: parseWithSchema(Type.Boolean(), obj.perWriteValidation),
605
+ scopeModels: parseWithSchema(Type.Boolean(), obj.scopeModels),
606
+ };
607
+ return Object.values(reliability).some((entry) => entry !== undefined) ? reliability : undefined;
608
+ }
609
+
610
+ function parseOtlpConfig(value: unknown): CrewOtlpConfig | undefined {
611
+ const obj = asRecord(value);
612
+ if (!obj) return undefined;
613
+ const headers: Record<string, string> = Object.create(null);
614
+ const rawHeaders = asRecord(obj.headers);
615
+ if (rawHeaders)
616
+ for (const [key, entry] of Object.entries(rawHeaders)) {
617
+ if (typeof entry !== "string") continue;
618
+ // Prevent prototype pollution via dangerous Object.prototype keys.
619
+ // Case-insensitive check to catch __Proto__, CONSTRUCTOR, etc.
620
+ const lowerKey = key.toLowerCase();
621
+ if (
622
+ lowerKey === "__proto__" ||
623
+ lowerKey === "constructor" ||
624
+ lowerKey === "prototype" ||
625
+ lowerKey === "hasownproperty" ||
626
+ lowerKey === "tostring" ||
627
+ lowerKey === "valueof" ||
628
+ lowerKey === "isprototypeof" ||
629
+ lowerKey === "propertyisenumerable" ||
630
+ lowerKey === "tolocalestring" ||
631
+ lowerKey === "__definegetter__" ||
632
+ lowerKey === "__definesetter__" ||
633
+ lowerKey === "__lookupgetter__" ||
634
+ lowerKey === "__lookupsetter__"
635
+ )
636
+ continue;
637
+ // Validate key format: must start with letter, then alphanumeric/hyphen/underscore.
638
+ // Blocks CRLF, NUL, spaces, shell metacharacters in header keys.
639
+ if (!/^[a-zA-Z][a-zA-Z0-9_-]{0,127}$/.test(key)) continue;
640
+ headers[key] = entry;
641
+ }
642
+ const otlp: CrewOtlpConfig = {
643
+ enabled: parseWithSchema(Type.Boolean(), obj.enabled),
644
+ // F19-5 (Wave 1A): ^https?:// pattern matches PiTeamsOtlpConfigSchema
645
+ // (config-schema.ts:258-265); non-http(s) endpoints parse to undefined.
646
+ endpoint: parseWithSchema(Type.String({ minLength: 1, pattern: "^https?://" }), obj.endpoint),
647
+ headers: Object.keys(headers).length > 0 ? headers : undefined,
648
+ intervalMs: parseWithSchema(Type.Integer({ minimum: 5000 }), obj.intervalMs),
649
+ };
650
+ return Object.values(otlp).some((entry) => entry !== undefined) ? otlp : undefined;
651
+ }
652
+
653
+ export function parseConfig(raw: unknown): PiTeamsConfig {
654
+ const obj = asRecord(raw);
655
+ if (!obj) return {};
656
+ return {
657
+ asyncByDefault: parseWithSchema(Type.Boolean(), obj.asyncByDefault),
658
+ executeWorkers: parseWithSchema(Type.Boolean(), obj.executeWorkers),
659
+ notifierIntervalMs: parseWithSchema(Type.Number({ minimum: 1_000 }), obj.notifierIntervalMs),
660
+ requireCleanWorktreeLeader: parseWithSchema(Type.Boolean(), obj.requireCleanWorktreeLeader),
661
+ ignoreMethod: parseWithSchema(Type.Union([Type.Literal("gitignore"), Type.Literal("exclude")]), obj.ignoreMethod),
662
+ autonomous: parseAutonomousConfig(obj.autonomous),
663
+ limits: parseLimitsConfig(obj.limits),
664
+ nesting: parseNestingConfig(obj.nesting),
665
+ runtime: parseRuntimeConfig(obj.runtime),
666
+ control: parseControlConfig(obj.control),
667
+ worktree: parseWorktreeConfig(obj.worktree),
668
+ goalWrap: parseGoalWrapConfig(obj.goalWrap),
669
+ agents: parseAgentsConfig(obj.agents),
670
+ tools: parseToolsConfig(obj.tools),
671
+ telemetry: parseTelemetryConfig(obj.telemetry),
672
+ policy: parsePolicyConfig(obj.policy),
673
+ notifications: parseNotificationsConfig(obj.notifications),
674
+ observability: parseObservabilityConfig(obj.observability),
675
+ reliability: parseReliabilityConfig(obj.reliability),
676
+ otlp: parseOtlpConfig(obj.otlp),
677
+ ui: parseUiConfig(obj.ui),
678
+ broker: parseBrokerConfig(obj.broker),
679
+ };
680
+ }
681
+
682
+ export function parseConfigWithWarnings(raw: unknown): ConfigValidationResult {
683
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return { config: {}, warnings: [] };
684
+ const parsed = parseConfig(raw);
685
+ const warnings = validateConfigWithWarnings(raw as Record<string, unknown>);
686
+ return { config: parsed, warnings };
687
+ }