opencode-onboard 0.4.2 → 0.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +303 -301
- package/content/.agents/agents/basic-engineer.md +4 -2
- package/content/.agents/skills/ob-global/SKILL.md +35 -0
- package/content/.opencode/commands/init.md +1 -1
- package/content/.opencode/commands/main.md +1 -1
- package/content/.opencode/commands/opsx-apply.md +131 -70
- package/content/.opencode/commands/plan.md +1 -1
- package/content/.opencode/skills/openspec-apply-change/SKILL.md +86 -64
- package/content/AGENTS.md +48 -22
- package/package.json +1 -1
- package/src/commands/join.js +3 -3
- package/src/commands/single.js +2 -0
- package/src/commands/wizard.js +15 -1
- package/src/presets/source.json +7 -1
- package/src/steps/copy/agents.js +28 -0
- package/src/steps/copy/copy.test.js +1 -0
- package/src/steps/copy/index.js +2 -1
- package/src/steps/metadata/index.js +1 -0
- package/src/steps/openspec/ensemble.js +30 -7
- package/src/steps/source/index.js +48 -0
- package/src/steps/source/source.test.js +33 -0
package/content/AGENTS.md
CHANGED
|
@@ -63,13 +63,30 @@ The output must be a real, populated `ARCHITECTURE.md` based on what you found i
|
|
|
63
63
|
|
|
64
64
|
---
|
|
65
65
|
|
|
66
|
-
### Step 4,
|
|
66
|
+
### Step 4, Populate OpenSpec config
|
|
67
|
+
|
|
68
|
+
Read `openspec/config.yaml`. It contains a template with commented-out examples. Fill in the `context:` field with real project information discovered during steps 1-3:
|
|
69
|
+
|
|
70
|
+
```yaml
|
|
71
|
+
context: |
|
|
72
|
+
Tech stack: <languages, frameworks, libraries found in the codebase>
|
|
73
|
+
Build system: <build tools, package managers>
|
|
74
|
+
Architecture: <monolith, microservices, monorepo, etc.>
|
|
75
|
+
Conventions: <coding style, commit conventions, branching strategy if found>
|
|
76
|
+
Domain: <what this project does, in one line>
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
Keep the `schema: spec-driven` line. Add `rules:` only if the codebase has clear conventions worth enforcing (e.g., max task size, proposal format). Do not invent rules that aren't evidenced by the codebase.
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
83
|
+
### Step 5, Rewrite this file
|
|
67
84
|
|
|
68
85
|
Replace the entire contents of this file (`AGENTS.md`) with everything below the line `<!-- AGENTS-TEMPLATE-START -->` in this same file. Delete the bootstrap section and the template marker, the file should contain only the template content when done.
|
|
69
86
|
|
|
70
87
|
---
|
|
71
88
|
|
|
72
|
-
### Step
|
|
89
|
+
### Step 6, Confirm
|
|
73
90
|
|
|
74
91
|
Tell the user:
|
|
75
92
|
|
|
@@ -80,6 +97,7 @@ Tell the user:
|
|
|
80
97
|
|
|
81
98
|
- ARCHITECTURE.md generated
|
|
82
99
|
- DESIGN.md generated
|
|
100
|
+
- openspec/config.yaml populated
|
|
83
101
|
- Project history archived in openspec
|
|
84
102
|
- AGENTS.md updated with real guidance
|
|
85
103
|
|
|
@@ -98,7 +116,7 @@ After restarting you are ready to work.
|
|
|
98
116
|
- Do NOT create branches or PRs
|
|
99
117
|
- Do NOT modify any project source files
|
|
100
118
|
- Do NOT create CLI wrapper files or scripts
|
|
101
|
-
- Only read source files for analysis, write only to ARCHITECTURE.md, DESIGN.md, AGENTS.md, and openspec/
|
|
119
|
+
- Only read source files for analysis, write only to ARCHITECTURE.md, DESIGN.md, AGENTS.md, openspec/config.yaml, and openspec/
|
|
102
120
|
|
|
103
121
|
<!-- AGENTS-TEMPLATE-START -->
|
|
104
122
|
# AGENTS.md
|
|
@@ -138,18 +156,25 @@ Trigger patterns, I recognize ALL of these, exact wording does not matter:
|
|
|
138
156
|
|
|
139
157
|
**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.**
|
|
140
158
|
|
|
141
|
-
## Multi-Agent Execution, opencode-ensemble
|
|
142
|
-
|
|
143
|
-
Parallel execution uses the `opencode-ensemble` plugin (`team_create`, `team_spawn`, etc.).
|
|
144
|
-
Works on **all platforms** (Windows, macOS, Linux) via OpenCode's built-in worktree support.
|
|
145
|
-
|
|
146
|
-
Core tools used in this workflow:
|
|
147
|
-
- `team_create`, `team_spawn`, `team_shutdown`, `team_merge`, `team_cleanup`
|
|
148
|
-
- `team_tasks_add`, `team_tasks_list`, `team_claim`, `team_tasks_complete`
|
|
149
|
-
- `team_message`, `team_results`, `team_status`
|
|
159
|
+
## Multi-Agent Execution, opencode-ensemble
|
|
160
|
+
|
|
161
|
+
Parallel execution uses the `opencode-ensemble` plugin (`team_create`, `team_spawn`, etc.).
|
|
162
|
+
Works on **all platforms** (Windows, macOS, Linux) via OpenCode's built-in worktree support.
|
|
163
|
+
|
|
164
|
+
Core tools used in this workflow:
|
|
165
|
+
- `team_create`, `team_spawn`, `team_shutdown`, `team_merge`, `team_cleanup`
|
|
166
|
+
- `team_tasks_add`, `team_tasks_list`, `team_claim`, `team_tasks_complete`
|
|
167
|
+
- `team_message`, `team_results`, `team_status`
|
|
150
168
|
|
|
151
169
|
**Dashboard**: Monitor running agents at **http://localhost:4747/**
|
|
152
170
|
|
|
171
|
+
**Hard limits:**
|
|
172
|
+
- **Max {{MAX_CONCURRENT_AGENTS}} truly concurrent agents.** All {{MAX_CONCURRENT_AGENTS}} must be spawned and running simultaneously, not sequentially. Spawn in waves if more than {{MAX_CONCURRENT_AGENTS}} are needed. Wait for wave N to finish before spawning wave N+1.
|
|
173
|
+
- **Non-overlapping file domains.** Each agent owns exclusive directories. Two agents must NEVER touch the same file.
|
|
174
|
+
- **Immediate shutdown on completion.** The moment an agent's domain has no more pending tasks → `team_shutdown` → `team_merge`. Keep agents alive if more tasks in their domain are pending (rolling batch).
|
|
175
|
+
- **Rolling batch assignment.** Agents receive up to 3 tasks initially. When they complete a batch, lead assigns the next batch of up to 3 from the board. Never leave pending tasks orphaned.
|
|
176
|
+
- **Stall detection at 5 minutes.** No commits after 5 min → nudge message → 2 min grace → force shutdown + respawn.
|
|
177
|
+
|
|
153
178
|
**Progress inspection commands (tell user explicitly after spawning):**
|
|
154
179
|
- `team_status` for live team snapshot
|
|
155
180
|
- `team_tasks_list` for task board state
|
|
@@ -159,7 +184,7 @@ Core tools used in this workflow:
|
|
|
159
184
|
If a teammate stalls due to model quota/rate-limit exhaustion:
|
|
160
185
|
1. `team_shutdown name:"<stuck-member>" force:true`
|
|
161
186
|
2. `team_spawn` same member/task with an available model
|
|
162
|
-
3. `team_message` start instruction with the exact next task ID
|
|
187
|
+
3. `team_message` start instruction with the exact next task ID
|
|
163
188
|
|
|
164
189
|
---
|
|
165
190
|
|
|
@@ -191,15 +216,16 @@ devops-manager (ship mode)
|
|
|
191
216
|
5. STOP. Ask user: "Ready to implement? (yes/no)", DO NOT proceed until confirmed.
|
|
192
217
|
```
|
|
193
218
|
|
|
194
|
-
### Phase 2, Implement
|
|
195
|
-
|
|
196
|
-
```
|
|
197
|
-
1. Run /opsx-apply.
|
|
198
|
-
- Lead adds all tasks to board.
|
|
199
|
-
- Lead spawns
|
|
200
|
-
- Each engineer
|
|
201
|
-
- Lead
|
|
202
|
-
|
|
219
|
+
### Phase 2, Implement
|
|
220
|
+
|
|
221
|
+
```
|
|
222
|
+
1. Run /opsx-apply.
|
|
223
|
+
- Lead adds all tasks to board.
|
|
224
|
+
- Lead spawns engineers with initial batch of up to 3 tasks each (rolling batch model).
|
|
225
|
+
- Each engineer claims tasks, implements, completes, messages lead.
|
|
226
|
+
- Lead assigns next batch (up to 3) to agents that report done. Repeat until board empty.
|
|
227
|
+
- Lead merges each engineer branch after shutdown, then marks tasks done in tasks.md.
|
|
228
|
+
2. Verify with tests/build/lint according to task scope.
|
|
203
229
|
```
|
|
204
230
|
|
|
205
231
|
### Phase 3, Ship
|
package/package.json
CHANGED
package/src/commands/join.js
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
import chalk from 'chalk'
|
|
2
|
-
import { header, info } from '../utils/exec.js'
|
|
3
2
|
import { installBrowser } from '../steps/browser/index.js'
|
|
4
3
|
import { checkRtk } from '../steps/optimization/index.js'
|
|
5
|
-
import {
|
|
4
|
+
import { checkPlatform, choosePlatform } from '../steps/platform/index.js'
|
|
5
|
+
import { header, info } from '../utils/exec.js'
|
|
6
6
|
import { readOnboardConfig } from './shared.js'
|
|
7
7
|
|
|
8
8
|
export async function runJoin() {
|
|
9
9
|
const logo = chalk.hex('#fe3d57')
|
|
10
10
|
console.log()
|
|
11
11
|
console.log(logo(' 🤝 opencode-onboard join'))
|
|
12
|
-
console.log(chalk.dim(' New team member setup
|
|
12
|
+
console.log(chalk.dim(' New team member setup, checks & local installs only'))
|
|
13
13
|
console.log(chalk.dim(' This will NOT modify any project files.'))
|
|
14
14
|
console.log()
|
|
15
15
|
|
package/src/commands/single.js
CHANGED
|
@@ -17,6 +17,7 @@ export async function runSingleCommand(command) {
|
|
|
17
17
|
hasOpenspec: !!savedWizard?.preserved?.openspec,
|
|
18
18
|
sourceMode: savedWizard?.sourceMode ?? 'current',
|
|
19
19
|
sourceRoots: Array.isArray(savedWizard?.sourceRoots) ? savedWizard.sourceRoots : [],
|
|
20
|
+
maxConcurrentAgents: savedWizard?.maxConcurrentAgents ?? 4,
|
|
20
21
|
}
|
|
21
22
|
const platform = savedWizard?.platform
|
|
22
23
|
const resolvedPlatform = platform === 'azure' || platform === 'github' ? platform : 'github'
|
|
@@ -47,6 +48,7 @@ export async function runSingleCommand(command) {
|
|
|
47
48
|
await writeOnboardConfig({
|
|
48
49
|
...ctx,
|
|
49
50
|
platform: resolvedPlatform,
|
|
51
|
+
maxConcurrentAgents: savedWizard?.maxConcurrentAgents ?? 4,
|
|
50
52
|
additionalSkillsProvider: 'npx-skills',
|
|
51
53
|
planModel: savedWizard?.models?.plan ?? null,
|
|
52
54
|
buildModel: savedWizard?.models?.build ?? null,
|
package/src/commands/wizard.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { select as wizardSelect } from '@inquirer/prompts'
|
|
1
2
|
import chalk from 'chalk'
|
|
2
3
|
import { chooseSourceScope } from '../steps/source/index.js'
|
|
3
4
|
import { cleanAiFiles } from '../steps/clean/index.js'
|
|
@@ -50,8 +51,20 @@ export async function runWizard(version) {
|
|
|
50
51
|
|
|
51
52
|
const scope = await chooseSourceScope()
|
|
52
53
|
|
|
54
|
+
const maxConcurrentAgents = await wizardSelect({
|
|
55
|
+
message: 'Max concurrent agents:',
|
|
56
|
+
default: 4,
|
|
57
|
+
choices: [
|
|
58
|
+
{ name: '2', value: 2, description: 'Conservative — lower resource usage' },
|
|
59
|
+
{ name: '3', value: 3, description: 'Moderate parallelism' },
|
|
60
|
+
{ name: '4 (default)', value: 4, description: 'Recommended for most projects' },
|
|
61
|
+
{ name: '5', value: 5, description: 'High parallelism — requires more resources' },
|
|
62
|
+
{ name: '6', value: 6, description: 'Maximum parallelism' },
|
|
63
|
+
],
|
|
64
|
+
})
|
|
65
|
+
|
|
53
66
|
const preserve = await cleanAiFiles()
|
|
54
|
-
const ctx = { ...preserve, ...scope }
|
|
67
|
+
const ctx = { ...preserve, ...scope, maxConcurrentAgents }
|
|
55
68
|
|
|
56
69
|
const platform = await choosePlatform()
|
|
57
70
|
|
|
@@ -69,6 +82,7 @@ export async function runWizard(version) {
|
|
|
69
82
|
await writeOnboardConfig({
|
|
70
83
|
...ctx,
|
|
71
84
|
platform,
|
|
85
|
+
maxConcurrentAgents,
|
|
72
86
|
additionalSkillsProvider: 'npx-skills',
|
|
73
87
|
...selectedModels,
|
|
74
88
|
optionalTools: { rtk, quota, caveman },
|
package/src/presets/source.json
CHANGED
|
@@ -11,7 +11,13 @@
|
|
|
11
11
|
"name": "Select folders in parent (../)",
|
|
12
12
|
"value": "parent",
|
|
13
13
|
"description": "Use when this repo only contains agent config"
|
|
14
|
+
},
|
|
15
|
+
{
|
|
16
|
+
"name": "Select child folders (./*/)",
|
|
17
|
+
"value": "children",
|
|
18
|
+
"description": "Use when source code lives in subdirectories of this repo"
|
|
14
19
|
}
|
|
15
20
|
],
|
|
16
|
-
"parentSelectionMessage": "Select source folders from parent directory:"
|
|
21
|
+
"parentSelectionMessage": "Select source folders from parent directory:",
|
|
22
|
+
"childrenSelectionMessage": "Select child folders to include as source:"
|
|
17
23
|
}
|
package/src/steps/copy/agents.js
CHANGED
|
@@ -61,6 +61,34 @@ function replaceBetween(content, start, end, replacement) {
|
|
|
61
61
|
return content.replace(pattern, `${start}\n${replacement.trim()}\n${end}`)
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
const CONCURRENCY_PLACEHOLDER = '{{MAX_CONCURRENT_AGENTS}}'
|
|
65
|
+
const DEFAULT_MAX_CONCURRENT_AGENTS = 4
|
|
66
|
+
|
|
67
|
+
export async function patchConcurrency(ctx) {
|
|
68
|
+
const maxAgents = String(ctx.maxConcurrentAgents ?? DEFAULT_MAX_CONCURRENT_AGENTS)
|
|
69
|
+
const cwd = process.cwd()
|
|
70
|
+
|
|
71
|
+
const filesToPatch = [
|
|
72
|
+
'AGENTS.md',
|
|
73
|
+
path.join('.opencode', 'commands', 'opsx-apply.md'),
|
|
74
|
+
path.join('.opencode', 'skills', 'openspec-apply-change', 'SKILL.md'),
|
|
75
|
+
]
|
|
76
|
+
|
|
77
|
+
let patched = 0
|
|
78
|
+
for (const rel of filesToPatch) {
|
|
79
|
+
const abs = path.join(cwd, rel)
|
|
80
|
+
if (!await fse.pathExists(abs)) continue
|
|
81
|
+
const content = await fse.readFile(abs, 'utf-8')
|
|
82
|
+
if (!content.includes(CONCURRENCY_PLACEHOLDER)) continue
|
|
83
|
+
await fse.writeFile(abs, content.replaceAll(CONCURRENCY_PLACEHOLDER, maxAgents), 'utf-8')
|
|
84
|
+
patched++
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (patched > 0) {
|
|
88
|
+
success(`Concurrency limit set to ${maxAgents} agents in ${patched} file(s)`)
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
64
92
|
export async function patchAgentsMd(ctx) {
|
|
65
93
|
const agentsMdPath = path.join(process.cwd(), 'AGENTS.md')
|
|
66
94
|
if (!await fse.pathExists(agentsMdPath)) return
|
package/src/steps/copy/index.js
CHANGED
|
@@ -3,7 +3,7 @@ import path from 'path'
|
|
|
3
3
|
import { fileURLToPath } from 'url'
|
|
4
4
|
import { copyContent } from '../../utils/copy.js'
|
|
5
5
|
import { error, header, success } from '../../utils/exec.js'
|
|
6
|
-
import { patchAgentsMd, patchDevopsManagerMd } from './agents.js'
|
|
6
|
+
import { patchAgentsMd, patchConcurrency, patchDevopsManagerMd } from './agents.js'
|
|
7
7
|
import { installSkills } from './skills.js'
|
|
8
8
|
|
|
9
9
|
const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
@@ -24,6 +24,7 @@ export async function copyContentStep(platform, ctx = {}) {
|
|
|
24
24
|
}, { spaces: 2 })
|
|
25
25
|
await patchDevopsManagerMd(platform)
|
|
26
26
|
await patchAgentsMd(ctx)
|
|
27
|
+
await patchConcurrency(ctx)
|
|
27
28
|
await installSkills()
|
|
28
29
|
success('Files copied to project root')
|
|
29
30
|
} catch (err) {
|
|
@@ -33,6 +33,7 @@ export async function writeOnboardConfig(data) {
|
|
|
33
33
|
platform: data.platform,
|
|
34
34
|
sourceMode: data.sourceMode,
|
|
35
35
|
sourceRoots: data.sourceRoots,
|
|
36
|
+
maxConcurrentAgents: data.maxConcurrentAgents ?? 4,
|
|
36
37
|
preserved: {
|
|
37
38
|
design: !!data.hasDesign,
|
|
38
39
|
architecture: !!data.hasArchitecture,
|
|
@@ -36,7 +36,14 @@ export const ENSEMBLE_SECTION = `6. **Implement via ensemble team**
|
|
|
36
36
|
DO NOT call team_claim yourself, only agents claim tasks.
|
|
37
37
|
DO NOT proceed to 6d until team_tasks_add succeeds.
|
|
38
38
|
|
|
39
|
-
**Step 6d.** Discover relevant skills, then spawn specialists.
|
|
39
|
+
**Step 6d.** Discover relevant skills, then spawn specialists with an INITIAL BATCH of tasks.
|
|
40
|
+
|
|
41
|
+
**ROLLING BATCH MODEL:**
|
|
42
|
+
Agents do NOT receive all their tasks upfront. Instead:
|
|
43
|
+
- Assign each agent an initial batch of up to 3 unblocked tasks.
|
|
44
|
+
- When an agent completes its batch and messages back, the lead assigns the next batch of up to 3 unassigned tasks from the board that match the agent's domain.
|
|
45
|
+
- Repeat until no pending tasks remain on the board.
|
|
46
|
+
- Only shut down an agent when the board has no more tasks for its domain.
|
|
40
47
|
|
|
41
48
|
Before spawning, scan \`.agents/skills/\` and read each \`SKILL.md\` description line.
|
|
42
49
|
Match skills to agents by domain:
|
|
@@ -49,11 +56,11 @@ export const ENSEMBLE_SECTION = `6. **Implement via ensemble team**
|
|
|
49
56
|
|
|
50
57
|
The spawn prompt must contain exactly:
|
|
51
58
|
1. Their name and role on this team
|
|
52
|
-
2.
|
|
59
|
+
2. Their initial batch of tasks (up to 3): include the LITERAL task IDs (e.g. "task-abc123") AND the task content. Copy them verbatim from the IDs returned by team_tasks_add. Do NOT paraphrase or omit IDs.
|
|
53
60
|
3. Key context they need (summarized from context files, do NOT tell them to read files themselves)
|
|
54
61
|
4. The 6 OpenCode tools they have available (these are OpenCode tools, NOT shell commands, call them directly as tools, never via bash):
|
|
55
62
|
team_claim, team_tasks_complete, team_tasks_list, team_tasks_add, team_message, team_broadcast
|
|
56
|
-
5. How to proceed: for EACH task ID listed
|
|
63
|
+
5. How to proceed: for EACH task ID listed, call team_claim tool with that exact task_id before starting it, call team_tasks_complete tool with that task_id after finishing it, then move to the next task. When all listed tasks are done, message lead with results. Lead may assign more tasks, do NOT shut down until lead confirms no more tasks.
|
|
57
64
|
6. Which skills to load: list the skill names and paths they MUST read before implementing. Example: "Before starting, read \`.agents/skills/next-best-practices/SKILL.md\` and follow its rules for all Next.js code."
|
|
58
65
|
|
|
59
66
|
Keep spawn prompts under 600 tokens. Do not describe team internals or how ensemble works.
|
|
@@ -86,12 +93,24 @@ export const ENSEMBLE_SECTION = `6. **Implement via ensemble team**
|
|
|
86
93
|
- \`team_view member:"<name>"\` for a teammate live session
|
|
87
94
|
- \`team_results from:"<name>"\` for full teammate report text
|
|
88
95
|
|
|
89
|
-
**Step 6f.** When a teammate messages back
|
|
90
|
-
Call team_results to read
|
|
91
|
-
|
|
92
|
-
|
|
96
|
+
**Step 6f.** When a teammate messages back (rolling re-assignment loop):
|
|
97
|
+
1. Call \`team_results from:"<name>"\` to read full message.
|
|
98
|
+
2. Call \`team_tasks_list\` to check remaining pending/unassigned tasks on the board.
|
|
99
|
+
3. **If there are more unassigned tasks matching this agent's domain:**
|
|
100
|
+
- Pick up to 3 unassigned, unblocked tasks for this agent's domain.
|
|
101
|
+
- Send them via \`team_message to:"<name>" text:"Next tasks: [task-<id1>] <desc>, [task-<id2>] <desc>. Claim each with team_claim before starting."\`
|
|
102
|
+
- Do NOT shut down the agent. Go back to waiting (step 6e).
|
|
103
|
+
4. **If no more tasks for this agent:**
|
|
104
|
+
- \`team_shutdown member:"<name>"\`
|
|
105
|
+
- \`team_merge member:"<name>"\`
|
|
106
|
+
- If team_merge blocks on local changes: \`git stash\`, retry merge, \`git stash pop\`.
|
|
107
|
+
5. **If ALL agents are shut down and tasks remain unassigned** (new domain, dependencies unblocked):
|
|
108
|
+
- Spawn new agents for the remaining tasks (back to step 6d).
|
|
109
|
+
6. **If ALL tasks are done:** proceed to step 7.
|
|
93
110
|
If a teammate reports rate-limit/quota/token exhaustion, immediately shutdown that teammate and respawn with an available model.
|
|
94
111
|
|
|
112
|
+
**ZERO PENDING TASKS GUARANTEE:** Before proceeding to step 7, call \`team_tasks_list\` and verify EVERY task is either \`done\` or \`blocked\`. If any task is \`pending\` and unassigned, assign it to an agent or spawn a new one. Never leave pending tasks orphaned.
|
|
113
|
+
|
|
95
114
|
7. **Quality check**
|
|
96
115
|
|
|
97
116
|
Spawn quality engineer with worktree:false (read-only, no file edits):
|
|
@@ -123,13 +142,17 @@ export const ENSEMBLE_SECTION = `6. **Implement via ensemble team**
|
|
|
123
142
|
- NEVER call team_spawn before team_tasks_add, tasks must exist before agents are spawned
|
|
124
143
|
- NEVER poll team_results or team_status in a loop, wait for teammates to message you
|
|
125
144
|
- NEVER call team_claim or team_tasks_complete as lead, only agents call these tools
|
|
145
|
+
- NEVER leave pending tasks orphaned, always verify board is empty before proceeding to step 7
|
|
126
146
|
- ALWAYS pass the LITERAL task IDs returned by team_tasks_add into each agent's spawn prompt, copy the exact IDs, never paraphrase
|
|
147
|
+
- ALWAYS assign initial batch of up to 3 tasks per agent; re-assign next batch (up to 3) via team_message when agent reports done
|
|
148
|
+
- ALWAYS call team_tasks_list after each agent reports done to check for remaining unassigned tasks
|
|
127
149
|
- ALWAYS repeat the same literal task IDs in the team_message start trigger, never send a generic "claim your first task" without the actual IDs
|
|
128
150
|
- NEVER send a start message that omits task IDs; if a task ID is missing from the start message, the agent cannot claim
|
|
129
151
|
- NEVER edit files between team_spawn and team_merge, team_merge blocks on overlapping local changes
|
|
130
152
|
- ALWAYS add every task to the board with team_tasks_add before spawning
|
|
131
153
|
- ALWAYS spawn agents sequentially (wait for each team_spawn result before the next), then send start messages to all of them together
|
|
132
154
|
- ALWAYS instruct agents to call team_claim before each task and team_tasks_complete after
|
|
155
|
+
- ALWAYS shut down + merge agents only when no more tasks remain for their domain
|
|
133
156
|
- If teammates are stuck, use team_message to resend tasks, then wait, never implement directly
|
|
134
157
|
- Mark tasks complete in openspec AFTER specialists finish, not before
|
|
135
158
|
- Pause on errors, blockers, or unclear requirements. Do not guess
|
|
@@ -8,6 +8,26 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url))
|
|
|
8
8
|
const SOURCE_PRESET_PATH = path.resolve(__dirname, '../../presets/source.json')
|
|
9
9
|
const sourcePreset = await fse.readJson(SOURCE_PRESET_PATH)
|
|
10
10
|
|
|
11
|
+
async function listChildFolders(cwd) {
|
|
12
|
+
const entries = await fse.readdir(cwd)
|
|
13
|
+
const dirs = []
|
|
14
|
+
|
|
15
|
+
for (const name of entries) {
|
|
16
|
+
if (name.startsWith('.')) continue
|
|
17
|
+
const abs = path.join(cwd, name)
|
|
18
|
+
try {
|
|
19
|
+
const stat = await fse.stat(abs)
|
|
20
|
+
if (!stat.isDirectory()) continue
|
|
21
|
+
dirs.push({ name, abs })
|
|
22
|
+
} catch {
|
|
23
|
+
// ignore invalid entries
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
dirs.sort((a, b) => a.name.localeCompare(b.name))
|
|
28
|
+
return dirs
|
|
29
|
+
}
|
|
30
|
+
|
|
11
31
|
async function listParentFolders(cwd) {
|
|
12
32
|
const parent = path.resolve(cwd, '..')
|
|
13
33
|
const entries = await fse.readdir(parent)
|
|
@@ -47,6 +67,34 @@ export async function chooseSourceScope() {
|
|
|
47
67
|
return { sourceMode: 'current', sourceRoots: [cwd] }
|
|
48
68
|
}
|
|
49
69
|
|
|
70
|
+
if (mode === 'children') {
|
|
71
|
+
const childFolders = await listChildFolders(cwd)
|
|
72
|
+
if (childFolders.length === 0) {
|
|
73
|
+
warn('No child folders found in current directory. Falling back to current folder.')
|
|
74
|
+
success(`Source scope: ${cwd}`)
|
|
75
|
+
return { sourceMode: 'current', sourceRoots: [cwd] }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
const selected = await checkbox({
|
|
79
|
+
message: sourcePreset.childrenSelectionMessage,
|
|
80
|
+
choices: childFolders.map(d => ({
|
|
81
|
+
name: `./${d.name}`,
|
|
82
|
+
value: d.abs,
|
|
83
|
+
checked: true,
|
|
84
|
+
})),
|
|
85
|
+
required: true,
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
if (!selected || selected.length === 0) {
|
|
89
|
+
warn('No folders selected. Falling back to current folder.')
|
|
90
|
+
success(`Source scope: ${cwd}`)
|
|
91
|
+
return { sourceMode: 'current', sourceRoots: [cwd] }
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
success(`Source scope: ${selected.map(p => path.basename(p)).join(', ')}`)
|
|
95
|
+
return { sourceMode: 'children-selected', sourceRoots: selected }
|
|
96
|
+
}
|
|
97
|
+
|
|
50
98
|
const parentFolders = await listParentFolders(cwd)
|
|
51
99
|
if (parentFolders.length === 0) {
|
|
52
100
|
warn('No sibling folders found in parent directory. Falling back to current folder.')
|
|
@@ -23,8 +23,10 @@ vi.mock('fs-extra', () => ({
|
|
|
23
23
|
choices: [
|
|
24
24
|
{ name: 'Current folder', value: 'current' },
|
|
25
25
|
{ name: 'Parent folder', value: 'parent' },
|
|
26
|
+
{ name: 'Child folders', value: 'children' },
|
|
26
27
|
],
|
|
27
28
|
parentSelectionMessage: 'Select sibling folders',
|
|
29
|
+
childrenSelectionMessage: 'Select child folders',
|
|
28
30
|
}),
|
|
29
31
|
readdir: vi.fn(),
|
|
30
32
|
stat: vi.fn().mockResolvedValue({ isDirectory: () => true }),
|
|
@@ -88,4 +90,35 @@ describe('chooseSourceScope()', () => {
|
|
|
88
90
|
|
|
89
91
|
expect(result.sourceMode).toBe('current')
|
|
90
92
|
})
|
|
93
|
+
|
|
94
|
+
it('lists child folders when user selects children mode', async () => {
|
|
95
|
+
select.mockResolvedValue('children')
|
|
96
|
+
fse.readdir.mockResolvedValue(['packages', 'apps'])
|
|
97
|
+
checkbox.mockResolvedValue([path.join(tmpDir, 'packages')])
|
|
98
|
+
|
|
99
|
+
const result = await chooseSourceScope()
|
|
100
|
+
|
|
101
|
+
expect(checkbox).toHaveBeenCalled()
|
|
102
|
+
expect(result.sourceMode).toBe('children-selected')
|
|
103
|
+
expect(result.sourceRoots).toContain(path.join(tmpDir, 'packages'))
|
|
104
|
+
})
|
|
105
|
+
|
|
106
|
+
it('falls back to current when no child folders found', async () => {
|
|
107
|
+
select.mockResolvedValue('children')
|
|
108
|
+
fse.readdir.mockResolvedValue([])
|
|
109
|
+
|
|
110
|
+
const result = await chooseSourceScope()
|
|
111
|
+
|
|
112
|
+
expect(result.sourceMode).toBe('current')
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
it('falls back to current when no child folders selected', async () => {
|
|
116
|
+
select.mockResolvedValue('children')
|
|
117
|
+
fse.readdir.mockResolvedValue(['packages'])
|
|
118
|
+
checkbox.mockResolvedValue([])
|
|
119
|
+
|
|
120
|
+
const result = await chooseSourceScope()
|
|
121
|
+
|
|
122
|
+
expect(result.sourceMode).toBe('current')
|
|
123
|
+
})
|
|
91
124
|
})
|