open-multi-agent-kit 0.96.0 → 0.96.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (351) hide show
  1. package/CHANGELOG.md +81 -0
  2. package/README.md +34 -6
  3. package/dist/cli/args.d.ts.map +1 -1
  4. package/dist/cli/args.js +1 -0
  5. package/dist/cli/args.js.map +1 -1
  6. package/dist/commands/doctor-provider.d.ts.map +1 -1
  7. package/dist/commands/doctor-provider.js +15 -20
  8. package/dist/commands/doctor-provider.js.map +1 -1
  9. package/dist/commands/resource-doctor-cli.d.ts +22 -0
  10. package/dist/commands/resource-doctor-cli.d.ts.map +1 -0
  11. package/dist/commands/resource-doctor-cli.js +98 -0
  12. package/dist/commands/resource-doctor-cli.js.map +1 -0
  13. package/dist/commands/sdk-session-cli.d.ts +24 -0
  14. package/dist/commands/sdk-session-cli.d.ts.map +1 -0
  15. package/dist/commands/sdk-session-cli.js +225 -0
  16. package/dist/commands/sdk-session-cli.js.map +1 -0
  17. package/dist/core/advisory-judge-model.d.ts +18 -0
  18. package/dist/core/advisory-judge-model.d.ts.map +1 -0
  19. package/dist/core/advisory-judge-model.js +168 -0
  20. package/dist/core/advisory-judge-model.js.map +1 -0
  21. package/dist/core/advisory-judge-response.d.ts +8 -0
  22. package/dist/core/advisory-judge-response.d.ts.map +1 -0
  23. package/dist/core/advisory-judge-response.js +61 -0
  24. package/dist/core/advisory-judge-response.js.map +1 -0
  25. package/dist/core/advisory-judge-types.d.ts +53 -0
  26. package/dist/core/advisory-judge-types.d.ts.map +1 -0
  27. package/dist/core/advisory-judge-types.js +2 -0
  28. package/dist/core/advisory-judge-types.js.map +1 -0
  29. package/dist/core/advisory-judge.d.ts +8 -0
  30. package/dist/core/advisory-judge.d.ts.map +1 -0
  31. package/dist/core/advisory-judge.js +178 -0
  32. package/dist/core/advisory-judge.js.map +1 -0
  33. package/dist/core/agent-session-services.d.ts +1 -0
  34. package/dist/core/agent-session-services.d.ts.map +1 -1
  35. package/dist/core/agent-session-services.js +1 -0
  36. package/dist/core/agent-session-services.js.map +1 -1
  37. package/dist/core/agent-session.d.ts +41 -2
  38. package/dist/core/agent-session.d.ts.map +1 -1
  39. package/dist/core/agent-session.js +396 -179
  40. package/dist/core/agent-session.js.map +1 -1
  41. package/dist/core/auth-storage.d.ts.map +1 -1
  42. package/dist/core/auth-storage.js +28 -4
  43. package/dist/core/auth-storage.js.map +1 -1
  44. package/dist/core/compaction-gate.d.ts +28 -0
  45. package/dist/core/compaction-gate.d.ts.map +1 -0
  46. package/dist/core/compaction-gate.js +34 -0
  47. package/dist/core/compaction-gate.js.map +1 -0
  48. package/dist/core/completion-sound.d.ts +109 -0
  49. package/dist/core/completion-sound.d.ts.map +1 -0
  50. package/dist/core/completion-sound.js +165 -0
  51. package/dist/core/completion-sound.js.map +1 -0
  52. package/dist/core/context-budget-system-prompt-items.d.ts.map +1 -1
  53. package/dist/core/context-budget-system-prompt-items.js +5 -3
  54. package/dist/core/context-budget-system-prompt-items.js.map +1 -1
  55. package/dist/core/domain-loadouts.d.ts.map +1 -1
  56. package/dist/core/domain-loadouts.js +2 -2
  57. package/dist/core/domain-loadouts.js.map +1 -1
  58. package/dist/core/durable-goal-checkpoint.d.ts +6 -0
  59. package/dist/core/durable-goal-checkpoint.d.ts.map +1 -0
  60. package/dist/core/durable-goal-checkpoint.js +195 -0
  61. package/dist/core/durable-goal-checkpoint.js.map +1 -0
  62. package/dist/core/durable-goal-parse.d.ts +3 -0
  63. package/dist/core/durable-goal-parse.d.ts.map +1 -0
  64. package/dist/core/durable-goal-parse.js +86 -0
  65. package/dist/core/durable-goal-parse.js.map +1 -0
  66. package/dist/core/durable-goal-reducer.d.ts +3 -0
  67. package/dist/core/durable-goal-reducer.d.ts.map +1 -0
  68. package/dist/core/durable-goal-reducer.js +175 -0
  69. package/dist/core/durable-goal-reducer.js.map +1 -0
  70. package/dist/core/durable-goal-store.d.ts +19 -0
  71. package/dist/core/durable-goal-store.d.ts.map +1 -0
  72. package/dist/core/durable-goal-store.js +126 -0
  73. package/dist/core/durable-goal-store.js.map +1 -0
  74. package/dist/core/durable-goal.d.ts +92 -0
  75. package/dist/core/durable-goal.d.ts.map +1 -0
  76. package/dist/core/durable-goal.js +62 -0
  77. package/dist/core/durable-goal.js.map +1 -0
  78. package/dist/core/extensions/builtin/goal-controller.d.ts +3 -0
  79. package/dist/core/extensions/builtin/goal-controller.d.ts.map +1 -0
  80. package/dist/core/extensions/builtin/goal-controller.js +125 -0
  81. package/dist/core/extensions/builtin/goal-controller.js.map +1 -0
  82. package/dist/core/extensions/builtin/identical-loop.d.ts +3 -0
  83. package/dist/core/extensions/builtin/identical-loop.d.ts.map +1 -0
  84. package/dist/core/extensions/builtin/identical-loop.js +31 -0
  85. package/dist/core/extensions/builtin/identical-loop.js.map +1 -0
  86. package/dist/core/extensions/builtin/prompt-preset.d.ts +3 -0
  87. package/dist/core/extensions/builtin/prompt-preset.d.ts.map +1 -0
  88. package/dist/core/extensions/builtin/prompt-preset.js +13 -0
  89. package/dist/core/extensions/builtin/prompt-preset.js.map +1 -0
  90. package/dist/core/extensions/builtin/tool-pair-repair.d.ts +3 -0
  91. package/dist/core/extensions/builtin/tool-pair-repair.d.ts.map +1 -0
  92. package/dist/core/extensions/builtin/tool-pair-repair.js +12 -0
  93. package/dist/core/extensions/builtin/tool-pair-repair.js.map +1 -0
  94. package/dist/core/extensions/types.d.ts +6 -2
  95. package/dist/core/extensions/types.d.ts.map +1 -1
  96. package/dist/core/extensions/types.js.map +1 -1
  97. package/dist/core/goal-continuation.d.ts +13 -0
  98. package/dist/core/goal-continuation.d.ts.map +1 -0
  99. package/dist/core/goal-continuation.js +10 -0
  100. package/dist/core/goal-continuation.js.map +1 -0
  101. package/dist/core/grok-harness.d.ts +1 -1
  102. package/dist/core/grok-harness.d.ts.map +1 -1
  103. package/dist/core/grok-harness.js +1 -1
  104. package/dist/core/grok-harness.js.map +1 -1
  105. package/dist/core/grok-playbook.d.ts +1 -1
  106. package/dist/core/grok-playbook.d.ts.map +1 -1
  107. package/dist/core/grok-playbook.js +1 -1
  108. package/dist/core/grok-playbook.js.map +1 -1
  109. package/dist/core/host-resource-probe.d.ts +63 -0
  110. package/dist/core/host-resource-probe.d.ts.map +1 -0
  111. package/dist/core/host-resource-probe.js +126 -0
  112. package/dist/core/host-resource-probe.js.map +1 -0
  113. package/dist/core/host-resource-snapshot.d.ts +54 -0
  114. package/dist/core/host-resource-snapshot.d.ts.map +1 -0
  115. package/dist/core/host-resource-snapshot.js +139 -0
  116. package/dist/core/host-resource-snapshot.js.map +1 -0
  117. package/dist/core/identical-loop.d.ts +15 -0
  118. package/dist/core/identical-loop.d.ts.map +1 -0
  119. package/dist/core/identical-loop.js +34 -0
  120. package/dist/core/identical-loop.js.map +1 -0
  121. package/dist/core/index.d.ts +1 -1
  122. package/dist/core/index.d.ts.map +1 -1
  123. package/dist/core/index.js +1 -1
  124. package/dist/core/index.js.map +1 -1
  125. package/dist/core/model-prompt-policy.d.ts +11 -0
  126. package/dist/core/model-prompt-policy.d.ts.map +1 -0
  127. package/dist/core/model-prompt-policy.js +17 -0
  128. package/dist/core/model-prompt-policy.js.map +1 -0
  129. package/dist/core/model-registry.d.ts.map +1 -1
  130. package/dist/core/model-registry.js +16 -4
  131. package/dist/core/model-registry.js.map +1 -1
  132. package/dist/core/model-resolver.d.ts.map +1 -1
  133. package/dist/core/model-resolver.js +2 -2
  134. package/dist/core/model-resolver.js.map +1 -1
  135. package/dist/core/prompt-budget.d.ts +21 -0
  136. package/dist/core/prompt-budget.d.ts.map +1 -0
  137. package/dist/core/prompt-budget.js +51 -0
  138. package/dist/core/prompt-budget.js.map +1 -0
  139. package/dist/core/prompt-cache.d.ts +14 -0
  140. package/dist/core/prompt-cache.d.ts.map +1 -0
  141. package/dist/core/prompt-cache.js +16 -0
  142. package/dist/core/prompt-cache.js.map +1 -0
  143. package/dist/core/prompt-preset.d.ts +8 -0
  144. package/dist/core/prompt-preset.d.ts.map +1 -0
  145. package/dist/core/prompt-preset.js +56 -0
  146. package/dist/core/prompt-preset.js.map +1 -0
  147. package/dist/core/prompt-settlement.d.ts +81 -0
  148. package/dist/core/prompt-settlement.d.ts.map +1 -0
  149. package/dist/core/prompt-settlement.js +84 -0
  150. package/dist/core/prompt-settlement.js.map +1 -0
  151. package/dist/core/provider-resilience.d.ts +16 -0
  152. package/dist/core/provider-resilience.d.ts.map +1 -1
  153. package/dist/core/provider-resilience.js +24 -2
  154. package/dist/core/provider-resilience.js.map +1 -1
  155. package/dist/core/provider-retry.d.ts +36 -0
  156. package/dist/core/provider-retry.d.ts.map +1 -0
  157. package/dist/core/provider-retry.js +73 -0
  158. package/dist/core/provider-retry.js.map +1 -0
  159. package/dist/core/provider-usage.d.ts +16 -2
  160. package/dist/core/provider-usage.d.ts.map +1 -1
  161. package/dist/core/provider-usage.js +150 -8
  162. package/dist/core/provider-usage.js.map +1 -1
  163. package/dist/core/reasoning-router-resolver.d.ts.map +1 -1
  164. package/dist/core/reasoning-router-resolver.js +1 -1
  165. package/dist/core/reasoning-router-resolver.js.map +1 -1
  166. package/dist/core/resource-admission-config.d.ts +55 -0
  167. package/dist/core/resource-admission-config.d.ts.map +1 -0
  168. package/dist/core/resource-admission-config.js +97 -0
  169. package/dist/core/resource-admission-config.js.map +1 -0
  170. package/dist/core/resource-admission.d.ts +81 -0
  171. package/dist/core/resource-admission.d.ts.map +1 -0
  172. package/dist/core/resource-admission.js +128 -0
  173. package/dist/core/resource-admission.js.map +1 -0
  174. package/dist/core/resource-governor-format.d.ts +50 -0
  175. package/dist/core/resource-governor-format.d.ts.map +1 -0
  176. package/dist/core/resource-governor-format.js +113 -0
  177. package/dist/core/resource-governor-format.js.map +1 -0
  178. package/dist/core/resource-governor-settings.d.ts +56 -0
  179. package/dist/core/resource-governor-settings.d.ts.map +1 -0
  180. package/dist/core/resource-governor-settings.js +89 -0
  181. package/dist/core/resource-governor-settings.js.map +1 -0
  182. package/dist/core/resource-loader.d.ts.map +1 -1
  183. package/dist/core/resource-loader.js +32 -0
  184. package/dist/core/resource-loader.js.map +1 -1
  185. package/dist/core/resource-observation-journal.d.ts +74 -0
  186. package/dist/core/resource-observation-journal.d.ts.map +1 -0
  187. package/dist/core/resource-observation-journal.js +198 -0
  188. package/dist/core/resource-observation-journal.js.map +1 -0
  189. package/dist/core/resource-safety-gate.d.ts +54 -0
  190. package/dist/core/resource-safety-gate.d.ts.map +1 -0
  191. package/dist/core/resource-safety-gate.js +51 -0
  192. package/dist/core/resource-safety-gate.js.map +1 -0
  193. package/dist/core/run-journal.d.ts.map +1 -1
  194. package/dist/core/run-journal.js +13 -1
  195. package/dist/core/run-journal.js.map +1 -1
  196. package/dist/core/run-resource-lease.d.ts +73 -0
  197. package/dist/core/run-resource-lease.d.ts.map +1 -0
  198. package/dist/core/run-resource-lease.js +78 -0
  199. package/dist/core/run-resource-lease.js.map +1 -0
  200. package/dist/core/sandbox/backend.d.ts +12 -0
  201. package/dist/core/sandbox/backend.d.ts.map +1 -1
  202. package/dist/core/sandbox/backend.js +52 -42
  203. package/dist/core/sandbox/backend.js.map +1 -1
  204. package/dist/core/sandbox/policy.d.ts +2 -0
  205. package/dist/core/sandbox/policy.d.ts.map +1 -1
  206. package/dist/core/sandbox/policy.js +2 -1
  207. package/dist/core/sandbox/policy.js.map +1 -1
  208. package/dist/core/sdk.d.ts +2 -0
  209. package/dist/core/sdk.d.ts.map +1 -1
  210. package/dist/core/sdk.js +1 -0
  211. package/dist/core/sdk.js.map +1 -1
  212. package/dist/core/session-bash-service.d.ts +12 -0
  213. package/dist/core/session-bash-service.d.ts.map +1 -1
  214. package/dist/core/session-bash-service.js +14 -0
  215. package/dist/core/session-bash-service.js.map +1 -1
  216. package/dist/core/session-system-prompt.d.ts +30 -0
  217. package/dist/core/session-system-prompt.d.ts.map +1 -0
  218. package/dist/core/session-system-prompt.js +39 -0
  219. package/dist/core/session-system-prompt.js.map +1 -0
  220. package/dist/core/session-termination.d.ts +7 -2
  221. package/dist/core/session-termination.d.ts.map +1 -1
  222. package/dist/core/session-termination.js +29 -1
  223. package/dist/core/session-termination.js.map +1 -1
  224. package/dist/core/settings-manager.d.ts +12 -0
  225. package/dist/core/settings-manager.d.ts.map +1 -1
  226. package/dist/core/settings-manager.js +36 -0
  227. package/dist/core/settings-manager.js.map +1 -1
  228. package/dist/core/slash-commands.d.ts.map +1 -1
  229. package/dist/core/slash-commands.js +1 -0
  230. package/dist/core/slash-commands.js.map +1 -1
  231. package/dist/core/strict-record.d.ts +2 -0
  232. package/dist/core/strict-record.d.ts.map +1 -0
  233. package/dist/core/strict-record.js +9 -0
  234. package/dist/core/strict-record.js.map +1 -0
  235. package/dist/core/subagent-lane-launcher.d.ts +64 -0
  236. package/dist/core/subagent-lane-launcher.d.ts.map +1 -0
  237. package/dist/core/subagent-lane-launcher.js +107 -0
  238. package/dist/core/subagent-lane-launcher.js.map +1 -0
  239. package/dist/core/system-cpu-sampler.d.ts +45 -0
  240. package/dist/core/system-cpu-sampler.d.ts.map +1 -0
  241. package/dist/core/system-cpu-sampler.js +95 -0
  242. package/dist/core/system-cpu-sampler.js.map +1 -0
  243. package/dist/core/system-prompt.d.ts.map +1 -1
  244. package/dist/core/system-prompt.js +5 -8
  245. package/dist/core/system-prompt.js.map +1 -1
  246. package/dist/core/tool-pair-repair.d.ts +11 -0
  247. package/dist/core/tool-pair-repair.d.ts.map +1 -0
  248. package/dist/core/tool-pair-repair.js +38 -0
  249. package/dist/core/tool-pair-repair.js.map +1 -0
  250. package/dist/core/tools/artifact-spill.d.ts +13 -0
  251. package/dist/core/tools/artifact-spill.d.ts.map +1 -0
  252. package/dist/core/tools/artifact-spill.js +26 -0
  253. package/dist/core/tools/artifact-spill.js.map +1 -0
  254. package/dist/core/tools/bash.d.ts +7 -4
  255. package/dist/core/tools/bash.d.ts.map +1 -1
  256. package/dist/core/tools/bash.js +8 -7
  257. package/dist/core/tools/bash.js.map +1 -1
  258. package/dist/core/tools/index.d.ts +1 -1
  259. package/dist/core/tools/index.d.ts.map +1 -1
  260. package/dist/core/tools/index.js.map +1 -1
  261. package/dist/core/tools/read.d.ts +1 -0
  262. package/dist/core/tools/read.d.ts.map +1 -1
  263. package/dist/core/tools/read.js +10 -1
  264. package/dist/core/tools/read.js.map +1 -1
  265. package/dist/core/tools/tool-definition-wrapper.d.ts.map +1 -1
  266. package/dist/core/tools/tool-definition-wrapper.js +2 -0
  267. package/dist/core/tools/tool-definition-wrapper.js.map +1 -1
  268. package/dist/core/workload-classifier.d.ts +32 -0
  269. package/dist/core/workload-classifier.d.ts.map +1 -0
  270. package/dist/core/workload-classifier.js +66 -0
  271. package/dist/core/workload-classifier.js.map +1 -0
  272. package/dist/core/workload-family-matcher.d.ts +3 -0
  273. package/dist/core/workload-family-matcher.d.ts.map +1 -0
  274. package/dist/core/workload-family-matcher.js +126 -0
  275. package/dist/core/workload-family-matcher.js.map +1 -0
  276. package/dist/core/workload-permit-pool.d.ts +78 -0
  277. package/dist/core/workload-permit-pool.d.ts.map +1 -0
  278. package/dist/core/workload-permit-pool.js +138 -0
  279. package/dist/core/workload-permit-pool.js.map +1 -0
  280. package/dist/core/workload-shard-executor.d.ts +68 -0
  281. package/dist/core/workload-shard-executor.d.ts.map +1 -0
  282. package/dist/core/workload-shard-executor.js +156 -0
  283. package/dist/core/workload-shard-executor.js.map +1 -0
  284. package/dist/core/workload-shard-plan.d.ts +81 -0
  285. package/dist/core/workload-shard-plan.d.ts.map +1 -0
  286. package/dist/core/workload-shard-plan.js +183 -0
  287. package/dist/core/workload-shard-plan.js.map +1 -0
  288. package/dist/core/workload-shard-runner.d.ts +11 -0
  289. package/dist/core/workload-shard-runner.d.ts.map +1 -0
  290. package/dist/core/workload-shard-runner.js +98 -0
  291. package/dist/core/workload-shard-runner.js.map +1 -0
  292. package/dist/core/workload-shard-store.d.ts +44 -0
  293. package/dist/core/workload-shard-store.d.ts.map +1 -0
  294. package/dist/core/workload-shard-store.js +189 -0
  295. package/dist/core/workload-shard-store.js.map +1 -0
  296. package/dist/core/workload-sharders.d.ts +47 -0
  297. package/dist/core/workload-sharders.d.ts.map +1 -0
  298. package/dist/core/workload-sharders.js +162 -0
  299. package/dist/core/workload-sharders.js.map +1 -0
  300. package/dist/core/workload-shell-scan.d.ts +12 -0
  301. package/dist/core/workload-shell-scan.d.ts.map +1 -0
  302. package/dist/core/workload-shell-scan.js +79 -0
  303. package/dist/core/workload-shell-scan.js.map +1 -0
  304. package/dist/index.d.ts +18 -1
  305. package/dist/index.d.ts.map +1 -1
  306. package/dist/index.js +18 -0
  307. package/dist/index.js.map +1 -1
  308. package/dist/main.d.ts.map +1 -1
  309. package/dist/main.js +14 -0
  310. package/dist/main.js.map +1 -1
  311. package/dist/modes/interactive/interactive-mode.d.ts +4 -0
  312. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  313. package/dist/modes/interactive/interactive-mode.js +81 -0
  314. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  315. package/dist/modes/interactive/theme/theme.d.ts.map +1 -1
  316. package/dist/modes/interactive/theme/theme.js +14 -3
  317. package/dist/modes/interactive/theme/theme.js.map +1 -1
  318. package/docs/adaptorch-preview-spec.md +1 -1
  319. package/docs/adaptorch-preview.md +2 -2
  320. package/docs/book-to-skill.md +1 -1
  321. package/docs/containerization.md +9 -0
  322. package/docs/docs.json +11 -3
  323. package/docs/environment-variables.md +11 -4
  324. package/docs/extensions.md +14 -1
  325. package/docs/grok-harness.md +35 -12
  326. package/docs/index.md +7 -6
  327. package/docs/loadout-domains/grok-harness.md +2 -2
  328. package/docs/models.md +10 -9
  329. package/docs/provider-resilience.md +9 -3
  330. package/docs/providers.md +9 -4
  331. package/docs/run-protocol.md +32 -0
  332. package/docs/sdk.md +100 -3
  333. package/docs/security.md +22 -32
  334. package/docs/sessions.md +14 -0
  335. package/docs/settings.md +64 -2
  336. package/docs/skills.md +16 -0
  337. package/docs/usage.md +37 -15
  338. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  339. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  340. package/examples/extensions/custom-provider-gitlab-duo/index.ts +6 -1
  341. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  342. package/examples/extensions/gondolin/package-lock.json +2 -2
  343. package/examples/extensions/gondolin/package.json +1 -1
  344. package/examples/extensions/sandbox/package-lock.json +2 -2
  345. package/examples/extensions/sandbox/package.json +1 -1
  346. package/examples/extensions/subagent/agent-capability-router.test.ts +7 -0
  347. package/examples/extensions/subagent/domain-profiles.ts +0 -1
  348. package/examples/extensions/with-deps/package-lock.json +2 -2
  349. package/examples/extensions/with-deps/package.json +1 -1
  350. package/npm-shrinkwrap.json +18 -18
  351. package/package.json +7 -9
