@pi-unipi/unipi 3.0.0-alpha.2 → 3.0.0-alpha.21

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 (359) hide show
  1. package/CHANGELOG.md +292 -0
  2. package/README.md +8 -11
  3. package/docs/prefix-cache-architecture.md +14 -0
  4. package/package.json +22 -24
  5. package/packages/ask-user/README.md +55 -107
  6. package/packages/ask-user/ask-ui.ts +306 -732
  7. package/packages/ask-user/clipboard.ts +50 -0
  8. package/packages/ask-user/config.ts +74 -78
  9. package/packages/ask-user/package.json +2 -2
  10. package/packages/ask-user/questions.ts +151 -0
  11. package/packages/ask-user/skills/ask-user/SKILL.md +59 -133
  12. package/packages/ask-user/tools.ts +198 -432
  13. package/packages/autocomplete/package.json +1 -1
  14. package/packages/autocomplete/src/constants.ts +38 -43
  15. package/packages/autocomplete/src/sorting.ts +8 -0
  16. package/packages/background-tasks/package.json +2 -2
  17. package/packages/background-tasks/src/config.ts +0 -73
  18. package/packages/background-tasks/src/index.ts +20 -16
  19. package/packages/background-tasks/src/registry.ts +1 -171
  20. package/packages/background-tasks/src/types.ts +0 -32
  21. package/packages/btw/README.md +13 -56
  22. package/packages/btw/extensions/btw.ts +403 -1796
  23. package/packages/btw/package.json +5 -2
  24. package/packages/btw/skills/btw/SKILL.md +7 -143
  25. package/packages/compactor/README.md +56 -287
  26. package/packages/compactor/package.json +5 -9
  27. package/packages/compactor/skills/compactor/SKILL.md +13 -20
  28. package/packages/compactor/src/card.ts +90 -0
  29. package/packages/compactor/src/commands/index.ts +73 -130
  30. package/packages/compactor/src/compaction/compact-args.ts +5 -3
  31. package/packages/compactor/src/compaction/filter-noise.ts +1 -1
  32. package/packages/compactor/src/compaction/hooks.ts +487 -311
  33. package/packages/compactor/src/compaction/source.ts +178 -0
  34. package/packages/compactor/src/compaction/summarize.ts +586 -52
  35. package/packages/compactor/src/config/manager.ts +130 -127
  36. package/packages/compactor/src/config/schema.ts +32 -48
  37. package/packages/compactor/src/index.ts +40 -431
  38. package/packages/compactor/src/info-screen.ts +44 -190
  39. package/packages/compactor/src/session/recall-blocks.ts +1 -1
  40. package/packages/compactor/src/stats.ts +76 -0
  41. package/packages/compactor/src/tools/context-budget.ts +17 -34
  42. package/packages/compactor/src/tools/ctx-doctor.ts +45 -36
  43. package/packages/compactor/src/tools/register.ts +29 -269
  44. package/packages/compactor/src/types.ts +47 -186
  45. package/packages/core/README.md +25 -0
  46. package/packages/core/child-agent.ts +1 -0
  47. package/packages/core/command-echo.ts +63 -0
  48. package/packages/core/compaction-context.ts +41 -0
  49. package/packages/core/constants.ts +8 -38
  50. package/packages/core/events.ts +0 -53
  51. package/packages/core/fusion-status.ts +4 -0
  52. package/packages/core/global.d.ts +10 -0
  53. package/packages/core/harness-messages.ts +392 -0
  54. package/packages/core/index.ts +14 -0
  55. package/packages/core/kanboard-status.ts +49 -0
  56. package/packages/core/long-horizon-owner-status.ts +46 -0
  57. package/packages/core/model-cache.ts +81 -7
  58. package/packages/core/package.json +6 -3
  59. package/packages/core/src/attach/detect.ts +163 -0
  60. package/packages/core/src/child-agent/child-args.ts +31 -0
  61. package/packages/core/src/child-agent/delivery.ts +41 -0
  62. package/packages/core/src/child-agent/index.ts +5 -0
  63. package/packages/core/src/child-agent/readers.ts +203 -0
  64. package/packages/core/src/child-agent/runtime.ts +584 -0
  65. package/packages/core/src/evidence.ts +78 -0
  66. package/packages/core/src/hints/browser.ts +278 -0
  67. package/packages/core/src/hints/crab-data.ts +19 -0
  68. package/packages/core/src/hints/index.ts +865 -0
  69. package/packages/core/src/hints/lines.ts +607 -0
  70. package/packages/core/src/hints/store.ts +120 -0
  71. package/packages/core/src/jev/chatter.ts +24 -0
  72. package/packages/core/src/jev/settings.ts +177 -14
  73. package/packages/core/src/package-colors.ts +2 -0
  74. package/packages/core/src/settings/catalog.ts +39 -15
  75. package/packages/core/src/settings/engine.ts +48 -2
  76. package/packages/core/src/settings/hub.ts +83 -20
  77. package/packages/core/src/settings/open.ts +49 -0
  78. package/packages/core/src/settings/schema.ts +27 -7
  79. package/packages/core/src/tui/hub-kit.ts +18 -5
  80. package/packages/core/src/tui/kit.ts +198 -0
  81. package/packages/core/src/tui/progress.ts +73 -0
  82. package/packages/core/src/turn/arbiter.ts +290 -0
  83. package/packages/footer/README.md +4 -6
  84. package/packages/footer/package.json +3 -3
  85. package/packages/footer/src/glance-editor.ts +19 -11
  86. package/packages/footer/src/help.ts +1 -1
  87. package/packages/footer/src/index.ts +36 -3
  88. package/packages/footer/src/presets.ts +3 -4
  89. package/packages/footer/src/rendering/icons.ts +0 -20
  90. package/packages/footer/src/segments/compactor.ts +52 -142
  91. package/packages/footer/src/segments/status-ext.ts +0 -1
  92. package/packages/fusion/README.md +14 -4
  93. package/packages/fusion/package.json +4 -3
  94. package/packages/fusion/src/child-args.ts +1 -0
  95. package/packages/fusion/src/index.ts +161 -8
  96. package/packages/fusion/src/pi-settings.ts +57 -0
  97. package/packages/fusion/src/picker.ts +186 -51
  98. package/packages/fusion/src/preset.ts +56 -2
  99. package/packages/fusion/src/prompts.ts +1 -1
  100. package/packages/fusion/src/sidekick-runtime.ts +1 -420
  101. package/packages/fusion/src/tools.ts +107 -128
  102. package/packages/fusion/src/transcript.ts +27 -85
  103. package/packages/info-screen/package.json +2 -2
  104. package/packages/input-shortcuts/README.md +18 -30
  105. package/packages/input-shortcuts/package.json +2 -2
  106. package/packages/input-shortcuts/src/burst.ts +88 -0
  107. package/packages/input-shortcuts/src/chord-overlay.ts +20 -70
  108. package/packages/input-shortcuts/src/cleared-input.ts +97 -0
  109. package/packages/input-shortcuts/src/index.ts +99 -193
  110. package/packages/input-shortcuts/src/last-response.ts +40 -0
  111. package/packages/input-shortcuts/src/registers.ts +8 -23
  112. package/packages/input-shortcuts/src/types.ts +4 -16
  113. package/packages/input-shortcuts/src/undo-redo.ts +59 -47
  114. package/packages/kanboard/README.md +51 -47
  115. package/packages/kanboard/index.ts +293 -55
  116. package/packages/kanboard/package.json +8 -8
  117. package/packages/kanboard/skills/kanboard/SKILL.md +61 -27
  118. package/packages/kanboard/src/badges.ts +93 -0
  119. package/packages/kanboard/src/bin.ts +4 -2
  120. package/packages/kanboard/src/commands.ts +218 -84
  121. package/packages/kanboard/src/debug.ts +20 -0
  122. package/packages/kanboard/src/guard.ts +267 -109
  123. package/packages/kanboard/src/monitor.ts +330 -0
  124. package/packages/kanboard/src/notice-buffer.ts +74 -0
  125. package/packages/kanboard/src/progress.ts +27 -0
  126. package/packages/kanboard/src/reminders.ts +234 -0
  127. package/packages/kanboard/src/settings.ts +69 -15
  128. package/packages/kanboard/src/shapes.ts +5 -0
  129. package/packages/long-horizon/index.ts +190 -13
  130. package/packages/long-horizon/package.json +3 -3
  131. package/packages/long-horizon/src/commands.ts +103 -7
  132. package/packages/long-horizon/src/compaction-brief.ts +35 -0
  133. package/packages/long-horizon/src/engine/continuation.ts +84 -15
  134. package/packages/long-horizon/src/engine/goal-state.ts +64 -13
  135. package/packages/long-horizon/src/engine/nudge-stash.ts +71 -0
  136. package/packages/long-horizon/src/engine/ralph.ts +3 -2
  137. package/packages/long-horizon/src/engine/runaway.ts +68 -11
  138. package/packages/long-horizon/src/engine/verifier.ts +19 -0
  139. package/packages/long-horizon/src/gate.ts +37 -12
  140. package/packages/long-horizon/src/judge/resolve.ts +1 -0
  141. package/packages/long-horizon/src/owner.ts +35 -2
  142. package/packages/long-horizon/src/progress.ts +87 -0
  143. package/packages/long-horizon/src/prompts/goal.ts +1 -1
  144. package/packages/long-horizon/src/runtime.ts +172 -18
  145. package/packages/long-horizon/src/settings.ts +54 -39
  146. package/packages/long-horizon/src/tools/goal.ts +6 -0
  147. package/packages/long-horizon/src/tools/graph.ts +1 -1
  148. package/packages/long-horizon/src/tools/swarm.ts +3 -3
  149. package/packages/mcp/package.json +2 -2
  150. package/packages/memory/commands.ts +7 -2
  151. package/packages/memory/index.ts +210 -32
  152. package/packages/memory/package.json +3 -3
  153. package/packages/memory/save-session.ts +264 -0
  154. package/packages/memory/settings.ts +17 -0
  155. package/packages/memory/tools.ts +253 -203
  156. package/packages/notify/package.json +2 -2
  157. package/packages/skill-registry/README.md +53 -16
  158. package/packages/skill-registry/index.ts +230 -0
  159. package/packages/skill-registry/package.json +19 -8
  160. package/packages/skill-registry/src/editor.ts +206 -0
  161. package/packages/skill-registry/src/judge.ts +289 -0
  162. package/packages/skill-registry/src/registry.ts +87 -0
  163. package/packages/skill-registry/src/settings.ts +259 -0
  164. package/packages/skill-registry/src/vault.ts +80 -0
  165. package/packages/subagents/README.md +39 -119
  166. package/packages/subagents/package.json +6 -13
  167. package/packages/subagents/skills/subagents/SKILL.md +18 -42
  168. package/packages/subagents/src/agents.ts +213 -0
  169. package/packages/subagents/src/cards.ts +182 -0
  170. package/packages/subagents/src/index.ts +609 -1084
  171. package/packages/subagents/src/manager.ts +429 -0
  172. package/packages/subagents/src/profiles.ts +130 -0
  173. package/packages/subagents/src/transcript.ts +154 -0
  174. package/packages/subagents/src/ui.ts +389 -0
  175. package/packages/unipi/bundled.js +39756 -45569
  176. package/packages/unipi/index.ts +55 -10
  177. package/packages/updater/package.json +2 -2
  178. package/packages/utility/README.md +76 -69
  179. package/packages/utility/package.json +2 -2
  180. package/packages/utility/skills/image/SKILL.md +37 -0
  181. package/packages/utility/skills/utility/SKILL.md +17 -60
  182. package/packages/utility/src/answer/extract.ts +128 -0
  183. package/packages/utility/src/answer/index.ts +213 -0
  184. package/packages/utility/src/answer/reply.ts +133 -0
  185. package/packages/utility/src/answer/web.ts +167 -0
  186. package/packages/utility/src/attach/index.ts +205 -0
  187. package/packages/utility/src/commands.ts +38 -142
  188. package/packages/utility/src/diagnostics/engine.ts +50 -5
  189. package/packages/utility/src/image/index.ts +33 -0
  190. package/packages/utility/src/image/models.ts +221 -0
  191. package/packages/{image/src → utility/src/image}/recognize.ts +2 -2
  192. package/packages/utility/src/image/settings.ts +178 -0
  193. package/packages/{image/src/image-source.ts → utility/src/image/source.ts} +1 -1
  194. package/packages/utility/src/image/tools.ts +168 -0
  195. package/packages/utility/src/image/transports.ts +186 -0
  196. package/packages/utility/src/index.ts +68 -389
  197. package/packages/utility/src/lifecycle/cleanup.ts +116 -311
  198. package/packages/utility/src/rename/gate.ts +102 -0
  199. package/packages/utility/src/rename/index.ts +191 -0
  200. package/packages/utility/src/rename/session.ts +170 -0
  201. package/packages/utility/src/render/delegated.ts +285 -0
  202. package/packages/utility/src/render/harness.ts +683 -0
  203. package/packages/utility/src/render/parse.ts +156 -0
  204. package/packages/utility/src/render/pi-settings.ts +38 -0
  205. package/packages/utility/src/render/reply-bg.ts +307 -0
  206. package/packages/utility/src/render/simple.ts +724 -0
  207. package/packages/utility/src/render/spacing.ts +235 -0
  208. package/packages/utility/src/render/styled.ts +251 -0
  209. package/packages/utility/src/render/tools.ts +353 -0
  210. package/packages/utility/src/settings.ts +166 -233
  211. package/packages/utility/src/types.ts +1 -222
  212. package/packages/watchdog/index.ts +17 -6
  213. package/packages/watchdog/package.json +3 -3
  214. package/packages/watchdog/src/config.ts +3 -2
  215. package/packages/web-api/package.json +2 -2
  216. package/packages/workflow/README.md +3 -0
  217. package/packages/workflow/index.ts +0 -2
  218. package/packages/workflow/package.json +2 -2
  219. package/packages/workflow/src/permission/decide.ts +6 -4
  220. package/packages/workflow/src/permission/index.ts +17 -7
  221. package/packages/workflow/src/permission/settings.ts +39 -4
  222. package/packages/workflow/src/plan/index.ts +19 -4
  223. package/packages/background-tasks/extensions/delegate-child.ts +0 -1
  224. package/packages/background-tasks/src/context-parent-snapshot.ts +0 -142
  225. package/packages/background-tasks/src/context-token-budget.ts +0 -903
  226. package/packages/background-tasks/src/context-visible-conversation-v2.ts +0 -551
  227. package/packages/background-tasks/src/delegate/artifacts.ts +0 -487
  228. package/packages/background-tasks/src/delegate/budget.ts +0 -415
  229. package/packages/background-tasks/src/delegate/hook-contract-evidence.json +0 -18
  230. package/packages/background-tasks/src/delegate/hook-contract.ts +0 -154
  231. package/packages/background-tasks/src/delegate/launch.ts +0 -484
  232. package/packages/background-tasks/src/delegate/result-package.ts +0 -459
  233. package/packages/background-tasks/src/delegate/runner.ts +0 -430
  234. package/packages/background-tasks/src/delegate/seed.ts +0 -423
  235. package/packages/background-tasks/src/delegate/types.ts +0 -323
  236. package/packages/background-tasks/src/delegate-child-extension.ts +0 -978
  237. package/packages/background-tasks/src/delegate-extension.ts +0 -654
  238. package/packages/background-tasks/src/fixtures/delegate-context-incident.json +0 -17
  239. package/packages/compactor/skills/compactor-detail/SKILL.md +0 -116
  240. package/packages/compactor/skills/compactor-doctor/SKILL.md +0 -71
  241. package/packages/compactor/skills/compactor-stats/SKILL.md +0 -51
  242. package/packages/compactor/src/compaction/build-sections.ts +0 -76
  243. package/packages/compactor/src/compaction/extract/goals.ts +0 -36
  244. package/packages/compactor/src/compaction/extract/preferences.ts +0 -40
  245. package/packages/compactor/src/compaction/format.ts +0 -53
  246. package/packages/compactor/src/compaction/merge.ts +0 -143
  247. package/packages/compactor/src/config/presets.ts +0 -160
  248. package/packages/compactor/src/executor/executor.ts +0 -233
  249. package/packages/compactor/src/executor/runtime.ts +0 -115
  250. package/packages/compactor/src/security/evaluator.ts +0 -89
  251. package/packages/compactor/src/security/policy.ts +0 -96
  252. package/packages/compactor/src/security/scanner.ts +0 -65
  253. package/packages/compactor/src/session/auto-inject.ts +0 -60
  254. package/packages/compactor/src/session/db.ts +0 -325
  255. package/packages/compactor/src/session/extract.ts +0 -144
  256. package/packages/compactor/src/session/resume-inject.ts +0 -64
  257. package/packages/compactor/src/session/snapshot.ts +0 -327
  258. package/packages/compactor/src/tools/ctx-batch-execute.ts +0 -41
  259. package/packages/compactor/src/tools/ctx-execute-file.ts +0 -23
  260. package/packages/compactor/src/tools/ctx-execute.ts +0 -23
  261. package/packages/compactor/src/tools/ctx-stats.ts +0 -96
  262. package/packages/image/README.md +0 -99
  263. package/packages/image/package.json +0 -61
  264. package/packages/image/skills/image/SKILL.md +0 -76
  265. package/packages/image/src/generate.ts +0 -256
  266. package/packages/image/src/index.ts +0 -135
  267. package/packages/image/src/models.ts +0 -519
  268. package/packages/image/src/openai-images-api.ts +0 -282
  269. package/packages/image/src/register-providers.ts +0 -209
  270. package/packages/image/src/settings.ts +0 -173
  271. package/packages/image/src/tools.ts +0 -326
  272. package/packages/input-shortcuts/src/clipboard.ts +0 -119
  273. package/packages/kanboard/src/runner.ts +0 -817
  274. package/packages/skill-registry/skills/auto/SKILL.md +0 -465
  275. package/packages/skill-registry/skills/brainstorm/SKILL.md +0 -219
  276. package/packages/skill-registry/skills/chore-create/SKILL.md +0 -315
  277. package/packages/skill-registry/skills/chore-execute/SKILL.md +0 -314
  278. package/packages/skill-registry/skills/consolidate/SKILL.md +0 -142
  279. package/packages/skill-registry/skills/consultant/SKILL.md +0 -97
  280. package/packages/skill-registry/skills/debug/SKILL.md +0 -228
  281. package/packages/skill-registry/skills/document/SKILL.md +0 -120
  282. package/packages/skill-registry/skills/fix/SKILL.md +0 -212
  283. package/packages/skill-registry/skills/gather-context/SKILL.md +0 -145
  284. package/packages/skill-registry/skills/plan/SKILL.md +0 -231
  285. package/packages/skill-registry/skills/quick-fix/SKILL.md +0 -135
  286. package/packages/skill-registry/skills/quick-work/SKILL.md +0 -110
  287. package/packages/skill-registry/skills/research/SKILL.md +0 -251
  288. package/packages/skill-registry/skills/review-work/SKILL.md +0 -187
  289. package/packages/skill-registry/skills/scan-issues/SKILL.md +0 -195
  290. package/packages/skill-registry/skills/work/SKILL.md +0 -218
  291. package/packages/skill-registry/skills/worktree-create/SKILL.md +0 -69
  292. package/packages/skill-registry/skills/worktree-list/SKILL.md +0 -78
  293. package/packages/skill-registry/skills/worktree-merge/SKILL.md +0 -89
  294. package/packages/subagents/agents/delegate.md +0 -14
  295. package/packages/subagents/agents/oracle.md +0 -78
  296. package/packages/subagents/agents/researcher.md +0 -52
  297. package/packages/subagents/agents/reviewer.md +0 -80
  298. package/packages/subagents/agents/scout.md +0 -50
  299. package/packages/subagents/agents/worker.md +0 -59
  300. package/packages/subagents/prompts/council.md +0 -48
  301. package/packages/subagents/prompts/gather-context-and-clarify.md +0 -13
  302. package/packages/subagents/prompts/parallel-cleanup.md +0 -59
  303. package/packages/subagents/prompts/parallel-research.md +0 -50
  304. package/packages/subagents/prompts/parallel-review.md +0 -54
  305. package/packages/subagents/prompts/review-loop.md +0 -43
  306. package/packages/subagents/skills/council-mode/SKILL.md +0 -230
  307. package/packages/subagents/skills/subagents/references/constraints-and-recipes.md +0 -259
  308. package/packages/subagents/skills/subagents/references/execution-controls.md +0 -454
  309. package/packages/subagents/skills/subagents/references/management-authoring-rpc.md +0 -161
  310. package/packages/subagents/skills/subagents/references/multi-lane-orchestration.md +0 -39
  311. package/packages/subagents/skills/subagents/references/prompting-and-roles.md +0 -267
  312. package/packages/subagents/src/acceptance.ts +0 -331
  313. package/packages/subagents/src/agent-manager.ts +0 -405
  314. package/packages/subagents/src/agent-memory.ts +0 -300
  315. package/packages/subagents/src/agent-overrides.ts +0 -183
  316. package/packages/subagents/src/agent-runner.ts +0 -271
  317. package/packages/subagents/src/async-runner.ts +0 -422
  318. package/packages/subagents/src/authority-policy.ts +0 -47
  319. package/packages/subagents/src/budgets.ts +0 -285
  320. package/packages/subagents/src/child-safety.ts +0 -140
  321. package/packages/subagents/src/config.ts +0 -249
  322. package/packages/subagents/src/conversation-viewer.ts +0 -281
  323. package/packages/subagents/src/core-compat.ts +0 -94
  324. package/packages/subagents/src/custom-agents.ts +0 -343
  325. package/packages/subagents/src/file-system-retry.ts +0 -50
  326. package/packages/subagents/src/fleet-data.ts +0 -105
  327. package/packages/subagents/src/fleet-view.ts +0 -322
  328. package/packages/subagents/src/foreground-detach.ts +0 -59
  329. package/packages/subagents/src/fork-context.ts +0 -285
  330. package/packages/subagents/src/global.d.ts +0 -7
  331. package/packages/subagents/src/guide.ts +0 -129
  332. package/packages/subagents/src/mission-state.ts +0 -133
  333. package/packages/subagents/src/mission-store.ts +0 -436
  334. package/packages/subagents/src/output-limits.ts +0 -142
  335. package/packages/subagents/src/parity-types.ts +0 -315
  336. package/packages/subagents/src/pi-args.ts +0 -167
  337. package/packages/subagents/src/result-files.ts +0 -232
  338. package/packages/subagents/src/result-watcher.ts +0 -221
  339. package/packages/subagents/src/retained-children.ts +0 -176
  340. package/packages/subagents/src/run-fanout-budget.ts +0 -356
  341. package/packages/subagents/src/scheduled-runs.ts +0 -349
  342. package/packages/subagents/src/schemas.ts +0 -263
  343. package/packages/subagents/src/slash-commands.ts +0 -80
  344. package/packages/subagents/src/supervisor-channel.ts +0 -249
  345. package/packages/subagents/src/tool-handler.ts +0 -1144
  346. package/packages/subagents/src/types.ts +0 -238
  347. package/packages/subagents/src/widget.ts +0 -457
  348. package/packages/subagents/src/workflow-script.ts +0 -737
  349. package/packages/subagents/src/workflow-worker.ts +0 -384
  350. package/packages/subagents/src/worktree.ts +0 -614
  351. package/packages/utility/src/analytics/collector.ts +0 -293
  352. package/packages/utility/src/info-screen.ts +0 -80
  353. package/packages/utility/src/lifecycle/process.ts +0 -162
  354. package/packages/utility/src/prefix-cache.ts +0 -263
  355. package/packages/utility/src/skill-discovery.ts +0 -542
  356. package/packages/utility/src/tools/env.ts +0 -113
  357. package/packages/utility/src/tui/name-badge-state.ts +0 -315
  358. package/packages/utility/src/tui/name-badge.ts +0 -109
  359. /package/packages/{subagents/src → core/src/child-agent}/pi-spawn.ts +0 -0
