@iowarp/clio-coder 0.4.0 → 0.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (581) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/README.md +116 -16
  3. package/dist/{acp-G5WJBNCT.js → acp-ZILU3AUO.js} +14 -14
  4. package/dist/{agents-FMV2Q5G4.js → agents-HYWGBGQR.js} +40 -29
  5. package/dist/assets/codewiki.json +1 -1
  6. package/dist/{auth-3IDSJEIK.js → auth-N3QT7CBO.js} +21 -19
  7. package/dist/{builtins-XCZWXSC7.js → builtins-UJLMOVOV.js} +6 -6
  8. package/dist/{chunk-PQEFIJ36.js → chunk-2LEAKD4C.js} +1 -2
  9. package/dist/{chunk-3BT2XMV4.js → chunk-2NM363SV.js} +50 -57
  10. package/dist/{chunk-J4W7KFM7.js → chunk-2VG7KLYV.js} +24 -279
  11. package/dist/{chunk-SST6Z5JA.js → chunk-336NSSKI.js} +1 -1
  12. package/dist/{chunk-K5C3NCBD.js → chunk-3I5NY75V.js} +5 -5
  13. package/dist/{chunk-VAZSBTKF.js → chunk-3QSOM6PA.js} +2 -2
  14. package/dist/{chunk-TZ3SGWZZ.js → chunk-5QIAJV2D.js} +2 -2
  15. package/dist/{chunk-OQBA45DZ.js → chunk-5YHDIDBP.js} +2 -2
  16. package/dist/{chunk-NHLBIGRH.js → chunk-6CCS4G3W.js} +40 -17
  17. package/dist/{chunk-2JDWVJND.js → chunk-6DWBAZ5U.js} +2 -2
  18. package/dist/{chunk-LDQ2ZF2M.js → chunk-6FN3E6KX.js} +5 -5
  19. package/dist/{chunk-X3YGUTOB.js → chunk-6HMJX2VU.js} +59 -59
  20. package/dist/{chunk-VEO4AP2K.js → chunk-6I5ILFOF.js} +3 -2
  21. package/dist/{chunk-B7OBL7PK.js → chunk-6NJQITNH.js} +18 -11
  22. package/dist/{chunk-IQ7KR472.js → chunk-6VC4OV3Z.js} +6 -3
  23. package/dist/{chunk-P3FOHJT4.js → chunk-72GZI5EV.js} +2 -2
  24. package/dist/{chunk-AB44T6BB.js → chunk-74YWRRU5.js} +8 -7
  25. package/dist/{chunk-SJ5ZKQ4S.js → chunk-7EPLI7VL.js} +2 -2
  26. package/dist/{chunk-HWHKMHUA.js → chunk-7OBGU7UB.js} +3 -3
  27. package/dist/{chunk-2OQE55CK.js → chunk-7PWAODYW.js} +2 -2
  28. package/dist/{chunk-IBEBSCYA.js → chunk-7RY5VZPH.js} +42 -33
  29. package/dist/{chunk-LQ3DZAMX.js → chunk-ABLSQ6JX.js} +2 -2
  30. package/dist/{chunk-QLFS5GO2.js → chunk-AFKWHWXF.js} +12 -22
  31. package/dist/{chunk-FWDFM5ZU.js → chunk-AK5XEFVZ.js} +3 -3
  32. package/dist/{chunk-O6TL7WWY.js → chunk-AKB4GYDL.js} +4 -4
  33. package/dist/chunk-AP73CFDC.js +308 -0
  34. package/dist/{chunk-JDG2WCRO.js → chunk-BBTJOK6Y.js} +5 -5
  35. package/dist/{chunk-SP2RXXYO.js → chunk-BDPT6GTK.js} +10 -10
  36. package/dist/{chunk-NQQH3YT7.js → chunk-BJGUKIG4.js} +3 -3
  37. package/dist/{chunk-O4XIVISU.js → chunk-BTGG6BG2.js} +79 -38
  38. package/dist/chunk-BTK3SSSK.js +67 -0
  39. package/dist/{chunk-M6L6IDJG.js → chunk-BWH6N36D.js} +1 -1
  40. package/dist/chunk-BWW4HLO4.js +301 -0
  41. package/dist/{chunk-I5VEOC6I.js → chunk-C537JADH.js} +10 -10
  42. package/dist/{chunk-HI63TFOG.js → chunk-CDNVLKUX.js} +3 -3
  43. package/dist/{chunk-UM7N4G5A.js → chunk-CWVRRIEI.js} +2 -2
  44. package/dist/{chunk-BKFM6EJV.js → chunk-D4CJV46V.js} +2 -2
  45. package/dist/chunk-D7E7WCDF.js +27 -0
  46. package/dist/{chunk-WZR7K7ZX.js → chunk-E67WX76H.js} +6 -6
  47. package/dist/{chunk-FQ4SKYE4.js → chunk-EBY63B7G.js} +2 -5
  48. package/dist/chunk-EIMVLWB3.js +1842 -0
  49. package/dist/{chunk-RAPCMZL4.js → chunk-FSP7CMNU.js} +9 -1
  50. package/dist/{chunk-MLKNTWH2.js → chunk-FYUN5KZ3.js} +21 -21
  51. package/dist/{chunk-YJX4SHTD.js → chunk-GIZNH63R.js} +1 -1
  52. package/dist/{chunk-X75E3D2N.js → chunk-GPPB3JBE.js} +18 -16
  53. package/dist/{chunk-GR5G2PVF.js → chunk-GVQJ5CCZ.js} +15 -10
  54. package/dist/chunk-HJWWJ6IL.js +184 -0
  55. package/dist/{chunk-VO67MWHC.js → chunk-HRQMFLJP.js} +3 -3
  56. package/dist/{chunk-VYMXRQI6.js → chunk-HUAS7ITX.js} +89 -93
  57. package/dist/{chunk-BKFJHQCA.js → chunk-I66EAJFY.js} +2 -2
  58. package/dist/{chunk-DYJP44XW.js → chunk-IG7BCQBA.js} +1 -1
  59. package/dist/{chunk-32KWKNSF.js → chunk-IXJT6DCX.js} +3 -3
  60. package/dist/{chunk-C2LTL2W6.js → chunk-J4HBWF6Y.js} +360 -172
  61. package/dist/chunk-J5LZHVIT.js +284 -0
  62. package/dist/{chunk-QLL7ILRG.js → chunk-JA5QWE4Z.js} +510 -202
  63. package/dist/{chunk-BBVJUZHB.js → chunk-JIEGK6UF.js} +5 -173
  64. package/dist/{chunk-36EJLSQQ.js → chunk-JZWT5J3Y.js} +4 -4
  65. package/dist/{chunk-HZHHCK24.js → chunk-K6BF4U2H.js} +476 -322
  66. package/dist/{chunk-Z2RR6MAK.js → chunk-MA3H6DM5.js} +6 -6
  67. package/dist/{chunk-CODPRO7Q.js → chunk-MOPSG2X7.js} +6 -6
  68. package/dist/{chunk-76ONBSIA.js → chunk-MQXIVJ35.js} +14 -5
  69. package/dist/{chunk-YDFRH54B.js → chunk-N5UK64DP.js} +4 -4
  70. package/dist/chunk-NDDGJUXD.js +81 -0
  71. package/dist/{chunk-4ZG3XFUR.js → chunk-NLFAQR7Z.js} +12 -1
  72. package/dist/{chunk-VO2LKSTM.js → chunk-O42A54GG.js} +2 -2
  73. package/dist/{chunk-W5VSYASO.js → chunk-OJTRZGR3.js} +46 -87
  74. package/dist/{chunk-NHCZP4K7.js → chunk-PBP4B7XR.js} +2 -2
  75. package/dist/{chunk-3URVFKWK.js → chunk-PJJ6MY27.js} +5 -5
  76. package/dist/{chunk-ZGVHUX3M.js → chunk-PMZVFWSR.js} +2 -2
  77. package/dist/{chunk-GKF55TAZ.js → chunk-PVAMAVBB.js} +3 -2
  78. package/dist/{chunk-6DB53AJS.js → chunk-QKIFBZKT.js} +7 -7
  79. package/dist/{chunk-HJJTYUHX.js → chunk-QMXC4JB7.js} +20 -20
  80. package/dist/chunk-QTFGO774.js +644 -0
  81. package/dist/{chunk-CY6FY24N.js → chunk-R23Z6K6I.js} +2 -2
  82. package/dist/{chunk-ULC6OTWO.js → chunk-R32CLGZ6.js} +13 -13
  83. package/dist/{chunk-KMVISBZR.js → chunk-RKSR6VSF.js} +1 -1
  84. package/dist/{chunk-F4CKPOEQ.js → chunk-SCYB3HA4.js} +72 -26
  85. package/dist/{chunk-LY4S7GJC.js → chunk-SINK3QR6.js} +18 -16
  86. package/dist/{chunk-U77AMWDL.js → chunk-TD3PGPQA.js} +43 -22
  87. package/dist/{chunk-QBJA7R7N.js → chunk-THYWACCR.js} +2 -2
  88. package/dist/{chunk-QREDIESB.js → chunk-TVH4ONAM.js} +14 -8
  89. package/dist/chunk-U2WB7TZS.js +271 -0
  90. package/dist/{chunk-KFZI4NIL.js → chunk-UAPGZHYC.js} +145 -31
  91. package/dist/{chunk-DQITNCXG.js → chunk-UEDMSP56.js} +29 -14
  92. package/dist/{chunk-3XML7CDN.js → chunk-UXN6JT4W.js} +2 -2
  93. package/dist/{chunk-CMZWFGD2.js → chunk-VAVR4IJY.js} +19 -15
  94. package/dist/{chunk-BPKCPIL7.js → chunk-VEGN6WIQ.js} +2 -2
  95. package/dist/chunk-VKFQTNDV.js +66 -0
  96. package/dist/{chunk-7MCTRUCE.js → chunk-VKRH2TCS.js} +2 -2
  97. package/dist/{chunk-5LXZXPKX.js → chunk-VN3SHNBN.js} +2 -2
  98. package/dist/{chunk-UOSL25KY.js → chunk-VXMFAE2W.js} +1 -1
  99. package/dist/{chunk-BBVYXMFO.js → chunk-WNP7O5WZ.js} +2 -2
  100. package/dist/{chunk-SUCTJL45.js → chunk-WR6U3OVP.js} +3 -3
  101. package/dist/{chunk-BUMFYQFY.js → chunk-XKA2ICR3.js} +124 -113
  102. package/dist/{chunk-UXMFQ54G.js → chunk-Y45G3AXC.js} +39 -39
  103. package/dist/{chunk-J5Q24KAG.js → chunk-Y4CAGMM6.js} +2 -2
  104. package/dist/{chunk-NYS75XW5.js → chunk-YAKGTXEV.js} +5 -3
  105. package/dist/{chunk-V5DHCITQ.js → chunk-ZAZB4JMW.js} +23 -12
  106. package/dist/{chunk-Z4TXYIEG.js → chunk-ZJLUDYFY.js} +9 -9
  107. package/dist/{chunk-HKIYEGME.js → chunk-ZTJUVLTU.js} +2 -2
  108. package/dist/{chunk-2ANTL7MR.js → chunk-ZW55JB7N.js} +2 -2
  109. package/dist/cli/index.js +36 -36
  110. package/dist/{clio-2JXHBBY5.js → clio-IT3G3VQH.js} +12 -13
  111. package/dist/{code-nav-3YYRMYNF.js → code-nav-RK6S7F6E.js} +5 -5
  112. package/dist/codewiki/build-worker.js +1 -1
  113. package/dist/{compile-cache-7FPE6PS3.js → compile-cache-OQAVB7Y5.js} +2 -4
  114. package/dist/{components-RYZV4JGP.js → components-UBWCQSRW.js} +4 -4
  115. package/dist/{config-QZPCMYSO.js → config-3QZRWZJF.js} +53 -49
  116. package/dist/{configure-TEGEBYCA.js → configure-FL7Y3KJF.js} +25 -22
  117. package/dist/{context-E6H5RNMC.js → context-5HE7ODYK.js} +47 -43
  118. package/dist/{context-AV7OEZ4D.js → context-KYQFRVDC.js} +11 -11
  119. package/dist/{context-GSXUE4CT.js → context-XNHL75JV.js} +32 -19
  120. package/dist/{context-clear-SHIBYK6T.js → context-clear-N545L53A.js} +47 -43
  121. package/dist/{context-working-set-5ZGKPGZQ.js → context-working-set-QHKXSV2F.js} +10 -9
  122. package/dist/{dispatch-runner-EFMJT4LD.js → dispatch-runner-RGIE5PCT.js} +56 -47
  123. package/dist/{docs-23KQS3XK.js → docs-5NAF6AU7.js} +5 -11
  124. package/dist/doctor-ZGPEGHIP.js +772 -0
  125. package/dist/{eval-TFBYQH4H.js → eval-GXLL44RD.js} +63 -45
  126. package/dist/{eval-inventory-SXH7PDKX.js → eval-inventory-HBWSWQOK.js} +7 -8
  127. package/dist/{evidence-ERGESKGN.js → evidence-HWLBRH3Q.js} +44 -40
  128. package/dist/{evolve-VDXTSYCJ.js → evolve-FTZBMNVW.js} +43 -39
  129. package/dist/{extensions-7BGBHN57.js → extensions-VHRBEID7.js} +4 -4
  130. package/dist/{fleet-2RRVDF2V.js → fleet-CKZHJWZJ.js} +86 -73
  131. package/dist/{fleet-commands-VJ726XIA.js → fleet-commands-EXDXBMV6.js} +10 -9
  132. package/dist/{fleet-decisions-EPAPM3XJ.js → fleet-decisions-OTHB6KRL.js} +5 -5
  133. package/dist/{fleet-graph-JF5QOATM.js → fleet-graph-YTEZUCUT.js} +13 -11
  134. package/dist/{fleet-inspect-VLY4S7QM.js → fleet-inspect-SS6YMDCK.js} +44 -41
  135. package/dist/{fleet-preflight-AIZUEJOY.js → fleet-preflight-PBY4VYOM.js} +5 -9
  136. package/dist/{fleet-validate-AJRPDMDV.js → fleet-validate-KMEM5L3S.js} +16 -13
  137. package/dist/{fleet-verify-JFEL2L3H.js → fleet-verify-QD5M7E7Q.js} +42 -39
  138. package/dist/{fleet-view-ZCON35AG.js → fleet-view-WAMJYNDT.js} +44 -52
  139. package/dist/{init-DN2WWLFE.js → init-5XQRBOFV.js} +63 -58
  140. package/dist/{install-XGLBQY5E.js → install-NKNG7KD4.js} +2 -2
  141. package/dist/{interop-OZBKXAYL.js → interop-34TVO25M.js} +13 -11
  142. package/dist/{library-YWZG7IMW.js → library-3QY6KF57.js} +17 -14
  143. package/dist/{memory-I4C4HMLW.js → memory-L4UTIIIW.js} +45 -41
  144. package/dist/{models-CEYXJBO6.js → models-ZVX3QOWE.js} +39 -31
  145. package/dist/{monitor-NZ6GCI3P.js → monitor-CEKVSYTS.js} +50 -46
  146. package/dist/{orchestrator-GCGQ4N5I.js → orchestrator-77BAP6BC.js} +1807 -1039
  147. package/dist/{panes-KY6W3V2E.js → panes-7STHOAUJ.js} +4 -4
  148. package/dist/{panes-HMABYVO4.js → panes-SHAUIRXY.js} +10 -8
  149. package/dist/{paths-II4K7DNR.js → paths-L7LGY6RN.js} +4 -4
  150. package/dist/{reset-DQ6FGCSH.js → reset-EOLM7GVE.js} +7 -8
  151. package/dist/{resources-BB3MVJMD.js → resources-74GKTLSF.js} +17 -13
  152. package/dist/{run-H2GQDUER.js → run-HBAUJNNZ.js} +86 -76
  153. package/dist/{share-GTJN6A5O.js → share-G3APVLVP.js} +17 -14
  154. package/dist/{skills-L55TEW6R.js → skills-35HHUKCR.js} +19 -16
  155. package/dist/{skills-eval-XVXPH2JI.js → skills-eval-QN4HSHDC.js} +55 -57
  156. package/dist/{skills-inventory-S4MXPJFV.js → skills-inventory-J357J34F.js} +15 -12
  157. package/dist/{slash-commands-ZSGASKJC.js → slash-commands-JZZCQA32.js} +31 -29
  158. package/dist/{steer-RZGSCY4R.js → steer-XAVHJM22.js} +3 -3
  159. package/dist/{support-PKEUNNQL.js → support-U7QOWY26.js} +6 -6
  160. package/dist/{targets-NCPZ644J.js → targets-DSM6CY3M.js} +59 -53
  161. package/dist/{terminal-lease-44SV3YCN.js → terminal-lease-JOPFUVEM.js} +14 -9
  162. package/dist/{tools-DAF3DI3C.js → tools-MKNWVPBH.js} +10 -8
  163. package/dist/{trace-FYVW2MQA.js → trace-ECQ7TIYZ.js} +48 -3
  164. package/dist/{tui-primitives-2AKXQNZK.js → tui-primitives-K5PASC3O.js} +8 -4
  165. package/dist/{uninstall-DW2PNOIC.js → uninstall-N34PCTGJ.js} +4 -6
  166. package/dist/{upgrade-3XPP6OQL.js → upgrade-H7TOM7YL.js} +35 -62
  167. package/dist/{usage-3NLHGTU2.js → usage-X52N3IDJ.js} +51 -48
  168. package/dist/{verifiers-SSQONKRT.js → verifiers-EJTVVSMA.js} +10 -9
  169. package/dist/{verify-3U6J7FZI.js → verify-YJL6XET2.js} +9 -8
  170. package/dist/{web-fetch-S7RR6GZ7.js → web-fetch-MPIFL3LL.js} +1 -2
  171. package/dist/{wiki-generate-CEHYGPGQ.js → wiki-generate-4NDZTQ4B.js} +66 -63
  172. package/dist/{with-panes-MKB46MPQ.js → with-panes-OBOBFIIR.js} +62 -26
  173. package/dist/worker/entry.js +57 -52
  174. package/docs/acp.md +1 -1
  175. package/docs/built-in-agents.md +40 -32
  176. package/docs/commands-and-modes.md +9 -3
  177. package/docs/config-knobs-audit.md +3 -0
  178. package/docs/configuration-and-targets.md +105 -164
  179. package/docs/environment-variables.md +2 -1
  180. package/docs/exit-codes-and-output.md +2 -0
  181. package/docs/fleet-demo-runbook.md +6 -3
  182. package/docs/fleet-dispatch.md +8 -6
  183. package/docs/installation-and-lifecycle.md +1 -1
  184. package/docs/release-cut-checklist.md +164 -177
  185. package/docs/trace-store.md +15 -10
  186. package/docs/troubleshooting.md +1 -1
  187. package/evals/README.md +5 -0
  188. package/evals/behavioral-corpus-grader.mjs +103 -0
  189. package/evals/behavioral-machinery-baseline.json +2276 -0
  190. package/evals/behavioral-machinery-driver.ts +250 -0
  191. package/evals/behavioral-machinery-support.ts +253 -0
  192. package/evals/behavioral-machinery.yaml +614 -0
  193. package/evals/behavioral-model-negative-control.yaml +68 -0
  194. package/evals/behavioral-model.yaml +216 -0
  195. package/evals/fixtures/behavioral-facts.txt +3 -0
  196. package/evals/fixtures/behavioral-main.ts +7 -0
  197. package/evals/fixtures/decoy/note.txt +2 -0
  198. package/evals/fixtures/private/credentials.txt +1 -0
  199. package/evals/tracked-metrics-baseline.yaml +229 -0
  200. package/evals/tracked-metrics-grader.mjs +129 -0
  201. package/package.json +5 -15
  202. package/skills/README.md +6 -6
  203. package/skills/coding/ast-grep/SKILL.md +8 -2
  204. package/skills/coding/coding-standards/SKILL.md +8 -2
  205. package/skills/coding/prototype/SKILL.md +8 -2
  206. package/skills/coding/tdd/SKILL.md +8 -2
  207. package/skills/context/context-handoff/SKILL.md +12 -3
  208. package/skills/context/context-handoff/evals.md +3 -3
  209. package/skills/context/context-prime/SKILL.md +8 -2
  210. package/skills/context/context-prime/evals.md +2 -2
  211. package/skills/git/file-ticket/SKILL.md +9 -4
  212. package/skills/git/file-ticket/evals.md +2 -2
  213. package/skills/git/fix-issue/SKILL.md +12 -5
  214. package/skills/git/fix-issue/evals.md +4 -4
  215. package/skills/git/resolve-merge-conflicts/SKILL.md +8 -2
  216. package/skills/git/resolve-merge-conflicts/evals.md +2 -2
  217. package/skills/git/ship/SKILL.md +9 -3
  218. package/skills/git/ship/evals.md +4 -4
  219. package/skills/git/worktree-create/SKILL.md +7 -2
  220. package/skills/git/worktree-create/evals.md +6 -6
  221. package/skills/git/worktree-merge/SKILL.md +7 -2
  222. package/skills/git/worktree-merge/evals.md +6 -6
  223. package/skills/meta/{clio-dev → clio-coder-dev}/SKILL.md +14 -8
  224. package/skills/meta/{clio-dev → clio-coder-dev}/evals.md +3 -3
  225. package/skills/meta/{clio-test → clio-coder-test}/SKILL.md +11 -5
  226. package/skills/meta/{clio-test → clio-coder-test}/evals.md +1 -1
  227. package/skills/meta/credentials/SKILL.md +9 -2
  228. package/skills/meta/find-skills/SKILL.md +8 -2
  229. package/skills/meta/herdr/SKILL.md +8 -2
  230. package/skills/meta/skill-craft/SKILL.md +21 -4
  231. package/skills/planning/architecture/SKILL.md +9 -3
  232. package/skills/planning/backlog/SKILL.md +8 -2
  233. package/skills/planning/prd/SKILL.md +8 -2
  234. package/skills/planning/product-intent/SKILL.md +8 -2
  235. package/skills/planning/tech-spec/SKILL.md +9 -3
  236. package/skills/registry.yaml +66 -66
  237. package/skills/research/arxiv-literature/SKILL.md +8 -2
  238. package/skills/research/experiment-protocol/SKILL.md +9 -2
  239. package/skills/research/scientific-debugging/SKILL.md +9 -2
  240. package/skills/research/scientific-debugging/evals.md +2 -2
  241. package/skills/research/scientific-modernization/SKILL.md +9 -2
  242. package/skills/skill-marketplace.json +257 -38
  243. package/skills/workflow/cut-it/SKILL.md +10 -2
  244. package/skills/workflow/design-council/SKILL.md +11 -2
  245. package/skills/workflow/grill-me/SKILL.md +8 -2
  246. package/skills/workflow/workflow-distiller/SKILL.md +8 -2
  247. package/src/cli/acp.ts +2 -2
  248. package/src/cli/auth.ts +2 -2
  249. package/src/cli/bootstrap-generate.ts +5 -5
  250. package/src/cli/clio.ts +6 -1
  251. package/src/cli/config-inspect.ts +3 -3
  252. package/src/cli/configure-interop.ts +1 -1
  253. package/src/cli/configure.ts +27 -27
  254. package/src/cli/default-target.ts +1 -1
  255. package/src/cli/docs.ts +6 -6
  256. package/src/cli/doctor-naming.ts +323 -0
  257. package/src/cli/doctor-panes.ts +46 -14
  258. package/src/cli/doctor-toolchain.ts +29 -8
  259. package/src/cli/doctor.ts +15 -1
  260. package/src/cli/eval-inventory.ts +8 -8
  261. package/src/cli/evidence-detail.ts +1 -1
  262. package/src/cli/evidence-inventory.ts +1 -1
  263. package/src/cli/fleet-decisions.ts +1 -1
  264. package/src/cli/fleet-inspect.ts +1 -1
  265. package/src/cli/fleet-preflight.ts +1 -1
  266. package/src/cli/fleet-verify.ts +1 -1
  267. package/src/cli/fleet-view.ts +40 -23
  268. package/src/cli/fleet.ts +8 -5
  269. package/src/cli/interop-inspect.ts +2 -2
  270. package/src/cli/models.ts +3 -3
  271. package/src/cli/modes/print.ts +1 -1
  272. package/src/cli/reset.ts +1 -1
  273. package/src/cli/run.ts +11 -15
  274. package/src/cli/share.ts +1 -1
  275. package/src/cli/skills-eval.ts +11 -11
  276. package/src/cli/targets.ts +23 -24
  277. package/src/cli/trace-inspect.ts +1 -1
  278. package/src/cli/trace.ts +41 -0
  279. package/src/cli/uninstall.ts +2 -2
  280. package/src/cli/upgrade.ts +29 -34
  281. package/src/cli/usage.ts +2 -2
  282. package/src/cli/validate-model.ts +1 -1
  283. package/src/cli/verifiers-inspect.ts +1 -1
  284. package/src/cli/wiki-generate.ts +5 -5
  285. package/src/core/bash-exec.ts +1 -1
  286. package/src/core/boot-trace.ts +3 -3
  287. package/src/core/bus-trace.ts +2 -2
  288. package/src/core/compile-cache.ts +1 -1
  289. package/src/core/config.ts +1062 -886
  290. package/src/core/defaults.ts +321 -528
  291. package/src/core/domain-loader.ts +6 -4
  292. package/src/core/event-bus.ts +1 -1
  293. package/src/core/file-references.ts +78 -88
  294. package/src/core/git-commit-attribution.ts +3 -9
  295. package/src/core/guardrails.ts +24 -4
  296. package/src/core/naming-compat.ts +45 -0
  297. package/src/core/naming-events.ts +62 -0
  298. package/src/core/recent-models.ts +1 -7
  299. package/src/core/residency-protection.ts +4 -8
  300. package/src/core/resolve-config-value.ts +1 -1
  301. package/src/core/response-model-id.ts +0 -14
  302. package/src/core/session-routing.ts +84 -74
  303. package/src/core/skill-activation.ts +18 -0
  304. package/src/core/termination.ts +4 -4
  305. package/src/core/timers.ts +2 -12
  306. package/src/domains/agents/catalog.ts +2 -12
  307. package/src/domains/agents/extension.ts +3 -3
  308. package/src/domains/agents/registry.ts +7 -7
  309. package/src/domains/agents/result-contract.ts +1 -1
  310. package/src/domains/agents/spec.ts +0 -11
  311. package/src/domains/config/classify.ts +35 -47
  312. package/src/domains/config/extension.ts +5 -5
  313. package/src/domains/config/keybindings.ts +48 -47
  314. package/src/domains/config/watcher.ts +1 -1
  315. package/src/domains/context/codewiki/indexer.ts +1 -1
  316. package/src/domains/context/extension.ts +3 -3
  317. package/src/domains/context/prompt-context.ts +1 -1
  318. package/src/domains/context/wiki/assemble.ts +2 -2
  319. package/src/domains/context/working-set/replay/metrics.ts +0 -4
  320. package/src/domains/context/working-set/replay/report.ts +1 -1
  321. package/src/domains/context/working-set/replay/synthetic.ts +2 -2
  322. package/src/domains/dispatch/active-route-planner.ts +2 -2
  323. package/src/domains/dispatch/agent-candidates.ts +1 -1
  324. package/src/domains/dispatch/agent-ledger-store.ts +1 -1
  325. package/src/domains/dispatch/agent-ledger.ts +1 -1
  326. package/src/domains/dispatch/assignment.ts +5 -5
  327. package/src/domains/dispatch/capacity-lease.ts +2 -5
  328. package/src/domains/dispatch/code-step.ts +1 -1
  329. package/src/domains/dispatch/contract.ts +2 -2
  330. package/src/domains/dispatch/extension.ts +65 -61
  331. package/src/domains/dispatch/fleet-node-prompt.ts +1 -1
  332. package/src/domains/dispatch/fleet-preflight.ts +8 -17
  333. package/src/domains/dispatch/fleet-run.ts +0 -7
  334. package/src/domains/dispatch/gate-decisions.ts +25 -14
  335. package/src/domains/dispatch/joint-route-resolver.ts +1 -1
  336. package/src/domains/dispatch/path-scope.ts +0 -61
  337. package/src/domains/dispatch/placement.ts +2 -2
  338. package/src/domains/dispatch/receipt-findings.ts +1 -1
  339. package/src/domains/dispatch/receipt-integrity.ts +20 -8
  340. package/src/domains/dispatch/reservation-store.ts +0 -23
  341. package/src/domains/dispatch/route-decision.ts +1 -1
  342. package/src/domains/dispatch/route-history.ts +1 -1
  343. package/src/domains/dispatch/route-policy.ts +1 -1
  344. package/src/domains/dispatch/route-readiness.ts +0 -28
  345. package/src/domains/dispatch/run-event-journal.ts +6 -5
  346. package/src/domains/dispatch/tool-stats.ts +1 -1
  347. package/src/domains/dispatch/transport.ts +2 -19
  348. package/src/domains/dispatch/types.ts +3 -3
  349. package/src/domains/dispatch/worker-protocol.ts +2 -2
  350. package/src/domains/eval/artifacts/store.ts +5 -4
  351. package/src/domains/eval/compare/thresholds.ts +0 -8
  352. package/src/domains/eval/execution-provenance.ts +1 -1
  353. package/src/domains/eval/metrics/token-stream.ts +0 -7
  354. package/src/domains/eval/report.ts +1 -1
  355. package/src/domains/eval/runner.ts +1 -1
  356. package/src/domains/eval/runners/clio-run.ts +7 -6
  357. package/src/domains/eval/runners/external-command.ts +4 -3
  358. package/src/domains/eval/schema/artifact.ts +1 -1
  359. package/src/domains/eval/schema/behavioral-metrics.ts +3 -2
  360. package/src/domains/eval/schema/behavioral.ts +7 -6
  361. package/src/domains/eval/schema/execution-envelope.ts +4 -2
  362. package/src/domains/eval/schema/naming.ts +10 -0
  363. package/src/domains/eval/schema/suite.ts +2 -2
  364. package/src/domains/eval/schema/validate.ts +9 -5
  365. package/src/domains/eval/schema/verdict.ts +4 -2
  366. package/src/domains/eval/store.ts +12 -2
  367. package/src/domains/eval/suites/run.ts +12 -6
  368. package/src/domains/eval/types.ts +1 -1
  369. package/src/domains/eval/workspaces/git.ts +1 -1
  370. package/src/domains/eval/workspaces/temp-copy.ts +1 -1
  371. package/src/domains/evidence/build.ts +13 -5
  372. package/src/domains/interop/consent.ts +7 -7
  373. package/src/domains/interop/extension.ts +1 -1
  374. package/src/domains/lifecycle/doctor.ts +27 -21
  375. package/src/domains/lifecycle/migrations/2026-09-01-clio-coder-naming.ts +155 -0
  376. package/src/domains/lifecycle/migrations/2026-09-01-retire-panes-knobs.ts +59 -0
  377. package/src/domains/lifecycle/migrations/2026-09-01-settings-v2.ts +338 -0
  378. package/src/domains/lifecycle/migrations/index.ts +20 -1
  379. package/src/domains/lifecycle/naming-history.ts +153 -0
  380. package/src/domains/lifecycle/naming-mutable-state.ts +104 -0
  381. package/src/domains/lifecycle/naming-resources.ts +261 -0
  382. package/src/domains/lifecycle/naming-tool-markers.ts +139 -0
  383. package/src/domains/lifecycle/naming-yazi.ts +81 -0
  384. package/src/domains/lifecycle/upgrade-notice.ts +1 -1
  385. package/src/domains/memory/task-memory-handoff.ts +6 -3
  386. package/src/domains/middleware/index.ts +7 -0
  387. package/src/domains/middleware/marketplace-offer.ts +339 -0
  388. package/src/domains/middleware/runtime.ts +2 -2
  389. package/src/domains/mux/contract.ts +157 -25
  390. package/src/domains/mux/detect.ts +29 -5
  391. package/src/domains/mux/dock-controller.ts +365 -0
  392. package/src/domains/mux/extension.ts +7 -1
  393. package/src/domains/mux/index.ts +20 -0
  394. package/src/domains/mux/operations.ts +28 -0
  395. package/src/domains/mux/protocol.ts +19 -6
  396. package/src/domains/mux/socket-client.ts +167 -19
  397. package/src/domains/mux/types.ts +49 -6
  398. package/src/domains/mux/yazi/event-stream.ts +6 -4
  399. package/src/domains/mux/yazi/profile.ts +1 -1
  400. package/src/domains/mux/yazi/session.ts +26 -3
  401. package/src/domains/observability/extension.ts +1 -1
  402. package/src/domains/observability/out-of-turn-usage.ts +2 -2
  403. package/src/domains/observability/projection.ts +1 -1
  404. package/src/domains/observability/trace-store.ts +22 -12
  405. package/src/domains/prompts/compiler.ts +2 -2
  406. package/src/domains/prompts/extension.ts +3 -3
  407. package/src/domains/prompts/fragments/identity/clio-worker.md +1 -1
  408. package/src/domains/prompts/fragments/operating/skills.md +25 -18
  409. package/src/domains/providers/extension.ts +5 -5
  410. package/src/domains/providers/knowledge-base-path.ts +1 -1
  411. package/src/domains/providers/model-runtime-capabilities.ts +14 -2
  412. package/src/domains/providers/models/cloud-models/alcf.yaml +1 -1
  413. package/src/domains/providers/models/local-models/{clio-local-coding-targets.yaml → clio-coder-local-coding-targets.yaml} +4 -4
  414. package/src/domains/providers/plugins.ts +1 -1
  415. package/src/domains/providers/resolver.ts +1 -1
  416. package/src/domains/providers/runtimes/cloud/alcf.ts +4 -4
  417. package/src/domains/providers/runtimes/common/lmstudio-http.ts +1 -5
  418. package/src/domains/providers/runtimes/common/local-synth.ts +3 -3
  419. package/src/domains/providers/runtimes/common/probe-helpers.ts +2 -2
  420. package/src/domains/providers/runtimes/local-native/llamacpp-completion.ts +1 -1
  421. package/src/domains/providers/runtimes/local-native/lmstudio.ts +2 -9
  422. package/src/domains/providers/runtimes/protocol/litellm.ts +1 -91
  423. package/src/domains/providers/types/knowledge-base.ts +13 -0
  424. package/src/domains/providers/types/local-model-quirks.ts +2 -2
  425. package/src/domains/providers/types/target-descriptor.ts +3 -3
  426. package/src/domains/resources/extension.ts +1 -1
  427. package/src/domains/resources/library.ts +6 -6
  428. package/src/domains/resources/prompts/loader.ts +1 -1
  429. package/src/domains/resources/skills/content-hash.ts +7 -6
  430. package/src/domains/resources/skills/evals.ts +1 -1
  431. package/src/domains/resources/skills/install.ts +36 -12
  432. package/src/domains/resources/skills/loader.ts +39 -13
  433. package/src/domains/resources/skills/marketplace.ts +21 -2
  434. package/src/domains/resources/skills/promotion.ts +214 -0
  435. package/src/domains/safety/audit.ts +2 -2
  436. package/src/domains/safety/finish-contract.ts +2 -1
  437. package/src/domains/safety/run-effects.ts +1 -1
  438. package/src/domains/scheduling/extension.ts +4 -4
  439. package/src/domains/session/archive-readers.ts +9 -1
  440. package/src/domains/session/compaction/branch-summary.ts +4 -0
  441. package/src/domains/session/compaction/compact.ts +1 -1
  442. package/src/domains/session/compaction/tokens.ts +4 -0
  443. package/src/domains/session/context-accounting.ts +1 -1
  444. package/src/domains/session/decision-board.ts +1 -1
  445. package/src/domains/session/extension.ts +1 -1
  446. package/src/domains/session/handoff.ts +2 -2
  447. package/src/domains/session/prompt-manifest.ts +0 -5
  448. package/src/domains/session/protected-artifact-journal.ts +12 -6
  449. package/src/domains/session/retry.ts +1 -1
  450. package/src/domains/share/archive.ts +85 -36
  451. package/src/domains/toolchain/install.ts +1 -1
  452. package/src/engine/acp/adapter.ts +3 -3
  453. package/src/engine/acp/event-mapper.ts +3 -3
  454. package/src/engine/acp/server.ts +27 -26
  455. package/src/engine/acp/tool-mediator.ts +2 -2
  456. package/src/engine/acp/types.ts +1 -1
  457. package/src/engine/alcf-oauth.ts +5 -5
  458. package/src/engine/antigravity/subprocess-runtime.ts +2 -2
  459. package/src/engine/api-registry.ts +1 -1
  460. package/src/engine/apis/llamacpp-residency.ts +1 -19
  461. package/src/engine/apis/lmstudio.ts +1 -6
  462. package/src/engine/apis/ollama-native.ts +3 -3
  463. package/src/engine/apis/openai-completions.ts +2 -2
  464. package/src/engine/apis/residency.ts +3 -9
  465. package/src/engine/claude/sdk-module.ts +1 -12
  466. package/src/engine/claude/sdk-runtime.ts +6 -6
  467. package/src/engine/claude/subprocess-runtime.ts +1 -1
  468. package/src/engine/claude/tool-safety.ts +4 -4
  469. package/src/engine/loop-guard.ts +0 -5
  470. package/src/engine/session.ts +14 -7
  471. package/src/engine/tui-primitives.ts +1 -1
  472. package/src/engine/worker-events.ts +6 -6
  473. package/src/engine/worker-runtime.ts +14 -13
  474. package/src/entry/orchestrator.ts +107 -88
  475. package/src/interactive/application-controller.ts +21 -21
  476. package/src/interactive/bus-notices.ts +2 -2
  477. package/src/interactive/chat-loop.ts +8 -8
  478. package/src/interactive/chat-panel.ts +1 -1
  479. package/src/interactive/clio-editor.ts +105 -1
  480. package/src/interactive/context-activity.ts +8 -6
  481. package/src/interactive/context-meter.ts +3 -6
  482. package/src/interactive/context-overlay.ts +1 -1
  483. package/src/interactive/cost-overlay.ts +1 -1
  484. package/src/interactive/council-grid.ts +2 -2
  485. package/src/interactive/dispatch-board.ts +30 -33
  486. package/src/interactive/editor-bash.ts +21 -0
  487. package/src/interactive/editor-steer.ts +9 -4
  488. package/src/interactive/editor-submit.ts +24 -18
  489. package/src/interactive/external-editor.ts +1 -1
  490. package/src/interactive/file-reference-completion.ts +360 -0
  491. package/src/interactive/footer/dashboard.ts +15 -17
  492. package/src/interactive/footer/notifications.ts +14 -8
  493. package/src/interactive/footer/widgets.ts +2 -44
  494. package/src/interactive/interactive-application.ts +79 -59
  495. package/src/interactive/interactive-event-projection.ts +1 -1
  496. package/src/interactive/interactive-input-runtime.ts +2 -2
  497. package/src/interactive/interactive-presentation.ts +21 -16
  498. package/src/interactive/interactive-shell.ts +2 -2
  499. package/src/interactive/interactive-slash-runtime.ts +63 -17
  500. package/src/interactive/interactive-subscriptions.ts +1 -1
  501. package/src/interactive/interactive-tickers.ts +1 -1
  502. package/src/interactive/keybinding-manager.ts +4 -4
  503. package/src/interactive/layout.ts +1 -1
  504. package/src/interactive/memory-overlay.ts +2 -2
  505. package/src/interactive/modal-marker.ts +2 -21
  506. package/src/interactive/mutation-preview.ts +1 -1
  507. package/src/interactive/overlay-frame.ts +4 -12
  508. package/src/interactive/overlay-general-openers.ts +6 -2
  509. package/src/interactive/overlay-key-routing.ts +7 -7
  510. package/src/interactive/overlay-lifecycle.ts +1 -1
  511. package/src/interactive/overlay-model-selectors.ts +3 -3
  512. package/src/interactive/overlays/agents.ts +2 -2
  513. package/src/interactive/overlays/ask-user.ts +25 -11
  514. package/src/interactive/overlays/context-reset.ts +28 -4
  515. package/src/interactive/overlays/cwd-fallback.ts +28 -4
  516. package/src/interactive/overlays/decisions.ts +4 -2
  517. package/src/interactive/overlays/fleet-run-approval.ts +3 -3
  518. package/src/interactive/overlays/handoff-review.ts +2 -2
  519. package/src/interactive/overlays/interop.ts +1 -1
  520. package/src/interactive/overlays/library-install-confirm.ts +1 -1
  521. package/src/interactive/overlays/message-picker.ts +1 -1
  522. package/src/interactive/overlays/model-selector.ts +39 -31
  523. package/src/interactive/overlays/prompts.ts +10 -4
  524. package/src/interactive/overlays/session-selector.ts +2 -2
  525. package/src/interactive/overlays/settings.ts +339 -293
  526. package/src/interactive/overlays/side-question.ts +2 -2
  527. package/src/interactive/overlays/skills-hub.ts +8 -8
  528. package/src/interactive/overlays/tree-selector.ts +2 -2
  529. package/src/interactive/panes-runtime.ts +36 -10
  530. package/src/interactive/permission-overlay.ts +2 -2
  531. package/src/interactive/renderers/retry-status.ts +12 -4
  532. package/src/interactive/renderers/tool-execution.ts +4 -4
  533. package/src/interactive/session-transcript.ts +3 -3
  534. package/src/interactive/slash-autocomplete.ts +454 -165
  535. package/src/interactive/slash-commands.ts +279 -183
  536. package/src/interactive/slash-spec.ts +160 -17
  537. package/src/interactive/status/controller.ts +2 -2
  538. package/src/interactive/stream-pacing-policy.ts +1 -1
  539. package/src/interactive/tasks-overlay.ts +20 -13
  540. package/src/interactive/terminal-lease.ts +6 -7
  541. package/src/interactive/turn-context.ts +11 -11
  542. package/src/interactive/turn-persistence.ts +2 -2
  543. package/src/interactive/turn-prewarm.ts +2 -2
  544. package/src/interactive/turn-runtime.ts +8 -8
  545. package/src/interactive/turn-state.ts +1 -1
  546. package/src/interactive/view/artifacts.ts +12 -9
  547. package/src/interactive/view/view-overlay.ts +13 -10
  548. package/src/interactive/watch-pane.ts +38 -10
  549. package/src/interactive/welcome-dashboard.ts +15 -15
  550. package/src/interactive/worker-progress.ts +4 -4
  551. package/src/interactive/worker-receipts.ts +1 -1
  552. package/src/interactive/worker-stream.ts +1 -4
  553. package/src/interactive/yazi-bridge.ts +12 -5
  554. package/src/tools/ask-user.ts +3 -3
  555. package/src/tools/bash.ts +1 -1
  556. package/src/tools/builtin-tool-catalog.ts +1 -1
  557. package/src/tools/compete-worktrees.ts +27 -17
  558. package/src/tools/context/index.ts +15 -1
  559. package/src/tools/dispatch-admission.ts +4 -2
  560. package/src/tools/dispatch-arguments.ts +1 -1
  561. package/src/tools/dispatch-plan.ts +0 -11
  562. package/src/tools/dispatch-run-events.ts +12 -10
  563. package/src/tools/dispatch-runner.ts +11 -6
  564. package/src/tools/dispatch.ts +1 -1
  565. package/src/tools/find.ts +1 -6
  566. package/src/tools/monitor.ts +4 -4
  567. package/src/tools/observers.ts +0 -33
  568. package/src/tools/task-worktree.ts +28 -7
  569. package/src/tools/verify/authoring.ts +3 -6
  570. package/src/tools/verify/frontend.ts +1 -1
  571. package/src/tools/web-fetch.ts +1 -1
  572. package/src/worker/entry.ts +4 -3
  573. package/src/worker/protocol.ts +16 -14
  574. package/src/worker/spec-contract.ts +2 -2
  575. package/src/worker/stdin-demux.ts +2 -2
  576. package/dist/chunk-GXNLGKAB.js +0 -571
  577. package/dist/chunk-VFA6GDY5.js +0 -1625
  578. package/dist/doctor-QOA5FNY5.js +0 -313
  579. package/src/engine/apis/degraded-inference.ts +0 -95
  580. /package/skills/meta/{clio-test → clio-coder-test}/references/harness.md +0 -0
  581. /package/skills/meta/{clio-test → clio-coder-test}/references/test-map.md +0 -0
