opencode-swarm-plugin 0.12.2 → 0.12.6
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/.beads/issues.jsonl +215 -0
- package/README.md +111 -106
- package/bin/swarm.ts +13 -14
- package/dist/index.js +100 -46
- package/dist/plugin.js +68 -33
- package/examples/commands/swarm.md +51 -210
- package/package.json +1 -1
- package/src/agent-mail.ts +26 -11
- package/src/beads.ts +75 -20
- package/src/rate-limiter.ts +12 -6
- package/src/schemas/bead.ts +4 -4
- package/src/schemas/evaluation.ts +2 -2
- package/src/schemas/task.ts +3 -3
- package/src/storage.ts +37 -15
- package/src/swarm.ts +13 -0
- package/examples/agents/swarm-planner.md +0 -138
|
@@ -2,276 +2,117 @@
|
|
|
2
2
|
description: Decompose task into parallel subtasks and coordinate agents
|
|
3
3
|
---
|
|
4
4
|
|
|
5
|
-
You are a swarm coordinator.
|
|
5
|
+
You are a swarm coordinator. Decompose the task into beads and spawn parallel agents.
|
|
6
6
|
|
|
7
|
-
##
|
|
7
|
+
## Task
|
|
8
8
|
|
|
9
|
-
|
|
10
|
-
/swarm <task description or bead-id>
|
|
11
|
-
/swarm --to-main <task> # Skip PR, push directly to main (use sparingly)
|
|
12
|
-
/swarm --no-sync <task> # Skip mid-task context sync (for simple independent tasks)
|
|
13
|
-
```
|
|
14
|
-
|
|
15
|
-
**Default behavior: Feature branch + PR with context sync.** All swarm work goes to a feature branch, agents share context mid-task, and creates a PR for review.
|
|
16
|
-
|
|
17
|
-
## Step 1: Initialize Session
|
|
18
|
-
|
|
19
|
-
Use the plugin's agent-mail tools to register:
|
|
20
|
-
|
|
21
|
-
```
|
|
22
|
-
agentmail_init with project_path=$PWD, task_description="Swarm coordinator: <task>"
|
|
23
|
-
```
|
|
24
|
-
|
|
25
|
-
This returns your agent name and session state. Remember it.
|
|
9
|
+
$ARGUMENTS
|
|
26
10
|
|
|
27
|
-
##
|
|
11
|
+
## Flags (parse from task above)
|
|
28
12
|
|
|
29
|
-
|
|
13
|
+
- `--to-main` - Push directly to main, skip PR
|
|
14
|
+
- `--no-sync` - Skip mid-task context sharing
|
|
30
15
|
|
|
31
|
-
|
|
32
|
-
# Create branch from bead ID or task name
|
|
33
|
-
git checkout -b swarm/<bead-id> # e.g., swarm/trt-buddy-d7d
|
|
34
|
-
# Or for ad-hoc tasks:
|
|
35
|
-
git checkout -b swarm/<short-description> # e.g., swarm/contextual-checkins
|
|
16
|
+
**Default: Feature branch + PR with context sync.**
|
|
36
17
|
|
|
37
|
-
|
|
38
|
-
```
|
|
18
|
+
## Workflow
|
|
39
19
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
If given a bead-id:
|
|
20
|
+
### 1. Initialize
|
|
43
21
|
|
|
44
22
|
```
|
|
45
|
-
|
|
23
|
+
agentmail_init(project_path="$PWD", task_description="Swarm: <task summary>")
|
|
46
24
|
```
|
|
47
25
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
## Step 4: Select Strategy & Decompose
|
|
26
|
+
### 2. Create Feature Branch (unless --to-main)
|
|
51
27
|
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
```
|
|
57
|
-
Task(
|
|
58
|
-
subagent_type="general",
|
|
59
|
-
description="Plan swarm decomposition",
|
|
60
|
-
prompt="You are @swarm-planner. Decompose this task: <task description>. Use swarm_select_strategy and swarm_plan_prompt to guide your decomposition. Return ONLY valid BeadTree JSON."
|
|
61
|
-
)
|
|
28
|
+
```bash
|
|
29
|
+
git checkout -b swarm/<short-task-name>
|
|
30
|
+
git push -u origin HEAD
|
|
62
31
|
```
|
|
63
32
|
|
|
64
|
-
###
|
|
33
|
+
### 3. Decompose Task
|
|
65
34
|
|
|
66
|
-
|
|
35
|
+
Use strategy selection and planning:
|
|
67
36
|
|
|
68
37
|
```
|
|
69
|
-
swarm_select_strategy
|
|
38
|
+
swarm_select_strategy(task="<the task>")
|
|
39
|
+
swarm_plan_prompt(task="<the task>", strategy="<auto or selected>")
|
|
70
40
|
```
|
|
71
41
|
|
|
72
|
-
|
|
42
|
+
Follow the prompt to create a BeadTree, then validate:
|
|
73
43
|
|
|
74
44
|
```
|
|
75
|
-
|
|
45
|
+
swarm_validate_decomposition(response="<your BeadTree JSON>")
|
|
76
46
|
```
|
|
77
47
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
4. **Validate**:
|
|
48
|
+
### 4. Create Beads
|
|
81
49
|
|
|
82
50
|
```
|
|
83
|
-
|
|
51
|
+
beads_create_epic(epic_title="<task>", subtasks=[{title, files, priority}...])
|
|
84
52
|
```
|
|
85
53
|
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
Once you have a valid BeadTree:
|
|
54
|
+
Rules:
|
|
89
55
|
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
56
|
+
- Each bead completable by one agent
|
|
57
|
+
- Independent where possible (parallelizable)
|
|
58
|
+
- 3-7 beads per swarm
|
|
93
59
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
- Each bead should be completable by one agent
|
|
97
|
-
- Beads should be independent (parallelizable) where possible
|
|
98
|
-
- If there are dependencies, order them in the subtasks array
|
|
99
|
-
- Aim for 3-7 beads per swarm (too few = not parallel, too many = coordination overhead)
|
|
100
|
-
|
|
101
|
-
## Step 5: Reserve Files
|
|
102
|
-
|
|
103
|
-
For each subtask, reserve the files it will touch:
|
|
60
|
+
### 5. Reserve Files
|
|
104
61
|
|
|
105
62
|
```
|
|
106
|
-
agentmail_reserve
|
|
63
|
+
agentmail_reserve(paths=[<files>], reason="<bead-id>: <description>")
|
|
107
64
|
```
|
|
108
65
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
- No two agents should edit the same file
|
|
112
|
-
- If overlap exists, merge beads or sequence them
|
|
66
|
+
No two agents should edit the same file.
|
|
113
67
|
|
|
114
|
-
|
|
68
|
+
### 6. Spawn Agents
|
|
115
69
|
|
|
116
|
-
**CRITICAL: Spawn ALL
|
|
70
|
+
**CRITICAL: Spawn ALL in a SINGLE message with multiple Task calls.**
|
|
117
71
|
|
|
118
|
-
|
|
72
|
+
For each subtask:
|
|
119
73
|
|
|
120
74
|
```
|
|
121
|
-
swarm_spawn_subtask
|
|
75
|
+
swarm_spawn_subtask(bead_id="<id>", epic_id="<epic>", subtask_title="<title>", files=[...])
|
|
122
76
|
```
|
|
123
77
|
|
|
124
|
-
Then spawn
|
|
78
|
+
Then spawn:
|
|
125
79
|
|
|
126
80
|
```
|
|
127
|
-
Task(
|
|
128
|
-
subagent_type="general",
|
|
129
|
-
description="Swarm worker: <bead-title>",
|
|
130
|
-
prompt="<output from swarm_spawn_subtask>"
|
|
131
|
-
)
|
|
81
|
+
Task(subagent_type="swarm-worker", description="<bead-title>", prompt="<from swarm_spawn_subtask>")
|
|
132
82
|
```
|
|
133
83
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
## Step 7: Monitor Progress (unless --no-sync)
|
|
137
|
-
|
|
138
|
-
Check swarm status:
|
|
84
|
+
### 7. Monitor (unless --no-sync)
|
|
139
85
|
|
|
140
86
|
```
|
|
141
|
-
swarm_status
|
|
87
|
+
swarm_status(epic_id="<epic-id>")
|
|
88
|
+
agentmail_inbox()
|
|
142
89
|
```
|
|
143
90
|
|
|
144
|
-
|
|
91
|
+
If incompatibilities spotted, broadcast:
|
|
145
92
|
|
|
146
93
|
```
|
|
147
|
-
|
|
94
|
+
agentmail_send(to=["*"], subject="Coordinator Update", body="<guidance>", importance="high")
|
|
148
95
|
```
|
|
149
96
|
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
1. **Review decisions made** - Are agents making compatible choices?
|
|
153
|
-
2. **Check for pattern conflicts** - Different approaches to the same problem?
|
|
154
|
-
3. **Identify shared concerns** - Common blockers or discoveries?
|
|
155
|
-
|
|
156
|
-
**If you spot incompatibilities, broadcast shared context:**
|
|
157
|
-
|
|
158
|
-
```
|
|
159
|
-
agentmail_send with to=["*"], subject="Coordinator Update", body="<guidance>", thread_id="<epic-id>", importance="high"
|
|
160
|
-
```
|
|
161
|
-
|
|
162
|
-
## Step 8: Collect Results
|
|
163
|
-
|
|
164
|
-
When agents complete, they send completion messages. Summarize the thread:
|
|
97
|
+
### 8. Complete
|
|
165
98
|
|
|
166
99
|
```
|
|
167
|
-
|
|
100
|
+
swarm_complete(project_key="$PWD", agent_name="<your-name>", bead_id="<epic-id>", summary="<done>", files_touched=[...])
|
|
101
|
+
beads_sync()
|
|
168
102
|
```
|
|
169
103
|
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
Use the swarm completion tool:
|
|
173
|
-
|
|
174
|
-
```
|
|
175
|
-
swarm_complete with project_key=$PWD, agent_name=<YOUR_NAME>, bead_id="<epic-id>", summary="<what was accomplished>", files_touched=[<all files>]
|
|
176
|
-
```
|
|
177
|
-
|
|
178
|
-
This:
|
|
179
|
-
|
|
180
|
-
- Runs UBS bug scan on touched files
|
|
181
|
-
- Releases file reservations
|
|
182
|
-
- Closes the bead
|
|
183
|
-
- Records outcome for learning
|
|
184
|
-
|
|
185
|
-
Then sync beads:
|
|
186
|
-
|
|
187
|
-
```
|
|
188
|
-
beads_sync
|
|
189
|
-
```
|
|
190
|
-
|
|
191
|
-
## Step 10: Create PR
|
|
104
|
+
### 9. Create PR (unless --to-main)
|
|
192
105
|
|
|
193
106
|
```bash
|
|
194
|
-
gh pr create --title "feat: <epic title>" --body "
|
|
195
|
-
## Summary
|
|
196
|
-
<1-3 bullet points from swarm results>
|
|
197
|
-
|
|
198
|
-
## Beads Completed
|
|
199
|
-
- <bead-id>: <summary>
|
|
200
|
-
- <bead-id>: <summary>
|
|
201
|
-
|
|
202
|
-
## Files Changed
|
|
203
|
-
<aggregate list>
|
|
204
|
-
|
|
205
|
-
## Testing
|
|
206
|
-
- [ ] Type check passes
|
|
207
|
-
- [ ] Tests pass (if applicable)
|
|
208
|
-
EOF
|
|
209
|
-
)"
|
|
107
|
+
gh pr create --title "feat: <epic title>" --body "## Summary\n<bullets>\n\n## Beads\n<list>"
|
|
210
108
|
```
|
|
211
109
|
|
|
212
|
-
Report summary:
|
|
213
|
-
|
|
214
|
-
```markdown
|
|
215
|
-
## Swarm Complete: <task>
|
|
216
|
-
|
|
217
|
-
### PR: #<number>
|
|
218
|
-
|
|
219
|
-
### Agents Spawned: N
|
|
220
|
-
|
|
221
|
-
### Beads Closed: N
|
|
222
|
-
|
|
223
|
-
### Work Completed
|
|
224
|
-
|
|
225
|
-
- [bead-id]: [summary]
|
|
226
|
-
|
|
227
|
-
### Files Changed
|
|
228
|
-
|
|
229
|
-
- [aggregate list]
|
|
230
|
-
```
|
|
231
|
-
|
|
232
|
-
## Failure Handling
|
|
233
|
-
|
|
234
|
-
If an agent fails:
|
|
235
|
-
|
|
236
|
-
- Check its messages: `agentmail_inbox`
|
|
237
|
-
- The bead remains in-progress
|
|
238
|
-
- Manually investigate or re-spawn
|
|
239
|
-
|
|
240
|
-
If file conflicts occur:
|
|
241
|
-
|
|
242
|
-
- Agent Mail reservations should prevent this
|
|
243
|
-
- If it happens, one agent needs to wait
|
|
244
|
-
|
|
245
|
-
## Direct-to-Main Mode (--to-main)
|
|
246
|
-
|
|
247
|
-
Only use when explicitly requested. Skips branch/PR:
|
|
248
|
-
|
|
249
|
-
- Trivial fixes across many files
|
|
250
|
-
- Automated migrations with high confidence
|
|
251
|
-
- User explicitly says "push to main"
|
|
252
|
-
|
|
253
|
-
## No-Sync Mode (--no-sync)
|
|
254
|
-
|
|
255
|
-
Skip mid-task context sharing when tasks are truly independent:
|
|
256
|
-
|
|
257
|
-
- Simple mechanical changes (find/replace, formatting, lint fixes)
|
|
258
|
-
- Tasks with zero integration points
|
|
259
|
-
- Completely separate feature areas with no shared types
|
|
260
|
-
|
|
261
|
-
In this mode:
|
|
262
|
-
|
|
263
|
-
- Agents skip the mid-task progress message
|
|
264
|
-
- Coordinator skips Step 7 (monitoring)
|
|
265
|
-
- Faster execution, less coordination overhead
|
|
266
|
-
|
|
267
|
-
**Default is sync ON** - prefer sharing context. Use `--no-sync` deliberately.
|
|
268
|
-
|
|
269
110
|
## Strategy Reference
|
|
270
111
|
|
|
271
|
-
| Strategy
|
|
272
|
-
|
|
|
273
|
-
|
|
|
274
|
-
|
|
|
275
|
-
|
|
|
112
|
+
| Strategy | Best For | Keywords |
|
|
113
|
+
| ------------- | ----------------------- | ------------------------------------- |
|
|
114
|
+
| file-based | Refactoring, migrations | refactor, migrate, rename, update all |
|
|
115
|
+
| feature-based | New features | add, implement, build, create, new |
|
|
116
|
+
| risk-based | Bug fixes, security | fix, bug, security, critical, urgent |
|
|
276
117
|
|
|
277
|
-
|
|
118
|
+
Begin decomposition now.
|
package/package.json
CHANGED
package/src/agent-mail.ts
CHANGED
|
@@ -107,8 +107,10 @@ function loadSessionState(sessionID: string): AgentMailState | null {
|
|
|
107
107
|
|
|
108
108
|
/**
|
|
109
109
|
* Save session state to disk
|
|
110
|
+
*
|
|
111
|
+
* @returns true if save succeeded, false if failed
|
|
110
112
|
*/
|
|
111
|
-
function saveSessionState(sessionID: string, state: AgentMailState):
|
|
113
|
+
function saveSessionState(sessionID: string, state: AgentMailState): boolean {
|
|
112
114
|
try {
|
|
113
115
|
// Ensure directory exists
|
|
114
116
|
if (!existsSync(SESSION_STATE_DIR)) {
|
|
@@ -116,9 +118,16 @@ function saveSessionState(sessionID: string, state: AgentMailState): void {
|
|
|
116
118
|
}
|
|
117
119
|
const path = getSessionStatePath(sessionID);
|
|
118
120
|
writeFileSync(path, JSON.stringify(state, null, 2));
|
|
121
|
+
return true;
|
|
119
122
|
} catch (error) {
|
|
120
123
|
// Non-fatal - state just won't persist
|
|
121
|
-
console.
|
|
124
|
+
console.error(
|
|
125
|
+
`[agent-mail] CRITICAL: Could not save session state: ${error}`,
|
|
126
|
+
);
|
|
127
|
+
console.error(
|
|
128
|
+
`[agent-mail] Session state will not persist across CLI invocations!`,
|
|
129
|
+
);
|
|
130
|
+
return false;
|
|
122
131
|
}
|
|
123
132
|
}
|
|
124
133
|
|
|
@@ -718,6 +727,9 @@ export async function mcpCall<T>(
|
|
|
718
727
|
// Track consecutive failures
|
|
719
728
|
consecutiveFailures++;
|
|
720
729
|
|
|
730
|
+
// Check if error is retryable FIRST
|
|
731
|
+
const retryable = isRetryableError(error);
|
|
732
|
+
|
|
721
733
|
// Check if we should attempt server restart
|
|
722
734
|
if (
|
|
723
735
|
consecutiveFailures >= RECOVERY_CONFIG.failureThreshold &&
|
|
@@ -734,15 +746,18 @@ export async function mcpCall<T>(
|
|
|
734
746
|
if (restarted) {
|
|
735
747
|
// Reset availability cache since server restarted
|
|
736
748
|
agentMailAvailable = null;
|
|
737
|
-
//
|
|
738
|
-
|
|
739
|
-
|
|
749
|
+
// Only retry if the error was retryable in the first place
|
|
750
|
+
if (retryable) {
|
|
751
|
+
// Don't count this attempt against retries - try again
|
|
752
|
+
attempt--;
|
|
753
|
+
continue;
|
|
754
|
+
}
|
|
740
755
|
}
|
|
741
756
|
}
|
|
742
757
|
}
|
|
743
758
|
|
|
744
|
-
//
|
|
745
|
-
if (!
|
|
759
|
+
// If error is not retryable, throw immediately
|
|
760
|
+
if (!retryable) {
|
|
746
761
|
console.warn(
|
|
747
762
|
`[agent-mail] Non-retryable error for ${toolName}: ${lastError.message}`,
|
|
748
763
|
);
|
|
@@ -1006,18 +1021,18 @@ export const agentmail_read_message = tool({
|
|
|
1006
1021
|
message_id: args.message_id,
|
|
1007
1022
|
});
|
|
1008
1023
|
|
|
1009
|
-
// Fetch with body -
|
|
1010
|
-
// Since there's no get_message, we
|
|
1024
|
+
// Fetch with body - fetch more messages to find the requested one
|
|
1025
|
+
// Since there's no get_message endpoint, we need to fetch a reasonable batch
|
|
1011
1026
|
const messages = await mcpCall<MessageHeader[]>("fetch_inbox", {
|
|
1012
1027
|
project_key: state.projectKey,
|
|
1013
1028
|
agent_name: state.agentName,
|
|
1014
|
-
limit:
|
|
1029
|
+
limit: 50, // Fetch more messages to increase chance of finding the target
|
|
1015
1030
|
include_bodies: true, // Only for single message fetch
|
|
1016
1031
|
});
|
|
1017
1032
|
|
|
1018
1033
|
const message = messages.find((m) => m.id === args.message_id);
|
|
1019
1034
|
if (!message) {
|
|
1020
|
-
return `Message ${args.message_id} not found
|
|
1035
|
+
return `Message ${args.message_id} not found in recent 50 messages. Try using agentmail_search to locate it.`;
|
|
1021
1036
|
}
|
|
1022
1037
|
|
|
1023
1038
|
// Record successful request
|
package/src/beads.ts
CHANGED
|
@@ -55,6 +55,10 @@ export class BeadValidationError extends Error {
|
|
|
55
55
|
|
|
56
56
|
/**
|
|
57
57
|
* Build a bd create command from args
|
|
58
|
+
*
|
|
59
|
+
* Note: Bun's `$` template literal properly escapes arguments when passed as array.
|
|
60
|
+
* Each array element is treated as a separate argument, preventing shell injection.
|
|
61
|
+
* Example: ["bd", "create", "; rm -rf /"] becomes: bd create "; rm -rf /"
|
|
58
62
|
*/
|
|
59
63
|
function buildCreateCommand(args: BeadCreateArgs): string[] {
|
|
60
64
|
const parts = ["bd", "create", args.title];
|
|
@@ -250,25 +254,40 @@ export const beads_create_epic = tool({
|
|
|
250
254
|
|
|
251
255
|
return JSON.stringify(result, null, 2);
|
|
252
256
|
} catch (error) {
|
|
253
|
-
// Partial failure -
|
|
254
|
-
const
|
|
255
|
-
|
|
256
|
-
|
|
257
|
+
// Partial failure - execute rollback automatically
|
|
258
|
+
const rollbackCommands: string[] = [];
|
|
259
|
+
|
|
260
|
+
for (const bead of created) {
|
|
261
|
+
try {
|
|
262
|
+
const closeCmd = [
|
|
263
|
+
"bd",
|
|
264
|
+
"close",
|
|
265
|
+
bead.id,
|
|
266
|
+
"--reason",
|
|
267
|
+
"Rollback partial epic",
|
|
268
|
+
"--json",
|
|
269
|
+
];
|
|
270
|
+
await Bun.$`${closeCmd}`.quiet().nothrow();
|
|
271
|
+
rollbackCommands.push(
|
|
272
|
+
`bd close ${bead.id} --reason "Rollback partial epic"`,
|
|
273
|
+
);
|
|
274
|
+
} catch (rollbackError) {
|
|
275
|
+
// Log rollback failure but continue
|
|
276
|
+
console.error(`Failed to rollback bead ${bead.id}:`, rollbackError);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
257
279
|
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
280
|
+
// Throw error with rollback info
|
|
281
|
+
const errorMsg = error instanceof Error ? error.message : String(error);
|
|
282
|
+
const rollbackInfo =
|
|
283
|
+
rollbackCommands.length > 0
|
|
284
|
+
? `\n\nRolled back ${rollbackCommands.length} bead(s):\n${rollbackCommands.join("\n")}`
|
|
285
|
+
: "\n\nNo beads to rollback.";
|
|
264
286
|
|
|
265
|
-
|
|
266
|
-
{
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
},
|
|
270
|
-
null,
|
|
271
|
-
2,
|
|
287
|
+
throw new BeadError(
|
|
288
|
+
`Epic creation failed: ${errorMsg}${rollbackInfo}`,
|
|
289
|
+
"beads_create_epic",
|
|
290
|
+
1,
|
|
272
291
|
);
|
|
273
292
|
}
|
|
274
293
|
},
|
|
@@ -487,10 +506,38 @@ export const beads_sync = tool({
|
|
|
487
506
|
},
|
|
488
507
|
async execute(args, ctx) {
|
|
489
508
|
const autoPull = args.auto_pull ?? true;
|
|
509
|
+
const TIMEOUT_MS = 30000; // 30 seconds
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* Helper to run a command with timeout
|
|
513
|
+
*/
|
|
514
|
+
const withTimeout = async <T>(
|
|
515
|
+
promise: Promise<T>,
|
|
516
|
+
timeoutMs: number,
|
|
517
|
+
operation: string,
|
|
518
|
+
): Promise<T> => {
|
|
519
|
+
const timeoutPromise = new Promise<never>((_, reject) =>
|
|
520
|
+
setTimeout(
|
|
521
|
+
() =>
|
|
522
|
+
reject(
|
|
523
|
+
new BeadError(
|
|
524
|
+
`Operation timed out after ${timeoutMs}ms`,
|
|
525
|
+
operation,
|
|
526
|
+
),
|
|
527
|
+
),
|
|
528
|
+
timeoutMs,
|
|
529
|
+
),
|
|
530
|
+
);
|
|
531
|
+
return Promise.race([promise, timeoutPromise]);
|
|
532
|
+
};
|
|
490
533
|
|
|
491
534
|
// 1. Pull if requested
|
|
492
535
|
if (autoPull) {
|
|
493
|
-
const pullResult = await
|
|
536
|
+
const pullResult = await withTimeout(
|
|
537
|
+
Bun.$`git pull --rebase`.quiet().nothrow(),
|
|
538
|
+
TIMEOUT_MS,
|
|
539
|
+
"git pull --rebase",
|
|
540
|
+
);
|
|
494
541
|
if (pullResult.exitCode !== 0) {
|
|
495
542
|
throw new BeadError(
|
|
496
543
|
`Failed to pull: ${pullResult.stderr.toString()}`,
|
|
@@ -501,7 +548,11 @@ export const beads_sync = tool({
|
|
|
501
548
|
}
|
|
502
549
|
|
|
503
550
|
// 2. Sync beads
|
|
504
|
-
const syncResult = await
|
|
551
|
+
const syncResult = await withTimeout(
|
|
552
|
+
Bun.$`bd sync`.quiet().nothrow(),
|
|
553
|
+
TIMEOUT_MS,
|
|
554
|
+
"bd sync",
|
|
555
|
+
);
|
|
505
556
|
if (syncResult.exitCode !== 0) {
|
|
506
557
|
throw new BeadError(
|
|
507
558
|
`Failed to sync beads: ${syncResult.stderr.toString()}`,
|
|
@@ -511,7 +562,11 @@ export const beads_sync = tool({
|
|
|
511
562
|
}
|
|
512
563
|
|
|
513
564
|
// 3. Push
|
|
514
|
-
const pushResult = await
|
|
565
|
+
const pushResult = await withTimeout(
|
|
566
|
+
Bun.$`git push`.quiet().nothrow(),
|
|
567
|
+
TIMEOUT_MS,
|
|
568
|
+
"git push",
|
|
569
|
+
);
|
|
515
570
|
if (pushResult.exitCode !== 0) {
|
|
516
571
|
throw new BeadError(
|
|
517
572
|
`Failed to push: ${pushResult.stderr.toString()}`,
|
package/src/rate-limiter.ts
CHANGED
|
@@ -122,9 +122,15 @@ export function getLimitsForEndpoint(endpoint: string): EndpointLimits {
|
|
|
122
122
|
const perHourEnv =
|
|
123
123
|
process.env[`OPENCODE_RATE_LIMIT_${upperEndpoint}_PER_HOUR`];
|
|
124
124
|
|
|
125
|
+
// Parse and validate env vars, fall back to defaults on NaN
|
|
126
|
+
const parsedPerMinute = perMinuteEnv ? parseInt(perMinuteEnv, 10) : NaN;
|
|
127
|
+
const parsedPerHour = perHourEnv ? parseInt(perHourEnv, 10) : NaN;
|
|
128
|
+
|
|
125
129
|
return {
|
|
126
|
-
perMinute:
|
|
127
|
-
|
|
130
|
+
perMinute: Number.isNaN(parsedPerMinute)
|
|
131
|
+
? defaults.perMinute
|
|
132
|
+
: parsedPerMinute,
|
|
133
|
+
perHour: Number.isNaN(parsedPerHour) ? defaults.perHour : parsedPerHour,
|
|
128
134
|
};
|
|
129
135
|
}
|
|
130
136
|
|
|
@@ -211,10 +217,11 @@ export class RedisRateLimiter implements RateLimiter {
|
|
|
211
217
|
const windowDuration = this.getWindowDuration(window);
|
|
212
218
|
const windowStart = now - windowDuration;
|
|
213
219
|
|
|
214
|
-
// Remove expired entries
|
|
220
|
+
// Remove expired entries, count current ones, and fetch oldest in a single pipeline
|
|
215
221
|
const pipeline = this.redis.pipeline();
|
|
216
222
|
pipeline.zremrangebyscore(key, 0, windowStart);
|
|
217
223
|
pipeline.zcard(key);
|
|
224
|
+
pipeline.zrange(key, 0, 0, "WITHSCORES"); // Fetch oldest entry atomically
|
|
218
225
|
|
|
219
226
|
const results = await pipeline.exec();
|
|
220
227
|
if (!results) {
|
|
@@ -225,11 +232,10 @@ export class RedisRateLimiter implements RateLimiter {
|
|
|
225
232
|
const remaining = Math.max(0, limit - count);
|
|
226
233
|
const allowed = count < limit;
|
|
227
234
|
|
|
228
|
-
// Calculate reset time based on oldest entry in window
|
|
235
|
+
// Calculate reset time based on oldest entry in window (fetched atomically)
|
|
229
236
|
let resetAt = now + windowDuration;
|
|
230
237
|
if (!allowed) {
|
|
231
|
-
|
|
232
|
-
const oldest = await this.redis.zrange(key, 0, 0, "WITHSCORES");
|
|
238
|
+
const oldest = (results[2]?.[1] as string[]) || [];
|
|
233
239
|
if (oldest.length >= 2) {
|
|
234
240
|
const oldestTimestamp = parseInt(oldest[1], 10);
|
|
235
241
|
resetAt = oldestTimestamp + windowDuration;
|
package/src/schemas/bead.ts
CHANGED
|
@@ -42,15 +42,15 @@ export type BeadDependency = z.infer<typeof BeadDependencySchema>;
|
|
|
42
42
|
export const BeadSchema = z.object({
|
|
43
43
|
id: z
|
|
44
44
|
.string()
|
|
45
|
-
.regex(/^[a-z0-9-
|
|
45
|
+
.regex(/^[a-z0-9]+(-[a-z0-9]+)+(\.\d+)?$/, "Invalid bead ID format"),
|
|
46
46
|
title: z.string().min(1, "Title required"),
|
|
47
47
|
description: z.string().optional().default(""),
|
|
48
48
|
status: BeadStatusSchema.default("open"),
|
|
49
49
|
priority: z.number().int().min(0).max(3).default(2),
|
|
50
50
|
issue_type: BeadTypeSchema.default("task"),
|
|
51
|
-
created_at: z.string(), // ISO-8601
|
|
52
|
-
updated_at: z.string().optional(),
|
|
53
|
-
closed_at: z.string().optional(),
|
|
51
|
+
created_at: z.string().datetime({ offset: true }), // ISO-8601 with timezone offset
|
|
52
|
+
updated_at: z.string().datetime({ offset: true }).optional(),
|
|
53
|
+
closed_at: z.string().datetime({ offset: true }).optional(),
|
|
54
54
|
parent_id: z.string().optional(),
|
|
55
55
|
dependencies: z.array(BeadDependencySchema).optional().default([]),
|
|
56
56
|
metadata: z.record(z.string(), z.unknown()).optional(),
|
|
@@ -53,7 +53,7 @@ export const EvaluationSchema = z.object({
|
|
|
53
53
|
criteria: z.record(z.string(), CriterionEvaluationSchema),
|
|
54
54
|
overall_feedback: z.string(),
|
|
55
55
|
retry_suggestion: z.string().nullable(),
|
|
56
|
-
timestamp: z.string().optional(), // ISO-8601
|
|
56
|
+
timestamp: z.string().datetime({ offset: true }).optional(), // ISO-8601 with timezone
|
|
57
57
|
});
|
|
58
58
|
export type Evaluation = z.infer<typeof EvaluationSchema>;
|
|
59
59
|
|
|
@@ -91,7 +91,7 @@ export const WeightedEvaluationSchema = z.object({
|
|
|
91
91
|
criteria: z.record(z.string(), WeightedCriterionEvaluationSchema),
|
|
92
92
|
overall_feedback: z.string(),
|
|
93
93
|
retry_suggestion: z.string().nullable(),
|
|
94
|
-
timestamp: z.string().optional(), // ISO-8601
|
|
94
|
+
timestamp: z.string().datetime({ offset: true }).optional(), // ISO-8601 with timezone
|
|
95
95
|
/** Average weight across all criteria (indicates overall confidence) */
|
|
96
96
|
average_weight: z.number().min(0).max(1).optional(),
|
|
97
97
|
/** Raw score before weighting */
|
package/src/schemas/task.ts
CHANGED
|
@@ -94,7 +94,7 @@ export const SwarmSpawnResultSchema = z.object({
|
|
|
94
94
|
coordinator_name: z.string(), // Agent Mail name of coordinator
|
|
95
95
|
thread_id: z.string(), // Agent Mail thread for this swarm
|
|
96
96
|
agents: z.array(SpawnedAgentSchema),
|
|
97
|
-
started_at: z.string(), // ISO-8601
|
|
97
|
+
started_at: z.string().datetime({ offset: true }), // ISO-8601 with timezone
|
|
98
98
|
});
|
|
99
99
|
export type SwarmSpawnResult = z.infer<typeof SwarmSpawnResultSchema>;
|
|
100
100
|
|
|
@@ -109,7 +109,7 @@ export const AgentProgressSchema = z.object({
|
|
|
109
109
|
message: z.string().optional(),
|
|
110
110
|
files_touched: z.array(z.string()).optional(),
|
|
111
111
|
blockers: z.array(z.string()).optional(),
|
|
112
|
-
timestamp: z.string(), // ISO-8601
|
|
112
|
+
timestamp: z.string().datetime({ offset: true }), // ISO-8601 with timezone
|
|
113
113
|
});
|
|
114
114
|
export type AgentProgress = z.infer<typeof AgentProgressSchema>;
|
|
115
115
|
|
|
@@ -124,6 +124,6 @@ export const SwarmStatusSchema = z.object({
|
|
|
124
124
|
failed: z.number().int().min(0),
|
|
125
125
|
blocked: z.number().int().min(0),
|
|
126
126
|
agents: z.array(SpawnedAgentSchema),
|
|
127
|
-
last_update: z.string(), // ISO-8601
|
|
127
|
+
last_update: z.string().datetime({ offset: true }), // ISO-8601 with timezone
|
|
128
128
|
});
|
|
129
129
|
export type SwarmStatus = z.infer<typeof SwarmStatusSchema>;
|