@@ -0,0 +1,109 @@
1
+ import type { PromptSettledEvent } from "./prompt-settlement.ts";
2
+ /**
3
+ * Cross-platform completion sound service (OMK v0.97.x roadmap §17, M4/PR7).
4
+ *
5
+ * Consumes `prompt_settled` only (§16.1: never `agent_end`) and is a pure UX
6
+ * signal — it must never influence run outcome, block the event loop, or
7
+ * delay process exit (§17.4: spawn with `stdio: "ignore"`, `shell: false`,
8
+ * `unref()`, bounded kill timer, never awaited by the prompt path).
9
+ *
10
+ * Security (§22.1): every backend uses a fixed executable and fixed argv;
11
+ * no user text, paths, or model output is ever interpolated.
12
+ *
13
+ * Backend selection is a deterministic candidate chain per platform
14
+ * (§17.3); a spawn failure falls through to the next candidate and finally
15
+ * to the terminal BEL. All failures are diagnostics only (§4.6).
16
+ */
17
+ export interface CompletionSoundSettings {
18
+ enabled?: boolean;
19
+ minDurationMs?: number;
20
+ onSuccess?: boolean;
21
+ onFailure?: boolean;
22
+ terminalBellFallback?: boolean;
23
+ }
24
+ export interface NotificationSettings {
25
+ completionSound?: CompletionSoundSettings;
26
+ }
27
+ export type CompletionSoundBackend = "macos-afplay" | "windows-system-sound" | "linux-canberra" | "linux-paplay" | "linux-aplay" | "terminal-bell" | "none";
28
+ export interface CompletionSoundRequest {
29
+ readonly promptRunId: string;
30
+ readonly outcome: "completed" | "failed" | "aborted";
31
+ readonly durationMs: number;
32
+ }
33
+ export interface CompletionSoundResult {
34
+ readonly backend: CompletionSoundBackend;
35
+ readonly attempted: boolean;
36
+ readonly success: boolean;
37
+ readonly diagnostic?: string;
38
+ }
39
+ export declare const COMPLETION_SOUND_ENV = "OMK_COMPLETION_SOUND";
40
+ export interface ResolvedCompletionSoundSettings {
41
+ readonly enabled: boolean;
42
+ readonly minDurationMs: number;
43
+ readonly onSuccess: boolean;
44
+ readonly onFailure: boolean;
45
+ readonly terminalBellFallback: boolean;
46
+ }
47
+ /** Defaults per §17.1; `OMK_COMPLETION_SOUND=0|1` (§18.2) overrides `enabled`. */
48
+ export declare function resolveCompletionSoundSettings(settings?: CompletionSoundSettings, env?: NodeJS.ProcessEnv): ResolvedCompletionSoundSettings;
49
+ export interface CompletionSoundSurface {
50
+ /** Only the interactive TUI plays sounds by default (§17.4). */
51
+ readonly isTui: boolean;
52
+ readonly isTty: boolean;
53
+ readonly isCi: boolean;
54
+ }
55
+ /** §17.1/§17.4 gate: settings, outcome mapping, duration floor, and surface. */
56
+ export declare function shouldPlayCompletionSound(input: {
57
+ readonly settings: ResolvedCompletionSoundSettings;
58
+ readonly event: PromptSettledEvent;
59
+ readonly surface: CompletionSoundSurface;
60
+ }): boolean;
61
+ export interface CompletionSoundCandidate {
62
+ readonly backend: CompletionSoundBackend;
63
+ /** Fixed argv (§22.1); empty for terminal-bell (written, not spawned). */
64
+ readonly argv: readonly string[];
65
+ }
66
+ /**
67
+ * Deterministic backend candidate chain per platform (§17.3, §23.2 property
68
+ * 14). The runner tries candidates in order on spawn failure.
69
+ */
70
+ export declare function selectCompletionSoundCandidates(input: {
71
+ readonly platform: NodeJS.Platform;
72
+ readonly isWsl: boolean;
73
+ readonly terminalBellFallback: boolean;
74
+ }): readonly CompletionSoundCandidate[];
75
+ export interface CompletionSoundIo {
76
+ /** Spawn a fixed argv; resolve success/failure. Must never throw synchronously into the caller. */
77
+ readonly spawnBackend: (argv: readonly string[]) => Promise<{
78
+ readonly ok: boolean;
79
+ readonly diagnostic?: string;
80
+ }>;
81
+ readonly writeBell: () => boolean;
82
+ }
83
+ /** Default IO: detached-ish fire-and-forget spawn per §17.4. */
84
+ export declare function defaultCompletionSoundIo(): CompletionSoundIo;
85
+ /**
86
+ * Stateful service: once per promptRunId (§17.4), deterministic candidate
87
+ * walk, all failures downgraded to diagnostics. `handleSettled` is fire and
88
+ * forget — callers must not await it on the prompt path.
89
+ */
90
+ export interface CompletionSoundServiceDeps {
91
+ readonly getSettings: () => CompletionSoundSettings | undefined;
92
+ readonly surface: () => CompletionSoundSurface;
93
+ readonly candidates: () => readonly CompletionSoundCandidate[];
94
+ readonly io?: CompletionSoundIo;
95
+ readonly env?: NodeJS.ProcessEnv;
96
+ /** §20.2 observability hook; fail-open, never affects the result. */
97
+ readonly onResult?: (result: CompletionSoundResult) => void;
98
+ }
99
+ export declare class CompletionSoundService {
100
+ private readonly playedRunIds;
101
+ private lastResult;
102
+ private readonly deps;
103
+ constructor(deps: CompletionSoundServiceDeps);
104
+ /** Most recent attempt, for diagnostics/tests. */
105
+ get lastPlaybackResult(): CompletionSoundResult | null;
106
+ handleSettled(event: PromptSettledEvent): Promise<CompletionSoundResult>;
107
+ private finish;
108
+ }
109
+ //# sourceMappingURL=completion-sound.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"completion-sound.d.ts","sourceRoot":"","sources":["../../src/core/completion-sound.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawn } from \"node:child_process\";\nimport type { PromptSettledEvent } from \"./prompt-settlement.ts\";\n\n/**\n * Cross-platform completion sound service (OMK v0.97.x roadmap §17, M4/PR7).\n *\n * Consumes `prompt_settled` only (§16.1: never `agent_end`) and is a pure UX\n * signal — it must never influence run outcome, block the event loop, or\n * delay process exit (§17.4: spawn with `stdio: \"ignore\"`, `shell: false`,\n * `unref()`, bounded kill timer, never awaited by the prompt path).\n *\n * Security (§22.1): every backend uses a fixed executable and fixed argv;\n * no user text, paths, or model output is ever interpolated.\n *\n * Backend selection is a deterministic candidate chain per platform\n * (§17.3); a spawn failure falls through to the next candidate and finally\n * to the terminal BEL. All failures are diagnostics only (§4.6).\n */\n\nexport interface CompletionSoundSettings {\n\tenabled?: boolean; // default: false (§17.1: backward compat, CI/headless surprise, accessibility)\n\tminDurationMs?: number; // default: 5000\n\tonSuccess?: boolean; // default: true\n\tonFailure?: boolean; // default: true\n\tterminalBellFallback?: boolean; // default: true\n}\n\nexport interface NotificationSettings {\n\tcompletionSound?: CompletionSoundSettings;\n}\n\nexport type CompletionSoundBackend =\n\t| \"macos-afplay\"\n\t| \"windows-system-sound\"\n\t| \"linux-canberra\"\n\t| \"linux-paplay\"\n\t| \"linux-aplay\"\n\t| \"terminal-bell\"\n\t| \"none\";\n\nexport interface CompletionSoundRequest {\n\treadonly promptRunId: string;\n\treadonly outcome: \"completed\" | \"failed\" | \"aborted\";\n\treadonly durationMs: number;\n}\n\nexport interface CompletionSoundResult {\n\treadonly backend: CompletionSoundBackend;\n\treadonly attempted: boolean;\n\treadonly success: boolean;\n\treadonly diagnostic?: string;\n}\n\nexport const COMPLETION_SOUND_ENV = \"OMK_COMPLETION_SOUND\";\nconst SPAWN_KILL_TIMEOUT_MS = 1000;\n\nexport interface ResolvedCompletionSoundSettings {\n\treadonly enabled: boolean;\n\treadonly minDurationMs: number;\n\treadonly onSuccess: boolean;\n\treadonly onFailure: boolean;\n\treadonly terminalBellFallback: boolean;\n}\n\n/** Defaults per §17.1; `OMK_COMPLETION_SOUND=0|1` (§18.2) overrides `enabled`. */\nexport function resolveCompletionSoundSettings(\n\tsettings?: CompletionSoundSettings,\n\tenv: NodeJS.ProcessEnv = process.env,\n): ResolvedCompletionSoundSettings {\n\tlet enabled = settings?.enabled ?? false;\n\tconst envValue = env[COMPLETION_SOUND_ENV]?.trim();\n\tif (envValue === \"0\") {\n\t\tenabled = false;\n\t} else if (envValue === \"1\") {\n\t\tenabled = true;\n\t}\n\treturn {\n\t\tenabled,\n\t\tminDurationMs:\n\t\t\ttypeof settings?.minDurationMs === \"number\" && settings.minDurationMs >= 0 ? settings.minDurationMs : 5000,\n\t\tonSuccess: settings?.onSuccess ?? true,\n\t\tonFailure: settings?.onFailure ?? true,\n\t\tterminalBellFallback: settings?.terminalBellFallback ?? true,\n\t};\n}\n\nexport interface CompletionSoundSurface {\n\t/** Only the interactive TUI plays sounds by default (§17.4). */\n\treadonly isTui: boolean;\n\treadonly isTty: boolean;\n\treadonly isCi: boolean;\n}\n\n/** §17.1/§17.4 gate: settings, outcome mapping, duration floor, and surface. */\nexport function shouldPlayCompletionSound(input: {\n\treadonly settings: ResolvedCompletionSoundSettings;\n\treadonly event: PromptSettledEvent;\n\treadonly surface: CompletionSoundSurface;\n}): boolean {\n\tconst { settings, event, surface } = input;\n\tif (!settings.enabled || !surface.isTui || !surface.isTty || surface.isCi) {\n\t\treturn false;\n\t}\n\tif (event.durationMs < settings.minDurationMs) {\n\t\treturn false;\n\t}\n\tif (event.outcome === \"completed\") {\n\t\treturn settings.onSuccess;\n\t}\n\tif (event.outcome === \"failed\") {\n\t\treturn settings.onFailure;\n\t}\n\t// User-initiated aborts never chime: the user is already at the keyboard.\n\treturn false;\n}\n\nexport interface CompletionSoundCandidate {\n\treadonly backend: CompletionSoundBackend;\n\t/** Fixed argv (§22.1); empty for terminal-bell (written, not spawned). */\n\treadonly argv: readonly string[];\n}\n\n/**\n * Deterministic backend candidate chain per platform (§17.3, §23.2 property\n * 14). The runner tries candidates in order on spawn failure.\n */\nexport function selectCompletionSoundCandidates(input: {\n\treadonly platform: NodeJS.Platform;\n\treadonly isWsl: boolean;\n\treadonly terminalBellFallback: boolean;\n}): readonly CompletionSoundCandidate[] {\n\tconst bell: CompletionSoundCandidate[] = input.terminalBellFallback ? [{ backend: \"terminal-bell\", argv: [] }] : [];\n\tif (input.platform === \"darwin\") {\n\t\treturn [{ backend: \"macos-afplay\", argv: [\"afplay\", \"/System/Library/Sounds/Glass.aiff\"] }, ...bell];\n\t}\n\tif (input.platform === \"win32\") {\n\t\treturn [{ backend: \"windows-system-sound\", argv: windowsSystemSoundArgv(\"powershell.exe\") }, ...bell];\n\t}\n\tif (input.platform === \"linux\" && input.isWsl) {\n\t\t// §17.3 WSL: Windows PowerShell first, then the terminal BEL.\n\t\treturn [{ backend: \"windows-system-sound\", argv: windowsSystemSoundArgv(\"powershell.exe\") }, ...bell];\n\t}\n\tif (input.platform === \"linux\") {\n\t\treturn [\n\t\t\t{ backend: \"linux-canberra\", argv: [\"canberra-gtk-play\", \"-i\", \"complete\"] },\n\t\t\t{ backend: \"linux-paplay\", argv: [\"paplay\", \"/usr/share/sounds/freedesktop/stereo/complete.oga\"] },\n\t\t\t{ backend: \"linux-aplay\", argv: [\"aplay\", \"-q\", \"/usr/share/sounds/alsa/Front_Center.wav\"] },\n\t\t\t...bell,\n\t\t];\n\t}\n\treturn [...bell];\n}\n\n/** Fixed expression only (§17.3 Windows): no user text ever enters the command. */\nfunction windowsSystemSoundArgv(executable: string): readonly string[] {\n\treturn [executable, \"-NoProfile\", \"-NonInteractive\", \"-Command\", \"[System.Media.SystemSounds]::Asterisk.Play()\"];\n}\n\nexport interface CompletionSoundIo {\n\t/** Spawn a fixed argv; resolve success/failure. Must never throw synchronously into the caller. */\n\treadonly spawnBackend: (argv: readonly string[]) => Promise<{ readonly ok: boolean; readonly diagnostic?: string }>;\n\treadonly writeBell: () => boolean;\n}\n\n/** Default IO: detached-ish fire-and-forget spawn per §17.4. */\nexport function defaultCompletionSoundIo(): CompletionSoundIo {\n\treturn {\n\t\tspawnBackend: (argv) =>\n\t\t\tnew Promise((resolve) => {\n\t\t\t\ttry {\n\t\t\t\t\tconst [executable, ...args] = argv;\n\t\t\t\t\tconst child = spawn(executable, args, { stdio: \"ignore\", shell: false, windowsHide: true });\n\t\t\t\t\tconst killTimer = setTimeout(() => {\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tchild.kill();\n\t\t\t\t\t\t} catch {\n\t\t\t\t\t\t\t// Best effort only.\n\t\t\t\t\t\t}\n\t\t\t\t\t}, SPAWN_KILL_TIMEOUT_MS);\n\t\t\t\t\tkillTimer.unref();\n\t\t\t\t\tchild.once(\"error\", (error: NodeJS.ErrnoException) =>\n\t\t\t\t\t\tresolve({ ok: false, diagnostic: error.code ?? \"spawn_error\" }),\n\t\t\t\t\t);\n\t\t\t\t\t// Success means \"spawned\"; playback is not awaited (§17.4).\n\t\t\t\t\tchild.once(\"spawn\", () => {\n\t\t\t\t\t\tchild.unref();\n\t\t\t\t\t\tresolve({ ok: true });\n\t\t\t\t\t});\n\t\t\t\t} catch {\n\t\t\t\t\tresolve({ ok: false, diagnostic: \"spawn_error\" });\n\t\t\t\t}\n\t\t\t}),\n\t\twriteBell: () => {\n\t\t\ttry {\n\t\t\t\tprocess.stdout.write(\"\\u0007\");\n\t\t\t\treturn true;\n\t\t\t} catch {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t},\n\t};\n}\n\n/**\n * Stateful service: once per promptRunId (§17.4), deterministic candidate\n * walk, all failures downgraded to diagnostics. `handleSettled` is fire and\n * forget — callers must not await it on the prompt path.\n */\nexport interface CompletionSoundServiceDeps {\n\treadonly getSettings: () => CompletionSoundSettings | undefined;\n\treadonly surface: () => CompletionSoundSurface;\n\treadonly candidates: () => readonly CompletionSoundCandidate[];\n\treadonly io?: CompletionSoundIo;\n\treadonly env?: NodeJS.ProcessEnv;\n\t/** §20.2 observability hook; fail-open, never affects the result. */\n\treadonly onResult?: (result: CompletionSoundResult) => void;\n}\n\nexport class CompletionSoundService {\n\tprivate readonly playedRunIds = new Set<string>();\n\tprivate lastResult: CompletionSoundResult | null = null;\n\tprivate readonly deps: CompletionSoundServiceDeps;\n\n\tconstructor(deps: CompletionSoundServiceDeps) {\n\t\tthis.deps = deps;\n\t}\n\n\t/** Most recent attempt, for diagnostics/tests. */\n\tget lastPlaybackResult(): CompletionSoundResult | null {\n\t\treturn this.lastResult;\n\t}\n\n\tasync handleSettled(event: PromptSettledEvent): Promise<CompletionSoundResult> {\n\t\ttry {\n\t\t\tconst settings = resolveCompletionSoundSettings(this.deps.getSettings(), this.deps.env ?? process.env);\n\t\t\tif (this.playedRunIds.has(event.promptRunId)) {\n\t\t\t\treturn this.finish({ backend: \"none\", attempted: false, success: false, diagnostic: \"already_played\" });\n\t\t\t}\n\t\t\tif (!shouldPlayCompletionSound({ settings, event, surface: this.deps.surface() })) {\n\t\t\t\treturn this.finish({ backend: \"none\", attempted: false, success: true });\n\t\t\t}\n\t\t\tthis.playedRunIds.add(event.promptRunId);\n\t\t\tconst io = this.deps.io ?? defaultCompletionSoundIo();\n\t\t\tconst diagnostics: string[] = [];\n\t\t\tfor (const candidate of this.deps.candidates()) {\n\t\t\t\tif (candidate.backend === \"terminal-bell\") {\n\t\t\t\t\tconst ok = io.writeBell();\n\t\t\t\t\treturn this.finish({\n\t\t\t\t\t\tbackend: \"terminal-bell\",\n\t\t\t\t\t\tattempted: true,\n\t\t\t\t\t\tsuccess: ok,\n\t\t\t\t\t\tdiagnostic: diagnostics.length > 0 ? diagnostics.join(\", \") : undefined,\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t\tconst spawned = await io.spawnBackend(candidate.argv);\n\t\t\t\tif (spawned.ok) {\n\t\t\t\t\treturn this.finish({ backend: candidate.backend, attempted: true, success: true });\n\t\t\t\t}\n\t\t\t\tdiagnostics.push(`${candidate.backend}: ${spawned.diagnostic ?? \"failed\"}`);\n\t\t\t}\n\t\t\treturn this.finish({ backend: \"none\", attempted: true, success: false, diagnostic: diagnostics.join(\", \") });\n\t\t} catch (error) {\n\t\t\t// §23.2 property 13: sound failure never changes the prompt outcome.\n\t\t\treturn this.finish({ backend: \"none\", attempted: true, success: false, diagnostic: String(error) });\n\t\t}\n\t}\n\n\tprivate finish(result: CompletionSoundResult): CompletionSoundResult {\n\t\tthis.lastResult = result;\n\t\ttry {\n\t\t\tthis.deps.onResult?.(result);\n\t\t} catch {\n\t\t\t// Observability must never affect the sound path.\n\t\t}\n\t\treturn result;\n\t}\n}\n"]}
@@ -0,0 +1,165 @@
1
+ import { spawn } from "node:child_process";
2
+ export const COMPLETION_SOUND_ENV = "OMK_COMPLETION_SOUND";
3
+ const SPAWN_KILL_TIMEOUT_MS = 1000;
4
+ /** Defaults per §17.1; `OMK_COMPLETION_SOUND=0|1` (§18.2) overrides `enabled`. */
5
+ export function resolveCompletionSoundSettings(settings, env = process.env) {
6
+ let enabled = settings?.enabled ?? false;
7
+ const envValue = env[COMPLETION_SOUND_ENV]?.trim();
8
+ if (envValue === "0") {
9
+ enabled = false;
10
+ }
11
+ else if (envValue === "1") {
12
+ enabled = true;
13
+ }
14
+ return {
15
+ enabled,
16
+ minDurationMs: typeof settings?.minDurationMs === "number" && settings.minDurationMs >= 0 ? settings.minDurationMs : 5000,
17
+ onSuccess: settings?.onSuccess ?? true,
18
+ onFailure: settings?.onFailure ?? true,
19
+ terminalBellFallback: settings?.terminalBellFallback ?? true,
20
+ };
21
+ }
22
+ /** §17.1/§17.4 gate: settings, outcome mapping, duration floor, and surface. */
23
+ export function shouldPlayCompletionSound(input) {
24
+ const { settings, event, surface } = input;
25
+ if (!settings.enabled || !surface.isTui || !surface.isTty || surface.isCi) {
26
+ return false;
27
+ }
28
+ if (event.durationMs < settings.minDurationMs) {
29
+ return false;
30
+ }
31
+ if (event.outcome === "completed") {
32
+ return settings.onSuccess;
33
+ }
34
+ if (event.outcome === "failed") {
35
+ return settings.onFailure;
36
+ }
37
+ // User-initiated aborts never chime: the user is already at the keyboard.
38
+ return false;
39
+ }
40
+ /**
41
+ * Deterministic backend candidate chain per platform (§17.3, §23.2 property
42
+ * 14). The runner tries candidates in order on spawn failure.
43
+ */
44
+ export function selectCompletionSoundCandidates(input) {
45
+ const bell = input.terminalBellFallback ? [{ backend: "terminal-bell", argv: [] }] : [];
46
+ if (input.platform === "darwin") {
47
+ return [{ backend: "macos-afplay", argv: ["afplay", "/System/Library/Sounds/Glass.aiff"] }, ...bell];
48
+ }
49
+ if (input.platform === "win32") {
50
+ return [{ backend: "windows-system-sound", argv: windowsSystemSoundArgv("powershell.exe") }, ...bell];
51
+ }
52
+ if (input.platform === "linux" && input.isWsl) {
53
+ // §17.3 WSL: Windows PowerShell first, then the terminal BEL.
54
+ return [{ backend: "windows-system-sound", argv: windowsSystemSoundArgv("powershell.exe") }, ...bell];
55
+ }
56
+ if (input.platform === "linux") {
57
+ return [
58
+ { backend: "linux-canberra", argv: ["canberra-gtk-play", "-i", "complete"] },
59
+ { backend: "linux-paplay", argv: ["paplay", "/usr/share/sounds/freedesktop/stereo/complete.oga"] },
60
+ { backend: "linux-aplay", argv: ["aplay", "-q", "/usr/share/sounds/alsa/Front_Center.wav"] },
61
+ ...bell,
62
+ ];
63
+ }
64
+ return [...bell];
65
+ }
66
+ /** Fixed expression only (§17.3 Windows): no user text ever enters the command. */
67
+ function windowsSystemSoundArgv(executable) {
68
+ return [executable, "-NoProfile", "-NonInteractive", "-Command", "[System.Media.SystemSounds]::Asterisk.Play()"];
69
+ }
70
+ /** Default IO: detached-ish fire-and-forget spawn per §17.4. */
71
+ export function defaultCompletionSoundIo() {
72
+ return {
73
+ spawnBackend: (argv) => new Promise((resolve) => {
74
+ try {
75
+ const [executable, ...args] = argv;
76
+ const child = spawn(executable, args, { stdio: "ignore", shell: false, windowsHide: true });
77
+ const killTimer = setTimeout(() => {
78
+ try {
79
+ child.kill();
80
+ }
81
+ catch {
82
+ // Best effort only.
83
+ }
84
+ }, SPAWN_KILL_TIMEOUT_MS);
85
+ killTimer.unref();
86
+ child.once("error", (error) => resolve({ ok: false, diagnostic: error.code ?? "spawn_error" }));
87
+ // Success means "spawned"; playback is not awaited (§17.4).
88
+ child.once("spawn", () => {
89
+ child.unref();
90
+ resolve({ ok: true });
91
+ });
92
+ }
93
+ catch {
94
+ resolve({ ok: false, diagnostic: "spawn_error" });
95
+ }
96
+ }),
97
+ writeBell: () => {
98
+ try {
99
+ process.stdout.write("\u0007");
100
+ return true;
101
+ }
102
+ catch {
103
+ return false;
104
+ }
105
+ },
106
+ };
107
+ }
108
+ export class CompletionSoundService {
109
+ playedRunIds = new Set();
110
+ lastResult = null;
111
+ deps;
112
+ constructor(deps) {
113
+ this.deps = deps;
114
+ }
115
+ /** Most recent attempt, for diagnostics/tests. */
116
+ get lastPlaybackResult() {
117
+ return this.lastResult;
118
+ }
119
+ async handleSettled(event) {
120
+ try {
121
+ const settings = resolveCompletionSoundSettings(this.deps.getSettings(), this.deps.env ?? process.env);
122
+ if (this.playedRunIds.has(event.promptRunId)) {
123
+ return this.finish({ backend: "none", attempted: false, success: false, diagnostic: "already_played" });
124
+ }
125
+ if (!shouldPlayCompletionSound({ settings, event, surface: this.deps.surface() })) {
126
+ return this.finish({ backend: "none", attempted: false, success: true });
127
+ }
128
+ this.playedRunIds.add(event.promptRunId);
129
+ const io = this.deps.io ?? defaultCompletionSoundIo();
130
+ const diagnostics = [];
131
+ for (const candidate of this.deps.candidates()) {
132
+ if (candidate.backend === "terminal-bell") {
133
+ const ok = io.writeBell();
134
+ return this.finish({
135
+ backend: "terminal-bell",
136
+ attempted: true,
137
+ success: ok,
138
+ diagnostic: diagnostics.length > 0 ? diagnostics.join(", ") : undefined,
139
+ });
140
+ }
141
+ const spawned = await io.spawnBackend(candidate.argv);
142
+ if (spawned.ok) {
143
+ return this.finish({ backend: candidate.backend, attempted: true, success: true });
144
+ }
145
+ diagnostics.push(`${candidate.backend}: ${spawned.diagnostic ?? "failed"}`);
146
+ }
147
+ return this.finish({ backend: "none", attempted: true, success: false, diagnostic: diagnostics.join(", ") });
148
+ }
149
+ catch (error) {
150
+ // §23.2 property 13: sound failure never changes the prompt outcome.
151
+ return this.finish({ backend: "none", attempted: true, success: false, diagnostic: String(error) });
152
+ }
153
+ }
154
+ finish(result) {
155
+ this.lastResult = result;
156
+ try {
157
+ this.deps.onResult?.(result);
158
+ }
159
+ catch {
160
+ // Observability must never affect the sound path.
161
+ }
162
+ return result;
163
+ }
164
+ }
165
+ //# sourceMappingURL=completion-sound.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"completion-sound.js","sourceRoot":"","sources":["../../src/core/completion-sound.ts"],"names":[],"mappings":"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","sourcesContent":["import { spawn } from \"node:child_process\";\nimport type { PromptSettledEvent } from \"./prompt-settlement.ts\";\n\n/**\n * Cross-platform completion sound service (OMK v0.97.x roadmap §17, M4/PR7).\n *\n * Consumes `prompt_settled` only (§16.1: never `agent_end`) and is a pure UX\n * signal — it must never influence run outcome, block the event loop, or\n * delay process exit (§17.4: spawn with `stdio: \"ignore\"`, `shell: false`,\n * `unref()`, bounded kill timer, never awaited by the prompt path).\n *\n * Security (§22.1): every backend uses a fixed executable and fixed argv;\n * no user text, paths, or model output is ever interpolated.\n *\n * Backend selection is a deterministic candidate chain per platform\n * (§17.3); a spawn failure falls through to the next candidate and finally\n * to the terminal BEL. All failures are diagnostics only (§4.6).\n */\n\nexport interface CompletionSoundSettings {\n\tenabled?: boolean; // default: false (§17.1: backward compat, CI/headless surprise, accessibility)\n\tminDurationMs?: number; // default: 5000\n\tonSuccess?: boolean; // default: true\n\tonFailure?: boolean; // default: true\n\tterminalBellFallback?: boolean; // default: true\n}\n\nexport interface NotificationSettings {\n\tcompletionSound?: CompletionSoundSettings;\n}\n\nexport type CompletionSoundBackend =\n\t| \"macos-afplay\"\n\t| \"windows-system-sound\"\n\t| \"linux-canberra\"\n\t| \"linux-paplay\"\n\t| \"linux-aplay\"\n\t| \"terminal-bell\"\n\t| \"none\";\n\nexport interface CompletionSoundRequest {\n\treadonly promptRunId: string;\n\treadonly outcome: \"completed\" | \"failed\" | \"aborted\";\n\treadonly durationMs: number;\n}\n\nexport interface CompletionSoundResult {\n\treadonly backend: CompletionSoundBackend;\n\treadonly attempted: boolean;\n\treadonly success: boolean;\n\treadonly diagnostic?: string;\n}\n\nexport const COMPLETION_SOUND_ENV = \"OMK_COMPLETION_SOUND\";\nconst SPAWN_KILL_TIMEOUT_MS = 1000;\n\nexport interface ResolvedCompletionSoundSettings {\n\treadonly enabled: boolean;\n\treadonly minDurationMs: number;\n\treadonly onSuccess: boolean;\n\treadonly onFailure: boolean;\n\treadonly terminalBellFallback: boolean;\n}\n\n/** Defaults per §17.1; `OMK_COMPLETION_SOUND=0|1` (§18.2) overrides `enabled`. */\nexport function resolveCompletionSoundSettings(\n\tsettings?: CompletionSoundSettings,\n\tenv: NodeJS.ProcessEnv = process.env,\n): ResolvedCompletionSoundSettings {\n\tlet enabled = settings?.enabled ?? false;\n\tconst envValue = env[COMPLETION_SOUND_ENV]?.trim();\n\tif (envValue === \"0\") {\n\t\tenabled = false;\n\t} else if (envValue === \"1\") {\n\t\tenabled = true;\n\t}\n\treturn {\n\t\tenabled,\n\t\tminDurationMs:\n\t\t\ttypeof settings?.minDurationMs === \"number\" && settings.minDurationMs >= 0 ? settings.minDurationMs : 5000,\n\t\tonSuccess: settings?.onSuccess ?? true,\n\t\tonFailure: settings?.onFailure ?? true,\n\t\tterminalBellFallback: settings?.terminalBellFallback ?? true,\n\t};\n}\n\nexport interface CompletionSoundSurface {\n\t/** Only the interactive TUI plays sounds by default (§17.4). */\n\treadonly isTui: boolean;\n\treadonly isTty: boolean;\n\treadonly isCi: boolean;\n}\n\n/** §17.1/§17.4 gate: settings, outcome mapping, duration floor, and surface. */\nexport function shouldPlayCompletionSound(input: {\n\treadonly settings: ResolvedCompletionSoundSettings;\n\treadonly event: PromptSettledEvent;\n\treadonly surface: CompletionSoundSurface;\n}): boolean {\n\tconst { settings, event, surface } = input;\n\tif (!settings.enabled || !surface.isTui || !surface.isTty || surface.isCi) {\n\t\treturn false;\n\t}\n\tif (event.durationMs < settings.minDurationMs) {\n\t\treturn false;\n\t}\n\tif (event.outcome === \"completed\") {\n\t\treturn settings.onSuccess;\n\t}\n\tif (event.outcome === \"failed\") {\n\t\treturn settings.onFailure;\n\t}\n\t// User-initiated aborts never chime: the user is already at the keyboard.\n\treturn false;\n}\n\nexport interface CompletionSoundCandidate {\n\treadonly backend: CompletionSoundBackend;\n\t/** Fixed argv (§22.1); empty for terminal-bell (written, not spawned). */\n\treadonly argv: readonly string[];\n}\n\n/**\n * Deterministic backend candidate chain per platform (§17.3, §23.2 property\n * 14). The runner tries candidates in order on spawn failure.\n */\nexport function selectCompletionSoundCandidates(input: {\n\treadonly platform: NodeJS.Platform;\n\treadonly isWsl: boolean;\n\treadonly terminalBellFallback: boolean;\n}): readonly CompletionSoundCandidate[] {\n\tconst bell: CompletionSoundCandidate[] = input.terminalBellFallback ? [{ backend: \"terminal-bell\", argv: [] }] : [];\n\tif (input.platform === \"darwin\") {\n\t\treturn [{ backend: \"macos-afplay\", argv: [\"afplay\", \"/System/Library/Sounds/Glass.aiff\"] }, ...bell];\n\t}\n\tif (input.platform === \"win32\") {\n\t\treturn [{ backend: \"windows-system-sound\", argv: windowsSystemSoundArgv(\"powershell.exe\") }, ...bell];\n\t}\n\tif (input.platform === \"linux\" && input.isWsl) {\n\t\t// §17.3 WSL: Windows PowerShell first, then the terminal BEL.\n\t\treturn [{ backend: \"windows-system-sound\", argv: windowsSystemSoundArgv(\"powershell.exe\") }, ...bell];\n\t}\n\tif (input.platform === \"linux\") {\n\t\treturn [\n\t\t\t{ backend: \"linux-canberra\", argv: [\"canberra-gtk-play\", \"-i\", \"complete\"] },\n\t\t\t{ backend: \"linux-paplay\", argv: [\"paplay\", \"/usr/share/sounds/freedesktop/stereo/complete.oga\"] },\n\t\t\t{ backend: \"linux-aplay\", argv: [\"aplay\", \"-q\", \"/usr/share/sounds/alsa/Front_Center.wav\"] },\n\t\t\t...bell,\n\t\t];\n\t}\n\treturn [...bell];\n}\n\n/** Fixed expression only (§17.3 Windows): no user text ever enters the command. */\nfunction windowsSystemSoundArgv(executable: string): readonly string[] {\n\treturn [executable, \"-NoProfile\", \"-NonInteractive\", \"-Command\", \"[System.Media.SystemSounds]::Asterisk.Play()\"];\n}\n\nexport interface CompletionSoundIo {\n\t/** Spawn a fixed argv; resolve success/failure. Must never throw synchronously into the caller. */\n\treadonly spawnBackend: (argv: readonly string[]) => Promise<{ readonly ok: boolean; readonly diagnostic?: string }>;\n\treadonly writeBell: () => boolean;\n}\n\n/** Default IO: detached-ish fire-and-forget spawn per §17.4. */\nexport function defaultCompletionSoundIo(): CompletionSoundIo {\n\treturn {\n\t\tspawnBackend: (argv) =>\n\t\t\tnew Promise((resolve) => {\n\t\t\t\ttry {\n\t\t\t\t\tconst [executable, ...args] = argv;\n\t\t\t\t\tconst child = spawn(executable, args, { stdio: \"ignore\", shell: false, windowsHide: true });\n\t\t\t\t\tconst killTimer = setTimeout(() => {\n\t\t\t\t\t\ttry {\n\t\t\t\t\t\t\tchild.kill();\n\t\t\t\t\t\t} catch {\n\t\t\t\t\t\t\t// Best effort only.\n\t\t\t\t\t\t}\n\t\t\t\t\t}, SPAWN_KILL_TIMEOUT_MS);\n\t\t\t\t\tkillTimer.unref();\n\t\t\t\t\tchild.once(\"error\", (error: NodeJS.ErrnoException) =>\n\t\t\t\t\t\tresolve({ ok: false, diagnostic: error.code ?? \"spawn_error\" }),\n\t\t\t\t\t);\n\t\t\t\t\t// Success means \"spawned\"; playback is not awaited (§17.4).\n\t\t\t\t\tchild.once(\"spawn\", () => {\n\t\t\t\t\t\tchild.unref();\n\t\t\t\t\t\tresolve({ ok: true });\n\t\t\t\t\t});\n\t\t\t\t} catch {\n\t\t\t\t\tresolve({ ok: false, diagnostic: \"spawn_error\" });\n\t\t\t\t}\n\t\t\t}),\n\t\twriteBell: () => {\n\t\t\ttry {\n\t\t\t\tprocess.stdout.write(\"\\u0007\");\n\t\t\t\treturn true;\n\t\t\t} catch {\n\t\t\t\treturn false;\n\t\t\t}\n\t\t},\n\t};\n}\n\n/**\n * Stateful service: once per promptRunId (§17.4), deterministic candidate\n * walk, all failures downgraded to diagnostics. `handleSettled` is fire and\n * forget — callers must not await it on the prompt path.\n */\nexport interface CompletionSoundServiceDeps {\n\treadonly getSettings: () => CompletionSoundSettings | undefined;\n\treadonly surface: () => CompletionSoundSurface;\n\treadonly candidates: () => readonly CompletionSoundCandidate[];\n\treadonly io?: CompletionSoundIo;\n\treadonly env?: NodeJS.ProcessEnv;\n\t/** §20.2 observability hook; fail-open, never affects the result. */\n\treadonly onResult?: (result: CompletionSoundResult) => void;\n}\n\nexport class CompletionSoundService {\n\tprivate readonly playedRunIds = new Set<string>();\n\tprivate lastResult: CompletionSoundResult | null = null;\n\tprivate readonly deps: CompletionSoundServiceDeps;\n\n\tconstructor(deps: CompletionSoundServiceDeps) {\n\t\tthis.deps = deps;\n\t}\n\n\t/** Most recent attempt, for diagnostics/tests. */\n\tget lastPlaybackResult(): CompletionSoundResult | null {\n\t\treturn this.lastResult;\n\t}\n\n\tasync handleSettled(event: PromptSettledEvent): Promise<CompletionSoundResult> {\n\t\ttry {\n\t\t\tconst settings = resolveCompletionSoundSettings(this.deps.getSettings(), this.deps.env ?? process.env);\n\t\t\tif (this.playedRunIds.has(event.promptRunId)) {\n\t\t\t\treturn this.finish({ backend: \"none\", attempted: false, success: false, diagnostic: \"already_played\" });\n\t\t\t}\n\t\t\tif (!shouldPlayCompletionSound({ settings, event, surface: this.deps.surface() })) {\n\t\t\t\treturn this.finish({ backend: \"none\", attempted: false, success: true });\n\t\t\t}\n\t\t\tthis.playedRunIds.add(event.promptRunId);\n\t\t\tconst io = this.deps.io ?? defaultCompletionSoundIo();\n\t\t\tconst diagnostics: string[] = [];\n\t\t\tfor (const candidate of this.deps.candidates()) {\n\t\t\t\tif (candidate.backend === \"terminal-bell\") {\n\t\t\t\t\tconst ok = io.writeBell();\n\t\t\t\t\treturn this.finish({\n\t\t\t\t\t\tbackend: \"terminal-bell\",\n\t\t\t\t\t\tattempted: true,\n\t\t\t\t\t\tsuccess: ok,\n\t\t\t\t\t\tdiagnostic: diagnostics.length > 0 ? diagnostics.join(\", \") : undefined,\n\t\t\t\t\t});\n\t\t\t\t}\n\t\t\t\tconst spawned = await io.spawnBackend(candidate.argv);\n\t\t\t\tif (spawned.ok) {\n\t\t\t\t\treturn this.finish({ backend: candidate.backend, attempted: true, success: true });\n\t\t\t\t}\n\t\t\t\tdiagnostics.push(`${candidate.backend}: ${spawned.diagnostic ?? \"failed\"}`);\n\t\t\t}\n\t\t\treturn this.finish({ backend: \"none\", attempted: true, success: false, diagnostic: diagnostics.join(\", \") });\n\t\t} catch (error) {\n\t\t\t// §23.2 property 13: sound failure never changes the prompt outcome.\n\t\t\treturn this.finish({ backend: \"none\", attempted: true, success: false, diagnostic: String(error) });\n\t\t}\n\t}\n\n\tprivate finish(result: CompletionSoundResult): CompletionSoundResult {\n\t\tthis.lastResult = result;\n\t\ttry {\n\t\t\tthis.deps.onResult?.(result);\n\t\t} catch {\n\t\t\t// Observability must never affect the sound path.\n\t\t}\n\t\treturn result;\n\t}\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"context-budget-system-prompt-items.d.ts","sourceRoot":"","sources":["../../src/core/context-budget-system-prompt-items.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,mBAAmB,EAAY,MAAM,iCAAiC,CAAC;AAErF,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AACxD,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AAEzC,UAAU,2BAA2B;IACpC,QAAQ,CAAC,YAAY,EAAE,SAAS,WAAW,EAAE,CAAC;IAC9C,QAAQ,CAAC,MAAM,EAAE,SAAS,KAAK,EAAE,CAAC;IAClC,QAAQ,CAAC,aAAa,EAAE,OAAO,CAAC;IAChC,QAAQ,CAAC,OAAO,EAAE;QACjB,QAAQ,CAAC,gBAAgB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;QAC9C,QAAQ,CAAC,qBAAqB,CAAC,EAAE,OAAO,CAAC;QACzC,QAAQ,CAAC,uBAAuB,CAAC,EAAE,OAAO,CAAC;QAC3C,QAAQ,CAAC,iBAAiB,CAAC,EAAE,MAAM,CAAC;QACpC,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;KAC/B,CAAC;CACF;AAQD,wBAAgB,6BAA6B,CAC5C,KAAK,EAAE,2BAA2B,EAClC,cAAc,EAAE,MAAM,GACpB,mBAAmB,EAAE,CAkBvB;AAwID,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAEjD;AAED,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAE7C","sourcesContent":["import { pathToFileURL } from \"node:url\";\nimport { type ContextBudgetItemV2, fnv1aHex } from \"./context-budget-governor-v2.ts\";\nimport { scoreContextFileRelevance, scoreSkillRelevance } from \"./context-budget-relevance.ts\";\nimport type { ContextFile } from \"./resource-loader.ts\";\nimport type { Skill } from \"./skills.ts\";\n\ninterface SystemPromptBudgetItemInput {\n\treadonly contextFiles: readonly ContextFile[];\n\treadonly skills: readonly Skill[];\n\treadonly includeSkills: boolean;\n\treadonly options: {\n\t\treadonly activeSkillNames?: readonly string[];\n\t\treadonly includeSkillInventory?: boolean;\n\t\treadonly includeFullContextFiles?: boolean;\n\t\treadonly maxInactiveSkills?: number;\n\t\treadonly queryContext?: string;\n\t};\n}\n\nconst CONTEXT_EXCERPT_CHARS = 1200;\nconst MIN_EXCERPT_CHARS = 400;\nconst MAX_EXCERPT_CHARS = 3000;\nconst MAX_INACTIVE_SKILLS = 15;\nconst CONTEXT_FULL_BUDGET_RATIO = 0.5;\n\nexport function createSystemPromptBudgetItems(\n\tinput: SystemPromptBudgetItemInput,\n\tresourceBudget: number,\n): ContextBudgetItemV2[] {\n\tconst items: ContextBudgetItemV2[] = [];\n\tconst contextFullCount = input.options.includeFullContextFiles !== false ? input.contextFiles.length : 0;\n\tconst excerptChars = computeExcerptChars(resourceBudget, contextFullCount);\n\n\tfor (const contextFile of input.contextFiles) {\n\t\tconst queryScore = scoreContextFileRelevance(\n\t\t\t{ path: contextFile.path, content: contextFile.content, isGlobal: contextFile.isGlobal ?? false },\n\t\t\tinput.options.queryContext,\n\t\t);\n\t\tpushContextItems(items, contextFile, queryScore, excerptChars, input.options.includeFullContextFiles !== false);\n\t}\n\n\tconst visibleSkills = input.skills.filter((skill) => !skill.disableModelInvocation);\n\tif (input.includeSkills && input.options.includeSkillInventory !== false && visibleSkills.length > 0) {\n\t\tpushSkillItems(items, visibleSkills, input.options);\n\t}\n\treturn items;\n}\n\nfunction pushContextItems(\n\titems: ContextBudgetItemV2[],\n\tcontextFile: ContextFile,\n\tqueryScore: number,\n\texcerptChars: number,\n\tincludeFullContextFiles: boolean,\n): void {\n\tconst scope = contextFile.isGlobal ? \"parent\" : \"project\";\n\tconst tagName = contextFile.isGlobal ? \"parent_instructions\" : \"project_instructions\";\n\titems.push({\n\t\tid: `context-pointer:${contextFile.path}`,\n\t\ttier: contextFile.isGlobal ? \"system\" : \"current-files\",\n\t\tpriority: contextFile.isGlobal ? \"hard\" : \"high\",\n\t\trelevance: (contextFile.isGlobal ? 1 : 0.8) * 0.4 + queryScore * 0.6,\n\t\ttext: renderContextPointer(contextFile, scope),\n\t});\n\tif (!includeFullContextFiles) return;\n\titems.push({\n\t\tid: `context-full:${contextFile.path}`,\n\t\ttier: contextFile.isGlobal ? \"system\" : \"current-files\",\n\t\tpriority: contextFile.isGlobal ? \"high\" : \"medium\",\n\t\trelevance: (contextFile.isGlobal ? 0.9 : 0.6) * 0.4 + queryScore * 0.6,\n\t\tredundancyKey: contextFile.path,\n\t\tsourceRef: {\n\t\t\turi: pathToFileURL(contextFile.path).href,\n\t\t\tcontentHash: fnv1aHex(contextFile.content),\n\t\t\tretrievable: true,\n\t\t},\n\t\ttext: renderContextFull(contextFile, tagName, excerptChars),\n\t});\n}\n\nfunction pushSkillItems(\n\titems: ContextBudgetItemV2[],\n\tvisibleSkills: readonly Skill[],\n\toptions: SystemPromptBudgetItemInput[\"options\"],\n): void {\n\titems.push({ id: \"skill-header\", tier: \"skills\", priority: \"hard\", text: renderSkillHeader() });\n\tconst activeSkillNames = new Set(options.activeSkillNames ?? []);\n\tconst maxInactive = options.maxInactiveSkills ?? MAX_INACTIVE_SKILLS;\n\tfor (const skill of visibleSkills.filter((skill) => activeSkillNames.has(skill.name))) {\n\t\titems.push(renderSkillBudgetItem(skill, \"hard\", 1));\n\t}\n\n\tconst sortedInactive = visibleSkills\n\t\t.filter((skill) => !activeSkillNames.has(skill.name))\n\t\t.sort((a, b) => scoreSkillRelevance(b, options.queryContext) - scoreSkillRelevance(a, options.queryContext));\n\tfor (const skill of sortedInactive.slice(0, maxInactive)) {\n\t\titems.push(renderSkillBudgetItem(skill, \"low\", Math.max(0.25, scoreSkillRelevance(skill, options.queryContext))));\n\t}\n\titems.push({ id: \"skill-footer\", tier: \"skills\", priority: \"hard\", text: \"</available_skills>\" });\n\n\tconst omittedInactiveCount = sortedInactive.length - Math.min(sortedInactive.length, maxInactive);\n\tif (omittedInactiveCount > 0) {\n\t\titems.push({\n\t\t\tid: \"skill-omitted-summary\",\n\t\t\ttier: \"skills\",\n\t\t\tpriority: \"low\",\n\t\t\trelevance: 0.1,\n\t\t\ttext: `<!-- ${omittedInactiveCount} additional skills available. Use 'read' to load a specific skill file when a task matches. -->`,\n\t\t});\n\t}\n}\n\nfunction renderSkillBudgetItem(skill: Skill, priority: \"hard\" | \"low\", relevance: number): ContextBudgetItemV2 {\n\treturn {\n\t\tid: `skill:${skill.name}`,\n\t\ttier: \"skills\",\n\t\tpriority,\n\t\trelevance,\n\t\tredundancyKey: skill.name,\n\t\ttext: renderSkillEntry(skill),\n\t};\n}\n\nfunction computeExcerptChars(resourceBudget: number, contextFullCount: number): number {\n\tif (contextFullCount <= 0) {\n\t\treturn CONTEXT_EXCERPT_CHARS;\n\t}\n\tconst allocatedTokens = Math.floor(resourceBudget * CONTEXT_FULL_BUDGET_RATIO) / contextFullCount;\n\tconst estimatedChars = Math.floor(allocatedTokens * 4);\n\treturn Math.max(MIN_EXCERPT_CHARS, Math.min(MAX_EXCERPT_CHARS, estimatedChars));\n}\n\nfunction renderContextPointer(contextFile: ContextFile, scope: string): string {\n\tconst marker = contextFile.containsJailbreak ? \" sanitized=true\" : \"\";\n\treturn `<context_file_pointer scope=\"${escapeXml(scope)}\" path=\"${escapeXml(contextFile.path)}\"${marker} chars=\"${contextFile.content.length}\" />`;\n}\n\nfunction renderContextFull(contextFile: ContextFile, tagName: string, excerptChars = CONTEXT_EXCERPT_CHARS): string {\n\tconst content =\n\t\tcontextFile.content.length > excerptChars\n\t\t\t? boundedExcerpt(contextFile.content, excerptChars)\n\t\t\t: contextFile.content;\n\tconst truncated = content.length < contextFile.content.length ? \" truncated=true\" : \"\";\n\treturn `<${tagName} path=\"${escapeXml(contextFile.path)}\"${truncated}>\\n${escapeXmlText(content)}\\n</${tagName}>`;\n}\n\nfunction boundedExcerpt(content: string, excerptChars = CONTEXT_EXCERPT_CHARS): string {\n\tconst headChars = Math.floor(excerptChars * 0.7);\n\tconst tailChars = excerptChars - headChars;\n\tlet head = content.slice(0, headChars);\n\tconst lastSpace = head.search(/\\s+\\S*$/);\n\tif (lastSpace > headChars * 0.5) {\n\t\thead = head.slice(0, lastSpace);\n\t}\n\tlet tail = content.slice(-tailChars);\n\tconst firstSpace = tail.search(/\\S+\\s+/);\n\tif (firstSpace > 0 && firstSpace < tailChars * 0.5) {\n\t\ttail = tail.slice(firstSpace).trimStart();\n\t}\n\treturn `${head}\\n\\n[...context-budget excerpt omitted ${content.length - excerptChars} chars...]\\n\\n${tail}`;\n}\n\nfunction renderSkillHeader(): string {\n\treturn [\n\t\t\"\\n\\nThe following skills provide specialized instructions for specific tasks.\",\n\t\t\"Use the read tool to load a skill's file when the task matches its description.\",\n\t\t\"When a skill file references a relative path, resolve it against the skill directory (parent of SKILL.md / dirname of the path) and use that absolute path in tool commands.\",\n\t\t\"\",\n\t\t\"<available_skills>\",\n\t].join(\"\\n\");\n}\n\nfunction renderSkillEntry(skill: Skill): string {\n\treturn [\n\t\t\" <skill>\",\n\t\t` <name>${escapeXml(skill.name)}</name>`,\n\t\t` <description>${escapeXml(skill.description)}</description>`,\n\t\t` <location>${escapeXml(skill.filePath)}</location>`,\n\t\t\" </skill>\",\n\t].join(\"\\n\");\n}\n\nexport function escapeXmlText(str: string): string {\n\treturn str.replace(/&/g, \"&amp;\").replace(/</g, \"&lt;\").replace(/>/g, \"&gt;\");\n}\n\nexport function escapeXml(str: string): string {\n\treturn escapeXmlText(str).replace(/\"/g, \"&quot;\").replace(/'/g, \"&apos;\");\n}\n"]}
1
+ {"version":3,"file":"context-budget-system-prompt-items.d.ts","sourceRoot":"","sources":["../../src/core/context-budget-system-prompt-items.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,KAAK,mBAAmB,EAAY,MAAM,iCAAiC,CAAC;AAErF,OAAO,KAAK,EAAE,WAAW,EAAE,MAAM,sBAAsB,CAAC;AACxD,OAAO,KAAK,EAAE,KAAK,EAAE,MAAM,aAAa,CAAC;AAEzC,UAAU,2BAA2B;IACpC,QAAQ,CAAC,YAAY,EAAE,SAAS,WAAW,EAAE,CAAC;IAC9C,QAAQ,CAAC,MAAM,EAAE,SAAS,KAAK,EAAE,CAAC;IAClC,QAAQ,CAAC,aAAa,EAAE,OAAO,CAAC;IAChC,QAAQ,CAAC,OAAO,EAAE;QACjB,QAAQ,CAAC,gBAAgB,CAAC,EAAE,SAAS,MAAM,EAAE,CAAC;QAC9C,QAAQ,CAAC,qBAAqB,CAAC,EAAE,OAAO,CAAC;QACzC,QAAQ,CAAC,uBAAuB,CAAC,EAAE,OAAO,CAAC;QAC3C,QAAQ,CAAC,iBAAiB,CAAC,EAAE,MAAM,CAAC;QACpC,QAAQ,CAAC,YAAY,CAAC,EAAE,MAAM,CAAC;KAC/B,CAAC;CACF;AAQD,wBAAgB,6BAA6B,CAC5C,KAAK,EAAE,2BAA2B,EAClC,cAAc,EAAE,MAAM,GACpB,mBAAmB,EAAE,CAqBvB;AAyID,wBAAgB,aAAa,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAEjD;AAED,wBAAgB,SAAS,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAE7C","sourcesContent":["import { pathToFileURL } from \"node:url\";\nimport { type ContextBudgetItemV2, fnv1aHex } from \"./context-budget-governor-v2.ts\";\nimport { scoreContextFileRelevance, scoreSkillRelevance } from \"./context-budget-relevance.ts\";\nimport type { ContextFile } from \"./resource-loader.ts\";\nimport type { Skill } from \"./skills.ts\";\n\ninterface SystemPromptBudgetItemInput {\n\treadonly contextFiles: readonly ContextFile[];\n\treadonly skills: readonly Skill[];\n\treadonly includeSkills: boolean;\n\treadonly options: {\n\t\treadonly activeSkillNames?: readonly string[];\n\t\treadonly includeSkillInventory?: boolean;\n\t\treadonly includeFullContextFiles?: boolean;\n\t\treadonly maxInactiveSkills?: number;\n\t\treadonly queryContext?: string;\n\t};\n}\n\nconst CONTEXT_EXCERPT_CHARS = 1200;\nconst MIN_EXCERPT_CHARS = 400;\nconst MAX_EXCERPT_CHARS = 3000;\nconst MAX_INACTIVE_SKILLS = 15;\nconst CONTEXT_FULL_BUDGET_RATIO = 0.5;\n\nexport function createSystemPromptBudgetItems(\n\tinput: SystemPromptBudgetItemInput,\n\tresourceBudget: number,\n): ContextBudgetItemV2[] {\n\tconst items: ContextBudgetItemV2[] = [];\n\tconst contextFullCount = input.options.includeFullContextFiles !== false ? input.contextFiles.length : 0;\n\tconst excerptChars = computeExcerptChars(resourceBudget, contextFullCount);\n\n\tfor (const contextFile of input.contextFiles) {\n\t\tconst queryScore = scoreContextFileRelevance(\n\t\t\t{ path: contextFile.path, content: contextFile.content, isGlobal: contextFile.isGlobal ?? false },\n\t\t\tinput.options.queryContext,\n\t\t);\n\t\tpushContextItems(items, contextFile, queryScore, excerptChars, input.options.includeFullContextFiles !== false);\n\t}\n\n\tconst activeSkillNames = new Set(input.options.activeSkillNames ?? []);\n\tconst visibleSkills = input.skills.filter(\n\t\t(skill) => !skill.disableModelInvocation || activeSkillNames.has(skill.name),\n\t);\n\tif (input.includeSkills && input.options.includeSkillInventory !== false && visibleSkills.length > 0) {\n\t\tpushSkillItems(items, visibleSkills, input.options);\n\t}\n\treturn items;\n}\n\nfunction pushContextItems(\n\titems: ContextBudgetItemV2[],\n\tcontextFile: ContextFile,\n\tqueryScore: number,\n\texcerptChars: number,\n\tincludeFullContextFiles: boolean,\n): void {\n\tconst scope = contextFile.isGlobal ? \"parent\" : \"project\";\n\tconst tagName = contextFile.isGlobal ? \"parent_instructions\" : \"project_instructions\";\n\titems.push({\n\t\tid: `context-pointer:${contextFile.path}`,\n\t\ttier: contextFile.isGlobal ? \"system\" : \"current-files\",\n\t\tpriority: contextFile.isGlobal ? \"hard\" : \"high\",\n\t\trelevance: (contextFile.isGlobal ? 1 : 0.8) * 0.4 + queryScore * 0.6,\n\t\ttext: renderContextPointer(contextFile, scope),\n\t});\n\tif (!includeFullContextFiles) return;\n\titems.push({\n\t\tid: `context-full:${contextFile.path}`,\n\t\ttier: contextFile.isGlobal ? \"system\" : \"current-files\",\n\t\tpriority: contextFile.isGlobal ? \"high\" : \"medium\",\n\t\trelevance: (contextFile.isGlobal ? 0.9 : 0.6) * 0.4 + queryScore * 0.6,\n\t\tredundancyKey: contextFile.path,\n\t\tsourceRef: {\n\t\t\turi: pathToFileURL(contextFile.path).href,\n\t\t\tcontentHash: fnv1aHex(contextFile.content),\n\t\t\tretrievable: true,\n\t\t},\n\t\ttext: renderContextFull(contextFile, tagName, excerptChars),\n\t});\n}\n\nfunction pushSkillItems(\n\titems: ContextBudgetItemV2[],\n\tvisibleSkills: readonly Skill[],\n\toptions: SystemPromptBudgetItemInput[\"options\"],\n): void {\n\titems.push({ id: \"skill-header\", tier: \"skills\", priority: \"hard\", text: renderSkillHeader() });\n\tconst activeSkillNames = new Set(options.activeSkillNames ?? []);\n\tconst maxInactive = options.maxInactiveSkills ?? MAX_INACTIVE_SKILLS;\n\tfor (const skill of visibleSkills.filter((skill) => activeSkillNames.has(skill.name))) {\n\t\titems.push(renderSkillBudgetItem(skill, \"hard\", 1));\n\t}\n\n\tconst sortedInactive = visibleSkills\n\t\t.filter((skill) => !activeSkillNames.has(skill.name))\n\t\t.sort((a, b) => scoreSkillRelevance(b, options.queryContext) - scoreSkillRelevance(a, options.queryContext));\n\tfor (const skill of sortedInactive.slice(0, maxInactive)) {\n\t\titems.push(renderSkillBudgetItem(skill, \"low\", Math.max(0.25, scoreSkillRelevance(skill, options.queryContext))));\n\t}\n\titems.push({ id: \"skill-footer\", tier: \"skills\", priority: \"hard\", text: \"</available_skills>\" });\n\n\tconst omittedInactiveCount = sortedInactive.length - Math.min(sortedInactive.length, maxInactive);\n\tif (omittedInactiveCount > 0) {\n\t\titems.push({\n\t\t\tid: \"skill-omitted-summary\",\n\t\t\ttier: \"skills\",\n\t\t\tpriority: \"low\",\n\t\t\trelevance: 0.1,\n\t\t\ttext: `<!-- ${omittedInactiveCount} additional skills available. Use 'read' to load a specific skill file when a task matches. -->`,\n\t\t});\n\t}\n}\n\nfunction renderSkillBudgetItem(skill: Skill, priority: \"hard\" | \"low\", relevance: number): ContextBudgetItemV2 {\n\treturn {\n\t\tid: `skill:${skill.name}`,\n\t\ttier: \"skills\",\n\t\tpriority,\n\t\trelevance,\n\t\tredundancyKey: skill.name,\n\t\ttext: renderSkillEntry(skill, priority === \"hard\"),\n\t};\n}\n\nfunction computeExcerptChars(resourceBudget: number, contextFullCount: number): number {\n\tif (contextFullCount <= 0) {\n\t\treturn CONTEXT_EXCERPT_CHARS;\n\t}\n\tconst allocatedTokens = Math.floor(resourceBudget * CONTEXT_FULL_BUDGET_RATIO) / contextFullCount;\n\tconst estimatedChars = Math.floor(allocatedTokens * 4);\n\treturn Math.max(MIN_EXCERPT_CHARS, Math.min(MAX_EXCERPT_CHARS, estimatedChars));\n}\n\nfunction renderContextPointer(contextFile: ContextFile, scope: string): string {\n\tconst marker = contextFile.containsJailbreak ? \" sanitized=true\" : \"\";\n\treturn `<context_file_pointer scope=\"${escapeXml(scope)}\" path=\"${escapeXml(contextFile.path)}\"${marker} chars=\"${contextFile.content.length}\" />`;\n}\n\nfunction renderContextFull(contextFile: ContextFile, tagName: string, excerptChars = CONTEXT_EXCERPT_CHARS): string {\n\tconst content =\n\t\tcontextFile.content.length > excerptChars\n\t\t\t? boundedExcerpt(contextFile.content, excerptChars)\n\t\t\t: contextFile.content;\n\tconst truncated = content.length < contextFile.content.length ? \" truncated=true\" : \"\";\n\treturn `<${tagName} path=\"${escapeXml(contextFile.path)}\"${truncated}>\\n${escapeXmlText(content)}\\n</${tagName}>`;\n}\n\nfunction boundedExcerpt(content: string, excerptChars = CONTEXT_EXCERPT_CHARS): string {\n\tconst headChars = Math.floor(excerptChars * 0.7);\n\tconst tailChars = excerptChars - headChars;\n\tlet head = content.slice(0, headChars);\n\tconst lastSpace = head.search(/\\s+\\S*$/);\n\tif (lastSpace > headChars * 0.5) {\n\t\thead = head.slice(0, lastSpace);\n\t}\n\tlet tail = content.slice(-tailChars);\n\tconst firstSpace = tail.search(/\\S+\\s+/);\n\tif (firstSpace > 0 && firstSpace < tailChars * 0.5) {\n\t\ttail = tail.slice(firstSpace).trimStart();\n\t}\n\treturn `${head}\\n\\n[...context-budget excerpt omitted ${content.length - excerptChars} chars...]\\n\\n${tail}`;\n}\n\nfunction renderSkillHeader(): string {\n\treturn [\n\t\t\"\\n\\nThe following skills provide specialized instructions for specific tasks.\",\n\t\t\"Use the read tool to load a skill's file when the task matches its description.\",\n\t\t\"When a skill file references a relative path, resolve it against the skill directory (parent of SKILL.md / dirname of the path) and use that absolute path in tool commands.\",\n\t\t\"\",\n\t\t\"<available_skills>\",\n\t].join(\"\\n\");\n}\n\nfunction renderSkillEntry(skill: Skill, active = false): string {\n\treturn [\n\t\t\" <skill>\",\n\t\t` <name>${escapeXml(skill.name)}</name>`,\n\t\t` <description>${escapeXml(skill.description)}</description>`,\n\t\t...(active ? [\" <activation>active</activation>\"] : []),\n\t\t` <location>${escapeXml(skill.filePath)}</location>`,\n\t\t\" </skill>\",\n\t].join(\"\\n\");\n}\n\nexport function escapeXmlText(str: string): string {\n\treturn str.replace(/&/g, \"&amp;\").replace(/</g, \"&lt;\").replace(/>/g, \"&gt;\");\n}\n\nexport function escapeXml(str: string): string {\n\treturn escapeXmlText(str).replace(/\"/g, \"&quot;\").replace(/'/g, \"&apos;\");\n}\n"]}
@@ -14,7 +14,8 @@ export function createSystemPromptBudgetItems(input, resourceBudget) {
14
14
  const queryScore = scoreContextFileRelevance({ path: contextFile.path, content: contextFile.content, isGlobal: contextFile.isGlobal ?? false }, input.options.queryContext);
15
15
  pushContextItems(items, contextFile, queryScore, excerptChars, input.options.includeFullContextFiles !== false);
16
16
  }
17
- const visibleSkills = input.skills.filter((skill) => !skill.disableModelInvocation);
17
+ const activeSkillNames = new Set(input.options.activeSkillNames ?? []);
18
+ const visibleSkills = input.skills.filter((skill) => !skill.disableModelInvocation || activeSkillNames.has(skill.name));
18
19
  if (input.includeSkills && input.options.includeSkillInventory !== false && visibleSkills.length > 0) {
19
20
  pushSkillItems(items, visibleSkills, input.options);
20
21
  }
@@ -78,7 +79,7 @@ function renderSkillBudgetItem(skill, priority, relevance) {
78
79
  priority,
79
80
  relevance,
80
81
  redundancyKey: skill.name,
81
- text: renderSkillEntry(skill),
82
+ text: renderSkillEntry(skill, priority === "hard"),
82
83
  };
83
84
  }
