muonroi-cli 1.4.1 → 1.5.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 (172) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +122 -122
  3. package/dist/packages/agent-harness-core/src/predicate.d.ts +1 -1
  4. package/dist/src/agent-harness/__tests__/mock-model.spec.js +48 -1
  5. package/dist/src/agent-harness/mock-model.d.ts +11 -0
  6. package/dist/src/agent-harness/mock-model.js +21 -0
  7. package/dist/src/cli/cost-forensics.js +12 -12
  8. package/dist/src/council/__tests__/clarification-prompt.test.js +51 -0
  9. package/dist/src/council/__tests__/clarifier-ready-gate.test.js +32 -0
  10. package/dist/src/council/__tests__/decisions-lock.test.js +17 -1
  11. package/dist/src/council/__tests__/oauth-reachable.test.d.ts +1 -0
  12. package/dist/src/council/__tests__/oauth-reachable.test.js +31 -0
  13. package/dist/src/council/__tests__/parse-outcome-fallback.test.js +11 -0
  14. package/dist/src/council/clarifier.js +9 -1
  15. package/dist/src/council/debate.js +5 -1
  16. package/dist/src/council/decisions-lock.js +3 -3
  17. package/dist/src/council/index.js +12 -5
  18. package/dist/src/council/leader.d.ts +0 -17
  19. package/dist/src/council/leader.js +22 -15
  20. package/dist/src/council/planner.js +1 -1
  21. package/dist/src/council/prompts.js +63 -57
  22. package/dist/src/council/types.d.ts +7 -0
  23. package/dist/src/ee/__tests__/ee-onboarding.test.d.ts +1 -0
  24. package/dist/src/ee/__tests__/ee-onboarding.test.js +32 -0
  25. package/dist/src/ee/auth.d.ts +9 -0
  26. package/dist/src/ee/auth.js +19 -0
  27. package/dist/src/ee/ee-onboarding.d.ts +5 -0
  28. package/dist/src/ee/ee-onboarding.js +76 -0
  29. package/dist/src/generated/version.d.ts +1 -1
  30. package/dist/src/generated/version.js +1 -1
  31. package/dist/src/headless/output.js +6 -4
  32. package/dist/src/headless/output.test.js +4 -3
  33. package/dist/src/index.js +20 -1
  34. package/dist/src/mcp/__tests__/auto-setup.test.js +74 -0
  35. package/dist/src/mcp/__tests__/client-pool.spec.d.ts +1 -0
  36. package/dist/src/mcp/__tests__/client-pool.spec.js +98 -0
  37. package/dist/src/mcp/__tests__/parallel-build.spec.d.ts +1 -0
  38. package/dist/src/mcp/__tests__/parallel-build.spec.js +67 -0
  39. package/dist/src/mcp/__tests__/smart-filter.test.js +56 -0
  40. package/dist/src/mcp/auto-setup.js +56 -2
  41. package/dist/src/mcp/client-pool.d.ts +46 -0
  42. package/dist/src/mcp/client-pool.js +212 -0
  43. package/dist/src/mcp/oauth-callback.js +2 -2
  44. package/dist/src/mcp/parse-headers.test.js +14 -14
  45. package/dist/src/mcp/runtime.d.ts +28 -0
  46. package/dist/src/mcp/runtime.js +117 -51
  47. package/dist/src/mcp/self-verify-runner.d.ts +14 -0
  48. package/dist/src/mcp/self-verify-runner.js +38 -0
  49. package/dist/src/mcp/setup-guide-text.d.ts +9 -0
  50. package/dist/src/mcp/setup-guide-text.js +84 -0
  51. package/dist/src/mcp/smart-filter.js +49 -0
  52. package/dist/src/mcp/smoke.test.js +43 -43
  53. package/dist/src/mcp/tools-server.d.ts +7 -0
  54. package/dist/src/mcp/tools-server.js +19 -22
  55. package/dist/src/models/catalog.json +349 -349
  56. package/dist/src/ops/__tests__/doctor-ee-health.test.js +21 -0
  57. package/dist/src/ops/doctor.d.ts +3 -2
  58. package/dist/src/ops/doctor.js +47 -11
  59. package/dist/src/ops/doctor.test.js +4 -3
  60. package/dist/src/orchestrator/__tests__/mcp-capability-block.test.d.ts +1 -0
  61. package/dist/src/orchestrator/__tests__/mcp-capability-block.test.js +39 -0
  62. package/dist/src/orchestrator/__tests__/project-stack.test.d.ts +1 -0
  63. package/dist/src/orchestrator/__tests__/project-stack.test.js +65 -0
  64. package/dist/src/orchestrator/batch-turn-runner.js +7 -11
  65. package/dist/src/orchestrator/message-processor.js +57 -27
  66. package/dist/src/orchestrator/orchestrator.js +26 -0
  67. package/dist/src/orchestrator/prompts.d.ts +51 -0
  68. package/dist/src/orchestrator/prompts.js +257 -134
  69. package/dist/src/orchestrator/scope-ceiling.js +6 -1
  70. package/dist/src/orchestrator/stream-runner.js +20 -15
  71. package/dist/src/orchestrator/text-tool-call-detector.test.js +13 -13
  72. package/dist/src/pil/__tests__/clarity-gate.test.js +24 -215
  73. package/dist/src/pil/__tests__/config.test.js +1 -17
  74. package/dist/src/pil/__tests__/discovery.test.js +144 -11
  75. package/dist/src/pil/__tests__/layer1-intent-trace.test.js +7 -2
  76. package/dist/src/pil/__tests__/layer1-intent.test.js +3 -0
  77. package/dist/src/pil/__tests__/layer16-clarity.test.js +32 -116
  78. package/dist/src/pil/__tests__/layer4-gsd.test.js +37 -0
  79. package/dist/src/pil/__tests__/layer6-output.test.js +137 -18
  80. package/dist/src/pil/__tests__/llm-classify.test.js +49 -2
  81. package/dist/src/pil/agent-operating-contract.d.ts +1 -1
  82. package/dist/src/pil/agent-operating-contract.js +2 -0
  83. package/dist/src/pil/agent-operating-contract.test.js +7 -2
  84. package/dist/src/pil/cheap-model-playbook.js +35 -35
  85. package/dist/src/pil/cheap-model-workbooks.js +16 -13
  86. package/dist/src/pil/clarity-gate.d.ts +21 -19
  87. package/dist/src/pil/clarity-gate.js +26 -153
  88. package/dist/src/pil/config.d.ts +9 -1
  89. package/dist/src/pil/config.js +15 -4
  90. package/dist/src/pil/discovery.js +211 -136
  91. package/dist/src/pil/layer1-intent.d.ts +12 -0
  92. package/dist/src/pil/layer1-intent.js +283 -38
  93. package/dist/src/pil/layer1-intent.test.js +210 -4
  94. package/dist/src/pil/layer16-clarity.d.ts +25 -11
  95. package/dist/src/pil/layer16-clarity.js +19 -306
  96. package/dist/src/pil/layer4-gsd.js +18 -6
  97. package/dist/src/pil/layer6-output.d.ts +2 -0
  98. package/dist/src/pil/layer6-output.js +137 -22
  99. package/dist/src/pil/llm-classify.d.ts +26 -0
  100. package/dist/src/pil/llm-classify.js +34 -5
  101. package/dist/src/pil/native-capabilities-workbook.d.ts +1 -1
  102. package/dist/src/pil/native-capabilities-workbook.js +82 -76
  103. package/dist/src/pil/schema.d.ts +8 -0
  104. package/dist/src/pil/schema.js +12 -1
  105. package/dist/src/pil/task-tier-map.js +4 -0
  106. package/dist/src/pil/types.d.ts +11 -1
  107. package/dist/src/product-loop/done-gate.js +3 -3
  108. package/dist/src/product-loop/loop-driver.js +18 -18
  109. package/dist/src/product-loop/progress-snapshot.js +4 -4
  110. package/dist/src/providers/auth/gemini-oauth.js +6 -15
  111. package/dist/src/providers/auth/grok-oauth.js +6 -15
  112. package/dist/src/providers/auth/openai-oauth.js +6 -15
  113. package/dist/src/providers/mcp-vision-bridge.js +48 -48
  114. package/dist/src/reporter/index.js +1 -1
  115. package/dist/src/scaffold/bb-ecosystem-apply.js +47 -47
  116. package/dist/src/scaffold/bb-quality-gate.js +5 -5
  117. package/dist/src/scaffold/continuation-prompt.js +60 -60
  118. package/dist/src/scaffold/init-new.js +453 -453
  119. package/dist/src/self-qa/__tests__/scenario-planner.test.js +3 -3
  120. package/dist/src/self-qa/agentic-loop.js +24 -19
  121. package/dist/src/self-qa/spec-emitter.js +26 -23
  122. package/dist/src/storage/__tests__/migrations.test.js +2 -2
  123. package/dist/src/storage/interaction-log.js +5 -5
  124. package/dist/src/storage/migrations.js +122 -122
  125. package/dist/src/storage/sessions.js +42 -42
  126. package/dist/src/storage/transcript.js +91 -84
  127. package/dist/src/storage/usage.js +14 -14
  128. package/dist/src/storage/workspaces.js +12 -12
  129. package/dist/src/tools/__tests__/native-tools.test.d.ts +1 -0
  130. package/dist/src/tools/__tests__/native-tools.test.js +53 -0
  131. package/dist/src/tools/git-safety.d.ts +61 -0
  132. package/dist/src/tools/git-safety.js +141 -0
  133. package/dist/src/tools/git-safety.test.d.ts +1 -0
  134. package/dist/src/tools/git-safety.test.js +111 -0
  135. package/dist/src/tools/native-tools.d.ts +31 -0
  136. package/dist/src/tools/native-tools.js +273 -0
  137. package/dist/src/tools/registry-git-safety.test.d.ts +7 -0
  138. package/dist/src/tools/registry-git-safety.test.js +92 -0
  139. package/dist/src/tools/registry.js +39 -4
  140. package/dist/src/ui/__tests__/markdown-render.test.d.ts +1 -0
  141. package/dist/src/ui/__tests__/markdown-render.test.js +48 -0
  142. package/dist/src/ui/app.js +0 -0
  143. package/dist/src/ui/components/message-view.js +4 -1
  144. package/dist/src/ui/components/structured-response-view.js +7 -3
  145. package/dist/src/ui/components/tool-group.js +7 -1
  146. package/dist/src/ui/markdown-render.d.ts +41 -0
  147. package/dist/src/ui/markdown-render.js +223 -0
  148. package/dist/src/ui/markdown.d.ts +10 -0
  149. package/dist/src/ui/markdown.js +12 -35
  150. package/dist/src/ui/slash/council-inspect.js +4 -4
  151. package/dist/src/ui/slash/export.js +4 -4
  152. package/dist/src/ui/utils/text.d.ts +8 -0
  153. package/dist/src/ui/utils/text.js +16 -0
  154. package/dist/src/ui/utils/text.test.d.ts +1 -0
  155. package/dist/src/ui/utils/text.test.js +23 -0
  156. package/dist/src/usage/ledger.js +48 -15
  157. package/dist/src/utils/__tests__/footprint-gitignore.test.d.ts +1 -0
  158. package/dist/src/utils/__tests__/footprint-gitignore.test.js +50 -0
  159. package/dist/src/utils/clipboard-image.js +23 -23
  160. package/dist/src/utils/open-url.d.ts +56 -0
  161. package/dist/src/utils/open-url.js +58 -0
  162. package/dist/src/utils/open-url.test.d.ts +1 -0
  163. package/dist/src/utils/open-url.test.js +86 -0
  164. package/dist/src/utils/settings.d.ts +12 -0
  165. package/dist/src/utils/settings.js +48 -0
  166. package/dist/src/utils/side-question.js +2 -2
  167. package/dist/src/utils/skills.js +3 -3
  168. package/dist/src/verify/__tests__/coverage-parsers.test.js +30 -30
  169. package/dist/src/verify/environment.js +2 -1
  170. package/package.json +1 -1
  171. package/dist/src/pil/layer16-clarity.test.js +0 -31
  172. /package/dist/src/{pil/layer16-clarity.test.d.ts → council/__tests__/clarification-prompt.test.d.ts} +0 -0
