@plainconceptsplatform/agent-harness 2.3.0 → 2.4.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (155) hide show
  1. package/README.md +23 -12
  2. package/{src → cli}/commands/join.js +3 -4
  3. package/{src → cli}/commands/migrate.js +1 -1
  4. package/{src → cli}/commands/wizard.js +1 -1
  5. package/{src → cli}/index.js +1 -1
  6. package/{src → cli}/presets/agents-content.json +4 -4
  7. package/cli/presets/browser.json +10 -0
  8. package/{src → cli}/presets/clean.json +21 -21
  9. package/{src → cli}/presets/optimization.json +37 -37
  10. package/{src → cli}/presets/platforms.json +76 -76
  11. package/{src → cli}/presets/quota.json +16 -16
  12. package/{src → cli}/presets/source.json +23 -23
  13. package/cli/steps/browser/index.js +48 -0
  14. package/{src → cli}/steps/clean/index.js +120 -120
  15. package/{src → cli}/steps/copy/agents.js +119 -119
  16. package/{src → cli}/steps/copy/commands.js +95 -95
  17. package/{src → cli}/steps/copy/fullstack-engineer.js +89 -88
  18. package/{src → cli}/steps/copy/index.js +1 -1
  19. package/{src → cli}/steps/copy/opencode-json.js +196 -161
  20. package/{src → cli}/steps/copy/skills.js +2 -2
  21. package/{src → cli}/steps/models/format.js +88 -88
  22. package/{src → cli}/steps/models/index.js +64 -64
  23. package/{src → cli}/steps/openspec/index.js +136 -136
  24. package/{src → cli}/steps/optimization/codegraph.js +127 -127
  25. package/{src → cli}/steps/optimization/detect.js +66 -66
  26. package/{src → cli}/steps/optimization/humanizer.js +17 -17
  27. package/{src → cli}/steps/optimization/index.js +163 -163
  28. package/{src → cli}/steps/optimization/memory.js +88 -88
  29. package/{src → cli}/steps/optimization/patch-guardrails.js +109 -109
  30. package/{src → cli}/steps/optimization/quota.js +119 -119
  31. package/{src → cli}/steps/optimization/simple-english.js +17 -17
  32. package/{src → cli}/steps/optimization/skills-lock.js +30 -30
  33. package/{src → cli}/steps/platform/index.js +109 -109
  34. package/{src → cli}/steps/source/index.js +123 -123
  35. package/{src → cli}/utils/agent-color.js +111 -83
  36. package/{src → cli}/utils/exec-spinner.js +47 -47
  37. package/{src → cli}/utils/exec.js +134 -134
  38. package/{src → cli}/utils/models-pricing.js +42 -42
  39. package/{src → cli}/utils/paths.js +1 -1
  40. package/{src → cli}/utils/process.js +3 -3
  41. package/{src → cli}/utils/terminal.js +6 -6
  42. package/harness/.agents/skills/browser-automation/SKILL.md +72 -0
  43. package/{src/content → harness}/.agents/skills/pc-make-engineer/SKILL.md +219 -219
  44. package/{src/content → harness}/.agents/skills/pc-make-engineer/template.md +80 -80
  45. package/{src/content → harness}/.agents/skills/pc-plan-apply/simple-mode.md +1 -1
  46. package/{src/content → harness}/.agents/skills/pc-plan-archive/SKILL.md +3 -0
  47. package/{src/content → harness}/.agents/skills/pc-plan-explore/SKILL.md +3 -0
  48. package/{src/content → harness}/.agents/skills/pc-plan-goal/SKILL.md +11 -20
  49. package/{src/content → harness}/.agents/skills/pc-plan-goal/branching.md +1 -1
  50. package/{src/content → harness}/.agents/skills/pc-plan-goal/output.md +5 -8
  51. package/{src/content → harness}/.agents/skills/pc-plan-story/SKILL.md +3 -0
  52. package/{src/content → harness}/.agents/skills/pc-userstory-browser/SKILL.md +136 -132
  53. package/{src/content → harness}/.opencode/package.json +9 -10
  54. package/{src/content → harness}/.opencode/plugins/pc-subagent-tiers.js +368 -337
  55. package/{src/content → harness}/ARCHITECTURE.md +16 -16
  56. package/{src/content → harness}/DESIGN.md +16 -16
  57. package/{src/content → harness}/opencode.jsonc +47 -41
  58. package/{src/content → harness}/openspec/config.yaml +20 -20
  59. package/{src/content → harness}/skills-lock.json +17 -17
  60. package/package.json +7 -7
  61. package/src/content/.agents/skills/browser-automation/SKILL.md +0 -66
  62. package/src/presets/browser.json +0 -22
  63. package/src/steps/browser/index.js +0 -91
  64. /package/{src → cli}/commands/shared.js +0 -0
  65. /package/{src → cli}/commands/single.js +0 -0
  66. /package/{src → cli}/commands/update.js +0 -0
  67. /package/{src → cli}/fragments/archive/az.md +0 -0
  68. /package/{src → cli}/fragments/archive/gh.md +0 -0
  69. /package/{src → cli}/fragments/archive/gl.md +0 -0
  70. /package/{src → cli}/fragments/archive/none.md +0 -0
  71. /package/{src → cli}/fragments/guardrails/codegraph.md +0 -0
  72. /package/{src → cli}/fragments/guardrails/humanizer.md +0 -0
  73. /package/{src → cli}/fragments/guardrails/memory.md +0 -0
  74. /package/{src → cli}/fragments/guardrails/rtk.md +0 -0
  75. /package/{src → cli}/fragments/guardrails/simple-english.md +0 -0
  76. /package/{src → cli}/fragments/ops-backlog/az.md +0 -0
  77. /package/{src → cli}/fragments/ops-backlog/gh.md +0 -0
  78. /package/{src → cli}/fragments/ops-backlog/jira.md +0 -0
  79. /package/{src → cli}/fragments/ops-evidence/az.md +0 -0
  80. /package/{src → cli}/fragments/ops-evidence/gh.md +0 -0
  81. /package/{src → cli}/fragments/ops-evidence/jira.md +0 -0
  82. /package/{src → cli}/fragments/ops-review/az.md +0 -0
  83. /package/{src → cli}/fragments/ops-review/gh.md +0 -0
  84. /package/{src → cli}/fragments/ops-review/gl.md +0 -0
  85. /package/{src → cli}/fragments/ops-ship/az.md +0 -0
  86. /package/{src → cli}/fragments/ops-ship/gh.md +0 -0
  87. /package/{src → cli}/fragments/ops-ship/gl.md +0 -0
  88. /package/{src → cli}/presets/models.json +0 -0
  89. /package/{src → cli}/presets/openspec.json +0 -0
  90. /package/{src → cli}/steps/metadata/index.js +0 -0
  91. /package/{src → cli}/steps/models/write.js +0 -0
  92. /package/{src → cli}/utils/copy.js +0 -0
  93. /package/{src → cli}/utils/legacy-check.js +0 -0
  94. /package/{src → cli}/utils/models-cache.js +0 -0
  95. /package/{src → cli}/utils/update-manifest.js +0 -0
  96. /package/{src/content → harness}/.agents/skills/pc-guardrails-generic/SKILL.md +0 -0
  97. /package/{src/content → harness}/.agents/skills/pc-guardrails-project/SKILL.md +0 -0
  98. /package/{src/content → harness}/.agents/skills/pc-make-architecture/SKILL.md +0 -0
  99. /package/{src/content → harness}/.agents/skills/pc-make-architecture/structure-template.md +0 -0
  100. /package/{src/content → harness}/.agents/skills/pc-make-design/SKILL.md +0 -0
  101. /package/{src/content → harness}/.agents/skills/pc-make-engineer/signal-mapping.md +0 -0
  102. /package/{src/content → harness}/.agents/skills/pc-make-evidence-scaffold/SKILL.md +0 -0
  103. /package/{src/content → harness}/.agents/skills/pc-make-evidence-scaffold/evidence-contract.md +0 -0
  104. /package/{src/content → harness}/.agents/skills/pc-make-guardrails/SKILL.md +0 -0
  105. /package/{src/content → harness}/.agents/skills/pc-make-guardrails/category-reference.md +0 -0
  106. /package/{src/content → harness}/.agents/skills/pc-make-merge-risk-assess/SKILL.md +0 -0
  107. /package/{src/content → harness}/.agents/skills/pc-make-merge-risk-assess/category-reference.md +0 -0
  108. /package/{src/content → harness}/.agents/skills/pc-make-user-model/SKILL.md +0 -0
  109. /package/{src/content → harness}/.agents/skills/pc-ops-evidence/SKILL.md +0 -0
  110. /package/{src/content → harness}/.agents/skills/pc-ops-ship/SKILL.md +0 -0
  111. /package/{src/content → harness}/.agents/skills/pc-plan-apply/SKILL.md +0 -0
  112. /package/{src/content → harness}/.agents/skills/pc-plan-goal/failure-policy.md +0 -0
  113. /package/{src/content → harness}/.agents/skills/pc-plan-goal/output-mode.md +0 -0
  114. /package/{src/content → harness}/.agents/skills/pc-plan-propose/SKILL.md +0 -0
  115. /package/{src/content → harness}/.agents/skills/pc-plan-propose/task-annotation.md +0 -0
  116. /package/{src/content → harness}/.agents/skills/pc-plan-quick/SKILL.md +0 -0
  117. /package/{src/content → harness}/.agents/skills/pc-repo-audit/SKILL.md +0 -0
  118. /package/{src/content → harness}/.agents/skills/pc-repo-help/SKILL.md +0 -0
  119. /package/{src/content → harness}/.agents/skills/pc-repo-initialize/SKILL.md +0 -0
  120. /package/{src/content → harness}/.agents/skills/pc-repo-onboard/SKILL.md +0 -0
  121. /package/{src/content → harness}/.agents/skills/pc-repo-verify/SKILL.md +0 -0
  122. /package/{src/content → harness}/.agents/skills/pc-userstory-az/SKILL.md +0 -0
  123. /package/{src/content → harness}/.agents/skills/pc-userstory-gh/SKILL.md +0 -0
  124. /package/{src/content → harness}/.agents/skills/pc-userstory-jira/SKILL.md +0 -0
  125. /package/{src/content → harness}/.opencode/_gitignore +0 -0
  126. /package/{src/content → harness}/.opencode/commands/init.md +0 -0
  127. /package/{src/content → harness}/.opencode/commands/make-architecture.md +0 -0
  128. /package/{src/content → harness}/.opencode/commands/make-design.md +0 -0
  129. /package/{src/content → harness}/.opencode/commands/make-engineer.md +0 -0
  130. /package/{src/content → harness}/.opencode/commands/make-evidence-scaffold.md +0 -0
  131. /package/{src/content → harness}/.opencode/commands/make-guardrails.md +0 -0
  132. /package/{src/content → harness}/.opencode/commands/make-user-model.md +0 -0
  133. /package/{src/content → harness}/.opencode/commands/ops-backlog.md +0 -0
  134. /package/{src/content → harness}/.opencode/commands/ops-evidence.md +0 -0
  135. /package/{src/content → harness}/.opencode/commands/ops-review.md +0 -0
  136. /package/{src/content → harness}/.opencode/commands/ops-ship.md +0 -0
  137. /package/{src/content → harness}/.opencode/commands/plan-apply.md +0 -0
  138. /package/{src/content → harness}/.opencode/commands/plan-archive.md +0 -0
  139. /package/{src/content → harness}/.opencode/commands/plan-explore.md +0 -0
  140. /package/{src/content → harness}/.opencode/commands/plan-goal.md +0 -0
  141. /package/{src/content → harness}/.opencode/commands/plan-propose.md +0 -0
  142. /package/{src/content → harness}/.opencode/commands/plan-quick.md +0 -0
  143. /package/{src/content → harness}/.opencode/commands/plan-story.md +0 -0
  144. /package/{src/content → harness}/.opencode/commands/repo-audit.md +0 -0
  145. /package/{src/content → harness}/.opencode/commands/repo-help.md +0 -0
  146. /package/{src/content → harness}/.opencode/commands/repo-initialize.md +0 -0
  147. /package/{src/content → harness}/.opencode/commands/repo-onboard.md +0 -0
  148. /package/{src/content → harness}/.opencode/commands/repo-verify.md +0 -0
  149. /package/{src/content → harness}/.opencode/plugins/pc-subagent-monitor.js +0 -0
  150. /package/{src/content → harness}/.opencode/plugins/pc-system-reminders.js +0 -0
  151. /package/{src/content → harness}/.opencode/tui/pc-subagents.tsx +0 -0
  152. /package/{src/content → harness}/.opencode/tui.json +0 -0
  153. /package/{src/content → harness}/AGENTS.md +0 -0
  154. /package/{src/content → harness}/openspec/changes/archive/.gitkeep +0 -0
  155. /package/{src/content → harness}/openspec/specs/.gitkeep +0 -0
