@plainconceptsplatform/agent-harness 2.0.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 (151) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +419 -0
  3. package/package.json +68 -0
  4. package/src/commands/join.js +244 -0
  5. package/src/commands/shared.js +27 -0
  6. package/src/commands/single.js +79 -0
  7. package/src/commands/update.js +109 -0
  8. package/src/commands/wizard.js +134 -0
  9. package/src/content/.agents/skills/browser-automation/SKILL.md +66 -0
  10. package/src/content/.agents/skills/pc-guardrails-generic/SKILL.md +68 -0
  11. package/src/content/.agents/skills/pc-guardrails-project/SKILL.md +8 -0
  12. package/src/content/.agents/skills/pc-make-architecture/SKILL.md +51 -0
  13. package/src/content/.agents/skills/pc-make-architecture/structure-template.md +38 -0
  14. package/src/content/.agents/skills/pc-make-design/SKILL.md +68 -0
  15. package/src/content/.agents/skills/pc-make-engineer/SKILL.md +219 -0
  16. package/src/content/.agents/skills/pc-make-engineer/signal-mapping.md +68 -0
  17. package/src/content/.agents/skills/pc-make-engineer/template.md +81 -0
  18. package/src/content/.agents/skills/pc-make-evidence-scaffold/SKILL.md +18 -0
  19. package/src/content/.agents/skills/pc-make-evidence-scaffold/evidence-contract.md +29 -0
  20. package/src/content/.agents/skills/pc-make-guardrails/SKILL.md +74 -0
  21. package/src/content/.agents/skills/pc-make-guardrails/category-reference.md +68 -0
  22. package/src/content/.agents/skills/pc-make-merge-risk-assess/SKILL.md +70 -0
  23. package/src/content/.agents/skills/pc-make-merge-risk-assess/category-reference.md +98 -0
  24. package/src/content/.agents/skills/pc-make-user-model/SKILL.md +66 -0
  25. package/src/content/.agents/skills/pc-ops-evidence/SKILL.md +127 -0
  26. package/src/content/.agents/skills/pc-ops-ship/SKILL.md +18 -0
  27. package/src/content/.agents/skills/pc-plan-apply/SKILL.md +83 -0
  28. package/src/content/.agents/skills/pc-plan-apply/simple-mode.md +21 -0
  29. package/src/content/.agents/skills/pc-plan-archive/SKILL.md +63 -0
  30. package/src/content/.agents/skills/pc-plan-explore/SKILL.md +9 -0
  31. package/src/content/.agents/skills/pc-plan-goal/SKILL.md +94 -0
  32. package/src/content/.agents/skills/pc-plan-goal/branching.md +30 -0
  33. package/src/content/.agents/skills/pc-plan-goal/failure-policy.md +30 -0
  34. package/src/content/.agents/skills/pc-plan-goal/output-mode.md +9 -0
  35. package/src/content/.agents/skills/pc-plan-goal/output.md +68 -0
  36. package/src/content/.agents/skills/pc-plan-propose/SKILL.md +125 -0
  37. package/src/content/.agents/skills/pc-plan-propose/task-annotation.md +39 -0
  38. package/src/content/.agents/skills/pc-plan-quick/SKILL.md +62 -0
  39. package/src/content/.agents/skills/pc-plan-story/SKILL.md +146 -0
  40. package/src/content/.agents/skills/pc-repo-audit/SKILL.md +44 -0
  41. package/src/content/.agents/skills/pc-repo-help/SKILL.md +91 -0
  42. package/src/content/.agents/skills/pc-repo-initialize/SKILL.md +130 -0
  43. package/src/content/.agents/skills/pc-repo-onboard/SKILL.md +87 -0
  44. package/src/content/.agents/skills/pc-repo-verify/SKILL.md +34 -0
  45. package/src/content/.agents/skills/pc-userstory-az/SKILL.md +157 -0
  46. package/src/content/.agents/skills/pc-userstory-browser/SKILL.md +132 -0
  47. package/src/content/.agents/skills/pc-userstory-gh/SKILL.md +120 -0
  48. package/src/content/.agents/skills/pc-userstory-jira/SKILL.md +131 -0
  49. package/src/content/.opencode/_gitignore +7 -0
  50. package/src/content/.opencode/commands/init.md +5 -0
  51. package/src/content/.opencode/commands/make-architecture.md +5 -0
  52. package/src/content/.opencode/commands/make-design.md +5 -0
  53. package/src/content/.opencode/commands/make-engineer.md +5 -0
  54. package/src/content/.opencode/commands/make-evidence-scaffold.md +5 -0
  55. package/src/content/.opencode/commands/make-guardrails.md +5 -0
  56. package/src/content/.opencode/commands/make-user-model.md +5 -0
  57. package/src/content/.opencode/commands/ops-backlog.md +10 -0
  58. package/src/content/.opencode/commands/ops-evidence.md +9 -0
  59. package/src/content/.opencode/commands/ops-review.md +8 -0
  60. package/src/content/.opencode/commands/ops-ship.md +9 -0
  61. package/src/content/.opencode/commands/plan-apply.md +9 -0
  62. package/src/content/.opencode/commands/plan-archive.md +5 -0
  63. package/src/content/.opencode/commands/plan-explore.md +9 -0
  64. package/src/content/.opencode/commands/plan-goal.md +5 -0
  65. package/src/content/.opencode/commands/plan-propose.md +9 -0
  66. package/src/content/.opencode/commands/plan-quick.md +5 -0
  67. package/src/content/.opencode/commands/plan-story.md +9 -0
  68. package/src/content/.opencode/commands/repo-audit.md +5 -0
  69. package/src/content/.opencode/commands/repo-help.md +5 -0
  70. package/src/content/.opencode/commands/repo-initialize.md +5 -0
  71. package/src/content/.opencode/commands/repo-onboard.md +5 -0
  72. package/src/content/.opencode/commands/repo-verify.md +5 -0
  73. package/src/content/.opencode/package.json +10 -0
  74. package/src/content/.opencode/plugins/pc-subagent-monitor.js +139 -0
  75. package/src/content/.opencode/plugins/pc-subagent-tiers.js +179 -0
  76. package/src/content/.opencode/plugins/pc-system-reminders.js +96 -0
  77. package/src/content/.opencode/plugins/pc-system-reminders.test.js +35 -0
  78. package/src/content/.opencode/tui/pc-subagents.tsx +98 -0
  79. package/src/content/.opencode/tui.json +6 -0
  80. package/src/content/AGENTS.md +71 -0
  81. package/src/content/ARCHITECTURE.md +16 -0
  82. package/src/content/DESIGN.md +16 -0
  83. package/src/content/opencode.jsonc +31 -0
  84. package/src/content/openspec/changes/archive/.gitkeep +0 -0
  85. package/src/content/openspec/config.yaml +20 -0
  86. package/src/content/openspec/specs/.gitkeep +0 -0
  87. package/src/content/skills-lock.json +17 -0
  88. package/src/fragments/archive/az.md +95 -0
  89. package/src/fragments/archive/gh.md +94 -0
  90. package/src/fragments/archive/gl.md +94 -0
  91. package/src/fragments/archive/none.md +73 -0
  92. package/src/fragments/guardrails/codegraph.md +7 -0
  93. package/src/fragments/guardrails/humanizer.md +4 -0
  94. package/src/fragments/guardrails/memory.md +4 -0
  95. package/src/fragments/guardrails/rtk.md +3 -0
  96. package/src/fragments/guardrails/simple-english.md +4 -0
  97. package/src/fragments/ops-backlog/az.md +29 -0
  98. package/src/fragments/ops-backlog/gh.md +30 -0
  99. package/src/fragments/ops-backlog/jira.md +29 -0
  100. package/src/fragments/ops-evidence/az.md +41 -0
  101. package/src/fragments/ops-evidence/gh.md +53 -0
  102. package/src/fragments/ops-evidence/jira.md +38 -0
  103. package/src/fragments/ops-review/az.md +63 -0
  104. package/src/fragments/ops-review/gh.md +53 -0
  105. package/src/fragments/ops-review/gl.md +57 -0
  106. package/src/fragments/ops-ship/az.md +81 -0
  107. package/src/fragments/ops-ship/gh.md +69 -0
  108. package/src/fragments/ops-ship/gl.md +86 -0
  109. package/src/index.js +107 -0
  110. package/src/presets/agents-content.json +53 -0
  111. package/src/presets/browser.json +22 -0
  112. package/src/presets/clean.json +21 -0
  113. package/src/presets/models.json +68 -0
  114. package/src/presets/openspec.json +1 -0
  115. package/src/presets/optimization.json +37 -0
  116. package/src/presets/platforms.json +76 -0
  117. package/src/presets/quota.json +16 -0
  118. package/src/presets/source.json +23 -0
  119. package/src/steps/browser/index.js +91 -0
  120. package/src/steps/clean/index.js +120 -0
  121. package/src/steps/copy/agents.js +118 -0
  122. package/src/steps/copy/commands.js +91 -0
  123. package/src/steps/copy/fullstack-engineer.js +83 -0
  124. package/src/steps/copy/index.js +88 -0
  125. package/src/steps/copy/opencode-json.js +129 -0
  126. package/src/steps/copy/skills.js +196 -0
  127. package/src/steps/metadata/index.js +108 -0
  128. package/src/steps/models/format.js +88 -0
  129. package/src/steps/models/index.js +64 -0
  130. package/src/steps/models/write.js +34 -0
  131. package/src/steps/openspec/index.js +136 -0
  132. package/src/steps/optimization/codegraph.js +127 -0
  133. package/src/steps/optimization/humanizer.js +17 -0
  134. package/src/steps/optimization/index.js +163 -0
  135. package/src/steps/optimization/memory.js +88 -0
  136. package/src/steps/optimization/patch-guardrails.js +108 -0
  137. package/src/steps/optimization/quota.js +119 -0
  138. package/src/steps/optimization/simple-english.js +17 -0
  139. package/src/steps/optimization/skills-lock.js +30 -0
  140. package/src/steps/platform/index.js +109 -0
  141. package/src/steps/source/index.js +123 -0
  142. package/src/utils/copy.js +108 -0
  143. package/src/utils/exec-spinner.js +47 -0
  144. package/src/utils/exec.js +134 -0
  145. package/src/utils/legacy-check.js +30 -0
  146. package/src/utils/models-cache.js +58 -0
  147. package/src/utils/models-pricing.js +42 -0
  148. package/src/utils/paths.js +64 -0
  149. package/src/utils/process.js +3 -0
  150. package/src/utils/terminal.js +6 -0
  151. package/src/utils/update-manifest.js +49 -0
