muonroi-cli 1.8.0 → 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.
Files changed (101) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +122 -122
  3. package/dist/src/agent-harness/mock-model.js +112 -4
  4. package/dist/src/cli/cost-forensics.js +17 -14
  5. package/dist/src/council/prompts.js +20 -20
  6. package/dist/src/flow/compaction/index.d.ts +1 -0
  7. package/dist/src/flow/compaction/index.js +4 -0
  8. package/dist/src/generated/version.d.ts +1 -1
  9. package/dist/src/generated/version.js +1 -1
  10. package/dist/src/index.js +4 -2
  11. package/dist/src/lsp/manager.js +11 -3
  12. package/dist/src/lsp/manager.test.js +39 -0
  13. package/dist/src/mcp/auto-setup.js +0 -8
  14. package/dist/src/mcp/oauth-callback.js +2 -2
  15. package/dist/src/mcp/parse-headers.test.js +14 -14
  16. package/dist/src/mcp/self-verify-jobs.d.ts +1 -0
  17. package/dist/src/mcp/self-verify-runner.js +1 -1
  18. package/dist/src/mcp/setup-guide-text.js +74 -74
  19. package/dist/src/mcp/smoke.test.js +43 -43
  20. package/dist/src/ops/doctor.js +7 -7
  21. package/dist/src/orchestrator/__tests__/batch-turn-runner.test.js +1 -0
  22. package/dist/src/orchestrator/__tests__/message-processor.test.js +1 -0
  23. package/dist/src/orchestrator/__tests__/prompts.test.d.ts +1 -0
  24. package/dist/src/orchestrator/__tests__/prompts.test.js +88 -0
  25. package/dist/src/orchestrator/__tests__/sub-session-delegation.test.d.ts +1 -0
  26. package/dist/src/orchestrator/__tests__/sub-session-delegation.test.js +507 -0
  27. package/dist/src/orchestrator/auto-commit.js +9 -7
  28. package/dist/src/orchestrator/message-processor.d.ts +2 -0
  29. package/dist/src/orchestrator/message-processor.js +7 -0
  30. package/dist/src/orchestrator/orchestrator.d.ts +3 -0
  31. package/dist/src/orchestrator/orchestrator.js +362 -28
  32. package/dist/src/orchestrator/pending-calls.js +2 -1
  33. package/dist/src/orchestrator/preprocessor.js +3 -2
  34. package/dist/src/orchestrator/prompts.d.ts +8 -0
  35. package/dist/src/orchestrator/prompts.js +65 -18
  36. package/dist/src/orchestrator/sub-agent-cap.d.ts +2 -0
  37. package/dist/src/orchestrator/sub-agent-cap.js +12 -4
  38. package/dist/src/orchestrator/subagent-compactor.d.ts +7 -0
  39. package/dist/src/orchestrator/subagent-compactor.js +65 -14
  40. package/dist/src/orchestrator/subagent-compactor.spec.js +125 -9
  41. package/dist/src/orchestrator/tool-engine.d.ts +2 -0
  42. package/dist/src/orchestrator/tool-engine.js +80 -21
  43. package/dist/src/orchestrator/turn-runner-deps.d.ts +2 -0
  44. package/dist/src/pil/__tests__/llm-classify.test.js +79 -1
  45. package/dist/src/pil/layer1-intent.js +37 -37
  46. package/dist/src/pil/layer2_5-ponytail.js +8 -8
  47. package/dist/src/pil/llm-classify.d.ts +11 -0
  48. package/dist/src/pil/llm-classify.js +193 -0
  49. package/dist/src/product-loop/done-gate.js +3 -3
  50. package/dist/src/product-loop/loop-driver.js +18 -18
  51. package/dist/src/product-loop/progress-snapshot.js +4 -4
  52. package/dist/src/providers/mcp-vision-bridge.js +48 -48
  53. package/dist/src/reporter/index.js +1 -1
  54. package/dist/src/scaffold/bb-ecosystem-apply.js +47 -47
  55. package/dist/src/scaffold/bb-quality-gate.js +5 -5
  56. package/dist/src/scaffold/continuation-prompt.js +60 -60
  57. package/dist/src/scaffold/init-new.js +453 -453
  58. package/dist/src/self-qa/__tests__/scenario-planner.test.js +3 -3
  59. package/dist/src/self-qa/agentic-loop.js +11 -2
  60. package/dist/src/storage/__tests__/hierarchical-session.test.d.ts +1 -0
  61. package/dist/src/storage/__tests__/hierarchical-session.test.js +161 -0
  62. package/dist/src/storage/__tests__/migrations.test.js +2 -2
  63. package/dist/src/storage/__tests__/sweep-stale-pending.test.js +2 -2
  64. package/dist/src/storage/index.d.ts +1 -1
  65. package/dist/src/storage/index.js +1 -1
  66. package/dist/src/storage/interaction-log.js +5 -5
  67. package/dist/src/storage/migrations.js +135 -123
  68. package/dist/src/storage/session-experience-store.js +4 -4
  69. package/dist/src/storage/sessions.js +43 -43
  70. package/dist/src/storage/transcript-response-entry.test.js +18 -5
  71. package/dist/src/storage/transcript.d.ts +2 -0
  72. package/dist/src/storage/transcript.js +223 -97
  73. package/dist/src/storage/usage.js +34 -17
  74. package/dist/src/storage/workspaces.js +12 -12
  75. package/dist/src/tools/native-tools.js +10 -1
  76. package/dist/src/tools/registry.d.ts +1 -0
  77. package/dist/src/tools/registry.js +30 -2
  78. package/dist/src/types/index.d.ts +1 -1
  79. package/dist/src/ui/app.js +0 -0
  80. package/dist/src/ui/slash/council-inspect.js +4 -4
  81. package/dist/src/ui/slash/expand.js +14 -1
  82. package/dist/src/ui/use-app-logic.js +0 -0
  83. package/dist/src/utils/__tests__/compaction-caps.test.js +9 -9
  84. package/dist/src/utils/__tests__/llm-deadline.test.js +16 -4
  85. package/dist/src/utils/__tests__/logger.test.d.ts +1 -0
  86. package/dist/src/utils/__tests__/logger.test.js +115 -0
  87. package/dist/src/utils/__tests__/settings-agent-first.test.d.ts +1 -0
  88. package/dist/src/utils/__tests__/settings-agent-first.test.js +48 -0
  89. package/dist/src/utils/clipboard-image.js +23 -23
  90. package/dist/src/utils/install-manager.d.ts +1 -0
  91. package/dist/src/utils/install-manager.js +76 -7
  92. package/dist/src/utils/install-manager.test.js +43 -1
  93. package/dist/src/utils/logger.d.ts +28 -0
  94. package/dist/src/utils/logger.js +171 -0
  95. package/dist/src/utils/settings.d.ts +9 -1
  96. package/dist/src/utils/settings.js +59 -22
  97. package/dist/src/utils/side-question.js +2 -2
  98. package/dist/src/utils/skills.js +3 -3
  99. package/dist/src/utils/update-checker.test.js +5 -1
  100. package/dist/src/verify/__tests__/coverage-parsers.test.js +30 -30
  101. package/package.json +1 -1
