mixdog 0.9.0 → 0.9.2

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 (240) hide show
  1. package/package.json +10 -3
  2. package/scripts/_bench-cwc.json +20 -0
  3. package/scripts/agent-loop-policy-test.mjs +37 -0
  4. package/scripts/agent-parallel-smoke.mjs +54 -10
  5. package/scripts/background-task-meta-smoke.mjs +1 -1
  6. package/scripts/bench-run.mjs +262 -0
  7. package/scripts/compact-smoke.mjs +12 -0
  8. package/scripts/compact-trigger-migration-smoke.mjs +67 -1
  9. package/scripts/ingest-pure-conversation-smoke.mjs +148 -0
  10. package/scripts/internal-comms-bench.mjs +727 -0
  11. package/scripts/internal-comms-smoke.mjs +75 -0
  12. package/scripts/lead-workflow-smoke.mjs +4 -4
  13. package/scripts/live-worker-smoke.mjs +9 -9
  14. package/scripts/output-style-bench.mjs +285 -0
  15. package/scripts/output-style-smoke.mjs +13 -10
  16. package/scripts/patch-replay.mjs +90 -0
  17. package/scripts/provider-stream-stall-test.mjs +276 -0
  18. package/scripts/provider-toolcall-test.mjs +599 -1
  19. package/scripts/routing-corpus.mjs +281 -0
  20. package/scripts/session-bench.mjs +1526 -0
  21. package/scripts/session-diag.mjs +595 -0
  22. package/scripts/session-ingest-smoke.mjs +2 -2
  23. package/scripts/task-bench.mjs +207 -0
  24. package/scripts/tool-failures.mjs +6 -6
  25. package/scripts/tool-smoke.mjs +306 -66
  26. package/scripts/toolcall-args-test.mjs +81 -0
  27. package/src/agents/debugger/AGENT.md +4 -4
  28. package/src/agents/heavy-worker/AGENT.md +4 -2
  29. package/src/agents/reviewer/AGENT.md +4 -4
  30. package/src/agents/worker/AGENT.md +4 -2
  31. package/src/app.mjs +10 -6
  32. package/src/defaults/{hidden-roles.json → agents.json} +7 -7
  33. package/src/examples/schedules/SCHEDULE.example.md +32 -0
  34. package/src/examples/webhooks/WEBHOOK.example.md +40 -0
  35. package/src/headless-role.mjs +14 -14
  36. package/src/help.mjs +1 -0
  37. package/src/lib/mixdog-debug.cjs +0 -22
  38. package/src/lib/plugin-paths.cjs +1 -7
  39. package/src/lib/rules-builder.cjs +34 -56
  40. package/src/mixdog-session-runtime.mjs +710 -319
  41. package/src/output-styles/default.md +12 -7
  42. package/src/output-styles/minimal.md +25 -0
  43. package/src/output-styles/oneline.md +21 -0
  44. package/src/output-styles/simple.md +10 -9
  45. package/src/repl.mjs +12 -4
  46. package/src/rules/agent/00-common.md +7 -5
  47. package/src/rules/agent/30-explorer.md +7 -8
  48. package/src/rules/lead/01-general.md +3 -1
  49. package/src/rules/lead/lead-tool.md +7 -0
  50. package/src/rules/shared/01-tool.md +17 -12
  51. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +90 -32
  52. package/src/runtime/agent/orchestrator/agent-runtime/agent-loop-policy.mjs +32 -0
  53. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +18 -6
  54. package/src/runtime/agent/orchestrator/agent-runtime/cache-strategy.mjs +23 -20
  55. package/src/runtime/agent/orchestrator/agent-runtime/session-builder.mjs +48 -14
  56. package/src/runtime/agent/orchestrator/agent-trace.mjs +87 -12
  57. package/src/runtime/agent/orchestrator/config.mjs +3 -0
  58. package/src/runtime/agent/orchestrator/context/collect.mjs +131 -67
  59. package/src/runtime/agent/orchestrator/{internal-roles.mjs → internal-agents.mjs} +72 -72
  60. package/src/runtime/agent/orchestrator/internal-tools.mjs +13 -26
  61. package/src/runtime/agent/orchestrator/mcp/client.mjs +94 -16
  62. package/src/runtime/agent/orchestrator/providers/anthropic-betas.mjs +7 -0
  63. package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +188 -0
  64. package/src/runtime/agent/orchestrator/providers/anthropic-leaked-toolcall.mjs +444 -0
  65. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +359 -106
  66. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +63 -51
  67. package/src/runtime/agent/orchestrator/providers/api-usage.mjs +27 -20
  68. package/src/runtime/agent/orchestrator/providers/gemini.mjs +184 -17
  69. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +8 -1
  70. package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +18 -8
  71. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +210 -21
  72. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +86 -30
  73. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +254 -280
  74. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +191 -50
  75. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +18 -0
  76. package/src/runtime/agent/orchestrator/providers/opencode-go-usage.mjs +11 -5
  77. package/src/runtime/agent/orchestrator/providers/registry.mjs +2 -1
  78. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +265 -1
  79. package/src/runtime/agent/orchestrator/session/compact.mjs +560 -51
  80. package/src/runtime/agent/orchestrator/session/context-utils.mjs +250 -3
  81. package/src/runtime/agent/orchestrator/session/loop.mjs +394 -132
  82. package/src/runtime/agent/orchestrator/session/manager.mjs +217 -170
  83. package/src/runtime/agent/orchestrator/session/store.mjs +4 -4
  84. package/src/runtime/agent/orchestrator/session/tool-envelope.mjs +61 -0
  85. package/src/runtime/agent/orchestrator/session/tool-result-offload.mjs +5 -0
  86. package/src/runtime/agent/orchestrator/stall-policy.mjs +63 -15
  87. package/src/runtime/agent/orchestrator/tools/bash-session.mjs +1 -1
  88. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +194 -32
  89. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.test.mjs +143 -0
  90. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +1 -44
  91. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +34 -18
  92. package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +0 -0
  93. package/src/runtime/agent/orchestrator/tools/builtin/list-formatting.mjs +10 -0
  94. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +5 -4
  95. package/src/runtime/agent/orchestrator/tools/builtin/path-utils.mjs +15 -0
  96. package/src/runtime/agent/orchestrator/tools/builtin/read-args.mjs +9 -44
  97. package/src/runtime/agent/orchestrator/tools/builtin/read-constants.mjs +2 -1
  98. package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +13 -4
  99. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +10 -17
  100. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +18 -2
  101. package/src/runtime/agent/orchestrator/tools/builtin/shell-output.mjs +3 -2
  102. package/src/runtime/agent/orchestrator/tools/builtin/tool-output-limit.mjs +10 -0
  103. package/src/runtime/agent/orchestrator/tools/builtin.mjs +59 -1
  104. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +5 -5
  105. package/src/runtime/agent/orchestrator/tools/code-graph.mjs +4076 -3985
  106. package/src/runtime/agent/orchestrator/tools/patch.mjs +116 -2
  107. package/src/runtime/channels/backends/discord.mjs +99 -9
  108. package/src/runtime/channels/backends/telegram.mjs +501 -0
  109. package/src/runtime/channels/index.mjs +441 -1254
  110. package/src/runtime/channels/lib/cli-worker-host.mjs +1 -8
  111. package/src/runtime/channels/lib/config.mjs +54 -3
  112. package/src/runtime/channels/lib/drop-trace.mjs +1 -1
  113. package/src/runtime/channels/lib/executor.mjs +0 -3
  114. package/src/runtime/channels/lib/format.mjs +4 -2
  115. package/src/runtime/channels/lib/memory-client.mjs +0 -38
  116. package/src/runtime/channels/lib/output-forwarder.mjs +77 -71
  117. package/src/runtime/channels/lib/runtime-paths.mjs +29 -6
  118. package/src/runtime/channels/lib/scheduler.mjs +1 -1
  119. package/src/runtime/channels/lib/session-discovery.mjs +0 -4
  120. package/src/runtime/channels/lib/telegram-format.mjs +283 -0
  121. package/src/runtime/channels/lib/tool-format.mjs +1 -2
  122. package/src/runtime/channels/lib/transcript-discovery.mjs +20 -11
  123. package/src/runtime/channels/lib/webhook.mjs +59 -31
  124. package/src/runtime/channels/tool-defs.mjs +1 -1
  125. package/src/runtime/lib/keychain-cjs.cjs +0 -1
  126. package/src/runtime/memory/data/runtime-manifest.json +6 -7
  127. package/src/runtime/memory/index.mjs +187 -43
  128. package/src/runtime/memory/lib/agent-ipc.mjs +2 -2
  129. package/src/runtime/memory/lib/core-memory-store.mjs +1 -1
  130. package/src/runtime/memory/lib/llm-worker-host.mjs +0 -4
  131. package/src/runtime/memory/lib/memory-cycle1.mjs +1 -1
  132. package/src/runtime/memory/lib/memory-cycle2.mjs +9 -6
  133. package/src/runtime/memory/lib/memory-cycle3.mjs +1 -1
  134. package/src/runtime/memory/lib/memory-ops-policy.mjs +0 -1
  135. package/src/runtime/memory/lib/memory.mjs +101 -4
  136. package/src/runtime/memory/lib/pg/adapter.mjs +139 -15
  137. package/src/runtime/memory/lib/runtime-fetcher.mjs +43 -18
  138. package/src/runtime/memory/lib/session-ingest.mjs +116 -7
  139. package/src/runtime/memory/lib/trace-store.mjs +69 -22
  140. package/src/runtime/memory/tool-defs.mjs +6 -3
  141. package/src/runtime/search/index.mjs +2 -7
  142. package/src/runtime/search/lib/config.mjs +0 -4
  143. package/src/runtime/search/lib/state.mjs +1 -15
  144. package/src/runtime/search/lib/web-tools.mjs +0 -1
  145. package/src/runtime/shared/channel-notification-routing.mjs +12 -0
  146. package/src/runtime/shared/channel-notification-routing.test.mjs +45 -0
  147. package/src/runtime/shared/child-spawn-gate.mjs +0 -6
  148. package/src/runtime/shared/config.mjs +9 -0
  149. package/src/runtime/shared/llm/http-agent.mjs +12 -5
  150. package/src/runtime/shared/schedules-store.mjs +21 -19
  151. package/src/runtime/shared/tool-surface.mjs +98 -13
  152. package/src/runtime/shared/transcript-writer.mjs +129 -0
  153. package/src/runtime/shared/update-checker.mjs +214 -0
  154. package/src/standalone/agent-tool.mjs +255 -109
  155. package/src/standalone/channel-admin.mjs +133 -40
  156. package/src/standalone/channel-worker.mjs +8 -291
  157. package/src/standalone/explore-tool.mjs +2 -2
  158. package/src/standalone/memory-runtime-proxy.mjs +3 -1
  159. package/src/standalone/provider-admin.mjs +11 -0
  160. package/src/standalone/seeds.mjs +1 -11
  161. package/src/standalone/usage-dashboard.mjs +1 -1
  162. package/src/tui/App.jsx +2137 -750
  163. package/src/tui/components/ConfirmBar.jsx +47 -0
  164. package/src/tui/components/ContextPanel.jsx +5 -3
  165. package/src/tui/components/ItemRightHintOverprint.jsx +54 -0
  166. package/src/tui/components/Markdown.jsx +22 -98
  167. package/src/tui/components/Message.jsx +14 -35
  168. package/src/tui/components/Picker.jsx +87 -12
  169. package/src/tui/components/PromptInput.jsx +146 -9
  170. package/src/tui/components/QueuedCommands.jsx +1 -1
  171. package/src/tui/components/SlashCommandPalette.jsx +8 -5
  172. package/src/tui/components/Spinner.jsx +7 -7
  173. package/src/tui/components/StatusLine.jsx +40 -21
  174. package/src/tui/components/TextEntryPanel.jsx +51 -7
  175. package/src/tui/components/ToolExecution.jsx +177 -100
  176. package/src/tui/components/TurnDone.jsx +4 -4
  177. package/src/tui/components/UsagePanel.jsx +1 -1
  178. package/src/tui/components/tool-output-format.mjs +312 -40
  179. package/src/tui/components/tool-output-format.test.mjs +180 -1
  180. package/src/tui/display-width.mjs +69 -0
  181. package/src/tui/display-width.test.mjs +35 -0
  182. package/src/tui/dist/index.mjs +7324 -2393
  183. package/src/tui/engine.mjs +287 -126
  184. package/src/tui/index.jsx +117 -7
  185. package/src/tui/keyboard-protocol.mjs +42 -0
  186. package/src/tui/lib/voice-recorder.mjs +453 -0
  187. package/src/tui/markdown/format-token.mjs +354 -142
  188. package/src/tui/markdown/format-token.test.mjs +155 -17
  189. package/src/tui/markdown/measure-rendered-rows.mjs +85 -0
  190. package/src/tui/markdown/render-ansi.test.mjs +1 -1
  191. package/src/tui/markdown/streaming-markdown.mjs +167 -0
  192. package/src/tui/markdown/streaming-markdown.test.mjs +70 -0
  193. package/src/tui/markdown/table-layout.mjs +9 -9
  194. package/src/tui/paste-attachments.mjs +0 -11
  195. package/src/tui/prompt-history-store.mjs +129 -0
  196. package/src/tui/prompt-history-store.test.mjs +52 -0
  197. package/src/tui/statusline-ansi-bridge.test.mjs +3 -3
  198. package/src/tui/theme.mjs +41 -647
  199. package/src/tui/themes/base.mjs +86 -0
  200. package/src/tui/themes/basic.mjs +85 -0
  201. package/src/tui/themes/catppuccin.mjs +72 -0
  202. package/src/tui/themes/dracula.mjs +70 -0
  203. package/src/tui/themes/everforest.mjs +71 -0
  204. package/src/tui/themes/gruvbox.mjs +71 -0
  205. package/src/tui/themes/index.mjs +71 -0
  206. package/src/tui/themes/indigo.mjs +78 -0
  207. package/src/tui/themes/kanagawa.mjs +80 -0
  208. package/src/tui/themes/light.mjs +81 -0
  209. package/src/tui/themes/nord.mjs +72 -0
  210. package/src/tui/themes/onedark.mjs +16 -0
  211. package/src/tui/themes/rosepine.mjs +70 -0
  212. package/src/tui/themes/teal.mjs +81 -0
  213. package/src/tui/themes/tokyonight.mjs +79 -0
  214. package/src/tui/themes/utils.mjs +106 -0
  215. package/src/tui/themes/warm.mjs +79 -0
  216. package/src/tui/transcript-tool-failures.mjs +13 -2
  217. package/src/ui/markdown.mjs +1 -1
  218. package/src/ui/model-display.mjs +2 -2
  219. package/src/ui/statusline.mjs +26 -27
  220. package/src/vendor/statusline/bin/statusline-lib.mjs +0 -623
  221. package/src/vendor/statusline/bin/statusline-route.mjs +5 -12
  222. package/src/vendor/statusline/src/gateway/claude-current.mjs +3 -3
  223. package/src/vendor/statusline/src/gateway/route-meta.mjs +30 -16
  224. package/src/workflows/default/WORKFLOW.md +39 -12
  225. package/src/workflows/sequential/WORKFLOW.md +46 -0
  226. package/src/workflows/solo/WORKFLOW.md +7 -0
  227. package/vendor/ink/build/display-width.js +62 -0
  228. package/vendor/ink/build/ink.js +154 -20
  229. package/vendor/ink/build/measure-text.js +4 -1
  230. package/vendor/ink/build/output.js +115 -9
  231. package/vendor/ink/build/render-node-to-output.js +4 -1
  232. package/vendor/ink/build/render.js +4 -0
  233. package/src/hooks/lib/permission-rules.cjs +0 -170
  234. package/src/hooks/lib/settings-loader.cjs +0 -112
  235. package/src/lib/hook-pipe-path.cjs +0 -10
  236. package/src/output-styles/extreme-simple.md +0 -20
  237. package/src/rules/lead/04-workflow.md +0 -51
  238. package/src/runtime/channels/lib/hook-pipe-server.mjs +0 -671
  239. package/src/workflows/default/workflow.json +0 -13
  240. package/src/workflows/solo/workflow.json +0 -7
