@plainconceptsplatform/agent-harness 2.0.0 → 2.1.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 (117) hide show
  1. package/README.md +426 -419
  2. package/package.json +3 -3
  3. package/src/commands/join.js +244 -244
  4. package/src/commands/shared.js +27 -27
  5. package/src/commands/single.js +79 -79
  6. package/src/commands/update.js +109 -109
  7. package/src/commands/wizard.js +134 -134
  8. package/src/content/.agents/skills/browser-automation/SKILL.md +66 -66
  9. package/src/content/.agents/skills/pc-guardrails-generic/SKILL.md +68 -68
  10. package/src/content/.agents/skills/pc-guardrails-project/SKILL.md +8 -8
  11. package/src/content/.agents/skills/pc-make-architecture/SKILL.md +51 -51
  12. package/src/content/.agents/skills/pc-make-architecture/structure-template.md +38 -38
  13. package/src/content/.agents/skills/pc-make-design/SKILL.md +68 -68
  14. package/src/content/.agents/skills/pc-make-engineer/SKILL.md +219 -219
  15. package/src/content/.agents/skills/pc-make-engineer/signal-mapping.md +68 -68
  16. package/src/content/.agents/skills/pc-make-engineer/template.md +81 -81
  17. package/src/content/.agents/skills/pc-make-evidence-scaffold/SKILL.md +18 -18
  18. package/src/content/.agents/skills/pc-make-evidence-scaffold/evidence-contract.md +29 -29
  19. package/src/content/.agents/skills/pc-make-guardrails/SKILL.md +74 -74
  20. package/src/content/.agents/skills/pc-make-guardrails/category-reference.md +68 -68
  21. package/src/content/.agents/skills/pc-make-merge-risk-assess/SKILL.md +70 -70
  22. package/src/content/.agents/skills/pc-make-merge-risk-assess/category-reference.md +98 -98
  23. package/src/content/.agents/skills/pc-make-user-model/SKILL.md +66 -66
  24. package/src/content/.agents/skills/pc-ops-evidence/SKILL.md +127 -127
  25. package/src/content/.agents/skills/pc-ops-ship/SKILL.md +18 -18
  26. package/src/content/.agents/skills/pc-plan-apply/SKILL.md +83 -83
  27. package/src/content/.agents/skills/pc-plan-apply/simple-mode.md +21 -21
  28. package/src/content/.agents/skills/pc-plan-archive/SKILL.md +63 -63
  29. package/src/content/.agents/skills/pc-plan-explore/SKILL.md +9 -9
  30. package/src/content/.agents/skills/pc-plan-goal/SKILL.md +94 -94
  31. package/src/content/.agents/skills/pc-plan-goal/branching.md +30 -30
  32. package/src/content/.agents/skills/pc-plan-goal/failure-policy.md +30 -30
  33. package/src/content/.agents/skills/pc-plan-goal/output-mode.md +9 -9
  34. package/src/content/.agents/skills/pc-plan-goal/output.md +68 -68
  35. package/src/content/.agents/skills/pc-plan-propose/SKILL.md +125 -125
  36. package/src/content/.agents/skills/pc-plan-propose/task-annotation.md +39 -39
  37. package/src/content/.agents/skills/pc-plan-quick/SKILL.md +62 -62
  38. package/src/content/.agents/skills/pc-plan-story/SKILL.md +146 -146
  39. package/src/content/.agents/skills/pc-repo-audit/SKILL.md +44 -44
  40. package/src/content/.agents/skills/pc-repo-help/SKILL.md +91 -91
  41. package/src/content/.agents/skills/pc-repo-initialize/SKILL.md +130 -130
  42. package/src/content/.agents/skills/pc-repo-onboard/SKILL.md +87 -87
  43. package/src/content/.agents/skills/pc-repo-verify/SKILL.md +34 -34
  44. package/src/content/.agents/skills/pc-userstory-az/SKILL.md +157 -157
  45. package/src/content/.agents/skills/pc-userstory-browser/SKILL.md +132 -132
  46. package/src/content/.agents/skills/pc-userstory-gh/SKILL.md +120 -120
  47. package/src/content/.agents/skills/pc-userstory-jira/SKILL.md +131 -131
  48. package/src/content/.opencode/_gitignore +9 -7
  49. package/src/content/.opencode/commands/init.md +5 -5
  50. package/src/content/.opencode/commands/make-architecture.md +5 -5
  51. package/src/content/.opencode/commands/make-design.md +5 -5
  52. package/src/content/.opencode/commands/make-engineer.md +5 -5
  53. package/src/content/.opencode/commands/make-evidence-scaffold.md +5 -5
  54. package/src/content/.opencode/commands/make-guardrails.md +5 -5
  55. package/src/content/.opencode/commands/make-user-model.md +5 -5
  56. package/src/content/.opencode/commands/ops-backlog.md +10 -10
  57. package/src/content/.opencode/commands/ops-evidence.md +9 -9
  58. package/src/content/.opencode/commands/ops-review.md +8 -8
  59. package/src/content/.opencode/commands/ops-ship.md +9 -9
  60. package/src/content/.opencode/commands/plan-apply.md +9 -9
  61. package/src/content/.opencode/commands/plan-archive.md +5 -5
  62. package/src/content/.opencode/commands/plan-explore.md +9 -9
  63. package/src/content/.opencode/commands/plan-goal.md +5 -5
  64. package/src/content/.opencode/commands/plan-propose.md +9 -9
  65. package/src/content/.opencode/commands/plan-quick.md +5 -5
  66. package/src/content/.opencode/commands/plan-story.md +9 -9
  67. package/src/content/.opencode/commands/repo-audit.md +5 -5
  68. package/src/content/.opencode/commands/repo-help.md +5 -5
  69. package/src/content/.opencode/commands/repo-initialize.md +5 -5
  70. package/src/content/.opencode/commands/repo-onboard.md +5 -5
  71. package/src/content/.opencode/commands/repo-verify.md +5 -5
  72. package/src/content/.opencode/plugins/pc-subagent-monitor.js +139 -139
  73. package/src/content/.opencode/plugins/pc-subagent-tiers.js +281 -179
  74. package/src/content/.opencode/plugins/pc-system-reminders.js +96 -96
  75. package/src/content/.opencode/tui/pc-subagents.tsx +98 -98
  76. package/src/content/.opencode/tui.json +6 -6
  77. package/src/content/AGENTS.md +71 -71
  78. package/src/content/opencode.jsonc +39 -31
  79. package/src/fragments/archive/az.md +95 -95
  80. package/src/fragments/archive/gh.md +94 -94
  81. package/src/fragments/archive/gl.md +94 -94
  82. package/src/fragments/archive/none.md +73 -73
  83. package/src/fragments/guardrails/codegraph.md +7 -7
  84. package/src/fragments/guardrails/humanizer.md +4 -4
  85. package/src/fragments/guardrails/memory.md +4 -4
  86. package/src/fragments/guardrails/rtk.md +3 -3
  87. package/src/fragments/guardrails/simple-english.md +4 -4
  88. package/src/fragments/ops-backlog/az.md +28 -28
  89. package/src/fragments/ops-backlog/gh.md +29 -29
  90. package/src/fragments/ops-backlog/jira.md +28 -28
  91. package/src/fragments/ops-evidence/az.md +41 -41
  92. package/src/fragments/ops-evidence/gh.md +53 -53
  93. package/src/fragments/ops-evidence/jira.md +38 -38
  94. package/src/fragments/ops-review/az.md +62 -62
  95. package/src/fragments/ops-review/gh.md +52 -52
  96. package/src/fragments/ops-review/gl.md +56 -56
  97. package/src/fragments/ops-ship/az.md +80 -80
  98. package/src/fragments/ops-ship/gh.md +68 -68
  99. package/src/fragments/ops-ship/gl.md +85 -85
  100. package/src/index.js +107 -107
  101. package/src/presets/agents-content.json +53 -53
  102. package/src/presets/models.json +68 -68
  103. package/src/steps/copy/agents.js +118 -118
  104. package/src/steps/copy/commands.js +91 -91
  105. package/src/steps/copy/fullstack-engineer.js +85 -83
  106. package/src/steps/copy/index.js +88 -88
  107. package/src/steps/copy/opencode-json.js +147 -129
  108. package/src/steps/copy/skills.js +196 -196
  109. package/src/steps/metadata/index.js +108 -108
  110. package/src/steps/models/write.js +34 -34
  111. package/src/steps/optimization/patch-guardrails.js +108 -108
  112. package/src/utils/copy.js +108 -108
  113. package/src/utils/legacy-check.js +30 -30
  114. package/src/utils/models-cache.js +58 -58
  115. package/src/utils/paths.js +67 -64
  116. package/src/utils/update-manifest.js +49 -49
  117. package/src/content/.opencode/plugins/pc-system-reminders.test.js +0 -35
