pi-crew 0.9.16 → 0.9.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (435) hide show
  1. package/CHANGELOG.md +130 -0
  2. package/README.md +19 -0
  3. package/dist/build-meta.json +21784 -0
  4. package/dist/index.mjs +83169 -0
  5. package/dist/index.mjs.map +7 -0
  6. package/docs/A +358 -0
  7. package/docs/M-A +357 -0
  8. package/docs/REVIEW-FINDINGS-2026-06 +357 -0
  9. package/docs/Y +357 -0
  10. package/docs/a +357 -0
  11. package/docs/aA +358 -0
  12. package/docs/archive/README.md +91 -0
  13. package/docs/migration/atomic-write-v2-migration.md +297 -0
  14. package/docs/patterns/command-agent-skill.md +1 -1
  15. package/docs/perf/performance-review-2026-07.md +287 -0
  16. package/docs/skills/REFERENCE.md +0 -17
  17. package/docs//303/242mmaAAA/303/242 +357 -0
  18. package/index.bundle.ts +25 -0
  19. package/index.ts +95 -4
  20. package/package.json +15 -4
  21. package/skills/iterative-audit/SKILL.md +0 -1
  22. package/skills/widget-rendering/SKILL.md +17 -0
  23. package/src/adapters/claude-adapter.ts +1 -3
  24. package/src/adapters/export-util.ts +16 -16
  25. package/src/adapters/index.ts +5 -5
  26. package/src/agents/agent-config.ts +20 -14
  27. package/src/agents/agent-serializer.ts +1 -1
  28. package/src/agents/discover-agents.ts +83 -39
  29. package/src/benchmark/benchmark-runner.ts +51 -51
  30. package/src/benchmark/feedback-loop.ts +1 -1
  31. package/src/config/config.ts +303 -591
  32. package/src/config/defaults.ts +28 -2
  33. package/src/config/drift-detector.ts +11 -14
  34. package/src/config/markers.ts +201 -203
  35. package/src/config/resilient-parser.ts +14 -6
  36. package/src/config/role-tools.ts +3 -3
  37. package/src/config/suggestions.ts +5 -12
  38. package/src/config/types.ts +9 -25
  39. package/src/errors.ts +151 -157
  40. package/src/extension/action-suggestions.ts +54 -9
  41. package/src/extension/async-notifier.ts +43 -13
  42. package/src/extension/autonomous-policy.ts +77 -37
  43. package/src/extension/command-completions.ts +9 -8
  44. package/src/extension/context-status-injection.ts +14 -12
  45. package/src/extension/crew-autocomplete.ts +8 -16
  46. package/src/extension/crew-cleanup.ts +2 -1
  47. package/src/extension/crew-shortcuts.ts +9 -6
  48. package/src/extension/cross-extension-rpc.ts +136 -42
  49. package/src/extension/help.ts +1 -1
  50. package/src/extension/import-index.ts +10 -5
  51. package/src/extension/knowledge-injection.ts +88 -15
  52. package/src/extension/management.ts +89 -349
  53. package/src/extension/message-renderers.ts +3 -4
  54. package/src/extension/notification-router.ts +22 -5
  55. package/src/extension/notification-sink.ts +6 -3
  56. package/src/extension/pi-api.ts +17 -8
  57. package/src/extension/plan-orchestrate.ts +8 -27
  58. package/src/extension/project-init.ts +21 -6
  59. package/src/extension/register.ts +329 -975
  60. package/src/extension/registration/artifact-cleanup.ts +9 -4
  61. package/src/extension/registration/command-utils.ts +5 -1
  62. package/src/extension/registration/commands.ts +806 -314
  63. package/src/extension/registration/compaction-guard.ts +15 -15
  64. package/src/extension/registration/lifecycle.ts +259 -0
  65. package/src/extension/registration/observability.ts +293 -0
  66. package/src/extension/registration/subagent-helpers.ts +29 -7
  67. package/src/extension/registration/subagent-tools.ts +253 -44
  68. package/src/extension/registration/team-tool.ts +19 -84
  69. package/src/extension/registration/ui.ts +168 -0
  70. package/src/extension/registration/viewers.ts +64 -24
  71. package/src/extension/result-watcher.ts +18 -7
  72. package/src/extension/run-bundle-schema.ts +21 -6
  73. package/src/extension/run-export.ts +15 -6
  74. package/src/extension/run-import.ts +64 -29
  75. package/src/extension/run-index.ts +28 -9
  76. package/src/extension/run-maintenance.ts +32 -12
  77. package/src/extension/session-summary.ts +1 -1
  78. package/src/extension/team-manager-command.ts +64 -7
  79. package/src/extension/team-onboard.ts +149 -150
  80. package/src/extension/team-recommendation.ts +121 -21
  81. package/src/extension/team-tool/anchor.ts +31 -64
  82. package/src/extension/team-tool/api.ts +929 -141
  83. package/src/extension/team-tool/auto-summarize.ts +14 -26
  84. package/src/extension/team-tool/cache-control.ts +1 -5
  85. package/src/extension/team-tool/cancel.ts +138 -38
  86. package/src/extension/team-tool/chain-dispatch.ts +5 -13
  87. package/src/extension/team-tool/chain-executor.ts +29 -51
  88. package/src/extension/team-tool/config-patch.ts +1 -1
  89. package/src/extension/team-tool/context.ts +40 -20
  90. package/src/extension/team-tool/destructive-gate.ts +10 -5
  91. package/src/extension/team-tool/doctor.ts +170 -51
  92. package/src/extension/team-tool/explain.ts +228 -214
  93. package/src/extension/team-tool/failure-patterns.ts +3 -9
  94. package/src/extension/team-tool/goal-wrap.ts +71 -29
  95. package/src/extension/team-tool/goal.ts +185 -35
  96. package/src/extension/team-tool/handle-schedule.ts +34 -15
  97. package/src/extension/team-tool/handle-settings.ts +94 -56
  98. package/src/extension/team-tool/health-monitor.ts +27 -108
  99. package/src/extension/team-tool/inspect.ts +42 -9
  100. package/src/extension/team-tool/intent-policy.ts +4 -1
  101. package/src/extension/team-tool/lifecycle-actions.ts +244 -48
  102. package/src/extension/team-tool/orchestrate.ts +7 -18
  103. package/src/extension/team-tool/parallel-dispatch.ts +51 -29
  104. package/src/extension/team-tool/plan.ts +29 -5
  105. package/src/extension/team-tool/respond.ts +49 -16
  106. package/src/extension/team-tool/run-not-found.ts +3 -8
  107. package/src/extension/team-tool/run.ts +80 -317
  108. package/src/extension/team-tool/status.ts +135 -43
  109. package/src/extension/team-tool/workflow-manage.ts +92 -25
  110. package/src/extension/team-tool-types.ts +8 -1
  111. package/src/extension/team-tool.ts +142 -551
  112. package/src/extension/validate-resources.ts +39 -8
  113. package/src/hooks/registry.ts +66 -17
  114. package/src/hooks/types.ts +1 -1
  115. package/src/i18n.ts +25 -11
  116. package/src/observability/correlation.ts +17 -3
  117. package/src/observability/event-bus.ts +51 -56
  118. package/src/observability/event-to-metric.ts +117 -15
  119. package/src/observability/exporters/otlp-exporter.ts +56 -17
  120. package/src/observability/exporters/prometheus-exporter.ts +1 -1
  121. package/src/observability/metric-registry.ts +16 -4
  122. package/src/observability/metric-retention.ts +8 -2
  123. package/src/observability/metric-sink.ts +11 -3
  124. package/src/observability/metrics-primitives.ts +42 -9
  125. package/src/plugins/plugin-define.ts +2 -2
  126. package/src/plugins/plugin-registry.ts +25 -25
  127. package/src/plugins/plugins/index.ts +1 -1
  128. package/src/plugins/plugins/nextjs.ts +16 -16
  129. package/src/plugins/plugins/vite.ts +7 -11
  130. package/src/plugins/plugins/vitest.ts +6 -11
  131. package/src/runtime/adaptive-plan.ts +225 -40
  132. package/src/runtime/agent-control.ts +58 -15
  133. package/src/runtime/agent-memory.ts +15 -9
  134. package/src/runtime/agent-observability.ts +10 -2
  135. package/src/runtime/anchor-manager.ts +27 -25
  136. package/src/runtime/async-marker.ts +7 -1
  137. package/src/runtime/async-runner.ts +39 -24
  138. package/src/runtime/auto-summarize.ts +7 -13
  139. package/src/runtime/background-runner.ts +189 -251
  140. package/src/runtime/batch-barrier.ts +18 -16
  141. package/src/runtime/cancellation-token.ts +16 -6
  142. package/src/runtime/cancellation.ts +46 -8
  143. package/src/runtime/capability-inventory.ts +6 -4
  144. package/src/runtime/chain-parser.ts +44 -11
  145. package/src/runtime/chain-runner.ts +63 -69
  146. package/src/runtime/checkpoint.ts +12 -56
  147. package/src/runtime/child-pi.ts +344 -79
  148. package/src/runtime/coalesce-tasks.ts +268 -0
  149. package/src/runtime/code-summary.ts +71 -26
  150. package/src/runtime/compact-stages/index.ts +15 -5
  151. package/src/runtime/compact-stages/tail-capture-stage.ts +7 -2
  152. package/src/runtime/compact-stages/truncation-stage.ts +4 -1
  153. package/src/runtime/compaction-summary.ts +17 -10
  154. package/src/runtime/completion-guard.ts +33 -16
  155. package/src/runtime/crash-classification.ts +28 -7
  156. package/src/runtime/crash-recovery.ts +177 -37
  157. package/src/runtime/crew-agent-records.ts +145 -47
  158. package/src/runtime/crew-hooks.ts +9 -24
  159. package/src/runtime/cross-extension-rpc.ts +25 -23
  160. package/src/runtime/custom-tools/irc-tool.ts +43 -15
  161. package/src/runtime/custom-tools/submit-result-tool.ts +16 -5
  162. package/src/runtime/delivery-coordinator.ts +34 -7
  163. package/src/runtime/delta-conflict.ts +9 -21
  164. package/src/runtime/deterministic-ast.ts +1 -1
  165. package/src/runtime/diagnostic-export.ts +53 -20
  166. package/src/runtime/direct-run.ts +12 -2
  167. package/src/runtime/dwf-state-store.ts +1 -1
  168. package/src/runtime/dynamic-workflow-context.ts +187 -59
  169. package/src/runtime/dynamic-workflow-runner.ts +42 -20
  170. package/src/runtime/effectiveness.ts +16 -8
  171. package/src/runtime/errors/crew-errors.ts +7 -11
  172. package/src/runtime/event-stream-bridge.ts +9 -3
  173. package/src/runtime/foreground-control.ts +54 -14
  174. package/src/runtime/foreground-watchdog.ts +9 -6
  175. package/src/runtime/goal-achievement.ts +27 -10
  176. package/src/runtime/goal-evaluator.ts +54 -19
  177. package/src/runtime/goal-loop-runner.ts +280 -99
  178. package/src/runtime/goal-state-store.ts +27 -17
  179. package/src/runtime/green-contract.ts +11 -2
  180. package/src/runtime/group-join.ts +64 -31
  181. package/src/runtime/handoff-manager.ts +37 -42
  182. package/src/runtime/heartbeat-gradient.ts +10 -2
  183. package/src/runtime/heartbeat-watcher.ts +32 -6
  184. package/src/runtime/hidden-handoff.ts +13 -31
  185. package/src/runtime/important-line-classifier.ts +12 -2
  186. package/src/runtime/iteration-hooks.ts +25 -15
  187. package/src/runtime/live-agent-control.ts +55 -7
  188. package/src/runtime/live-agent-manager.ts +130 -27
  189. package/src/runtime/live-control-realtime.ts +23 -3
  190. package/src/runtime/live-irc.ts +4 -1
  191. package/src/runtime/live-session-health.ts +12 -4
  192. package/src/runtime/live-session-runtime.ts +379 -117
  193. package/src/runtime/manifest-cache.ts +28 -12
  194. package/src/runtime/mcp-proxy.ts +10 -18
  195. package/src/runtime/metric-parser.ts +1 -5
  196. package/src/runtime/model-fallback.ts +55 -18
  197. package/src/runtime/model-resolver.ts +2 -6
  198. package/src/runtime/model-scope.ts +19 -3
  199. package/src/runtime/notebook-helpers.ts +60 -62
  200. package/src/runtime/orphan-worker-registry.ts +37 -26
  201. package/src/runtime/output-validator.ts +18 -3
  202. package/src/runtime/overflow-recovery.ts +5 -4
  203. package/src/runtime/parallel-research.ts +29 -5
  204. package/src/runtime/parallel-utils.ts +9 -11
  205. package/src/runtime/path-overlap.ts +150 -0
  206. package/src/runtime/peer-dep.ts +12 -17
  207. package/src/runtime/per-write-validator.ts +1 -3
  208. package/src/runtime/phase-tracker.ts +342 -330
  209. package/src/runtime/pi-args.ts +26 -8
  210. package/src/runtime/pi-json-output.ts +1 -1
  211. package/src/runtime/pi-spawn.ts +30 -19
  212. package/src/runtime/pipeline-runner.ts +56 -55
  213. package/src/runtime/plan-templates.ts +5 -6
  214. package/src/runtime/policy-engine.ts +43 -7
  215. package/src/runtime/post-checks.ts +12 -4
  216. package/src/runtime/post-exit-stdio-guard.ts +2 -2
  217. package/src/runtime/process-lifecycle.ts +20 -10
  218. package/src/runtime/process-status.ts +16 -5
  219. package/src/runtime/progress-event-coalescer.ts +2 -1
  220. package/src/runtime/progress-tracker.ts +103 -103
  221. package/src/runtime/prose-compressor.ts +9 -11
  222. package/src/runtime/recovery-recipes.ts +118 -24
  223. package/src/runtime/replace.ts +25 -10
  224. package/src/runtime/resilient-edit.ts +10 -25
  225. package/src/runtime/result-extractor.ts +2 -6
  226. package/src/runtime/retry-executor.ts +20 -4
  227. package/src/runtime/retry-runner.ts +27 -51
  228. package/src/runtime/role-permission.ts +6 -1
  229. package/src/runtime/run-coalesced-task-group.ts +256 -0
  230. package/src/runtime/run-drift.ts +14 -15
  231. package/src/runtime/run-tracker.ts +6 -28
  232. package/src/runtime/runtime-policy.ts +1 -6
  233. package/src/runtime/runtime-resolver.ts +80 -15
  234. package/src/runtime/scheduler.ts +47 -18
  235. package/src/runtime/semaphore.ts +5 -7
  236. package/src/runtime/sensitive-paths.ts +2 -1
  237. package/src/runtime/session-usage.ts +1 -1
  238. package/src/runtime/settings-store.ts +16 -12
  239. package/src/runtime/sidechain-output.ts +6 -2
  240. package/src/runtime/single-agent-compose.ts +1 -4
  241. package/src/runtime/skill-effectiveness.ts +29 -97
  242. package/src/runtime/skill-instructions.ts +40 -83
  243. package/src/runtime/stale-reconciler.ts +73 -197
  244. package/src/runtime/stream-preview.ts +1 -1
  245. package/src/runtime/streaming-output.ts +1 -1
  246. package/src/runtime/subagent-manager.ts +44 -169
  247. package/src/runtime/subprocess-tool-registry.ts +4 -1
  248. package/src/runtime/supervisor-contact.ts +10 -5
  249. package/src/runtime/task-display.ts +19 -5
  250. package/src/runtime/task-graph-scheduler.ts +95 -21
  251. package/src/runtime/task-graph.ts +5 -11
  252. package/src/runtime/task-health.ts +54 -47
  253. package/src/runtime/task-id.ts +12 -19
  254. package/src/runtime/task-output-context.ts +265 -81
  255. package/src/runtime/task-packet.ts +12 -20
  256. package/src/runtime/task-quality.ts +6 -14
  257. package/src/runtime/task-runner/context-retrieval.ts +4 -13
  258. package/src/runtime/task-runner/live-executor.ts +114 -36
  259. package/src/runtime/task-runner/output-splitter.ts +152 -0
  260. package/src/runtime/task-runner/progress.ts +50 -12
  261. package/src/runtime/task-runner/prompt-builder.ts +31 -7
  262. package/src/runtime/task-runner/prompt-pipeline.ts +31 -7
  263. package/src/runtime/task-runner/result-utils.ts +3 -1
  264. package/src/runtime/task-runner/retrieval-orchestrator.ts +310 -0
  265. package/src/runtime/task-runner/run-projection.ts +27 -8
  266. package/src/runtime/task-runner/state-helpers.ts +50 -14
  267. package/src/runtime/task-runner/tail-read.ts +2 -6
  268. package/src/runtime/task-runner.ts +180 -326
  269. package/src/runtime/team-runner-artifacts.ts +13 -0
  270. package/src/runtime/team-runner.ts +456 -974
  271. package/src/runtime/tool-output-pruner.ts +6 -9
  272. package/src/runtime/tool-progress.ts +11 -21
  273. package/src/runtime/verification-gates.ts +33 -24
  274. package/src/runtime/verification-integrity.ts +2 -7
  275. package/src/runtime/verification-worktree.ts +62 -12
  276. package/src/runtime/worker-heartbeat.ts +5 -1
  277. package/src/runtime/worker-startup.ts +29 -5
  278. package/src/runtime/workflow-state.ts +17 -9
  279. package/src/runtime/workspace-lock.ts +30 -35
  280. package/src/runtime/workspace-tree.ts +20 -32
  281. package/src/runtime/yield-handler.ts +43 -9
  282. package/src/runtime/zombie-scanner.ts +5 -4
  283. package/src/schema/config-schema.ts +266 -172
  284. package/src/schema/team-tool-schema.ts +29 -74
  285. package/src/schema/validation-types.ts +25 -17
  286. package/src/skills/discover-skills.ts +35 -10
  287. package/src/skills/skill-templates.ts +109 -27
  288. package/src/skills/validate.ts +26 -8
  289. package/src/state/active-run-registry.ts +87 -28
  290. package/src/state/artifact-store.ts +9 -5
  291. package/src/state/atomic-write-v2.ts +85 -63
  292. package/src/state/atomic-write.ts +106 -27
  293. package/src/state/blob-store.ts +47 -20
  294. package/src/state/contracts.ts +20 -5
  295. package/src/state/crew-init.ts +5 -22
  296. package/src/state/decision-ledger.ts +19 -73
  297. package/src/state/event-log-rotation.ts +41 -15
  298. package/src/state/event-log.ts +168 -53
  299. package/src/state/event-reconstructor.ts +11 -1
  300. package/src/state/gitignore-manager.ts +2 -8
  301. package/src/state/health-store.ts +57 -57
  302. package/src/state/hook-instinct-bridge.ts +1 -1
  303. package/src/state/hook-integrations.ts +1 -1
  304. package/src/state/instinct-store.ts +17 -5
  305. package/src/state/jsonl-writer.ts +1 -1
  306. package/src/state/locks.ts +23 -9
  307. package/src/state/mailbox.ts +151 -28
  308. package/src/state/observation-store.ts +20 -14
  309. package/src/state/run-cache.ts +142 -135
  310. package/src/state/run-graph.ts +6 -15
  311. package/src/state/run-metrics.ts +5 -18
  312. package/src/state/schedule.ts +15 -9
  313. package/src/state/state-store.ts +137 -47
  314. package/src/state/task-claims.ts +12 -2
  315. package/src/state/tiered-eval.ts +52 -43
  316. package/src/state/types-eval.ts +2 -2
  317. package/src/state/types.ts +24 -20
  318. package/src/state/usage.ts +20 -4
  319. package/src/state/worker-atomic-writer.ts +6 -3
  320. package/src/subagents/index.ts +2 -2
  321. package/src/teams/discover-teams.ts +21 -6
  322. package/src/tools/safe-bash-extension.ts +5 -6
  323. package/src/tools/safe-bash.ts +10 -11
  324. package/src/types/new-api-types.ts +6 -10
  325. package/src/ui/agent-management-overlay.ts +52 -40
  326. package/src/ui/card-colors.ts +13 -4
  327. package/src/ui/crew-footer.ts +8 -7
  328. package/src/ui/crew-select-list.ts +1 -1
  329. package/src/ui/dashboard-panes/agents-pane.ts +41 -25
  330. package/src/ui/dashboard-panes/cancellation-pane.ts +1 -1
  331. package/src/ui/dashboard-panes/capability-pane.ts +32 -15
  332. package/src/ui/dashboard-panes/health-pane.ts +2 -1
  333. package/src/ui/dashboard-panes/metrics-pane.ts +4 -1
  334. package/src/ui/dashboard-panes/progress-pane.ts +12 -9
  335. package/src/ui/heartbeat-aggregator.ts +15 -4
  336. package/src/ui/keybinding-map.ts +40 -7
  337. package/src/ui/live-conversation-overlay.ts +36 -20
  338. package/src/ui/live-duration.ts +1 -4
  339. package/src/ui/live-run-sidebar.ts +88 -25
  340. package/src/ui/loaders.ts +2 -8
  341. package/src/ui/mascot.ts +20 -36
  342. package/src/ui/overlays/agent-picker-overlay.ts +11 -3
  343. package/src/ui/overlays/confirm-overlay.ts +4 -2
  344. package/src/ui/overlays/help-overlay.ts +25 -14
  345. package/src/ui/overlays/mailbox-compose-overlay.ts +48 -11
  346. package/src/ui/overlays/mailbox-compose-preview.ts +20 -5
  347. package/src/ui/overlays/mailbox-detail-overlay.ts +27 -6
  348. package/src/ui/pi-ui-compat.ts +7 -7
  349. package/src/ui/powerbar-publisher.ts +120 -42
  350. package/src/ui/render-diff.ts +10 -3
  351. package/src/ui/render-scheduler.ts +12 -4
  352. package/src/ui/run-action-dispatcher.ts +81 -18
  353. package/src/ui/run-dashboard.ts +172 -76
  354. package/src/ui/run-event-bus.ts +59 -37
  355. package/src/ui/run-snapshot-cache.ts +276 -86
  356. package/src/ui/settings-overlay.ts +361 -74
  357. package/src/ui/status-colors.ts +12 -1
  358. package/src/ui/syntax-highlight.ts +1 -1
  359. package/src/ui/terminal-status.ts +1 -3
  360. package/src/ui/theme-adapter.ts +16 -14
  361. package/src/ui/theme-discovery.ts +13 -2
  362. package/src/ui/tool-progress-formatter.ts +7 -7
  363. package/src/ui/tool-render.ts +128 -56
  364. package/src/ui/tool-renderers/brief-mode.ts +45 -27
  365. package/src/ui/tool-renderers/index.ts +109 -43
  366. package/src/ui/transcript-cache.ts +32 -6
  367. package/src/ui/transcript-entries.ts +25 -23
  368. package/src/ui/transcript-viewer.ts +78 -29
  369. package/src/ui/widget/index.ts +99 -40
  370. package/src/ui/widget/widget-formatters.ts +13 -6
  371. package/src/ui/widget/widget-model.ts +19 -8
  372. package/src/ui/widget/widget-renderer.ts +23 -13
  373. package/src/ui/widget/widget-types.ts +1 -1
  374. package/src/utils/bm25-search.ts +199 -199
  375. package/src/utils/conflict-detect.ts +22 -21
  376. package/src/utils/env-filter.ts +14 -7
  377. package/src/utils/file-coalescer.ts +5 -1
  378. package/src/utils/fingerprint.ts +3 -6
  379. package/src/utils/frontmatter.ts +3 -1
  380. package/src/utils/fs-watch.ts +2 -6
  381. package/src/utils/gh-protocol.ts +119 -44
  382. package/src/utils/git.ts +13 -15
  383. package/src/utils/guards.ts +2 -5
  384. package/src/utils/ids.ts +9 -2
  385. package/src/utils/incremental-reader.ts +14 -3
  386. package/src/utils/internal-error.ts +2 -1
  387. package/src/utils/names.ts +12 -3
  388. package/src/utils/paths.ts +49 -5
  389. package/src/utils/project-detector.ts +2 -2
  390. package/src/utils/redaction.ts +29 -19
  391. package/src/utils/resolve-shell.ts +9 -7
  392. package/src/utils/run-watcher-registry.ts +19 -31
  393. package/src/utils/safe-paths.ts +46 -29
  394. package/src/utils/scan-cache.ts +9 -2
  395. package/src/utils/session-utils.ts +2 -4
  396. package/src/utils/sleep.ts +12 -5
  397. package/src/utils/sse-parser.ts +5 -17
  398. package/src/utils/visual.ts +52 -32
  399. package/src/workflows/discover-workflows.ts +41 -109
  400. package/src/workflows/intermediate-store.ts +5 -21
  401. package/src/workflows/preflight-validator.ts +6 -33
  402. package/src/workflows/topology-analyzer.ts +5 -21
  403. package/src/workflows/workflow-config.ts +7 -6
  404. package/src/worktree/branch-freshness.ts +66 -8
  405. package/src/worktree/cleanup.ts +130 -20
  406. package/src/worktree/worktree-manager.ts +212 -69
  407. package/skills/artifact-analysis-loop/SKILL.md +0 -303
  408. package/skills/detection-pipeline-design/SKILL.md +0 -286
  409. package/skills/hunting-investigation-loop/SKILL.md +0 -402
  410. package/skills/incident-playbook-construction/SKILL.md +0 -384
  411. package/skills/security-review/SKILL.md +0 -561
  412. package/skills/threat-hypothesis-framework/SKILL.md +0 -176
  413. package/skills/ui-render-performance/SKILL.md +0 -58
  414. /package/docs/{followup-review-round3-2026-05-12.md → archive/followup-review-round3-2026-05-12.md} +0 -0
  415. /package/docs/{followup-review-round4-2026-05-13.md → archive/followup-review-round4-2026-05-13.md} +0 -0
  416. /package/docs/{pi-crew-bugs.md → archive/pi-crew-bugs.md} +0 -0
  417. /package/docs/{pi-crew-test-final.md → archive/pi-crew-test-final.md} +0 -0
  418. /package/docs/{pi-crew-test-results.md → archive/pi-crew-test-results.md} +0 -0
  419. /package/docs/{pi-crew-test-round2.md → archive/pi-crew-test-round2.md} +0 -0
  420. /package/docs/{pi-crew-test-round4.md → archive/pi-crew-test-round4.md} +0 -0
  421. /package/docs/{pi-crew-test-round5.md → archive/pi-crew-test-round5.md} +0 -0
  422. /package/docs/{pi-crew-test-round6.md → archive/pi-crew-test-round6.md} +0 -0
  423. /package/docs/{pi-crew-v0.5.10-audit-fix-plan.md → archive/pi-crew-v0.5.10-audit-fix-plan.md} +0 -0
  424. /package/docs/{pi-crew-v0.5.11-audit-fix-plan.md → archive/pi-crew-v0.5.11-audit-fix-plan.md} +0 -0
  425. /package/docs/{pi-crew-v0.5.12-audit-fix-plan.md → archive/pi-crew-v0.5.12-audit-fix-plan.md} +0 -0
  426. /package/docs/{pi-crew-v0.5.13-audit-fix-plan.md → archive/pi-crew-v0.5.13-audit-fix-plan.md} +0 -0
  427. /package/docs/{pi-crew-v0.5.14-audit-fix-plan.md → archive/pi-crew-v0.5.14-audit-fix-plan.md} +0 -0
  428. /package/docs/{pi-crew-v0.5.16-audit-fix-plan.md → archive/pi-crew-v0.5.16-audit-fix-plan.md} +0 -0
  429. /package/docs/{pi-crew-v0.5.17-audit-fix-plan.md → archive/pi-crew-v0.5.17-audit-fix-plan.md} +0 -0
  430. /package/docs/{pi-crew-v0.5.5-audit-fix-plan.md → archive/pi-crew-v0.5.5-audit-fix-plan.md} +0 -0
  431. /package/docs/{pi-crew-v0.5.9-audit-fix-plan.md → archive/pi-crew-v0.5.9-audit-fix-plan.md} +0 -0
  432. /package/docs/{pi-mono-opportunities.md → archive/pi-mono-opportunities.md} +0 -0
  433. /package/docs/{pi-mono-review.md → archive/pi-mono-review.md} +0 -0
  434. /package/docs/{pi-subagent4-comparison.md → archive/pi-subagent4-comparison.md} +0 -0
  435. /package/docs/{pi-subagents3-deep-analysis.md → archive/pi-subagents3-deep-analysis.md} +0 -0
