mixdog 0.9.15 → 0.9.17

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 (134) hide show
  1. package/package.json +2 -1
  2. package/scripts/atomic-lock-tryonce-test.mjs +66 -0
  3. package/scripts/explore-bench.mjs +5 -4
  4. package/scripts/ingest-pure-conversation-smoke.mjs +27 -0
  5. package/scripts/output-style-smoke.mjs +3 -3
  6. package/scripts/provider-toolcall-test.mjs +79 -2
  7. package/scripts/termio-input-smoke.mjs +199 -0
  8. package/scripts/tool-efficiency-diag.mjs +88 -0
  9. package/scripts/tool-smoke.mjs +40 -24
  10. package/scripts/tui-frame-harness-shim.mjs +32 -0
  11. package/scripts/tui-frame-harness.mjs +306 -0
  12. package/src/agents/heavy-worker/AGENT.md +6 -7
  13. package/src/agents/worker/AGENT.md +6 -7
  14. package/src/lib/keychain-cjs.cjs +28 -11
  15. package/src/lib/rules-builder.cjs +6 -10
  16. package/src/mixdog-session-runtime.mjs +102 -20
  17. package/src/output-styles/simple.md +22 -24
  18. package/src/rules/agent/00-core.md +12 -11
  19. package/src/rules/agent/30-explorer.md +22 -21
  20. package/src/rules/lead/01-general.md +8 -8
  21. package/src/rules/lead/lead-brief.md +11 -12
  22. package/src/rules/shared/01-tool.md +25 -14
  23. package/src/runtime/agent/orchestrator/agent-trace-format.mjs +4 -3
  24. package/src/runtime/agent/orchestrator/config.mjs +1 -1
  25. package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +33 -1
  26. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +31 -1
  27. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +49 -0
  28. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +35 -1
  29. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +29 -8
  30. package/src/runtime/agent/orchestrator/providers/gemini.mjs +13 -5
  31. package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +1 -1
  32. package/src/runtime/agent/orchestrator/providers/model-list-sanitize.mjs +11 -0
  33. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +18 -0
  34. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +1 -1
  35. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +30 -2
  36. package/src/runtime/agent/orchestrator/session/cache/scoped-cache.mjs +19 -0
  37. package/src/runtime/agent/orchestrator/session/compact/constants.mjs +2 -2
  38. package/src/runtime/agent/orchestrator/session/compact/engine.mjs +1 -1
  39. package/src/runtime/agent/orchestrator/session/compact/summary.mjs +37 -15
  40. package/src/runtime/agent/orchestrator/session/context-utils.mjs +62 -0
  41. package/src/runtime/agent/orchestrator/session/lifecycle-scan.mjs +177 -0
  42. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +12 -19
  43. package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +10 -0
  44. package/src/runtime/agent/orchestrator/session/loop.mjs +30 -1
  45. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +8 -0
  46. package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +13 -18
  47. package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +37 -19
  48. package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +4 -1
  49. package/src/runtime/agent/orchestrator/session/manager/usage-metrics.mjs +142 -3
  50. package/src/runtime/agent/orchestrator/session/manager.mjs +59 -2
  51. package/src/runtime/agent/orchestrator/session/store-summary-index.mjs +118 -22
  52. package/src/runtime/agent/orchestrator/session/store.mjs +38 -25
  53. package/src/runtime/agent/orchestrator/stall-policy.mjs +22 -12
  54. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +4 -2
  55. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +7 -5
  56. package/src/runtime/agent/orchestrator/tools/builtin/path-diagnostics.mjs +38 -1
  57. package/src/runtime/agent/orchestrator/tools/builtin/path-utils.mjs +24 -0
  58. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +17 -16
  59. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +14 -1
  60. package/src/runtime/agent/orchestrator/tools/builtin/rg-runner.mjs +25 -1
  61. package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +73 -3
  62. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +23 -10
  63. package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +55 -0
  64. package/src/runtime/agent/orchestrator/tools/builtin/snapshot-store.mjs +10 -2
  65. package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +8 -2
  66. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +21 -13
  67. package/src/runtime/agent/orchestrator/tools/patch/paths.mjs +13 -2
  68. package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +44 -6
  69. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +5 -0
  70. package/src/runtime/channels/backends/telegram.mjs +29 -9
  71. package/src/runtime/channels/lib/event-queue.mjs +6 -2
  72. package/src/runtime/channels/lib/memory-client.mjs +66 -1
  73. package/src/runtime/channels/lib/runtime-paths.mjs +8 -19
  74. package/src/runtime/channels/lib/webhook/deliveries.mjs +22 -1
  75. package/src/runtime/memory/index.mjs +132 -8
  76. package/src/runtime/memory/lib/cycle-scheduler.mjs +24 -5
  77. package/src/runtime/memory/lib/embedding-warmup.mjs +3 -0
  78. package/src/runtime/memory/lib/ko-morph.mjs +1 -0
  79. package/src/runtime/memory/lib/memory-cycle1.mjs +45 -3
  80. package/src/runtime/memory/lib/memory-cycle2-gate.mjs +7 -3
  81. package/src/runtime/memory/lib/memory-cycle2-mutations.mjs +54 -10
  82. package/src/runtime/memory/lib/memory-cycle2.mjs +21 -8
  83. package/src/runtime/memory/lib/memory-embed.mjs +48 -0
  84. package/src/runtime/memory/lib/memory-recall-store.mjs +57 -13
  85. package/src/runtime/memory/lib/memory.mjs +88 -1
  86. package/src/runtime/memory/lib/session-ingest.mjs +34 -3
  87. package/src/runtime/memory/lib/trace-store.mjs +52 -3
  88. package/src/runtime/memory/lib/transcript-ingest.mjs +27 -4
  89. package/src/runtime/shared/atomic-file.mjs +110 -0
  90. package/src/runtime/shared/child-guardian.mjs +4 -2
  91. package/src/runtime/shared/open-url.mjs +2 -1
  92. package/src/runtime/shared/singleton-owner.mjs +26 -0
  93. package/src/runtime/shared/tool-execution-contract.mjs +25 -0
  94. package/src/runtime/shared/transcript-writer.mjs +49 -5
  95. package/src/session-runtime/config-helpers.mjs +7 -2
  96. package/src/session-runtime/cwd-plugins.mjs +6 -1
  97. package/src/session-runtime/effort.mjs +6 -1
  98. package/src/session-runtime/plugin-mcp.mjs +116 -12
  99. package/src/session-runtime/provider-models.mjs +47 -8
  100. package/src/session-runtime/settings-api.mjs +7 -1
  101. package/src/standalone/channel-worker.mjs +25 -3
  102. package/src/standalone/explore-tool.mjs +5 -5
  103. package/src/standalone/hook-bus/config.mjs +38 -2
  104. package/src/standalone/hook-bus/handlers.mjs +103 -11
  105. package/src/standalone/hook-bus.mjs +20 -6
  106. package/src/standalone/memory-runtime-proxy.mjs +40 -5
  107. package/src/tui/App.jsx +214 -30
  108. package/src/tui/app/core-memory-picker.mjs +6 -6
  109. package/src/tui/app/model-options.mjs +10 -4
  110. package/src/tui/app/settings-picker.mjs +5 -30
  111. package/src/tui/app/slash-commands.mjs +0 -1
  112. package/src/tui/app/slash-dispatch.mjs +0 -16
  113. package/src/tui/app/text-layout.mjs +57 -0
  114. package/src/tui/app/transcript-window.mjs +162 -2
  115. package/src/tui/app/use-mouse-input.mjs +60 -47
  116. package/src/tui/app/use-transcript-window.mjs +96 -17
  117. package/src/tui/components/PromptInput.jsx +18 -1
  118. package/src/tui/components/StatusLine.jsx +1 -1
  119. package/src/tui/components/TextEntryPanel.jsx +97 -33
  120. package/src/tui/dist/index.mjs +856 -288
  121. package/src/tui/engine/tool-card-results.mjs +22 -5
  122. package/src/tui/engine/tui-steering-persist.mjs +66 -35
  123. package/src/tui/engine.mjs +41 -9
  124. package/src/tui/index.jsx +101 -7
  125. package/src/ui/statusline-segments.mjs +54 -36
  126. package/src/ui/statusline.mjs +141 -95
  127. package/src/workflows/default/WORKFLOW.md +27 -31
  128. package/vendor/ink/build/components/App.js +62 -17
  129. package/vendor/ink/build/ink.js +78 -3
  130. package/vendor/ink/build/input-parser.d.ts +9 -4
  131. package/vendor/ink/build/input-parser.js +45 -176
  132. package/vendor/ink/build/log-update.js +47 -2
  133. package/vendor/ink/build/termio-keypress.js +240 -0
  134. package/vendor/ink/build/termio-tokenize.js +253 -0
