muonroi-cli 1.4.1 → 1.6.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 (194) 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/artifact-cache.d.ts +56 -0
  26. package/dist/src/ee/artifact-cache.js +155 -0
  27. package/dist/src/ee/artifact-cache.test.d.ts +1 -0
  28. package/dist/src/ee/artifact-cache.test.js +69 -0
  29. package/dist/src/ee/auth.d.ts +9 -0
  30. package/dist/src/ee/auth.js +19 -0
  31. package/dist/src/ee/ee-onboarding.d.ts +5 -0
  32. package/dist/src/ee/ee-onboarding.js +76 -0
  33. package/dist/src/ee/search.js +7 -5
  34. package/dist/src/ee/search.test.d.ts +1 -0
  35. package/dist/src/ee/search.test.js +23 -0
  36. package/dist/src/generated/version.d.ts +1 -1
  37. package/dist/src/generated/version.js +1 -1
  38. package/dist/src/headless/output.js +6 -4
  39. package/dist/src/headless/output.test.js +4 -3
  40. package/dist/src/index.js +20 -1
  41. package/dist/src/mcp/__tests__/auto-setup.test.js +74 -0
  42. package/dist/src/mcp/__tests__/client-pool.spec.d.ts +1 -0
  43. package/dist/src/mcp/__tests__/client-pool.spec.js +98 -0
  44. package/dist/src/mcp/__tests__/parallel-build.spec.d.ts +1 -0
  45. package/dist/src/mcp/__tests__/parallel-build.spec.js +67 -0
  46. package/dist/src/mcp/__tests__/smart-filter.test.js +56 -0
  47. package/dist/src/mcp/auto-setup.js +56 -2
  48. package/dist/src/mcp/client-pool.d.ts +46 -0
  49. package/dist/src/mcp/client-pool.js +212 -0
  50. package/dist/src/mcp/oauth-callback.js +2 -2
  51. package/dist/src/mcp/parse-headers.test.js +14 -14
  52. package/dist/src/mcp/runtime.d.ts +28 -0
  53. package/dist/src/mcp/runtime.js +117 -51
  54. package/dist/src/mcp/self-verify-runner.d.ts +14 -0
  55. package/dist/src/mcp/self-verify-runner.js +38 -0
  56. package/dist/src/mcp/setup-guide-text.d.ts +9 -0
  57. package/dist/src/mcp/setup-guide-text.js +84 -0
  58. package/dist/src/mcp/smart-filter.js +49 -0
  59. package/dist/src/mcp/smoke.test.js +43 -43
  60. package/dist/src/mcp/tools-server.d.ts +7 -0
  61. package/dist/src/mcp/tools-server.js +19 -22
  62. package/dist/src/models/catalog.json +349 -349
  63. package/dist/src/ops/__tests__/doctor-ee-health.test.js +21 -0
  64. package/dist/src/ops/doctor.d.ts +3 -2
  65. package/dist/src/ops/doctor.js +47 -11
  66. package/dist/src/ops/doctor.test.js +4 -3
  67. package/dist/src/orchestrator/__tests__/mcp-capability-block.test.d.ts +1 -0
  68. package/dist/src/orchestrator/__tests__/mcp-capability-block.test.js +39 -0
  69. package/dist/src/orchestrator/__tests__/project-stack.test.d.ts +1 -0
  70. package/dist/src/orchestrator/__tests__/project-stack.test.js +65 -0
  71. package/dist/src/orchestrator/batch-turn-runner.js +7 -11
  72. package/dist/src/orchestrator/compaction.d.ts +2 -0
  73. package/dist/src/orchestrator/compaction.js +14 -1
  74. package/dist/src/orchestrator/compaction.test.js +25 -1
  75. package/dist/src/orchestrator/message-processor.js +72 -32
  76. package/dist/src/orchestrator/orchestrator.js +26 -0
  77. package/dist/src/orchestrator/prompts.d.ts +51 -0
  78. package/dist/src/orchestrator/prompts.js +257 -134
  79. package/dist/src/orchestrator/scope-ceiling.js +6 -1
  80. package/dist/src/orchestrator/scope-reminder.d.ts +12 -0
  81. package/dist/src/orchestrator/scope-reminder.js +16 -0
  82. package/dist/src/orchestrator/scope-reminder.test.js +22 -1
  83. package/dist/src/orchestrator/stream-runner.js +23 -15
  84. package/dist/src/orchestrator/subagent-compactor.d.ts +14 -5
  85. package/dist/src/orchestrator/subagent-compactor.js +30 -8
  86. package/dist/src/orchestrator/subagent-compactor.spec.js +18 -0
  87. package/dist/src/orchestrator/text-tool-call-detector.test.js +13 -13
  88. package/dist/src/pil/__tests__/clarity-gate.test.js +24 -215
  89. package/dist/src/pil/__tests__/config.test.js +1 -17
  90. package/dist/src/pil/__tests__/discovery.test.js +144 -11
  91. package/dist/src/pil/__tests__/layer1-intent-trace.test.js +7 -2
  92. package/dist/src/pil/__tests__/layer1-intent.test.js +3 -0
  93. package/dist/src/pil/__tests__/layer16-clarity.test.js +32 -116
  94. package/dist/src/pil/__tests__/layer4-gsd.test.js +37 -0
  95. package/dist/src/pil/__tests__/layer6-output.test.js +158 -18
  96. package/dist/src/pil/__tests__/llm-classify.test.js +49 -2
  97. package/dist/src/pil/__tests__/surface-compaction-artifacts.test.d.ts +1 -0
  98. package/dist/src/pil/__tests__/surface-compaction-artifacts.test.js +112 -0
  99. package/dist/src/pil/agent-operating-contract.d.ts +1 -1
  100. package/dist/src/pil/agent-operating-contract.js +2 -0
  101. package/dist/src/pil/agent-operating-contract.test.js +7 -2
  102. package/dist/src/pil/cheap-model-playbook.js +35 -35
  103. package/dist/src/pil/cheap-model-workbooks.js +16 -13
  104. package/dist/src/pil/clarity-gate.d.ts +21 -19
  105. package/dist/src/pil/clarity-gate.js +26 -153
  106. package/dist/src/pil/config.d.ts +9 -1
  107. package/dist/src/pil/config.js +15 -4
  108. package/dist/src/pil/discovery.js +211 -136
  109. package/dist/src/pil/layer1-intent.d.ts +12 -0
  110. package/dist/src/pil/layer1-intent.js +283 -38
  111. package/dist/src/pil/layer1-intent.test.js +210 -4
  112. package/dist/src/pil/layer16-clarity.d.ts +25 -11
  113. package/dist/src/pil/layer16-clarity.js +19 -306
  114. package/dist/src/pil/layer3-ee-injection.d.ts +19 -0
  115. package/dist/src/pil/layer3-ee-injection.js +96 -4
  116. package/dist/src/pil/layer4-gsd.js +18 -6
  117. package/dist/src/pil/layer6-output.d.ts +2 -0
  118. package/dist/src/pil/layer6-output.js +151 -25
  119. package/dist/src/pil/llm-classify.d.ts +26 -0
  120. package/dist/src/pil/llm-classify.js +34 -5
  121. package/dist/src/pil/native-capabilities-workbook.d.ts +1 -1
  122. package/dist/src/pil/native-capabilities-workbook.js +82 -76
  123. package/dist/src/pil/pipeline.js +15 -9
  124. package/dist/src/pil/schema.d.ts +8 -0
  125. package/dist/src/pil/schema.js +12 -1
  126. package/dist/src/pil/task-tier-map.js +4 -0
  127. package/dist/src/pil/types.d.ts +11 -1
  128. package/dist/src/product-loop/done-gate.js +3 -3
  129. package/dist/src/product-loop/loop-driver.js +18 -18
  130. package/dist/src/product-loop/progress-snapshot.js +4 -4
  131. package/dist/src/providers/auth/gemini-oauth.js +6 -15
  132. package/dist/src/providers/auth/grok-oauth.js +6 -15
  133. package/dist/src/providers/auth/openai-oauth.js +6 -15
  134. package/dist/src/providers/mcp-vision-bridge.js +48 -48
  135. package/dist/src/reporter/index.js +1 -1
  136. package/dist/src/scaffold/bb-ecosystem-apply.js +47 -47
  137. package/dist/src/scaffold/bb-quality-gate.js +5 -5
  138. package/dist/src/scaffold/continuation-prompt.js +60 -60
  139. package/dist/src/scaffold/init-new.js +453 -453
  140. package/dist/src/self-qa/__tests__/scenario-planner.test.js +3 -3
  141. package/dist/src/self-qa/agentic-loop.js +24 -19
  142. package/dist/src/self-qa/spec-emitter.js +26 -23
  143. package/dist/src/storage/__tests__/migrations.test.js +2 -2
  144. package/dist/src/storage/interaction-log.js +5 -5
  145. package/dist/src/storage/migrations.js +122 -122
  146. package/dist/src/storage/sessions.js +42 -42
  147. package/dist/src/storage/transcript.js +91 -84
  148. package/dist/src/storage/usage.js +14 -14
  149. package/dist/src/storage/workspaces.js +12 -12
  150. package/dist/src/tools/__tests__/native-tools.test.d.ts +1 -0
  151. package/dist/src/tools/__tests__/native-tools.test.js +53 -0
  152. package/dist/src/tools/git-safety.d.ts +61 -0
  153. package/dist/src/tools/git-safety.js +141 -0
  154. package/dist/src/tools/git-safety.test.d.ts +1 -0
  155. package/dist/src/tools/git-safety.test.js +111 -0
  156. package/dist/src/tools/native-tools.d.ts +31 -0
  157. package/dist/src/tools/native-tools.js +273 -0
  158. package/dist/src/tools/registry-ee-query.test.js +18 -1
  159. package/dist/src/tools/registry-git-safety.test.d.ts +7 -0
  160. package/dist/src/tools/registry-git-safety.test.js +92 -0
  161. package/dist/src/tools/registry.js +52 -6
  162. package/dist/src/ui/__tests__/markdown-render.test.d.ts +1 -0
  163. package/dist/src/ui/__tests__/markdown-render.test.js +48 -0
  164. package/dist/src/ui/app.js +0 -0
  165. package/dist/src/ui/components/message-view.js +4 -1
  166. package/dist/src/ui/components/structured-response-view.js +7 -3
  167. package/dist/src/ui/components/tool-group.js +7 -1
  168. package/dist/src/ui/markdown-render.d.ts +41 -0
  169. package/dist/src/ui/markdown-render.js +223 -0
  170. package/dist/src/ui/markdown.d.ts +10 -0
  171. package/dist/src/ui/markdown.js +12 -35
  172. package/dist/src/ui/slash/council-inspect.js +4 -4
  173. package/dist/src/ui/slash/export.js +4 -4
  174. package/dist/src/ui/utils/text.d.ts +8 -0
  175. package/dist/src/ui/utils/text.js +16 -0
  176. package/dist/src/ui/utils/text.test.d.ts +1 -0
  177. package/dist/src/ui/utils/text.test.js +23 -0
  178. package/dist/src/usage/ledger.js +48 -15
  179. package/dist/src/utils/__tests__/footprint-gitignore.test.d.ts +1 -0
  180. package/dist/src/utils/__tests__/footprint-gitignore.test.js +50 -0
  181. package/dist/src/utils/clipboard-image.js +23 -23
  182. package/dist/src/utils/open-url.d.ts +56 -0
  183. package/dist/src/utils/open-url.js +58 -0
  184. package/dist/src/utils/open-url.test.d.ts +1 -0
  185. package/dist/src/utils/open-url.test.js +86 -0
  186. package/dist/src/utils/settings.d.ts +12 -0
  187. package/dist/src/utils/settings.js +48 -0
  188. package/dist/src/utils/side-question.js +2 -2
  189. package/dist/src/utils/skills.js +3 -3
  190. package/dist/src/verify/__tests__/coverage-parsers.test.js +30 -30
  191. package/dist/src/verify/environment.js +2 -1
  192. package/package.json +1 -1
  193. package/dist/src/pil/layer16-clarity.test.js +0 -31
  194. /package/dist/src/{pil/layer16-clarity.test.d.ts → council/__tests__/clarification-prompt.test.d.ts} +0 -0
