@plainconceptsplatform/agent-harness 2.2.1 → 2.4.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.
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/update.js +13 -1
  5. package/{src → cli}/commands/wizard.js +1 -1
  6. package/{src → cli}/index.js +1 -1
  7. package/{src → cli}/presets/agents-content.json +4 -4
  8. package/cli/presets/browser.json +10 -0
  9. package/{src → cli}/presets/clean.json +21 -21
  10. package/{src → cli}/presets/optimization.json +37 -37
  11. package/{src → cli}/presets/platforms.json +76 -76
  12. package/{src → cli}/presets/quota.json +16 -16
  13. package/{src → cli}/presets/source.json +23 -23
  14. package/cli/steps/browser/index.js +48 -0
  15. package/{src → cli}/steps/clean/index.js +120 -120
  16. package/{src → cli}/steps/copy/agents.js +119 -119
  17. package/{src → cli}/steps/copy/commands.js +95 -95
  18. package/{src → cli}/steps/copy/fullstack-engineer.js +5 -1
  19. package/{src → cli}/steps/copy/index.js +1 -1
  20. package/{src → cli}/steps/copy/opencode-json.js +50 -1
  21. package/{src → cli}/steps/copy/skills.js +2 -2
  22. package/{src → cli}/steps/metadata/index.js +4 -0
  23. package/{src → cli}/steps/models/format.js +88 -88
  24. package/{src → cli}/steps/models/index.js +64 -64
  25. package/{src → cli}/steps/openspec/index.js +136 -136
  26. package/{src → cli}/steps/optimization/codegraph.js +127 -127
  27. package/cli/steps/optimization/detect.js +66 -0
  28. package/{src → cli}/steps/optimization/humanizer.js +17 -17
  29. package/{src → cli}/steps/optimization/index.js +163 -163
  30. package/{src → cli}/steps/optimization/memory.js +88 -88
  31. package/{src → cli}/steps/optimization/patch-guardrails.js +109 -109
  32. package/{src → cli}/steps/optimization/quota.js +119 -119
  33. package/{src → cli}/steps/optimization/simple-english.js +17 -17
  34. package/{src → cli}/steps/optimization/skills-lock.js +30 -30
  35. package/{src → cli}/steps/platform/index.js +109 -109
  36. package/{src → cli}/steps/source/index.js +123 -123
  37. package/cli/utils/agent-color.js +111 -0
  38. package/{src → cli}/utils/exec-spinner.js +47 -47
  39. package/{src → cli}/utils/exec.js +134 -134
  40. package/{src → cli}/utils/models-pricing.js +42 -42
  41. package/{src → cli}/utils/paths.js +1 -1
  42. package/{src → cli}/utils/process.js +3 -3
  43. package/{src → cli}/utils/terminal.js +6 -6
  44. package/harness/.agents/skills/browser-automation/SKILL.md +72 -0
  45. package/{src/content → harness}/.agents/skills/pc-make-engineer/SKILL.md +1 -1
  46. package/{src/content → harness}/.agents/skills/pc-make-engineer/template.md +1 -2
  47. package/{src/content → harness}/.agents/skills/pc-plan-archive/SKILL.md +3 -0
  48. package/{src/content → harness}/.agents/skills/pc-plan-explore/SKILL.md +3 -0
  49. package/{src/content → harness}/.agents/skills/pc-plan-goal/SKILL.md +5 -16
  50. package/{src/content → harness}/.agents/skills/pc-plan-goal/branching.md +1 -1
  51. package/{src/content → harness}/.agents/skills/pc-plan-goal/output.md +5 -8
  52. package/{src/content → harness}/.agents/skills/pc-plan-story/SKILL.md +3 -0
  53. package/{src/content → harness}/.agents/skills/pc-userstory-browser/SKILL.md +136 -132
  54. package/{src/content → harness}/.opencode/package.json +9 -10
  55. package/{src/content → harness}/.opencode/plugins/pc-subagent-tiers.js +91 -4
  56. package/{src/content → harness}/ARCHITECTURE.md +16 -16
  57. package/{src/content → harness}/DESIGN.md +16 -16
  58. package/{src/content → harness}/opencode.jsonc +11 -3
  59. package/{src/content → harness}/openspec/config.yaml +20 -20
  60. package/{src/content → harness}/skills-lock.json +17 -17
  61. package/package.json +7 -7
  62. package/src/content/.agents/skills/browser-automation/SKILL.md +0 -66
  63. package/src/presets/browser.json +0 -22
  64. package/src/steps/browser/index.js +0 -91
  65. /package/{src → cli}/commands/shared.js +0 -0
  66. /package/{src → cli}/commands/single.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/models/write.js +0 -0
  91. /package/{src → cli}/utils/copy.js +0 -0
  92. /package/{src → cli}/utils/legacy-check.js +0 -0
  93. /package/{src → cli}/utils/models-cache.js +0 -0
  94. /package/{src → cli}/utils/update-manifest.js +0 -0
  95. /package/{src/content → harness}/.agents/skills/pc-guardrails-generic/SKILL.md +0 -0
  96. /package/{src/content → harness}/.agents/skills/pc-guardrails-project/SKILL.md +0 -0
  97. /package/{src/content → harness}/.agents/skills/pc-make-architecture/SKILL.md +0 -0
  98. /package/{src/content → harness}/.agents/skills/pc-make-architecture/structure-template.md +0 -0
  99. /package/{src/content → harness}/.agents/skills/pc-make-design/SKILL.md +0 -0
  100. /package/{src/content → harness}/.agents/skills/pc-make-engineer/signal-mapping.md +0 -0
  101. /package/{src/content → harness}/.agents/skills/pc-make-evidence-scaffold/SKILL.md +0 -0
  102. /package/{src/content → harness}/.agents/skills/pc-make-evidence-scaffold/evidence-contract.md +0 -0
  103. /package/{src/content → harness}/.agents/skills/pc-make-guardrails/SKILL.md +0 -0
  104. /package/{src/content → harness}/.agents/skills/pc-make-guardrails/category-reference.md +0 -0
  105. /package/{src/content → harness}/.agents/skills/pc-make-merge-risk-assess/SKILL.md +0 -0
  106. /package/{src/content → harness}/.agents/skills/pc-make-merge-risk-assess/category-reference.md +0 -0
  107. /package/{src/content → harness}/.agents/skills/pc-make-user-model/SKILL.md +0 -0
  108. /package/{src/content → harness}/.agents/skills/pc-ops-evidence/SKILL.md +0 -0
  109. /package/{src/content → harness}/.agents/skills/pc-ops-ship/SKILL.md +0 -0
  110. /package/{src/content → harness}/.agents/skills/pc-plan-apply/SKILL.md +0 -0
  111. /package/{src/content → harness}/.agents/skills/pc-plan-apply/simple-mode.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,95 +1,95 @@
