agent-mockingbird 0.0.1

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 (227) hide show
  1. package/.agents/skills/btca-cli/SKILL.md +64 -0
  2. package/.agents/skills/btca-cli/agents/openai.yaml +3 -0
  3. package/.agents/skills/frontend-design/SKILL.md +42 -0
  4. package/.agents/skills/frontend-design/agents/openai.yaml +3 -0
  5. package/.env.example +36 -0
  6. package/.githooks/pre-commit +33 -0
  7. package/.github/workflows/ci.yml +309 -0
  8. package/.opencode/bun.lock +18 -0
  9. package/.opencode/package.json +5 -0
  10. package/.opencode/tools/agent_type_manager.ts +100 -0
  11. package/.opencode/tools/config_manager.ts +87 -0
  12. package/.opencode/tools/cron_manager.ts +145 -0
  13. package/.opencode/tools/memory_get.ts +43 -0
  14. package/.opencode/tools/memory_remember.ts +53 -0
  15. package/.opencode/tools/memory_search.ts +48 -0
  16. package/AGENTS.md +126 -0
  17. package/MEMORY.md +2 -0
  18. package/README.md +451 -0
  19. package/THIRD_PARTY_NOTICES.md +11 -0
  20. package/agent-mockingbird.config.example.json +135 -0
  21. package/apps/server/package.json +32 -0
  22. package/apps/server/src/backend/agents/bootstrapContext.ts +362 -0
  23. package/apps/server/src/backend/agents/openclawImport.test.ts +133 -0
  24. package/apps/server/src/backend/agents/openclawImport.ts +797 -0
  25. package/apps/server/src/backend/agents/opencodeConfig.ts +428 -0
  26. package/apps/server/src/backend/agents/service.ts +10 -0
  27. package/apps/server/src/backend/config/example-config.test.ts +20 -0
  28. package/apps/server/src/backend/config/orchestration.ts +243 -0
  29. package/apps/server/src/backend/config/policy.ts +158 -0
  30. package/apps/server/src/backend/config/schema.test.ts +15 -0
  31. package/apps/server/src/backend/config/schema.ts +391 -0
  32. package/apps/server/src/backend/config/semantic.test.ts +34 -0
  33. package/apps/server/src/backend/config/semantic.ts +149 -0
  34. package/apps/server/src/backend/config/service.test.ts +75 -0
  35. package/apps/server/src/backend/config/service.ts +207 -0
  36. package/apps/server/src/backend/config/smoke.ts +77 -0
  37. package/apps/server/src/backend/config/store.test.ts +123 -0
  38. package/apps/server/src/backend/config/store.ts +581 -0
  39. package/apps/server/src/backend/config/testFixtures.ts +5 -0
  40. package/apps/server/src/backend/config/types.ts +56 -0
  41. package/apps/server/src/backend/contracts/events.ts +320 -0
  42. package/apps/server/src/backend/contracts/runtime.ts +111 -0
  43. package/apps/server/src/backend/cron/executor.ts +435 -0
  44. package/apps/server/src/backend/cron/repository.ts +170 -0
  45. package/apps/server/src/backend/cron/service.ts +660 -0
  46. package/apps/server/src/backend/cron/storage.ts +92 -0
  47. package/apps/server/src/backend/cron/types.ts +138 -0
  48. package/apps/server/src/backend/cron/utils.ts +351 -0
  49. package/apps/server/src/backend/db/client.ts +20 -0
  50. package/apps/server/src/backend/db/migrate.ts +40 -0
  51. package/apps/server/src/backend/db/repository.ts +1762 -0
  52. package/apps/server/src/backend/db/schema.ts +113 -0
  53. package/apps/server/src/backend/db/usageDashboard.test.ts +102 -0
  54. package/apps/server/src/backend/db/wipe.ts +13 -0
  55. package/apps/server/src/backend/defaults.ts +32 -0
  56. package/apps/server/src/backend/env.ts +48 -0
  57. package/apps/server/src/backend/heartbeat/activeHours.ts +45 -0
  58. package/apps/server/src/backend/heartbeat/defaultJob.ts +88 -0
  59. package/apps/server/src/backend/heartbeat/heartbeat.test.ts +110 -0
  60. package/apps/server/src/backend/heartbeat/runtimeService.ts +190 -0
  61. package/apps/server/src/backend/heartbeat/service.ts +176 -0
  62. package/apps/server/src/backend/heartbeat/state.test.ts +63 -0
  63. package/apps/server/src/backend/heartbeat/state.ts +167 -0
  64. package/apps/server/src/backend/heartbeat/types.ts +54 -0
  65. package/apps/server/src/backend/http/boundedQueue.test.ts +49 -0
  66. package/apps/server/src/backend/http/boundedQueue.ts +92 -0
  67. package/apps/server/src/backend/http/parsers.ts +40 -0
  68. package/apps/server/src/backend/http/router.ts +61 -0
  69. package/apps/server/src/backend/http/routes/agentRoutes.ts +67 -0
  70. package/apps/server/src/backend/http/routes/backgroundRoutes.ts +203 -0
  71. package/apps/server/src/backend/http/routes/chatRoutes.ts +107 -0
  72. package/apps/server/src/backend/http/routes/configRoutes.ts +602 -0
  73. package/apps/server/src/backend/http/routes/cronRoutes.ts +221 -0
  74. package/apps/server/src/backend/http/routes/dashboardRoutes.ts +308 -0
  75. package/apps/server/src/backend/http/routes/eventRoutes.ts +7 -0
  76. package/apps/server/src/backend/http/routes/heartbeatRoutes.test.ts +41 -0
  77. package/apps/server/src/backend/http/routes/heartbeatRoutes.ts +28 -0
  78. package/apps/server/src/backend/http/routes/index.ts +101 -0
  79. package/apps/server/src/backend/http/routes/mcpRoutes.ts +213 -0
  80. package/apps/server/src/backend/http/routes/memoryRoutes.ts +154 -0
  81. package/apps/server/src/backend/http/routes/runRoutes.ts +310 -0
  82. package/apps/server/src/backend/http/routes/runtimeRoutes.ts +197 -0
  83. package/apps/server/src/backend/http/routes/skillRoutes.ts +112 -0
  84. package/apps/server/src/backend/http/routes/uiRoutes.test.ts +161 -0
  85. package/apps/server/src/backend/http/routes/uiRoutes.ts +177 -0
  86. package/apps/server/src/backend/http/routes/usageRoutes.test.ts +104 -0
  87. package/apps/server/src/backend/http/routes/usageRoutes.ts +767 -0
  88. package/apps/server/src/backend/http/schemas.ts +64 -0
  89. package/apps/server/src/backend/http/sse.ts +144 -0
  90. package/apps/server/src/backend/integration/backend-core.test.ts +2316 -0
  91. package/apps/server/src/backend/logging/logger.ts +64 -0
  92. package/apps/server/src/backend/mcp/service.ts +326 -0
  93. package/apps/server/src/backend/memory/cli.ts +170 -0
  94. package/apps/server/src/backend/memory/conceptExpansion.test.ts +28 -0
  95. package/apps/server/src/backend/memory/conceptExpansion.ts +80 -0
  96. package/apps/server/src/backend/memory/qmdPort.test.ts +54 -0
  97. package/apps/server/src/backend/memory/qmdPort.ts +61 -0
  98. package/apps/server/src/backend/memory/records.test.ts +66 -0
  99. package/apps/server/src/backend/memory/records.ts +229 -0
  100. package/apps/server/src/backend/memory/service.ts +2012 -0
  101. package/apps/server/src/backend/memory/sqliteVec.ts +58 -0
  102. package/apps/server/src/backend/memory/types.ts +104 -0
  103. package/apps/server/src/backend/opencode/agentMockingbirdPlugin.test.ts +396 -0
  104. package/apps/server/src/backend/opencode/client.ts +98 -0
  105. package/apps/server/src/backend/opencode/models.ts +41 -0
  106. package/apps/server/src/backend/opencode/systemPrompt.test.ts +146 -0
  107. package/apps/server/src/backend/opencode/systemPrompt.ts +284 -0
  108. package/apps/server/src/backend/paths.ts +57 -0
  109. package/apps/server/src/backend/prompts/service.ts +100 -0
  110. package/apps/server/src/backend/queue/queue.test.ts +189 -0
  111. package/apps/server/src/backend/queue/service.ts +177 -0
  112. package/apps/server/src/backend/queue/types.ts +39 -0
  113. package/apps/server/src/backend/run/service.ts +576 -0
  114. package/apps/server/src/backend/run/storage.ts +47 -0
  115. package/apps/server/src/backend/run/types.ts +44 -0
  116. package/apps/server/src/backend/runtime/errors.ts +61 -0
  117. package/apps/server/src/backend/runtime/index.ts +72 -0
  118. package/apps/server/src/backend/runtime/memoryPromptDedup.test.ts +153 -0
  119. package/apps/server/src/backend/runtime/memoryPromptDedup.ts +76 -0
  120. package/apps/server/src/backend/runtime/opencodeRuntime/backgroundMethods.ts +765 -0
  121. package/apps/server/src/backend/runtime/opencodeRuntime/coreMethods.ts +705 -0
  122. package/apps/server/src/backend/runtime/opencodeRuntime/eventMethods.ts +503 -0
  123. package/apps/server/src/backend/runtime/opencodeRuntime/memoryMethods.ts +462 -0
  124. package/apps/server/src/backend/runtime/opencodeRuntime/promptMethods.ts +1167 -0
  125. package/apps/server/src/backend/runtime/opencodeRuntime/shared.ts +254 -0
  126. package/apps/server/src/backend/runtime/opencodeRuntime.test.ts +2899 -0
  127. package/apps/server/src/backend/runtime/opencodeRuntime.ts +135 -0
  128. package/apps/server/src/backend/runtime/sessionScope.ts +45 -0
  129. package/apps/server/src/backend/skills/service.ts +442 -0
  130. package/apps/server/src/backend/workspace/resolve.ts +27 -0
  131. package/apps/server/src/cli/agent-mockingbird.mjs +2522 -0
  132. package/apps/server/src/cli/agent-mockingbird.test.ts +68 -0
  133. package/apps/server/src/cli/runtime-assets.mjs +269 -0
  134. package/apps/server/src/cli/runtime-assets.test.ts +52 -0
  135. package/apps/server/src/cli/runtime-layout.mjs +75 -0
  136. package/apps/server/src/cli/standaloneBuild.test.ts +19 -0
  137. package/apps/server/src/cli/standaloneBuild.ts +19 -0
  138. package/apps/server/src/cli/standaloneCronBinary.test.ts +187 -0
  139. package/apps/server/src/index.ts +178 -0
  140. package/apps/server/tsconfig.json +12 -0
  141. package/backlog.md +5 -0
  142. package/bin/agent-mockingbird +2522 -0
  143. package/bin/runtime-layout.mjs +75 -0
  144. package/build-bin.ts +34 -0
  145. package/build-cli.mjs +37 -0
  146. package/build.ts +40 -0
  147. package/bun-env.d.ts +11 -0
  148. package/bun.lock +888 -0
  149. package/bunfig.toml +2 -0
  150. package/components.json +21 -0
  151. package/config.json +130 -0
  152. package/deploy/RELEASE_INSTALL.md +112 -0
  153. package/deploy/docker-compose.yml +42 -0
  154. package/deploy/systemd/README.md +46 -0
  155. package/deploy/systemd/agent-mockingbird.service +28 -0
  156. package/deploy/systemd/opencode.service +25 -0
  157. package/docs/legacy-config-ui-reference.md +51 -0
  158. package/docs/memory-e2e-trace-2026-03-04.md +63 -0
  159. package/docs/memory-ops.md +96 -0
  160. package/docs/memory-runtime-contract.md +42 -0
  161. package/docs/memory-tuning-remote-2026-03-04.md +59 -0
  162. package/docs/opencode-rebase-workflow-plan.md +614 -0
  163. package/docs/opencode-startup-sync-plan.md +94 -0
  164. package/docs/vendor-opencode.md +41 -0
  165. package/drizzle/0000_famous_turbo.sql +49 -0
  166. package/drizzle/0001_cron_memory_aux.sql +160 -0
  167. package/drizzle/0002_runtime_session_bindings.sql +28 -0
  168. package/drizzle/0003_background_runs.sql +27 -0
  169. package/drizzle/0004_memory_open_write.sql +63 -0
  170. package/drizzle/0005_signal_channel.sql +47 -0
  171. package/drizzle/0006_usage_event_dimensions.sql +7 -0
  172. package/drizzle/meta/0000_snapshot.json +341 -0
  173. package/drizzle/meta/_journal.json +55 -0
  174. package/drizzle.config.ts +14 -0
  175. package/eslint.config.mjs +77 -0
  176. package/knip.json +18 -0
  177. package/memory/2026-03-04.md +4 -0
  178. package/opencode.lock.json +16 -0
  179. package/package.json +67 -0
  180. package/packages/agent-mockingbird-installer/README.md +31 -0
  181. package/packages/agent-mockingbird-installer/bin/agent-mockingbird-installer.mjs +44 -0
  182. package/packages/agent-mockingbird-installer/opencode.lock.json +16 -0
  183. package/packages/agent-mockingbird-installer/package.json +23 -0
  184. package/packages/contracts/package.json +19 -0
  185. package/packages/contracts/src/agentTypes.ts +122 -0
  186. package/packages/contracts/src/cron.ts +146 -0
  187. package/packages/contracts/src/dashboard.ts +378 -0
  188. package/packages/contracts/src/index.ts +3 -0
  189. package/packages/contracts/tsconfig.json +4 -0
  190. package/patches/opencode/0001-Wafflebot-OpenCode-baseline.patch +2341 -0
  191. package/patches/opencode/0002-Fix-OpenCode-web-entry-and-settings-icons.patch +104 -0
  192. package/patches/opencode/0003-fix-app-remove-duplicate-sidebar-mount.patch +32 -0
  193. package/patches/opencode/0004-Add-heartbeat-settings-and-usage-nav.patch +506 -0
  194. package/patches/opencode/0005-Use-chart-icon-for-usage-nav.patch +38 -0
  195. package/patches/opencode/0006-Modernize-cron-settings.patch +399 -0
  196. package/patches/opencode/0007-Rename-waffle-namespaces-to-mockingbird.patch +1110 -0
  197. package/patches/opencode/0008-Remove-cron-contract-section.patch +178 -0
  198. package/patches/opencode/0009-Rework-cron-tab-as-operations-console.patch +414 -0
  199. package/patches/opencode/0010-Refine-heartbeat-settings-controls.patch +208 -0
  200. package/runtime-assets/opencode-config/opencode.jsonc +25 -0
  201. package/runtime-assets/opencode-config/package.json +5 -0
  202. package/runtime-assets/opencode-config/plugins/agent-mockingbird.ts +715 -0
  203. package/runtime-assets/workspace/.agents/skills/config-auditor/SKILL.md +25 -0
  204. package/runtime-assets/workspace/.agents/skills/config-editor/SKILL.md +24 -0
  205. package/runtime-assets/workspace/.agents/skills/cron-manager/SKILL.md +57 -0
  206. package/runtime-assets/workspace/.agents/skills/memory-ops/SKILL.md +120 -0
  207. package/runtime-assets/workspace/.agents/skills/runtime-diagnose/SKILL.md +25 -0
  208. package/runtime-assets/workspace/AGENTS.md +56 -0
  209. package/runtime-assets/workspace/MEMORY.md +4 -0
  210. package/scripts/build-release-bundle.sh +66 -0
  211. package/scripts/check-ship.ts +383 -0
  212. package/scripts/dev-opencode.sh +17 -0
  213. package/scripts/dev-stack-opencode.sh +15 -0
  214. package/scripts/dev-stack.sh +61 -0
  215. package/scripts/install-systemd.sh +87 -0
  216. package/scripts/memory-e2e.sh +76 -0
  217. package/scripts/memory-trace-e2e.sh +141 -0
  218. package/scripts/migrate-opencode-env.ts +108 -0
  219. package/scripts/onboard/bootstrap.sh +32 -0
  220. package/scripts/opencode-swap.ts +78 -0
  221. package/scripts/opencode-sync.ts +715 -0
  222. package/scripts/runtime-assets-sync.mjs +83 -0
  223. package/scripts/setup-git-hooks.ts +39 -0
  224. package/tsconfig.json +45 -0
  225. package/tui.json +98 -0
  226. package/turbo.json +36 -0
  227. package/vendor/OPENCODE_VENDOR.md +13 -0
