@plainconceptsplatform/agent-harness 3.5.0 → 3.7.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 (144) hide show
  1. package/LICENSE +21 -21
  2. package/README.md +471 -454
  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 -316
  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 -141
  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 -91
  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 +81 -81
  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 +10 -10
  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 +10 -10
  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 +7 -7
  81. package/harness/.opencode/commands/plan-explore.md +11 -11
  82. package/harness/.opencode/commands/plan-goal.md +5 -5
  83. package/harness/.opencode/commands/plan-propose.md +11 -11
  84. package/harness/.opencode/commands/plan-quick.md +7 -7
  85. package/harness/.opencode/commands/plan-story.md +11 -11
  86. package/harness/.opencode/commands/repo-audit.md +7 -7
  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-commit-coauthor.js +41 -0
  93. package/harness/.opencode/plugins/pc-subagent-monitor.js +184 -184
  94. package/harness/.opencode/plugins/pc-subagent-tiers.js +367 -367
  95. package/harness/.opencode/plugins/pc-system-reminders.js +404 -386
  96. package/harness/.opencode/scripts/check-diff.mjs +279 -252
  97. package/harness/.opencode/scripts/code-rules.mjs +288 -288
  98. package/harness/.opencode/scripts/platform-manifest.mjs +112 -0
  99. package/harness/AGENTS.md +49 -49
  100. package/harness/ARCHITECTURE.md +13 -13
  101. package/harness/DESIGN.md +13 -13
  102. package/harness/opencode.jsonc +36 -36
  103. package/harness/openspec/config.yaml +20 -20
  104. package/harness/skills-lock.json +17 -17
  105. package/package.json +14 -12
  106. package/skills/README.md +18 -18
  107. package/skills/agent-harness-cli/SKILL.md +86 -86
  108. package/skills/browser-automation/SKILL.md +72 -72
  109. package/skills/pc-guardrails-generic/SKILL.md +53 -53
  110. package/skills/pc-guardrails-project/SKILL.md +8 -8
  111. package/skills/pc-make-architecture/SKILL.md +33 -32
  112. package/skills/pc-make-architecture/structure-template.md +64 -64
  113. package/skills/pc-make-design/SKILL.md +45 -45
  114. package/skills/pc-make-engineer/SKILL.md +59 -59
  115. package/skills/pc-make-engineer/signal-mapping.md +53 -53
  116. package/skills/pc-make-engineer/template.md +36 -36
  117. package/skills/pc-make-guardrails/SKILL.md +44 -44
  118. package/skills/pc-make-guardrails/category-reference.md +112 -112
  119. package/skills/pc-make-merge-risk-assess/SKILL.md +70 -70
  120. package/skills/pc-make-merge-risk-assess/category-reference.md +114 -114
  121. package/skills/pc-make-user-model/SKILL.md +56 -56
  122. package/skills/pc-ops-ship/SKILL.md +22 -22
  123. package/skills/pc-plan-apply/SKILL.md +88 -88
  124. package/skills/pc-plan-apply/simple-mode.md +21 -21
  125. package/skills/pc-plan-archive/SKILL.md +66 -66
  126. package/skills/pc-plan-explore/SKILL.md +21 -21
  127. package/skills/pc-plan-goal/SKILL.md +91 -91
  128. package/skills/pc-plan-goal/branching.md +39 -39
  129. package/skills/pc-plan-goal/failure-policy.md +30 -30
  130. package/skills/pc-plan-goal/output-mode.md +10 -10
  131. package/skills/pc-plan-goal/output.md +71 -71
  132. package/skills/pc-plan-propose/SKILL.md +125 -125
  133. package/skills/pc-plan-propose/task-annotation.md +39 -39
  134. package/skills/pc-plan-quick/SKILL.md +46 -46
  135. package/skills/pc-plan-story/SKILL.md +93 -93
  136. package/skills/pc-repo-audit/SKILL.md +44 -44
  137. package/skills/pc-repo-help/SKILL.md +87 -87
  138. package/skills/pc-repo-initialize/SKILL.md +119 -119
  139. package/skills/pc-repo-onboard/SKILL.md +32 -32
  140. package/skills/pc-repo-verify/SKILL.md +150 -150
  141. package/skills/pc-userstory-az/SKILL.md +71 -71
  142. package/skills/pc-userstory-browser/SKILL.md +64 -64
  143. package/skills/pc-userstory-gh/SKILL.md +63 -63
  144. package/skills/pc-userstory-jira/SKILL.md +74 -74
