opencode-onboard 0.1.12 → 0.2.0

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.
@@ -63,8 +63,18 @@ Rules:
63
63
  - Do not force push
64
64
  - Report blockers immediately rather than working around them
65
65
 
66
+ ## Workflow
67
+
68
+ When spawned by the lead:
69
+ 1. For each assigned task: call `team_claim task_id:<id>` before starting
70
+ 2. Implement the task
71
+ 3. Call `team_tasks_complete task_id:<id>` after finishing
72
+ 4. When all tasks are done or blocked, send results to lead via `team_message`
73
+
66
74
  ## Output Format
67
75
 
76
+ Send via `team_message` to lead when done:
77
+
68
78
  ```
69
79
  ## Back Engineer, Done
70
80
 
@@ -62,8 +62,18 @@ Rules:
62
62
  - Do not force push
63
63
  - Report blockers immediately rather than working around them
64
64
 
65
+ ## Workflow
66
+
67
+ When spawned by the lead:
68
+ 1. For each assigned task: call `team_claim task_id:<id>` before starting
69
+ 2. Implement the task
70
+ 3. Call `team_tasks_complete task_id:<id>` after finishing
71
+ 4. When all tasks are done or blocked, send results to lead via `team_message`
72
+
65
73
  ## Output Format
66
74
 
75
+ Send via `team_message` to lead when done:
76
+
67
77
  ```
68
78
  ## Front Engineer, Done
69
79
 
@@ -60,8 +60,17 @@ Rules:
60
60
  - Do not force push
61
61
  - Report all failures, do not silently skip failing tests
62
62
 
63
+ ## Workflow
64
+
65
+ When spawned by the lead:
66
+ 1. Read the task list and context files provided in the spawn prompt
67
+ 2. Run tests, build, lint, and verify acceptance criteria
68
+ 3. When done, send results to lead via `team_message`
69
+
63
70
  ## Output Format
64
71
 
