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,715 @@
1
+ #!/usr/bin/env bun
2
+ import {
3
+ existsSync,
4
+ mkdirSync,
5
+ mkdtempSync,
6
+ readFileSync,
7
+ readdirSync,
8
+ rmSync,
9
+ writeFileSync,
10
+ } from "node:fs";
11
+ import os from "node:os";
12
+ import path from "node:path";
13
+ import process from "node:process";
14
+ import { spawnSync } from "node:child_process";
15
+
16
+ type LockFile = {
17
+ upstream: {
18
+ remote: string;
19
+ tag: string;
20
+ commit: string;
21
+ };
22
+ packageVersion: string;
23
+ paths: {
24
+ cleanroom: string;
25
+ vendor: string;
26
+ patches: string;
27
+ };
28
+ branch: {
29
+ name: string;
30
+ };
31
+ };
32
+
33
+ type ParsedArgs = {
34
+ status: boolean;
35
+ json: boolean;
36
+ rebuildOnly: boolean;
37
+ exportPatches: boolean;
38
+ check: boolean;
39
+ ref?: string;
40
+ hardRef?: string;
41
+ };
42
+
43
+ type ExecOptions = {
44
+ cwd?: string;
45
+ env?: NodeJS.ProcessEnv;
46
+ allowFailure?: boolean;
47
+ };
48
+
49
+ type StatusReport = {
50
+ lock: {
51
+ tag: string;
52
+ commit: string;
53
+ packageVersion: string;
54
+ };
55
+ cleanroom: {
56
+ path: string;
57
+ exists: boolean;
58
+ pristine: boolean | null;
59
+ head: string | null;
60
+ matchesLock: boolean | null;
61
+ };
62
+ branch: {
63
+ name: string;
64
+ head: string | null;
65
+ };
66
+ vendor: {
67
+ path: string;
68
+ exists: boolean;
69
+ state: "missing" | "clean" | "dirty" | "conflicted" | "invalid";
70
+ head: string | null;
71
+ };
72
+ patches: {
73
+ path: string;
74
+ count: number;
75
+ matchesBranch: boolean | null;
76
+ };
77
+ };
78
+
79
+ const repoRoot = path.resolve(import.meta.dir, "..");
80
+ const lockPath = path.join(repoRoot, "opencode.lock.json");
81
+ const patchComparePrefix = "opencode-sync-export-";
82
+
83
+ async function main() {
84
+ const args = parseArgs(process.argv.slice(2));
85
+ const lock = readLock();
86
+
87
+ if (args.status) {
88
+ const report = collectStatus(lock);
89
+ printStatus(report, args.json);
90
+ process.exit(report.vendor.state === "invalid" ? 1 : 0);
91
+ }
92
+
93
+ if (args.check) {
94
+ runCheck(lock);
95
+ return;
96
+ }
97
+
98
+ if (args.exportPatches) {
99
+ ensureCleanroomClone(lock);
100
+ ensureCleanroomAtCommit(lock, lock.upstream.commit);
101
+ ensureVendorBranchState(lock, { requirePatchesMatch: false });
102
+ exportPatchesFromBranch(lock);
103
+ verifyBranchMatchesPatches(lock);
104
+ console.log("Exported patches/opencode successfully.");
105
+ return;
106
+ }
107
+
108
+ if (args.rebuildOnly) {
109
+ ensureCleanroomClone(lock);
110
+ ensureCleanroomAtCommit(lock, lock.upstream.commit);
111
+ recreateVendorWorktree(lock, lock.upstream.commit);
112
+ verifyBranchMatchesPatches(lock);
113
+ console.log(`Rebuilt ${lock.paths.vendor} from ${lock.upstream.tag}.`);
114
+ return;
115
+ }
116
+
117
+ if (args.ref) {
118
+ ensureCleanroomClone(lock);
119
+ ensureCleanroomPristine(lock);
120
+ const targetTag = normalizeTag(args.ref);
121
+ const targetCommit = resolveUpstreamCommit(lock.paths.cleanroom, targetTag);
122
+ ensureVendorSafeForRefChange(lock);
123
+ ensureCleanroomAtCommit(lock, targetCommit);
124
+ recreateVendorWorktree(lock, targetCommit);
125
+ runValidation(lock.paths.vendor);
126
+ exportPatchesFromBranch(lock, targetCommit);
127
+ verifyBranchMatchesPatches(lock);
128
+ verifyPatchReproducibility(lock, {
129
+ baseCommit: targetCommit,
130
+ compareDir: path.resolve(repoRoot, lock.paths.vendor),
131
+ useTemporaryClone: true,
132
+ });
133
+ writeLock(lock, {
134
+ upstream: {
135
+ remote: lock.upstream.remote,
136
+ tag: targetTag,
137
+ commit: targetCommit,
138
+ },
139
+ packageVersion: stripLeadingV(targetTag),
140
+ paths: lock.paths,
141
+ branch: lock.branch,
142
+ });
143
+ console.log(`Updated ${path.relative(repoRoot, lockPath)} to ${targetTag} (${targetCommit}).`);
144
+ return;
145
+ }
146
+
147
+ if (args.hardRef) {
148
+ ensureCleanroomClone(lock);
149
+ ensureCleanroomPristine(lock);
150
+ const targetTag = normalizeTag(args.hardRef);
151
+ const targetCommit = resolveUpstreamCommit(lock.paths.cleanroom, targetTag);
152
+ ensureCleanroomAtCommit(lock, targetCommit);
153
+ recreateVendorWorktree(lock, targetCommit, { force: true });
154
+ runValidation(lock.paths.vendor);
155
+ exportPatchesFromBranch(lock, targetCommit);
156
+ verifyBranchMatchesPatches(lock);
157
+ verifyPatchReproducibility(lock, {
158
+ baseCommit: targetCommit,
159
+ compareDir: path.resolve(repoRoot, lock.paths.vendor),
160
+ useTemporaryClone: true,
161
+ });
162
+ writeLock(lock, {
163
+ upstream: {
164
+ remote: lock.upstream.remote,
165
+ tag: targetTag,
166
+ commit: targetCommit,
167
+ },
168
+ packageVersion: stripLeadingV(targetTag),
169
+ paths: lock.paths,
170
+ branch: lock.branch,
171
+ });
172
+ console.log(`Force-updated ${path.relative(repoRoot, lockPath)} to ${targetTag} (${targetCommit}).`);
173
+ return;
174
+ }
175
+
176
+ throw new Error(
177
+ "Specify one of --status, --rebuild-only, --export-patches, --check, --ref <tag>, or --hard-ref <tag>.",
178
+ );
179
+ }
180
+
181
+ function parseArgs(argv: string[]): ParsedArgs {
182
+ const parsed: ParsedArgs = {
183
+ status: false,
184
+ json: false,
185
+ rebuildOnly: false,
186
+ exportPatches: false,
187
+ check: false,
188
+ };
189
+
190
+ for (let index = 0; index < argv.length; index += 1) {
191
+ const arg = argv[index];
192
+ if (arg === "--status") {
193
+ parsed.status = true;
194
+ continue;
195
+ }
196
+ if (arg === "--json") {
197
+ parsed.json = true;
198
+ continue;
199
+ }
200
+ if (arg === "--rebuild-only") {
201
+ parsed.rebuildOnly = true;
202
+ continue;
203
+ }
204
+ if (arg === "--export-patches") {
205
+ parsed.exportPatches = true;
206
+ continue;
207
+ }
208
+ if (arg === "--check") {
209
+ parsed.check = true;
210
+ continue;
211
+ }
212
+ if (arg === "--ref") {
213
+ const value = argv[index + 1];
214
+ if (!value) {
215
+ throw new Error("Missing value after --ref.");
216
+ }
217
+ parsed.ref = value;
218
+ index += 1;
219
+ continue;
220
+ }
221
+ if (arg === "--hard-ref") {
222
+ const value = argv[index + 1];
223
+ if (!value) {
224
+ throw new Error("Missing value after --hard-ref.");
225
+ }
226
+ parsed.hardRef = value;
227
+ index += 1;
228
+ continue;
229
+ }
230
+ throw new Error(`Unknown argument: ${arg}`);
231
+ }
232
+
233
+ const selected = [
234
+ parsed.status,
235
+ parsed.rebuildOnly,
236
+ parsed.exportPatches,
237
+ parsed.check,
238
+ Boolean(parsed.ref),
239
+ Boolean(parsed.hardRef),
240
+ ].filter(Boolean);
241
+ if (selected.length !== 1) {
242
+ throw new Error("Exactly one primary operation is required.");
243
+ }
244
+ if (parsed.json && !parsed.status) {
245
+ throw new Error("--json is only supported together with --status.");
246
+ }
247
+
248
+ return parsed;
249
+ }
250
+
251
+ function readLock(): LockFile {
252
+ if (!existsSync(lockPath)) {
253
+ throw new Error(`Missing lock file: ${path.relative(repoRoot, lockPath)}`);
254
+ }
255
+ return JSON.parse(readFileSync(lockPath, "utf8")) as LockFile;
256
+ }
257
+
258
+ function writeLock(previous: LockFile, next: LockFile) {
259
+ if (previous.upstream.commit === next.upstream.commit && previous.upstream.tag === next.upstream.tag) {
260
+ return;
261
+ }
262
+ writeFileSync(lockPath, `${JSON.stringify(next, null, 2)}\n`);
263
+ }
264
+
265
+ function collectStatus(lock: LockFile): StatusReport {
266
+ const cleanroomPath = path.resolve(repoRoot, lock.paths.cleanroom);
267
+ const vendorPath = path.resolve(repoRoot, lock.paths.vendor);
268
+ const patchesPath = path.resolve(repoRoot, lock.paths.patches);
269
+ const cleanroomExists = isGitRepository(cleanroomPath);
270
+ const cleanroomHead = cleanroomExists ? gitOutput(cleanroomPath, ["rev-parse", "HEAD"]).trim() : null;
271
+ const cleanroomPristine = cleanroomExists ? gitIsPristine(cleanroomPath) : null;
272
+ const cleanroomMatchesLock = cleanroomHead ? cleanroomHead === lock.upstream.commit : null;
273
+
274
+ const vendorExists = existsSync(vendorPath);
275
+ let vendorState: StatusReport["vendor"]["state"] = "missing";
276
+ let vendorHead: string | null = null;
277
+ let branchHead: string | null = null;
278
+ let patchesMatch: boolean | null = null;
279
+ if (vendorExists) {
280
+ if (!isGitRepository(vendorPath)) {
281
+ vendorState = "invalid";
282
+ } else {
283
+ vendorHead = gitOutput(vendorPath, ["rev-parse", "HEAD"]).trim();
284
+ branchHead = vendorHead;
285
+ const porcelain = gitOutput(vendorPath, ["status", "--porcelain"]).trim().split("\n").filter(Boolean);
286
+ if (porcelain.some((line) => line.startsWith("UU ") || line.includes(" -> "))) {
287
+ vendorState = "conflicted";
288
+ } else if (porcelain.length > 0) {
289
+ vendorState = "dirty";
290
+ } else {
291
+ vendorState = "clean";
292
+ }
293
+ if (vendorState !== "invalid") {
294
+ patchesMatch = branchMatchesPatches(lock, vendorPath);
295
+ }
296
+ }
297
+ }
298
+
299
+ return {
300
+ lock: {
301
+ tag: lock.upstream.tag,
302
+ commit: lock.upstream.commit,
303
+ packageVersion: lock.packageVersion,
304
+ },
305
+ cleanroom: {
306
+ path: lock.paths.cleanroom,
307
+ exists: cleanroomExists,
308
+ pristine: cleanroomPristine,
309
+ head: cleanroomHead,
310
+ matchesLock: cleanroomMatchesLock,
311
+ },
312
+ branch: {
313
+ name: lock.branch.name,
314
+ head: branchHead,
315
+ },
316
+ vendor: {
317
+ path: lock.paths.vendor,
318
+ exists: vendorExists,
319
+ state: vendorState,
320
+ head: vendorHead,
321
+ },
322
+ patches: {
323
+ path: lock.paths.patches,
324
+ count: patchFiles(patchesPath).length,
325
+ matchesBranch: patchesMatch,
326
+ },
327
+ };
328
+ }
329
+
330
+ function printStatus(report: StatusReport, asJson: boolean) {
331
+ if (asJson) {
332
+ console.log(JSON.stringify(report, null, 2));
333
+ return;
334
+ }
335
+ console.log(`lock tag: ${report.lock.tag}`);
336
+ console.log(`lock commit: ${report.lock.commit}`);
337
+ console.log(`package version: ${report.lock.packageVersion}`);
338
+ console.log(`cleanroom: ${report.cleanroom.path}`);
339
+ console.log(`cleanroom exists: ${report.cleanroom.exists ? "yes" : "no"}`);
340
+ console.log(`cleanroom pristine: ${stringifyStatus(report.cleanroom.pristine)}`);
341
+ console.log(`cleanroom head: ${report.cleanroom.head ?? "missing"}`);
342
+ console.log(`cleanroom matches lock: ${stringifyStatus(report.cleanroom.matchesLock)}`);
343
+ console.log(`patch branch: ${report.branch.name}`);
344
+ console.log(`patch branch head: ${report.branch.head ?? "missing"}`);
345
+ console.log(`vendor: ${report.vendor.path}`);
346
+ console.log(`vendor state: ${report.vendor.state}`);
347
+ console.log(`vendor head: ${report.vendor.head ?? "missing"}`);
348
+ console.log(`patches count: ${report.patches.count}`);
349
+ console.log(`patches match branch: ${stringifyStatus(report.patches.matchesBranch)}`);
350
+ }
351
+
352
+ function stringifyStatus(value: boolean | null) {
353
+ if (value === null) {
354
+ return "unknown";
355
+ }
356
+ return value ? "yes" : "no";
357
+ }
358
+
359
+ function runCheck(lock: LockFile) {
360
+ validateLock(lock);
361
+ const tempRoot = mkdtempSync(path.join(os.tmpdir(), "agent-mockingbird-opencode-check-"));
362
+ try {
363
+ const cleanroomPath = path.join(tempRoot, "cleanroom");
364
+ run(["git", "clone", lock.upstream.remote, cleanroomPath], repoRoot);
365
+ run(["git", "fetch", "--tags", "origin"], cleanroomPath);
366
+ run(["git", "checkout", "--detach", lock.upstream.commit], cleanroomPath);
367
+ const vendorPath = path.join(tempRoot, "vendor");
368
+ run(["git", "worktree", "add", "--detach", vendorPath, lock.upstream.commit], cleanroomPath);
369
+ run(["git", "checkout", "-B", lock.branch.name, lock.upstream.commit], vendorPath);
370
+ applyPatchSeries(lock, vendorPath);
371
+ runValidation(vendorPath, { includeRepoValidation: false });
372
+ verifyPatchReproducibility(lock, {
373
+ baseCommit: lock.upstream.commit,
374
+ compareDir: vendorPath,
375
+ useTemporaryClone: true,
376
+ });
377
+ } finally {
378
+ rmSync(tempRoot, { recursive: true, force: true });
379
+ }
380
+ console.log("OpenCode workflow check passed.");
381
+ }
382
+
383
+ function validateLock(lock: LockFile) {
384
+ if (!lock.upstream?.remote || !lock.upstream?.tag || !lock.upstream?.commit) {
385
+ throw new Error("opencode.lock.json is missing upstream metadata.");
386
+ }
387
+ if (!lock.packageVersion) {
388
+ throw new Error("opencode.lock.json is missing packageVersion.");
389
+ }
390
+ if (!lock.paths?.cleanroom || !lock.paths?.vendor || !lock.paths?.patches) {
391
+ throw new Error("opencode.lock.json is missing paths metadata.");
392
+ }
393
+ if (!lock.branch?.name) {
394
+ throw new Error("opencode.lock.json is missing branch metadata.");
395
+ }
396
+ }
397
+
398
+ function ensureCleanroomClone(lock: LockFile) {
399
+ validateLock(lock);
400
+ const cleanroomPath = path.resolve(repoRoot, lock.paths.cleanroom);
401
+ if (isGitRepository(cleanroomPath)) {
402
+ const remote = gitOutput(cleanroomPath, ["remote", "get-url", "origin"]).trim();
403
+ if (remote !== lock.upstream.remote) {
404
+ throw new Error(`Cleanroom origin mismatch: expected ${lock.upstream.remote}, got ${remote}`);
405
+ }
406
+ run(["git", "fetch", "--tags", "origin"], cleanroomPath);
407
+ return;
408
+ }
409
+ if (existsSync(cleanroomPath)) {
410
+ throw new Error(`Cleanroom path exists but is not a git clone: ${cleanroomPath}`);
411
+ }
412
+ mkdirSync(path.dirname(cleanroomPath), { recursive: true });
413
+ run(["git", "clone", lock.upstream.remote, cleanroomPath], repoRoot);
414
+ run(["git", "fetch", "--tags", "origin"], cleanroomPath);
415
+ }
416
+
417
+ function ensureCleanroomPristine(lock: LockFile) {
418
+ const cleanroomPath = path.resolve(repoRoot, lock.paths.cleanroom);
419
+ if (!gitIsPristine(cleanroomPath)) {
420
+ throw new Error(`Cleanroom is dirty: ${cleanroomPath}`);
421
+ }
422
+ }
423
+
424
+ function ensureCleanroomAtCommit(lock: LockFile, commit: string) {
425
+ const cleanroomPath = path.resolve(repoRoot, lock.paths.cleanroom);
426
+ ensureCleanroomPristine(lock);
427
+ run(["git", "checkout", "--detach", commit], cleanroomPath);
428
+ run(["git", "reset", "--hard", commit], cleanroomPath);
429
+ run(["git", "clean", "-fdx"], cleanroomPath);
430
+ }
431
+
432
+ function resolveUpstreamCommit(cleanroomPathInput: string, ref: string) {
433
+ const cleanroomPath = path.resolve(repoRoot, cleanroomPathInput);
434
+ run(["git", "fetch", "--tags", "origin"], cleanroomPath);
435
+ return gitOutput(cleanroomPath, ["rev-parse", `refs/tags/${ref}^{commit}`]).trim();
436
+ }
437
+
438
+ function ensureVendorSafeForRefChange(lock: LockFile) {
439
+ const vendorPath = path.resolve(repoRoot, lock.paths.vendor);
440
+ if (!existsSync(vendorPath)) {
441
+ return;
442
+ }
443
+ if (!isGitRepository(vendorPath)) {
444
+ throw new Error(`Vendor path exists but is not a git worktree: ${vendorPath}`);
445
+ }
446
+ const dirty = gitOutput(vendorPath, ["status", "--porcelain"]).trim();
447
+ if (dirty) {
448
+ throw new Error(`Vendor worktree is dirty. Commit or discard changes first: ${vendorPath}`);
449
+ }
450
+ if (!branchMatchesPatches(lock, vendorPath)) {
451
+ throw new Error("Vendor branch has changes that are not represented by patches/opencode.");
452
+ }
453
+ }
454
+
455
+ function recreateVendorWorktree(lock: LockFile, baseCommit: string, options?: { force?: boolean }) {
456
+ const cleanroomPath = path.resolve(repoRoot, lock.paths.cleanroom);
457
+ const vendorPath = path.resolve(repoRoot, lock.paths.vendor);
458
+ removeVendorWorktree(lock, options?.force ?? false);
459
+ run(["git", "worktree", "prune"], cleanroomPath);
460
+ mkdirSync(path.dirname(vendorPath), { recursive: true });
461
+ run(["git", "worktree", "add", "--detach", vendorPath, baseCommit], cleanroomPath);
462
+ run(["git", "checkout", "-B", lock.branch.name, baseCommit], vendorPath);
463
+ applyPatchSeries(lock, vendorPath);
464
+ }
465
+
466
+ function removeVendorWorktree(lock: LockFile, force: boolean) {
467
+ const cleanroomPath = path.resolve(repoRoot, lock.paths.cleanroom);
468
+ const vendorPath = path.resolve(repoRoot, lock.paths.vendor);
469
+ run(["git", "worktree", "prune"], cleanroomPath, { allowFailure: true });
470
+ if (!existsSync(vendorPath)) {
471
+ return;
472
+ }
473
+ if (isGitRepository(vendorPath)) {
474
+ const worktrees = gitOutput(cleanroomPath, ["worktree", "list", "--porcelain"]).split("\n");
475
+ const registered = worktrees.some((line) => line === `worktree ${vendorPath}`);
476
+ if (registered) {
477
+ run(["git", "worktree", "remove", force ? "--force" : vendorPath, force ? vendorPath : ""].filter(Boolean), cleanroomPath);
478
+ run(["git", "worktree", "prune"], cleanroomPath);
479
+ return;
480
+ }
481
+ }
482
+ if (!force && isPathDirty(vendorPath)) {
483
+ throw new Error(`Refusing to remove dirty vendor path: ${vendorPath}`);
484
+ }
485
+ rmSync(vendorPath, { recursive: true, force: true });
486
+ }
487
+
488
+ function isPathDirty(targetPath: string) {
489
+ if (!existsSync(targetPath)) {
490
+ return false;
491
+ }
492
+ const entries = readdirSync(targetPath);
493
+ return entries.length > 0;
494
+ }
495
+
496
+ function applyPatchSeries(lock: LockFile, vendorPath: string) {
497
+ const patches = patchFiles(path.resolve(repoRoot, lock.paths.patches));
498
+ if (patches.length === 0) {
499
+ return;
500
+ }
501
+ run(["git", "am", "--3way", ...patches], vendorPath, { env: gitAmEnv() });
502
+ }
503
+
504
+ function exportPatchesFromBranch(lock: LockFile, baseCommit?: string) {
505
+ ensureVendorBranchState(lock, { requirePatchesMatch: false });
506
+ const vendorPath = path.resolve(repoRoot, lock.paths.vendor);
507
+ const patchesPath = path.resolve(repoRoot, lock.paths.patches);
508
+ rmSync(patchesPath, { recursive: true, force: true });
509
+ mkdirSync(patchesPath, { recursive: true });
510
+ run(
511
+ [
512
+ "git",
513
+ "format-patch",
514
+ "--quiet",
515
+ "--output-directory",
516
+ patchesPath,
517
+ `${baseCommit ?? readLock().upstream.commit}..HEAD`,
518
+ ],
519
+ vendorPath,
520
+ );
521
+ }
522
+
523
+ function ensureVendorBranchState(lock: LockFile, options: { requirePatchesMatch: boolean }) {
524
+ const vendorPath = path.resolve(repoRoot, lock.paths.vendor);
525
+ if (!isGitRepository(vendorPath)) {
526
+ throw new Error(`Vendor worktree is missing: ${vendorPath}`);
527
+ }
528
+ const dirty = gitOutput(vendorPath, ["status", "--porcelain"]).trim();
529
+ if (dirty) {
530
+ throw new Error(`Vendor worktree is dirty: ${vendorPath}`);
531
+ }
532
+ const branch = gitOutput(vendorPath, ["rev-parse", "--abbrev-ref", "HEAD"]).trim();
533
+ if (branch !== lock.branch.name) {
534
+ throw new Error(`Vendor worktree branch mismatch: expected ${lock.branch.name}, got ${branch}`);
535
+ }
536
+ if (options.requirePatchesMatch && !branchMatchesPatches(lock, vendorPath)) {
537
+ throw new Error("Current branch state does not match patches/opencode.");
538
+ }
539
+ }
540
+
541
+ function verifyBranchMatchesPatches(lock: LockFile) {
542
+ ensureVendorBranchState(lock, { requirePatchesMatch: false });
543
+ const vendorPath = path.resolve(repoRoot, lock.paths.vendor);
544
+ if (!branchMatchesPatches(lock, vendorPath)) {
545
+ throw new Error("Exported patch series is not reproducible from the current branch.");
546
+ }
547
+ }
548
+
549
+ function branchMatchesPatches(lock: LockFile, vendorPath: string) {
550
+ const cleanroomPath = path.resolve(repoRoot, lock.paths.cleanroom);
551
+ if (!isGitRepository(cleanroomPath)) {
552
+ return false;
553
+ }
554
+
555
+ const tempDir = mkdtempSync(path.join(os.tmpdir(), "agent-mockingbird-opencode-branch-match-"));
556
+ try {
557
+ const compareWorktree = path.join(tempDir, "vendor");
558
+ run(["git", "worktree", "add", "--detach", compareWorktree, lock.upstream.commit], cleanroomPath);
559
+ const patches = patchFiles(path.resolve(repoRoot, lock.paths.patches));
560
+ if (patches.length > 0) {
561
+ run(["git", "am", "--3way", ...patches], compareWorktree, { env: gitAmEnv() });
562
+ }
563
+ const compareTree = gitOutput(compareWorktree, ["rev-parse", "HEAD^{tree}"]).trim();
564
+ const vendorTree = gitOutput(vendorPath, ["rev-parse", "HEAD^{tree}"]).trim();
565
+ return compareTree === vendorTree;
566
+ } finally {
567
+ const compareWorktree = path.join(tempDir, "vendor");
568
+ if (existsSync(compareWorktree)) {
569
+ run(["git", "worktree", "remove", "--force", compareWorktree], cleanroomPath, { allowFailure: true });
570
+ run(["git", "worktree", "prune"], cleanroomPath, { allowFailure: true });
571
+ }
572
+ rmSync(tempDir, { recursive: true, force: true });
573
+ }
574
+ }
575
+
576
+ function verifyPatchReproducibility(
577
+ lock: LockFile,
578
+ options: {
579
+ baseCommit: string;
580
+ compareDir: string;
581
+ useTemporaryClone: boolean;
582
+ cleanroomOverride?: string;
583
+ },
584
+ ) {
585
+ const tempRoot = mkdtempSync(path.join(os.tmpdir(), "agent-mockingbird-opencode-repro-"));
586
+ try {
587
+ const cleanroomPath = options.cleanroomOverride ?? path.join(tempRoot, "cleanroom");
588
+ if (!options.cleanroomOverride) {
589
+ run(["git", "clone", lock.upstream.remote, cleanroomPath], repoRoot);
590
+ run(["git", "fetch", "--tags", "origin"], cleanroomPath);
591
+ }
592
+ run(["git", "checkout", "--detach", options.baseCommit], cleanroomPath);
593
+ const compareWorktree = path.join(tempRoot, "vendor");
594
+ run(["git", "worktree", "add", "--detach", compareWorktree, options.baseCommit], cleanroomPath);
595
+ run(["git", "checkout", "-B", lock.branch.name, options.baseCommit], compareWorktree);
596
+ const patches = patchFiles(path.resolve(repoRoot, lock.paths.patches));
597
+ if (patches.length > 0) {
598
+ run(["git", "am", "--3way", ...patches], compareWorktree, { env: gitAmEnv() });
599
+ }
600
+ compareDirectories(compareWorktree, options.compareDir);
601
+ } finally {
602
+ rmSync(tempRoot, { recursive: true, force: true });
603
+ }
604
+ }
605
+
606
+ function compareDirectories(left: string, right: string) {
607
+ if (isGitRepository(left) && isGitRepository(right)) {
608
+ const leftDirty = gitOutput(left, ["status", "--porcelain", "--untracked-files=no"]).trim();
609
+ const rightDirty = gitOutput(right, ["status", "--porcelain", "--untracked-files=no"]).trim();
610
+ if (leftDirty || rightDirty) {
611
+ throw new Error("Tracked file changes detected while verifying patch reproducibility.");
612
+ }
613
+ const leftTree = gitOutput(left, ["rev-parse", "HEAD^{tree}"]).trim();
614
+ const rightTree = gitOutput(right, ["rev-parse", "HEAD^{tree}"]).trim();
615
+ if (leftTree !== rightTree) {
616
+ throw new Error(`Patch reproducibility tree mismatch: ${leftTree} != ${rightTree}`);
617
+ }
618
+ return;
619
+ }
620
+ run(["diff", "-qr", "--exclude", ".git", left, right], repoRoot);
621
+ }
622
+
623
+ function patchFiles(patchesPath: string) {
624
+ if (!existsSync(patchesPath)) {
625
+ return [];
626
+ }
627
+ return readdirSync(patchesPath)
628
+ .filter((entry) => entry.endsWith(".patch"))
629
+ .sort()
630
+ .map((entry) => path.join(patchesPath, entry));
631
+ }
632
+
633
+ function gitAmEnv() {
634
+ return {
635
+ ...process.env,
636
+ GIT_AUTHOR_NAME: process.env.GIT_AUTHOR_NAME ?? "Agent Mockingbird CI",
637
+ GIT_AUTHOR_EMAIL: process.env.GIT_AUTHOR_EMAIL ?? "agent-mockingbird-ci@example.invalid",
638
+ GIT_COMMITTER_NAME: process.env.GIT_COMMITTER_NAME ?? "Agent Mockingbird CI",
639
+ GIT_COMMITTER_EMAIL: process.env.GIT_COMMITTER_EMAIL ?? "agent-mockingbird-ci@example.invalid",
640
+ };
641
+ }
642
+
643
+ function bunInstallEnv() {
644
+ return {
645
+ ...process.env,
646
+ GITHUB_SERVER_URL: "https://github.com",
647
+ GITHUB_API_URL: "https://api.github.com",
648
+ };
649
+ }
650
+
651
+ function runValidation(vendorPath: string, options?: { includeRepoValidation?: boolean }) {
652
+ const includeRepoValidation =
653
+ options?.includeRepoValidation ?? path.resolve(vendorPath) === path.resolve(repoRoot, readLock().paths.vendor);
654
+ run(["bun", "install", "--cwd", vendorPath], repoRoot, { env: bunInstallEnv() });
655
+ run(["bun", "run", "typecheck"], path.join(vendorPath, "packages", "app"));
656
+ run(["bun", "run", "build"], path.join(vendorPath, "packages", "app"));
657
+ run(
658
+ ["bun", "test", "test/cli/plugin-auth-picker.test.ts", "test/plugin/module-exports.test.ts"],
659
+ path.join(vendorPath, "packages", "opencode"),
660
+ );
661
+ if (includeRepoValidation) {
662
+ run(["bun", "run", "build"], repoRoot);
663
+ run(["bun", "run", "typecheck"], repoRoot);
664
+ }
665
+ }
666
+
667
+ function normalizeTag(tag: string) {
668
+ return tag.startsWith("v") ? tag : `v${tag}`;
669
+ }
670
+
671
+ function stripLeadingV(tag: string) {
672
+ return tag.startsWith("v") ? tag.slice(1) : tag;
673
+ }
674
+
675
+ function isGitRepository(targetPath: string) {
676
+ if (!existsSync(targetPath)) {
677
+ return false;
678
+ }
679
+ const result = run(["git", "rev-parse", "--is-inside-work-tree"], targetPath, { allowFailure: true });
680
+ return result.status === 0;
681
+ }
682
+
683
+ function gitIsPristine(targetPath: string) {
684
+ return gitOutput(targetPath, ["status", "--porcelain"]).trim() === "";
685
+ }
686
+
687
+ function gitOutput(cwd: string, args: string[]) {
688
+ return run(["git", ...args], cwd).stdout;
689
+ }
690
+
691
+ function run(command: string[], cwd: string, options: ExecOptions = {}) {
692
+ const result = spawnSync(command[0], command.slice(1), {
693
+ cwd: options.cwd ?? cwd,
694
+ env: options.env ?? process.env,
695
+ encoding: "utf8",
696
+ });
697
+ if (result.status !== 0 && !options.allowFailure) {
698
+ const stderr = (result.stderr || "").trim();
699
+ const stdout = (result.stdout || "").trim();
700
+ throw new Error(
701
+ `Command failed: ${command.join(" ")}\n${stderr || stdout || `exit ${result.status ?? "unknown"}`}`,
702
+ );
703
+ }
704
+ return {
705
+ status: result.status ?? 0,
706
+ stdout: result.stdout || "",
707
+ stderr: result.stderr || "",
708
+ };
709
+ }
710
+
711
+ main().catch((error) => {
712
+ const message = error instanceof Error ? error.message : String(error);
713
+ console.error(message);
714
+ process.exit(1);
715
+ });