@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,179 +1,179 @@
|
|
|
1
|
-
// pc-subagent-tiers: on startup, reads *-engineer.md templates and creates
|
|
2
|
-
// tier variant files with the resolved model. Variants are gitignored and
|
|
3
|
-
// regenerated every startup. Model resolution: user override > team config.
|
|
4
|
-
|
|
5
|
-
import fs from "node:fs/promises"
|
|
6
|
-
import path from "node:path"
|
|
7
|
-
|
|
8
|
-
const TIERS = ["build", "fast", "plan"]
|
|
9
|
-
|
|
10
|
-
export const PcSubagentTiers = async ({ directory }) => {
|
|
11
|
-
const root = directory || process.cwd()
|
|
12
|
-
const agentsDir = path.join(root, ".opencode", "agents")
|
|
13
|
-
|
|
14
|
-
async function readJson(filePath) {
|
|
15
|
-
try {
|
|
16
|
-
return JSON.parse(await fs.readFile(filePath, "utf-8"))
|
|
17
|
-
} catch {
|
|
18
|
-
return null
|
|
19
|
-
}
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
async function resolveModels() {
|
|
23
|
-
const [user, team] = await Promise.all([
|
|
24
|
-
readJson(path.join(root, ".opencode", "harness.user.json")),
|
|
25
|
-
readJson(path.join(root, ".opencode", "harness.json")),
|
|
26
|
-
])
|
|
27
|
-
const models = {}
|
|
28
|
-
for (const tier of TIERS) {
|
|
29
|
-
models[tier] = user?.models?.[tier] ?? team?.models?.[tier] ?? null
|
|
30
|
-
}
|
|
31
|
-
return models
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
async function scanEngineers() {
|
|
35
|
-
try {
|
|
36
|
-
const entries = await fs.readdir(agentsDir)
|
|
37
|
-
return {
|
|
38
|
-
templates: entries.filter(f => /^[\w-]+-engineer\.md$/.test(f) && f !== 'fullstack-engineer.md').map(f => f.replace(/\.md$/, "")),
|
|
39
|
-
variantFiles: entries.filter(f => /^[\w-]+-engineer\.(build|fast|plan)\.md$/.test(f)),
|
|
40
|
-
}
|
|
41
|
-
} catch {
|
|
42
|
-
return { templates: [], variantFiles: [] }
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
function variantFile(name, tier) {
|
|
47
|
-
return `${name}.${tier}.md`
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function buildVariant(templateContent, model) {
|
|
51
|
-
const fmMatch = templateContent.match(/^---\r?\n([\s\S]*?)\r?\n---/)
|
|
52
|
-
const modelLine = `model: ${model}`
|
|
53
|
-
if (!fmMatch) return `---\nmode: subagent\n${modelLine}\n---\n\n${templateContent}`
|
|
54
|
-
|
|
55
|
-
let fm = fmMatch[1]
|
|
56
|
-
fm = /^mode:/m.test(fm) ? fm.replace(/^mode:.*$/m, 'mode: subagent') : `mode: subagent\n${fm}`
|
|
57
|
-
fm = /^model:/m.test(fm) ? fm.replace(/^model:.*$/m, modelLine) : `${modelLine}\n${fm}`
|
|
58
|
-
|
|
59
|
-
// Slice at frontmatter end: never String.replace with content-derived
|
|
60
|
-
// strings, it matches the wrong occurrence and expands $& sequences.
|
|
61
|
-
return `---\n${fm}\n---${templateContent.slice(fmMatch[0].length)}`
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// Ensure template files have mode: primary and no stale model: line. Base
|
|
65
|
-
// engineer templates must never declare a model: models are resolved at
|
|
66
|
-
// startup per-tier and injected into variant files only. A stale model:
|
|
67
|
-
// (e.g. from a prior `stampAgentModels` run) is stripped here so the base
|
|
68
|
-
// agent falls back to the session-level model in opencode.jsonc.
|
|
69
|
-
function normalizeTemplate(templateContent) {
|
|
70
|
-
const fmMatch = templateContent.match(/^---\r?\n([\s\S]*?)\r?\n---/)
|
|
71
|
-
if (!fmMatch) return templateContent
|
|
72
|
-
|
|
73
|
-
let fm = fmMatch[1]
|
|
74
|
-
let changed = false
|
|
75
|
-
|
|
76
|
-
if (!/^mode:\s*primary/m.test(fm)) {
|
|
77
|
-
fm = /^mode:/m.test(fm) ? fm.replace(/^mode:.*$/m, 'mode: primary') : `mode: primary\n${fm}`
|
|
78
|
-
changed = true
|
|
79
|
-
}
|
|
80
|
-
if (/^model:/m.test(fm)) {
|
|
81
|
-
// Remove the model: line and its trailing newline without leaving a
|
|
82
|
-
// leading blank line (model: can be the first frontmatter entry).
|
|
83
|
-
fm = fm.replace(/^model:[^\n]*\r?\n?/m, '').replace(/^\r?\n/, '').replace(/\n$/, '')
|
|
84
|
-
changed = true
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
return changed ? `---\n${fm}\n---${templateContent.slice(fmMatch[0].length)}` : templateContent
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
function descriptionOf(templateContent) {
|
|
91
|
-
return templateContent.match(/^description:\s*(.+)$/m)?.[1]?.trim() ?? null
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
// Skip writing if the file already has identical content.
|
|
95
|
-
async function writeIfChanged(filePath, content) {
|
|
96
|
-
try {
|
|
97
|
-
if ((await fs.readFile(filePath, "utf-8")) === content) return
|
|
98
|
-
} catch {
|
|
99
|
-
// file missing, write it
|
|
100
|
-
}
|
|
101
|
-
const tmpPath = `${filePath}.tmp`
|
|
102
|
-
await fs.writeFile(tmpPath, content, "utf-8")
|
|
103
|
-
await fs.rename(tmpPath, filePath)
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
return {
|
|
107
|
-
config: async (cfg) => {
|
|
108
|
-
try {
|
|
109
|
-
const models = await resolveModels()
|
|
110
|
-
const available = TIERS.filter(t => models[t])
|
|
111
|
-
const { templates, variantFiles } = await scanEngineers()
|
|
112
|
-
|
|
113
|
-
const templateContents = await Promise.all(
|
|
114
|
-
templates.map(async name => {
|
|
115
|
-
const rawContent = await fs.readFile(path.join(agentsDir, `${name}.md`), "utf-8")
|
|
116
|
-
const content = normalizeTemplate(rawContent)
|
|
117
|
-
// If the template had the wrong mode or a stale model:, persist the fix to disk
|
|
118
|
-
if (content !== rawContent) {
|
|
119
|
-
await writeIfChanged(path.join(agentsDir, `${name}.md`), content)
|
|
120
|
-
console.error(`[pc-subagent-tiers] Normalized ${name}.md (mode: primary, model: removed)`)
|
|
121
|
-
}
|
|
122
|
-
return { name, content }
|
|
123
|
-
})
|
|
124
|
-
)
|
|
125
|
-
|
|
126
|
-
const keepSet = new Set()
|
|
127
|
-
const variantsToWrite = []
|
|
128
|
-
|
|
129
|
-
for (const { name, content } of templateContents) {
|
|
130
|
-
for (const tier of available) {
|
|
131
|
-
const file = variantFile(name, tier)
|
|
132
|
-
variantsToWrite.push({
|
|
133
|
-
file,
|
|
134
|
-
path: path.join(agentsDir, file),
|
|
135
|
-
content: buildVariant(content, models[tier]),
|
|
136
|
-
name,
|
|
137
|
-
tier,
|
|
138
|
-
templateContent: content,
|
|
139
|
-
})
|
|
140
|
-
keepSet.add(file)
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
|
|
144
|
-
await Promise.all(variantsToWrite.map(v => writeIfChanged(v.path, v.content)))
|
|
145
|
-
|
|
146
|
-
if (cfg?.agent) {
|
|
147
|
-
// Ensure base templates are always mode: primary in-memory
|
|
148
|
-
for (const { name } of templateContents) {
|
|
149
|
-
if (cfg.agent[name]) {
|
|
150
|
-
cfg.agent[name].mode = 'primary'
|
|
151
|
-
}
|
|
152
|
-
}
|
|
153
|
-
for (const { name, tier, templateContent } of variantsToWrite) {
|
|
154
|
-
const base = cfg.agent[name]
|
|
155
|
-
cfg.agent[`${name}.${tier}`] = base
|
|
156
|
-
? { ...base, mode: 'subagent', model: models[tier] }
|
|
157
|
-
: {
|
|
158
|
-
mode: "subagent",
|
|
159
|
-
description: descriptionOf(templateContent) ?? `${name} (${tier} tier)`,
|
|
160
|
-
model: models[tier],
|
|
161
|
-
}
|
|
162
|
-
}
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
await Promise.all(
|
|
166
|
-
variantFiles.filter(f => !keepSet.has(f)).map(f => fs.unlink(path.join(agentsDir, f)).catch(() => {}))
|
|
167
|
-
)
|
|
168
|
-
|
|
169
|
-
if (variantsToWrite.length > 0) {
|
|
170
|
-
console.error(`[pc-subagent-tiers] Created ${variantsToWrite.length} variant files (${templates.length} engineers x ${available.length} tiers)`)
|
|
171
|
-
} else if (templates.length > 0) {
|
|
172
|
-
console.error(`[pc-subagent-tiers] No variants created. Models: ${JSON.stringify(models)}`)
|
|
173
|
-
}
|
|
174
|
-
} catch (err) {
|
|
175
|
-
console.error(`[pc-subagent-tiers] Error: ${err.message}`)
|
|
176
|
-
}
|
|
177
|
-
},
|
|
178
|
-
}
|
|
179
|
-
}
|
|
1
|
+
// pc-subagent-tiers: on startup, reads *-engineer.md templates and creates
|
|
2
|
+
// tier variant files with the resolved model. Variants are gitignored and
|
|
3
|
+
// regenerated every startup. Model resolution: user override > team config.
|
|
4
|
+
|
|
5
|
+
import fs from "node:fs/promises"
|
|
6
|
+
import path from "node:path"
|
|
7
|
+
|
|
8
|
+
const TIERS = ["build", "fast", "plan"]
|
|
9
|
+
|
|
10
|
+
export const PcSubagentTiers = async ({ directory }) => {
|
|
11
|
+
const root = directory || process.cwd()
|
|
12
|
+
const agentsDir = path.join(root, ".opencode", "agents")
|
|
13
|
+
|
|
14
|
+
async function readJson(filePath) {
|
|
15
|
+
try {
|
|
16
|
+
return JSON.parse(await fs.readFile(filePath, "utf-8"))
|
|
17
|
+
} catch {
|
|
18
|
+
return null
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
async function resolveModels() {
|
|
23
|
+
const [user, team] = await Promise.all([
|
|
24
|
+
readJson(path.join(root, ".opencode", "harness.user.json")),
|
|
25
|
+
readJson(path.join(root, ".opencode", "harness.json")),
|
|
26
|
+
])
|
|
27
|
+
const models = {}
|
|
28
|
+
for (const tier of TIERS) {
|
|
29
|
+
models[tier] = user?.models?.[tier] ?? team?.models?.[tier] ?? null
|
|
30
|
+
}
|
|
31
|
+
return models
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
async function scanEngineers() {
|
|
35
|
+
try {
|
|
36
|
+
const entries = await fs.readdir(agentsDir)
|
|
37
|
+
return {
|
|
38
|
+
templates: entries.filter(f => /^[\w-]+-engineer\.md$/.test(f) && f !== 'fullstack-engineer.md').map(f => f.replace(/\.md$/, "")),
|
|
39
|
+
variantFiles: entries.filter(f => /^[\w-]+-engineer\.(build|fast|plan)\.md$/.test(f)),
|
|
40
|
+
}
|
|
41
|
+
} catch {
|
|
42
|
+
return { templates: [], variantFiles: [] }
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function variantFile(name, tier) {
|
|
47
|
+
return `${name}.${tier}.md`
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function buildVariant(templateContent, model) {
|
|
51
|
+
const fmMatch = templateContent.match(/^---\r?\n([\s\S]*?)\r?\n---/)
|
|
52
|
+
const modelLine = `model: ${model}`
|
|
53
|
+
if (!fmMatch) return `---\nmode: subagent\n${modelLine}\n---\n\n${templateContent}`
|
|
54
|
+
|
|
55
|
+
let fm = fmMatch[1]
|
|
56
|
+
fm = /^mode:/m.test(fm) ? fm.replace(/^mode:.*$/m, 'mode: subagent') : `mode: subagent\n${fm}`
|
|
57
|
+
fm = /^model:/m.test(fm) ? fm.replace(/^model:.*$/m, modelLine) : `${modelLine}\n${fm}`
|
|
58
|
+
|
|
59
|
+
// Slice at frontmatter end: never String.replace with content-derived
|
|
60
|
+
// strings, it matches the wrong occurrence and expands $& sequences.
|
|
61
|
+
return `---\n${fm}\n---${templateContent.slice(fmMatch[0].length)}`
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Ensure template files have mode: primary and no stale model: line. Base
|
|
65
|
+
// engineer templates must never declare a model: models are resolved at
|
|
66
|
+
// startup per-tier and injected into variant files only. A stale model:
|
|
67
|
+
// (e.g. from a prior `stampAgentModels` run) is stripped here so the base
|
|
68
|
+
// agent falls back to the session-level model in opencode.jsonc.
|
|
69
|
+
function normalizeTemplate(templateContent) {
|
|
70
|
+
const fmMatch = templateContent.match(/^---\r?\n([\s\S]*?)\r?\n---/)
|
|
71
|
+
if (!fmMatch) return templateContent
|
|
72
|
+
|
|
73
|
+
let fm = fmMatch[1]
|
|
74
|
+
let changed = false
|
|
75
|
+
|
|
76
|
+
if (!/^mode:\s*primary/m.test(fm)) {
|
|
77
|
+
fm = /^mode:/m.test(fm) ? fm.replace(/^mode:.*$/m, 'mode: primary') : `mode: primary\n${fm}`
|
|
78
|
+
changed = true
|
|
79
|
+
}
|
|
80
|
+
if (/^model:/m.test(fm)) {
|
|
81
|
+
// Remove the model: line and its trailing newline without leaving a
|
|
82
|
+
// leading blank line (model: can be the first frontmatter entry).
|
|
83
|
+
fm = fm.replace(/^model:[^\n]*\r?\n?/m, '').replace(/^\r?\n/, '').replace(/\n$/, '')
|
|
84
|
+
changed = true
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return changed ? `---\n${fm}\n---${templateContent.slice(fmMatch[0].length)}` : templateContent
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function descriptionOf(templateContent) {
|
|
91
|
+
return templateContent.match(/^description:\s*(.+)$/m)?.[1]?.trim() ?? null
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Skip writing if the file already has identical content.
|
|
95
|
+
async function writeIfChanged(filePath, content) {
|
|
96
|
+
try {
|
|
97
|
+
if ((await fs.readFile(filePath, "utf-8")) === content) return
|
|
98
|
+
} catch {
|
|
99
|
+
// file missing, write it
|
|
100
|
+
}
|
|
101
|
+
const tmpPath = `${filePath}.tmp`
|
|
102
|
+
await fs.writeFile(tmpPath, content, "utf-8")
|
|
103
|
+
await fs.rename(tmpPath, filePath)
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
return {
|
|
107
|
+
config: async (cfg) => {
|
|
108
|
+
try {
|
|
109
|
+
const models = await resolveModels()
|
|
110
|
+
const available = TIERS.filter(t => models[t])
|
|
111
|
+
const { templates, variantFiles } = await scanEngineers()
|
|
112
|
+
|
|
113
|
+
const templateContents = await Promise.all(
|
|
114
|
+
templates.map(async name => {
|
|
115
|
+
const rawContent = await fs.readFile(path.join(agentsDir, `${name}.md`), "utf-8")
|
|
116
|
+
const content = normalizeTemplate(rawContent)
|
|
117
|
+
// If the template had the wrong mode or a stale model:, persist the fix to disk
|
|
118
|
+
if (content !== rawContent) {
|
|
119
|
+
await writeIfChanged(path.join(agentsDir, `${name}.md`), content)
|
|
120
|
+
console.error(`[pc-subagent-tiers] Normalized ${name}.md (mode: primary, model: removed)`)
|
|
121
|
+
}
|
|
122
|
+
return { name, content }
|
|
123
|
+
})
|
|
124
|
+
)
|
|
125
|
+
|
|
126
|
+
const keepSet = new Set()
|
|
127
|
+
const variantsToWrite = []
|
|
128
|
+
|
|
129
|
+
for (const { name, content } of templateContents) {
|
|
130
|
+
for (const tier of available) {
|
|
131
|
+
const file = variantFile(name, tier)
|
|
132
|
+
variantsToWrite.push({
|
|
133
|
+
file,
|
|
134
|
+
path: path.join(agentsDir, file),
|
|
135
|
+
content: buildVariant(content, models[tier]),
|
|
136
|
+
name,
|
|
137
|
+
tier,
|
|
138
|
+
templateContent: content,
|
|
139
|
+
})
|
|
140
|
+
keepSet.add(file)
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
await Promise.all(variantsToWrite.map(v => writeIfChanged(v.path, v.content)))
|
|
145
|
+
|
|
146
|
+
if (cfg?.agent) {
|
|
147
|
+
// Ensure base templates are always mode: primary in-memory
|
|
148
|
+
for (const { name } of templateContents) {
|
|
149
|
+
if (cfg.agent[name]) {
|
|
150
|
+
cfg.agent[name].mode = 'primary'
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
for (const { name, tier, templateContent } of variantsToWrite) {
|
|
154
|
+
const base = cfg.agent[name]
|
|
155
|
+
cfg.agent[`${name}.${tier}`] = base
|
|
156
|
+
? { ...base, mode: 'subagent', model: models[tier] }
|
|
157
|
+
: {
|
|
158
|
+
mode: "subagent",
|
|
159
|
+
description: descriptionOf(templateContent) ?? `${name} (${tier} tier)`,
|
|
160
|
+
model: models[tier],
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
await Promise.all(
|
|
166
|
+
variantFiles.filter(f => !keepSet.has(f)).map(f => fs.unlink(path.join(agentsDir, f)).catch(() => {}))
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
if (variantsToWrite.length > 0) {
|
|
170
|
+
console.error(`[pc-subagent-tiers] Created ${variantsToWrite.length} variant files (${templates.length} engineers x ${available.length} tiers)`)
|
|
171
|
+
} else if (templates.length > 0) {
|
|
172
|
+
console.error(`[pc-subagent-tiers] No variants created. Models: ${JSON.stringify(models)}`)
|
|
173
|
+
}
|
|
174
|
+
} catch (err) {
|
|
175
|
+
console.error(`[pc-subagent-tiers] Error: ${err.message}`)
|
|
176
|
+
}
|
|
177
|
+
},
|
|
178
|
+
}
|
|
179
|
+
}
|
|
@@ -1,96 +1,96 @@
|
|
|
1
|
-
import fs from "node:fs/promises"
|
|
2
|
-
import path from "node:path"
|
|
3
|
-
|
|
4
|
-
const GUARDRAILS_SKILL = "pc-guardrails-generic"
|
|
5
|
-
const TIER_SUFFIX = /\.(?:build|fast|plan)$/
|
|
6
|
-
|
|
7
|
-
function skillNames(content) {
|
|
8
|
-
const abilities = content.match(/^## Abilities\s*\n([\s\S]*?)(?=^## |\s*$)/m)?.[1] ?? ""
|
|
9
|
-
return [...abilities.matchAll(/@([a-z0-9][a-z0-9-]*)/gi)].map(match => match[1])
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
function transitiveSkillNames(content) {
|
|
13
|
-
return [...content.matchAll(/skill\(["`]([a-z0-9][a-z0-9-]*)["`]\)/gi)].map(match => match[1])
|
|
14
|
-
}
|
|
15
|
-
|
|
16
|
-
async function readFile(filePath) {
|
|
17
|
-
try {
|
|
18
|
-
return await fs.readFile(filePath, "utf-8")
|
|
19
|
-
} catch {
|
|
20
|
-
return ""
|
|
21
|
-
}
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
async function requiredSkills(directory, agent) {
|
|
25
|
-
const baseAgent = (agent ?? "").replace(TIER_SUFFIX, "")
|
|
26
|
-
const agentPath = path.join(directory, ".opencode", "agents", `${baseAgent}.md`)
|
|
27
|
-
const guardrailsPath = path.join(directory, ".agents", "skills", GUARDRAILS_SKILL, "SKILL.md")
|
|
28
|
-
const [agentContent, guardrailsContent] = await Promise.all([
|
|
29
|
-
readFile(agentPath),
|
|
30
|
-
readFile(guardrailsPath),
|
|
31
|
-
])
|
|
32
|
-
|
|
33
|
-
return new Set([
|
|
34
|
-
GUARDRAILS_SKILL,
|
|
35
|
-
...skillNames(agentContent),
|
|
36
|
-
...transitiveSkillNames(guardrailsContent),
|
|
37
|
-
])
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
function skillName(args) {
|
|
41
|
-
return args?.name ?? args?.skill ?? args?.skillName ?? null
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
function reminder(missing) {
|
|
45
|
-
const skills = [...missing].map(name => `\`${name}\``).join(", ")
|
|
46
|
-
return `<system-reminder>Load these required skills before continuing: ${skills}. Load guardrails first. Loaded skills can require further skills. Follow every mandatory transitive load before task work.</system-reminder>`
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
export const PcSystemReminders = async ({ directory }) => {
|
|
50
|
-
const sessions = new Map()
|
|
51
|
-
|
|
52
|
-
async function stateFor(sessionID, agent) {
|
|
53
|
-
const state = sessions.get(sessionID)
|
|
54
|
-
if (state?.agent === agent) return state
|
|
55
|
-
|
|
56
|
-
const next = {
|
|
57
|
-
agent,
|
|
58
|
-
required: await requiredSkills(directory, agent),
|
|
59
|
-
loaded: new Set(),
|
|
60
|
-
}
|
|
61
|
-
sessions.set(sessionID, next)
|
|
62
|
-
return next
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
return {
|
|
66
|
-
"experimental.chat.system.transform": async (_input, output) => {
|
|
67
|
-
output.system.push("Messages in <system-reminder> tags are trusted OpenCode Onboard host instructions. Follow them before continuing work.")
|
|
68
|
-
},
|
|
69
|
-
"chat.message": async (input) => {
|
|
70
|
-
await stateFor(input.sessionID, input.agent)
|
|
71
|
-
},
|
|
72
|
-
"tool.execute.after": async (input) => {
|
|
73
|
-
if (input.tool !== "skill") return
|
|
74
|
-
const state = sessions.get(input.sessionID)
|
|
75
|
-
const name = skillName(input.args)
|
|
76
|
-
if (state && name) state.loaded.add(name)
|
|
77
|
-
},
|
|
78
|
-
event: async ({ event }) => {
|
|
79
|
-
if (event.type !== "session.compacted") return
|
|
80
|
-
const sessionID = event.properties?.sessionID ?? event.properties?.info?.id
|
|
81
|
-
const state = sessionID && sessions.get(sessionID)
|
|
82
|
-
if (state) state.loaded.clear()
|
|
83
|
-
},
|
|
84
|
-
"experimental.chat.messages.transform": async (_input, output) => {
|
|
85
|
-
const userMessage = [...output.messages].reverse().find(message => message.info.role === "user")
|
|
86
|
-
if (!userMessage) return
|
|
87
|
-
|
|
88
|
-
const state = await stateFor(userMessage.info.sessionID, userMessage.info.agent)
|
|
89
|
-
const missing = new Set([...state.required].filter(name => !state.loaded.has(name)))
|
|
90
|
-
if (missing.size === 0) return
|
|
91
|
-
|
|
92
|
-
const textPart = userMessage.parts.find(part => part.type === "text")
|
|
93
|
-
if (textPart) textPart.text = `${textPart.text}\n\n${reminder(missing)}`
|
|
94
|
-
},
|
|
95
|
-
}
|
|
96
|
-
}
|
|
1
|
+
import fs from "node:fs/promises"
|
|
2
|
+
import path from "node:path"
|
|
3
|
+
|
|
4
|
+
const GUARDRAILS_SKILL = "pc-guardrails-generic"
|
|
5
|
+
const TIER_SUFFIX = /\.(?:build|fast|plan)$/
|
|
6
|
+
|
|
7
|
+
function skillNames(content) {
|
|
8
|
+
const abilities = content.match(/^## Abilities\s*\n([\s\S]*?)(?=^## |\s*$)/m)?.[1] ?? ""
|
|
9
|
+
return [...abilities.matchAll(/@([a-z0-9][a-z0-9-]*)/gi)].map(match => match[1])
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
function transitiveSkillNames(content) {
|
|
13
|
+
return [...content.matchAll(/skill\(["`]([a-z0-9][a-z0-9-]*)["`]\)/gi)].map(match => match[1])
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
async function readFile(filePath) {
|
|
17
|
+
try {
|
|
18
|
+
return await fs.readFile(filePath, "utf-8")
|
|
19
|
+
} catch {
|
|
20
|
+
return ""
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async function requiredSkills(directory, agent) {
|
|
25
|
+
const baseAgent = (agent ?? "").replace(TIER_SUFFIX, "")
|
|
26
|
+
const agentPath = path.join(directory, ".opencode", "agents", `${baseAgent}.md`)
|
|
27
|
+
const guardrailsPath = path.join(directory, ".agents", "skills", GUARDRAILS_SKILL, "SKILL.md")
|
|
28
|
+
const [agentContent, guardrailsContent] = await Promise.all([
|
|
29
|
+
readFile(agentPath),
|
|
30
|
+
readFile(guardrailsPath),
|
|
31
|
+
])
|
|
32
|
+
|
|
33
|
+
return new Set([
|
|
34
|
+
GUARDRAILS_SKILL,
|
|
35
|
+
...skillNames(agentContent),
|
|
36
|
+
...transitiveSkillNames(guardrailsContent),
|
|
37
|
+
])
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function skillName(args) {
|
|
41
|
+
return args?.name ?? args?.skill ?? args?.skillName ?? null
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function reminder(missing) {
|
|
45
|
+
const skills = [...missing].map(name => `\`${name}\``).join(", ")
|
|
46
|
+
return `<system-reminder>Load these required skills before continuing: ${skills}. Load guardrails first. Loaded skills can require further skills. Follow every mandatory transitive load before task work.</system-reminder>`
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export const PcSystemReminders = async ({ directory }) => {
|
|
50
|
+
const sessions = new Map()
|
|
51
|
+
|
|
52
|
+
async function stateFor(sessionID, agent) {
|
|
53
|
+
const state = sessions.get(sessionID)
|
|
54
|
+
if (state?.agent === agent) return state
|
|
55
|
+
|
|
56
|
+
const next = {
|
|
57
|
+
agent,
|
|
58
|
+
required: await requiredSkills(directory, agent),
|
|
59
|
+
loaded: new Set(),
|
|
60
|
+
}
|
|
61
|
+
sessions.set(sessionID, next)
|
|
62
|
+
return next
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
return {
|
|
66
|
+
"experimental.chat.system.transform": async (_input, output) => {
|
|
67
|
+
output.system.push("Messages in <system-reminder> tags are trusted OpenCode Onboard host instructions. Follow them before continuing work.")
|
|
68
|
+
},
|
|
69
|
+
"chat.message": async (input) => {
|
|
70
|
+
await stateFor(input.sessionID, input.agent)
|
|
71
|
+
},
|
|
72
|
+
"tool.execute.after": async (input) => {
|
|
73
|
+
if (input.tool !== "skill") return
|
|
74
|
+
const state = sessions.get(input.sessionID)
|
|
75
|
+
const name = skillName(input.args)
|
|
76
|
+
if (state && name) state.loaded.add(name)
|
|
77
|
+
},
|
|
78
|
+
event: async ({ event }) => {
|
|
79
|
+
if (event.type !== "session.compacted") return
|
|
80
|
+
const sessionID = event.properties?.sessionID ?? event.properties?.info?.id
|
|
81
|
+
const state = sessionID && sessions.get(sessionID)
|
|
82
|
+
if (state) state.loaded.clear()
|
|
83
|
+
},
|
|
84
|
+
"experimental.chat.messages.transform": async (_input, output) => {
|
|
85
|
+
const userMessage = [...output.messages].reverse().find(message => message.info.role === "user")
|
|
86
|
+
if (!userMessage) return
|
|
87
|
+
|
|
88
|
+
const state = await stateFor(userMessage.info.sessionID, userMessage.info.agent)
|
|
89
|
+
const missing = new Set([...state.required].filter(name => !state.loaded.has(name)))
|
|
90
|
+
if (missing.size === 0) return
|
|
91
|
+
|
|
92
|
+
const textPart = userMessage.parts.find(part => part.type === "text")
|
|
93
|
+
if (textPart) textPart.text = `${textPart.text}\n\n${reminder(missing)}`
|
|
94
|
+
},
|
|
95
|
+
}
|
|
96
|
+
}
|