72
+ Send via `team_message` to lead when done:
73
+
65
74
  ```
66
75
  ## Quality Engineer, Done
67
76
 
@@ -0,0 +1,170 @@
1
+ ---
2
+ description: Implement tasks from an OpenSpec change via ensemble agent team
3
+ ---
4
+
5
+ Implement tasks from an OpenSpec change using the ensemble agent team.
6
+
7
+ **Input**: Optionally specify a change name (e.g., `/opsx-apply add-auth`). If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes.
8
+
9
+ **Steps**
10
+
11
+ 1. **Select the change**
12
+
13
+ If a name is provided, use it. Otherwise:
14
+ - Infer from conversation context if the user mentioned a change
15
+ - Auto-select if only one active change exists
16
+ - If ambiguous, run `rtk openspec list --json` to get available changes and use the **AskUserQuestion tool** to let the user select
17
+
18
+ Always announce: "Using change: <name>" and how to override (e.g., `/opsx-apply <other>`).
19
+
20
+ 2. **Check status to understand the schema**
21
+
22
+ ```bash
23
+ rtk openspec status --change "<name>" --json
24
+ ```
25
+
26
+ Parse the JSON to understand:
27
+ - `schemaName`: The workflow being used (e.g., "spec-driven")
28
+ - Which artifact contains the tasks (typically "tasks" for spec-driven, check status for others)
29
+
30
+ 3. **Get apply instructions**
31
+
32
+ ```bash
33
+ rtk openspec instructions apply --change "<name>" --json
34
+ ```
35
+
36
+ This returns:
37
+ - `contextFiles`: artifact ID -> array of concrete file paths (varies by schema)
38
+ - Progress (total, complete, remaining)
39
+ - Task list with status
40
+ - Dynamic instruction based on current state
41
+
42
+ **Handle states:**
43
+ - If `state: "blocked"` (missing artifacts): show message, suggest using `/opsx-continue`
44
+ - If `state: "all_done"`: congratulate, suggest archive with `/opsx-archive`
45
+ - Otherwise: proceed to implementation
46
+
47
+ 4. **Read context files**
48
+
49
+ Read every file path listed under `contextFiles` from the apply instructions output.
50
+ Do NOT tell agents to read files themselves, summarize the content here and pass it in spawn prompts.
51
+
52
+ 5. **Show current progress**
53
+
54
+ Display:
55
+ - Schema being used
56
+ - Progress: "N/M tasks complete"
57
+ - Remaining tasks overview
58
+
59
+ 6. **Implement via ensemble team**
60
+
61
+ NEVER implement tasks directly. Always delegate to specialists via ensemble.
62
+ Do NOT touch any source files before the team is running, not even a single edit.
63
+
64
+ Steps MUST be followed in order. Do not skip any step.
65
+
66
+ **Step 6a.** Create feature branch if not already on one: `feature/{id}-{slug}`
67
+
68
+ **Step 6b.** Create the team:
69
+ ```
70
+ team_create "<change-name>-<random 4 digit number>"
71
+ ```
72
+ Announce: "Team running. Monitor at http://localhost:4747/"
73
+
74
+ **Step 6c.** Add ALL tasks to the shared board BEFORE spawning anyone.
75
+ Schema: { content: string, priority: "high"|"medium"|"low", depends_on?: string[] }
76
+ Use depends_on to block tasks that require other tasks first, pass the IDs returned by team_tasks_add.
77
+ ```
78
+ team_tasks_add tasks:[
79
+ { content: "1.1 <exact task text from tasks.md>", priority: "high" },
80
+ { content: "1.2 <exact task text>", priority: "high" },
81
+ { content: "3.1 <task that needs 1.x done first>", priority: "medium", depends_on: ["<id-of-1.1>"] },
82
+ ...every task, one entry each...
83
+ ]
84
+ ```
85
+ Save the task IDs returned. Pass them to agents in step 6d.
86
+ DO NOT call team_claim yourself, only agents claim tasks.
87
+ DO NOT proceed to 6d until team_tasks_add succeeds.
88
+
89
+ **Step 6d.** Spawn all needed specialists, then kick them off in parallel.
90
+
91
+ Each team_spawn MUST include the agent field (required, causes NOT NULL error if omitted).
92
+
93
+ The spawn prompt must contain exactly:
94
+ 1. Their name and role on this team
95
+ 2. Which tasks are theirs, list the task IDs and content from the board
96
+ 3. Key context they need (summarized from context files, do NOT tell them to read files themselves)
97
+ 4. The 6 OpenCode tools they have available (these are OpenCode tools, NOT shell commands, call them directly as tools, never via bash):
98
+ team_claim, team_tasks_complete, team_tasks_list, team_tasks_add, team_message, team_broadcast
99
+ 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
100
+
101
+ Keep spawn prompts under 500 tokens. Do not describe team internals or how ensemble works.
102
+ Only spawn agents whose tasks are actually needed by this change. Skip agents with no tasks.
103
+
104
+ First spawn all agents (wait for each team_spawn to confirm before the next):
105
+ ```
106
+ team_spawn name:"back" agent:"back-engineer" prompt:"..."
107
+ (wait for result)
108
+ team_spawn name:"front" agent:"front-engineer" prompt:"..."
109
+ (wait for result)
110
+ team_spawn name:"infra" agent:"infra-engineer" prompt:"..."
111
+ (wait for result)
112
+ ```
113
+
114
+ Then immediately send each spawned agent a start message to kick them off:
115
+ ```
116
+ team_message to:"back" text:"Start now. Claim your first task with team_claim and begin implementing."
117
+ team_message to:"front" text:"Start now. Claim your first task with team_claim and begin implementing."
118
+ team_message to:"infra" text:"Start now. Claim your first task with team_claim and begin implementing."
119
+ ```
120
+
121
+ **Step 6e.** After sending start messages, tell the user what is running, then STOP and wait.
122
+ Do NOT call team_results, team_status, or team_broadcast in a loop.
123
+ Teammates will message you when done or blocked. Wait for those messages.
124
+
125
+ **Step 6f.** When a teammate messages back, you receive a ping only, the full content is NOT in the notification.
126
+ Call team_results to read the full message and mark it read. Then for each teammate: team_shutdown → team_merge.
127
+ If team_merge blocks ("overlapping local changes"), commit or stash your local changes first, then retry.
128
+ Fix any other blockers reported.
129
+
130
+ 7. **Quality check**
131
+
132
+ Spawn quality engineer with worktree:false (read-only, no file edits):
133
+ ```
134
+ 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>"
135
+ ```
136
+ Wait for message → team_results → fix blockers → team_shutdown (no team_merge needed, worktree:false)
137
+
138
+ 8. **Mark tasks complete in openspec**
139
+
140
+ Update tasks.md: `- [ ]` → `- [x]` for each completed task.
141
+ Run `rtk openspec status --change "<name>" --json` to confirm.
142
+
143
+ 9. **Show status, then cleanup**
144
+
145
+ Display:
146
+ - Tasks completed this session
147
+ - Overall progress: "N/M tasks complete"
148
+ - If all done: suggest archive with `/opsx-archive`
149
+ - If paused: explain why and wait for guidance
150
+
151
+ Then run `team_cleanup`.
152
+
153
+ **Guardrails**
154
+ - NEVER skip or reorder steps 6a-6f
155
+ - NEVER implement tasks directly. Always use team_create + team_spawn, no exceptions
156
+ - NEVER touch source files before team_create is called, not even one edit
157
+ - NEVER call team_spawn without the agent field, it is required and will fail without it
158
+ - NEVER call team_spawn before team_tasks_add, tasks must exist before agents are spawned
159
+ - NEVER poll team_results or team_status in a loop, wait for teammates to message you
160
+ - NEVER call team_claim or team_tasks_complete as lead, only agents call these tools
161
+ - ALWAYS pass the task IDs returned by team_tasks_add to each agent's spawn prompt
162
+ - NEVER edit files between team_spawn and team_merge, team_merge blocks on overlapping local changes
163
+ - ALWAYS add every task to the board with team_tasks_add before spawning
164
+ - ALWAYS spawn agents sequentially (wait for each team_spawn result before the next), then send start messages to all of them together
165
+ - ALWAYS instruct agents to call team_claim before each task and team_tasks_complete after
166
+ - If teammates are stuck, use team_message to resend tasks, then wait, never implement directly
167
+ - Mark tasks complete in openspec AFTER specialists finish, not before
168
+ - Pause on errors, blockers, or unclear requirements. Do not guess
169
+ - Use contextFiles from CLI output, do not assume specific file paths
170
+ - Use `rtk` wrapper for ALL CLI commands. Never run openspec, git, gh, or az directly
@@ -32,6 +32,22 @@ function resolveAgentName(session) {
32
32
  return "lead"
33
33
  }
34
34
 
35
+ // Maps ensemble tool name → function that extracts the log entry fields from args
36
+ const ENSEMBLE_TOOL_HANDLERS = {
37
+ team_create: (args) => ({ action: "team-created", team: args.name }),
38
+ team_spawn: (args) => ({ action: "teammate-spawned", name: args.name, agentType: args.agent }),
39
+ team_shutdown: (args) => ({ action: "teammate-shutdown", name: args.name }),
40
+ team_merge: (args) => ({ action: "teammate-merged", name: args.name }),
41
+ team_cleanup: () => ({ action: "team-cleanup" }),
42
+ team_status: () => ({ action: "team-status-checked" }),
43
+ team_results: (args) => ({ action: "team-results-read", from: args.from }),
44
+ team_message: (args) => ({ action: "team-message", to: args.to ?? "lead", preview: String(args.text ?? "").slice(0, 120) }),
45
+ team_broadcast: (args) => ({ action: "team-broadcast", preview: String(args.text ?? "").slice(0, 120) }),
46
+ team_tasks_add: (args) => ({ action: "tasks-added", count: Array.isArray(args.tasks) ? args.tasks.length : "?" }),
47
+ team_tasks_complete: (args) => ({ action: "task-completed", taskId: args.task_id }),
48
+ team_claim: (args) => ({ action: "task-claimed", taskId: args.task_id }),
49
+ }
50
+
35
51
  export const SessionLogPlugin = async ({ client, directory }) => {
36
52
  return {
37
53
  event: async ({ event }) => {
@@ -76,7 +92,10 @@ export const SessionLogPlugin = async ({ client, directory }) => {
76
92
  const state = sessionState.get(sessionId)
77
93
  if (!state) return
78
94
 
79
- if (input?.tool === "read") {
95
+ const tool = input?.tool
96
+
97
+ // Track skill loads
98
+ if (tool === "read") {
80
99
  const filePath = input?.args?.filePath ?? ""
81
100
  const match = filePath.match(/[/\\]skills[/\\]([^/\\]+)[/\\]SKILL\.md$/i)
82
101
  if (match) {
@@ -84,7 +103,16 @@ export const SessionLogPlugin = async ({ client, directory }) => {
84
103
  if (!state.skills.includes(skillName)) state.skills.push(skillName)
85
104
  appendEntry(directory, { ts: ts(), agent: state.agentName, action: "skill-loaded", skill: skillName })
86
105
  }
106
+ return
87
107
  }
108
+
109
+ const args = input?.args ?? {}
110
+
111
+ // Track ensemble tool calls
112
+ const ensembleHandler = ENSEMBLE_TOOL_HANDLERS[tool]
113
+ if (!ensembleHandler) return
114
+
115
+ appendEntry(directory, { ts: ts(), agent: state.agentName, ...ensembleHandler(args) })
88
116
  } catch (_) {}
89
117
  },
90
118
  }
@@ -0,0 +1,176 @@
1
+ ---
2
+ name: openspec-apply-change
3
+ description: Implement tasks from an OpenSpec change via ensemble agent team. Use when the user wants to start implementing, continue implementation, or work through tasks.
4
+ license: MIT
5
+ compatibility: Requires openspec CLI and opencode-ensemble plugin.
6
+ metadata:
7
+ author: openspec-onboard
8
+ version: "2.0"
9
+ ---
10
+
11
+ Implement tasks from an OpenSpec change using the ensemble agent team.
12
+
13
+ **Input**: Optionally specify a change name. If omitted, check if it can be inferred from conversation context. If vague or ambiguous you MUST prompt for available changes.
14
+
15
+ **Steps**
16
+
17
+ 1. **Select the change**
18
+
19
+ If a name is provided, use it. Otherwise:
20
+ - Infer from conversation context if the user mentioned a change
21
+ - Auto-select if only one active change exists
22
+ - If ambiguous, run `rtk openspec list --json` to get available changes and use the **AskUserQuestion tool** to let the user select
23
+
24
+ Always announce: "Using change: <name>" and how to override (e.g., `/opsx-apply <other>`).
25
+
26
+ 2. **Check status to understand the schema**
27
+
28
+ ```bash
29
+ rtk openspec status --change "<name>" --json
30
+ ```
31
+
32
+ Parse the JSON to understand:
33
+ - `schemaName`: The workflow being used (e.g., "spec-driven")
34
+ - Which artifact contains the tasks (typically "tasks" for spec-driven, check status for others)
35
+
36
+ 3. **Get apply instructions**
37
+
38
+ ```bash
39
+ rtk openspec instructions apply --change "<name>" --json
40
+ ```
41
+
42
+ This returns:
43
+ - `contextFiles`: artifact ID -> array of concrete file paths (varies by schema - could be proposal/specs/design/tasks or spec/tests/implementation/docs)
44
+ - Progress (total, complete, remaining)
45
+ - Task list with status
46
+ - Dynamic instruction based on current state
47
+
48
+ **Handle states:**
49
+ - If `state: "blocked"` (missing artifacts): show message, suggest using openspec-continue-change
50
+ - If `state: "all_done"`: congratulate, suggest archive with `/opsx-archive`
51
+ - Otherwise: proceed to implementation
52
+
53
+ 4. **Read context files**
54
+
55
+ Read every file path listed under `contextFiles` from the apply instructions output.
56
+ Do NOT tell agents to read files themselves, summarize the content here and pass it in spawn prompts.
57
+
58
+ 5. **Show current progress**
59
+
60
+ Display:
61
+ - Schema being used
62
+ - Progress: "N/M tasks complete"
63
+ - Remaining tasks overview
64
+
65
+ 6. **Implement via ensemble team**
66
+
67
+ NEVER implement tasks directly. Always delegate to specialists via ensemble.
68
+ Do NOT touch any source files before the team is running, not even a single edit.
69
+
70
+ Steps MUST be followed in order. Do not skip any step.
71
+
72
+ **Step 6a.** Create feature branch if not already on one: `feature/{id}-{slug}`
73
+
74
+ **Step 6b.** Create the team:
75
+ ```
76
+ team_create "<change-name>-<random 4 digit number>"
77
+ ```
78
+ Announce: "Team running. Monitor at http://localhost:4747/"
79
+
80
+ **Step 6c.** Add ALL tasks to the shared board BEFORE spawning anyone.
81
+ Schema: { content: string, priority: "high"|"medium"|"low", depends_on?: string[] }
82
+ Use depends_on to block tasks that require other tasks first, pass the IDs returned by team_tasks_add.
83
+ ```
84
+ team_tasks_add tasks:[
85
+ { content: "1.1 <exact task text from tasks.md>", priority: "high" },
86
+ { content: "1.2 <exact task text>", priority: "high" },
87
+ { content: "3.1 <task that needs 1.x done first>", priority: "medium", depends_on: ["<id-of-1.1>"] },
88
+ ...every task, one entry each...
89
+ ]
90
+ ```
91
+ Save the task IDs returned. Pass them to agents in step 6d.
92
+ DO NOT call team_claim yourself, only agents claim tasks.
93
+ DO NOT proceed to 6d until team_tasks_add succeeds.
94
+
95
+ **Step 6d.** Spawn all needed specialists, then kick them off in parallel.
96
+
97
+ Each team_spawn MUST include the agent field (required, causes NOT NULL error if omitted).
98
+
99
+ The spawn prompt must contain exactly:
100
+ 1. Their name and role on this team
101
+ 2. Which tasks are theirs, list the task IDs and content from the board
102
+ 3. Key context they need (summarized from context files, do NOT tell them to read files themselves)
103
+ 4. The 6 OpenCode tools they have available (these are OpenCode tools, NOT shell commands, call them directly as tools, never via bash):
104
+ team_claim, team_tasks_complete, team_tasks_list, team_tasks_add, team_message, team_broadcast
105
+ 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
106
+
107
+ Keep spawn prompts under 500 tokens. Do not describe team internals or how ensemble works.
108
+ Only spawn agents whose tasks are actually needed by this change. Skip agents with no tasks.
109
+
110
+ First spawn all agents (wait for each team_spawn to confirm before the next):
111
+ ```
112
+ team_spawn name:"back" agent:"back-engineer" prompt:"..."
113
+ (wait for result)
114
+ team_spawn name:"front" agent:"front-engineer" prompt:"..."
115
+ (wait for result)
116
+ team_spawn name:"infra" agent:"infra-engineer" prompt:"..."
117
+ (wait for result)
118
+ ```
119
+
120
+ Then immediately send each spawned agent a start message to kick them off:
121
+ ```
122
+ team_message to:"back" text:"Start now. Claim your first task with team_claim and begin implementing."
123
+ team_message to:"front" text:"Start now. Claim your first task with team_claim and begin implementing."
124
+ team_message to:"infra" text:"Start now. Claim your first task with team_claim and begin implementing."
125
+ ```
126
+
127
+ **Step 6e.** After sending start messages, tell the user what is running, then STOP and wait.
128
+ Do NOT call team_results, team_status, or team_broadcast in a loop.
129
+ Teammates will message you when done or blocked. Wait for those messages.
130
+
131
+ **Step 6f.** When a teammate messages back, you receive a ping only, the full content is NOT in the notification.
132
+ Call team_results to read the full message and mark it read. Then for each teammate: team_shutdown → team_merge.
133
+ If team_merge blocks ("overlapping local changes"), commit or stash your local changes first, then retry.
134
+ Fix any other blockers reported.
135
+
136
+ 7. **Quality check**
137
+
138
+ Spawn quality engineer with worktree:false (read-only, no file edits):
139
+ ```
140
+ 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>"
141
+ ```
142
+ Wait for message → team_results → fix blockers → team_shutdown (no team_merge needed, worktree:false)
143
+
144
+ 8. **Mark tasks complete in openspec**
145
+
146
+ Update tasks.md: `- [ ]` → `- [x]` for each completed task.
147
+ Run `rtk openspec status --change "<name>" --json` to confirm.
148
+
149
+ 9. **Show status, then cleanup**
150
+
151
+ Display:
152
+ - Tasks completed this session
153
+ - Overall progress: "N/M tasks complete"
154
+ - If all done: suggest archive with `/opsx-archive`
155
+ - If paused: explain why and wait for guidance
156
+
157
+ Then run `team_cleanup`.
158
+
159
+ **Guardrails**
160
+ - NEVER skip or reorder steps 6a-6f
161
+ - NEVER implement tasks directly. Always use team_create + team_spawn, no exceptions
162
+ - NEVER touch source files before team_create is called, not even one edit
163
+ - NEVER call team_spawn without the agent field, it is required and will fail without it
164
+ - NEVER call team_spawn before team_tasks_add, tasks must exist before agents are spawned
165
+ - NEVER poll team_results or team_status in a loop, wait for teammates to message you
166
+ - NEVER call team_claim or team_tasks_complete as lead, only agents call these tools
167
+ - ALWAYS pass the task IDs returned by team_tasks_add to each agent's spawn prompt
168
+ - NEVER edit files between team_spawn and team_merge, team_merge blocks on overlapping local changes
169
+ - ALWAYS add every task to the board with team_tasks_add before spawning
170
+ - ALWAYS spawn agents sequentially (wait for each team_spawn result before the next), then send start messages to all of them together
171
+ - ALWAYS instruct agents to call team_claim before each task and team_tasks_complete after
172
+ - If teammates are stuck, use team_message to resend tasks, then wait, never implement directly
173
+ - Mark tasks complete in openspec AFTER specialists finish, not before
174
+ - Pause on errors, blockers, or unclear requirements. Do not guess
175
+ - Use contextFiles from CLI output, do not assume specific file paths
176
+ - Use `rtk` wrapper for ALL CLI commands. Never run openspec, git, gh, or az directly
package/content/AGENTS.md CHANGED
@@ -72,14 +72,20 @@ Replace the entire contents of this file (`AGENTS.md`) with everything below the
72
72
  Tell the user:
73
73
 
74
74
  ```