@@ -1,196 +1,196 @@
1
- import fse from 'fs-extra'
2
- import path from 'path'
3
- import { fileURLToPath } from 'url'
4
- import { info, success } from '../../utils/exec.js'
5
- import { canUpdateManagedFile, readUpdateManifest, recordManagedFile, writeUpdateManifest } from '../../utils/update-manifest.js'
6
-
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')
10
-
11
- // Userstory skills parse backlog work items: selected by backlogPlatform only.
12
- // Mixing the two axes here installs the wrong variant on mixed setups, because
13
- // all variants rename to the same generic dir and the first copy wins.
14
- const BACKLOG_PLATFORM_SKILLS = {
15
- 'pc-userstory-gh': 'github',
16
- 'pc-userstory-az': 'azure',
17
- 'pc-userstory-jira': 'jira',
18
- 'pc-userstory-browser': 'browser',
19
- }
20
-
21
- // Platform-specific skills are renamed to their generic form on install.
22
- // The -gh / -az / -jira / -gl suffix is only needed here to keep all variants in source.
23
- // After install only one platform is present so no suffix is needed.
24
- const SKILL_RENAME = {
25
- 'pc-userstory-gh': 'pc-userstory',
26
- 'pc-userstory-az': 'pc-userstory',
27
- 'pc-userstory-jira': 'pc-userstory',
28
- 'pc-userstory-browser': 'pc-userstory',
29
- }
30
-
31
- function shouldInstallSkill(skill, backlogPlatform, _repoPlatform) {
32
- if (skill in BACKLOG_PLATFORM_SKILLS) return BACKLOG_PLATFORM_SKILLS[skill] === backlogPlatform
33
- return true
34
- }
35
-
36
- // Shipped skills whose content is a PLACEHOLDER — a /make-* command
37
- // overwrites them with project-specific content. During forceOverwrite,
38
- // these must be preserved when they have already been generated (detected
39
- // by the <!-- Last updated: marker). Without this check, the update would
40
- // wipe project-specific guardrails, risk assessments, etc.
41
- const GENERATABLE_SKILLS = new Set([
42
- 'pc-guardrails-project',
43
- 'pc-merge-risk-assess',
44
- ])
45
-
46
- const MARKER_SKILLS = new Set([
47
- 'pc-guardrails-generic',
48
- 'pc-plan-archive',
49
- 'pc-ops-ship',
50
- 'pc-ops-evidence',
51
- 'pc-repo-initialize',
52
- ])
53
-
54
- async function isGeneratedSkill(dest) {
55
- const skillMd = path.join(dest, 'SKILL.md')
56
- if (!await fse.pathExists(skillMd)) return false
57
- const content = await fse.readFile(skillMd, 'utf-8')
58
- return content.includes('<!-- Last updated:')
59
- }
60
-
61
- async function syncSkillFiles(src, dest, relativeRoot, cwd, manifest) {
62
- for (const entry of await fse.readdir(src, { withFileTypes: true })) {
63
- const sourcePath = path.join(src, entry.name)
64
- const destinationPath = path.join(dest, entry.name)
65
- const relativePath = path.join(relativeRoot, entry.name)
66
- if (entry.isDirectory()) {
67
- await syncSkillFiles(sourcePath, destinationPath, relativePath, cwd, manifest)
68
- continue
69
- }
70
- if (!await fse.pathExists(destinationPath) || await canUpdateManagedFile(relativePath, cwd, manifest)) {
71
- await fse.ensureDir(path.dirname(destinationPath))
72
- await fse.copyFile(sourcePath, destinationPath)
73
- await recordManagedFile(manifest, relativePath, sourcePath)
74
- success(`Updated skill: ${relativePath}`)
75
- } else {
76
- info(`Preserving modified skill file: ${relativePath}`)
77
- }
78
- }
79
- }
80
-
81
- async function recordSkillSourceFiles(src, relativeRoot, manifest) {
82
- for (const entry of await fse.readdir(src, { withFileTypes: true })) {
83
- const sourcePath = path.join(src, entry.name)
84
- const relativePath = path.join(relativeRoot, entry.name)
85
- if (entry.isDirectory()) {
86
- await recordSkillSourceFiles(sourcePath, relativePath, manifest)
87
- } else {
88
- await recordManagedFile(manifest, relativePath, sourcePath)
89
- }
90
- }
91
- }
92
-
93
- async function installObSkills(backlogPlatform = 'github', repoPlatform, { forceOverwrite = false, updateMode = false } = {}) {
94
- const repo = repoPlatform ?? backlogPlatform
95
- const destSkillsDir = path.join(process.cwd(), '.agents', 'skills')
96
- await fse.ensureDir(destSkillsDir)
97
- const manifest = await readUpdateManifest()
98
-
99
- // Build the set of skill names we ship (source dirs, after rename).
100
- // Only these may be removed during forceOverwrite — project-generated
101
- // skills (pc-merge-risk-assess, custom loops, etc.) must survive.
102
- const contentSkills = await fse.readdir(CONTENT_SKILLS_DIR)
103
- const shippedNames = new Set()
104
- for (const skill of contentSkills) {
105
- const stat = await fse.stat(path.join(CONTENT_SKILLS_DIR, skill)).catch(() => null)
106
- if (stat?.isDirectory()) shippedNames.add(SKILL_RENAME[skill] ?? skill)
107
- }
108
- // Also account for stale platform-variant names (e.g. pc-userstory-gh
109
- // when the current platform is azure).
110
- Object.keys(SKILL_RENAME).forEach(k => shippedNames.add(k))
111
-
112
- if (forceOverwrite && !updateMode) {
113
- for (const entry of await fse.readdir(destSkillsDir)) {
114
- if (!entry.startsWith('pc-')) continue
115
- if (!shippedNames.has(entry)) {
116
- info(`Preserving project-generated skill: ${entry}`)
117
- continue
118
- }
119
- // Generatable skills: preserve if already populated by /make-*
120
- if (GENERATABLE_SKILLS.has(entry) && await isGeneratedSkill(path.join(destSkillsDir, entry))) {
121
- info(`Preserving generated skill: ${entry}`)
122
- continue
123
- }
124
- await fse.remove(path.join(destSkillsDir, entry))
125
- info(`Removing shipped skill: ${entry}`)
126
- }
127
- }
128
-
129
- const skills = contentSkills
130
- for (const skill of skills) {
131
- const src = path.join(CONTENT_SKILLS_DIR, skill)
132
- const destName = SKILL_RENAME[skill] ?? skill
133
- const dest = path.join(destSkillsDir, destName)
134
- const stat = await fse.stat(src)
135
- if (!stat.isDirectory()) continue
136
- if (!shouldInstallSkill(skill, backlogPlatform, repo)) {
137
- if (forceOverwrite) {
138
- const staleDest = path.join(destSkillsDir, skill)
139
- if (skill !== destName && await fse.pathExists(staleDest)) {
140
- await fse.remove(staleDest)
141
- info(`Removing stale skill: ${skill}`)
142
- }
143
- }
144
- info(`Skipping skill: ${skill} (not needed for platforms: ${backlogPlatform}/${repo})`)
145
- continue
146
- }
147
- if (updateMode) {
148
- if (!await fse.pathExists(dest)) {
149
- await syncSkillFiles(src, dest, path.join('.agents', 'skills', destName), process.cwd(), manifest)
150
- } else if (MARKER_SKILLS.has(destName) || GENERATABLE_SKILLS.has(destName)) {
151
- info(`Preserving project-owned skill: ${destName}`)
152
- } else {
153
- await syncSkillFiles(src, dest, path.join('.agents', 'skills', destName), process.cwd(), manifest)
154
- }
155
- continue
156
- }
157
- if (await fse.pathExists(dest) && !forceOverwrite) {
158
- info(`${destName} already exists, skipping`)
159
- continue
160
- }
161
- // Even with forceOverwrite, never overwrite an already-generated
162
- // generatable skill — its content was produced by /make-guardrails or
163
- // similar, not by the shipped placeholder.
164
- if (forceOverwrite && GENERATABLE_SKILLS.has(destName) && await isGeneratedSkill(dest)) {
165
- info(`Preserving generated skill: ${destName}`)
166
- continue
167
- }
168
- await fse.copy(src, dest, { overwrite: true })
169
- if (!MARKER_SKILLS.has(destName) && !GENERATABLE_SKILLS.has(destName)) {
170
- await recordSkillSourceFiles(src, path.join('.agents', 'skills', destName), manifest)
171
- }
172
- success(`${forceOverwrite ? 'Updated' : 'Installed'} skill: ${destName}`)
173
- }
174
-
175
- await writeUpdateManifest(manifest)
176
- }
177
-
178
- export async function installSkills(backlogPlatform = 'github', repoPlatform, opts = {}) {
179
- info('Installing built-in pc-skills...')
180
- await installObSkills(backlogPlatform, repoPlatform, opts)
181
- console.log()
182
-
183
- if (await fse.pathExists(CONTENT_SKILLS_LOCK)) {
184
- const destLock = path.join(process.cwd(), 'skills-lock.json')
185
- if (await fse.pathExists(destLock) && (opts.updateMode || !opts.forceOverwrite)) {
186
- info('skills-lock.json already exists, skipping')
187
- } else {
188
- await fse.copy(CONTENT_SKILLS_LOCK, destLock, { overwrite: true })
189
- success(`${opts.forceOverwrite ? 'Updated' : 'Installed'} skills-lock.json`)
190
- }
191
- }
192
-
193
- // npx skills experimental_install is now called once at the end of the
194
- // optimization step, after the user has selected which tools to enable.
195
- // This avoids multiple npx skills add calls during onboarding.
196
- }
1
+ import fse from 'fs-extra'
2
+ import path from 'path'
3
+ import { fileURLToPath } from 'url'
4
+ import { info, success } from '../../utils/exec.js'
5
+ import { canUpdateManagedFile, readUpdateManifest, recordManagedFile, writeUpdateManifest } from '../../utils/update-manifest.js'
6
+
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')
10
+
11
+ // Userstory skills parse backlog work items: selected by backlogPlatform only.
12
+ // Mixing the two axes here installs the wrong variant on mixed setups, because
13
+ // all variants rename to the same generic dir and the first copy wins.
14
+ const BACKLOG_PLATFORM_SKILLS = {
15
+ 'pc-userstory-gh': 'github',
16
+ 'pc-userstory-az': 'azure',
17
+ 'pc-userstory-jira': 'jira',
18
+ 'pc-userstory-browser': 'browser',
19
+ }
20
+
21
+ // Platform-specific skills are renamed to their generic form on install.
22
+ // The -gh / -az / -jira / -gl suffix is only needed here to keep all variants in source.
23
+ // After install only one platform is present so no suffix is needed.
24
+ const SKILL_RENAME = {
25
+ 'pc-userstory-gh': 'pc-userstory',
26
+ 'pc-userstory-az': 'pc-userstory',
27
+ 'pc-userstory-jira': 'pc-userstory',
28
+ 'pc-userstory-browser': 'pc-userstory',
29
+ }
30
+
31
+ function shouldInstallSkill(skill, backlogPlatform, _repoPlatform) {
32
+ if (skill in BACKLOG_PLATFORM_SKILLS) return BACKLOG_PLATFORM_SKILLS[skill] === backlogPlatform
33
+ return true
34
+ }
35
+
36
+ // Shipped skills whose content is a PLACEHOLDER — a /make-* command
37
+ // overwrites them with project-specific content. During forceOverwrite,
38
+ // these must be preserved when they have already been generated (detected
39
+ // by the <!-- Last updated: marker). Without this check, the update would
40
+ // wipe project-specific guardrails, risk assessments, etc.
41
+ const GENERATABLE_SKILLS = new Set([
42
+ 'pc-guardrails-project',
43
+ 'pc-merge-risk-assess',
44
+ ])
45
+
46
+ const MARKER_SKILLS = new Set([
47
+ 'pc-guardrails-generic',
48
+ 'pc-plan-archive',
49
+ 'pc-ops-ship',
50
+ 'pc-ops-evidence',
51
+ 'pc-repo-initialize',
52
+ ])
53
+
54
+ async function isGeneratedSkill(dest) {
55
+ const skillMd = path.join(dest, 'SKILL.md')
56
+ if (!await fse.pathExists(skillMd)) return false
57
+ const content = await fse.readFile(skillMd, 'utf-8')
58
+ return content.includes('<!-- Last updated:')
59
+ }
60
+
61
+ async function syncSkillFiles(src, dest, relativeRoot, cwd, manifest) {
62
+ for (const entry of await fse.readdir(src, { withFileTypes: true })) {
63
+ const sourcePath = path.join(src, entry.name)
64
+ const destinationPath = path.join(dest, entry.name)
65
+ const relativePath = path.join(relativeRoot, entry.name)
66
+ if (entry.isDirectory()) {
67
+ await syncSkillFiles(sourcePath, destinationPath, relativePath, cwd, manifest)
68
+ continue
69
+ }
70
+ if (!await fse.pathExists(destinationPath) || await canUpdateManagedFile(relativePath, cwd, manifest)) {
71
+ await fse.ensureDir(path.dirname(destinationPath))
72
+ await fse.copyFile(sourcePath, destinationPath)
73
+ await recordManagedFile(manifest, relativePath, sourcePath)
74
+ success(`Updated skill: ${relativePath}`)
75
+ } else {
76
+ info(`Preserving modified skill file: ${relativePath}`)
77
+ }
78
+ }
79
+ }
80
+
81
+ async function recordSkillSourceFiles(src, relativeRoot, manifest) {
82
+ for (const entry of await fse.readdir(src, { withFileTypes: true })) {
83
+ const sourcePath = path.join(src, entry.name)
84
+ const relativePath = path.join(relativeRoot, entry.name)
85
+ if (entry.isDirectory()) {
86
+ await recordSkillSourceFiles(sourcePath, relativePath, manifest)
87
+ } else {
88
+ await recordManagedFile(manifest, relativePath, sourcePath)
89
+ }
90
+ }
91
+ }
92
+
93
+ async function installObSkills(backlogPlatform = 'github', repoPlatform, { forceOverwrite = false, updateMode = false } = {}) {
94
+ const repo = repoPlatform ?? backlogPlatform
95
+ const destSkillsDir = path.join(process.cwd(), '.agents', 'skills')
96
+ await fse.ensureDir(destSkillsDir)
97
+ const manifest = await readUpdateManifest()
98
+
99
+ // Build the set of skill names we ship (source dirs, after rename).
100
+ // Only these may be removed during forceOverwrite — project-generated
101
+ // skills (pc-merge-risk-assess, custom loops, etc.) must survive.
102
+ const contentSkills = await fse.readdir(CONTENT_SKILLS_DIR)
103
+ const shippedNames = new Set()
104
+ for (const skill of contentSkills) {
105
+ const stat = await fse.stat(path.join(CONTENT_SKILLS_DIR, skill)).catch(() => null)
106
+ if (stat?.isDirectory()) shippedNames.add(SKILL_RENAME[skill] ?? skill)
107
+ }
108
+ // Also account for stale platform-variant names (e.g. pc-userstory-gh
109
+ // when the current platform is azure).
110
+ Object.keys(SKILL_RENAME).forEach(k => shippedNames.add(k))
111
+
112
+ if (forceOverwrite && !updateMode) {
113
+ for (const entry of await fse.readdir(destSkillsDir)) {
114
+ if (!entry.startsWith('pc-')) continue
115
+ if (!shippedNames.has(entry)) {
116
+ info(`Preserving project-generated skill: ${entry}`)
117
+ continue
118
+ }
119
+ // Generatable skills: preserve if already populated by /make-*
120
+ if (GENERATABLE_SKILLS.has(entry) && await isGeneratedSkill(path.join(destSkillsDir, entry))) {
121
+ info(`Preserving generated skill: ${entry}`)
122
+ continue
123
+ }
124
+ await fse.remove(path.join(destSkillsDir, entry))
125
+ info(`Removing shipped skill: ${entry}`)
126
+ }
127
+ }
128
+
129
+ const skills = contentSkills
130
+ for (const skill of skills) {
131
+ const src = path.join(CONTENT_SKILLS_DIR, skill)
132
+ const destName = SKILL_RENAME[skill] ?? skill
133
+ const dest = path.join(destSkillsDir, destName)
134
+ const stat = await fse.stat(src)
135
+ if (!stat.isDirectory()) continue
136
+ if (!shouldInstallSkill(skill, backlogPlatform, repo)) {
137
+ if (forceOverwrite) {
138
+ const staleDest = path.join(destSkillsDir, skill)
139
+ if (skill !== destName && await fse.pathExists(staleDest)) {
140
+ await fse.remove(staleDest)
141
+ info(`Removing stale skill: ${skill}`)
142
+ }
143
+ }
144
+ info(`Skipping skill: ${skill} (not needed for platforms: ${backlogPlatform}/${repo})`)
145
+ continue
146
+ }
147
+ if (updateMode) {
148
+ if (!await fse.pathExists(dest)) {
149
+ await syncSkillFiles(src, dest, path.join('.agents', 'skills', destName), process.cwd(), manifest)
150
+ } else if (MARKER_SKILLS.has(destName) || GENERATABLE_SKILLS.has(destName)) {
151
+ info(`Preserving project-owned skill: ${destName}`)
152
+ } else {
153
+ await syncSkillFiles(src, dest, path.join('.agents', 'skills', destName), process.cwd(), manifest)
154
+ }
155
+ continue
156
+ }
157
+ if (await fse.pathExists(dest) && !forceOverwrite) {
158
+ info(`${destName} already exists, skipping`)
159
+ continue
160
+ }
161
+ // Even with forceOverwrite, never overwrite an already-generated
162
+ // generatable skill — its content was produced by /make-guardrails or
163
+ // similar, not by the shipped placeholder.
164
+ if (forceOverwrite && GENERATABLE_SKILLS.has(destName) && await isGeneratedSkill(dest)) {
165
+ info(`Preserving generated skill: ${destName}`)
166
+ continue
167
+ }
168
+ await fse.copy(src, dest, { overwrite: true })
169
+ if (!MARKER_SKILLS.has(destName) && !GENERATABLE_SKILLS.has(destName)) {
170
+ await recordSkillSourceFiles(src, path.join('.agents', 'skills', destName), manifest)
171
+ }
172
+ success(`${forceOverwrite ? 'Updated' : 'Installed'} skill: ${destName}`)
173
+ }
174
+
175
+ await writeUpdateManifest(manifest)
176
+ }
177
+
178
+ export async function installSkills(backlogPlatform = 'github', repoPlatform, opts = {}) {
179
+ info('Installing built-in pc-skills...')
180
+ await installObSkills(backlogPlatform, repoPlatform, opts)
181
+ console.log()
182
+
183
+ if (await fse.pathExists(CONTENT_SKILLS_LOCK)) {
184
+ const destLock = path.join(process.cwd(), 'skills-lock.json')
185
+ if (await fse.pathExists(destLock) && (opts.updateMode || !opts.forceOverwrite)) {
186
+ info('skills-lock.json already exists, skipping')
187
+ } else {
188
+ await fse.copy(CONTENT_SKILLS_LOCK, destLock, { overwrite: true })
189
+ success(`${opts.forceOverwrite ? 'Updated' : 'Installed'} skills-lock.json`)
190
+ }
191
+ }
192
+
193
+ // npx skills experimental_install is now called once at the end of the
194
+ // optimization step, after the user has selected which tools to enable.
195
+ // This avoids multiple npx skills add calls during onboarding.
196
+ }
@@ -1,108 +1,108 @@
1
- import { execa } from 'execa'
2
- import fse from 'fs-extra'
3
- import path from 'path'
4
- import { createRequire } from 'node:module'
5
- import { header, success, warn } from '../../utils/exec.js'
6
- import { configPath } from '../../utils/paths.js'
7
-
8
- const require = createRequire(import.meta.url)
9
- const { version: harnessVersion } = require('../../../package.json')
10
-
11
- function clampConcurrency(n) {
12
- const v = Number(n)
13
- if (!Number.isFinite(v)) return 3
14
- return Math.min(5, Math.max(1, Math.round(v)))
15
- }
16
-
17
- async function detectOpencodeVersion() {
18
- try {
19
- const result = await execa('opencode', ['--version'], { reject: false })
20
- if (result.exitCode !== 0) return null
21
- const output = (result.stdout || result.stderr || '').trim()
22
- return output || null
23
- } catch {
24
- return null
25
- }
26
- }
27
-
28
- export async function writeHarnessConfig(data) {
29
- header('Step 10, Writing onboarding metadata')
30
-
31
- const opencodeVersion = await detectOpencodeVersion()
32
- const cwd = data.cwd ?? process.cwd()
33
- const target = configPath(cwd)
34
-
35
- // Read the existing file so load-bearing config (models, maxConcurrent)
36
- // is preserved across metadata refreshes when not explicitly provided.
37
- const existing = await fse.readJson(target).catch(() => null)
38
-
39
- const selectedModels = Object.fromEntries(
40
- Object.entries({
41
- plan: data.planModel,
42
- build: data.buildModel,
43
- fast: data.fastModel,
44
- }).filter(([, value]) => value)
45
- )
46
- const models = Object.keys(selectedModels).length > 0
47
- ? { ...existing?.models, ...selectedModels }
48
- : existing?.models ?? null
49
- const maxConcurrent = clampConcurrency(data.maxConcurrentAgents ?? existing?.agents?.maxConcurrent ?? 3)
50
-
51
- const optionalTools = data.optionalTools ?? existing?.tools ?? {}
52
- const { caveman: _caveman, ...existingTools } = existing?.tools ?? {}
53
- const tools = {
54
- rtk: !!(optionalTools.rtk?.optedIn ?? optionalTools.rtk),
55
- quota: !!(optionalTools.quota?.optedIn ?? optionalTools.quota),
56
- simpleEnglish: !!(
57
- optionalTools.simpleEnglish?.optedIn ?? optionalTools.simpleEnglish ??
58
- optionalTools.caveman?.optedIn ?? optionalTools.caveman
59
- ),
60
- codegraph: !!(optionalTools.codegraph?.optedIn ?? optionalTools.codegraph),
61
- memory: !!(optionalTools.memory?.optedIn ?? optionalTools.memory),
62
- }
63
-
64
- const payload = {
65
- ...existing,
66
- version: 2,
67
- generatedAt: new Date().toISOString(),
68
- harnessVersion,
69
- opencodeVersion,
70
-
71
- platform: {
72
- ...existing?.platform,
73
- backlog: data.backlogPlatform ?? 'none',
74
- repo: data.repoPlatform ?? 'none',
75
- },
76
-
77
- ...(models ? { models } : {}),
78
-
79
- agents: {
80
- ...existing?.agents,
81
- maxConcurrent,
82
- },
83
-
84
- source: {
85
- ...existing?.source,
86
- mode: data.sourceMode ?? 'current',
87
- roots: data.sourceRoots ?? [],
88
- },
89
-
90
- tools: { ...existingTools, ...tools },
91
-
92
- preexisting: {
93
- ...existing?.preexisting,
94
- design: !!data.hasDesign,
95
- architecture: !!data.hasArchitecture,
96
- openspec: !!data.hasOpenspec,
97
- },
98
- }
99
-
100
- try {
101
- await fse.ensureDir(path.dirname(target))
102
- await fse.writeJson(target, payload, { spaces: 2 })
103
- success('Wrote .opencode/harness.json')
104
- if (!opencodeVersion) warn('Could not detect opencode version, saved as null')
105
- } catch (err) {
106
- warn(`Could not write onboarding metadata: ${err.message}`)
107
- }
108
- }
1
+ import { execa } from 'execa'
2
+ import fse from 'fs-extra'
3
+ import path from 'path'
4
+ import { createRequire } from 'node:module'
5
+ import { header, success, warn } from '../../utils/exec.js'
6
+ import { configPath } from '../../utils/paths.js'
7
+
8
+ const require = createRequire(import.meta.url)
9
+ const { version: harnessVersion } = require('../../../package.json')
10
+
11
+ function clampConcurrency(n) {
12
+ const v = Number(n)
13
+ if (!Number.isFinite(v)) return 3
14
+ return Math.min(5, Math.max(1, Math.round(v)))
15
+ }
16
+
17
+ async function detectOpencodeVersion() {
18
+ try {
19
+ const result = await execa('opencode', ['--version'], { reject: false })
20
+ if (result.exitCode !== 0) return null
21
+ const output = (result.stdout || result.stderr || '').trim()
22
+ return output || null
23
+ } catch {
24
+ return null
25
+ }
26
+ }
27
+
28
+ export async function writeHarnessConfig(data) {
29
+ header('Step 10, Writing onboarding metadata')
30
+
31
+ const opencodeVersion = await detectOpencodeVersion()
32
+ const cwd = data.cwd ?? process.cwd()
33
+ const target = configPath(cwd)
34
+
35
+ // Read the existing file so load-bearing config (models, maxConcurrent)
36
+ // is preserved across metadata refreshes when not explicitly provided.
37
+ const existing = await fse.readJson(target).catch(() => null)
38
+
39
+ const selectedModels = Object.fromEntries(
40
+ Object.entries({
41
+ plan: data.planModel,
42
+ build: data.buildModel,
43
+ fast: data.fastModel,
44
+ }).filter(([, value]) => value)
45
+ )
46
+ const models = Object.keys(selectedModels).length > 0
47
+ ? { ...existing?.models, ...selectedModels }
48
+ : existing?.models ?? null
49
+ const maxConcurrent = clampConcurrency(data.maxConcurrentAgents ?? existing?.agents?.maxConcurrent ?? 3)
50
+
51
+ const optionalTools = data.optionalTools ?? existing?.tools ?? {}
52
+ const { caveman: _caveman, ...existingTools } = existing?.tools ?? {}
53
+ const tools = {
54
+ rtk: !!(optionalTools.rtk?.optedIn ?? optionalTools.rtk),
55
+ quota: !!(optionalTools.quota?.optedIn ?? optionalTools.quota),
56
+ simpleEnglish: !!(
57
+ optionalTools.simpleEnglish?.optedIn ?? optionalTools.simpleEnglish ??
58
+ optionalTools.caveman?.optedIn ?? optionalTools.caveman
59
+ ),
60
+ codegraph: !!(optionalTools.codegraph?.optedIn ?? optionalTools.codegraph),
61
+ memory: !!(optionalTools.memory?.optedIn ?? optionalTools.memory),
62
+ }
63
+
64
+ const payload = {
65
+ ...existing,
66
+ version: 2,
67
+ generatedAt: new Date().toISOString(),
68
+ harnessVersion,
69
+ opencodeVersion,
70
+
71
+ platform: {
72
+ ...existing?.platform,
73
+ backlog: data.backlogPlatform ?? 'none',
74
+ repo: data.repoPlatform ?? 'none',
75
+ },
76
+
77
+ ...(models ? { models } : {}),
78
+
79
+ agents: {
80
+ ...existing?.agents,
81
+ maxConcurrent,
82
+ },
83
+
84
+ source: {
85
+ ...existing?.source,
86
+ mode: data.sourceMode ?? 'current',
87
+ roots: data.sourceRoots ?? [],
88
+ },
89
+
90
+ tools: { ...existingTools, ...tools },
91
+
92
+ preexisting: {
93
+ ...existing?.preexisting,
94
+ design: !!data.hasDesign,
95
+ architecture: !!data.hasArchitecture,
96
+ openspec: !!data.hasOpenspec,
97
+ },
98
+ }
99
+
100
+ try {
101
+ await fse.ensureDir(path.dirname(target))
102
+ await fse.writeJson(target, payload, { spaces: 2 })
103
+ success('Wrote .opencode/harness.json')
104
+ if (!opencodeVersion) warn('Could not detect opencode version, saved as null')
105
+ } catch (err) {
106
+ warn(`Could not write onboarding metadata: ${err.message}`)
107
+ }
108
+ }