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.
- package/README.md +252 -111
- package/package.json +6 -4
- package/skills/codebase-design/DEEPENING.md +35 -0
- package/skills/codebase-design/DESIGN-IT-TWICE.md +34 -0
- package/skills/codebase-design/SKILL.md +93 -0
- package/skills/diagnosing-bugs/SKILL.md +123 -0
- package/skills/domain-modeling/ADR-FORMAT.md +55 -0
- package/skills/domain-modeling/CONTEXT-FORMAT.md +32 -0
- package/skills/domain-modeling/SKILL.md +102 -0
- package/skills/doubt/SKILL.md +80 -0
- package/skills/grilling/SKILL.md +96 -0
- package/skills/source-discipline/SKILL.md +78 -0
- package/skills/tdd/SKILL.md +87 -0
- package/skills/to-spec/SKILL.md +69 -0
- package/skills/to-spec/SPEC-TEMPLATE.md +50 -0
- package/skills/to-tickets/SKILL.md +74 -0
- package/skills/to-tickets/TICKET-TEMPLATE.md +41 -0
- package/src/bridge.ts +1 -0
- package/src/buddy/companion.ts +104 -5
- package/src/buddy/sprites.ts +4 -4
- package/src/buddy/tui.ts +175 -65
- package/src/core/bridge.ts +34 -0
- package/src/core/lifecycle.ts +67 -0
- package/src/core/policy.ts +128 -0
- package/src/core/summary.ts +33 -0
- package/src/core/types.ts +193 -0
- package/src/features/buddy.ts +1 -2
- package/src/features/guard.ts +421 -37
- package/src/features/index.ts +18 -4
- package/src/features/recovery.ts +153 -0
- package/src/features/safety.ts +147 -0
- package/src/features/sched.ts +183 -89
- package/src/features/tasks.ts +134 -33
- package/src/features/truncator.ts +116 -0
- package/src/features/usage.ts +46 -65
- package/src/features/workflow.ts +256 -0
- package/src/index.ts +96 -67
- package/src/lib/busy.ts +1 -25
- package/src/lib/exec.ts +13 -0
- package/src/lib/inject.ts +10 -56
- package/src/lib/mirror.ts +13 -0
- package/src/lib/probe.ts +1 -15
- package/src/lib/state.ts +10 -39
- package/src/lib/tmux.ts +1 -0
- package/src/lib/ui.ts +208 -0
- package/src/merge.ts +2 -66
- package/src/platform/probe.ts +25 -0
- package/src/platform/process/exec.ts +317 -0
- package/src/platform/process/tmux.ts +60 -0
- package/src/platform/session/busy.ts +33 -0
- package/src/platform/session/inject.ts +89 -0
- package/src/platform/session/notify.ts +20 -0
- package/src/platform/storage/state.ts +99 -0
- package/src/platform/storage/store.ts +61 -0
- package/src/summary.ts +1 -0
- package/src/tools.ts +8 -244
- package/src/tui.ts +57 -186
- package/src/types.ts +1 -73
- package/src/v2/context.ts +470 -0
- package/src/v2/host.ts +120 -0
- package/src/v2/loader.ts +150 -0
- package/src/workflow/agents/codebase-researcher.ts +27 -0
- package/src/workflow/agents/design-explorer.ts +33 -0
- package/src/workflow/agents/doubt-reviewer.ts +26 -0
- package/src/workflow/agents/engineering-coach.ts +23 -0
- package/src/workflow/agents/performance-auditor.ts +29 -0
- package/src/workflow/agents/security-auditor.ts +23 -0
- package/src/workflow/agents/spec-reviewer.ts +15 -0
- package/src/workflow/agents/standards-reviewer.ts +24 -0
- package/src/workflow/agents/test-engineer.ts +28 -0
- package/src/workflow/catalog.ts +210 -0
- package/src/workflow/templates/build.ts +47 -0
- package/src/workflow/templates/define.ts +45 -0
- package/src/workflow/templates/diagnose.ts +58 -0
- package/src/workflow/templates/plan.ts +52 -0
- package/src/workflow/templates/ship.ts +64 -0
- package/src/buddy/reactions.ts +0 -41
- package/src/buddy/types.ts +0 -30
- package/src/config.ts +0 -19
- package/src/features/checkpoints.ts +0 -128
- package/src/features/sandbox.ts +0 -104
- package/src/validate.ts +0 -197
|
@@ -0,0 +1,256 @@
|
|
|
1
|
+
import { existsSync } from "node:fs"
|
|
2
|
+
import { resolve, dirname } from "node:path"
|
|
3
|
+
import { fileURLToPath } from "node:url"
|
|
4
|
+
import type { FeatureModule, WorkflowOptions } from "../core/types.ts"
|
|
5
|
+
import { DEFINE_TEMPLATE } from "../workflow/templates/define.ts"
|
|
6
|
+
import { PLAN_TEMPLATE } from "../workflow/templates/plan.ts"
|
|
7
|
+
import { BUILD_TEMPLATE } from "../workflow/templates/build.ts"
|
|
8
|
+
import { DIAGNOSE_TEMPLATE } from "../workflow/templates/diagnose.ts"
|
|
9
|
+
import { SHIP_TEMPLATE } from "../workflow/templates/ship.ts"
|
|
10
|
+
import { STANDARDS_REVIEWER_PROMPT } from "../workflow/agents/standards-reviewer.ts"
|
|
11
|
+
import { SPEC_REVIEWER_PROMPT } from "../workflow/agents/spec-reviewer.ts"
|
|
12
|
+
import { SECURITY_AUDITOR_PROMPT } from "../workflow/agents/security-auditor.ts"
|
|
13
|
+
import { TEST_ENGINEER_PROMPT } from "../workflow/agents/test-engineer.ts"
|
|
14
|
+
import { PERFORMANCE_AUDITOR_PROMPT } from "../workflow/agents/performance-auditor.ts"
|
|
15
|
+
import { DOUBT_REVIEWER_PROMPT } from "../workflow/agents/doubt-reviewer.ts"
|
|
16
|
+
import { CODEBASE_RESEARCHER_PROMPT } from "../workflow/agents/codebase-researcher.ts"
|
|
17
|
+
import { DESIGN_EXPLORER_PROMPT } from "../workflow/agents/design-explorer.ts"
|
|
18
|
+
import { ENGINEERING_COACH_PROMPT } from "../workflow/agents/engineering-coach.ts"
|
|
19
|
+
|
|
20
|
+
function getBundledSkillsDir(customPath?: string): string {
|
|
21
|
+
if (customPath) return customPath
|
|
22
|
+
const currentDir =
|
|
23
|
+
typeof import.meta.dir === "string" ? import.meta.dir : dirname(fileURLToPath(import.meta.url))
|
|
24
|
+
return resolve(currentDir, "../../skills")
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export const WORKFLOW_COMMANDS = {
|
|
28
|
+
define: {
|
|
29
|
+
description: "Interrogate requirements and draft SPEC.md with recommended defaults",
|
|
30
|
+
template: DEFINE_TEMPLATE,
|
|
31
|
+
},
|
|
32
|
+
plan: {
|
|
33
|
+
description: "Decompose spec into vertical tracer-bullet tasks in tasks/plan.md",
|
|
34
|
+
template: PLAN_TEMPLATE,
|
|
35
|
+
},
|
|
36
|
+
build: {
|
|
37
|
+
description: "Autonomous TDD implementation with tripwires and atomic commits",
|
|
38
|
+
template: BUILD_TEMPLATE,
|
|
39
|
+
},
|
|
40
|
+
diagnose: {
|
|
41
|
+
description: "Disciplined bug reproduction and isolation loop ([DEBUG-xxxx] tags)",
|
|
42
|
+
template: DIAGNOSE_TEMPLATE,
|
|
43
|
+
},
|
|
44
|
+
ship: {
|
|
45
|
+
description: "3-way parallel review (Standards, Spec, Security) with GO/NO-GO verdict",
|
|
46
|
+
template: SHIP_TEMPLATE,
|
|
47
|
+
},
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export const WORKFLOW_AGENTS = {
|
|
51
|
+
"standards-reviewer": {
|
|
52
|
+
mode: "subagent" as const,
|
|
53
|
+
description: "Senior Staff Engineer auditing diffs for repo conventions and code smells",
|
|
54
|
+
prompt: STANDARDS_REVIEWER_PROMPT,
|
|
55
|
+
tools: {
|
|
56
|
+
write: false,
|
|
57
|
+
edit: false,
|
|
58
|
+
},
|
|
59
|
+
permission: {
|
|
60
|
+
edit: "deny" as const,
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
"spec-reviewer": {
|
|
64
|
+
mode: "subagent" as const,
|
|
65
|
+
description: "Product Engineer auditing diffs strictly against originating specifications",
|
|
66
|
+
prompt: SPEC_REVIEWER_PROMPT,
|
|
67
|
+
tools: {
|
|
68
|
+
write: false,
|
|
69
|
+
edit: false,
|
|
70
|
+
},
|
|
71
|
+
permission: {
|
|
72
|
+
edit: "deny" as const,
|
|
73
|
+
},
|
|
74
|
+
},
|
|
75
|
+
"security-auditor": {
|
|
76
|
+
mode: "subagent" as const,
|
|
77
|
+
description: "Adversarial Security Engineer auditing diffs for OWASP vulnerabilities and secrets",
|
|
78
|
+
prompt: SECURITY_AUDITOR_PROMPT,
|
|
79
|
+
tools: {
|
|
80
|
+
write: false,
|
|
81
|
+
edit: false,
|
|
82
|
+
},
|
|
83
|
+
permission: {
|
|
84
|
+
edit: "deny" as const,
|
|
85
|
+
},
|
|
86
|
+
},
|
|
87
|
+
"test-engineer": {
|
|
88
|
+
mode: "subagent" as const,
|
|
89
|
+
description: "QA Engineer auditing test strategy, coverage gaps, and Prove-It verification",
|
|
90
|
+
prompt: TEST_ENGINEER_PROMPT,
|
|
91
|
+
tools: {
|
|
92
|
+
write: false,
|
|
93
|
+
edit: false,
|
|
94
|
+
},
|
|
95
|
+
permission: {
|
|
96
|
+
edit: "deny" as const,
|
|
97
|
+
},
|
|
98
|
+
},
|
|
99
|
+
"performance-auditor": {
|
|
100
|
+
mode: "subagent" as const,
|
|
101
|
+
description: "Senior Performance Engineer auditing latency, N+1 queries, and resource leaks",
|
|
102
|
+
prompt: PERFORMANCE_AUDITOR_PROMPT,
|
|
103
|
+
tools: {
|
|
104
|
+
write: false,
|
|
105
|
+
edit: false,
|
|
106
|
+
},
|
|
107
|
+
permission: {
|
|
108
|
+
edit: "deny" as const,
|
|
109
|
+
},
|
|
110
|
+
},
|
|
111
|
+
"doubt-reviewer": {
|
|
112
|
+
mode: "subagent" as const,
|
|
113
|
+
description: "Adversarial Verification Engineer evaluating artifacts without author bias",
|
|
114
|
+
prompt: DOUBT_REVIEWER_PROMPT,
|
|
115
|
+
tools: {
|
|
116
|
+
write: false,
|
|
117
|
+
edit: false,
|
|
118
|
+
},
|
|
119
|
+
permission: {
|
|
120
|
+
edit: "deny" as const,
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
"codebase-researcher": {
|
|
124
|
+
mode: "subagent" as const,
|
|
125
|
+
description: "Scout Agent tracing seams, dependencies, and call graphs without polluting context",
|
|
126
|
+
prompt: CODEBASE_RESEARCHER_PROMPT,
|
|
127
|
+
tools: {
|
|
128
|
+
write: false,
|
|
129
|
+
edit: false,
|
|
130
|
+
},
|
|
131
|
+
permission: {
|
|
132
|
+
edit: "deny" as const,
|
|
133
|
+
},
|
|
134
|
+
},
|
|
135
|
+
"design-explorer": {
|
|
136
|
+
mode: "subagent" as const,
|
|
137
|
+
description: "Principal Architect producing contrasting 'Design It Twice' interface proposals",
|
|
138
|
+
prompt: DESIGN_EXPLORER_PROMPT,
|
|
139
|
+
tools: {
|
|
140
|
+
write: false,
|
|
141
|
+
edit: false,
|
|
142
|
+
},
|
|
143
|
+
permission: {
|
|
144
|
+
edit: "deny" as const,
|
|
145
|
+
},
|
|
146
|
+
},
|
|
147
|
+
"engineering-coach": {
|
|
148
|
+
mode: "subagent" as const,
|
|
149
|
+
description: "Elite Staff Mentor providing Socratic debugging guidance and design critique",
|
|
150
|
+
prompt: ENGINEERING_COACH_PROMPT,
|
|
151
|
+
tools: {
|
|
152
|
+
write: false,
|
|
153
|
+
edit: false,
|
|
154
|
+
},
|
|
155
|
+
permission: {
|
|
156
|
+
edit: "deny" as const,
|
|
157
|
+
},
|
|
158
|
+
},
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export const workflow: FeatureModule = {
|
|
162
|
+
name: "workflow",
|
|
163
|
+
defaultEnabled: true,
|
|
164
|
+
tools: [],
|
|
165
|
+
async init(_ctx, options) {
|
|
166
|
+
const opts = (options ?? {}) as WorkflowOptions
|
|
167
|
+
if (opts.enabled === false) {
|
|
168
|
+
return {}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
const skillsPath = getBundledSkillsDir(opts.skillsPath)
|
|
172
|
+
|
|
173
|
+
return {
|
|
174
|
+
config: async (cfg: any) => {
|
|
175
|
+
if (opts.commands !== false) {
|
|
176
|
+
cfg.command = {
|
|
177
|
+
...WORKFLOW_COMMANDS,
|
|
178
|
+
...(cfg.command ?? {}),
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (opts.subagents !== false) {
|
|
183
|
+
cfg.agent = {
|
|
184
|
+
...WORKFLOW_AGENTS,
|
|
185
|
+
...(cfg.agent ?? {}),
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (existsSync(skillsPath)) {
|
|
190
|
+
cfg.skills = typeof cfg.skills === "object" && cfg.skills !== null ? cfg.skills : {}
|
|
191
|
+
if (!Array.isArray(cfg.skills.paths)) {
|
|
192
|
+
cfg.skills.paths = []
|
|
193
|
+
}
|
|
194
|
+
if (!cfg.skills.paths.includes(skillsPath)) {
|
|
195
|
+
cfg.skills.paths.push(skillsPath)
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
},
|
|
199
|
+
}
|
|
200
|
+
},
|
|
201
|
+
|
|
202
|
+
setup: async (v2Context, options) => {
|
|
203
|
+
const opts = (options ?? {}) as WorkflowOptions
|
|
204
|
+
if (opts.enabled === false) return
|
|
205
|
+
|
|
206
|
+
if (opts.commands !== false && v2Context.command?.transform) {
|
|
207
|
+
await v2Context.command.transform(async (draft) => {
|
|
208
|
+
for (const [name, cmd] of Object.entries(WORKFLOW_COMMANDS)) {
|
|
209
|
+
draft.update(name, (current) => {
|
|
210
|
+
current.name = current.name ?? name
|
|
211
|
+
current.description = current.description ?? cmd.description
|
|
212
|
+
current.template = current.template ?? cmd.template
|
|
213
|
+
})
|
|
214
|
+
}
|
|
215
|
+
})
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
if (opts.subagents !== false && v2Context.agent?.transform) {
|
|
219
|
+
await v2Context.agent.transform(async (draft) => {
|
|
220
|
+
for (const [id, ag] of Object.entries(WORKFLOW_AGENTS)) {
|
|
221
|
+
draft.update(id, (current) => {
|
|
222
|
+
current.mode = current.mode ?? ag.mode
|
|
223
|
+
current.description = current.description ?? ag.description
|
|
224
|
+
current.system = current.system ?? ag.prompt
|
|
225
|
+
if (ag.permission?.edit === "deny") {
|
|
226
|
+
const perms = (current.permissions as any[]) ?? []
|
|
227
|
+
const hasDenyEdit = perms.some((p: any) => p.action === "edit" && p.effect === "deny")
|
|
228
|
+
if (!hasDenyEdit) {
|
|
229
|
+
perms.push({
|
|
230
|
+
action: "edit",
|
|
231
|
+
resource: "*",
|
|
232
|
+
effect: "deny",
|
|
233
|
+
})
|
|
234
|
+
current.permissions = perms as any
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
})
|
|
238
|
+
}
|
|
239
|
+
})
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
const skillsPath = getBundledSkillsDir(opts.skillsPath)
|
|
243
|
+
if (existsSync(skillsPath) && v2Context.skill?.transform) {
|
|
244
|
+
await v2Context.skill.transform(async (draft) => {
|
|
245
|
+
const existing = draft.list?.() ?? []
|
|
246
|
+
const alreadyAdded = existing.some((s: any) => s.type === "directory" && s.path === skillsPath)
|
|
247
|
+
if (!alreadyAdded) {
|
|
248
|
+
draft.source({
|
|
249
|
+
type: "directory",
|
|
250
|
+
path: skillsPath,
|
|
251
|
+
} as any)
|
|
252
|
+
}
|
|
253
|
+
})
|
|
254
|
+
}
|
|
255
|
+
},
|
|
256
|
+
}
|
package/src/index.ts
CHANGED
|
@@ -1,85 +1,114 @@
|
|
|
1
|
-
import type { Hooks
|
|
1
|
+
import type { Hooks } from "@opencode-ai/plugin"
|
|
2
2
|
import { features } from "./features/index.ts"
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { mergeHooks } from "./core/lifecycle.ts"
|
|
4
|
+
import { resolveToolPolicy } from "./core/policy.ts"
|
|
5
|
+
import { summarise } from "./core/summary.ts"
|
|
6
|
+
import { createHybridPlugin, type HybridPlugin } from "./core/bridge.ts"
|
|
7
|
+
import type { FeatureModule, OverclockOptions, SharedDeps } from "./core/types.ts"
|
|
5
8
|
import { missingSurfaces } from "./lib/probe.ts"
|
|
6
9
|
import { toast } from "./lib/inject.ts"
|
|
7
10
|
import { firstRun } from "./lib/state.ts"
|
|
8
|
-
import { validateConfig, summarise } from "./validate.ts"
|
|
9
11
|
import { createBusyTracker } from "./lib/busy.ts"
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
+
import { createV2Host } from "./v2/host.ts"
|
|
13
|
+
|
|
14
|
+
function featureOptions(
|
|
15
|
+
options: OverclockOptions,
|
|
16
|
+
feature: FeatureModule,
|
|
17
|
+
): Record<string, unknown> | null {
|
|
18
|
+
const setting = options[feature.name] ?? options.features?.[feature.name] ?? feature.defaultEnabled
|
|
19
|
+
if (setting === false) return null
|
|
20
|
+
return typeof setting === "object" && setting !== null ? (setting as Record<string, unknown>) : {}
|
|
21
|
+
}
|
|
12
22
|
|
|
13
23
|
/**
|
|
14
|
-
* Entry.
|
|
15
|
-
*
|
|
24
|
+
* Entry. Read options -> probe surfaces -> init enabled modules -> merge hooks.
|
|
25
|
+
* Employs createHybridPlugin so the plugin is runnable on both V1 and V2 OpenCode harnesses.
|
|
16
26
|
*/
|
|
17
|
-
export const Overclock:
|
|
18
|
-
|
|
27
|
+
export const Overclock: HybridPlugin<OverclockOptions> = createHybridPlugin<OverclockOptions>({
|
|
28
|
+
id: "overclock",
|
|
29
|
+
|
|
30
|
+
/** V1 lifecycle: tools, execution interception, event bus hooks */
|
|
31
|
+
server: async (ctx, pluginOptions) => {
|
|
32
|
+
const options = (pluginOptions ?? {}) as OverclockOptions
|
|
33
|
+
const { policy, issues } = resolveToolPolicy(options, features)
|
|
34
|
+
|
|
35
|
+
const shared: SharedDeps = {
|
|
36
|
+
busy: createBusyTracker(),
|
|
37
|
+
toolName: (declared) => policy.rename[declared] ?? declared,
|
|
38
|
+
}
|
|
39
|
+
const parts: Partial<Hooks>[] = [{ event: async ({ event }) => shared.busy.onEvent(event) }]
|
|
40
|
+
const skipped: string[] = []
|
|
41
|
+
const enabled: FeatureModule[] = []
|
|
42
|
+
|
|
43
|
+
for (const feature of features) {
|
|
44
|
+
const opts = featureOptions(options, feature)
|
|
45
|
+
if (opts === null) continue
|
|
19
46
|
|
|
20
|
-
|
|
21
|
-
|
|
47
|
+
const missing = missingSurfaces(ctx.client, feature.requires ?? [])
|
|
48
|
+
if (missing.length) {
|
|
49
|
+
console.warn(
|
|
50
|
+
`[overclock] ${feature.name} disabled: client lacks ${missing.join(", ")} (upstream drift?)`,
|
|
51
|
+
)
|
|
52
|
+
skipped.push(feature.name)
|
|
53
|
+
continue
|
|
54
|
+
}
|
|
22
55
|
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
}
|
|
31
|
-
// First part, so the tracker is current before any module's own event hook reads it.
|
|
32
|
-
const parts: Partial<Hooks>[] = [{ event: async ({ event }) => shared.busy.onEvent(event) }]
|
|
33
|
-
const skipped: string[] = []
|
|
34
|
-
const enabled: FeatureModule[] = []
|
|
56
|
+
try {
|
|
57
|
+
parts.push(await feature.init(ctx, opts, shared))
|
|
58
|
+
enabled.push(feature)
|
|
59
|
+
} catch (e) {
|
|
60
|
+
console.warn(`[overclock] feature ${feature.name} failed init: ${e}`)
|
|
61
|
+
}
|
|
62
|
+
}
|
|
35
63
|
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
64
|
+
if (Array.isArray(options.plugins) && options.plugins.length > 0) {
|
|
65
|
+
const v2Host = createV2Host(ctx, options)
|
|
66
|
+
const loadedV2 = await v2Host.loadPlugins(options.plugins)
|
|
67
|
+
if (loadedV2.length > 0) {
|
|
68
|
+
console.warn(`[overclock] loaded ${loadedV2.length} v2 plugin(s): ${loadedV2.join(", ")}`)
|
|
69
|
+
}
|
|
70
|
+
parts.push(v2Host.createHooks())
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
for (const issue of issues) {
|
|
74
|
+
console.warn(`[overclock] config: ${issue.path ? `${issue.path}: ` : ""}${issue.message}`)
|
|
75
|
+
}
|
|
76
|
+
if (issues.length) {
|
|
77
|
+
void toast(
|
|
78
|
+
ctx.client,
|
|
79
|
+
`overclock: ${issues.length} config issue${issues.length > 1 ? "s" : ""} (see logs)`,
|
|
80
|
+
"warning",
|
|
43
81
|
)
|
|
44
|
-
skipped.push(feature.name)
|
|
45
|
-
continue
|
|
46
82
|
}
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
console.warn(`[overclock] feature ${feature.name} failed init: ${e}`)
|
|
83
|
+
|
|
84
|
+
const summary = summarise(enabled, skipped, policy)
|
|
85
|
+
console.warn(`[overclock] ${summary}`)
|
|
86
|
+
if (await firstRun(ctx.directory)) {
|
|
87
|
+
void toast(ctx.client, `overclock active: ${summary}`, "info")
|
|
53
88
|
}
|
|
54
|
-
}
|
|
55
89
|
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
90
|
+
if (skipped.length) {
|
|
91
|
+
void toast(ctx.client, `overclock: ${skipped.join(", ")} disabled (SDK drift)`, "warning")
|
|
92
|
+
}
|
|
93
|
+
return mergeHooks(parts, policy)
|
|
94
|
+
},
|
|
59
95
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
for (const issue of issues) {
|
|
64
|
-
console.warn(`[overclock] config: ${issue.path ? `${issue.path}: ` : ""}${issue.message}`)
|
|
65
|
-
}
|
|
66
|
-
if (issues.length) {
|
|
67
|
-
void toast(
|
|
68
|
-
ctx.client,
|
|
69
|
-
`overclock: ${issues.length} config issue${issues.length > 1 ? "s" : ""} (see logs)`,
|
|
70
|
-
"warning",
|
|
71
|
-
)
|
|
72
|
-
}
|
|
96
|
+
/** V2 lifecycle: domain transforms (agents, commands, catalog, aisdk) */
|
|
97
|
+
setup: async (v2Context, pluginOptions) => {
|
|
98
|
+
const options = (pluginOptions ?? {}) as OverclockOptions
|
|
73
99
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
if (await firstRun(ctx.directory)) {
|
|
79
|
-
void toast(ctx.client, `overclock active: ${summarise(enabled, skipped, policy)}`, "info")
|
|
80
|
-
}
|
|
100
|
+
for (const feature of features) {
|
|
101
|
+
if (!feature.setup) continue
|
|
102
|
+
const opts = featureOptions(options, feature)
|
|
103
|
+
if (opts === null) continue
|
|
81
104
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
}
|
|
105
|
+
try {
|
|
106
|
+
await feature.setup(v2Context, opts)
|
|
107
|
+
} catch (e) {
|
|
108
|
+
console.warn(`[overclock] feature ${feature.name} failed v2 setup: ${e}`)
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
},
|
|
112
|
+
})
|
|
113
|
+
|
|
114
|
+
export default Overclock
|
package/src/lib/busy.ts
CHANGED
|
@@ -1,25 +1 @@
|
|
|
1
|
-
|
|
2
|
-
* Session busy tracking via `session.status` (doc-preferred; `session.idle` deprecated).
|
|
3
|
-
* Unknown/never-firing statuses degrade gracefully: empty set = nothing reported busy.
|
|
4
|
-
*/
|
|
5
|
-
export interface BusyTracker {
|
|
6
|
-
/** feed bus events */
|
|
7
|
-
onEvent(event: { type: string; properties?: unknown }): void
|
|
8
|
-
isBusy(sessionID: string): boolean
|
|
9
|
-
}
|
|
10
|
-
|
|
11
|
-
export function createBusyTracker(): BusyTracker {
|
|
12
|
-
const busy = new Set<string>()
|
|
13
|
-
return {
|
|
14
|
-
onEvent(event) {
|
|
15
|
-
const p = (event.properties ?? {}) as { sessionID?: string; status?: { type?: string } }
|
|
16
|
-
if (!p.sessionID) return
|
|
17
|
-
if (event.type === "session.status") {
|
|
18
|
-
p.status?.type === "idle" ? busy.delete(p.sessionID) : busy.add(p.sessionID)
|
|
19
|
-
} else if (event.type === "session.idle" || event.type === "session.deleted") {
|
|
20
|
-
busy.delete(p.sessionID)
|
|
21
|
-
}
|
|
22
|
-
},
|
|
23
|
-
isBusy: (id) => busy.has(id),
|
|
24
|
-
}
|
|
25
|
-
}
|
|
1
|
+
export { createBusyTracker, type BusyTracker } from "../platform/session/busy.ts"
|
package/src/lib/exec.ts
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export {
|
|
2
|
+
execBash,
|
|
3
|
+
killProcessTree,
|
|
4
|
+
shellQuote,
|
|
5
|
+
sanitizeEnv,
|
|
6
|
+
redactSensitiveOutput,
|
|
7
|
+
NON_INTERACTIVE_ENV,
|
|
8
|
+
DEFAULT_PRESERVED_ENV,
|
|
9
|
+
SENSITIVE_ENV_PATTERN,
|
|
10
|
+
SENSITIVE_OUTPUT_PATTERNS,
|
|
11
|
+
type ExecBashOptions,
|
|
12
|
+
type ExecBashResult,
|
|
13
|
+
} from "../platform/process/exec.ts"
|
package/src/lib/inject.ts
CHANGED
|
@@ -1,56 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
export async function sessionModel(client: Client, sessionID: string): Promise<ModelRef | undefined> {
|
|
12
|
-
try {
|
|
13
|
-
const res = await client.session.messages({ path: { id: sessionID } })
|
|
14
|
-
const msgs = res.data ?? []
|
|
15
|
-
for (let i = msgs.length - 1; i >= 0; i--) {
|
|
16
|
-
const info = msgs[i]?.info
|
|
17
|
-
if (info?.role === "assistant" && info.modelID) {
|
|
18
|
-
return { providerID: info.providerID, modelID: info.modelID }
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
} catch (e) {
|
|
22
|
-
console.warn(`[overclock] sessionModel lookup failed (${sessionID}): ${e}`)
|
|
23
|
-
}
|
|
24
|
-
return undefined
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/**
|
|
28
|
-
* Re-entry: push text into session as user prompt, on the session's own model.
|
|
29
|
-
* promptAsync = fire-and-forget, server queues if busy. Failure -> warn, never throw.
|
|
30
|
-
*/
|
|
31
|
-
export async function inject(client: Client, sessionID: string, text: string): Promise<boolean> {
|
|
32
|
-
try {
|
|
33
|
-
const model = await sessionModel(client, sessionID)
|
|
34
|
-
await client.session.promptAsync({
|
|
35
|
-
path: { id: sessionID },
|
|
36
|
-
body: { parts: [{ type: "text", text }], ...(model ? { model } : {}) },
|
|
37
|
-
})
|
|
38
|
-
return true
|
|
39
|
-
} catch (e) {
|
|
40
|
-
console.warn(`[overclock] inject failed (session ${sessionID}): ${e}`)
|
|
41
|
-
return false
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
|
|
45
|
-
/** TUI toast, best-effort (headless server -> no TUI, swallow). */
|
|
46
|
-
export async function toast(
|
|
47
|
-
client: Client,
|
|
48
|
-
message: string,
|
|
49
|
-
variant: "info" | "success" | "warning" | "error" = "info",
|
|
50
|
-
): Promise<void> {
|
|
51
|
-
try {
|
|
52
|
-
await client.tui.showToast({ body: { message, variant } })
|
|
53
|
-
} catch {
|
|
54
|
-
// no TUI attached
|
|
55
|
-
}
|
|
56
|
-
}
|
|
1
|
+
export {
|
|
2
|
+
inject,
|
|
3
|
+
sessionContext,
|
|
4
|
+
sessionModel,
|
|
5
|
+
toast,
|
|
6
|
+
type InjectOptions,
|
|
7
|
+
type ModelRef,
|
|
8
|
+
type SessionContext,
|
|
9
|
+
type ToastVariant,
|
|
10
|
+
} from "../platform/session/inject.ts"
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
export {
|
|
2
|
+
scheduleStore,
|
|
3
|
+
taskStore,
|
|
4
|
+
usageStore,
|
|
5
|
+
type DayBucket,
|
|
6
|
+
type DayBucketView,
|
|
7
|
+
type ScheduleEntry,
|
|
8
|
+
type ScheduleEntryView,
|
|
9
|
+
type TaskMirrorEntry,
|
|
10
|
+
type UsageState,
|
|
11
|
+
type UsageStateView,
|
|
12
|
+
type UsageTokens,
|
|
13
|
+
} from "../platform/storage/store.ts"
|
package/src/lib/probe.ts
CHANGED
|
@@ -1,15 +1 @@
|
|
|
1
|
-
|
|
2
|
-
* Runtime surface probe (doc: "probe for the surfaces we depend on ... instead of
|
|
3
|
-
* failing silently when upstream moves"). Dot-paths resolved against the SDK client;
|
|
4
|
-
* leaf must be a function.
|
|
5
|
-
*/
|
|
6
|
-
export function missingSurfaces(client: unknown, paths: string[]): string[] {
|
|
7
|
-
return paths.filter((path) => {
|
|
8
|
-
let node: unknown = client
|
|
9
|
-
for (const key of path.split(".")) {
|
|
10
|
-
if (node == null || typeof node !== "object") return true
|
|
11
|
-
node = (node as Record<string, unknown>)[key]
|
|
12
|
-
}
|
|
13
|
-
return typeof node !== "function"
|
|
14
|
-
})
|
|
15
|
-
}
|
|
1
|
+
export { missingSurfaces } from "../platform/probe.ts"
|
package/src/lib/state.ts
CHANGED
|
@@ -1,39 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
* True once per project, then never again. Marker lives beside the other state, so
|
|
12
|
-
* deleting .opencode/overclock/ re-arms the first-run notice.
|
|
13
|
-
*/
|
|
14
|
-
export async function firstRun(directory: string): Promise<boolean> {
|
|
15
|
-
const dir = await ensureStateDir(directory)
|
|
16
|
-
const marker = Bun.file(`${dir}/.installed`)
|
|
17
|
-
if (await marker.exists()) return false
|
|
18
|
-
await Bun.write(marker, new Date().toISOString())
|
|
19
|
-
return true
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
export async function readJson<T>(path: string, fallback: T): Promise<T> {
|
|
23
|
-
const file = Bun.file(path)
|
|
24
|
-
if (!(await file.exists())) return fallback
|
|
25
|
-
try {
|
|
26
|
-
return (await file.json()) as T
|
|
27
|
-
} catch {
|
|
28
|
-
return fallback
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
export async function writeJson(path: string, value: unknown): Promise<void> {
|
|
33
|
-
await Bun.write(path, JSON.stringify(value, null, 2))
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
/** POSIX single-quote escape. */
|
|
37
|
-
export function shellQuote(s: string): string {
|
|
38
|
-
return `'${s.replace(/'/g, `'\\''`)}'`
|
|
39
|
-
}
|
|
1
|
+
export {
|
|
2
|
+
defineStore,
|
|
3
|
+
ensureStateDir,
|
|
4
|
+
firstRun,
|
|
5
|
+
readJson,
|
|
6
|
+
shellQuote,
|
|
7
|
+
stateDir,
|
|
8
|
+
writeJson,
|
|
9
|
+
type Store,
|
|
10
|
+
} from "../platform/storage/state.ts"
|
package/src/lib/tmux.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { isInsideTmux, spawnTaskPane, type TmuxPane } from "../platform/process/tmux.ts"
|