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.
Files changed (30) hide show
  1. package/package.json +1 -1
  2. package/src/lib/rules-builder.cjs +44 -15
  3. package/src/rules/lead/lead-brief.md +7 -8
  4. package/src/rules/shared/10-tool-workflow.md +28 -8
  5. package/src/rules/shared/20-research.md +4 -0
  6. package/src/rules/shared/30-exploration.md +23 -35
  7. package/src/rules/shared/40-editing.md +4 -0
  8. package/src/rules/shared/50-execution.md +1 -0
  9. package/src/rules/shared/60-verification.md +9 -0
  10. package/src/rules/shared/70-delivery.md +1 -2
  11. package/src/rules/shared/80-memory.md +6 -2
  12. package/src/runtime/agent/orchestrator/providers/lib/grok-tool-schema.mjs +22 -1
  13. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +7 -2
  14. package/src/runtime/agent/orchestrator/session/evidence-union.test.mjs +1 -1
  15. package/src/runtime/agent/orchestrator/session/image-strip-recovery.test.mjs +61 -0
  16. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +10 -10
  17. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +9 -19
  18. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +17 -37
  19. package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +1 -32
  20. package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.test.mjs +10 -15
  21. package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-context-expander.mjs +1 -1
  22. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +3 -3
  23. package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +1 -1
  24. package/src/runtime/agent/orchestrator/tools/shell-command.mjs +4 -9
  25. package/src/session-runtime/agent-disable.test.mjs +5 -3
  26. package/src/session-runtime/tool-policy-surface.test.mjs +64 -11
  27. package/src/session-runtime/workflow.mjs +8 -3
  28. package/src/tui/session/turn.mjs +5 -0
  29. package/src/workflows/default/WORKFLOW.md +5 -4
  30. package/src/workflows/solo/WORKFLOW.md +3 -5
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mixdog",
3
- "version": "0.9.143",
3
+ "version": "0.9.144",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Standalone mixdog coding-agent CLI/TUI workspace.",
@@ -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
- const WEB_SEARCH_ROUTE_RE = /^[ \t]*current or external information discovery→`web_search`;[ \t]*\r?\n?/gm;
206
- const WEB_FETCH_ROUTE_RE = /^[ \t]*page or documentation body retrieval from a known URL→`web_fetch`\.[ \t]*\r?\n?/gm;
207
- const RECALL_ROUTE_RE = /^-[ \t]*past facts recorded in prior work or sessions→`recall`[ \t]*\r?\n[ \t]*\(stored history only, never current local state\)\.[ \t]*\r?\n?/gm;
208
- const MEMORY_ROUTE_RE = /^-[ \t]*Durable memory creation or update→`memory`; store a compact English[ \t]*\r?\n[ \t]*statement\.[\s\S]*$/m;
209
- const EMPTY_RESEARCH_RE = /^# Research[ \t]*\r?\n(?:[ \t]*\r?\n)*-[ \t]*Research routes:[ \t]*\r?\n?/gm;
210
- const EMPTY_MEMORY_RE = /^# Memory[ \t]*\r?\n(?:[ \t]*\r?\n)*/gm;
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
- let out = String(text || '');
220
- if (deny.has('web_search')) out = out.replace(WEB_SEARCH_ROUTE_RE, '');
221
- if (deny.has('web_fetch')) out = out.replace(WEB_FETCH_ROUTE_RE, '');
222
- if (deny.has('recall')) out = out.replace(RECALL_ROUTE_RE, '');
223
- if (deny.has('memory')) out = out.replace(MEMORY_ROUTE_RE, '');
224
- if (deny.has('web_search') && deny.has('web_fetch')) {
225
- out = out.replace(EMPTY_RESEARCH_RE, '');
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
- if (deny.has('recall') && deny.has('memory')) out = out.replace(EMPTY_MEMORY_RE, '');
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
- - Minimum chars, maximum info: one-line fragments. Every role's `Task:` is
4
- mandatory and lossless build it from the original request and the official
5
- spec/test acceptance criteria, preserving intent, required and forbidden
6
- outcomes, completion/stop boundary, user-supplied exact targets, and exact
7
- replacements/outputs. Never infer exactness from task name, file count, or
8
- difficulty.
9
- - Omit role-known rules, repeated context/facts, and padding; split scope
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
- - Minimize tool turns through maximal useful parallelism: in each turn, issue
4
- every necessary non-overlapping call whose inputs are already known.
5
- - Defer a call only when its inputs depend on an earlier result; never add
6
- duplicate or irrelevant calls merely to increase fanout.
7
- - Apply one analysis to many targets as one parameterized call when supported,
8
- not one call per target.
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. Stop when it is already known or sufficiently
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
- - Local Project exploration routes:
6
- unknown file/directory location, paths only needed→`find`;
7
- wildcard/recursive paths→`glob` (including known-root unknown descendants);
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
- exact symbol, body, or relation→`code_graph`
10
- (identifier declarations/usages→`code_graph`; literal values/strings→`grep`);
11
- literal/regex pattern search within file contents→`grep`;
12
- content or an anchored line range from a known file when pattern search is
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
- - Before choosing an implementation, inspect only the nearest relevant code,
27
- configuration, and established pattern needed to verify local conventions or
28
- dependency availability.
29
-
30
- - Requirements define what must be true; evidence establishes what is true.
31
- Never use one as the other. Treat supplied target locations as resolved;
32
- access them directly without locator searches. Before deciding how to parse,
33
- count, transform, or summarize files whose format has not been inspected,
34
- inspect the original content itself. Within the current project, pass project-relative
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.
@@ -2,4 +2,5 @@
2
2
 
3
3
  - Evidence or artifacts available only through program execution, calculation,
4
4
  data transformation, generated output, or unsupported-format decoding→`shell`;
5
+ an already-open shell is never a routing reason.
5
6
 
@@ -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
 
@@ -2,5 +2,4 @@
2
2
 
3
3
  - Commit, push, release, and deployment happen only on the user's explicit
4
4
  request.
5
- - Repository state or history explicitly asked about, and every repository
6
- mutation→`git`; never part of exploration batching.
5
+ - Every repository mutation→`git`.
@@ -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 provide an explicit Project slug for another named Project. `*` is
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
- return normalizeGrokPropertySchema({ ...first, ...siblings });
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
- let _fixedProviderToolSurface = sessionRef?._providerToolSurfaceSnapshot || null;
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}'", confirm: true }],
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
- const READ_GUARD_DEFAULT_LIMIT = 1000;
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', 'run_in_background', 'monitor_interval_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, timeout_ms, run_in_background, and monitor_interval_ms`;
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
- // 15 s coordination budget is promoted to a tracked background task.
69
- // Raised from 10 s (2026-08-15): repo verification units (node --test +
70
- // tool-smoke) cluster at 10.7-11.2 s, so every routine check crossed the
71
- // budget by a hair and cost a notification round-trip.
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 15 s coordination budget promotes it without shortening its
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
- // A run_in_background start never holds the caller, so the cap IS its
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 15 s a still-running command becomes a tracked background task and
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: monitorIntervalMs,
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
- }, { monitorIntervalMs });
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.). Descriptions carry the tool CONTRACT
5
- // only (behavior + argument shapes); usage policy lives in rules/shared/*.md.
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/heredocs/echo/Set-Content.'
21
- : 'Use read, NOT cat/head/tail; list, NOT ls; find/glob, NOT find; grep, NOT grep/rg; edit/apply_patch, NOT sed/awk/heredocs/echo.';
22
- // When background tasks are disabled for this process, drop the
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; not a JSON array or annotated path. A glob (e.g. "logs/*.log") fans out to per-file results (cap 10, newest first); literal-named files win over expansion.',
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 1000.',
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. ${_shellToolRouting} ${_shellBackgroundDisabled ? 'Commands run in the foreground until completion.' : 'Commands use a 15s foreground window by default—not a timeout. Still-running work continues as a tracked task_id and completes by notification. Periodic monitoring is off by default; set monitor_interval_ms to 300000 (5m) or longer. Use run_in_background for known long-running commands or intentional servers/watchers, task monitor to change or disable monitoring, task read for an extra current snapshot, and never poll in a loop.'}`,
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. Directories never match; enumerate them with list. Omit path for the current Project; if the base location is unknown, use find first. Newest first by default. Replaces find -name.',
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; not a prerequisite for another tool on that directory — tree walks go to glob, name hunts to find. No wildcard; meta:true adds size/mtime/mode.",
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. High-risk destructive commands require confirm:true. Successful output is compacted.',
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
- assert.match(String(await git(repo, 'prune')), /^Error: git prune requires confirm:true/);
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
- assert.match(String(await git(repo, `reset --hard ${base}`)), /^Error: git reset --hard requires confirm:true/);
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
- assert.match(String(await git(repo, deleteCommand)), /^Error: git reflog delete requires confirm:true/);
79
- parseOk(await git(repo, deleteCommand, { confirm: true }));
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
- assert.match(String(await git(repo, 'reflog expire --expire-unreachable=now --all --rewrite')), /^Error: git reflog expire requires confirm:true/);
85
- parseOk(await git(repo, 'reflog expire --expire-unreachable=now --all --rewrite', { confirm: true }));
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', { confirm: true }));
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', 'confirm', 'output_limit']);
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.match(properties.confirm.description, /only when a rejected high-risk command/i);
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 = 10_000;
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. File modes use files[]; symbol modes use symbols[]; symbols may combine both to filter a file outline. Exact identifiers use find_symbol/references/callers/callees; keywords use symbol_search/search. find_symbol returns declaration/body; references returns declaration/usages plus optional body; callers/callees return locations.',
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). Required for imports, dependents, related, impact, and file-outline symbols; optional for overview or to scope symbol modes.' },
12
- symbols: { anyOf: [{ type: 'string' }, { type: 'array', items: { type: 'string' } }], description: 'Exact identifiers or keywords; batch in one symbols[] call. Required by lookup and relation modes; optional filters for mode:symbols.' },
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 optional autoBackgroundMs soft threshold (MIXDOG_SHELL_AUTO_
607
- // BACKGROUND_MS opt-in) — an EARLIER promotion before the timeout, and
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
- : (reason === 'explicit'
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 (startInBackground && !_isBackground) {
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
- const enabled = helpers.activeWorkflowContext({}, data);
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, /in each turn, issue\s+every necessary non-overlapping call whose inputs are already known/i);
76
- assert.match(full, /Defer a call only when its inputs depend on an earlier result/i);
77
- assert.match(full, /Apply one analysis to many targets as one parameterized call/i);
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, /environment variable or the home directory\s+are resolved locations/i);
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, /batching never licenses a guessed `glob\.path`/i);
83
- assert.match(full, /unknown location `find` first/i);
84
- assert.match(full, /inspect only the nearest relevant code,\s+configuration, and established pattern needed to verify local conventions or\s+dependency availability/i);
85
- assert.match(full, /Do not re-read content already returned by any tool or reopen a successfully\s+edited file solely to confirm the edit/i);
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.doesNotMatch(full, /passed checks|affected failed checks once/i);
90
- assert.match(full, /Repository state or history explicitly asked about, and every repository\s+mutation→`git`; never part of exploration batching/i);
91
- assert.doesNotMatch(full, /read-only `git`/i);
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
- export const DEFAULT_WORKFLOW_ID = 'default';
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 === DEFAULT_WORKFLOW_ID || pack.id === 'cowork') ? 1 : 2;
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 === DEFAULT_WORKFLOW_ID ? 'Default' : 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
@@ -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, or delegation.
14
- Ask the user only for decisions.
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
- On approval, complete all in-scope work without reapproval. Lead delegates
17
- maximally: one suitable agent per independent scope, all spawned in one turn;
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, state mutation, or delegation.
15
- Ask the user only for decisions.
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.