mixdog 0.9.150 → 0.9.152

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 (211) hide show
  1. package/README.md +110 -51
  2. package/package.json +3 -2
  3. package/scripts/tool-stress.mjs +0 -2
  4. package/src/defaults/skills/setup/SKILL.md +18 -27
  5. package/src/headless-exec.mjs +19 -13
  6. package/src/headless-exec.test.mjs +46 -8
  7. package/src/rules/shared/30-exploration.md +2 -1
  8. package/src/runtime/agent/orchestrator/config.mjs +6 -66
  9. package/src/runtime/agent/orchestrator/context/collect-skills.test.mjs +153 -0
  10. package/src/runtime/agent/orchestrator/context/collect.mjs +49 -45
  11. package/src/runtime/agent/orchestrator/mcp/client.mjs +242 -15
  12. package/src/runtime/agent/orchestrator/mcp/features.test.mjs +66 -0
  13. package/src/runtime/agent/orchestrator/mcp/security.test.mjs +36 -20
  14. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +54 -33
  15. package/src/runtime/agent/orchestrator/providers/anthropic.mjs +1 -0
  16. package/src/runtime/agent/orchestrator/providers/antigravity-oauth.mjs +7 -0
  17. package/src/runtime/agent/orchestrator/providers/gemini-schema.mjs +7 -0
  18. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +3 -8
  19. package/src/runtime/agent/orchestrator/providers/gemini.mjs +7 -0
  20. package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +10 -2
  21. package/src/runtime/agent/orchestrator/providers/lib/provider-replay.mjs +50 -0
  22. package/src/runtime/agent/orchestrator/providers/lib/sse-framing.mjs +93 -0
  23. package/src/runtime/agent/orchestrator/providers/model-list-sanitize.mjs +3 -3
  24. package/src/runtime/agent/orchestrator/providers/openai-compat-presets.mjs +8 -0
  25. package/src/runtime/agent/orchestrator/providers/openai-compat-stream.mjs +7 -0
  26. package/src/runtime/agent/orchestrator/providers/openai-compat-wire.mjs +25 -2
  27. package/src/runtime/agent/orchestrator/providers/openai-compat.mjs +35 -4
  28. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +22 -0
  29. package/src/runtime/agent/orchestrator/providers/openai-oauth-ws.mjs +0 -2
  30. package/src/runtime/agent/orchestrator/providers/openai-responses-payload.mjs +13 -0
  31. package/src/runtime/agent/orchestrator/providers/openai-ws-pool.mjs +4 -7
  32. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +9 -0
  33. package/src/runtime/agent/orchestrator/providers/provider-replay.test.mjs +235 -0
  34. package/src/runtime/agent/orchestrator/providers/sse-framing.test.mjs +719 -0
  35. package/src/runtime/agent/orchestrator/providers/stream-json-pool.mjs +399 -39
  36. package/src/runtime/agent/orchestrator/providers/stream-json-worker.mjs +26 -0
  37. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +14 -4
  38. package/src/runtime/agent/orchestrator/session/cache/read-cache.mjs +13 -0
  39. package/src/runtime/agent/orchestrator/session/compact/handoff.mjs +2 -4
  40. package/src/runtime/agent/orchestrator/session/compact/runner.mjs +42 -34
  41. package/src/runtime/agent/orchestrator/session/compaction-read-reset.test.mjs +161 -0
  42. package/src/runtime/agent/orchestrator/session/context-utils.mjs +14 -4
  43. package/src/runtime/agent/orchestrator/session/loop/recall-fasttrack.mjs +23 -44
  44. package/src/runtime/agent/orchestrator/session/loop/tool-exec.mjs +12 -1
  45. package/src/runtime/agent/orchestrator/session/manager/ask-session.mjs +31 -18
  46. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +22 -125
  47. package/src/runtime/agent/orchestrator/session/manager/message-sanitize.mjs +3 -0
  48. package/src/runtime/agent/orchestrator/session/manager/session-crud.mjs +6 -1
  49. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +9 -23
  50. package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint-journal.mjs +556 -0
  51. package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint-journal.test.mjs +457 -0
  52. package/src/runtime/agent/orchestrator/session/manager/turn-checkpoint.mjs +105 -186
  53. package/src/runtime/agent/orchestrator/session/manager/turn-interruption.mjs +115 -0
  54. package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +2 -0
  55. package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +7 -0
  56. package/src/runtime/agent/orchestrator/session/store.mjs +23 -1
  57. package/src/runtime/agent/orchestrator/tools/builtin/absolute-glob-expand.test.mjs +63 -0
  58. package/src/runtime/agent/orchestrator/tools/builtin/atomic-write.mjs +29 -10
  59. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool-cwd-env.test.mjs +14 -0
  60. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +19 -10
  61. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +1 -1
  62. package/src/runtime/agent/orchestrator/tools/builtin/enoent-outside-project.test.mjs +56 -0
  63. package/src/runtime/agent/orchestrator/tools/builtin/external-tool-adapters.mjs +6 -2
  64. package/src/runtime/agent/orchestrator/tools/builtin/find-search-budget.test.mjs +30 -14
  65. package/src/runtime/agent/orchestrator/tools/builtin/git-command-policy.mjs +15 -4
  66. package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +105 -31
  67. package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.test.mjs +103 -1
  68. package/src/runtime/agent/orchestrator/tools/builtin/grep-single-file-rescue.test.mjs +106 -0
  69. package/src/runtime/agent/orchestrator/tools/builtin/lib/absolute-glob-expand.mjs +129 -0
  70. package/src/runtime/agent/orchestrator/tools/builtin/lib/grep-single-file-rescue.mjs +218 -0
  71. package/src/runtime/agent/orchestrator/tools/builtin/list-tool.mjs +26 -652
  72. package/src/runtime/agent/orchestrator/tools/builtin/native-search-client.mjs +64 -72
  73. package/src/runtime/agent/orchestrator/tools/builtin/native-search-runner.mjs +2 -5
  74. package/src/runtime/agent/orchestrator/tools/builtin/native-search-transport.mjs +75 -0
  75. package/src/runtime/agent/orchestrator/tools/builtin/native-search-transport.test.mjs +24 -0
  76. package/src/runtime/agent/orchestrator/tools/builtin/noise-dir-visibility.test.mjs +69 -0
  77. package/src/runtime/agent/orchestrator/tools/builtin/path-diagnostics.mjs +33 -6
  78. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +26 -6
  79. package/src/runtime/agent/orchestrator/tools/builtin/runtime-capabilities.mjs +7 -66
  80. package/src/runtime/agent/orchestrator/tools/builtin/search-builders.mjs +11 -0
  81. package/src/runtime/agent/orchestrator/tools/builtin/search-glob-tool.mjs +25 -1
  82. package/src/runtime/agent/orchestrator/tools/builtin/search-grep-tool.mjs +60 -5
  83. package/src/runtime/agent/orchestrator/tools/builtin/search-path-diagnostics.mjs +54 -1
  84. package/src/runtime/agent/orchestrator/tools/builtin/write-symlink.test.mjs +153 -0
  85. package/src/runtime/agent/orchestrator/tools/code-graph/aggregate-anchor-relocation.test.mjs +55 -0
  86. package/src/runtime/agent/orchestrator/tools/code-graph/build.mjs +12 -0
  87. package/src/runtime/agent/orchestrator/tools/code-graph/constants.mjs +4 -1
  88. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +78 -8
  89. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.test.mjs +44 -0
  90. package/src/runtime/agent/orchestrator/tools/code-graph/graph-binary.mjs +12 -2
  91. package/src/runtime/agent/orchestrator/tools/code-graph/graph-model.mjs +35 -21
  92. package/src/runtime/agent/orchestrator/tools/code-graph/memory-cache.mjs +28 -13
  93. package/src/runtime/agent/orchestrator/tools/code-graph/memory-cache.test.mjs +54 -0
  94. package/src/runtime/agent/orchestrator/tools/graph-manifest.json +11 -11
  95. package/src/runtime/agent/orchestrator/tools/patch/dispatch.mjs +44 -10
  96. package/src/runtime/agent/orchestrator/tools/patch/orchestrator.mjs +57 -5
  97. package/src/runtime/agent/orchestrator/tools/patch/patch-symlink.test.mjs +141 -0
  98. package/src/runtime/agent/orchestrator/tools/patch/v4a-convert.mjs +7 -3
  99. package/src/runtime/agent/orchestrator/tools/patch/v4a-pure-move.test.mjs +57 -0
  100. package/src/runtime/agent/orchestrator/tools/patch/v4a-section-coalesce.test.mjs +81 -0
  101. package/src/runtime/agent/orchestrator/tools/patch-manifest.json +11 -11
  102. package/src/runtime/agent/orchestrator/tools/progress-message.mjs +8 -0
  103. package/src/runtime/agent/orchestrator/tools/spawn-manifest.json +11 -11
  104. package/src/runtime/browser-bridge/client.mjs +103 -0
  105. package/src/runtime/browser-bridge/tool-defs.mjs +42 -0
  106. package/src/runtime/computer-bridge/client.mjs +101 -0
  107. package/src/runtime/computer-bridge/tool-defs.mjs +53 -0
  108. package/src/runtime/media/store.mjs +18 -8
  109. package/src/runtime/media/store.test.mjs +14 -0
  110. package/src/runtime/memory/index.mjs +3 -5
  111. package/src/runtime/memory/lib/pg/process.mjs +29 -2
  112. package/src/runtime/memory/lib/pg/process.test.mjs +36 -0
  113. package/src/runtime/memory/lib/pg/supervisor.mjs +5 -1
  114. package/src/runtime/memory/lib/query-handlers.mjs +7 -6
  115. package/src/runtime/memory/lib/recall-format.mjs +13 -31
  116. package/src/runtime/memory/lib/transcript-ingest.mjs +8 -6
  117. package/src/runtime/memory/lib/transcript-ingest.test.mjs +52 -0
  118. package/src/runtime/shared/child-spawn-gate.mjs +17 -0
  119. package/src/runtime/shared/child-spawn-remote.mjs +69 -7
  120. package/src/runtime/shared/child-spawn-remote.test.mjs +124 -0
  121. package/src/runtime/shared/pristine-execution-contract.json +5 -1
  122. package/src/runtime/shared/pristine-execution.mjs +5 -2
  123. package/src/runtime/shared/provider-api-key.mjs +1 -0
  124. package/src/runtime/shared/session-runtime-health.mjs +91 -0
  125. package/src/runtime/shared/session-runtime-health.test.mjs +53 -0
  126. package/src/runtime/shared/skill-document.mjs +86 -0
  127. package/src/runtime/shared/skill-document.test.mjs +81 -0
  128. package/src/runtime/shared/tool-surface.mjs +18 -0
  129. package/src/runtime/shared/turn-snapshot-store.mjs +1 -0
  130. package/src/runtime/shared/turn-snapshot.mjs +32 -9
  131. package/src/runtime/shared/user-cwd.mjs +33 -14
  132. package/src/runtime/shared/user-cwd.test.mjs +43 -0
  133. package/src/session-runtime/cwd-plugins.mjs +26 -26
  134. package/src/session-runtime/cwd-plugins.test.mjs +75 -0
  135. package/src/session-runtime/cwd-tool-routing.test.mjs +5 -0
  136. package/src/session-runtime/global-extensions.mjs +26 -0
  137. package/src/session-runtime/global-extensions.test.mjs +28 -0
  138. package/src/session-runtime/goal-runtime.mjs +721 -0
  139. package/src/session-runtime/goal-runtime.test.mjs +108 -0
  140. package/src/session-runtime/lifecycle-api.mjs +19 -4
  141. package/src/session-runtime/lifecycle-api.test.mjs +21 -0
  142. package/src/session-runtime/mcp-glue.mjs +49 -47
  143. package/src/session-runtime/plugin-mcp-standard.test.mjs +177 -0
  144. package/src/session-runtime/plugin-mcp.mjs +96 -22
  145. package/src/session-runtime/prewarm.mjs +1 -10
  146. package/src/session-runtime/resource-api-global.test.mjs +69 -0
  147. package/src/session-runtime/resource-api.mjs +173 -58
  148. package/src/session-runtime/runtime-core.mjs +148 -20
  149. package/src/session-runtime/runtime-tunables.mjs +6 -8
  150. package/src/session-runtime/session-turn-api.mjs +22 -11
  151. package/src/session-runtime/skills-api.mjs +79 -25
  152. package/src/session-runtime/skills-api.test.mjs +99 -0
  153. package/src/session-runtime/tool-catalog-schema.mjs +0 -5
  154. package/src/session-runtime/tool-catalog.mjs +4 -13
  155. package/src/session-runtime/tool-policy-surface.test.mjs +33 -11
  156. package/src/session-runtime/tool-surface.mjs +12 -0
  157. package/src/standalone/agent-dispatch-broker.mjs +5 -9
  158. package/src/standalone/agent-tool/notify.mjs +6 -8
  159. package/src/standalone/agent-tool/notify.test.mjs +37 -0
  160. package/src/standalone/channel-worker-heartbeat.test.mjs +30 -0
  161. package/src/standalone/channel-worker.mjs +31 -1
  162. package/src/standalone/daemon.mjs +31 -15
  163. package/src/standalone/hook-bus/config.mjs +1 -0
  164. package/src/standalone/memory-runtime-proxy.mjs +5 -2
  165. package/src/standalone/plugin-admin.mjs +17 -0
  166. package/src/standalone/plugin-admin.test.mjs +7 -0
  167. package/src/standalone/provider-admin.mjs +1 -0
  168. package/src/standalone/session-client.mjs +23 -3
  169. package/src/standalone/session-protocol.mjs +5 -0
  170. package/src/standalone/session-runtime-agent-control-client.mjs +133 -0
  171. package/src/standalone/session-runtime-agent-control-client.test.mjs +71 -0
  172. package/src/standalone/session-runtime-dispatch-cancel.test.mjs +139 -0
  173. package/src/standalone/session-runtime-host-factory.mjs +17 -0
  174. package/src/standalone/session-runtime-host-factory.test.mjs +31 -0
  175. package/src/standalone/session-runtime-host-health.test.mjs +5 -0
  176. package/src/standalone/session-runtime-host.mjs +696 -58
  177. package/src/standalone/session-runtime-inline-host.mjs +351 -0
  178. package/src/standalone/session-runtime-inline-host.test.mjs +83 -0
  179. package/src/standalone/session-runtime-provider-cooldown.mjs +78 -0
  180. package/src/standalone/session-runtime-provider-cooldown.test.mjs +80 -0
  181. package/src/standalone/session-runtime-shard-host.test.mjs +569 -0
  182. package/src/standalone/session-runtime-shard-router.mjs +149 -0
  183. package/src/standalone/session-runtime-shard-router.test.mjs +101 -0
  184. package/src/standalone/session-runtime-worker.mjs +476 -18
  185. package/src/standalone/session-service.mjs +81 -6
  186. package/src/tui/app/app-view.jsx +1 -0
  187. package/src/tui/app/core-memory-picker.mjs +2 -0
  188. package/src/tui/app/extension-pickers.mjs +1 -1
  189. package/src/tui/app/maintenance-pickers.mjs +3 -3
  190. package/src/tui/app/model-options.mjs +2 -0
  191. package/src/tui/app/model-picker.mjs +14 -4
  192. package/src/tui/app/panel-handoff.test.mjs +311 -0
  193. package/src/tui/app/project-picker.mjs +1 -0
  194. package/src/tui/app/route-pickers.mjs +21 -6
  195. package/src/tui/app/settings-picker.mjs +26 -3
  196. package/src/tui/app/slash-commands.mjs +1 -0
  197. package/src/tui/app/slash-dispatch.mjs +73 -22
  198. package/src/tui/app/theme-effort-pickers.mjs +7 -2
  199. package/src/tui/components/Picker.jsx +10 -1
  200. package/src/tui/dist/index.mjs +163 -44
  201. package/src/tui/session/context-state.mjs +1 -0
  202. package/src/tui/session/goal-continuation.mjs +110 -0
  203. package/src/tui/session/goal-continuation.test.mjs +84 -0
  204. package/src/tui/session/queue-helpers.mjs +5 -2
  205. package/src/tui/session/session-action-surface.test.mjs +108 -0
  206. package/src/tui/session/session-api-ext.mjs +39 -8
  207. package/src/tui/session/session-api.mjs +58 -0
  208. package/src/tui/session/session-flow.mjs +6 -0
  209. package/src/tui/session/turn.mjs +13 -0
  210. package/src/tui/session-local.mjs +15 -1
  211. package/src/runtime/agent/orchestrator/tools/builtin/fuzzy-match.mjs +0 -301
