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.
- package/content/.opencode/plugins/session-log.js +48 -7
- package/content/AGENTS.md +2 -2
- package/package.json +1 -1
- package/src/index.js +44 -38
- package/src/steps/check-platform.js +2 -2
- package/src/steps/check-rtk.js +1 -1
- package/src/steps/choose-models.js +1 -1
- package/src/steps/choose-platform.js +1 -1
- package/src/steps/choose-skills-provider.js +1 -1
- package/src/steps/choose-source-scope.js +81 -0
- package/src/steps/clean-ai-files.js +1 -1
- package/src/steps/copy-content.js +8 -1
- package/src/steps/init-openspec.js +152 -25
- package/src/steps/install-browser.js +1 -1
|
@@ -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
|
|
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
|
|
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
|
-
|
|
104
|
-
|
|
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
|
-
|
|
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
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 {
|
|
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.
|
|
63
|
-
const
|
|
64
|
-
|
|
65
|
-
// 3.
|
|
66
|
-
const
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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
|
|
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
|
|
54
|
+
header('Step 5, Checking GitHub CLI')
|
|
55
55
|
|
|
56
56
|
const hasGh = await commandExists('gh')
|
|
57
57
|
|
package/src/steps/check-rtk.js
CHANGED
|
@@ -66,7 +66,7 @@ async function writeModelToAgent(agentFile, modelId) {
|
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
export async function chooseModels() {
|
|
69
|
-
header('Step
|
|
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
|
|
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
|
|
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
|
+
}
|
|
@@ -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
|
|
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
|
|
3
|
+
import fse from 'fs-extra'
|
|
5
4
|
import { error, header, success, warn } from '../utils/exec.js'
|
|
6
5
|
|
|
7
|
-
const
|
|
8
|
-
|
|
9
|
-
|
|
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
|
|
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
|
-
//
|
|
44
|
-
|
|
45
|
-
|
|
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
|
-
|
|
49
|
-
|
|
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
|
|
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
|
|
15
|
+
header('Step 11, Installing opencode-browser')
|
|
16
16
|
|
|
17
17
|
try {
|
|
18
18
|
const child = execa('npx', ['@different-ai/opencode-browser', 'install'], {
|