mixdog 0.9.18 → 0.9.19
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 +3 -2
- package/scripts/output-style-smoke.mjs +2 -2
- package/scripts/recall-bench-cases.json +11 -0
- package/scripts/recall-bench.mjs +91 -2
- package/scripts/tool-efficiency-diag.mjs +4 -1
- package/scripts/tool-smoke.mjs +101 -27
- package/src/agents/debugger/AGENT.md +3 -3
- package/src/agents/heavy-worker/AGENT.md +7 -10
- package/src/agents/maintainer/AGENT.md +1 -2
- package/src/agents/reviewer/AGENT.md +1 -2
- package/src/agents/worker/AGENT.md +5 -8
- package/src/defaults/agents.json +3 -0
- package/src/mixdog-session-runtime.mjs +23 -6
- package/src/rules/agent/00-core.md +4 -3
- package/src/rules/agent/30-explorer.md +53 -22
- package/src/rules/lead/lead-tool.md +3 -2
- package/src/rules/shared/01-tool.md +24 -29
- package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/anthropic.mjs +1 -1
- package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +24 -3
- package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +3 -3
- package/src/runtime/agent/orchestrator/session/context-utils.mjs +2 -2
- package/src/runtime/agent/orchestrator/session/loop/steering-ladder.mjs +250 -35
- package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +6 -4
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +198 -6
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +10 -2
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +13 -15
- package/src/runtime/agent/orchestrator/tools/builtin/read-batch.mjs +6 -1
- package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +45 -3
- package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +5 -2
- package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +195 -3
- package/src/runtime/agent/orchestrator/tools/builtin.mjs +17 -1
- package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +38 -0
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +4 -4
- package/src/runtime/agent/orchestrator/tools/shell-state.mjs +1 -1
- package/src/runtime/channels/backends/discord-gateway.mjs +14 -1
- package/src/runtime/channels/backends/discord.mjs +43 -1
- package/src/runtime/channels/index.mjs +145 -58
- package/src/runtime/channels/lib/inbound-routing.mjs +19 -12
- package/src/runtime/channels/lib/memory-client.mjs +32 -14
- package/src/runtime/channels/lib/output-forwarder.mjs +38 -7
- package/src/runtime/channels/lib/owner-heartbeat.mjs +13 -4
- package/src/runtime/channels/lib/runtime-paths.mjs +35 -1
- package/src/runtime/channels/lib/tool-dispatch.mjs +17 -7
- package/src/runtime/memory/lib/memory-recall-scope-filter.mjs +8 -2
- package/src/runtime/memory/lib/memory-recall-store.mjs +182 -9
- package/src/runtime/memory/lib/query-handlers.mjs +36 -4
- package/src/runtime/memory/lib/recall-format.mjs +106 -6
- package/src/runtime/memory/lib/session-ingest.mjs +1 -1
- package/src/runtime/shared/atomic-file.mjs +10 -4
- package/src/runtime/shared/background-tasks.mjs +4 -2
- package/src/runtime/shared/tool-execution-contract.mjs +1 -1
- package/src/runtime/shared/tool-result-summary.mjs +1 -1
- package/src/runtime/shared/tool-surface.mjs +30 -1
- package/src/session-runtime/config-lifecycle.mjs +1 -1
- package/src/session-runtime/cwd-plugins.mjs +46 -3
- package/src/session-runtime/mcp-glue.mjs +24 -3
- package/src/session-runtime/output-styles.mjs +44 -10
- package/src/session-runtime/workflow.mjs +16 -1
- package/src/standalone/channel-worker.mjs +74 -5
- package/src/standalone/explore-tool.mjs +1 -1
- package/src/tui/App.jsx +57 -77
- package/src/tui/app/channel-pickers.mjs +45 -0
- package/src/tui/app/slash-commands.mjs +0 -1
- package/src/tui/app/slash-dispatch.mjs +0 -16
- package/src/tui/app/transcript-window.mjs +44 -1
- package/src/tui/app/use-mouse-input.mjs +9 -2
- package/src/tui/app/use-prompt-handlers.mjs +7 -94
- package/src/tui/app/use-transcript-scroll.mjs +5 -1
- package/src/tui/app/use-transcript-window.mjs +65 -5
- package/src/tui/components/PromptInput.jsx +33 -64
- package/src/tui/components/ToolExecution.jsx +2 -2
- package/src/tui/dist/index.mjs +5330 -5443
- package/src/tui/engine.mjs +49 -10
- package/src/tui/lib/voice-setup.mjs +166 -0
- package/src/tui/paste-attachments.mjs +12 -5
- package/src/tui/prompt-history-store.mjs +125 -12
- package/scripts/bench/cache-probe-tasks.json +0 -8
- package/scripts/bench/lead-review-tasks-r3.json +0 -20
- package/scripts/bench/lead-review-tasks.json +0 -20
- package/scripts/bench/r4-mixed-tasks.json +0 -20
- package/scripts/bench/r5-orchestrated-task.json +0 -7
- package/scripts/bench/review-tasks.json +0 -20
- package/scripts/bench/round-codex.json +0 -114
- package/scripts/bench/round-mixdog-lead-r3.json +0 -269
- package/scripts/bench/round-mixdog-lead.json +0 -269
- package/scripts/bench/round-mixdog.json +0 -126
- package/scripts/bench/round-r10-bigsample.json +0 -679
- package/scripts/bench/round-r11-codexalign.json +0 -257
- package/scripts/bench/round-r13-clientmeta.json +0 -464
- package/scripts/bench/round-r14-betafeatures.json +0 -466
- package/scripts/bench/round-r15-fulldefault.json +0 -462
- package/scripts/bench/round-r16-sessionid.json +0 -466
- package/scripts/bench/round-r17-wirebytes.json +0 -456
- package/scripts/bench/round-r18-prewarm.json +0 -468
- package/scripts/bench/round-r19-clean.json +0 -472
- package/scripts/bench/round-r20-prewarm-clean.json +0 -475
- package/scripts/bench/round-r21-delta-retry.json +0 -473
- package/scripts/bench/round-r22-full-probe.json +0 -693
- package/scripts/bench/round-r23-itemprobe.json +0 -701
- package/scripts/bench/round-r24-shapefix.json +0 -677
- package/scripts/bench/round-r25-serial.json +0 -464
- package/scripts/bench/round-r26-parallel3.json +0 -671
- package/scripts/bench/round-r27-parallel10.json +0 -894
- package/scripts/bench/round-r28-parallel10-stagger.json +0 -882
- package/scripts/bench/round-r29-parallel10-stagger166.json +0 -886
- package/scripts/bench/round-r30-instid.json +0 -253
- package/scripts/bench/round-r31-upgradeprobe.json +0 -256
- package/scripts/bench/round-r32-vs-codex-lead.json +0 -254
- package/scripts/bench/round-r33-vs-codex-codex.json +0 -115
- package/scripts/bench/round-r34-orchestrated.json +0 -120
- package/scripts/bench/round-r35-orchestrated-codex.json +0 -61
- package/scripts/bench/round-r36-orchestrated-capped.json +0 -128
- package/scripts/bench/round-r4-codex.json +0 -114
- package/scripts/bench/round-r4-mixed.json +0 -225
- package/scripts/bench/round-r5-gpt-lead.json +0 -259
- package/scripts/bench/round-r6-codex.json +0 -114
- package/scripts/bench/round-r6-solo.json +0 -257
- package/scripts/bench/round-r7-full.json +0 -254
- package/scripts/bench/round-r8-fulldefault.json +0 -255
- package/src/tui/components/prompt-input/voice-indicator.mjs +0 -39
- package/src/tui/lib/voice-recorder.mjs +0 -469
|
@@ -8,27 +8,58 @@ kind: retrieval
|
|
|
8
8
|
|
|
9
9
|
Coordinate locator. Deliverable = WHERE as `path:line`, never WHY — no
|
|
10
10
|
explaining or tracing. You ARE the `explore` tool: never call it; shared
|
|
11
|
-
rules pointing at `explore` don't apply
|
|
12
|
-
|
|
11
|
+
rules pointing at `explore` don't apply.
|
|
12
|
+
|
|
13
|
+
Tools: grep/find/glob/code_graph ONLY. `read` and `list` are FORBIDDEN —
|
|
14
|
+
grep match lines already carry every `path:line` you may output; opening a
|
|
15
|
+
file to "confirm" or "understand" a hit is the WHY you must not answer.
|
|
16
|
+
This ban has NO exception: not after a hit, not for the reason field, not
|
|
17
|
+
"just one read" — a `read`/`list` call anywhere in the session is a defect.
|
|
18
|
+
|
|
19
|
+
Turn 1 is the whole search, ONE message: one grep whose `pattern[]` packs
|
|
20
|
+
3-6 token variants (camelCase/kebab-case/snake_case/SCREAMING_SNAKE casings,
|
|
21
|
+
synonyms, library/domain names), plus `find` with name fragments, plus `code_graph`
|
|
22
|
+
symbol_search when the query names an identifier. A single-pattern,
|
|
23
|
+
single-tool first turn is a defect.
|
|
24
|
+
|
|
25
|
+
Search tokens are CODE tokens: first translate natural-language or
|
|
26
|
+
non-English queries into probable English identifiers (e.g. "최대 루프 반복
|
|
27
|
+
횟수" → maxLoop, loop-policy, iterations). Grep non-ASCII text only when
|
|
28
|
+
the query quotes a literal string.
|
|
29
|
+
|
|
30
|
+
Search scope is ALWAYS the session working directory: omit `path` (default
|
|
31
|
+
scope) or pass ONLY a path that appeared in an earlier result this session.
|
|
32
|
+
Inventing a directory (`/workspace/...`, guessed `src/lib`, another repo's
|
|
33
|
+
layout) is a defect — those results are always empty; on zero hits change
|
|
34
|
+
TOKENS, never guess paths.
|
|
13
35
|
|
|
14
36
|
Credibility is mechanical: hit = line/path contains ANY query token or
|
|
15
|
-
obvious synonym
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
No third branch: "hits exist but I want better
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
37
|
+
obvious synonym — never judge "is this the real implementation". Sole
|
|
38
|
+
exception: a generic-word-only match (schema, handler, config, resolver…)
|
|
39
|
+
with no SPECIFIC query token (product/library/domain name) = zero, not a hit.
|
|
40
|
+
A `code_graph` symbol hit (find_symbol/symbol_search returning `path:line`)
|
|
41
|
+
IS an anchor — emit it directly; never re-locate it with grep.
|
|
42
|
+
|
|
43
|
+
Rule zero — after EVERY tool result: ≥1 `path:line` matching a query token
|
|
44
|
+
→ answer NOW with those coordinates, mark weak anchors `?`; zero → one more
|
|
45
|
+
batch if budget remains. No third branch: "hits exist but I want better
|
|
46
|
+
ones" IS branch one. Once ANY turn yields a matching line, the only legal
|
|
47
|
+
next output is the final answer text — every further tool call is a defect.
|
|
48
|
+
|
|
49
|
+
Turns: hard max 3, expected 1; start each tool message with `turn N/3`.
|
|
50
|
+
Turns 2-3 are miss-recovery only (prior turn had ZERO matching lines) and
|
|
51
|
+
must change tokens OR scope, never reword.
|
|
52
|
+
Flow/how questions: first matching entry/definition anchors ARE the answer,
|
|
53
|
+
one anchor per concept — never trace the chain.
|
|
54
|
+
Compound queries ("where is X and what value/default does it use"): the
|
|
55
|
+
definition anchor answers BOTH parts — never launch extra searches for the
|
|
56
|
+
value, threshold, or content; one anchor per concept, all from the same batch.
|
|
57
|
+
|
|
58
|
+
Answer, nothing else: anchor lines `path:line — symbol — short reason`
|
|
59
|
+
(`?` if weak), max 3 lines — extra anchors are cost, not quality; pick the
|
|
60
|
+
3 with the most specific token match. Or `EXPLORATION_FAILED` — only after
|
|
61
|
+
the turn budget is
|
|
62
|
+
spent with zero anchors, or when every anchor matches only generic words.
|
|
63
|
+
Before emitting `EXPLORATION_FAILED`, re-scan ALL earlier tool results: if
|
|
64
|
+
any line anywhere matched a specific query token, answer with that anchor
|
|
65
|
+
(`?` if weak) instead — a weak anchor beats a false miss.
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
# Lead Tool Use
|
|
2
2
|
|
|
3
|
-
-
|
|
4
|
-
|
|
3
|
+
- Write-role agents run their own build/test verification via `shell`; Lead
|
|
4
|
+
runs cross-scope verification, benches, and everything git via `shell`
|
|
5
|
+
directly.
|
|
5
6
|
- Use the session's current project/workspace. Change the work project only
|
|
6
7
|
when the user asks for another project or a tool call needs another root.
|
|
7
8
|
- Use `agent` for scoped implementation, research, review, and debugging — not
|
|
@@ -1,32 +1,27 @@
|
|
|
1
1
|
# Tool Use
|
|
2
2
|
|
|
3
|
-
- Batch independent
|
|
4
|
-
prior result
|
|
5
|
-
|
|
6
|
-
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
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.
|
|
3
|
+
- Batch independent calls in one turn; serialize only when a call needs a
|
|
4
|
+
prior result. Merge equivalent variants/scopes into ONE call wherever the
|
|
5
|
+
schema accepts arrays (`pattern[]`, `path[]`, `symbols[]`, `query[]`).
|
|
6
|
+
- Route by what is already known: known symbol/relation → `code_graph`;
|
|
7
|
+
exact text in a known scope → `grep`; unknown location or concept-level
|
|
8
|
+
question → `explore`; name fragment → `find`; exact name pattern → `glob`;
|
|
9
|
+
known directory → `list`; known file/region → `read`.
|
|
10
|
+
- Valid anchors come from user input or tool output in this session; locate
|
|
11
|
+
anything else with `find` before `grep`/`read`. On ENOENT the next call is
|
|
12
|
+
`find` on the basename — never a retried guess.
|
|
13
|
+
- Retrieval stops when evidence covers the deliverable: single-answer tasks
|
|
14
|
+
end at the first sufficient anchor; enumeration tasks (review, audit) end
|
|
15
|
+
when the stated scope is covered. Never re-verify a hit already on screen;
|
|
16
|
+
a plausible anchor means the next call is the action itself
|
|
17
|
+
(patch/answer/handoff) — never "one more confirming read".
|
|
18
|
+
- Content-mode `grep` requests enough context (`-C`) to act without
|
|
19
|
+
re-reading the same file; bare match lines are existence checks only.
|
|
20
|
+
- Repeat a search concept only after a zero-result call, changing tokens or
|
|
21
|
+
scope — never wording alone.
|
|
22
|
+
- `read` windows use `offset`/`limit`; multiple spans or files = ONE call
|
|
23
|
+
with `{path,offset,limit}[]` regions. Size the window to the whole logical
|
|
24
|
+
unit (function/section) — over-read once instead of paging the same file
|
|
25
|
+
in small windows across turns; a 3rd window into one file means the first
|
|
26
|
+
should have been wider.
|
|
32
27
|
- Don't mix `apply_patch` with shell or other state-changing calls in one turn.
|
|
@@ -376,7 +376,7 @@ function toAnthropicMessages(messages) {
|
|
|
376
376
|
continue;
|
|
377
377
|
}
|
|
378
378
|
|
|
379
|
-
//
|
|
379
|
+
// First-party client parity: fold a user text turn that directly follows a
|
|
380
380
|
// tool_result turn into that tool_result's content. A sibling text
|
|
381
381
|
// turn after tool_result renders as `</function_results>\n\nHuman:`
|
|
382
382
|
// on the wire and trains the model toward 3-token empty end_turn
|
|
@@ -378,7 +378,7 @@ function toAnthropicMessages(messages) {
|
|
|
378
378
|
}
|
|
379
379
|
continue;
|
|
380
380
|
}
|
|
381
|
-
//
|
|
381
|
+
// First-party client parity: fold a user text turn that directly follows a
|
|
382
382
|
// tool_result turn into that tool_result's content (empty end_turn
|
|
383
383
|
// livelock prevention; see foldUserTextIntoToolResultTail).
|
|
384
384
|
if (m.role === 'user' && foldUserTextIntoToolResultTail(result, normalizeContentForAnthropic(m.content))) {
|
|
@@ -241,14 +241,35 @@ function windowFromPercent(label, value, source) {
|
|
|
241
241
|
return Object.keys(out).length > 2 ? out : null;
|
|
242
242
|
}
|
|
243
243
|
|
|
244
|
+
// Grok Build billing periods are migrating from monthly to a shared weekly
|
|
245
|
+
// pool (xAI rollout is per-account). Derive the window cadence from the
|
|
246
|
+
// actual billing period length instead of hardcoding monthly: <=10 days
|
|
247
|
+
// means a weekly cycle, anything longer (or unknown) stays monthly.
|
|
248
|
+
function grokBillingCadence(config) {
|
|
249
|
+
if (config?.weeklyLimit !== undefined || config?.weekly_limit !== undefined) {
|
|
250
|
+
return { label: 'W', period: 'weekly' };
|
|
251
|
+
}
|
|
252
|
+
const start = resetAtMs(config?.billingPeriodStart ?? config?.billing_period_start);
|
|
253
|
+
const end = resetAtMs(config?.billingPeriodEnd ?? config?.billing_period_end);
|
|
254
|
+
if (start && end && end > start && (end - start) <= 10 * 24 * 60 * 60_000) {
|
|
255
|
+
return { label: 'W', period: 'weekly' };
|
|
256
|
+
}
|
|
257
|
+
return { label: 'M', period: 'monthly' };
|
|
258
|
+
}
|
|
259
|
+
|
|
244
260
|
function creditWindowFromBilling(config) {
|
|
245
261
|
if (!config || typeof config !== 'object') return null;
|
|
246
|
-
const limit = num(
|
|
262
|
+
const limit = num(
|
|
263
|
+
config.weeklyLimit?.val ?? config.weeklyLimit
|
|
264
|
+
?? config.monthlyLimit?.val ?? config.monthlyLimit
|
|
265
|
+
?? config.includedLimit?.val,
|
|
266
|
+
null,
|
|
267
|
+
);
|
|
247
268
|
const used = num(config.used?.val ?? config.includedUsed?.val ?? config.used, null);
|
|
248
269
|
if (limit === null || used === null || !(limit > 0)) return null;
|
|
249
270
|
const resetAt = resetAtMs(config.billingPeriodEnd ?? config.billing_period_end);
|
|
250
271
|
return {
|
|
251
|
-
label:
|
|
272
|
+
label: grokBillingCadence(config).label,
|
|
252
273
|
source: 'grok-build-billing',
|
|
253
274
|
usedPct: round(Math.min(100, used * 100 / limit), 2),
|
|
254
275
|
usedCredits: round(used, 2),
|
|
@@ -265,7 +286,7 @@ function balanceFromGrokBilling(config) {
|
|
|
265
286
|
if (cap === null || !(cap > 0)) return null;
|
|
266
287
|
return {
|
|
267
288
|
source: 'grok-build-on-demand',
|
|
268
|
-
period:
|
|
289
|
+
period: grokBillingCadence(config).period,
|
|
269
290
|
budgetCredits: round(cap, 2),
|
|
270
291
|
spentCredits: round(used, 2),
|
|
271
292
|
remainingCredits: round(Math.max(0, cap - used), 2),
|
|
@@ -29,15 +29,15 @@ import {
|
|
|
29
29
|
// 408 — request timeout
|
|
30
30
|
// 500/502/503/504 — server errors (overload / bad gateway / timeout)
|
|
31
31
|
// 429 is excluded from blanket transient retry, but withRetry() honors a
|
|
32
|
-
// SHORT Retry-After header (< SHORT_RETRY_AFTER_MAX_MS)
|
|
33
|
-
//
|
|
32
|
+
// SHORT Retry-After header (< SHORT_RETRY_AFTER_MAX_MS): a short-retry
|
|
33
|
+
// threshold of 20s matches first-party client behavior:
|
|
34
34
|
// brief throttle windows retry once the server-stated wait elapses; long or
|
|
35
35
|
// headerless 429s still surface immediately (quota windows are
|
|
36
36
|
// deterministic — sleeping into an outer watchdog just relabels the error).
|
|
37
37
|
const TRANSIENT_STATUSES = new Set([408, 500, 502, 503, 504])
|
|
38
38
|
|
|
39
39
|
// Max server-stated Retry-After we are willing to sleep on a 429 before
|
|
40
|
-
// surfacing it. Mirrors
|
|
40
|
+
// surfacing it. Mirrors the first-party client's 20s short-retry threshold.
|
|
41
41
|
const SHORT_RETRY_AFTER_MAX_MS = 20_000
|
|
42
42
|
|
|
43
43
|
// HTTP statuses that mean "permanent: stop retrying, surface to caller".
|
|
@@ -651,8 +651,8 @@ export function sanitizeAnthropicContentPairs(messages) {
|
|
|
651
651
|
|
|
652
652
|
/**
|
|
653
653
|
* Fold a plain user text turn into the trailing tool_result block of the
|
|
654
|
-
* previous user message (
|
|
655
|
-
*
|
|
654
|
+
* previous user message (first-party client parity: merge user content
|
|
655
|
+
* blocks into the trailing tool_result). Any sibling text after a
|
|
656
656
|
* tool_result renders as `</function_results>\n\nHuman:<...>` on the
|
|
657
657
|
* Anthropic wire; repeated mid-conversation this teaches the model to emit
|
|
658
658
|
* 3-token empty end_turn completions (upstream A/B sai-20260310-161901:
|
|
@@ -8,6 +8,91 @@ import { appendAgentTrace } from '../../agent-trace.mjs';
|
|
|
8
8
|
import { level2SteerMessage } from './completion-guards.mjs';
|
|
9
9
|
import { isEagerDispatchable } from './tool-helpers.mjs';
|
|
10
10
|
|
|
11
|
+
// --- Steering detector helpers (pure) ---
|
|
12
|
+
const _WORD_RE = /[A-Za-z_$][\w$]*/g;
|
|
13
|
+
const _IDENT_RE = /^[A-Za-z_$][\w$]{2,}$/;
|
|
14
|
+
const _SRC_DIR_RE = /(^|[\\/])(src|lib|app|packages|components|pages)([\\/]|$)/i;
|
|
15
|
+
function _grepPatterns(c) {
|
|
16
|
+
const p = c?.arguments?.pattern;
|
|
17
|
+
if (typeof p === 'string') return [p];
|
|
18
|
+
if (Array.isArray(p)) return p.filter((x) => typeof x === 'string');
|
|
19
|
+
return [];
|
|
20
|
+
}
|
|
21
|
+
function _patternTokens(c) {
|
|
22
|
+
const out = new Set();
|
|
23
|
+
for (const pat of _grepPatterns(c)) {
|
|
24
|
+
for (const t of (String(pat).match(_WORD_RE) || [])) {
|
|
25
|
+
if (t.length >= 3) out.add(t.toLowerCase());
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return out;
|
|
29
|
+
}
|
|
30
|
+
function _pathsOf(arg) {
|
|
31
|
+
const out = [];
|
|
32
|
+
const push = (v) => {
|
|
33
|
+
if (typeof v === 'string' && v) out.push(v);
|
|
34
|
+
else if (v && typeof v === 'object' && typeof v.path === 'string' && v.path) out.push(v.path);
|
|
35
|
+
};
|
|
36
|
+
if (Array.isArray(arg)) arg.forEach(push); else push(arg);
|
|
37
|
+
return out;
|
|
38
|
+
}
|
|
39
|
+
// Case-insensitive (win32 paths) forward-slash normalization for all path compares.
|
|
40
|
+
function _normPath(p) { return String(p).replace(/\\/g, '/').toLowerCase(); }
|
|
41
|
+
function _baseName(p) { const s = _normPath(p); const i = s.lastIndexOf('/'); return i >= 0 ? s.slice(i + 1) : s; }
|
|
42
|
+
function _dirName(p) { const s = _normPath(p); const i = s.lastIndexOf('/'); return i >= 0 ? s.slice(0, i) : ''; }
|
|
43
|
+
// Exact (normalized) same-file test — used where "related" is too loose.
|
|
44
|
+
function _samePath(a, b) { return !!a && !!b && _normPath(a) === _normPath(b); }
|
|
45
|
+
// Loose sibling/variant relation — cluster detector pairs this WITH token overlap.
|
|
46
|
+
function _pathsRelated(a, b) {
|
|
47
|
+
if (!a || !b) return false;
|
|
48
|
+
const na = _normPath(a); const nb = _normPath(b);
|
|
49
|
+
if (na === nb) return true; // identical
|
|
50
|
+
if (na.includes(nb) || nb.includes(na)) return true; // normalized variant
|
|
51
|
+
if (_dirName(na) && _dirName(na) === _dirName(nb)) return true; // siblings
|
|
52
|
+
const ba = _baseName(na); const bb = _baseName(nb);
|
|
53
|
+
return !!ba && ba === bb; // same file name
|
|
54
|
+
}
|
|
55
|
+
function _setsOverlap(a, b) { for (const x of a) { if (b.has(x)) return true; } return false; }
|
|
56
|
+
// A single chained segment is read-only only if it STARTS with a read-only verb.
|
|
57
|
+
function _isReadOnlyShellSegment(seg) {
|
|
58
|
+
const s = seg.trim();
|
|
59
|
+
if (!s) return null; // empty (trailing operator)
|
|
60
|
+
if (/^git\s+(status|log)\b/.test(s)) return true;
|
|
61
|
+
if (/^git\s+diff\b[^\n]*--stat\b/.test(s)) return true;
|
|
62
|
+
if (/^get-childitem\b/.test(s)) return true;
|
|
63
|
+
return /^(cat|ls|dir|type)\b/.test(s);
|
|
64
|
+
}
|
|
65
|
+
// Read-only only when EVERY chained segment is read-only — a single mutating
|
|
66
|
+
// segment (e.g. `git status && npm test`) disqualifies the whole command.
|
|
67
|
+
function _isReadOnlyShellCmd(cmd) {
|
|
68
|
+
const s = String(cmd || '').toLowerCase().trim();
|
|
69
|
+
if (!s) return false;
|
|
70
|
+
let sawCmd = false;
|
|
71
|
+
for (const seg of s.split(/&&|\|\||[;&|]/)) {
|
|
72
|
+
const verdict = _isReadOnlyShellSegment(seg);
|
|
73
|
+
if (verdict === null) continue; // blank segment
|
|
74
|
+
if (verdict === false) return false; // a mutating segment
|
|
75
|
+
sawCmd = true;
|
|
76
|
+
}
|
|
77
|
+
return sawCmd;
|
|
78
|
+
}
|
|
79
|
+
// Extract (path, offset) windows from a read call: path may be a string, an
|
|
80
|
+
// array of strings, or an array of {path,offset,limit} regions; a top-level
|
|
81
|
+
// offset applies to string entries.
|
|
82
|
+
function _readWindows(c) {
|
|
83
|
+
const a = c?.arguments || {};
|
|
84
|
+
const out = [];
|
|
85
|
+
const push = (p, off) => { if (typeof p === 'string' && p) out.push({ path: p, offset: Number(off) }); };
|
|
86
|
+
const v = a.path;
|
|
87
|
+
if (Array.isArray(v)) {
|
|
88
|
+
for (const e of v) {
|
|
89
|
+
if (typeof e === 'string') push(e, a.offset);
|
|
90
|
+
else if (e && typeof e === 'object') push(e.path, e.offset ?? a.offset);
|
|
91
|
+
}
|
|
92
|
+
} else if (typeof v === 'string') push(v, a.offset);
|
|
93
|
+
return out;
|
|
94
|
+
}
|
|
95
|
+
|
|
11
96
|
// Build the completion-first steering-ladder controller. `ctx` supplies live
|
|
12
97
|
// accessors so every read reflects the loop's current mutable state:
|
|
13
98
|
// - messages, sessionId, sessionAgent, tools (stable refs/values)
|
|
@@ -43,13 +128,26 @@ export function createSteeringLadder(ctx) {
|
|
|
43
128
|
// call (missed parallelism). Not reset per-iteration — only by the
|
|
44
129
|
// steering-hint fire below or by a turn that batches/edits.
|
|
45
130
|
let _serialReadOnlyStreak = 0;
|
|
46
|
-
//
|
|
47
|
-
//
|
|
48
|
-
//
|
|
49
|
-
//
|
|
50
|
-
let
|
|
51
|
-
let
|
|
131
|
+
// Same-concept grep cluster (broadened serial-rewording spiral): counts
|
|
132
|
+
// consecutive grep-only turns that stay on ONE concept — related by
|
|
133
|
+
// identical/normalized/sibling paths OR pattern-token overlap, not just an
|
|
134
|
+
// identical path. `_sameGrepPrev` holds the prior turn's {paths,tokens}.
|
|
135
|
+
let _sameGrepStreak = 0;
|
|
136
|
+
let _sameGrepPrev = null;
|
|
137
|
+
// Prior turn's uncapped content/context grep paths — seeds the
|
|
138
|
+
// grep-context-then-read detector (a read of the same file next turn).
|
|
139
|
+
let _lastGrepContentPaths = null;
|
|
140
|
+
// Consecutive turns grepping the SAME identifier-like token in source dirs
|
|
141
|
+
// (symbol lookups that belong in code_graph).
|
|
142
|
+
let _identGrepStreak = 0;
|
|
143
|
+
let _identGrepToken = null;
|
|
52
144
|
let _level2FireCount = 0;
|
|
145
|
+
// Read-fragmentation detector state: normalized path -> cumulative
|
|
146
|
+
// single-window offset reads. Mirrors session-bench read_fragmentation
|
|
147
|
+
// (3+ windowed reads of one file within an 800-line span). Multi-window
|
|
148
|
+
// same-path regions in ONE call are the recommended batched form and are
|
|
149
|
+
// never counted. Fires once per path per session.
|
|
150
|
+
const _readWindowsByPath = new Map();
|
|
53
151
|
|
|
54
152
|
// Level-2 steering emitter shared by both ladder paths (single-call
|
|
55
153
|
// level-1 streak and the independent all-read-only streak). Sets the latch
|
|
@@ -78,14 +176,14 @@ export function createSteeringLadder(ctx) {
|
|
|
78
176
|
const iterations = getIterations();
|
|
79
177
|
const editCount = getEditCount();
|
|
80
178
|
// Steering hint gate: at most ONE hint per turn (priority:
|
|
81
|
-
// level-2 >
|
|
179
|
+
// level-2 > grep/shell detectors > level-1).
|
|
82
180
|
let _hintFiredThisTurn = hintAlreadyFired;
|
|
83
|
-
// Missed-parallelism steering:
|
|
181
|
+
// Missed-parallelism steering: 2+ consecutive turns of a single
|
|
84
182
|
// read-only tool call suggest the model isn't batching independent
|
|
85
|
-
// lookups. Nudge once, then reset (fires again after
|
|
183
|
+
// lookups. Nudge once, then reset (fires again after 2 more).
|
|
86
184
|
if (calls.length === 1 && isEagerDispatchable(calls[0].name, tools)) {
|
|
87
185
|
_serialReadOnlyStreak += 1;
|
|
88
|
-
if (_serialReadOnlyStreak >=
|
|
186
|
+
if (_serialReadOnlyStreak >= 2 && !_hintFiredThisTurn) {
|
|
89
187
|
_serialReadOnlyStreak = 0;
|
|
90
188
|
// Escalation ladder (Step 1). Cumulative level-1 fires are
|
|
91
189
|
// tracked and NEVER reset. Once level-1 has fired >=3 times with
|
|
@@ -96,7 +194,7 @@ export function createSteeringLadder(ctx) {
|
|
|
96
194
|
if (_level1FireCount >= 3 && editCount === 0 && (iterations - _level2LatchAtIteration) >= 5) {
|
|
97
195
|
_emitLevel2Steer();
|
|
98
196
|
} else {
|
|
99
|
-
pushSystemReminder('Last
|
|
197
|
+
pushSystemReminder('Last 2 turns each ran a single read-only tool. Batch independent lookups (read/grep/glob/code_graph) into ONE turn, or start editing if you have enough context.');
|
|
100
198
|
}
|
|
101
199
|
_hintFiredThisTurn = true;
|
|
102
200
|
}
|
|
@@ -121,36 +219,153 @@ export function createSteeringLadder(ctx) {
|
|
|
121
219
|
_allReadOnlyStreak = 0;
|
|
122
220
|
}
|
|
123
221
|
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
//
|
|
127
|
-
//
|
|
222
|
+
const _grepCalls = calls.filter((c) => c?.name === 'grep');
|
|
223
|
+
const _allGrep = calls.length > 0 && _grepCalls.length === calls.length;
|
|
224
|
+
// Detector: repeated identifier-like grep in source dirs → code_graph.
|
|
225
|
+
// Symbol lookups (a bare \w+ token scoped to src/) belong in
|
|
226
|
+
// code_graph (find_symbol/references/callers), not text grep. Fires
|
|
227
|
+
// when the same token is grepped on 2+ consecutive qualifying turns.
|
|
128
228
|
{
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
const
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
if (
|
|
137
|
-
if (
|
|
138
|
-
else {
|
|
139
|
-
if (
|
|
140
|
-
pushSystemReminder(`
|
|
141
|
-
|
|
142
|
-
|
|
229
|
+
let _identTok = null;
|
|
230
|
+
for (const c of _grepCalls) {
|
|
231
|
+
const _srcScoped = _pathsOf(c?.arguments?.path).some((p) => _SRC_DIR_RE.test(p));
|
|
232
|
+
if (!_srcScoped) continue;
|
|
233
|
+
const _hit = _grepPatterns(c).map((p) => p.trim()).find((p) => _IDENT_RE.test(p));
|
|
234
|
+
if (_hit) { _identTok = _hit.toLowerCase(); break; }
|
|
235
|
+
}
|
|
236
|
+
if (_identTok) {
|
|
237
|
+
if (_identTok === _identGrepToken) _identGrepStreak += 1;
|
|
238
|
+
else { _identGrepToken = _identTok; _identGrepStreak = 1; }
|
|
239
|
+
if (_identGrepStreak >= 2 && !_hintFiredThisTurn) {
|
|
240
|
+
pushSystemReminder(`Grepping the symbol "${_identGrepToken}" across source dirs again — use code_graph (find_symbol/references/callers) for symbol lookups instead of text grep.`);
|
|
241
|
+
_identGrepStreak = 0;
|
|
242
|
+
_identGrepToken = null;
|
|
243
|
+
_hintFiredThisTurn = true;
|
|
244
|
+
}
|
|
245
|
+
} else {
|
|
246
|
+
_identGrepStreak = 0;
|
|
247
|
+
_identGrepToken = null;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
// Same-concept grep cluster (broadened serial-rewording): 3+
|
|
251
|
+
// consecutive grep-only turns on one concept — related by
|
|
252
|
+
// an exact-same path, OR a sibling/variant path that ALSO shares a
|
|
253
|
+
// pattern token (sibling alone is too loose to count).
|
|
254
|
+
// Fires once per spiral, then resets; read-the-span is the answer.
|
|
255
|
+
{
|
|
256
|
+
if (_allGrep) {
|
|
257
|
+
const _paths = [];
|
|
258
|
+
const _tokens = new Set();
|
|
259
|
+
for (const c of _grepCalls) {
|
|
260
|
+
for (const p of _pathsOf(c?.arguments?.path)) _paths.push(p);
|
|
261
|
+
for (const t of _patternTokens(c)) _tokens.add(t);
|
|
262
|
+
}
|
|
263
|
+
let _related = false;
|
|
264
|
+
if (_sameGrepPrev) {
|
|
265
|
+
// Exact same-path spiral fires alone; a merely sibling/
|
|
266
|
+
// variant path must ALSO share a pattern token to count.
|
|
267
|
+
const _pathExact = _paths.some((a) => _sameGrepPrev.paths.some((b) => _samePath(a, b)));
|
|
268
|
+
const _pathRel = _paths.some((a) => _sameGrepPrev.paths.some((b) => _pathsRelated(a, b)));
|
|
269
|
+
const _tokRel = _setsOverlap(_tokens, _sameGrepPrev.tokens);
|
|
270
|
+
_related = _pathExact || (_pathRel && _tokRel);
|
|
271
|
+
}
|
|
272
|
+
_sameGrepStreak = _related ? _sameGrepStreak + 1 : 1;
|
|
273
|
+
const _label = _paths[0] || [..._tokens][0] || 'same concept';
|
|
274
|
+
_sameGrepPrev = { paths: _paths, tokens: _tokens };
|
|
275
|
+
if (_sameGrepStreak >= 3 && !_hintFiredThisTurn) {
|
|
276
|
+
pushSystemReminder(`3+ consecutive grep turns on the same concept (${_label}) — reworded patterns / sibling paths are not converging. Read the relevant span (read with offset/limit) or act on what you have.`);
|
|
277
|
+
_sameGrepStreak = 0;
|
|
278
|
+
_sameGrepPrev = null;
|
|
143
279
|
_hintFiredThisTurn = true;
|
|
144
280
|
}
|
|
145
281
|
} else {
|
|
146
|
-
|
|
147
|
-
|
|
282
|
+
_sameGrepStreak = 0;
|
|
283
|
+
_sameGrepPrev = null;
|
|
148
284
|
}
|
|
149
285
|
}
|
|
286
|
+
// Detector: content/context grep on X then read of X next turn — the
|
|
287
|
+
// grep context should have sufficed. Fire off the PRIOR turn's
|
|
288
|
+
// uncapped content grep, then recompute for the next turn (skip when
|
|
289
|
+
// the grep was capped via head_limit or paths-only mode).
|
|
290
|
+
{
|
|
291
|
+
if (_lastGrepContentPaths && !_hintFiredThisTurn) {
|
|
292
|
+
const _readCall = calls.find((c) => c?.name === 'read');
|
|
293
|
+
if (_readCall) {
|
|
294
|
+
const _rp = _pathsOf(_readCall?.arguments?.path);
|
|
295
|
+
// Same-file only — a sibling read is a new lookup, not a re-read.
|
|
296
|
+
const _hit = _rp.find((p) => [..._lastGrepContentPaths].some((g) => _samePath(p, g)));
|
|
297
|
+
if (_hit) {
|
|
298
|
+
pushSystemReminder(`Reading ${_baseName(_hit)} right after a content grep on it — the grep context should have sufficed; answer from grep output or widen -C instead of re-reading.`);
|
|
299
|
+
_hintFiredThisTurn = true;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
const _next = new Set();
|
|
304
|
+
for (const c of _grepCalls) {
|
|
305
|
+
const a = c?.arguments || {};
|
|
306
|
+
const _hasCtx = a['-A'] != null || a['-B'] != null || a['-C'] != null;
|
|
307
|
+
const _isContent = a.output_mode === 'content' || a.output_mode === 'content_with_context' || _hasCtx;
|
|
308
|
+
const _pathsOnly = a.output_mode === 'files_with_matches' || a.output_mode === 'count';
|
|
309
|
+
const _capped = a.head_limit != null;
|
|
310
|
+
if (_isContent && !_pathsOnly && !_capped) {
|
|
311
|
+
for (const p of _pathsOf(a.path)) _next.add(p);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
_lastGrepContentPaths = _next.size ? _next : null;
|
|
315
|
+
}
|
|
316
|
+
// Detector: read fragmentation — 2nd+ single-window read into the
|
|
317
|
+
// SAME file with offsets inside an 800-line span means the model is
|
|
318
|
+
// paging small windows across turns instead of reading one wider
|
|
319
|
+
// span (or batching {path,offset,limit}[] regions in one call).
|
|
320
|
+
{
|
|
321
|
+
for (const c of calls.filter((x) => x?.name === 'read')) {
|
|
322
|
+
const _byPath = new Map();
|
|
323
|
+
for (const w of _readWindows(c)) {
|
|
324
|
+
const k = _normPath(w.path);
|
|
325
|
+
if (!_byPath.has(k)) _byPath.set(k, []);
|
|
326
|
+
_byPath.get(k).push(w);
|
|
327
|
+
}
|
|
328
|
+
for (const [k, ws] of _byPath) {
|
|
329
|
+
if (ws.length !== 1) continue; // batched regions on one path — the good form
|
|
330
|
+
const off = ws[0].offset;
|
|
331
|
+
if (!Number.isFinite(off) || off <= 0) continue;
|
|
332
|
+
let rec = _readWindowsByPath.get(k);
|
|
333
|
+
if (!rec) {
|
|
334
|
+
rec = { offsets: [], fired: false };
|
|
335
|
+
_readWindowsByPath.set(k, rec);
|
|
336
|
+
if (_readWindowsByPath.size > 32) _readWindowsByPath.delete(_readWindowsByPath.keys().next().value);
|
|
337
|
+
}
|
|
338
|
+
rec.offsets.push(off);
|
|
339
|
+
if (!rec.fired && !_hintFiredThisTurn && rec.offsets.length >= 2
|
|
340
|
+
&& (Math.max(...rec.offsets) - Math.min(...rec.offsets)) <= 800) {
|
|
341
|
+
pushSystemReminder(`Windowed reads are fragmenting ${_baseName(k)} — stop paging small windows; read ONE wider span, or batch all needed spans as {path,offset,limit}[] regions in a single call.`);
|
|
342
|
+
rec.fired = true;
|
|
343
|
+
_hintFiredThisTurn = true;
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
// Detector: read-only shell (git status/log/diff --stat, ls/dir/cat/
|
|
349
|
+
// type/Get-ChildItem) inspects state the dedicated tools cover.
|
|
350
|
+
// Nudge toward them; never blocks execution.
|
|
351
|
+
{
|
|
352
|
+
if (!_hintFiredThisTurn && calls.some((c) => c?.name === 'shell' && _isReadOnlyShellCmd(c?.arguments?.command))) {
|
|
353
|
+
pushSystemReminder('Read-only shell (git status/log/diff --stat, ls/dir/cat/type/Get-ChildItem) inspects state the dedicated tools cover — use grep/read/list/find/code_graph; shell is for changing state or running programs.');
|
|
354
|
+
_hintFiredThisTurn = true;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
},
|
|
358
|
+
// Reviewer fix: a zero-tool turn must not bridge ANY cross-turn streak
|
|
359
|
+
// across non-tool turns — that would fire level-2 (or the grep/read
|
|
360
|
+
// detectors) early on a worker that paused to synthesize text mid-run.
|
|
361
|
+
resetAllReadOnlyStreak() {
|
|
362
|
+
_allReadOnlyStreak = 0;
|
|
363
|
+
_serialReadOnlyStreak = 0;
|
|
364
|
+
_sameGrepStreak = 0;
|
|
365
|
+
_sameGrepPrev = null;
|
|
366
|
+
_lastGrepContentPaths = null;
|
|
367
|
+
_identGrepStreak = 0;
|
|
368
|
+
_identGrepToken = null;
|
|
150
369
|
},
|
|
151
|
-
// Reviewer fix: a zero-tool turn must not bridge the all-read-only
|
|
152
|
-
// streak across non-tool turns — that would fire level-2 early on a
|
|
153
|
-
// worker that paused to synthesize text mid-run.
|
|
154
|
-
resetAllReadOnlyStreak() { _allReadOnlyStreak = 0; },
|
|
155
370
|
};
|
|
156
371
|
}
|
|
@@ -87,10 +87,10 @@ function stringToolPermissionAllowList(toolPermission) {
|
|
|
87
87
|
return null;
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
-
// Read-write agent bundle: edit surface
|
|
91
|
-
//
|
|
92
|
-
//
|
|
93
|
-
//
|
|
90
|
+
// Read-write agent bundle: full edit surface INCLUDING shell/task so
|
|
91
|
+
// write-role agents can run their own verification (build/test) without
|
|
92
|
+
// bouncing it back to Lead. Deploy/ship remains a workflow-level rule,
|
|
93
|
+
// not a tool-surface restriction.
|
|
94
94
|
const AGENT_STRING_PERMISSION_READ_WRITE_ALLOW = Object.freeze([
|
|
95
95
|
'code_graph',
|
|
96
96
|
'find',
|
|
@@ -99,6 +99,8 @@ const AGENT_STRING_PERMISSION_READ_WRITE_ALLOW = Object.freeze([
|
|
|
99
99
|
'grep',
|
|
100
100
|
'read',
|
|
101
101
|
'apply_patch',
|
|
102
|
+
'shell',
|
|
103
|
+
'task',
|
|
102
104
|
'explore',
|
|
103
105
|
'search',
|
|
104
106
|
'web_fetch',
|