1
- import fse from 'fs-extra'
2
- import path from 'path'
3
- import { fileURLToPath } from 'url'
4
- import { success } from '../../utils/exec.js'
5
-
6
- const __dirname = path.dirname(fileURLToPath(import.meta.url))
7
-
8
- const _archive = {
9
- azure: await fse.readFile(path.resolve(__dirname, '../../fragments/archive/az.md'), 'utf-8'),
10
- github: await fse.readFile(path.resolve(__dirname, '../../fragments/archive/gh.md'), 'utf-8'),
11
- gitlab: await fse.readFile(path.resolve(__dirname, '../../fragments/archive/gl.md'), 'utf-8'),
12
- none: await fse.readFile(path.resolve(__dirname, '../../fragments/archive/none.md'), 'utf-8'),
13
- }
14
-
15
- const _ship = {
16
- github: await fse.readFile(path.resolve(__dirname, '../../fragments/ops-ship/gh.md'), 'utf-8'),
17
- azure: await fse.readFile(path.resolve(__dirname, '../../fragments/ops-ship/az.md'), 'utf-8'),
18
- gitlab: await fse.readFile(path.resolve(__dirname, '../../fragments/ops-ship/gl.md'), 'utf-8'),
19
- }
20
-
21
- const _review = {
22
- github: await fse.readFile(path.resolve(__dirname, '../../fragments/ops-review/gh.md'), 'utf-8'),
23
- azure: await fse.readFile(path.resolve(__dirname, '../../fragments/ops-review/az.md'), 'utf-8'),
24
- gitlab: await fse.readFile(path.resolve(__dirname, '../../fragments/ops-review/gl.md'), 'utf-8'),
25
- }
26
-
27
- const _backlog = {
28
- github: await fse.readFile(path.resolve(__dirname, '../../fragments/ops-backlog/gh.md'), 'utf-8'),
29
- azure: await fse.readFile(path.resolve(__dirname, '../../fragments/ops-backlog/az.md'), 'utf-8'),
30
- jira: await fse.readFile(path.resolve(__dirname, '../../fragments/ops-backlog/jira.md'), 'utf-8'),
31
- }
32
-
33
- // Evidence comments target the backlog platform (where the issue/work-item lives).
34
- const _evidence = {
35
- github: await fse.readFile(path.resolve(__dirname, '../../fragments/ops-evidence/gh.md'), 'utf-8'),
36
- azure: await fse.readFile(path.resolve(__dirname, '../../fragments/ops-evidence/az.md'), 'utf-8'),
37
- jira: await fse.readFile(path.resolve(__dirname, '../../fragments/ops-evidence/jira.md'), 'utf-8'),
38
- }
39
-
40
- // relativePath is POSIX-style relative to the project root; commands live in
41
- // .opencode/commands/, skill-backed procedures (plan-archive, ops-ship) in
42
- // .agents/skills/<name>/SKILL.md.
43
- function patchFile(relativePath, startMarker, endMarker, content, platform, cwd = process.cwd()) {
44
- const targetPath = path.join(cwd, ...relativePath.split('/'))
45
- if (!fse.pathExistsSync(targetPath)) return
46
-
47
- let fileContent = fse.readFileSync(targetPath, 'utf-8')
48
- if (!fileContent.includes(startMarker) || !fileContent.includes(endMarker)) return
49
-
50
- const pattern = new RegExp(`${startMarker.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}[\\s\\S]*?${endMarker.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`)
51
- // Replacer function, not a replacement string: the injected fragments contain
52
- // shell quoting like $'...' and a string replacement expands $' as "everything
53
- // after the match", silently truncating the command. That is how the ops-ship
54
- // screenshot comment lost its entire --body argument.
55
- fileContent = fileContent.replace(pattern, () => `${startMarker}\n${content.trim()}\n${endMarker}`)
56
- fse.writeFileSync(targetPath, `${fileContent.replace(/\s*$/, '')}\n`, 'utf-8')
57
- success(`${relativePath} content injected for platform: ${platform}`)
58
- }
59
-
60
- export async function patchArchiveCommand(platform, cwd = process.cwd()) {
61
- const repoPlatform = typeof platform === 'object' ? (platform.repoPlatform ?? platform.backlogPlatform ?? 'github') : platform
62
- const replacement = _archive[repoPlatform]
63
- if (!replacement) return
64
- patchFile('.agents/skills/pc-plan-archive/SKILL.md', '<!-- PC-PLATFORM-ARCHIVE-START -->', '<!-- PC-PLATFORM-ARCHIVE-END -->', replacement, repoPlatform, cwd)
65
- }
66
-
67
- export async function patchOpsShip(platform, cwd = process.cwd()) {
68
- const repoPlatform = typeof platform === 'object' ? (platform.repoPlatform ?? 'github') : platform
69
- const replacement = _ship[repoPlatform]
70
- if (!replacement) return
71
- patchFile('.agents/skills/pc-ops-ship/SKILL.md', '<!-- PC-PLATFORM-SHIP-START -->', '<!-- PC-PLATFORM-SHIP-END -->', replacement, repoPlatform, cwd)
72
- }
73
-
74
- export async function patchOpsReview(platform, cwd = process.cwd()) {
75
- const repoPlatform = typeof platform === 'object' ? (platform.repoPlatform ?? 'github') : platform
76
- const replacement = _review[repoPlatform]
77
- if (!replacement) return
78
- patchFile('.opencode/commands/ops-review.md', '<!-- PC-PLATFORM-REVIEW-START -->', '<!-- PC-PLATFORM-REVIEW-END -->', replacement, repoPlatform, cwd)
79
- }
80
-
81
- export async function patchOpsBacklog(platform, cwd = process.cwd()) {
82
- const backlogPlatform = typeof platform === 'object' ? (platform.backlogPlatform ?? 'github') : platform
83
- const replacement = _backlog[backlogPlatform]
84
- if (!replacement) return
85
- patchFile('.opencode/commands/ops-backlog.md', '<!-- PC-PLATFORM-BACKLOG-START -->', '<!-- PC-PLATFORM-BACKLOG-END -->', replacement, backlogPlatform, cwd)
86
- }
87
-
88
- export async function patchOpsEvidence(platform, cwd = process.cwd()) {
89
- const backlogPlatform = typeof platform === 'object' ? (platform.backlogPlatform ?? 'github') : platform
90
- const replacement = _evidence[backlogPlatform]
91
- // browser/none have no CLI to post with: leave the markers empty (the skill skips commenting).
92
- if (!replacement) return
93
- patchFile('.agents/skills/pc-ops-evidence/SKILL.md', '<!-- PC-PLATFORM-EVIDENCE-START -->', '<!-- PC-PLATFORM-EVIDENCE-END -->', replacement, backlogPlatform, cwd)
94
- }
95
-
1
+ import fse from 'fs-extra'
2
+ import path from 'path'
3
+ import { fileURLToPath } from 'url'
4
+ import { success } from '../../utils/exec.js'
5
+
6
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
7
+
8
+ const _archive = {
9
+ azure: await fse.readFile(path.resolve(__dirname, '../../fragments/archive/az.md'), 'utf-8'),
10
+ github: await fse.readFile(path.resolve(__dirname, '../../fragments/archive/gh.md'), 'utf-8'),
11
+ gitlab: await fse.readFile(path.resolve(__dirname, '../../fragments/archive/gl.md'), 'utf-8'),
12
+ none: await fse.readFile(path.resolve(__dirname, '../../fragments/archive/none.md'), 'utf-8'),
13
+ }
14
+
15
+ const _ship = {
16
+ github: await fse.readFile(path.resolve(__dirname, '../../fragments/ops-ship/gh.md'), 'utf-8'),
17
+ azure: await fse.readFile(path.resolve(__dirname, '../../fragments/ops-ship/az.md'), 'utf-8'),
18
+ gitlab: await fse.readFile(path.resolve(__dirname, '../../fragments/ops-ship/gl.md'), 'utf-8'),
19
+ }
20
+
21
+ const _review = {
22
+ github: await fse.readFile(path.resolve(__dirname, '../../fragments/ops-review/gh.md'), 'utf-8'),
23
+ azure: await fse.readFile(path.resolve(__dirname, '../../fragments/ops-review/az.md'), 'utf-8'),
24
+ gitlab: await fse.readFile(path.resolve(__dirname, '../../fragments/ops-review/gl.md'), 'utf-8'),
25
+ }
26
+
27
+ const _backlog = {
28
+ github: await fse.readFile(path.resolve(__dirname, '../../fragments/ops-backlog/gh.md'), 'utf-8'),
29
+ azure: await fse.readFile(path.resolve(__dirname, '../../fragments/ops-backlog/az.md'), 'utf-8'),
30
+ jira: await fse.readFile(path.resolve(__dirname, '../../fragments/ops-backlog/jira.md'), 'utf-8'),
31
+ }
32
+
33
+ // Evidence comments target the backlog platform (where the issue/work-item lives).
34
+ const _evidence = {
35
+ github: await fse.readFile(path.resolve(__dirname, '../../fragments/ops-evidence/gh.md'), 'utf-8'),
36
+ azure: await fse.readFile(path.resolve(__dirname, '../../fragments/ops-evidence/az.md'), 'utf-8'),
37
+ jira: await fse.readFile(path.resolve(__dirname, '../../fragments/ops-evidence/jira.md'), 'utf-8'),
38
+ }
39
+
40
+ // relativePath is POSIX-style relative to the project root; commands live in
41
+ // .opencode/commands/, skill-backed procedures (plan-archive, ops-ship) in
42
+ // .agents/skills/<name>/SKILL.md.
43
+ function patchFile(relativePath, startMarker, endMarker, content, platform, cwd = process.cwd()) {
44
+ const targetPath = path.join(cwd, ...relativePath.split('/'))
45
+ if (!fse.pathExistsSync(targetPath)) return
46
+
47
+ let fileContent = fse.readFileSync(targetPath, 'utf-8')
48
+ if (!fileContent.includes(startMarker) || !fileContent.includes(endMarker)) return
49
+
50
+ const pattern = new RegExp(`${startMarker.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}[\\s\\S]*?${endMarker.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`)
51
+ // Replacer function, not a replacement string: the injected fragments contain
52
+ // shell quoting like $'...' and a string replacement expands $' as "everything
53
+ // after the match", silently truncating the command. That is how the ops-ship
54
+ // screenshot comment lost its entire --body argument.
55
+ fileContent = fileContent.replace(pattern, () => `${startMarker}\n${content.trim()}\n${endMarker}`)
56
+ fse.writeFileSync(targetPath, `${fileContent.replace(/\s*$/, '')}\n`, 'utf-8')
57
+ success(`${relativePath} content injected for platform: ${platform}`)
58
+ }
59
+
60
+ export async function patchArchiveCommand(platform, cwd = process.cwd()) {
61
+ const repoPlatform = typeof platform === 'object' ? (platform.repoPlatform ?? platform.backlogPlatform ?? 'github') : platform
62
+ const replacement = _archive[repoPlatform]
63
+ if (!replacement) return
64
+ patchFile('.agents/skills/pc-plan-archive/SKILL.md', '<!-- PC-PLATFORM-ARCHIVE-START -->', '<!-- PC-PLATFORM-ARCHIVE-END -->', replacement, repoPlatform, cwd)
65
+ }
66
+
67
+ export async function patchOpsShip(platform, cwd = process.cwd()) {
68
+ const repoPlatform = typeof platform === 'object' ? (platform.repoPlatform ?? 'github') : platform
69
+ const replacement = _ship[repoPlatform]
70
+ if (!replacement) return
71
+ patchFile('.agents/skills/pc-ops-ship/SKILL.md', '<!-- PC-PLATFORM-SHIP-START -->', '<!-- PC-PLATFORM-SHIP-END -->', replacement, repoPlatform, cwd)
72
+ }
73
+
74
+ export async function patchOpsReview(platform, cwd = process.cwd()) {
75
+ const repoPlatform = typeof platform === 'object' ? (platform.repoPlatform ?? 'github') : platform
76
+ const replacement = _review[repoPlatform]
77
+ if (!replacement) return
78
+ patchFile('.opencode/commands/ops-review.md', '<!-- PC-PLATFORM-REVIEW-START -->', '<!-- PC-PLATFORM-REVIEW-END -->', replacement, repoPlatform, cwd)
79
+ }
80
+
81
+ export async function patchOpsBacklog(platform, cwd = process.cwd()) {
82
+ const backlogPlatform = typeof platform === 'object' ? (platform.backlogPlatform ?? 'github') : platform
83
+ const replacement = _backlog[backlogPlatform]
84
+ if (!replacement) return
85
+ patchFile('.opencode/commands/ops-backlog.md', '<!-- PC-PLATFORM-BACKLOG-START -->', '<!-- PC-PLATFORM-BACKLOG-END -->', replacement, backlogPlatform, cwd)
86
+ }
87
+
88
+ export async function patchOpsEvidence(platform, cwd = process.cwd()) {
89
+ const backlogPlatform = typeof platform === 'object' ? (platform.backlogPlatform ?? 'github') : platform
90
+ const replacement = _evidence[backlogPlatform]
91
+ // browser/none have no CLI to post with: leave the markers empty (the skill skips commenting).
92
+ if (!replacement) return
93
+ patchFile('.agents/skills/pc-ops-evidence/SKILL.md', '<!-- PC-PLATFORM-EVIDENCE-START -->', '<!-- PC-PLATFORM-EVIDENCE-END -->', replacement, backlogPlatform, cwd)
94
+ }
95
+
@@ -1,6 +1,7 @@
1
1
  import fse from 'fs-extra'