75
- Initialization complete.
75
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
76
+ Initialization complete.
77
+ ━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━
76
78
 
77
79
  - ARCHITECTURE.md generated
78
80
  - DESIGN.md generated
79
81
  - Project history archived in openspec
80
82
  - AGENTS.md updated with real guidance
81
83
 
82
- You're ready to work.
84
+ !! RESTART OPENCODE NOW !!
85
+
86
+ Quit and reopen OpenCode before doing anything else.
87
+ Nothing will work correctly until you do.
88
+ After restarting you are ready to work.
83
89
  ```
84
90
 
85
91
  ---
@@ -108,13 +114,15 @@ This is the agent orchestration layer for your project. It provides:
108
114
 
109
115
  ## I Am the Lead, Full Workflow Ownership
110
116
 
111
- When the user provides a work item URL, says "implement the plan", or "I've added comments to the PR", **I own the full lifecycle**. I load the appropriate skill and use ensemble tools (`team_create`, `team_spawn`, etc.) to coordinate the agent team.
117
+ 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 the appropriate skill and use ensemble tools to coordinate the agent team.
118
+
119
+ Trigger patterns, I recognize ALL of these, exact wording does not matter:
120
+ - 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
121
+ - 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
122
+ - `implement the plan` / `implement` / `start` / `go` → run `/opsx-apply` → ship
123
+ - `I've added comments to the PR` → read PR comments → fix → update PR
112
124
 
