open-multi-agent-kit 0.92.0 → 0.94.0

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 (173) hide show
  1. package/CHANGELOG.md +38 -1
  2. package/README.md +2 -2
  3. package/dist/core/agent-session.d.ts +36 -14
  4. package/dist/core/agent-session.d.ts.map +1 -1
  5. package/dist/core/agent-session.js +155 -357
  6. package/dist/core/agent-session.js.map +1 -1
  7. package/dist/core/compaction/branch-summarization.d.ts +5 -1
  8. package/dist/core/compaction/branch-summarization.d.ts.map +1 -1
  9. package/dist/core/compaction/branch-summarization.js +5 -7
  10. package/dist/core/compaction/branch-summarization.js.map +1 -1
  11. package/dist/core/compaction/compaction.d.ts +12 -3
  12. package/dist/core/compaction/compaction.d.ts.map +1 -1
  13. package/dist/core/compaction/compaction.js +28 -15
  14. package/dist/core/compaction/compaction.js.map +1 -1
  15. package/dist/core/extensions/builtin/command-safety-gate.d.ts +8 -0
  16. package/dist/core/extensions/builtin/command-safety-gate.d.ts.map +1 -1
  17. package/dist/core/extensions/builtin/command-safety-gate.js +23 -0
  18. package/dist/core/extensions/builtin/command-safety-gate.js.map +1 -1
  19. package/dist/core/extensions/runner.d.ts.map +1 -1
  20. package/dist/core/extensions/runner.js +4 -0
  21. package/dist/core/extensions/runner.js.map +1 -1
  22. package/dist/core/extensions/types.d.ts +2 -0
  23. package/dist/core/extensions/types.d.ts.map +1 -1
  24. package/dist/core/extensions/types.js.map +1 -1
  25. package/dist/core/keybindings.d.ts +5 -0
  26. package/dist/core/keybindings.d.ts.map +1 -1
  27. package/dist/core/keybindings.js +5 -0
  28. package/dist/core/keybindings.js.map +1 -1
  29. package/dist/core/model-resolver.d.ts +41 -3
  30. package/dist/core/model-resolver.d.ts.map +1 -1
  31. package/dist/core/model-resolver.js +17 -9
  32. package/dist/core/model-resolver.js.map +1 -1
  33. package/dist/core/package-manager.d.ts.map +1 -1
  34. package/dist/core/package-manager.js +11 -1
  35. package/dist/core/package-manager.js.map +1 -1
  36. package/dist/core/provider-resilience.d.ts.map +1 -1
  37. package/dist/core/provider-resilience.js +3 -1
  38. package/dist/core/provider-resilience.js.map +1 -1
  39. package/dist/core/reasoning-router-v4-weights.d.ts.map +1 -1
  40. package/dist/core/reasoning-router-v4-weights.js +9 -3
  41. package/dist/core/reasoning-router-v4-weights.js.map +1 -1
  42. package/dist/core/reasoning-router-v4.d.ts.map +1 -1
  43. package/dist/core/reasoning-router-v4.js +94 -24
  44. package/dist/core/reasoning-router-v4.js.map +1 -1
  45. package/dist/core/resource-loader.d.ts.map +1 -1
  46. package/dist/core/resource-loader.js +11 -5
  47. package/dist/core/resource-loader.js.map +1 -1
  48. package/dist/core/sandbox/default-policy.d.ts +14 -0
  49. package/dist/core/sandbox/default-policy.d.ts.map +1 -0
  50. package/dist/core/sandbox/default-policy.js +58 -0
  51. package/dist/core/sandbox/default-policy.js.map +1 -0
  52. package/dist/core/sdk.d.ts.map +1 -1
  53. package/dist/core/sdk.js +1 -1
  54. package/dist/core/sdk.js.map +1 -1
  55. package/dist/core/session-bash-runtime.d.ts +62 -0
  56. package/dist/core/session-bash-runtime.d.ts.map +1 -0
  57. package/dist/core/session-bash-runtime.js +167 -0
  58. package/dist/core/session-bash-runtime.js.map +1 -0
  59. package/dist/core/session-bash-service.d.ts +61 -0
  60. package/dist/core/session-bash-service.d.ts.map +1 -0
  61. package/dist/core/session-bash-service.js +155 -0
  62. package/dist/core/session-bash-service.js.map +1 -0
  63. package/dist/core/session-compaction-service.d.ts +48 -0
  64. package/dist/core/session-compaction-service.d.ts.map +1 -0
  65. package/dist/core/session-compaction-service.js +250 -0
  66. package/dist/core/session-compaction-service.js.map +1 -0
  67. package/dist/core/session-file-compaction.d.ts.map +1 -1
  68. package/dist/core/session-file-compaction.js +39 -5
  69. package/dist/core/session-file-compaction.js.map +1 -1
  70. package/dist/core/session-termination.d.ts.map +1 -1
  71. package/dist/core/session-termination.js +1 -1
  72. package/dist/core/session-termination.js.map +1 -1
  73. package/dist/core/settings-manager.d.ts +3 -0
  74. package/dist/core/settings-manager.d.ts.map +1 -1
  75. package/dist/core/settings-manager.js +8 -0
  76. package/dist/core/settings-manager.js.map +1 -1
  77. package/dist/core/skills-catalog-cache.d.ts +25 -0
  78. package/dist/core/skills-catalog-cache.d.ts.map +1 -0
  79. package/dist/core/skills-catalog-cache.js +123 -0
  80. package/dist/core/skills-catalog-cache.js.map +1 -0
  81. package/dist/core/skills.d.ts +6 -0
  82. package/dist/core/skills.d.ts.map +1 -1
  83. package/dist/core/skills.js +25 -4
  84. package/dist/core/skills.js.map +1 -1
  85. package/dist/core/system-prompt.d.ts.map +1 -1
  86. package/dist/core/system-prompt.js +2 -2
  87. package/dist/core/system-prompt.js.map +1 -1
  88. package/dist/core/tools/bash.d.ts +5 -0
  89. package/dist/core/tools/bash.d.ts.map +1 -1
  90. package/dist/core/tools/bash.js +41 -13
  91. package/dist/core/tools/bash.js.map +1 -1
  92. package/dist/core/tools/diagnostics.d.ts +29 -0
  93. package/dist/core/tools/diagnostics.d.ts.map +1 -0
  94. package/dist/core/tools/diagnostics.js +325 -0
  95. package/dist/core/tools/diagnostics.js.map +1 -0
  96. package/dist/core/tools/index.d.ts +2 -1
  97. package/dist/core/tools/index.d.ts.map +1 -1
  98. package/dist/core/tools/index.js +18 -1
  99. package/dist/core/tools/index.js.map +1 -1
  100. package/dist/core/verified-bash-adapter.d.ts +18 -1
  101. package/dist/core/verified-bash-adapter.d.ts.map +1 -1
  102. package/dist/core/verified-bash-adapter.js +38 -1
  103. package/dist/core/verified-bash-adapter.js.map +1 -1
  104. package/dist/core/verified-bash-runtime.d.ts +13 -0
  105. package/dist/core/verified-bash-runtime.d.ts.map +1 -0
  106. package/dist/core/verified-bash-runtime.js +81 -0
  107. package/dist/core/verified-bash-runtime.js.map +1 -0
  108. package/dist/index.d.ts +4 -2
  109. package/dist/index.d.ts.map +1 -1
  110. package/dist/index.js +4 -2
  111. package/dist/index.js.map +1 -1
  112. package/dist/modes/interactive/components/control-panel-box.d.ts +27 -0
  113. package/dist/modes/interactive/components/control-panel-box.d.ts.map +1 -1
  114. package/dist/modes/interactive/components/control-panel-box.js +60 -8
  115. package/dist/modes/interactive/components/control-panel-box.js.map +1 -1
  116. package/dist/modes/interactive/components/control-panel-layout.d.ts.map +1 -1
  117. package/dist/modes/interactive/components/control-panel-layout.js +100 -45
  118. package/dist/modes/interactive/components/control-panel-layout.js.map +1 -1
  119. package/dist/modes/interactive/components/control-panel-runtime-status.d.ts +9 -0
  120. package/dist/modes/interactive/components/control-panel-runtime-status.d.ts.map +1 -1
  121. package/dist/modes/interactive/components/control-panel-runtime-status.js +10 -8
  122. package/dist/modes/interactive/components/control-panel-runtime-status.js.map +1 -1
  123. package/dist/modes/interactive/components/control-panel-sparkles.d.ts +5 -0
  124. package/dist/modes/interactive/components/control-panel-sparkles.d.ts.map +1 -1
  125. package/dist/modes/interactive/components/control-panel-sparkles.js +14 -9
  126. package/dist/modes/interactive/components/control-panel-sparkles.js.map +1 -1
  127. package/dist/modes/interactive/components/footer.d.ts +10 -0
  128. package/dist/modes/interactive/components/footer.d.ts.map +1 -1
  129. package/dist/modes/interactive/components/footer.js +3 -3
  130. package/dist/modes/interactive/components/footer.js.map +1 -1
  131. package/dist/modes/interactive/components/status-sidebar.d.ts +49 -0
  132. package/dist/modes/interactive/components/status-sidebar.d.ts.map +1 -0
  133. package/dist/modes/interactive/components/status-sidebar.js +253 -0
  134. package/dist/modes/interactive/components/status-sidebar.js.map +1 -0
  135. package/dist/modes/interactive/interactive-mode.d.ts +9 -0
  136. package/dist/modes/interactive/interactive-mode.d.ts.map +1 -1
  137. package/dist/modes/interactive/interactive-mode.js +94 -20
  138. package/dist/modes/interactive/interactive-mode.js.map +1 -1
  139. package/dist/modes/interactive/theme/omk-neon-control.json +13 -13
  140. package/dist/modes/rpc/rpc-mode.d.ts.map +1 -1
  141. package/dist/modes/rpc/rpc-mode.js +1 -0
  142. package/dist/modes/rpc/rpc-mode.js.map +1 -1
  143. package/dist/types/evidence.d.ts +1 -1
  144. package/dist/types/evidence.d.ts.map +1 -1
  145. package/dist/types/evidence.js.map +1 -1
  146. package/dist/utils/clipboard.d.ts.map +1 -1
  147. package/dist/utils/clipboard.js +19 -8
  148. package/dist/utils/clipboard.js.map +1 -1
  149. package/dist/utils/frontmatter.d.ts.map +1 -1
  150. package/dist/utils/frontmatter.js +31 -5
  151. package/dist/utils/frontmatter.js.map +1 -1
  152. package/docs/compaction.md +7 -0
  153. package/docs/docs.json +4 -0
  154. package/docs/environment-variables.md +94 -0
  155. package/docs/index.md +1 -0
  156. package/docs/rpc.md +5 -2
  157. package/docs/sdk.md +10 -4
  158. package/docs/settings.md +1 -0
  159. package/examples/extensions/custom-provider-anthropic/package-lock.json +2 -2
  160. package/examples/extensions/custom-provider-anthropic/package.json +1 -1
  161. package/examples/extensions/custom-provider-gitlab-duo/package.json +1 -1
  162. package/examples/extensions/gondolin/package-lock.json +2 -2
  163. package/examples/extensions/gondolin/package.json +1 -1
  164. package/examples/extensions/sandbox/package-lock.json +2 -2
  165. package/examples/extensions/sandbox/package.json +1 -1
  166. package/examples/extensions/subagent/agents.test.ts +86 -0
  167. package/examples/extensions/subagent/agents.ts +18 -4
  168. package/examples/extensions/subagent/capabilities.ts +15 -1
  169. package/examples/extensions/subagent/domain-profiles.ts +272 -0
  170. package/examples/extensions/with-deps/package-lock.json +2 -2
  171. package/examples/extensions/with-deps/package.json +1 -1
  172. package/npm-shrinkwrap.json +15 -15
  173. package/package.json +10 -7
