opencode-onboard 0.1.13 → 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/.agents/agents/back-engineer.md +10 -0
- package/content/.agents/agents/front-engineer.md +10 -0
- package/content/.agents/agents/quality-engineer.md +9 -0
- package/content/.opencode/commands/opsx-apply.md +170 -0
- package/content/.opencode/plugins/session-log.js +75 -6
- package/content/.opencode/skills/openspec-apply-change/SKILL.md +176 -0
- package/content/AGENTS.md +39 -25
- package/package.json +1 -1
- package/src/index.js +53 -33
- package/src/steps/check-platform.js +2 -2
- package/src/steps/check-rtk.js +1 -1
- package/src/steps/choose-models.js +8 -7
- 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 +64 -30
- package/src/steps/copy-content.js +10 -3
- package/src/steps/init-openspec.js +84 -67
- package/src/steps/install-browser.js +1 -1
- package/src/steps/patch-agents-md.js +85 -0
- package/src/utils/copy.js +20 -6
|
@@ -1,9 +1,14 @@
|
|
|
1
1
|
import { execa } from 'execa'
|
|
2
|
-
import fs from 'node:fs'
|
|
3
2
|
import path from 'node:path'
|
|
3
|
+
import fse from 'fs-extra'
|
|
4
4
|
import { error, header, success, warn } from '../utils/exec.js'
|
|
5
5
|
|
|
6
|
-
const
|
|
6
|
+
const APPLY_TARGETS = [
|
|
7
|
+
path.join('.opencode', 'commands', 'opsx-apply.md'),
|
|
8
|
+
path.join('.opencode', 'skills', 'openspec-apply-change', 'SKILL.md'),
|
|
9
|
+
]
|
|
10
|
+
|
|
11
|
+
const ENSEMBLE_SECTION = `6. **Implement via ensemble team**
|
|
7
12
|
|
|
8
13
|
NEVER implement tasks directly. Always delegate to specialists via ensemble.
|
|
9
14
|
Do NOT touch any source files before the team is running, not even a single edit.
|
|
@@ -12,31 +17,43 @@ const ENSEMBLE_PATCH = `6. **Implement via ensemble team**
|
|
|
12
17
|
|
|
13
18
|
**Step 6a.** Create feature branch if not already on one: \`feature/{id}-{slug}\`
|
|
14
19
|
|
|
15
|
-
**Step 6b.**
|
|
20
|
+
**Step 6b.** Create the team:
|
|
16
21
|
\`\`\`
|
|
17
|
-
|
|
18
|
-
team_create "<change-name>"
|
|
22
|
+
team_create "<change-name>-<random 4 digit number>"
|
|
19
23
|
\`\`\`
|
|
20
|
-
"not in a team" error from team_cleanup is expected, ignore it.
|
|
21
24
|
Announce: "Team running. Monitor at http://localhost:4747/"
|
|
22
25
|
|
|
23
26
|
**Step 6c.** Add ALL tasks to the shared board BEFORE spawning anyone.
|
|
24
|
-
Schema: { content: string, priority: "high"|"medium"|"low"
|
|
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.
|
|
25
29
|
\`\`\`
|
|
26
30
|
team_tasks_add tasks:[
|
|
27
31
|
{ content: "1.1 <exact task text from tasks.md>", priority: "high" },
|
|
28
32
|
{ content: "1.2 <exact task text>", priority: "high" },
|
|
29
|
-
|
|
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...
|
|
30
35
|
]
|
|
31
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.
|
|
32
39
|
DO NOT proceed to 6d until team_tasks_add succeeds.
|
|
33
|
-
Do NOT call team_claim or interact with tasks further — the board is for visibility only.
|
|
34
40
|
|
|
35
|
-
**Step 6d.** Spawn
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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):
|
|
40
57
|
\`\`\`
|
|
41
58
|
team_spawn name:"back" agent:"back-engineer" prompt:"..."
|
|
42
59
|
(wait for result)
|
|
@@ -46,26 +63,33 @@ const ENSEMBLE_PATCH = `6. **Implement via ensemble team**
|
|
|
46
63
|
(wait for result)
|
|
47
64
|
\`\`\`
|
|
48
65
|
|
|
49
|
-
|
|
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.
|
|
50
74
|
Do NOT call team_results, team_status, or team_broadcast in a loop.
|
|
51
75
|
Teammates will message you when done or blocked. Wait for those messages.
|
|
52
76
|
|
|
53
|
-
**Step 6f.** When
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
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.
|
|
57
81
|
|
|
58
82
|
7. **Quality check**
|
|
59
83
|
|
|
60
84
|
Spawn quality engineer with worktree:false (read-only, no file edits):
|
|
61
85
|
\`\`\`
|
|
62
|
-
team_spawn name:"quality" agent:"quality-engineer" worktree:false prompt:"<task list, context
|
|
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>"
|
|
63
87
|
\`\`\`
|
|
64
|
-
Wait for message
|
|
88
|
+
Wait for message -> team_results -> fix blockers -> team_shutdown (no team_merge needed, worktree:false)
|
|
65
89
|
|
|
66
90
|
8. **Mark tasks complete in openspec**
|
|
67
91
|
|
|
68
|
-
Update tasks.md: \`- [ ]\`
|
|
92
|
+
Update tasks.md: \`- [ ]\` -> \`- [x]\` for each completed task.
|
|
69
93
|
Run \`rtk openspec status --change "<name>" --json\` to confirm.
|
|
70
94
|
|
|
71
95
|
9. **Show status, then cleanup**
|
|
@@ -82,53 +106,47 @@ const ENSEMBLE_PATCH = `6. **Implement via ensemble team**
|
|
|
82
106
|
- NEVER skip or reorder steps 6a-6f
|
|
83
107
|
- NEVER implement tasks directly. Always use team_create + team_spawn, no exceptions
|
|
84
108
|
- NEVER touch source files before team_create is called, not even one edit
|
|
85
|
-
- NEVER call team_spawn without the agent field
|
|
86
|
-
- NEVER call team_spawn before team_tasks_add
|
|
87
|
-
- NEVER poll team_results or team_status in a loop
|
|
88
|
-
- NEVER call team_claim or team_tasks_complete
|
|
89
|
-
-
|
|
90
|
-
-
|
|
91
|
-
- ALWAYS add every task
|
|
92
|
-
- ALWAYS spawn
|
|
93
|
-
-
|
|
94
|
-
- If teammates are stuck, use team_message to resend tasks, then wait
|
|
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
|
|
95
119
|
- Mark tasks complete in openspec AFTER specialists finish, not before
|
|
96
120
|
- Pause on errors, blockers, or unclear requirements. Do not guess
|
|
97
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
|
|
98
123
|
`
|
|
99
124
|
|
|
100
|
-
|
|
101
|
-
const
|
|
102
|
-
/^#{1,3}\s+\d+\..*(implement|loop until|make the code changes|for each (pending )?task)/im,
|
|
103
|
-
/\*\*Implement tasks.*loop until/im,
|
|
104
|
-
/^6\.\s+\*\*Implement tasks/im,
|
|
105
|
-
]
|
|
125
|
+
const STEP_6_START = /^6\.\s+\*\*Implement\b/im
|
|
126
|
+
const FLUID_SECTION = /^\*\*Fluid Workflow Integration\*\*/im
|
|
106
127
|
|
|
107
|
-
function
|
|
108
|
-
if (!
|
|
128
|
+
async function patchApplyFile(filePath) {
|
|
129
|
+
if (!await fse.pathExists(filePath)) return { ok: false, reason: 'missing-file' }
|
|
109
130
|
|
|
110
|
-
const
|
|
111
|
-
const
|
|
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' }
|
|
112
134
|
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
if (SOLO_IMPL_PATTERNS.some(p => p.test(lines[i]))) {
|
|
117
|
-
cutLine = i
|
|
118
|
-
break
|
|
119
|
-
}
|
|
120
|
-
}
|
|
135
|
+
const before = original.slice(0, startMatch.index).replace(/\s*$/, '')
|
|
136
|
+
const fromStep6 = original.slice(startMatch.index)
|
|
137
|
+
const fluidMatch = fromStep6.match(FLUID_SECTION)
|
|
121
138
|
|
|
122
|
-
|
|
139
|
+
const after = fluidMatch && fluidMatch.index !== undefined
|
|
140
|
+
? `\n\n${fromStep6.slice(fluidMatch.index).replace(/^\s*/, '')}`
|
|
141
|
+
: ''
|
|
123
142
|
|
|
124
|
-
const
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
return true
|
|
143
|
+
const patched = `${before}\n\n${ENSEMBLE_SECTION}${after}`
|
|
144
|
+
await fse.writeFile(filePath, patched, 'utf-8')
|
|
145
|
+
return { ok: true }
|
|
128
146
|
}
|
|
129
147
|
|
|
130
148
|
export async function initOpenspec() {
|
|
131
|
-
header('Step
|
|
149
|
+
header('Step 7, Initializing OpenSpec')
|
|
132
150
|
|
|
133
151
|
try {
|
|
134
152
|
const result = await execa('npx', ['@fission-ai/openspec', 'init', '--tools', 'opencode', '--force'], {
|
|
@@ -146,19 +164,18 @@ export async function initOpenspec() {
|
|
|
146
164
|
error(`Failed to run openspec init: ${err.message}`)
|
|
147
165
|
}
|
|
148
166
|
|
|
149
|
-
//
|
|
150
|
-
const
|
|
151
|
-
path.join(process.cwd(),
|
|
152
|
-
path.join(process.cwd(), '.opencode', 'skills', 'openspec-apply-change', 'SKILL.md'),
|
|
153
|
-
]
|
|
154
|
-
|
|
155
|
-
for (const target of targets) {
|
|
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)
|
|
156
170
|
try {
|
|
157
|
-
const
|
|
158
|
-
if (
|
|
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
|
+
}
|
|
159
177
|
} catch (err) {
|
|
160
|
-
warn(`Could not patch ${
|
|
178
|
+
warn(`Could not patch ${rel}: ${err.message}`)
|
|
161
179
|
}
|
|
162
180
|
}
|
|
163
181
|
}
|
|
164
|
-
|
|
@@ -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'], {
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import fse from 'fs-extra'
|
|
2
|
+
import path from 'path'
|
|
3
|
+
import { info, success } from '../utils/exec.js'
|
|
4
|
+
|
|
5
|
+
// Each block is identified by its heading line. We remove from the heading up to (and including) the next `---` separator.
|
|
6
|
+
const STEP1_HEADING = '### Step 1, Archive project history into OpenSpec'
|
|
7
|
+
const STEP2_HEADING = '### Step 2, Generate DESIGN.md'
|
|
8
|
+
const STEP3_HEADING = '### Step 3, Generate ARCHITECTURE.md'
|
|
9
|
+
|
|
10
|
+
// Confirm message lines that reference each step, removed when the step is skipped
|
|
11
|
+
const STEP1_CONFIRM_LINE = '- Project history archived in openspec'
|
|
12
|
+
const STEP2_CONFIRM_LINE = '- DESIGN.md generated'
|
|
13
|
+
const STEP3_CONFIRM_LINE = '- ARCHITECTURE.md generated'
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Remove a bootstrap step block from AGENTS.md content.
|
|
17
|
+
* Removes from the step heading line up to and including the next `---` separator line.
|
|
18
|
+
*/
|
|
19
|
+
function removeStepBlock(content, heading) {
|
|
20
|
+
const lines = content.split('\n')
|
|
21
|
+
const start = lines.findIndex(l => l.trim() === heading.trim())
|
|
22
|
+
if (start === -1) return content
|
|
23
|
+
|
|
24
|
+
// Find the next `---` separator after the heading
|
|
25
|
+
let end = -1
|
|
26
|
+
for (let i = start + 1; i < lines.length; i++) {
|
|
27
|
+
if (lines[i].trim() === '---') { end = i; break }
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
if (end === -1) return content
|
|
31
|
+
|
|
32
|
+
// Remove the block including any blank line before the heading
|
|
33
|
+
const removeFrom = start > 0 && lines[start - 1].trim() === '' ? start - 1 : start
|
|
34
|
+
lines.splice(removeFrom, end - removeFrom + 1)
|
|
35
|
+
return lines.join('\n')
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Remove a specific line from the confirm message block in AGENTS.md.
|
|
40
|
+
*/
|
|
41
|
+
function removeConfirmLine(content, line) {
|
|
42
|
+
return content.split('\n').filter(l => l.trim() !== line.trim()).join('\n')
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Renumber remaining bootstrap steps sequentially (Step 1, Step 2, ...).
|
|
47
|
+
*/
|
|
48
|
+
function renumberSteps(content) {
|
|
49
|
+
let counter = 0
|
|
50
|
+
return content.replace(/^### Step \d+,/gm, () => `### Step ${++counter},`)
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export async function patchAgentsMd(ctx) {
|
|
54
|
+
const agentsMdPath = path.join(process.cwd(), 'AGENTS.md')
|
|
55
|
+
if (!await fse.pathExists(agentsMdPath)) return
|
|
56
|
+
|
|
57
|
+
let content = await fse.readFile(agentsMdPath, 'utf-8')
|
|
58
|
+
const patches = []
|
|
59
|
+
|
|
60
|
+
if (ctx.hasOpenspec) {
|
|
61
|
+
content = removeStepBlock(content, STEP1_HEADING)
|
|
62
|
+
content = removeConfirmLine(content, STEP1_CONFIRM_LINE)
|
|
63
|
+
patches.push('Step 1 (openspec history) removed, openspec/ already exists')
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (ctx.hasDesign) {
|
|
67
|
+
content = removeStepBlock(content, STEP2_HEADING)
|
|
68
|
+
content = removeConfirmLine(content, STEP2_CONFIRM_LINE)
|
|
69
|
+
patches.push('Step 2 (DESIGN.md) removed, DESIGN.md already exists')
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (ctx.hasArchitecture) {
|
|
73
|
+
content = removeStepBlock(content, STEP3_HEADING)
|
|
74
|
+
content = removeConfirmLine(content, STEP3_CONFIRM_LINE)
|
|
75
|
+
patches.push('Step 3 (ARCHITECTURE.md) removed, ARCHITECTURE.md already exists')
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (patches.length === 0) return
|
|
79
|
+
|
|
80
|
+
content = renumberSteps(content)
|
|
81
|
+
await fse.writeFile(agentsMdPath, content, 'utf-8')
|
|
82
|
+
|
|
83
|
+
for (const msg of patches) info(msg)
|
|
84
|
+
success('AGENTS.md patched for existing project state')
|
|
85
|
+
}
|
package/src/utils/copy.js
CHANGED
|
@@ -2,24 +2,38 @@ import fse from 'fs-extra'
|
|
|
2
2
|
import path from 'path'
|
|
3
3
|
|
|
4
4
|
// Folders never copied (skills handled separately by chooseSkillsProvider, .bootstrap is internal tooling)
|
|
5
|
+
// These are excluded from the general content copy, they are installed separately
|
|
6
|
+
// by initOpenspec after openspec init runs, so our versions win over the generated ones.
|
|
5
7
|
const ALWAYS_EXCLUDE = ['.bootstrap', 'skills', 'node_modules']
|
|
8
|
+
const OPENSPEC_APPLY_FILES = [
|
|
9
|
+
path.join('.opencode', 'commands', 'opsx-apply.md'),
|
|
10
|
+
path.join('.opencode', 'skills', 'openspec-apply-change', 'SKILL.md'),
|
|
11
|
+
]
|
|
6
12
|
|
|
7
13
|
/**
|
|
8
|
-
* Copy content/ directory to destination
|
|
9
|
-
*
|
|
14
|
+
* Copy content/ directory to destination.
|
|
15
|
+
* Excludes:
|
|
16
|
+
* - .agents/skills and .opencode/skills (handled separately)
|
|
17
|
+
* - .bootstrap (internal tooling)
|
|
18
|
+
* - node_modules
|
|
19
|
+
* - opsx-apply.md and openspec-apply-change/SKILL.md (installed by initOpenspec)
|
|
20
|
+
* - DESIGN.md and ARCHITECTURE.md if ctx says they already exist (preserve user's files)
|
|
10
21
|
* @param {string} contentDir - absolute path to content/
|
|
11
22
|
* @param {string} destDir - absolute path to destination (project root)
|
|
12
23
|
* @param {'azure'|'github'} platform
|
|
24
|
+
* @param {{ hasDesign?: boolean, hasArchitecture?: boolean }} ctx
|
|
13
25
|
*/
|
|
14
|
-
export async function copyContent(contentDir, destDir, platform) {
|
|
26
|
+
export async function copyContent(contentDir, destDir, platform, ctx = {}) {
|
|
15
27
|
await fse.copy(contentDir, destDir, {
|
|
16
28
|
overwrite: false,
|
|
17
29
|
filter: (src) => {
|
|
18
30
|
const rel = path.relative(contentDir, src)
|
|
19
31
|
const parts = rel.split(path.sep)
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
)
|
|
32
|
+
if (parts.some(part => ALWAYS_EXCLUDE.some(pattern => part.includes(pattern)))) return false
|
|
33
|
+
if (OPENSPEC_APPLY_FILES.some(f => rel === f)) return false
|
|
34
|
+
if (ctx.hasDesign && rel === 'DESIGN.md') return false
|
|
35
|
+
if (ctx.hasArchitecture && rel === 'ARCHITECTURE.md') return false
|
|
36
|
+
return true
|
|
23
37
|
},
|
|
24
38
|
})
|
|
25
39
|
}
|