open-multi-agent-kit 0.97.0 → 0.98.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (261) hide show
  1. package/CHANGELOG.md +36 -1
  2. package/README.md +41 -9
  3. package/dist/cli/args.d.ts +2 -3
  4. package/dist/cli/args.d.ts.map +1 -1
  5. package/dist/cli/args.js +13 -189
  6. package/dist/cli/args.js.map +1 -1
  7. package/dist/cli/help.d.ts +3 -0
  8. package/dist/cli/help.d.ts.map +1 -0
  9. package/dist/cli/help.js +194 -0
  10. package/dist/cli/help.js.map +1 -0
  11. package/dist/cli/initial-message.d.ts.map +1 -1
  12. package/dist/cli/initial-message.js +2 -1
  13. package/dist/cli/initial-message.js.map +1 -1
  14. package/dist/cli/resource-paths.d.ts +4 -0
  15. package/dist/cli/resource-paths.d.ts.map +1 -0
  16. package/dist/cli/resource-paths.js +59 -0
  17. package/dist/cli/resource-paths.js.map +1 -0
  18. package/dist/commands/adaptorch-doctor-cli.d.ts +13 -0
  19. package/dist/commands/adaptorch-doctor-cli.d.ts.map +1 -0
  20. package/dist/commands/adaptorch-doctor-cli.js +115 -0
  21. package/dist/commands/adaptorch-doctor-cli.js.map +1 -0
  22. package/dist/commands/resource-doctor-cli.d.ts +4 -0
  23. package/dist/commands/resource-doctor-cli.d.ts.map +1 -1
  24. package/dist/commands/resource-doctor-cli.js +35 -5
  25. package/dist/commands/resource-doctor-cli.js.map +1 -1
  26. package/dist/core/adaptorch-bridge.d.ts +11 -13
  27. package/dist/core/adaptorch-bridge.d.ts.map +1 -1
  28. package/dist/core/adaptorch-bridge.js +11 -13
  29. package/dist/core/adaptorch-bridge.js.map +1 -1
  30. package/dist/core/agent-session-snapshot.d.ts +16 -0
  31. package/dist/core/agent-session-snapshot.d.ts.map +1 -0
  32. package/dist/core/agent-session-snapshot.js +111 -0
  33. package/dist/core/agent-session-snapshot.js.map +1 -0
  34. package/dist/core/agent-session.d.ts +5 -9
  35. package/dist/core/agent-session.d.ts.map +1 -1
  36. package/dist/core/agent-session.js +27 -219
  37. package/dist/core/agent-session.js.map +1 -1
  38. package/dist/core/agent-tool-settings.d.ts +10 -0
  39. package/dist/core/agent-tool-settings.d.ts.map +1 -1
  40. package/dist/core/agent-tool-settings.js +10 -1
  41. package/dist/core/agent-tool-settings.js.map +1 -1
  42. package/dist/core/compaction/compaction.d.ts +7 -0
  43. package/dist/core/compaction/compaction.d.ts.map +1 -1
  44. package/dist/core/compaction/compaction.js +25 -61
  45. package/dist/core/compaction/compaction.js.map +1 -1
  46. package/dist/core/compaction/index.d.ts +1 -0
  47. package/dist/core/compaction/index.d.ts.map +1 -1
  48. package/dist/core/compaction/index.js +1 -0
  49. package/dist/core/compaction/index.js.map +1 -1
  50. package/dist/core/compaction/knowledge-triage-format.d.ts +9 -0
  51. package/dist/core/compaction/knowledge-triage-format.d.ts.map +1 -0
  52. package/dist/core/compaction/knowledge-triage-format.js +71 -0
  53. package/dist/core/compaction/knowledge-triage-format.js.map +1 -0
  54. package/dist/core/compaction/knowledge-triage-types.d.ts +7 -0
  55. package/dist/core/compaction/knowledge-triage-types.d.ts.map +1 -0
  56. package/dist/core/compaction/knowledge-triage-types.js +2 -0
  57. package/dist/core/compaction/knowledge-triage-types.js.map +1 -0
  58. package/dist/core/compaction/knowledge-triage.d.ts +29 -0
  59. package/dist/core/compaction/knowledge-triage.d.ts.map +1 -0
  60. package/dist/core/compaction/knowledge-triage.js +189 -0
  61. package/dist/core/compaction/knowledge-triage.js.map +1 -0
  62. package/dist/core/compaction/transaction.d.ts.map +1 -1
  63. package/dist/core/compaction/transaction.js +1 -1
  64. package/dist/core/compaction/transaction.js.map +1 -1
  65. package/dist/core/compaction/utils.d.ts +4 -0
  66. package/dist/core/compaction/utils.d.ts.map +1 -1
  67. package/dist/core/compaction/utils.js +37 -0
  68. package/dist/core/compaction/utils.js.map +1 -1
  69. package/dist/core/completion-sound-io.d.ts +14 -0
  70. package/dist/core/completion-sound-io.d.ts.map +1 -0
  71. package/dist/core/completion-sound-io.js +64 -0
  72. package/dist/core/completion-sound-io.js.map +1 -0
  73. package/dist/core/completion-sound.d.ts +4 -10
  74. package/dist/core/completion-sound.d.ts.map +1 -1
  75. package/dist/core/completion-sound.js +28 -58
  76. package/dist/core/completion-sound.js.map +1 -1
  77. package/dist/core/context-budget-relevance.d.ts +15 -2
  78. package/dist/core/context-budget-relevance.d.ts.map +1 -1
  79. package/dist/core/context-budget-relevance.js +21 -9
  80. package/dist/core/context-budget-relevance.js.map +1 -1
  81. package/dist/core/context-budget-system-prompt-items.d.ts +1 -1
  82. package/dist/core/context-budget-system-prompt-items.d.ts.map +1 -1
  83. package/dist/core/context-budget-system-prompt-items.js.map +1 -1
  84. package/dist/core/context-budget-system-prompt.d.ts +1 -1
  85. package/dist/core/context-budget-system-prompt.d.ts.map +1 -1
  86. package/dist/core/context-budget-system-prompt.js.map +1 -1
  87. package/dist/core/context-file.d.ts +23 -0
  88. package/dist/core/context-file.d.ts.map +1 -0
  89. package/dist/core/context-file.js +17 -0
  90. package/dist/core/context-file.js.map +1 -0
  91. package/dist/core/keybindings.d.ts +1 -1
  92. package/dist/core/keybindings.d.ts.map +1 -1
  93. package/dist/core/keybindings.js +6 -1
  94. package/dist/core/keybindings.js.map +1 -1
  95. package/dist/core/model-prompt-policy.d.ts +1 -1
  96. package/dist/core/model-prompt-policy.d.ts.map +1 -1
  97. package/dist/core/model-prompt-policy.js.map +1 -1
  98. package/dist/core/prompt-settlement.d.ts +6 -3
  99. package/dist/core/prompt-settlement.d.ts.map +1 -1
  100. package/dist/core/prompt-settlement.js +10 -14
  101. package/dist/core/prompt-settlement.js.map +1 -1
  102. package/dist/core/reasoning-router-promotion.d.ts +41 -1
  103. package/dist/core/reasoning-router-promotion.d.ts.map +1 -1
  104. package/dist/core/reasoning-router-promotion.js +31 -2
  105. package/dist/core/reasoning-router-promotion.js.map +1 -1
  106. package/dist/core/reasoning-router-replay-stability.d.ts +65 -0
  107. package/dist/core/reasoning-router-replay-stability.d.ts.map +1 -0
  108. package/dist/core/reasoning-router-replay-stability.js +67 -0
  109. package/dist/core/reasoning-router-replay-stability.js.map +1 -0
  110. package/dist/core/reasoning-router-v4-weights.d.ts +6 -6
  111. package/dist/core/reasoning-router-v4-weights.d.ts.map +1 -1
  112. package/dist/core/reasoning-router-v4-weights.js +3 -3
  113. package/dist/core/reasoning-router-v4-weights.js.map +1 -1
  114. package/dist/core/reasoning-router-v4.d.ts.map +1 -1
  115. package/dist/core/reasoning-router-v4.js +6 -6
  116. package/dist/core/reasoning-router-v4.js.map +1 -1
  117. package/dist/core/redaction.d.ts.map +1 -1
  118. package/dist/core/redaction.js +1 -1
  119. package/dist/core/redaction.js.map +1 -1
  120. package/dist/core/resource-admission.d.ts +2 -1
  121. package/dist/core/resource-admission.d.ts.map +1 -1
  122. package/dist/core/resource-admission.js +11 -0
  123. package/dist/core/resource-admission.js.map +1 -1
  124. package/dist/core/resource-governor-format.d.ts +1 -1
  125. package/dist/core/resource-governor-format.d.ts.map +1 -1
  126. package/dist/core/resource-governor-format.js.map +1 -1
  127. package/dist/core/resource-governor-settings.d.ts +6 -12
  128. package/dist/core/resource-governor-settings.d.ts.map +1 -1
  129. package/dist/core/resource-governor-settings.js.map +1 -1
  130. package/dist/core/resource-governor-types.d.ts +24 -0
  131. package/dist/core/resource-governor-types.d.ts.map +1 -0
  132. package/dist/core/resource-governor-types.js +15 -0
  133. package/dist/core/resource-governor-types.js.map +1 -0
  134. package/dist/core/resource-loader.d.ts +1 -7
  135. package/dist/core/resource-loader.d.ts.map +1 -1
  136. package/dist/core/resource-loader.js.map +1 -1
  137. package/dist/core/resource-observation-journal.d.ts +2 -1
  138. package/dist/core/resource-observation-journal.d.ts.map +1 -1
  139. package/dist/core/resource-observation-journal.js +4 -3
  140. package/dist/core/resource-observation-journal.js.map +1 -1
  141. package/dist/core/resource-observation-report-reader.d.ts +7 -0
  142. package/dist/core/resource-observation-report-reader.d.ts.map +1 -0
  143. package/dist/core/resource-observation-report-reader.js +157 -0
  144. package/dist/core/resource-observation-report-reader.js.map +1 -0
  145. package/dist/core/resource-observation-report.d.ts +24 -0
  146. package/dist/core/resource-observation-report.d.ts.map +1 -0
  147. package/dist/core/resource-observation-report.js +101 -0
  148. package/dist/core/resource-observation-report.js.map +1 -0
  149. package/dist/core/router-feedback-collector.d.ts +1 -1
  150. package/dist/core/router-feedback-collector.d.ts.map +1 -1
  151. package/dist/core/router-feedback-collector.js.map +1 -1
  152. package/dist/core/session-failure-cause.d.ts +33 -0
  153. package/dist/core/session-failure-cause.d.ts.map +1 -0
  154. package/dist/core/session-failure-cause.js +141 -0
  155. package/dist/core/session-failure-cause.js.map +1 -0
  156. package/dist/core/session-system-prompt.d.ts +1 -1
  157. package/dist/core/session-system-prompt.d.ts.map +1 -1
  158. package/dist/core/session-system-prompt.js.map +1 -1
  159. package/dist/core/settings-manager.d.ts +1 -1
  160. package/dist/core/settings-manager.d.ts.map +1 -1
  161. package/dist/core/settings-manager.js.map +1 -1
  162. package/dist/core/system-prompt.d.ts +1 -1
  163. package/dist/core/system-prompt.d.ts.map +1 -1
  164. package/dist/core/system-prompt.js.map +1 -1
  165. package/dist/core/tools/bash.d.ts.map +1 -1
  166. package/dist/core/tools/bash.js +7 -7
  167. package/dist/core/tools/bash.js.map +1 -1
  168. package/dist/core/workload-classifier.d.ts +2 -10
  169. package/dist/core/workload-classifier.d.ts.map +1 -1
  170. package/dist/core/workload-classifier.js +17 -0
  171. package/dist/core/workload-classifier.js.map +1 -1
  172. package/dist/core/workload-family-matcher.d.ts +1 -1
  173. package/dist/core/workload-family-matcher.d.ts.map +1 -1
  174. package/dist/core/workload-family-matcher.js.map +1 -1
  175. package/dist/core/workload-shard-execution-types.d.ts +40 -0
  176. package/dist/core/workload-shard-execution-types.d.ts.map +1 -0
  177. package/dist/core/workload-shard-execution-types.js +14 -0
  178. package/dist/core/workload-shard-execution-types.js.map +1 -0
  179. package/dist/core/workload-shard-executor.d.ts +3 -27
  180. package/dist/core/workload-shard-executor.d.ts.map +1 -1
  181. package/dist/core/workload-shard-executor.js.map +1 -1
  182. package/dist/core/workload-shard-runner.d.ts +1 -1
  183. package/dist/core/workload-shard-runner.d.ts.map +1 -1
  184. package/dist/core/workload-shard-runner.js.map +1 -1
  185. package/dist/core/workload-types.d.ts +24 -0
  186. package/dist/core/workload-types.d.ts.map +1 -0
  187. package/dist/core/workload-types.js +15 -0
  188. package/dist/core/workload-types.js.map +1 -0
  189. package/dist/main.d.ts +2 -0
  190. package/dist/main.d.ts.map +1 -1
  191. package/dist/main.js +27 -47
  192. package/dist/main.js.map +1 -1
  193. package/dist/modes/interactive/theme/omk-control-panel.json +1 -1
  194. package/dist/utils/clipboard-image.d.ts.map +1 -1
  195. package/dist/utils/clipboard-image.js +42 -33
  196. package/dist/utils/clipboard-image.js.map +1 -1
  197. package/dist/utils/clipboard.d.ts.map +1 -1
  198. package/dist/utils/clipboard.js +14 -2
  199. package/dist/utils/clipboard.js.map +1 -1
  200. package/docs/adaptorch-preview-spec.md +20 -18
  201. package/docs/adaptorch-preview.md +9 -9
  202. package/docs/compaction.md +24 -3
  203. package/docs/correctness-wall.md +10 -10
  204. package/docs/docs.json +12 -0
  205. package/docs/environment-variables.md +9 -2
  206. package/docs/index.md +3 -0
  207. package/docs/runtime-algorithms.md +359 -0
  208. package/docs/sdk.md +2 -2
  209. package/docs/settings.md +11 -10
  210. package/docs/usage.md +18 -2
  211. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  212. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  213. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  214. package/examples/extensions/gondolin/package-lock.json +2 -2
  215. package/examples/extensions/gondolin/package.json +1 -1
  216. package/examples/extensions/sandbox/package-lock.json +2 -2
  217. package/examples/extensions/sandbox/package.json +1 -1
  218. package/examples/extensions/with-deps/package-lock.json +2 -2
  219. package/examples/extensions/with-deps/package.json +1 -1
  220. package/npm-shrinkwrap.json +18 -18
  221. package/package.json +6 -6
  222. package/dist/core/context-budget-governor.d.ts +0 -67
  223. package/dist/core/context-budget-governor.d.ts.map +0 -1
  224. package/dist/core/context-budget-governor.js +0 -224
  225. package/dist/core/context-budget-governor.js.map +0 -1
  226. package/dist/core/lean-ctx.d.ts +0 -99
  227. package/dist/core/lean-ctx.d.ts.map +0 -1
  228. package/dist/core/lean-ctx.js +0 -225
  229. package/dist/core/lean-ctx.js.map +0 -1
  230. package/dist/core/read-anchors.d.ts +0 -110
  231. package/dist/core/read-anchors.d.ts.map +0 -1
  232. package/dist/core/read-anchors.js +0 -193
  233. package/dist/core/read-anchors.js.map +0 -1
  234. package/dist/core/recovery-checkpoint.d.ts +0 -196
  235. package/dist/core/recovery-checkpoint.d.ts.map +0 -1
  236. package/dist/core/recovery-checkpoint.js +0 -382
  237. package/dist/core/recovery-checkpoint.js.map +0 -1
  238. package/dist/core/sandbox-policy-v2.d.ts +0 -49
  239. package/dist/core/sandbox-policy-v2.d.ts.map +0 -1
  240. package/dist/core/sandbox-policy-v2.js +0 -368
  241. package/dist/core/sandbox-policy-v2.js.map +0 -1
  242. package/dist/guardrails/audit-logger.d.ts +0 -4
  243. package/dist/guardrails/audit-logger.d.ts.map +0 -1
  244. package/dist/guardrails/audit-logger.js +0 -28
  245. package/dist/guardrails/audit-logger.js.map +0 -1
  246. package/dist/guardrails/freedomd-adapter.d.ts +0 -16
  247. package/dist/guardrails/freedomd-adapter.d.ts.map +0 -1
  248. package/dist/guardrails/freedomd-adapter.js +0 -87
  249. package/dist/guardrails/freedomd-adapter.js.map +0 -1
  250. package/dist/orchestration/lane-grant-auditor.d.ts +0 -4
  251. package/dist/orchestration/lane-grant-auditor.d.ts.map +0 -1
  252. package/dist/orchestration/lane-grant-auditor.js +0 -87
  253. package/dist/orchestration/lane-grant-auditor.js.map +0 -1
  254. package/dist/types/guardrails.d.ts +0 -61
  255. package/dist/types/guardrails.d.ts.map +0 -1
  256. package/dist/types/guardrails.js +0 -9
  257. package/dist/types/guardrails.js.map +0 -1
  258. package/dist/types/lane-grant.d.ts +0 -58
  259. package/dist/types/lane-grant.d.ts.map +0 -1
  260. package/dist/types/lane-grant.js +0 -9
  261. package/dist/types/lane-grant.js.map +0 -1
