muonroi-cli 1.7.1 → 1.8.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 (328) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +122 -122
  3. package/dist/packages/agent-harness-core/src/event-filter.js +1 -0
  4. package/dist/packages/agent-harness-core/src/event-redact.js +8 -0
  5. package/dist/packages/agent-harness-core/src/mcp-server.d.ts +16 -0
  6. package/dist/packages/agent-harness-core/src/mcp-server.js +88 -10
  7. package/dist/packages/agent-harness-core/src/protocol.d.ts +15 -0
  8. package/dist/packages/agent-harness-core/src/selector.js +2 -2
  9. package/dist/packages/agent-harness-core/src/transports/sidechannel.js +1 -1
  10. package/dist/packages/agent-harness-opentui/src/agent-mode.js +1 -1
  11. package/dist/packages/agent-harness-opentui/src/install.js +1 -1
  12. package/dist/src/__test-stubs__/vitest-setup.js +4 -0
  13. package/dist/src/agent-harness/__tests__/mock-model.spec.js +54 -0
  14. package/dist/src/agent-harness/mock-model.d.ts +1 -1
  15. package/dist/src/agent-harness/mock-model.js +32 -3
  16. package/dist/src/agent-harness/test-spawn.js +3 -1
  17. package/dist/src/chat/chat-keychain.js +9 -1
  18. package/dist/src/cli/cost-forensics.js +12 -12
  19. package/dist/src/cli/keys.d.ts +2 -2
  20. package/dist/src/cli/keys.js +107 -28
  21. package/dist/src/cli/usage-report.js +2 -2
  22. package/dist/src/council/__tests__/abort-threading.test.d.ts +1 -0
  23. package/dist/src/council/__tests__/abort-threading.test.js +193 -0
  24. package/dist/src/council/__tests__/clarification-prompt.test.js +21 -1
  25. package/dist/src/council/__tests__/council-turn-length-emit.test.d.ts +1 -0
  26. package/dist/src/council/__tests__/council-turn-length-emit.test.js +130 -0
  27. package/dist/src/council/__tests__/debate-round-budget.test.d.ts +1 -0
  28. package/dist/src/council/__tests__/debate-round-budget.test.js +45 -0
  29. package/dist/src/council/__tests__/evaluator-metrics.test.js +3 -0
  30. package/dist/src/council/__tests__/post-debate-recommendation.test.d.ts +1 -0
  31. package/dist/src/council/__tests__/post-debate-recommendation.test.js +58 -0
  32. package/dist/src/council/__tests__/research-tools.test.js +4 -0
  33. package/dist/src/council/__tests__/round-tools.test.js +6 -0
  34. package/dist/src/council/clarifier-question-cap.test.d.ts +12 -0
  35. package/dist/src/council/clarifier-question-cap.test.js +73 -0
  36. package/dist/src/council/clarifier.d.ts +29 -1
  37. package/dist/src/council/clarifier.js +64 -1
  38. package/dist/src/council/debate-planner.d.ts +2 -1
  39. package/dist/src/council/debate-planner.js +5 -3
  40. package/dist/src/council/debate.d.ts +9 -0
  41. package/dist/src/council/debate.js +88 -8
  42. package/dist/src/council/index.d.ts +46 -1
  43. package/dist/src/council/index.js +125 -19
  44. package/dist/src/council/llm.js +41 -13
  45. package/dist/src/council/prompts.js +56 -21
  46. package/dist/src/council/types.d.ts +14 -1
  47. package/dist/src/ee/__tests__/export-transcripts.test.js +1 -1
  48. package/dist/src/ee/bridge.d.ts +3 -1
  49. package/dist/src/ee/bridge.js +9 -3
  50. package/dist/src/ee/bridge.test.js +1 -2
  51. package/dist/src/ee/client.js +15 -2
  52. package/dist/src/ee/recall-ledger.d.ts +20 -0
  53. package/dist/src/ee/recall-ledger.js +40 -1
  54. package/dist/src/ee/search.d.ts +25 -0
  55. package/dist/src/ee/search.js +73 -0
  56. package/dist/src/ee/types.d.ts +15 -1
  57. package/dist/src/ee/who-am-i.d.ts +41 -0
  58. package/dist/src/ee/who-am-i.js +143 -0
  59. package/dist/src/ee/who-am-i.test.d.ts +1 -0
  60. package/dist/src/ee/who-am-i.test.js +101 -0
  61. package/dist/src/generated/version.d.ts +1 -1
  62. package/dist/src/generated/version.js +1 -1
  63. package/dist/src/headless/council-answers.js +1 -0
  64. package/dist/src/headless/output.d.ts +30 -1
  65. package/dist/src/headless/output.js +180 -0
  66. package/dist/src/headless/output.test.js +61 -1
  67. package/dist/src/hooks/index.js +48 -2
  68. package/dist/src/index.d.ts +2 -0
  69. package/dist/src/index.js +143 -104
  70. package/dist/src/lsp/client-normalize.test.d.ts +12 -0
  71. package/dist/src/lsp/client-normalize.test.js +34 -0
  72. package/dist/src/lsp/client.d.ts +1 -0
  73. package/dist/src/lsp/client.js +10 -2
  74. package/dist/src/lsp/manager.d.ts +1 -1
  75. package/dist/src/lsp/manager.js +2 -2
  76. package/dist/src/lsp/manager.test.js +2 -2
  77. package/dist/src/lsp/runtime.d.ts +1 -1
  78. package/dist/src/lsp/runtime.js +2 -2
  79. package/dist/src/maintain/__tests__/gh-create-pr.test.js +7 -3
  80. package/dist/src/maintain/__tests__/pr-builder.test.js +7 -3
  81. package/dist/src/mcp/__tests__/client-pool.spec.js +3 -3
  82. package/dist/src/mcp/__tests__/ee-tools.test.js +42 -14
  83. package/dist/src/mcp/__tests__/forensics-tools.test.js +3 -3
  84. package/dist/src/mcp/__tests__/lsp-tools.test.js +4 -4
  85. package/dist/src/mcp/client-pool.d.ts +1 -1
  86. package/dist/src/mcp/client-pool.js +25 -9
  87. package/dist/src/mcp/ee-tools.d.ts +10 -0
  88. package/dist/src/mcp/ee-tools.js +34 -0
  89. package/dist/src/mcp/oauth-callback.js +2 -2
  90. package/dist/src/mcp/parse-headers.test.js +14 -14
  91. package/dist/src/mcp/self-verify-jobs.js +1 -3
  92. package/dist/src/mcp/setup-guide-text.js +74 -74
  93. package/dist/src/mcp/smoke.test.js +43 -43
  94. package/dist/src/models/catalog-gemini.test.js +12 -10
  95. package/dist/src/models/catalog.json +106 -38
  96. package/dist/src/ops/doctor.js +7 -7
  97. package/dist/src/orchestrator/__tests__/auto-commit.test.d.ts +1 -0
  98. package/dist/src/orchestrator/__tests__/auto-commit.test.js +142 -0
  99. package/dist/src/orchestrator/__tests__/batch-turn-runner.test.js +2 -0
  100. package/dist/src/orchestrator/__tests__/delegations.test.d.ts +1 -0
  101. package/dist/src/orchestrator/__tests__/delegations.test.js +96 -0
  102. package/dist/src/orchestrator/__tests__/message-processor.test.js +1 -0
  103. package/dist/src/orchestrator/__tests__/stream-runner.test.js +29 -0
  104. package/dist/src/orchestrator/agent.test.js +5 -2
  105. package/dist/src/orchestrator/auto-commit.d.ts +110 -0
  106. package/dist/src/orchestrator/auto-commit.js +364 -0
  107. package/dist/src/orchestrator/batch-turn-runner.d.ts +3 -2
  108. package/dist/src/orchestrator/batch-turn-runner.js +2 -1
  109. package/dist/src/orchestrator/compaction-proposer-prompt.d.ts +8 -0
  110. package/dist/src/orchestrator/compaction-proposer-prompt.js +43 -0
  111. package/dist/src/orchestrator/compaction.d.ts +22 -0
  112. package/dist/src/orchestrator/compaction.js +42 -0
  113. package/dist/src/orchestrator/delegations.d.ts +1 -0
  114. package/dist/src/orchestrator/delegations.js +41 -0
  115. package/dist/src/orchestrator/error-utils.js +27 -2
  116. package/dist/src/orchestrator/message-processor.d.ts +21 -5
  117. package/dist/src/orchestrator/message-processor.js +219 -2573
  118. package/dist/src/orchestrator/orchestrator.d.ts +8 -2
  119. package/dist/src/orchestrator/orchestrator.js +187 -34
  120. package/dist/src/orchestrator/preprocessor.d.ts +13 -0
  121. package/dist/src/orchestrator/preprocessor.js +145 -0
  122. package/dist/src/orchestrator/prompts.d.ts +8 -0
  123. package/dist/src/orchestrator/prompts.js +76 -60
  124. package/dist/src/orchestrator/safety-askcard.d.ts +66 -0
  125. package/dist/src/orchestrator/safety-askcard.js +74 -0
  126. package/dist/src/orchestrator/sandbox.test.js +8 -5
  127. package/dist/src/orchestrator/scope-ceiling.js +1 -1
  128. package/dist/src/orchestrator/stall-watchdog.d.ts +47 -0
  129. package/dist/src/orchestrator/stall-watchdog.js +23 -0
  130. package/dist/src/orchestrator/stall-watchdog.test.js +36 -1
  131. package/dist/src/orchestrator/stream-runner.js +52 -30
  132. package/dist/src/orchestrator/sub-agent-model-tier.js +1 -1
  133. package/dist/src/orchestrator/subagent-compactor.d.ts +14 -0
  134. package/dist/src/orchestrator/subagent-compactor.js +83 -10
  135. package/dist/src/orchestrator/subagent-compactor.spec.js +132 -0
  136. package/dist/src/orchestrator/text-tool-call-detector.d.ts +9 -5
  137. package/dist/src/orchestrator/text-tool-call-detector.js +16 -12
  138. package/dist/src/orchestrator/text-tool-call-detector.test.js +66 -8
  139. package/dist/src/orchestrator/tool-args-repair.js +1 -1
  140. package/dist/src/orchestrator/tool-engine.d.ts +151 -0
  141. package/dist/src/orchestrator/tool-engine.js +3079 -0
  142. package/dist/src/orchestrator/turn-runner-deps.d.ts +1 -0
  143. package/dist/src/pil/__tests__/discovery-types.test.js +15 -4
  144. package/dist/src/pil/__tests__/discovery.test.js +89 -145
  145. package/dist/src/pil/__tests__/dual-run.test.js +2 -0
  146. package/dist/src/pil/__tests__/layer1-intent-trace.test.js +3 -0
  147. package/dist/src/pil/__tests__/layer1-intent.test.js +3 -0
  148. package/dist/src/pil/__tests__/layer16-clarity.test.js +35 -32
  149. package/dist/src/pil/__tests__/layer18-acceptance.test.js +1 -78
  150. package/dist/src/pil/__tests__/layer2_5-ponytail.test.d.ts +1 -0
  151. package/dist/src/pil/__tests__/layer2_5-ponytail.test.js +53 -0
  152. package/dist/src/pil/__tests__/layer3-ee-injection.test.js +67 -4
  153. package/dist/src/pil/__tests__/layer3-injected-chunk.test.js +14 -2
  154. package/dist/src/pil/__tests__/layer4-gsd.test.js +30 -0
  155. package/dist/src/pil/__tests__/layer5-context.test.js +39 -1
  156. package/dist/src/pil/__tests__/layer6-output.test.js +30 -2
  157. package/dist/src/pil/__tests__/orchestrator-integration.test.js +3 -1
  158. package/dist/src/pil/__tests__/pipeline.test.js +11 -8
  159. package/dist/src/pil/__tests__/response-tools.test.js +52 -0
  160. package/dist/src/pil/agent-operating-contract.d.ts +1 -1
  161. package/dist/src/pil/agent-operating-contract.js +5 -3
  162. package/dist/src/pil/agent-operating-contract.test.js +2 -2
  163. package/dist/src/pil/cheap-model-playbook.d.ts +1 -1
  164. package/dist/src/pil/cheap-model-playbook.js +1 -1
  165. package/dist/src/pil/cheap-model-workbooks.d.ts +1 -1
  166. package/dist/src/pil/cheap-model-workbooks.js +1 -1
  167. package/dist/src/pil/config.d.ts +20 -0
  168. package/dist/src/pil/config.js +35 -0
  169. package/dist/src/pil/discovery-types.d.ts +32 -9
  170. package/dist/src/pil/discovery.d.ts +3 -3
  171. package/dist/src/pil/discovery.js +138 -244
  172. package/dist/src/pil/index.d.ts +1 -1
  173. package/dist/src/pil/index.js +1 -1
  174. package/dist/src/pil/layer1-intent.d.ts +9 -1
  175. package/dist/src/pil/layer1-intent.js +99 -44
  176. package/dist/src/pil/layer1-intent.test.js +112 -2
  177. package/dist/src/pil/layer15-context-scan.js +1 -1
  178. package/dist/src/pil/layer16-clarity.d.ts +21 -25
  179. package/dist/src/pil/layer16-clarity.js +52 -88
  180. package/dist/src/pil/layer18-acceptance.d.ts +13 -3
  181. package/dist/src/pil/layer18-acceptance.js +1 -75
  182. package/dist/src/pil/layer2-personality.js +7 -8
  183. package/dist/src/pil/layer2_5-ponytail.d.ts +2 -0
  184. package/dist/src/pil/layer2_5-ponytail.js +41 -0
  185. package/dist/src/pil/layer3-ee-injection.js +180 -14
  186. package/dist/src/pil/layer4-gsd.js +22 -8
  187. package/dist/src/pil/layer5-context.d.ts +1 -0
  188. package/dist/src/pil/layer5-context.js +26 -11
  189. package/dist/src/pil/layer6-output.js +23 -5
  190. package/dist/src/pil/llm-classify.js +3 -3
  191. package/dist/src/pil/native-capabilities-workbook.d.ts +1 -1
  192. package/dist/src/pil/native-capabilities-workbook.js +4 -3
  193. package/dist/src/pil/pipeline.js +15 -1
  194. package/dist/src/pil/response-tools.d.ts +16 -0
  195. package/dist/src/pil/response-tools.js +41 -0
  196. package/dist/src/pil/schema.d.ts +8 -0
  197. package/dist/src/pil/schema.js +24 -3
  198. package/dist/src/pil/task-tier-map.js +7 -7
  199. package/dist/src/pil/types.d.ts +9 -3
  200. package/dist/src/product-loop/__tests__/discovery-interview.test.js +103 -1
  201. package/dist/src/product-loop/__tests__/gather-selectable-alts.test.js +34 -0
  202. package/dist/src/product-loop/__tests__/sprint-self-verify.test.js +12 -12
  203. package/dist/src/product-loop/discovery-interview.d.ts +20 -0
  204. package/dist/src/product-loop/discovery-interview.js +121 -7
  205. package/dist/src/product-loop/done-gate.js +3 -3
  206. package/dist/src/product-loop/gather.js +19 -5
  207. package/dist/src/product-loop/loop-driver.js +20 -20
  208. package/dist/src/product-loop/progress-snapshot.js +4 -4
  209. package/dist/src/product-loop/sprint-self-verify.js +2 -2
  210. package/dist/src/providers/__tests__/reasoning-roundtrip.test.js +12 -0
  211. package/dist/src/providers/auth/__tests__/gemini-oauth.test.d.ts +1 -5
  212. package/dist/src/providers/auth/__tests__/gemini-oauth.test.js +1 -5
  213. package/dist/src/providers/auth/gcloud.d.ts +28 -0
  214. package/dist/src/providers/auth/gcloud.js +102 -0
  215. package/dist/src/providers/auth/gemini-oauth.d.ts +46 -19
  216. package/dist/src/providers/auth/gemini-oauth.js +292 -105
  217. package/dist/src/providers/auth/grok-oauth.d.ts +1 -1
  218. package/dist/src/providers/auth/grok-oauth.js +1 -1
  219. package/dist/src/providers/auth/registry.js +28 -6
  220. package/dist/src/providers/gemini.d.ts +1 -1
  221. package/dist/src/providers/gemini.js +10 -3
  222. package/dist/src/providers/keychain.d.ts +2 -2
  223. package/dist/src/providers/keychain.js +20 -2
  224. package/dist/src/providers/mcp-vision-bridge.js +48 -48
  225. package/dist/src/providers/openai.js +5 -1
  226. package/dist/src/providers/runtime.d.ts +1 -1
  227. package/dist/src/providers/runtime.js +11 -2
  228. package/dist/src/providers/siliconflow-sse-repair.js +1 -1
  229. package/dist/src/providers/strategies/google.strategy.d.ts +9 -2
  230. package/dist/src/providers/strategies/google.strategy.js +159 -9
  231. package/dist/src/providers/strategies/thinking-mode.js +19 -6
  232. package/dist/src/providers/types.d.ts +1 -1
  233. package/dist/src/reporter/index.js +1 -1
  234. package/dist/src/router/decide.test.js +40 -8
  235. package/dist/src/router/step-router.d.ts +20 -0
  236. package/dist/src/router/step-router.js +73 -0
  237. package/dist/src/scaffold/bb-ecosystem-apply.js +47 -47
  238. package/dist/src/scaffold/bb-quality-gate.js +5 -5
  239. package/dist/src/scaffold/continuation-prompt.js +60 -60
  240. package/dist/src/scaffold/init-new.js +453 -453
  241. package/dist/src/self-qa/__tests__/scenario-planner.test.js +3 -3
  242. package/dist/src/self-qa/agentic-loop.js +19 -19
  243. package/dist/src/self-qa/orchestrator.js +5 -5
  244. package/dist/src/self-qa/scenario-planner.js +1 -1
  245. package/dist/src/self-qa/spec-emitter.js +4 -4
  246. package/dist/src/storage/__tests__/migrations.test.js +2 -2
  247. package/dist/src/storage/index.d.ts +1 -1
  248. package/dist/src/storage/index.js +1 -1
  249. package/dist/src/storage/interaction-log.js +5 -5
  250. package/dist/src/storage/migrations.js +122 -122
  251. package/dist/src/storage/session-experience-store.js +4 -4
  252. package/dist/src/storage/sessions.d.ts +6 -0
  253. package/dist/src/storage/sessions.js +49 -42
  254. package/dist/src/storage/transcript-view.js +7 -1
  255. package/dist/src/storage/transcript.d.ts +1 -0
  256. package/dist/src/storage/transcript.js +55 -0
  257. package/dist/src/storage/ui-interaction-log.d.ts +17 -0
  258. package/dist/src/storage/usage.js +14 -14
  259. package/dist/src/storage/workspaces.js +12 -12
  260. package/dist/src/tools/__tests__/native-tools.test.js +6 -0
  261. package/dist/src/tools/bash.d.ts +36 -23
  262. package/dist/src/tools/bash.js +59 -301
  263. package/dist/src/tools/bash.test.js +2 -257
  264. package/dist/src/tools/file-tracker.d.ts +5 -0
  265. package/dist/src/tools/file-tracker.js +14 -0
  266. package/dist/src/tools/file.js +2 -0
  267. package/dist/src/tools/git-safety.d.ts +38 -1
  268. package/dist/src/tools/git-safety.js +94 -4
  269. package/dist/src/tools/git-safety.test.js +45 -1
  270. package/dist/src/tools/native-tools.d.ts +1 -1
  271. package/dist/src/tools/native-tools.js +56 -0
  272. package/dist/src/tools/registry-bash-empty-command.test.js +4 -3
  273. package/dist/src/tools/registry-git-safety.test.js +17 -6
  274. package/dist/src/tools/registry.d.ts +5 -0
  275. package/dist/src/tools/registry.js +185 -11
  276. package/dist/src/types/index.d.ts +7 -1
  277. package/dist/src/ui/app.d.ts +2 -1
  278. package/dist/src/ui/app.js +0 -0
  279. package/dist/src/ui/components/council-question-card.js +1 -0
  280. package/dist/src/ui/components/message-view.js +37 -3
  281. package/dist/src/ui/components/structured-response-view.js +7 -0
  282. package/dist/src/ui/components/task-list-panel.js +3 -12
  283. package/dist/src/ui/containers/chat-feed.d.ts +40 -0
  284. package/dist/src/ui/containers/chat-feed.js +66 -0
  285. package/dist/src/ui/containers/modals-layer.d.ts +87 -0
  286. package/dist/src/ui/containers/modals-layer.js +18 -0
  287. package/dist/src/ui/modals/session-picker-modal.js +14 -9
  288. package/dist/src/ui/modals/update-modal.js +2 -1
  289. package/dist/src/ui/slash/__tests__/menu-parity.test.d.ts +1 -0
  290. package/dist/src/ui/slash/__tests__/menu-parity.test.js +1 -0
  291. package/dist/src/ui/slash/__tests__/ponytail.test.d.ts +1 -0
  292. package/dist/src/ui/slash/__tests__/ponytail.test.js +37 -0
  293. package/dist/src/ui/slash/council-inspect.js +4 -4
  294. package/dist/src/ui/slash/export.js +16 -1
  295. package/dist/src/ui/slash/menu-items.js +1 -0
  296. package/dist/src/ui/slash/ponytail.d.ts +2 -0
  297. package/dist/src/ui/slash/ponytail.js +19 -0
  298. package/dist/src/ui/status-bar/store.js +0 -1
  299. package/dist/src/ui/status-bar/store.test.js +2 -2
  300. package/dist/src/ui/types.d.ts +7 -0
  301. package/dist/src/ui/use-app-logic.d.ts +26 -0
  302. package/dist/src/ui/use-app-logic.js +0 -0
  303. package/dist/src/ui/utils/relaunch.d.ts +17 -0
  304. package/dist/src/ui/utils/relaunch.js +28 -4
  305. package/dist/src/ui/utils/relaunch.test.js +50 -0
  306. package/dist/src/ui/utils/text.d.ts +11 -0
  307. package/dist/src/ui/utils/text.js +21 -1
  308. package/dist/src/ui/utils/text.test.js +25 -1
  309. package/dist/src/ui/utils/tools.js +3 -1
  310. package/dist/src/usage/cost-log.js +2 -2
  311. package/dist/src/usage/estimator.d.ts +14 -0
  312. package/dist/src/usage/estimator.js +22 -0
  313. package/dist/src/utils/__tests__/compaction-caps.test.d.ts +21 -0
  314. package/dist/src/utils/__tests__/compaction-caps.test.js +187 -0
  315. package/dist/src/utils/__tests__/llm-deadline-abort.test.d.ts +1 -0
  316. package/dist/src/utils/__tests__/llm-deadline-abort.test.js +33 -0
  317. package/dist/src/utils/clipboard-image.js +23 -23
  318. package/dist/src/utils/llm-deadline.d.ts +9 -1
  319. package/dist/src/utils/llm-deadline.js +31 -2
  320. package/dist/src/utils/permission-mode.d.ts +33 -0
  321. package/dist/src/utils/permission-mode.js +164 -3
  322. package/dist/src/utils/permission-mode.test.js +92 -1
  323. package/dist/src/utils/settings.d.ts +27 -31
  324. package/dist/src/utils/settings.js +100 -109
  325. package/dist/src/utils/side-question.js +2 -2
  326. package/dist/src/utils/skills.js +3 -3
  327. package/dist/src/verify/__tests__/coverage-parsers.test.js +30 -30
  328. package/package.json +1 -1
