@plainconceptsplatform/agent-harness 2.2.0 → 2.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plainconceptsplatform/agent-harness",
3
- "version": "2.2.0",
3
+ "version": "2.2.1",
4
4
  "description": "Installs the Plain Concepts Platform Harness into any codebase, and keeps it up to date. Wires OpenCode, OpenSpec, codegraph, and agentmemory into a multi-agent workflow that runs on native parallel subagents.",
5
5
  "keywords": [
6
6
  "opencode",
@@ -235,7 +235,11 @@ export async function runMigrate({ cwd = process.cwd(), force = false, dryRun =
235
235
  // 4. Rewrite identifiers everywhere that survives, so preserved skills and
236
236
  // custom agents stop pointing at names that no longer exist.
237
237
  let rewritten = 0
238
- for (const dir of [skillsDir, opencodeDir, path.join(cwd, 'openspec')]) {
238
+ // .github/workflows is included because GitHub Agentic Workflow definitions
239
+ // call the skills by name (skill("pc-repo-audit")), so a stale ob- name there
240
+ // fails at run time in CI rather than locally. Their compiled .lock.yml files
241
+ // embed the same prompt text, so recompile with `gh aw compile` afterwards.
242
+ for (const dir of [skillsDir, opencodeDir, path.join(cwd, 'openspec'), path.join(cwd, '.github', 'workflows')]) {
239
243
  if (!await fse.pathExists(dir)) continue
240
244
  for (const file of await collectTextFiles(dir)) {
241
245
  const before = await fse.readFile(file, 'utf-8')
@@ -281,6 +285,18 @@ export async function runMigrate({ cwd = process.cwd(), force = false, dryRun =
281
285
  console.log(chalk.bold('Next: run update to install the current harness.'))
282
286
  console.log(chalk.dim(' npx @plainconceptsplatform/agent-harness@latest update'))
283
287
 
288
+ // The .md definitions were rewritten above, but the compiled .lock.yml files
289
+ // embed the same prompt text and are what CI actually runs.
290
+ if (await fse.pathExists(path.join(cwd, '.github', 'workflows'))) {
291
+ const locks = (await fse.readdir(path.join(cwd, '.github', 'workflows')).catch(() => []))
292
+ .filter(f => f.endsWith('.lock.yml'))
293
+ if (locks.length > 0) {
294
+ console.log()
295
+ console.log(chalk.bold(`Then recompile ${locks.length} agentic workflow(s), whose .lock.yml still holds the old skill names:`))
296
+ console.log(chalk.dim(' gh aw compile'))
297
+ }
298
+ }
299
+
284
300
  return { migrated: true, plan, rewritten, demoted }
285
301
  }
286
302
 
@@ -1,118 +1,119 @@
1
- import fse from 'fs-extra'
2
- import path from 'path'
3
- import { fileURLToPath } from 'url'
4
- import { info, success, warn } from '../../utils/exec.js'
5
-
6
- const __dirname = path.dirname(fileURLToPath(import.meta.url))
7
- const agentsContent = await fse.readJson(path.resolve(__dirname, '../../presets/agents-content.json'))
8
-
9
- // Steps are matched by their title text, not by exact heading string:
10
- // heading level (###/####) and step numbers have drifted before and silently
11
- // broke removal. Matching `^#{3,4} Step N, <title>` survives both.
12
- const HISTORY_STEP_TITLE = 'Archive project history'
13
- const CHAIN_STEP_TITLE = 'Chain make commands'
14
-
15
- const CHAIN_CONFIRM_LINE = '- ARCHITECTURE.md generated'
16
-
17
- function stepHeadingPattern(title) {
18
- const escaped = title.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
19
- return new RegExp(`^#{2,4} Step \\d+, ${escaped}`)
20
- }
21
-
22
- // The step heading is kept and its body replaced with an explicit skip note.
23
- // Removing the block and renumbering the remaining steps (the old approach)
24
- // went stale against prose cross-references like "Skip steps 2, 3, and 4";
25
- // stable numbering plus a visible reason is unambiguous for the agent.
26
- export function skipStepBlock(content, title, note) {
27
- const lines = content.split('\n')
28
- const pattern = stepHeadingPattern(title)
29
- const start = lines.findIndex(l => pattern.test(l.trim()))
30
- if (start === -1) return { content, matched: false }
31
-
32
- let end = lines.length
33
- for (let i = start + 1; i < lines.length; i++) {
34
- if (lines[i].trim() === '---') { end = i; break }
35
- }
36
-
37
- lines.splice(start + 1, end - start - 1, '', `> ${note}`, '')
38
- return { content: lines.join('\n'), matched: true }
39
- }
40
-
41
- function removeConfirmLine(content, line) {
42
- return content.split('\n').filter(l => l.trim() !== line.trim()).join('\n')
43
- }
44
-
45
- const PLATFORM_WORKFLOW_START = '<!-- PC-PLATFORM-WORKFLOW-START -->'
46
- const PLATFORM_WORKFLOW_END = '<!-- PC-PLATFORM-WORKFLOW-END -->'
47
- const PLATFORM_SKILLS_GUIDE_START = '<!-- PC-PLATFORM-SKILLS-GUIDE-START -->'
48
- const PLATFORM_SKILLS_GUIDE_END = '<!-- PC-PLATFORM-SKILLS-GUIDE-END -->'
49
-
50
- function platformContent(platform, key) {
51
- const p = agentsContent.platform[platform] ?? agentsContent.platform.github
52
- return p[key] ?? ''
53
- }
54
-
55
- function mixedPlatformContent(backlogPlatform, repoPlatform, key) {
56
- if (backlogPlatform === repoPlatform) {
57
- return platformContent(backlogPlatform, key)
58
- }
59
- const backlog = agentsContent.platform[backlogPlatform] ?? agentsContent.platform.github
60
- const repo = agentsContent.platform[repoPlatform] ?? agentsContent.platform.github
61
- const mixed = agentsContent.platform.mixed ?? {}
62
- const mixedKey = `${backlogPlatform}-${repoPlatform}`
63
- return mixed[mixedKey]?.[key] ?? mixed.default?.[key]?.replace('{backlog}', backlog[key] ?? '').replace('{repo}', repo[key] ?? '') ?? platformContent(repoPlatform, key)
64
- }
65
-
66
- function replaceBetween(content, start, end, replacement) {
67
- if (!content.includes(start) || !content.includes(end)) return content
68
- const pattern = new RegExp(`${start}[\\s\\S]*?${end}`)
69
- return content.replace(pattern, `${start}\n${replacement.trim()}\n${end}`)
70
- }
71
-
72
- export async function patchAgentsMd(ctx) {
73
- const obInitSkillPath = path.join(process.cwd(), '.agents', 'skills', 'pc-repo-initialize', 'SKILL.md')
74
- if (!await fse.pathExists(obInitSkillPath)) return
75
-
76
- let content = await fse.readFile(obInitSkillPath, 'utf-8')
77
- const patches = []
78
-
79
- const skips = [
80
- [ctx.hasOpenspec, HISTORY_STEP_TITLE, null,
81
- 'Skipped during onboarding: this project already had an openspec/ history. Do not archive again; continue with the next step.'],
82
- [ctx.hasDesign || ctx.hasArchitecture, CHAIN_STEP_TITLE, CHAIN_CONFIRM_LINE,
83
- 'Skipped during onboarding: project files already exist. Run /make-architecture or /make-design individually to regenerate.'],
84
- ]
85
-
86
- for (const [enabled, title, confirmLine, note] of skips) {
87
- if (!enabled) continue
88
- const result = skipStepBlock(content, title, note)
89
- if (!result.matched) {
90
- warn(`pc-repo-initialize SKILL.md step "${title}" not found: template drift? Skipping this patch.`)
91
- continue
92
- }
93
- content = result.content
94
- if (confirmLine) content = removeConfirmLine(content, confirmLine)
95
- patches.push(`Step "${title}" marked as skipped, file already exists`)
96
- }
97
-
98
- if (patches.length > 0) {
99
- await fse.writeFile(obInitSkillPath, content, 'utf-8')
100
- for (const msg of patches) info(msg)
101
- success('pc-repo-initialize SKILL.md patched for existing project state')
102
- }
103
- }
104
-
105
- export async function patchAgentGuidance(backlogPlatform, repoPlatform, cwd = process.cwd()) {
106
- const agentsMdPath = path.join(cwd, 'AGENTS.md')
107
- if (await fse.pathExists(agentsMdPath)) {
108
- const repo = repoPlatform ?? backlogPlatform ?? 'github'
109
- let content = await fse.readFile(agentsMdPath, 'utf-8')
110
- content = replaceBetween(content, PLATFORM_WORKFLOW_START, PLATFORM_WORKFLOW_END, mixedPlatformContent(backlogPlatform, repo, 'workflow'))
111
- content = replaceBetween(content, PLATFORM_SKILLS_GUIDE_START, PLATFORM_SKILLS_GUIDE_END, mixedPlatformContent(backlogPlatform, repo, 'skillsGuide'))
112
- await fse.writeFile(agentsMdPath, `${content.replace(/\s*$/, '')}\n`, 'utf-8')
113
- const label = backlogPlatform === repo ? repo : `${backlogPlatform}→${repo}`
114
- success(`AGENTS.md patched for platform workflow: ${label}`)
115
- }
116
- }
117
-
118
-
1
+ import fse from 'fs-extra'
2
+ import path from 'path'
3
+ import { fileURLToPath } from 'url'
4
+ import { info, success, warn } from '../../utils/exec.js'
5
+
6
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
7
+ const agentsContent = await fse.readJson(path.resolve(__dirname, '../../presets/agents-content.json'))
8
+
9
+ // Steps are matched by their title text, not by exact heading string:
10
+ // heading level (###/####) and step numbers have drifted before and silently
11
+ // broke removal. Matching `^#{3,4} Step N, <title>` survives both.
12
+ const HISTORY_STEP_TITLE = 'Archive project history'
13
+ const CHAIN_STEP_TITLE = 'Chain make commands'
14
+
15
+ const CHAIN_CONFIRM_LINE = '- ARCHITECTURE.md generated'
16
+
17
+ function stepHeadingPattern(title) {
18
+ const escaped = title.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
19
+ return new RegExp(`^#{2,4} Step \\d+, ${escaped}`)
20
+ }
21
+
22
+ // The step heading is kept and its body replaced with an explicit skip note.
23
+ // Removing the block and renumbering the remaining steps (the old approach)
24
+ // went stale against prose cross-references like "Skip steps 2, 3, and 4";
25
+ // stable numbering plus a visible reason is unambiguous for the agent.
26
+ export function skipStepBlock(content, title, note) {
27
+ const lines = content.split('\n')
28
+ const pattern = stepHeadingPattern(title)
29
+ const start = lines.findIndex(l => pattern.test(l.trim()))
30
+ if (start === -1) return { content, matched: false }
31
+
32
+ let end = lines.length
33
+ for (let i = start + 1; i < lines.length; i++) {
34
+ if (lines[i].trim() === '---') { end = i; break }
35
+ }
36
+
37
+ lines.splice(start + 1, end - start - 1, '', `> ${note}`, '')
38
+ return { content: lines.join('\n'), matched: true }
39
+ }
40
+
41
+ function removeConfirmLine(content, line) {
42
+ return content.split('\n').filter(l => l.trim() !== line.trim()).join('\n')
43
+ }
44
+
45
+ const PLATFORM_WORKFLOW_START = '<!-- PC-PLATFORM-WORKFLOW-START -->'
46
+ const PLATFORM_WORKFLOW_END = '<!-- PC-PLATFORM-WORKFLOW-END -->'
47
+ const PLATFORM_SKILLS_GUIDE_START = '<!-- PC-PLATFORM-SKILLS-GUIDE-START -->'
48
+ const PLATFORM_SKILLS_GUIDE_END = '<!-- PC-PLATFORM-SKILLS-GUIDE-END -->'
49
+
50
+ function platformContent(platform, key) {
51
+ const p = agentsContent.platform[platform] ?? agentsContent.platform.github
52
+ return p[key] ?? ''
53
+ }
54
+
55
+ function mixedPlatformContent(backlogPlatform, repoPlatform, key) {
56
+ if (backlogPlatform === repoPlatform) {
57
+ return platformContent(backlogPlatform, key)
58
+ }
59
+ const backlog = agentsContent.platform[backlogPlatform] ?? agentsContent.platform.github
60
+ const repo = agentsContent.platform[repoPlatform] ?? agentsContent.platform.github
61
+ const mixed = agentsContent.platform.mixed ?? {}
62
+ const mixedKey = `${backlogPlatform}-${repoPlatform}`
63
+ return mixed[mixedKey]?.[key] ?? mixed.default?.[key]?.replace('{backlog}', backlog[key] ?? '').replace('{repo}', repo[key] ?? '') ?? platformContent(repoPlatform, key)
64
+ }
65
+
66
+ function replaceBetween(content, start, end, replacement) {
67
+ if (!content.includes(start) || !content.includes(end)) return content
68
+ const pattern = new RegExp(`${start}[\\s\\S]*?${end}`)
69
+ // 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, null,
82
+ 'Skipped during onboarding: this project already had an openspec/ history. Do not archive again; continue with the next step.'],
83
+ [ctx.hasDesign || ctx.hasArchitecture, CHAIN_STEP_TITLE, CHAIN_CONFIRM_LINE,
84
+ 'Skipped during onboarding: project files already exist. Run /make-architecture or /make-design individually to regenerate.'],
85
+ ]
86
+
87
+ for (const [enabled, title, confirmLine, note] of skips) {
88
+ if (!enabled) continue
89
+ const result = skipStepBlock(content, title, note)
90
+ if (!result.matched) {
91
+ warn(`pc-repo-initialize SKILL.md step "${title}" not found: template drift? Skipping this patch.`)
92
+ continue
93
+ }
94
+ content = result.content
95
+ if (confirmLine) content = removeConfirmLine(content, confirmLine)
96
+ patches.push(`Step "${title}" marked as skipped, file already exists`)
97
+ }
98
+
99
+ if (patches.length > 0) {
100
+ await fse.writeFile(obInitSkillPath, content, 'utf-8')
101
+ for (const msg of patches) info(msg)
102
+ success('pc-repo-initialize SKILL.md patched for existing project state')
103
+ }
104
+ }
105
+
106
+ export async function patchAgentGuidance(backlogPlatform, repoPlatform, cwd = process.cwd()) {
107
+ const agentsMdPath = path.join(cwd, 'AGENTS.md')
108
+ if (await fse.pathExists(agentsMdPath)) {
109
+ const repo = repoPlatform ?? backlogPlatform ?? 'github'
110
+ let content = await fse.readFile(agentsMdPath, 'utf-8')
111
+ content = replaceBetween(content, PLATFORM_WORKFLOW_START, PLATFORM_WORKFLOW_END, mixedPlatformContent(backlogPlatform, repo, 'workflow'))
112
+ content = replaceBetween(content, PLATFORM_SKILLS_GUIDE_START, PLATFORM_SKILLS_GUIDE_END, mixedPlatformContent(backlogPlatform, repo, 'skillsGuide'))
113
+ await fse.writeFile(agentsMdPath, `${content.replace(/\s*$/, '')}\n`, 'utf-8')
114
+ const label = backlogPlatform === repo ? repo : `${backlogPlatform}→${repo}`
115
+ success(`AGENTS.md patched for platform workflow: ${label}`)
116
+ }
117
+ }
118
+
119
+
@@ -1,91 +1,95 @@
1
- import fse from 'fs-extra'
2
- import path from 'path'
3
- import { fileURLToPath } from 'url'
4
- import { success } from '../../utils/exec.js'
5
-
6
- const __dirname = path.dirname(fileURLToPath(import.meta.url))
7
-
8
- const _archive = {
9
- azure: await fse.readFile(path.resolve(__dirname, '../../fragments/archive/az.md'), 'utf-8'),
10
- github: await fse.readFile(path.resolve(__dirname, '../../fragments/archive/gh.md'), 'utf-8'),
11
- gitlab: await fse.readFile(path.resolve(__dirname, '../../fragments/archive/gl.md'), 'utf-8'),
12
- none: await fse.readFile(path.resolve(__dirname, '../../fragments/archive/none.md'), 'utf-8'),
13
- }
14
-
15
- const _ship = {
16
- github: await fse.readFile(path.resolve(__dirname, '../../fragments/ops-ship/gh.md'), 'utf-8'),
17
- azure: await fse.readFile(path.resolve(__dirname, '../../fragments/ops-ship/az.md'), 'utf-8'),
18
- gitlab: await fse.readFile(path.resolve(__dirname, '../../fragments/ops-ship/gl.md'), 'utf-8'),
19
- }
20
-
21
- const _review = {
22
- github: await fse.readFile(path.resolve(__dirname, '../../fragments/ops-review/gh.md'), 'utf-8'),
23
- azure: await fse.readFile(path.resolve(__dirname, '../../fragments/ops-review/az.md'), 'utf-8'),
24
- gitlab: await fse.readFile(path.resolve(__dirname, '../../fragments/ops-review/gl.md'), 'utf-8'),
25
- }
26
-
27
- const _backlog = {
28
- github: await fse.readFile(path.resolve(__dirname, '../../fragments/ops-backlog/gh.md'), 'utf-8'),
29
- azure: await fse.readFile(path.resolve(__dirname, '../../fragments/ops-backlog/az.md'), 'utf-8'),
30
- jira: await fse.readFile(path.resolve(__dirname, '../../fragments/ops-backlog/jira.md'), 'utf-8'),
31
- }
32
-
33
- // Evidence comments target the backlog platform (where the issue/work-item lives).
34
- const _evidence = {
35
- github: await fse.readFile(path.resolve(__dirname, '../../fragments/ops-evidence/gh.md'), 'utf-8'),
36
- azure: await fse.readFile(path.resolve(__dirname, '../../fragments/ops-evidence/az.md'), 'utf-8'),
37
- jira: await fse.readFile(path.resolve(__dirname, '../../fragments/ops-evidence/jira.md'), 'utf-8'),
38
- }
39
-
40
- // relativePath is POSIX-style relative to the project root; commands live in
41
- // .opencode/commands/, skill-backed procedures (plan-archive, ops-ship) in
42
- // .agents/skills/<name>/SKILL.md.
43
- function patchFile(relativePath, startMarker, endMarker, content, platform, cwd = process.cwd()) {
44
- const targetPath = path.join(cwd, ...relativePath.split('/'))
45
- if (!fse.pathExistsSync(targetPath)) return
46
-
47
- let fileContent = fse.readFileSync(targetPath, 'utf-8')
48
- if (!fileContent.includes(startMarker) || !fileContent.includes(endMarker)) return
49
-
50
- const pattern = new RegExp(`${startMarker.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}[\\s\\S]*?${endMarker.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`)
51
- fileContent = fileContent.replace(pattern, `${startMarker}\n${content.trim()}\n${endMarker}`)
52
- fse.writeFileSync(targetPath, `${fileContent.replace(/\s*$/, '')}\n`, 'utf-8')
53
- success(`${relativePath} content injected for platform: ${platform}`)
54
- }
55
-
56
- export async function patchArchiveCommand(platform, cwd = process.cwd()) {
57
- const repoPlatform = typeof platform === 'object' ? (platform.repoPlatform ?? platform.backlogPlatform ?? 'github') : platform
58
- const replacement = _archive[repoPlatform]
59
- if (!replacement) return
60
- patchFile('.agents/skills/pc-plan-archive/SKILL.md', '<!-- PC-PLATFORM-ARCHIVE-START -->', '<!-- PC-PLATFORM-ARCHIVE-END -->', replacement, repoPlatform, cwd)
61
- }
62
-
63
- export async function patchOpsShip(platform, cwd = process.cwd()) {
64
- const repoPlatform = typeof platform === 'object' ? (platform.repoPlatform ?? 'github') : platform
65
- const replacement = _ship[repoPlatform]
66
- if (!replacement) return
67
- patchFile('.agents/skills/pc-ops-ship/SKILL.md', '<!-- PC-PLATFORM-SHIP-START -->', '<!-- PC-PLATFORM-SHIP-END -->', replacement, repoPlatform, cwd)
68
- }
69
-
70
- export async function patchOpsReview(platform, cwd = process.cwd()) {
71
- const repoPlatform = typeof platform === 'object' ? (platform.repoPlatform ?? 'github') : platform
72
- const replacement = _review[repoPlatform]
73
- if (!replacement) return
74
- patchFile('.opencode/commands/ops-review.md', '<!-- PC-PLATFORM-REVIEW-START -->', '<!-- PC-PLATFORM-REVIEW-END -->', replacement, repoPlatform, cwd)
75
- }
76
-
77
- export async function patchOpsBacklog(platform, cwd = process.cwd()) {
78
- const backlogPlatform = typeof platform === 'object' ? (platform.backlogPlatform ?? 'github') : platform
79
- const replacement = _backlog[backlogPlatform]
80
- if (!replacement) return
81
- patchFile('.opencode/commands/ops-backlog.md', '<!-- PC-PLATFORM-BACKLOG-START -->', '<!-- PC-PLATFORM-BACKLOG-END -->', replacement, backlogPlatform, cwd)
82
- }
83
-
84
- export async function patchOpsEvidence(platform, cwd = process.cwd()) {
85
- const backlogPlatform = typeof platform === 'object' ? (platform.backlogPlatform ?? 'github') : platform
86
- const replacement = _evidence[backlogPlatform]
87
- // browser/none have no CLI to post with: leave the markers empty (the skill skips commenting).
88
- if (!replacement) return
89
- patchFile('.agents/skills/pc-ops-evidence/SKILL.md', '<!-- PC-PLATFORM-EVIDENCE-START -->', '<!-- PC-PLATFORM-EVIDENCE-END -->', replacement, backlogPlatform, cwd)
90
- }
91
-
1
+ import fse from 'fs-extra'
2
+ import path from 'path'
3
+ import { fileURLToPath } from 'url'
4
+ import { success } from '../../utils/exec.js'
5
+
6
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
7
+
8
+ const _archive = {
9
+ azure: await fse.readFile(path.resolve(__dirname, '../../fragments/archive/az.md'), 'utf-8'),
10
+ github: await fse.readFile(path.resolve(__dirname, '../../fragments/archive/gh.md'), 'utf-8'),
11
+ gitlab: await fse.readFile(path.resolve(__dirname, '../../fragments/archive/gl.md'), 'utf-8'),
12
+ none: await fse.readFile(path.resolve(__dirname, '../../fragments/archive/none.md'), 'utf-8'),
13
+ }
14
+
15
+ const _ship = {
16
+ github: await fse.readFile(path.resolve(__dirname, '../../fragments/ops-ship/gh.md'), 'utf-8'),
17
+ azure: await fse.readFile(path.resolve(__dirname, '../../fragments/ops-ship/az.md'), 'utf-8'),
18
+ gitlab: await fse.readFile(path.resolve(__dirname, '../../fragments/ops-ship/gl.md'), 'utf-8'),
19
+ }
20
+
21
+ const _review = {
22
+ github: await fse.readFile(path.resolve(__dirname, '../../fragments/ops-review/gh.md'), 'utf-8'),
23
+ azure: await fse.readFile(path.resolve(__dirname, '../../fragments/ops-review/az.md'), 'utf-8'),
24
+ gitlab: await fse.readFile(path.resolve(__dirname, '../../fragments/ops-review/gl.md'), 'utf-8'),
25
+ }
26
+
27
+ const _backlog = {
28
+ github: await fse.readFile(path.resolve(__dirname, '../../fragments/ops-backlog/gh.md'), 'utf-8'),
29
+ azure: await fse.readFile(path.resolve(__dirname, '../../fragments/ops-backlog/az.md'), 'utf-8'),
30
+ jira: await fse.readFile(path.resolve(__dirname, '../../fragments/ops-backlog/jira.md'), 'utf-8'),
31
+ }
32
+
33
+ // Evidence comments target the backlog platform (where the issue/work-item lives).
34
+ const _evidence = {
35
+ github: await fse.readFile(path.resolve(__dirname, '../../fragments/ops-evidence/gh.md'), 'utf-8'),
36
+ azure: await fse.readFile(path.resolve(__dirname, '../../fragments/ops-evidence/az.md'), 'utf-8'),
37
+ jira: await fse.readFile(path.resolve(__dirname, '../../fragments/ops-evidence/jira.md'), 'utf-8'),
38
+ }
39
+
40
+ // relativePath is POSIX-style relative to the project root; commands live in
41
+ // .opencode/commands/, skill-backed procedures (plan-archive, ops-ship) in
42
+ // .agents/skills/<name>/SKILL.md.
43
+ function patchFile(relativePath, startMarker, endMarker, content, platform, cwd = process.cwd()) {
44
+ const targetPath = path.join(cwd, ...relativePath.split('/'))
45
+ if (!fse.pathExistsSync(targetPath)) return
46
+
47
+ let fileContent = fse.readFileSync(targetPath, 'utf-8')
48
+ if (!fileContent.includes(startMarker) || !fileContent.includes(endMarker)) return
49
+
50
+ const pattern = new RegExp(`${startMarker.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}[\\s\\S]*?${endMarker.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')}`)
51
+ // Replacer function, not a replacement string: the injected fragments contain
52
+ // shell quoting like $'...' and a string replacement expands $' as "everything
53
+ // after the match", silently truncating the command. That is how the ops-ship
54
+ // screenshot comment lost its entire --body argument.
55
+ fileContent = fileContent.replace(pattern, () => `${startMarker}\n${content.trim()}\n${endMarker}`)
56
+ fse.writeFileSync(targetPath, `${fileContent.replace(/\s*$/, '')}\n`, 'utf-8')
57
+ success(`${relativePath} content injected for platform: ${platform}`)
58
+ }
59
+
60
+ export async function patchArchiveCommand(platform, cwd = process.cwd()) {
61
+ const repoPlatform = typeof platform === 'object' ? (platform.repoPlatform ?? platform.backlogPlatform ?? 'github') : platform
62
+ const replacement = _archive[repoPlatform]
63
+ if (!replacement) return
64
+ patchFile('.agents/skills/pc-plan-archive/SKILL.md', '<!-- PC-PLATFORM-ARCHIVE-START -->', '<!-- PC-PLATFORM-ARCHIVE-END -->', replacement, repoPlatform, cwd)
65
+ }
66
+
67
+ export async function patchOpsShip(platform, cwd = process.cwd()) {
68
+ const repoPlatform = typeof platform === 'object' ? (platform.repoPlatform ?? 'github') : platform
69
+ const replacement = _ship[repoPlatform]
70
+ if (!replacement) return
71
+ patchFile('.agents/skills/pc-ops-ship/SKILL.md', '<!-- PC-PLATFORM-SHIP-START -->', '<!-- PC-PLATFORM-SHIP-END -->', replacement, repoPlatform, cwd)
72
+ }
73
+
74
+ export async function patchOpsReview(platform, cwd = process.cwd()) {
75
+ const repoPlatform = typeof platform === 'object' ? (platform.repoPlatform ?? 'github') : platform
76
+ const replacement = _review[repoPlatform]
77
+ if (!replacement) return
78
+ patchFile('.opencode/commands/ops-review.md', '<!-- PC-PLATFORM-REVIEW-START -->', '<!-- PC-PLATFORM-REVIEW-END -->', replacement, repoPlatform, cwd)
79
+ }
80
+
81
+ export async function patchOpsBacklog(platform, cwd = process.cwd()) {
82
+ const backlogPlatform = typeof platform === 'object' ? (platform.backlogPlatform ?? 'github') : platform
83
+ const replacement = _backlog[backlogPlatform]
84
+ if (!replacement) return
85
+ patchFile('.opencode/commands/ops-backlog.md', '<!-- PC-PLATFORM-BACKLOG-START -->', '<!-- PC-PLATFORM-BACKLOG-END -->', replacement, backlogPlatform, cwd)
86
+ }
87
+
88
+ export async function patchOpsEvidence(platform, cwd = process.cwd()) {
89
+ const backlogPlatform = typeof platform === 'object' ? (platform.backlogPlatform ?? 'github') : platform
90
+ const replacement = _evidence[backlogPlatform]
91
+ // browser/none have no CLI to post with: leave the markers empty (the skill skips commenting).
92
+ if (!replacement) return
93
+ patchFile('.agents/skills/pc-ops-evidence/SKILL.md', '<!-- PC-PLATFORM-EVIDENCE-START -->', '<!-- PC-PLATFORM-EVIDENCE-END -->', replacement, backlogPlatform, cwd)
94
+ }
95
+
@@ -1,108 +1,109 @@
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
+ // Replacer function so $&, $' and $` in injected content stay literal.
27
+ return content.replace(pattern, () => `${startMarker}\n${replacement}\n${endMarker}`)
28
+ }
29
+
30
+ async function patchOptionalToolGuidance(destSkillsDir, selections) {
31
+ const files = []
32
+
33
+ async function collect(dir) {
34
+ for (const entry of await fse.readdir(dir, { withFileTypes: true })) {
35
+ const entryPath = path.join(dir, entry.name)
36
+ if (entry.isDirectory()) await collect(entryPath)
37
+ else if (entry.name.endsWith('.md')) files.push(entryPath)
38
+ }
39
+ }
40
+
41
+ if (!await fse.pathExists(destSkillsDir)) return 0
42
+ await collect(destSkillsDir)
43
+
44
+ let patched = 0
45
+ for (const filePath of files) {
46
+ let content = await fse.readFile(filePath, 'utf-8')
47
+ const original = content
48
+ for (const [key, guidance] of Object.entries(OPTIONAL_TOOL_GUIDANCE)) {
49
+ const suffix = key.toUpperCase()
50
+ content = replaceMarkerSection(
51
+ content,
52
+ `<!-- PC-OPTIMIZATION-${suffix}-START -->`,
53
+ `<!-- PC-OPTIMIZATION-${suffix}-END -->`,
54
+ selections[key] ? guidance : '',
55
+ )
56
+ }
57
+ if (content !== original) {
58
+ await fse.writeFile(filePath, `${content.replace(/\s*$/, '')}\n`, 'utf-8')
59
+ patched++
60
+ }
61
+ }
62
+ return patched
63
+ }
64
+
65
+ export async function patchGuardrails(selections = {}, { cwd = process.cwd() } = {}) {
66
+ const destSkillsDir = path.join(cwd, '.agents', 'skills')
67
+ const guardrailsPath = path.join(destSkillsDir, 'pc-guardrails-generic', 'SKILL.md')
68
+ let patchedCount = 0
69
+
70
+ if (await fse.pathExists(guardrailsPath)) {
71
+ let content = await fse.readFile(guardrailsPath, 'utf-8')
72
+ content = replaceMarkerSection(
73
+ content,
74
+ '<!-- PC-GUARDRAILS-CAVEMAN-START -->',
75
+ '<!-- PC-GUARDRAILS-CAVEMAN-END -->',
76
+ '',
77
+ ).replace(
78
+ '<!-- PC-GUARDRAILS-CAVEMAN-START -->\n\n<!-- PC-GUARDRAILS-CAVEMAN-END -->',
79
+ '<!-- PC-GUARDRAILS-SIMPLE-ENGLISH-START -->\n<!-- PC-GUARDRAILS-SIMPLE-ENGLISH-END -->',
80
+ ).replace(
81
+ '1. Load ALL skills listed under your own `## Abilities` now (Guardrails first, then the rest), by calling the `skill` tool once per `@skill-name`.',
82
+ '1. The `pc-system-reminders` plugin has already loaded the skills listed under your `## Abilities`, guardrails first.',
83
+ )
84
+ for (const [key, markerSuffix] of Object.entries(MARKER_SECTIONS)) {
85
+ let sectionContent = ''
86
+ if (selections[key]) {
87
+ const presetName = key === 'simpleEnglish' ? 'simple-english' : key
88
+ const presetPath = path.join(GUARDRAILS_FRAGMENT_DIR, `${presetName}.md`)
89
+ if (await fse.pathExists(presetPath)) {
90
+ sectionContent = (await fse.readFile(presetPath, 'utf-8')).trim()
91
+ }
92
+ }
93
+ content = replaceMarkerSection(
94
+ content,
95
+ `<!-- PC-GUARDRAILS-${markerSuffix}-START -->`,
96
+ `<!-- PC-GUARDRAILS-${markerSuffix}-END -->`,
97
+ sectionContent,
98
+ )
99
+ if (selections[key]) patchedCount++
100
+ }
101
+ await fse.writeFile(guardrailsPath, `${content.replace(/\s*$/, '')}\n`, 'utf-8')
102
+ } else {
103
+ info('pc-guardrails-generic not found, skipping guardrails patching')
104
+ }
105
+
106
+ const optionalPatched = await patchOptionalToolGuidance(destSkillsDir, selections)
107
+ success(`Guardrails patched: ${patchedCount} sections injected; ${optionalPatched} optional tool file(s) updated`)
108
+ return { patched: true, count: patchedCount, optionalPatched }
109
+ }