@@ -0,0 +1,64 @@
1
+ import { spawn } from "node:child_process";
2
+ import { tmpdir } from "node:os";
3
+ const SPAWN_KILL_TIMEOUT_MS = 1000;
4
+ const SOUND_ENV_KEYS = [
5
+ "DBUS_SESSION_BUS_ADDRESS",
6
+ "DISPLAY",
7
+ "LANG",
8
+ "LC_ALL",
9
+ "PULSE_SERVER",
10
+ "SystemRoot",
11
+ "TEMP",
12
+ "TMP",
13
+ "WAYLAND_DISPLAY",
14
+ "WINDIR",
15
+ "XDG_RUNTIME_DIR",
16
+ ];
17
+ /** Process boundary for sound backends: no inherited PATH, credentials, or workspace cwd. */
18
+ export function completionSoundProcessOptions(sourceEnv = process.env) {
19
+ const env = {};
20
+ for (const key of SOUND_ENV_KEYS) {
21
+ const value = sourceEnv[key];
22
+ if (value !== undefined)
23
+ env[key] = value;
24
+ }
25
+ return { cwd: tmpdir(), env, shell: false, stdio: "ignore", windowsHide: true };
26
+ }
27
+ /** Default IO: bounded fire-and-forget spawn with terminal BEL fallback. */
28
+ export function defaultCompletionSoundIo() {
29
+ return {
30
+ spawnBackend: (argv) => new Promise((resolve) => {
31
+ try {
32
+ const [executable, ...args] = argv;
33
+ const child = spawn(executable, args, completionSoundProcessOptions());
34
+ const killTimer = setTimeout(() => {
35
+ try {
36
+ child.kill();
37
+ }
38
+ catch {
39
+ // Best effort only.
40
+ }
41
+ }, SPAWN_KILL_TIMEOUT_MS);
42
+ killTimer.unref();
43
+ child.once("error", (error) => resolve({ ok: false, diagnostic: error.code ?? "spawn_error" }));
44
+ child.once("spawn", () => {
45
+ child.unref();
46
+ resolve({ ok: true });
47
+ });
48
+ }
49
+ catch {
50
+ resolve({ ok: false, diagnostic: "spawn_error" });
51
+ }
52
+ }),
53
+ writeBell: () => {
54
+ try {
55
+ process.stdout.write("\u0007");
56
+ return true;
57
+ }
58
+ catch {
59
+ return false;
60
+ }
61
+ },
62
+ };
63
+ }
64
+ //# sourceMappingURL=completion-sound-io.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"completion-sound-io.js","sourceRoot":"","sources":["../../src/core/completion-sound-io.ts"],"names":[],"mappings":"AAAA,OAAO,EAAqB,KAAK,EAAE,MAAM,oBAAoB,CAAC;AAC9D,OAAO,EAAE,MAAM,EAAE,MAAM,SAAS,CAAC;AAEjC,MAAM,qBAAqB,GAAG,IAAI,CAAC;AACnC,MAAM,cAAc,GAAG;IACtB,0BAA0B;IAC1B,SAAS;IACT,MAAM;IACN,QAAQ;IACR,cAAc;IACd,YAAY;IACZ,MAAM;IACN,KAAK;IACL,iBAAiB;IACjB,QAAQ;IACR,iBAAiB;CACR,CAAC;AAQX,6FAA6F;AAC7F,MAAM,UAAU,6BAA6B,CAAC,SAAS,GAAsB,OAAO,CAAC,GAAG,EAAgB;IACvG,MAAM,GAAG,GAAsB,EAAE,CAAC;IAClC,KAAK,MAAM,GAAG,IAAI,cAAc,EAAE,CAAC;QAClC,MAAM,KAAK,GAAG,SAAS,CAAC,GAAG,CAAC,CAAC;QAC7B,IAAI,KAAK,KAAK,SAAS;YAAE,GAAG,CAAC,GAAG,CAAC,GAAG,KAAK,CAAC;IAC3C,CAAC;IACD,OAAO,EAAE,GAAG,EAAE,MAAM,EAAE,EAAE,GAAG,EAAE,KAAK,EAAE,KAAK,EAAE,KAAK,EAAE,QAAQ,EAAE,WAAW,EAAE,IAAI,EAAE,CAAC;AAAA,CAChF;AAED,4EAA4E;AAC5E,MAAM,UAAU,wBAAwB,GAAsB;IAC7D,OAAO;QACN,YAAY,EAAE,CAAC,IAAI,EAAE,EAAE,CACtB,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,EAAE,CAAC;YACxB,IAAI,CAAC;gBACJ,MAAM,CAAC,UAAU,EAAE,GAAG,IAAI,CAAC,GAAG,IAAI,CAAC;gBACnC,MAAM,KAAK,GAAG,KAAK,CAAC,UAAU,EAAE,IAAI,EAAE,6BAA6B,EAAE,CAAC,CAAC;gBACvE,MAAM,SAAS,GAAG,UAAU,CAAC,GAAG,EAAE,CAAC;oBAClC,IAAI,CAAC;wBACJ,KAAK,CAAC,IAAI,EAAE,CAAC;oBACd,CAAC;oBAAC,MAAM,CAAC;wBACR,oBAAoB;oBACrB,CAAC;gBAAA,CACD,EAAE,qBAAqB,CAAC,CAAC;gBAC1B,SAAS,CAAC,KAAK,EAAE,CAAC;gBAClB,KAAK,CAAC,IAAI,CAAC,OAAO,EAAE,CAAC,KAA4B,EAAE,EAAE,CACpD,OAAO,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,UAAU,EAAE,KAAK,CAAC,IAAI,IAAI,aAAa,EAAE,CAAC,CAC/D,CAAC;gBACF,KAAK,CAAC,IAAI,CAAC,OAAO,EAAE,GAAG,EAAE,CAAC;oBACzB,KAAK,CAAC,KAAK,EAAE,CAAC;oBACd,OAAO,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC;gBAAA,CACtB,CAAC,CAAC;YACJ,CAAC;YAAC,MAAM,CAAC;gBACR,OAAO,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,UAAU,EAAE,aAAa,EAAE,CAAC,CAAC;YACnD,CAAC;QAAA,CACD,CAAC;QACH,SAAS,EAAE,GAAG,EAAE,CAAC;YAChB,IAAI,CAAC;gBACJ,OAAO,CAAC,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;gBAC/B,OAAO,IAAI,CAAC;YACb,CAAC;YAAC,MAAM,CAAC;gBACR,OAAO,KAAK,CAAC;YACd,CAAC;QAAA,CACD;KACD,CAAC;AAAA,CACF","sourcesContent":["import { type SpawnOptions, spawn } from \"node:child_process\";\nimport { tmpdir } from \"node:os\";\n\nconst SPAWN_KILL_TIMEOUT_MS = 1000;\nconst SOUND_ENV_KEYS = [\n\t\"DBUS_SESSION_BUS_ADDRESS\",\n\t\"DISPLAY\",\n\t\"LANG\",\n\t\"LC_ALL\",\n\t\"PULSE_SERVER\",\n\t\"SystemRoot\",\n\t\"TEMP\",\n\t\"TMP\",\n\t\"WAYLAND_DISPLAY\",\n\t\"WINDIR\",\n\t\"XDG_RUNTIME_DIR\",\n] as const;\n\nexport interface CompletionSoundIo {\n\t/** Spawn a fixed absolute argv; resolve success/failure. */\n\treadonly spawnBackend: (argv: readonly string[]) => Promise<{ readonly ok: boolean; readonly diagnostic?: string }>;\n\treadonly writeBell: () => boolean;\n}\n\n/** Process boundary for sound backends: no inherited PATH, credentials, or workspace cwd. */\nexport function completionSoundProcessOptions(sourceEnv: NodeJS.ProcessEnv = process.env): SpawnOptions {\n\tconst env: NodeJS.ProcessEnv = {};\n\tfor (const key of SOUND_ENV_KEYS) {\n\t\tconst value = sourceEnv[key];\n\t\tif (value !== undefined) env[key] = value;\n\t}\n\treturn { cwd: tmpdir(), env, shell: false, stdio: \"ignore\", windowsHide: true };\n}\n\n/** Default IO: bounded fire-and-forget spawn with terminal BEL fallback. */\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, completionSoundProcessOptions());\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\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"]}
@@ -1,4 +1,6 @@
1
+ import { type CompletionSoundIo } from "./completion-sound-io.ts";
1
2
  import type { PromptSettledEvent } from "./prompt-settlement.ts";
3
+ export { type CompletionSoundIo, completionSoundProcessOptions, defaultCompletionSoundIo, } from "./completion-sound-io.ts";
2
4
  /**
3
5
  * Cross-platform completion sound service (OMK v0.97.x roadmap §17, M4/PR7).
4
6
  *
@@ -19,6 +21,7 @@ export interface CompletionSoundSettings {
19
21
  minDurationMs?: number;
20
22
  onSuccess?: boolean;
21
23
  onFailure?: boolean;
24
+ onAbort?: boolean;
22
25
  terminalBellFallback?: boolean;
23
26
  }
24
27
  export interface NotificationSettings {
@@ -42,6 +45,7 @@ export interface ResolvedCompletionSoundSettings {
42
45
  readonly minDurationMs: number;
43
46
  readonly onSuccess: boolean;
44
47
  readonly onFailure: boolean;
48
+ readonly onAbort?: boolean;
45
49
  readonly terminalBellFallback: boolean;
46
50
  }
47
51
  /** Defaults per §17.1; `OMK_COMPLETION_SOUND=0|1` (§18.2) overrides `enabled`. */
@@ -72,16 +76,6 @@ export declare function selectCompletionSoundCandidates(input: {
72
76
  readonly isWsl: boolean;
73
77
  readonly terminalBellFallback: boolean;
74
78
  }): 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