@@ -1,161 +1,196 @@
1
- import fse from 'fs-extra'
2
- import { applyEdits, modify, parse } from 'jsonc-parser'
3
- import path from 'node:path'
4
- import { success } from '../../utils/exec.js'
5
-
6
- // The reminder plugin loads every agent ability before work, including
7
- // user-installed skills outside the pc-* and openspec-* namespaces.
8
- const SHARED_PERMISSIONS = [
9
- ['question', 'allow'],
10
- ['todowrite', 'allow'],
11
- ]
12
-
13
- function applyModify(text, jsonPath, value) {
14
- const edits = modify(text, jsonPath, value, {
15
- formattingOptions: { insertSpaces: true, tabSize: 2 },
16
- })
17
- return applyEdits(text, edits)
18
- }
19
-
20
- export async function patchOpencodeJson(cwd = process.cwd()) {
21
- const opencodePath = path.join(cwd, 'opencode.jsonc')
22
-
23
- let text
24
- if (await fse.pathExists(opencodePath)) {
25
- text = await fse.readFile(opencodePath, 'utf-8')
26
- } else {
27
- text = JSON.stringify({ $schema: 'https://opencode.ai/config.json' }, null, 2)
28
- }
29
-
30
- const errors = []
31
- const parsed = parse(text, errors)
32
- if (errors.length > 0 || typeof parsed !== 'object') {
33
- return { patched: false, reason: 'parse error' }
34
- }
35
-
36
- // build and plan are overridden, not disabled. Earlier versions disabled them
37
- // and pointed default_agent at fullstack-engineer; now they are the only two
38
- // primaries and pc-subagent-tiers regenerates their agent files from
39
- // fullstack-engineer.md. A repo patched by an earlier version still carries
40
- // `disable: true`, which would hide both agents, so that flag is cleared here.
41
- const hasStaleDisable = (
42
- parsed?.agent?.build?.disable !== undefined ||
43
- parsed?.agent?.plan?.disable !== undefined
44
- )
45
- const needsAgentOverride = hasStaleDisable || !(
46
- parsed?.agent?.build?.mode === 'primary' &&
47
- parsed?.agent?.plan?.mode === 'primary' &&
48
- parsed?.agent?.plan?.permission?.edit === 'deny'
49
- )
50
-
51
- // default_agent pointing at fullstack-engineer is now invalid: it became a
52
- // subagent when build and plan took over as the only primaries, so opencode
53
- // has no valid default to open a session with. A deliberate choice of build
54
- // or plan is left alone; anything else is repointed at plan, so a session
55
- // starts read-only.
56
- const currentDefault = parsed?.default_agent
57
- const needsDefaultAgent = currentDefault !== 'plan' && currentDefault !== 'build'
58
- const needsSkillPermission = parsed?.permission?.skill !== 'allow'
59
- const missingSharedPermissions = SHARED_PERMISSIONS.filter(
60
- ([key, value]) => parsed?.permission?.[key] !== value,
61
- )
62
- // opencode only scans .opencode/skills/ by default, but we install to .agents/skills/
63
- const needsSkillsPaths = !(
64
- Array.isArray(parsed?.skills?.paths) &&
65
- parsed.skills.paths.includes('.agents/skills')
66
- )
67
- const needsCompaction = !(
68
- parsed?.compaction?.auto === true &&
69
- parsed?.compaction?.prune === true
70
- )
71
-
72
- if (!needsAgentOverride && !needsDefaultAgent && !needsSkillPermission && missingSharedPermissions.length === 0 && !needsSkillsPaths && !needsCompaction) {
73
- return { patched: false }
74
- }
75
-
76
- // Apply edits sequentially so offsets stay correct
77
- if (needsAgentOverride) {
78
- // undefined removes the key, clearing the disable left by earlier versions.
79
- if (parsed?.agent?.build?.disable !== undefined) {
80
- text = applyModify(text, ['agent', 'build', 'disable'], undefined)
81
- }
82
- if (parsed?.agent?.plan?.disable !== undefined) {
83
- text = applyModify(text, ['agent', 'plan', 'disable'], undefined)
84
- }
85
- text = applyModify(text, ['agent', 'build', 'mode'], 'primary')
86
- text = applyModify(text, ['agent', 'plan', 'mode'], 'primary')
87
- text = applyModify(text, ['agent', 'plan', 'permission', 'edit'], 'deny')
88
- }
89
- if (needsDefaultAgent) {
90
- text = applyModify(text, ['default_agent'], 'plan')
91
- }
92
- if (needsSkillPermission) text = applyModify(text, ['permission', 'skill'], 'allow')
93
- for (const [key, value] of missingSharedPermissions) {
94
- text = applyModify(text, ['permission', key], value)
95
- }
96
-
97
- // Ensure .agents/skills is in skills.paths so opencode discovers installed skills
98
- if (needsSkillsPaths) {
99
- const existingPaths = Array.isArray(parsed?.skills?.paths) ? parsed.skills.paths : []
100
- if (!existingPaths.includes('.agents/skills')) {
101
- const updatedPaths = [...existingPaths, '.agents/skills']
102
- text = applyModify(text, ['skills', 'paths'], updatedPaths)
103
- }
104
- }
105
-
106
- // Enable context pruning to reduce token costs in long agent sessions
107
- if (needsCompaction) {
108
- text = applyModify(text, ['compaction', 'auto'], true)
109
- text = applyModify(text, ['compaction', 'prune'], true)
110
- text = applyModify(text, ['compaction', 'reserved'], 10000)
111
- }
112
-
113
- await fse.writeFile(opencodePath, text, 'utf-8')
114
- if (needsAgentOverride) {
115
- success('Set build/plan as the primary agents in opencode.jsonc (plan is read-only)')
116
- }
117
- if (needsDefaultAgent) {
118
- success(`default_agent -> plan${currentDefault ? ` (was ${currentDefault})` : ''}`)
119
- }
120
- if (needsSkillPermission) {
121
- success('Allowed skill loading in opencode.jsonc')
122
- }
123
- if (missingSharedPermissions.length > 0) {
124
- success('Allowed question/todowrite tools in opencode.jsonc')
125
- }
126
- if (needsSkillsPaths) {
127
- success('Added .agents/skills to skills.paths in opencode.jsonc')
128
- }
129
- if (needsCompaction) {
130
- success('Enabled context pruning (compaction.prune) in opencode.jsonc')
131
- }
132
-
133
- return { patched: true }
134
- }
135
-
136
- const OPENCODE_PACKAGE_DEPENDENCIES = {
137
- '@opencode-ai/plugin': '1.18.19',
138
- '@opentui/core': '0.5.6',
139
- '@opentui/solid': '0.5.6',
140
- 'solid-js': '1.9.12',
141
- }
142
-
143
- export async function patchOpencodePackage(cwd = process.cwd()) {
144
- const packagePath = path.join(cwd, '.opencode', 'package.json')
145
- if (!await fse.pathExists(packagePath)) return { patched: false }
146
-
147
- const packageJson = await fse.readJson(packagePath).catch(() => null)
148
- if (!packageJson?.dependencies) return { patched: false }
149
-
150
- let patched = false
151
- for (const [name, version] of Object.entries(OPENCODE_PACKAGE_DEPENDENCIES)) {
152
- if (!(name in packageJson.dependencies) || packageJson.dependencies[name] === version) continue
153
- packageJson.dependencies[name] = version
154
- patched = true
155
- }
156
- if (!patched) return { patched: false }
157
-
158
- await fse.writeJson(packagePath, packageJson, { spaces: 2 })
159
- success('Updated OpenCode plugin dependencies in .opencode/package.json')
160
- return { patched: true }
161
- }
1
+ import fse from 'fs-extra'
2
+ import { applyEdits, modify, parse } from 'jsonc-parser'
3
+ import path from 'node:path'
4
+ import { success } from '../../utils/exec.js'
5
+
6
+ // The reminder plugin loads every agent ability before work, including
7
+ // user-installed skills outside the pc-* and openspec-* namespaces.
8
+ const SHARED_PERMISSIONS = [
9
+ ['question', 'allow'],
10
+ ['todowrite', 'allow'],
11
+ ]
12
+
13
+ // agent-browser replaced the @different-ai/opencode-browser plugin; any entry
14
+ // with that prefix in the plugin array is stale and gets stripped on update.
15
+ const STALE_BROWSER_PLUGIN_PREFIX = '@different-ai/opencode-browser'
16
+ const AGENT_BROWSER_MCP = {
17
+ type: 'local',
18
+ command: ['agent-browser', 'mcp', '--tools', 'core'],
19
+ enabled: true,
20
+ }
21
+
22
+ function applyModify(text, jsonPath, value) {
23
+ const edits = modify(text, jsonPath, value, {
24
+ formattingOptions: { insertSpaces: true, tabSize: 2 },
25
+ })
26
+ return applyEdits(text, edits)
27
+ }
28
+
29
+ export async function patchOpencodeJson(cwd = process.cwd()) {
30
+ const opencodePath = path.join(cwd, 'opencode.jsonc')
31
+
32
+ let text
33
+ if (await fse.pathExists(opencodePath)) {
34
+ text = await fse.readFile(opencodePath, 'utf-8')
35
+ } else {
36
+ text = JSON.stringify({ $schema: 'https://opencode.ai/config.json' }, null, 2)
37
+ }
38
+
39
+ const errors = []
40
+ const parsed = parse(text, errors)
41
+ if (errors.length > 0 || typeof parsed !== 'object') {
42
+ return { patched: false, reason: 'parse error' }
43
+ }
44
+
45
+ // build and plan are overridden, not disabled. Earlier versions disabled them
46
+ // and pointed default_agent at fullstack-engineer; now they are the only two
47
+ // primaries and pc-subagent-tiers regenerates their agent files from
48
+ // fullstack-engineer.md. A repo patched by an earlier version still carries
49
+ // `disable: true`, which would hide both agents, so that flag is cleared here.
50
+ const hasStaleDisable = (
51
+ parsed?.agent?.build?.disable !== undefined ||
52
+ parsed?.agent?.plan?.disable !== undefined
53
+ )
54
+ const needsAgentOverride = hasStaleDisable || !(
55
+ parsed?.agent?.build?.mode === 'primary' &&
56
+ parsed?.agent?.plan?.mode === 'primary' &&
57
+ parsed?.agent?.plan?.permission?.edit === 'deny'
58
+ )
59
+
60
+ // default_agent pointing at fullstack-engineer is now invalid: it became a
61
+ // subagent when build and plan took over as the only primaries, so opencode
62
+ // has no valid default to open a session with. A deliberate choice of build
63
+ // or plan is left alone; anything else is repointed at plan, so a session
64
+ // starts read-only.
65
+ const currentDefault = parsed?.default_agent
66
+ const needsDefaultAgent = currentDefault !== 'plan' && currentDefault !== 'build'
67
+ const needsSkillPermission = parsed?.permission?.skill !== 'allow'
68
+ const missingSharedPermissions = SHARED_PERMISSIONS.filter(
69
+ ([key, value]) => parsed?.permission?.[key] !== value,
70
+ )
71
+ // opencode only scans .opencode/skills/ by default, but we install to .agents/skills/
72
+ const needsSkillsPaths = !(
73
+ Array.isArray(parsed?.skills?.paths) &&
74
+ parsed.skills.paths.includes('.agents/skills')
75
+ )
76
+ const needsCompaction = !(
77
+ parsed?.compaction?.auto === true &&
78
+ parsed?.compaction?.prune === true
79
+ )
80
+ const staleBrowserPlugins = (Array.isArray(parsed?.plugin) ? parsed.plugin : [])
81
+ .filter(entry => typeof entry === 'string' && entry.startsWith(STALE_BROWSER_PLUGIN_PREFIX))
82
+ const needsBrowserMcp = JSON.stringify(parsed?.mcp?.['agent-browser']) !== JSON.stringify(AGENT_BROWSER_MCP)
83
+
84
+ if (!needsAgentOverride && !needsDefaultAgent && !needsSkillPermission && missingSharedPermissions.length === 0 && !needsSkillsPaths && !needsCompaction && staleBrowserPlugins.length === 0 && !needsBrowserMcp) {
85
+ return { patched: false }
86
+ }
87
+
88
+ // Apply edits sequentially so offsets stay correct
89
+ if (needsAgentOverride) {
90
+ // undefined removes the key, clearing the disable left by earlier versions.
91
+ if (parsed?.agent?.build?.disable !== undefined) {
92
+ text = applyModify(text, ['agent', 'build', 'disable'], undefined)
93
+ }
94
+ if (parsed?.agent?.plan?.disable !== undefined) {
95
+ text = applyModify(text, ['agent', 'plan', 'disable'], undefined)
96
+ }
97
+ text = applyModify(text, ['agent', 'build', 'mode'], 'primary')
98
+ text = applyModify(text, ['agent', 'plan', 'mode'], 'primary')
99
+ text = applyModify(text, ['agent', 'plan', 'permission', 'edit'], 'deny')
100
+ }
101
+ if (needsDefaultAgent) {
102
+ text = applyModify(text, ['default_agent'], 'plan')
103
+ }
104
+ if (needsSkillPermission) text = applyModify(text, ['permission', 'skill'], 'allow')
105
+ for (const [key, value] of missingSharedPermissions) {
106
+ text = applyModify(text, ['permission', key], value)
107
+ }
108
+
109
+ // Ensure .agents/skills is in skills.paths so opencode discovers installed skills
110
+ if (needsSkillsPaths) {
111
+ const existingPaths = Array.isArray(parsed?.skills?.paths) ? parsed.skills.paths : []
112
+ if (!existingPaths.includes('.agents/skills')) {
113
+ const updatedPaths = [...existingPaths, '.agents/skills']
114
+ text = applyModify(text, ['skills', 'paths'], updatedPaths)
115
+ }
116
+ }
117
+
118
+ // Enable context pruning to reduce token costs in long agent sessions
119
+ if (needsCompaction) {
120
+ text = applyModify(text, ['compaction', 'auto'], true)
121
+ text = applyModify(text, ['compaction', 'prune'], true)
122
+ text = applyModify(text, ['compaction', 'reserved'], 10000)
123
+ }
124
+
125
+ // Swap the retired opencode-browser plugin for the agent-browser MCP server
126
+ if (staleBrowserPlugins.length > 0) {
127
+ const keptPlugins = parsed.plugin.filter(entry => !staleBrowserPlugins.includes(entry))
128
+ text = applyModify(text, ['plugin'], keptPlugins)
129
+ }
130
+ if (needsBrowserMcp) {
131
+ text = applyModify(text, ['mcp', 'agent-browser'], AGENT_BROWSER_MCP)
132
+ }
133
+
134
+ await fse.writeFile(opencodePath, text, 'utf-8')
135
+ if (needsAgentOverride) {
136
+ success('Set build/plan as the primary agents in opencode.jsonc (plan is read-only)')
137
+ }
138
+ if (needsDefaultAgent) {
139
+ success(`default_agent -> plan${currentDefault ? ` (was ${currentDefault})` : ''}`)
140
+ }
141
+ if (needsSkillPermission) {
142
+ success('Allowed skill loading in opencode.jsonc')
143
+ }
144
+ if (missingSharedPermissions.length > 0) {
145
+ success('Allowed question/todowrite tools in opencode.jsonc')
146
+ }
147
+ if (needsSkillsPaths) {
148
+ success('Added .agents/skills to skills.paths in opencode.jsonc')
149
+ }
150
+ if (needsCompaction) {
151
+ success('Enabled context pruning (compaction.prune) in opencode.jsonc')
152
+ }
153
+ if (staleBrowserPlugins.length > 0) {
154
+ success(`Removed ${staleBrowserPlugins.length} stale opencode-browser plugin entr${staleBrowserPlugins.length === 1 ? 'y' : 'ies'} in opencode.jsonc`)
155
+ }
156
+ if (needsBrowserMcp) {
157
+ success('Configured agent-browser MCP server in opencode.jsonc')
158
+ }
159
+
160
+ return { patched: true }
161
+ }
162
+
163
+ const OPENCODE_PACKAGE_DEPENDENCIES = {
164
+ '@opencode-ai/plugin': '1.18.19',
165
+ '@opentui/core': '0.5.6',
166
+ '@opentui/solid': '0.5.6',
167
+ 'solid-js': '1.9.12',
168
+ }
169
+
170
+ // agent-browser replaced this plugin; remove it from consumer installs on update
171
+ const STALE_PACKAGE_DEPENDENCIES = ['@different-ai/opencode-browser']
172
+
173
+ export async function patchOpencodePackage(cwd = process.cwd()) {
174
+ const packagePath = path.join(cwd, '.opencode', 'package.json')
175
+ if (!await fse.pathExists(packagePath)) return { patched: false }
176
+
177
+ const packageJson = await fse.readJson(packagePath).catch(() => null)
178
+ if (!packageJson?.dependencies) return { patched: false }
179
+
180
+ let patched = false
181
+ for (const [name, version] of Object.entries(OPENCODE_PACKAGE_DEPENDENCIES)) {
182
+ if (!(name in packageJson.dependencies) || packageJson.dependencies[name] === version) continue
183
+ packageJson.dependencies[name] = version
184
+ patched = true
185
+ }
186
+ for (const name of STALE_PACKAGE_DEPENDENCIES) {
187
+ if (!(name in packageJson.dependencies)) continue
188
+ delete packageJson.dependencies[name]
189
+ patched = true
190
+ }
191
+ if (!patched) return { patched: false }
192
+
193
+ await fse.writeJson(packagePath, packageJson, { spaces: 2 })
194
+ success('Updated OpenCode plugin dependencies in .opencode/package.json')
195
+ return { patched: true }
196
+ }
@@ -5,8 +5,8 @@ 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, '../../content/.agents/skills')
9
- const CONTENT_SKILLS_LOCK = path.resolve(__dirname, '../../content/skills-lock.json')
8
+ const CONTENT_SKILLS_DIR = path.resolve(__dirname, '../../../harness/.agents/skills')
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.
12
12
  // Mixing the two axes here installs the wrong variant on mixed setups, because
