mixdog 0.9.22 → 0.9.24

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 (132) hide show
  1. package/README.md +1 -4
  2. package/package.json +1 -1
  3. package/scripts/boot-smoke.mjs +1 -1
  4. package/scripts/channel-daemon-smoke.mjs +483 -0
  5. package/scripts/channel-daemon-stub.mjs +80 -0
  6. package/scripts/debounced-skills-async-save-test.mjs +57 -0
  7. package/scripts/explore-bench-tmp.mjs +17 -0
  8. package/scripts/find-fuzzy-hidden-test.mjs +145 -0
  9. package/scripts/mcp-grace-deferred-test.mjs +149 -0
  10. package/scripts/statusline-quota-hysteresis-test.mjs +37 -0
  11. package/scripts/tool-smoke.mjs +68 -47
  12. package/src/rules/agent/30-explorer.md +6 -0
  13. package/src/rules/shared/01-tool.md +11 -4
  14. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +76 -1
  15. package/src/runtime/agent/orchestrator/config.mjs +33 -7
  16. package/src/runtime/agent/orchestrator/context/collect.mjs +43 -8
  17. package/src/runtime/agent/orchestrator/mcp/child-tree.mjs +226 -0
  18. package/src/runtime/agent/orchestrator/mcp/client.mjs +184 -33
  19. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +38 -1
  20. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +39 -1
  21. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +1 -1
  22. package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +5 -3
  23. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +2 -2
  24. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +2 -2
  25. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +1 -1
  26. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +24 -7
  27. package/src/runtime/agent/orchestrator/session/loop/completion-guards.mjs +3 -2
  28. package/src/runtime/agent/orchestrator/session/loop/deferred-call-through.mjs +6 -3
  29. package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +1 -1
  30. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +37 -4
  31. package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +11 -1
  32. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +14 -3
  33. package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +40 -6
  34. package/src/runtime/agent/orchestrator/session/store.mjs +30 -14
  35. package/src/runtime/agent/orchestrator/session/tool-batch.mjs +2 -1
  36. package/src/runtime/agent/orchestrator/tools/bash-session.mjs +13 -5
  37. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +62 -24
  38. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +7 -6
  39. package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +20 -0
  40. package/src/runtime/agent/orchestrator/tools/builtin/fuzzy-match.mjs +34 -3
  41. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +220 -27
  42. package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +61 -0
  43. package/src/runtime/agent/orchestrator/tools/builtin/shell-job-paths.mjs +1 -1
  44. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +67 -27
  45. package/src/runtime/agent/orchestrator/tools/builtin/shell-runtime.mjs +6 -5
  46. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +54 -5
  47. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +97 -54
  48. package/src/runtime/agent/orchestrator/tools/patch/paths.mjs +49 -31
  49. package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +35 -2
  50. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +126 -22
  51. package/src/runtime/channels/lib/crash-log.mjs +4 -2
  52. package/src/runtime/channels/lib/output-forwarder.mjs +2 -1
  53. package/src/runtime/channels/lib/owned-runtime.mjs +67 -223
  54. package/src/runtime/channels/lib/owner-heartbeat.mjs +9 -62
  55. package/src/runtime/channels/lib/parent-bridge.mjs +16 -1
  56. package/src/runtime/channels/lib/runtime-paths.mjs +32 -113
  57. package/src/runtime/channels/lib/session-discovery.mjs +2 -1
  58. package/src/runtime/channels/lib/tool-dispatch.mjs +22 -14
  59. package/src/runtime/channels/lib/tool-format.mjs +7 -2
  60. package/src/runtime/channels/lib/worker-main.mjs +42 -30
  61. package/src/runtime/memory/index.mjs +54 -7
  62. package/src/runtime/memory/lib/embedding-warmup.mjs +7 -0
  63. package/src/runtime/memory/lib/pg/process.mjs +85 -40
  64. package/src/runtime/memory/lib/pg/supervisor.mjs +82 -17
  65. package/src/runtime/memory/lib/query-handlers.mjs +4 -1
  66. package/src/runtime/memory/lib/recall-format.mjs +7 -3
  67. package/src/runtime/memory/tool-defs.mjs +1 -1
  68. package/src/runtime/shared/atomic-file.mjs +150 -6
  69. package/src/runtime/shared/background-tasks.mjs +1 -1
  70. package/src/runtime/shared/config.mjs +53 -1
  71. package/src/runtime/shared/tool-execution-contract.mjs +1 -1
  72. package/src/runtime/shared/tool-primitives.mjs +31 -1
  73. package/src/runtime/shared/tool-surface.mjs +42 -13
  74. package/src/runtime/shared/update-checker.mjs +3 -0
  75. package/src/runtime/shared/user-data-guard.mjs +66 -0
  76. package/src/session-runtime/config-lifecycle.mjs +221 -20
  77. package/src/session-runtime/lifecycle-api.mjs +9 -0
  78. package/src/session-runtime/mcp-glue.mjs +93 -1
  79. package/src/session-runtime/resource-api.mjs +62 -8
  80. package/src/session-runtime/runtime-core.mjs +118 -4
  81. package/src/session-runtime/session-text.mjs +41 -0
  82. package/src/session-runtime/session-turn-api.mjs +50 -0
  83. package/src/session-runtime/settings-api.mjs +8 -1
  84. package/src/session-runtime/tool-catalog.mjs +350 -38
  85. package/src/session-runtime/tool-defs.mjs +7 -7
  86. package/src/session-runtime/workflow.mjs +2 -1
  87. package/src/standalone/channel-admin.mjs +32 -3
  88. package/src/standalone/channel-daemon-client.mjs +226 -0
  89. package/src/standalone/channel-daemon-transport.mjs +545 -0
  90. package/src/standalone/channel-daemon.mjs +176 -0
  91. package/src/standalone/channel-worker.mjs +224 -4
  92. package/src/standalone/explore-tool.mjs +87 -15
  93. package/src/standalone/hook-bus.mjs +71 -3
  94. package/src/tui/App.jsx +107 -19
  95. package/src/tui/app/clipboard.mjs +39 -19
  96. package/src/tui/app/doctor.mjs +57 -0
  97. package/src/tui/app/extension-pickers.mjs +53 -9
  98. package/src/tui/app/maintenance-pickers.mjs +0 -5
  99. package/src/tui/app/slash-dispatch.mjs +4 -4
  100. package/src/tui/app/text-layout.mjs +11 -0
  101. package/src/tui/app/use-mouse-input.mjs +235 -51
  102. package/src/tui/app/use-prompt-handlers.mjs +49 -30
  103. package/src/tui/app/use-transcript-scroll.mjs +124 -27
  104. package/src/tui/app/use-transcript-window.mjs +55 -1
  105. package/src/tui/components/Message.jsx +1 -1
  106. package/src/tui/components/PromptInput.jsx +3 -1
  107. package/src/tui/components/QueuedCommands.jsx +21 -10
  108. package/src/tui/components/StatusLine.jsx +3 -3
  109. package/src/tui/components/ToolExecution.jsx +16 -4
  110. package/src/tui/components/TranscriptItem.jsx +1 -1
  111. package/src/tui/dist/index.mjs +820 -326
  112. package/src/tui/engine/agent-job-feed.mjs +5 -0
  113. package/src/tui/engine/notification-plan.mjs +5 -0
  114. package/src/tui/engine/session-api.mjs +23 -8
  115. package/src/tui/engine/session-flow.mjs +6 -0
  116. package/src/tui/engine/tool-card-results.mjs +14 -5
  117. package/src/tui/engine/turn.mjs +9 -2
  118. package/src/tui/engine.mjs +32 -5
  119. package/src/tui/index.jsx +62 -18
  120. package/src/tui/paste-attachments.mjs +26 -0
  121. package/src/ui/statusline-agents.mjs +36 -0
  122. package/src/ui/statusline.mjs +60 -10
  123. package/src/ui/tool-card.mjs +8 -1
  124. package/src/vendor/statusline/bin/statusline-route.mjs +23 -7
  125. package/src/workflows/bench/WORKFLOW.md +46 -0
  126. package/src/workflows/default/WORKFLOW.md +6 -0
  127. package/src/workflows/solo/WORKFLOW.md +5 -0
  128. package/vendor/ink/build/ink.js +23 -1
  129. package/vendor/ink/build/output.js +154 -71
  130. package/vendor/ink/build/render-node-to-output.js +44 -2
  131. package/vendor/ink/build/render.js +4 -0
  132. package/vendor/ink/build/renderer.js +4 -1
