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.
@@ -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 ENSEMBLE_PATCH = `6. **Implement via ensemble team**
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.** Clean up stale state, then create the team:
20
+ **Step 6b.** Create the team:
16
21
  \`\`\`
17
- team_cleanup force:true acknowledge_uncommitted:true
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" }. No other fields.
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
- ...every task from tasks.md, one entry each...
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 specialists ONE AT A TIME. Wait for each team_spawn result before calling the next.
36
- Each team_spawn MUST include agent field (required, causes NOT NULL error if omitted).
37
- Include: full task list and all context file paths. Tell each agent to:
38
- - implement their tasks in order
39
- - send results to lead via team_message when all done or blocked
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
- **Step 6e.** After all spawns, tell the user what is running, then STOP and wait.
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 teammates message back: call team_results to get full output
54
- (team_message delivery is truncated always use team_results for the full content).
55
- Then for each teammate: team_shutdown team_merge. Fix any blockers reported.
56
- Do NOT edit any files between spawns and merges — team_merge blocks on overlapping local changes.
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 files, run tests + build + lint + verify acceptance criteria, send results to lead when done>"
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 team_results fix blockers team_shutdown (no team_merge needed, worktree:false)
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: \`- [ ]\` \`- [x]\` for each completed task.
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 it is required and will fail without it
86
- - NEVER call team_spawn before team_tasks_add tasks must exist before agents are spawned
87
- - NEVER poll team_results or team_status in a loop wait for teammates to message you
88
- - NEVER call team_claim or team_tasks_complete the task board is visual only, tasks.md is the source of truth
89
- - NEVER edit files between team_spawn and team_merge team_merge blocks on overlapping local changes
90
- - ALWAYS run team_cleanup force:true before team_create to clear stale state
91
- - ALWAYS add every task from tasks.md to the board with team_tasks_add before spawning
92
- - ALWAYS spawn one at a time, waiting for each result before the next (avoids worktree contention)
93
- - Do NOT instruct agents to call team_claim, team_tasks_complete, or any task board tool — agents only implement and report back via team_message
94
- - If teammates are stuck, use team_message to resend tasks, then wait never implement directly
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
- // Patterns that identify the solo implementation step in openspec-generated files
101
- const SOLO_IMPL_PATTERNS = [
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 patchOpensxApply(filePath) {
108
- if (!fs.existsSync(filePath)) return false
128
+ async function patchApplyFile(filePath) {
129
+ if (!await fse.pathExists(filePath)) return { ok: false, reason: 'missing-file' }
109
130
 
110
- const content = fs.readFileSync(filePath, 'utf8')
111
- const lines = content.split('\n')
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
- // Find the line index where the solo implementation step starts
114
- let cutLine = -1
115
- for (let i = 0; i < lines.length; i++) {
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
- if (cutLine === -1) return false // Pattern not found, skip
139
+ const after = fluidMatch && fluidMatch.index !== undefined
140
+ ? `\n\n${fromStep6.slice(fluidMatch.index).replace(/^\s*/, '')}`
141
+ : ''
123
142
 
124
- const preamble = lines.slice(0, cutLine).join('\n').trimEnd()
125
- const patched = preamble + '\n\n' + ENSEMBLE_PATCH
126
- fs.writeFileSync(filePath, patched, 'utf8')
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 6, Initializing OpenSpec')
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
- // Patch opsx-apply.md to use ensemble orchestration instead of solo implementation
150
- const targets = [
151
- path.join(process.cwd(), '.opencode', 'commands', 'opsx-apply.md'),
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 patched = patchOpensxApply(target)
158
- if (patched) success(`Patched ${path.relative(process.cwd(), target)} for ensemble`)
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 ${path.relative(process.cwd(), target)}: ${err.message}`)
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 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'], {
@@ -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, excluding skills (handled separately by chooseSkillsProvider)
9
- * and internal bootstrap tooling.
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
- return !parts.some(part =>
21
- ALWAYS_EXCLUDE.some(pattern => part.includes(pattern))
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
  }