opencode-onboard 0.2.0 → 0.2.3

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.
@@ -3,7 +3,7 @@ import path from "node:path"
3
3
 
4
4
  const LOG_FILE = ".agents/session-log.json"
5
5
 
6
- // Per-session state: editCount and skills loaded
6
+ // Per-session state: editCount and loaded skills
7
7
  const sessionState = new Map()
8
8
 
9
9
  function ts() {
@@ -32,6 +32,29 @@ function resolveAgentName(session) {
32
32
  return "lead"
33
33
  }
34
34
 
35
+ function addSkillToState(state, skillName) {
36
+ if (!skillName || !state) return false
37
+ if (!state.skills) state.skills = new Set()
38
+ if (state.skills.has(skillName)) return false
39
+ state.skills.add(skillName)
40
+ return true
41
+ }
42
+
43
+ function buildTeamSkillsSummary() {
44
+ const byAgent = {}
45
+ for (const state of sessionState.values()) {
46
+ if (!state?.agentName) continue
47
+ if (!byAgent[state.agentName]) byAgent[state.agentName] = new Set()
48
+ for (const skill of state.skills ?? []) byAgent[state.agentName].add(skill)
49
+ }
50
+
51
+ const summary = {}
52
+ for (const [agent, skills] of Object.entries(byAgent)) {
53
+ summary[agent] = Array.from(skills).sort()
54
+ }
55
+ return summary
56
+ }
57
+
35
58
  // Maps ensemble tool name → function that extracts the log entry fields from args
36
59
  const ENSEMBLE_TOOL_HANDLERS = {
37
60
  team_create: (args) => ({ action: "team-created", team: args.name }),
@@ -60,7 +83,7 @@ export const SessionLogPlugin = async ({ client, directory }) => {
60
83
  const session = res?.data
61
84
  const agentName = resolveAgentName(session)
62
85
 
63
- sessionState.set(sessionId, { agentName, editCount: 0, skills: [] })
86
+ sessionState.set(sessionId, { agentName, editCount: 0, skills: new Set() })
64
87
  appendEntry(directory, { ts: ts(), agent: agentName, action: "started", sessionId })
65
88
  }
66
89
 
@@ -77,7 +100,8 @@ export const SessionLogPlugin = async ({ client, directory }) => {
77
100
  const state = sessionState.get(sessionId)
78
101
  if (!state) return
79
102
 
80
- const { agentName, editCount, skills } = state
103
+ const { agentName, editCount } = state
104
+ const skills = Array.from(state.skills ?? []).sort()
81
105
  appendEntry(directory, { ts: ts(), agent: agentName, action: "completed", filesEdited: editCount, skills })
82
106
  sessionState.delete(sessionId)
83
107
  }
@@ -94,14 +118,26 @@ export const SessionLogPlugin = async ({ client, directory }) => {
94
118
 
95
119
  const tool = input?.tool
96
120
 
97
- // Track skill loads
121
+ // Track skill loads via skill tool (primary)
122
+ if (tool === "skill") {
123
+ const skillName = input?.args?.name
124
+ const added = addSkillToState(state, skillName)
125
+ if (added) {
126
+ appendEntry(directory, { ts: ts(), agent: state.agentName, action: "skill-loaded", skill: skillName, source: "skill-tool" })
127
+ }
128
+ return
129
+ }
130
+
131
+ // Track skill loads via reading SKILL.md (fallback)
98
132
  if (tool === "read") {
99
133
  const filePath = input?.args?.filePath ?? ""
100
134
  const match = filePath.match(/[/\\]skills[/\\]([^/\\]+)[/\\]SKILL\.md$/i)
101
135
  if (match) {
102
136
  const skillName = match[1]
103
- if (!state.skills.includes(skillName)) state.skills.push(skillName)
104
- appendEntry(directory, { ts: ts(), agent: state.agentName, action: "skill-loaded", skill: skillName })
137
+ const added = addSkillToState(state, skillName)
138
+ if (added) {
139
+ appendEntry(directory, { ts: ts(), agent: state.agentName, action: "skill-loaded", skill: skillName, source: "read-skill-file" })
140
+ }
105
141
  }
106
142
  return
107
143
  }
@@ -112,7 +148,12 @@ export const SessionLogPlugin = async ({ client, directory }) => {
112
148
  const ensembleHandler = ENSEMBLE_TOOL_HANDLERS[tool]
113
149
  if (!ensembleHandler) return
114
150
 
115
- appendEntry(directory, { ts: ts(), agent: state.agentName, ...ensembleHandler(args) })
151
+ const entry = { ts: ts(), agent: state.agentName, ...ensembleHandler(args) }
152
+ appendEntry(directory, entry)
153
+
154
+ if (tool === "team_cleanup") {
155
+ appendEntry(directory, { ts: ts(), agent: state.agentName, action: "team-skills-summary", byAgent: buildTeamSkillsSummary() })
156
+ }
116
157
  } catch (_) {}
117
158
  },
118
159
  }
package/content/AGENTS.md CHANGED
@@ -40,7 +40,7 @@ openspec archive "project-history"
40
40
  1. **Read `DESIGN.md` now** using a file read tool. The file contains a prompt with instructions and an output format.
41
41
  2. **Store the full prompt text** in your context.
42
42
  3. **Overwrite `DESIGN.md` with an empty string** (zero bytes). Do this before generating any content.
43
- 4. **Analyze the actual codebase**: read CSS files, Tailwind config, component files, token definitions. Do not rely on prior knowledge, read the files.
43
+ 4. **Analyze the actual codebase**: use `.agents/source-roots.json` as source roots when present, then read CSS files, Tailwind config, component files, token definitions. Do not rely on prior knowledge, read the files.
44
44
  5. **Write the result into `DESIGN.md`** following exactly the format and sections described in the stored prompt.
45
45
 
46
46
  The output must be a real, populated `DESIGN.md` based on what you found in the codebase, not from memory or assumptions.
@@ -54,7 +54,7 @@ The output must be a real, populated `DESIGN.md` based on what you found in the
54
54
  1. **Read `ARCHITECTURE.md` now** using a file read tool. The file contains a prompt with instructions and an output format.
55
55
  2. **Store the full prompt text** in your context.
56
56
  3. **Overwrite `ARCHITECTURE.md` with an empty string** (zero bytes). Do this before generating any content.
57
- 4. **Analyze the actual codebase**: read folder structure, config files, route definitions, data models, integration points. Do not rely on prior knowledge, read the files.
57
+ 4. **Analyze the actual codebase**: use `.agents/source-roots.json` as source roots when present, then read folder structure, config files, route definitions, data models, integration points. Do not rely on prior knowledge, read the files.
58
58
  5. **Write the result into `ARCHITECTURE.md`** following exactly the format and sections described in the stored prompt.
59
59
 
60
60
  The output must be a real, populated `ARCHITECTURE.md` based on what you found in the codebase, covering all sections the prompt describes.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-onboard",
3
- "version": "0.2.0",
3
+ "version": "0.2.3",
4
4
  "description": "Prepare any brownfield codebase for AI agent workflows using OpenCode, OpenSpec, and ensemble orchestration.",
5
5
  "keywords": [
6
6
  "opencode",
package/src/index.js CHANGED
@@ -5,8 +5,9 @@ import { checkEnv } from './steps/check-env.js'
5
5
  import { checkPlatform } from './steps/check-platform.js'
6
6
  import { checkRtk } from './steps/check-rtk.js'
7
7
  import { chooseModels } from './steps/choose-models.js'
8
- import { choosePlatform } from './steps/choose-platform.js'
9
- import { chooseSkillsProvider } from './steps/choose-skills-provider.js'
8
+ import { choosePlatform } from './steps/choose-platform.js'
9
+ import { chooseSourceScope } from './steps/choose-source-scope.js'
10
+ import { chooseSkillsProvider } from './steps/choose-skills-provider.js'
10
11
  import { cleanAiFiles } from './steps/clean-ai-files.js'
11
12
  import { copyContentStep } from './steps/copy-content.js'
12
13
  import { initOpenspec } from './steps/init-openspec.js'
@@ -59,35 +60,39 @@ try {
59
60
  // 1. Check Node + pnpm
60
61
  await checkEnv()
61
62
 
62
- // 2. Clean existing AI config files, detect preserved state
63
- const ctx = await cleanAiFiles()
64
-
65
- // 3. Choose platform
66
- const platform = await choosePlatform()
67
-
68
- // 4. Check platform CLI (az or gh)
69
- await checkPlatform(platform)
70
-
71
- // 5. Copy content
72
- await copyContentStep(platform, ctx)
73
-
74
- // 5b. Patch AGENTS.md to skip steps for already-existing files
75
- await patchAgentsMd(ctx)
76
-
77
- // 6. Init OpenSpec
78
- await initOpenspec()
79
-
80
- // 7. Install skills
81
- await chooseSkillsProvider()
82
-
83
- // 8. Choose models
84
- await chooseModels()
85
-
86
- // 9. Check RTK
87
- await checkRtk()
88
-
89
- // 10. Install opencode-browser
90
- await installBrowser()
63
+ // 2. Choose source code scope for init analysis
64
+ const scope = await chooseSourceScope()
65
+
66
+ // 3. Clean existing AI config files, detect preserved state
67
+ const preserve = await cleanAiFiles()
68
+ const ctx = { ...preserve, ...scope }
69
+
70
+ // 4. Choose platform
71
+ const platform = await choosePlatform()
72
+
73
+ // 5. Check platform CLI (az or gh)
74
+ await checkPlatform(platform)
75
+
76
+ // 6. Copy content
77
+ await copyContentStep(platform, ctx)
78
+
79
+ // 6b. Patch AGENTS.md to skip steps for already-existing files
80
+ await patchAgentsMd(ctx)
81
+
82
+ // 7. Init OpenSpec
83
+ await initOpenspec()
84
+
85
+ // 8. Install skills
86
+ await chooseSkillsProvider()
87
+
88
+ // 9. Choose models
89
+ await chooseModels()
90
+
91
+ // 10. Check RTK
92
+ await checkRtk()
93
+
94
+ // 11. Install opencode-browser
95
+ await installBrowser()
91
96
 
92
97
  // Done
93
98
  const toGenerate = [
@@ -103,13 +108,14 @@ try {
103
108
  console.log(' Open this project in OpenCode and type:')
104
109
  console.log(chalk.bold(' "init"'))
105
110
  console.log()
106
- if (toGenerate.length > 0) {
107
- console.log(` OpenCode will generate ${toGenerate.join(' and ')}`)
108
- console.log(' from your actual codebase, then activate the agent team.')
109
- } else {
110
- console.log(' OpenCode will activate the agent team.')
111
- }
112
- console.log()
111
+ if (toGenerate.length > 0) {
112
+ console.log(` OpenCode will generate ${toGenerate.join(' and ')}`)
113
+ console.log(' from your actual codebase, then activate the agent team.')
114
+ } else {
115
+ console.log(' OpenCode will activate the agent team.')
116
+ }
117
+ console.log(` Source scope: ${ctx.sourceMode === 'parent-selected' ? ctx.sourceRoots.map(p => `../${p.split(/[/\\]/).pop()}`).join(', ') : 'current folder'}`)
118
+ console.log()
113
119
  } catch (err) {
114
120
  if (err.name === 'ExitPromptError') {
115
121
  console.log()
@@ -10,7 +10,7 @@ export async function checkPlatform(platform) {
10
10
  }
11
11
 
12
12
  async function checkAzure() {
13
- header('Step 4, Checking Azure DevOps CLI')
13
+ header('Step 5, Checking Azure DevOps CLI')
14
14
 
15
15
  // Check az is installed
16
16
  const hasAz = await commandExists('az')
@@ -51,7 +51,7 @@ async function checkAzure() {
51
51
  }
52
52
 
53
53
  async function checkGithub() {
54
- header('Step 4, Checking GitHub CLI')
54
+ header('Step 5, Checking GitHub CLI')
55
55
 
56
56
  const hasGh = await commandExists('gh')
57
57
 
@@ -1,7 +1,7 @@
1
1
  import { code, commandExists, header, info, success, warn } from '../utils/exec.js'
2
2
 
3
3
  export async function checkRtk() {
4
- header('Step 9, Checking rtk')
4
+ header('Step 10, Checking rtk')
5
5
 
6
6
  const available = await commandExists('rtk')
7
7
 
@@ -66,7 +66,7 @@ async function writeModelToAgent(agentFile, modelId) {
66
66
  }
67
67
 
68
68
  export async function chooseModels() {
69
- header('Step 8, Choose models')
69
+ header('Step 9, Choose models')
70
70
 
71
71
  info('Fetching available models from models.dev...')
72
72
  const { models: rawModels, source } = await fetchModels()
@@ -10,7 +10,7 @@ const PLATFORMS_PRESET_PATH = path.resolve(__dirname, '../presets/platforms.json
10
10
  const platformsPreset = await fse.readJson(PLATFORMS_PRESET_PATH)
11
11
 
12
12
  export async function choosePlatform() {
13
- header('Step 3, Version control platform')
13
+ header('Step 4, Version control platform')
14
14
 
15
15
  const platform = await select({
16
16
  message: 'Which platform are you using?',
@@ -28,7 +28,7 @@ async function installObSkills() {
28
28
  }
29
29
 
30
30
  export async function chooseSkillsProvider() {
31
- header('Step 7, Installing skills')
31
+ header('Step 8, Installing skills')
32
32
 
33
33
  // ob-skills are always installed, mandatory
34
34
  info('Installing built-in ob-skills...')
@@ -0,0 +1,81 @@
1
+ import { checkbox, select } from '@inquirer/prompts'
2
+ import fse from 'fs-extra'
3
+ import path from 'path'
4
+ import { header, info, success, warn } from '../utils/exec.js'
5
+
6
+ async function listParentFolders(cwd) {
7
+ const parent = path.resolve(cwd, '..')
8
+ const entries = await fse.readdir(parent)
9
+ const dirs = []
10
+
11
+ for (const name of entries) {
12
+ if (name.startsWith('.')) continue
13
+ const abs = path.join(parent, name)
14
+ try {
15
+ const stat = await fse.stat(abs)
16
+ if (!stat.isDirectory()) continue
17
+ if (path.resolve(abs) === path.resolve(cwd)) continue
18
+ dirs.push({ name, abs })
19
+ } catch {
20
+ // ignore invalid entries
21
+ }
22
+ }
23
+
24
+ dirs.sort((a, b) => a.name.localeCompare(b.name))
25
+ return dirs
26
+ }
27
+
28
+ export async function chooseSourceScope() {
29
+ header('Step 2, Source code scope')
30
+
31
+ const cwd = process.cwd()
32
+ info('Choose where agents should read source code from during init analysis.')
33
+
34
+ const mode = await select({
35
+ message: 'Source code location:',
36
+ default: 'current',
37
+ choices: [
38
+ {
39
+ name: 'Current folder (default)',
40
+ value: 'current',
41
+ description: 'Use this repository only',
42
+ },
43
+ {
44
+ name: 'Select folders in parent (../)',
45
+ value: 'parent',
46
+ description: 'Use when this repo only contains agent config',
47
+ },
48
+ ],
49
+ })
50
+
51
+ if (mode === 'current') {
52
+ success(`Source scope: ${cwd}`)
53
+ return { sourceMode: 'current', sourceRoots: [cwd] }
54
+ }
55
+
56
+ const parentFolders = await listParentFolders(cwd)
57
+ if (parentFolders.length === 0) {
58
+ warn('No sibling folders found in parent directory. Falling back to current folder.')
59
+ success(`Source scope: ${cwd}`)
60
+ return { sourceMode: 'current', sourceRoots: [cwd] }
61
+ }
62
+
63
+ const selected = await checkbox({
64
+ message: 'Select source folders from parent directory:',
65
+ choices: parentFolders.map(d => ({
66
+ name: `../${d.name}`,
67
+ value: d.abs,
68
+ checked: false,
69
+ })),
70
+ required: true,
71
+ })
72
+
73
+ if (!selected || selected.length === 0) {
74
+ warn('No folders selected. Falling back to current folder.')
75
+ success(`Source scope: ${cwd}`)
76
+ return { sourceMode: 'current', sourceRoots: [cwd] }
77
+ }
78
+
79
+ success(`Source scope: ${selected.map(p => path.basename(p)).join(', ')}`)
80
+ return { sourceMode: 'parent-selected', sourceRoots: selected }
81
+ }
@@ -53,7 +53,7 @@ async function hasOpenspecHistory(cwd) {
53
53
  }
54
54
 
55
55
  export async function cleanAiFiles() {
56
- header('Step 2, Existing AI config files')
56
+ header('Step 3, Existing AI config files')
57
57
 
58
58
  const cwd = process.cwd()
59
59
 
@@ -1,5 +1,6 @@
1
1
  import path from 'path'
2
2
  import { fileURLToPath } from 'url'
3
+ import fse from 'fs-extra'
3
4
  import { copyContent } from '../utils/copy.js'
4
5
  import { error, header, success } from '../utils/exec.js'
5
6
 
@@ -7,12 +8,18 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url))
7
8
  const CONTENT_DIR = path.resolve(__dirname, '../../content')
8
9
 
9
10
  export async function copyContentStep(platform, ctx = {}) {
10
- header('Step 5, Copying opencode-onboard files')
11
+ header('Step 6, Copying opencode-onboard files')
11
12
 
12
13
  const dest = process.cwd()
13
14
 
14
15
  try {
15
16
  await copyContent(CONTENT_DIR, dest, platform, ctx)
17
+ const rootsFile = path.join(dest, '.agents', 'source-roots.json')
18
+ await fse.ensureDir(path.dirname(rootsFile))
19
+ await fse.writeJson(rootsFile, {
20
+ mode: ctx.sourceMode || 'current',
21
+ roots: ctx.sourceRoots || [dest],
22
+ }, { spaces: 2 })
16
23
  success('Files copied to project root')
17
24
  } catch (err) {
18
25
  error(`Failed to copy content: ${err.message}`)
@@ -1,28 +1,152 @@
1
1
  import { execa } from 'execa'
2
- import fse from 'fs-extra'
3
2
  import path from 'node:path'
4
- import { fileURLToPath } from 'node:url'
3
+ import fse from 'fs-extra'
5
4
  import { error, header, success, warn } from '../utils/exec.js'
6
5
 
7
- const __dirname = path.dirname(fileURLToPath(import.meta.url))
8
-
9
- // Our owned apply command and skill, stored in the package content folder.
10
- // After openspec init generates its versions, we delete them and copy ours in.
11
- const OUR_CONTENT_DIR = path.resolve(__dirname, '../../content')
12
-
13
- const APPLY_OVERRIDES = [
14
- {
15
- src: path.join(OUR_CONTENT_DIR, '.opencode', 'commands', 'opsx-apply.md'),
16
- dest: path.join('.opencode', 'commands', 'opsx-apply.md'),
17
- },
18
- {
19
- src: path.join(OUR_CONTENT_DIR, '.opencode', 'skills', 'openspec-apply-change', 'SKILL.md'),
20
- dest: path.join('.opencode', 'skills', 'openspec-apply-change', 'SKILL.md'),
21
- },
6
+ const APPLY_TARGETS = [
7
+ path.join('.opencode', 'commands', 'opsx-apply.md'),
8
+ path.join('.opencode', 'skills', 'openspec-apply-change', 'SKILL.md'),
22
9
  ]
23
10
 
11
+ const ENSEMBLE_SECTION = `6. **Implement via ensemble team**
12
+
13
+ NEVER implement tasks directly. Always delegate to specialists via ensemble.
14
+ Do NOT touch any source files before the team is running, not even a single edit.
15
+
16
+ Steps MUST be followed in order. Do not skip any step.
17
+
18
+ **Step 6a.** Create feature branch if not already on one: \`feature/{id}-{slug}\`
19
+
20
+ **Step 6b.** Create the team:
21
+ \`\`\`
22
+ team_create "<change-name>-<random 4 digit number>"
23
+ \`\`\`
24
+ Announce: "Team running. Monitor at http://localhost:4747/"
25
+
26
+ **Step 6c.** Add ALL tasks to the shared board BEFORE spawning anyone.
27
+ Schema: { content: string, priority: "high"|"medium"|"low", depends_on?: string[] }
28
+ Use depends_on to block tasks that require other tasks first, pass the IDs returned by team_tasks_add.
29
+ \`\`\`
30
+ team_tasks_add tasks:[
31
+ { content: "1.1 <exact task text from tasks.md>", priority: "high" },
32
+ { content: "1.2 <exact task text>", priority: "high" },
33
+ { content: "3.1 <task that needs 1.x done first>", priority: "medium", depends_on: ["<id-of-1.1>"] },
34
+ ...every task, one entry each...
35
+ ]
36
+ \`\`\`
37
+ Save the task IDs returned. Pass them to agents in step 6d.
38
+ DO NOT call team_claim yourself, only agents claim tasks.
39
+ DO NOT proceed to 6d until team_tasks_add succeeds.
40
+
41
+ **Step 6d.** Spawn all needed specialists, then kick them off in parallel.
42
+
43
+ Each team_spawn MUST include the agent field (required, causes NOT NULL error if omitted).
44
+
45
+ The spawn prompt must contain exactly:
46
+ 1. Their name and role on this team
47
+ 2. Which tasks are theirs, list the task IDs and content from the board
48
+ 3. Key context they need (summarized from context files, do NOT tell them to read files themselves)
49
+ 4. The 6 OpenCode tools they have available (these are OpenCode tools, NOT shell commands, call them directly as tools, never via bash):
50
+ team_claim, team_tasks_complete, team_tasks_list, team_tasks_add, team_message, team_broadcast
51
+ 5. How to proceed: call team_claim tool with the task_id to claim a task before starting it, call team_tasks_complete tool after finishing it, repeat until all their tasks are done, then call team_message tool to notify lead with results or blockers
52
+
53
+ Keep spawn prompts under 500 tokens. Do not describe team internals or how ensemble works.
54
+ Only spawn agents whose tasks are actually needed by this change. Skip agents with no tasks.
55
+
56
+ First spawn all agents (wait for each team_spawn to confirm before the next):
57
+ \`\`\`
58
+ team_spawn name:"back" agent:"back-engineer" prompt:"..."
59
+ (wait for result)
60
+ team_spawn name:"front" agent:"front-engineer" prompt:"..."
61
+ (wait for result)
62
+ team_spawn name:"infra" agent:"infra-engineer" prompt:"..."
63
+ (wait for result)
64
+ \`\`\`
65
+
66
+ Then immediately send each spawned agent a start message to kick them off:
67
+ \`\`\`
68
+ team_message to:"back" text:"Start now. Claim your first task with team_claim and begin implementing."
69
+ team_message to:"front" text:"Start now. Claim your first task with team_claim and begin implementing."
70
+ team_message to:"infra" text:"Start now. Claim your first task with team_claim and begin implementing."
71
+ \`\`\`
72
+
73
+ **Step 6e.** After sending start messages, tell the user what is running, then STOP and wait.
74
+ Do NOT call team_results, team_status, or team_broadcast in a loop.
75
+ Teammates will message you when done or blocked. Wait for those messages.
76
+
77
+ **Step 6f.** When a teammate messages back, you receive a ping only, the full content is NOT in the notification.
78
+ Call team_results to read the full message and mark it read. Then for each teammate: team_shutdown -> team_merge.
79
+ If team_merge blocks ("overlapping local changes"), commit or stash your local changes first, then retry.
80
+ Fix any other blockers reported.
81
+
82
+ 7. **Quality check**
83
+
84
+ Spawn quality engineer with worktree:false (read-only, no file edits):
85
+ \`\`\`
86
+ team_spawn name:"quality" agent:"quality-engineer" worktree:false prompt:"<task list, context summary, run tests + build + lint + verify acceptance criteria, send results to lead when done>"
87
+ \`\`\`
88
+ Wait for message -> team_results -> fix blockers -> team_shutdown (no team_merge needed, worktree:false)
89
+
90
+ 8. **Mark tasks complete in openspec**
91
+
92
+ Update tasks.md: \`- [ ]\` -> \`- [x]\` for each completed task.
93
+ Run \`rtk openspec status --change "<name>" --json\` to confirm.
94
+
95
+ 9. **Show status, then cleanup**
96
+
97
+ Display:
98
+ - Tasks completed this session
99
+ - Overall progress: "N/M tasks complete"
100
+ - If all done: suggest archive with \`/opsx-archive\`
101
+ - If paused: explain why and wait for guidance
102
+
103
+ Then run \`team_cleanup\`.
104
+
105
+ **Guardrails**
106
+ - NEVER skip or reorder steps 6a-6f
107
+ - NEVER implement tasks directly. Always use team_create + team_spawn, no exceptions
108
+ - NEVER touch source files before team_create is called, not even one edit
109
+ - NEVER call team_spawn without the agent field, it is required and will fail without it
110
+ - NEVER call team_spawn before team_tasks_add, tasks must exist before agents are spawned
111
+ - NEVER poll team_results or team_status in a loop, wait for teammates to message you
112
+ - NEVER call team_claim or team_tasks_complete as lead, only agents call these tools
113
+ - ALWAYS pass the task IDs returned by team_tasks_add to each agent's spawn prompt
114
+ - NEVER edit files between team_spawn and team_merge, team_merge blocks on overlapping local changes
115
+ - ALWAYS add every task to the board with team_tasks_add before spawning
116
+ - ALWAYS spawn agents sequentially (wait for each team_spawn result before the next), then send start messages to all of them together
117
+ - ALWAYS instruct agents to call team_claim before each task and team_tasks_complete after
118
+ - If teammates are stuck, use team_message to resend tasks, then wait, never implement directly
119
+ - Mark tasks complete in openspec AFTER specialists finish, not before
120
+ - Pause on errors, blockers, or unclear requirements. Do not guess
121
+ - Use contextFiles from CLI output, do not assume specific file paths
122
+ - Use \`rtk\` wrapper for ALL CLI commands. Never run openspec, git, gh, or az directly
123
+ `
124
+
125
+ const STEP_6_START = /^6\.\s+\*\*Implement\b/im
126
+ const FLUID_SECTION = /^\*\*Fluid Workflow Integration\*\*/im
127
+
128
+ async function patchApplyFile(filePath) {
129
+ if (!await fse.pathExists(filePath)) return { ok: false, reason: 'missing-file' }
130
+
131
+ const original = await fse.readFile(filePath, 'utf-8')
132
+ const startMatch = original.match(STEP_6_START)
133
+ if (!startMatch || startMatch.index === undefined) return { ok: false, reason: 'missing-step-6' }
134
+
135
+ const before = original.slice(0, startMatch.index).replace(/\s*$/, '')
136
+ const fromStep6 = original.slice(startMatch.index)
137
+ const fluidMatch = fromStep6.match(FLUID_SECTION)
138
+
139
+ const after = fluidMatch && fluidMatch.index !== undefined
140
+ ? `\n\n${fromStep6.slice(fluidMatch.index).replace(/^\s*/, '')}`
141
+ : ''
142
+
143
+ const patched = `${before}\n\n${ENSEMBLE_SECTION}${after}`
144
+ await fse.writeFile(filePath, patched, 'utf-8')
145
+ return { ok: true }
146
+ }
147
+
24
148
  export async function initOpenspec() {
25
- header('Step 6, Initializing OpenSpec')
149
+ header('Step 7, Initializing OpenSpec')
26
150
 
27
151
  try {
28
152
  const result = await execa('npx', ['@fission-ai/openspec', 'init', '--tools', 'opencode', '--force'], {
@@ -40,15 +164,18 @@ export async function initOpenspec() {
40
164
  error(`Failed to run openspec init: ${err.message}`)
41
165
  }
42
166
 
43
- // Replace the openspec-generated apply command and skill with our ensemble-native versions.
44
- // The generated files implement tasks directly (solo agent). Ours delegate to the ensemble team.
45
- for (const { src, dest } of APPLY_OVERRIDES) {
46
- const destAbs = path.join(process.cwd(), dest)
167
+ // Keep openspec defaults for selection/status/context steps, replace only implementation + guardrails.
168
+ for (const rel of APPLY_TARGETS) {
169
+ const abs = path.join(process.cwd(), rel)
47
170
  try {
48
- await fse.copy(src, destAbs, { overwrite: true })
49
- success(`Installed ensemble apply → ${dest}`)
171
+ const res = await patchApplyFile(abs)
172
+ if (res.ok) {
173
+ success(`Patched ensemble implementation section in ${rel}`)
174
+ } else {
175
+ warn(`Could not patch ${rel} (${res.reason})`)
176
+ }
50
177
  } catch (err) {
51
- warn(`Could not install ${dest}: ${err.message}`)
178
+ warn(`Could not patch ${rel}: ${err.message}`)
52
179
  }
53
180
  }
54
181
  }
@@ -12,7 +12,7 @@ const AUTO_ANSWERS = [
12
12
  ]
13
13
 
14
14
  export async function installBrowser() {
15
- header('Step 10, Installing opencode-browser')
15
+ header('Step 11, Installing opencode-browser')
16
16
 
17
17
  try {
18
18
  const child = execa('npx', ['@different-ai/opencode-browser', 'install'], {