@plainconceptsplatform/agent-harness 2.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (151) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +419 -0
  3. package/package.json +68 -0
  4. package/src/commands/join.js +244 -0
  5. package/src/commands/shared.js +27 -0
  6. package/src/commands/single.js +79 -0
  7. package/src/commands/update.js +109 -0
  8. package/src/commands/wizard.js +134 -0
  9. package/src/content/.agents/skills/browser-automation/SKILL.md +66 -0
  10. package/src/content/.agents/skills/pc-guardrails-generic/SKILL.md +68 -0
  11. package/src/content/.agents/skills/pc-guardrails-project/SKILL.md +8 -0
  12. package/src/content/.agents/skills/pc-make-architecture/SKILL.md +51 -0
  13. package/src/content/.agents/skills/pc-make-architecture/structure-template.md +38 -0
  14. package/src/content/.agents/skills/pc-make-design/SKILL.md +68 -0
  15. package/src/content/.agents/skills/pc-make-engineer/SKILL.md +219 -0
  16. package/src/content/.agents/skills/pc-make-engineer/signal-mapping.md +68 -0
  17. package/src/content/.agents/skills/pc-make-engineer/template.md +81 -0
  18. package/src/content/.agents/skills/pc-make-evidence-scaffold/SKILL.md +18 -0
  19. package/src/content/.agents/skills/pc-make-evidence-scaffold/evidence-contract.md +29 -0
  20. package/src/content/.agents/skills/pc-make-guardrails/SKILL.md +74 -0
  21. package/src/content/.agents/skills/pc-make-guardrails/category-reference.md +68 -0
  22. package/src/content/.agents/skills/pc-make-merge-risk-assess/SKILL.md +70 -0
  23. package/src/content/.agents/skills/pc-make-merge-risk-assess/category-reference.md +98 -0
  24. package/src/content/.agents/skills/pc-make-user-model/SKILL.md +66 -0
  25. package/src/content/.agents/skills/pc-ops-evidence/SKILL.md +127 -0
  26. package/src/content/.agents/skills/pc-ops-ship/SKILL.md +18 -0
  27. package/src/content/.agents/skills/pc-plan-apply/SKILL.md +83 -0
  28. package/src/content/.agents/skills/pc-plan-apply/simple-mode.md +21 -0
  29. package/src/content/.agents/skills/pc-plan-archive/SKILL.md +63 -0
  30. package/src/content/.agents/skills/pc-plan-explore/SKILL.md +9 -0
  31. package/src/content/.agents/skills/pc-plan-goal/SKILL.md +94 -0
  32. package/src/content/.agents/skills/pc-plan-goal/branching.md +30 -0
  33. package/src/content/.agents/skills/pc-plan-goal/failure-policy.md +30 -0
  34. package/src/content/.agents/skills/pc-plan-goal/output-mode.md +9 -0
  35. package/src/content/.agents/skills/pc-plan-goal/output.md +68 -0
  36. package/src/content/.agents/skills/pc-plan-propose/SKILL.md +125 -0
  37. package/src/content/.agents/skills/pc-plan-propose/task-annotation.md +39 -0
  38. package/src/content/.agents/skills/pc-plan-quick/SKILL.md +62 -0
  39. package/src/content/.agents/skills/pc-plan-story/SKILL.md +146 -0
  40. package/src/content/.agents/skills/pc-repo-audit/SKILL.md +44 -0
  41. package/src/content/.agents/skills/pc-repo-help/SKILL.md +91 -0
  42. package/src/content/.agents/skills/pc-repo-initialize/SKILL.md +130 -0
  43. package/src/content/.agents/skills/pc-repo-onboard/SKILL.md +87 -0
  44. package/src/content/.agents/skills/pc-repo-verify/SKILL.md +34 -0
  45. package/src/content/.agents/skills/pc-userstory-az/SKILL.md +157 -0
  46. package/src/content/.agents/skills/pc-userstory-browser/SKILL.md +132 -0
  47. package/src/content/.agents/skills/pc-userstory-gh/SKILL.md +120 -0
  48. package/src/content/.agents/skills/pc-userstory-jira/SKILL.md +131 -0
  49. package/src/content/.opencode/_gitignore +7 -0
  50. package/src/content/.opencode/commands/init.md +5 -0
  51. package/src/content/.opencode/commands/make-architecture.md +5 -0
  52. package/src/content/.opencode/commands/make-design.md +5 -0
  53. package/src/content/.opencode/commands/make-engineer.md +5 -0
  54. package/src/content/.opencode/commands/make-evidence-scaffold.md +5 -0
  55. package/src/content/.opencode/commands/make-guardrails.md +5 -0
  56. package/src/content/.opencode/commands/make-user-model.md +5 -0
  57. package/src/content/.opencode/commands/ops-backlog.md +10 -0
  58. package/src/content/.opencode/commands/ops-evidence.md +9 -0
  59. package/src/content/.opencode/commands/ops-review.md +8 -0
  60. package/src/content/.opencode/commands/ops-ship.md +9 -0
  61. package/src/content/.opencode/commands/plan-apply.md +9 -0
  62. package/src/content/.opencode/commands/plan-archive.md +5 -0
  63. package/src/content/.opencode/commands/plan-explore.md +9 -0
  64. package/src/content/.opencode/commands/plan-goal.md +5 -0
  65. package/src/content/.opencode/commands/plan-propose.md +9 -0
  66. package/src/content/.opencode/commands/plan-quick.md +5 -0
  67. package/src/content/.opencode/commands/plan-story.md +9 -0
  68. package/src/content/.opencode/commands/repo-audit.md +5 -0
  69. package/src/content/.opencode/commands/repo-help.md +5 -0
  70. package/src/content/.opencode/commands/repo-initialize.md +5 -0
  71. package/src/content/.opencode/commands/repo-onboard.md +5 -0
  72. package/src/content/.opencode/commands/repo-verify.md +5 -0
  73. package/src/content/.opencode/package.json +10 -0
  74. package/src/content/.opencode/plugins/pc-subagent-monitor.js +139 -0
  75. package/src/content/.opencode/plugins/pc-subagent-tiers.js +179 -0
  76. package/src/content/.opencode/plugins/pc-system-reminders.js +96 -0
  77. package/src/content/.opencode/plugins/pc-system-reminders.test.js +35 -0
  78. package/src/content/.opencode/tui/pc-subagents.tsx +98 -0
  79. package/src/content/.opencode/tui.json +6 -0
  80. package/src/content/AGENTS.md +71 -0
  81. package/src/content/ARCHITECTURE.md +16 -0
  82. package/src/content/DESIGN.md +16 -0
  83. package/src/content/opencode.jsonc +31 -0
  84. package/src/content/openspec/changes/archive/.gitkeep +0 -0
  85. package/src/content/openspec/config.yaml +20 -0
  86. package/src/content/openspec/specs/.gitkeep +0 -0
  87. package/src/content/skills-lock.json +17 -0
  88. package/src/fragments/archive/az.md +95 -0
  89. package/src/fragments/archive/gh.md +94 -0
  90. package/src/fragments/archive/gl.md +94 -0
  91. package/src/fragments/archive/none.md +73 -0
  92. package/src/fragments/guardrails/codegraph.md +7 -0
  93. package/src/fragments/guardrails/humanizer.md +4 -0
  94. package/src/fragments/guardrails/memory.md +4 -0
  95. package/src/fragments/guardrails/rtk.md +3 -0
  96. package/src/fragments/guardrails/simple-english.md +4 -0
  97. package/src/fragments/ops-backlog/az.md +29 -0
  98. package/src/fragments/ops-backlog/gh.md +30 -0
  99. package/src/fragments/ops-backlog/jira.md +29 -0
  100. package/src/fragments/ops-evidence/az.md +41 -0
  101. package/src/fragments/ops-evidence/gh.md +53 -0
  102. package/src/fragments/ops-evidence/jira.md +38 -0
  103. package/src/fragments/ops-review/az.md +63 -0
  104. package/src/fragments/ops-review/gh.md +53 -0
  105. package/src/fragments/ops-review/gl.md +57 -0
  106. package/src/fragments/ops-ship/az.md +81 -0
  107. package/src/fragments/ops-ship/gh.md +69 -0
  108. package/src/fragments/ops-ship/gl.md +86 -0
  109. package/src/index.js +107 -0
  110. package/src/presets/agents-content.json +53 -0
  111. package/src/presets/browser.json +22 -0
  112. package/src/presets/clean.json +21 -0
  113. package/src/presets/models.json +68 -0
  114. package/src/presets/openspec.json +1 -0
  115. package/src/presets/optimization.json +37 -0
  116. package/src/presets/platforms.json +76 -0
  117. package/src/presets/quota.json +16 -0
  118. package/src/presets/source.json +23 -0
  119. package/src/steps/browser/index.js +91 -0
  120. package/src/steps/clean/index.js +120 -0
  121. package/src/steps/copy/agents.js +118 -0
  122. package/src/steps/copy/commands.js +91 -0
  123. package/src/steps/copy/fullstack-engineer.js +83 -0
  124. package/src/steps/copy/index.js +88 -0
  125. package/src/steps/copy/opencode-json.js +129 -0
  126. package/src/steps/copy/skills.js +196 -0
  127. package/src/steps/metadata/index.js +108 -0
  128. package/src/steps/models/format.js +88 -0
  129. package/src/steps/models/index.js +64 -0
  130. package/src/steps/models/write.js +34 -0
  131. package/src/steps/openspec/index.js +136 -0
  132. package/src/steps/optimization/codegraph.js +127 -0
  133. package/src/steps/optimization/humanizer.js +17 -0
  134. package/src/steps/optimization/index.js +163 -0
  135. package/src/steps/optimization/memory.js +88 -0
  136. package/src/steps/optimization/patch-guardrails.js +108 -0
  137. package/src/steps/optimization/quota.js +119 -0
  138. package/src/steps/optimization/simple-english.js +17 -0
  139. package/src/steps/optimization/skills-lock.js +30 -0
  140. package/src/steps/platform/index.js +109 -0
  141. package/src/steps/source/index.js +123 -0
  142. package/src/utils/copy.js +108 -0
  143. package/src/utils/exec-spinner.js +47 -0
  144. package/src/utils/exec.js +134 -0
  145. package/src/utils/legacy-check.js +30 -0
  146. package/src/utils/models-cache.js +58 -0
  147. package/src/utils/models-pricing.js +42 -0
  148. package/src/utils/paths.js +64 -0
  149. package/src/utils/process.js +3 -0
  150. package/src/utils/terminal.js +6 -0
  151. package/src/utils/update-manifest.js +49 -0