@@ -1,88 +1,88 @@
1
- import { search, select } from '@inquirer/prompts'
2
- import fse from 'fs-extra'
3
- import path from 'path'
4
- import { fileURLToPath } from 'url'
5
-
6
- const __dirname = path.dirname(fileURLToPath(import.meta.url));
7
- const MODELS_PRESET_PATH = path.resolve(__dirname, '../../presets/models.json');
8
- const modelsPreset = await fse.readJson(MODELS_PRESET_PATH);
9
-
10
- function costTier(input) {
11
- if (input === undefined || input === null) return '';
12
- const tier = modelsPreset.costTiers.find(t => t.max === undefined || input <= t.max);
13
- return tier ? ` ${tier.label}` : '';
14
- }
15
-
16
- function costTierDisplay(cost, canonicalCost) {
17
- return costTier(canonicalCost !== undefined ? canonicalCost : cost);
18
- }
19
-
20
- function formatPrice(price) {
21
- if (price === undefined || price === null) return '?';
22
- if (price === 0) return '$0 (subscription)';
23
- return `$${price}/M`;
24
- }
25
-
26
- export function buildDisplayModels(rawModels) {
27
- return rawModels.map(m => {
28
- const priceStr = formatPrice(m.cost);
29
- const canonicalNote = m.canonicalCost !== undefined
30
- ? ` · official price: ${formatPrice(m.canonicalCost)}/M`
31
- : '';
32
- const context = m.context ? `${m.context / 1000}k` : '?';
33
- return {
34
- ...m,
35
- label: `${m.name}${costTierDisplay(m.cost, m.canonicalCost)}, ${m.id}`,
36
- description: `${priceStr}${canonicalNote} · context: ${context}`,
37
- };
38
- });
39
- }
40
-
41
- export function buildModelChoices(input, models) {
42
- const q = (input || '').toLowerCase();
43
- const filtered = q
44
- ? models.filter(m =>
45
- m.label.toLowerCase().includes(q) ||
46
- m.id.toLowerCase().includes(q)
47
- )
48
- : models;
49
-
50
- return [
51
- {
52
- name: 'None',
53
- value: null,
54
- description: 'Leave this model unset',
55
- },
56
- {
57
- name: '← Change provider',
58
- value: '__back__',
59
- description: 'Go back to provider selection',
60
- },
61
- ...filtered.slice(0, 50).map(m => ({
62
- name: m.label,
63
- value: m.id,
64
- description: m.description,
65
- })),
66
- ];
67
- }
68
-
69
- export function pickModel(message, models) {
70
- return search({
71
- message,
72
- source: input => buildModelChoices(input, models),
73
- });
74
- }
75
-
76
- export function filterModelsByProvider(models, provider) {
77
- if (!provider) return models
78
- return models.filter(m => m.id.startsWith(`${provider}/`))
79
- }
80
-
81
- export function pickProvider(message) {
82
- return select({
83
- message,
84
- choices: modelsPreset.providers.map(p => ({ value: p.value, name: p.name })),
85
- })
86
- }
87
-
88
- export { modelsPreset };
1
+ import { search, select } from '@inquirer/prompts'
2
+ import fse from 'fs-extra'
3
+ import path from 'path'
4
+ import { fileURLToPath } from 'url'
5
+
6
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
7
+ const MODELS_PRESET_PATH = path.resolve(__dirname, '../../presets/models.json');
8
+ const modelsPreset = await fse.readJson(MODELS_PRESET_PATH);
9
+
10
+ function costTier(input) {
11
+ if (input === undefined || input === null) return '';
12
+ const tier = modelsPreset.costTiers.find(t => t.max === undefined || input <= t.max);
13
+ return tier ? ` ${tier.label}` : '';
14
+ }
15
+
16
+ function costTierDisplay(cost, canonicalCost) {
17
+ return costTier(canonicalCost !== undefined ? canonicalCost : cost);
18
+ }
19
+
20
+ function formatPrice(price) {
21
+ if (price === undefined || price === null) return '?';
22
+ if (price === 0) return '$0 (subscription)';
23
+ return `$${price}/M`;
24
+ }
25
+
26
+ export function buildDisplayModels(rawModels) {
27
+ return rawModels.map(m => {
28
+ const priceStr = formatPrice(m.cost);
29
+ const canonicalNote = m.canonicalCost !== undefined
30
+ ? ` · official price: ${formatPrice(m.canonicalCost)}/M`
31
+ : '';
32
+ const context = m.context ? `${m.context / 1000}k` : '?';
33
+ return {
34
+ ...m,
35
+ label: `${m.name}${costTierDisplay(m.cost, m.canonicalCost)}, ${m.id}`,
36
+ description: `${priceStr}${canonicalNote} · context: ${context}`,
37
+ };
38
+ });
39
+ }
40
+
41
+ export function buildModelChoices(input, models) {
42
+ const q = (input || '').toLowerCase();
43
+ const filtered = q
44
+ ? models.filter(m =>
45
+ m.label.toLowerCase().includes(q) ||
46
+ m.id.toLowerCase().includes(q)
47
+ )
48
+ : models;
49
+
50
+ return [
51
+ {
52
+ name: 'None',
53
+ value: null,
54
+ description: 'Leave this model unset',
55
+ },
56
+ {
57
+ name: '← Change provider',
58
+ value: '__back__',
59
+ description: 'Go back to provider selection',
60
+ },
61
+ ...filtered.slice(0, 50).map(m => ({
62
+ name: m.label,
63
+ value: m.id,
64
+ description: m.description,
65
+ })),
66
+ ];
67
+ }
68
+
69
+ export function pickModel(message, models) {
70
+ return search({
71
+ message,
72
+ source: input => buildModelChoices(input, models),
73
+ });
74
+ }
75
+
76
+ export function filterModelsByProvider(models, provider) {
77
+ if (!provider) return models
78
+ return models.filter(m => m.id.startsWith(`${provider}/`))
79
+ }
80
+
81
+ export function pickProvider(message) {
82
+ return select({
83
+ message,
84
+ choices: modelsPreset.providers.map(p => ({ value: p.value, name: p.name })),
85
+ })
86
+ }
87
+
88
+ export { modelsPreset };