@@ -1,4 +1,5 @@
1
1
  import { beforeEach, describe, expect, test, vi } from "vitest";
2
+ import { sessionRecallLedger } from "../../ee/recall-ledger.js";
2
3
  import { layer3EeInjection, RECALL_FEEDBACK_NUDGE } from "../layer3-ee-injection.js";
3
4
  vi.mock("../../ee/bridge.js", () => ({
4
5
  searchByText: vi.fn().mockResolvedValue([]),
@@ -22,6 +23,9 @@ describe("layer3EeInjection (bridge-based)", () => {
22
23
  beforeEach(() => {
23
24
  vi.mocked(searchByText).mockReset();
24
25
  vi.mocked(searchByText).mockResolvedValue([]);
26
+ // The recall-feedback ledger is a process singleton; reset so passive-injection
27
+ // debt recorded by one test does not lengthen the pending reminder in the next.
28
+ sessionRecallLedger.reset();
25
29
  });
26
30
  test("Test 1: returns enriched context with hints when searchByText returns points with payload.text", async () => {
27
31
  vi.mocked(searchByText).mockResolvedValue([
@@ -93,10 +97,10 @@ describe("layer3EeInjection (bridge-based)", () => {
93
97
  const chars = parseInt(charsMatch[1], 10);
94
98
  // Two parallel collections, each at 15% of budget: 15% of 100 tokens * 4 chars/token
95
99
  // = 60 chars per block + 3 for "..." suffix, joined with newline. Allow generous
96
- // ceiling for header text + 2 blocks, PLUS the fixed ee_feedback nudge appended
97
- // when rateable experience is present. The 2000-char input means a truncation
98
- // regression would blow well past this bound regardless.
99
- expect(chars).toBeLessThanOrEqual(260 + RECALL_FEEDBACK_NUDGE.length + 1);
100
+ // ceiling for header text + 2 blocks, PLUS the dynamic pending-feedback reminder
101
+ // (≤5 [id collection] lines) that replaced the fixed nudge. The 2000-char input
102
+ // means a truncation regression would blow well past this bound regardless.
103
+ expect(chars).toBeLessThanOrEqual(600);
100
104
  }
101
105
  }
102
106
  });
@@ -137,6 +141,65 @@ describe("Layer 3 formatter mode (ctx._brainData populated)", () => {
137
141
  expect(result.enriched).toContain("mock fs in unit tests");
138
142
  expect(result.t1Rules).toEqual(["never skip tests"]);
139
143
  });
144
+ test("unified path: records rateable points (with id) into the ledger + names [id collection] in the reminder", async () => {
145
+ // Symmetric with the legacy-path recall-loop closure. When the server (PIL
146
+ // schema_version 1.1+) attributes points with id/collection, the unified
147
+ // formatter must record them as pending debt and surface an actionable reminder.
148
+ sessionRecallLedger.reset();
149
+ const { layer3EeInjection } = await import("../layer3-ee-injection.js");
150
+ const ctx = {
151
+ raw: "x",
152
+ enriched: "x",
153
+ taskType: "debug",
154
+ domain: null,
155
+ confidence: 0.85,
156
+ outputStyle: "balanced",
157
+ tokenBudget: 2000,
158
+ metrics: null,
159
+ layers: [],
160
+ _brainData: {
161
+ t0_principles: [{ text: "always run tests", score: 0.9, id: "pr1", collection: "experience-principles" }],
162
+ t1_rules: [],
163
+ t2_patterns: [{ text: "mock fs in unit tests", score: 0.7, id: "be1", collection: "experience-behavioral" }],
164
+ retrieval_skipped_reason: null,
165
+ },
166
+ };
167
+ const result = await layer3EeInjection(ctx);
168
+ // Inline [id:..] handle rendered so the reminder refers to something visible.
169
+ expect(result.enriched).toContain("[id:pr1]");
170
+ // Dynamic reminder names the actual [id collection] so ee_feedback is actionable.
171
+ expect(result.enriched).toMatch(/ee_feedback\(id, collection, followed\|ignored\|noise\)/);
172
+ expect(result.enriched).toContain("[pr1 experience-principles]");
173
+ expect(result.enriched).toContain("[be1 experience-behavioral]");
174
+ // Both points recorded as rateable pending debt.
175
+ expect(sessionRecallLedger.pendingCount()).toBe(2);
176
+ });
177
+ test("unified path: no id (older server) renders text but stays unrateable (static nudge)", async () => {
178
+ sessionRecallLedger.reset();
179
+ const { layer3EeInjection } = await import("../layer3-ee-injection.js");
180
+ const ctx = {
181
+ raw: "x",
182
+ enriched: "x",
183
+ taskType: "debug",
184
+ domain: null,
185
+ confidence: 0.85,
186
+ outputStyle: "balanced",
187
+ tokenBudget: 2000,
188
+ metrics: null,
189
+ layers: [],
190
+ _brainData: {
191
+ t0_principles: [{ text: "always run tests", score: 0.9 }],
192
+ t1_rules: [],
193
+ t2_patterns: [{ text: "mock fs in unit tests", score: 0.7 }],
194
+ retrieval_skipped_reason: null,
195
+ },
196
+ };
197
+ const result = await layer3EeInjection(ctx);
198
+ expect(result.enriched).toContain("always run tests");
199
+ // No ids → nothing recorded; the static nudge is the fallback.
200
+ expect(sessionRecallLedger.pendingCount()).toBe(0);
201
+ expect(result.enriched).toContain(RECALL_FEEDBACK_NUDGE);
202
+ });
140
203
  test("emits no block when ctx._brainData is null AND legacy disabled by flag", async () => {
141
204
  const { layer3EeInjection } = await import("../layer3-ee-injection.js");
142
205
  const ctx = {
@@ -24,6 +24,7 @@ vi.mock("../../storage/interaction-log.js", () => ({
24
24
  vi.mock("../../ee/intercept.js", () => ({
25
25
  updateLastSurfacedState: vi.fn(),
26
26
  }));
27
+ import { sessionRecallLedger } from "../../ee/recall-ledger.js";
27
28
  import { layer3EeInjection } from "../layer3-ee-injection.js";
28
29
  const BASE_CTX = {
29
30
  raw: "test prompt for experience injection",
@@ -41,6 +42,9 @@ describe("layer3 experience_injected chunk emission (CQ-16b)", () => {
41
42
  beforeEach(() => {
42
43
  capturedSinkCalls.length = 0;
43
44
  mockSearchByText.mockResolvedValue([]);
45
+ // Reset the process-singleton recall ledger so pending debt from a prior test
46
+ // does not change the dynamic feedback reminder content of the next.
47
+ sessionRecallLedger.reset();
44
48
  });
45
49
  it("emits experience_injected chunk when searchByText returns high-score point", async () => {
46
50
  mockSearchByText.mockResolvedValue([
@@ -103,8 +107,16 @@ describe("layer3 experience_injected chunk emission (CQ-16b)", () => {
103
107
  collection: "experience-behavioral",
104
108
  },
105
109
  ]);
106
- const result = await layer3EeInjection(BASE_CTX);
107
- expect(result.enriched).toContain("ee_feedback(id, followed|ignored|noise)");
110
+ const result = await layer3EeInjection({ ...BASE_CTX, sessionId: "test-session-nudge" });
111
+ // The dynamic pending-feedback reminder replaced the fixed nudge when the ledger
112
+ // is enabled (default soft); it names the actual [id collection] so ee_feedback is
113
+ // actionable. Accept either form (reminder when the ledger recorded debt, or the
114
+ // static fallback when disabled).
115
+ expect(result.enriched).toMatch(/ee_feedback\(id, (collection, )?followed\|ignored\|noise\)/);
116
+ // Reminder names the actual [id collection]; the mock returns the same point in
117
+ // both search arms so the principles arm wins first-sighting (real searches return
118
+ // distinct points per collection) — assert id-named, collection-agnostic.
119
+ expect(result.enriched).toMatch(/\[p1 experience-(principles|behavioral)\]/);
108
120
  });
109
121
  it("does NOT emit experience_injected when searchByText returns empty array", async () => {
110
122
  mockSearchByText.mockResolvedValue([]);
@@ -1,7 +1,11 @@
1
1
  import { describe, expect, it, vi } from "vitest";
2
2
  vi.mock("../../ee/bridge.js", () => ({
3
3
  routeTask: vi.fn().mockResolvedValue(null),
4
+ // WhoAmI v4.0: L4 reads delegation_style to bias a 'discuss' phase. Default null
5
+ // = fail-open, so every pre-existing test below behaves exactly as before.
6
+ getWhoAmIProfile: vi.fn(() => null),
4
7
  }));
8
+ import { getWhoAmIProfile } from "../../ee/bridge.js";
5
9
  import { layer4Gsd } from "../layer4-gsd.js";
6
10
  function makeCtx(overrides = {}) {
7
11
  return {
@@ -139,6 +143,32 @@ describe("layer4Gsd (playbook)", () => {
139
143
  const result = await layer4Gsd(makeCtx({ raw, enriched: raw, taskType: "refactor", intentKind: "task", deliverableKind: "code" }));
140
144
  expect(result.enriched).not.toContain("QUESTION / explanatory");
141
145
  });
146
+ it("autonomous delegation drops a 'discuss' phase (WhoAmI v4.0 bias toward direct)", async () => {
147
+ vi.mocked(getWhoAmIProfile).mockReturnValueOnce({
148
+ level: "standard",
149
+ dims: { "work_patterns.delegation_style": { value: "autonomous", confidence: 0.68, samples: 19 } },
150
+ });
151
+ const result = await layer4Gsd(makeCtx({ gsdPhase: "discuss", raw: "do the thing" }));
152
+ expect(result.gsdPhase ?? null).toBeNull();
153
+ const layer = result.layers.find((l) => l.name === "gsd-workflow-structuring");
154
+ expect(layer.delta).toContain("phase=none");
155
+ expect(layer.delta).toContain("autonomous:skip-discuss");
156
+ });
157
+ it("collaborative delegation keeps the 'discuss' phase", async () => {
158
+ vi.mocked(getWhoAmIProfile).mockReturnValueOnce({
159
+ level: "standard",
160
+ dims: { "work_patterns.delegation_style": { value: "collaborative", confidence: 0.7, samples: 20 } },
161
+ });
162
+ const result = await layer4Gsd(makeCtx({ gsdPhase: "discuss", raw: "do the thing" }));
163
+ expect(result.gsdPhase).toBe("discuss");
164
+ const layer = result.layers.find((l) => l.name === "gsd-workflow-structuring");
165
+ expect(layer.delta).toContain("phase=discuss");
166
+ });
167
+ it("absent profile leaves 'discuss' untouched (fail-open default)", async () => {
168
+ // getWhoAmIProfile defaults to null in the mock — the phase must be unchanged.
169
+ const result = await layer4Gsd(makeCtx({ gsdPhase: "discuss", raw: "do the thing" }));
170
+ expect(result.gsdPhase).toBe("discuss");
171
+ });
142
172
  it("uses ctx.gsdPhase from L1 (unified path) without calling routeTask", async () => {
143
173
  const { routeTask } = await import("../../ee/bridge.js");
144
174
  vi.mocked(routeTask).mockClear();
@@ -3,6 +3,9 @@ vi.mock("../../ee/bridge.js", () => ({
3
3
  getEmbeddingRaw: vi.fn().mockResolvedValue(null),
4
4
  searchCollection: vi.fn().mockResolvedValue([]),
5
5
  searchByText: vi.fn().mockResolvedValue([]),
6
+ // WhoAmI v4.0: L5 reads session_length to tune the resume-digest stale window.
7
+ // Default null = fail-open → 30m default, so the pre-v4.0 assertions below hold.
8
+ getWhoAmIProfile: vi.fn(() => null),
6
9
  }));
7
10
  vi.mock("node:fs", async (importOriginal) => {
8
11
  const actual = await importOriginal();
@@ -16,7 +19,8 @@ vi.mock("node:fs", async (importOriginal) => {
16
19
  },
17
20
  };
18
21
  });
19
- import { layer5Context } from "../layer5-context.js";
22
+ import { getWhoAmIProfile } from "../../ee/bridge.js";
23
+ import { layer5Context, staleThresholdMsForSessionLength } from "../layer5-context.js";
20
24
  function makeCtx(overrides = {}) {
21
25
  return {
22
26
  raw: "continue working on the auth module",
@@ -133,5 +137,39 @@ describe("layer5Context", () => {
133
137
  expect(result.enriched).toContain("already enriched prompt");
134
138
  expect(result.enriched).toContain("[flow-context:");
135
139
  });
140
+ // WhoAmI v4.0: session_length tunes the resume-digest stale window. A 45-minute-old
141
+ // digest is stale at the 30m default but FRESH for a long-session user (window→60m).
142
+ // This proves the profile actually flows through layer5Context, not just the helper.
143
+ it("long session_length relaxes the stale window — 45m digest is not stale", async () => {
144
+ vi.mocked(getWhoAmIProfile).mockReturnValueOnce({
145
+ level: "minimal",
146
+ dims: { "work_patterns.session_length": { value: "long", confidence: 0.5, samples: 16 } },
147
+ });
148
+ const result = await layer5Context(makeCtx({ resumeDigest: "Deep-work session context", digestAgeMs: 45 * 60 * 1000 }));
149
+ expect(result.enriched).not.toContain("stale");
150
+ const layer = result.layers.find((l) => l.name === "context-enrichment");
151
+ expect(layer.delta).toContain("window=60m");
152
+ expect(layer.delta).not.toContain(",stale");
153
+ });
154
+ it("short session_length tightens the stale window — 20m digest is stale", async () => {
155
+ vi.mocked(getWhoAmIProfile).mockReturnValueOnce({
156
+ level: "minimal",
157
+ dims: { "work_patterns.session_length": { value: "short", confidence: 0.5, samples: 16 } },
158
+ });
159
+ const result = await layer5Context(makeCtx({ resumeDigest: "Quick-burst session context", digestAgeMs: 20 * 60 * 1000 }));
160
+ expect(result.enriched).toContain("stale");
161
+ const layer = result.layers.find((l) => l.name === "context-enrichment");
162
+ expect(layer.delta).toContain("window=15m");
163
+ });
164
+ });
165
+ describe("staleThresholdMsForSessionLength — pure session_length → stale window mapping", () => {
166
+ it("long → 60m, short → 15m, medium/absent/unknown → 30m default (fail-open)", () => {
167
+ expect(staleThresholdMsForSessionLength("long")).toBe(60 * 60 * 1000);
168
+ expect(staleThresholdMsForSessionLength("short")).toBe(15 * 60 * 1000);
169
+ expect(staleThresholdMsForSessionLength("medium")).toBe(30 * 60 * 1000);
170
+ expect(staleThresholdMsForSessionLength(undefined)).toBe(30 * 60 * 1000);
171
+ expect(staleThresholdMsForSessionLength(null)).toBe(30 * 60 * 1000);
172
+ expect(staleThresholdMsForSessionLength("telegraphic")).toBe(30 * 60 * 1000);
173
+ });
136
174
  });
137
175
  //# sourceMappingURL=layer5-context.test.js.map
@@ -1,8 +1,11 @@
1
- import { beforeEach, describe, expect, it, vi } from "vitest";
1
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
2
+ import { getWhoAmIProfile } from "../../ee/bridge.js";
2
3
  import { applyPilSuffix, getResponseToolSet, isImplementationIntent, isQuestionLike, layer6Output, } from "../layer6-output.js";
3
- // Mock bridge for PIL-03 classifyViaBrain tests
4
+ // Mock bridge for PIL-03 classifyViaBrain tests. getWhoAmIProfile defaults to null
5
+ // (fail-open) so applyPilSuffix never reads the real on-device profile during tests.
4
6
  vi.mock("../../ee/bridge.js", () => ({
5
7
  classifyViaBrain: vi.fn().mockResolvedValue(null),
8
+ getWhoAmIProfile: vi.fn(() => null),
6
9
  }));
7
10
  async function getMockBrain() {
8
11
  const { classifyViaBrain } = await import("../../ee/bridge.js");
@@ -434,4 +437,29 @@ describe("isImplementationIntent — improve / cải thiện (regression: sessio
434
437
  expect(isImplementationIntent("why does the suite fail — break it down")).toBe(false);
435
438
  });
436
439
  });
440
+ describe("applyPilSuffix — WhoAmI feedback_style correction-affordance", () => {
441
+ const withFeedback = (value) => ({
442
+ level: "standard",
443
+ dims: { "communication.feedback_style": { value, confidence: 0.7, samples: 20 } },
444
+ });
445
+ // Reset around each test so a mockReturnValue never leaks into (or out of) this
446
+ // block — clearAllMocks elsewhere only clears history, not the return override.
447
+ beforeEach(() => vi.mocked(getWhoAmIProfile).mockReturnValue(null));
448
+ afterEach(() => vi.mocked(getWhoAmIProfile).mockReturnValue(null));
449
+ it("precise-correction + action task (refactor) → AUDITABLE CHANGES rule", () => {
450
+ vi.mocked(getWhoAmIProfile).mockReturnValue(withFeedback("precise-correction"));
451
+ expect(applyPilSuffix("SYS", makeCtx("refactor"))).toContain("AUDITABLE CHANGES");
452
+ });
453
+ it("precise-correction + non-action task (analyze) → no rule (diffs don't apply)", () => {
454
+ vi.mocked(getWhoAmIProfile).mockReturnValue(withFeedback("precise-correction"));
455
+ expect(applyPilSuffix("SYS", makeCtx("analyze"))).not.toContain("AUDITABLE CHANGES");
456
+ });
457
+ it("implicit feedback → no rule (intentionally unwired — covered by brevity/delegation)", () => {
458
+ vi.mocked(getWhoAmIProfile).mockReturnValue(withFeedback("implicit"));
459
+ expect(applyPilSuffix("SYS", makeCtx("debug"))).not.toContain("AUDITABLE CHANGES");
460
+ });
461
+ it("absent profile → no rule (fail-open)", () => {
462
+ expect(applyPilSuffix("SYS", makeCtx("generate"))).not.toContain("AUDITABLE CHANGES");
463
+ });
464
+ });
437
465
  //# sourceMappingURL=layer6-output.test.js.map
@@ -17,6 +17,8 @@ vi.mock("../../ee/bridge.js", () => ({
17
17
  searchCollection: vi.fn().mockResolvedValue([]),
18
18
  getEmbeddingRaw: vi.fn().mockResolvedValue(null),
19
19
  routeTask: vi.fn().mockResolvedValue(null),
20
+ getWhoAmIProfile: vi.fn(() => null),
21
+ outputStyleFromProfile: vi.fn(() => null),
20
22
  }));
21
23
  import { applyPilSuffix, runPipeline } from "../index.js";
22
24
  beforeEach(() => {
@@ -27,7 +29,7 @@ describe("PIL orchestrator contract", () => {
27
29
  const ctx = await runPipeline("refactor this function");
28
30
  // Layer 2 personality now appends hints, so enriched starts with raw
29
31
  expect(ctx.enriched.startsWith("refactor this function")).toBe(true);
30
- expect(ctx.layers).toHaveLength(6);
32
+ expect(ctx.layers).toHaveLength(7);
31
33
  });
32
34
  it("applyPilSuffix appends OUTPUT RULES suffix for coding taskType (non-null)", () => {
33
35
  const codingCtx = {
@@ -8,6 +8,8 @@ vi.mock("../../ee/bridge.js", () => ({
8
8
  searchCollection: vi.fn().mockResolvedValue([]),
9
9
  getEmbeddingRaw: vi.fn().mockResolvedValue(null),
10
10
  routeTask: vi.fn().mockResolvedValue(null),
11
+ getWhoAmIProfile: vi.fn(() => null),
12
+ outputStyleFromProfile: vi.fn(() => null),
11
13
  }));
12
14
  import { classify } from "../../router/classifier/index.js";
13
15
  import { runPipeline } from "../pipeline.js";
@@ -18,10 +20,10 @@ beforeEach(() => {
18
20
  mockClassify.mockReturnValue({ tier: "hot", confidence: 0.85, reason: "regex:refactor" });
19
21
  });
20
22
  describe("runPipeline()", () => {
21
- it("returns PipelineContext with 6 LayerResults for normal input", async () => {
23
+ it("returns PipelineContext with 7 LayerResults for normal input", async () => {
22
24
  const ctx = await runPipeline("refactor this function");
23
25
  expect(ctx.raw).toBe("refactor this function");
24
- expect(ctx.layers).toHaveLength(6);
26
+ expect(ctx.layers).toHaveLength(7);
25
27
  });
26
28
  it("returns enriched that starts with raw (layers may append hints)", async () => {
27
29
  const ctx = await runPipeline("some prompt");
@@ -44,24 +46,25 @@ describe("runPipeline()", () => {
44
46
  const ctx = await runPipeline("");
45
47
  expect(ctx.raw).toBe("");
46
48
  expect(ctx.enriched.startsWith(ctx.raw)).toBe(true);
47
- expect(ctx.layers).toHaveLength(6);
49
+ expect(ctx.layers).toHaveLength(7);
48
50
  });
49
51
  it("conversational turn (taskType=null) skips layers 2-5 with delta=skipped:null-taskType", async () => {
50
52
  mockClassify.mockReturnValue({ tier: "abstain", confidence: 0.2, reason: "low-confidence" });
51
53
  const ctx = await runPipeline("hello how are you");
52
- expect(ctx.layers).toHaveLength(6);
54
+ expect(ctx.layers).toHaveLength(7);
53
55
  expect(ctx.layers[1].delta).toBe("skipped:null-taskType");
54
56
  expect(ctx.layers[2].delta).toBe("skipped:null-taskType");
55
57
  expect(ctx.layers[3].delta).toBe("skipped:null-taskType");
56
58
  expect(ctx.layers[4].delta).toBe("skipped:null-taskType");
59
+ expect(ctx.layers[5].delta).toBe("skipped:null-taskType");
57
60
  expect(ctx.taskType).toBeNull();
58
61
  });
59
- it("coding task runs all 6 layers normally (no skip)", async () => {
62
+ it("coding task runs all 7 layers normally (no skip)", async () => {
60
63
  const ctx = await runPipeline("refactor this function");
61
- expect(ctx.layers).toHaveLength(6);
64
+ expect(ctx.layers).toHaveLength(7);
62
65
  expect(ctx.taskType).toBe("refactor");
63
- // layers 2-5 should NOT have skipped delta
64
- for (let i = 1; i <= 4; i++) {
66
+ // layers 2-5 (which include indices 1 to 5) should NOT have skipped delta
67
+ for (let i = 1; i <= 5; i++) {
65
68
  expect(ctx.layers[i].delta).not.toBe("skipped:null-taskType");
66
69
  }
67
70
  });
@@ -172,6 +172,7 @@ describe("schemas validate correct input", () => {
172
172
  expect(result.success).toBe(false);
173
173
  });
174
174
  });
175
+ import { normalizeStructuredResponseTaskType } from "../response-tools.js";
175
176
  describe("respond_general catch-all tool", () => {
176
177
  it("respond_general tool exists with correct schema", () => {
177
178
  const tools = buildResponseTools("general");
@@ -187,4 +188,55 @@ describe("respond_general catch-all tool", () => {
187
188
  expect(isResponseTool("respond_general")).toBe(true);
188
189
  });
189
190
  });
191
+ describe("normalizeStructuredResponseTaskType", () => {
192
+ // Regression: session 48d22fe436f6 — model called respond_analyze but sent
193
+ // { response: "..." } (general shape). TUI rendered empty findings box.
194
+ it("normalizes 'analyze' → 'general' when findings missing but response present", () => {
195
+ expect(normalizeStructuredResponseTaskType("analyze", { response: "Some analysis text" })).toBe("general");
196
+ });
197
+ it("keeps 'analyze' when findings array is present", () => {
198
+ expect(normalizeStructuredResponseTaskType("analyze", {
199
+ findings: [{ text: "issue", evidence: "file:1", severity: "high" }],
200
+ })).toBe("analyze");
201
+ });
202
+ it("normalizes 'debug' → 'general' when root_cause missing but response present", () => {
203
+ expect(normalizeStructuredResponseTaskType("debug", { response: "Bug is in the store" })).toBe("general");
204
+ });
205
+ it("keeps 'debug' when root_cause is present", () => {
206
+ expect(normalizeStructuredResponseTaskType("debug", {
207
+ hypothesis: "race condition",
208
+ root_cause: "shared mutable state",
209
+ fix: { file: "src/a.ts", diff: "+x" },
210
+ verify_command: "bun test",
211
+ })).toBe("debug");
212
+ });
213
+ it("normalizes 'plan' → 'general' when steps missing but response present", () => {
214
+ expect(normalizeStructuredResponseTaskType("plan", { response: "Step 1: do A" })).toBe("general");
215
+ });
216
+ it("normalizes 'refactor' → 'general' when changes missing but response present", () => {
217
+ expect(normalizeStructuredResponseTaskType("refactor", { response: "Refactored X" })).toBe("general");
218
+ });
219
+ it("normalizes 'documentation' → 'general' when content missing but response present", () => {
220
+ expect(normalizeStructuredResponseTaskType("documentation", { response: "Docs here" })).toBe("general");
221
+ });
222
+ it("normalizes 'generate' → 'general' when files missing but response present", () => {
223
+ expect(normalizeStructuredResponseTaskType("generate", { response: "Generated code" })).toBe("general");
224
+ });
225
+ it("keeps 'general' unchanged always", () => {
226
+ expect(normalizeStructuredResponseTaskType("general", { response: "hello" })).toBe("general");
227
+ expect(normalizeStructuredResponseTaskType("general", {})).toBe("general");
228
+ });
229
+ it("does NOT normalize when response field is empty string", () => {
230
+ expect(normalizeStructuredResponseTaskType("analyze", { response: "" })).toBe("analyze");
231
+ });
232
+ it("does NOT normalize when response field is whitespace-only", () => {
233
+ expect(normalizeStructuredResponseTaskType("analyze", { response: " " })).toBe("analyze");
234
+ });
235
+ it("does NOT normalize when response field is missing entirely", () => {
236
+ expect(normalizeStructuredResponseTaskType("analyze", { summary: "Some summary" })).toBe("analyze");
237
+ });
238
+ it("keeps unknown taskType unchanged", () => {
239
+ expect(normalizeStructuredResponseTaskType("unknown_type", { response: "hello" })).toBe("unknown_type");
240
+ });
241
+ });
190
242
  //# sourceMappingURL=response-tools.test.js.map
@@ -37,7 +37,7 @@
37
37
  * one imperative line targeting that phase's most damaging failure mode. Kept
38
38
  * tight (primacy matters more than detail; tokens are the cost).
39
39
  */
40
- export declare const AGENT_OPERATING_CONTRACT = "[AGENT OPERATING CONTRACT \u2014 read first; applies to every step]\n\n1. BEFORE ACTING: do only what was asked. Never assume scope or facts \u2014 if ambiguous, ask or use defaults; never invent requirements.\n2. READING: base statements on what you read/ran THIS turn. Do not infer contents of files you did not open.\n3. EXECUTING: smallest correct change; never widen scope or mask failures (no `|| true`, skipped tests, or swallowed catch).\n4. WHEN UNSURE: verify and cross-check BEFORE concluding. Bugs need a reproduction; reading code is not proof.\n5. REPORTING: answer ONLY what was asked. Every fact or file:line MUST come from this turn; else label \"unverified\"; do not guess. Synthesize evidence gracefully \u2014 do NOT dump massive verbatim tool outputs into the final answer. Cite concise file:line references. Never claim a build/test ran, or describe edits, you did not actually do this turn; if a check can't run, fix it or say so \u2014 don't imply success.\n\n6. LANGUAGE: Reply in user's detected language for final output. Internal reasoning, tools, and code remain in English.\n\n7. ANTI-M\u00D9 / COMPACTION: After seeing \"[pre-compaction warning at step...\" or \"[context compacted at step...\", decide if you need full prior tool results. Emit PRESERVE_FULL_CONTEXT for full veto this turn, or the lighter KEEP_TOOL_IDS: id1,id2 (ids from prior stub \"(id=...)\") to protect only high-value results (read_file/grep on src/PLAN/error etc are auto-protected). Use the ee_query tool with \"tool-artifact id=XXX\" for on-demand full re-hydrate of elided ones. Self-check \"task finished?\" / \"compacted yet?\". Use EE checkpoints.\n\n8. GIT SAFETY: never push on red \u2014 run the check, await its result in a SEPARATE step, confirm 0 failures, then push. Never `git add -A`/`commit -a`; stage explicitly so secrets (.env, .muonroi-cli/, keys) aren't committed. Never `--no-verify`.\n\n[END CONTRACT \u2014 instructions follow]";
40
+ export declare const AGENT_OPERATING_CONTRACT = "[AGENT OPERATING CONTRACT \u2014 read first; applies to every step]\n\n1. BEFORE ACTING: do only what was asked. Never assume scope or facts \u2014 if ambiguous, ask or use defaults; never invent requirements. RESEARCH FIRST: explore code (read/grep) and recall EE brain before editing. RECALL FIRST: ee.query in unfamiliar areas to surface past lessons.\n2. READING: base statements on what you read/ran THIS turn. Do not infer contents of files you did not open.\n3. EXECUTING: smallest correct change; never widen scope or mask failures (no `|| true`, skipped tests, or swallowed catch).\n4. WHEN UNSURE: verify and cross-check BEFORE concluding. Reading code is not proof \u2014 reproduce the bug.\n5. REPORTING: answer ONLY what was asked. Every fact or file:line MUST come from this turn; else label \"unverified\"; do not guess. Synthesize evidence gracefully \u2014 do NOT dump massive verbatim tool outputs into the final answer. Cite concise file:line references. Never claim a build/test ran, or describe edits, you did not actually do this turn; if a check can't run, fix it or say so \u2014 don't imply success.\n\n6. LANGUAGE: Reply in user's detected language for final output. Internal reasoning, tools, and code remain in English.\n\n7. ANTI-M\u00D9 / COMPACTION: On compaction, emit PRESERVE_FULL_CONTEXT (veto) or KEEP_TOOL_IDS (from stub id=) to protect results. Use ee_query with \"tool-artifact id=XXX\" to re-hydrate. Self-check via EE checkpoints. Suggest /compact near tool limits.\n\n8. GIT SAFETY: never push on red \u2014 run the check, await its result in a SEPARATE step, confirm 0 failures, then push. Never `git add -A`/`commit -a`; stage explicitly so secrets (.env, .muonroi-cli/, keys) aren't committed. Never `--no-verify`.\n\n9. VERIFICATION: when finishing a task, ALWAYS self-verify your work. Use the `selfverify_*` native tools (start/status/result) to run the QA harness which drives the live TUI like a real user to catch regressions that unit tests can't.\n\n[END CONTRACT \u2014 instructions follow]";
41
41
  export interface ContractSectionOptions {
42
42
  /** Chitchat turns carry no tools and make no factual claims — skip the contract. */
43
43
  chitchat?: boolean;
@@ -39,18 +39,20 @@
39
39
  */
40
40
  export const AGENT_OPERATING_CONTRACT = `[AGENT OPERATING CONTRACT — read first; applies to every step]
41
41
 
42
- 1. BEFORE ACTING: do only what was asked. Never assume scope or facts — if ambiguous, ask or use defaults; never invent requirements.
42
+ 1. BEFORE ACTING: do only what was asked. Never assume scope or facts — if ambiguous, ask or use defaults; never invent requirements. RESEARCH FIRST: explore code (read/grep) and recall EE brain before editing. RECALL FIRST: ee.query in unfamiliar areas to surface past lessons.
43
43
  2. READING: base statements on what you read/ran THIS turn. Do not infer contents of files you did not open.
44
44
  3. EXECUTING: smallest correct change; never widen scope or mask failures (no \`|| true\`, skipped tests, or swallowed catch).
45
- 4. WHEN UNSURE: verify and cross-check BEFORE concluding. Bugs need a reproduction; reading code is not proof.
45
+ 4. WHEN UNSURE: verify and cross-check BEFORE concluding. Reading code is not proof reproduce the bug.
46
46
  5. REPORTING: answer ONLY what was asked. Every fact or file:line MUST come from this turn; else label "unverified"; do not guess. Synthesize evidence gracefully — do NOT dump massive verbatim tool outputs into the final answer. Cite concise file:line references. Never claim a build/test ran, or describe edits, you did not actually do this turn; if a check can't run, fix it or say so — don't imply success.
47
47
 
48
48
  6. LANGUAGE: Reply in user's detected language for final output. Internal reasoning, tools, and code remain in English.
49
49
 
50
- 7. ANTI-MÙ / COMPACTION: After seeing "[pre-compaction warning at step..." or "[context compacted at step...", decide if you need full prior tool results. Emit PRESERVE_FULL_CONTEXT for full veto this turn, or the lighter KEEP_TOOL_IDS: id1,id2 (ids from prior stub "(id=...)") to protect only high-value results (read_file/grep on src/PLAN/error etc are auto-protected). Use the ee_query tool with "tool-artifact id=XXX" for on-demand full re-hydrate of elided ones. Self-check "task finished?" / "compacted yet?". Use EE checkpoints.
50
+ 7. ANTI-MÙ / COMPACTION: On compaction, emit PRESERVE_FULL_CONTEXT (veto) or KEEP_TOOL_IDS (from stub id=) to protect results. Use ee_query with "tool-artifact id=XXX" to re-hydrate. Self-check via EE checkpoints. Suggest /compact near tool limits.
51
51
 
52
52
  8. GIT SAFETY: never push on red — run the check, await its result in a SEPARATE step, confirm 0 failures, then push. Never \`git add -A\`/\`commit -a\`; stage explicitly so secrets (.env, .muonroi-cli/, keys) aren't committed. Never \`--no-verify\`.
53
53
 
54
+ 9. VERIFICATION: when finishing a task, ALWAYS self-verify your work. Use the \`selfverify_*\` native tools (start/status/result) to run the QA harness which drives the live TUI like a real user to catch regressions that unit tests can't.
55
+
54
56
  [END CONTRACT — instructions follow]`;
55
57
  /**
56
58
  * Build the contract block for insertion at the front of the system prompt.
@@ -47,8 +47,8 @@ describe("AGENT_OPERATING_CONTRACT", () => {
47
47
  expect(AGENT_OPERATING_CONTRACT).toMatch(/push on red|never push/i);
48
48
  expect(AGENT_OPERATING_CONTRACT).toMatch(/git add -A|stage explicitly/i);
49
49
  });
50
- it("stays compact (under 1900 chars) to preserve attention budget on every turn (git-safety rule added)", () => {
51
- expect(AGENT_OPERATING_CONTRACT.length).toBeLessThan(1900);
50
+ it("stays compact (under 2000 chars) to preserve attention budget on every turn (git-safety rule added)", () => {
51
+ expect(AGENT_OPERATING_CONTRACT.length).toBeLessThan(2000);
52
52
  });
53
53
  });
54
54
  describe("buildContractSection", () => {
@@ -27,7 +27,7 @@ import type { ShellKind } from "../utils/shell.js";
27
27
  * Wrapped with the `[CRITICAL TOOL-USE RULES ...]` marker so the model knows
28
28
  * to treat these as overrides to anything that follows.
29
29
  */
30
- export declare const CHEAP_MODEL_PLAYBOOK = "[CRITICAL TOOL-USE RULES \u2014 read before invoking any tool; these override defaults that follow]\n\n1. Bash output is AUTOMATICALLY cached. Every `bash` call returns a `run_id`\n (e.g. `bash-1`) you can re-query via `bash_output_get(run_id, mode=tail|head|grep|lines)`.\n - When you want only the last N lines: do NOT pipe `| tail -N`. Run the\n bare command, then call `bash_output_get(run_id, mode=tail, lines=N)`.\n - Same for `| head`, `| grep PATTERN`, `> file`. Pipes/redirects HIDE\n the full output from the cache; `bash_output_get` reads from the cache\n without re-running.\n - This applies to EVERY bash call, not just retries.\n - To VIEW a file use `read_file` (start_line/end_line) \u2014 never sed/cat a\n file. `bash_output_get` is for COMMAND output, not files.\n\n2. Before reading more than 3 files to understand a topic, delegate to\n `task(agent=\"explore\")`. The sub-agent returns a compressed summary;\n you save reading tokens.\n\n3. Use the `grep` tool (ripgrep) for content search \u2014 NOT `bash` with\n `grep` / `find` piped.\n\n4. When a tool returns `ERROR: ...`, do NOT retry the identical call.\n Pick a different tool, change inputs meaningfully, or stop and report.\n\n5. Fix the ROOT CAUSE, never mask a failure to make it \"pass\"\n (`continue-on-error`, swallowed try/catch, skipped/deleted test, `|| true`).\n If a step fails from a missing secret/config, make it CONDITIONAL (skip when\n absent) so it still runs when present \u2014 do NOT blanket-ignore it.\n\n6. For a build / CI / test failure, read the ACTUAL failure log or stack trace\n BEFORE hypothesizing \u2014 fix the real error, not a guess from source alone.\n\n7. ANTI-M\u00D9 / COMPACTION (for long sessions): On pre-warn or \"[context compacted at step...\", emit PRESERVE_FULL_CONTEXT (full veto) or lighter KEEP_TOOL_IDS: id1,id2 (from stub id=) to protect specific high-value results. read_file/grep/lsp/bash on src/PLAN/error are auto-kept (idea 1). Use ee.query tool with \"tool-artifact id=XXX\" for on-demand full. Self-check \"task finished?\" / \"compacted yet?\". Use EE checkpoints.\n\n[END CRITICAL TOOL-USE RULES \u2014 your regular instructions begin below]\n\n";
30
+ export declare const CHEAP_MODEL_PLAYBOOK = "[CRITICAL TOOL-USE RULES \u2014 read before invoking any tool; these override defaults that follow]\n\n1. Bash output is AUTOMATICALLY cached. Every `bash` call returns a `run_id`\n (e.g. `bash-1`) you can re-query via `bash_output_get(run_id, mode=tail|head|grep|lines)`.\n - When you want only the last N lines: do NOT pipe `| tail -N`. Run the\n bare command, then call `bash_output_get(run_id, mode=tail, lines=N)`.\n - Same for `| head`, `| grep PATTERN`, `> file`. Pipes/redirects HIDE\n the full output from the cache; `bash_output_get` reads from the cache\n without re-running.\n - This applies to EVERY bash call, not just retries.\n - To VIEW a file use `read_file` (start_line/end_line) \u2014 never sed/cat a\n file. `bash_output_get` is for COMMAND output, not files.\n\n2. Before reading more than 3 files to understand a topic, delegate to\n `task(agent=\"explore\")`. The sub-agent returns a compressed summary;\n you save reading tokens.\n\n3. Use the `grep` tool (ripgrep) for content search \u2014 NOT `bash` with\n `grep` / `find` piped.\n\n4. When a tool returns `ERROR: ...`, do NOT retry the identical call.\n Pick a different tool, change inputs meaningfully, or stop and report.\n\n5. Fix the ROOT CAUSE, never mask a failure to make it \"pass\"\n (`continue-on-error`, swallowed try/catch, skipped/deleted test, `|| true`).\n If a step fails from a missing secret/config, make it CONDITIONAL (skip when\n absent) so it still runs when present \u2014 do NOT blanket-ignore it.\n\n6. For a build / CI / test failure, read the ACTUAL failure log or stack trace\n BEFORE hypothesizing \u2014 fix the real error, not a guess from source alone.\n\n7. ANTI-M\u00D9 / COMPACTION (for long sessions): On pre-warn or \"[context compacted at step...\", emit PRESERVE_FULL_CONTEXT (full veto) or lighter KEEP_TOOL_IDS: id1,id2 (from stub id=) to protect specific high-value results. read_file/grep/lsp/bash on src/PLAN/error are auto-kept (idea 1). Use ee.query tool with \"tool-artifact id=XXX\" for on-demand full. Self-check \"task finished?\" / \"compacted yet?\". Use EE checkpoints. If you are reaching tool/step limits in a long session, suggest the user run \"/compact\" in the chat to compress this session's history.\n\n[END CRITICAL TOOL-USE RULES \u2014 your regular instructions begin below]\n\n";
31
31
  /**
32
32
  * Predicate gating playbook injection.
33
33
  *
@@ -56,7 +56,7 @@ export const CHEAP_MODEL_PLAYBOOK = `[CRITICAL TOOL-USE RULES — read before in
56
56
  6. For a build / CI / test failure, read the ACTUAL failure log or stack trace
57
57
  BEFORE hypothesizing — fix the real error, not a guess from source alone.
58
58
 
59
- 7. ANTI-MÙ / COMPACTION (for long sessions): On pre-warn or "[context compacted at step...", emit PRESERVE_FULL_CONTEXT (full veto) or lighter KEEP_TOOL_IDS: id1,id2 (from stub id=) to protect specific high-value results. read_file/grep/lsp/bash on src/PLAN/error are auto-kept (idea 1). Use ee.query tool with "tool-artifact id=XXX" for on-demand full. Self-check "task finished?" / "compacted yet?". Use EE checkpoints.
59
+ 7. ANTI-MÙ / COMPACTION (for long sessions): On pre-warn or "[context compacted at step...", emit PRESERVE_FULL_CONTEXT (full veto) or lighter KEEP_TOOL_IDS: id1,id2 (from stub id=) to protect specific high-value results. read_file/grep/lsp/bash on src/PLAN/error are auto-kept (idea 1). Use ee.query tool with "tool-artifact id=XXX" for on-demand full. Self-check "task finished?" / "compacted yet?". Use EE checkpoints. If you are reaching tool/step limits in a long session, suggest the user run "/compact" in the chat to compress this session's history.
60
60
 
61
61
  [END CRITICAL TOOL-USE RULES — your regular instructions begin below]
62
62
 
@@ -25,7 +25,7 @@ import type { TaskType } from "./types.js";
25
25
  * Universal anti-ramble convergence block — applies to every task type.
26
26
  * Kept tight; the per-task addendum below specialises it.
27
27
  */
28
- export declare const CHEAP_MODEL_CONVERGENCE = "[CONVERGENCE \u2014 minimise tool calls; the system prompt + tools are re-sent every call, so each extra step is expensive]\n\n- Plan the FEWEST reads you need, then read the specific file/section directly.\n Do NOT broad-grep, re-read a file you already read, or explore \"just in case\".\n- The moment you have enough to act, STOP investigating and make the change.\n- Make the SMALLEST correct change for the request; do not widen scope.\n- Finish the action before you answer \u2014 never stop mid-step (e.g. \"I'm verifying\u2026\").\n When done, state completion in ONE line (what changed + that it's verified);\n no recap, no next-steps padding.\n- GROUND every claim in what you actually read or ran THIS turn: cite real\n file:line, and never invent counts, line numbers, names, or bugs. If a number\n (test/file count) is not verified by a command you ran, run the check or mark\n it \"unverified\" \u2014 do NOT guess a value or assert a finding you did not observe.\n- ANTI-M\u00D9: After compaction note or pre-warn, emit PRESERVE_FULL_CONTEXT (full) or KEEP_TOOL_IDS: id1,id2 to protect high-value (auto for read_file/grep on src/PLAN/error). Use the ee_query tool with \"tool-artifact id=XXX\" for on-demand full re-hydrate. Recall checkpoints. ";
28
+ export declare const CHEAP_MODEL_CONVERGENCE = "[CONVERGENCE \u2014 minimise tool calls; the system prompt + tools are re-sent every call, so each extra step is expensive]\n\n- Plan the FEWEST reads you need, then read the specific file/section directly.\n Do NOT broad-grep, re-read a file you already read, or explore \"just in case\".\n- The moment you have enough to act, STOP investigating and make the change.\n- Make the SMALLEST correct change for the request; do not widen scope.\n- Finish the action before you answer \u2014 never stop mid-step (e.g. \"I'm verifying\u2026\").\n When done, state completion in ONE line (what changed + that it's verified);\n no recap, no next-steps padding.\n- GROUND every claim in what you actually read or ran THIS turn: cite real\n file:line, and never invent counts, line numbers, names, or bugs. If a number\n (test/file count) is not verified by a command you ran, run the check or mark\n it \"unverified\" \u2014 do NOT guess a value or assert a finding you did not observe.\n- ANTI-M\u00D9: After compaction note or pre-warn, emit PRESERVE_FULL_CONTEXT (full) or KEEP_TOOL_IDS: id1,id2 to protect high-value (auto for read_file/grep on src/PLAN/error). Use the ee_query tool with \"tool-artifact id=XXX\" for on-demand full re-hydrate. Recall checkpoints. If you are reaching tool/step limits in a long session, suggest the user run \"/compact\" to compress history. ";
29
29
  /**
30
30
  * Map a sub-agent role (agentKey) to the workbook TaskType that best matches
31
31
  * its job. Sub-agents never run PIL Layer 1, so they have no classifier-derived
@@ -36,7 +36,7 @@ export const CHEAP_MODEL_CONVERGENCE = `[CONVERGENCE — minimise tool calls; th
36
36
  file:line, and never invent counts, line numbers, names, or bugs. If a number
37
37
  (test/file count) is not verified by a command you ran, run the check or mark
38
38
  it "unverified" — do NOT guess a value or assert a finding you did not observe.
39
- - ANTI-MÙ: After compaction note or pre-warn, emit PRESERVE_FULL_CONTEXT (full) or KEEP_TOOL_IDS: id1,id2 to protect high-value (auto for read_file/grep on src/PLAN/error). Use the ee_query tool with "tool-artifact id=XXX" for on-demand full re-hydrate. Recall checkpoints. `;
39
+ - ANTI-MÙ: After compaction note or pre-warn, emit PRESERVE_FULL_CONTEXT (full) or KEEP_TOOL_IDS: id1,id2 to protect high-value (auto for read_file/grep on src/PLAN/error). Use the ee_query tool with "tool-artifact id=XXX" for on-demand full re-hydrate. Recall checkpoints. If you are reaching tool/step limits in a long session, suggest the user run "/compact" to compress history. `;
40
40
  /**
41
41
  * Per-task-type addenda. Each is 1–2 tight lines targeting that type's most
42
42
  * common budget-model failure mode. Types not listed fall back to the
@@ -5,6 +5,16 @@
5
5
  * Default OFF during rollout; flip to ON after dual-run validation.
6
6
  */
7
7
  export declare function isUnifiedPilEnabled(): boolean;
8
+ /**
9
+ * MUONROI_PIL_UNIFIED_BUDGET_MS: client-side timeout for the unified
10
+ * /api/pil-context call in Layer 1 Pass 3. The server's uncached path is
11
+ * classifier-bound (~1.5-3s; its own classifier timeout is 3500ms), so the
12
+ * legacy 1500ms budget aborted before the server could ever answer — the
13
+ * unified path then ALWAYS fell back to legacy, making MUONROI_PIL_UNIFIED=1 a
14
+ * no-op. Default 3500ms clears realistic uncached latency; cache hits return in
15
+ * ~1ms regardless. Clamped to [1000, 8000].
16
+ */
17
+ export declare function getUnifiedPilBudgetMs(): number;
8
18
  /**
9
19
  * MUONROI_LLM_FIRST_CLASSIFY: model-first Layer-1 classification. When enabled
10
20
  * (default), the configured model classifies taskType/intentKind/style at the
@@ -14,5 +24,15 @@ export declare function isUnifiedPilEnabled(): boolean;
14
24
  * (the orchestrator does this on the main path); without it, the cascade runs.
15
25
  */
16
26
  export declare function isLlmFirstClassifyEnabled(): boolean;
27
+ /**
28
+ * G3 (b1): on the model-first classify path, also fetch the unified pil-context
29
+ * brain so layer3 renders the rich source="unified" injection (server-attributed
30
+ * t0/t2 + t1_rules + the rateable ledger) instead of falling through to its
31
+ * legacy dense-only /api/search round-trip. Default ON; set "0" to revert to
32
+ * the legacy layer3 injection on the default path. Independent of
33
+ * MUONROI_PIL_UNIFIED (which gates the OFFLINE cascade's unified pass).
34
+ */
35
+ export declare function isLlmFirstBrainEnabled(): boolean;
17
36
  export declare function isDiscoveryEnabled(): boolean;
18
37
  export declare function getMaxInterviewQuestions(): number;
38
+ export declare function isPonytailModeEnabled(): boolean;
@@ -11,6 +11,21 @@ export function isUnifiedPilEnabled() {
11
11
  return true;
12
12
  return false;
13
13
  }
14
+ /**
15
+ * MUONROI_PIL_UNIFIED_BUDGET_MS: client-side timeout for the unified
16
+ * /api/pil-context call in Layer 1 Pass 3. The server's uncached path is
17
+ * classifier-bound (~1.5-3s; its own classifier timeout is 3500ms), so the
18
+ * legacy 1500ms budget aborted before the server could ever answer — the
19
+ * unified path then ALWAYS fell back to legacy, making MUONROI_PIL_UNIFIED=1 a
20
+ * no-op. Default 3500ms clears realistic uncached latency; cache hits return in
21
+ * ~1ms regardless. Clamped to [1000, 8000].
22
+ */
23
+ export function getUnifiedPilBudgetMs() {
24
+ const raw = Number(process.env.MUONROI_PIL_UNIFIED_BUDGET_MS);
25
+ if (!Number.isFinite(raw))
26
+ return 3500;
27
+ return Math.max(1000, Math.min(8000, Math.trunc(raw)));
28
+ }
14
29
  /**
15
30
  * MUONROI_LLM_FIRST_CLASSIFY: model-first Layer-1 classification. When enabled
16
31
  * (default), the configured model classifies taskType/intentKind/style at the
@@ -22,6 +37,17 @@ export function isUnifiedPilEnabled() {
22
37
  export function isLlmFirstClassifyEnabled() {
23
38
  return process.env.MUONROI_LLM_FIRST_CLASSIFY !== "0";
24
39
  }
40
+ /**
41
+ * G3 (b1): on the model-first classify path, also fetch the unified pil-context
42
+ * brain so layer3 renders the rich source="unified" injection (server-attributed
43
+ * t0/t2 + t1_rules + the rateable ledger) instead of falling through to its
44
+ * legacy dense-only /api/search round-trip. Default ON; set "0" to revert to
45
+ * the legacy layer3 injection on the default path. Independent of
46
+ * MUONROI_PIL_UNIFIED (which gates the OFFLINE cascade's unified pass).
47
+ */
48
+ export function isLlmFirstBrainEnabled() {
49
+ return process.env.MUONROI_LLM_FIRST_BRAIN !== "0";
50
+ }
25
51
  export function isDiscoveryEnabled() {
26
52
  return process.env.MUONROI_PIL_DISCOVERY !== "0";
27
53
  }
@@ -33,4 +59,13 @@ export function getMaxInterviewQuestions() {
33
59
  const v = Number(process.env.MUONROI_PIL_MAX_QUESTIONS);
34
60
  return Number.isFinite(v) && v >= 1 && v <= 5 ? v : 3;
35
61
  }
62
+ export function isPonytailModeEnabled() {
63
+ if (process.env.MUONROI_PONYTAIL_DISABLE === "1")
64
+ return false;
65
+ if (process.env.MUONROI_PONYTAIL_DISABLE === "0")
66
+ return true;
67
+ if (process.env.VITEST === "true" || process.env.NODE_ENV === "test")
68
+ return false;
69
+ return true;
70
+ }
36
71
  //# sourceMappingURL=config.js.map