mixdog 0.9.166 → 0.9.167

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 (221) hide show
  1. package/package.json +1 -1
  2. package/scripts/audit-usage-ledger.mjs +86 -0
  3. package/scripts/generate-voice-runtime-manifest.mjs +22 -17
  4. package/scripts/memory-chunk-quality.mjs +128 -0
  5. package/scripts/memory-layer2-check.mjs +133 -0
  6. package/scripts/model-catalog-audit.mjs +6 -0
  7. package/scripts/native-tool-download.mjs +14 -0
  8. package/scripts/prepare-native-assets.mjs +2 -7
  9. package/scripts/tool-failures.mjs +7 -2
  10. package/scripts/verify-release-assets.mjs +48 -90
  11. package/src/defaults/skills/browser-use/SKILL.md +33 -18
  12. package/src/defaults/skills/computer-use/SKILL.md +16 -8
  13. package/src/lib/keychain-cjs.cjs +0 -1
  14. package/src/lib/rules-builder.cjs +4 -1
  15. package/src/rules/agent/40-cycle1-agent.md +21 -13
  16. package/src/rules/lead/01-general.md +8 -3
  17. package/src/rules/shared/10-tool-workflow.md +20 -11
  18. package/src/rules/shared/30-exploration.md +11 -4
  19. package/src/rules/shared/40-editing.md +2 -3
  20. package/src/rules/shared/50-execution.md +6 -3
  21. package/src/rules/shared/60-verification.md +3 -2
  22. package/src/rules/shared/70-delivery.md +0 -4
  23. package/src/runtime/agent/orchestrator/agent-runtime/agent-dispatch.mjs +51 -45
  24. package/src/runtime/agent/orchestrator/agent-runtime/agent-progress-watchdog.mjs +3 -4
  25. package/src/runtime/agent/orchestrator/agent-trace-format.mjs +44 -7
  26. package/src/runtime/agent/orchestrator/agent-trace.mjs +9 -0
  27. package/src/runtime/agent/orchestrator/config.mjs +9 -4
  28. package/src/runtime/agent/orchestrator/context/collect.mjs +25 -1055
  29. package/src/runtime/agent/orchestrator/context/deferred-tools.mjs +272 -0
  30. package/src/runtime/agent/orchestrator/context/role-instructions.mjs +296 -0
  31. package/src/runtime/agent/orchestrator/context/skill-catalog.mjs +523 -0
  32. package/src/runtime/agent/orchestrator/providers/account-pool.mjs +7 -2
  33. package/src/runtime/agent/orchestrator/providers/anthropic-sse.mjs +13 -14
  34. package/src/runtime/agent/orchestrator/providers/antigravity-oauth-catalog.mjs +214 -0
  35. package/src/runtime/agent/orchestrator/providers/antigravity-oauth-login.mjs +220 -116
  36. package/src/runtime/agent/orchestrator/providers/antigravity-oauth-tokens.mjs +74 -44
  37. package/src/runtime/agent/orchestrator/providers/antigravity-oauth.mjs +191 -29
  38. package/src/runtime/agent/orchestrator/providers/antigravity-request.mjs +13 -12
  39. package/src/runtime/agent/orchestrator/providers/cursor-wire-guards.mjs +1 -9
  40. package/src/runtime/agent/orchestrator/providers/cursor-wire-protobuf.mjs +5 -2
  41. package/src/runtime/agent/orchestrator/providers/cursor-wire.mjs +48 -25
  42. package/src/runtime/agent/orchestrator/providers/gemini-schema.mjs +28 -2
  43. package/src/runtime/agent/orchestrator/providers/gemini-stream.mjs +3 -1
  44. package/src/runtime/agent/orchestrator/providers/grok-oauth-tokens.mjs +1 -8
  45. package/src/runtime/agent/orchestrator/providers/lib/action-input-contract.mjs +32 -0
  46. package/src/runtime/agent/orchestrator/providers/lib/anthropic-request-utils.mjs +21 -6
  47. package/src/runtime/agent/orchestrator/providers/lib/grok-tool-schema.mjs +61 -13
  48. package/src/runtime/agent/orchestrator/providers/model-catalog-projection.mjs +6 -1
  49. package/src/runtime/agent/orchestrator/providers/model-catalog.mjs +123 -118
  50. package/src/runtime/agent/orchestrator/providers/model-pricing-rates.mjs +54 -0
  51. package/src/runtime/agent/orchestrator/providers/oauth-credential-probes.mjs +5 -2
  52. package/src/runtime/agent/orchestrator/providers/oauth-usage.mjs +34 -3
  53. package/src/runtime/agent/orchestrator/providers/openai-codex-endpoints.mjs +21 -0
  54. package/src/runtime/agent/orchestrator/providers/openai-oauth-catalog.mjs +154 -0
  55. package/src/runtime/agent/orchestrator/providers/openai-oauth-http-sse.mjs +2 -1
  56. package/src/runtime/agent/orchestrator/providers/openai-oauth-tokens.mjs +322 -0
  57. package/src/runtime/agent/orchestrator/providers/openai-oauth.mjs +243 -702
  58. package/src/runtime/agent/orchestrator/providers/openai-responses-payload.mjs +2 -2
  59. package/src/runtime/agent/orchestrator/providers/openai-startup-prewarm.mjs +221 -0
  60. package/src/runtime/agent/orchestrator/providers/openai-ws-stream.mjs +3 -2
  61. package/src/runtime/agent/orchestrator/providers/openai-ws.mjs +1 -1
  62. package/src/runtime/agent/orchestrator/providers/provider-catalog-cache.mjs +19 -0
  63. package/src/runtime/agent/orchestrator/providers/provider-model-identities.mjs +40 -0
  64. package/src/runtime/agent/orchestrator/session/agent-loop.mjs +120 -710
  65. package/src/runtime/agent/orchestrator/session/cache/read-cache.mjs +36 -15
  66. package/src/runtime/agent/orchestrator/session/cache/util.mjs +9 -0
  67. package/src/runtime/agent/orchestrator/session/compact/execution-tail.mjs +17 -4
  68. package/src/runtime/agent/orchestrator/session/compact/runner.mjs +36 -31
  69. package/src/runtime/agent/orchestrator/session/compact.mjs +4 -5
  70. package/src/runtime/agent/orchestrator/session/context-compaction-policy.mjs +6 -5
  71. package/src/runtime/agent/orchestrator/session/eager-dispatch.mjs +8 -6
  72. package/src/runtime/agent/orchestrator/session/loop/assistant-commit.mjs +121 -0
  73. package/src/runtime/agent/orchestrator/session/loop/compact-policy.mjs +2 -3
  74. package/src/runtime/agent/orchestrator/session/loop/diagnostics.mjs +74 -0
  75. package/src/runtime/agent/orchestrator/session/loop/fresh-context.mjs +44 -17
  76. package/src/runtime/agent/orchestrator/session/loop/no-tool-turn.mjs +266 -0
  77. package/src/runtime/agent/orchestrator/session/loop/request-boundary.mjs +111 -0
  78. package/src/runtime/agent/orchestrator/session/loop/request-projection.mjs +99 -0
  79. package/src/runtime/agent/orchestrator/session/loop/steering.mjs +91 -0
  80. package/src/runtime/agent/orchestrator/session/loop/usage.mjs +36 -0
  81. package/src/runtime/agent/orchestrator/session/manager/compaction-runner.mjs +3 -3
  82. package/src/runtime/agent/orchestrator/session/manager/runtime-liveness.mjs +12 -0
  83. package/src/runtime/agent/orchestrator/session/manager/session-lifecycle.mjs +20 -11
  84. package/src/runtime/agent/orchestrator/session/pre-send-compact.mjs +3 -5
  85. package/src/runtime/agent/orchestrator/session/send-with-recovery.mjs +9 -3
  86. package/src/runtime/agent/orchestrator/session/store-summary-reader.mjs +39 -0
  87. package/src/runtime/agent/orchestrator/session/store-transcript-cache.mjs +3 -2
  88. package/src/runtime/agent/orchestrator/session/tool-batch.mjs +21 -3
  89. package/src/runtime/agent/orchestrator/tools/builtin/arg-guard.mjs +1 -1
  90. package/src/runtime/agent/orchestrator/tools/builtin/bash-tool.mjs +2 -3
  91. package/src/runtime/agent/orchestrator/tools/builtin/builtin-tools.mjs +25 -25
  92. package/src/runtime/agent/orchestrator/tools/builtin/git-command-tool.mjs +62 -10
  93. package/src/runtime/agent/orchestrator/tools/builtin/path-utils.mjs +17 -0
  94. package/src/runtime/agent/orchestrator/tools/builtin/read-batch.mjs +61 -4
  95. package/src/runtime/agent/orchestrator/tools/builtin/read-single-tool.mjs +13 -9
  96. package/src/runtime/agent/orchestrator/tools/builtin/read-snapshot-runtime.mjs +12 -2
  97. package/src/runtime/agent/orchestrator/tools/builtin/read-streaming.mjs +3 -2
  98. package/src/runtime/agent/orchestrator/tools/builtin/read-tool.mjs +8 -3
  99. package/src/runtime/agent/orchestrator/tools/builtin/runtime-capabilities.mjs +69 -6
  100. package/src/runtime/agent/orchestrator/tools/builtin.mjs +14 -17
  101. package/src/runtime/agent/orchestrator/tools/code-graph/dispatch.mjs +1 -1
  102. package/src/runtime/agent/orchestrator/tools/code-graph/source-access.mjs +1 -1
  103. package/src/runtime/agent/orchestrator/tools/code-graph-tool-defs.mjs +6 -6
  104. package/src/runtime/agent/orchestrator/tools/patch-tool-defs.mjs +1 -1
  105. package/src/runtime/bridge-discovery.mjs +8 -0
  106. package/src/runtime/browser-bridge/action-schema.mjs +37 -43
  107. package/src/runtime/browser-bridge/client.mjs +111 -99
  108. package/src/runtime/browser-bridge/input-fields.mjs +187 -0
  109. package/src/runtime/browser-bridge/tool-defs.mjs +12 -227
  110. package/src/runtime/computer-bridge/action-schema.mjs +2 -46
  111. package/src/runtime/computer-bridge/client.mjs +69 -77
  112. package/src/runtime/computer-bridge/core-actions.mjs +7 -5
  113. package/src/runtime/computer-bridge/error-recovery.mjs +41 -37
  114. package/src/runtime/computer-bridge/tool-defs.mjs +1 -1
  115. package/src/runtime/github/commands.mjs +4 -1
  116. package/src/runtime/github/contract.mjs +1 -1
  117. package/src/runtime/github/tool.mjs +2 -2
  118. package/src/runtime/media/adapters/antigravity-image.mjs +72 -0
  119. package/src/runtime/media/adapters/gemini-image.mjs +20 -11
  120. package/src/runtime/media/auth.mjs +12 -0
  121. package/src/runtime/media/catalog.mjs +30 -10
  122. package/src/runtime/media/jobs.mjs +4 -0
  123. package/src/runtime/media/lanes.mjs +40 -31
  124. package/src/runtime/memory/index.mjs +2 -9
  125. package/src/runtime/memory/lib/compact-handoff.mjs +26 -18
  126. package/src/runtime/memory/lib/memory-chunk-audit.mjs +41 -0
  127. package/src/runtime/memory/lib/memory-chunk-quality.mjs +388 -0
  128. package/src/runtime/memory/lib/memory-cycle1.mjs +95 -394
  129. package/src/runtime/memory/lib/memory-recall-scope-filter.mjs +29 -4
  130. package/src/runtime/memory/lib/memory-recall-store.mjs +8 -12
  131. package/src/runtime/memory/lib/memory-retrievers.mjs +8 -11
  132. package/src/runtime/memory/lib/memory.mjs +2 -0
  133. package/src/runtime/memory/lib/query-handlers.mjs +98 -144
  134. package/src/runtime/memory/lib/query-ranking.mjs +23 -9
  135. package/src/runtime/memory/lib/recall-format.mjs +37 -27
  136. package/src/runtime/memory/lib/session-ingest-runtime.mjs +14 -8
  137. package/src/runtime/memory/lib/session-ingest.mjs +45 -84
  138. package/src/runtime/memory/lib/transcript-ingest.mjs +19 -21
  139. package/src/runtime/memory/tool-defs.mjs +2 -2
  140. package/src/runtime/office/authoring/pptx-author-session.mjs +3 -7
  141. package/src/runtime/office/core/office-actions-batch.mjs +2 -8
  142. package/src/runtime/office/core/office-actions-open.mjs +124 -0
  143. package/src/runtime/office/core/office-actions-read.mjs +127 -0
  144. package/src/runtime/office/core/office-core.mjs +80 -1
  145. package/src/runtime/office/core/office-sessions.mjs +116 -138
  146. package/src/runtime/office/index.mjs +18 -271
  147. package/src/runtime/office/pdf/pdf-analysis.mjs +2 -2
  148. package/src/runtime/office/pdf/pdf-limits.mjs +2 -0
  149. package/src/runtime/office/tool-defs.mjs +7 -6
  150. package/src/runtime/shared/agent-route-config.mjs +8 -8
  151. package/src/runtime/shared/child-spawn-gate.mjs +10 -13
  152. package/src/runtime/shared/config.mjs +2 -7
  153. package/src/runtime/shared/env.mjs +3 -3
  154. package/src/runtime/shared/err-text.mjs +1 -1
  155. package/src/runtime/shared/extension-scopes.mjs +3 -2
  156. package/src/runtime/shared/heap-cap.mjs +10 -6
  157. package/src/runtime/shared/llm/cost.mjs +35 -10
  158. package/src/runtime/shared/llm/usage-accounting.mjs +11 -1
  159. package/src/runtime/shared/llm/usage-context.mjs +6 -0
  160. package/src/runtime/shared/llm/usage-ledger-import.mjs +5 -0
  161. package/src/runtime/shared/llm/usage-ledger-repair.mjs +109 -0
  162. package/src/runtime/shared/llm/usage-ledger.mjs +59 -20
  163. package/src/runtime/shared/llm/usage-pricing-refresh.mjs +26 -0
  164. package/src/runtime/shared/llm/usage-rollup.mjs +17 -15
  165. package/src/runtime/shared/native-asset.mjs +3 -4
  166. package/src/runtime/shared/object.mjs +4 -0
  167. package/src/runtime/shared/pg-schema-db.mjs +21 -0
  168. package/src/runtime/shared/plugin-metadata.mjs +3 -1
  169. package/src/runtime/shared/resource-admission.mjs +1 -6
  170. package/src/runtime/shared/schedules-db.mjs +8 -24
  171. package/src/runtime/shared/schema-value-error.d.mts +2 -0
  172. package/src/runtime/shared/schema-value-error.mjs +68 -0
  173. package/src/runtime/shared/singleton-owner.mjs +11 -24
  174. package/src/runtime/shared/staged-update.mjs +1 -10
  175. package/src/runtime/shared/tool-card-model.mjs +8 -5
  176. package/src/runtime/shared/tool-execution-contract.mjs +1 -1
  177. package/src/runtime/shared/tool-surface.mjs +1 -5
  178. package/src/runtime/shared/user-data-guard.mjs +1 -4
  179. package/src/runtime/shared/webhooks-db.mjs +8 -24
  180. package/src/runtime/web-search/index.mjs +32 -8
  181. package/src/runtime/web-search/tool-defs.mjs +4 -4
  182. package/src/session-runtime/config-lifecycle.mjs +34 -0
  183. package/src/session-runtime/context-status.mjs +19 -0
  184. package/src/session-runtime/cwd-plugins.mjs +12 -29
  185. package/src/session-runtime/goal-tasks.mjs +2 -2
  186. package/src/session-runtime/goal-tool-defs.mjs +3 -2
  187. package/src/session-runtime/internal-tool-executor.mjs +7 -0
  188. package/src/session-runtime/lifecycle-api.mjs +5 -11
  189. package/src/session-runtime/notification-bus.mjs +41 -0
  190. package/src/session-runtime/prewarm.mjs +22 -0
  191. package/src/session-runtime/runtime-bootstrap.mjs +90 -0
  192. package/src/session-runtime/runtime-core.mjs +88 -202
  193. package/src/session-runtime/runtime-feature-gates.mjs +37 -0
  194. package/src/session-runtime/session-lifecycle.mjs +5 -5
  195. package/src/session-runtime/session-turn-api.mjs +1 -12
  196. package/src/session-runtime/setup-tool/executor.mjs +13 -5
  197. package/src/session-runtime/setup-tool/tool-defs.mjs +52 -46
  198. package/src/session-runtime/tool-defs.mjs +73 -4
  199. package/src/session-runtime/usage-stats-api.mjs +4 -1
  200. package/src/session-runtime/warmup-schedulers.mjs +8 -2
  201. package/src/session-runtime/workflow.mjs +14 -0
  202. package/src/standalone/channel-session-router.mjs +3 -1
  203. package/src/standalone/channel-worker.mjs +18 -9
  204. package/src/standalone/daemon-crash-capture.mjs +534 -0
  205. package/src/standalone/daemon-telemetry.mjs +175 -0
  206. package/src/standalone/daemon.mjs +31 -21
  207. package/src/standalone/session-client.mjs +31 -23
  208. package/src/standalone/usage-dashboard-model.mjs +2 -0
  209. package/src/standalone/usage-stats-hours.mjs +18 -11
  210. package/src/standalone/usage-stats-model.mjs +39 -28
  211. package/src/standalone/usage-stats-period.mjs +43 -45
  212. package/src/tui/app/use-transcript-window.mjs +1 -1
  213. package/src/tui/components/StatusLine.jsx +13 -42
  214. package/src/tui/dist/index.mjs +256 -239
  215. package/src/tui/session/session-api-ext.mjs +44 -41
  216. package/src/tui/session/session-api.mjs +4 -0
  217. package/src/tui/session/tool-card-results.mjs +25 -78
  218. package/src/tui/session/tool-result-status.mjs +94 -1
  219. package/src/tui/session/turn-aggregate-cards.mjs +17 -54
  220. package/src/ui/statusline-format.mjs +6 -26
  221. package/src/workflows/default/WORKFLOW.md +5 -0