79
  /**
86
80
  * Stateful service: once per promptRunId (§17.4), deterministic candidate
87
81
  * walk, all failures downgraded to diagnostics. `handleSettled` is fire and
@@ -1 +1 @@
1
- {"version":3,"file":"completion-sound.d.ts","sourceRoot":"","sources":["../../src/core/completion-sound.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAEjE;;;;;;;;;;;;;;GAcG;AAEH,MAAM,WAAW,uBAAuB;IACvC,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,oBAAoB,CAAC,EAAE,OAAO,CAAC;CAC/B;AAED,MAAM,WAAW,oBAAoB;IACpC,eAAe,CAAC,EAAE,uBAAuB,CAAC;CAC1C;AAED,MAAM,MAAM,sBAAsB,GAC/B,cAAc,GACd,sBAAsB,GACtB,gBAAgB,GAChB,cAAc,GACd,aAAa,GACb,eAAe,GACf,MAAM,CAAC;AAEV,MAAM,WAAW,sBAAsB;IACtC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,QAAQ,CAAC,OAAO,EAAE,WAAW,GAAG,QAAQ,GAAG,SAAS,CAAC;IACrD,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;CAC5B;AAED,MAAM,WAAW,qBAAqB;IACrC,QAAQ,CAAC,OAAO,EAAE,sBAAsB,CAAC;IACzC,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAC;CAC7B;AAED,eAAO,MAAM,oBAAoB,yBAAyB,CAAC;AAG3D,MAAM,WAAW,+BAA+B;IAC/C,QAAQ,CAAC,OAAO,EAAE,OAAO,CAAC;IAC1B,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B,QAAQ,CAAC,SAAS,EAAE,OAAO,CAAC;IAC5B,QAAQ,CAAC,oBAAoB,EAAE,OAAO,CAAC;CACvC;AAED,oFAAkF;AAClF,wBAAgB,8BAA8B,CAC7C,QAAQ,CAAC,EAAE,uBAAuB,EAClC,GAAG,GAAE,MAAM,CAAC,UAAwB,GAClC,+BAA+B,CAgBjC;AAED,MAAM,WAAW,sBAAsB;IACtC,iEAAgE;IAChE,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,IAAI,EAAE,OAAO,CAAC;CACvB;AAED,kFAAgF;AAChF,wBAAgB,yBAAyB,CAAC,KAAK,EAAE;IAChD,QAAQ,CAAC,QAAQ,EAAE,+BAA+B,CAAC;IACnD,QAAQ,CAAC,KAAK,EAAE,kBAAkB,CAAC;IACnC,QAAQ,CAAC,OAAO,EAAE,sBAAsB,CAAC;CACzC,GAAG,OAAO,CAgBV;AAED,MAAM,WAAW,wBAAwB;IACxC,QAAQ,CAAC,OAAO,EAAE,sBAAsB,CAAC;IACzC,2EAA0E;IAC1E,QAAQ,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,CAAC;CACjC;AAED;;;GAGG;AACH,wBAAgB,+BAA+B,CAAC,KAAK,EAAE;IACtD,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC,QAAQ,CAAC;IACnC,QAAQ,CAAC,KAAK,EAAE,OAAO,CAAC;IACxB,QAAQ,CAAC,oBAAoB,EAAE,OAAO,CAAC;CACvC,GAAG,SAAS,wBAAwB,EAAE,CAqBtC;AAOD,MAAM,WAAW,iBAAiB;IACjC,mGAAmG;IACnG,QAAQ,CAAC,YAAY,EAAE,CAAC,IAAI,EAAE,SAAS,MAAM,EAAE,KAAK,OAAO,CAAC;QAAE,QAAQ,CAAC,EAAE,EAAE,OAAO,CAAC;QAAC,QAAQ,CAAC,UAAU,CAAC,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;IACpH,QAAQ,CAAC,SAAS,EAAE,MAAM,OAAO,CAAC;CAClC;AAED,iEAAgE;AAChE,wBAAgB,wBAAwB,IAAI,iBAAiB,CAoC5D;AAED;;;;GAIG;AACH,MAAM,WAAW,0BAA0B;IAC1C,QAAQ,CAAC,WAAW,EAAE,MAAM,uBAAuB,GAAG,SAAS,CAAC;IAChE,QAAQ,CAAC,OAAO,EAAE,MAAM,sBAAsB,CAAC;IAC/C,QAAQ,CAAC,UAAU,EAAE,MAAM,SAAS,wBAAwB,EAAE,CAAC;IAC/D,QAAQ,CAAC,EAAE,CAAC,EAAE,iBAAiB,CAAC;IAChC,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,CAAC,UAAU,CAAC;IACjC,sEAAqE;IACrE,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC,MAAM,EAAE,qBAAqB,KAAK,IAAI,CAAC;CAC5D;AAED,qBAAa,sBAAsB;IAClC,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAqB;IAClD,OAAO,CAAC,UAAU,CAAsC;IACxD,OAAO,CAAC,QAAQ,CAAC,IAAI,CAA6B;IAElD,YAAY,IAAI,EAAE,0BAA0B,EAE3C;IAED,kDAAkD;IAClD,IAAI,kBAAkB,IAAI,qBAAqB,GAAG,IAAI,CAErD;IAEK,aAAa,CAAC,KAAK,EAAE,kBAAkB,GAAG,OAAO,CAAC,qBAAqB,CAAC,CAiC7E;IAED,OAAO,CAAC,MAAM;CASd","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
+ {"version":3,"file":"completion-sound.d.ts","sourceRoot":"","sources":["../../src/core/completion-sound.ts"],"names":[],"mappings":"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","sourcesContent":["import { type CompletionSoundIo, defaultCompletionSoundIo } from \"./completion-sound-io.ts\";\nimport type { PromptSettledEvent } from \"./prompt-settlement.ts\";\n\nexport {\n\ttype CompletionSoundIo,\n\tcompletionSoundProcessOptions,\n\tdefaultCompletionSoundIo,\n} from \"./completion-sound-io.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: true; interactive TTY only, with OMK_COMPLETION_SOUND=0 opt-out\n\tminDurationMs?: number; // default: 5000; applies to successful completion only\n\tonSuccess?: boolean; // default: true\n\tonFailure?: boolean; // default: true\n\tonAbort?: 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\";\nexport interface ResolvedCompletionSoundSettings {\n\treadonly enabled: boolean;\n\treadonly minDurationMs: number;\n\treadonly onSuccess: boolean;\n\treadonly onFailure: boolean;\n\treadonly onAbort?: 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 ?? true;\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\tonAbort: settings?.onAbort ?? 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\tswitch (event.outcome) {\n\t\tcase \"completed\":\n\t\t\treturn event.durationMs >= settings.minDurationMs && settings.onSuccess;\n\t\tcase \"failed\":\n\t\t\treturn settings.onFailure;\n\t\tcase \"aborted\":\n\t\t\treturn settings.onAbort ?? true;\n\t\tdefault: {\n\t\t\tconst exhaustive: never = event.outcome;\n\t\t\treturn exhaustive;\n\t\t}\n\t}\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: [\"/usr/bin/afplay\", \"/System/Library/Sounds/Glass.aiff\"] }, ...bell];\n\t}\n\tif (input.platform === \"win32\") {\n\t\treturn [\n\t\t\t{\n\t\t\t\tbackend: \"windows-system-sound\",\n\t\t\t\targv: windowsSystemSoundArgv(\"C:\\\\Windows\\\\System32\\\\WindowsPowerShell\\\\v1.0\\\\powershell.exe\"),\n\t\t\t},\n\t\t\t...bell,\n\t\t];\n\t}\n\tif (input.platform === \"linux\" && input.isWsl) {\n\t\t// WSL has no trustworthy fixed mount path for Windows PowerShell; use BEL.\n\t\treturn [...bell];\n\t}\n\tif (input.platform === \"linux\") {\n\t\treturn [\n\t\t\t{ backend: \"linux-canberra\", argv: [\"/usr/bin/canberra-gtk-play\", \"-i\", \"complete\"] },\n\t\t\t{ backend: \"linux-paplay\", argv: [\"/usr/bin/paplay\", \"/usr/share/sounds/freedesktop/stereo/complete.oga\"] },\n\t\t\t{ backend: \"linux-aplay\", argv: [\"/usr/bin/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\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,9 +1,9 @@
1
- import { spawn } from "node:child_process";
1
+ import { defaultCompletionSoundIo } from "./completion-sound-io.js";
2
+ export { completionSoundProcessOptions, defaultCompletionSoundIo, } from "./completion-sound-io.js";
2
3
  export const COMPLETION_SOUND_ENV = "OMK_COMPLETION_SOUND";
3
- const SPAWN_KILL_TIMEOUT_MS = 1000;
4
4
  /** Defaults per §17.1; `OMK_COMPLETION_SOUND=0|1` (§18.2) overrides `enabled`. */
