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.
- package/LICENSE +21 -21
- package/README.md +122 -122
- package/dist/packages/agent-harness-core/src/predicate.d.ts +1 -1
- package/dist/src/agent-harness/__tests__/mock-model.spec.js +48 -1
- package/dist/src/agent-harness/mock-model.d.ts +11 -0
- package/dist/src/agent-harness/mock-model.js +21 -0
- package/dist/src/cli/cost-forensics.js +12 -12
- package/dist/src/council/__tests__/clarification-prompt.test.js +51 -0
- package/dist/src/council/__tests__/clarifier-ready-gate.test.js +32 -0
- package/dist/src/council/__tests__/decisions-lock.test.js +17 -1
- package/dist/src/council/__tests__/oauth-reachable.test.d.ts +1 -0
- package/dist/src/council/__tests__/oauth-reachable.test.js +31 -0
- package/dist/src/council/__tests__/parse-outcome-fallback.test.js +11 -0
- package/dist/src/council/clarifier.js +9 -1
- package/dist/src/council/debate.js +5 -1
- package/dist/src/council/decisions-lock.js +3 -3
- package/dist/src/council/index.js +12 -5
- package/dist/src/council/leader.d.ts +0 -17
- package/dist/src/council/leader.js +22 -15
- package/dist/src/council/planner.js +1 -1
- package/dist/src/council/prompts.js +63 -57
- package/dist/src/council/types.d.ts +7 -0
- package/dist/src/ee/__tests__/ee-onboarding.test.d.ts +1 -0
- package/dist/src/ee/__tests__/ee-onboarding.test.js +32 -0
- package/dist/src/ee/auth.d.ts +9 -0
- package/dist/src/ee/auth.js +19 -0
- package/dist/src/ee/ee-onboarding.d.ts +5 -0
- package/dist/src/ee/ee-onboarding.js +76 -0
- package/dist/src/generated/version.d.ts +1 -1
- package/dist/src/generated/version.js +1 -1
- package/dist/src/headless/output.js +6 -4
- package/dist/src/headless/output.test.js +4 -3
- package/dist/src/index.js +20 -1
- package/dist/src/mcp/__tests__/auto-setup.test.js +74 -0
- package/dist/src/mcp/__tests__/client-pool.spec.d.ts +1 -0
- package/dist/src/mcp/__tests__/client-pool.spec.js +98 -0
- package/dist/src/mcp/__tests__/parallel-build.spec.d.ts +1 -0
- package/dist/src/mcp/__tests__/parallel-build.spec.js +67 -0
- package/dist/src/mcp/__tests__/smart-filter.test.js +56 -0
- package/dist/src/mcp/auto-setup.js +56 -2
- package/dist/src/mcp/client-pool.d.ts +46 -0
- package/dist/src/mcp/client-pool.js +212 -0
- package/dist/src/mcp/oauth-callback.js +2 -2
- package/dist/src/mcp/parse-headers.test.js +14 -14
- package/dist/src/mcp/runtime.d.ts +28 -0
- package/dist/src/mcp/runtime.js +117 -51
- package/dist/src/mcp/self-verify-runner.d.ts +14 -0
- package/dist/src/mcp/self-verify-runner.js +38 -0
- package/dist/src/mcp/setup-guide-text.d.ts +9 -0
- package/dist/src/mcp/setup-guide-text.js +84 -0
- package/dist/src/mcp/smart-filter.js +49 -0
- package/dist/src/mcp/smoke.test.js +43 -43
- package/dist/src/mcp/tools-server.d.ts +7 -0
- package/dist/src/mcp/tools-server.js +19 -22
- package/dist/src/models/catalog.json +349 -349
- package/dist/src/ops/__tests__/doctor-ee-health.test.js +21 -0
- package/dist/src/ops/doctor.d.ts +3 -2
- package/dist/src/ops/doctor.js +47 -11
- package/dist/src/ops/doctor.test.js +4 -3
- package/dist/src/orchestrator/__tests__/mcp-capability-block.test.d.ts +1 -0
- package/dist/src/orchestrator/__tests__/mcp-capability-block.test.js +39 -0
- package/dist/src/orchestrator/__tests__/project-stack.test.d.ts +1 -0
- package/dist/src/orchestrator/__tests__/project-stack.test.js +65 -0
- package/dist/src/orchestrator/batch-turn-runner.js +7 -11
- package/dist/src/orchestrator/message-processor.js +57 -27
- package/dist/src/orchestrator/orchestrator.js +26 -0
- package/dist/src/orchestrator/prompts.d.ts +51 -0
- package/dist/src/orchestrator/prompts.js +257 -134
- package/dist/src/orchestrator/scope-ceiling.js +6 -1
- package/dist/src/orchestrator/stream-runner.js +20 -15
- package/dist/src/orchestrator/text-tool-call-detector.test.js +13 -13
- package/dist/src/pil/__tests__/clarity-gate.test.js +24 -215
- package/dist/src/pil/__tests__/config.test.js +1 -17
- package/dist/src/pil/__tests__/discovery.test.js +144 -11
- package/dist/src/pil/__tests__/layer1-intent-trace.test.js +7 -2
- package/dist/src/pil/__tests__/layer1-intent.test.js +3 -0
- package/dist/src/pil/__tests__/layer16-clarity.test.js +32 -116
- package/dist/src/pil/__tests__/layer4-gsd.test.js +37 -0
- package/dist/src/pil/__tests__/layer6-output.test.js +137 -18
- package/dist/src/pil/__tests__/llm-classify.test.js +49 -2
- package/dist/src/pil/agent-operating-contract.d.ts +1 -1
- package/dist/src/pil/agent-operating-contract.js +2 -0
- package/dist/src/pil/agent-operating-contract.test.js +7 -2
- package/dist/src/pil/cheap-model-playbook.js +35 -35
- package/dist/src/pil/cheap-model-workbooks.js +16 -13
- package/dist/src/pil/clarity-gate.d.ts +21 -19
- package/dist/src/pil/clarity-gate.js +26 -153
- package/dist/src/pil/config.d.ts +9 -1
- package/dist/src/pil/config.js +15 -4
- package/dist/src/pil/discovery.js +211 -136
- package/dist/src/pil/layer1-intent.d.ts +12 -0
- package/dist/src/pil/layer1-intent.js +283 -38
- package/dist/src/pil/layer1-intent.test.js +210 -4
- package/dist/src/pil/layer16-clarity.d.ts +25 -11
- package/dist/src/pil/layer16-clarity.js +19 -306
- package/dist/src/pil/layer4-gsd.js +18 -6
- package/dist/src/pil/layer6-output.d.ts +2 -0
- package/dist/src/pil/layer6-output.js +137 -22
- package/dist/src/pil/llm-classify.d.ts +26 -0
- package/dist/src/pil/llm-classify.js +34 -5
- package/dist/src/pil/native-capabilities-workbook.d.ts +1 -1
- package/dist/src/pil/native-capabilities-workbook.js +82 -76
- package/dist/src/pil/schema.d.ts +8 -0
- package/dist/src/pil/schema.js +12 -1
- package/dist/src/pil/task-tier-map.js +4 -0
- package/dist/src/pil/types.d.ts +11 -1
- package/dist/src/product-loop/done-gate.js +3 -3
- package/dist/src/product-loop/loop-driver.js +18 -18
- package/dist/src/product-loop/progress-snapshot.js +4 -4
- package/dist/src/providers/auth/gemini-oauth.js +6 -15
- package/dist/src/providers/auth/grok-oauth.js +6 -15
- package/dist/src/providers/auth/openai-oauth.js +6 -15
- package/dist/src/providers/mcp-vision-bridge.js +48 -48
- package/dist/src/reporter/index.js +1 -1
- package/dist/src/scaffold/bb-ecosystem-apply.js +47 -47
- package/dist/src/scaffold/bb-quality-gate.js +5 -5
- package/dist/src/scaffold/continuation-prompt.js +60 -60
- package/dist/src/scaffold/init-new.js +453 -453
- package/dist/src/self-qa/__tests__/scenario-planner.test.js +3 -3
- package/dist/src/self-qa/agentic-loop.js +24 -19
- package/dist/src/self-qa/spec-emitter.js +26 -23
- package/dist/src/storage/__tests__/migrations.test.js +2 -2
- package/dist/src/storage/interaction-log.js +5 -5
- package/dist/src/storage/migrations.js +122 -122
- package/dist/src/storage/sessions.js +42 -42
- package/dist/src/storage/transcript.js +91 -84
- package/dist/src/storage/usage.js +14 -14
- package/dist/src/storage/workspaces.js +12 -12
- package/dist/src/tools/__tests__/native-tools.test.d.ts +1 -0
- package/dist/src/tools/__tests__/native-tools.test.js +53 -0
- package/dist/src/tools/git-safety.d.ts +61 -0
- package/dist/src/tools/git-safety.js +141 -0
- package/dist/src/tools/git-safety.test.d.ts +1 -0
- package/dist/src/tools/git-safety.test.js +111 -0
- package/dist/src/tools/native-tools.d.ts +31 -0
- package/dist/src/tools/native-tools.js +273 -0
- package/dist/src/tools/registry-git-safety.test.d.ts +7 -0
- package/dist/src/tools/registry-git-safety.test.js +92 -0
- package/dist/src/tools/registry.js +39 -4
- package/dist/src/ui/__tests__/markdown-render.test.d.ts +1 -0
- package/dist/src/ui/__tests__/markdown-render.test.js +48 -0
- package/dist/src/ui/app.js +0 -0
- package/dist/src/ui/components/message-view.js +4 -1
- package/dist/src/ui/components/structured-response-view.js +7 -3
- package/dist/src/ui/components/tool-group.js +7 -1
- package/dist/src/ui/markdown-render.d.ts +41 -0
- package/dist/src/ui/markdown-render.js +223 -0
- package/dist/src/ui/markdown.d.ts +10 -0
- package/dist/src/ui/markdown.js +12 -35
- package/dist/src/ui/slash/council-inspect.js +4 -4
- package/dist/src/ui/slash/export.js +4 -4
- package/dist/src/ui/utils/text.d.ts +8 -0
- package/dist/src/ui/utils/text.js +16 -0
- package/dist/src/ui/utils/text.test.d.ts +1 -0
- package/dist/src/ui/utils/text.test.js +23 -0
- package/dist/src/usage/ledger.js +48 -15
- package/dist/src/utils/__tests__/footprint-gitignore.test.d.ts +1 -0
- package/dist/src/utils/__tests__/footprint-gitignore.test.js +50 -0
- package/dist/src/utils/clipboard-image.js +23 -23
- package/dist/src/utils/open-url.d.ts +56 -0
- package/dist/src/utils/open-url.js +58 -0
- package/dist/src/utils/open-url.test.d.ts +1 -0
- package/dist/src/utils/open-url.test.js +86 -0
- package/dist/src/utils/settings.d.ts +12 -0
- package/dist/src/utils/settings.js +48 -0
- package/dist/src/utils/side-question.js +2 -2
- package/dist/src/utils/skills.js +3 -3
- package/dist/src/verify/__tests__/coverage-parsers.test.js +30 -30
- package/dist/src/verify/environment.js +2 -1
- package/package.json +1 -1
- package/dist/src/pil/layer16-clarity.test.js +0 -31
- /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
|
|
@@ -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
|
|
@@ -12,7 +12,9 @@ import { needsVisionProxy } from "../providers/vision-proxy.js";
|
|
|
12
12
|
import { getBashRun, sliceBashOutput } from "./bash-output-cache.js";
|
|
13
13
|
import { editFile, readFile, writeFile } from "./file.js";
|
|
14
14
|
import { FileTracker } from "./file-tracker.js";
|
|
15
|
+
import { analyzeGitCommand, checkPushGate, pushBlockedMessage, recordCommandOutcome, stagingWarning, } from "./git-safety.js";
|
|
15
16
|
import { executeGrep } from "./grep.js";
|
|
17
|
+
import { registerNativeMuonroiTools } from "./native-tools.js";
|
|
16
18
|
import { VISION_TOOL_NAMES } from "./vision-gate.js";
|
|
17
19
|
function getBashRepeatState() {
|
|
18
20
|
if (!globalThis.__muonroiBashRepeatState) {
|
|
@@ -124,6 +126,15 @@ export function createBuiltinTools(bash, mode, opts) {
|
|
|
124
126
|
// user turns / askcards no longer wipes it. See getBashRepeatState().
|
|
125
127
|
const repeatState = getBashRepeatState();
|
|
126
128
|
const repeatKey = resolveBashRepeatKey(opts?.sessionId);
|
|
129
|
+
// Git-safety state key. MUST be stable across createBuiltinTools() rebuilds
|
|
130
|
+
// within one process — otherwise a failed-test record made before a registry
|
|
131
|
+
// rebuild (askcard answer, sub-agent turn) would be invisible to the push
|
|
132
|
+
// gate after the rebuild. Unlike resolveBashRepeatKey's anon fallback (which
|
|
133
|
+
// intentionally generates a fresh key per instance to isolate repeat-reminder
|
|
134
|
+
// state), we want the gate to PERSIST: use the real sessionId when present,
|
|
135
|
+
// else a single process-stable key. Over-sharing here is the safe direction
|
|
136
|
+
// (it can only over-block a push, never wrongly allow one).
|
|
137
|
+
const gitSafetyKey = opts?.sessionId && opts.sessionId.length > 0 ? opts.sessionId : `__proc_default__:${process.pid}`;
|
|
127
138
|
tools.bash = dynamicTool({
|
|
128
139
|
description: "Execute a shell command. Output is automatically cached — every call returns a " +
|
|
129
140
|
"run_id you can re-query via bash_output_get(run_id, mode=tail|head|grep|lines). " +
|
|
@@ -149,19 +160,36 @@ export function createBuiltinTools(bash, mode, opts) {
|
|
|
149
160
|
if (typeof input.command !== "string" || input.command.trim() === "") {
|
|
150
161
|
return 'ERROR: the `bash` tool requires a non-empty "command" string, but the call had empty arguments. Provide the shell command to run, e.g. {"command":"ls -la"}.';
|
|
151
162
|
}
|
|
163
|
+
const cmd = typeof input.command === "string" ? input.command : "";
|
|
164
|
+
// Git safety (pre-execution). Block `git push` while a verification
|
|
165
|
+
// command failed this session and was not re-run green; warn on broad
|
|
166
|
+
// `git add -A` / `git commit -a` when sensitive paths exist. Applied to
|
|
167
|
+
// BOTH foreground and background paths. See git-safety.ts for the audit
|
|
168
|
+
// motivation (session 18285908637a). gitSafetyKey is STABLE per process
|
|
169
|
+
// (or the real sessionId) — unlike repeatKey, whose anon fallback changes
|
|
170
|
+
// on every registry rebuild and would silently drop the gate across turns.
|
|
171
|
+
const gitShape = analyzeGitCommand(cmd);
|
|
172
|
+
const stageWarn = gitShape.isBroadStage ? stagingWarning(bash.getCwd()) : "";
|
|
173
|
+
if (gitShape.isPush) {
|
|
174
|
+
const gate = checkPushGate(gitSafetyKey);
|
|
175
|
+
if (gate.blocked) {
|
|
176
|
+
return `${pushBlockedMessage(gate.failed)}${stageWarn}`;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
152
179
|
if (input.background) {
|
|
153
180
|
const result = await bash.startBackground(input.command);
|
|
154
|
-
return formatResult(result)
|
|
181
|
+
return `${formatResult(result)}${stageWarn}`;
|
|
155
182
|
}
|
|
156
183
|
// 3-3: compute canonical form BEFORE running so we can attach an
|
|
157
184
|
// inline reminder if it matches the previous bash call.
|
|
158
|
-
const cmd = typeof input.command === "string" ? input.command : "";
|
|
159
185
|
const canonical = cmd ? canonicalizeBashCommand(cmd) : "";
|
|
160
186
|
const entry = repeatState.get(repeatKey) ?? { lastCanonical: null, lastRunId: null };
|
|
161
187
|
const repeatedIntent = canonical !== "" && canonical === entry.lastCanonical && entry.lastRunId !== null;
|
|
162
188
|
const prevRunId = entry.lastRunId;
|
|
163
189
|
const result = await bash.execute(input.command, input.timeout ?? 30000);
|
|
164
190
|
const formatted = formatResult(result);
|
|
191
|
+
// Record verification outcome so a later `git push` can be gated on it.
|
|
192
|
+
recordCommandOutcome(gitSafetyKey, canonical, result.success);
|
|
165
193
|
// Update last-canonical state AFTER we compared, so the current call's
|
|
166
194
|
// runId becomes the comparison target for the next one. Session-scoped
|
|
167
195
|
// map persists across createBuiltinTools() rebuilds (Phase 4R).
|
|
@@ -185,9 +213,9 @@ export function createBuiltinTools(bash, mode, opts) {
|
|
|
185
213
|
const hint = chars >= 4_000
|
|
186
214
|
? ` — ${chars} chars cached; use bash_output_get(run_id, mode=tail|head|grep|lines) to re-query`
|
|
187
215
|
: "";
|
|
188
|
-
return `${formatted}\n\n[bash_run_id: ${result.bashRunId}${hint}]${reminder}`;
|
|
216
|
+
return `${formatted}\n\n[bash_run_id: ${result.bashRunId}${hint}]${reminder}${stageWarn}`;
|
|
189
217
|
}
|
|
190
|
-
return formatted
|
|
218
|
+
return `${formatted}${stageWarn}`;
|
|
191
219
|
},
|
|
192
220
|
});
|
|
193
221
|
// bash_output_get — re-query the cached full output of a previous bash run.
|
|
@@ -473,6 +501,13 @@ export function createBuiltinTools(bash, mode, opts) {
|
|
|
473
501
|
}
|
|
474
502
|
},
|
|
475
503
|
});
|
|
504
|
+
// Native muonroi-tools builtins — ee_health, ee_feedback, usage_forensics,
|
|
505
|
+
// lsp_query, setup_guide, selfverify_*. These run IN-PROCESS; the CLI no
|
|
506
|
+
// longer self-spawns itself as an MCP server to expose them to its own inner
|
|
507
|
+
// agent (that self-spawn cold-started 2-3.5s and overran the build deadline,
|
|
508
|
+
// and a seed-time bug once persisted a crashing vitest-worker command). The
|
|
509
|
+
// muonroi-tools MCP server stays only for EXTERNAL agents. See native-tools.ts.
|
|
510
|
+
registerNativeMuonroiTools(tools, { cwd: bash.getCwd() });
|
|
476
511
|
}
|
|
477
512
|
// Vision proxy tools — only for text-only models (DeepSeek, etc.)
|
|
478
513
|
if (opts?.modelId && needsVisionProxy(opts.modelId)) {
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|