opencode-overclock 0.3.0 → 0.5.0

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 (82) hide show
  1. package/README.md +252 -111
  2. package/package.json +6 -4
  3. package/skills/codebase-design/DEEPENING.md +35 -0
  4. package/skills/codebase-design/DESIGN-IT-TWICE.md +34 -0
  5. package/skills/codebase-design/SKILL.md +93 -0
  6. package/skills/diagnosing-bugs/SKILL.md +123 -0
  7. package/skills/domain-modeling/ADR-FORMAT.md +55 -0
  8. package/skills/domain-modeling/CONTEXT-FORMAT.md +32 -0
  9. package/skills/domain-modeling/SKILL.md +102 -0
  10. package/skills/doubt/SKILL.md +80 -0
  11. package/skills/grilling/SKILL.md +96 -0
  12. package/skills/source-discipline/SKILL.md +78 -0
  13. package/skills/tdd/SKILL.md +87 -0
  14. package/skills/to-spec/SKILL.md +69 -0
  15. package/skills/to-spec/SPEC-TEMPLATE.md +50 -0
  16. package/skills/to-tickets/SKILL.md +74 -0
  17. package/skills/to-tickets/TICKET-TEMPLATE.md +41 -0
  18. package/src/bridge.ts +1 -0
  19. package/src/buddy/companion.ts +104 -5
  20. package/src/buddy/sprites.ts +4 -4
  21. package/src/buddy/tui.ts +175 -65
  22. package/src/core/bridge.ts +34 -0
  23. package/src/core/lifecycle.ts +67 -0
  24. package/src/core/policy.ts +128 -0
  25. package/src/core/summary.ts +33 -0
  26. package/src/core/types.ts +193 -0
  27. package/src/features/buddy.ts +1 -2
  28. package/src/features/guard.ts +421 -37
  29. package/src/features/index.ts +18 -4
  30. package/src/features/recovery.ts +153 -0
  31. package/src/features/safety.ts +147 -0
  32. package/src/features/sched.ts +183 -89
  33. package/src/features/tasks.ts +134 -33
  34. package/src/features/truncator.ts +116 -0
  35. package/src/features/usage.ts +46 -65
  36. package/src/features/workflow.ts +256 -0
  37. package/src/index.ts +96 -67
  38. package/src/lib/busy.ts +1 -25
  39. package/src/lib/exec.ts +13 -0
  40. package/src/lib/inject.ts +10 -56
  41. package/src/lib/mirror.ts +13 -0
  42. package/src/lib/probe.ts +1 -15
  43. package/src/lib/state.ts +10 -39
  44. package/src/lib/tmux.ts +1 -0
  45. package/src/lib/ui.ts +208 -0
  46. package/src/merge.ts +2 -66
  47. package/src/platform/probe.ts +25 -0
  48. package/src/platform/process/exec.ts +317 -0
  49. package/src/platform/process/tmux.ts +60 -0
  50. package/src/platform/session/busy.ts +33 -0
  51. package/src/platform/session/inject.ts +89 -0
  52. package/src/platform/session/notify.ts +20 -0
  53. package/src/platform/storage/state.ts +99 -0
  54. package/src/platform/storage/store.ts +61 -0
  55. package/src/summary.ts +1 -0
  56. package/src/tools.ts +8 -244
  57. package/src/tui.ts +57 -186
  58. package/src/types.ts +1 -73
  59. package/src/v2/context.ts +470 -0
  60. package/src/v2/host.ts +120 -0
  61. package/src/v2/loader.ts +150 -0
  62. package/src/workflow/agents/codebase-researcher.ts +27 -0
  63. package/src/workflow/agents/design-explorer.ts +33 -0
  64. package/src/workflow/agents/doubt-reviewer.ts +26 -0
  65. package/src/workflow/agents/engineering-coach.ts +23 -0
  66. package/src/workflow/agents/performance-auditor.ts +29 -0
  67. package/src/workflow/agents/security-auditor.ts +23 -0
  68. package/src/workflow/agents/spec-reviewer.ts +15 -0
  69. package/src/workflow/agents/standards-reviewer.ts +24 -0
  70. package/src/workflow/agents/test-engineer.ts +28 -0
  71. package/src/workflow/catalog.ts +210 -0
  72. package/src/workflow/templates/build.ts +47 -0
  73. package/src/workflow/templates/define.ts +45 -0
  74. package/src/workflow/templates/diagnose.ts +58 -0
  75. package/src/workflow/templates/plan.ts +52 -0
  76. package/src/workflow/templates/ship.ts +64 -0
  77. package/src/buddy/reactions.ts +0 -41
  78. package/src/buddy/types.ts +0 -30
  79. package/src/config.ts +0 -19
  80. package/src/features/checkpoints.ts +0 -128
  81. package/src/features/sandbox.ts +0 -104
  82. package/src/validate.ts +0 -197
