@plainconceptsplatform/agent-harness 2.0.0 → 2.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.
Files changed (116) hide show
  1. package/README.md +423 -419
  2. package/package.json +3 -3
  3. package/src/commands/join.js +244 -244
  4. package/src/commands/shared.js +27 -27
  5. package/src/commands/single.js +79 -79
  6. package/src/commands/update.js +109 -109
  7. package/src/commands/wizard.js +134 -134
  8. package/src/content/.agents/skills/browser-automation/SKILL.md +66 -66
  9. package/src/content/.agents/skills/pc-guardrails-generic/SKILL.md +68 -68
  10. package/src/content/.agents/skills/pc-guardrails-project/SKILL.md +8 -8
  11. package/src/content/.agents/skills/pc-make-architecture/SKILL.md +51 -51
  12. package/src/content/.agents/skills/pc-make-architecture/structure-template.md +38 -38
  13. package/src/content/.agents/skills/pc-make-design/SKILL.md +68 -68
  14. package/src/content/.agents/skills/pc-make-engineer/SKILL.md +219 -219
  15. package/src/content/.agents/skills/pc-make-engineer/signal-mapping.md +68 -68
  16. package/src/content/.agents/skills/pc-make-engineer/template.md +81 -81
  17. package/src/content/.agents/skills/pc-make-evidence-scaffold/SKILL.md +18 -18
  18. package/src/content/.agents/skills/pc-make-evidence-scaffold/evidence-contract.md +29 -29
  19. package/src/content/.agents/skills/pc-make-guardrails/SKILL.md +74 -74
  20. package/src/content/.agents/skills/pc-make-guardrails/category-reference.md +68 -68
  21. package/src/content/.agents/skills/pc-make-merge-risk-assess/SKILL.md +70 -70
  22. package/src/content/.agents/skills/pc-make-merge-risk-assess/category-reference.md +98 -98
  23. package/src/content/.agents/skills/pc-make-user-model/SKILL.md +66 -66
  24. package/src/content/.agents/skills/pc-ops-evidence/SKILL.md +127 -127
  25. package/src/content/.agents/skills/pc-ops-ship/SKILL.md +18 -18
  26. package/src/content/.agents/skills/pc-plan-apply/SKILL.md +83 -83
  27. package/src/content/.agents/skills/pc-plan-apply/simple-mode.md +21 -21
  28. package/src/content/.agents/skills/pc-plan-archive/SKILL.md +63 -63
  29. package/src/content/.agents/skills/pc-plan-explore/SKILL.md +9 -9
  30. package/src/content/.agents/skills/pc-plan-goal/SKILL.md +94 -94
  31. package/src/content/.agents/skills/pc-plan-goal/branching.md +30 -30
  32. package/src/content/.agents/skills/pc-plan-goal/failure-policy.md +30 -30
  33. package/src/content/.agents/skills/pc-plan-goal/output-mode.md +9 -9
  34. package/src/content/.agents/skills/pc-plan-goal/output.md +68 -68
  35. package/src/content/.agents/skills/pc-plan-propose/SKILL.md +125 -125
  36. package/src/content/.agents/skills/pc-plan-propose/task-annotation.md +39 -39
  37. package/src/content/.agents/skills/pc-plan-quick/SKILL.md +62 -62
  38. package/src/content/.agents/skills/pc-plan-story/SKILL.md +146 -146
  39. package/src/content/.agents/skills/pc-repo-audit/SKILL.md +44 -44
  40. package/src/content/.agents/skills/pc-repo-help/SKILL.md +91 -91
  41. package/src/content/.agents/skills/pc-repo-initialize/SKILL.md +130 -130
  42. package/src/content/.agents/skills/pc-repo-onboard/SKILL.md +87 -87
  43. package/src/content/.agents/skills/pc-repo-verify/SKILL.md +34 -34
  44. package/src/content/.agents/skills/pc-userstory-az/SKILL.md +157 -157
  45. package/src/content/.agents/skills/pc-userstory-browser/SKILL.md +132 -132
  46. package/src/content/.agents/skills/pc-userstory-gh/SKILL.md +120 -120
  47. package/src/content/.agents/skills/pc-userstory-jira/SKILL.md +131 -131
  48. package/src/content/.opencode/_gitignore +7 -7
  49. package/src/content/.opencode/commands/init.md +5 -5
  50. package/src/content/.opencode/commands/make-architecture.md +5 -5
  51. package/src/content/.opencode/commands/make-design.md +5 -5
  52. package/src/content/.opencode/commands/make-engineer.md +5 -5
  53. package/src/content/.opencode/commands/make-evidence-scaffold.md +5 -5
  54. package/src/content/.opencode/commands/make-guardrails.md +5 -5
  55. package/src/content/.opencode/commands/make-user-model.md +5 -5
  56. package/src/content/.opencode/commands/ops-backlog.md +10 -10
  57. package/src/content/.opencode/commands/ops-evidence.md +9 -9
  58. package/src/content/.opencode/commands/ops-review.md +8 -8
  59. package/src/content/.opencode/commands/ops-ship.md +9 -9
  60. package/src/content/.opencode/commands/plan-apply.md +9 -9
  61. package/src/content/.opencode/commands/plan-archive.md +5 -5
  62. package/src/content/.opencode/commands/plan-explore.md +9 -9
  63. package/src/content/.opencode/commands/plan-goal.md +5 -5
  64. package/src/content/.opencode/commands/plan-propose.md +9 -9
  65. package/src/content/.opencode/commands/plan-quick.md +5 -5
  66. package/src/content/.opencode/commands/plan-story.md +9 -9
  67. package/src/content/.opencode/commands/repo-audit.md +5 -5
  68. package/src/content/.opencode/commands/repo-help.md +5 -5
  69. package/src/content/.opencode/commands/repo-initialize.md +5 -5
  70. package/src/content/.opencode/commands/repo-onboard.md +5 -5
  71. package/src/content/.opencode/commands/repo-verify.md +5 -5
  72. package/src/content/.opencode/plugins/pc-subagent-monitor.js +139 -139
  73. package/src/content/.opencode/plugins/pc-subagent-tiers.js +179 -179
  74. package/src/content/.opencode/plugins/pc-system-reminders.js +96 -96
  75. package/src/content/.opencode/tui/pc-subagents.tsx +98 -98
  76. package/src/content/.opencode/tui.json +6 -6
  77. package/src/content/AGENTS.md +71 -71
  78. package/src/fragments/archive/az.md +95 -95
  79. package/src/fragments/archive/gh.md +94 -94
  80. package/src/fragments/archive/gl.md +94 -94
  81. package/src/fragments/archive/none.md +73 -73
  82. package/src/fragments/guardrails/codegraph.md +7 -7
  83. package/src/fragments/guardrails/humanizer.md +4 -4
  84. package/src/fragments/guardrails/memory.md +4 -4
  85. package/src/fragments/guardrails/rtk.md +3 -3
  86. package/src/fragments/guardrails/simple-english.md +4 -4
  87. package/src/fragments/ops-backlog/az.md +28 -28
  88. package/src/fragments/ops-backlog/gh.md +29 -29
  89. package/src/fragments/ops-backlog/jira.md +28 -28
  90. package/src/fragments/ops-evidence/az.md +41 -41
  91. package/src/fragments/ops-evidence/gh.md +53 -53
  92. package/src/fragments/ops-evidence/jira.md +38 -38
  93. package/src/fragments/ops-review/az.md +62 -62
  94. package/src/fragments/ops-review/gh.md +52 -52
  95. package/src/fragments/ops-review/gl.md +56 -56
  96. package/src/fragments/ops-ship/az.md +80 -80
  97. package/src/fragments/ops-ship/gh.md +68 -68
  98. package/src/fragments/ops-ship/gl.md +85 -85
  99. package/src/index.js +107 -107
  100. package/src/presets/agents-content.json +53 -53
  101. package/src/presets/models.json +68 -68
  102. package/src/steps/copy/agents.js +118 -118
  103. package/src/steps/copy/commands.js +91 -91
  104. package/src/steps/copy/fullstack-engineer.js +83 -83
  105. package/src/steps/copy/index.js +88 -88
  106. package/src/steps/copy/opencode-json.js +129 -129
  107. package/src/steps/copy/skills.js +196 -196
  108. package/src/steps/metadata/index.js +108 -108
  109. package/src/steps/models/write.js +34 -34
  110. package/src/steps/optimization/patch-guardrails.js +108 -108
  111. package/src/utils/copy.js +108 -108
  112. package/src/utils/legacy-check.js +30 -30
  113. package/src/utils/models-cache.js +58 -58
  114. package/src/utils/paths.js +64 -64
  115. package/src/utils/update-manifest.js +49 -49
  116. package/src/content/.opencode/plugins/pc-system-reminders.test.js +0 -35