@@ -0,0 +1,118 @@
1
+ import fse from 'fs-extra'
2
+ import path from 'path'
3
+ import { fileURLToPath } from 'url'
4
+ import { info, success, warn } from '../../utils/exec.js'
5
+
6
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
7
+ const agentsContent = await fse.readJson(path.resolve(__dirname, '../../presets/agents-content.json'))
8
+
9
+ // Steps are matched by their title text, not by exact heading string:
10
+ // heading level (###/####) and step numbers have drifted before and silently
11
+ // broke removal. Matching `^#{3,4} Step N, <title>` survives both.
12
+ const HISTORY_STEP_TITLE = 'Archive project history'
13
+ const CHAIN_STEP_TITLE = 'Chain make commands'
14
+
15
+ const CHAIN_CONFIRM_LINE = '- ARCHITECTURE.md generated'
16
+
17
+ function stepHeadingPattern(title) {
18
+ const escaped = title.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
19
+ return new RegExp(`^#{2,4} Step \\d+, ${escaped}`)
20
+ }
21
+
22
+ // The step heading is kept and its body replaced with an explicit skip note.
23
+ // Removing the block and renumbering the remaining steps (the old approach)
24
+ // went stale against prose cross-references like "Skip steps 2, 3, and 4";
25
+ // stable numbering plus a visible reason is unambiguous for the agent.
26
+ export function skipStepBlock(content, title, note) {
27
+ const lines = content.split('\n')
28
+ const pattern = stepHeadingPattern(title)
29
+ const start = lines.findIndex(l => pattern.test(l.trim()))
30
+ if (start === -1) return { content, matched: false }
31
+
32
+ let end = lines.length
33
+ for (let i = start + 1; i < lines.length; i++) {
34
+ if (lines[i].trim() === '---') { end = i; break }
35
+ }
36
+
37
+ lines.splice(start + 1, end - start - 1, '', `> ${note}`, '')
38
+ return { content: lines.join('\n'), matched: true }
39
+ }
40
+
41
+ function removeConfirmLine(content, line) {
42
+ return content.split('\n').filter(l => l.trim() !== line.trim()).join('\n')
43
+ }
44
+
45
+ const PLATFORM_WORKFLOW_START = '<!-- PC-PLATFORM-WORKFLOW-START -->'
46
+ const PLATFORM_WORKFLOW_END = '<!-- PC-PLATFORM-WORKFLOW-END -->'
47
+ const PLATFORM_SKILLS_GUIDE_START = '<!-- PC-PLATFORM-SKILLS-GUIDE-START -->'
48
+ const PLATFORM_SKILLS_GUIDE_END = '<!-- PC-PLATFORM-SKILLS-GUIDE-END -->'
49
+
50
+ function platformContent(platform, key) {
51
+ const p = agentsContent.platform[platform] ?? agentsContent.platform.github
52
+ return p[key] ?? ''
53
+ }
54
+
55
+ function mixedPlatformContent(backlogPlatform, repoPlatform, key) {
56
+ if (backlogPlatform === repoPlatform) {
57
+ return platformContent(backlogPlatform, key)
58
+ }
59
+ const backlog = agentsContent.platform[backlogPlatform] ?? agentsContent.platform.github
60
+ const repo = agentsContent.platform[repoPlatform] ?? agentsContent.platform.github
61
+ const mixed = agentsContent.platform.mixed ?? {}
62
+ const mixedKey = `${backlogPlatform}-${repoPlatform}`
63
+ return mixed[mixedKey]?.[key] ?? mixed.default?.[key]?.replace('{backlog}', backlog[key] ?? '').replace('{repo}', repo[key] ?? '') ?? platformContent(repoPlatform, key)
64
+ }
65
+
66
+ function replaceBetween(content, start, end, replacement) {
67
+ if (!content.includes(start) || !content.includes(end)) return content
68
+ const pattern = new RegExp(`${start}[\\s\\S]*?${end}`)
69
+ return content.replace(pattern, `${start}\n${replacement.trim()}\n${end}`)
70
+ }
71
+
72
+ export async function patchAgentsMd(ctx) {
73
+ const obInitSkillPath = path.join(process.cwd(), '.agents', 'skills', 'pc-repo-initialize', 'SKILL.md')
74
+ if (!await fse.pathExists(obInitSkillPath)) return
75
+
76
+ let content = await fse.readFile(obInitSkillPath, 'utf-8')
77
+ const patches = []
78
+
79
+ const skips = [
80
+ [ctx.hasOpenspec, HISTORY_STEP_TITLE, null,
81
+ 'Skipped during onboarding: this project already had an openspec/ history. Do not archive again; continue with the next step.'],
82
+ [ctx.hasDesign || ctx.hasArchitecture, CHAIN_STEP_TITLE, CHAIN_CONFIRM_LINE,
83
+ 'Skipped during onboarding: project files already exist. Run /make-architecture or /make-design individually to regenerate.'],
84
+ ]
85
+
86
+ for (const [enabled, title, confirmLine, note] of skips) {
87
+ if (!enabled) continue
88
+ const result = skipStepBlock(content, title, note)
89
+ if (!result.matched) {
90
+ warn(`pc-repo-initialize SKILL.md step "${title}" not found: template drift? Skipping this patch.`)
91
+ continue
92
+ }
93
+ content = result.content
94
+ if (confirmLine) content = removeConfirmLine(content, confirmLine)
95
+ patches.push(`Step "${title}" marked as skipped, file already exists`)
96
+ }
97
+
98
+ if (patches.length > 0) {
99
+ await fse.writeFile(obInitSkillPath, content, 'utf-8')
100
+ for (const msg of patches) info(msg)
101
+ success('pc-repo-initialize SKILL.md patched for existing project state')
102
+ }
103
+ }
104
+
105
+ export async function patchAgentGuidance(backlogPlatform, repoPlatform, cwd = process.cwd()) {
106
+ const agentsMdPath = path.join(cwd, 'AGENTS.md')
107
+ if (await fse.pathExists(agentsMdPath)) {
108
+ const repo = repoPlatform ?? backlogPlatform ?? 'github'
109
+ let content = await fse.readFile(agentsMdPath, 'utf-8')
110
+ content = replaceBetween(content, PLATFORM_WORKFLOW_START, PLATFORM_WORKFLOW_END, mixedPlatformContent(backlogPlatform, repo, 'workflow'))
111
+ content = replaceBetween(content, PLATFORM_SKILLS_GUIDE_START, PLATFORM_SKILLS_GUIDE_END, mixedPlatformContent(backlogPlatform, repo, 'skillsGuide'))
112
+ await fse.writeFile(agentsMdPath, `${content.replace(/\s*$/, '')}\n`, 'utf-8')
113
+ const label = backlogPlatform === repo ? repo : `${backlogPlatform}→${repo}`
114
+ success(`AGENTS.md patched for platform workflow: ${label}`)
115
+ }
116
+ }
117
+
118
+
@@ -0,0 +1,91 @@
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
+ fileContent = fileContent.replace(pattern, `${startMarker}\n${content.trim()}\n${endMarker}`)
52
+ fse.writeFileSync(targetPath, `${fileContent.replace(/\s*$/, '')}\n`, 'utf-8')
53
+ success(`${relativePath} content injected for platform: ${platform}`)
54
+ }
55
+
56
+ export async function patchArchiveCommand(platform, cwd = process.cwd()) {
57
+ const repoPlatform = typeof platform === 'object' ? (platform.repoPlatform ?? platform.backlogPlatform ?? 'github') : platform
58
+ const replacement = _archive[repoPlatform]
59
+ if (!replacement) return
60
+ patchFile('.agents/skills/pc-plan-archive/SKILL.md', '<!-- PC-PLATFORM-ARCHIVE-START -->', '<!-- PC-PLATFORM-ARCHIVE-END -->', replacement, repoPlatform, cwd)
61
+ }
62
+
63
+ export async function patchOpsShip(platform, cwd = process.cwd()) {
64
+ const repoPlatform = typeof platform === 'object' ? (platform.repoPlatform ?? 'github') : platform
65
+ const replacement = _ship[repoPlatform]
66
+ if (!replacement) return
67
+ patchFile('.agents/skills/pc-ops-ship/SKILL.md', '<!-- PC-PLATFORM-SHIP-START -->', '<!-- PC-PLATFORM-SHIP-END -->', replacement, repoPlatform, cwd)
68
+ }
69
+
70
+ export async function patchOpsReview(platform, cwd = process.cwd()) {
71
+ const repoPlatform = typeof platform === 'object' ? (platform.repoPlatform ?? 'github') : platform
72
+ const replacement = _review[repoPlatform]
73
+ if (!replacement) return
74
+ patchFile('.opencode/commands/ops-review.md', '<!-- PC-PLATFORM-REVIEW-START -->', '<!-- PC-PLATFORM-REVIEW-END -->', replacement, repoPlatform, cwd)
75
+ }
76
+
77
+ export async function patchOpsBacklog(platform, cwd = process.cwd()) {
78
+ const backlogPlatform = typeof platform === 'object' ? (platform.backlogPlatform ?? 'github') : platform
79
+ const replacement = _backlog[backlogPlatform]
80
+ if (!replacement) return
81
+ patchFile('.opencode/commands/ops-backlog.md', '<!-- PC-PLATFORM-BACKLOG-START -->', '<!-- PC-PLATFORM-BACKLOG-END -->', replacement, backlogPlatform, cwd)
82
+ }
83
+
84
+ export async function patchOpsEvidence(platform, cwd = process.cwd()) {
85
+ const backlogPlatform = typeof platform === 'object' ? (platform.backlogPlatform ?? 'github') : platform
86
+ const replacement = _evidence[backlogPlatform]
87
+ // browser/none have no CLI to post with: leave the markers empty (the skill skips commenting).
88
+ if (!replacement) return
89
+ patchFile('.agents/skills/pc-ops-evidence/SKILL.md', '<!-- PC-PLATFORM-EVIDENCE-START -->', '<!-- PC-PLATFORM-EVIDENCE-END -->', replacement, backlogPlatform, cwd)
90
+ }
91
+
@@ -0,0 +1,83 @@
1
+ import fse from 'fs-extra'
2
+ import path from 'path'
3
+ import { success } from '../../utils/exec.js'
4
+
5
+ const FULLSTACK_FILE = 'fullstack-engineer.md'
6
+ 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.'
7
+ const FULLSTACK_IDENTITY = 'You are the default engineer, mostly used by the user for architecture and planning. You are more complete but less accurate than specialized engineers, prefer spawning a specialist when one matches the task domain.'
8
+
9
+ // The reminder plugin owns ability bootstrap. Remove the old prompt-level
10
+ // directive when regenerating so it cannot duplicate the plugin reminder.
11
+ function stripStartupDirective(text) {
12
+ if (!text) return text
13
+ return text
14
+ .split(/\r?\n\r?\n+/)
15
+ .filter(paragraph => !paragraph.trim().startsWith('**Startup'))
16
+ .join('\n\n')
17
+ .trim()
18
+ }
19
+
20
+ export async function removeLegacyStartupDirectives({ cwd = process.cwd() } = {}) {
21
+ const agentsDir = path.join(cwd, '.opencode', 'agents')
22
+ if (!await fse.pathExists(agentsDir)) return 0
23
+
24
+ let updated = 0
25
+ for (const fileName of await fse.readdir(agentsDir)) {
26
+ if (!fileName.endsWith('.md')) continue
27
+ const filePath = path.join(agentsDir, fileName)
28
+ const content = await fse.readFile(filePath, 'utf-8')
29
+ const cleaned = stripStartupDirective(content)
30
+ if (cleaned === content.trim()) continue
31
+ await fse.writeFile(filePath, `${cleaned}\n`, 'utf-8')
32
+ updated++
33
+ }
34
+ return updated
35
+ }
36
+
37
+ export async function generateFullstackEngineer({ cwd = process.cwd(), updateMode = false } = {}) {
38
+ const agentsDir = path.join(cwd, '.opencode', 'agents')
39
+ await fse.ensureDir(agentsDir)
40
+ const filePath = path.join(agentsDir, FULLSTACK_FILE)
41
+
42
+ if (updateMode && await fse.pathExists(filePath)) {
43
+ return { generated: false, preserved: true }
44
+ }
45
+
46
+ let existingAbilities = null
47
+ let existingIdentity = null
48
+ if (await fse.pathExists(filePath)) {
49
+ const content = await fse.readFile(filePath, 'utf-8')
50
+ const abilitiesMatch = content.match(/## Abilities\n([\s\S]*?)$/)
51
+ if (abilitiesMatch) existingAbilities = abilitiesMatch[1].trim()
52
+ const identityMatch = content.match(/^---\n[\s\S]*?\n---\n\n(.*?)(?:\n\n## Abilities)/s)
53
+ if (identityMatch) existingIdentity = stripStartupDirective(identityMatch[1])
54
+ }
55
+
56
+ const frontmatter = [
57
+ '---',
58
+ `description: ${FULLSTACK_DESCRIPTION}`,
59
+ 'mode: primary',
60
+ 'color: warning',
61
+ 'permission:',
62
+ ' edit: allow',
63
+ ' bash: allow',
64
+ ' read: allow',
65
+ ' glob: allow',
66
+ ' grep: allow',
67
+ ' question: allow',
68
+ ' todowrite: allow',
69
+ '---',
70
+ ]
71
+
72
+ const identity = existingIdentity ?? FULLSTACK_IDENTITY
73
+
74
+ const abilities = existingAbilities ?? [
75
+ '- Guardrails: @pc-guardrails-generic, @pc-guardrails-project',
76
+ ].join('\n')
77
+
78
+ const content = `${frontmatter.join('\n')}\n\n${identity}\n\n## Abilities\n${abilities}\n`
79
+
80
+ await fse.writeFile(filePath, content, 'utf-8')
81
+ success(`Generated ${FULLSTACK_FILE}`)
82
+ return { generated: true }
83
+ }
@@ -0,0 +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
+ }
@@ -0,0 +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
+ }