@@ -5,11 +5,11 @@
5
5
  * loader has finished booting.
6
6
  *
7
7
  * There is exactly one schema: the file on disk and the in-memory shape use
8
- * the same key names (`targets`, `orchestrator.target`, ...). Validation is
8
+ * the same version-2 key names (`targets`, `chat.target`, ...). Validation is
9
9
  * strict: unknown keys and type violations are errors carrying the exact key
10
- * path; there are no legacy readers, aliases, or migrations. Missing keys take
11
- * DEFAULT_SETTINGS values, which is well-defined because the file is
12
- * machine-owned and written whole.
10
+ * path. Version-1 paths are non-executing tombstones with migration hints;
11
+ * lifecycle owns the one-time file migration. Missing keys take
12
+ * DEFAULT_SETTINGS values.
13
13
  */
14
14
 
15
15
  import { existsSync, readFileSync } from "node:fs";
@@ -25,6 +25,7 @@ import {
25
25
  THINKING_LEVELS,
26
26
  type ThinkingLevel,
27
27
  } from "./defaults.js";
28
+ import { warnLegacyNaming } from "./naming-compat.js";
28
29
  import { safeResourceWrite } from "./safe-resource-write.js";
29
30
  import { withStateFileLockSync } from "./state-file-lock.js";
