mixdog 0.9.71 → 0.9.74

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 (27) hide show
  1. package/package.json +1 -1
  2. package/src/lib/rules-builder.cjs +21 -48
  3. package/src/output-styles/default.md +17 -25
  4. package/src/output-styles/minimal.md +9 -13
  5. package/src/output-styles/simple.md +12 -21
  6. package/src/rules/lead/01-general.md +2 -2
  7. package/src/rules/shared/01-tool.md +11 -13
  8. package/src/runtime/agent/orchestrator/config.mjs +0 -3
  9. package/src/runtime/agent/orchestrator/session/loop/steering-ladder.mjs +9 -0
  10. package/src/runtime/agent/orchestrator/session/loop/stored-tool-args.mjs +8 -4
  11. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +7 -1
  12. package/src/runtime/agent/orchestrator/session/manager/prompt-utils.mjs +26 -0
  13. package/src/runtime/agent/orchestrator/session/manager/rules-cache.mjs +1 -1
  14. package/src/runtime/agent/orchestrator/session/store/listing.mjs +1 -0
  15. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +1 -1
  16. package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +5 -1
  17. package/src/runtime/agent/orchestrator/tools/code-graph/disk-cache.mjs +5 -2
  18. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +13 -2
  19. package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +2 -2
  20. package/src/runtime/memory/lib/session-ingest.mjs +6 -0
  21. package/src/runtime/memory/tool-defs.mjs +1 -1
  22. package/src/runtime/shared/background-tasks.mjs +1 -1
  23. package/src/runtime/shared/user-data-guard.mjs +1 -2
  24. package/src/session-runtime/lifecycle-api.mjs +5 -1
  25. package/src/tui/dist/index.mjs +2 -3
  26. package/src/workflows/solo/WORKFLOW.md +2 -2
  27. package/src/rules/lead/02-channels.md +0 -3
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mixdog",
3
- "version": "0.9.71",
3
+ "version": "0.9.74",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Standalone mixdog coding-agent CLI/TUI workspace.",
@@ -21,8 +21,8 @@
21
21
  * Source files (rules/):
22
22
  * - shared/01-tool.md — universal tool policy (Lead + agent BP1, identical full set)
23
23
  * - lead/lead-tool.md — Lead-specific control-tower / delegation / ToolSearch guidance
24
- * - lead/lead-brief.md — Lead brief contract (agent handoff briefs)
25
- * - lead/01-02 — Lead general / channels
24
+ * - lead/lead-brief.md — Lead brief contract (skipped in solo workflow)
25
+ * - lead/01-general.md — Lead general
26
26
  * - output-styles/<name>.md — Lead output style, selected by config outputStyle
27
27
  * - agent/00-core.md — universal agent constraints (BP2, all profiles)
28
28
  * - agent/00-common.md — public-agent-only extras (BP2 full profile)
@@ -133,7 +133,7 @@ function buildProfilePreferencesContent(dataDir) {
133
133
  lines.push(`- Use "${profile.title}" when directly addressing the user; do not repeat it in routine progress updates or pre-tool preambles.`);
134
134
  }
135
135
  const shell = process.platform === 'win32' ? 'powershell' : 'bash';
136
- lines.push(`- Shell environment: ${shell}. Write shell commands and scripts in ${shell} syntax unless the user specifies otherwise; keep commands, paths, symbols, and exact errors verbatim.`);
136
+ lines.push(`- Shell environment: ${shell}. Write shell commands and scripts in ${shell} syntax unless the user specifies otherwise.`);
137
137
  return lines.length ? `# Profile Preferences\n\n${lines.join('\n')}` : '';
138
138
  }
139
139
 
@@ -240,27 +240,6 @@ function collectMarkdownFilesRecursive(dir) {
240
240
  return collected;
241
241
  }
242
242
 
