opencode-onboard 0.4.4 → 0.4.7

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.
Files changed (37) hide show
  1. package/README.md +58 -19
  2. package/content/.agents/agents/basic-engineer.md +5 -5
  3. package/content/.agents/agents/devops-manager.md +14 -10
  4. package/content/.agents/skills/ob-global/SKILL.md +9 -7
  5. package/content/.opencode/commands/ob-create-architecture.md +76 -0
  6. package/content/.opencode/commands/ob-create-design.md +53 -0
  7. package/content/.opencode/commands/{create-engineer.md → ob-create-engineer.md} +9 -8
  8. package/content/.opencode/commands/ob-init.md +8 -0
  9. package/content/.opencode/commands/{main.md → ob-main.md} +2 -2
  10. package/content/.opencode/commands/{plan.md → ob-plan.md} +2 -2
  11. package/content/.opencode/commands/opsx-apply.md +212 -193
  12. package/content/.opencode/skills/openspec-apply-change/SKILL.md +234 -176
  13. package/content/AGENTS.md +161 -49
  14. package/content/ARCHITECTURE.md +16 -327
  15. package/content/DESIGN.md +16 -26
  16. package/package.json +1 -1
  17. package/src/commands/join.js +6 -1
  18. package/src/commands/single.js +1 -1
  19. package/src/presets/models.json +2 -2
  20. package/src/presets/platforms.json +4 -0
  21. package/src/steps/copy/agents.js +200 -3
  22. package/src/steps/copy/agents.test.js +45 -0
  23. package/src/steps/copy/copy.test.js +15 -2
  24. package/src/steps/copy/index.js +2 -1
  25. package/src/steps/metadata/index.js +6 -5
  26. package/src/steps/metadata/metadata.test.js +16 -8
  27. package/src/steps/models/write.js +17 -4
  28. package/src/steps/models/write.test.js +57 -56
  29. package/src/steps/openspec/ensemble.js +81 -54
  30. package/src/steps/openspec/ensemble.test.js +40 -8
  31. package/src/steps/optimization/codegraph.js +51 -0
  32. package/src/steps/optimization/codegraph.test.js +104 -0
  33. package/src/steps/optimization/global.js +21 -1
  34. package/src/steps/optimization/global.test.js +3 -0
  35. package/src/steps/platform/index.js +8 -1
  36. package/src/steps/platform/platform.test.js +19 -0
  37. package/content/.opencode/commands/init.md +0 -8
package/content/DESIGN.md CHANGED
@@ -1,26 +1,16 @@
1
- > Execute this command
2
-
3
- Analyze the design system of this codebase with the goal of creating a DESIGN.md file in the project root and giving the user a file for easy copy & pasting.
4
-
5
- Reference material:
6
- Overview : https://stitch.withgoogle.com/docs/design-md/overview/
7
- Format : https://stitch.withgoogle.com/docs/design-md/format/
8
- Spec : https://github.com/google-labs-code/design.md
9
-
10
- Examples from the spec repo:
11
- https://github.com/google-labs-code/design.md/blob/main/examples/atmospheric-glass/DESIGN.md
12
- https://github.com/google-labs-code/design.md/blob/main/examples/paws-and-paths/DESIGN.md
13
-
14
- Requirements:
15
- - Begin with YAML frontmatter containing all structured design tokens
16
- (colors, typography, spacing, elevation, motion, radii, shadows, etc.)
17
- - Follow with free-form Markdown that describes the look & feel and
18
- captures design intent that token values alone cannot convey
19
- - The file must be entirely self-contained, do not reference any
20
- files, variables, or paths from the codebase
21
- - All token values must use valid YAML design token format
22
-
23
- If you have access to a running local server or screenshots of the
24
- product, compare your DESIGN.md against the rendered UI. Revise until
25
- both the YAML tokens and the written description faithfully capture
26
- the product's visual identity.
1
+ > NOT GENERATED YET
2
+ >
3
+ > This file has not been populated yet. It is intentionally empty.
4
+ >
5
+ > **If this is a greenfield project** (no UI exists yet): skip this for now.
6
+ > Come back and run `/ob-create-design` once you have a design system, UI components, or styles in place.
7
+ >
8
+ > **If this is a brownfield project** (existing UI/styles): run this command now to generate the design documentation:
9
+ >
10
+ > ```
11
+ > /ob-create-design
12
+ > ```
13
+ >
14
+ > This command analyzes your CSS, Tailwind config, component files, and design tokens,
15
+ > then writes a complete DESIGN.md with structured YAML tokens and written design intent.
16
+ > It is safe to rerun any time your design system changes.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-onboard",
3
- "version": "0.4.4",
3
+ "version": "0.4.7",
4
4
  "description": "Prepare any brownfield codebase for AI agent workflows using OpenCode, OpenSpec, and ensemble orchestration.",
