muonroi-cli 1.7.1 → 1.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -21
- package/README.md +122 -122
- package/dist/packages/agent-harness-core/src/event-filter.js +1 -0
- package/dist/packages/agent-harness-core/src/event-redact.js +8 -0
- package/dist/packages/agent-harness-core/src/mcp-server.d.ts +16 -0
- package/dist/packages/agent-harness-core/src/mcp-server.js +88 -10
- package/dist/packages/agent-harness-core/src/protocol.d.ts +15 -0
- package/dist/packages/agent-harness-core/src/selector.js +2 -2
- package/dist/packages/agent-harness-core/src/transports/sidechannel.js +1 -1
- package/dist/packages/agent-harness-opentui/src/agent-mode.js +1 -1
- package/dist/packages/agent-harness-opentui/src/install.js +1 -1
- package/dist/src/__test-stubs__/vitest-setup.js +4 -0
- package/dist/src/agent-harness/__tests__/mock-model.spec.js +54 -0
- package/dist/src/agent-harness/mock-model.d.ts +1 -1
- package/dist/src/agent-harness/mock-model.js +32 -3
- package/dist/src/agent-harness/test-spawn.js +3 -1
- package/dist/src/chat/chat-keychain.js +9 -1
- package/dist/src/cli/cost-forensics.js +12 -12
- package/dist/src/cli/keys.d.ts +2 -2
- package/dist/src/cli/keys.js +107 -28
- package/dist/src/cli/usage-report.js +2 -2
- package/dist/src/council/__tests__/abort-threading.test.d.ts +1 -0
- package/dist/src/council/__tests__/abort-threading.test.js +193 -0
- package/dist/src/council/__tests__/clarification-prompt.test.js +21 -1
- package/dist/src/council/__tests__/council-turn-length-emit.test.d.ts +1 -0
- package/dist/src/council/__tests__/council-turn-length-emit.test.js +130 -0
- package/dist/src/council/__tests__/debate-round-budget.test.d.ts +1 -0
- package/dist/src/council/__tests__/debate-round-budget.test.js +45 -0
- package/dist/src/council/__tests__/evaluator-metrics.test.js +3 -0
- package/dist/src/council/__tests__/post-debate-recommendation.test.d.ts +1 -0
- package/dist/src/council/__tests__/post-debate-recommendation.test.js +58 -0
- package/dist/src/council/__tests__/research-tools.test.js +4 -0
- package/dist/src/council/__tests__/round-tools.test.js +6 -0
- package/dist/src/council/clarifier-question-cap.test.d.ts +12 -0
- package/dist/src/council/clarifier-question-cap.test.js +73 -0
- package/dist/src/council/clarifier.d.ts +29 -1
- package/dist/src/council/clarifier.js +64 -1
- package/dist/src/council/debate-planner.d.ts +2 -1
- package/dist/src/council/debate-planner.js +5 -3
- package/dist/src/council/debate.d.ts +9 -0
- package/dist/src/council/debate.js +88 -8
- package/dist/src/council/index.d.ts +46 -1
- package/dist/src/council/index.js +125 -19
- package/dist/src/council/llm.js +41 -13
- package/dist/src/council/prompts.js +56 -21
- package/dist/src/council/types.d.ts +14 -1
- package/dist/src/ee/__tests__/export-transcripts.test.js +1 -1
- package/dist/src/ee/bridge.d.ts +3 -1
- package/dist/src/ee/bridge.js +9 -3
- package/dist/src/ee/bridge.test.js +1 -2
- package/dist/src/ee/client.js +15 -2
- package/dist/src/ee/recall-ledger.d.ts +20 -0
- package/dist/src/ee/recall-ledger.js +40 -1
- package/dist/src/ee/search.d.ts +25 -0
- package/dist/src/ee/search.js +73 -0
- package/dist/src/ee/types.d.ts +15 -1
- package/dist/src/ee/who-am-i.d.ts +41 -0
- package/dist/src/ee/who-am-i.js +143 -0
- package/dist/src/ee/who-am-i.test.d.ts +1 -0
- package/dist/src/ee/who-am-i.test.js +101 -0
- package/dist/src/generated/version.d.ts +1 -1
- package/dist/src/generated/version.js +1 -1
- package/dist/src/headless/council-answers.js +1 -0
- package/dist/src/headless/output.d.ts +30 -1
- package/dist/src/headless/output.js +180 -0
- package/dist/src/headless/output.test.js +61 -1
- package/dist/src/hooks/index.js +48 -2
- package/dist/src/index.d.ts +2 -0
- package/dist/src/index.js +143 -104
- package/dist/src/lsp/client-normalize.test.d.ts +12 -0
- package/dist/src/lsp/client-normalize.test.js +34 -0
- package/dist/src/lsp/client.d.ts +1 -0
- package/dist/src/lsp/client.js +10 -2
- package/dist/src/lsp/manager.d.ts +1 -1
- package/dist/src/lsp/manager.js +2 -2
- package/dist/src/lsp/manager.test.js +2 -2
- package/dist/src/lsp/runtime.d.ts +1 -1
- package/dist/src/lsp/runtime.js +2 -2
- package/dist/src/maintain/__tests__/gh-create-pr.test.js +7 -3
- package/dist/src/maintain/__tests__/pr-builder.test.js +7 -3
- package/dist/src/mcp/__tests__/client-pool.spec.js +3 -3
- package/dist/src/mcp/__tests__/ee-tools.test.js +42 -14
- package/dist/src/mcp/__tests__/forensics-tools.test.js +3 -3
- package/dist/src/mcp/__tests__/lsp-tools.test.js +4 -4
- package/dist/src/mcp/client-pool.d.ts +1 -1
- package/dist/src/mcp/client-pool.js +25 -9
- package/dist/src/mcp/ee-tools.d.ts +10 -0
- package/dist/src/mcp/ee-tools.js +34 -0
- package/dist/src/mcp/oauth-callback.js +2 -2
- package/dist/src/mcp/parse-headers.test.js +14 -14
- package/dist/src/mcp/self-verify-jobs.js +1 -3
- package/dist/src/mcp/setup-guide-text.js +74 -74
- package/dist/src/mcp/smoke.test.js +43 -43
- package/dist/src/models/catalog-gemini.test.js +12 -10
- package/dist/src/models/catalog.json +106 -38
- package/dist/src/ops/doctor.js +7 -7
- package/dist/src/orchestrator/__tests__/auto-commit.test.d.ts +1 -0
- package/dist/src/orchestrator/__tests__/auto-commit.test.js +142 -0
- package/dist/src/orchestrator/__tests__/batch-turn-runner.test.js +2 -0
- package/dist/src/orchestrator/__tests__/delegations.test.d.ts +1 -0
- package/dist/src/orchestrator/__tests__/delegations.test.js +96 -0
- package/dist/src/orchestrator/__tests__/message-processor.test.js +1 -0
- package/dist/src/orchestrator/__tests__/stream-runner.test.js +29 -0
- package/dist/src/orchestrator/agent.test.js +5 -2
- package/dist/src/orchestrator/auto-commit.d.ts +110 -0
- package/dist/src/orchestrator/auto-commit.js +364 -0
- package/dist/src/orchestrator/batch-turn-runner.d.ts +3 -2
- package/dist/src/orchestrator/batch-turn-runner.js +2 -1
- package/dist/src/orchestrator/compaction-proposer-prompt.d.ts +8 -0
- package/dist/src/orchestrator/compaction-proposer-prompt.js +43 -0
- package/dist/src/orchestrator/compaction.d.ts +22 -0
- package/dist/src/orchestrator/compaction.js +42 -0
- package/dist/src/orchestrator/delegations.d.ts +1 -0
- package/dist/src/orchestrator/delegations.js +41 -0
- package/dist/src/orchestrator/error-utils.js +27 -2
- package/dist/src/orchestrator/message-processor.d.ts +21 -5
- package/dist/src/orchestrator/message-processor.js +219 -2573
- package/dist/src/orchestrator/orchestrator.d.ts +8 -2
- package/dist/src/orchestrator/orchestrator.js +187 -34
- package/dist/src/orchestrator/preprocessor.d.ts +13 -0
- package/dist/src/orchestrator/preprocessor.js +145 -0
- package/dist/src/orchestrator/prompts.d.ts +8 -0
- package/dist/src/orchestrator/prompts.js +76 -60
- package/dist/src/orchestrator/safety-askcard.d.ts +66 -0
- package/dist/src/orchestrator/safety-askcard.js +74 -0
- package/dist/src/orchestrator/sandbox.test.js +8 -5
- package/dist/src/orchestrator/scope-ceiling.js +1 -1
- package/dist/src/orchestrator/stall-watchdog.d.ts +47 -0
- package/dist/src/orchestrator/stall-watchdog.js +23 -0
- package/dist/src/orchestrator/stall-watchdog.test.js +36 -1
- package/dist/src/orchestrator/stream-runner.js +52 -30
- package/dist/src/orchestrator/sub-agent-model-tier.js +1 -1
- package/dist/src/orchestrator/subagent-compactor.d.ts +14 -0
- package/dist/src/orchestrator/subagent-compactor.js +83 -10
- package/dist/src/orchestrator/subagent-compactor.spec.js +132 -0
- package/dist/src/orchestrator/text-tool-call-detector.d.ts +9 -5
- package/dist/src/orchestrator/text-tool-call-detector.js +16 -12
- package/dist/src/orchestrator/text-tool-call-detector.test.js +66 -8
- package/dist/src/orchestrator/tool-args-repair.js +1 -1
- package/dist/src/orchestrator/tool-engine.d.ts +151 -0
- package/dist/src/orchestrator/tool-engine.js +3079 -0
- package/dist/src/orchestrator/turn-runner-deps.d.ts +1 -0
- package/dist/src/pil/__tests__/discovery-types.test.js +15 -4
- package/dist/src/pil/__tests__/discovery.test.js +89 -145
- package/dist/src/pil/__tests__/dual-run.test.js +2 -0
- package/dist/src/pil/__tests__/layer1-intent-trace.test.js +3 -0
- package/dist/src/pil/__tests__/layer1-intent.test.js +3 -0
- package/dist/src/pil/__tests__/layer16-clarity.test.js +35 -32
- package/dist/src/pil/__tests__/layer18-acceptance.test.js +1 -78
- package/dist/src/pil/__tests__/layer2_5-ponytail.test.d.ts +1 -0
- package/dist/src/pil/__tests__/layer2_5-ponytail.test.js +53 -0
- package/dist/src/pil/__tests__/layer3-ee-injection.test.js +67 -4
- package/dist/src/pil/__tests__/layer3-injected-chunk.test.js +14 -2
- package/dist/src/pil/__tests__/layer4-gsd.test.js +30 -0
- package/dist/src/pil/__tests__/layer5-context.test.js +39 -1
- package/dist/src/pil/__tests__/layer6-output.test.js +30 -2
- package/dist/src/pil/__tests__/orchestrator-integration.test.js +3 -1
- package/dist/src/pil/__tests__/pipeline.test.js +11 -8
- package/dist/src/pil/__tests__/response-tools.test.js +52 -0
- package/dist/src/pil/agent-operating-contract.d.ts +1 -1
- package/dist/src/pil/agent-operating-contract.js +5 -3
- package/dist/src/pil/agent-operating-contract.test.js +2 -2
- package/dist/src/pil/cheap-model-playbook.d.ts +1 -1
- package/dist/src/pil/cheap-model-playbook.js +1 -1
- package/dist/src/pil/cheap-model-workbooks.d.ts +1 -1
- package/dist/src/pil/cheap-model-workbooks.js +1 -1
- package/dist/src/pil/config.d.ts +20 -0
- package/dist/src/pil/config.js +35 -0
- package/dist/src/pil/discovery-types.d.ts +32 -9
- package/dist/src/pil/discovery.d.ts +3 -3
- package/dist/src/pil/discovery.js +138 -244
- package/dist/src/pil/index.d.ts +1 -1
- package/dist/src/pil/index.js +1 -1
- package/dist/src/pil/layer1-intent.d.ts +9 -1
- package/dist/src/pil/layer1-intent.js +99 -44
- package/dist/src/pil/layer1-intent.test.js +112 -2
- package/dist/src/pil/layer15-context-scan.js +1 -1
- package/dist/src/pil/layer16-clarity.d.ts +21 -25
- package/dist/src/pil/layer16-clarity.js +52 -88
- package/dist/src/pil/layer18-acceptance.d.ts +13 -3
- package/dist/src/pil/layer18-acceptance.js +1 -75
- package/dist/src/pil/layer2-personality.js +7 -8
- package/dist/src/pil/layer2_5-ponytail.d.ts +2 -0
- package/dist/src/pil/layer2_5-ponytail.js +41 -0
- package/dist/src/pil/layer3-ee-injection.js +180 -14
- package/dist/src/pil/layer4-gsd.js +22 -8
- package/dist/src/pil/layer5-context.d.ts +1 -0
- package/dist/src/pil/layer5-context.js +26 -11
- package/dist/src/pil/layer6-output.js +23 -5
- package/dist/src/pil/llm-classify.js +3 -3
- package/dist/src/pil/native-capabilities-workbook.d.ts +1 -1
- package/dist/src/pil/native-capabilities-workbook.js +4 -3
- package/dist/src/pil/pipeline.js +15 -1
- package/dist/src/pil/response-tools.d.ts +16 -0
- package/dist/src/pil/response-tools.js +41 -0
- package/dist/src/pil/schema.d.ts +8 -0
- package/dist/src/pil/schema.js +24 -3
- package/dist/src/pil/task-tier-map.js +7 -7
- package/dist/src/pil/types.d.ts +9 -3
- package/dist/src/product-loop/__tests__/discovery-interview.test.js +103 -1
- package/dist/src/product-loop/__tests__/gather-selectable-alts.test.js +34 -0
- package/dist/src/product-loop/__tests__/sprint-self-verify.test.js +12 -12
- package/dist/src/product-loop/discovery-interview.d.ts +20 -0
- package/dist/src/product-loop/discovery-interview.js +121 -7
- package/dist/src/product-loop/done-gate.js +3 -3
- package/dist/src/product-loop/gather.js +19 -5
- package/dist/src/product-loop/loop-driver.js +20 -20
- package/dist/src/product-loop/progress-snapshot.js +4 -4
- package/dist/src/product-loop/sprint-self-verify.js +2 -2
- package/dist/src/providers/__tests__/reasoning-roundtrip.test.js +12 -0
- package/dist/src/providers/auth/__tests__/gemini-oauth.test.d.ts +1 -5
- package/dist/src/providers/auth/__tests__/gemini-oauth.test.js +1 -5
- package/dist/src/providers/auth/gcloud.d.ts +28 -0
- package/dist/src/providers/auth/gcloud.js +102 -0
- package/dist/src/providers/auth/gemini-oauth.d.ts +46 -19
- package/dist/src/providers/auth/gemini-oauth.js +292 -105
- package/dist/src/providers/auth/grok-oauth.d.ts +1 -1
- package/dist/src/providers/auth/grok-oauth.js +1 -1
- package/dist/src/providers/auth/registry.js +28 -6
- package/dist/src/providers/gemini.d.ts +1 -1
- package/dist/src/providers/gemini.js +10 -3
- package/dist/src/providers/keychain.d.ts +2 -2
- package/dist/src/providers/keychain.js +20 -2
- package/dist/src/providers/mcp-vision-bridge.js +48 -48
- package/dist/src/providers/openai.js +5 -1
- package/dist/src/providers/runtime.d.ts +1 -1
- package/dist/src/providers/runtime.js +11 -2
- package/dist/src/providers/siliconflow-sse-repair.js +1 -1
- package/dist/src/providers/strategies/google.strategy.d.ts +9 -2
- package/dist/src/providers/strategies/google.strategy.js +159 -9
- package/dist/src/providers/strategies/thinking-mode.js +19 -6
- package/dist/src/providers/types.d.ts +1 -1
- package/dist/src/reporter/index.js +1 -1
- package/dist/src/router/decide.test.js +40 -8
- package/dist/src/router/step-router.d.ts +20 -0
- package/dist/src/router/step-router.js +73 -0
- 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 +19 -19
- package/dist/src/self-qa/orchestrator.js +5 -5
- package/dist/src/self-qa/scenario-planner.js +1 -1
- package/dist/src/self-qa/spec-emitter.js +4 -4
- package/dist/src/storage/__tests__/migrations.test.js +2 -2
- package/dist/src/storage/index.d.ts +1 -1
- package/dist/src/storage/index.js +1 -1
- package/dist/src/storage/interaction-log.js +5 -5
- package/dist/src/storage/migrations.js +122 -122
- package/dist/src/storage/session-experience-store.js +4 -4
- package/dist/src/storage/sessions.d.ts +6 -0
- package/dist/src/storage/sessions.js +49 -42
- package/dist/src/storage/transcript-view.js +7 -1
- package/dist/src/storage/transcript.d.ts +1 -0
- package/dist/src/storage/transcript.js +55 -0
- package/dist/src/storage/ui-interaction-log.d.ts +17 -0
- package/dist/src/storage/usage.js +14 -14
- package/dist/src/storage/workspaces.js +12 -12
- package/dist/src/tools/__tests__/native-tools.test.js +6 -0
- package/dist/src/tools/bash.d.ts +36 -23
- package/dist/src/tools/bash.js +59 -301
- package/dist/src/tools/bash.test.js +2 -257
- package/dist/src/tools/file-tracker.d.ts +5 -0
- package/dist/src/tools/file-tracker.js +14 -0
- package/dist/src/tools/file.js +2 -0
- package/dist/src/tools/git-safety.d.ts +38 -1
- package/dist/src/tools/git-safety.js +94 -4
- package/dist/src/tools/git-safety.test.js +45 -1
- package/dist/src/tools/native-tools.d.ts +1 -1
- package/dist/src/tools/native-tools.js +56 -0
- package/dist/src/tools/registry-bash-empty-command.test.js +4 -3
- package/dist/src/tools/registry-git-safety.test.js +17 -6
- package/dist/src/tools/registry.d.ts +5 -0
- package/dist/src/tools/registry.js +185 -11
- package/dist/src/types/index.d.ts +7 -1
- package/dist/src/ui/app.d.ts +2 -1
- package/dist/src/ui/app.js +0 -0
- package/dist/src/ui/components/council-question-card.js +1 -0
- package/dist/src/ui/components/message-view.js +37 -3
- package/dist/src/ui/components/structured-response-view.js +7 -0
- package/dist/src/ui/components/task-list-panel.js +3 -12
- package/dist/src/ui/containers/chat-feed.d.ts +40 -0
- package/dist/src/ui/containers/chat-feed.js +66 -0
- package/dist/src/ui/containers/modals-layer.d.ts +87 -0
- package/dist/src/ui/containers/modals-layer.js +18 -0
- package/dist/src/ui/modals/session-picker-modal.js +14 -9
- package/dist/src/ui/modals/update-modal.js +2 -1
- package/dist/src/ui/slash/__tests__/menu-parity.test.d.ts +1 -0
- package/dist/src/ui/slash/__tests__/menu-parity.test.js +1 -0
- package/dist/src/ui/slash/__tests__/ponytail.test.d.ts +1 -0
- package/dist/src/ui/slash/__tests__/ponytail.test.js +37 -0
- package/dist/src/ui/slash/council-inspect.js +4 -4
- package/dist/src/ui/slash/export.js +16 -1
- package/dist/src/ui/slash/menu-items.js +1 -0
- package/dist/src/ui/slash/ponytail.d.ts +2 -0
- package/dist/src/ui/slash/ponytail.js +19 -0
- package/dist/src/ui/status-bar/store.js +0 -1
- package/dist/src/ui/status-bar/store.test.js +2 -2
- package/dist/src/ui/types.d.ts +7 -0
- package/dist/src/ui/use-app-logic.d.ts +26 -0
- package/dist/src/ui/use-app-logic.js +0 -0
- package/dist/src/ui/utils/relaunch.d.ts +17 -0
- package/dist/src/ui/utils/relaunch.js +28 -4
- package/dist/src/ui/utils/relaunch.test.js +50 -0
- package/dist/src/ui/utils/text.d.ts +11 -0
- package/dist/src/ui/utils/text.js +21 -1
- package/dist/src/ui/utils/text.test.js +25 -1
- package/dist/src/ui/utils/tools.js +3 -1
- package/dist/src/usage/cost-log.js +2 -2
- package/dist/src/usage/estimator.d.ts +14 -0
- package/dist/src/usage/estimator.js +22 -0
- package/dist/src/utils/__tests__/compaction-caps.test.d.ts +21 -0
- package/dist/src/utils/__tests__/compaction-caps.test.js +187 -0
- package/dist/src/utils/__tests__/llm-deadline-abort.test.d.ts +1 -0
- package/dist/src/utils/__tests__/llm-deadline-abort.test.js +33 -0
- package/dist/src/utils/clipboard-image.js +23 -23
- package/dist/src/utils/llm-deadline.d.ts +9 -1
- package/dist/src/utils/llm-deadline.js +31 -2
- package/dist/src/utils/permission-mode.d.ts +33 -0
- package/dist/src/utils/permission-mode.js +164 -3
- package/dist/src/utils/permission-mode.test.js +92 -1
- package/dist/src/utils/settings.d.ts +27 -31
- package/dist/src/utils/settings.js +100 -109
- 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/package.json +1 -1
|
@@ -26,6 +26,7 @@ import { SETUP_GUIDE_TEXT } from "../mcp/setup-guide-text.js";
|
|
|
26
26
|
export const NATIVE_MUONROI_TOOL_NAMES = [
|
|
27
27
|
"ee_health",
|
|
28
28
|
"ee_feedback",
|
|
29
|
+
"ee_write",
|
|
29
30
|
"usage_forensics",
|
|
30
31
|
"lsp_query",
|
|
31
32
|
"setup_guide",
|
|
@@ -103,6 +104,61 @@ export function registerNativeMuonroiTools(tools, opts = {}) {
|
|
|
103
104
|
}
|
|
104
105
|
},
|
|
105
106
|
});
|
|
107
|
+
tools.ee_write = dynamicTool({
|
|
108
|
+
description: "Save a NEW lesson to the Experience Engine brain so you and future sessions recall it. Call this the MOMENT " +
|
|
109
|
+
"you hit a mistake / error / dead-end and find the working fix: record the pitfall AND the fix in one concise, " +
|
|
110
|
+
"generalizable lesson (1-3 sentences — what to do or avoid next time, NOT a play-by-play of this turn). The " +
|
|
111
|
+
"lesson is embedded immediately and becomes recallable via ee_query in THIS and future sessions. Use ee_query " +
|
|
112
|
+
"first when unsure if a lesson already exists. collection defaults to experience-behavioral (use " +
|
|
113
|
+
"experience-principles only for a broad, project-independent principle).",
|
|
114
|
+
inputSchema: jsonSchema({
|
|
115
|
+
type: "object",
|
|
116
|
+
properties: {
|
|
117
|
+
lesson: {
|
|
118
|
+
type: "string",
|
|
119
|
+
description: "The lesson: the mistake/pitfall AND the fix, concise and generalizable.",
|
|
120
|
+
},
|
|
121
|
+
title: { type: "string", description: "Optional short title (<=120 chars)." },
|
|
122
|
+
collection: {
|
|
123
|
+
type: "string",
|
|
124
|
+
enum: ["experience-behavioral", "experience-principles"],
|
|
125
|
+
description: "Target tier; default experience-behavioral.",
|
|
126
|
+
},
|
|
127
|
+
},
|
|
128
|
+
required: ["lesson"],
|
|
129
|
+
additionalProperties: false,
|
|
130
|
+
}),
|
|
131
|
+
execute: async (input) => {
|
|
132
|
+
const lesson = typeof input?.lesson === "string" ? input.lesson.trim() : "";
|
|
133
|
+
if (lesson.length < 12) {
|
|
134
|
+
return errLine("invalid_args", "ee_write requires a non-empty, substantive lesson (>=12 chars)");
|
|
135
|
+
}
|
|
136
|
+
// Cap oversized bodies (EE import-quality rule: bare points >1500 chars are
|
|
137
|
+
// status-dumps, not lessons).
|
|
138
|
+
const text = lesson.length > 1500 ? `${lesson.slice(0, 1497)}...` : lesson;
|
|
139
|
+
const collection = input?.collection === "experience-principles" ? "experience-principles" : "experience-behavioral";
|
|
140
|
+
const title = typeof input?.title === "string" && input.title.trim() ? input.title.trim().slice(0, 120) : undefined;
|
|
141
|
+
try {
|
|
142
|
+
const { writeExperienceEE } = await import("../ee/search.js");
|
|
143
|
+
const path = await import("node:path");
|
|
144
|
+
const cwd = opts.cwd ?? process.cwd();
|
|
145
|
+
// confidence 0.65 (above the import default 0.6): an agent-asserted lesson
|
|
146
|
+
// is deliberate but unproven — the ee_feedback loop then tunes it.
|
|
147
|
+
const result = await writeExperienceEE(text, {
|
|
148
|
+
collection,
|
|
149
|
+
title,
|
|
150
|
+
projectSlug: path.basename(cwd),
|
|
151
|
+
confidence: 0.65,
|
|
152
|
+
});
|
|
153
|
+
if (!result.ok)
|
|
154
|
+
return errLine("write_failed", result.error ?? "import-memory POST failed");
|
|
155
|
+
return json({ ok: true, id: result.id, collection, recallable: "now — same session via ee_query" });
|
|
156
|
+
}
|
|
157
|
+
catch (e) {
|
|
158
|
+
return errLine("write_failed", e instanceof Error ? e.message : String(e));
|
|
159
|
+
}
|
|
160
|
+
},
|
|
161
|
+
});
|
|
106
162
|
// ── Self-diagnostics: usage_forensics ─────────────────────────────────────
|
|
107
163
|
tools.usage_forensics = dynamicTool({
|
|
108
164
|
description: "Per-session token-cost forensics by session-id prefix: peak input, cache-hit ratio, per-event breakdown.",
|
|
@@ -26,7 +26,8 @@ describe("bash tool — empty/missing command corrective guard", () => {
|
|
|
26
26
|
const bash = new BashTool(os.tmpdir());
|
|
27
27
|
const tools = createBuiltinTools(bash, "agent");
|
|
28
28
|
const out = await runBash(tools, {});
|
|
29
|
-
|
|
29
|
+
// Guard uses "BLOCKED (empty-bash):" prefix — more semantic than bare "ERROR".
|
|
30
|
+
expect(out).toMatch(/BLOCKED/);
|
|
30
31
|
expect(out).toMatch(/non-empty/i);
|
|
31
32
|
expect(out).toMatch(/command/i);
|
|
32
33
|
});
|
|
@@ -34,14 +35,14 @@ describe("bash tool — empty/missing command corrective guard", () => {
|
|
|
34
35
|
const bash = new BashTool(os.tmpdir());
|
|
35
36
|
const tools = createBuiltinTools(bash, "agent");
|
|
36
37
|
const out = await runBash(tools, { command: "" });
|
|
37
|
-
expect(out).toMatch(/
|
|
38
|
+
expect(out).toMatch(/BLOCKED/);
|
|
38
39
|
expect(out).toMatch(/non-empty/i);
|
|
39
40
|
});
|
|
40
41
|
it("returns the corrective error for a whitespace-only command", async () => {
|
|
41
42
|
const bash = new BashTool(os.tmpdir());
|
|
42
43
|
const tools = createBuiltinTools(bash, "agent");
|
|
43
44
|
const out = await runBash(tools, { command: " " });
|
|
44
|
-
expect(out).toMatch(/
|
|
45
|
+
expect(out).toMatch(/BLOCKED/);
|
|
45
46
|
expect(out).toMatch(/non-empty/i);
|
|
46
47
|
});
|
|
47
48
|
it("does NOT block a real command", async () => {
|
|
@@ -12,11 +12,11 @@ import { BashTool } from "./bash.js";
|
|
|
12
12
|
import { clearBashOutputCache } from "./bash-output-cache.js";
|
|
13
13
|
import { __resetGitSafetyState, recordCommandOutcome } from "./git-safety.js";
|
|
14
14
|
import { createBuiltinTools } from "./registry.js";
|
|
15
|
-
async function runBash(tools, args) {
|
|
15
|
+
async function runBash(tools, args, extra) {
|
|
16
16
|
const t = tools.bash;
|
|
17
17
|
if (!t?.execute)
|
|
18
18
|
throw new Error("bash tool has no execute");
|
|
19
|
-
const out = await t.execute(args);
|
|
19
|
+
const out = await t.execute(args, extra);
|
|
20
20
|
return typeof out === "string" ? out : JSON.stringify(out);
|
|
21
21
|
}
|
|
22
22
|
describe("git-safety wiring in bash tool", () => {
|
|
@@ -28,6 +28,7 @@ describe("git-safety wiring in bash tool", () => {
|
|
|
28
28
|
});
|
|
29
29
|
afterEach(() => {
|
|
30
30
|
delete process.env.MUONROI_ALLOW_PUSH_ON_RED;
|
|
31
|
+
delete globalThis.__muonroiSafetyApproved;
|
|
31
32
|
});
|
|
32
33
|
it("BLOCKS git push (without executing) after a verification failed this session", async () => {
|
|
33
34
|
const bash = new BashTool(os.tmpdir());
|
|
@@ -35,7 +36,7 @@ describe("git-safety wiring in bash tool", () => {
|
|
|
35
36
|
// Simulate a failed test earlier in the session.
|
|
36
37
|
recordCommandOutcome("GS1", "npm test", false);
|
|
37
38
|
const out = await runBash(tools, { command: "git push origin main", timeout: 10_000 });
|
|
38
|
-
expect(out).toMatch(/^BLOCKED:/);
|
|
39
|
+
expect(out).toMatch(/^BLOCKED \(git-safety\):/);
|
|
39
40
|
expect(out).toMatch(/npm test/);
|
|
40
41
|
// The distinctive block message proves git push never ran (a real push in
|
|
41
42
|
// tmpdir would fail with a git error like "not a git repository", not this).
|
|
@@ -72,16 +73,26 @@ describe("git-safety wiring in bash tool", () => {
|
|
|
72
73
|
// Fresh anon registry (simulates the per-turn rebuild).
|
|
73
74
|
const toolsB = createBuiltinTools(bash, "agent"); // no sessionId
|
|
74
75
|
const pushOut = await runBash(toolsB, { command: "git push origin main", timeout: 10_000 });
|
|
75
|
-
expect(pushOut).toMatch(/^BLOCKED:/);
|
|
76
|
+
expect(pushOut).toMatch(/^BLOCKED \(git-safety\):/);
|
|
76
77
|
}, 30_000);
|
|
77
|
-
it("
|
|
78
|
+
it("uses command-matched safety approval when the retry has a new toolCallId", async () => {
|
|
79
|
+
const bash = new BashTool(os.tmpdir());
|
|
80
|
+
const tools = createBuiltinTools(bash, "agent", { sessionId: "GS-APPROVE" });
|
|
81
|
+
recordCommandOutcome("GS-APPROVE", "npm test", false);
|
|
82
|
+
globalThis.__muonroiSafetyApproved = new Map([
|
|
83
|
+
["blocked-tool-call-id", { kind: "once", command: "git push origin main" }],
|
|
84
|
+
]);
|
|
85
|
+
const out = await runBash(tools, { command: "git push origin main", timeout: 10_000 }, { toolCallId: "retry-tool-call-id" });
|
|
86
|
+
expect(out).not.toMatch(/^BLOCKED/);
|
|
87
|
+
}, 20_000);
|
|
88
|
+
it("blocks broad git add when sensitive paths exist", async () => {
|
|
78
89
|
const dir = mkdtempSync(join(os.tmpdir(), "gs-stage-"));
|
|
79
90
|
writeFileSync(join(dir, ".env"), "API_KEY=secret");
|
|
80
91
|
try {
|
|
81
92
|
const bash = new BashTool(dir);
|
|
82
93
|
const tools = createBuiltinTools(bash, "agent", { sessionId: "GS4" });
|
|
83
94
|
const out = await runBash(tools, { command: "git add -A", timeout: 10_000 });
|
|
84
|
-
expect(out).toMatch(
|
|
95
|
+
expect(out).toMatch(/^BLOCKED \(git-safety\):/);
|
|
85
96
|
expect(out).toMatch(/\.env/);
|
|
86
97
|
}
|
|
87
98
|
finally {
|
|
@@ -13,6 +13,7 @@ interface ToolRegistryOpts {
|
|
|
13
13
|
runDelegation?: (request: TaskRequest, abortSignal?: AbortSignal) => Promise<ToolResult>;
|
|
14
14
|
readDelegation?: (id: string) => Promise<ToolResult>;
|
|
15
15
|
listDelegations?: () => Promise<ToolResult>;
|
|
16
|
+
killDelegation?: (id: string) => Promise<ToolResult>;
|
|
16
17
|
modelId?: string;
|
|
17
18
|
/**
|
|
18
19
|
* When false, the 3 vision-proxy tools (analyze_image, ask_vision_proxy,
|
|
@@ -51,6 +52,10 @@ interface BashRepeatEntry {
|
|
|
51
52
|
}
|
|
52
53
|
declare global {
|
|
53
54
|
var __muonroiBashRepeatState: Map<string, BashRepeatEntry> | undefined;
|
|
55
|
+
var __muonroiSafetyApproved: Map<string, {
|
|
56
|
+
kind: "once" | "session";
|
|
57
|
+
command: string;
|
|
58
|
+
}> | undefined;
|
|
54
59
|
}
|
|
55
60
|
export declare const MAX_TOOL_OUTPUT_CHARS: number;
|
|
56
61
|
export declare function truncateOutput(text: string, maxChars?: number): string;
|
|
@@ -12,10 +12,16 @@ 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,
|
|
15
|
+
import { analyzeGitCommand, checkPushGate, checkSensitiveStaging, commitBlockedMessage, pushBlockedMessage, recordCommandOutcome, } from "./git-safety.js";
|
|
16
16
|
import { executeGrep } from "./grep.js";
|
|
17
17
|
import { registerNativeMuonroiTools } from "./native-tools.js";
|
|
18
18
|
import { VISION_TOOL_NAMES } from "./vision-gate.js";
|
|
19
|
+
function getSafetyApprovedMap() {
|
|
20
|
+
if (!globalThis.__muonroiSafetyApproved) {
|
|
21
|
+
globalThis.__muonroiSafetyApproved = new Map();
|
|
22
|
+
}
|
|
23
|
+
return globalThis.__muonroiSafetyApproved;
|
|
24
|
+
}
|
|
19
25
|
function getBashRepeatState() {
|
|
20
26
|
if (!globalThis.__muonroiBashRepeatState) {
|
|
21
27
|
globalThis.__muonroiBashRepeatState = new Map();
|
|
@@ -80,7 +86,7 @@ export function createBuiltinTools(bash, mode, opts) {
|
|
|
80
86
|
const fileTracker = new FileTracker();
|
|
81
87
|
// read_file
|
|
82
88
|
tools.read_file = dynamicTool({
|
|
83
|
-
description: "Read file contents
|
|
89
|
+
description: "Read file contents. For large files, you MUST use start_line and end_line to extract only the needed sections (e.g. specific functions). Reading full large files will quickly exhaust your context budget. Use grep or lsp first to find line numbers.",
|
|
84
90
|
inputSchema: jsonSchema({
|
|
85
91
|
type: "object",
|
|
86
92
|
properties: {
|
|
@@ -97,7 +103,7 @@ export function createBuiltinTools(bash, mode, opts) {
|
|
|
97
103
|
});
|
|
98
104
|
// grep
|
|
99
105
|
tools.grep = dynamicTool({
|
|
100
|
-
description: "Fast regex content search across the codebase using ripgrep. Returns matching lines with file paths and line numbers.",
|
|
106
|
+
description: "Fast regex content search across the codebase using ripgrep. Returns matching lines with file paths and line numbers. Use this to find precise line numbers before calling read_file with start_line/end_line.",
|
|
101
107
|
inputSchema: jsonSchema({
|
|
102
108
|
type: "object",
|
|
103
109
|
properties: {
|
|
@@ -135,6 +141,11 @@ export function createBuiltinTools(bash, mode, opts) {
|
|
|
135
141
|
// else a single process-stable key. Over-sharing here is the safe direction
|
|
136
142
|
// (it can only over-block a push, never wrongly allow one).
|
|
137
143
|
const gitSafetyKey = opts?.sessionId && opts.sessionId.length > 0 ? opts.sessionId : `__proc_default__:${process.pid}`;
|
|
144
|
+
// Per-session empty-bash streak counter: escalates from guidance (strike 1-2)
|
|
145
|
+
// to hard block (strike 3+) so a cheap model that repeatedly emits `bash: {}`
|
|
146
|
+
// cannot loop indefinitely (live: deepseek session bf58d0f46b51 — 8+ empty calls).
|
|
147
|
+
const _emptyBashStreak = new Map();
|
|
148
|
+
const _prefixBlock = (kind, msg) => `BLOCKED (${kind}): ${msg}`;
|
|
138
149
|
tools.bash = dynamicTool({
|
|
139
150
|
description: "Execute a shell command. Output is automatically cached — every call returns a " +
|
|
140
151
|
"run_id you can re-query via bash_output_get(run_id, mode=tail|head|grep|lines). " +
|
|
@@ -149,7 +160,10 @@ export function createBuiltinTools(bash, mode, opts) {
|
|
|
149
160
|
},
|
|
150
161
|
required: ["command"],
|
|
151
162
|
}),
|
|
152
|
-
|
|
163
|
+
// Phase 8 — safety-blocked commands queue, keyed by sessionId + toolCallId.
|
|
164
|
+
// When the orchestrator intercepts a blocked result, the approval handler
|
|
165
|
+
// stores the approved command here so bash.execute() can retry it.
|
|
166
|
+
execute: async (input, extra) => {
|
|
153
167
|
// Corrective guard for malformed calls: a cheap model sometimes emits a
|
|
154
168
|
// bash call with a missing / empty `command` (live: deepseek sent `{}`
|
|
155
169
|
// repeatedly until the loop-guard fired). Passing undefined to
|
|
@@ -158,9 +172,41 @@ export function createBuiltinTools(bash, mode, opts) {
|
|
|
158
172
|
// both look like progress and feed the loop. Return a crisp instruction
|
|
159
173
|
// so the next step supplies a real command instead of repeating.
|
|
160
174
|
if (typeof input.command !== "string" || input.command.trim() === "") {
|
|
161
|
-
|
|
175
|
+
// Track empty-bash streak per session; escalate from guidance to hard block.
|
|
176
|
+
const _ebKey = gitSafetyKey ?? "no-session";
|
|
177
|
+
let _eb = _emptyBashStreak.get(_ebKey) ?? 0;
|
|
178
|
+
_eb++;
|
|
179
|
+
_emptyBashStreak.set(_ebKey, _eb);
|
|
180
|
+
if (_eb >= 3) {
|
|
181
|
+
return ('BLOCKED (empty-bash): the `bash` tool has been called with an empty/missing "command" 3+ times in a row. ' +
|
|
182
|
+
"Bash is now DISABLED for the remainder of this session — use read_file, grep, or other tools instead. " +
|
|
183
|
+
"If you need to run a shell command, state the blocker explicitly and the CLI will enable it again on the next turn.");
|
|
184
|
+
}
|
|
185
|
+
if (_eb >= 2) {
|
|
186
|
+
return ('BLOCKED (empty-bash): ERROR (2nd consecutive empty bash call): the `bash` tool requires a non-empty "command" string ' +
|
|
187
|
+
"but this is the 2nd call in a row with empty arguments. One more empty call will BLOCK bash for the session. " +
|
|
188
|
+
'Provide a real command, e.g. {"command":"ls -la"}.');
|
|
189
|
+
}
|
|
190
|
+
return 'BLOCKED (empty-bash): 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"}.';
|
|
162
191
|
}
|
|
192
|
+
// Reset the empty-bash streak on any successful command.
|
|
193
|
+
_emptyBashStreak.delete(gitSafetyKey ?? "no-session");
|
|
163
194
|
const cmd = typeof input.command === "string" ? input.command : "";
|
|
195
|
+
// Safety override check: approval is granted after a blocked call, but
|
|
196
|
+
// the model's retry receives a new toolCallId. Match by id first, then
|
|
197
|
+
// by exact command so allow-once survives the retry boundary.
|
|
198
|
+
const _approvedMap = getSafetyApprovedMap();
|
|
199
|
+
const _approvalKey = extra?.toolCallId && _approvedMap.has(extra.toolCallId)
|
|
200
|
+
? extra.toolCallId
|
|
201
|
+
: [..._approvedMap.entries()].find(([, approval]) => approval.command === cmd)?.[0];
|
|
202
|
+
const _approvalEntry = _approvalKey ? _approvedMap.get(_approvalKey) : undefined;
|
|
203
|
+
if (_approvalEntry) {
|
|
204
|
+
if (_approvalEntry.kind === "once") {
|
|
205
|
+
_approvedMap.delete(_approvalKey);
|
|
206
|
+
}
|
|
207
|
+
const result = await bash.execute(input.command, input.timeout ?? 30000);
|
|
208
|
+
return formatResult(result);
|
|
209
|
+
}
|
|
164
210
|
// Git safety (pre-execution). Block `git push` while a verification
|
|
165
211
|
// command failed this session and was not re-run green; warn on broad
|
|
166
212
|
// `git add -A` / `git commit -a` when sensitive paths exist. Applied to
|
|
@@ -169,16 +215,46 @@ export function createBuiltinTools(bash, mode, opts) {
|
|
|
169
215
|
// (or the real sessionId) — unlike repeatKey, whose anon fallback changes
|
|
170
216
|
// on every registry rebuild and would silently drop the gate across turns.
|
|
171
217
|
const gitShape = analyzeGitCommand(cmd);
|
|
172
|
-
|
|
218
|
+
// Hard-block broad staging when sensitive files are present.
|
|
219
|
+
// This runs PRE-EXECUTION (before bash.execute) regardless of permission mode.
|
|
220
|
+
if (gitShape.isBroadStage) {
|
|
221
|
+
const stagingBlock = checkSensitiveStaging(bash.getCwd());
|
|
222
|
+
if (stagingBlock.blocked) {
|
|
223
|
+
return _prefixBlock("git-safety", stagingBlock.message);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
173
226
|
if (gitShape.isPush) {
|
|
174
227
|
const gate = checkPushGate(gitSafetyKey);
|
|
175
228
|
if (gate.blocked) {
|
|
176
|
-
return
|
|
229
|
+
return _prefixBlock("git-safety", pushBlockedMessage(gate.failed));
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
// G1 follow-up: a raw bash `git commit` must not bypass the LSP commit
|
|
233
|
+
// gate that the `git_commit` tool + auto-commit backstop enforce. Derive
|
|
234
|
+
// the to-be-committed paths from git state and block (pre-exec, never run
|
|
235
|
+
// the commit) if any staged source file has a severity-1 LSP error. The
|
|
236
|
+
// gate is fail-OPEN (LSP slow/down → allow) and self-disables under
|
|
237
|
+
// MUONROI_COMMIT_GATE=0 / the unit-test suite. Lazy-import keeps the
|
|
238
|
+
// LSP-heavy module off the hot bash path (mirrors git_commit below).
|
|
239
|
+
if (gitShape.isCommit) {
|
|
240
|
+
const { gateStagedPaths, isCommitGateEnabled, pathsForCommitGate } = await import("../orchestrator/auto-commit.js");
|
|
241
|
+
if (isCommitGateEnabled()) {
|
|
242
|
+
const commitCwd = bash.getCwd();
|
|
243
|
+
const paths = await pathsForCommitGate(commitCwd, {
|
|
244
|
+
broadAdd: gitShape.isBroadAdd,
|
|
245
|
+
commitAll: gitShape.isCommitAll,
|
|
246
|
+
});
|
|
247
|
+
if (paths.length > 0) {
|
|
248
|
+
const gate = await gateStagedPaths(commitCwd, paths);
|
|
249
|
+
if (!gate.ok) {
|
|
250
|
+
return _prefixBlock("git-safety", commitBlockedMessage(gate.summary));
|
|
251
|
+
}
|
|
252
|
+
}
|
|
177
253
|
}
|
|
178
254
|
}
|
|
179
255
|
if (input.background) {
|
|
180
256
|
const result = await bash.startBackground(input.command);
|
|
181
|
-
return
|
|
257
|
+
return formatResult(result);
|
|
182
258
|
}
|
|
183
259
|
// 3-3: compute canonical form BEFORE running so we can attach an
|
|
184
260
|
// inline reminder if it matches the previous bash call.
|
|
@@ -213,9 +289,9 @@ export function createBuiltinTools(bash, mode, opts) {
|
|
|
213
289
|
const hint = chars >= 4_000
|
|
214
290
|
? ` — ${chars} chars cached; use bash_output_get(run_id, mode=tail|head|grep|lines) to re-query`
|
|
215
291
|
: "";
|
|
216
|
-
return `${formatted}\n\n[bash_run_id: ${result.bashRunId}${hint}]${reminder}
|
|
292
|
+
return `${formatted}\n\n[bash_run_id: ${result.bashRunId}${hint}]${reminder}`;
|
|
217
293
|
}
|
|
218
|
-
return
|
|
294
|
+
return formatted;
|
|
219
295
|
},
|
|
220
296
|
});
|
|
221
297
|
// bash_output_get — re-query the cached full output of a previous bash run.
|
|
@@ -344,6 +420,58 @@ export function createBuiltinTools(bash, mode, opts) {
|
|
|
344
420
|
};
|
|
345
421
|
},
|
|
346
422
|
});
|
|
423
|
+
// git_commit — the agent commits its OWN work with a message IT writes.
|
|
424
|
+
tools.git_commit = dynamicTool({
|
|
425
|
+
description: "Commit the files you have created/edited so far, with a commit message YOU write. Use this to commit each " +
|
|
426
|
+
"cohesive, working chunk the moment it is finished — and after EACH step of a multi-step plan — instead of " +
|
|
427
|
+
"leaving everything for one commit at the end. message: a clear conventional subject describing WHAT changed " +
|
|
428
|
+
"(e.g. 'feat(auth): add token refresh'), optionally followed by a body — NOT a restatement of the request. " +
|
|
429
|
+
"Only files you wrote via write_file/edit_file are staged (secrets + CLI artifacts are excluded, and the " +
|
|
430
|
+
"'Coding by - Muonroi-CLI' attribution line is appended automatically). No-op if none of your written files " +
|
|
431
|
+
"have uncommitted changes.",
|
|
432
|
+
inputSchema: jsonSchema({
|
|
433
|
+
type: "object",
|
|
434
|
+
properties: {
|
|
435
|
+
message: {
|
|
436
|
+
type: "string",
|
|
437
|
+
description: "Conventional commit subject (optionally + body) describing what changed, authored by you.",
|
|
438
|
+
},
|
|
439
|
+
},
|
|
440
|
+
required: ["message"],
|
|
441
|
+
}),
|
|
442
|
+
execute: async (input) => {
|
|
443
|
+
const message = typeof input?.message === "string" ? input.message.trim() : "";
|
|
444
|
+
if (message.length < 3) {
|
|
445
|
+
return { success: false, output: "git_commit requires a non-empty commit message." };
|
|
446
|
+
}
|
|
447
|
+
const written = fileTracker.writtenPaths();
|
|
448
|
+
if (written.length === 0) {
|
|
449
|
+
return {
|
|
450
|
+
success: false,
|
|
451
|
+
output: "Nothing to commit — you have not created or edited any file via write_file/edit_file this session.",
|
|
452
|
+
};
|
|
453
|
+
}
|
|
454
|
+
try {
|
|
455
|
+
const { commitSpecificPaths } = await import("../orchestrator/auto-commit.js");
|
|
456
|
+
const result = await commitSpecificPaths(bash.getCwd(), written, message);
|
|
457
|
+
if (!result.committed) {
|
|
458
|
+
// G1: when the LSP quality gate blocked the commit, surface the
|
|
459
|
+
// per-file errors so the agent can fix them and call git_commit again.
|
|
460
|
+
// Tell the agent to FIX the errors — do NOT advertise the bypass
|
|
461
|
+
// env here (that is a USER escape hatch; surfacing it to the agent
|
|
462
|
+
// just invites it to circumvent the gate instead of fixing).
|
|
463
|
+
const detail = result.reason === "lsp-errors"
|
|
464
|
+
? `\nStaged files have errors — fix them and call git_commit again:\n${result.detail ?? ""}`
|
|
465
|
+
: "";
|
|
466
|
+
return { success: false, output: `No commit made (${result.reason}).${detail}` };
|
|
467
|
+
}
|
|
468
|
+
return { success: true, output: `Committed ${result.fileCount} file(s) → ${result.sha}` };
|
|
469
|
+
}
|
|
470
|
+
catch (e) {
|
|
471
|
+
return { success: false, output: `git_commit failed: ${e instanceof Error ? e.message : String(e)}` };
|
|
472
|
+
}
|
|
473
|
+
},
|
|
474
|
+
});
|
|
347
475
|
// task
|
|
348
476
|
if (opts?.runTask) {
|
|
349
477
|
const runTask = opts.runTask;
|
|
@@ -358,11 +486,20 @@ export function createBuiltinTools(bash, mode, opts) {
|
|
|
358
486
|
},
|
|
359
487
|
description: { type: "string", description: "Short description of the task" },
|
|
360
488
|
prompt: { type: "string", description: "Detailed instructions for the sub-agent" },
|
|
489
|
+
maxToolRounds: {
|
|
490
|
+
type: "number",
|
|
491
|
+
description: "Optional maximum tool execution rounds (default is 12 for foreground tasks)",
|
|
492
|
+
},
|
|
361
493
|
},
|
|
362
494
|
required: ["agent", "description", "prompt"],
|
|
363
495
|
}),
|
|
364
496
|
execute: async (input) => {
|
|
365
|
-
const result = await runTask({
|
|
497
|
+
const result = await runTask({
|
|
498
|
+
agent: input.agent,
|
|
499
|
+
description: input.description,
|
|
500
|
+
prompt: input.prompt,
|
|
501
|
+
maxToolRounds: input.maxToolRounds,
|
|
502
|
+
});
|
|
366
503
|
return formatResult(result);
|
|
367
504
|
},
|
|
368
505
|
});
|
|
@@ -378,6 +515,10 @@ export function createBuiltinTools(bash, mode, opts) {
|
|
|
378
515
|
agent: { type: "string", description: "Sub-agent type (usually 'explore')" },
|
|
379
516
|
description: { type: "string", description: "Short description of the research task" },
|
|
380
517
|
prompt: { type: "string", description: "Detailed research instructions" },
|
|
518
|
+
maxToolRounds: {
|
|
519
|
+
type: "number",
|
|
520
|
+
description: "Optional maximum tool execution rounds (default is 60 for explore delegations)",
|
|
521
|
+
},
|
|
381
522
|
},
|
|
382
523
|
required: ["agent", "description", "prompt"],
|
|
383
524
|
}),
|
|
@@ -386,6 +527,7 @@ export function createBuiltinTools(bash, mode, opts) {
|
|
|
386
527
|
agent: input.agent,
|
|
387
528
|
description: input.description,
|
|
388
529
|
prompt: input.prompt,
|
|
530
|
+
maxToolRounds: input.maxToolRounds,
|
|
389
531
|
});
|
|
390
532
|
return formatResult(result);
|
|
391
533
|
},
|
|
@@ -424,6 +566,24 @@ export function createBuiltinTools(bash, mode, opts) {
|
|
|
424
566
|
},
|
|
425
567
|
});
|
|
426
568
|
}
|
|
569
|
+
// delegation_kill
|
|
570
|
+
if (opts?.killDelegation) {
|
|
571
|
+
const killDelegation = opts.killDelegation;
|
|
572
|
+
tools.delegation_kill = dynamicTool({
|
|
573
|
+
description: "Terminate a running background delegation/subagent by ID.",
|
|
574
|
+
inputSchema: jsonSchema({
|
|
575
|
+
type: "object",
|
|
576
|
+
properties: {
|
|
577
|
+
id: { type: "string", description: "Delegation ID to terminate" },
|
|
578
|
+
},
|
|
579
|
+
required: ["id"],
|
|
580
|
+
}),
|
|
581
|
+
execute: async (input) => {
|
|
582
|
+
const result = await killDelegation(input.id);
|
|
583
|
+
return formatResult(result);
|
|
584
|
+
},
|
|
585
|
+
});
|
|
586
|
+
}
|
|
427
587
|
// ee_query — semantic recall over the Experience Engine brain. This is the
|
|
428
588
|
// in-CLI counterpart of the MCP `ee.query` tool (src/mcp/ee-tools.ts):
|
|
429
589
|
// without it, the Agent Operating Contract + checkpoint reminders instruct
|
|
@@ -503,6 +663,20 @@ export function createBuiltinTools(bash, mode, opts) {
|
|
|
503
663
|
if (resp === null) {
|
|
504
664
|
return "[ee_unavailable] Experience Engine returned no response (server down, timeout, circuit open, or unconfigured). Proceed without EE recall — re-read the source directly if you need the elided content.";
|
|
505
665
|
}
|
|
666
|
+
// Record recalled entries as pending feedback debt in the same in-process
|
|
667
|
+
// ledger the native ee_feedback builtin clears, so an in-CLI active recall
|
|
668
|
+
// accrues a verdict obligation exactly like the external MCP ee.query does
|
|
669
|
+
// (mcp/ee-tools.ts). Layer 3 surfaces this debt as a reminder on the next
|
|
670
|
+
// enriched turn; without it the in-CLI recall arm was write-only and
|
|
671
|
+
// ee_feedback.clear() was a no-op.
|
|
672
|
+
try {
|
|
673
|
+
const { sessionRecallLedger, isRecallLedgerEnabled } = await import("../ee/recall-ledger.js");
|
|
674
|
+
if (isRecallLedgerEnabled())
|
|
675
|
+
sessionRecallLedger.record(resp.entries, query);
|
|
676
|
+
}
|
|
677
|
+
catch (err) {
|
|
678
|
+
console.error(`[tools:ee_query] recall-ledger record failed: ${err?.message}`);
|
|
679
|
+
}
|
|
506
680
|
// Compact ranked `[id col]` index, not a JSON dump — the recallMode text
|
|
507
681
|
// is ~30k (wide net), so JSON.stringify wasted the budget on escaping.
|
|
508
682
|
return truncateOutput(formatRecallForAgent(resp, {
|
|
@@ -33,6 +33,7 @@ export interface TaskRequest {
|
|
|
33
33
|
agent: BuiltinSubagentId | string;
|
|
34
34
|
description: string;
|
|
35
35
|
prompt: string;
|
|
36
|
+
maxToolRounds?: number;
|
|
36
37
|
}
|
|
37
38
|
export interface TaskRun {
|
|
38
39
|
agent: string;
|
|
@@ -207,6 +208,7 @@ export interface ChatEntry {
|
|
|
207
208
|
toolResult?: ToolResult;
|
|
208
209
|
toolGroup?: ToolGroup;
|
|
209
210
|
structuredResponse?: StructuredResponse;
|
|
211
|
+
reasoning?: string;
|
|
210
212
|
}
|
|
211
213
|
export interface PaymentPrecheck {
|
|
212
214
|
security?: string;
|
|
@@ -221,13 +223,17 @@ export interface StructuredResponse {
|
|
|
221
223
|
taskType: string;
|
|
222
224
|
data: Record<string, unknown>;
|
|
223
225
|
}
|
|
224
|
-
export type CouncilQuestionPhase = "clarify" | "preflight" | "plan-confirm" | "post-debate" | "pil-interview" | "pil-acceptance" | "tool-loop-cap";
|
|
226
|
+
export type CouncilQuestionPhase = "clarify" | "preflight" | "plan-confirm" | "post-debate" | "pil-interview" | "pil-acceptance" | "tool-loop-cap" | "safety-override";
|
|
225
227
|
export type CouncilOptionKind = "choice" | "freetext" | "chat";
|
|
226
228
|
export interface CouncilQuestionOption {
|
|
227
229
|
label: string;
|
|
228
230
|
description?: string;
|
|
229
231
|
value: string;
|
|
230
232
|
kind: CouncilOptionKind;
|
|
233
|
+
/** Model-driven: if true, picking this cancels the whole interaction (discovery) */
|
|
234
|
+
isCancel?: boolean;
|
|
235
|
+
/** Model-driven: if true, picking this triggers a re-interview round (discovery) */
|
|
236
|
+
isAdjust?: boolean;
|
|
231
237
|
}
|
|
232
238
|
export interface CouncilQuestionData {
|
|
233
239
|
questionId: string;
|
package/dist/src/ui/app.d.ts
CHANGED
|
@@ -14,8 +14,9 @@ import "./slash/council.js";
|
|
|
14
14
|
import "./slash/ideal.js";
|
|
15
15
|
import "./slash/export.js";
|
|
16
16
|
import "./slash/status.js";
|
|
17
|
+
import "./slash/ponytail.js";
|
|
17
18
|
import type { AppProps } from "./types.js";
|
|
18
19
|
export type { AppStartupConfig } from "./types.js";
|
|
19
|
-
export declare function App({ agent, startupConfig, initialMessage, onExit }: AppProps): import("react").ReactNode;
|
|
20
|
+
export declare function App({ agent, startupConfig, initialMessage, onExit, onRelaunch }: AppProps): import("react").ReactNode;
|
|
20
21
|
export type { McpRunInfo } from "./components/message-view.js";
|
|
21
22
|
export { computeMcpRunInfo } from "./components/message-view.js";
|
package/dist/src/ui/app.js
CHANGED
|
Binary file
|
|
@@ -8,6 +8,7 @@ const PHASE_LABEL = {
|
|
|
8
8
|
"pil-interview": "Understanding",
|
|
9
9
|
"pil-acceptance": "Confirm Intent",
|
|
10
10
|
"tool-loop-cap": "Tool-loop guard",
|
|
11
|
+
"safety-override": "Safety override",
|
|
11
12
|
};
|
|
12
13
|
/**
|
|
13
14
|
* AskUserQuestion-style card. Controlled component — parent owns the state
|
|
@@ -2,7 +2,7 @@ import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "@opentui/reac
|
|
|
2
2
|
import { memo } from "react";
|
|
3
3
|
import { Markdown } from "../markdown.js";
|
|
4
4
|
import { PlanView } from "../plan.js";
|
|
5
|
-
import { stripStrayModelMacros, trunc } from "../utils/text.js";
|
|
5
|
+
import { stripInvisibleChars, stripStrayModelMacros, trunc } from "../utils/text.js";
|
|
6
6
|
import { describeMcpFsTool, toolArgs, toolLabel, tryParseArg } from "../utils/tools.js";
|
|
7
7
|
import { DiffView, ReadFilePreviewView } from "./diff-view.js";
|
|
8
8
|
import { LspDiagnosticsView, LspResultView } from "./lsp-views.js";
|
|
@@ -32,6 +32,8 @@ export function AssistantMessageContent({ content, t, expanded, isFinal, }) {
|
|
|
32
32
|
// Strip stray model self-annotation macros (e.g. grok's trailing
|
|
33
33
|
// `\confidence{85}`) that leak into the answer — not instructed in the prompt.
|
|
34
34
|
content = stripStrayModelMacros(content);
|
|
35
|
+
// Strip invisible/control Unicode characters that mess up terminal rendering.
|
|
36
|
+
content = stripInvisibleChars(content);
|
|
35
37
|
const lines = content.split("\n");
|
|
36
38
|
const isLong = lines.length > ASSISTANT_MSG_COLLAPSED_LINES;
|
|
37
39
|
// Phase 5 F7 — the FINAL assistant message in a turn IS the answer the
|
|
@@ -114,8 +116,34 @@ function MessageViewImpl({ entry, index, t, modeColor, expandedMessages, mcpRun,
|
|
|
114
116
|
switch (entry.type) {
|
|
115
117
|
case "user":
|
|
116
118
|
return (_jsx("box", { border: ["left"], customBorderChars: SPLIT, borderColor: entry.modeColor || modeColor, marginTop: index === 0 ? 0 : 1, marginBottom: 1, children: _jsxs("box", { paddingTop: 1, paddingBottom: 1, paddingLeft: 2, backgroundColor: t.backgroundPanel, flexShrink: 0, flexDirection: "column", children: [entry.sourceLabel ? _jsx("text", { fg: t.textMuted, children: entry.sourceLabel }) : null, _jsx(UserMessageContent, { content: entry.content, t: t, expanded: expandedMessages?.has(index) ?? false })] }) }));
|
|
117
|
-
case "assistant":
|
|
118
|
-
|
|
119
|
+
case "assistant": {
|
|
120
|
+
let fallbackSr = null;
|
|
121
|
+
const rawContent = entry.content.trim();
|
|
122
|
+
if (rawContent.startsWith("{") && rawContent.endsWith("}")) {
|
|
123
|
+
try {
|
|
124
|
+
const parsed = JSON.parse(rawContent);
|
|
125
|
+
if (Array.isArray(parsed.steps) && parsed.steps.length > 0 && parsed.steps[0].action) {
|
|
126
|
+
fallbackSr = { taskType: "plan", data: parsed };
|
|
127
|
+
}
|
|
128
|
+
else if (Array.isArray(parsed.changes) && parsed.changes.length > 0) {
|
|
129
|
+
fallbackSr = { taskType: "refactor", data: parsed };
|
|
130
|
+
}
|
|
131
|
+
else if (parsed.hypothesis && parsed.root_cause) {
|
|
132
|
+
fallbackSr = { taskType: "debug", data: parsed };
|
|
133
|
+
}
|
|
134
|
+
else if (Array.isArray(parsed.findings)) {
|
|
135
|
+
fallbackSr = { taskType: "analyze", data: parsed };
|
|
136
|
+
}
|
|
137
|
+
else if (Array.isArray(parsed.files) && parsed.files.length > 0 && parsed.files[0].path) {
|
|
138
|
+
fallbackSr = { taskType: "generate", data: parsed };
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
catch {
|
|
142
|
+
// ignore
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return (_jsxs("box", { paddingLeft: 3, marginTop: 1, flexShrink: 0, flexDirection: "column", children: [entry.sourceLabel ? _jsx("text", { fg: t.textMuted, children: entry.sourceLabel }) : null, entry.reasoning ? (_jsxs("box", { paddingLeft: 0, marginBottom: 1, flexShrink: 0, flexDirection: "column", children: [_jsx("text", { fg: t.textMuted, children: "[Thought] Thought" }), _jsx("box", { border: ["left"], borderColor: t.textMuted, paddingLeft: 2, marginTop: 1, flexDirection: "column", children: _jsx(Markdown, { content: entry.reasoning, t: t }) })] })) : null, fallbackSr ? (_jsx(StructuredResponseView, { t: t, sr: fallbackSr, modeColor: entry.modeColor || modeColor })) : (_jsx(AssistantMessageContent, { content: entry.content, t: t, expanded: expandedMessages?.has(index) ?? false, isFinal: isFinalAssistant === true }))] }));
|
|
146
|
+
}
|
|
119
147
|
case "tool_call":
|
|
120
148
|
return (_jsx("box", { paddingLeft: 3, marginTop: 1, children: _jsxs("text", { children: [_jsx("span", { style: { fg: entry.modeColor || modeColor }, children: "▣ " }), _jsx("span", { style: { fg: t.textMuted }, children: entry.content.replace("▣ ", "") })] }) }));
|
|
121
149
|
case "tool_group":
|
|
@@ -234,6 +262,12 @@ export const MessageView = memo(MessageViewImpl, (prev, next) => {
|
|
|
234
262
|
return false;
|
|
235
263
|
if ((prevMcp?.count ?? 0) !== (nextMcp?.count ?? 0))
|
|
236
264
|
return false;
|
|
265
|
+
// Phase 5 F7 — isFinalAssistant changes when a new assistant message is
|
|
266
|
+
// appended (the previously-final message is no longer final). Without this
|
|
267
|
+
// check, the memo skips the re-render and the old final assistant stays
|
|
268
|
+
// incorrectly expanded for the rest of the session.
|
|
269
|
+
if ((prev.isFinalAssistant ?? false) !== (next.isFinalAssistant ?? false))
|
|
270
|
+
return false;
|
|
237
271
|
return true;
|
|
238
272
|
});
|
|
239
273
|
//# sourceMappingURL=message-view.js.map
|
|
@@ -23,6 +23,13 @@ export function StructuredResponseView({ t, sr, modeColor }) {
|
|
|
23
23
|
}
|
|
24
24
|
case "analyze": {
|
|
25
25
|
const r = d;
|
|
26
|
+
// Graceful fallback: model may have called respond_analyze but sent a
|
|
27
|
+
// free-form { response: "..." } payload (schema mismatch due to tool being
|
|
28
|
+
// unavailable in the current turn). Render as plain markdown rather than
|
|
29
|
+
// an empty findings list (session 48d22fe436f6 swallowed-answer bug).
|
|
30
|
+
if ((!r.findings || r.findings.length === 0) && typeof r.response === "string" && r.response.trim()) {
|
|
31
|
+
return (_jsx("box", { flexDirection: "column", paddingLeft: 2, marginTop: 1, children: renderMarkdown(r.response, t) }));
|
|
32
|
+
}
|
|
26
33
|
const sevColor = (s) => (s === "high" ? t.diffRemovedFg : s === "medium" ? t.planStepNum : t.textMuted);
|
|
27
34
|
return (_jsx("box", { flexDirection: "column", paddingLeft: 2, marginTop: 1, children: (r.findings ?? []).map((f, i) => (_jsxs("box", { flexDirection: "column", children: [_jsxs("text", { children: [_jsx("span", { style: { fg: sevColor(f.severity) }, children: `[${f.severity.toUpperCase()}] ` }), _jsx("span", { children: f.text })] }), _jsx("text", { fg: t.textMuted, children: ` evidence: ${f.evidence}` })] }, `af${i}`))) }));
|
|
28
35
|
}
|