@@ -1,120 +1,120 @@
1
- import { checkbox } from '@inquirer/prompts'
2
- import fse from 'fs-extra'
3
- import path from 'path'
4
- import { fileURLToPath } from 'url'
5
- import { findAiFiles } from '../../utils/copy.js'
6
- import { header, info, success, warn } from '../../utils/exec.js'
7
- import { MARKERS } from '../../utils/terminal.js'
8
-
9
- const __dirname = path.dirname(fileURLToPath(import.meta.url))
10
- const CLEAN_PRESET_PATH = path.resolve(__dirname, '../../presets/clean.json')
11
- const cleanPreset = await fse.readJson(CLEAN_PRESET_PATH)
12
-
13
- async function childrenExcludingPreserved(dir) {
14
- const results = []
15
- if (!await fse.pathExists(dir)) return results
16
- const entries = await fse.readdir(dir)
17
- for (const entry of entries) {
18
- if (cleanPreset.preserveSubfolders.includes(entry)) continue
19
- results.push(path.join(dir, entry))
20
- }
21
- return results
22
- }
23
-
24
- async function isPopulated(filePath) {
25
- if (!await fse.pathExists(filePath)) return false
26
- const content = await fse.readFile(filePath, 'utf-8')
27
- const trimmed = content.trim()
28
- if (!trimmed) return false
29
- if (trimmed.startsWith('<!-- onboard-prompt')) return false
30
- return true
31
- }
32
-
33
- async function hasOpenspecHistory(cwd) {
34
- const changesDir = path.join(cwd, 'openspec', 'changes')
35
- const archiveDir = path.join(cwd, 'openspec', 'archive')
36
- if (await fse.pathExists(changesDir)) {
37
- const entries = await fse.readdir(changesDir)
38
- if (entries.length > 0) return true
39
- }
40
- if (await fse.pathExists(archiveDir)) {
41
- const entries = await fse.readdir(archiveDir)
42
- if (entries.length > 0) return true
43
- }
44
- return false
45
- }
46
-
47
- export async function cleanAiFiles({ removeLegacyConfig = false } = {}) {
48
- header('Step 2, Existing AI config files')
49
-
50
- const cwd = process.cwd()
51
- const ctx = {
52
- hasDesign: await isPopulated(path.join(cwd, 'DESIGN.md')),
53
- hasArchitecture: await isPopulated(path.join(cwd, 'ARCHITECTURE.md')),
54
- hasOpenspec: await hasOpenspecHistory(cwd),
55
- }
56
-
57
- if (ctx.hasDesign) info('DESIGN.md exists and is populated, keeping it')
58
- if (ctx.hasArchitecture) info('ARCHITECTURE.md exists and is populated, keeping it')
59
- if (ctx.hasOpenspec) info('openspec/ history exists, keeping it')
60
-
61
- const legacyConfig = path.join(cwd, '.opencode', 'opencode.json')
62
- if (removeLegacyConfig && await fse.pathExists(legacyConfig)) {
63
- await fse.remove(legacyConfig)
64
- info('Removed legacy .opencode/opencode.json')
65
- }
66
-
67
- const flatFiles = await findAiFiles(cwd, cleanPreset.detectFiles)
68
- const dirTargets = cleanPreset.directoryTargets
69
- const dirEntries = []
70
- for (const dirName of dirTargets) {
71
- const dirPath = path.join(cwd, dirName)
72
- const children = await childrenExcludingPreserved(dirPath)
73
- if (children.length > 0) {
74
- dirEntries.push(...children)
75
- } else if (await fse.pathExists(dirPath)) {
76
- // Directory exists but all children are preserved (e.g. only skills/):
77
- // still offer the directory itself as a removable option.
78
- dirEntries.push(dirPath)
79
- }
80
- }
81
-
82
- const filteredFlat = flatFiles.filter(f => {
83
- const rel = path.relative(cwd, f)
84
- if (dirTargets.includes(rel)) return false
85
- if (cleanPreset.preserve.some(p => rel === p || rel.startsWith(p + path.sep))) return false
86
- return true
87
- })
88
-
89
- const allToRemove = [...filteredFlat, ...dirEntries]
90
-
91
- if (allToRemove.length === 0) {
92
- success('No existing AI config files to remove')
93
- return ctx
94
- }
95
-
96
- const choices = allToRemove.map(f => ({
97
- name: path.relative(cwd, f).replace(/\\/g, '/'),
98
- value: f,
99
- checked: true,
100
- }))
101
-
102
- const selected = await checkbox({
103
- message: cleanPreset.selectionMessage,
104
- choices,
105
- })
106
-
107
- if (!selected || selected.length === 0) {
108
- success('No AI config files selected for removal')
109
- return ctx
110
- }
111
-
112
- warn('Removing selected AI config files:')
113
- for (const f of selected) {
114
- info(`${MARKERS.EMPTY}${f.replace(cwd + path.sep, '')}`)
115
- await fse.remove(f)
116
- }
117
- success('Removed existing AI config files')
118
-
119
- return ctx
120
- }
1
+ import { checkbox } from '@inquirer/prompts'
2
+ import fse from 'fs-extra'
3
+ import path from 'path'
4
+ import { fileURLToPath } from 'url'
5
+ import { findAiFiles } from '../../utils/copy.js'
6
+ import { header, info, success, warn } from '../../utils/exec.js'
7
+ import { MARKERS } from '../../utils/terminal.js'
8
+
9
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
10
+ const CLEAN_PRESET_PATH = path.resolve(__dirname, '../../presets/clean.json')
11
+ const cleanPreset = await fse.readJson(CLEAN_PRESET_PATH)
12
+
13
+ async function childrenExcludingPreserved(dir) {
14
+ const results = []
15
+ if (!await fse.pathExists(dir)) return results
16
+ const entries = await fse.readdir(dir)
17
+ for (const entry of entries) {
18
+ if (cleanPreset.preserveSubfolders.includes(entry)) continue
19
+ results.push(path.join(dir, entry))
20
+ }
21
+ return results
22
+ }
23
+
24
+ async function isPopulated(filePath) {
25
+ if (!await fse.pathExists(filePath)) return false
26
+ const content = await fse.readFile(filePath, 'utf-8')
27
+ const trimmed = content.trim()
28
+ if (!trimmed) return false
29
+ if (trimmed.startsWith('<!-- onboard-prompt')) return false
30
+ return true
31
+ }
32
+
33
+ async function hasOpenspecHistory(cwd) {
34
+ const changesDir = path.join(cwd, 'openspec', 'changes')
35
+ const archiveDir = path.join(cwd, 'openspec', 'archive')
36
+ if (await fse.pathExists(changesDir)) {
37
+ const entries = await fse.readdir(changesDir)
38
+ if (entries.length > 0) return true
39
+ }
40
+ if (await fse.pathExists(archiveDir)) {
41
+ const entries = await fse.readdir(archiveDir)
42
+ if (entries.length > 0) return true
43
+ }
44
+ return false
45
+ }
46
+
47
+ export async function cleanAiFiles({ removeLegacyConfig = false } = {}) {
48
+ header('Step 2, Existing AI config files')
49
+
50
+ const cwd = process.cwd()
51
+ const ctx = {
52
+ hasDesign: await isPopulated(path.join(cwd, 'DESIGN.md')),
53
+ hasArchitecture: await isPopulated(path.join(cwd, 'ARCHITECTURE.md')),
54
+ hasOpenspec: await hasOpenspecHistory(cwd),
55
+ }
56
+
57
+ if (ctx.hasDesign) info('DESIGN.md exists and is populated, keeping it')
58
+ if (ctx.hasArchitecture) info('ARCHITECTURE.md exists and is populated, keeping it')
59
+ if (ctx.hasOpenspec) info('openspec/ history exists, keeping it')
60
+
61
+ const legacyConfig = path.join(cwd, '.opencode', 'opencode.json')
62
+ if (removeLegacyConfig && await fse.pathExists(legacyConfig)) {
63
+ await fse.remove(legacyConfig)
64
+ info('Removed legacy .opencode/opencode.json')
65
+ }
66
+
67
+ const flatFiles = await findAiFiles(cwd, cleanPreset.detectFiles)
68
+ const dirTargets = cleanPreset.directoryTargets
69
+ const dirEntries = []
70
+ for (const dirName of dirTargets) {
71
+ const dirPath = path.join(cwd, dirName)
72
+ const children = await childrenExcludingPreserved(dirPath)
73
+ if (children.length > 0) {
74
+ dirEntries.push(...children)
75
+ } else if (await fse.pathExists(dirPath)) {
76
+ // Directory exists but all children are preserved (e.g. only skills/):
77
+ // still offer the directory itself as a removable option.
78
+ dirEntries.push(dirPath)
79
+ }
80
+ }
81
+
82
+ const filteredFlat = flatFiles.filter(f => {
83
+ const rel = path.relative(cwd, f)
84
+ if (dirTargets.includes(rel)) return false
85
+ if (cleanPreset.preserve.some(p => rel === p || rel.startsWith(p + path.sep))) return false
86
+ return true
87
+ })
88
+
89
+ const allToRemove = [...filteredFlat, ...dirEntries]
90
+
91
+ if (allToRemove.length === 0) {
92
+ success('No existing AI config files to remove')
93
+ return ctx
94
+ }
95
+
96
+ const choices = allToRemove.map(f => ({
97
+ name: path.relative(cwd, f).replace(/\\/g, '/'),
98
+ value: f,
99
+ checked: true,
100
+ }))
101
+
102
+ const selected = await checkbox({
103
+ message: cleanPreset.selectionMessage,
104
+ choices,
105
+ })
106
+
107
+ if (!selected || selected.length === 0) {
108
+ success('No AI config files selected for removal')
109
+ return ctx
110
+ }
111
+
112
+ warn('Removing selected AI config files:')
113
+ for (const f of selected) {
114
+ info(`${MARKERS.EMPTY}${f.replace(cwd + path.sep, '')}`)
115
+ await fse.remove(f)
116
+ }
117
+ success('Removed existing AI config files')
118
+
119
+ return ctx
120
+ }
@@ -1,120 +1,120 @@
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: heading
10
- // level (##/###/####), the `Step N,` prefix and any ` (if Yes)` suffix have all
11
- // drifted before, and every drift silently turned the patch into a no-op that
12
- // only showed up as a warning nobody read.
13
- const HISTORY_STEP_TITLE = 'Archive project history'
14
- const ARCHITECTURE_STEP_TITLE = 'Generate ARCHITECTURE.md'
15
- const DESIGN_STEP_TITLE = 'Generate DESIGN.md'
16
-
17
- function stepHeadingPattern(title) {
18
- const escaped = title.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
19
- return new RegExp(`^#{2,4}\\s+(?:Step\\s+\\d+[a-z]?[,:]\\s+)?${escaped}\\b`, 'i')
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
- // The block ends at the next heading or rule, NOT at end of file. Scanning
33
- // only for `---` meant a document without one lost everything below the
34
- // matched step.
35
- let end = lines.length
36
- for (let i = start + 1; i < lines.length; i++) {
37
- const line = lines[i].trim()
38
- if (line === '---' || /^#{1,4}\s/.test(line)) { end = i; break }
39
- }
40
-
41
- lines.splice(start + 1, end - start - 1, '', `> ${note}`, '')
42
- return { content: lines.join('\n'), matched: true }
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
- // Replacer function so $&, $' and $` in injected content stay literal.
70
- return content.replace(pattern, () => `${start}\n${replacement.trim()}\n${end}`)
71
- }
72
-
73
- export async function patchAgentsMd(ctx) {
74
- const obInitSkillPath = path.join(process.cwd(), '.agents', 'skills', 'pc-repo-initialize', 'SKILL.md')
75
- if (!await fse.pathExists(obInitSkillPath)) return
76
-
77
- let content = await fse.readFile(obInitSkillPath, 'utf-8')
78
- const patches = []
79
-
80
- const skips = [
81
- [ctx.hasOpenspec, HISTORY_STEP_TITLE,
82
- 'Skipped during onboarding: this project already had an openspec/ history. Do not archive again; continue with the next step.'],
83
- [ctx.hasArchitecture, ARCHITECTURE_STEP_TITLE,
84
- 'Skipped during onboarding: ARCHITECTURE.md already exists. Run /make-architecture to regenerate it, which keeps its update mode.'],
85
- [ctx.hasDesign, DESIGN_STEP_TITLE,
86
- 'Skipped during onboarding: DESIGN.md already exists. Run /make-design to regenerate it, which keeps its update mode.'],
87
- ]
88
-
89
- for (const [enabled, title, note] of skips) {
90
- if (!enabled) continue
91
- const result = skipStepBlock(content, title, note)
92
- if (!result.matched) {
93
- warn(`pc-repo-initialize SKILL.md step "${title}" not found: template drift? Skipping this patch.`)
94
- continue
95
- }
96
- content = result.content
97
- patches.push(`Step "${title}" marked as skipped, file already exists`)
98
- }
99
-
100
- if (patches.length > 0) {
101
- await fse.writeFile(obInitSkillPath, content, 'utf-8')
102
- for (const msg of patches) info(msg)
103
- success('pc-repo-initialize SKILL.md patched for existing project state')
104
- }
105
- }
106
-
107
- export async function patchAgentGuidance(backlogPlatform, repoPlatform, cwd = process.cwd()) {
108
- const agentsMdPath = path.join(cwd, 'AGENTS.md')
109
- if (await fse.pathExists(agentsMdPath)) {
110
- const repo = repoPlatform ?? backlogPlatform ?? 'github'
111
- let content = await fse.readFile(agentsMdPath, 'utf-8')
112
- content = replaceBetween(content, PLATFORM_WORKFLOW_START, PLATFORM_WORKFLOW_END, mixedPlatformContent(backlogPlatform, repo, 'workflow'))
113
- content = replaceBetween(content, PLATFORM_SKILLS_GUIDE_START, PLATFORM_SKILLS_GUIDE_END, mixedPlatformContent(backlogPlatform, repo, 'skillsGuide'))
114
- await fse.writeFile(agentsMdPath, `${content.replace(/\s*$/, '')}\n`, 'utf-8')
115
- const label = backlogPlatform === repo ? repo : `${backlogPlatform}→${repo}`
116
- success(`AGENTS.md patched for platform workflow: ${label}`)
117
- }
118
- }
119
-
120
-
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: heading
10
+ // level (##/###/####), the `Step N,` prefix and any ` (if Yes)` suffix have all
11
+ // drifted before, and every drift silently turned the patch into a no-op that
12
+ // only showed up as a warning nobody read.
13
+ const HISTORY_STEP_TITLE = 'Archive project history'
14
+ const ARCHITECTURE_STEP_TITLE = 'Generate ARCHITECTURE.md'
15
+ const DESIGN_STEP_TITLE = 'Generate DESIGN.md'
16
+
17
+ function stepHeadingPattern(title) {
18
+ const escaped = title.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
19
+ return new RegExp(`^#{2,4}\\s+(?:Step\\s+\\d+[a-z]?[,:]\\s+)?${escaped}\\b`, 'i')
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
+ // The block ends at the next heading or rule, NOT at end of file. Scanning
33
+ // only for `---` meant a document without one lost everything below the
34
+ // matched step.
35
+ let end = lines.length
36
+ for (let i = start + 1; i < lines.length; i++) {
37
+ const line = lines[i].trim()
38
+ if (line === '---' || /^#{1,4}\s/.test(line)) { end = i; break }
39
+ }
40
+
41
+ lines.splice(start + 1, end - start - 1, '', `> ${note}`, '')
42
+ return { content: lines.join('\n'), matched: true }
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
+ // Replacer function so $&, $' and $` in injected content stay literal.
70
+ return content.replace(pattern, () => `${start}\n${replacement.trim()}\n${end}`)
71
+ }
72
+
73
+ export async function patchAgentsMd(ctx) {
74
+ const obInitSkillPath = path.join(process.cwd(), '.agents', 'skills', 'pc-repo-initialize', 'SKILL.md')
75
+ if (!await fse.pathExists(obInitSkillPath)) return
76
+
77
+ let content = await fse.readFile(obInitSkillPath, 'utf-8')
78
+ const patches = []
79
+
80
+ const skips = [
81
+ [ctx.hasOpenspec, HISTORY_STEP_TITLE,
82
+ 'Skipped during onboarding: this project already had an openspec/ history. Do not archive again; continue with the next step.'],
83
+ [ctx.hasArchitecture, ARCHITECTURE_STEP_TITLE,
84
+ 'Skipped during onboarding: ARCHITECTURE.md already exists. Run /make-architecture to regenerate it, which keeps its update mode.'],
85
+ [ctx.hasDesign, DESIGN_STEP_TITLE,
86
+ 'Skipped during onboarding: DESIGN.md already exists. Run /make-design to regenerate it, which keeps its update mode.'],
87
+ ]
88
+
89
+ for (const [enabled, title, note] of skips) {
90
+ if (!enabled) continue
91
+ const result = skipStepBlock(content, title, note)
92
+ if (!result.matched) {
93
+ warn(`pc-repo-initialize SKILL.md step "${title}" not found: template drift? Skipping this patch.`)
94
+ continue
95
+ }
96
+ content = result.content
97
+ patches.push(`Step "${title}" marked as skipped, file already exists`)
98
+ }
99
+
100
+ if (patches.length > 0) {
101
+ await fse.writeFile(obInitSkillPath, content, 'utf-8')
102
+ for (const msg of patches) info(msg)
103
+ success('pc-repo-initialize SKILL.md patched for existing project state')
104
+ }
105
+ }
106
+
107
+ export async function patchAgentGuidance(backlogPlatform, repoPlatform, cwd = process.cwd()) {
108
+ const agentsMdPath = path.join(cwd, 'AGENTS.md')
109
+ if (await fse.pathExists(agentsMdPath)) {
110
+ const repo = repoPlatform ?? backlogPlatform ?? 'github'
111
+ let content = await fse.readFile(agentsMdPath, 'utf-8')
112
+ content = replaceBetween(content, PLATFORM_WORKFLOW_START, PLATFORM_WORKFLOW_END, mixedPlatformContent(backlogPlatform, repo, 'workflow'))
113
+ content = replaceBetween(content, PLATFORM_SKILLS_GUIDE_START, PLATFORM_SKILLS_GUIDE_END, mixedPlatformContent(backlogPlatform, repo, 'skillsGuide'))
114
+ await fse.writeFile(agentsMdPath, `${content.replace(/\s*$/, '')}\n`, 'utf-8')
115
+ const label = backlogPlatform === repo ? repo : `${backlogPlatform}→${repo}`
116
+ success(`AGENTS.md patched for platform workflow: ${label}`)
117
+ }
118
+ }
119
+
120
+
@@ -1,79 +1,79 @@
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
- // relativePath is POSIX-style relative to the project root; commands live in
34
- // .opencode/commands/, skill-backed procedures (plan-archive, ops-ship) in
35
- // .agents/skills/<name>/SKILL.md.
36
- function patchFile(relativePath, startMarker, endMarker, content, platform, cwd = process.cwd()) {
37
- const targetPath = path.join(cwd, ...relativePath.split('/'))
38
- if (!fse.pathExistsSync(targetPath)) return
39
-
40
- let fileContent = fse.readFileSync(targetPath, 'utf-8')
41
- if (!fileContent.includes(startMarker) || !fileContent.includes(endMarker)) return
42
-
43
- const pattern = new RegExp(`${startMarker.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}[\\s\\S]*?${endMarker.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`)
44
- // Replacer function, not a replacement string: the injected fragments contain
45
- // shell quoting like $'...' and a string replacement expands $' as "everything
46
- // after the match", silently truncating the command. That is how the ops-ship
47
- // screenshot comment lost its entire --body argument.
48
- fileContent = fileContent.replace(pattern, () => `${startMarker}\n${content.trim()}\n${endMarker}`)
49
- fse.writeFileSync(targetPath, `${fileContent.replace(/\s*$/, '')}\n`, 'utf-8')
50
- success(`${relativePath} content injected for platform: ${platform}`)
51
- }
52
-
53
- export async function patchArchiveCommand(platform, cwd = process.cwd()) {
54
- const repoPlatform = typeof platform === 'object' ? (platform.repoPlatform ?? platform.backlogPlatform ?? 'github') : platform
55
- const replacement = _archive[repoPlatform]
56
- if (!replacement) return
57
- patchFile('.agents/skills/pc-plan-archive/SKILL.md', '<!-- PC-PLATFORM-ARCHIVE-START -->', '<!-- PC-PLATFORM-ARCHIVE-END -->', replacement, repoPlatform, cwd)
58
- }
59
-
60
- export async function patchOpsShip(platform, cwd = process.cwd()) {
61
- const repoPlatform = typeof platform === 'object' ? (platform.repoPlatform ?? 'github') : platform
62
- const replacement = _ship[repoPlatform]
63
- if (!replacement) return
64
- patchFile('.agents/skills/pc-ops-ship/SKILL.md', '<!-- PC-PLATFORM-SHIP-START -->', '<!-- PC-PLATFORM-SHIP-END -->', replacement, repoPlatform, cwd)
65
- }
66
-
67
- export async function patchOpsReview(platform, cwd = process.cwd()) {
68
- const repoPlatform = typeof platform === 'object' ? (platform.repoPlatform ?? 'github') : platform
69
- const replacement = _review[repoPlatform]
70
- if (!replacement) return
71
- patchFile('.opencode/commands/ops-review.md', '<!-- PC-PLATFORM-REVIEW-START -->', '<!-- PC-PLATFORM-REVIEW-END -->', replacement, repoPlatform, cwd)
72
- }
73
-
74
- export async function patchOpsBacklog(platform, cwd = process.cwd()) {
75
- const backlogPlatform = typeof platform === 'object' ? (platform.backlogPlatform ?? 'github') : platform
76
- const replacement = _backlog[backlogPlatform]
77
- if (!replacement) return
78
- patchFile('.opencode/commands/ops-backlog.md', '<!-- PC-PLATFORM-BACKLOG-START -->', '<!-- PC-PLATFORM-BACKLOG-END -->', replacement, backlogPlatform, cwd)
79
- }
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
+ // relativePath is POSIX-style relative to the project root; commands live in
34
+ // .opencode/commands/, skill-backed procedures (plan-archive, ops-ship) in
35
+ // .agents/skills/<name>/SKILL.md.
36
+ function patchFile(relativePath, startMarker, endMarker, content, platform, cwd = process.cwd()) {
37
+ const targetPath = path.join(cwd, ...relativePath.split('/'))
38
+ if (!fse.pathExistsSync(targetPath)) return
39
+
40
+ let fileContent = fse.readFileSync(targetPath, 'utf-8')
41
+ if (!fileContent.includes(startMarker) || !fileContent.includes(endMarker)) return
42
+
43
+ const pattern = new RegExp(`${startMarker.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}[\\s\\S]*?${endMarker.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`)
44
+ // Replacer function, not a replacement string: the injected fragments contain
45
+ // shell quoting like $'...' and a string replacement expands $' as "everything
46
+ // after the match", silently truncating the command. That is how the ops-ship
47
+ // screenshot comment lost its entire --body argument.
48
+ fileContent = fileContent.replace(pattern, () => `${startMarker}\n${content.trim()}\n${endMarker}`)
49
+ fse.writeFileSync(targetPath, `${fileContent.replace(/\s*$/, '')}\n`, 'utf-8')
50
+ success(`${relativePath} content injected for platform: ${platform}`)
51
+ }
52
+
53
+ export async function patchArchiveCommand(platform, cwd = process.cwd()) {
54
+ const repoPlatform = typeof platform === 'object' ? (platform.repoPlatform ?? platform.backlogPlatform ?? 'github') : platform
55
+ const replacement = _archive[repoPlatform]
56
+ if (!replacement) return
57
+ patchFile('.agents/skills/pc-plan-archive/SKILL.md', '<!-- PC-PLATFORM-ARCHIVE-START -->', '<!-- PC-PLATFORM-ARCHIVE-END -->', replacement, repoPlatform, cwd)
58
+ }
59
+
60
+ export async function patchOpsShip(platform, cwd = process.cwd()) {
61
+ const repoPlatform = typeof platform === 'object' ? (platform.repoPlatform ?? 'github') : platform
62
+ const replacement = _ship[repoPlatform]
63
+ if (!replacement) return
64
+ patchFile('.agents/skills/pc-ops-ship/SKILL.md', '<!-- PC-PLATFORM-SHIP-START -->', '<!-- PC-PLATFORM-SHIP-END -->', replacement, repoPlatform, cwd)
65
+ }
66
+
67
+ export async function patchOpsReview(platform, cwd = process.cwd()) {
68
+ const repoPlatform = typeof platform === 'object' ? (platform.repoPlatform ?? 'github') : platform
69
+ const replacement = _review[repoPlatform]
70
+ if (!replacement) return
71
+ patchFile('.opencode/commands/ops-review.md', '<!-- PC-PLATFORM-REVIEW-START -->', '<!-- PC-PLATFORM-REVIEW-END -->', replacement, repoPlatform, cwd)
72
+ }
73
+
74
+ export async function patchOpsBacklog(platform, cwd = process.cwd()) {
75
+ const backlogPlatform = typeof platform === 'object' ? (platform.backlogPlatform ?? 'github') : platform
76
+ const replacement = _backlog[backlogPlatform]
77
+ if (!replacement) return
78
+ patchFile('.opencode/commands/ops-backlog.md', '<!-- PC-PLATFORM-BACKLOG-START -->', '<!-- PC-PLATFORM-BACKLOG-END -->', replacement, backlogPlatform, cwd)
79
+ }