@@ -1,285 +0,0 @@
1
- /**
2
- * @pi-unipi/subagents — Turn / tool / usage budgets
3
- *
4
- * Ported from pi-subagents src/runs/shared/{turn-budget,tool-budget,
5
- * usage-budget}.ts. Semantics identical; env override uses OUR prefix
6
- * (UNIPI_SUBAGENT_TOOL_BUDGET). In-process foreground children enforce the
7
- * turn budget at assistant-turn boundaries and the tool budget by blocking
8
- * tools past the hard cap.
9
- */
10
-
11
- import type {
12
- ResolvedTurnBudget,
13
- ResolvedToolBudget,
14
- ToolBudgetConfig,
15
- TurnBudgetState,
16
- ToolBudgetState,
17
- UsageBudgetConfig,
18
- } from "./parity-types.js";
19
-
20
- // ============================================================================
21
- // Turn budget
22
- // ============================================================================
23
-
24
- export const DEFAULT_TURN_BUDGET_GRACE_TURNS = 1;
25
-
26
- export function resolveTurnBudgetConfig(
27
- raw: unknown,
28
- label = "turnBudget",
29
- ): { turnBudget?: ResolvedTurnBudget; error?: string } {
30
- if (raw === undefined) return {};
31
- if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
32
- return { error: `${label} must be an object with maxTurns and optional graceTurns.` };
33
- }
34
- const unknownField = Object.keys(raw as Record<string, unknown>).find(
35
- (key) => key !== "maxTurns" && key !== "graceTurns",
36
- );
37
- if (unknownField) return { error: `${label}.${unknownField} is not supported.` };
38
- const budget = raw as { maxTurns?: unknown; graceTurns?: unknown };
39
- if (typeof budget.maxTurns !== "number" || !Number.isInteger(budget.maxTurns) || budget.maxTurns < 1) {
40
- return { error: `${label}.maxTurns must be an integer >= 1.` };
41
- }
42
- const graceTurns = budget.graceTurns ?? DEFAULT_TURN_BUDGET_GRACE_TURNS;
43
- if (typeof graceTurns !== "number" || !Number.isInteger(graceTurns) || graceTurns < 0) {
44
- return { error: `${label}.graceTurns must be an integer >= 0.` };
45
- }
46
- return { turnBudget: { maxTurns: budget.maxTurns, graceTurns } };
47
- }
48
-
49
- export function appendTurnBudgetSystemPrompt(
50
- systemPrompt: string,
51
- budget: { maxTurns: number; graceTurns?: number } | undefined,
52
- ): string {
53
- if (!budget) return systemPrompt;
54
- const graceTurns = budget.graceTurns ?? DEFAULT_TURN_BUDGET_GRACE_TURNS;
55
- const grace = graceTurns === 1 ? "1 additional assistant turn" : `${graceTurns} additional assistant turns`;
56
- const block = [
57
- "## Turn budget",
58
- `This child run has a soft budget of ${budget.maxTurns} assistant turn${budget.maxTurns === 1 ? "" : "s"}.`,
59
- `After that, ${grace} may be allowed only for a final wrap-up.`,
60
- "When you approach or reach the soft budget, stop starting new tool work and return the final answer immediately.",
61
- "If you continue past the soft budget plus grace turns, the supervisor may abort the run and return only partial output.",
62
- ].join("\n");
63
- return systemPrompt.trim() ? `${systemPrompt.trim()}\n\n${block}` : block;
64
- }
65
-
66
- export function turnBudgetSoftNote(budget: ResolvedTurnBudget, turnCount: number): string {
67
- return `Turn budget wrap-up was requested after ${turnCount} assistant turn${turnCount === 1 ? "" : "s"} (soft limit ${budget.maxTurns}, grace ${budget.graceTurns}). Output may be partial.`;
68
- }
69
-
70
- export function turnBudgetExceededMessage(budget: ResolvedTurnBudget, turnCount: number): string {
71
- return `Subagent exceeded turn budget after ${turnCount} assistant turn${turnCount === 1 ? "" : "s"} (soft limit ${budget.maxTurns} + grace ${budget.graceTurns}).`;
72
- }
73
-
74
- export function turnBudgetDeferredNote(budget: ResolvedTurnBudget, turnCount: number): string {
75
- return `Turn-budget termination was deferred at ${turnCount} assistant turn${turnCount === 1 ? "" : "s"} (soft limit ${budget.maxTurns} + grace ${budget.graceTurns}) because the assistant started tool work. The run ended before another safe assistant boundary; output may be partial.`;
76
- }
77
-
78
- export function formatTurnBudgetOutput(message: string, output: string): string {
79
- return output.trim()
80
- ? `${message}\n\nPartial output before turn-budget abort:\n${output}`
81
- : message;
82
- }
83
-
84
- export function turnBudgetDecision(
85
- budget: ResolvedTurnBudget,
86
- turnCount: number,
87
- terminalAssistantStop: boolean,
88
- toolWorkActiveOrStarting: boolean,
89
- enforceHardLimit = false,
90
- ): "continue" | "defer" | "abort" {
91
- const hardLimit = budget.maxTurns + budget.graceTurns;
92
- if (terminalAssistantStop || turnCount < hardLimit) return "continue";
93
- if (toolWorkActiveOrStarting && !enforceHardLimit) return "defer";
94
- return "abort";
95
- }
96
-
97
- export function turnBudgetState(
98
- budget: ResolvedTurnBudget,
99
- turnCount: number,
100
- exceeded: boolean,
101
- ): TurnBudgetState {
102
- return {
103
- ...budget,
104
- turns: turnCount,
105
- outcome: exceeded ? "exceeded" : "wrap-up-requested",
106
- };
107
- }
108
-
109
- // ============================================================================
110
- // Tool budget
111
- // ============================================================================
112
-
113
- export const DEFAULT_TOOL_BUDGET_BLOCK = ["read", "grep", "find", "ls"] as const;
114
- export const TOOL_BUDGET_ENV = "UNIPI_SUBAGENT_TOOL_BUDGET";
115
-
116
- export function normalizeToolBudgetBlock(block: ToolBudgetConfig["block"] | undefined): "*" | string[] {
117
- if (block === "*") return "*";
118
- if (block === undefined) return [...DEFAULT_TOOL_BUDGET_BLOCK];
119
- return [...new Set(block.map((tool) => tool.trim()).filter(Boolean))];
120
- }
121
-
122
- export function validateToolBudgetConfig(
123
- raw: unknown,
124
- label = "toolBudget",
125
- options: { minimumHard?: 0 | 1 } = {},
126
- ): { budget?: ResolvedToolBudget; error?: string } {
127
- if (raw === undefined) return {};
128
- if (!raw || typeof raw !== "object" || Array.isArray(raw)) {
129
- return { error: `${label} must be an object with hard and optional soft/block.` };
130
- }
131
- const value = raw as ToolBudgetConfig;
132
- const minimumHard = options.minimumHard ?? 1;
133
- if (typeof value.hard !== "number" || !Number.isInteger(value.hard) || value.hard < minimumHard) {
134
- return { error: `${label}.hard must be an integer >= ${minimumHard}.` };
135
- }
136
- if (value.soft !== undefined && (typeof value.soft !== "number" || !Number.isInteger(value.soft) || value.soft < 1)) {
137
- return { error: `${label}.soft must be an integer >= 1 when provided.` };
138
- }
139
- if (value.soft !== undefined && value.soft > value.hard) {
140
- return { error: `${label}.soft must be <= ${label}.hard.` };
141
- }
142
- if (value.block !== undefined && value.block !== "*") {
143
- if (!Array.isArray(value.block)) return { error: `${label}.block must be "*" or an array of tool names.` };
144
- if (value.block.length === 0) return { error: `${label}.block must contain at least one tool name.` };
145
- for (const item of value.block) {
146
- if (typeof item !== "string" || !item.trim()) return { error: `${label}.block must contain non-empty tool names.` };
147
- }
148
- }
149
- return {
150
- budget: {
151
- hard: value.hard,
152
- ...(value.soft !== undefined ? { soft: value.soft } : {}),
153
- block: normalizeToolBudgetBlock(value.block),
154
- },
155
- };
156
- }
157
-
158
- export function shouldBlockToolForBudget(
159
- budget: ResolvedToolBudget,
160
- toolName: string,
161
- nextToolCount: number,
162
- ): boolean {
163
- if (nextToolCount <= budget.hard) return false;
164
- return budget.block === "*" || budget.block.includes(toolName);
165
- }
166
-
167
- export function toolBudgetSoftNudge(budget: ResolvedToolBudget, toolCount: number): string {
168
- return `Tool budget soft limit reached after ${toolCount} tool call${toolCount === 1 ? "" : "s"} (soft ${budget.soft}, hard ${budget.hard}). Stop starting new browsing/search work and finalize from the context you already have.`;
169
- }
170
-
171
- export function toolBudgetBlockedMessage(budget: ResolvedToolBudget, toolName: string, toolCount: number): string {
172
- return `Tool budget hard limit reached after ${toolCount} tool call${toolCount === 1 ? "" : "s"} (hard ${budget.hard}). The '${toolName}' tool is blocked so you can finalize from the context you already have.`;
173
- }
174
-
175
- export function toolBudgetState(
176
- budget: ResolvedToolBudget,
177
- toolCount: number,
178
- blockedTool?: string,
179
- ): ToolBudgetState {
180
- const overHard = toolCount > budget.hard;
181
- const overSoft = budget.soft !== undefined && toolCount >= budget.soft;
182
- return {
183
- ...budget,
184
- toolCalls: toolCount,
185
- outcome: overHard ? "hard-blocked" : overSoft ? "soft-reached" : "within-budget",
186
- };
187
- }
188
-
189
- // ============================================================================
190
- // Usage budget
191
- // ============================================================================
192
-
193
- export interface UsageBudgetLimitConfig {
194
- soft?: number;
195
- hard: number;
196
- }
197
-
198
- export interface UsageBudgetState {
199
- version: 1;
200
- source: "reported";
201
- tokens?: UsageBudgetLimitConfig & { used: number; outcome: "within-budget" | "soft-exceeded" | "hard-exceeded" };
202
- costUsd?: UsageBudgetLimitConfig & { used: number; outcome: "within-budget" | "soft-exceeded" | "hard-exceeded" };
203
- exhausted: boolean;
204
- reason?: "tokens" | "costUsd";
205
- }
206
-
207
- function validateLimit(raw: unknown, label: string): { limit?: UsageBudgetLimitConfig; error?: string } {
208
- if (!raw || typeof raw !== "object" || Array.isArray(raw)) return { error: `${label} must be an object with hard and optional soft.` };
209
- const value = raw as Record<string, unknown>;
210
- const unknown = Object.keys(value).find((key) => key !== "soft" && key !== "hard");
211
- if (unknown) return { error: `${label}.${unknown} is not supported.` };
212
- if (typeof value.hard !== "number" || !Number.isFinite(value.hard) || value.hard <= 0) {
213
- return { error: `${label}.hard must be a positive number.` };
214
- }
215
- if (value.soft !== undefined && (typeof value.soft !== "number" || !Number.isFinite(value.soft) || value.soft <= 0)) {
216
- return { error: `${label}.soft must be a positive number when provided.` };
217
- }
218
- if (value.soft !== undefined && value.soft > value.hard) {
219
- return { error: `${label}.soft must be <= ${label}.hard.` };
220
- }
221
- return { limit: { hard: value.hard, ...(value.soft !== undefined ? { soft: value.soft } : {}) } };
222
- }
223
-
224
- export function validateUsageBudgetConfig(value: unknown, label = "usageBudget"): { budget?: UsageBudgetConfig; error?: string } {
225
- if (value === undefined) return {};
226
- if (!value || typeof value !== "object" || Array.isArray(value)) return { error: `${label} must be an object.` };
227
- const raw = value as Record<string, unknown>;
228
- const unknown = Object.keys(raw).find((key) => key !== "tokens" && key !== "costUsd");
229
- if (unknown) return { error: `${label}.${unknown} is not supported.` };
230
- const budget: UsageBudgetConfig = {};
231
- if (raw.tokens !== undefined) {
232
- const tokens = validateLimit(raw.tokens, `${label}.tokens`);
233
- if (tokens.error) return { error: tokens.error };
234
- if (tokens.limit) budget.tokens = tokens.limit;
235
- }
236
- if (raw.costUsd !== undefined) {
237
- const costUsd = validateLimit(raw.costUsd, `${label}.costUsd`);
238
- if (costUsd.error) return { error: costUsd.error };
239
- if (costUsd.limit) budget.costUsd = costUsd.limit;
240
- }
241
- if (!budget.tokens && !budget.costUsd) return { error: `${label} must include tokens or costUsd.` };
242
- return { budget };
243
- }
244
-
245
- function metricState(
246
- limit: UsageBudgetLimitConfig | undefined,
247
- used: number,
248
- ): UsageBudgetState["tokens"] {
249
- if (!limit) return undefined;
250
- return {
251
- ...limit,
252
- used,
253
- outcome: used >= limit.hard ? "hard-exceeded" : limit.soft !== undefined && used >= limit.soft ? "soft-exceeded" : "within-budget",
254
- };
255
- }
256
-
257
- export function usageBudgetState(
258
- config: UsageBudgetConfig | undefined,
259
- totals: { inputTokens?: number; outputTokens?: number; costUsd?: number } | undefined,
260
- ): UsageBudgetState | undefined {
261
- if (!config) return undefined;
262
- const inputTokens = totals?.inputTokens ?? 0;
263
- const outputTokens = totals?.outputTokens ?? 0;
264
- const tokens = metricState(config.tokens, inputTokens + outputTokens);
265
- const costUsd = metricState(config.costUsd, totals?.costUsd ?? 0);
266
- const reason = tokens?.outcome === "hard-exceeded" ? "tokens" : costUsd?.outcome === "hard-exceeded" ? "costUsd" : undefined;
267
- return {
268
- version: 1,
269
- source: "reported",
270
- ...(tokens ? { tokens } : {}),
271
- ...(costUsd ? { costUsd } : {}),
272
- exhausted: reason !== undefined,
273
- ...(reason ? { reason } : {}),
274
- };
275
- }
276
-
277
- export function usageBudgetExceededMessage(state: UsageBudgetState): string {
278
- if (state.reason === "tokens" && state.tokens) {
279
- return `Usage budget exhausted: reported tokens ${state.tokens.used} reached hard limit ${state.tokens.hard}.`;
280
- }
281
- if (state.reason === "costUsd" && state.costUsd) {
282
- return `Usage budget exhausted: reported cost $${state.costUsd.used.toFixed(6)} reached hard limit $${state.costUsd.hard.toFixed(6)}.`;
283
- }
284
- return "Usage budget exhausted.";
285
- }
@@ -1,140 +0,0 @@
1
- /**
2
- * @pi-unipi/subagents — Child safety, depth guard, context resolution
3
- *
4
- * Ported from pi-subagents subagent-prompt-runtime.ts boundary instructions
5
- * and context-mode.ts. Tool references adapted to OUR names (spawn_helper).
6
- * In-process foreground children are always "fresh" context; explicit fork
7
- * resolves here to a visible error pointing at the async path (Phase 3 adds
8
- * the process-based fork runner).
9
- */
10
-
11
- import type { AgentConfig, SubagentsConfig } from "./types.js";
12
-
13
- export type ContextMode = "fresh" | "fork";
14
-
15
- export function isContextMode(value: unknown): value is ContextMode {
16
- return value === "fresh" || value === "fork";
17
- }
18
-
19
- export const CHILD_SUBAGENT_BOUNDARY_INSTRUCTIONS = [
20
- "You are a child subagent, not the parent orchestrator.",
21
- "The parent session owns delegation, orchestration, review fanout, and follow-up worker launches.",
22
- "Ignore prior parent-only orchestration instructions in inherited conversation history.",
23
- "Do not propose or run subagents. Complete only your assigned role-specific task with the tools available to you.",
24
- "If you need to edit files, use the available editing tools. Do not print tool-call syntax, patches, or pseudo-tool calls as text.",
25
- ].join("\n");
26
-
27
- export const CHILD_FANOUT_BOUNDARY_INSTRUCTIONS = [
28
- "You are a child subagent with explicit fanout responsibility for this assigned task.",
29
- "The parent session owns final orchestration, acceptance, and follow-up implementation launches.",
30
- "You may use the `spawn_helper` tool only for the fanout work explicitly requested in this task.",
31
- "Do not broaden yourself into general parent orchestration. Do not launch follow-up workers unless the task explicitly asks for that.",
32
- "The maxSubagentDepth cap still applies and may block further fanout.",
33
- "If you need to edit files, use the available editing tools. Do not print tool-call syntax, patches, or pseudo-tool calls as text.",
34
- ].join("\n");
35
-
36
- /** Whether the resolved agent's tools opt the child into fanout (spawn_helper). */
37
- export function isFanoutChild(agent: AgentConfig | undefined): boolean {
38
- return !!agent?.builtinToolNames?.includes("spawn_helper");
39
- }
40
-
41
- /** Prepend boundary instructions to a child prompt (fanout children get the fanout variant). */
42
- export function withChildBoundaryInstructions(prompt: string, agent: AgentConfig | undefined): string {
43
- const boundary = isFanoutChild(agent) ? CHILD_FANOUT_BOUNDARY_INSTRUCTIONS : CHILD_SUBAGENT_BOUNDARY_INSTRUCTIONS;
44
- const text = prompt.trim();
45
- return text ? `${boundary}\n\n${text}` : boundary;
46
- }
47
-
48
- // ============================================================================
49
- // Depth guard
50
- // ============================================================================
51
-
52
- export const MAX_SUBAGENT_DEPTH_ENV = "UNIPI_SUBAGENT_MAX_DEPTH";
53
- export const CURRENT_SUBAGENT_DEPTH_ENV = "UNIPI_SUBAGENT_DEPTH";
54
-
55
- /**
56
- * Resolve the effective depth cap. An inherited stricter cap cannot be
57
- * relaxed; per-agent caps can only tighten. Reference semantics.
58
- */
59
- export function resolveMaxSubagentDepth(
60
- agent: AgentConfig | undefined,
61
- config: SubagentsConfig | undefined,
62
- env: NodeJS.ProcessEnv = process.env,
63
- ): number {
64
- const inheritedRaw = env[MAX_SUBAGENT_DEPTH_ENV];
65
- const inherited = inheritedRaw !== undefined && inheritedRaw.trim() !== "" ? Number(inheritedRaw) : undefined;
66
- if (inherited !== undefined && Number.isInteger(inherited) && inherited >= 0) {
67
- const agentCap = agent?.maxSubagentDepth;
68
- if (agentCap !== undefined && Number.isInteger(agentCap) && agentCap >= 0 && agentCap < inherited) {
69
- return agentCap;
70
- }
71
- return inherited;
72
- }
73
- const configured = config?.maxSubagentDepth;
74
- if (configured !== undefined && Number.isInteger(configured) && configured > 0) return configured;
75
- return 2; // reference default
76
- }
77
-
78
- /**
79
- * Environment for a child process/session: depth counters increment so a
80
- * child's own spawns hit the cap.
81
- */
82
- export function childDepthEnv(env: NodeJS.ProcessEnv, maxDepth: number): Record<string, string> {
83
- const currentRaw = env[CURRENT_SUBAGENT_DEPTH_ENV];
84
- const current = currentRaw !== undefined && currentRaw.trim() !== "" ? Number(currentRaw) : 0;
85
- return {
86
- [MAX_SUBAGENT_DEPTH_ENV]: String(maxDepth),
87
- [CURRENT_SUBAGENT_DEPTH_ENV]: String(Number.isInteger(current) && current >= 0 ? current + 1 : 1),
88
- };
89
- }
90
-
91
- /** Whether spawning is blocked by the depth guard for this environment. */
92
- export function depthExceeded(env: NodeJS.ProcessEnv, maxDepth: number): boolean {
93
- const currentRaw = env[CURRENT_SUBAGENT_DEPTH_ENV];
94
- if (currentRaw === undefined || currentRaw.trim() === "") return false;
95
- const current = Number(currentRaw);
96
- return Number.isInteger(current) && current >= maxDepth;
97
- }
98
-
99
- // ============================================================================
100
- // Context resolution (in-process foreground)
101
- // ============================================================================
102
-
103
- export class ContextUnavailableError extends Error {
104
- constructor(message: string) {
105
- super(message);
106
- this.name = "ContextUnavailableError";
107
- }
108
- }
109
-
110
- /**
111
- * Resolve launch context: explicit call value > config defaultSubagentContext >
112
- * agent defaultContext > "fresh". Explicit fork on the IN-PROCESS path fails
113
- * fast with a pointer to the async path (reference behavior: explicit fork
114
- * never silently downgrades).
115
- */
116
- export function resolveContext(
117
- callContext: string | undefined,
118
- agent: AgentConfig | undefined,
119
- config: SubagentsConfig | undefined,
120
- ): ContextMode {
121
- if (callContext === "fresh" || callContext === "fork") return callContext;
122
- if (callContext === "profile") {
123
- return agent?.defaultContext ?? "fresh";
124
- }
125
- const configDefault = config?.defaultSubagentContext;
126
- if (configDefault === "fresh" || configDefault === "fork") return configDefault;
127
- return agent?.defaultContext ?? "fresh";
128
- }
129
-
130
- /**
131
- * In-process foreground runs are always fresh-context. An explicit fork
132
- * request rejects with guidance (Phase 3's process runner provides fork).
133
- */
134
- export function assertForegroundContextSupported(mode: ContextMode, async: boolean): void {
135
- if (mode === "fork" && !async) {
136
- throw new ContextUnavailableError(
137
- 'context: "fork" requires background execution (run_in_background: true); the in-process foreground path is fresh-context only. Re-run with run_in_background, or use context: "fresh".',
138
- );
139
- }
140
- }
@@ -1,249 +0,0 @@
1
- /**
2
- * @pi-unipi/subagents — Config management
3
- *
4
- * Loads config from ~/.unipi/config/subagents.json (global)
5
- * and <workspace>/.unipi/config/subagents.json (override).
6
- * Auto-generates on first run. Repairs corrupted files.
7
- */
8
-
9
- import { existsSync, mkdirSync, readFileSync, renameSync, writeFileSync } from "node:fs";
10
- import { join } from "node:path";
11
- import { homedir } from "node:os";
12
- import {
13
- getSettings,
14
- globalSettingsPath,
15
- projectSettingsPath,
16
- registerSettings,
17
- setSettings,
18
- settingsLayers,
19
- tryRead,
20
- } from "@pi-unipi/core";
21
- import type { SubagentsConfig } from "./types.js";
22
-
23
- const DEFAULT_CONFIG: SubagentsConfig = {
24
- maxConcurrent: 4,
25
- enabled: true,
26
- types: {
27
- explore: { enabled: true },
28
- work: { enabled: true },
29
- },
30
- };
31
-
32
- /** Get global config path: ~/.unipi/config/subagents.json */
33
- function getGlobalConfigPath(): string {
34
- return join(homedir(), ".unipi", "config", "subagents.json");
35
- }
36
-
37
- /** Get workspace config path: <cwd>/.unipi/config/subagents.json */
38
- function getWorkspaceConfigPath(cwd: string): string {
39
- return join(cwd, ".unipi", "config", "subagents.json");
40
- }
41
-
42
- /** Ensure directory exists. */
43
- function ensureDir(filePath: string): void {
44
- const dir = filePath.substring(0, filePath.lastIndexOf("/"));
45
- if (!existsSync(dir)) {
46
- mkdirSync(dir, { recursive: true });
47
- }
48
- }
49
-
50
- /** Ensure a directory exists (not a file path). */
51
- function ensureDirExists(dirPath: string): void {
52
- if (!existsSync(dirPath)) {
53
- mkdirSync(dirPath, { recursive: true });
54
- }
55
- }
56
-
57
- /** Write config atomically (write then rename). */
58
- function writeConfigAtomic(filePath: string, config: SubagentsConfig): void {
59
- const tmpPath = filePath + ".tmp";
60
- writeFileSync(tmpPath, JSON.stringify(config, null, 2), "utf-8");
61
- renameSync(tmpPath, filePath);
62
- }
63
-
64
- /** Load and parse config from a path. Returns null on failure. */
65
- function loadConfigFromPath(filePath: string): SubagentsConfig | null {
66
- if (!existsSync(filePath)) return null;
67
-
68
- try {
69
- const content = readFileSync(filePath, "utf-8");
70
- const parsed = JSON.parse(content);
71
- // Basic validation
72
- if (typeof parsed !== "object" || parsed === null) return null;
73
- return parsed as SubagentsConfig;
74
- } catch {
75
- return null;
76
- }
77
- }
78
-
79
- /**
80
- * Validate parity keys. Follows the reference behavior: invalid values for
81
- * strict keys (toolTimeoutMs, budgets, concurrency caps) are rejected with a
82
- * visible error rather than silently ignored. Best-effort keys (placement,
83
- * logging mode) fall back to defaults like the reference does.
84
- *
85
- * Returns a list of validation problems; empty means valid.
86
- */
87
- export function validateParityConfig(config: SubagentsConfig): string[] {
88
- const problems: string[] = [];
89
- const positiveInt = (label: string, value: unknown): void => {
90
- if (value === undefined) return;
91
- if (typeof value !== "number" || !Number.isInteger(value) || value <= 0 || value > 2147483647) {
92
- problems.push(`${label} must be a positive integer no greater than 2147483647 (got ${JSON.stringify(value)})`);
93
- }
94
- };
95
-
96
- positiveInt("timeoutMs", config.timeoutMs);
97
- positiveInt("toolTimeoutMs", config.toolTimeoutMs);
98
- positiveInt("globalConcurrencyLimit", config.globalConcurrencyLimit);
99
- if (config.maxSubagentSpawnsPerSession !== undefined && config.maxSubagentSpawnsPerSession !== 0) {
100
- positiveInt("maxSubagentSpawnsPerSession", config.maxSubagentSpawnsPerSession);
101
- }
102
- if (config.maxSubagentSpawnsPerRun !== undefined && config.maxSubagentSpawnsPerRun !== 0) {
103
- positiveInt("maxSubagentSpawnsPerRun", config.maxSubagentSpawnsPerRun);
104
- }
105
- if (config.maxActiveAsyncRunsPerSession !== undefined && config.maxActiveAsyncRunsPerSession !== 0) {
106
- positiveInt("maxActiveAsyncRunsPerSession", config.maxActiveAsyncRunsPerSession);
107
- }
108
- if (config.maxSubagentDepth !== undefined) positiveInt("maxSubagentDepth", config.maxSubagentDepth);
109
-
110
- if (config.defaultSubagentContext !== undefined && !['"fresh"', '"fork"'].includes(JSON.stringify(config.defaultSubagentContext))) {
111
- problems.push(`defaultSubagentContext must be "fresh" or "fork" (got ${JSON.stringify(config.defaultSubagentContext)})`);
112
- }
113
- if (config.fleetViewPlacement !== undefined && !['"belowEditor"', '"aboveEditor"'].includes(JSON.stringify(config.fleetViewPlacement))) {
114
- problems.push(`fleetViewPlacement must be "belowEditor" or "aboveEditor" (got ${JSON.stringify(config.fleetViewPlacement)}); falling back to "belowEditor"`);
115
- }
116
- if (
117
- config.resultScanLogging !== undefined &&
118
- !["all", "activity", "off"].includes(config.resultScanLogging)
119
- ) {
120
- problems.push(`resultScanLogging must be "all", "activity", or "off" (got ${JSON.stringify(config.resultScanLogging)})`);
121
- }
122
- if (config.inlineToolDisplay !== undefined && !["rich", "summary"].includes(config.inlineToolDisplay)) {
123
- problems.push(`inlineToolDisplay must be "rich" or "summary" (got ${JSON.stringify(config.inlineToolDisplay)})`);
124
- }
125
- if (config.parallel) {
126
- positiveInt("parallel.maxTasks", config.parallel.maxTasks);
127
- positiveInt("parallel.concurrency", config.parallel.concurrency);
128
- }
129
- if (config.maxOutput) {
130
- positiveInt("maxOutput.bytes", config.maxOutput.bytes);
131
- positiveInt("maxOutput.lines", config.maxOutput.lines);
132
- }
133
-
134
- return problems;
135
- }
136
-
137
- /** Repair corrupted config: rename to .bak and generate fresh. */
138
- function repairCorrupted(filePath: string): SubagentsConfig {
139
- const backupPath = filePath + ".bak";
140
- try {
141
- renameSync(filePath, backupPath);
142
- } catch {
143
- // If rename fails, just overwrite
144
- }
145
- writeConfigAtomic(filePath, DEFAULT_CONFIG);
146
- return DEFAULT_CONFIG;
147
- }
148
-
149
- /**
150
- * Initialize config on extension start.
151
- * - If missing: generate with defaults
152
- * - If corrupted: rename to .bak, generate fresh
153
- * - If valid: load
154
- */
155
- // Registered with the unified settings hub — the engine's global ⊕ project
156
- // layering replaces the manual merge. Legacy flat JSONs
157
- // (~/.unipi/config/subagents.json + <cwd>/.unipi/config/subagents.json) are
158
- // imported once into the engine layout on first init.
159
- registerSettings({
160
- namespace: "subagents",
161
- label: "Subagents",
162
- defaults: DEFAULT_CONFIG as unknown as Record<string, unknown>,
163
- schema: [
164
- {
165
- title: "Fleet",
166
- fields: [
167
- { key: "enabled", type: "boolean", label: "Enabled" },
168
- { key: "maxConcurrent", type: "number", label: "Max concurrent", min: 1, max: 32 },
169
- { key: "types.explore.enabled", type: "boolean", label: "Explore type" },
170
- { key: "types.work.enabled", type: "boolean", label: "Work type" },
171
- ],
172
- },
173
- ],
174
- });
175
-
176
- /** One-time import of the legacy flat JSONs into the engine layout. */
177
- function importLegacySubagentsConfig(cwd: string): void {
178
- const layers = settingsLayers("subagents", cwd);
179
- if (!layers.global) {
180
- const raw = loadConfigFromPath(getGlobalConfigPath());
181
- if (raw) setSettings("subagents", raw as unknown as Record<string, unknown>, "global", cwd);
182
- }
183
- if (!layers.project) {
184
- const raw = loadConfigFromPath(getWorkspaceConfigPath(cwd));
185
- if (raw) setSettings("subagents", raw as unknown as Record<string, unknown>, "project", cwd);
186
- }
187
- }
188
-
189
- export function initConfig(cwd: string): SubagentsConfig {
190
- const globalAgentsDir = join(homedir(), ".unipi", "config", "agents");
191
- ensureDirExists(globalAgentsDir);
192
- if (cwd && !cwd.startsWith(homedir())) {
193
- ensureDirExists(join(cwd, ".unipi", "config", "agents"));
194
- }
195
-
196
- try {
197
- importLegacySubagentsConfig(cwd);
198
- const merged = getSettings("subagents", cwd) as unknown as SubagentsConfig;
199
- reportConfigProblems(merged);
200
- return merged;
201
- } catch {
202
- reportConfigProblems(DEFAULT_CONFIG);
203
- return { ...DEFAULT_CONFIG };
204
- }
205
- }
206
-
207
- /** Surface config validation problems visibly (non-fatal, reference behavior). */
208
- function reportConfigProblems(config: SubagentsConfig): void {
209
- const problems = validateParityConfig(config);
210
- for (const problem of problems) {
211
- console.error(`[unipi/subagents] config: ${problem}`);
212
- }
213
- }
214
-
215
- /** Load the RAW parsed global config (for parity settings extraction). */
216
- export function loadRawGlobalConfig(): Record<string, unknown> | null {
217
- const raw = tryRead(globalSettingsPath("subagents"));
218
- if (!raw) return null;
219
- try {
220
- return JSON.parse(raw) as Record<string, unknown>;
221
- } catch {
222
- return null;
223
- }
224
- }
225
-
226
- /** Load the RAW parsed workspace config (for parity settings extraction). */
227
- export function loadRawWorkspaceConfig(cwd: string): Record<string, unknown> | null {
228
- const raw = tryRead(projectSettingsPath(cwd, "subagents"));
229
- if (!raw) return null;
230
- try {
231
- return JSON.parse(raw) as Record<string, unknown>;
232
- } catch {
233
- return null;
234
- }
235
- }
236
-
237
- /**
238
- * Save global config.
239
- */
240
- export function saveGlobalConfig(config: SubagentsConfig): void {
241
- setSettings("subagents", config as unknown as Record<string, unknown>, "global", process.cwd());
242
- }
243
-
244
- /**
245
- * Save workspace config.
246
- */
247
- export function saveWorkspaceConfig(cwd: string, config: SubagentsConfig): void {
248
- setSettings("subagents", config as unknown as Record<string, unknown>, "project", cwd);
249
- }