@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.
Files changed (116) hide show
  1. package/README.md +423 -419
  2. package/package.json +3 -3
  3. package/src/commands/join.js +244 -244
  4. package/src/commands/shared.js +27 -27
  5. package/src/commands/single.js +79 -79
  6. package/src/commands/update.js +109 -109
  7. package/src/commands/wizard.js +134 -134
  8. package/src/content/.agents/skills/browser-automation/SKILL.md +66 -66
  9. package/src/content/.agents/skills/pc-guardrails-generic/SKILL.md +68 -68
  10. package/src/content/.agents/skills/pc-guardrails-project/SKILL.md +8 -8
  11. package/src/content/.agents/skills/pc-make-architecture/SKILL.md +51 -51
  12. package/src/content/.agents/skills/pc-make-architecture/structure-template.md +38 -38
  13. package/src/content/.agents/skills/pc-make-design/SKILL.md +68 -68
  14. package/src/content/.agents/skills/pc-make-engineer/SKILL.md +219 -219
  15. package/src/content/.agents/skills/pc-make-engineer/signal-mapping.md +68 -68
  16. package/src/content/.agents/skills/pc-make-engineer/template.md +81 -81
  17. package/src/content/.agents/skills/pc-make-evidence-scaffold/SKILL.md +18 -18
  18. package/src/content/.agents/skills/pc-make-evidence-scaffold/evidence-contract.md +29 -29
  19. package/src/content/.agents/skills/pc-make-guardrails/SKILL.md +74 -74
  20. package/src/content/.agents/skills/pc-make-guardrails/category-reference.md +68 -68
  21. package/src/content/.agents/skills/pc-make-merge-risk-assess/SKILL.md +70 -70
  22. package/src/content/.agents/skills/pc-make-merge-risk-assess/category-reference.md +98 -98
  23. package/src/content/.agents/skills/pc-make-user-model/SKILL.md +66 -66
  24. package/src/content/.agents/skills/pc-ops-evidence/SKILL.md +127 -127
  25. package/src/content/.agents/skills/pc-ops-ship/SKILL.md +18 -18
  26. package/src/content/.agents/skills/pc-plan-apply/SKILL.md +83 -83
  27. package/src/content/.agents/skills/pc-plan-apply/simple-mode.md +21 -21
  28. package/src/content/.agents/skills/pc-plan-archive/SKILL.md +63 -63
  29. package/src/content/.agents/skills/pc-plan-explore/SKILL.md +9 -9
  30. package/src/content/.agents/skills/pc-plan-goal/SKILL.md +94 -94
  31. package/src/content/.agents/skills/pc-plan-goal/branching.md +30 -30
  32. package/src/content/.agents/skills/pc-plan-goal/failure-policy.md +30 -30
  33. package/src/content/.agents/skills/pc-plan-goal/output-mode.md +9 -9
  34. package/src/content/.agents/skills/pc-plan-goal/output.md +68 -68
  35. package/src/content/.agents/skills/pc-plan-propose/SKILL.md +125 -125
  36. package/src/content/.agents/skills/pc-plan-propose/task-annotation.md +39 -39
  37. package/src/content/.agents/skills/pc-plan-quick/SKILL.md +62 -62
  38. package/src/content/.agents/skills/pc-plan-story/SKILL.md +146 -146
  39. package/src/content/.agents/skills/pc-repo-audit/SKILL.md +44 -44
  40. package/src/content/.agents/skills/pc-repo-help/SKILL.md +91 -91
  41. package/src/content/.agents/skills/pc-repo-initialize/SKILL.md +130 -130
  42. package/src/content/.agents/skills/pc-repo-onboard/SKILL.md +87 -87
  43. package/src/content/.agents/skills/pc-repo-verify/SKILL.md +34 -34
  44. package/src/content/.agents/skills/pc-userstory-az/SKILL.md +157 -157
  45. package/src/content/.agents/skills/pc-userstory-browser/SKILL.md +132 -132
  46. package/src/content/.agents/skills/pc-userstory-gh/SKILL.md +120 -120
  47. package/src/content/.agents/skills/pc-userstory-jira/SKILL.md +131 -131
  48. package/src/content/.opencode/_gitignore +7 -7
  49. package/src/content/.opencode/commands/init.md +5 -5
  50. package/src/content/.opencode/commands/make-architecture.md +5 -5
  51. package/src/content/.opencode/commands/make-design.md +5 -5
  52. package/src/content/.opencode/commands/make-engineer.md +5 -5
  53. package/src/content/.opencode/commands/make-evidence-scaffold.md +5 -5
  54. package/src/content/.opencode/commands/make-guardrails.md +5 -5
  55. package/src/content/.opencode/commands/make-user-model.md +5 -5
  56. package/src/content/.opencode/commands/ops-backlog.md +10 -10
  57. package/src/content/.opencode/commands/ops-evidence.md +9 -9
  58. package/src/content/.opencode/commands/ops-review.md +8 -8
  59. package/src/content/.opencode/commands/ops-ship.md +9 -9
  60. package/src/content/.opencode/commands/plan-apply.md +9 -9
  61. package/src/content/.opencode/commands/plan-archive.md +5 -5
  62. package/src/content/.opencode/commands/plan-explore.md +9 -9
  63. package/src/content/.opencode/commands/plan-goal.md +5 -5
  64. package/src/content/.opencode/commands/plan-propose.md +9 -9
  65. package/src/content/.opencode/commands/plan-quick.md +5 -5
  66. package/src/content/.opencode/commands/plan-story.md +9 -9
  67. package/src/content/.opencode/commands/repo-audit.md +5 -5
  68. package/src/content/.opencode/commands/repo-help.md +5 -5
  69. package/src/content/.opencode/commands/repo-initialize.md +5 -5
  70. package/src/content/.opencode/commands/repo-onboard.md +5 -5
  71. package/src/content/.opencode/commands/repo-verify.md +5 -5
  72. package/src/content/.opencode/plugins/pc-subagent-monitor.js +139 -139
  73. package/src/content/.opencode/plugins/pc-subagent-tiers.js +179 -179
  74. package/src/content/.opencode/plugins/pc-system-reminders.js +96 -96
  75. package/src/content/.opencode/tui/pc-subagents.tsx +98 -98
  76. package/src/content/.opencode/tui.json +6 -6
  77. package/src/content/AGENTS.md +71 -71
  78. package/src/fragments/archive/az.md +95 -95
  79. package/src/fragments/archive/gh.md +94 -94
  80. package/src/fragments/archive/gl.md +94 -94
  81. package/src/fragments/archive/none.md +73 -73
  82. package/src/fragments/guardrails/codegraph.md +7 -7
  83. package/src/fragments/guardrails/humanizer.md +4 -4
  84. package/src/fragments/guardrails/memory.md +4 -4
  85. package/src/fragments/guardrails/rtk.md +3 -3
  86. package/src/fragments/guardrails/simple-english.md +4 -4
  87. package/src/fragments/ops-backlog/az.md +28 -28
  88. package/src/fragments/ops-backlog/gh.md +29 -29
  89. package/src/fragments/ops-backlog/jira.md +28 -28
  90. package/src/fragments/ops-evidence/az.md +41 -41
  91. package/src/fragments/ops-evidence/gh.md +53 -53
  92. package/src/fragments/ops-evidence/jira.md +38 -38
  93. package/src/fragments/ops-review/az.md +62 -62
  94. package/src/fragments/ops-review/gh.md +52 -52
  95. package/src/fragments/ops-review/gl.md +56 -56
  96. package/src/fragments/ops-ship/az.md +80 -80
  97. package/src/fragments/ops-ship/gh.md +68 -68
  98. package/src/fragments/ops-ship/gl.md +85 -85
  99. package/src/index.js +107 -107
  100. package/src/presets/agents-content.json +53 -53
  101. package/src/presets/models.json +68 -68
  102. package/src/steps/copy/agents.js +118 -118
  103. package/src/steps/copy/commands.js +91 -91
  104. package/src/steps/copy/fullstack-engineer.js +83 -83
  105. package/src/steps/copy/index.js +88 -88
  106. package/src/steps/copy/opencode-json.js +129 -129
  107. package/src/steps/copy/skills.js +196 -196
  108. package/src/steps/metadata/index.js +108 -108
  109. package/src/steps/models/write.js +34 -34
  110. package/src/steps/optimization/patch-guardrails.js +108 -108
  111. package/src/utils/copy.js +108 -108
  112. package/src/utils/legacy-check.js +30 -30
  113. package/src/utils/models-cache.js +58 -58
  114. package/src/utils/paths.js +64 -64
  115. package/src/utils/update-manifest.js +49 -49
  116. 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
+ }