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