113
- Trigger patterns:
114
- - `work on this <azure-devops-url>` → spawn `devops-manager` in read mode → propose OpenSpec → **confirm with user** → implement → ship
115
- - `work on this <github-url>` → spawn `devops-manager` in read mode → propose OpenSpec → **confirm with user** → implement → ship
116
- - `implement the plan` → run `/opsx-apply` (ensemble orchestration is built into the command) → ship
117
- - `I've added comments to the PR` → spawn `devops-manager` in feedback mode → fix → update PR
125
+ **A GitHub or Azure DevOps URL anywhere in the user's message is always a trigger, regardless of surrounding words.**
118
126
 
119
127
  **Never delegate without a plan. Never write implementation code directly, always spawn specialists, no exceptions. "Small feature", "faster to do it directly", or "environment issues" are not valid reasons to skip ensemble.**
120
128
 
@@ -130,11 +138,14 @@ Works on **all platforms** (Windows, macOS, Linux) via OpenCode's built-in workt
130
138
  | `team_shutdown` | Stop a teammate, preserve their branch |
131
139
  | `team_merge` | Merge a teammate's branch into working dir |
132
140
  | `team_cleanup` | Tear down the team |
133
- | `team_results` | Retrieve full message from a teammate |
134
- | `team_message` | Send a direct message to a teammate |
141
+ | `team_results` | Retrieve full message content (delivery is a ping only) |
142
+ | `team_message` | Send a direct message to a teammate or lead |
135
143
  | `team_broadcast` | Message all teammates |
