mixdog 0.9.36 → 0.9.38

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 (115) hide show
  1. package/package.json +3 -2
  2. package/scripts/abort-recovery-test.mjs +118 -0
  3. package/scripts/compact-smoke.mjs +7 -7
  4. package/scripts/dispatch-persist-recovery-test.mjs +141 -0
  5. package/scripts/explore-bench-tmp.mjs +19 -0
  6. package/scripts/explore-bench.mjs +101 -14
  7. package/scripts/explore-prompt-policy-test.mjs +31 -0
  8. package/scripts/ingest-pure-conversation-smoke.mjs +11 -11
  9. package/scripts/notify-completion-mirror-test.mjs +73 -0
  10. package/scripts/openai-ws-early-settle-test.mjs +176 -0
  11. package/scripts/output-style-smoke.mjs +17 -17
  12. package/scripts/provider-toolcall-test.mjs +333 -14
  13. package/scripts/recall-bench-cases.json +3 -3
  14. package/scripts/recall-bench.mjs +1 -1
  15. package/scripts/recall-quality-cases.json +4 -4
  16. package/scripts/recall-usecase-cases.json +2 -2
  17. package/scripts/session-bench.mjs +13 -13
  18. package/scripts/session-sweep.mjs +266 -0
  19. package/scripts/steering-drain-buckets-test.mjs +72 -11
  20. package/scripts/submit-commandbusy-race-test.mjs +114 -0
  21. package/scripts/tool-smoke.mjs +83 -10
  22. package/src/app.mjs +2 -1
  23. package/src/defaults/cycle3-review-prompt.md +9 -0
  24. package/src/defaults/skills/setup/SKILL.md +93 -293
  25. package/src/lib/rules-builder.cjs +3 -2
  26. package/src/output-styles/default.md +2 -2
  27. package/src/output-styles/extreme-minimal.md +1 -1
  28. package/src/output-styles/minimal.md +1 -1
  29. package/src/output-styles/simple.md +2 -3
  30. package/src/rules/agent/30-explorer.md +40 -14
  31. package/src/rules/lead/01-general.md +4 -0
  32. package/src/rules/shared/01-tool.md +7 -3
  33. package/src/runtime/agent/orchestrator/agent-trace-format.mjs +15 -3
  34. package/src/runtime/agent/orchestrator/config.mjs +1 -1
  35. package/src/runtime/agent/orchestrator/dispatch-persist.mjs +31 -8
  36. package/src/runtime/agent/orchestrator/providers/anthropic-oauth-credentials.mjs +13 -7
  37. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +6 -6
  38. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +6 -6
  39. package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +35 -5
  40. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +27 -0
  41. package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +36 -37
  42. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +2 -2
  43. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +72 -1
  44. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +26 -3
  45. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +12 -28
  46. package/src/runtime/agent/orchestrator/providers/openai-transport-policy.mjs +18 -15
  47. package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +5 -2
  48. package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +119 -3
  49. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +113 -2
  50. package/src/runtime/agent/orchestrator/providers/registry.mjs +44 -5
  51. package/src/runtime/agent/orchestrator/providers/tool-stream-state.mjs +78 -0
  52. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +57 -31
  53. package/src/runtime/agent/orchestrator/session/cache/scoped-cache.mjs +8 -6
  54. package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +28 -2
  55. package/src/runtime/agent/orchestrator/session/loop/tool-classify.mjs +1 -3
  56. package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +15 -2
  57. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +30 -55
  58. package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +2 -2
  59. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +24 -16
  60. package/src/runtime/agent/orchestrator/session/result-classification.mjs +2 -0
  61. package/src/runtime/agent/orchestrator/session/store.mjs +116 -21
  62. package/src/runtime/agent/orchestrator/session/tool-batch.mjs +5 -2
  63. package/src/runtime/agent/orchestrator/stall-policy.mjs +55 -0
  64. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +35 -20
  65. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +9 -9
  66. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +77 -31
  67. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +25 -3
  68. package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +9 -2
  69. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +73 -25
  70. package/src/runtime/agent/orchestrator/tools/builtin.mjs +2 -1
  71. package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +17 -7
  72. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +14 -12
  73. package/src/runtime/agent/orchestrator/tools/code-graph/graph-model.mjs +5 -0
  74. package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +56 -6
  75. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +1 -1
  76. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +9 -0
  77. package/src/runtime/channels/lib/worker-main.mjs +5 -0
  78. package/src/runtime/memory/lib/core-memory-store.mjs +104 -0
  79. package/src/runtime/memory/lib/ko-morph.mjs +1 -1
  80. package/src/runtime/memory/lib/memory-cycle3.mjs +64 -4
  81. package/src/runtime/memory/lib/memory-text-utils.mjs +4 -4
  82. package/src/runtime/memory/lib/recall-format.mjs +3 -3
  83. package/src/runtime/shared/child-spawn-gate.mjs +15 -0
  84. package/src/runtime/shared/config.mjs +98 -12
  85. package/src/runtime/shared/process-listener-headroom.mjs +12 -0
  86. package/src/session-runtime/cwd-plugins.mjs +6 -3
  87. package/src/session-runtime/model-route-api.mjs +50 -2
  88. package/src/session-runtime/provider-auth-api.mjs +8 -1
  89. package/src/session-runtime/provider-models.mjs +3 -3
  90. package/src/session-runtime/resource-api.mjs +22 -0
  91. package/src/session-runtime/runtime-core.mjs +56 -10
  92. package/src/session-runtime/settings-api.mjs +11 -2
  93. package/src/session-runtime/warmup-schedulers.mjs +7 -1
  94. package/src/standalone/agent-tool.mjs +15 -9
  95. package/src/standalone/explore-tool.mjs +4 -1
  96. package/src/tui/App.jsx +6 -5
  97. package/src/tui/app/transcript-window.mjs +60 -10
  98. package/src/tui/app/use-transcript-window.mjs +2 -1
  99. package/src/tui/components/ContextPanel.jsx +4 -1
  100. package/src/tui/components/ToolExecution.jsx +15 -12
  101. package/src/tui/components/TranscriptItem.jsx +1 -1
  102. package/src/tui/components/tool-execution/surface-detail.mjs +8 -8
  103. package/src/tui/dist/index.mjs +482 -151
  104. package/src/tui/engine/agent-job-feed.mjs +36 -6
  105. package/src/tui/engine/notification-plan.mjs +3 -3
  106. package/src/tui/engine/queue-helpers.mjs +8 -0
  107. package/src/tui/engine/render-timing.mjs +104 -8
  108. package/src/tui/engine/session-api.mjs +58 -3
  109. package/src/tui/engine/session-flow.mjs +93 -29
  110. package/src/tui/engine/tool-card-results.mjs +28 -13
  111. package/src/tui/engine/turn.mjs +238 -157
  112. package/src/tui/engine.mjs +17 -8
  113. package/src/tui/index.jsx +10 -1
  114. package/src/workflows/default/WORKFLOW.md +8 -4
  115. package/src/workflows/solo/WORKFLOW.md +8 -4