@@ -23,7 +23,7 @@ import {
23
23
  } from './statusline-format.mjs';
24
24
  import { shellJobsStatus, memoryCycleStatus } from './statusline-segments.mjs';
25
25
  import {
26
- summarizeWorkerTags, agentStatuslinePayload, classifyAgentWorkers, activeHiddenAgentWorkers,
26
+ summarizeWorkerTags, agentStatuslinePayload, classifyAgentWorkers, activeHiddenAgentWorkers, agentWebSearchStatus,
27
27
  } from './statusline-agents.mjs';
28
28
  export { createSessionStats, applyUsageDelta } from './session-stats.mjs';
29
29
  // Facade re-exports: keep these public symbols resolving from statusline.mjs.
@@ -90,6 +90,28 @@ function rememberNonEmptyQuotaSegments(key, segments) {
90
90
  _lastNonEmptyQuotaSegmentsByKey.delete(oldestKey);
91
91
  }
92
92
  }
93
+
94
+ // Monotonic hysteresis for the quota/usage segment. Once a value has rendered
95
+ // (held), a new non-empty result replaces it ONLY when it is at least as fresh
96
+ // or is confirmed own-instance live data. This stops the 5H/7D values flapping
97
+ // when another mixdog instance overwrites the shared active-instance/usage cache
98
+ // with an OLDER snapshot: metricsMatch flips false, the source alternates to a
99
+ // provider-wide cache snapshot captured at a different time, and without this
100
+ // gate the two sources would oscillate tick-to-tick.
101
+ // - nothing displayed yet .................. accept
102
+ // - incoming is own-instance live data ..... accept (always wins)
103
+ // - either side lacks a comparable asOf .... accept (preserves prior behavior)
104
+ // - displayed value is own live data ....... accept only a STRICTLY newer snapshot
105
+ // - both shared-cache snapshots ............ accept same-or-newer asOf
106
+ export function acceptQuotaSnapshot(held, incoming) {
107
+ if (!held) return true;
108
+ if (incoming && incoming.owned) return true;
109
+ const incomingAsOf = num(incoming && incoming.asOf);
110
+ const heldAsOf = num(held.asOf);
111
+ if (!incomingAsOf || !heldAsOf) return true;
112
+ if (held.owned) return incomingAsOf > heldAsOf;
113
+ return incomingAsOf >= heldAsOf;
114
+ }
93
115
  // Option A boot gate: the L1 usage/quota segment stays fully empty until THIS