@@ -1,1055 +1,25 @@
1
- import { readFileSync, existsSync, readdirSync } from 'fs';
2
- import { basename, dirname, join } from 'path';
3
- import { maxMtimeRecursive } from '../cache-mtime.mjs';
4
- import { mixdogHome, resolvePluginData, mixdogRoot } from '../../../shared/plugin-paths.mjs';
5
- import { pluginSkillsRoots } from '../../../shared/plugin-manifest.mjs';
6
- import { readMarkdownDocument } from '../../../shared/markdown-frontmatter.mjs';
7
- import { parseSkillDocument } from '../../../shared/skill-document.mjs';
8
- import { readSkillToolDependencies, skillToolDependenciesRoot } from '../../../shared/skill-tool-dependencies.mjs';
9
- import { loadConfig, normalizeSkillsConfig } from '../config.mjs';
10
- import { builtinFeatureActive, withGrandfatheredBuiltins } from '../../../../session-runtime/builtin-features.mjs';
11
- import { extensionScopesFromConfig, skillAllowedForCwd } from '../../../shared/extension-scopes.mjs';
12
- import { currentSkillContext, latestSkillBodies, skillMessageText } from './skill-state.mjs';
13
-
14
- function skillsDisabled() {
15
- return /^(?:1|true|on|yes)$/i.test(String(process.env.MIXDOG_DISABLE_SKILLS || ''));
16
- }
17
-
18
- // --- mixdog asset roots (standalone CLI owns its own paths; never .claude) ---
19
- // Skills are machine-global under <mixdogData>/skills. Project-local skill
20
- // directories are intentionally outside the runtime resolution chain.
21
- function mixdogGlobalDir(kind) {
22
- try {
23
- return join(resolvePluginData(), kind);
24
- } catch {
25
- return join(process.env.MIXDOG_DATA_DIR || join(mixdogHome(), 'data'), kind);
26
- }
27
- }
28
- function mixdogAssetDirs(_projectDir, kind) {
29
- return [mixdogGlobalDir(kind)];
30
- }
31
- /**
32
- * The package's own bundle (src/defaults/) is a plugin root like any other:
33
- * `plugin.json` + `skills/<name>/SKILL.md`, read in place through the same
34
- * manifest resolver as installed plugins, never copied — a release updates it
35
- * immediately, and a user-global or plugin skill of the same name shadows it.
36
- * It is not in the registry: the Built-in settings panel owns its UI.
37
- */
38
- function builtinPluginRoot() {
39
- return join(mixdogRoot(), 'defaults');
40
- }
41
- function builtinSkillDirs() {
42
- return pluginSkillsRoots(builtinPluginRoot()).filter((dir) => existsSync(dir));
43
- }
44
- /**
45
- * Absolute path to the plugin registry file, or null when the data dir is
46
- * unresolvable. Included in the skills mtime gate so plugin add/remove
47
- * (which rewrites registry.json) invalidates the cached skill list even
48
- * when no surviving skills dir got a newer mtime.
49
- */
50
- function pluginRegistryPath() {
51
- try {
52
- return join(resolvePluginData(), 'plugins', 'registry.json');
53
- } catch {
54
- return null;
55
- }
56
- }
57
- /**
58
- * Read `<resolvePluginData()>/plugins/registry.json` (safe JSON parse, ignore
59
- * errors) and yield every existing skill root of each enabled registered
60
- * plugin: `<root>/skills` plus the manifest `skills` path, from the same
61
- * resolver the Plugins panel counts with, so the two never disagree.
62
- */
63
- function pluginSkillDirs() {
64
- const registryPath = pluginRegistryPath();
65
- if (!registryPath)
66
- return [];
67
- let registry;
68
- try {
69
- registry = JSON.parse(readFileSync(registryPath, 'utf-8'));
70
- } catch {
71
- return [];
72
- }
73
- if (!registry || !Array.isArray(registry.plugins))
74
- return [];
75
- const dirs = [];
76
- for (const entry of registry.plugins) {
77
- if (entry?.enabled === false)
78
- continue;
79
- const root = entry && typeof entry.root === 'string' ? entry.root : null;
80
- if (!root || !existsSync(root))
81
- continue;
82
- const plugin = String(entry.id || entry.name || '') || null;
83
- for (const skillsDir of pluginSkillsRoots(root)) {
84
- if (existsSync(skillsDir))
85
- dirs.push({ dir: skillsDir, plugin });
86
- }
87
- }
88
- return dirs;
89
- }
90
- /**
91
- * Collect available skills (frontmatter only — token efficient).
92
- * Full content is read on demand when a skill is loaded.
93
- */
94
- export function collectSkills(cwd) {
95
- if (skillsDisabled()) return [];
96
- void cwd;
97
- const skills = [];
98
- // Plugin-provided skills load after user-global ones and built-ins last,
99
- // so the user keeps precedence; `seen` below dedupes by frontmatter name.
100
- // Each entry remembers its owner so a plugin's skills can be shown and
101
- // toggled with the plugin instead of as loose entries.
102
- const sources = [
103
- ...mixdogAssetDirs(null, 'skills').map((dir) => ({ dir, source: 'global', plugin: null })),
104
- ...pluginSkillDirs().map(({ dir, plugin }) => ({ dir, source: 'plugin', plugin })),
105
- ...builtinSkillDirs().map((dir) => ({ dir, source: 'builtin', plugin: null })),
106
- ];
107
- const seen = new Set();
108
- for (const { dir, source, plugin } of sources) {
109
- if (!existsSync(dir))
110
- continue;
111
- try {
112
- const files = readdirSync(dir, { recursive: true });
113
- for (const f of files) {
114
- if (basename(String(f)) !== 'SKILL.md')
115
- continue;
116
- const filePath = join(dir, String(f));
117
- const content = readSafe(filePath);
118
- if (!content)
119
- continue;
120
- let skill;
121
- try {
122
- skill = parseSkillDocument(content);
123
- } catch {
124
- continue;
125
- }
126
- // Agent Skills requires the manifest name to match its folder.
127
- if (basename(dirname(filePath)) !== skill.name)
128
- continue;
129
- if (seen.has(skill.name))
130
- continue;
131
- seen.add(skill.name);
132
- skills.push({
133
- name: skill.name,
134
- description: skill.description,
135
- whenToUse: skill.whenToUse,
136
- filePath,
137
- source,
138
- plugin,
139
- requires: requiredFeatures(skill.frontmatter),
140
- ...readSkillToolDependencies(filePath, skill.frontmatter),
141
- });
142
- }
143
- }
144
- catch { /* ignore */ }
145
- }
146
- return skills;
147
- }
148
-
149
- function normalizeSkillNameKey(name) {
150
- return String(name || '').trim().toLowerCase();
151
- }
152
-
153
- /**
154
- * `metadata.requires` in the frontmatter names the built-in features a skill
155
- * depends on (`office`, `git`, `memory`, `webSearch`). A skill that describes
156
- * how to drive a tool must not be offered while that tool is uninstalled or
157
- * switched off, or the model is pointed at a call that cannot succeed.
158
- */
159
- function requiredFeatures(frontmatter) {
160
- const metadata = frontmatter?.metadata;
161
- const raw = metadata && typeof metadata === 'object' ? metadata.requires : null;
162
- const list = Array.isArray(raw) ? raw : raw == null ? [] : String(raw).split(',');
163
- return list.map((value) => String(value || '').trim()).filter(Boolean);
164
- }
165
-
166
- function missingFeature(skill, config) {
167
- const requires = Array.isArray(skill?.requires) ? skill.requires : [];
168
- return requires.find((feature) => !builtinFeatureActive(config, feature)) || null;
169
- }
170
-
171
- /** Config for feature gating. A profile that predates the `builtins` section
172
- * is grandfathered as installed exactly like the daemon does at adoption, so
173
- * the skill list and the tool surface agree. */
174
- function featureConfig(config = null) {
175
- return withGrandfatheredBuiltins(config || loadConfig({ secrets: false }));
176
- }
177
-
178
- function getDisabledSkillNameSet(config = null) {
179
- const cfg = config || loadConfig({ secrets: false });
180
- const keys = normalizeSkillsConfig(cfg.skills).disabled
181
- .map((n) => normalizeSkillNameKey(n))
182
- .filter(Boolean);
183
- return new Set(keys);
184
- }
185
-
186
- // Feature-owned bundle entries follow their parent, including when an older
187
- // profile still has their former individual OFF preference. User overrides
188
- // and standalone built-ins retain their independent preference.
189
- function followsBuiltinFeature(skill) {
190
- return skill?.source === 'builtin' && Array.isArray(skill.requires) && skill.requires.length > 0;
191
- }
192
-
193
- export function isSkillDisabled(name, config = null) {
194
- const n = normalizeSkillNameKey(name);
195
- if (!n) return false;
196
- const skill = collectSkillsCached(null).find((entry) => normalizeSkillNameKey(entry.name) === n);
197
- if (!followsBuiltinFeature(skill) && getDisabledSkillNameSet(config).has(n)) return true;
198
- return Boolean(skill && missingFeature(skill, featureConfig(config)));
199
- }
200
-
201
- /** The built-in feature a skill needs that is not active, or null. */
202
- export function skillMissingFeature(name, config = null) {
203
- const n = normalizeSkillNameKey(name);
204
- const skill = collectSkillsCached(null).find((entry) => normalizeSkillNameKey(entry.name) === n);
205
- return skill ? missingFeature(skill, featureConfig(config)) : null;
206
- }
207
-
208
- /**
209
- * Skills a session may see: not switched off, feature available, and — when
210
- * `cwd` is given — inside the skill's (or its plugin's) project scope. Without
211
- * a cwd only global skills pass, since a scoped skill belongs to a project.
212
- */
213
- export function filterSkillsExcludingDisabled(skills, config = null, cwd = null) {
214
- if (skillsDisabled()) return [];
215
- const cfg = featureConfig(config);
216
- const disabled = getDisabledSkillNameSet(cfg);
217
- const scopes = extensionScopesFromConfig(cfg);
218
- return (Array.isArray(skills) ? skills : []).filter((s) => {
219
- const key = normalizeSkillNameKey(s?.name);
220
- return key && (followsBuiltinFeature(s) || !disabled.has(key))
221
- && !missingFeature(s, cfg) && skillAllowedForCwd(scopes, s, cwd);
222
- });
223
- }
224
-
225
- export function collectPromptSkillsCached(cwd, config = null) {
226
- return filterSkillsExcludingDisabled(collectSkillsCached(cwd), config, cwd);
227
- }
228
- // --- Skill cache (mtime-based, keyed by cwd) ---
229
- const _skillsCache = new Map();
230
- const _mtimeCache = new Map();
231
- const _MTIME_TTL_MS = 2000;
232
- export function collectSkillsCached(cwd) {
233
- if (skillsDisabled()) return [];
234
- void cwd;
235
- const key = 'global';
236
- // Same mixdog-owned dirs collectSkills() reads, used as the freshness gate.
237
- const skillsDirs = mixdogAssetDirs(null, 'skills');
238
- skillsDirs.push(skillToolDependenciesRoot());
239
- skillsDirs.push(...pluginSkillDirs().map(({ dir }) => dir), ...builtinSkillDirs());
240
- // registry.json itself gates plugin add/remove: removal deletes the
241
- // plugin's skills dir (so no dir mtime advances), but saveRegistry()
242
- // always rewrites this file. maxMtimeRecursive stats plain files directly.
243
- const registryPath = pluginRegistryPath();
244
- if (registryPath)
245
- skillsDirs.push(registryPath);
246
- let mtime;
247
- const mtimeCached = _mtimeCache.get(key);
248
- if (mtimeCached && Date.now() - mtimeCached.checkedAt < _MTIME_TTL_MS) {
249
- mtime = mtimeCached.mtime;
250
- } else {
251
- mtime = maxMtimeRecursive(skillsDirs);
252
- _mtimeCache.set(key, { mtime, checkedAt: Date.now() });
253
- if (_mtimeCache.size > 16) {
254
- _mtimeCache.delete(_mtimeCache.keys().next().value);
255
- }
256
- }
257
- const entry = _skillsCache.get(key);
258
- if (entry && entry.mtime >= mtime) {
259
- return entry.value;
260
- }
261
- const skills = collectSkills(cwd);
262
- _skillsCache.set(key, { value: skills, mtime });
263
- if (_skillsCache.size > 16) {
264
- _skillsCache.delete(_skillsCache.keys().next().value);
265
- }
266
- return skills;
267
- }
268
- export function invalidateSkillsCache(cwd) {
269
- void cwd;
270
- _skillsCache.clear();
271
- _mtimeCache.clear();
272
- }
273
- /** Drop only the mtime TTL so the next call re-stats the skill roots while the
274
- * cached list stays — lets a test prove the gate itself notices an edit. */
275
- export function invalidateSkillsMtimeGate() {
276
- _mtimeCache.clear();
277
- }
278
-
279
- /**
280
- * Load full skill content plus its on-disk directory (for base-dir + ${MIXDOG_SKILL_DIR}).
281
- */
282
- export function loadSkillResource(name, cwd) {
283
- const skillName = String(name || '').trim();
284
- if (!skillName)
285
- return null;
286
- const skills = collectSkillsCached(cwd);
287
- const skill = skills.find(s => s.name === skillName);
288
- if (!skill)
289
- return null;
290
- const content = readSafe(skill.filePath);
291
- if (content == null)
292
- return null;
293
- try {
294
- return {
295
- content: parseSkillDocument(content).body,
296
- dir: dirname(skill.filePath),
297
- filePath: skill.filePath,
298
- source: skill.source || 'global',
299
- toolDependencies: skill.toolDependencies || [],
300
- declaredToolDependencies: skill.declaredToolDependencies || [],
301
- dependencySource: skill.dependencySource || 'none',
302
- dependencyIssues: skill.dependencyIssues || [],
303
- };
304
- } catch {
305
- return null;
306
- }
307
- }
308
-
309
- function escapeSkillXmlText(value) {
310
- return String(value || '').replace(/[<>&]/g, (ch) => ({ '<': '&lt;', '>': '&gt;', '&': '&amp;' }[ch]));
311
- }
312
-
313
- /**
314
- * Wrap loaded SKILL.md body for Skill tool results (runtime + loop).
315
- */
316
- export function buildSkillResultEnvelope(name, content, skillDir) {
317
- const escapedName = escapeSkillXmlText(name);
318
- let body = String(content == null ? '' : content);
319
- let dirLine = '';
320
- if (skillDir) {
321
- const normDir = String(skillDir).replace(/\\/g, '/');
322
- body = body.replace(/\$\{MIXDOG_SKILL_DIR\}/g, normDir);
323
- const escapedDir = escapeSkillXmlText(normDir);
324
- dirLine = `<base-dir>${escapedDir}</base-dir>\n`;
325
- }
326
- return `<skill>\n<name>${escapedName}</name>\n${dirLine}${body}\n</skill>`;
327
- }
328
-
329
- /**
330
- * Short, model-visible tool_result stub for a loaded skill. The full SKILL.md
331
- * body is delivered separately as ONE injected user message (newMessages),
332
- * never in the tool_result — so the body appears exactly once.
333
- */
334
- function buildSkillStub(name, source) {
335
- // The stub doubles as the transcript's provenance marker: a built-in skill
336
- // (packaged with a built-in feature) loads silently, while user and plugin
337
- // skills surface as a card. `source` mirrors collectSkills() entries.
338
- const kind = source === 'builtin' ? 'built-in skill' : 'skill';
339
- return `Loaded ${kind}: ${String(name || '').trim()}`;
340
- }
341
-
342
- /**
343
- * Is this skill's body still present in the conversation the model will see?
344
- *
345
- * The check is deliberately about the LIVE transcript, not about whether the
346
- * skill was ever loaded. The live turn supersedes the saved transcript after
347
- * compaction. Match the full current body, not just its name, so edited skills
348
- * and skills absent from the rebuilt context are delivered again.
349
- */
350
- export function skillBodyPresentInSession(session, body) {
351
- if (!session || !body) return false;
352
- const requested = latestSkillBodies([{ role: 'user', content: body }])[0];
353
- if (!requested) return false;
354
- const current = latestSkillBodies(currentSkillContext(session))
355
- .find(entry => entry.name === requested.name);
356
- return !!current && skillMessageText(current.message.content).trimStart() === body;
357
- }
358
-
359
- /**
360
- * Build the Skill tool-result envelope used by BOTH the agent-loop viewSkill
361
- * path and the runtime skillToolContent path so behavior matches across main
362
- * + agent sessions:
363
- * - result = short stub (`Loaded skill: <name>`), no body.
364
- * - newMessages = exactly ONE role:'user' message carrying the full
365
- * buildSkillResultEnvelope output (<base-dir> + body).
366
- * The injected user message is flagged `meta:'skill'` so compaction's
367
- * "latest human prompt" selection does not mistake the skill body for the
368
- * human's request.
369
- *
370
- * Passing `session` makes an unchanged repeat load body-free. Tool
371
- * dependencies are still reported, so a
372
- * repeat call re-arms the skill's deferred tools exactly as before.
373
- */
374
- export function buildSkillToolEnvelope(name, content, skillDir, {
375
- source = 'global', toolDependencies = [], dependencyIssues = [],
376
- } = {}, session = null) {
377
- const body = buildSkillResultEnvelope(name, content, skillDir);
378
- const alreadyLoaded = skillBodyPresentInSession(session, body);
379
- return {
380
- __toolEnvelope: true,
381
- result: alreadyLoaded
382
- ? `${buildSkillStub(name, source)} (already active; instructions unchanged — follow them without calling Skill again)`
383
- : buildSkillStub(name, source),
384
- ...(toolDependencies.length ? { skillToolDependencies: toolDependencies } : {}),
385
- ...(dependencyIssues.length ? { skillDependencyIssues: dependencyIssues } : {}),
386
- newMessages: alreadyLoaded ? [] : [
387
- { role: 'user', content: body, meta: 'skill' },
388
- ],
389
- };
390
- }
391
-
392
- // Only selection triggers enter the model's listing. Descriptions belong to
393
- // the UI; operating instructions arrive in the body through Skill().
394
- const SKILL_MANIFEST_TRIGGER_MAX = 250;
395
- const SKILL_MANIFEST_TRIGGER_MIN = 60;
396
- // Whole-manifest ceiling (~1% of a 200k-token window at 4 chars/token).
397
- const SKILL_MANIFEST_CHAR_BUDGET = 8_000;
398
-
399
- function compactSkillManifestText(value, max = SKILL_MANIFEST_TRIGGER_MAX) {
400
- const text = String(value || '').replace(/\s+/g, ' ').trim();
401
- const limit = Math.max(1, Math.floor(max));
402
- if (text.length <= limit) return text;
403
- const head = text.slice(0, Math.max(1, limit - 1));
404
- const space = head.lastIndexOf(' ');
405
- const cut = space >= Math.floor(limit * 0.6) ? head.slice(0, space) : head;
406
- return `${cut.replace(/[\s,.;:!?/\-]+$/, '')}...`;
407
- }
408
-
409
- function skillManifestToolNames(skill) {
410
- const dependencies = Array.isArray(skill?.toolDependencies) ? skill.toolDependencies : [];
411
- return [...new Set(dependencies
412
- .filter((entry) => ['tool', 'mcp'].includes(entry?.type)
413
- && /^[A-Za-z0-9_.:-]+$/.test(String(entry?.value || '')))
414
- .map((entry) => entry.type === 'mcp' ? `mcp:${entry.value}` : entry.value))];
415
- }
416
-
417
- /**
418
- * Build the compact skill manifest shown to the model.
419
- * Full SKILL.md content is still loaded only through Skill(name).
420
- */
421
- export function buildSkillManifest(skills, { limit = 80, charBudget = SKILL_MANIFEST_CHAR_BUDGET } = {}) {
422
- if (skillsDisabled()) return '';
423
- const list = (Array.isArray(skills) ? skills : [])
424
- .map((skill) => ({
425
- name: String(skill?.name || '').trim(),
426
- trigger: String(skill?.whenToUse || '').trim(),
427
- linkedTools: skillManifestToolNames(skill),
428
- }))
429
- .filter((skill) => skill.name)
430
- .sort((a, b) => a.name.localeCompare(b.name));
431
- if (!list.length) return '';
432
- const max = Math.max(1, Number(limit) || 80);
433
- const visible = list.slice(0, max);
434
- // Names are never truncated; the shared budget only shrinks triggers,
435
- // evenly, down to the readable floor.
436
- const budget = Math.max(1_000, Number(charBudget) || SKILL_MANIFEST_CHAR_BUDGET);
437
- const toolSuffix = (skill) => skill.linkedTools.length ? ` [tools: ${skill.linkedTools.join(', ')}]` : '';
438
- const nameOverhead = visible.reduce((sum, skill) => sum + skill.name.length + toolSuffix(skill).length + 4, 0);
439
- const perEntry = Math.floor((budget - nameOverhead) / visible.length);
440
- const triggerCap = Math.min(
441
- SKILL_MANIFEST_TRIGGER_MAX,
442
- Math.max(SKILL_MANIFEST_TRIGGER_MIN, Number.isFinite(perEntry) ? perEntry : SKILL_MANIFEST_TRIGGER_MAX),
443
- );
444
- for (const skill of visible) skill.trigger = compactSkillManifestText(skill.trigger, triggerCap);
445
- const lines = [
446
- '# available-skills',
447
- 'Selection triggers and linked tools for Skill({"name":"<skill-name>"}). mcp:<server> denotes that server’s tools.',
448
- '<available_skills>',
449
- ...visible.map((skill) => (skill.trigger ? `- ${skill.name}: ${skill.trigger}` : `- ${skill.name}`) + toolSuffix(skill)),
450
- ...(list.length > visible.length ? [`- ... ${list.length - visible.length} more skills omitted`] : []),
451
- '</available_skills>',
452
- ];
453
- return lines.join('\n');
454
- }
455
-
456
- const DEFERRED_TOOLS_BLOCK_RE = /(\n\n---\n*)?<available-deferred-tools>[\s\S]*?<\/available-deferred-tools>\s*/gi;
457
- const MCP_INSTRUCTIONS_BLOCK_RE = /(\n\n---\n*)?<mcp-instructions>[\s\S]*?<\/mcp-instructions>\s*/gi;
458
- const DEFERRED_TOOL_NAME_SAFE_RE = /^[A-Za-z0-9_.:-]+$/;
459
- const MCP_SERVER_NAME_SAFE_RE = /^[A-Za-z0-9_.:-]+$/;
460
- const MCP_INSTRUCTION_MAX_CHARS = 600;
461
-
462
- function sanitizeDeferredToolManifestName(name) {
463
- const text = String(name || '').trim();
464
- if (!text || text.includes('<') || text.includes('>')) return '';
465
- if (!DEFERRED_TOOL_NAME_SAFE_RE.test(text)) return '';
466
- return text;
467
- }
468
-
469
- function skillRoutedToolNames(messages) {
470
- const names = new Set();
471
- for (const message of Array.isArray(messages) ? messages : []) {
472
- if (message?.role !== 'system' || typeof message.content !== 'string') continue;
473
- for (const block of message.content.matchAll(/<available_skills>([\s\S]*?)<\/available_skills>/g)) {
474
- // Use only routes actually visible to this session, not the global
475
- // skill catalog (which may include disabled or omitted skills).
476
- for (const route of block[1].matchAll(/^- [^:\r\n]+:\s+\S[^\r\n]* \[tools: ([^\]\r\n]+)\]$/gm)) {
477
- for (const name of route[1].split(',').map((value) => value.trim())) {
478
- // MCP dependencies name servers, not individual tools.
479
- if (/^[A-Za-z0-9_]+$/.test(name) && !name.startsWith('mcp__')) names.add(name);
480
- }
481
- }
482
- }
483
- }
484
- return names;
485
- }
486
-
487
- function hasDeferredToolManifestBlock(text) {
488
- const raw = String(text || '');
489
- return /<available-deferred-tools>[\s\S]*?<\/available-deferred-tools>/i.test(raw)
490
- || /<mcp-instructions>[\s\S]*?<\/mcp-instructions>/i.test(raw);
491
- }
492
-
493
- /**
494
- * Skill-style manifest for tools in the deferred pool (catalog minus active
495
- * wire tools). Each entry is either a bare name string or `{ name, description }`;
496
- * output lines are `- name: description` (description omitted when absent),
497
- * mirroring the available-skills manifest so the model calls deferred tools
498
- * directly. Descriptions are compacted and stripped of `<`/`>`.
499
- * Empty pool → '' (caller omits the block).
500
- */
501
- export function buildDeferredToolManifest(entries) {
502
- const list = [];
503
- const seen = new Set();
504
- for (const entry of Array.isArray(entries) ? entries : []) {
505
- const rawName = typeof entry === 'string' ? entry : entry?.name;
506
- const name = sanitizeDeferredToolManifestName(rawName);
507
- if (!name || seen.has(name)) continue;
508
- seen.add(name);
509
- const description = typeof entry === 'string'
510
- ? ''
511
- : compactSkillManifestText(String(entry?.description || '').replace(/[<>]/g, ''), 100);
512
- list.push({ name, description });
513
- }
514
- if (!list.length) return '';
515
- list.sort((a, b) => a.name.localeCompare(b.name));
516
- return [
517
- '<available-deferred-tools>',
518
- 'Deferred tool names and purposes; schemas load on demand.',
519
- ...list.map((entry) => (entry.description ? `- ${entry.name}: ${entry.description}` : `- ${entry.name}`)),
520
- '</available-deferred-tools>',
521
- ].join('\n');
522
- }
523
-
524
- function sanitizeMcpManifestServerName(name) {
525
- const text = String(name || '').trim();
526
- if (!text || text.includes('<') || text.includes('>')) return '';
527
- if (!MCP_SERVER_NAME_SAFE_RE.test(text)) return '';
528
- return text;
529
- }
530
-
531
- function sanitizeMcpInstructionText(text, max = MCP_INSTRUCTION_MAX_CHARS) {
532
- const stripped = String(text || '').replace(/[<>]/g, '').trim();
533
- if (!stripped) return '';
534
- const cap = Math.max(1, Number(max) || MCP_INSTRUCTION_MAX_CHARS);
535
- return stripped.length > cap ? `${stripped.slice(0, Math.max(1, cap - 3))}...` : stripped;
536
- }
537
-
538
- /**
539
- * Per-server MCP initialize instructions for deferred-pool tools only.
540
- * Empty when no instructions or no matching deferred MCP tools → omit block.
541
- * Emits ONLY the server heading + instruction body: the per-server tool names
542
- * are deliberately NOT repeated here — every pool tool is already listed once
543
- * (with its description) in <available-deferred-tools>, and re-listing ~30
544
- * names per server doubled the MCP share of the BP2 prefix (2026-08-05 audit).
545
- * Server membership stays evident from the mcp__<server>__ name prefix.
546
- */
547
- function buildMcpInstructionsManifest(mcpServerInstructions, poolNames) {
548
- const map = mcpServerInstructions && typeof mcpServerInstructions === 'object'
549
- ? mcpServerInstructions
550
- : {};
551
- const pool = [...new Set((Array.isArray(poolNames) ? poolNames : [])
552
- .map((name) => sanitizeDeferredToolManifestName(name))
553
- .filter(Boolean))];
554
- const toolsByServer = new Map();
555
- for (const name of pool) {
556
- const match = name.match(/^mcp__(.+?)__(.+)$/);
557
- if (!match) continue;
558
- const server = match[1];
559
- if (!toolsByServer.has(server)) toolsByServer.set(server, []);
560
- toolsByServer.get(server).push(name);
561
- }
562
- const servers = [...toolsByServer.keys()]
563
- .filter((server) => sanitizeMcpManifestServerName(server)
564
- && sanitizeMcpInstructionText(map[server])
565
- && toolsByServer.get(server).length)
566
- .sort((a, b) => a.localeCompare(b));
567
- if (!servers.length) return '';
568
- const lines = ['<mcp-instructions>'];
569
- for (const server of servers) {
570
- const safeServer = sanitizeMcpManifestServerName(server);
571
- const body = sanitizeMcpInstructionText(map[server]);
572
- lines.push(`## ${safeServer}`, body);
573
- }
574
- lines.push('</mcp-instructions>');
575
- return lines.join('\n');
576
- }
577
-
578
- export function stripDeferredToolManifestBlock(text) {
579
- return String(text || '')
580
- .replace(DEFERRED_TOOLS_BLOCK_RE, '')
581
- .replace(MCP_INSTRUCTIONS_BLOCK_RE, '')
582
- .replace(/\n\n---\n*$/,'')
583
- .trimEnd();
584
- }
585
-
586
- // Rebuild path: replace the FIRST previously-injected <available-deferred-tools>
587
- // block (with its leading `---` separator) with the fresh manifest IN PLACE, so
588
- // the block keeps its original position and no sibling BP2 block (skills
589
- // manifest, agent rules, …) is reordered or dropped. The fresh manifest already
590
- // carries the mcp-instructions companion, so any pre-existing standalone one is
591
- // removed first to avoid duplication.
592
- function rebuildDeferredToolManifestBlock(text, manifest) {
593
- let out = String(text || '').replace(MCP_INSTRUCTIONS_BLOCK_RE, '');
594
- let replaced = false;
595
- out = out.replace(DEFERRED_TOOLS_BLOCK_RE, (match, sep) => {
596
- if (replaced) return '';
597
- replaced = true;
598
- return `${sep || ''}${manifest}`;
599
- });
600
- if (!replaced) {
601
- const base = out.trimEnd();
602
- out = base ? `${base}\n\n---\n\n${manifest}` : manifest;
603
- }
604
- return out;
605
- }
606
-
607
- /**
608
- * Inject the skill-style deferred pool (name + description) into BP2 at session
609
- * start. Normally once; with `{ rebuild: true }` it strips any existing
610
- * <available-deferred-tools>/<mcp-instructions> block and re-injects the fresh
611
- * pool in place (used by the first-turn MCP refresh, before the prompt renders,
612
- * so late-connected MCP tools land in the INITIAL manifest — never duplicated).
613
- */
614
- export function applyInitialDeferredToolManifestToBp2(session, poolNames, options = {}) {
615
- const rebuild = options?.rebuild === true;
616
- if (!session || !Array.isArray(session.messages)) return false;
617
- if (session.deferredToolBp2Applied && !rebuild) return false;
618
- const pool = Array.isArray(poolNames) ? poolNames : [];
619
- const descByName = new Map();
620
- for (const tool of Array.isArray(session?.deferredToolCatalog) ? session.deferredToolCatalog : []) {
621
- const name = String(tool?.name || '').trim();
622
- if (name && !descByName.has(name)) descByName.set(name, String(tool?.description || ''));
623
- }
624
- const skillRoutedNames = skillRoutedToolNames(session.messages);
625
- const entries = pool.map((name) => ({
626
- name,
627
- description: skillRoutedNames.has(String(name).trim())
628
- ? ''
629
- : descByName.get(String(name).trim()) || '',
630
- }));
631
- const parts = [];
632
- const deferredManifest = buildDeferredToolManifest(entries);
633
- if (deferredManifest) parts.push(deferredManifest);
634
- const mcpManifest = buildMcpInstructionsManifest(session.mcpServerInstructions, pool);
635
- if (mcpManifest) parts.push(mcpManifest);
636
- const manifest = parts.join('\n\n');
637
- if (!manifest) {
638
- for (const message of session.messages) {
639
- if (message?.role === 'system' && typeof message.content === 'string') {
640
- message.content = stripDeferredToolManifestBlock(message.content);
641
- }
642
- }
643
- session.messages = session.messages.filter((message) => (
644
- message?.role !== 'system' || String(message.content || '').trim()
645
- ));
646
- session.deferredToolBp2Applied = true;
647
- delete session.deferredToolBp1Applied;
648
- session.updatedAt = Date.now();
649
- return true;
650
- }
651
-
652
- const existingMessage = session.messages.find((message) => (
653
- message?.role === 'system'
654
- && typeof message.content === 'string'
655
- && hasDeferredToolManifestBlock(message.content)
656
- ));
657
- const systemIndexes = session.messages
658
- .map((message, index) => (message?.role === 'system' ? index : -1))
659
- .filter((index) => index >= 0);
660
- if (!systemIndexes.length) return false;
661
- const tier3Index = systemIndexes.find((index) => session.messages[index]?.cacheTier === 'tier3');
662
- let idx = systemIndexes.length >= 2 && systemIndexes[1] !== tier3Index
663
- ? systemIndexes[1]
664
- : -1;
665
- if (idx < 0) {
666
- idx = tier3Index >= 0 ? tier3Index : systemIndexes[0] + 1;
667
- session.messages.splice(idx, 0, { role: 'system', content: '' });
668
- }
669
- if (existingMessage === session.messages[idx] && !rebuild) {
670
- session.deferredToolBp2Applied = true;
671
- delete session.deferredToolBp1Applied;
672
- return true;
673
- }
674
- for (let i = 0; i < session.messages.length; i++) {
675
- if (i === idx || session.messages[i]?.role !== 'system' || typeof session.messages[i].content !== 'string') continue;
676
- session.messages[i].content = stripDeferredToolManifestBlock(session.messages[i].content);
677
- }
678
- const raw = typeof session.messages[idx].content === 'string' ? session.messages[idx].content : '';
679
- if (rebuild && hasDeferredToolManifestBlock(raw)) {
680
- // Anchored in-place rebuild: swap the previously injected manifest
681
- // block for the fresh one at its EXISTING position.
682
- session.messages[idx].content = rebuildDeferredToolManifestBlock(raw, manifest);
683
- } else {
684
- const base = stripDeferredToolManifestBlock(raw);
685
- session.messages[idx].content = base
686
- ? `${base}\n\n---\n\n${manifest}`
687
- : manifest;
688
- }
689
- session.deferredToolBp2Applied = true;
690
- delete session.deferredToolBp1Applied;
691
- session.updatedAt = Date.now();
692
- return true;
693
- }
694
-
695
- /**
696
- * Build the fixed skill loader meta-tool.
697
- * A tiny stable schema keeps provider cache keys steady; concrete skill
698
- * listings/content are resolved at call time.
699
- *
700
- * The structure is constant regardless of how many skills are in scope.
701
- * Non-agent sessions only expose the loader when a skill exists; agent
702
- * sessions always expose it so the schema shape stays fixed. Memoise so
703
- * every createSession doesn't rebuild
704
- * identical objects (trivial work, but the allocation noise shows up in
705
- * repeated Pool C fan-out).
706
- */
707
- let _skillToolDefsCache = null;
708
- /**
709
- * @param {Array} skills — discovered skill frontmatter list (may be empty)
710
- * @param {object} [opts]
711
- * @param {boolean} [opts.ownerIsAgentSession=false]
712
- * Agent sessions ALWAYS include the meta-tool regardless of the current
713
- * cwd's skill inventory — the concrete skill list is resolved at tool-call
714
- * time (cwd-scoped) so the tool schema stays bit-identical across roles /
715
- * cwds and the provider cache shard does not fragment.
716
- * Non-agent sessions keep the historical "empty when skills.length===0"
717
- * behaviour.
718
- */
719
- export function buildSkillToolDefs(skills, { ownerIsAgentSession = false } = {}) {
720
- if (skillsDisabled()) return [];
721
- if (!ownerIsAgentSession && !skills.length) return [];
722
- if (_skillToolDefsCache) return _skillToolDefsCache;
723
- _skillToolDefsCache = [
724
- {
725
- name: 'Skill',
726
- title: 'Skill',
727
- annotations: {
728
- title: 'Skill',
729
- readOnlyHint: true,
730
- destructiveHint: false,
731
- idempotentHint: true,
732
- openWorldHint: false,
733
- agentHidden: false,
734
- },
735
- description: 'Load or refresh an available skill’s SKILL.md before task actions when its body is missing or needs an update. Reuse a body already in context for matching requests; a later turn or repeated mention is not a reason to call Skill again.',
736
- inputSchema: {
737
- type: 'object',
738
- properties: {
739
- name: { type: 'string', description: 'Exact name from available-skills.' },
740
- },
741
- required: ['name'],
742
- additionalProperties: false,
743
- },
744
- },
745
- ];
746
- return _skillToolDefsCache;
747
- }
748
- // --- Agent-scoped instruction loader ---
749
- // Emits a BP2 agent/system block scoped to the calling agent:
750
- // - Public/custom agents: their own agents/<agent>.md when present,
751
- // plus the public agent-worker contract.
752
- // - Hidden agents: their own rules/agent/<agent>.md section only.
753
- // - Null agent: falls back to the full all-in-one block
754
- // (explicit-cache unified-shard path).
755
- //
756
- // Agent-specific markdown intentionally rides BP2, behind the shared BP1 tool
757
- // and skill manifest prefix, so agent changes do not disturb the common layer.
758
- //
759
- // Classification is dynamic — hidden retrieval/maintenance sets come from the
760
- // `kind` field in internal-agents.mjs. Any other non-null agent is public/custom.
761
- import { listHiddenAgentsByKind, getAgentCatalogShareAgents } from '../internal-agents.mjs';
762
-
763
- function loadAgentClassification() {
764
- // Not cached — called only on instruction rebuild (mtime-busted), and
765
- // listHiddenAgentsByKind now reads from the mtime-aware cache inside
766
- // internal-agents.mjs so the classification always reflects the current
767
- // agents.json on disk.
768
- return {
769
- retrieval: new Set(listHiddenAgentsByKind('retrieval')),
770
- maintenance: new Set(listHiddenAgentsByKind('maintenance')),
771
- };
772
- }
773
-
774
- const _scopedRoleInstructionsCache = new Map();
775
- // Short-TTL gate for the role-instruction freshness stat. loadScopedRoleInstructions() ran
776
- // maxMtimeRecursive() over agents/ + rules/agent/ on EVERY call (many per
777
- // turn across roles), so even a warm cache paid dozens of statSync per turn.
778
- // Mirror collectSkillsCached(): only re-stat after _ROLE_INSTRUCTIONS_MTIME_TTL_MS, and
779
- // trust the cached mtime within that window. Edits still propagate within ~1
780
- // stat interval, which is well under human-perceptible latency.
781
- const _scopedRoleInstructionsMtimeCache = new Map();
782
- const _ROLE_INSTRUCTIONS_MTIME_TTL_MS = 2000;
783
-
784
- function loadHiddenAgentSnippets(pluginRoot) {
785
- try {
786
- const agentRulesDir = join(pluginRoot, 'rules', 'agent');
787
- if (!existsSync(agentRulesDir)) return [];
788
- const files = readdirSync(agentRulesDir)
789
- .filter(f => f.endsWith('.md') && f !== '00-common.md' && f !== '00-core.md')
790
- .sort();
791
- const pairs = [];
792
- for (const f of files) {
793
- const raw = readSafe(join(agentRulesDir, f));
794
- if (!raw) continue;
795
- const { body } = readMarkdownDocument(raw);
796
- if (!body) continue;
797
- const name = f.replace(/^\d+-/, '').replace(/\.md$/, '');
798
- pairs.push({ name, body });
799
- }
800
- return pairs;
801
- } catch {
802
- return [];
803
- }
804
- }
805
-
806
- // Role-markdown roots, in precedence order (later wins on the same name):
807
- // the shipped agents/ tree, then the user data dir. User-authored roles and
808
- // built-in overrides written by the Workflows editor live in the data dir —
809
- // reading only the install root left a custom agent with an EMPTY role
810
- // catalog while Lead already saw the edited AGENT.md.
811
- function agentSectionDirs(pluginRoot) {
812
- const dirs = [];
813
- if (pluginRoot) dirs.push(join(pluginRoot, 'agents'));
814
- try {
815
- const userDir = mixdogGlobalDir('agents');
816
- if (userDir && !dirs.includes(userDir)) dirs.push(userDir);
817
- } catch { /* unresolvable data dir — built-in roles still load */ }
818
- return dirs;
819
- }
820
-
821
- function loadAgentSections(pluginRoot) {
822
- // agents/ accepts both the compatibility flat layout and the current
823
- // nested agents/<agent>/AGENT.md layout.
824
- // The previous flat-only readdir silently dropped every nested agent, so a
825
- // public agent like heavy-worker produced an EMPTY scoped instruction block
826
- // (BP2) — the model lost its agent contract and the tool smoke's
827
- // "heavy-worker AGENT.md must be included" assertion failed. Walk both.
828
- const byName = new Map();
829
- for (const agentsDir of agentSectionDirs(pluginRoot)) {
830
- if (!existsSync(agentsDir)) continue;
831
- let entries;
832
- try {
833
- entries = readdirSync(agentsDir, { withFileTypes: true });
834
- } catch {
835
- continue;
836
- }
837
- for (const entry of entries) {
838
- let name = '';
839
- let raw = null;
840
- if (entry.isDirectory()) {
841
- name = entry.name;
842
- raw = readSafe(join(agentsDir, entry.name, 'AGENT.md'));
843
- } else if (entry.isFile() && entry.name.endsWith('.md')) {
844
- name = entry.name.replace(/\.md$/, '');
845
- raw = readSafe(join(agentsDir, entry.name));
846
- }
847
- if (!name || !raw) continue;
848
- const { body } = readMarkdownDocument(raw);
849
- if (!body) continue;
850
- byName.set(name, `## ${name}\n\n${body}`);
851
- }
852
- }
853
- return [...byName.entries()]
854
- .sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0))
855
- .map(([, text]) => text);
856
- }
857
-
858
- // Empty by design: scoped agent markdown already rides BP2 for every provider.
859
- // Keeping the set in place preserves the old branch point for a future
860
- // provider-specific experiment without changing today's cache layout.
861
- const EXPLICIT_CACHE_PROVIDERS = new Set();
862
-
863
- export function loadScopedRoleInstructions(agent, provider = null) {
864
- const useUnified = !!(provider && EXPLICIT_CACHE_PROVIDERS.has(provider));
865
- const cacheKey = useUnified ? '__unified__' : (agent || '__all__');
866
- const cached = _scopedRoleInstructionsCache.get(cacheKey);
867
- const pluginRoot = mixdogRoot();
868
- // Use maxMtimeRecursive so edits to .md files inside agents/ and
869
- // rules/agent/ propagate — parent dir mtime is unchanged on
870
- // Linux/macOS when only a nested file's content changes. Gate the stat
871
- // behind a short TTL so repeated same-turn calls reuse the last mtime
872
- // instead of re-walking the trees on every invocation.
873
- let mtime = 0;
874
- if (pluginRoot) {
875
- const mtimeCached = _scopedRoleInstructionsMtimeCache.get(cacheKey);
876
- if (mtimeCached && Date.now() - mtimeCached.checkedAt < _ROLE_INSTRUCTIONS_MTIME_TTL_MS) {
877
- mtime = mtimeCached.mtime;
878
- } else {
879
- mtime = maxMtimeRecursive([
880
- ...agentSectionDirs(pluginRoot),
881
- join(pluginRoot, 'rules', 'agent'),
882
- join(pluginRoot, 'defaults', 'agents.json'),
883
- ]);
884
- _scopedRoleInstructionsMtimeCache.set(cacheKey, { mtime, checkedAt: Date.now() });
885
- if (_scopedRoleInstructionsMtimeCache.size > 16) {
886
- _scopedRoleInstructionsMtimeCache.delete(_scopedRoleInstructionsMtimeCache.keys().next().value);
887
- }
888
- }
889
- }
890
- if (cached && mtime <= cached.mtime) {
891
- return cached.value;
892
- }
893
- // Compute classification before file loading — internal-agents metadata
894
- // failures (malformed/missing agents.json) must propagate, not be
895
- // silently swallowed by the file-IO catch below.
896
- const classification = loadAgentClassification();
897
- const agentSharesCatalog = agent && classification.retrieval.has(agent)
898
- ? new Set(getAgentCatalogShareAgents(agent))
899
- : new Set();
900
- try {
901
- const agentSections = loadAgentSections(pluginRoot);
902
- const hiddenPairs = loadHiddenAgentSnippets(pluginRoot);
903
-
904
- // Pick which agent-rule sections + agents/<agent>.md sections to emit
905
- // based on agent classification. Self-only emit keeps BP2 minimal.
906
- let agentRuleSectionsToEmit = null; // null -> drop the agent-rule block entirely
907
- let agentSectionsToEmit = agentSections; // default: full (unknown-agent fallback)
908
- if (useUnified) {
909
- // Explicit-cache providers — every agent sees the same all-in-one
910
- // instruction surface. Cross-agent calls hit the same provider-side prefix
911
- // prefix, eliminating the cross-agent cache miss seen on Pool C
912
- // transitions for openai-oauth/openai. This branch is disabled by the
913
- // empty provider set above; BP2 remains the active agent surface.
914
- agentRuleSectionsToEmit = hiddenPairs.map(p => `## ${p.name}\n\n${p.body}`);
915
- agentSectionsToEmit = agentSections;
916
- } else if (agent && classification.retrieval.has(agent)) {
917
- // Retrieval agents get their own contract section in BP2.
918
- const self = hiddenPairs.find(p => p.name === agent);
919
- agentRuleSectionsToEmit = self ? [`## ${self.name}\n\n${self.body}`] : [];
920
- agentSectionsToEmit = agentSections.filter(s =>
921
- [...agentSharesCatalog].some(name => s.startsWith(`## ${name}\n`)));
922
- } else if (agent && classification.maintenance.has(agent)) {
923
- const self = hiddenPairs.find(p => p.name === agent);
924
- agentRuleSectionsToEmit = [];
925
- if (self) {
926
- agentRuleSectionsToEmit.push(`## ${self.name}\n\n${self.body}`);
927
- } else {
928
- // Self body from agents/<agent>.md for a maintenance agent
929
- // without a rules/agent/*.md entry so newly-added hidden agents
930
- // work without needing a duplicate agent rule file.
931
- const fromAgent = agentSections.find(s => s.startsWith(`## ${agent}\n`));
932
- if (fromAgent) agentRuleSectionsToEmit.push(fromAgent);
933
- }
934
- agentSectionsToEmit = [];
935
- } else if (agent) {
936
- // Public/custom agent — self-only agents/<agent>.md when present,
937
- // not the full hidden/maintenance bundle. The universal agent
938
- // contract rides BP2 (rules/agent/00-common.md).
939
- agentRuleSectionsToEmit = [];
940
- agentSectionsToEmit = agentSections.filter(s => s.startsWith(`## ${agent}\n`));
941
- } else {
942
- // Null agent — full instruction surface emitted (explicit-cache providers that
943
- // shard by __all__ key).
944
- agentRuleSectionsToEmit = hiddenPairs.map(p => `## ${p.name}\n\n${p.body}`);
945
- // agentSectionsToEmit already set to full agentSections above.
946
- }
947
-
948
- const blocks = [];
949
- if (agentSectionsToEmit.length) {
950
- blocks.push(`# Agent Role Catalog\n\n${agentSectionsToEmit.join('\n\n---\n\n')}`);
951
- }
952
- if (agentRuleSectionsToEmit && agentRuleSectionsToEmit.length) {
953
- blocks.push(`# Agent Role Rules\n\n${agentRuleSectionsToEmit.join('\n\n---\n\n')}`);
954
- }
955
- const value = blocks.join('\n\n---\n\n');
956
- _scopedRoleInstructionsCache.set(cacheKey, { mtime, value });
957
- return value;
958
- } catch {
959
- return '';
960
- }
961
- }
962
-
963
- // --- Compose system prompt — 4-BP cache layout ---
964
- // Returns the three stable system blocks and the BP3 core used for refreshes.
965
- // directly to the breakpoint plan:
966
- // BP1 (1h, system block #1) = baseRules — shared tool policy
967
- // BP2 (1h, system block #2) = stableSystemContext — profile, skills, deferred/MCP
968
- // BP3 (1h, system block #3) = sessionMarker — workflow/role, memory, session/project environment
969
- // BP4 (5m/1h, messages tail) = live user/task/tool message tail
970
- //
971
- // Dynamic schedule/webhook/task payloads stay in normal user messages so
972
- // changing one event does not rewrite the stable memory layer.
973
- //
974
- // `profile.skip` still filters specific buckets (claudemd, skills, memory)
975
- // for backward compatibility with existing profiles.
976
- export function composeSystemPrompt(opts) {
977
- const profile = opts.profile || null;
978
- const _skip = profile?.skip || {};
979
-
980
- // ── BP1: globally shared tool policy ────────────────────────────────
981
- const baseParts = [];
982
- if (opts.agentRules) baseParts.push(opts.agentRules);
983
- const baseRules = baseParts.join('\n\n---\n\n');
984
-
985
- // ── BP2: persistent profile/tool catalog layer ──────────────────────
986
- const stableSystemParts = [];
987
- if (opts.metaContext && typeof opts.metaContext === 'string' && opts.metaContext.trim()) {
988
- stableSystemParts.push(opts.metaContext.trim());
989
- }
990
- if (!_skip.skills && opts.skillManifest && typeof opts.skillManifest === 'string' && opts.skillManifest.trim()) {
991
- stableSystemParts.push(opts.skillManifest.trim());
992
- }
993
- // deferredToolManifest: optional BP2 slice; production path is
994
- // applyInitialDeferredToolManifestToBp2 once after applyDeferredToolSurface.
995
- if (opts.deferredToolManifest && typeof opts.deferredToolManifest === 'string' && opts.deferredToolManifest.trim()) {
996
- stableSystemParts.push(opts.deferredToolManifest.trim());
997
- }
998
- const stableSystemContext = stableSystemParts.join('\n\n---\n\n');
999
-
1000
- // ── BP3: workflow/role + session environment layer ─────────────────
1001
- const roleInstructionContext = opts.skipRoleCatalog
1002
- ? ''
1003
- : loadScopedRoleInstructions(opts.agent || null, opts.provider || null);
1004
- const sessionMarkerParts = [];
1005
- // Keep the active workflow first within BP3 so it leads the role and
1006
- // environment material that varies with the session.
1007
- if (opts.workflowContext && typeof opts.workflowContext === 'string' && opts.workflowContext.trim()) {
1008
- sessionMarkerParts.push(opts.workflowContext.trim());
1009
- }
1010
- if (opts.roleRules) sessionMarkerParts.push(opts.roleRules);
1011
- if (opts.userPrompt) sessionMarkerParts.push(opts.userPrompt);
1012
- if (roleInstructionContext) sessionMarkerParts.push(roleInstructionContext);
1013
- if (!_skip.memory && opts.coreMemoryContext && typeof opts.coreMemoryContext === 'string' && opts.coreMemoryContext.trim()) {
1014
- sessionMarkerParts.push('# Core Memory\n' + opts.coreMemoryContext.trim());
1015
- }
1016
- const sessionMarkerCore = sessionMarkerParts.length
1017
- ? sessionMarkerParts.join('\n\n---\n\n')
1018
- : '';
1019
- const environmentParts = [];
1020
- // Response language is the LAST system text before the conversation. It
1021
- // rides the environment block (not the tier3 core) because the `# Session`
1022
- // / shell / git lines are English and would otherwise trail it, pulling the
1023
- // pre-tool preamble toward English.
1024
- for (const value of [
1025
- opts.sessionStartContext,
1026
- opts.projectInstructionsContext,
1027
- opts.environmentContext,
1028
- opts.languageContext,
1029
- ]) {
1030
- if (typeof value === 'string' && value.trim()) environmentParts.push(value.trim());
1031
- }
1032
- // Volatile session/project environment. Kept OUT of sessionMarkerCore so
1033
- // Anthropic providers can leave it as an UNMARKED system block (covered by
1034
- // the messages-tail breakpoint) while prefix-order providers still see it
1035
- // after the stable context. sessionMarker keeps the combined legacy shape
1036
- // for callers that consume a single BP3 string.
1037
- const sessionEnvironment = environmentParts.join('\n\n---\n\n');
1038
- const sessionMarker = [sessionMarkerCore, sessionEnvironment]
1039
- .filter(Boolean)
1040
- .join('\n\n---\n\n');
1041
-
1042
- return { baseRules, stableSystemContext, sessionMarkerCore, sessionEnvironment, sessionMarker };
1043
- }
1044
- // --- Helpers ---
1045
- function readSafe(path) {
1046
- try {
1047
- if (!existsSync(path))
1048
- return null;
1049
- const content = readFileSync(path, 'utf-8').trim();
1050
- return content || null;
1051
- }
1052
- catch {
1053
- return null;
1054
- }
1055
- }
1
+ export {
2
+ applyInitialDeferredToolManifestToBp2,
3
+ buildDeferredToolManifest,
4
+ stripDeferredToolManifestBlock,
5
+ } from './deferred-tools.mjs';
6
+ export {
7
+ buildSkillManifest,
8
+ buildSkillResultEnvelope,
9
+ buildSkillToolDefs,
10
+ buildSkillToolEnvelope,
11
+ collectPromptSkillsCached,
12
+ collectSkills,
13
+ collectSkillsCached,
14
+ filterSkillsExcludingDisabled,
15
+ invalidateSkillsCache,
16
+ invalidateSkillsMtimeGate,
17
+ isSkillDisabled,
18
+ loadSkillResource,
19
+ skillBodyPresentInSession,
20
+ skillMissingFeature,
21
+ } from './skill-catalog.mjs';
22
+ export {
23
+ composeSystemPrompt,
24
+ loadScopedRoleInstructions,
25
+ } from './role-instructions.mjs';