@@ -0,0 +1,73 @@
1
+ // notifyFnForSession must not double-inject a background-task completion. The
2
+ // mirror skip is gated on an EXPLICIT model-visible-delivery ack
3
+ // (modelVisibleDelivered), set ONLY by the TUI execution-ui path that injects
4
+ // the completion body into the active loop — never on a generic truthy
5
+ // onNotification return. So: delivered ack → mirror suppressed; headless /
6
+ // display-only / API listener (no ack) → mirror kept as the sole delivery.
7
+ import test from 'node:test';
8
+ import assert from 'node:assert/strict';
9
+
10
+ import { shouldMirrorCompletionToPendingQueue } from '../src/session-runtime/runtime-core.mjs';
11
+
12
+ // A terminal agent completion with a Result body — accepted by
13
+ // shouldPersistModelVisibleToolCompletion.
14
+ const completionText = 'Async agent task task_1 completed finished.\n\nResult:\n> ok';
15
+ const completionMeta = { type: 'agent_task_result', execution_id: 'task_1', status: 'completed' };
16
+
17
+ test('delivered-live completion: no pending mirror (explicit model-visible ack)', () => {
18
+ const mirror = shouldMirrorCompletionToPendingQueue({
19
+ callerSessionId: 'sess_live',
20
+ modelVisibleDelivered: true,
21
+ hasEnqueue: true,
22
+ text: completionText,
23
+ meta: completionMeta,
24
+ });
25
+ assert.equal(mirror, false, 'live twin already injected → mirror suppressed');
26
+ });
27
+
28
+ test('headless completion: pending mirror kept (no ack)', () => {
29
+ const mirror = shouldMirrorCompletionToPendingQueue({
30
+ callerSessionId: 'sess_headless',
31
+ modelVisibleDelivered: false,
32
+ hasEnqueue: true,
33
+ text: completionText,
34
+ meta: completionMeta,
35
+ });
36
+ assert.equal(mirror, true, 'no listener delivered → queue copy is the sole delivery');
37
+ });
38
+
39
+ test('display-only listener returning true (no ack): pending mirror kept', () => {
40
+ // A generic display/API listener consumes the event (would return true) but
41
+ // never injects the model-visible body, so it sets NO modelVisibleDelivered
42
+ // ack. The mirror must stay, or the model never sees the completion body.
43
+ const mirror = shouldMirrorCompletionToPendingQueue({
44
+ callerSessionId: 'sess_display_only',
45
+ modelVisibleDelivered: false,
46
+ hasEnqueue: true,
47
+ text: completionText,
48
+ meta: completionMeta,
49
+ });
50
+ assert.equal(mirror, true, 'generic handled===true is NOT an ack → mirror kept');
51
+ });
52
+
53
+ test('unhandled but non-terminal notification is never mirrored', () => {
54
+ const mirror = shouldMirrorCompletionToPendingQueue({
55
+ callerSessionId: 'sess_headless',
56
+ modelVisibleDelivered: false,
57
+ hasEnqueue: true,
58
+ text: 'still running...',
59
+ meta: { status: 'running' },
60
+ });
61
+ assert.equal(mirror, false, 'only persistable terminal completions mirror');
62
+ });
63
+
64
+ test('missing session id or enqueue capability never mirrors', () => {
65
+ assert.equal(shouldMirrorCompletionToPendingQueue({
66
+ callerSessionId: '', modelVisibleDelivered: false, hasEnqueue: true,
67
+ text: completionText, meta: completionMeta,
68
+ }), false, 'no caller session → nothing to mirror into');
69
+ assert.equal(shouldMirrorCompletionToPendingQueue({
70
+ callerSessionId: 'sess_headless', modelVisibleDelivered: false, hasEnqueue: false,
71
+ text: completionText, meta: completionMeta,
72
+ }), false, 'no enqueue capability → cannot mirror');
73
+ });
@@ -0,0 +1,176 @@
1
+ // Regression: OpenAI OAuth WS early tool-call settle.
2
+ //
3
+ // A tool-call response can be fully formed (function_call_arguments.done +
4
+ // output_item.done) yet the server never emits the terminal
5
+ // response.completed/response.done frame. Before the fix _streamResponse
6
+ // coasted on that silence until a stall error (gated out of the loop via
7
+ // pendingToolUse) or the 30-min agent watchdog, and -- because the socket was
8
+ // still pooled -- the missing terminal frame later leaked onto the NEXT request
9
+ // as orphan bytes (http_status=0 wedge). The fix resolves early with the
10
+ // captured tool call and flags `closeSocket` so the pool discards the socket.
11
+ import test from 'node:test';
12
+ import assert from 'node:assert/strict';
13
+ import { EventEmitter } from 'node:events';
14
+ import { _streamResponse } from '../src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs';
15
+
16
+ // Minimal fake WS: matches the .on/.off/.close/.ping surface _streamResponse
17
+ // uses. close() is a no-op here -- the early-settle path sets done=true before
18
+ // calling it, so the real closeHandler would short-circuit anyway.
19
+ class FakeSocket extends EventEmitter {
20
+ constructor() {
21
+ super();
22
+ this.readyState = 1;
23
+ this.closed = null;
24
+ }
25
+ close(code, reason) { this.closed = { code, reason }; }
26
+ ping() {}
27
+ feed(events) { for (const e of events) this.emit('message', JSON.stringify(e)); }
28
+ }
29
+
30
+ const FAST_TIMEOUTS = { interChunkMs: 40, preResponseCreatedMs: 5000, firstMeaningfulMs: 5000 };
31
+
32
+ const TOOL_CALL_EVENTS = [
33
+ { type: 'response.created', response: { id: 'resp_1', model: 'gpt-5.5' } },
34
+ { type: 'response.output_item.added', item: { type: 'function_call', id: 'fc_1', name: 'explore', call_id: 'call_1' } },
35
+ { type: 'response.function_call_arguments.delta', item_id: 'fc_1', delta: '{"query"' },
36
+ { type: 'response.function_call_arguments.done', item_id: 'fc_1', arguments: '{"query":"x"}', call_id: 'call_1', name: 'explore' },
37
+ { type: 'response.output_item.done', item: { type: 'function_call', id: 'fc_1', name: 'explore', call_id: 'call_1', arguments: '{"query":"x"}' } },
38
+ ];
39
+
40
+ test('ws early settle: complete tool call resolves without response.completed and flags closeSocket', async () => {
41
+ const socket = new FakeSocket();
42
+ const state = {};
43
+ const emitted = [];
44
+ const p = _streamResponse({
45
+ entry: { socket },
46
+ state,
47
+ onToolCall: (c) => emitted.push(c),
48
+ _timeouts: FAST_TIMEOUTS,
49
+ });
50
+ socket.feed(TOOL_CALL_EVENTS);
51
+ const result = await p;
52
+ assert.equal(result.closeSocket, true, 'socket must be dropped, not pooled');
53
+ assert.ok(Array.isArray(result.toolCalls) && result.toolCalls.length === 1);
54
+ assert.equal(result.toolCalls[0].id, 'call_1');
55
+ assert.equal(result.toolCalls[0].name, 'explore');
56
+ assert.deepEqual(result.toolCalls[0].arguments, { query: 'x' });
57
+ assert.equal(emitted.length, 1, 'tool call dispatched exactly once');
58
+ assert.equal(state.wsEarlySettle, 'inter_chunk');
59
+ assert.ok(socket.closed, 'socket.close() invoked so it is never reused');
60
+ });
61
+
62
+ test('ws early settle: normal response.completed path keeps the socket (no closeSocket)', async () => {
63
+ const socket = new FakeSocket();
64
+ const p = _streamResponse({ entry: { socket }, state: {}, _timeouts: FAST_TIMEOUTS });
65
+ socket.feed([
66
+ ...TOOL_CALL_EVENTS,
67
+ {
68
+ type: 'response.completed',
69
+ response: {
70
+ id: 'resp_1',
71
+ model: 'gpt-5.5',
72
+ output: [{ type: 'function_call', id: 'fc_1', name: 'explore', call_id: 'call_1', arguments: '{"query":"x"}' }],
73
+ },
74
+ },
75
+ ]);
76
+ const result = await p;
77
+ assert.equal(result.closeSocket, undefined, 'normal completion must not mark closeSocket');
78
+ assert.equal(result.toolCalls.length, 1);
79
+ });
80
+
81
+ test('ws early settle: deferred salvage still pending does NOT early-settle (fails as stall)', async () => {
82
+ const socket = new FakeSocket();
83
+ const p = _streamResponse({ entry: { socket }, state: {}, _timeouts: FAST_TIMEOUTS });
84
+ // No output_item.added -> pendingCalls empty; args.done carries no call_id/name
85
+ // -> a deferred placeholder that only response.completed could salvage.
86
+ socket.feed([
87
+ { type: 'response.created', response: { id: 'resp_1', model: 'gpt-5.5' } },
88
+ { type: 'response.function_call_arguments.delta', item_id: 'fc_1', delta: '{"query"' },
89
+ { type: 'response.function_call_arguments.done', item_id: 'fc_1', arguments: '{"query":"x"}' },
90
+ ]);
91
+ await assert.rejects(p, /inter-chunk inactivity/);
92
+ });
93
+
94
+ test('ws early settle: complete tool then partial second tool does NOT early-settle', async () => {
95
+ const socket = new FakeSocket();
96
+ const p = _streamResponse({ entry: { socket }, state: {}, _timeouts: FAST_TIMEOUTS });
97
+ socket.feed([
98
+ ...TOOL_CALL_EVENTS,
99
+ { type: 'response.output_item.added', item: { type: 'function_call', id: 'fc_2', name: 'grep', call_id: 'call_2' } },
100
+ { type: 'response.function_call_arguments.delta', item_id: 'fc_2', delta: '{"pattern"' },
101
+ ]);
102
+ await assert.rejects(p, /inter-chunk inactivity/);
103
+ assert.equal(socket.closed?.reason, 'inter_chunk_timeout', 'partial second tool must use stall path, not early settle');
104
+ });
105
+
106
+ test('ws early settle: complete tool_search_call clears in-flight and can early-settle', async () => {
107
+ const socket = new FakeSocket();
108
+ const state = {};
109
+ const p = _streamResponse({ entry: { socket }, state, _timeouts: FAST_TIMEOUTS });
110
+ socket.feed([
111
+ { type: 'response.created', response: { id: 'resp_1', model: 'gpt-5.5' } },
112
+ { type: 'response.output_item.added', item: { type: 'tool_search_call', id: 'ts_1' } },
113
+ { type: 'response.output_item.done', item: { type: 'tool_search_call', id: 'ts_1', arguments: '{"query":"fetch"}' } },
114
+ ]);
115
+ const result = await p;
116
+ assert.equal(result.closeSocket, true);
117
+ assert.equal(result.toolCalls[0].id, 'ts_1');
118
+ assert.equal(result.toolCalls[0].name, 'load_tool');
119
+ assert.deepEqual(result.toolCalls[0].arguments, { query: 'fetch' });
120
+ assert.equal(state.wsEarlySettle, 'inter_chunk');
121
+ });
122
+
123
+ test('ws early settle: complete tool_search_call plus partial custom tool does NOT early-settle', async () => {
124
+ const socket = new FakeSocket();
125
+ const p = _streamResponse({ entry: { socket }, state: {}, _timeouts: FAST_TIMEOUTS });
126
+ socket.feed([
127
+ { type: 'response.created', response: { id: 'resp_1', model: 'gpt-5.5' } },
128
+ { type: 'response.output_item.added', item: { type: 'tool_search_call', id: 'ts_1' } },
129
+ { type: 'response.output_item.done', item: { type: 'tool_search_call', id: 'ts_1', arguments: '{"query":"fetch"}' } },
130
+ { type: 'response.output_item.added', item: { type: 'custom_tool_call', id: 'ct_2', call_id: 'call_custom_2', name: 'apply_patch' } },
131
+ { type: 'response.custom_tool_call_input.delta', item_id: 'ct_2', delta: '*** Begin Patch' },
132
+ ]);
133
+ await assert.rejects(p, /inter-chunk inactivity/);
134
+ assert.equal(socket.closed?.reason, 'inter_chunk_timeout', 'partial custom tool must keep stall path');
135
+ });
136
+
137
+ test('ws early settle: output_item.done salvages deferred function_call and can early-settle', async () => {
138
+ const socket = new FakeSocket();
139
+ const p = _streamResponse({ entry: { socket }, state: {}, _timeouts: FAST_TIMEOUTS });
140
+ socket.feed([
141
+ { type: 'response.created', response: { id: 'resp_1', model: 'gpt-5.5' } },
142
+ { type: 'response.function_call_arguments.delta', item_id: 'fc_salvage', delta: '{"query"' },
143
+ { type: 'response.function_call_arguments.done', item_id: 'fc_salvage', arguments: '{"query":"x"}' },
144
+ { type: 'response.output_item.done', item: { type: 'function_call', id: 'fc_salvage', name: 'explore', call_id: 'call_salvage', arguments: '{"query":"x"}' } },
145
+ ]);
146
+ const result = await p;
147
+ assert.equal(result.closeSocket, true);
148
+ assert.equal(result.toolCalls[0].id, 'call_salvage');
149
+ assert.equal(result.toolCalls[0].name, 'explore');
150
+ assert.deepEqual(result.toolCalls[0].arguments, { query: 'x' });
151
+ });
152
+
153
+ test('ws early settle: call_id-only tool_search done clears id-tracked active item', async () => {
154
+ const socket = new FakeSocket();
155
+ const p = _streamResponse({ entry: { socket }, state: {}, _timeouts: FAST_TIMEOUTS });
156
+ socket.feed([
157
+ { type: 'response.created', response: { id: 'resp_1', model: 'gpt-5.5' } },
158
+ { type: 'response.output_item.added', item: { type: 'tool_search_call', id: 'ts_added', call_id: 'ts_call' } },
159
+ { type: 'response.output_item.done', item: { type: 'tool_search_call', call_id: 'ts_call', arguments: '{"query":"fetch"}' } },
160
+ ]);
161
+ const result = await p;
162
+ assert.equal(result.closeSocket, true);
163
+ assert.equal(result.toolCalls[0].id, 'ts_call');
164
+ });
165
+
166
+ test('ws early settle: function args.done without item.done does NOT early-settle', async () => {
167
+ const socket = new FakeSocket();
168
+ const p = _streamResponse({ entry: { socket }, state: {}, _timeouts: FAST_TIMEOUTS });
169
+ socket.feed([
170
+ { type: 'response.created', response: { id: 'resp_1', model: 'gpt-5.5' } },
171
+ { type: 'response.output_item.added', item: { type: 'function_call', id: 'fc_empty', name: 'explore', call_id: 'call_empty' } },
172
+ { type: 'response.function_call_arguments.done', item_id: 'fc_empty', arguments: '{}', call_id: 'call_empty', name: 'explore' },
173
+ ]);
174
+ await assert.rejects(p, /inter-chunk inactivity/);
175
+ assert.equal(socket.closed?.reason, 'inter_chunk_timeout', 'function item must stay active until output_item.done');
176
+ });
@@ -13,7 +13,7 @@ function assert(condition, message) {
13
13
  if (!condition) throw new Error(message);
14
14
  }
