mixdog 0.9.4 → 0.9.6

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 (121) hide show
  1. package/README.md +112 -38
  2. package/package.json +4 -1
  3. package/scripts/bench/r5-orchestrated-task.json +7 -0
  4. package/scripts/bench/round-r13-clientmeta.json +464 -0
  5. package/scripts/bench/round-r14-betafeatures.json +466 -0
  6. package/scripts/bench/round-r15-fulldefault.json +462 -0
  7. package/scripts/bench/round-r16-sessionid.json +466 -0
  8. package/scripts/bench/round-r17-wirebytes.json +456 -0
  9. package/scripts/bench/round-r18-prewarm.json +468 -0
  10. package/scripts/bench/round-r19-clean.json +472 -0
  11. package/scripts/bench/round-r20-prewarm-clean.json +475 -0
  12. package/scripts/bench/round-r21-delta-retry.json +473 -0
  13. package/scripts/bench/round-r22-full-probe.json +693 -0
  14. package/scripts/bench/round-r23-itemprobe.json +701 -0
  15. package/scripts/bench/round-r24-shapefix.json +677 -0
  16. package/scripts/bench/round-r25-serial.json +464 -0
  17. package/scripts/bench/round-r26-parallel3.json +671 -0
  18. package/scripts/bench/round-r27-parallel10.json +894 -0
  19. package/scripts/bench/round-r28-parallel10-stagger.json +882 -0
  20. package/scripts/bench/round-r29-parallel10-stagger166.json +886 -0
  21. package/scripts/bench/round-r30-instid.json +253 -0
  22. package/scripts/bench/round-r31-upgradeprobe.json +256 -0
  23. package/scripts/bench/round-r32-vs-codex-lead.json +254 -0
  24. package/scripts/bench/round-r33-vs-codex-codex.json +115 -0
  25. package/scripts/bench/round-r34-orchestrated.json +120 -0
  26. package/scripts/bench/round-r35-orchestrated-codex.json +61 -0
  27. package/scripts/bench/round-r36-orchestrated-capped.json +128 -0
  28. package/scripts/bench-run.mjs +40 -7
  29. package/scripts/internal-comms-bench.mjs +2 -4
  30. package/scripts/model-catalog-audit.mjs +209 -0
  31. package/scripts/model-list-sanitize-test.mjs +37 -0
  32. package/scripts/recall-bench.mjs +76 -13
  33. package/scripts/recall-quality-cases.json +12 -0
  34. package/scripts/tool-smoke.mjs +2 -2
  35. package/src/agents/heavy-worker/AGENT.md +7 -7
  36. package/src/agents/scheduler-task/AGENT.md +2 -3
  37. package/src/agents/webhook-handler/AGENT.md +2 -3
  38. package/src/agents/worker/AGENT.md +9 -10
  39. package/src/app.mjs +12 -1
  40. package/src/headless-role.mjs +7 -1
  41. package/src/mixdog-session-runtime.mjs +15 -14
  42. package/src/output-styles/default.md +14 -18
  43. package/src/output-styles/minimal.md +4 -5
  44. package/src/output-styles/simple.md +7 -8
  45. package/src/rules/agent/00-common.md +6 -5
  46. package/src/rules/agent/30-explorer.md +16 -5
  47. package/src/rules/lead/01-general.md +5 -5
  48. package/src/rules/lead/lead-brief.md +6 -3
  49. package/src/rules/lead/lead-tool.md +6 -4
  50. package/src/rules/shared/01-tool.md +17 -21
  51. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +8 -3
  52. package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +19 -0
  53. package/src/runtime/agent/orchestrator/providers/model-list-sanitize.mjs +11 -3
  54. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +9 -1
  55. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +53 -9
  56. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +23 -10
  57. package/src/runtime/agent/orchestrator/providers/openai-ws-delta.mjs +11 -9
  58. package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +33 -4
  59. package/src/runtime/agent/orchestrator/providers/provider-catalog-cache.mjs +80 -0
  60. package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +93 -0
  61. package/src/runtime/agent/orchestrator/session/loop.mjs +27 -5
  62. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +1 -1
  63. package/src/runtime/agent/orchestrator/stall-policy.mjs +20 -1
  64. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +7 -7
  65. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +1 -1
  66. package/src/runtime/channels/backends/discord-gateway.mjs +2 -2
  67. package/src/runtime/channels/backends/discord.mjs +15 -26
  68. package/src/runtime/channels/backends/telegram.mjs +8 -12
  69. package/src/runtime/channels/index.mjs +23 -43
  70. package/src/runtime/channels/lib/backend-dispatch.mjs +5 -3
  71. package/src/runtime/channels/lib/config.mjs +37 -149
  72. package/src/runtime/channels/lib/event-pipeline.mjs +4 -4
  73. package/src/runtime/channels/lib/event-queue.mjs +15 -9
  74. package/src/runtime/channels/lib/inbound-routing.mjs +2 -39
  75. package/src/runtime/channels/lib/interaction-workflows.mjs +5 -113
  76. package/src/runtime/channels/lib/scheduler.mjs +27 -113
  77. package/src/runtime/channels/lib/webhook/deliveries.mjs +3 -2
  78. package/src/runtime/channels/lib/webhook.mjs +8 -47
  79. package/src/runtime/memory/index.mjs +9 -0
  80. package/src/runtime/memory/lib/core-memory-store.mjs +5 -1
  81. package/src/runtime/memory/lib/ko-morph.mjs +195 -0
  82. package/src/runtime/memory/lib/memory-recall-store.mjs +47 -10
  83. package/src/runtime/memory/lib/memory-text-utils.mjs +46 -0
  84. package/src/runtime/memory/lib/query-handlers.mjs +27 -5
  85. package/src/runtime/memory/tool-defs.mjs +2 -2
  86. package/src/runtime/shared/config.mjs +14 -4
  87. package/src/runtime/shared/markdown-frontmatter.mjs +19 -0
  88. package/src/runtime/shared/schedules-store.mjs +13 -3
  89. package/src/runtime/shared/tool-execution-contract.mjs +2 -2
  90. package/src/session-runtime/config-helpers.mjs +45 -10
  91. package/src/session-runtime/quick-model-rows.mjs +46 -11
  92. package/src/session-runtime/quick-search-models.mjs +28 -27
  93. package/src/session-runtime/settings-api.mjs +34 -1
  94. package/src/session-runtime/tool-defs.mjs +1 -1
  95. package/src/session-runtime/workflow.mjs +1 -1
  96. package/src/standalone/agent-tool/tool-def.mjs +1 -1
  97. package/src/standalone/agent-tool.mjs +30 -0
  98. package/src/standalone/channel-admin.mjs +102 -90
  99. package/src/standalone/explore-tool.mjs +39 -10
  100. package/src/tui/App.jsx +131 -28
  101. package/src/tui/app/app-format.mjs +11 -4
  102. package/src/tui/app/channel-pickers.mjs +2 -4
  103. package/src/tui/app/maintenance-pickers.mjs +60 -28
  104. package/src/tui/app/model-options.mjs +5 -1
  105. package/src/tui/app/settings-picker.mjs +0 -1
  106. package/src/tui/app/transcript-window.mjs +8 -2
  107. package/src/tui/app/use-transcript-scroll.mjs +3 -1
  108. package/src/tui/app/use-transcript-window.mjs +19 -1
  109. package/src/tui/components/ConfirmBar.jsx +10 -7
  110. package/src/tui/components/Picker.jsx +32 -11
  111. package/src/tui/components/PromptInput.jsx +10 -1
  112. package/src/tui/components/StatusLine.jsx +6 -3
  113. package/src/tui/dist/index.mjs +441 -293
  114. package/src/tui/engine.mjs +48 -12
  115. package/src/tui/index.jsx +11 -52
  116. package/src/ui/statusline.mjs +10 -0
  117. package/src/workflows/default/WORKFLOW.md +15 -17
  118. package/src/workflows/sequential/WORKFLOW.md +15 -17
  119. package/vendor/ink/build/ink.js +9 -1
  120. package/scripts/recall-usecase-probe.json +0 -6
  121. package/src/runtime/channels/lib/holidays.mjs +0 -138
