@plainconceptsplatform/agent-harness 3.2.0 → 3.3.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 (141) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +454 -452
  3. package/cli/commands/join.js +259 -259
  4. package/cli/commands/migrate.js +310 -310
  5. package/cli/commands/shared.js +27 -27
  6. package/cli/commands/single.js +79 -79
  7. package/cli/commands/update.js +121 -121
  8. package/cli/commands/wizard.js +134 -134
  9. package/cli/fragments/archive/az.md +97 -97
  10. package/cli/fragments/archive/gh.md +96 -96
  11. package/cli/fragments/archive/gl.md +96 -96
  12. package/cli/fragments/archive/none.md +75 -75
  13. package/cli/fragments/guardrails/codegraph.md +5 -5
  14. package/cli/fragments/guardrails/humanizer.md +4 -4
  15. package/cli/fragments/guardrails/memory.md +4 -4
  16. package/cli/fragments/guardrails/rtk.md +3 -3
  17. package/cli/fragments/guardrails/simple-english.md +4 -4
  18. package/cli/fragments/ops-backlog/az.md +28 -28
  19. package/cli/fragments/ops-backlog/gh.md +28 -28
  20. package/cli/fragments/ops-backlog/jira.md +28 -28
  21. package/cli/fragments/ops-review/az.md +62 -62
  22. package/cli/fragments/ops-review/gh.md +51 -51
  23. package/cli/fragments/ops-review/gl.md +55 -55
  24. package/cli/fragments/ops-ship/az.md +81 -81
  25. package/cli/fragments/ops-ship/gh.md +67 -67
  26. package/cli/fragments/ops-ship/gl.md +84 -84
  27. package/cli/index.js +116 -116
  28. package/cli/presets/agents-content.json +34 -34
  29. package/cli/presets/browser.json +10 -10
  30. package/cli/presets/clean.json +21 -21
  31. package/cli/presets/models.json +68 -68
  32. package/cli/presets/optimization.json +32 -32
  33. package/cli/presets/platforms.json +76 -76
  34. package/cli/presets/source.json +23 -23
  35. package/cli/steps/browser/index.js +48 -48
  36. package/cli/steps/clean/index.js +120 -120
  37. package/cli/steps/copy/agents.js +120 -120
  38. package/cli/steps/copy/commands.js +79 -79
  39. package/cli/steps/copy/fullstack-engineer.js +89 -89
  40. package/cli/steps/copy/index.js +88 -88
  41. package/cli/steps/copy/opencode-json.js +324 -324
  42. package/cli/steps/copy/skills.js +316 -307
  43. package/cli/steps/metadata/index.js +115 -115
  44. package/cli/steps/models/format.js +88 -88
  45. package/cli/steps/models/index.js +64 -64
  46. package/cli/steps/models/write.js +38 -38
  47. package/cli/steps/openspec/index.js +136 -136
  48. package/cli/steps/optimization/codegraph.js +141 -127
  49. package/cli/steps/optimization/detect.js +66 -66
  50. package/cli/steps/optimization/humanizer.js +17 -17
  51. package/cli/steps/optimization/index.js +158 -158
  52. package/cli/steps/optimization/memory.js +91 -88
  53. package/cli/steps/optimization/patch-guardrails.js +111 -111
  54. package/cli/steps/optimization/simple-english.js +17 -17
  55. package/cli/steps/optimization/skills-lock.js +30 -30
  56. package/cli/steps/platform/index.js +109 -109
  57. package/cli/steps/source/index.js +123 -123
  58. package/cli/utils/agent-color.js +111 -111
  59. package/cli/utils/copy.js +134 -134
  60. package/cli/utils/exec-spinner.js +47 -47
  61. package/cli/utils/exec.js +134 -134
  62. package/cli/utils/legacy-check.js +30 -30
  63. package/cli/utils/models-cache.js +58 -58
  64. package/cli/utils/models-pricing.js +42 -42
  65. package/cli/utils/paths.js +80 -80
  66. package/cli/utils/process.js +3 -3
  67. package/cli/utils/terminal.js +6 -6
  68. package/cli/utils/update-manifest.js +75 -75
  69. package/harness/.opencode/_gitignore +9 -9
  70. package/harness/.opencode/commands/init.md +5 -5
  71. package/harness/.opencode/commands/make-architecture.md +5 -5
  72. package/harness/.opencode/commands/make-design.md +5 -5
  73. package/harness/.opencode/commands/make-engineer.md +5 -5
  74. package/harness/.opencode/commands/make-guardrails.md +5 -5
  75. package/harness/.opencode/commands/make-user-model.md +5 -5
  76. package/harness/.opencode/commands/ops-backlog.md +10 -10
  77. package/harness/.opencode/commands/ops-review.md +8 -8
  78. package/harness/.opencode/commands/ops-ship.md +9 -9
  79. package/harness/.opencode/commands/plan-apply.md +9 -9
  80. package/harness/.opencode/commands/plan-archive.md +5 -5
  81. package/harness/.opencode/commands/plan-explore.md +9 -9
  82. package/harness/.opencode/commands/plan-goal.md +5 -5
  83. package/harness/.opencode/commands/plan-propose.md +9 -9
  84. package/harness/.opencode/commands/plan-quick.md +5 -5
  85. package/harness/.opencode/commands/plan-story.md +9 -9
  86. package/harness/.opencode/commands/repo-audit.md +5 -5
  87. package/harness/.opencode/commands/repo-help.md +5 -5
  88. package/harness/.opencode/commands/repo-initialize.md +5 -5
  89. package/harness/.opencode/commands/repo-onboard.md +5 -5
  90. package/harness/.opencode/commands/repo-verify.md +5 -5
  91. package/harness/.opencode/package.json +5 -5
  92. package/harness/.opencode/plugins/pc-subagent-monitor.js +184 -184
  93. package/harness/.opencode/plugins/pc-subagent-tiers.js +354 -354
  94. package/harness/.opencode/plugins/pc-system-reminders.js +386 -386
  95. package/harness/.opencode/scripts/check-diff.mjs +26 -7
  96. package/harness/.opencode/scripts/code-rules.mjs +288 -280
  97. package/harness/AGENTS.md +49 -49
  98. package/harness/ARCHITECTURE.md +13 -13
  99. package/harness/DESIGN.md +13 -13
  100. package/harness/opencode.jsonc +36 -36
  101. package/harness/openspec/config.yaml +20 -20
  102. package/harness/skills-lock.json +17 -17
  103. package/package.json +14 -12
  104. package/skills/README.md +18 -18
  105. package/skills/agent-harness-cli/SKILL.md +86 -86
  106. package/skills/browser-automation/SKILL.md +72 -72
  107. package/skills/pc-guardrails-project/SKILL.md +8 -8
  108. package/skills/pc-make-architecture/SKILL.md +32 -32
  109. package/skills/pc-make-architecture/structure-template.md +64 -64
  110. package/skills/pc-make-design/SKILL.md +45 -45
  111. package/skills/pc-make-engineer/SKILL.md +59 -59
  112. package/skills/pc-make-engineer/signal-mapping.md +53 -53
  113. package/skills/pc-make-engineer/template.md +36 -36
  114. package/skills/pc-make-guardrails/SKILL.md +44 -44
  115. package/skills/pc-make-guardrails/category-reference.md +112 -112
  116. package/skills/pc-make-merge-risk-assess/SKILL.md +70 -70
  117. package/skills/pc-make-merge-risk-assess/category-reference.md +114 -114
  118. package/skills/pc-make-user-model/SKILL.md +56 -56
  119. package/skills/pc-ops-ship/SKILL.md +22 -18
  120. package/skills/pc-plan-apply/SKILL.md +86 -86
  121. package/skills/pc-plan-apply/simple-mode.md +21 -21
  122. package/skills/pc-plan-archive/SKILL.md +66 -66
  123. package/skills/pc-plan-explore/SKILL.md +21 -21
  124. package/skills/pc-plan-goal/SKILL.md +91 -91
  125. package/skills/pc-plan-goal/branching.md +39 -39
  126. package/skills/pc-plan-goal/failure-policy.md +30 -30
  127. package/skills/pc-plan-goal/output-mode.md +10 -10
  128. package/skills/pc-plan-goal/output.md +71 -71
  129. package/skills/pc-plan-propose/SKILL.md +125 -125
  130. package/skills/pc-plan-propose/task-annotation.md +39 -39
  131. package/skills/pc-plan-quick/SKILL.md +46 -46
  132. package/skills/pc-plan-story/SKILL.md +93 -93
  133. package/skills/pc-repo-audit/SKILL.md +44 -44
  134. package/skills/pc-repo-help/SKILL.md +87 -87
  135. package/skills/pc-repo-initialize/SKILL.md +119 -119
  136. package/skills/pc-repo-onboard/SKILL.md +32 -32
  137. package/skills/pc-repo-verify/SKILL.md +150 -150
  138. package/skills/pc-userstory-az/SKILL.md +71 -71
  139. package/skills/pc-userstory-browser/SKILL.md +64 -64
  140. package/skills/pc-userstory-gh/SKILL.md +63 -63
  141. package/skills/pc-userstory-jira/SKILL.md +74 -74
