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.
@@ -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. Take a complex task, break it into beads, and unleash parallel agents.
5
+ You are a swarm coordinator. Decompose the task into beads and spawn parallel agents.
6
6
 
7
- ## Usage
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
- ## Step 2: Create Feature Branch
11
+ ## Flags (parse from task above)
28
12
 
29
- **CRITICAL: Never push directly to main.**
13
+ - `--to-main` - Push directly to main, skip PR
14
+ - `--no-sync` - Skip mid-task context sharing
30
15
 
31
- ```bash
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
- git push -u origin HEAD
38
- ```
18
+ ## Workflow
39
19
 
40
- ## Step 3: Understand the Task
41
-
42
- If given a bead-id:
20
+ ### 1. Initialize
43
21
 
44
22
  ```
45
- beads_query with id=<bead-id>
23
+ agentmail_init(project_path="$PWD", task_description="Swarm: <task summary>")
46
24
  ```
47
25
 
48
- If given a description, analyze it to understand scope.
49
-
50
- ## Step 4: Select Strategy & Decompose
26
+ ### 2. Create Feature Branch (unless --to-main)
51
27
 
52
- ### Option A: Use the Planner Agent (Recommended)
53
-
54
- Spawn the `@swarm-planner` agent to handle decomposition:
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
- ### Option B: Manual Decomposition
33
+ ### 3. Decompose Task
65
34
 
66
- 1. **Select strategy**:
35
+ Use strategy selection and planning:
67
36
 
68
37
  ```
69
- swarm_select_strategy with task="<task description>"
38
+ swarm_select_strategy(task="<the task>")
39
+ swarm_plan_prompt(task="<the task>", strategy="<auto or selected>")
70
40
  ```
71
41
 
72
- 2. **Get planning prompt**:
42
+ Follow the prompt to create a BeadTree, then validate:
73
43
 
74
44
  ```
75
- swarm_plan_prompt with task="<task description>", strategy="<selected or auto>"
45
+ swarm_validate_decomposition(response="<your BeadTree JSON>")
76
46
  ```
77
47
 
78
- 3. **Create decomposition** following the prompt guidelines
79
-
80
- 4. **Validate**:
48
+ ### 4. Create Beads
81
49
 
82
50
  ```
83
- swarm_validate_decomposition with response="<your BeadTree JSON>"
51
+ beads_create_epic(epic_title="<task>", subtasks=[{title, files, priority}...])
84
52
  ```
85
53
 
86
- ### Create Beads
87
-
88
- Once you have a valid BeadTree:
54
+ Rules:
89
55
 
90
- ```
91
- beads_create_epic with epic_title="<parent task>", subtasks=[{title, description, files, priority}...]
92
- ```
56
+ - Each bead completable by one agent
57
+ - Independent where possible (parallelizable)
58
+ - 3-7 beads per swarm
93
59
 
94
- **Decomposition rules:**
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 with paths=[<files>], reason="<bead-id>: <brief description>"
63
+ agentmail_reserve(paths=[<files>], reason="<bead-id>: <description>")
107
64
  ```
108
65
 
109
- **Conflict prevention:**
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
- ## Step 6: Spawn the Swarm
68
+ ### 6. Spawn Agents
115
69
 
116
- **CRITICAL: Spawn ALL agents in a SINGLE message with multiple Task calls.**
70
+ **CRITICAL: Spawn ALL in a SINGLE message with multiple Task calls.**
117
71
 
118
- Use the prompt generator for each subtask:
72
+ For each subtask:
119
73
 
120
74
  ```
121
- swarm_spawn_subtask with bead_id="<bead-id>", epic_id="<epic-id>", subtask_title="<title>", subtask_description="<description>", files=[<files>], shared_context="Branch: swarm/<id>, sync_enabled: true"
75
+ swarm_spawn_subtask(bead_id="<id>", epic_id="<epic>", subtask_title="<title>", files=[...])
122
76
  ```
123
77
 
124
- Then spawn agents with the generated prompts:
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
- Spawn ALL agents in parallel in a single response.
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 with epic_id="<parent-bead-id>"
87
+ swarm_status(epic_id="<epic-id>")
88
+ agentmail_inbox()
142
89
  ```
143
90
 
144
- Monitor inbox for progress updates:
91
+ If incompatibilities spotted, broadcast:
145
92
 
146
93
  ```
147
- agentmail_inbox
94
+ agentmail_send(to=["*"], subject="Coordinator Update", body="<guidance>", importance="high")
148
95
  ```
149
96
 
150
- **When you receive progress updates:**
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
- agentmail_summarize_thread with thread_id="<epic-id>"
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
- ## Step 9: Complete Swarm
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 "$(cat <<'EOF'
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 | Best For | Auto-Detected Keywords |
272
- | ----------------- | --------------------------- | ---------------------------------------------- |
273
- | **file-based** | Refactoring, migrations | refactor, migrate, rename, update all, convert |
274
- | **feature-based** | New features, functionality | add, implement, build, create, feature, new |
275
- | **risk-based** | Bug fixes, security | fix, bug, security, critical, urgent, hotfix |
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
- Use `swarm_select_strategy` to see which strategy is recommended and why.
118
+ Begin decomposition now.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "opencode-swarm-plugin",
3
- "version": "0.12.2",
3
+ "version": "0.12.6",
4
4
  "description": "Multi-agent swarm coordination for OpenCode with learning capabilities, beads integration, and Agent Mail",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
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): void {
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.warn(`[agent-mail] Could not save session state: ${error}`);
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
- // Don't count this attempt against retries - try again
738
- attempt--;
739
- continue;
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
- // Check if error is retryable
745
- if (!isRetryableError(error)) {
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 - we need to use fetch_inbox with specific message
1010
- // Since there's no get_message, we'll use search
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: 1,
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 - return what was created with rollback hint
254
- const rollbackHint = created
255
- .map((b) => `bd close ${b.id} --reason "Rollback partial epic"`)
256
- .join("\n");
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
- const result: EpicCreateResult = {
259
- success: false,
260
- epic: created[0] || ({} as Bead),
261
- subtasks: created.slice(1),
262
- rollback_hint: rollbackHint,
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
- return JSON.stringify(
266
- {
267
- ...result,
268
- error: error instanceof Error ? error.message : String(error),
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 Bun.$`git pull --rebase`.quiet().nothrow();
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 Bun.$`bd sync`.quiet().nothrow();
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 Bun.$`git push`.quiet().nothrow();
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()}`,
@@ -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: perMinuteEnv ? parseInt(perMinuteEnv, 10) : defaults.perMinute,
127
- perHour: perHourEnv ? parseInt(perHourEnv, 10) : defaults.perHour,
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 and count current ones in a pipeline
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
- // Get the oldest entry's timestamp to calculate precise reset
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;
@@ -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-]+-[a-z0-9]+(\.\d+)?$/, "Invalid bead ID format"),
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 */
@@ -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>;