mixdog 0.9.18 → 0.9.19

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. package/package.json +3 -2
  2. package/scripts/output-style-smoke.mjs +2 -2
  3. package/scripts/recall-bench-cases.json +11 -0
  4. package/scripts/recall-bench.mjs +91 -2
  5. package/scripts/tool-efficiency-diag.mjs +4 -1
  6. package/scripts/tool-smoke.mjs +101 -27
  7. package/src/agents/debugger/AGENT.md +3 -3
  8. package/src/agents/heavy-worker/AGENT.md +7 -10
  9. package/src/agents/maintainer/AGENT.md +1 -2
  10. package/src/agents/reviewer/AGENT.md +1 -2
  11. package/src/agents/worker/AGENT.md +5 -8
  12. package/src/defaults/agents.json +3 -0
  13. package/src/mixdog-session-runtime.mjs +23 -6
  14. package/src/rules/agent/00-core.md +4 -3
  15. package/src/rules/agent/30-explorer.md +53 -22
  16. package/src/rules/lead/lead-tool.md +3 -2
  17. package/src/rules/shared/01-tool.md +24 -29
  18. package/src/runtime/agent/orchestrator/providers/anthropic-oauth.mjs +1 -1
  19. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +1 -1
  20. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +24 -3
  21. package/src/runtime/agent/orchestrator/providers/retry-classifier.mjs +3 -3
  22. package/src/runtime/agent/orchestrator/session/context-utils.mjs +2 -2
  23. package/src/runtime/agent/orchestrator/session/loop/steering-ladder.mjs +250 -35
  24. package/src/runtime/agent/orchestrator/session/manager/tool-resolution.mjs +6 -4
  25. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +198 -6
  26. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +10 -2
  27. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +13 -15
  28. package/src/runtime/agent/orchestrator/tools/builtin/read-batch.mjs +6 -1
  29. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +45 -3
  30. package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +5 -2
  31. package/src/runtime/agent/orchestrator/tools/builtin/search-tool.mjs +195 -3
  32. package/src/runtime/agent/orchestrator/tools/builtin.mjs +17 -1
  33. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +38 -0
  34. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +4 -4
  35. package/src/runtime/agent/orchestrator/tools/shell-state.mjs +1 -1
  36. package/src/runtime/channels/backends/discord-gateway.mjs +14 -1
  37. package/src/runtime/channels/backends/discord.mjs +43 -1
  38. package/src/runtime/channels/index.mjs +145 -58
  39. package/src/runtime/channels/lib/inbound-routing.mjs +19 -12
  40. package/src/runtime/channels/lib/memory-client.mjs +32 -14
  41. package/src/runtime/channels/lib/output-forwarder.mjs +38 -7
  42. package/src/runtime/channels/lib/owner-heartbeat.mjs +13 -4
  43. package/src/runtime/channels/lib/runtime-paths.mjs +35 -1
  44. package/src/runtime/channels/lib/tool-dispatch.mjs +17 -7
  45. package/src/runtime/memory/lib/memory-recall-scope-filter.mjs +8 -2
  46. package/src/runtime/memory/lib/memory-recall-store.mjs +182 -9
  47. package/src/runtime/memory/lib/query-handlers.mjs +36 -4
  48. package/src/runtime/memory/lib/recall-format.mjs +106 -6
  49. package/src/runtime/memory/lib/session-ingest.mjs +1 -1
  50. package/src/runtime/shared/atomic-file.mjs +10 -4
  51. package/src/runtime/shared/background-tasks.mjs +4 -2
  52. package/src/runtime/shared/tool-execution-contract.mjs +1 -1
  53. package/src/runtime/shared/tool-result-summary.mjs +1 -1
  54. package/src/runtime/shared/tool-surface.mjs +30 -1
  55. package/src/session-runtime/config-lifecycle.mjs +1 -1
  56. package/src/session-runtime/cwd-plugins.mjs +46 -3
  57. package/src/session-runtime/mcp-glue.mjs +24 -3
  58. package/src/session-runtime/output-styles.mjs +44 -10
  59. package/src/session-runtime/workflow.mjs +16 -1
  60. package/src/standalone/channel-worker.mjs +74 -5
  61. package/src/standalone/explore-tool.mjs +1 -1
  62. package/src/tui/App.jsx +57 -77
  63. package/src/tui/app/channel-pickers.mjs +45 -0
  64. package/src/tui/app/slash-commands.mjs +0 -1
  65. package/src/tui/app/slash-dispatch.mjs +0 -16
  66. package/src/tui/app/transcript-window.mjs +44 -1
  67. package/src/tui/app/use-mouse-input.mjs +9 -2
  68. package/src/tui/app/use-prompt-handlers.mjs +7 -94
  69. package/src/tui/app/use-transcript-scroll.mjs +5 -1
  70. package/src/tui/app/use-transcript-window.mjs +65 -5
  71. package/src/tui/components/PromptInput.jsx +33 -64
  72. package/src/tui/components/ToolExecution.jsx +2 -2
  73. package/src/tui/dist/index.mjs +5330 -5443
  74. package/src/tui/engine.mjs +49 -10
  75. package/src/tui/lib/voice-setup.mjs +166 -0
  76. package/src/tui/paste-attachments.mjs +12 -5
  77. package/src/tui/prompt-history-store.mjs +125 -12
  78. package/scripts/bench/cache-probe-tasks.json +0 -8
  79. package/scripts/bench/lead-review-tasks-r3.json +0 -20
  80. package/scripts/bench/lead-review-tasks.json +0 -20
  81. package/scripts/bench/r4-mixed-tasks.json +0 -20
  82. package/scripts/bench/r5-orchestrated-task.json +0 -7
  83. package/scripts/bench/review-tasks.json +0 -20
  84. package/scripts/bench/round-codex.json +0 -114
  85. package/scripts/bench/round-mixdog-lead-r3.json +0 -269
  86. package/scripts/bench/round-mixdog-lead.json +0 -269
  87. package/scripts/bench/round-mixdog.json +0 -126
  88. package/scripts/bench/round-r10-bigsample.json +0 -679
  89. package/scripts/bench/round-r11-codexalign.json +0 -257
  90. package/scripts/bench/round-r13-clientmeta.json +0 -464
  91. package/scripts/bench/round-r14-betafeatures.json +0 -466
  92. package/scripts/bench/round-r15-fulldefault.json +0 -462
  93. package/scripts/bench/round-r16-sessionid.json +0 -466
  94. package/scripts/bench/round-r17-wirebytes.json +0 -456
  95. package/scripts/bench/round-r18-prewarm.json +0 -468
  96. package/scripts/bench/round-r19-clean.json +0 -472
  97. package/scripts/bench/round-r20-prewarm-clean.json +0 -475
  98. package/scripts/bench/round-r21-delta-retry.json +0 -473
  99. package/scripts/bench/round-r22-full-probe.json +0 -693
  100. package/scripts/bench/round-r23-itemprobe.json +0 -701
  101. package/scripts/bench/round-r24-shapefix.json +0 -677
  102. package/scripts/bench/round-r25-serial.json +0 -464
  103. package/scripts/bench/round-r26-parallel3.json +0 -671
  104. package/scripts/bench/round-r27-parallel10.json +0 -894
  105. package/scripts/bench/round-r28-parallel10-stagger.json +0 -882
  106. package/scripts/bench/round-r29-parallel10-stagger166.json +0 -886
  107. package/scripts/bench/round-r30-instid.json +0 -253
  108. package/scripts/bench/round-r31-upgradeprobe.json +0 -256
  109. package/scripts/bench/round-r32-vs-codex-lead.json +0 -254
  110. package/scripts/bench/round-r33-vs-codex-codex.json +0 -115
  111. package/scripts/bench/round-r34-orchestrated.json +0 -120
  112. package/scripts/bench/round-r35-orchestrated-codex.json +0 -61
  113. package/scripts/bench/round-r36-orchestrated-capped.json +0 -128
  114. package/scripts/bench/round-r4-codex.json +0 -114
  115. package/scripts/bench/round-r4-mixed.json +0 -225
  116. package/scripts/bench/round-r5-gpt-lead.json +0 -259
  117. package/scripts/bench/round-r6-codex.json +0 -114
  118. package/scripts/bench/round-r6-solo.json +0 -257
  119. package/scripts/bench/round-r7-full.json +0 -254
  120. package/scripts/bench/round-r8-fulldefault.json +0 -255
  121. package/src/tui/components/prompt-input/voice-indicator.mjs +0 -39
  122. package/src/tui/lib/voice-recorder.mjs +0 -469
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mixdog",
3
- "version": "0.9.18",
3
+ "version": "0.9.19",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Standalone mixdog coding-agent CLI/TUI workspace.",
@@ -26,7 +26,8 @@
26
26
  "files": [
27
27
  "README.md",
28
28
  "scripts/",
29
- "!scripts/bench/cache-hit-*.json",
29
+ "!scripts/bench/",
30
+ "!scripts/recall-bench-*.txt",
30
31
  "src/",
31
32
  "vendor/"
32
33
  ],
