@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,34 +1,34 @@
1
- import fse from 'fs-extra'
2
- import { applyEdits, modify, parse } from 'jsonc-parser'
3
- import path from 'path'
4
- import { success, warn } from '../../utils/exec.js'
5
-
6
- /**
7
- * Writes the selected build model as the default model in opencode.jsonc.
8
- * Per-tier models (plan/build/fast) are recorded in harness.json (models)
9
- * by the metadata step, then consumed by the agent-variant generator and /plan-apply.
10
- *
11
- * opencode tolerates JSONC (comments, trailing commas) in its config, so this
12
- * must not use readJson/writeJson: a strict parse crashes on valid configs and
13
- * a strict rewrite deletes user comments. jsonc-parser edits only the `model`
14
- * key and preserves the rest of the file byte-for-byte.
15
- */
16
- export async function writeModelsToConfigs({ buildModel, cwd = process.cwd() }) {
17
- const opencodeJsonPath = path.join(cwd, 'opencode.jsonc')
18
- if (!await fse.pathExists(opencodeJsonPath)) return
19
-
20
- const text = await fse.readFile(opencodeJsonPath, 'utf-8')
21
- const errors = []
22
- parse(text, errors)
23
- if (errors.length > 0) {
24
- warn('opencode.jsonc could not be parsed: leaving it untouched. Set "model" manually.')
25
- return
26
- }
27
-
28
- // `undefined` removes the key; a string value sets it.
29
- const edits = modify(text, ['model'], buildModel || undefined, {
30
- formattingOptions: { insertSpaces: true, tabSize: 2 },
31
- })
32
- await fse.writeFile(opencodeJsonPath, applyEdits(text, edits), 'utf-8')
33
- if (buildModel) success(`default model -> ${buildModel} (written to opencode.jsonc)`)
34
- }
1
+ import fse from 'fs-extra'
2
+ import { applyEdits, modify, parse } from 'jsonc-parser'
3
+ import path from 'path'
4
+ import { success, warn } from '../../utils/exec.js'
5
+
6
+ /**
7
+ * Writes the selected build model as the default model in opencode.jsonc.
8
+ * Per-tier models (plan/build/fast) are recorded in harness.json (models)
9
+ * by the metadata step, then consumed by the agent-variant generator and /plan-apply.
10
+ *
11
+ * opencode tolerates JSONC (comments, trailing commas) in its config, so this
12
+ * must not use readJson/writeJson: a strict parse crashes on valid configs and
13
+ * a strict rewrite deletes user comments. jsonc-parser edits only the `model`
14
+ * key and preserves the rest of the file byte-for-byte.
15
+ */
16
+ export async function writeModelsToConfigs({ buildModel, cwd = process.cwd() }) {
17
+ const opencodeJsonPath = path.join(cwd, 'opencode.jsonc')
18
+ if (!await fse.pathExists(opencodeJsonPath)) return
19
+
20
+ const text = await fse.readFile(opencodeJsonPath, 'utf-8')
21
+ const errors = []
22
+ parse(text, errors)
23
+ if (errors.length > 0) {
24
+ warn('opencode.jsonc could not be parsed: leaving it untouched. Set "model" manually.')
25
+ return
26
+ }
27
+
28
+ // `undefined` removes the key; a string value sets it.
29
+ const edits = modify(text, ['model'], buildModel || undefined, {
30
+ formattingOptions: { insertSpaces: true, tabSize: 2 },
31
+ })
32
+ await fse.writeFile(opencodeJsonPath, applyEdits(text, edits), 'utf-8')
33
+ if (buildModel) success(`default model -> ${buildModel} (written to opencode.jsonc)`)
34
+ }
@@ -1,108 +1,108 @@
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
-
6
- const __dirname = path.dirname(fileURLToPath(import.meta.url))
7
- const GUARDRAILS_FRAGMENT_DIR = path.resolve(__dirname, '../../fragments/guardrails')
8
-
9
- const MARKER_SECTIONS = {
10
- rtk: 'RTK',
11
- codegraph: 'CODEGRAPH',
12
- memory: 'MEMORY',
13
- simpleEnglish: 'SIMPLE-ENGLISH',
14
- humanizer: 'HUMANIZER',
15
- }
16
-
17
- const OPTIONAL_TOOL_GUIDANCE = {
18
- codegraph: 'Use `codegraph_explore` to refine relevant symbols and file-disjointness for the current wave.',
19
- memory: 'Use Agentmemory MCP tools for cross-session context and task-result notes.',
20
- }
21
-
22
- function replaceMarkerSection(content, startMarker, endMarker, replacement) {
23
- if (!content.includes(startMarker) || !content.includes(endMarker)) return content
24
- const escape = value => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
25
- const pattern = new RegExp(`${escape(startMarker)}[\\s\\S]*?${escape(endMarker)}`)
26
- return content.replace(pattern, `${startMarker}\n${replacement}\n${endMarker}`)
27
- }
28
-
29
- async function patchOptionalToolGuidance(destSkillsDir, selections) {
30
- const files = []
31
-
32
- async function collect(dir) {
33
- for (const entry of await fse.readdir(dir, { withFileTypes: true })) {
34
- const entryPath = path.join(dir, entry.name)
35
- if (entry.isDirectory()) await collect(entryPath)
36
- else if (entry.name.endsWith('.md')) files.push(entryPath)
37
- }
38
- }
39
-
40
- if (!await fse.pathExists(destSkillsDir)) return 0
41
- await collect(destSkillsDir)
42
-
43
- let patched = 0
44
- for (const filePath of files) {
45
- let content = await fse.readFile(filePath, 'utf-8')
46
- const original = content
47
- for (const [key, guidance] of Object.entries(OPTIONAL_TOOL_GUIDANCE)) {
48
- const suffix = key.toUpperCase()
49
- content = replaceMarkerSection(
50
- content,
51
- `<!-- PC-OPTIMIZATION-${suffix}-START -->`,
52
- `<!-- PC-OPTIMIZATION-${suffix}-END -->`,
53
- selections[key] ? guidance : '',
54
- )
55
- }
56
- if (content !== original) {
57
- await fse.writeFile(filePath, `${content.replace(/\s*$/, '')}\n`, 'utf-8')
58
- patched++
59
- }
60
- }
61
- return patched
62
- }
63
-
64
- export async function patchGuardrails(selections = {}, { cwd = process.cwd() } = {}) {
65
- const destSkillsDir = path.join(cwd, '.agents', 'skills')
66
- const guardrailsPath = path.join(destSkillsDir, 'pc-guardrails-generic', 'SKILL.md')
67
- let patchedCount = 0
68
-
69
- if (await fse.pathExists(guardrailsPath)) {
70
- let content = await fse.readFile(guardrailsPath, 'utf-8')
71
- content = replaceMarkerSection(
72
- content,
73
- '<!-- PC-GUARDRAILS-CAVEMAN-START -->',
74
- '<!-- PC-GUARDRAILS-CAVEMAN-END -->',
75
- '',
76
- ).replace(
77
- '<!-- PC-GUARDRAILS-CAVEMAN-START -->\n\n<!-- PC-GUARDRAILS-CAVEMAN-END -->',
78
- '<!-- PC-GUARDRAILS-SIMPLE-ENGLISH-START -->\n<!-- PC-GUARDRAILS-SIMPLE-ENGLISH-END -->',
79
- ).replace(
80
- '1. Load ALL skills listed under your own `## Abilities` now (Guardrails first, then the rest), by calling the `skill` tool once per `@skill-name`.',
81
- '1. The `pc-system-reminders` plugin has already loaded the skills listed under your `## Abilities`, guardrails first.',
82
- )
83
- for (const [key, markerSuffix] of Object.entries(MARKER_SECTIONS)) {
84
- let sectionContent = ''
85
- if (selections[key]) {
86
- const presetName = key === 'simpleEnglish' ? 'simple-english' : key
87
- const presetPath = path.join(GUARDRAILS_FRAGMENT_DIR, `${presetName}.md`)
88
- if (await fse.pathExists(presetPath)) {
89
- sectionContent = (await fse.readFile(presetPath, 'utf-8')).trim()
90
- }
91
- }
92
- content = replaceMarkerSection(
93
- content,
94
- `<!-- PC-GUARDRAILS-${markerSuffix}-START -->`,
95
- `<!-- PC-GUARDRAILS-${markerSuffix}-END -->`,
96
- sectionContent,
97
- )
98
- if (selections[key]) patchedCount++
99
- }
100
- await fse.writeFile(guardrailsPath, `${content.replace(/\s*$/, '')}\n`, 'utf-8')
101
- } else {
102
- info('pc-guardrails-generic not found, skipping guardrails patching')
103
- }
104
-
105
- const optionalPatched = await patchOptionalToolGuidance(destSkillsDir, selections)
106
- success(`Guardrails patched: ${patchedCount} sections injected; ${optionalPatched} optional tool file(s) updated`)
107
- return { patched: true, count: patchedCount, optionalPatched }
108
- }
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
+
6
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
7
+ const GUARDRAILS_FRAGMENT_DIR = path.resolve(__dirname, '../../fragments/guardrails')
8
+
9
+ const MARKER_SECTIONS = {
10
+ rtk: 'RTK',
11
+ codegraph: 'CODEGRAPH',
12
+ memory: 'MEMORY',
13
+ simpleEnglish: 'SIMPLE-ENGLISH',
14
+ humanizer: 'HUMANIZER',
15
+ }
16
+
17
+ const OPTIONAL_TOOL_GUIDANCE = {
18
+ codegraph: 'Use `codegraph_explore` to refine relevant symbols and file-disjointness for the current wave.',
19
+ memory: 'Use Agentmemory MCP tools for cross-session context and task-result notes.',
20
+ }
21
+
22
+ function replaceMarkerSection(content, startMarker, endMarker, replacement) {
23
+ if (!content.includes(startMarker) || !content.includes(endMarker)) return content
24
+ const escape = value => value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
25
+ const pattern = new RegExp(`${escape(startMarker)}[\\s\\S]*?${escape(endMarker)}`)
26
+ return content.replace(pattern, `${startMarker}\n${replacement}\n${endMarker}`)
27
+ }
28
+
29
+ async function patchOptionalToolGuidance(destSkillsDir, selections) {
30
+ const files = []
31
+
32
+ async function collect(dir) {
33
+ for (const entry of await fse.readdir(dir, { withFileTypes: true })) {
34
+ const entryPath = path.join(dir, entry.name)
35
+ if (entry.isDirectory()) await collect(entryPath)
36
+ else if (entry.name.endsWith('.md')) files.push(entryPath)
37
+ }
38
+ }
39
+
40
+ if (!await fse.pathExists(destSkillsDir)) return 0
41
+ await collect(destSkillsDir)
42
+
43
+ let patched = 0
44
+ for (const filePath of files) {
45
+ let content = await fse.readFile(filePath, 'utf-8')
46
+ const original = content
47
+ for (const [key, guidance] of Object.entries(OPTIONAL_TOOL_GUIDANCE)) {
48
+ const suffix = key.toUpperCase()
49
+ content = replaceMarkerSection(
50
+ content,
51
+ `<!-- PC-OPTIMIZATION-${suffix}-START -->`,
52
+ `<!-- PC-OPTIMIZATION-${suffix}-END -->`,
53
+ selections[key] ? guidance : '',
54
+ )
55
+ }
56
+ if (content !== original) {
57
+ await fse.writeFile(filePath, `${content.replace(/\s*$/, '')}\n`, 'utf-8')
58
+ patched++
59
+ }
60
+ }
61
+ return patched
62
+ }
63
+
64
+ export async function patchGuardrails(selections = {}, { cwd = process.cwd() } = {}) {
65
+ const destSkillsDir = path.join(cwd, '.agents', 'skills')
66
+ const guardrailsPath = path.join(destSkillsDir, 'pc-guardrails-generic', 'SKILL.md')
67
+ let patchedCount = 0
68
+
69
+ if (await fse.pathExists(guardrailsPath)) {
70
+ let content = await fse.readFile(guardrailsPath, 'utf-8')
71
+ content = replaceMarkerSection(
72
+ content,
73
+ '<!-- PC-GUARDRAILS-CAVEMAN-START -->',
74
+ '<!-- PC-GUARDRAILS-CAVEMAN-END -->',
75
+ '',
76
+ ).replace(
77
+ '<!-- PC-GUARDRAILS-CAVEMAN-START -->\n\n<!-- PC-GUARDRAILS-CAVEMAN-END -->',
78
+ '<!-- PC-GUARDRAILS-SIMPLE-ENGLISH-START -->\n<!-- PC-GUARDRAILS-SIMPLE-ENGLISH-END -->',
79
+ ).replace(
80
+ '1. Load ALL skills listed under your own `## Abilities` now (Guardrails first, then the rest), by calling the `skill` tool once per `@skill-name`.',
81
+ '1. The `pc-system-reminders` plugin has already loaded the skills listed under your `## Abilities`, guardrails first.',
82
+ )
83
+ for (const [key, markerSuffix] of Object.entries(MARKER_SECTIONS)) {
84
+ let sectionContent = ''
85
+ if (selections[key]) {
86
+ const presetName = key === 'simpleEnglish' ? 'simple-english' : key
87
+ const presetPath = path.join(GUARDRAILS_FRAGMENT_DIR, `${presetName}.md`)
88
+ if (await fse.pathExists(presetPath)) {
89
+ sectionContent = (await fse.readFile(presetPath, 'utf-8')).trim()
90
+ }
91
+ }
92
+ content = replaceMarkerSection(
93
+ content,
94
+ `<!-- PC-GUARDRAILS-${markerSuffix}-START -->`,
95
+ `<!-- PC-GUARDRAILS-${markerSuffix}-END -->`,
96
+ sectionContent,
97
+ )
98
+ if (selections[key]) patchedCount++
99
+ }
100
+ await fse.writeFile(guardrailsPath, `${content.replace(/\s*$/, '')}\n`, 'utf-8')
101
+ } else {
102
+ info('pc-guardrails-generic not found, skipping guardrails patching')
103
+ }
104
+
105
+ const optionalPatched = await patchOptionalToolGuidance(destSkillsDir, selections)
106
+ success(`Guardrails patched: ${patchedCount} sections injected; ${optionalPatched} optional tool file(s) updated`)
107
+ return { patched: true, count: patchedCount, optionalPatched }
108
+ }
package/src/utils/copy.js CHANGED
@@ -1,108 +1,108 @@
1
- import fse from 'fs-extra'
2
- import path from 'path'
3
- import { canUpdateManagedFile, hashFile, readUpdateManifest, recordManagedFile, writeUpdateManifest } from './update-manifest.js'
4
-
5
- // Folders never copied (skills handled separately by installSkills, .bootstrap is internal tooling)
6
- const ALWAYS_EXCLUDE = ['.bootstrap', 'skills', 'node_modules']
7
-
8
- // Files never overwritten even with forceOverwrite: user owns these.
9
- // The CLI ships templates, but init / the wizard / the user populate them
10
- // with project-specific content. Overwriting on update would destroy user work.
11
- const NEVER_OVERWRITE = [
12
- `openspec${path.sep}config.yaml`,
13
- 'opencode.jsonc',
14
- ]
15
-
16
- const MARKER_COMMANDS = new Set(['.opencode/commands/ops-review.md', '.opencode/commands/ops-backlog.md'])
17
-
18
- function isTemplateTest(relativePath) {
19
- return relativePath.split(path.sep).join('/').startsWith('.opencode/plugins/') && relativePath.endsWith('.test.js')
20
- }
21
-
22
- export function isManagedContentPath(relativePath) {
23
- const normalized = relativePath.split(path.sep).join('/')
24
- if (normalized.startsWith('.opencode/plugins/')) return true
25
- if (normalized === '.opencode/tui/pc-subagents.tsx') return true
26
- if (normalized.startsWith('.opencode/commands/') && !MARKER_COMMANDS.has(normalized)) return true
27
- return normalized === 'openspec/specs/.gitkeep' || normalized === 'openspec/changes/archive/.gitkeep'
28
- }
29
-
30
- /**
31
- * Copy content/ directory to destination.
32
- * Excludes:
33
- * - .agents/skills and .opencode/skills (handled separately)
34
- * - .bootstrap (internal tooling)
35
- * - node_modules
36
- * - DESIGN.md and ARCHITECTURE.md if ctx says they already exist (preserve user's files)
37
- * @param {string} contentDir - absolute path to content/
38
- * @param {string} destDir - absolute path to destination (project root)
39
- * @param {'azure'|'github'} platform
40
- * @param {{ hasDesign?: boolean, hasArchitecture?: boolean, updateMode?: boolean }} ctx
41
- */
42
- export async function copyContent(contentDir, destDir, platform, ctx = {}) {
43
- const manifest = ctx.updateMode ? await readUpdateManifest(destDir) : null
44
- await fse.copy(contentDir, destDir, {
45
- overwrite: ctx.updateMode || ctx.forceOverwrite || false,
46
- filter: async (src) => {
47
- const rel = path.relative(contentDir, src)
48
- const parts = rel.split(path.sep)
49
- if (parts.some(part => ALWAYS_EXCLUDE.includes(part))) return false
50
- if (isTemplateTest(rel)) return false
51
- if (ctx.hasDesign && rel === 'DESIGN.md') return false
52
- if (ctx.hasArchitecture && rel === 'ARCHITECTURE.md') return false
53
- // User-owned config files are never overwritten, even with forceOverwrite.
54
- // The update command calls writeModelsToConfigs separately to set the
55
- // model field in opencode.jsonc without destroying user additions.
56
- if (NEVER_OVERWRITE.includes(rel)) return false
57
- if (!ctx.updateMode) return true
58
-
59
- const stat = await fse.stat(src)
60
- if (stat.isDirectory()) return true
61
- if (!isManagedContentPath(rel)) {
62
- return !(await fse.pathExists(path.join(destDir, rel)))
63
- }
64
- return canUpdateManagedFile(rel, destDir, manifest)
65
- },
66
- })
67
- }
68
-
69
- export async function recordManagedContent(contentDir, destDir, { updateMode = false } = {}) {
70
- const manifest = await readUpdateManifest(destDir)
71
-
72
- async function walk(directory) {
73
- for (const entry of await fse.readdir(directory, { withFileTypes: true })) {
74
- const sourcePath = path.join(directory, entry.name)
75
- const relativePath = path.relative(contentDir, sourcePath)
76
- if (entry.isDirectory()) {
77
- await walk(sourcePath)
78
- continue
79
- }
80
- if (isTemplateTest(relativePath)) continue
81
- if (!isManagedContentPath(relativePath)) continue
82
-
83
- const destinationPath = path.join(destDir, relativePath)
84
- if (!await fse.pathExists(destinationPath)) continue
85
- const sourceHash = await hashFile(sourcePath)
86
- const destinationHash = await hashFile(destinationPath)
87
- const manifestPath = relativePath.split(path.sep).join('/')
88
- const previousHash = manifest.files?.[manifestPath]
89
- if (!updateMode || destinationHash === sourceHash || destinationHash === previousHash) {
90
- await recordManagedFile(manifest, relativePath, sourcePath)
91
- }
92
- }
93
- }
94
-
95
- await walk(contentDir)
96
- await writeUpdateManifest(manifest, destDir)
97
- }
98
-
99
- export async function findAiFiles(dir, files) {
100
- const found = []
101
- for (const file of files) {
102
- const fullPath = path.join(dir, file)
103
- if (await fse.pathExists(fullPath)) {
104
- found.push(fullPath)
105
- }
106
- }
107
- return found
108
- }
1
+ import fse from 'fs-extra'
2
+ import path from 'path'
3
+ import { canUpdateManagedFile, hashFile, readUpdateManifest, recordManagedFile, writeUpdateManifest } from './update-manifest.js'
4
+
5
+ // Folders never copied (skills handled separately by installSkills, .bootstrap is internal tooling)
6
+ const ALWAYS_EXCLUDE = ['.bootstrap', 'skills', 'node_modules']
7
+
8
+ // Files never overwritten even with forceOverwrite: user owns these.
9
+ // The CLI ships templates, but init / the wizard / the user populate them
10
+ // with project-specific content. Overwriting on update would destroy user work.
11
+ const NEVER_OVERWRITE = [
12
+ `openspec${path.sep}config.yaml`,
13
+ 'opencode.jsonc',
14
+ ]
15
+
16
+ const MARKER_COMMANDS = new Set(['.opencode/commands/ops-review.md', '.opencode/commands/ops-backlog.md'])
17
+
18
+ function isTemplateTest(relativePath) {
19
+ return relativePath.split(path.sep).join('/').startsWith('.opencode/plugins/') && relativePath.endsWith('.test.js')
20
+ }
21
+
22
+ export function isManagedContentPath(relativePath) {
23
+ const normalized = relativePath.split(path.sep).join('/')
24
+ if (normalized.startsWith('.opencode/plugins/')) return true
25
+ if (normalized === '.opencode/tui/pc-subagents.tsx') return true
26
+ if (normalized.startsWith('.opencode/commands/') && !MARKER_COMMANDS.has(normalized)) return true
27
+ return normalized === 'openspec/specs/.gitkeep' || normalized === 'openspec/changes/archive/.gitkeep'
28
+ }
29
+
30
+ /**
31
+ * Copy content/ directory to destination.
32
+ * Excludes:
33
+ * - .agents/skills and .opencode/skills (handled separately)
34
+ * - .bootstrap (internal tooling)
35
+ * - node_modules
36
+ * - DESIGN.md and ARCHITECTURE.md if ctx says they already exist (preserve user's files)
37
+ * @param {string} contentDir - absolute path to content/
38
+ * @param {string} destDir - absolute path to destination (project root)
39
+ * @param {'azure'|'github'} platform
40
+ * @param {{ hasDesign?: boolean, hasArchitecture?: boolean, updateMode?: boolean }} ctx
41
+ */
42
+ export async function copyContent(contentDir, destDir, platform, ctx = {}) {
43
+ const manifest = ctx.updateMode ? await readUpdateManifest(destDir) : null
44
+ await fse.copy(contentDir, destDir, {
45
+ overwrite: ctx.updateMode || ctx.forceOverwrite || false,
46
+ filter: async (src) => {
47
+ const rel = path.relative(contentDir, src)
48
+ const parts = rel.split(path.sep)
49
+ if (parts.some(part => ALWAYS_EXCLUDE.includes(part))) return false
50
+ if (isTemplateTest(rel)) return false
51
+ if (ctx.hasDesign && rel === 'DESIGN.md') return false
52
+ if (ctx.hasArchitecture && rel === 'ARCHITECTURE.md') return false
53
+ // User-owned config files are never overwritten, even with forceOverwrite.
54
+ // The update command calls writeModelsToConfigs separately to set the
55
+ // model field in opencode.jsonc without destroying user additions.
56
+ if (NEVER_OVERWRITE.includes(rel)) return false
57
+ if (!ctx.updateMode) return true
58
+
59
+ const stat = await fse.stat(src)
60
+ if (stat.isDirectory()) return true
61
+ if (!isManagedContentPath(rel)) {
62
+ return !(await fse.pathExists(path.join(destDir, rel)))
63
+ }
64
+ return canUpdateManagedFile(rel, destDir, manifest)
65
+ },
66
+ })
67
+ }
68
+
69
+ export async function recordManagedContent(contentDir, destDir, { updateMode = false } = {}) {
70
+ const manifest = await readUpdateManifest(destDir)
71
+
72
+ async function walk(directory) {
73
+ for (const entry of await fse.readdir(directory, { withFileTypes: true })) {
74
+ const sourcePath = path.join(directory, entry.name)
75
+ const relativePath = path.relative(contentDir, sourcePath)
76
+ if (entry.isDirectory()) {
77
+ await walk(sourcePath)
78
+ continue
79
+ }
80
+ if (isTemplateTest(relativePath)) continue
81
+ if (!isManagedContentPath(relativePath)) continue
82
+
83
+ const destinationPath = path.join(destDir, relativePath)
84
+ if (!await fse.pathExists(destinationPath)) continue
85
+ const sourceHash = await hashFile(sourcePath)
86
+ const destinationHash = await hashFile(destinationPath)
87
+ const manifestPath = relativePath.split(path.sep).join('/')
88
+ const previousHash = manifest.files?.[manifestPath]
89
+ if (!updateMode || destinationHash === sourceHash || destinationHash === previousHash) {
90
+ await recordManagedFile(manifest, relativePath, sourcePath)
91
+ }
92
+ }
93
+ }
94
+
95
+ await walk(contentDir)
96
+ await writeUpdateManifest(manifest, destDir)
97
+ }
98
+
99
+ export async function findAiFiles(dir, files) {
100
+ const found = []
101
+ for (const file of files) {
102
+ const fullPath = path.join(dir, file)
103
+ if (await fse.pathExists(fullPath)) {
104
+ found.push(fullPath)
105
+ }
106
+ }
107
+ return found
108
+ }
@@ -1,30 +1,30 @@
1
- import fse from 'fs-extra'
2
- import path from 'node:path'
3
- import { CONFIG_FILE, LEGACY_CONFIG_FILES, OPENCODE_DIR } from './paths.js'
4
-
5
- /**
6
- * Detect a repo onboarded by opencode-onboard v1.x.
7
- *
8
- * v2 renamed both the state files and the `ob-` skill prefix, and it does not
9
- * migrate: see the rename plan. Running against a v1 repo would otherwise fail
10
- * silently in two places, because `patchFile` returns without logging when its
11
- * markers are absent and the skill sync only removes stale directories under
12
- * `forceOverwrite && !updateMode`. The result is a half-patched harness that
13
- * still reports success, so refuse the run instead.
14
- */
15
- export async function findLegacyInstall(cwd = process.cwd()) {
16
- const opencodeDir = path.join(cwd, OPENCODE_DIR)
17
- if (await fse.pathExists(path.join(opencodeDir, CONFIG_FILE))) return null
18
-
19
- const found = []
20
- for (const file of LEGACY_CONFIG_FILES) {
21
- if (await fse.pathExists(path.join(opencodeDir, file))) found.push(`${OPENCODE_DIR}/${file}`)
22
- }
23
-
24
- const skillsDir = path.join(cwd, '.agents', 'skills')
25
- const legacySkills = (await fse.readdir(skillsDir).catch(() => []))
26
- .filter(entry => entry.startsWith('ob-'))
27
-
28
- if (found.length === 0 && legacySkills.length === 0) return null
29
- return { files: found, skills: legacySkills }
30
- }
1
+ import fse from 'fs-extra'
2
+ import path from 'node:path'
3
+ import { CONFIG_FILE, LEGACY_CONFIG_FILES, OPENCODE_DIR } from './paths.js'
4
+
5
+ /**
6
+ * Detect a repo onboarded by opencode-onboard v1.x.
7
+ *
8
+ * v2 renamed both the state files and the `ob-` skill prefix, and it does not
9
+ * migrate: see the rename plan. Running against a v1 repo would otherwise fail
10
+ * silently in two places, because `patchFile` returns without logging when its
11
+ * markers are absent and the skill sync only removes stale directories under
12
+ * `forceOverwrite && !updateMode`. The result is a half-patched harness that
13
+ * still reports success, so refuse the run instead.
14
+ */
15
+ export async function findLegacyInstall(cwd = process.cwd()) {
16
+ const opencodeDir = path.join(cwd, OPENCODE_DIR)
17
+ if (await fse.pathExists(path.join(opencodeDir, CONFIG_FILE))) return null
18
+
19
+ const found = []
20
+ for (const file of LEGACY_CONFIG_FILES) {
21
+ if (await fse.pathExists(path.join(opencodeDir, file))) found.push(`${OPENCODE_DIR}/${file}`)
22
+ }
23
+
24
+ const skillsDir = path.join(cwd, '.agents', 'skills')
25
+ const legacySkills = (await fse.readdir(skillsDir).catch(() => []))
26
+ .filter(entry => entry.startsWith('ob-'))
27
+
28
+ if (found.length === 0 && legacySkills.length === 0) return null
29
+ return { files: found, skills: legacySkills }
30
+ }