@plainconceptsplatform/agent-harness 3.9.0 → 3.10.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/LICENSE +21 -21
- package/README.md +551 -551
- package/cli/commands/join.js +259 -259
- package/cli/commands/migrate.js +310 -310
- package/cli/commands/shared.js +27 -27
- package/cli/commands/single.js +79 -79
- package/cli/commands/update.js +121 -121
- package/cli/commands/wizard.js +134 -134
- package/cli/fragments/archive/az.md +97 -97
- package/cli/fragments/archive/gh.md +96 -96
- package/cli/fragments/archive/gl.md +96 -96
- package/cli/fragments/archive/none.md +75 -75
- package/cli/fragments/guardrails/codegraph.md +5 -5
- package/cli/fragments/guardrails/humanizer.md +4 -4
- package/cli/fragments/guardrails/memory.md +4 -4
- package/cli/fragments/guardrails/rtk.md +3 -3
- package/cli/fragments/guardrails/simple-english.md +4 -4
- package/cli/fragments/ops-backlog/az.md +28 -28
- package/cli/fragments/ops-backlog/gh.md +28 -28
- package/cli/fragments/ops-backlog/jira.md +28 -28
- package/cli/fragments/ops-review/az.md +62 -62
- package/cli/fragments/ops-review/gh.md +51 -51
- package/cli/fragments/ops-review/gl.md +55 -55
- package/cli/fragments/ops-ship/az.md +81 -81
- package/cli/fragments/ops-ship/gh.md +67 -67
- package/cli/fragments/ops-ship/gl.md +84 -84
- package/cli/index.js +116 -116
- package/cli/presets/agents-content.json +34 -34
- package/cli/presets/browser.json +10 -10
- package/cli/presets/clean.json +21 -21
- package/cli/presets/models.json +68 -68
- package/cli/presets/optimization.json +32 -32
- package/cli/presets/platforms.json +76 -76
- package/cli/presets/source.json +23 -23
- package/cli/steps/browser/index.js +52 -48
- package/cli/steps/clean/index.js +120 -120
- package/cli/steps/copy/agents.js +176 -176
- package/cli/steps/copy/commands.js +79 -79
- package/cli/steps/copy/fullstack-engineer.js +81 -81
- package/cli/steps/copy/index.js +88 -88
- package/cli/steps/copy/opencode-json.js +359 -324
- package/cli/steps/copy/skills.js +316 -316
- package/cli/steps/metadata/index.js +115 -115
- package/cli/steps/models/format.js +88 -88
- package/cli/steps/models/index.js +64 -64
- package/cli/steps/models/write.js +38 -38
- package/cli/steps/openspec/index.js +136 -136
- package/cli/steps/optimization/codegraph.js +141 -141
- package/cli/steps/optimization/detect.js +66 -66
- package/cli/steps/optimization/humanizer.js +17 -17
- package/cli/steps/optimization/index.js +158 -158
- package/cli/steps/optimization/memory.js +91 -91
- package/cli/steps/optimization/patch-guardrails.js +111 -111
- package/cli/steps/optimization/simple-english.js +17 -17
- package/cli/steps/optimization/skills-lock.js +30 -30
- package/cli/steps/platform/index.js +109 -109
- package/cli/steps/source/index.js +123 -123
- package/cli/utils/agent-color.js +111 -111
- package/cli/utils/copy.js +134 -134
- package/cli/utils/exec-spinner.js +47 -47
- package/cli/utils/exec.js +134 -134
- package/cli/utils/legacy-check.js +30 -30
- package/cli/utils/models-cache.js +58 -58
- package/cli/utils/models-pricing.js +42 -42
- package/cli/utils/paths.js +82 -82
- package/cli/utils/process.js +3 -3
- package/cli/utils/terminal.js +6 -6
- package/cli/utils/update-manifest.js +75 -75
- package/harness/.opencode/_gitignore +12 -12
- package/harness/.opencode/commands/init.md +5 -5
- package/harness/.opencode/commands/make-architecture.md +5 -5
- package/harness/.opencode/commands/make-design.md +5 -5
- package/harness/.opencode/commands/make-engineer.md +5 -5
- package/harness/.opencode/commands/make-guardrails.md +5 -5
- package/harness/.opencode/commands/make-user-model.md +5 -5
- package/harness/.opencode/commands/ops-backlog.md +10 -10
- package/harness/.opencode/commands/ops-review.md +10 -10
- package/harness/.opencode/commands/ops-ship.md +9 -9
- package/harness/.opencode/commands/plan-apply.md +9 -9
- package/harness/.opencode/commands/plan-archive.md +7 -7
- package/harness/.opencode/commands/plan-explore.md +11 -11
- package/harness/.opencode/commands/plan-goal.md +5 -5
- package/harness/.opencode/commands/plan-propose.md +11 -11
- package/harness/.opencode/commands/plan-quick.md +7 -7
- package/harness/.opencode/commands/plan-story.md +11 -11
- package/harness/.opencode/commands/repo-audit.md +7 -7
- package/harness/.opencode/commands/repo-help.md +5 -5
- package/harness/.opencode/commands/repo-initialize.md +5 -5
- package/harness/.opencode/commands/repo-onboard.md +5 -5
- package/harness/.opencode/commands/repo-verify.md +5 -5
- package/harness/.opencode/plugins/pc-commit-coauthor.js +41 -41
- package/harness/.opencode/plugins/pc-subagent-monitor.js +184 -184
- package/harness/.opencode/plugins/pc-system-reminders.js +424 -424
- package/harness/.opencode/scripts/check-diff.mjs +305 -305
- package/harness/.opencode/scripts/code-rules.mjs +288 -288
- package/harness/.opencode/scripts/platform-contract.mjs +366 -366
- package/harness/AGENTS.md +55 -55
- package/harness/ARCHITECTURE.md +13 -13
- package/harness/DESIGN.md +13 -13
- package/harness/opencode.jsonc +37 -36
- package/harness/openspec/config.yaml +20 -20
- package/harness/skills-lock.json +17 -17
- package/package.json +14 -12
- package/skills/README.md +18 -18
- package/skills/agent-harness-cli/SKILL.md +86 -86
- package/skills/browser-automation/SKILL.md +100 -72
- package/skills/pc-guardrails-generic/SKILL.md +53 -53
- package/skills/pc-guardrails-project/SKILL.md +8 -8
- package/skills/pc-make-architecture/SKILL.md +33 -33
- package/skills/pc-make-architecture/structure-template.md +64 -64
- package/skills/pc-make-design/SKILL.md +45 -45
- package/skills/pc-make-engineer/SKILL.md +59 -59
- package/skills/pc-make-engineer/signal-mapping.md +53 -53
- package/skills/pc-make-engineer/template.md +36 -36
- package/skills/pc-make-guardrails/SKILL.md +44 -44
- package/skills/pc-make-guardrails/category-reference.md +112 -112
- package/skills/pc-make-merge-risk-assess/SKILL.md +70 -70
- package/skills/pc-make-merge-risk-assess/category-reference.md +114 -114
- package/skills/pc-make-user-model/SKILL.md +56 -56
- package/skills/pc-ops-ship/SKILL.md +22 -22
- package/skills/pc-plan-apply/SKILL.md +90 -90
- package/skills/pc-plan-apply/simple-mode.md +21 -21
- package/skills/pc-plan-archive/SKILL.md +66 -66
- package/skills/pc-plan-explore/SKILL.md +21 -21
- package/skills/pc-plan-goal/SKILL.md +91 -91
- package/skills/pc-plan-goal/branching.md +39 -39
- package/skills/pc-plan-goal/failure-policy.md +30 -30
- package/skills/pc-plan-goal/output-mode.md +10 -10
- package/skills/pc-plan-goal/output.md +71 -71
- package/skills/pc-plan-propose/SKILL.md +127 -127
- package/skills/pc-plan-propose/task-annotation.md +39 -39
- package/skills/pc-plan-quick/SKILL.md +46 -46
- package/skills/pc-plan-story/SKILL.md +93 -93
- package/skills/pc-repo-audit/SKILL.md +44 -44
- package/skills/pc-repo-help/SKILL.md +87 -87
- package/skills/pc-repo-initialize/SKILL.md +119 -119
- package/skills/pc-repo-onboard/SKILL.md +32 -32
- package/skills/pc-repo-verify/SKILL.md +150 -150
- package/skills/pc-userstory-az/SKILL.md +71 -71
- package/skills/pc-userstory-browser/SKILL.md +73 -64
- package/skills/pc-userstory-gh/SKILL.md +63 -63
- package/skills/pc-userstory-jira/SKILL.md +74 -74
|
@@ -1,324 +1,359 @@
|
|
|
1
|
-
import fse from 'fs-extra'
|
|
2
|
-
import { parse } from 'jsonc-parser'
|
|
3
|
-
import path from 'node:path'
|
|
4
|
-
import { success } from '../../utils/exec.js'
|
|
5
|
-
|
|
6
|
-
// The harness targets OpenCode 2. This patcher migrates a repository's
|
|
7
|
-
// opencode.jsonc from the V1 shape to the native V2 shape, preserving any keys
|
|
8
|
-
// it does not know about. V2 reads V1 config in memory, but the harness wants a
|
|
9
|
-
// file that is already native so behaviour does not depend on the compatibility
|
|
10
|
-
// layer, and the CI merge step requires comment-free JSON.
|
|
11
|
-
//
|
|
12
|
-
// V1 -> V2 field map this handles:
|
|
13
|
-
// plugin[] -> plugins[]
|
|
14
|
-
// permissions object -> ordered permissions[] rules (bash->shell, task->subagent)
|
|
15
|
-
// agent{} -> agents{} (prompt->system, permission->permissions)
|
|
16
|
-
// skills.paths[] -> skills[]
|
|
17
|
-
// mcp.<name> -> mcp.servers.<name> (enabled -> !disabled)
|
|
18
|
-
// compaction.prune -> removed
|
|
19
|
-
// compaction.reserved -> compaction.buffer
|
|
20
|
-
// experimental.mcp_timeout -> mcp.timeout.{catalog,execution}
|
|
21
|
-
// instructions -> removed (V2 loads AGENTS.md, not this field)
|
|
22
|
-
|
|
23
|
-
const CONFIG_SCHEMA = 'https://opencode.ai/config.json'
|
|
24
|
-
|
|
25
|
-
// Third-party plugins the harness configures, and how they move to V2. The
|
|
26
|
-
// notifier is dropped: V2 has native attention notifications and sounds.
|
|
27
|
-
const DROPPED_PLUGIN_PREFIXES = [
|
|
28
|
-
'@opencode-ai/',
|
|
29
|
-
'@mohak34/opencode-notifier',
|
|
30
|
-
'@different-ai/opencode-browser',
|
|
31
|
-
'@slkiser/opencode-quota',
|
|
32
|
-
]
|
|
33
|
-
|
|
34
|
-
// V1 permission keys -> V2 permission actions.
|
|
35
|
-
const ACTION_RENAMES = {
|
|
36
|
-
bash: 'shell',
|
|
37
|
-
task: 'subagent',
|
|
38
|
-
write: 'edit',
|
|
39
|
-
patch: 'edit',
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function basePackageName(entry) {
|
|
43
|
-
if (typeof entry !== 'string') return ''
|
|
44
|
-
const at = entry.lastIndexOf('@')
|
|
45
|
-
return at > 0 ? entry.slice(0, at) : entry
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// V1 permission keys that are not V2 permission actions. todowrite is a built-in
|
|
49
|
-
// tool allowed by the base policy, so it needs no rule.
|
|
50
|
-
const NON_ACTION_PERMISSION_KEYS = new Set(['todowrite'])
|
|
51
|
-
|
|
52
|
-
function permissionMapToRules(map) {
|
|
53
|
-
const rules = []
|
|
54
|
-
for (const [key, value] of Object.entries(map ?? {})) {
|
|
55
|
-
if (value !== 'allow' && value !== 'deny' && value !== 'ask') continue
|
|
56
|
-
if (NON_ACTION_PERMISSION_KEYS.has(key)) continue
|
|
57
|
-
rules.push({ action: ACTION_RENAMES[key] ?? key, resource: '*', effect: value })
|
|
58
|
-
}
|
|
59
|
-
return rules
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function upsertRules(existing, additions) {
|
|
63
|
-
const merged = [...existing]
|
|
64
|
-
for (const rule of additions) {
|
|
65
|
-
const index = merged.findIndex(r => r.action === rule.action && r.resource === rule.resource)
|
|
66
|
-
if (index === -1) merged.push(rule)
|
|
67
|
-
else merged[index] = rule
|
|
68
|
-
}
|
|
69
|
-
return merged
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
function migratePluginArray(obj) {
|
|
73
|
-
if (!Array.isArray(obj.plugin)) return false
|
|
74
|
-
const plugins = []
|
|
75
|
-
for (const entry of obj.plugin) {
|
|
76
|
-
if (typeof entry !== 'string') {
|
|
77
|
-
plugins.push(entry)
|
|
78
|
-
continue
|
|
79
|
-
}
|
|
80
|
-
const base = basePackageName(entry)
|
|
81
|
-
if (DROPPED_PLUGIN_PREFIXES.some(prefix => base.startsWith(prefix))) continue
|
|
82
|
-
plugins.push(entry)
|
|
83
|
-
}
|
|
84
|
-
obj.plugins = plugins
|
|
85
|
-
delete obj.plugin
|
|
86
|
-
return true
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
function migrateAgents(obj) {
|
|
90
|
-
let changed = false
|
|
91
|
-
const agents = { ...(obj.agents ?? {}) }
|
|
92
|
-
|
|
93
|
-
if (obj.agent && typeof obj.agent === 'object' && !Array.isArray(obj.agent)) {
|
|
94
|
-
for (const [id, raw] of Object.entries(obj.agent)) {
|
|
95
|
-
if (!raw || typeof raw !== 'object') continue
|
|
96
|
-
const agent = { ...raw }
|
|
97
|
-
if (agent.prompt !== undefined) {
|
|
98
|
-
agent.system = agent.prompt
|
|
99
|
-
delete agent.prompt
|
|
100
|
-
}
|
|
101
|
-
if (agent.disable !== undefined) {
|
|
102
|
-
agent.disabled = agent.disable
|
|
103
|
-
delete agent.disable
|
|
104
|
-
}
|
|
105
|
-
if (agent.permission && typeof agent.permission === 'object') {
|
|
106
|
-
agent.permissions = upsertRules(agent.permissions ?? [], permissionMapToRules(agent.permission))
|
|
107
|
-
delete agent.permission
|
|
108
|
-
}
|
|
109
|
-
delete agent.maxSteps
|
|
110
|
-
agents[id] = agent
|
|
111
|
-
}
|
|
112
|
-
delete obj.agent
|
|
113
|
-
changed = true
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
// build and plan are the only primaries; plan is read-only. Guarantee both
|
|
117
|
-
// even when the repo never had an explicit entry.
|
|
118
|
-
const build = { ...(agents.build ?? {}), mode: 'primary' }
|
|
119
|
-
delete build.disabled
|
|
120
|
-
delete build.disable
|
|
121
|
-
if (JSON.stringify(agents.build) !== JSON.stringify(build)) {
|
|
122
|
-
agents.build = build
|
|
123
|
-
changed = true
|
|
124
|
-
}
|
|
125
|
-
const plan = {
|
|
126
|
-
...(agents.plan ?? {}),
|
|
127
|
-
mode: 'primary',
|
|
128
|
-
permissions: upsertRules(agents.plan?.permissions ?? [], [
|
|
129
|
-
{ action: 'edit', resource: '*', effect: 'deny' },
|
|
130
|
-
{ action: 'subagent', resource: '*', effect: 'deny' },
|
|
131
|
-
]),
|
|
132
|
-
}
|
|
133
|
-
delete plan.disabled
|
|
134
|
-
delete plan.disable
|
|
135
|
-
if (JSON.stringify(agents.plan) !== JSON.stringify(plan)) {
|
|
136
|
-
agents.plan = plan
|
|
137
|
-
changed = true
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
if (changed) obj.agents = agents
|
|
141
|
-
return changed
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
function migrateMcp(obj) {
|
|
145
|
-
if (!obj.mcp || typeof obj.mcp !== 'object' || Array.isArray(obj.mcp)) return false
|
|
146
|
-
if (obj.mcp.servers) return false
|
|
147
|
-
const servers = {}
|
|
148
|
-
let inheritedTimeout = 300000
|
|
149
|
-
for (const [name, raw] of Object.entries(obj.mcp)) {
|
|
150
|
-
if (!raw || typeof raw !== 'object') continue
|
|
151
|
-
const server = { ...raw }
|
|
152
|
-
if ('enabled' in server) {
|
|
153
|
-
server.disabled = !server.enabled
|
|
154
|
-
delete server.enabled
|
|
155
|
-
}
|
|
156
|
-
if (typeof server.timeout === 'number') {
|
|
157
|
-
inheritedTimeout = server.timeout
|
|
158
|
-
server.timeout = { catalog: server.timeout, execution: server.timeout }
|
|
159
|
-
}
|
|
160
|
-
servers[name] = server
|
|
161
|
-
}
|
|
162
|
-
obj.mcp = {
|
|
163
|
-
servers,
|
|
164
|
-
timeout: { catalog: inheritedTimeout, execution: inheritedTimeout },
|
|
165
|
-
}
|
|
166
|
-
return true
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
if (
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
}
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
//
|
|
287
|
-
//
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
'
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
}
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
1
|
+
import fse from 'fs-extra'
|
|
2
|
+
import { parse } from 'jsonc-parser'
|
|
3
|
+
import path from 'node:path'
|
|
4
|
+
import { success } from '../../utils/exec.js'
|
|
5
|
+
|
|
6
|
+
// The harness targets OpenCode 2. This patcher migrates a repository's
|
|
7
|
+
// opencode.jsonc from the V1 shape to the native V2 shape, preserving any keys
|
|
8
|
+
// it does not know about. V2 reads V1 config in memory, but the harness wants a
|
|
9
|
+
// file that is already native so behaviour does not depend on the compatibility
|
|
10
|
+
// layer, and the CI merge step requires comment-free JSON.
|
|
11
|
+
//
|
|
12
|
+
// V1 -> V2 field map this handles:
|
|
13
|
+
// plugin[] -> plugins[]
|
|
14
|
+
// permissions object -> ordered permissions[] rules (bash->shell, task->subagent)
|
|
15
|
+
// agent{} -> agents{} (prompt->system, permission->permissions)
|
|
16
|
+
// skills.paths[] -> skills[]
|
|
17
|
+
// mcp.<name> -> mcp.servers.<name> (enabled -> !disabled)
|
|
18
|
+
// compaction.prune -> removed
|
|
19
|
+
// compaction.reserved -> compaction.buffer
|
|
20
|
+
// experimental.mcp_timeout -> mcp.timeout.{catalog,execution}
|
|
21
|
+
// instructions -> removed (V2 loads AGENTS.md, not this field)
|
|
22
|
+
|
|
23
|
+
const CONFIG_SCHEMA = 'https://opencode.ai/config.json'
|
|
24
|
+
|
|
25
|
+
// Third-party plugins the harness configures, and how they move to V2. The
|
|
26
|
+
// notifier is dropped: V2 has native attention notifications and sounds.
|
|
27
|
+
const DROPPED_PLUGIN_PREFIXES = [
|
|
28
|
+
'@opencode-ai/',
|
|
29
|
+
'@mohak34/opencode-notifier',
|
|
30
|
+
'@different-ai/opencode-browser',
|
|
31
|
+
'@slkiser/opencode-quota',
|
|
32
|
+
]
|
|
33
|
+
|
|
34
|
+
// V1 permission keys -> V2 permission actions.
|
|
35
|
+
const ACTION_RENAMES = {
|
|
36
|
+
bash: 'shell',
|
|
37
|
+
task: 'subagent',
|
|
38
|
+
write: 'edit',
|
|
39
|
+
patch: 'edit',
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function basePackageName(entry) {
|
|
43
|
+
if (typeof entry !== 'string') return ''
|
|
44
|
+
const at = entry.lastIndexOf('@')
|
|
45
|
+
return at > 0 ? entry.slice(0, at) : entry
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// V1 permission keys that are not V2 permission actions. todowrite is a built-in
|
|
49
|
+
// tool allowed by the base policy, so it needs no rule.
|
|
50
|
+
const NON_ACTION_PERMISSION_KEYS = new Set(['todowrite'])
|
|
51
|
+
|
|
52
|
+
function permissionMapToRules(map) {
|
|
53
|
+
const rules = []
|
|
54
|
+
for (const [key, value] of Object.entries(map ?? {})) {
|
|
55
|
+
if (value !== 'allow' && value !== 'deny' && value !== 'ask') continue
|
|
56
|
+
if (NON_ACTION_PERMISSION_KEYS.has(key)) continue
|
|
57
|
+
rules.push({ action: ACTION_RENAMES[key] ?? key, resource: '*', effect: value })
|
|
58
|
+
}
|
|
59
|
+
return rules
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function upsertRules(existing, additions) {
|
|
63
|
+
const merged = [...existing]
|
|
64
|
+
for (const rule of additions) {
|
|
65
|
+
const index = merged.findIndex(r => r.action === rule.action && r.resource === rule.resource)
|
|
66
|
+
if (index === -1) merged.push(rule)
|
|
67
|
+
else merged[index] = rule
|
|
68
|
+
}
|
|
69
|
+
return merged
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function migratePluginArray(obj) {
|
|
73
|
+
if (!Array.isArray(obj.plugin)) return false
|
|
74
|
+
const plugins = []
|
|
75
|
+
for (const entry of obj.plugin) {
|
|
76
|
+
if (typeof entry !== 'string') {
|
|
77
|
+
plugins.push(entry)
|
|
78
|
+
continue
|
|
79
|
+
}
|
|
80
|
+
const base = basePackageName(entry)
|
|
81
|
+
if (DROPPED_PLUGIN_PREFIXES.some(prefix => base.startsWith(prefix))) continue
|
|
82
|
+
plugins.push(entry)
|
|
83
|
+
}
|
|
84
|
+
obj.plugins = plugins
|
|
85
|
+
delete obj.plugin
|
|
86
|
+
return true
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function migrateAgents(obj) {
|
|
90
|
+
let changed = false
|
|
91
|
+
const agents = { ...(obj.agents ?? {}) }
|
|
92
|
+
|
|
93
|
+
if (obj.agent && typeof obj.agent === 'object' && !Array.isArray(obj.agent)) {
|
|
94
|
+
for (const [id, raw] of Object.entries(obj.agent)) {
|
|
95
|
+
if (!raw || typeof raw !== 'object') continue
|
|
96
|
+
const agent = { ...raw }
|
|
97
|
+
if (agent.prompt !== undefined) {
|
|
98
|
+
agent.system = agent.prompt
|
|
99
|
+
delete agent.prompt
|
|
100
|
+
}
|
|
101
|
+
if (agent.disable !== undefined) {
|
|
102
|
+
agent.disabled = agent.disable
|
|
103
|
+
delete agent.disable
|
|
104
|
+
}
|
|
105
|
+
if (agent.permission && typeof agent.permission === 'object') {
|
|
106
|
+
agent.permissions = upsertRules(agent.permissions ?? [], permissionMapToRules(agent.permission))
|
|
107
|
+
delete agent.permission
|
|
108
|
+
}
|
|
109
|
+
delete agent.maxSteps
|
|
110
|
+
agents[id] = agent
|
|
111
|
+
}
|
|
112
|
+
delete obj.agent
|
|
113
|
+
changed = true
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// build and plan are the only primaries; plan is read-only. Guarantee both
|
|
117
|
+
// even when the repo never had an explicit entry.
|
|
118
|
+
const build = { ...(agents.build ?? {}), mode: 'primary' }
|
|
119
|
+
delete build.disabled
|
|
120
|
+
delete build.disable
|
|
121
|
+
if (JSON.stringify(agents.build) !== JSON.stringify(build)) {
|
|
122
|
+
agents.build = build
|
|
123
|
+
changed = true
|
|
124
|
+
}
|
|
125
|
+
const plan = {
|
|
126
|
+
...(agents.plan ?? {}),
|
|
127
|
+
mode: 'primary',
|
|
128
|
+
permissions: upsertRules(agents.plan?.permissions ?? [], [
|
|
129
|
+
{ action: 'edit', resource: '*', effect: 'deny' },
|
|
130
|
+
{ action: 'subagent', resource: '*', effect: 'deny' },
|
|
131
|
+
]),
|
|
132
|
+
}
|
|
133
|
+
delete plan.disabled
|
|
134
|
+
delete plan.disable
|
|
135
|
+
if (JSON.stringify(agents.plan) !== JSON.stringify(plan)) {
|
|
136
|
+
agents.plan = plan
|
|
137
|
+
changed = true
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (changed) obj.agents = agents
|
|
141
|
+
return changed
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function migrateMcp(obj) {
|
|
145
|
+
if (!obj.mcp || typeof obj.mcp !== 'object' || Array.isArray(obj.mcp)) return false
|
|
146
|
+
if (obj.mcp.servers) return false
|
|
147
|
+
const servers = {}
|
|
148
|
+
let inheritedTimeout = 300000
|
|
149
|
+
for (const [name, raw] of Object.entries(obj.mcp)) {
|
|
150
|
+
if (!raw || typeof raw !== 'object') continue
|
|
151
|
+
const server = { ...raw }
|
|
152
|
+
if ('enabled' in server) {
|
|
153
|
+
server.disabled = !server.enabled
|
|
154
|
+
delete server.enabled
|
|
155
|
+
}
|
|
156
|
+
if (typeof server.timeout === 'number') {
|
|
157
|
+
inheritedTimeout = server.timeout
|
|
158
|
+
server.timeout = { catalog: server.timeout, execution: server.timeout }
|
|
159
|
+
}
|
|
160
|
+
servers[name] = server
|
|
161
|
+
}
|
|
162
|
+
obj.mcp = {
|
|
163
|
+
servers,
|
|
164
|
+
timeout: { catalog: inheritedTimeout, execution: inheritedTimeout },
|
|
165
|
+
}
|
|
166
|
+
return true
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// Stale MCP server entries that browse replaced. The V2 @different-ai/opencode-browser
|
|
170
|
+
// plugin (and its Chrome extension) was replaced by agent-browser in v2.4, then by browse
|
|
171
|
+
// in v3.10. Both old entries are stripped and the current browse entry is ensured.
|
|
172
|
+
const BROWSE_MCP_SERVER = {
|
|
173
|
+
type: 'local',
|
|
174
|
+
command: ['browse', 'mcp'],
|
|
175
|
+
environment: { OPENCODE_BROWSER_RUNTIME: 'auto-update' },
|
|
176
|
+
disabled: false,
|
|
177
|
+
}
|
|
178
|
+
const STALE_MCP_SERVER_NAMES = ['agent-browser', 'opencode-browser', '@different-ai/opencode-browser']
|
|
179
|
+
|
|
180
|
+
function migrateBrowserMcp(obj) {
|
|
181
|
+
if (!obj.mcp?.servers || typeof obj.mcp.servers !== 'object') return false
|
|
182
|
+
let changed = false
|
|
183
|
+
for (const name of STALE_MCP_SERVER_NAMES) {
|
|
184
|
+
if (name in obj.mcp.servers) {
|
|
185
|
+
delete obj.mcp.servers[name]
|
|
186
|
+
changed = true
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
const current = obj.mcp.servers['browse']
|
|
190
|
+
if (!current) {
|
|
191
|
+
obj.mcp.servers['browse'] = { ...BROWSE_MCP_SERVER }
|
|
192
|
+
changed = true
|
|
193
|
+
} else if (JSON.stringify(current.command) !== JSON.stringify(BROWSE_MCP_SERVER.command)) {
|
|
194
|
+
obj.mcp.servers['browse'] = { ...current, ...BROWSE_MCP_SERVER, environment: current.environment ?? BROWSE_MCP_SERVER.environment }
|
|
195
|
+
changed = true
|
|
196
|
+
} else if (!current.environment?.OPENCODE_BROWSER_RUNTIME) {
|
|
197
|
+
obj.mcp.servers['browse'] = { ...current, environment: { ...(current.environment ?? {}), OPENCODE_BROWSER_RUNTIME: 'auto-update' } }
|
|
198
|
+
changed = true
|
|
199
|
+
}
|
|
200
|
+
return changed
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
/** Mutate `obj` in place to the native V2 shape; returns true when it changed. */
|
|
204
|
+
function migrateToV2(obj) {
|
|
205
|
+
let changed = false
|
|
206
|
+
|
|
207
|
+
if (obj.$schema !== CONFIG_SCHEMA) {
|
|
208
|
+
obj.$schema = CONFIG_SCHEMA
|
|
209
|
+
changed = true
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
if ('instructions' in obj) {
|
|
213
|
+
delete obj.instructions
|
|
214
|
+
changed = true
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (obj.experimental && typeof obj.experimental === 'object') {
|
|
218
|
+
for (const key of ['openTelemetry', 'batch_tool', 'primary_tools', 'continue_loop_on_deny', 'mcp_timeout']) {
|
|
219
|
+
if (key in obj.experimental) {
|
|
220
|
+
delete obj.experimental[key]
|
|
221
|
+
changed = true
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
if (Object.keys(obj.experimental).length === 0) {
|
|
225
|
+
delete obj.experimental
|
|
226
|
+
changed = true
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const compaction = obj.compaction && typeof obj.compaction === 'object' ? { ...obj.compaction } : {}
|
|
231
|
+
if ('prune' in compaction) {
|
|
232
|
+
delete compaction.prune
|
|
233
|
+
changed = true
|
|
234
|
+
}
|
|
235
|
+
if (compaction.auto !== true) {
|
|
236
|
+
compaction.auto = true
|
|
237
|
+
changed = true
|
|
238
|
+
}
|
|
239
|
+
if ('reserved' in compaction) {
|
|
240
|
+
compaction.buffer = compaction.reserved
|
|
241
|
+
delete compaction.reserved
|
|
242
|
+
changed = true
|
|
243
|
+
}
|
|
244
|
+
if (!Number.isFinite(compaction.buffer)) {
|
|
245
|
+
compaction.buffer = 10000
|
|
246
|
+
changed = true
|
|
247
|
+
}
|
|
248
|
+
if (JSON.stringify(obj.compaction) !== JSON.stringify(compaction)) {
|
|
249
|
+
obj.compaction = compaction
|
|
250
|
+
changed = true
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const additions = []
|
|
254
|
+
if (obj.permission && typeof obj.permission === 'object' && !Array.isArray(obj.permission)) {
|
|
255
|
+
additions.push(...permissionMapToRules(obj.permission))
|
|
256
|
+
delete obj.permission
|
|
257
|
+
changed = true
|
|
258
|
+
}
|
|
259
|
+
if (additions.length > 0) {
|
|
260
|
+
const merged = upsertRules(Array.isArray(obj.permissions) ? obj.permissions : [], additions)
|
|
261
|
+
if (JSON.stringify(obj.permissions) !== JSON.stringify(merged)) {
|
|
262
|
+
obj.permissions = merged
|
|
263
|
+
changed = true
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (obj.skills && typeof obj.skills === 'object' && !Array.isArray(obj.skills)) {
|
|
268
|
+
const paths = Array.isArray(obj.skills.paths) ? obj.skills.paths : []
|
|
269
|
+
obj.skills = paths.includes('.agents/skills') ? paths : [...paths, '.agents/skills']
|
|
270
|
+
changed = true
|
|
271
|
+
} else if (Array.isArray(obj.skills)) {
|
|
272
|
+
if (!obj.skills.includes('.agents/skills')) {
|
|
273
|
+
obj.skills = [...obj.skills, '.agents/skills']
|
|
274
|
+
changed = true
|
|
275
|
+
}
|
|
276
|
+
} else {
|
|
277
|
+
obj.skills = ['.agents/skills']
|
|
278
|
+
changed = true
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (migrateAgents(obj)) changed = true
|
|
282
|
+
if (migrateMcp(obj)) changed = true
|
|
283
|
+
if (migrateBrowserMcp(obj)) changed = true
|
|
284
|
+
if (migratePluginArray(obj)) changed = true
|
|
285
|
+
|
|
286
|
+
// A session starts read-only: fullstack-engineer is a subagent now, so a
|
|
287
|
+
// default_agent left pointing at it has no valid primary to open with.
|
|
288
|
+
if (obj.default_agent !== 'plan' && obj.default_agent !== 'build') {
|
|
289
|
+
obj.default_agent = 'plan'
|
|
290
|
+
changed = true
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
return changed
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
export async function patchOpencodeJson(cwd = process.cwd()) {
|
|
297
|
+
const opencodePath = path.join(cwd, 'opencode.jsonc')
|
|
298
|
+
|
|
299
|
+
let text
|
|
300
|
+
if (await fse.pathExists(opencodePath)) {
|
|
301
|
+
text = await fse.readFile(opencodePath, 'utf-8')
|
|
302
|
+
} else {
|
|
303
|
+
text = JSON.stringify({ $schema: CONFIG_SCHEMA }, null, 2)
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
const errors = []
|
|
307
|
+
const parsed = parse(text, errors)
|
|
308
|
+
if (errors.length > 0 || typeof parsed !== 'object' || parsed === null) {
|
|
309
|
+
return { patched: false, reason: 'parse error' }
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
if (!migrateToV2(parsed)) {
|
|
313
|
+
return { patched: false }
|
|
314
|
+
}
|
|
315
|
+
|
|
316
|
+
await fse.writeFile(opencodePath, `${JSON.stringify(parsed, null, 2)}\n`, 'utf-8')
|
|
317
|
+
success('Migrated opencode.jsonc to the native OpenCode 2 shape')
|
|
318
|
+
return { patched: true }
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// OpenCode 2 plugin packages. @opencode/plugin is the V2 plugin API; the V1
|
|
322
|
+
// @opencode-ai/plugin and the retired TUI peers are removed on update.
|
|
323
|
+
const OPENCODE_PACKAGE_DEPENDENCIES = {
|
|
324
|
+
'@opencode/plugin': '2.0.22',
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
const STALE_PACKAGE_DEPENDENCIES = [
|
|
328
|
+
'@opencode-ai/plugin',
|
|
329
|
+
'@mohak34/opencode-notifier',
|
|
330
|
+
'@different-ai/opencode-browser',
|
|
331
|
+
'@opentui/core',
|
|
332
|
+
'@opentui/solid',
|
|
333
|
+
'solid-js',
|
|
334
|
+
]
|
|
335
|
+
|
|
336
|
+
export async function patchOpencodePackage(cwd = process.cwd()) {
|
|
337
|
+
const packagePath = path.join(cwd, '.opencode', 'package.json')
|
|
338
|
+
if (!await fse.pathExists(packagePath)) return { patched: false }
|
|
339
|
+
|
|
340
|
+
const packageJson = await fse.readJson(packagePath).catch(() => null)
|
|
341
|
+
if (!packageJson?.dependencies) return { patched: false }
|
|
342
|
+
|
|
343
|
+
let patched = false
|
|
344
|
+
for (const [name, version] of Object.entries(OPENCODE_PACKAGE_DEPENDENCIES)) {
|
|
345
|
+
if (packageJson.dependencies[name] === version) continue
|
|
346
|
+
packageJson.dependencies[name] = version
|
|
347
|
+
patched = true
|
|
348
|
+
}
|
|
349
|
+
for (const name of STALE_PACKAGE_DEPENDENCIES) {
|
|
350
|
+
if (!(name in packageJson.dependencies)) continue
|
|
351
|
+
delete packageJson.dependencies[name]
|
|
352
|
+
patched = true
|
|
353
|
+
}
|
|
354
|
+
if (!patched) return { patched: false }
|
|
355
|
+
|
|
356
|
+
await fse.writeJson(packagePath, packageJson, { spaces: 2 })
|
|
357
|
+
success('Updated OpenCode plugin dependencies in .opencode/package.json')
|
|
358
|
+
return { patched: true }
|
|
359
|
+
}
|