@@ -0,0 +1,5 @@
1
+ ---
2
+ description: Quick plan: analyze the codebase and create a task checklist using the Todo pane. No files, no OpenSpec.
3
+ ---
4
+
5
+ Load the `pc-plan-quick` skill and follow every step defined in it.
@@ -0,0 +1,9 @@
1
+ ---
2
+ description: Write a detailed, repo-aware user story from a feature idea or need.
3
+ ---
4
+
5
+ Load the `pc-plan-story` skill and follow every step defined in it.
6
+
7
+ Input:
8
+
9
+ $ARGUMENTS
@@ -0,0 +1,5 @@
1
+ ---
2
+ description: Audit every configured source root against the fullstack engineer's guardrails and abilities. Read-only.
3
+ ---
4
+
5
+ Load the `pc-repo-audit` skill and follow every step defined in it.
@@ -0,0 +1,5 @@
1
+ ---
2
+ description: Show all available commands and when to use each one.
3
+ ---
4
+
5
+ Load the `pc-repo-help` skill and display its reference to the user exactly as written. Do not summarize.
@@ -0,0 +1,5 @@
1
+ ---
2
+ description: Initialize the project. Presents a single form with all setup questions, then executes selected steps.
3
+ ---
4
+
5
+ Load the `pc-repo-initialize` skill and follow every step defined in it.
@@ -0,0 +1,5 @@
1
+ ---
2
+ description: Walk the user through the project and its agentic infrastructure. Explains what exists, how agents work, and how to use the system.
3
+ ---
4
+
5
+ Load the `pc-repo-onboard` skill and follow every step defined in it.
@@ -0,0 +1,5 @@
1
+ ---
2
+ description: Verify current-branch changes, plus immutable dependency installs, builds, and tests for every discovered project.
3
+ ---
4
+
5
+ Load the `pc-repo-verify` skill and follow every step defined in it.
@@ -0,0 +1,10 @@
1
+ {
2
+ "dependencies": {
3
+ "@opencode-ai/plugin": "1.18.19",
4
+ "@different-ai/opencode-browser": "4.6.1",
5
+ "@mohak34/opencode-notifier": "0.2.8",
6
+ "@opentui/core": "0.5.6",
7
+ "@opentui/solid": "0.5.6",
8
+ "solid-js": "1.9.12"
9
+ }
10
+ }
@@ -0,0 +1,139 @@
1
+ // pc-subagent-monitor: tracks spawned subagents in .opencode/harness-run.json for
2
+ // live TUI display and crash recovery. Never throws: monitor failures must
3
+ // not break a session.
4
+
5
+ import fs from "node:fs/promises"
6
+ import path from "node:path"
7
+
8
+ export const PcSubagentMonitor = async ({ directory, client }) => {
9
+ const root = directory || process.cwd()
10
+ const statePath = path.join(root, ".opencode", "harness-run.json")
11
+ const state = { updatedAt: null, agents: {} }
12
+
13
+ try {
14
+ const prev = JSON.parse(await fs.readFile(statePath, "utf-8"))
15
+ if (prev?.agents && typeof prev.agents === "object") {
16
+ for (const [id, entry] of Object.entries(prev.agents)) {
17
+ if (entry?.status === "running") entry.stale = true
18
+ state.agents[id] = entry
19
+ }
20
+ }
21
+ } catch {
22
+ // fresh start
23
+ }
24
+
25
+ let _modelsCache = null
26
+ async function loadModels() {
27
+ if (_modelsCache) return _modelsCache
28
+ const result = { build: null, fast: null, plan: null }
29
+ for (const file of ["harness.user.json", "harness.json"]) {
30
+ try {
31
+ const raw = await fs.readFile(path.join(root, ".opencode", file), "utf-8")
32
+ const { models = {} } = JSON.parse(raw)
33
+ for (const tier of ["build", "fast", "plan"]) {
34
+ if (!result[tier] && models[tier]) result[tier] = models[tier]
35
+ }
36
+ } catch {
37
+ continue
38
+ }
39
+ }
40
+ _modelsCache = result
41
+ return result
42
+ }
43
+
44
+ async function modelForAgent(agent) {
45
+ if (!agent) return null
46
+ const dotIdx = agent.lastIndexOf(".")
47
+ const tier = dotIdx !== -1 ? agent.slice(dotIdx + 1) : null
48
+ if (tier && ["build", "fast", "plan"].includes(tier)) {
49
+ const models = await loadModels()
50
+ return models[tier] ?? null
51
+ }
52
+ return null
53
+ }
54
+
55
+ function parseTasks(title) {
56
+ if (!title) return []
57
+ const m = /^\s*([\d]+(?:\.[\d]+)*(?:\s*,\s*[\d]+(?:\.[\d]+)*)*)/.exec(title)
58
+ return m ? m[1].split(",").map(s => s.trim()) : []
59
+ }
60
+
61
+ // Debounced persist: coalesce rapid writes when multiple subagents spawn
62
+ // in the same microtask batch (e.g. a wave of 5 task() calls).
63
+ let _persistScheduled = false
64
+ async function persist() {
65
+ if (_persistScheduled) return
66
+ _persistScheduled = true
67
+ queueMicrotask(async () => {
68
+ _persistScheduled = false
69
+ state.updatedAt = new Date().toISOString()
70
+ try {
71
+ await fs.mkdir(path.dirname(statePath), { recursive: true })
72
+ const tmpPath = `${statePath}.tmp`
73
+ await fs.writeFile(tmpPath, JSON.stringify(state, null, 2), "utf-8")
74
+ await fs.rename(tmpPath, statePath)
75
+ } catch {
76
+ // best-effort
77
+ }
78
+ })
79
+ }
80
+
81
+ function sessionInfo(props) {
82
+ const info = props?.info ?? props ?? {}
83
+ return {
84
+ id: info.id ?? info.sessionID ?? props?.sessionID,
85
+ parentID: info.parentID ?? info.parentId,
86
+ agent: info.agent,
87
+ title: info.title,
88
+ }
89
+ }
90
+
91
+ function pruneStale() {
92
+ for (const [id, entry] of Object.entries(state.agents)) {
93
+ if (entry?.stale) delete state.agents[id]
94
+ }
95
+ }
96
+
97
+ return {
98
+ event: async ({ event }) => {
99
+ try {
100
+ if (!event?.type?.startsWith("session.")) return
101
+ const info = sessionInfo(event.properties)
102
+ if (!info.id) return
103
+
104
+ if (event.type === "session.created" && info.parentID) {
105
+ pruneStale()
106
+
107
+ state.agents[info.id] = {
108
+ agent: info.agent ?? null,
109
+ model: await modelForAgent(info.agent),
110
+ tasks: parseTasks(info.title),
111
+ title: info.title ?? null,
112
+ status: "running",
113
+ startedAt: new Date().toISOString(),
114
+ endedAt: null,
115
+ }
116
+ await persist()
117
+ return
118
+ }
119
+
120
+ const entry = state.agents[info.id]
121
+ if (!entry) return
122
+
123
+ if (event.type === "session.idle" && entry.status === "running") {
124
+ entry.status = "done"
125
+ entry.endedAt = new Date().toISOString()
126
+ await persist()
127
+ client?.tui?.showToast?.({
128
+ body: {
129
+ message: `subagent done: ${entry.title ?? info.id}`,
130
+ variant: "success",
131
+ },
132
+ })
133
+ }
134
+ } catch {
135
+ // monitoring must never disrupt the run
136
+ }
137
+ },
138
+ }
139
+ }
@@ -0,0 +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
+ }
@@ -0,0 +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
+ }
@@ -0,0 +1,35 @@
1
+ import { describe, expect, it } from "vitest"
2
+ import { PcSystemReminders } from "./pc-system-reminders.js"
3
+
4
+ describe("PcSystemReminders", () => {
5
+ it("reminds an agent to load its abilities before work", async () => {
6
+ const plugin = await PcSystemReminders({ directory: process.cwd() })
7
+ const output = {
8
+ messages: [{
9
+ info: { role: "user", sessionID: "session-1", agent: "missing-agent" },
10
+ parts: [{ type: "text", text: "Implement the change" }],
11
+ }],
12
+ }
13
+
14
+ await plugin["chat.message"]({ sessionID: "session-1", agent: "missing-agent" })
15
+ await plugin["experimental.chat.messages.transform"]({}, output)
16
+
17
+ expect(output.messages[0].parts[0].text).toContain("pc-guardrails-generic")
18
+ })
19
+
20
+ it("stops reminding after the required guardrail loads", async () => {
21
+ const plugin = await PcSystemReminders({ directory: process.cwd() })
22
+ const output = {
23
+ messages: [{
24
+ info: { role: "user", sessionID: "session-2", agent: "missing-agent" },
25
+ parts: [{ type: "text", text: "Implement the change" }],
26
+ }],
27
+ }
28
+
29
+ await plugin["chat.message"]({ sessionID: "session-2", agent: "missing-agent" })
30
+ await plugin["tool.execute.after"]({ tool: "skill", sessionID: "session-2", args: { name: "pc-guardrails-generic" } })
31
+ await plugin["experimental.chat.messages.transform"]({}, output)
32
+
33
+ expect(output.messages[0].parts[0].text).not.toContain("system-reminder")
34
+ })
35
+ })
@@ -0,0 +1,98 @@
1
+ /** @jsxImportSource @opentui/solid */
2
+ import type { TuiPlugin, TuiPluginModule } from "@opencode-ai/plugin/tui"
3
+ import { createSignal, For, Show } from "solid-js"
4
+ import { readFile } from "node:fs/promises"
5
+ import { join } from "node:path"
6
+
7
+ const id = "ob.subagents"
8
+
9
+ type Row = { id: string; agent: string; model: string; task: string; status: string }
10
+
11
+ // Renders a live "Subagents" panel in the session sidebar.
12
+ //
13
+ // Server plugins (opencode.json) cannot draw UI, only TUI plugins (tui.json)
14
+ // can, via the `sidebar_content` slot. This panel is fed by the
15
+ // `.opencode/harness-run.json` state file that the `pc-subagent-monitor` server
16
+ // plugin maintains, so the two cooperate: server plugin = data producer,
17
+ // this TUI plugin = renderer.
18
+ const tui: TuiPlugin = async (api) => {
19
+ const statePath = join(process.cwd(), ".opencode", "harness-run.json")
20
+ const [rows, setRows] = createSignal<Row[]>([])
21
+ // Only show live subagents, finished/failed ones are kept in harness-run.json
22
+ // for recovery but are not navigable targets the user cares about here.
23
+ // Entries marked stale (left "running" by a crashed process) are not live.
24
+ const active = () => rows().filter((r) => r.status === "running")
25
+
26
+ const refresh = async () => {
27
+ try {
28
+ const data = JSON.parse(await readFile(statePath, "utf-8"))
29
+ const agents = data?.agents ?? {}
30
+ setRows(
31
+ Object.entries(agents)
32
+ .filter(([, a]: [string, any]) => !a?.stale)
33
+ .map(([sid, a]: [string, any]) => ({
34
+ id: sid,
35
+ agent: a?.agent ?? "?",
36
+ model: a?.model ?? "",
37
+ task: Array.isArray(a?.tasks) && a.tasks.length ? a.tasks.join(",") : (a?.title ?? ""),
38
+ status: a?.status ?? "running",
39
+ })),
40
+ )
41
+ } catch (err: any) {
42
+ // Missing file = genuinely idle. Anything else (transient read/parse
43
+ // hiccup) keeps the last good rows instead of flashing "idle" mid-run.
44
+ if (err?.code === "ENOENT") setRows([])
45
+ }
46
+ }
47
+
48
+ await refresh()
49
+ // session.updated fires constantly; debounce so we don't re-read the file
50
+ // on every keystroke-sized event.
51
+ let timer: ReturnType<typeof setTimeout> | null = null
52
+ const scheduleRefresh = () => {
53
+ if (timer) return
54
+ timer = setTimeout(() => {
55
+ timer = null
56
+ void refresh()
57
+ }, 200)
58
+ }
59
+ for (const evt of ["session.created", "session.idle", "session.updated"]) {
60
+ try {
61
+ api.event.on(evt as any, scheduleRefresh)
62
+ } catch {
63
+ /* event type unavailable on this host, ignore */
64
+ }
65
+ }
66
+
67
+ api.slots.register({
68
+ order: 50,
69
+ slots: {
70
+ sidebar_content() {
71
+ return (
72
+ <box flexDirection="column">
73
+ <text>Subagents</text>
74
+ <Show when={active().length === 0}>
75
+ <text> idle</text>
76
+ </Show>
77
+ <For each={active()}>
78
+ {(r) => (
79
+ <box onMouseUp={() => api.route.navigate("session", { sessionID: r.id })}>
80
+ <text>
81
+ {"▶ "}
82
+ {r.agent}
83
+ {r.model ? ` · ${r.model}` : ""}
84
+ {r.task ? `, ${r.task}` : ""}
85
+ </text>
86
+ </box>
87
+ )}
88
+ </For>
89
+ </box>
90
+ )
91
+ },
92
+ },
93
+ })
94
+ }
95
+
96
+ const pluginModule: TuiPluginModule & { id: string } = { id, tui }
97
+
98
+ export default pluginModule
@@ -0,0 +1,6 @@
1
+ {
2
+ "$schema": "https://opencode.ai/tui.json",
3
+ "plugin": [
4
+ "./tui/pc-subagents.tsx"
5
+ ]
6
+ }