mixdog 0.9.2 → 0.9.3
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.
- package/package.json +2 -1
- package/scripts/anthropic-maxtokens-test.mjs +119 -0
- package/scripts/build-tui.mjs +13 -1
- package/scripts/explore-bench.mjs +124 -0
- package/scripts/hook-bus-test.mjs +191 -0
- package/scripts/path-suffix-test.mjs +57 -0
- package/scripts/recall-bench.mjs +207 -0
- package/scripts/tool-smoke.mjs +7 -4
- package/src/agents/debugger/AGENT.md +2 -2
- package/src/agents/heavy-worker/AGENT.md +20 -11
- package/src/agents/reviewer/AGENT.md +2 -2
- package/src/agents/worker/AGENT.md +17 -11
- package/src/mixdog-session-runtime.mjs +424 -1812
- package/src/repl.mjs +5 -5
- package/src/rules/agent/30-explorer.md +8 -11
- package/src/rules/lead/lead-tool.md +9 -5
- package/src/rules/shared/01-tool.md +11 -5
- package/src/runtime/agent/orchestrator/context/collect.mjs +51 -0
- package/src/runtime/agent/orchestrator/mcp/client.mjs +6 -2
- package/src/runtime/agent/orchestrator/providers/anthropic-effort.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/anthropic-max-tokens.mjs +93 -0
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +22 -68
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +46 -7
- package/src/runtime/agent/orchestrator/providers/api-usage.mjs +1 -13
- package/src/runtime/agent/orchestrator/providers/gemini.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/grok-oauth.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/lib/usage-primitives.mjs +32 -0
- package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +54 -20
- package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +19 -12
- package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +7 -5
- package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +33 -23
- package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +31 -14
- package/src/runtime/agent/orchestrator/providers/opencode-go-usage.mjs +38 -12
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +7 -8
- package/src/runtime/agent/orchestrator/session/loop/compact-debug.mjs +28 -0
- package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +262 -0
- package/src/runtime/agent/orchestrator/session/loop/context-overflow.mjs +38 -0
- package/src/runtime/agent/orchestrator/session/loop/env.mjs +14 -0
- package/src/runtime/agent/orchestrator/session/loop/hidden-agents.mjs +21 -0
- package/src/runtime/agent/orchestrator/session/loop/pre-dispatch-deny.mjs +49 -0
- package/src/runtime/agent/orchestrator/session/loop/steering.mjs +63 -0
- package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +100 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-classify.mjs +52 -0
- package/src/runtime/agent/orchestrator/session/loop/tool-helpers.mjs +218 -0
- package/src/runtime/agent/orchestrator/session/loop/transcript-repair.mjs +101 -0
- package/src/runtime/agent/orchestrator/session/loop/usage.mjs +35 -0
- package/src/runtime/agent/orchestrator/session/loop.mjs +169 -918
- package/src/runtime/agent/orchestrator/session/manager/context-meta.mjs +227 -0
- package/src/runtime/agent/orchestrator/session/manager/pending-messages.mjs +235 -0
- package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +137 -0
- package/src/runtime/agent/orchestrator/session/manager/rules-cache.mjs +155 -0
- package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +303 -0
- package/src/runtime/agent/orchestrator/session/manager.mjs +65 -1032
- package/src/runtime/agent/orchestrator/stall-policy.mjs +3 -3
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +2 -2
- package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +241 -0
- package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.test.mjs +162 -0
- package/src/runtime/agent/orchestrator/tools/builtin/fuzzy-match.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/path-diagnostics.mjs +42 -2
- package/src/runtime/agent/orchestrator/tools/builtin/read-formatting.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +11 -4
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +5 -0
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +50 -39
- package/src/runtime/agent/orchestrator/tools/builtin.mjs +11 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +303 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/constants.mjs +43 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +382 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +497 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/graph-binary.mjs +295 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/graph-model.mjs +158 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/lang-predicates.mjs +128 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/memory-cache.mjs +66 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/project-root.mjs +44 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/search.mjs +1192 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/source-access.mjs +81 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/span.mjs +19 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/symbol-index.mjs +280 -0
- package/src/runtime/agent/orchestrator/tools/code-graph/text-mask.mjs +347 -0
- package/src/runtime/agent/orchestrator/tools/code-graph.mjs +36 -4277
- package/src/runtime/agent/orchestrator/tools/patch.mjs +3 -3
- package/src/runtime/agent/orchestrator/tools/progress-message.mjs +1 -2
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +1 -2
- package/src/runtime/agent/orchestrator/tools/shell-snapshot.mjs +2 -4
- package/src/runtime/channels/index.mjs +14 -233
- package/src/runtime/channels/lib/boot-profile.mjs +23 -0
- package/src/runtime/channels/lib/crash-log.mjs +106 -0
- package/src/runtime/channels/lib/index-drop-trace.mjs +72 -0
- package/src/runtime/channels/lib/output-forwarder.mjs +8 -0
- package/src/runtime/channels/lib/telegram-format.mjs +19 -22
- package/src/runtime/channels/lib/whisper-language.mjs +42 -0
- package/src/runtime/memory/index.mjs +314 -359
- package/src/runtime/memory/lib/core-memory-store.mjs +351 -1
- package/src/runtime/memory/lib/cycle-signatures.mjs +34 -0
- package/src/runtime/memory/lib/http-wire.mjs +57 -0
- package/src/runtime/memory/lib/memory-cycle2.mjs +56 -3
- package/src/runtime/memory/lib/memory-recall-scope-filter.mjs +24 -0
- package/src/runtime/memory/lib/memory-retrievers.mjs +8 -0
- package/src/runtime/memory/lib/memory.mjs +20 -0
- package/src/runtime/memory/lib/promotion-fingerprint.mjs +50 -0
- package/src/runtime/memory/lib/recall-format.mjs +183 -0
- package/src/runtime/memory/tool-defs.mjs +4 -4
- package/src/runtime/shared/abort-controller.mjs +1 -1
- package/src/runtime/shared/background-tasks.mjs +2 -3
- package/src/runtime/shared/buffered-appender.mjs +149 -0
- package/src/runtime/shared/task-notification-envelope.mjs +98 -0
- package/src/runtime/shared/task-notification-envelope.test.mjs +107 -0
- package/src/runtime/shared/tool-execution-contract.mjs +2 -2
- package/src/runtime/shared/transcript-writer.mjs +29 -2
- package/src/session-runtime/config-helpers.mjs +209 -0
- package/src/session-runtime/effort.mjs +128 -0
- package/src/session-runtime/fs-utils.mjs +10 -0
- package/src/session-runtime/model-capabilities.mjs +130 -0
- package/src/session-runtime/output-styles.mjs +124 -0
- package/src/session-runtime/plugin-mcp.mjs +114 -0
- package/src/session-runtime/session-text.mjs +100 -0
- package/src/session-runtime/statusline-route.mjs +35 -0
- package/src/session-runtime/tool-catalog.mjs +720 -0
- package/src/session-runtime/workflow.mjs +358 -0
- package/src/standalone/agent-tool.mjs +49 -10
- package/src/standalone/channel-worker.mjs +3 -2
- package/src/standalone/explore-tool.mjs +10 -3
- package/src/standalone/hook-bus.mjs +165 -8
- package/src/standalone/opencode-go-login.mjs +121 -0
- package/src/standalone/provider-admin.mjs +14 -3
- package/src/tui/App.jsx +884 -143
- package/src/tui/components/PromptInput.jsx +272 -15
- package/src/tui/components/ToolExecution.jsx +20 -10
- package/src/tui/components/tool-output-format.mjs +2 -2
- package/src/tui/dist/index.mjs +1771 -1947
- package/src/tui/engine/agent-envelope.mjs +296 -0
- package/src/tui/engine/boot-profile.mjs +21 -0
- package/src/tui/engine/labels.mjs +67 -0
- package/src/tui/engine/notice-text.mjs +112 -0
- package/src/tui/engine/queue-helpers.mjs +161 -0
- package/src/tui/engine/session-stats.mjs +46 -0
- package/src/tui/engine/tool-call-fields.mjs +23 -0
- package/src/tui/engine/tool-result-text.mjs +126 -0
- package/src/tui/engine.mjs +311 -851
- package/src/tui/input-editing.mjs +58 -8
- package/src/tui/input-editing.selection.test.mjs +75 -0
- package/src/tui/keyboard-protocol.mjs +2 -2
- package/src/tui/lib/voice-recorder.mjs +35 -19
- package/src/tui/markdown/format-token.mjs +7 -8
- package/src/tui/markdown/format-token.test.mjs +3 -3
- package/src/tui/paste-attachments.mjs +38 -0
- package/src/tui/paste-fix.test.mjs +119 -0
- package/src/tui/themes/base.mjs +2 -2
- package/src/tui/themes/kanagawa.mjs +4 -4
- package/src/tui/themes/teal.mjs +4 -5
- package/src/tui/themes/utils.mjs +1 -1
- package/src/ui/statusline.mjs +49 -0
- package/src/workflows/default/WORKFLOW.md +16 -9
- package/src/workflows/sequential/WORKFLOW.md +16 -11
- package/src/workflows/solo/WORKFLOW.md +5 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "mixdog",
|
|
3
|
-
"version": "0.9.
|
|
3
|
+
"version": "0.9.3",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "Standalone mixdog coding-agent CLI/TUI workspace.",
|
|
@@ -51,6 +51,7 @@
|
|
|
51
51
|
"bench:task": "node scripts/task-bench.mjs",
|
|
52
52
|
"bench:corpus": "node scripts/routing-corpus.mjs",
|
|
53
53
|
"bench:run": "node scripts/bench-run.mjs",
|
|
54
|
+
"bench:recall": "node scripts/recall-bench.mjs",
|
|
54
55
|
"patch:replay": "node scripts/patch-replay.mjs",
|
|
55
56
|
"build:tui": "node scripts/build-tui.mjs"
|
|
56
57
|
},
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import test from 'node:test';
|
|
3
|
+
import assert from 'node:assert/strict';
|
|
4
|
+
import { _test } from '../src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs';
|
|
5
|
+
import { _test as _apiKeyTest } from '../src/runtime/agent/orchestrator/providers/anthropic.mjs';
|
|
6
|
+
|
|
7
|
+
const { resolveMaxTokens } = _test;
|
|
8
|
+
const { resolveMaxTokens: resolveMaxTokensApiKey } = _apiKeyTest;
|
|
9
|
+
const ENV_VAR = 'MIXDOG_ANTHROPIC_MAX_OUTPUT_TOKENS';
|
|
10
|
+
|
|
11
|
+
function withEnvOverride(value, fn) {
|
|
12
|
+
const had = Object.prototype.hasOwnProperty.call(process.env, ENV_VAR);
|
|
13
|
+
const prev = process.env[ENV_VAR];
|
|
14
|
+
try {
|
|
15
|
+
if (value == null) delete process.env[ENV_VAR];
|
|
16
|
+
else process.env[ENV_VAR] = value;
|
|
17
|
+
fn();
|
|
18
|
+
} finally {
|
|
19
|
+
if (had) process.env[ENV_VAR] = prev;
|
|
20
|
+
else delete process.env[ENV_VAR];
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
test('claude-sonnet-5 resolves to 65536 (catalog or fallback)', () => {
|
|
25
|
+
withEnvOverride(null, () => {
|
|
26
|
+
assert.equal(resolveMaxTokens('claude-sonnet-5'), 65536);
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('unknown model id falls back to a sane >=8192 value', () => {
|
|
31
|
+
withEnvOverride(null, () => {
|
|
32
|
+
const result = resolveMaxTokens('claude-totally-unknown-model-xyz');
|
|
33
|
+
assert.equal(typeof result, 'number');
|
|
34
|
+
assert.ok(Number.isFinite(result));
|
|
35
|
+
assert.ok(result >= 8192, `expected >=8192, got ${result}`);
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('MIXDOG_ANTHROPIC_MAX_OUTPUT_TOKENS=32768 overrides the result', () => {
|
|
40
|
+
withEnvOverride('32768', () => {
|
|
41
|
+
assert.equal(resolveMaxTokens('claude-sonnet-5'), 32768);
|
|
42
|
+
assert.equal(resolveMaxTokens('claude-totally-unknown-model-xyz'), 32768);
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('invalid MIXDOG_ANTHROPIC_MAX_OUTPUT_TOKENS=0 does not override', () => {
|
|
47
|
+
withEnvOverride(null, () => {
|
|
48
|
+
const baseline = resolveMaxTokens('claude-sonnet-5');
|
|
49
|
+
withEnvOverride('0', () => {
|
|
50
|
+
assert.equal(resolveMaxTokens('claude-sonnet-5'), baseline);
|
|
51
|
+
assert.notEqual(baseline, 0);
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
// Regression guard for the reviewer-caught masking gap: sonnet-5's baseline
|
|
57
|
+
// already equals DEFAULT_SAFETY_CAP (65536), so an invalid override that
|
|
58
|
+
// wrongly returns the default cap is invisible on sonnet-5. Assert on models
|
|
59
|
+
// whose baseline is BELOW the cap — an invalid env value must leave them at
|
|
60
|
+
// their catalog/fallback value, never jump them to 65536.
|
|
61
|
+
test('invalid env values do not bypass catalog/fallback on low-cap models', () => {
|
|
62
|
+
withEnvOverride(null, () => {
|
|
63
|
+
const sonnet46 = resolveMaxTokens('claude-sonnet-4-6');
|
|
64
|
+
const unknown = resolveMaxTokens('claude-totally-unknown-model-xyz');
|
|
65
|
+
for (const bad of ['0', '-1', 'garbage', ' ']) {
|
|
66
|
+
withEnvOverride(bad, () => {
|
|
67
|
+
assert.equal(resolveMaxTokens('claude-sonnet-4-6'), sonnet46, `env=${JSON.stringify(bad)}`);
|
|
68
|
+
assert.equal(resolveMaxTokens('claude-totally-unknown-model-xyz'), unknown, `env=${JSON.stringify(bad)}`);
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
});
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// --- API-key provider (anthropic.mjs) — shares the anthropic-max-tokens.mjs
|
|
75
|
+
// helper and reads the same on-disk anthropic-oauth-models.json catalog.
|
|
76
|
+
|
|
77
|
+
test('API-key provider: claude-sonnet-5 resolves to 65536 (catalog or fallback)', () => {
|
|
78
|
+
withEnvOverride(null, () => {
|
|
79
|
+
assert.equal(resolveMaxTokensApiKey('claude-sonnet-5'), 65536);
|
|
80
|
+
});
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('API-key provider: claude-sonnet-4-6 stays at legacy 16384 when catalog has no entry', () => {
|
|
84
|
+
withEnvOverride(null, () => {
|
|
85
|
+
const result = resolveMaxTokensApiKey('claude-sonnet-4-6');
|
|
86
|
+
assert.ok(result === 16384 || result >= 16384, `expected >=16384, got ${result}`);
|
|
87
|
+
});
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
test('API-key provider: haiku no longer starves visible output at 8192-only ceiling', () => {
|
|
91
|
+
withEnvOverride(null, () => {
|
|
92
|
+
const result = resolveMaxTokensApiKey('claude-haiku-4-5-20251001');
|
|
93
|
+
assert.ok(result >= 8192, `expected >=8192, got ${result}`);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
test('API-key provider: unknown model id falls back to a sane >=8192 value', () => {
|
|
98
|
+
withEnvOverride(null, () => {
|
|
99
|
+
const result = resolveMaxTokensApiKey('claude-totally-unknown-model-xyz');
|
|
100
|
+
assert.equal(typeof result, 'number');
|
|
101
|
+
assert.ok(Number.isFinite(result));
|
|
102
|
+
assert.ok(result >= 8192, `expected >=8192, got ${result}`);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test('API-key provider: MIXDOG_ANTHROPIC_MAX_OUTPUT_TOKENS=32768 overrides the result', () => {
|
|
107
|
+
withEnvOverride('32768', () => {
|
|
108
|
+
assert.equal(resolveMaxTokensApiKey('claude-sonnet-5'), 32768);
|
|
109
|
+
assert.equal(resolveMaxTokensApiKey('claude-totally-unknown-model-xyz'), 32768);
|
|
110
|
+
});
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
test('OAuth and API-key providers agree on fallback heuristic for the same model id', () => {
|
|
114
|
+
withEnvOverride(null, () => {
|
|
115
|
+
for (const id of ['claude-opus-4-8', 'claude-sonnet-4-6', 'claude-sonnet-5', 'claude-haiku-4-5-20251001', 'claude-totally-unknown-model-xyz']) {
|
|
116
|
+
assert.equal(resolveMaxTokens(id), resolveMaxTokensApiKey(id), `mismatch for ${id}`);
|
|
117
|
+
}
|
|
118
|
+
});
|
|
119
|
+
});
|
package/scripts/build-tui.mjs
CHANGED
|
@@ -44,7 +44,19 @@ await build({
|
|
|
44
44
|
// Only `ink` is redirected to Mixdog's checked-in renderer instead of
|
|
45
45
|
// node_modules/ink.
|
|
46
46
|
packages: 'external',
|
|
47
|
-
external: [
|
|
47
|
+
external: [
|
|
48
|
+
'../vendor/*',
|
|
49
|
+
'../../vendor/*',
|
|
50
|
+
// Voice runtime modules stay external (lazy dynamic imports by design):
|
|
51
|
+
// voice-runtime-fetcher.mjs resolves its bundled manifest via
|
|
52
|
+
// import.meta.url ('../data/voice-runtime-manifest.json'), which breaks
|
|
53
|
+
// when inlined into src/tui/dist/index.mjs (resolves to src/tui/data/).
|
|
54
|
+
// The '../../runtime/...' specifier is depth-safe: src/tui/lib/* and
|
|
55
|
+
// src/tui/dist/* are both 2 levels below src/, so the relative path
|
|
56
|
+
// resolves to src/runtime/channels/lib/* either way.
|
|
57
|
+
'../../runtime/channels/lib/voice-runtime-fetcher.mjs',
|
|
58
|
+
'../../runtime/channels/lib/whisper-server.mjs',
|
|
59
|
+
],
|
|
48
60
|
plugins: [mixdogInkAliasPlugin],
|
|
49
61
|
logLevel: 'info',
|
|
50
62
|
});
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
// Explore quality bench — round-based tuning harness.
|
|
2
|
+
//
|
|
3
|
+
// Runs a fixed query set through runExplore against this repo, then reads
|
|
4
|
+
// agent-trace.jsonl to report per-session tool-call counts, wall time, and
|
|
5
|
+
// anchor quality. Usage:
|
|
6
|
+
// node scripts/explore-bench.mjs [roundLabel]
|
|
7
|
+
// Fresh process per run = no in-memory result cache carryover.
|
|
8
|
+
import { readFileSync, existsSync } from 'node:fs';
|
|
9
|
+
import { join } from 'node:path';
|
|
10
|
+
import { homedir } from 'node:os';
|
|
11
|
+
import { runExplore } from '../src/standalone/explore-tool.mjs';
|
|
12
|
+
|
|
13
|
+
const ROUND = process.argv[2] || 'r0';
|
|
14
|
+
const CWD = 'C:/Project/mixdog';
|
|
15
|
+
|
|
16
|
+
// Representative locator queries with ground truth: expect = substrings, at
|
|
17
|
+
// least one must appear in the anchor output (quality hit). expectFail = the
|
|
18
|
+
// correct answer is EXPLORATION_FAILED (miss discipline).
|
|
19
|
+
const QUERIES = [
|
|
20
|
+
{
|
|
21
|
+
q: 'where is the tool-call stall watchdog policy resolved and what defaults does it use',
|
|
22
|
+
expect: ['agent-progress-watchdog', 'stall-policy'],
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
q: 'session compact / smart compact trigger threshold logic',
|
|
26
|
+
expect: ['compact'],
|
|
27
|
+
},
|
|
28
|
+
{
|
|
29
|
+
q: 'where are hidden agent tool schema profiles defined (read / read-write-search)',
|
|
30
|
+
expect: ['agent-dispatch', 'agents.json', 'internal-agents'],
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
q: 'recall memory chunk importance scoring implementation',
|
|
34
|
+
expect: ['importance', 'memory'],
|
|
35
|
+
},
|
|
36
|
+
{
|
|
37
|
+
q: 'websocket reconnect backoff handling',
|
|
38
|
+
expect: ['openai-oauth-ws', 'retry-classifier'],
|
|
39
|
+
},
|
|
40
|
+
// ── extended set: symbol-exact, filename, error-string, korean, flow, config ──
|
|
41
|
+
{
|
|
42
|
+
q: 'buildExplorerPrompt definition', // exact known symbol
|
|
43
|
+
expect: ['explore-tool'],
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
q: 'where does the explore tool cache results and what is the TTL',
|
|
47
|
+
expect: ['explore-tool'],
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
q: 'error text "maintenance route unresolved for agent" — where is it thrown',
|
|
51
|
+
expect: ['agent-dispatch'],
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
q: '에이전트 세션의 최대 루프 반복 횟수는 어디서 정해져?', // korean concept query
|
|
55
|
+
expect: ['agent-loop-policy'],
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
q: 'how does a fixed agent slot map to a workflow slot (explore/explorer)',
|
|
59
|
+
expect: ['mixdog-session-runtime', 'internal-agents'],
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
q: 'where is the mixdog config json file path resolved (data dir)',
|
|
63
|
+
expect: ['config', 'plugin-paths'],
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
q: 'GraphQL schema stitching resolver implementation', // not in this repo
|
|
67
|
+
expectFail: true,
|
|
68
|
+
},
|
|
69
|
+
];
|
|
70
|
+
|
|
71
|
+
function tracePath() {
|
|
72
|
+
return process.env.MIXDOG_AGENT_TRACE_PATH
|
|
73
|
+
|| join(process.env.MIXDOG_DATA_DIR || join(process.env.MIXDOG_HOME || join(homedir(), '.mixdog'), 'data'), 'history', 'agent-trace.jsonl');
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function readTraceSince(ts) {
|
|
77
|
+
const p = tracePath();
|
|
78
|
+
if (!existsSync(p)) return [];
|
|
79
|
+
const rows = [];
|
|
80
|
+
for (const line of readFileSync(p, 'utf8').split('\n')) {
|
|
81
|
+
if (!line) continue;
|
|
82
|
+
try {
|
|
83
|
+
const r = JSON.parse(line);
|
|
84
|
+
if (r.ts >= ts && r.agent === 'explorer') rows.push(r);
|
|
85
|
+
} catch { /* skip */ }
|
|
86
|
+
}
|
|
87
|
+
return rows;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const t0 = Date.now();
|
|
91
|
+
const results = [];
|
|
92
|
+
for (const { q, expect, expectFail } of QUERIES) {
|
|
93
|
+
const qt0 = Date.now();
|
|
94
|
+
const res = await runExplore({ query: q, cwd: CWD }, { callerCwd: CWD });
|
|
95
|
+
const text = res?.content?.[0]?.text || '';
|
|
96
|
+
const anchors = text.split('\n').filter((l) => /:\d+/.test(l)).length;
|
|
97
|
+
const failed = /EXPLORATION_FAILED/.test(text);
|
|
98
|
+
const lower = text.toLowerCase();
|
|
99
|
+
const hit = expectFail
|
|
100
|
+
? (failed && anchors === 0)
|
|
101
|
+
: (!failed && (expect || []).some((e) => lower.includes(e.toLowerCase())));
|
|
102
|
+
results.push({ q: q.slice(0, 48), ms: Date.now() - qt0, anchors, failed, hit, bytes: text.length });
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// Attribute tool calls per explorer session spawned during this run.
|
|
106
|
+
const rows = readTraceSince(t0);
|
|
107
|
+
const bySession = new Map();
|
|
108
|
+
for (const r of rows) {
|
|
109
|
+
if (r.kind !== 'tool') continue;
|
|
110
|
+
const s = bySession.get(r.session_id) || { calls: 0, tools: [] };
|
|
111
|
+
s.calls++; s.tools.push(r.tool_name);
|
|
112
|
+
bySession.set(r.session_id, s);
|
|
113
|
+
}
|
|
114
|
+
const callCounts = [...bySession.values()].map((s) => s.calls).sort((a, b) => a - b);
|
|
115
|
+
const p50 = callCounts[Math.floor(callCounts.length / 2)] ?? 0;
|
|
116
|
+
|
|
117
|
+
console.log(`\n=== explore-bench round=${ROUND} ===`);
|
|
118
|
+
for (const r of results) {
|
|
119
|
+
console.log(` [${String(r.ms).padStart(6)}ms] ${r.hit ? 'HIT ' : 'MISS'} anchors=${r.anchors} failed=${r.failed} bytes=${r.bytes} ${r.q}`);
|
|
120
|
+
}
|
|
121
|
+
console.log(` quality: ${results.filter((r) => r.hit).length}/${results.length} hits`);
|
|
122
|
+
console.log(` sessions=${bySession.size} toolcalls p50=${p50} max=${callCounts[callCounts.length - 1] ?? 0} total=${callCounts.reduce((a, b) => a + b, 0)}`);
|
|
123
|
+
for (const [id, s] of bySession) console.log(` ${id.slice(-8)}: ${s.tools.join(',')}`);
|
|
124
|
+
console.log(` wall total=${((Date.now() - t0) / 1000).toFixed(1)}s`);
|
|
@@ -328,3 +328,194 @@ console.log(JSON.stringify({ hookSpecificOutput: {
|
|
|
328
328
|
else process.env.MIXDOG_HOOKS_FILE = prev;
|
|
329
329
|
}
|
|
330
330
|
});
|
|
331
|
+
|
|
332
|
+
test('mcp_tool handler routes output through parseHandlerOutput', async () => {
|
|
333
|
+
const root = tempRoot();
|
|
334
|
+
const hooksFile = join(root, 'hooks.json');
|
|
335
|
+
writeJson(hooksFile, {
|
|
336
|
+
hooks: {
|
|
337
|
+
PreToolUse: [{
|
|
338
|
+
matcher: 'shell',
|
|
339
|
+
hooks: [{ type: 'mcp_tool', server: 'guard', tool: 'check' }],
|
|
340
|
+
}],
|
|
341
|
+
},
|
|
342
|
+
});
|
|
343
|
+
const prev = process.env.MIXDOG_HOOKS_FILE;
|
|
344
|
+
process.env.MIXDOG_HOOKS_FILE = hooksFile;
|
|
345
|
+
try {
|
|
346
|
+
let seenName = null;
|
|
347
|
+
const bus = createStandaloneHookBus({
|
|
348
|
+
dataDir: root,
|
|
349
|
+
mcpToolRunner: async ({ name }) => {
|
|
350
|
+
seenName = name;
|
|
351
|
+
return JSON.stringify({ hookSpecificOutput: {
|
|
352
|
+
hookEventName: 'PreToolUse',
|
|
353
|
+
permissionDecision: 'deny',
|
|
354
|
+
permissionDecisionReason: 'mcp denied',
|
|
355
|
+
}});
|
|
356
|
+
},
|
|
357
|
+
});
|
|
358
|
+
const denied = await bus.beforeTool({ sessionId: 's', cwd: root, name: 'shell', args: { command: 'x' } });
|
|
359
|
+
assert.equal(seenName, 'mcp__guard__check');
|
|
360
|
+
assert.equal(denied.action, 'deny');
|
|
361
|
+
assert.match(denied.reason, /mcp denied/);
|
|
362
|
+
} finally {
|
|
363
|
+
if (prev == null) delete process.env.MIXDOG_HOOKS_FILE;
|
|
364
|
+
else process.env.MIXDOG_HOOKS_FILE = prev;
|
|
365
|
+
}
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
test('prompt handler ok:false maps to deny', async () => {
|
|
369
|
+
const root = tempRoot();
|
|
370
|
+
const hooksFile = join(root, 'hooks.json');
|
|
371
|
+
writeJson(hooksFile, {
|
|
372
|
+
hooks: {
|
|
373
|
+
PreToolUse: [{
|
|
374
|
+
matcher: 'shell',
|
|
375
|
+
hooks: [{ type: 'prompt', prompt: 'is this safe?' }],
|
|
376
|
+
}],
|
|
377
|
+
},
|
|
378
|
+
});
|
|
379
|
+
const prev = process.env.MIXDOG_HOOKS_FILE;
|
|
380
|
+
process.env.MIXDOG_HOOKS_FILE = hooksFile;
|
|
381
|
+
try {
|
|
382
|
+
const bus = createStandaloneHookBus({
|
|
383
|
+
dataDir: root,
|
|
384
|
+
promptRunner: async () => JSON.stringify({ ok: false, reason: 'unsafe' }),
|
|
385
|
+
});
|
|
386
|
+
const denied = await bus.beforeTool({ sessionId: 's', cwd: root, name: 'shell', args: { command: 'x' } });
|
|
387
|
+
assert.equal(denied.action, 'deny');
|
|
388
|
+
assert.match(denied.reason, /unsafe/);
|
|
389
|
+
} finally {
|
|
390
|
+
if (prev == null) delete process.env.MIXDOG_HOOKS_FILE;
|
|
391
|
+
else process.env.MIXDOG_HOOKS_FILE = prev;
|
|
392
|
+
}
|
|
393
|
+
});
|
|
394
|
+
|
|
395
|
+
test('unconfigured prompt/mcp_tool and unsupported agent emit hook:error and skip', async () => {
|
|
396
|
+
const root = tempRoot();
|
|
397
|
+
const hooksFile = join(root, 'hooks.json');
|
|
398
|
+
writeJson(hooksFile, {
|
|
399
|
+
hooks: {
|
|
400
|
+
PreToolUse: [{
|
|
401
|
+
matcher: 'shell',
|
|
402
|
+
hooks: [
|
|
403
|
+
{ type: 'prompt', prompt: 'x' },
|
|
404
|
+
{ type: 'mcp_tool', server: 'g', tool: 't' },
|
|
405
|
+
{ type: 'agent', agent: 'a' },
|
|
406
|
+
],
|
|
407
|
+
}],
|
|
408
|
+
},
|
|
409
|
+
});
|
|
410
|
+
const prev = process.env.MIXDOG_HOOKS_FILE;
|
|
411
|
+
process.env.MIXDOG_HOOKS_FILE = hooksFile;
|
|
412
|
+
try {
|
|
413
|
+
const bus = createStandaloneHookBus({ dataDir: root });
|
|
414
|
+
const result = await bus.beforeTool({ sessionId: 's', cwd: root, name: 'shell', args: { command: 'x' } });
|
|
415
|
+
assert.equal(result, null);
|
|
416
|
+
const errs = bus.status().recent.filter((e) => e.name === 'hook:error');
|
|
417
|
+
assert.ok(errs.some((e) => /prompt not configured/.test(e.payload?.error || '')));
|
|
418
|
+
assert.ok(errs.some((e) => /mcp_tool not configured/.test(e.payload?.error || '')));
|
|
419
|
+
assert.ok(errs.some((e) => /unsupported hook type: agent/.test(e.payload?.error || '')));
|
|
420
|
+
} finally {
|
|
421
|
+
if (prev == null) delete process.env.MIXDOG_HOOKS_FILE;
|
|
422
|
+
else process.env.MIXDOG_HOOKS_FILE = prev;
|
|
423
|
+
}
|
|
424
|
+
});
|
|
425
|
+
|
|
426
|
+
test('setRewakeHandler fires on async child exit code 2', async () => {
|
|
427
|
+
const root = tempRoot();
|
|
428
|
+
const hookScript = join(root, 'rewake.mjs');
|
|
429
|
+
writeFileSync(hookScript, `process.stderr.write('needs rewake'); process.exit(2);`, 'utf8');
|
|
430
|
+
const hooksFile = join(root, 'hooks.json');
|
|
431
|
+
writeJson(hooksFile, {
|
|
432
|
+
hooks: {
|
|
433
|
+
Stop: [{
|
|
434
|
+
hooks: [{ type: 'command', command: process.execPath, args: [hookScript], asyncRewake: true }],
|
|
435
|
+
}],
|
|
436
|
+
},
|
|
437
|
+
});
|
|
438
|
+
const prev = process.env.MIXDOG_HOOKS_FILE;
|
|
439
|
+
process.env.MIXDOG_HOOKS_FILE = hooksFile;
|
|
440
|
+
try {
|
|
441
|
+
const bus = createStandaloneHookBus({ dataDir: root });
|
|
442
|
+
const fired = new Promise((resolve) => bus.setRewakeHandler((info) => resolve(info)));
|
|
443
|
+
await bus.dispatch('Stop', { session_id: 's', cwd: root });
|
|
444
|
+
const info = await fired;
|
|
445
|
+
assert.equal(info.eventName, 'Stop');
|
|
446
|
+
assert.match(info.text, /needs rewake/);
|
|
447
|
+
} finally {
|
|
448
|
+
if (prev == null) delete process.env.MIXDOG_HOOKS_FILE;
|
|
449
|
+
else process.env.MIXDOG_HOOKS_FILE = prev;
|
|
450
|
+
}
|
|
451
|
+
});
|
|
452
|
+
|
|
453
|
+
test('prompt handler plain-text "no" denies', async () => {
|
|
454
|
+
const root = tempRoot();
|
|
455
|
+
const hooksFile = join(root, 'hooks.json');
|
|
456
|
+
writeJson(hooksFile, {
|
|
457
|
+
hooks: {
|
|
458
|
+
PreToolUse: [{ matcher: 'shell', hooks: [{ type: 'prompt', prompt: 'safe?' }] }],
|
|
459
|
+
},
|
|
460
|
+
});
|
|
461
|
+
const prev = process.env.MIXDOG_HOOKS_FILE;
|
|
462
|
+
process.env.MIXDOG_HOOKS_FILE = hooksFile;
|
|
463
|
+
try {
|
|
464
|
+
const bus = createStandaloneHookBus({ dataDir: root, promptRunner: async () => 'no' });
|
|
465
|
+
const denied = await bus.beforeTool({ sessionId: 's', cwd: root, name: 'shell', args: { command: 'x' } });
|
|
466
|
+
assert.equal(denied.action, 'deny');
|
|
467
|
+
assert.match(denied.reason, /no/i);
|
|
468
|
+
} finally {
|
|
469
|
+
if (prev == null) delete process.env.MIXDOG_HOOKS_FILE;
|
|
470
|
+
else process.env.MIXDOG_HOOKS_FILE = prev;
|
|
471
|
+
}
|
|
472
|
+
});
|
|
473
|
+
|
|
474
|
+
test('prompt handler timeout does not hang and reports error', async () => {
|
|
475
|
+
const root = tempRoot();
|
|
476
|
+
const hooksFile = join(root, 'hooks.json');
|
|
477
|
+
writeJson(hooksFile, {
|
|
478
|
+
hooks: {
|
|
479
|
+
PreToolUse: [{ matcher: 'shell', hooks: [{ type: 'prompt', prompt: 'x', timeout: 0.05 }] }],
|
|
480
|
+
},
|
|
481
|
+
});
|
|
482
|
+
const prev = process.env.MIXDOG_HOOKS_FILE;
|
|
483
|
+
process.env.MIXDOG_HOOKS_FILE = hooksFile;
|
|
484
|
+
try {
|
|
485
|
+
const bus = createStandaloneHookBus({
|
|
486
|
+
dataDir: root,
|
|
487
|
+
promptRunner: () => new Promise(() => {}),
|
|
488
|
+
});
|
|
489
|
+
const result = await bus.beforeTool({ sessionId: 's', cwd: root, name: 'shell', args: { command: 'x' } });
|
|
490
|
+
assert.equal(result, null);
|
|
491
|
+
const errs = bus.status().recent.filter((e) => e.name === 'hook:error');
|
|
492
|
+
assert.ok(errs.some((e) => /timed out/i.test(e.payload?.error || '')));
|
|
493
|
+
} finally {
|
|
494
|
+
if (prev == null) delete process.env.MIXDOG_HOOKS_FILE;
|
|
495
|
+
else process.env.MIXDOG_HOOKS_FILE = prev;
|
|
496
|
+
}
|
|
497
|
+
});
|
|
498
|
+
|
|
499
|
+
test('prompt deny uses universal exit-2 path on Stop event', async () => {
|
|
500
|
+
const root = tempRoot();
|
|
501
|
+
const hooksFile = join(root, 'hooks.json');
|
|
502
|
+
writeJson(hooksFile, {
|
|
503
|
+
hooks: {
|
|
504
|
+
Stop: [{ hooks: [{ type: 'prompt', prompt: 'done?' }] }],
|
|
505
|
+
},
|
|
506
|
+
});
|
|
507
|
+
const prev = process.env.MIXDOG_HOOKS_FILE;
|
|
508
|
+
process.env.MIXDOG_HOOKS_FILE = hooksFile;
|
|
509
|
+
try {
|
|
510
|
+
const bus = createStandaloneHookBus({
|
|
511
|
+
dataDir: root,
|
|
512
|
+
promptRunner: async () => JSON.stringify({ ok: false, reason: 'not done' }),
|
|
513
|
+
});
|
|
514
|
+
const result = await bus.dispatch('Stop', { session_id: 's', cwd: root });
|
|
515
|
+
assert.equal(result.blocked, true);
|
|
516
|
+
assert.match(result.reason, /not done/);
|
|
517
|
+
} finally {
|
|
518
|
+
if (prev == null) delete process.env.MIXDOG_HOOKS_FILE;
|
|
519
|
+
else process.env.MIXDOG_HOOKS_FILE = prev;
|
|
520
|
+
}
|
|
521
|
+
});
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import test from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { mkdtempSync, mkdirSync, writeFileSync, rmSync } from 'node:fs';
|
|
4
|
+
import { tmpdir } from 'node:os';
|
|
5
|
+
import { join } from 'node:path';
|
|
6
|
+
import { findBySuffixStrip } from '../src/runtime/agent/orchestrator/tools/builtin/path-diagnostics.mjs';
|
|
7
|
+
|
|
8
|
+
function makeRepo() {
|
|
9
|
+
const root = mkdtempSync(join(tmpdir(), 'mixdog-suffix-test-'));
|
|
10
|
+
mkdirSync(join(root, 'src', 'tui'), { recursive: true });
|
|
11
|
+
writeFileSync(join(root, 'src', 'tui', 'input-editing.mjs'), '// real file\n');
|
|
12
|
+
mkdirSync(join(root, 'vendor', 'ink', 'build'), { recursive: true });
|
|
13
|
+
writeFileSync(join(root, 'vendor', 'ink', 'build', 'index.js'), '// vendored\n');
|
|
14
|
+
return root;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
test('resolves a hallucinated absolute prefix to the real repo-relative file', () => {
|
|
18
|
+
const root = makeRepo();
|
|
19
|
+
try {
|
|
20
|
+
const hallucinated = '/Users/danma/Local/Project/ink/src/tui/input-editing.mjs';
|
|
21
|
+
const hit = findBySuffixStrip(root, hallucinated);
|
|
22
|
+
assert.equal(hit, 'src/tui/input-editing.mjs');
|
|
23
|
+
} finally {
|
|
24
|
+
rmSync(root, { recursive: true, force: true });
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
test('resolves a hallucinated prefix onto a vendor/ path (no skip-dir filtering)', () => {
|
|
29
|
+
const root = makeRepo();
|
|
30
|
+
try {
|
|
31
|
+
const hallucinated = '/Users/danma/Local/Project/vendor/ink/build/index.js';
|
|
32
|
+
const hit = findBySuffixStrip(root, hallucinated);
|
|
33
|
+
assert.equal(hit, 'vendor/ink/build/index.js');
|
|
34
|
+
} finally {
|
|
35
|
+
rmSync(root, { recursive: true, force: true });
|
|
36
|
+
}
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
test('returns null when no tail length matches a real file', () => {
|
|
40
|
+
const root = makeRepo();
|
|
41
|
+
try {
|
|
42
|
+
const hit = findBySuffixStrip(root, '/Users/danma/Local/Project/does/not/exist.mjs');
|
|
43
|
+
assert.equal(hit, null);
|
|
44
|
+
} finally {
|
|
45
|
+
rmSync(root, { recursive: true, force: true });
|
|
46
|
+
}
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
test('a bare basename (1 segment) is never matched', () => {
|
|
50
|
+
const root = makeRepo();
|
|
51
|
+
try {
|
|
52
|
+
const hit = findBySuffixStrip(root, 'input-editing.mjs');
|
|
53
|
+
assert.equal(hit, null);
|
|
54
|
+
} finally {
|
|
55
|
+
rmSync(root, { recursive: true, force: true });
|
|
56
|
+
}
|
|
57
|
+
});
|