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,178 @@
1
+ From 9ad326e102738b40f41f423a58de68373bfdbbef Mon Sep 17 00:00:00 2001
2
+ From: Matt Campbell <matt@battleshopper.com>
3
+ Date: Thu, 19 Mar 2026 16:50:15 -0500
4
+ Subject: [PATCH 08/10] Remove cron contract section
5
+
6
+ ---
7
+ .../src/components/settings-cron-data.test.ts | 23 +---------------
8
+ .../app/src/components/settings-cron-data.ts | 21 +--------------
9
+ packages/app/src/components/settings-cron.tsx | 27 +------------------
10
+ 3 files changed, 3 insertions(+), 68 deletions(-)
11
+
12
+ diff --git a/packages/app/src/components/settings-cron-data.test.ts b/packages/app/src/components/settings-cron-data.test.ts
13
+ index c1ec8fcd3..2cc587ed7 100644
14
+ --- a/packages/app/src/components/settings-cron-data.test.ts
15
+ +++ b/packages/app/src/components/settings-cron-data.test.ts
16
+ @@ -2,7 +2,7 @@ import { describe, expect, test } from "bun:test"
17
+ import { DEFAULT_JOB, loadCronSettings, serializeCronJobForEdit, type CronJobDefinition } from "./settings-cron-data"
18
+
19
+ describe("loadCronSettings", () => {
20
+ - test("loads jobs, health, and contract without requesting handlers", async () => {
21
+ + test("loads jobs and health without requesting handlers or contract metadata", async () => {
22
+ const calls: Array<{ path: string; init?: RequestInit }> = []
23
+ const requestJson = async <T>(path: string, init?: RequestInit) => {
24
+ calls.push({ path, init })
25
+ @@ -12,15 +12,6 @@ describe("loadCronSettings", () => {
26
+ if (path === "/api/mockingbird/cron/health") {
27
+ return { health: { enabled: true, jobs: { total: 0, enabled: 0 } } } as T
28
+ }
29
+ - if (path === "/api/mockingbird/cron/manage") {
30
+ - return {
31
+ - contract: {
32
+ - runModes: {
33
+ - agent: { requires: ["agentPromptTemplate"], forbids: ["conditionModulePath"] },
34
+ - },
35
+ - },
36
+ - } as T
37
+ - }
38
+ throw new Error(`Unexpected path: ${path}`)
39
+ }
40
+
41
+ @@ -29,22 +20,10 @@ describe("loadCronSettings", () => {
42
+ expect(payload).toEqual({
43
+ jobs: [],
44
+ health: { enabled: true, jobs: { total: 0, enabled: 0 } },
45
+ - contract: {
46
+ - runModes: {
47
+ - agent: { requires: ["agentPromptTemplate"], forbids: ["conditionModulePath"] },
48
+ - },
49
+ - },
50
+ })
51
+ expect(calls).toEqual([
52
+ { path: "/api/mockingbird/cron/jobs", init: undefined },
53
+ { path: "/api/mockingbird/cron/health", init: undefined },
54
+ - {
55
+ - path: "/api/mockingbird/cron/manage",
56
+ - init: {
57
+ - method: "POST",
58
+ - body: JSON.stringify({ action: "describe_contract" }),
59
+ - },
60
+ - },
61
+ ])
62
+ })
63
+ })
64
+ diff --git a/packages/app/src/components/settings-cron-data.ts b/packages/app/src/components/settings-cron-data.ts
65
+ index 2eeab5f94..8c044168f 100644
66
+ --- a/packages/app/src/components/settings-cron-data.ts
67
+ +++ b/packages/app/src/components/settings-cron-data.ts
68
+ @@ -27,16 +27,6 @@ export type CronHealthSnapshot = {
69
+ }
70
+ }
71
+
72
+ -type CronContractMode = {
73
+ - requires?: string[]
74
+ - optional?: string[]
75
+ - forbids?: string[]
76
+ -}
77
+ -
78
+ -export type CronContract = {
79
+ - runModes: Record<string, CronContractMode>
80
+ -}
81
+ -
82
+ type CronJobsResponse = {
83
+ jobs: CronJobDefinition[]
84
+ }
85
+ @@ -45,10 +35,6 @@ type CronHealthResponse = {
86
+ health: CronHealthSnapshot
87
+ }
88
+
89
+ -type CronContractResponse = {
90
+ - contract: CronContract
91
+ -}
92
+ -
93
+ type MockingbirdJson = <T>(path: string, init?: RequestInit) => Promise<T>
94
+
95
+ const DEFAULT_AGENT_JOB = {
96
+ @@ -64,19 +50,14 @@ const DEFAULT_AGENT_JOB = {
97
+ export const DEFAULT_JOB = prettyJson(DEFAULT_AGENT_JOB)
98
+
99
+ export async function loadCronSettings(requestJson: MockingbirdJson = mockingbirdJson) {
100
+ - const [jobs, health, contract] = await Promise.all([
101
+ + const [jobs, health] = await Promise.all([
102
+ requestJson<CronJobsResponse>("/api/mockingbird/cron/jobs"),
103
+ requestJson<CronHealthResponse>("/api/mockingbird/cron/health"),
104
+ - requestJson<CronContractResponse>("/api/mockingbird/cron/manage", {
105
+ - method: "POST",
106
+ - body: JSON.stringify({ action: "describe_contract" }),
107
+ - }),
108
+ ])
109
+
110
+ return {
111
+ jobs: jobs.jobs,
112
+ health: health.health,
113
+ - contract: contract.contract,
114
+ }
115
+ }
116
+
117
+ diff --git a/packages/app/src/components/settings-cron.tsx b/packages/app/src/components/settings-cron.tsx
118
+ index 67f282245..f56fef497 100644
119
+ --- a/packages/app/src/components/settings-cron.tsx
120
+ +++ b/packages/app/src/components/settings-cron.tsx
121
+ @@ -3,7 +3,7 @@ import { showToast } from "@opencode-ai/ui/toast"
122
+ import { For, onMount, Show, type Component } from "solid-js"
123
+ import { createStore } from "solid-js/store"
124
+ import { mockingbirdJson } from "@/utils/mockingbird"
125
+ -import { DEFAULT_JOB, loadCronSettings, serializeCronJobForEdit, type CronContract, type CronHealthSnapshot, type CronJobDefinition } from "./settings-cron-data"
126
+ +import { DEFAULT_JOB, loadCronSettings, serializeCronJobForEdit, type CronHealthSnapshot, type CronJobDefinition } from "./settings-cron-data"
127
+ import { MockingbirdCard, MockingbirdMetaRow, MockingbirdNotice, MockingbirdSettingsPage, MockingbirdSettingsSection, MockingbirdTextArea, MockingbirdToolbar } from "./settings-mockingbird-shared"
128
+
129
+ export const SettingsCron: Component = () => {
130
+ @@ -14,7 +14,6 @@ export const SettingsCron: Component = () => {
131
+ error: "",
132
+ jobs: [] as CronJobDefinition[],
133
+ health: null as CronHealthSnapshot | null,
134
+ - contract: null as CronContract | null,
135
+ createText: DEFAULT_JOB,
136
+ editingId: "",
137
+ editText: "",
138
+ @@ -27,7 +26,6 @@ export const SettingsCron: Component = () => {
139
+ const payload = await loadCronSettings()
140
+ setState("jobs", payload.jobs)
141
+ setState("health", payload.health)
142
+ - setState("contract", payload.contract)
143
+ } catch (error) {
144
+ setState("error", error instanceof Error ? error.message : "Failed to load cron jobs")
145
+ } finally {
146
+ @@ -158,29 +156,6 @@ export const SettingsCron: Component = () => {
147
+ )}
148
+ </Show>
149
+
150
+ - <Show when={state.contract}>
151
+ - {(contract) => (
152
+ - <MockingbirdSettingsSection title="Run mode contract" description="Current backend requirements for each cron run mode.">
153
+ - <For each={Object.entries(contract().runModes)}>
154
+ - {([mode, details]) => (
155
+ - <div class="px-4 py-4 border-b border-border-weak-base last:border-none flex flex-col gap-1">
156
+ - <div class="text-14-medium text-text-strong">{mode}</div>
157
+ - <div class="text-12-regular text-text-weak">
158
+ - Requires: {details.requires?.length ? details.requires.join(", ") : "Nothing special"}
159
+ - </div>
160
+ - <Show when={details.optional?.length}>
161
+ - <div class="text-12-regular text-text-weak">Optional: {details.optional?.join(", ")}</div>
162
+ - </Show>
163
+ - <Show when={details.forbids?.length}>
164
+ - <div class="text-12-regular text-text-weak">Forbids: {details.forbids?.join(", ")}</div>
165
+ - </Show>
166
+ - </div>
167
+ - )}
168
+ - </For>
169
+ - </MockingbirdSettingsSection>
170
+ - )}
171
+ - </Show>
172
+ -
173
+ <MockingbirdSettingsSection title="Jobs">
174
+ <Show
175
+ when={state.jobs.length > 0}
176
+ --
177
+ 2.53.0
178
+
@@ -0,0 +1,414 @@
1
+ From 42e13ff48c3dfa2ff51f90fef6bdf808b1e28f17 Mon Sep 17 00:00:00 2001
2
+ From: Matt Campbell <matt@battleshopper.com>
3
+ Date: Thu, 19 Mar 2026 17:40:25 -0500
4
+ Subject: [PATCH 09/10] Rework cron tab as operations console
5
+
6
+ ---
7
+ .../src/components/settings-cron-data.test.ts | 102 +++++++++++-----
8
+ .../app/src/components/settings-cron-data.ts | 65 +++++-----
9
+ packages/app/src/components/settings-cron.tsx | 112 ++++--------------
10
+ 3 files changed, 131 insertions(+), 148 deletions(-)
11
+
12
+ diff --git a/packages/app/src/components/settings-cron-data.test.ts b/packages/app/src/components/settings-cron-data.test.ts
13
+ index 2cc587ed7..a81509423 100644
14
+ --- a/packages/app/src/components/settings-cron-data.test.ts
15
+ +++ b/packages/app/src/components/settings-cron-data.test.ts
16
+ @@ -1,5 +1,5 @@
17
+ import { describe, expect, test } from "bun:test"
18
+ -import { DEFAULT_JOB, loadCronSettings, serializeCronJobForEdit, type CronJobDefinition } from "./settings-cron-data"
19
+ +import { describeCronJobTarget, formatCronSchedule, loadCronSettings, type CronJobDefinition } from "./settings-cron-data"
20
+
21
+ describe("loadCronSettings", () => {
22
+ test("loads jobs and health without requesting handlers or contract metadata", async () => {
23
+ @@ -28,48 +28,96 @@ describe("loadCronSettings", () => {
24
+ })
25
+ })
26
+
27
+ -describe("Cron settings payload helpers", () => {
28
+ - test("default job example uses the current agent contract", () => {
29
+ - const parsed = JSON.parse(DEFAULT_JOB)
30
+ - expect(parsed).toMatchObject({
31
+ - name: "Hourly review",
32
+ +describe("Cron settings display helpers", () => {
33
+ + test("formats every schedules as human intervals", () => {
34
+ + const job = {
35
+ + id: "cron-1",
36
+ + name: "Location tracker",
37
+ + enabled: true,
38
+ scheduleKind: "every",
39
+ - everyMs: 3_600_000,
40
+ - runMode: "agent",
41
+ - agentPromptTemplate: expect.any(String),
42
+ + scheduleExpr: null,
43
+ + everyMs: 120_000,
44
+ + atIso: null,
45
+ + timezone: null,
46
+ + runMode: "conditional_agent",
47
+ + conditionModulePath: "cron/location-tracker.ts",
48
+ + conditionDescription: null,
49
+ + agentPromptTemplate: null,
50
+ + agentModelOverride: null,
51
+ + maxAttempts: 3,
52
+ + retryBackoffMs: 1_000,
53
+ payload: {},
54
+ - })
55
+ - expect(parsed.handlerKey).toBeUndefined()
56
+ + } satisfies CronJobDefinition
57
+ +
58
+ + expect(formatCronSchedule(job)).toBe("Every 2m")
59
+ })
60
+
61
+ - test("edit payload excludes deprecated handlerKey", () => {
62
+ + test("formats cron schedules with timezone", () => {
63
+ const job = {
64
+ - id: "cron-1",
65
+ - name: "Daily review",
66
+ + id: "cron-2",
67
+ + name: "Release check",
68
+ enabled: true,
69
+ scheduleKind: "cron",
70
+ - scheduleExpr: "0 9 * * *",
71
+ + scheduleExpr: "0 16 * * *",
72
+ everyMs: null,
73
+ atIso: null,
74
+ timezone: "America/Chicago",
75
+ - runMode: "background",
76
+ - conditionModulePath: "cron/check.ts",
77
+ - conditionDescription: "Checks for changes",
78
+ + runMode: "conditional_agent",
79
+ + conditionModulePath: "cron/release-check.ts",
80
+ + conditionDescription: null,
81
+ agentPromptTemplate: null,
82
+ agentModelOverride: null,
83
+ maxAttempts: 3,
84
+ retryBackoffMs: 1_000,
85
+ - payload: { channel: "main" },
86
+ + payload: {},
87
+ } satisfies CronJobDefinition
88
+
89
+ - const parsed = JSON.parse(serializeCronJobForEdit(job)) as Record<string, unknown>
90
+ + expect(formatCronSchedule(job)).toBe("0 16 * * * (America/Chicago)")
91
+ + })
92
+
93
+ - expect(parsed).toMatchObject({
94
+ - name: "Daily review",
95
+ - runMode: "background",
96
+ - conditionModulePath: "cron/check.ts",
97
+ - payload: { channel: "main" },
98
+ - })
99
+ - expect(parsed.handlerKey).toBeUndefined()
100
+ + test("describes code-backed cron targets", () => {
101
+ + const job = {
102
+ + id: "cron-3",
103
+ + name: "WiFi monitor",
104
+ + enabled: true,
105
+ + scheduleKind: "every",
106
+ + scheduleExpr: null,
107
+ + everyMs: 120_000,
108
+ + atIso: null,
109
+ + timezone: null,
110
+ + runMode: "conditional_agent",
111
+ + conditionModulePath: "cron/wifi-device-monitor.ts",
112
+ + conditionDescription: "Checks for nearby devices",
113
+ + agentPromptTemplate: null,
114
+ + agentModelOverride: null,
115
+ + maxAttempts: 3,
116
+ + retryBackoffMs: 1_000,
117
+ + payload: {},
118
+ + } satisfies CronJobDefinition
119
+ +
120
+ + expect(describeCronJobTarget(job)).toBe("cron/wifi-device-monitor.ts")
121
+ + })
122
+ +
123
+ + test("labels prompt-backed agent jobs without exposing editable prompt UI", () => {
124
+ + const job = {
125
+ + id: "cron-4",
126
+ + name: "Summary",
127
+ + enabled: true,
128
+ + scheduleKind: "cron",
129
+ + scheduleExpr: "0 9 * * *",
130
+ + everyMs: null,
131
+ + atIso: null,
132
+ + timezone: "America/Chicago",
133
+ + runMode: "agent",
134
+ + conditionModulePath: null,
135
+ + conditionDescription: null,
136
+ + agentPromptTemplate: "Summarize",
137
+ + agentModelOverride: null,
138
+ + maxAttempts: 3,
139
+ + retryBackoffMs: 1_000,
140
+ + payload: {},
141
+ + } satisfies CronJobDefinition
142
+ +
143
+ + expect(describeCronJobTarget(job)).toBe("Prompt-backed agent job")
144
+ })
145
+ })
146
+ diff --git a/packages/app/src/components/settings-cron-data.ts b/packages/app/src/components/settings-cron-data.ts
147
+ index 8c044168f..992820096 100644
148
+ --- a/packages/app/src/components/settings-cron-data.ts
149
+ +++ b/packages/app/src/components/settings-cron-data.ts
150
+ @@ -1,4 +1,4 @@
151
+ -import { prettyJson, mockingbirdJson } from "@/utils/mockingbird"
152
+ +import { mockingbirdJson } from "@/utils/mockingbird"
153
+
154
+ export type CronJobDefinition = {
155
+ id: string
156
+ @@ -37,18 +37,6 @@ type CronHealthResponse = {
157
+
158
+ type MockingbirdJson = <T>(path: string, init?: RequestInit) => Promise<T>
159
+
160
+ -const DEFAULT_AGENT_JOB = {
161
+ - name: "Hourly review",
162
+ - enabled: true,
163
+ - scheduleKind: "every",
164
+ - everyMs: 3_600_000,
165
+ - runMode: "agent",
166
+ - agentPromptTemplate: "Review recent workspace activity and summarize anything that needs attention.",
167
+ - payload: {},
168
+ -} as const
169
+ -
170
+ -export const DEFAULT_JOB = prettyJson(DEFAULT_AGENT_JOB)
171
+ -
172
+ export async function loadCronSettings(requestJson: MockingbirdJson = mockingbirdJson) {
173
+ const [jobs, health] = await Promise.all([
174
+ requestJson<CronJobsResponse>("/api/mockingbird/cron/jobs"),
175
+ @@ -61,22 +49,37 @@ export async function loadCronSettings(requestJson: MockingbirdJson = mockingbir
176
+ }
177
+ }
178
+
179
+ -export function serializeCronJobForEdit(job: CronJobDefinition) {
180
+ - return prettyJson({
181
+ - name: job.name,
182
+ - enabled: job.enabled,
183
+ - scheduleKind: job.scheduleKind,
184
+ - scheduleExpr: job.scheduleExpr,
185
+ - everyMs: job.everyMs,
186
+ - atIso: job.atIso,
187
+ - timezone: job.timezone,
188
+ - runMode: job.runMode,
189
+ - conditionModulePath: job.conditionModulePath,
190
+ - conditionDescription: job.conditionDescription,
191
+ - agentPromptTemplate: job.agentPromptTemplate,
192
+ - agentModelOverride: job.agentModelOverride,
193
+ - maxAttempts: job.maxAttempts,
194
+ - retryBackoffMs: job.retryBackoffMs,
195
+ - payload: job.payload,
196
+ - })
197
+ +function formatEveryInterval(everyMs: number | null) {
198
+ + if (!everyMs || !Number.isFinite(everyMs) || everyMs < 1_000) return "Every custom interval"
199
+ + if (everyMs % 86_400_000 === 0) return `Every ${everyMs / 86_400_000}d`
200
+ + if (everyMs % 3_600_000 === 0) return `Every ${everyMs / 3_600_000}h`
201
+ + if (everyMs % 60_000 === 0) return `Every ${everyMs / 60_000}m`
202
+ + if (everyMs % 1_000 === 0) return `Every ${everyMs / 1_000}s`
203
+ + return `Every ${everyMs}ms`
204
+ +}
205
+ +
206
+ +export function formatCronSchedule(job: CronJobDefinition) {
207
+ + if (job.scheduleKind === "every") {
208
+ + return formatEveryInterval(job.everyMs)
209
+ + }
210
+ +
211
+ + if (job.scheduleKind === "cron") {
212
+ + const expr = job.scheduleExpr?.trim() || "Missing cron expression"
213
+ + return job.timezone ? `${expr} (${job.timezone})` : expr
214
+ + }
215
+ +
216
+ + const atIso = job.atIso?.trim()
217
+ + return atIso ? `At ${atIso}` : "No schedule"
218
+ +}
219
+ +
220
+ +export function describeCronJobTarget(job: CronJobDefinition) {
221
+ + if (job.conditionModulePath?.trim()) {
222
+ + return job.conditionModulePath
223
+ + }
224
+ +
225
+ + if (job.runMode === "agent") {
226
+ + return "Prompt-backed agent job"
227
+ + }
228
+ +
229
+ + return "No code target"
230
+ }
231
+ diff --git a/packages/app/src/components/settings-cron.tsx b/packages/app/src/components/settings-cron.tsx
232
+ index f56fef497..24395d8b5 100644
233
+ --- a/packages/app/src/components/settings-cron.tsx
234
+ +++ b/packages/app/src/components/settings-cron.tsx
235
+ @@ -1,22 +1,19 @@
236
+ import { Button } from "@opencode-ai/ui/button"
237
+ +import { Switch } from "@opencode-ai/ui/switch"
238
+ import { showToast } from "@opencode-ai/ui/toast"
239
+ import { For, onMount, Show, type Component } from "solid-js"
240
+ import { createStore } from "solid-js/store"
241
+ import { mockingbirdJson } from "@/utils/mockingbird"
242
+ -import { DEFAULT_JOB, loadCronSettings, serializeCronJobForEdit, type CronHealthSnapshot, type CronJobDefinition } from "./settings-cron-data"
243
+ -import { MockingbirdCard, MockingbirdMetaRow, MockingbirdNotice, MockingbirdSettingsPage, MockingbirdSettingsSection, MockingbirdTextArea, MockingbirdToolbar } from "./settings-mockingbird-shared"
244
+ +import { describeCronJobTarget, formatCronSchedule, loadCronSettings, type CronHealthSnapshot, type CronJobDefinition } from "./settings-cron-data"
245
+ +import { MockingbirdMetaRow, MockingbirdNotice, MockingbirdSettingsPage, MockingbirdSettingsSection, MockingbirdToolbar } from "./settings-mockingbird-shared"
246
+
247
+ export const SettingsCron: Component = () => {
248
+ const [state, setState] = createStore({
249
+ loading: true,
250
+ - saving: false,
251
+ busyId: "",
252
+ error: "",
253
+ jobs: [] as CronJobDefinition[],
254
+ health: null as CronHealthSnapshot | null,
255
+ - createText: DEFAULT_JOB,
256
+ - editingId: "",
257
+ - editText: "",
258
+ })
259
+
260
+ async function load() {
261
+ @@ -37,55 +34,19 @@ export const SettingsCron: Component = () => {
262
+ await load()
263
+ }
264
+
265
+ - async function createJob() {
266
+ - let payload: unknown
267
+ - try {
268
+ - payload = JSON.parse(state.createText)
269
+ - } catch (error) {
270
+ - setState("error", error instanceof Error ? error.message : "Invalid cron job JSON")
271
+ - return
272
+ - }
273
+ -
274
+ - setState("saving", true)
275
+ + async function setEnabled(id: string, enabled: boolean) {
276
+ + setState("busyId", id)
277
+ setState("error", "")
278
+ try {
279
+ - await mockingbirdJson("/api/mockingbird/cron/jobs", {
280
+ - method: "POST",
281
+ - body: JSON.stringify(payload),
282
+ + await mockingbirdJson(`/api/mockingbird/cron/jobs/${encodeURIComponent(id)}`, {
283
+ + method: "PATCH",
284
+ + body: JSON.stringify({ enabled }),
285
+ })
286
+ showToast({
287
+ variant: "success",
288
+ icon: "circle-check",
289
+ - title: "Cron job created",
290
+ - })
291
+ - await refresh()
292
+ - } catch (error) {
293
+ - setState("error", error instanceof Error ? error.message : "Failed to create cron job")
294
+ - } finally {
295
+ - setState("saving", false)
296
+ - }
297
+ - }
298
+ -
299
+ - async function saveEdit() {
300
+ - if (!state.editingId) return
301
+ -
302
+ - let payload: unknown
303
+ - try {
304
+ - payload = JSON.parse(state.editText)
305
+ - } catch (error) {
306
+ - setState("error", error instanceof Error ? error.message : "Invalid cron patch JSON")
307
+ - return
308
+ - }
309
+ -
310
+ - setState("busyId", state.editingId)
311
+ - setState("error", "")
312
+ - try {
313
+ - await mockingbirdJson(`/api/mockingbird/cron/jobs/${encodeURIComponent(state.editingId)}`, {
314
+ - method: "PATCH",
315
+ - body: JSON.stringify(payload),
316
+ + title: enabled ? "Cron job enabled" : "Cron job disabled",
317
+ })
318
+ - setState("editingId", "")
319
+ - setState("editText", "")
320
+ await refresh()
321
+ } catch (error) {
322
+ setState("error", error instanceof Error ? error.message : "Failed to update cron job")
323
+ @@ -133,10 +94,10 @@ export const SettingsCron: Component = () => {
324
+ return (
325
+ <MockingbirdSettingsPage
326
+ title="Cron"
327
+ - description="Inspect cron health, create jobs, update jobs with JSON patches, and run them immediately."
328
+ + description="Inspect cron health, enable or disable jobs, run them immediately, and remove them when needed."
329
+ actions={
330
+ <MockingbirdToolbar>
331
+ - <Button variant="ghost" size="large" onClick={() => void refresh()} disabled={state.loading || state.saving}>
332
+ + <Button variant="ghost" size="large" onClick={() => void refresh()} disabled={state.loading}>
333
+ Refresh
334
+ </Button>
335
+ </MockingbirdToolbar>
336
+ @@ -168,23 +129,23 @@ export const SettingsCron: Component = () => {
337
+ <div class="flex flex-col gap-1">
338
+ <div class="text-14-medium text-text-strong">{job.name}</div>
339
+ <div class="text-12-regular text-text-weak">
340
+ - {job.id} | {job.scheduleKind} | {job.runMode}
341
+ + {job.id} | {job.runMode} | {job.enabled ? "Enabled" : "Disabled"}
342
+ </div>
343
+ <div class="text-12-regular text-text-weak">
344
+ - {job.scheduleExpr ?? job.everyMs ?? job.atIso ?? "No schedule"}
345
+ + {formatCronSchedule(job)}
346
+ + </div>
347
+ + <div class="text-12-regular text-text-weak">
348
+ + {describeCronJobTarget(job)}
349
+ </div>
350
+ </div>
351
+ <div class="flex flex-wrap items-center gap-2">
352
+ - <Button
353
+ - variant="ghost"
354
+ - size="large"
355
+ - onClick={() => {
356
+ - setState("editingId", state.editingId === job.id ? "" : job.id)
357
+ - setState("editText", state.editingId === job.id ? "" : serializeCronJobForEdit(job))
358
+ - }}
359
+ + <Switch
360
+ + checked={job.enabled}
361
+ + onChange={(enabled) => void setEnabled(job.id, enabled)}
362
+ + disabled={state.busyId === job.id}
363
+ >
364
+ - {state.editingId === job.id ? "Close editor" : "Edit JSON"}
365
+ - </Button>
366
+ + Enabled
367
+ + </Switch>
368
+ <Button
369
+ variant="ghost"
370
+ size="large"
371
+ @@ -203,40 +164,11 @@ export const SettingsCron: Component = () => {
372
+ </Button>
373
+ </div>
374
+ </div>
375
+ -
376
+ - <Show when={state.editingId === job.id}>
377
+ - <div class="flex flex-col gap-3">
378
+ - <MockingbirdTextArea
379
+ - rows={12}
380
+ - value={state.editText}
381
+ - onInput={(event) => setState("editText", event.currentTarget.value)}
382
+ - />
383
+ - <div class="flex justify-end">
384
+ - <Button size="large" onClick={() => void saveEdit()} disabled={state.busyId === job.id}>
385
+ - Save patch
386
+ - </Button>
387
+ - </div>
388
+ - </div>
389
+ - </Show>
390
+ </div>
391
+ )}
392
+ </For>
393
+ </Show>
394
+ </MockingbirdSettingsSection>
395
+ -
396
+ - <MockingbirdSettingsSection title="Create job" description="Post a complete cron job definition as JSON using the current run mode contract.">
397
+ - <MockingbirdCard
398
+ - footer={
399
+ - <div class="flex justify-end">
400
+ - <Button size="large" onClick={() => void createJob()} disabled={state.saving}>
401
+ - Create job
402
+ - </Button>
403
+ - </div>
404
+ - }
405
+ - >
406
+ - <MockingbirdTextArea rows={16} value={state.createText} onInput={(event) => setState("createText", event.currentTarget.value)} />
407
+ - </MockingbirdCard>
408
+ - </MockingbirdSettingsSection>
409
+ </MockingbirdSettingsPage>
410
+ )
411
+ }
412
+ --
413
+ 2.53.0
414
+