mixdog 0.9.23 → 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.
- package/package.json +1 -1
- package/scripts/boot-smoke.mjs +1 -1
- package/scripts/build-runtime-windows.ps1 +242 -242
- package/scripts/channel-daemon-smoke.mjs +327 -9
- package/scripts/channel-daemon-stub.mjs +12 -1
- package/scripts/debounced-skills-async-save-test.mjs +57 -0
- package/scripts/explore-bench-tmp.mjs +17 -0
- package/scripts/find-fuzzy-hidden-test.mjs +145 -0
- package/scripts/mcp-grace-deferred-test.mjs +149 -0
- package/scripts/recall-usecase-cases.json +1 -1
- package/scripts/smoke-runtime-negative.ps1 +106 -106
- package/scripts/tool-efficiency-diag.mjs +1 -1
- package/scripts/tool-smoke.mjs +38 -30
- package/src/rules/agent/30-explorer.md +6 -0
- package/src/rules/lead/02-channels.md +3 -3
- package/src/rules/shared/01-tool.md +11 -4
- package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +76 -1
- package/src/runtime/agent/orchestrator/config.mjs +33 -7
- package/src/runtime/agent/orchestrator/context/collect.mjs +43 -8
- package/src/runtime/agent/orchestrator/mcp/child-tree.mjs +39 -0
- package/src/runtime/agent/orchestrator/mcp/client.mjs +145 -31
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +38 -1
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +39 -1
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +24 -7
- package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +11 -1
- package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +7 -3
- package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +40 -6
- package/src/runtime/agent/orchestrator/session/tool-batch.mjs +2 -1
- package/src/runtime/agent/orchestrator/tools/bash-session.mjs +13 -5
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +62 -24
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +7 -6
- package/src/runtime/agent/orchestrator/tools/builtin/cache-layers.mjs +20 -0
- package/src/runtime/agent/orchestrator/tools/builtin/fuzzy-match.mjs +34 -3
- package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +220 -27
- package/src/runtime/agent/orchestrator/tools/builtin/shell-analysis.mjs +61 -0
- package/src/runtime/agent/orchestrator/tools/builtin/shell-jobs.mjs +43 -16
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +54 -5
- package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +97 -54
- package/src/runtime/agent/orchestrator/tools/patch/paths.mjs +49 -31
- package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +35 -2
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +70 -21
- package/src/runtime/channels/lib/crash-log.mjs +4 -2
- package/src/runtime/channels/lib/output-forwarder.mjs +2 -1
- package/src/runtime/channels/lib/owned-runtime.mjs +65 -180
- package/src/runtime/channels/lib/owner-heartbeat.mjs +9 -13
- package/src/runtime/channels/lib/runtime-paths.mjs +6 -6
- package/src/runtime/channels/lib/tool-dispatch.mjs +9 -17
- package/src/runtime/channels/lib/tool-format.mjs +7 -2
- package/src/runtime/channels/lib/worker-main.mjs +9 -28
- package/src/runtime/memory/lib/query-handlers.mjs +4 -1
- package/src/runtime/memory/lib/recall-format.mjs +7 -3
- package/src/runtime/memory/tool-defs.mjs +1 -1
- package/src/runtime/shared/atomic-file.mjs +130 -2
- package/src/runtime/shared/background-tasks.mjs +1 -1
- package/src/runtime/shared/config.mjs +53 -1
- package/src/runtime/shared/tool-execution-contract.mjs +1 -1
- package/src/runtime/shared/tool-surface.mjs +19 -0
- package/src/runtime/shared/update-checker.mjs +3 -0
- package/src/runtime/shared/user-data-guard.mjs +66 -0
- package/src/session-runtime/config-lifecycle.mjs +175 -15
- package/src/session-runtime/mcp-glue.mjs +30 -0
- package/src/session-runtime/runtime-core.mjs +91 -7
- package/src/session-runtime/session-turn-api.mjs +42 -16
- package/src/session-runtime/tool-catalog.mjs +44 -0
- package/src/standalone/channel-admin.mjs +32 -3
- package/src/standalone/channel-daemon-client.mjs +3 -1
- package/src/standalone/channel-daemon-transport.mjs +202 -8
- package/src/standalone/channel-daemon.mjs +54 -17
- package/src/standalone/channel-worker.mjs +18 -7
- package/src/standalone/explore-tool.mjs +87 -15
- package/src/tui/App.jsx +2 -2
- package/src/tui/components/StatusLine.jsx +3 -3
- package/src/tui/components/ToolExecution.jsx +14 -2
- package/src/tui/components/TranscriptItem.jsx +1 -1
- package/src/tui/dist/index.mjs +209 -44
- package/src/tui/engine/agent-job-feed.mjs +5 -0
- package/src/tui/engine/notification-plan.mjs +5 -0
- package/src/tui/engine/session-api.mjs +6 -1
- package/src/tui/engine/tool-card-results.mjs +14 -5
- package/src/tui/engine/turn.mjs +9 -2
- package/src/tui/engine.mjs +31 -12
- package/src/ui/statusline-agents.mjs +36 -0
- package/src/ui/statusline.mjs +15 -5
- package/src/runtime/channels/lib/seat-lock.mjs +0 -196
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
// Regression tests for turn-time deferred-manifest construction (claude-code
|
|
2
|
+
// style). An MCP server that finishes its handshake BETWEEN session-create and
|
|
3
|
+
// the user's FIRST send is folded — on the first turn, synchronously, with no
|
|
4
|
+
// boot await — into the INITIAL <available-deferred-tools> manifest (BP1) and
|
|
5
|
+
// pre-marked announced, so the first-turn tool-catalog reconcile emits NO late
|
|
6
|
+
// <system-reminder> for it. A server that connects AFTER the first turn keeps
|
|
7
|
+
// the append-only late-reminder path. Unit-style: exercise the tool-catalog
|
|
8
|
+
// exports directly (no runtime, no spawn) the way session-turn-api/runtime-core
|
|
9
|
+
// wire them.
|
|
10
|
+
import test from 'node:test';
|
|
11
|
+
import assert from 'node:assert/strict';
|
|
12
|
+
import {
|
|
13
|
+
applyDeferredToolSurface,
|
|
14
|
+
refreshInitialDeferredMcpSurface,
|
|
15
|
+
reconcileDeferredMcpToolCatalog,
|
|
16
|
+
} from '../src/session-runtime/tool-catalog.mjs';
|
|
17
|
+
|
|
18
|
+
function baseSession() {
|
|
19
|
+
return {
|
|
20
|
+
provider: 'legacy',
|
|
21
|
+
tools: [{ name: 'read', description: 'read a file' }, { name: 'grep', description: 'search' }],
|
|
22
|
+
messages: [{ role: 'system', content: 'BASE PROMPT' }],
|
|
23
|
+
};
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// A freshly-created session: the create-time surface is baked WITHOUT any MCP
|
|
27
|
+
// (server still mid-handshake at create). `shell` is a deferred (non-active)
|
|
28
|
+
// standalone tool so BP1 carries a manifest block even before MCP arrives.
|
|
29
|
+
function createdSession() {
|
|
30
|
+
const session = baseSession();
|
|
31
|
+
applyDeferredToolSurface(session, 'full', [{ name: 'shell', description: 'run a command' }], { provider: 'legacy' });
|
|
32
|
+
return session;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function systemContent(session) {
|
|
36
|
+
return session.messages.find((m) => m.role === 'system').content;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const mcpTool = { name: 'mcp__unity__get_scene', description: 'Return the active Unity scene graph.' };
|
|
40
|
+
const mcpTool2 = { name: 'mcp__unity__run_tests', description: 'Run the Unity test runner.' };
|
|
41
|
+
|
|
42
|
+
// Mirror the session-turn-api first-turn gate: refresh runs ONCE, only for a
|
|
43
|
+
// session flagged fresh (deferredInitialRefreshPending) at create; a resumed
|
|
44
|
+
// session (reloaded transcript, flag absent) takes the late-reconcile path and
|
|
45
|
+
// its already-baked BP1 is never rebuilt or re-announced.
|
|
46
|
+
function firstTurnGate(session, liveMcp) {
|
|
47
|
+
if (session.deferredInitialRefreshPending) {
|
|
48
|
+
session.deferredInitialRefreshPending = false;
|
|
49
|
+
return refreshInitialDeferredMcpSurface(session, liveMcp) ? 'refreshed' : 'refresh-noop';
|
|
50
|
+
}
|
|
51
|
+
return 'late';
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
test('MCP connecting between session-create and first send lands in the INITIAL manifest, not a late reminder', () => {
|
|
55
|
+
const session = createdSession();
|
|
56
|
+
assert.ok(!systemContent(session).includes('mcp__unity__get_scene'), 'MCP absent from create-time BP1');
|
|
57
|
+
|
|
58
|
+
// First-turn refresh: the live registry now includes the connected server.
|
|
59
|
+
const changed = refreshInitialDeferredMcpSurface(session, [mcpTool]);
|
|
60
|
+
assert.equal(changed, true, 'first-turn refresh folded the newly-connected MCP tool');
|
|
61
|
+
|
|
62
|
+
const sys = systemContent(session);
|
|
63
|
+
assert.ok(sys.includes('<available-deferred-tools>'), 'manifest block present');
|
|
64
|
+
assert.ok(sys.includes('mcp__unity__get_scene'), 'MCP tool in the INITIAL manifest');
|
|
65
|
+
assert.ok(session.deferredAnnouncedTools.includes('mcp__unity__get_scene'), 'MCP tool pre-marked announced');
|
|
66
|
+
|
|
67
|
+
let enqueued = null;
|
|
68
|
+
const result = reconcileDeferredMcpToolCatalog(session, [mcpTool], {
|
|
69
|
+
enqueue: (text) => { enqueued = text; return true; },
|
|
70
|
+
});
|
|
71
|
+
assert.equal(result, null, 'no late-tool announcement for a first-turn-folded tool');
|
|
72
|
+
assert.equal(enqueued, null, 'nothing enqueued for a first-turn-folded tool');
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test('an MCP server connecting AFTER the first turn is still announced via the late reminder', () => {
|
|
76
|
+
const session = createdSession();
|
|
77
|
+
// First turn: no MCP connected yet — nothing to fold.
|
|
78
|
+
assert.equal(refreshInitialDeferredMcpSurface(session, []), false, 'no live MCP => first-turn no-op');
|
|
79
|
+
assert.ok(!(session.deferredAnnouncedTools || []).includes('mcp__unity__get_scene'), 'MCP not pre-announced');
|
|
80
|
+
|
|
81
|
+
// A later turn: the server has since connected → late path fires.
|
|
82
|
+
let enqueued = null;
|
|
83
|
+
const result = reconcileDeferredMcpToolCatalog(session, [mcpTool], {
|
|
84
|
+
enqueue: (text) => { enqueued = text; return true; },
|
|
85
|
+
});
|
|
86
|
+
assert.deepEqual(result, ['mcp__unity__get_scene'], 'late tool announced');
|
|
87
|
+
assert.ok(enqueued && enqueued.includes('mcp__unity__get_scene'), 'late reminder enqueued');
|
|
88
|
+
assert.ok(enqueued.includes('connected after this session started'), 'reminder carries the late-tool sentinel');
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
test('recreated session (MCP already connected at create) seeds its manifest, no late re-announce', () => {
|
|
92
|
+
// Recreate/reset path: createCurrentSession folds the live MCP tools into the
|
|
93
|
+
// surface at create time, so a cwd-change recreate seeds its BP1 directly and
|
|
94
|
+
// never needs the first-turn refresh.
|
|
95
|
+
const session = baseSession();
|
|
96
|
+
applyDeferredToolSurface(session, 'full', [{ name: 'shell', description: 'run a command' }, mcpTool], { provider: 'legacy' });
|
|
97
|
+
assert.ok(systemContent(session).includes('mcp__unity__get_scene'), 'MCP in recreated BP1');
|
|
98
|
+
assert.ok(session.deferredAnnouncedTools.includes('mcp__unity__get_scene'), 'recreated MCP pre-marked announced');
|
|
99
|
+
|
|
100
|
+
let enqueued = null;
|
|
101
|
+
const result = reconcileDeferredMcpToolCatalog(session, [mcpTool], {
|
|
102
|
+
enqueue: (text) => { enqueued = text; return true; },
|
|
103
|
+
});
|
|
104
|
+
assert.equal(result, null, 'no late announcement on recreate');
|
|
105
|
+
assert.equal(enqueued, null, 'nothing enqueued on recreate');
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
test('first-turn refresh is idempotent: a re-render with no new MCP is a no-op and keeps ONE manifest block', () => {
|
|
109
|
+
const session = createdSession();
|
|
110
|
+
assert.equal(refreshInitialDeferredMcpSurface(session, [mcpTool]), true, 'first fold applies');
|
|
111
|
+
const once = systemContent(session);
|
|
112
|
+
assert.equal(refreshInitialDeferredMcpSurface(session, [mcpTool]), false, 'no genuinely-new MCP => no-op');
|
|
113
|
+
const twice = systemContent(session);
|
|
114
|
+
assert.equal(once, twice, 'BP1 byte-identical on re-render');
|
|
115
|
+
assert.equal((twice.match(/<available-deferred-tools>/g) || []).length, 1, 'exactly one manifest block (no duplicate)');
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
test('a second newly-connected MCP tool re-renders BP1 in place (both listed, still one block)', () => {
|
|
119
|
+
const session = createdSession();
|
|
120
|
+
refreshInitialDeferredMcpSurface(session, [mcpTool]);
|
|
121
|
+
assert.equal(refreshInitialDeferredMcpSurface(session, [mcpTool, mcpTool2]), true, 're-fold applies the new tool');
|
|
122
|
+
const sys = systemContent(session);
|
|
123
|
+
assert.ok(sys.includes('mcp__unity__get_scene') && sys.includes('mcp__unity__run_tests'), 'both MCP tools listed');
|
|
124
|
+
assert.equal((sys.match(/<available-deferred-tools>/g) || []).length, 1, 'still exactly one block (rebuilt in place)');
|
|
125
|
+
assert.ok(session.deferredAnnouncedTools.includes('mcp__unity__run_tests'), 'the new tool is pre-announced too');
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
test('a resumed session (no fresh flag, prior baked BP1) is NOT refreshed on its next turn', () => {
|
|
129
|
+
// A prior run baked BP1 with the MCP tool; resume reloads that transcript.
|
|
130
|
+
const session = baseSession();
|
|
131
|
+
applyDeferredToolSurface(session, 'full', [{ name: 'shell', description: 'run a command' }, mcpTool], { provider: 'legacy' });
|
|
132
|
+
const before = systemContent(session);
|
|
133
|
+
const announcedBefore = [...session.deferredAnnouncedTools];
|
|
134
|
+
// Resume path never sets the per-session fresh flag.
|
|
135
|
+
assert.equal(session.deferredInitialRefreshPending, undefined, 'resumed session carries no fresh flag');
|
|
136
|
+
const route = firstTurnGate(session, [mcpTool, mcpTool2]);
|
|
137
|
+
assert.equal(route, 'late', 'resumed session takes the late-reconcile path, not the initial refresh');
|
|
138
|
+
assert.equal(systemContent(session), before, 'BP1 untouched on resume (no rebuild)');
|
|
139
|
+
assert.deepEqual([...session.deferredAnnouncedTools], announcedBefore, 'announced set unchanged on resume');
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test('a fresh session (flagged) is refreshed exactly once, then falls to the late path', () => {
|
|
143
|
+
const session = createdSession();
|
|
144
|
+
session.deferredInitialRefreshPending = true;
|
|
145
|
+
assert.equal(firstTurnGate(session, [mcpTool]), 'refreshed', 'fresh session refreshes on its first turn');
|
|
146
|
+
assert.equal(session.deferredInitialRefreshPending, false, 'fresh flag consumed (one-shot)');
|
|
147
|
+
assert.ok(session.deferredAnnouncedTools.includes('mcp__unity__get_scene'), 'first-turn MCP pre-announced');
|
|
148
|
+
assert.equal(firstTurnGate(session, [mcpTool2]), 'late', 'second turn no longer refreshes');
|
|
149
|
+
});
|
|
@@ -15,4 +15,4 @@
|
|
|
15
15
|
{ "id": "uc5-yesterday", "label": "UC5 calendar yesterday", "args": { "period": "yesterday", "limit": 10 }, "expect": "browse" },
|
|
16
16
|
{ "id": "uc5-thisweek", "label": "UC5 calendar this_week", "args": { "period": "this_week", "limit": 10 }, "expect": "browse" },
|
|
17
17
|
{ "id": "uc6-category", "label": "UC6 category decision 7d", "args": { "period": "7d", "category": "decision", "limit": 10 }, "expect": "browse" }
|
|
18
|
-
]
|
|
18
|
+
]
|
|
@@ -1,106 +1,106 @@
|
|
|
1
|
-
# smoke-runtime-negative.ps1 — Negative-path tests against released win32-x64
|
|
2
|
-
# runtime tarball. Validates: download, sha256, corrupt-tarball, fresh-extract
|
|
3
|
-
# boot, double-init refusal, hostile-env survival.
|
|
4
|
-
#
|
|
5
|
-
# Env: TAG / OS / ARCH / RUNTIME_RELEASE_REPOSITORY (default tribgames/mixdog)
|
|
6
|
-
|
|
7
|
-
$ErrorActionPreference = 'Stop'
|
|
8
|
-
|
|
9
|
-
$Tag = if ($env:TAG) { $env:TAG } else { 'runtime-v0.4.0' }
|
|
10
|
-
$Os = if ($env:OS) { $env:OS } else { 'win32' }
|
|
11
|
-
$Arch = if ($env:ARCH) { $env:ARCH } else { 'x64' }
|
|
12
|
-
$ReleaseRepo = if ($env:RUNTIME_RELEASE_REPOSITORY) { $env:RUNTIME_RELEASE_REPOSITORY } else { 'tribgames/mixdog' }
|
|
13
|
-
$PgVer = '16.4'
|
|
14
|
-
$PgvectorVer = '0.8.2'
|
|
15
|
-
|
|
16
|
-
$Asset = "mixdog-runtime-${Os}-${Arch}-pg${PgVer}-pgvector${PgvectorVer}.tar.gz"
|
|
17
|
-
$Url = "https://github.com/${ReleaseRepo}/releases/download/${Tag}/${Asset}"
|
|
18
|
-
|
|
19
|
-
$Work = New-Item -ItemType Directory -Force -Path "$env:TEMP\smoke-$([guid]::NewGuid().ToString('N'))" | Select-Object -ExpandProperty FullName
|
|
20
|
-
|
|
21
|
-
try {
|
|
22
|
-
Write-Host "==> Negative-path smoke for ${Os}-${Arch} from $Url"
|
|
23
|
-
|
|
24
|
-
Write-Host "==> Test 1: HEAD reaches release asset"
|
|
25
|
-
$resp = Invoke-WebRequest -Uri $Url -Method Head -UseBasicParsing -ErrorAction Stop
|
|
26
|
-
Write-Host " HTTP $($resp.StatusCode)"
|
|
27
|
-
|
|
28
|
-
Write-Host "==> Test 2: full download + sha256"
|
|
29
|
-
$TarPath = Join-Path $Work $Asset
|
|
30
|
-
Invoke-WebRequest -Uri $Url -OutFile $TarPath -UseBasicParsing
|
|
31
|
-
$sha = (Get-FileHash -Algorithm SHA256 $TarPath).Hash.ToLower()
|
|
32
|
-
Write-Host " sha256=$sha"
|
|
33
|
-
|
|
34
|
-
Write-Host "==> Test 3: corrupt tarball — flip a byte and ensure tar errors"
|
|
35
|
-
$CorruptPath = "$Work\corrupt.tar.gz"
|
|
36
|
-
Copy-Item $TarPath $CorruptPath
|
|
37
|
-
$bytes = [byte[]]::new(1); $rand = [System.Random]::new(); $rand.NextBytes($bytes)
|
|
38
|
-
$stream = [System.IO.File]::OpenWrite($CorruptPath)
|
|
39
|
-
$stream.Position = 102400
|
|
40
|
-
$stream.Write($bytes, 0, 1)
|
|
41
|
-
$stream.Close()
|
|
42
|
-
$CorruptDir = "$Work\corrupt"
|
|
43
|
-
New-Item -ItemType Directory -Force -Path $CorruptDir | Out-Null
|
|
44
|
-
$TarProc = Start-Process -FilePath tar -ArgumentList @('-xzf', $CorruptPath, '-C', $CorruptDir.Replace('\','/')) -Wait:$false -PassThru -WindowStyle Hidden
|
|
45
|
-
if (-not $TarProc.WaitForExit(15000)) {
|
|
46
|
-
Stop-Process -Id $TarProc.Id -Force -ErrorAction SilentlyContinue
|
|
47
|
-
Write-Host " PASS: corrupted tarball extraction timed out and was stopped"
|
|
48
|
-
} elseif ($TarProc.ExitCode -eq 0) {
|
|
49
|
-
Write-Host " WARN: corrupted tarball extracted without error (tar gzip recovery)"
|
|
50
|
-
} else {
|
|
51
|
-
Write-Host " PASS: corrupted tarball rejected by tar (exit $($TarProc.ExitCode))"
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
Write-Host "==> Test 4: fresh-extract boot (extension load + distance query)"
|
|
55
|
-
$FreshDir = "$Work\fresh"
|
|
56
|
-
New-Item -ItemType Directory -Force -Path $FreshDir | Out-Null
|
|
57
|
-
& tar -xzf $TarPath -C ($FreshDir.Replace('\','/'))
|
|
58
|
-
if ($LASTEXITCODE -ne 0) { throw "fresh extract failed" }
|
|
59
|
-
|
|
60
|
-
$PgBin = "$FreshDir\bin"
|
|
61
|
-
$Data = "$FreshDir\pgdata"
|
|
62
|
-
$Log = "$FreshDir\pg.log"
|
|
63
|
-
$Port = 55897
|
|
64
|
-
|
|
65
|
-
# Hostile env: minimal PATH (System32 only), no PGROOT/PGDATA
|
|
66
|
-
$SavedPath = $env:PATH
|
|
67
|
-
$SavedPgRoot = $env:PGROOT
|
|
68
|
-
$env:PATH = "$env:SystemRoot\System32;$env:SystemRoot"
|
|
69
|
-
$env:PGROOT = $null
|
|
70
|
-
$env:PGDATA = $null
|
|
71
|
-
|
|
72
|
-
try {
|
|
73
|
-
& "$PgBin\postgres.exe" --version
|
|
74
|
-
if ($LASTEXITCODE -ne 0) { throw "FAIL: postgres --version" }
|
|
75
|
-
& "$PgBin\initdb.exe" -D $Data --username=postgres --auth-local=trust --no-locale -E UTF8 | Out-Null
|
|
76
|
-
if ($LASTEXITCODE -ne 0) { throw "FAIL: initdb" }
|
|
77
|
-
& "$PgBin\pg_ctl.exe" -D $Data -o "-p $Port -h 127.0.0.1" -l $Log -w start
|
|
78
|
-
if ($LASTEXITCODE -ne 0) { Get-Content $Log | Select-Object -Last 30; throw "FAIL: pg_ctl start" }
|
|
79
|
-
|
|
80
|
-
try {
|
|
81
|
-
& "$PgBin\psql.exe" -h 127.0.0.1 -p $Port -U postgres -d postgres -c "CREATE EXTENSION vector;" | Out-Null
|
|
82
|
-
if ($LASTEXITCODE -ne 0) { throw "FAIL: CREATE EXTENSION" }
|
|
83
|
-
$ExtV = & "$PgBin\psql.exe" -h 127.0.0.1 -p $Port -U postgres -d postgres -tAc "SELECT extversion FROM pg_extension WHERE extname='vector';"
|
|
84
|
-
if ($ExtV.Trim() -ne $PgvectorVer) { throw "FAIL: extversion='$ExtV' expected='$PgvectorVer'" }
|
|
85
|
-
Write-Host " PASS: fresh-extract boot + vector extension"
|
|
86
|
-
} finally {
|
|
87
|
-
& "$PgBin\pg_ctl.exe" -D $Data -m fast stop 2>$null | Out-Null
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
Write-Host "==> Test 5: second initdb on initialized dir refused"
|
|
91
|
-
$rc = (Start-Process -FilePath "$PgBin\initdb.exe" -ArgumentList "-D",$Data,"-U","postgres" -Wait -PassThru -NoNewWindow).ExitCode
|
|
92
|
-
if ($rc -eq 0) {
|
|
93
|
-
Write-Host " WARN: second initdb succeeded (unexpected)"
|
|
94
|
-
} else {
|
|
95
|
-
Write-Host " PASS: second initdb refused (exit $rc)"
|
|
96
|
-
}
|
|
97
|
-
} finally {
|
|
98
|
-
$env:PATH = $SavedPath
|
|
99
|
-
$env:PGROOT = $SavedPgRoot
|
|
100
|
-
}
|
|
101
|
-
|
|
102
|
-
Write-Host "==> All negative-path smokes: PASS"
|
|
103
|
-
}
|
|
104
|
-
finally {
|
|
105
|
-
Remove-Item -Recurse -Force $Work -ErrorAction SilentlyContinue
|
|
106
|
-
}
|
|
1
|
+
# smoke-runtime-negative.ps1 — Negative-path tests against released win32-x64
|
|
2
|
+
# runtime tarball. Validates: download, sha256, corrupt-tarball, fresh-extract
|
|
3
|
+
# boot, double-init refusal, hostile-env survival.
|
|
4
|
+
#
|
|
5
|
+
# Env: TAG / OS / ARCH / RUNTIME_RELEASE_REPOSITORY (default tribgames/mixdog)
|
|
6
|
+
|
|
7
|
+
$ErrorActionPreference = 'Stop'
|
|
8
|
+
|
|
9
|
+
$Tag = if ($env:TAG) { $env:TAG } else { 'runtime-v0.4.0' }
|
|
10
|
+
$Os = if ($env:OS) { $env:OS } else { 'win32' }
|
|
11
|
+
$Arch = if ($env:ARCH) { $env:ARCH } else { 'x64' }
|
|
12
|
+
$ReleaseRepo = if ($env:RUNTIME_RELEASE_REPOSITORY) { $env:RUNTIME_RELEASE_REPOSITORY } else { 'tribgames/mixdog' }
|
|
13
|
+
$PgVer = '16.4'
|
|
14
|
+
$PgvectorVer = '0.8.2'
|
|
15
|
+
|
|
16
|
+
$Asset = "mixdog-runtime-${Os}-${Arch}-pg${PgVer}-pgvector${PgvectorVer}.tar.gz"
|
|
17
|
+
$Url = "https://github.com/${ReleaseRepo}/releases/download/${Tag}/${Asset}"
|
|
18
|
+
|
|
19
|
+
$Work = New-Item -ItemType Directory -Force -Path "$env:TEMP\smoke-$([guid]::NewGuid().ToString('N'))" | Select-Object -ExpandProperty FullName
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
Write-Host "==> Negative-path smoke for ${Os}-${Arch} from $Url"
|
|
23
|
+
|
|
24
|
+
Write-Host "==> Test 1: HEAD reaches release asset"
|
|
25
|
+
$resp = Invoke-WebRequest -Uri $Url -Method Head -UseBasicParsing -ErrorAction Stop
|
|
26
|
+
Write-Host " HTTP $($resp.StatusCode)"
|
|
27
|
+
|
|
28
|
+
Write-Host "==> Test 2: full download + sha256"
|
|
29
|
+
$TarPath = Join-Path $Work $Asset
|
|
30
|
+
Invoke-WebRequest -Uri $Url -OutFile $TarPath -UseBasicParsing
|
|
31
|
+
$sha = (Get-FileHash -Algorithm SHA256 $TarPath).Hash.ToLower()
|
|
32
|
+
Write-Host " sha256=$sha"
|
|
33
|
+
|
|
34
|
+
Write-Host "==> Test 3: corrupt tarball — flip a byte and ensure tar errors"
|
|
35
|
+
$CorruptPath = "$Work\corrupt.tar.gz"
|
|
36
|
+
Copy-Item $TarPath $CorruptPath
|
|
37
|
+
$bytes = [byte[]]::new(1); $rand = [System.Random]::new(); $rand.NextBytes($bytes)
|
|
38
|
+
$stream = [System.IO.File]::OpenWrite($CorruptPath)
|
|
39
|
+
$stream.Position = 102400
|
|
40
|
+
$stream.Write($bytes, 0, 1)
|
|
41
|
+
$stream.Close()
|
|
42
|
+
$CorruptDir = "$Work\corrupt"
|
|
43
|
+
New-Item -ItemType Directory -Force -Path $CorruptDir | Out-Null
|
|
44
|
+
$TarProc = Start-Process -FilePath tar -ArgumentList @('-xzf', $CorruptPath, '-C', $CorruptDir.Replace('\','/')) -Wait:$false -PassThru -WindowStyle Hidden
|
|
45
|
+
if (-not $TarProc.WaitForExit(15000)) {
|
|
46
|
+
Stop-Process -Id $TarProc.Id -Force -ErrorAction SilentlyContinue
|
|
47
|
+
Write-Host " PASS: corrupted tarball extraction timed out and was stopped"
|
|
48
|
+
} elseif ($TarProc.ExitCode -eq 0) {
|
|
49
|
+
Write-Host " WARN: corrupted tarball extracted without error (tar gzip recovery)"
|
|
50
|
+
} else {
|
|
51
|
+
Write-Host " PASS: corrupted tarball rejected by tar (exit $($TarProc.ExitCode))"
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
Write-Host "==> Test 4: fresh-extract boot (extension load + distance query)"
|
|
55
|
+
$FreshDir = "$Work\fresh"
|
|
56
|
+
New-Item -ItemType Directory -Force -Path $FreshDir | Out-Null
|
|
57
|
+
& tar -xzf $TarPath -C ($FreshDir.Replace('\','/'))
|
|
58
|
+
if ($LASTEXITCODE -ne 0) { throw "fresh extract failed" }
|
|
59
|
+
|
|
60
|
+
$PgBin = "$FreshDir\bin"
|
|
61
|
+
$Data = "$FreshDir\pgdata"
|
|
62
|
+
$Log = "$FreshDir\pg.log"
|
|
63
|
+
$Port = 55897
|
|
64
|
+
|
|
65
|
+
# Hostile env: minimal PATH (System32 only), no PGROOT/PGDATA
|
|
66
|
+
$SavedPath = $env:PATH
|
|
67
|
+
$SavedPgRoot = $env:PGROOT
|
|
68
|
+
$env:PATH = "$env:SystemRoot\System32;$env:SystemRoot"
|
|
69
|
+
$env:PGROOT = $null
|
|
70
|
+
$env:PGDATA = $null
|
|
71
|
+
|
|
72
|
+
try {
|
|
73
|
+
& "$PgBin\postgres.exe" --version
|
|
74
|
+
if ($LASTEXITCODE -ne 0) { throw "FAIL: postgres --version" }
|
|
75
|
+
& "$PgBin\initdb.exe" -D $Data --username=postgres --auth-local=trust --no-locale -E UTF8 | Out-Null
|
|
76
|
+
if ($LASTEXITCODE -ne 0) { throw "FAIL: initdb" }
|
|
77
|
+
& "$PgBin\pg_ctl.exe" -D $Data -o "-p $Port -h 127.0.0.1" -l $Log -w start
|
|
78
|
+
if ($LASTEXITCODE -ne 0) { Get-Content $Log | Select-Object -Last 30; throw "FAIL: pg_ctl start" }
|
|
79
|
+
|
|
80
|
+
try {
|
|
81
|
+
& "$PgBin\psql.exe" -h 127.0.0.1 -p $Port -U postgres -d postgres -c "CREATE EXTENSION vector;" | Out-Null
|
|
82
|
+
if ($LASTEXITCODE -ne 0) { throw "FAIL: CREATE EXTENSION" }
|
|
83
|
+
$ExtV = & "$PgBin\psql.exe" -h 127.0.0.1 -p $Port -U postgres -d postgres -tAc "SELECT extversion FROM pg_extension WHERE extname='vector';"
|
|
84
|
+
if ($ExtV.Trim() -ne $PgvectorVer) { throw "FAIL: extversion='$ExtV' expected='$PgvectorVer'" }
|
|
85
|
+
Write-Host " PASS: fresh-extract boot + vector extension"
|
|
86
|
+
} finally {
|
|
87
|
+
& "$PgBin\pg_ctl.exe" -D $Data -m fast stop 2>$null | Out-Null
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
Write-Host "==> Test 5: second initdb on initialized dir refused"
|
|
91
|
+
$rc = (Start-Process -FilePath "$PgBin\initdb.exe" -ArgumentList "-D",$Data,"-U","postgres" -Wait -PassThru -NoNewWindow).ExitCode
|
|
92
|
+
if ($rc -eq 0) {
|
|
93
|
+
Write-Host " WARN: second initdb succeeded (unexpected)"
|
|
94
|
+
} else {
|
|
95
|
+
Write-Host " PASS: second initdb refused (exit $rc)"
|
|
96
|
+
}
|
|
97
|
+
} finally {
|
|
98
|
+
$env:PATH = $SavedPath
|
|
99
|
+
$env:PGROOT = $SavedPgRoot
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
Write-Host "==> All negative-path smokes: PASS"
|
|
103
|
+
}
|
|
104
|
+
finally {
|
|
105
|
+
Remove-Item -Recurse -Force $Work -ErrorAction SilentlyContinue
|
|
106
|
+
}
|
|
@@ -88,4 +88,4 @@ for (const target of ['heavy-worker', 'worker']) {
|
|
|
88
88
|
console.log(` single-call batches: ${batch1}/${batchTot} (${pct(batch1, batchTot)}%)`);
|
|
89
89
|
console.log(` content greps with -C/-A/-B: ${grepContentWithCtx}/${grepContent} (${pct(grepContentWithCtx, grepContent)}%) grep->read follow-ups: ${grepThenRead}`);
|
|
90
90
|
console.log(` same-path re-reads >=3x: ${rereads}`);
|
|
91
|
-
}
|
|
91
|
+
}
|
package/scripts/tool-smoke.mjs
CHANGED
|
@@ -1050,32 +1050,28 @@ const prevChannelSingleton = process.env.MIXDOG_CHANNEL_SINGLETON;
|
|
|
1050
1050
|
const prevChannelWorkerProcess = process.env.MIXDOG_CHANNEL_WORKER_PROCESS;
|
|
1051
1051
|
const prevRuntimeRoot = process.env.MIXDOG_RUNTIME_ROOT;
|
|
1052
1052
|
const prevEnvOut = process.env.SMOKE_CHANNEL_ENV_OUT;
|
|
1053
|
+
const prevDaemonEntry = process.env.MIXDOG_CHANNEL_DAEMON_ENTRY;
|
|
1053
1054
|
try {
|
|
1054
|
-
|
|
1055
|
+
// Daemon-mode worker env coverage: start() spawn-or-attaches the machine
|
|
1056
|
+
// -global daemon (the stub daemon entry — no Discord token) instead of
|
|
1057
|
+
// forking `entry`, so assert the flags on the SPAWNED DAEMON's env (the stub
|
|
1058
|
+
// dumps them to SMOKE_CHANNEL_ENV_OUT). The old fork-path env assertion died
|
|
1059
|
+
// with the fork path itself; full flip/attach coverage lives in
|
|
1060
|
+
// scripts/channel-daemon-smoke.mjs.
|
|
1061
|
+
const stubEntry = join(root, 'scripts', 'channel-daemon-stub.mjs');
|
|
1055
1062
|
const dataDir = join(channelWorkerTmp, 'data');
|
|
1056
1063
|
const runtimeDir = join(channelWorkerTmp, 'runtime');
|
|
1057
1064
|
const envOut = join(channelWorkerTmp, 'env.json');
|
|
1058
1065
|
mkdirSync(dataDir, { recursive: true });
|
|
1059
1066
|
mkdirSync(runtimeDir, { recursive: true });
|
|
1060
|
-
writeFileSync(entry, `
|
|
1061
|
-
import { writeFileSync } from 'node:fs';
|
|
1062
|
-
writeFileSync(process.env.SMOKE_CHANNEL_ENV_OUT, JSON.stringify({
|
|
1063
|
-
cliOwned: process.env.MIXDOG_CLI_OWNED,
|
|
1064
|
-
daemon: process.env.MIXDOG_CHANNEL_DAEMON,
|
|
1065
|
-
}));
|
|
1066
|
-
process.send?.({ type: 'ready' });
|
|
1067
|
-
process.on('message', (msg) => {
|
|
1068
|
-
if (msg?.type === 'shutdown') process.exit(0);
|
|
1069
|
-
});
|
|
1070
|
-
setInterval(() => {}, 10000);
|
|
1071
|
-
`);
|
|
1072
1067
|
process.env.MIXDOG_CHANNEL_DAEMON = '1';
|
|
1073
1068
|
process.env.MIXDOG_CHANNEL_SINGLETON = '1';
|
|
1074
1069
|
process.env.MIXDOG_CHANNEL_WORKER_PROCESS = '1';
|
|
1075
1070
|
process.env.MIXDOG_RUNTIME_ROOT = runtimeDir;
|
|
1076
1071
|
process.env.SMOKE_CHANNEL_ENV_OUT = envOut;
|
|
1072
|
+
process.env.MIXDOG_CHANNEL_DAEMON_ENTRY = stubEntry;
|
|
1077
1073
|
channelEnvWorker = createStandaloneChannelWorker({
|
|
1078
|
-
entry,
|
|
1074
|
+
entry: stubEntry,
|
|
1079
1075
|
rootDir: root,
|
|
1080
1076
|
dataDir,
|
|
1081
1077
|
cwd: root,
|
|
@@ -1100,6 +1096,11 @@ setInterval(() => {}, 10000);
|
|
|
1100
1096
|
else process.env.MIXDOG_RUNTIME_ROOT = prevRuntimeRoot;
|
|
1101
1097
|
if (prevEnvOut == null) delete process.env.SMOKE_CHANNEL_ENV_OUT;
|
|
1102
1098
|
else process.env.SMOKE_CHANNEL_ENV_OUT = prevEnvOut;
|
|
1099
|
+
if (prevDaemonEntry == null) delete process.env.MIXDOG_CHANNEL_DAEMON_ENTRY;
|
|
1100
|
+
else process.env.MIXDOG_CHANNEL_DAEMON_ENTRY = prevDaemonEntry;
|
|
1101
|
+
// Detach only ends OUR attachment; the stub daemon self-shuts after its
|
|
1102
|
+
// client-grace window. Give it that window before deleting its tmp root.
|
|
1103
|
+
await new Promise((resolveWait) => setTimeout(resolveWait, 700));
|
|
1103
1104
|
rmSync(channelWorkerTmp, { recursive: true, force: true });
|
|
1104
1105
|
}
|
|
1105
1106
|
|
|
@@ -1191,10 +1192,10 @@ if (EXPLORE_TOOL.annotations?.agentHidden === true) {
|
|
|
1191
1192
|
}
|
|
1192
1193
|
}
|
|
1193
1194
|
const exploreProps = EXPLORE_TOOL.inputSchema?.properties || {};
|
|
1194
|
-
if (!/
|
|
1195
|
-
throw new Error('explore description must
|
|
1195
|
+
if (!/broad\/uncertain/i.test(EXPLORE_TOOL.description || '') || !/machine-wide/i.test(EXPLORE_TOOL.description || '') || !/independent targets/i.test(EXPLORE_TOOL.description || '') || (EXPLORE_TOOL.description || '').length > 600) {
|
|
1196
|
+
throw new Error('explore description must keep the locator + facet fan-out contract');
|
|
1196
1197
|
}
|
|
1197
|
-
if (!/Narrow locator query/i.test(exploreProps.query?.description || '') || !/independent
|
|
1198
|
+
if (!/Narrow locator query/i.test(exploreProps.query?.description || '') || !/independent facets/i.test(exploreProps.query?.description || '') || !/Project\/root/i.test(exploreProps.cwd?.description || '')) {
|
|
1198
1199
|
throw new Error('explore schema must stay compact and preserve query/cwd shape');
|
|
1199
1200
|
}
|
|
1200
1201
|
const normalizedExplore = normalizeExploreQueries('["where is model selection?"," ","which file owns agent async?"]');
|
|
@@ -1412,7 +1413,11 @@ setInternalToolsProvider({
|
|
|
1412
1413
|
const writeTools = (writeAgentSession.tools || []).map((tool) => tool?.name).filter(Boolean);
|
|
1413
1414
|
const fullTools = (fullAgentSession.tools || []).map((tool) => tool?.name).filter(Boolean);
|
|
1414
1415
|
const publicExploreTools = (publicExploreSession.tools || []).map((tool) => tool?.name).filter(Boolean);
|
|
1415
|
-
|
|
1416
|
+
// Read-role AGENT sessions carry shell/task so review/debug agents can run
|
|
1417
|
+
// their own verification (build/test); the plain readonly preset (public
|
|
1418
|
+
// explore role) still omits them.
|
|
1419
|
+
const expectedReadTools = ['code_graph', 'find', 'glob', 'list', 'grep', 'read', 'shell', 'task', 'explore', 'search', 'web_fetch', 'Skill'];
|
|
1420
|
+
const expectedPublicReadTools = ['code_graph', 'find', 'glob', 'list', 'grep', 'read', 'explore', 'search', 'web_fetch', 'Skill'];
|
|
1416
1421
|
const expectedWriteTools = ['code_graph', 'find', 'glob', 'list', 'grep', 'read', 'apply_patch', 'shell', 'task', 'explore', 'search', 'web_fetch', 'Skill'];
|
|
1417
1422
|
if (JSON.stringify(readTools) !== JSON.stringify(expectedReadTools)) {
|
|
1418
1423
|
throw new Error(`read agent schema must be fixed allow-list: expected=${expectedReadTools.join(', ')} actual=${readTools.join(', ')}`);
|
|
@@ -1423,12 +1428,15 @@ setInternalToolsProvider({
|
|
|
1423
1428
|
if (readTools.includes('load_tool') || writeTools.includes('load_tool')) {
|
|
1424
1429
|
throw new Error(`agent session fixed schemas must omit load_tool: read=${readTools.join(', ')} write=${writeTools.join(', ')}`);
|
|
1425
1430
|
}
|
|
1426
|
-
if (readTools.includes('
|
|
1427
|
-
throw new Error(`read agent schema must omit
|
|
1431
|
+
if (readTools.includes('apply_patch')) {
|
|
1432
|
+
throw new Error(`read agent schema must omit apply_patch: read=${readTools.join(', ')}`);
|
|
1428
1433
|
}
|
|
1429
|
-
for (const name of ['shell', '
|
|
1430
|
-
if (readTools.includes(name)) {
|
|
1431
|
-
throw new Error(`read agent schema must
|
|
1434
|
+
for (const name of ['shell', 'task']) {
|
|
1435
|
+
if (!readTools.includes(name)) {
|
|
1436
|
+
throw new Error(`read agent schema must carry verification tool ${name}: read=${readTools.join(', ')}`);
|
|
1437
|
+
}
|
|
1438
|
+
if (publicExploreTools.includes(name)) {
|
|
1439
|
+
throw new Error(`public explore role must omit ${name}: explore=${publicExploreTools.join(', ')}`);
|
|
1432
1440
|
}
|
|
1433
1441
|
}
|
|
1434
1442
|
for (const name of ['apply_patch', 'shell', 'task']) {
|
|
@@ -1450,13 +1458,13 @@ setInternalToolsProvider({
|
|
|
1450
1458
|
if (!fullTools.includes('explore')) {
|
|
1451
1459
|
throw new Error(`full agent schema must expose explore: full=${fullTools.join(', ')}`);
|
|
1452
1460
|
}
|
|
1453
|
-
//
|
|
1454
|
-
//
|
|
1455
|
-
//
|
|
1456
|
-
//
|
|
1457
|
-
//
|
|
1458
|
-
if (JSON.stringify(publicExploreTools) !== JSON.stringify(
|
|
1459
|
-
throw new Error(`public explore role must ship the
|
|
1461
|
+
// The explore wrapper stays IN the schema for every read role — including
|
|
1462
|
+
// the explore agent itself. Recursion is broken at call time in
|
|
1463
|
+
// pre-dispatch-deny.mjs via recursiveWrapperToolNameForPublicAgent, not by
|
|
1464
|
+
// schema stripping. (Read AGENT sessions add shell/task on top, so the
|
|
1465
|
+
// public explore bundle is its own cache group now.)
|
|
1466
|
+
if (JSON.stringify(publicExploreTools) !== JSON.stringify(expectedPublicReadTools)) {
|
|
1467
|
+
throw new Error(`public explore role must ship the readonly bundle (incl. explore): expected=${expectedPublicReadTools.join(', ')} actual=${publicExploreTools.join(', ')}`);
|
|
1460
1468
|
}
|
|
1461
1469
|
if (recursiveWrapperToolNameForPublicAgent('explore') !== 'explore') {
|
|
1462
1470
|
throw new Error('call-time anti-recursion must map public explore agent to its own wrapper tool');
|
|
@@ -22,6 +22,12 @@ synonyms, library/domain names), plus `find` with name fragments, plus `code_gra
|
|
|
22
22
|
symbol_search when the query names an identifier. A single-pattern,
|
|
23
23
|
single-tool first turn is a defect.
|
|
24
24
|
|
|
25
|
+
A turn-1 broad grep MUST set `output_mode:"files_with_matches"` — an
|
|
26
|
+
unscoped `content`/`content_with_context` scan reads every match body and is
|
|
27
|
+
a defect (a full-content scan across the tree costs seconds). Use
|
|
28
|
+
`content_with_context` ONLY against a `path` that appeared in an earlier
|
|
29
|
+
result THIS session, and always with `head_limit`.
|
|
30
|
+
|
|
25
31
|
Search tokens are CODE tokens: first translate natural-language or
|
|
26
32
|
non-English queries into probable English identifiers (e.g. "최대 루프 반복
|
|
27
33
|
횟수" → maxLoop, loop-policy, iterations). Grep non-ASCII text only when
|
|
@@ -1,3 +1,3 @@
|
|
|
1
|
-
# Channels
|
|
2
|
-
|
|
3
|
-
- Channel features are handled by the runtime.
|
|
1
|
+
# Channels
|
|
2
|
+
|
|
3
|
+
- Channel features are handled by the runtime.
|
|
@@ -4,12 +4,19 @@
|
|
|
4
4
|
prior result. Merge equivalent variants/scopes into ONE call wherever the
|
|
5
5
|
schema accepts arrays (`pattern[]`, `path[]`, `symbols[]`, `query[]`).
|
|
6
6
|
- Route by what is already known: known symbol/relation → `code_graph`;
|
|
7
|
-
exact text in a known scope → `grep`; unknown location
|
|
8
|
-
|
|
9
|
-
|
|
7
|
+
exact text in a known scope → `grep`; unknown location, machine-wide/
|
|
8
|
+
out-of-repo whereabouts, or concept-level question → `explore` (which uses
|
|
9
|
+
the hardened `find` internally); name fragment → `find`; exact name pattern
|
|
10
|
+
→ `glob`; known directory → `list`; known file/region → `read`.
|
|
11
|
+
- `explore` fan-out: at task start, decompose what the task needs to know
|
|
12
|
+
into independent facets (implementation site, config/load path, tests,
|
|
13
|
+
error origin, ...) and send them as ONE `query[]` call — facets run in
|
|
14
|
+
parallel. Never fan out rephrasings of the same target; on
|
|
15
|
+
EXPLORATION_FAILED, retry once with changed tokens.
|
|
10
16
|
- Valid anchors come from user input or tool output in this session; locate
|
|
11
17
|
anything else with `find` before `grep`/`read`. On ENOENT the next call is
|
|
12
|
-
`find` on the basename — never a retried guess
|
|
18
|
+
`find` on the basename — never a retried guess; and never guess an absolute
|
|
19
|
+
path outside the project — `find` from a verified broad root instead.
|
|
13
20
|
- Retrieval stops when evidence covers the deliverable: single-answer tasks
|
|
14
21
|
end at the first sufficient anchor; enumeration tasks (review, audit) end
|
|
15
22
|
when the stated scope is covered. Never re-verify a hit already on screen;
|