@@ -1,7 +1,15 @@
1
+ import { exec } from "child_process";
1
2
  import fs from "fs";
2
3
  import os from "os";
3
4
  import path from "path";
4
- import { afterEach, describe, expect, it } from "vitest";
5
+ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
6
+ vi.mock("child_process", async () => {
7
+ const actual = await vi.importActual("child_process");
8
+ return {
9
+ ...actual,
10
+ exec: vi.fn(),
11
+ };
12
+ });
5
13
  import { buildScriptUninstallPlan, getInstallMetadataPath, getReleaseTargetForPlatform, getScriptInstallContext, getScriptInstallDir, loadScriptInstallMetadata, parseChecksumsFile, saveScriptInstallMetadata, } from "./install-manager.js";
6
14
  let tempDirs = [];
7
15
  afterEach(() => {
@@ -124,4 +132,38 @@ describe("buildScriptUninstallPlan", () => {
124
132
  expect(plan?.removePaths).toContain(path.join(installDir, currentTarget.binaryName));
125
133
  });
126
134
  });
135
+ describe("runManagedUpdate", () => {
136
+ beforeEach(() => {
137
+ vi.stubGlobal("fetch", vi.fn());
138
+ });
139
+ afterEach(() => {
140
+ vi.restoreAllMocks();
141
+ });
142
+ it("handles dev-link update when newer version is available", async () => {
143
+ vi.mocked(exec).mockImplementation(((cmd, callback) => {
144
+ callback(null, "hash\trefs/tags/v2.0.0\n", "");
145
+ return {};
146
+ }));
147
+ const { runManagedUpdate } = await import("./install-manager.js");
148
+ const result = await runManagedUpdate("1.0.0");
149
+ expect(result.success).toBe(true);
150
+ expect(result.output).toContain("A new version of muonroi-cli is available!");
151
+ expect(result.output).toContain("Current version: v1.0.0");
152
+ expect(result.output).toContain("Latest version: v2.0.0");
153
+ expect(result.output).toContain("git -C");
154
+ expect(result.output).toContain("pull && bun install && bun run build");
155
+ });
156
+ it("handles dev-link when already up to date", async () => {
157
+ vi.mocked(exec).mockImplementation(((cmd, callback) => {
158
+ callback(null, "hash\trefs/tags/v1.0.0\n", "");
159
+ return {};
160
+ }));
161
+ const { runManagedUpdate } = await import("./install-manager.js");
162
+ const result = await runManagedUpdate("1.0.0");
163
+ expect(result.success).toBe(true);
164
+ expect(result.output).toContain("You are already up to date!");
165
+ expect(result.output).toContain("Current version: v1.0.0");
166
+ expect(result.output).toContain("Latest version: v1.0.0");
167
+ });
168
+ });
127
169
  //# sourceMappingURL=install-manager.test.js.map
