@plainconceptsplatform/agent-harness 2.0.1 → 2.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -164,12 +164,14 @@ Agents define _how to work_. They are universal personas (same behavior across p
164
164
  Current baseline uses a generic execution model:
165
165
 
166
166
  ```
167
- lead lead/orchestrator, planning, pull request lifecycle
168
- fullstack-engineer primary planning agent, accumulates all skills (user-facing, not spawned)
169
- *-engineer user-created specialists, spawned by the lead for parallel implementation
167
+ build primary. Implements. Full write access. The default.
168
+ plan primary. Same body as build, but cannot edit files.
169
+ fullstack-engineer subagent. The body build and plan share, and the fallback worker.
170
+ *-engineer subagent. User-created specialists, spawned for parallel implementation.
171
+ *-engineer.<tier> subagent. The same specialist pinned to a plan/build/fast model.
170
172
  ```
171
173
 
172
- `fullstack-engineer` is `mode: primary`, so it is the user's planning session agent rather than a spawned worker. Project-specific specialization comes from user-created custom engineers via `/make-engineer`. During `/plan-apply`, the lead inspects the engineers that actually exist in `.opencode/agents/` and spawns matching specialists. `fullstack-engineer` is never assigned to tasks. If no specialist matches, the user should create one.
174
+ `build` and `plan` are the only agents a human selects, and they are overrides of opencode's own two primaries rather than new names. The `pc-subagent-tiers` plugin regenerates both from `fullstack-engineer.md` on every startup, so they always carry the current abilities, each on its own tier model. `plan` differs from `build` in one frontmatter line: `edit: deny`. It can still read the tree, shell out to git and openspec, and spawn engineers, so planning works and cannot write. Everything else is `mode: subagent` and reached through `task()`, never picked from the agent list. Project-specific specialization comes from user-created engineers via `/make-engineer`. During `/plan-apply` the lead inspects the engineers that actually exist in `.opencode/agents/` and spawns matching specialists. Prefer a specialist over `fullstack-engineer`; if none matches, create one.
173
175
 
174
176
  ### Skills, platform knowledge
175
177
 
@@ -289,7 +291,8 @@ your-project/
289
291
  │ ├── opencode.json ← default model + plugin config
290
292
  │ ├── harness.json ← harness config: platform, models, maxConcurrentAgents
291
293
  │ ├── harness-managed.json ← hashes of every managed file, so "update" spares your edits
292
- │ ├── agents/ ← fullstack-engineer (primary, planning) + user-created *-engineer files
294
+ │ ├── agents/ ← build.md + plan.md (generated primaries), fullstack-engineer,
295
+ │ │ and user-created *-engineer files (all subagents)
293
296
  │ ├── tui.json ← registers the Subagents sidebar panel
294
297
  │ ├── tui/
295
298
  │ │ └── pc-subagents.tsx ← TUI plugin: live Subagents panel in the sidebar
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@plainconceptsplatform/agent-harness",
3
- "version": "2.0.1",
3
+ "version": "2.1.0",
4
4
  "description": "Installs the Plain Concepts Platform Harness into any codebase, and keeps it up to date. Wires OpenCode, OpenSpec, codegraph, and agentmemory into a multi-agent workflow that runs on native parallel subagents.",
5
5
  "keywords": [
6
6
  "opencode",
@@ -4,9 +4,9 @@ description: Create a custom engineer agent via persona-driven interactive desig
4
4
  license: MIT
5
5
  ---
6
6
 
7
- Create one file only: `.opencode/agents/{persona}-engineer.md`. The `pc-subagent-tiers` plugin creates tier variants (`.build.md`, `.fast.md`, `.plan.md`) at startup, so you never write those.
7
+ Create one file only: `.opencode/agents/{persona}-engineer.md`. The `pc-subagent-tiers` plugin creates tier variants (`.build.md`, `.fast.md`, `.plan.md`) at startup, so you never write those. It also generates `build.md` and `plan.md`, the only two primary agents: never create or edit those either.
8
8
 
9
- Fidelity to the [template](template.md) is the whole job. The file contains frontmatter plus one identity paragraph plus the `## Abilities` section. No other `##` headings. No expertise notes, architecture details, conventions, file maps, or workflow steps. Those belong in skills. Always set `mode: primary`. Never write `model:`. Only reference skills installed in the project's `.agents/skills/` directory.
9
+ Fidelity to the [template](template.md) is the whole job. The file contains frontmatter plus one identity paragraph plus the `## Abilities` section. No other `##` headings. No expertise notes, architecture details, conventions, file maps, or workflow steps. Those belong in skills. Always set `mode: subagent`. Engineers are reached through `task()`, never picked by a human, so a primary engineer would only clutter the agent picker. Never write `model:`. Only reference skills installed in the project's `.agents/skills/` directory.
10
10
 
11
11
  Skills first: you must complete Step 4 (present the form, discover skills for every detected signal and architecture, let the user confirm the set, then install 5-10 skills) before writing any file.
12
12
 
@@ -185,7 +185,7 @@ If either check fails, fix the file and re-validate.
185
185
 
186
186
  ## Step 7: Update fullstack-engineer.md abilities
187
187
 
188
- The `fullstack-engineer.md` is `mode: primary`, the planning session agent, not a spawned worker. Having all skills here is fine since it does planning, not parallel implementation.
188
+ `fullstack-engineer.md` is `mode: subagent`, but it is also the body that `pc-subagent-tiers` copies into `build.md` and `plan.md` on every startup. So every skill listed here reaches both primary agents, which is why it accumulates all of them: it plans and delegates rather than doing parallel implementation itself.
189
189
 
190
190
  After creating the persona engineer and validating its references, additively merge new skills into fullstack:
191
191
 
@@ -216,4 +216,4 @@ Report:
216
216
  - Skills that failed validation or install (list each with reason)
217
217
  - `fullstack-engineer.md` updated (additive, list new skills added)
218
218
  - How to use: "This agent will be spawned by the lead during `/plan-apply` for tasks matching its specialty."
219
- - "Restart opencode for the `pc-subagent-tiers` plugin to pick up the new engineer."
219
+ - "Restart opencode for the `pc-subagent-tiers` plugin to pick up the new engineer and rebuild `build.md` and `plan.md` from the updated fullstack abilities."
@@ -5,8 +5,8 @@ The agent file is exactly this structure: frontmatter plus one identity paragrap
5
5
  ```markdown
6
6
  ---
7
7
  description: <one sentence naming the persona + top 3-5 detected technologies>
8
- mode: primary
9
- color: <pick: primary|secondary|accent|error|info: avoid colors used by existing agents; warning is reserved for the lead engineer>
8
+ mode: subagent
9
+ color: <pick: primary|secondary|accent|error|info: avoid colors used by existing agents; warning is reserved for the build and plan primaries>
10
10
  permission:
11
11
  edit: allow
12
12
  bash: allow
@@ -62,7 +62,7 @@ Rules:
62
62
 
63
63
  After writing the agent file, verify:
64
64
 
65
- 1. Frontmatter exists: starts with `---`, has `description`, `mode: primary`, `color`, `permission` block.
65
+ 1. Frontmatter exists: starts with `---`, has `description`, `mode: subagent`, `color`, `permission` block.
66
66
  2. No `model:` field in the frontmatter. The `pc-subagent-tiers` plugin injects it.
67
67
  3. `## Abilities` is the only `##` heading. No other `##` sections exist in the file.
68
68
  4. One identity paragraph before `## Abilities`: 2-3 sentences max, not multiple paragraphs.
@@ -58,7 +58,7 @@ Load `@openspec-propose` skill and follow its instructions to generate proposal.
58
58
  - `description:` from the YAML frontmatter: the engineer's specialization summary
59
59
  - `## Abilities` section: the skills listed under Development, Testing, Infrastructure (e.g. `@nodejs-backend`, `@secure-nextjs-api-routes`)
60
60
  Build a map of `agent-name -> { description, abilities }`.
61
- 2. For each task, compare the task text and domain against every engineer's description AND abilities. Pick the engineer whose combined profile most closely matches. `fullstack-engineer` is `mode: primary` (the user's planning agent), not a spawned worker. If no specialist matches a task, leave the agent field blank and record the missing specialization in the proposal. An annotated OpenSpec task needs a real subagent; never substitute the lead or an obsolete generic agent name.
61
+ 2. For each task, compare the task text and domain against every engineer's description AND abilities. Pick the engineer whose combined profile most closely matches. `fullstack-engineer` is the fallback worker and the body behind `build` and `plan`; prefer a real specialist over it, and never annotate a task with `build` or `plan`, which are the user's own primaries. If no specialist matches a task, leave the agent field blank and record the missing specialization in the proposal. An annotated OpenSpec task needs a real subagent; never substitute the lead or an obsolete generic agent name.
62
62
  3. Pick a tier, derive `depends_on`, derive `touches`, and annotate each task line. Follow the [task annotation](task-annotation.md) reference for the full tier selection guide, dependency derivation, touches derivation, and annotation format with examples.
63
63
 
64
64
  ## Step 3: Show the plan and ask for confirmation (stop)
@@ -33,7 +33,7 @@ Present as a table:
33
33
  Then explain the agent selection model:
34
34
  - Primary agents appear in Tab and handle direct user interaction
35
35
  - Subagent engineers are spawned by the lead for parallel implementation waves
36
- - Specialist engineers are preferred when their domain matches the task; `fullstack-engineer` is the user's planning agent (`mode: primary`), not a spawned worker. If no specialist matches, create one with `/make-engineer`.
36
+ - Specialist engineers are preferred when their domain matches the task. `build` and `plan` are the only agents the user selects, and both run the `fullstack-engineer` body; `plan` cannot edit files. Everything else is `mode: subagent` and spawned. If no specialist matches, create one with `/make-engineer`.
37
37
 
38
38
  ## Step 3: Command reference
39
39
 
@@ -5,3 +5,5 @@ harness-managed.json
5
5
  source-roots.json
6
6
  tmp/
7
7
  *-engineer.*.md
8
+ agents/build.md
9
+ agents/plan.md
@@ -1,12 +1,45 @@
1
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.
2
+ // tier variant files with the resolved model, plus the two primary agents the
3
+ // user actually talks to. Everything generated here is gitignored and rebuilt
4
+ // every startup. Model resolution: user override > team config.
5
+ //
6
+ // Agent topology:
7
+ // build.md / plan.md mode: primary the only agents a human selects.
8
+ // Both are fullstack-engineer with a
9
+ // tier model; plan cannot edit.
10
+ // fullstack-engineer.md mode: subagent the shared body of build and plan,
11
+ // and the fallback worker.
12
+ // *-engineer.md mode: subagent specialists, spawned by task().
13
+ // *-engineer.<tier>.md mode: subagent the same specialist pinned to a tier.
14
+ //
15
+ // Overriding opencode's built-in build and plan (rather than disabling them, as
16
+ // earlier versions did) means the agent picker offers exactly two entries and
17
+ // both carry our prompt and abilities.
4
18
 
5
19
  import fs from "node:fs/promises"
6
20
  import path from "node:path"
7
21
 
8
22
  const TIERS = ["build", "fast", "plan"]
9
23
 
24
+ // The two primaries, and the tier each takes its model from. plan denies edit
25
+ // so a planning session cannot mutate the tree; bash stays allowed because the
26
+ // planning skills shell out to git and openspec to read state.
27
+ const PRIMARIES = {
28
+ build: {
29
+ tier: "build",
30
+ description: "Implement changes in this repository. Full write access, spawns specialist engineers for parallel work.",
31
+ permission: null,
32
+ },
33
+ plan: {
34
+ tier: "plan",
35
+ description: "Explore and plan without touching the tree. Read-only: proposes work for build to carry out.",
36
+ permission: { edit: "deny" },
37
+ },
38
+ }
39
+
40
+ const FULLSTACK_NAME = "fullstack-engineer"
41
+ const FULLSTACK_TEMPLATE = `${FULLSTACK_NAME}.md`
42
+
10
43
  export const PcSubagentTiers = async ({ directory }) => {
11
44
  const root = directory || process.cwd()
12
45
  const agentsDir = path.join(root, ".opencode", "agents")
@@ -35,11 +68,12 @@ export const PcSubagentTiers = async ({ directory }) => {
35
68
  try {
36
69
  const entries = await fs.readdir(agentsDir)
37
70
  return {
38
- templates: entries.filter(f => /^[\w-]+-engineer\.md$/.test(f) && f !== 'fullstack-engineer.md').map(f => f.replace(/\.md$/, "")),
71
+ templates: entries.filter(f => /^[\w-]+-engineer\.md$/.test(f) && f !== FULLSTACK_TEMPLATE).map(f => f.replace(/\.md$/, "")),
39
72
  variantFiles: entries.filter(f => /^[\w-]+-engineer\.(build|fast|plan)\.md$/.test(f)),
73
+ hasFullstack: entries.includes(FULLSTACK_TEMPLATE),
40
74
  }
41
75
  } catch {
42
- return { templates: [], variantFiles: [] }
76
+ return { templates: [], variantFiles: [], hasFullstack: false }
43
77
  }
44
78
  }
45
79
 
@@ -61,11 +95,13 @@ export const PcSubagentTiers = async ({ directory }) => {
61
95
  return `---\n${fm}\n---${templateContent.slice(fmMatch[0].length)}`
62
96
  }
63
97
 
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.
98
+ // Ensure template files are mode: subagent with no stale model: line. Only
99
+ // build and plan are primary; every engineer is reached through task(), never
100
+ // picked by a human, so a primary engineer would just clutter the picker.
101
+ // Templates must never declare a model either: models resolve per tier at
102
+ // startup and are injected into generated files only, so a stale model: (from
103
+ // a prior `stampAgentModels` run, or from when these were primary) is
104
+ // stripped and the agent falls back to the session model in opencode.jsonc.
69
105
  function normalizeTemplate(templateContent) {
70
106
  const fmMatch = templateContent.match(/^---\r?\n([\s\S]*?)\r?\n---/)
71
107
  if (!fmMatch) return templateContent
@@ -73,8 +109,8 @@ export const PcSubagentTiers = async ({ directory }) => {
73
109
  let fm = fmMatch[1]
74
110
  let changed = false
75
111
 
76
- if (!/^mode:\s*primary/m.test(fm)) {
77
- fm = /^mode:/m.test(fm) ? fm.replace(/^mode:.*$/m, 'mode: primary') : `mode: primary\n${fm}`
112
+ if (!/^mode:\s*subagent/m.test(fm)) {
113
+ fm = /^mode:/m.test(fm) ? fm.replace(/^mode:.*$/m, 'mode: subagent') : `mode: subagent\n${fm}`
78
114
  changed = true
79
115
  }
80
116
  if (/^model:/m.test(fm)) {
@@ -91,6 +127,32 @@ export const PcSubagentTiers = async ({ directory }) => {
91
127
  return templateContent.match(/^description:\s*(.+)$/m)?.[1]?.trim() ?? null
92
128
  }
93
129
 
130
+ // Build a primary from the fullstack body: same identity and the same
131
+ // ## Abilities block, with our own frontmatter on top. The body is taken
132
+ // verbatim so /make-engineer only has to maintain fullstack-engineer.md and
133
+ // both primaries inherit whatever it lists.
134
+ function buildPrimary(name, spec, fullstackContent, model) {
135
+ const body = fullstackContent.replace(/^---\r?\n[\s\S]*?\r?\n---\r?\n?/, '').trim()
136
+
137
+ const lines = [
138
+ '---',
139
+ `description: ${spec.description}`,
140
+ 'mode: primary',
141
+ ]
142
+ if (model) lines.push(`model: ${model}`)
143
+ lines.push('color: warning')
144
+ lines.push('permission:')
145
+ // plan denies edit; everything else stays allowed so the planning skills can
146
+ // still read the tree, shell out to git and openspec, and spawn engineers.
147
+ lines.push(` edit: ${spec.permission?.edit ?? 'allow'}`)
148
+ for (const key of ['bash', 'read', 'glob', 'grep', 'question', 'todowrite', 'task', 'skill']) {
149
+ lines.push(` ${key}: ${spec.permission?.[key] ?? 'allow'}`)
150
+ }
151
+ lines.push('---')
152
+
153
+ return `${lines.join('\n')}\n\n${body}\n`
154
+ }
155
+
94
156
  // Skip writing if the file already has identical content.
95
157
  async function writeIfChanged(filePath, content) {
96
158
  try {
@@ -108,7 +170,43 @@ export const PcSubagentTiers = async ({ directory }) => {
108
170
  try {
109
171
  const models = await resolveModels()
110
172
  const available = TIERS.filter(t => models[t])
111
- const { templates, variantFiles } = await scanEngineers()
173
+ const { templates, variantFiles, hasFullstack } = await scanEngineers()
174
+
175
+ // build.md and plan.md are regenerated from fullstack-engineer.md every
176
+ // startup, which is also how an edit to fullstack propagates to both.
177
+ // normalizeTemplate runs on it too, so a repo carrying the old
178
+ // mode: primary fullstack is migrated in place on first launch.
179
+ if (hasFullstack) {
180
+ const fullstackPath = path.join(agentsDir, FULLSTACK_TEMPLATE)
181
+ const rawFullstack = await fs.readFile(fullstackPath, "utf-8")
182
+ const fullstack = normalizeTemplate(rawFullstack)
183
+ if (fullstack !== rawFullstack) {
184
+ await writeIfChanged(fullstackPath, fullstack)
185
+ console.error(`[pc-subagent-tiers] Normalized ${FULLSTACK_TEMPLATE} (mode: subagent)`)
186
+ }
187
+
188
+ for (const [name, spec] of Object.entries(PRIMARIES)) {
189
+ const model = models[spec.tier]
190
+ await writeIfChanged(
191
+ path.join(agentsDir, `${name}.md`),
192
+ buildPrimary(name, spec, fullstack, model),
193
+ )
194
+ if (cfg?.agent) {
195
+ // Override opencode's built-in agent of the same name rather than
196
+ // disabling it, so the picker shows ours with our model.
197
+ cfg.agent[name] = {
198
+ ...cfg.agent[name],
199
+ mode: 'primary',
200
+ description: spec.description,
201
+ ...(model ? { model } : {}),
202
+ ...(spec.permission ? { permission: { ...cfg.agent[name]?.permission, ...spec.permission } } : {}),
203
+ }
204
+ }
205
+ }
206
+ console.error(`[pc-subagent-tiers] Wrote primaries: build (${models.build ?? 'session model'}), plan (${models.plan ?? 'session model'})`)
207
+ } else {
208
+ console.error(`[pc-subagent-tiers] ${FULLSTACK_TEMPLATE} missing: build and plan not generated`)
209
+ }
112
210
 
113
211
  const templateContents = await Promise.all(
114
212
  templates.map(async name => {
@@ -117,7 +215,7 @@ export const PcSubagentTiers = async ({ directory }) => {
117
215
  // If the template had the wrong mode or a stale model:, persist the fix to disk
118
216
  if (content !== rawContent) {
119
217
  await writeIfChanged(path.join(agentsDir, `${name}.md`), content)
120
- console.error(`[pc-subagent-tiers] Normalized ${name}.md (mode: primary, model: removed)`)
218
+ console.error(`[pc-subagent-tiers] Normalized ${name}.md (mode: subagent, model: removed)`)
121
219
  }
122
220
  return { name, content }
123
221
  })
@@ -144,12 +242,16 @@ export const PcSubagentTiers = async ({ directory }) => {
144
242
  await Promise.all(variantsToWrite.map(v => writeIfChanged(v.path, v.content)))
145
243
 
146
244
  if (cfg?.agent) {
147
- // Ensure base templates are always mode: primary in-memory
245
+ // Ensure base templates are always mode: subagent in-memory. A repo
246
+ // upgraded from an earlier version may still have primary in config.
148
247
  for (const { name } of templateContents) {
149
248
  if (cfg.agent[name]) {
150
- cfg.agent[name].mode = 'primary'
249
+ cfg.agent[name].mode = 'subagent'
151
250
  }
152
251
  }
252
+ if (cfg.agent[FULLSTACK_NAME]) {
253
+ cfg.agent[FULLSTACK_NAME].mode = 'subagent'
254
+ }
153
255
  for (const { name, tier, templateContent } of variantsToWrite) {
154
256
  const base = cfg.agent[name]
155
257
  cfg.agent[`${name}.${tier}`] = base
@@ -38,7 +38,7 @@ Command aliases: OpenSpec skills may reference `/opsx-propose`, `/opsx-apply`, `
38
38
 
39
39
  ## Engineer selection
40
40
 
41
- Inspect `.opencode/agents/*.md` before spawning. Prefer the most specialized custom engineer. `fullstack-engineer` is `mode: primary`, the planning agent, and is not a spawned worker. If no specialist matches, tell the user to create one with `/make-engineer`. Spawn only engineers present in that directory.
41
+ Inspect `.opencode/agents/*.md` before spawning. Prefer the most specialized custom engineer. `build` and `plan` are the only primaries and are never spawned; `fullstack-engineer` is the body they share and the fallback worker, so prefer a specialist over it. If no specialist matches, tell the user to create one with `/make-engineer`. Spawn only engineers present in that directory.
42
42
 
43
43
  The `pc-plan-apply` skill is authoritative for subagent waves, dependency ordering, retries, and concurrency. Read `agents.maxConcurrent` from `.opencode/harness.json` before spawning workers.
44
44
 
@@ -1,31 +1,39 @@
1
- {
2
- "$schema": "https://opencode.ai/config.json",
3
- "instructions": [
4
- "AGENTS.md"
5
- ],
6
- "plugin": [
7
- "@different-ai/opencode-browser@4.6.1",
8
- "@mohak34/opencode-notifier@0.2.8"
9
- ],
10
- "experimental": {
11
- "mcp_timeout": 300000
12
- },
13
- "compaction": {
14
- "auto": true,
15
- "prune": true,
16
- "reserved": 10000
17
- },
18
- "permission": {
19
- "question": "allow",
20
- "todowrite": "allow",
21
- "skill": "allow"
22
- },
23
- "skills": {
24
- "paths": [".agents/skills"]
25
- },
26
- "default_agent": "fullstack-engineer",
27
- "agent": {
28
- "build": { "disable": true },
29
- "plan": { "disable": true }
30
- }
31
- }
1
+ {
2
+ "$schema": "https://opencode.ai/config.json",
3
+ "instructions": [
4
+ "AGENTS.md"
5
+ ],
6
+ "plugin": [
7
+ "@different-ai/opencode-browser@4.6.1",
8
+ "@mohak34/opencode-notifier@0.2.8"
9
+ ],
10
+ "experimental": {
11
+ "mcp_timeout": 300000
12
+ },
13
+ "compaction": {
14
+ "auto": true,
15
+ "prune": true,
16
+ "reserved": 10000
17
+ },
18
+ "permission": {
19
+ "question": "allow",
20
+ "todowrite": "allow",
21
+ "skill": "allow"
22
+ },
23
+ "skills": {
24
+ "paths": [".agents/skills"]
25
+ },
26
+ // build and plan are the only primaries. The pc-subagent-tiers plugin
27
+ // regenerates .opencode/agents/{build,plan}.md from fullstack-engineer.md on
28
+ // every startup and overrides these two entries with the resolved tier model,
29
+ // so what is here is only the floor if the plugin cannot run. Every engineer
30
+ // is mode: subagent and reached through task(), never picked by a human.
31
+ "default_agent": "build",
32
+ "agent": {
33
+ "build": { "mode": "primary" },
34
+ "plan": {
35
+ "mode": "primary",
36
+ "permission": { "edit": "deny" }
37
+ }
38
+ }
39
+ }
@@ -4,7 +4,7 @@ import { success } from '../../utils/exec.js'
4
4
 
5
5
  const FULLSTACK_FILE = 'fullstack-engineer.md'
6
6
  const FULLSTACK_DESCRIPTION = 'Default engineer that accumulates skills from all created persona engineers. Use as fallback when no specialist matches: but prefer spawning a specific engineer for deterministic results.'
7
- const FULLSTACK_IDENTITY = 'You are the default engineer, mostly used by the user for architecture and planning. You are more complete but less accurate than specialized engineers, prefer spawning a specialist when one matches the task domain.'
7
+ const FULLSTACK_IDENTITY = 'You are the default engineer, and your body is what the build and plan agents run. You are more complete but less accurate than specialized engineers, so prefer spawning a specialist when one matches the task domain.'
8
8
 
9
9
  // The reminder plugin owns ability bootstrap. Remove the old prompt-level
10
10
  // directive when regenerating so it cannot duplicate the plugin reminder.
@@ -56,7 +56,9 @@ export async function generateFullstackEngineer({ cwd = process.cwd(), updateMod
56
56
  const frontmatter = [
57
57
  '---',
58
58
  `description: ${FULLSTACK_DESCRIPTION}`,
59
- 'mode: primary',
59
+ // subagent, not primary: build.md and plan.md are the primaries, and
60
+ // pc-subagent-tiers generates both from this file on every startup.
61
+ 'mode: subagent',
60
62
  'color: warning',
61
63
  'permission:',
62
64
  ' edit: allow',
@@ -33,9 +33,19 @@ export async function patchOpencodeJson(cwd = process.cwd()) {
33
33
  return { patched: false, reason: 'parse error' }
34
34
  }
35
35
 
36
- const needsAgentDisable = !(
37
- parsed?.agent?.build?.disable === true &&
38
- parsed?.agent?.plan?.disable === true
36
+ // build and plan are overridden, not disabled. Earlier versions disabled them
37
+ // and pointed default_agent at fullstack-engineer; now they are the only two
38
+ // primaries and pc-subagent-tiers regenerates their agent files from
39
+ // fullstack-engineer.md. A repo patched by an earlier version still carries
40
+ // `disable: true`, which would hide both agents, so that flag is cleared here.
41
+ const hasStaleDisable = (
42
+ parsed?.agent?.build?.disable !== undefined ||
43
+ parsed?.agent?.plan?.disable !== undefined
44
+ )
45
+ const needsAgentOverride = hasStaleDisable || !(
46
+ parsed?.agent?.build?.mode === 'primary' &&
47
+ parsed?.agent?.plan?.mode === 'primary' &&
48
+ parsed?.agent?.plan?.permission?.edit === 'deny'
39
49
  )
40
50
  const needsSkillPermission = parsed?.permission?.skill !== 'allow'
41
51
  const missingSharedPermissions = SHARED_PERMISSIONS.filter(
@@ -51,14 +61,22 @@ export async function patchOpencodeJson(cwd = process.cwd()) {
51
61
  parsed?.compaction?.prune === true
52
62
  )
53
63
 
54
- if (!needsAgentDisable && !needsSkillPermission && missingSharedPermissions.length === 0 && !needsSkillsPaths && !needsCompaction) {
64
+ if (!needsAgentOverride && !needsSkillPermission && missingSharedPermissions.length === 0 && !needsSkillsPaths && !needsCompaction) {
55
65
  return { patched: false }
56
66
  }
57
67
 
58
68
  // Apply edits sequentially so offsets stay correct
59
- if (needsAgentDisable) {
60
- text = applyModify(text, ['agent', 'build', 'disable'], true)
61
- text = applyModify(text, ['agent', 'plan', 'disable'], true)
69
+ if (needsAgentOverride) {
70
+ // undefined removes the key, clearing the disable left by earlier versions.
71
+ if (parsed?.agent?.build?.disable !== undefined) {
72
+ text = applyModify(text, ['agent', 'build', 'disable'], undefined)
73
+ }
74
+ if (parsed?.agent?.plan?.disable !== undefined) {
75
+ text = applyModify(text, ['agent', 'plan', 'disable'], undefined)
76
+ }
77
+ text = applyModify(text, ['agent', 'build', 'mode'], 'primary')
78
+ text = applyModify(text, ['agent', 'plan', 'mode'], 'primary')
79
+ text = applyModify(text, ['agent', 'plan', 'permission', 'edit'], 'deny')
62
80
  }
63
81
  if (needsSkillPermission) text = applyModify(text, ['permission', 'skill'], 'allow')
64
82
  for (const [key, value] of missingSharedPermissions) {
@@ -82,8 +100,8 @@ export async function patchOpencodeJson(cwd = process.cwd()) {
82
100
  }
83
101
 
84
102
  await fse.writeFile(opencodePath, text, 'utf-8')
85
- if (needsAgentDisable) {
86
- success('Disabled built-in build/plan agents in opencode.jsonc')
103
+ if (needsAgentOverride) {
104
+ success('Set build/plan as the primary agents in opencode.jsonc (plan is read-only)')
87
105
  }
88
106
  if (needsSkillPermission) {
89
107
  success('Allowed skill loading in opencode.jsonc')
@@ -34,6 +34,9 @@ export const IGNORED_ENTRIES = [
34
34
  MANIFEST_FILE,
35
35
  'source-roots.json',
36
36
  '*-engineer.*.md',
37
+ // Regenerated from fullstack-engineer.md by pc-subagent-tiers every startup.
38
+ 'agents/build.md',
39
+ 'agents/plan.md',
37
40
  ]
38
41
 
39
42
  export function configPath(cwd = process.cwd()) {