@@ -0,0 +1,111 @@
1
+ import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
2
+ import { tmpdir } from "node:os";
3
+ import { join } from "node:path";
4
+ import { afterEach, beforeEach, describe, expect, it } from "vitest";
5
+ import { __resetGitSafetyState, analyzeGitCommand, checkPushGate, detectSensitiveStaging, recordCommandOutcome, stagingWarning, } from "./git-safety.js";
6
+ describe("analyzeGitCommand", () => {
7
+ it("detects git push (with flags and chained)", () => {
8
+ expect(analyzeGitCommand("git push").isPush).toBe(true);
9
+ expect(analyzeGitCommand("git push origin main").isPush).toBe(true);
10
+ expect(analyzeGitCommand("git -c x=y push --force").isPush).toBe(true);
11
+ expect(analyzeGitCommand("git add -A && git commit -m x && git push origin main").isPush).toBe(true);
12
+ });
13
+ it("does not match 'push' inside a quoted commit message", () => {
14
+ expect(analyzeGitCommand('git commit -m "fix git push regression"').isPush).toBe(false);
15
+ });
16
+ it("detects a real push on its own line in a multi-line script", () => {
17
+ expect(analyzeGitCommand("git config user.name x\ngit push origin main").isPush).toBe(true);
18
+ });
19
+ it("does not bleed across a newline into an unrelated command", () => {
20
+ // 'git status' then a separate line with the word 'push' is NOT a git push.
21
+ expect(analyzeGitCommand("git status\necho push-notification").isPush).toBe(false);
22
+ expect(analyzeGitCommand("git log\nrm push.txt").isPush).toBe(false);
23
+ });
24
+ it("detects broad staging (-A / . / --all / commit -a)", () => {
25
+ expect(analyzeGitCommand("git add -A").isBroadStage).toBe(true);
26
+ expect(analyzeGitCommand("git add .").isBroadStage).toBe(true);
27
+ expect(analyzeGitCommand("git add --all").isBroadStage).toBe(true);
28
+ expect(analyzeGitCommand("git commit -am 'x'").isBroadStage).toBe(true);
29
+ expect(analyzeGitCommand("git commit -a").isBroadStage).toBe(true);
30
+ });
31
+ it("detects broad staging even with git global options before the subcommand", () => {
32
+ expect(analyzeGitCommand("git -c core.editor=true commit -a").isBroadStage).toBe(true);
33
+ expect(analyzeGitCommand("git -c x=y add -A").isBroadStage).toBe(true);
34
+ });
35
+ it("does not flag explicit/narrow staging or non-staging flags", () => {
36
+ expect(analyzeGitCommand("git add src/foo.ts src/bar.ts").isBroadStage).toBe(false);
37
+ expect(analyzeGitCommand("git add ./src/foo.ts").isBroadStage).toBe(false);
38
+ expect(analyzeGitCommand("git commit -m 'message'").isBroadStage).toBe(false);
39
+ expect(analyzeGitCommand("git commit --amend").isBroadStage).toBe(false);
40
+ // -a must be a clean flag cluster — a malformed `-a--otherflag` is not `-a`.
41
+ expect(analyzeGitCommand("git commit -a--otherflag").isBroadStage).toBe(false);
42
+ });
43
+ });
44
+ describe("push gate", () => {
45
+ beforeEach(() => {
46
+ __resetGitSafetyState();
47
+ delete process.env.MUONROI_ALLOW_PUSH_ON_RED;
48
+ });
49
+ afterEach(() => {
50
+ delete process.env.MUONROI_ALLOW_PUSH_ON_RED;
51
+ });
52
+ it("does not block when no verification has failed", () => {
53
+ expect(checkPushGate("s1").blocked).toBe(false);
54
+ });
55
+ it("blocks push after a verification command fails", () => {
56
+ recordCommandOutcome("s1", "npm test", false);
57
+ const gate = checkPushGate("s1");
58
+ expect(gate.blocked).toBe(true);
59
+ expect(gate.failed).toContain("npm test");
60
+ });
61
+ it("clears the block when that same command re-runs green", () => {
62
+ recordCommandOutcome("s1", "npm test", false);
63
+ expect(checkPushGate("s1").blocked).toBe(true);
64
+ recordCommandOutcome("s1", "npm test", true);
65
+ expect(checkPushGate("s1").blocked).toBe(false);
66
+ });
67
+ it("a different verify passing does NOT clear an unrelated failed verify", () => {
68
+ recordCommandOutcome("s1", "npm test", false);
69
+ recordCommandOutcome("s1", "npm run build", true); // build green, tests still red
70
+ expect(checkPushGate("s1").blocked).toBe(true);
71
+ expect(checkPushGate("s1").failed).toEqual(["npm test"]);
72
+ });
73
+ it("is session-scoped (one session's failure does not gate another)", () => {
74
+ recordCommandOutcome("s1", "vitest run", false);
75
+ expect(checkPushGate("s1").blocked).toBe(true);
76
+ expect(checkPushGate("s2").blocked).toBe(false);
77
+ });
78
+ it("ignores non-verification command outcomes", () => {
79
+ recordCommandOutcome("s1", "git status", false);
80
+ recordCommandOutcome("s1", "ls -la", false);
81
+ expect(checkPushGate("s1").blocked).toBe(false);
82
+ });
83
+ it("respects the MUONROI_ALLOW_PUSH_ON_RED override", () => {
84
+ recordCommandOutcome("s1", "npm test", false);
85
+ process.env.MUONROI_ALLOW_PUSH_ON_RED = "1";
86
+ expect(checkPushGate("s1").blocked).toBe(false);
87
+ });
88
+ });
89
+ describe("sensitive staging detection", () => {
90
+ let dir;
91
+ beforeEach(() => {
92
+ dir = mkdtempSync(join(tmpdir(), "git-safety-"));
93
+ });
94
+ afterEach(() => {
95
+ rmSync(dir, { recursive: true, force: true });
96
+ });
97
+ it("flags .env and .muonroi-cli present in the repo root", () => {
98
+ writeFileSync(join(dir, ".env"), "SECRET=1");
99
+ writeFileSync(join(dir, ".muonroi-cli"), ""); // a file is enough for existsSync
100
+ const found = detectSensitiveStaging(dir);
101
+ expect(found).toContain(".env");
102
+ expect(found).toContain(".muonroi-cli");
103
+ expect(stagingWarning(dir)).toMatch(/WARNING/);
104
+ });
105
+ it("returns no warning for a clean repo", () => {
106
+ writeFileSync(join(dir, "README.md"), "# ok");
107
+ expect(detectSensitiveStaging(dir)).toEqual([]);
108
+ expect(stagingWarning(dir)).toBe("");
109
+ });
110
+ });
111
+ //# sourceMappingURL=git-safety.test.js.map
@@ -0,0 +1,31 @@
1
+ /**
2
+ * src/tools/native-tools.ts
3
+ *
4
+ * NATIVE in-process builtins for the capabilities that muonroi-tools previously
5
+ * exposed only via a self-spawned MCP subprocess: ee_health, ee_feedback,
6
+ * usage_forensics, lsp_query, setup_guide, and selfverify_* (start/status/
7
+ * result/list/cancel).
8
+ *
9
+ * Why native: muonroi-tools is THIS CLI. Self-spawning a 137MB CLI as an MCP
10
+ * server per turn cold-started ~2-3.5s and overran the build deadline (and a
11
+ * seed-time bug once persisted a vitest-worker command that crashed on launch).
12
+ * For the CLI's OWN inner agent these tools should run in-process — no subprocess,
13
+ * no MCP round-trip, no cold-start. The muonroi-tools MCP server (tools-server.ts)
14
+ * stays for EXTERNAL agents (Claude Code etc.). `ee_query` is already native
15
+ * (registry.ts) and is intentionally NOT duplicated here.
16
+ *
17
+ * Each tool reuses the SAME core the MCP server wraps (ee/search, cli/cost-
18
+ * forensics, lsp/runtime, the shared self-verify JobManager), so behaviour is
19
+ * identical across the two surfaces.
20
+ */
21
+ import { type ToolSet } from "ai";
22
+ /** The native tool names this module registers — used by the MCP-twin dedup. */
23
+ export declare const NATIVE_MUONROI_TOOL_NAMES: readonly ["ee_health", "ee_feedback", "usage_forensics", "lsp_query", "setup_guide", "selfverify_start", "selfverify_status", "selfverify_result", "selfverify_list", "selfverify_cancel"];
24
+ export interface NativeToolOpts {
25
+ /** Workspace cwd for lsp_query. Defaults to process.cwd(). */
26
+ cwd?: string;
27
+ }
28
+ /**
29
+ * Add the native muonroi-tools builtins to `tools`. Mutates and returns it.
30
+ */
31
+ export declare function registerNativeMuonroiTools(tools: ToolSet, opts?: NativeToolOpts): ToolSet;
@@ -0,0 +1,273 @@
1
+ /**
2
+ * src/tools/native-tools.ts
3
+ *
4
+ * NATIVE in-process builtins for the capabilities that muonroi-tools previously
5
+ * exposed only via a self-spawned MCP subprocess: ee_health, ee_feedback,
6
+ * usage_forensics, lsp_query, setup_guide, and selfverify_* (start/status/
7
+ * result/list/cancel).
8
+ *
9
+ * Why native: muonroi-tools is THIS CLI. Self-spawning a 137MB CLI as an MCP
10
+ * server per turn cold-started ~2-3.5s and overran the build deadline (and a
11
+ * seed-time bug once persisted a vitest-worker command that crashed on launch).
12
+ * For the CLI's OWN inner agent these tools should run in-process — no subprocess,
13
+ * no MCP round-trip, no cold-start. The muonroi-tools MCP server (tools-server.ts)
14
+ * stays for EXTERNAL agents (Claude Code etc.). `ee_query` is already native
15
+ * (registry.ts) and is intentionally NOT duplicated here.
16
+ *
17
+ * Each tool reuses the SAME core the MCP server wraps (ee/search, cli/cost-
18
+ * forensics, lsp/runtime, the shared self-verify JobManager), so behaviour is
19
+ * identical across the two surfaces.
20
+ */
21
+ import { dynamicTool, jsonSchema } from "ai";
22
+ import { LSP_TOOL_OPERATIONS } from "../lsp/types.js";
23
+ import { getSelfVerifyJobManager } from "../mcp/self-verify-runner.js";
24
+ import { SETUP_GUIDE_TEXT } from "../mcp/setup-guide-text.js";
25
+ /** The native tool names this module registers — used by the MCP-twin dedup. */
26
+ export const NATIVE_MUONROI_TOOL_NAMES = [
27
+ "ee_health",
28
+ "ee_feedback",
29
+ "usage_forensics",
30
+ "lsp_query",
31
+ "setup_guide",
32
+ "selfverify_start",
33
+ "selfverify_status",
34
+ "selfverify_result",
35
+ "selfverify_list",
36
+ "selfverify_cancel",
37
+ ];
38
+ const json = (data) => JSON.stringify(data);
39
+ const errLine = (error, message) => `ERROR ${error}: ${message}`;
40
+ /**
41
+ * Add the native muonroi-tools builtins to `tools`. Mutates and returns it.
42
+ */
43
+ export function registerNativeMuonroiTools(tools, opts = {}) {
44
+ // ── Experience Engine: health + feedback (ee_query is already native) ──────
45
+ tools.ee_health = dynamicTool({
46
+ description: "Check Experience Engine server reachability (returns {ok, status}).",
47
+ inputSchema: jsonSchema({ type: "object", properties: {}, additionalProperties: false }),
48
+ execute: async () => {
49
+ try {
50
+ const { healthEE } = await import("../ee/search.js");
51
+ return json(await healthEE());
52
+ }
53
+ catch (e) {
54
+ return errLine("ee_unavailable", e instanceof Error ? e.message : String(e));
55
+ }
56
+ },
57
+ });
58
+ tools.ee_feedback = dynamicTool({
59
+ description: "Rate an Experience Engine recall entry so the brain keeps what helped and prunes the rest. Call after " +
60
+ "acting on an ee_query result — once per `[id col]` you used or judged. verdict: 'followed' (you changed " +
61
+ "your approach because of it), 'ignored' (topical but did not apply this time), 'noise' (wrong by category — " +
62
+ "REQUIRES reason: wrong_repo | wrong_language | wrong_task | stale_rule). id may be a short prefix.",
63
+ inputSchema: jsonSchema({
64
+ type: "object",
65
+ properties: {
66
+ id: { type: "string", description: "Entry id (short prefix accepted)" },
67
+ collection: { type: "string", description: "EE collection the entry came from" },
68
+ verdict: { type: "string", enum: ["followed", "ignored", "noise"] },
69
+ reason: { type: "string", enum: ["wrong_repo", "wrong_language", "wrong_task", "stale_rule"] },
70
+ },
71
+ required: ["id", "collection", "verdict"],
72
+ }),
73
+ execute: async (input) => {
74
+ const id = typeof input?.id === "string" ? input.id.trim() : "";
75
+ const collection = typeof input?.collection === "string" ? input.collection.trim() : "";
76
+ const verdict = input?.verdict;
77
+ const reason = input?.reason;
78
+ if (!id || !collection || !verdict) {
79
+ return errLine("invalid_args", "ee_feedback requires id, collection, and verdict");
80
+ }
81
+ if (verdict === "noise" && !reason) {
82
+ return errLine("reason_required", "verdict 'noise' requires reason: wrong_repo | wrong_language | wrong_task | stale_rule");
83
+ }
84
+ try {
85
+ const { feedbackEE } = await import("../ee/search.js");
86
+ const { sessionRecallLedger } = await import("../ee/recall-ledger.js");
87
+ const result = await feedbackEE(id, collection, verdict, reason);
88
+ if (!result.ok)
89
+ return errLine("feedback_failed", result.error ?? "feedback POST failed");
90
+ const clearedId = result.resolvedId ?? id;
91
+ sessionRecallLedger.clear(clearedId);
92
+ sessionRecallLedger.clear(id);
93
+ return json({
94
+ ok: true,
95
+ id: clearedId,
96
+ verdict: result.verdict,
97
+ ...(result.reason ? { reason: result.reason } : {}),
98
+ pendingRemaining: sessionRecallLedger.pendingCount(),
99
+ });
100
+ }
101
+ catch (e) {
102
+ return errLine("feedback_failed", e instanceof Error ? e.message : String(e));
103
+ }
104
+ },
105
+ });
106
+ // ── Self-diagnostics: usage_forensics ─────────────────────────────────────
107
+ tools.usage_forensics = dynamicTool({
108
+ description: "Per-session token-cost forensics by session-id prefix: peak input, cache-hit ratio, per-event breakdown.",
109
+ inputSchema: jsonSchema({
110
+ type: "object",
111
+ properties: { prefix: { type: "string", description: "Session id prefix (1-100 chars)" } },
112
+ required: ["prefix"],
113
+ }),
114
+ execute: async (input) => {
115
+ const prefix = typeof input?.prefix === "string" ? input.prefix.trim() : "";
116
+ if (!prefix)
117
+ return errLine("invalid_args", "usage_forensics requires a non-empty prefix");
118
+ try {
119
+ const { resolveSessionIds, collectCostForensics } = await import("../cli/cost-forensics.js");
120
+ const ids = await resolveSessionIds(prefix);
121
+ if (ids.length === 0)
122
+ return errLine("not_found", `no session matches prefix '${prefix}'`);
123
+ if (ids.length > 1)
124
+ return errLine("ambiguous", `prefix '${prefix}' matched ${ids.length} sessions`);
125
+ return json(await collectCostForensics(ids[0]));
126
+ }
127
+ catch (e) {
128
+ return errLine("db_error", e instanceof Error ? e.message : String(e));
129
+ }
130
+ },
131
+ });
132
+ // ── Code intelligence: lsp_query ──────────────────────────────────────────
133
+ tools.lsp_query = dynamicTool({
134
+ description: "Semantic code intelligence via language servers. operation is one of: goToDefinition, findReferences, hover, documentSymbol, workspaceSymbol, goToImplementation, prepareCallHierarchy, incomingCalls, outgoingCalls. " +
135
+ "filePath: absolute, or relative to the workspace root. line/character: 1-based — required for position-based ops; omit for documentSymbol; use query (not position) for workspaceSymbol. Returns {success, output}.",
136
+ inputSchema: jsonSchema({
137
+ type: "object",
138
+ properties: {
139
+ operation: { type: "string", enum: [...LSP_TOOL_OPERATIONS] },
140
+ filePath: { type: "string", description: "Absolute or workspace-relative path" },
141
+ line: { type: "number", description: "1-based line (position ops)" },
142
+ character: { type: "number", description: "1-based character (position ops)" },
143
+ query: { type: "string", description: "Symbol query (workspaceSymbol)" },
144
+ },
145
+ required: ["operation", "filePath"],
146
+ }),
147
+ execute: async (input) => {
148
+ const cwd = opts.cwd ?? process.cwd();
149
+ try {
150
+ const { queryLsp, isLspToolEnabled } = await import("../lsp/runtime.js");
151
+ if (!(await isLspToolEnabled(cwd))) {
152
+ return errLine("lsp_disabled", "LSP tool is disabled in settings (lsp.enabled / lsp.tool)");
153
+ }
154
+ return json(await queryLsp(cwd, input));
155
+ }
156
+ catch (e) {
157
+ return errLine("lsp_error", e instanceof Error ? e.message : String(e));
158
+ }
159
+ },
160
+ });
161
+ // ── Onboarding: setup_guide ───────────────────────────────────────────────
162
+ tools.setup_guide = dynamicTool({
163
+ description: "Returns the up-to-date setup / install / first-run / MCP wiring / verify guide for muonroi-cli. Call this " +
164
+ "when the user asks how to set up, install, or get started — instead of guessing, reading files, or shelling commands.",
165
+ inputSchema: jsonSchema({ type: "object", properties: {}, additionalProperties: false }),
166
+ execute: async () => SETUP_GUIDE_TEXT,
167
+ });
168
+ // ── Self-QA harness: selfverify_* (shared JobManager, in-process) ──────────
169
+ tools.selfverify_start = dynamicTool({
170
+ description: "Start a self-verify run (mode=tier1 heuristic, or mode=agentic LLM-driven). Returns {runId} immediately; " +
171
+ "poll selfverify_status, then selfverify_result.",
172
+ inputSchema: jsonSchema({
173
+ type: "object",
174
+ properties: {
175
+ mode: { type: "string", enum: ["tier1", "agentic"] },
176
+ since: { type: "string" },
177
+ max: { type: "number" },
178
+ emit: { type: "boolean" },
179
+ out: { type: "string" },
180
+ goal: { type: "string" },
181
+ llm: { type: "string" },
182
+ turns: { type: "number" },
183
+ },
184
+ required: ["mode"],
185
+ }),
186
+ execute: async (input) => {
187
+ const jm = getSelfVerifyJobManager();
188
+ if (input?.mode === "agentic") {
189
+ if (!input?.goal || !input?.llm)
190
+ return errLine("invalid_args", "agentic mode requires both goal and llm");
191
+ const { getModelInfo } = await import("../models/registry.js");
192
+ if (!getModelInfo(input.llm))
193
+ return errLine("unknown_model", `llm '${input.llm}' is not in catalog.json`);
194
+ return json({ runId: jm.start({ kind: "agentic", goal: input.goal, llm: input.llm, turns: input.turns }) });
195
+ }
196
+ return json({
197
+ runId: jm.start({ kind: "tier1", since: input?.since, max: input?.max, emit: input?.emit, out: input?.out }),
198
+ });
199
+ },
200
+ });
201
+ tools.selfverify_status = dynamicTool({
202
+ description: "Get status + log tail of a self-verify run.",
203
+ inputSchema: jsonSchema({
204
+ type: "object",
205
+ properties: { runId: { type: "string" } },
206
+ required: ["runId"],
207
+ }),
208
+ execute: async (input) => {
209
+ const job = getSelfVerifyJobManager().status(input?.runId);
210
+ if (!job)
211
+ return errLine("not_found", `runId ${input?.runId} not found`);
212
+ const summary = job.report && job.kind === "tier1" && "summary" in job.report
213
+ ? job.report.summary
214
+ : job.report && job.kind === "agentic" && "verdict" in job.report
215
+ ? { verdict: job.report.verdict }
216
+ : null;
217
+ return json({
218
+ runId: job.runId,
219
+ status: job.status,
220
+ kind: job.kind,
221
+ startedAt: job.startedAt,
222
+ finishedAt: job.finishedAt,
223
+ elapsedMs: (job.finishedAt ?? Date.now()) - job.startedAt,
224
+ logTail: job.logBuffer.slice(-40),
225
+ summary,
226
+ error: job.error,
227
+ });
228
+ },
229
+ });
230
+ tools.selfverify_result = dynamicTool({
231
+ description: "Fetch the full report of a completed self-verify run.",
232
+ inputSchema: jsonSchema({
233
+ type: "object",
234
+ properties: { runId: { type: "string" } },
235
+ required: ["runId"],
236
+ }),
237
+ execute: async (input) => {
238
+ const job = getSelfVerifyJobManager().status(input?.runId);
239
+ if (!job)
240
+ return errLine("not_found", `runId ${input?.runId} not found`);
241
+ if (job.status === "running")
242
+ return errLine("still_running", "run not finished; poll selfverify_status first");
243
+ if (job.status === "error")
244
+ return errLine("run_error", job.error ?? "unknown error");
245
+ if (job.status === "cancelled")
246
+ return errLine("cancelled", "run was cancelled");
247
+ return json(job.report ?? {});
248
+ },
249
+ });
250
+ tools.selfverify_list = dynamicTool({
251
+ description: "List recent self-verify runs.",
252
+ inputSchema: jsonSchema({ type: "object", properties: {}, additionalProperties: false }),
253
+ execute: async () => json(getSelfVerifyJobManager()
254
+ .list()
255
+ .map((j) => ({
256
+ runId: j.runId,
257
+ kind: j.kind,
258
+ status: j.status,
259
+ elapsedMs: (j.finishedAt ?? Date.now()) - j.startedAt,
260
+ }))),
261
+ });
262
+ tools.selfverify_cancel = dynamicTool({
263
+ description: "Cancel a running self-verify run (best-effort).",
264
+ inputSchema: jsonSchema({
265
+ type: "object",
266
+ properties: { runId: { type: "string" } },
267
+ required: ["runId"],
268
+ }),
269
+ execute: async (input) => json({ cancelled: getSelfVerifyJobManager().cancel(input?.runId) }),
270
+ });
271
+ return tools;
272
+ }
273
+ //# sourceMappingURL=native-tools.js.map
@@ -9,7 +9,8 @@
9
9
  * (no network).