@@ -0,0 +1,75 @@
1
+ #!/usr/bin/env node
2
+ // Static smoke for internal-comms token-optimization rules (min chars / max
3
+ // info). Asserts the Lead brief contract and the agent handoff contract are
4
+ // present and injected, without any model call. Live token A/B is a separate
5
+ // bench (scripts/internal-comms-bench.mjs).
6
+ import { createRequire } from 'node:module';
7
+ import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from 'node:fs';
8
+ import { tmpdir } from 'node:os';
9
+ import { dirname, join, resolve } from 'node:path';
10
+ import { fileURLToPath } from 'node:url';
11
+
12
+ const root = resolve(dirname(fileURLToPath(import.meta.url)), '..');
13
+ const require = createRequire(import.meta.url);
14
+ const rulesBuilder = require('../src/lib/rules-builder.cjs');
15
+
16
+ function assert(condition, message) {
17
+ if (!condition) throw new Error(message);
18
+ }
19
+ function readSrc(...parts) {
20
+ return readFileSync(join(root, 'src', ...parts), 'utf8');
21
+ }
22
+ // Rules wrap across lines; collapse whitespace so phrase asserts are line-agnostic.
23
+ function flat(text) {
24
+ return String(text || '').replace(/\s+/g, ' ');
25
+ }
26
+
27
+ // --- Lead brief contract: canonical in lead-tool.md, referenced from WORKFLOW -
28
+ const workflow = readSrc('workflows', 'default', 'WORKFLOW.md');
29
+ const leadTool = readSrc('rules', 'lead', 'lead-tool.md');
30
+ const BRIEF_FIELDS = ['Goal:', 'Anchors:', 'Allow/Forbid:', 'Deliver:', 'Verify:'];
31
+ // Canonical brief contract lives in lead-tool.md (Lead tool-use rules).
32
+ assert(/minimum characters, maximum information/i.test(flat(leadTool)), 'lead-tool.md: brief must state min-char/max-info principle');
33
+ for (const field of BRIEF_FIELDS) assert(leadTool.includes(field), `lead-tool.md: brief missing labeled field ${field}`);
34
+ assert(leadTool.includes('Stop:'), 'lead-tool.md: brief must add Stop: for heavy-worker bound');
35
+ assert(/role-known|already (?:owns|knows)|wasted cost|wasted/i.test(flat(leadTool)), 'lead-tool.md: brief must ban restating known rules/background as cost');
36
+ // WORKFLOW.md must not duplicate the field list; it defers to the lead-tool contract.
37
+ assert(/lead-tool brief contract/i.test(flat(workflow)), 'WORKFLOW.md: must defer to the lead-tool brief contract');
38
+ assert(!BRIEF_FIELDS.every((field) => workflow.includes(field)), 'WORKFLOW.md: must not duplicate the full brief field list');
39
+
40
+ // --- Agent handoff contract (00-common.md) ---------------------------------
41
+ const common = readSrc('rules', 'agent', '00-common.md');
42
+ assert(/minimum characters, maximum information/i.test(flat(common)), '00-common: handoff must state token-optimized principle');
43
+ assert(/fragments/i.test(common), '00-common: handoff must require fragments');
44
+ assert(/file:line/i.test(common), '00-common: handoff must anchor evidence to file:line');
45
+ assert(/Banned as pure cost/i.test(flat(common)), '00-common: handoff must list banned cost items');
46
+ for (const banned of ['headings', 'tables', 'narration', 'raw logs', 'next-checks']) {
47
+ assert(common.toLowerCase().includes(banned), `00-common: banned list missing ${banned}`);
48
+ }
49
+
50
+ // --- Per-role output contracts --------------------------------------------
51
+ const roles = {
52
+ 'worker/AGENT.md': readSrc('agents', 'worker', 'AGENT.md'),
53
+ 'heavy-worker/AGENT.md': readSrc('agents', 'heavy-worker', 'AGENT.md'),
54
+ 'reviewer/AGENT.md': readSrc('agents', 'reviewer', 'AGENT.md'),
55
+ 'debugger/AGENT.md': readSrc('agents', 'debugger', 'AGENT.md'),
56
+ };
57
+ for (const [name, text] of Object.entries(roles)) {
58
+ assert(/file:line/i.test(text), `${name}: role output must anchor to file:line`);
59
+ assert(/fragments|no report bloat|no prose|no narration|no preamble/i.test(flat(text)), `${name}: role output must forbid prose bloat`);
60
+ }
61
+ assert(/severity-ordered/i.test(flat(roles['reviewer/AGENT.md'])), 'reviewer: must keep severity-ordered findings');
62
+ assert(/confirmed[\s\S]*inferences/i.test(roles['debugger/AGENT.md']), 'debugger: must separate confirmed facts vs inferences');
63
+ assert(/Stop:|how-to-verify|how to verify/i.test(flat(roles['heavy-worker/AGENT.md'])), 'heavy-worker: must bound scope / state how to verify');
64
+
65
+ // --- Injection: Lead rules actually carry the brief contract ---------------
66
+ const dataDir = mkdtempSync(join(tmpdir(), 'mixdog-internal-comms-smoke-'));
67
+ try {
68
+ const leadRules = rulesBuilder.buildInjectionContent({ PLUGIN_ROOT: join(root, 'src'), DATA_DIR: dataDir });
69
+ assert(/minimum characters, maximum information/i.test(flat(leadRules)), 'injected Lead rules must carry the brief token principle');
70
+ for (const field of BRIEF_FIELDS) assert(leadRules.includes(field), `injected Lead rules missing brief field ${field}`);
71
+ } finally {
72
+ rmSync(dataDir, { recursive: true, force: true });
73
+ }
74
+
75
+ process.stdout.write('internal comms smoke passed\n');
@@ -315,7 +315,7 @@ function agentSummary(call) {
315
315
  id: call?.id || call?.callId || null,
316
316
  name: call?.name || call?.toolName || 'tool',
317
317
  type: input?.type || input?.action || null,
318
- role: input?.role || null,
318
+ agent: input?.agent || null,
319
319
  tag: input?.tag || null,
320
320
  mode: input?.mode || null,
321
321
  wait: input?.wait ?? null,
@@ -491,15 +491,15 @@ async function runScenario(name) {
491
491
  .filter(Number.isFinite);
492
492
  const firstMutationIter = mutationIters.length ? Math.min(...mutationIters) : null;
493
493
  const implementationSpawnCalls = spawnCalls.filter((call) => {
494
- const role = String(call.role || '').toLowerCase();
494
+ const agent = String(call.agent || '').toLowerCase();
495
495
  const tag = String(call.tag || '').toLowerCase();
496
- if (!['worker', 'heavy-worker', 'debugger'].includes(role)) return false;
496
+ if (!['worker', 'heavy-worker', 'debugger'].includes(agent)) return false;
497
497
  return !/(verify|smoke|review|policy)/.test(tag);
498
498
  });
499
499
  const preEditImplementationSpawns = firstMutationIter == null
500
500
  ? implementationSpawnCalls.length
501
501
  : implementationSpawnCalls.filter((call) => Number(call.iter) < firstMutationIter).length;
502
- const distinctRoles = [...new Set(agentCalls.map((call) => call.role).filter(Boolean))];
502
+ const distinctRoles = [...new Set(agentCalls.map((call) => call.agent).filter(Boolean))];
503
503
  const distinctTags = [...new Set(agentCalls.map((call) => call.tag).filter(Boolean))];
504
504
  const firstAgentIter = Math.min(...agentCalls.map((call) => Number(call.iter)).filter(Number.isFinite));
505
505
  const firstIterAgentCount = Number.isFinite(firstAgentIter)
@@ -36,7 +36,7 @@ async function main() {
36
36
  const leadToolRules = readFileSync('src/rules/lead/lead-tool.md', 'utf8');
37
37
  const workflowRules = readFileSync('src/workflows/default/WORKFLOW.md', 'utf8');
38
38
  assert(/Use `agent` for scoped implementation/i.test(leadToolRules), 'lead rules must direct scoped work to agents');
39
- assert(/delegat(?:es|ing) them concurrently/i.test(workflowRules), 'workflow rules must keep independent work parallel');
39
+ assert(/PARALLEL across independent scopes/i.test(workflowRules), 'workflow rules must keep independent work parallel');
40
40
  assert(/always start background tasks/i.test(AGENT_TOOL.description || '') && /distinct tags/i.test(AGENT_TOOL.description || '') && /completion notification/i.test(AGENT_TOOL.description || ''), 'agent tool description must expose async parallel tags');
41
41
 
42
42
  mkdirSync(dataDir, { recursive: true });
@@ -107,7 +107,7 @@ async function main() {
107
107
  if (/debug/i.test(prompt)) {
108
108
  return { content: `debugger ${readFileSync(join(cwdOverride, 'notes.txt'), 'utf8').trim()}` };
109
109
  }
110
- return { content: `ack ${session?.role || 'worker'}` };
110
+ return { content: `ack ${session?.agent || 'worker'}` };
111
111
  } finally {
112
112
  activeAsks -= 1;
113
113
  if (session) {
@@ -152,7 +152,7 @@ async function main() {
152
152
 
153
153
  const spawnOut = await agentRunner.execute({
154
154
  type: 'spawn',
155
- role: 'worker',
155
+ agent: 'worker',
156
156
  tag: 'impl1',
157
157
  cwd: root,
158
158
  prompt: 'write implementation: update feature.txt with apply_patch',
@@ -169,14 +169,14 @@ async function main() {
169
169
 
170
170
  const reviewOut = await agentRunner.execute({
171
171
  type: 'spawn',
172
- role: 'reviewer',
172
+ agent: 'reviewer',
173
173
  tag: 'rev1',
174
174
  cwd: root,
175
175
  prompt: 'review feature.txt for the worker change',
176
176
  }, { invocationSource: 'model-tool', cwd: root });
177
177
  const debugOut = await agentRunner.execute({
178
178
  type: 'spawn',
179
- role: 'debugger',
179
+ agent: 'debugger',
180
180
  tag: 'dbg1',
181
181
  cwd: root,
182
182
  prompt: 'debug notes.txt timeout clue',
@@ -187,21 +187,21 @@ async function main() {
187
187
  const parallelSpawns = await Promise.all([
188
188
  agentRunner.execute({
189
189
  type: 'spawn',
190
- role: 'worker',
190
+ agent: 'worker',
191
191
  tag: 'parWorker',
192
192
  cwd: root,
193
193
  prompt: 'parallel slow worker task',
194
194
  }, { invocationSource: 'model-tool', cwd: root }),
195
195
  agentRunner.execute({
196
196
  type: 'spawn',
197
- role: 'reviewer',
197
+ agent: 'reviewer',
198
198
  tag: 'parReviewer',
199
199
  cwd: root,
200
200
  prompt: 'parallel slow review task',
201
201
  }, { invocationSource: 'model-tool', cwd: root }),
202
202
  agentRunner.execute({
203
203
  type: 'spawn',
204
- role: 'debugger',
204
+ agent: 'debugger',
205
205
  tag: 'parDebugger',
206
206
  cwd: root,
207
207
  prompt: 'parallel slow debug task',
@@ -217,7 +217,7 @@ async function main() {
217
217
 
218
218
  const busyOut = await agentRunner.execute({
219
219
  type: 'spawn',
220
- role: 'worker',
220
+ agent: 'worker',
221
221
  tag: 'busy1',
222
222
  cwd: root,
223
223
  prompt: 'long busy worker task',
@@ -0,0 +1,285 @@
1
+ #!/usr/bin/env node
2
+ // Lead output-style verbosity bench (default >= simple >= minimal >= oneline).
3
+ import { createRequire } from 'node:module';
4
+ import { execFileSync } from 'node:child_process';
5
+ import {
6
+ copyFileSync,
7
+ existsSync,
8
+ mkdirSync,
9
+ mkdtempSync,
10
+ readdirSync,
11
+ readFileSync,
12
+ rmSync,
13
+ writeFileSync,
14
+ } from 'node:fs';
15
+ import { homedir } from 'node:os';
16
+ import { dirname, join, resolve } from 'node:path';
17
+ import { fileURLToPath, pathToFileURL } from 'node:url';
18
+
19
+ const __dir = dirname(fileURLToPath(import.meta.url));
20
+ const REPO_ROOT = resolve(__dir, '..');
21
+ const PLUGIN_ROOT = join(REPO_ROOT, 'src');
22
+ const STYLES = ['default', 'simple', 'minimal', 'oneline'];
23
+ const DEFAULT_PROMPT =
24
+ 'Reply in plain English only. What is 2+2? Give a short summary suitable for a status report (no tools, no file reads).';
25
+ const MODEL_ALIASES = {
26
+ opus: { provider: 'anthropic-oauth', model: 'claude-opus-4-8' },
27
+ sonnet: { provider: 'anthropic-oauth', model: 'claude-sonnet-5' },
28
+ gpt: { provider: 'openai-oauth', model: 'gpt-5.5' },
29
+ 'gpt-5.5': { provider: 'openai-oauth', model: 'gpt-5.5' },
30
+ grok: { provider: 'grok-oauth', model: 'grok-composer-2.5-fast' },
31
+ };
32
+ // Filenames verified in provider ensureAuth / token paths (resolvePluginData / getPluginData).
33
+ const AUTH_ARTIFACT_BY_PROVIDER = {
34
+ 'grok-oauth': ['grok-oauth.json', 'grok-oauth-models.json'],
35
+ 'anthropic-oauth': ['anthropic-oauth-credentials.json', 'anthropic-oauth-models.json'],
36
+ 'openai-oauth': ['openai-oauth.json', 'openai-oauth-models.json'],
37
+ };
38
+
39
+ function argValue(name, fallback = null) {
40
+ const idx = process.argv.indexOf(name);
41
+ if (idx >= 0 && idx + 1 < process.argv.length) return process.argv[idx + 1];
42
+ const pref = `${name}=`;
43
+ const hit = process.argv.find((a) => a.startsWith(pref));
44
+ return hit ? hit.slice(pref.length) : fallback;
45
+ }
46
+ function hasFlag(name) { return process.argv.includes(name); }
47
+ function resolveModelOpts(modelArg, providerArg) {
48
+ const key = String(modelArg || '').trim().toLowerCase();
49
+ if (MODEL_ALIASES[key] && !providerArg) return { ...MODEL_ALIASES[key] };
50
+ return { provider: providerArg || null, model: modelArg || null };
51
+ }
52
+ function defaultUserDataDir() {
53
+ return process.env.MIXDOG_DATA_DIR || join(process.env.MIXDOG_HOME || join(homedir(), '.mixdog'), 'data');
54
+ }
55
+ function readUnifiedConfig(dataDir) {
56
+ try {
57
+ const unified = JSON.parse(readFileSync(join(dataDir, 'mixdog-config.json'), 'utf8'));
58
+ return unified && typeof unified === 'object' ? unified : {};
59
+ } catch { return {}; }
60
+ }
61
+ function outputStyleBodyFromMeta(meta) {
62
+ const marker = '# Output Style';
63
+ const idx = String(meta || '').lastIndexOf(marker);
64
+ return idx < 0 ? '' : String(meta).slice(idx).trim();
65
+ }
66
+ function measureOutputText(text) {
67
+ const trimmed = String(text || '').trim();
68
+ const lines = trimmed ? trimmed.split(/\r?\n/) : [];
69
+ const bullets = lines.filter((l) => /^\s*[-*•]\s+/.test(l)).length;
70
+ const withoutCode = trimmed.replace(/`[^`]*`/g, '');
71
+ const sentenceMarks = withoutCode.match(/[.!?。!?]+(?=\s|$)/g) || [];
72
+ return {
73
+ chars: trimmed.length,
74
+ lines: lines.length,
75
+ bullets,
76
+ sentences: sentenceMarks.length || (trimmed ? 1 : 0),
77
+ text: trimmed,
78
+ };
79
+ }
80
+ function runInjectionScaffold() {
81
+ const rulesBuilder = createRequire(import.meta.url)(join(PLUGIN_ROOT, 'lib', 'rules-builder.cjs'));
82
+ const baseDir = mkdtempSync(join(REPO_ROOT, '.tmp-output-style-bench-'));
83
+ const templatePath = join(PLUGIN_ROOT, 'defaults', 'mixdog-config.template.json');
84
+ const baseConfig = existsSync(templatePath)
85
+ ? JSON.parse(readFileSync(templatePath, 'utf8'))
86
+ : { outputStyle: 'default' };
87
+ const markers = {
88
+ default: 'most detailed of the three',
89
+ simple: 'Practical concise',
90
+ minimal: 'a very short summary',
91
+ oneline: 'exactly one sentence',
92
+ };
93
+ const snippets = {};
94
+ try {
95
+ for (const styleId of STYLES) {
96
+ const dataDir = join(baseDir, styleId);
97
+ mkdirSync(dataDir, { recursive: true });
98
+ writeFileSync(join(dataDir, 'mixdog-config.json'), JSON.stringify({ ...baseConfig, outputStyle: styleId }, null, 2));
99
+ snippets[styleId] = outputStyleBodyFromMeta(rulesBuilder.buildLeadMetaContent({ PLUGIN_ROOT, DATA_DIR: dataDir }));
100
+ if (!snippets[styleId].includes(markers[styleId])) throw new Error(`${styleId} injection marker missing`);
101
+ }
102
+ if (new Set(STYLES.map((id) => snippets[id])).size !== STYLES.length) throw new Error('injection bodies not distinct');
103
+ return { snippets };
104
+ } finally {
105
+ rmSync(baseDir, { recursive: true, force: true });
106
+ }
107
+ }
108
+ function authArtifactNamesForSandbox(realDataDir, provider) {
109
+ const names = new Set();
110
+ for (const file of AUTH_ARTIFACT_BY_PROVIDER[provider] || []) names.add(file);
111
+ try {
112
+ for (const entry of readdirSync(realDataDir, { withFileTypes: true })) {
113
+ if (!entry.isFile() || !entry.name.endsWith('.json')) continue;
114
+ if (/oauth/i.test(entry.name) || /credentials/i.test(entry.name)) names.add(entry.name);
115
+ }
116
+ } catch { /* missing real data dir */ }
117
+ return [...names];
118
+ }
119
+ function copyAuthArtifacts(realDataDir, sandboxDataDir, provider) {
120
+ const copied = [];
121
+ const skipped = [];
122
+ for (const name of authArtifactNamesForSandbox(realDataDir, provider)) {
123
+ const src = join(realDataDir, name);
124
+ const dest = join(sandboxDataDir, name);
125
+ if (!existsSync(src)) {
126
+ skipped.push(name);
127
+ continue;
128
+ }
129
+ try {
130
+ copyFileSync(src, dest);
131
+ copied.push(name);
132
+ } catch {
133
+ skipped.push(name);
134
+ }
135
+ }
136
+ return { copied, skipped };
137
+ }
138
+ function prepareStyleSandbox(baseSandbox, styleId, userUnified, realDataDir, provider) {
139
+ const dataDir = join(baseSandbox, styleId);
140
+ mkdirSync(dataDir, { recursive: true });
141
+ writeFileSync(join(dataDir, 'mixdog-config.json'), JSON.stringify({ ...userUnified, outputStyle: styleId }, null, 2));
142
+ copyAuthArtifacts(realDataDir, dataDir, provider);
143
+ return dataDir;
144
+ }
145
+ function findPresetRoute(config, key) {
146
+ const wanted = String(key || '').trim().toLowerCase();
147
+ if (!wanted) return null;
148
+ const presets = Array.isArray(config?.presets) ? config.presets : [];
149
+ return presets.find((p) => {
150
+ const id = String(p?.id || '').trim().toLowerCase();
151
+ const name = String(p?.name || '').trim().toLowerCase();
152
+ return id === wanted || name === wanted;
153
+ }) || null;
154
+ }
155
+ function resolveLeadProviderModel(userUnified, cli) {
156
+ if (cli.provider && cli.model) return { provider: cli.provider, model: cli.model };
157
+ const leadPreset = findPresetRoute(userUnified, 'workflow-lead')
158
+ || findPresetRoute(userUnified, userUnified.default)
159
+ || findPresetRoute(userUnified, 'gpt-5.5');
160
+ if (leadPreset?.provider && leadPreset?.model) return { provider: leadPreset.provider, model: leadPreset.model };
161
+ const alias = MODEL_ALIASES.gpt;
162
+ return { provider: cli.provider || alias.provider, model: cli.model || alias.model };
163
+ }
164
+ function runLiveLeadTurn({ dataDir, prompt, provider, model, cwd, effort, fast }) {
165
+ const cfgUrl = pathToFileURL(join(PLUGIN_ROOT, 'runtime/agent/orchestrator/config.mjs')).href;
166
+ const regUrl = pathToFileURL(join(PLUGIN_ROOT, 'runtime/agent/orchestrator/providers/registry.mjs')).href;
167
+ const mgrUrl = pathToFileURL(join(PLUGIN_ROOT, 'runtime/agent/orchestrator/session/manager.mjs')).href;
168
+ const driver = [
169
+ `import * as cfgMod from ${JSON.stringify(cfgUrl)};`,
170
+ `import * as reg from ${JSON.stringify(regUrl)};`,
171
+ `import { createSession, askSession, closeSession } from ${JSON.stringify(mgrUrl)};`,
172
+ `const config = cfgMod.loadConfig({ secrets: true });`,
173
+ `await reg.initProviders(config.providers || {});`,
174
+ `const sessionOpts = { provider: ${JSON.stringify(provider)}, model: ${JSON.stringify(model)},`,
175
+ ` owner: 'cli', agent: 'lead', lane: 'cli', sourceType: 'lead', sourceName: 'output-style-bench',`,
176
+ ` cwd: ${JSON.stringify(cwd)}, tools: 'full', fast: ${fast ? 'true' : 'false'} };`,
177
+ effort ? `sessionOpts.effort = ${JSON.stringify(effort)};` : '',
178
+ `const session = createSession(sessionOpts);`,
179
+ `let result;`,
180
+ `try { result = await askSession(session.id, ${JSON.stringify(prompt)}, null, null, ${JSON.stringify(cwd)}); }`,
181
+ `finally { try { closeSession(session.id, 'output-style-bench'); } catch {} }`,
182
+ `process.stdout.write(JSON.stringify({ text: String(result?.text || result?.content || '').trim(), sessionId: session.id }));`,
183
+ ].filter(Boolean).join('\n');
184
+ const raw = execFileSync('node', ['--input-type=module', '-e', driver], {
185
+ encoding: 'utf8',
186
+ maxBuffer: 64 * 1024 * 1024,
187
+ env: { ...process.env, MIXDOG_ROOT: PLUGIN_ROOT, MIXDOG_DATA_DIR: dataDir },
188
+ });
189
+ const jsonStart = raw.lastIndexOf('{');
190
+ if (jsonStart < 0) throw new Error(`live driver failed: ${raw.slice(0, 400)}`);
191
+ return JSON.parse(raw.slice(jsonStart));
192
+ }
193
+ function styleCharCountsFromResults(results) {
194
+ return Object.fromEntries(
195
+ STYLES.map((id) => [id, results.find((r) => r.style === id)?.metrics.chars ?? 0]),
196
+ );
197
+ }
198
+ function evaluateVerbosityOrdering(results) {
199
+ const counts = STYLES.map((id) => results.find((r) => r.style === id)?.metrics.chars ?? 0);
200
+ let orderingOk = true;
201
+ for (let i = 1; i < counts.length; i += 1) {
202
+ if (counts[i - 1] < counts[i]) orderingOk = false;
203
+ }
204
+ const chain = STYLES.map((id, i) => `${id}=${counts[i]}`).join(' ');
205
+ const passLabel = STYLES.map((id) => `chars(${id})`).join(' >= ');
206
+ const verdict = orderingOk
207
+ ? `PASS ${passLabel}`
208
+ : `WARN ordering violated: ${chain}`;
209
+ return { orderingOk, verdict, counts: styleCharCountsFromResults(results) };
210
+ }
211
+ function printUsage() {
212
+ process.stdout.write(`output-style-bench — Lead verbosity ladder (default >= simple >= minimal >= oneline).
213
+
214
+ Output style is Lead-only (buildLeadMetaContent when owner is not agent).
215
+ runHeadlessRole worker paths (owner=agent) do NOT inject outputStyle.
216
+
217
+ Usage:
218
+ node scripts/output-style-bench.mjs [--json]
219
+ node scripts/output-style-bench.mjs --run [--model gpt] [--provider P] [--effort E] [--fast] [--prompt "..."] [--json]
220
+
221
+ --run required for live calls. Temp MIXDOG_DATA_DIR: outputStyle override + read-only copy of OAuth/credential JSON from your real data dir.
222
+ `);
223
+ }
224
+ function main() {
225
+ const jsonMode = hasFlag('--json');
226
+ const doRun = hasFlag('--run');
227
+ const prompt = argValue('--prompt', DEFAULT_PROMPT);
228
+ const cli = resolveModelOpts(argValue('--model', null), argValue('--provider', null));
229
+ const effort = argValue('--effort', null);
230
+ const fast = hasFlag('--fast');
231
+ const cwd = process.cwd();
232
+ let scaffold;
233
+ try { scaffold = runInjectionScaffold(); }
234
+ catch (e) { process.stderr.write(`[output-style-bench] scaffold FAILED: ${e.message}\n`); process.exit(1); }
235
+ if (!doRun) {
236
+ printUsage();
237
+ const charCounts = Object.fromEntries(STYLES.map((id) => [id, scaffold.snippets[id].length]));
238
+ if (jsonMode) {
239
+ console.log(JSON.stringify({ mode: 'scaffold', role: 'lead', owner: 'cli', charCounts, liveCommand: 'node scripts/output-style-bench.mjs --run --model gpt' }, null, 2));
240
+ } else {
241
+ process.stdout.write(`[output-style-bench] scaffold ok: ${STYLES.map((id) => `${id}=${charCounts[id]}`).join(', ')}\n`);
242
+ }
243
+ process.exit(0);
244
+ }
245
+ const realDataDir = defaultUserDataDir();
246
+ const userUnified = readUnifiedConfig(realDataDir);
247
+ const route = resolveLeadProviderModel(userUnified, cli);
248
+ const baseSandbox = mkdtempSync(join(REPO_ROOT, '.tmp-output-style-bench-live-'));
249
+ const results = [];
250
+ try {
251
+ for (const styleId of STYLES) {
252
+ const dataDir = prepareStyleSandbox(baseSandbox, styleId, userUnified, realDataDir, route.provider);
253
+ process.stderr.write(`[output-style-bench] style=${styleId} ${route.provider}/${route.model}\n`);
254
+ try {
255
+ const live = runLiveLeadTurn({ dataDir, prompt, ...route, cwd, effort, fast });
256
+ const metrics = measureOutputText(live.text);
257
+ results.push({ style: styleId, ok: true, sessionId: live.sessionId, metrics, raw: metrics.text });
258
+ } catch (err) {
259
+ results.push({
260
+ style: styleId,
261
+ ok: false,
262
+ error: String(err.stdout || err.stderr || err.message).slice(0, 2000),
263
+ metrics: measureOutputText(''),
264
+ });
265
+ }
266
+ }
267
+ } finally {
268
+ rmSync(baseSandbox, { recursive: true, force: true });
269
+ }
270
+ const { orderingOk, verdict } = evaluateVerbosityOrdering(results);
271
+ if (jsonMode) {
272
+ console.log(JSON.stringify({ mode: 'live', role: 'lead', prompt, route, results, verdict, orderingOk }, null, 2));
273
+ } else {
274
+ console.log(`live role=lead ${route.provider}/${route.model}`);
275
+ for (const r of results) {
276
+ const m = r.metrics;
277
+ console.log(`- ${r.style}: ${r.ok ? 'ok' : 'FAIL'} chars=${m.chars} lines=${m.lines} bullets=${m.bullets} sentences=${m.sentences}`);
278
+ if (r.ok) console.log(` ${m.text}`);
279
+ else console.log(` error: ${String(r.error || '').slice(0, 300)}`);
280
+ }
281
+ console.log(verdict);
282
+ }
283
+ process.exit(results.every((r) => r.ok) ? 0 : 1);
284
+ }
285
+ main();
@@ -49,7 +49,8 @@ const defaultStyle = readFileSync(join(root, 'src', 'output-styles', 'default.md
49
49
  for (const required of [
50
50
  'name: default',
51
51
  'Concise engineering summaries',
52
- 'Mixdog default — concise engineering summaries',
52
+ 'Mixdog default — the most detailed of the three styles',
53
+ 'match length to the work',
53
54
  'Use labels such as',
54
55
  '`바뀐 점`, `확인한 것`,',
55
56
  'Synthesize agent or retrieval results',
@@ -60,7 +61,7 @@ for (const required of [
60
61
  assert(!defaultStyle.includes('Claude Code-compact'), 'default.md must not reference the old compact style name');
61
62
  assert(!defaultStyle.includes('Hard cap user-visible replies'), 'default.md must not hard-cap replies to two short sentences');
62
63
  assert(!defaultStyle.includes('Be short and direct.'), 'default.md must not keep the old generic concise preset');
63
- assert(!defaultStyle.includes('Practical concise — outcome-first'), 'default.md must not use the simple preset body');
64
+ assert(!defaultStyle.includes('Practical concise — outcome-first handoffs'), 'default.md must not use the simple preset body');
64
65
 
65
66
  const simpleStyle = readFileSync(join(root, 'src', 'output-styles', 'simple.md'), 'utf8');
66
67
  for (const required of [
@@ -75,11 +76,12 @@ for (const required of [
75
76
  ]) {
76
77
  assert(simpleStyle.includes(required), `simple.md missing: ${required}`);
77
78
  }
78
- assert(!simpleStyle.includes('Mixdog default — concise engineering summaries'), 'simple.md must not duplicate default preset body');
79
+ assert(!simpleStyle.includes('Mixdog default — the most detailed of the three styles'), 'simple.md must not duplicate default preset body');
79
80
  for (const [name, style] of Object.entries({
80
81
  default: defaultStyle,
81
82
  simple: simpleStyle,
82
- extreme: readFileSync(join(root, 'src', 'output-styles', 'extreme-simple.md'), 'utf8'),
83
+ minimal: readFileSync(join(root, 'src', 'output-styles', 'minimal.md'), 'utf8'),
84
+ oneline: readFileSync(join(root, 'src', 'output-styles', 'oneline.md'), 'utf8'),
83
85
  })) {
84
86
  assert(!style.includes('pre-tool preamble'), `${name} output style must not own language preamble rules`);
85
87
  assert(!style.includes('selected/default user language'), `${name} output style must not duplicate profile language rules`);
@@ -101,7 +103,7 @@ try {
101
103
  writeFileSync(join(dataDir, 'output-styles', 'custom-smoke.md'), '---\nname: custom-smoke\n---\n\n# Custom Output Style\n\ncustom smoke style\n');
102
104
  const customRules = rulesBuilder.buildInjectionContent({ PLUGIN_ROOT: join(root, 'src'), DATA_DIR: dataDir });
103
105
  assert(customRules.includes('# Custom Output Style'), 'configured outputStyle must select custom style');
104
- assert(!customRules.includes('Mixdog default — concise engineering summaries'), 'custom outputStyle should not append default style');
106
+ assert(!customRules.includes('Mixdog default — the most detailed of the three styles'), 'custom outputStyle should not append default style');
105
107
  const profileMeta = rulesBuilder.buildLeadMetaContent({ PLUGIN_ROOT: join(root, 'src'), DATA_DIR: dataDir });
106
108
  assert(profileMeta.includes('Use "재영님" when directly addressing the user'), 'profile title must inject into Lead BP3 meta');
107
109
  assert(profileMeta.includes('Do not repeat it in routine progress updates or pre-tool preambles'), 'profile title must not encourage title in preambles');
@@ -111,12 +113,13 @@ try {
111
113
  rmSync(dataDir, { recursive: true, force: true });
112
114
  }
113
115
 
116
+ // assertCleanOutput encodes the Simple style compact contract (not Default, which may be longer).
114
117
  const goodOutputs = {
115
- explanation: '기본 출력은 결과 한 줄과 근거 한 가지로 끝내고, 최종 보고만 짧은 bullet 라벨을 씁니다.',
116
- implementation: '- 바뀐 점: `src/output-styles/default.md`에 이전 simple 프리셋을 반영했습니다.\n- 확인한 것: `node scripts/output-style-smoke.mjs`를 실행했습니다.',
117
- crowded: '- 바뀐 점: default는 요약 라벨 보고, simple은 handoff용 controlled detail을 씁니다.\n- 확인한 것: smoke 예시가 compact guardrail을 유지합니다.',
118
- blocker: '`src/output-styles/default.md`를 찾을 수 없어 변경이 막혔습니다.',
119
- semicolon: '`default.md`를 갱신했고; 출력 스모크가 예시 응답 모양을 검증합니다.',
118
+ explanation: 'Simple 스타일은 결과 한 줄과 근거 한 가지로 끝내고, 최종 handoff만 짧은 bullet 라벨을 씁니다.',
119
+ implementation: '- 바뀐 점: `scripts/output-style-smoke.mjs`의 default/simple 문자열 검증을 현재 프리셋에 맞췄습니다.\n- 확인한 것: `node scripts/output-style-smoke.mjs`를 실행했습니다.',
120
+ crowded: '- 바뀐 점: compact guardrail은 Simple handoff용 controlled detail을 검증합니다.\n- 확인한 것: bad 샘플 거부 규칙은 그대로입니다.',
121
+ blocker: '`scripts/output-style-smoke.mjs`를 찾을 수 없어 변경이 막혔습니다.',
122
+ semicolon: '스모크 스크립트를 갱신했고; Simple 계약에 맞는 예시 응답만 통과합니다.',
120
123
  };
121
124
  for (const [name, output] of Object.entries(goodOutputs)) assertCleanOutput(name, output, { allowedLabels: DEFAULT_REPORT_LABELS });
122
125
 
@@ -0,0 +1,90 @@
1
+ #!/usr/bin/env node
2
+ // patch-replay.mjs — re-run captured apply_patch FAILURES against current code.
3
+ // Failures frozen by patch.mjs (MIXDOG_PATCH_REPLAY_CAPTURE=1) into
4
+ // <data>/history/patch-replays/*.json with original args + target file snapshots.
5
+ // Replays into a throwaway temp copy (never touches the repo) and reports pass/fail.
6
+ // node scripts/patch-replay.mjs --list
7
+ // node scripts/patch-replay.mjs --replay <id>
8
+ // node scripts/patch-replay.mjs --replay-all [--json]
9
+ import { existsSync, readFileSync, readdirSync, mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';
10
+ import { homedir, tmpdir } from 'node:os';
11
+ import { dirname, join, resolve } from 'node:path';
12
+ import { executePatchTool } from '../src/runtime/agent/orchestrator/tools/patch.mjs';
13
+
14
+ function argValue(name, fallback = null) {
15
+ const idx = process.argv.indexOf(name);
16
+ if (idx >= 0 && idx + 1 < process.argv.length) return process.argv[idx + 1];
17
+ const pref = `${name}=`;
18
+ const hit = process.argv.find((a) => a.startsWith(pref));
19
+ return hit ? hit.slice(pref.length) : fallback;
20
+ }
21
+ function hasFlag(name) { return process.argv.includes(name); }
22
+
23
+ function replayDir() {
24
+ if (process.env.MIXDOG_PATCH_REPLAY_DIR) return resolve(process.env.MIXDOG_PATCH_REPLAY_DIR);
25
+ const data = process.env.MIXDOG_DATA_DIR || resolve(homedir(), '.mixdog', 'data');
26
+ return resolve(data, 'history', 'patch-replays');
27
+ }
28
+
29
+ function loadRecords() {
30
+ const dir = replayDir();
31
+ if (!existsSync(dir)) return [];
32
+ return readdirSync(dir).filter((f) => f.endsWith('.json')).map((f) => {
33
+ try { return { file: join(dir, f), ...JSON.parse(readFileSync(join(dir, f), 'utf8')) }; }
34
+ catch { return null; }
35
+ }).filter(Boolean).sort((a, b) => Number(b.ts || 0) - Number(a.ts || 0));
36
+ }
37
+
38
+ function isErr(text) { return /^Error[\s:[]/.test(String(text || '').trimStart()); }
39
+
40
+ async function replayOne(rec) {
41
+ const tmp = mkdtempSync(join(tmpdir(), 'mixdog-patch-replay-'));
42
+ try {
43
+ for (const [rel, content] of Object.entries(rec.file_snapshots || {})) {
44
+ if (content == null) continue;
45
+ const abs = join(tmp, rel);
46
+ mkdirSync(dirname(abs), { recursive: true });
47
+ writeFileSync(abs, content);
48
+ }
49
+ const args = { ...(rec.args || {}), base_path: tmp };
50
+ let result;
51
+ try { result = await executePatchTool('apply_patch', args, tmp, {}); }
52
+ catch (e) { result = `Error: ${e?.message || String(e)}`; }
53
+ return { id: rec.id, ok: !isErr(result), before: rec.error_first_line, after: String(result).split('\n')[0].slice(0, 200) };
54
+ } finally {
55
+ try { rmSync(tmp, { recursive: true, force: true }); } catch {}
56
+ }
57
+ }
58
+
59
+ const jsonMode = hasFlag('--json');
60
+ const records = loadRecords();
61
+
62
+ if (hasFlag('--list') || (!hasFlag('--replay-all') && !argValue('--replay'))) {
63
+ if (jsonMode) { console.log(JSON.stringify(records.map(({ file_snapshots, args, ...m }) => m), null, 2)); process.exit(0); }
64
+ console.log(`captured apply_patch failures: ${records.length} (dir: ${replayDir()})`);
65
+ for (const r of records.slice(0, 50)) {
66
+ console.log(`- ${r.id} targets=${(r.targets || []).length} ${new Date(r.ts).toISOString()}`);
67
+ console.log(` ${String(r.error_first_line || '').slice(0, 140)}`);
68
+ }
69
+ if (!records.length) console.log('(none - set MIXDOG_PATCH_REPLAY_CAPTURE=1 to capture)');
70
+ process.exit(0);
71
+ }
72
+
73
+ const one = argValue('--replay', null);
74
+ const targets = one ? records.filter((r) => r.id === one || r.id.startsWith(one)) : records;
75
+ if (!targets.length) { console.error(one ? `no replay matched: ${one}` : 'no captured failures'); process.exit(1); }
76
+
77
+ const results = [];
78
+ for (const rec of targets) results.push(await replayOne(rec));
79
+ const passed = results.filter((r) => r.ok).length;
80
+
81
+ if (jsonMode) {
82
+ console.log(JSON.stringify({ total: results.length, passed, failed: results.length - passed, results }, null, 2));
83
+ } else {
84
+ console.log(`patch-replay: ${passed}/${results.length} now succeed`);
85
+ for (const r of results) {
86
+ console.log(`- ${r.id}: ${r.ok ? 'PASS' : 'still fails'}`);
87
+ if (!r.ok) console.log(` after: ${r.after}`);
88
+ }
89
+ }
90
+ process.exitCode = passed === results.length ? 0 : 1;