243
- // Address-form rule. Reads only `user.title` — `user.name` is intentionally
244
- // not consumed; the user is addressed solely by the configured form.
245
- // Returns '' when title is empty so nothing is injected.
246
- function composeUserAddressBullet(memoryConfig) {
247
- const userTitle = (memoryConfig.user && memoryConfig.user.title || '').trim();
248
- if (!userTitle) return '';
249
- return `- User address form: ${userTitle} (use this address form exactly as written; do not combine it with any other name or honorific)`;
250
- }
251
-
252
- function splitLeadGeneral(general, addressBullet = '') {
253
- // Language is owned solely by the `# Language` section (buildLanguageSection);
254
- // general.md never carries a language line, so there is no language content to
255
- // promote into the meta layer here. The only meta-tier content this split
256
- // emits is the optional user address-form bullet.
257
- const role = String(general || '').trim();
258
- const meta = addressBullet
259
- ? ['# General', '', addressBullet].join('\n').trim()
260
- : '';
261
- return { role, meta };
262
- }
263
-
264
243
  function buildSharedToolContent({ PLUGIN_ROOT }) {
265
244
  const SHARED_DIR = path.join(PLUGIN_ROOT, 'rules', 'shared');
266
245
  return readOptional(path.join(SHARED_DIR, '01-tool.md'));
@@ -269,22 +248,26 @@ function buildSharedToolContent({ PLUGIN_ROOT }) {
269
248
  function buildLeadRoleContent({ PLUGIN_ROOT, DATA_DIR }) {
270
249
  const RULES_DIR = path.join(PLUGIN_ROOT, 'rules');
271
250
  const LEAD_DIR = path.join(RULES_DIR, 'lead');
272
- const memoryConfig = readConfigSection(DATA_DIR, 'memory');
273
- const addressBullet = composeUserAddressBullet(memoryConfig);
274
251
  const general = readOptional(path.join(LEAD_DIR, '01-general.md'));
275
- const generalSplit = splitLeadGeneral(general, addressBullet);
276
252
  const parts = [];
277
253
 
278
254
  const toolLead = readOptional(path.join(LEAD_DIR, 'lead-tool.md'));
279
255
  if (toolLead) parts.push(toolLead);
280
256
 
281
- const briefLead = readOptional(path.join(LEAD_DIR, 'lead-brief.md'));
282
- if (briefLead) parts.push(briefLead);
283
-
284
- if (generalSplit.role) parts.push(generalSplit.role);
257
+ // Solo workflow forbids delegation, so the agent-brief contract is dead
258
+ // weight there. Cache safety: lead rules cache keys on mixdog-config.json
259
+ // mtime, so switching workflow rebuilds this block.
260
+ const workflowActive = String(
261
+ (readConfigSection(DATA_DIR, 'agent').workflow || {}).active
262
+ || readConfigSection(DATA_DIR, 'workflow').active
263
+ || 'default',
264
+ ).trim().toLowerCase();
265
+ if (workflowActive !== 'solo') {
266
+ const briefLead = readOptional(path.join(LEAD_DIR, 'lead-brief.md'));
267
+ if (briefLead) parts.push(briefLead);
268
+ }
285
269
 
286
- const channels = readOptional(path.join(LEAD_DIR, '02-channels.md'));
287
- if (channels) parts.push(channels);
270
+ if (general) parts.push(general);
288
271
 
289
272
  return parts.join('\n\n');
290
273
  }
@@ -292,11 +275,6 @@ function buildLeadRoleContent({ PLUGIN_ROOT, DATA_DIR }) {
292
275
  function buildLeadMetaContent({ PLUGIN_ROOT, DATA_DIR }) {
293
276
  const RULES_DIR = path.join(PLUGIN_ROOT, 'rules');
294
277
  const LEAD_DIR = path.join(RULES_DIR, 'lead');
295
- const HISTORY_DIR = path.join(DATA_DIR, 'history');
296
- const memoryConfig = readConfigSection(DATA_DIR, 'memory');
297
- const addressBullet = composeUserAddressBullet(memoryConfig);
298
- const general = readOptional(path.join(LEAD_DIR, '01-general.md'));
299
- const generalSplit = splitLeadGeneral(general, addressBullet);
300
278
  const parts = [];
301
279
 
302
280
  const profilePreferences = buildProfilePreferencesContent(DATA_DIR);
@@ -305,16 +283,11 @@ function buildLeadMetaContent({ PLUGIN_ROOT, DATA_DIR }) {
305
283
  const languageSection = buildLanguageSection(DATA_DIR);
306
284
  if (languageSection) parts.push(languageSection);
307
285
 
308
- if (generalSplit.meta) parts.push(generalSplit.meta);
309
-
310
- const userProfile = readOptional(path.join(HISTORY_DIR, 'user.md'));
311
- if (userProfile) parts.push(`# User Profile\n\n${userProfile}`);
312
-
313
- const botPersona = readOptional(path.join(HISTORY_DIR, 'bot.md'));
314
- if (botPersona) parts.push(`# Bot Persona\n\n${botPersona}`);
315
-
316
- const userWorkflowMd = readOptional(path.join(DATA_DIR, 'user-workflow.md'));
317
- if (userWorkflowMd) parts.push(`# User Workflow\n\n${userWorkflowMd}`);
286
+ // Common instructions (renamed from user-workflow.md; legacy file still
287
+ // honored so existing installs keep their guidance without migration).
288
+ const commonInstructionsMd = readOptional(path.join(DATA_DIR, 'instructions.md'))
289
+ || readOptional(path.join(DATA_DIR, 'user-workflow.md'));
290
+ if (commonInstructionsMd) parts.push(`# Common Instructions\n\n${commonInstructionsMd}`);
318
291
 
319
292
  const outputStyle = loadOutputStyle({ PLUGIN_ROOT, DATA_DIR });
320
293
  if (outputStyle) parts.push(outputStyle);
@@ -13,36 +13,28 @@ essay-form. Depth comes from picking the right facts, not explaining more.
13
13
  Content
14
14
  - Lead with the outcome in one short sentence, then only the detail that
15
15
  matters: what changed, key evidence (paths, commands, errors, verification).
16
- - Summarize at the concept level: name the problem/behavior and direction, not
17
- the code path. Cite a symbol/path only as an anchor, never as the explanation.
18
- - Compress by cutting content (filler, hedging, connective padding, restated
19
- facts), not by clipping grammar: keep natural, complete sentences in the
20
- user's language never telegraph-style stub endings. Technical terms and
21
- code stay exact.
22
- - State conclusions, not reasoning: no mechanism walkthroughs, background, or
23
- chained qualifiers unless asked. One decisive fact beats three hedges.
24
- - Say each point once: problem and fix in ONE compact statement, not a restated
25
- pair. Prefer fewer, denser items over covering every nuance.
26
- - Size budget: roughly TWICE the Simple style — per point about 2 rendered
27
- lines, whole report ~10–15 lines. Spend the extra room on evidence and
28
- context Simple would drop, not on longer sentences.
16
+ - Summarize at the concept level: name the problem/behavior and direction,
17
+ not the code path; cite a symbol/path only as an anchor.
18
+ State conclusions, not reasoning: no mechanism walkthroughs or chained
19
+ qualifiers unless asked.
20
+ - Compress by cutting content (filler, hedging, restated facts), not grammar:
21
+ natural, complete sentences in the user's language; technical terms, paths,
22
+ commands, symbols, code, and exact errors stay verbatim.
23
+ - Say each point once: problem and fix in ONE compact statement. Size budget:
24
+ roughly TWICE the Simple style about 2 rendered lines per point, whole
25
+ report ~10–15 lines, spent on evidence Simple would drop.
29
26
  - Use labels such as `Changes`, `Verification`, and `Risks / next steps` in
30
- final reports to structure the summary; skip labels on interim progress.
31
- - Collapse trivial tasks to a couple of sentences instead of forcing sections.
27
+ final reports; skip labels on interim progress, and collapse trivial tasks
28
+ to a couple of sentences instead of forcing sections.
32
29
  - Synthesize agent or retrieval results; never forward raw reports, long file
33
30
  lists, tool traces, or session metadata.
34
31
  - Do not hide blockers, failed verification, or required follow-up; surface them
35
32
  in one short clause.
36
- - Keep paths, commands, symbols, API names, code, and exact errors verbatim.
37
33
 
38
34
  Layout (hard rules)
39
- - One bullet or numbered item = one idea, at most 2 rendered lines including its
40
- sub-bullet. If it needs more, cut the detail — do not add lines.
41
- - Open each item with a short **bold key point**, then the brief elaboration
42
- never bury the point mid-sentence.
43
- - Insert a blank line between numbered items, and between any list items running
44
- past one line (loose list). Never emit a wall of consecutive multi-line items.
45
- - Keep paragraphs to ~3 lines max, with a blank line between paragraphs, lists,
46
- and code blocks.
47
- - Nest at most one sub-level; deeper detail means you are over-explaining.
35
+ - One bullet or numbered item = one idea, at most 2 rendered lines, opened
36
+ with a short **bold key point**; if it needs more, cut the detail.
37
+ - Blank line between numbered items and between any multi-line list items
38
+ never a wall of consecutive multi-line items. Paragraphs ~3 lines max with
39
+ blank lines between blocks; nest at most one sub-level.
48
40
  - Never name this style unless asked.
@@ -10,18 +10,14 @@ keep-coding-instructions: true
10
10
  Minimal — a very short summary: one or two sentences, nothing more.
11
11
 
12
12
  - Summarize only the net result in one short sentence; add a second short
13
- sentence only if a second fact (verification, blocker) genuinely needs it.
14
- Never cram unrelated facts into a run-on just to stay at one sentence.
15
- - Size budget: roughly HALF the Simple style — 1–2 plain sentences, ~2–3
16
- rendered lines at most, however large the task was.
17
- - Compress by cutting content, not grammar: natural, complete sentences only.
18
- Concept-level onlynever walk through code or mechanisms.
19
- - Summarize, never itemize: do not describe which files changed or how. State
20
- only what the change accomplishes.
21
- - No headings, bullets, numbered lists, labels, or sections — plain sentences
22
- only, even when the request says "report" or "summary".
23
- - Preferred pattern: `<target> changed. <verification> passed.`
24
- - If verification was not run, say the change is done and verification was not
25
- run.
13
+ sentence only for a fact (verification, blocker) that genuinely needs it
14
+ never a run-on that crams extra facts in.
15
+ - Size budget: roughly HALF the Simple style — 1–2 plain, complete sentences
16
+ (~2–3 rendered lines) however large the task was, concept-level only.
17
+ - Summarize, never itemize: no headings, bullets, labels, or sections and no
18
+ file-by-file detailstate only what the change accomplishes, even when
19
+ the request says "report" or "summary".
20
+ - Preferred pattern: `<target> changed. <verification> passed.` If
21
+ verification was not run, say so.
26
22
  - Preserve only the single decisive path, command, symbol, API name, code, or
27
23
  error verbatim.
@@ -12,31 +12,22 @@ Practical concise — outcome-first handoffs for coding work: summarize the
12
12
  result, do not narrate or explain the change.
13
13
 
14
14
  - Open with the outcome in one sentence: done, blocked, or awaiting a decision.
15
- - Summarize at the concept level: name the behavior and direction, not the
16
- code path; cite a symbol/path only as an anchor, never as the explanation.
17
- - Compress by cutting content (filler, hedging, pleasantries, restated facts),
18
- not grammar: keep natural, complete sentences in the user's language — never
19
- telegraph-style stub endings. Technical terms and code stay exact.
20
- - Summarize what the change accomplishes, not a per-file changelog. Name a
21
- path (`file_path:line_number`) only when the reader truly needs it to
22
- navigate.
23
- - Controlled detail: usually 13 short bullets or 2–3 sentences total; state
24
- each point once outcome or fix direction, not both. No step-by-step
25
- narration or file/line inventory.
26
- - Size budget: roughly HALF the Default style and TWICE Minimal — one rendered
27
- line per point, whole reply ~5–7 lines. Above that you are writing Default;
28
- below ~3 lines consider whether prose (Minimal) reads better.
29
- - Layout: one idea per bullet, ONE line each (two only when a verbatim
30
- path/error forces it), led with a short bold key phrase; blank line between
31
- multi-line list items — never a dense wall of text. If a point runs past one
32
- line, cut the elaboration — detail beyond key phrase + one clause belongs to
33
- Default.
15
+ - Summarize at the concept level what the change accomplishes, not a
16
+ per-file changelog or code path; cite a path (`file_path:line_number`) only
17
+ as a navigation anchor, never as the explanation.
18
+ - Compress by cutting content (filler, acknowledgments, hedging, restated
19
+ facts), not grammar: natural, complete sentences in the user's language;
20
+ paths, commands, symbols, code, and exact errors stay verbatim.
21
+ - Controlled detail: 1–3 short bullets or 2–3 sentences; state each point
22
+ once. Size budget: roughly HALF the Default style and TWICE Minimal —
23
+ whole reply ~57 lines.
24
+ - Layout: one idea per bullet, ONE line each, led with a short bold key
25
+ phrase; blank line between multi-line list items — never a dense wall of
26
+ text.
34
27
  - Final handoffs may use labels like `Changes`, `Verification`, and
35
28
  `Risks / next steps`; do not label interim progress.
36
29
  - Synthesize agent or retrieval results; never forward raw reports, long file
37
30
  lists, tool traces, or session metadata.
38
31
  - Do not hide blockers, failed verification, or required follow-up — state
39
32
  them in one short clause; if verification was not run, say so once.
40
- - Keep paths, commands, symbols, API names, code, and exact errors verbatim.
41
- - Skip filler, acknowledgments, hedging, and repeated conclusions.
42
33
  - Never name this style unless asked.
@@ -2,7 +2,7 @@
2
2
 
3
3
  - You are Mixdog, the current coding-agent CLI/TUI assistant with
4
4
  multi-provider agent workflows. Never identify as generic OpenAI/ChatGPT.
5
- - A preamble is at most one useful sentence. Never use direct names,
6
- honorifics, headings, labels, or routine lookup narration.
5
+ - A preamble is at most one useful sentence, with no direct names, honorifics,
6
+ headings, labels, or routine lookup narration.
7
7
  - Destructive/hard-to-reverse action needs explicit confirmation.
8
8
  - Act proactively; ask only for decisions.
@@ -15,10 +15,12 @@
15
15
  concurrently regardless of tool. Later turns are only for targets dependent
16
16
  on prior results or unresolved facets. Shell/write calls are serial.
17
17
  - After locator results, collect all known candidate files/regions before
18
- inspection. Batch compatible reads, including same-file regions, in one
18
+ inspection. Batch compatible reads, including same-file regions real
19
+ `{path,offset,limit}` arrays covering the whole logical unit — in one
19
20
  `path[]` call and graph targets in arrays. Parallelize independent
20
- incompatible read-only inspections. Do not start a singleton while a known
21
- compatible candidate remains. Put all known edits in one patch.
21
+ incompatible read-only inspections. Never page or reread returned spans,
22
+ and do not start a singleton while a known compatible candidate remains.
23
+ Put all new edits in one patch.
22
24
  - Project root, session cwd, user-provided and tool-returned paths are
23
25
  verified. Use `find` first for every genuinely guessed path/name fragment, in
24
26
  the same turn as independent probes. Never find verified roots or use
@@ -27,22 +29,18 @@
27
29
  - At task start, batch all `explore` facets in one `query[]` call, maximum 8,
28
30
  without rephrased duplicates. Retry `EXPLORATION_FAILED` once with changed
29
31
  tokens.
30
- - Stop when evidence covers the deliverable; never re-locate, re-verify,
31
- refine, or upgrade a sufficient anchor. Every returned requested `path:line`
32
- freezes the LOCATION only. When content was not returned, read or
33
- code_graph detail inspection is valid; never re-locate it.
32
+ - Stop when evidence covers the deliverable; never re-locate, re-verify, or
33
+ upgrade a sufficient anchor. A returned requested `path:line`
34
+ freezes the LOCATION only; when content was not returned,
35
+ read or code_graph detail inspection is valid never re-locate it.
34
36
  - Give grep content mode enough `-C` to avoid rereads; a sufficient contextual
35
37
  grep means no overlapping `read`. After `files_with_matches`, `count`,
36
38
  capped, or insufficient context, inspect only missing content. A nonzero
37
39
  `content_with_context` result resolves the concept; act directly without
38
- token changes, narrowing, or re-search. Only zero/error results permit token
39
- or scope changes.
40
- - Batch independent read files/regions as real arrays, using
41
- `{path,offset,limit}` regions, and read the whole logical unit. Never page or
42
- reread returned spans.
40
+ re-search; only zero/error results permit token or scope changes.
43
41
  - Apply edits before verification, then verify in a separate shell turn and
44
42
  consume results in order. Otherwise remain parallel.
45
43
  - A long-running command promoted to background is a decision point, not a
46
- cue to wait. Estimate whether observed progress can finish within budget;
44
+ cue to wait: estimate whether observed progress can finish within budget;
47
45
  otherwise diagnose the bottleneck and switch routes. Choose waiting
48
46
  explicitly.
@@ -396,7 +396,6 @@ export function loadConfig(options = {}) {
396
396
  // bypass the per-provider auto-clear table.
397
397
  autoClear: { enabled: true, ...raw.autoClear },
398
398
  compaction: raw.compaction && typeof raw.compaction === 'object' ? { ...raw.compaction } : {},
399
- trajectory: { enabled: true, ...raw.trajectory },
400
399
  runtime: raw.runtime && typeof raw.runtime === 'object' ? raw.runtime : {},
401
400
  shell: raw.shell && typeof raw.shell === 'object' ? raw.shell : {},
402
401
  update: raw.update && typeof raw.update === 'object' ? { ...raw.update } : {},
@@ -426,7 +425,6 @@ export function loadConfig(options = {}) {
426
425
  skills: normalizeSkillsConfig(null),
427
426
  autoClear: { enabled: true },
428
427
  compaction: {},
429
- trajectory: { enabled: true },
430
428
  runtime: {},
431
429
  shell: {},
432
430
  update: {},
@@ -566,7 +564,6 @@ function buildAgentSaveBuilder(config, appliedDirty) {
566
564
  skills: normalizeSkillsConfig(config.skills),
567
565
  autoClear: config.autoClear || {},
568
566
  compaction: config.compaction || {},
569
- trajectory: config.trajectory || {},
570
567
  runtime: config.runtime || {},
571
568
  shell: config.shell || {},
572
569
  update: config.update || {},
@@ -124,6 +124,15 @@ export function createSteeringLadder(ctx) {
124
124
  pushSystemReminder(editPushEligible
125
125
  ? 'Last 2 turns each ran a single read-only tool. Batch independent lookups (read/grep/glob/code_graph) into ONE turn, or start editing if you have enough context.'
126
126
  : 'Last 2 turns each ran a single read-only tool. Batch independent lookups (read/grep/glob/code_graph) into ONE turn.');
127
+ try {
128
+ appendAgentTrace({
129
+ sessionId,
130
+ iteration: iterations,
131
+ kind: 'steer',
132
+ payload: { tag: 'level1_batching', level1_fires: _level1FireCount, edit_count: editCount },
133
+ agent: sessionAgent || null,
134
+ });
135
+ } catch { /* best-effort */ }
127
136
  }
128
137
  _hintFiredThisTurn = true;
129
138
  }
@@ -11,8 +11,12 @@ const STORED_TOOL_ARG_BODY_KEY_RE = /^(?:content|old_string|new_string|patch|rew
11
11
  const STORED_TOOL_ARG_LONG_KEY_RE = /^(?:command|script)$/i;
12
12
  // Marker-alone value produced by compactStoredToolArgString for body keys.
13
13
  const STORED_TOOL_ARG_MARKER_RE = /^\[mixdog compacted\b[^\]\n]*\]$/;
14
- const STORED_TOOL_ARG_BODY_LIMIT = 2_000;
15
- const STORED_TOOL_ARG_LONG_LIMIT = 8_000;
14
+ // One shared budget for stored bodies AND long commands, matching the codex
15
+ // reference truncation budget (TruncationPolicyConfig::bytes(10_000)). Below
16
+ // this, the model always sees its own recent patch text verbatim — the 2 KB
17
+ // body cut made every mid-size successful patch render as a marker and Opus
18
+ // mimicked the marker as literal patch input on the next call.
19
+ const STORED_TOOL_ARG_LIMIT = 10_000;
16
20
  const STORED_TOOL_ARG_PREVIEW_HEAD = 360;
17
21
  const STORED_TOOL_ARG_PREVIEW_TAIL = 160;
18
22
 
@@ -20,10 +24,10 @@ function compactStoredToolArgString(value, key = '') {
20
24
  if (typeof value !== 'string') return value;
21
25
  const isBody = STORED_TOOL_ARG_BODY_KEY_RE.test(key);
22
26
  const isLong = isBody || STORED_TOOL_ARG_LONG_KEY_RE.test(key);
23
- const limit = isBody ? STORED_TOOL_ARG_BODY_LIMIT : (isLong ? STORED_TOOL_ARG_LONG_LIMIT : Infinity);
27
+ const limit = isLong ? STORED_TOOL_ARG_LIMIT : Infinity;
24
28
  if (value.length <= limit) return value;
25
29
  const hash = createHash('sha256').update(value).digest('hex').slice(0, 16);
26
- const marker = `[mixdog compacted ${key || 'string'}: ${value.length} chars, sha256:${hash}]`;
30
+ const marker = `[mixdog compacted ${key || 'string'}: ${value.length} chars, sha256:${hash}; do not copy]`;
27
31
  // Body args (patch / old_string / new_string / content / rewrite) are
28
32
  // apply_patch / edit inputs. Keeping a head/tail preview leaves real patch
29
33
  // fragments (a "*** Begin Patch" opening, diff lines) inside a SUCCESSFUL
@@ -21,6 +21,7 @@ import {
21
21
  prefixSessionStartContent,
22
22
  buildCurrentTimeBlock,
23
23
  buildSessionStartBlock,
24
+ buildProjectInstructionsBlock,
24
25
  hasUserConversationMessage,
25
26
  } from './prompt-utils.mjs';
26
27
  import {
@@ -379,11 +380,16 @@ export async function askSession(sessionId, prompt, context, onToolCall, cwdOver
379
380
  const _sessionStartBlock = shouldInjectSessionStart
380
381
  ? buildSessionStartBlock(session, effectiveCwd)
381
382
  : '';
383
+ // Project instructions ride the same once-per-session gate as the
384
+ // `# Session` block (Lead only — agent-owned sessions return '').
385
+ const _projectInstructionsBlock = _sessionStartBlock
386
+ ? buildProjectInstructionsBlock(effectiveCwd)
387
+ : '';
382
388
  const _currentTimeBlock = buildCurrentTimeBlock(prompt);
383
389
  const _turnReminderBlock = _currentTimeBlock
384
390
  ? `<system-reminder>\n# Current Time\n${_currentTimeBlock}\n</system-reminder>`
385
391
  : '';
386
- const _turnPrefixBlock = [_sessionStartBlock, _turnReminderBlock].filter(Boolean).join('\n\n');
392
+ const _turnPrefixBlock = [_sessionStartBlock, _projectInstructionsBlock, _turnReminderBlock].filter(Boolean).join('\n\n');
387
393
  const _baseUserTurnContent = prefixUserTurnContent(prompt, _contextBlock);
388
394
  const _userTurnContent = prefixSessionStartContent(_baseUserTurnContent, _turnPrefixBlock);
389
395
  if (shouldInjectSessionStart && _sessionStartBlock) {
@@ -1,5 +1,7 @@
1
1
  // Prompt content + temporal helpers, extracted verbatim from manager.mjs
2
2
  // (behavior-preserving). Pure string/date utilities with no session state.
3
+ import { existsSync, readFileSync } from 'node:fs';
4
+ import { join } from 'node:path';
3
5
  import { isInternalRuntimeNotificationText as contractIsInternalRuntimeNotificationText } from '../../../../shared/tool-execution-contract.mjs';
4
6
  import { SUMMARY_PREFIX } from '../compact.mjs';
5
7
 
@@ -113,6 +115,30 @@ export function buildSessionStartBlock(session, cwd) {
113
115
  return lines.length > 1 ? lines.join('\n') : '';
114
116
  }
115
117
 
118
+ // Project-scoped user instructions (<project>/.mixdog/instructions.md),
119
+ // injected once per session right after the `# Session` block — BP4 head, so
120
+ // the BP1–3 system prefix stays byte-identical across projects and the block
121
+ // sits in the stable message prefix for same-project cache reuse. Missing or
122
+ // empty file → '' (nothing injected). Best-effort: unreadable files never
123
+ // break a turn.
124
+ const PROJECT_INSTRUCTIONS_MAX_CHARS = 16_000;
125
+ export function buildProjectInstructionsBlock(cwd) {
126
+ const dir = String(cwd || '').trim();
127
+ if (!dir) return '';
128
+ try {
129
+ const file = join(dir, '.mixdog', 'instructions.md');
130
+ if (!existsSync(file)) return '';
131
+ const text = readFileSync(file, 'utf8').trim();
132
+ if (!text) return '';
133
+ const body = text.length > PROJECT_INSTRUCTIONS_MAX_CHARS
134
+ ? `${text.slice(0, PROJECT_INSTRUCTIONS_MAX_CHARS)}\n[... project instructions truncated]`
135
+ : text;
136
+ return `# Project Instructions\n${body}`;
137
+ } catch {
138
+ return '';
139
+ }
140
+ }
141
+
116
142
  export function isReferenceFilesMessage(message) {
117
143
  return message?.role === 'user'
118
144
  && typeof message.content === 'string'
@@ -104,8 +104,8 @@ export function _buildLeadMetaContext() {
104
104
  const RULES_DIR = join(PLUGIN_ROOT, 'rules');
105
105
  const mtime = maxMtimeRecursive([
106
106
  join(RULES_DIR, 'lead'),
107
- join(DATA_DIR, 'history'),
108
107
  join(DATA_DIR, 'mixdog-config.json'),
108
+ join(DATA_DIR, 'instructions.md'),
109
109
  join(DATA_DIR, 'user-workflow.md'),
110
110
  join(PLUGIN_ROOT, 'output-styles'),
111
111
  join(DATA_DIR, 'output-styles'),
@@ -3,6 +3,7 @@
3
3
  // The two halves share the in-flight save map so an unpersisted session still
4
4
  // shows up in listings; the cycle is import-only (calls happen at runtime).
5
5
  import { existsSync, readFileSync, readdirSync, statSync, unlinkSync } from 'fs';
6
+ import { join } from 'path';
6
7
  import { loadConfig } from '../../config.mjs';
7
8
  import { isAgentOwner } from '../../agent-owner.mjs';
8
9
  import { scanTopLevelLifecycle } from '../lifecycle-scan.mjs';
@@ -80,7 +80,7 @@ export const BUILTIN_TOOLS = [
80
80
  properties: {
81
81
  command: { type: 'string', description: 'Command.' },
82
82
  cwd: { type: 'string', description: 'Working directory; persists across calls. Omit to reuse; absolute path changes it.' },
83
- timeout: { type: 'number', description: `Timeout ms; default ${_shellDefaultTimeoutMs()}. On sync timeout the command moves to background as a task_id and keeps running; an explicit timeout then blocks at most BASH_MAX_TIMEOUT_MS, the remainder enforced as a background deadline. Sleep-like commands and MIXDOG_SHELL_DISABLE_BACKGROUND_TASKS opt out of promotion: they block for the full explicit timeout, then are killed with a [timeout] marker. async with timeout omitted runs until done/cancelled; an explicit timeout is still enforced.` },
83
+ timeout: { type: 'number', description: `Timeout ms; default ${_shellDefaultTimeoutMs()}. On sync timeout the command is promoted to a background task_id and keeps running, with any explicit timeout enforced as a background deadline. Sleep-like commands skip promotion: they block the full timeout, then are killed with a [timeout] marker. async runs until done/cancelled unless a timeout is set.` },
84
84
  merge_stderr: { type: 'boolean', description: 'Merge stderr.' },
85
85
  mode: { type: 'string', enum: ['sync', 'async'], description: executionModeSchemaDescription('sync') },
86
86
  shell: { type: 'string', enum: ['bash', 'powershell'], description: 'Force shell. Windows defaults to PowerShell; bash = Git Bash/POSIX.' },
@@ -453,7 +453,11 @@ export function _spawnCodeGraphWorker(
453
453
  const genStillCurrent = getGeneration(graphCwd) === genAtStart;
454
454
  if (genStillCurrent && cacheResult) {
455
455
  setMemoryCache(graphCwd, { ts: Date.now(), signature: msg.signature, graph: msg.graph });
456
- setDiskCache(graphCwd, msg.graph);
456
+ // The Worker strictly persisted this graph before posting
457
+ // success. Adopt it in the main process without scheduling a
458
+ // redundant flush that can race the next Worker on the same
459
+ // manifest lock.
460
+ setDiskCache(graphCwd, msg.graph, { persist: false });
457
461
  }
458
462
  settle(genStillCurrent ? msg.graph : new Error('code-graph build invalidated during prewarm'));
459
463
  } else {
@@ -424,7 +424,7 @@ export function listCachedCodeGraphRoots() {
424
424
  }
425
425
  }
426
426
 
427
- export function _setDiskCodeGraphEntry(cwd, graph) {
427
+ export function _setDiskCodeGraphEntry(cwd, graph, { persist = true } = {}) {
428
428
  _loadDiskCodeGraphCache();
429
429
  // Stamp the cache entry with the persistence timestamp (not the build
430
430
  // start) so manifest/signature metadata stays fresh. Disk retention is
@@ -434,5 +434,8 @@ export function _setDiskCodeGraphEntry(cwd, graph) {
434
434
  serialized.builtAt = Date.now();
435
435
  _diskCodeGraphCache.set(_canonicalGraphCwd(cwd), serialized);
436
436
  _pruneDiskCodeGraphEntries();
437
- _scheduleDiskCodeGraphCacheFlush();
437
+ // Worker success is already fenced by drainCodeGraphCacheStrict(); the
438
+ // parent only adopts that result. Rewriting it here races a following
439
+ // Worker on manifest.json.lock without adding durability.
440
+ if (persist) _scheduleDiskCodeGraphCacheFlush();
438
441
  }
@@ -3,7 +3,7 @@
3
3
  // executePatchTool entry point + replay capture. Moved verbatim from
4
4
  // patch.mjs; control flow and output are unchanged.
5
5
 
6
- import { chmodSync, existsSync, readFileSync, writeFileSync, mkdirSync, rmSync, statSync } from 'node:fs';
6
+ import { chmodSync, existsSync, readFileSync, readdirSync, writeFileSync, mkdirSync, rmSync, statSync } from 'node:fs';
7
7
  import { dirname as pathDirname, resolve as pathResolve, isAbsolute, join as pathJoin } from 'node:path';
8
8
  import { parsePatch } from 'diff';
9
9
  import { getAbortSignalForSession } from '../../session/abort-lookup.mjs';
@@ -664,7 +664,11 @@ function patchTargetPaths(patchStr, basePath) {
664
664
  }
665
665
 
666
666
  function maybeCapturePatchReplay(args, cwd, errorText) {
667
- if (process.env.MIXDOG_PATCH_REPLAY_CAPTURE !== '1') return;
667
+ // Default ON: every apply_patch failure is frozen for `npm run patch:replay`
668
+ // (args + target-file snapshots). Set MIXDOG_PATCH_REPLAY_CAPTURE=0 to
669
+ // disable. Retention is bounded below to the newest records.
670
+ const _flag = String(process.env.MIXDOG_PATCH_REPLAY_CAPTURE ?? '1').trim().toLowerCase();
671
+ if (_flag === '0' || _flag === 'false' || _flag === 'off') return;
668
672
  try {
669
673
  const patchStr = typeof args?.patch === 'string' ? args.patch : '';
670
674
  const basePath = pathResolve(String(args?.base_path || cwd || process.cwd()));
@@ -693,6 +697,13 @@ function maybeCapturePatchReplay(args, cwd, errorText) {
693
697
  file_snapshots: files,
694
698
  };
695
699
  writeFileSync(pathJoin(dir, `${id}.json`), JSON.stringify(record, null, 2), { mode: 0o600 });
700
+ // Retention: keep the newest 40 captures. The id prefix is Date.now() in
701
+ // base36 (fixed width until ~2059), so a lexicographic sort is
702
+ // chronological and the oldest records sort first.
703
+ const _kept = readdirSync(dir).filter((f) => f.endsWith('.json')).sort();
704
+ for (const stale of _kept.slice(0, Math.max(0, _kept.length - 40))) {
705
+ try { rmSync(pathJoin(dir, stale), { force: true }); } catch { /* best-effort */ }
706
+ }
696
707
  } catch { /* capture is best-effort; never affect the tool result */ }
697
708
  }
698
709
 
@@ -26,7 +26,7 @@ export const PATCH_TOOL_DEFS = [
26
26
  name: 'apply_patch',
27
27
  title: 'Mixdog Apply Patch',
28
28
  annotations: { title: 'Mixdog Apply Patch', readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: false, compressible: false, compressibleLossless: true },
29
- description: 'Apply known file edits in one atomic patch; sections run in listed order and all touched paths roll back if any section fails. Do not split a dependent edit across turns.',
29
+ description: 'Apply file edits in one patch; sections run in listed order and all touched paths roll back if any section fails. Do not split a dependent edit across turns.',
30
30
  freeformDescription: APPLY_PATCH_FREEFORM_DESCRIPTION,
31
31
  freeform: {
32
32
  type: 'grammar',
@@ -36,7 +36,7 @@ export const PATCH_TOOL_DEFS = [
36
36
  inputSchema: {
37
37
  type: 'object',
38
38
  properties: {
39
- patch: { type: 'string', description: 'Patch text. V4A preferred; use one file block per target file with exact current context; include all known edits in listed order. On failure, the tool rolls all earlier writes back.' },
39
+ patch: { type: 'string', description: 'Patch text. V4A preferred; one file block per target file, 3 lines of exact context per hunk, @@ anchors when ambiguous; include all new edits in listed order. On failure, the tool rolls all earlier writes back.' },
40
40
  format: { type: 'string', enum: ['unified', 'v4a'], description: 'Auto-detected.' },
41
41
  base_path: { type: 'string', description: 'Repo root.' },
42
42
  dry_run: { type: 'boolean', description: 'Default false. true = validate only, no write.' },
@@ -238,6 +238,12 @@ function stripUserTurnPrefixEnvelopes(text) {
238
238
  // — its next line is not a `Cwd:/Model:/Workflow:` field — so it is
239
239
  // preserved verbatim (zero-loss). Anchored ^.
240
240
  out = out.replace(/^# Session\n(?:(?:Cwd|Model|Workflow): [^\n]*\n)+(?:\n|$)/, '')
241
+ // 1b) Leading `# Project Instructions\n<body>` (buildProjectInstructionsBlock,
242
+ // emitted right after the `# Session` block). Anchored to start; body runs
243
+ // to the next `# ` section boundary or end — the human prompt follows a
244
+ // `# Task` marker, so real user text is never inside this span.
245
+ out = out.replace(/^# Project Instructions\n[\s\S]*?(?=\n# |$)/, '')
246
+ out = out.replace(/^\n+/, '')
241
247
  // 2) Leading `# Additional context\n<body>\n\n` (manager.mjs:3168). Anchored
242
248
  // to start; body runs up to the next `# ` section boundary or end. The
243
249
  // `\n\n` separator manager.mjs emits is included.
@@ -36,7 +36,7 @@ export const TOOL_DEFS = [
36
36
  properties: {
37
37
  query: { anyOf: [{ type: 'string' }, { type: 'array', items: { type: 'string' }, minItems: 1 }], description: 'Search text, or array for independent fan-out queries.' },
38
38
  id: { anyOf: [{ type: 'number' }, { type: 'array', items: { type: 'number' }, minItems: 1 }], description: 'Exact #id(s) from recall. Do not invent ids.' },
39
- period: { type: 'string', description: "last (recent sessions; +query topic-filter; limit=session count [default 5], offset=session paging), Nm/Nh/Nd (rolling), today/yesterday/this_week/last_week, all, YYYY-MM-DD, YYYY-MM-DD~YYYY-MM-DD, HH:MM~HH:MM (today), or 'YYYY-MM-DD HH:MM~HH:MM'." },
39
+ period: { type: 'string', description: "last (recent sessions; +query topic-filter; limit=session count), Nm/Nh/Nd, today/yesterday/this_week/last_week, all, YYYY-MM-DD, date~date, HH:MM~HH:MM, or 'YYYY-MM-DD HH:MM~HH:MM'." },
40
40
  limit: { type: 'number', description: 'Max entries.' },
41
41
  offset: { type: 'number', description: 'Skip entries.' },
42
42
  sort: { type: 'string', enum: ['importance', 'date'], description: 'importance or date.' },
@@ -51,7 +51,7 @@ export function executionModeSchemaDescription(defaultMode = 'sync') {
51
51
  if (defaultMode === 'async') {
52
52
  return 'sync = inline result; async = task_id + completion notification. Default async.';
53
53
  }
54
- return 'Runs sync (inline result); no default auto-background. async forces a background task_id + completion notification.';
54
+ return 'Default sync.';
55
55
  }
56
56
 
57
57
  export function taskIdFromArgs(args = {}) {
@@ -31,9 +31,8 @@ const RECOVERY_NOTICE = 'RECOVERY-REQUIRED.txt';
31
31
 
32
32
  const USER_DATA_FILES = [
33
33
  'mixdog-config.json',
34
+ 'instructions.md',
34
35
  'user-workflow.md',
35
- 'history/user.md',
36
- 'history/bot.md',
37
36
  ];
38
37
 
39
38
  const USER_DATA_DIRS = [
@@ -425,7 +425,11 @@ export function createLifecycleApi(deps) {
425
425
  setSession(resumed);
426
426
  applyResolvedCwd(resolveResumeCwd(resumed, getCurrentCwd()), { markRefresh: false });
427
427
  const route = getRoute();
428
- const resumeEffort = hasOwn(route, 'effort') ? route.effort : resumed.effort;
428
+ // The resumed session's OWN effort wins. resolveRoute always returns an
429
+ // effort key, so this used to reinstate the effort of whichever session
430
+ // happened to be open before — a schedule/webhook session opened with a
431
+ // different effort than the one it actually ran with.
432
+ const resumeEffort = resumed.effort || (hasOwn(route, 'effort') ? route.effort : undefined);
429
433
  setRoute(resolveRoute(getConfig(), { provider: resumed.provider, model: resumed.model, effort: resumeEffort }));
430
434
  await refreshRouteEffort();
431
435
  const session = getSession();
@@ -1179,9 +1179,8 @@ var init_user_data_guard = __esm({
1179
1179
  "src/runtime/shared/user-data-guard.mjs"() {
1180
1180
  USER_DATA_FILES = [
1181
1181
  "mixdog-config.json",
1182
- "user-workflow.md",
1183
- "history/user.md",
1184
- "history/bot.md"
1182
+ "instructions.md",
1183
+ "user-workflow.md"
1185
1184
  ];
1186
1185
  USER_DATA_DIRS = [
1187
1186
  "schedules",
@@ -7,8 +7,8 @@ agents:
7
7
 
8
8
  # Solo
9
9
 
10
- Lead handles everything directly: consult the user and build the plan
11
- together. Before the user explicitly approves the latest plan, work is
10
+ Consult the user and build the plan together. Before the user explicitly
11
+ approves the latest plan, work is
12
12
  read-only investigation and planning — no edits, no state mutation. A new or
13
13
  changed request resets planning; a scope change requires fresh approval.
14
14
 
@@ -1,3 +0,0 @@
1
- # Channels
2
-
3
- The runtime handles channel features.