mixdog 0.9.143 → 0.9.144
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/src/lib/rules-builder.cjs +44 -15
- package/src/rules/lead/lead-brief.md +7 -8
- package/src/rules/shared/10-tool-workflow.md +28 -8
- package/src/rules/shared/20-research.md +4 -0
- package/src/rules/shared/30-exploration.md +23 -35
- package/src/rules/shared/40-editing.md +4 -0
- package/src/rules/shared/50-execution.md +1 -0
- package/src/rules/shared/60-verification.md +9 -0
- package/src/rules/shared/70-delivery.md +1 -2
- package/src/rules/shared/80-memory.md +6 -2
- package/src/runtime/agent/orchestrator/providers/lib/grok-tool-schema.mjs +22 -1
- package/src/runtime/agent/orchestrator/session/agent-loop.mjs +7 -2
- package/src/runtime/agent/orchestrator/session/evidence-union.test.mjs +1 -1
- package/src/runtime/agent/orchestrator/session/image-strip-recovery.test.mjs +61 -0
- package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +10 -10
- package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +9 -19
- package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +17 -37
- package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +1 -32
- package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.test.mjs +10 -15
- package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-context-expander.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +3 -3
- package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +1 -1
- package/src/runtime/agent/orchestrator/tools/shell-command.mjs +4 -9
- package/src/session-runtime/agent-disable.test.mjs +5 -3
- package/src/session-runtime/tool-policy-surface.test.mjs +64 -11
- package/src/session-runtime/workflow.mjs +8 -3
- package/src/tui/session/turn.mjs +5 -0
- package/src/workflows/default/WORKFLOW.md +5 -4
- package/src/workflows/solo/WORKFLOW.md +3 -5
package/package.json
CHANGED
|
@@ -202,12 +202,32 @@ function stripFrontmatter(markdown) {
|
|
|
202
202
|
return String(markdown || '').replace(/^---[ \t]*\r?\n[\s\S]*?\r?\n---[ \t]*(?:\r?\n|$)/, '').trim();
|
|
203
203
|
}
|
|
204
204
|
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
const
|
|
210
|
-
|
|
205
|
+
// Tool dependency is declared as metadata, not matched against prose. A
|
|
206
|
+
// `<!-- tools: a, b -->` marker binds the block that follows it: the block
|
|
207
|
+
// survives while any listed tool is on the session surface and disappears
|
|
208
|
+
// once every one of them is omitted. Markers never reach the model.
|
|
209
|
+
const TOOL_MARKER_RE = /^[ \t]*<!--[ \t]*tools:[ \t]*([^>]*?)[ \t]*-->[ \t]*$/;
|
|
210
|
+
|
|
211
|
+
function markerTools(line) {
|
|
212
|
+
const match = TOOL_MARKER_RE.exec(String(line ?? ''));
|
|
213
|
+
if (!match) return null;
|
|
214
|
+
return match[1].split(',').map((name) => name.trim().toLowerCase()).filter(Boolean);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
// A marked block runs from the line after the marker through every deeper
|
|
218
|
+
// indented continuation line, ending at the next marker, blank line, or a
|
|
219
|
+
// line at the same or shallower indent.
|
|
220
|
+
function markedBlockEnd(lines, start) {
|
|
221
|
+
const indent = lines[start].search(/\S/);
|
|
222
|
+
let end = start + 1;
|
|
223
|
+
while (end < lines.length) {
|
|
224
|
+
const line = lines[end];
|
|
225
|
+
if (!line.trim() || markerTools(line)) break;
|
|
226
|
+
if (line.search(/\S/) <= indent) break;
|
|
227
|
+
end += 1;
|
|
228
|
+
}
|
|
229
|
+
return end;
|
|
230
|
+
}
|
|
211
231
|
|
|
212
232
|
function omitKeySet(omitTools) {
|
|
213
233
|
return new Set((Array.isArray(omitTools) ? omitTools : []).map((name) => String(name || '').toLowerCase()).filter(Boolean));
|
|
@@ -216,16 +236,25 @@ function omitKeySet(omitTools) {
|
|
|
216
236
|
/** Drop routing clauses for tools that are not on the session surface. */
|
|
217
237
|
function omitToolRoutes(text, omitTools = []) {
|
|
218
238
|
const deny = omitKeySet(omitTools);
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
239
|
+
const lines = String(text || '').split(/\r?\n/);
|
|
240
|
+
const kept = [];
|
|
241
|
+
let index = 0;
|
|
242
|
+
while (index < lines.length) {
|
|
243
|
+
const tools = markerTools(lines[index]);
|
|
244
|
+
if (!tools) {
|
|
245
|
+
kept.push(lines[index]);
|
|
246
|
+
index += 1;
|
|
247
|
+
continue;
|
|
248
|
+
}
|
|
249
|
+
index += 1;
|
|
250
|
+
if (index >= lines.length) break;
|
|
251
|
+
const end = markedBlockEnd(lines, index);
|
|
252
|
+
if (!tools.length || !tools.every((name) => deny.has(name))) {
|
|
253
|
+
kept.push(...lines.slice(index, end));
|
|
254
|
+
}
|
|
255
|
+
index = end;
|
|
226
256
|
}
|
|
227
|
-
|
|
228
|
-
return out.replace(/[ \t]+\n/g, '\n').replace(/\n{3,}/g, '\n\n');
|
|
257
|
+
return kept.join('\n').replace(/[ \t]+\n/g, '\n').replace(/\n{3,}/g, '\n\n').trim();
|
|
229
258
|
}
|
|
230
259
|
|
|
231
260
|
// Framing line under the style header: the block owns user-facing prose only,
|
|
@@ -1,13 +1,12 @@
|
|
|
1
1
|
# Lead Brief
|
|
2
2
|
|
|
3
|
-
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
without discarding requirements.
|
|
3
|
+
- Every role's `Task:` is mandatory and lossless — build it from the original
|
|
4
|
+
request and the official spec/test acceptance criteria, preserving intent,
|
|
5
|
+
required and forbidden outcomes, completion/stop boundary, user-supplied
|
|
6
|
+
exact targets, and exact replacements/outputs.
|
|
7
|
+
- Never infer exactness from task name, file count, or difficulty.
|
|
8
|
+
- Minimum chars, maximum info: one-line fragments, no role-known rules, no
|
|
9
|
+
repeated context or facts, no padding.
|
|
11
10
|
- Other fields are task-specific deltas — `Anchors:` (`file:line` plus a
|
|
12
11
|
one-line conclusion, never log/code bodies), `Allow/Forbid:`, `Deliver:`
|
|
13
12
|
(sets handoff shape/size); omit empty fields. State outcomes, not methods,
|
|
@@ -1,20 +1,40 @@
|
|
|
1
1
|
# Tool Workflow
|
|
2
2
|
|
|
3
|
-
-
|
|
4
|
-
|
|
5
|
-
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
3
|
+
- Investigate, build, and verify only what the requested outcome requires, at
|
|
4
|
+
the level it requires; trust internal and framework guarantees.
|
|
5
|
+
- Minimize tool turns through maximal useful parallelism. Cost is counted in
|
|
6
|
+
rounds, not calls: a batch is one round, so a call-count saving never
|
|
7
|
+
justifies a worse-routed call. Plan the fewest evidence-complete dependent
|
|
8
|
+
rounds first, then the fewest calls within each round.
|
|
9
|
+
- In each round, issue every necessary non-overlapping call whose inputs are
|
|
10
|
+
already known; defer a call only when its target or arguments require an
|
|
11
|
+
earlier result.
|
|
12
|
+
- Route each remaining evidence facet once to its primary owner, preferring the
|
|
13
|
+
operation that directly returns the evidence needed for the next decision. A
|
|
14
|
+
summary, overview, or enumeration is not a prerequisite when that operation's
|
|
15
|
+
complete inputs are already known; if independently required, batch it with
|
|
16
|
+
the detailed operation.
|
|
17
|
+
- Never duplicate a facet, widen retrieval speculatively, or cap fanout; apply
|
|
18
|
+
one analysis to many targets as one parameterized call when supported.
|
|
9
19
|
1. Determine the required outcome and missing information; requirements are
|
|
10
20
|
not evidence.
|
|
11
21
|
2. If needed, gather only missing information through Research or Exploration;
|
|
12
22
|
use Execution when the information can only be produced by running a program
|
|
13
|
-
or observing runtime state.
|
|
14
|
-
obtained.
|
|
23
|
+
or observing runtime state.
|
|
15
24
|
3. Perform the required answer, edit, or execution in the fewest safe coherent
|
|
16
25
|
calls.
|
|
17
26
|
4. Verify only affected facets and essential invariants when required.
|
|
27
|
+
- Known state — system guarantees, supplied facts, visible tool returns,
|
|
28
|
+
applied patches, and passed checks — is never re-found, re-derived, or
|
|
29
|
+
re-verified at any granularity: no re-query call, no confirmation subcommand
|
|
30
|
+
inside a shell command, no availability probe for what the operation itself
|
|
31
|
+
would report, no reopening a file to confirm an edit, no rerun of a passed
|
|
32
|
+
check.
|
|
33
|
+
- Mine each returned result fully before opening the next round. A follow-up
|
|
34
|
+
is valid only for evidence a result omitted, invalidated, or newly made
|
|
35
|
+
necessary; an independently required call that no result created belonged in
|
|
36
|
+
the earlier batch.
|
|
37
|
+
- Evidence that determines the answer, edit, or deliverable ends retrieval.
|
|
18
38
|
- Treat failure as new evidence and repeat steps 1–4 only for affected facets.
|
|
19
39
|
Report a blocker when no deterministic next action remains.
|
|
20
40
|
- Use only named tools present in the current tool surface.
|
|
@@ -1,6 +1,10 @@
|
|
|
1
|
+
<!-- tools: web_search, web_fetch -->
|
|
1
2
|
# Research
|
|
2
3
|
|
|
4
|
+
<!-- tools: web_search, web_fetch -->
|
|
3
5
|
- Research routes:
|
|
6
|
+
<!-- tools: web_search -->
|
|
4
7
|
current or external information discovery→`web_search`;
|
|
8
|
+
<!-- tools: web_fetch -->
|
|
5
9
|
page or documentation body retrieval from a known URL→`web_fetch`.
|
|
6
10
|
|
|
@@ -2,43 +2,31 @@
|
|
|
2
2
|
|
|
3
3
|
- Use read-only means for inspection; never mutate to clear an obstacle or
|
|
4
4
|
unexpected state. Preserve evidence before a required mutation can destroy it.
|
|
5
|
-
-
|
|
6
|
-
|
|
7
|
-
|
|
5
|
+
- Ownership is exclusive: each evidence type has one owner, another tool's
|
|
6
|
+
ability to reach the same target is never an alternative route, and a
|
|
7
|
+
successful owner result closes that facet — only a different evidence type
|
|
8
|
+
routes elsewhere.
|
|
9
|
+
- Route the missing evidence to its primary owner:
|
|
10
|
+
repository state, history, or diff→`git`;
|
|
11
|
+
exact symbol declaration, body, usage, or relation→`code_graph`;
|
|
12
|
+
literal, regex, or text location→`grep`;
|
|
13
|
+
known-file content, range, or image→`read`;
|
|
14
|
+
wildcard or recursive file paths→`glob`;
|
|
8
15
|
known directory's immediate entries→`list`;
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
insufficient or unnecessary→`read`.
|
|
14
|
-
- Read-only tools — `find`, `glob`, `list`, `grep`, `code_graph`, `read` —
|
|
15
|
-
always batch safely in parallel.
|
|
16
|
-
- Paths reachable by expanding an environment variable or the home directory
|
|
17
|
-
are resolved locations, not unknowns.
|
|
18
|
-
- In the first response, launch all investigations knowable from the request
|
|
19
|
-
alone (enumeration, content probes, file samples) as one batch; each
|
|
20
|
-
follow-up batch exists only for questions the previous results created.
|
|
21
|
-
- Batching never licenses a guessed `glob.path`
|
|
22
|
-
(unknown location → `find` first; omit path for the current Project).
|
|
16
|
+
unknown file or directory location→`find`.
|
|
17
|
+
- Use a path locator only when the owner's required target is unknown. Paths
|
|
18
|
+
reachable by expanding an environment variable or the home directory are
|
|
19
|
+
resolved locations, not unknowns.
|
|
23
20
|
- Enumerate sibling directories or same-kind files with one wildcard call
|
|
24
21
|
(`glob`, or `read` with a glob for content sampling), never a
|
|
25
22
|
directory-by-directory `list` walk or one `read` per file.
|
|
26
|
-
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
paths and omit optional scopes equal to its root; explicit paths may be
|
|
36
|
-
outside cwd only for targets outside the project.
|
|
37
|
-
- Do not re-read content already returned by any tool or reopen a successfully
|
|
38
|
-
edited file solely to confirm the edit. Read only missing context or content
|
|
39
|
-
invalidated by a reported failure, partial operation, or external change.
|
|
40
|
-
- `code_graph references` supplies the declaration and scoped usages and ends
|
|
41
|
-
that facet; values/locations end at the context `grep` returns; `read` covers
|
|
42
|
-
only omitted lines or missing anchored ranges. Any visible returned span can
|
|
43
|
-
supply exact source context; do not fetch it again.
|
|
23
|
+
- Treat supplied target locations as resolved; access them directly without
|
|
24
|
+
locator searches. Within the current project, pass project-relative paths and
|
|
25
|
+
omit optional scopes equal to its root; explicit paths may be outside cwd
|
|
26
|
+
only for targets outside the project.
|
|
27
|
+
- Before deciding how to parse, count, transform, or summarize files whose
|
|
28
|
+
format has not been inspected, inspect the original content itself.
|
|
29
|
+
- Returned declarations, bodies, usages, relations, and contextual spans from
|
|
30
|
+
any tool — not only `read` — are source context; `read` covers only omitted
|
|
31
|
+
lines or missing anchored ranges.
|
|
44
32
|
|
|
@@ -1,11 +1,15 @@
|
|
|
1
1
|
# Editing
|
|
2
2
|
|
|
3
|
+
- A required new file is created directly: Add File is itself the atomic
|
|
4
|
+
absence check, so inspect only if it reports the target already exists.
|
|
3
5
|
- Source: use exact current target text from any visible evidence, including
|
|
4
6
|
user input, tool output, or an applied edit result; never reconstruct it from
|
|
5
7
|
another file, a sample, or expectation.
|
|
6
8
|
- Placement: with `edit`, use an exact unique target string, expanding exact
|
|
7
9
|
surrounding text when needed; with `apply_patch`, use exact unchanged context
|
|
8
10
|
and add a class/function locator when context alone is not unique.
|
|
11
|
+
- Apply all determined changes in the fewest safe calls the active tool
|
|
12
|
+
supports; a file written in one call is written complete.
|
|
9
13
|
- Batch scope: never split one file across concurrent edit calls. Group
|
|
10
14
|
same-intent changes with exact context into coherent calls; issue disjoint
|
|
11
15
|
calls together in one turn, and defer ambiguous or result-dependent changes.
|
|
@@ -5,6 +5,15 @@
|
|
|
5
5
|
invariants; use an umbrella suite only when the user explicitly requests it
|
|
6
6
|
or a documented project or release process requires it.
|
|
7
7
|
- Issue all independent checks in one turn.
|
|
8
|
+
- Blocking checks cover only essential integrity, security, compatibility, and
|
|
9
|
+
buildability invariants. Treat mutable behavior, UX, exact text, snapshots,
|
|
10
|
+
and implementation shape as advisory specifications; update them when the
|
|
11
|
+
requested behavior changes instead of preserving obsolete behavior.
|
|
12
|
+
- A check runs at the strictness the task requires; never raise a tool's own
|
|
13
|
+
severity beyond it.
|
|
8
14
|
- If verification fails, collect all failures, leave Verification, complete all
|
|
9
15
|
determinable fixes, then re-enter Verification for the resulting state.
|
|
16
|
+
- A successful verification closes the task unless later changes affect it;
|
|
17
|
+
rerun a failed action only after its inputs or subject change, otherwise
|
|
18
|
+
report it unresolved.
|
|
10
19
|
|
|
@@ -1,11 +1,15 @@
|
|
|
1
|
+
<!-- tools: recall, memory -->
|
|
1
2
|
# Memory
|
|
2
3
|
|
|
4
|
+
<!-- tools: recall -->
|
|
3
5
|
- past facts recorded in prior work or sessions→`recall`
|
|
4
6
|
(stored history only, never current local state).
|
|
7
|
+
<!-- tools: memory -->
|
|
5
8
|
- Durable memory creation or update→`memory`; store a compact English
|
|
6
9
|
statement.
|
|
10
|
+
<!-- tools: memory -->
|
|
7
11
|
- Use judgment to decide whether a durable memory should be stored, whether
|
|
8
12
|
user confirmation is needed, and which scope best fits the context.
|
|
13
|
+
<!-- tools: memory -->
|
|
9
14
|
- Omit `project_id` for the current Project, use `"common"` for shared memory,
|
|
10
|
-
or
|
|
11
|
-
read-only.
|
|
15
|
+
or an explicit Project slug for another named Project; `*` is read-only.
|
|
@@ -65,6 +65,26 @@ function firstExclusiveRequired(branches) {
|
|
|
65
65
|
return [];
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
+
const ARRAY_DROP_NOTE = 'This provider accepts a single value here, not an array.';
|
|
69
|
+
|
|
70
|
+
function describesArray(schema) {
|
|
71
|
+
if (!schema || typeof schema !== 'object' || Array.isArray(schema)) return false;
|
|
72
|
+
return schema.type === 'array' || (Array.isArray(schema.type) && schema.type.includes('array'));
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// Flattening keeps one branch, so a description that still promises the dropped
|
|
76
|
+
// shape would advertise more than the wire schema accepts. Project the loss
|
|
77
|
+
// into the text the model actually reads.
|
|
78
|
+
function projectDroppedBranches(schema, dropped) {
|
|
79
|
+
if (describesArray(schema) || !dropped.some(describesArray)) return schema;
|
|
80
|
+
const description = String(schema.description || '').trim();
|
|
81
|
+
if (description.includes(ARRAY_DROP_NOTE)) return schema;
|
|
82
|
+
return {
|
|
83
|
+
...schema,
|
|
84
|
+
description: description ? `${description} ${ARRAY_DROP_NOTE}` : ARRAY_DROP_NOTE,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
68
88
|
function normalizeGrokPropertySchema(schema) {
|
|
69
89
|
if (!schema || typeof schema !== 'object' || Array.isArray(schema)) return schema;
|
|
70
90
|
const branches = [
|
|
@@ -75,7 +95,8 @@ function normalizeGrokPropertySchema(schema) {
|
|
|
75
95
|
const first = branches.find(branch => branch && typeof branch === 'object' && !Array.isArray(branch));
|
|
76
96
|
if (first) {
|
|
77
97
|
const { anyOf: _anyOf, oneOf: _oneOf, ...siblings } = schema;
|
|
78
|
-
|
|
98
|
+
const dropped = branches.filter(branch => branch !== first);
|
|
99
|
+
return normalizeGrokPropertySchema(projectDroppedBranches({ ...first, ...siblings }, dropped));
|
|
79
100
|
}
|
|
80
101
|
}
|
|
81
102
|
if (!schema.properties || typeof schema.properties !== 'object') return schema;
|
|
@@ -203,7 +203,13 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
203
203
|
};
|
|
204
204
|
const sessionRef = opts.session || null;
|
|
205
205
|
let _providerPrefixGuardState = sessionRef?._providerPrefixGuardState || null;
|
|
206
|
-
|
|
206
|
+
// Provider tool snapshots are request-loop state, never durable session
|
|
207
|
+
// state. Older builds persisted this field, which let a resumed session
|
|
208
|
+
// keep advertising a retired schema even after session.tools was rebuilt.
|
|
209
|
+
if (sessionRef && Object.prototype.hasOwnProperty.call(sessionRef, '_providerToolSurfaceSnapshot')) {
|
|
210
|
+
delete sessionRef._providerToolSurfaceSnapshot;
|
|
211
|
+
}
|
|
212
|
+
let _fixedProviderToolSurface = null;
|
|
207
213
|
const loopUsageMetricsEpoch = () => Number(sessionRef?.usageMetricsEpoch) || 0;
|
|
208
214
|
const loopUsageMetricsTurnId = () => Number(sessionRef?.usageMetricsTurnId) || 0;
|
|
209
215
|
// Sub-agent (worker/heavy-worker/reviewer/…) sessions
|
|
@@ -496,7 +502,6 @@ export async function agentLoop(provider, messages, model, tools, onToolCall, cw
|
|
|
496
502
|
});
|
|
497
503
|
if (!_fixedProviderToolSurface) {
|
|
498
504
|
_fixedProviderToolSurface = _candidateSendTools;
|
|
499
|
-
if (sessionRef) sessionRef._providerToolSurfaceSnapshot = _fixedProviderToolSurface;
|
|
500
505
|
}
|
|
501
506
|
sendTools = _fixedProviderToolSurface;
|
|
502
507
|
requestToolScope = {
|
|
@@ -142,7 +142,7 @@ test('apply_patch, shell, and mutating git batches invalidate all earlier eviden
|
|
|
142
142
|
['apply_patch', {}],
|
|
143
143
|
['shell', {}],
|
|
144
144
|
['git', { command: 'git commit -m test' }],
|
|
145
|
-
['git', { command: "git reflog delete 'HEAD@{1}'"
|
|
145
|
+
['git', { command: "git reflog delete 'HEAD@{1}'" }],
|
|
146
146
|
]) {
|
|
147
147
|
const messages = [
|
|
148
148
|
call('read_1', 'read', { file_path: 'src/a.mjs' }),
|
|
@@ -164,6 +164,67 @@ test('agent loop heals one rejected tail image and the next turn stays usable',
|
|
|
164
164
|
assert.equal(nextCalls, 1);
|
|
165
165
|
});
|
|
166
166
|
|
|
167
|
+
test('agent loop keeps provider tool snapshots turn-local', async () => {
|
|
168
|
+
const oldTool = {
|
|
169
|
+
name: 'shell',
|
|
170
|
+
description: 'old schema',
|
|
171
|
+
inputSchema: { type: 'object', properties: { retired: { type: 'boolean' } } },
|
|
172
|
+
};
|
|
173
|
+
const currentTool = {
|
|
174
|
+
name: 'shell',
|
|
175
|
+
description: 'current schema',
|
|
176
|
+
inputSchema: { type: 'object', properties: { command: { type: 'string' } } },
|
|
177
|
+
};
|
|
178
|
+
const nextTool = {
|
|
179
|
+
name: 'shell',
|
|
180
|
+
description: 'next schema',
|
|
181
|
+
inputSchema: { type: 'object', properties: { command: { type: 'string' }, timeout_ms: { type: 'number' } } },
|
|
182
|
+
};
|
|
183
|
+
const sentToolDescriptions = [];
|
|
184
|
+
const provider = {
|
|
185
|
+
async send(_messages, _model, tools) {
|
|
186
|
+
sentToolDescriptions.push(tools?.[0]?.description);
|
|
187
|
+
return { content: 'done', toolCalls: [], stopReason: 'end_turn' };
|
|
188
|
+
},
|
|
189
|
+
};
|
|
190
|
+
const session = {
|
|
191
|
+
id: 'provider-tool-snapshot-lifetime-test',
|
|
192
|
+
owner: 'cli',
|
|
193
|
+
contextWindow: 200_000,
|
|
194
|
+
rawContextWindow: 200_000,
|
|
195
|
+
compaction: { auto: false },
|
|
196
|
+
_providerToolSurfaceSnapshot: [oldTool],
|
|
197
|
+
};
|
|
198
|
+
const messages = [
|
|
199
|
+
{ role: 'system', content: 'system' },
|
|
200
|
+
{ role: 'user', content: 'first turn' },
|
|
201
|
+
];
|
|
202
|
+
|
|
203
|
+
await agentLoop(
|
|
204
|
+
provider,
|
|
205
|
+
messages,
|
|
206
|
+
'fake-model',
|
|
207
|
+
[currentTool],
|
|
208
|
+
null,
|
|
209
|
+
process.cwd(),
|
|
210
|
+
{ session, sessionId: session.id },
|
|
211
|
+
);
|
|
212
|
+
assert.equal(Object.hasOwn(session, '_providerToolSurfaceSnapshot'), false);
|
|
213
|
+
|
|
214
|
+
messages.push({ role: 'user', content: 'next turn' });
|
|
215
|
+
await agentLoop(
|
|
216
|
+
provider,
|
|
217
|
+
messages,
|
|
218
|
+
'fake-model',
|
|
219
|
+
[nextTool],
|
|
220
|
+
null,
|
|
221
|
+
process.cwd(),
|
|
222
|
+
{ session, sessionId: session.id },
|
|
223
|
+
);
|
|
224
|
+
|
|
225
|
+
assert.deepEqual(sentToolDescriptions, ['current schema', 'next schema']);
|
|
226
|
+
});
|
|
227
|
+
|
|
167
228
|
test('mid-stream xAI generation crash is retryable even as invalid_request_error', () => {
|
|
168
229
|
const err = new Error('xAI Responses stream error: Internal error during token generation');
|
|
169
230
|
err.providerWireError = true;
|
|
@@ -32,8 +32,14 @@ const GREP_CTX_HEAD_LIMIT_MAX = 40;
|
|
|
32
32
|
|
|
33
33
|
// Unbounded (no offset/limit) plain full reads default to this window instead of
|
|
34
34
|
// pulling the whole file; the read tool's ranged-read footer then hands the
|
|
35
|
-
// caller the next offset to page with.
|
|
36
|
-
|
|
35
|
+
// caller the next offset to page with. At 1000 a single default read returned
|
|
36
|
+
// 51KB of an unread log; halving it capped the largest read at 20KB and the
|
|
37
|
+
// callers paged on instead of re-reading (tool-budget bench, 20260821).
|
|
38
|
+
// MIXDOG_READ_DEFAULT_LIMIT overrides for A/B runs.
|
|
39
|
+
const READ_GUARD_DEFAULT_LIMIT = (() => {
|
|
40
|
+
const parsed = parseInt(process.env.MIXDOG_READ_DEFAULT_LIMIT ?? '', 10);
|
|
41
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : 500;
|
|
42
|
+
})();
|
|
37
43
|
|
|
38
44
|
// Best-effort clamp notice channel: stash a one-line note on the args so a
|
|
39
45
|
// surfacing consumer can echo it. Underscore-prefixed; ignored by executors.
|
|
@@ -660,10 +666,10 @@ function guardRead(a) {
|
|
|
660
666
|
}
|
|
661
667
|
|
|
662
668
|
function guardShell(a) {
|
|
663
|
-
const allowed = new Set(['command', 'timeout_ms'
|
|
669
|
+
const allowed = new Set(['command', 'timeout_ms']);
|
|
664
670
|
const unsupported = Object.keys(a).find((key) => !allowed.has(key));
|
|
665
671
|
if (unsupported) {
|
|
666
|
-
return `Error: shell arg "${unsupported}" is unsupported; use only command
|
|
672
|
+
return `Error: shell arg "${unsupported}" is unsupported; use only command and timeout_ms`;
|
|
667
673
|
}
|
|
668
674
|
if (!hasOwn(a, 'command')) {
|
|
669
675
|
return 'Error: shell requires "command"';
|
|
@@ -677,12 +683,6 @@ function guardShell(a) {
|
|
|
677
683
|
if (hasOwn(a, 'timeout_ms') && (typeof a.timeout_ms !== 'number' || !Number.isFinite(a.timeout_ms) || a.timeout_ms < 0)) {
|
|
678
684
|
return `Error: shell arg "timeout_ms" must be a non-negative number (got ${describeType(a.timeout_ms)})`;
|
|
679
685
|
}
|
|
680
|
-
if (hasOwn(a, 'run_in_background') && typeof a.run_in_background !== 'boolean') {
|
|
681
|
-
return `Error: shell arg "run_in_background" must be a boolean (got ${describeType(a.run_in_background)})`;
|
|
682
|
-
}
|
|
683
|
-
if (hasOwn(a, 'monitor_interval_ms') && !isValidShellMonitorIntervalMs(a.monitor_interval_ms)) {
|
|
684
|
-
return `Error: shell arg "monitor_interval_ms" must be 0 or an integer from ${SHELL_MONITOR_INTERVAL_MIN_MS} to ${SHELL_MONITOR_INTERVAL_MAX_MS} ms`;
|
|
685
|
-
}
|
|
686
686
|
return null;
|
|
687
687
|
}
|
|
688
688
|
|
|
@@ -62,14 +62,12 @@ import {
|
|
|
62
62
|
SHELL_RUNTIME_CANDIDATES,
|
|
63
63
|
} from './runtime-capabilities.mjs';
|
|
64
64
|
import { planDirectExeSpawn } from './shell-direct-exe.mjs';
|
|
65
|
-
import { resolveShellMonitorIntervalMs } from './shell-monitor.mjs';
|
|
66
65
|
|
|
67
66
|
// Commands start in the foreground. Only work still running after the
|
|
68
|
-
//
|
|
69
|
-
//
|
|
70
|
-
//
|
|
71
|
-
|
|
72
|
-
export const DEFAULT_SHELL_AUTO_BACKGROUND_MS = 15_000;
|
|
67
|
+
// 10 s coordination budget is promoted to a tracked background task.
|
|
68
|
+
// Short commands therefore complete in the original tool turn, while longer
|
|
69
|
+
// work returns partial output plus task_id and finishes by notification.
|
|
70
|
+
export const DEFAULT_SHELL_AUTO_BACKGROUND_MS = 10_000;
|
|
73
71
|
|
|
74
72
|
// Post-exec drift detection. After a foreground shell command, compare the
|
|
75
73
|
// live mtime+size of files mixdog has already read this session against their
|
|
@@ -470,11 +468,6 @@ export async function executeBashTool(args, workDir, options = {}) {
|
|
|
470
468
|
const _bgTasksDisabled = /^(1|true|yes|on)$/i.test(
|
|
471
469
|
String(process.env.MIXDOG_SHELL_DISABLE_BACKGROUND_TASKS || '').trim(),
|
|
472
470
|
);
|
|
473
|
-
const runInBackground = args.run_in_background === true;
|
|
474
|
-
const monitorIntervalMs = resolveShellMonitorIntervalMs(args.monitor_interval_ms);
|
|
475
|
-
if (runInBackground && _bgTasksDisabled) {
|
|
476
|
-
return formatShellToolFailure('background tasks are disabled for this process');
|
|
477
|
-
}
|
|
478
471
|
|
|
479
472
|
let shellEffects;
|
|
480
473
|
let combinedBashAbort = null;
|
|
@@ -485,7 +478,7 @@ export async function executeBashTool(args, workDir, options = {}) {
|
|
|
485
478
|
}
|
|
486
479
|
// timeout_ms is a caller-requested HARD total deadline, not a foreground
|
|
487
480
|
// wait budget. Omitted/0 means no deadline: the command starts foreground,
|
|
488
|
-
// then the
|
|
481
|
+
// then the 10 s coordination budget promotes it without shortening its
|
|
489
482
|
// lifetime. This matches the public schema and avoids accidental kills
|
|
490
483
|
// caused by callers guessing how long a build might take.
|
|
491
484
|
const _envMaxTimeout = parseInt(process.env.BASH_MAX_TIMEOUT_MS ?? '', 10);
|
|
@@ -528,9 +521,7 @@ export async function executeBashTool(args, workDir, options = {}) {
|
|
|
528
521
|
const backgroundMaxMs = Number.isFinite(_bgMaxEnvMs) && _bgMaxEnvMs > 0
|
|
529
522
|
? Math.min(Math.floor(_bgMaxEnvMs), TIMER_MAX_MS)
|
|
530
523
|
: 0;
|
|
531
|
-
|
|
532
|
-
// deadline; there is no foreground window to carve out of it first.
|
|
533
|
-
const execTimeoutMs = runInBackground && !hasExplicitTimeout ? backgroundMaxMs : timeout;
|
|
524
|
+
const execTimeoutMs = timeout;
|
|
534
525
|
// A caller deadline at or below the foreground window has no remaining
|
|
535
526
|
// budget to transfer. Let execShellCommand enforce that timeout instead of
|
|
536
527
|
// adopting the child with timeoutMs=0, which means unlimited to shell-jobs.
|
|
@@ -539,7 +530,7 @@ export async function executeBashTool(args, workDir, options = {}) {
|
|
|
539
530
|
const mergeStderr = true;
|
|
540
531
|
// Main-agent blocking budget. A timeout is the command's total deadline,
|
|
541
532
|
// not permission to hold the conversation open for that whole duration:
|
|
542
|
-
// after
|
|
533
|
+
// after 10 s a still-running command becomes a tracked background task and
|
|
543
534
|
// completion is pushed to the owner. Explicit timeouts keep their remaining
|
|
544
535
|
// deadline after promotion.
|
|
545
536
|
// MIXDOG_SHELL_AUTO_BACKGROUND_MS overrides; an explicit 0 disables.
|
|
@@ -623,7 +614,6 @@ export async function executeBashTool(args, workDir, options = {}) {
|
|
|
623
614
|
timeoutMs: execTimeoutMs,
|
|
624
615
|
abortSignal: combinedBashAbort.signal,
|
|
625
616
|
autoBackgroundMs,
|
|
626
|
-
startInBackground: runInBackground,
|
|
627
617
|
// On a foreground timeout, promote the still-running child to a
|
|
628
618
|
// tracked background job only when an explicit deadline has
|
|
629
619
|
// remaining budget; omitted deadlines may stay unlimited.
|
|
@@ -678,7 +668,7 @@ export async function executeBashTool(args, workDir, options = {}) {
|
|
|
678
668
|
stderr: (!mergeStderr && result.stderrPath) ? normalizeOutputPath(result.stderrPath) : null,
|
|
679
669
|
cwd: bashWorkDir,
|
|
680
670
|
timeoutMs: result.backgroundTimeoutMs || 0,
|
|
681
|
-
monitor_interval_ms:
|
|
671
|
+
monitor_interval_ms: 0,
|
|
682
672
|
},
|
|
683
673
|
resultType: 'shell_task_result',
|
|
684
674
|
cancel: () => killShellJob(result.jobId),
|
|
@@ -690,7 +680,7 @@ export async function executeBashTool(args, workDir, options = {}) {
|
|
|
690
680
|
callerSessionId: options?.callerSessionId || options?.sessionId,
|
|
691
681
|
routingSessionId: options?.routingSessionId || options?.sessionId,
|
|
692
682
|
clientHostPid: options?.clientHostPid,
|
|
693
|
-
}
|
|
683
|
+
});
|
|
694
684
|
} catch { /* best effort */ }
|
|
695
685
|
}
|
|
696
686
|
const partialOutput = renderBackgroundPartialOutput(
|
|
@@ -1,15 +1,12 @@
|
|
|
1
1
|
// --- Tool definitions for external models ---
|
|
2
2
|
//
|
|
3
3
|
// CANONICAL SOURCE for built-in tool schemas and annotations (compressible,
|
|
4
|
-
// readOnlyHint, destructiveHint, etc.).
|
|
5
|
-
//
|
|
4
|
+
// readOnlyHint, destructiveHint, etc.). A description carries the tool's
|
|
5
|
+
// behavior, argument shapes, and the usage boundaries that only apply to that
|
|
6
|
+
// tool; cross-tool policy lives in rules/shared/*.md.
|
|
6
7
|
// Platform-specific command syntax belongs next to the command argument.
|
|
7
8
|
import { GIT_TOOL_DEF } from './git-command-tool.mjs';
|
|
8
|
-
import {
|
|
9
|
-
SHELL_MONITOR_INTERVAL_DEFAULT_MS,
|
|
10
|
-
SHELL_MONITOR_INTERVAL_MAX_MS,
|
|
11
|
-
SHELL_MONITOR_INTERVAL_MIN_MS,
|
|
12
|
-
} from './shell-monitor.mjs';
|
|
9
|
+
import { SHELL_MONITOR_INTERVAL_MAX_MS } from './shell-monitor.mjs';
|
|
13
10
|
const _shellSyntaxCheat =
|
|
14
11
|
process.platform === 'win32'
|
|
15
12
|
? ' PowerShell: use ; between independent commands; use if ($LASTEXITCODE -ne 0) { exit $LASTEXITCODE } between dependent commands; single-quote inline scripts, avoid nested double quotes; /c/→C:\\; $PID is reserved.'
|
|
@@ -17,12 +14,9 @@ const _shellSyntaxCheat =
|
|
|
17
14
|
// Keep the routing map short and adjacent to the shell's primary description.
|
|
18
15
|
// PowerShell aliases appear only on win32.
|
|
19
16
|
const _shellToolRouting = process.platform === 'win32'
|
|
20
|
-
? 'Use read, NOT cat/Get-Content/head/tail; list, NOT ls/dir; find/glob, NOT find; grep, NOT grep/rg/Select-String; edit/apply_patch, NOT sed/awk/
|
|
21
|
-
: 'Use read, NOT cat/head/tail; list, NOT ls; find/glob, NOT find; grep, NOT grep/rg; edit/apply_patch, NOT sed/awk/
|
|
22
|
-
//
|
|
23
|
-
// run_in_background field from the schema entirely so the model cannot burn a
|
|
24
|
-
// failure turn attempting it. Mirrors bash-tool's runtime guard, which stays
|
|
25
|
-
// as defense in depth. Process-stable env, evaluated once at module load.
|
|
17
|
+
? 'Use read, NOT cat/Get-Content/head/tail; list, NOT ls/dir; find/glob, NOT find; grep, NOT grep/rg/Select-String; edit/apply_patch, NOT sed/awk/echo/Set-Content or a file-writing heredoc.'
|
|
18
|
+
: 'Use read, NOT cat/head/tail; list, NOT ls; find/glob, NOT find; grep, NOT grep/rg; edit/apply_patch, NOT sed/awk/echo or a file-writing heredoc.';
|
|
19
|
+
// Process-stable switch used to describe foreground-only execution accurately.
|
|
26
20
|
const _shellBackgroundDisabled = /^(1|true|yes|on)$/i.test(
|
|
27
21
|
String(process.env.MIXDOG_SHELL_DISABLE_BACKGROUND_TASKS || '').trim(),
|
|
28
22
|
);
|
|
@@ -32,12 +26,12 @@ export const BUILTIN_TOOLS = [
|
|
|
32
26
|
name: 'read',
|
|
33
27
|
title: 'Read',
|
|
34
28
|
annotations: { title: 'Read', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false, compressible: false },
|
|
35
|
-
description: 'Read-only; safe to batch in parallel. Known-file contents or line ranges. Images render for viewing; not directories. Replaces cat/head/tail.',
|
|
29
|
+
description: 'Read-only; safe to batch in parallel. Known-file contents or line ranges, bounded to the narrowest range that answers the question. Spans another tool already returned are source context; read only what they omit. Images render for viewing; not directories. Replaces cat/head/tail.',
|
|
36
30
|
inputSchema: {
|
|
37
31
|
type: 'object',
|
|
38
32
|
properties: {
|
|
39
33
|
file_path: {
|
|
40
|
-
type: 'string', description: 'Known file path as plain text
|
|
34
|
+
type: 'string', description: 'Known file path as plain text. A glob (e.g. "logs/*.log") fans out to per-file results (cap 10, newest first); literal-named files win over expansion.',
|
|
41
35
|
},
|
|
42
36
|
offset: {
|
|
43
37
|
type: 'integer',
|
|
@@ -47,7 +41,7 @@ export const BUILTIN_TOOLS = [
|
|
|
47
41
|
limit: {
|
|
48
42
|
type: 'integer',
|
|
49
43
|
minimum: 1,
|
|
50
|
-
description: 'Maximum line count as a bare integer; default
|
|
44
|
+
description: 'Maximum line count as a bare integer; default 500.',
|
|
51
45
|
},
|
|
52
46
|
},
|
|
53
47
|
required: ['file_path'],
|
|
@@ -58,7 +52,7 @@ export const BUILTIN_TOOLS = [
|
|
|
58
52
|
name: 'edit',
|
|
59
53
|
title: 'Edit',
|
|
60
54
|
annotations: { title: 'Edit', readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false, compressible: false, compressibleLossless: true },
|
|
61
|
-
description: 'Replace exact text in one file. old_string must match once unless replace_all is true. Empty old_string creates a missing file or fills an empty file; it never overwrites a non-empty file.',
|
|
55
|
+
description: 'Replace exact text in one file. Use exact text already in context; never re-open the file to build old_string or to verify a successful edit. old_string must match once unless replace_all is true. Empty old_string creates a missing file or fills an empty file; it never overwrites a non-empty file.',
|
|
62
56
|
inputSchema: {
|
|
63
57
|
type: 'object',
|
|
64
58
|
properties: {
|
|
@@ -87,7 +81,7 @@ export const BUILTIN_TOOLS = [
|
|
|
87
81
|
name: 'shell',
|
|
88
82
|
title: 'Shell',
|
|
89
83
|
annotations: { title: 'Shell', readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true, compressible: true },
|
|
90
|
-
description: `Run programs, runtime/state operations, calculations, transformations, file generation, and unsupported-format inspection. Avoid file operations covered by dedicated tools unless explicitly instructed or after verifying that a dedicated tool cannot do the job.
|
|
84
|
+
description: `Run programs, runtime/state operations, calculations, transformations, file generation, and unsupported-format inspection. ${_shellToolRouting} Avoid file operations covered by dedicated tools unless explicitly instructed or after verifying that a dedicated tool cannot do the job. An already-open shell is never a reason to route work to it. ${_shellBackgroundDisabled ? 'Commands run in the foreground until completion.' : 'Commands use a 10s foreground window by default—not a timeout. Still-running work continues as a tracked task_id and completes by notification. Use task monitor only after promotion when periodic progress is needed, task read for an extra current snapshot, and never poll in a loop.'}`,
|
|
91
85
|
inputSchema: {
|
|
92
86
|
type: 'object',
|
|
93
87
|
properties: {
|
|
@@ -97,20 +91,6 @@ export const BUILTIN_TOOLS = [
|
|
|
97
91
|
minimum: 0,
|
|
98
92
|
description: 'Optional hard total deadline in ms; kills the command even after background promotion. Omit or 0 = no deadline.',
|
|
99
93
|
},
|
|
100
|
-
...(_shellBackgroundDisabled ? {} : {
|
|
101
|
-
run_in_background: {
|
|
102
|
-
type: 'boolean',
|
|
103
|
-
default: false,
|
|
104
|
-
description: 'Start immediately as a tracked task for a known long-running command or intentional server/watcher. Default false.',
|
|
105
|
-
},
|
|
106
|
-
monitor_interval_ms: {
|
|
107
|
-
type: 'integer',
|
|
108
|
-
minimum: 0,
|
|
109
|
-
maximum: SHELL_MONITOR_INTERVAL_MAX_MS,
|
|
110
|
-
default: SHELL_MONITOR_INTERVAL_DEFAULT_MS,
|
|
111
|
-
description: 'Periodic progress interval for tracked shell tasks in ms. Default 0 disables it; use 300000 (5m) or longer to enable.',
|
|
112
|
-
},
|
|
113
|
-
}),
|
|
114
94
|
},
|
|
115
95
|
required: ['command'],
|
|
116
96
|
additionalProperties: false,
|
|
@@ -121,7 +101,7 @@ export const BUILTIN_TOOLS = [
|
|
|
121
101
|
name: 'task',
|
|
122
102
|
title: 'Task',
|
|
123
103
|
annotations: { title: 'Task', readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: false },
|
|
124
|
-
description: 'List shell tasks, read one current output snapshot, change periodic monitoring, or cancel by task_id; completion always arrives by notification.',
|
|
104
|
+
description: 'List shell tasks, read one current output snapshot, change periodic monitoring, or cancel by task_id. Call it only when a snapshot must drive a decision; never poll in a loop — completion always arrives by notification.',
|
|
125
105
|
inputSchema: {
|
|
126
106
|
type: 'object',
|
|
127
107
|
properties: {
|
|
@@ -142,7 +122,7 @@ export const BUILTIN_TOOLS = [
|
|
|
142
122
|
name: 'grep',
|
|
143
123
|
title: 'Grep',
|
|
144
124
|
annotations: { title: 'Grep', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false, compressible: true },
|
|
145
|
-
description: 'Read-only; safe to batch in parallel. Search file contents for literal or regex matches and return contextual path:line blocks. Ripgrep-dialect regex (e.g. "log.*Error"; escape literal braces; patterns match within one line). Replaces grep/rg.',
|
|
125
|
+
description: 'Read-only; safe to batch in parallel. Search file contents for literal or regex matches and return contextual path:line blocks that are directly usable; read only the lines they omit. A wide reconnaissance pattern goes to mode:files first; context:0 when only the location is needed. Ripgrep-dialect regex (e.g. "log.*Error"; escape literal braces; patterns match within one line). Replaces grep/rg.',
|
|
146
126
|
inputSchema: {
|
|
147
127
|
type: 'object',
|
|
148
128
|
properties: {
|
|
@@ -172,7 +152,7 @@ export const BUILTIN_TOOLS = [
|
|
|
172
152
|
name: 'glob',
|
|
173
153
|
title: 'Glob',
|
|
174
154
|
annotations: { title: 'Glob', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false, compressible: true },
|
|
175
|
-
description: 'Read-only; safe to batch in parallel. Return wildcard-matching file paths under a known base directory when those paths are needed.
|
|
155
|
+
description: 'Read-only; safe to batch in parallel. Return wildcard-matching file paths under a known base directory when those paths are needed. Omit path for the current Project; if the base location is unknown, use find first. Directories never match. Newest first by default. Replaces find -name.',
|
|
176
156
|
inputSchema: {
|
|
177
157
|
type: 'object',
|
|
178
158
|
properties: {
|
|
@@ -194,7 +174,7 @@ export const BUILTIN_TOOLS = [
|
|
|
194
174
|
name: 'find',
|
|
195
175
|
title: 'Find Files',
|
|
196
176
|
annotations: { title: 'Find Files', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false, compressible: true },
|
|
197
|
-
description: 'Read-only; safe to batch in parallel. Fuzzy filename/directory path lookup when the location itself is unknown; returns paths only.',
|
|
177
|
+
description: 'Read-only; safe to batch in parallel. Fuzzy filename/directory path lookup when the location itself is unknown; returns paths only. Skip it when the path is already known or resolvable.',
|
|
198
178
|
inputSchema: {
|
|
199
179
|
type: 'object',
|
|
200
180
|
properties: {
|
|
@@ -213,7 +193,7 @@ export const BUILTIN_TOOLS = [
|
|
|
213
193
|
name: 'list',
|
|
214
194
|
title: 'List Directory',
|
|
215
195
|
annotations: { title: 'List Directory', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false, compressible: true },
|
|
216
|
-
description: "Read-only; safe to batch in parallel. Return a known directory's immediate entries (path + type) when the entry list itself is needed;
|
|
196
|
+
description: "Read-only; safe to batch in parallel. Return a known directory's immediate entries (path + type) when the entry list itself is needed; never as a prerequisite for another tool on that directory. No wildcard; meta:true adds size/mtime/mode.",
|
|
217
197
|
inputSchema: {
|
|
218
198
|
type: 'object',
|
|
219
199
|
properties: {
|
|
@@ -41,12 +41,11 @@ export const GIT_TOOL_DEF = {
|
|
|
41
41
|
openWorldHint: true,
|
|
42
42
|
compressible: true,
|
|
43
43
|
},
|
|
44
|
-
description: 'Run one Git command directly, without a shell. Shell operators and substitution are rejected. Repository mutations are serialized.
|
|
44
|
+
description: 'Run one Git command directly, without a shell. History, blame, and old commits are evidence only when the task itself is about the past; work on current code ends at status and diff. Use diff directly when changed content for a known target is required; status is the repository summary. Shell operators and substitution are rejected. Repository mutations are serialized. Successful output is compacted.',
|
|
45
45
|
inputSchema: {
|
|
46
46
|
type: 'object',
|
|
47
47
|
properties: {
|
|
48
48
|
command: { type: 'string', description: 'Full command beginning with git. Quote arguments as for a shell; shell operators are not allowed.' },
|
|
49
|
-
confirm: { type: 'boolean', description: 'Set true only when a rejected high-risk command explicitly requires it.' },
|
|
50
49
|
output_limit: { type: 'integer', minimum: 1, maximum: 500, description: 'Item/line cap. Default 50; git log defaults to 10.' },
|
|
51
50
|
},
|
|
52
51
|
required: ['command'],
|
|
@@ -251,34 +250,6 @@ function runGit(plan, argv, options = {}) {
|
|
|
251
250
|
return runProcess('git', [...plan.globalArgs, ...argv], { cwd: plan.cwd, signal: options.signal });
|
|
252
251
|
}
|
|
253
252
|
|
|
254
|
-
function destructiveReason(plan) {
|
|
255
|
-
const { operation, args } = plan;
|
|
256
|
-
// Dry-run previews mutate nothing: git either prints what WOULD happen or
|
|
257
|
-
// rejects the flag where unsupported, so the confirm gate only taxes safe
|
|
258
|
-
// inspection (2026-08-17 bench: `reflog expire --dry-run` was refused and
|
|
259
|
-
// the model paid a +56s workaround detour). `-n` short clusters count only
|
|
260
|
-
// for the subcommands where -n MEANS dry-run (clean/push/prune) — on
|
|
261
|
-
// e.g. `commit -n` it means --no-verify and must keep the gate.
|
|
262
|
-
if (args.includes('--dry-run')) return '';
|
|
263
|
-
if (['clean', 'push', 'prune'].includes(operation)
|
|
264
|
-
&& args.some((value) => /^-[a-z]*n[a-z]*$/.test(value))) return '';
|
|
265
|
-
if (operation === 'reset' && args.includes('--hard')) return 'git reset --hard';
|
|
266
|
-
if (operation === 'clean' && args.some((value) => value === '--force' || /^-[^-]*f/.test(value))) return 'git clean --force';
|
|
267
|
-
if (operation === 'gc' && args.some((value) => value === '--prune=now' || value === '--aggressive')) return `git gc ${args.find((value) => value.startsWith('--prune') || value === '--aggressive')}`;
|
|
268
|
-
if (operation === 'prune') return 'git prune';
|
|
269
|
-
if (operation === 'filter-branch') return 'git filter-branch';
|
|
270
|
-
if (operation === 'update-ref' && args.includes('-d')) return 'git update-ref -d';
|
|
271
|
-
if (operation === 'symbolic-ref' && args.includes('--delete')) return 'git symbolic-ref --delete';
|
|
272
|
-
if (operation === 'reflog' && ['delete', 'expire'].includes(actionOf(operation, args))) return `git reflog ${actionOf(operation, args)}`;
|
|
273
|
-
if (operation === 'push' && args.some((value) => value.startsWith('--force') || value.startsWith('+'))) return 'git push --force';
|
|
274
|
-
if (operation === 'branch' && args.some((value) => value === '-D' || value === '--delete-force')) return 'git branch -D';
|
|
275
|
-
if (operation === 'commit' && args.includes('--amend')) return 'git commit --amend';
|
|
276
|
-
if (operation === 'stash' && actionOf(operation, args) === 'clear') return 'git stash clear';
|
|
277
|
-
if (operation === 'worktree' && actionOf(operation, args) === 'remove' && args.includes('--force')) return 'git worktree remove --force';
|
|
278
|
-
if (['checkout', 'switch'].includes(operation) && args.some((value) => value === '-f' || value === '--force' || value === '--discard-changes')) return `git ${operation} --force`;
|
|
279
|
-
return null;
|
|
280
|
-
}
|
|
281
|
-
|
|
282
253
|
function hasOutputFormat(args) {
|
|
283
254
|
return args.some((value) => /^(?:--format|--pretty)(?:=|$)/.test(value) || value === '--oneline');
|
|
284
255
|
}
|
|
@@ -605,8 +576,6 @@ export async function executeGitTool(input, workDir, options = {}) {
|
|
|
605
576
|
}
|
|
606
577
|
const limitDefault = plan.operation === 'log' ? 10 : 50;
|
|
607
578
|
const limit = Math.min(500, Math.max(1, Number(input.output_limit) || limitDefault));
|
|
608
|
-
const reason = destructiveReason(plan);
|
|
609
|
-
if (reason && input.confirm !== true) return fail(`${reason} requires confirm:true`);
|
|
610
579
|
const signal = options?.signal || options?.abortSignal || null;
|
|
611
580
|
if (plan.operation === 'init' || plan.operation === 'clone') {
|
|
612
581
|
const target = creationTarget(plan);
|
|
@@ -51,8 +51,7 @@ test('git command tool preserves shell syntax, compacts output, and gates destru
|
|
|
51
51
|
assert.match(JSON.stringify(parseOk(await git(repo, 'count-objects -v'))), /count:/);
|
|
52
52
|
parseOk(await git(repo, 'check-ref-format refs/heads/test'));
|
|
53
53
|
assert.match(String(await git(repo, 'archive HEAD')), /^Error: git archive requires -o\/--output/);
|
|
54
|
-
|
|
55
|
-
parseOk(await git(repo, 'prune --expire=now', { confirm: true }));
|
|
54
|
+
parseOk(await git(repo, 'prune --expire=now'));
|
|
56
55
|
const shown = parseOk(await git(repo, `show ${base}`, { output_limit: 5 }));
|
|
57
56
|
assert.equal(shown.commit.oid, base);
|
|
58
57
|
assert.deepEqual(shown.diff.files, ['base.txt']);
|
|
@@ -66,8 +65,7 @@ test('git command tool preserves shell syntax, compacts output, and gates destru
|
|
|
66
65
|
const batchShow = parseOk(await git(repo, `show ${base} ${secretCommit}`, { output_limit: 5 })).commits;
|
|
67
66
|
assert.deepEqual(batchShow.map((row) => row.commit.oid), [base, secretCommit]);
|
|
68
67
|
|
|
69
|
-
|
|
70
|
-
parseOk(await git(repo, `reset --hard ${base}`, { confirm: true }));
|
|
68
|
+
parseOk(await git(repo, `reset --hard ${base}`));
|
|
71
69
|
const fsck = parseOk(await git(repo, 'fsck --full --unreachable --no-reflogs', { output_limit: 20 }));
|
|
72
70
|
assert.match(JSON.stringify(fsck), new RegExp(secretCommit));
|
|
73
71
|
|
|
@@ -75,16 +73,13 @@ test('git command tool preserves shell syntax, compacts output, and gates destru
|
|
|
75
73
|
assert.ok(reflog.entries.some((row) => row.oid === secretCommit));
|
|
76
74
|
const selectors = reflog.entries.map((row) => row.selector).filter((value, index, all) => all.indexOf(value) === index).slice(0, 2);
|
|
77
75
|
const deleteCommand = `reflog delete --rewrite ${selectors.map(quote).join(' ')}`;
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
// Dry-run previews skip the confirm gate: nothing mutates.
|
|
76
|
+
parseOk(await git(repo, deleteCommand));
|
|
77
|
+
// Dry-run previews mutate nothing and still return a normal result.
|
|
81
78
|
parseOk(await git(repo, 'reflog expire --dry-run --verbose --expire-unreachable=now --all'));
|
|
82
79
|
parseOk(await git(repo, 'prune --dry-run'));
|
|
83
80
|
parseOk(await git(repo, 'clean -nd'));
|
|
84
|
-
|
|
85
|
-
parseOk(await git(repo, '
|
|
86
|
-
assert.match(String(await git(repo, 'gc --prune=now')), /^Error: git gc --prune=now requires confirm:true/);
|
|
87
|
-
parseOk(await git(repo, 'gc --prune=now', { confirm: true }));
|
|
81
|
+
parseOk(await git(repo, 'reflog expire --expire-unreachable=now --all --rewrite'));
|
|
82
|
+
parseOk(await git(repo, 'gc --prune=now'));
|
|
88
83
|
|
|
89
84
|
renameSync(join(repo, 'base.txt'), join(repo, 'renamed.txt'));
|
|
90
85
|
parseOk(await git(repo, 'add --all'));
|
|
@@ -92,7 +87,7 @@ test('git command tool preserves shell syntax, compacts output, and gates destru
|
|
|
92
87
|
const renameText = renameDiff.patch || renameDiff.output;
|
|
93
88
|
assert.match(renameText, /rename from base\.txt/);
|
|
94
89
|
assert.match(renameText, /rename to renamed\.txt/);
|
|
95
|
-
parseOk(await git(repo, 'reset --hard HEAD'
|
|
90
|
+
parseOk(await git(repo, 'reset --hard HEAD'));
|
|
96
91
|
|
|
97
92
|
writeFileSync(join(repo, 'base.txt'), `${Array.from({ length: 120 }, (_, i) => `changed-${i}`).join('\n')}\n`);
|
|
98
93
|
const rawDiff = spawnSync('git', ['-C', repo, 'diff', '--', 'base.txt'], { encoding: 'utf8' }).stdout;
|
|
@@ -135,7 +130,6 @@ test('git command tool preserves shell syntax, compacts output, and gates destru
|
|
|
135
130
|
assert.match(JSON.stringify(parseOk(await git(repo, 'worktree list --porcelain'))), /topic/);
|
|
136
131
|
assert.match(JSON.stringify(parseOk(await git(repo, 'branch --list'))), /topic/);
|
|
137
132
|
|
|
138
|
-
assert.match(String(await git(repo, 'push --force origin HEAD')), /^Error: git push --force requires confirm:true/);
|
|
139
133
|
assert.match(String(await executeGitTool({ command: 'git status && git log' }, root)), /^Error: git command must not contain shell operators/);
|
|
140
134
|
});
|
|
141
135
|
|
|
@@ -184,10 +178,11 @@ test('git failure detail folds progress frames and keeps the fatal tail', () =>
|
|
|
184
178
|
|
|
185
179
|
test('git schema exposes only the compact shell-compatible contract', () => {
|
|
186
180
|
const properties = GIT_TOOL_DEF.inputSchema.properties;
|
|
187
|
-
assert.deepEqual(Object.keys(properties), ['command', '
|
|
181
|
+
assert.deepEqual(Object.keys(properties), ['command', 'output_limit']);
|
|
188
182
|
assert.deepEqual(GIT_TOOL_DEF.inputSchema.required, ['command']);
|
|
189
183
|
assert.equal(properties.command.minLength, undefined);
|
|
190
|
-
assert.
|
|
184
|
+
assert.doesNotMatch(GIT_TOOL_DEF.description, /confirm/i);
|
|
185
|
+
assert.match(GIT_TOOL_DEF.description, /Use diff directly when changed content for a known target is required/i);
|
|
191
186
|
assert.match(GIT_TOOL_DEF.description, /Run one Git command directly, without a shell/i);
|
|
192
187
|
assert.match(GIT_TOOL_DEF.description, /repository mutations are serialized/i);
|
|
193
188
|
});
|
|
@@ -16,7 +16,7 @@ import {
|
|
|
16
16
|
relativeGrepLine,
|
|
17
17
|
} from './search-input-helpers.mjs';
|
|
18
18
|
|
|
19
|
-
export const GREP_CONTEXT_CHAR_BUDGET_DEFAULT =
|
|
19
|
+
export const GREP_CONTEXT_CHAR_BUDGET_DEFAULT = 5_000;
|
|
20
20
|
const GREP_FOCUSED_CONTEXT_RADIUS = 12;
|
|
21
21
|
const GREP_FOCUSED_RAW_BLOCKS = 3;
|
|
22
22
|
// Anchors must stay usable as evidence without a follow-up read: keep the
|
|
@@ -3,13 +3,13 @@ export const CODE_GRAPH_TOOL_DEFS = [
|
|
|
3
3
|
name: 'code_graph',
|
|
4
4
|
title: 'Code Graph',
|
|
5
5
|
annotations: { title: 'Code Graph', readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false, compressible: false, compressibleLossless: true },
|
|
6
|
-
description: 'Read-only; safe to batch in parallel. Source-file structure, symbol relations, and flow.
|
|
6
|
+
description: 'Read-only; safe to batch in parallel. Source-file structure, symbol relations, and flow. Exact identifiers route directly to find_symbol/references/callers/callees; symbol-name keywords use symbol_search/search. Source text, literals, regex, and conceptual keywords use grep. File modes use files[]; symbol modes use symbols[]. overview and mode:symbols return file summaries. find_symbol returns declaration/body; references returns declaration/usages plus optional body; callers/callees return locations.',
|
|
7
7
|
inputSchema: {
|
|
8
8
|
type: 'object',
|
|
9
9
|
properties: {
|
|
10
10
|
mode: { type: 'string', enum: ['overview', 'imports', 'dependents', 'related', 'impact', 'symbols', 'find_symbol', 'symbol_search', 'search', 'references', 'callers', 'callees'], description: 'File modes: overview, imports, dependents, related, and impact. symbols with files[] returns an optionally filtered file outline; remaining modes use symbols[].' },
|
|
11
|
-
files: { anyOf: [{ type: 'string' }, { type: 'array', items: { type: 'string' } }], description: 'Project-relative source path(s)
|
|
12
|
-
symbols: { anyOf: [{ type: 'string' }, { type: 'array', items: { type: 'string' } }], description: 'Exact identifiers or keywords; batch in one symbols[] call
|
|
11
|
+
files: { anyOf: [{ type: 'string' }, { type: 'array', items: { type: 'string' } }], description: 'Project-relative source path(s); required by file modes, optional to scope symbol modes.' },
|
|
12
|
+
symbols: { anyOf: [{ type: 'string' }, { type: 'array', items: { type: 'string' } }], description: 'Exact identifiers or symbol-name keywords; batch in one symbols[] call; required by symbol modes, optional filter for mode:symbols.' },
|
|
13
13
|
body: { type: 'boolean', description: 'Include declaration body: find_symbol defaults true; references is opt-in.' },
|
|
14
14
|
limit: { type: 'integer', minimum: 1, maximum: 500, description: 'Maximum results; modes may enforce lower caps.' },
|
|
15
15
|
depth: { type: 'integer', minimum: 1, maximum: 5, description: 'Overview hierarchy or caller traversal depth; default 1.' },
|
|
@@ -23,7 +23,7 @@ eof_line: "*** End of File" LF
|
|
|
23
23
|
// Lark custom tool. The tiny JSON schema remains only for function-only
|
|
24
24
|
// compatibility paths; runtime knobs stay off the model surface.
|
|
25
25
|
const APPLY_PATCH_FREEFORM_DESCRIPTION =
|
|
26
|
-
'Edit files with one raw V4A patch; do not wrap it in JSON. Use one Add/Delete/Update File block per target path and multiple @@ hunks within one Update File block. Add File atomically creates the file and missing parent directories, failing without changes if the target already exists. Multi-file patches commit valid files and report rejected files separately.';
|
|
26
|
+
'Edit files with one raw V4A patch; do not wrap it in JSON. Use one Add/Delete/Update File block per target path and multiple @@ hunks within one Update File block. Add File atomically creates the file and missing parent directories, failing without changes if the target already exists; call it directly without a prior read, list, or mkdir. Use exact lines already in context; never re-open the file to build context or to verify a successful patch. Multi-file patches commit valid files and report rejected files separately.';
|
|
27
27
|
|
|
28
28
|
const APPLY_PATCH_JSON_DESCRIPTION = 'Edit files with one complete V4A patch in `patch`.';
|
|
29
29
|
|
|
@@ -217,7 +217,6 @@ export function execShellCommand({
|
|
|
217
217
|
timeoutMs,
|
|
218
218
|
abortSignal,
|
|
219
219
|
autoBackgroundMs,
|
|
220
|
-
startInBackground = false,
|
|
221
220
|
onProgress,
|
|
222
221
|
onOutputTail,
|
|
223
222
|
clientHostPid,
|
|
@@ -603,8 +602,8 @@ export function execShellCommand({
|
|
|
603
602
|
// resolve the call immediately with a 'backgrounded' result while the
|
|
604
603
|
// child keeps running, adopted into the shell-jobs registry but still
|
|
605
604
|
// owned by this CLI process:
|
|
606
|
-
// 1. the
|
|
607
|
-
//
|
|
605
|
+
// 1. the autoBackgroundMs soft foreground threshold — an EARLIER
|
|
606
|
+
// promotion before the timeout, and
|
|
608
607
|
// 2. the foreground timeout deadline (backgroundOnTimeout) — the default
|
|
609
608
|
// promote-on-timeout that replaces the old tree-kill.
|
|
610
609
|
// A capped explicit foreground timeout supplies its remaining deadline to
|
|
@@ -773,9 +772,7 @@ export function execShellCommand({
|
|
|
773
772
|
const secs = Math.max(0, Math.round(_elapsedSinceStart() / 1000));
|
|
774
773
|
const _verb = reason === 'timeout'
|
|
775
774
|
? `moved to background at timeout after ${secs}s`
|
|
776
|
-
:
|
|
777
|
-
? 'started in background'
|
|
778
|
-
: `auto-backgrounded after ${secs}s`);
|
|
775
|
+
: `auto-backgrounded after ${secs}s`;
|
|
779
776
|
resolveResult(
|
|
780
777
|
new ExecResult({
|
|
781
778
|
stdout,
|
|
@@ -889,9 +886,7 @@ export function execShellCommand({
|
|
|
889
886
|
// Arm the auto-background timer only for the genuine foreground one-shot
|
|
890
887
|
// path: a positive threshold strictly below the hard timeout, and not a
|
|
891
888
|
// trailing-`&` background command (those already detach + settle on exit).
|
|
892
|
-
if (
|
|
893
|
-
setImmediate(() => { fireAutoBackground({ reason: 'explicit' }); });
|
|
894
|
-
} else if (
|
|
889
|
+
if (
|
|
895
890
|
typeof autoBackgroundMs === 'number' &&
|
|
896
891
|
autoBackgroundMs > 0 &&
|
|
897
892
|
!_isBackground &&
|
|
@@ -58,19 +58,21 @@ test('disabled agents are stored apart from the route and survive canonicalizati
|
|
|
58
58
|
|
|
59
59
|
test('a disabled agent leaves the Lead prompt and the delegation surface', () => {
|
|
60
60
|
const { data, helpers } = fixture(['worker', 'reviewer']);
|
|
61
|
-
|
|
61
|
+
// The shipped fallback is Solo, so a delegating pack is selected explicitly.
|
|
62
|
+
const cowork = (extra = {}) => ({ workflow: { active: 'default' }, ...extra });
|
|
63
|
+
const enabled = helpers.activeWorkflowContext(cowork(), data);
|
|
62
64
|
assert.match(enabled.context, /# Available Agents/);
|
|
63
65
|
assert.match(enabled.context, /\(worker\)/);
|
|
64
66
|
assert.equal(enabled.summary.delegatesAgents, true);
|
|
65
67
|
|
|
66
|
-
const partial = helpers.activeWorkflowContext({ disabledAgents: ['worker'] }, data);
|
|
68
|
+
const partial = helpers.activeWorkflowContext(cowork({ disabledAgents: ['worker'] }), data);
|
|
67
69
|
assert.equal(partial.context.includes('(worker)'), false);
|
|
68
70
|
assert.match(partial.context, /\(reviewer\)/);
|
|
69
71
|
assert.equal(partial.summary.delegatesAgents, true);
|
|
70
72
|
|
|
71
73
|
// Nobody left to delegate to: the agent tool drops exactly as it does for a
|
|
72
74
|
// non-delegating pack.
|
|
73
|
-
const none = helpers.activeWorkflowContext({ disabledAgents: ['worker', 'reviewer'] }, data);
|
|
75
|
+
const none = helpers.activeWorkflowContext(cowork({ disabledAgents: ['worker', 'reviewer'] }), data);
|
|
74
76
|
assert.equal(none.context.includes('# Available Agents'), false);
|
|
75
77
|
assert.equal(none.summary.delegatesAgents, false);
|
|
76
78
|
assert.deepEqual(helpers.delegatableAgentIds({ disabledAgents: ['worker'] }, data), ['reviewer']);
|
|
@@ -11,21 +11,59 @@ import { DEFERRED_DEFAULT_LEAD_TOOLS } from './tool-catalog-data.mjs';
|
|
|
11
11
|
const require = createRequire(import.meta.url);
|
|
12
12
|
const { omitToolRoutes, buildSharedToolContent } = require('../lib/rules-builder.cjs');
|
|
13
13
|
|
|
14
|
+
// Tool dependency is declared by `<!-- tools: … -->` markers, so this fixture
|
|
15
|
+
// carries the markers rather than prose the builder would have to match.
|
|
14
16
|
const SAMPLE_ROUTES = [
|
|
17
|
+
'<!-- tools: web_search, web_fetch -->',
|
|
15
18
|
'# Research',
|
|
16
19
|
'',
|
|
20
|
+
'<!-- tools: web_search, web_fetch -->',
|
|
17
21
|
'- Research routes:',
|
|
22
|
+
'<!-- tools: web_search -->',
|
|
18
23
|
' current or external information discovery→`web_search`;',
|
|
24
|
+
'<!-- tools: web_fetch -->',
|
|
19
25
|
' page or documentation body retrieval from a known URL→`web_fetch`.',
|
|
26
|
+
'<!-- tools: recall, memory -->',
|
|
20
27
|
'# Memory',
|
|
21
28
|
'',
|
|
29
|
+
'<!-- tools: recall -->',
|
|
22
30
|
'- past facts recorded in prior work or sessions→`recall`',
|
|
23
31
|
' (stored history only, never current local state).',
|
|
32
|
+
'<!-- tools: memory -->',
|
|
24
33
|
'- Durable memory creation or update→`memory`; store a compact English',
|
|
25
34
|
' statement.',
|
|
35
|
+
'<!-- tools: memory -->',
|
|
26
36
|
'- Use judgment to decide whether a durable memory should be stored.',
|
|
27
37
|
].join('\n');
|
|
28
38
|
|
|
39
|
+
test('omitToolRoutes strips markers and keeps every clause when nothing is omitted', () => {
|
|
40
|
+
const kept = omitToolRoutes(SAMPLE_ROUTES, []);
|
|
41
|
+
assert.equal(kept.includes('<!--'), false);
|
|
42
|
+
assert.equal(kept.includes('`web_search`'), true);
|
|
43
|
+
assert.equal(kept.includes('`web_fetch`'), true);
|
|
44
|
+
assert.equal(kept.includes('`recall`'), true);
|
|
45
|
+
assert.equal(kept.includes('`memory`'), true);
|
|
46
|
+
assert.equal(kept.includes('# Research'), true);
|
|
47
|
+
assert.equal(kept.includes('# Memory'), true);
|
|
48
|
+
// The continuation line stays attached to the clause it belongs to.
|
|
49
|
+
assert.match(kept, /→`recall`\n\s+\(stored history only, never current local state\)\./);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
test('omitToolRoutes drops a clause only when every tool it declares is omitted', () => {
|
|
53
|
+
const noSearchOnly = omitToolRoutes(SAMPLE_ROUTES, ['web_search']);
|
|
54
|
+
assert.equal(noSearchOnly.includes('`web_search`'), false);
|
|
55
|
+
assert.equal(noSearchOnly.includes('`web_fetch`'), true);
|
|
56
|
+
// The section and its lead-in survive while one route remains.
|
|
57
|
+
assert.equal(noSearchOnly.includes('# Research'), true);
|
|
58
|
+
assert.equal(noSearchOnly.includes('Research routes:'), true);
|
|
59
|
+
|
|
60
|
+
const noMemoryOnly = omitToolRoutes(SAMPLE_ROUTES, ['memory']);
|
|
61
|
+
assert.equal(noMemoryOnly.includes('`recall`'), true);
|
|
62
|
+
assert.equal(noMemoryOnly.includes('`memory`'), false);
|
|
63
|
+
// Guidance that only makes sense with the memory tool goes with it.
|
|
64
|
+
assert.equal(noMemoryOnly.includes('Use judgment'), false);
|
|
65
|
+
});
|
|
66
|
+
|
|
29
67
|
test('omitToolRoutes drops web search and memory clauses independently', () => {
|
|
30
68
|
const noSearch = omitToolRoutes(SAMPLE_ROUTES, ['web_search', 'web_fetch']);
|
|
31
69
|
assert.equal(noSearch.includes('`web_search`'), false);
|
|
@@ -72,26 +110,41 @@ test('shared tool rules keep workflow and shell-boundary anchors', () => {
|
|
|
72
110
|
// intentionally changes.
|
|
73
111
|
const full = buildSharedToolContent({ PLUGIN_ROOT: join(process.cwd(), 'src') });
|
|
74
112
|
assert.match(full, /Minimize tool turns through maximal useful parallelism/i);
|
|
75
|
-
assert.match(full, /
|
|
76
|
-
assert.match(full, /
|
|
77
|
-
assert.match(full, /
|
|
113
|
+
assert.match(full, /In each round, issue every necessary non-overlapping call whose inputs are\s+already known/i);
|
|
114
|
+
assert.match(full, /Investigate, build, and verify only what the requested outcome requires, at\s+the level it requires/i);
|
|
115
|
+
assert.match(full, /A check runs at the strictness the task requires; never raise a tool's own\s+severity beyond it/i);
|
|
116
|
+
assert.match(full, /Cost is counted in\s+rounds, not calls/i);
|
|
117
|
+
assert.match(full, /Plan the fewest evidence-complete dependent\s+rounds first/i);
|
|
118
|
+
assert.match(full, /defer a call only when its target or arguments require an\s+earlier result/i);
|
|
119
|
+
assert.match(full, /Route each remaining evidence facet once to its primary owner, preferring the\s+operation that directly returns the evidence needed for the next decision/i);
|
|
120
|
+
assert.match(full, /summary, overview, or enumeration is not a prerequisite when that operation's\s+complete inputs are already known/i);
|
|
121
|
+
assert.match(full, /apply\s+one analysis to many targets as one parameterized call/i);
|
|
78
122
|
assert.match(full, /use Execution when the information can only be produced by running a program\s+or observing runtime state/i);
|
|
79
123
|
assert.match(full, /Evidence or artifacts available only through program execution, calculation,\s+data transformation, generated output, or unsupported-format decoding→`shell`/i);
|
|
80
|
-
assert.match(full, /
|
|
124
|
+
assert.match(full, /an already-open shell is never a routing reason/i);
|
|
125
|
+
assert.match(full, /Route the missing evidence to its primary owner/i);
|
|
126
|
+
assert.match(full, /repository state, history, or diff→`git`/i);
|
|
127
|
+
assert.match(full, /Ownership is exclusive: each evidence type has one owner/i);
|
|
128
|
+
assert.match(full, /a\s+successful owner result closes that facet/i);
|
|
129
|
+
assert.match(full, /Blocking checks cover only essential integrity, security, compatibility, and\s+buildability invariants/i);
|
|
130
|
+
assert.match(full, /environment variable or the home directory are\s+resolved locations/i);
|
|
81
131
|
assert.match(full, /Use read-only means for inspection; never mutate to clear an obstacle or\s+unexpected state/i);
|
|
82
|
-
assert.match(full, /
|
|
83
|
-
assert.match(full, /
|
|
84
|
-
assert.match(full, /
|
|
85
|
-
assert.match(full, /
|
|
132
|
+
assert.match(full, /literal, regex, or text location→`grep`;\s+known-file content, range, or image→`read`/i);
|
|
133
|
+
assert.match(full, /is never re-found, re-derived, or\s+re-verified at any granularity/i);
|
|
134
|
+
assert.match(full, /Mine each returned result fully before opening the next round/i);
|
|
135
|
+
assert.match(full, /Evidence that determines the answer, edit, or deliverable ends retrieval/i);
|
|
86
136
|
assert.match(full, /Enter Verification only after all planned work is complete/i);
|
|
87
137
|
assert.match(full, /use an umbrella suite only when the user explicitly requests it\s+or a documented project or release process requires it/i);
|
|
88
138
|
assert.match(full, /If verification fails, collect all failures, leave Verification/i);
|
|
89
|
-
assert.
|
|
90
|
-
assert.
|
|
91
|
-
assert.
|
|
139
|
+
assert.match(full, /A successful verification closes the task unless later changes affect it/i);
|
|
140
|
+
assert.doesNotMatch(full, /affected failed checks once/i);
|
|
141
|
+
assert.match(full, /Every repository mutation→`git`/i);
|
|
142
|
+
assert.doesNotMatch(full, /always batch safely in parallel/i);
|
|
143
|
+
assert.match(full, /A required new file is created directly: Add File is itself the atomic\s+absence check/i);
|
|
92
144
|
assert.match(full, /Source: use exact current target text from any visible evidence/i);
|
|
93
145
|
assert.match(full, /Placement: with `edit`, use an exact unique target string/i);
|
|
94
146
|
assert.match(full, /with `apply_patch`, use exact unchanged context/i);
|
|
147
|
+
assert.match(full, /Apply all determined changes in the fewest safe calls the active tool\s+supports/i);
|
|
95
148
|
assert.match(full, /Batch scope: never split one file across concurrent edit calls/i);
|
|
96
149
|
assert.match(full, /Commit, push, release, and deployment happen only on the user's explicit\s+request/i);
|
|
97
150
|
assert.match(full, /past facts recorded in prior work or sessions→`recall`/i);
|
|
@@ -29,7 +29,10 @@ const STARTER_AGENT_ORDER = new Map([
|
|
|
29
29
|
['heavy-worker', 1],
|
|
30
30
|
['reviewer', 2],
|
|
31
31
|
]);
|
|
32
|
-
|
|
32
|
+
// Fallback workflow for a config with no explicit selection, for an unknown
|
|
33
|
+
// id, and for the pack reset after a delete. Solo is the shipped default
|
|
34
|
+
// working mode; the cowork pack (directory id `default`) is opt-in.
|
|
35
|
+
export const DEFAULT_WORKFLOW_ID = 'solo';
|
|
33
36
|
|
|
34
37
|
const WEB_SEARCH_CAPABLE_PROVIDERS = new Set([
|
|
35
38
|
'openai-oauth', 'openai', 'grok-oauth', 'xai', 'gemini', 'anthropic', 'anthropic-oauth',
|
|
@@ -245,8 +248,10 @@ export function createWorkflowHelpers({ rootDir, dataDir, readMarkdownDocument,
|
|
|
245
248
|
// Solo leads (user decision: solo is the default working mode), the
|
|
246
249
|
// cowork pack (built-in id `default`) second, then customs alphabetically
|
|
247
250
|
// — every picker (TUI, desktop sidebar, onboarding) shares this order.
|
|
251
|
+
// The cowork id stays literal here: DEFAULT_WORKFLOW_ID now means "the
|
|
252
|
+
// fallback pack" (solo), not "the pack whose directory is `default`".
|
|
248
253
|
const weight = (pack) => pack.id === 'solo' ? 0
|
|
249
|
-
: (pack.id ===
|
|
254
|
+
: (pack.id === 'default' || pack.id === 'cowork') ? 1 : 2;
|
|
250
255
|
return [...byId.values()].sort((a, b) =>
|
|
251
256
|
(weight(a) - weight(b)) || a.name.localeCompare(b.name));
|
|
252
257
|
}
|
|
@@ -277,7 +282,7 @@ export function createWorkflowHelpers({ rootDir, dataDir, readMarkdownDocument,
|
|
|
277
282
|
const id = normalizeWorkflowId(pack?.id, DEFAULT_WORKFLOW_ID);
|
|
278
283
|
return {
|
|
279
284
|
id,
|
|
280
|
-
name: clean(pack?.name) || (id ===
|
|
285
|
+
name: clean(pack?.name) || (id === 'default' ? 'Default' : id),
|
|
281
286
|
description: clean(pack?.description),
|
|
282
287
|
source: clean(pack?.source),
|
|
283
288
|
// Delegation surface field: the session stores this summary as
|
package/src/tui/session/turn.mjs
CHANGED
|
@@ -972,6 +972,11 @@ export function createRunTurn(bag) {
|
|
|
972
972
|
// computed this number.
|
|
973
973
|
onContextPressure: (info) => {
|
|
974
974
|
if (!markTurnProgress('context-pressure')) return;
|
|
975
|
+
// This direct pressure publication exists only to make the gauge hit
|
|
976
|
+
// 100% on the exact frame auto-compaction starts. Routine pre-send
|
|
977
|
+
// checks stay on the canonical contextStatus path instead of
|
|
978
|
+
// competing with its provider-aligned value.
|
|
979
|
+
if (info?.willCompact !== true) return;
|
|
975
980
|
const used = Math.max(0, Number(info?.usedTokens) || 0);
|
|
976
981
|
if (!used) return;
|
|
977
982
|
set({
|
|
@@ -10,11 +10,12 @@ Consult the user and build the plan together. Before the user explicitly
|
|
|
10
10
|
approves the latest plan, work is read-only investigation and planning — no
|
|
11
11
|
edits, state mutation, or delegation. A new or changed request resets
|
|
12
12
|
planning; a scope change requires fresh approval. Explicit read-only requests
|
|
13
|
-
proceed immediately; approval precedes edits, state mutation,
|
|
14
|
-
|
|
13
|
+
proceed immediately; approval precedes edits, state mutation, and delegation,
|
|
14
|
+
and on approval all in-scope work completes without reapproval. Ask the user
|
|
15
|
+
only for decisions.
|
|
15
16
|
|
|
16
|
-
|
|
17
|
-
|
|
17
|
+
Lead delegates maximally: one suitable agent per independent scope,
|
|
18
|
+
all spawned in one turn;
|
|
18
19
|
only a scope that depends on another's output waits. Split the plan into as
|
|
19
20
|
many scopes as possible: disjoint file/module sets are independent; merge only
|
|
20
21
|
on a true output dependency. Prefer parallel scopes over sequential slices in
|
|
@@ -11,11 +11,9 @@ Consult the user and build the plan together. Before the user explicitly
|
|
|
11
11
|
approves the latest plan, work is read-only investigation and planning — no
|
|
12
12
|
edits, state mutation, or delegation. A new or changed request resets
|
|
13
13
|
planning; a scope change requires fresh approval. Explicit read-only requests
|
|
14
|
-
proceed immediately; approval precedes edits
|
|
15
|
-
Ask the user only for
|
|
16
|
-
|
|
17
|
-
On approval, complete all in-scope work without reapproval. Lead executes
|
|
18
|
-
every scope itself — never spawn, send, or delegate to agents.
|
|
14
|
+
proceed immediately; approval precedes edits and state mutation, and on
|
|
15
|
+
approval all in-scope work completes without reapproval. Ask the user only for
|
|
16
|
+
decisions. Lead executes every scope itself.
|
|
19
17
|
|
|
20
18
|
Report the result against the approved plan. Build happens only on an explicit
|
|
21
19
|
user request.
|