@plainconceptsplatform/agent-harness 3.9.0 → 3.10.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.
- package/LICENSE +21 -21
- package/README.md +551 -551
- package/cli/commands/join.js +259 -259
- package/cli/commands/migrate.js +310 -310
- package/cli/commands/shared.js +27 -27
- package/cli/commands/single.js +79 -79
- package/cli/commands/update.js +121 -121
- package/cli/commands/wizard.js +134 -134
- package/cli/fragments/archive/az.md +97 -97
- package/cli/fragments/archive/gh.md +96 -96
- package/cli/fragments/archive/gl.md +96 -96
- package/cli/fragments/archive/none.md +75 -75
- package/cli/fragments/guardrails/codegraph.md +5 -5
- package/cli/fragments/guardrails/humanizer.md +4 -4
- package/cli/fragments/guardrails/memory.md +4 -4
- package/cli/fragments/guardrails/rtk.md +3 -3
- package/cli/fragments/guardrails/simple-english.md +4 -4
- package/cli/fragments/ops-backlog/az.md +28 -28
- package/cli/fragments/ops-backlog/gh.md +28 -28
- package/cli/fragments/ops-backlog/jira.md +28 -28
- package/cli/fragments/ops-review/az.md +62 -62
- package/cli/fragments/ops-review/gh.md +51 -51
- package/cli/fragments/ops-review/gl.md +55 -55
- package/cli/fragments/ops-ship/az.md +81 -81
- package/cli/fragments/ops-ship/gh.md +67 -67
- package/cli/fragments/ops-ship/gl.md +84 -84
- package/cli/index.js +116 -116
- package/cli/presets/agents-content.json +34 -34
- package/cli/presets/browser.json +10 -10
- package/cli/presets/clean.json +21 -21
- package/cli/presets/models.json +68 -68
- package/cli/presets/optimization.json +32 -32
- package/cli/presets/platforms.json +76 -76
- package/cli/presets/source.json +23 -23
- package/cli/steps/browser/index.js +52 -48
- package/cli/steps/clean/index.js +120 -120
- package/cli/steps/copy/agents.js +176 -176
- package/cli/steps/copy/commands.js +79 -79
- package/cli/steps/copy/fullstack-engineer.js +81 -81
- package/cli/steps/copy/index.js +88 -88
- package/cli/steps/copy/opencode-json.js +359 -324
- package/cli/steps/copy/skills.js +316 -316
- package/cli/steps/metadata/index.js +115 -115
- package/cli/steps/models/format.js +88 -88
- package/cli/steps/models/index.js +64 -64
- package/cli/steps/models/write.js +38 -38
- package/cli/steps/openspec/index.js +136 -136
- package/cli/steps/optimization/codegraph.js +141 -141
- package/cli/steps/optimization/detect.js +66 -66
- package/cli/steps/optimization/humanizer.js +17 -17
- package/cli/steps/optimization/index.js +158 -158
- package/cli/steps/optimization/memory.js +91 -91
- package/cli/steps/optimization/patch-guardrails.js +111 -111
- package/cli/steps/optimization/simple-english.js +17 -17
- package/cli/steps/optimization/skills-lock.js +30 -30
- package/cli/steps/platform/index.js +109 -109
- package/cli/steps/source/index.js +123 -123
- package/cli/utils/agent-color.js +111 -111
- package/cli/utils/copy.js +134 -134
- package/cli/utils/exec-spinner.js +47 -47
- package/cli/utils/exec.js +134 -134
- package/cli/utils/legacy-check.js +30 -30
- package/cli/utils/models-cache.js +58 -58
- package/cli/utils/models-pricing.js +42 -42
- package/cli/utils/paths.js +82 -82
- package/cli/utils/process.js +3 -3
- package/cli/utils/terminal.js +6 -6
- package/cli/utils/update-manifest.js +75 -75
- package/harness/.opencode/_gitignore +12 -12
- package/harness/.opencode/commands/init.md +5 -5
- package/harness/.opencode/commands/make-architecture.md +5 -5
- package/harness/.opencode/commands/make-design.md +5 -5
- package/harness/.opencode/commands/make-engineer.md +5 -5
- package/harness/.opencode/commands/make-guardrails.md +5 -5
- package/harness/.opencode/commands/make-user-model.md +5 -5
- package/harness/.opencode/commands/ops-backlog.md +10 -10
- package/harness/.opencode/commands/ops-review.md +10 -10
- package/harness/.opencode/commands/ops-ship.md +9 -9
- package/harness/.opencode/commands/plan-apply.md +9 -9
- package/harness/.opencode/commands/plan-archive.md +7 -7
- package/harness/.opencode/commands/plan-explore.md +11 -11
- package/harness/.opencode/commands/plan-goal.md +5 -5
- package/harness/.opencode/commands/plan-propose.md +11 -11
- package/harness/.opencode/commands/plan-quick.md +7 -7
- package/harness/.opencode/commands/plan-story.md +11 -11
- package/harness/.opencode/commands/repo-audit.md +7 -7
- package/harness/.opencode/commands/repo-help.md +5 -5
- package/harness/.opencode/commands/repo-initialize.md +5 -5
- package/harness/.opencode/commands/repo-onboard.md +5 -5
- package/harness/.opencode/commands/repo-verify.md +5 -5
- package/harness/.opencode/plugins/pc-commit-coauthor.js +41 -41
- package/harness/.opencode/plugins/pc-subagent-monitor.js +184 -184
- package/harness/.opencode/plugins/pc-system-reminders.js +424 -424
- package/harness/.opencode/scripts/check-diff.mjs +305 -305
- package/harness/.opencode/scripts/code-rules.mjs +288 -288
- package/harness/.opencode/scripts/platform-contract.mjs +366 -366
- package/harness/AGENTS.md +55 -55
- package/harness/ARCHITECTURE.md +13 -13
- package/harness/DESIGN.md +13 -13
- package/harness/opencode.jsonc +37 -36
- package/harness/openspec/config.yaml +20 -20
- package/harness/skills-lock.json +17 -17
- package/package.json +14 -12
- package/skills/README.md +18 -18
- package/skills/agent-harness-cli/SKILL.md +86 -86
- package/skills/browser-automation/SKILL.md +100 -72
- package/skills/pc-guardrails-generic/SKILL.md +53 -53
- package/skills/pc-guardrails-project/SKILL.md +8 -8
- package/skills/pc-make-architecture/SKILL.md +33 -33
- package/skills/pc-make-architecture/structure-template.md +64 -64
- package/skills/pc-make-design/SKILL.md +45 -45
- package/skills/pc-make-engineer/SKILL.md +59 -59
- package/skills/pc-make-engineer/signal-mapping.md +53 -53
- package/skills/pc-make-engineer/template.md +36 -36
- package/skills/pc-make-guardrails/SKILL.md +44 -44
- package/skills/pc-make-guardrails/category-reference.md +112 -112
- package/skills/pc-make-merge-risk-assess/SKILL.md +70 -70
- package/skills/pc-make-merge-risk-assess/category-reference.md +114 -114
- package/skills/pc-make-user-model/SKILL.md +56 -56
- package/skills/pc-ops-ship/SKILL.md +22 -22
- package/skills/pc-plan-apply/SKILL.md +90 -90
- package/skills/pc-plan-apply/simple-mode.md +21 -21
- package/skills/pc-plan-archive/SKILL.md +66 -66
- package/skills/pc-plan-explore/SKILL.md +21 -21
- package/skills/pc-plan-goal/SKILL.md +91 -91
- package/skills/pc-plan-goal/branching.md +39 -39
- package/skills/pc-plan-goal/failure-policy.md +30 -30
- package/skills/pc-plan-goal/output-mode.md +10 -10
- package/skills/pc-plan-goal/output.md +71 -71
- package/skills/pc-plan-propose/SKILL.md +127 -127
- package/skills/pc-plan-propose/task-annotation.md +39 -39
- package/skills/pc-plan-quick/SKILL.md +46 -46
- package/skills/pc-plan-story/SKILL.md +93 -93
- package/skills/pc-repo-audit/SKILL.md +44 -44
- package/skills/pc-repo-help/SKILL.md +87 -87
- package/skills/pc-repo-initialize/SKILL.md +119 -119
- package/skills/pc-repo-onboard/SKILL.md +32 -32
- package/skills/pc-repo-verify/SKILL.md +150 -150
- package/skills/pc-userstory-az/SKILL.md +71 -71
- package/skills/pc-userstory-browser/SKILL.md +73 -64
- package/skills/pc-userstory-gh/SKILL.md +63 -63
- package/skills/pc-userstory-jira/SKILL.md +74 -74
package/cli/steps/clean/index.js
CHANGED
|
@@ -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
|
+
}
|
package/cli/steps/copy/agents.js
CHANGED
|
@@ -1,176 +1,176 @@
|
|
|
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
|
-
import { ContractError, isTemplateSource, loadContract } from '../../../harness/.opencode/scripts/platform-contract.mjs'
|
|
6
|
-
|
|
7
|
-
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
8
|
-
const agentsContent = await fse.readJson(path.resolve(__dirname, '../../presets/agents-content.json'))
|
|
9
|
-
|
|
10
|
-
// Steps are matched by their title text, not by exact heading string: heading
|
|
11
|
-
// level (##/###/####), the `Step N,` prefix and any ` (if Yes)` suffix have all
|
|
12
|
-
// drifted before, and every drift silently turned the patch into a no-op that
|
|
13
|
-
// only showed up as a warning nobody read.
|
|
14
|
-
const HISTORY_STEP_TITLE = 'Archive project history'
|
|
15
|
-
const ARCHITECTURE_STEP_TITLE = 'Generate ARCHITECTURE.md'
|
|
16
|
-
const DESIGN_STEP_TITLE = 'Generate DESIGN.md'
|
|
17
|
-
|
|
18
|
-
function stepHeadingPattern(title) {
|
|
19
|
-
const escaped = title.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
20
|
-
return new RegExp(`^#{2,4}\\s+(?:Step\\s+\\d+[a-z]?[,:]\\s+)?${escaped}\\b`, 'i')
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
// The step heading is kept and its body replaced with an explicit skip note.
|
|
24
|
-
// Removing the block and renumbering the remaining steps (the old approach)
|
|
25
|
-
// went stale against prose cross-references like "Skip steps 2, 3, and 4";
|
|
26
|
-
// stable numbering plus a visible reason is unambiguous for the agent.
|
|
27
|
-
export function skipStepBlock(content, title, note) {
|
|
28
|
-
const lines = content.split('\n')
|
|
29
|
-
const pattern = stepHeadingPattern(title)
|
|
30
|
-
const start = lines.findIndex(l => pattern.test(l.trim()))
|
|
31
|
-
if (start === -1) return { content, matched: false }
|
|
32
|
-
|
|
33
|
-
// The block ends at the next heading or rule, NOT at end of file. Scanning
|
|
34
|
-
// only for `---` meant a document without one lost everything below the
|
|
35
|
-
// matched step.
|
|
36
|
-
let end = lines.length
|
|
37
|
-
for (let i = start + 1; i < lines.length; i++) {
|
|
38
|
-
const line = lines[i].trim()
|
|
39
|
-
if (line === '---' || /^#{1,4}\s/.test(line)) { end = i; break }
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
lines.splice(start + 1, end - start - 1, '', `> ${note}`, '')
|
|
43
|
-
return { content: lines.join('\n'), matched: true }
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
const PLATFORM_WORKFLOW_START = '<!-- PC-PLATFORM-WORKFLOW-START -->'
|
|
47
|
-
const PLATFORM_WORKFLOW_END = '<!-- PC-PLATFORM-WORKFLOW-END -->'
|
|
48
|
-
const PLATFORM_SKILLS_GUIDE_START = '<!-- PC-PLATFORM-SKILLS-GUIDE-START -->'
|
|
49
|
-
const PLATFORM_SKILLS_GUIDE_END = '<!-- PC-PLATFORM-SKILLS-GUIDE-END -->'
|
|
50
|
-
const PLATFORM_SKILLS_START = '<!-- PC-PLATFORM-SKILLS-START -->'
|
|
51
|
-
const PLATFORM_SKILLS_END = '<!-- PC-PLATFORM-SKILLS-END -->'
|
|
52
|
-
const PLATFORM_CONTRACT_START = '<!-- PC-PLATFORM-CONTRACT-START -->'
|
|
53
|
-
const PLATFORM_CONTRACT_END = '<!-- PC-PLATFORM-CONTRACT-END -->'
|
|
54
|
-
|
|
55
|
-
const MANDATORY_SKILLS_BODY = 'Mandatory, before any edit, shell command or subagent spawn: read `.agents/skills/pc-guardrails-generic/SKILL.md`, then `.agents/skills/pc-guardrails-project/SKILL.md` if present. They are instructions, not references: a loaded skill may name further skills, and those load too before the work they cover.'
|
|
56
|
-
|
|
57
|
-
function platformContent(platform, key) {
|
|
58
|
-
const p = agentsContent.platform[platform] ?? agentsContent.platform.github
|
|
59
|
-
return p[key] ?? ''
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function mixedPlatformContent(backlogPlatform, repoPlatform, key) {
|
|
63
|
-
if (backlogPlatform === repoPlatform) {
|
|
64
|
-
return platformContent(backlogPlatform, key)
|
|
65
|
-
}
|
|
66
|
-
const backlog = agentsContent.platform[backlogPlatform] ?? agentsContent.platform.github
|
|
67
|
-
const repo = agentsContent.platform[repoPlatform] ?? agentsContent.platform.github
|
|
68
|
-
const mixed = agentsContent.platform.mixed ?? {}
|
|
69
|
-
const mixedKey = `${backlogPlatform}-${repoPlatform}`
|
|
70
|
-
return mixed[mixedKey]?.[key] ?? mixed.default?.[key]?.replace('{backlog}', backlog[key] ?? '').replace('{repo}', repo[key] ?? '') ?? platformContent(repoPlatform, key)
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
function replaceBetween(content, start, end, replacement) {
|
|
74
|
-
if (!content.includes(start) || !content.includes(end)) return content
|
|
75
|
-
const pattern = new RegExp(`${start}[\\s\\S]*?${end}`)
|
|
76
|
-
// Replacer function so $&, $' and $` in injected content stay literal.
|
|
77
|
-
const body = replacement.trim()
|
|
78
|
-
return content.replace(pattern, () => (body ? `${start}\n${body}\n${end}` : `${start}\n${end}`))
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
// Reading the file works in every agent, not only the ones that discover
|
|
82
|
-
// `.agents/skills` natively (Claude Code reads `.claude/skills`). Installs from
|
|
83
|
-
// before this block have no marker pair and update never rewrites AGENTS.md, so
|
|
84
|
-
// append a section then; the marker pair keeps later updates idempotent.
|
|
85
|
-
function patchMandatorySkillsBlock(content) {
|
|
86
|
-
if (content.includes(PLATFORM_SKILLS_START)) {
|
|
87
|
-
return replaceBetween(content, PLATFORM_SKILLS_START, PLATFORM_SKILLS_END, MANDATORY_SKILLS_BODY)
|
|
88
|
-
}
|
|
89
|
-
return `${content.replace(/\s*$/, '')}\n\n## Mandatory skills\n\n${PLATFORM_SKILLS_START}\n${MANDATORY_SKILLS_BODY}\n${PLATFORM_SKILLS_END}\n`
|
|
90
|
-
}
|
|
91
|
-
|
|
92
|
-
export function reuseBlock(contract, templateSource = false) {
|
|
93
|
-
const { docs = [], packages = [], names = [] } = contract?.reuse ?? {}
|
|
94
|
-
if (!docs.length && !packages.length && !names.length) return ''
|
|
95
|
-
const code = list => list.map(item => `\`${item}\``).join(', ')
|
|
96
|
-
return [
|
|
97
|
-
'## Reuse before you build',
|
|
98
|
-
'',
|
|
99
|
-
`This repository receives shared code from ${contract.source.name} (${contract.source.url}), declared in \`.platform/contract.yml\`.`,
|
|
100
|
-
'',
|
|
101
|
-
...(docs.length ? [`- Never design something new before reading ${code(docs)}.`] : []),
|
|
102
|
-
...(packages.length ? [`- Never re-implement an export of ${code(packages)}; import it.`] : []),
|
|
103
|
-
...(names.length ? [`- Never write your own ${code(names)}; reuse the existing one.`] : []),
|
|
104
|
-
...(templateSource ? [] : [`- Never edit a path listed under \`owned\`. Ask for the change at ${contract.source.changes}, or add a dated exception to \`${contract.exceptions}\`.`]),
|
|
105
|
-
].join('\n')
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
function contractBlock(cwd) {
|
|
109
|
-
try {
|
|
110
|
-
const contract = loadContract(cwd)
|
|
111
|
-
return reuseBlock(contract, isTemplateSource(cwd, contract))
|
|
112
|
-
} catch (err) {
|
|
113
|
-
if (!(err instanceof ContractError)) throw err
|
|
114
|
-
warn(`${err.message} The "Reuse before you build" block in AGENTS.md was left as it was.`)
|
|
115
|
-
return null
|
|
116
|
-
}
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
// Installs from before the contract have no marker pair, and update never rewrites AGENTS.md: append it then.
|
|
120
|
-
function patchContractBlock(content, cwd) {
|
|
121
|
-
const block = contractBlock(cwd)
|
|
122
|
-
if (block == null) return content
|
|
123
|
-
if (content.includes(PLATFORM_CONTRACT_START)) return replaceBetween(content, PLATFORM_CONTRACT_START, PLATFORM_CONTRACT_END, block)
|
|
124
|
-
return block ? `${content.replace(/\s*$/, '')}\n\n${PLATFORM_CONTRACT_START}\n${block}\n${PLATFORM_CONTRACT_END}\n` : content
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
export async function patchAgentsMd(ctx) {
|
|
128
|
-
const obInitSkillPath = path.join(process.cwd(), '.agents', 'skills', 'pc-repo-initialize', 'SKILL.md')
|
|
129
|
-
if (!await fse.pathExists(obInitSkillPath)) return
|
|
130
|
-
|
|
131
|
-
let content = await fse.readFile(obInitSkillPath, 'utf-8')
|
|
132
|
-
const patches = []
|
|
133
|
-
|
|
134
|
-
const skips = [
|
|
135
|
-
[ctx.hasOpenspec, HISTORY_STEP_TITLE,
|
|
136
|
-
'Skipped during onboarding: this project already had an openspec/ history. Do not archive again; continue with the next step.'],
|
|
137
|
-
[ctx.hasArchitecture, ARCHITECTURE_STEP_TITLE,
|
|
138
|
-
'Skipped during onboarding: ARCHITECTURE.md already exists. Run /make-architecture to regenerate it, which keeps its update mode.'],
|
|
139
|
-
[ctx.hasDesign, DESIGN_STEP_TITLE,
|
|
140
|
-
'Skipped during onboarding: DESIGN.md already exists. Run /make-design to regenerate it, which keeps its update mode.'],
|
|
141
|
-
]
|
|
142
|
-
|
|
143
|
-
for (const [enabled, title, note] of skips) {
|
|
144
|
-
if (!enabled) continue
|
|
145
|
-
const result = skipStepBlock(content, title, note)
|
|
146
|
-
if (!result.matched) {
|
|
147
|
-
warn(`pc-repo-initialize SKILL.md step "${title}" not found: template drift? Skipping this patch.`)
|
|
148
|
-
continue
|
|
149
|
-
}
|
|
150
|
-
content = result.content
|
|
151
|
-
patches.push(`Step "${title}" marked as skipped, file already exists`)
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
if (patches.length > 0) {
|
|
155
|
-
await fse.writeFile(obInitSkillPath, content, 'utf-8')
|
|
156
|
-
for (const msg of patches) info(msg)
|
|
157
|
-
success('pc-repo-initialize SKILL.md patched for existing project state')
|
|
158
|
-
}
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
export async function patchAgentGuidance(backlogPlatform, repoPlatform, cwd = process.cwd()) {
|
|
162
|
-
const agentsMdPath = path.join(cwd, 'AGENTS.md')
|
|
163
|
-
if (await fse.pathExists(agentsMdPath)) {
|
|
164
|
-
const repo = repoPlatform ?? backlogPlatform ?? 'github'
|
|
165
|
-
let content = await fse.readFile(agentsMdPath, 'utf-8')
|
|
166
|
-
content = replaceBetween(content, PLATFORM_WORKFLOW_START, PLATFORM_WORKFLOW_END, mixedPlatformContent(backlogPlatform, repo, 'workflow'))
|
|
167
|
-
content = replaceBetween(content, PLATFORM_SKILLS_GUIDE_START, PLATFORM_SKILLS_GUIDE_END, mixedPlatformContent(backlogPlatform, repo, 'skillsGuide'))
|
|
168
|
-
content = patchMandatorySkillsBlock(content)
|
|
169
|
-
content = patchContractBlock(content, cwd)
|
|
170
|
-
await fse.writeFile(agentsMdPath, `${content.replace(/\s*$/, '')}\n`, 'utf-8')
|
|
171
|
-
const label = backlogPlatform === repo ? repo : `${backlogPlatform}→${repo}`
|
|
172
|
-
success(`AGENTS.md patched for platform workflow: ${label}`)
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
|
|
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
|
+
import { ContractError, isTemplateSource, loadContract } from '../../../harness/.opencode/scripts/platform-contract.mjs'
|
|
6
|
+
|
|
7
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
8
|
+
const agentsContent = await fse.readJson(path.resolve(__dirname, '../../presets/agents-content.json'))
|
|
9
|
+
|
|
10
|
+
// Steps are matched by their title text, not by exact heading string: heading
|
|
11
|
+
// level (##/###/####), the `Step N,` prefix and any ` (if Yes)` suffix have all
|
|
12
|
+
// drifted before, and every drift silently turned the patch into a no-op that
|
|
13
|
+
// only showed up as a warning nobody read.
|
|
14
|
+
const HISTORY_STEP_TITLE = 'Archive project history'
|
|
15
|
+
const ARCHITECTURE_STEP_TITLE = 'Generate ARCHITECTURE.md'
|
|
16
|
+
const DESIGN_STEP_TITLE = 'Generate DESIGN.md'
|
|
17
|
+
|
|
18
|
+
function stepHeadingPattern(title) {
|
|
19
|
+
const escaped = title.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
|
20
|
+
return new RegExp(`^#{2,4}\\s+(?:Step\\s+\\d+[a-z]?[,:]\\s+)?${escaped}\\b`, 'i')
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
// The step heading is kept and its body replaced with an explicit skip note.
|
|
24
|
+
// Removing the block and renumbering the remaining steps (the old approach)
|
|
25
|
+
// went stale against prose cross-references like "Skip steps 2, 3, and 4";
|
|
26
|
+
// stable numbering plus a visible reason is unambiguous for the agent.
|
|
27
|
+
export function skipStepBlock(content, title, note) {
|
|
28
|
+
const lines = content.split('\n')
|
|
29
|
+
const pattern = stepHeadingPattern(title)
|
|
30
|
+
const start = lines.findIndex(l => pattern.test(l.trim()))
|
|
31
|
+
if (start === -1) return { content, matched: false }
|
|
32
|
+
|
|
33
|
+
// The block ends at the next heading or rule, NOT at end of file. Scanning
|
|
34
|
+
// only for `---` meant a document without one lost everything below the
|
|
35
|
+
// matched step.
|
|
36
|
+
let end = lines.length
|
|
37
|
+
for (let i = start + 1; i < lines.length; i++) {
|
|
38
|
+
const line = lines[i].trim()
|
|
39
|
+
if (line === '---' || /^#{1,4}\s/.test(line)) { end = i; break }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
lines.splice(start + 1, end - start - 1, '', `> ${note}`, '')
|
|
43
|
+
return { content: lines.join('\n'), matched: true }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const PLATFORM_WORKFLOW_START = '<!-- PC-PLATFORM-WORKFLOW-START -->'
|
|
47
|
+
const PLATFORM_WORKFLOW_END = '<!-- PC-PLATFORM-WORKFLOW-END -->'
|
|
48
|
+
const PLATFORM_SKILLS_GUIDE_START = '<!-- PC-PLATFORM-SKILLS-GUIDE-START -->'
|
|
49
|
+
const PLATFORM_SKILLS_GUIDE_END = '<!-- PC-PLATFORM-SKILLS-GUIDE-END -->'
|
|
50
|
+
const PLATFORM_SKILLS_START = '<!-- PC-PLATFORM-SKILLS-START -->'
|
|
51
|
+
const PLATFORM_SKILLS_END = '<!-- PC-PLATFORM-SKILLS-END -->'
|
|
52
|
+
const PLATFORM_CONTRACT_START = '<!-- PC-PLATFORM-CONTRACT-START -->'
|
|
53
|
+
const PLATFORM_CONTRACT_END = '<!-- PC-PLATFORM-CONTRACT-END -->'
|
|
54
|
+
|
|
55
|
+
const MANDATORY_SKILLS_BODY = 'Mandatory, before any edit, shell command or subagent spawn: read `.agents/skills/pc-guardrails-generic/SKILL.md`, then `.agents/skills/pc-guardrails-project/SKILL.md` if present. They are instructions, not references: a loaded skill may name further skills, and those load too before the work they cover.'
|
|
56
|
+
|
|
57
|
+
function platformContent(platform, key) {
|
|
58
|
+
const p = agentsContent.platform[platform] ?? agentsContent.platform.github
|
|
59
|
+
return p[key] ?? ''
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function mixedPlatformContent(backlogPlatform, repoPlatform, key) {
|
|
63
|
+
if (backlogPlatform === repoPlatform) {
|
|
64
|
+
return platformContent(backlogPlatform, key)
|
|
65
|
+
}
|
|
66
|
+
const backlog = agentsContent.platform[backlogPlatform] ?? agentsContent.platform.github
|
|
67
|
+
const repo = agentsContent.platform[repoPlatform] ?? agentsContent.platform.github
|
|
68
|
+
const mixed = agentsContent.platform.mixed ?? {}
|
|
69
|
+
const mixedKey = `${backlogPlatform}-${repoPlatform}`
|
|
70
|
+
return mixed[mixedKey]?.[key] ?? mixed.default?.[key]?.replace('{backlog}', backlog[key] ?? '').replace('{repo}', repo[key] ?? '') ?? platformContent(repoPlatform, key)
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function replaceBetween(content, start, end, replacement) {
|
|
74
|
+
if (!content.includes(start) || !content.includes(end)) return content
|
|
75
|
+
const pattern = new RegExp(`${start}[\\s\\S]*?${end}`)
|
|
76
|
+
// Replacer function so $&, $' and $` in injected content stay literal.
|
|
77
|
+
const body = replacement.trim()
|
|
78
|
+
return content.replace(pattern, () => (body ? `${start}\n${body}\n${end}` : `${start}\n${end}`))
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Reading the file works in every agent, not only the ones that discover
|
|
82
|
+
// `.agents/skills` natively (Claude Code reads `.claude/skills`). Installs from
|
|
83
|
+
// before this block have no marker pair and update never rewrites AGENTS.md, so
|
|
84
|
+
// append a section then; the marker pair keeps later updates idempotent.
|
|
85
|
+
function patchMandatorySkillsBlock(content) {
|
|
86
|
+
if (content.includes(PLATFORM_SKILLS_START)) {
|
|
87
|
+
return replaceBetween(content, PLATFORM_SKILLS_START, PLATFORM_SKILLS_END, MANDATORY_SKILLS_BODY)
|
|
88
|
+
}
|
|
89
|
+
return `${content.replace(/\s*$/, '')}\n\n## Mandatory skills\n\n${PLATFORM_SKILLS_START}\n${MANDATORY_SKILLS_BODY}\n${PLATFORM_SKILLS_END}\n`
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function reuseBlock(contract, templateSource = false) {
|
|
93
|
+
const { docs = [], packages = [], names = [] } = contract?.reuse ?? {}
|
|
94
|
+
if (!docs.length && !packages.length && !names.length) return ''
|
|
95
|
+
const code = list => list.map(item => `\`${item}\``).join(', ')
|
|
96
|
+
return [
|
|
97
|
+
'## Reuse before you build',
|
|
98
|
+
'',
|
|
99
|
+
`This repository receives shared code from ${contract.source.name} (${contract.source.url}), declared in \`.platform/contract.yml\`.`,
|
|
100
|
+
'',
|
|
101
|
+
...(docs.length ? [`- Never design something new before reading ${code(docs)}.`] : []),
|
|
102
|
+
...(packages.length ? [`- Never re-implement an export of ${code(packages)}; import it.`] : []),
|
|
103
|
+
...(names.length ? [`- Never write your own ${code(names)}; reuse the existing one.`] : []),
|
|
104
|
+
...(templateSource ? [] : [`- Never edit a path listed under \`owned\`. Ask for the change at ${contract.source.changes}, or add a dated exception to \`${contract.exceptions}\`.`]),
|
|
105
|
+
].join('\n')
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function contractBlock(cwd) {
|
|
109
|
+
try {
|
|
110
|
+
const contract = loadContract(cwd)
|
|
111
|
+
return reuseBlock(contract, isTemplateSource(cwd, contract))
|
|
112
|
+
} catch (err) {
|
|
113
|
+
if (!(err instanceof ContractError)) throw err
|
|
114
|
+
warn(`${err.message} The "Reuse before you build" block in AGENTS.md was left as it was.`)
|
|
115
|
+
return null
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// Installs from before the contract have no marker pair, and update never rewrites AGENTS.md: append it then.
|
|
120
|
+
function patchContractBlock(content, cwd) {
|
|
121
|
+
const block = contractBlock(cwd)
|
|
122
|
+
if (block == null) return content
|
|
123
|
+
if (content.includes(PLATFORM_CONTRACT_START)) return replaceBetween(content, PLATFORM_CONTRACT_START, PLATFORM_CONTRACT_END, block)
|
|
124
|
+
return block ? `${content.replace(/\s*$/, '')}\n\n${PLATFORM_CONTRACT_START}\n${block}\n${PLATFORM_CONTRACT_END}\n` : content
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export async function patchAgentsMd(ctx) {
|
|
128
|
+
const obInitSkillPath = path.join(process.cwd(), '.agents', 'skills', 'pc-repo-initialize', 'SKILL.md')
|
|
129
|
+
if (!await fse.pathExists(obInitSkillPath)) return
|
|
130
|
+
|
|
131
|
+
let content = await fse.readFile(obInitSkillPath, 'utf-8')
|
|
132
|
+
const patches = []
|
|
133
|
+
|
|
134
|
+
const skips = [
|
|
135
|
+
[ctx.hasOpenspec, HISTORY_STEP_TITLE,
|
|
136
|
+
'Skipped during onboarding: this project already had an openspec/ history. Do not archive again; continue with the next step.'],
|
|
137
|
+
[ctx.hasArchitecture, ARCHITECTURE_STEP_TITLE,
|
|
138
|
+
'Skipped during onboarding: ARCHITECTURE.md already exists. Run /make-architecture to regenerate it, which keeps its update mode.'],
|
|
139
|
+
[ctx.hasDesign, DESIGN_STEP_TITLE,
|
|
140
|
+
'Skipped during onboarding: DESIGN.md already exists. Run /make-design to regenerate it, which keeps its update mode.'],
|
|
141
|
+
]
|
|
142
|
+
|
|
143
|
+
for (const [enabled, title, note] of skips) {
|
|
144
|
+
if (!enabled) continue
|
|
145
|
+
const result = skipStepBlock(content, title, note)
|
|
146
|
+
if (!result.matched) {
|
|
147
|
+
warn(`pc-repo-initialize SKILL.md step "${title}" not found: template drift? Skipping this patch.`)
|
|
148
|
+
continue
|
|
149
|
+
}
|
|
150
|
+
content = result.content
|
|
151
|
+
patches.push(`Step "${title}" marked as skipped, file already exists`)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
if (patches.length > 0) {
|
|
155
|
+
await fse.writeFile(obInitSkillPath, content, 'utf-8')
|
|
156
|
+
for (const msg of patches) info(msg)
|
|
157
|
+
success('pc-repo-initialize SKILL.md patched for existing project state')
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
export async function patchAgentGuidance(backlogPlatform, repoPlatform, cwd = process.cwd()) {
|
|
162
|
+
const agentsMdPath = path.join(cwd, 'AGENTS.md')
|
|
163
|
+
if (await fse.pathExists(agentsMdPath)) {
|
|
164
|
+
const repo = repoPlatform ?? backlogPlatform ?? 'github'
|
|
165
|
+
let content = await fse.readFile(agentsMdPath, 'utf-8')
|
|
166
|
+
content = replaceBetween(content, PLATFORM_WORKFLOW_START, PLATFORM_WORKFLOW_END, mixedPlatformContent(backlogPlatform, repo, 'workflow'))
|
|
167
|
+
content = replaceBetween(content, PLATFORM_SKILLS_GUIDE_START, PLATFORM_SKILLS_GUIDE_END, mixedPlatformContent(backlogPlatform, repo, 'skillsGuide'))
|
|
168
|
+
content = patchMandatorySkillsBlock(content)
|
|
169
|
+
content = patchContractBlock(content, cwd)
|
|
170
|
+
await fse.writeFile(agentsMdPath, `${content.replace(/\s*$/, '')}\n`, 'utf-8')
|
|
171
|
+
const label = backlogPlatform === repo ? repo : `${backlogPlatform}→${repo}`
|
|
172
|
+
success(`AGENTS.md patched for platform workflow: ${label}`)
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
|