94
116
  // process has captured its FIRST confirmed (current-process) OAuth usage
95
117
  // snapshot for THAT provider. The latch is monotonic PER PROVIDER — once a
@@ -310,10 +332,21 @@ function renderNativeStatusline({
310
332
  if (usageReady && _oauthUsageArmedProviders.has(normalizedHoldProvider)) {
311
333
  const holdKey = quotaSegmentsHoldKey({ provider, model, effort, fast, sessionId, clientHostPid });
312
334
  if (quotaSegments.length) {
313
- rememberNonEmptyQuotaSegments(holdKey, quotaSegments);
335
+ // Monotonic replace: keep the currently displayed value unless the new
336
+ // one is same-or-newer, or is confirmed own-instance live data.
337
+ const incoming = {
338
+ asOf: num(quotaStatus?.quotaWindowsAsOf),
339
+ owned: quotaStatus?.quotaWindowsOwned === true,
340
+ };
341
+ const held = _lastNonEmptyQuotaSegmentsByKey.get(holdKey);
342
+ if (acceptQuotaSnapshot(held, incoming)) {
343
+ rememberNonEmptyQuotaSegments(holdKey, { segments: quotaSegments, asOf: incoming.asOf, owned: incoming.owned });
344
+ } else if (held?.segments?.length) {
345
+ quotaSegments = held.segments;
346
+ }
314
347
  } else {
315
348
  const held = _lastNonEmptyQuotaSegmentsByKey.get(holdKey);
316
- if (held && held.length) quotaSegments = held;
349
+ if (held?.segments?.length) quotaSegments = held.segments;
317
350
  }
318
351
  }
319
352
  for (const seg of quotaSegments) addL1(seg);
@@ -329,7 +362,9 @@ function renderNativeStatusline({
329
362
  const sp = l2SpinnerFrame(spinnerNow);
330
363
  const spin = `${GRN}${sp}${R}`;
331
364
  const elapsedSuffix = (label) => (label ? ` ${D}·${R} ${label}` : '');
332
- // Segment order: Running Agents → Exploring → Searching → Running Shells.
365
+ // Segment order: Running Agents → Exploring → Web Searching → Running Shells.
366
+ // (activeTools.search counts WEB searches — category 'Web Research' — not
367
+ // local file search, which is intentionally not surfaced.)
333
368
  if (runningWorkers.length) {
334
369
  const n = runningWorkers.length;
335
370
  const label = `Running ${n} Agent${n === 1 ? '' : 's'}`;
@@ -349,9 +384,17 @@ function renderNativeStatusline({
349
384
  const elapsed = num(exploreInfo.startedAt) > 0 ? formatElapsed(Date.now() - num(exploreInfo.startedAt)) : '';
350
385
  addL2(`${spin} ${B}Exploring${R}${elapsedSuffix(elapsed)}`);
351
386
  }
352
- if (searchInfo && num(searchInfo.count) > 0) {
353
- const elapsed = num(searchInfo.startedAt) > 0 ? formatElapsed(Date.now() - num(searchInfo.startedAt)) : '';
354
- addL2(`${spin} ${B}Searching${R}${elapsedSuffix(elapsed)}`);
387
+ // Web Searching = lead's own web searches (activeTools.search) PLUS any
388
+ // spawned agent sub-session whose current tool call is a web search
389
+ // (agentWebSearchStatus reads the live session-runtime map). Earliest start
390
+ // wins for the elapsed label.
391
+ const agentSearch = agentWebSearchStatus({ sessionId, clientHostPid });
392
+ const webSearchCount = (searchInfo ? num(searchInfo.count) : 0) + num(agentSearch.count);
393
+ if (webSearchCount > 0) {
394
+ const starts = [searchInfo ? num(searchInfo.startedAt) : 0, num(agentSearch.startedAt)].filter((v) => v > 0);
395
+ const searchStart = starts.length ? Math.min(...starts) : 0;
396
+ const elapsed = searchStart > 0 ? formatElapsed(Date.now() - searchStart) : '';
397
+ addL2(`${spin} ${B}Web Searching${R}${elapsedSuffix(elapsed)}`);
355
398
  }
356
399
  if (shellStatus.count > 0) {
357
400
  const n = shellStatus.count;
@@ -530,6 +573,10 @@ function fallbackQuotaStatus({ provider, model } = {}) {
530
573
  value = {
531
574
  ...routeInfo,
532
575
  quotaWindows: limits.quotaWindows || [],
576
+ // Shared provider-wide OAuth usage cache snapshot: not owned by
577
+ // this instance. asOf = the snapshot's cachedAt for hysteresis.
578
+ quotaWindowsAsOf: num(usageSnapshot?.cachedAt),
579
+ quotaWindowsOwned: false,
533
580
  balance: limits.balance || null,
534
581
  routeSpend: limits.routeSpend || null,
535
582
  };
@@ -556,12 +603,15 @@ function providerKindForQuota(provider) {
556
603
  function mergeQuotaStatus(primary, fallback) {
557
604
  if (!primary) return fallback || null;
558
605
  if (!fallback) return primary;
606
+ const usePrimaryWindows = Array.isArray(primary.quotaWindows) && primary.quotaWindows.length;
559
607
  return {
560
608
  ...fallback,
561
609
  ...primary,
562
- quotaWindows: Array.isArray(primary.quotaWindows) && primary.quotaWindows.length
563
- ? primary.quotaWindows
564
- : (fallback.quotaWindows || []),
610
+ quotaWindows: usePrimaryWindows ? primary.quotaWindows : (fallback.quotaWindows || []),
611
+ // Keep asOf/owned aligned with whichever windows won, so the hysteresis gate
612
+ // compares against the timestamp of the value actually being rendered.
613
+ quotaWindowsAsOf: usePrimaryWindows ? num(primary.quotaWindowsAsOf) : num(fallback.quotaWindowsAsOf),
614
+ quotaWindowsOwned: usePrimaryWindows ? primary.quotaWindowsOwned === true : fallback.quotaWindowsOwned === true,
565
615
  balance: primary.balance || fallback.balance || null,
566
616
  routeSpend: primary.routeSpend || fallback.routeSpend || null,
567
617
  providerKind: primary.providerKind || fallback.providerKind || providerKindForQuota(primary.provider || fallback.provider),
@@ -9,6 +9,7 @@
9
9
  * malformed argument objects (the engine hands us `{ name, arguments, id }`).
10
10
  */
11
11
  import { bold, cyan, gray } from './ansi.mjs';
12
+ import { parseMcpToolName, isSelfMcpToolName } from '../runtime/shared/tool-primitives.mjs';
12
13
 
13
14
  const MAX_SUMMARY = 72;
14
15
 
@@ -38,13 +39,19 @@ export function renderToolCard(call) {
38
39
 
39
40
  const summary = safeSummary(name, args);
40
41
  const bullet = cyan('>');
41
- const label = bold(name);
42
+ const label = bold(mcpCardLabel(name));
42
43
  if (!summary) return ` ${bullet} ${label}`;
43
44
  return ` ${bullet} ${label} ${gray(truncate(summary, MAX_SUMMARY))}`;
44
45
  }
45
46
 
46
47
  // --- helpers -----------------------------------------------------------------
47
48
 
49
+ function mcpCardLabel(name) {
50
+ const mcp = parseMcpToolName(name);
51
+ if (!mcp || isSelfMcpToolName(name)) return name;
52
+ return `MCP ${mcp.server}.${mcp.tool}`;
53
+ }
54
+
48
55
  function safeSummary(name, args) {
49
56
  try {
50
57
  const fn = SUMMARIZERS[name];
@@ -343,27 +343,30 @@ function cachedQuotaWindowsFallback(provider, model) {
343
343
  // OAuth quota is provider/account-scoped, not model-scoped. Prefer the
344
344
  // current route key, then provider-wide cache, then the freshest same-provider
345
345
  // route entry so a model switch keeps showing quota before the next live fetch.
346
- if (!provider || !model) return [];
346
+ // Returns the chosen entry's cachedAt alongside the windows so the caller can
347
+ // apply monotonic hysteresis (never displace a displayed value with an older
348
+ // shared-cache snapshot written by a different instance).
349
+ if (!provider || !model) return { quotaWindows: [], cachedAt: 0 };
347
350
  try {
348
351
  const cachePath = path.join(pluginDataDir(), 'gateway-oauth-usage-cache.json');
349
352
  const cache = readJson(cachePath);
350
353
  const routes = cache && typeof cache.routes === 'object' ? cache.routes : null;
351
- if (!routes) return [];
354
+ if (!routes) return { quotaWindows: [], cachedAt: 0 };
352
355
  const routeKey = `${String(provider).toLowerCase()}${String(model)}`;
353
356
  const providerOnlyKey = String(provider).toLowerCase();
354
357
  const routePrefix = `${providerOnlyKey}`;
355
358
  const entry = routes[routeKey] || routes[providerOnlyKey] || Object.entries(routes)
356
359
  .filter(([key, value]) => key.startsWith(routePrefix) && Array.isArray(value?.quotaWindows))
357
360
  .sort((a, b) => (Number(b[1]?.cachedAt) || 0) - (Number(a[1]?.cachedAt) || 0))[0]?.[1];
358
- if (!Array.isArray(entry?.quotaWindows)) return [];
361
+ if (!Array.isArray(entry?.quotaWindows)) return { quotaWindows: [], cachedAt: 0 };
359
362
  // Boot guard: do not render previous-launch usage before the current runtime
360
363
  // has captured at least one snapshot. Once captured in this process, hold it
361
364
  // instead of blanking it during idle/network gaps.
362
365
  const cachedAt = Number(entry.cachedAt);
363
- if (!Number.isFinite(cachedAt) || cachedAt < STATUSLINE_PROCESS_STARTED_AT_MS) return [];
364
- return entry.quotaWindows;
366
+ if (!Number.isFinite(cachedAt) || cachedAt < STATUSLINE_PROCESS_STARTED_AT_MS) return { quotaWindows: [], cachedAt: 0 };
367
+ return { quotaWindows: entry.quotaWindows, cachedAt };
365
368
  } catch {
366
- return [];
369
+ return { quotaWindows: [], cachedAt: 0 };
367
370
  }
368
371
  }
369
372
 
@@ -499,11 +502,18 @@ function configuredGatewayStatus(options = {}) {
499
502
 
500
503
  export function loadGatewayStatus(options = {}) {
501
504
  const configured = configuredGatewayStatus(options);
505
+ const quotaFallback = configured
506
+ ? cachedQuotaWindowsFallback(configured.provider, configured.model)
507
+ : { quotaWindows: [], cachedAt: 0 };
502
508
  const configuredStatus = configured ? {
503
509
  ...configured,
504
510
  contextUsedPct: pctOf(options.activeContextTokens, compactBoundaryForStatus(configured)),
505
511
  lastUsage: null,
506
- quotaWindows: cachedQuotaWindowsFallback(configured.provider, configured.model),
512
+ quotaWindows: quotaFallback.quotaWindows,
513
+ // Shared provider-wide cache snapshot: not owned by this instance. asOf is
514
+ // the cache entry's cachedAt so an older snapshot can be rejected downstream.
515
+ quotaWindowsAsOf: num(quotaFallback.cachedAt, 0),
516
+ quotaWindowsOwned: false,
507
517
  balance: null,
508
518
  routeSpend: null,
509
519
  } : null;
@@ -609,6 +619,12 @@ export function loadGatewayStatus(options = {}) {
609
619
  : null,
610
620
  lastUsage,
611
621
  quotaWindows: activeQuotaWindows.length ? activeQuotaWindows : (configuredStatus?.quotaWindows || []),
622
+ // Provenance for monotonic hysteresis: live active-instance windows are
623
+ // own-instance data timestamped by the advert's updatedAt; otherwise inherit
624
+ // the shared-cache fallback's (unowned) asOf. Older/unowned snapshots must
625
+ // not displace a value already rendered from newer/owned data.
626
+ quotaWindowsAsOf: activeQuotaWindows.length ? (updatedAt || 0) : num(configuredStatus?.quotaWindowsAsOf, 0),
627
+ quotaWindowsOwned: activeQuotaWindows.length ? metricsMatch : false,
612
628
  balance: metricsMatch && active.gateway_balance && typeof active.gateway_balance === 'object' ? active.gateway_balance : (configuredStatus?.balance || null),
613
629
  routeSpend: metricsMatch && active.gateway_route_spend && typeof active.gateway_route_spend === 'object' ? active.gateway_route_spend : (configuredStatus?.routeSpend || null),
614
630
  providerKind: statusProviderKind,
@@ -0,0 +1,46 @@
1
+ ---
2
+ id: bench
3
+ name: Bench
4
+ description: "Autonomous benchmark workflow — loop to completion without user approval."
5
+ hidden: true
6
+ agents: worker, heavy-worker, reviewer, debugger, maintainer
7
+ ---
8
+
9
+ # Bench Workflow
10
+
11
+ Autonomous run: no user is present. Never wait for approval or ask
12
+ questions — decide and proceed immediately. Loop until the task is verified
13
+ complete or provably blocked.
14
+
15
+ Lead supervises: delegates, coordinates, judges, decides. Route by complexity:
16
+ - Lead directly: simple 1–2 step work, plus coordination, pre-planning, config
17
+ changes, and final git deployment.
18
+ - Worker: implementation that takes several steps. Heavy Worker:
19
+ high-complexity, multi-step implementation.
20
+ - Reviewer: verify implementation scopes (diff, regressions, missing checks).
21
+ - Debugger: very high complexity, or when root-causing has already failed at
22
+ least once.
23
+
24
+ 1. Plan — form the plan yourself and start executing immediately; no approval
25
+ step exists.
26
+ 2. Delegate — split into the maximum number of independent scopes.
27
+ - PARALLEL across independent scopes by default (implementation, analysis,
28
+ review, debugging alike); spawn every scope in the SAME turn. Shared or
29
+ cross-cutting code does NOT justify merging — split per path and verify
30
+ the shared parts yourself. The only single scope is a genuinely
31
+ inseparable dependency; then state it.
32
+ - SEQUENTIAL within a single complex scope: ordered steps with a
33
+ build/test-green gate between them, not one shot.
34
+ - Write briefs per the Lead brief contract.
35
+ - After spawning async agents, END THE TURN — no polling, guessing, or
36
+ dependent work until the completion notification resumes you.
37
+ 3. Review — pair one reviewer 1:1 with each implementation scope, spawned in the
38
+ same turn, never deferred or batched; wait for its result. Fact-check agent
39
+ responses and cross-check implementation and review results yourself before
40
+ acting. Send fixes back to the original scope and loop verify -> fix ->
41
+ re-verify until clean. Skip review only for simple, low-risk tasks. If a bug
42
+ survives 2+ fix cycles, have the debugger investigate first instead of
43
+ another fix round.
44
+ 4. Finish — verify the final state yourself; only stop when the task is
45
+ complete and verified, or a hard blocker makes progress impossible. State
46
+ the outcome and evidence in the final message.
@@ -7,7 +7,13 @@ agents: worker, heavy-worker, reviewer, debugger, maintainer
7
7
 
8
8
  # Default Workflow
9
9
 
10
+ HARD APPROVAL GATE — before the user gives an explicit go-ahead ("do it",
11
+ "proceed", "ㄱㄱ"), NO edits, apply_patch, state-changing shell commands, or
12
+ agent spawns. Read-only exploration only. Agreeing with a diagnosis or
13
+ pointing out a problem is NOT approval.
14
+
10
15
  Lead supervises: delegates, coordinates, judges, decides. Route by complexity:
16
+ (routing applies only AFTER approval)
11
17
  - Lead directly: simple 1–2 step work, plus coordination, pre-planning, config
12
18
  changes, and final git deployment.
13
19
  - Worker: implementation that takes several steps. Heavy Worker:
@@ -7,6 +7,11 @@ agents:
7
7
 
8
8
  # Solo Workflow
9
9
 
10
+ HARD APPROVAL GATE — before the user gives an explicit go-ahead ("do it",
11
+ "proceed", "ㄱㄱ"), NO edits, apply_patch, or state-changing shell commands.
12
+ Read-only exploration only. Agreeing with a diagnosis or pointing out a
13
+ problem is NOT approval.
14
+
10
15
  1. Plan — Lead discusses the request with the user, forms a plan, and waits for
11
16
  approval before execution.
12
17
  Only an explicit go-ahead is approval; diagnosis agreement is not. When
@@ -225,6 +225,10 @@ export default class Ink {
225
225
  // refreshed every render; read back via getSelectionRows() so the app can
226
226
  // stitch selections taller than the viewport. null when no selection.
227
227
  lastSelectionRows = null;
228
+ // [mixdog fork] One-shot flag: the next interactive frame takes the full
229
+ // clearTerminal+rewrite path even when nothing changed. Set via
230
+ // forceFullRepaint(); consumed (and reset) in renderInteractiveFrame.
231
+ forceClearNextFrame = false;
228
232
  constructor(options) {
229
233
  autoBind(this);
230
234
  this.options = options;
@@ -569,6 +573,19 @@ export default class Ink {
569
573
  getSelectionRows = () => {
570
574
  return this.lastSelectionRows;
571
575
  };
576
+ // [mixdog fork] Force a one-shot whole-screen clear + rewrite. Windows
577
+ // Terminal keeps its NATIVE (shift+drag) selection overlay alive across
578
+ // incremental in-place repaints, so a stale native highlight can sit on top
579
+ // of the app-drawn selection; a real clearTerminal+full rewrite dismisses
580
+ // it. The frame is BSU/ESU-wrapped by renderInteractiveFrame, so there is
581
+ // no visible flash. Called by the app on mouse-press via the render handle.
582
+ forceFullRepaint = () => {
583
+ if (this.isUnmounted || this.isUnmounting) {
584
+ return;
585
+ }
586
+ this.forceClearNextFrame = true;
587
+ this.rootNode.onImmediateRender();
588
+ };
572
589
  restoreLastOutput = () => {
573
590
  if (!this.interactive) {
574
591
  return;
@@ -1115,7 +1132,11 @@ export default class Ink {
1115
1132
  if (isFullscreen && outputToRender.endsWith('\n')) {
1116
1133
  outputToRender += '\u001B[0m';
1117
1134
  }
1118
- const shouldClearTerminal = shouldClearTerminalForFrame({
1135
+ // [mixdog fork] One-shot forced clear (see forceFullRepaint): TTY-only,
1136
+ // consumed here so it applies to exactly one frame.
1137
+ const forcedClear = this.forceClearNextFrame && isTty;
1138
+ this.forceClearNextFrame = false;
1139
+ const shouldClearTerminal = forcedClear || shouldClearTerminalForFrame({
1119
1140
  isTty,
1120
1141
  viewportRows,
1121
1142
  previousViewportRows,
@@ -1147,6 +1168,7 @@ export default class Ink {
1147
1168
  // with identical output text but must still force a full clear to
1148
1169
  // avoid ghosting stale cells left outside the new viewport.
1149
1170
  if (shouldClearTerminal &&
1171
+ !forcedClear &&
1150
1172
  !this.isUnmounting &&
1151
1173
  output === this.lastOutput &&
1152
1174
  staticOutput === '' &&