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
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* council-turn-length emit contract (G2-b).
|
|
3
|
+
*
|
|
4
|
+
* Drives the REAL `runDebate` generator with a recording CouncilLLM and a
|
|
5
|
+
* capturing `globalThis.__muonroiAgentRuntime`, then asserts the observe-only
|
|
6
|
+
* `council-turn-length` harness event fires for each fully-assembled speaker
|
|
7
|
+
* turn — opening statements (round 0) and discussion turns (round 1+) — carrying
|
|
8
|
+
* the right charCount / wordCount / round / model / correlationId.
|
|
9
|
+
*
|
|
10
|
+
* This exercises the production emit path in src/council/debate.ts end-to-end
|
|
11
|
+
* (not a mocked re-implementation). The MCP harness tool surface only exposes
|
|
12
|
+
* toast/stream.delta via tui_last_event, so this generator-level drive is the
|
|
13
|
+
* faithful wire-level proof that the event fires during a real council run.
|
|
14
|
+
*/
|
|
15
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
16
|
+
import { runDebate } from "../debate.js";
|
|
17
|
+
const OPENING = "Opening alpha beta gamma."; // 25 chars, 4 words
|
|
18
|
+
const ROUND = "Round response one two three."; // round-turn text
|
|
19
|
+
function recordingLlm() {
|
|
20
|
+
return {
|
|
21
|
+
// openingWithRetry + leader-eval + summaries all go through generate. Opening
|
|
22
|
+
// statements use the returned text verbatim → that drives the round-0 emit.
|
|
23
|
+
generate: async () => OPENING,
|
|
24
|
+
// debateWithRetry (discussion turns) is the ONLY caller of debate → drives the
|
|
25
|
+
// round-1+ emit with a known, non-failure string.
|
|
26
|
+
debate: async () => ({ text: ROUND, toolCalls: [] }),
|
|
27
|
+
research: async () => "findings",
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
function makeParticipants() {
|
|
31
|
+
return [
|
|
32
|
+
{ role: "architect", model: "model-arch", position: "", stance: { name: "architect", lens: "design" } },
|
|
33
|
+
{ role: "qa", model: "model-qa", position: "", stance: { name: "qa", lens: "risk" } },
|
|
34
|
+
];
|
|
35
|
+
}
|
|
36
|
+
function makeSpec() {
|
|
37
|
+
return {
|
|
38
|
+
problemStatement: "Decide X vs Y for a small service.",
|
|
39
|
+
constraints: [],
|
|
40
|
+
successCriteria: [],
|
|
41
|
+
scope: "",
|
|
42
|
+
rawQA: [],
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
function makeConfig(participants) {
|
|
46
|
+
return {
|
|
47
|
+
topic: "X vs Y",
|
|
48
|
+
conversationContext: "",
|
|
49
|
+
leaderModelId: "model-leader",
|
|
50
|
+
participants,
|
|
51
|
+
debatePlan: {
|
|
52
|
+
intentSummary: "Pick the better option.",
|
|
53
|
+
stances: [
|
|
54
|
+
{ name: "architect", lens: "design" },
|
|
55
|
+
{ name: "qa", lens: "risk" },
|
|
56
|
+
],
|
|
57
|
+
outputShape: {
|
|
58
|
+
kind: "decision",
|
|
59
|
+
sections: [{ key: "rec", heading: "Recommendation", shape: "list" }],
|
|
60
|
+
guardrails: [],
|
|
61
|
+
},
|
|
62
|
+
plannedRounds: 1,
|
|
63
|
+
},
|
|
64
|
+
// Skip research so the flow goes straight to opening statements deterministically.
|
|
65
|
+
researchSkipOverride: true,
|
|
66
|
+
runId: "sess-turnlen-test",
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
describe("council-turn-length emit (real runDebate)", () => {
|
|
70
|
+
let captured;
|
|
71
|
+
let prevRuntime;
|
|
72
|
+
beforeEach(() => {
|
|
73
|
+
captured = [];
|
|
74
|
+
const g = globalThis;
|
|
75
|
+
prevRuntime = g.__muonroiAgentRuntime;
|
|
76
|
+
g.__muonroiAgentRuntime = { emitEvent: (e) => captured.push(e) };
|
|
77
|
+
});
|
|
78
|
+
afterEach(() => {
|
|
79
|
+
globalThis.__muonroiAgentRuntime = prevRuntime;
|
|
80
|
+
});
|
|
81
|
+
function turnLengthEvents() {
|
|
82
|
+
return captured.filter((e) => e.t === "event" && e.kind === "council-turn-length");
|
|
83
|
+
}
|
|
84
|
+
it("emits a round-0 event per opening speaker with correct counts + correlationId", async () => {
|
|
85
|
+
const participants = makeParticipants();
|
|
86
|
+
const gen = runDebate(makeSpec(), makeConfig(participants), recordingLlm());
|
|
87
|
+
let step = await gen.next();
|
|
88
|
+
while (!step.done)
|
|
89
|
+
step = await gen.next();
|
|
90
|
+
const opening = turnLengthEvents().filter((e) => e.round === 0);
|
|
91
|
+
// One opening statement per participant.
|
|
92
|
+
expect(opening.length).toBe(2);
|
|
93
|
+
for (const e of opening) {
|
|
94
|
+
expect(e.charCount).toBe(OPENING.length);
|
|
95
|
+
expect(e.wordCount).toBe(4);
|
|
96
|
+
expect(e.correlationId).toBe("sess-turnlen-test");
|
|
97
|
+
expect(typeof e.model).toBe("string");
|
|
98
|
+
expect(e.model.length).toBeGreaterThan(0);
|
|
99
|
+
}
|
|
100
|
+
// Roles are the participant stance names / roles.
|
|
101
|
+
const roles = opening.map((e) => e.role);
|
|
102
|
+
expect(roles).toContain("architect");
|
|
103
|
+
expect(roles).toContain("qa");
|
|
104
|
+
});
|
|
105
|
+
it("emits round-1 discussion-turn events with the discussion text's counts", async () => {
|
|
106
|
+
const gen = runDebate(makeSpec(), makeConfig(makeParticipants()), recordingLlm());
|
|
107
|
+
let step = await gen.next();
|
|
108
|
+
while (!step.done)
|
|
109
|
+
step = await gen.next();
|
|
110
|
+
const round1 = turnLengthEvents().filter((e) => e.round === 1);
|
|
111
|
+
expect(round1.length).toBeGreaterThanOrEqual(1);
|
|
112
|
+
for (const e of round1) {
|
|
113
|
+
expect(e.charCount).toBe(ROUND.trim().length);
|
|
114
|
+
expect(e.wordCount).toBe(ROUND.trim().split(/\s+/).filter(Boolean).length);
|
|
115
|
+
expect(e.correlationId).toBe("sess-turnlen-test");
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
it("does not emit when no agent runtime is installed (normal user mode)", async () => {
|
|
119
|
+
globalThis.__muonroiAgentRuntime = undefined;
|
|
120
|
+
const localCaptured = [];
|
|
121
|
+
// Re-install a capturer AFTER clearing to prove the helper truly no-ops when
|
|
122
|
+
// the runtime is absent: here it's absent, so localCaptured stays empty.
|
|
123
|
+
const gen = runDebate(makeSpec(), makeConfig(makeParticipants()), recordingLlm());
|
|
124
|
+
let step = await gen.next();
|
|
125
|
+
while (!step.done)
|
|
126
|
+
step = await gen.next();
|
|
127
|
+
expect(localCaptured.length).toBe(0);
|
|
128
|
+
});
|
|
129
|
+
});
|
|
130
|
+
//# sourceMappingURL=council-turn-length-emit.test.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* C (latency UX) — per-kind debate round cap. Before this, only implementation_plan
|
|
3
|
+
* was capped (3); a `decision` debate inherited the absolute ceiling (8) and ran as
|
|
4
|
+
* many rounds as the planner proposed. Live, a Redis-vs-in-memory decision ran 5
|
|
5
|
+
* rounds / ~10 min with the leader itself calling rounds 4-5 "minor disagreements".
|
|
6
|
+
* resolveDebateRoundBudget now caps the discussion kinds at 3 (exploration 5).
|
|
7
|
+
*/
|
|
8
|
+
import { describe, expect, it } from "vitest";
|
|
9
|
+
import { resolveDebateRoundBudget } from "../debate.js";
|
|
10
|
+
describe("resolveDebateRoundBudget — per-kind round cap (issue C)", () => {
|
|
11
|
+
it("caps a decision at 3 rounds even when the planner proposes 5", () => {
|
|
12
|
+
const r = resolveDebateRoundBudget("decision", 5);
|
|
13
|
+
expect(r.maxRounds).toBe(3);
|
|
14
|
+
expect(r.effectiveCeiling).toBe(3);
|
|
15
|
+
expect(r.kindCapped).toBe(true);
|
|
16
|
+
});
|
|
17
|
+
it("caps evaluation and investigation at 3", () => {
|
|
18
|
+
expect(resolveDebateRoundBudget("evaluation", 8).maxRounds).toBe(3);
|
|
19
|
+
expect(resolveDebateRoundBudget("investigation", 8).maxRounds).toBe(3);
|
|
20
|
+
});
|
|
21
|
+
it("keeps implementation_plan at its existing 3-round cap", () => {
|
|
22
|
+
expect(resolveDebateRoundBudget("implementation_plan", 5).maxRounds).toBe(3);
|
|
23
|
+
});
|
|
24
|
+
it("allows exploration more breadth (5)", () => {
|
|
25
|
+
const r = resolveDebateRoundBudget("exploration", 8);
|
|
26
|
+
expect(r.maxRounds).toBe(5);
|
|
27
|
+
expect(r.effectiveCeiling).toBe(5);
|
|
28
|
+
});
|
|
29
|
+
it("falls back to the absolute ceiling (8) for an unknown kind", () => {
|
|
30
|
+
const r = resolveDebateRoundBudget("something-else", 7);
|
|
31
|
+
expect(r.maxRounds).toBe(7);
|
|
32
|
+
expect(r.effectiveCeiling).toBe(8);
|
|
33
|
+
expect(r.kindCapped).toBe(false);
|
|
34
|
+
});
|
|
35
|
+
it("uses the default budget (3) when the planner proposes nothing", () => {
|
|
36
|
+
expect(resolveDebateRoundBudget("decision", undefined).maxRounds).toBe(3);
|
|
37
|
+
expect(resolveDebateRoundBudget(undefined, undefined).maxRounds).toBe(3);
|
|
38
|
+
expect(resolveDebateRoundBudget(undefined, 0).maxRounds).toBe(3);
|
|
39
|
+
});
|
|
40
|
+
it("respects a planner proposal lower than the cap", () => {
|
|
41
|
+
expect(resolveDebateRoundBudget("decision", 1).maxRounds).toBe(1);
|
|
42
|
+
expect(resolveDebateRoundBudget("implementation_plan", 2).maxRounds).toBe(2);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
//# sourceMappingURL=debate-round-budget.test.js.map
|
|
@@ -359,6 +359,7 @@ describe("CQ-10: planDebate returns FALLBACK_PLAN after double failure", () => {
|
|
|
359
359
|
vi.doMock("../../providers/runtime.js", () => ({
|
|
360
360
|
detectProviderForModel: vi.fn().mockReturnValue("openai"),
|
|
361
361
|
createProviderFactory: vi.fn().mockReturnValue({ factory: {} }),
|
|
362
|
+
createProviderFactoryAsync: vi.fn().mockResolvedValue({ factory: {} }),
|
|
362
363
|
resolveModelRuntime: vi.fn().mockReturnValue({ model: {}, providerOptions: undefined }),
|
|
363
364
|
}));
|
|
364
365
|
vi.doMock("../prompts.js", () => ({
|
|
@@ -401,6 +402,7 @@ describe("CQ-10: planDebate returns FALLBACK_PLAN after double failure", () => {
|
|
|
401
402
|
vi.doMock("../../providers/runtime.js", () => ({
|
|
402
403
|
detectProviderForModel: vi.fn().mockReturnValue("openai"),
|
|
403
404
|
createProviderFactory: vi.fn().mockReturnValue({ factory: {} }),
|
|
405
|
+
createProviderFactoryAsync: vi.fn().mockResolvedValue({ factory: {} }),
|
|
404
406
|
resolveModelRuntime: vi.fn().mockReturnValue({ model: {}, providerOptions: undefined }),
|
|
405
407
|
}));
|
|
406
408
|
vi.doMock("../prompts.js", () => ({
|
|
@@ -433,6 +435,7 @@ describe("CQ-10: planDebate returns FALLBACK_PLAN after double failure", () => {
|
|
|
433
435
|
vi.doMock("../../providers/runtime.js", () => ({
|
|
434
436
|
detectProviderForModel: vi.fn().mockReturnValue("openai"),
|
|
435
437
|
createProviderFactory: vi.fn().mockReturnValue({ factory: {} }),
|
|
438
|
+
createProviderFactoryAsync: vi.fn().mockResolvedValue({ factory: {} }),
|
|
436
439
|
resolveModelRuntime: vi.fn().mockReturnValue({ model: {}, providerOptions: undefined }),
|
|
437
440
|
}));
|
|
438
441
|
vi.doMock("../prompts.js", () => ({
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Issue #3 — post-debate default mismatch.
|
|
3
|
+
*
|
|
4
|
+
* Before the fix, `runCouncil`'s post-debate AskCard recommended "generate_plan"
|
|
5
|
+
* (Lock plan & execute Sprint 1) for ANY successful synthesis with no plan yet —
|
|
6
|
+
* ignoring `debatePlan.outputShape.kind`. For a pure decision/evaluation topic the
|
|
7
|
+
* user wanted a decision, not to build, so defaulting to "kick off a sprint" was
|
|
8
|
+
* the wrong next step. `pickPostDebateRecommendation` now only defaults to
|
|
9
|
+
* generate_plan for `implementation_plan`-shaped debates; everything else defaults
|
|
10
|
+
* to `save_exit` (the synthesis IS the deliverable). The generate_plan OPTION is
|
|
11
|
+
* still offered — only the pre-selected default changed.
|
|
12
|
+
*/
|
|
13
|
+
import { describe, expect, it } from "vitest";
|
|
14
|
+
import { pickPostDebateRecommendation } from "../index.js";
|
|
15
|
+
const base = {
|
|
16
|
+
synthesisFailed: false,
|
|
17
|
+
hasEmptySections: false,
|
|
18
|
+
refinementTopics: [],
|
|
19
|
+
confidenceLevel: "high",
|
|
20
|
+
hasPlan: false,
|
|
21
|
+
};
|
|
22
|
+
describe("pickPostDebateRecommendation — issue #3 default", () => {
|
|
23
|
+
it("defaults implementation_plan (no plan yet) to generate_plan", () => {
|
|
24
|
+
const r = pickPostDebateRecommendation({ ...base, outputKind: "implementation_plan" });
|
|
25
|
+
expect(r.value).toBe("generate_plan");
|
|
26
|
+
});
|
|
27
|
+
for (const kind of ["decision", "evaluation", "investigation", "exploration", "other"]) {
|
|
28
|
+
it(`defaults ${kind} (no plan) to save_exit, not generate_plan`, () => {
|
|
29
|
+
const r = pickPostDebateRecommendation({ ...base, outputKind: kind });
|
|
30
|
+
expect(r.value).toBe("save_exit");
|
|
31
|
+
// Reason names the shape so the card explains WHY save is the default.
|
|
32
|
+
expect(r.reason).toContain(kind);
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
it("retry_synthesis wins on synthesis failure regardless of kind", () => {
|
|
36
|
+
const r = pickPostDebateRecommendation({ ...base, synthesisFailed: true, outputKind: "decision" });
|
|
37
|
+
expect(r.value).toBe("retry_synthesis");
|
|
38
|
+
});
|
|
39
|
+
it("refine wins when the debate left sections empty", () => {
|
|
40
|
+
const r = pickPostDebateRecommendation({
|
|
41
|
+
...base,
|
|
42
|
+
hasEmptySections: true,
|
|
43
|
+
refinementTopics: ["Risks", "Trade-offs"],
|
|
44
|
+
outputKind: "implementation_plan",
|
|
45
|
+
});
|
|
46
|
+
expect(r.value).toBe("refine");
|
|
47
|
+
expect(r.reason).toContain("2");
|
|
48
|
+
});
|
|
49
|
+
it("low confidence routes to ask_followup before the kind split", () => {
|
|
50
|
+
const r = pickPostDebateRecommendation({ ...base, confidenceLevel: "low", outputKind: "implementation_plan" });
|
|
51
|
+
expect(r.value).toBe("ask_followup");
|
|
52
|
+
});
|
|
53
|
+
it("an existing plan always defaults to save_exit", () => {
|
|
54
|
+
const r = pickPostDebateRecommendation({ ...base, hasPlan: true, outputKind: "implementation_plan" });
|
|
55
|
+
expect(r.value).toBe("save_exit");
|
|
56
|
+
});
|
|
57
|
+
});
|
|
58
|
+
//# sourceMappingURL=post-debate-recommendation.test.js.map
|
|
@@ -51,6 +51,7 @@ describe("CQ-04: research() URL detection and gap annotation", () => {
|
|
|
51
51
|
createProviderFactory: vi.fn().mockReturnValue({
|
|
52
52
|
factory: {},
|
|
53
53
|
}),
|
|
54
|
+
createProviderFactoryAsync: vi.fn().mockResolvedValue({ factory: {} }),
|
|
54
55
|
resolveModelRuntime: vi.fn().mockReturnValue({
|
|
55
56
|
model: {},
|
|
56
57
|
providerOptions: undefined,
|
|
@@ -93,6 +94,7 @@ describe("CQ-04: research() URL detection and gap annotation", () => {
|
|
|
93
94
|
vi.doMock("../../providers/runtime.js", () => ({
|
|
94
95
|
detectProviderForModel: vi.fn().mockReturnValue("openai"),
|
|
95
96
|
createProviderFactory: vi.fn().mockReturnValue({ factory: {} }),
|
|
97
|
+
createProviderFactoryAsync: vi.fn().mockResolvedValue({ factory: {} }),
|
|
96
98
|
resolveModelRuntime: vi.fn().mockReturnValue({ model: {}, providerOptions: undefined }),
|
|
97
99
|
}));
|
|
98
100
|
vi.doMock("../../tools/registry.js", () => ({
|
|
@@ -141,6 +143,7 @@ describe("CQ-03: research() MCP tool merge", () => {
|
|
|
141
143
|
vi.doMock("../../providers/runtime.js", () => ({
|
|
142
144
|
detectProviderForModel: vi.fn().mockReturnValue("openai"),
|
|
143
145
|
createProviderFactory: vi.fn().mockReturnValue({ factory: {} }),
|
|
146
|
+
createProviderFactoryAsync: vi.fn().mockResolvedValue({ factory: {} }),
|
|
144
147
|
resolveModelRuntime: vi.fn().mockReturnValue({ model: {}, providerOptions: undefined }),
|
|
145
148
|
}));
|
|
146
149
|
vi.doMock("../../tools/registry.js", () => ({
|
|
@@ -185,6 +188,7 @@ describe("CQ-03: research() MCP tool merge", () => {
|
|
|
185
188
|
vi.doMock("../../providers/runtime.js", () => ({
|
|
186
189
|
detectProviderForModel: vi.fn().mockReturnValue("openai"),
|
|
187
190
|
createProviderFactory: vi.fn().mockReturnValue({ factory: {} }),
|
|
191
|
+
createProviderFactoryAsync: vi.fn().mockResolvedValue({ factory: {} }),
|
|
188
192
|
resolveModelRuntime: vi.fn().mockReturnValue({ model: {}, providerOptions: undefined }),
|
|
189
193
|
}));
|
|
190
194
|
vi.doMock("../../tools/registry.js", () => ({
|
|
@@ -33,6 +33,7 @@ describe("debate() call shape — tools off by default, on with explicit opt-in"
|
|
|
33
33
|
vi.doMock("../../providers/runtime.js", () => ({
|
|
34
34
|
detectProviderForModel: vi.fn().mockReturnValue("openai"),
|
|
35
35
|
createProviderFactory: vi.fn().mockReturnValue({ factory: {} }),
|
|
36
|
+
createProviderFactoryAsync: vi.fn().mockResolvedValue({ factory: {} }),
|
|
36
37
|
resolveModelRuntime: vi.fn().mockReturnValue({ model: {}, providerOptions: undefined }),
|
|
37
38
|
}));
|
|
38
39
|
vi.doMock("../prompts.js", () => ({
|
|
@@ -63,6 +64,7 @@ describe("debate() call shape — tools off by default, on with explicit opt-in"
|
|
|
63
64
|
vi.doMock("../../providers/runtime.js", () => ({
|
|
64
65
|
detectProviderForModel: vi.fn().mockReturnValue("openai"),
|
|
65
66
|
createProviderFactory: vi.fn().mockReturnValue({ factory: {} }),
|
|
67
|
+
createProviderFactoryAsync: vi.fn().mockResolvedValue({ factory: {} }),
|
|
66
68
|
resolveModelRuntime: vi.fn().mockReturnValue({ model: {}, providerOptions: undefined }),
|
|
67
69
|
}));
|
|
68
70
|
// Builtin tools — must contain grep+read_file so the filter keeps something
|
|
@@ -113,6 +115,7 @@ describe("debate() call shape — tools off by default, on with explicit opt-in"
|
|
|
113
115
|
vi.doMock("../../providers/runtime.js", () => ({
|
|
114
116
|
detectProviderForModel: vi.fn().mockReturnValue("openai"),
|
|
115
117
|
createProviderFactory: vi.fn().mockReturnValue({ factory: {} }),
|
|
118
|
+
createProviderFactoryAsync: vi.fn().mockResolvedValue({ factory: {} }),
|
|
116
119
|
resolveModelRuntime: vi.fn().mockReturnValue({ model: {}, providerOptions: undefined }),
|
|
117
120
|
}));
|
|
118
121
|
vi.doMock("../../tools/registry.js", () => ({
|
|
@@ -162,6 +165,7 @@ describe("CQ-07: debate() returns { text, toolCalls } — not bare string", () =
|
|
|
162
165
|
vi.doMock("../../providers/runtime.js", () => ({
|
|
163
166
|
detectProviderForModel: vi.fn().mockReturnValue("openai"),
|
|
164
167
|
createProviderFactory: vi.fn().mockReturnValue({ factory: {} }),
|
|
168
|
+
createProviderFactoryAsync: vi.fn().mockResolvedValue({ factory: {} }),
|
|
165
169
|
resolveModelRuntime: vi.fn().mockReturnValue({ model: {}, providerOptions: undefined }),
|
|
166
170
|
}));
|
|
167
171
|
vi.doMock("../../tools/registry.js", () => ({
|
|
@@ -209,6 +213,7 @@ describe("CQ-07: debate() returns { text, toolCalls } — not bare string", () =
|
|
|
209
213
|
vi.doMock("../../providers/runtime.js", () => ({
|
|
210
214
|
detectProviderForModel: vi.fn().mockReturnValue("openai"),
|
|
211
215
|
createProviderFactory: vi.fn().mockReturnValue({ factory: {} }),
|
|
216
|
+
createProviderFactoryAsync: vi.fn().mockResolvedValue({ factory: {} }),
|
|
212
217
|
resolveModelRuntime: vi.fn().mockReturnValue({ model: {}, providerOptions: undefined }),
|
|
213
218
|
}));
|
|
214
219
|
vi.doMock("../../tools/registry.js", () => ({
|
|
@@ -244,6 +249,7 @@ describe("CQ-07: debate() returns { text, toolCalls } — not bare string", () =
|
|
|
244
249
|
vi.doMock("../../providers/runtime.js", () => ({
|
|
245
250
|
detectProviderForModel: vi.fn().mockReturnValue("openai"),
|
|
246
251
|
createProviderFactory: vi.fn().mockReturnValue({ factory: {} }),
|
|
252
|
+
createProviderFactoryAsync: vi.fn().mockResolvedValue({ factory: {} }),
|
|
247
253
|
resolveModelRuntime: vi.fn().mockReturnValue({ model: {}, providerOptions: undefined }),
|
|
248
254
|
}));
|
|
249
255
|
vi.doMock("../../tools/registry.js", () => ({
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/council/clarifier-question-cap.test.ts
|
|
3
|
+
*
|
|
4
|
+
* G2-a regression backstop. The clarifier prompt asks for "typically 0-2"
|
|
5
|
+
* questions and tells the model NOT to ask generic greenfield questions in an
|
|
6
|
+
* existing repo, but the model has repeatedly ignored both (session
|
|
7
|
+
* cfc711c57df0: 6 generic greenfield questions on "improve council quality").
|
|
8
|
+
* capClarifierQuestions enforces this deterministically in code. These tests
|
|
9
|
+
* pin that behaviour; the live council surfacing is verified via the MCP
|
|
10
|
+
* harness separately.
|
|
11
|
+
*/
|
|
12
|
+
export {};
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* src/council/clarifier-question-cap.test.ts
|
|
3
|
+
*
|
|
4
|
+
* G2-a regression backstop. The clarifier prompt asks for "typically 0-2"
|
|
5
|
+
* questions and tells the model NOT to ask generic greenfield questions in an
|
|
6
|
+
* existing repo, but the model has repeatedly ignored both (session
|
|
7
|
+
* cfc711c57df0: 6 generic greenfield questions on "improve council quality").
|
|
8
|
+
* capClarifierQuestions enforces this deterministically in code. These tests
|
|
9
|
+
* pin that behaviour; the live council surfacing is verified via the MCP
|
|
10
|
+
* harness separately.
|
|
11
|
+
*/
|
|
12
|
+
import { describe, expect, it } from "vitest";
|
|
13
|
+
import { capClarifierQuestions, isGenericGreenfieldQuestion, MAX_CLARIFY_QUESTIONS_PER_ROUND } from "./clarifier.js";
|
|
14
|
+
const scoped = (n) => Array.from({ length: n }, (_, i) => ({ question: `Scoped decision ${i} for this change?`, why: "scope" }));
|
|
15
|
+
describe("isGenericGreenfieldQuestion", () => {
|
|
16
|
+
it("matches generic greenfield questions (English)", () => {
|
|
17
|
+
expect(isGenericGreenfieldQuestion({ question: "Who is the target audience?" })).toBe(true);
|
|
18
|
+
expect(isGenericGreenfieldQuestion({ question: "Which programming language should we use?" })).toBe(true);
|
|
19
|
+
expect(isGenericGreenfieldQuestion({ question: "Which database do you want?" })).toBe(true);
|
|
20
|
+
expect(isGenericGreenfieldQuestion({ question: "What is the deployment target?" })).toBe(true);
|
|
21
|
+
expect(isGenericGreenfieldQuestion({ question: "What kind of product is this?" })).toBe(true);
|
|
22
|
+
});
|
|
23
|
+
it("matches generic greenfield questions (Vietnamese)", () => {
|
|
24
|
+
expect(isGenericGreenfieldQuestion({ question: "Đối tượng người dùng là ai?" })).toBe(true);
|
|
25
|
+
expect(isGenericGreenfieldQuestion({ question: "Dùng ngôn ngữ lập trình nào?" })).toBe(true);
|
|
26
|
+
expect(isGenericGreenfieldQuestion({ question: "Cơ sở dữ liệu nào phù hợp?" })).toBe(true);
|
|
27
|
+
});
|
|
28
|
+
it("does NOT match scoped, change-specific questions", () => {
|
|
29
|
+
expect(isGenericGreenfieldQuestion({ question: "Should the clarifier cap be configurable?" })).toBe(false);
|
|
30
|
+
expect(isGenericGreenfieldQuestion({ question: "Phạm vi của thay đổi này là gì?", why: "scope" })).toBe(false);
|
|
31
|
+
expect(isGenericGreenfieldQuestion({ question: "Do we keep backward compat for the old flag?" })).toBe(false);
|
|
32
|
+
});
|
|
33
|
+
});
|
|
34
|
+
describe("capClarifierQuestions", () => {
|
|
35
|
+
it("hard-caps to MAX_CLARIFY_QUESTIONS_PER_ROUND when the model over-asks", () => {
|
|
36
|
+
const { kept, dropped } = capClarifierQuestions(scoped(6), false);
|
|
37
|
+
expect(kept.length).toBe(MAX_CLARIFY_QUESTIONS_PER_ROUND);
|
|
38
|
+
expect(dropped).toBe(6 - MAX_CLARIFY_QUESTIONS_PER_ROUND);
|
|
39
|
+
});
|
|
40
|
+
it("leaves a compliant 0-2 round untouched", () => {
|
|
41
|
+
expect(capClarifierQuestions(scoped(2), false)).toEqual({ kept: scoped(2), dropped: 0 });
|
|
42
|
+
expect(capClarifierQuestions([], false)).toEqual({ kept: [], dropped: 0 });
|
|
43
|
+
});
|
|
44
|
+
it("existing repo: drops generic greenfield questions then caps the rest", () => {
|
|
45
|
+
const qs = [
|
|
46
|
+
{ question: "Who is the target audience?" },
|
|
47
|
+
{ question: "Which framework should we use?" },
|
|
48
|
+
{ question: "Should we preserve the legacy --flag for this change?" },
|
|
49
|
+
{ question: "Is the cap value configurable via settings?" },
|
|
50
|
+
];
|
|
51
|
+
const { kept, dropped } = capClarifierQuestions(qs, true);
|
|
52
|
+
expect(kept.every((q) => !isGenericGreenfieldQuestion(q))).toBe(true);
|
|
53
|
+
expect(kept.length).toBe(2); // 2 generic dropped → 2 scoped remain, within cap
|
|
54
|
+
expect(dropped).toBe(2);
|
|
55
|
+
});
|
|
56
|
+
it("existing repo: never zeroes out a round — keeps the top pick if all look generic", () => {
|
|
57
|
+
const allGeneric = [
|
|
58
|
+
{ question: "Who is the target audience?" },
|
|
59
|
+
{ question: "Which database?" },
|
|
60
|
+
{ question: "What kind of product is this?" },
|
|
61
|
+
];
|
|
62
|
+
const { kept, dropped } = capClarifierQuestions(allGeneric, true);
|
|
63
|
+
expect(kept.length).toBe(1);
|
|
64
|
+
expect(dropped).toBe(2);
|
|
65
|
+
});
|
|
66
|
+
it("greenfield topic (no Current Project snapshot): does NOT drop generics, only caps", () => {
|
|
67
|
+
const qs = [{ question: "Who is the target audience?" }, { question: "Which language?" }];
|
|
68
|
+
const { kept, dropped } = capClarifierQuestions(qs, false);
|
|
69
|
+
expect(kept.length).toBe(2); // legitimate for a greenfield build
|
|
70
|
+
expect(dropped).toBe(0);
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
//# sourceMappingURL=clarifier-question-cap.test.js.map
|
|
@@ -3,6 +3,34 @@ import type { CouncilQuestionOption, StreamChunk } from "../types/index.js";
|
|
|
3
3
|
import type { ClarifiedSpec, CouncilLLM, QuestionResponder } from "./types.js";
|
|
4
4
|
/** P5: Hard cap on clarification rounds regardless of judge verdict. */
|
|
5
5
|
export declare const MAX_CLARIFY_ROUNDS = 12;
|
|
6
|
+
/**
|
|
7
|
+
* G2-a: Hard cap on clarifier questions SURFACED per round, regardless of how
|
|
8
|
+
* many the model emits. buildClarificationPrompt asks for "typically 0-2", but
|
|
9
|
+
* that soft hint has repeatedly been ignored (e.g. 6 generic greenfield
|
|
10
|
+
* questions on a scoped "improve council quality" topic). This deterministic
|
|
11
|
+
* cap enforces the documented target so the user is never carpet-bombed.
|
|
12
|
+
*/
|
|
13
|
+
export declare const MAX_CLARIFY_QUESTIONS_PER_ROUND = 2;
|
|
14
|
+
export declare function isGenericGreenfieldQuestion(q: {
|
|
15
|
+
question: string;
|
|
16
|
+
why?: string;
|
|
17
|
+
}): boolean;
|
|
18
|
+
/**
|
|
19
|
+
* G2-a: deterministically enforce the clarifier's "typically 0-2 questions"
|
|
20
|
+
* rule. In an existing repo (`existingRepo` — a "## Current Project" snapshot
|
|
21
|
+
* is present) first drop generic greenfield questions the prompt already told
|
|
22
|
+
* the model not to ask; then hard-cap the remainder at
|
|
23
|
+
* MAX_CLARIFY_QUESTIONS_PER_ROUND. Never zero out a round that had questions —
|
|
24
|
+
* if every one looked generic, keep the model's top pick rather than asking
|
|
25
|
+
* nothing. Pure + exported so it is unit-testable in isolation.
|
|
26
|
+
*/
|
|
27
|
+
export declare function capClarifierQuestions<T extends {
|
|
28
|
+
question: string;
|
|
29
|
+
why?: string;
|
|
30
|
+
}>(questions: T[], existingRepo: boolean): {
|
|
31
|
+
kept: T[];
|
|
32
|
+
dropped: number;
|
|
33
|
+
};
|
|
6
34
|
/**
|
|
7
35
|
* P5: Call the readiness judge to determine whether the current spec + Q&A is
|
|
8
36
|
* sufficient to start a productive debate, or whether critical gaps remain.
|
|
@@ -41,7 +69,7 @@ export interface ClarifyOptionsResult {
|
|
|
41
69
|
* so the UI knows to suppress the "(Recommended)" tag.
|
|
42
70
|
*/
|
|
43
71
|
export declare function buildClarifyOptions(suggestions: string[] | undefined, recommended?: string): ClarifyOptionsResult;
|
|
44
|
-
export declare function runClarification(topic: string, leaderModelId: string, conversationContext: string, respondToQuestion: QuestionResponder, llm: CouncilLLM,
|
|
72
|
+
export declare function runClarification(topic: string, leaderModelId: string, conversationContext: string, respondToQuestion: QuestionResponder, llm: CouncilLLM, signal?: AbortSignal, seedQuestions?: GrayAreaQuestion[], maxRounds?: number,
|
|
45
73
|
/**
|
|
46
74
|
* Optional pre-filled answers keyed by seed-question id. When the loop's
|
|
47
75
|
* discover phase has already proven a dimension from the project (e.g.
|
|
@@ -4,6 +4,47 @@ import { phaseDone, phaseError, phaseStart } from "./phase-events.js";
|
|
|
4
4
|
import { buildClarificationPrompt, buildReadinessJudgePrompt, buildSpecSynthesisPrompt } from "./prompts.js";
|
|
5
5
|
/** P5: Hard cap on clarification rounds regardless of judge verdict. */
|
|
6
6
|
export const MAX_CLARIFY_ROUNDS = 12;
|
|
7
|
+
/**
|
|
8
|
+
* G2-a: Hard cap on clarifier questions SURFACED per round, regardless of how
|
|
9
|
+
* many the model emits. buildClarificationPrompt asks for "typically 0-2", but
|
|
10
|
+
* that soft hint has repeatedly been ignored (e.g. 6 generic greenfield
|
|
11
|
+
* questions on a scoped "improve council quality" topic). This deterministic
|
|
12
|
+
* cap enforces the documented target so the user is never carpet-bombed.
|
|
13
|
+
*/
|
|
14
|
+
export const MAX_CLARIFY_QUESTIONS_PER_ROUND = 2;
|
|
15
|
+
/**
|
|
16
|
+
* G2-a: Generic "greenfield" questions that the prompt already tells the model
|
|
17
|
+
* NOT to ask when a "## Current Project" snapshot is present (existing repo) —
|
|
18
|
+
* product type, target audience, which language/framework, which database,
|
|
19
|
+
* hosting/deploy target. Matched in both English and Vietnamese on the
|
|
20
|
+
* question + why text. Used only as an existing-repo filter; never applied to
|
|
21
|
+
* greenfield topics where these questions are legitimate.
|
|
22
|
+
*/
|
|
23
|
+
const GREENFIELD_QUESTION_RE = /\b(target audience|product type|what (kind|type) of (product|app|application)|which (programming )?language|which framework|which database|which db|hosting|deploy(ment)? target|tech stack)\b|đối tượng (người dùng|sử dụng)|loại (sản phẩm|ứng dụng)|ngôn ngữ (lập trình|nào)|framework nào|cơ sở dữ liệu|database nào|triển khai ở đâu/i;
|
|
24
|
+
export function isGenericGreenfieldQuestion(q) {
|
|
25
|
+
return GREENFIELD_QUESTION_RE.test(`${q.question} ${q.why ?? ""}`);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* G2-a: deterministically enforce the clarifier's "typically 0-2 questions"
|
|
29
|
+
* rule. In an existing repo (`existingRepo` — a "## Current Project" snapshot
|
|
30
|
+
* is present) first drop generic greenfield questions the prompt already told
|
|
31
|
+
* the model not to ask; then hard-cap the remainder at
|
|
32
|
+
* MAX_CLARIFY_QUESTIONS_PER_ROUND. Never zero out a round that had questions —
|
|
33
|
+
* if every one looked generic, keep the model's top pick rather than asking
|
|
34
|
+
* nothing. Pure + exported so it is unit-testable in isolation.
|
|
35
|
+
*/
|
|
36
|
+
export function capClarifierQuestions(questions, existingRepo) {
|
|
37
|
+
const before = questions.length;
|
|
38
|
+
let kept = questions;
|
|
39
|
+
if (existingRepo) {
|
|
40
|
+
const filtered = kept.filter((q) => !isGenericGreenfieldQuestion(q));
|
|
41
|
+
kept = filtered.length > 0 ? filtered : kept.slice(0, 1);
|
|
42
|
+
}
|
|
43
|
+
if (kept.length > MAX_CLARIFY_QUESTIONS_PER_ROUND) {
|
|
44
|
+
kept = kept.slice(0, MAX_CLARIFY_QUESTIONS_PER_ROUND);
|
|
45
|
+
}
|
|
46
|
+
return { kept, dropped: before - kept.length };
|
|
47
|
+
}
|
|
7
48
|
/**
|
|
8
49
|
* P5: Call the readiness judge to determine whether the current spec + Q&A is
|
|
9
50
|
* sufficient to start a productive debate, or whether critical gaps remain.
|
|
@@ -97,7 +138,7 @@ export function buildClarifyOptions(suggestions, recommended) {
|
|
|
97
138
|
}
|
|
98
139
|
return { options, defaultIndex };
|
|
99
140
|
}
|
|
100
|
-
export async function* runClarification(topic, leaderModelId, conversationContext, respondToQuestion, llm,
|
|
141
|
+
export async function* runClarification(topic, leaderModelId, conversationContext, respondToQuestion, llm, signal, seedQuestions, maxRounds,
|
|
101
142
|
/**
|
|
102
143
|
* Optional pre-filled answers keyed by seed-question id. When the loop's
|
|
103
144
|
* discover phase has already proven a dimension from the project (e.g.
|
|
@@ -128,6 +169,11 @@ costAware = false) {
|
|
|
128
169
|
// gaps from the previous readiness verdict, passed to the next question-generator
|
|
129
170
|
let pendingGaps = [];
|
|
130
171
|
for (let round = 0; round < max; round++) {
|
|
172
|
+
// User cancelled during clarification — stop asking further rounds. The
|
|
173
|
+
// generate calls themselves are already abort-aware (wrapped llm); this
|
|
174
|
+
// guard prevents starting a fresh round after a mid-round cancel.
|
|
175
|
+
if (signal?.aborted)
|
|
176
|
+
break;
|
|
131
177
|
const useSeed = round === 0 && seeded.length > 0;
|
|
132
178
|
const roundId = `phase:clarification-round-${round + 1}`;
|
|
133
179
|
const roundStart = Date.now();
|
|
@@ -182,6 +228,23 @@ costAware = false) {
|
|
|
182
228
|
questions = [];
|
|
183
229
|
}
|
|
184
230
|
}
|
|
231
|
+
// G2-a: enforce the "typically 0-2 questions per round" rule the prompt asks
|
|
232
|
+
// for but the model/seeds repeatedly exceed. Applies to BOTH the LLM path
|
|
233
|
+
// AND the PIL gray-area SEED path — live drive (greenfield "/ideal") showed
|
|
234
|
+
// round-0 seeds (productType/targetPlatform/audience) surfacing uncapped,
|
|
235
|
+
// which a model-path-only cap missed. In an existing repo (a
|
|
236
|
+
// "## Current Project" snapshot is present) generic greenfield questions are
|
|
237
|
+
// also dropped — the documented EE-3589e10d failure. A non-silent content
|
|
238
|
+
// line surfaces whatever was trimmed.
|
|
239
|
+
const existingRepo = conversationContext.includes("## Current Project");
|
|
240
|
+
const capped = capClarifierQuestions(questions, existingRepo);
|
|
241
|
+
questions = capped.kept;
|
|
242
|
+
if (capped.dropped > 0) {
|
|
243
|
+
yield {
|
|
244
|
+
type: "content",
|
|
245
|
+
content: `\n_(clarifier: trimmed ${capped.dropped} question${capped.dropped === 1 ? "" : "s"} — cap ${MAX_CLARIFY_QUESTIONS_PER_ROUND}/round${existingRepo ? " + dropped generic greenfield questions for an existing repo" : ""})_\n`,
|
|
246
|
+
};
|
|
247
|
+
}
|
|
185
248
|
if (questions.length === 0) {
|
|
186
249
|
// The clarifier asking nothing IS the readiness signal — the leader already
|
|
187
250
|
// decided no gaps remain. Mark the spec ready directly rather than leaving the
|
|
@@ -5,4 +5,5 @@ import type { ClarifiedSpec, CouncilLLM, DebatePlan } from "./types.js";
|
|
|
5
5
|
export declare function planDebate(spec: ClarifiedSpec, leaderModelId: string, llm: CouncilLLM, eeWarnings?: CouncilWarning[], // CQ-13: experience snippets to seed prompt
|
|
6
6
|
experienceMode?: CouncilExperienceMode, // CQ-14: controls Experience Auditor injection
|
|
7
7
|
taskType?: string, // CQ-11: task type from PIL (e.g. "architecture", "bugfix")
|
|
8
|
-
complexityTier?: string
|
|
8
|
+
complexityTier?: string, // CQ-11: complexity tier from PIL (e.g. "heavy", "medium", "light")
|
|
9
|
+
signal?: AbortSignal): AsyncGenerator<StreamChunk, DebatePlan, unknown>;
|
|
@@ -97,7 +97,8 @@ function injectAuditorStance(plan, eeWarnings, experienceMode) {
|
|
|
97
97
|
export async function* planDebate(spec, leaderModelId, llm, eeWarnings, // CQ-13: experience snippets to seed prompt
|
|
98
98
|
experienceMode, // CQ-14: controls Experience Auditor injection
|
|
99
99
|
taskType, // CQ-11: task type from PIL (e.g. "architecture", "bugfix")
|
|
100
|
-
complexityTier)
|
|
100
|
+
complexityTier, // CQ-11: complexity tier from PIL (e.g. "heavy", "medium", "light")
|
|
101
|
+
signal) {
|
|
101
102
|
const eeSnippets = eeWarnings?.map((w) => w.text).filter(Boolean) ?? [];
|
|
102
103
|
const { system: baseSystem, prompt } = buildDebatePlanPrompt(spec);
|
|
103
104
|
// Build calibration context from PIL metadata
|
|
@@ -133,7 +134,8 @@ complexityTier) {
|
|
|
133
134
|
// Bound attempt-1: a wedged provider response here would freeze the whole
|
|
134
135
|
// council/loop silently (no streamText stall watchdog covers generateObject).
|
|
135
136
|
// On timeout this rejects → caught below → retry (guarded) → fallback plan.
|
|
136
|
-
|
|
137
|
+
// The user-abort signal is combined in so an Esc during planning aborts it.
|
|
138
|
+
const { signal: timedSignal, cleanup: cleanupTimeout } = withTimeoutSignal(signal, getProviderStallTimeoutMs());
|
|
137
139
|
const { object } = await withDeadlineRace(() => generateObject({
|
|
138
140
|
model: runtime.model,
|
|
139
141
|
schema: DebatePlanSchema,
|
|
@@ -141,7 +143,7 @@ complexityTier) {
|
|
|
141
143
|
prompt,
|
|
142
144
|
abortSignal: timedSignal,
|
|
143
145
|
...(runtime.providerOptions ? { providerOptions: runtime.providerOptions } : {}),
|
|
144
|
-
}), getProviderStallTimeoutMs() + 5_000, "plan_debate").finally(() => cleanupTimeout());
|
|
146
|
+
}), getProviderStallTimeoutMs() + 5_000, "plan_debate", signal).finally(() => cleanupTimeout());
|
|
145
147
|
// Validate with existing sanitize helpers for normalization
|
|
146
148
|
const stances = sanitizeStances(object.stances);
|
|
147
149
|
const outputShape = sanitizeShape(object.outputShape);
|
|
@@ -1,4 +1,13 @@
|
|
|
1
1
|
import type { StreamChunk } from "../types/index.js";
|
|
2
2
|
import type { ClarifiedSpec, CouncilConfig, CouncilLLM, DebateState } from "./types.js";
|
|
3
|
+
/**
|
|
4
|
+
* Resolve the initial round budget + hard ceiling from the plan's output-shape kind
|
|
5
|
+
* and the planner-proposed round count. Pure + exported for unit testing the cap.
|
|
6
|
+
*/
|
|
7
|
+
export declare function resolveDebateRoundBudget(planKind: string | undefined, plannedRounds: number | undefined): {
|
|
8
|
+
maxRounds: number;
|
|
9
|
+
effectiveCeiling: number;
|
|
10
|
+
kindCapped: boolean;
|
|
11
|
+
};
|
|
3
12
|
export declare function runDebate(spec: ClarifiedSpec, config: CouncilConfig, llm: CouncilLLM): AsyncGenerator<StreamChunk, DebateState, unknown>;
|
|
4
13
|
export declare function evaluateResearchNeed(spec: ClarifiedSpec, leaderModelId: string, conversationContext: string, llm: CouncilLLM, costAware?: boolean): AsyncGenerator<StreamChunk, boolean, unknown>;
|