5
5
  "keywords": [
6
6
  "opencode",
@@ -18,7 +18,12 @@ export async function runJoin() {
18
18
  const saved = await readOnboardConfig()
19
19
  const savedPlatform = saved?.wizard?.platform
20
20
  if (savedPlatform) {
21
- info(`Detected project platform: ${savedPlatform === 'github' ? 'GitHub' : 'Azure DevOps'}`)
21
+ const display = savedPlatform === 'github'
22
+ ? 'GitHub'
23
+ : savedPlatform === 'azure'
24
+ ? 'Azure DevOps'
25
+ : 'None'
26
+ info(`Detected project platform: ${display}`)
22
27
  await checkPlatform(savedPlatform)
23
28
  } else {
24
29
  const platform = await choosePlatform()
@@ -20,7 +20,7 @@ export async function runSingleCommand(command) {
20
20
  maxConcurrentAgents: savedWizard?.maxConcurrentAgents ?? 4,
21
21
  }
22
22
  const platform = savedWizard?.platform
23
- const resolvedPlatform = platform === 'azure' || platform === 'github' ? platform : 'github'
23
+ const resolvedPlatform = platform === 'azure' || platform === 'github' || platform === 'none' ? platform : 'github'
24
24
 
25
25
  const handlers = {
26
26
  clean: async () => {
@@ -16,10 +16,10 @@
16
16
  "build": {
17
17
  "prompt": "Build model:",
18
18
  "info": [
19
- "BUILD model: used by front-engineer, back-engineer, infra-engineer, quality-engineer, security-auditor.",
19
+ "BUILD model: used by basic-engineer and any custom engineer created in the project.",
20
20
  "Needs to be capable for implementation work. Claude Sonnet, GPT-4o, or equivalent."
21
21
  ],
22
- "agents": ["front-engineer", "back-engineer", "infra-engineer", "quality-engineer", "security-auditor"]
22
+ "agents": ["basic-engineer"]
23
23
  },
24
24
  "fast": {
25
25
  "prompt": "Fast model:",
@@ -33,5 +33,9 @@
33
33
  ]
34
34
  }
35
35
  }
36
+ },
37
+ {
38
+ "value": "none",
39
+ "name": "None"
36
40
  }
37
41
  ]
@@ -38,8 +38,23 @@ function renumberSteps(content) {
38
38
 
39
39
  const PLATFORM_SKILLS_START = '<!-- OB-PLATFORM-SKILLS-START -->'
40
40
  const PLATFORM_SKILLS_END = '<!-- OB-PLATFORM-SKILLS-END -->'
41
+ const PLATFORM_MODE_START = '<!-- OB-PLATFORM-MODE-START -->'
42
+ const PLATFORM_MODE_END = '<!-- OB-PLATFORM-MODE-END -->'
43
+ const PLATFORM_WORKFLOW_START = '<!-- OB-PLATFORM-WORKFLOW-START -->'
44
+ const PLATFORM_WORKFLOW_END = '<!-- OB-PLATFORM-WORKFLOW-END -->'
45
+ const PLATFORM_PIPELINE_START = '<!-- OB-PLATFORM-PIPELINE-START -->'
46
+ const PLATFORM_PIPELINE_END = '<!-- OB-PLATFORM-PIPELINE-END -->'
41
47
 
42
48
  function buildPlatformSkillsSection(platform) {
49
+ if (platform === 'none') {
50
+ return [
51
+ '- Selected platform: `none` (from onboarding platform step).',
52
+ '- Do NOT load `ob-userstory-gh`, `ob-userstory-az`, `ob-pullrequest-gh`, or `ob-pullrequest-az`.',
53
+ '- Work only from direct user instructions in the main conversation or from local OpenSpec artifacts.',
54
+ '- Ignore GitHub/Azure DevOps URL inference unless the user explicitly reconfigures onboarding later.',
55
+ ].join('\n')
56
+ }
57
+
43
58
  if (platform === 'azure') {
44
59
  return [
45
60
  '- Selected platform: `azure` (from onboarding platform step).',
@@ -55,6 +70,176 @@ function buildPlatformSkillsSection(platform) {
55
70
  ].join('\n')
56
71
  }
57
72
 
73
+ function buildPlatformModeSection(platform) {
74
+ if (platform === 'none') {
75
+ return 'This project does not use GitHub or Azure DevOps integration. Do not read work items, create PRs, or process PR feedback. Operate only from direct user instructions and local repository context.'
76
+ }
77
+
78
+ return 'This project uses platform-integrated workflow modes described below.'
79
+ }
80
+
81
+ function buildLeadWorkflowSection(platform) {
82
+ if (platform === 'none') {
83
+ return [
84
+ 'When the user gives a task directly in the conversation, **I own the full lifecycle**. I load `ob-global` first, then work from the user request and local repository context. I may use OpenSpec when structured planning helps, but I do not depend on issue links or PR workflows.',
85
+ '',
86
+ 'Trigger patterns, I recognize ALL of these, exact wording does not matter:',
87
+ '- User describes a feature, bug, or refactor directly → clarify if needed → optionally run `/opsx-propose` → implement in the main session or `/opsx-apply` as appropriate',
88
+ '- `implement the plan` / `implement` / `start` / `go` → run `/opsx-apply` against the current OpenSpec change when one exists',
89
+ '- `just do it` / `quick fix` / `raw conversation` → work directly in the main session without PR or work-item automation',
90
+ '',
91
+ '**GitHub Issue URLs, Azure DevOps work item URLs, and PR URLs are NOT automatic triggers in this mode.** Only use platform-specific flows if onboarding is later reconfigured for GitHub or Azure DevOps.',
92
+ ].join('\n')
93
+ }
94
+
95
+ return [
96
+ 'When the user provides a work item URL or says "implement the plan" or "I\'ve added comments to the PR", **I own the full lifecycle**. I load `ob-global` skill first, then the appropriate userstory skill, and use ensemble tools to coordinate the agent team.',
97
+ '',
98
+ 'Trigger patterns, I recognize ALL of these, exact wording does not matter:',
99
+ platform === 'azure'
100
+ ? '- User pastes or mentions an Azure DevOps URL → load `ob-userstory-az` skill → parse work item → run `/opsx-propose` → confirm with user → run `/opsx-apply` → ship'
101
+ : '- User pastes or mentions a GitHub Issue URL → load `ob-userstory-gh` skill → parse issue → run `/opsx-propose` → confirm with user → run `/opsx-apply` → ship',
102
+ '- `implement the plan` / `implement` / `start` / `go` → run `/opsx-apply` → ship',
103
+ '- `I\'ve added comments to the PR` → read PR comments → fix → update PR',
104
+ platform === 'azure'
105
+ ? '- Any Azure DevOps PR URL in a feedback/fix request (e.g. "check comments", "fix PR feedback") → run PR Feedback Loop'
106
+ : '- Any GitHub PR URL in a feedback/fix request (e.g. "check comments", "fix PR feedback") → run PR Feedback Loop',
107
+ '',
108
+ platform === 'azure'
109
+ ? '**An Azure DevOps URL anywhere in the user\'s message is always a trigger, regardless of surrounding words.**'
110
+ : '**A GitHub URL anywhere in the user\'s message is always a trigger, regardless of surrounding words.**',
111
+ ].join('\n')
112
+ }
113
+
114
+ function buildPipelineSection(platform) {
115
+ if (platform === 'none') {
116
+ return [
117
+ '```',
118
+ 'main session (lead mode)',
119
+ ' → load ob-global + understand direct user request',
120
+ ' ↓',
121
+ ' optional openspec-propose',
122
+ ' → proposal.md + specs + tasks when structured planning helps',
123
+ ' ↓',
124
+ ' [confirm with user when scope needs it]',
125
+ ' ↓',
126
+ 'basic-engineer + custom-engineer-* (parallel as needed)',
127
+ ' → claim tasks + load abilities + implement',
128
+ ' ↓',
129
+ 'main session',
130
+ ' → verify completion → summarize results to user',
131
+ '```',
132
+ '',
133
+ '### Phase 1, Clarify & Plan',
134
+ '',
135
+ '```',
136
+ '1. Load `ob-global`.',
137
+ '2. Understand the task directly from the conversation and local repo context.',
138
+ '3. If the work benefits from explicit specs/tasks, run `/opsx-propose` or create/update OpenSpec artifacts.',
139
+ '4. Show the plan or intended scope when the work is non-trivial.',
140
+ '5. If the request is small and clear, implementation can begin directly in the main session.',
141
+ '```',
142
+ '',
143
+ '### Phase 2, Implement',
144
+ '',
145
+ '```',
146
+ '0. Run `/quota` to check remaining budget before spawning, when available.',
147
+ '1. If using OpenSpec tasks, run `/opsx-apply`.',
148
+ ' - Step 5b: classify cost tier, announce scope, ask user to confirm if ≥4 tasks.',
149
+ ' - Lead adds all tasks to board.',
150
+ ' - When dependencies exist, lead uses multiple `team_tasks_add` waves so later tasks can reference real task IDs returned by earlier waves.',
151
+ ' - Lead discovers available engineers from `.agents/agents/*.md`, prefers matching custom engineers, then spawns engineers with initial batch of up to 3 tasks each (rolling batch model).',
152
+ ' - Each engineer claims tasks, implements, completes, messages lead.',
153
+ ' - Lead assigns next batch (up to 3) to agents that report done. Repeat until board empty.',
154
+ ' - Lead merges each engineer branch after shutdown, then marks tasks done in tasks.md.',
155
+ '2. If the task is small and no OpenSpec plan is needed, implement directly in the main session instead.',
156
+ '3. Verify with tests/build/lint according to task scope.',
157
+ '```',
158
+ '',
159
+ 'There is no PR shipping phase and no PR feedback loop in `none` mode. Report completion directly to the user in the main conversation.',
160
+ ].join('\n')
161
+ }
162
+
163
+ const phase1Line = platform === 'azure'
164
+ ? '1. Detect URL type → load matching skill (`ob-userstory-az`)'
165
+ : '1. Detect URL type → load matching skill (`ob-userstory-gh`)'
166
+ const prFlow = platform === 'azure'
167
+ ? '3. team_spawn name:devops agent:devops-manager (ship mode)\n → commit & push → create PR → post comment'
168
+ : '3. team_spawn name:devops agent:devops-manager (ship mode)\n → commit & push → create PR → post comment'
169
+
170
+ return [
171
+ '```',
172
+ 'devops-manager (lead mode)',
173
+ ' → load ob-global + parse work item via skill',
174
+ ' ↓',
175
+ ' openspec-propose',
176
+ ' → proposal.md + specs + tasks',
177
+ ' ↓',
178
+ ' [confirm with user]',
179
+ ' ↓',
180
+ 'basic-engineer + custom-engineer-* (parallel as needed)',
181
+ ' → claim tasks + load abilities + implement',
182
+ ' ↓',
183
+ 'devops-manager (ship mode)',
184
+ ' → verify completion → commit → push → PR → post comment',
185
+ '```',
186
+ '',
187
+ '### Phase 1, Parse & Propose',
188
+ '',
189
+ '```',
190
+ phase1Line,
191
+ '2. Follow skill steps: fetch issue/work item via CLI, create OpenSpec change',
192
+ '3. Run /opsx-propose → generates proposal.md, specs/, design.md, tasks.md',
193
+ '4. Show the plan: change name, total tasks, task list summary',
194
+ '5. STOP. Ask user: "Ready to implement? (yes/no)", DO NOT proceed until confirmed.',
195
+ '```',
196
+ '',
197
+ '### Phase 2, Implement',
198
+ '',
199
+ '```',
200
+ '0. Run /quota to check remaining budget before spawning.',
201
+ '1. Run /opsx-apply.',
202
+ ' - Step 5b: classify cost tier, announce scope, ask user to confirm if ≥4 tasks.',
203
+ ' - Lead adds all tasks to board.',
204
+ ' - When dependencies exist, lead uses multiple `team_tasks_add` waves so later tasks can reference real task IDs returned by earlier waves.',
205
+ ' - Lead discovers available engineers from `.agents/agents/*.md`, prefers matching custom engineers, then spawns engineers with initial batch of up to 3 tasks each (rolling batch model).',
206
+ ' - Each engineer claims tasks, implements, completes, messages lead.',
207
+ ' - Lead assigns next batch (up to 3) to agents that report done. Repeat until board empty.',
208
+ ' - Lead merges each engineer branch after shutdown, then marks tasks done in tasks.md.',
209
+ '2. Verify with tests/build/lint according to task scope.',
210
+ '3. Run /quota after all agents are merged.',
211
+ '```',
212
+ '',
213
+ '### Phase 3, Ship',
214
+ '',
215
+ '```',
216
+ prFlow,
217
+ '4. Wait → team_results → report PR URL to user',
218
+ '5. team_cleanup',
219
+ '```',
220
+ '',
221
+ '### Phase 4, PR Feedback Loop',
222
+ '',
223
+ '```',
224
+ 'When user says "I\'ve added comments to the PR" or asks to fix PR comments from PR URLs:',
225
+ '1. team_create "pr-feedback-<id>-<random>"',
226
+ '2. team_tasks_add with at least these lead-managed tasks:',
227
+ ' - Parse and classify PR feedback (devops-manager)',
228
+ ' - Implement feedback items (basic-engineer and/or custom engineers)',
229
+ ' - Verify with tests/build/lint (implementation worker or dedicated verifier if available)',
230
+ ' - Push updates and post PR replies (devops-manager)',
231
+ '3. team_spawn devops-manager (feedback mode) with explicit task IDs, then team_message "Start now"',
232
+ '4. Wait for message → team_results',
233
+ '5. Add/update implementation tasks on board, then spawn needed engineers in parallel with explicit task IDs + team_message "Start now"',
234
+ '6. Wait for engineer results → team_shutdown + team_merge per engineer',
235
+ '7. Run verification tasks (tests/build/lint) and fix blockers if any',
236
+ '8. team_spawn devops-manager (ship mode) with "push + update PR threads" task ID + team_message "Start now"',
237
+ '9. Wait → team_results → report what was updated',
238
+ '10. team_cleanup',
239
+ '```',
240
+ ].join('\n')
241
+ }
242
+
58
243
  function replaceBetween(content, start, end, replacement) {
59
244
  if (!content.includes(start) || !content.includes(end)) return content
60
245
  const pattern = new RegExp(`${start}[\\s\\S]*?${end}`)
@@ -122,13 +307,25 @@ export async function patchAgentsMd(ctx) {
122
307
  }
123
308
  }
124
309
 
125
- export async function patchDevopsManagerMd(platform) {
126
- const devopsPath = path.join(process.cwd(), '.agents', 'agents', 'devops-manager.md')
310
+ export async function patchAgentGuidance(platform, cwd = process.cwd()) {
311
+ const agentsMdPath = path.join(cwd, 'AGENTS.md')
312
+ if (await fse.pathExists(agentsMdPath)) {
313
+ let content = await fse.readFile(agentsMdPath, 'utf-8')
314
+ content = replaceBetween(content, PLATFORM_WORKFLOW_START, PLATFORM_WORKFLOW_END, buildLeadWorkflowSection(platform))
315
+ content = replaceBetween(content, PLATFORM_PIPELINE_START, PLATFORM_PIPELINE_END, buildPipelineSection(platform))
316
+ await fse.writeFile(agentsMdPath, `${content.replace(/\s*$/, '')}\n`, 'utf-8')
317
+ success(`AGENTS.md patched for platform workflow: ${platform}`)
318
+ }
319
+ }
320
+
321
+ export async function patchDevopsManagerMd(platform, cwd = process.cwd()) {
322
+ const devopsPath = path.join(cwd, '.agents', 'agents', 'devops-manager.md')
127
323
  if (!await fse.pathExists(devopsPath)) return
128
324
 
129
- const resolved = platform === 'azure' ? 'azure' : 'github'
325
+ const resolved = platform === 'azure' ? 'azure' : platform === 'none' ? 'none' : 'github'
130
326
  let content = await fse.readFile(devopsPath, 'utf-8')
131
327
  content = replaceBetween(content, PLATFORM_SKILLS_START, PLATFORM_SKILLS_END, buildPlatformSkillsSection(resolved))
328
+ content = replaceBetween(content, PLATFORM_MODE_START, PLATFORM_MODE_END, buildPlatformModeSection(resolved))
132
329
  await fse.writeFile(devopsPath, `${content.replace(/\s*$/, '')}\n`, 'utf-8')
133
330
  success(`devops-manager.md patched for platform: ${resolved}`)
134
331
  }
@@ -0,0 +1,45 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest'
2
+ import fs from 'node:fs'
3
+ import path from 'node:path'
4
+ import os from 'node:os'
5
+ import fse from 'fs-extra'
6
+ import { patchAgentGuidance, patchDevopsManagerMd } from './agents.js'
7
+
8
+ describe('platform patching', () => {
9
+ let tmpDir
10
+
11
+ beforeEach(() => {
12
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'agents-patch-test-'))
13
+ })
14
+
15
+ afterEach(() => {
16
+ fs.rmSync(tmpDir, { recursive: true, force: true })
17
+ })
18
+
19
+ it('patches AGENTS.md for none mode with raw-conversation workflow', async () => {
20
+ const source = path.join(process.cwd(), 'content', 'AGENTS.md')
21
+ const dest = path.join(tmpDir, 'AGENTS.md')
22
+ await fse.copyFile(source, dest)
23
+
24
+ await patchAgentGuidance('none', tmpDir)
25
+
26
+ const content = await fse.readFile(dest, 'utf-8')
27
+ expect(content).toContain('GitHub Issue URLs, Azure DevOps work item URLs, and PR URLs are NOT automatic triggers in this mode.')
28
+ expect(content).toContain('There is no PR shipping phase and no PR feedback loop in `none` mode.')
29
+ expect(content).not.toContain('A GitHub or Azure DevOps URL anywhere in the user\'s message is always a trigger')
30
+ })
31
+
32
+ it('patches devops-manager for none mode without platform skills', async () => {
33
+ const source = path.join(process.cwd(), 'content', '.agents', 'agents', 'devops-manager.md')
34
+ const dest = path.join(tmpDir, '.agents', 'agents', 'devops-manager.md')
35
+ await fse.ensureDir(path.dirname(dest))
36
+ await fse.copyFile(source, dest)
37
+
38
+ await patchDevopsManagerMd('none', tmpDir)
39
+
40
+ const content = await fse.readFile(dest, 'utf-8')
41
+ expect(content).toContain('Selected platform: `none`')
42
+ expect(content).toContain('Do NOT load `ob-userstory-gh`, `ob-userstory-az`, `ob-pullrequest-gh`, or `ob-pullrequest-az`.')
43
+ expect(content).toContain('This project does not use GitHub or Azure DevOps integration.')
44
+ })
45
+ })
@@ -11,6 +11,7 @@ vi.mock('../../utils/copy.js', () => ({
11
11
  }))
12
12
 
13
13
  vi.mock('./agents.js', () => ({
14
+ patchAgentGuidance: vi.fn(),
14
15
  patchAgentsMd: vi.fn(),
15
16
  patchConcurrency: vi.fn(),
16
17
  patchDevopsManagerMd: vi.fn(),
@@ -21,7 +22,7 @@ vi.mock('./skills.js', () => ({
21
22
  }))
22
23
 
23
24
  import { copyContent } from '../../utils/copy.js'
24
- import { success, error } from '../../utils/exec.js'
25
+ import { error } from '../../utils/exec.js'
25
26
  import { copyContentStep } from './index.js'
26
27
 
27
28
  describe('copyContentStep()', () => {
@@ -47,7 +48,6 @@ describe('copyContentStep()', () => {
47
48
  'github',
48
49
  {}
49
50
  )
50
- expect(success).toHaveBeenCalledWith('Files copied to project root')
51
51
  })
52
52
 
53
53
  it('calls copyContent with azure platform', async () => {
@@ -63,6 +63,19 @@ describe('copyContentStep()', () => {
63
63
  )
64
64
  })
65
65
 
66
+ it('calls copyContent with none platform', async () => {
67
+ copyContent.mockResolvedValue(undefined)
68
+
69
+ await copyContentStep('none')
70
+
71
+ expect(copyContent).toHaveBeenCalledWith(
72
+ expect.stringContaining('content'),
73
+ process.cwd(),
74
+ 'none',
75
+ {}
76
+ )
77
+ })
78
+
66
79
  it('calls process.exit(1) when copyContent throws', async () => {
67
80
  copyContent.mockRejectedValue(new Error('disk full'))
68
81
 
@@ -3,7 +3,7 @@ import path from 'path'
3
3
  import { fileURLToPath } from 'url'
4
4
  import { copyContent } from '../../utils/copy.js'
5
5
  import { error, header, success } from '../../utils/exec.js'
6
- import { patchAgentsMd, patchConcurrency, patchDevopsManagerMd } from './agents.js'
6
+ import { patchAgentGuidance, patchAgentsMd, patchConcurrency, patchDevopsManagerMd } from './agents.js'
7
7
  import { installSkills } from './skills.js'
8
8
 
9
9
  const __dirname = path.dirname(fileURLToPath(import.meta.url))
@@ -23,6 +23,7 @@ export async function copyContentStep(platform, ctx = {}) {
23
23
  roots: ctx.sourceRoots || [dest],
24
24
  }, { spaces: 2 })
25
25
  await patchDevopsManagerMd(platform)
26
+ await patchAgentGuidance(platform)
26
27
  await patchAgentsMd(ctx)
27
28
  await patchConcurrency(ctx)
28
29
  await installSkills()
@@ -18,11 +18,12 @@ async function detectOpencodeVersion() {
18
18
  }
19
19
  }
20
20
 
21
- export async function writeOnboardConfig(data) {
22
- header('Step 10, Writing onboarding metadata')
23
-
24
- const opencodeVersion = await detectOpencodeVersion()
25
- const target = path.join(process.cwd(), '.opencode', 'opencode-onboard.json')
21
+ export async function writeOnboardConfig(data) {
22
+ header('Step 10, Writing onboarding metadata')
23
+
24
+ const opencodeVersion = await detectOpencodeVersion()
25
+ const cwd = data.cwd ?? process.cwd()
26
+ const target = path.join(cwd, '.opencode', 'opencode-onboard.json')
26
27
 
27
28
  const payload = {
28
29
  schema: 1,
@@ -25,17 +25,14 @@ import fse from 'fs-extra'
25
25
  import { writeOnboardConfig } from './index.js'
26
26
 
27
27
  describe('writeOnboardConfig()', () => {
28
- let tmpDir, originalCwd
28
+ let tmpDir
29
29
 
30
30
  beforeEach(() => {
31
31
  vi.clearAllMocks()
32
32
  tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'metadata-test-'))
33
- originalCwd = process.cwd()
34
- process.chdir(tmpDir)
35
33
  })
36
34
 
37
35
  afterEach(() => {
38
- process.chdir(originalCwd)
39
36
  fs.rmSync(tmpDir, { recursive: true, force: true })
40
37
  })
41
38
 
@@ -55,6 +52,7 @@ describe('writeOnboardConfig()', () => {
55
52
  fastModel: 'fast-model',
56
53
  optionalTools: ['rtk'],
57
54
  cavemanGuidance: true,
55
+ cwd: tmpDir,
58
56
  })
59
57
 
60
58
  expect(fse.ensureDir).toHaveBeenCalled()
@@ -70,7 +68,7 @@ describe('writeOnboardConfig()', () => {
70
68
  it('detects opencode version from CLI', async () => {
71
69
  execa.mockResolvedValue({ exitCode: 0, stdout: '2.0.0', stderr: '' })
72
70
 
73
- await writeOnboardConfig({ platform: 'github', sourceMode: 'current', sourceRoots: [] })
71
+ await writeOnboardConfig({ platform: 'github', sourceMode: 'current', sourceRoots: [], cwd: tmpDir })
74
72
 
75
73
  const call = fse.writeJson.mock.calls[0]
76
74
  const payload = call[1]
@@ -80,7 +78,7 @@ describe('writeOnboardConfig()', () => {
80
78
  it('handles missing opencode gracefully', async () => {
81
79
  execa.mockResolvedValue({ exitCode: 1, stdout: '', stderr: '' })
82
80
 
83
- await writeOnboardConfig({ platform: 'github', sourceMode: 'current', sourceRoots: [] })
81
+ await writeOnboardConfig({ platform: 'github', sourceMode: 'current', sourceRoots: [], cwd: tmpDir })
84
82
 
85
83
  const call = fse.writeJson.mock.calls[0]
86
84
  const payload = call[1]
@@ -90,10 +88,20 @@ describe('writeOnboardConfig()', () => {
90
88
  it('includes note field', async () => {
91
89
  execa.mockResolvedValue({ exitCode: 0, stdout: '1', stderr: '' })
92
90
 
93
- await writeOnboardConfig({ platform: 'github', sourceMode: 'current', sourceRoots: [] })
91
+ await writeOnboardConfig({ platform: 'github', sourceMode: 'current', sourceRoots: [], cwd: tmpDir })
94
92
 
95
93
  const call = fse.writeJson.mock.calls[0]
96
94
  const payload = call[1]
97
95
  expect(payload.note).toContain('Informational file only')
98
96
  })
99
- })
97
+
98
+ it('persists none as an explicit platform mode', async () => {
99
+ execa.mockResolvedValue({ exitCode: 0, stdout: '1', stderr: '' })
100
+
101
+ await writeOnboardConfig({ platform: 'none', sourceMode: 'current', sourceRoots: [], cwd: tmpDir })
102
+
103
+ const call = fse.writeJson.mock.calls[0]
104
+ const payload = call[1]
105
+ expect(payload.wizard.platform).toBe('none')
106
+ })
107
+ })
@@ -11,8 +11,21 @@ export async function writeModelToAgent(agentFile, modelId) {
11
11
  await fse.writeFile(agentFile, updated, 'utf-8');
12
12
  }
13
13
 
14
- export async function writeModelsToConfigs({ planModel, buildModel, fastModel, agentsDir, preset }) {
15
- for (const name of preset.roles.build.agents) {
14
+ async function listBuildAgentNames(agentsDir, preset) {
15
+ const fastAgents = new Set(preset.roles.fast.agents)
16
+ const configuredBuildAgents = preset.roles.build.agents
17
+ const discovered = await fse.pathExists(agentsDir)
18
+ ? (await fse.readdir(agentsDir))
19
+ .filter(name => name.endsWith('.md'))
20
+ .map(name => path.basename(name, '.md'))
21
+ .filter(name => !fastAgents.has(name))
22
+ : []
23
+
24
+ return [...new Set([...configuredBuildAgents, ...discovered])]
25
+ }
26
+
27
+ export async function writeModelsToConfigs({ planModel, buildModel, fastModel, agentsDir, preset, cwd = process.cwd() }) {
28
+ for (const name of await listBuildAgentNames(agentsDir, preset)) {
16
29
  const file = path.join(agentsDir, `${name}.md`);
17
30
  if (await fse.pathExists(file)) {
18
31
  await writeModelToAgent(file, buildModel);
@@ -28,7 +41,7 @@ export async function writeModelsToConfigs({ planModel, buildModel, fastModel, a
28
41
  }
29
42
  }
30
43
 
31
- const opencodeJsonPath = path.join(process.cwd(), '.opencode', 'opencode.json');
44
+ const opencodeJsonPath = path.join(cwd, '.opencode', 'opencode.json');
32
45
  if (await fse.pathExists(opencodeJsonPath)) {
33
46
  const config = await fse.readJson(opencodeJsonPath);
34
47
  config.model = buildModel;
@@ -36,7 +49,7 @@ export async function writeModelsToConfigs({ planModel, buildModel, fastModel, a
36
49
  success(`default model -> ${buildModel} (written to .opencode/opencode.json)`);
37
50
  }
38
51
 
39
- const ensembleJsonPath = path.join(process.cwd(), '.opencode', 'ensemble.json');
52
+ const ensembleJsonPath = path.join(cwd, '.opencode', 'ensemble.json');
40
53
  if (await fse.pathExists(ensembleJsonPath)) {
41
54
  const ensemble = await fse.readJson(ensembleJsonPath);
42
55
  delete ensemble.defaultModel;