@@ -1,88 +1,88 @@
1
- import fse from "fs-extra"
2
- import path from "path"
3
- import { fileURLToPath } from "url"
4
- import { copyContent, recordManagedContent } from "../../utils/copy.js"
5
- import { error, header, success } from "../../utils/exec.js"
6
- import { exit } from "../../utils/process.js"
7
- import { patchAgentGuidance, patchAgentsMd } from "./agents.js"
8
- import { patchArchiveCommand, patchOpsShip, patchOpsReview, patchOpsBacklog, patchOpsEvidence } from "./commands.js"
9
- import { installSkills } from "./skills.js"
10
- import { generateFullstackEngineer, removeLegacyStartupDirectives } from "./fullstack-engineer.js"
11
- import { patchOpencodeJson, patchOpencodePackage } from "./opencode-json.js"
12
-
13
- const __dirname = path.dirname(fileURLToPath(import.meta.url))
14
- const CONTENT_DIR = path.resolve(__dirname, "../../content")
15
-
16
- export async function copyContentStep(platform, ctx = {}) {
17
- header("Step 5, Copying agent-harness files")
18
-
19
- const dest = process.cwd()
20
-
21
- // Support both old single-string platform and new { backlogPlatform, repoPlatform }
22
- const backlogPlatform = typeof platform === 'object' ? platform.backlogPlatform : platform
23
- const repoPlatform = typeof platform === 'object' ? platform.repoPlatform : platform
24
-
25
- try {
26
- await copyContent(CONTENT_DIR, dest, platform, ctx)
27
-
28
- // .gitignore is stripped by npm during publish, so the source file is _gitignore.
29
- // Also, fse.copy with overwrite:false won't overwrite an existing .gitignore,
30
- // so merge manually to preserve both opencode's defaults and our additions.
31
- const srcGitignore = path.join(CONTENT_DIR, ".opencode", "_gitignore")
32
- const destGitignore = path.join(dest, ".opencode", ".gitignore")
33
- if (await fse.pathExists(srcGitignore)) {
34
- const srcLines = (await fse.readFile(srcGitignore, "utf-8")).split("\n").map(l => l.trim()).filter(Boolean)
35
- const destLines = (await fse.pathExists(destGitignore))
36
- ? (await fse.readFile(destGitignore, "utf-8")).split("\n").map(l => l.trim()).filter(Boolean)
37
- : []
38
- const merged = Array.from(new Set([...destLines, ...srcLines]))
39
- await fse.writeFile(destGitignore, `${merged.join("\n")}\n`, "utf-8")
40
- }
41
-
42
- // Remove the _gitignore template from dest: it was only needed for the merge above.
43
- const destGitignoreTemplate = path.join(dest, ".opencode", "_gitignore")
44
- if (await fse.pathExists(destGitignoreTemplate)) {
45
- await fse.remove(destGitignoreTemplate)
46
- }
47
-
48
- const rootsFile = path.join(dest, ".opencode", "source-roots.json")
49
- if (!ctx.updateMode || !await fse.pathExists(rootsFile)) {
50
- await fse.writeJson(
51
- rootsFile,
52
- {
53
- mode: ctx.sourceMode || "current",
54
- // An empty array is truthy, so `|| [dest]` never kicked in and reruns
55
- // wrote `roots: []`, leaving agents with no source scope.
56
- roots: ctx.sourceRoots?.length ? ctx.sourceRoots : [dest],
57
- },
58
- { spaces: 2 },
59
- )
60
- }
61
-
62
- await patchAgentGuidance(backlogPlatform, repoPlatform)
63
- await patchOpsReview({ backlogPlatform, repoPlatform })
64
- await patchOpsBacklog({ backlogPlatform, repoPlatform })
65
- await patchOpencodeJson()
66
- await patchOpencodePackage()
67
- if (!ctx.updateMode) await patchAgentsMd(ctx)
68
-
69
- if (!ctx.skipSkills) {
70
- await installSkills(backlogPlatform, repoPlatform, {
71
- forceOverwrite: ctx.forceOverwrite,
72
- updateMode: ctx.updateMode,
73
- })
74
- }
75
- // These patch SKILL.md files (pc-plan-archive, pc-ops-ship, pc-ops-evidence),
76
- // so they must run after installSkills has copied the skills into the project.
77
- await patchArchiveCommand({ backlogPlatform, repoPlatform })
78
- await patchOpsShip({ backlogPlatform, repoPlatform })
79
- await patchOpsEvidence({ backlogPlatform, repoPlatform })
80
- await generateFullstackEngineer({ updateMode: ctx.updateMode })
81
- await removeLegacyStartupDirectives()
82
- await recordManagedContent(CONTENT_DIR, dest, { updateMode: ctx.updateMode })
83
- success("Files copied to project root")
84
- } catch (err) {
85
- error(`Failed to copy content: ${err.message}`)
86
- exit(1)
87
- }
88
- }
1
+ import fse from "fs-extra"
2
+ import path from "path"
3
+ import { fileURLToPath } from "url"
4
+ import { copyContent, recordManagedContent } from "../../utils/copy.js"
5
+ import { error, header, success } from "../../utils/exec.js"
6
+ import { exit } from "../../utils/process.js"
7
+ import { patchAgentGuidance, patchAgentsMd } from "./agents.js"
8
+ import { patchArchiveCommand, patchOpsShip, patchOpsReview, patchOpsBacklog, patchOpsEvidence } from "./commands.js"
9
+ import { installSkills } from "./skills.js"
10
+ import { generateFullstackEngineer, removeLegacyStartupDirectives } from "./fullstack-engineer.js"
11
+ import { patchOpencodeJson, patchOpencodePackage } from "./opencode-json.js"
12
+
13
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
14
+ const CONTENT_DIR = path.resolve(__dirname, "../../content")
15
+
16
+ export async function copyContentStep(platform, ctx = {}) {
17
+ header("Step 5, Copying agent-harness files")
18
+
19
+ const dest = process.cwd()
20
+
21
+ // Support both old single-string platform and new { backlogPlatform, repoPlatform }
22
+ const backlogPlatform = typeof platform === 'object' ? platform.backlogPlatform : platform
23
+ const repoPlatform = typeof platform === 'object' ? platform.repoPlatform : platform
24
+
25
+ try {
26
+ await copyContent(CONTENT_DIR, dest, platform, ctx)
27
+
28
+ // .gitignore is stripped by npm during publish, so the source file is _gitignore.
29
+ // Also, fse.copy with overwrite:false won't overwrite an existing .gitignore,
30
+ // so merge manually to preserve both opencode's defaults and our additions.
31
+ const srcGitignore = path.join(CONTENT_DIR, ".opencode", "_gitignore")
32
+ const destGitignore = path.join(dest, ".opencode", ".gitignore")
33
+ if (await fse.pathExists(srcGitignore)) {
34
+ const srcLines = (await fse.readFile(srcGitignore, "utf-8")).split("\n").map(l => l.trim()).filter(Boolean)
35
+ const destLines = (await fse.pathExists(destGitignore))
36
+ ? (await fse.readFile(destGitignore, "utf-8")).split("\n").map(l => l.trim()).filter(Boolean)
37
+ : []
38
+ const merged = Array.from(new Set([...destLines, ...srcLines]))
39
+ await fse.writeFile(destGitignore, `${merged.join("\n")}\n`, "utf-8")
40
+ }
41
+
42
+ // Remove the _gitignore template from dest: it was only needed for the merge above.
43
+ const destGitignoreTemplate = path.join(dest, ".opencode", "_gitignore")
44
+ if (await fse.pathExists(destGitignoreTemplate)) {
45
+ await fse.remove(destGitignoreTemplate)
46
+ }
47
+
48
+ const rootsFile = path.join(dest, ".opencode", "source-roots.json")
49
+ if (!ctx.updateMode || !await fse.pathExists(rootsFile)) {
50
+ await fse.writeJson(
51
+ rootsFile,
52
+ {
53
+ mode: ctx.sourceMode || "current",
54
+ // An empty array is truthy, so `|| [dest]` never kicked in and reruns
55
+ // wrote `roots: []`, leaving agents with no source scope.
56
+ roots: ctx.sourceRoots?.length ? ctx.sourceRoots : [dest],
57
+ },
58
+ { spaces: 2 },
59
+ )
60
+ }
61
+
62
+ await patchAgentGuidance(backlogPlatform, repoPlatform)
63
+ await patchOpsReview({ backlogPlatform, repoPlatform })
64
+ await patchOpsBacklog({ backlogPlatform, repoPlatform })
65
+ await patchOpencodeJson()
66
+ await patchOpencodePackage()
67
+ if (!ctx.updateMode) await patchAgentsMd(ctx)
68
+
69
+ if (!ctx.skipSkills) {
70
+ await installSkills(backlogPlatform, repoPlatform, {
71
+ forceOverwrite: ctx.forceOverwrite,
72
+ updateMode: ctx.updateMode,
73
+ })
74
+ }
75
+ // These patch SKILL.md files (pc-plan-archive, pc-ops-ship, pc-ops-evidence),
76
+ // so they must run after installSkills has copied the skills into the project.
77
+ await patchArchiveCommand({ backlogPlatform, repoPlatform })
78
+ await patchOpsShip({ backlogPlatform, repoPlatform })
79
+ await patchOpsEvidence({ backlogPlatform, repoPlatform })
80
+ await generateFullstackEngineer({ updateMode: ctx.updateMode })
81
+ await removeLegacyStartupDirectives()
82
+ await recordManagedContent(CONTENT_DIR, dest, { updateMode: ctx.updateMode })
83
+ success("Files copied to project root")
84
+ } catch (err) {
85
+ error(`Failed to copy content: ${err.message}`)
86
+ exit(1)
87
+ }
88
+ }
@@ -1,129 +1,129 @@
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
- const needsAgentDisable = !(
37
- parsed?.agent?.build?.disable === true &&
38
- parsed?.agent?.plan?.disable === true
39
- )
40
- const needsSkillPermission = parsed?.permission?.skill !== 'allow'
41
- const missingSharedPermissions = SHARED_PERMISSIONS.filter(
42
- ([key, value]) => parsed?.permission?.[key] !== value,
43
- )
44
- // opencode only scans .opencode/skills/ by default, but we install to .agents/skills/
45
- const needsSkillsPaths = !(
46
- Array.isArray(parsed?.skills?.paths) &&
47
- parsed.skills.paths.includes('.agents/skills')
48
- )
49
- const needsCompaction = !(
50
- parsed?.compaction?.auto === true &&
51
- parsed?.compaction?.prune === true
52
- )
53
-
54
- if (!needsAgentDisable && !needsSkillPermission && missingSharedPermissions.length === 0 && !needsSkillsPaths && !needsCompaction) {
55
- return { patched: false }
56
- }
57
-
58
- // Apply edits sequentially so offsets stay correct
59
- if (needsAgentDisable) {
60
- text = applyModify(text, ['agent', 'build', 'disable'], true)
61
- text = applyModify(text, ['agent', 'plan', 'disable'], true)
62
- }
63
- if (needsSkillPermission) text = applyModify(text, ['permission', 'skill'], 'allow')
64
- for (const [key, value] of missingSharedPermissions) {
65
- text = applyModify(text, ['permission', key], value)
66
- }
67
-
68
- // Ensure .agents/skills is in skills.paths so opencode discovers installed skills
69
- if (needsSkillsPaths) {
70
- const existingPaths = Array.isArray(parsed?.skills?.paths) ? parsed.skills.paths : []
71
- if (!existingPaths.includes('.agents/skills')) {
72
- const updatedPaths = [...existingPaths, '.agents/skills']
73
- text = applyModify(text, ['skills', 'paths'], updatedPaths)
74
- }
75
- }
76
-
77
- // Enable context pruning to reduce token costs in long agent sessions
78
- if (needsCompaction) {
79
- text = applyModify(text, ['compaction', 'auto'], true)
80
- text = applyModify(text, ['compaction', 'prune'], true)
81
- text = applyModify(text, ['compaction', 'reserved'], 10000)
82
- }
83
-
84
- await fse.writeFile(opencodePath, text, 'utf-8')
85
- if (needsAgentDisable) {
86
- success('Disabled built-in build/plan agents in opencode.jsonc')
87
- }
88
- if (needsSkillPermission) {
89
- success('Allowed skill loading in opencode.jsonc')
90
- }
91
- if (missingSharedPermissions.length > 0) {
92
- success('Allowed question/todowrite tools in opencode.jsonc')
93
- }
94
- if (needsSkillsPaths) {
95
- success('Added .agents/skills to skills.paths in opencode.jsonc')
96
- }
97
- if (needsCompaction) {
98
- success('Enabled context pruning (compaction.prune) in opencode.jsonc')
99
- }
100
-
101
- return { patched: true }
102
- }
103
-
104
- const OPENCODE_PACKAGE_DEPENDENCIES = {
105
- '@opencode-ai/plugin': '1.18.19',
106
- '@opentui/core': '0.5.6',
107
- '@opentui/solid': '0.5.6',
108
- 'solid-js': '1.9.12',
109
- }
110
-
111
- export async function patchOpencodePackage(cwd = process.cwd()) {
112
- const packagePath = path.join(cwd, '.opencode', 'package.json')
113
- if (!await fse.pathExists(packagePath)) return { patched: false }
114
-
115
- const packageJson = await fse.readJson(packagePath).catch(() => null)
116
- if (!packageJson?.dependencies) return { patched: false }
117
-
118
- let patched = false
119
- for (const [name, version] of Object.entries(OPENCODE_PACKAGE_DEPENDENCIES)) {
120
- if (!(name in packageJson.dependencies) || packageJson.dependencies[name] === version) continue
121
- packageJson.dependencies[name] = version
122
- patched = true
123
- }
124
- if (!patched) return { patched: false }
125
-
126
- await fse.writeJson(packagePath, packageJson, { spaces: 2 })
127
- success('Updated OpenCode plugin dependencies in .opencode/package.json')
128
- return { patched: true }
129
- }
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
+ const needsAgentDisable = !(
37
+ parsed?.agent?.build?.disable === true &&
38
+ parsed?.agent?.plan?.disable === true
39
+ )
40
+ const needsSkillPermission = parsed?.permission?.skill !== 'allow'
41
+ const missingSharedPermissions = SHARED_PERMISSIONS.filter(
42
+ ([key, value]) => parsed?.permission?.[key] !== value,
43
+ )
44
+ // opencode only scans .opencode/skills/ by default, but we install to .agents/skills/
45
+ const needsSkillsPaths = !(
46
+ Array.isArray(parsed?.skills?.paths) &&
47
+ parsed.skills.paths.includes('.agents/skills')
48
+ )
49
+ const needsCompaction = !(
50
+ parsed?.compaction?.auto === true &&
51
+ parsed?.compaction?.prune === true
52
+ )
53
+
54
+ if (!needsAgentDisable && !needsSkillPermission && missingSharedPermissions.length === 0 && !needsSkillsPaths && !needsCompaction) {
55
+ return { patched: false }
56
+ }
57
+
58
+ // Apply edits sequentially so offsets stay correct
59
+ if (needsAgentDisable) {
60
+ text = applyModify(text, ['agent', 'build', 'disable'], true)
61
+ text = applyModify(text, ['agent', 'plan', 'disable'], true)
62
+ }
63
+ if (needsSkillPermission) text = applyModify(text, ['permission', 'skill'], 'allow')
64
+ for (const [key, value] of missingSharedPermissions) {
65
+ text = applyModify(text, ['permission', key], value)
66
+ }
67
+
68
+ // Ensure .agents/skills is in skills.paths so opencode discovers installed skills
69
+ if (needsSkillsPaths) {
70
+ const existingPaths = Array.isArray(parsed?.skills?.paths) ? parsed.skills.paths : []
71
+ if (!existingPaths.includes('.agents/skills')) {
72
+ const updatedPaths = [...existingPaths, '.agents/skills']
73
+ text = applyModify(text, ['skills', 'paths'], updatedPaths)
74
+ }
75
+ }
76
+
77
+ // Enable context pruning to reduce token costs in long agent sessions
78
+ if (needsCompaction) {
79
+ text = applyModify(text, ['compaction', 'auto'], true)
80
+ text = applyModify(text, ['compaction', 'prune'], true)
81
+ text = applyModify(text, ['compaction', 'reserved'], 10000)
82
+ }
83
+
84
+ await fse.writeFile(opencodePath, text, 'utf-8')
85
+ if (needsAgentDisable) {
86
+ success('Disabled built-in build/plan agents in opencode.jsonc')
87
+ }
88
+ if (needsSkillPermission) {
89
+ success('Allowed skill loading in opencode.jsonc')
90
+ }
91
+ if (missingSharedPermissions.length > 0) {
92
+ success('Allowed question/todowrite tools in opencode.jsonc')
93
+ }
94
+ if (needsSkillsPaths) {
95
+ success('Added .agents/skills to skills.paths in opencode.jsonc')
96
+ }
97
+ if (needsCompaction) {
98
+ success('Enabled context pruning (compaction.prune) in opencode.jsonc')
99
+ }
100
+
101
+ return { patched: true }
102
+ }
103
+
104
+ const OPENCODE_PACKAGE_DEPENDENCIES = {
105
+ '@opencode-ai/plugin': '1.18.19',
106
+ '@opentui/core': '0.5.6',
107
+ '@opentui/solid': '0.5.6',
108
+ 'solid-js': '1.9.12',
109
+ }
110
+
111
+ export async function patchOpencodePackage(cwd = process.cwd()) {
112
+ const packagePath = path.join(cwd, '.opencode', 'package.json')
113
+ if (!await fse.pathExists(packagePath)) return { patched: false }
114
+
115
+ const packageJson = await fse.readJson(packagePath).catch(() => null)
116
+ if (!packageJson?.dependencies) return { patched: false }
117
+
118
+ let patched = false
119
+ for (const [name, version] of Object.entries(OPENCODE_PACKAGE_DEPENDENCIES)) {
120
+ if (!(name in packageJson.dependencies) || packageJson.dependencies[name] === version) continue
121
+ packageJson.dependencies[name] = version
122
+ patched = true
123
+ }
124
+ if (!patched) return { patched: false }
125
+
126
+ await fse.writeJson(packagePath, packageJson, { spaces: 2 })
127
+ success('Updated OpenCode plugin dependencies in .opencode/package.json')
128
+ return { patched: true }
129
+ }