5
5
  export function resolveCompletionSoundSettings(settings, env = process.env) {
6
- let enabled = settings?.enabled ?? false;
6
+ let enabled = settings?.enabled ?? true;
7
7
  const envValue = env[COMPLETION_SOUND_ENV]?.trim();
8
8
  if (envValue === "0") {
9
9
  enabled = false;
@@ -16,6 +16,7 @@ export function resolveCompletionSoundSettings(settings, env = process.env) {
16
16
  minDurationMs: typeof settings?.minDurationMs === "number" && settings.minDurationMs >= 0 ? settings.minDurationMs : 5000,
17
17
  onSuccess: settings?.onSuccess ?? true,
18
18
  onFailure: settings?.onFailure ?? true,
19
+ onAbort: settings?.onAbort ?? true,
19
20
  terminalBellFallback: settings?.terminalBellFallback ?? true,
20
21
  };
21
22
  }
@@ -25,17 +26,18 @@ export function shouldPlayCompletionSound(input) {
25
26
  if (!settings.enabled || !surface.isTui || !surface.isTty || surface.isCi) {
26
27
  return false;
27
28
  }
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;
29
+ switch (event.outcome) {
30
+ case "completed":
31
+ return event.durationMs >= settings.minDurationMs && settings.onSuccess;
32
+ case "failed":
33
+ return settings.onFailure;
34
+ case "aborted":
35
+ return settings.onAbort ?? true;
36
+ default: {
37
+ const exhaustive = event.outcome;
38
+ return exhaustive;
39
+ }
36
40
  }
37
- // User-initiated aborts never chime: the user is already at the keyboard.
38
- return false;
39
41
  }
40
42
  /**
41
43
  * Deterministic backend candidate chain per platform (§17.3, §23.2 property
@@ -44,20 +46,26 @@ export function shouldPlayCompletionSound(input) {
44
46
  export function selectCompletionSoundCandidates(input) {
45
47
  const bell = input.terminalBellFallback ? [{ backend: "terminal-bell", argv: [] }] : [];
46
48
  if (input.platform === "darwin") {
47
- return [{ backend: "macos-afplay", argv: ["afplay", "/System/Library/Sounds/Glass.aiff"] }, ...bell];
49
+ return [{ backend: "macos-afplay", argv: ["/usr/bin/afplay", "/System/Library/Sounds/Glass.aiff"] }, ...bell];
48
50
  }
49
51
  if (input.platform === "win32") {
50
- return [{ backend: "windows-system-sound", argv: windowsSystemSoundArgv("powershell.exe") }, ...bell];
52
+ return [
53
+ {
54
+ backend: "windows-system-sound",
55
+ argv: windowsSystemSoundArgv("C:\\Windows\\System32\\WindowsPowerShell\\v1.0\\powershell.exe"),
56
+ },
57
+ ...bell,
58
+ ];
51
59
  }
52
60
  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];
61
+ // WSL has no trustworthy fixed mount path for Windows PowerShell; use BEL.
62
+ return [...bell];
55
63
  }
56
64
  if (input.platform === "linux") {
57
65
  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"] },
66
+ { backend: "linux-canberra", argv: ["/usr/bin/canberra-gtk-play", "-i", "complete"] },
67
+ { backend: "linux-paplay", argv: ["/usr/bin/paplay", "/usr/share/sounds/freedesktop/stereo/complete.oga"] },
68
+ { backend: "linux-aplay", argv: ["/usr/bin/aplay", "-q", "/usr/share/sounds/alsa/Front_Center.wav"] },
61
69
  ...bell,
62
70
  ];
63
71
  }
@@ -67,44 +75,6 @@ export function selectCompletionSoundCandidates(input) {
67
75
  function windowsSystemSoundArgv(executable) {
68
76
  return [executable, "-NoProfile", "-NonInteractive", "-Command", "[System.Media.SystemSounds]::Asterisk.Play()"];
69
77
  }
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
78
  export class CompletionSoundService {
109
79
  playedRunIds = new Set();
110
80
  lastResult = null;
@@ -1 +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
+ {"version":3,"file":"completion-sound.js","sourceRoot":"","sources":["../../src/core/completion-sound.ts"],"names":[],"mappings":"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","sourcesContent":["import { type CompletionSoundIo, defaultCompletionSoundIo } from \"./completion-sound-io.ts\";\nimport type { PromptSettledEvent } from \"./prompt-settlement.ts\";\n\nexport {\n\ttype CompletionSoundIo,\n\tcompletionSoundProcessOptions,\n\tdefaultCompletionSoundIo,\n} from \"./completion-sound-io.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: true; interactive TTY only, with OMK_COMPLETION_SOUND=0 opt-out\n\tminDurationMs?: number; // default: 5000; applies to successful completion only\n\tonSuccess?: boolean; // default: true\n\tonFailure?: boolean; // default: true\n\tonAbort?: 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\";\nexport interface ResolvedCompletionSoundSettings {\n\treadonly enabled: boolean;\n\treadonly minDurationMs: number;\n\treadonly onSuccess: boolean;\n\treadonly onFailure: boolean;\n\treadonly onAbort?: 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 ?? true;\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\tonAbort: settings?.onAbort ?? 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\tswitch (event.outcome) {\n\t\tcase \"completed\":\n\t\t\treturn event.durationMs >= settings.minDurationMs && settings.onSuccess;\n\t\tcase \"failed\":\n\t\t\treturn settings.onFailure;\n\t\tcase \"aborted\":\n\t\t\treturn settings.onAbort ?? true;\n\t\tdefault: {\n\t\t\tconst exhaustive: never = event.outcome;\n\t\t\treturn exhaustive;\n\t\t}\n\t}\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: [\"/usr/bin/afplay\", \"/System/Library/Sounds/Glass.aiff\"] }, ...bell];\n\t}\n\tif (input.platform === \"win32\") {\n\t\treturn [\n\t\t\t{\n\t\t\t\tbackend: \"windows-system-sound\",\n\t\t\t\targv: windowsSystemSoundArgv(\"C:\\\\Windows\\\\System32\\\\WindowsPowerShell\\\\v1.0\\\\powershell.exe\"),\n\t\t\t},\n\t\t\t...bell,\n\t\t];\n\t}\n\tif (input.platform === \"linux\" && input.isWsl) {\n\t\t// WSL has no trustworthy fixed mount path for Windows PowerShell; use BEL.\n\t\treturn [...bell];\n\t}\n\tif (input.platform === \"linux\") {\n\t\treturn [\n\t\t\t{ backend: \"linux-canberra\", argv: [\"/usr/bin/canberra-gtk-play\", \"-i\", \"complete\"] },\n\t\t\t{ backend: \"linux-paplay\", argv: [\"/usr/bin/paplay\", \"/usr/share/sounds/freedesktop/stereo/complete.oga\"] },\n\t\t\t{ backend: \"linux-aplay\", argv: [\"/usr/bin/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\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"]}
@@ -33,10 +33,23 @@ export declare function scoreSkillRelevance(skill: {
33
33
  /**
34
34
  * Score context file relevance against a query context.
35
35
  *
36
- * - queryContext undefined -> isGlobal ? 0.9 : 0.6
36
+ * - Baseline (no query evidence) -> isGlobal ? 0.9 : 0.6
37
37
  * - Coverage of query tokens against path segments + content sample
38
38
  * - Path tokens weighted 2x
39
- * - isGlobal bonus: +0.1
39
+ * - Coverage allocates the headroom ABOVE the baseline; it never cuts into it
40
+ *
41
+ * A context file is not retrieved reference material — it is the standing
42
+ * instruction set the agent is required to follow, and it applies whether or
43
+ * not the user's current sentence reuses its vocabulary. Scoring it purely by
44
+ * lexical overlap made the rules rank LOWEST exactly when the agent was doing
45
+ * work: this repository's own AGENTS.md scored 0.9 idle but 0.1 under queries
46
+ * like "rename this variable". Coverage therefore raises the score from the
47
+ * baseline toward 1 instead of replacing it, which also reconciles the two
48
+ * readings the function used to have — an absent query and a present-but-
49
+ * non-overlapping query now agree that there is simply no evidence to add.
50
+ *
51
+ * Lexical scoring stays the right metric for skills, which really are
52
+ * topic-scoped; see scoreSkillRelevance.
40
53
  */
41
54
  export declare function scoreContextFileRelevance(contextFile: {
42
55
  readonly path: string;
@@ -1 +1 @@
1
- {"version":3,"file":"context-budget-relevance.d.ts","sourceRoot":"","sources":["../../src/core/context-budget-relevance.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAyJH;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,CAmCpE;AAkED;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAClC,KAAK,EAAE;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;CAAE,EAC9D,YAAY,EAAE,MAAM,GAAG,SAAS,GAC9B,MAAM,CA2BR;AAYD;;;;;;;GAOG;AACH,wBAAgB,yBAAyB,CACxC,WAAW,EAAE;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAA;CAAE,EAC5F,YAAY,EAAE,MAAM,GAAG,SAAS,GAC9B,MAAM,CAkCR","sourcesContent":["/**\n * Query-aware relevance scoring for context budget items.\n *\n * Replaces heuristic proxies (description length, isGlobal hardcoding)\n * with token-based coverage scoring that measures semantic overlap\n * between the current query context and each resource item.\n *\n * Design: Coverage-based IR (not Jaccard) — avoids penalizing long\n * descriptions whose large union would dilute the score.\n */\n\n// ---------------------------------------------------------------------------\n// Stop words — English + common Korean particles\n// ---------------------------------------------------------------------------\n\nconst STOP_WORDS: ReadonlySet<string> = new Set([\n\t// English common stop words\n\t\"the\",\n\t\"a\",\n\t\"an\",\n\t\"is\",\n\t\"are\",\n\t\"was\",\n\t\"were\",\n\t\"be\",\n\t\"been\",\n\t\"being\",\n\t\"have\",\n\t\"has\",\n\t\"had\",\n\t\"do\",\n\t\"does\",\n\t\"did\",\n\t\"will\",\n\t\"would\",\n\t\"could\",\n\t\"should\",\n\t\"may\",\n\t\"might\",\n\t\"shall\",\n\t\"can\",\n\t\"to\",\n\t\"of\",\n\t\"in\",\n\t\"on\",\n\t\"for\",\n\t\"and\",\n\t\"or\",\n\t\"with\",\n\t\"how\",\n\t\"i\",\n\t\"you\",\n\t\"he\",\n\t\"she\",\n\t\"it\",\n\t\"we\",\n\t\"they\",\n\t\"me\",\n\t\"him\",\n\t\"her\",\n\t\"us\",\n\t\"them\",\n\t\"my\",\n\t\"your\",\n\t\"his\",\n\t\"its\",\n\t\"our\",\n\t\"their\",\n\t\"this\",\n\t\"that\",\n\t\"these\",\n\t\"those\",\n\t\"what\",\n\t\"which\",\n\t\"who\",\n\t\"whom\",\n\t\"where\",\n\t\"when\",\n\t\"why\",\n\t\"not\",\n\t\"no\",\n\t\"nor\",\n\t\"so\",\n\t\"if\",\n\t\"then\",\n\t\"than\",\n\t\"too\",\n\t\"very\",\n\t\"just\",\n\t\"about\",\n\t\"above\",\n\t\"after\",\n\t\"again\",\n\t\"all\",\n\t\"also\",\n\t\"am\",\n\t\"any\",\n\t\"because\",\n\t\"before\",\n\t\"between\",\n\t\"both\",\n\t\"but\",\n\t\"by\",\n\t\"from\",\n\t\"into\",\n\t\"more\",\n\t\"most\",\n\t\"other\",\n\t\"out\",\n\t\"over\",\n\t\"own\",\n\t\"same\",\n\t\"some\",\n\t\"such\",\n\t\"there\",\n\t\"up\",\n\t\"down\",\n\t\"here\",\n\t\"at\",\n\t\"as\",\n\t\"while\",\n\t\"through\",\n\t\"during\",\n\t\"each\",\n\t\"few\",\n\t\"further\",\n\t\"once\",\n\t\"only\",\n\t\"until\",\n\t// Korean particles (조사)\n\t\"은\",\n\t\"는\",\n\t\"이\",\n\t\"가\",\n\t\"을\",\n\t\"를\",\n\t\"에\",\n\t\"의\",\n\t\"와\",\n\t\"과\",\n\t\"도\",\n\t\"로\",\n\t\"으로\",\n\t\"에서\",\n\t\"까지\",\n\t\"부터\",\n\t\"만\",\n\t\"라도\",\n\t\"나\",\n\t\"이며\",\n\t\"이고\",\n\t\"이나\",\n\t\"라\",\n\t\"이다\",\n\t\"하다\",\n\t\"된\",\n]);\n\n// ---------------------------------------------------------------------------\n// Tokenizer\n// ---------------------------------------------------------------------------\n\n/**\n * Tokenize text for relevance scoring.\n *\n * 1. Lowercase\n * 2. Split: Latin words via regex, Korean syllable groups via regex\n * 3. Remove stop words\n * 4. Remove short Latin tokens (<=2 chars)\n * 5. Deduplicate\n */\nexport function tokenizeForRelevance(text: string): readonly string[] {\n\tconst lower = text.toLowerCase();\n\tconst raw = lower.match(/[\\uac00-\\ud7a3]+|[a-z0-9]+/g);\n\tif (!raw) {\n\t\treturn [];\n\t}\n\n\tconst seen = new Set<string>();\n\tconst tokens: string[] = [];\n\n\tfor (const token of raw) {\n\t\t// Skip stop words\n\t\tif (STOP_WORDS.has(token)) {\n\t\t\tcontinue;\n\t\t}\n\t\t// For Latin tokens, skip very short ones (<=2 chars)\n\t\tif (/^[a-z0-9]+$/.test(token) && token.length <= 2) {\n\t\t\tcontinue;\n\t\t}\n\t\t// For Korean, keep 2+ syllable groups\n\t\tif (/^[\\uac00-\\ud7a3]+$/.test(token)) {\n\t\t\tif (token.length >= 2 && !seen.has(token)) {\n\t\t\t\tseen.add(token);\n\t\t\t\ttokens.push(token);\n\t\t\t}\n\t\t\tcontinue;\n\t\t}\n\t\t// Deduplicate\n\t\tif (!seen.has(token)) {\n\t\t\tseen.add(token);\n\t\t\ttokens.push(token);\n\t\t}\n\t}\n\n\treturn tokens;\n}\n\n// ---------------------------------------------------------------------------\n// Coverage scoring\n// ---------------------------------------------------------------------------\n\n/**\n * Compute coverage: fraction of query tokens found in the item token set.\n * Optional per-token weights boost high-value matches (name, path).\n */\nfunction computeCoverage(\n\tqueryTokens: readonly string[],\n\titemTokenSet: ReadonlySet<string>,\n\titemTokenWeights?: ReadonlyMap<string, number>,\n): number {\n\tif (queryTokens.length === 0) {\n\t\treturn 0;\n\t}\n\n\tlet matchWeight = 0;\n\tfor (const qt of queryTokens) {\n\t\tif (itemTokenSet.has(qt)) {\n\t\t\tmatchWeight += itemTokenWeights?.get(qt) ?? 1;\n\t\t}\n\t}\n\n\t// Normalize: each query token contributes at most 1 to the numerator\n\treturn Math.min(1, matchWeight / queryTokens.length);\n}\n\n/**\n * Build a token set with optional high-weight tokens.\n */\nfunction buildWeightedTokenSet(\n\tbaseText: string,\n\thighWeightText: string | undefined,\n\tweight: number,\n): { tokens: Set<string>; weights: Map<string, number> } {\n\tconst baseTokens = tokenizeForRelevance(baseText);\n\tconst highTokens = highWeightText ? tokenizeForRelevance(highWeightText) : [];\n\n\tconst tokenSet = new Set<string>();\n\tconst weights = new Map<string, number>();\n\n\tfor (const t of baseTokens) {\n\t\ttokenSet.add(t);\n\t\tif (!weights.has(t)) {\n\t\t\tweights.set(t, 1);\n\t\t}\n\t}\n\tfor (const t of highTokens) {\n\t\ttokenSet.add(t);\n\t\tweights.set(t, Math.max(weights.get(t) ?? 0, weight));\n\t}\n\n\treturn { tokens: tokenSet, weights };\n}\n\n// ---------------------------------------------------------------------------\n// Skill relevance\n// ---------------------------------------------------------------------------\n\nconst SKILL_NEUTRAL_SCORE = 0.3;\nconst NAME_MATCH_BONUS = 0.2;\nconst NAME_TOKEN_WEIGHT = 3;\n\n/**\n * Score skill relevance against a query context.\n *\n * - queryContext undefined or empty -> 0.3 (neutral)\n * - Coverage of query tokens against skill name+description\n * - Name tokens weighted 3x\n * - Name substring match bonus: +0.2 (capped at 1.0)\n */\nexport function scoreSkillRelevance(\n\tskill: { readonly name: string; readonly description: string },\n\tqueryContext: string | undefined,\n): number {\n\tif (!queryContext || queryContext.trim() === \"\") {\n\t\treturn SKILL_NEUTRAL_SCORE;\n\t}\n\n\tconst queryTokens = tokenizeForRelevance(queryContext);\n\tif (queryTokens.length === 0) {\n\t\treturn SKILL_NEUTRAL_SCORE;\n\t}\n\n\t// Build skill token set: description is base, name tokens get 3x weight\n\tconst { tokens: skillTokenSet, weights: skillWeights } = buildWeightedTokenSet(\n\t\tskill.description,\n\t\tskill.name,\n\t\tNAME_TOKEN_WEIGHT,\n\t);\n\n\tlet score = computeCoverage(queryTokens, skillTokenSet, skillWeights);\n\n\t// Name substring bonus: if skill.name appears in query text (or vice versa)\n\tconst queryLower = queryContext.toLowerCase();\n\tconst nameLower = skill.name.toLowerCase();\n\tif (queryLower.includes(nameLower) || nameLower.includes(queryLower.trim())) {\n\t\tscore = Math.min(1, score + NAME_MATCH_BONUS);\n\t}\n\n\treturn clamp01(score);\n}\n\n// ---------------------------------------------------------------------------\n// Context file relevance\n// ---------------------------------------------------------------------------\n\nconst CONTEXT_NEUTRAL_GLOBAL = 0.9;\nconst CONTEXT_NEUTRAL_LOCAL = 0.6;\nconst PATH_TOKEN_WEIGHT = 2;\nconst GLOBAL_BONUS = 0.1;\nconst CONTENT_SAMPLE_CHARS = 2000;\n\n/**\n * Score context file relevance against a query context.\n *\n * - queryContext undefined -> isGlobal ? 0.9 : 0.6\n * - Coverage of query tokens against path segments + content sample\n * - Path tokens weighted 2x\n * - isGlobal bonus: +0.1\n */\nexport function scoreContextFileRelevance(\n\tcontextFile: { readonly path: string; readonly content: string; readonly isGlobal: boolean },\n\tqueryContext: string | undefined,\n): number {\n\tif (!queryContext || queryContext.trim() === \"\") {\n\t\treturn contextFile.isGlobal ? CONTEXT_NEUTRAL_GLOBAL : CONTEXT_NEUTRAL_LOCAL;\n\t}\n\n\tconst queryTokens = tokenizeForRelevance(queryContext);\n\tif (queryTokens.length === 0) {\n\t\treturn contextFile.isGlobal ? CONTEXT_NEUTRAL_GLOBAL : CONTEXT_NEUTRAL_LOCAL;\n\t}\n\n\t// Path segments as high-weight text\n\tconst pathText = extractPathSegments(contextFile.path);\n\n\t// Content sample for performance\n\tconst contentSample =\n\t\tcontextFile.content.length > CONTENT_SAMPLE_CHARS\n\t\t\t? contextFile.content.slice(0, CONTENT_SAMPLE_CHARS)\n\t\t\t: contextFile.content;\n\n\t// Build token set: content is base, path tokens get 2x weight\n\tconst { tokens: fileTokenSet, weights: fileWeights } = buildWeightedTokenSet(\n\t\tcontentSample,\n\t\tpathText,\n\t\tPATH_TOKEN_WEIGHT,\n\t);\n\n\tlet score = computeCoverage(queryTokens, fileTokenSet, fileWeights);\n\n\t// Global file bonus\n\tif (contextFile.isGlobal) {\n\t\tscore = Math.min(1, score + GLOBAL_BONUS);\n\t}\n\n\treturn clamp01(score);\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Extract meaningful segments from a file path for token matching.\n */\nfunction extractPathSegments(path: string): string {\n\treturn path\n\t\t.toLowerCase()\n\t\t.split(/[/\\\\._-]+/)\n\t\t.filter((seg) => seg.length > 0)\n\t\t.join(\" \");\n}\n\nfunction clamp01(value: number): number {\n\treturn Math.max(0, Math.min(1, value));\n}\n"]}
1
+ {"version":3,"file":"context-budget-relevance.d.ts","sourceRoot":"","sources":["../../src/core/context-budget-relevance.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAyJH;;;;;;;;GAQG;AACH,wBAAgB,oBAAoB,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,MAAM,EAAE,CAmCpE;AAkED;;;;;;;GAOG;AACH,wBAAgB,mBAAmB,CAClC,KAAK,EAAE;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAA;CAAE,EAC9D,YAAY,EAAE,MAAM,GAAG,SAAS,GAC9B,MAAM,CA2BR;AAWD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,yBAAyB,CACxC,WAAW,EAAE;IAAE,QAAQ,CAAC,IAAI,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,OAAO,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAA;CAAE,EAC5F,YAAY,EAAE,MAAM,GAAG,SAAS,GAC9B,MAAM,CAiCR","sourcesContent":["/**\n * Query-aware relevance scoring for context budget items.\n *\n * Replaces heuristic proxies (description length, isGlobal hardcoding)\n * with token-based coverage scoring that measures semantic overlap\n * between the current query context and each resource item.\n *\n * Design: Coverage-based IR (not Jaccard) — avoids penalizing long\n * descriptions whose large union would dilute the score.\n */\n\n// ---------------------------------------------------------------------------\n// Stop words — English + common Korean particles\n// ---------------------------------------------------------------------------\n\nconst STOP_WORDS: ReadonlySet<string> = new Set([\n\t// English common stop words\n\t\"the\",\n\t\"a\",\n\t\"an\",\n\t\"is\",\n\t\"are\",\n\t\"was\",\n\t\"were\",\n\t\"be\",\n\t\"been\",\n\t\"being\",\n\t\"have\",\n\t\"has\",\n\t\"had\",\n\t\"do\",\n\t\"does\",\n\t\"did\",\n\t\"will\",\n\t\"would\",\n\t\"could\",\n\t\"should\",\n\t\"may\",\n\t\"might\",\n\t\"shall\",\n\t\"can\",\n\t\"to\",\n\t\"of\",\n\t\"in\",\n\t\"on\",\n\t\"for\",\n\t\"and\",\n\t\"or\",\n\t\"with\",\n\t\"how\",\n\t\"i\",\n\t\"you\",\n\t\"he\",\n\t\"she\",\n\t\"it\",\n\t\"we\",\n\t\"they\",\n\t\"me\",\n\t\"him\",\n\t\"her\",\n\t\"us\",\n\t\"them\",\n\t\"my\",\n\t\"your\",\n\t\"his\",\n\t\"its\",\n\t\"our\",\n\t\"their\",\n\t\"this\",\n\t\"that\",\n\t\"these\",\n\t\"those\",\n\t\"what\",\n\t\"which\",\n\t\"who\",\n\t\"whom\",\n\t\"where\",\n\t\"when\",\n\t\"why\",\n\t\"not\",\n\t\"no\",\n\t\"nor\",\n\t\"so\",\n\t\"if\",\n\t\"then\",\n\t\"than\",\n\t\"too\",\n\t\"very\",\n\t\"just\",\n\t\"about\",\n\t\"above\",\n\t\"after\",\n\t\"again\",\n\t\"all\",\n\t\"also\",\n\t\"am\",\n\t\"any\",\n\t\"because\",\n\t\"before\",\n\t\"between\",\n\t\"both\",\n\t\"but\",\n\t\"by\",\n\t\"from\",\n\t\"into\",\n\t\"more\",\n\t\"most\",\n\t\"other\",\n\t\"out\",\n\t\"over\",\n\t\"own\",\n\t\"same\",\n\t\"some\",\n\t\"such\",\n\t\"there\",\n\t\"up\",\n\t\"down\",\n\t\"here\",\n\t\"at\",\n\t\"as\",\n\t\"while\",\n\t\"through\",\n\t\"during\",\n\t\"each\",\n\t\"few\",\n\t\"further\",\n\t\"once\",\n\t\"only\",\n\t\"until\",\n\t// Korean particles (조사)\n\t\"은\",\n\t\"는\",\n\t\"이\",\n\t\"가\",\n\t\"을\",\n\t\"를\",\n\t\"에\",\n\t\"의\",\n\t\"와\",\n\t\"과\",\n\t\"도\",\n\t\"로\",\n\t\"으로\",\n\t\"에서\",\n\t\"까지\",\n\t\"부터\",\n\t\"만\",\n\t\"라도\",\n\t\"나\",\n\t\"이며\",\n\t\"이고\",\n\t\"이나\",\n\t\"라\",\n\t\"이다\",\n\t\"하다\",\n\t\"된\",\n]);\n\n// ---------------------------------------------------------------------------\n// Tokenizer\n// ---------------------------------------------------------------------------\n\n/**\n * Tokenize text for relevance scoring.\n *\n * 1. Lowercase\n * 2. Split: Latin words via regex, Korean syllable groups via regex\n * 3. Remove stop words\n * 4. Remove short Latin tokens (<=2 chars)\n * 5. Deduplicate\n */\nexport function tokenizeForRelevance(text: string): readonly string[] {\n\tconst lower = text.toLowerCase();\n\tconst raw = lower.match(/[\\uac00-\\ud7a3]+|[a-z0-9]+/g);\n\tif (!raw) {\n\t\treturn [];\n\t}\n\n\tconst seen = new Set<string>();\n\tconst tokens: string[] = [];\n\n\tfor (const token of raw) {\n\t\t// Skip stop words\n\t\tif (STOP_WORDS.has(token)) {\n\t\t\tcontinue;\n\t\t}\n\t\t// For Latin tokens, skip very short ones (<=2 chars)\n\t\tif (/^[a-z0-9]+$/.test(token) && token.length <= 2) {\n\t\t\tcontinue;\n\t\t}\n\t\t// For Korean, keep 2+ syllable groups\n\t\tif (/^[\\uac00-\\ud7a3]+$/.test(token)) {\n\t\t\tif (token.length >= 2 && !seen.has(token)) {\n\t\t\t\tseen.add(token);\n\t\t\t\ttokens.push(token);\n\t\t\t}\n\t\t\tcontinue;\n\t\t}\n\t\t// Deduplicate\n\t\tif (!seen.has(token)) {\n\t\t\tseen.add(token);\n\t\t\ttokens.push(token);\n\t\t}\n\t}\n\n\treturn tokens;\n}\n\n// ---------------------------------------------------------------------------\n// Coverage scoring\n// ---------------------------------------------------------------------------\n\n/**\n * Compute coverage: fraction of query tokens found in the item token set.\n * Optional per-token weights boost high-value matches (name, path).\n */\nfunction computeCoverage(\n\tqueryTokens: readonly string[],\n\titemTokenSet: ReadonlySet<string>,\n\titemTokenWeights?: ReadonlyMap<string, number>,\n): number {\n\tif (queryTokens.length === 0) {\n\t\treturn 0;\n\t}\n\n\tlet matchWeight = 0;\n\tfor (const qt of queryTokens) {\n\t\tif (itemTokenSet.has(qt)) {\n\t\t\tmatchWeight += itemTokenWeights?.get(qt) ?? 1;\n\t\t}\n\t}\n\n\t// Normalize: each query token contributes at most 1 to the numerator\n\treturn Math.min(1, matchWeight / queryTokens.length);\n}\n\n/**\n * Build a token set with optional high-weight tokens.\n */\nfunction buildWeightedTokenSet(\n\tbaseText: string,\n\thighWeightText: string | undefined,\n\tweight: number,\n): { tokens: Set<string>; weights: Map<string, number> } {\n\tconst baseTokens = tokenizeForRelevance(baseText);\n\tconst highTokens = highWeightText ? tokenizeForRelevance(highWeightText) : [];\n\n\tconst tokenSet = new Set<string>();\n\tconst weights = new Map<string, number>();\n\n\tfor (const t of baseTokens) {\n\t\ttokenSet.add(t);\n\t\tif (!weights.has(t)) {\n\t\t\tweights.set(t, 1);\n\t\t}\n\t}\n\tfor (const t of highTokens) {\n\t\ttokenSet.add(t);\n\t\tweights.set(t, Math.max(weights.get(t) ?? 0, weight));\n\t}\n\n\treturn { tokens: tokenSet, weights };\n}\n\n// ---------------------------------------------------------------------------\n// Skill relevance\n// ---------------------------------------------------------------------------\n\nconst SKILL_NEUTRAL_SCORE = 0.3;\nconst NAME_MATCH_BONUS = 0.2;\nconst NAME_TOKEN_WEIGHT = 3;\n\n/**\n * Score skill relevance against a query context.\n *\n * - queryContext undefined or empty -> 0.3 (neutral)\n * - Coverage of query tokens against skill name+description\n * - Name tokens weighted 3x\n * - Name substring match bonus: +0.2 (capped at 1.0)\n */\nexport function scoreSkillRelevance(\n\tskill: { readonly name: string; readonly description: string },\n\tqueryContext: string | undefined,\n): number {\n\tif (!queryContext || queryContext.trim() === \"\") {\n\t\treturn SKILL_NEUTRAL_SCORE;\n\t}\n\n\tconst queryTokens = tokenizeForRelevance(queryContext);\n\tif (queryTokens.length === 0) {\n\t\treturn SKILL_NEUTRAL_SCORE;\n\t}\n\n\t// Build skill token set: description is base, name tokens get 3x weight\n\tconst { tokens: skillTokenSet, weights: skillWeights } = buildWeightedTokenSet(\n\t\tskill.description,\n\t\tskill.name,\n\t\tNAME_TOKEN_WEIGHT,\n\t);\n\n\tlet score = computeCoverage(queryTokens, skillTokenSet, skillWeights);\n\n\t// Name substring bonus: if skill.name appears in query text (or vice versa)\n\tconst queryLower = queryContext.toLowerCase();\n\tconst nameLower = skill.name.toLowerCase();\n\tif (queryLower.includes(nameLower) || nameLower.includes(queryLower.trim())) {\n\t\tscore = Math.min(1, score + NAME_MATCH_BONUS);\n\t}\n\n\treturn clamp01(score);\n}\n\n// ---------------------------------------------------------------------------\n// Context file relevance\n// ---------------------------------------------------------------------------\n\nconst CONTEXT_NEUTRAL_GLOBAL = 0.9;\nconst CONTEXT_NEUTRAL_LOCAL = 0.6;\nconst PATH_TOKEN_WEIGHT = 2;\nconst CONTENT_SAMPLE_CHARS = 2000;\n\n/**\n * Score context file relevance against a query context.\n *\n * - Baseline (no query evidence) -> isGlobal ? 0.9 : 0.6\n * - Coverage of query tokens against path segments + content sample\n * - Path tokens weighted 2x\n * - Coverage allocates the headroom ABOVE the baseline; it never cuts into it\n *\n * A context file is not retrieved reference material — it is the standing\n * instruction set the agent is required to follow, and it applies whether or\n * not the user's current sentence reuses its vocabulary. Scoring it purely by\n * lexical overlap made the rules rank LOWEST exactly when the agent was doing\n * work: this repository's own AGENTS.md scored 0.9 idle but 0.1 under queries\n * like \"rename this variable\". Coverage therefore raises the score from the\n * baseline toward 1 instead of replacing it, which also reconciles the two\n * readings the function used to have — an absent query and a present-but-\n * non-overlapping query now agree that there is simply no evidence to add.\n *\n * Lexical scoring stays the right metric for skills, which really are\n * topic-scoped; see scoreSkillRelevance.\n */\nexport function scoreContextFileRelevance(\n\tcontextFile: { readonly path: string; readonly content: string; readonly isGlobal: boolean },\n\tqueryContext: string | undefined,\n): number {\n\tif (!queryContext || queryContext.trim() === \"\") {\n\t\treturn contextFile.isGlobal ? CONTEXT_NEUTRAL_GLOBAL : CONTEXT_NEUTRAL_LOCAL;\n\t}\n\n\tconst queryTokens = tokenizeForRelevance(queryContext);\n\tif (queryTokens.length === 0) {\n\t\treturn contextFile.isGlobal ? CONTEXT_NEUTRAL_GLOBAL : CONTEXT_NEUTRAL_LOCAL;\n\t}\n\n\t// Path segments as high-weight text\n\tconst pathText = extractPathSegments(contextFile.path);\n\n\t// Content sample for performance\n\tconst contentSample =\n\t\tcontextFile.content.length > CONTENT_SAMPLE_CHARS\n\t\t\t? contextFile.content.slice(0, CONTENT_SAMPLE_CHARS)\n\t\t\t: contextFile.content;\n\n\t// Build token set: content is base, path tokens get 2x weight\n\tconst { tokens: fileTokenSet, weights: fileWeights } = buildWeightedTokenSet(\n\t\tcontentSample,\n\t\tpathText,\n\t\tPATH_TOKEN_WEIGHT,\n\t);\n\n\tconst coverage = computeCoverage(queryTokens, fileTokenSet, fileWeights);\n\n\t// The baseline is a floor, not a starting guess: query evidence distributes\n\t// the remaining headroom above it. Zero coverage returns the baseline exactly,\n\t// matching the no-query branch above.\n\tconst baseline = contextFile.isGlobal ? CONTEXT_NEUTRAL_GLOBAL : CONTEXT_NEUTRAL_LOCAL;\n\treturn clamp01(baseline + (1 - baseline) * clamp01(coverage));\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Extract meaningful segments from a file path for token matching.\n */\nfunction extractPathSegments(path: string): string {\n\treturn path\n\t\t.toLowerCase()\n\t\t.split(/[/\\\\._-]+/)\n\t\t.filter((seg) => seg.length > 0)\n\t\t.join(\" \");\n}\n\nfunction clamp01(value: number): number {\n\treturn Math.max(0, Math.min(1, value));\n}\n"]}
@@ -277,15 +277,27 @@ export function scoreSkillRelevance(skill, queryContext) {
277
277
  const CONTEXT_NEUTRAL_GLOBAL = 0.9;
278
278
  const CONTEXT_NEUTRAL_LOCAL = 0.6;
279
279
  const PATH_TOKEN_WEIGHT = 2;
280
- const GLOBAL_BONUS = 0.1;
281
280
  const CONTENT_SAMPLE_CHARS = 2000;
282
281
  /**
283
282
  * Score context file relevance against a query context.
284
283
  *
285
- * - queryContext undefined -> isGlobal ? 0.9 : 0.6
284
+ * - Baseline (no query evidence) -> isGlobal ? 0.9 : 0.6
286
285
  * - Coverage of query tokens against path segments + content sample
287
286
  * - Path tokens weighted 2x
288
- * - isGlobal bonus: +0.1
287
+ * - Coverage allocates the headroom ABOVE the baseline; it never cuts into it
288
+ *
289
+ * A context file is not retrieved reference material — it is the standing
290
+ * instruction set the agent is required to follow, and it applies whether or
291
+ * not the user's current sentence reuses its vocabulary. Scoring it purely by
292
+ * lexical overlap made the rules rank LOWEST exactly when the agent was doing
293
+ * work: this repository's own AGENTS.md scored 0.9 idle but 0.1 under queries
294
+ * like "rename this variable". Coverage therefore raises the score from the
295
+ * baseline toward 1 instead of replacing it, which also reconciles the two
296
+ * readings the function used to have — an absent query and a present-but-
297
+ * non-overlapping query now agree that there is simply no evidence to add.
298
+ *
299
+ * Lexical scoring stays the right metric for skills, which really are
300
+ * topic-scoped; see scoreSkillRelevance.
289
301
  */
290
302
  export function scoreContextFileRelevance(contextFile, queryContext) {
291
303
  if (!queryContext || queryContext.trim() === "") {
@@ -303,12 +315,12 @@ export function scoreContextFileRelevance(contextFile, queryContext) {
303
315
  : contextFile.content;
304
316
  // Build token set: content is base, path tokens get 2x weight
305
317
  const { tokens: fileTokenSet, weights: fileWeights } = buildWeightedTokenSet(contentSample, pathText, PATH_TOKEN_WEIGHT);
306
- let score = computeCoverage(queryTokens, fileTokenSet, fileWeights);
307
- // Global file bonus
308
- if (contextFile.isGlobal) {
309
- score = Math.min(1, score + GLOBAL_BONUS);
310
- }
311
- return clamp01(score);
318
+ const coverage = computeCoverage(queryTokens, fileTokenSet, fileWeights);
319
+ // The baseline is a floor, not a starting guess: query evidence distributes
320
+ // the remaining headroom above it. Zero coverage returns the baseline exactly,
321
+ // matching the no-query branch above.
322
+ const baseline = contextFile.isGlobal ? CONTEXT_NEUTRAL_GLOBAL : CONTEXT_NEUTRAL_LOCAL;
323
+ return clamp01(baseline + (1 - baseline) * clamp01(coverage));
312
324
  }
313
325
  // ---------------------------------------------------------------------------
314
326
  // Helpers
@@ -1 +1 @@
1
- {"version":3,"file":"context-budget-relevance.js","sourceRoot":"","sources":["../../src/core/context-budget-relevance.ts"],"names":[],"mappings":"AAAA;;;;;;;;;GASG;AAEH,8EAA8E;AAC9E,mDAAiD;AACjD,8EAA8E;AAE9E,MAAM,UAAU,GAAwB,IAAI,GAAG,CAAC;IAC/C,4BAA4B;IAC5B,KAAK;IACL,GAAG;IACH,IAAI;IACJ,IAAI;IACJ,KAAK;IACL,KAAK;IACL,MAAM;IACN,IAAI;IACJ,MAAM;IACN,OAAO;IACP,MAAM;IACN,KAAK;IACL,KAAK;IACL,IAAI;IACJ,MAAM;IACN,KAAK;IACL,MAAM;IACN,OAAO;IACP,OAAO;IACP,QAAQ;IACR,KAAK;IACL,OAAO;IACP,OAAO;IACP,KAAK;IACL,IAAI;IACJ,IAAI;IACJ,IAAI;IACJ,IAAI;IACJ,KAAK;IACL,KAAK;IACL,IAAI;IACJ,MAAM;IACN,KAAK;IACL,GAAG;IACH,KAAK;IACL,IAAI;IACJ,KAAK;IACL,IAAI;IACJ,IAAI;IACJ,MAAM;IACN,IAAI;IACJ,KAAK;IACL,KAAK;IACL,IAAI;IACJ,MAAM;IACN,IAAI;IACJ,MAAM;IACN,KAAK;IACL,KAAK;IACL,KAAK;IACL,OAAO;IACP,MAAM;IACN,MAAM;IACN,OAAO;IACP,OAAO;IACP,MAAM;IACN,OAAO;IACP,KAAK;IACL,MAAM;IACN,OAAO;IACP,MAAM;IACN,KAAK;IACL,KAAK;IACL,IAAI;IACJ,KAAK;IACL,IAAI;IACJ,IAAI;IACJ,MAAM;IACN,MAAM;IACN,KAAK;IACL,MAAM;IACN,MAAM;IACN,OAAO;IACP,OAAO;IACP,OAAO;IACP,OAAO;IACP,KAAK;IACL,MAAM;IACN,IAAI;IACJ,KAAK;IACL,SAAS;IACT,QAAQ;IACR,SAAS;IACT,MAAM;IACN,KAAK;IACL,IAAI;IACJ,MAAM;IACN,MAAM;IACN,MAAM;IACN,MAAM;IACN,OAAO;IACP,KAAK;IACL,MAAM;IACN,KAAK;IACL,MAAM;IACN,MAAM;IACN,MAAM;IACN,OAAO;IACP,IAAI;IACJ,MAAM;IACN,MAAM;IACN,IAAI;IACJ,IAAI;IACJ,OAAO;IACP,SAAS;IACT,QAAQ;IACR,MAAM;IACN,KAAK;IACL,SAAS;IACT,MAAM;IACN,MAAM;IACN,OAAO;IACP,4BAAwB;IACxB,KAAG;IACH,KAAG;IACH,KAAG;IACH,KAAG;IACH,KAAG;IACH,KAAG;IACH,KAAG;IACH,KAAG;IACH,KAAG;IACH,KAAG;IACH,KAAG;IACH,KAAG;IACH,QAAI;IACJ,QAAI;IACJ,QAAI;IACJ,QAAI;IACJ,KAAG;IACH,QAAI;IACJ,KAAG;IACH,QAAI;IACJ,QAAI;IACJ,QAAI;IACJ,KAAG;IACH,QAAI;IACJ,QAAI;IACJ,KAAG;CACH,CAAC,CAAC;AAEH,8EAA8E;AAC9E,YAAY;AACZ,8EAA8E;AAE9E;;;;;;;;GAQG;AACH,MAAM,UAAU,oBAAoB,CAAC,IAAY,EAAqB;IACrE,MAAM,KAAK,GAAG,IAAI,CAAC,WAAW,EAAE,CAAC;IACjC,MAAM,GAAG,GAAG,KAAK,CAAC,KAAK,CAAC,6BAA6B,CAAC,CAAC;IACvD,IAAI,CAAC,GAAG,EAAE,CAAC;QACV,OAAO,EAAE,CAAC;IACX,CAAC;IAED,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;IAC/B,MAAM,MAAM,GAAa,EAAE,CAAC;IAE5B,KAAK,MAAM,KAAK,IAAI,GAAG,EAAE,CAAC;QACzB,kBAAkB;QAClB,IAAI,UAAU,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC;YAC3B,SAAS;QACV,CAAC;QACD,qDAAqD;QACrD,IAAI,aAAa,CAAC,IAAI,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC,MAAM,IAAI,CAAC,EAAE,CAAC;YACpD,SAAS;QACV,CAAC;QACD,sCAAsC;QACtC,IAAI,oBAAoB,CAAC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;YACtC,IAAI,KAAK,CAAC,MAAM,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC;gBAC3C,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;gBAChB,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YACpB,CAAC;YACD,SAAS;QACV,CAAC;QACD,cAAc;QACd,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC;YACtB,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;YAChB,MAAM,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACpB,CAAC;IACF,CAAC;IAED,OAAO,MAAM,CAAC;AAAA,CACd;AAED,8EAA8E;AAC9E,mBAAmB;AACnB,8EAA8E;AAE9E;;;GAGG;AACH,SAAS,eAAe,CACvB,WAA8B,EAC9B,YAAiC,EACjC,gBAA8C,EACrC;IACT,IAAI,WAAW,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC9B,OAAO,CAAC,CAAC;IACV,CAAC;IAED,IAAI,WAAW,GAAG,CAAC,CAAC;IACpB,KAAK,MAAM,EAAE,IAAI,WAAW,EAAE,CAAC;QAC9B,IAAI,YAAY,CAAC,GAAG,CAAC,EAAE,CAAC,EAAE,CAAC;YAC1B,WAAW,IAAI,gBAAgB,EAAE,GAAG,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC;QAC/C,CAAC;IACF,CAAC;IAED,qEAAqE;IACrE,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,WAAW,GAAG,WAAW,CAAC,MAAM,CAAC,CAAC;AAAA,CACrD;AAED;;GAEG;AACH,SAAS,qBAAqB,CAC7B,QAAgB,EAChB,cAAkC,EAClC,MAAc,EAC0C;IACxD,MAAM,UAAU,GAAG,oBAAoB,CAAC,QAAQ,CAAC,CAAC;IAClD,MAAM,UAAU,GAAG,cAAc,CAAC,CAAC,CAAC,oBAAoB,CAAC,cAAc,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;IAE9E,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAU,CAAC;IACnC,MAAM,OAAO,GAAG,IAAI,GAAG,EAAkB,CAAC;IAE1C,KAAK,MAAM,CAAC,IAAI,UAAU,EAAE,CAAC;QAC5B,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;QAChB,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;YACrB,OAAO,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;QACnB,CAAC;IACF,CAAC;IACD,KAAK,MAAM,CAAC,IAAI,UAAU,EAAE,CAAC;QAC5B,QAAQ,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;QAChB,OAAO,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,EAAE,MAAM,CAAC,CAAC,CAAC;IACvD,CAAC;IAED,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,OAAO,EAAE,CAAC;AAAA,CACrC;AAED,8EAA8E;AAC9E,kBAAkB;AAClB,8EAA8E;AAE9E,MAAM,mBAAmB,GAAG,GAAG,CAAC;AAChC,MAAM,gBAAgB,GAAG,GAAG,CAAC;AAC7B,MAAM,iBAAiB,GAAG,CAAC,CAAC;AAE5B;;;;;;;GAOG;AACH,MAAM,UAAU,mBAAmB,CAClC,KAA8D,EAC9D,YAAgC,EACvB;IACT,IAAI,CAAC,YAAY,IAAI,YAAY,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC;QACjD,OAAO,mBAAmB,CAAC;IAC5B,CAAC;IAED,MAAM,WAAW,GAAG,oBAAoB,CAAC,YAAY,CAAC,CAAC;IACvD,IAAI,WAAW,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC9B,OAAO,mBAAmB,CAAC;IAC5B,CAAC;IAED,wEAAwE;IACxE,MAAM,EAAE,MAAM,EAAE,aAAa,EAAE,OAAO,EAAE,YAAY,EAAE,GAAG,qBAAqB,CAC7E,KAAK,CAAC,WAAW,EACjB,KAAK,CAAC,IAAI,EACV,iBAAiB,CACjB,CAAC;IAEF,IAAI,KAAK,GAAG,eAAe,CAAC,WAAW,EAAE,aAAa,EAAE,YAAY,CAAC,CAAC;IAEtE,4EAA4E;IAC5E,MAAM,UAAU,GAAG,YAAY,CAAC,WAAW,EAAE,CAAC;IAC9C,MAAM,SAAS,GAAG,KAAK,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC;IAC3C,IAAI,UAAU,CAAC,QAAQ,CAAC,SAAS,CAAC,IAAI,SAAS,CAAC,QAAQ,CAAC,UAAU,CAAC,IAAI,EAAE,CAAC,EAAE,CAAC;QAC7E,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,GAAG,gBAAgB,CAAC,CAAC;IAC/C,CAAC;IAED,OAAO,OAAO,CAAC,KAAK,CAAC,CAAC;AAAA,CACtB;AAED,8EAA8E;AAC9E,yBAAyB;AACzB,8EAA8E;AAE9E,MAAM,sBAAsB,GAAG,GAAG,CAAC;AACnC,MAAM,qBAAqB,GAAG,GAAG,CAAC;AAClC,MAAM,iBAAiB,GAAG,CAAC,CAAC;AAC5B,MAAM,YAAY,GAAG,GAAG,CAAC;AACzB,MAAM,oBAAoB,GAAG,IAAI,CAAC;AAElC;;;;;;;GAOG;AACH,MAAM,UAAU,yBAAyB,CACxC,WAA4F,EAC5F,YAAgC,EACvB;IACT,IAAI,CAAC,YAAY,IAAI,YAAY,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC;QACjD,OAAO,WAAW,CAAC,QAAQ,CAAC,CAAC,CAAC,sBAAsB,CAAC,CAAC,CAAC,qBAAqB,CAAC;IAC9E,CAAC;IAED,MAAM,WAAW,GAAG,oBAAoB,CAAC,YAAY,CAAC,CAAC;IACvD,IAAI,WAAW,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;QAC9B,OAAO,WAAW,CAAC,QAAQ,CAAC,CAAC,CAAC,sBAAsB,CAAC,CAAC,CAAC,qBAAqB,CAAC;IAC9E,CAAC;IAED,oCAAoC;IACpC,MAAM,QAAQ,GAAG,mBAAmB,CAAC,WAAW,CAAC,IAAI,CAAC,CAAC;IAEvD,iCAAiC;IACjC,MAAM,aAAa,GAClB,WAAW,CAAC,OAAO,CAAC,MAAM,GAAG,oBAAoB;QAChD,CAAC,CAAC,WAAW,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC,EAAE,oBAAoB,CAAC;QACpD,CAAC,CAAC,WAAW,CAAC,OAAO,CAAC;IAExB,8DAA8D;IAC9D,MAAM,EAAE,MAAM,EAAE,YAAY,EAAE,OAAO,EAAE,WAAW,EAAE,GAAG,qBAAqB,CAC3E,aAAa,EACb,QAAQ,EACR,iBAAiB,CACjB,CAAC;IAEF,IAAI,KAAK,GAAG,eAAe,CAAC,WAAW,EAAE,YAAY,EAAE,WAAW,CAAC,CAAC;IAEpE,oBAAoB;IACpB,IAAI,WAAW,CAAC,QAAQ,EAAE,CAAC;QAC1B,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,GAAG,YAAY,CAAC,CAAC;IAC3C,CAAC;IAED,OAAO,OAAO,CAAC,KAAK,CAAC,CAAC;AAAA,CACtB;AAED,8EAA8E;AAC9E,UAAU;AACV,8EAA8E;AAE9E;;GAEG;AACH,SAAS,mBAAmB,CAAC,IAAY,EAAU;IAClD,OAAO,IAAI;SACT,WAAW,EAAE;SACb,KAAK,CAAC,WAAW,CAAC;SAClB,MAAM,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,MAAM,GAAG,CAAC,CAAC;SAC/B,IAAI,CAAC,GAAG,CAAC,CAAC;AAAA,CACZ;AAED,SAAS,OAAO,CAAC,KAAa,EAAU;IACvC,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,KAAK,CAAC,CAAC,CAAC;AAAA,CACvC","sourcesContent":["/**\n * Query-aware relevance scoring for context budget items.\n *\n * Replaces heuristic proxies (description length, isGlobal hardcoding)\n * with token-based coverage scoring that measures semantic overlap\n * between the current query context and each resource item.\n *\n * Design: Coverage-based IR (not Jaccard) — avoids penalizing long\n * descriptions whose large union would dilute the score.\n */\n\n// ---------------------------------------------------------------------------\n// Stop words — English + common Korean particles\n// ---------------------------------------------------------------------------\n\nconst STOP_WORDS: ReadonlySet<string> = new Set([\n\t// English common stop words\n\t\"the\",\n\t\"a\",\n\t\"an\",\n\t\"is\",\n\t\"are\",\n\t\"was\",\n\t\"were\",\n\t\"be\",\n\t\"been\",\n\t\"being\",\n\t\"have\",\n\t\"has\",\n\t\"had\",\n\t\"do\",\n\t\"does\",\n\t\"did\",\n\t\"will\",\n\t\"would\",\n\t\"could\",\n\t\"should\",\n\t\"may\",\n\t\"might\",\n\t\"shall\",\n\t\"can\",\n\t\"to\",\n\t\"of\",\n\t\"in\",\n\t\"on\",\n\t\"for\",\n\t\"and\",\n\t\"or\",\n\t\"with\",\n\t\"how\",\n\t\"i\",\n\t\"you\",\n\t\"he\",\n\t\"she\",\n\t\"it\",\n\t\"we\",\n\t\"they\",\n\t\"me\",\n\t\"him\",\n\t\"her\",\n\t\"us\",\n\t\"them\",\n\t\"my\",\n\t\"your\",\n\t\"his\",\n\t\"its\",\n\t\"our\",\n\t\"their\",\n\t\"this\",\n\t\"that\",\n\t\"these\",\n\t\"those\",\n\t\"what\",\n\t\"which\",\n\t\"who\",\n\t\"whom\",\n\t\"where\",\n\t\"when\",\n\t\"why\",\n\t\"not\",\n\t\"no\",\n\t\"nor\",\n\t\"so\",\n\t\"if\",\n\t\"then\",\n\t\"than\",\n\t\"too\",\n\t\"very\",\n\t\"just\",\n\t\"about\",\n\t\"above\",\n\t\"after\",\n\t\"again\",\n\t\"all\",\n\t\"also\",\n\t\"am\",\n\t\"any\",\n\t\"because\",\n\t\"before\",\n\t\"between\",\n\t\"both\",\n\t\"but\",\n\t\"by\",\n\t\"from\",\n\t\"into\",\n\t\"more\",\n\t\"most\",\n\t\"other\",\n\t\"out\",\n\t\"over\",\n\t\"own\",\n\t\"same\",\n\t\"some\",\n\t\"such\",\n\t\"there\",\n\t\"up\",\n\t\"down\",\n\t\"here\",\n\t\"at\",\n\t\"as\",\n\t\"while\",\n\t\"through\",\n\t\"during\",\n\t\"each\",\n\t\"few\",\n\t\"further\",\n\t\"once\",\n\t\"only\",\n\t\"until\",\n\t// Korean particles (조사)\n\t\"은\",\n\t\"는\",\n\t\"이\",\n\t\"가\",\n\t\"을\",\n\t\"를\",\n\t\"에\",\n\t\"의\",\n\t\"와\",\n\t\"과\",\n\t\"도\",\n\t\"로\",\n\t\"으로\",\n\t\"에서\",\n\t\"까지\",\n\t\"부터\",\n\t\"만\",\n\t\"라도\",\n\t\"나\",\n\t\"이며\",\n\t\"이고\",\n\t\"이나\",\n\t\"라\",\n\t\"이다\",\n\t\"하다\",\n\t\"된\",\n]);\n\n// ---------------------------------------------------------------------------\n// Tokenizer\n// ---------------------------------------------------------------------------\n\n/**\n * Tokenize text for relevance scoring.\n *\n * 1. Lowercase\n * 2. Split: Latin words via regex, Korean syllable groups via regex\n * 3. Remove stop words\n * 4. Remove short Latin tokens (<=2 chars)\n * 5. Deduplicate\n */\nexport function tokenizeForRelevance(text: string): readonly string[] {\n\tconst lower = text.toLowerCase();\n\tconst raw = lower.match(/[\\uac00-\\ud7a3]+|[a-z0-9]+/g);\n\tif (!raw) {\n\t\treturn [];\n\t}\n\n\tconst seen = new Set<string>();\n\tconst tokens: string[] = [];\n\n\tfor (const token of raw) {\n\t\t// Skip stop words\n\t\tif (STOP_WORDS.has(token)) {\n\t\t\tcontinue;\n\t\t}\n\t\t// For Latin tokens, skip very short ones (<=2 chars)\n\t\tif (/^[a-z0-9]+$/.test(token) && token.length <= 2) {\n\t\t\tcontinue;\n\t\t}\n\t\t// For Korean, keep 2+ syllable groups\n\t\tif (/^[\\uac00-\\ud7a3]+$/.test(token)) {\n\t\t\tif (token.length >= 2 && !seen.has(token)) {\n\t\t\t\tseen.add(token);\n\t\t\t\ttokens.push(token);\n\t\t\t}\n\t\t\tcontinue;\n\t\t}\n\t\t// Deduplicate\n\t\tif (!seen.has(token)) {\n\t\t\tseen.add(token);\n\t\t\ttokens.push(token);\n\t\t}\n\t}\n\n\treturn tokens;\n}\n\n// ---------------------------------------------------------------------------\n// Coverage scoring\n// ---------------------------------------------------------------------------\n\n/**\n * Compute coverage: fraction of query tokens found in the item token set.\n * Optional per-token weights boost high-value matches (name, path).\n */\nfunction computeCoverage(\n\tqueryTokens: readonly string[],\n\titemTokenSet: ReadonlySet<string>,\n\titemTokenWeights?: ReadonlyMap<string, number>,\n): number {\n\tif (queryTokens.length === 0) {\n\t\treturn 0;\n\t}\n\n\tlet matchWeight = 0;\n\tfor (const qt of queryTokens) {\n\t\tif (itemTokenSet.has(qt)) {\n\t\t\tmatchWeight += itemTokenWeights?.get(qt) ?? 1;\n\t\t}\n\t}\n\n\t// Normalize: each query token contributes at most 1 to the numerator\n\treturn Math.min(1, matchWeight / queryTokens.length);\n}\n\n/**\n * Build a token set with optional high-weight tokens.\n */\nfunction buildWeightedTokenSet(\n\tbaseText: string,\n\thighWeightText: string | undefined,\n\tweight: number,\n): { tokens: Set<string>; weights: Map<string, number> } {\n\tconst baseTokens = tokenizeForRelevance(baseText);\n\tconst highTokens = highWeightText ? tokenizeForRelevance(highWeightText) : [];\n\n\tconst tokenSet = new Set<string>();\n\tconst weights = new Map<string, number>();\n\n\tfor (const t of baseTokens) {\n\t\ttokenSet.add(t);\n\t\tif (!weights.has(t)) {\n\t\t\tweights.set(t, 1);\n\t\t}\n\t}\n\tfor (const t of highTokens) {\n\t\ttokenSet.add(t);\n\t\tweights.set(t, Math.max(weights.get(t) ?? 0, weight));\n\t}\n\n\treturn { tokens: tokenSet, weights };\n}\n\n// ---------------------------------------------------------------------------\n// Skill relevance\n// ---------------------------------------------------------------------------\n\nconst SKILL_NEUTRAL_SCORE = 0.3;\nconst NAME_MATCH_BONUS = 0.2;\nconst NAME_TOKEN_WEIGHT = 3;\n\n/**\n * Score skill relevance against a query context.\n *\n * - queryContext undefined or empty -> 0.3 (neutral)\n * - Coverage of query tokens against skill name+description\n * - Name tokens weighted 3x\n * - Name substring match bonus: +0.2 (capped at 1.0)\n */\nexport function scoreSkillRelevance(\n\tskill: { readonly name: string; readonly description: string },\n\tqueryContext: string | undefined,\n): number {\n\tif (!queryContext || queryContext.trim() === \"\") {\n\t\treturn SKILL_NEUTRAL_SCORE;\n\t}\n\n\tconst queryTokens = tokenizeForRelevance(queryContext);\n\tif (queryTokens.length === 0) {\n\t\treturn SKILL_NEUTRAL_SCORE;\n\t}\n\n\t// Build skill token set: description is base, name tokens get 3x weight\n\tconst { tokens: skillTokenSet, weights: skillWeights } = buildWeightedTokenSet(\n\t\tskill.description,\n\t\tskill.name,\n\t\tNAME_TOKEN_WEIGHT,\n\t);\n\n\tlet score = computeCoverage(queryTokens, skillTokenSet, skillWeights);\n\n\t// Name substring bonus: if skill.name appears in query text (or vice versa)\n\tconst queryLower = queryContext.toLowerCase();\n\tconst nameLower = skill.name.toLowerCase();\n\tif (queryLower.includes(nameLower) || nameLower.includes(queryLower.trim())) {\n\t\tscore = Math.min(1, score + NAME_MATCH_BONUS);\n\t}\n\n\treturn clamp01(score);\n}\n\n// ---------------------------------------------------------------------------\n// Context file relevance\n// ---------------------------------------------------------------------------\n\nconst CONTEXT_NEUTRAL_GLOBAL = 0.9;\nconst CONTEXT_NEUTRAL_LOCAL = 0.6;\nconst PATH_TOKEN_WEIGHT = 2;\nconst GLOBAL_BONUS = 0.1;\nconst CONTENT_SAMPLE_CHARS = 2000;\n\n/**\n * Score context file relevance against a query context.\n *\n * - queryContext undefined -> isGlobal ? 0.9 : 0.6\n * - Coverage of query tokens against path segments + content sample\n * - Path tokens weighted 2x\n * - isGlobal bonus: +0.1\n */\nexport function scoreContextFileRelevance(\n\tcontextFile: { readonly path: string; readonly content: string; readonly isGlobal: boolean },\n\tqueryContext: string | undefined,\n): number {\n\tif (!queryContext || queryContext.trim() === \"\") {\n\t\treturn contextFile.isGlobal ? CONTEXT_NEUTRAL_GLOBAL : CONTEXT_NEUTRAL_LOCAL;\n\t}\n\n\tconst queryTokens = tokenizeForRelevance(queryContext);\n\tif (queryTokens.length === 0) {\n\t\treturn contextFile.isGlobal ? CONTEXT_NEUTRAL_GLOBAL : CONTEXT_NEUTRAL_LOCAL;\n\t}\n\n\t// Path segments as high-weight text\n\tconst pathText = extractPathSegments(contextFile.path);\n\n\t// Content sample for performance\n\tconst contentSample =\n\t\tcontextFile.content.length > CONTENT_SAMPLE_CHARS\n\t\t\t? contextFile.content.slice(0, CONTENT_SAMPLE_CHARS)\n\t\t\t: contextFile.content;\n\n\t// Build token set: content is base, path tokens get 2x weight\n\tconst { tokens: fileTokenSet, weights: fileWeights } = buildWeightedTokenSet(\n\t\tcontentSample,\n\t\tpathText,\n\t\tPATH_TOKEN_WEIGHT,\n\t);\n\n\tlet score = computeCoverage(queryTokens, fileTokenSet, fileWeights);\n\n\t// Global file bonus\n\tif (contextFile.isGlobal) {\n\t\tscore = Math.min(1, score + GLOBAL_BONUS);\n\t}\n\n\treturn clamp01(score);\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Extract meaningful segments from a file path for token matching.\n */\nfunction extractPathSegments(path: string): string {\n\treturn path\n\t\t.toLowerCase()\n\t\t.split(/[/\\\\._-]+/)\n\t\t.filter((seg) => seg.length > 0)\n\t\t.join(\" \");\n}\n\nfunction clamp01(value: number): number {\n\treturn Math.max(0, Math.min(1, value));\n}\n"]}
1
+ {"version":3,"file":"context-budget-relevance.js","sourceRoot":"","sources":["../../src/core/context-budget-relevance.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Query-aware relevance scoring for context budget items.\n *\n * Replaces heuristic proxies (description length, isGlobal hardcoding)\n * with token-based coverage scoring that measures semantic overlap\n * between the current query context and each resource item.\n *\n * Design: Coverage-based IR (not Jaccard) — avoids penalizing long\n * descriptions whose large union would dilute the score.\n */\n\n// ---------------------------------------------------------------------------\n// Stop words — English + common Korean particles\n// ---------------------------------------------------------------------------\n\nconst STOP_WORDS: ReadonlySet<string> = new Set([\n\t// English common stop words\n\t\"the\",\n\t\"a\",\n\t\"an\",\n\t\"is\",\n\t\"are\",\n\t\"was\",\n\t\"were\",\n\t\"be\",\n\t\"been\",\n\t\"being\",\n\t\"have\",\n\t\"has\",\n\t\"had\",\n\t\"do\",\n\t\"does\",\n\t\"did\",\n\t\"will\",\n\t\"would\",\n\t\"could\",\n\t\"should\",\n\t\"may\",\n\t\"might\",\n\t\"shall\",\n\t\"can\",\n\t\"to\",\n\t\"of\",\n\t\"in\",\n\t\"on\",\n\t\"for\",\n\t\"and\",\n\t\"or\",\n\t\"with\",\n\t\"how\",\n\t\"i\",\n\t\"you\",\n\t\"he\",\n\t\"she\",\n\t\"it\",\n\t\"we\",\n\t\"they\",\n\t\"me\",\n\t\"him\",\n\t\"her\",\n\t\"us\",\n\t\"them\",\n\t\"my\",\n\t\"your\",\n\t\"his\",\n\t\"its\",\n\t\"our\",\n\t\"their\",\n\t\"this\",\n\t\"that\",\n\t\"these\",\n\t\"those\",\n\t\"what\",\n\t\"which\",\n\t\"who\",\n\t\"whom\",\n\t\"where\",\n\t\"when\",\n\t\"why\",\n\t\"not\",\n\t\"no\",\n\t\"nor\",\n\t\"so\",\n\t\"if\",\n\t\"then\",\n\t\"than\",\n\t\"too\",\n\t\"very\",\n\t\"just\",\n\t\"about\",\n\t\"above\",\n\t\"after\",\n\t\"again\",\n\t\"all\",\n\t\"also\",\n\t\"am\",\n\t\"any\",\n\t\"because\",\n\t\"before\",\n\t\"between\",\n\t\"both\",\n\t\"but\",\n\t\"by\",\n\t\"from\",\n\t\"into\",\n\t\"more\",\n\t\"most\",\n\t\"other\",\n\t\"out\",\n\t\"over\",\n\t\"own\",\n\t\"same\",\n\t\"some\",\n\t\"such\",\n\t\"there\",\n\t\"up\",\n\t\"down\",\n\t\"here\",\n\t\"at\",\n\t\"as\",\n\t\"while\",\n\t\"through\",\n\t\"during\",\n\t\"each\",\n\t\"few\",\n\t\"further\",\n\t\"once\",\n\t\"only\",\n\t\"until\",\n\t// Korean particles (조사)\n\t\"은\",\n\t\"는\",\n\t\"이\",\n\t\"가\",\n\t\"을\",\n\t\"를\",\n\t\"에\",\n\t\"의\",\n\t\"와\",\n\t\"과\",\n\t\"도\",\n\t\"로\",\n\t\"으로\",\n\t\"에서\",\n\t\"까지\",\n\t\"부터\",\n\t\"만\",\n\t\"라도\",\n\t\"나\",\n\t\"이며\",\n\t\"이고\",\n\t\"이나\",\n\t\"라\",\n\t\"이다\",\n\t\"하다\",\n\t\"된\",\n]);\n\n// ---------------------------------------------------------------------------\n// Tokenizer\n// ---------------------------------------------------------------------------\n\n/**\n * Tokenize text for relevance scoring.\n *\n * 1. Lowercase\n * 2. Split: Latin words via regex, Korean syllable groups via regex\n * 3. Remove stop words\n * 4. Remove short Latin tokens (<=2 chars)\n * 5. Deduplicate\n */\nexport function tokenizeForRelevance(text: string): readonly string[] {\n\tconst lower = text.toLowerCase();\n\tconst raw = lower.match(/[\\uac00-\\ud7a3]+|[a-z0-9]+/g);\n\tif (!raw) {\n\t\treturn [];\n\t}\n\n\tconst seen = new Set<string>();\n\tconst tokens: string[] = [];\n\n\tfor (const token of raw) {\n\t\t// Skip stop words\n\t\tif (STOP_WORDS.has(token)) {\n\t\t\tcontinue;\n\t\t}\n\t\t// For Latin tokens, skip very short ones (<=2 chars)\n\t\tif (/^[a-z0-9]+$/.test(token) && token.length <= 2) {\n\t\t\tcontinue;\n\t\t}\n\t\t// For Korean, keep 2+ syllable groups\n\t\tif (/^[\\uac00-\\ud7a3]+$/.test(token)) {\n\t\t\tif (token.length >= 2 && !seen.has(token)) {\n\t\t\t\tseen.add(token);\n\t\t\t\ttokens.push(token);\n\t\t\t}\n\t\t\tcontinue;\n\t\t}\n\t\t// Deduplicate\n\t\tif (!seen.has(token)) {\n\t\t\tseen.add(token);\n\t\t\ttokens.push(token);\n\t\t}\n\t}\n\n\treturn tokens;\n}\n\n// ---------------------------------------------------------------------------\n// Coverage scoring\n// ---------------------------------------------------------------------------\n\n/**\n * Compute coverage: fraction of query tokens found in the item token set.\n * Optional per-token weights boost high-value matches (name, path).\n */\nfunction computeCoverage(\n\tqueryTokens: readonly string[],\n\titemTokenSet: ReadonlySet<string>,\n\titemTokenWeights?: ReadonlyMap<string, number>,\n): number {\n\tif (queryTokens.length === 0) {\n\t\treturn 0;\n\t}\n\n\tlet matchWeight = 0;\n\tfor (const qt of queryTokens) {\n\t\tif (itemTokenSet.has(qt)) {\n\t\t\tmatchWeight += itemTokenWeights?.get(qt) ?? 1;\n\t\t}\n\t}\n\n\t// Normalize: each query token contributes at most 1 to the numerator\n\treturn Math.min(1, matchWeight / queryTokens.length);\n}\n\n/**\n * Build a token set with optional high-weight tokens.\n */\nfunction buildWeightedTokenSet(\n\tbaseText: string,\n\thighWeightText: string | undefined,\n\tweight: number,\n): { tokens: Set<string>; weights: Map<string, number> } {\n\tconst baseTokens = tokenizeForRelevance(baseText);\n\tconst highTokens = highWeightText ? tokenizeForRelevance(highWeightText) : [];\n\n\tconst tokenSet = new Set<string>();\n\tconst weights = new Map<string, number>();\n\n\tfor (const t of baseTokens) {\n\t\ttokenSet.add(t);\n\t\tif (!weights.has(t)) {\n\t\t\tweights.set(t, 1);\n\t\t}\n\t}\n\tfor (const t of highTokens) {\n\t\ttokenSet.add(t);\n\t\tweights.set(t, Math.max(weights.get(t) ?? 0, weight));\n\t}\n\n\treturn { tokens: tokenSet, weights };\n}\n\n// ---------------------------------------------------------------------------\n// Skill relevance\n// ---------------------------------------------------------------------------\n\nconst SKILL_NEUTRAL_SCORE = 0.3;\nconst NAME_MATCH_BONUS = 0.2;\nconst NAME_TOKEN_WEIGHT = 3;\n\n/**\n * Score skill relevance against a query context.\n *\n * - queryContext undefined or empty -> 0.3 (neutral)\n * - Coverage of query tokens against skill name+description\n * - Name tokens weighted 3x\n * - Name substring match bonus: +0.2 (capped at 1.0)\n */\nexport function scoreSkillRelevance(\n\tskill: { readonly name: string; readonly description: string },\n\tqueryContext: string | undefined,\n): number {\n\tif (!queryContext || queryContext.trim() === \"\") {\n\t\treturn SKILL_NEUTRAL_SCORE;\n\t}\n\n\tconst queryTokens = tokenizeForRelevance(queryContext);\n\tif (queryTokens.length === 0) {\n\t\treturn SKILL_NEUTRAL_SCORE;\n\t}\n\n\t// Build skill token set: description is base, name tokens get 3x weight\n\tconst { tokens: skillTokenSet, weights: skillWeights } = buildWeightedTokenSet(\n\t\tskill.description,\n\t\tskill.name,\n\t\tNAME_TOKEN_WEIGHT,\n\t);\n\n\tlet score = computeCoverage(queryTokens, skillTokenSet, skillWeights);\n\n\t// Name substring bonus: if skill.name appears in query text (or vice versa)\n\tconst queryLower = queryContext.toLowerCase();\n\tconst nameLower = skill.name.toLowerCase();\n\tif (queryLower.includes(nameLower) || nameLower.includes(queryLower.trim())) {\n\t\tscore = Math.min(1, score + NAME_MATCH_BONUS);\n\t}\n\n\treturn clamp01(score);\n}\n\n// ---------------------------------------------------------------------------\n// Context file relevance\n// ---------------------------------------------------------------------------\n\nconst CONTEXT_NEUTRAL_GLOBAL = 0.9;\nconst CONTEXT_NEUTRAL_LOCAL = 0.6;\nconst PATH_TOKEN_WEIGHT = 2;\nconst CONTENT_SAMPLE_CHARS = 2000;\n\n/**\n * Score context file relevance against a query context.\n *\n * - Baseline (no query evidence) -> isGlobal ? 0.9 : 0.6\n * - Coverage of query tokens against path segments + content sample\n * - Path tokens weighted 2x\n * - Coverage allocates the headroom ABOVE the baseline; it never cuts into it\n *\n * A context file is not retrieved reference material — it is the standing\n * instruction set the agent is required to follow, and it applies whether or\n * not the user's current sentence reuses its vocabulary. Scoring it purely by\n * lexical overlap made the rules rank LOWEST exactly when the agent was doing\n * work: this repository's own AGENTS.md scored 0.9 idle but 0.1 under queries\n * like \"rename this variable\". Coverage therefore raises the score from the\n * baseline toward 1 instead of replacing it, which also reconciles the two\n * readings the function used to have — an absent query and a present-but-\n * non-overlapping query now agree that there is simply no evidence to add.\n *\n * Lexical scoring stays the right metric for skills, which really are\n * topic-scoped; see scoreSkillRelevance.\n */\nexport function scoreContextFileRelevance(\n\tcontextFile: { readonly path: string; readonly content: string; readonly isGlobal: boolean },\n\tqueryContext: string | undefined,\n): number {\n\tif (!queryContext || queryContext.trim() === \"\") {\n\t\treturn contextFile.isGlobal ? CONTEXT_NEUTRAL_GLOBAL : CONTEXT_NEUTRAL_LOCAL;\n\t}\n\n\tconst queryTokens = tokenizeForRelevance(queryContext);\n\tif (queryTokens.length === 0) {\n\t\treturn contextFile.isGlobal ? CONTEXT_NEUTRAL_GLOBAL : CONTEXT_NEUTRAL_LOCAL;\n\t}\n\n\t// Path segments as high-weight text\n\tconst pathText = extractPathSegments(contextFile.path);\n\n\t// Content sample for performance\n\tconst contentSample =\n\t\tcontextFile.content.length > CONTENT_SAMPLE_CHARS\n\t\t\t? contextFile.content.slice(0, CONTENT_SAMPLE_CHARS)\n\t\t\t: contextFile.content;\n\n\t// Build token set: content is base, path tokens get 2x weight\n\tconst { tokens: fileTokenSet, weights: fileWeights } = buildWeightedTokenSet(\n\t\tcontentSample,\n\t\tpathText,\n\t\tPATH_TOKEN_WEIGHT,\n\t);\n\n\tconst coverage = computeCoverage(queryTokens, fileTokenSet, fileWeights);\n\n\t// The baseline is a floor, not a starting guess: query evidence distributes\n\t// the remaining headroom above it. Zero coverage returns the baseline exactly,\n\t// matching the no-query branch above.\n\tconst baseline = contextFile.isGlobal ? CONTEXT_NEUTRAL_GLOBAL : CONTEXT_NEUTRAL_LOCAL;\n\treturn clamp01(baseline + (1 - baseline) * clamp01(coverage));\n}\n\n// ---------------------------------------------------------------------------\n// Helpers\n// ---------------------------------------------------------------------------\n\n/**\n * Extract meaningful segments from a file path for token matching.\n */\nfunction extractPathSegments(path: string): string {\n\treturn path\n\t\t.toLowerCase()\n\t\t.split(/[/\\\\._-]+/)\n\t\t.filter((seg) => seg.length > 0)\n\t\t.join(\" \");\n}\n\nfunction clamp01(value: number): number {\n\treturn Math.max(0, Math.min(1, value));\n}\n"]}