@@ -203,29 +203,6 @@ function hasKeys(value) {
203
203
  return !!value && typeof value === 'object' && !Array.isArray(value) && Object.keys(value).length > 0;
204
204
  }
205
205
 
206
- // Process-local write intent for project MCP overrides. A loaded config contains
207
- // snapshots for every project, but only entries toggled by this process may
208
- // override a newer value read from disk inside the save lock.
209
- const dirtyMcpProjectOverrides = new Map();
210
- let mcpProjectOverrideDirtySeq = 0;
211
- function mcpProjectOverrideDirtyKey(projectKey, serverName) {
212
- return `${String(projectKey)}\u0000${String(serverName)}`;
213
- }
214
- export function markMcpProjectOverrideDirty(projectKey, serverName, enabled) {
215
- const pairKey = mcpProjectOverrideDirtyKey(projectKey, serverName);
216
- dirtyMcpProjectOverrides.set(pairKey, {
217
- enabled: enabled !== false,
218
- seq: ++mcpProjectOverrideDirtySeq,
219
- });
220
- }
221
- function clearDurablySavedMcpProjectOverrides(appliedDirty) {
222
- for (const { pairKey, seq } of appliedDirty) {
223
- if (dirtyMcpProjectOverrides.get(pairKey)?.seq === seq) {
224
- dirtyMcpProjectOverrides.delete(pairKey);
225
- }
226
- }
227
- }
228
-
229
206
  function normalizeWebSearchRoute(route) {
230
207
  if (!route || typeof route !== 'object' || Array.isArray(route))
231
208
  return null;
@@ -435,6 +412,7 @@ function canonicalizeLegacyAgentStorage(value = {}) {
435
412
  delete next.capabilities;
436
413
  delete next.defaultProvider;
437
414
  delete next.guide;
415
+ delete next.mcpProjectOverrides;
438
416
  return next;
439
417
  }
440
418
 
@@ -451,6 +429,7 @@ function agentConfigStorageNeedsMigration(value = {}) {
451
429
  || Object.prototype.hasOwnProperty.call(value || {}, 'searchRoute')
452
430
  || Object.prototype.hasOwnProperty.call(value || {}, 'capabilities')
453
431
  || Object.prototype.hasOwnProperty.call(value || {}, 'defaultProvider')
432
+ || Object.prototype.hasOwnProperty.call(value || {}, 'mcpProjectOverrides')
454
433
  || presets.some((preset) => !normalizePreset(preset))
455
434
  || (modules && typeof modules === 'object' && Object.prototype.hasOwnProperty.call(modules, 'memory'))
456
435
  || normalizedFields.some((key) => JSON.stringify(value?.[key]) !== JSON.stringify(canonical?.[key]));
@@ -560,9 +539,6 @@ export function loadConfig(options = {}) {
560
539
  const loaded = canonicalizeAgentRouteStorage({
561
540
  providers: mergedProviders,
562
541
  mcpServers,
563
- mcpProjectOverrides: raw.mcpProjectOverrides && typeof raw.mcpProjectOverrides === 'object' && !Array.isArray(raw.mcpProjectOverrides)
564
- ? raw.mcpProjectOverrides
565
- : {},
566
542
  presets: normalizedPresets,
567
543
  default: raw.default || null,
568
544
  maintenance: { ...DEFAULT_MAINTENANCE, ...normalizedMaint },
@@ -628,7 +604,6 @@ export function loadConfig(options = {}) {
628
604
  return {
629
605
  ...defaults,
630
606
  mcpServers: {},
631
- mcpProjectOverrides: {},
632
607
  presets: DEFAULT_PRESETS.map(p => ({ ...p })),
633
608
  default: null,
634
609
  maintenance: { ...DEFAULT_MAINTENANCE },
@@ -691,7 +666,7 @@ export async function patchSkillsDisabledAsync(disabledNames) {
691
666
  return nextSkills;
692
667
  }
693
668
 
694
- function buildAgentSaveBuilder(config, appliedDirty) {
669
+ function buildAgentSaveBuilder(config) {
695
670
  const canonicalRoutes = canonicalizeLegacyAgentStorage(config);
696
671
  // Strip ephemeral defaults from providers but preserve any unknown
697
672
  // per-provider subkey so future schema additions round-trip through the
@@ -745,42 +720,10 @@ function buildAgentSaveBuilder(config, appliedDirty) {
745
720
  // a concurrent instance survive the save (lost-update guard).
746
721
  return (existingRaw) => {
747
722
  const mcpServers = config.mcpServers || {};
748
- const diskOverrides = existingRaw.mcpProjectOverrides && typeof existingRaw.mcpProjectOverrides === 'object'
749
- ? existingRaw.mcpProjectOverrides
750
- : {};
751
- const memoryOverrides = config.mcpProjectOverrides && typeof config.mcpProjectOverrides === 'object'
752
- ? config.mcpProjectOverrides
753
- : {};
754
- const mcpProjectOverrides = {};
755
- for (const projectKey of new Set([...Object.keys(diskOverrides), ...Object.keys(memoryOverrides)])) {
756
- const diskProject = diskOverrides[projectKey] && typeof diskOverrides[projectKey] === 'object'
757
- ? diskOverrides[projectKey]
758
- : {};
759
- const memoryProject = memoryOverrides[projectKey] && typeof memoryOverrides[projectKey] === 'object'
760
- ? memoryOverrides[projectKey]
761
- : {};
762
- // Disk wins for untouched snapshot entries. Only an override this
763
- // process explicitly toggled may replace the in-lock disk value.
764
- const mergedProject = { ...memoryProject, ...diskProject };
765
- for (const serverName of Object.keys(memoryProject)) {
766
- const pairKey = mcpProjectOverrideDirtyKey(projectKey, serverName);
767
- const dirty = dirtyMcpProjectOverrides.get(pairKey);
768
- if (dirty) {
769
- const appliedValue = { enabled: dirty.enabled };
770
- mergedProject[serverName] = appliedValue;
771
- appliedDirty.push({ pairKey, seq: dirty.seq, appliedValue });
772
- }
773
- }
774
- for (const serverName of Object.keys(mergedProject)) {
775
- if (!Object.prototype.hasOwnProperty.call(mcpServers, serverName)) delete mergedProject[serverName];
776
- }
777
- if (Object.keys(mergedProject).length > 0) mcpProjectOverrides[projectKey] = mergedProject;
778
- }
779
723
  const next = {
780
724
  ...existingRaw,
781
725
  providers: persistedProviders,
782
726
  mcpServers,
783
- mcpProjectOverrides,
784
727
  presets,
785
728
  default: config.default || null,
786
729
  maintenance: canonicalRoutes.maintenance,
@@ -810,19 +753,16 @@ function buildAgentSaveBuilder(config, appliedDirty) {
810
753
  delete next.capabilities;
811
754
  delete next.defaultProvider;
812
755
  delete next.guide;
756
+ delete next.mcpProjectOverrides;
813
757
  return next;
814
758
  };
815
759
  }
816
760
  export function saveConfig(config) {
817
- const appliedDirty = [];
818
- persistAgentConfig(buildAgentSaveBuilder(config, appliedDirty));
819
- clearDurablySavedMcpProjectOverrides(appliedDirty);
761
+ persistAgentConfig(buildAgentSaveBuilder(config));
820
762
  }
821
763
  // Async twin used by the debounced config-save flush timer.
822
764
  export async function saveConfigAsync(config) {
823
- const appliedDirty = [];
824
- await persistAgentConfigAsync(buildAgentSaveBuilder(config, appliedDirty));
825
- clearDurablySavedMcpProjectOverrides(appliedDirty);
765
+ await persistAgentConfigAsync(buildAgentSaveBuilder(config));
826
766
  }
827
767
  // --- Preset helpers ---
828
768
  // preset shape: { id, name, type: 'agent', provider, model, effort?, fast?, tools? }
@@ -0,0 +1,153 @@
1
+ import assert from 'node:assert/strict';
2
+ import { existsSync, mkdirSync, mkdtempSync, rmSync, writeFileSync } from 'node:fs';
3
+ import { tmpdir } from 'node:os';
4
+ import { join } from 'node:path';
5
+ import test from 'node:test';
6
+
7
+ import {
8
+ collectSkillsCached,
9
+ invalidateSkillsCache,
10
+ loadSkillResource,
11
+ } from './collect.mjs';
12
+
13
+ test('discovers global standard skill folders and ignores project-local skills and reference Markdown', () => {
14
+ const root = mkdtempSync(join(tmpdir(), 'mixdog-standard-skill-'));
15
+ const previousDataDir = process.env.MIXDOG_DATA_DIR;
16
+ process.env.MIXDOG_DATA_DIR = join(root, 'data');
17
+ try {
18
+ const cwd = join(root, 'project');
19
+ const skillDir = join(process.env.MIXDOG_DATA_DIR, 'skills', 'copied-skill');
20
+ const projectSkillDir = join(cwd, '.mixdog', 'skills', 'project-only');
21
+ mkdirSync(join(skillDir, 'references'), { recursive: true });
22
+ mkdirSync(join(skillDir, 'scripts'), { recursive: true });
23
+ mkdirSync(projectSkillDir, { recursive: true });
24
+ writeFileSync(join(skillDir, 'SKILL.md'), [
25
+ '---',
26
+ 'name: copied-skill',
27
+ 'description: >',
28
+ ' Use when a copied standard skill',
29
+ ' should be discovered.',
30
+ 'metadata:',
31
+ ' source: external',
32
+ '---',
33
+ '',
34
+ '# Instructions',
35
+ '',
36
+ 'Read `references/guide.md` and run `scripts/check.py` when needed.',
37
+ '',
38
+ ].join('\n'));
39
+ writeFileSync(join(skillDir, 'references', 'guide.md'), [
40
+ '---',
41
+ 'name: not-a-skill',
42
+ 'description: This reference must never become a skill.',
43
+ '---',
44
+ '',
45
+ '# Reference',
46
+ '',
47
+ ].join('\n'));
48
+ writeFileSync(join(skillDir, 'scripts', 'check.py'), 'print("ok")\n');
49
+ writeFileSync(join(projectSkillDir, 'SKILL.md'), [
50
+ '---',
51
+ 'name: project-only',
52
+ 'description: This project-local skill must be ignored.',
53
+ '---',
54
+ '',
55
+ '# Project instructions',
56
+ '',
57
+ ].join('\n'));
58
+
59
+ invalidateSkillsCache(cwd);
60
+ const copied = collectSkillsCached(cwd)
61
+ .find((skill) => skill.name === 'copied-skill');
62
+ assert.ok(copied);
63
+ assert.equal(copied.description,
64
+ 'Use when a copied standard skill should be discovered.');
65
+ assert.equal(collectSkillsCached(cwd)
66
+ .some((skill) => skill.name === 'not-a-skill'), false);
67
+ assert.equal(collectSkillsCached(cwd)
68
+ .some((skill) => skill.name === 'project-only'), false);
69
+
70
+ const loaded = loadSkillResource('copied-skill', cwd);
71
+ assert.match(loaded.content, /^# Instructions/);
72
+ assert.equal(loaded.dir, skillDir);
73
+ assert.equal(existsSync(join(loaded.dir, 'references', 'guide.md')), true);
74
+ assert.equal(existsSync(join(loaded.dir, 'scripts', 'check.py')), true);
75
+ } finally {
76
+ invalidateSkillsCache(join(root, 'project'));
77
+ if (previousDataDir === undefined) delete process.env.MIXDOG_DATA_DIR;
78
+ else process.env.MIXDOG_DATA_DIR = previousDataDir;
79
+ rmSync(root, { recursive: true, force: true });
80
+ }
81
+ });
82
+
83
+ test('loads skills only from globally enabled plugins', () => {
84
+ const root = mkdtempSync(join(tmpdir(), 'mixdog-plugin-skill-'));
85
+ const previousDataDir = process.env.MIXDOG_DATA_DIR;
86
+ process.env.MIXDOG_DATA_DIR = join(root, 'data');
87
+ try {
88
+ const enabledRoot = join(root, 'enabled-plugin');
89
+ const disabledRoot = join(root, 'disabled-plugin');
90
+ const writeSkill = (pluginRoot, name) => {
91
+ const dir = join(pluginRoot, 'skills', name);
92
+ mkdirSync(dir, { recursive: true });
93
+ writeFileSync(join(dir, 'SKILL.md'), [
94
+ '---',
95
+ `name: ${name}`,
96
+ `description: Use when ${name} is needed.`,
97
+ '---',
98
+ '',
99
+ '# Instructions',
100
+ '',
101
+ ].join('\n'));
102
+ };
103
+ writeSkill(enabledRoot, 'enabled-plugin-skill');
104
+ writeSkill(disabledRoot, 'disabled-plugin-skill');
105
+ const registryDir = join(process.env.MIXDOG_DATA_DIR, 'plugins');
106
+ mkdirSync(registryDir, { recursive: true });
107
+ writeFileSync(join(registryDir, 'registry.json'), JSON.stringify({
108
+ plugins: [
109
+ { id: 'enabled', root: enabledRoot, enabled: true },
110
+ { id: 'disabled', root: disabledRoot, enabled: false },
111
+ ],
112
+ }));
113
+
114
+ invalidateSkillsCache();
115
+ const names = collectSkillsCached(join(root, 'any-project')).map((skill) => skill.name);
116
+ assert.equal(names.includes('enabled-plugin-skill'), true);
117
+ assert.equal(names.includes('disabled-plugin-skill'), false);
118
+ } finally {
119
+ invalidateSkillsCache();
120
+ if (previousDataDir === undefined) delete process.env.MIXDOG_DATA_DIR;
121
+ else process.env.MIXDOG_DATA_DIR = previousDataDir;
122
+ rmSync(root, { recursive: true, force: true });
123
+ }
124
+ });
125
+
126
+ test('requires a standard folder name that matches the manifest name', () => {
127
+ const root = mkdtempSync(join(tmpdir(), 'mixdog-mismatched-skill-'));
128
+ const previousDataDir = process.env.MIXDOG_DATA_DIR;
129
+ process.env.MIXDOG_DATA_DIR = join(root, 'data');
130
+ try {
131
+ const cwd = join(root, 'project');
132
+ const skillDir = join(process.env.MIXDOG_DATA_DIR, 'skills', 'folder-name');
133
+ mkdirSync(skillDir, { recursive: true });
134
+ writeFileSync(join(skillDir, 'SKILL.md'), [
135
+ '---',
136
+ 'name: different-name',
137
+ 'description: Use when testing mismatched folders.',
138
+ '---',
139
+ '',
140
+ '# Instructions',
141
+ '',
142
+ ].join('\n'));
143
+
144
+ invalidateSkillsCache(cwd);
145
+ assert.equal(collectSkillsCached(cwd)
146
+ .some((skill) => skill.name === 'different-name'), false);
147
+ } finally {
148
+ invalidateSkillsCache(join(root, 'project'));
149
+ if (previousDataDir === undefined) delete process.env.MIXDOG_DATA_DIR;
150
+ else process.env.MIXDOG_DATA_DIR = previousDataDir;
151
+ rmSync(root, { recursive: true, force: true });
152
+ }
153
+ });
@@ -1,12 +1,10 @@
1
1
  import { readFileSync, existsSync, readdirSync } from 'fs';
2
2
  import { homedir } from 'os';
3
- import { dirname, join } from 'path';
3
+ import { basename, dirname, join } from 'path';
4
4
  import { maxMtimeRecursive } from '../cache-mtime.mjs';
5
5
  import { resolvePluginData, mixdogRoot } from '../../../shared/plugin-paths.mjs';
6
- import {
7
- parseMarkdownFrontmatter,
8
- readMarkdownDocument,
9
- } from '../../../shared/markdown-frontmatter.mjs';
6
+ import { readMarkdownDocument } from '../../../shared/markdown-frontmatter.mjs';
7
+ import { parseSkillDocument } from '../../../shared/skill-document.mjs';
10
8
  import { loadConfig, normalizeSkillsConfig } from '../config.mjs';
11
9
 
12
10
  function skillsDisabled() {
@@ -14,8 +12,8 @@ function skillsDisabled() {
14
12
  }
15
13
 
16
14
  // --- mixdog asset roots (standalone CLI owns its own paths; never .claude) ---
17
- // Project-local: <cwd>/.mixdog/<kind>
18
- // Data-local: <mixdogData>/<kind> (standalone default: ~/.mixdog/data/<kind>)
15
+ // Skills are machine-global under <mixdogData>/skills. Project-local skill
16
+ // directories are intentionally outside the runtime resolution chain.
19
17
  function mixdogHome() {
20
18
  return process.env.MIXDOG_HOME || join(homedir(), '.mixdog');
21
19
  }
@@ -27,16 +25,8 @@ function mixdogGlobalDir(kind) {
27
25
  return join(process.env.MIXDOG_DATA_DIR || join(mixdogHome(), 'data'), kind);
28
26
  }
29
27
  }
30
- function mixdogProjectDir(projectDir, kind) {
31
- return projectDir ? join(projectDir, '.mixdog', kind) : null;
32
- }
33
- /** Ordered asset search dirs for a kind: project-local first, then global. */
34
- function mixdogAssetDirs(projectDir, kind) {
35
- const dirs = [];
36
- const local = mixdogProjectDir(projectDir, kind);
37
- if (local) dirs.push(local);
38
- dirs.push(mixdogGlobalDir(kind));
39
- return dirs;
28
+ function mixdogAssetDirs(_projectDir, kind) {
29
+ return [mixdogGlobalDir(kind)];
40
30
  }
41
31
  /**
42
32
  * Absolute path to the plugin registry file, or null when the data dir is
@@ -70,6 +60,8 @@ function pluginSkillDirs() {
70
60
  return [];
71
61
  const dirs = [];
72
62
  for (const entry of registry.plugins) {
63
+ if (entry?.enabled === false)
64
+ continue;
73
65
  const root = entry && typeof entry.root === 'string' ? entry.root : null;
74
66
  if (!root || !existsSync(root))
75
67
  continue;
@@ -85,19 +77,11 @@ function pluginSkillDirs() {
85
77
  */
86
78
  export function collectSkills(cwd) {
87
79
  if (skillsDisabled()) return [];
88
- // When cwd is null/missing (e.g. agent maintenance callers that pass
89
- // cwd:null on purpose so provider-cache shards don't fork per caller
90
- // workspace), skip project-scoped skills entirely — DO NOT fall back
91
- // to process.cwd(), which would leak the MCP launch dir into the
92
- // shard key and fragment the cache.
93
- const projectDir = (typeof cwd === 'string' && cwd.length > 0) ? cwd : null;
80
+ void cwd;
94
81
  const skills = [];
95
- // mixdog-owned only (never .claude): project-local <cwd>/.mixdog/skills
96
- // first, then user-global. When cwd is missing, only the global dir is
97
- // searched.
98
- const dirs = mixdogAssetDirs(projectDir, 'skills');
99
- // Plugin-provided skills load last so project-local and global mixdog
100
- // skill dirs keep precedence; `seen` below dedupes by frontmatter name.
82
+ const dirs = mixdogAssetDirs(null, 'skills');
83
+ // Plugin-provided skills load last so user-global skills keep precedence;
84
+ // `seen` below dedupes by frontmatter name.
101
85
  dirs.push(...pluginSkillDirs());
102
86
  const seen = new Set();
103
87
  for (const dir of dirs) {
@@ -106,21 +90,27 @@ export function collectSkills(cwd) {
106
90
  try {
107
91
  const files = readdirSync(dir, { recursive: true });
108
92
  for (const f of files) {
109
- if (!String(f).endsWith('.md'))
93
+ if (basename(String(f)) !== 'SKILL.md')
110
94
  continue;
111
95
  const filePath = join(dir, String(f));
112
96
  const content = readSafe(filePath);
113
97
  if (!content)
114
98
  continue;
115
- const fm = parseMarkdownFrontmatter(content);
116
- if (!fm.name)
99
+ let skill;
100
+ try {
101
+ skill = parseSkillDocument(content);
102
+ } catch {
117
103
  continue;
118
- if (seen.has(fm.name))
104
+ }
105
+ // Agent Skills requires the manifest name to match its folder.
106
+ if (basename(dirname(filePath)) !== skill.name)
119
107
  continue;
120
- seen.add(fm.name);
108
+ if (seen.has(skill.name))
109
+ continue;
110
+ seen.add(skill.name);
121
111
  skills.push({
122
- name: fm.name,
123
- description: fm.description || '',
112
+ name: skill.name,
113
+ description: skill.description,
124
114
  filePath,
125
115
  });
126
116
  }
@@ -167,10 +157,10 @@ const _mtimeCache = new Map();
167
157
  const _MTIME_TTL_MS = 2000;
168
158
  export function collectSkillsCached(cwd) {
169
159
  if (skillsDisabled()) return [];
170
- const key = cwd ?? '';
171
- const projectDir = (typeof cwd === 'string' && cwd.length > 0) ? cwd : null;
160
+ void cwd;
161
+ const key = 'global';
172
162
  // Same mixdog-owned dirs collectSkills() reads, used as the freshness gate.
173
- const skillsDirs = mixdogAssetDirs(projectDir, 'skills');
163
+ const skillsDirs = mixdogAssetDirs(null, 'skills');
174
164
  skillsDirs.push(...pluginSkillDirs());
175
165
  // registry.json itself gates plugin add/remove: removal deletes the
176
166
  // plugin's skills dir (so no dir mtime advances), but saveRegistry()
@@ -200,6 +190,11 @@ export function collectSkillsCached(cwd) {
200
190
  }
201
191
  return skills;
202
192
  }
193
+ export function invalidateSkillsCache(cwd) {
194
+ void cwd;
195
+ _skillsCache.clear();
196
+ _mtimeCache.clear();
197
+ }
203
198
  /**
204
199
  * Load full skill content by name.
205
200
  */
@@ -209,7 +204,12 @@ function loadSkillContent(name, cwd) {
209
204
  if (!skill)
210
205
  return null;
211
206
  const content = readSafe(skill.filePath);
212
- return content == null ? null : readMarkdownDocument(content).body;
207
+ if (content == null) return null;
208
+ try {
209
+ return parseSkillDocument(content).body;
210
+ } catch {
211
+ return null;
212
+ }
213
213
  }
214
214
 
215
215
  /**
@@ -226,11 +226,15 @@ export function loadSkillResource(name, cwd) {
226
226
  const content = readSafe(skill.filePath);
227
227
  if (content == null)
228
228
  return null;
229
- return {
230
- content: readMarkdownDocument(content).body,
231
- dir: dirname(skill.filePath),
232
- filePath: skill.filePath,
233
- };
229
+ try {
230
+ return {
231
+ content: parseSkillDocument(content).body,
232
+ dir: dirname(skill.filePath),
233
+ filePath: skill.filePath,
234
+ };
235
+ } catch {
236
+ return null;
237
+ }
234
238
  }
235
239
 
236
240
  function escapeSkillXmlText(value) {