2
2
  import path from 'path'
3
3
  import { success } from '../../utils/exec.js'
4
+ import { agentColor, yamlColor } from '../../utils/agent-color.js'
4
5
 
5
6
  const FULLSTACK_FILE = 'fullstack-engineer.md'
6
7
  const FULLSTACK_DESCRIPTION = 'Default engineer that accumulates skills from all created persona engineers. Use as fallback when no specialist matches: but prefer spawning a specific engineer for deterministic results.'
@@ -59,7 +60,10 @@ export async function generateFullstackEngineer({ cwd = process.cwd(), updateMod
59
60
  // subagent, not primary: build.md and plan.md are the primaries, and
60
61
  // pc-subagent-tiers generates both from this file on every startup.
61
62
  'mode: subagent',
62
- 'color: warning',
63
+ // warning is reserved for plan, so fullstack takes its name-derived hex
64
+ // like every other subagent.
65
+ // Quoted: a bare #hex is a YAML comment, which opencode rejects.
66
+ `color: ${yamlColor(agentColor('fullstack-engineer'))}`,
63
67
  'permission:',
64
68
  ' edit: allow',
65
69
  ' bash: allow',
@@ -11,7 +11,7 @@ import { generateFullstackEngineer, removeLegacyStartupDirectives } from "./full
11
11
  import { patchOpencodeJson, patchOpencodePackage } from "./opencode-json.js"
12
12
 
13
13
  const __dirname = path.dirname(fileURLToPath(import.meta.url))
14
- const CONTENT_DIR = path.resolve(__dirname, "../../content")
14
+ const CONTENT_DIR = path.resolve(__dirname, "../../../harness")
15
15
 
16
16
  export async function copyContentStep(platform, ctx = {}) {
17
17
  header("Step 5, Copying agent-harness files")
@@ -10,6 +10,15 @@ const SHARED_PERMISSIONS = [
10
10
  ['todowrite', 'allow'],
11
11
  ]
12
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
+
13
22
  function applyModify(text, jsonPath, value) {
14
23
  const edits = modify(text, jsonPath, value, {
15
24
  formattingOptions: { insertSpaces: true, tabSize: 2 },
@@ -47,6 +56,14 @@ export async function patchOpencodeJson(cwd = process.cwd()) {
47
56
  parsed?.agent?.plan?.mode === 'primary' &&
48
57
  parsed?.agent?.plan?.permission?.edit === 'deny'
49
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'
50
67
  const needsSkillPermission = parsed?.permission?.skill !== 'allow'
51
68
  const missingSharedPermissions = SHARED_PERMISSIONS.filter(
52
69
  ([key, value]) => parsed?.permission?.[key] !== value,
@@ -60,8 +77,11 @@ export async function patchOpencodeJson(cwd = process.cwd()) {
60
77
  parsed?.compaction?.auto === true &&
61
78
  parsed?.compaction?.prune === true
62
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)
63
83
 
64
- if (!needsAgentOverride && !needsSkillPermission && missingSharedPermissions.length === 0 && !needsSkillsPaths && !needsCompaction) {
84
+ if (!needsAgentOverride && !needsDefaultAgent && !needsSkillPermission && missingSharedPermissions.length === 0 && !needsSkillsPaths && !needsCompaction && staleBrowserPlugins.length === 0 && !needsBrowserMcp) {
65
85
  return { patched: false }
66
86
  }
67
87
 
@@ -78,6 +98,9 @@ export async function patchOpencodeJson(cwd = process.cwd()) {
78
98
  text = applyModify(text, ['agent', 'plan', 'mode'], 'primary')
79
99
  text = applyModify(text, ['agent', 'plan', 'permission', 'edit'], 'deny')
80
100
  }
101
+ if (needsDefaultAgent) {
102
+ text = applyModify(text, ['default_agent'], 'plan')
103
+ }
81
104
  if (needsSkillPermission) text = applyModify(text, ['permission', 'skill'], 'allow')
82
105
  for (const [key, value] of missingSharedPermissions) {
83
106
  text = applyModify(text, ['permission', key], value)
@@ -99,10 +122,22 @@ export async function patchOpencodeJson(cwd = process.cwd()) {
99
122
  text = applyModify(text, ['compaction', 'reserved'], 10000)
100
123
  }
101
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
+
102
134
  await fse.writeFile(opencodePath, text, 'utf-8')
103
135
  if (needsAgentOverride) {
104
136
  success('Set build/plan as the primary agents in opencode.jsonc (plan is read-only)')
105
137
  }
138
+ if (needsDefaultAgent) {
139
+ success(`default_agent -> plan${currentDefault ? ` (was ${currentDefault})` : ''}`)
140
+ }
106
141
  if (needsSkillPermission) {
107
142
  success('Allowed skill loading in opencode.jsonc')
108
143
  }
@@ -115,6 +150,12 @@ export async function patchOpencodeJson(cwd = process.cwd()) {
115
150
  if (needsCompaction) {
116
151
  success('Enabled context pruning (compaction.prune) in opencode.jsonc')
117
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
+ }
118
159
 
119
160
  return { patched: true }
120
161
  }
@@ -126,6 +167,9 @@ const OPENCODE_PACKAGE_DEPENDENCIES = {
126
167
  'solid-js': '1.9.12',
127
168
  }
128
169
 
170
+ // agent-browser replaced this plugin; remove it from consumer installs on update
171
+ const STALE_PACKAGE_DEPENDENCIES = ['@different-ai/opencode-browser']
172
+
129
173
  export async function patchOpencodePackage(cwd = process.cwd()) {
130
174
  const packagePath = path.join(cwd, '.opencode', 'package.json')
131
175
  if (!await fse.pathExists(packagePath)) return { patched: false }
@@ -139,6 +183,11 @@ export async function patchOpencodePackage(cwd = process.cwd()) {
139
183
  packageJson.dependencies[name] = version
140
184
  patched = true
141
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
+ }
142
191
  if (!patched) return { patched: false }
143
192
 
144
193
  await fse.writeJson(packagePath, packageJson, { spaces: 2 })
@@ -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
@@ -59,6 +59,10 @@ export async function writeHarnessConfig(data) {
59
59
  ),
60
60
  codegraph: !!(optionalTools.codegraph?.optedIn ?? optionalTools.codegraph),
61
61
  memory: !!(optionalTools.memory?.optedIn ?? optionalTools.memory),
62
+ // humanizer was missing here, so a selection never survived a metadata
63
+ // write and patchGuardrails always saw it as off, leaving its marker
64
+ // section empty however the project was actually set up.
65
+ humanizer: !!(optionalTools.humanizer?.optedIn ?? optionalTools.humanizer),
62
66
  }
63
67
 
64
68
  const payload = {
@@ -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 };