@@ -3,6 +3,7 @@ import { homedir } from 'node:os';
3
3
  import { basename, dirname, isAbsolute, join, resolve } from 'node:path';
4
4
  import { fileURLToPath } from 'node:url';
5
5
  import { performance } from 'node:perf_hooks';
6
+ import httpMod from 'node:http';
6
7
  import { ensureStandaloneEnvironment } from './standalone/seeds.mjs';
7
8
  import { createStandaloneAgent } from './standalone/agent-tool.mjs';
8
9
  import { isAgentOwner } from './runtime/agent/orchestrator/agent-owner.mjs';
@@ -167,6 +168,9 @@ import {
167
168
  normalizePluginMcpServerConfig,
168
169
  pluginManifest,
169
170
  pluginMcpServerName,
171
+ pluginRawMcpServers,
172
+ pluginMcpEnableScript,
173
+ resolveContainedPluginPath,
170
174
  } from './session-runtime/plugin-mcp.mjs';
171
175
  import {
172
176
  WORKFLOW_ROUTE_SLOTS,
@@ -395,6 +399,13 @@ export async function createMixdogSessionRuntime({
395
399
  ]);
396
400
  bootProfile('imports:ready', { ms: (performance.now() - importsStartedAt).toFixed(1) });
397
401
  const pluginDataDir = cfgMod.getPluginData();
402
+ // Re-wire the idle/tombstone sweep. startIdleCleanup() lost its caller in a
403
+ // refactor, so closed-session tombstones were never deleted after their 24h
404
+ // grace — the store grew unbounded (observed: 1.8k files / 114MB), which
405
+ // made summary-index rebuilds and per-save index rewrites stall boot for
406
+ // seconds. Timer is unref'd and first fires after CLEANUP_INITIAL_DELAY_MS
407
+ // (5min), so this adds zero boot-path cost.
408
+ try { mgr.startIdleCleanup?.(); } catch { /* cleanup is best-effort */ }
398
409
  const memoryRuntime = createStandaloneMemoryRuntime({
399
410
  entry: join(STANDALONE_ROOT, MEMORY_RUNTIME.replace(/^\.\//, '')),
400
411
  dataDir: pluginDataDir,
@@ -1531,11 +1542,48 @@ export async function createMixdogSessionRuntime({
1531
1542
  // published to active-instance.json by getMemoryModule().init(). Without
1532
1543
  // this, memory only starts on the first turn's getMemoryModule() call —
1533
1544
  // so early channel traffic finds no memory_port and gets buffered (or,
1534
- // pre-drainer, silently dropped). Fire-and-forget BEFORE channel claim so
1535
- // the port is (racing to be) live by the time the worker sends its first
1536
- // ingest.
1537
- try { void getMemoryModule().catch((error) => bootProfile('channels:memory-eager-init-failed', { error: error?.message || String(error) })); }
1538
- catch (error) { bootProfile('channels:memory-eager-init-failed', { error: error?.message || String(error) }); }
1545
+ // pre-drainer, silently dropped). Runs BEFORE channel claim so the port is
1546
+ // racing to be live by the time the worker sends its first ingest.
1547
+ //
1548
+ // Not fire-and-forget: init() only resolves the module handle, so a bare
1549
+ // getMemoryModule() could return before /health reports ok leaving early
1550
+ // ingests to hit a not-yet-listening port. Await the proxy start() and then
1551
+ // poll /health with a bounded retry so the daemon is provably reachable
1552
+ // (or logged failed) rather than assumed-up. Still non-blocking to the
1553
+ // caller: the whole probe is detached, but it internally awaits readiness.
1554
+ void (async () => {
1555
+ try {
1556
+ // Yield one event-loop tick before the heavy chain below (module
1557
+ // resolve, daemon fork/health-poll) starts, so Ink's next render
1558
+ // (scheduled via setImmediate/timers) and any queued keypress
1559
+ // events get a turn first instead of being starved by this
1560
+ // detached chain's synchronous setup work.
1561
+ await new Promise((r) => setImmediate(r));
1562
+ const mod = await getMemoryModule();
1563
+ const started = typeof mod?.start === 'function' ? await mod.start() : null;
1564
+ const port = started?.port;
1565
+ if (!port) { bootProfile('channels:memory-eager-init-failed', { error: 'no port from start()' }); return; }
1566
+ for (let i = 0; i < 30; i++) {
1567
+ try {
1568
+ const ok = await new Promise((res) => {
1569
+ const req = httpMod.request({ hostname: '127.0.0.1', port, path: '/health', timeout: 1500 }, (r) => {
1570
+ let d = ''; r.on('data', (c) => { d += c; }); r.on('end', () => {
1571
+ try { res(JSON.parse(d)?.status === 'ok'); } catch { res(false); }
1572
+ });
1573
+ });
1574
+ req.on('error', () => res(false));
1575
+ req.on('timeout', () => { req.destroy(); res(false); });
1576
+ req.end();
1577
+ });
1578
+ if (ok) { bootProfile('channels:memory-eager-init-ready', { port }); return; }
1579
+ } catch {}
1580
+ await new Promise((r) => setTimeout(r, 500));
1581
+ }
1582
+ bootProfile('channels:memory-eager-init-failed', { error: `health not ok after retries (port ${port})` });
1583
+ } catch (error) {
1584
+ bootProfile('channels:memory-eager-init-failed', { error: error?.message || String(error) });
1585
+ }
1586
+ })();
1539
1587
  // Publish this session's record + transcript file BEFORE the worker's
1540
1588
  // activate-time discovery polls, so output forwarding binds to this
1541
1589
  // terminal session immediately instead of waiting for the first turn.
@@ -1561,6 +1609,12 @@ export async function createMixdogSessionRuntime({
1561
1609
  }
1562
1610
  bootProfile('channels:start-scheduled', { delayMs: 0, immediate: true });
1563
1611
  void (async () => {
1612
+ // Yield before the createCurrentSession/transcript/fork chain below —
1613
+ // same rationale as the memory-eager-init yield above: this detached
1614
+ // chain runs synchronous config/fs work (createCurrentSession, backend
1615
+ // flush, transcript writer) back-to-back, and without a tick break it
1616
+ // can run ahead of Ink's queued render/input handling.
1617
+ await new Promise((r) => setImmediate(r));
1564
1618
  // Immediate-occupancy guarantee: make sure a session + transcript
1565
1619
  // exist BEFORE the worker boots — a freshly-forked worker claims and
1566
1620
  // runs transcript discovery inside its own start(), so publishing the
@@ -1640,8 +1694,38 @@ export async function createMixdogSessionRuntime({
1640
1694
  // channel; see startRemote()/stopRemote() and the `/remote` toggle.
1641
1695
  // `remote.autoStart` in mixdog-config.json makes every session claim remote
1642
1696
  // at boot (same force-takeover semantics as `mixdog --remote` / `/remote`).
1643
- if (!remoteEnabled && config?.remote?.autoStart === true) remoteEnabled = true;
1644
- if (remoteEnabled) startRemote();
1697
+ // The flag lives in the TOP-LEVEL `remote` section of mixdog-config.json
1698
+ // (sibling of agent/ui/channels), not inside the agent section that
1699
+ // cfgMod.loadConfig returns — read it via the shared whole-file reader.
1700
+ // `config?.remote?.autoStart` is kept for back-compat with agent-section
1701
+ // placement.
1702
+ if (!remoteEnabled) {
1703
+ try {
1704
+ if (config?.remote?.autoStart === true
1705
+ || sharedCfgMod?.readSection?.('remote')?.autoStart === true) {
1706
+ remoteEnabled = true;
1707
+ }
1708
+ } catch { /* unreadable config never blocks boot */ }
1709
+ }
1710
+ // Boot-time remote start (autoStart or --remote) is DEFERRED past the TUI's
1711
+ // first frame. startRemote() front-loads heavy work — memory daemon fork
1712
+ // (PG + forced ONNX embed warmup in the child), eager session create, and
1713
+ // the channel-worker fork — and running it inline here interleaves that
1714
+ // CPU/disk load with engine boot, visibly delaying the first ink frame by
1715
+ // seconds. The deferred timer reuses prewarmTimers.channelStartTimer so an
1716
+ // early /remote (startRemote clears it), stopRemote(), and close() all
1717
+ // cancel it through the existing clearTimeout paths. Runtime /remote calls
1718
+ // still start immediately (user-initiated, UI already painted).
1719
+ if (remoteEnabled) {
1720
+ const remoteAutoStartDelayMs = envDelayMs('MIXDOG_REMOTE_AUTOSTART_DELAY_MS', 1_500, { min: 0, max: 60_000 });
1721
+ bootProfile('channels:autostart-deferred', { delayMs: remoteAutoStartDelayMs });
1722
+ prewarmTimers.channelStartTimer = setTimeout(() => {
1723
+ prewarmTimers.channelStartTimer = null;
1724
+ if (closeRequested || !remoteEnabled) return;
1725
+ startRemote();
1726
+ }, remoteAutoStartDelayMs);
1727
+ prewarmTimers.channelStartTimer.unref?.();
1728
+ }
1645
1729
 
1646
1730
  function contextStatusCacheKeyFor({ messages, tools }) {
1647
1731
  const compaction = session?.compaction || {};
@@ -2769,19 +2853,17 @@ export async function createMixdogSessionRuntime({
2769
2853
  },
2770
2854
  async enablePluginMcp(plugin = {}) {
2771
2855
  const root = clean(plugin.root);
2772
- const script = clean(plugin.mcpScript);
2856
+ const script = pluginMcpEnableScript(root, plugin);
2773
2857
  if (!root || !script) throw new Error('plugin has no MCP script');
2774
2858
  const serverName = pluginMcpServerName(plugin);
2775
2859
  const nextConfig = { ...config };
2776
- if (script === '.mcp.json') {
2777
- const mcpJsonPath = join(root, script);
2778
- if (!existsSync(mcpJsonPath)) throw new Error(`plugin MCP manifest not found: ${mcpJsonPath}`);
2779
- const manifest = readJsonSafe(mcpJsonPath) || {};
2780
- const rawServers = manifest.mcpServers;
2781
- const isPlainObject = (v) => v !== null && typeof v === 'object' && !Array.isArray(v);
2782
- if (!isPlainObject(rawServers)) throw new Error(`plugin .mcp.json missing mcpServers object: ${mcpJsonPath}`);
2783
- const keys = Object.keys(rawServers).filter((k) => isPlainObject(rawServers[k]));
2784
- if (!keys.length) throw new Error(`plugin .mcp.json has no mcpServers: ${mcpJsonPath}`);
2860
+ const manifestMcp = pluginRawMcpServers(root, script);
2861
+ if (manifestMcp) {
2862
+ const { rawServers, mcpRoot } = manifestMcp;
2863
+ const keys = Object.keys(rawServers).filter((k) => {
2864
+ const v = rawServers[k];
2865
+ return v !== null && typeof v === 'object' && !Array.isArray(v);
2866
+ });
2785
2867
  const ownedPrefix = `${serverName}--`;
2786
2868
  const nextServers = {};
2787
2869
  for (const [k, v] of Object.entries(nextConfig.mcpServers || {})) {
@@ -2789,7 +2871,7 @@ export async function createMixdogSessionRuntime({
2789
2871
  nextServers[k] = v;
2790
2872
  }
2791
2873
  for (const serverKey of keys) {
2792
- const cfg = normalizePluginMcpServerConfig(rawServers[serverKey], root);
2874
+ const cfg = normalizePluginMcpServerConfig(rawServers[serverKey], mcpRoot);
2793
2875
  cfg.env = {
2794
2876
  ...(cfg.env || {}),
2795
2877
  MIXDOG_PLUGIN_ROOT: root,
@@ -2800,8 +2882,8 @@ export async function createMixdogSessionRuntime({
2800
2882
  }
2801
2883
  nextConfig.mcpServers = nextServers;
2802
2884
  } else {
2803
- const scriptPath = join(root, script);
2804
- if (!existsSync(scriptPath)) throw new Error(`plugin MCP script not found: ${scriptPath}`);
2885
+ const scriptPath = resolveContainedPluginPath(root, script);
2886
+ if (!scriptPath || !existsSync(scriptPath)) throw new Error(`plugin MCP script not found: ${join(root, script)}`);
2805
2887
  nextConfig.mcpServers = {
2806
2888
  ...(nextConfig.mcpServers || {}),
2807
2889
  [serverName]: {
@@ -12,34 +12,32 @@ Practical concise — outcome-first handoffs for coding work: summarize the
12
12
  result, do not narrate or explain the change.
13
13
 
14
14
  - Open with the outcome in one sentence: done, blocked, or awaiting a decision.
15
- - Summarize at the concept level: name the behavior and direction, not the code
16
- path. Cite a symbol/path only as an anchor, never as the explanation.
15
+ - Summarize at the concept level: name the behavior and direction, not the
16
+ code path; cite a symbol/path only as an anchor, never as the explanation.
17
17
  - Compress by cutting content (filler, hedging, pleasantries, restated facts),
18
- not by clipping grammar: keep natural, complete sentences in the user's
19
- language — never telegraph-style stub endings. Technical terms and code stay
20
- exact.
21
- - Summarize what the change accomplishes rather than listing every file and how
22
- each was edited. Name a path (`file_path:line_number`) only when the reader
23
- truly needs it to navigate not as a per-file changelog.
24
- - Keep controlled detail: usually 1–3 short bullets or 2–3 sentences total.
25
- State each point once — outcome or fix direction, not both restated. No
26
- step-by-step narration or file/line inventory.
27
- - Size budget: roughly HALF the Default style and TWICE Minimal per point one
28
- rendered line, whole reply ~5–7 lines. Above that you are writing Default;
18
+ not grammar: keep natural, complete sentences in the user's language — never
19
+ telegraph-style stub endings. Technical terms and code stay exact.
20
+ - Summarize what the change accomplishes, not a per-file changelog. Name a
21
+ path (`file_path:line_number`) only when the reader truly needs it to
22
+ navigate.
23
+ - Controlled detail: usually 1–3 short bullets or 2–3 sentences total; state
24
+ each point once outcome or fix direction, not both. No step-by-step
25
+ narration or file/line inventory.
26
+ - Size budget: roughly HALF the Default style and TWICE Minimal — one rendered
27
+ line per point, whole reply ~5–7 lines. Above that you are writing Default;
29
28
  below ~3 lines consider whether prose (Minimal) reads better.
30
29
  - Layout: one idea per bullet, ONE line each (two only when a verbatim
31
- path/error forces it). Lead each item with a short bold key phrase. Put a
32
- blank line between multi-line list items — never emit a dense wall of text.
33
- - If a point runs past one line, cut the elaboration instead of wrapping; detail
34
- beyond the key phrase + one clause belongs to the Default style.
35
- - On final handoffs, optional labels such as `바뀐 점`, `확인한 것`, and
36
- `남은 리스크/다음 단계` fit Korean-facing profiles; use plain English labels
37
- when the thread is English. Do not label interim progress.
30
+ path/error forces it), led with a short bold key phrase; blank line between
31
+ multi-line list items — never a dense wall of text. If a point runs past one
32
+ line, cut the elaboration detail beyond key phrase + one clause belongs to
33
+ Default.
34
+ - Final handoffs may use labels like `바뀐 점`, `확인한 것`,
35
+ `남은 리스크/다음 단계` for Korean-facing profiles (plain English labels in
36
+ English threads); do not label interim progress.
38
37
  - Synthesize agent or retrieval results; never forward raw reports, long file
39
38
  lists, tool traces, or session metadata.
40
- - Do not hide blockers, failed verification, or required follow-up; state them
41
- in one short clause.
39
+ - Do not hide blockers, failed verification, or required follow-up state
40
+ them in one short clause; if verification was not run, say so once.
42
41
  - Keep paths, commands, symbols, API names, code, and exact errors verbatim.
43
- - Skip filler, acknowledgments, hedging, and repeated conclusions; if
44
- verification was not run, say so once.
42
+ - Skip filler, acknowledgments, hedging, and repeated conclusions.
45
43
  - Never name this style unless asked.
@@ -1,19 +1,20 @@
1
1
  # Agent Constraints
2
2
 
3
3
  - Use English for agent task communication.
4
- - NEVER PREAMBLE: no tool-call preambles, status/progress narration, "I
5
- will..." setup, or transition text before tool calls.
6
- - If tools are needed, call them immediately. Emit text only for the final
7
- handoff after tool work is done.
4
+ - ONE TURN = ONE BATCH: emit all independent tool calls of a step in the same
5
+ turn; a lone read-only call that could have ridden the previous turn is a
6
+ wasted round-trip.
7
+ - NEVER PREAMBLE: no status/progress narration, "I will..." setup, or any text
8
+ before tool calls — call needed tools immediately; emit text only for the
9
+ final handoff after tool work is done.
8
10
  - Final handoff: minimum characters, maximum information for Lead. Follow the
9
- role's stricter output contract if defined; else emit fragments — outcome
10
- (1 line), key `file:line`(s), verification result, material risks (only if
11
+ role's stricter output contract if defined; else fragments — outcome (1
12
+ line), key `file:line`(s), verification result, material risks (only if
11
13
  any).
12
- - Density over length: never repeat what Lead already knows (the brief, the
13
- process, how you searched); state only what changed and where to look.
14
- Verification = command + result in one line. Same fact twice = delete one.
15
- - Handoff cap ~30 lines unless `Deliver:` raises it — a safety ceiling, not
16
- a target.
14
+ - Never repeat what Lead already knows (the brief, the process, how you
15
+ searched); state only what changed and where to look. Verification =
16
+ command + result in one line. Same fact twice = delete one.
17
+ - Handoff cap ~30 lines unless `Deliver:` raises it — a ceiling, not a target.
17
18
  - Banned as pure cost: report headings, markdown tables (unless requested),
18
19
  prose narration, raw logs/tool traces, speculative next-checks, restated
19
20
  brief, articles/politeness.
@@ -6,28 +6,29 @@ kind: retrieval
6
6
 
7
7
  # Role: explorer
8
8
 
9
- You are a one-shot locator, not a researcher. You find WHERE, never WHY —
10
- no understanding code, explaining behaviour, or tracing logic. Coordinates
11
- are the entire deliverable.
9
+ Coordinate locator. Deliverable = WHERE as `path:line`, never WHY — no
10
+ explaining or tracing. You ARE the `explore` tool: never call it; shared
11
+ rules pointing at `explore` don't apply — use grep/code_graph/find/glob
12
+ directly.
12
13
 
13
- Default shape: ONE maximal-batch turn, then the answer. Fire everything at
14
- once: a single `grep` whose pattern[] carries ALL literal strings/errors/
15
- identifier guesses synonyms, casings, variants plus `code_graph`/`find`/
16
- `glob` in the SAME turn when useful. Width is free, turns are not.
14
+ Credibility is mechanical: hit = line/path contains ANY query token or
15
+ obvious synonym. No other standard "is this the real implementation" is
16
+ the caller's question. Sole exception: a generic-word-only match (schema,
17
+ handler, config, resolver…) with no SPECIFIC query token (product/library/
18
+ domain name) = zero, not a hit.
17
19
 
18
- Answer straight from search output: hits already carry `path:line` never
19
- spend a turn on `read` to confirm or polish an anchor. `dist`/generated
20
- hits are leads, not answers: trace them to source.
20
+ Rule zero after EVERY tool result: ≥1 `path:line` matching a query
21
+ token answer NOW with those coordinates; no verify, no re-grep, no
22
+ ranking; mark weak anchors `?`. Zero one more batch if budget remains.
23
+ No third branch: "hits exist but I want better ones" IS branch one.
21
24
 
22
- Budget: 5 turns of maximal batches. Credible `path:line` on screen means the
23
- search is OVER answer in that turn; no verifying or collecting extras;
24
- mark weak anchors `?`. Turns 2-5 exist ONLY for a zero-anchor turn 1: derive
25
- new tokens from returned paths/names or widen scope, packing ALL candidates
26
- into the next batch. Concept queries: grep implementation vocabulary
27
- (file/dir names, symbol fragments, config keys). `EXPLORATION_FAILED` only
28
- after all 5 turns found zero anchors — giving up early is as wrong as
29
- over-searching; at turn 5 answer best-so-far.
25
+ Turns: 3 tool turns HARD max; start each tool message with `turn N/3`.
26
+ Turn 1 answer; turns 2-3 are miss-recovery only, and must change tokens
27
+ OR scope never reword. One turn = ONE batch of AT MOST 5 calls: a single
28
+ grep packing ALL variants in pattern[] plus code_graph/find/glob; a 6th
29
+ call = packing failure. Flow/how questions: return first matching entry
30
+ anchors (≤5); the caller traces the chain.
30
31
 
31
- Answer format, nothing else:
32
- - up to 5 lines: `path:linesymbol/name short reason` (append `?` if weak)
33
- - or `EXPLORATION_FAILED`
32
+ Answer, nothing else: up to 5 lines `path:line — symbol — short reason`
33
+ (`?` if weak), or `EXPLORATION_FAILED`only after 3 turns of zero
34
+ anchors, or when every anchor matches only generic words.
@@ -1,11 +1,11 @@
1
1
  # General
2
2
 
3
- - Omit direct names, honorifics, headings, and labels in preambles.
4
- - Preambles are optional: use only when they add user-visible value, keep to
5
- one short sentence, skip routine lookup narration.
3
+ - Preambles are optional: one short sentence only when it adds user-visible
4
+ value; no direct names, honorifics, headings, labels, or routine lookup
5
+ narration.
6
6
  - Destructive/hard-to-reverse actions require explicit confirmation.
7
- - Never push, build, or deploy without an explicit user request.
8
- Implementation approval is not deploy approval.
9
- - Proactively handle what you can first rather than deferring to the user;
10
- propose only the parts needing a decision, consultatively ("Shall we
11
- proceed this way?").
7
+ - Never push, build, or deploy without an explicit user request
8
+ implementation approval is not deploy approval.
9
+ - Handle what you can proactively rather than deferring to the user; propose
10
+ only the parts needing a decision, consultatively ("Shall we proceed this
11
+ way?").
@@ -1,21 +1,20 @@
1
1
  # Lead Brief Contract
2
2
 
3
3
  - Brief = one-line fragments `Goal:` `Anchors:` `Allow/Forbid:` `Deliver:`
4
- `Verify:` (+`Stop:` heavy-worker). No role-known rules, background, or
5
- motivation minimum characters, maximum information.
6
- - Density over length: every line must carry information the agent cannot
7
- know — never restate role rules, prior context, or the same fact twice.
8
- A brief that feels long is a scope problem; split the scope, don't pad.
9
- - Anchors = `file:line` + one-line conclusion each. Never paste log/code
10
- bodies; the agent reads the file itself.
11
- - Instruct by outcome ("make X behave Y"), not by method (no code snippets,
12
- no step-by-step edits) unless the method IS the requirement.
4
+ `Verify:` (+`Stop:` heavy-worker). Minimum characters, maximum information:
5
+ no background/motivation, no role-known rules; never restate prior context
6
+ or the same fact twice. A brief that feels long is a scope problem — split
7
+ the scope, don't pad.
8
+ - Anchors = `file:line` + one-line conclusion each; never paste log/code
9
+ bodies the agent reads the file itself.
10
+ - Instruct by outcome ("make X behave Y"), not method (no code snippets, no
11
+ step-by-step edits) unless the method IS the requirement.
13
12
  - `Deliver:` states output size/shape (e.g. "fragments <=15 lines", "verdict
14
- + top-3 risks", "detail to file, path only"). Never request a long report
13
+ + top-3 risks", "detail to file, path only"); never request a long report
15
14
  in the handoff itself.
16
15
  - Full brief only on fresh spawn or `respawned: true` (dead-tag send = cold
17
- session; re-supply anchors). Live-session follow-ups = delta only; never
18
- restate Goal/rules.
16
+ session; re-supply anchors); live-session follow-ups = delta only, never
17
+ restating Goal/rules.
19
18
  - Never `send` mid-run; batch all adjustments into ONE follow-up after
20
19
  completion. Interrupt only to cancel.
21
20
  - All agent communication in English.
@@ -4,18 +4,29 @@
4
4
  prior result — including edit loops: batch the post-edit verification read
5
5
  with the next target's lookup.
6
6
  - Pick by target: symbols/callers/deps → `code_graph`; exact text in a verified
7
- scope → `grep`; unknown path/name → `find`; structure → `glob`; dirs →
8
- `list`; verified file `read`; broad unknown with no anchor → `explore`.
9
- Never `grep`/`read` guessed paths.
10
- - One concept one batched `grep pattern:[...]` with
11
- `output_mode:"content_with_context"` (or one `code_graph symbols[]`). Refine
12
- from returned paths; never repeat equivalent patterns or scopes.
13
- - On miss/error, normalize the target once and switch tool; on a plausible hit,
14
- stop and answer from the framed context.
15
- - Retrieval serves the NEXT action (edit, answer, handoff), not certainty. One
16
- anchor is enough to act on; re-reading/re-grepping an area seen this session
17
- is waste. When acting and looking are both possible, act.
18
- - `read` uses `offset`/`limit` only. For 2+ spans from known file(s), make one
19
- batched `read` with `{path,offset,limit}` region objects. Adjacent spans in
20
- one file (within a few hundred lines) are ONE window, not repeated reads.
7
+ scope → `grep`; dirs → `list`; verified file → `read`.
8
+ Locating files: concept-level or cross-file questions ("where is X handled",
9
+ "how does Y flow") → `explore`, even when you could guess a keyword — its
10
+ fan-out spends explorer context, not yours, and guess-and-retry keywords
11
+ belong to it, not grep. Confident name fragment → `find`; exact name
12
+ pattern `glob`; exact content literal `grep`. Flow/trace with a known
13
+ symbol `code_graph`, never `explore` it returns entry-point anchors
14
+ only; follow the chain yourself. Never `grep`/`read` guessed paths.
15
+ - One concept ONE search call: all synonyms/variants in one
16
+ `grep pattern:[...]` (or `code_graph symbols[]`), scopes as `path[]`;
17
+ single-pattern bursts = packing failure. A second call on the same concept
18
+ is a defect unless the first returned zero then change tokens or scope,
19
+ never reword.
20
+ - Content-mode `grep` always carries `-C 5`+ ; re-reading the same file after
21
+ a content grep is a wasted round-trip. Bare match lines = existence checks
22
+ only.
23
+ - Any path not returned by a tool this session is a guess: find first, then
24
+ grep/read the returned path. On ENOENT the next call is find on the
25
+ basename — never a second guess or the same path with changed args.
26
+ - Retrieval serves the NEXT action (edit, answer, handoff), not certainty:
27
+ the FIRST anchor ends the search — no verifying, ranking, or re-checking a
28
+ hit already on screen. When acting and looking are both possible, act.
29
+ - `read` uses `offset`/`limit` only; 2+ spans from known file(s) = ONE batched
30
+ `read` with `{path,offset,limit}` regions. Adjacent spans in one file
31
+ (within a few hundred lines) are ONE window, not repeated reads.
21
32
  - Don't mix `apply_patch` with shell or other state-changing calls in one turn.
@@ -207,12 +207,13 @@ function _redactLogText(text) {
207
207
  function classifyToolFailure(resultText, toolName) {
208
208
  const text = String(resultText ?? '').toLowerCase();
209
209
  if (/requires either|invalid arguments|unknown parameter|must be|schema|expected|required|old_string is .*>=/.test(text)) return 'schema/args';
210
- if (/enoent|cannot find|not found at this path|path does not exist|no such file|file not found in graph|unreadable/.test(text)) return 'path/cwd';
210
+ if (/not in allow-list|not allowed/.test(text)) return 'permission';
211
+ if (String(toolName || '') === 'shell' || /^\s*\[exit code:\s*\d+\]/i.test(String(resultText ?? ''))) return 'command-exit';
212
+ if (/enoent|cannot find|not found at this path|path does not exist|no such file|file not found in graph|unreadable/.test(text)) return 'path/enoent';
211
213
  if (/timed out|timeout|interrupted|aborted/.test(text)) return 'timeout/abort';
212
214
  if (/hunk rejected|patch failed|context mismatch|expected first old\/context|context not found/.test(text)) return 'patch/context';
213
- if (/not in allow-list|permission|denied|forbidden|not allowed/.test(text)) return 'permission';
215
+ if (/permission|denied|forbidden/.test(text)) return 'permission';
214
216
  if (/unknown tool|tool.*not.*available|missing.*tool/.test(text)) return 'tool-surface';
215
- if (String(toolName || '') === 'shell' || /^\s*\[exit code:\s*\d+\]/i.test(String(resultText ?? ''))) return 'command-exit';
216
217
  return 'runtime/failure';
217
218
  }
218
219
 
@@ -149,7 +149,7 @@ function buildDefaultConfig(options = {}) {
149
149
  const providers = {};
150
150
  // API providers — enabled if env key exists
151
151
  for (const [name, envKey] of Object.entries(AGENT_PROVIDER_ENV)) {
152
- const apiKey = process.env[envKey];
152
+ const apiKey = detectCredentials ? process.env[envKey] : undefined;
153
153
  providers[name] = {
154
154
  enabled: !!apiKey,
155
155
  apiKey: apiKey || undefined,
@@ -43,6 +43,15 @@ function parseClaudeVersion(model) {
43
43
  if (pair) {
44
44
  return { family: pair[1], major: Number(pair[2]), minor: null };
45
45
  }
46
+ // Legacy naming: version comes BEFORE the family (claude-3-7-sonnet,
47
+ // claude-3-5-sonnet, claude-3-opus). These are manual-thinking models —
48
+ // parse them so isLegacyAnthropicReasoningModel excludes them instead of
49
+ // letting modelSupportsEffort's `startsWith('claude-')` fallthrough grant
50
+ // them adaptive thinking + the effort beta (a hard 400).
51
+ const legacy = m.match(/^claude-(\d+)-(\d+)-(haiku|sonnet|opus)/);
52
+ if (legacy) {
53
+ return { family: legacy[3], major: Number(legacy[1]), minor: Number(legacy[2]) };
54
+ }
46
55
  return null;
47
56
  }
48
57
 
@@ -69,7 +78,17 @@ export function modelSupportsEffort(model) {
69
78
  if (m.includes('sonnet-5') || m.includes('fable-5')) return true;
70
79
  if (/^claude-opus-4-(6|7|8)(?:$|[-@])/.test(m)) return true;
71
80
  if (/^claude-opus-5-/.test(m)) return true;
72
- if (m.startsWith('claude-')) return true;
81
+ // Fallthrough for not-yet-enumerated modern models: only grant effort when
82
+ // parseClaudeVersion resolves a family with major>=4 (and not a manual-
83
+ // thinking legacy already excluded above). A bare `startsWith('claude-')`
84
+ // here would hand thinking:adaptive + the effort beta to legacy/unknown
85
+ // IDs (e.g. claude-3-7-sonnet) → hard 400. Unknown-shape IDs (no version
86
+ // parsed) fall through to false: no effort, no adaptive thinking.
87
+ const parsed = parseClaudeVersion(model);
88
+ if (parsed && (parsed.family === 'sonnet' || parsed.family === 'opus' || parsed.family === 'fable')
89
+ && parsed.major >= 4) {
90
+ return true;
91
+ }
73
92
  return false;
74
93
  }
75
94
 
@@ -180,6 +199,19 @@ export function applyAnthropicEffortToBody(
180
199
  : {};
181
200
  body.output_config = { ...existing, effort: normalized };
182
201
  }
202
+ // Adaptive-thinking models (4.6+) require `thinking:{type:"adaptive"}`
203
+ // rather than the legacy budget_tokens shape — sending
204
+ // `thinking:{type:"enabled"}` here 400s on sonnet-5/opus-4-7/4-8.
205
+ // display:"summarized" keeps reasoning blocks populated (4.7+ defaults
206
+ // to "omitted", silently hiding reasoning text). Gated on the same
207
+ // modelSupportsEffort() allowlist so older models never receive it.
208
+ // Set unconditionally (independent of `normalized`) so effort-capable
209
+ // turns always carry adaptive thinking + round-trip signatures.
210
+ body.thinking = { type: 'adaptive', display: 'summarized' };
211
+ // Adaptive/4.7+ models reject any non-default sampling param with a 400.
212
+ delete body.temperature;
213
+ delete body.top_p;
214
+ delete body.top_k;
183
215
  return;
184
216
  }
185
217
 
@@ -24,7 +24,7 @@ import {
24
24
  resolveAnthropicModelAfter404,
25
25
  ensureLatestAnthropicModel,
26
26
  } from './anthropic-model-resolve.mjs';
27
- import { sanitizeToolPairs, sanitizeAnthropicContentPairs } from '../session/context-utils.mjs';
27
+ import { sanitizeToolPairs, sanitizeAnthropicContentPairs, foldUserTextIntoToolResultTail } from '../session/context-utils.mjs';
28
28
  import {
29
29
  TOKEN_REFRESH_SKEW_MS,
30
30
  resolveCliVersion,
@@ -303,6 +303,11 @@ function nativeAnthropicTools(opts) {
303
303
  }
304
304
  function deferredAnthropicTools(activeTools, opts) {
305
305
  if (opts?.session?.deferredNativeTools !== true) return [];
306
+ // A request whose ONLY tools are deferred is rejected by the API with
307
+ // `400: At least one tool must have defer_loading=false` — happens on the
308
+ // iteration-cap final turn (loop sends tools: [] to force a text answer).
309
+ // No active tools ⇒ send no deferred catalog either.
310
+ if (!Array.isArray(activeTools) || activeTools.length === 0) return [];
306
311
  const active = new Set((activeTools || []).map((tool) => String(tool?.name || '').trim()).filter(Boolean));
307
312
  const catalog = Array.isArray(opts.session.deferredToolCatalog) ? opts.session.deferredToolCatalog : [];
308
313
  return catalog
@@ -328,6 +333,15 @@ function toAnthropicMessages(messages) {
328
333
  content = m.assistantBlocks.slice();
329
334
  } else {
330
335
  content = [];
336
+ // Adaptive-thinking round-trip: prior-turn thinking blocks are
337
+ // REQUIRED back, unmodified (signature intact; empty thinking
338
+ // field allowed), and MUST precede tool_use blocks. Emit them
339
+ // first, verbatim as received from the SSE parser.
340
+ if (Array.isArray(m.thinkingBlocks)) {
341
+ for (const tb of m.thinkingBlocks) {
342
+ if (tb && typeof tb === 'object') content.push(tb);
343
+ }
344
+ }
331
345
  if (m.content) content.push({ type: 'text', text: m.content });
332
346
  for (const tc of m.toolCalls) {
333
347
  content.push({
@@ -362,6 +376,14 @@ function toAnthropicMessages(messages) {
362
376
  continue;
363
377
  }
364
378
 
379
+ // Claude Code parity: fold a user text turn that directly follows a
380
+ // tool_result turn into that tool_result's content. A sibling text
381
+ // turn after tool_result renders as `</function_results>\n\nHuman:`
382
+ // on the wire and trains the model toward 3-token empty end_turn
383
+ // completions (see foldUserTextIntoToolResultTail).
384
+ if (m.role === 'user' && foldUserTextIntoToolResultTail(result, normalizeContentForAnthropic(m.content))) {
385
+ continue;
386
+ }
365
387
  result.push({ role: m.role, content: normalizeContentForAnthropic(m.content) });
366
388
  }
367
389
  return sanitizeAnthropicContentPairs(result);
@@ -1055,6 +1077,14 @@ export class AnthropicOAuthProvider {
1055
1077
  if (attemptIndex > 0 && firstAttemptError) {
1056
1078
  try { firstAttemptError.midstreamRetries = attemptIndex; } catch {}
1057
1079
  try { firstAttemptError.midstreamClassifier = firstAttemptClassifier; } catch {}
1080
+ // firstAttemptError is what actually propagates here when
1081
+ // live text was emitted this attempt — stamp the unsafe
1082
+ // flags onto IT too, else upstream sees an unmarked error
1083
+ // and retries, duplicating already-streamed output.
1084
+ try {
1085
+ firstAttemptError.liveTextEmitted = true;
1086
+ firstAttemptError.unsafeToRetry = true;
1087
+ } catch {}
1058
1088
  throw firstAttemptError;
1059
1089
  }
1060
1090
  throw err;