@@ -173,8 +173,12 @@ export async function createEngineSession({
173
173
  if (!status) return;
174
174
  const displayWindow = Number(status.contextWindow || 0);
175
175
  const compactBoundary = Number(status.compaction?.boundaryTokens || 0);
176
+ // Prefer the resolved trigger (boundary - buffer): the statusline uses it
177
+ // as the display denominator so context % reads 100% exactly when
178
+ // auto-compact fires, instead of stalling at ~90% of the boundary.
176
179
  const autoCompact = Number(
177
- status.compaction?.autoCompactTokenLimit
180
+ status.compaction?.triggerTokens
181
+ || status.compaction?.autoCompactTokenLimit
178
182
  || runtime.session?.autoCompactTokenLimit
179
183
  || 0,
180
184
  );
@@ -655,7 +659,13 @@ export async function createEngineSession({
655
659
  // cards are pushed alongside the result-bearing one before their own delay
656
660
  // elapses and would otherwise paint an empty reserved band.
657
661
  // Mirrors components/ToolExecution.jsx TOOL_PENDING_SHOW_DELAY_MS.
658
- const TOOL_CARD_PUSH_DELAY_MS = 1000;
662
+ // [jitter fix] Reserve the tool-card row immediately instead of after a
663
+ // 1000ms delay. Delayed insertion made rows appear late and shove the body
664
+ // vertically; pushing now (with the real 'Running' header height via
665
+ // deferredDisplayReady) keeps the layout stable. The timer path is kept as a
666
+ // 0ms microtask fallback for entries that are registered but not explicitly
667
+ // surfaced, preserving call-order flush semantics.
668
+ const TOOL_CARD_PUSH_DELAY_MS = 0;
659
669
  let deferredSeqCounter = 0;
660
670
  const deferredEntries = []; // creation-order list; each is pushed at most once
661
671
  // Push this entry AND every earlier-created still-deferred entry, in order,
@@ -927,7 +937,7 @@ export async function createEngineSession({
927
937
  // never held back waiting for the next Ink frame. 16ms (~60fps) left every
928
938
  // other Ink frame idle, which made fast provider streams visibly land in
929
939
  // coarse chunks ("10 chars at a time").
930
- const STREAM_BATCH_INTERVAL_MS = 8;
940
+ const STREAM_BATCH_INTERVAL_MS = 16;
931
941
  let _batchTimer = null;
932
942
  let _pendingTextFlush = false; // true when a text/spinner update is queued
933
943
  let _pendingThinkFlush = false; // true when a thinking update is queued
@@ -1155,6 +1165,10 @@ export async function createEngineSession({
1155
1165
  }
1156
1166
 
1157
1167
  const touchedAggregates = new Set();
1168
+ // [jitter fix] Last standalone (Agent) card in this batch to reserve a
1169
+ // row for. Flushed AFTER the syncAggregateHeader loop so any earlier-seq
1170
+ // aggregate it would flush-through already has its pendingSpec built.
1171
+ let standaloneReserve = null;
1158
1172
  for (let i = 0; i < batchCalls.length; i++) {
1159
1173
  const c = batchCalls[i];
1160
1174
  const name = toolCallName(c);
@@ -1207,6 +1221,13 @@ export async function createEngineSession({
1207
1221
  cardByCallId.set(callId, card);
1208
1222
  }
1209
1223
  toolCards.push(card);
1224
+ // [jitter fix] Immediate row-reserve is deferred to after the
1225
+ // syncAggregateHeader loop below (see standaloneReserve): calling
1226
+ // ensureVisible() here would flushDeferredUpTo() every earlier-seq
1227
+ // entry — including an aggregate whose pendingSpec syncAggregateHeader
1228
+ // hasn't built yet — marking it pushed without inserting (lost/
1229
+ // out-of-order card). Record it and flush once headers exist.
1230
+ standaloneReserve = card;
1210
1231
  // A standalone card (Agent) breaks the consecutive run too: a
1211
1232
  // later same-bucket call must open a fresh card BELOW it, not
1212
1233
  // merge into an aggregate above it.
@@ -1234,6 +1255,11 @@ export async function createEngineSession({
1234
1255
  for (const aggregateCard of touchedAggregates) {
1235
1256
  syncAggregateHeader(aggregateCard);
1236
1257
  }
1258
+ // [jitter fix] Now that every touched aggregate has its pendingSpec,
1259
+ // reserve the standalone (Agent) card's row immediately. Its
1260
+ // ensureVisible() flushes earlier-seq entries too, but those aggregates
1261
+ // are now push-ready so none is marked pushed without inserting.
1262
+ standaloneReserve?.ensureVisible?.();
1237
1263
  if (committedAssistantSegment) {
1238
1264
  // A pre-tool assistant preamble has already had one render frame to
1239
1265
  // settle. Do not let the first grouped tool card sit off-screen until
@@ -1280,7 +1306,7 @@ export async function createEngineSession({
1280
1306
  detail: compactEventDetail(event),
1281
1307
  });
1282
1308
  },
1283
- onStageChange: (stage) => {
1309
+ onStageChange: async (stage) => {
1284
1310
  if (!state.spinner) return;
1285
1311
  const value = String(stage || '');
1286
1312
  if (value === 'compacting') {
@@ -1299,6 +1325,7 @@ export async function createEngineSession({
1299
1325
  mode: 'compacting',
1300
1326
  },
1301
1327
  });
1328
+ await yieldToRenderer();
1302
1329
  return;
1303
1330
  }
1304
1331
  if (value === 'requesting' || value === 'streaming') compactingActive = false;
@@ -2225,6 +2252,11 @@ export async function createEngineSession({
2225
2252
  const startedAt = Date.now();
2226
2253
  set({ commandBusy: true, commandStatus: { active: true, verb: 'Compacting conversation', startedAt, mode: 'compacting' } });
2227
2254
  try {
2255
+ // Give Ink one event-loop turn to paint the compacting spinner before
2256
+ // runtime.compact() starts synchronous session/transcript work (same
2257
+ // yield as the auto-clear path; without it /compact looks frozen with
2258
+ // no spinner until the compact already finished).
2259
+ await new Promise((resolve) => setTimeout(resolve, 0));
2228
2260
  const result = await runtime.compact({ recoverAgent: true });
2229
2261
  syncContextStats({ allowEstimated: true });
2230
2262
  set({ ...routeState(), stats: { ...state.stats } });
@@ -2248,6 +2280,15 @@ export async function createEngineSession({
2248
2280
  error: result.error,
2249
2281
  }),
2250
2282
  });
2283
+ } else {
2284
+ // null = session missing/closed: still surface a done row so
2285
+ // /compact never ends silently without a completion marker.
2286
+ pushItem({
2287
+ kind: 'statusdone',
2288
+ id: nextId(),
2289
+ label: 'Compact failed',
2290
+ detail: 'no active session',
2291
+ });
2251
2292
  }
2252
2293
  return result;
2253
2294
  } finally {
@@ -2556,14 +2597,9 @@ export async function createEngineSession({
2556
2597
  pushNotice('webhook/ngrok authtoken forgotten', 'info');
2557
2598
  return result;
2558
2599
  },
2559
- saveChannel: (entry) => {
2560
- const result = runtime.saveChannel(entry);
2561
- pushNotice(`channel saved: ${entry.name}`, 'info');
2562
- return result;
2563
- },
2564
- deleteChannel: (name) => {
2565
- const result = runtime.deleteChannel(name);
2566
- pushNotice(`channel deleted: ${name}`, 'info');
2600
+ setChannel: (entry) => {
2601
+ const result = runtime.setChannel(entry);
2602
+ pushNotice('channel saved', 'info');
2567
2603
  return result;
2568
2604
  },
2569
2605
  setWebhookConfig: (patch) => {
package/src/tui/index.jsx CHANGED
@@ -16,6 +16,7 @@ import { installProcessSignalCleanup } from '../runtime/shared/process-shutdown.
16
16
  import { touchUiHeartbeat } from '../runtime/channels/lib/runtime-paths.mjs';
17
17
  import { emitTerminalBackground, loadThemeSettingFromConfig, theme } from './theme.mjs';
18
18
  import { POP_KITTY, DISABLE_MODIFY_OTHER_KEYS } from './keyboard-protocol.mjs';
19
+ import { displayWidth } from './display-width.mjs';
19
20
 
20
21
  const TERMINAL_MODE_RESET = '\x1b[?1006l\x1b[?1005l\x1b[?1015l\x1b[?1003l\x1b[?1002l\x1b[?1000l\x1b[?2004l\x1b[?25h';
21
22
  const TERMINAL_OSC_RESET_BG = '\x1b]111\x07';
@@ -152,16 +153,18 @@ function ansiFg(rgb) {
152
153
  * the seconds between alt-screen entry and the first ink frame are not a blank
153
154
  * void. The logo is drawn at the SAME rows/columns as App.jsx's welcome banner
154
155
  * (marginTop 3 + 5 logo rows + blank + subtitle), so ink's first real frame
155
- * overprints it in place with no visible jump. The bottom panel silhouette
156
- * carries an animated "Loading projects…" spinner so the boot reads as an
157
- * explicit loading state instead of an empty box that later fills with the
158
- * list. Returns { stop } — call it right before ink mounts.
156
+ * overprints it in place with no visible jump. Do NOT pre-paint the bottom
157
+ * picker/prompt frame here: the first ink frame may hide or move those rows
158
+ * while the initial project picker settles, and stale raw borders look like a
159
+ * broken frame. Returns { stop } — call it right before ink mounts.
159
160
  */
160
161
  function paintBootSplash() {
161
162
  try {
162
163
  const cols = Math.max(1, Number(process.stdout.columns) || 80);
163
164
  const rows = Math.max(1, Number(process.stdout.rows) || 24);
164
- const center = (s) => `${' '.repeat(Math.max(0, Math.floor((cols - s.length) / 2)))}${s}`;
165
+ const windowsLikeTerminal = process.platform === 'win32' || Boolean(process.env.WT_SESSION);
166
+ const frameCols = Math.max(1, cols - (windowsLikeTerminal ? 1 : 0));
167
+ const center = (s) => `${' '.repeat(Math.max(0, Math.floor((frameCols - displayWidth(s)) / 2)))}${s}`;
165
168
  const logo = [
166
169
  '███╗ ███╗██╗██╗ ██╗██████╗ ██████╗ ██████╗ ',
167
170
  '████╗ ████║██║╚██╗██╔╝██╔══██╗██╔═══██╗██╔════╝ ',
@@ -181,56 +184,12 @@ function paintBootSplash() {
181
184
  }
182
185
  out += '\r\n';
183
186
  out += `${subtleFg}${center(`mixdog coding agent · ${process.cwd()}`)}${reset}`;
184
- // Bottom placeholder: the first REAL screen is the project picker — a tall
185
- // floating panel sitting directly above the 3-row statusline — not the
186
- // 3-row prompt box. Painting a short prompt silhouette here made the panel
187
- // look like it "unfolded" from 3 rows to full height on the first ink
188
- // frame. Mirror App.jsx's startup math instead:
189
- // floatingPanelRows = min(17, rows - 15), bottom row = rows - 3.
190
- const floating = Math.min(17, rows - 15);
191
- let spinnerRow = 0;
192
- let spinnerFg = '';
193
- if (floating >= 3) {
194
- const borderFg = ansiFg(theme.promptBorder) || subtleFg;
195
- // Leave the last COLUMN untouched: writing the bottom-right cell makes
196
- // Windows Terminal/conhost scroll the alt screen one row (same reason
197
- // App.jsx reserves rightSafetyColumns), which made the whole splash jump
198
- // when the box was painted at full width on the final row.
199
- const boxW = Math.max(4, cols - 1);
200
- const innerW = boxW - 2;
201
- const topRow = rows - 3 - floating + 1;
202
- out += `\x1b[${topRow};1H${borderFg}╭${'─'.repeat(innerW)}╮`;
203
- for (let i = 1; i < floating - 1; i++) {
204
- out += `\x1b[${topRow + i};1H│${' '.repeat(innerW)}│`;
205
- }
206
- out += `\x1b[${rows - 3};1H╰${'─'.repeat(innerW)}╯${reset}`;
207
- // "Loading projects…" line inside the panel silhouette. Picker.jsx
208
- // renders border, then (paddingX=1, no paddingY) title/help row as its
209
- // very first interior row — not vertically centered. Match that rhythm
210
- // (topRow + 1) instead of centering, so ink's first frame doesn't jump
211
- // the text down/up when it overprints this splash.
212
- spinnerRow = topRow + 1;
213
- spinnerFg = subtleFg;
214
- out += `\x1b[${spinnerRow};3H${subtleFg} Loading projects…${reset}`;
215
- }
187
+
216
188
  // Park the cursor at home so ink's first frame paints top-down over the
217
189
  // splash instead of starting at the bottom row and scrolling the screen.
218
190
  out += '\x1b[H';
219
191
  process.stdout.write(out);
220
- // Animate only the single spinner glyph cell — never reflow anything else,
221
- // so stopping mid-frame can't leave torn output for ink to overwrite.
222
- if (spinnerRow > 0) {
223
- const frames = ['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '⠦', '⠧', '⠇', '⠏'];
224
- let tick = 0;
225
- const timer = setInterval(() => {
226
- try {
227
- process.stdout.write(`\x1b[${spinnerRow};3H${spinnerFg}${frames[tick % frames.length]}${reset}\x1b[H`);
228
- tick += 1;
229
- } catch { /* ignore */ }
230
- }, 80);
231
- timer.unref?.();
232
- return { stop: () => clearInterval(timer) };
233
- }
192
+
234
193
  return { stop: () => {} };
235
194
  } catch { /* cosmetic only — never block boot */ }
236
195
  return { stop: () => {} };
@@ -472,7 +431,7 @@ export async function runTui({ provider, model, toolMode, remote, forceOnboardin
472
431
  // does not exit abruptly. Explicit exits go through /exit or /quit so teardown
473
432
  // still restores the cursor, mouse mode, and alternate screen cleanly.
474
433
  try {
475
- const instance = render(<App store={store} forceOnboarding={forceOnboarding === true} />, { exitOnCtrlC: false, maxFps: 120, onRender: makeRenderProfiler() });
434
+ const instance = render(<App store={store} forceOnboarding={forceOnboarding === true} />, { exitOnCtrlC: false, maxFps: 60, onRender: makeRenderProfiler() });
476
435
  bootProfile('render:mounted', { ms: (performance.now() - startedAt).toFixed(1) });
477
436
  const { waitUntilExit } = instance;
478
437
  // [mixdog fork] Hand the ink renderer's drag-selection setter to the store so
@@ -173,6 +173,16 @@ function resolveContextUsedPct({
173
173
  autoCompactTokenLimit,
174
174
  compact,
175
175
  });
176
+ // Trigger-as-denominator: when a sub-boundary compaction trigger
177
+ // (boundary - buffer) is known, context % is measured against IT so the
178
+ // gauge reads 100% exactly when auto-compact fires instead of stalling at
179
+ // ~90% of the boundary window. The gateway's own pct is computed against
180
+ // the boundary denominator, so it is bypassed in that case.
181
+ const trigger = num(autoCompactTokenLimit);
182
+ const triggerDenominator = trigger > 0 && (!(boundary > 0) || trigger < boundary);
183
+ if (triggerDenominator && numerator > 0) {
184
+ return (numerator / trigger) * 100;
185
+ }
176
186
  const gatewayRawPct = gatewayStatus?.contextUsedPct;
177
187
  if (
178
188
  gatewayStatus
@@ -16,16 +16,15 @@ Lead supervises: delegates, coordinates, judges, decides. Route by complexity:
16
16
  - Debugger: deploy for very high complexity, or when root-causing has already
17
17
  failed at least once.
18
18
 
19
- 1. Plan — discuss the request with the user, form a plan, and wait for approval.
19
+ 1. Plan — discuss the request with the user, form a plan, wait for approval.
20
20
  2. Delegate — split into the maximum number of independent scopes.
21
21
  - PARALLEL across independent scopes by default (implementation, analysis,
22
- review, debugging split alike). Spawn every scope in the SAME turn. Shared
23
- or cross-cutting code does NOT justify merging: split per path and verify
24
- the shared parts yourself. The only single scope is a genuinely
25
- inseparable dependency — then state it.
26
- - SEQUENTIAL within a single complex scope — split it into ordered steps
27
- rather than handing it off in one shot, with a build/test-green gate
28
- between steps.
22
+ review, debugging alike). Spawn every scope in the SAME turn. Shared or
23
+ cross-cutting code does NOT justify merging: split per path and verify the
24
+ shared parts yourself. The only single scope is a genuinely inseparable
25
+ dependency — then state it.
26
+ - SEQUENTIAL within a single complex scope — split into ordered steps rather
27
+ than one shot, with a build/test-green gate between steps.
29
28
  - Write briefs per the Lead brief contract (token-optimized labeled
30
29
  fragments).
31
30
  - After spawning async agent(s), END THE TURN — do not poll, guess, or start
@@ -34,15 +33,14 @@ Lead supervises: delegates, coordinates, judges, decides. Route by complexity:
34
33
  3. Review — pair one reviewer 1:1 with each implementation scope, spawned in the
35
34
  same turn; never defer or batch the reviewer call — wait for its result.
36
35
  Fact-check the agent response and cross-check implementation and review
37
- results yourself before acting on them. Send fixes back to the original
38
- scope and loop verify -> fix -> re-verify until clean. Skip review only for
39
- simple, low-risk tasks. If the user asks for debugging, or a bug survives
40
- 2+ fix cycles, have the debugger investigate first instead of another fix
41
- round.
42
- 4. Report — deliver the final report: synthesize results (outcome + key
43
- evidence; never forward raw agent output), state the final state, and ask the
44
- user whether to ship/deploy when relevant. Only after user feedback with no
45
- issues prepare deploy/build/commit.
36
+ results yourself before acting. Send fixes back to the original scope and
37
+ loop verify -> fix -> re-verify until clean. Skip review only for simple,
38
+ low-risk tasks. If the user asks for debugging, or a bug survives 2+ fix
39
+ cycles, have the debugger investigate first instead of another fix round.
40
+ 4. Report — synthesize results (outcome + key evidence; never forward raw agent
41
+ output), state the final state, and ask whether to ship/deploy when
42
+ relevant. Only after user feedback with no issues prepare
43
+ deploy/build/commit.
46
44
 
47
45
  On any major change or direction shift mid-work, pause and re-consult the user
48
46
  before continuing.
@@ -20,15 +20,13 @@ Lead supervises: delegates, coordinates, judges, decides. Route by complexity:
20
20
  - Debugger: deploy for very high complexity, or when root-causing has already
21
21
  failed at least once.
22
22
 
23
- 1. Plan — discuss the request with the user, form a plan, and wait for approval.
24
- 2. Delegate — split into ordered scopes and hand them off ONE AT A TIME.
23
+ 1. Plan — discuss the request with the user, form a plan, wait for approval.
24
+ 2. Delegate — split into ordered scopes and hand off ONE AT A TIME.
25
25
  - NO PARALLEL, NO FAN-OUT. Even when scopes are independent, spawn exactly
26
- one agent per turn; never spawn multiple agents in the same turn. Wait for
27
- the current scope's completion notification before spawning the next scope
28
- (1:1 sequential).
29
- - SEQUENTIAL within a single complex scope — split it into ordered steps
30
- rather than handing it off in one shot, with a build/test-green gate
31
- between steps.
26
+ one agent per turn; never multiple in the same turn. Wait for the current
27
+ scope's completion notification before spawning the next (1:1 sequential).
28
+ - SEQUENTIAL within a single complex scope — split into ordered steps rather
29
+ than one shot, with a build/test-green gate between steps.
32
30
  - Write briefs per the Lead brief contract (token-optimized labeled
33
31
  fragments).
34
32
  - After spawning an async agent, END THE TURN — do not poll, guess, or start
@@ -37,15 +35,15 @@ Lead supervises: delegates, coordinates, judges, decides. Route by complexity:
37
35
  3. Review — after each implementation scope completes, pair one reviewer 1:1
38
36
  with that scope (spawned on its own turn, not batched); never defer the
39
37
  reviewer call — wait for its result. Fact-check the agent response and
40
- cross-check implementation and review results yourself before acting on
41
- them. Send fixes back to the original scope and loop verify -> fix ->
42
- re-verify until clean. Skip review only for simple, low-risk tasks. If the
43
- user asks for debugging, or a bug survives 2+ fix cycles, have the debugger
44
- investigate first instead of another fix round.
45
- 4. Report — deliver the final report: synthesize results (outcome + key
46
- evidence; never forward raw agent output), state the final state, and ask the
47
- user whether to ship/deploy when relevant. Only after user feedback with no
48
- issues prepare deploy/build/commit.
38
+ cross-check implementation and review results yourself before acting. Send
39
+ fixes back to the original scope and loop verify -> fix -> re-verify until
40
+ clean. Skip review only for simple, low-risk tasks. If the user asks for
41
+ debugging, or a bug survives 2+ fix cycles, have the debugger investigate
42
+ first instead of another fix round.
43
+ 4. Report — synthesize results (outcome + key evidence; never forward raw agent
44
+ output), state the final state, and ask whether to ship/deploy when
45
+ relevant. Only after user feedback with no issues prepare
46
+ deploy/build/commit.
49
47
 
50
48
  On any major change or direction shift mid-work, pause and re-consult the user
51
49
  before continuing.
@@ -106,7 +106,15 @@ const shouldClearTerminalForFrame = ({ isTty, viewportRows, previousViewportRows
106
106
  nextOutputHeight < previousOutputHeight &&
107
107
  (wasFullscreen || isFullscreen || wasOverflowing || isOverflowing);
108
108
  const shouldClearOnUnmount = isUnmounting && wasFullscreen;
109
- if (isWindowsConsole && (wasFullscreen || isFullscreen)) {
109
+ // [jitter fix] Do not full-clear EVERY fullscreen frame on Windows consoles:
110
+ // that repaint-from-scratch caused visible vertical jitter. A full clear is
111
+ // only needed when the geometry actually changes — the viewport was resized
112
+ // (viewportRows differs from the previous frame) or the output height
113
+ // shrank at/over the viewport (stale wrapped rows possible). Otherwise
114
+ // repaint in place via the incremental erase path.
115
+ const viewportResized = previousViewportRows != null && previousViewportRows !== viewportRows;
116
+ if (isWindowsConsole && (wasFullscreen || isFullscreen) &&
117
+ (viewportResized || isShrinkingAtViewport || isLeavingFullscreen)) {
110
118
  return true;
111
119
  }
112
120
  return (
@@ -1,6 +0,0 @@
1
- [
2
- { "id": "t1", "label": "topic ko-en mixed", "args": { "query": "recall period last 개선", "limit": 8 }, "expect": "results" },
3
- { "id": "t2", "label": "topic ko only", "args": { "query": "recall 개선", "limit": 8 }, "expect": "results" },
4
- { "id": "t3", "label": "topic en only", "args": { "query": "recall period last improvement", "limit": 8 }, "expect": "results" },
5
- { "id": "t4", "label": "category no-raw", "args": { "period": "7d", "category": "decision", "limit": 10, "includeRaw": false }, "expect": "browse" }
6
- ]
@@ -1,138 +0,0 @@
1
- import { readFileSync, writeFileSync, mkdirSync, existsSync } from "fs";
2
- import { join, dirname } from "path";
3
- import { homedir } from "os";
4
- import { DATA_DIR } from "./config.mjs";
5
- // Computed lazily (not at module top) so this module can be imported in a
6
- // cycle with config.mjs without touching DATA_DIR before config.mjs has
7
- // finished evaluating it (ESM temporal-dead-zone safety).
8
- function cacheFile() {
9
- return join(DATA_DIR, "holidays-cache.json");
10
- }
11
- const FALLBACK_FILE = join(homedir(), ".mixdog", "schedules", "holidays.json");
12
- const CACHE_MAX_AGE_MS = 30 * 24 * 60 * 60 * 1e3;
13
- // In-memory, pre-warmed holiday cache for SYNC reads (isHolidaySync).
14
- // Keyed by `${year}:${countryCode}` -> Set<dateStr>. Absent key == cold
15
- // (not yet warmed); callers fall back to their documented cold-cache
16
- // behavior until a warm completes.
17
- const memCache = /* @__PURE__ */ new Map();
18
- const warming = /* @__PURE__ */ new Set();
19
- function cacheKey(year, countryCode) {
20
- return `${year}:${countryCode}`;
21
- }
22
- function holidaySet(holidays) {
23
- return new Set((holidays || []).map((h) => h.date));
24
- }
25
- async function fetchHolidays(year, countryCode) {
26
- const url = `https://date.nager.at/api/v3/publicholidays/${year}/${countryCode}`;
27
- const res = await fetch(url, { signal: AbortSignal.timeout(10_000) });
28
- if (!res.ok) throw new Error(`Nager API ${res.status}: ${res.statusText}`);
29
- return res.json();
30
- }
31
- function loadCache(year, countryCode) {
32
- try {
33
- const file = cacheFile();
34
- if (!existsSync(file)) return null;
35
- const cache = JSON.parse(readFileSync(file, "utf8"));
36
- if (cache.year !== year || cache.countryCode !== countryCode) return null;
37
- if (Date.now() - cache.fetchedAt > CACHE_MAX_AGE_MS) return null;
38
- return cache.holidays;
39
- } catch {
40
- return null;
41
- }
42
- }
43
- function saveCache(year, countryCode, holidays) {
44
- const cache = { year, countryCode, fetchedAt: Date.now(), holidays };
45
- try {
46
- const file = cacheFile();
47
- mkdirSync(dirname(file), { recursive: true });
48
- writeFileSync(file, JSON.stringify(cache, null, 2));
49
- } catch {
50
- }
51
- }
52
- function loadFallback() {
53
- try {
54
- if (!existsSync(FALLBACK_FILE)) return /* @__PURE__ */ new Set();
55
- const data = JSON.parse(readFileSync(FALLBACK_FILE, "utf8"));
56
- const dates = data.holidays ?? [];
57
- return new Set(dates);
58
- } catch {
59
- return /* @__PURE__ */ new Set();
60
- }
61
- }
62
- async function isHoliday(date, countryCode) {
63
- const year = date.getFullYear();
64
- const dateStr = `${year}-${String(date.getMonth() + 1).padStart(2, "0")}-${String(date.getDate()).padStart(2, "0")}`;
65
- let holidays = loadCache(year, countryCode);
66
- if (!holidays) {
67
- try {
68
- holidays = await fetchHolidays(year, countryCode);
69
- saveCache(year, countryCode, holidays);
70
- } catch (err) {
71
- process.stderr.write(`mixdog holidays: API fetch failed: ${err}
72
- `);
73
- holidays = null;
74
- }
75
- }
76
- if (holidays) {
77
- return holidays.some((h) => h.date === dateStr);
78
- }
79
- const fallback = loadFallback();
80
- return fallback.has(dateStr);
81
- }
82
- // Synchronous, non-blocking holiday lookup backed by the pre-warmed
83
- // in-memory cache. `dateStr` is a plain 'YYYY-MM-DD' string (the same
84
- // representation warmHolidays stores), so there is no Date round-trip to
85
- // shift the day across host offsets. Returns:
86
- // true — date is a recognized public holiday (cache warm),
87
- // false — date is NOT a holiday (cache warm),
88
- // null — cache is cold (not warmed yet); the async warm is kicked off
89
- // and the caller should apply its documented cold-cache
90
- // fallback. Never performs network OR blocking disk I/O — the
91
- // cold path consults the in-memory cache only.
92
- function isHolidaySync(dateStr, countryCode) {
93
- if (!countryCode) return false;
94
- const year = Number(dateStr.slice(0, 4));
95
- const key = cacheKey(year, countryCode);
96
- const set = memCache.get(key);
97
- if (set === void 0) {
98
- // Cold: no synchronous disk probe here. Kick off the async warm
99
- // (which does any on-disk/network lookup off the sync path) and
100
- // report cold so the caller applies its documented fallback.
101
- warmHolidays(dateStr, countryCode);
102
- return null;
103
- }
104
- return set.has(dateStr);
105
- }
106
- // Asynchronously populate the in-memory sync cache for the given
107
- // dateStr's year/country (from the on-disk cache when fresh, else Nager).
108
- // `dateStr` is a plain 'YYYY-MM-DD' string. Idempotent and
109
- // self-deduplicating; failures leave the entry cold so the next sync
110
- // read retries.
111
- async function warmHolidays(dateStr, countryCode) {
112
- if (!countryCode) return;
113
- const year = Number(dateStr.slice(0, 4));
114
- const key = cacheKey(year, countryCode);
115
- if (memCache.has(key) || warming.has(key)) return;
116
- warming.add(key);
117
- try {
118
- // Yield once before touching disk so the cold sync caller that
119
- // triggered this warm is never blocked by the loadCache file read.
120
- await Promise.resolve();
121
- let holidays = loadCache(year, countryCode);
122
- if (!holidays) {
123
- holidays = await fetchHolidays(year, countryCode);
124
- saveCache(year, countryCode, holidays);
125
- }
126
- memCache.set(key, holidaySet(holidays));
127
- } catch (err) {
128
- process.stderr.write(`mixdog holidays: warm failed: ${err}
129
- `);
130
- } finally {
131
- warming.delete(key);
132
- }
133
- }
134
- export {
135
- isHoliday,
136
- isHolidaySync,
137
- warmHolidays
138
- };