15
15
 
16
- const DEFAULT_REPORT_LABELS = new Set(['바뀐 ', '확인한 ', '남은 리스크/다음 단계', '다음 단계']);
16
+ const DEFAULT_REPORT_LABELS = new Set(['\uBC14\uB010 \uC810', '\uD655\uC778\uD55C \uAC83', '\uB0A8\uC740 \uB9AC\uC2A4\uD06C/\uB2E4\uC74C \uB2E8\uACC4', '\uB2E4\uC74C \uB2E8\uACC4']);
17
17
 
18
18
  function assertCleanOutput(name, value, { maxLines = 3, maxBullets = 3, allowedLabels = new Set() } = {}) {
19
19
  const text = String(value || '').trim();
@@ -28,7 +28,7 @@ function assertCleanOutput(name, value, { maxLines = 3, maxBullets = 3, allowedL
28
28
  }
29
29
  assert(!/\b(?:tool trace|session metadata|model metadata|searched-path list)\b/i.test(text), `${name}: tool/meta trace found`);
30
30
  assert(!/\b(?:Mapped|Searched|Read|Called) for \d/i.test(text), `${name}: timing/tool status found`);
31
- assert(!/^(?:네|예|맞아요|좋아요|알겠습니다|Sure|Okay|Understood)[,.\s]/i.test(text), `${name}: acknowledgment preface found`);
31
+ assert(!/^(?:\uB124|\uC608|\uB9DE\uC544\uC694|\uC88B\uC544\uC694|\uC54C\uACA0\uC2B5\uB2C8\uB2E4|Sure|Okay|Understood)[,.\s]/i.test(text), `${name}: acknowledgment preface found`);
32
32
  assert(!/(.+)(?:\n|\s{2,})\1/.test(text), `${name}: repeated conclusion found`);
33
33
 
34
34
  const lines = text.split(/\r?\n/).filter((line) => line.trim());
@@ -41,7 +41,7 @@ function assertCleanOutput(name, value, { maxLines = 3, maxBullets = 3, allowedL
41
41
  assert(lines.length <= maxLines, `${name}: too many lines`);
42
42
 
43
43
  const withoutCode = text.replace(/`[^`]*`/g, '');
44
- const sentenceMarks = withoutCode.match(/[.!?。!?]|다\.|요\./g) || [];
44
+ const sentenceMarks = withoutCode.match(/[.!?。!?]|\uB2E4\.|\uC694\./g) || [];
45
45
  assert(sentenceMarks.length <= 2, `${name}: too many sentences`);
46
46
  }
47
47
 
@@ -52,7 +52,7 @@ for (const required of [
52
52
  'Mixdog default — the most detailed style',
53
53
  'State conclusions, not reasoning',
54
54
  'Use labels such as',
55
- '`바뀐 점`, `확인한 것`,',
55
+ '`\uBC14\uB010 \uC810`, `\uD655\uC778\uD55C \uAC83`,',
56
56
  'Synthesize agent or retrieval results',
57
57
  'Do not hide blockers',
58
58
  ]) {
@@ -97,7 +97,7 @@ try {
97
97
 
98
98
  mkdirSync(join(dataDir, 'output-styles'), { recursive: true });
99
99
  writeFileSync(join(dataDir, 'mixdog-config.json'), JSON.stringify({
100
- agent: { profile: { title: '홍길동님', language: 'system' } },
100
+ agent: { profile: { title: '\uD64D\uAE38\uB3D9\uB2D8', language: 'system' } },
101
101
  outputStyle: 'custom-smoke',
102
102
  }));
103
103
  writeFileSync(join(dataDir, 'output-styles', 'custom-smoke.md'), '---\nname: custom-smoke\n---\n\n# Custom Output Style\n\ncustom smoke style\n');
@@ -105,7 +105,7 @@ try {
105
105
  assert(customRules.includes('# Custom Output Style'), 'configured outputStyle must select custom style');
106
106
  assert(!customRules.includes('Mixdog default — the most detailed of the three styles'), 'custom outputStyle should not append default style');
107
107
  const profileMeta = rulesBuilder.buildLeadMetaContent({ PLUGIN_ROOT: join(root, 'src'), DATA_DIR: dataDir });
108
- assert(profileMeta.includes('Use "홍길동님" when directly addressing the user'), 'profile title must inject into Lead BP3 meta');
108
+ assert(profileMeta.includes('Use "\uD64D\uAE38\uB3D9\uB2D8" when directly addressing the user'), 'profile title must inject into Lead BP3 meta');
109
109
  assert(profileMeta.includes('do not repeat it in routine progress updates or pre-tool preambles'), 'profile title must not encourage title in preambles');
110
110
  assert(/Default user-facing response language from system locale/.test(profileMeta), 'system profile language must resolve from system locale');
111
111
  assert(profileMeta.includes('pre-tool preambles (even single-line)'), 'profile language must cover pre-tool preambles');
@@ -115,26 +115,26 @@ try {
115
115
 
116
116
  // assertCleanOutput encodes the Simple style compact contract (not Default, which may be longer).
117
117
  const goodOutputs = {
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 계약에 맞는 예시 응답만 통과합니다.',
118
+ explanation: 'Simple \uC2A4\uD0C0\uC77C\uC740 \uACB0\uACFC \uD55C \uC904\uACFC \uADFC\uAC70 \uD55C \uAC00\uC9C0\uB85C \uB05D\uB0B4\uACE0, \uCD5C\uC885 handoff\uB9CC \uC9E7\uC740 bullet \uB77C\uBCA8\uC744 \uC501\uB2C8\uB2E4.',
119
+ implementation: '- \uBC14\uB010 \uC810: `scripts/output-style-smoke.mjs`\uC758 default/simple \uBB38\uC790\uC5F4 \uAC80\uC99D\uC744 \uD604\uC7AC \uD504\uB9AC\uC14B\uC5D0 \uB9DE\uCDC4\uC2B5\uB2C8\uB2E4.\n- \uD655\uC778\uD55C \uAC83: `node scripts/output-style-smoke.mjs`\uB97C \uC2E4\uD589\uD588\uC2B5\uB2C8\uB2E4.',
120
+ crowded: '- \uBC14\uB010 \uC810: compact guardrail\uC740 Simple handoff\uC6A9 controlled detail\uC744 \uAC80\uC99D\uD569\uB2C8\uB2E4.\n- \uD655\uC778\uD55C \uAC83: bad \uC0D8\uD50C \uAC70\uBD80 \uADDC\uCE59\uC740 \uADF8\uB300\uB85C\uC785\uB2C8\uB2E4.',
121
+ blocker: '`scripts/output-style-smoke.mjs`\uB97C \uCC3E\uC744 \uC218 \uC5C6\uC5B4 \uBCC0\uACBD\uC774 \uB9C9\uD614\uC2B5\uB2C8\uB2E4.',
122
+ semicolon: '\uC2A4\uBAA8\uD06C \uC2A4\uD06C\uB9BD\uD2B8\uB97C \uAC31\uC2E0\uD588\uACE0; Simple \uACC4\uC57D\uC5D0 \uB9DE\uB294 \uC608\uC2DC \uC751\uB2F5\uB9CC \uD1B5\uACFC\uD569\uB2C8\uB2E4.',
123
123
  };
124
124
  for (const [name, output] of Object.entries(goodOutputs)) assertCleanOutput(name, output, { allowedLabels: DEFAULT_REPORT_LABELS });
125
125
 
126
126
  const badOutputs = {
127
127
  heading: '## Summary\nDone.',
128
128
  label: 'Changed: updated default style.',
129
- koreanLabel: '변경: default 스타일을 수정했습니다.\n검증: 스모크를 통과했습니다.',
129
+ koreanLabel: '\uBCC0\uACBD: default \uC2A4\uD0C0\uC77C\uC744 \uC218\uC815\uD588\uC2B5\uB2C8\uB2E4.\n\uAC80\uC99D: \uC2A4\uBAA8\uD06C\uB97C \uD1B5\uACFC\uD588\uC2B5\uB2C8\uB2E4.',
130
130
  numbered: '1. Updated style\n2. Ran smoke',
131
131
  paragraphs: 'Done.\n\nDone again.',
132
132
  nested: '- Updated\n - Nested detail',
133
- ack: '네, default 스타일을 수정했습니다.',
134
- timing: 'Mapped for 2m 32s\n\ndefault 스타일을 수정했습니다.',
135
- tooManySentences: '수정했습니다. 검증했습니다. 보고했습니다.',
136
- mixed: '수정했습니다.\n- 검증했습니다.',
137
- tooManyBullets: '- 바뀐 점: A\n- 확인한 것: B\n- 다음 단계: C\n- 남은 리스크/다음 단계: D',
133
+ ack: '\uB124, default \uC2A4\uD0C0\uC77C\uC744 \uC218\uC815\uD588\uC2B5\uB2C8\uB2E4.',
134
+ timing: 'Mapped for 2m 32s\n\ndefault \uC2A4\uD0C0\uC77C\uC744 \uC218\uC815\uD588\uC2B5\uB2C8\uB2E4.',
135
+ tooManySentences: '\uC218\uC815\uD588\uC2B5\uB2C8\uB2E4. \uAC80\uC99D\uD588\uC2B5\uB2C8\uB2E4. \uBCF4\uACE0\uD588\uC2B5\uB2C8\uB2E4.',
136
+ mixed: '\uC218\uC815\uD588\uC2B5\uB2C8\uB2E4.\n- \uAC80\uC99D\uD588\uC2B5\uB2C8\uB2E4.',
137
+ tooManyBullets: '- \uBC14\uB010 \uC810: A\n- \uD655\uC778\uD55C \uAC83: B\n- \uB2E4\uC74C \uB2E8\uACC4: C\n- \uB0A8\uC740 \uB9AC\uC2A4\uD06C/\uB2E4\uC74C \uB2E8\uACC4: D',
138
138
  };
139
139
  for (const [name, output] of Object.entries(badOutputs)) {
140
140
  let failed = false;