144
+ | `team_status` | View all members and task summary |
145
+ | `team_tasks_list` | View the shared task board |
136
146
  | `team_tasks_add` | Add tasks to shared board |
137
- | `team_tasks_complete` | Mark task done |
147
+ | `team_tasks_complete` | Mark task done, auto-unblocks dependents |
148
+ | `team_claim` | Atomically claim a pending task (teammates use this) |
138
149
 
139
150
  **Dashboard**: Monitor running agents at **http://localhost:4747/**
140
151
 
@@ -151,12 +162,12 @@ devops-manager (read mode)
151
162
 
152
163
  [confirm with user]
153
164
 
154
- front-engineer + back-engineer + infra-engineer ← parallel, only spawn what the task needs
165
+ back-engineer front-engineer infra-engineer ← sequential, one at a time, only spawn what the task needs
155
166
 
156
- quality-engineer
167
+ quality-engineer (worktree:false)
157
168
  → tests, build, lint, acceptance criteria
158
169
 
159
- security-auditor
170
+ security-auditor (worktree:false)
160
171
  → vulnerability audit, secrets, auth gaps
161
172
 
162
173
  devops-manager (ship mode)
@@ -166,32 +177,35 @@ devops-manager (ship mode)
166
177
  ### Phase 1, Parse & Propose
167
178
 
168
179
  ```
169
- 1. team_spawn devops-manager (read mode) fetch work item via skill, output summary
170
- 2. Load skill: openspec-propose generate proposal.md, specs/, tasks.md
171
- - team_create spawn design + specs in parallel → merge → write tasks.md
172
- 3. Show the plan: change name, schema, total tasks, task list summary
173
- 4. STOP. Ask user: "Ready to implement? (yes/no)", DO NOT proceed until confirmed.
180
+ 1. Detect URL typeload matching skill (ob-userstory-gh or ob-userstory-az)
181
+ 2. Follow skill steps: fetch issue/work item via CLI, create OpenSpec change
182
+ 3. Run /opsx-proposegenerates proposal.md, specs/, design.md, tasks.md
183
+ 4. Show the plan: change name, total tasks, task list summary
184
+ 5. STOP. Ask user: "Ready to implement? (yes/no)", DO NOT proceed until confirmed.
174
185
  ```
175
186
 
176
187
  ### Phase 2, Implement
177
188
 
178
189
  ```
179
190
  1. Run /opsx-apply, handles context reading, ensemble orchestration, and task marking.
191
+ - Lead adds all tasks to board, then spawns specialists ONE AT A TIME (not parallel)
192
+ - Each specialist claims tasks, implements, completes tasks, messages lead when done
193
+ - Lead merges each branch after shutdown, then marks tasks done in tasks.md
180
194
  2. After /opsx-apply completes, proceed to quality check.
181
195
  ```
182
196
 
183
197
  ### Phase 3, Quality
184
198
 
185
199
  ```
186
- 7. team_spawn name:quality agent:quality-engineer → tests, build, lint
187
- 8. Wait → team_results → fix any blockers → team_shutdown
200
+ 3. team_spawn name:quality agent:quality-engineer worktree:false → tests, build, lint
201
+ 4. Wait → team_results → fix any blockers → team_shutdown (no merge, worktree:false)
188
202
  ```
189
203
 
190
204
  ### Phase 4, Security
191
205
 
192
206
  ```
193
- 9. team_spawn name:security agent:security-auditor → audit full change
194
- 10. Wait → team_results → fix Critical findings → team_shutdown
207
+ 5. team_spawn name:security agent:security-auditor worktree:false → audit full change
208
+ 6. Wait → team_results → fix Critical findings → team_shutdown (no merge, worktree:false)
195
209
  ```
196
210
 
197
211
  ### Phase 5, Ship
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-onboard",
3
- "version": "0.1.12",
3
+ "version": "0.2.0",
4
4
  "description": "Prepare any brownfield codebase for AI agent workflows using OpenCode, OpenSpec, and ensemble orchestration.",