@@ -0,0 +1,28 @@
1
+ export type LogLevel = "debug" | "info" | "warn" | "error";
2
+ export type LogNamespace = "cli" | "ui" | "orchestrator" | "storage" | "ee" | "mcp" | "pil" | "router";
3
+ export interface LogContext {
4
+ elapsedMs?: number;
5
+ error?: Error | unknown;
6
+ [key: string]: unknown;
7
+ }
8
+ /**
9
+ * Checks if the given log level is enabled based on the current process level weight.
10
+ */
11
+ export declare function isLogLevelEnabled(level: LogLevel): boolean;
12
+ /**
13
+ * Redacts common patterns of API keys and credential strings from log messages.
14
+ */
15
+ export declare function redactSecrets(str: string): string;
16
+ /**
17
+ * Recursively redacts sensitive fields from context objects.
18
+ */
19
+ export declare function redactObject(obj: unknown): unknown;
20
+ /**
21
+ * Structured unified logging system.
22
+ */
23
+ export declare const logger: {
24
+ debug(ns: LogNamespace, msg: string, ctx?: LogContext): void;
25
+ info(ns: LogNamespace, msg: string, ctx?: LogContext): void;
26
+ warn(ns: LogNamespace, msg: string, ctx?: LogContext): void;
27
+ error(ns: LogNamespace, msg: string, ctx?: LogContext): void;
28
+ };
@@ -0,0 +1,171 @@
1
+ import * as fs from "fs";
2
+ import * as path from "path";
3
+ import * as os from "os";
4
+ const LEVEL_WEIGHTS = {
5
+ debug: 0,
6
+ info: 1,
7
+ warn: 2,
8
+ error: 3,
9
+ };
10
+ // Default log level is 'info' unless explicitly configured.
11
+ const CURRENT_WEIGHT = (() => {
12
+ const envLevel = process.env.MUONROI_LOG_LEVEL?.toLowerCase();
13
+ if (envLevel === "debug")
14
+ return LEVEL_WEIGHTS.debug;
15
+ if (envLevel === "info")
16
+ return LEVEL_WEIGHTS.info;
17
+ if (envLevel === "warn")
18
+ return LEVEL_WEIGHTS.warn;
19
+ if (envLevel === "error")
20
+ return LEVEL_WEIGHTS.error;
21
+ return LEVEL_WEIGHTS.info;
22
+ })();
23
+ /**
24
+ * Checks if the given log level is enabled based on the current process level weight.
25
+ */
26
+ export function isLogLevelEnabled(level) {
27
+ return LEVEL_WEIGHTS[level] >= CURRENT_WEIGHT;
28
+ }
29
+ /**
30
+ * Redacts common patterns of API keys and credential strings from log messages.
31
+ */
32
+ export function redactSecrets(str) {
33
+ return str
34
+ .replace(/\bsk-[A-Za-z0-9-_]{20,}\b/g, "[REDACTED_API_KEY]")
35
+ .replace(/\bxai-[A-Za-z0-9-_]{20,}\b/g, "[REDACTED_API_KEY]")
36
+ .replace(/\bAIzaSy[A-Za-z0-9-_]{30,}\b/g, "[REDACTED_API_KEY]");
37
+ }
38
+ /**
39
+ * Recursively redacts sensitive fields from context objects.
40
+ */
41
+ export function redactObject(obj) {
42
+ if (obj === null || obj === undefined)
43
+ return obj;
44
+ if (typeof obj === "string") {
45
+ return redactSecrets(obj);
46
+ }
47
+ if (Array.isArray(obj)) {
48
+ return obj.map(redactObject);
49
+ }
50
+ if (typeof obj === "object") {
51
+ const res = {};
52
+ for (const [k, v] of Object.entries(obj)) {
53
+ const lowerK = k.toLowerCase();
54
+ if (lowerK.includes("key") ||
55
+ lowerK.includes("secret") ||
56
+ lowerK.includes("token") ||
57
+ lowerK.includes("password") ||
58
+ lowerK.includes("auth")) {
59
+ res[k] = "[REDACTED]";
60
+ }
61
+ else {
62
+ res[k] = redactObject(v);
63
+ }
64
+ }
65
+ return res;
66
+ }
67
+ return obj;
68
+ }
69
+ /**
70
+ * Returns true if the interactive TUI is active, preventing console writes.
71
+ */
72
+ function isTuiActive() {
73
+ try {
74
+ return globalThis.__muonroiTuiActive === true;
75
+ }
76
+ catch {
77
+ return false;
78
+ }
79
+ }
80
+ /**
81
+ * Writes logs safely to ~/.muonroi-cli/debug.log.
82
+ */
83
+ function appendToFile(level, ns, msg, ctx) {
84
+ try {
85
+ const dir = path.join(os.homedir(), ".muonroi-cli");
86
+ if (!fs.existsSync(dir)) {
87
+ fs.mkdirSync(dir, { recursive: true });
88
+ }
89
+ const logPath = path.join(dir, "debug.log");
90
+ const ts = new Date().toISOString();
91
+ const redactedCtx = ctx ? redactObject(ctx) : null;
92
+ const ctxStr = redactedCtx ? ` ${JSON.stringify(redactedCtx)}` : "";
93
+ const logLine = `[${ts}] [${level.toUpperCase()}] [${ns.toUpperCase()}] ${redactSecrets(msg)}${ctxStr}\n`;
94
+ fs.appendFileSync(logPath, logLine, "utf8");
95
+ }
96
+ catch {
97
+ /* fail-open */
98
+ }
99
+ }
100
+ /**
101
+ * Formats console log lines with colors.
102
+ */
103
+ function formatConsole(level, ns, msg, ctx) {
104
+ const ts = new Date().toISOString().split("T")[1].slice(0, -1); // HH:MM:SS.mmm
105
+ const levelStr = level.toUpperCase();
106
+ const nsStr = ns.toUpperCase();
107
+ const cleanMsg = redactSecrets(msg);
108
+ const redactedCtx = ctx ? redactObject(ctx) : null;
109
+ const ctxStr = redactedCtx ? ` ${JSON.stringify(redactedCtx)}` : "";
110
+ // Apply colors for developer convenience in terminal logs (non-TUI)
111
+ let color = "\x1b[0m"; // Reset
112
+ if (level === "debug")
113
+ color = "\x1b[90m"; // Gray
114
+ if (level === "info")
115
+ color = "\x1b[32m"; // Green
116
+ if (level === "warn")
117
+ color = "\x1b[33m"; // Yellow
118
+ if (level === "error")
119
+ color = "\x1b[31m"; // Red
120
+ return `${color}[${ts}] [${levelStr}] [${nsStr}] ${cleanMsg}${ctxStr}\x1b[0m`;
121
+ }
122
+ /**
123
+ * Structured unified logging system.
124
+ */
125
+ export const logger = {
126
+ debug(ns, msg, ctx) {
127
+ if (!isLogLevelEnabled("debug"))
128
+ return;
129
+ if (isTuiActive()) {
130
+ appendToFile("debug", ns, msg, ctx);
131
+ }
132
+ else {
133
+ // eslint-disable-next-line no-console
134
+ console.log(formatConsole("debug", ns, msg, ctx));
135
+ }
136
+ },
137
+ info(ns, msg, ctx) {
138
+ if (!isLogLevelEnabled("info"))
139
+ return;
140
+ if (isTuiActive()) {
141
+ appendToFile("info", ns, msg, ctx);
142
+ }
143
+ else {
144
+ // eslint-disable-next-line no-console
145
+ console.log(formatConsole("info", ns, msg, ctx));
146
+ }
147
+ },
148
+ warn(ns, msg, ctx) {
149
+ if (!isLogLevelEnabled("warn"))
150
+ return;
151
+ if (isTuiActive()) {
152
+ appendToFile("warn", ns, msg, ctx);
153
+ }
154
+ else {
155
+ // eslint-disable-next-line no-console
156
+ console.warn(formatConsole("warn", ns, msg, ctx));
157
+ }
158
+ },
159
+ error(ns, msg, ctx) {
160
+ if (!isLogLevelEnabled("error"))
161
+ return;
162
+ if (isTuiActive()) {
163
+ appendToFile("error", ns, msg, ctx);
164
+ }
165
+ else {
166
+ // eslint-disable-next-line no-console
167
+ console.error(formatConsole("error", ns, msg, ctx));
168
+ }
169
+ },
170
+ };
171
+ //# sourceMappingURL=logger.js.map
@@ -75,6 +75,14 @@ export interface UserSettings {
75
75
  * defaultModel field stays as the hard pin for legacy paths.
76
76
  */
77
77
  defaultProvider?: ProviderId;
78
+ /** When true, the agent prioritizes task completion over strict runaway safety caps. */
79
+ agentFirst?: boolean;
80
+ /** Custom soft limit on the number of tool execution steps before pausing. */
81
+ maxToolRounds?: number;
82
+ /** Custom hard limit on the number of tool execution steps per turn. */
83
+ hardMaxToolRounds?: number;
84
+ /** Custom limit on the number of LLM call round-trips allowed in a single turn. */
85
+ maxLlmCallsPerTurn?: number;
78
86
  /** Shell used by the bash tool. On Windows, defaults to Git Bash when present. */
79
87
  shell?: ShellSettings;
80
88
  lsp?: LspSettings;
@@ -343,7 +351,7 @@ export declare function getTopLevelCompactKeepLast(): number;
343
351
  * higher default so single-tool turns are unaffected. Env override:
344
352
  * MUONROI_TOP_LEVEL_TOOL_BUDGET_CHARS.
345
353
  */
346
- export declare function getTopLevelToolBudgetChars(): number;
354
+ export declare function getTopLevelToolBudgetChars(maxRounds?: number): number;
347
355
  export declare function getRoleModel(role: ModelRole): string | undefined;
348
356
  export declare function getRoleModels(): Partial<Record<ModelRole, string>>;
349
357
  export declare function getCouncilRounds(): number;
@@ -4,6 +4,7 @@ import * as path from "path";
4
4
  import { getEffectiveReasoningEffort, getFirstCatalogModel, getFirstCatalogProvider, getModelByTier, getModelIds, getModelInfo, MODELS, normalizeModelId, } from "../models/registry.js";
5
5
  import { apiBaseFor, PROVIDER_ENDPOINTS } from "../providers/endpoints.js";
6
6
  import { ALL_PROVIDER_IDS } from "../providers/types.js";
7
+ import { logger } from "./logger.js";
7
8
  import { normalizeShellSettings } from "./shell.js";
8
9
  export function getCatalogDefaultModel() {
9
10
  const provider = getDefaultProvider();
@@ -79,18 +80,58 @@ function ensureDir(dir) {
79
80
  }
80
81
  }
81
82
  function readJson(filePath) {
82
- try {
83
- if (!fs.existsSync(filePath))
84
- return null;
85
- return JSON.parse(fs.readFileSync(filePath, "utf-8"));
86
- }
87
- catch {
88
- return null;
83
+ const RETRIES = 5;
84
+ let lastErr;
85
+ for (let attempt = 0; attempt < RETRIES; attempt++) {
86
+ try {
87
+ if (!fs.existsSync(filePath))
88
+ return null;
89
+ return JSON.parse(fs.readFileSync(filePath, "utf-8"));
90
+ }
91
+ catch (err) {
92
+ lastErr = err;
93
+ const code = err?.code;
94
+ if (code === "EBUSY" || code === "EPERM" || err instanceof SyntaxError) {
95
+ logger.warn("cli", `Lock contention or syntax error reading ${path.basename(filePath)}, retrying (attempt ${attempt + 1}/${RETRIES})`, { error: err });
96
+ // Spin wait briefly to let the lock release or the write complete
97
+ const end = Date.now() + 20 * (attempt + 1);
98
+ while (Date.now() < end) {
99
+ // busy wait
100
+ }
101
+ }
102
+ else {
103
+ return null;
104
+ }
105
+ }
89
106
  }
107
+ return null;
90
108
  }
91
109
  function writeJson(filePath, data) {
92
110
  ensureDir(path.dirname(filePath));
93
- fs.writeFileSync(filePath, JSON.stringify(data, null, 2), { mode: 0o600 });
111
+ const serialized = JSON.stringify(data, null, 2);
112
+ const RETRIES = 5;
113
+ let lastErr;
114
+ for (let attempt = 0; attempt < RETRIES; attempt++) {
115
+ try {
116
+ fs.writeFileSync(filePath, serialized, { mode: 0o600 });
117
+ return;
118
+ }
119
+ catch (err) {
120
+ lastErr = err;
121
+ const code = err?.code;
122
+ if (code === "EBUSY" || code === "EPERM") {
123
+ logger.warn("cli", `Lock contention writing ${path.basename(filePath)}, retrying (attempt ${attempt + 1}/${RETRIES})`, { error: err });
124
+ const end = Date.now() + 20 * (attempt + 1);
125
+ while (Date.now() < end) {
126
+ // busy wait
127
+ }
128
+ }
129
+ else {
130
+ throw err;
131
+ }
132
+ }
133
+ }
134
+ throw lastErr;
94
135
  }
95
136
  /**
96
137
  * Ensure the CLI's own project-local footprint (`.muonroi-cli/`) is gitignored
@@ -559,13 +600,13 @@ export function getSubAgentBudgetChars() {
559
600
  const envRaw = process.env.MUONROI_SUB_AGENT_BUDGET_CHARS;
560
601
  if (envRaw) {
561
602
  const n = Number(envRaw);
562
- if (Number.isFinite(n) && n >= 20_000 && n <= 600_000)
603
+ if (Number.isFinite(n) && n >= 20_000 && n <= 5_000_000)
563
604
  return Math.floor(n);
564
605
  }
565
606
  const val = loadUserSettings().subAgentBudgetChars;
566
- if (typeof val === "number" && val >= 20_000 && val <= 600_000)
607
+ if (typeof val === "number" && val >= 20_000 && val <= 5_000_000)
567
608
  return Math.floor(val);
568
- return 120_000;
609
+ return 240_000;
569
610
  }
570
611
  /**
571
612
  * Stall watchdog timeout (ms) for streaming model calls: if the provider sends
@@ -694,24 +735,20 @@ export function getTopLevelCompactKeepLast() {
694
735
  * higher default so single-tool turns are unaffected. Env override:
695
736
  * MUONROI_TOP_LEVEL_TOOL_BUDGET_CHARS.
696
737
  */
697
- export function getTopLevelToolBudgetChars() {
738
+ export function getTopLevelToolBudgetChars(maxRounds) {
698
739
  const envRaw = process.env.MUONROI_TOP_LEVEL_TOOL_BUDGET_CHARS;
699
740
  if (envRaw) {
700
741
  const n = Number(envRaw);
701
- if (Number.isFinite(n) && n >= 50_000 && n <= 1_500_000)
742
+ if (Number.isFinite(n) && n >= 50_000 && n <= 10_000_000)
702
743
  return Math.floor(n);
703
744
  }
704
745
  const val = loadUserSettings().topLevelToolBudgetChars;
705
- if (typeof val === "number" && val >= 50_000 && val <= 1_500_000)
746
+ if (typeof val === "number" && val >= 50_000 && val <= 10_000_000)
706
747
  return Math.floor(val);
707
- // Phase C5 symmetry lowered from 400_000 to 200_000 chars. Evidence from
708
- // session f1eef338c784: top-level tool loop ran 49 turns consuming 3.4M
709
- // input tokens before the budget cap (set at 400K chars) meaningfully
710
- // constrained tool outputs. At 200K chars the 50% tier fires at 100K chars
711
- // (~3 tool turns), and the 80% tier at 160K chars (~5 turns); small tasks
712
- // (1-3 turns) are unaffected. The sub-agent budget is 120K, so 200K maintains
713
- // a ~1.7x ratio reflecting the broader top-level conversation context.
714
- return 200_000;
748
+ // Dynamically scale default based on maxRounds relative to default base (40)
749
+ const baseRounds = 40;
750
+ const scale = maxRounds && maxRounds > baseRounds ? maxRounds / baseRounds : 1;
751
+ return Math.floor(400_000 * scale);
715
752
  }
716
753
  export function getRoleModel(role) {
717
754
  return loadUserSettings().roleModels?.[role];
@@ -1,7 +1,7 @@
1
1
  import { generateText } from "ai";
2
2
  import { resolveModelRuntime } from "../providers/runtime.js";
3
- const SIDE_QUESTION_SYSTEM = `You are a helpful coding assistant answering a quick side question. The user is in the middle of a coding session and needs a fast, concise answer. Keep your response short and focused — this is a side question, not the main task.
4
-
3
+ const SIDE_QUESTION_SYSTEM = `You are a helpful coding assistant answering a quick side question. The user is in the middle of a coding session and needs a fast, concise answer. Keep your response short and focused — this is a side question, not the main task.
4
+
5
5
  If conversation context is provided below, use it to give a more relevant answer.`;
6
6
  export async function runSideQuestion(question, provider, modelId, conversationContext, signal) {
7
7
  const runtime = resolveModelRuntime(provider, modelId);
@@ -163,9 +163,9 @@ export function discoverSkills(projectRoot) {
163
163
  _skillsCache = { skills, cachedAt: now, cwd: projectRoot };
164
164
  return skills;
165
165
  }
166
- const SKILLS_INSTRUCTIONS = `AGENT SKILLS (optional):
167
- The following <available_skills> list specialized workflows. Use them when they might help the user's request — not only on exact keyword matches.
168
- If a skill's description fits the task or could improve consistency, read that skill's instructions first using read_file with the path from <location>, then follow the SKILL.md body.
166
+ const SKILLS_INSTRUCTIONS = `AGENT SKILLS (optional):
167
+ The following <available_skills> list specialized workflows. Use them when they might help the user's request — not only on exact keyword matches.
168
+ If a skill's description fits the task or could improve consistency, read that skill's instructions first using read_file with the path from <location>, then follow the SKILL.md body.
169
169
  Paths inside a skill (scripts/, references/, assets/) are relative to the skill directory (the folder containing SKILL.md); prefer absolute paths in tool calls.`;
170
170
  /** OpenCode-style XML catalog plus activation instructions for read_file. Returns null if no skills. */
171
171
  export function formatSkillsForPrompt(skills) {
@@ -23,7 +23,11 @@ describe("checkForUpdate", () => {
23
23
  expect(result.hasUpdate).toBe(true);
24
24
  expect(result.latestVersion).toBe("2.0.0");
25
25
  expect(result.currentVersion).toBe("1.0.0");
26
- expect(mockFetch).toHaveBeenCalledWith(RELEASE_URL, expect.objectContaining({ headers: { Accept: "application/vnd.github+json" } }));
26
+ expect(mockFetch).toHaveBeenCalledWith(RELEASE_URL, expect.objectContaining({
27
+ headers: expect.objectContaining({
28
+ Accept: "application/vnd.github+json",
29
+ }),
30
+ }));
27
31
  });
28
32
  it("returns hasUpdate=false when current version matches latest", async () => {
29
33
  vi.stubGlobal("fetch", vi.fn().mockResolvedValue({
@@ -1,38 +1,38 @@
1
1
  import { describe, expect, it } from "vitest";
2
2
  import { extractCoverageFromOutput, parseBunCoverage, parseJestCoverage, parsePytestCoverage, parseVitestCoverage, } from "../coverage-parsers.js";
3
- const BUN_OUTPUT = `
4
- [0.12ms] 11 tests passed
5
- [0.00ms] 0 tests failed
6
-
7
- -----------------------|---------|---------|---------|---------|-----------------------
8
- File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
9
- -----------------------|---------|---------|---------|---------|-----------------------
10
- All files | 85.50 | 70.00 | 90.00 | 85.50 |
11
- index.ts | 85.50 | 70.00 | 90.00 | 85.50 | 10-15
12
- -----------------------|---------|---------|---------|---------|-----------------------
3
+ const BUN_OUTPUT = `
4
+ [0.12ms] 11 tests passed
5
+ [0.00ms] 0 tests failed
6
+
7
+ -----------------------|---------|---------|---------|---------|-----------------------
8
+ File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
9
+ -----------------------|---------|---------|---------|---------|-----------------------
10
+ All files | 85.50 | 70.00 | 90.00 | 85.50 |
11
+ index.ts | 85.50 | 70.00 | 90.00 | 85.50 | 10-15
12
+ -----------------------|---------|---------|---------|---------|-----------------------
13
13
  `;
14
- const VITEST_OUTPUT = `
15
- Files | % Stmts | % Branch | % Funcs | % Lines | Uncovered Lines
16
- --------------------------|---------|---------|---------|---------|-----------------
17
- All files | 92.31 | 85.71 | 100 | 92.31 |
18
- reality-anchor.ts | 100 | 100 | 100 | 100 |
19
- verify-result.ts | 83.33 | 75 | 100 | 83.33 | 12-15
20
- --------------------------|---------|---------|---------|---------|-----------------
14
+ const VITEST_OUTPUT = `
15
+ Files | % Stmts | % Branch | % Funcs | % Lines | Uncovered Lines
16
+ --------------------------|---------|---------|---------|---------|-----------------
17
+ All files | 92.31 | 85.71 | 100 | 92.31 |
18
+ reality-anchor.ts | 100 | 100 | 100 | 100 |
19
+ verify-result.ts | 83.33 | 75 | 100 | 83.33 | 12-15
20
+ --------------------------|---------|---------|---------|---------|-----------------
21
21
  `;
22
- const JEST_OUTPUT = `
23
- ----------|---------|----------|---------|---------|-------------------
24
- File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
25
- ----------|---------|----------|---------|---------|-------------------
26
- All files | 75.42 | 62.15 | 80.52 | 75.42 |
27
- ----------|---------|----------|---------|---------|-------------------
22
+ const JEST_OUTPUT = `
23
+ ----------|---------|----------|---------|---------|-------------------
24
+ File | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s
25
+ ----------|---------|----------|---------|---------|-------------------
26
+ All files | 75.42 | 62.15 | 80.52 | 75.42 |
27
+ ----------|---------|----------|---------|---------|-------------------
28
28
  `;
29
- const PYTEST_OUTPUT = `
30
- Name Stmts Miss Cover
31
- ---------------------------------------------
32
- muonroi/__init__.py 0 0 100%
33
- muonroi/cli.py 42 8 81%
34
- ---------------------------------------------
35
- TOTAL 42 8 81%
29
+ const PYTEST_OUTPUT = `
30
+ Name Stmts Miss Cover
31
+ ---------------------------------------------
32
+ muonroi/__init__.py 0 0 100%
33
+ muonroi/cli.py 42 8 81%
34
+ ---------------------------------------------
35
+ TOTAL 42 8 81%
36
36
  `;
37
37
  describe("Coverage Parsers", () => {
38
38
  it("should parse bun coverage", () => {
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "workspaces": [
4
4
  "packages/*"
5
5
  ],
6
- "version": "1.8.0",
6
+ "version": "1.8.1",
7
7
  "description": "BYOK AI coding agent with multi-model council debate, role-based routing, and auto-compact.",
8
8
  "repository": {
9
9
  "type": "git",