@@ -97,7 +97,7 @@ try {
97
97
 
98
98
  mkdirSync(join(dataDir, 'output-styles'), { recursive: true });
99
99
  writeFileSync(join(dataDir, 'mixdog-config.json'), JSON.stringify({
100
- agent: { profile: { title: '재영님', language: 'system' } },
100
+ agent: { profile: { title: '홍길동님', language: 'system' } },
101
101
  outputStyle: 'custom-smoke',
102
102
  }));
103
103
  writeFileSync(join(dataDir, 'output-styles', 'custom-smoke.md'), '---\nname: custom-smoke\n---\n\n# Custom Output Style\n\ncustom smoke style\n');
@@ -105,7 +105,7 @@ try {
105
105
  assert(customRules.includes('# Custom Output Style'), 'configured outputStyle must select custom style');
106
106
  assert(!customRules.includes('Mixdog default — the most detailed of the three styles'), 'custom outputStyle should not append default style');
107
107
  const profileMeta = rulesBuilder.buildLeadMetaContent({ PLUGIN_ROOT: join(root, 'src'), DATA_DIR: dataDir });
108
- assert(profileMeta.includes('Use "재영님" when directly addressing the user'), 'profile title must inject into Lead BP3 meta');
108
+ assert(profileMeta.includes('Use "홍길동님" when directly addressing the user'), 'profile title must inject into Lead BP3 meta');
109
109
  assert(profileMeta.includes('do not repeat it in routine progress updates or pre-tool preambles'), 'profile title must not encourage title in preambles');
110
110
  assert(/Default user-facing response language from system locale/.test(profileMeta), 'system profile language must resolve from system locale');
111
111
  assert(profileMeta.includes('pre-tool preambles (even single-line)'), 'profile language must cover pre-tool preambles');
@@ -0,0 +1,11 @@
1
+ [
2
+ { "id": "browse-last", "label": "query-less recent browse (period=last)", "args": { "period": "last", "limit": 10 }, "expect": "browse" },
3
+ { "id": "period-24h", "label": "period window 24h", "args": { "period": "24h", "limit": 10 }, "expect": "browse" },
4
+ { "id": "period-7d", "label": "period window 7d", "args": { "period": "7d", "limit": 10 }, "expect": "browse" },
5
+ { "id": "scope-all", "label": "all-scope query", "args": { "query": "recall", "projectScope": "all", "limit": 10 }, "expect": "results" },
6
+ { "id": "recent-dryrun-delta", "label": "recent-work topical: dryrun delta sum", "args": { "query": "드라이런 델타 합산", "limit": 10 }, "expect": { "kind": "results", "topNContains": ["드라이런"], "topN": 5 } },
7
+ { "id": "recent-remote-singleton", "label": "recent-work topical: remote singleton seat", "args": { "query": "remote 싱글톤 좌석", "limit": 10 }, "expect": { "kind": "results", "topNContains": ["싱글톤"], "topN": 5 } },
8
+ { "id": "recency-today", "label": "query+period recency: today's work (24h)", "args": { "query": "오늘 진행한 작업", "period": "24h", "limit": 10 }, "expect": { "kind": "results", "recencyOrdered": true } },
9
+ { "id": "negative-projectaa", "label": "negative: every row must literally mention the term (no filler)", "args": { "query": "ProjectAA 밸런스", "limit": 10 }, "expect": { "kind": "allContain", "allContain": ["projectaa"] } },
10
+ { "id": "xlang-embedding-crash", "label": "cross-language: embedding worker crash (strict topN)", "args": { "query": "embedding worker crash", "limit": 10 }, "expect": { "kind": "results", "topNContains": ["embedding"], "topN": 3 } }
11
+ ]
@@ -113,13 +113,87 @@ function scoreTopNContains(lines, substrings, n) {
113
113
  return { perSubstring, hitAtN, mrr, n };
114
114
  }
115
115
 
116
- function evaluateCase(kase, { count, ms, isError }, quality) {
116
+ // Parse leading "[YYYY-MM-DD HH:MM]" timestamps from result lines and check
117
+ // they are non-increasing (newest-first). Returns null when fewer than 2
118
+ // lines carry a parseable timestamp (nothing to order).
119
+ function scoreRecencyOrdered(lines) {
120
+ const tsRe = /^\s*\[(\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2})/;
121
+ const headerRe = /^\s*##\s/;
122
+ const parse = (raw) => Date.parse(raw.replace(' ', 'T'));
123
+ // Non-increasing check within a single ordered list of {raw,ts}. Returns
124
+ // the first {prev,cur} pair where cur is newer than prev, else null.
125
+ const firstBreak = (stamps) => {
126
+ for (let i = 1; i < stamps.length; i++) {
127
+ if (Number.isFinite(stamps[i].ts) && Number.isFinite(stamps[i - 1].ts) && stamps[i].ts > stamps[i - 1].ts) {
128
+ return { prev: stamps[i - 1].raw, cur: stamps[i].raw };
129
+ }
130
+ }
131
+ return null;
132
+ };
133
+ const hasSessions = lines.some((l) => headerRe.test(l));
134
+ if (!hasSessions) {
135
+ // Ungrouped: single global non-increasing check over all timestamped lines.
136
+ const stamps = [];
137
+ for (const line of lines) {
138
+ const m = tsRe.exec(line);
139
+ if (m) stamps.push({ raw: m[1], ts: parse(m[1]) });
140
+ }
141
+ if (stamps.length < 2) return { parsed: stamps.length, groups: 0, ordered: true, firstViolation: null };
142
+ const b = firstBreak(stamps);
143
+ return { parsed: stamps.length, groups: 0, ordered: b === null, firstViolation: b };
144
+ }
145
+ // Session-grouped: partition timestamped lines by their "## session" header.
146
+ // Timestamps only compared WITHIN a group; groups themselves must descend by
147
+ // their first line's timestamp.
148
+ const groups = [];
149
+ let cur = null;
150
+ for (const line of lines) {
151
+ if (headerRe.test(line)) { cur = { stamps: [] }; groups.push(cur); continue; }
152
+ const m = tsRe.exec(line);
153
+ if (m && cur) cur.stamps.push({ raw: m[1], ts: parse(m[1]) });
154
+ }
155
+ const nonEmpty = groups.filter((g) => g.stamps.length);
156
+ const parsed = nonEmpty.reduce((s, g) => s + g.stamps.length, 0);
157
+ let firstViolation = null;
158
+ for (const g of nonEmpty) {
159
+ const b = firstBreak(g.stamps);
160
+ if (b) { firstViolation = { ...b, scope: 'within-session' }; break; }
161
+ }
162
+ if (!firstViolation) {
163
+ const heads = nonEmpty.map((g) => g.stamps[0]);
164
+ const b = firstBreak(heads);
165
+ if (b) firstViolation = { ...b, scope: 'across-sessions' };
166
+ }
167
+ return { parsed, groups: nonEmpty.length, ordered: firstViolation === null, firstViolation };
168
+ }
169
+
170
+ // Score expect.allContain: every result line must contain at least one of the
171
+ // given substrings (case-insensitive). Returns offending lines (matching none).
172
+ // Use for negative cases where rows legitimately mention the term.
173
+ function scoreAllContain(lines, substrings) {
174
+ const needles = substrings.map((s) => String(s || '').toLowerCase()).filter(Boolean);
175
+ const offenders = lines.filter((line) => {
176
+ const l = line.toLowerCase();
177
+ return !needles.some((n) => l.includes(n));
178
+ });
179
+ return { needles: substrings, offenders, ok: offenders.length === 0 };
180
+ }
181
+
182
+ function evaluateCase(kase, { count, ms, isError }, quality, recency, allContain) {
117
183
  const warnings = [];
118
184
  if (isError) warnings.push('error result');
119
185
  if (ms > WARN_LATENCY_MS) warnings.push(`latency ${ms}ms > ${WARN_LATENCY_MS}ms`);
120
186
  if ((kase.expect === 'browse' || kase.expect === 'idlookup') && count === 0) {
121
187
  warnings.push('0 results for a browse/id-lookup case (expected data present)');
122
188
  }
189
+ if (kase.expect === 'empty' && count > 0) {
190
+ warnings.push(`expected empty but got ${count} result(s) — possible filler/unrelated match`);
191
+ }
192
+ if (allContain) {
193
+ for (const line of allContain.offenders) {
194
+ warnings.push(`allContain miss: result line contains none of [${allContain.needles.join(', ')}]: "${line}"`);
195
+ }
196
+ }
123
197
  if (quality) {
124
198
  for (const p of quality.perSubstring) {
125
199
  if (!p.hit) {
@@ -129,6 +203,10 @@ function evaluateCase(kase, { count, ms, isError }, quality) {
129
203
  }
130
204
  }
131
205
  }
206
+ if (recency && !recency.ordered && recency.firstViolation) {
207
+ const v = recency.firstViolation;
208
+ warnings.push(`recencyOrdered violation (${v.scope || 'global'}): ${v.cur} newer than prior ${v.prev}`);
209
+ }
132
210
  const status = warnings.length ? 'WARN' : 'PASS';
133
211
  return { status, warnings };
134
212
  }
@@ -156,7 +234,10 @@ async function runCase(memoryModule, kase) {
156
234
  const topNContains = expectObj && Array.isArray(expectObj.topNContains) ? expectObj.topNContains : null;
157
235
  const cutoffN = expectObj && Number.isInteger(expectObj.topN) ? expectObj.topN : 5;
158
236
  const quality = topNContains ? scoreTopNContains(resultLines(text), topNContains, cutoffN) : null;
159
- const evalResult = evaluateCase({ ...kase, expect: expectKind }, { count, ms, isError }, quality);
237
+ const recency = expectObj && expectObj.recencyOrdered ? scoreRecencyOrdered(resultLines(text)) : null;
238
+ const allContainNeedles = expectObj && Array.isArray(expectObj.allContain) ? expectObj.allContain : null;
239
+ const allContain = allContainNeedles ? scoreAllContain(resultLines(text), allContainNeedles) : null;
240
+ const evalResult = evaluateCase({ ...kase, expect: expectKind }, { count, ms, isError }, quality, recency, allContain);
160
241
  return {
161
242
  id: kase.id,
162
243
  label: kase.label,
@@ -167,6 +248,8 @@ async function runCase(memoryModule, kase) {
167
248
  errMsg,
168
249
  top3: topN(text, 3),
169
250
  quality,
251
+ recency,
252
+ allContain,
170
253
  status: evalResult.status,
171
254
  warnings: evalResult.warnings,
172
255
  };
@@ -182,6 +265,9 @@ function printCase(row) {
182
265
  process.stdout.write(` substr "${p.needle}" -> rank ${p.rank ?? 'none'}${p.hit ? '' : ' (miss)'}\n`);
183
266
  }
184
267
  }
268
+ if (row.recency) {
269
+ process.stdout.write(` recency: ${row.recency.ordered ? 'ordered' : 'OUT-OF-ORDER'} (${row.recency.parsed} timestamped lines)\n`);
270
+ }
185
271
  if (row.top3.length) {
186
272
  for (const line of row.top3) process.stdout.write(` - ${line}\n`);
187
273
  } else {
@@ -218,6 +304,7 @@ function printSummary(rows) {
218
304
  async function main() {
219
305
  const casesPath = argValue('cases', DEFAULT_CASES_PATH);
220
306
  const jsonMode = hasFlag('json');
307
+ const strict = hasFlag('strict');
221
308
  const cases = loadCases(casesPath);
222
309
 
223
310
  let memoryModule;
@@ -262,6 +349,8 @@ async function main() {
262
349
 
263
350
  const hardErrors = rows.filter((r) => r.isError);
264
351
  if (hardErrors.length) process.exitCode = 1;
352
+ // --strict: any WARN row fails the run. Default behavior (errors-only) unchanged.
353
+ if (strict && rows.some((r) => r.status === 'WARN')) process.exitCode = 1;
265
354
  }
266
355
 
267
356
  main().catch((e) => {
@@ -66,7 +66,10 @@ for (const target of ['heavy-worker', 'worker']) {
66
66
  }
67
67
  if (n === 'grep' && ['content', 'content_with_context'].includes(String(a.output_mode || ''))) {
68
68
  grepContent++;
69
- if (a['-C'] != null || a['-A'] != null || a['-B'] != null) grepContentWithCtx++;
69
+ // Implicit context: output_mode content_with_context carries context
70
+ // without explicit -C/-A/-B flags.
71
+ if (a['-C'] != null || a['-A'] != null || a['-B'] != null
72
+ || String(a.output_mode) === 'content_with_context') grepContentWithCtx++;
70
73
  const gpath = typeof a.path === 'string' ? a.path : JSON.stringify(a.path || '');
71
74
  for (let j = i + 1; j < Math.min(i + 4, tools.length); j++) {
72
75
  const u = tools[j];
@@ -508,8 +508,8 @@ if (!/^read 2\b/m.test(String(readRegionBatchOut))
508
508
  || (String(readRegionBatchOut).match(/scripts\/smoke\.mjs \[full\] \[ok\]/g) || []).length < 2
509
509
  || !/1→import \{ spawnSync \}/.test(String(readRegionBatchOut))
510
510
  || !/3→import \{ fileURLToPath \}/.test(String(readRegionBatchOut))
511
- || !/pass offset:2 to continue/.test(String(readRegionBatchOut))
512
- || !/pass offset:4 to continue/.test(String(readRegionBatchOut))) {
511
+ || !/(pass offset:2 to continue|ONE window: offset:2, limit:\d+)/.test(String(readRegionBatchOut))
512
+ || !/(pass offset:4 to continue|ONE window: offset:4, limit:\d+)/.test(String(readRegionBatchOut))) {
513
513
  throw new Error(`read region batch must preserve both requested spans:\n${readRegionBatchOut}`);
514
514
  }
515
515
 
@@ -534,6 +534,46 @@ if (readStringifiedLineErr || readStringifiedLineArgs.path[0].offset !== 7 || re
534
534
  throw new Error(`read guard must losslessly convert legacy line/context inside stringified arrays to offset/limit: err=${readStringifiedLineErr} args=${JSON.stringify(readStringifiedLineArgs)}`);
535
535
  }
536
536
 
537
+ // Absorb shape 1: region array + top-level offset/limit → top-level becomes
538
+ // the default window for regions that lack their own; no hard error.
539
+ const readRegionPlusTopLevelArgs = {
540
+ path: [{ path: 'scripts/smoke.mjs', offset: 3, limit: 4 }, { path: 'scripts/smoke.mjs' }],
541
+ offset: 0,
542
+ limit: 2,
543
+ };
544
+ const readRegionPlusTopLevelErr = validateBuiltinArgs('read', readRegionPlusTopLevelArgs);
545
+ if (readRegionPlusTopLevelErr
546
+ || 'offset' in readRegionPlusTopLevelArgs || 'limit' in readRegionPlusTopLevelArgs
547
+ || readRegionPlusTopLevelArgs.path[0].offset !== 3 || readRegionPlusTopLevelArgs.path[0].limit !== 4
548
+ || readRegionPlusTopLevelArgs.path[1].offset !== 0 || readRegionPlusTopLevelArgs.path[1].limit !== 2) {
549
+ throw new Error(`read guard must absorb region-array + top-level offset/limit: err=${readRegionPlusTopLevelErr} args=${JSON.stringify(readRegionPlusTopLevelArgs)}`);
550
+ }
551
+
552
+ // Absorb shape 2: parallel offset/limit as JSON-stringified arrays with path[]
553
+ // → zipped into per-file region objects (pairwise recovery), no int error.
554
+ const readZipWindowArgs = {
555
+ path: ['scripts/smoke.mjs', 'scripts/smoke.mjs'],
556
+ offset: '[0, 5]',
557
+ limit: '[2, 3]',
558
+ };
559
+ const readZipWindowErr = validateBuiltinArgs('read', readZipWindowArgs);
560
+ if (readZipWindowErr || !Array.isArray(readZipWindowArgs.path)
561
+ || readZipWindowArgs.path[0].offset !== 0 || readZipWindowArgs.path[0].limit !== 2
562
+ || readZipWindowArgs.path[1].offset !== 5 || readZipWindowArgs.path[1].limit !== 3
563
+ || 'offset' in readZipWindowArgs || 'limit' in readZipWindowArgs) {
564
+ throw new Error(`read guard must zip stringified offset/limit arrays onto path[]: err=${readZipWindowErr} args=${JSON.stringify(readZipWindowArgs)}`);
565
+ }
566
+
567
+ // Absorb shape 3: code_graph file/files as a JSON-stringified array → parsed to
568
+ // a real array before lookup (dispatched into files[]).
569
+ const cgStringifiedFileArgs = { mode: 'symbols', file: JSON.stringify(['a.mjs', 'b.mjs']) };
570
+ const cgStringifiedFileErr = validateBuiltinArgs('code_graph', cgStringifiedFileArgs);
571
+ if (cgStringifiedFileErr || 'file' in cgStringifiedFileArgs
572
+ || !Array.isArray(cgStringifiedFileArgs.files)
573
+ || cgStringifiedFileArgs.files[0] !== 'a.mjs' || cgStringifiedFileArgs.files[1] !== 'b.mjs') {
574
+ throw new Error(`code_graph guard must parse JSON-stringified file array: err=${cgStringifiedFileErr} args=${JSON.stringify(cgStringifiedFileArgs)}`);
575
+ }
576
+
537
577
  const graphOut = await executeCodeGraphTool('code_graph', {
538
578
  mode: 'symbols',
539
579
  file: 'scripts/smoke.mjs',
@@ -549,6 +589,17 @@ if (!/# symbol_search executeBuiltinTool\b/.test(String(graphSymbolBatchOut)) ||
549
589
  throw new Error(`code_graph symbol_search symbols[] batch execution failed:\n${graphSymbolBatchOut}`);
550
590
  }
551
591
 
592
+ // Absorb shape 3 (real dispatch): file as a JSON-stringified array batches per
593
+ // file instead of hitting "file not found: [...]".
594
+ const graphStringifiedFileOut = await executeCodeGraphTool('code_graph', {
595
+ mode: 'symbols',
596
+ file: JSON.stringify(['scripts/smoke.mjs']),
597
+ }, root);
598
+ if (/file not found/.test(String(graphStringifiedFileOut))
599
+ || !/binding|spawnSync|symbol/i.test(String(graphStringifiedFileOut))) {
600
+ throw new Error(`code_graph must parse JSON-stringified file array before lookup:\n${graphStringifiedFileOut}`);
601
+ }
602
+
552
603
  const graphMissingFileOut = await executeCodeGraphTool('code_graph', {
553
604
  mode: 'symbols',
554
605
  file: 'src/runtime/loop.mjs',
@@ -634,6 +685,33 @@ if (!/^Error[\s:[]/.test(String(shellTimeoutOut)) || !/\[timeout: 500ms\b/.test(
634
685
  throw new Error(`bash timeout must be milliseconds and classified as Error:\n${shellTimeoutOut}`);
635
686
  }
636
687
 
688
+ // Auto-promotion: a sync foreground command still running past the (soft)
689
+ // promotion budget is detached into a tracked background task and returns the
690
+ // same task_id envelope as explicit async — the caller never pre-chose async.
691
+ // Shrink the budget via MIXDOG_SHELL_AUTO_BACKGROUND_MS so the smoke stays fast.
692
+ const _priorAutoBgBudget = process.env.MIXDOG_SHELL_AUTO_BACKGROUND_MS;
693
+ process.env.MIXDOG_SHELL_AUTO_BACKGROUND_MS = '800';
694
+ let shellAutoPromoteOut;
695
+ try {
696
+ shellAutoPromoteOut = await executeBuiltinTool('shell', {
697
+ command: 'Start-Sleep -Seconds 6; Write-Output tool-smoke-autopromote-done',
698
+ cwd: root,
699
+ timeout: 30_000,
700
+ shell: 'powershell',
701
+ }, root);
702
+ } finally {
703
+ if (_priorAutoBgBudget === undefined) delete process.env.MIXDOG_SHELL_AUTO_BACKGROUND_MS;
704
+ else process.env.MIXDOG_SHELL_AUTO_BACKGROUND_MS = _priorAutoBgBudget;
705
+ }
706
+ if (!/auto-backgrounded/i.test(String(shellAutoPromoteOut)) || !/task_id:\s*\S+/i.test(String(shellAutoPromoteOut))) {
707
+ throw new Error(`shell auto-promotion must return a background task envelope (task_id + auto-backgrounded):\n${shellAutoPromoteOut}`);
708
+ }
709
+ // Clean up the promoted job so it doesn't outlive the smoke as an orphan.
710
+ const _autoPromoteTaskId = (/task_id:\s*(\S+)/i.exec(String(shellAutoPromoteOut)) || [])[1];
711
+ if (_autoPromoteTaskId) {
712
+ try { await executeBuiltinTool('task', { action: 'cancel', task_id: _autoPromoteTaskId }, root); } catch {}
713
+ }
714
+
637
715
  const legacyEscapedAlternationErr = validateBuiltinArgs('grep', { pattern: 'state\\.items\\.map\\|items\\.map', path: root });
638
716
  if (legacyEscapedAlternationErr) {
639
717
  throw new Error(`grep legacy \\| alternation should be accepted: ${legacyEscapedAlternationErr}`);
@@ -1217,8 +1295,10 @@ setInternalToolsProvider({
1217
1295
  if (workerToolNames.includes('tool_search')) {
1218
1296
  throw new Error(`agent session schema must not expose deferred tool_search: ${workerToolNames.join(', ')}`);
1219
1297
  }
1220
- if (workerToolNames.includes('shell')) {
1221
- throw new Error(`read-write agent session schema must not expose shell: ${workerToolNames.join(', ')}`);
1298
+ for (const name of ['shell', 'task']) {
1299
+ if (!workerToolNames.includes(name)) {
1300
+ throw new Error(`read-write agent session schema must expose ${name} for self-verification: ${workerToolNames.join(', ')}`);
1301
+ }
1222
1302
  }
1223
1303
  for (const name of ['skills_list', 'skill_view', 'skill_execute']) {
1224
1304
  if (workerToolNames.includes(name)) {
@@ -1266,7 +1346,7 @@ setInternalToolsProvider({
1266
1346
  const fullTools = (fullAgentSession.tools || []).map((tool) => tool?.name).filter(Boolean);
1267
1347
  const publicExploreTools = (publicExploreSession.tools || []).map((tool) => tool?.name).filter(Boolean);
1268
1348
  const expectedReadTools = ['code_graph', 'find', 'glob', 'list', 'grep', 'read', 'explore', 'search', 'web_fetch', 'Skill'];
1269
- const expectedWriteTools = ['code_graph', 'find', 'glob', 'list', 'grep', 'read', 'apply_patch', 'explore', 'search', 'web_fetch', 'Skill'];
1349
+ const expectedWriteTools = ['code_graph', 'find', 'glob', 'list', 'grep', 'read', 'apply_patch', 'shell', 'task', 'explore', 'search', 'web_fetch', 'Skill'];
1270
1350
  if (JSON.stringify(readTools) !== JSON.stringify(expectedReadTools)) {
1271
1351
  throw new Error(`read agent schema must be fixed allow-list: expected=${expectedReadTools.join(', ')} actual=${readTools.join(', ')}`);
1272
1352
  }
@@ -1276,20 +1356,17 @@ setInternalToolsProvider({
1276
1356
  if (readTools.includes('tool_search') || writeTools.includes('tool_search')) {
1277
1357
  throw new Error(`agent session fixed schemas must omit tool_search: read=${readTools.join(', ')} write=${writeTools.join(', ')}`);
1278
1358
  }
1279
- if (readTools.includes('shell') || writeTools.includes('shell')) {
1280
- throw new Error(`read/read-write agent schemas must omit shell: read=${readTools.join(', ')} write=${writeTools.join(', ')}`);
1359
+ if (readTools.includes('shell')) {
1360
+ throw new Error(`read agent schema must omit shell: read=${readTools.join(', ')}`);
1281
1361
  }
1282
1362
  for (const name of ['shell', 'apply_patch', 'task']) {
1283
1363
  if (readTools.includes(name)) {
1284
1364
  throw new Error(`read agent schema must omit non-read tool ${name}: read=${readTools.join(', ')}`);
1285
1365
  }
1286
1366
  }
1287
- for (const name of ['apply_patch', 'task']) {
1288
- if (name === 'apply_patch' && !writeTools.includes(name)) {
1289
- throw new Error(`read-write agent schema must preserve apply_patch: write=${writeTools.join(', ')}`);
1290
- }
1291
- if (name !== 'apply_patch' && writeTools.includes(name)) {
1292
- throw new Error(`read-write agent schema must omit non-edit tool ${name}: write=${writeTools.join(', ')}`);
1367
+ for (const name of ['apply_patch', 'shell', 'task']) {
1368
+ if (!writeTools.includes(name)) {
1369
+ throw new Error(`read-write agent schema must preserve ${name}: write=${writeTools.join(', ')}`);
1293
1370
  }
1294
1371
  }
1295
1372
  for (const name of ['memory', 'recall', 'reply']) {
@@ -1334,7 +1411,7 @@ setInternalToolsProvider({
1334
1411
  try {
1335
1412
  const resumed = await resumeSession(resumeAgentSession.id, 'full');
1336
1413
  const resumedTools = (resumed?.tools || []).map((tool) => tool?.name).filter(Boolean);
1337
- const expectedWriteTools = ['code_graph', 'find', 'glob', 'list', 'grep', 'read', 'apply_patch', 'explore', 'search', 'web_fetch', 'Skill'];
1414
+ const expectedWriteTools = ['code_graph', 'find', 'glob', 'list', 'grep', 'read', 'apply_patch', 'shell', 'task', 'explore', 'search', 'web_fetch', 'Skill'];
1338
1415
  if (JSON.stringify(resumedTools) !== JSON.stringify(expectedWriteTools)) {
1339
1416
  throw new Error(`resumed read-write agent schema must keep fixed allow-list: expected=${expectedWriteTools.join(', ')} actual=${resumedTools.join(', ')}`);
1340
1417
  }
@@ -1522,7 +1599,7 @@ const readPathDescription = readPathSchema.description || '';
1522
1599
  if (!/File path/i.test(readPathDescription) || !/\{path,offset,limit\}\[\]/i.test(readPathDescription) || !/Pass arrays directly/i.test(readPathDescription) || !/legacy recovery only/i.test(readPathDescription)) {
1523
1600
  throw new Error('read schema must keep directory-vs-file guidance');
1524
1601
  }
1525
- if (!/Dirs use list/i.test((BUILTIN_TOOLS.find((tool) => tool.name === 'read')?.description) || '')) {
1602
+ if (!/Not for directory listing/i.test((BUILTIN_TOOLS.find((tool) => tool.name === 'read')?.description) || '')) {
1526
1603
  throw new Error('read description must keep directory-vs-file guidance');
1527
1604
  }
1528
1605
  const readTool = BUILTIN_TOOLS.find((tool) => tool.name === 'read');
@@ -1533,7 +1610,7 @@ const readArrayItemAnyOf = readArraySchema?.items?.anyOf || [];
1533
1610
  if (!readArrayItemAnyOf.some((entry) => entry?.type === 'object' && entry?.properties?.offset && entry?.properties?.limit)) {
1534
1611
  throw new Error('read schema must expose array-of-region objects for batched spans');
1535
1612
  }
1536
- if (/line\+context/i.test(readDescription) || !/numeric offset\+limit/i.test(readDescription) || !/Batch paths\/regions as real arrays/i.test(readDescription) || !/grep content_with_context or code_graph anchors/i.test(readDescription)) {
1613
+ if (/line\+context/i.test(readDescription) || !/numeric offset\+limit/i.test(readDescription) || !/Batch paths\/regions as real arrays/i.test(readDescription)) {
1537
1614
  throw new Error('read description must expose offset/limit as the single window form');
1538
1615
  }
1539
1616
  if (readProps.line || readProps.context) {
@@ -1596,9 +1673,6 @@ if (codeGraphSymbolSearchErr) {
1596
1673
  if (!/code structure\/flow/i.test(codeGraphDescription) || !/symbols\/references\/calls\/deps/i.test(codeGraphDescription)) {
1597
1674
  throw new Error('code_graph description must stay structure-oriented and name its symbol modes');
1598
1675
  }
1599
- if (!/before text grep/i.test(codeGraphDescription)) {
1600
- throw new Error('code_graph description must steer symbol/caller lookups to structure lookup before grep');
1601
- }
1602
1676
  if (!/repo-local/i.test(codeGraphDescription) || !/NOT web search|not web/i.test(codeGraphDescription)) {
1603
1677
  throw new Error('code_graph description must mark itself repo-local (not web search)');
1604
1678
  }
@@ -1853,23 +1927,23 @@ const grepPathDescription = grepTool?.inputSchema?.properties?.path?.description
1853
1927
  const grepGlobDescription = grepTool?.inputSchema?.properties?.glob?.description || '';
1854
1928
  const grepOutputModeDescription = grepTool?.inputSchema?.properties?.output_mode?.description || '';
1855
1929
  const grepHeadLimitDescription = grepTool?.inputSchema?.properties?.head_limit?.description || '';
1856
- if (!/synonyms in pattern\[\]/i.test(grepPatternDescription) || !/OR in ONE grep/i.test(grepPatternDescription) || !/serial rewording/i.test(grepPatternDescription) || !/equivalent repeats/i.test(grepPatternDescription) || !/Narrowest file\/dir/i.test(grepPathDescription) || !/broad scopes return paths first/i.test(grepPathDescription) || !/refine from returned paths/i.test(grepPathDescription)) {
1930
+ if (!/Array = variants in one call/i.test(grepPatternDescription) || !/Known file or dir/i.test(grepPathDescription)) {
1857
1931
  throw new Error('grep schema must keep compact pattern/path guidance');
1858
1932
  }
1859
- if (!/same grep/i.test(grepGlobDescription) || !/no follow-up grep/i.test(grepGlobDescription) || !/equivalent scope changes/i.test(grepGlobDescription)) {
1860
- throw new Error('grep glob schema must steer narrowing into the same grep call');
1933
+ if (!/narrow scope/i.test(grepGlobDescription)) {
1934
+ throw new Error('grep glob schema must describe scope narrowing');
1861
1935
  }
1862
- if (!/Broad scope: files_with_matches\/count/i.test(grepOutputModeDescription) || !/Narrow scope: content_with_context/i.test(grepOutputModeDescription) || !/answer from/i.test(grepOutputModeDescription) || !/skip read/i.test(grepOutputModeDescription) || !/span is not shown/i.test(grepOutputModeDescription)) {
1863
- throw new Error('grep output_mode schema must steer content_with_context away from follow-up reads');
1936
+ if (!/files_with_matches\/count/i.test(grepOutputModeDescription) || !/content_with_context/i.test(grepOutputModeDescription)) {
1937
+ throw new Error('grep output_mode schema must name its output shapes');
1864
1938
  }
1865
- if (grepTool?.inputSchema?.properties?.head_limit?.minimum !== 0 || !/keep small/i.test(grepHeadLimitDescription)) {
1939
+ if (grepTool?.inputSchema?.properties?.head_limit?.minimum !== 0 || !/Max results/i.test(grepHeadLimitDescription)) {
1866
1940
  throw new Error('grep head_limit schema must keep locator caps explicit');
1867
1941
  }
1868
1942
  if (grepTool?.inputSchema?.properties?.type) {
1869
1943
  throw new Error('grep type schema must stay hidden; prefer glob for extension narrowing');
1870
1944
  }
1871
- if (!/code flow before text search/i.test(codeGraphProps.mode?.description || '') || !/one anchor is enough/i.test(codeGraphProps.symbols?.description || '') || !/one call/i.test(codeGraphProps.symbols?.description || '')) {
1872
- throw new Error('code_graph schema fields must steer away from repeated lookup loops');
1945
+ if (!/Repo-local/i.test(codeGraphProps.mode?.description || '') || !/one call/i.test(codeGraphProps.symbols?.description || '')) {
1946
+ throw new Error('code_graph schema fields must stay compact and repo-local');
1873
1947
  }
1874
1948
 
1875
1949
  const longToolSearchText = compactToolSearchDescription(`${patchDescription}\n${patchDescription}`);
@@ -8,7 +8,7 @@ Root-cause analysis agent.
8
8
 
9
9
  Smallest confirmed cause chain before fixes. Return likely cause, evidence
10
10
  (`file:line`), smallest next check/fix. Mark confirmed facts vs inferences;
11
- avoid broad speculation. Fragments only; no narration, no report bloat.
11
+ avoid broad speculation.
12
12
 
13
- Converge, don't sweep: when new reads stop adding evidence, report the best
14
- cause chain so far — a partial/blocked report is a valid completion.
13
+ Converge, don't sweep: when new evidence stops accruing, report the best
14
+ cause chain so far.
@@ -5,16 +5,13 @@ permission: read-write
5
5
  # Heavy Worker
6
6
  Broad implementation agent.
7
7
 
8
- Bounded slices; smallest coherent change, not rewrite. Stop: unclear scope,
9
- growing blast radius, or Lead-only verification.
8
+ Bounded slices; smallest coherent change, not rewrite. Stop when scope is
9
+ unclear or the blast radius grows.
10
10
 
11
- EDIT-FIRST DISCIPLINE. Survey the slice with ONE batched read/grep round, then
12
- patch the first bounded piece — edit incrementally, don't read exhaustively.
13
- NEVER "one more confirming read": a plausible anchor means the next call is
14
- `apply_patch`. Repeated read-only turns without an edit = stalling; on a
15
- runtime reminder, patch the piece you understand or report blocked — a valid
16
- completion. Self-check comes AFTER edits; deep verification is Lead's.
11
+ EDIT-FIRST DISCIPLINE. Survey the slice with one batched retrieval round,
12
+ then patch the first bounded piece — edit incrementally, don't read
13
+ exhaustively. Repeated read-only turns without an edit = stalling; on a
14
+ runtime reminder, patch the piece you understand or report blocked.
17
15
 
18
- Minimal checks + how-to-verify. Hand off outcome as fragments anchored to
19
- `file:line`; no narration or bloat.
16
+ Self-verify edits with shell (targeted test/build).
20
17
 
@@ -7,5 +7,4 @@ permission: read-write
7
7
  Maintenance and cleanup agent.
8
8
 
9
9
  Smallest coherent change; no scope growth. Repeated read-only turns without
10
- an edit = stalling; patch or return blocked — a blocked report is a valid
11
- completion. Hand off as fragments (`file:line`); no narration.
10
+ an edit = stalling; patch or return blocked.
@@ -8,5 +8,4 @@ Regression/risk review agent.
8
8
 
9
9
  Find actionable correctness/regression/security/verification risks. Findings
10
10
  first, severity-ordered, one line with `file:line`; skip non-risky nits. If
11
- clean, one line + only material residual risk. Fragments only; no narration,
12
- no report bloat.
11
+ clean, one line + only material residual risk.
@@ -7,13 +7,10 @@ Scoped implementation agent.
7
7
 
8
8
  Smallest scoped change; no drive-by cleanup. Stop when done/blocked.
9
9
 
10
- EDIT-FIRST DISCIPLINE. Brief anchors (`file:line`) are pre-verified — trust and
11
- patch. No anchor: locate with AT MOST 1-2 reads, then edit — NEVER "one more
12
- confirming read"; if you know the file and the change, the next call is
13
- `apply_patch`. Repeated read-only turns without an edit = stalling; on a
14
- runtime reminder, patch now or return blocked with what's missing — a blocked
15
- report is a valid completion. Threshold is "plausible", not "proven";
16
- self-check comes AFTER the edit, and Lead/Reviewer own final verification.
10
+ EDIT-FIRST DISCIPLINE. Brief anchors (`file:line`) are pre-verified — trust
11
+ and patch. No anchor: one locating batch, then `apply_patch`. Repeated
12
+ read-only turns without an edit = stalling; on a runtime reminder, patch now
13
+ or return blocked. Threshold is "plausible", not "proven".
17
14
 
18
- Hand off outcome as fragments anchored to `file:line`; no narration or bloat.
15
+ Self-verify edits with shell (node --check/test).
19
16
 
@@ -7,6 +7,7 @@
7
7
  "description": "Filesystem navigation agent invoked by the `explore` MCP tool",
8
8
  "invokedBy": "explore",
9
9
  "toolSchemaProfile": "read",
10
+ "schemaAllowedTools": ["grep", "find", "glob", "code_graph"],
10
11
  "kind": "retrieval",
11
12
  "permission": "read",
12
13
  "stallCap": { "idleSeconds": 240, "toolRunningSeconds": 180 }
@@ -54,6 +55,7 @@
54
55
  "description": "Scheduled-task executor invoked by scheduler tick",
55
56
  "invokedBy": "scheduler",
56
57
  "toolSchemaProfile": "read-write-search",
58
+ "schemaAllowedTools": ["code_graph", "find", "glob", "list", "grep", "read", "apply_patch", "search", "web_fetch"],
57
59
  "kind": "maintenance",
58
60
  "permission": "read-write",
59
61
  "inboundEvent": true,
@@ -67,6 +69,7 @@
67
69
  "description": "Webhook payload handler invoked by inbound webhook events",
68
70
  "invokedBy": "webhook",
69
71
  "toolSchemaProfile": "read-write-search",
72
+ "schemaAllowedTools": ["code_graph", "find", "glob", "list", "grep", "read", "apply_patch", "search", "web_fetch"],
70
73
  "kind": "maintenance",
71
74
  "permission": "read-write",
72
75
  "inboundEvent": true,
@@ -315,7 +315,7 @@ async function profiledImport(label, spec, { optional = false } = {}) {
315
315
  throw error;
316
316
  }
317
317
  }
318
- const outputStyleStatus = (dataDir = STANDALONE_DATA_DIR) => outputStyleStatusRaw(STANDALONE_ROOT, dataDir || STANDALONE_DATA_DIR);
318
+ const outputStyleStatus = (dataDir = STANDALONE_DATA_DIR, opts = {}) => outputStyleStatusRaw(STANDALONE_ROOT, dataDir || STANDALONE_DATA_DIR, opts);
319
319
  // Workflow/agent pack loaders bound to this runtime's root/data layout.
320
320
  const {
321
321
  listWorkflowPacks,
@@ -325,6 +325,7 @@ const {
325
325
  activeWorkflowSummary,
326
326
  loadAgentDefinition,
327
327
  workflowContextBlock,
328
+ activeWorkflowContext,
328
329
  } = createWorkflowHelpers({
329
330
  rootDir: STANDALONE_ROOT,
330
331
  dataDir: STANDALONE_DATA_DIR,
@@ -918,6 +919,16 @@ export async function createMixdogSessionRuntime({
918
919
  stopRemote('superseded-by-newer-remote-session');
919
920
  emitRemoteStateChange(false, 'superseded');
920
921
  }
922
+ // Symmetric acquire: the worker took the bridge seat (boot make-before-
923
+ // break or activate). Flip remote ON via the same side-state a user
924
+ // /remote toggles — remoteEnabled + transcript writer — but WITHOUT
925
+ // re-invoking channel start (the worker is already running; startRemote
926
+ // would re-fork/activate). Idempotent: no-op when already enabled.
927
+ if (msg?.params?.state === 'acquired' && !remoteEnabled) {
928
+ remoteEnabled = true;
929
+ ensureRemoteTranscriptWriter();
930
+ emitRemoteStateChange(true, 'acquired');
931
+ }
921
932
  return;
922
933
  }
923
934
  if (msg?.method !== 'notifications/claude/channel') return;
@@ -1339,18 +1350,24 @@ export async function createMixdogSessionRuntime({
1339
1350
  await resolveMissingRouteModelForFirstTurn();
1340
1351
  requireModelRoute();
1341
1352
  bootProfile('session:create:route-ready', { ms: (performance.now() - startedAt).toFixed(1) });
1342
- await refreshRouteEffort();
1353
+ // refreshRouteEffort (effort/model-meta) and loadCoreMemoryContext (memory
1354
+ // files) are independent — refreshRouteEffort only touches route effort/
1355
+ // display fields, never provider/model that the memory load reads — so run
1356
+ // them concurrently instead of serially on the boot path.
1357
+ const [, coreMemoryContext] = await Promise.all([
1358
+ refreshRouteEffort(),
1359
+ loadCoreMemoryContext(),
1360
+ ]);
1343
1361
  bootProfile('session:create:effort-ready', { ms: (performance.now() - startedAt).toFixed(1) });
1344
1362
  const providerImpl = reg.getProvider(route.provider);
1345
1363
  if (!providerImpl) {
1346
1364
  throw new Error(`Provider "${route.provider}" is not configured.`);
1347
1365
  }
1348
1366
  bootProfile('session:create:provider-ready', { ms: (performance.now() - startedAt).toFixed(1) });
1349
- const coreMemoryContext = await loadCoreMemoryContext();
1350
1367
  if (closeRequested) throw new Error('runtime is closing');
1351
1368
  const dataDir = cfgMod.getPluginData?.() || STANDALONE_DATA_DIR;
1352
- const workflow = activeWorkflowSummary(config, dataDir);
1353
- const workflowContext = workflowContextBlock(config, dataDir);
1369
+ // Load the active WORKFLOW.md pack once for both summary + context block.
1370
+ const { summary: workflow, context: workflowContext } = activeWorkflowContext(config, dataDir);
1354
1371
  const sessionOpts = {
1355
1372
  provider: route.provider,
1356
1373
  model: route.model,
@@ -2991,7 +3008,7 @@ export async function createMixdogSessionRuntime({
2991
3008
  let selectedRoute = resolveRoute(config, requested);
2992
3009
  await reg.initProviders(ensureProviderEnabled(config, selectedRoute.provider));
2993
3010
  const modelMeta = await lookupModelMeta(selectedRoute.provider, selectedRoute.model);
2994
- // Guard (A / strict reject): a free-text string must never become the
3011
+ // Guard (option A / strict reject): a free-text string must never become the
2995
3012
  // lead model. When the caller did not pin a provider, the route did not
2996
3013
  // match a preset, and the model is unknown to both the live provider
2997
3014
  // catalog (modelMeta is still the {id,provider} placeholder) and the
@@ -1,9 +1,8 @@
1
1
  # Agent Constraints
2
2
 
3
3
  - Use English for agent task communication.
4
- - ONE TURN = ONE BATCH: emit all independent tool calls of a step in the same
5
- turn; a lone read-only call that could have ridden the previous turn is a
6
- wasted round-trip.
4
+ - ONE TURN = ONE BATCH: a read-only call that could have ridden the prior
5
+ turn is a wasted round-trip.
7
6
  - NEVER PREAMBLE: no status/progress narration, "I will..." setup, or any text
8
7
  before tool calls — call needed tools immediately; emit text only for the
9
8
  final handoff after tool work is done.
@@ -15,6 +14,8 @@
15
14
  searched); state only what changed and where to look. Verification =
16
15
  command + result in one line. Same fact twice = delete one.
17
16
  - Handoff cap ~30 lines unless `Deliver:` raises it — a ceiling, not a target.
17
+ - A blocked or partial report is a valid completion: state done/missing/
18
+ blocker in fragments — never keep retrieving to avoid reporting.
18
19
  - Banned as pure cost: report headings, markdown tables (unless requested),
19
20
  prose narration, raw logs/tool traces, speculative next-checks, restated
20
21
  brief, articles/politeness.