10
10
  */
11
11
  import os from "node:os";
12
- import { describe, expect, it } from "vitest";
12
+ import { afterEach, describe, expect, it } from "vitest";
13
+ import { __resetArtifactCacheForTests, recordArtifact } from "../ee/artifact-cache.js";
13
14
  import { BashTool } from "./bash.js";
14
15
  import { createBuiltinTools, isToolArtifactQuery } from "./registry.js";
15
16
  describe("ee_query builtin tool", () => {
@@ -45,4 +46,20 @@ describe("isToolArtifactQuery — ee_query intent routing", () => {
45
46
  expect(isToolArtifactQuery("tool-artifact storage design")).toBe(false);
46
47
  });
47
48
  });
49
+ describe("ee_query — anti-mù rehydrate (local-first, durable when EE is down)", () => {
50
+ afterEach(() => __resetArtifactCacheForTests());
51
+ it("rehydrates a tool-artifact from the in-session cache with NO EE/network call", async () => {
52
+ // Simulates: the compactor elided this output earlier (recordArtifact), EE is
53
+ // now down. The agent's ee_query("tool-artifact id=X") must still return the
54
+ // full content from the local cache rather than an [ee_unavailable] note.
55
+ recordArtifact("call_42", "read_file", "FULL ELIDED CONTENT — line A\nline B\nline C");
56
+ const tools = createBuiltinTools(new BashTool(os.tmpdir()), "agent");
57
+ const t = tools.ee_query;
58
+ const out = String(await t.execute?.({ query: "tool-artifact id=call_42" }));
59
+ expect(out).toContain("rehydrated from in-session cache");
60
+ expect(out).toContain("tool=read_file");
61
+ expect(out).toContain("FULL ELIDED CONTENT");
62
+ expect(out).not.toMatch(/ee_unavailable/);
63
+ });
64
+ });
48
65
  //# sourceMappingURL=registry-ee-query.test.js.map
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Integration: git-safety guards wired into the bash tool (registry.ts).
3
+ * Unit logic lives in git-safety.test.ts; this asserts the WIRING — a blocked
4
+ * push returns the block message WITHOUT executing, and a broad stage appends
5
+ * the sensitive-path warning to the tool output.
6
+ */
7
+ export {};
@@ -0,0 +1,92 @@
1
+ /**
2
+ * Integration: git-safety guards wired into the bash tool (registry.ts).
3
+ * Unit logic lives in git-safety.test.ts; this asserts the WIRING — a blocked
4
+ * push returns the block message WITHOUT executing, and a broad stage appends
5
+ * the sensitive-path warning to the tool output.
6
+ */
7
+ import { mkdtempSync, rmSync, writeFileSync } from "node:fs";
8
+ import os from "node:os";
9
+ import { join } from "node:path";
10
+ import { afterEach, beforeEach, describe, expect, it } from "vitest";
11
+ import { BashTool } from "./bash.js";
12
+ import { clearBashOutputCache } from "./bash-output-cache.js";
13
+ import { __resetGitSafetyState, recordCommandOutcome } from "./git-safety.js";
14
+ import { createBuiltinTools } from "./registry.js";
15
+ async function runBash(tools, args) {
16
+ const t = tools.bash;
17
+ if (!t?.execute)
18
+ throw new Error("bash tool has no execute");
19
+ const out = await t.execute(args);
20
+ return typeof out === "string" ? out : JSON.stringify(out);
21
+ }
22
+ describe("git-safety wiring in bash tool", () => {
23
+ beforeEach(() => {
24
+ clearBashOutputCache();
25
+ globalThis.__muonroiBashRepeatState = new Map();
26
+ __resetGitSafetyState();
27
+ delete process.env.MUONROI_ALLOW_PUSH_ON_RED;
28
+ });
29
+ afterEach(() => {
30
+ delete process.env.MUONROI_ALLOW_PUSH_ON_RED;
31
+ });
32
+ it("BLOCKS git push (without executing) after a verification failed this session", async () => {
33
+ const bash = new BashTool(os.tmpdir());
34
+ const tools = createBuiltinTools(bash, "agent", { sessionId: "GS1" });
35
+ // Simulate a failed test earlier in the session.
36
+ recordCommandOutcome("GS1", "npm test", false);
37
+ const out = await runBash(tools, { command: "git push origin main", timeout: 10_000 });
38
+ expect(out).toMatch(/^BLOCKED:/);
39
+ expect(out).toMatch(/npm test/);
40
+ // The distinctive block message proves git push never ran (a real push in
41
+ // tmpdir would fail with a git error like "not a git repository", not this).
42
+ expect(out).not.toMatch(/not a git repository|fatal:/i);
43
+ }, 20_000);
44
+ it("ALLOWS git push once the failed verification re-runs green", async () => {
45
+ const bash = new BashTool(os.tmpdir());
46
+ const tools = createBuiltinTools(bash, "agent", { sessionId: "GS2" });
47
+ recordCommandOutcome("GS2", "npm test", false);
48
+ recordCommandOutcome("GS2", "npm test", true); // re-ran green
49
+ const out = await runBash(tools, { command: "git push origin main", timeout: 10_000 });
50
+ expect(out).not.toMatch(/^BLOCKED:/);
51
+ }, 20_000);
52
+ it("respects MUONROI_ALLOW_PUSH_ON_RED override", async () => {
53
+ process.env.MUONROI_ALLOW_PUSH_ON_RED = "1";
54
+ const bash = new BashTool(os.tmpdir());
55
+ const tools = createBuiltinTools(bash, "agent", { sessionId: "GS3" });
56
+ recordCommandOutcome("GS3", "vitest run", false);
57
+ const out = await runBash(tools, { command: "git push", timeout: 10_000 });
58
+ expect(out).not.toMatch(/^BLOCKED:/);
59
+ }, 20_000);
60
+ it("blocks push across registry rebuilds even with NO sessionId (stable process key)", async () => {
61
+ // Regression for the anon-key false negative: createBuiltinTools() without a
62
+ // sessionId must still gate the push, because production call sites
63
+ // (message-processor) don't thread sessionId and rebuild the registry every
64
+ // turn. A failing verify in one anon registry must block a push in the next.
65
+ const bash = new BashTool(os.tmpdir());
66
+ // `npm test` is a recognized verification command and fails fast here
67
+ // (no package.json in a temp dir) → recorded as a failed verify under the
68
+ // stable process key.
69
+ const toolsA = createBuiltinTools(bash, "agent"); // no sessionId
70
+ const failOut = await runBash(toolsA, { command: "npm test", timeout: 20_000 });
71
+ expect(failOut).toMatch(/ERROR/); // the verify failed
72
+ // Fresh anon registry (simulates the per-turn rebuild).
73
+ const toolsB = createBuiltinTools(bash, "agent"); // no sessionId
74
+ const pushOut = await runBash(toolsB, { command: "git push origin main", timeout: 10_000 });
75
+ expect(pushOut).toMatch(/^BLOCKED:/);
76
+ }, 30_000);
77
+ it("appends a sensitive-path WARNING on a broad git add when secrets exist", async () => {
78
+ const dir = mkdtempSync(join(os.tmpdir(), "gs-stage-"));
79
+ writeFileSync(join(dir, ".env"), "API_KEY=secret");
80
+ try {
81
+ const bash = new BashTool(dir);
82
+ const tools = createBuiltinTools(bash, "agent", { sessionId: "GS4" });
83
+ const out = await runBash(tools, { command: "git add -A", timeout: 10_000 });
84
+ expect(out).toMatch(/\[WARNING:/);
85
+ expect(out).toMatch(/\.env/);
86
+ }
87
+ finally {
88
+ rmSync(dir, { recursive: true, force: true });
89
+ }
90
+ }, 20_000);
91
+ });
92
+ //# sourceMappingURL=registry-git-safety.test.js.map