muonroi-cli 1.7.2 → 1.8.1
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/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 +143 -6
- 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 +7 -4
- 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 +36 -1
- 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/flow/compaction/index.d.ts +1 -0
- package/dist/src/flow/compaction/index.js +4 -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 +59 -62
- 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 +13 -5
- package/dist/src/lsp/manager.test.js +41 -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/auto-setup.js +0 -8
- 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/self-verify-jobs.d.ts +1 -0
- package/dist/src/mcp/self-verify-jobs.js +1 -3
- package/dist/src/mcp/self-verify-runner.js +1 -1
- package/dist/src/models/catalog-gemini.test.js +12 -10
- package/dist/src/models/catalog.json +106 -38
- 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 +3 -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 +2 -0
- package/dist/src/orchestrator/__tests__/prompts.test.d.ts +1 -0
- package/dist/src/orchestrator/__tests__/prompts.test.js +88 -0
- package/dist/src/orchestrator/__tests__/stream-runner.test.js +29 -0
- package/dist/src/orchestrator/__tests__/sub-session-delegation.test.d.ts +1 -0
- package/dist/src/orchestrator/__tests__/sub-session-delegation.test.js +507 -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 +366 -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 +23 -5
- package/dist/src/orchestrator/message-processor.js +226 -2573
- package/dist/src/orchestrator/orchestrator.d.ts +11 -2
- package/dist/src/orchestrator/orchestrator.js +530 -43
- package/dist/src/orchestrator/pending-calls.js +2 -1
- package/dist/src/orchestrator/preprocessor.d.ts +13 -0
- package/dist/src/orchestrator/preprocessor.js +146 -0
- package/dist/src/orchestrator/prompts.d.ts +16 -0
- package/dist/src/orchestrator/prompts.js +130 -67
- 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-cap.d.ts +2 -0
- package/dist/src/orchestrator/sub-agent-cap.js +12 -4
- package/dist/src/orchestrator/sub-agent-model-tier.js +1 -1
- package/dist/src/orchestrator/subagent-compactor.d.ts +21 -0
- package/dist/src/orchestrator/subagent-compactor.js +148 -24
- package/dist/src/orchestrator/subagent-compactor.spec.js +249 -1
- 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 +153 -0
- package/dist/src/orchestrator/tool-engine.js +3138 -0
- package/dist/src/orchestrator/turn-runner-deps.d.ts +3 -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__/llm-classify.test.js +79 -1
- 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 +62 -7
- 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.d.ts +11 -0
- package/dist/src/pil/llm-classify.js +196 -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/gather.js +19 -5
- package/dist/src/product-loop/loop-driver.js +2 -2
- 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/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/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/self-qa/agentic-loop.js +30 -21
- 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__/hierarchical-session.test.d.ts +1 -0
- package/dist/src/storage/__tests__/hierarchical-session.test.js +161 -0
- package/dist/src/storage/__tests__/sweep-stale-pending.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/migrations.js +13 -1
- package/dist/src/storage/transcript-response-entry.test.js +18 -5
- package/dist/src/storage/transcript-view.js +7 -1
- package/dist/src/storage/transcript.d.ts +3 -0
- package/dist/src/storage/transcript.js +254 -73
- package/dist/src/storage/ui-interaction-log.d.ts +17 -0
- package/dist/src/storage/usage.js +22 -5
- 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 +66 -1
- 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 +6 -0
- package/dist/src/tools/registry.js +215 -13
- package/dist/src/types/index.d.ts +8 -2
- package/dist/src/ui/app.d.ts +1 -0
- 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/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/expand.js +14 -1
- 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/use-app-logic.d.ts +26 -0
- package/dist/src/ui/use-app-logic.js +0 -0
- package/dist/src/ui/utils/relaunch.js +1 -1
- 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/__tests__/llm-deadline.test.js +16 -4
- package/dist/src/utils/__tests__/logger.test.d.ts +1 -0
- package/dist/src/utils/__tests__/logger.test.js +115 -0
- package/dist/src/utils/__tests__/settings-agent-first.test.d.ts +1 -0
- package/dist/src/utils/__tests__/settings-agent-first.test.js +48 -0
- package/dist/src/utils/install-manager.d.ts +1 -0
- package/dist/src/utils/install-manager.js +76 -7
- package/dist/src/utils/install-manager.test.js +43 -1
- package/dist/src/utils/llm-deadline.d.ts +9 -1
- package/dist/src/utils/llm-deadline.js +31 -2
- package/dist/src/utils/logger.d.ts +28 -0
- package/dist/src/utils/logger.js +171 -0
- 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 +36 -32
- package/dist/src/utils/settings.js +151 -123
- package/dist/src/utils/update-checker.test.js +5 -1
- package/package.json +1 -1
|
@@ -0,0 +1,366 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/orchestrator/auto-commit.ts
|
|
3
|
+
*
|
|
4
|
+
* Deterministic "task done -> commit" enforcement (user directive 2026-06-21:
|
|
5
|
+
* the soft prompt rule "commit incrementally" was being ignored, so commits did
|
|
6
|
+
* not actually happen in real use). At the end of a successful agentic turn this
|
|
7
|
+
* auto-commits ONLY the files the agent changed during that turn, with the
|
|
8
|
+
* required attribution line.
|
|
9
|
+
*
|
|
10
|
+
* Scoping is by snapshot-diff, NOT a file-mutation tracker: we record the set of
|
|
11
|
+
* dirty/untracked paths BEFORE the turn and commit only paths that became dirty
|
|
12
|
+
* DURING it (dirtyAfter - dirtyBefore). This deliberately skips any file the user
|
|
13
|
+
* already had uncommitted before the turn — auto-commit must never fold a user's
|
|
14
|
+
* unrelated work-in-progress into an agent commit.
|
|
15
|
+
*
|
|
16
|
+
* Safety gates: git repo only · `MUONROI_AUTO_COMMIT=0` opt-out · never under
|
|
17
|
+
* unit tests (VITEST) · a sensitive-path denylist (.env / keys / secrets /
|
|
18
|
+
* .muonroi-cli) · fail-soft (a git error is logged, never breaks the turn).
|
|
19
|
+
*
|
|
20
|
+
* git is invoked via execFile with an argument array (no shell) so file paths
|
|
21
|
+
* cannot inject shell commands.
|
|
22
|
+
*/
|
|
23
|
+
import { execFile } from "node:child_process";
|
|
24
|
+
import { logger } from "../utils/logger.js";
|
|
25
|
+
import { resolve } from "node:path";
|
|
26
|
+
import { promisify } from "node:util";
|
|
27
|
+
const pexecFile = promisify(execFile);
|
|
28
|
+
/** Attribution line every auto-commit message ends with (mirrors the prompt rule). */
|
|
29
|
+
export const AUTO_COMMIT_ATTRIBUTION = "Coding by - Muonroi-CLI";
|
|
30
|
+
/** Paths we must never auto-stage, regardless of who changed them (secrets). */
|
|
31
|
+
const SENSITIVE_RE = /(^|\/)(\.env(\.[^/]*)?$|.*\.pem$|.*\.key$|.*\.p12$|.*\.pfx$|.*secret.*|.*credential.*|id_rsa|id_ed25519)/i;
|
|
32
|
+
/**
|
|
33
|
+
* CLI-generated artifacts + build/dependency junk that must not be swept into an
|
|
34
|
+
* agent commit. The snapshot-diff catches everything that became dirty during the
|
|
35
|
+
* turn — including the CLI's OWN session/flow state (all `.muonroi-*`) which it
|
|
36
|
+
* writes into cwd. Real user repos usually gitignore node_modules/dist (so git
|
|
37
|
+
* status hides them), but exclude them too for repos that don't.
|
|
38
|
+
*/
|
|
39
|
+
const ARTIFACT_RE = /(^|\/)(\.muonroi-|node_modules\/|dist\/|build\/|coverage\/|\.next\/|\.turbo\/|\.git\/)|(^|\/)\.DS_Store$|\.log$/i;
|
|
40
|
+
/**
|
|
41
|
+
* G1 commit quality gate. Default ON; disable with `MUONROI_COMMIT_GATE=0`
|
|
42
|
+
* (mirrors the `MUONROI_AUTO_COMMIT=0` convention). Off automatically under the
|
|
43
|
+
* unit-test suite so specs that commit fixtures aren't gated.
|
|
44
|
+
*/
|
|
45
|
+
export function isCommitGateEnabled() {
|
|
46
|
+
if (process.env.MUONROI_COMMIT_GATE === "0")
|
|
47
|
+
return false;
|
|
48
|
+
if (process.env.VITEST || process.env.NODE_ENV === "test")
|
|
49
|
+
return false;
|
|
50
|
+
return true;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* G1: per-file LSP-errors gate. Runs the SAME LSP diagnostics produced at
|
|
54
|
+
* write-time on each staged path; blocks the commit if any staged file has an
|
|
55
|
+
* ERROR (severity 1). Scoped to each file's OWN diagnostics so unrelated repo
|
|
56
|
+
* breakage never blocks. Files with no registered LSP server return no
|
|
57
|
+
* diagnostics and pass (so docs/config/non-source commits are unaffected).
|
|
58
|
+
*
|
|
59
|
+
* Fails OPEN on timeout or any error — the gate must never hang a turn or block
|
|
60
|
+
* a commit on its own failure. Only a clean, in-budget run with a real
|
|
61
|
+
* severity-1 diagnostic blocks.
|
|
62
|
+
*/
|
|
63
|
+
/**
|
|
64
|
+
* G1 (pure): the gate's BLOCK decision for one staged file — its own
|
|
65
|
+
* severity-1 (error) diagnostics. Scoped by absolute path so a diagnostic LSP
|
|
66
|
+
* reports against a DIFFERENT file (cross-file type breakage) never blocks this
|
|
67
|
+
* commit, and warnings/infos (severity >= 2) are ignored. Exported for testing.
|
|
68
|
+
*/
|
|
69
|
+
export function blockingErrorsForFile(diagFiles, absPath) {
|
|
70
|
+
const out = [];
|
|
71
|
+
for (const f of diagFiles) {
|
|
72
|
+
if (resolve(f.filePath) !== absPath)
|
|
73
|
+
continue;
|
|
74
|
+
for (const d of f.diagnostics) {
|
|
75
|
+
if ((d.severity ?? 1) === 1)
|
|
76
|
+
out.push(d);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return out;
|
|
80
|
+
}
|
|
81
|
+
export async function gateStagedPaths(cwd, paths, budgetMs = 9_000) {
|
|
82
|
+
if (!isCommitGateEnabled())
|
|
83
|
+
return { ok: true };
|
|
84
|
+
try {
|
|
85
|
+
const { readFile } = await import("node:fs/promises");
|
|
86
|
+
const { syncFileWithLsp, summarizeDiagnostics } = await import("../lsp/runtime.js");
|
|
87
|
+
// Per-file diagnostics wait. The LSP default (1.5s) is fine for a WARM
|
|
88
|
+
// server (diagnostics cached) but a COLD tsserver loading the project on
|
|
89
|
+
// the first file pushes publishDiagnostics later — so a commit issued
|
|
90
|
+
// seconds after the first edit would slip past a 1.5s wait. Wait longer per
|
|
91
|
+
// file, bounded by the overall budgetMs (fail-open) so the gate can't hang.
|
|
92
|
+
const perFileWaitMs = 4_000;
|
|
93
|
+
const errorFiles = [];
|
|
94
|
+
const work = (async () => {
|
|
95
|
+
for (const p of paths) {
|
|
96
|
+
const abs = resolve(cwd, p);
|
|
97
|
+
let content;
|
|
98
|
+
try {
|
|
99
|
+
content = await readFile(abs, "utf8");
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
continue; // deleted / binary / unreadable → nothing to gate
|
|
103
|
+
}
|
|
104
|
+
const diags = await syncFileWithLsp(cwd, abs, content, false, true, perFileWaitMs).catch(() => []);
|
|
105
|
+
const errs = blockingErrorsForFile(diags, abs);
|
|
106
|
+
if (errs.length > 0) {
|
|
107
|
+
const serverId = diags.find((f) => resolve(f.filePath) === abs)?.serverId ?? "lsp";
|
|
108
|
+
errorFiles.push({ filePath: abs, serverId, diagnostics: errs });
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
})();
|
|
112
|
+
const TIMED_OUT = Symbol("timeout");
|
|
113
|
+
const outcome = await Promise.race([
|
|
114
|
+
work.then(() => "done"),
|
|
115
|
+
new Promise((r) => setTimeout(() => r(TIMED_OUT), budgetMs)),
|
|
116
|
+
]);
|
|
117
|
+
if (outcome === TIMED_OUT) {
|
|
118
|
+
logger.error("orchestrator", `[commit-gate] LSP gate exceeded ${budgetMs}ms — allowing commit (fail-open)`);
|
|
119
|
+
return { ok: true };
|
|
120
|
+
}
|
|
121
|
+
if (errorFiles.length === 0)
|
|
122
|
+
return { ok: true };
|
|
123
|
+
const summary = summarizeDiagnostics(errorFiles) ?? `${errorFiles.length} file(s) have LSP errors`;
|
|
124
|
+
return { ok: false, summary };
|
|
125
|
+
}
|
|
126
|
+
catch (err) {
|
|
127
|
+
logger.error("orchestrator", "gate failed open", {
|
|
128
|
+
error: err,
|
|
129
|
+
stack: err?.stack?.split("\n").slice(0, 3),
|
|
130
|
+
});
|
|
131
|
+
return { ok: true };
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
/**
|
|
135
|
+
* G1 follow-up: the set of paths a bash-tool `git commit` would include, so the
|
|
136
|
+
* LSP commit gate can run on them. A raw `git commit` (unlike the `git_commit`
|
|
137
|
+
* tool) doesn't tell us its paths, so derive them from git state:
|
|
138
|
+
* - always: the already-staged set (`git diff --cached --name-only`)
|
|
139
|
+
* - `git commit -a`: + tracked modifications it auto-stages at commit time
|
|
140
|
+
* (`git diff --name-only`)
|
|
141
|
+
* - `git add -A`/`.`/`--all` chained in the SAME command: + the whole
|
|
142
|
+
* working-tree change set (`git status --porcelain`), since the add hasn't
|
|
143
|
+
* run yet at pre-exec time so it isn't reflected in `--cached`.
|
|
144
|
+
* Deleted/binary/unreadable paths are skipped later by gateStagedPaths when it
|
|
145
|
+
* reads them. KNOWN GAP: `git add <specific-path> && git commit` in one command
|
|
146
|
+
* where <specific-path> was not pre-staged is NOT covered (we don't parse
|
|
147
|
+
* pathspecs); the `git_commit` tool + auto-commit backstop remain the primary
|
|
148
|
+
* gates. Returns repo-relative paths (gateStagedPaths resolves them against cwd).
|
|
149
|
+
*/
|
|
150
|
+
export async function pathsForCommitGate(cwd, opts) {
|
|
151
|
+
const set = new Set();
|
|
152
|
+
const addLines = (out) => {
|
|
153
|
+
for (const line of out.split("\n")) {
|
|
154
|
+
const p = line.trim();
|
|
155
|
+
if (p)
|
|
156
|
+
set.add(p);
|
|
157
|
+
}
|
|
158
|
+
};
|
|
159
|
+
const staged = await git(cwd, ["diff", "--cached", "--name-only"]);
|
|
160
|
+
if (staged.ok)
|
|
161
|
+
addLines(staged.stdout);
|
|
162
|
+
if (opts.broadAdd) {
|
|
163
|
+
// `git add -A/.` stages every working-tree change; `--porcelain` enumerates
|
|
164
|
+
// exactly that superset (and omits gitignored dirs like node_modules/dist).
|
|
165
|
+
const status = await git(cwd, ["status", "--porcelain"]);
|
|
166
|
+
if (status.ok)
|
|
167
|
+
for (const p of parsePorcelainPaths(status.stdout))
|
|
168
|
+
set.add(p);
|
|
169
|
+
}
|
|
170
|
+
else if (opts.commitAll) {
|
|
171
|
+
// `git commit -a` auto-stages tracked modifications (not untracked files).
|
|
172
|
+
const mod = await git(cwd, ["diff", "--name-only"]);
|
|
173
|
+
if (mod.ok)
|
|
174
|
+
addLines(mod.stdout);
|
|
175
|
+
}
|
|
176
|
+
return [...set];
|
|
177
|
+
}
|
|
178
|
+
export function isAutoCommitEnabled() {
|
|
179
|
+
if (process.env.MUONROI_AUTO_COMMIT === "0")
|
|
180
|
+
return false;
|
|
181
|
+
// Never auto-commit while the unit-test suite runs — it executes in the repo
|
|
182
|
+
// working tree and would commit junk.
|
|
183
|
+
if (process.env.VITEST || process.env.NODE_ENV === "test")
|
|
184
|
+
return false;
|
|
185
|
+
return true;
|
|
186
|
+
}
|
|
187
|
+
async function git(cwd, args) {
|
|
188
|
+
try {
|
|
189
|
+
const { stdout } = await pexecFile("git", args, {
|
|
190
|
+
cwd,
|
|
191
|
+
timeout: 15_000,
|
|
192
|
+
maxBuffer: 16 * 1024 * 1024,
|
|
193
|
+
windowsHide: true,
|
|
194
|
+
});
|
|
195
|
+
return { stdout, ok: true };
|
|
196
|
+
}
|
|
197
|
+
catch {
|
|
198
|
+
// Expected for non-repos / hook rejections / nothing-to-commit — caller maps
|
|
199
|
+
// !ok to a skip reason; no throw escapes (fail-soft by contract).
|
|
200
|
+
return { stdout: "", ok: false };
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
/**
|
|
204
|
+
* Parse `git status --porcelain` output into the set of changed paths. Handles
|
|
205
|
+
* renames (`orig -> new`, keeps the new path) and quoted paths with spaces.
|
|
206
|
+
*/
|
|
207
|
+
export function parsePorcelainPaths(out) {
|
|
208
|
+
const set = new Set();
|
|
209
|
+
for (const line of out.split("\n")) {
|
|
210
|
+
if (line.length < 4)
|
|
211
|
+
continue;
|
|
212
|
+
let path = line.slice(3); // strip the 2-char XY status + the separating space
|
|
213
|
+
if (path.includes(" -> "))
|
|
214
|
+
path = path.slice(path.indexOf(" -> ") + 4);
|
|
215
|
+
path = path.trim().replace(/^"(.*)"$/, "$1");
|
|
216
|
+
if (path)
|
|
217
|
+
set.add(path);
|
|
218
|
+
}
|
|
219
|
+
return set;
|
|
220
|
+
}
|
|
221
|
+
export function isSensitivePath(path) {
|
|
222
|
+
return SENSITIVE_RE.test(path);
|
|
223
|
+
}
|
|
224
|
+
/** CLI artifact / build junk that must never be folded into an agent commit. */
|
|
225
|
+
export function isCliArtifactPath(path) {
|
|
226
|
+
return ARTIFACT_RE.test(path);
|
|
227
|
+
}
|
|
228
|
+
/** Any path the auto-commit must skip: a secret or a CLI/build artifact. */
|
|
229
|
+
export function isExcludedPath(path) {
|
|
230
|
+
return isSensitivePath(path) || isCliArtifactPath(path);
|
|
231
|
+
}
|
|
232
|
+
/**
|
|
233
|
+
* Backstop subject naming the changed FILES — used only by the deterministic
|
|
234
|
+
* end-of-turn safety net (when the agent did not commit its own work via the
|
|
235
|
+
* git_commit tool). Deliberately NOT derived from the raw user prompt (a
|
|
236
|
+
* truncated prompt is meaningless, especially for a multi-step plan); a file list
|
|
237
|
+
* at least says what changed. The meaningful, model-authored message is the
|
|
238
|
+
* git_commit tool's job (the agent writes it per chunk/plan-step).
|
|
239
|
+
*/
|
|
240
|
+
export function buildFileListSubject(paths) {
|
|
241
|
+
const names = paths.map((p) => p.split("/").pop() || p);
|
|
242
|
+
const shown = names.slice(0, 3).join(", ");
|
|
243
|
+
const more = names.length > 3 ? ` +${names.length - 3} more` : "";
|
|
244
|
+
return `chore: update ${paths.length} file(s) — ${shown}${more}`.slice(0, 72);
|
|
245
|
+
}
|
|
246
|
+
/**
|
|
247
|
+
* Split an AGENT-authored message into a bounded subject (first line, <=72 so it
|
|
248
|
+
* passes conventional-commit hooks) and a bounded body. Strips CRs and drops any
|
|
249
|
+
* attribution line the agent already added (we append exactly one ourselves).
|
|
250
|
+
* Callers pass subject/body/attribution as SEPARATE `-m` flags so git inserts the
|
|
251
|
+
* blank-line separators itself — embedding "\n\n" in a single `-m` arg gets
|
|
252
|
+
* mangled by Windows execFile (the attribution then glues onto the subject).
|
|
253
|
+
*/
|
|
254
|
+
export function splitCommitMessage(message) {
|
|
255
|
+
const lines = message
|
|
256
|
+
.replace(/\r/g, "")
|
|
257
|
+
.split("\n")
|
|
258
|
+
.filter((l) => l.trim() !== AUTO_COMMIT_ATTRIBUTION);
|
|
259
|
+
const subject = (lines[0] ?? "").trim().slice(0, 72);
|
|
260
|
+
const body = lines.slice(1).join("\n").trim().slice(0, 2000);
|
|
261
|
+
return { subject, body };
|
|
262
|
+
}
|
|
263
|
+
/** Snapshot the dirty/untracked path set before a turn (empty when not a repo). */
|
|
264
|
+
export async function snapshotDirtyPaths(cwd) {
|
|
265
|
+
const r = await git(cwd, ["status", "--porcelain", "--untracked-files=all"]);
|
|
266
|
+
if (!r.ok)
|
|
267
|
+
return new Set();
|
|
268
|
+
return parsePorcelainPaths(r.stdout);
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Commit the files the agent changed this turn. `dirtyBefore` is the snapshot
|
|
272
|
+
* from before the turn. Returns a result describing what happened; never throws.
|
|
273
|
+
*/
|
|
274
|
+
export async function maybeAutoCommitTurn(opts) {
|
|
275
|
+
if (!isAutoCommitEnabled())
|
|
276
|
+
return { committed: false, reason: "disabled" };
|
|
277
|
+
const { cwd, dirtyBefore, userMessage } = opts;
|
|
278
|
+
const inRepo = await git(cwd, ["rev-parse", "--is-inside-work-tree"]);
|
|
279
|
+
if (!inRepo.ok || inRepo.stdout.trim() !== "true")
|
|
280
|
+
return { committed: false, reason: "not-a-repo" };
|
|
281
|
+
const dirtyAfter = await snapshotDirtyPaths(cwd);
|
|
282
|
+
const newPaths = [...dirtyAfter].filter((p) => !dirtyBefore.has(p) && !isExcludedPath(p));
|
|
283
|
+
if (newPaths.length === 0)
|
|
284
|
+
return { committed: false, reason: "no-agent-changes" };
|
|
285
|
+
const add = await git(cwd, ["add", "--", ...newPaths]);
|
|
286
|
+
if (!add.ok) {
|
|
287
|
+
logger.error("orchestrator", `[auto-commit] git add failed for ${newPaths.length} path(s) in ${cwd}`);
|
|
288
|
+
return { committed: false, reason: "add-failed" };
|
|
289
|
+
}
|
|
290
|
+
// G1 quality gate: do NOT auto-commit code that fails LSP error checks. The
|
|
291
|
+
// backstop simply skips (no spurious chore commit of broken code); the agent
|
|
292
|
+
// will see/fix it next turn. Staged paths stay staged (idempotent).
|
|
293
|
+
const gate = await gateStagedPaths(cwd, newPaths);
|
|
294
|
+
if (!gate.ok) {
|
|
295
|
+
logger.error("orchestrator", `[auto-commit] skipped — staged files have LSP errors:\n${gate.summary}`);
|
|
296
|
+
return { committed: false, reason: "lsp-errors", detail: gate.summary };
|
|
297
|
+
}
|
|
298
|
+
// Separate -m flags → git inserts the blank line between subject and
|
|
299
|
+
// attribution (Windows-safe; embedded "\n\n" in one -m arg gets mangled).
|
|
300
|
+
// Scope the commit to exactly the agent's paths (pathspec).
|
|
301
|
+
const commit = await git(cwd, [
|
|
302
|
+
"commit",
|
|
303
|
+
"-m",
|
|
304
|
+
buildFileListSubject(newPaths),
|
|
305
|
+
"-m",
|
|
306
|
+
AUTO_COMMIT_ATTRIBUTION,
|
|
307
|
+
"--",
|
|
308
|
+
...newPaths,
|
|
309
|
+
]);
|
|
310
|
+
if (!commit.ok) {
|
|
311
|
+
logger.error("orchestrator", `[auto-commit] git commit failed in ${cwd} (a pre-commit/commit-msg hook may have rejected it)`);
|
|
312
|
+
return { committed: false, reason: "commit-failed" };
|
|
313
|
+
}
|
|
314
|
+
const head = await git(cwd, ["rev-parse", "--short", "HEAD"]);
|
|
315
|
+
return { committed: true, sha: head.ok ? head.stdout.trim() : undefined, fileCount: newPaths.length };
|
|
316
|
+
}
|
|
317
|
+
/**
|
|
318
|
+
* Commit a SPECIFIC set of paths (absolute or repo-relative) with an
|
|
319
|
+
* AGENT-AUTHORED message — backs the git_commit tool. Stages only `paths` (minus
|
|
320
|
+
* secrets/artifacts), commits with the model's message + the attribution line,
|
|
321
|
+
* scoped by pathspec. No-op if nothing among `paths` is actually staged
|
|
322
|
+
* (e.g. already committed). Never throws.
|
|
323
|
+
*/
|
|
324
|
+
export async function commitSpecificPaths(cwd, paths, message) {
|
|
325
|
+
if (process.env.MUONROI_AUTO_COMMIT === "0")
|
|
326
|
+
return { committed: false, reason: "disabled" };
|
|
327
|
+
const safe = paths.filter((p) => !isExcludedPath(p));
|
|
328
|
+
if (safe.length === 0)
|
|
329
|
+
return { committed: false, reason: "no-eligible-paths" };
|
|
330
|
+
const inRepo = await git(cwd, ["rev-parse", "--is-inside-work-tree"]);
|
|
331
|
+
if (!inRepo.ok || inRepo.stdout.trim() !== "true")
|
|
332
|
+
return { committed: false, reason: "not-a-repo" };
|
|
333
|
+
const add = await git(cwd, ["add", "--", ...safe]);
|
|
334
|
+
if (!add.ok) {
|
|
335
|
+
logger.error("orchestrator", `[git_commit] git add failed for ${safe.length} path(s) in ${cwd}`);
|
|
336
|
+
return { committed: false, reason: "add-failed" };
|
|
337
|
+
}
|
|
338
|
+
// Only commit if these paths actually have staged changes (idempotent across
|
|
339
|
+
// repeat calls — already-committed files stage nothing).
|
|
340
|
+
const staged = await git(cwd, ["diff", "--cached", "--name-only", "--", ...safe]);
|
|
341
|
+
if (!staged.ok || !staged.stdout.trim())
|
|
342
|
+
return { committed: false, reason: "nothing-staged" };
|
|
343
|
+
// G1 quality gate: block the commit if any staged file has an LSP error. The
|
|
344
|
+
// git_commit tool surfaces reason+detail so the agent can fix and recommit.
|
|
345
|
+
const gate = await gateStagedPaths(cwd, safe);
|
|
346
|
+
if (!gate.ok) {
|
|
347
|
+
return { committed: false, reason: "lsp-errors", detail: gate.summary };
|
|
348
|
+
}
|
|
349
|
+
const { subject, body } = splitCommitMessage(message);
|
|
350
|
+
// Separate -m flags so git inserts the blank-line separators itself.
|
|
351
|
+
const mArgs = body
|
|
352
|
+
? ["-m", subject, "-m", body, "-m", AUTO_COMMIT_ATTRIBUTION]
|
|
353
|
+
: ["-m", subject, "-m", AUTO_COMMIT_ATTRIBUTION];
|
|
354
|
+
const commit = await git(cwd, ["commit", ...mArgs, "--", ...safe]);
|
|
355
|
+
if (!commit.ok) {
|
|
356
|
+
logger.error("orchestrator", `[git_commit] git commit failed in ${cwd} (a pre-commit/commit-msg hook may have rejected it)`);
|
|
357
|
+
return { committed: false, reason: "commit-failed" };
|
|
358
|
+
}
|
|
359
|
+
const head = await git(cwd, ["rev-parse", "--short", "HEAD"]);
|
|
360
|
+
return {
|
|
361
|
+
committed: true,
|
|
362
|
+
sha: head.ok ? head.stdout.trim() : undefined,
|
|
363
|
+
fileCount: staged.stdout.trim().split("\n").length,
|
|
364
|
+
};
|
|
365
|
+
}
|
|
366
|
+
//# sourceMappingURL=auto-commit.js.map
|
|
@@ -3,7 +3,7 @@ import type { getModelInfo } from "../models/registry.js";
|
|
|
3
3
|
import { type resolveModelRuntime } from "../providers/runtime.js";
|
|
4
4
|
import type { BashTool } from "../tools/bash.js";
|
|
5
5
|
import type { AgentMode, StreamChunk, TaskRequest, ToolCall, ToolResult } from "../types/index.js";
|
|
6
|
-
import type { BatchClientOptions, LegacyProvider
|
|
6
|
+
import type { BatchClientOptions, LegacyProvider } from "./agent-options.js";
|
|
7
7
|
import type { TurnRunnerDepsBase } from "./turn-runner-deps.js";
|
|
8
8
|
/**
|
|
9
9
|
* Dependency surface BatchTurnRunner needs to reach back into Agent state
|
|
@@ -46,7 +46,8 @@ export declare class BatchTurnRunner {
|
|
|
46
46
|
constructor(deps: BatchTurnRunnerDeps);
|
|
47
47
|
run(args: {
|
|
48
48
|
userModelMessage: ModelMessage;
|
|
49
|
-
|
|
49
|
+
userEnrichedMessage: ModelMessage;
|
|
50
|
+
observer?: any;
|
|
50
51
|
provider: LegacyProvider;
|
|
51
52
|
subagents: unknown[];
|
|
52
53
|
system: string;
|
|
@@ -133,6 +133,7 @@ export class BatchTurnRunner {
|
|
|
133
133
|
const batchRequestId = `turn-${Date.now()}-${stepNumber}`;
|
|
134
134
|
// Phase O1 — capture providerOptions SHAPE for batch path too.
|
|
135
135
|
deps.setLastProviderOptionsShape(extractProviderOptionsShape(runtime.providerOptions));
|
|
136
|
+
const _messagesForCall = deps.messages.map((m) => (m === args.userModelMessage ? args.userEnrichedMessage : m));
|
|
136
137
|
await addBatchRequests({
|
|
137
138
|
...deps.getBatchClientOptions(signal),
|
|
138
139
|
batchId: batch.batch_id,
|
|
@@ -143,7 +144,7 @@ export class BatchTurnRunner {
|
|
|
143
144
|
chat_get_completion: buildBatchChatCompletionRequest({
|
|
144
145
|
modelId: runtime.modelId,
|
|
145
146
|
system,
|
|
146
|
-
messages: [...
|
|
147
|
+
messages: [..._messagesForCall, ...turnMessages],
|
|
147
148
|
temperature: 0.7,
|
|
148
149
|
maxOutputTokens: !batchCaps.acceptsParam("maxOutputTokens", runtime.modelInfo)
|
|
149
150
|
? undefined
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compaction proposer prompt — model decides if compaction is needed and what to keep/drop.
|
|
3
|
+
* Replaces the heuristic shouldCompactContext + findCutPoint with LLM judgement.
|
|
4
|
+
*
|
|
5
|
+
* The principle: only compact when the model says yes, and keep exactly what the model says to keep.
|
|
6
|
+
* No hard-coded thresholds, no automatic FIFO cutting.
|
|
7
|
+
*/
|
|
8
|
+
export declare const COMPACT_PROPOSER_SYSTEM_PROMPT = "You are a conversation-compaction proposer. Your job is to decide:\n1. Whether compaction is NEEDED right now\n2. Which tool outputs / messages to KEEP verbatim\n3. Which tool outputs / messages to DROP entirely\n4. Which tool outputs / messages to SUMMARIZE\n\nRules:\n- DO NOT compact if the current task is IN PROGRESS and tool-output evidence is needed for the next steps.\n- DO NOT compact mid-turn \u2014 wait until a natural pause (user message boundary, task completed).\n- ALWAYS keep error messages, test-failure logs, and critical data (file paths, function names, API responses).\n- ALWAYS keep the most recent N tool-turn results that contain evidence for unfinished work.\n- Transient outputs (directory listings, git log --oneline, simple confirmations) can be DROPPED.\n- Raw file contents that were used as evidence for edits should be DROPPED once the edit is verified (the edit result is what matters).\n- Long search/grep outputs should be SUMMARIZED to preserve which files matched, not the full content.\n\nOutput format \u2014 strict JSON, no markdown fences, no extra text:\n{\n \"shouldCompact\": boolean,\n \"reason\": \"short explanation why compact or not\",\n \"actions\": [\n {\n \"messageIndex\": number,\n \"action\": \"keep\" | \"drop\" | \"summarize\",\n \"reason\": \"why this action for this message\"\n }\n ]\n}\n\n- \"messageIndex\" refers to the index in the messages array I provide.\n- \"keep\" = keep verbatim in the active conversation.\n- \"drop\" = remove entirely from conversation (not even in summary).\n- \"summarize\" = include in the summary generation input.\n- The most recent turn (user + assistant + tool messages) should always be \"keep\".\n- Only mark messages as \"drop\" if they are truly transient and irrelevant to future work.\n- When in doubt, prefer \"keep\" or \"summarize\" over \"drop\".";
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Compaction proposer prompt — model decides if compaction is needed and what to keep/drop.
|
|
3
|
+
* Replaces the heuristic shouldCompactContext + findCutPoint with LLM judgement.
|
|
4
|
+
*
|
|
5
|
+
* The principle: only compact when the model says yes, and keep exactly what the model says to keep.
|
|
6
|
+
* No hard-coded thresholds, no automatic FIFO cutting.
|
|
7
|
+
*/
|
|
8
|
+
export const COMPACT_PROPOSER_SYSTEM_PROMPT = `You are a conversation-compaction proposer. Your job is to decide:
|
|
9
|
+
1. Whether compaction is NEEDED right now
|
|
10
|
+
2. Which tool outputs / messages to KEEP verbatim
|
|
11
|
+
3. Which tool outputs / messages to DROP entirely
|
|
12
|
+
4. Which tool outputs / messages to SUMMARIZE
|
|
13
|
+
|
|
14
|
+
Rules:
|
|
15
|
+
- DO NOT compact if the current task is IN PROGRESS and tool-output evidence is needed for the next steps.
|
|
16
|
+
- DO NOT compact mid-turn — wait until a natural pause (user message boundary, task completed).
|
|
17
|
+
- ALWAYS keep error messages, test-failure logs, and critical data (file paths, function names, API responses).
|
|
18
|
+
- ALWAYS keep the most recent N tool-turn results that contain evidence for unfinished work.
|
|
19
|
+
- Transient outputs (directory listings, git log --oneline, simple confirmations) can be DROPPED.
|
|
20
|
+
- Raw file contents that were used as evidence for edits should be DROPPED once the edit is verified (the edit result is what matters).
|
|
21
|
+
- Long search/grep outputs should be SUMMARIZED to preserve which files matched, not the full content.
|
|
22
|
+
|
|
23
|
+
Output format — strict JSON, no markdown fences, no extra text:
|
|
24
|
+
{
|
|
25
|
+
"shouldCompact": boolean,
|
|
26
|
+
"reason": "short explanation why compact or not",
|
|
27
|
+
"actions": [
|
|
28
|
+
{
|
|
29
|
+
"messageIndex": number,
|
|
30
|
+
"action": "keep" | "drop" | "summarize",
|
|
31
|
+
"reason": "why this action for this message"
|
|
32
|
+
}
|
|
33
|
+
]
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
- "messageIndex" refers to the index in the messages array I provide.
|
|
37
|
+
- "keep" = keep verbatim in the active conversation.
|
|
38
|
+
- "drop" = remove entirely from conversation (not even in summary).
|
|
39
|
+
- "summarize" = include in the summary generation input.
|
|
40
|
+
- The most recent turn (user + assistant + tool messages) should always be "keep".
|
|
41
|
+
- Only mark messages as "drop" if they are truly transient and irrelevant to future work.
|
|
42
|
+
- When in doubt, prefer "keep" or "summarize" over "drop".`;
|
|
43
|
+
//# sourceMappingURL=compaction-proposer-prompt.js.map
|
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
import { type ModelMessage } from "ai";
|
|
2
2
|
import type { ProviderFactory as LegacyProvider } from "../providers/runtime.js";
|
|
3
|
+
/**
|
|
4
|
+
* A single action the compaction proposer model wants to take on one message.
|
|
5
|
+
*/
|
|
6
|
+
export interface CompactionProposeAction {
|
|
7
|
+
messageIndex: number;
|
|
8
|
+
action: "keep" | "drop" | "summarize";
|
|
9
|
+
reason: string;
|
|
10
|
+
}
|
|
11
|
+
/**
|
|
12
|
+
* The full decision returned by the compaction proposer model.
|
|
13
|
+
*/
|
|
14
|
+
export interface CompactionProposal {
|
|
15
|
+
shouldCompact: boolean;
|
|
16
|
+
reason: string;
|
|
17
|
+
actions: CompactionProposeAction[];
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Call the model to propose what to compact/keep/drop.
|
|
21
|
+
* The model sees the current messages and decides whether compaction is needed at all.
|
|
22
|
+
* Returns null if the proposer call fails (caller should fall back gracefully).
|
|
23
|
+
*/
|
|
24
|
+
export declare function proposeCompaction(provider: LegacyProvider, modelId: string, messages: ModelMessage[], signal?: AbortSignal): Promise<CompactionProposal | null>;
|
|
3
25
|
export interface CompactionSettings {
|
|
4
26
|
reserveTokens: number;
|
|
5
27
|
keepRecentTokens: number;
|
|
@@ -2,8 +2,50 @@ import { generateText } from "ai";
|
|
|
2
2
|
import { isMetaAnalysisPrompt } from "../pil/layer6-output.js";
|
|
3
3
|
import { getProviderCapabilities } from "../providers/capabilities.js";
|
|
4
4
|
import { requireRuntimeProvider, resolveModelRuntime } from "../providers/runtime.js";
|
|
5
|
+
import { COMPACT_PROPOSER_SYSTEM_PROMPT } from "./compaction-proposer-prompt.js";
|
|
5
6
|
import { containsEncryptedReasoning } from "./reasoning.js";
|
|
6
7
|
import { countTokens } from "./token-counter.js";
|
|
8
|
+
/**
|
|
9
|
+
* Call the model to propose what to compact/keep/drop.
|
|
10
|
+
* The model sees the current messages and decides whether compaction is needed at all.
|
|
11
|
+
* Returns null if the proposer call fails (caller should fall back gracefully).
|
|
12
|
+
*/
|
|
13
|
+
export async function proposeCompaction(provider, modelId, messages, signal) {
|
|
14
|
+
try {
|
|
15
|
+
const runtime = resolveModelRuntime(provider, modelId);
|
|
16
|
+
const compactCaps = getProviderCapabilities(requireRuntimeProvider(runtime));
|
|
17
|
+
const serialized = serializeConversation(messages);
|
|
18
|
+
const result = await generateText({
|
|
19
|
+
model: runtime.model,
|
|
20
|
+
system: COMPACT_PROPOSER_SYSTEM_PROMPT,
|
|
21
|
+
prompt: `Current conversation messages:\n\n${serialized}\n\nDecide if compaction is needed and what to keep/drop/summarize. Return strict JSON.`,
|
|
22
|
+
abortSignal: signal,
|
|
23
|
+
maxRetries: 1,
|
|
24
|
+
temperature: 0.1,
|
|
25
|
+
...(!compactCaps.acceptsParam("maxOutputTokens", runtime.modelInfo) ? {} : { maxOutputTokens: 2048 }),
|
|
26
|
+
...(runtime.providerOptions ? { providerOptions: runtime.providerOptions } : {}),
|
|
27
|
+
});
|
|
28
|
+
const text = result.text.trim();
|
|
29
|
+
// Attempt to parse top-level JSON object. The model might wrap in markdown fences — strip those.
|
|
30
|
+
const jsonStr = text.replace(/^```(?:json)?\s*|\s*```$/gi, "").trim();
|
|
31
|
+
const parsed = JSON.parse(jsonStr);
|
|
32
|
+
// Validate shape
|
|
33
|
+
if (typeof parsed.shouldCompact !== "boolean" || !Array.isArray(parsed.actions)) {
|
|
34
|
+
return null;
|
|
35
|
+
}
|
|
36
|
+
// Validate each action
|
|
37
|
+
for (const action of parsed.actions) {
|
|
38
|
+
if (typeof action.messageIndex !== "number" || !["keep", "drop", "summarize"].includes(action.action)) {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
return parsed;
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
// Proposer failure is non-fatal — caller falls back to heuristic or no-compact
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
7
49
|
const TOOL_RESULT_MAX_CHARS = 8000;
|
|
8
50
|
const MIN_KEPT_TOKENS_ON_RETRY = 4000;
|
|
9
51
|
export const DEFAULT_RESERVE_TOKENS = 16_384;
|
|
@@ -40,6 +40,7 @@ export declare class DelegationManager {
|
|
|
40
40
|
start(request: TaskRequest, options: StartDelegationOptions): Promise<ToolResult>;
|
|
41
41
|
list(): Promise<DelegationRun[]>;
|
|
42
42
|
read(id: string): Promise<string>;
|
|
43
|
+
kill(id: string): Promise<ToolResult>;
|
|
43
44
|
consumeNotifications(): Promise<DelegationNotification[]>;
|
|
44
45
|
private getById;
|
|
45
46
|
}
|
|
@@ -107,6 +107,47 @@ export class DelegationManager {
|
|
|
107
107
|
return `Delegation "${id}" completed, but its saved output could not be read.`;
|
|
108
108
|
}
|
|
109
109
|
}
|
|
110
|
+
async kill(id) {
|
|
111
|
+
const record = await this.getById(id);
|
|
112
|
+
if (!record) {
|
|
113
|
+
return {
|
|
114
|
+
success: false,
|
|
115
|
+
output: `Delegation "${id}" not found.`,
|
|
116
|
+
};
|
|
117
|
+
}
|
|
118
|
+
if (record.status !== "running") {
|
|
119
|
+
return {
|
|
120
|
+
success: false,
|
|
121
|
+
output: `Delegation "${id}" is not running (status: ${record.status}).`,
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
const pid = record.pid;
|
|
125
|
+
if (!pid) {
|
|
126
|
+
const jobPath = path.join(await ensureDelegationsDir(this.getCwd()), `${id}.json`);
|
|
127
|
+
await failDelegation(jobPath, "Cancelled: Process PID was not recorded.");
|
|
128
|
+
return {
|
|
129
|
+
success: true,
|
|
130
|
+
output: `Delegation "${id}" marked as cancelled (no active process ID was found).`,
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
try {
|
|
134
|
+
if (process.platform === "win32") {
|
|
135
|
+
spawn("taskkill", ["/F", "/T", "/PID", pid.toString()]).unref();
|
|
136
|
+
}
|
|
137
|
+
else {
|
|
138
|
+
process.kill(pid, "SIGTERM");
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
catch (err) {
|
|
142
|
+
// ignore
|
|
143
|
+
}
|
|
144
|
+
const jobPath = path.join(await ensureDelegationsDir(this.getCwd()), `${id}.json`);
|
|
145
|
+
await failDelegation(jobPath, "Cancelled by user.");
|
|
146
|
+
return {
|
|
147
|
+
success: true,
|
|
148
|
+
output: `Delegation "${id}" (PID: ${pid}) has been terminated.`,
|
|
149
|
+
};
|
|
150
|
+
}
|
|
110
151
|
async consumeNotifications() {
|
|
111
152
|
const dir = await ensureDelegationsDir(this.getCwd());
|
|
112
153
|
const files = await readDelegationFiles(dir);
|
|
@@ -67,10 +67,24 @@ function humanizeApiErrorBase(error) {
|
|
|
67
67
|
}
|
|
68
68
|
if (APICallError.isInstance(error)) {
|
|
69
69
|
const detail = extractResponseDetail(error.responseBody);
|
|
70
|
+
const status = error.statusCode;
|
|
71
|
+
// 5xx response bodies are usually generic server-side noise that MASKS the
|
|
72
|
+
// actionable status. Verified live: SiliconFlow returns a 500 with body
|
|
73
|
+
// {code:60000,message:"Request failed: Unknown error."} — surfacing that
|
|
74
|
+
// string told the user nothing and hid the fact it was a retryable 500.
|
|
75
|
+
// For 5xx prefer the canned "retry later" message + the status code; keep a
|
|
76
|
+
// body detail only when it actually adds information.
|
|
77
|
+
if (status && status >= 500) {
|
|
78
|
+
const canned = STATUS_MESSAGES[status] ?? "The API server returned an error. Please try again later.";
|
|
79
|
+
if (!detail || isOpaqueDetail(detail))
|
|
80
|
+
return `${canned} (HTTP ${status})`;
|
|
81
|
+
return `${detail} (HTTP ${status})`;
|
|
82
|
+
}
|
|
83
|
+
// 4xx: the body almost always says WHAT was wrong — keep it verbatim.
|
|
70
84
|
if (detail)
|
|
71
85
|
return detail;
|
|
72
|
-
if (
|
|
73
|
-
return STATUS_MESSAGES[
|
|
86
|
+
if (status && STATUS_MESSAGES[status]) {
|
|
87
|
+
return STATUS_MESSAGES[status];
|
|
74
88
|
}
|
|
75
89
|
}
|
|
76
90
|
const raw = error instanceof Error ? error.message : String(error);
|
|
@@ -99,6 +113,17 @@ function extractResponseDetail(body) {
|
|
|
99
113
|
}
|
|
100
114
|
return null;
|
|
101
115
|
}
|
|
116
|
+
/**
|
|
117
|
+
* A provider "detail" string that carries no diagnostic value — generic
|
|
118
|
+
* server-error phrasing that masks the more useful HTTP status. Verified live:
|
|
119
|
+
* SiliconFlow 500s carry message "Request failed: Unknown error.". Used only on
|
|
120
|
+
* the 5xx path so an informative server message is still preferred when present.
|
|
121
|
+
*/
|
|
122
|
+
function isOpaqueDetail(detail) {
|
|
123
|
+
const s = detail.trim().toLowerCase();
|
|
124
|
+
return (s.length < 4 ||
|
|
125
|
+
/unknown error|request failed|internal server error|service (unavailable|temporarily)|bad gateway|gateway time-?out|^error\.?$|^failed\.?$/.test(s));
|
|
126
|
+
}
|
|
102
127
|
const RESPONSE_BODY_CAP = 1000;
|
|
103
128
|
export function summarizeApiErrorForLog(error) {
|
|
104
129
|
if (!APICallError.isInstance(error))
|