package/LICENSE CHANGED
@@ -1,21 +1,21 @@
1
- MIT License
2
-
3
- Copyright (c) 2026 muonroi
4
-
5
- Permission is hereby granted, free of charge, to any person obtaining a copy
6
- of this software and associated documentation files (the "Software"), to deal
7
- in the Software without restriction, including without limitation the rights
8
- to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
- copies of the Software, and to permit persons to whom the Software is
10
- furnished to do so, subject to the following conditions:
11
-
12
- The above copyright notice and this permission notice shall be included in all
13
- copies or substantial portions of the Software.
14
-
15
- THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
- IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
- FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
- AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
- LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
- OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
- SOFTWARE.
1
+ MIT License
2
+
3
+ Copyright (c) 2026 muonroi
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,122 +1,122 @@
1
- <p align="center">
2
- <h1 align="center">muonroi-cli</h1>
3
- <p align="center">
4
- <em>An AI coding agent where models argue with each other before answering.</em>
5
- </p>
6
- <p align="center">
7
- <a href="https://github.com/muonroi/muonroi-cli/actions/workflows/ci-matrix.yml"><img alt="CI" src="https://github.com/muonroi/muonroi-cli/actions/workflows/ci-matrix.yml/badge.svg"></a>
8
- <a href="https://www.npmjs.com/package/muonroi-cli"><img alt="npm" src="https://img.shields.io/npm/v/muonroi-cli.svg"></a>
9
- <img alt="Providers" src="https://img.shields.io/badge/providers-7%20supported-blue">
10
- <img alt="License" src="https://img.shields.io/badge/license-MIT-yellow">
11
- <img alt="Runtime" src="https://img.shields.io/badge/runtime-Bun%201.3%2B-orange">
12
- </p>
13
- </p>
14
-
15
- ---
16
-
17
- > Routes each task to the optimal model, runs adversarial multi-model debates for high-stakes decisions, and persists behavioral memory across sessions. Bring your own API keys. Total cost: ~$5/month.
18
-
19
- <p align="center">
20
- <img src="https://raw.githubusercontent.com/muonroi/muonroi-cli/master/docs/demo.gif" alt="Council debate — REST vs gRPC decision" width="840" />
21
- </p>
22
-
23
- ## Quick Start
24
-
25
- ### Install
26
-
27
- **Recommended — prebuilt standalone binary (zero runtime deps, no prereqs):**
28
-
29
- Linux / macOS:
30
- ```bash
31
- curl -fsSL https://raw.githubusercontent.com/muonroi/muonroi-cli/master/install.sh | bash
32
- ```
33
-
34
- Windows PowerShell:
35
- ```powershell
36
- irm https://raw.githubusercontent.com/muonroi/muonroi-cli/master/install.ps1 | iex
37
- ```
38
-
39
- The installers download a `bun --compile` binary from GitHub Releases — single executable, all native deps bundled, no Node/Bun/build tools required.
40
-
41
- **Bun runtime (requires Bun ≥ 1.x):**
42
- ```bash
43
- bun add -g muonroi-cli
44
- muonroi-cli
45
- ```
46
-
47
- > **⚠ Why no `npm install -g`?** The TUI engine (`@opentui/core`) uses Bun-only `import ... with { type: "file" }` for Tree-sitter syntax highlight queries (`.scm` files). Node ESM cannot load these, so running under bare Node throws `ERR_UNKNOWN_FILE_EXTENSION`. Use the standalone installer (above) or `bun add -g`.
48
-
49
- ### First run
50
-
51
- On first launch the CLI shows a wizard that:
52
- 1. Lists supported providers (currently **DeepSeek** and **SiliconFlow**).
53
- 2. Offers four ways to add credentials: paste an API key, import an encrypted bundle (`keys export`/`keys import`), sync from Bitwarden, or skip and add later via `/providers` inside the TUI.
54
-
55
- After setup, role routing auto-balances across enabled providers:
56
-
57
- ```json
58
- // ~/.muonroi-cli/user-settings.json (generated by the wizard — edit if needed)
59
- {
60
- "defaultProvider": "deepseek",
61
- "providers": {
62
- "deepseek": { "enabled": true },
63
- "siliconflow": { "enabled": true }
64
- },
65
- "roleModels": {
66
- "leader": "deepseek-v4-pro",
67
- "implement": "deepseek-v4-flash",
68
- "verify": "deepseek-v4-pro",
69
- "research": "deepseek-v4-flash"
70
- }
71
- }
72
- ```
73
-
74
- ### Moving keys between devices
75
-
76
- ```bash
77
- # Source device — encrypts every stored key into one passphrase-protected file
78
- muonroi-cli keys export ~/muonroi-keys.json
79
-
80
- # Move the file via any channel (USB, Drive, AirDrop, email...)
81
-
82
- # Target device — same passphrase rehydrates the OS keychain
83
- muonroi-cli keys import ~/muonroi-keys.json
84
- ```
85
-
86
- Inside the TUI press `/providers` then `B` to sync directly from a Bitwarden vault instead.
87
-
88
- ### Updates
89
-
90
- The CLI checks npm once per day on startup and prompts when a newer version is available. Set `autoUpdate: true` in `user-settings.json` to skip the prompt and update silently. Manual: `muonroi-cli update`.
91
-
92
- ## Documentation
93
-
94
- Full documentation at **[docs.muonroi.com/docs/cli](https://docs.muonroi.com/docs/cli/overview)**
95
-
96
- | Topic | Link |
97
- |---|---|
98
- | Overview & architecture | [CLI Overview](https://docs.muonroi.com/docs/cli/overview) |
99
- | Multi-Model Council | [Council Debate Guide](https://docs.muonroi.com/docs/cli/guides/council-debate) |
100
- | Prompt Intelligence Layer | [PIL Pipeline Guide](https://docs.muonroi.com/docs/cli/guides/pil-pipeline) |
101
- | Experience Engine | [Experience Engine Guide](https://docs.muonroi.com/docs/cli/guides/experience-engine) |
102
- | Agent Harness | [Agent Harness Guide](https://docs.muonroi.com/docs/cli/guides/agent-harness) |
103
- | Settings reference | [CLI Settings Reference](https://docs.muonroi.com/docs/cli/reference/cli-settings-reference) |
104
- | Commands reference | [Commands Reference](https://docs.muonroi.com/docs/cli/reference/commands-reference) |
105
- | Providers reference | [Providers Reference](https://docs.muonroi.com/docs/cli/reference/providers-reference) |
106
-
107
- ## Development
108
-
109
- ```bash
110
- git clone https://github.com/muonroi/muonroi-cli.git
111
- cd muonroi-cli && bun install
112
-
113
- bun run dev # run from source
114
- bun run typecheck # type check
115
- bun run test # vitest
116
- bun run lint # biome check
117
- bun run build:binary # standalone binary
118
- ```
119
-
120
- ## License
121
-
122
- MIT
1
+ <p align="center">
2
+ <h1 align="center">muonroi-cli</h1>
3
+ <p align="center">
4
+ <em>An AI coding agent where models argue with each other before answering.</em>
5
+ </p>
6
+ <p align="center">
7
+ <a href="https://github.com/muonroi/muonroi-cli/actions/workflows/ci-matrix.yml"><img alt="CI" src="https://github.com/muonroi/muonroi-cli/actions/workflows/ci-matrix.yml/badge.svg"></a>
8
+ <a href="https://www.npmjs.com/package/muonroi-cli"><img alt="npm" src="https://img.shields.io/npm/v/muonroi-cli.svg"></a>
9
+ <img alt="Providers" src="https://img.shields.io/badge/providers-7%20supported-blue">
10
+ <img alt="License" src="https://img.shields.io/badge/license-MIT-yellow">
11
+ <img alt="Runtime" src="https://img.shields.io/badge/runtime-Bun%201.3%2B-orange">
12
+ </p>
13
+ </p>
14
+
15
+ ---
16
+
17
+ > Routes each task to the optimal model, runs adversarial multi-model debates for high-stakes decisions, and persists behavioral memory across sessions. Bring your own API keys. Total cost: ~$5/month.
18
+
19
+ <p align="center">
20
+ <img src="https://raw.githubusercontent.com/muonroi/muonroi-cli/master/docs/demo.gif" alt="Council debate — REST vs gRPC decision" width="840" />
21
+ </p>
22
+
23
+ ## Quick Start
24
+
25
+ ### Install
26
+
27
+ **Recommended — prebuilt standalone binary (zero runtime deps, no prereqs):**
28
+
29
+ Linux / macOS:
30
+ ```bash
31
+ curl -fsSL https://raw.githubusercontent.com/muonroi/muonroi-cli/master/install.sh | bash
32
+ ```
33
+
34
+ Windows PowerShell:
35
+ ```powershell
36
+ irm https://raw.githubusercontent.com/muonroi/muonroi-cli/master/install.ps1 | iex
37
+ ```
38
+
39
+ The installers download a `bun --compile` binary from GitHub Releases — single executable, all native deps bundled, no Node/Bun/build tools required.
40
+
41
+ **Bun runtime (requires Bun ≥ 1.x):**
42
+ ```bash
43
+ bun add -g muonroi-cli
44
+ muonroi-cli
45
+ ```
46
+
47
+ > **⚠ Why no `npm install -g`?** The TUI engine (`@opentui/core`) uses Bun-only `import ... with { type: "file" }` for Tree-sitter syntax highlight queries (`.scm` files). Node ESM cannot load these, so running under bare Node throws `ERR_UNKNOWN_FILE_EXTENSION`. Use the standalone installer (above) or `bun add -g`.
48
+
49
+ ### First run
50
+
51
+ On first launch the CLI shows a wizard that:
52
+ 1. Lists supported providers (currently **DeepSeek** and **SiliconFlow**).
53
+ 2. Offers four ways to add credentials: paste an API key, import an encrypted bundle (`keys export`/`keys import`), sync from Bitwarden, or skip and add later via `/providers` inside the TUI.
54
+
55
+ After setup, role routing auto-balances across enabled providers:
56
+
57
+ ```json
58
+ // ~/.muonroi-cli/user-settings.json (generated by the wizard — edit if needed)
59
+ {
60
+ "defaultProvider": "deepseek",
61
+ "providers": {
62
+ "deepseek": { "enabled": true },
63
+ "siliconflow": { "enabled": true }
64
+ },
65
+ "roleModels": {
66
+ "leader": "deepseek-v4-pro",
67
+ "implement": "deepseek-v4-flash",
68
+ "verify": "deepseek-v4-pro",
69
+ "research": "deepseek-v4-flash"
70
+ }
71
+ }
72
+ ```
73
+
74
+ ### Moving keys between devices
75
+
76
+ ```bash
77
+ # Source device — encrypts every stored key into one passphrase-protected file
78
+ muonroi-cli keys export ~/muonroi-keys.json
79
+
80
+ # Move the file via any channel (USB, Drive, AirDrop, email...)
81
+
82
+ # Target device — same passphrase rehydrates the OS keychain
83
+ muonroi-cli keys import ~/muonroi-keys.json
84
+ ```
85
+
86
+ Inside the TUI press `/providers` then `B` to sync directly from a Bitwarden vault instead.
87
+
88
+ ### Updates
89
+
90
+ The CLI checks npm once per day on startup and prompts when a newer version is available. Set `autoUpdate: true` in `user-settings.json` to skip the prompt and update silently. Manual: `muonroi-cli update`.
91
+
92
+ ## Documentation
93
+
94
+ Full documentation at **[docs.muonroi.com/docs/cli](https://docs.muonroi.com/docs/cli/overview)**
95
+
96
+ | Topic | Link |
97
+ |---|---|
98
+ | Overview & architecture | [CLI Overview](https://docs.muonroi.com/docs/cli/overview) |
99
+ | Multi-Model Council | [Council Debate Guide](https://docs.muonroi.com/docs/cli/guides/council-debate) |
100
+ | Prompt Intelligence Layer | [PIL Pipeline Guide](https://docs.muonroi.com/docs/cli/guides/pil-pipeline) |
101
+ | Experience Engine | [Experience Engine Guide](https://docs.muonroi.com/docs/cli/guides/experience-engine) |
102
+ | Agent Harness | [Agent Harness Guide](https://docs.muonroi.com/docs/cli/guides/agent-harness) |
103
+ | Settings reference | [CLI Settings Reference](https://docs.muonroi.com/docs/cli/reference/cli-settings-reference) |
104
+ | Commands reference | [Commands Reference](https://docs.muonroi.com/docs/cli/reference/commands-reference) |
105
+ | Providers reference | [Providers Reference](https://docs.muonroi.com/docs/cli/reference/providers-reference) |
106
+
107
+ ## Development
108
+
109
+ ```bash
110
+ git clone https://github.com/muonroi/muonroi-cli.git
111
+ cd muonroi-cli && bun install
112
+
113
+ bun run dev # run from source
114
+ bun run typecheck # type check
115
+ bun run test # vitest
116
+ bun run lint # biome check
117
+ bun run build:binary # standalone binary
118
+ ```
119
+
120
+ ## License
121
+
122
+ MIT
@@ -7,10 +7,10 @@ declare const FieldOp: z.ZodObject<{
7
7
  state: "state";
8
8
  }>;
9
9
  op: z.ZodEnum<{
10
- regex: "regex";
11
10
  eq: "eq";
12
11
  neq: "neq";
13
12
  contains: "contains";
13
+ regex: "regex";
14
14
  }>;
15
15
  rhs: z.ZodString;
16
16
  }, z.core.$strip>;
@@ -5,7 +5,7 @@
5
5
  import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
6
6
  import { tmpdir } from "node:os";
7
7
  import { join } from "node:path";
8
- import { stepCountIs, streamText, tool } from "ai";
8
+ import { generateObject, stepCountIs, streamText, tool } from "ai";
9
9
  import { afterAll, describe, expect, it } from "vitest";
10
10
  import { z } from "zod";
11
11
  import { createMockModel, loadMockModelFromDir, textOnlyStream, toolCallStream } from "../mock-model.js";
@@ -84,6 +84,37 @@ describe("createMockModel", () => {
84
84
  await drainStream(r2);
85
85
  expect(handle.calls.length).toBe(1);
86
86
  });
87
+ it("doGenerate backs generateObject with the configured JSON (council debate-planner path)", async () => {
88
+ const handle = createMockModel({
89
+ stream: textOnlyStream("unused"),
90
+ generate: JSON.stringify({ name: "counter", count: 3 }),
91
+ });
92
+ const { object } = await generateObject({
93
+ model: handle.model,
94
+ schema: z.object({ name: z.string(), count: z.number() }),
95
+ prompt: "plan the build",
96
+ });
97
+ expect(object).toEqual({ name: "counter", count: 3 });
98
+ });
99
+ it("doGenerate sequences across calls and repeats the last entry when exhausted", async () => {
100
+ const handle = createMockModel({
101
+ stream: textOnlyStream("unused"),
102
+ generate: [JSON.stringify({ n: 1 }), JSON.stringify({ n: 2 })],
103
+ });
104
+ const schema = z.object({ n: z.number() });
105
+ const a = await generateObject({ model: handle.model, schema, prompt: "1" });
106
+ const b = await generateObject({ model: handle.model, schema, prompt: "2" });
107
+ const c = await generateObject({ model: handle.model, schema, prompt: "3" });
108
+ expect(a.object.n).toBe(1);
109
+ expect(b.object.n).toBe(2);
110
+ expect(c.object.n).toBe(2); // exhausted → last entry repeats
111
+ });
112
+ it("doGenerate defaults to {} when no generate fixture is supplied (caller retry/fallback runs)", async () => {
113
+ const handle = createMockModel({ stream: textOnlyStream("unused") });
114
+ // An empty object fails a required-field schema → generateObject rejects,
115
+ // which is exactly what lets debate-planner fall through to its retry path.
116
+ await expect(generateObject({ model: handle.model, schema: z.object({ required: z.string() }), prompt: "x" })).rejects.toBeTruthy();
117
+ });
87
118
  it("textOnlyStream emits a well-formed finish chunk", () => {
88
119
  const chunks = textOnlyStream("hi");
89
120
  const finish = chunks.find((c) => c.type === "finish");
@@ -141,6 +172,22 @@ describe("loadMockModelFromDir", () => {
141
172
  expect(handle).not.toBeNull();
142
173
  expect(handle?.defaultProviderOptions).toEqual({ openai: { store: false } });
143
174
  });
175
+ it("propagates generate (doGenerate JSON) from the fixture file for generateObject", async () => {
176
+ const dir = mkFixtureDir({
177
+ provider: "mock",
178
+ modelId: "mock-gpt",
179
+ stream: textOnlyStream("unused"),
180
+ generate: JSON.stringify({ ok: true, label: "built" }),
181
+ });
182
+ const handle = await loadMockModelFromDir(dir);
183
+ expect(handle).not.toBeNull();
184
+ const { object } = await generateObject({
185
+ model: handle.model,
186
+ schema: z.object({ ok: z.boolean(), label: z.string() }),
187
+ prompt: "go",
188
+ });
189
+ expect(object).toEqual({ ok: true, label: "built" });
190
+ });
144
191
  it("supports multi-round stream arrays from the fixture file", async () => {
145
192
  const dir = mkFixtureDir({
146
193
  provider: "mock",
@@ -33,6 +33,17 @@ export interface MockModelFixture {
33
33
  * entry repeats (so multi-round loops don't crash if the fixture is short).
34
34
  */
35
35
  stream: StreamChunks | StreamChunks[];
36
+ /**
37
+ * JSON text returned per `doGenerate` call — the path `generateObject` uses
38
+ * (council `debate-planner` plans the debate via `generateObject`, research
39
+ * classifiers via `generateText`, etc.). Without this the mock's `doGenerate`
40
+ * returns `"{}"`, which `generateObject` schema-validates → throws → the
41
+ * caller's retry/fallback path runs. Supply the exact object JSON to exercise
42
+ * the happy path.
43
+ * - Single string → same JSON on every call.
44
+ * - Array → one entry consumed per call; last entry repeats when exhausted.
45
+ */
46
+ generate?: string | string[];
36
47
  /** Reported provider id. Default "mock". */
37
48
  provider?: string;
38
49
  /** Reported model id. Default "mock-model". */
@@ -38,7 +38,9 @@ function isNestedArray(v) {
38
38
  */
39
39
  export function createMockModel(fx) {
40
40
  const streams = isNestedArray(fx.stream) ? fx.stream : [fx.stream];
41
+ const generates = fx.generate === undefined ? [] : Array.isArray(fx.generate) ? fx.generate : [fx.generate];
41
42
  let callIdx = 0;
43
+ let genIdx = 0;
42
44
  const provider = fx.provider ?? "mock";
43
45
  const modelId = fx.modelId ?? "mock-model";
44
46
  const model = new MockLanguageModelV3({
@@ -60,6 +62,24 @@ export function createMockModel(fx) {
60
62
  }),
61
63
  };
62
64
  },
65
+ // doGenerate backs `generateObject` / non-streaming `generateText`. The AI
66
+ // SDK reads the first text content part as the object JSON (generateObject)
67
+ // or the completion text (generateText). Default "{}" keeps the mock from
68
+ // throwing "Not implemented" — generateObject then schema-rejects it and the
69
+ // caller's retry/fallback runs. Supply `generate` to drive the happy path.
70
+ doGenerate: async () => {
71
+ const text = generates.length > 0 ? generates[Math.min(genIdx, generates.length - 1)] : "{}";
72
+ genIdx += 1;
73
+ if (process.env.MUONROI_DEBUG_MOCK_MODEL === "1") {
74
+ process.stderr.write(`[mock-model] doGenerate #${genIdx} → ${text.length} chars\n`);
75
+ }
76
+ return {
77
+ content: [{ type: "text", text }],
78
+ finishReason: { unified: "stop", raw: undefined },
79
+ usage: buildUsage(10, text.length),
80
+ warnings: [],
81
+ };
82
+ },
63
83
  });
64
84
  return {
65
85
  model,
@@ -69,6 +89,7 @@ export function createMockModel(fx) {
69
89
  reset() {
70
90
  model.doStreamCalls.length = 0;
71
91
  callIdx = 0;
92
+ genIdx = 0;
72
93
  },
73
94
  };
74
95
  }
@@ -42,21 +42,21 @@ export function collectCostForensics(sessionId) {
42
42
  if (!session)
43
43
  throw new Error(`Session not found: ${sessionId}`);
44
44
  const rows = db
45
- .prepare(`
46
- SELECT id, source, model, message_seq, input_tokens, output_tokens,
47
- cache_read_tokens, cache_creation_tokens, cost_micros, created_at,
48
- provider_options_shape
49
- FROM usage_events
50
- WHERE session_id = ?
51
- ORDER BY id ASC
45
+ .prepare(`
46
+ SELECT id, source, model, message_seq, input_tokens, output_tokens,
47
+ cache_read_tokens, cache_creation_tokens, cost_micros, created_at,
48
+ provider_options_shape
49
+ FROM usage_events
50
+ WHERE session_id = ?
51
+ ORDER BY id ASC
52
52
  `)
53
53
  .all(sessionId);
54
54
  const counts = db
55
- .prepare(`
56
- SELECT event_type, COUNT(*) AS c
57
- FROM interaction_logs
58
- WHERE session_id = ?
59
- GROUP BY event_type
55
+ .prepare(`
56
+ SELECT event_type, COUNT(*) AS c
57
+ FROM interaction_logs
58
+ WHERE session_id = ?
59
+ GROUP BY event_type
60
60
  `)
61
61
  .all(sessionId);
62
62
  const countMap = new Map(counts.map((r) => [r.event_type, r.c]));
@@ -0,0 +1,51 @@
1
+ import { describe, expect, it } from "vitest";
2
+ import { buildClarificationPrompt, buildReadinessJudgePrompt } from "../prompts.js";
3
+ // Guards the de-robotized askcard contract (2026-06-14). The council clarifier
4
+ // used to (a) force "AT LEAST 2 questions" → rambling/over-asking, (b) tell the
5
+ // model to OMIT a recommendation unless certain → unranked option lists, and
6
+ // (c) carry no existing-repo grounding → generic greenfield questions on a
7
+ // brownfield repo. These assertions lock in the fixed behaviour so a future
8
+ // edit can't silently regress it.
9
+ describe("buildClarificationPrompt — de-robotized askcard", () => {
10
+ const { system } = buildClarificationPrompt("Add a retry to the EE bridge call", "## Current Project\nmuonroi-cli — TypeScript CLI. Stack: bun, vitest.\n");
11
+ it("does NOT force a minimum question quota", () => {
12
+ expect(system).not.toMatch(/AT LEAST 2 questions/i);
13
+ expect(system).not.toMatch(/MUST ask AT LEAST/i);
14
+ expect(system).not.toMatch(/Minimum-question rule/i);
15
+ });
16
+ it("explicitly allows zero questions / returning []", () => {
17
+ expect(system).toMatch(/return \[\]/i);
18
+ expect(system).toMatch(/ZERO questions/i);
19
+ });
20
+ it("makes a recommendation MANDATORY (decisive), not optional", () => {
21
+ expect(system).toMatch(/ALWAYS include "recommended"/);
22
+ // The old "OMIT the field entirely" default must be gone.
23
+ expect(system).not.toMatch(/OMIT the field entirely/i);
24
+ expect(system).toMatch(/never face an unranked list/i);
25
+ });
26
+ it("grounds questions in the existing repo, not greenfield", () => {
27
+ expect(system).toMatch(/Current Project/);
28
+ expect(system).toMatch(/EXISTING repository/);
29
+ expect(system).toMatch(/do NOT ask generic greenfield/i);
30
+ });
31
+ it("still emits the structured JSON contract (question/suggestions/recommended)", () => {
32
+ expect(system).toMatch(/"suggestions"/);
33
+ expect(system).toMatch(/"recommended"/);
34
+ expect(system).toMatch(/Output ONLY a JSON array/);
35
+ });
36
+ });
37
+ describe("buildReadinessJudgePrompt — no JS string-concat leak", () => {
38
+ const { system } = buildReadinessJudgePrompt("Optimize queries", [], {
39
+ problemStatement: "p",
40
+ constraints: [],
41
+ successCriteria: ["c"],
42
+ scope: "s",
43
+ });
44
+ it("does not leak JS concatenation syntax into the prompt text", () => {
45
+ // Was: `...means "probably " +\n "ready but some ambiguity remains"` — the
46
+ // `" +` and the leading ` "` leaked verbatim into the model-facing string.
47
+ expect(system).not.toContain('" +');
48
+ expect(system).toContain("probably ready but some ambiguity remains");
49
+ });
50
+ });
51
+ //# sourceMappingURL=clarification-prompt.test.js.map
@@ -151,6 +151,38 @@ describe("P5 ready-gate: Test C — hard cap at MAX_CLARIFY_ROUNDS", () => {
151
151
  });
152
152
  });
153
153
  // ---------------------------------------------------------------------------
154
+ // Test E: clarifier asks ZERO questions → spec.ready=true, judge NOT called
155
+ // (de-robotized prompt now commonly returns [] — the gate must reflect that,
156
+ // not stay at its not-ready default, and must not pay for a judge LLM call.)
157
+ // ---------------------------------------------------------------------------
158
+ describe("P5 ready-gate: Test E — zero questions ⇒ ready without a judge call", () => {
159
+ it("marks spec.ready=true and skips the readiness judge when the clarifier asks nothing", async () => {
160
+ let callCount = 0;
161
+ const generate = vi.fn().mockImplementation(async () => {
162
+ callCount++;
163
+ if (callCount === 1)
164
+ return "[]"; // clarify round 0: nothing to ask
165
+ // spec synthesis (if reached) — no judge call should occur on this path
166
+ return JSON.stringify({
167
+ problemStatement: "Add a retry to the EE bridge",
168
+ constraints: [],
169
+ successCriteria: ["Retries once on transient failure"],
170
+ scope: "EE bridge only",
171
+ });
172
+ });
173
+ const mockLLM = { generate };
174
+ const gen = runClarification("Add a retry to the EE bridge", "leader-model", "## Current Project\nTypeScript CLI", alwaysAnswer, mockLLM);
175
+ const spec = await _drain(gen);
176
+ expect(spec.ready).toBe(true);
177
+ expect(spec.confidenceScore).toBe(1);
178
+ expect(spec.remainingGaps).toEqual([]);
179
+ expect(spec.clarifyHistory).toEqual([]);
180
+ // The readiness judge ("debate facilitator" system prompt) must NOT fire.
181
+ const judgeCalled = generate.mock.calls.some(([, system]) => typeof system === "string" && system.includes("debate facilitator"));
182
+ expect(judgeCalled).toBe(false);
183
+ });
184
+ });
185
+ // ---------------------------------------------------------------------------
154
186
  // Test D: judgeReadiness unit tests
155
187
  // ---------------------------------------------------------------------------
156
188
  describe("P5 ready-gate: Test D — judgeReadiness unit", () => {
@@ -7,7 +7,7 @@ import * as fs from "node:fs";
7
7
  import * as os from "node:os";
8
8
  import * as path from "node:path";
9
9
  import { afterEach, beforeEach, describe, expect, it } from "vitest";
10
- import { buildStackLockSection, detectOutOfStackProposals, prependDecisionsLock, readDecisionsLock, renderDecisionsLock, writeDecisionsLock, } from "../decisions-lock.js";
10
+ import { buildStackLockSection, detectOutOfStackProposals, extractStackFromSpec, prependDecisionsLock, readDecisionsLock, renderDecisionsLock, writeDecisionsLock, } from "../decisions-lock.js";
11
11
  import { buildFollowupPrompt, buildLeaderEvaluationPrompt, buildOpeningPrompt, buildResponsePrompt, } from "../prompts.js";
12
12
  // ── Fixtures ──────────────────────────────────────────────────────────────────
13
13
  function makeSpec(overrides = {}) {
@@ -72,6 +72,22 @@ describe("C1: buildStackLockSection", () => {
72
72
  expect(section).toBe("");
73
73
  });
74
74
  });
75
+ // ── extractStackFromSpec: mediatr keyword (Cyrillic-char regression) ──────────
76
+ describe("extractStackFromSpec — mediatr keyword", () => {
77
+ it("detects the BB/.NET backend from a 'MediatR' mention alone", () => {
78
+ // Pre-fix the keyword used a Cyrillic 'р' (U+0440), so ASCII "mediatr" could
79
+ // never match. A spec mentioning ONLY MediatR (no BaseTemplate / building-block)
80
+ // must still resolve the backend.
81
+ const spec = makeSpec({ problemStatement: "Wire up CQRS handlers with MediatR", constraints: [], scope: "" });
82
+ const stack = extractStackFromSpec(spec);
83
+ expect(stack).not.toBeNull();
84
+ expect(stack?.backend ?? "").toContain("MediatR");
85
+ });
86
+ it("returns null when no BB/.NET/Muonroi keyword is present", () => {
87
+ const spec = makeSpec({ problemStatement: "Build a plain Express API", constraints: [], scope: "" });
88
+ expect(extractStackFromSpec(spec)).toBeNull();
89
+ });
90
+ });
75
91
  // ── C1: STACK LOCK injected into debate system prompts ────────────────────────
76
92
  describe("C1: STACK LOCK injected into debate system prompts", () => {
77
93
  const bbSpec = makeSpecWithBBStack();
@@ -0,0 +1,31 @@
1
+ import { beforeAll, describe, expect, it, vi } from "vitest";
2
+ // F15 regression: council participant resolution must treat OAuth-authed
3
+ // providers (no API key) as reachable. getConfiguredProviders() is the
4
+ // authoritative cred check; the old loadKeyForProvider-only path threw for
5
+ // OAuth-only providers → 0 participants → council bailed "No reachable
6
+ // provider" even though the model (e.g. grok via xAI OAuth) answers fine.
7
+ // xai is configured via OAuth only (present in getConfiguredProviders, but
8
+ // loadKeyForProvider would have thrown — no API key).
9
+ vi.mock("../../providers/keychain.js", () => ({
10
+ getConfiguredProviders: vi.fn(async () => ["xai"]),
11
+ }));
12
+ // Hermetic settings: no explicit role models, nothing disabled.
13
+ vi.mock("../../utils/settings.js", () => ({
14
+ getRoleModels: () => ({}),
15
+ getRoleModel: () => undefined,
16
+ isProviderDisabled: () => false,
17
+ }));
18
+ import { loadCatalog } from "../../models/registry.js";
19
+ import { resolveParticipants } from "../leader.js";
20
+ describe("F15 — council reachability counts OAuth-only providers", () => {
21
+ beforeAll(async () => {
22
+ await loadCatalog();
23
+ });
24
+ it("resolves >=2 participants when the session provider is OAuth-only (xai, no API key)", async () => {
25
+ const participants = await resolveParticipants("grok-build-0.1", false);
26
+ // Pre-fix this returned [] (loadKeyForProvider('xai') threw) → council bailed.
27
+ expect(participants.length).toBeGreaterThanOrEqual(2);
28
+ expect(participants.every((p) => p.model.startsWith("grok"))).toBe(true);
29
+ });
30
+ });
31
+ //# sourceMappingURL=oauth-reachable.test.js.map
@@ -99,5 +99,16 @@ describe("parseOutcome — raw log + shape-based fallback (CQ-20)", () => {
99
99
  expect(result?.outcome?.summary).toBe("We should go with option A because it is simpler.");
100
100
  expect(errorSpy).not.toHaveBeenCalled();
101
101
  });
102
+ it("Test 6: shape fallback EXTRACTS section content from markdown headings (regex regression)", async () => {
103
+ // Pre-fix the heading regex used a literal "s+" instead of "\\s+" (and
104
+ // replaced spaces with "s+"), so it never matched a real "## Heading" line —
105
+ // every section came back empty even when the synthesis clearly contained them.
106
+ const md = "Here is the evaluation summary line that is plenty long enough.\n\n" +
107
+ "## Strengths\n- Fast startup\n- Low cost\n\n" +
108
+ "## Summary\nThe approach is solid overall.";
109
+ const result = await runPlanningWith(md, sampleDebatePlan);
110
+ expect(result?.outcome?.sections?.strengths).toEqual(["- Fast startup", "- Low cost"]);
111
+ expect(result?.outcome?.sections?.summary_text).toContain("The approach is solid overall.");
112
+ });
102
113
  });
103
114
  //# sourceMappingURL=parse-outcome-fallback.test.js.map