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,146 @@
1
+ import { afterEach, describe, expect, test } from "bun:test";
2
+ import { mkdirSync, mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
3
+ import { tmpdir } from "node:os";
4
+ import path from "node:path";
5
+
6
+ import type { AgentMockingbirdConfig } from "../config/schema";
7
+
8
+ const tempDirs: string[] = [];
9
+ const originalConfigPath = process.env.AGENT_MOCKINGBIRD_CONFIG_PATH;
10
+
11
+ afterEach(() => {
12
+ process.env.AGENT_MOCKINGBIRD_CONFIG_PATH = originalConfigPath;
13
+ for (const dir of tempDirs.splice(0)) {
14
+ rmSync(dir, { recursive: true, force: true });
15
+ }
16
+ });
17
+
18
+ describe("systemPrompt helpers", () => {
19
+ test("includes config, memory, cron, and workspace bootstrap guidance", async () => {
20
+ const root = mkdtempSync(path.join(tmpdir(), "agent-mockingbird-system-prompt-"));
21
+ tempDirs.push(root);
22
+ const workspaceDir = path.join(root, "workspace");
23
+ mkdirSync(workspaceDir, { recursive: true });
24
+ writeFileSync(path.join(workspaceDir, "AGENTS.md"), "# Workspace Guide\n\nUse the workspace guide.\n");
25
+ const exampleConfig = JSON.parse(
26
+ readFileSync(path.resolve(import.meta.dir, "../../../../../agent-mockingbird.config.example.json"), "utf8"),
27
+ ) as AgentMockingbirdConfig;
28
+ exampleConfig.workspace = {
29
+ pinnedDirectory: workspaceDir,
30
+ };
31
+ exampleConfig.runtime.opencode.directory = workspaceDir;
32
+ exampleConfig.runtime.memory.workspaceDir = workspaceDir;
33
+ exampleConfig.runtime.memory.enabled = true;
34
+ exampleConfig.runtime.memory.toolMode = "hybrid";
35
+ exampleConfig.runtime.opencode.bootstrap.enabled = true;
36
+ exampleConfig.runtime.opencode.bootstrap.includeAgentPrompt = false;
37
+ writeFileSync(
38
+ path.join(root, "config.json"),
39
+ JSON.stringify(exampleConfig),
40
+ );
41
+ process.env.AGENT_MOCKINGBIRD_CONFIG_PATH = path.join(root, "config.json");
42
+
43
+ const { buildAgentMockingbirdSystemPrompt } = await import("./systemPrompt");
44
+ const prompt = buildAgentMockingbirdSystemPrompt() ?? "";
45
+
46
+ expect(prompt).toContain("Config policy:");
47
+ expect(prompt).toContain("Memory policy:");
48
+ expect(prompt).toContain("Cron policy:");
49
+ expect(prompt).toContain("# Project Context");
50
+ expect(prompt).toContain("Use the workspace guide.");
51
+ });
52
+
53
+ test("builds a compaction prompt with OpenClaw-style headings and local guidance", async () => {
54
+ const root = mkdtempSync(path.join(tmpdir(), "agent-mockingbird-compaction-context-"));
55
+ tempDirs.push(root);
56
+ const workspaceDir = path.join(root, "workspace");
57
+ mkdirSync(workspaceDir, { recursive: true });
58
+ writeFileSync(path.join(workspaceDir, "AGENTS.md"), "# Workspace Guide\n\nUse the workspace guide.\n");
59
+ const exampleConfig = JSON.parse(
60
+ readFileSync(path.resolve(import.meta.dir, "../../../../../agent-mockingbird.config.example.json"), "utf8"),
61
+ ) as AgentMockingbirdConfig;
62
+ exampleConfig.workspace = {
63
+ pinnedDirectory: workspaceDir,
64
+ };
65
+ exampleConfig.runtime.opencode.directory = workspaceDir;
66
+ exampleConfig.runtime.memory.workspaceDir = workspaceDir;
67
+ exampleConfig.runtime.memory.enabled = true;
68
+ exampleConfig.runtime.memory.toolMode = "hybrid";
69
+ exampleConfig.runtime.opencode.bootstrap.enabled = true;
70
+ exampleConfig.runtime.opencode.bootstrap.includeAgentPrompt = false;
71
+ writeFileSync(path.join(root, "config.json"), JSON.stringify(exampleConfig));
72
+ process.env.AGENT_MOCKINGBIRD_CONFIG_PATH = path.join(root, "config.json");
73
+
74
+ const { buildAgentMockingbirdCompactionPrompt } = await import("./systemPrompt");
75
+ const prompt = buildAgentMockingbirdCompactionPrompt();
76
+
77
+ expect(prompt).toContain("Compaction rules:");
78
+ expect(prompt).toContain("## Decisions");
79
+ expect(prompt).toContain("## Pending user asks");
80
+ expect(prompt).toContain("## Exact identifiers");
81
+ expect(prompt).toContain("Agent Mockingbird continuation notes:");
82
+ expect(prompt).toContain("Memory follow-through:");
83
+ expect(prompt).toContain("Workspace bootstrap context:");
84
+ expect(prompt).toContain("# Project Context");
85
+ });
86
+
87
+ test("adds session-aware compaction prompt context from the mirrored transcript", async () => {
88
+ const root = mkdtempSync(path.join(tmpdir(), "agent-mockingbird-compaction-session-"));
89
+ tempDirs.push(root);
90
+ const workspaceDir = path.join(root, "workspace");
91
+ mkdirSync(workspaceDir, { recursive: true });
92
+ writeFileSync(path.join(workspaceDir, "AGENTS.md"), "# Workspace Guide\n\nUse the workspace guide.\n");
93
+ const exampleConfig = JSON.parse(
94
+ readFileSync(path.resolve(import.meta.dir, "../../../../../agent-mockingbird.config.example.json"), "utf8"),
95
+ ) as AgentMockingbirdConfig;
96
+ exampleConfig.workspace = {
97
+ pinnedDirectory: workspaceDir,
98
+ };
99
+ exampleConfig.runtime.opencode.directory = workspaceDir;
100
+ exampleConfig.runtime.memory.workspaceDir = workspaceDir;
101
+ writeFileSync(path.join(root, "config.json"), JSON.stringify(exampleConfig));
102
+ process.env.AGENT_MOCKINGBIRD_CONFIG_PATH = path.join(root, "config.json");
103
+
104
+ const { appendChatExchange, createSession, resetDatabaseToDefaults, setRuntimeSessionBinding } = await import(
105
+ "../db/repository"
106
+ );
107
+ resetDatabaseToDefaults();
108
+ const session = createSession({ title: "Compaction Test" });
109
+ setRuntimeSessionBinding("opencode", session.id, "sess-ctx");
110
+ appendChatExchange({
111
+ sessionId: session.id,
112
+ userContent: "Please update apps/server/src/backend/opencode/systemPrompt.ts and keep port 3001 and https://example.test/docs in mind.",
113
+ assistantContent: "I am investigating the compaction hook and runtime routes now.",
114
+ source: "runtime",
115
+ usage: {
116
+ requestCountDelta: 1,
117
+ inputTokensDelta: 10,
118
+ outputTokensDelta: 10,
119
+ estimatedCostUsdDelta: 0,
120
+ },
121
+ });
122
+ appendChatExchange({
123
+ sessionId: session.id,
124
+ userContent: "Do not lose the unresolved follow-up about /api/mockingbird/runtime/compaction-context on 2026-03-15.",
125
+ assistantContent: "I will preserve that exact endpoint and date in the continuation notes.",
126
+ source: "runtime",
127
+ usage: {
128
+ requestCountDelta: 1,
129
+ inputTokensDelta: 10,
130
+ outputTokensDelta: 10,
131
+ estimatedCostUsdDelta: 0,
132
+ },
133
+ });
134
+
135
+ const { buildAgentMockingbirdCompactionPrompt } = await import("./systemPrompt");
136
+ const prompt = buildAgentMockingbirdCompactionPrompt("sess-ctx");
137
+
138
+ expect(prompt).toContain("Session-specific context to preserve:");
139
+ expect(prompt).toContain("Transcript continuity requirements:");
140
+ expect(prompt).toContain("Latest user ask to carry forward:");
141
+ expect(prompt).toContain("/api/mockingbird/runtime/compaction-context");
142
+ expect(prompt).toContain("2026-03-15");
143
+ expect(prompt).toContain("Recent turns to preserve verbatim when useful:");
144
+ expect(prompt).toContain("apps/server/src/backend/opencode/systemPrompt.ts");
145
+ });
146
+ });
@@ -0,0 +1,284 @@
1
+ import { buildWorkspaceBootstrapPromptContext } from "../agents/bootstrapContext";
2
+ import { getConfigSnapshot } from "../config/service";
3
+ import {
4
+ getLocalSessionIdByRuntimeBinding,
5
+ listMessagesForSession,
6
+ } from "../db/repository";
7
+ import { env } from "../env";
8
+
9
+ const OPENCODE_RUNTIME_ID = "opencode";
10
+ const OPENCODE_COMPACTION_HEADINGS = [
11
+ "## Decisions",
12
+ "## Open TODOs",
13
+ "## Constraints/Rules",
14
+ "## Pending user asks",
15
+ "## Exact identifiers",
16
+ ] as const;
17
+ const MAX_COMPACTION_RECENT_MESSAGES = 6;
18
+ const MAX_COMPACTION_RECENT_MESSAGE_CHARS = 220;
19
+ const MAX_COMPACTION_IDENTIFIERS = 10;
20
+ const MAX_COMPACTION_PATHS = 8;
21
+
22
+ function trimCompactionLine(value: string) {
23
+ const compact = value.replace(/\s+/g, " ").trim();
24
+ if (compact.length <= MAX_COMPACTION_RECENT_MESSAGE_CHARS) return compact;
25
+ return `${compact.slice(0, MAX_COMPACTION_RECENT_MESSAGE_CHARS - 3).trimEnd()}...`;
26
+ }
27
+
28
+ function collectRecentTranscriptLines(externalSessionId?: string) {
29
+ const sessionId = externalSessionId?.trim()
30
+ ? getLocalSessionIdByRuntimeBinding(OPENCODE_RUNTIME_ID, externalSessionId)
31
+ : null;
32
+ if (!sessionId) return [];
33
+ const messages = listMessagesForSession(sessionId);
34
+ return messages
35
+ .slice(-MAX_COMPACTION_RECENT_MESSAGES)
36
+ .map(message => {
37
+ const content = trimCompactionLine(message.content);
38
+ if (!content) return null;
39
+ const role = message.role === "user" ? "User" : "Assistant";
40
+ return `- ${role}: ${content}`;
41
+ })
42
+ .filter((line): line is string => Boolean(line));
43
+ }
44
+
45
+ function collectLatestUserAsk(externalSessionId?: string) {
46
+ const sessionId = externalSessionId?.trim()
47
+ ? getLocalSessionIdByRuntimeBinding(OPENCODE_RUNTIME_ID, externalSessionId)
48
+ : null;
49
+ if (!sessionId) return null;
50
+ const messages = listMessagesForSession(sessionId);
51
+ for (let index = messages.length - 1; index >= 0; index -= 1) {
52
+ const message = messages[index];
53
+ if (message?.role !== "user") continue;
54
+ const content = trimCompactionLine(message.content);
55
+ if (content) return content;
56
+ }
57
+ return null;
58
+ }
59
+
60
+ function collectOpaqueIdentifiers(externalSessionId?: string) {
61
+ const sessionId = externalSessionId?.trim()
62
+ ? getLocalSessionIdByRuntimeBinding(OPENCODE_RUNTIME_ID, externalSessionId)
63
+ : null;
64
+ if (!sessionId) return [];
65
+ const messages = listMessagesForSession(sessionId);
66
+ const haystack = messages.slice(-10).map(message => message.content).join("\n");
67
+ const matches =
68
+ haystack.match(
69
+ /([A-Fa-f0-9]{8,}|https?:\/\/\S+|\/[\w./-]{2,}|[A-Za-z]:\\[\w\\./-]+|\b\d{4}-\d{2}-\d{2}\b|\b\d{6,}\b|localhost:\d{2,5}|\b\d{2,5}\b)/g,
70
+ ) ?? [];
71
+ return [...new Set(matches.map(match => match.replace(/[),.;]+$/g, "").trim()).filter(Boolean))].slice(
72
+ 0,
73
+ MAX_COMPACTION_IDENTIFIERS,
74
+ );
75
+ }
76
+
77
+ function collectMentionedPaths(externalSessionId?: string) {
78
+ const sessionId = externalSessionId?.trim()
79
+ ? getLocalSessionIdByRuntimeBinding(OPENCODE_RUNTIME_ID, externalSessionId)
80
+ : null;
81
+ if (!sessionId) return [];
82
+ const messages = listMessagesForSession(sessionId);
83
+ const haystack = messages.slice(-12).map(message => message.content).join("\n");
84
+ const matches =
85
+ haystack.match(
86
+ /\b(?:[A-Za-z0-9_.-]+\/)+[A-Za-z0-9_.-]+\b|\b[A-Za-z0-9_.-]+\.(?:ts|tsx|js|jsx|json|md|sql|mjs|cjs|css|html)\b/g,
87
+ ) ?? [];
88
+ return [...new Set(matches.map(match => match.replace(/[),.;]+$/g, "").trim()).filter(Boolean))].slice(
89
+ 0,
90
+ MAX_COMPACTION_PATHS,
91
+ );
92
+ }
93
+
94
+ function buildSessionAwareCompactionContext(externalSessionId?: string) {
95
+ const latestAsk = collectLatestUserAsk(externalSessionId);
96
+ const identifiers = collectOpaqueIdentifiers(externalSessionId);
97
+ const paths = collectMentionedPaths(externalSessionId);
98
+ const recentTranscript = collectRecentTranscriptLines(externalSessionId);
99
+ const sections: string[] = [];
100
+
101
+ if (latestAsk || identifiers.length > 0 || paths.length > 0) {
102
+ const lines = [
103
+ "Transcript continuity requirements:",
104
+ "- Preserve the latest unresolved user ask and any unfinished work, approvals, or blockers.",
105
+ "- Preserve exact literal identifiers when they matter for continuation: file paths, URLs, ports, dates, hashes, IDs.",
106
+ ];
107
+ if (latestAsk) {
108
+ lines.push(`- Latest user ask to carry forward: ${latestAsk}`);
109
+ }
110
+ if (identifiers.length > 0) {
111
+ lines.push(`- Exact identifiers seen recently: ${identifiers.join(", ")}`);
112
+ }
113
+ if (paths.length > 0) {
114
+ lines.push(`- Files and paths explicitly mentioned: ${paths.join(", ")}`);
115
+ }
116
+ sections.push(lines.join("\n"));
117
+ }
118
+
119
+ if (recentTranscript.length > 0) {
120
+ sections.push(
121
+ [
122
+ "Recent turns to preserve verbatim when useful:",
123
+ ...recentTranscript,
124
+ ].join("\n"),
125
+ );
126
+ }
127
+
128
+ return sections;
129
+ }
130
+
131
+ function currentMemoryConfig() {
132
+ return getConfigSnapshot().config.runtime.memory;
133
+ }
134
+
135
+ function buildCompactionRequirementLines() {
136
+ return [
137
+ "Compaction rules:",
138
+ "- Write the summary body in the primary language used in the conversation.",
139
+ "- Keep the exact section headings shown below unchanged.",
140
+ "- Do not translate or alter code, file paths, identifiers, error messages, URLs, ports, dates, hashes, or filenames.",
141
+ "- Preserve active tasks, current status, blockers, approvals, unresolved questions, and promised follow-ups.",
142
+ "- Preserve the latest unresolved user ask even if it appeared very recently.",
143
+ "- Prefer recent context over older history when choosing what to keep.",
144
+ "",
145
+ "Produce the summary with these exact headings, in this exact order:",
146
+ ...OPENCODE_COMPACTION_HEADINGS,
147
+ ];
148
+ }
149
+
150
+ function buildConfigPolicyLines() {
151
+ return [
152
+ "Config policy:",
153
+ "- Use config_manager for runtime configuration changes.",
154
+ "- Use agent_type_manager for dedicated agent type CRUD operations.",
155
+ "- Prefer patch_config with expectedHash from get_config to avoid conflicts.",
156
+ "- Safe config writes enforce policy checks and may reject protected paths.",
157
+ "- Keep runSmokeTest enabled unless explicitly instructed otherwise.",
158
+ ];
159
+ }
160
+
161
+ function buildInteractionPolicyLines() {
162
+ return [
163
+ "Interaction policy:",
164
+ "- If the user asks for an interactive multiple-choice question, use the question UI/tool instead of plain text.",
165
+ "- Keep option labels short and descriptions concise.",
166
+ ];
167
+ }
168
+
169
+ function buildMemoryPolicyLines() {
170
+ return [
171
+ "Memory policy:",
172
+ "- Use memory_search when a request likely depends on prior durable context.",
173
+ "- Prefer one search call first; then use memory_get only for the top 1-2 cited records before relying on details.",
174
+ "- For people/relationships, use concrete terms (for example: daughter, spouse, partner, child, parent, names) instead of only generic words.",
175
+ "- For broad domains (for example: portfolio), run one adjacent-term refinement (for example: metals, silver, bonds, allocation) if the first search misses.",
176
+ "- Skip memory tool calls for clearly self-contained tasks.",
177
+ "- If the first memory_search misses, do one refined query with entity/relationship terms before concluding no memory exists.",
178
+ "- Use memory_remember when new context could be useful later.",
179
+ "- Prefer supersedes when replacing older memory records.",
180
+ ];
181
+ }
182
+
183
+ function buildCronPolicyLines() {
184
+ return [
185
+ "Cron policy:",
186
+ "- Use cron_manager for recurring automation and background checks.",
187
+ "- Prefer deterministic jobs when possible; only invoke the model when useful.",
188
+ "- Review existing jobs before creating new ones to avoid duplicates.",
189
+ ];
190
+ }
191
+
192
+ export function buildAgentMockingbirdSystemPrompt() {
193
+ const memoryConfig = currentMemoryConfig();
194
+ const config = getConfigSnapshot().config;
195
+ const workspaceContext = buildWorkspaceBootstrapPromptContext({
196
+ config,
197
+ });
198
+ const lines: string[] = [];
199
+
200
+ lines.push(...buildConfigPolicyLines());
201
+
202
+ lines.push("");
203
+ lines.push(...buildInteractionPolicyLines());
204
+
205
+ if (memoryConfig.enabled && memoryConfig.toolMode !== "inject_only") {
206
+ lines.push("", ...buildMemoryPolicyLines());
207
+ }
208
+
209
+ if (env.AGENT_MOCKINGBIRD_CRON_ENABLED) {
210
+ lines.push("", ...buildCronPolicyLines());
211
+ }
212
+
213
+ if (workspaceContext.section) {
214
+ lines.push("", workspaceContext.section);
215
+ }
216
+
217
+ return lines.length ? lines.join("\n") : undefined;
218
+ }
219
+
220
+ export function buildAgentMockingbirdCompactionContext(externalSessionId?: string) {
221
+ const memoryConfig = currentMemoryConfig();
222
+ const workspaceContext = buildWorkspaceBootstrapPromptContext({
223
+ config: getConfigSnapshot().config,
224
+ });
225
+ const sections: string[] = [];
226
+
227
+ sections.push(
228
+ [
229
+ "Agent Mockingbird continuation notes:",
230
+ "- Mention any config changes made through config_manager or agent_type_manager and whether they were applied successfully.",
231
+ "- Mention any pending approvals or interactive questions that still block progress.",
232
+ "- Mention any cron jobs or automation changes that were created, updated, or investigated if they affect next steps.",
233
+ ].join("\n"),
234
+ );
235
+
236
+ if (memoryConfig.enabled && memoryConfig.toolMode !== "inject_only") {
237
+ sections.push(
238
+ [
239
+ "Memory follow-through:",
240
+ "- Include any retrieved memory records that materially changed the approach.",
241
+ "- Include any new facts that should probably be persisted with memory_remember if they were not saved yet.",
242
+ ].join("\n"),
243
+ );
244
+ }
245
+
246
+ sections.push(...buildSessionAwareCompactionContext(externalSessionId));
247
+
248
+ if (workspaceContext.section) {
249
+ sections.push(
250
+ [
251
+ "Workspace bootstrap context:",
252
+ workspaceContext.section,
253
+ ].join("\n"),
254
+ );
255
+ }
256
+
257
+ return sections;
258
+ }
259
+
260
+ export function buildAgentMockingbirdCompactionPrompt(externalSessionId?: string) {
261
+ const contextSections = buildAgentMockingbirdCompactionContext(externalSessionId);
262
+ const lines: string[] = [];
263
+
264
+ lines.push(
265
+ "You are generating a compact factual continuation summary for an Agent Mockingbird coding session.",
266
+ );
267
+ lines.push(
268
+ "The next agent will use your summary to resume work without losing important technical context.",
269
+ );
270
+ lines.push("");
271
+ lines.push(...buildCompactionRequirementLines());
272
+
273
+ if (contextSections.length > 0) {
274
+ lines.push("");
275
+ lines.push("Session-specific context to preserve:");
276
+ lines.push("");
277
+ lines.push(...contextSections);
278
+ }
279
+
280
+ lines.push("");
281
+ lines.push("Return only the summary, using the exact headings above.");
282
+
283
+ return lines.join("\n");
284
+ }
@@ -0,0 +1,57 @@
1
+ import { createHash } from "node:crypto";
2
+ import { existsSync } from "node:fs";
3
+ import path from "node:path";
4
+
5
+ import { env } from "./env";
6
+
7
+ function sourceRoot() {
8
+ return path.resolve(import.meta.dir, "../../../../");
9
+ }
10
+
11
+ export function getBinaryDir(): string {
12
+ return process.cwd();
13
+ }
14
+
15
+ function getProjectRoot(): string {
16
+ return process.cwd();
17
+ }
18
+
19
+ export function resolveDataPath(...segments: string[]): string {
20
+ return path.resolve(getProjectRoot(), "data", ...segments);
21
+ }
22
+
23
+ function resolveAgentMockingbirdDataDir(): string {
24
+ const configuredPath = env.AGENT_MOCKINGBIRD_CONFIG_PATH?.trim();
25
+ if (!configuredPath) {
26
+ return resolveDataPath();
27
+ }
28
+ return path.dirname(path.resolve(configuredPath));
29
+ }
30
+
31
+ function workspaceFingerprint(workspaceDir: string): string {
32
+ return createHash("sha256").update(path.resolve(workspaceDir)).digest("hex").slice(0, 16);
33
+ }
34
+
35
+ export function resolveManagedOpencodeConfigDir(workspaceDir: string): string {
36
+ const explicitConfigDir = process.env.OPENCODE_CONFIG_DIR?.trim();
37
+ if (explicitConfigDir) {
38
+ return path.resolve(explicitConfigDir);
39
+ }
40
+ return path.join(resolveAgentMockingbirdDataDir(), "opencode-config", workspaceFingerprint(workspaceDir));
41
+ }
42
+
43
+ export function resolveAppDistDir(): string | null {
44
+ const candidates = [
45
+ path.resolve(process.cwd(), "dist", "app"),
46
+ path.resolve(process.cwd(), "vendor", "opencode", "packages", "app", "dist"),
47
+ path.resolve(sourceRoot(), "dist", "app"),
48
+ path.resolve(sourceRoot(), "vendor", "opencode", "packages", "app", "dist"),
49
+ path.resolve(path.dirname(process.execPath), "app"),
50
+ ];
51
+ for (const candidate of candidates) {
52
+ if (existsSync(path.join(candidate, "index.html"))) {
53
+ return candidate;
54
+ }
55
+ }
56
+ return null;
57
+ }
@@ -0,0 +1,100 @@
1
+ import {
2
+ type QuestionInfo,
3
+ type QuestionRequest,
4
+ type PermissionRequest,
5
+ } from "@opencode-ai/sdk/v2/client";
6
+
7
+ import { getConfigSnapshot } from "../config/service";
8
+ import { getLocalSessionIdByRuntimeBinding } from "../db/repository";
9
+ import { createOpencodeV2ClientFromConnection, unwrapSdkData } from "../opencode/client";
10
+
11
+ const OPENCODE_RUNTIME_ID = "opencode";
12
+
13
+ interface PendingPermissionPrompt {
14
+ id: string;
15
+ sessionId: string;
16
+ permission: string;
17
+ patterns: string[];
18
+ metadata: Record<string, unknown>;
19
+ always: string[];
20
+ }
21
+
22
+ interface PendingQuestionPrompt {
23
+ id: string;
24
+ sessionId: string;
25
+ questions: QuestionInfo[];
26
+ }
27
+
28
+ interface PendingPromptsSnapshot {
29
+ pendingPermissions: PendingPermissionPrompt[];
30
+ pendingQuestions: PendingQuestionPrompt[];
31
+ }
32
+
33
+ function createPromptClient() {
34
+ const config = getConfigSnapshot().config.runtime.opencode;
35
+ return {
36
+ client: createOpencodeV2ClientFromConnection({
37
+ baseUrl: config.baseUrl,
38
+ directory: config.directory,
39
+ }),
40
+ timeoutMs: config.timeoutMs,
41
+ };
42
+ }
43
+
44
+ function toLocalSessionId(externalSessionId: string) {
45
+ return getLocalSessionIdByRuntimeBinding(OPENCODE_RUNTIME_ID, externalSessionId);
46
+ }
47
+
48
+ function normalizePendingPermission(input: PermissionRequest): PendingPermissionPrompt | null {
49
+ const localSessionId = toLocalSessionId(input.sessionID);
50
+ if (!localSessionId) return null;
51
+ return {
52
+ id: input.id,
53
+ sessionId: localSessionId,
54
+ permission: input.permission,
55
+ patterns: Array.isArray(input.patterns) ? input.patterns : [],
56
+ metadata:
57
+ input.metadata && typeof input.metadata === "object" && !Array.isArray(input.metadata)
58
+ ? input.metadata
59
+ : {},
60
+ always: Array.isArray(input.always) ? input.always : [],
61
+ };
62
+ }
63
+
64
+ function normalizePendingQuestion(input: QuestionRequest): PendingQuestionPrompt | null {
65
+ const localSessionId = toLocalSessionId(input.sessionID);
66
+ if (!localSessionId) return null;
67
+ return {
68
+ id: input.id,
69
+ sessionId: localSessionId,
70
+ questions: Array.isArray(input.questions) ? input.questions : [],
71
+ };
72
+ }
73
+
74
+ export async function listPendingPrompts(): Promise<PendingPromptsSnapshot> {
75
+ const { client, timeoutMs } = createPromptClient();
76
+ const options = {
77
+ responseStyle: "data" as const,
78
+ throwOnError: true as const,
79
+ signal: AbortSignal.timeout(timeoutMs),
80
+ };
81
+
82
+ const [permissionListRaw, questionListRaw] = await Promise.all([
83
+ client.permission.list(undefined, options),
84
+ client.question.list(undefined, options),
85
+ ]);
86
+
87
+ const permissions = (unwrapSdkData<PermissionRequest[]>(permissionListRaw) ?? [])
88
+ .map(normalizePendingPermission)
89
+ .filter((item): item is PendingPermissionPrompt => Boolean(item))
90
+ .sort((left, right) => left.id.localeCompare(right.id));
91
+ const questions = (unwrapSdkData<QuestionRequest[]>(questionListRaw) ?? [])
92
+ .map(normalizePendingQuestion)
93
+ .filter((item): item is PendingQuestionPrompt => Boolean(item))
94
+ .sort((left, right) => left.id.localeCompare(right.id));
95
+
96
+ return {
97
+ pendingPermissions: permissions,
98
+ pendingQuestions: questions,
99
+ };
100
+ }