30
31
  import { MAX_TIMER_DELAY_MS } from "./timers.js";
@@ -47,7 +48,7 @@ export function settingsPath(): string {
47
48
  export type SettingsIssueKind = "unreadable" | "syntax" | "schema";
48
49
 
49
50
  export interface SettingsIssue {
50
- /** Dotted key path, e.g. `orchestrator.target` or `targets[2].runtime`. */
51
+ /** Dotted key path, e.g. `chat.target` or `targets[2].runtime`. */
51
52
  path: string;
52
53
  message: string;
53
54
  kind?: SettingsIssueKind;
@@ -206,6 +207,20 @@ class Issues {
206
207
  if (!knownSet.has(key)) this.add(path ? `${path}.${key}` : key, "unknown key");
207
208
  }
208
209
  }
210
+
211
+ /**
212
+ * A key that used to be honored and is not any more. `unknown key` is the
213
+ * true answer for a typo and the wrong one here: the operator wrote a real
214
+ * key that a real release read, and the useful thing to tell them is what
215
+ * replaced it, not that the schema has never heard of it. The keys stay
216
+ * listed as known at their {@link unknownKeys} call so this message is the
217
+ * one that lands.
218
+ */
219
+ retiredKeys(path: string, raw: Record<string, unknown>, retired: Readonly<Record<string, string>>): void {
220
+ for (const [key, because] of Object.entries(retired)) {
221
+ if (key in raw) this.add(path ? `${path}.${key}` : key, `retired: ${because}. Remove this key.`);
222
+ }
223
+ }
209
224
  }
210
225
 
211
226
  function expectString(issues: Issues, path: string, value: unknown): string | undefined {
@@ -332,7 +347,13 @@ const THINKING_FORMATS = [
332
347
  ] as const;
333
348
  const STRUCTURED_OUTPUTS = ["json-schema", "gbnf", "xgrammar", "none"] as const;
334
349
  const AUTONOMY_LEVELS = ["read-only", "suggest", "auto-edit", "full-auto"] as const;
335
- const TOOL_GOVERNANCE = ["clio-policy", "agent-managed", "deny-all"] as const;
350
+ const TOOL_GOVERNANCE = ["clio-coder-policy", "agent-managed", "deny-all"] as const;
351
+
352
+ function normalizeLegacyNamingValue(value: unknown, legacy: string, canonical: string): unknown {
353
+ if (value !== legacy) return value;
354
+ warnLegacyNaming(legacy, canonical);
355
+ return canonical;
356
+ }
336
357
 
337
358
  type TargetCapabilities = NonNullable<ClioSettings["targets"][number]["capabilities"]>;
338
359
 
@@ -552,7 +573,12 @@ function validateTarget(issues: Issues, path: string, value: unknown): ClioSetti
552
573
  if (v !== undefined) target.capabilities = v;
553
574
  }
554
575
  if ("lifecycle" in value) {
555
- const v = expectEnum(issues, `${path}.lifecycle`, value.lifecycle, ["user-managed", "clio-managed"] as const);
576
+ const v = expectEnum(
577
+ issues,
578
+ `${path}.lifecycle`,
579
+ normalizeLegacyNamingValue(value.lifecycle, "clio-managed", "clio-coder-managed"),
580
+ ["user-managed", "clio-coder-managed"] as const,
581
+ );
556
582
  if (v !== undefined) target.lifecycle = v;
557
583
  }