5
5
  "keywords": [
6
6
  "opencode",
package/src/index.js CHANGED
@@ -10,6 +10,8 @@ import { chooseSkillsProvider } from './steps/choose-skills-provider.js'
10
10
  import { cleanAiFiles } from './steps/clean-ai-files.js'
11
11
  import { copyContentStep } from './steps/copy-content.js'
12
12
  import { initOpenspec } from './steps/init-openspec.js'
13
+ import { patchAgentsMd } from './steps/patch-agents-md.js'
14
+ import { installBrowser } from './steps/install-browser.js'
13
15
 
14
16
  if (process.stdout.isTTY) console.clear()
15
17
  console.log()
@@ -57,8 +59,8 @@ try {
57
59
  // 1. Check Node + pnpm
58
60
  await checkEnv()
59
61
 
60
- // 2. Clean existing AI config files
61
- await cleanAiFiles()
62
+ // 2. Clean existing AI config files, detect preserved state
63
+ const ctx = await cleanAiFiles()
62
64
 
63
65
  // 3. Choose platform
64
66
  const platform = await choosePlatform()
@@ -67,7 +69,10 @@ try {
67
69
  await checkPlatform(platform)
68
70
 
69
71
  // 5. Copy content
70
- await copyContentStep(platform)
72
+ await copyContentStep(platform, ctx)
73
+
74
+ // 5b. Patch AGENTS.md to skip steps for already-existing files
75
+ await patchAgentsMd(ctx)
71
76
 
72
77
  // 6. Init OpenSpec
73
78
  await initOpenspec()
@@ -85,16 +90,25 @@ try {
85
90
  await installBrowser()
86
91
 
87
92
  // Done
93
+ const toGenerate = [
94
+ !ctx.hasDesign && 'DESIGN.md',
95
+ !ctx.hasArchitecture && 'ARCHITECTURE.md',
96
+ ].filter(Boolean)
97
+
88
98
  console.log()
89
99
  console.log(chalk.bold.green('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'))
90
100
  console.log(chalk.bold.green(' Onboarding complete!'))
91
101
  console.log(chalk.bold.green('━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━'))
92
102
  console.log()
93
- console.log(' Next step:')
94
- console.log(chalk.hex('#fe3d57')(' Open OpenCode in this project and type: ') + chalk.bold('"init"'))
103
+ console.log(' Open this project in OpenCode and type:')
104
+ console.log(chalk.bold(' "init"'))
95
105
  console.log()
96
- console.log(' OpenCode will generate ARCHITECTURE.md and DESIGN.md')
97
- console.log(' from your actual codebase, then activate the agent team.')
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
+ }
98
112
  console.log()
99
113
  } catch (err) {
100
114
  if (err.name === 'ExitPromptError') {
@@ -91,20 +91,21 @@ export async function chooseModels() {
91
91
  console.log()
92
92
 
93
93
  // Plan model
94
- info('PLAN model, used by the main agent for proposals, specs, architecture decisions.')
95
- info('Pick something capable with strong reasoning.')
94
+ info('PLAN model: used by the main agent to read issues, write proposals, coordinate the team.')
95
+ info('This model needs to be strong. Use Claude Sonnet/Opus, GPT-4o, o3, or equivalent.')
96
+ info('A weak model here will silently skip steps and break the workflow.')
96
97
  const planModel = await pickModel('Plan model:', models)
97
98
  console.log()
98
99
 
99
100
  // Build model
100
- info('BUILD model, used by front-engineer, back-engineer, infra-engineer, quality-engineer, security-auditor.')
101
- info('Pick something capable for implementation work.')
101
+ info('BUILD model: used by front-engineer, back-engineer, infra-engineer, quality-engineer, security-auditor.')
102
+ info('Needs to be capable for implementation work. Claude Sonnet, GPT-4o, or equivalent.')
102
103
  const buildModel = await pickModel('Build model:', models)
103
104
  console.log()
104
105
 
105
106
  // Fast model
106
- info('FAST model, used by devops-manager for reading issues, classifying PR comments.')
107
- info('Pick something fast and cheap, no heavy reasoning needed.')
107
+ info('FAST model: used by devops-manager for reading issues and classifying PR comments.')
108
+ info('Something fast and cheap is fine here, no heavy reasoning needed.')
108
109
  const fastModel = await pickModel('Fast model:', models)
109
110
  console.log()
110
111
 
@@ -1,11 +1,14 @@
1
1
  import fse from 'fs-extra'
2
2
  import path from 'path'
3
3
  import { findAiFiles } from '../utils/copy.js'
4
- import { header, info, prompt, success, warn } from '../utils/exec.js'
4
+ import { header, info, success, warn } from '../utils/exec.js'
5
+
6
+ // Files/dirs that are valuable pre-existing work, never removed
7
+ const PRESERVE = ['DESIGN.md', 'ARCHITECTURE.md', 'openspec']
5
8
 
6
9
  /**
7
- * Enumerate immediate children of a directory, returning their absolute paths.
8
- * Skips any entry named 'skills' at any level to preserve user-installed skills.
10
+ * Enumerate immediate children of a directory.
11
+ * Skips 'skills' to preserve user-installed skills.
9
12
  */
10
13
  async function childrenExcludingSkills(dir) {
11
14
  const results = []
@@ -18,15 +21,55 @@ async function childrenExcludingSkills(dir) {
18
21
  return results
19
22
  }
20
23
 
24
+ /**
25
+ * Returns true if the file exists and has real content (not empty, not a prompt template).
26
+ * Prompt templates contain a specific marker written by the onboard CLI.
27
+ */
28
+ async function isPopulated(filePath) {
29
+ if (!await fse.pathExists(filePath)) return false
30
+ const content = await fse.readFile(filePath, 'utf-8')
31
+ const trimmed = content.trim()
32
+ if (!trimmed) return false
33
+ // DESIGN.md and ARCHITECTURE.md shipped as prompt templates contain this marker
34
+ if (trimmed.startsWith('<!-- onboard-prompt')) return false
35
+ return true
36
+ }
37
+
38
+ /**
39
+ * Returns true if openspec/ exists and has at least one change or archive entry.
40
+ */
41
+ async function hasOpenspecHistory(cwd) {
42
+ const changesDir = path.join(cwd, 'openspec', 'changes')
43
+ const archiveDir = path.join(cwd, 'openspec', 'archive')
44
+ if (await fse.pathExists(changesDir)) {
45
+ const entries = await fse.readdir(changesDir)
46
+ if (entries.length > 0) return true
47
+ }
48
+ if (await fse.pathExists(archiveDir)) {
49
+ const entries = await fse.readdir(archiveDir)
50
+ if (entries.length > 0) return true
51
+ }
52
+ return false
53
+ }
54
+
21
55
  export async function cleanAiFiles() {
22
56
  header('Step 2, Existing AI config files')
23
57
 
24
58
  const cwd = process.cwd()
25
59
 
26
- // Flat AI config files (not directories)
27
- const flatFiles = await findAiFiles(cwd)
60
+ // Detect what should be preserved before touching anything
61
+ const ctx = {
62
+ hasDesign: await isPopulated(path.join(cwd, 'DESIGN.md')),
63
+ hasArchitecture: await isPopulated(path.join(cwd, 'ARCHITECTURE.md')),
64
+ hasOpenspec: await hasOpenspecHistory(cwd),
65
+ }
28
66
 
29
- // For directory targets (.opencode, .agents), enumerate children and skip skills/
67
+ if (ctx.hasDesign) info('DESIGN.md exists and is populated, keeping it')
68
+ if (ctx.hasArchitecture) info('ARCHITECTURE.md exists and is populated, keeping it')
69
+ if (ctx.hasOpenspec) info('openspec/ history exists, keeping it')
70
+
71
+ // Build the list of files to remove
72
+ const flatFiles = await findAiFiles(cwd)
30
73
  const dirTargets = ['.opencode', '.agents']
31
74
  const dirEntries = []
32
75
  for (const dirName of dirTargets) {
@@ -35,37 +78,28 @@ export async function cleanAiFiles() {
35
78
  dirEntries.push(...children)
36
79
  }
37
80
 
38
- // Remove the directory targets themselves from flat list (we handle them via children)
81
+ // Remove directory targets themselves from flat list (handled via children)
82
+ // Also remove any preserved entries
39
83
  const filteredFlat = flatFiles.filter(f => {
40
84
  const rel = path.relative(cwd, f)
41
- return !dirTargets.includes(rel)
85
+ if (dirTargets.includes(rel)) return false
86
+ if (PRESERVE.some(p => rel === p || rel.startsWith(p + path.sep))) return false
87
+ return true
42
88
  })
43
89
 
44
- const allFiles = [...filteredFlat, ...dirEntries]
90
+ const allToRemove = [...filteredFlat, ...dirEntries]
45
91
 
46
- if (allFiles.length === 0) {
47
- success('No existing AI config files found')
48
- return
92
+ if (allToRemove.length === 0) {
93
+ success('No existing AI config files to remove')
94
+ return ctx
49
95
  }
50
96
 
51
- warn('Found the following AI config files:')
52
- for (const f of allFiles) {
53
- info(f.replace(cwd, '.'))
54
- }
55
- console.log()
56
- prompt('Press Enter to remove them all (skills/ folders will be kept)')
57
- console.log()
58
-
59
- await new Promise(resolve => {
60
- process.stdin.resume()
61
- process.stdin.once('data', () => {
62
- process.stdin.pause()
63
- resolve()
64
- })
65
- })
66
-
67
- for (const f of allFiles) {
97
+ warn('Removing existing AI config files:')
98
+ for (const f of allToRemove) {
99
+ info(' ' + f.replace(cwd + path.sep, ''))
68
100
  await fse.remove(f)
69
101
  }
70
102
  success('Removed existing AI config files')
103
+
104
+ return ctx
71
105
  }
@@ -6,13 +6,13 @@ import { error, header, success } from '../utils/exec.js'
6
6
  const __dirname = path.dirname(fileURLToPath(import.meta.url))
7
7
  const CONTENT_DIR = path.resolve(__dirname, '../../content')
8
8
 
9
- export async function copyContentStep(platform) {
9
+ export async function copyContentStep(platform, ctx = {}) {
10
10
  header('Step 5, Copying opencode-onboard files')
11
11
 
12
12
  const dest = process.cwd()
13
13
 
14
14
  try {
15
- await copyContent(CONTENT_DIR, dest, platform)
15
+ await copyContent(CONTENT_DIR, dest, platform, ctx)
16
16
  success('Files copied to project root')
17
17
  } catch (err) {
18
18
  error(`Failed to copy content: ${err.message}`)
@@ -1,130 +1,26 @@
1
1
  import { execa } from 'execa'
2
- import fs from 'node:fs'
2
+ import fse from 'fs-extra'
3
3
  import path from 'node:path'
4
+ import { fileURLToPath } from 'node:url'
4
5
  import { error, header, success, warn } from '../utils/exec.js'
5
6
 
6
- const ENSEMBLE_PATCH = `6. **Implement via ensemble team**
7
-
8
- NEVER implement tasks directly. Always delegate to specialists via ensemble.
9
- Do NOT touch any source files before the team is running, not even a single edit.
10
-
11
- Steps MUST be followed in order. Do not skip any step.
12
-
13
- **Step 6a.** Create feature branch if not already on one: \`feature/{id}-{slug}\`
14
-
15
- **Step 6b.** Clean up stale state, then create the team:
16
- \`\`\`
17
- team_cleanup force:true acknowledge_uncommitted:true
18
- team_create "<change-name>"
19
- \`\`\`
20
- "not in a team" error from team_cleanup is expected, ignore it.
21
- Announce: "Team running. Monitor at http://localhost:4747/"
22
-
23
- **Step 6c.** Add ALL tasks to the shared board BEFORE spawning anyone.
24
- Schema: { content: string, priority: "high"|"medium"|"low" }. No other fields.
25
- \`\`\`
26
- team_tasks_add tasks:[
27
- { content: "1.1 <exact task text from tasks.md>", priority: "high" },
28
- { content: "1.2 <exact task text>", priority: "high" },
29
- ...every task from tasks.md, one entry each...
30
- ]
31
- \`\`\`
32
- Save the task IDs returned. You MUST pass them to agents in step 6d.
33
- DO NOT proceed to 6d until team_tasks_add succeeds.
34
-
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, all context file paths, the task IDs for their tasks, and these instructions:
38
- - call team_claim with the task ID before starting each task
39
- - call team_tasks_complete with the task ID after finishing each task
40
- - send results to lead via team_message when all done or blocked
41
- \`\`\`
42
- team_spawn name:"back" agent:"back-engineer" prompt:"..."
43
- (wait for result)
44
- team_spawn name:"front" agent:"front-engineer" prompt:"..."
45
- (wait for result)
46
- team_spawn name:"infra" agent:"infra-engineer" prompt:"..."
47
- (wait for result)
48
- \`\`\`
49
-
50
- **Step 6e.** After all spawns, tell the user what is running, then STOP and wait.
51
- Do NOT call team_results, team_status, or team_broadcast in a loop.
52
- Teammates will message you when done or blocked. Wait for those messages.
53
-
54
- **Step 6f.** When teammates message back: call team_results to get full output,
55
- then team_shutdown + team_merge for each. Fix any blockers reported.
56
-
57
- 7. **Quality check**
58
-
59
- Spawn quality engineer, wait for message back:
60
- \`\`\`
61
- team_spawn name:"quality" agent:"quality-engineer" prompt:"<task list, context files, run tests + build + lint + verify acceptance criteria, call team_claim/team_tasks_complete per task, send results to lead when done>"
62
- \`\`\`
63
- Wait for message → team_results → fix blockers → team_shutdown + team_merge
64
-
65
- 8. **Mark tasks complete in openspec**
66
-
67
- Update tasks.md: \`- [ ]\` → \`- [x]\` for each completed task.
68
- Run \`rtk openspec status --change "<name>" --json\` to confirm.
69
-
70
- 9. **Show status, then cleanup**
71
-
72
- Display:
73
- - Tasks completed this session
74
- - Overall progress: "N/M tasks complete"
75
- - If all done: suggest archive with \`/opsx-archive\`
76
- - If paused: explain why and wait for guidance
77
-
78
- Then run \`team_cleanup\`.
79
-
80
- **Guardrails**
81
- - NEVER skip or reorder steps 6a-6f
82
- - NEVER implement tasks directly. Always use team_create + team_spawn, no exceptions
83
- - NEVER touch source files before team_create is called, not even one edit
84
- - NEVER call team_spawn without the agent field — it is required and will fail without it
85
- - NEVER call team_spawn before team_tasks_add — tasks must exist before agents are spawned
86
- - NEVER poll team_results or team_status in a loop — wait for teammates to message you
87
- - NEVER claim or complete tasks as lead — only subagents call team_claim and team_tasks_complete
88
- - ALWAYS run team_cleanup force:true before team_create to clear stale state
89
- - ALWAYS add every task from tasks.md to the board with team_tasks_add before spawning
90
- - ALWAYS pass the task IDs returned by team_tasks_add to each agent's spawn prompt
91
- - ALWAYS spawn one at a time, waiting for each result before the next (avoids worktree contention)
92
- - If teammates are stuck, use team_message to resend tasks, then wait — never implement directly
93
- - Mark tasks complete in openspec AFTER specialists finish, not before
94
- - Pause on errors, blockers, or unclear requirements. Do not guess
95
- - Use contextFiles from CLI output, do not assume specific file paths
96
- `
97
-
98
- // Patterns that identify the solo implementation step in openspec-generated files
99
- const SOLO_IMPL_PATTERNS = [
100
- /^#{1,3}\s+\d+\..*(implement|loop until|make the code changes|for each (pending )?task)/im,
101
- /\*\*Implement tasks.*loop until/im,
102
- /^6\.\s+\*\*Implement tasks/im,
7
+ const __dirname = path.dirname(fileURLToPath(import.meta.url))
8
+
9
+ // Our owned apply command and skill, stored in the package content folder.
10
+ // After openspec init generates its versions, we delete them and copy ours in.
11
+ const OUR_CONTENT_DIR = path.resolve(__dirname, '../../content')
12
+
13
+ const APPLY_OVERRIDES = [
14
+ {
15
+ src: path.join(OUR_CONTENT_DIR, '.opencode', 'commands', 'opsx-apply.md'),
16
+ dest: path.join('.opencode', 'commands', 'opsx-apply.md'),
17
+ },
18
+ {
19
+ src: path.join(OUR_CONTENT_DIR, '.opencode', 'skills', 'openspec-apply-change', 'SKILL.md'),
20
+ dest: path.join('.opencode', 'skills', 'openspec-apply-change', 'SKILL.md'),
21
+ },
103
22
  ]
104
23
 
105
- function patchOpensxApply(filePath) {
106
- if (!fs.existsSync(filePath)) return false
107
-
108
- const content = fs.readFileSync(filePath, 'utf8')
109
- const lines = content.split('\n')
110
-
111
- // Find the line index where the solo implementation step starts
112
- let cutLine = -1
113
- for (let i = 0; i < lines.length; i++) {
114
- if (SOLO_IMPL_PATTERNS.some(p => p.test(lines[i]))) {
115
- cutLine = i
116
- break
117
- }
118
- }
119
-
120
- if (cutLine === -1) return false // Pattern not found, skip
121
-
122
- const preamble = lines.slice(0, cutLine).join('\n').trimEnd()
123
- const patched = preamble + '\n\n' + ENSEMBLE_PATCH
124
- fs.writeFileSync(filePath, patched, 'utf8')
125
- return true
126
- }
127
-
128
24
  export async function initOpenspec() {
129
25
  header('Step 6, Initializing OpenSpec')
130
26
 
@@ -144,19 +40,15 @@ export async function initOpenspec() {
144
40
  error(`Failed to run openspec init: ${err.message}`)
145
41
  }
146
42
 
147
- // Patch opsx-apply.md to use ensemble orchestration instead of solo implementation
148
- const targets = [
149
- path.join(process.cwd(), '.opencode', 'commands', 'opsx-apply.md'),
150
- path.join(process.cwd(), '.opencode', 'skills', 'openspec-apply-change', 'SKILL.md'),
151
- ]
152
-
153
- for (const target of targets) {
43
+ // Replace the openspec-generated apply command and skill with our ensemble-native versions.
44
+ // The generated files implement tasks directly (solo agent). Ours delegate to the ensemble team.
45
+ for (const { src, dest } of APPLY_OVERRIDES) {
46
+ const destAbs = path.join(process.cwd(), dest)
154
47
  try {
155
- const patched = patchOpensxApply(target)
156
- if (patched) success(`Patched ${path.relative(process.cwd(), target)} for ensemble`)
48
+ await fse.copy(src, destAbs, { overwrite: true })
49
+ success(`Installed ensemble apply → ${dest}`)
157
50
  } catch (err) {
158
- warn(`Could not patch ${path.relative(process.cwd(), target)}: ${err.message}`)
51
+ warn(`Could not install ${dest}: ${err.message}`)
159
52
  }
160
53
  }
161
54
  }
162
-
@@ -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
  }