@@ -0,0 +1,158 @@
1
+ import type { AgentMockingbirdConfig } from "./schema";
2
+ import { ConfigApplyError } from "./types";
3
+
4
+ interface ConfigPolicyDecision {
5
+ mode: "builder" | "strict";
6
+ changedPaths: string[];
7
+ rejectedPaths: string[];
8
+ requireExpectedHash: boolean;
9
+ requireSmokeTest: boolean;
10
+ autoRollbackOnFailure: boolean;
11
+ }
12
+
13
+ function isPlainObject(value: unknown): value is Record<string, unknown> {
14
+ return Boolean(value) && typeof value === "object" && !Array.isArray(value);
15
+ }
16
+
17
+ function stableSerialize(value: unknown): string {
18
+ if (value === null) return "null";
19
+ if (typeof value === "number" || typeof value === "boolean") return JSON.stringify(value);
20
+ if (typeof value === "string") return JSON.stringify(value);
21
+ if (Array.isArray(value)) return `[${value.map(item => stableSerialize(item)).join(",")}]`;
22
+ if (!isPlainObject(value)) return JSON.stringify(value);
23
+ const entries = Object.entries(value).sort(([a], [b]) => a.localeCompare(b));
24
+ return `{${entries.map(([key, entry]) => `${JSON.stringify(key)}:${stableSerialize(entry)}`).join(",")}}`;
25
+ }
26
+
27
+ function normalizePath(path: string) {
28
+ return path.trim().replace(/\.+$/g, "");
29
+ }
30
+
31
+ function pathMatchesRule(path: string, rawRule: string) {
32
+ const normalizedPath = normalizePath(path);
33
+ const rule = normalizePath(rawRule);
34
+ if (!normalizedPath || !rule) return false;
35
+ if (rule === "*") return true;
36
+ if (rule.endsWith(".*")) {
37
+ const prefix = rule.slice(0, -2);
38
+ return normalizedPath === prefix || normalizedPath.startsWith(`${prefix}.`);
39
+ }
40
+ return normalizedPath === rule || normalizedPath.startsWith(`${rule}.`);
41
+ }
42
+
43
+ function pushPath(set: Set<string>, path: string) {
44
+ const normalized = normalizePath(path);
45
+ if (normalized) {
46
+ set.add(normalized);
47
+ }
48
+ }
49
+
50
+ function collectPatchPaths(value: unknown, path: string, out: Set<string>) {
51
+ if (Array.isArray(value)) {
52
+ pushPath(out, path);
53
+ return;
54
+ }
55
+ if (!isPlainObject(value)) {
56
+ pushPath(out, path);
57
+ return;
58
+ }
59
+
60
+ const entries = Object.entries(value);
61
+ if (entries.length === 0) {
62
+ pushPath(out, path);
63
+ return;
64
+ }
65
+
66
+ for (const [key, child] of entries) {
67
+ const nextPath = path ? `${path}.${key}` : key;
68
+ collectPatchPaths(child, nextPath, out);
69
+ }
70
+ }
71
+
72
+ function collectChangedPaths(current: unknown, next: unknown, path: string, out: Set<string>) {
73
+ if (Array.isArray(current) || Array.isArray(next)) {
74
+ if (stableSerialize(current) !== stableSerialize(next)) {
75
+ pushPath(out, path);
76
+ }
77
+ return;
78
+ }
79
+
80
+ if (isPlainObject(current) && isPlainObject(next)) {
81
+ const keys = new Set([...Object.keys(current), ...Object.keys(next)]);
82
+ if (keys.size === 0) {
83
+ if (stableSerialize(current) !== stableSerialize(next)) {
84
+ pushPath(out, path);
85
+ }
86
+ return;
87
+ }
88
+ for (const key of keys) {
89
+ const left = current[key];
90
+ const right = next[key];
91
+ const nextPath = path ? `${path}.${key}` : key;
92
+ if (!(key in current) || !(key in next)) {
93
+ pushPath(out, nextPath);
94
+ continue;
95
+ }
96
+ collectChangedPaths(left, right, nextPath, out);
97
+ }
98
+ return;
99
+ }
100
+
101
+ if (stableSerialize(current) !== stableSerialize(next)) {
102
+ pushPath(out, path);
103
+ }
104
+ }
105
+
106
+ function evaluatePolicy(config: AgentMockingbirdConfig, changedPaths: string[]): ConfigPolicyDecision {
107
+ const policy = config.runtime.configPolicy;
108
+ const mode = policy.mode;
109
+ const rejected = new Set<string>();
110
+
111
+ for (const path of changedPaths) {
112
+ const denied = policy.denyPaths.some(rule => pathMatchesRule(path, rule));
113
+ if (denied) {
114
+ rejected.add(path);
115
+ continue;
116
+ }
117
+ if (mode === "strict") {
118
+ const allowed = policy.strictAllowPaths.some(rule => pathMatchesRule(path, rule));
119
+ if (!allowed) {
120
+ rejected.add(path);
121
+ }
122
+ }
123
+ }
124
+
125
+ return {
126
+ mode,
127
+ changedPaths,
128
+ rejectedPaths: [...rejected].sort((a, b) => a.localeCompare(b)),
129
+ requireExpectedHash: policy.requireExpectedHash,
130
+ requireSmokeTest: policy.requireSmokeTest,
131
+ autoRollbackOnFailure: policy.autoRollbackOnFailure,
132
+ };
133
+ }
134
+
135
+ export function evaluateConfigPolicyForPatch(config: AgentMockingbirdConfig, patch: unknown): ConfigPolicyDecision {
136
+ const changed = new Set<string>();
137
+ collectPatchPaths(patch, "", changed);
138
+ return evaluatePolicy(config, [...changed].sort((a, b) => a.localeCompare(b)));
139
+ }
140
+
141
+ export function evaluateConfigPolicyForReplace(
142
+ current: AgentMockingbirdConfig,
143
+ candidate: AgentMockingbirdConfig,
144
+ ): ConfigPolicyDecision {
145
+ const changed = new Set<string>();
146
+ collectChangedPaths(current, candidate, "", changed);
147
+ return evaluatePolicy(current, [...changed].sort((a, b) => a.localeCompare(b)));
148
+ }
149
+
150
+ export function assertConfigPolicyAllows(decision: ConfigPolicyDecision) {
151
+ if (decision.rejectedPaths.length > 0) {
152
+ throw new ConfigApplyError("policy", "Config policy rejected one or more changes", {
153
+ mode: decision.mode,
154
+ rejectedPaths: decision.rejectedPaths,
155
+ changedPaths: decision.changedPaths,
156
+ });
157
+ }
158
+ }
@@ -0,0 +1,15 @@
1
+ import { expect, test } from "bun:test";
2
+
3
+ import { agentTypeDefinitionSchema } from "./schema";
4
+
5
+ test("agent type schema rejects legacy heartbeat config blocks", () => {
6
+ const parsed = agentTypeDefinitionSchema.safeParse({
7
+ id: "agent-1",
8
+ heartbeat: {
9
+ enabled: true,
10
+ interval: "30m",
11
+ ackMaxChars: 300,
12
+ },
13
+ });
14
+ expect(parsed.success).toBe(false);
15
+ });
@@ -0,0 +1,391 @@
1
+ import { z } from "zod";
2
+
3
+ const stringListSchema = z.array(z.string().min(1)).transform(values => {
4
+ const normalized = values.map(value => value.trim()).filter(Boolean);
5
+ return [...new Set(normalized)];
6
+ });
7
+
8
+ const mcpServerIdSchema = z.string().min(1).regex(/^[A-Za-z0-9._-]+$/);
9
+ const mcpHeadersSchema = z.record(z.string(), z.string()).default({});
10
+ const mcpEnvironmentSchema = z.record(z.string(), z.string()).default({});
11
+
12
+ export const specialistAgentSchema = z.object({
13
+ id: z.string().min(1),
14
+ name: z.string().min(1),
15
+ specialty: z.string().min(1),
16
+ summary: z.string().min(1),
17
+ model: z.string().min(1),
18
+ status: z.enum(["available", "busy", "offline"]),
19
+ });
20
+
21
+ const agentTypeModeSchema = z.enum(["subagent", "primary", "all"]);
22
+ const openCodePermissionScalarSchema = z.enum(["allow", "deny", "ask"]);
23
+ const openCodePermissionRuleMapSchema = z.record(z.string(), openCodePermissionScalarSchema);
24
+ const openCodePermissionValueSchema = z.union([openCodePermissionScalarSchema, openCodePermissionRuleMapSchema]);
25
+ const openCodePermissionSchema = z.record(z.string(), openCodePermissionValueSchema);
26
+
27
+ const queueModeSchema = z.enum(["collect", "followup", "replace"]);
28
+
29
+ export const agentTypeDefinitionSchema = z
30
+ .object({
31
+ id: z.string().min(1),
32
+ name: z.string().min(1).optional(),
33
+ description: z.string().min(1).optional(),
34
+ prompt: z.string().min(1).optional(),
35
+ model: z.string().min(1).optional(),
36
+ variant: z.string().min(1).optional(),
37
+ mode: agentTypeModeSchema.default("subagent"),
38
+ hidden: z.boolean().default(false),
39
+ disable: z.boolean().default(false),
40
+ temperature: z.number().optional(),
41
+ topP: z.number().optional(),
42
+ steps: z.number().int().positive().optional(),
43
+ permission: openCodePermissionSchema.optional(),
44
+ options: z.record(z.string(), z.unknown()).default({}),
45
+ queueMode: queueModeSchema.optional(),
46
+ })
47
+ .strict();
48
+
49
+ const agentTypeDefinitionListSchema = z.array(agentTypeDefinitionSchema).transform(agentTypes => {
50
+ const deduped = new Map<string, (typeof agentTypes)[number]>();
51
+ for (const rawType of agentTypes) {
52
+ const id = rawType.id.trim();
53
+ if (!id) continue;
54
+ deduped.set(id, {
55
+ ...rawType,
56
+ id,
57
+ name: rawType.name?.trim() || undefined,
58
+ description: rawType.description?.trim() || undefined,
59
+ prompt: rawType.prompt?.trim() || undefined,
60
+ model: rawType.model?.trim() || undefined,
61
+ variant: rawType.variant?.trim() || undefined,
62
+ });
63
+ }
64
+ return [...deduped.values()].sort((a, b) => a.id.localeCompare(b.id));
65
+ });
66
+
67
+ export const configuredMcpServerSchema = z.discriminatedUnion("type", [
68
+ z
69
+ .object({
70
+ id: mcpServerIdSchema,
71
+ type: z.literal("remote"),
72
+ enabled: z.boolean().default(true),
73
+ url: z.string().url(),
74
+ headers: mcpHeadersSchema,
75
+ oauth: z.enum(["auto", "off"]).default("auto"),
76
+ timeoutMs: z.number().int().positive().optional(),
77
+ })
78
+ .strict(),
79
+ z
80
+ .object({
81
+ id: mcpServerIdSchema,
82
+ type: z.literal("local"),
83
+ enabled: z.boolean().default(true),
84
+ command: z.array(z.string().min(1)).min(1),
85
+ environment: mcpEnvironmentSchema,
86
+ timeoutMs: z.number().int().positive().optional(),
87
+ })
88
+ .strict(),
89
+ ]);
90
+
91
+ const configuredMcpServerListSchema = z.array(configuredMcpServerSchema).transform(servers => {
92
+ const deduped = new Map<string, (typeof servers)[number]>();
93
+ for (const server of servers) {
94
+ deduped.set(server.id, server);
95
+ }
96
+ return [...deduped.values()].sort((a, b) => a.id.localeCompare(b.id));
97
+ });
98
+
99
+ const runtimeOpencodeSchema = z
100
+ .object({
101
+ baseUrl: z.string().url(),
102
+ providerId: z.string().min(1),
103
+ modelId: z.string().min(1),
104
+ fallbackModels: stringListSchema.default([]),
105
+ imageModel: z.string().min(1).nullable().default(null),
106
+ smallModel: z.string().min(1),
107
+ timeoutMs: z.number().int().positive(),
108
+ promptTimeoutMs: z.number().int().positive(),
109
+ runWaitTimeoutMs: z.number().int().positive().default(180_000),
110
+ childSessionHideAfterDays: z.number().int().min(0).max(365).default(3),
111
+ directory: z.string().min(1).nullable().default(null),
112
+ bootstrap: z
113
+ .object({
114
+ enabled: z.boolean().default(true),
115
+ maxCharsPerFile: z.number().int().positive().default(20_000),
116
+ maxCharsTotal: z.number().int().positive().default(150_000),
117
+ subagentMinimal: z.boolean().default(true),
118
+ includeAgentPrompt: z.boolean().default(true),
119
+ })
120
+ .strict()
121
+ .default({
122
+ enabled: true,
123
+ maxCharsPerFile: 20_000,
124
+ maxCharsTotal: 150_000,
125
+ subagentMinimal: true,
126
+ includeAgentPrompt: true,
127
+ }),
128
+ })
129
+ .strict();
130
+
131
+ const runtimeSmokeTestSchema = z
132
+ .object({
133
+ prompt: z.string().min(1),
134
+ expectedResponsePattern: z.string().min(1),
135
+ })
136
+ .strict();
137
+
138
+ const runtimeRunStreamSchema = z
139
+ .object({
140
+ heartbeatMs: z.number().int().min(1_000).default(15_000),
141
+ replayPageSize: z.number().int().positive().max(1_000).default(200),
142
+ })
143
+ .strict();
144
+
145
+ const runtimeMemorySchema = z
146
+ .object({
147
+ enabled: z.boolean().default(true),
148
+ workspaceDir: z.string().min(1).default("./data/workspace"),
149
+ embedProvider: z.enum(["ollama", "none"]).default("ollama"),
150
+ embedModel: z.string().min(1).default("granite-embedding:278m"),
151
+ ollamaBaseUrl: z.string().url().default("http://127.0.0.1:11434"),
152
+ chunkTokens: z.number().int().positive().default(400),
153
+ chunkOverlap: z.number().int().min(0).default(80),
154
+ maxResults: z.number().int().positive().default(4),
155
+ minScore: z.number().min(0).max(1).default(0.35),
156
+ syncCooldownMs: z.number().int().min(0).default(10_000),
157
+ toolMode: z.enum(["hybrid", "inject_only", "tool_only"]).default("tool_only"),
158
+ injectionDedupeEnabled: z.boolean().default(true),
159
+ injectionDedupeFallbackRecallOnly: z.boolean().default(true),
160
+ injectionDedupeMaxTracked: z.number().int().min(32).max(10_000).default(256),
161
+ retrieval: z
162
+ .object({
163
+ engine: z.enum(["qmd_hybrid", "legacy"]).default("qmd_hybrid"),
164
+ strongSignalMinScore: z.number().min(0).max(1).default(0.85),
165
+ strongSignalMinGap: z.number().min(0).max(1).default(0.15),
166
+ candidateLimit: z.number().int().positive().max(200).default(40),
167
+ rrfK: z.number().int().positive().max(500).default(60),
168
+ expansionEnabled: z.boolean().default(true),
169
+ conceptExpansionEnabled: z.boolean().default(true),
170
+ conceptExpansionMaxPacks: z.number().int().positive().max(20).default(3),
171
+ conceptExpansionMaxTerms: z.number().int().positive().max(64).default(10),
172
+ rerankEnabled: z.boolean().default(true),
173
+ rerankTopN: z.number().int().positive().max(200).default(40),
174
+ semanticRescueEnabled: z.boolean().default(true),
175
+ semanticRescueMinVectorScore: z.number().min(0).max(1).default(0.75),
176
+ semanticRescueMaxResults: z.number().int().min(0).max(20).default(2),
177
+ expansionModel: z.string().min(1).nullable().default(null),
178
+ rerankModel: z.string().min(1).nullable().default(null),
179
+ vectorBackend: z.enum(["sqlite_vec", "legacy_json", "disabled"]).default("sqlite_vec"),
180
+ vectorUnavailableFallback: z.enum(["disabled", "legacy_json"]).default("disabled"),
181
+ vectorK: z.number().int().positive().max(500).default(60),
182
+ vectorProbeLimit: z.number().int().positive().max(200).default(20),
183
+ })
184
+ .strict()
185
+ .default({
186
+ engine: "qmd_hybrid",
187
+ strongSignalMinScore: 0.85,
188
+ strongSignalMinGap: 0.15,
189
+ candidateLimit: 40,
190
+ rrfK: 60,
191
+ expansionEnabled: true,
192
+ conceptExpansionEnabled: true,
193
+ conceptExpansionMaxPacks: 3,
194
+ conceptExpansionMaxTerms: 10,
195
+ rerankEnabled: true,
196
+ rerankTopN: 40,
197
+ semanticRescueEnabled: true,
198
+ semanticRescueMinVectorScore: 0.75,
199
+ semanticRescueMaxResults: 2,
200
+ expansionModel: null,
201
+ rerankModel: null,
202
+ vectorBackend: "sqlite_vec",
203
+ vectorUnavailableFallback: "disabled",
204
+ vectorK: 60,
205
+ vectorProbeLimit: 20,
206
+ }),
207
+ })
208
+ .strict();
209
+
210
+ const runtimeCronSchema = z
211
+ .object({
212
+ defaultMaxAttempts: z.number().int().min(1).default(3),
213
+ defaultRetryBackoffMs: z.number().int().min(1_000).default(30_000),
214
+ retryBackoffCapMs: z.number().int().min(1_000).default(3_600_000),
215
+ conditionalModuleTimeoutMs: z.number().int().min(1_000).max(300_000).default(30_000),
216
+ })
217
+ .strict();
218
+
219
+ const runtimeHeartbeatActiveHoursSchema = z
220
+ .object({
221
+ start: z.string().regex(/^\d{2}:\d{2}$/),
222
+ end: z.string().regex(/^\d{2}:\d{2}$/),
223
+ timezone: z.string().min(1),
224
+ })
225
+ .strict();
226
+
227
+ const runtimeHeartbeatSchema = z
228
+ .object({
229
+ enabled: z.boolean().default(true),
230
+ interval: z.string().regex(/^\d+[mhd]$/).default("30m"),
231
+ agentId: z.string().min(1).default("build"),
232
+ model: z.string().min(1),
233
+ prompt: z.string().min(1).default(
234
+ 'Read HEARTBEAT.md if it exists (workspace context). Follow it strictly. Do not infer or repeat old tasks from prior chats. If nothing needs attention, reply HEARTBEAT_OK.',
235
+ ),
236
+ ackMaxChars: z.number().int().positive().max(4_000).default(300),
237
+ activeHours: runtimeHeartbeatActiveHoursSchema.nullable().default(null),
238
+ })
239
+ .strict();
240
+
241
+ const runtimeQueueSchema = z
242
+ .object({
243
+ enabled: z.boolean().default(true),
244
+ defaultMode: queueModeSchema.default("collect"),
245
+ maxDepth: z.number().int().min(1).max(100).default(10),
246
+ coalesceDebounceMs: z.number().int().min(0).max(60_000).default(500),
247
+ })
248
+ .strict();
249
+
250
+ const runtimeConfigPolicySchema = z
251
+ .object({
252
+ mode: z.enum(["builder", "strict"]).default("builder"),
253
+ denyPaths: stringListSchema.default(["version", "runtime.configPolicy", "runtime.smokeTest"]),
254
+ strictAllowPaths: stringListSchema.default([
255
+ "runtime.opencode.runWaitTimeoutMs",
256
+ "runtime.opencode.childSessionHideAfterDays",
257
+ "runtime.opencode.bootstrap",
258
+ "runtime.opencode.imageModel",
259
+ "runtime.runStream",
260
+ "runtime.memory",
261
+ "runtime.heartbeat",
262
+ "runtime.cron",
263
+ "runtime.queue",
264
+ "ui.skills",
265
+ "ui.mcps",
266
+ "ui.mcpServers",
267
+ "ui.agents",
268
+ "ui.agentTypes",
269
+ ]),
270
+ requireExpectedHash: z.boolean().default(true),
271
+ requireSmokeTest: z.boolean().default(true),
272
+ autoRollbackOnFailure: z.boolean().default(true),
273
+ })
274
+ .strict();
275
+
276
+ export const agentMockingbirdConfigSchema = z
277
+ .object({
278
+ version: z.literal(2),
279
+ workspace: z
280
+ .object({
281
+ pinnedDirectory: z.string().min(1).default("./data/workspace"),
282
+ })
283
+ .strict(),
284
+ runtime: z
285
+ .object({
286
+ opencode: runtimeOpencodeSchema,
287
+ smokeTest: runtimeSmokeTestSchema,
288
+ runStream: runtimeRunStreamSchema.default({
289
+ heartbeatMs: 15_000,
290
+ replayPageSize: 200,
291
+ }),
292
+ memory: runtimeMemorySchema.default({
293
+ enabled: true,
294
+ workspaceDir: "./data/workspace",
295
+ embedProvider: "ollama",
296
+ embedModel: "granite-embedding:278m",
297
+ ollamaBaseUrl: "http://127.0.0.1:11434",
298
+ chunkTokens: 400,
299
+ chunkOverlap: 80,
300
+ maxResults: 4,
301
+ minScore: 0.35,
302
+ syncCooldownMs: 10_000,
303
+ toolMode: "tool_only",
304
+ injectionDedupeEnabled: true,
305
+ injectionDedupeFallbackRecallOnly: true,
306
+ injectionDedupeMaxTracked: 256,
307
+ retrieval: {
308
+ engine: "qmd_hybrid",
309
+ strongSignalMinScore: 0.85,
310
+ strongSignalMinGap: 0.15,
311
+ candidateLimit: 40,
312
+ rrfK: 60,
313
+ expansionEnabled: true,
314
+ conceptExpansionEnabled: true,
315
+ conceptExpansionMaxPacks: 3,
316
+ conceptExpansionMaxTerms: 10,
317
+ rerankEnabled: true,
318
+ rerankTopN: 40,
319
+ semanticRescueEnabled: true,
320
+ semanticRescueMinVectorScore: 0.75,
321
+ semanticRescueMaxResults: 2,
322
+ expansionModel: null,
323
+ rerankModel: null,
324
+ vectorBackend: "sqlite_vec",
325
+ vectorUnavailableFallback: "disabled",
326
+ vectorK: 60,
327
+ vectorProbeLimit: 20,
328
+ },
329
+ }),
330
+ heartbeat: runtimeHeartbeatSchema.default({
331
+ enabled: true,
332
+ interval: "30m",
333
+ agentId: "build",
334
+ model: "opencode/big-pickle",
335
+ prompt:
336
+ "Read HEARTBEAT.md if it exists (workspace context). Follow it strictly. Do not infer or repeat old tasks from prior chats. If nothing needs attention, reply HEARTBEAT_OK.",
337
+ ackMaxChars: 300,
338
+ activeHours: null,
339
+ }),
340
+ cron: runtimeCronSchema.default({
341
+ defaultMaxAttempts: 3,
342
+ defaultRetryBackoffMs: 30_000,
343
+ retryBackoffCapMs: 3_600_000,
344
+ conditionalModuleTimeoutMs: 30_000,
345
+ }),
346
+ queue: runtimeQueueSchema.default({
347
+ enabled: true,
348
+ defaultMode: "collect",
349
+ maxDepth: 10,
350
+ coalesceDebounceMs: 500,
351
+ }),
352
+ configPolicy: runtimeConfigPolicySchema.default({
353
+ mode: "builder",
354
+ denyPaths: ["version", "runtime.configPolicy", "runtime.smokeTest"],
355
+ strictAllowPaths: [
356
+ "runtime.opencode.runWaitTimeoutMs",
357
+ "runtime.opencode.childSessionHideAfterDays",
358
+ "runtime.opencode.bootstrap",
359
+ "runtime.opencode.imageModel",
360
+ "runtime.runStream",
361
+ "runtime.memory",
362
+ "runtime.heartbeat",
363
+ "runtime.cron",
364
+ "runtime.queue",
365
+ "ui.skills",
366
+ "ui.mcps",
367
+ "ui.mcpServers",
368
+ "ui.agents",
369
+ "ui.agentTypes",
370
+ ],
371
+ requireExpectedHash: true,
372
+ requireSmokeTest: true,
373
+ autoRollbackOnFailure: true,
374
+ }),
375
+ })
376
+ .strict(),
377
+ ui: z
378
+ .object({
379
+ skills: stringListSchema.default([]),
380
+ mcps: stringListSchema.default([]),
381
+ mcpServers: configuredMcpServerListSchema.default([]),
382
+ agents: z.array(specialistAgentSchema).default([]),
383
+ agentTypes: agentTypeDefinitionListSchema.default([]),
384
+ })
385
+ .strict(),
386
+ })
387
+ .strict();
388
+
389
+ export type AgentMockingbirdConfig = z.infer<typeof agentMockingbirdConfigSchema>;
390
+ export type ConfiguredMcpServer = z.infer<typeof configuredMcpServerSchema>;
391
+ export type AgentTypeDefinition = z.infer<typeof agentTypeDefinitionSchema>;
@@ -0,0 +1,34 @@
1
+ import { expect, test } from "bun:test";
2
+
3
+ import { resolveModelRefForValidation } from "./semantic";
4
+
5
+ function buildModelMap() {
6
+ return new Map<string, Set<string>>([
7
+ ["chutes", new Set(["zai-org/GLM-4.7-Flash", "openai/gpt-oss-20b"])],
8
+ ["anthropic", new Set(["claude-sonnet-4-5"])],
9
+ ]);
10
+ }
11
+
12
+ test("resolveModelRefForValidation keeps slash model IDs on default provider when exact model exists", () => {
13
+ const resolved = resolveModelRefForValidation("zai-org/GLM-4.7-Flash", "chutes", buildModelMap());
14
+ expect(resolved).toEqual({
15
+ providerId: "chutes",
16
+ modelId: "zai-org/GLM-4.7-Flash",
17
+ });
18
+ });
19
+
20
+ test("resolveModelRefForValidation accepts qualified provider/model references", () => {
21
+ const resolved = resolveModelRefForValidation("anthropic/claude-sonnet-4-5", "chutes", buildModelMap());
22
+ expect(resolved).toEqual({
23
+ providerId: "anthropic",
24
+ modelId: "claude-sonnet-4-5",
25
+ });
26
+ });
27
+
28
+ test("resolveModelRefForValidation falls back to default provider when provider prefix is unknown", () => {
29
+ const resolved = resolveModelRefForValidation("custom/my-model", "chutes", buildModelMap());
30
+ expect(resolved).toEqual({
31
+ providerId: "chutes",
32
+ modelId: "custom/my-model",
33
+ });
34
+ });