@@ -1,89 +1,89 @@
1
- import fse from 'fs-extra'
2
- import path from 'path'
3
- import { success } from '../../utils/exec.js'
4
- import { agentColor, yamlColor } from '../../utils/agent-color.js'
5
-
6
- const FULLSTACK_FILE = 'fullstack-engineer.md'
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.'
8
- const FULLSTACK_IDENTITY = 'You are the default engineer, and your body is what the build and plan agents run. You are more complete but less accurate than specialized engineers, so prefer spawning a specialist when one matches the task domain.'
9
-
10
- // The reminder plugin owns ability bootstrap. Remove the old prompt-level
11
- // directive when regenerating so it cannot duplicate the plugin reminder.
12
- function stripStartupDirective(text) {
13
- if (!text) return text
14
- return text
15
- .split(/\r?\n\r?\n+/)
16
- .filter(paragraph => !paragraph.trim().startsWith('**Startup'))
17
- .join('\n\n')
18
- .trim()
19
- }
20
-
21
- export async function removeLegacyStartupDirectives({ cwd = process.cwd() } = {}) {
22
- const agentsDir = path.join(cwd, '.opencode', 'agents')
23
- if (!await fse.pathExists(agentsDir)) return 0
24
-
25
- let updated = 0
26
- for (const fileName of await fse.readdir(agentsDir)) {
27
- if (!fileName.endsWith('.md')) continue
28
- const filePath = path.join(agentsDir, fileName)
29
- const content = await fse.readFile(filePath, 'utf-8')
30
- const cleaned = stripStartupDirective(content)
31
- if (cleaned === content.trim()) continue
32
- await fse.writeFile(filePath, `${cleaned}\n`, 'utf-8')
33
- updated++
34
- }
35
- return updated
36
- }
37
-
38
- export async function generateFullstackEngineer({ cwd = process.cwd(), updateMode = false } = {}) {
39
- const agentsDir = path.join(cwd, '.opencode', 'agents')
40
- await fse.ensureDir(agentsDir)
41
- const filePath = path.join(agentsDir, FULLSTACK_FILE)
42
-
43
- if (updateMode && await fse.pathExists(filePath)) {
44
- return { generated: false, preserved: true }
45
- }
46
-
47
- let existingAbilities = null
48
- let existingIdentity = null
49
- if (await fse.pathExists(filePath)) {
50
- const content = await fse.readFile(filePath, 'utf-8')
51
- const abilitiesMatch = content.match(/## Abilities\n([\s\S]*?)$/)
52
- if (abilitiesMatch) existingAbilities = abilitiesMatch[1].trim()
53
- const identityMatch = content.match(/^---\n[\s\S]*?\n---\n\n(.*?)(?:\n\n## Abilities)/s)
54
- if (identityMatch) existingIdentity = stripStartupDirective(identityMatch[1])
55
- }
56
-
57
- const frontmatter = [
58
- '---',
59
- `description: ${FULLSTACK_DESCRIPTION}`,
60
- // subagent, not primary: build.md and plan.md are the primaries, and
61
- // pc-subagent-tiers generates both from this file on every startup.
62
- 'mode: subagent',
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'))}`,
67
- 'permission:',
68
- ' edit: allow',
69
- ' bash: allow',
70
- ' read: allow',
71
- ' glob: allow',
72
- ' grep: allow',
73
- ' question: allow',
74
- ' todowrite: allow',
75
- '---',
76
- ]
77
-
78
- const identity = existingIdentity ?? FULLSTACK_IDENTITY
79
-
80
- const abilities = existingAbilities ?? [
81
- '- Guardrails: @pc-guardrails-generic, @pc-guardrails-project',
82
- ].join('\n')
83
-
84
- const content = `${frontmatter.join('\n')}\n\n${identity}\n\n## Abilities\n${abilities}\n`
85
-
86
- await fse.writeFile(filePath, content, 'utf-8')
87
- success(`Generated ${FULLSTACK_FILE}`)
88
- return { generated: true }
89
- }
1
+ import fse from 'fs-extra'
2
+ import path from 'path'
3
+ import { success } from '../../utils/exec.js'
4
+ import { agentColor, yamlColor } from '../../utils/agent-color.js'
5
+
6
+ const FULLSTACK_FILE = 'fullstack-engineer.md'
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.'
8
+ const FULLSTACK_IDENTITY = 'You are the default engineer, and your body is what the build and plan agents run. You are more complete but less accurate than specialized engineers, so prefer spawning a specialist when one matches the task domain.'
9
+
10
+ // The reminder plugin owns ability bootstrap. Remove the old prompt-level
11
+ // directive when regenerating so it cannot duplicate the plugin reminder.
12
+ function stripStartupDirective(text) {
13
+ if (!text) return text
14
+ return text
15
+ .split(/\r?\n\r?\n+/)
16
+ .filter(paragraph => !paragraph.trim().startsWith('**Startup'))
17
+ .join('\n\n')
18
+ .trim()
19
+ }
20
+
21
+ export async function removeLegacyStartupDirectives({ cwd = process.cwd() } = {}) {
22
+ const agentsDir = path.join(cwd, '.opencode', 'agents')
23
+ if (!await fse.pathExists(agentsDir)) return 0
24
+
25
+ let updated = 0
26
+ for (const fileName of await fse.readdir(agentsDir)) {
27
+ if (!fileName.endsWith('.md')) continue
28
+ const filePath = path.join(agentsDir, fileName)
29
+ const content = await fse.readFile(filePath, 'utf-8')
30
+ const cleaned = stripStartupDirective(content)
31
+ if (cleaned === content.trim()) continue
32
+ await fse.writeFile(filePath, `${cleaned}\n`, 'utf-8')
33
+ updated++
34
+ }
35
+ return updated
36
+ }
37
+
38
+ export async function generateFullstackEngineer({ cwd = process.cwd(), updateMode = false } = {}) {
39
+ const agentsDir = path.join(cwd, '.opencode', 'agents')
40
+ await fse.ensureDir(agentsDir)
41
+ const filePath = path.join(agentsDir, FULLSTACK_FILE)
42
+
43
+ if (updateMode && await fse.pathExists(filePath)) {
44
+ return { generated: false, preserved: true }
45
+ }
46
+
47
+ let existingAbilities = null
48
+ let existingIdentity = null
49
+ if (await fse.pathExists(filePath)) {
50
+ const content = await fse.readFile(filePath, 'utf-8')
51
+ const abilitiesMatch = content.match(/## Abilities\n([\s\S]*?)$/)
52
+ if (abilitiesMatch) existingAbilities = abilitiesMatch[1].trim()
53
+ const identityMatch = content.match(/^---\n[\s\S]*?\n---\n\n(.*?)(?:\n\n## Abilities)/s)
54
+ if (identityMatch) existingIdentity = stripStartupDirective(identityMatch[1])
55
+ }
56
+
57
+ const frontmatter = [
58
+ '---',
59
+ `description: ${FULLSTACK_DESCRIPTION}`,
60
+ // subagent, not primary: build.md and plan.md are the primaries, and
61
+ // pc-subagent-tiers generates both from this file on every startup.
62
+ 'mode: subagent',
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'))}`,
67
+ 'permission:',
68
+ ' edit: allow',
69
+ ' bash: allow',
70
+ ' read: allow',
71
+ ' glob: allow',
72
+ ' grep: allow',
73
+ ' question: allow',
74
+ ' todowrite: allow',
75
+ '---',
76
+ ]
77
+
78
+ const identity = existingIdentity ?? FULLSTACK_IDENTITY
79
+
80
+ const abilities = existingAbilities ?? [
81
+ '- Guardrails: @pc-guardrails-generic, @pc-guardrails-project',
82
+ ].join('\n')
83
+
84
+ const content = `${frontmatter.join('\n')}\n\n${identity}\n\n## Abilities\n${abilities}\n`
85
+
86
+ await fse.writeFile(filePath, content, 'utf-8')
87
+ success(`Generated ${FULLSTACK_FILE}`)
88
+ return { generated: true }
89
+ }
@@ -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 } from "./commands.js"
9
- import { installSkills, removeRetiredFiles } 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, "../../../harness")
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),
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 removeRetiredFiles()
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 } from "./commands.js"
9
+ import { installSkills, removeRetiredFiles } 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, "../../../harness")
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),
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 removeRetiredFiles()
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
+ }