558
584
  if ("gateway" in value) {
@@ -581,15 +607,231 @@ function validateTarget(issues: Issues, path: string, value: unknown): ClioSetti
581
607
  * every session that still mentions it. This is referential normalization,
582
608
  * not a legacy reader; the key names themselves are never aliased.
583
609
  */
584
- function validateWorkerTarget(
610
+ type FleetRoute = ClioSettings["fleet"]["default"];
611
+ type ExternalAgentDefaults = ClioSettings["integrations"]["externalAgents"]["defaults"];
612
+ type ExternalAgent = ClioSettings["integrations"]["externalAgents"]["entries"][number];
613
+ type FleetNodeConfig = ClioSettings["fleet"]["nodes"][number];
614
+
615
+ /**
616
+ * Complete durable path manifest for the settings-v1 to settings-v2 rename.
617
+ * `*` denotes a map key and `[]` denotes a list element. Values are copied
618
+ * without reinterpretation unless a separate retired-path entry says they are
619
+ * deliberately dropped.
620
+ */
621
+ export const SETTINGS_V1_PATH_MOVES = Object.freeze([
622
+ ["autonomy", "safety.autonomy"],
623
+ ["orchestrator.target", "chat.target"],
624
+ ["orchestrator.model", "chat.model"],
625
+ ["orchestrator.thinkingLevel", "chat.thinkingLevel"],
626
+ ["scope[]", "chat.modelPicker.cycleSet[]"],
627
+ ["modelSelector.favorites[]", "chat.modelPicker.favorites[]"],
628
+ ["modelSelector.recentLimit", "chat.modelPicker.recentLimit"],
629
+ ["defaults.maxTokens", "chat.maxOutputTokens"],
630
+ ["prewarm.enabled", "chat.prewarm"],
631
+ ["retry.enabled", "chat.retry.enabled"],
632
+ ["retry.maxRetries", "chat.retry.maxRetries"],
633
+ ["retry.baseDelayMs", "chat.retry.baseDelayMs"],
634
+ ["retry.maxDelayMs", "chat.retry.maxDelayMs"],
635
+ ["retry.streamStallMs", "chat.retry.streamStallMs"],
636
+ ["workers.default.target", "fleet.default.target"],
637
+ ["workers.default.model", "fleet.default.model"],
638
+ ["workers.default.thinkingLevel", "fleet.default.thinkingLevel"],
639
+ ["workers.default.node", "fleet.default.node"],
640
+ ["workers.profiles.*.target", "fleet.profiles.*.target"],
641
+ ["workers.profiles.*.model", "fleet.profiles.*.model"],
642
+ ["workers.profiles.*.thinkingLevel", "fleet.profiles.*.thinkingLevel"],
643
+ ["workers.profiles.*.node", "fleet.profiles.*.node"],
644
+ ["workers.rosters.*.members[].label", "fleet.rosters.*.members[].label"],
645
+ ["workers.rosters.*.members[].target", "fleet.rosters.*.members[].target"],
646
+ ["workers.rosters.*.members[].model", "fleet.rosters.*.members[].model"],
647
+ ["workers.rosters.*.members[].thinking", "fleet.rosters.*.members[].thinkingLevel"],
648
+ ["workers.rosters.*.members[].color", "fleet.rosters.*.members[].color"],
649
+ ["workers.agentBindings.*", "fleet.agentProfiles.*"],
650
+ ["routing.activeRoles[]", "fleet.adaptiveRouting.roles[]"],
651
+ ["routing.activePostures[]", "fleet.adaptiveRouting.postures[]"],
652
+ ["routing.agentAutomation.activeAgentRoles[].agentId", "fleet.adaptiveRouting.agentRoles[].agentId"],
653
+ ["routing.agentAutomation.activeAgentRoles[].executionRole", "fleet.adaptiveRouting.agentRoles[].executionRole"],
654
+ ["budget.concurrency", "fleet.concurrency"],
655
+ ["workers.maxRetries", "fleet.retry.maxRetries"],
656
+ ["workers.resilienceCooldownMs", "fleet.retry.routeCooldownMs"],
657
+ ["workers.onPermission", "fleet.permissions.mode"],
658
+ ["workers.escalation.timeoutMs", "fleet.permissions.escalation.timeoutMs"],
659
+ ["workers.escalation.fallback", "fleet.permissions.escalation.fallback"],
660
+ ["guardrails.workerToolCallCap", "fleet.limits.toolCallsPerRun"],
661
+ ["guardrails.internalDispatchTimeoutMs", "fleet.limits.internalRunTimeoutMs"],
662
+ ["guardrails.maxDispatchRuns", "fleet.history.maxRuns"],
663
+ ["compaction.auto", "context.compaction.auto"],
664
+ ["compaction.threshold", "context.compaction.threshold"],
665
+ ["compaction.model", "context.compaction.model"],
666
+ ["compaction.systemPrompt", "context.compaction.systemPrompt"],
667
+ ["background.target", "context.memory.target"],
668
+ ["background.model", "context.memory.model"],
669
+ ["memory.intervention.enabled", "context.memory.enabled"],
670
+ ["memory.intervention.everyNTools", "context.memory.cadenceToolCalls"],
671
+ ["memory.intervention.windowSteps", "context.memory.trajectorySteps"],
672
+ ["memory.intervention.maxTokens", "context.memory.maxOutputTokens"],
673
+ ["memory.intervention.timeoutMs", "context.memory.timeoutMs"],
674
+ ["budget.sessionCeilingUsd", "safety.limits.sessionCostUsd"],
675
+ ["guardrails.turnToolCallBudget", "safety.limits.chatToolCallsPerTurn"],
676
+ ["guardrails.readMaxBytes", "safety.limits.readBytesPerCall"],
677
+ ["guardrails.observationTurnBudgetBytes", "safety.limits.observationBytesPerTurn"],
678
+ ["watchdog.enabled", "safety.review.enabled"],
679
+ ["watchdog.target", "safety.review.target"],
680
+ ["watchdog.cadenceToolCalls", "safety.review.cadenceToolCalls"],
681
+ ["terminal.outputVerbosity", "interface.outputDetail"],
682
+ ["terminal.smoothStreaming", "interface.smoothStreaming"],
683
+ ["terminal.tuiMode", "interface.mode"],
684
+ ["terminal.fullscreenScrollbar", "interface.fullscreenScrollbar"],
685
+ ["terminal.showTerminalProgress", "interface.terminalProgress"],
686
+ ["terminal.notify", "interface.desktopNotifications"],
687
+ // The panes keys shipped for exactly one release (v0.4.0, the day before
688
+ // this rework) but they did ship, and each has a direct v2 successor with
689
+ // an identical value domain, so they move like everything else. The
690
+ // retired agents/keepFailed pair and the `panes` root itself stay in
691
+ // SETTINGS_V1_RETIRED-adjacent handling via V1_ONLY_ROOTS.
692
+ ["panes.enabled", "interface.panes.enabled"],
693
+ ["panes.notifications", "interface.panes.notifications"],
694
+ ["panes.journal", "fleet.history.journal"],
695
+ ["panes.yazi.enabled", "interface.panes.files.enabled"],
696
+ ["panes.yazi.mode", "interface.panes.files.mode"],
697
+ ["panes.yazi.profile", "interface.panes.files.profile"],
698
+ ["panes.yazi.followCwd", "interface.panes.files.followCwd"],
699
+ ["keybindings.*", "interface.keybindings.*"],
700
+ ["skills.trustProjectCompatRoots", "integrations.projectResources.trustProjectImports"],
701
+ ["delegation.defaults.connectTimeoutMs", "integrations.externalAgents.defaults.connectTimeoutMs"],
702
+ ["delegation.defaults.turnTimeoutMs", "integrations.externalAgents.defaults.turnTimeoutMs"],
703
+ ["delegation.defaults.permissionTimeoutMs", "integrations.externalAgents.defaults.permissionTimeoutMs"],
704
+ ["delegation.defaults.toolGovernance", "integrations.externalAgents.defaults.toolGovernance"],
705
+ ["delegation.agents[].id", "integrations.externalAgents.entries[].id"],
706
+ ["delegation.agents[].command", "integrations.externalAgents.entries[].command"],
707
+ ["delegation.agents[].args[]", "integrations.externalAgents.entries[].args[]"],
708
+ ["delegation.agents[].cwd", "integrations.externalAgents.entries[].cwd"],
709
+ ["delegation.agents[].env.*", "integrations.externalAgents.entries[].env.*"],
710
+ ["delegation.agents[].connectTimeoutMs", "integrations.externalAgents.entries[].connectTimeoutMs"],
711
+ ["delegation.agents[].turnTimeoutMs", "integrations.externalAgents.entries[].turnTimeoutMs"],
712
+ ["delegation.agents[].permissionTimeoutMs", "integrations.externalAgents.entries[].permissionTimeoutMs"],
713
+ ["delegation.agents[].stallTimeoutMs", "integrations.externalAgents.entries[].stallTimeoutMs"],
714
+ ["delegation.agents[].toolGovernance", "integrations.externalAgents.entries[].toolGovernance"],
715
+ ["delegation.agents[].projectContext", "integrations.externalAgents.entries[].projectContext"],
716
+ ["delegation.agents[].labels.*", "integrations.externalAgents.entries[].labels.*"],
717
+ ["runtimePlugins[]", "integrations.runtimePlugins[]"],
718
+ ["library.catalog", "integrations.library.catalog"],
719
+ ["library.remote", "integrations.library.remote"],
720
+ ["library.confirmedRemote", "integrations.library.confirmedRemote"],
721
+ ["library.sync", "integrations.library.sync"],
722
+ ["attribution.gitCommits", "integrations.git.commitAttribution"],
723
+ ["fleet.nodes[].clioEntry", "fleet.nodes[].clioCoderEntry"],
724
+ ] as const);
725
+
726
+ export const SETTINGS_V1_RETIRED_PATHS = Object.freeze({
727
+ identity: "it was accepted and ignored; no behavior is lost",
728
+ "background.thinkingLevel": "proactive memory always resolves thinking off",
729
+ theme: "the only registered theme was not read by runtime rendering",
730
+ "compaction.excludeLastTurns": "only the temporary legacy mask used it; context.workingSet.protectLastTurns remains",
731
+ } as const);
732
+
733
+ const V1_ONLY_ROOTS = new Set([
734
+ "identity",
735
+ "autonomy",
736
+ "runtimePlugins",
737
+ "orchestrator",
738
+ "background",
739
+ "memory",
740
+ "watchdog",
741
+ "workers",
742
+ "routing",
743
+ "scope",
744
+ "modelSelector",
745
+ "budget",
746
+ "defaults",
747
+ "theme",
748
+ "terminal",
749
+ "skills",
750
+ "library",
751
+ "attribution",
752
+ "delegation",
753
+ "keybindings",
754
+ "compaction",
755
+ "prewarm",
756
+ "panes",
757
+ "retry",
758
+ "guardrails",
759
+ ]);
760
+
761
+ interface PatternMatch {
762
+ path: string;
763
+ }
764
+
765
+ function pathPatternSegments(pattern: string): string[] {
766
+ return pattern.split(".");
767
+ }
768
+
769
+ function patternMatches(value: unknown, pattern: string): PatternMatch[] {
770
+ const out: PatternMatch[] = [];
771
+ const visit = (current: unknown, segments: ReadonlyArray<string>, path: string): void => {
772
+ const segment = segments[0];
773
+ if (segment === undefined) {
774
+ out.push({ path });
775
+ return;
776
+ }
777
+ const rest = segments.slice(1);
778
+ if (segment === "*") {
779
+ if (!isPlainObject(current)) return;
780
+ for (const [key, child] of Object.entries(current)) visit(child, rest, path ? `${path}.${key}` : key);
781
+ return;
782
+ }
783
+ const list = segment.endsWith("[]");
784
+ const key = list ? segment.slice(0, -2) : segment;
785
+ if (!isPlainObject(current) || !(key in current)) return;
786
+ const child = current[key];
787
+ const nextPath = path ? `${path}.${key}` : key;
788
+ if (!list) {
789
+ visit(child, rest, nextPath);
790
+ return;
791
+ }
792
+ if (!Array.isArray(child)) return;
793
+ for (let index = 0; index < child.length; index += 1) visit(child[index], rest, `${nextPath}[${index}]`);
794
+ };
795
+ visit(value, pathPatternSegments(pattern), "");
796
+ return out;
797
+ }
798
+
799
+ function reportV1Tombstones(issues: Issues, raw: Record<string, unknown>): void {
800
+ const reportedRoots = new Set<string>();
801
+ for (const [from, to] of SETTINGS_V1_PATH_MOVES) {
802
+ for (const match of patternMatches(raw, from)) {
803
+ issues.add(match.path, `retired settings-v1 path; use ${to} in version 2`);
804
+ reportedRoots.add(match.path.split(/[.[]/u)[0] ?? match.path);
805
+ }
806
+ }
807
+ for (const [path, reason] of Object.entries(SETTINGS_V1_RETIRED_PATHS)) {
808
+ for (const match of patternMatches(raw, path)) {
809
+ issues.add(match.path, `retired without replacement: ${reason}. Remove this key`);
810
+ reportedRoots.add(match.path.split(/[.[]/u)[0] ?? match.path);
811
+ }
812
+ }
813
+ for (const root of V1_ONLY_ROOTS) {
814
+ if (root in raw && !reportedRoots.has(root)) {
815
+ const replacements = new Set(
816
+ SETTINGS_V1_PATH_MOVES.filter(([from]) => from.split(/[.[]/u)[0] === root).map(
817
+ ([, to]) => to.split(/[.[]/u)[0] ?? to,
818
+ ),
819
+ );
820
+ const hint = replacements.size > 0 ? `; use ${[...replacements].join(" or ")} in version 2` : "";
821
+ issues.add(root, `retired settings-v1 namespace${hint}; migrate this document with \`clio-coder upgrade\``);
822
+ }
823
+ }
824
+ }
825
+
826
+ function validateRoute(
585
827
  issues: Issues,
586
828
  path: string,
587
829
  value: unknown,
588
- defaults: ClioSettings["orchestrator"],
830
+ defaults: FleetRoute,
589
831
  targets: ReadonlyArray<ClioSettings["targets"][number]>,
590
832
  options?: { nodeIds?: ReadonlySet<string> },
591
- ): ClioSettings["orchestrator"] & { node?: string } {
592
- const out: ClioSettings["orchestrator"] & { node?: string } = cloneValue(defaults);
833
+ ): FleetRoute {
834
+ const out = cloneValue(defaults);
593
835
  if (!isPlainObject(value)) {
594
836
  issues.add(path, `expected a map, got ${describe(value)}`);
595
837
  return out;
@@ -600,80 +842,58 @@ function validateWorkerTarget(
600
842
  value,
601
843
  allowNode ? ["target", "model", "thinkingLevel", "node"] : ["target", "model", "thinkingLevel"],
602
844
  );
603
- if ("target" in value && value.target !== null) {
604
- const v = expectString(issues, `${path}.target`, value.target);
605
- if (v !== undefined) out.target = targets.some((entry) => entry.id === v) ? v : null;
845
+ if ("target" in value) {
846
+ if (value.target === null) out.target = null;
847
+ else {
848
+ const parsed = expectString(issues, `${path}.target`, value.target);
849
+ if (parsed !== undefined) out.target = targets.some((entry) => entry.id === parsed) ? parsed : null;
850
+ }
606
851
  }
607
- if ("thinkingLevel" in value) {
608
- const v = expectEnum(issues, `${path}.thinkingLevel`, value.thinkingLevel, THINKING_LEVELS);
609
- if (v !== undefined) out.thinkingLevel = v;
610
- }
611
- if ("model" in value && value.model !== null) {
612
- const v = expectString(issues, `${path}.model`, value.model);
613
- if (v !== undefined) out.model = v;
614
- }
615
- // Node pins follow the target idiom: a pin naming a node that is not
616
- // configured (and is not the implicit local node) is dropped, not fatal.
617
- if (allowNode && "node" in value && value.node !== null) {
618
- const v = expectString(issues, `${path}.node`, value.node);
619
- if (v !== undefined) {
620
- const id = v.trim();
621
- if (id.length > 0 && (id === "local" || options?.nodeIds?.has(id) === true)) out.node = id;
852
+ if ("model" in value) {
853
+ if (value.model === null) out.model = null;
854
+ else {
855
+ const parsed = expectString(issues, `${path}.model`, value.model);
856
+ if (parsed !== undefined) out.model = parsed;
622
857
  }
623
858
  }
624
- if (!out.target) {
625
- out.model = null;
626
- } else if (out.model === null) {
627
- out.model = targets.find((entry) => entry.id === out.target)?.defaultModel ?? null;
859
+ if ("thinkingLevel" in value) {
860
+ const parsed = expectEnum(issues, `${path}.thinkingLevel`, value.thinkingLevel, THINKING_LEVELS);
861
+ if (parsed !== undefined) out.thinkingLevel = parsed;
862
+ }
863
+ if (allowNode && "node" in value) {
864
+ if (value.node === null) delete out.node;
865
+ else {
866
+ const parsed = expectString(issues, `${path}.node`, value.node);
867
+ if (parsed !== undefined && (parsed === "local" || options.nodeIds?.has(parsed) === true)) out.node = parsed;
868
+ }
628
869
  }
870
+ if (!out.target) out.model = null;
871
+ else if (out.model === null) out.model = targets.find((entry) => entry.id === out.target)?.defaultModel ?? null;
629
872
  return out;
630
873
  }
631
874
 
632
- /** `targetId/wireModelId` refs filtered to configured target ids. */
633
875
  function normalizeModelRefs(
634
876
  refs: ReadonlyArray<string>,
635
877
  targets: ReadonlyArray<ClioSettings["targets"][number]>,
636
878
  ): string[] {
637
- const byId = new Set(targets.map((target) => target.id));
638
- const out: string[] = [];
639
- for (const ref of refs) {
640
- const [targetId, ...modelParts] = ref.split("/");
641
- if (!targetId || !byId.has(targetId) || modelParts.length === 0) continue;
642
- const model = modelParts.join("/").trim();
643
- if (!model) continue;
644
- const normalized = `${targetId}/${model}`;
645
- if (!out.includes(normalized)) out.push(normalized);
646
- }
647
- return out;
879
+ const targetIds = new Set(targets.map((target) => target.id));
880
+ return refs.filter((ref) => {
881
+ const [targetId, ...model] = ref.split("/");
882
+ return Boolean(targetId && targetIds.has(targetId) && model.join("/").trim());
883
+ });
648
884
  }
649
885
 
650
- /**
651
- * Scope entries: `targetId` or `targetId/wireModelId`, deduplicated. A ref
652
- * whose target is not configured is kept, not dropped: a renamed or
653
- * temporarily removed target is the common staleness case, and silently
654
- * deleting the operator's cycle set at load destroys a preference they never
655
- * asked to change. Disclosure belongs to the presentation layer, which lists
656
- * unresolvable refs under "Unavailable"; the consumers that must resolve a ref
657
- * (advanceScopedTarget) filter it themselves.
658
- */
659
- function normalizeScope(refs: ReadonlyArray<string>): string[] {
660
- const out: string[] = [];
661
- for (const ref of refs) {
662
- const trimmed = ref.trim();
663
- const [targetId] = trimmed.split("/");
664
- if (!targetId) continue;
665
- if (!out.includes(trimmed)) out.push(trimmed);
666
- }
667
- return out;
886
+ function normalizeCycleSet(refs: ReadonlyArray<string>): string[] {
887
+ return refs.filter((ref) => Boolean(ref.trim().split("/")[0]));
668
888
  }
669
889
 
670
- function validateDelegationAgent(
890
+ function validateExternalAgent(
671
891
  issues: Issues,
672
892
  path: string,
673
893
  value: unknown,
674
- defaults: ClioSettings["delegation"]["defaults"],
894
+ defaults: ExternalAgentDefaults,
675
895
  seen: Set<string>,
676
- ): ClioSettings["delegation"]["agents"][number] | null {
896
+ ): ExternalAgent | null {
677
897
  if (!isPlainObject(value)) {
678
898
  issues.add(path, `expected a map, got ${describe(value)}`);
679
899
  return null;
@@ -702,11 +922,11 @@ function validateDelegationAgent(
702
922
  return null;
703
923
  }
704
924
  if (seen.has(id)) {
705
- issues.add(`${path}.id`, `duplicate delegation agent id '${id}'`);
925
+ issues.add(`${path}.id`, `duplicate external agent id '${id}'`);
706
926
  return null;
707
927
  }
708
928
  seen.add(id);
709
- const agent: ClioSettings["delegation"]["agents"][number] = {
929
+ const agent: ExternalAgent = {
710
930
  id,
711
931
  command,
712
932
  args: [],
@@ -716,89 +936,44 @@ function validateDelegationAgent(
716
936
  toolGovernance: defaults.toolGovernance,
717
937
  };
718
938
  if ("args" in value) {
719
- const v = expectStringArray(issues, `${path}.args`, value.args);
720
- if (v !== undefined) agent.args = v;
939
+ const parsed = expectStringArray(issues, `${path}.args`, value.args);
940
+ if (parsed !== undefined) agent.args = parsed;
721
941
  }
722
942
  for (const key of ["connectTimeoutMs", "turnTimeoutMs", "permissionTimeoutMs"] as const) {
723
- if (key in value) {
724
- const v = expectInteger(issues, `${path}.${key}`, value[key], { min: 1, max: MAX_TIMER_DELAY_MS });
725
- if (v !== undefined) agent[key] = v;
726
- }
943
+ if (!(key in value)) continue;
944
+ const parsed = expectInteger(issues, `${path}.${key}`, value[key], { min: 1, max: MAX_TIMER_DELAY_MS });
945
+ if (parsed !== undefined) agent[key] = parsed;
727
946
  }
728
947
  if ("stallTimeoutMs" in value) {
729
- const v = expectInteger(issues, `${path}.stallTimeoutMs`, value.stallTimeoutMs);
730
- if (v !== undefined) agent.stallTimeoutMs = v;
948
+ const parsed = expectInteger(issues, `${path}.stallTimeoutMs`, value.stallTimeoutMs);
949
+ if (parsed !== undefined) agent.stallTimeoutMs = parsed;
731
950
  }
732
951
  if ("toolGovernance" in value) {
733
- const v = expectEnum(issues, `${path}.toolGovernance`, value.toolGovernance, TOOL_GOVERNANCE);
734
- if (v !== undefined) agent.toolGovernance = v;
952
+ const parsed = expectEnum(
953
+ issues,
954
+ `${path}.toolGovernance`,
955
+ normalizeLegacyNamingValue(value.toolGovernance, "clio-policy", "clio-coder-policy"),
956
+ TOOL_GOVERNANCE,
957
+ );
958
+ if (parsed !== undefined) agent.toolGovernance = parsed;
735
959
  }
736
960
  if ("projectContext" in value) {
737
- const v = expectEnum(issues, `${path}.projectContext`, value.projectContext, ["none", "bounded"] as const);
738
- if (v !== undefined) agent.projectContext = v;
961
+ const parsed = expectEnum(issues, `${path}.projectContext`, value.projectContext, ["none", "bounded"] as const);
962
+ if (parsed !== undefined) agent.projectContext = parsed;
739
963
  }
740
- if ("cwd" in value) {
741
- const v = expectString(issues, `${path}.cwd`, value.cwd);
742
- if (v !== undefined) agent.cwd = v;
964
+ for (const key of ["cwd"] as const) {
965
+ if (!(key in value)) continue;
966
+ const parsed = expectString(issues, `${path}.${key}`, value[key]);
967
+ if (parsed !== undefined) agent[key] = parsed;
743
968
  }
744
- if ("env" in value) {
745
- const v = expectStringRecord(issues, `${path}.env`, value.env);
746
- if (v !== undefined && Object.keys(v).length > 0) agent.env = v;
747
- }
748
- if ("labels" in value) {
749
- const v = expectStringRecord(issues, `${path}.labels`, value.labels);
750
- if (v !== undefined && Object.keys(v).length > 0) agent.labels = v;
969
+ for (const key of ["env", "labels"] as const) {
970
+ if (!(key in value)) continue;
971
+ const parsed = expectStringRecord(issues, `${path}.${key}`, value[key]);
972
+ if (parsed !== undefined && Object.keys(parsed).length > 0) agent[key] = parsed;
751
973
  }
752
974
  return agent;
753
975
  }
754
976
 
755
- function validateDelegation(issues: Issues, value: unknown): ClioSettings["delegation"] {
756
- const out = cloneValue(DEFAULT_SETTINGS.delegation);
757
- if (!isPlainObject(value)) {
758
- issues.add("delegation", `expected a map, got ${describe(value)}`);
759
- return out;
760
- }
761
- issues.unknownKeys("delegation", value, ["agents", "defaults"]);
762
- if ("defaults" in value) {
763
- if (!isPlainObject(value.defaults)) {
764
- issues.add("delegation.defaults", `expected a map, got ${describe(value.defaults)}`);
765
- } else {
766
- issues.unknownKeys("delegation.defaults", value.defaults, [
767
- "connectTimeoutMs",
768
- "turnTimeoutMs",
769
- "permissionTimeoutMs",
770
- "toolGovernance",
771
- ]);
772
- for (const key of ["connectTimeoutMs", "turnTimeoutMs", "permissionTimeoutMs"] as const) {
773
- if (key in value.defaults) {
774
- const v = expectInteger(issues, `delegation.defaults.${key}`, value.defaults[key], {
775
- min: 1,
776
- max: MAX_TIMER_DELAY_MS,
777
- });
778
- if (v !== undefined) out.defaults[key] = v;
779
- }
780
- }
781
- if ("toolGovernance" in value.defaults) {
782
- const v = expectEnum(issues, "delegation.defaults.toolGovernance", value.defaults.toolGovernance, TOOL_GOVERNANCE);
783
- if (v !== undefined) out.defaults.toolGovernance = v;
784
- }
785
- }
786
- }
787
- if ("agents" in value) {
788
- if (!Array.isArray(value.agents)) {
789
- issues.add("delegation.agents", `expected a list, got ${describe(value.agents)}`);
790
- } else {
791
- const seen = new Set<string>();
792
- out.agents = value.agents
793
- .map((entry, i) => validateDelegationAgent(issues, `delegation.agents[${i}]`, entry, out.defaults, seen))
794
- .filter((entry): entry is NonNullable<typeof entry> => entry !== null);
795
- }
796
- }
797
- return out;
798
- }
799
-
800
- type FleetNodeConfig = ClioSettings["fleet"]["nodes"][number];
801
-
802
977
  function validateFleetNode(issues: Issues, path: string, value: unknown, seen: Set<string>): FleetNodeConfig | null {
803
978
  if (!isPlainObject(value)) {
804
979
  issues.add(path, `expected a map, got ${describe(value)}`);
@@ -811,7 +986,6 @@ function validateFleetNode(issues: Issues, path: string, value: unknown, seen: S
811
986
  "port",
812
987
  "identityFile",
813
988
  "clioCoderEntry",
814
- // Pre-0.3.1 spelling, still read.
815
989
  "clioEntry",
816
990
  "labels",
817
991
  "maxWorkers",
@@ -822,197 +996,79 @@ function validateFleetNode(issues: Issues, path: string, value: unknown, seen: S
822
996
  if (!("id" in value)) issues.add(`${path}.id`, "required");
823
997
  if (!("host" in value)) issues.add(`${path}.host`, "required");
824
998
  if (id === undefined || host === undefined) return null;
825
- const trimmedId = id.trim();
826
- if (trimmedId.length === 0) {
827
- issues.add(`${path}.id`, "must not be blank");
828
- return null;
829
- }
830
- if (trimmedId === "local") {
999
+ if (id === "local") {
831
1000
  issues.add(`${path}.id`, "'local' is reserved for the implicit local node");
832
1001
  return null;
833
1002
  }
834
- if (seen.has(trimmedId)) {
835
- issues.add(`${path}.id`, `duplicate fleet node id '${trimmedId}'`);
1003
+ if (seen.has(id)) {
1004
+ issues.add(`${path}.id`, `duplicate fleet node id '${id}'`);
836
1005
  return null;
837
1006
  }
838
- seen.add(trimmedId);
839
- const node: FleetNodeConfig = { id: trimmedId, host, maxWorkers: 2 };
840
- if ("user" in value) {
841
- const v = expectString(issues, `${path}.user`, value.user);
842
- if (v !== undefined) node.user = v;
1007
+ seen.add(id);
1008
+ const node: FleetNodeConfig = { id, host, maxWorkers: 2 };
1009
+ for (const key of ["user", "identityFile", "clioCoderEntry"] as const) {
1010
+ if (!(key in value)) continue;
1011
+ const parsed = expectString(issues, `${path}.${key}`, value[key]);
1012
+ if (parsed !== undefined) node[key] = parsed;
843
1013
  }
844
1014
  if ("port" in value) {
845
- const v = expectInteger(issues, `${path}.port`, value.port, { min: 1 });
846
- if (v !== undefined) node.port = v;
847
- }
848
- if ("identityFile" in value) {
849
- const v = expectString(issues, `${path}.identityFile`, value.identityFile);
850
- if (v !== undefined) node.identityFile = v;
851
- }
852
- // The key was `clioEntry` before 0.3.1. Both spellings read; the new one
853
- // wins when a node carries both.
854
- if ("clioEntry" in value) {
855
- const v = expectString(issues, `${path}.clioEntry`, value.clioEntry);
856
- if (v !== undefined) node.clioCoderEntry = v;
857
- }
858
- if ("clioCoderEntry" in value) {
859
- const v = expectString(issues, `${path}.clioCoderEntry`, value.clioCoderEntry);
860
- if (v !== undefined) node.clioCoderEntry = v;
1015
+ const parsed = expectInteger(issues, `${path}.port`, value.port, { min: 1 });
1016
+ if (parsed !== undefined) node.port = parsed;
861
1017
  }
862
1018
  if ("labels" in value) {
863
- const v = expectStringArray(issues, `${path}.labels`, value.labels);
864
- if (v !== undefined) node.labels = v;
1019
+ const parsed = expectStringArray(issues, `${path}.labels`, value.labels);
1020
+ if (parsed !== undefined) node.labels = parsed;
865
1021
  }
866
1022
  if ("maxWorkers" in value) {
867
- const v = expectInteger(issues, `${path}.maxWorkers`, value.maxWorkers, { min: 1 });
868
- if (v !== undefined) node.maxWorkers = v;
1023
+ const parsed = expectInteger(issues, `${path}.maxWorkers`, value.maxWorkers, { min: 1 });
1024
+ if (parsed !== undefined) node.maxWorkers = parsed;
869
1025
  }
870
1026
  if ("residency" in value) {
871
- const v = expectEnum(issues, `${path}.residency`, value.residency, ["observe", "manage"] as const);
872
- if (v !== undefined) node.residency = v;
1027
+ const parsed = expectEnum(issues, `${path}.residency`, value.residency, ["observe", "manage"] as const);
1028
+ if (parsed !== undefined) node.residency = parsed;
873
1029
  }
874
1030
  return node;
875
1031
  }
876
1032
 
877
- function validateFleet(issues: Issues, value: unknown): ClioSettings["fleet"] {
878
- const out = cloneValue(DEFAULT_SETTINGS.fleet);
879
- if (!isPlainObject(value)) {
880
- issues.add("fleet", `expected a map, got ${describe(value)}`);
881
- return out;
882
- }
883
- issues.unknownKeys("fleet", value, ["nodes"]);
884
- if ("nodes" in value) {
885
- if (!Array.isArray(value.nodes)) {
886
- issues.add("fleet.nodes", `expected a list, got ${describe(value.nodes)}`);
887
- } else {
888
- const seen = new Set<string>();
889
- out.nodes = value.nodes
890
- .map((entry, i) => validateFleetNode(issues, `fleet.nodes[${i}]`, entry, seen))
891
- .filter((entry): entry is NonNullable<typeof entry> => entry !== null);
892
- }
893
- }
894
- return out;
895
- }
896
-
897
- function validateMemorySettings(issues: Issues, value: unknown): ClioSettings["memory"] {
898
- const out = cloneValue(DEFAULT_SETTINGS.memory);
899
- if (!isPlainObject(value)) {
900
- issues.add("memory", `expected a map, got ${describe(value)}`);
901
- return out;
902
- }
903
- issues.unknownKeys("memory", value, ["intervention"]);
904
- if (!("intervention" in value)) return out;
905
- if (!isPlainObject(value.intervention)) {
906
- issues.add("memory.intervention", `expected a map, got ${describe(value.intervention)}`);
907
- return out;
908
- }
909
- const raw = value.intervention;
910
- issues.unknownKeys("memory.intervention", raw, ["enabled", "everyNTools", "windowSteps", "maxTokens", "timeoutMs"]);
911
- if ("enabled" in raw) {
912
- const enabled = expectBoolean(issues, "memory.intervention.enabled", raw.enabled);
913
- if (enabled !== undefined) out.intervention.enabled = enabled;
914
- }
915
- for (const key of ["everyNTools", "windowSteps", "maxTokens", "timeoutMs"] as const) {
916
- if (!(key in raw)) continue;
917
- const parsed = expectInteger(issues, `memory.intervention.${key}`, raw[key], {
918
- min: key === "everyNTools" ? 2 : 1,
919
- });
920
- if (parsed !== undefined) out.intervention[key] = parsed;
921
- }
922
- return out;
923
- }
924
-
925
- /**
926
- * The watchdog block. `target` and `cadenceToolCalls` stay absent rather than
927
- * taking sentinel values: an absent target means the session's active target,
928
- * and an absent cadence means the watchdog fires only at turn end. A written
929
- * default for either would turn "not configured" into a configuration.
930
- */
931
- function validateWatchdogSettings(issues: Issues, value: unknown): ClioSettings["watchdog"] {
932
- const out = cloneValue(DEFAULT_SETTINGS.watchdog);
933
- if (!isPlainObject(value)) {
934
- issues.add("watchdog", `expected a map, got ${describe(value)}`);
935
- return out;
936
- }
937
- issues.unknownKeys("watchdog", value, ["enabled", "target", "cadenceToolCalls"]);
938
- if ("enabled" in value) {
939
- const enabled = expectBoolean(issues, "watchdog.enabled", value.enabled);
940
- if (enabled !== undefined) out.enabled = enabled;
941
- }
942
- if ("target" in value && value.target !== null) {
943
- const target = expectString(issues, "watchdog.target", value.target);
944
- if (target !== undefined) {
945
- if (target.trim().length === 0) issues.add("watchdog.target", "expected a non-empty target id");
946
- else out.target = target.trim();
947
- }
948
- }
949
- if ("cadenceToolCalls" in value && value.cadenceToolCalls !== null) {
950
- const cadence = expectInteger(issues, "watchdog.cadenceToolCalls", value.cadenceToolCalls, { min: 1 });
951
- if (cadence !== undefined) out.cadenceToolCalls = cadence;
952
- }
953
- return out;
954
- }
955
-
956
- function validateKeybindings(issues: Issues, value: unknown): ClioSettings["keybindings"] {
1033
+ function validateKeybindings(issues: Issues, path: string, value: unknown): Record<string, string | string[]> {
957
1034
  if (!isPlainObject(value)) {
958
- issues.add("keybindings", `expected a map, got ${describe(value)}`);
1035
+ issues.add(path, `expected a map, got ${describe(value)}`);
959
1036
  return {};
960
1037
  }
961
1038
  const next: Record<string, string | string[]> = {};
962
1039
  for (const [rawKey, rawValue] of Object.entries(value)) {
963
- const id = rawKey.trim();
1040
+ const legacyId = rawKey.trim();
1041
+ const id = legacyId.startsWith("clio.") ? `clio-coder.${legacyId.slice("clio.".length)}` : legacyId;
964
1042
  if (!id) {
965
- issues.add("keybindings", "empty keybinding id");
966
- continue;
967
- }
968
- if (typeof rawValue === "string") {
969
- const v = expectString(issues, `keybindings.${id}`, rawValue);
970
- if (v !== undefined) next[id] = v;
1043
+ issues.add(path, "empty keybinding id");
971
1044
  continue;
972
1045
  }
973
- if (Array.isArray(rawValue)) {
974
- const v = expectStringArray(issues, `keybindings.${id}`, rawValue);
975
- if (v !== undefined && v.length > 0) next[id] = v;
976
- continue;
1046
+ if (id !== legacyId) {
1047
+ warnLegacyNaming(legacyId, id);
1048
+ // Canonical values win even when the legacy key occurs later in YAML.
1049
+ if (Object.hasOwn(value, id)) continue;
977
1050
  }
978
- issues.add(`keybindings.${id}`, `expected a string or list of strings, got ${describe(rawValue)}`);
1051
+ if (typeof rawValue === "string") {
1052
+ const parsed = expectString(issues, `${path}.${legacyId}`, rawValue);
1053
+ if (parsed !== undefined) next[id] = parsed;
1054
+ } else if (Array.isArray(rawValue)) {
1055
+ const parsed = expectStringArray(issues, `${path}.${legacyId}`, rawValue);
1056
+ if (parsed !== undefined && parsed.length > 0) next[id] = parsed;
1057
+ } else issues.add(`${path}.${legacyId}`, `expected a string or list of strings, got ${describe(rawValue)}`);
979
1058
  }
980
1059
  return next;
981
1060
  }
982
1061
 
983
1062
  const TOP_LEVEL_KEYS = [
984
1063
  "version",
985
- // Accepted and ignored. Settings files generated before 0.3.1 carry an
986
- // `identity:` line for a control nothing ever read; listing it here keeps
987
- // those files loading without an unknown-key issue.
988
- "identity",
989
- "autonomy",
990
1064
  "targets",
991
- "runtimePlugins",
992
- "orchestrator",
993
- "background",
994
- "memory",
995
- "watchdog",
996
- "workers",
1065
+ "chat",
997
1066
  "fleet",
998
- "routing",
999
- "scope",
1000
- "modelSelector",
1001
- "budget",
1002
- "defaults",
1003
- "theme",
1004
- "terminal",
1005
- "skills",
1006
- "library",
1007
- "attribution",
1008
- "delegation",
1009
- "keybindings",
1010
- "compaction",
1011
1067
  "context",
1012
- "prewarm",
1013
- "panes",
1014
- "retry",
1015
- "guardrails",
1068
+ "safety",
1069
+ "interface",
1070
+ "integrations",
1071
+ ...V1_ONLY_ROOTS,
1016
1072
  ] as const;
1017
1073
 
1018
1074
  export interface SettingsValidationResult {
@@ -1030,529 +1086,412 @@ export interface SettingsValidationResult {
1030
1086
  export function validateSettings(raw: unknown): SettingsValidationResult {
1031
1087
  const issues = new Issues();
1032
1088
  const settings = cloneValue(DEFAULT_SETTINGS);
1033
- // A genuinely missing settings file falls back to defaults before ever
1034
- // reaching here (validateSettingsFile returns early when the file is absent).
1035
- // So a null/undefined raw here means a present-but-empty or `null` document,
1036
- // which is malformed and must be a root-shape issue, not silent defaults.
1037
1089
  if (!isPlainObject(raw)) {
1038
1090
  issues.add("(root)", `expected a map, got ${describe(raw)}`);
1039
1091
  return { settings, issues: issues.list };
1040
1092
  }
1093
+ reportV1Tombstones(issues, raw);
1041
1094
  issues.unknownKeys("", raw, TOP_LEVEL_KEYS);
1042
-
1043
- if ("version" in raw && raw.version !== 1) {
1044
- issues.add("version", `expected 1, got ${describe(raw.version)}`);
1045
- }
1046
- if ("autonomy" in raw) {
1047
- const v = expectEnum(issues, "autonomy", raw.autonomy, AUTONOMY_LEVELS);
1048
- if (v !== undefined) settings.autonomy = v;
1095
+ if ("version" in raw && raw.version !== 2) {
1096
+ issues.add("version", `expected 2, got ${describe(raw.version)}; run \`clio-coder upgrade\` for version 1`);
1049
1097
  }
1050
1098
 
1051
1099
  if ("targets" in raw) {
1052
- if (!Array.isArray(raw.targets)) {
1053
- issues.add("targets", `expected a list, got ${describe(raw.targets)}`);
1054
- } else {
1100
+ if (!Array.isArray(raw.targets)) issues.add("targets", `expected a list, got ${describe(raw.targets)}`);
1101
+ else {
1055
1102
  const seen = new Set<string>();
1056
- const out: ClioSettings["targets"] = [];
1057
- for (let i = 0; i < raw.targets.length; i += 1) {
1058
- const target = validateTarget(issues, `targets[${i}]`, raw.targets[i]);
1103
+ const targets: ClioSettings["targets"] = [];
1104
+ for (let index = 0; index < raw.targets.length; index += 1) {
1105
+ const target = validateTarget(issues, `targets[${index}]`, raw.targets[index]);
1059
1106
  if (!target) continue;
1060
1107
  if (seen.has(target.id)) {
1061
- issues.add(`targets[${i}].id`, `duplicate target id '${target.id}'`);
1108
+ issues.add(`targets[${index}].id`, `duplicate target id '${target.id}'`);
1062
1109
  continue;
1063
1110
  }
1064
1111
  seen.add(target.id);
1065
- out.push(target);
1112
+ targets.push(target);
1066
1113
  }
1067
- settings.targets = out;
1114
+ settings.targets = targets;
1068
1115
  }
1069
1116
  }
1070
1117
 
1071
- if ("runtimePlugins" in raw) {
1072
- const v = expectStringArray(issues, "runtimePlugins", raw.runtimePlugins);
1073
- if (v !== undefined) settings.runtimePlugins = v;
1074
- }
1075
- if ("library" in raw) {
1076
- if (!isPlainObject(raw.library)) {
1077
- issues.add("library", `expected a map, got ${describe(raw.library)}`);
1078
- } else {
1079
- issues.unknownKeys("library", raw.library, ["catalog", "remote", "confirmedRemote", "sync"]);
1080
- for (const key of ["catalog", "remote", "confirmedRemote"] as const) {
1081
- const value = raw.library[key];
1082
- if (value === undefined) continue;
1083
- if (value !== null && typeof value !== "string")
1084
- issues.add(`library.${key}`, `expected a string or null, got ${describe(value)}`);
1085
- else settings.library[key] = value as string | null;
1086
- }
1087
- if (raw.library.sync !== undefined) {
1088
- if (typeof raw.library.sync !== "boolean")
1089
- issues.add("library.sync", `expected a boolean, got ${describe(raw.library.sync)}`);
1090
- else settings.library.sync = raw.library.sync;
1118
+ if ("chat" in raw) {
1119
+ if (!isPlainObject(raw.chat)) issues.add("chat", `expected a map, got ${describe(raw.chat)}`);
1120
+ else {
1121
+ const chat = raw.chat;
1122
+ issues.unknownKeys("chat", chat, [
1123
+ "target",
1124
+ "model",
1125
+ "thinkingLevel",
1126
+ "modelPicker",
1127
+ "maxOutputTokens",
1128
+ "prewarm",
1129
+ "retry",
1130
+ ]);
1131
+ const routeInput: Record<string, unknown> = {};
1132
+ for (const key of ["target", "model", "thinkingLevel"] as const) {
1133
+ if (key in chat) routeInput[key] = chat[key];
1091
1134
  }
1092
- }
1093
- }
1094
-
1095
- if ("orchestrator" in raw) {
1096
- settings.orchestrator = validateWorkerTarget(
1097
- issues,
1098
- "orchestrator",
1099
- raw.orchestrator,
1100
- settings.orchestrator,
1101
- settings.targets,
1102
- );
1103
- }
1104
- if ("background" in raw) {
1105
- settings.background = validateWorkerTarget(
1106
- issues,
1107
- "background",
1108
- raw.background,
1109
- settings.background,
1110
- settings.targets,
1111
- );
1112
- }
1113
- if ("memory" in raw) settings.memory = validateMemorySettings(issues, raw.memory);
1114
- if ("watchdog" in raw) settings.watchdog = validateWatchdogSettings(issues, raw.watchdog);
1115
-
1116
- // Fleet nodes validate before workers so profile node pins can be checked
1117
- // against the configured node ids.
1118
- if ("fleet" in raw) {
1119
- settings.fleet = validateFleet(issues, raw.fleet);
1120
- }
1121
- const fleetNodeIds = new Set(settings.fleet.nodes.map((node) => node.id));
1122
-
1123
- if ("routing" in raw) {
1124
- if (!isPlainObject(raw.routing)) {
1125
- issues.add("routing", `expected a map, got ${describe(raw.routing)}`);
1126
- } else {
1127
- issues.unknownKeys("routing", raw.routing, ["activeRoles", "activePostures", "agentAutomation"]);
1128
- const activeRoles = expectStringArray(issues, "routing.activeRoles", raw.routing.activeRoles);
1129
- if (activeRoles !== undefined) {
1130
- const allowed = new Set<string>(ACTIVE_ROUTING_ROLES);
1131
- const rawRoles = raw.routing.activeRoles as unknown[];
1132
- const stringRoles = rawRoles.filter((role): role is string => typeof role === "string");
1133
- const complete = activeRoles.length === rawRoles.length;
1134
- for (const role of activeRoles) {
1135
- if (!allowed.has(role)) issues.add("routing.activeRoles", `unsupported active role '${role}'`);
1136
- }
1137
- if (new Set(stringRoles).size !== stringRoles.length) {
1138
- issues.add("routing.activeRoles", "expected unique roles");
1139
- }
1140
- if (complete && activeRoles.every((role) => allowed.has(role))) {
1141
- settings.routing.activeRoles = activeRoles as typeof settings.routing.activeRoles;
1135
+ const route = validateRoute(
1136
+ issues,
1137
+ "chat",
1138
+ routeInput,
1139
+ { target: settings.chat.target, model: settings.chat.model, thinkingLevel: settings.chat.thinkingLevel },
1140
+ settings.targets,
1141
+ );
1142
+ settings.chat.target = route.target;
1143
+ settings.chat.model = route.model;
1144
+ settings.chat.thinkingLevel = route.thinkingLevel;
1145
+ if ("modelPicker" in chat) {
1146
+ if (!isPlainObject(chat.modelPicker))
1147
+ issues.add("chat.modelPicker", `expected a map, got ${describe(chat.modelPicker)}`);
1148
+ else {
1149
+ issues.unknownKeys("chat.modelPicker", chat.modelPicker, ["cycleSet", "favorites", "recentLimit"]);
1150
+ if ("cycleSet" in chat.modelPicker) {
1151
+ const parsed = expectStringArray(issues, "chat.modelPicker.cycleSet", chat.modelPicker.cycleSet);
1152
+ if (parsed !== undefined) settings.chat.modelPicker.cycleSet = normalizeCycleSet(parsed);
1153
+ }
1154
+ if ("favorites" in chat.modelPicker) {
1155
+ const parsed = expectStringArray(issues, "chat.modelPicker.favorites", chat.modelPicker.favorites);
1156
+ if (parsed !== undefined) settings.chat.modelPicker.favorites = normalizeModelRefs(parsed, settings.targets);
1157
+ }
1158
+ if ("recentLimit" in chat.modelPicker) {
1159
+ const parsed = expectInteger(issues, "chat.modelPicker.recentLimit", chat.modelPicker.recentLimit, { min: 1 });
1160
+ if (parsed !== undefined) settings.chat.modelPicker.recentLimit = parsed;
1161
+ }
1142
1162
  }
1143
1163
  }
1144
- const activePostures = expectStringArray(issues, "routing.activePostures", raw.routing.activePostures);
1145
- if (activePostures !== undefined) {
1146
- const allowed = new Set<string>(ACTIVE_ROUTING_POSTURES);
1147
- const rawPostures = raw.routing.activePostures as unknown[];
1148
- const stringPostures = rawPostures.filter((posture): posture is string => typeof posture === "string");
1149
- const complete = activePostures.length === rawPostures.length;
1150
- for (const posture of activePostures) {
1151
- if (!allowed.has(posture)) issues.add("routing.activePostures", `unsupported active posture '${posture}'`);
1152
- }
1153
- if (new Set(stringPostures).size !== stringPostures.length) {
1154
- issues.add("routing.activePostures", "expected unique postures");
1155
- }
1156
- if (complete && activePostures.every((posture) => allowed.has(posture))) {
1157
- settings.routing.activePostures = activePostures as typeof settings.routing.activePostures;
1158
- }
1164
+ if ("maxOutputTokens" in chat) {
1165
+ const parsed = expectInteger(issues, "chat.maxOutputTokens", chat.maxOutputTokens, { min: 0 });
1166
+ if (parsed !== undefined) settings.chat.maxOutputTokens = parsed;
1159
1167
  }
1160
- if (!isPlainObject(raw.routing.agentAutomation)) {
1161
- issues.add("routing.agentAutomation", "expected a map");
1162
- } else {
1163
- const automation = raw.routing.agentAutomation;
1164
- issues.unknownKeys("routing.agentAutomation", automation, ["activeAgentRoles"]);
1165
- if (!Array.isArray(automation.activeAgentRoles)) {
1166
- issues.add("routing.agentAutomation.activeAgentRoles", "expected a list");
1167
- } else {
1168
- const pairs: typeof settings.routing.agentAutomation.activeAgentRoles = [];
1169
- const seen = new Set<string>();
1170
- for (const [index, value] of automation.activeAgentRoles.entries()) {
1171
- const path = `routing.agentAutomation.activeAgentRoles[${index}]`;
1172
- if (!isPlainObject(value)) {
1173
- issues.add(path, "expected a map");
1174
- continue;
1175
- }
1176
- issues.unknownKeys(path, value, ["agentId", "executionRole"]);
1177
- const agentId = expectString(issues, `${path}.agentId`, value.agentId);
1178
- const executionRole = expectEnum(
1179
- issues,
1180
- `${path}.executionRole`,
1181
- value.executionRole,
1182
- ACTIVE_AGENT_AUTOMATION_ROLES,
1183
- );
1184
- if (agentId === "auto") {
1185
- issues.add(`${path}.agentId`, "'auto' is reserved for coordinator-owned agent selection");
1186
- }
1187
- if (agentId === undefined || agentId === "auto" || executionRole === undefined) continue;
1188
- const key = `${agentId}\u0000${executionRole}`;
1189
- if (seen.has(key)) {
1190
- issues.add(path, "duplicate agent and execution-role pair");
1191
- continue;
1192
- }
1193
- seen.add(key);
1194
- pairs.push({ agentId, executionRole });
1168
+ if ("prewarm" in chat) {
1169
+ const parsed = expectBoolean(issues, "chat.prewarm", chat.prewarm);
1170
+ if (parsed !== undefined) settings.chat.prewarm = parsed;
1171
+ }
1172
+ if ("retry" in chat) {
1173
+ if (!isPlainObject(chat.retry)) issues.add("chat.retry", `expected a map, got ${describe(chat.retry)}`);
1174
+ else {
1175
+ issues.unknownKeys("chat.retry", chat.retry, [
1176
+ "enabled",
1177
+ "maxRetries",
1178
+ "baseDelayMs",
1179
+ "maxDelayMs",
1180
+ "streamStallMs",
1181
+ ]);
1182
+ if ("enabled" in chat.retry) {
1183
+ const parsed = expectBoolean(issues, "chat.retry.enabled", chat.retry.enabled);
1184
+ if (parsed !== undefined) settings.chat.retry.enabled = parsed;
1195
1185
  }
1196
- if (pairs.length === automation.activeAgentRoles.length) {
1197
- settings.routing.agentAutomation.activeAgentRoles = pairs;
1186
+ for (const key of ["maxRetries", "baseDelayMs", "maxDelayMs", "streamStallMs"] as const) {
1187
+ if (!(key in chat.retry)) continue;
1188
+ const parsed = expectInteger(issues, `chat.retry.${key}`, chat.retry[key], { min: 0 });
1189
+ if (parsed !== undefined) settings.chat.retry[key] = parsed;
1198
1190
  }
1199
1191
  }
1200
1192
  }
1201
1193
  }
1202
1194
  }
1203
1195
 
1204
- if ("workers" in raw) {
1205
- if (!isPlainObject(raw.workers)) {
1206
- issues.add("workers", `expected a map, got ${describe(raw.workers)}`);
1207
- } else {
1208
- issues.unknownKeys("workers", raw.workers, [
1196
+ let rawFleet: Record<string, unknown> | null = null;
1197
+ if ("fleet" in raw) {
1198
+ if (!isPlainObject(raw.fleet)) issues.add("fleet", `expected a map, got ${describe(raw.fleet)}`);
1199
+ else {
1200
+ rawFleet = raw.fleet;
1201
+ issues.unknownKeys("fleet", rawFleet, [
1209
1202
  "default",
1210
1203
  "profiles",
1211
1204
  "rosters",
1212
- "agentBindings",
1213
- "maxRetries",
1214
- "onPermission",
1215
- "escalation",
1216
- "resilienceCooldownMs",
1205
+ "agentProfiles",
1206
+ "adaptiveRouting",
1207
+ "nodes",
1208
+ "permissions",
1209
+ "concurrency",
1210
+ "retry",
1211
+ "limits",
1212
+ "history",
1217
1213
  ]);
1218
- if ("default" in raw.workers) {
1219
- settings.workers.default = validateWorkerTarget(
1220
- issues,
1221
- "workers.default",
1222
- raw.workers.default,
1223
- settings.workers.default,
1224
- settings.targets,
1225
- { nodeIds: fleetNodeIds },
1226
- );
1227
- }
1228
- if ("profiles" in raw.workers) {
1229
- if (!isPlainObject(raw.workers.profiles)) {
1230
- issues.add("workers.profiles", `expected a map, got ${describe(raw.workers.profiles)}`);
1231
- } else {
1232
- const profiles: ClioSettings["workers"]["profiles"] = {};
1233
- for (const [rawName, rawProfile] of Object.entries(raw.workers.profiles)) {
1234
- const name = rawName.trim();
1235
- if (!name) {
1236
- issues.add("workers.profiles", "empty profile name");
1237
- continue;
1238
- }
1239
- const profile = validateWorkerTarget(
1240
- issues,
1241
- `workers.profiles.${name}`,
1242
- rawProfile,
1243
- DEFAULT_SETTINGS.workers.default,
1244
- settings.targets,
1245
- { nodeIds: fleetNodeIds },
1246
- );
1247
- if (!profile.target) continue;
1248
- profiles[name] = profile;
1214
+ }
1215
+ }
1216
+ if (rawFleet && "nodes" in rawFleet) {
1217
+ if (!Array.isArray(rawFleet.nodes)) issues.add("fleet.nodes", `expected a list, got ${describe(rawFleet.nodes)}`);
1218
+ else {
1219
+ const seen = new Set<string>();
1220
+ settings.fleet.nodes = rawFleet.nodes
1221
+ .map((entry, index) => validateFleetNode(issues, `fleet.nodes[${index}]`, entry, seen))
1222
+ .filter((entry): entry is FleetNodeConfig => entry !== null);
1223
+ }
1224
+ }
1225
+ const fleetNodeIds = new Set(settings.fleet.nodes.map((node) => node.id));
1226
+ if (rawFleet) {
1227
+ if ("default" in rawFleet) {
1228
+ settings.fleet.default = validateRoute(
1229
+ issues,
1230
+ "fleet.default",
1231
+ rawFleet.default,
1232
+ settings.fleet.default,
1233
+ settings.targets,
1234
+ {
1235
+ nodeIds: fleetNodeIds,
1236
+ },
1237
+ );
1238
+ }
1239
+ if ("profiles" in rawFleet) {
1240
+ if (!isPlainObject(rawFleet.profiles))
1241
+ issues.add("fleet.profiles", `expected a map, got ${describe(rawFleet.profiles)}`);
1242
+ else {
1243
+ const profiles: ClioSettings["fleet"]["profiles"] = {};
1244
+ for (const [rawName, rawProfile] of Object.entries(rawFleet.profiles)) {
1245
+ const name = rawName.trim();
1246
+ if (!name) {
1247
+ issues.add("fleet.profiles", "empty profile name");
1248
+ continue;
1249
1249
  }
1250
- settings.workers.profiles = profiles;
1250
+ const profile = validateRoute(
1251
+ issues,
1252
+ `fleet.profiles.${name}`,
1253
+ rawProfile,
1254
+ DEFAULT_SETTINGS.fleet.default,
1255
+ settings.targets,
1256
+ {
1257
+ nodeIds: fleetNodeIds,
1258
+ },
1259
+ );
1260
+ if (profile.target) profiles[name] = profile;
1251
1261
  }
1262
+ settings.fleet.profiles = profiles;
1252
1263
  }
1253
- if ("rosters" in raw.workers) {
1254
- if (!isPlainObject(raw.workers.rosters)) {
1255
- issues.add("workers.rosters", `expected a map, got ${describe(raw.workers.rosters)}`);
1256
- } else {
1257
- const rosters: ClioSettings["workers"]["rosters"] = {};
1258
- for (const [rawName, rawRoster] of Object.entries(raw.workers.rosters)) {
1259
- const name = rawName.trim();
1260
- const path = `workers.rosters.${name}`;
1261
- if (!name) {
1262
- issues.add("workers.rosters", "empty roster name");
1263
- continue;
1264
- }
1265
- if (!isPlainObject(rawRoster)) {
1266
- issues.add(path, `expected a map, got ${describe(rawRoster)}`);
1267
- continue;
1268
- }
1269
- issues.unknownKeys(path, rawRoster, ["members"]);
1270
- if (!Array.isArray(rawRoster.members)) {
1271
- issues.add(`${path}.members`, "expected a list");
1272
- continue;
1273
- }
1274
- if (rawRoster.members.length < 2 || rawRoster.members.length > 5) {
1275
- issues.add(`${path}.members`, "expected 2 to 5 members");
1276
- }
1277
- const members: ClioSettings["workers"]["rosters"][string]["members"] = [];
1278
- const labels = new Set<string>();
1279
- for (const [index, rawMember] of rawRoster.members.entries()) {
1280
- const memberPath = `${path}.members[${index}]`;
1281
- if (!isPlainObject(rawMember)) {
1282
- issues.add(memberPath, `expected a map, got ${describe(rawMember)}`);
1283
- continue;
1284
- }
1285
- issues.unknownKeys(memberPath, rawMember, ["label", "target", "model", "thinking", "color"]);
1286
- const label = expectString(issues, `${memberPath}.label`, rawMember.label);
1287
- const target = expectString(issues, `${memberPath}.target`, rawMember.target);
1288
- if (label !== undefined && !COUNCIL_MEMBER_LABEL_PATTERN.test(label)) {
1289
- issues.add(`${memberPath}.label`, "expected [a-z][a-z0-9_-]{0,31}");
1290
- }
1291
- if (label !== undefined && labels.has(label)) issues.add(`${memberPath}.label`, "duplicate label");
1292
- if (label !== undefined) labels.add(label);
1293
- if (target !== undefined && !settings.targets.some((entry) => entry.id === target)) {
1294
- issues.add(`${memberPath}.target`, `unknown target '${target}'`);
1295
- }
1296
- const model =
1297
- rawMember.model === undefined ? undefined : expectString(issues, `${memberPath}.model`, rawMember.model);
1298
- const thinking =
1299
- rawMember.thinking === undefined
1300
- ? undefined
1301
- : expectEnum(issues, `${memberPath}.thinking`, rawMember.thinking, THINKING_LEVELS);
1302
- const color =
1303
- rawMember.color === undefined ? undefined : expectString(issues, `${memberPath}.color`, rawMember.color);
1304
- if (color !== undefined && !THEME_NAMED_COLORS.includes(color as never) && !/^#[0-9a-fA-F]{6}$/u.test(color)) {
1305
- issues.add(`${memberPath}.color`, "expected a theme named color or 6-digit hex color");
1306
- }
1307
- if (label === undefined || target === undefined) continue;
1308
- members.push({
1309
- label,
1310
- target,
1311
- ...(model ? { model } : {}),
1312
- ...(thinking ? { thinking } : {}),
1313
- ...(color ? { color } : {}),
1314
- });
1315
- }
1316
- if (rawRoster.members.length >= 2 && rawRoster.members.length <= 5 && members.length === rawRoster.members.length)
1317
- rosters[name] = { members };
1264
+ }
1265
+ if ("rosters" in rawFleet) {
1266
+ if (!isPlainObject(rawFleet.rosters))
1267
+ issues.add("fleet.rosters", `expected a map, got ${describe(rawFleet.rosters)}`);
1268
+ else {
1269
+ const rosters: ClioSettings["fleet"]["rosters"] = {};
1270
+ for (const [rawName, rawRoster] of Object.entries(rawFleet.rosters)) {
1271
+ const name = rawName.trim();
1272
+ const path = `fleet.rosters.${name}`;
1273
+ if (!name) {
1274
+ issues.add("fleet.rosters", "empty roster name");
1275
+ continue;
1318
1276
  }
1319
- settings.workers.rosters = rosters;
1320
- }
1321
- }
1322
- if ("agentBindings" in raw.workers) {
1323
- if (!isPlainObject(raw.workers.agentBindings)) {
1324
- issues.add("workers.agentBindings", `expected a map, got ${describe(raw.workers.agentBindings)}`);
1325
- } else {
1326
- const agentBindings: ClioSettings["workers"]["agentBindings"] = {};
1327
- for (const [rawAgentId, rawProfileName] of Object.entries(raw.workers.agentBindings)) {
1328
- const agentId = rawAgentId.trim();
1329
- if (!agentId) {
1330
- issues.add("workers.agentBindings", "empty agent id");
1277
+ if (!isPlainObject(rawRoster)) {
1278
+ issues.add(path, `expected a map, got ${describe(rawRoster)}`);
1279
+ continue;
1280
+ }
1281
+ issues.unknownKeys(path, rawRoster, ["members"]);
1282
+ if (!Array.isArray(rawRoster.members)) {
1283
+ issues.add(`${path}.members`, "expected a list");
1284
+ continue;
1285
+ }
1286
+ if (rawRoster.members.length < 2 || rawRoster.members.length > 5)
1287
+ issues.add(`${path}.members`, "expected 2 to 5 members");
1288
+ const members: ClioSettings["fleet"]["rosters"][string]["members"] = [];
1289
+ const labels = new Set<string>();
1290
+ for (const [index, rawMember] of rawRoster.members.entries()) {
1291
+ const memberPath = `${path}.members[${index}]`;
1292
+ if (!isPlainObject(rawMember)) {
1293
+ issues.add(memberPath, `expected a map, got ${describe(rawMember)}`);
1331
1294
  continue;
1332
1295
  }
1333
- if (agentId === "auto") {
1334
- issues.add("workers.agentBindings", "'auto' is reserved for coordinator-owned agent selection");
1335
- continue;
1296
+ issues.unknownKeys(memberPath, rawMember, ["label", "target", "model", "thinkingLevel", "color"]);
1297
+ const label = expectString(issues, `${memberPath}.label`, rawMember.label);
1298
+ const target = expectString(issues, `${memberPath}.target`, rawMember.target);
1299
+ if (label !== undefined && !COUNCIL_MEMBER_LABEL_PATTERN.test(label))
1300
+ issues.add(`${memberPath}.label`, "expected [a-z][a-z0-9_-]{0,31}");
1301
+ if (label !== undefined && labels.has(label)) issues.add(`${memberPath}.label`, "duplicate label");
1302
+ if (label !== undefined) labels.add(label);
1303
+ if (target !== undefined && !settings.targets.some((entry) => entry.id === target))
1304
+ issues.add(`${memberPath}.target`, `unknown target '${target}'`);
1305
+ const model =
1306
+ rawMember.model === undefined ? undefined : expectString(issues, `${memberPath}.model`, rawMember.model);
1307
+ const thinkingLevel =
1308
+ rawMember.thinkingLevel === undefined
1309
+ ? undefined
1310
+ : expectEnum(issues, `${memberPath}.thinkingLevel`, rawMember.thinkingLevel, THINKING_LEVELS);
1311
+ const color =
1312
+ rawMember.color === undefined ? undefined : expectString(issues, `${memberPath}.color`, rawMember.color);
1313
+ if (color !== undefined && !THEME_NAMED_COLORS.includes(color as never) && !/^#[0-9a-fA-F]{6}$/u.test(color)) {
1314
+ issues.add(`${memberPath}.color`, "expected a theme named color or 6-digit hex color");
1336
1315
  }
1337
- const profileName = expectString(issues, `workers.agentBindings.${agentId}`, rawProfileName);
1338
- if (profileName !== undefined) agentBindings[agentId] = profileName;
1316
+ if (label === undefined || target === undefined) continue;
1317
+ members.push({
1318
+ label,
1319
+ target,
1320
+ ...(model ? { model } : {}),
1321
+ ...(thinkingLevel ? { thinkingLevel } : {}),
1322
+ ...(color ? { color } : {}),
1323
+ });
1339
1324
  }
1340
- settings.workers.agentBindings = agentBindings;
1325
+ if (rawRoster.members.length >= 2 && rawRoster.members.length <= 5 && members.length === rawRoster.members.length)
1326
+ rosters[name] = { members };
1341
1327
  }
1328
+ settings.fleet.rosters = rosters;
1342
1329
  }
1343
- if ("maxRetries" in raw.workers) {
1344
- const v = expectInteger(issues, "workers.maxRetries", raw.workers.maxRetries, { min: 0 });
1345
- if (v !== undefined) settings.workers.maxRetries = v;
1346
- }
1347
- if ("onPermission" in raw.workers) {
1348
- const v = expectEnum(issues, "workers.onPermission", raw.workers.onPermission, [
1349
- "deny",
1350
- "fail",
1351
- "escalate",
1352
- ] as const);
1353
- if (v !== undefined) settings.workers.onPermission = v;
1354
- }
1355
- if ("escalation" in raw.workers) {
1356
- if (!isPlainObject(raw.workers.escalation)) {
1357
- issues.add("workers.escalation", `expected a map, got ${describe(raw.workers.escalation)}`);
1358
- } else {
1359
- const base = settings.workers.escalation ?? DEFAULT_SETTINGS.workers.escalation;
1360
- const escalation = { timeoutMs: base?.timeoutMs ?? 120000, fallback: base?.fallback ?? "deny" };
1361
- issues.unknownKeys("workers.escalation", raw.workers.escalation, ["timeoutMs", "fallback"]);
1362
- if ("timeoutMs" in raw.workers.escalation) {
1363
- const v = expectInteger(issues, "workers.escalation.timeoutMs", raw.workers.escalation.timeoutMs, {
1364
- min: 1,
1365
- });
1366
- if (v !== undefined) escalation.timeoutMs = v;
1330
+ }
1331
+ if ("agentProfiles" in rawFleet) {
1332
+ if (!isPlainObject(rawFleet.agentProfiles))
1333
+ issues.add("fleet.agentProfiles", `expected a map, got ${describe(rawFleet.agentProfiles)}`);
1334
+ else {
1335
+ const bindings: ClioSettings["fleet"]["agentProfiles"] = {};
1336
+ for (const [rawAgentId, rawProfileName] of Object.entries(rawFleet.agentProfiles)) {
1337
+ const agentId = rawAgentId.trim();
1338
+ if (!agentId) {
1339
+ issues.add("fleet.agentProfiles", "empty agent id");
1340
+ continue;
1367
1341
  }
1368
- if ("fallback" in raw.workers.escalation) {
1369
- const v = expectEnum(issues, "workers.escalation.fallback", raw.workers.escalation.fallback, [
1370
- "deny",
1371
- "fail",
1372
- ] as const);
1373
- if (v !== undefined) escalation.fallback = v;
1342
+ if (agentId === "auto") {
1343
+ issues.add("fleet.agentProfiles", "'auto' is reserved for coordinator-owned agent selection");
1344
+ continue;
1374
1345
  }
1375
- settings.workers.escalation = escalation;
1346
+ const profile = expectString(issues, `fleet.agentProfiles.${agentId}`, rawProfileName);
1347
+ if (profile !== undefined) bindings[agentId] = profile;
1376
1348
  }
1377
- }
1378
- if ("resilienceCooldownMs" in raw.workers) {
1379
- const v = expectInteger(issues, "workers.resilienceCooldownMs", raw.workers.resilienceCooldownMs, { min: 0 });
1380
- if (v !== undefined) settings.workers.resilienceCooldownMs = v;
1381
- }
1382
- }
1383
- }
1384
-
1385
- if ("scope" in raw) {
1386
- const v = expectStringArray(issues, "scope", raw.scope);
1387
- if (v !== undefined) settings.scope = normalizeScope(v);
1388
- }
1389
-
1390
- if ("modelSelector" in raw) {
1391
- if (!isPlainObject(raw.modelSelector)) {
1392
- issues.add("modelSelector", `expected a map, got ${describe(raw.modelSelector)}`);
1393
- } else {
1394
- issues.unknownKeys("modelSelector", raw.modelSelector, ["favorites", "recentLimit"]);
1395
- if ("favorites" in raw.modelSelector) {
1396
- const v = expectStringArray(issues, "modelSelector.favorites", raw.modelSelector.favorites);
1397
- if (v !== undefined) settings.modelSelector.favorites = normalizeModelRefs(v, settings.targets);
1398
- }
1399
- if ("recentLimit" in raw.modelSelector) {
1400
- const v = expectInteger(issues, "modelSelector.recentLimit", raw.modelSelector.recentLimit, { min: 1 });
1401
- if (v !== undefined) settings.modelSelector.recentLimit = v;
1349
+ settings.fleet.agentProfiles = bindings;
1402
1350
  }
1403
1351
  }
1404
- }
1405
-
1406
- if ("budget" in raw) {
1407
- if (!isPlainObject(raw.budget)) {
1408
- issues.add("budget", `expected a map, got ${describe(raw.budget)}`);
1409
- } else {
1410
- issues.unknownKeys("budget", raw.budget, ["sessionCeilingUsd", "concurrency"]);
1411
- if ("sessionCeilingUsd" in raw.budget) {
1412
- const v = expectNumber(issues, "budget.sessionCeilingUsd", raw.budget.sessionCeilingUsd, { min: 0 });
1413
- if (v !== undefined) settings.budget.sessionCeilingUsd = v;
1414
- }
1415
- if ("concurrency" in raw.budget) {
1416
- if (raw.budget.concurrency === "auto") {
1417
- settings.budget.concurrency = "auto";
1418
- } else {
1419
- const v = expectInteger(issues, "budget.concurrency", raw.budget.concurrency, { min: 1 });
1420
- if (v !== undefined) settings.budget.concurrency = v;
1352
+ if ("adaptiveRouting" in rawFleet) {
1353
+ if (!isPlainObject(rawFleet.adaptiveRouting))
1354
+ issues.add("fleet.adaptiveRouting", `expected a map, got ${describe(rawFleet.adaptiveRouting)}`);
1355
+ else {
1356
+ const adaptive = rawFleet.adaptiveRouting;
1357
+ issues.unknownKeys("fleet.adaptiveRouting", adaptive, ["roles", "postures", "agentRoles"]);
1358
+ for (const [key, allowed] of [
1359
+ ["roles", ACTIVE_ROUTING_ROLES],
1360
+ ["postures", ACTIVE_ROUTING_POSTURES],
1361
+ ] as const) {
1362
+ if (!(key in adaptive)) continue;
1363
+ const parsed = expectStringArray(issues, `fleet.adaptiveRouting.${key}`, adaptive[key]);
1364
+ if (parsed === undefined) continue;
1365
+ const allowedSet = new Set<string>(allowed);
1366
+ for (const value of parsed)
1367
+ if (!allowedSet.has(value))
1368
+ issues.add(`fleet.adaptiveRouting.${key}`, `unsupported ${key === "roles" ? "role" : "posture"} '${value}'`);
1369
+ if (parsed.every((value) => allowedSet.has(value))) settings.fleet.adaptiveRouting[key] = parsed as never;
1370
+ }
1371
+ if ("agentRoles" in adaptive) {
1372
+ if (!Array.isArray(adaptive.agentRoles)) issues.add("fleet.adaptiveRouting.agentRoles", "expected a list");
1373
+ else {
1374
+ const pairs: ClioSettings["fleet"]["adaptiveRouting"]["agentRoles"] = [];
1375
+ const seen = new Set<string>();
1376
+ for (const [index, value] of adaptive.agentRoles.entries()) {
1377
+ const path = `fleet.adaptiveRouting.agentRoles[${index}]`;
1378
+ if (!isPlainObject(value)) {
1379
+ issues.add(path, "expected a map");
1380
+ continue;
1381
+ }
1382
+ issues.unknownKeys(path, value, ["agentId", "executionRole"]);
1383
+ const agentId = expectString(issues, `${path}.agentId`, value.agentId);
1384
+ const executionRole = expectEnum(
1385
+ issues,
1386
+ `${path}.executionRole`,
1387
+ value.executionRole,
1388
+ ACTIVE_AGENT_AUTOMATION_ROLES,
1389
+ );
1390
+ if (agentId === "auto")
1391
+ issues.add(`${path}.agentId`, "'auto' is reserved for coordinator-owned agent selection");
1392
+ if (agentId === undefined || agentId === "auto" || executionRole === undefined) continue;
1393
+ const pairKey = `${agentId}\u0000${executionRole}`;
1394
+ if (seen.has(pairKey)) {
1395
+ issues.add(path, "duplicate agent and execution-role pair");
1396
+ continue;
1397
+ }
1398
+ seen.add(pairKey);
1399
+ pairs.push({ agentId, executionRole });
1400
+ }
1401
+ if (pairs.length === adaptive.agentRoles.length) settings.fleet.adaptiveRouting.agentRoles = pairs;
1402
+ }
1421
1403
  }
1422
1404
  }
1423
1405
  }
1424
- }
1425
-
1426
- if ("defaults" in raw) {
1427
- if (!isPlainObject(raw.defaults)) {
1428
- issues.add("defaults", `expected a map, got ${describe(raw.defaults)}`);
1429
- } else {
1430
- issues.unknownKeys("defaults", raw.defaults, ["maxTokens"]);
1431
- if ("maxTokens" in raw.defaults) {
1432
- const v = expectInteger(issues, "defaults.maxTokens", raw.defaults.maxTokens, { min: 0 });
1433
- if (v !== undefined) settings.defaults.maxTokens = v;
1434
- }
1435
- }
1436
- }
1437
-
1438
- if ("theme" in raw) {
1439
- const v = expectString(issues, "theme", raw.theme);
1440
- if (v !== undefined) settings.theme = v;
1441
- }
1442
-
1443
- if ("terminal" in raw) {
1444
- if (!isPlainObject(raw.terminal)) {
1445
- issues.add("terminal", `expected a map, got ${describe(raw.terminal)}`);
1446
- } else {
1447
- issues.unknownKeys("terminal", raw.terminal, [
1448
- "showTerminalProgress",
1449
- "outputVerbosity",
1450
- "tuiMode",
1451
- "fullscreenScrollbar",
1452
- "smoothStreaming",
1453
- "notify",
1454
- ]);
1455
- if ("showTerminalProgress" in raw.terminal) {
1456
- const v = expectBoolean(issues, "terminal.showTerminalProgress", raw.terminal.showTerminalProgress);
1457
- if (v !== undefined) settings.terminal.showTerminalProgress = v;
1458
- }
1459
- if ("outputVerbosity" in raw.terminal) {
1460
- const v = expectString(issues, "terminal.outputVerbosity", raw.terminal.outputVerbosity);
1461
- if (v === "minimal" || v === "default" || v === "verbose") settings.terminal.outputVerbosity = v;
1462
- else if (v !== undefined) issues.add("terminal.outputVerbosity", "expected minimal, default, or verbose");
1463
- }
1464
- if ("tuiMode" in raw.terminal) {
1465
- const v = expectString(issues, "terminal.tuiMode", raw.terminal.tuiMode);
1466
- if (v === "regular" || v === "fullscreen") settings.terminal.tuiMode = v;
1467
- else if (v !== undefined) issues.add("terminal.tuiMode", "expected regular or fullscreen");
1468
- }
1469
- if ("fullscreenScrollbar" in raw.terminal) {
1470
- const v = expectString(issues, "terminal.fullscreenScrollbar", raw.terminal.fullscreenScrollbar);
1471
- if (v === "hidden" || v === "auto" || v === "always") settings.terminal.fullscreenScrollbar = v;
1472
- else if (v !== undefined) issues.add("terminal.fullscreenScrollbar", "expected hidden, auto, or always");
1473
- }
1474
- if ("smoothStreaming" in raw.terminal) {
1475
- const v = expectString(issues, "terminal.smoothStreaming", raw.terminal.smoothStreaming);
1476
- if (v === "off" || v === "auto" || v === "on") settings.terminal.smoothStreaming = v;
1477
- else if (v !== undefined) issues.add("terminal.smoothStreaming", "expected off, auto, or on");
1478
- }
1479
- if ("notify" in raw.terminal) {
1480
- const v = expectBoolean(issues, "terminal.notify", raw.terminal.notify);
1481
- if (v !== undefined) settings.terminal.notify = v;
1482
- }
1483
- }
1484
- }
1485
-
1486
- if ("skills" in raw) {
1487
- if (!isPlainObject(raw.skills)) {
1488
- issues.add("skills", `expected a map, got ${describe(raw.skills)}`);
1489
- } else {
1490
- issues.unknownKeys("skills", raw.skills, ["trustProjectCompatRoots"]);
1491
- if ("trustProjectCompatRoots" in raw.skills) {
1492
- const v = expectBoolean(issues, "skills.trustProjectCompatRoots", raw.skills.trustProjectCompatRoots);
1493
- if (v !== undefined) settings.skills.trustProjectCompatRoots = v;
1406
+ if ("permissions" in rawFleet) {
1407
+ if (!isPlainObject(rawFleet.permissions))
1408
+ issues.add("fleet.permissions", `expected a map, got ${describe(rawFleet.permissions)}`);
1409
+ else {
1410
+ issues.unknownKeys("fleet.permissions", rawFleet.permissions, ["mode", "escalation"]);
1411
+ if ("mode" in rawFleet.permissions) {
1412
+ const parsed = expectEnum(issues, "fleet.permissions.mode", rawFleet.permissions.mode, [
1413
+ "deny",
1414
+ "fail",
1415
+ "escalate",
1416
+ ] as const);
1417
+ if (parsed !== undefined) settings.fleet.permissions.mode = parsed;
1418
+ }
1419
+ if ("escalation" in rawFleet.permissions) {
1420
+ if (!isPlainObject(rawFleet.permissions.escalation))
1421
+ issues.add("fleet.permissions.escalation", `expected a map, got ${describe(rawFleet.permissions.escalation)}`);
1422
+ else {
1423
+ const escalation = rawFleet.permissions.escalation;
1424
+ issues.unknownKeys("fleet.permissions.escalation", escalation, ["timeoutMs", "fallback"]);
1425
+ if ("timeoutMs" in escalation) {
1426
+ const parsed = expectInteger(issues, "fleet.permissions.escalation.timeoutMs", escalation.timeoutMs, { min: 1 });
1427
+ if (parsed !== undefined) settings.fleet.permissions.escalation.timeoutMs = parsed;
1428
+ }
1429
+ if ("fallback" in escalation) {
1430
+ const parsed = expectEnum(issues, "fleet.permissions.escalation.fallback", escalation.fallback, [
1431
+ "deny",
1432
+ "fail",
1433
+ ] as const);
1434
+ if (parsed !== undefined) settings.fleet.permissions.escalation.fallback = parsed;
1435
+ }
1436
+ }
1437
+ }
1494
1438
  }
1495
1439
  }
1496
- }
1497
-
1498
- if ("attribution" in raw) {
1499
- if (!isPlainObject(raw.attribution)) {
1500
- issues.add("attribution", `expected a map, got ${describe(raw.attribution)}`);
1501
- } else {
1502
- issues.unknownKeys("attribution", raw.attribution, ["gitCommits"]);
1503
- if ("gitCommits" in raw.attribution) {
1504
- const v = expectBoolean(issues, "attribution.gitCommits", raw.attribution.gitCommits);
1505
- if (v !== undefined) settings.attribution.gitCommits = v;
1440
+ if ("concurrency" in rawFleet) {
1441
+ if (rawFleet.concurrency === "auto") settings.fleet.concurrency = "auto";
1442
+ else {
1443
+ const parsed = expectInteger(issues, "fleet.concurrency", rawFleet.concurrency, { min: 1 });
1444
+ if (parsed !== undefined) settings.fleet.concurrency = parsed;
1506
1445
  }
1507
1446
  }
1508
- }
1509
-
1510
- if ("delegation" in raw) {
1511
- settings.delegation = validateDelegation(issues, raw.delegation);
1512
- }
1513
-
1514
- if ("keybindings" in raw) {
1515
- settings.keybindings = validateKeybindings(issues, raw.keybindings);
1516
- }
1517
-
1518
- if ("compaction" in raw) {
1519
- if (!isPlainObject(raw.compaction)) {
1520
- issues.add("compaction", `expected a map, got ${describe(raw.compaction)}`);
1521
- } else {
1522
- issues.unknownKeys("compaction", raw.compaction, ["auto", "threshold", "excludeLastTurns", "model", "systemPrompt"]);
1523
- if ("auto" in raw.compaction) {
1524
- const v = expectBoolean(issues, "compaction.auto", raw.compaction.auto);
1525
- if (v !== undefined) settings.compaction.auto = v;
1526
- }
1527
- if ("threshold" in raw.compaction) {
1528
- const v = expectNumber(issues, "compaction.threshold", raw.compaction.threshold, { min: 0, max: 1 });
1529
- if (v !== undefined) settings.compaction.threshold = v;
1530
- }
1531
- if ("excludeLastTurns" in raw.compaction) {
1532
- const v = expectInteger(issues, "compaction.excludeLastTurns", raw.compaction.excludeLastTurns, { min: 1 });
1533
- if (v !== undefined) settings.compaction.excludeLastTurns = v;
1447
+ for (const [blockName, fields] of [
1448
+ [
1449
+ "retry",
1450
+ [
1451
+ ["maxRetries", 0],
1452
+ ["routeCooldownMs", 0],
1453
+ ],
1454
+ ],
1455
+ [
1456
+ "limits",
1457
+ [
1458
+ ["toolCallsPerRun", 1],
1459
+ ["internalRunTimeoutMs", 1],
1460
+ ],
1461
+ ],
1462
+ ["history", [["maxRuns", 1]]],
1463
+ ] as const) {
1464
+ if (!(blockName in rawFleet)) continue;
1465
+ const block = rawFleet[blockName];
1466
+ if (!isPlainObject(block)) {
1467
+ issues.add(`fleet.${blockName}`, `expected a map, got ${describe(block)}`);
1468
+ continue;
1534
1469
  }
1535
- if ("model" in raw.compaction) {
1536
- const v = expectString(issues, "compaction.model", raw.compaction.model);
1537
- if (v !== undefined) settings.compaction.model = v;
1470
+ const known = fields.map(([key]) => key);
1471
+ if (blockName === "history") known.push("journal" as never);
1472
+ issues.unknownKeys(`fleet.${blockName}`, block, known);
1473
+ for (const [key, min] of fields) {
1474
+ if (!(key in block)) continue;
1475
+ const parsed = expectInteger(issues, `fleet.${blockName}.${key}`, block[key], { min });
1476
+ if (parsed !== undefined) (settings.fleet[blockName] as unknown as Record<string, unknown>)[key] = parsed;
1538
1477
  }
1539
- if ("systemPrompt" in raw.compaction) {
1540
- const v = expectString(issues, "compaction.systemPrompt", raw.compaction.systemPrompt);
1541
- if (v !== undefined) settings.compaction.systemPrompt = v;
1478
+ if (blockName === "history" && "journal" in block) {
1479
+ const parsed = expectBoolean(issues, "fleet.history.journal", block.journal);
1480
+ if (parsed !== undefined) settings.fleet.history.journal = parsed;
1542
1481
  }
1543
1482
  }
1544
1483
  }
1545
1484
 
1546
1485
  if ("context" in raw) {
1547
- if (!isPlainObject(raw.context)) {
1548
- issues.add("context", `expected a map, got ${describe(raw.context)}`);
1549
- } else {
1550
- issues.unknownKeys("context", raw.context, ["workingSet"]);
1551
- if ("workingSet" in raw.context) {
1552
- if (!isPlainObject(raw.context.workingSet)) {
1553
- issues.add("context.workingSet", `expected a map, got ${describe(raw.context.workingSet)}`);
1554
- } else {
1555
- const workingSet = raw.context.workingSet;
1486
+ if (!isPlainObject(raw.context)) issues.add("context", `expected a map, got ${describe(raw.context)}`);
1487
+ else {
1488
+ const context = raw.context;
1489
+ issues.unknownKeys("context", context, ["workingSet", "compaction", "memory"]);
1490
+ if ("workingSet" in context) {
1491
+ if (!isPlainObject(context.workingSet))
1492
+ issues.add("context.workingSet", `expected a map, got ${describe(context.workingSet)}`);
1493
+ else {
1494
+ const workingSet = context.workingSet;
1556
1495
  issues.unknownKeys("context.workingSet", workingSet, [
1557
1496
  "enabled",
1558
1497
  "policy",
@@ -1561,136 +1500,380 @@ export function validateSettings(raw: unknown): SettingsValidationResult {
1561
1500
  "minEvictableTokens",
1562
1501
  ]);
1563
1502
  if ("enabled" in workingSet) {
1564
- const v = expectBoolean(issues, "context.workingSet.enabled", workingSet.enabled);
1565
- if (v !== undefined) settings.context.workingSet.enabled = v;
1503
+ const parsed = expectBoolean(issues, "context.workingSet.enabled", workingSet.enabled);
1504
+ if (parsed !== undefined) settings.context.workingSet.enabled = parsed;
1566
1505
  }
1567
1506
  if ("policy" in workingSet) {
1568
- const v = expectEnum(issues, "context.workingSet.policy", workingSet.policy, [
1507
+ const parsed = expectEnum(issues, "context.workingSet.policy", workingSet.policy, [
1569
1508
  "age-horizon",
1570
1509
  "structural-v1",
1571
1510
  ] as const);
1572
- if (v !== undefined) settings.context.workingSet.policy = v;
1511
+ if (parsed !== undefined) settings.context.workingSet.policy = parsed;
1573
1512
  }
1574
1513
  if ("target" in workingSet) {
1575
- const v = expectNumber(issues, "context.workingSet.target", workingSet.target);
1576
- if (v !== undefined && (v <= 0 || v >= 1)) {
1577
- issues.add("context.workingSet.target", `expected a number > 0 and < 1, got ${v}`);
1578
- } else if (v !== undefined) {
1579
- settings.context.workingSet.target = v;
1514
+ const parsed = expectNumber(issues, "context.workingSet.target", workingSet.target);
1515
+ if (parsed !== undefined && (parsed <= 0 || parsed >= 1))
1516
+ issues.add("context.workingSet.target", `expected a number > 0 and < 1, got ${parsed}`);
1517
+ else if (parsed !== undefined) settings.context.workingSet.target = parsed;
1518
+ }
1519
+ for (const [key, min] of [
1520
+ ["protectLastTurns", 1],
1521
+ ["minEvictableTokens", 0],
1522
+ ] as const) {
1523
+ if (!(key in workingSet)) continue;
1524
+ const parsed = expectInteger(issues, `context.workingSet.${key}`, workingSet[key], { min });
1525
+ if (parsed !== undefined) settings.context.workingSet[key] = parsed;
1526
+ }
1527
+ }
1528
+ }
1529
+ if ("compaction" in context) {
1530
+ if (!isPlainObject(context.compaction))
1531
+ issues.add("context.compaction", `expected a map, got ${describe(context.compaction)}`);
1532
+ else {
1533
+ const compaction = context.compaction;
1534
+ issues.unknownKeys("context.compaction", compaction, [
1535
+ "auto",
1536
+ "threshold",
1537
+ "model",
1538
+ "systemPrompt",
1539
+ "excludeLastTurns",
1540
+ ]);
1541
+ if ("excludeLastTurns" in compaction)
1542
+ issues.add(
1543
+ "context.compaction.excludeLastTurns",
1544
+ "retired without replacement; use context.workingSet.protectLastTurns",
1545
+ );
1546
+ if ("auto" in compaction) {
1547
+ const parsed = expectBoolean(issues, "context.compaction.auto", compaction.auto);
1548
+ if (parsed !== undefined) settings.context.compaction.auto = parsed;
1549
+ }
1550
+ if ("threshold" in compaction) {
1551
+ const parsed = expectNumber(issues, "context.compaction.threshold", compaction.threshold, { min: 0, max: 1 });
1552
+ if (parsed !== undefined) settings.context.compaction.threshold = parsed;
1553
+ }
1554
+ for (const key of ["model", "systemPrompt"] as const) {
1555
+ if (!(key in compaction)) continue;
1556
+ const parsed = expectString(issues, `context.compaction.${key}`, compaction[key]);
1557
+ if (parsed !== undefined) settings.context.compaction[key] = parsed;
1558
+ }
1559
+ }
1560
+ }
1561
+ if ("memory" in context) {
1562
+ if (!isPlainObject(context.memory)) issues.add("context.memory", `expected a map, got ${describe(context.memory)}`);
1563
+ else {
1564
+ const memory = context.memory;
1565
+ issues.unknownKeys("context.memory", memory, [
1566
+ "enabled",
1567
+ "target",
1568
+ "model",
1569
+ "cadenceToolCalls",
1570
+ "trajectorySteps",
1571
+ "maxOutputTokens",
1572
+ "timeoutMs",
1573
+ ]);
1574
+ if ("enabled" in memory) {
1575
+ const parsed = expectBoolean(issues, "context.memory.enabled", memory.enabled);
1576
+ if (parsed !== undefined) settings.context.memory.enabled = parsed;
1577
+ }
1578
+ if ("target" in memory) {
1579
+ if (memory.target === null) settings.context.memory.target = null;
1580
+ else {
1581
+ const parsed = expectString(issues, "context.memory.target", memory.target);
1582
+ if (parsed !== undefined)
1583
+ settings.context.memory.target = settings.targets.some((target) => target.id === parsed) ? parsed : null;
1580
1584
  }
1581
1585
  }
1582
- if ("protectLastTurns" in workingSet) {
1583
- const v = expectInteger(issues, "context.workingSet.protectLastTurns", workingSet.protectLastTurns, {
1584
- min: 1,
1585
- });
1586
- if (v !== undefined) settings.context.workingSet.protectLastTurns = v;
1586
+ if ("model" in memory) {
1587
+ if (memory.model === null) settings.context.memory.model = null;
1588
+ else {
1589
+ const parsed = expectString(issues, "context.memory.model", memory.model);
1590
+ if (parsed !== undefined) settings.context.memory.model = parsed;
1591
+ }
1587
1592
  }
1588
- if ("minEvictableTokens" in workingSet) {
1589
- const v = expectInteger(issues, "context.workingSet.minEvictableTokens", workingSet.minEvictableTokens, {
1590
- min: 0,
1591
- });
1592
- if (v !== undefined) settings.context.workingSet.minEvictableTokens = v;
1593
+ if (!settings.context.memory.target) settings.context.memory.model = null;
1594
+ else if (settings.context.memory.model === null)
1595
+ settings.context.memory.model =
1596
+ settings.targets.find((target) => target.id === settings.context.memory.target)?.defaultModel ?? null;
1597
+ for (const [key, min] of [
1598
+ ["cadenceToolCalls", 2],
1599
+ ["trajectorySteps", 1],
1600
+ ["maxOutputTokens", 1],
1601
+ ["timeoutMs", 1],
1602
+ ] as const) {
1603
+ if (!(key in memory)) continue;
1604
+ const parsed = expectInteger(issues, `context.memory.${key}`, memory[key], { min });
1605
+ if (parsed !== undefined) settings.context.memory[key] = parsed;
1593
1606
  }
1594
1607
  }
1595
1608
  }
1596
1609
  }
1597
1610
  }
1598
1611
 
1599
- if ("prewarm" in raw) {
1600
- if (!isPlainObject(raw.prewarm)) {
1601
- issues.add("prewarm", `expected a map, got ${describe(raw.prewarm)}`);
1602
- } else {
1603
- issues.unknownKeys("prewarm", raw.prewarm, ["enabled"]);
1604
- if ("enabled" in raw.prewarm) {
1605
- const v = expectBoolean(issues, "prewarm.enabled", raw.prewarm.enabled);
1606
- if (v !== undefined) settings.prewarm.enabled = v;
1612
+ if ("safety" in raw) {
1613
+ if (!isPlainObject(raw.safety)) issues.add("safety", `expected a map, got ${describe(raw.safety)}`);
1614
+ else {
1615
+ const safety = raw.safety;
1616
+ issues.unknownKeys("safety", safety, ["autonomy", "limits", "review"]);
1617
+ if ("autonomy" in safety) {
1618
+ const parsed = expectEnum(issues, "safety.autonomy", safety.autonomy, AUTONOMY_LEVELS);
1619
+ if (parsed !== undefined) settings.safety.autonomy = parsed;
1620
+ }
1621
+ if ("limits" in safety) {
1622
+ if (!isPlainObject(safety.limits)) issues.add("safety.limits", `expected a map, got ${describe(safety.limits)}`);
1623
+ else {
1624
+ const limits = safety.limits;
1625
+ issues.unknownKeys("safety.limits", limits, [
1626
+ "sessionCostUsd",
1627
+ "chatToolCallsPerTurn",
1628
+ "readBytesPerCall",
1629
+ "observationBytesPerTurn",
1630
+ ]);
1631
+ if ("sessionCostUsd" in limits) {
1632
+ const parsed = expectNumber(issues, "safety.limits.sessionCostUsd", limits.sessionCostUsd, { min: 0 });
1633
+ if (parsed !== undefined) settings.safety.limits.sessionCostUsd = parsed;
1634
+ }
1635
+ for (const key of ["chatToolCallsPerTurn", "readBytesPerCall", "observationBytesPerTurn"] as const) {
1636
+ if (!(key in limits)) continue;
1637
+ const parsed = expectInteger(issues, `safety.limits.${key}`, limits[key], { min: 1 });
1638
+ if (parsed !== undefined) settings.safety.limits[key] = parsed;
1639
+ }
1640
+ }
1641
+ }
1642
+ if ("review" in safety) {
1643
+ if (!isPlainObject(safety.review)) issues.add("safety.review", `expected a map, got ${describe(safety.review)}`);
1644
+ else {
1645
+ const review = safety.review;
1646
+ issues.unknownKeys("safety.review", review, ["enabled", "target", "cadenceToolCalls"]);
1647
+ if ("enabled" in review) {
1648
+ const parsed = expectBoolean(issues, "safety.review.enabled", review.enabled);
1649
+ if (parsed !== undefined) settings.safety.review.enabled = parsed;
1650
+ }
1651
+ if ("target" in review && review.target !== null) {
1652
+ const parsed = expectString(issues, "safety.review.target", review.target);
1653
+ if (parsed !== undefined && settings.targets.some((target) => target.id === parsed))
1654
+ settings.safety.review.target = parsed;
1655
+ else if (parsed !== undefined) issues.add("safety.review.target", `unknown target '${parsed}'`);
1656
+ }
1657
+ if ("cadenceToolCalls" in review && review.cadenceToolCalls !== null) {
1658
+ const parsed = expectInteger(issues, "safety.review.cadenceToolCalls", review.cadenceToolCalls, { min: 1 });
1659
+ if (parsed !== undefined) settings.safety.review.cadenceToolCalls = parsed;
1660
+ }
1661
+ }
1607
1662
  }
1608
1663
  }
1609
1664
  }
1610
1665
 
1611
- if ("panes" in raw) {
1612
- if (!isPlainObject(raw.panes)) {
1613
- issues.add("panes", `expected a map, got ${describe(raw.panes)}`);
1614
- } else {
1615
- // `agents` and `keepFailed` are retired: pane-opening is no longer a
1616
- // per-dispatch policy (src/interactive/pane-policy.ts holds the one
1617
- // decision). They stay accepted-and-ignored so a settings file written
1618
- // against an older release keeps passing the strict boot gate.
1619
- issues.unknownKeys("panes", raw.panes, ["enabled", "agents", "keepFailed", "notifications", "journal", "yazi"]);
1620
- if ("enabled" in raw.panes) {
1621
- const v = expectEnum(issues, "panes.enabled", raw.panes.enabled, ["auto", "embedded", "off"] as const);
1622
- if (v !== undefined) settings.panes.enabled = v;
1623
- }
1624
- if ("notifications" in raw.panes) {
1625
- const v = expectEnum(issues, "panes.notifications", raw.panes.notifications, ["failures", "all", "off"] as const);
1626
- if (v !== undefined) settings.panes.notifications = v;
1666
+ if ("interface" in raw) {
1667
+ if (!isPlainObject(raw.interface)) issues.add("interface", `expected a map, got ${describe(raw.interface)}`);
1668
+ else {
1669
+ const ui = raw.interface;
1670
+ issues.unknownKeys("interface", ui, [
1671
+ "outputDetail",
1672
+ "smoothStreaming",
1673
+ "mode",
1674
+ "fullscreenScrollbar",
1675
+ "terminalProgress",
1676
+ "desktopNotifications",
1677
+ "panes",
1678
+ "keybindings",
1679
+ ]);
1680
+ for (const key of ["terminalProgress", "desktopNotifications"] as const) {
1681
+ if (!(key in ui)) continue;
1682
+ const parsed = expectBoolean(issues, `interface.${key}`, ui[key]);
1683
+ if (parsed !== undefined) (settings.interface as unknown as Record<string, unknown>)[key] = parsed;
1627
1684
  }
1628
- if ("journal" in raw.panes) {
1629
- const v = expectBoolean(issues, "panes.journal", raw.panes.journal);
1630
- if (v !== undefined) settings.panes.journal = v;
1685
+ for (const [key, allowed] of [
1686
+ ["outputDetail", ["minimal", "default", "verbose"]],
1687
+ ["smoothStreaming", ["off", "auto", "on"]],
1688
+ ["mode", ["regular", "fullscreen"]],
1689
+ ["fullscreenScrollbar", ["hidden", "auto", "always"]],
1690
+ ] as const) {
1691
+ if (!(key in ui)) continue;
1692
+ const parsed = expectEnum(issues, `interface.${key}`, ui[key], allowed);
1693
+ if (parsed !== undefined) (settings.interface as unknown as Record<string, unknown>)[key] = parsed;
1631
1694
  }
1632
- if ("yazi" in raw.panes) {
1633
- if (!isPlainObject(raw.panes.yazi)) {
1634
- issues.add("panes.yazi", `expected a map, got ${describe(raw.panes.yazi)}`);
1635
- } else {
1636
- const yazi = raw.panes.yazi;
1637
- issues.unknownKeys("panes.yazi", yazi, ["enabled", "mode", "profile", "followCwd"]);
1638
- if ("enabled" in yazi) {
1639
- const v = expectBoolean(issues, "panes.yazi.enabled", yazi.enabled);
1640
- if (v !== undefined) settings.panes.yazi.enabled = v;
1695
+ if ("panes" in ui) {
1696
+ if (!isPlainObject(ui.panes)) issues.add("interface.panes", `expected a map, got ${describe(ui.panes)}`);
1697
+ else {
1698
+ const panes = ui.panes;
1699
+ issues.unknownKeys("interface.panes", panes, ["enabled", "notifications", "layout", "workers", "files"]);
1700
+ if ("enabled" in panes) {
1701
+ const parsed = expectEnum(issues, "interface.panes.enabled", panes.enabled, ["auto", "embedded", "off"] as const);
1702
+ if (parsed !== undefined) settings.interface.panes.enabled = parsed;
1703
+ }
1704
+ if ("notifications" in panes) {
1705
+ const parsed = expectEnum(issues, "interface.panes.notifications", panes.notifications, [
1706
+ "failures",
1707
+ "all",
1708
+ "off",
1709
+ ] as const);
1710
+ if (parsed !== undefined) settings.interface.panes.notifications = parsed;
1641
1711
  }
1642
- if ("mode" in yazi) {
1643
- const v = expectEnum(issues, "panes.yazi.mode", yazi.mode, ["companion", "chooser"] as const);
1644
- if (v !== undefined) settings.panes.yazi.mode = v;
1712
+ if ("layout" in panes) {
1713
+ const parsed = expectEnum(issues, "interface.panes.layout", panes.layout, ["off", "workers", "cockpit"] as const);
1714
+ if (parsed !== undefined) settings.interface.panes.layout = parsed;
1645
1715
  }
1646
- if ("profile" in yazi) {
1647
- const v = expectEnum(issues, "panes.yazi.profile", yazi.profile, ["managed", "user"] as const);
1648
- if (v !== undefined) settings.panes.yazi.profile = v;
1716
+ if ("workers" in panes) {
1717
+ if (!isPlainObject(panes.workers))
1718
+ issues.add("interface.panes.workers", `expected a map, got ${describe(panes.workers)}`);
1719
+ else {
1720
+ const workers = panes.workers;
1721
+ issues.unknownKeys("interface.panes.workers", workers, ["ratio"]);
1722
+ if ("ratio" in workers) {
1723
+ const parsed = expectNumber(issues, "interface.panes.workers.ratio", workers.ratio, {
1724
+ min: 0.05,
1725
+ max: 0.5,
1726
+ });
1727
+ if (parsed !== undefined) settings.interface.panes.workers.ratio = parsed;
1728
+ }
1729
+ }
1649
1730
  }
1650
- if ("followCwd" in yazi) {
1651
- const v = expectBoolean(issues, "panes.yazi.followCwd", yazi.followCwd);
1652
- if (v !== undefined) settings.panes.yazi.followCwd = v;
1731
+ if ("files" in panes) {
1732
+ if (!isPlainObject(panes.files))
1733
+ issues.add("interface.panes.files", `expected a map, got ${describe(panes.files)}`);
1734
+ else {
1735
+ const files = panes.files;
1736
+ issues.unknownKeys("interface.panes.files", files, ["enabled", "mode", "profile", "followCwd", "ratio"]);
1737
+ for (const key of ["enabled", "followCwd"] as const) {
1738
+ if (!(key in files)) continue;
1739
+ const parsed = expectBoolean(issues, `interface.panes.files.${key}`, files[key]);
1740
+ if (parsed !== undefined) settings.interface.panes.files[key] = parsed;
1741
+ }
1742
+ if ("mode" in files) {
1743
+ const parsed = expectEnum(issues, "interface.panes.files.mode", files.mode, ["companion", "chooser"] as const);
1744
+ if (parsed !== undefined) settings.interface.panes.files.mode = parsed;
1745
+ }
1746
+ if ("profile" in files) {
1747
+ const parsed = expectEnum(issues, "interface.panes.files.profile", files.profile, ["managed", "user"] as const);
1748
+ if (parsed !== undefined) settings.interface.panes.files.profile = parsed;
1749
+ }
1750
+ if ("ratio" in files) {
1751
+ const parsed = expectNumber(issues, "interface.panes.files.ratio", files.ratio, { min: 0.05, max: 0.5 });
1752
+ if (parsed !== undefined) settings.interface.panes.files.ratio = parsed;
1753
+ }
1754
+ }
1653
1755
  }
1654
1756
  }
1655
1757
  }
1758
+ if ("keybindings" in ui)
1759
+ settings.interface.keybindings = validateKeybindings(issues, "interface.keybindings", ui.keybindings);
1656
1760
  }
1657
1761
  }
1658
1762
 
1659
- if ("retry" in raw) {
1660
- if (!isPlainObject(raw.retry)) {
1661
- issues.add("retry", `expected a map, got ${describe(raw.retry)}`);
1662
- } else {
1663
- issues.unknownKeys("retry", raw.retry, ["enabled", "maxRetries", "baseDelayMs", "maxDelayMs", "streamStallMs"]);
1664
- if ("enabled" in raw.retry) {
1665
- const v = expectBoolean(issues, "retry.enabled", raw.retry.enabled);
1666
- if (v !== undefined) settings.retry.enabled = v;
1763
+ if ("integrations" in raw) {
1764
+ if (!isPlainObject(raw.integrations)) issues.add("integrations", `expected a map, got ${describe(raw.integrations)}`);
1765
+ else {
1766
+ const integrations = raw.integrations;
1767
+ issues.unknownKeys("integrations", integrations, [
1768
+ "projectResources",
1769
+ "externalAgents",
1770
+ "runtimePlugins",
1771
+ "library",
1772
+ "git",
1773
+ ]);
1774
+ if ("projectResources" in integrations) {
1775
+ if (!isPlainObject(integrations.projectResources))
1776
+ issues.add("integrations.projectResources", `expected a map, got ${describe(integrations.projectResources)}`);
1777
+ else {
1778
+ issues.unknownKeys("integrations.projectResources", integrations.projectResources, ["trustProjectImports"]);
1779
+ if ("trustProjectImports" in integrations.projectResources) {
1780
+ const parsed = expectBoolean(
1781
+ issues,
1782
+ "integrations.projectResources.trustProjectImports",
1783
+ integrations.projectResources.trustProjectImports,
1784
+ );
1785
+ if (parsed !== undefined) settings.integrations.projectResources.trustProjectImports = parsed;
1786
+ }
1787
+ }
1788
+ }
1789
+ if ("runtimePlugins" in integrations) {
1790
+ const parsed = expectStringArray(issues, "integrations.runtimePlugins", integrations.runtimePlugins);
1791
+ if (parsed !== undefined) settings.integrations.runtimePlugins = parsed;
1667
1792
  }
1668
- for (const key of ["maxRetries", "baseDelayMs", "maxDelayMs", "streamStallMs"] as const) {
1669
- if (key in raw.retry) {
1670
- const v = expectInteger(issues, `retry.${key}`, raw.retry[key], { min: 0 });
1671
- if (v !== undefined) settings.retry[key] = v;
1793
+ if ("library" in integrations) {
1794
+ if (!isPlainObject(integrations.library))
1795
+ issues.add("integrations.library", `expected a map, got ${describe(integrations.library)}`);
1796
+ else {
1797
+ const library = integrations.library;
1798
+ issues.unknownKeys("integrations.library", library, ["catalog", "remote", "confirmedRemote", "sync"]);
1799
+ for (const key of ["catalog", "remote", "confirmedRemote"] as const) {
1800
+ if (!(key in library)) continue;
1801
+ const value = library[key];
1802
+ if (value !== null && typeof value !== "string")
1803
+ issues.add(`integrations.library.${key}`, `expected a string or null, got ${describe(value)}`);
1804
+ else settings.integrations.library[key] = value as string | null;
1805
+ }
1806
+ if ("sync" in library) {
1807
+ const parsed = expectBoolean(issues, "integrations.library.sync", library.sync);
1808
+ if (parsed !== undefined) settings.integrations.library.sync = parsed;
1809
+ }
1672
1810
  }
1673
1811
  }
1674
- }
1675
- }
1676
-
1677
- if ("guardrails" in raw) {
1678
- if (!isPlainObject(raw.guardrails)) {
1679
- issues.add("guardrails", `expected a map, got ${describe(raw.guardrails)}`);
1680
- } else {
1681
- const keys = [
1682
- "turnToolCallBudget",
1683
- "workerToolCallCap",
1684
- "maxDispatchRuns",
1685
- "readMaxBytes",
1686
- "observationTurnBudgetBytes",
1687
- "internalDispatchTimeoutMs",
1688
- ] as const;
1689
- issues.unknownKeys("guardrails", raw.guardrails, keys);
1690
- for (const key of keys) {
1691
- if (key in raw.guardrails) {
1692
- const v = expectInteger(issues, `guardrails.${key}`, raw.guardrails[key], { min: 1 });
1693
- if (v !== undefined) settings.guardrails[key] = v;
1812
+ if ("git" in integrations) {
1813
+ if (!isPlainObject(integrations.git))
1814
+ issues.add("integrations.git", `expected a map, got ${describe(integrations.git)}`);
1815
+ else {
1816
+ issues.unknownKeys("integrations.git", integrations.git, ["commitAttribution"]);
1817
+ if ("commitAttribution" in integrations.git) {
1818
+ const parsed = expectBoolean(issues, "integrations.git.commitAttribution", integrations.git.commitAttribution);
1819
+ if (parsed !== undefined) settings.integrations.git.commitAttribution = parsed;
1820
+ }
1821
+ }
1822
+ }
1823
+ if ("externalAgents" in integrations) {
1824
+ if (!isPlainObject(integrations.externalAgents))
1825
+ issues.add("integrations.externalAgents", `expected a map, got ${describe(integrations.externalAgents)}`);
1826
+ else {
1827
+ const external = integrations.externalAgents;
1828
+ issues.unknownKeys("integrations.externalAgents", external, ["entries", "defaults"]);
1829
+ if ("defaults" in external) {
1830
+ if (!isPlainObject(external.defaults))
1831
+ issues.add("integrations.externalAgents.defaults", `expected a map, got ${describe(external.defaults)}`);
1832
+ else {
1833
+ const defaults = external.defaults;
1834
+ issues.unknownKeys("integrations.externalAgents.defaults", defaults, [
1835
+ "connectTimeoutMs",
1836
+ "turnTimeoutMs",
1837
+ "permissionTimeoutMs",
1838
+ "toolGovernance",
1839
+ ]);
1840
+ for (const key of ["connectTimeoutMs", "turnTimeoutMs", "permissionTimeoutMs"] as const) {
1841
+ if (!(key in defaults)) continue;
1842
+ const parsed = expectInteger(issues, `integrations.externalAgents.defaults.${key}`, defaults[key], {
1843
+ min: 1,
1844
+ max: MAX_TIMER_DELAY_MS,
1845
+ });
1846
+ if (parsed !== undefined) settings.integrations.externalAgents.defaults[key] = parsed;
1847
+ }
1848
+ if ("toolGovernance" in defaults) {
1849
+ const parsed = expectEnum(
1850
+ issues,
1851
+ "integrations.externalAgents.defaults.toolGovernance",
1852
+ normalizeLegacyNamingValue(defaults.toolGovernance, "clio-policy", "clio-coder-policy"),
1853
+ TOOL_GOVERNANCE,
1854
+ );
1855
+ if (parsed !== undefined) settings.integrations.externalAgents.defaults.toolGovernance = parsed;
1856
+ }
1857
+ }
1858
+ }
1859
+ if ("entries" in external) {
1860
+ if (!Array.isArray(external.entries))
1861
+ issues.add("integrations.externalAgents.entries", `expected a list, got ${describe(external.entries)}`);
1862
+ else {
1863
+ const seen = new Set<string>();
1864
+ settings.integrations.externalAgents.entries = external.entries
1865
+ .map((entry, index) =>
1866
+ validateExternalAgent(
1867
+ issues,
1868
+ `integrations.externalAgents.entries[${index}]`,
1869
+ entry,
1870
+ settings.integrations.externalAgents.defaults,
1871
+ seen,
1872
+ ),
1873
+ )
1874
+ .filter((entry): entry is ExternalAgent => entry !== null);
1875
+ }
1876
+ }
1694
1877
  }
1695
1878
  }
1696
1879
  }
@@ -1699,12 +1882,6 @@ export function validateSettings(raw: unknown): SettingsValidationResult {
1699
1882
  return { settings, issues: issues.list };
1700
1883
  }
1701
1884
 
1702
- /**
1703
- * Validate the settings file on disk without throwing. Missing file is valid.
1704
- * Resolves the path without the clioConfigDir mkdir side effect so read-only
1705
- * surfaces (plain `clio-coder doctor`, readSettings on a fresh machine) never
1706
- * create directories.
1707
- */
1708
1885
  export function validateSettingsFile(): SettingsValidationResult {
1709
1886
  const path = join(resolveClioDirs().config, "settings.yaml");
1710
1887
  if (!existsSync(path)) return { settings: cloneValue(DEFAULT_SETTINGS), issues: [] };
@@ -1832,10 +2009,6 @@ export function updateSavedSettingsDocument(
1832
2009
  }, options);
1833
2010
  }
1834
2011
 
1835
- export function settingsLockPath(): string {
1836
- return `${settingsPath()}.lock`;
1837
- }
1838
-
1839
2012
  /**
1840
2013
  * Run `fn` while holding the settings.yaml advisory lock. Factored out of
1841
2014
  * updateSettings so the one other sanctioned settings writer (lifecycle
@@ -1900,14 +2073,14 @@ export function useTargetInSettings(
1900
2073
  : target;
1901
2074
  if (!workerTarget) return null;
1902
2075
  const sharedModel = options.model ?? target.defaultModel ?? null;
1903
- settings.orchestrator.target = target.id;
1904
- settings.orchestrator.model = options.orchestratorModel ?? sharedModel;
1905
- settings.workers.default.target = workerTarget.id;
1906
- settings.workers.default.model =
2076
+ settings.chat.target = target.id;
2077
+ settings.chat.model = options.orchestratorModel ?? sharedModel;
2078
+ settings.fleet.default.target = workerTarget.id;
2079
+ settings.fleet.default.model =
1907
2080
  options.workerModel ?? (workerTarget === target ? sharedModel : (workerTarget.defaultModel ?? null));
1908
2081
  if (options.backgroundModel !== undefined) {
1909
- settings.background.target = target.id;
1910
- settings.background.model = options.backgroundModel;
2082
+ settings.context.memory.target = target.id;
2083
+ settings.context.memory.model = options.backgroundModel;
1911
2084
  }
1912
2085
  return { workerTargetId: workerTarget.id };
1913
2086
  }
@@ -1916,22 +2089,25 @@ export function useTargetInSettings(
1916
2089
  export function removeTargetFromSettings(settings: ClioSettings, id: string): boolean {
1917
2090
  if (!settings.targets.some((entry) => entry.id === id)) return false;
1918
2091
  settings.targets = settings.targets.filter((entry) => entry.id !== id);
1919
- if (settings.orchestrator.target === id) {
1920
- settings.orchestrator.target = null;
1921
- settings.orchestrator.model = null;
2092
+ if (settings.chat.target === id) {
2093
+ settings.chat.target = null;
2094
+ settings.chat.model = null;
1922
2095
  }
1923
- if (settings.background.target === id) {
1924
- settings.background.target = null;
1925
- settings.background.model = null;
2096
+ if (settings.context.memory.target === id) {
2097
+ settings.context.memory.target = null;
2098
+ settings.context.memory.model = null;
1926
2099
  }
1927
- if (settings.workers.default.target === id) {
1928
- settings.workers.default.target = null;
1929
- settings.workers.default.model = null;
2100
+ if (settings.fleet.default.target === id) {
2101
+ settings.fleet.default.target = null;
2102
+ settings.fleet.default.model = null;
1930
2103
  }
1931
- for (const [name, profile] of Object.entries(settings.workers.profiles)) {
1932
- if (profile.target === id) delete settings.workers.profiles[name];
2104
+ for (const [name, profile] of Object.entries(settings.fleet.profiles)) {
2105
+ if (profile.target === id) delete settings.fleet.profiles[name];
1933
2106
  }
1934
- settings.scope = settings.scope.filter((entry) => entry.split("/")[0] !== id);
2107
+ settings.chat.modelPicker.cycleSet = settings.chat.modelPicker.cycleSet.filter((entry) => entry.split("/")[0] !== id);
2108
+ settings.chat.modelPicker.favorites = settings.chat.modelPicker.favorites.filter(
2109
+ (entry) => entry.split("/")[0] !== id,
2110
+ );
1935
2111
  return true;
1936
2112
  }
1937
2113
 
@@ -1944,8 +2120,8 @@ export function setFleetProfileInSettings(
1944
2120
  ): boolean {
1945
2121
  const target = settings.targets.find((entry) => entry.id === targetId);
1946
2122
  if (!target) return false;
1947
- const existing = settings.workers.profiles[name];
1948
- settings.workers.profiles[name] = {
2123
+ const existing = settings.fleet.profiles[name];
2124
+ settings.fleet.profiles[name] = {
1949
2125
  target: target.id,
1950
2126
  model: options.model ?? target.defaultModel ?? null,
1951
2127
  thinkingLevel: options.thinkingLevel ?? existing?.thinkingLevel ?? "off",
@@ -1955,11 +2131,11 @@ export function setFleetProfileInSettings(
1955
2131
 
1956
2132
  /** Remove a fleet profile and its agent bindings; returns how many bindings went with it. */
1957
2133
  export function removeFleetProfileFromSettings(settings: ClioSettings, name: string): number {
1958
- delete settings.workers.profiles[name];
2134
+ delete settings.fleet.profiles[name];
1959
2135
  let removedBindings = 0;
1960
- for (const [agentId, profileName] of Object.entries(settings.workers.agentBindings)) {
2136
+ for (const [agentId, profileName] of Object.entries(settings.fleet.agentProfiles)) {
1961
2137
  if (profileName !== name) continue;
1962
- delete settings.workers.agentBindings[agentId];
2138
+ delete settings.fleet.agentProfiles[agentId];
1963
2139
  removedBindings += 1;
1964
2140
  }
1965
2141
  return removedBindings;
@@ -1967,7 +2143,7 @@ export function removeFleetProfileFromSettings(settings: ClioSettings, name: str
1967
2143
 
1968
2144
  /** Bind a native agent to a profile. False for an ACP delegation agent, which ignores native routing. */
1969
2145
  export function bindAgentProfileInSettings(settings: ClioSettings, agentId: string, profileName: string): boolean {
1970
- if (settings.delegation.agents.some((agent) => agent.id === agentId)) return false;
1971
- settings.workers.agentBindings[agentId] = profileName;
2146
+ if (settings.integrations.externalAgents.entries.some((agent) => agent.id === agentId)) return false;
2147
+ settings.fleet.agentProfiles[agentId] = profileName;
1972
2148
  return true;
1973
2149
  }