84
85
  function computeExcerptChars(resourceBudget, contextFullCount) {
@@ -124,11 +125,12 @@ function renderSkillHeader() {
124
125
  "<available_skills>",
125
126
  ].join("\n");
126
127
  }
127
- function renderSkillEntry(skill) {
128
+ function renderSkillEntry(skill, active = false) {
128
129
  return [
129
130
  " <skill>",
130
131
  ` <name>${escapeXml(skill.name)}</name>`,
131
132
  ` <description>${escapeXml(skill.description)}</description>`,
133
+ ...(active ? [" <activation>active</activation>"] : []),
132
134
  ` <location>${escapeXml(skill.filePath)}</location>`,
133
135
  " </skill>",
134
136
  ].join("\n");
@@ -1 +1 @@
1
- {"version":3,"file":"context-budget-system-prompt-items.js","sourceRoot":"","sources":["../../src/core/context-budget-system-prompt-items.ts"],"names":[],"mappings":"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","sourcesContent":["import { pathToFileURL } from \"node:url\";\nimport { type ContextBudgetItemV2, fnv1aHex } from \"./context-budget-governor-v2.ts\";\nimport { scoreContextFileRelevance, scoreSkillRelevance } from \"./context-budget-relevance.ts\";\nimport type { ContextFile } from \"./resource-loader.ts\";\nimport type { Skill } from \"./skills.ts\";\n\ninterface SystemPromptBudgetItemInput {\n\treadonly contextFiles: readonly ContextFile[];\n\treadonly skills: readonly Skill[];\n\treadonly includeSkills: boolean;\n\treadonly options: {\n\t\treadonly activeSkillNames?: readonly string[];\n\t\treadonly includeSkillInventory?: boolean;\n\t\treadonly includeFullContextFiles?: boolean;\n\t\treadonly maxInactiveSkills?: number;\n\t\treadonly queryContext?: string;\n\t};\n}\n\nconst CONTEXT_EXCERPT_CHARS = 1200;\nconst MIN_EXCERPT_CHARS = 400;\nconst MAX_EXCERPT_CHARS = 3000;\nconst MAX_INACTIVE_SKILLS = 15;\nconst CONTEXT_FULL_BUDGET_RATIO = 0.5;\n\nexport function createSystemPromptBudgetItems(\n\tinput: SystemPromptBudgetItemInput,\n\tresourceBudget: number,\n): ContextBudgetItemV2[] {\n\tconst items: ContextBudgetItemV2[] = [];\n\tconst contextFullCount = input.options.includeFullContextFiles !== false ? input.contextFiles.length : 0;\n\tconst excerptChars = computeExcerptChars(resourceBudget, contextFullCount);\n\n\tfor (const contextFile of input.contextFiles) {\n\t\tconst queryScore = scoreContextFileRelevance(\n\t\t\t{ path: contextFile.path, content: contextFile.content, isGlobal: contextFile.isGlobal ?? false },\n\t\t\tinput.options.queryContext,\n\t\t);\n\t\tpushContextItems(items, contextFile, queryScore, excerptChars, input.options.includeFullContextFiles !== false);\n\t}\n\n\tconst visibleSkills = input.skills.filter((skill) => !skill.disableModelInvocation);\n\tif (input.includeSkills && input.options.includeSkillInventory !== false && visibleSkills.length > 0) {\n\t\tpushSkillItems(items, visibleSkills, input.options);\n\t}\n\treturn items;\n}\n\nfunction pushContextItems(\n\titems: ContextBudgetItemV2[],\n\tcontextFile: ContextFile,\n\tqueryScore: number,\n\texcerptChars: number,\n\tincludeFullContextFiles: boolean,\n): void {\n\tconst scope = contextFile.isGlobal ? \"parent\" : \"project\";\n\tconst tagName = contextFile.isGlobal ? \"parent_instructions\" : \"project_instructions\";\n\titems.push({\n\t\tid: `context-pointer:${contextFile.path}`,\n\t\ttier: contextFile.isGlobal ? \"system\" : \"current-files\",\n\t\tpriority: contextFile.isGlobal ? \"hard\" : \"high\",\n\t\trelevance: (contextFile.isGlobal ? 1 : 0.8) * 0.4 + queryScore * 0.6,\n\t\ttext: renderContextPointer(contextFile, scope),\n\t});\n\tif (!includeFullContextFiles) return;\n\titems.push({\n\t\tid: `context-full:${contextFile.path}`,\n\t\ttier: contextFile.isGlobal ? \"system\" : \"current-files\",\n\t\tpriority: contextFile.isGlobal ? \"high\" : \"medium\",\n\t\trelevance: (contextFile.isGlobal ? 0.9 : 0.6) * 0.4 + queryScore * 0.6,\n\t\tredundancyKey: contextFile.path,\n\t\tsourceRef: {\n\t\t\turi: pathToFileURL(contextFile.path).href,\n\t\t\tcontentHash: fnv1aHex(contextFile.content),\n\t\t\tretrievable: true,\n\t\t},\n\t\ttext: renderContextFull(contextFile, tagName, excerptChars),\n\t});\n}\n\nfunction pushSkillItems(\n\titems: ContextBudgetItemV2[],\n\tvisibleSkills: readonly Skill[],\n\toptions: SystemPromptBudgetItemInput[\"options\"],\n): void {\n\titems.push({ id: \"skill-header\", tier: \"skills\", priority: \"hard\", text: renderSkillHeader() });\n\tconst activeSkillNames = new Set(options.activeSkillNames ?? []);\n\tconst maxInactive = options.maxInactiveSkills ?? MAX_INACTIVE_SKILLS;\n\tfor (const skill of visibleSkills.filter((skill) => activeSkillNames.has(skill.name))) {\n\t\titems.push(renderSkillBudgetItem(skill, \"hard\", 1));\n\t}\n\n\tconst sortedInactive = visibleSkills\n\t\t.filter((skill) => !activeSkillNames.has(skill.name))\n\t\t.sort((a, b) => scoreSkillRelevance(b, options.queryContext) - scoreSkillRelevance(a, options.queryContext));\n\tfor (const skill of sortedInactive.slice(0, maxInactive)) {\n\t\titems.push(renderSkillBudgetItem(skill, \"low\", Math.max(0.25, scoreSkillRelevance(skill, options.queryContext))));\n\t}\n\titems.push({ id: \"skill-footer\", tier: \"skills\", priority: \"hard\", text: \"</available_skills>\" });\n\n\tconst omittedInactiveCount = sortedInactive.length - Math.min(sortedInactive.length, maxInactive);\n\tif (omittedInactiveCount > 0) {\n\t\titems.push({\n\t\t\tid: \"skill-omitted-summary\",\n\t\t\ttier: \"skills\",\n\t\t\tpriority: \"low\",\n\t\t\trelevance: 0.1,\n\t\t\ttext: `<!-- ${omittedInactiveCount} additional skills available. Use 'read' to load a specific skill file when a task matches. -->`,\n\t\t});\n\t}\n}\n\nfunction renderSkillBudgetItem(skill: Skill, priority: \"hard\" | \"low\", relevance: number): ContextBudgetItemV2 {\n\treturn {\n\t\tid: `skill:${skill.name}`,\n\t\ttier: \"skills\",\n\t\tpriority,\n\t\trelevance,\n\t\tredundancyKey: skill.name,\n\t\ttext: renderSkillEntry(skill),\n\t};\n}\n\nfunction computeExcerptChars(resourceBudget: number, contextFullCount: number): number {\n\tif (contextFullCount <= 0) {\n\t\treturn CONTEXT_EXCERPT_CHARS;\n\t}\n\tconst allocatedTokens = Math.floor(resourceBudget * CONTEXT_FULL_BUDGET_RATIO) / contextFullCount;\n\tconst estimatedChars = Math.floor(allocatedTokens * 4);\n\treturn Math.max(MIN_EXCERPT_CHARS, Math.min(MAX_EXCERPT_CHARS, estimatedChars));\n}\n\nfunction renderContextPointer(contextFile: ContextFile, scope: string): string {\n\tconst marker = contextFile.containsJailbreak ? \" sanitized=true\" : \"\";\n\treturn `<context_file_pointer scope=\"${escapeXml(scope)}\" path=\"${escapeXml(contextFile.path)}\"${marker} chars=\"${contextFile.content.length}\" />`;\n}\n\nfunction renderContextFull(contextFile: ContextFile, tagName: string, excerptChars = CONTEXT_EXCERPT_CHARS): string {\n\tconst content =\n\t\tcontextFile.content.length > excerptChars\n\t\t\t? boundedExcerpt(contextFile.content, excerptChars)\n\t\t\t: contextFile.content;\n\tconst truncated = content.length < contextFile.content.length ? \" truncated=true\" : \"\";\n\treturn `<${tagName} path=\"${escapeXml(contextFile.path)}\"${truncated}>\\n${escapeXmlText(content)}\\n</${tagName}>`;\n}\n\nfunction boundedExcerpt(content: string, excerptChars = CONTEXT_EXCERPT_CHARS): string {\n\tconst headChars = Math.floor(excerptChars * 0.7);\n\tconst tailChars = excerptChars - headChars;\n\tlet head = content.slice(0, headChars);\n\tconst lastSpace = head.search(/\\s+\\S*$/);\n\tif (lastSpace > headChars * 0.5) {\n\t\thead = head.slice(0, lastSpace);\n\t}\n\tlet tail = content.slice(-tailChars);\n\tconst firstSpace = tail.search(/\\S+\\s+/);\n\tif (firstSpace > 0 && firstSpace < tailChars * 0.5) {\n\t\ttail = tail.slice(firstSpace).trimStart();\n\t}\n\treturn `${head}\\n\\n[...context-budget excerpt omitted ${content.length - excerptChars} chars...]\\n\\n${tail}`;\n}\n\nfunction renderSkillHeader(): string {\n\treturn [\n\t\t\"\\n\\nThe following skills provide specialized instructions for specific tasks.\",\n\t\t\"Use the read tool to load a skill's file when the task matches its description.\",\n\t\t\"When a skill file references a relative path, resolve it against the skill directory (parent of SKILL.md / dirname of the path) and use that absolute path in tool commands.\",\n\t\t\"\",\n\t\t\"<available_skills>\",\n\t].join(\"\\n\");\n}\n\nfunction renderSkillEntry(skill: Skill): string {\n\treturn [\n\t\t\" <skill>\",\n\t\t` <name>${escapeXml(skill.name)}</name>`,\n\t\t` <description>${escapeXml(skill.description)}</description>`,\n\t\t` <location>${escapeXml(skill.filePath)}</location>`,\n\t\t\" </skill>\",\n\t].join(\"\\n\");\n}\n\nexport function escapeXmlText(str: string): string {\n\treturn str.replace(/&/g, \"&amp;\").replace(/</g, \"&lt;\").replace(/>/g, \"&gt;\");\n}\n\nexport function escapeXml(str: string): string {\n\treturn escapeXmlText(str).replace(/\"/g, \"&quot;\").replace(/'/g, \"&apos;\");\n}\n"]}
1
+ {"version":3,"file":"context-budget-system-prompt-items.js","sourceRoot":"","sources":["../../src/core/context-budget-system-prompt-items.ts"],"names":[],"mappings":"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","sourcesContent":["import { pathToFileURL } from \"node:url\";\nimport { type ContextBudgetItemV2, fnv1aHex } from \"./context-budget-governor-v2.ts\";\nimport { scoreContextFileRelevance, scoreSkillRelevance } from \"./context-budget-relevance.ts\";\nimport type { ContextFile } from \"./resource-loader.ts\";\nimport type { Skill } from \"./skills.ts\";\n\ninterface SystemPromptBudgetItemInput {\n\treadonly contextFiles: readonly ContextFile[];\n\treadonly skills: readonly Skill[];\n\treadonly includeSkills: boolean;\n\treadonly options: {\n\t\treadonly activeSkillNames?: readonly string[];\n\t\treadonly includeSkillInventory?: boolean;\n\t\treadonly includeFullContextFiles?: boolean;\n\t\treadonly maxInactiveSkills?: number;\n\t\treadonly queryContext?: string;\n\t};\n}\n\nconst CONTEXT_EXCERPT_CHARS = 1200;\nconst MIN_EXCERPT_CHARS = 400;\nconst MAX_EXCERPT_CHARS = 3000;\nconst MAX_INACTIVE_SKILLS = 15;\nconst CONTEXT_FULL_BUDGET_RATIO = 0.5;\n\nexport function createSystemPromptBudgetItems(\n\tinput: SystemPromptBudgetItemInput,\n\tresourceBudget: number,\n): ContextBudgetItemV2[] {\n\tconst items: ContextBudgetItemV2[] = [];\n\tconst contextFullCount = input.options.includeFullContextFiles !== false ? input.contextFiles.length : 0;\n\tconst excerptChars = computeExcerptChars(resourceBudget, contextFullCount);\n\n\tfor (const contextFile of input.contextFiles) {\n\t\tconst queryScore = scoreContextFileRelevance(\n\t\t\t{ path: contextFile.path, content: contextFile.content, isGlobal: contextFile.isGlobal ?? false },\n\t\t\tinput.options.queryContext,\n\t\t);\n\t\tpushContextItems(items, contextFile, queryScore, excerptChars, input.options.includeFullContextFiles !== false);\n\t}\n\n\tconst activeSkillNames = new Set(input.options.activeSkillNames ?? []);\n\tconst visibleSkills = input.skills.filter(\n\t\t(skill) => !skill.disableModelInvocation || activeSkillNames.has(skill.name),\n\t);\n\tif (input.includeSkills && input.options.includeSkillInventory !== false && visibleSkills.length > 0) {\n\t\tpushSkillItems(items, visibleSkills, input.options);\n\t}\n\treturn items;\n}\n\nfunction pushContextItems(\n\titems: ContextBudgetItemV2[],\n\tcontextFile: ContextFile,\n\tqueryScore: number,\n\texcerptChars: number,\n\tincludeFullContextFiles: boolean,\n): void {\n\tconst scope = contextFile.isGlobal ? \"parent\" : \"project\";\n\tconst tagName = contextFile.isGlobal ? \"parent_instructions\" : \"project_instructions\";\n\titems.push({\n\t\tid: `context-pointer:${contextFile.path}`,\n\t\ttier: contextFile.isGlobal ? \"system\" : \"current-files\",\n\t\tpriority: contextFile.isGlobal ? \"hard\" : \"high\",\n\t\trelevance: (contextFile.isGlobal ? 1 : 0.8) * 0.4 + queryScore * 0.6,\n\t\ttext: renderContextPointer(contextFile, scope),\n\t});\n\tif (!includeFullContextFiles) return;\n\titems.push({\n\t\tid: `context-full:${contextFile.path}`,\n\t\ttier: contextFile.isGlobal ? \"system\" : \"current-files\",\n\t\tpriority: contextFile.isGlobal ? \"high\" : \"medium\",\n\t\trelevance: (contextFile.isGlobal ? 0.9 : 0.6) * 0.4 + queryScore * 0.6,\n\t\tredundancyKey: contextFile.path,\n\t\tsourceRef: {\n\t\t\turi: pathToFileURL(contextFile.path).href,\n\t\t\tcontentHash: fnv1aHex(contextFile.content),\n\t\t\tretrievable: true,\n\t\t},\n\t\ttext: renderContextFull(contextFile, tagName, excerptChars),\n\t});\n}\n\nfunction pushSkillItems(\n\titems: ContextBudgetItemV2[],\n\tvisibleSkills: readonly Skill[],\n\toptions: SystemPromptBudgetItemInput[\"options\"],\n): void {\n\titems.push({ id: \"skill-header\", tier: \"skills\", priority: \"hard\", text: renderSkillHeader() });\n\tconst activeSkillNames = new Set(options.activeSkillNames ?? []);\n\tconst maxInactive = options.maxInactiveSkills ?? MAX_INACTIVE_SKILLS;\n\tfor (const skill of visibleSkills.filter((skill) => activeSkillNames.has(skill.name))) {\n\t\titems.push(renderSkillBudgetItem(skill, \"hard\", 1));\n\t}\n\n\tconst sortedInactive = visibleSkills\n\t\t.filter((skill) => !activeSkillNames.has(skill.name))\n\t\t.sort((a, b) => scoreSkillRelevance(b, options.queryContext) - scoreSkillRelevance(a, options.queryContext));\n\tfor (const skill of sortedInactive.slice(0, maxInactive)) {\n\t\titems.push(renderSkillBudgetItem(skill, \"low\", Math.max(0.25, scoreSkillRelevance(skill, options.queryContext))));\n\t}\n\titems.push({ id: \"skill-footer\", tier: \"skills\", priority: \"hard\", text: \"</available_skills>\" });\n\n\tconst omittedInactiveCount = sortedInactive.length - Math.min(sortedInactive.length, maxInactive);\n\tif (omittedInactiveCount > 0) {\n\t\titems.push({\n\t\t\tid: \"skill-omitted-summary\",\n\t\t\ttier: \"skills\",\n\t\t\tpriority: \"low\",\n\t\t\trelevance: 0.1,\n\t\t\ttext: `<!-- ${omittedInactiveCount} additional skills available. Use 'read' to load a specific skill file when a task matches. -->`,\n\t\t});\n\t}\n}\n\nfunction renderSkillBudgetItem(skill: Skill, priority: \"hard\" | \"low\", relevance: number): ContextBudgetItemV2 {\n\treturn {\n\t\tid: `skill:${skill.name}`,\n\t\ttier: \"skills\",\n\t\tpriority,\n\t\trelevance,\n\t\tredundancyKey: skill.name,\n\t\ttext: renderSkillEntry(skill, priority === \"hard\"),\n\t};\n}\n\nfunction computeExcerptChars(resourceBudget: number, contextFullCount: number): number {\n\tif (contextFullCount <= 0) {\n\t\treturn CONTEXT_EXCERPT_CHARS;\n\t}\n\tconst allocatedTokens = Math.floor(resourceBudget * CONTEXT_FULL_BUDGET_RATIO) / contextFullCount;\n\tconst estimatedChars = Math.floor(allocatedTokens * 4);\n\treturn Math.max(MIN_EXCERPT_CHARS, Math.min(MAX_EXCERPT_CHARS, estimatedChars));\n}\n\nfunction renderContextPointer(contextFile: ContextFile, scope: string): string {\n\tconst marker = contextFile.containsJailbreak ? \" sanitized=true\" : \"\";\n\treturn `<context_file_pointer scope=\"${escapeXml(scope)}\" path=\"${escapeXml(contextFile.path)}\"${marker} chars=\"${contextFile.content.length}\" />`;\n}\n\nfunction renderContextFull(contextFile: ContextFile, tagName: string, excerptChars = CONTEXT_EXCERPT_CHARS): string {\n\tconst content =\n\t\tcontextFile.content.length > excerptChars\n\t\t\t? boundedExcerpt(contextFile.content, excerptChars)\n\t\t\t: contextFile.content;\n\tconst truncated = content.length < contextFile.content.length ? \" truncated=true\" : \"\";\n\treturn `<${tagName} path=\"${escapeXml(contextFile.path)}\"${truncated}>\\n${escapeXmlText(content)}\\n</${tagName}>`;\n}\n\nfunction boundedExcerpt(content: string, excerptChars = CONTEXT_EXCERPT_CHARS): string {\n\tconst headChars = Math.floor(excerptChars * 0.7);\n\tconst tailChars = excerptChars - headChars;\n\tlet head = content.slice(0, headChars);\n\tconst lastSpace = head.search(/\\s+\\S*$/);\n\tif (lastSpace > headChars * 0.5) {\n\t\thead = head.slice(0, lastSpace);\n\t}\n\tlet tail = content.slice(-tailChars);\n\tconst firstSpace = tail.search(/\\S+\\s+/);\n\tif (firstSpace > 0 && firstSpace < tailChars * 0.5) {\n\t\ttail = tail.slice(firstSpace).trimStart();\n\t}\n\treturn `${head}\\n\\n[...context-budget excerpt omitted ${content.length - excerptChars} chars...]\\n\\n${tail}`;\n}\n\nfunction renderSkillHeader(): string {\n\treturn [\n\t\t\"\\n\\nThe following skills provide specialized instructions for specific tasks.\",\n\t\t\"Use the read tool to load a skill's file when the task matches its description.\",\n\t\t\"When a skill file references a relative path, resolve it against the skill directory (parent of SKILL.md / dirname of the path) and use that absolute path in tool commands.\",\n\t\t\"\",\n\t\t\"<available_skills>\",\n\t].join(\"\\n\");\n}\n\nfunction renderSkillEntry(skill: Skill, active = false): string {\n\treturn [\n\t\t\" <skill>\",\n\t\t` <name>${escapeXml(skill.name)}</name>`,\n\t\t` <description>${escapeXml(skill.description)}</description>`,\n\t\t...(active ? [\" <activation>active</activation>\"] : []),\n\t\t` <location>${escapeXml(skill.filePath)}</location>`,\n\t\t\" </skill>\",\n\t].join(\"\\n\");\n}\n\nexport function escapeXmlText(str: string): string {\n\treturn str.replace(/&/g, \"&amp;\").replace(/</g, \"&lt;\").replace(/>/g, \"&gt;\");\n}\n\nexport function escapeXml(str: string): string {\n\treturn escapeXmlText(str).replace(/\"/g, \"&quot;\").replace(/'/g, \"&apos;\");\n}\n"]}