@@ -1 +1 @@
1
- {"version":3,"file":"provider-resilience.d.ts","sourceRoot":"","sources":["../../src/core/provider-resilience.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,MAAM,MAAM,iBAAiB,GAAG,QAAQ,CAAC;IACxC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;CACpB,CAAC,CAAC;AAEH,oFAAoF;AACpF,eAAO,MAAM,sBAAsB,QAAW,CAAC;AAE/C,wEAAwE;AACxE,eAAO,MAAM,kCAAkC,EAAE,SAAS,iBAAiB,EAOjE,CAAC;AAEX,MAAM,MAAM,0BAA0B,GAAG,QAAQ,CAAC;IACjD,wFAAwF;IACxF,QAAQ,CAAC,uBAAuB,CAAC,EAAE,OAAO,CAAC;IAC3C,kFAAkF;IAClF,QAAQ,CAAC,wBAAwB,CAAC,EAAE,OAAO,CAAC;IAC5C,kFAAkF;IAClF,QAAQ,CAAC,kBAAkB,CAAC,EAAE,SAAS,iBAAiB,EAAE,CAAC;CAC3D,CAAC,CAAC;AAEH,eAAO,MAAM,2BAA2B,EAAE,QAAQ,CACjD,IAAI,CAAC,0BAA0B,EAAE,yBAAyB,GAAG,0BAA0B,CAAC,CACxF,GAAG;IACH,QAAQ,CAAC,kBAAkB,EAAE,SAAS,iBAAiB,EAAE,CAAC;CAK1D,CAAC;AAEF,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAMvG;AAED,wBAAgB,0BAA0B,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAK5E;AAED,0FAA0F;AAC1F,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAGzE;AAED;;;GAGG;AACH,wBAAgB,+BAA+B,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAKjF;AAED,wBAAgB,yBAAyB,CACxC,QAAQ,EAAE,0BAA0B,GAAG,SAAS,GAC9C,SAAS,iBAAiB,EAAE,CAI9B;AAED,wBAAgB,yBAAyB,CAAC,QAAQ,EAAE,0BAA0B,GAAG,SAAS,GAAG;IAC5F,QAAQ,CAAC,uBAAuB,EAAE,OAAO,CAAC;IAC1C,QAAQ,CAAC,wBAAwB,EAAE,OAAO,CAAC;IAC3C,QAAQ,CAAC,kBAAkB,EAAE,SAAS,iBAAiB,EAAE,CAAC;CAC1D,CAOA;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,CACpC,UAAU,EAAE,SAAS,iBAAiB,EAAE,EACxC,OAAO,EAAE;IAAE,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,EACzE,SAAS,EAAE,CAAC,SAAS,EAAE,iBAAiB,KAAK,OAAO,GAClD,iBAAiB,GAAG,SAAS,CAQ/B;AAED,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAKlF","sourcesContent":["/**\n * Provider resilience — root-level recovery for sticky safety models and\n * sanitize-and-retry protocol failures (orphan tool_call_id, terminated streams).\n *\n * This is not a jailbreak layer. It keeps sessions alive when a provider\n * false-positives or returns a sticky transcript-shape error.\n */\n\nexport type FailoverCandidate = Readonly<{\n\treadonly provider: string;\n\treadonly id: string;\n}>;\n\n/** Models known to emit high false-positive content/safety stops on coding work. */\nexport const STICKY_SAFETY_MODEL_RE = /fable/i;\n\n/** Default failover chain when a sticky safety model refuses a turn. */\nexport const DEFAULT_SAFETY_FAILOVER_CANDIDATES: readonly FailoverCandidate[] = [\n\t{ provider: \"kimi-coding\", id: \"k3\" },\n\t{ provider: \"grok-oauth-proxy\", id: \"grok-4.5\" },\n\t{ provider: \"deepseek\", id: \"deepseek-v4-pro\" },\n\t{ provider: \"deepseek\", id: \"deepseek-v4-flash\" },\n\t{ provider: \"modelstudio-maas\", id: \"deepseek-v4-pro\" },\n\t{ provider: \"kimi-coding\", id: \"kimi-for-coding\" },\n] as const;\n\nexport type ProviderResilienceSettings = Readonly<{\n\t/** When true (default), refuse selecting sticky safety models (e.g. claude-fable-5). */\n\treadonly blockStickySafetyModels?: boolean;\n\t/** When true (default), auto-switch model on content/safety stop before retry. */\n\treadonly autoFailoverOnSafetyStop?: boolean;\n\t/** Ordered failover targets. Falls back to DEFAULT_SAFETY_FAILOVER_CANDIDATES. */\n\treadonly failoverCandidates?: readonly FailoverCandidate[];\n}>;\n\nexport const DEFAULT_PROVIDER_RESILIENCE: Required<\n\tPick<ProviderResilienceSettings, \"blockStickySafetyModels\" | \"autoFailoverOnSafetyStop\">\n> & {\n\treadonly failoverCandidates: readonly FailoverCandidate[];\n} = {\n\tblockStickySafetyModels: true,\n\tautoFailoverOnSafetyStop: true,\n\tfailoverCandidates: DEFAULT_SAFETY_FAILOVER_CANDIDATES,\n};\n\nexport function isStickySafetyModel(modelId: string | undefined, provider?: string | undefined): boolean {\n\tconst id = (modelId ?? \"\").trim();\n\tif (!id) return false;\n\tif (STICKY_SAFETY_MODEL_RE.test(id)) return true;\n\tconst p = (provider ?? \"\").toLowerCase();\n\treturn p.includes(\"anthropic\") && STICKY_SAFETY_MODEL_RE.test(id);\n}\n\nexport function isContentSafetyStopMessage(text: string | undefined): boolean {\n\tif (!text) return false;\n\treturn /content\\/safety stop|stop_reason\\s*=\\s*(refusal|sensitive)|safety stop|provider\\.refusal|kind=provider_refusal/i.test(\n\t\ttext,\n\t);\n}\n\n/** Orphan tool results / Kimi-K3 protocol shape errors that heal after sanitize+retry. */\nexport function isOrphanToolCallIdError(text: string | undefined): boolean {\n\tif (!text) return false;\n\treturn /tool_call_id\\s+is\\s+not\\s+found|tool_call_id\\s+not\\s+found|unknown\\s+tool_call_id/i.test(text);\n}\n\n/**\n * Errors the agent loop may auto-retry (after optional failover / message sanitize).\n * Kept in one place so agent-session and tests share the same contract.\n */\nexport function isTransientProviderErrorMessage(text: string | undefined): boolean {\n\tif (!text) return false;\n\treturn /overloaded|provider.?returned.?error|rate.?limit|too many requests|429|500|502|503|504|service.?unavailable|server.?error|internal.?error|network.?error|connection.?error|connection.?refused|connection.?lost|websocket.?closed|websocket.?error|other side closed|fetch failed|upstream.?connect|reset before headers|socket hang up|ended without|stream ended before message_stop|http2 request did not get a response|timed? out|timeout|\\bterminated\\b|retry delay|content\\/safety stop|stop_reason\\s*=\\s*(refusal|sensitive)|safety stop|tool_call_id\\s+is\\s+not\\s+found|tool_call_id\\s+not\\s+found|invalid_request_error/i.test(\n\t\ttext,\n\t);\n}\n\nexport function resolveFailoverCandidates(\n\tsettings: ProviderResilienceSettings | undefined,\n): readonly FailoverCandidate[] {\n\tconst custom = settings?.failoverCandidates;\n\tif (custom && custom.length > 0) return custom;\n\treturn DEFAULT_SAFETY_FAILOVER_CANDIDATES;\n}\n\nexport function resolveProviderResilience(settings: ProviderResilienceSettings | undefined): {\n\treadonly blockStickySafetyModels: boolean;\n\treadonly autoFailoverOnSafetyStop: boolean;\n\treadonly failoverCandidates: readonly FailoverCandidate[];\n} {\n\treturn {\n\t\tblockStickySafetyModels: settings?.blockStickySafetyModels ?? DEFAULT_PROVIDER_RESILIENCE.blockStickySafetyModels,\n\t\tautoFailoverOnSafetyStop:\n\t\t\tsettings?.autoFailoverOnSafetyStop ?? DEFAULT_PROVIDER_RESILIENCE.autoFailoverOnSafetyStop,\n\t\tfailoverCandidates: resolveFailoverCandidates(settings),\n\t};\n}\n\n/**\n * Pick first failover candidate that is not the current model and passes `isAllowed`.\n * Pure — caller performs auth checks and setModel.\n */\nexport function pickFailoverCandidate(\n\tcandidates: readonly FailoverCandidate[],\n\tcurrent: { readonly provider?: string; readonly id?: string } | undefined,\n\tisAllowed: (candidate: FailoverCandidate) => boolean,\n): FailoverCandidate | undefined {\n\tfor (const c of candidates) {\n\t\tif (current && c.provider === current.provider && c.id === current.id) continue;\n\t\tif (isStickySafetyModel(c.id, c.provider)) continue;\n\t\tif (!isAllowed(c)) continue;\n\t\treturn c;\n\t}\n\treturn undefined;\n}\n\nexport function stickySafetyBlockMessage(modelId: string, provider: string): string {\n\treturn (\n\t\t`Blocked sticky safety model ${provider}/${modelId} (providerResilience.blockStickySafetyModels). ` +\n\t\t`Use k3 / grok-4.5 / deepseek, or set providerResilience.blockStickySafetyModels=false.`\n\t);\n}\n"]}
1
+ {"version":3,"file":"provider-resilience.d.ts","sourceRoot":"","sources":["../../src/core/provider-resilience.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,MAAM,MAAM,iBAAiB,GAAG,QAAQ,CAAC;IACxC,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;IAC1B,QAAQ,CAAC,EAAE,EAAE,MAAM,CAAC;CACpB,CAAC,CAAC;AAEH,oFAAoF;AACpF,eAAO,MAAM,sBAAsB,QAAW,CAAC;AAE/C,wEAAwE;AACxE,eAAO,MAAM,kCAAkC,EAAE,SAAS,iBAAiB,EAQjE,CAAC;AAEX,MAAM,MAAM,0BAA0B,GAAG,QAAQ,CAAC;IACjD,wFAAwF;IACxF,QAAQ,CAAC,uBAAuB,CAAC,EAAE,OAAO,CAAC;IAC3C,kFAAkF;IAClF,QAAQ,CAAC,wBAAwB,CAAC,EAAE,OAAO,CAAC;IAC5C,kFAAkF;IAClF,QAAQ,CAAC,kBAAkB,CAAC,EAAE,SAAS,iBAAiB,EAAE,CAAC;CAC3D,CAAC,CAAC;AAEH,eAAO,MAAM,2BAA2B,EAAE,QAAQ,CACjD,IAAI,CAAC,0BAA0B,EAAE,yBAAyB,GAAG,0BAA0B,CAAC,CACxF,GAAG;IACH,QAAQ,CAAC,kBAAkB,EAAE,SAAS,iBAAiB,EAAE,CAAC;CAK1D,CAAC;AAEF,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,MAAM,GAAG,SAAS,EAAE,QAAQ,CAAC,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAMvG;AAED,wBAAgB,0BAA0B,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAK5E;AAED,0FAA0F;AAC1F,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAGzE;AAED;;;GAGG;AACH,wBAAgB,+BAA+B,CAAC,IAAI,EAAE,MAAM,GAAG,SAAS,GAAG,OAAO,CAKjF;AAED,wBAAgB,yBAAyB,CACxC,QAAQ,EAAE,0BAA0B,GAAG,SAAS,GAC9C,SAAS,iBAAiB,EAAE,CAI9B;AAED,wBAAgB,yBAAyB,CAAC,QAAQ,EAAE,0BAA0B,GAAG,SAAS,GAAG;IAC5F,QAAQ,CAAC,uBAAuB,EAAE,OAAO,CAAC;IAC1C,QAAQ,CAAC,wBAAwB,EAAE,OAAO,CAAC;IAC3C,QAAQ,CAAC,kBAAkB,EAAE,SAAS,iBAAiB,EAAE,CAAC;CAC1D,CAOA;AAED;;;GAGG;AACH,wBAAgB,qBAAqB,CACpC,UAAU,EAAE,SAAS,iBAAiB,EAAE,EACxC,OAAO,EAAE;IAAE,QAAQ,CAAC,QAAQ,CAAC,EAAE,MAAM,CAAC;IAAC,QAAQ,CAAC,EAAE,CAAC,EAAE,MAAM,CAAA;CAAE,GAAG,SAAS,EACzE,SAAS,EAAE,CAAC,SAAS,EAAE,iBAAiB,KAAK,OAAO,GAClD,iBAAiB,GAAG,SAAS,CAQ/B;AAED,wBAAgB,wBAAwB,CAAC,OAAO,EAAE,MAAM,EAAE,QAAQ,EAAE,MAAM,GAAG,MAAM,CAMlF","sourcesContent":["/**\n * Provider resilience — root-level recovery for sticky safety models and\n * sanitize-and-retry protocol failures (orphan tool_call_id, terminated streams).\n *\n * This is not a jailbreak layer. It keeps sessions alive when a provider\n * false-positives or returns a sticky transcript-shape error.\n */\n\nexport type FailoverCandidate = Readonly<{\n\treadonly provider: string;\n\treadonly id: string;\n}>;\n\n/** Models known to emit high false-positive content/safety stops on coding work. */\nexport const STICKY_SAFETY_MODEL_RE = /fable/i;\n\n/** Default failover chain when a sticky safety model refuses a turn. */\nexport const DEFAULT_SAFETY_FAILOVER_CANDIDATES: readonly FailoverCandidate[] = [\n\t{ provider: \"kimi-coding\", id: \"k3\" },\n\t{ provider: \"modelstudio-maas\", id: \"qwen3.8-max-preview\" },\n\t{ provider: \"grok-oauth-proxy\", id: \"grok-4.5\" },\n\t{ provider: \"deepseek\", id: \"deepseek-v4-pro\" },\n\t{ provider: \"deepseek\", id: \"deepseek-v4-flash\" },\n\t{ provider: \"modelstudio-maas\", id: \"deepseek-v4-pro\" },\n\t{ provider: \"kimi-coding\", id: \"kimi-for-coding\" },\n] as const;\n\nexport type ProviderResilienceSettings = Readonly<{\n\t/** When true (default), refuse selecting sticky safety models (e.g. claude-fable-5). */\n\treadonly blockStickySafetyModels?: boolean;\n\t/** When true (default), auto-switch model on content/safety stop before retry. */\n\treadonly autoFailoverOnSafetyStop?: boolean;\n\t/** Ordered failover targets. Falls back to DEFAULT_SAFETY_FAILOVER_CANDIDATES. */\n\treadonly failoverCandidates?: readonly FailoverCandidate[];\n}>;\n\nexport const DEFAULT_PROVIDER_RESILIENCE: Required<\n\tPick<ProviderResilienceSettings, \"blockStickySafetyModels\" | \"autoFailoverOnSafetyStop\">\n> & {\n\treadonly failoverCandidates: readonly FailoverCandidate[];\n} = {\n\tblockStickySafetyModels: true,\n\tautoFailoverOnSafetyStop: true,\n\tfailoverCandidates: DEFAULT_SAFETY_FAILOVER_CANDIDATES,\n};\n\nexport function isStickySafetyModel(modelId: string | undefined, provider?: string | undefined): boolean {\n\tconst id = (modelId ?? \"\").trim();\n\tif (!id) return false;\n\tif (STICKY_SAFETY_MODEL_RE.test(id)) return true;\n\tconst p = (provider ?? \"\").toLowerCase();\n\treturn p.includes(\"anthropic\") && STICKY_SAFETY_MODEL_RE.test(id);\n}\n\nexport function isContentSafetyStopMessage(text: string | undefined): boolean {\n\tif (!text) return false;\n\treturn /content\\/safety stop|stop_reason\\s*=\\s*(refusal|sensitive)|safety stop|provider\\.refusal|kind=provider_refusal/i.test(\n\t\ttext,\n\t);\n}\n\n/** Orphan tool results / Kimi-K3 protocol shape errors that heal after sanitize+retry. */\nexport function isOrphanToolCallIdError(text: string | undefined): boolean {\n\tif (!text) return false;\n\treturn /tool_call_id\\s+is\\s+not\\s+found|tool_call_id\\s+not\\s+found|unknown\\s+tool_call_id/i.test(text);\n}\n\n/**\n * Errors the agent loop may auto-retry (after optional failover / message sanitize).\n * Kept in one place so agent-session and tests share the same contract.\n */\nexport function isTransientProviderErrorMessage(text: string | undefined): boolean {\n\tif (!text) return false;\n\treturn /overloaded|provider.?returned.?error|rate.?limit|too many requests|429|500|502|503|504|service.?unavailable|server.?error|internal.?error|network.?error|connection.?error|connection.?refused|connection.?lost|websocket.?closed|websocket.?error|other side closed|fetch failed|upstream.?connect|reset before headers|socket hang up|ended without|stream ended before message_stop|http2 request did not get a response|timed? out|timeout|\\bterminated\\b|retry delay|content\\/safety stop|stop_reason\\s*=\\s*(refusal|sensitive)|safety stop|tool_call_id\\s+is\\s+not\\s+found|tool_call_id\\s+not\\s+found|invalid_request_error/i.test(\n\t\ttext,\n\t);\n}\n\nexport function resolveFailoverCandidates(\n\tsettings: ProviderResilienceSettings | undefined,\n): readonly FailoverCandidate[] {\n\tconst custom = settings?.failoverCandidates;\n\tif (custom && custom.length > 0) return custom;\n\treturn DEFAULT_SAFETY_FAILOVER_CANDIDATES;\n}\n\nexport function resolveProviderResilience(settings: ProviderResilienceSettings | undefined): {\n\treadonly blockStickySafetyModels: boolean;\n\treadonly autoFailoverOnSafetyStop: boolean;\n\treadonly failoverCandidates: readonly FailoverCandidate[];\n} {\n\treturn {\n\t\tblockStickySafetyModels: settings?.blockStickySafetyModels ?? DEFAULT_PROVIDER_RESILIENCE.blockStickySafetyModels,\n\t\tautoFailoverOnSafetyStop:\n\t\t\tsettings?.autoFailoverOnSafetyStop ?? DEFAULT_PROVIDER_RESILIENCE.autoFailoverOnSafetyStop,\n\t\tfailoverCandidates: resolveFailoverCandidates(settings),\n\t};\n}\n\n/**\n * Pick first failover candidate that is not the current model and passes `isAllowed`.\n * Pure — caller performs auth checks and setModel.\n */\nexport function pickFailoverCandidate(\n\tcandidates: readonly FailoverCandidate[],\n\tcurrent: { readonly provider?: string; readonly id?: string } | undefined,\n\tisAllowed: (candidate: FailoverCandidate) => boolean,\n): FailoverCandidate | undefined {\n\tfor (const c of candidates) {\n\t\tif (current && c.provider === current.provider && c.id === current.id) continue;\n\t\tif (isStickySafetyModel(c.id, c.provider)) continue;\n\t\tif (!isAllowed(c)) continue;\n\t\treturn c;\n\t}\n\treturn undefined;\n}\n\nexport function stickySafetyBlockMessage(modelId: string, provider: string): string {\n\treturn (\n\t\t`Blocked sticky safety model ${provider}/${modelId} (providerResilience.blockStickySafetyModels). ` +\n\t\t`Use kimi-coding/k3, modelstudio-maas/qwen3.8-max-preview, grok-4.5, or deepseek; ` +\n\t\t`or set providerResilience.blockStickySafetyModels=false.`\n\t);\n}\n"]}
@@ -10,6 +10,7 @@ export const STICKY_SAFETY_MODEL_RE = /fable/i;
10
10
  /** Default failover chain when a sticky safety model refuses a turn. */
11
11
  export const DEFAULT_SAFETY_FAILOVER_CANDIDATES = [
12
12
  { provider: "kimi-coding", id: "k3" },
13
+ { provider: "modelstudio-maas", id: "qwen3.8-max-preview" },
13
14
  { provider: "grok-oauth-proxy", id: "grok-4.5" },
14
15
  { provider: "deepseek", id: "deepseek-v4-pro" },
15
16
  { provider: "deepseek", id: "deepseek-v4-flash" },
@@ -81,6 +82,7 @@ export function pickFailoverCandidate(candidates, current, isAllowed) {
81
82
  }
82
83
  export function stickySafetyBlockMessage(modelId, provider) {
83
84
  return (`Blocked sticky safety model ${provider}/${modelId} (providerResilience.blockStickySafetyModels). ` +
84
- `Use k3 / grok-4.5 / deepseek, or set providerResilience.blockStickySafetyModels=false.`);
85
+ `Use kimi-coding/k3, modelstudio-maas/qwen3.8-max-preview, grok-4.5, or deepseek; ` +
86
+ `or set providerResilience.blockStickySafetyModels=false.`);
85
87
  }
86
88
  //# sourceMappingURL=provider-resilience.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"provider-resilience.js","sourceRoot":"","sources":["../../src/core/provider-resilience.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAOH,oFAAoF;AACpF,MAAM,CAAC,MAAM,sBAAsB,GAAG,QAAQ,CAAC;AAE/C,wEAAwE;AACxE,MAAM,CAAC,MAAM,kCAAkC,GAAiC;IAC/E,EAAE,QAAQ,EAAE,aAAa,EAAE,EAAE,EAAE,IAAI,EAAE;IACrC,EAAE,QAAQ,EAAE,kBAAkB,EAAE,EAAE,EAAE,UAAU,EAAE;IAChD,EAAE,QAAQ,EAAE,UAAU,EAAE,EAAE,EAAE,iBAAiB,EAAE;IAC/C,EAAE,QAAQ,EAAE,UAAU,EAAE,EAAE,EAAE,mBAAmB,EAAE;IACjD,EAAE,QAAQ,EAAE,kBAAkB,EAAE,EAAE,EAAE,iBAAiB,EAAE;IACvD,EAAE,QAAQ,EAAE,aAAa,EAAE,EAAE,EAAE,iBAAiB,EAAE;CACzC,CAAC;AAWX,MAAM,CAAC,MAAM,2BAA2B,GAIpC;IACH,uBAAuB,EAAE,IAAI;IAC7B,wBAAwB,EAAE,IAAI;IAC9B,kBAAkB,EAAE,kCAAkC;CACtD,CAAC;AAEF,MAAM,UAAU,mBAAmB,CAAC,OAA2B,EAAE,QAA6B,EAAW;IACxG,MAAM,EAAE,GAAG,CAAC,OAAO,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;IAClC,IAAI,CAAC,EAAE;QAAE,OAAO,KAAK,CAAC;IACtB,IAAI,sBAAsB,CAAC,IAAI,CAAC,EAAE,CAAC;QAAE,OAAO,IAAI,CAAC;IACjD,MAAM,CAAC,GAAG,CAAC,QAAQ,IAAI,EAAE,CAAC,CAAC,WAAW,EAAE,CAAC;IACzC,OAAO,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,IAAI,sBAAsB,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;AAAA,CAClE;AAED,MAAM,UAAU,0BAA0B,CAAC,IAAwB,EAAW;IAC7E,IAAI,CAAC,IAAI;QAAE,OAAO,KAAK,CAAC;IACxB,OAAO,iHAAiH,CAAC,IAAI,CAC5H,IAAI,CACJ,CAAC;AAAA,CACF;AAED,0FAA0F;AAC1F,MAAM,UAAU,uBAAuB,CAAC,IAAwB,EAAW;IAC1E,IAAI,CAAC,IAAI;QAAE,OAAO,KAAK,CAAC;IACxB,OAAO,oFAAoF,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAAA,CACvG;AAED;;;GAGG;AACH,MAAM,UAAU,+BAA+B,CAAC,IAAwB,EAAW;IAClF,IAAI,CAAC,IAAI;QAAE,OAAO,KAAK,CAAC;IACxB,OAAO,omBAAomB,CAAC,IAAI,CAC/mB,IAAI,CACJ,CAAC;AAAA,CACF;AAED,MAAM,UAAU,yBAAyB,CACxC,QAAgD,EACjB;IAC/B,MAAM,MAAM,GAAG,QAAQ,EAAE,kBAAkB,CAAC;IAC5C,IAAI,MAAM,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,MAAM,CAAC;IAC/C,OAAO,kCAAkC,CAAC;AAAA,CAC1C;AAED,MAAM,UAAU,yBAAyB,CAAC,QAAgD,EAIxF;IACD,OAAO;QACN,uBAAuB,EAAE,QAAQ,EAAE,uBAAuB,IAAI,2BAA2B,CAAC,uBAAuB;QACjH,wBAAwB,EACvB,QAAQ,EAAE,wBAAwB,IAAI,2BAA2B,CAAC,wBAAwB;QAC3F,kBAAkB,EAAE,yBAAyB,CAAC,QAAQ,CAAC;KACvD,CAAC;AAAA,CACF;AAED;;;GAGG;AACH,MAAM,UAAU,qBAAqB,CACpC,UAAwC,EACxC,OAAyE,EACzE,SAAoD,EACpB;IAChC,KAAK,MAAM,CAAC,IAAI,UAAU,EAAE,CAAC;QAC5B,IAAI,OAAO,IAAI,CAAC,CAAC,QAAQ,KAAK,OAAO,CAAC,QAAQ,IAAI,CAAC,CAAC,EAAE,KAAK,OAAO,CAAC,EAAE;YAAE,SAAS;QAChF,IAAI,mBAAmB,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,QAAQ,CAAC;YAAE,SAAS;QACpD,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC;YAAE,SAAS;QAC5B,OAAO,CAAC,CAAC;IACV,CAAC;IACD,OAAO,SAAS,CAAC;AAAA,CACjB;AAED,MAAM,UAAU,wBAAwB,CAAC,OAAe,EAAE,QAAgB,EAAU;IACnF,OAAO,CACN,+BAA+B,QAAQ,IAAI,OAAO,iDAAiD;QACnG,wFAAwF,CACxF,CAAC;AAAA,CACF","sourcesContent":["/**\n * Provider resilience — root-level recovery for sticky safety models and\n * sanitize-and-retry protocol failures (orphan tool_call_id, terminated streams).\n *\n * This is not a jailbreak layer. It keeps sessions alive when a provider\n * false-positives or returns a sticky transcript-shape error.\n */\n\nexport type FailoverCandidate = Readonly<{\n\treadonly provider: string;\n\treadonly id: string;\n}>;\n\n/** Models known to emit high false-positive content/safety stops on coding work. */\nexport const STICKY_SAFETY_MODEL_RE = /fable/i;\n\n/** Default failover chain when a sticky safety model refuses a turn. */\nexport const DEFAULT_SAFETY_FAILOVER_CANDIDATES: readonly FailoverCandidate[] = [\n\t{ provider: \"kimi-coding\", id: \"k3\" },\n\t{ provider: \"grok-oauth-proxy\", id: \"grok-4.5\" },\n\t{ provider: \"deepseek\", id: \"deepseek-v4-pro\" },\n\t{ provider: \"deepseek\", id: \"deepseek-v4-flash\" },\n\t{ provider: \"modelstudio-maas\", id: \"deepseek-v4-pro\" },\n\t{ provider: \"kimi-coding\", id: \"kimi-for-coding\" },\n] as const;\n\nexport type ProviderResilienceSettings = Readonly<{\n\t/** When true (default), refuse selecting sticky safety models (e.g. claude-fable-5). */\n\treadonly blockStickySafetyModels?: boolean;\n\t/** When true (default), auto-switch model on content/safety stop before retry. */\n\treadonly autoFailoverOnSafetyStop?: boolean;\n\t/** Ordered failover targets. Falls back to DEFAULT_SAFETY_FAILOVER_CANDIDATES. */\n\treadonly failoverCandidates?: readonly FailoverCandidate[];\n}>;\n\nexport const DEFAULT_PROVIDER_RESILIENCE: Required<\n\tPick<ProviderResilienceSettings, \"blockStickySafetyModels\" | \"autoFailoverOnSafetyStop\">\n> & {\n\treadonly failoverCandidates: readonly FailoverCandidate[];\n} = {\n\tblockStickySafetyModels: true,\n\tautoFailoverOnSafetyStop: true,\n\tfailoverCandidates: DEFAULT_SAFETY_FAILOVER_CANDIDATES,\n};\n\nexport function isStickySafetyModel(modelId: string | undefined, provider?: string | undefined): boolean {\n\tconst id = (modelId ?? \"\").trim();\n\tif (!id) return false;\n\tif (STICKY_SAFETY_MODEL_RE.test(id)) return true;\n\tconst p = (provider ?? \"\").toLowerCase();\n\treturn p.includes(\"anthropic\") && STICKY_SAFETY_MODEL_RE.test(id);\n}\n\nexport function isContentSafetyStopMessage(text: string | undefined): boolean {\n\tif (!text) return false;\n\treturn /content\\/safety stop|stop_reason\\s*=\\s*(refusal|sensitive)|safety stop|provider\\.refusal|kind=provider_refusal/i.test(\n\t\ttext,\n\t);\n}\n\n/** Orphan tool results / Kimi-K3 protocol shape errors that heal after sanitize+retry. */\nexport function isOrphanToolCallIdError(text: string | undefined): boolean {\n\tif (!text) return false;\n\treturn /tool_call_id\\s+is\\s+not\\s+found|tool_call_id\\s+not\\s+found|unknown\\s+tool_call_id/i.test(text);\n}\n\n/**\n * Errors the agent loop may auto-retry (after optional failover / message sanitize).\n * Kept in one place so agent-session and tests share the same contract.\n */\nexport function isTransientProviderErrorMessage(text: string | undefined): boolean {\n\tif (!text) return false;\n\treturn /overloaded|provider.?returned.?error|rate.?limit|too many requests|429|500|502|503|504|service.?unavailable|server.?error|internal.?error|network.?error|connection.?error|connection.?refused|connection.?lost|websocket.?closed|websocket.?error|other side closed|fetch failed|upstream.?connect|reset before headers|socket hang up|ended without|stream ended before message_stop|http2 request did not get a response|timed? out|timeout|\\bterminated\\b|retry delay|content\\/safety stop|stop_reason\\s*=\\s*(refusal|sensitive)|safety stop|tool_call_id\\s+is\\s+not\\s+found|tool_call_id\\s+not\\s+found|invalid_request_error/i.test(\n\t\ttext,\n\t);\n}\n\nexport function resolveFailoverCandidates(\n\tsettings: ProviderResilienceSettings | undefined,\n): readonly FailoverCandidate[] {\n\tconst custom = settings?.failoverCandidates;\n\tif (custom && custom.length > 0) return custom;\n\treturn DEFAULT_SAFETY_FAILOVER_CANDIDATES;\n}\n\nexport function resolveProviderResilience(settings: ProviderResilienceSettings | undefined): {\n\treadonly blockStickySafetyModels: boolean;\n\treadonly autoFailoverOnSafetyStop: boolean;\n\treadonly failoverCandidates: readonly FailoverCandidate[];\n} {\n\treturn {\n\t\tblockStickySafetyModels: settings?.blockStickySafetyModels ?? DEFAULT_PROVIDER_RESILIENCE.blockStickySafetyModels,\n\t\tautoFailoverOnSafetyStop:\n\t\t\tsettings?.autoFailoverOnSafetyStop ?? DEFAULT_PROVIDER_RESILIENCE.autoFailoverOnSafetyStop,\n\t\tfailoverCandidates: resolveFailoverCandidates(settings),\n\t};\n}\n\n/**\n * Pick first failover candidate that is not the current model and passes `isAllowed`.\n * Pure — caller performs auth checks and setModel.\n */\nexport function pickFailoverCandidate(\n\tcandidates: readonly FailoverCandidate[],\n\tcurrent: { readonly provider?: string; readonly id?: string } | undefined,\n\tisAllowed: (candidate: FailoverCandidate) => boolean,\n): FailoverCandidate | undefined {\n\tfor (const c of candidates) {\n\t\tif (current && c.provider === current.provider && c.id === current.id) continue;\n\t\tif (isStickySafetyModel(c.id, c.provider)) continue;\n\t\tif (!isAllowed(c)) continue;\n\t\treturn c;\n\t}\n\treturn undefined;\n}\n\nexport function stickySafetyBlockMessage(modelId: string, provider: string): string {\n\treturn (\n\t\t`Blocked sticky safety model ${provider}/${modelId} (providerResilience.blockStickySafetyModels). ` +\n\t\t`Use k3 / grok-4.5 / deepseek, or set providerResilience.blockStickySafetyModels=false.`\n\t);\n}\n"]}
1
+ {"version":3,"file":"provider-resilience.js","sourceRoot":"","sources":["../../src/core/provider-resilience.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAOH,oFAAoF;AACpF,MAAM,CAAC,MAAM,sBAAsB,GAAG,QAAQ,CAAC;AAE/C,wEAAwE;AACxE,MAAM,CAAC,MAAM,kCAAkC,GAAiC;IAC/E,EAAE,QAAQ,EAAE,aAAa,EAAE,EAAE,EAAE,IAAI,EAAE;IACrC,EAAE,QAAQ,EAAE,kBAAkB,EAAE,EAAE,EAAE,qBAAqB,EAAE;IAC3D,EAAE,QAAQ,EAAE,kBAAkB,EAAE,EAAE,EAAE,UAAU,EAAE;IAChD,EAAE,QAAQ,EAAE,UAAU,EAAE,EAAE,EAAE,iBAAiB,EAAE;IAC/C,EAAE,QAAQ,EAAE,UAAU,EAAE,EAAE,EAAE,mBAAmB,EAAE;IACjD,EAAE,QAAQ,EAAE,kBAAkB,EAAE,EAAE,EAAE,iBAAiB,EAAE;IACvD,EAAE,QAAQ,EAAE,aAAa,EAAE,EAAE,EAAE,iBAAiB,EAAE;CACzC,CAAC;AAWX,MAAM,CAAC,MAAM,2BAA2B,GAIpC;IACH,uBAAuB,EAAE,IAAI;IAC7B,wBAAwB,EAAE,IAAI;IAC9B,kBAAkB,EAAE,kCAAkC;CACtD,CAAC;AAEF,MAAM,UAAU,mBAAmB,CAAC,OAA2B,EAAE,QAA6B,EAAW;IACxG,MAAM,EAAE,GAAG,CAAC,OAAO,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC;IAClC,IAAI,CAAC,EAAE;QAAE,OAAO,KAAK,CAAC;IACtB,IAAI,sBAAsB,CAAC,IAAI,CAAC,EAAE,CAAC;QAAE,OAAO,IAAI,CAAC;IACjD,MAAM,CAAC,GAAG,CAAC,QAAQ,IAAI,EAAE,CAAC,CAAC,WAAW,EAAE,CAAC;IACzC,OAAO,CAAC,CAAC,QAAQ,CAAC,WAAW,CAAC,IAAI,sBAAsB,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC;AAAA,CAClE;AAED,MAAM,UAAU,0BAA0B,CAAC,IAAwB,EAAW;IAC7E,IAAI,CAAC,IAAI;QAAE,OAAO,KAAK,CAAC;IACxB,OAAO,iHAAiH,CAAC,IAAI,CAC5H,IAAI,CACJ,CAAC;AAAA,CACF;AAED,0FAA0F;AAC1F,MAAM,UAAU,uBAAuB,CAAC,IAAwB,EAAW;IAC1E,IAAI,CAAC,IAAI;QAAE,OAAO,KAAK,CAAC;IACxB,OAAO,oFAAoF,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;AAAA,CACvG;AAED;;;GAGG;AACH,MAAM,UAAU,+BAA+B,CAAC,IAAwB,EAAW;IAClF,IAAI,CAAC,IAAI;QAAE,OAAO,KAAK,CAAC;IACxB,OAAO,omBAAomB,CAAC,IAAI,CAC/mB,IAAI,CACJ,CAAC;AAAA,CACF;AAED,MAAM,UAAU,yBAAyB,CACxC,QAAgD,EACjB;IAC/B,MAAM,MAAM,GAAG,QAAQ,EAAE,kBAAkB,CAAC;IAC5C,IAAI,MAAM,IAAI,MAAM,CAAC,MAAM,GAAG,CAAC;QAAE,OAAO,MAAM,CAAC;IAC/C,OAAO,kCAAkC,CAAC;AAAA,CAC1C;AAED,MAAM,UAAU,yBAAyB,CAAC,QAAgD,EAIxF;IACD,OAAO;QACN,uBAAuB,EAAE,QAAQ,EAAE,uBAAuB,IAAI,2BAA2B,CAAC,uBAAuB;QACjH,wBAAwB,EACvB,QAAQ,EAAE,wBAAwB,IAAI,2BAA2B,CAAC,wBAAwB;QAC3F,kBAAkB,EAAE,yBAAyB,CAAC,QAAQ,CAAC;KACvD,CAAC;AAAA,CACF;AAED;;;GAGG;AACH,MAAM,UAAU,qBAAqB,CACpC,UAAwC,EACxC,OAAyE,EACzE,SAAoD,EACpB;IAChC,KAAK,MAAM,CAAC,IAAI,UAAU,EAAE,CAAC;QAC5B,IAAI,OAAO,IAAI,CAAC,CAAC,QAAQ,KAAK,OAAO,CAAC,QAAQ,IAAI,CAAC,CAAC,EAAE,KAAK,OAAO,CAAC,EAAE;YAAE,SAAS;QAChF,IAAI,mBAAmB,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,QAAQ,CAAC;YAAE,SAAS;QACpD,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC;YAAE,SAAS;QAC5B,OAAO,CAAC,CAAC;IACV,CAAC;IACD,OAAO,SAAS,CAAC;AAAA,CACjB;AAED,MAAM,UAAU,wBAAwB,CAAC,OAAe,EAAE,QAAgB,EAAU;IACnF,OAAO,CACN,+BAA+B,QAAQ,IAAI,OAAO,iDAAiD;QACnG,mFAAmF;QACnF,0DAA0D,CAC1D,CAAC;AAAA,CACF","sourcesContent":["/**\n * Provider resilience — root-level recovery for sticky safety models and\n * sanitize-and-retry protocol failures (orphan tool_call_id, terminated streams).\n *\n * This is not a jailbreak layer. It keeps sessions alive when a provider\n * false-positives or returns a sticky transcript-shape error.\n */\n\nexport type FailoverCandidate = Readonly<{\n\treadonly provider: string;\n\treadonly id: string;\n}>;\n\n/** Models known to emit high false-positive content/safety stops on coding work. */\nexport const STICKY_SAFETY_MODEL_RE = /fable/i;\n\n/** Default failover chain when a sticky safety model refuses a turn. */\nexport const DEFAULT_SAFETY_FAILOVER_CANDIDATES: readonly FailoverCandidate[] = [\n\t{ provider: \"kimi-coding\", id: \"k3\" },\n\t{ provider: \"modelstudio-maas\", id: \"qwen3.8-max-preview\" },\n\t{ provider: \"grok-oauth-proxy\", id: \"grok-4.5\" },\n\t{ provider: \"deepseek\", id: \"deepseek-v4-pro\" },\n\t{ provider: \"deepseek\", id: \"deepseek-v4-flash\" },\n\t{ provider: \"modelstudio-maas\", id: \"deepseek-v4-pro\" },\n\t{ provider: \"kimi-coding\", id: \"kimi-for-coding\" },\n] as const;\n\nexport type ProviderResilienceSettings = Readonly<{\n\t/** When true (default), refuse selecting sticky safety models (e.g. claude-fable-5). */\n\treadonly blockStickySafetyModels?: boolean;\n\t/** When true (default), auto-switch model on content/safety stop before retry. */\n\treadonly autoFailoverOnSafetyStop?: boolean;\n\t/** Ordered failover targets. Falls back to DEFAULT_SAFETY_FAILOVER_CANDIDATES. */\n\treadonly failoverCandidates?: readonly FailoverCandidate[];\n}>;\n\nexport const DEFAULT_PROVIDER_RESILIENCE: Required<\n\tPick<ProviderResilienceSettings, \"blockStickySafetyModels\" | \"autoFailoverOnSafetyStop\">\n> & {\n\treadonly failoverCandidates: readonly FailoverCandidate[];\n} = {\n\tblockStickySafetyModels: true,\n\tautoFailoverOnSafetyStop: true,\n\tfailoverCandidates: DEFAULT_SAFETY_FAILOVER_CANDIDATES,\n};\n\nexport function isStickySafetyModel(modelId: string | undefined, provider?: string | undefined): boolean {\n\tconst id = (modelId ?? \"\").trim();\n\tif (!id) return false;\n\tif (STICKY_SAFETY_MODEL_RE.test(id)) return true;\n\tconst p = (provider ?? \"\").toLowerCase();\n\treturn p.includes(\"anthropic\") && STICKY_SAFETY_MODEL_RE.test(id);\n}\n\nexport function isContentSafetyStopMessage(text: string | undefined): boolean {\n\tif (!text) return false;\n\treturn /content\\/safety stop|stop_reason\\s*=\\s*(refusal|sensitive)|safety stop|provider\\.refusal|kind=provider_refusal/i.test(\n\t\ttext,\n\t);\n}\n\n/** Orphan tool results / Kimi-K3 protocol shape errors that heal after sanitize+retry. */\nexport function isOrphanToolCallIdError(text: string | undefined): boolean {\n\tif (!text) return false;\n\treturn /tool_call_id\\s+is\\s+not\\s+found|tool_call_id\\s+not\\s+found|unknown\\s+tool_call_id/i.test(text);\n}\n\n/**\n * Errors the agent loop may auto-retry (after optional failover / message sanitize).\n * Kept in one place so agent-session and tests share the same contract.\n */\nexport function isTransientProviderErrorMessage(text: string | undefined): boolean {\n\tif (!text) return false;\n\treturn /overloaded|provider.?returned.?error|rate.?limit|too many requests|429|500|502|503|504|service.?unavailable|server.?error|internal.?error|network.?error|connection.?error|connection.?refused|connection.?lost|websocket.?closed|websocket.?error|other side closed|fetch failed|upstream.?connect|reset before headers|socket hang up|ended without|stream ended before message_stop|http2 request did not get a response|timed? out|timeout|\\bterminated\\b|retry delay|content\\/safety stop|stop_reason\\s*=\\s*(refusal|sensitive)|safety stop|tool_call_id\\s+is\\s+not\\s+found|tool_call_id\\s+not\\s+found|invalid_request_error/i.test(\n\t\ttext,\n\t);\n}\n\nexport function resolveFailoverCandidates(\n\tsettings: ProviderResilienceSettings | undefined,\n): readonly FailoverCandidate[] {\n\tconst custom = settings?.failoverCandidates;\n\tif (custom && custom.length > 0) return custom;\n\treturn DEFAULT_SAFETY_FAILOVER_CANDIDATES;\n}\n\nexport function resolveProviderResilience(settings: ProviderResilienceSettings | undefined): {\n\treadonly blockStickySafetyModels: boolean;\n\treadonly autoFailoverOnSafetyStop: boolean;\n\treadonly failoverCandidates: readonly FailoverCandidate[];\n} {\n\treturn {\n\t\tblockStickySafetyModels: settings?.blockStickySafetyModels ?? DEFAULT_PROVIDER_RESILIENCE.blockStickySafetyModels,\n\t\tautoFailoverOnSafetyStop:\n\t\t\tsettings?.autoFailoverOnSafetyStop ?? DEFAULT_PROVIDER_RESILIENCE.autoFailoverOnSafetyStop,\n\t\tfailoverCandidates: resolveFailoverCandidates(settings),\n\t};\n}\n\n/**\n * Pick first failover candidate that is not the current model and passes `isAllowed`.\n * Pure — caller performs auth checks and setModel.\n */\nexport function pickFailoverCandidate(\n\tcandidates: readonly FailoverCandidate[],\n\tcurrent: { readonly provider?: string; readonly id?: string } | undefined,\n\tisAllowed: (candidate: FailoverCandidate) => boolean,\n): FailoverCandidate | undefined {\n\tfor (const c of candidates) {\n\t\tif (current && c.provider === current.provider && c.id === current.id) continue;\n\t\tif (isStickySafetyModel(c.id, c.provider)) continue;\n\t\tif (!isAllowed(c)) continue;\n\t\treturn c;\n\t}\n\treturn undefined;\n}\n\nexport function stickySafetyBlockMessage(modelId: string, provider: string): string {\n\treturn (\n\t\t`Blocked sticky safety model ${provider}/${modelId} (providerResilience.blockStickySafetyModels). ` +\n\t\t`Use kimi-coding/k3, modelstudio-maas/qwen3.8-max-preview, grok-4.5, or deepseek; ` +\n\t\t`or set providerResilience.blockStickySafetyModels=false.`\n\t);\n}\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"reasoning-router-v4-weights.d.ts","sourceRoot":"","sources":["../../src/core/reasoning-router-v4-weights.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,qCAAqC;AACrC,MAAM,MAAM,WAAW,GAAG,SAAS,GAAG,aAAa,GAAG,UAAU,GAAG,OAAO,GAAG,UAAU,GAAG,QAAQ,GAAG,MAAM,CAAC;AAE5G,yEAAyE;AACzE,MAAM,MAAM,mBAAmB,GAAG,SAAS,GAAG,UAAU,GAAG,UAAU,GAAG,OAAO,GAAG,UAAU,GAAG,QAAQ,CAAC;AAExG,yIAAyI;AACzI,eAAO,MAAM,eAAe,EAAE,SAAS,WAAW,EAQjD,CAAC;AAEF;;;;GAIG;AACH,MAAM,WAAW,eAAe;IAC/B,oEAAoE;IACpE,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,+DAA+D;IAC/D,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,6HAA6H;IAC7H,QAAQ,CAAC,kBAAkB,EAAE,MAAM,CAAC;IACpC,yEAAyE;IACzE,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,4EAA4E;IAC5E,QAAQ,CAAC,kBAAkB,EAAE,MAAM,CAAC;IACpC,sEAAsE;IACtE,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,kEAAkE;IAClE,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,4GAA4G;IAC5G,QAAQ,CAAC,kBAAkB,EAAE,MAAM,CAAC;IACpC,uFAAuF;IACvF,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,wEAAwE;IACxE,QAAQ,CAAC,oBAAoB,EAAE,MAAM,CAAC;IACtC,6GAA6G;IAC7G,QAAQ,CAAC,sBAAsB,EAAE,MAAM,CAAC;IACxC,kHAAkH;IAClH,QAAQ,CAAC,sBAAsB,EAAE,MAAM,CAAC;IACxC,4EAA4E;IAC5E,QAAQ,CAAC,aAAa,EAAE,QAAQ,CAAC,MAAM,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC,CAAC;IAC9D,iGAAiG;IACjG,QAAQ,CAAC,uBAAuB,EAAE,MAAM,CAAC;IACzC,wGAAwG;IACxG,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,wEAAwE;IACxE,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,yHAAyH;IACzH,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,+IAA+I;IAC/I,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,yIAAyI;IACzI,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,sGAAsG;IACtG,QAAQ,CAAC,mBAAmB,EAAE,MAAM,CAAC;IACrC,4GAA4G;IAC5G,QAAQ,CAAC,sBAAsB,EAAE,MAAM,CAAC;IACxC,gDAAgD;IAChD,QAAQ,CAAC,uBAAuB,EAAE,MAAM,CAAC;CACzC;AAED;;;;;;GAMG;AACH,eAAO,MAAM,kBAAkB,EAAE,eA+BhC,CAAC","sourcesContent":["/**\n * Reasoning-router v4 — frozen types and calibratable weight schema.\n *\n * Pure, side-effect-free, erasable TypeScript. No runtime imports. This file\n * owns the closed task/lane unions, named scorer weights, and calibrated default\n * preset used by `/think auto`.\n *\n * `keywordFamily` is the class -> weight table for whole-prompt keyword-family\n * patterns (see reasoning-router-v4.ts). `trivial` carries weight 0 because it\n * is reached only via the zero-score fallback cascade.\n *\n * `secondClauseIntent` applies a bounded bonus when a second, distinct leading\n * verb is found after a short-range conjunction split. `negationWindowChars`\n * bounds negation gating so a nearby \"don't/skip/without/no need\" cue can\n * suppress a whole-prompt signal without crossing a clause boundary.\n *\n * `lowConfidenceThreshold` / `highConfidenceThreshold` bound the confidence\n * bands (`ClassifierVerdictV4.confidenceBand`) computed from score separation\n * plus absolute evidence strength in reasoning-router-v4.ts. They are\n * metadata-only: they never change `taskClass`, only how\n * `resolveThinkingLevelV4WithUncertainty` treats an already-decided class.\n */\n\n/** Closed set of v4 task classes. */\nexport type TaskClassV4 = \"trivial\" | \"simple-edit\" | \"code-gen\" | \"debug\" | \"refactor\" | \"review\" | \"plan\";\n\n/** Subagent lane types recognized by the v4 classifier and resolvers. */\nexport type ReasoningLaneTypeV4 = \"planner\" | \"security\" | \"explorer\" | \"coder\" | \"reviewer\" | \"tester\";\n\n/** Fixed iteration order over TaskClassV4 (matches the type-declaration order; also v4's argmax tie-break precedence, highest first). */\nexport const TASK_CLASSES_V4: readonly TaskClassV4[] = [\n\t\"debug\",\n\t\"refactor\",\n\t\"review\",\n\t\"plan\",\n\t\"simple-edit\",\n\t\"code-gen\",\n\t\"trivial\",\n];\n\n/**\n * Named weight schema for the v4 scorer (reasoning-router-v4.ts). Every field\n * is a plain, deterministic integer/ratio; there is no clock, randomness, or\n * I/O anywhere in this file or its consumer.\n */\nexport interface RouterWeightsV4 {\n\t/** Bump to \"code-gen\" when a code fence or diff hunk is present. */\n\treadonly codeFenceOrDiff: number;\n\t/** Bump to the primary leading-clause intent class, if any. */\n\treadonly leadingIntent: number;\n\t/** Bump to a distinct second-clause intent class in a detected compound prompt (v4-new; default half of `leadingIntent`). */\n\treadonly secondClauseIntent: number;\n\t/** Bump to \"simple-edit\" when the composite local-edit feature fires. */\n\treadonly localEdit: number;\n\t/** Bump to \"debug\" when the composite diagnostic-evidence feature fires. */\n\treadonly diagnosticEvidence: number;\n\t/** Bump to \"review\" when the composite review-scope feature fires. */\n\treadonly reviewScope: number;\n\t/** Bump to \"plan\" when the composite plan-brief feature fires. */\n\treadonly planBrief: number;\n\t/** Bump to \"plan\" for release-bound operational runbooks (commit/push/tag/publish/changelog/CI bundles). */\n\treadonly operationalRunbook: number;\n\t/** Bump to \"refactor\" when the refactor-cue pattern matches anywhere in the prompt. */\n\treadonly refactorCue: number;\n\t/** Bump to \"code-gen\" when an implementation-object noun is present. */\n\treadonly implementationObject: number;\n\t/** Bump to \"code-gen\" when a first-person request names a code artifact (for example, \"I need a script\"). */\n\treadonly codeGenArtifactRequest: number;\n\t/** Bump to \"review\" when a leading review request names an evaluative object such as holes/regressions/issues. */\n\treadonly evaluativeReviewObject: number;\n\t/** Per-class whole-prompt keyword-family bump; trivial has no family, 0. */\n\treadonly keywordFamily: Readonly<Record<TaskClassV4, number>>;\n\t/** Bump to a class when normalized seed->cluster anchors match (medium generalized evidence). */\n\treadonly normalizedIntentCluster: number;\n\t/** Bump to a class when the bounded intent-skeleton extractor matches (medium generalized evidence). */\n\treadonly intentSkeleton: number;\n\t/** Bump to \"code-gen\" for a bare \"add\" keyword, gated by !localEdit. */\n\treadonly addKeyword: number;\n\t/** Bump to the class matching `history[0]`, if supplied (v4-new; 0 under DEFAULT_WEIGHTS_V4, inert until calibrated). */\n\treadonly multiTurnPrior: number;\n\t/** Linear per-bucket bump applied to debug/review/plan under context pressure (v4-new; 0 under DEFAULT_WEIGHTS_V4, inert until calibrated). */\n\treadonly pressureBucket: number;\n\t/** Bump to the class matching an externally supplied judge vote, if any (v4-new; 0 under DEFAULT_WEIGHTS_V4, inert until calibrated). */\n\treadonly judgeVote: number;\n\t/** Bounded look-back window (characters) for negation-cue gating, never crossing a .,;!? boundary. */\n\treadonly negationWindowChars: number;\n\t/** confidence <= this value bands as \"low\" (confidence combines score separation and evidence strength). */\n\treadonly lowConfidenceThreshold: number;\n\t/** confidence >= this value bands as \"high\". */\n\treadonly highConfidenceThreshold: number;\n}\n\n/**\n * Calibrated default preset. The named weights below are the production v4\n * scorer configuration covered by\n * test/suite/regressions/013-reasoning-router-v4-accuracy.test.ts. Extension\n * fields (`multiTurnPrior`, `pressureBucket`, `judgeVote`) are wired but inert\n * until a future calibration changes them.\n */\nexport const DEFAULT_WEIGHTS_V4: RouterWeightsV4 = {\n\tcodeFenceOrDiff: 4,\n\tleadingIntent: 8,\n\tsecondClauseIntent: 4,\n\tlocalEdit: 7,\n\tdiagnosticEvidence: 7,\n\treviewScope: 3,\n\tplanBrief: 8,\n\toperationalRunbook: 8,\n\trefactorCue: 6,\n\timplementationObject: 3,\n\tcodeGenArtifactRequest: 5,\n\tevaluativeReviewObject: 3,\n\tkeywordFamily: {\n\t\ttrivial: 0,\n\t\t\"simple-edit\": 4,\n\t\t\"code-gen\": 4,\n\t\tdebug: 4,\n\t\trefactor: 4,\n\t\treview: 4,\n\t\tplan: 4,\n\t},\n\tnormalizedIntentCluster: 4,\n\tintentSkeleton: 5,\n\taddKeyword: 1,\n\tmultiTurnPrior: 0,\n\tpressureBucket: 0,\n\tjudgeVote: 0,\n\tnegationWindowChars: 24,\n\tlowConfidenceThreshold: 0.35,\n\thighConfidenceThreshold: 0.7,\n};\n"]}
1
+ {"version":3,"file":"reasoning-router-v4-weights.d.ts","sourceRoot":"","sources":["../../src/core/reasoning-router-v4-weights.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAEH,qCAAqC;AACrC,MAAM,MAAM,WAAW,GAAG,SAAS,GAAG,aAAa,GAAG,UAAU,GAAG,OAAO,GAAG,UAAU,GAAG,QAAQ,GAAG,MAAM,CAAC;AAE5G,yEAAyE;AACzE,MAAM,MAAM,mBAAmB,GAAG,SAAS,GAAG,UAAU,GAAG,UAAU,GAAG,OAAO,GAAG,UAAU,GAAG,QAAQ,CAAC;AAExG,yIAAyI;AACzI,eAAO,MAAM,eAAe,EAAE,SAAS,WAAW,EAQjD,CAAC;AAEF;;;;GAIG;AACH,MAAM,WAAW,eAAe;IAC/B,oEAAoE;IACpE,QAAQ,CAAC,eAAe,EAAE,MAAM,CAAC;IACjC,+DAA+D;IAC/D,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,6HAA6H;IAC7H,QAAQ,CAAC,kBAAkB,EAAE,MAAM,CAAC;IACpC,yEAAyE;IACzE,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,4EAA4E;IAC5E,QAAQ,CAAC,kBAAkB,EAAE,MAAM,CAAC;IACpC,sEAAsE;IACtE,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,kEAAkE;IAClE,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,4GAA4G;IAC5G,QAAQ,CAAC,kBAAkB,EAAE,MAAM,CAAC;IACpC,uFAAuF;IACvF,QAAQ,CAAC,WAAW,EAAE,MAAM,CAAC;IAC7B,wEAAwE;IACxE,QAAQ,CAAC,oBAAoB,EAAE,MAAM,CAAC;IACtC,6GAA6G;IAC7G,QAAQ,CAAC,sBAAsB,EAAE,MAAM,CAAC;IACxC,kHAAkH;IAClH,QAAQ,CAAC,sBAAsB,EAAE,MAAM,CAAC;IACxC,4EAA4E;IAC5E,QAAQ,CAAC,aAAa,EAAE,QAAQ,CAAC,MAAM,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC,CAAC;IAC9D,iGAAiG;IACjG,QAAQ,CAAC,uBAAuB,EAAE,MAAM,CAAC;IACzC,wGAAwG;IACxG,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,wEAAwE;IACxE,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,yHAAyH;IACzH,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,+IAA+I;IAC/I,QAAQ,CAAC,cAAc,EAAE,MAAM,CAAC;IAChC,yIAAyI;IACzI,QAAQ,CAAC,SAAS,EAAE,MAAM,CAAC;IAC3B,sGAAsG;IACtG,QAAQ,CAAC,mBAAmB,EAAE,MAAM,CAAC;IACrC,4GAA4G;IAC5G,QAAQ,CAAC,sBAAsB,EAAE,MAAM,CAAC;IACxC,gDAAgD;IAChD,QAAQ,CAAC,uBAAuB,EAAE,MAAM,CAAC;CACzC;AAED;;;;;;GAMG;AACH,eAAO,MAAM,kBAAkB,EAAE,eAqChC,CAAC","sourcesContent":["/**\n * Reasoning-router v4 — frozen types and calibratable weight schema.\n *\n * Pure, side-effect-free, erasable TypeScript. No runtime imports. This file\n * owns the closed task/lane unions, named scorer weights, and calibrated default\n * preset used by `/think auto`.\n *\n * `keywordFamily` is the class -> weight table for whole-prompt keyword-family\n * patterns (see reasoning-router-v4.ts). `trivial` carries weight 0 because it\n * is reached only via the zero-score fallback cascade.\n *\n * `secondClauseIntent` applies a bounded bonus when a second, distinct leading\n * verb is found after a short-range conjunction split. `negationWindowChars`\n * bounds negation gating so a nearby \"don't/skip/without/no need\" cue can\n * suppress a whole-prompt signal without crossing a clause boundary.\n *\n * `lowConfidenceThreshold` / `highConfidenceThreshold` bound the confidence\n * bands (`ClassifierVerdictV4.confidenceBand`) computed from score separation\n * plus absolute evidence strength in reasoning-router-v4.ts. They are\n * metadata-only: they never change `taskClass`, only how\n * `resolveThinkingLevelV4WithUncertainty` treats an already-decided class.\n */\n\n/** Closed set of v4 task classes. */\nexport type TaskClassV4 = \"trivial\" | \"simple-edit\" | \"code-gen\" | \"debug\" | \"refactor\" | \"review\" | \"plan\";\n\n/** Subagent lane types recognized by the v4 classifier and resolvers. */\nexport type ReasoningLaneTypeV4 = \"planner\" | \"security\" | \"explorer\" | \"coder\" | \"reviewer\" | \"tester\";\n\n/** Fixed iteration order over TaskClassV4 (matches the type-declaration order; also v4's argmax tie-break precedence, highest first). */\nexport const TASK_CLASSES_V4: readonly TaskClassV4[] = [\n\t\"debug\",\n\t\"refactor\",\n\t\"review\",\n\t\"plan\",\n\t\"simple-edit\",\n\t\"code-gen\",\n\t\"trivial\",\n];\n\n/**\n * Named weight schema for the v4 scorer (reasoning-router-v4.ts). Every field\n * is a plain, deterministic integer/ratio; there is no clock, randomness, or\n * I/O anywhere in this file or its consumer.\n */\nexport interface RouterWeightsV4 {\n\t/** Bump to \"code-gen\" when a code fence or diff hunk is present. */\n\treadonly codeFenceOrDiff: number;\n\t/** Bump to the primary leading-clause intent class, if any. */\n\treadonly leadingIntent: number;\n\t/** Bump to a distinct second-clause intent class in a detected compound prompt (v4-new; default half of `leadingIntent`). */\n\treadonly secondClauseIntent: number;\n\t/** Bump to \"simple-edit\" when the composite local-edit feature fires. */\n\treadonly localEdit: number;\n\t/** Bump to \"debug\" when the composite diagnostic-evidence feature fires. */\n\treadonly diagnosticEvidence: number;\n\t/** Bump to \"review\" when the composite review-scope feature fires. */\n\treadonly reviewScope: number;\n\t/** Bump to \"plan\" when the composite plan-brief feature fires. */\n\treadonly planBrief: number;\n\t/** Bump to \"plan\" for release-bound operational runbooks (commit/push/tag/publish/changelog/CI bundles). */\n\treadonly operationalRunbook: number;\n\t/** Bump to \"refactor\" when the refactor-cue pattern matches anywhere in the prompt. */\n\treadonly refactorCue: number;\n\t/** Bump to \"code-gen\" when an implementation-object noun is present. */\n\treadonly implementationObject: number;\n\t/** Bump to \"code-gen\" when a first-person request names a code artifact (for example, \"I need a script\"). */\n\treadonly codeGenArtifactRequest: number;\n\t/** Bump to \"review\" when a leading review request names an evaluative object such as holes/regressions/issues. */\n\treadonly evaluativeReviewObject: number;\n\t/** Per-class whole-prompt keyword-family bump; trivial has no family, 0. */\n\treadonly keywordFamily: Readonly<Record<TaskClassV4, number>>;\n\t/** Bump to a class when normalized seed->cluster anchors match (medium generalized evidence). */\n\treadonly normalizedIntentCluster: number;\n\t/** Bump to a class when the bounded intent-skeleton extractor matches (medium generalized evidence). */\n\treadonly intentSkeleton: number;\n\t/** Bump to \"code-gen\" for a bare \"add\" keyword, gated by !localEdit. */\n\treadonly addKeyword: number;\n\t/** Bump to the class matching `history[0]`, if supplied (v4-new; 0 under DEFAULT_WEIGHTS_V4, inert until calibrated). */\n\treadonly multiTurnPrior: number;\n\t/** Linear per-bucket bump applied to debug/review/plan under context pressure (v4-new; 0 under DEFAULT_WEIGHTS_V4, inert until calibrated). */\n\treadonly pressureBucket: number;\n\t/** Bump to the class matching an externally supplied judge vote, if any (v4-new; 0 under DEFAULT_WEIGHTS_V4, inert until calibrated). */\n\treadonly judgeVote: number;\n\t/** Bounded look-back window (characters) for negation-cue gating, never crossing a .,;!? boundary. */\n\treadonly negationWindowChars: number;\n\t/** confidence <= this value bands as \"low\" (confidence combines score separation and evidence strength). */\n\treadonly lowConfidenceThreshold: number;\n\t/** confidence >= this value bands as \"high\". */\n\treadonly highConfidenceThreshold: number;\n}\n\n/**\n * Calibrated default preset. The named weights below are the production v4\n * scorer configuration covered by\n * test/suite/regressions/013-reasoning-router-v4-accuracy.test.ts. Extension\n * fields (`multiTurnPrior`, `pressureBucket`, `judgeVote`) are wired but inert\n * until a future calibration changes them.\n */\nexport const DEFAULT_WEIGHTS_V4: RouterWeightsV4 = {\n\tcodeFenceOrDiff: 4,\n\tleadingIntent: 8,\n\tsecondClauseIntent: 4,\n\tlocalEdit: 7,\n\tdiagnosticEvidence: 7,\n\treviewScope: 3,\n\tplanBrief: 8,\n\toperationalRunbook: 8,\n\trefactorCue: 6,\n\timplementationObject: 3,\n\tcodeGenArtifactRequest: 5,\n\tevaluativeReviewObject: 3,\n\tkeywordFamily: {\n\t\ttrivial: 0,\n\t\t\"simple-edit\": 4,\n\t\t\"code-gen\": 4,\n\t\tdebug: 4,\n\t\trefactor: 4,\n\t\treview: 4,\n\t\tplan: 4,\n\t},\n\tnormalizedIntentCluster: 4,\n\tintentSkeleton: 5,\n\taddKeyword: 1,\n\t// Goal 016/L2 calibration (scripts/reasoning-router/calibrate-inert-weights.ts):\n\t// adversarial bounds on the frozen gold set after the topScore>0 vote gate —\n\t// multiTurnPrior/judgeVote flip at 4 (gold-0066), pressureBucket at 2.\n\t// multiTurnPrior=2 / judgeVote=2 sit one step below the bound; pressureBucket=1\n\t// is provably safe because the runtime bucket is hard-clamped to <= 3, so the\n\t// max possible bump (+3) stays under the flip threshold (+6).\n\tmultiTurnPrior: 2,\n\tpressureBucket: 1,\n\tjudgeVote: 2,\n\tnegationWindowChars: 24,\n\tlowConfidenceThreshold: 0.35,\n\thighConfidenceThreshold: 0.7,\n};\n"]}
@@ -62,9 +62,15 @@ export const DEFAULT_WEIGHTS_V4 = {
62
62
  normalizedIntentCluster: 4,
63
63
  intentSkeleton: 5,
64
64
  addKeyword: 1,
65
- multiTurnPrior: 0,
66
- pressureBucket: 0,
67
- judgeVote: 0,
65
+ // Goal 016/L2 calibration (scripts/reasoning-router/calibrate-inert-weights.ts):
66
+ // adversarial bounds on the frozen gold set after the topScore>0 vote gate —
67
+ // multiTurnPrior/judgeVote flip at 4 (gold-0066), pressureBucket at 2.
68
+ // multiTurnPrior=2 / judgeVote=2 sit one step below the bound; pressureBucket=1
69
+ // is provably safe because the runtime bucket is hard-clamped to <= 3, so the
70
+ // max possible bump (+3) stays under the flip threshold (+6).
71
+ multiTurnPrior: 2,
72
+ pressureBucket: 1,
73
+ judgeVote: 2,
68
74
  negationWindowChars: 24,
69
75
  lowConfidenceThreshold: 0.35,
70
76
  highConfidenceThreshold: 0.7,
@@ -1 +1 @@
1
- {"version":3,"file":"reasoning-router-v4-weights.js","sourceRoot":"","sources":["../../src/core/reasoning-router-v4-weights.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAQH,yIAAyI;AACzI,MAAM,CAAC,MAAM,eAAe,GAA2B;IACtD,OAAO;IACP,UAAU;IACV,QAAQ;IACR,MAAM;IACN,aAAa;IACb,UAAU;IACV,SAAS;CACT,CAAC;AAsDF;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAoB;IAClD,eAAe,EAAE,CAAC;IAClB,aAAa,EAAE,CAAC;IAChB,kBAAkB,EAAE,CAAC;IACrB,SAAS,EAAE,CAAC;IACZ,kBAAkB,EAAE,CAAC;IACrB,WAAW,EAAE,CAAC;IACd,SAAS,EAAE,CAAC;IACZ,kBAAkB,EAAE,CAAC;IACrB,WAAW,EAAE,CAAC;IACd,oBAAoB,EAAE,CAAC;IACvB,sBAAsB,EAAE,CAAC;IACzB,sBAAsB,EAAE,CAAC;IACzB,aAAa,EAAE;QACd,OAAO,EAAE,CAAC;QACV,aAAa,EAAE,CAAC;QAChB,UAAU,EAAE,CAAC;QACb,KAAK,EAAE,CAAC;QACR,QAAQ,EAAE,CAAC;QACX,MAAM,EAAE,CAAC;QACT,IAAI,EAAE,CAAC;KACP;IACD,uBAAuB,EAAE,CAAC;IAC1B,cAAc,EAAE,CAAC;IACjB,UAAU,EAAE,CAAC;IACb,cAAc,EAAE,CAAC;IACjB,cAAc,EAAE,CAAC;IACjB,SAAS,EAAE,CAAC;IACZ,mBAAmB,EAAE,EAAE;IACvB,sBAAsB,EAAE,IAAI;IAC5B,uBAAuB,EAAE,GAAG;CAC5B,CAAC","sourcesContent":["/**\n * Reasoning-router v4 — frozen types and calibratable weight schema.\n *\n * Pure, side-effect-free, erasable TypeScript. No runtime imports. This file\n * owns the closed task/lane unions, named scorer weights, and calibrated default\n * preset used by `/think auto`.\n *\n * `keywordFamily` is the class -> weight table for whole-prompt keyword-family\n * patterns (see reasoning-router-v4.ts). `trivial` carries weight 0 because it\n * is reached only via the zero-score fallback cascade.\n *\n * `secondClauseIntent` applies a bounded bonus when a second, distinct leading\n * verb is found after a short-range conjunction split. `negationWindowChars`\n * bounds negation gating so a nearby \"don't/skip/without/no need\" cue can\n * suppress a whole-prompt signal without crossing a clause boundary.\n *\n * `lowConfidenceThreshold` / `highConfidenceThreshold` bound the confidence\n * bands (`ClassifierVerdictV4.confidenceBand`) computed from score separation\n * plus absolute evidence strength in reasoning-router-v4.ts. They are\n * metadata-only: they never change `taskClass`, only how\n * `resolveThinkingLevelV4WithUncertainty` treats an already-decided class.\n */\n\n/** Closed set of v4 task classes. */\nexport type TaskClassV4 = \"trivial\" | \"simple-edit\" | \"code-gen\" | \"debug\" | \"refactor\" | \"review\" | \"plan\";\n\n/** Subagent lane types recognized by the v4 classifier and resolvers. */\nexport type ReasoningLaneTypeV4 = \"planner\" | \"security\" | \"explorer\" | \"coder\" | \"reviewer\" | \"tester\";\n\n/** Fixed iteration order over TaskClassV4 (matches the type-declaration order; also v4's argmax tie-break precedence, highest first). */\nexport const TASK_CLASSES_V4: readonly TaskClassV4[] = [\n\t\"debug\",\n\t\"refactor\",\n\t\"review\",\n\t\"plan\",\n\t\"simple-edit\",\n\t\"code-gen\",\n\t\"trivial\",\n];\n\n/**\n * Named weight schema for the v4 scorer (reasoning-router-v4.ts). Every field\n * is a plain, deterministic integer/ratio; there is no clock, randomness, or\n * I/O anywhere in this file or its consumer.\n */\nexport interface RouterWeightsV4 {\n\t/** Bump to \"code-gen\" when a code fence or diff hunk is present. */\n\treadonly codeFenceOrDiff: number;\n\t/** Bump to the primary leading-clause intent class, if any. */\n\treadonly leadingIntent: number;\n\t/** Bump to a distinct second-clause intent class in a detected compound prompt (v4-new; default half of `leadingIntent`). */\n\treadonly secondClauseIntent: number;\n\t/** Bump to \"simple-edit\" when the composite local-edit feature fires. */\n\treadonly localEdit: number;\n\t/** Bump to \"debug\" when the composite diagnostic-evidence feature fires. */\n\treadonly diagnosticEvidence: number;\n\t/** Bump to \"review\" when the composite review-scope feature fires. */\n\treadonly reviewScope: number;\n\t/** Bump to \"plan\" when the composite plan-brief feature fires. */\n\treadonly planBrief: number;\n\t/** Bump to \"plan\" for release-bound operational runbooks (commit/push/tag/publish/changelog/CI bundles). */\n\treadonly operationalRunbook: number;\n\t/** Bump to \"refactor\" when the refactor-cue pattern matches anywhere in the prompt. */\n\treadonly refactorCue: number;\n\t/** Bump to \"code-gen\" when an implementation-object noun is present. */\n\treadonly implementationObject: number;\n\t/** Bump to \"code-gen\" when a first-person request names a code artifact (for example, \"I need a script\"). */\n\treadonly codeGenArtifactRequest: number;\n\t/** Bump to \"review\" when a leading review request names an evaluative object such as holes/regressions/issues. */\n\treadonly evaluativeReviewObject: number;\n\t/** Per-class whole-prompt keyword-family bump; trivial has no family, 0. */\n\treadonly keywordFamily: Readonly<Record<TaskClassV4, number>>;\n\t/** Bump to a class when normalized seed->cluster anchors match (medium generalized evidence). */\n\treadonly normalizedIntentCluster: number;\n\t/** Bump to a class when the bounded intent-skeleton extractor matches (medium generalized evidence). */\n\treadonly intentSkeleton: number;\n\t/** Bump to \"code-gen\" for a bare \"add\" keyword, gated by !localEdit. */\n\treadonly addKeyword: number;\n\t/** Bump to the class matching `history[0]`, if supplied (v4-new; 0 under DEFAULT_WEIGHTS_V4, inert until calibrated). */\n\treadonly multiTurnPrior: number;\n\t/** Linear per-bucket bump applied to debug/review/plan under context pressure (v4-new; 0 under DEFAULT_WEIGHTS_V4, inert until calibrated). */\n\treadonly pressureBucket: number;\n\t/** Bump to the class matching an externally supplied judge vote, if any (v4-new; 0 under DEFAULT_WEIGHTS_V4, inert until calibrated). */\n\treadonly judgeVote: number;\n\t/** Bounded look-back window (characters) for negation-cue gating, never crossing a .,;!? boundary. */\n\treadonly negationWindowChars: number;\n\t/** confidence <= this value bands as \"low\" (confidence combines score separation and evidence strength). */\n\treadonly lowConfidenceThreshold: number;\n\t/** confidence >= this value bands as \"high\". */\n\treadonly highConfidenceThreshold: number;\n}\n\n/**\n * Calibrated default preset. The named weights below are the production v4\n * scorer configuration covered by\n * test/suite/regressions/013-reasoning-router-v4-accuracy.test.ts. Extension\n * fields (`multiTurnPrior`, `pressureBucket`, `judgeVote`) are wired but inert\n * until a future calibration changes them.\n */\nexport const DEFAULT_WEIGHTS_V4: RouterWeightsV4 = {\n\tcodeFenceOrDiff: 4,\n\tleadingIntent: 8,\n\tsecondClauseIntent: 4,\n\tlocalEdit: 7,\n\tdiagnosticEvidence: 7,\n\treviewScope: 3,\n\tplanBrief: 8,\n\toperationalRunbook: 8,\n\trefactorCue: 6,\n\timplementationObject: 3,\n\tcodeGenArtifactRequest: 5,\n\tevaluativeReviewObject: 3,\n\tkeywordFamily: {\n\t\ttrivial: 0,\n\t\t\"simple-edit\": 4,\n\t\t\"code-gen\": 4,\n\t\tdebug: 4,\n\t\trefactor: 4,\n\t\treview: 4,\n\t\tplan: 4,\n\t},\n\tnormalizedIntentCluster: 4,\n\tintentSkeleton: 5,\n\taddKeyword: 1,\n\tmultiTurnPrior: 0,\n\tpressureBucket: 0,\n\tjudgeVote: 0,\n\tnegationWindowChars: 24,\n\tlowConfidenceThreshold: 0.35,\n\thighConfidenceThreshold: 0.7,\n};\n"]}
1
+ {"version":3,"file":"reasoning-router-v4-weights.js","sourceRoot":"","sources":["../../src/core/reasoning-router-v4-weights.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;GAqBG;AAQH,yIAAyI;AACzI,MAAM,CAAC,MAAM,eAAe,GAA2B;IACtD,OAAO;IACP,UAAU;IACV,QAAQ;IACR,MAAM;IACN,aAAa;IACb,UAAU;IACV,SAAS;CACT,CAAC;AAsDF;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAoB;IAClD,eAAe,EAAE,CAAC;IAClB,aAAa,EAAE,CAAC;IAChB,kBAAkB,EAAE,CAAC;IACrB,SAAS,EAAE,CAAC;IACZ,kBAAkB,EAAE,CAAC;IACrB,WAAW,EAAE,CAAC;IACd,SAAS,EAAE,CAAC;IACZ,kBAAkB,EAAE,CAAC;IACrB,WAAW,EAAE,CAAC;IACd,oBAAoB,EAAE,CAAC;IACvB,sBAAsB,EAAE,CAAC;IACzB,sBAAsB,EAAE,CAAC;IACzB,aAAa,EAAE;QACd,OAAO,EAAE,CAAC;QACV,aAAa,EAAE,CAAC;QAChB,UAAU,EAAE,CAAC;QACb,KAAK,EAAE,CAAC;QACR,QAAQ,EAAE,CAAC;QACX,MAAM,EAAE,CAAC;QACT,IAAI,EAAE,CAAC;KACP;IACD,uBAAuB,EAAE,CAAC;IAC1B,cAAc,EAAE,CAAC;IACjB,UAAU,EAAE,CAAC;IACb,iFAAiF;IACjF,+EAA6E;IAC7E,uEAAuE;IACvE,gFAAgF;IAChF,8EAA8E;IAC9E,8DAA8D;IAC9D,cAAc,EAAE,CAAC;IACjB,cAAc,EAAE,CAAC;IACjB,SAAS,EAAE,CAAC;IACZ,mBAAmB,EAAE,EAAE;IACvB,sBAAsB,EAAE,IAAI;IAC5B,uBAAuB,EAAE,GAAG;CAC5B,CAAC","sourcesContent":["/**\n * Reasoning-router v4 — frozen types and calibratable weight schema.\n *\n * Pure, side-effect-free, erasable TypeScript. No runtime imports. This file\n * owns the closed task/lane unions, named scorer weights, and calibrated default\n * preset used by `/think auto`.\n *\n * `keywordFamily` is the class -> weight table for whole-prompt keyword-family\n * patterns (see reasoning-router-v4.ts). `trivial` carries weight 0 because it\n * is reached only via the zero-score fallback cascade.\n *\n * `secondClauseIntent` applies a bounded bonus when a second, distinct leading\n * verb is found after a short-range conjunction split. `negationWindowChars`\n * bounds negation gating so a nearby \"don't/skip/without/no need\" cue can\n * suppress a whole-prompt signal without crossing a clause boundary.\n *\n * `lowConfidenceThreshold` / `highConfidenceThreshold` bound the confidence\n * bands (`ClassifierVerdictV4.confidenceBand`) computed from score separation\n * plus absolute evidence strength in reasoning-router-v4.ts. They are\n * metadata-only: they never change `taskClass`, only how\n * `resolveThinkingLevelV4WithUncertainty` treats an already-decided class.\n */\n\n/** Closed set of v4 task classes. */\nexport type TaskClassV4 = \"trivial\" | \"simple-edit\" | \"code-gen\" | \"debug\" | \"refactor\" | \"review\" | \"plan\";\n\n/** Subagent lane types recognized by the v4 classifier and resolvers. */\nexport type ReasoningLaneTypeV4 = \"planner\" | \"security\" | \"explorer\" | \"coder\" | \"reviewer\" | \"tester\";\n\n/** Fixed iteration order over TaskClassV4 (matches the type-declaration order; also v4's argmax tie-break precedence, highest first). */\nexport const TASK_CLASSES_V4: readonly TaskClassV4[] = [\n\t\"debug\",\n\t\"refactor\",\n\t\"review\",\n\t\"plan\",\n\t\"simple-edit\",\n\t\"code-gen\",\n\t\"trivial\",\n];\n\n/**\n * Named weight schema for the v4 scorer (reasoning-router-v4.ts). Every field\n * is a plain, deterministic integer/ratio; there is no clock, randomness, or\n * I/O anywhere in this file or its consumer.\n */\nexport interface RouterWeightsV4 {\n\t/** Bump to \"code-gen\" when a code fence or diff hunk is present. */\n\treadonly codeFenceOrDiff: number;\n\t/** Bump to the primary leading-clause intent class, if any. */\n\treadonly leadingIntent: number;\n\t/** Bump to a distinct second-clause intent class in a detected compound prompt (v4-new; default half of `leadingIntent`). */\n\treadonly secondClauseIntent: number;\n\t/** Bump to \"simple-edit\" when the composite local-edit feature fires. */\n\treadonly localEdit: number;\n\t/** Bump to \"debug\" when the composite diagnostic-evidence feature fires. */\n\treadonly diagnosticEvidence: number;\n\t/** Bump to \"review\" when the composite review-scope feature fires. */\n\treadonly reviewScope: number;\n\t/** Bump to \"plan\" when the composite plan-brief feature fires. */\n\treadonly planBrief: number;\n\t/** Bump to \"plan\" for release-bound operational runbooks (commit/push/tag/publish/changelog/CI bundles). */\n\treadonly operationalRunbook: number;\n\t/** Bump to \"refactor\" when the refactor-cue pattern matches anywhere in the prompt. */\n\treadonly refactorCue: number;\n\t/** Bump to \"code-gen\" when an implementation-object noun is present. */\n\treadonly implementationObject: number;\n\t/** Bump to \"code-gen\" when a first-person request names a code artifact (for example, \"I need a script\"). */\n\treadonly codeGenArtifactRequest: number;\n\t/** Bump to \"review\" when a leading review request names an evaluative object such as holes/regressions/issues. */\n\treadonly evaluativeReviewObject: number;\n\t/** Per-class whole-prompt keyword-family bump; trivial has no family, 0. */\n\treadonly keywordFamily: Readonly<Record<TaskClassV4, number>>;\n\t/** Bump to a class when normalized seed->cluster anchors match (medium generalized evidence). */\n\treadonly normalizedIntentCluster: number;\n\t/** Bump to a class when the bounded intent-skeleton extractor matches (medium generalized evidence). */\n\treadonly intentSkeleton: number;\n\t/** Bump to \"code-gen\" for a bare \"add\" keyword, gated by !localEdit. */\n\treadonly addKeyword: number;\n\t/** Bump to the class matching `history[0]`, if supplied (v4-new; 0 under DEFAULT_WEIGHTS_V4, inert until calibrated). */\n\treadonly multiTurnPrior: number;\n\t/** Linear per-bucket bump applied to debug/review/plan under context pressure (v4-new; 0 under DEFAULT_WEIGHTS_V4, inert until calibrated). */\n\treadonly pressureBucket: number;\n\t/** Bump to the class matching an externally supplied judge vote, if any (v4-new; 0 under DEFAULT_WEIGHTS_V4, inert until calibrated). */\n\treadonly judgeVote: number;\n\t/** Bounded look-back window (characters) for negation-cue gating, never crossing a .,;!? boundary. */\n\treadonly negationWindowChars: number;\n\t/** confidence <= this value bands as \"low\" (confidence combines score separation and evidence strength). */\n\treadonly lowConfidenceThreshold: number;\n\t/** confidence >= this value bands as \"high\". */\n\treadonly highConfidenceThreshold: number;\n}\n\n/**\n * Calibrated default preset. The named weights below are the production v4\n * scorer configuration covered by\n * test/suite/regressions/013-reasoning-router-v4-accuracy.test.ts. Extension\n * fields (`multiTurnPrior`, `pressureBucket`, `judgeVote`) are wired but inert\n * until a future calibration changes them.\n */\nexport const DEFAULT_WEIGHTS_V4: RouterWeightsV4 = {\n\tcodeFenceOrDiff: 4,\n\tleadingIntent: 8,\n\tsecondClauseIntent: 4,\n\tlocalEdit: 7,\n\tdiagnosticEvidence: 7,\n\treviewScope: 3,\n\tplanBrief: 8,\n\toperationalRunbook: 8,\n\trefactorCue: 6,\n\timplementationObject: 3,\n\tcodeGenArtifactRequest: 5,\n\tevaluativeReviewObject: 3,\n\tkeywordFamily: {\n\t\ttrivial: 0,\n\t\t\"simple-edit\": 4,\n\t\t\"code-gen\": 4,\n\t\tdebug: 4,\n\t\trefactor: 4,\n\t\treview: 4,\n\t\tplan: 4,\n\t},\n\tnormalizedIntentCluster: 4,\n\tintentSkeleton: 5,\n\taddKeyword: 1,\n\t// Goal 016/L2 calibration (scripts/reasoning-router/calibrate-inert-weights.ts):\n\t// adversarial bounds on the frozen gold set after the topScore>0 vote gate —\n\t// multiTurnPrior/judgeVote flip at 4 (gold-0066), pressureBucket at 2.\n\t// multiTurnPrior=2 / judgeVote=2 sit one step below the bound; pressureBucket=1\n\t// is provably safe because the runtime bucket is hard-clamped to <= 3, so the\n\t// max possible bump (+3) stays under the flip threshold (+6).\n\tmultiTurnPrior: 2,\n\tpressureBucket: 1,\n\tjudgeVote: 2,\n\tnegationWindowChars: 24,\n\tlowConfidenceThreshold: 0.35,\n\thighConfidenceThreshold: 0.7,\n};\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"reasoning-router-v4.d.ts","sourceRoot":"","sources":["../../src/core/reasoning-router-v4.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AACpD,OAAO,EAEN,KAAK,mBAAmB,EACxB,KAAK,eAAe,EAEpB,KAAK,WAAW,EAChB,MAAM,kCAAkC,CAAC;AAE1C,YAAY,EAAE,mBAAmB,EAAE,eAAe,EAAE,WAAW,EAAE,MAAM,kCAAkC,CAAC;AAC1G,OAAO,EAAE,kBAAkB,EAAE,eAAe,EAAE,MAAM,kCAAkC,CAAC;AAs3BvF;;;;GAIG;AACH,MAAM,WAAW,qBAAqB;IACrC,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,mBAAmB,CAAC;IAC/B,OAAO,CAAC,EAAE,SAAS,WAAW,EAAE,CAAC;IACjC,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,SAAS,CAAC,EAAE,WAAW,GAAG,IAAI,CAAC;CAC/B;AAED,qFAAqF;AACrF,MAAM,MAAM,gBAAgB,GAAG,KAAK,GAAG,QAAQ,GAAG,MAAM,CAAC;AAEzD,wHAAwH;AACxH,MAAM,MAAM,gBAAgB,GACzB,oBAAoB,GACpB,gBAAgB,GAChB,sBAAsB,GACtB,YAAY,GACZ,eAAe,GACf,SAAS,CAAC;AAEb;;;;;GAKG;AACH,MAAM,WAAW,mBAAmB;IACnC,qEAAqE;IACrE,QAAQ,CAAC,SAAS,EAAE,WAAW,CAAC;IAChC,mFAAmF;IACnF,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC,CAAC;IACvD,mGAAmG;IACnG,QAAQ,CAAC,QAAQ,EAAE,WAAW,GAAG,IAAI,CAAC;IACtC,oIAAoI;IACpI,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,sGAAsG;IACtG,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,2FAA2F;IAC3F,QAAQ,CAAC,cAAc,EAAE,gBAAgB,CAAC;IAC1C,qHAAqH;IACrH,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,qGAAqG;IACrG,QAAQ,CAAC,cAAc,EAAE,gBAAgB,GAAG,IAAI,CAAC;IACjD,oHAAoH;IACpH,QAAQ,CAAC,oBAAoB,EAAE,SAAS,MAAM,EAAE,CAAC;IACjD,0GAA0G;IAC1G,QAAQ,CAAC,cAAc,EAAE,OAAO,CAAC;IACjC,oHAAoH;IACpH,QAAQ,CAAC,kBAAkB,EAAE,WAAW,GAAG,IAAI,CAAC;CAChD;AAMD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,cAAc,CAC7B,KAAK,EAAE,qBAAqB,EAC5B,OAAO,GAAE,eAAoC,GAC3C,mBAAmB,CA2ErB;AAMD;;;;GAIG;AACH,eAAO,MAAM,6BAA6B,8FAA6B,CAAC;AAUxE;;;;GAIG;AACH,wBAAgB,6BAA6B,CAC5C,SAAS,EAAE,WAAW,EACtB,eAAe,EAAE,SAAS,aAAa,EAAE,EACzC,QAAQ,EAAE,mBAAmB,GAAG,SAAS,GACvC,aAAa,CAEf;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,qCAAqC,CACpD,OAAO,EAAE,mBAAmB,EAC5B,eAAe,EAAE,SAAS,aAAa,EAAE,EACzC,QAAQ,EAAE,mBAAmB,GAAG,SAAS,EACzC,IAAI,SAAI,EACR,IAAI,GAAE;IAAE,KAAK,EAAE,aAAa,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,GAAG,IAAW,GAC9D,aAAa,CAIf","sourcesContent":["/**\n * Reasoning-router v4 — the sole `/think auto` classifier/resolver.\n *\n * Deterministic by construction: same (input, weights) -> same\n * ClassifierVerdictV4; same (verdict, availableLevels, laneType, bias, hint) ->\n * same ThinkingLevel. No clock, randomness, I/O, model calls, network access,\n * or state mutation anywhere in this file (ThinkingLevel is a type-only import\n * and is erased).\n *\n * `classifyTaskV4` returns a confidence-bearing verdict: task class, per-class\n * scores, runner-up, margin, confidence band, tie-break flag, fallback reason,\n * bounded-negation audit ids, and compound-intent metadata. None of this carries\n * prompt text — every field is a bounded enum, number, boolean, or a closed set\n * of short diagnostic-id strings.\n *\n * `resolveThinkingLevelV4WithUncertainty` starts from the canonical task-class\n * rule table, applies lane/bias/hint adjustments, and only adds non-negative\n * confidence escalation. A prompt cannot lower its own effort by asking the\n * router to \"think less\"; low confidence or fallback routing can only hold or\n * raise the resolved level.\n */\n\nimport type { ThinkingLevel } from \"omk-agent-core\";\nimport {\n\tDEFAULT_WEIGHTS_V4,\n\ttype ReasoningLaneTypeV4,\n\ttype RouterWeightsV4,\n\tTASK_CLASSES_V4,\n\ttype TaskClassV4,\n} from \"./reasoning-router-v4-weights.ts\";\n\nexport type { ReasoningLaneTypeV4, RouterWeightsV4, TaskClassV4 } from \"./reasoning-router-v4-weights.ts\";\nexport { DEFAULT_WEIGHTS_V4, TASK_CLASSES_V4 } from \"./reasoning-router-v4-weights.ts\";\n\nimport { resolveThinkingLevelCore, TASK_CLASS_THINKING_LEVELS } from \"./reasoning-router-resolver.ts\";\nimport { extractGeneralizedIntentEvidenceV4 } from \"./reasoning-router-v4-normalize.ts\";\n\n/** Prompts shorter than this (trimmed) fall back to trivial when no real signal scores > 0. */\nconst TRIVIAL_MAX_CHARS_V4 = 40;\n/** Prompts at/above this length with no real signal fall back to plan (long prose brief). */\nconst COMPLEX_PROSE_MIN_CHARS_V4 = 2400;\n/** Plan-brief prompts at/above this length count as a \"long brief\" even without PLAN_BRIEF_PATTERN. */\nconst LONG_BRIEF_MIN_CHARS_V4 = 512;\n/** Compound-clause split only applies within this many leading characters (short direct commands, not long prose). */\nconst COMPOUND_SPLIT_MAX_INDEX_V4 = 300;\n/** Minimum trimmed length of a candidate second clause to be worth checking for a leading intent. */\nconst COMPOUND_SECOND_CLAUSE_MIN_CHARS_V4 = 3;\n/** Bounded look-ahead cap when checking a second clause's leading intent (defense in depth; independent of regex cost). */\nconst COMPOUND_SECOND_CLAUSE_SCAN_CHARS_V4 = 200;\n\n/** Lane fallback class, used only when every scored class is <= 0 (zero-score cascade). */\nconst LANE_FALLBACK_CLASS_V4: Readonly<Record<ReasoningLaneTypeV4, TaskClassV4>> = {\n\tplanner: \"plan\",\n\tsecurity: \"review\",\n\texplorer: \"review\",\n\tcoder: \"code-gen\",\n\treviewer: \"review\",\n\ttester: \"code-gen\",\n};\n\ntype IntentLexemeClusterRoleV4 = \"leading-intent\" | \"whole-prompt\" | \"object-shape\" | \"korean-morphology\";\n\ntype IntentLexemeClusterV4 = {\n\treadonly id: string;\n\treadonly taskClass: TaskClassV4;\n\treadonly role: IntentLexemeClusterRoleV4;\n\treadonly surfaces: readonly string[];\n\treadonly phrases: readonly RegExp[];\n\treadonly negativeControlIds: readonly string[];\n};\n\nconst INTENT_LEXEME_CLUSTERS_V4: readonly IntentLexemeClusterV4[] = [\n\t{\n\t\tid: \"debug-leading-actions\",\n\t\ttaskClass: \"debug\",\n\t\trole: \"leading-intent\",\n\t\tsurfaces: [\"debug\", \"investigate why\", \"reproduce\", \"trace\", \"fix this traceback/panic/error\"],\n\t\tphrases: [/^(?:debug|investigate\\s+why|reproduce|trace\\b)|^fix\\s+this\\s+(?:traceback|panic|error)\\b/i],\n\t\tnegativeControlIds: [\"negated-debug-action\", \"generic-error-handling\"],\n\t},\n\t{\n\t\tid: \"review-leading-actions\",\n\t\ttaskClass: \"review\",\n\t\trole: \"leading-intent\",\n\t\tsurfaces: [\"review\", \"audit\", \"critique\", \"inspect\", \"assess\", \"approve\", \"double-check\", \"lgtm\"],\n\t\tphrases: [/^(?:review|audit(?!\\s+log\\b)|critique|inspect|assess|approve|double-?check|lgtm)\\b/i],\n\t\tnegativeControlIds: [\"audit-log-not-review\", \"review-negated-before-code-gen\"],\n\t},\n\t{\n\t\tid: \"review-leading-synonyms\",\n\t\ttaskClass: \"review\",\n\t\trole: \"leading-intent\",\n\t\tsurfaces: [\n\t\t\t\"give this PR a once-over\",\n\t\t\t\"look over\",\n\t\t\t\"sanity-check\",\n\t\t\t\"eyeball\",\n\t\t\t\"poke holes\",\n\t\t\t\"pressure-test\",\n\t\t\t\"tell me whether/if\",\n\t\t\t\"what could go wrong\",\n\t\t],\n\t\tphrases: [\n\t\t\t/^(?:give\\s+(?:this|the|my)?\\s*(?:pr|pull\\s+request|diff|code|change|branch)?\\s*(?:a\\s+)?once-?over|look\\s+over|sanity[-\\s]?check|eyeball|poke\\s+holes(?:\\s+in)?|pressure[-\\s]?test|sign\\s+off|tell\\s+me\\s+(?:if|whether)|tell\\s+me\\s+what\\s+could\\s+go\\s+wrong|where\\s+would\\b[^.!?;\\n]{0,100}\\bfall\\s+apart|what\\s+am\\s+i\\s+missing|check\\s+whether|look\\s+for\\s+(?:edge\\s+cases|risks?|flaws?)|find\\s+(?:flaws|risks?|issues?)|validate\\s+(?:the\\s+)?reasoning|give\\s+me\\s+a\\s+thumbs[-\\s]?up\\/?down|read\\s+(?:this|the|my)\\b[^.!?;\\n]{0,80}\\bchallenge|(?:is|are|does|do|can|could|would|should)\\s+(?:this|these|my|the)\\b[^.!?;\\n]{0,100}\\b(?:right\\s+way|handle|cover|safe|sane|clean|overkill|prove|leak|ship|concurrent|hidden\\s+coupling)|(?:at\\s+architecture\\s+level|architecture[-\\s]?level)\\b[^.!?;\\n]{0,80}\\bhidden\\s+coupling)\\b/i,\n\t\t],\n\t\tnegativeControlIds: [\"sanity-check-setup-script\", \"future-self-review-context\", \"check-whether-implementation\"],\n\t},\n\t{\n\t\tid: \"plan-leading-actions\",\n\t\ttaskClass: \"plan\",\n\t\trole: \"leading-intent\",\n\t\tsurfaces: [\"plan\", \"design\", \"architect\", \"decompose\", \"map out\", \"draw up\", \"think through\"],\n\t\tphrases: [\n\t\t\t/^(?:let(?:'|’)s\\s+)?(?:plan|design|architect|decompose|map\\s+out|draw\\s+up|think\\s+through)\\b/i,\n\t\t\t/^(?:write|create)\\s+(?:a\\s+)?(?:technical\\s+)?(?:spec|roadmap|strategy|plan)\\b/i,\n\t\t],\n\t\tnegativeControlIds: [\"design-token-edit\", \"draw-up-implementation-only\"],\n\t},\n\t{\n\t\tid: \"refactor-leading-actions\",\n\t\ttaskClass: \"refactor\",\n\t\trole: \"leading-intent\",\n\t\tsurfaces: [\"refactor\", \"extract\", \"rename\", \"deduplicate\", \"simplify\", \"clean up\", \"untangle\"],\n\t\tphrases: [\n\t\t\t/^(?:refactor(?:ing|ed)?|extract|rename|deduplicate|consolidate|modularize|reorganize|simplify|restructure|untangle)\\b|^clean\\s+up\\b|^split\\s+(?:the\\s+)?module\\b|^move\\s+logic\\b/i,\n\t\t],\n\t\tnegativeControlIds: [\"negated-refactor\", \"future-self-refactor\"],\n\t},\n\t{\n\t\tid: \"simple-edit-leading-actions\",\n\t\ttaskClass: \"simple-edit\",\n\t\trole: \"leading-intent\",\n\t\tsurfaces: [\"correct\", \"update\", \"swap\", \"remove\", \"adjust\", \"fix\", \"add\", \"change\", \"trim\", \"reword\"],\n\t\tphrases: [/^(?:correct|update|swap|remove|adjust|fix|add|change|trim|reword|tweak|bump)\\b/i],\n\t\tnegativeControlIds: [\"add-feature-not-simple-edit\", \"fix-crash-not-simple-edit\"],\n\t},\n\t{\n\t\tid: \"code-gen-leading-actions\",\n\t\ttaskClass: \"code-gen\",\n\t\trole: \"leading-intent\",\n\t\tsurfaces: [\n\t\t\t\"implement\",\n\t\t\t\"write\",\n\t\t\t\"create\",\n\t\t\t\"build\",\n\t\t\t\"generate\",\n\t\t\t\"scaffold\",\n\t\t\t\"prototype\",\n\t\t\t\"add\",\n\t\t\t\"whip up\",\n\t\t\t\"cook me\",\n\t\t],\n\t\tphrases: [/^(?:implement|write|create|build|generate|scaffold|prototype|add|whip\\s+up|cook\\s+me)\\b/i],\n\t\tnegativeControlIds: [\"write-a-plan\", \"create-a-review\"],\n\t},\n\t{\n\t\tid: \"debug-whole-prompt\",\n\t\ttaskClass: \"debug\",\n\t\trole: \"whole-prompt\",\n\t\tsurfaces: [\"debug\", \"investigate why\", \"reproduce\", \"track down\", \"figure out why\", \"get to the bottom\"],\n\t\tphrases: [\n\t\t\t/^(?:debug|investigate\\s+why|reproduce|trace\\b)|\\bfix\\s+this\\s+(?:traceback|panic|error)\\b|\\btrack\\s+down\\b|\\bfigure\\s+out\\s+why\\b|\\bget\\s+to\\s+the\\s+bottom\\b|디버깅|디버그|재현|원인\\s*분석/i,\n\t\t],\n\t\tnegativeControlIds: [\"not-a-bug-report\", \"error-handling-feature\"],\n\t},\n\t{\n\t\tid: \"review-whole-prompt\",\n\t\ttaskClass: \"review\",\n\t\trole: \"whole-prompt\",\n\t\tsurfaces: [\n\t\t\t\"review\",\n\t\t\t\"critique\",\n\t\t\t\"assess\",\n\t\t\t\"inspect\",\n\t\t\t\"once-over\",\n\t\t\t\"look over\",\n\t\t\t\"sanity-check\",\n\t\t\t\"eyeball\",\n\t\t\t\"poke holes\",\n\t\t\t\"pressure-test\",\n\t\t\t\"tell me whether correct\",\n\t\t],\n\t\tphrases: [\n\t\t\t/\\b(review|critique|assess|inspect|approve|lgtm|double-?check|sanity[-\\s]?check|once-?over|look\\s+over|eyeball|poke\\s+holes|pressure[-\\s]?test|sign\\s+off|thumbs[-\\s]?up\\/?down|what\\s+could\\s+go\\s+wrong|what\\s+am\\s+i\\s+missing|challenge\\s+the\\s+assumptions|hidden\\s+coupling|tell\\s+me\\s+(?:if|whether)[^.!?;\\n]{0,80}\\bcorrect|audit(?!\\s+log\\b))\\b|리뷰|검토|점검|괜찮은지\\s*봐\\s*줘|맞는지\\s*봐\\s*줘|처리되는지\\s*확인|허점.{0,10}찾|문제\\s*없는지/i,\n\t\t],\n\t\tnegativeControlIds: [\"review-free-implementation\", \"audit-log\"],\n\t},\n\t{\n\t\tid: \"plan-whole-prompt\",\n\t\ttaskClass: \"plan\",\n\t\trole: \"whole-prompt\",\n\t\tsurfaces: [\"plan\", \"design\", \"architecture\", \"roadmap\", \"map out\", \"draw up\", \"think through\"],\n\t\tphrases: [\n\t\t\t/\\b(plan|design|architect|architecture|roadmap|spec(?:ification)?|strategy|decompose|milestones?|map\\s+out|phases|draw\\s+up|think\\s+through|write\\s+a\\s+(?:technical\\s+)?spec|create\\s+a\\s+(?:roadmap|strategy|plan))\\b|설계|로드맵|아키텍처|계획.{0,6}세워|기획/i,\n\t\t],\n\t\tnegativeControlIds: [\"rather-than-design\", \"design-token-edit\"],\n\t},\n\t{\n\t\tid: \"code-gen-whole-prompt\",\n\t\ttaskClass: \"code-gen\",\n\t\trole: \"whole-prompt\",\n\t\tsurfaces: [\"implement\", \"write\", \"create\", \"build\", \"generate\", \"scaffold\", \"prototype\", \"whip up\", \"cook me\"],\n\t\tphrases: [\n\t\t\t/\\b(implement|write|create|build|generate|scaffold|prototype|whip\\s+up|cook\\s+me)\\b|구현|만들어\\s*줘|생성|작성|추가해/i,\n\t\t],\n\t\tnegativeControlIds: [\"write-plan\", \"create-roadmap\"],\n\t},\n\t{\n\t\tid: \"code-gen-artifact-request\",\n\t\ttaskClass: \"code-gen\",\n\t\trole: \"object-shape\",\n\t\tsurfaces: [\"I need a script\", \"we need a utility\", \"I need an endpoint\", \"we need a test\"],\n\t\tphrases: [\n\t\t\t/\\b(?:i|we)\\s+need\\s+(?:a|an|the|some)?\\s*(?:small\\s+|quick\\s+|new\\s+)?(?:script|utility|helper|component|endpoint|migration|test|tool|function|service)\\b(?!\\s+(?:plan|strategy|review|audit|roadmap|spec))/i,\n\t\t],\n\t\tnegativeControlIds: [\"need-a-plan\", \"need-a-review\", \"need-help-debugging\"],\n\t},\n\t{\n\t\tid: \"evaluative-review-object\",\n\t\ttaskClass: \"review\",\n\t\trole: \"object-shape\",\n\t\tsurfaces: [\"holes\", \"looks off\", \"correct\", \"safe\", \"risky\", \"issues\", \"regressions\"],\n\t\tphrases: [/\\b(holes?|looks?\\s+off|correct|safe|risky|issues?|regressions?)\\b/i],\n\t\tnegativeControlIds: [\"fix-holes\", \"implement-issue-fix\"],\n\t},\n\t{\n\t\tid: \"korean-debug-morphology\",\n\t\ttaskClass: \"debug\",\n\t\trole: \"korean-morphology\",\n\t\tsurfaces: [\n\t\t\t\"오류\",\n\t\t\t\"에러\",\n\t\t\t\"실패\",\n\t\t\t\"원인 찾아줘\",\n\t\t\t\"원인 파악\",\n\t\t\t\"숫자가 이상해\",\n\t\t\t\"합계가 안 맞아\",\n\t\t\t\"빈 파일\",\n\t\t\t\"CI가 빨개\",\n\t\t\t\"디버깅\",\n\t\t],\n\t\tphrases: [\n\t\t\t/오류|에러|실패|안\\s*돼|안\\s*됨|깨졌|깨짐|고장|디버깅|디버그|재현|원인\\s*(?:분석|파악|찾(?:아|기)?|찾아\\s*줘)?|숫자.{0,12}(?:이상|틀려|안\\s*맞)|합계.{0,12}(?:안\\s*맞|틀려|이상)|빈\\s*파일|(?:CI|러너).{0,12}빨개/i,\n\t\t],\n\t\tnegativeControlIds: [\"korean-error-handling-feature\", \"korean-greeting\"],\n\t},\n\t{\n\t\tid: \"korean-simple-edit-morphology\",\n\t\ttaskClass: \"simple-edit\",\n\t\trole: \"korean-morphology\",\n\t\tsurfaces: [\"오타\", \"맞춤법\", \"띄어쓰기\", \"문구\", \"제목\"],\n\t\tphrases: [/오타|맞춤법|띄어쓰기|문구|제목/i],\n\t\tnegativeControlIds: [\"korean-feature-edit\"],\n\t},\n\t{\n\t\tid: \"korean-review-morphology\",\n\t\ttaskClass: \"review\",\n\t\trole: \"korean-morphology\",\n\t\tsurfaces: [\"리뷰\", \"검토\", \"점검\", \"괜찮은지 봐줘\", \"맞는지 봐줘\", \"허점 찾아줘\"],\n\t\tphrases: [/리뷰|검토|점검|괜찮은지\\s*봐\\s*줘|맞는지\\s*봐\\s*줘|처리되는지\\s*확인|허점.{0,10}찾|문제\\s*없는지/i],\n\t\tnegativeControlIds: [\"korean-review-negated\"],\n\t},\n\t{\n\t\tid: \"korean-plan-morphology\",\n\t\ttaskClass: \"plan\",\n\t\trole: \"korean-morphology\",\n\t\tsurfaces: [\"설계\", \"로드맵\", \"아키텍처\", \"계획 세워\", \"계획 짜\", \"계획 수립\", \"계획 정리\"],\n\t\tphrases: [/설계|로드맵|아키텍처|계획.{0,8}(?:세워|짜|수립|정리)|기획/i],\n\t\tnegativeControlIds: [\"korean-plan-negated\"],\n\t},\n\t{\n\t\tid: \"korean-refactor-morphology\",\n\t\ttaskClass: \"refactor\",\n\t\trole: \"korean-morphology\",\n\t\tsurfaces: [\"리팩토링\", \"리팩터링\", \"구조 개선\"],\n\t\tphrases: [/리팩토링|리팩터링|구조\\s*개선/i],\n\t\tnegativeControlIds: [\"korean-refactor-negated\"],\n\t},\n\t{\n\t\tid: \"korean-code-gen-morphology\",\n\t\ttaskClass: \"code-gen\",\n\t\trole: \"korean-morphology\",\n\t\tsurfaces: [\"구현\", \"만들\", \"생성\", \"작성\", \"추가\", \"테스트\", \"수정\", \"변경\", \"바꿔\"],\n\t\tphrases: [/구현|만들|생성|작성|추가|테스트|삭제|지워|지우|제거|고쳐|고치|수정|변경|바꿔|바꾸|옮겨|옮기|넣어/i],\n\t\tnegativeControlIds: [\"korean-greeting\", \"korean-codegen-negated\"],\n\t},\n] as const;\n\nfunction clusterMatchesRoleV4(text: string, taskClass: TaskClassV4, role: IntentLexemeClusterRoleV4): boolean {\n\treturn INTENT_LEXEME_CLUSTERS_V4.some(\n\t\t(cluster) =>\n\t\t\tcluster.taskClass === taskClass &&\n\t\t\tcluster.role === role &&\n\t\t\tcluster.phrases.some((pattern) => pattern.test(text)),\n\t);\n}\n\n// ============================================================================\n// Whole-prompt / leading-clause patterns used by the v4 scorer.\n// ============================================================================\n\nconst LOCAL_EDIT_OBJECT_PATTERN =\n\t/\\b(spelling|grammar|capitalization|date\\s+format|author\\s+e-?mail|copyright\\s+year|headline|title|tooltip|placeholder|punctuation|comma|period|semicolon|closing\\s+html\\s+tag|closing\\s+tag|double\\s+space|whitespace|indentation|table\\s+alignment|typos?|one-?liner?|single\\s+line|sentence|stray|trailing)\\b|오타|맞춤법|띄어쓰기|문구|제목/i;\nconst LOCAL_EDIT_ACTION_OBJECT_PATTERN =\n\t/\\b(update|change|swap|remove|correct|fix|add|adjust|trim)\\s+(?:the\\s+|a\\s+|an\\s+)?(?:missing\\s+|stray\\s+|author\\s+|two\\s+)?(?:e-?mail|comma|period|semicolon|tag|word|words|headline|title|copyright|spelling|grammar)\\b/i;\nconst IMPLEMENTATION_OBJECT_PATTERN =\n\t/\\b(error\\s+handling|input\\s+validation|validation|auth(?:entication)?|retry|endpoint|migration|component|function|utility|helper|module|service|database|schema|column|middleware|rate\\s+limiter|cache|caching|oauth|jwt|webhook)\\b|에러\\s*처리|오류\\s*처리|입력\\s*검증|인증|재시도|엔드포인트|마이그레이션|컴포넌트|함수|유틸|헬퍼|모듈|서비스|데이터베이스|스키마|미들웨어|레이트\\s*리미터|캐시|웹훅|기능/i;\nconst HARD_DIAGNOSTIC_PATTERN =\n\t/\\b(stack\\s*trace|traceback|panic|segfault|crash(?:es|ing|ed)?|throws?|fails?|failing|flaky|hangs?|timeout|500|EADDRINUSE|silently\\s+(?:fails?|produces?|returns?|creates?)|(?:ci|build)\\s+(?:is\\s+)?red|runner\\s+(?:goes|went|is)\\s+red|comes?\\s+out\\s+wrong|totals?\\s+(?:are\\s+)?off|drift(?:ed|ing)?|mismatch(?:ed|es)?|mismatched|no\\s+longer\\s+matches|says\\s+success\\s+but|exits?\\s+0\\s+but|empty\\s+file|opens?\\s+blank|black\\s+image|zero[-\\s]?byte\\s+files?|headers\\s+only|rows?\\s+are\\s+missing|missing\\s+rows|stale\\s+(?:index|prices?|cache|results?)|search\\s+index\\s+(?:is\\s+)?stale|duplicate\\s+rows|NaN|never\\s+(?:send|sends|appear|appears|land|lands|writes?|written|updates?)|works\\s+locally\\s+and\\s+breaks|worked\\s+yesterday|settings\\s+revert|side\\s+effect\\s+never|stops?\\s+updating|data\\s+disappears)\\b|\\b(?:TypeError|ReferenceError|RangeError|Error):|크래시|스택\\s*트레이스|세그폴트|먹통|멈춰|타임아웃|숫자.{0,12}(?:이상|틀려|안\\s*맞)|합계.{0,12}(?:안\\s*맞|틀려|이상)|빈\\s*파일|(?:CI|러너).{0,12}빨개/i;\nconst BUG_OBJECT_PATTERN =\n\t/\\b(null\\s+pointer|null\\s+deref|race(?:\\s+condition)?|heap(?:\\s+overflow)?|use-after-free|memory\\s+leak|leaks?|deadlock|data\\s+corruption|stale\\s+data|off-by-one|encoding\\s+bug|regression\\s+(?:was\\s+)?introduced|exceptions?|assertion\\s+error|bugs?)\\b|버그|예외|메모리\\s*누수|데드락|무한\\s*루프/i;\nconst NON_DIAGNOSTIC_DEBUG_CONTEXT_PATTERN = /\\berror\\s+(?:handling|messages?|budgets?)\\b|에러\\s*처리|오류\\s*처리/i;\nconst GENERIC_DIAGNOSTIC_PATTERN = /\\b(errors?|broken|wrong\\s+results|rolls?\\s+back|rollback)\\b/i;\nconst PLAN_BRIEF_PATTERN =\n\t/\\b(context\\s+and\\s+constraints|starting\\s+state|target\\s+state|cross-cutting|deliver:|deliverables?|roadmap|migration\\s+wave|top\\s+(?:ten\\s+)?risks|component\\s+diagram|data\\s+model|architecture|go\\/no-go|phased\\s+(?:delivery|rollout)|bounded\\s+contexts?|strangler\\s+fig|event-driven|quarter-by-quarter|milestone)\\b/i;\nconst OPERATIONAL_RUNBOOK_SIGNAL_PATTERNS: readonly { pattern: RegExp; critical: boolean }[] = [\n\t{ pattern: /\\b(?:commit|commits|committed|committing)\\b|커밋/i, critical: false },\n\t{ pattern: /\\b(?:push|pushed|pushing)\\b|푸시|푸쉬/i, critical: true },\n\t{\n\t\tpattern: /\\b(?:release|releases|tag|version\\s+bump|bump\\s+(?:the\\s+)?version)\\b|릴리즈|태그|버전/i,\n\t\tcritical: true,\n\t},\n\t{ pattern: /\\bnpm\\s+publish\\b|\\bpublish(?:ing|ed)?\\b|퍼블리시|배포/i, critical: true },\n\t{ pattern: /\\b(?:CHANGELOG\\.md|changelog|release\\s+notes?)\\b|채널로그|체인지로그/i, critical: false },\n\t{ pattern: /\\bREADME\\.md\\b|\\breadme\\b/i, critical: false },\n\t{ pattern: /\\bci\\s*\\/\\s*cd\\b|\\bgithub\\s+actions?\\b|\\bworkflow\\b|깃허브\\s*액션|깃헙\\s*액션/i, critical: false },\n];\nconst EXPLICIT_RELEASE_RUNBOOK_PATTERN =\n\t/\\bnpm\\s+publish\\b|\\brelease\\b[^\\n]{0,80}\\bv?\\d+\\.\\d+\\.\\d+\\b|릴리즈[^\\n]{0,80}\\d+\\.\\d+\\.\\d+/i;\nconst REVIEW_SCOPE_PATTERN =\n\t/\\b(pr|pull\\s+request|diff|codebase|approach|structure|module\\s+boundary|locking\\s+strategy|abstraction|assumptions?|parser\\s+change|rollback\\s+strategy|concurrent\\s+case|concurrency\\s+plan|hidden\\s+coupling|failure\\s+mode|test\\s+quality|implementation\\s+details|strategy|plan|spec|schema|design|api|coverage|security\\s+posture|licensing|risks?|edge\\s+cases|clarity|consistency|dependencies|third-party|ci\\s+pipeline|threat\\s+model|retry\\s+logic|error\\s+handling\\s+strategy|error\\s+messages?)\\b/i;\nconst REFACTOR_CUE_PATTERN =\n\t/\\b(refactor(?:ing|ed)?|extract|rename|deduplicate|consolidate|modularize|reorganize|simplify|clean\\s*up|tidy\\s+up|restructure|split\\s+module|move\\s+logic|untangle|merge\\s+duplicate)\\b|리팩토링(?!하지\\s*마|하지\\s*말)|리팩터링(?!하지\\s*마|하지\\s*말)|구조\\s*개선/i;\nconst ADD_KEYWORD_PATTERN = /\\badd\\b/i;\nconst LOW_RISK_EDIT_ACTION_PATTERN = /^(?:correct|update|swap|remove|reword|tweak|bump|trim)\\b/i;\n\n/** Short-range compound-clause conjunction boundary. Only bare/compound \"then\"/\"and also\"/\";\" — never bare \"and\". */\nconst COMPOUND_SPLIT_PATTERN = /\\band\\s+then\\b|\\bthen\\b|\\band\\s+also\\b|;/i;\n\n/**\n * Bounded negation-cue vocabulary (spec 008 plan.md \"bounded negation window\").\n * Deliberately excludes bare \"not\" (too broad; verified against the current\n * GOLD_SET that every listed cue here is either absent or occurs AFTER, not\n * before, a tracked keyword — see this lane's evidence file for the audit).\n */\nconst NEGATION_CUE_PATTERN =\n\t/\\b(?:don't|do\\s+not|doesn't|does\\s+not|didn't|did\\s+not|won't|will\\s+not|shouldn't|should\\s+not|wouldn't|would\\s+not|can't|cannot|isn't|is\\s+not|aren't|are\\s+not|never|avoid|skip|without|instead\\s+of|rather\\s+than|no\\s+need\\s+(?:to|for)|no\\s+longer\\s+need(?:\\s+(?:to|for))?)\\b/i;\nconst DIRECT_NOT_OBJECT_CUE_PATTERN_V4 = /\\b(?:not|no)\\s+(?:a\\s+|an\\s+|the\\s+)?$/i;\nconst DOUBLE_NEGATION_RESCUE_PATTERN_V4 =\n\t/\\b(?:don't|do\\s+not|doesn't|does\\s+not|didn't|did\\s+not|won't|will\\s+not|shouldn't|should\\s+not|wouldn't|would\\s+not|can't|cannot)\\s+(?:skip|avoid)\\s+(?:the\\s+|a\\s+|an\\s+)?$/i;\nconst DELIBERATIVE_QUESTION_RESCUE_PATTERN_V4 =\n\t/\\b(?:shouldn't|should\\s+not|wouldn't|would\\s+not|can't|cannot)\\s+(?:we|i|you)\\s*$/i;\nconst POSTPOSITIONED_NEGATION_CUE_PATTERN_V4 =\n\t/^\\s*(?:-\\s*free\\b|(?:is|are|was|were)?\\s*(?:not\\s+(?:needed|required|desired)|unnecessary|not\\s+necessary)|(?:[은는이가을를도만]\\s*)?(?:하지\\s*(?:마(?:라|세요|십시오)?|말(?:고|아|라)?|않(?:아|는|고|을|게)?)|말고|금지))/i;\n\n/** A closed set of clause-boundary characters; a negation cue never reaches across one of these into a prior clause. */\nconst CLAUSE_BOUNDARY_CHARS = [\".\", \"!\", \"?\", \";\", \",\"] as const;\n\nfunction hasCodeFence(text: string): boolean {\n\treturn text.includes(\"```\");\n}\n\n/** Explicit hunk headers or `diff --git` count alone; bare +/- only count together. */\nfunction hasDiffMarkers(text: string): boolean {\n\tif (/^@@[^\\n]*@@/m.test(text) || /^diff --git /m.test(text)) return true;\n\treturn /^\\+(?!\\+)/m.test(text) && /^-(?!-)/m.test(text);\n}\n\n/** First line (after stripping one polite prefix), capped at 180 chars. */\nfunction firstClause(prompt: string): string {\n\tconst firstLine =\n\t\tprompt.replace(/^(?:please|pls|can you|could you|would you|help me|i need you to)\\s+/i, \"\").split(\"\\n\")[0] ?? \"\";\n\treturn firstLine.slice(0, 180);\n}\n\n// --- Leading-clause intent tests. All are `^`-anchored against the LEADING\n// clause text only, so a negation cue at the start (\"don't refactor...\") can\n// never match one of these: none of the alternations include \"don't\"/\"never\"/\n// etc, so they are negation-immune by construction.\n\nfunction hasLeadingReviewIntent(text: string): boolean {\n\treturn clusterMatchesRoleV4(text, \"review\", \"leading-intent\");\n}\n\nfunction hasLeadingPlanIntent(text: string): boolean {\n\treturn clusterMatchesRoleV4(text, \"plan\", \"leading-intent\");\n}\n\nfunction hasLeadingRefactorIntent(text: string): boolean {\n\treturn clusterMatchesRoleV4(text, \"refactor\", \"leading-intent\");\n}\n\nfunction hasLeadingDebugAction(text: string): boolean {\n\treturn clusterMatchesRoleV4(text, \"debug\", \"leading-intent\");\n}\n\nfunction hasLeadingCodeGenIntent(text: string): boolean {\n\treturn clusterMatchesRoleV4(text, \"code-gen\", \"leading-intent\");\n}\n\nfunction hasLeadingSimpleEditIntent(text: string): boolean {\n\treturn clusterMatchesRoleV4(text, \"simple-edit\", \"leading-intent\");\n}\n\n// ============================================================================\n// Bounded negation gating\n// ============================================================================\n\ninterface UnnegatedMatchResultV4 {\n\t/** True when the pattern matched at least once and at least one occurrence was NOT preceded by a negation cue. */\n\treadonly matched: boolean;\n\t/** True when the pattern matched at least once but EVERY occurrence was negated (useful for suppressedFeatureIds). */\n\treadonly suppressed: boolean;\n}\n\nconst NO_MATCH_RESULT_V4: UnnegatedMatchResultV4 = { matched: false, suppressed: false };\n\nfunction matchOperationalRunbookV4(prompt: string, windowChars: number): UnnegatedMatchResultV4 {\n\tlet distinctSignals = 0;\n\tlet criticalSignals = 0;\n\tlet sawSuppressed = false;\n\tfor (const { pattern, critical } of OPERATIONAL_RUNBOOK_SIGNAL_PATTERNS) {\n\t\tconst result = matchUnnegated(prompt, pattern, windowChars);\n\t\tif (result.matched) {\n\t\t\tdistinctSignals += 1;\n\t\t\tif (critical) criticalSignals += 1;\n\t\t}\n\t\tif (result.suppressed) sawSuppressed = true;\n\t}\n\tconst explicitRelease = matchUnnegated(prompt, EXPLICIT_RELEASE_RUNBOOK_PATTERN, windowChars).matched;\n\tconst matched = explicitRelease || distinctSignals >= 3 || (criticalSignals >= 2 && distinctSignals >= 2);\n\treturn { matched, suppressed: !matched && sawSuppressed };\n}\n\n/**\n * Negation-aware whole-prompt pattern scan. Finds every occurrence of `pattern`\n * in `prompt`; for each, looks back up to\n * `windowChars` characters (never crossing a `.,;!?` boundary into a prior\n * clause) for a `NEGATION_CUE_PATTERN` hit, and looks forward inside the same\n * bounded window for Hangul-only post-positioned negation cues such as\n * \"하지 말고\". `matched` is true iff at least one occurrence survives\n * un-negated. Deterministic, single pass per pattern, no shared regex state (a\n * fresh global-flag RegExp is constructed per call).\n */\nfunction hasPrePositionedNegationCueV4(scoped: string): boolean {\n\tif (!NEGATION_CUE_PATTERN.test(scoped) && !DIRECT_NOT_OBJECT_CUE_PATTERN_V4.test(scoped)) return false;\n\treturn !DOUBLE_NEGATION_RESCUE_PATTERN_V4.test(scoped) && !DELIBERATIVE_QUESTION_RESCUE_PATTERN_V4.test(scoped);\n}\n\nfunction hasPostPositionedNegationCueV4(prompt: string, matchEnd: number, windowChars: number): boolean {\n\tconst scoped = prompt.slice(matchEnd, Math.min(prompt.length, matchEnd + windowChars));\n\treturn POSTPOSITIONED_NEGATION_CUE_PATTERN_V4.test(scoped);\n}\n\nfunction matchUnnegated(prompt: string, pattern: RegExp, windowChars: number): UnnegatedMatchResultV4 {\n\tconst flags = pattern.flags.includes(\"g\") ? pattern.flags : `${pattern.flags}g`;\n\tconst globalPattern = new RegExp(pattern.source, flags);\n\tlet sawMatch = false;\n\tlet sawUnnegated = false;\n\tlet match = globalPattern.exec(prompt);\n\twhile (match !== null) {\n\t\tsawMatch = true;\n\t\tconst matchStart = match.index;\n\t\tconst windowStart = Math.max(0, matchStart - windowChars);\n\t\tconst windowText = prompt.slice(windowStart, matchStart);\n\t\tlet boundary = -1;\n\t\tfor (const boundaryChar of CLAUSE_BOUNDARY_CHARS) {\n\t\t\tconst idx = windowText.lastIndexOf(boundaryChar);\n\t\t\tif (idx > boundary) boundary = idx;\n\t\t}\n\t\tconst scoped = boundary >= 0 ? windowText.slice(boundary + 1) : windowText;\n\t\tconst preNegated = hasPrePositionedNegationCueV4(scoped);\n\t\tconst postNegated =\n\t\t\t!preNegated && hasPostPositionedNegationCueV4(prompt, matchStart + match[0].length, windowChars);\n\t\tif (!preNegated && !postNegated) sawUnnegated = true;\n\t\tif (match[0].length === 0) globalPattern.lastIndex += 1;\n\t\tmatch = globalPattern.exec(prompt);\n\t}\n\treturn { matched: sawUnnegated, suppressed: sawMatch && !sawUnnegated };\n}\n\nfunction matchClusterUnnegatedV4(\n\tprompt: string,\n\ttaskClass: TaskClassV4,\n\trole: IntentLexemeClusterRoleV4,\n\twindowChars: number,\n): UnnegatedMatchResultV4 {\n\tlet sawSuppressed = false;\n\tfor (const cluster of INTENT_LEXEME_CLUSTERS_V4) {\n\t\tif (cluster.taskClass !== taskClass || cluster.role !== role) continue;\n\t\tfor (const pattern of cluster.phrases) {\n\t\t\tconst result = matchUnnegated(prompt, pattern, windowChars);\n\t\t\tif (result.matched) return { matched: true, suppressed: false };\n\t\t\tif (result.suppressed) sawSuppressed = true;\n\t\t}\n\t}\n\treturn { matched: false, suppressed: sawSuppressed };\n}\n\n// ============================================================================\n// Bounded compound-intent detection\n// ============================================================================\n\ninterface CompoundSecondClauseV4 {\n\treadonly text: string;\n\treadonly startIndex: number;\n}\n\n/**\n * Splits off a bounded second clause after a short-range conjunction\n * (\"then\"/\"and then\"/\"and also\"/\";\"), only when the split point is within the\n * first `COMPOUND_SPLIT_MAX_INDEX_V4` characters (short direct commands, not\n * long prose briefs — verified this never fires inside the GOLD_SET's\n * >=2400-char plan entries). Returns null when no qualifying split exists.\n */\nfunction splitCompoundClauseV4(prompt: string): CompoundSecondClauseV4 | null {\n\tconst match = COMPOUND_SPLIT_PATTERN.exec(prompt);\n\tif (match === null || match.index > COMPOUND_SPLIT_MAX_INDEX_V4) return null;\n\tconst startIndex = match.index + match[0].length;\n\tconst second = prompt.slice(startIndex).trim();\n\tif (second.length < COMPOUND_SECOND_CLAUSE_MIN_CHARS_V4) return null;\n\treturn { text: second, startIndex };\n}\n\nfunction hasFutureSelfClauseV4(clause: string): boolean {\n\treturn /^(?:(?:i|we)(?:'|’)ll\\b|(?:i|we)\\s+will\\b|(?:i|we)(?:'|’)m\\s+going\\s+to\\b|before\\s+(?:i|we)\\b|after\\s+(?:i|we)\\b)/i.test(\n\t\tclause,\n\t);\n}\n\nfunction agentDirectedSecondClauseV4(clause: string): string | null {\n\tif (hasFutureSelfClauseV4(clause)) return null;\n\treturn clause.replace(/^(?:please|pls|also|then|can\\s+you|could\\s+you|would\\s+you|help\\s+me(?:\\s+to)?)\\s+/i, \"\");\n}\n\n/**\n * Leading-intent test for a compound prompt's SECOND clause. Reuses the same\n * `^`-anchored leading-intent tests as the primary clause (so it is equally\n * negation-immune), without the primary clause's diagnosticEvidence/localEdit\n * gating (a short second-clause fragment carries no such context of its own).\n * First-person future/context clauses (\"I'll...\", \"we will...\", \"before I...\")\n * are user context, not an agent-directed second task.\n */\nfunction secondClauseLeadingIntentV4(clause: string): TaskClassV4 | null {\n\tconst agentDirectedClause = agentDirectedSecondClauseV4(clause);\n\tif (agentDirectedClause === null) return null;\n\tconst bounded = agentDirectedClause.slice(0, COMPOUND_SECOND_CLAUSE_SCAN_CHARS_V4);\n\treturn leadingIntentFromLeadingClauseV4(bounded);\n}\n\nfunction leadingIntentFromLeadingClauseV4(leading: string): TaskClassV4 | null {\n\tif (hasLeadingDebugAction(leading)) return \"debug\";\n\tif (hasLeadingReviewIntent(leading)) return \"review\";\n\tif (hasLeadingPlanIntent(leading)) return \"plan\";\n\tif (hasLeadingRefactorIntent(leading)) return \"refactor\";\n\tif (hasLeadingSimpleEditIntent(leading)) return \"simple-edit\";\n\tif (hasLeadingCodeGenIntent(leading)) return \"code-gen\";\n\treturn null;\n}\n\nfunction leadingIntentIsPostNegatedV4(leading: string, windowChars: number): boolean {\n\tconst leadingIntentMatch =\n\t\t/^(?:debug|investigate\\s+why|reproduce|trace\\b|fix\\s+this\\s+(?:traceback|panic|error)|review|audit(?!\\s+log\\b)|critique|inspect|assess|approve|double-?check|lgtm|plan|design|architect|decompose|write\\s+(?:a\\s+)?(?:technical\\s+)?(?:spec|roadmap|strategy|plan)|create\\s+(?:a\\s+)?(?:technical\\s+)?(?:spec|roadmap|strategy|plan)|refactor(?:ing|ed)?|extract|rename|deduplicate|consolidate|modularize|reorganize|simplify|restructure|untangle|clean\\s+up|split\\s+(?:the\\s+)?module|move\\s+logic|correct|update|swap|remove|adjust|fix|add|change|trim|reword|tweak|bump|implement|build|generate|scaffold|prototype)\\b/i.exec(\n\t\t\tleading,\n\t\t);\n\treturn (\n\t\tleadingIntentMatch !== null && hasPostPositionedNegationCueV4(leading, leadingIntentMatch[0].length, windowChars)\n\t);\n}\n\nfunction classifyShortKoreanZeroScoreTaskV4(prompt: string, windowChars: number): TaskClassV4 | null {\n\tconst generalizedEvidence = extractGeneralizedIntentEvidenceV4(prompt);\n\tfor (const taskClass of TASK_CLASSES_V4) {\n\t\tif (generalizedEvidence.skeletonMatch[taskClass]) return taskClass;\n\t}\n\tif (matchClusterUnnegatedV4(prompt, \"debug\", \"korean-morphology\", windowChars).matched) return \"debug\";\n\tif (matchClusterUnnegatedV4(prompt, \"simple-edit\", \"korean-morphology\", windowChars).matched) return \"simple-edit\";\n\tif (matchClusterUnnegatedV4(prompt, \"review\", \"korean-morphology\", windowChars).matched) return \"review\";\n\tif (matchClusterUnnegatedV4(prompt, \"plan\", \"korean-morphology\", windowChars).matched) return \"plan\";\n\tif (matchClusterUnnegatedV4(prompt, \"refactor\", \"korean-morphology\", windowChars).matched) return \"refactor\";\n\tif (matchClusterUnnegatedV4(prompt, \"code-gen\", \"korean-morphology\", windowChars).matched) return \"code-gen\";\n\tfor (const taskClass of TASK_CLASSES_V4) {\n\t\tif (generalizedEvidence.clusterMatch[taskClass]) return taskClass;\n\t}\n\treturn null;\n}\n\n// ============================================================================\n// Feature extraction\n// ============================================================================\n\ninterface ContextualFeaturesV4 {\n\treadonly firstClause: string;\n\treadonly codeFence: boolean;\n\treadonly diffHunk: boolean;\n\treadonly localEdit: boolean;\n\treadonly diagnosticEvidence: boolean;\n\treadonly reviewScope: boolean;\n\treadonly planBrief: boolean;\n\treadonly operationalRunbook: boolean;\n\treadonly refactorCue: boolean;\n\treadonly implementationObject: boolean;\n\treadonly codeGenArtifactRequest: boolean;\n\treadonly evaluativeReviewObject: boolean;\n\treadonly leadingIntent: TaskClassV4 | null;\n\treadonly secondClauseIntent: TaskClassV4 | null;\n\treadonly compoundIntent: boolean;\n\treadonly keywordFamilyMatch: Readonly<Record<TaskClassV4, boolean>>;\n\treadonly normalizedIntentClusterMatch: Readonly<Record<TaskClassV4, boolean>>;\n\treadonly intentSkeletonMatch: Readonly<Record<TaskClassV4, boolean>>;\n\treadonly generalizedEvidenceCount: Readonly<Record<TaskClassV4, number>>;\n\treadonly addKeywordMatch: boolean;\n}\n\ninterface LeadingIntentInputV4 {\n\treadonly firstClause: string;\n\treadonly localEdit: boolean;\n\treadonly diagnosticEvidence: boolean;\n}\n\n/** Decision order for the leading intent classifier. */\nfunction leadingIntentV4(input: LeadingIntentInputV4): TaskClassV4 | null {\n\tif (\n\t\tinput.diagnosticEvidence &&\n\t\t(hasLeadingDebugAction(input.firstClause) || hasLeadingSimpleEditIntent(input.firstClause))\n\t)\n\t\treturn \"debug\";\n\tif (hasLeadingReviewIntent(input.firstClause)) return \"review\";\n\tif (hasLeadingPlanIntent(input.firstClause)) return \"plan\";\n\tif (hasLeadingRefactorIntent(input.firstClause)) return \"refactor\";\n\tif (input.localEdit && hasLeadingSimpleEditIntent(input.firstClause)) return \"simple-edit\";\n\tif (!input.localEdit && hasLeadingCodeGenIntent(input.firstClause)) return \"code-gen\";\n\treturn null;\n}\n\n/** Diagnostic-evidence decision structure, parameterized over precomputed negation-aware matches. */\nfunction hasDiagnosticEvidenceV4(\n\tleading: string,\n\thardDiagnosticMatched: boolean,\n\tbugObjectMatched: boolean,\n\tgenericDiagnosticMatched: boolean,\n\tnonDiagnosticContext: boolean,\n): boolean {\n\tif (hasLeadingDebugAction(leading) || hardDiagnosticMatched) return true;\n\tconst hasContextualDiagnostic = bugObjectMatched || (genericDiagnosticMatched && !nonDiagnosticContext);\n\tif (!hasContextualDiagnostic) return false;\n\treturn !hasLeadingReviewIntent(leading) && !hasLeadingPlanIntent(leading) && !hasLeadingRefactorIntent(leading);\n}\n\ntype GeneralizedIntentEvidenceV4 = ReturnType<typeof extractGeneralizedIntentEvidenceV4>;\n\nfunction isDeferredClassV4(evidence: GeneralizedIntentEvidenceV4, taskClass: TaskClassV4): boolean {\n\treturn evidence.deferredClasses.includes(taskClass);\n}\n\nfunction suppressGeneralizedClassesV4(\n\tevidence: GeneralizedIntentEvidenceV4,\n\tsuppressedClasses: readonly TaskClassV4[],\n): GeneralizedIntentEvidenceV4 {\n\tif (suppressedClasses.length === 0) return evidence;\n\tconst clusterMatch = { ...evidence.clusterMatch };\n\tconst skeletonMatch = { ...evidence.skeletonMatch };\n\tfor (const taskClass of suppressedClasses) {\n\t\tclusterMatch[taskClass] = false;\n\t\tskeletonMatch[taskClass] = false;\n\t}\n\treturn { ...evidence, clusterMatch, skeletonMatch };\n}\n\nfunction countGeneralizedEvidenceV4(evidence: GeneralizedIntentEvidenceV4): Record<TaskClassV4, number> {\n\tconst counts = emptyScoresV4();\n\tfor (const taskClass of TASK_CLASSES_V4) {\n\t\tcounts[taskClass] = (evidence.clusterMatch[taskClass] ? 1 : 0) + (evidence.skeletonMatch[taskClass] ? 1 : 0);\n\t}\n\treturn counts;\n}\n\n/**\n * Extracts every v4 feature from one prompt, negation-gating every\n * whole-prompt scan exactly once and recording a `negation:<channel>` id in\n * `suppressed` whenever a match existed but every occurrence was negated.\n */\nfunction extractFeaturesV4(prompt: string, weights: RouterWeightsV4, suppressed: string[]): ContextualFeaturesV4 {\n\tconst leading = firstClause(prompt);\n\tconst window = weights.negationWindowChars;\n\tconst secondClause = splitCompoundClauseV4(prompt);\n\tconst agentSignalPrompt =\n\t\tsecondClause !== null && hasFutureSelfClauseV4(secondClause.text)\n\t\t\t? prompt.slice(0, secondClause.startIndex).trim()\n\t\t\t: prompt;\n\tconst generalizedEvidence = extractGeneralizedIntentEvidenceV4(agentSignalPrompt);\n\tconst shortKoreanPrompt = prompt.length < TRIVIAL_MAX_CHARS_V4 && /[가-힣]/.test(prompt);\n\tconst shortKoreanDebugFallbackOnly =\n\t\tshortKoreanPrompt &&\n\t\t(generalizedEvidence.clusterMatch.debug || generalizedEvidence.skeletonMatch.debug) &&\n\t\t!generalizedEvidence.clusterMatch.refactor &&\n\t\t!generalizedEvidence.skeletonMatch.refactor &&\n\t\t!generalizedEvidence.clusterMatch.review &&\n\t\t!generalizedEvidence.skeletonMatch.review;\n\tconst scoredGeneralizedEvidence = suppressGeneralizedClassesV4(\n\t\tgeneralizedEvidence,\n\t\tshortKoreanDebugFallbackOnly\n\t\t\t? [...generalizedEvidence.deferredClasses, \"debug\"]\n\t\t\t: generalizedEvidence.deferredClasses,\n\t);\n\tfor (const taskClass of generalizedEvidence.deferredClasses) suppressed.push(`deferral:${taskClass}`);\n\n\tconst implementationObjectResult = matchUnnegated(agentSignalPrompt, IMPLEMENTATION_OBJECT_PATTERN, window);\n\tif (implementationObjectResult.suppressed) suppressed.push(\"negation:implementation-object\");\n\tconst implementationObject = !shortKoreanDebugFallbackOnly && implementationObjectResult.matched;\n\n\tconst codeGenArtifactResult = matchClusterUnnegatedV4(agentSignalPrompt, \"code-gen\", \"object-shape\", window);\n\tif (codeGenArtifactResult.suppressed) suppressed.push(\"negation:code-gen-artifact-request\");\n\tconst codeGenArtifactRequest = codeGenArtifactResult.matched;\n\n\tconst lowRiskEditAction = LOW_RISK_EDIT_ACTION_PATTERN.test(leading);\n\tconst localEditObjectResult = matchUnnegated(agentSignalPrompt, LOCAL_EDIT_OBJECT_PATTERN, window);\n\tif (localEditObjectResult.suppressed) suppressed.push(\"negation:local-edit-object\");\n\tconst localEditActionResult = matchUnnegated(agentSignalPrompt, LOCAL_EDIT_ACTION_OBJECT_PATTERN, window);\n\tif (localEditActionResult.suppressed) suppressed.push(\"negation:local-edit-action-object\");\n\tconst localEdit =\n\t\t!implementationObject &&\n\t\t!scoredGeneralizedEvidence.clusterMatch.review &&\n\t\t!scoredGeneralizedEvidence.skeletonMatch.review &&\n\t\t(lowRiskEditAction || localEditObjectResult.matched || localEditActionResult.matched);\n\n\tconst hardDiagnosticResult = matchUnnegated(agentSignalPrompt, HARD_DIAGNOSTIC_PATTERN, window);\n\tif (hardDiagnosticResult.suppressed) suppressed.push(\"negation:hard-diagnostic\");\n\tconst bugObjectResult = matchUnnegated(agentSignalPrompt, BUG_OBJECT_PATTERN, window);\n\tif (bugObjectResult.suppressed) suppressed.push(\"negation:bug-object\");\n\tconst genericDiagnosticResult = matchUnnegated(agentSignalPrompt, GENERIC_DIAGNOSTIC_PATTERN, window);\n\tconst nonDiagnosticContext = NON_DIAGNOSTIC_DEBUG_CONTEXT_PATTERN.test(agentSignalPrompt);\n\tif (genericDiagnosticResult.suppressed && !nonDiagnosticContext) suppressed.push(\"negation:generic-diagnostic\");\n\tconst diagnosticEvidence =\n\t\t!isDeferredClassV4(generalizedEvidence, \"debug\") &&\n\t\thasDiagnosticEvidenceV4(\n\t\t\tleading,\n\t\t\thardDiagnosticResult.matched,\n\t\t\tbugObjectResult.matched,\n\t\t\tgenericDiagnosticResult.matched,\n\t\t\tnonDiagnosticContext,\n\t\t);\n\n\tconst reviewScopeResult = matchUnnegated(agentSignalPrompt, REVIEW_SCOPE_PATTERN, window);\n\tif (reviewScopeResult.suppressed) suppressed.push(\"negation:review-scope\");\n\tconst reviewScope =\n\t\t!isDeferredClassV4(generalizedEvidence, \"review\") && hasLeadingReviewIntent(leading) && reviewScopeResult.matched;\n\n\tconst planBriefResult = matchUnnegated(agentSignalPrompt, PLAN_BRIEF_PATTERN, window);\n\tif (planBriefResult.suppressed) suppressed.push(\"negation:plan-brief\");\n\tconst planBrief =\n\t\t!isDeferredClassV4(generalizedEvidence, \"plan\") &&\n\t\thasLeadingPlanIntent(leading) &&\n\t\t(planBriefResult.matched || prompt.length >= LONG_BRIEF_MIN_CHARS_V4);\n\n\tconst operationalRunbookResult = matchOperationalRunbookV4(agentSignalPrompt, window);\n\tif (operationalRunbookResult.suppressed) suppressed.push(\"negation:operational-runbook\");\n\tconst operationalRunbook = operationalRunbookResult.matched;\n\n\tconst refactorCueResult = matchUnnegated(agentSignalPrompt, REFACTOR_CUE_PATTERN, window);\n\tif (refactorCueResult.suppressed) suppressed.push(\"negation:refactor-cue\");\n\tconst refactorCue = !isDeferredClassV4(generalizedEvidence, \"refactor\") && refactorCueResult.matched;\n\n\tconst rawPrimaryIntent = leadingIntentV4({ firstClause: leading, localEdit, diagnosticEvidence });\n\tconst primaryIntent =\n\t\trawPrimaryIntent !== null &&\n\t\t(leadingIntentIsPostNegatedV4(leading, window) || isDeferredClassV4(generalizedEvidence, rawPrimaryIntent))\n\t\t\t? null\n\t\t\t: rawPrimaryIntent;\n\n\tconst secondClauseIntent = secondClause === null ? null : secondClauseLeadingIntentV4(secondClause.text);\n\tconst compoundIntent = secondClauseIntent !== null && secondClauseIntent !== primaryIntent;\n\n\tconst debugKeywordResult = diagnosticEvidence\n\t\t? matchClusterUnnegatedV4(agentSignalPrompt, \"debug\", \"whole-prompt\", window)\n\t\t: NO_MATCH_RESULT_V4;\n\tif (debugKeywordResult.suppressed) suppressed.push(\"negation:keyword-debug\");\n\n\tconst reviewKeywordResult = isDeferredClassV4(generalizedEvidence, \"review\")\n\t\t? NO_MATCH_RESULT_V4\n\t\t: matchClusterUnnegatedV4(agentSignalPrompt, \"review\", \"whole-prompt\", window);\n\tif (reviewKeywordResult.suppressed) suppressed.push(\"negation:keyword-review\");\n\n\tconst planKeywordResult = isDeferredClassV4(generalizedEvidence, \"plan\")\n\t\t? NO_MATCH_RESULT_V4\n\t\t: matchClusterUnnegatedV4(agentSignalPrompt, \"plan\", \"whole-prompt\", window);\n\tif (planKeywordResult.suppressed) suppressed.push(\"negation:keyword-plan\");\n\n\tconst codeGenKeywordResult = matchClusterUnnegatedV4(agentSignalPrompt, \"code-gen\", \"whole-prompt\", window);\n\tif (codeGenKeywordResult.suppressed) suppressed.push(\"negation:keyword-code-gen\");\n\n\tconst evaluativeReviewResult =\n\t\t!isDeferredClassV4(generalizedEvidence, \"review\") && hasLeadingReviewIntent(leading)\n\t\t\t? matchClusterUnnegatedV4(agentSignalPrompt, \"review\", \"object-shape\", window)\n\t\t\t: NO_MATCH_RESULT_V4;\n\tif (evaluativeReviewResult.suppressed) suppressed.push(\"negation:evaluative-review-object\");\n\tconst evaluativeReviewObject = evaluativeReviewResult.matched;\n\n\tconst addKeywordResult = localEdit\n\t\t? NO_MATCH_RESULT_V4\n\t\t: matchUnnegated(agentSignalPrompt, ADD_KEYWORD_PATTERN, window);\n\tif (addKeywordResult.suppressed) suppressed.push(\"negation:add-keyword\");\n\n\treturn {\n\t\tfirstClause: leading,\n\t\tcodeFence: hasCodeFence(prompt),\n\t\tdiffHunk: hasDiffMarkers(prompt),\n\t\tlocalEdit,\n\t\tdiagnosticEvidence,\n\t\treviewScope,\n\t\tplanBrief,\n\t\toperationalRunbook,\n\t\trefactorCue,\n\t\timplementationObject,\n\t\tcodeGenArtifactRequest,\n\t\tevaluativeReviewObject,\n\t\tleadingIntent: primaryIntent,\n\t\tsecondClauseIntent,\n\t\tcompoundIntent,\n\t\tkeywordFamilyMatch: {\n\t\t\ttrivial: false,\n\t\t\tdebug: debugKeywordResult.matched,\n\t\t\trefactor: refactorCue,\n\t\t\treview: reviewKeywordResult.matched,\n\t\t\tplan: planKeywordResult.matched,\n\t\t\t\"simple-edit\": localEditObjectResult.matched,\n\t\t\t\"code-gen\": codeGenKeywordResult.matched,\n\t\t},\n\t\tnormalizedIntentClusterMatch: scoredGeneralizedEvidence.clusterMatch,\n\t\tintentSkeletonMatch: scoredGeneralizedEvidence.skeletonMatch,\n\t\tgeneralizedEvidenceCount: countGeneralizedEvidenceV4(scoredGeneralizedEvidence),\n\t\taddKeywordMatch: addKeywordResult.matched,\n\t};\n}\n\n// ============================================================================\n// Scoring\n// ============================================================================\n\nfunction emptyScoresV4(): Record<TaskClassV4, number> {\n\treturn {\n\t\ttrivial: 0,\n\t\t\"simple-edit\": 0,\n\t\t\"code-gen\": 0,\n\t\tdebug: 0,\n\t\trefactor: 0,\n\t\treview: 0,\n\t\tplan: 0,\n\t};\n}\n\nfunction computeScoresV4(features: ContextualFeaturesV4, weights: RouterWeightsV4): Record<TaskClassV4, number> {\n\tconst scores = emptyScoresV4();\n\tif (features.codeFence || features.diffHunk) scores[\"code-gen\"] += weights.codeFenceOrDiff;\n\tif (features.leadingIntent !== null) scores[features.leadingIntent] += weights.leadingIntent;\n\tif (features.secondClauseIntent !== null) scores[features.secondClauseIntent] += weights.secondClauseIntent;\n\tif (features.localEdit) scores[\"simple-edit\"] += weights.localEdit;\n\tif (features.diagnosticEvidence) scores.debug += weights.diagnosticEvidence;\n\tif (features.reviewScope) scores.review += weights.reviewScope;\n\tif (features.planBrief) scores.plan += weights.planBrief;\n\tif (features.operationalRunbook) scores.plan += weights.operationalRunbook;\n\tif (features.refactorCue) scores.refactor += weights.refactorCue;\n\tif (features.implementationObject) scores[\"code-gen\"] += weights.implementationObject;\n\tif (features.codeGenArtifactRequest) scores[\"code-gen\"] += weights.codeGenArtifactRequest;\n\tif (features.evaluativeReviewObject) scores.review += weights.evaluativeReviewObject;\n\n\tfor (const taskClass of TASK_CLASSES_V4) {\n\t\tif (features.keywordFamilyMatch[taskClass]) scores[taskClass] += weights.keywordFamily[taskClass];\n\t\tif (features.normalizedIntentClusterMatch[taskClass]) scores[taskClass] += weights.normalizedIntentCluster;\n\t\tif (features.intentSkeletonMatch[taskClass]) scores[taskClass] += weights.intentSkeleton;\n\t}\n\tif (features.addKeywordMatch) scores[\"code-gen\"] += weights.addKeyword;\n\n\treturn scores;\n}\n\n/**\n * Extension signals (history / context-pressure / judge vote). Every\n * coefficient is 0 under DEFAULT_WEIGHTS_V4 (inert until calibrated by a\n * future governance-backed lane); the mechanism exists so\n * `TaskClassifierInputV4`'s optional fields are meaningfully wired rather than\n * silently accepted-and-ignored.\n */\nfunction applyExtensionSignalsV4(\n\tscores: Record<TaskClassV4, number>,\n\tinput: TaskClassifierInputV4,\n\tweights: RouterWeightsV4,\n): void {\n\tconst priorClass = input.history !== undefined && input.history.length > 0 ? input.history[0] : null;\n\tif (priorClass !== null && weights.multiTurnPrior !== 0) scores[priorClass] += weights.multiTurnPrior;\n\n\tconst judgeVote = input.judgeVote ?? null;\n\tif (judgeVote !== null && weights.judgeVote !== 0) scores[judgeVote] += weights.judgeVote;\n\n\tconst pressureBucket = input.pressureBucket ?? 0;\n\tif (pressureBucket > 0 && weights.pressureBucket !== 0) {\n\t\tconst bump = pressureBucket * weights.pressureBucket;\n\t\tscores.debug += bump;\n\t\tscores.review += bump;\n\t\tscores.plan += bump;\n\t}\n}\n\nfunction clampConfidenceV4(value: number): number {\n\treturn Math.max(0, Math.min(1, value));\n}\n\nfunction confidenceEvidenceStrengthV4(topScore: number, margin: number, weights: RouterWeightsV4): number {\n\tif (topScore <= 0) return 0;\n\tconst separation = clampConfidenceV4(margin / topScore);\n\tconst evidenceFloor = weights.leadingIntent > 0 ? weights.leadingIntent : 1;\n\treturn Math.min(separation, clampConfidenceV4(topScore / evidenceFloor));\n}\n\n// ============================================================================\n// Public types\n// ============================================================================\n\n/**\n * Caller-supplied turn input. Identical shape to `TaskClassifierInputV3` /\n * `TaskClassifierInputV2` (prompt + optional lane/history/pressure/judge\n * slots), so existing callers can switch versions without reshaping data.\n */\nexport interface TaskClassifierInputV4 {\n\tprompt: string;\n\tlaneType?: ReasoningLaneTypeV4;\n\thistory?: readonly TaskClassV4[];\n\tpressureBucket?: number;\n\tjudgeVote?: TaskClassV4 | null;\n}\n\n/** Confidence band derived from score separation plus absolute evidence strength. */\nexport type ConfidenceBandV4 = \"low\" | \"medium\" | \"high\";\n\n/** Which branch of the zero-score fallback cascade decided `taskClass`; null when a real weighted signal decided it. */\nexport type FallbackReasonV4 =\n\t| \"code-fence-or-diff\"\n\t| \"trivial-length\"\n\t| \"ko-short-task-signal\"\n\t| \"long-prose\"\n\t| \"lane-fallback\"\n\t| \"default\";\n\n/**\n * The full, privacy-safe classification verdict (spec 008 Req 2). Every field\n * is a bounded enum, number, boolean, or a closed set of short diagnostic-id\n * strings (`suppressedFeatureIds`) — never raw prompt text, so this value is\n * safe to pass to evaluation, learning, and Adaptorch-advisory code.\n */\nexport interface ClassifierVerdictV4 {\n\t/** The decided task class (post zero-score-cascade if one fired). */\n\treadonly taskClass: TaskClassV4;\n\t/** Raw per-class score map from the weighted signals (pre-cascade; audit-only). */\n\treadonly scores: Readonly<Record<TaskClassV4, number>>;\n\t/** Second-highest-scoring class by the argmax, or null if every other class tied at the bottom. */\n\treadonly runnerUp: TaskClassV4 | null;\n\t/** `scores[argmaxClass] - scores[runnerUp]` (pre-cascade); 0 or negative-margin-free by construction (weights are non-negative). */\n\treadonly margin: number;\n\t/** Normalized confidence in [0, 1], combining `margin / topScore` with absolute evidence strength. */\n\treadonly confidence: number;\n\t/** Confidence bucketed by `weights.lowConfidenceThreshold` / `highConfidenceThreshold`. */\n\treadonly confidenceBand: ConfidenceBandV4;\n\t/** True when the argmax class won only via precedence-order tie-break over an exact score tie with the runner-up. */\n\treadonly tieBreak: boolean;\n\t/** Non-null exactly when the zero-score cascade (not a real weighted signal) decided `taskClass`. */\n\treadonly fallbackReason: FallbackReasonV4 | null;\n\t/** Audit trail of `negation:<channel>` ids for every whole-prompt signal that had a match but was fully negated. */\n\treadonly suppressedFeatureIds: readonly string[];\n\t/** True when a distinct second-clause leading intent was detected (bounded compound-intent detection). */\n\treadonly compoundIntent: boolean;\n\t/** The second clause's own leading intent, or null when no compound structure (or no distinct intent) was found. */\n\treadonly secondClauseIntent: TaskClassV4 | null;\n}\n\n// ============================================================================\n// Classifier\n// ============================================================================\n\n/**\n * Deterministic confidence-bearing classifier (spec 008 Req 2).\n *\n * Pipeline: extract negation/compound-aware contextual features -> score every\n * class from `weights` -> argmax with `TASK_CLASSES_V4` precedence tie-break,\n * tracking the runner-up -> compute margin/confidence/confidenceBand/tieBreak\n * from RAW score separation plus evidence strength -> if `topScore <= 0`, replace `taskClass` (and record\n * `fallbackReason`) via the zero-score cascade (fence/diff -> code-gen;\n * length < 40 -> trivial; length >= 2400 -> plan; lane fallback; default\n * code-gen) — the raw scores/margin/runnerUp/tieBreak fields still reflect the\n * pre-cascade computation, for audit purposes.\n *\n * Pure: same (input, weights) -> same ClassifierVerdictV4.\n */\nexport function classifyTaskV4(\n\tinput: TaskClassifierInputV4,\n\tweights: RouterWeightsV4 = DEFAULT_WEIGHTS_V4,\n): ClassifierVerdictV4 {\n\tconst prompt = input.prompt.trim();\n\tconst suppressed: string[] = [];\n\tconst features = extractFeaturesV4(prompt, weights, suppressed);\n\tconst scores = computeScoresV4(features, weights);\n\tapplyExtensionSignalsV4(scores, input, weights);\n\n\tlet top: TaskClassV4 = TASK_CLASSES_V4[0];\n\tfor (const c of TASK_CLASSES_V4) {\n\t\tif (scores[c] > scores[top]) top = c;\n\t}\n\tlet runnerUp: TaskClassV4 | null = null;\n\tfor (const c of TASK_CLASSES_V4) {\n\t\tif (c === top) continue;\n\t\tif (runnerUp === null || scores[c] > scores[runnerUp]) runnerUp = c;\n\t}\n\n\tconst topScore = scores[top];\n\tconst runnerUpScore = runnerUp !== null ? scores[runnerUp] : 0;\n\tconst margin = topScore - runnerUpScore;\n\tconst tieBreak = runnerUp !== null && topScore === runnerUpScore;\n\tconst rawConfidence = confidenceEvidenceStrengthV4(topScore, margin, weights);\n\tconst singleGeneralizedEvidence =\n\t\tfeatures.generalizedEvidenceCount[top] === 1 &&\n\t\ttopScore <= Math.max(weights.normalizedIntentCluster, weights.intentSkeleton);\n\tconst confidence = singleGeneralizedEvidence\n\t\t? Math.min(rawConfidence, Math.max(0, weights.highConfidenceThreshold - 0.01))\n\t\t: rawConfidence;\n\tconst confidenceBand: ConfidenceBandV4 =\n\t\tconfidence <= weights.lowConfidenceThreshold\n\t\t\t? \"low\"\n\t\t\t: confidence >= weights.highConfidenceThreshold\n\t\t\t\t? \"high\"\n\t\t\t\t: \"medium\";\n\n\tlet taskClass: TaskClassV4 = top;\n\tlet fallbackReason: FallbackReasonV4 | null = null;\n\tif (topScore <= 0) {\n\t\tif (features.codeFence || features.diffHunk) {\n\t\t\ttaskClass = \"code-gen\";\n\t\t\tfallbackReason = \"code-fence-or-diff\";\n\t\t} else if (prompt.length < TRIVIAL_MAX_CHARS_V4) {\n\t\t\tconst shortKoreanTaskClass = classifyShortKoreanZeroScoreTaskV4(prompt, weights.negationWindowChars);\n\t\t\tif (shortKoreanTaskClass !== null) {\n\t\t\t\ttaskClass = shortKoreanTaskClass;\n\t\t\t\tfallbackReason = \"ko-short-task-signal\";\n\t\t\t} else {\n\t\t\t\ttaskClass = \"trivial\";\n\t\t\t\tfallbackReason = \"trivial-length\";\n\t\t\t}\n\t\t} else if (prompt.length >= COMPLEX_PROSE_MIN_CHARS_V4) {\n\t\t\ttaskClass = \"plan\";\n\t\t\tfallbackReason = \"long-prose\";\n\t\t} else if (input.laneType !== undefined) {\n\t\t\ttaskClass = LANE_FALLBACK_CLASS_V4[input.laneType];\n\t\t\tfallbackReason = \"lane-fallback\";\n\t\t} else {\n\t\t\ttaskClass = \"code-gen\";\n\t\t\tfallbackReason = \"default\";\n\t\t}\n\t}\n\n\treturn {\n\t\ttaskClass,\n\t\tscores,\n\t\trunnerUp,\n\t\tmargin,\n\t\tconfidence,\n\t\tconfidenceBand,\n\t\ttieBreak,\n\t\tfallbackReason,\n\t\tsuppressedFeatureIds: suppressed,\n\t\tcompoundIntent: features.compoundIntent,\n\t\tsecondClauseIntent: features.secondClauseIntent,\n\t};\n}\n\n// ============================================================================\n// Resolver\n// ============================================================================\n\n/**\n * Static rule table: task class -> recommended ThinkingLevel. Re-exported\n * from the shared resolver core (byte-identical values; previously a\n * duplicated literal here — see reasoning-router-resolver.ts).\n */\nexport const TASK_CLASS_THINKING_LEVELS_V4 = TASK_CLASS_THINKING_LEVELS;\n\n/**\n * Bounded, strictly non-negative escalation applied when the verdict's own\n * confidence is low or a fallback (not a real signal) decided the class. This\n * is the ONLY place confidence touches the target index, and it can only add:\n * confidence can never lower effort (spec 008 Req 2 acceptance criterion).\n */\nconst LOW_CONFIDENCE_ESCALATION_STEPS_V4 = 1;\n\n/**\n * Thin auto-mode wrapper with no uncertainty adjustment: base rule table ->\n * lane step -> clamp to availableLevels. This is v4's \"confident\" path and\n * delegates to the shared resolver core with bias=0, hint=null, escalationSteps=0.\n */\nexport function resolveThinkingLevelV4ForAuto(\n\ttaskClass: TaskClassV4,\n\tavailableLevels: readonly ThinkingLevel[],\n\tlaneType: ReasoningLaneTypeV4 | undefined,\n): ThinkingLevel {\n\treturn resolveThinkingLevelCore(taskClass, availableLevels, laneType, 0, null, 0);\n}\n\n/**\n * Uncertainty-aware resolver (spec 008 Req 2 / plan.md step 5).\n *\n * Pipeline: base rule table for `verdict.taskClass` -> lane step -> bounded\n * bias [-2,+2] -> optional hint fusion (±2 bounded) -> bounded\n * non-negative confidence escalation (+1 ladder step exactly when\n * `verdict.confidenceBand === \"low\"` or `verdict.fallbackReason !== null`) ->\n * clamp to `availableLevels`.\n *\n * The confidence-escalation term is strictly `>= 0`: low confidence can only\n * hold the base+lane+bias+hint target or push it one step higher, never lower\n * it. This guarantees a low-confidence verdict never resolves BELOW what\n * `resolveThinkingLevelV4ForAuto` would give the same class (with bias=0,\n * hint=null) — text alone cannot talk the resolver down.\n */\nexport function resolveThinkingLevelV4WithUncertainty(\n\tverdict: ClassifierVerdictV4,\n\tavailableLevels: readonly ThinkingLevel[],\n\tlaneType: ReasoningLaneTypeV4 | undefined,\n\tbias = 0,\n\thint: { level: ThinkingLevel; confidence: number } | null = null,\n): ThinkingLevel {\n\tconst escalationSteps =\n\t\tverdict.confidenceBand === \"low\" || verdict.fallbackReason !== null ? LOW_CONFIDENCE_ESCALATION_STEPS_V4 : 0;\n\treturn resolveThinkingLevelCore(verdict.taskClass, availableLevels, laneType, bias, hint, escalationSteps);\n}\n"]}
1
+ {"version":3,"file":"reasoning-router-v4.d.ts","sourceRoot":"","sources":["../../src/core/reasoning-router-v4.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAEH,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,gBAAgB,CAAC;AACpD,OAAO,EAEN,KAAK,mBAAmB,EACxB,KAAK,eAAe,EAEpB,KAAK,WAAW,EAChB,MAAM,kCAAkC,CAAC;AAE1C,YAAY,EAAE,mBAAmB,EAAE,eAAe,EAAE,WAAW,EAAE,MAAM,kCAAkC,CAAC;AAC1G,OAAO,EAAE,kBAAkB,EAAE,eAAe,EAAE,MAAM,kCAAkC,CAAC;AAk8BvF;;;;GAIG;AACH,MAAM,WAAW,qBAAqB;IACrC,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,CAAC,EAAE,mBAAmB,CAAC;IAC/B,OAAO,CAAC,EAAE,SAAS,WAAW,EAAE,CAAC;IACjC,cAAc,CAAC,EAAE,MAAM,CAAC;IACxB,SAAS,CAAC,EAAE,WAAW,GAAG,IAAI,CAAC;CAC/B;AAED,qFAAqF;AACrF,MAAM,MAAM,gBAAgB,GAAG,KAAK,GAAG,QAAQ,GAAG,MAAM,CAAC;AAEzD,wHAAwH;AACxH,MAAM,MAAM,gBAAgB,GACzB,oBAAoB,GACpB,gBAAgB,GAChB,sBAAsB,GACtB,YAAY,GACZ,eAAe,GACf,SAAS,CAAC;AAEb;;;;;GAKG;AACH,MAAM,WAAW,mBAAmB;IACnC,qEAAqE;IACrE,QAAQ,CAAC,SAAS,EAAE,WAAW,CAAC;IAChC,mFAAmF;IACnF,QAAQ,CAAC,MAAM,EAAE,QAAQ,CAAC,MAAM,CAAC,WAAW,EAAE,MAAM,CAAC,CAAC,CAAC;IACvD,mGAAmG;IACnG,QAAQ,CAAC,QAAQ,EAAE,WAAW,GAAG,IAAI,CAAC;IACtC,oIAAoI;IACpI,QAAQ,CAAC,MAAM,EAAE,MAAM,CAAC;IACxB,sGAAsG;IACtG,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;IAC5B,2FAA2F;IAC3F,QAAQ,CAAC,cAAc,EAAE,gBAAgB,CAAC;IAC1C,qHAAqH;IACrH,QAAQ,CAAC,QAAQ,EAAE,OAAO,CAAC;IAC3B,qGAAqG;IACrG,QAAQ,CAAC,cAAc,EAAE,gBAAgB,GAAG,IAAI,CAAC;IACjD,oHAAoH;IACpH,QAAQ,CAAC,oBAAoB,EAAE,SAAS,MAAM,EAAE,CAAC;IACjD,0GAA0G;IAC1G,QAAQ,CAAC,cAAc,EAAE,OAAO,CAAC;IACjC,oHAAoH;IACpH,QAAQ,CAAC,kBAAkB,EAAE,WAAW,GAAG,IAAI,CAAC;CAChD;AAMD;;;;;;;;;;;;;GAaG;AACH,wBAAgB,cAAc,CAC7B,KAAK,EAAE,qBAAqB,EAC5B,OAAO,GAAE,eAAoC,GAC3C,mBAAmB,CAgFrB;AAMD;;;;GAIG;AACH,eAAO,MAAM,6BAA6B,8FAA6B,CAAC;AAUxE;;;;GAIG;AACH,wBAAgB,6BAA6B,CAC5C,SAAS,EAAE,WAAW,EACtB,eAAe,EAAE,SAAS,aAAa,EAAE,EACzC,QAAQ,EAAE,mBAAmB,GAAG,SAAS,GACvC,aAAa,CAEf;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAgB,qCAAqC,CACpD,OAAO,EAAE,mBAAmB,EAC5B,eAAe,EAAE,SAAS,aAAa,EAAE,EACzC,QAAQ,EAAE,mBAAmB,GAAG,SAAS,EACzC,IAAI,SAAI,EACR,IAAI,GAAE;IAAE,KAAK,EAAE,aAAa,CAAC;IAAC,UAAU,EAAE,MAAM,CAAA;CAAE,GAAG,IAAW,GAC9D,aAAa,CAIf","sourcesContent":["/**\n * Reasoning-router v4 — the sole `/think auto` classifier/resolver.\n *\n * Deterministic by construction: same (input, weights) -> same\n * ClassifierVerdictV4; same (verdict, availableLevels, laneType, bias, hint) ->\n * same ThinkingLevel. No clock, randomness, I/O, model calls, network access,\n * or state mutation anywhere in this file (ThinkingLevel is a type-only import\n * and is erased).\n *\n * `classifyTaskV4` returns a confidence-bearing verdict: task class, per-class\n * scores, runner-up, margin, confidence band, tie-break flag, fallback reason,\n * bounded-negation audit ids, and compound-intent metadata. None of this carries\n * prompt text — every field is a bounded enum, number, boolean, or a closed set\n * of short diagnostic-id strings.\n *\n * `resolveThinkingLevelV4WithUncertainty` starts from the canonical task-class\n * rule table, applies lane/bias/hint adjustments, and only adds non-negative\n * confidence escalation. A prompt cannot lower its own effort by asking the\n * router to \"think less\"; low confidence or fallback routing can only hold or\n * raise the resolved level.\n */\n\nimport type { ThinkingLevel } from \"omk-agent-core\";\nimport {\n\tDEFAULT_WEIGHTS_V4,\n\ttype ReasoningLaneTypeV4,\n\ttype RouterWeightsV4,\n\tTASK_CLASSES_V4,\n\ttype TaskClassV4,\n} from \"./reasoning-router-v4-weights.ts\";\n\nexport type { ReasoningLaneTypeV4, RouterWeightsV4, TaskClassV4 } from \"./reasoning-router-v4-weights.ts\";\nexport { DEFAULT_WEIGHTS_V4, TASK_CLASSES_V4 } from \"./reasoning-router-v4-weights.ts\";\n\nimport { resolveThinkingLevelCore, TASK_CLASS_THINKING_LEVELS } from \"./reasoning-router-resolver.ts\";\nimport { extractGeneralizedIntentEvidenceV4 } from \"./reasoning-router-v4-normalize.ts\";\n\n/** Prompts shorter than this (trimmed) fall back to trivial when no real signal scores > 0. */\nconst TRIVIAL_MAX_CHARS_V4 = 40;\n/** Prompts at/above this length with no real signal fall back to plan (long prose brief). */\nconst COMPLEX_PROSE_MIN_CHARS_V4 = 2400;\n/** Plan-brief prompts at/above this length count as a \"long brief\" even without PLAN_BRIEF_PATTERN. */\nconst LONG_BRIEF_MIN_CHARS_V4 = 512;\n/** Compound-clause split only applies within this many leading characters (short direct commands, not long prose). */\nconst COMPOUND_SPLIT_MAX_INDEX_V4 = 300;\n/** Minimum trimmed length of a candidate second clause to be worth checking for a leading intent. */\nconst COMPOUND_SECOND_CLAUSE_MIN_CHARS_V4 = 3;\n/** Bounded look-ahead cap when checking a second clause's leading intent (defense in depth; independent of regex cost). */\nconst COMPOUND_SECOND_CLAUSE_SCAN_CHARS_V4 = 200;\n\n/** Lane fallback class, used only when every scored class is <= 0 (zero-score cascade). */\nconst LANE_FALLBACK_CLASS_V4: Readonly<Record<ReasoningLaneTypeV4, TaskClassV4>> = {\n\tplanner: \"plan\",\n\tsecurity: \"review\",\n\texplorer: \"review\",\n\tcoder: \"code-gen\",\n\treviewer: \"review\",\n\ttester: \"code-gen\",\n};\n\ntype IntentLexemeClusterRoleV4 = \"leading-intent\" | \"whole-prompt\" | \"object-shape\" | \"korean-morphology\";\n\ntype IntentLexemeClusterV4 = {\n\treadonly id: string;\n\treadonly taskClass: TaskClassV4;\n\treadonly role: IntentLexemeClusterRoleV4;\n\treadonly surfaces: readonly string[];\n\treadonly phrases: readonly RegExp[];\n\treadonly negativeControlIds: readonly string[];\n};\n\nconst INTENT_LEXEME_CLUSTERS_V4: readonly IntentLexemeClusterV4[] = [\n\t{\n\t\tid: \"debug-leading-actions\",\n\t\ttaskClass: \"debug\",\n\t\trole: \"leading-intent\",\n\t\tsurfaces: [\"debug\", \"investigate why\", \"reproduce\", \"trace\", \"fix this traceback/panic/error\"],\n\t\tphrases: [/^(?:debug|investigate\\s+why|reproduce|trace\\b)|^fix\\s+this\\s+(?:traceback|panic|error)\\b/i],\n\t\tnegativeControlIds: [\"negated-debug-action\", \"generic-error-handling\"],\n\t},\n\t{\n\t\tid: \"review-leading-actions\",\n\t\ttaskClass: \"review\",\n\t\trole: \"leading-intent\",\n\t\tsurfaces: [\"review\", \"audit\", \"critique\", \"inspect\", \"assess\", \"approve\", \"double-check\", \"lgtm\"],\n\t\tphrases: [/^(?:review|audit(?!\\s+log\\b)|critique|inspect|assess|approve|double-?check|lgtm)\\b/i],\n\t\tnegativeControlIds: [\"audit-log-not-review\", \"review-negated-before-code-gen\"],\n\t},\n\t{\n\t\tid: \"review-leading-synonyms\",\n\t\ttaskClass: \"review\",\n\t\trole: \"leading-intent\",\n\t\tsurfaces: [\n\t\t\t\"give this PR a once-over\",\n\t\t\t\"look over\",\n\t\t\t\"sanity-check\",\n\t\t\t\"eyeball\",\n\t\t\t\"poke holes\",\n\t\t\t\"pressure-test\",\n\t\t\t\"tell me whether/if\",\n\t\t\t\"what could go wrong\",\n\t\t],\n\t\tphrases: [\n\t\t\t/^(?:give\\s+(?:this|the|my)?\\s*(?:pr|pull\\s+request|diff|code|change|branch)?\\s*(?:a\\s+)?once-?over|look\\s+over|sanity[-\\s]?check|eyeball|poke\\s+holes(?:\\s+in)?|pressure[-\\s]?test|sign\\s+off|tell\\s+me\\s+(?:if|whether)|tell\\s+me\\s+what\\s+could\\s+go\\s+wrong|where\\s+would\\b[^.!?;\\n]{0,100}\\bfall\\s+apart|what\\s+am\\s+i\\s+missing|check\\s+whether|look\\s+for\\s+(?:edge\\s+cases|risks?|flaws?)|find\\s+(?:flaws|risks?|issues?)|validate\\s+(?:the\\s+)?reasoning|give\\s+me\\s+a\\s+thumbs[-\\s]?up\\/?down|read\\s+(?:this|the|my)\\b[^.!?;\\n]{0,80}\\bchallenge|(?:is|are|does|do|can|could|would|should)\\s+(?:this|these|my|the)\\b[^.!?;\\n]{0,100}\\b(?:right\\s+way|handle|cover|safe|sane|clean|overkill|prove|leak|ship|concurrent|hidden\\s+coupling)|(?:at\\s+architecture\\s+level|architecture[-\\s]?level)\\b[^.!?;\\n]{0,80}\\bhidden\\s+coupling)\\b/i,\n\t\t],\n\t\tnegativeControlIds: [\"sanity-check-setup-script\", \"future-self-review-context\", \"check-whether-implementation\"],\n\t},\n\t{\n\t\tid: \"plan-leading-actions\",\n\t\ttaskClass: \"plan\",\n\t\trole: \"leading-intent\",\n\t\tsurfaces: [\"plan\", \"design\", \"architect\", \"decompose\", \"map out\", \"draw up\", \"think through\"],\n\t\tphrases: [\n\t\t\t/^(?:let(?:'|’)s\\s+)?(?:plan|design|architect|decompose|map\\s+out|draw\\s+up|think\\s+through)\\b/i,\n\t\t\t/^(?:write|create)\\s+(?:a\\s+)?(?:technical\\s+)?(?:spec|roadmap|strategy|plan)\\b/i,\n\t\t],\n\t\tnegativeControlIds: [\"design-token-edit\", \"draw-up-implementation-only\"],\n\t},\n\t{\n\t\tid: \"refactor-leading-actions\",\n\t\ttaskClass: \"refactor\",\n\t\trole: \"leading-intent\",\n\t\tsurfaces: [\"refactor\", \"extract\", \"rename\", \"deduplicate\", \"simplify\", \"clean up\", \"untangle\"],\n\t\tphrases: [\n\t\t\t/^(?:refactor(?:ing|ed)?|extract|rename|deduplicate|consolidate|modularize|reorganize|simplify|restructure|untangle)\\b|^clean\\s+up\\b|^split\\s+(?:the\\s+)?module\\b|^move\\s+logic\\b/i,\n\t\t],\n\t\tnegativeControlIds: [\"negated-refactor\", \"future-self-refactor\"],\n\t},\n\t{\n\t\tid: \"simple-edit-leading-actions\",\n\t\ttaskClass: \"simple-edit\",\n\t\trole: \"leading-intent\",\n\t\tsurfaces: [\"correct\", \"update\", \"swap\", \"remove\", \"adjust\", \"fix\", \"add\", \"change\", \"trim\", \"reword\"],\n\t\tphrases: [/^(?:correct|update|swap|remove|adjust|fix|add|change|trim|reword|tweak|bump)\\b/i],\n\t\tnegativeControlIds: [\"add-feature-not-simple-edit\", \"fix-crash-not-simple-edit\"],\n\t},\n\t{\n\t\tid: \"code-gen-leading-actions\",\n\t\ttaskClass: \"code-gen\",\n\t\trole: \"leading-intent\",\n\t\tsurfaces: [\n\t\t\t\"implement\",\n\t\t\t\"write\",\n\t\t\t\"create\",\n\t\t\t\"build\",\n\t\t\t\"generate\",\n\t\t\t\"scaffold\",\n\t\t\t\"prototype\",\n\t\t\t\"add\",\n\t\t\t\"whip up\",\n\t\t\t\"cook me\",\n\t\t],\n\t\tphrases: [/^(?:implement|write|create|build|generate|scaffold|prototype|add|whip\\s+up|cook\\s+me)\\b/i],\n\t\tnegativeControlIds: [\"write-a-plan\", \"create-a-review\"],\n\t},\n\t{\n\t\tid: \"debug-whole-prompt\",\n\t\ttaskClass: \"debug\",\n\t\trole: \"whole-prompt\",\n\t\tsurfaces: [\"debug\", \"investigate why\", \"reproduce\", \"track down\", \"figure out why\", \"get to the bottom\"],\n\t\tphrases: [\n\t\t\t/^(?:debug|investigate\\s+why|reproduce|trace\\b)|\\bfix\\s+this\\s+(?:traceback|panic|error)\\b|\\btrack\\s+down\\b|\\bfigure\\s+out\\s+why\\b|\\bget\\s+to\\s+the\\s+bottom\\b|디버깅|디버그|재현|원인\\s*분석/i,\n\t\t],\n\t\tnegativeControlIds: [\"not-a-bug-report\", \"error-handling-feature\"],\n\t},\n\t{\n\t\tid: \"review-whole-prompt\",\n\t\ttaskClass: \"review\",\n\t\trole: \"whole-prompt\",\n\t\tsurfaces: [\n\t\t\t\"review\",\n\t\t\t\"critique\",\n\t\t\t\"assess\",\n\t\t\t\"inspect\",\n\t\t\t\"once-over\",\n\t\t\t\"look over\",\n\t\t\t\"sanity-check\",\n\t\t\t\"eyeball\",\n\t\t\t\"poke holes\",\n\t\t\t\"pressure-test\",\n\t\t\t\"tell me whether correct\",\n\t\t],\n\t\tphrases: [\n\t\t\t/\\b(review|critique|assess|inspect|approve|lgtm|double-?check|sanity[-\\s]?check|once-?over|look\\s+over|eyeball|poke\\s+holes|pressure[-\\s]?test|sign\\s+off|thumbs[-\\s]?up\\/?down|what\\s+could\\s+go\\s+wrong|what\\s+am\\s+i\\s+missing|challenge\\s+the\\s+assumptions|hidden\\s+coupling|tell\\s+me\\s+(?:if|whether)[^.!?;\\n]{0,80}\\bcorrect|audit(?!\\s+log\\b))\\b|리뷰|검토|점검|괜찮은지\\s*봐\\s*줘|맞는지\\s*봐\\s*줘|처리되는지\\s*확인|허점.{0,10}찾|문제\\s*없는지/i,\n\t\t],\n\t\tnegativeControlIds: [\"review-free-implementation\", \"audit-log\"],\n\t},\n\t{\n\t\tid: \"plan-whole-prompt\",\n\t\ttaskClass: \"plan\",\n\t\trole: \"whole-prompt\",\n\t\tsurfaces: [\"plan\", \"design\", \"architecture\", \"roadmap\", \"map out\", \"draw up\", \"think through\"],\n\t\tphrases: [\n\t\t\t/\\b(plan|design|architect|architecture|roadmap|spec(?:ification)?|strategy|decompose|milestones?|map\\s+out|phases|draw\\s+up|think\\s+through|write\\s+a\\s+(?:technical\\s+)?spec|create\\s+a\\s+(?:roadmap|strategy|plan))\\b|설계|로드맵|아키텍처|계획.{0,6}세워|기획/i,\n\t\t],\n\t\tnegativeControlIds: [\"rather-than-design\", \"design-token-edit\"],\n\t},\n\t{\n\t\tid: \"code-gen-whole-prompt\",\n\t\ttaskClass: \"code-gen\",\n\t\trole: \"whole-prompt\",\n\t\tsurfaces: [\"implement\", \"write\", \"create\", \"build\", \"generate\", \"scaffold\", \"prototype\", \"whip up\", \"cook me\"],\n\t\tphrases: [\n\t\t\t/\\b(implement|write|create|build|generate|scaffold|prototype|whip\\s+up|cook\\s+me)\\b|구현|만들어\\s*줘|생성|작성|추가해/i,\n\t\t],\n\t\tnegativeControlIds: [\"write-plan\", \"create-roadmap\"],\n\t},\n\t{\n\t\tid: \"code-gen-artifact-request\",\n\t\ttaskClass: \"code-gen\",\n\t\trole: \"object-shape\",\n\t\tsurfaces: [\"I need a script\", \"we need a utility\", \"I need an endpoint\", \"we need a test\"],\n\t\tphrases: [\n\t\t\t/\\b(?:i|we)\\s+need\\s+(?:a|an|the|some)?\\s*(?:small\\s+|quick\\s+|new\\s+)?(?:script|utility|helper|component|endpoint|migration|test|tool|function|service)\\b(?!\\s+(?:plan|strategy|review|audit|roadmap|spec))/i,\n\t\t],\n\t\tnegativeControlIds: [\"need-a-plan\", \"need-a-review\", \"need-help-debugging\"],\n\t},\n\t{\n\t\tid: \"evaluative-review-object\",\n\t\ttaskClass: \"review\",\n\t\trole: \"object-shape\",\n\t\tsurfaces: [\"holes\", \"looks off\", \"correct\", \"safe\", \"risky\", \"issues\", \"regressions\"],\n\t\tphrases: [/\\b(holes?|looks?\\s+off|correct|safe|risky|issues?|regressions?)\\b/i],\n\t\tnegativeControlIds: [\"fix-holes\", \"implement-issue-fix\"],\n\t},\n\t{\n\t\tid: \"korean-debug-morphology\",\n\t\ttaskClass: \"debug\",\n\t\trole: \"korean-morphology\",\n\t\tsurfaces: [\n\t\t\t\"오류\",\n\t\t\t\"에러\",\n\t\t\t\"실패\",\n\t\t\t\"원인 찾아줘\",\n\t\t\t\"원인 파악\",\n\t\t\t\"숫자가 이상해\",\n\t\t\t\"합계가 안 맞아\",\n\t\t\t\"빈 파일\",\n\t\t\t\"CI가 빨개\",\n\t\t\t\"디버깅\",\n\t\t],\n\t\tphrases: [\n\t\t\t/오류|에러|실패|안\\s*돼|안\\s*됨|깨졌|깨짐|고장|디버깅|디버그|재현|원인\\s*(?:분석|파악|찾(?:아|기)?|찾아\\s*줘)?|숫자.{0,12}(?:이상|틀려|안\\s*맞)|합계.{0,12}(?:안\\s*맞|틀려|이상)|빈\\s*파일|(?:CI|러너).{0,12}빨개/i,\n\t\t],\n\t\tnegativeControlIds: [\"korean-error-handling-feature\", \"korean-greeting\"],\n\t},\n\t{\n\t\tid: \"korean-simple-edit-morphology\",\n\t\ttaskClass: \"simple-edit\",\n\t\trole: \"korean-morphology\",\n\t\tsurfaces: [\n\t\t\t\"오타\",\n\t\t\t\"맞춤법\",\n\t\t\t\"띄어쓰기\",\n\t\t\t\"문구\",\n\t\t\t\"제목\",\n\t\t\t\"바꿔\",\n\t\t\t\"바꾸\",\n\t\t\t\"수정\",\n\t\t\t\"변경\",\n\t\t\t\"고쳐\",\n\t\t\t\"고치\",\n\t\t\t\"삭제\",\n\t\t\t\"제거\",\n\t\t\t\"옮겨\",\n\t\t\t\"옮기\",\n\t\t],\n\t\tphrases: [\n\t\t\t// Typo/copy nouns plus modification verbs. Creation verbs live in\n\t\t\t// korean-code-gen-morphology; modifying an existing artifact is an edit,\n\t\t\t// not a generation. (016: moved from code-gen after hard-negative mining.)\n\t\t\t/오타|맞춤법|띄어쓰기|문구|제목|바꿔|바꾸|수정|변경|고쳐|고치|삭제|지워|지우|제거|옮겨|옮기|넣어/i,\n\t\t],\n\t\tnegativeControlIds: [\"korean-feature-edit\"],\n\t},\n\t{\n\t\tid: \"korean-review-morphology\",\n\t\ttaskClass: \"review\",\n\t\trole: \"korean-morphology\",\n\t\tsurfaces: [\"리뷰\", \"검토\", \"점검\", \"괜찮은지 봐줘\", \"맞는지 봐줘\", \"허점 찾아줘\"],\n\t\tphrases: [/리뷰|검토|점검|괜찮은지\\s*봐\\s*줘|맞는지\\s*봐\\s*줘|처리되는지\\s*확인|허점.{0,10}찾|문제\\s*없는지/i],\n\t\tnegativeControlIds: [\"korean-review-negated\"],\n\t},\n\t{\n\t\tid: \"korean-plan-morphology\",\n\t\ttaskClass: \"plan\",\n\t\trole: \"korean-morphology\",\n\t\tsurfaces: [\"설계\", \"로드맵\", \"아키텍처\", \"계획 세워\", \"계획 짜\", \"계획 수립\", \"계획 정리\"],\n\t\tphrases: [/설계|로드맵|아키텍처|계획.{0,8}(?:세워|짜|수립|정리)|기획/i],\n\t\tnegativeControlIds: [\"korean-plan-negated\"],\n\t},\n\t{\n\t\tid: \"korean-refactor-morphology\",\n\t\ttaskClass: \"refactor\",\n\t\trole: \"korean-morphology\",\n\t\tsurfaces: [\"리팩토링\", \"리팩터링\", \"구조 개선\"],\n\t\tphrases: [/리팩토링|리팩터링|구조\\s*개선/i],\n\t\tnegativeControlIds: [\"korean-refactor-negated\"],\n\t},\n\t{\n\t\tid: \"korean-code-gen-morphology\",\n\t\ttaskClass: \"code-gen\",\n\t\trole: \"korean-morphology\",\n\t\tsurfaces: [\"구현\", \"만들\", \"생성\", \"작성\", \"추가\", \"테스트\"],\n\t\tphrases: [/구현|만들|생성|작성|추가|테스트/i],\n\t\tnegativeControlIds: [\"korean-greeting\", \"korean-codegen-negated\"],\n\t},\n] as const;\n\nfunction clusterMatchesRoleV4(text: string, taskClass: TaskClassV4, role: IntentLexemeClusterRoleV4): boolean {\n\treturn INTENT_LEXEME_CLUSTERS_V4.some(\n\t\t(cluster) =>\n\t\t\tcluster.taskClass === taskClass &&\n\t\t\tcluster.role === role &&\n\t\t\tcluster.phrases.some((pattern) => pattern.test(text)),\n\t);\n}\n\n// ============================================================================\n// Whole-prompt / leading-clause patterns used by the v4 scorer.\n// ============================================================================\n\nconst LOCAL_EDIT_OBJECT_PATTERN =\n\t/\\b(spelling|grammar|capitalization|date\\s+format|author\\s+e-?mail|copyright\\s+year|headline|title|tooltip|placeholder|punctuation|comma|period|semicolon|closing\\s+html\\s+tag|closing\\s+tag|double\\s+space|whitespace|indentation|table\\s+alignment|typos?|one-?liner?|single\\s+line|sentence|stray|trailing)\\b|오타|맞춤법|띄어쓰기|문구|제목/i;\nconst LOCAL_EDIT_ACTION_OBJECT_PATTERN =\n\t/\\b(update|change|swap|remove|correct|fix|add|adjust|trim)\\s+(?:the\\s+|a\\s+|an\\s+)?(?:missing\\s+|stray\\s+|author\\s+|two\\s+)?(?:e-?mail|comma|period|semicolon|tag|word|words|headline|title|copyright|spelling|grammar)\\b/i;\nconst IMPLEMENTATION_OBJECT_PATTERN =\n\t/\\b(error\\s+handling|input\\s+validation|validation|auth(?:entication)?|retry|endpoint|migration|component|function|utility|helper|module|service|database|schema|column|middleware|rate\\s+limiter|cache|caching|oauth|jwt|webhook)\\b|에러\\s*처리|오류\\s*처리|입력\\s*검증|인증|재시도|엔드포인트|마이그레이션|컴포넌트|함수|유틸|헬퍼|모듈|서비스|데이터베이스|스키마|미들웨어|레이트\\s*리미터|캐시|웹훅|기능/i;\nconst HARD_DIAGNOSTIC_PATTERN =\n\t/\\b(stack\\s*trace|traceback|panic|segfault|crash(?:es|ing|ed)?|throws?|fails?|failing|flaky|hangs?|timeout|500|EADDRINUSE|silently\\s+(?:fails?|produces?|returns?|creates?)|(?:ci|build)\\s+(?:is\\s+)?red|runner\\s+(?:goes|went|is)\\s+red|comes?\\s+out\\s+wrong|totals?\\s+(?:are\\s+)?off|drift(?:ed|ing)?|mismatch(?:ed|es)?|mismatched|no\\s+longer\\s+matches|says\\s+success\\s+but|exits?\\s+0\\s+but|empty\\s+file|opens?\\s+blank|black\\s+image|zero[-\\s]?byte\\s+files?|headers\\s+only|rows?\\s+are\\s+missing|missing\\s+rows|stale\\s+(?:index|prices?|cache|results?)|search\\s+index\\s+(?:is\\s+)?stale|duplicate\\s+rows|NaN|never\\s+(?:send|sends|appear|appears|land|lands|writes?|written|updates?)|works\\s+locally\\s+and\\s+breaks|worked\\s+yesterday|settings\\s+revert|side\\s+effect\\s+never|stops?\\s+updating|data\\s+disappears)\\b|\\b(?:TypeError|ReferenceError|RangeError|Error):|크래시|스택\\s*트레이스|세그폴트|먹통|멈춰|타임아웃|숫자.{0,12}(?:이상|틀려|안\\s*맞)|합계.{0,12}(?:안\\s*맞|틀려|이상)|빈\\s*파일|(?:CI|러너).{0,12}빨개/i;\nconst BUG_OBJECT_PATTERN =\n\t/\\b(null\\s+pointer|null\\s+deref|race(?:\\s+condition)?|heap(?:\\s+overflow)?|use-after-free|memory\\s+leak|leaks?|deadlock|data\\s+corruption|stale\\s+data|off-by-one|encoding\\s+bug|regression\\s+(?:was\\s+)?introduced|exceptions?|assertion\\s+error|bugs?)\\b|버그|예외|메모리\\s*누수|데드락|무한\\s*루프/i;\nconst NON_DIAGNOSTIC_DEBUG_CONTEXT_PATTERN = /\\berror\\s+(?:handling|messages?|budgets?)\\b|에러\\s*처리|오류\\s*처리/i;\nconst GENERIC_DIAGNOSTIC_PATTERN = /\\b(errors?|broken|wrong\\s+results|rolls?\\s+back|rollback)\\b/i;\nconst PLAN_BRIEF_PATTERN =\n\t/\\b(context\\s+and\\s+constraints|starting\\s+state|target\\s+state|cross-cutting|deliver:|deliverables?|roadmap|migration\\s+wave|top\\s+(?:ten\\s+)?risks|component\\s+diagram|data\\s+model|architecture|go\\/no-go|phased\\s+(?:delivery|rollout)|bounded\\s+contexts?|strangler\\s+fig|event-driven|quarter-by-quarter|milestone)\\b/i;\nconst OPERATIONAL_RUNBOOK_SIGNAL_PATTERNS: readonly { pattern: RegExp; critical: boolean }[] = [\n\t{ pattern: /\\b(?:commit|commits|committed|committing)\\b|커밋/i, critical: false },\n\t{ pattern: /\\b(?:push|pushed|pushing)\\b|푸시|푸쉬/i, critical: true },\n\t{\n\t\tpattern: /\\b(?:release|releases|tag|version\\s+bump|bump\\s+(?:the\\s+)?version)\\b|릴리즈|태그|버전/i,\n\t\tcritical: true,\n\t},\n\t{ pattern: /\\bnpm\\s+publish\\b|\\bpublish(?:ing|ed)?\\b|퍼블리시|배포/i, critical: true },\n\t{ pattern: /\\b(?:CHANGELOG\\.md|changelog|release\\s+notes?)\\b|채널로그|체인지로그/i, critical: false },\n\t{ pattern: /\\bREADME\\.md\\b|\\breadme\\b/i, critical: false },\n\t{ pattern: /\\bci\\s*\\/\\s*cd\\b|\\bgithub\\s+actions?\\b|\\bworkflow\\b|깃허브\\s*액션|깃헙\\s*액션/i, critical: false },\n];\nconst EXPLICIT_RELEASE_RUNBOOK_PATTERN =\n\t/\\bnpm\\s+publish\\b|\\brelease\\b[^\\n]{0,80}\\bv?\\d+\\.\\d+\\.\\d+\\b|릴리즈[^\\n]{0,80}\\d+\\.\\d+\\.\\d+/i;\nconst REVIEW_SCOPE_PATTERN =\n\t/\\b(pr|pull\\s+request|diff|codebase|approach|structure|module\\s+boundary|locking\\s+strategy|abstraction|assumptions?|parser\\s+change|rollback\\s+strategy|concurrent\\s+case|concurrency\\s+plan|hidden\\s+coupling|failure\\s+mode|test\\s+quality|implementation\\s+details|strategy|plan|spec|schema|design|api|coverage|security\\s+posture|licensing|risks?|edge\\s+cases|clarity|consistency|dependencies|third-party|ci\\s+pipeline|threat\\s+model|retry\\s+logic|error\\s+handling\\s+strategy|error\\s+messages?)\\b/i;\nconst REFACTOR_CUE_PATTERN =\n\t/\\b(refactor(?:ing|ed)?|extract|rename|deduplicate|consolidate|modularize|reorganize|simplify|clean\\s*up|tidy\\s+up|restructure|split\\s+module|move\\s+logic|untangle|merge\\s+duplicate)\\b|리팩토링(?!하지\\s*마|하지\\s*말)|리팩터링(?!하지\\s*마|하지\\s*말)|구조\\s*개선/i;\nconst ADD_KEYWORD_PATTERN = /\\badd\\b/i;\nconst LOW_RISK_EDIT_ACTION_PATTERN = /^(?:correct|update|swap|remove|reword|tweak|bump|trim)\\b/i;\n\n/** Short-range compound-clause conjunction boundary. Only bare/compound \"then\"/\"and also\"/\";\" — never bare \"and\". */\nconst COMPOUND_SPLIT_PATTERN = /\\band\\s+then\\b|\\bthen\\b|\\band\\s+also\\b|;/i;\n\n/**\n * Bounded negation-cue vocabulary (spec 008 plan.md \"bounded negation window\").\n * Deliberately excludes bare \"not\" (too broad; verified against the current\n * GOLD_SET that every listed cue here is either absent or occurs AFTER, not\n * before, a tracked keyword — see this lane's evidence file for the audit).\n */\nconst NEGATION_CUE_PATTERN =\n\t/\\b(?:don't|do\\s+not|doesn't|does\\s+not|didn't|did\\s+not|won't|will\\s+not|shouldn't|should\\s+not|wouldn't|would\\s+not|can't|cannot|isn't|is\\s+not|aren't|are\\s+not|never|avoid|skip|without|instead\\s+of|rather\\s+than|no\\s+need\\s+(?:to|for)|no\\s+longer\\s+need(?:\\s+(?:to|for))?)\\b/i;\nconst DIRECT_NOT_OBJECT_CUE_PATTERN_V4 = /\\b(?:not|no)\\s+(?:a\\s+|an\\s+|the\\s+)?$/i;\nconst DOUBLE_NEGATION_RESCUE_PATTERN_V4 =\n\t/\\b(?:don't|do\\s+not|doesn't|does\\s+not|didn't|did\\s+not|won't|will\\s+not|shouldn't|should\\s+not|wouldn't|would\\s+not|can't|cannot)\\s+(?:skip|avoid)\\s+(?:the\\s+|a\\s+|an\\s+)?$/i;\nconst DELIBERATIVE_QUESTION_RESCUE_PATTERN_V4 =\n\t/\\b(?:shouldn't|should\\s+not|wouldn't|would\\s+not|can't|cannot)\\s+(?:we|i|you)\\s*$/i;\nconst POSTPOSITIONED_NEGATION_CUE_PATTERN_V4 =\n\t/^\\s*(?:-\\s*free\\b|(?:is|are|was|were)?\\s*(?:not\\s+(?:needed|required|desired)|unnecessary|not\\s+necessary)|(?:[은는이가을를도만]\\s*)?(?:하지\\s*(?:마(?:라|세요|십시오)?|말(?:고|아|라)?|않(?:아|는|고|을|게)?)|말고|금지))/i;\n\n/** A closed set of clause-boundary characters; a negation cue never reaches across one of these into a prior clause. */\nconst CLAUSE_BOUNDARY_CHARS = [\".\", \"!\", \"?\", \";\", \",\"] as const;\n\nfunction hasCodeFence(text: string): boolean {\n\treturn text.includes(\"```\");\n}\n\n/** Explicit hunk headers or `diff --git` count alone; bare +/- only count together. */\nfunction hasDiffMarkers(text: string): boolean {\n\tif (/^@@[^\\n]*@@/m.test(text) || /^diff --git /m.test(text)) return true;\n\treturn /^\\+(?!\\+)/m.test(text) && /^-(?!-)/m.test(text);\n}\n\n/** First line (after stripping one polite prefix), capped at 180 chars. */\n// Non-imperative preamble shapes stripped before ^-anchored intent tests\n// (goal 016/M3+M5): quoted-speech attribution (my teammate said \"...\" but\n// actually: ...) and labeled filler lines (background:/context:/note:).\n// Bounded to 3 repetitions so a wall of filler cannot loop the classifier.\nconst QUOTED_SPEECH_PREFIX_V4 = /^.{0,60}?\\bsaid\\s+\"[^\"]*\"[,.]?\\s*(?:but\\s+actually[,:]?\\s*)?/i;\nconst LABELED_FILLER_PREFIX_V4 = /^(?:background|context|note)\\s*:[^.!?\\n]*[.!?]?\\s*/i;\n\nfunction stripNonImperativePrefixV4(text: string): string {\n\tlet result = text;\n\tfor (let i = 0; i < 3; i++) {\n\t\tconst stripped = result.replace(QUOTED_SPEECH_PREFIX_V4, \"\").replace(LABELED_FILLER_PREFIX_V4, \"\");\n\t\tif (stripped === result) break;\n\t\tresult = stripped;\n\t}\n\treturn result;\n}\n\nfunction firstClause(prompt: string): string {\n\tconst firstLine =\n\t\tstripNonImperativePrefixV4(\n\t\t\tprompt.replace(/^(?:please|pls|can you|could you|would you|help me|i need you to)\\s+/i, \"\"),\n\t\t).split(\"\\n\")[0] ?? \"\";\n\treturn firstLine.slice(0, 180);\n}\n\n// --- Leading-clause intent tests. All are `^`-anchored against the LEADING\n// clause text only, so a negation cue at the start (\"don't refactor...\") can\n// never match one of these: none of the alternations include \"don't\"/\"never\"/\n// etc, so they are negation-immune by construction.\n\nfunction hasLeadingReviewIntent(text: string): boolean {\n\treturn clusterMatchesRoleV4(text, \"review\", \"leading-intent\");\n}\n\nfunction hasLeadingPlanIntent(text: string): boolean {\n\treturn clusterMatchesRoleV4(text, \"plan\", \"leading-intent\");\n}\n\nfunction hasLeadingRefactorIntent(text: string): boolean {\n\treturn clusterMatchesRoleV4(text, \"refactor\", \"leading-intent\");\n}\n\nfunction hasLeadingDebugAction(text: string): boolean {\n\treturn clusterMatchesRoleV4(text, \"debug\", \"leading-intent\");\n}\n\nfunction hasLeadingCodeGenIntent(text: string): boolean {\n\treturn clusterMatchesRoleV4(text, \"code-gen\", \"leading-intent\");\n}\n\nfunction hasLeadingSimpleEditIntent(text: string): boolean {\n\treturn clusterMatchesRoleV4(text, \"simple-edit\", \"leading-intent\");\n}\n\n// --- Quoted-speech masking (goal 016/M3) ---------------------------------\n// A double-quoted span that itself reads as an imperative (\"my teammate said\n// 'implement it'\") is reported speech, not the requester's intent: mask it so\n// it cannot fire whole-prompt evidence. Quoted nouns/objects are preserved\n// (\"fix the 'undefined' error\" keeps its debug signal).\n\nconst QUOTED_SPAN_PATTERN_V4 = /\"([^\"\\n]{1,80})\"/g;\n\nfunction quotedSpanIsImperativeV4(span: string): boolean {\n\tconst text = span.trim();\n\treturn (\n\t\thasLeadingDebugAction(text) ||\n\t\thasLeadingReviewIntent(text) ||\n\t\thasLeadingPlanIntent(text) ||\n\t\thasLeadingRefactorIntent(text) ||\n\t\thasLeadingCodeGenIntent(text) ||\n\t\thasLeadingSimpleEditIntent(text)\n\t);\n}\n\nfunction maskQuotedImperativesV4(text: string): string {\n\treturn text.replace(QUOTED_SPAN_PATTERN_V4, (whole: string, inner: string) =>\n\t\tquotedSpanIsImperativeV4(inner) ? \" \".repeat(whole.length) : whole,\n\t);\n}\n\n// ============================================================================\n// Bounded negation gating\n// ============================================================================\n\ninterface UnnegatedMatchResultV4 {\n\t/** True when the pattern matched at least once and at least one occurrence was NOT preceded by a negation cue. */\n\treadonly matched: boolean;\n\t/** True when the pattern matched at least once but EVERY occurrence was negated (useful for suppressedFeatureIds). */\n\treadonly suppressed: boolean;\n}\n\nconst NO_MATCH_RESULT_V4: UnnegatedMatchResultV4 = { matched: false, suppressed: false };\n\nfunction matchOperationalRunbookV4(prompt: string, windowChars: number): UnnegatedMatchResultV4 {\n\tlet distinctSignals = 0;\n\tlet criticalSignals = 0;\n\tlet sawSuppressed = false;\n\tfor (const { pattern, critical } of OPERATIONAL_RUNBOOK_SIGNAL_PATTERNS) {\n\t\tconst result = matchUnnegated(prompt, pattern, windowChars);\n\t\tif (result.matched) {\n\t\t\tdistinctSignals += 1;\n\t\t\tif (critical) criticalSignals += 1;\n\t\t}\n\t\tif (result.suppressed) sawSuppressed = true;\n\t}\n\tconst explicitRelease = matchUnnegated(prompt, EXPLICIT_RELEASE_RUNBOOK_PATTERN, windowChars).matched;\n\tconst matched = explicitRelease || distinctSignals >= 3 || (criticalSignals >= 2 && distinctSignals >= 2);\n\treturn { matched, suppressed: !matched && sawSuppressed };\n}\n\n/**\n * Negation-aware whole-prompt pattern scan. Finds every occurrence of `pattern`\n * in `prompt`; for each, looks back up to\n * `windowChars` characters (never crossing a `.,;!?` boundary into a prior\n * clause) for a `NEGATION_CUE_PATTERN` hit, and looks forward inside the same\n * bounded window for Hangul-only post-positioned negation cues such as\n * \"하지 말고\". `matched` is true iff at least one occurrence survives\n * un-negated. Deterministic, single pass per pattern, no shared regex state (a\n * fresh global-flag RegExp is constructed per call).\n */\nfunction hasPrePositionedNegationCueV4(scoped: string): boolean {\n\tif (!NEGATION_CUE_PATTERN.test(scoped) && !DIRECT_NOT_OBJECT_CUE_PATTERN_V4.test(scoped)) return false;\n\treturn !DOUBLE_NEGATION_RESCUE_PATTERN_V4.test(scoped) && !DELIBERATIVE_QUESTION_RESCUE_PATTERN_V4.test(scoped);\n}\n\nfunction hasPostPositionedNegationCueV4(prompt: string, matchEnd: number, windowChars: number): boolean {\n\tconst scoped = prompt.slice(matchEnd, Math.min(prompt.length, matchEnd + windowChars));\n\treturn POSTPOSITIONED_NEGATION_CUE_PATTERN_V4.test(scoped);\n}\n\nfunction matchUnnegated(prompt: string, pattern: RegExp, windowChars: number): UnnegatedMatchResultV4 {\n\tconst flags = pattern.flags.includes(\"g\") ? pattern.flags : `${pattern.flags}g`;\n\tconst globalPattern = new RegExp(pattern.source, flags);\n\tlet sawMatch = false;\n\tlet sawUnnegated = false;\n\tlet match = globalPattern.exec(prompt);\n\twhile (match !== null) {\n\t\tsawMatch = true;\n\t\tconst matchStart = match.index;\n\t\tconst windowStart = Math.max(0, matchStart - windowChars);\n\t\tconst windowText = prompt.slice(windowStart, matchStart);\n\t\tlet boundary = -1;\n\t\tfor (const boundaryChar of CLAUSE_BOUNDARY_CHARS) {\n\t\t\tconst idx = windowText.lastIndexOf(boundaryChar);\n\t\t\tif (idx > boundary) boundary = idx;\n\t\t}\n\t\tconst scoped = boundary >= 0 ? windowText.slice(boundary + 1) : windowText;\n\t\tconst preNegated = hasPrePositionedNegationCueV4(scoped);\n\t\tconst postNegated =\n\t\t\t!preNegated && hasPostPositionedNegationCueV4(prompt, matchStart + match[0].length, windowChars);\n\t\tif (!preNegated && !postNegated) sawUnnegated = true;\n\t\tif (match[0].length === 0) globalPattern.lastIndex += 1;\n\t\tmatch = globalPattern.exec(prompt);\n\t}\n\treturn { matched: sawUnnegated, suppressed: sawMatch && !sawUnnegated };\n}\n\nfunction matchClusterUnnegatedV4(\n\tprompt: string,\n\ttaskClass: TaskClassV4,\n\trole: IntentLexemeClusterRoleV4,\n\twindowChars: number,\n): UnnegatedMatchResultV4 {\n\tlet sawSuppressed = false;\n\tfor (const cluster of INTENT_LEXEME_CLUSTERS_V4) {\n\t\tif (cluster.taskClass !== taskClass || cluster.role !== role) continue;\n\t\tfor (const pattern of cluster.phrases) {\n\t\t\tconst result = matchUnnegated(prompt, pattern, windowChars);\n\t\t\tif (result.matched) return { matched: true, suppressed: false };\n\t\t\tif (result.suppressed) sawSuppressed = true;\n\t\t}\n\t}\n\treturn { matched: false, suppressed: sawSuppressed };\n}\n\n// ============================================================================\n// Bounded compound-intent detection\n// ============================================================================\n\ninterface CompoundSecondClauseV4 {\n\treadonly text: string;\n\treadonly startIndex: number;\n}\n\n/**\n * Splits off a bounded second clause after a short-range conjunction\n * (\"then\"/\"and then\"/\"and also\"/\";\"), only when the split point is within the\n * first `COMPOUND_SPLIT_MAX_INDEX_V4` characters (short direct commands, not\n * long prose briefs — verified this never fires inside the GOLD_SET's\n * >=2400-char plan entries). Returns null when no qualifying split exists.\n */\nfunction splitCompoundClauseV4(prompt: string): CompoundSecondClauseV4 | null {\n\tconst match = COMPOUND_SPLIT_PATTERN.exec(prompt);\n\tif (match === null || match.index > COMPOUND_SPLIT_MAX_INDEX_V4) return null;\n\tconst startIndex = match.index + match[0].length;\n\tconst second = prompt.slice(startIndex).trim();\n\tif (second.length < COMPOUND_SECOND_CLAUSE_MIN_CHARS_V4) return null;\n\treturn { text: second, startIndex };\n}\n\nfunction hasFutureSelfClauseV4(clause: string): boolean {\n\treturn /^(?:(?:i|we)(?:'|’)ll\\b|(?:i|we)\\s+will\\b|(?:i|we)(?:'|’)m\\s+going\\s+to\\b|before\\s+(?:i|we)\\b|after\\s+(?:i|we)\\b)/i.test(\n\t\tclause,\n\t);\n}\n\nfunction agentDirectedSecondClauseV4(clause: string): string | null {\n\tif (hasFutureSelfClauseV4(clause)) return null;\n\treturn clause.replace(/^(?:please|pls|also|then|can\\s+you|could\\s+you|would\\s+you|help\\s+me(?:\\s+to)?)\\s+/i, \"\");\n}\n\n/**\n * Leading-intent test for a compound prompt's SECOND clause. Reuses the same\n * `^`-anchored leading-intent tests as the primary clause (so it is equally\n * negation-immune), without the primary clause's diagnosticEvidence/localEdit\n * gating (a short second-clause fragment carries no such context of its own).\n * First-person future/context clauses (\"I'll...\", \"we will...\", \"before I...\")\n * are user context, not an agent-directed second task.\n */\nfunction secondClauseLeadingIntentV4(clause: string): TaskClassV4 | null {\n\tconst agentDirectedClause = agentDirectedSecondClauseV4(clause);\n\tif (agentDirectedClause === null) return null;\n\tconst bounded = agentDirectedClause.slice(0, COMPOUND_SECOND_CLAUSE_SCAN_CHARS_V4);\n\treturn leadingIntentFromLeadingClauseV4(bounded);\n}\n\nfunction leadingIntentFromLeadingClauseV4(leading: string): TaskClassV4 | null {\n\tif (hasLeadingDebugAction(leading)) return \"debug\";\n\tif (hasLeadingReviewIntent(leading)) return \"review\";\n\tif (hasLeadingPlanIntent(leading)) return \"plan\";\n\tif (hasLeadingRefactorIntent(leading)) return \"refactor\";\n\tif (hasLeadingSimpleEditIntent(leading)) return \"simple-edit\";\n\tif (hasLeadingCodeGenIntent(leading)) return \"code-gen\";\n\treturn null;\n}\n\nfunction leadingIntentIsPostNegatedV4(leading: string, windowChars: number): boolean {\n\tconst leadingIntentMatch =\n\t\t/^(?:debug|investigate\\s+why|reproduce|trace\\b|fix\\s+this\\s+(?:traceback|panic|error)|review|audit(?!\\s+log\\b)|critique|inspect|assess|approve|double-?check|lgtm|plan|design|architect|decompose|write\\s+(?:a\\s+)?(?:technical\\s+)?(?:spec|roadmap|strategy|plan)|create\\s+(?:a\\s+)?(?:technical\\s+)?(?:spec|roadmap|strategy|plan)|refactor(?:ing|ed)?|extract|rename|deduplicate|consolidate|modularize|reorganize|simplify|restructure|untangle|clean\\s+up|split\\s+(?:the\\s+)?module|move\\s+logic|correct|update|swap|remove|adjust|fix|add|change|trim|reword|tweak|bump|implement|build|generate|scaffold|prototype)\\b/i.exec(\n\t\t\tleading,\n\t\t);\n\treturn (\n\t\tleadingIntentMatch !== null && hasPostPositionedNegationCueV4(leading, leadingIntentMatch[0].length, windowChars)\n\t);\n}\n\nfunction classifyShortKoreanZeroScoreTaskV4(prompt: string, windowChars: number): TaskClassV4 | null {\n\tconst generalizedEvidence = extractGeneralizedIntentEvidenceV4(prompt);\n\tfor (const taskClass of TASK_CLASSES_V4) {\n\t\tif (generalizedEvidence.skeletonMatch[taskClass]) return taskClass;\n\t}\n\tif (matchClusterUnnegatedV4(prompt, \"debug\", \"korean-morphology\", windowChars).matched) return \"debug\";\n\tif (matchClusterUnnegatedV4(prompt, \"simple-edit\", \"korean-morphology\", windowChars).matched) return \"simple-edit\";\n\tif (matchClusterUnnegatedV4(prompt, \"review\", \"korean-morphology\", windowChars).matched) return \"review\";\n\tif (matchClusterUnnegatedV4(prompt, \"plan\", \"korean-morphology\", windowChars).matched) return \"plan\";\n\tif (matchClusterUnnegatedV4(prompt, \"refactor\", \"korean-morphology\", windowChars).matched) return \"refactor\";\n\tif (matchClusterUnnegatedV4(prompt, \"code-gen\", \"korean-morphology\", windowChars).matched) return \"code-gen\";\n\tfor (const taskClass of TASK_CLASSES_V4) {\n\t\tif (generalizedEvidence.clusterMatch[taskClass]) return taskClass;\n\t}\n\treturn null;\n}\n\n// ============================================================================\n// Feature extraction\n// ============================================================================\n\ninterface ContextualFeaturesV4 {\n\treadonly firstClause: string;\n\treadonly codeFence: boolean;\n\treadonly diffHunk: boolean;\n\treadonly localEdit: boolean;\n\treadonly diagnosticEvidence: boolean;\n\treadonly reviewScope: boolean;\n\treadonly planBrief: boolean;\n\treadonly operationalRunbook: boolean;\n\treadonly refactorCue: boolean;\n\treadonly implementationObject: boolean;\n\treadonly codeGenArtifactRequest: boolean;\n\treadonly evaluativeReviewObject: boolean;\n\treadonly leadingIntent: TaskClassV4 | null;\n\treadonly secondClauseIntent: TaskClassV4 | null;\n\treadonly compoundIntent: boolean;\n\treadonly keywordFamilyMatch: Readonly<Record<TaskClassV4, boolean>>;\n\treadonly normalizedIntentClusterMatch: Readonly<Record<TaskClassV4, boolean>>;\n\treadonly intentSkeletonMatch: Readonly<Record<TaskClassV4, boolean>>;\n\treadonly generalizedEvidenceCount: Readonly<Record<TaskClassV4, number>>;\n\treadonly addKeywordMatch: boolean;\n}\n\ninterface LeadingIntentInputV4 {\n\treadonly firstClause: string;\n\treadonly localEdit: boolean;\n\treadonly diagnosticEvidence: boolean;\n}\n\n/** Decision order for the leading intent classifier. */\nfunction leadingIntentV4(input: LeadingIntentInputV4): TaskClassV4 | null {\n\tif (\n\t\tinput.diagnosticEvidence &&\n\t\t(hasLeadingDebugAction(input.firstClause) || hasLeadingSimpleEditIntent(input.firstClause))\n\t)\n\t\treturn \"debug\";\n\tif (hasLeadingReviewIntent(input.firstClause)) return \"review\";\n\tif (hasLeadingPlanIntent(input.firstClause)) return \"plan\";\n\tif (hasLeadingRefactorIntent(input.firstClause)) return \"refactor\";\n\tif (input.localEdit && hasLeadingSimpleEditIntent(input.firstClause)) return \"simple-edit\";\n\tif (!input.localEdit && hasLeadingCodeGenIntent(input.firstClause)) return \"code-gen\";\n\treturn null;\n}\n\n/** Diagnostic-evidence decision structure, parameterized over precomputed negation-aware matches. */\nfunction hasDiagnosticEvidenceV4(\n\tleading: string,\n\thardDiagnosticMatched: boolean,\n\tbugObjectMatched: boolean,\n\tgenericDiagnosticMatched: boolean,\n\tnonDiagnosticContext: boolean,\n): boolean {\n\tif (hasLeadingDebugAction(leading) || hardDiagnosticMatched) return true;\n\tconst hasContextualDiagnostic = bugObjectMatched || (genericDiagnosticMatched && !nonDiagnosticContext);\n\tif (!hasContextualDiagnostic) return false;\n\treturn !hasLeadingReviewIntent(leading) && !hasLeadingPlanIntent(leading) && !hasLeadingRefactorIntent(leading);\n}\n\ntype GeneralizedIntentEvidenceV4 = ReturnType<typeof extractGeneralizedIntentEvidenceV4>;\n\nfunction isDeferredClassV4(evidence: GeneralizedIntentEvidenceV4, taskClass: TaskClassV4): boolean {\n\treturn evidence.deferredClasses.includes(taskClass);\n}\n\nfunction suppressGeneralizedClassesV4(\n\tevidence: GeneralizedIntentEvidenceV4,\n\tsuppressedClasses: readonly TaskClassV4[],\n): GeneralizedIntentEvidenceV4 {\n\tif (suppressedClasses.length === 0) return evidence;\n\tconst clusterMatch = { ...evidence.clusterMatch };\n\tconst skeletonMatch = { ...evidence.skeletonMatch };\n\tfor (const taskClass of suppressedClasses) {\n\t\tclusterMatch[taskClass] = false;\n\t\tskeletonMatch[taskClass] = false;\n\t}\n\treturn { ...evidence, clusterMatch, skeletonMatch };\n}\n\nfunction countGeneralizedEvidenceV4(evidence: GeneralizedIntentEvidenceV4): Record<TaskClassV4, number> {\n\tconst counts = emptyScoresV4();\n\tfor (const taskClass of TASK_CLASSES_V4) {\n\t\tcounts[taskClass] = (evidence.clusterMatch[taskClass] ? 1 : 0) + (evidence.skeletonMatch[taskClass] ? 1 : 0);\n\t}\n\treturn counts;\n}\n\n/**\n * Extracts every v4 feature from one prompt, negation-gating every\n * whole-prompt scan exactly once and recording a `negation:<channel>` id in\n * `suppressed` whenever a match existed but every occurrence was negated.\n */\nfunction extractFeaturesV4(prompt: string, weights: RouterWeightsV4, suppressed: string[]): ContextualFeaturesV4 {\n\tconst leading = firstClause(prompt);\n\tconst window = weights.negationWindowChars;\n\tconst secondClause = splitCompoundClauseV4(prompt);\n\tconst agentSignalPrompt = maskQuotedImperativesV4(\n\t\tsecondClause !== null && hasFutureSelfClauseV4(secondClause.text)\n\t\t\t? prompt.slice(0, secondClause.startIndex).trim()\n\t\t\t: prompt,\n\t);\n\tconst generalizedEvidence = extractGeneralizedIntentEvidenceV4(agentSignalPrompt);\n\tconst shortKoreanPrompt = prompt.length < TRIVIAL_MAX_CHARS_V4 && /[가-힣]/.test(prompt);\n\tconst shortKoreanDebugFallbackOnly =\n\t\tshortKoreanPrompt &&\n\t\t(generalizedEvidence.clusterMatch.debug || generalizedEvidence.skeletonMatch.debug) &&\n\t\t!generalizedEvidence.clusterMatch.refactor &&\n\t\t!generalizedEvidence.skeletonMatch.refactor &&\n\t\t!generalizedEvidence.clusterMatch.review &&\n\t\t!generalizedEvidence.skeletonMatch.review;\n\tconst scoredGeneralizedEvidence = suppressGeneralizedClassesV4(\n\t\tgeneralizedEvidence,\n\t\tshortKoreanDebugFallbackOnly\n\t\t\t? [...generalizedEvidence.deferredClasses, \"debug\"]\n\t\t\t: generalizedEvidence.deferredClasses,\n\t);\n\tfor (const taskClass of generalizedEvidence.deferredClasses) suppressed.push(`deferral:${taskClass}`);\n\n\tconst implementationObjectResult = matchUnnegated(agentSignalPrompt, IMPLEMENTATION_OBJECT_PATTERN, window);\n\tif (implementationObjectResult.suppressed) suppressed.push(\"negation:implementation-object\");\n\tconst implementationObject = !shortKoreanDebugFallbackOnly && implementationObjectResult.matched;\n\n\tconst codeGenArtifactResult = matchClusterUnnegatedV4(agentSignalPrompt, \"code-gen\", \"object-shape\", window);\n\tif (codeGenArtifactResult.suppressed) suppressed.push(\"negation:code-gen-artifact-request\");\n\tconst codeGenArtifactRequest = codeGenArtifactResult.matched;\n\n\tconst lowRiskEditAction = LOW_RISK_EDIT_ACTION_PATTERN.test(leading);\n\tconst localEditObjectResult = matchUnnegated(agentSignalPrompt, LOCAL_EDIT_OBJECT_PATTERN, window);\n\tif (localEditObjectResult.suppressed) suppressed.push(\"negation:local-edit-object\");\n\tconst localEditActionResult = matchUnnegated(agentSignalPrompt, LOCAL_EDIT_ACTION_OBJECT_PATTERN, window);\n\tif (localEditActionResult.suppressed) suppressed.push(\"negation:local-edit-action-object\");\n\tconst localEdit =\n\t\t!implementationObject &&\n\t\t!scoredGeneralizedEvidence.clusterMatch.review &&\n\t\t!scoredGeneralizedEvidence.skeletonMatch.review &&\n\t\t(lowRiskEditAction || localEditObjectResult.matched || localEditActionResult.matched);\n\n\tconst hardDiagnosticResult = matchUnnegated(agentSignalPrompt, HARD_DIAGNOSTIC_PATTERN, window);\n\tif (hardDiagnosticResult.suppressed) suppressed.push(\"negation:hard-diagnostic\");\n\tconst bugObjectResult = matchUnnegated(agentSignalPrompt, BUG_OBJECT_PATTERN, window);\n\tif (bugObjectResult.suppressed) suppressed.push(\"negation:bug-object\");\n\tconst genericDiagnosticResult = matchUnnegated(agentSignalPrompt, GENERIC_DIAGNOSTIC_PATTERN, window);\n\tconst nonDiagnosticContext = NON_DIAGNOSTIC_DEBUG_CONTEXT_PATTERN.test(agentSignalPrompt);\n\tif (genericDiagnosticResult.suppressed && !nonDiagnosticContext) suppressed.push(\"negation:generic-diagnostic\");\n\tconst diagnosticEvidence =\n\t\t!isDeferredClassV4(generalizedEvidence, \"debug\") &&\n\t\thasDiagnosticEvidenceV4(\n\t\t\tleading,\n\t\t\thardDiagnosticResult.matched,\n\t\t\tbugObjectResult.matched,\n\t\t\tgenericDiagnosticResult.matched,\n\t\t\tnonDiagnosticContext,\n\t\t);\n\n\tconst reviewScopeResult = matchUnnegated(agentSignalPrompt, REVIEW_SCOPE_PATTERN, window);\n\tif (reviewScopeResult.suppressed) suppressed.push(\"negation:review-scope\");\n\tconst reviewScope =\n\t\t!isDeferredClassV4(generalizedEvidence, \"review\") && hasLeadingReviewIntent(leading) && reviewScopeResult.matched;\n\n\tconst planBriefResult = matchUnnegated(agentSignalPrompt, PLAN_BRIEF_PATTERN, window);\n\tif (planBriefResult.suppressed) suppressed.push(\"negation:plan-brief\");\n\tconst planBrief =\n\t\t!isDeferredClassV4(generalizedEvidence, \"plan\") &&\n\t\thasLeadingPlanIntent(leading) &&\n\t\t(planBriefResult.matched || prompt.length >= LONG_BRIEF_MIN_CHARS_V4);\n\n\tconst operationalRunbookResult = matchOperationalRunbookV4(agentSignalPrompt, window);\n\tif (operationalRunbookResult.suppressed) suppressed.push(\"negation:operational-runbook\");\n\tconst operationalRunbook = operationalRunbookResult.matched;\n\n\tconst refactorCueResult = matchUnnegated(agentSignalPrompt, REFACTOR_CUE_PATTERN, window);\n\tif (refactorCueResult.suppressed) suppressed.push(\"negation:refactor-cue\");\n\tconst refactorCue = !isDeferredClassV4(generalizedEvidence, \"refactor\") && refactorCueResult.matched;\n\n\tconst rawPrimaryIntent = leadingIntentV4({ firstClause: leading, localEdit, diagnosticEvidence });\n\tconst primaryIntent =\n\t\trawPrimaryIntent !== null &&\n\t\t(leadingIntentIsPostNegatedV4(leading, window) || isDeferredClassV4(generalizedEvidence, rawPrimaryIntent))\n\t\t\t? null\n\t\t\t: rawPrimaryIntent;\n\n\tconst secondClauseIntent = secondClause === null ? null : secondClauseLeadingIntentV4(secondClause.text);\n\tconst compoundIntent = secondClauseIntent !== null && secondClauseIntent !== primaryIntent;\n\n\tconst debugKeywordResult = diagnosticEvidence\n\t\t? matchClusterUnnegatedV4(agentSignalPrompt, \"debug\", \"whole-prompt\", window)\n\t\t: NO_MATCH_RESULT_V4;\n\tif (debugKeywordResult.suppressed) suppressed.push(\"negation:keyword-debug\");\n\n\tconst reviewKeywordResult = isDeferredClassV4(generalizedEvidence, \"review\")\n\t\t? NO_MATCH_RESULT_V4\n\t\t: matchClusterUnnegatedV4(agentSignalPrompt, \"review\", \"whole-prompt\", window);\n\tif (reviewKeywordResult.suppressed) suppressed.push(\"negation:keyword-review\");\n\n\tconst planKeywordResult = isDeferredClassV4(generalizedEvidence, \"plan\")\n\t\t? NO_MATCH_RESULT_V4\n\t\t: matchClusterUnnegatedV4(agentSignalPrompt, \"plan\", \"whole-prompt\", window);\n\tif (planKeywordResult.suppressed) suppressed.push(\"negation:keyword-plan\");\n\n\tconst codeGenKeywordResult = matchClusterUnnegatedV4(agentSignalPrompt, \"code-gen\", \"whole-prompt\", window);\n\tif (codeGenKeywordResult.suppressed) suppressed.push(\"negation:keyword-code-gen\");\n\n\tconst evaluativeReviewResult =\n\t\t!isDeferredClassV4(generalizedEvidence, \"review\") && hasLeadingReviewIntent(leading)\n\t\t\t? matchClusterUnnegatedV4(agentSignalPrompt, \"review\", \"object-shape\", window)\n\t\t\t: NO_MATCH_RESULT_V4;\n\tif (evaluativeReviewResult.suppressed) suppressed.push(\"negation:evaluative-review-object\");\n\tconst evaluativeReviewObject = evaluativeReviewResult.matched;\n\n\tconst addKeywordResult = localEdit\n\t\t? NO_MATCH_RESULT_V4\n\t\t: matchUnnegated(agentSignalPrompt, ADD_KEYWORD_PATTERN, window);\n\tif (addKeywordResult.suppressed) suppressed.push(\"negation:add-keyword\");\n\n\treturn {\n\t\tfirstClause: leading,\n\t\tcodeFence: hasCodeFence(prompt),\n\t\tdiffHunk: hasDiffMarkers(prompt),\n\t\tlocalEdit,\n\t\tdiagnosticEvidence,\n\t\treviewScope,\n\t\tplanBrief,\n\t\toperationalRunbook,\n\t\trefactorCue,\n\t\timplementationObject,\n\t\tcodeGenArtifactRequest,\n\t\tevaluativeReviewObject,\n\t\tleadingIntent: primaryIntent,\n\t\tsecondClauseIntent,\n\t\tcompoundIntent,\n\t\tkeywordFamilyMatch: {\n\t\t\ttrivial: false,\n\t\t\tdebug: debugKeywordResult.matched,\n\t\t\trefactor: refactorCue,\n\t\t\treview: reviewKeywordResult.matched,\n\t\t\tplan: planKeywordResult.matched,\n\t\t\t\"simple-edit\": localEditObjectResult.matched,\n\t\t\t\"code-gen\": codeGenKeywordResult.matched,\n\t\t},\n\t\tnormalizedIntentClusterMatch: scoredGeneralizedEvidence.clusterMatch,\n\t\tintentSkeletonMatch: scoredGeneralizedEvidence.skeletonMatch,\n\t\tgeneralizedEvidenceCount: countGeneralizedEvidenceV4(scoredGeneralizedEvidence),\n\t\taddKeywordMatch: addKeywordResult.matched,\n\t};\n}\n\n// ============================================================================\n// Scoring\n// ============================================================================\n\nfunction emptyScoresV4(): Record<TaskClassV4, number> {\n\treturn {\n\t\ttrivial: 0,\n\t\t\"simple-edit\": 0,\n\t\t\"code-gen\": 0,\n\t\tdebug: 0,\n\t\trefactor: 0,\n\t\treview: 0,\n\t\tplan: 0,\n\t};\n}\n\nfunction computeScoresV4(features: ContextualFeaturesV4, weights: RouterWeightsV4): Record<TaskClassV4, number> {\n\tconst scores = emptyScoresV4();\n\tif (features.codeFence || features.diffHunk) scores[\"code-gen\"] += weights.codeFenceOrDiff;\n\tif (features.leadingIntent !== null) scores[features.leadingIntent] += weights.leadingIntent;\n\tif (features.secondClauseIntent !== null) scores[features.secondClauseIntent] += weights.secondClauseIntent;\n\tif (features.localEdit) scores[\"simple-edit\"] += weights.localEdit;\n\tif (features.diagnosticEvidence) scores.debug += weights.diagnosticEvidence;\n\tif (features.reviewScope) scores.review += weights.reviewScope;\n\tif (features.planBrief) scores.plan += weights.planBrief;\n\tif (features.operationalRunbook) scores.plan += weights.operationalRunbook;\n\tif (features.refactorCue) scores.refactor += weights.refactorCue;\n\tif (features.implementationObject) scores[\"code-gen\"] += weights.implementationObject;\n\tif (features.codeGenArtifactRequest) scores[\"code-gen\"] += weights.codeGenArtifactRequest;\n\tif (features.evaluativeReviewObject) scores.review += weights.evaluativeReviewObject;\n\n\tfor (const taskClass of TASK_CLASSES_V4) {\n\t\tif (features.keywordFamilyMatch[taskClass]) scores[taskClass] += weights.keywordFamily[taskClass];\n\t\tif (features.normalizedIntentClusterMatch[taskClass]) scores[taskClass] += weights.normalizedIntentCluster;\n\t\tif (features.intentSkeletonMatch[taskClass]) scores[taskClass] += weights.intentSkeleton;\n\t}\n\tif (features.addKeywordMatch) scores[\"code-gen\"] += weights.addKeyword;\n\n\treturn scores;\n}\n\n/**\n * Extension signals (history / context-pressure / judge vote). Every\n * coefficient is 0 under DEFAULT_WEIGHTS_V4 (inert until calibrated by a\n * future governance-backed lane); the mechanism exists so\n * `TaskClassifierInputV4`'s optional fields are meaningfully wired rather than\n * silently accepted-and-ignored.\n */\nfunction applyExtensionSignalsV4(\n\tscores: Record<TaskClassV4, number>,\n\tinput: TaskClassifierInputV4,\n\tweights: RouterWeightsV4,\n): void {\n\t// Gate (goal 016/L2): extension votes apply only when a real signal already\n\t// exists. On a zero-score state the fallback cascade (trivial-length,\n\t// ko-short, long-prose, lane) owns the verdict — a stray history/judge vote\n\t// must not hijack it (measured: any weight >= 1 flips gold-0001 \"hi\" to the\n\t// voted class otherwise). With the gate, inert weights can be nonzero safely.\n\tlet topScore = 0;\n\tfor (const c of TASK_CLASSES_V4) topScore = Math.max(topScore, scores[c]);\n\tif (topScore <= 0) return;\n\n\tconst priorClass = input.history !== undefined && input.history.length > 0 ? input.history[0] : null;\n\tif (priorClass !== null && weights.multiTurnPrior !== 0) scores[priorClass] += weights.multiTurnPrior;\n\n\tconst judgeVote = input.judgeVote ?? null;\n\tif (judgeVote !== null && weights.judgeVote !== 0) scores[judgeVote] += weights.judgeVote;\n\n\tconst pressureBucket = input.pressureBucket ?? 0;\n\tif (pressureBucket > 0 && weights.pressureBucket !== 0) {\n\t\tconst bump = pressureBucket * weights.pressureBucket;\n\t\tscores.debug += bump;\n\t\tscores.review += bump;\n\t\tscores.plan += bump;\n\t}\n}\n\nfunction clampConfidenceV4(value: number): number {\n\treturn Math.max(0, Math.min(1, value));\n}\n\nfunction confidenceEvidenceStrengthV4(topScore: number, margin: number, weights: RouterWeightsV4): number {\n\tif (topScore <= 0) return 0;\n\tconst separation = clampConfidenceV4(margin / topScore);\n\tconst evidenceFloor = weights.leadingIntent > 0 ? weights.leadingIntent : 1;\n\treturn Math.min(separation, clampConfidenceV4(topScore / evidenceFloor));\n}\n\n// ============================================================================\n// Public types\n// ============================================================================\n\n/**\n * Caller-supplied turn input. Identical shape to `TaskClassifierInputV3` /\n * `TaskClassifierInputV2` (prompt + optional lane/history/pressure/judge\n * slots), so existing callers can switch versions without reshaping data.\n */\nexport interface TaskClassifierInputV4 {\n\tprompt: string;\n\tlaneType?: ReasoningLaneTypeV4;\n\thistory?: readonly TaskClassV4[];\n\tpressureBucket?: number;\n\tjudgeVote?: TaskClassV4 | null;\n}\n\n/** Confidence band derived from score separation plus absolute evidence strength. */\nexport type ConfidenceBandV4 = \"low\" | \"medium\" | \"high\";\n\n/** Which branch of the zero-score fallback cascade decided `taskClass`; null when a real weighted signal decided it. */\nexport type FallbackReasonV4 =\n\t| \"code-fence-or-diff\"\n\t| \"trivial-length\"\n\t| \"ko-short-task-signal\"\n\t| \"long-prose\"\n\t| \"lane-fallback\"\n\t| \"default\";\n\n/**\n * The full, privacy-safe classification verdict (spec 008 Req 2). Every field\n * is a bounded enum, number, boolean, or a closed set of short diagnostic-id\n * strings (`suppressedFeatureIds`) — never raw prompt text, so this value is\n * safe to pass to evaluation, learning, and Adaptorch-advisory code.\n */\nexport interface ClassifierVerdictV4 {\n\t/** The decided task class (post zero-score-cascade if one fired). */\n\treadonly taskClass: TaskClassV4;\n\t/** Raw per-class score map from the weighted signals (pre-cascade; audit-only). */\n\treadonly scores: Readonly<Record<TaskClassV4, number>>;\n\t/** Second-highest-scoring class by the argmax, or null if every other class tied at the bottom. */\n\treadonly runnerUp: TaskClassV4 | null;\n\t/** `scores[argmaxClass] - scores[runnerUp]` (pre-cascade); 0 or negative-margin-free by construction (weights are non-negative). */\n\treadonly margin: number;\n\t/** Normalized confidence in [0, 1], combining `margin / topScore` with absolute evidence strength. */\n\treadonly confidence: number;\n\t/** Confidence bucketed by `weights.lowConfidenceThreshold` / `highConfidenceThreshold`. */\n\treadonly confidenceBand: ConfidenceBandV4;\n\t/** True when the argmax class won only via precedence-order tie-break over an exact score tie with the runner-up. */\n\treadonly tieBreak: boolean;\n\t/** Non-null exactly when the zero-score cascade (not a real weighted signal) decided `taskClass`. */\n\treadonly fallbackReason: FallbackReasonV4 | null;\n\t/** Audit trail of `negation:<channel>` ids for every whole-prompt signal that had a match but was fully negated. */\n\treadonly suppressedFeatureIds: readonly string[];\n\t/** True when a distinct second-clause leading intent was detected (bounded compound-intent detection). */\n\treadonly compoundIntent: boolean;\n\t/** The second clause's own leading intent, or null when no compound structure (or no distinct intent) was found. */\n\treadonly secondClauseIntent: TaskClassV4 | null;\n}\n\n// ============================================================================\n// Classifier\n// ============================================================================\n\n/**\n * Deterministic confidence-bearing classifier (spec 008 Req 2).\n *\n * Pipeline: extract negation/compound-aware contextual features -> score every\n * class from `weights` -> argmax with `TASK_CLASSES_V4` precedence tie-break,\n * tracking the runner-up -> compute margin/confidence/confidenceBand/tieBreak\n * from RAW score separation plus evidence strength -> if `topScore <= 0`, replace `taskClass` (and record\n * `fallbackReason`) via the zero-score cascade (fence/diff -> code-gen;\n * length < 40 -> trivial; length >= 2400 -> plan; lane fallback; default\n * code-gen) — the raw scores/margin/runnerUp/tieBreak fields still reflect the\n * pre-cascade computation, for audit purposes.\n *\n * Pure: same (input, weights) -> same ClassifierVerdictV4.\n */\nexport function classifyTaskV4(\n\tinput: TaskClassifierInputV4,\n\tweights: RouterWeightsV4 = DEFAULT_WEIGHTS_V4,\n): ClassifierVerdictV4 {\n\tconst prompt = input.prompt.trim();\n\tconst suppressed: string[] = [];\n\tconst features = extractFeaturesV4(prompt, weights, suppressed);\n\tconst scores = computeScoresV4(features, weights);\n\tapplyExtensionSignalsV4(scores, input, weights);\n\n\tlet top: TaskClassV4 = TASK_CLASSES_V4[0];\n\tfor (const c of TASK_CLASSES_V4) {\n\t\tif (scores[c] > scores[top]) top = c;\n\t}\n\tlet runnerUp: TaskClassV4 | null = null;\n\tfor (const c of TASK_CLASSES_V4) {\n\t\tif (c === top) continue;\n\t\tif (runnerUp === null || scores[c] > scores[runnerUp]) runnerUp = c;\n\t}\n\n\tconst topScore = scores[top];\n\tconst runnerUpScore = runnerUp !== null ? scores[runnerUp] : 0;\n\tconst margin = topScore - runnerUpScore;\n\tconst tieBreak = runnerUp !== null && topScore === runnerUpScore;\n\tconst rawConfidence = confidenceEvidenceStrengthV4(topScore, margin, weights);\n\tconst singleGeneralizedEvidence =\n\t\tfeatures.generalizedEvidenceCount[top] === 1 &&\n\t\ttopScore <= Math.max(weights.normalizedIntentCluster, weights.intentSkeleton);\n\tconst confidence = singleGeneralizedEvidence\n\t\t? Math.min(rawConfidence, Math.max(0, weights.highConfidenceThreshold - 0.01))\n\t\t: rawConfidence;\n\tconst confidenceBand: ConfidenceBandV4 =\n\t\tconfidence <= weights.lowConfidenceThreshold\n\t\t\t? \"low\"\n\t\t\t: confidence >= weights.highConfidenceThreshold\n\t\t\t\t? \"high\"\n\t\t\t\t: \"medium\";\n\n\tlet taskClass: TaskClassV4 = top;\n\tlet fallbackReason: FallbackReasonV4 | null = null;\n\tif (topScore <= 0) {\n\t\tif (features.codeFence || features.diffHunk) {\n\t\t\ttaskClass = \"code-gen\";\n\t\t\tfallbackReason = \"code-fence-or-diff\";\n\t\t} else {\n\t\t\t// Hangul zero-score prompts route by morphology at ANY length, not just\n\t\t\t// short ones: previously a >40-char Korean edit request fell through to\n\t\t\t// the code-gen default even when a morphology cluster matched. (016)\n\t\t\tconst koreanTaskClass = /[가-힣]/.test(prompt)\n\t\t\t\t? classifyShortKoreanZeroScoreTaskV4(prompt, weights.negationWindowChars)\n\t\t\t\t: null;\n\t\t\tif (koreanTaskClass !== null) {\n\t\t\t\ttaskClass = koreanTaskClass;\n\t\t\t\tfallbackReason = \"ko-short-task-signal\";\n\t\t\t} else if (prompt.length < TRIVIAL_MAX_CHARS_V4) {\n\t\t\t\ttaskClass = \"trivial\";\n\t\t\t\tfallbackReason = \"trivial-length\";\n\t\t\t} else if (prompt.length >= COMPLEX_PROSE_MIN_CHARS_V4) {\n\t\t\t\ttaskClass = \"plan\";\n\t\t\t\tfallbackReason = \"long-prose\";\n\t\t\t} else if (input.laneType !== undefined) {\n\t\t\t\ttaskClass = LANE_FALLBACK_CLASS_V4[input.laneType];\n\t\t\t\tfallbackReason = \"lane-fallback\";\n\t\t\t} else {\n\t\t\t\ttaskClass = \"code-gen\";\n\t\t\t\tfallbackReason = \"default\";\n\t\t\t}\n\t\t}\n\t}\n\n\treturn {\n\t\ttaskClass,\n\t\tscores,\n\t\trunnerUp,\n\t\tmargin,\n\t\tconfidence,\n\t\tconfidenceBand,\n\t\ttieBreak,\n\t\tfallbackReason,\n\t\tsuppressedFeatureIds: suppressed,\n\t\tcompoundIntent: features.compoundIntent,\n\t\tsecondClauseIntent: features.secondClauseIntent,\n\t};\n}\n\n// ============================================================================\n// Resolver\n// ============================================================================\n\n/**\n * Static rule table: task class -> recommended ThinkingLevel. Re-exported\n * from the shared resolver core (byte-identical values; previously a\n * duplicated literal here — see reasoning-router-resolver.ts).\n */\nexport const TASK_CLASS_THINKING_LEVELS_V4 = TASK_CLASS_THINKING_LEVELS;\n\n/**\n * Bounded, strictly non-negative escalation applied when the verdict's own\n * confidence is low or a fallback (not a real signal) decided the class. This\n * is the ONLY place confidence touches the target index, and it can only add:\n * confidence can never lower effort (spec 008 Req 2 acceptance criterion).\n */\nconst LOW_CONFIDENCE_ESCALATION_STEPS_V4 = 1;\n\n/**\n * Thin auto-mode wrapper with no uncertainty adjustment: base rule table ->\n * lane step -> clamp to availableLevels. This is v4's \"confident\" path and\n * delegates to the shared resolver core with bias=0, hint=null, escalationSteps=0.\n */\nexport function resolveThinkingLevelV4ForAuto(\n\ttaskClass: TaskClassV4,\n\tavailableLevels: readonly ThinkingLevel[],\n\tlaneType: ReasoningLaneTypeV4 | undefined,\n): ThinkingLevel {\n\treturn resolveThinkingLevelCore(taskClass, availableLevels, laneType, 0, null, 0);\n}\n\n/**\n * Uncertainty-aware resolver (spec 008 Req 2 / plan.md step 5).\n *\n * Pipeline: base rule table for `verdict.taskClass` -> lane step -> bounded\n * bias [-2,+2] -> optional hint fusion (±2 bounded) -> bounded\n * non-negative confidence escalation (+1 ladder step exactly when\n * `verdict.confidenceBand === \"low\"` or `verdict.fallbackReason !== null`) ->\n * clamp to `availableLevels`.\n *\n * The confidence-escalation term is strictly `>= 0`: low confidence can only\n * hold the base+lane+bias+hint target or push it one step higher, never lower\n * it. This guarantees a low-confidence verdict never resolves BELOW what\n * `resolveThinkingLevelV4ForAuto` would give the same class (with bias=0,\n * hint=null) — text alone cannot talk the resolver down.\n */\nexport function resolveThinkingLevelV4WithUncertainty(\n\tverdict: ClassifierVerdictV4,\n\tavailableLevels: readonly ThinkingLevel[],\n\tlaneType: ReasoningLaneTypeV4 | undefined,\n\tbias = 0,\n\thint: { level: ThinkingLevel; confidence: number } | null = null,\n): ThinkingLevel {\n\tconst escalationSteps =\n\t\tverdict.confidenceBand === \"low\" || verdict.fallbackReason !== null ? LOW_CONFIDENCE_ESCALATION_STEPS_V4 : 0;\n\treturn resolveThinkingLevelCore(verdict.taskClass, availableLevels, laneType, bias, hint, escalationSteps);\n}\n"]}