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.
- package/content/.agents/agents/back-engineer.md +10 -0
- package/content/.agents/agents/front-engineer.md +10 -0
- package/content/.agents/agents/quality-engineer.md +9 -0
- package/content/.opencode/commands/opsx-apply.md +170 -0
- package/content/.opencode/plugins/session-log.js +29 -1
- package/content/.opencode/skills/openspec-apply-change/SKILL.md +176 -0
- package/content/AGENTS.md +37 -23
- package/package.json +1 -1
- package/src/index.js +21 -7
- package/src/steps/choose-models.js +7 -6
- package/src/steps/clean-ai-files.js +63 -29
- package/src/steps/copy-content.js +2 -2
- package/src/steps/init-openspec.js +24 -132
- package/src/steps/patch-agents-md.js +85 -0
- package/src/utils/copy.js +20 -6
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
170
|
-
2.
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
180
|
+
1. Detect URL type → load 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-propose → generates 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
|
-
|
|
187
|
-
|
|
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
|
-
|
|
194
|
-
|
|
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
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('
|
|
94
|
-
console.log(chalk.
|
|
103
|
+
console.log(' Open this project in OpenCode and type:')
|
|
104
|
+
console.log(chalk.bold(' "init"'))
|
|
95
105
|
console.log()
|
|
96
|
-
|
|
97
|
-
|
|
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
|
|
95
|
-
info('
|
|
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
|
|
101
|
-
info('
|
|
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
|
|
107
|
-
info('
|
|
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,
|
|
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
|
|
8
|
-
* Skips
|
|
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
|
-
//
|
|
27
|
-
const
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
90
|
+
const allToRemove = [...filteredFlat, ...dirEntries]
|
|
45
91
|
|
|
46
|
-
if (
|
|
47
|
-
success('No existing AI config files
|
|
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('
|
|
52
|
-
for (const f of
|
|
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
|
|
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
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
-
//
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
path.join(process.cwd(),
|
|
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
|
-
|
|
156
|
-
|
|
48
|
+
await fse.copy(src, destAbs, { overwrite: true })
|
|
49
|
+
success(`Installed ensemble apply → ${dest}`)
|
|
157
50
|
} catch (err) {
|
|
158
|
-
warn(`Could not
|
|
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
|
|
9
|
-
*
|
|
14
|
+
* Copy content/ directory to destination.
|
|
15
|
+
* Excludes:
|
|
16
|
+
* - .agents/skills and .opencode/skills (handled separately)
|
|
17
|
+
* - .bootstrap (internal tooling)
|
|
18
|
+
* - node_modules
|
|
19
|
+
* - opsx-apply.md and openspec-apply-change/SKILL.md (installed by initOpenspec)
|
|
20
|
+
* - DESIGN.md and ARCHITECTURE.md if ctx says they already exist (preserve user's files)
|
|
10
21
|
* @param {string} contentDir - absolute path to content/
|
|
11
22
|
* @param {string} destDir - absolute path to destination (project root)
|
|
12
23
|
* @param {'azure'|'github'} platform
|
|
24
|
+
* @param {{ hasDesign?: boolean, hasArchitecture?: boolean }} ctx
|
|
13
25
|
*/
|
|
14
|
-
export async function copyContent(contentDir, destDir, platform) {
|
|
26
|
+
export async function copyContent(contentDir, destDir, platform, ctx = {}) {
|
|
15
27
|
await fse.copy(contentDir, destDir, {
|
|
16
28
|
overwrite: false,
|
|
17
29
|
filter: (src) => {
|
|
18
30
|
const rel = path.relative(contentDir, src)
|
|
19
31
|
const parts = rel.split(path.sep)
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
)
|
|
32
|
+
if (parts.some(part => ALWAYS_EXCLUDE.some(pattern => part.includes(pattern)))) return false
|
|
33
|
+
if (OPENSPEC_APPLY_FILES.some(f => rel === f)) return false
|
|
34
|
+
if (ctx.hasDesign && rel === 'DESIGN.md') return false
|
|
35
|
+
if (ctx.hasArchitecture && rel === 'ARCHITECTURE.md') return false
|
|
36
|
+
return true
|
|
23
37
|
},
|
|
24
38
|
})
|
|
25
39
|
}
|