@@ -3,12 +3,9 @@ import * as os from "node:os";
3
3
  import * as path from "node:path";
4
4
  import { type Static, type TSchema, Type } from "@sinclair/typebox";
5
5
  import { Value } from "@sinclair/typebox/value";
6
- import {
7
- PiTeamsAutonomyProfileSchema,
8
- PiTeamsConfigSchema,
9
- } from "../schema/config-schema.ts";
10
- import { withFileLockSync } from "../state/locks.ts";
6
+ import { PiTeamsAutonomyProfileSchema, PiTeamsConfigSchema } from "../schema/config-schema.ts";
11
7
  import { atomicWriteFile } from "../state/atomic-write.ts";
8
+ import { withFileLockSync } from "../state/locks.ts";
12
9
  import { logInternalError } from "../utils/internal-error.ts";
13
10
  import { projectCrewRoot, projectPiRoot } from "../utils/paths.ts";
14
11
  import { suggestConfigKey } from "./suggestions.ts";
@@ -69,6 +66,108 @@ import type {
69
66
  UpdateConfigOptions,
70
67
  } from "./types.ts";
71
68
 
69
+ // (F16) loadConfig was called 1 Hz idle / 6 Hz active with 0 cache — added 2s TTL+mtime cache following the manifestCache pattern in state-store.ts:75-130.
70
+ const CONFIG_CACHE_TTL_MS = 2000;
71
+
72
+ interface ConfigCacheEntry {
73
+ value: LoadedPiTeamsConfig;
74
+ mtimes: Record<string, number>;
75
+ cachedAt: number;
76
+ }
77
+
78
+ interface ConfigCacheKeyParts {
79
+ filePath: string;
80
+ legacyPath: string;
81
+ projectPath: string;
82
+ projectPiCrewJsonPath: string;
83
+ cwd: string | null;
84
+ }
85
+
86
+ const configCache = new Map<string, ConfigCacheEntry>();
87
+
88
+ /** @internal — TTL override for unit tests (matches __test__setManifestCache pattern in state-store.ts:82). */
89
+ export function __test__setConfigCacheTtlMs(ttlMs: number): void {
90
+ configCacheTtlMsOverride = ttlMs;
91
+ }
92
+
93
+ /** @internal — read the effective TTL in use by the cache. */
94
+ export function __test__getConfigCacheTtlMs(): number {
95
+ return configCacheTtlMsOverride ?? CONFIG_CACHE_TTL_MS;
96
+ }
97
+
98
+ /** @internal — peek at the cached entry for a given key shape. */
99
+ export function __test__getConfigCacheEntry(parts: ConfigCacheKeyParts): ConfigCacheEntry | undefined {
100
+ return configCache.get(buildConfigCacheKey(parts));
101
+ }
102
+
103
+ /** @internal — number of cached entries (for tests/diagnostics). */
104
+ export function __test__configCacheSize(): number {
105
+ return configCache.size;
106
+ }
107
+
108
+ let configCacheTtlMsOverride: number | null = null;
109
+
110
+ function buildConfigCacheKey(parts: ConfigCacheKeyParts): string {
111
+ return JSON.stringify([parts.filePath, parts.legacyPath, parts.projectPath, parts.projectPiCrewJsonPath, parts.cwd]);
112
+ }
113
+
114
+ function statMtimeMs(filePath: string): number | undefined {
115
+ try {
116
+ return fs.statSync(filePath).mtimeMs;
117
+ } catch (error) {
118
+ if ((error as NodeJS.ErrnoException).code === "ENOENT") return undefined;
119
+ throw error;
120
+ }
121
+ }
122
+
123
+ function readCachedConfigParts(filePath: string, legacyPath: string, cwd: string | undefined): ConfigCacheKeyParts {
124
+ const projectPath = cwd ? projectConfigPath(cwd) : "";
125
+ const piCrewJsonPath = cwd ? projectPiCrewJsonPath(cwd) : "";
126
+ return {
127
+ filePath,
128
+ legacyPath,
129
+ projectPath,
130
+ projectPiCrewJsonPath: piCrewJsonPath,
131
+ cwd: cwd ?? null,
132
+ };
133
+ }
134
+
135
+ function readCacheMtimes(parts: ConfigCacheKeyParts): Record<string, number> {
136
+ const mtimes: Record<string, number> = {};
137
+ for (const p of [parts.filePath, parts.legacyPath, parts.projectPath, parts.projectPiCrewJsonPath]) {
138
+ if (!p) continue; // skip empty (cwd-undefined project paths)
139
+ const m = statMtimeMs(p);
140
+ if (m !== undefined) mtimes[p] = m;
141
+ }
142
+ return mtimes;
143
+ }
144
+
145
+ function matchesCachedMtimes(cached: Record<string, number>, current: Record<string, number>): boolean {
146
+ const cachedKeys = Object.keys(cached);
147
+ const currentKeys = Object.keys(current);
148
+ if (cachedKeys.length !== currentKeys.length) return false;
149
+ for (const key of cachedKeys) {
150
+ if (current[key] !== cached[key]) return false;
151
+ }
152
+ return true;
153
+ }
154
+
155
+ function setConfigCache(key: string, value: LoadedPiTeamsConfig, mtimes: Record<string, number>): void {
156
+ if (configCache.has(key)) configCache.delete(key);
157
+ configCache.set(key, { value, mtimes, cachedAt: Date.now() });
158
+ const ttlMs = configCacheTtlMsOverride ?? CONFIG_CACHE_TTL_MS;
159
+ // TTL eviction on insert (mirrors manifestCache eviction in state-store.ts:108-117)
160
+ const now = Date.now();
161
+ for (const [k, entry] of configCache.entries()) {
162
+ if (now - entry.cachedAt > ttlMs) configCache.delete(k);
163
+ }
164
+ }
165
+
166
+ /** Drop all cached loadConfig results. Call after config writes or from tests. */
167
+ export function invalidateConfigCache(): void {
168
+ configCache.clear();
169
+ }
170
+
72
171
  function resolveHomeDir(): string {
73
172
  const envValue = process.env.PI_TEAMS_HOME?.trim();
74
173
  const defaultHome = os.homedir();
@@ -112,14 +211,7 @@ export function configPath(): string {
112
211
  }
113
212
 
114
213
  export function legacyConfigPath(): string {
115
- return path.join(
116
- resolveHomeDir(),
117
- ".pi",
118
- "agent",
119
- "extensions",
120
- "pi-crew",
121
- "config.json",
122
- );
214
+ return path.join(resolveHomeDir(), ".pi", "agent", "extensions", "pi-crew", "config.json");
123
215
  }
124
216
 
125
217
  export function projectConfigPath(cwd: string): string {
@@ -134,23 +226,14 @@ export function projectPiCrewJsonPath(cwd: string): string {
134
226
  return path.join(projectPiRoot(cwd), "pi-crew.json");
135
227
  }
136
228
 
137
- function withoutUndefined<T extends Record<string, unknown>>(
138
- value: T,
139
- ): Partial<T> {
140
- return Object.fromEntries(
141
- Object.entries(value).filter(([, entry]) => entry !== undefined),
142
- ) as Partial<T>;
229
+ function withoutUndefined<T extends Record<string, unknown>>(value: T): Partial<T> {
230
+ return Object.fromEntries(Object.entries(value).filter(([, entry]) => entry !== undefined)) as Partial<T>;
143
231
  }
144
232
 
145
233
  function errorPathFromValidation(error: unknown): string {
146
234
  if (error && typeof error === "object") {
147
- if (typeof (error as { path?: unknown }).path === "string")
148
- return (error as { path: string }).path;
149
- if (
150
- typeof (error as { instancePath?: unknown }).instancePath ===
151
- "string"
152
- )
153
- return (error as { instancePath: string }).instancePath;
235
+ if (typeof (error as { path?: unknown }).path === "string") return (error as { path: string }).path;
236
+ if (typeof (error as { instancePath?: unknown }).instancePath === "string") return (error as { instancePath: string }).instancePath;
154
237
  if (
155
238
  typeof (error as { keyword?: unknown }).keyword === "string" &&
156
239
  typeof (error as { schemaPath?: unknown }).schemaPath === "string"
@@ -161,28 +244,18 @@ function errorPathFromValidation(error: unknown): string {
161
244
  }
162
245
 
163
246
  /** Known top-level config keys from the schema — used for fuzzy suggestions. */
164
- const KNOWN_TOP_LEVEL_KEYS = Object.keys(
165
- PiTeamsConfigSchema.properties ?? {},
166
- ) as string[];
247
+ const KNOWN_TOP_LEVEL_KEYS = Object.keys(PiTeamsConfigSchema.properties ?? {}) as string[];
167
248
 
168
249
  function validateConfigWithWarnings(raw: unknown): string[] {
169
250
  if (!Value.Check(PiTeamsConfigSchema, raw)) {
170
251
  return [...Value.Errors(PiTeamsConfigSchema, raw)].map((error) => {
171
252
  const path = errorPathFromValidation(error);
172
- const message =
173
- (error as { message?: unknown }).message ?? "invalid value";
253
+ const message = (error as { message?: unknown }).message ?? "invalid value";
174
254
  // Enhance "additionalProperties" errors with fuzzy suggestions
175
- if (
176
- (error as { keyword?: unknown }).keyword ===
177
- "additionalProperties"
178
- ) {
255
+ if ((error as { keyword?: unknown }).keyword === "additionalProperties") {
179
256
  const offendingKey = path.split("/").pop() ?? path;
180
- const suggestion = suggestConfigKey(
181
- offendingKey,
182
- KNOWN_TOP_LEVEL_KEYS,
183
- );
184
- if (suggestion)
185
- return `${path}: ${message} (did you mean '${suggestion}'?)`;
257
+ const suggestion = suggestConfigKey(offendingKey, KNOWN_TOP_LEVEL_KEYS);
258
+ if (suggestion) return `${path}: ${message} (did you mean '${suggestion}'?)`;
186
259
  }
187
260
  return `${path}: ${message}`;
188
261
  });
@@ -190,18 +263,11 @@ function validateConfigWithWarnings(raw: unknown): string[] {
190
263
  return [];
191
264
  }
192
265
 
193
- function projectOverrideWarning(
194
- projectPath: string,
195
- dottedPath: string,
196
- ): string {
266
+ function projectOverrideWarning(projectPath: string, dottedPath: string): string {
197
267
  return `${projectPath}: project-level sensitive config '${dottedPath}' is ignored; set it in user config to trust it explicitly`;
198
268
  }
199
269
 
200
- function sanitizeProjectConfig(
201
- projectPath: string,
202
- userConfig: PiTeamsConfig,
203
- config: PiTeamsConfig,
204
- ): ConfigValidationResult {
270
+ function sanitizeProjectConfig(projectPath: string, userConfig: PiTeamsConfig, config: PiTeamsConfig): ConfigValidationResult {
205
271
  const sanitized: PiTeamsConfig = { ...config };
206
272
  const warnings: string[] = [];
207
273
  const dropTopLevel = (key: keyof PiTeamsConfig): void => {
@@ -214,73 +280,36 @@ function sanitizeProjectConfig(
214
280
  dropTopLevel("requireCleanWorktreeLeader");
215
281
  if (config.runtime) {
216
282
  const runtime = { ...config.runtime };
217
- for (const key of [
218
- "mode",
219
- "preferLiveSession",
220
- "allowChildProcessFallback",
221
- "inheritContext",
222
- "isolationPolicy",
223
- ] as const) {
283
+ for (const key of ["mode", "preferLiveSession", "allowChildProcessFallback", "inheritContext", "isolationPolicy"] as const) {
224
284
  if (runtime[key] !== undefined) {
225
285
  delete runtime[key];
226
- warnings.push(
227
- projectOverrideWarning(projectPath, `runtime.${key}`),
228
- );
286
+ warnings.push(projectOverrideWarning(projectPath, `runtime.${key}`));
229
287
  }
230
288
  }
231
289
  if (runtime.requirePlanApproval === false) {
232
290
  delete runtime.requirePlanApproval;
233
- warnings.push(
234
- projectOverrideWarning(
235
- projectPath,
236
- "runtime.requirePlanApproval",
237
- ),
238
- );
291
+ warnings.push(projectOverrideWarning(projectPath, "runtime.requirePlanApproval"));
239
292
  }
240
- sanitized.runtime = Object.values(runtime).some(
241
- (entry) => entry !== undefined,
242
- )
243
- ? runtime
244
- : undefined;
293
+ sanitized.runtime = Object.values(runtime).some((entry) => entry !== undefined) ? runtime : undefined;
245
294
  }
246
295
  if (config.autonomous) {
247
296
  const autonomous = { ...config.autonomous };
248
- for (const key of [
249
- "profile",
250
- "enabled",
251
- "injectPolicy",
252
- "preferAsyncForLongTasks",
253
- "allowWorktreeSuggestion",
254
- ] as const) {
297
+ for (const key of ["profile", "enabled", "injectPolicy", "preferAsyncForLongTasks", "allowWorktreeSuggestion"] as const) {
255
298
  if (autonomous[key] !== undefined) {
256
299
  delete autonomous[key];
257
- warnings.push(
258
- projectOverrideWarning(projectPath, `autonomous.${key}`),
259
- );
300
+ warnings.push(projectOverrideWarning(projectPath, `autonomous.${key}`));
260
301
  }
261
302
  }
262
- sanitized.autonomous = Object.values(autonomous).some(
263
- (entry) => entry !== undefined,
264
- )
265
- ? autonomous
266
- : undefined;
303
+ sanitized.autonomous = Object.values(autonomous).some((entry) => entry !== undefined) ? autonomous : undefined;
267
304
  }
268
305
  if (config.worktree?.setupHook !== undefined) {
269
306
  sanitized.worktree = { ...config.worktree, setupHook: undefined };
270
- if (
271
- !Object.values(sanitized.worktree).some(
272
- (entry) => entry !== undefined,
273
- )
274
- )
275
- sanitized.worktree = undefined;
276
- warnings.push(
277
- projectOverrideWarning(projectPath, "worktree.setupHook"),
278
- );
307
+ if (!Object.values(sanitized.worktree).some((entry) => entry !== undefined)) sanitized.worktree = undefined;
308
+ warnings.push(projectOverrideWarning(projectPath, "worktree.setupHook"));
279
309
  }
280
310
  if (config.otlp?.headers !== undefined) {
281
311
  sanitized.otlp = { ...config.otlp, headers: undefined };
282
- if (!Object.values(sanitized.otlp).some((entry) => entry !== undefined))
283
- sanitized.otlp = undefined;
312
+ if (!Object.values(sanitized.otlp).some((entry) => entry !== undefined)) sanitized.otlp = undefined;
284
313
  warnings.push(projectOverrideWarning(projectPath, "otlp.headers"));
285
314
  }
286
315
  // FIX: Block project config from setting otlp.endpoint — it controls where
@@ -288,66 +317,37 @@ function sanitizeProjectConfig(
288
317
  if (config.otlp?.endpoint !== undefined) {
289
318
  if (!sanitized.otlp) sanitized.otlp = { ...config.otlp, endpoint: undefined };
290
319
  else sanitized.otlp = { ...sanitized.otlp, endpoint: undefined };
291
- if (!Object.values(sanitized.otlp).some((entry) => entry !== undefined))
292
- sanitized.otlp = undefined;
320
+ if (!Object.values(sanitized.otlp).some((entry) => entry !== undefined)) sanitized.otlp = undefined;
293
321
  warnings.push(projectOverrideWarning(projectPath, "otlp.endpoint"));
294
322
  }
295
- if (
296
- config.agents?.disableBuiltins !== undefined ||
297
- config.agents?.overrides !== undefined
298
- ) {
323
+ if (config.agents?.disableBuiltins !== undefined || config.agents?.overrides !== undefined) {
299
324
  const agents = { ...config.agents };
300
325
  if (agents.disableBuiltins !== undefined) {
301
326
  delete agents.disableBuiltins;
302
- warnings.push(
303
- projectOverrideWarning(projectPath, "agents.disableBuiltins"),
304
- );
327
+ warnings.push(projectOverrideWarning(projectPath, "agents.disableBuiltins"));
305
328
  }
306
329
  if (agents.overrides !== undefined) {
307
330
  delete agents.overrides;
308
- warnings.push(
309
- projectOverrideWarning(projectPath, "agents.overrides"),
310
- );
331
+ warnings.push(projectOverrideWarning(projectPath, "agents.overrides"));
311
332
  }
312
- sanitized.agents = Object.values(agents).some(
313
- (entry) => entry !== undefined,
314
- )
315
- ? agents
316
- : undefined;
333
+ sanitized.agents = Object.values(agents).some((entry) => entry !== undefined) ? agents : undefined;
317
334
  }
318
- if (
319
- config.tools?.enableSteer !== undefined ||
320
- config.tools?.terminateOnForeground !== undefined
321
- ) {
335
+ if (config.tools?.enableSteer !== undefined || config.tools?.terminateOnForeground !== undefined) {
322
336
  const tools = { ...config.tools };
323
337
  if (tools.enableSteer !== undefined) {
324
338
  delete tools.enableSteer;
325
- warnings.push(
326
- projectOverrideWarning(projectPath, "tools.enableSteer"),
327
- );
339
+ warnings.push(projectOverrideWarning(projectPath, "tools.enableSteer"));
328
340
  }
329
341
  if (tools.terminateOnForeground !== undefined) {
330
342
  delete tools.terminateOnForeground;
331
- warnings.push(
332
- projectOverrideWarning(
333
- projectPath,
334
- "tools.terminateOnForeground",
335
- ),
336
- );
343
+ warnings.push(projectOverrideWarning(projectPath, "tools.terminateOnForeground"));
337
344
  }
338
- sanitized.tools = Object.values(tools).some(
339
- (entry) => entry !== undefined,
340
- )
341
- ? tools
342
- : undefined;
345
+ sanitized.tools = Object.values(tools).some((entry) => entry !== undefined) ? tools : undefined;
343
346
  }
344
347
  return { config: sanitized, warnings };
345
348
  }
346
349
 
347
- function mergeConfig(
348
- base: PiTeamsConfig,
349
- override: PiTeamsConfig,
350
- ): PiTeamsConfig {
350
+ function mergeConfig(base: PiTeamsConfig, override: PiTeamsConfig): PiTeamsConfig {
351
351
  const warnings: string[] = [];
352
352
  const merged: PiTeamsConfig = {
353
353
  ...base,
@@ -356,41 +356,31 @@ function mergeConfig(
356
356
  if (base.autonomous || override.autonomous) {
357
357
  merged.autonomous = {
358
358
  ...(base.autonomous ?? {}),
359
- ...withoutUndefined(
360
- (override.autonomous ?? {}) as Record<string, unknown>,
361
- ),
359
+ ...withoutUndefined((override.autonomous ?? {}) as Record<string, unknown>),
362
360
  };
363
361
  }
364
362
  if (base.limits || override.limits) {
365
363
  merged.limits = {
366
364
  ...(base.limits ?? {}),
367
- ...withoutUndefined(
368
- (override.limits ?? {}) as Record<string, unknown>,
369
- ),
365
+ ...withoutUndefined((override.limits ?? {}) as Record<string, unknown>),
370
366
  };
371
367
  }
372
368
  if (base.runtime || override.runtime) {
373
369
  merged.runtime = {
374
370
  ...(base.runtime ?? {}),
375
- ...withoutUndefined(
376
- (override.runtime ?? {}) as Record<string, unknown>,
377
- ),
371
+ ...withoutUndefined((override.runtime ?? {}) as Record<string, unknown>),
378
372
  };
379
373
  }
380
374
  if (base.control || override.control) {
381
375
  merged.control = {
382
376
  ...(base.control ?? {}),
383
- ...withoutUndefined(
384
- (override.control ?? {}) as Record<string, unknown>,
385
- ),
377
+ ...withoutUndefined((override.control ?? {}) as Record<string, unknown>),
386
378
  };
387
379
  }
388
380
  if (base.worktree || override.worktree) {
389
381
  merged.worktree = {
390
382
  ...(base.worktree ?? {}),
391
- ...withoutUndefined(
392
- (override.worktree ?? {}) as Record<string, unknown>,
393
- ),
383
+ ...withoutUndefined((override.worktree ?? {}) as Record<string, unknown>),
394
384
  };
395
385
  }
396
386
  if (base.ui || override.ui) {
@@ -402,75 +392,52 @@ function mergeConfig(
402
392
  if (base.agents || override.agents) {
403
393
  merged.agents = {
404
394
  ...(base.agents ?? {}),
405
- ...withoutUndefined(
406
- (override.agents ?? {}) as Record<string, unknown>,
407
- ),
395
+ ...withoutUndefined((override.agents ?? {}) as Record<string, unknown>),
408
396
  overrides: {
409
397
  ...(base.agents?.overrides ?? {}),
410
- ...(withoutUndefined(
411
- (override.agents?.overrides ?? {}) as Record<
412
- string,
413
- unknown
414
- >,
415
- ) as Record<string, AgentOverrideConfig>),
398
+ ...(withoutUndefined((override.agents?.overrides ?? {}) as Record<string, unknown>) as Record<string, AgentOverrideConfig>),
416
399
  },
417
400
  };
418
401
  }
419
402
  if (base.tools || override.tools) {
420
403
  merged.tools = {
421
404
  ...(base.tools ?? {}),
422
- ...withoutUndefined(
423
- (override.tools ?? {}) as Record<string, unknown>,
424
- ),
405
+ ...withoutUndefined((override.tools ?? {}) as Record<string, unknown>),
425
406
  };
426
407
  }
427
408
  if (base.telemetry || override.telemetry) {
428
409
  merged.telemetry = {
429
410
  ...(base.telemetry ?? {}),
430
- ...withoutUndefined(
431
- (override.telemetry ?? {}) as Record<string, unknown>,
432
- ),
411
+ ...withoutUndefined((override.telemetry ?? {}) as Record<string, unknown>),
433
412
  };
434
413
  }
435
414
  if (base.policy || override.policy) {
436
415
  merged.policy = {
437
416
  ...(base.policy ?? {}),
438
- ...withoutUndefined(
439
- (override.policy ?? {}) as Record<string, unknown>,
440
- ),
417
+ ...withoutUndefined((override.policy ?? {}) as Record<string, unknown>),
441
418
  };
442
419
  }
443
420
  if (base.notifications || override.notifications) {
444
421
  merged.notifications = {
445
422
  ...(base.notifications ?? {}),
446
- ...withoutUndefined(
447
- (override.notifications ?? {}) as Record<string, unknown>,
448
- ),
423
+ ...withoutUndefined((override.notifications ?? {}) as Record<string, unknown>),
449
424
  };
450
425
  }
451
426
  if (base.observability || override.observability) {
452
427
  merged.observability = {
453
428
  ...(base.observability ?? {}),
454
- ...withoutUndefined(
455
- (override.observability ?? {}) as Record<string, unknown>,
456
- ),
429
+ ...withoutUndefined((override.observability ?? {}) as Record<string, unknown>),
457
430
  };
458
431
  }
459
432
  if (base.reliability || override.reliability) {
460
433
  merged.reliability = {
461
434
  ...(base.reliability ?? {}),
462
- ...withoutUndefined(
463
- (override.reliability ?? {}) as Record<string, unknown>,
464
- ),
435
+ ...withoutUndefined((override.reliability ?? {}) as Record<string, unknown>),
465
436
  retryPolicy:
466
- base.reliability?.retryPolicy ||
467
- override.reliability?.retryPolicy
437
+ base.reliability?.retryPolicy || override.reliability?.retryPolicy
468
438
  ? {
469
439
  ...(base.reliability?.retryPolicy ?? {}),
470
- ...withoutUndefined(
471
- (override.reliability?.retryPolicy ??
472
- {}) as Record<string, unknown>,
473
- ),
440
+ ...withoutUndefined((override.reliability?.retryPolicy ?? {}) as Record<string, unknown>),
474
441
  }
475
442
  : undefined,
476
443
  };
@@ -478,16 +445,13 @@ function mergeConfig(
478
445
  if (base.otlp || override.otlp) {
479
446
  merged.otlp = {
480
447
  ...(base.otlp ?? {}),
481
- ...withoutUndefined(
482
- (override.otlp ?? {}) as Record<string, unknown>,
483
- ),
448
+ ...withoutUndefined((override.otlp ?? {}) as Record<string, unknown>),
484
449
  headers: {
485
450
  ...(base.otlp?.headers ?? {}),
486
451
  ...(override.otlp?.headers ?? {}),
487
452
  },
488
453
  };
489
- if (Object.keys(merged.otlp.headers ?? {}).length === 0)
490
- delete merged.otlp.headers;
454
+ if (Object.keys(merged.otlp.headers ?? {}).length === 0) delete merged.otlp.headers;
491
455
  // Validate OTLP headers for injection attacks:
492
456
  // - Check top-level keys for dangerous prototype pollution patterns
493
457
  // - Block ALL control characters except tab (0x09) to prevent header
@@ -506,22 +470,23 @@ function mergeConfig(
506
470
  if (DANGEROUS_OBJECT_KEYS.has(lowerKey)) return true;
507
471
  return false;
508
472
  };
509
- if (checkKey(k)) { invalidHeaders.push(k); continue; }
473
+ if (checkKey(k)) {
474
+ invalidHeaders.push(k);
475
+ continue;
476
+ }
510
477
  // Block any control characters except tab (0x09) in values.
511
478
  // Round 28 fix: /[\x00-\x08\x0a-\x1f]/ blocks LF (0x0A) and CR (0x0D) too.
512
479
  const valStr = String(v);
513
- if (/[\x00-\x08\x0a-\x1f]/.test(valStr)) { invalidHeaders.push(k); }
480
+ if (/[\x00-\x08\x0a-\x1f]/.test(valStr)) {
481
+ invalidHeaders.push(k);
482
+ }
514
483
  }
515
484
  if (invalidHeaders.length > 0) {
516
485
  delete merged.otlp.headers;
517
486
  warnings.push(`OTLP headers blocked due to invalid characters: ${invalidHeaders.join(", ")}`);
518
487
  }
519
488
  }
520
- if (
521
- merged.agents?.overrides &&
522
- Object.keys(merged.agents.overrides).length === 0
523
- )
524
- delete merged.agents.overrides;
489
+ if (merged.agents?.overrides && Object.keys(merged.agents.overrides).length === 0) delete merged.agents.overrides;
525
490
  return merged;
526
491
  }
527
492
 
@@ -580,8 +545,7 @@ function sanitizeObject(obj: Record<string, unknown>): Record<string, unknown> {
580
545
  }
581
546
 
582
547
  export function asRecord(value: unknown): Record<string, unknown> | undefined {
583
- if (!value || typeof value !== "object" || Array.isArray(value))
584
- return undefined;
548
+ if (!value || typeof value !== "object" || Array.isArray(value)) return undefined;
585
549
  // Defensive: create a sanitized copy to prevent prototype pollution.
586
550
  // Uses Object.create(null) so the result has no prototype chain.
587
551
  // WARNING: The returned object has no prototype methods (no hasOwnProperty,
@@ -590,11 +554,7 @@ export function asRecord(value: unknown): Record<string, unknown> | undefined {
590
554
  return sanitizeObject(value as Record<string, unknown>);
591
555
  }
592
556
 
593
- function parseWithSchema<T extends TSchema>(
594
- schema: T,
595
- value: unknown,
596
- context?: string,
597
- ): Static<T> | undefined {
557
+ function parseWithSchema<T extends TSchema>(schema: T, value: unknown, context?: string): Static<T> | undefined {
598
558
  if (!Value.Check(schema, value)) {
599
559
  if (context) {
600
560
  logInternalError("config.parseWithSchema", undefined, `${context}: schema validation failed`);
@@ -604,18 +564,11 @@ function parseWithSchema<T extends TSchema>(
604
564
  return Value.Decode(schema, value);
605
565
  }
606
566
 
607
- function parseIntegerInRange(
608
- value: unknown,
609
- minimum = 1,
610
- maximum = Number.MAX_SAFE_INTEGER,
611
- ): number | undefined {
567
+ function parseIntegerInRange(value: unknown, minimum = 1, maximum = Number.MAX_SAFE_INTEGER): number | undefined {
612
568
  return parseWithSchema(Type.Integer({ minimum, maximum }), value);
613
569
  }
614
570
 
615
- function parsePositiveInteger(
616
- value: unknown,
617
- max = Number.MAX_SAFE_INTEGER,
618
- ): number | undefined {
571
+ function parsePositiveInteger(value: unknown, max = Number.MAX_SAFE_INTEGER): number | undefined {
619
572
  return parseIntegerInRange(value, 1, max);
620
573
  }
621
574
 
@@ -626,34 +579,21 @@ function parseProfile(value: unknown): PiTeamsAutonomyProfile | undefined {
626
579
  function parseStringList(value: unknown): string[] | undefined {
627
580
  const items = parseWithSchema(Type.Array(Type.String()), value);
628
581
  if (!items || items.length === 0) return undefined;
629
- const normalized = items
630
- .map((entry) => entry.trim())
631
- .filter((entry) => entry.length > 0);
582
+ const normalized = items.map((entry) => entry.trim()).filter((entry) => entry.length > 0);
632
583
  return normalized.length > 0 ? normalized : undefined;
633
584
  }
634
585
 
635
586
  function parseStringArrayOrFalse(value: unknown): string[] | false | undefined {
636
587
  if (value === false) return false;
637
- if (typeof value === "string")
638
- return value.trim() === "" ? [] : parseStringList(value.split(","));
588
+ if (typeof value === "string") return value.trim() === "" ? [] : parseStringList(value.split(","));
639
589
  return parseStringList(value);
640
590
  }
641
591
 
642
592
  export function effectiveAutonomousConfig(
643
593
  config: PiTeamsAutonomousConfig | undefined,
644
- ): Required<
645
- Pick<
646
- PiTeamsAutonomousConfig,
647
- | "profile"
648
- | "enabled"
649
- | "injectPolicy"
650
- | "preferAsyncForLongTasks"
651
- | "allowWorktreeSuggestion"
652
- >
653
- > &
594
+ ): Required<Pick<PiTeamsAutonomousConfig, "profile" | "enabled" | "injectPolicy" | "preferAsyncForLongTasks" | "allowWorktreeSuggestion">> &
654
595
  Pick<PiTeamsAutonomousConfig, "magicKeywords"> {
655
- const profile =
656
- config?.enabled === false ? "manual" : (config?.profile ?? "suggested");
596
+ const profile = config?.enabled === false ? "manual" : (config?.profile ?? "suggested");
657
597
  const profileDefaults: Record<
658
598
  PiTeamsAutonomyProfile,
659
599
  {
@@ -693,21 +633,14 @@ export function effectiveAutonomousConfig(
693
633
  profile,
694
634
  enabled: config?.enabled ?? defaults.enabled,
695
635
  injectPolicy: config?.injectPolicy ?? defaults.injectPolicy,
696
- preferAsyncForLongTasks:
697
- config?.preferAsyncForLongTasks ?? defaults.preferAsyncForLongTasks,
698
- allowWorktreeSuggestion:
699
- config?.allowWorktreeSuggestion ?? defaults.allowWorktreeSuggestion,
636
+ preferAsyncForLongTasks: config?.preferAsyncForLongTasks ?? defaults.preferAsyncForLongTasks,
637
+ allowWorktreeSuggestion: config?.allowWorktreeSuggestion ?? defaults.allowWorktreeSuggestion,
700
638
  magicKeywords: config?.magicKeywords,
701
639
  };
702
640
  }
703
641
 
704
- function parseStringArrayRecord(
705
- value: unknown,
706
- ): Record<string, string[]> | undefined {
707
- const record = parseWithSchema(
708
- Type.Record(Type.String({ minLength: 1 }), Type.Array(Type.String())),
709
- value,
710
- );
642
+ function parseStringArrayRecord(value: unknown): Record<string, string[]> | undefined {
643
+ const record = parseWithSchema(Type.Record(Type.String({ minLength: 1 }), Type.Array(Type.String())), value);
711
644
  if (!record) return undefined;
712
645
  const result: Record<string, string[]> = {};
713
646
  for (const [key, rawValues] of Object.entries(record)) {
@@ -717,87 +650,42 @@ function parseStringArrayRecord(
717
650
  return Object.keys(result).length > 0 ? result : undefined;
718
651
  }
719
652
 
720
- function parseAutonomousConfig(
721
- value: unknown,
722
- ): PiTeamsAutonomousConfig | undefined {
653
+ function parseAutonomousConfig(value: unknown): PiTeamsAutonomousConfig | undefined {
723
654
  const obj = asRecord(value);
724
655
  if (!obj) return undefined;
725
656
  const config: PiTeamsAutonomousConfig = {
726
657
  profile: parseProfile(obj.profile),
727
658
  enabled: parseWithSchema(Type.Boolean(), obj.enabled),
728
659
  injectPolicy: parseWithSchema(Type.Boolean(), obj.injectPolicy),
729
- preferAsyncForLongTasks: parseWithSchema(
730
- Type.Boolean(),
731
- obj.preferAsyncForLongTasks,
732
- ),
733
- allowWorktreeSuggestion: parseWithSchema(
734
- Type.Boolean(),
735
- obj.allowWorktreeSuggestion,
736
- ),
660
+ preferAsyncForLongTasks: parseWithSchema(Type.Boolean(), obj.preferAsyncForLongTasks),
661
+ allowWorktreeSuggestion: parseWithSchema(Type.Boolean(), obj.allowWorktreeSuggestion),
737
662
  magicKeywords: parseStringArrayRecord(obj.magicKeywords),
738
663
  };
739
- return Object.values(config).some((entry) => entry !== undefined)
740
- ? config
741
- : undefined;
664
+ return Object.values(config).some((entry) => entry !== undefined) ? config : undefined;
742
665
  }
743
666
 
744
667
  function parseLimitsConfig(value: unknown): CrewLimitsConfig | undefined {
745
668
  const obj = asRecord(value);
746
669
  if (!obj) return undefined;
747
670
  const limits: CrewLimitsConfig = {
748
- maxConcurrentWorkers: parsePositiveInteger(
749
- obj.maxConcurrentWorkers,
750
- LIMIT_CEILINGS.maxConcurrentWorkers,
751
- ),
752
- allowUnboundedConcurrency: parseWithSchema(
753
- Type.Boolean(),
754
- obj.allowUnboundedConcurrency,
755
- ),
756
- maxTaskDepth: parsePositiveInteger(
757
- obj.maxTaskDepth,
758
- LIMIT_CEILINGS.maxTaskDepth,
759
- ),
760
- maxChildrenPerTask: parsePositiveInteger(
761
- obj.maxChildrenPerTask,
762
- LIMIT_CEILINGS.maxChildrenPerTask,
763
- ),
764
- maxRunMinutes: parsePositiveInteger(
765
- obj.maxRunMinutes,
766
- LIMIT_CEILINGS.maxRunMinutes,
767
- ),
768
- maxRetriesPerTask: parsePositiveInteger(
769
- obj.maxRetriesPerTask,
770
- LIMIT_CEILINGS.maxRetriesPerTask,
771
- ),
772
- maxTasksPerRun: parsePositiveInteger(
773
- obj.maxTasksPerRun,
774
- LIMIT_CEILINGS.maxTasksPerRun,
775
- ),
776
- heartbeatStaleMs: parsePositiveInteger(
777
- obj.heartbeatStaleMs,
778
- LIMIT_CEILINGS.heartbeatStaleMs,
779
- ),
671
+ maxConcurrentWorkers: parsePositiveInteger(obj.maxConcurrentWorkers, LIMIT_CEILINGS.maxConcurrentWorkers),
672
+ allowUnboundedConcurrency: parseWithSchema(Type.Boolean(), obj.allowUnboundedConcurrency),
673
+ maxTaskDepth: parsePositiveInteger(obj.maxTaskDepth, LIMIT_CEILINGS.maxTaskDepth),
674
+ maxChildrenPerTask: parsePositiveInteger(obj.maxChildrenPerTask, LIMIT_CEILINGS.maxChildrenPerTask),
675
+ maxRunMinutes: parsePositiveInteger(obj.maxRunMinutes, LIMIT_CEILINGS.maxRunMinutes),
676
+ maxRetriesPerTask: parsePositiveInteger(obj.maxRetriesPerTask, LIMIT_CEILINGS.maxRetriesPerTask),
677
+ maxTasksPerRun: parsePositiveInteger(obj.maxTasksPerRun, LIMIT_CEILINGS.maxTasksPerRun),
678
+ heartbeatStaleMs: parsePositiveInteger(obj.heartbeatStaleMs, LIMIT_CEILINGS.heartbeatStaleMs),
780
679
  };
781
- return Object.values(limits).some((entry) => entry !== undefined)
782
- ? limits
783
- : undefined;
680
+ return Object.values(limits).some((entry) => entry !== undefined) ? limits : undefined;
784
681
  }
785
682
 
786
- function parseIsolationPolicy(
787
- value: unknown,
788
- ): CrewRuntimeConfig["isolationPolicy"] | undefined {
683
+ function parseIsolationPolicy(value: unknown): CrewRuntimeConfig["isolationPolicy"] | undefined {
789
684
  const obj = asRecord(value);
790
685
  if (!obj) return undefined;
791
686
  const isolatedRoles = parseStringList(obj.isolatedRoles);
792
- const defaultRuntime = parseWithSchema(
793
- Type.Union([
794
- Type.Literal("live-session"),
795
- Type.Literal("child-process"),
796
- ]),
797
- obj.defaultRuntime,
798
- );
799
- if (isolatedRoles === undefined && defaultRuntime === undefined)
800
- return undefined;
687
+ const defaultRuntime = parseWithSchema(Type.Union([Type.Literal("live-session"), Type.Literal("child-process")]), obj.defaultRuntime);
688
+ if (isolatedRoles === undefined && defaultRuntime === undefined) return undefined;
801
689
  return {
802
690
  ...(isolatedRoles !== undefined ? { isolatedRoles } : {}),
803
691
  ...(defaultRuntime !== undefined ? { defaultRuntime } : {}),
@@ -809,73 +697,29 @@ function parseRuntimeConfig(value: unknown): CrewRuntimeConfig | undefined {
809
697
  if (!obj) return undefined;
810
698
  const runtime: CrewRuntimeConfig = {
811
699
  mode: parseWithSchema(
812
- Type.Union([
813
- Type.Literal("auto"),
814
- Type.Literal("scaffold"),
815
- Type.Literal("child-process"),
816
- Type.Literal("live-session"),
817
- ]),
700
+ Type.Union([Type.Literal("auto"), Type.Literal("scaffold"), Type.Literal("child-process"), Type.Literal("live-session")]),
818
701
  obj.mode,
819
702
  ),
820
- preferLiveSession: parseWithSchema(
821
- Type.Boolean(),
822
- obj.preferLiveSession,
823
- ),
824
- allowChildProcessFallback: parseWithSchema(
825
- Type.Boolean(),
826
- obj.allowChildProcessFallback,
827
- ),
828
- maxTurns: parsePositiveInteger(
829
- obj.maxTurns,
830
- LIMIT_CEILINGS.runtimeMaxTurns,
831
- ),
832
- graceTurns: parsePositiveInteger(
833
- obj.graceTurns,
834
- LIMIT_CEILINGS.runtimeGraceTurns,
835
- ),
703
+ preferLiveSession: parseWithSchema(Type.Boolean(), obj.preferLiveSession),
704
+ allowChildProcessFallback: parseWithSchema(Type.Boolean(), obj.allowChildProcessFallback),
705
+ maxTurns: parsePositiveInteger(obj.maxTurns, LIMIT_CEILINGS.runtimeMaxTurns),
706
+ graceTurns: parsePositiveInteger(obj.graceTurns, LIMIT_CEILINGS.runtimeGraceTurns),
836
707
  inheritContext: parseWithSchema(Type.Boolean(), obj.inheritContext),
837
- promptMode: parseWithSchema(
838
- Type.Union([Type.Literal("replace"), Type.Literal("append")]),
839
- obj.promptMode,
840
- ),
841
- groupJoin: parseWithSchema(
842
- Type.Union([
843
- Type.Literal("off"),
844
- Type.Literal("group"),
845
- Type.Literal("smart"),
846
- ]),
847
- obj.groupJoin,
848
- ),
849
- groupJoinAckTimeoutMs: parsePositiveInteger(
850
- obj.groupJoinAckTimeoutMs,
851
- 86_400_000,
852
- ),
853
- requirePlanApproval: parseWithSchema(
854
- Type.Boolean(),
855
- obj.requirePlanApproval,
856
- ),
708
+ promptMode: parseWithSchema(Type.Union([Type.Literal("replace"), Type.Literal("append")]), obj.promptMode),
709
+ groupJoin: parseWithSchema(Type.Union([Type.Literal("off"), Type.Literal("group"), Type.Literal("smart")]), obj.groupJoin),
710
+ groupJoinAckTimeoutMs: parsePositiveInteger(obj.groupJoinAckTimeoutMs, 86_400_000),
711
+ requirePlanApproval: parseWithSchema(Type.Boolean(), obj.requirePlanApproval),
857
712
  completionMutationGuard: parseWithSchema(
858
- Type.Union([
859
- Type.Literal("off"),
860
- Type.Literal("warn"),
861
- Type.Literal("fail"),
862
- ]),
713
+ Type.Union([Type.Literal("off"), Type.Literal("warn"), Type.Literal("fail")]),
863
714
  obj.completionMutationGuard,
864
715
  ),
865
716
  effectivenessGuard: parseWithSchema(
866
- Type.Union([
867
- Type.Literal("off"),
868
- Type.Literal("warn"),
869
- Type.Literal("block"),
870
- Type.Literal("fail"),
871
- ]),
717
+ Type.Union([Type.Literal("off"), Type.Literal("warn"), Type.Literal("block"), Type.Literal("fail")]),
872
718
  obj.effectivenessGuard,
873
719
  ),
874
720
  isolationPolicy: parseIsolationPolicy(obj.isolationPolicy),
875
721
  };
876
- return Object.values(runtime).some((entry) => entry !== undefined)
877
- ? runtime
878
- : undefined;
722
+ return Object.values(runtime).some((entry) => entry !== undefined) ? runtime : undefined;
879
723
  }
880
724
 
881
725
  function parseControlConfig(value: unknown): CrewControlConfig | undefined {
@@ -885,9 +729,7 @@ function parseControlConfig(value: unknown): CrewControlConfig | undefined {
885
729
  enabled: parseWithSchema(Type.Boolean(), obj.enabled),
886
730
  needsAttentionAfterMs: parsePositiveInteger(obj.needsAttentionAfterMs),
887
731
  };
888
- return Object.values(control).some((entry) => entry !== undefined)
889
- ? control
890
- : undefined;
732
+ return Object.values(control).some((entry) => entry !== undefined) ? control : undefined;
891
733
  }
892
734
 
893
735
  function parseWorktreeConfig(value: unknown): CrewWorktreeConfig | undefined {
@@ -897,21 +739,14 @@ function parseWorktreeConfig(value: unknown): CrewWorktreeConfig | undefined {
897
739
  const setupHook = rawSetupHook?.trim();
898
740
  const worktree: CrewWorktreeConfig = {
899
741
  setupHook: setupHook ? setupHook : undefined,
900
- setupHookTimeoutMs: parsePositiveInteger(
901
- obj.setupHookTimeoutMs,
902
- 300_000,
903
- ),
742
+ setupHookTimeoutMs: parsePositiveInteger(obj.setupHookTimeoutMs, 300_000),
904
743
  linkNodeModules: parseWithSchema(Type.Boolean(), obj.linkNodeModules),
905
744
  };
906
- return Object.values(worktree).some((entry) => entry !== undefined)
907
- ? worktree
908
- : undefined;
745
+ return Object.values(worktree).some((entry) => entry !== undefined) ? worktree : undefined;
909
746
  }
910
747
 
911
748
  /** Parse goalWrap config (RFC v0.5 vision: apply goal completion-guarantee to builtins). */
912
- function parseGoalWrapConfig(
913
- value: unknown,
914
- ): Record<string, GoalWrapWorkflowConfig> | undefined {
749
+ function parseGoalWrapConfig(value: unknown): Record<string, GoalWrapWorkflowConfig> | undefined {
915
750
  const obj = asRecord(value);
916
751
  if (!obj) return undefined;
917
752
  const result: Record<string, GoalWrapWorkflowConfig> = {};
@@ -932,9 +767,12 @@ function parseGoalWrapConfig(
932
767
  const commands = Array.isArray(verObj.commands)
933
768
  ? verObj.commands.filter((c): c is string => typeof c === "string" && c.length > 0)
934
769
  : undefined;
935
- const mode = verObj.mode === "text-only" ? "text-only" as const : undefined;
770
+ const mode = verObj.mode === "text-only" ? ("text-only" as const) : undefined;
936
771
  if (commands || mode) {
937
- parsed.verification = { ...(commands ? { commands } : { commands: [] }), ...(mode ? { mode } : {}) };
772
+ parsed.verification = {
773
+ ...(commands ? { commands } : { commands: [] }),
774
+ ...(mode ? { mode } : {}),
775
+ };
938
776
  }
939
777
  }
940
778
  if (Object.values(parsed).some((v) => v !== undefined)) {
@@ -950,65 +788,34 @@ function parseAgentOverride(value: unknown): AgentOverrideConfig | undefined {
950
788
  if (!obj) return undefined;
951
789
  const override: AgentOverrideConfig = {
952
790
  disabled: parseWithSchema(Type.Boolean(), obj.disabled),
953
- model: parseWithSchema(
954
- Type.Union([Type.String(), Type.Literal(false)]),
955
- obj.model,
956
- ),
791
+ model: parseWithSchema(Type.Union([Type.String(), Type.Literal(false)]), obj.model),
957
792
  fallbackModels: parseStringArrayOrFalse(obj.fallbackModels),
958
- thinking: parseWithSchema(
959
- Type.Union([Type.String(), Type.Literal(false)]),
960
- obj.thinking,
961
- ),
793
+ thinking: parseWithSchema(Type.Union([Type.String(), Type.Literal(false)]), obj.thinking),
962
794
  tools: parseStringArrayOrFalse(obj.tools),
963
795
  skills: parseStringArrayOrFalse(obj.skills),
964
796
  };
965
- return Object.values(override).some((entry) => entry !== undefined)
966
- ? override
967
- : undefined;
797
+ return Object.values(override).some((entry) => entry !== undefined) ? override : undefined;
968
798
  }
969
799
 
970
800
  function parseUiConfig(value: unknown): CrewUiConfig | undefined {
971
801
  const obj = asRecord(value);
972
802
  if (!obj) return undefined;
973
- const rawWidgetPlacement = parseWithSchema(
974
- Type.Union([Type.Literal("aboveEditor"), Type.Literal("belowEditor")]),
975
- obj.widgetPlacement,
976
- );
977
- const rawDashboardPlacement = parseWithSchema(
978
- Type.Union([Type.Literal("center"), Type.Literal("right")]),
979
- obj.dashboardPlacement,
980
- );
803
+ const rawWidgetPlacement = parseWithSchema(Type.Union([Type.Literal("aboveEditor"), Type.Literal("belowEditor")]), obj.widgetPlacement);
804
+ const rawDashboardPlacement = parseWithSchema(Type.Union([Type.Literal("center"), Type.Literal("right")]), obj.dashboardPlacement);
981
805
  const ui: CrewUiConfig = {
982
806
  widgetPlacement: rawWidgetPlacement,
983
807
  widgetMaxLines: parsePositiveInteger(obj.widgetMaxLines, 50),
984
808
  powerbar: parseWithSchema(Type.Boolean(), obj.powerbar),
985
809
  dashboardPlacement: rawDashboardPlacement,
986
810
  dashboardWidth: parseIntegerInRange(obj.dashboardWidth, 32, 120),
987
- dashboardLiveRefreshMs: parseIntegerInRange(
988
- obj.dashboardLiveRefreshMs,
989
- 250,
990
- 60_000,
991
- ),
992
- autoOpenDashboard: parseWithSchema(
993
- Type.Boolean(),
994
- obj.autoOpenDashboard,
995
- ),
996
- autoOpenDashboardForForegroundRuns: parseWithSchema(
997
- Type.Boolean(),
998
- obj.autoOpenDashboardForForegroundRuns,
999
- ),
811
+ dashboardLiveRefreshMs: parseIntegerInRange(obj.dashboardLiveRefreshMs, 250, 60_000),
812
+ autoOpenDashboard: parseWithSchema(Type.Boolean(), obj.autoOpenDashboard),
813
+ autoOpenDashboardForForegroundRuns: parseWithSchema(Type.Boolean(), obj.autoOpenDashboardForForegroundRuns),
1000
814
  showModel: parseWithSchema(Type.Boolean(), obj.showModel),
1001
815
  showTokens: parseWithSchema(Type.Boolean(), obj.showTokens),
1002
816
  showTools: parseWithSchema(Type.Boolean(), obj.showTools),
1003
- transcriptTailBytes: parseIntegerInRange(
1004
- obj.transcriptTailBytes,
1005
- 1024,
1006
- 50 * 1024 * 1024,
1007
- ),
1008
- mascotStyle: parseWithSchema(
1009
- Type.Union([Type.Literal("cat"), Type.Literal("armin")]),
1010
- obj.mascotStyle,
1011
- ),
817
+ transcriptTailBytes: parseIntegerInRange(obj.transcriptTailBytes, 1024, 50 * 1024 * 1024),
818
+ mascotStyle: parseWithSchema(Type.Union([Type.Literal("cat"), Type.Literal("armin")]), obj.mascotStyle),
1012
819
  mascotEffect: parseWithSchema(
1013
820
  Type.Union([
1014
821
  Type.Literal("random"),
@@ -1024,23 +831,15 @@ function parseUiConfig(value: unknown): CrewUiConfig | undefined {
1024
831
  obj.mascotEffect,
1025
832
  ),
1026
833
  };
1027
- return Object.values(ui).some((entry) => entry !== undefined)
1028
- ? ui
1029
- : undefined;
834
+ return Object.values(ui).some((entry) => entry !== undefined) ? ui : undefined;
1030
835
  }
1031
836
 
1032
837
  function parseAgentsConfig(value: unknown): CrewAgentsConfig | undefined {
1033
838
  const obj = asRecord(value);
1034
839
  if (!obj) return undefined;
1035
840
  const overrides: Record<string, AgentOverrideConfig> = {};
1036
- if (
1037
- obj.overrides &&
1038
- typeof obj.overrides === "object" &&
1039
- !Array.isArray(obj.overrides)
1040
- ) {
1041
- for (const [name, rawOverride] of Object.entries(
1042
- obj.overrides as Record<string, unknown>,
1043
- )) {
841
+ if (obj.overrides && typeof obj.overrides === "object" && !Array.isArray(obj.overrides)) {
842
+ for (const [name, rawOverride] of Object.entries(obj.overrides as Record<string, unknown>)) {
1044
843
  const parsed = parseAgentOverride(rawOverride);
1045
844
  if (parsed && name.trim()) overrides[name.trim()] = parsed;
1046
845
  }
@@ -1049,28 +848,18 @@ function parseAgentsConfig(value: unknown): CrewAgentsConfig | undefined {
1049
848
  disableBuiltins: parseWithSchema(Type.Boolean(), obj.disableBuiltins),
1050
849
  overrides: Object.keys(overrides).length > 0 ? overrides : undefined,
1051
850
  };
1052
- return Object.values(agents).some((entry) => entry !== undefined)
1053
- ? agents
1054
- : undefined;
851
+ return Object.values(agents).some((entry) => entry !== undefined) ? agents : undefined;
1055
852
  }
1056
853
 
1057
854
  function parseToolsConfig(value: unknown): CrewToolsConfig | undefined {
1058
855
  const obj = asRecord(value);
1059
856
  if (!obj) return undefined;
1060
857
  const tools: CrewToolsConfig = {
1061
- enableClaudeStyleAliases: parseWithSchema(
1062
- Type.Boolean(),
1063
- obj.enableClaudeStyleAliases,
1064
- ),
858
+ enableClaudeStyleAliases: parseWithSchema(Type.Boolean(), obj.enableClaudeStyleAliases),
1065
859
  enableSteer: parseWithSchema(Type.Boolean(), obj.enableSteer),
1066
- terminateOnForeground: parseWithSchema(
1067
- Type.Boolean(),
1068
- obj.terminateOnForeground,
1069
- ),
860
+ terminateOnForeground: parseWithSchema(Type.Boolean(), obj.terminateOnForeground),
1070
861
  };
1071
- return Object.values(tools).some((entry) => entry !== undefined)
1072
- ? tools
1073
- : undefined;
862
+ return Object.values(tools).some((entry) => entry !== undefined) ? tools : undefined;
1074
863
  }
1075
864
 
1076
865
  function parseTelemetryConfig(value: unknown): CrewTelemetryConfig | undefined {
@@ -1079,125 +868,68 @@ function parseTelemetryConfig(value: unknown): CrewTelemetryConfig | undefined {
1079
868
  const telemetry: CrewTelemetryConfig = {
1080
869
  enabled: parseWithSchema(Type.Boolean(), obj.enabled),
1081
870
  };
1082
- return Object.values(telemetry).some((entry) => entry !== undefined)
1083
- ? telemetry
1084
- : undefined;
871
+ return Object.values(telemetry).some((entry) => entry !== undefined) ? telemetry : undefined;
1085
872
  }
1086
873
 
1087
874
  function parsePolicyConfig(value: unknown): CrewPolicyConfig | undefined {
1088
875
  const obj = asRecord(value);
1089
876
  if (!obj) return undefined;
1090
877
  const policy: CrewPolicyConfig = {
1091
- requireIntentForDestructiveActions: parseWithSchema(
1092
- Type.Boolean(),
1093
- obj.requireIntentForDestructiveActions,
1094
- ),
1095
- disabledCapabilities: parseWithSchema(
1096
- Type.Array(Type.String()),
1097
- obj.disabledCapabilities,
1098
- ),
878
+ requireIntentForDestructiveActions: parseWithSchema(Type.Boolean(), obj.requireIntentForDestructiveActions),
879
+ disabledCapabilities: parseWithSchema(Type.Array(Type.String()), obj.disabledCapabilities),
1099
880
  };
1100
- return Object.values(policy).some((entry) => entry !== undefined)
1101
- ? policy
1102
- : undefined;
881
+ return Object.values(policy).some((entry) => entry !== undefined) ? policy : undefined;
1103
882
  }
1104
883
 
1105
- function parseNotificationsConfig(
1106
- value: unknown,
1107
- ): CrewNotificationsConfig | undefined {
884
+ function parseNotificationsConfig(value: unknown): CrewNotificationsConfig | undefined {
1108
885
  const obj = asRecord(value);
1109
886
  if (!obj) return undefined;
1110
887
  const notifications: CrewNotificationsConfig = {
1111
888
  enabled: parseWithSchema(Type.Boolean(), obj.enabled),
1112
889
  severityFilter: parseWithSchema(
1113
- Type.Array(
1114
- Type.Union([
1115
- Type.Literal("info"),
1116
- Type.Literal("warning"),
1117
- Type.Literal("error"),
1118
- Type.Literal("critical"),
1119
- ]),
1120
- ),
890
+ Type.Array(Type.Union([Type.Literal("info"), Type.Literal("warning"), Type.Literal("error"), Type.Literal("critical")])),
1121
891
  obj.severityFilter,
1122
892
  ),
1123
- dedupWindowMs: parsePositiveInteger(
1124
- obj.dedupWindowMs,
1125
- 24 * 60 * 60 * 1000,
1126
- ),
1127
- batchWindowMs: parseWithSchema(
1128
- Type.Integer({ minimum: 0, maximum: 60_000 }),
1129
- obj.batchWindowMs,
1130
- ),
1131
- quietHours: parseWithSchema(
1132
- Type.String({ pattern: "^\\d{2}:\\d{2}-\\d{2}:\\d{2}$" }),
1133
- obj.quietHours,
1134
- ),
893
+ dedupWindowMs: parsePositiveInteger(obj.dedupWindowMs, 24 * 60 * 60 * 1000),
894
+ batchWindowMs: parseWithSchema(Type.Integer({ minimum: 0, maximum: 60_000 }), obj.batchWindowMs),
895
+ quietHours: parseWithSchema(Type.String({ pattern: "^\\d{2}:\\d{2}-\\d{2}:\\d{2}$" }), obj.quietHours),
1135
896
  sinkRetentionDays: parsePositiveInteger(obj.sinkRetentionDays, 90),
1136
897
  };
1137
- return Object.values(notifications).some((entry) => entry !== undefined)
1138
- ? notifications
1139
- : undefined;
898
+ return Object.values(notifications).some((entry) => entry !== undefined) ? notifications : undefined;
1140
899
  }
1141
900
 
1142
- function parseObservabilityConfig(
1143
- value: unknown,
1144
- ): CrewObservabilityConfig | undefined {
901
+ function parseObservabilityConfig(value: unknown): CrewObservabilityConfig | undefined {
1145
902
  const obj = asRecord(value);
1146
903
  if (!obj) return undefined;
1147
904
  const observability: CrewObservabilityConfig = {
1148
905
  enabled: parseWithSchema(Type.Boolean(), obj.enabled),
1149
- pollIntervalMs: parseWithSchema(
1150
- Type.Integer({ minimum: 1000, maximum: 60_000 }),
1151
- obj.pollIntervalMs,
1152
- ),
906
+ pollIntervalMs: parseWithSchema(Type.Integer({ minimum: 1000, maximum: 60_000 }), obj.pollIntervalMs),
1153
907
  metricRetentionDays: parsePositiveInteger(obj.metricRetentionDays, 365),
1154
908
  };
1155
- return Object.values(observability).some((entry) => entry !== undefined)
1156
- ? observability
1157
- : undefined;
909
+ return Object.values(observability).some((entry) => entry !== undefined) ? observability : undefined;
1158
910
  }
1159
911
 
1160
- function parseReliabilityConfig(
1161
- value: unknown,
1162
- ): CrewReliabilityConfig | undefined {
912
+ function parseReliabilityConfig(value: unknown): CrewReliabilityConfig | undefined {
1163
913
  const obj = asRecord(value);
1164
914
  if (!obj) return undefined;
1165
915
  const retryObj = asRecord(obj.retryPolicy);
1166
916
  const retryPolicy: CrewRetryPolicyConfig | undefined = retryObj
1167
917
  ? {
1168
918
  maxAttempts: parsePositiveInteger(retryObj.maxAttempts, 10),
1169
- backoffMs: parseWithSchema(
1170
- Type.Integer({ minimum: 100, maximum: 60_000 }),
1171
- retryObj.backoffMs,
1172
- ),
1173
- jitterRatio: parseWithSchema(
1174
- Type.Number({ minimum: 0, maximum: 1 }),
1175
- retryObj.jitterRatio,
1176
- ),
1177
- exponentialFactor: parseWithSchema(
1178
- Type.Number({ minimum: 1, maximum: 5 }),
1179
- retryObj.exponentialFactor,
1180
- ),
919
+ backoffMs: parseWithSchema(Type.Integer({ minimum: 100, maximum: 60_000 }), retryObj.backoffMs),
920
+ jitterRatio: parseWithSchema(Type.Number({ minimum: 0, maximum: 1 }), retryObj.jitterRatio),
921
+ exponentialFactor: parseWithSchema(Type.Number({ minimum: 1, maximum: 5 }), retryObj.exponentialFactor),
1181
922
  retryableErrors: parseStringList(retryObj.retryableErrors),
1182
923
  }
1183
924
  : undefined;
1184
925
  const reliability: CrewReliabilityConfig = {
1185
926
  autoRetry: parseWithSchema(Type.Boolean(), obj.autoRetry),
1186
- retryPolicy:
1187
- retryPolicy &&
1188
- Object.values(retryPolicy).some((entry) => entry !== undefined)
1189
- ? retryPolicy
1190
- : undefined,
927
+ retryPolicy: retryPolicy && Object.values(retryPolicy).some((entry) => entry !== undefined) ? retryPolicy : undefined,
1191
928
  autoRecover: parseWithSchema(Type.Boolean(), obj.autoRecover),
1192
929
  deadletterThreshold: parsePositiveInteger(obj.deadletterThreshold),
1193
- cleanupOrphanedTempDirs: parseWithSchema(
1194
- Type.Boolean(),
1195
- obj.cleanupOrphanedTempDirs,
1196
- ),
930
+ cleanupOrphanedTempDirs: parseWithSchema(Type.Boolean(), obj.cleanupOrphanedTempDirs),
1197
931
  };
1198
- return Object.values(reliability).some((entry) => entry !== undefined)
1199
- ? reliability
1200
- : undefined;
932
+ return Object.values(reliability).some((entry) => entry !== undefined) ? reliability : undefined;
1201
933
  }
1202
934
 
1203
935
  function parseOtlpConfig(value: unknown): CrewOtlpConfig | undefined {
@@ -1236,14 +968,9 @@ function parseOtlpConfig(value: unknown): CrewOtlpConfig | undefined {
1236
968
  enabled: parseWithSchema(Type.Boolean(), obj.enabled),
1237
969
  endpoint: parseWithSchema(Type.String({ minLength: 1 }), obj.endpoint),
1238
970
  headers: Object.keys(headers).length > 0 ? headers : undefined,
1239
- intervalMs: parseWithSchema(
1240
- Type.Integer({ minimum: 5000 }),
1241
- obj.intervalMs,
1242
- ),
971
+ intervalMs: parseWithSchema(Type.Integer({ minimum: 5000 }), obj.intervalMs),
1243
972
  };
1244
- return Object.values(otlp).some((entry) => entry !== undefined)
1245
- ? otlp
1246
- : undefined;
973
+ return Object.values(otlp).some((entry) => entry !== undefined) ? otlp : undefined;
1247
974
  }
1248
975
 
1249
976
  export function parseConfig(raw: unknown): PiTeamsConfig {
@@ -1252,14 +979,8 @@ export function parseConfig(raw: unknown): PiTeamsConfig {
1252
979
  return {
1253
980
  asyncByDefault: parseWithSchema(Type.Boolean(), obj.asyncByDefault),
1254
981
  executeWorkers: parseWithSchema(Type.Boolean(), obj.executeWorkers),
1255
- notifierIntervalMs: parseWithSchema(
1256
- Type.Number({ minimum: 1_000 }),
1257
- obj.notifierIntervalMs,
1258
- ),
1259
- requireCleanWorktreeLeader: parseWithSchema(
1260
- Type.Boolean(),
1261
- obj.requireCleanWorktreeLeader,
1262
- ),
982
+ notifierIntervalMs: parseWithSchema(Type.Number({ minimum: 1_000 }), obj.notifierIntervalMs),
983
+ requireCleanWorktreeLeader: parseWithSchema(Type.Boolean(), obj.requireCleanWorktreeLeader),
1263
984
  autonomous: parseAutonomousConfig(obj.autonomous),
1264
985
  limits: parseLimitsConfig(obj.limits),
1265
986
  runtime: parseRuntimeConfig(obj.runtime),
@@ -1279,8 +1000,7 @@ export function parseConfig(raw: unknown): PiTeamsConfig {
1279
1000
  }
1280
1001
 
1281
1002
  export function parseConfigWithWarnings(raw: unknown): ConfigValidationResult {
1282
- if (!raw || typeof raw !== "object" || Array.isArray(raw))
1283
- return { config: {}, warnings: [] };
1003
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return { config: {}, warnings: [] };
1284
1004
  const parsed = parseConfig(raw);
1285
1005
  const warnings = validateConfigWithWarnings(raw as Record<string, unknown>);
1286
1006
  return { config: parsed, warnings };
@@ -1292,8 +1012,7 @@ function unsetPath(record: Record<string, unknown>, dottedPath: string): void {
1292
1012
  let target: Record<string, unknown> = record;
1293
1013
  for (const part of parts.slice(0, -1)) {
1294
1014
  const current = target[part];
1295
- if (!current || typeof current !== "object" || Array.isArray(current))
1296
- return;
1015
+ if (!current || typeof current !== "object" || Array.isArray(current)) return;
1297
1016
  target = current as Record<string, unknown>;
1298
1017
  }
1299
1018
  delete target[parts[parts.length - 1]!];
@@ -1333,17 +1052,14 @@ function readOptionalConfig(filePath: string): {
1333
1052
  config: PiTeamsConfig;
1334
1053
  warnings: string[];
1335
1054
  } {
1336
- if (!fs.existsSync(filePath))
1337
- return { exists: false, config: {}, warnings: [] };
1055
+ if (!fs.existsSync(filePath)) return { exists: false, config: {}, warnings: [] };
1338
1056
  try {
1339
1057
  const raw = readConfigRecord(filePath);
1340
1058
  const parsed = parseConfigWithWarnings(raw);
1341
1059
  return {
1342
1060
  exists: true,
1343
1061
  config: parsed.config,
1344
- warnings: parsed.warnings.map(
1345
- (warning) => `${filePath}: ${warning}`,
1346
- ),
1062
+ warnings: parsed.warnings.map((warning) => `${filePath}: ${warning}`),
1347
1063
  };
1348
1064
  } catch (error) {
1349
1065
  const message = error instanceof Error ? error.message : String(error);
@@ -1358,6 +1074,27 @@ function readOptionalConfig(filePath: string): {
1358
1074
  export function loadConfig(cwd?: string): LoadedPiTeamsConfig {
1359
1075
  const filePath = configPath();
1360
1076
  const legacyPath = legacyConfigPath();
1077
+
1078
+ // (F16) Quick-win cache: skip the expensive full-reparse on hot paths
1079
+ // (preload tick 1 Hz, per-write validation, subagent completion) when the
1080
+ // on-disk config files are unchanged. See CONFIG_CACHE_TTL_MS at the
1081
+ // top of this file for the rationale and the manifestCache pattern in
1082
+ // state-store.ts:75-130 for the canonical implementation we mirror.
1083
+ const cacheParts = readCachedConfigParts(filePath, legacyPath, cwd);
1084
+ const cacheKey = buildConfigCacheKey(cacheParts);
1085
+ const cached = configCache.get(cacheKey);
1086
+ const ttlMs = configCacheTtlMsOverride ?? CONFIG_CACHE_TTL_MS;
1087
+ if (cached && Date.now() - cached.cachedAt <= ttlMs) {
1088
+ const currentMtimes = readCacheMtimes(cacheParts);
1089
+ if (matchesCachedMtimes(cached.mtimes, currentMtimes)) {
1090
+ // Refresh insertion order so a frequently-accessed key isn't
1091
+ // evicted from the Map on the next eviction sweep.
1092
+ configCache.delete(cacheKey);
1093
+ configCache.set(cacheKey, cached);
1094
+ return cached.value;
1095
+ }
1096
+ }
1097
+
1361
1098
  const paths = cwd ? [filePath, projectConfigPath(cwd)] : [filePath];
1362
1099
  const warnings: string[] = [];
1363
1100
  const legacyConfig = readOptionalConfig(legacyPath);
@@ -1367,12 +1104,7 @@ export function loadConfig(cwd?: string): LoadedPiTeamsConfig {
1367
1104
  }
1368
1105
  const userConfig = readOptionalConfig(filePath);
1369
1106
  warnings.push(...userConfig.warnings);
1370
- let config = mergeConfig(
1371
- legacyConfig.exists && legacyPath !== filePath
1372
- ? legacyConfig.config
1373
- : {},
1374
- userConfig.config,
1375
- );
1107
+ let config = mergeConfig(legacyConfig.exists && legacyPath !== filePath ? legacyConfig.config : {}, userConfig.config);
1376
1108
  if (cwd) {
1377
1109
  const projectPath = projectConfigPath(cwd);
1378
1110
  const projectConfig = readOptionalConfig(projectPath);
@@ -1381,15 +1113,8 @@ export function loadConfig(cwd?: string): LoadedPiTeamsConfig {
1381
1113
  // Sensitive fields have already been sanitized by sanitizeProjectConfig.
1382
1114
  let effectiveConfig = {};
1383
1115
  if (projectConfig.exists) {
1384
- const projectSafeConfig = sanitizeProjectConfig(
1385
- projectPath,
1386
- config,
1387
- projectConfig.config,
1388
- );
1389
- warnings.push(
1390
- ...projectConfig.warnings,
1391
- ...projectSafeConfig.warnings,
1392
- );
1116
+ const projectSafeConfig = sanitizeProjectConfig(projectPath, config, projectConfig.config);
1117
+ warnings.push(...projectConfig.warnings, ...projectSafeConfig.warnings);
1393
1118
  // merge(base=projectConfig, override=userConfig) → override wins
1394
1119
  effectiveConfig = mergeConfig(effectiveConfig, projectSafeConfig.config);
1395
1120
  }
@@ -1397,7 +1122,6 @@ export function loadConfig(cwd?: string): LoadedPiTeamsConfig {
1397
1122
  effectiveConfig = mergeConfig(effectiveConfig, config);
1398
1123
  config = effectiveConfig;
1399
1124
 
1400
-
1401
1125
  // `.pi/pi-crew.json` is the project-owned config file.
1402
1126
  // Merge project config FIRST (base), then user config on top (override).
1403
1127
  // This ensures user preferences always take precedence over project settings.
@@ -1406,11 +1130,7 @@ export function loadConfig(cwd?: string): LoadedPiTeamsConfig {
1406
1130
  const piCrewJsonConfig = readOptionalConfig(piCrewJsonPath);
1407
1131
  if (piCrewJsonConfig.exists) {
1408
1132
  warnings.push(...piCrewJsonConfig.warnings);
1409
- const projectPart = sanitizeProjectConfig(
1410
- piCrewJsonPath,
1411
- config,
1412
- piCrewJsonConfig.config,
1413
- );
1133
+ const projectPart = sanitizeProjectConfig(piCrewJsonPath, config, piCrewJsonConfig.config);
1414
1134
  warnings.push(...projectPart.warnings);
1415
1135
  // base=project config, override=user config → user wins
1416
1136
  const mergedProject = mergeConfig(projectPart.config, config);
@@ -1418,53 +1138,49 @@ export function loadConfig(cwd?: string): LoadedPiTeamsConfig {
1418
1138
  paths.push(piCrewJsonPath);
1419
1139
  }
1420
1140
  }
1421
- return {
1141
+ const result: LoadedPiTeamsConfig = {
1422
1142
  path: filePath,
1423
1143
  paths,
1424
1144
  config,
1425
1145
  warnings: warnings.length > 0 ? warnings : undefined,
1426
1146
  };
1147
+ // Only cache when at least one of the watched paths exists — this avoids
1148
+ // pinning stale empty results when a user later creates one of these files
1149
+ // in the cache window. mtime stat below picks up the new file (it appears
1150
+ // in currentMtimes but not in cached.mtimes) and triggers a re-parse.
1151
+ if (Object.keys(readCacheMtimes(cacheParts)).length > 0) {
1152
+ setConfigCache(cacheKey, result, readCacheMtimes(cacheParts));
1153
+ }
1154
+ return result;
1427
1155
  }
1428
1156
 
1429
- export function updateConfig(
1430
- patch: PiTeamsConfig,
1431
- options: UpdateConfigOptions = {},
1432
- ): SavedPiTeamsConfig {
1433
- const filePath =
1434
- options.scope === "project" && options.cwd
1435
- ? projectConfigPath(options.cwd)
1436
- : configPath();
1157
+ export function updateConfig(patch: PiTeamsConfig, options: UpdateConfigOptions = {}): SavedPiTeamsConfig {
1158
+ const filePath = options.scope === "project" && options.cwd ? projectConfigPath(options.cwd) : configPath();
1437
1159
  const lockPath = filePath + ".lock";
1438
1160
  return withFileLockSync(lockPath, () => {
1439
1161
  let current: Record<string, unknown>;
1440
1162
  try {
1441
1163
  current = readConfigRecord(filePath);
1442
1164
  } catch (error) {
1443
- const message =
1444
- error instanceof Error ? error.message : String(error);
1165
+ const message = error instanceof Error ? error.message : String(error);
1445
1166
  throw new Error(`Could not update pi-crew config: ${message}`);
1446
1167
  }
1447
1168
  let merged = mergeConfig(parseConfig(current), patch);
1448
1169
  if (options.unsetPaths?.length) {
1449
- const raw = JSON.parse(JSON.stringify(merged)) as Record<
1450
- string,
1451
- unknown
1452
- >;
1170
+ const raw = JSON.parse(JSON.stringify(merged)) as Record<string, unknown>;
1453
1171
  for (const unset of options.unsetPaths) unsetPath(raw, unset);
1454
1172
  merged = parseConfig(raw);
1455
1173
  }
1456
1174
  fs.mkdirSync(path.dirname(filePath), { recursive: true });
1457
- atomicWriteFile(
1458
- filePath,
1459
- `${JSON.stringify(merged, null, 2)}\n`,
1460
- );
1175
+ atomicWriteFile(filePath, `${JSON.stringify(merged, null, 2)}\n`);
1176
+ // (F16) Invalidate the loadConfig cache after a write — the next
1177
+ // caller must see the new value, not a 0-2s stale snapshot.
1178
+ invalidateConfigCache();
1461
1179
  return { path: filePath, config: merged };
1462
1180
  });
1463
1181
  }
1464
1182
 
1465
- export function updateAutonomousConfig(
1466
- patch: PiTeamsAutonomousConfig,
1467
- ): SavedPiTeamsConfig {
1183
+ export function updateAutonomousConfig(patch: PiTeamsAutonomousConfig): SavedPiTeamsConfig {
1468
1184
  const filePath = configPath();
1469
1185
  const lockPath = filePath + ".lock";
1470
1186
  return withFileLockSync(lockPath, () => {
@@ -1472,21 +1188,17 @@ export function updateAutonomousConfig(
1472
1188
  try {
1473
1189
  current = readConfigRecord(filePath);
1474
1190
  } catch (error) {
1475
- const message =
1476
- error instanceof Error ? error.message : String(error);
1191
+ const message = error instanceof Error ? error.message : String(error);
1477
1192
  throw new Error(`Could not update pi-crew config: ${message}`);
1478
1193
  }
1479
1194
  const currentAutonomous =
1480
- current.autonomous &&
1481
- typeof current.autonomous === "object" &&
1482
- !Array.isArray(current.autonomous)
1195
+ current.autonomous && typeof current.autonomous === "object" && !Array.isArray(current.autonomous)
1483
1196
  ? (current.autonomous as Record<string, unknown>)
1484
1197
  : {};
1485
1198
  current.autonomous = { ...currentAutonomous, ...patch };
1486
- atomicWriteFile(
1487
- filePath,
1488
- `${JSON.stringify(current, null, 2)}\n`,
1489
- );
1199
+ atomicWriteFile(filePath, `${JSON.stringify(current, null, 2)}\n`);
1200
+ // (F16) Invalidate the loadConfig cache after a write — see updateConfig.
1201
+ invalidateConfigCache();
1490
1202
  return { path: filePath, config: parseConfig(current) };
1491
1203
  });
1492
1204
  }