package/src/tui.ts CHANGED
@@ -1,7 +1,14 @@
1
1
  import type { TuiPlugin, TuiPluginModule } from "@opencode-ai/plugin/tui"
2
2
  import { registerBuddy } from "./buddy/tui.ts"
3
-
4
- const STATE_SUBDIR = ".opencode/overclock"
3
+ import { createUi } from "./lib/ui.ts"
4
+ import {
5
+ scheduleStore,
6
+ taskStore,
7
+ usageStore,
8
+ type ScheduleEntryView,
9
+ type TaskMirrorEntry,
10
+ type UsageStateView,
11
+ } from "./lib/mirror.ts"
5
12
 
6
13
  export interface TuiOptions {
7
14
  notifyIdle?: boolean
@@ -11,44 +18,16 @@ export interface TuiOptions {
11
18
  buddy?: boolean
12
19
  }
13
20
 
14
- /**
15
- * The buddy toggles from the same overclock.json as the server features, so one
16
- * config file governs both surfaces. `features.buddy: false` disables; missing
17
- * file or unreadable config = default on.
18
- */
19
- export async function buddyEnabledInConfig(directory: string): Promise<boolean> {
20
- try {
21
- const file = Bun.file(`${directory}/.opencode/overclock.json`)
22
- if (!(await file.exists())) return true
23
- const config = (await file.json()) as { features?: Record<string, unknown> }
24
- return config.features?.["buddy"] !== false
25
- } catch {
26
- return true
27
- }
28
- }
29
-
30
- export interface TaskMirrorEntry {
31
- id: string
32
- description: string
33
- status: "running" | "exited" | "killed"
34
- exitCode: number | null
35
- startedAt: number
36
- }
37
-
38
- export interface UsageDayBucket {
39
- cost: number
40
- tokens: { input: number; output: number; reasoning: number; cacheRead: number; cacheWrite: number }
41
- messages: number
42
- }
43
-
44
- export interface UsageStateShape {
45
- days: Record<string, UsageDayBucket>
46
- }
21
+ /** Read views the summary formatters accept. Re-exported for tests and downstream typing. */
22
+ export type { TaskMirrorEntry, ScheduleEntryView, UsageStateView }
47
23
 
48
- export interface ScheduleMirrorEntry {
49
- id: string
50
- spec: string
51
- target: string
24
+ /** Check if buddy feature is enabled via plugin options. */
25
+ export function isBuddyEnabled(options?: TuiOptions): boolean {
26
+ if (!options) return true
27
+ if (options.buddy === false) return false
28
+ const features = (options as { features?: Record<string, unknown> }).features
29
+ if (features?.buddy === false) return false
30
+ return true
52
31
  }
53
32
 
54
33
  /** Local YYYY-MM-DD from an epoch-ms timestamp (mirrors usage.ts, kept local to stay decoupled). */
@@ -75,7 +54,7 @@ export function formatTasksSummary(entries: TaskMirrorEntry[] | undefined | null
75
54
 
76
55
  /** Pure: format the /oc-usage toast summary from a parsed usage.json, for a given "now". */
77
56
  export function formatUsageSummary(
78
- state: UsageStateShape | undefined | null,
57
+ state: UsageStateView | undefined | null,
79
58
  now: number = Date.now(),
80
59
  ): string {
81
60
  if (!state) return "no data yet"
@@ -86,169 +65,61 @@ export function formatUsageSummary(
86
65
  }
87
66
 
88
67
  /** Pure: format the /oc-schedules toast summary from a parsed schedules.json. */
89
- export function formatSchedulesSummary(schedules: ScheduleMirrorEntry[] | undefined | null): string {
68
+ export function formatSchedulesSummary(schedules: ScheduleEntryView[] | undefined | null): string {
90
69
  if (!schedules) return "no data yet"
91
70
  if (!schedules.length) return "no schedules"
92
71
  const list = schedules.map((s) => `${s.id} (${s.spec})`).join(", ")
93
72
  return `${schedules.length} schedule${schedules.length === 1 ? "" : "s"}: ${list}`
94
73
  }
95
74
 
96
- async function readState<T>(directory: string, file: string): Promise<T | undefined> {
97
- try {
98
- const f = Bun.file(`${directory}/${STATE_SUBDIR}/${file}`)
99
- if (!(await f.exists())) return undefined
100
- return (await f.json()) as T
101
- } catch {
102
- return undefined
103
- }
104
- }
105
-
106
75
  const tui: TuiPlugin = async (api, options) => {
107
76
  const opts = (options ?? {}) as TuiOptions
108
- const notifyIdle = opts.notifyIdle !== false
109
- const notifyPermission = opts.notifyPermission !== false
110
- const notifyQuestion = opts.notifyQuestion !== false
111
- const notifyError = opts.notifyError !== false
112
-
113
- const unsubs: Array<() => void> = []
114
-
115
- try {
116
- if (notifyIdle) {
117
- unsubs.push(
118
- api.event.on("session.status", (event) => {
119
- if (event.properties.status.type !== "idle") return
120
- void api.attention.notify({
121
- title: "opencode",
122
- message: "turn complete",
123
- sound: { name: "done", when: "blurred" },
124
- notification: { when: "blurred" },
125
- })
126
- }),
127
- )
128
- }
129
- } catch (e) {
130
- console.warn(`[overclock-tui] session.status subscription failed: ${e}`)
131
- }
132
-
133
- try {
134
- if (notifyPermission) {
135
- unsubs.push(
136
- api.event.on("permission.asked", () => {
137
- void api.attention.notify({
138
- title: "opencode",
139
- message: "needs permission",
140
- sound: { name: "permission", when: "blurred" },
141
- notification: { when: "blurred" },
142
- })
143
- }),
144
- )
145
- }
146
- } catch (e) {
147
- console.warn(`[overclock-tui] permission.asked subscription failed: ${e}`)
148
- }
149
-
150
- try {
151
- if (notifyQuestion) {
152
- unsubs.push(
153
- api.event.on("question.asked", () => {
154
- void api.attention.notify({
155
- title: "opencode",
156
- message: "asking a question",
157
- sound: { name: "question", when: "blurred" },
158
- notification: { when: "blurred" },
159
- })
160
- }),
161
- )
162
- }
163
- } catch (e) {
164
- console.warn(`[overclock-tui] question.asked subscription failed: ${e}`)
165
- }
166
-
167
- try {
168
- if (notifyError) {
169
- unsubs.push(
170
- api.event.on("session.error", () => {
171
- void api.attention.notify({
172
- title: "opencode",
173
- message: "session error",
174
- sound: { name: "error", when: "blurred" },
175
- notification: { when: "blurred" },
176
- })
177
- }),
178
- )
179
- }
180
- } catch (e) {
181
- console.warn(`[overclock-tui] session.error subscription failed: ${e}`)
182
- }
183
-
184
- try {
185
- for (const unsub of unsubs) api.lifecycle.onDispose(async () => unsub())
186
- } catch (e) {
187
- console.warn(`[overclock-tui] lifecycle registration failed: ${e}`)
77
+ const ui = createUi(api)
78
+
79
+ if (opts.notifyIdle !== false) {
80
+ ui.on("session.status", (event) => {
81
+ if (event.properties?.status?.type === "idle") {
82
+ ui.notify({ message: "turn complete", sound: "done" })
83
+ }
84
+ })
188
85
  }
189
86
 
190
- try {
191
- const unregister = api.command?.register(() => [
192
- {
193
- title: "Overclock: Tasks",
194
- value: "overclock.tasks",
195
- slash: { name: "oc-tasks" },
196
- onSelect: async () => {
197
- const entries = await readState<TaskMirrorEntry[]>(api.state.path.directory, "tasks.json")
198
- api.ui.toast({ message: formatTasksSummary(entries) })
199
- },
200
- },
201
- ])
202
- if (unregister) api.lifecycle.onDispose(async () => unregister())
203
- } catch (e) {
204
- console.warn(`[overclock-tui] /oc-tasks command registration failed: ${e}`)
87
+ if (opts.notifyPermission !== false) {
88
+ ui.on("permission.asked", () => ui.notify({ message: "needs permission", sound: "permission" }))
205
89
  }
206
90
 
207
- try {
208
- const unregister = api.command?.register(() => [
209
- {
210
- title: "Overclock: Usage",
211
- value: "overclock.usage",
212
- slash: { name: "oc-usage" },
213
- onSelect: async () => {
214
- const state = await readState<UsageStateShape>(api.state.path.directory, "usage.json")
215
- api.ui.toast({ message: formatUsageSummary(state) })
216
- },
217
- },
218
- ])
219
- if (unregister) api.lifecycle.onDispose(async () => unregister())
220
- } catch (e) {
221
- console.warn(`[overclock-tui] /oc-usage command registration failed: ${e}`)
91
+ if (opts.notifyQuestion !== false) {
92
+ ui.on("question.asked", () => ui.notify({ message: "asking a question", sound: "question" }))
222
93
  }
223
94
 
224
- try {
225
- const unregister = api.command?.register(() => [
226
- {
227
- title: "Overclock: Schedules",
228
- value: "overclock.schedules",
229
- slash: { name: "oc-schedules" },
230
- onSelect: async () => {
231
- const schedules = await readState<ScheduleMirrorEntry[]>(
232
- api.state.path.directory,
233
- "schedules.json",
234
- )
235
- api.ui.toast({ message: formatSchedulesSummary(schedules) })
236
- },
237
- },
238
- ])
239
- if (unregister) api.lifecycle.onDispose(async () => unregister())
240
- } catch (e) {
241
- console.warn(`[overclock-tui] /oc-schedules command registration failed: ${e}`)
95
+ if (opts.notifyError !== false) {
96
+ ui.on("session.error", () => ui.notify({ message: "session error", sound: "error" }))
242
97
  }
243
98
 
244
- // ASCII pet beside the prompt. Anything here failing (missing @opentui/solid,
245
- // slot API drift) must degrade to "no buddy", never take the notifications down.
246
- try {
247
- if (opts.buddy !== false && (await buddyEnabledInConfig(api.state.path.directory))) {
248
- await registerBuddy(api)
99
+ ui.command({
100
+ title: "Overclock: Tasks",
101
+ slash: "oc-tasks",
102
+ run: async () => ui.toast(formatTasksSummary(await ui.readOptional(taskStore))),
103
+ })
104
+
105
+ ui.command({
106
+ title: "Overclock: Usage",
107
+ slash: "oc-usage",
108
+ run: async () => ui.toast(formatUsageSummary(await ui.readOptional(usageStore))),
109
+ })
110
+
111
+ ui.command({
112
+ title: "Overclock: Schedules",
113
+ slash: "oc-schedules",
114
+ run: async () => ui.toast(formatSchedulesSummary(await ui.readOptional(scheduleStore))),
115
+ })
116
+
117
+ if (isBuddyEnabled(opts)) {
118
+ try {
119
+ await registerBuddy(ui)
120
+ } catch (e) {
121
+ console.warn(`[overclock-tui] buddy disabled: ${e}`)
249
122
  }
250
- } catch (e) {
251
- console.warn(`[overclock-tui] buddy disabled: ${e}`)
252
123
  }
253
124
  }
254
125
 
package/src/types.ts CHANGED
@@ -1,73 +1 @@
1
- import type { Hooks, PluginInput } from "@opencode-ai/plugin"
2
- import type { BusyTracker } from "./lib/busy.ts"
3
-
4
- /**
5
- * Singletons built once by the entry and handed to every module.
6
- * Derived from the event bus, so they must have exactly one subscription -- a per-module
7
- * copy would be N subscriptions maintaining N identical copies of the same state.
8
- */
9
- export interface SharedDeps {
10
- /** live per-session busy/idle state; the entry owns the subscription that feeds it */
11
- busy: BusyTracker
12
- /**
13
- * Declared tool name -> the name the model was actually offered (see `toolNames` config).
14
- * Needed wherever a module names one of its own tools in text the model reads: under a
15
- * remap the declared name is not a tool the model has.
16
- */
17
- toolName(declared: string): string
18
- }
19
-
20
- /** Per-feature config from .opencode/overclock.json. `false` = off, object = options. */
21
- export type FeatureConfig = boolean | Record<string, unknown>
22
-
23
- /** A problem found in overclock.json. Lives here so config consumers need not import validate. */
24
- export interface ConfigIssue {
25
- /** dotted location in overclock.json, e.g. "features.tasks.killOnExit" */
26
- path: string
27
- message: string
28
- }
29
-
30
- /** Option value kinds a module declares, so a typo in overclock.json can be caught. */
31
- export type OptionType = "boolean" | "number" | "string" | "array" | "object"
32
-
33
- export interface OverclockConfig {
34
- features?: Record<string, FeatureConfig>
35
- /**
36
- * Model-visible tool ids: declared name -> replacement. The key of the `tool` hook map is
37
- * literally the name sent to the provider, so this is the whole remap. Exists for hosts
38
- * behind a proxy that whitelists tool names and rejects unknown ones.
39
- *
40
- * A replacement that collides with a built-in tool *overrides* that built-in in the final
41
- * tool map -- borrow a name you do not mind losing. Permission ids are deliberately not
42
- * remapped: they key the user's opencode permission config, not the wire format.
43
- */
44
- toolNames?: Record<string, string>
45
- /**
46
- * The only tool names the model may be offered. Entries are literal names or the name of a
47
- * bundled list (see KNOWN_ALLOWLISTS), so extending one is `["claude-code", "MyExtraTool"]`.
48
- * A bundled list also supplies default aliases for this plugin's tools; `toolNames` overrides
49
- * those per tool.
50
- *
51
- * Any tool whose final name is not permitted is withheld from the model rather than offered
52
- * and rejected -- a single unrecognised name can fail a whole request, so a loud gap at
53
- * startup beats a session that cannot reach the provider.
54
- */
55
- toolAllowlist?: string[] | string
56
- }
57
-
58
- /**
59
- * One feature = one module.
60
- * init returns partial Hooks. Same hook from many modules -> composed in registry order.
61
- */
62
- export interface FeatureModule {
63
- name: string
64
- /** on by default? */
65
- defaultEnabled: boolean
66
- /** SDK client surfaces (dot-paths) the module needs. Missing -> module skipped + warn. */
67
- requires?: string[]
68
- /** Tool names registered. Declared, not derived -- feeds the first-run summary. */
69
- tools?: string[]
70
- /** Accepted option keys -> expected type. Anything else in config is a typo. */
71
- options?: Record<string, OptionType>
72
- init(ctx: PluginInput, options: Record<string, unknown>, shared: SharedDeps): Promise<Partial<Hooks>>
73
- }
1
+ export * from "./core/types.ts"