@plainconceptsplatform/agent-harness 2.7.0 → 3.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.
- package/README.md +12 -6
- package/cli/commands/migrate.js +1 -1
- package/cli/presets/quota.json +6 -2
- package/cli/steps/copy/opencode-json.js +260 -135
- package/cli/steps/copy/skills.js +12 -2
- package/cli/steps/optimization/quota.js +36 -20
- package/cli/utils/copy.js +0 -1
- package/harness/.opencode/_gitignore +9 -9
- package/harness/.opencode/package.json +1 -5
- package/harness/.opencode/plugins/pc-subagent-monitor.js +144 -179
- package/harness/.opencode/plugins/pc-subagent-tiers.js +216 -248
- package/harness/.opencode/plugins/pc-system-reminders.js +386 -434
- package/harness/opencode.jsonc +21 -32
- package/package.json +3 -1
- package/skills/README.md +18 -0
- package/skills/agent-harness-cli/SKILL.md +86 -0
- package/skills/pc-guardrails-generic/SKILL.md +52 -0
- package/{harness/.agents/skills → skills}/pc-make-architecture/SKILL.md +1 -0
- package/skills/pc-make-architecture/structure-template.md +64 -0
- package/{harness/.agents/skills → skills}/pc-make-engineer/SKILL.md +59 -59
- package/{harness/.agents/skills → skills}/pc-make-engineer/template.md +36 -42
- package/{harness/.agents/skills → skills}/pc-make-guardrails/SKILL.md +1 -0
- package/{harness/.agents/skills → skills}/pc-make-guardrails/category-reference.md +40 -1
- package/harness/.agents/skills/pc-guardrails-generic/SKILL.md +0 -47
- package/harness/.agents/skills/pc-make-architecture/structure-template.md +0 -38
- package/harness/.agents/skills/pc-make-evidence-scaffold/SKILL.md +0 -18
- package/harness/.agents/skills/pc-make-evidence-scaffold/evidence-contract.md +0 -29
- package/harness/.opencode/commands/make-evidence-scaffold.md +0 -5
- package/harness/.opencode/tui/pc-subagents.tsx +0 -98
- package/harness/.opencode/tui.json +0 -6
- /package/{harness/.agents/skills → skills}/browser-automation/SKILL.md +0 -0
- /package/{harness/.agents/skills → skills}/pc-guardrails-project/SKILL.md +0 -0
- /package/{harness/.agents/skills → skills}/pc-make-design/SKILL.md +0 -0
- /package/{harness/.agents/skills → skills}/pc-make-engineer/signal-mapping.md +0 -0
- /package/{harness/.agents/skills → skills}/pc-make-merge-risk-assess/SKILL.md +0 -0
- /package/{harness/.agents/skills → skills}/pc-make-merge-risk-assess/category-reference.md +0 -0
- /package/{harness/.agents/skills → skills}/pc-make-user-model/SKILL.md +0 -0
- /package/{harness/.agents/skills → skills}/pc-ops-evidence/SKILL.md +0 -0
- /package/{harness/.agents/skills → skills}/pc-ops-ship/SKILL.md +0 -0
- /package/{harness/.agents/skills → skills}/pc-plan-apply/SKILL.md +0 -0
- /package/{harness/.agents/skills → skills}/pc-plan-apply/simple-mode.md +0 -0
- /package/{harness/.agents/skills → skills}/pc-plan-archive/SKILL.md +0 -0
- /package/{harness/.agents/skills → skills}/pc-plan-explore/SKILL.md +0 -0
- /package/{harness/.agents/skills → skills}/pc-plan-goal/SKILL.md +0 -0
- /package/{harness/.agents/skills → skills}/pc-plan-goal/branching.md +0 -0
- /package/{harness/.agents/skills → skills}/pc-plan-goal/failure-policy.md +0 -0
- /package/{harness/.agents/skills → skills}/pc-plan-goal/output-mode.md +0 -0
- /package/{harness/.agents/skills → skills}/pc-plan-goal/output.md +0 -0
- /package/{harness/.agents/skills → skills}/pc-plan-propose/SKILL.md +0 -0
- /package/{harness/.agents/skills → skills}/pc-plan-propose/task-annotation.md +0 -0
- /package/{harness/.agents/skills → skills}/pc-plan-quick/SKILL.md +0 -0
- /package/{harness/.agents/skills → skills}/pc-plan-story/SKILL.md +0 -0
- /package/{harness/.agents/skills → skills}/pc-repo-audit/SKILL.md +0 -0
- /package/{harness/.agents/skills → skills}/pc-repo-help/SKILL.md +0 -0
- /package/{harness/.agents/skills → skills}/pc-repo-initialize/SKILL.md +0 -0
- /package/{harness/.agents/skills → skills}/pc-repo-onboard/SKILL.md +0 -0
- /package/{harness/.agents/skills → skills}/pc-repo-verify/SKILL.md +0 -0
- /package/{harness/.agents/skills → skills}/pc-userstory-az/SKILL.md +0 -0
- /package/{harness/.agents/skills → skills}/pc-userstory-browser/SKILL.md +0 -0
- /package/{harness/.agents/skills → skills}/pc-userstory-gh/SKILL.md +0 -0
- /package/{harness/.agents/skills → skills}/pc-userstory-jira/SKILL.md +0 -0
package/README.md
CHANGED
|
@@ -284,17 +284,15 @@ your-project/
|
|
|
284
284
|
├── AGENTS.md ← bootstrap mode, replaced after first "/repo-initialize"
|
|
285
285
|
├── ARCHITECTURE.md ← prompt for agents to fill in from your codebase
|
|
286
286
|
├── DESIGN.md ← prompt for agents to fill in from your codebase
|
|
287
|
+
├── opencode.jsonc ← native OpenCode 2 config: agents, permissions, MCP servers
|
|
287
288
|
├── .opencode/
|
|
288
|
-
│ ├── opencode.json ← default model + plugin config
|
|
289
289
|
│ ├── harness.json ← harness config: platform, models, maxConcurrentAgents
|
|
290
290
|
│ ├── harness-managed.json ← hashes of every managed file, so "update" spares your edits
|
|
291
291
|
│ ├── agents/ ← build.md + plan.md (generated primaries), fullstack-engineer,
|
|
292
292
|
│ │ and user-created *-engineer files (all subagents)
|
|
293
|
-
│ ├── tui.json ← registers the Subagents sidebar panel
|
|
294
|
-
│ ├── tui/
|
|
295
|
-
│ │ └── pc-subagents.tsx ← TUI plugin: live Subagents panel in the sidebar
|
|
296
293
|
│ └── plugins/
|
|
297
294
|
│ ├── pc-subagent-monitor.js ← server plugin: subagent state → .opencode/harness-run.json, and the wave cap
|
|
295
|
+
│ ├── pc-subagent-tiers.js ← server plugin: derives tier agents and the two primaries
|
|
298
296
|
│ └── pc-system-reminders.js ← server plugin: gates work on the agent's abilities, denies the never-rules
|
|
299
297
|
└── .agents/
|
|
300
298
|
└── skills/
|
|
@@ -304,6 +302,8 @@ your-project/
|
|
|
304
302
|
└── browser-automation/
|
|
305
303
|
```
|
|
306
304
|
|
|
305
|
+
On OpenCode 2 the terminal client is configured globally (`~/.config/opencode/cli.json`), so the harness ships no project-local `tui.json`; the native session sidebar shows subagents without a sidecar panel.
|
|
306
|
+
|
|
307
307
|
Platform skills ship as suffixed variants (`pc-userstory-gh`, `pc-userstory-az`, `pc-userstory-jira`, `pc-userstory-browser`) and the installer copies only the matching one, renamed to its generic name. Platform operations (ship, review, backlog) are injected directly into the `/ops-*` command files from `cli/fragments/ops-*/` during onboarding. Source-roots metadata lands in `.opencode/source-roots.json`. The always-shipped reminder plugin holds editing, shell and spawning until every ability under the agent's `## Abilities` is loaded, guardrails first, and re-arms after compaction. Token-optimization guidance is injected into `pc-guardrails-generic` marker blocks during onboarding.
|
|
308
308
|
|
|
309
309
|
---
|
|
@@ -348,7 +348,7 @@ Long unattended agent sessions can consume significant tokens. Set these control
|
|
|
348
348
|
|
|
349
349
|
2. **Route models by task type**: use a fast and cheap model (for example `haiku`, `gpt-4o-mini`) for orchestration and status loops; reserve expensive models (for example `sonnet`, `opus`, `gpt-4o`) for implementation tasks only.
|
|
350
350
|
|
|
351
|
-
3. **Install the quota plugin**: the [`@slkiser/opencode-quota`](https://www.npmjs.com/package/@slkiser/opencode-quota) plugin adds `/quota` and `/quota_status` commands that surface real-time token usage inside OpenCode sessions.
|
|
351
|
+
3. **Install the quota plugin**: the [`@slkiser/opencode-quota`](https://www.npmjs.com/package/@slkiser/opencode-quota) plugin adds `/quota` and `/quota_status` commands that surface real-time token usage inside OpenCode sessions. The harness installs it with popups, toasts and reset notifications switched off (`enableToast`, `resetNotifications`), slash reports kept in the chat (`tuiCommandDisplay: "inline"`), and the sidebar panel left on. Settings live in `opencode-quota/quota-toast.jsonc` at the repository root, which is where opencode-quota 5.x reads them.
|
|
352
352
|
|
|
353
353
|
4. **Use `/quota` checkpoints**: run `/quota` before starting any `/plan-apply` session and after each agent wave. Pause at 75 percent consumed; stop at 90 percent.
|
|
354
354
|
|
|
@@ -400,7 +400,13 @@ Data the CLI reads lives in two places, split by kind:
|
|
|
400
400
|
|
|
401
401
|
Every filename the harness writes into a project is declared once in `cli/utils/paths.js`. Change it there, and remember the OpenCode plugins under `harness/.opencode/plugins/` read those same names at runtime.
|
|
402
402
|
|
|
403
|
-
`skills/` holds skills
|
|
403
|
+
`skills/` holds both the installable `pc-*` skills and the `agent-harness-cli` skill for agents operating this CLI. See [skills/README.md](./skills/README.md). All of them ship to npm and are discoverable by `npx skills`, so you can install individual skills into any project without the full harness:
|
|
404
|
+
|
|
405
|
+
```bash
|
|
406
|
+
npx skills add PlainConceptsPlatform/agent-harness
|
|
407
|
+
# or scope to a single skill:
|
|
408
|
+
npx skills add PlainConceptsPlatform/agent-harness --skill pc-ops-ship
|
|
409
|
+
```
|
|
404
410
|
|
|
405
411
|
```bash
|
|
406
412
|
git clone https://github.com/PlainConceptsPlatform/agent-harness.git
|
package/cli/commands/migrate.js
CHANGED
|
@@ -9,7 +9,7 @@ import { CONFIG_FILE, MANIFEST_FILE, OPENCODE_DIR, USER_CONFIG_FILE } from '../u
|
|
|
9
9
|
import { exit } from '../utils/process.js'
|
|
10
10
|
|
|
11
11
|
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
12
|
-
const CONTENT_SKILLS_DIR = path.resolve(__dirname, '../../
|
|
12
|
+
const CONTENT_SKILLS_DIR = path.resolve(__dirname, '../../skills')
|
|
13
13
|
|
|
14
14
|
// State files, old name to new. The run-state file is deleted rather than moved:
|
|
15
15
|
// it is live wave state owned by the monitor plugin and is rebuilt on demand.
|
package/cli/presets/quota.json
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"plugin": "@slkiser/opencode-quota@
|
|
2
|
+
"plugin": "@slkiser/opencode-quota@5.0.1",
|
|
3
3
|
"prompt": {
|
|
4
4
|
"message": "Install opencode-quota with recommended defaults?",
|
|
5
5
|
"default": true,
|
|
@@ -11,6 +11,10 @@
|
|
|
11
11
|
"percentDisplayMode": "used",
|
|
12
12
|
"showSessionTokens": true,
|
|
13
13
|
"enableToast": false,
|
|
14
|
-
"
|
|
14
|
+
"resetNotifications": { "enabled": false },
|
|
15
|
+
"tuiCommandDisplay": "inline",
|
|
16
|
+
"tuiCompactStatus": { "enabled": false },
|
|
17
|
+
"tuiPromptBar": { "enabled": false },
|
|
18
|
+
"tuiSidebarPanel": { "enabled": true }
|
|
15
19
|
}
|
|
16
20
|
}
|
|
@@ -1,178 +1,303 @@
|
|
|
1
1
|
import fse from 'fs-extra'
|
|
2
|
-
import {
|
|
2
|
+
import { parse } from 'jsonc-parser'
|
|
3
3
|
import path from 'node:path'
|
|
4
4
|
import { success } from '../../utils/exec.js'
|
|
5
5
|
|
|
6
|
-
// The
|
|
7
|
-
//
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
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'
|
|
12
24
|
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
const
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
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 OPENCODE_QUOTA_PACKAGE = '@slkiser/opencode-quota'
|
|
28
|
+
const OPENCODE_QUOTA_VERSION = '5.0.1'
|
|
29
|
+
const DROPPED_PLUGIN_PREFIXES = ['@opencode-ai/', '@mohak34/opencode-notifier', '@different-ai/opencode-browser']
|
|
30
|
+
|
|
31
|
+
// V1 permission keys -> V2 permission actions.
|
|
32
|
+
const ACTION_RENAMES = {
|
|
33
|
+
bash: 'shell',
|
|
34
|
+
task: 'subagent',
|
|
35
|
+
write: 'edit',
|
|
36
|
+
patch: 'edit',
|
|
20
37
|
}
|
|
21
38
|
|
|
22
|
-
function
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
return applyEdits(text, edits)
|
|
39
|
+
function basePackageName(entry) {
|
|
40
|
+
if (typeof entry !== 'string') return ''
|
|
41
|
+
const at = entry.lastIndexOf('@')
|
|
42
|
+
return at > 0 ? entry.slice(0, at) : entry
|
|
27
43
|
}
|
|
28
44
|
|
|
29
|
-
|
|
30
|
-
|
|
45
|
+
// V1 permission keys that are not V2 permission actions. todowrite is a built-in
|
|
46
|
+
// tool allowed by the base policy, so it needs no rule.
|
|
47
|
+
const NON_ACTION_PERMISSION_KEYS = new Set(['todowrite'])
|
|
31
48
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
49
|
+
function permissionMapToRules(map) {
|
|
50
|
+
const rules = []
|
|
51
|
+
for (const [key, value] of Object.entries(map ?? {})) {
|
|
52
|
+
if (value !== 'allow' && value !== 'deny' && value !== 'ask') continue
|
|
53
|
+
if (NON_ACTION_PERMISSION_KEYS.has(key)) continue
|
|
54
|
+
rules.push({ action: ACTION_RENAMES[key] ?? key, resource: '*', effect: value })
|
|
37
55
|
}
|
|
56
|
+
return rules
|
|
57
|
+
}
|
|
38
58
|
|
|
39
|
-
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
59
|
+
function upsertRules(existing, additions) {
|
|
60
|
+
const merged = [...existing]
|
|
61
|
+
for (const rule of additions) {
|
|
62
|
+
const index = merged.findIndex(r => r.action === rule.action && r.resource === rule.resource)
|
|
63
|
+
if (index === -1) merged.push(rule)
|
|
64
|
+
else merged[index] = rule
|
|
43
65
|
}
|
|
66
|
+
return merged
|
|
67
|
+
}
|
|
44
68
|
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
// default_agent pointing at fullstack-engineer is now invalid: it became a
|
|
62
|
-
// subagent when build and plan took over as the only primaries, so opencode
|
|
63
|
-
// has no valid default to open a session with. A deliberate choice of build
|
|
64
|
-
// or plan is left alone; anything else is repointed at plan, so a session
|
|
65
|
-
// starts read-only.
|
|
66
|
-
const currentDefault = parsed?.default_agent
|
|
67
|
-
const needsDefaultAgent = currentDefault !== 'plan' && currentDefault !== 'build'
|
|
68
|
-
const needsSkillPermission = parsed?.permission?.skill !== 'allow'
|
|
69
|
-
const missingSharedPermissions = SHARED_PERMISSIONS.filter(
|
|
70
|
-
([key, value]) => parsed?.permission?.[key] !== value,
|
|
71
|
-
)
|
|
72
|
-
// opencode only scans .opencode/skills/ by default, but we install to .agents/skills/
|
|
73
|
-
const needsSkillsPaths = !(
|
|
74
|
-
Array.isArray(parsed?.skills?.paths) &&
|
|
75
|
-
parsed.skills.paths.includes('.agents/skills')
|
|
76
|
-
)
|
|
77
|
-
const needsCompaction = !(
|
|
78
|
-
parsed?.compaction?.auto === true &&
|
|
79
|
-
parsed?.compaction?.prune === true
|
|
80
|
-
)
|
|
81
|
-
const staleBrowserPlugins = (Array.isArray(parsed?.plugin) ? parsed.plugin : [])
|
|
82
|
-
.filter(entry => typeof entry === 'string' && entry.startsWith(STALE_BROWSER_PLUGIN_PREFIX))
|
|
83
|
-
const needsBrowserMcp = JSON.stringify(parsed?.mcp?.['agent-browser']) !== JSON.stringify(AGENT_BROWSER_MCP)
|
|
84
|
-
|
|
85
|
-
if (!needsAgentOverride && !needsDefaultAgent && !needsSkillPermission && missingSharedPermissions.length === 0 && !needsSkillsPaths && !needsCompaction && staleBrowserPlugins.length === 0 && !needsBrowserMcp) {
|
|
86
|
-
return { patched: false }
|
|
69
|
+
function migratePluginArray(obj) {
|
|
70
|
+
if (!Array.isArray(obj.plugin)) return false
|
|
71
|
+
const plugins = []
|
|
72
|
+
for (const entry of obj.plugin) {
|
|
73
|
+
if (typeof entry !== 'string') {
|
|
74
|
+
plugins.push(entry)
|
|
75
|
+
continue
|
|
76
|
+
}
|
|
77
|
+
const base = basePackageName(entry)
|
|
78
|
+
if (DROPPED_PLUGIN_PREFIXES.some(prefix => base.startsWith(prefix))) continue
|
|
79
|
+
if (base === OPENCODE_QUOTA_PACKAGE) {
|
|
80
|
+
plugins.push(`${OPENCODE_QUOTA_PACKAGE}@${OPENCODE_QUOTA_VERSION}`)
|
|
81
|
+
continue
|
|
82
|
+
}
|
|
83
|
+
plugins.push(entry)
|
|
87
84
|
}
|
|
85
|
+
obj.plugins = plugins
|
|
86
|
+
delete obj.plugin
|
|
87
|
+
return true
|
|
88
|
+
}
|
|
88
89
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
90
|
+
function migrateAgents(obj) {
|
|
91
|
+
let changed = false
|
|
92
|
+
const agents = { ...(obj.agents ?? {}) }
|
|
93
|
+
|
|
94
|
+
if (obj.agent && typeof obj.agent === 'object' && !Array.isArray(obj.agent)) {
|
|
95
|
+
for (const [id, raw] of Object.entries(obj.agent)) {
|
|
96
|
+
if (!raw || typeof raw !== 'object') continue
|
|
97
|
+
const agent = { ...raw }
|
|
98
|
+
if (agent.prompt !== undefined) {
|
|
99
|
+
agent.system = agent.prompt
|
|
100
|
+
delete agent.prompt
|
|
101
|
+
}
|
|
102
|
+
if (agent.disable !== undefined) {
|
|
103
|
+
agent.disabled = agent.disable
|
|
104
|
+
delete agent.disable
|
|
105
|
+
}
|
|
106
|
+
if (agent.permission && typeof agent.permission === 'object') {
|
|
107
|
+
agent.permissions = upsertRules(agent.permissions ?? [], permissionMapToRules(agent.permission))
|
|
108
|
+
delete agent.permission
|
|
109
|
+
}
|
|
110
|
+
delete agent.maxSteps
|
|
111
|
+
agents[id] = agent
|
|
94
112
|
}
|
|
95
|
-
|
|
96
|
-
|
|
113
|
+
delete obj.agent
|
|
114
|
+
changed = true
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// build and plan are the only primaries; plan is read-only. Guarantee both
|
|
118
|
+
// even when the repo never had an explicit entry.
|
|
119
|
+
const build = { ...(agents.build ?? {}), mode: 'primary' }
|
|
120
|
+
delete build.disabled
|
|
121
|
+
delete build.disable
|
|
122
|
+
if (JSON.stringify(agents.build) !== JSON.stringify(build)) {
|
|
123
|
+
agents.build = build
|
|
124
|
+
changed = true
|
|
125
|
+
}
|
|
126
|
+
const plan = {
|
|
127
|
+
...(agents.plan ?? {}),
|
|
128
|
+
mode: 'primary',
|
|
129
|
+
permissions: upsertRules(agents.plan?.permissions ?? [], [
|
|
130
|
+
{ action: 'edit', resource: '*', effect: 'deny' },
|
|
131
|
+
{ action: 'subagent', resource: '*', effect: 'deny' },
|
|
132
|
+
]),
|
|
133
|
+
}
|
|
134
|
+
delete plan.disabled
|
|
135
|
+
delete plan.disable
|
|
136
|
+
if (JSON.stringify(agents.plan) !== JSON.stringify(plan)) {
|
|
137
|
+
agents.plan = plan
|
|
138
|
+
changed = true
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (changed) obj.agents = agents
|
|
142
|
+
return changed
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
function migrateMcp(obj) {
|
|
146
|
+
if (!obj.mcp || typeof obj.mcp !== 'object' || Array.isArray(obj.mcp)) return false
|
|
147
|
+
if (obj.mcp.servers) return false
|
|
148
|
+
const servers = {}
|
|
149
|
+
let inheritedTimeout = 300000
|
|
150
|
+
for (const [name, raw] of Object.entries(obj.mcp)) {
|
|
151
|
+
if (!raw || typeof raw !== 'object') continue
|
|
152
|
+
const server = { ...raw }
|
|
153
|
+
if ('enabled' in server) {
|
|
154
|
+
server.disabled = !server.enabled
|
|
155
|
+
delete server.enabled
|
|
97
156
|
}
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
// Read-only has to include spawning: a plan session that can call task()
|
|
102
|
-
// can have a build worker make the change for it.
|
|
103
|
-
text = applyModify(text, ['agent', 'plan', 'permission', 'task'], 'deny')
|
|
104
|
-
}
|
|
105
|
-
if (needsDefaultAgent) {
|
|
106
|
-
text = applyModify(text, ['default_agent'], 'plan')
|
|
107
|
-
}
|
|
108
|
-
if (needsSkillPermission) text = applyModify(text, ['permission', 'skill'], 'allow')
|
|
109
|
-
for (const [key, value] of missingSharedPermissions) {
|
|
110
|
-
text = applyModify(text, ['permission', key], value)
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
// Ensure .agents/skills is in skills.paths so opencode discovers installed skills
|
|
114
|
-
if (needsSkillsPaths) {
|
|
115
|
-
const existingPaths = Array.isArray(parsed?.skills?.paths) ? parsed.skills.paths : []
|
|
116
|
-
if (!existingPaths.includes('.agents/skills')) {
|
|
117
|
-
const updatedPaths = [...existingPaths, '.agents/skills']
|
|
118
|
-
text = applyModify(text, ['skills', 'paths'], updatedPaths)
|
|
157
|
+
if (typeof server.timeout === 'number') {
|
|
158
|
+
inheritedTimeout = server.timeout
|
|
159
|
+
server.timeout = { catalog: server.timeout, execution: server.timeout }
|
|
119
160
|
}
|
|
161
|
+
servers[name] = server
|
|
162
|
+
}
|
|
163
|
+
obj.mcp = {
|
|
164
|
+
servers,
|
|
165
|
+
timeout: { catalog: inheritedTimeout, execution: inheritedTimeout },
|
|
120
166
|
}
|
|
167
|
+
return true
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
/** Mutate `obj` in place to the native V2 shape; returns true when it changed. */
|
|
171
|
+
function migrateToV2(obj) {
|
|
172
|
+
let changed = false
|
|
121
173
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
text = applyModify(text, ['compaction', 'prune'], true)
|
|
126
|
-
text = applyModify(text, ['compaction', 'reserved'], 10000)
|
|
174
|
+
if (obj.$schema !== CONFIG_SCHEMA) {
|
|
175
|
+
obj.$schema = CONFIG_SCHEMA
|
|
176
|
+
changed = true
|
|
127
177
|
}
|
|
128
178
|
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
text = applyModify(text, ['plugin'], keptPlugins)
|
|
179
|
+
if ('instructions' in obj) {
|
|
180
|
+
delete obj.instructions
|
|
181
|
+
changed = true
|
|
133
182
|
}
|
|
134
|
-
|
|
135
|
-
|
|
183
|
+
|
|
184
|
+
if (obj.experimental && typeof obj.experimental === 'object') {
|
|
185
|
+
for (const key of ['openTelemetry', 'batch_tool', 'primary_tools', 'continue_loop_on_deny', 'mcp_timeout']) {
|
|
186
|
+
if (key in obj.experimental) {
|
|
187
|
+
delete obj.experimental[key]
|
|
188
|
+
changed = true
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
if (Object.keys(obj.experimental).length === 0) {
|
|
192
|
+
delete obj.experimental
|
|
193
|
+
changed = true
|
|
194
|
+
}
|
|
136
195
|
}
|
|
137
196
|
|
|
138
|
-
|
|
139
|
-
if (
|
|
140
|
-
|
|
197
|
+
const compaction = obj.compaction && typeof obj.compaction === 'object' ? { ...obj.compaction } : {}
|
|
198
|
+
if ('prune' in compaction) {
|
|
199
|
+
delete compaction.prune
|
|
200
|
+
changed = true
|
|
201
|
+
}
|
|
202
|
+
if (compaction.auto !== true) {
|
|
203
|
+
compaction.auto = true
|
|
204
|
+
changed = true
|
|
205
|
+
}
|
|
206
|
+
if ('reserved' in compaction) {
|
|
207
|
+
compaction.buffer = compaction.reserved
|
|
208
|
+
delete compaction.reserved
|
|
209
|
+
changed = true
|
|
141
210
|
}
|
|
142
|
-
if (
|
|
143
|
-
|
|
211
|
+
if (!Number.isFinite(compaction.buffer)) {
|
|
212
|
+
compaction.buffer = 10000
|
|
213
|
+
changed = true
|
|
144
214
|
}
|
|
145
|
-
if (
|
|
146
|
-
|
|
215
|
+
if (JSON.stringify(obj.compaction) !== JSON.stringify(compaction)) {
|
|
216
|
+
obj.compaction = compaction
|
|
217
|
+
changed = true
|
|
147
218
|
}
|
|
148
|
-
|
|
149
|
-
|
|
219
|
+
|
|
220
|
+
const additions = []
|
|
221
|
+
if (obj.permission && typeof obj.permission === 'object' && !Array.isArray(obj.permission)) {
|
|
222
|
+
additions.push(...permissionMapToRules(obj.permission))
|
|
223
|
+
delete obj.permission
|
|
224
|
+
changed = true
|
|
150
225
|
}
|
|
151
|
-
if (
|
|
152
|
-
|
|
226
|
+
if (additions.length > 0) {
|
|
227
|
+
const merged = upsertRules(Array.isArray(obj.permissions) ? obj.permissions : [], additions)
|
|
228
|
+
if (JSON.stringify(obj.permissions) !== JSON.stringify(merged)) {
|
|
229
|
+
obj.permissions = merged
|
|
230
|
+
changed = true
|
|
231
|
+
}
|
|
153
232
|
}
|
|
154
|
-
|
|
155
|
-
|
|
233
|
+
|
|
234
|
+
if (obj.skills && typeof obj.skills === 'object' && !Array.isArray(obj.skills)) {
|
|
235
|
+
const paths = Array.isArray(obj.skills.paths) ? obj.skills.paths : []
|
|
236
|
+
obj.skills = paths.includes('.agents/skills') ? paths : [...paths, '.agents/skills']
|
|
237
|
+
changed = true
|
|
238
|
+
} else if (Array.isArray(obj.skills)) {
|
|
239
|
+
if (!obj.skills.includes('.agents/skills')) {
|
|
240
|
+
obj.skills = [...obj.skills, '.agents/skills']
|
|
241
|
+
changed = true
|
|
242
|
+
}
|
|
243
|
+
} else {
|
|
244
|
+
obj.skills = ['.agents/skills']
|
|
245
|
+
changed = true
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
if (migrateAgents(obj)) changed = true
|
|
249
|
+
if (migrateMcp(obj)) changed = true
|
|
250
|
+
if (migratePluginArray(obj)) changed = true
|
|
251
|
+
|
|
252
|
+
// A session starts read-only: fullstack-engineer is a subagent now, so a
|
|
253
|
+
// default_agent left pointing at it has no valid primary to open with.
|
|
254
|
+
if (obj.default_agent !== 'plan' && obj.default_agent !== 'build') {
|
|
255
|
+
obj.default_agent = 'plan'
|
|
256
|
+
changed = true
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
return changed
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export async function patchOpencodeJson(cwd = process.cwd()) {
|
|
263
|
+
const opencodePath = path.join(cwd, 'opencode.jsonc')
|
|
264
|
+
|
|
265
|
+
let text
|
|
266
|
+
if (await fse.pathExists(opencodePath)) {
|
|
267
|
+
text = await fse.readFile(opencodePath, 'utf-8')
|
|
268
|
+
} else {
|
|
269
|
+
text = JSON.stringify({ $schema: CONFIG_SCHEMA }, null, 2)
|
|
156
270
|
}
|
|
157
|
-
|
|
158
|
-
|
|
271
|
+
|
|
272
|
+
const errors = []
|
|
273
|
+
const parsed = parse(text, errors)
|
|
274
|
+
if (errors.length > 0 || typeof parsed !== 'object' || parsed === null) {
|
|
275
|
+
return { patched: false, reason: 'parse error' }
|
|
159
276
|
}
|
|
160
|
-
|
|
161
|
-
|
|
277
|
+
|
|
278
|
+
if (!migrateToV2(parsed)) {
|
|
279
|
+
return { patched: false }
|
|
162
280
|
}
|
|
163
281
|
|
|
282
|
+
await fse.writeFile(opencodePath, `${JSON.stringify(parsed, null, 2)}\n`, 'utf-8')
|
|
283
|
+
success('Migrated opencode.jsonc to the native OpenCode 2 shape')
|
|
164
284
|
return { patched: true }
|
|
165
285
|
}
|
|
166
286
|
|
|
287
|
+
// OpenCode 2 plugin packages. @opencode/plugin is the V2 plugin API; the V1
|
|
288
|
+
// @opencode-ai/plugin and the retired TUI peers are removed on update.
|
|
167
289
|
const OPENCODE_PACKAGE_DEPENDENCIES = {
|
|
168
|
-
'@opencode
|
|
169
|
-
'@opentui/core': '0.5.6',
|
|
170
|
-
'@opentui/solid': '0.5.6',
|
|
171
|
-
'solid-js': '1.9.12',
|
|
290
|
+
'@opencode/plugin': '2.0.22',
|
|
172
291
|
}
|
|
173
292
|
|
|
174
|
-
|
|
175
|
-
|
|
293
|
+
const STALE_PACKAGE_DEPENDENCIES = [
|
|
294
|
+
'@opencode-ai/plugin',
|
|
295
|
+
'@mohak34/opencode-notifier',
|
|
296
|
+
'@different-ai/opencode-browser',
|
|
297
|
+
'@opentui/core',
|
|
298
|
+
'@opentui/solid',
|
|
299
|
+
'solid-js',
|
|
300
|
+
]
|
|
176
301
|
|
|
177
302
|
export async function patchOpencodePackage(cwd = process.cwd()) {
|
|
178
303
|
const packagePath = path.join(cwd, '.opencode', 'package.json')
|
|
@@ -183,7 +308,7 @@ export async function patchOpencodePackage(cwd = process.cwd()) {
|
|
|
183
308
|
|
|
184
309
|
let patched = false
|
|
185
310
|
for (const [name, version] of Object.entries(OPENCODE_PACKAGE_DEPENDENCIES)) {
|
|
186
|
-
if (
|
|
311
|
+
if (packageJson.dependencies[name] === version) continue
|
|
187
312
|
packageJson.dependencies[name] = version
|
|
188
313
|
patched = true
|
|
189
314
|
}
|
package/cli/steps/copy/skills.js
CHANGED
|
@@ -5,7 +5,7 @@ import { info, success } from '../../utils/exec.js'
|
|
|
5
5
|
import { canUpdateManagedFile, readUpdateManifest, recordManagedFile, writeUpdateManifest } from '../../utils/update-manifest.js'
|
|
6
6
|
|
|
7
7
|
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
8
|
-
const CONTENT_SKILLS_DIR = path.resolve(__dirname, '../../../
|
|
8
|
+
const CONTENT_SKILLS_DIR = path.resolve(__dirname, '../../../skills')
|
|
9
9
|
const CONTENT_SKILLS_LOCK = path.resolve(__dirname, '../../../harness/skills-lock.json')
|
|
10
10
|
|
|
11
11
|
// Userstory skills parse backlog work items: selected by backlogPlatform only.
|
|
@@ -28,7 +28,16 @@ export const SKILL_RENAME = {
|
|
|
28
28
|
'pc-userstory-browser': 'pc-userstory',
|
|
29
29
|
}
|
|
30
30
|
|
|
31
|
+
// Skills that ship in the source tree but must never be copied into a target
|
|
32
|
+
// project. agent-harness-cli documents the CLI itself for agents working on
|
|
33
|
+
// THIS repo; it has no value in a consumer repo and would clutter .agents/skills.
|
|
34
|
+
// Listed by directory name as read from CONTENT_SKILLS_DIR.
|
|
35
|
+
export const NON_INSTALLABLE_SKILLS = new Set([
|
|
36
|
+
'agent-harness-cli',
|
|
37
|
+
])
|
|
38
|
+
|
|
31
39
|
function shouldInstallSkill(skill, backlogPlatform, _repoPlatform) {
|
|
40
|
+
if (NON_INSTALLABLE_SKILLS.has(skill)) return false
|
|
32
41
|
if (skill in BACKLOG_PLATFORM_SKILLS) return BACKLOG_PLATFORM_SKILLS[skill] === backlogPlatform
|
|
33
42
|
return true
|
|
34
43
|
}
|
|
@@ -185,7 +194,8 @@ async function installObSkills(backlogPlatform = 'github', repoPlatform, { force
|
|
|
185
194
|
// Build the set of skill names we ship (source dirs, after rename).
|
|
186
195
|
// Only these may be removed during forceOverwrite — project-generated
|
|
187
196
|
// skills (pc-merge-risk-assess, custom loops, etc.) must survive.
|
|
188
|
-
const contentSkills = await fse.readdir(CONTENT_SKILLS_DIR)
|
|
197
|
+
const contentSkills = (await fse.readdir(CONTENT_SKILLS_DIR))
|
|
198
|
+
.filter(name => !NON_INSTALLABLE_SKILLS.has(name))
|
|
189
199
|
const shippedNames = new Set()
|
|
190
200
|
for (const skill of contentSkills) {
|
|
191
201
|
const stat = await fse.stat(path.join(CONTENT_SKILLS_DIR, skill)).catch(() => null)
|