@desplega.ai/agent-swarm 1.148.0 → 1.149.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.
Files changed (179) hide show
  1. package/README.md +2 -1
  2. package/dist/{acp-adapter-grez7xhd.js → acp-adapter-4jncb126.js} +12 -5
  3. package/dist/{actions-kweznh5a.js → actions-31vp3ae7.js} +10 -9
  4. package/dist/{app-5fvvjv9m.js → app-1hh67x8w.js} +6 -6
  5. package/dist/{assistant-40m4w50s.js → assistant-z66dmz6p.js} +13 -11
  6. package/dist/{boot-reembed-cn357y5n.js → boot-reembed-fqzxxzjh.js} +7 -7
  7. package/dist/{boot-reembed-9rxmb26n.js → boot-reembed-wxa8ya0s.js} +8 -8
  8. package/dist/{boot-scrub-logs-5tpp0x3y.js → boot-scrub-logs-jnyndh79.js} +5 -5
  9. package/dist/{claude-adapter-9pz5q2pb.js → claude-adapter-3qpa9w0j.js} +9 -9
  10. package/dist/{claude-managed-adapter-sactwn31.js → claude-managed-adapter-qvsmh6mv.js} +2 -2
  11. package/dist/{claude-sdk-session-v8w0s7qm.js → claude-sdk-session-2yxgjs5d.js} +9 -9
  12. package/dist/{cli-y6b6fb76.js → cli-08b07b4r.js} +1 -1
  13. package/dist/{cli-8fc8de14.js → cli-0z2v4nhw.js} +1 -1
  14. package/dist/{cli-tebsjpk7.js → cli-19f354qr.js} +2 -2
  15. package/dist/{cli-kzvhgvk3.js → cli-2qts1hys.js} +3 -3
  16. package/dist/{cli-kpv03zhj.js → cli-3apbzgyk.js} +27 -15
  17. package/dist/{cli-5khqvvnq.js → cli-3q5ejx9p.js} +2 -2
  18. package/dist/{cli-d8brzxjb.js → cli-4wtq5jjv.js} +2 -2
  19. package/dist/{cli-ct0et58h.js → cli-5my3bjsd.js} +1 -1
  20. package/dist/{cli-cn6mmc7f.js → cli-6664w7y4.js} +1 -1
  21. package/dist/{cli-f8j56xv2.js → cli-6pqcb4q0.js} +53 -13
  22. package/dist/{cli-m1p5cysp.js → cli-7smrsr25.js} +3 -3
  23. package/dist/{cli-0amc6myn.js → cli-9fy6nk6g.js} +1 -1
  24. package/dist/{cli-dcem3pr1.js → cli-9hspd3dp.js} +2 -2
  25. package/dist/{cli-0er5m69z.js → cli-aae6hz6a.js} +4 -4
  26. package/dist/{cli-brxyat7h.js → cli-cr12paw2.js} +5 -5
  27. package/dist/{cli-dk3me3qs.js → cli-d8ftsp62.js} +8 -8
  28. package/dist/{cli-v3e51x7w.js → cli-e95cx1eb.js} +3 -3
  29. package/dist/{cli-ztn0ga15.js → cli-gjhjfeeg.js} +1 -1
  30. package/dist/{cli-xqcpg1st.js → cli-gv88e1vf.js} +1 -1
  31. package/dist/{cli-rvqz34y5.js → cli-jecc01cb.js} +1 -1
  32. package/dist/{cli-hzhhrt7b.js → cli-k0cr4kat.js} +1 -1
  33. package/dist/{cli-hz8xhefy.js → cli-kgz4np97.js} +4 -4
  34. package/dist/{cli-anedxf1w.js → cli-kjwt6hdf.js} +1 -1
  35. package/dist/{cli-3n7cbfjn.js → cli-mnmsfd1w.js} +6 -2
  36. package/dist/{cli-n9grk3yd.js → cli-n3dz2942.js} +37 -26
  37. package/dist/{cli-cdjezzc7.js → cli-nye12xk5.js} +8 -5
  38. package/dist/{cli-9216xm24.js → cli-phsxnkp6.js} +20 -20
  39. package/dist/{cli-7gn7c2af.js → cli-q50ef8g0.js} +1 -1
  40. package/dist/{cli-qy6h009w.js → cli-qaacms9y.js} +7 -6
  41. package/dist/{cli-xqcq9y5e.js → cli-r2ap2czm.js} +1 -0
  42. package/dist/{cli-hp2est46.js → cli-sd5pv3b1.js} +3 -3
  43. package/dist/{cli-0b6y7kxm.js → cli-xm87hzbw.js} +1 -1
  44. package/dist/{cli-6s2y152y.js → cli-yab68w40.js} +285 -11
  45. package/dist/{cli-p73drk6p.js → cli-yb9qhqam.js} +167 -40
  46. package/dist/{cli-56y3hch2.js → cli-ygweqcn4.js} +133 -22
  47. package/dist/{cli-n2cwscdd.js → cli-ykxrnd1q.js} +2 -2
  48. package/dist/{cli-m9sxbkhm.js → cli-zvz62chv.js} +3 -3
  49. package/dist/cli.js +14 -14
  50. package/dist/{codex-adapter-ehhj2qwc.js → codex-adapter-8meawydj.js} +4 -4
  51. package/dist/{codex-hook-bw8p5nh4.js → codex-hook-hfrt9shr.js} +2 -2
  52. package/dist/{codex-session-runner-5hne566k.js → codex-session-runner-gjgxyp5j.js} +4 -4
  53. package/dist/{commands-xnvh951h.js → commands-hz3nb95f.js} +5 -5
  54. package/dist/{db-ycr5zfr4.js → db-ggz97zbm.js} +5 -5
  55. package/dist/{e2b-6hb103d8.js → e2b-p04vmtx0.js} +1 -1
  56. package/dist/{handlers-1vt77kgq.js → handlers-nbzdzn9k.js} +14 -11
  57. package/dist/{hook-91ntpmff.js → hook-bks2q1f6.js} +7 -7
  58. package/dist/{hook-qfwjydvn.js → hook-kq9wp0ep.js} +8 -8
  59. package/dist/{http-h6g7bv5b.js → http-hrf77v87.js} +96 -45
  60. package/dist/{index-47ghesbn.js → index-6nsbg88z.js} +443 -144
  61. package/dist/{index-antf9t4p.js → index-8hmeg8j1.js} +14 -14
  62. package/dist/{index-d2mytx95.js → index-cqtebbhb.js} +13 -13
  63. package/dist/{index-0ez2aack.js → index-n28pw1zt.js} +15 -15
  64. package/dist/{keepalive-c4sp7yvd.js → keepalive-dxc12gvg.js} +7 -7
  65. package/dist/{lead-4r7z9e8v.js → lead-yckfn18d.js} +32 -32
  66. package/dist/{maintenance-yqjfdjas.js → maintenance-6kfb2m5e.js} +8 -8
  67. package/dist/{oauth-refresh-sweep-wtqgnh3c.js → oauth-refresh-sweep-1x1ac722.js} +6 -6
  68. package/dist/{onboard-a4krmh46.js → onboard-dcyt1609.js} +4 -3
  69. package/dist/{opencode-adapter-fef1mrn2.js → opencode-adapter-dce4wskd.js} +2 -2
  70. package/dist/{otel-impl-725kq0vp.js → otel-impl-tbk3e5dr.js} +2 -2
  71. package/dist/{pi-mono-adapter-2has4gnp.js → pi-mono-adapter-zvshk5cj.js} +2 -2
  72. package/dist/{pricing-refresh-kqxd7w9h.js → pricing-refresh-kaqg567e.js} +7 -7
  73. package/dist/{rbac-roles-gws5xej6.js → rbac-roles-ammq6rd2.js} +5 -5
  74. package/dist/{rbac-roles-t3wygpth.js → rbac-roles-cf97v1vd.js} +6 -6
  75. package/dist/{render-v2-x1wxvn51.js → render-v2-80w603vy.js} +6 -6
  76. package/dist/{seed-pricing-t6y8d0vf.js → seed-pricing-zbrn83xv.js} +6 -6
  77. package/dist/{setup-x50py6xg.js → setup-fe66kczs.js} +2 -2
  78. package/dist/{worker-zsz0qfxk.js → worker-qmb2d1e2.js} +32 -32
  79. package/dist/{x-n2phr5vm.js → x-n3e0v3xa.js} +2 -2
  80. package/openapi.json +46 -4
  81. package/package.json +1 -1
  82. package/src/agentmail/handlers.ts +5 -0
  83. package/src/automation-preflight-alert.ts +41 -0
  84. package/src/be/automation-preflight.ts +3 -3
  85. package/src/be/budget-refusal-notify.ts +1 -0
  86. package/src/be/db/tasks/read.ts +5 -0
  87. package/src/be/db.ts +132 -2
  88. package/src/be/migrations/150_deferred_task_waits.sql +18 -0
  89. package/src/be/migrations/151_repair_scheduled_task_required_params.sql +39 -0
  90. package/src/be/migrations/152_routing_decisions.sql +44 -0
  91. package/src/be/scripts/typecheck.ts +33 -22
  92. package/src/be/seed-scripts/catalog/delegate.ts +5 -1
  93. package/src/be/seed-skills/bundled-files.generated.json +10 -0
  94. package/src/be/seed-skills/index.ts +3 -0
  95. package/src/be/steering.ts +1 -0
  96. package/src/be/swarm-config-guard.ts +11 -0
  97. package/src/commands/onboard/steps/post-task.tsx +1 -0
  98. package/src/github/handlers.ts +9 -0
  99. package/src/gitlab/handlers.ts +4 -0
  100. package/src/heartbeat/heartbeat.ts +8 -0
  101. package/src/http/approval-requests.ts +1 -0
  102. package/src/http/apps.ts +1 -0
  103. package/src/http/config.ts +15 -6
  104. package/src/http/mcp.ts +31 -4
  105. package/src/http/script-runs.ts +6 -0
  106. package/src/http/tasks.ts +52 -30
  107. package/src/integrations/kapso/inbound.ts +1 -0
  108. package/src/jira/sync.ts +2 -0
  109. package/src/linear/sync.ts +2 -0
  110. package/src/prompts/session-templates.ts +29 -15
  111. package/src/scheduler/deferred-task-waits.ts +123 -0
  112. package/src/scheduler/schedule-task.ts +42 -0
  113. package/src/scheduler/scheduler.ts +22 -31
  114. package/src/script-workflows/workflow-ctx.ts +2 -0
  115. package/src/scripts-runtime/types/stdlib.d.ts +38 -24
  116. package/src/scripts-runtime/types/swarm-sdk.d.ts +38 -24
  117. package/src/server.ts +10 -1
  118. package/src/slack/actions.ts +1 -0
  119. package/src/slack/assistant.ts +2 -0
  120. package/src/slack/handlers.ts +3 -0
  121. package/src/slack/thread-buffer.ts +2 -0
  122. package/src/tasks/worker-follow-up.ts +9 -0
  123. package/src/tests/acp-adapter.test.ts +35 -6
  124. package/src/tests/acp-session-token.test.ts +67 -0
  125. package/src/tests/asset-key-api.test.ts +15 -2
  126. package/src/tests/asset-key-mcp.test.ts +2 -0
  127. package/src/tests/automation-preflight-alert.test.ts +91 -0
  128. package/src/tests/claude-worker-parity.test.ts +3 -0
  129. package/src/tests/config-session-auth.test.ts +143 -0
  130. package/src/tests/defer-task.test.ts +117 -6
  131. package/src/tests/deferred-task-wake.test.ts +456 -0
  132. package/src/tests/heartbeat-reroute-decision.test.ts +1 -0
  133. package/src/tests/http-api-integration.test.ts +33 -4
  134. package/src/tests/model-control.test.ts +18 -5
  135. package/src/tests/opencode-adapter.test.ts +2 -2
  136. package/src/tests/promote-draft-task-route.test.ts +4 -0
  137. package/src/tests/prompt-template-session.test.ts +13 -4
  138. package/src/tests/rbac-wire-e2e.test.ts +2 -2
  139. package/src/tests/routing-decision-persistence.test.ts +520 -0
  140. package/src/tests/routing-reason-contract.test.ts +89 -0
  141. package/src/tests/routing-reason-inventory.test.ts +55 -0
  142. package/src/tests/schedule-target-type.test.ts +48 -1
  143. package/src/tests/scheduled-task-required-params-migration.test.ts +182 -0
  144. package/src/tests/scheduled-tasks.test.ts +9 -5
  145. package/src/tests/script-connections.test.ts +4 -0
  146. package/src/tests/script-runs-http.test.ts +31 -0
  147. package/src/tests/scripts-typecheck.test.ts +11 -0
  148. package/src/tests/secret-scrubber.test.ts +12 -0
  149. package/src/tests/send-task-output-schema.test.ts +66 -1
  150. package/src/tests/send-task-requested-by.test.ts +1 -0
  151. package/src/tests/send-task-slack-routing-guard.test.ts +2 -0
  152. package/src/tests/store-progress-blocked-waiting-gate.test.ts +151 -0
  153. package/src/tests/swarm-tool-result-gate.test.ts +24 -0
  154. package/src/tests/task-tool-manifest.test.ts +47 -0
  155. package/src/tests/task-tool-preload-mcp.test.ts +125 -0
  156. package/src/tests/task-tools-ctx.test.ts +2 -0
  157. package/src/tests/tool-output-agent-id.test.ts +1 -0
  158. package/src/tests/ui-followup-lead-delegation.test.ts +6 -1
  159. package/src/tests/workflow-agent-task.test.ts +24 -1
  160. package/src/tests/workflow-engine-v2.test.ts +58 -2
  161. package/src/tools/defer-task.ts +73 -21
  162. package/src/tools/send-task.ts +31 -0
  163. package/src/tools/skills/skill-publish.ts +1 -0
  164. package/src/tools/store-progress.ts +43 -0
  165. package/src/tools/templates.ts +2 -1
  166. package/src/tools/utils.ts +33 -2
  167. package/src/types.ts +14 -1
  168. package/src/utils/acp-session-token.ts +14 -4
  169. package/src/utils/secret-scrubber.ts +2 -0
  170. package/src/utils/task-tool-manifest.ts +33 -0
  171. package/src/workflows/engine.ts +4 -1
  172. package/src/workflows/executors/agent-task.ts +7 -1
  173. package/templates/ai-toolbox.manifest.json +6 -1
  174. package/templates/skills/comms/config.json +18 -0
  175. package/templates/skills/comms/content.md +74 -0
  176. package/templates/skills/comms/files/references/ste-rules.md +73 -0
  177. package/templates/skills/comms/files/references/visual-shapes.md +112 -0
  178. package/templates/skills/swarm-scripts/SKILL.md +3 -2
  179. package/templates/skills/swarm-scripts/content.md +3 -2
@@ -4,12 +4,14 @@ import * as z from "zod";
4
4
  import { resolveTaskAuditUserId } from "@/be/audit-user";
5
5
  import {
6
6
  completeTask,
7
+ createLogEntry,
7
8
  createScheduledTask,
8
9
  getAgentById,
9
10
  getDbClient,
10
11
  getTaskById,
11
12
  updateAgentStatusFromCapacity,
12
13
  } from "@/be/db";
14
+ import { reconcileDeferredTaskWaits } from "@/scheduler/deferred-task-waits";
13
15
  import { runTaskTerminalEffects } from "@/tasks/task-terminal-effects";
14
16
  import { getTaskOutputValidationError } from "@/tasks/terminal-result-guard";
15
17
  import { assertOwnsTask, ownerCtx } from "@/tools/task-tool-ctx";
@@ -32,7 +34,7 @@ export const registerDeferTaskTool = (server: McpServer) => {
32
34
  title: "Defer Task",
33
35
  annotations: { destructiveHint: false, idempotentHint: false },
34
36
  description:
35
- "Completes this task now with status `completed` and books a wake-up for you. Use when the result needs time: a build, a deploy, a reply. The task reaches its final state on this call; the lead sees your summary as its output. A one-off schedule wakes you up later with a child task that carries this task as its parent. Provide delayMs or runAt, a summary of what you did, and a note that says what is pending and what to check.",
37
+ "Completes this task now with status `completed` and books a wake-up for you. Use when the result needs time: a build, a deploy, a reply. The task reaches its final state on this call; the lead sees your summary as its output unless the task has an outputSchema. For a task with an outputSchema, provide output as a JSON string matching that schema; it is stored verbatim as terminal output, while deferral details remain visible in the task log. A one-off schedule wakes you up later with a child task that carries this task as its parent. Optionally provide wakeOn to wake early when another task completes or fails; delayMs or runAt remains required as the ceiling. Provide delayMs or runAt, a summary of what you did, and a note that says what is pending and what to check.",
36
38
  inputSchema: z.object({
37
39
  taskId: z.string().describe("The ID of the task you are working on."),
38
40
  delayMs: z
@@ -46,12 +48,28 @@ export const registerDeferTaskTool = (server: McpServer) => {
46
48
  .datetime()
47
49
  .optional()
48
50
  .describe("Wake up at this ISO datetime (e.g. '2026-03-06T15:00:00Z'). Must be future."),
51
+ wakeOn: z
52
+ .object({
53
+ event: z.enum(["task.completed", "task.failed", "settled"]),
54
+ taskId: z.string().min(1),
55
+ })
56
+ .strict()
57
+ .optional()
58
+ .describe(
59
+ "Wake early on this task event. settled covers completed or failed. Already-terminal tasks are rejected; a delayMs/runAt ceiling is still required.",
60
+ ),
49
61
  summary: z
50
62
  .string()
51
63
  .min(1)
52
64
  .max(4000)
53
65
  .describe(
54
- "What you did so far and where things stand. This becomes the task's output; the lead and your wake-up run both read it.",
66
+ "What you did so far and where things stand. Stored in the task log for tasks with an outputSchema; otherwise becomes the task's output.",
67
+ ),
68
+ output: z
69
+ .string()
70
+ .optional()
71
+ .describe(
72
+ "Required when the task has an outputSchema: a JSON string matching that schema, stored verbatim as terminal output. Ignored for tasks without an outputSchema.",
55
73
  ),
56
74
  note: z
57
75
  .string()
@@ -71,7 +89,11 @@ export const registerDeferTaskTool = (server: McpServer) => {
71
89
  nextRunAt: z.string().optional(),
72
90
  }),
73
91
  },
74
- async ({ taskId, delayMs, runAt, summary, note, checks }, requestInfo, _meta) => {
92
+ async (
93
+ { taskId, delayMs, runAt, wakeOn, summary, output, note, checks },
94
+ requestInfo,
95
+ _meta,
96
+ ) => {
75
97
  if (!requestInfo.agentId) {
76
98
  return toolErr('Agent ID not found. Set the "X-Agent-ID" header.');
77
99
  }
@@ -122,26 +144,37 @@ export const registerDeferTaskTool = (server: McpServer) => {
122
144
  }
123
145
  const nextRunAt = delayMs ? new Date(Date.now() + delayMs).toISOString() : runAt!;
124
146
 
147
+ const wakeDescription = wakeOn
148
+ ? `on ${wakeOn.event} for task ${wakeOn.taskId}, or by ${nextRunAt}`
149
+ : `at ${nextRunAt}`;
125
150
  const checksBlock = renderChecks(checks);
126
151
  const taskTemplate = `Resume task ${taskId}: ${note}${checksBlock}`;
127
152
  const createdBy =
128
153
  (await resolveTaskAuditUserId(requestInfo.sourceTaskId, requestInfo.agentId)) ?? undefined;
129
154
 
130
- // The deferral note is written as the task's FINAL output — a task with
131
- // an outputSchema must satisfy it on completion (store-progress enforces
132
- // the same rule). Validate before creating anything: the real schedule
133
- // id isn't known yet, but it never changes whether this prose is valid
134
- // JSON against the schema, so a placeholder stands in for it here.
135
- const previewOutput = `${summary}\n\nDeferred until ${nextRunAt} (schedule pending). Pending: ${note}${checksBlock}`;
136
- const outputValidationError = getTaskOutputValidationError(task.outputSchema, previewOutput);
137
- if (outputValidationError) {
138
- return toolErr(
139
- `Task ${taskId} has an outputSchema; its terminal output must satisfy it, but a deferral note cannot. ${outputValidationError}`,
140
- );
155
+ // Validate before creating the schedule or changing the task.
156
+ if (task.outputSchema) {
157
+ if (!output) {
158
+ return toolErr(
159
+ `Task ${taskId} has an outputSchema. Call defer-task with output: a JSON string matching that schema. Summary and note are stored separately in the task log.`,
160
+ );
161
+ }
162
+ const outputValidationError = getTaskOutputValidationError(task.outputSchema, output);
163
+ if (outputValidationError) return toolErr(outputValidationError);
141
164
  }
142
165
 
143
166
  try {
144
167
  const committed = await getDbClient().transaction(async () => {
168
+ if (wakeOn) {
169
+ if (wakeOn.taskId === taskId)
170
+ throw new DeferAbortedError("Cannot wake on the task being deferred.");
171
+ const watched = await getTaskById(wakeOn.taskId);
172
+ if (!watched) throw new DeferAbortedError(`Watched task ${wakeOn.taskId} not found.`);
173
+ if (isTerminalTaskStatus(watched.status))
174
+ throw new DeferAbortedError(
175
+ `Watched task ${wakeOn.taskId} is already ${watched.status}; read its result instead of deferring.`,
176
+ );
177
+ }
145
178
  const schedule = await createScheduledTask({
146
179
  // Unique name (`getScheduledTaskByName` is a unique lookup). The UUID
147
180
  // prevents concurrent deferrals of the same task from colliding.
@@ -165,12 +198,22 @@ export const registerDeferTaskTool = (server: McpServer) => {
165
198
  createdBy,
166
199
  });
167
200
 
168
- const output = `${summary}\n\nDeferred until ${nextRunAt} (schedule ${schedule.id}). Pending: ${note}${checksBlock}`;
201
+ if (wakeOn) {
202
+ await getDbClient().run(
203
+ "INSERT INTO deferred_task_waits (scheduleId, taskId, eventName, created_by, updated_by) VALUES (?, ?, ?, ?, ?)",
204
+ [schedule.id, wakeOn.taskId, wakeOn.event, createdBy ?? null, createdBy ?? null],
205
+ );
206
+ getDbClient().afterCommit(() => {
207
+ void reconcileDeferredTaskWaits(wakeOn.taskId).catch((err) => {
208
+ console.error("[defer-task] Event wake reconciliation failed:", err);
209
+ });
210
+ });
211
+ }
212
+
213
+ const deferralDetails = `${summary}\n\nDeferred until ${nextRunAt} (schedule ${schedule.id}). Pending: ${note}${checksBlock}`;
169
214
 
170
- // Deliberately NOT running `getTaskOutputValidationError` again here:
171
- // already validated above against a placeholder id; re-run would be
172
- // redundant since the id never affects JSON-shape validity.
173
- const completed = await completeTask(taskId, output);
215
+ const terminalOutput = task.outputSchema ? output! : deferralDetails;
216
+ const completed = await completeTask(taskId, terminalOutput);
174
217
  if (!completed) {
175
218
  // Another writer terminally completed/failed/cancelled this task
176
219
  // between our early check and this transaction's write. Abort:
@@ -182,6 +225,15 @@ export const registerDeferTaskTool = (server: McpServer) => {
182
225
  );
183
226
  }
184
227
 
228
+ if (task.outputSchema) {
229
+ await createLogEntry({
230
+ eventType: "task_progress",
231
+ taskId,
232
+ agentId: requestInfo.agentId,
233
+ newValue: deferralDetails,
234
+ });
235
+ }
236
+
185
237
  // afterCommit: the transaction can still roll back; business-use must
186
238
  // not be told the task completed for a write that never landed.
187
239
  getDbClient().afterCommit(() => {
@@ -207,7 +259,7 @@ export const registerDeferTaskTool = (server: McpServer) => {
207
259
  await updateAgentStatusFromCapacity(task.agentId);
208
260
  }
209
261
 
210
- return { scheduleId: schedule.id, output, completed };
262
+ return { scheduleId: schedule.id, output: terminalOutput, completed };
211
263
  });
212
264
 
213
265
  await runTaskTerminalEffects({
@@ -218,7 +270,7 @@ export const registerDeferTaskTool = (server: McpServer) => {
218
270
  });
219
271
 
220
272
  return toolOk(
221
- `Task ${taskId} completed and deferred. Wake-up at ${nextRunAt} (schedule ${committed.scheduleId}). This task is final; the wake-up task continues the work.`,
273
+ `Task ${taskId} completed and deferred. Wake-up ${wakeDescription} (schedule ${committed.scheduleId}). This task is final; the wake-up task continues the work.`,
222
274
  {
223
275
  data: {
224
276
  yourAgentId: requestInfo.agentId,
@@ -31,6 +31,7 @@ import {
31
31
  FollowUpConfigSchema,
32
32
  ModelTierSchema,
33
33
  ReasoningEffortSchema,
34
+ RoutingReasonSchema,
34
35
  splitLegacyModelAlias,
35
36
  } from "@/types";
36
37
  import { findJsonSchemaShapeErrors } from "@/workflows/json-schema-validator";
@@ -61,6 +62,14 @@ export const sendTaskInputSchema = z
61
62
  .string()
62
63
  .optional()
63
64
  .describe("The agent to assign/offer task to. Omit to create unassigned task for pool."),
65
+ routingReason: RoutingReasonSchema.optional().describe(
66
+ "Why this agent was selected. Required when agentId is supplied; omit for pool routing.",
67
+ ),
68
+ routingNote: z
69
+ .string()
70
+ .max(200)
71
+ .optional()
72
+ .describe("Optional routing context (maximum 200 characters)."),
64
73
  task: z.string().min(1).describe("The task description to send."),
65
74
  key: AssetKeySchema.optional().describe(
66
75
  "Logical namespace key. Child tasks inherit their parent namespace when provided.",
@@ -174,6 +183,13 @@ export const sendTaskInputSchema = z
174
183
  path: [hasChannel ? "slackThreadTs" : "slackChannelId"],
175
184
  });
176
185
  }
186
+ if (data.agentId !== undefined && data.routingReason === undefined) {
187
+ ctx.addIssue({
188
+ code: z.ZodIssueCode.custom,
189
+ message: "routingReason is required when agentId is supplied.",
190
+ path: ["routingReason"],
191
+ });
192
+ }
177
193
  checkOutputSchemaShape(data.outputSchema, ctx);
178
194
  });
179
195
 
@@ -222,6 +238,8 @@ export async function sendTaskHandler(
222
238
  ctx: ToolCtx,
223
239
  {
224
240
  agentId,
241
+ routingReason,
242
+ routingNote,
225
243
  task,
226
244
  key,
227
245
  offerMode,
@@ -247,6 +265,10 @@ export async function sendTaskHandler(
247
265
  outputSchema,
248
266
  }: SendTaskArgs,
249
267
  ): Promise<SwarmToolResult> {
268
+ // Defense in depth for direct TypeScript callers that bypass MCP schema parsing.
269
+ if (agentId !== undefined && routingReason === undefined) {
270
+ return toolErr("routingReason is required when agentId is supplied.");
271
+ }
250
272
  if (ctx.kind === "owner" && !ctx.agentId) {
251
273
  return toolErr('Agent ID not found. The MCP client should define the "X-Agent-ID" header.', {
252
274
  data: { yourAgentId: ctx.agentId },
@@ -339,6 +361,9 @@ export async function sendTaskHandler(
339
361
  effectiveAgentId = effectiveParentTask.agentId;
340
362
  }
341
363
  }
364
+ const effectiveRoutingReason =
365
+ agentId !== undefined ? routingReason : effectiveAgentId ? "continuity" : undefined;
366
+ const effectiveRoutingNote = effectiveRoutingReason ? routingNote : undefined;
342
367
 
343
368
  // The three dedup guards are pure reads, so they run twice: once here as a
344
369
  // fast path (keeping this tool's existing early-exit responses), and once
@@ -470,6 +495,8 @@ export async function sendTaskHandler(
470
495
  overrideSlackContext,
471
496
  followUpConfig,
472
497
  outputSchema,
498
+ routingReason: effectiveRoutingReason,
499
+ routingNote: effectiveRoutingNote,
473
500
  routingAffinity:
474
501
  effectiveLeadOnly || requiredCapabilities?.length
475
502
  ? { leadOnly: effectiveLeadOnly, capabilities: requiredCapabilities ?? [] }
@@ -537,6 +564,8 @@ export async function sendTaskHandler(
537
564
  overrideSlackContext,
538
565
  followUpConfig,
539
566
  outputSchema,
567
+ routingReason: effectiveRoutingReason,
568
+ routingNote: effectiveRoutingNote,
540
569
  routingAffinity:
541
570
  effectiveLeadOnly || requiredCapabilities?.length
542
571
  ? { leadOnly: effectiveLeadOnly, capabilities: requiredCapabilities ?? [] }
@@ -578,6 +607,8 @@ export async function sendTaskHandler(
578
607
  overrideSlackContext,
579
608
  followUpConfig,
580
609
  outputSchema,
610
+ routingReason: effectiveRoutingReason,
611
+ routingNote: effectiveRoutingNote,
581
612
  routingAffinity:
582
613
  effectiveLeadOnly || requiredCapabilities?.length
583
614
  ? { leadOnly: effectiveLeadOnly, capabilities: requiredCapabilities ?? [] }
@@ -71,6 +71,7 @@ To reject: close this task with a rejection reason.`;
71
71
 
72
72
  const task = await createTaskExtended(taskDescription, {
73
73
  agentId: leadAgent.id,
74
+ routingReason: "skill",
74
75
  creatorAgentId: requestInfo.agentId,
75
76
  source: "mcp",
76
77
  taskType: "skill-approval",
@@ -29,6 +29,17 @@ import { scrubSecrets } from "@/utils/secret-scrubber";
29
29
  // echoed the schema example, producing noise rows keyed `mcp-<taskId>-<ts>`
30
30
  // that double-counted alongside the harness's authoritative entry.
31
31
 
32
+ // Deliberately narrow and phrase-based (not a bare "wait"/"block" substring
33
+ // match) to under-fire rather than over-fire: measured against 196 real
34
+ // progress rows across all statuses, this matched 0 — see PR body for the
35
+ // full false-positive measurement methodology.
36
+ const BLOCKED_WAITING_PATTERN =
37
+ /\b(waiting (for|on)|blocked (on|until|by)|still waiting|awaiting)\b/i;
38
+
39
+ // Below this, two check-ins are close enough together that "blocked" reads as
40
+ // noise and calling defer-task buys nothing over checking in again shortly.
41
+ const BLOCKED_WAITING_MIN_ELAPSED_MS = 3 * 60 * 1000;
42
+
32
43
  export const storeProgressOutputSchema = swarmToolOutputSchema({
33
44
  // Bounded confirmation only. The handler keeps the full task row internally
34
45
  // for completion memory, raters, and follow-up creation, but never echoes it
@@ -56,6 +67,12 @@ export const storeProgressOutputSchema = swarmToolOutputSchema({
56
67
  .describe(
57
68
  "True when force: true replaced output and/or failureReason on an already-terminal task without replaying completion side effects.",
58
69
  ),
70
+ blockedWaitingElapsedMs: z
71
+ .number()
72
+ .optional()
73
+ .describe(
74
+ "Present only when this progress text reads as blocked-waiting: milliseconds since the task's prior update. Drives the store-progress nudge toward defer-task.",
75
+ ),
59
76
  });
60
77
 
61
78
  export const registerStoreProgressTool = (server: McpServer) => {
@@ -251,6 +268,27 @@ export const registerStoreProgressTool = (server: McpServer) => {
251
268
 
252
269
  let updatedTask = existingTask;
253
270
  const isTerminal = isTerminalTaskStatus(existingTask.status);
271
+ // This call's own status can finish the task even though existingTask
272
+ // (its state before this call) is not yet terminal — gate on both so a
273
+ // completing call carrying blocked-waiting-shaped text (e.g. "awaiting
274
+ // review") never nudges toward defer-task.
275
+ const goingTerminal = status !== undefined && isTerminalTaskStatus(status);
276
+
277
+ // Computed against the task's state as of BEFORE this call's update,
278
+ // so "elapsed" reads as time since the prior check-in, not zero.
279
+ let blockedWaitingElapsedMs: number | undefined;
280
+ if (progress && !isTerminal && !goingTerminal && BLOCKED_WAITING_PATTERN.test(progress)) {
281
+ const referenceIso = existingTask.lastUpdatedAt ?? existingTask.createdAt;
282
+ if (referenceIso) {
283
+ const elapsed = Date.now() - new Date(referenceIso).getTime();
284
+ // Below the floor, a sub-minute check-in reads as "blocked" purely
285
+ // from noise, and defer-task buys nothing over just checking in
286
+ // again shortly — so treat it as not blocked-waiting yet.
287
+ if (elapsed >= BLOCKED_WAITING_MIN_ELAPSED_MS) {
288
+ blockedWaitingElapsedMs = elapsed;
289
+ }
290
+ }
291
+ }
254
292
 
255
293
  // Attachments — pointer-based, append-only. Insert each row inside
256
294
  // this transaction; the helper dedups by sha256 (when present) or by
@@ -422,6 +460,7 @@ export const registerStoreProgressTool = (server: McpServer) => {
422
460
  ? `Task "${taskId}" marked as ${status}.`
423
461
  : `Progress stored for task "${taskId}".`,
424
462
  task: updatedTask,
463
+ blockedWaitingElapsedMs,
425
464
  };
426
465
  });
427
466
 
@@ -463,6 +502,10 @@ export const registerStoreProgressTool = (server: McpServer) => {
463
502
  ...("wasForcedOverwrite" in result && result.wasForcedOverwrite
464
503
  ? { wasForcedOverwrite: true }
465
504
  : {}),
505
+ ...("blockedWaitingElapsedMs" in result &&
506
+ typeof result.blockedWaitingElapsedMs === "number"
507
+ ? { blockedWaitingElapsedMs: result.blockedWaitingElapsedMs }
508
+ : {}),
466
509
  };
467
510
  return success ? toolOk(message, { data }) : toolErr(message, { data });
468
511
  },
@@ -197,6 +197,7 @@ Use the crashed agent's identity above as context for who was on it and what kin
197
197
 
198
198
  Dispatch via \`send-task\` with ALL of:
199
199
  - an explicit \`agentId\` (the chosen worker) — REQUIRED. If you omit it, \`send-task\` auto-routes to the original task's agent, which is the dead worker, and the work re-strands.
200
+ - \`routingReason: "reroute_fault"\`
200
201
  - \`taskType: "resume"\`
201
202
  - the tag \`resume-generation:{{generation_next}}\`
202
203
  - \`parentTaskId: {{original_task_id}}\`
@@ -251,7 +252,7 @@ Required capabilities: {{required_capabilities}}{{artifacts_block}}
251
252
 
252
253
  This task has been sitting unassigned because no currently-registered agent matches its required role/capabilities. Pick an agent to take this work over and RE-DELEGATE it — do NOT execute it yourself.
253
254
 
254
- Dispatch via \`send-task\` with an explicit \`agentId\` (REQUIRED — omitting it re-pools the work under the same affinity tag and it will starve again) and \`parentTaskId: {{original_task_id}}\`.
255
+ Dispatch via \`send-task\` with an explicit \`agentId\` (REQUIRED — omitting it re-pools the work under the same affinity tag and it will starve again), \`routingReason: "overflow"\`, and \`parentTaskId: {{original_task_id}}\`.
255
256
 
256
257
  This work will NOT fall back to the unassigned pool — you are the only re-delegation path.`,
257
258
  variables: [
@@ -246,6 +246,23 @@ const slackReadNudge = (r: SwarmToolResult): string | undefined => {
246
246
  : undefined;
247
247
  };
248
248
 
249
+ // Sole caller (storeProgressBlockedWaitingNudge) only reaches this once ms is
250
+ // past BLOCKED_WAITING_MIN_ELAPSED_MS (3 minutes), so there's no sub-minute case.
251
+ function formatIdleDuration(ms: number): string {
252
+ const minutes = Math.round(ms / 60_000);
253
+ if (minutes < 60) return `${minutes}m`;
254
+ const hours = Math.floor(minutes / 60);
255
+ const remMinutes = minutes % 60;
256
+ return remMinutes > 0 ? `${hours}h${remMinutes}m` : `${hours}h`;
257
+ }
258
+
259
+ const storeProgressBlockedWaitingNudge = (r: SwarmToolResult): string | undefined => {
260
+ if (!r.ok) return undefined;
261
+ const ms = (r.data as { blockedWaitingElapsedMs?: unknown } | undefined)?.blockedWaitingElapsedMs;
262
+ if (typeof ms !== "number") return undefined;
263
+ return `That reads as blocked-waiting, ${formatIdleDuration(ms)} since your last update — call defer-task so a wake-up task resumes you instead of polling manually.`;
264
+ };
265
+
249
266
  const workflowLongScriptTimeoutNudge = (r: SwarmToolResult): string | undefined => {
250
267
  if (!r.ok) return undefined;
251
268
  const hint = (r.data as { longScriptTimeoutHint?: unknown } | undefined)?.longScriptTimeoutHint;
@@ -260,6 +277,7 @@ const workflowLongScriptTimeoutNudge = (r: SwarmToolResult): string | undefined
260
277
  export const NUDGES: Record<string, (result: SwarmToolResult) => string | undefined> = {
261
278
  "defer-task": (r) =>
262
279
  r.ok ? "Stop working on this task now; the wake-up task will carry your note." : undefined,
280
+ "store-progress": storeProgressBlockedWaitingNudge,
263
281
  "script-run": scriptRunNudge,
264
282
  "script-upsert": scriptAuthoringNudge,
265
283
  "launch-script-run": scriptAuthoringNudge,
@@ -745,6 +763,13 @@ type ToolConfig<
745
763
  _meta?: Record<string, unknown>;
746
764
  };
747
765
 
766
+ const preloadedToolsByServer = new WeakMap<McpServer, ReadonlySet<string>>();
767
+
768
+ /** Configure before registration. The set belongs to one MCP session, never the fleet. */
769
+ export function setPreloadedTools(server: McpServer, names: readonly string[]): void {
770
+ preloadedToolsByServer.set(server, new Set(names));
771
+ }
772
+
748
773
  /**
749
774
  * Creates a tool registration helper that automatically extracts request info
750
775
  * and passes it as the second parameter to the callback.
@@ -770,10 +795,13 @@ export const createToolRegistrar = (server: McpServer) => {
770
795
  config: ToolConfig<InputArgs, OutputArgs>,
771
796
  cb: ToolCallbackWithInfo<InputArgs>,
772
797
  ) => {
798
+ const toolConfig = preloadedToolsByServer.get(server)?.has(name)
799
+ ? { ...config, _meta: { ...config._meta, "anthropic/alwaysLoad": true } }
800
+ : config;
773
801
  // When inputSchema is undefined, the MCP SDK calls handler(extra) with a single arg.
774
802
  // When inputSchema is defined, it calls handler(args, extra) with two args.
775
803
  if (config.inputSchema === undefined) {
776
- return server.registerTool(name, config, (async (meta: Meta) => {
804
+ return server.registerTool(name, toolConfig, (async (meta: Meta) => {
777
805
  const requestInfo = getRequestInfo(meta);
778
806
  return withSpan(
779
807
  "mcp.tool",
@@ -793,7 +821,10 @@ export const createToolRegistrar = (server: McpServer) => {
793
821
  }) as Parameters<typeof server.registerTool>[2]);
794
822
  }
795
823
 
796
- return server.registerTool(name, config, (async (args: InferInput<InputArgs>, meta: Meta) => {
824
+ return server.registerTool(name, toolConfig, (async (
825
+ args: InferInput<InputArgs>,
826
+ meta: Meta,
827
+ ) => {
797
828
  const requestInfo = getRequestInfo(meta);
798
829
  return withSpan(
799
830
  // Span name carries the tool: a static `mcp.tool` is unreadable in a
package/src/types.ts CHANGED
@@ -340,6 +340,15 @@ export const AgentTaskSourceSchema = z.enum([
340
340
  ]);
341
341
  export type AgentTaskSource = z.infer<typeof AgentTaskSourceSchema>;
342
342
 
343
+ export const RoutingReasonSchema = z.enum([
344
+ "skill",
345
+ "continuity",
346
+ "overflow",
347
+ "human_pinned",
348
+ "reroute_fault",
349
+ ]);
350
+ export type RoutingReason = z.infer<typeof RoutingReasonSchema>;
351
+
343
352
  // ---------------------------------------------------------------------------
344
353
  // Harness Provider
345
354
  // ---------------------------------------------------------------------------
@@ -504,6 +513,8 @@ export const AgentTaskSchema = z
504
513
  title: z.string().optional(), // Human-facing display title override (e.g. session rename); falls back to `task` when unset
505
514
  status: AgentTaskStatusSchema,
506
515
  source: AgentTaskSourceSchema.default("mcp"),
516
+ routingReason: RoutingReasonSchema.optional(),
517
+ routingNote: z.string().max(200).optional(),
507
518
 
508
519
  // Task metadata
509
520
  taskType: z.string().max(50).optional(), // e.g., "bug", "feature", "chore"
@@ -664,6 +675,8 @@ export const CreateTaskOptionsSchema = z.object({
664
675
  agentId: z.string().nullable().optional(),
665
676
  creatorAgentId: z.string().optional(),
666
677
  source: AgentTaskSourceSchema.optional(),
678
+ routingReason: RoutingReasonSchema.optional(),
679
+ routingNote: z.string().max(200).optional(),
667
680
  taskType: z.string().max(50).optional(),
668
681
  tags: z.array(z.string()).optional(),
669
682
  priority: z.number().int().min(0).max(100).optional(),
@@ -1936,7 +1949,7 @@ export const WorkflowNodeSchema = z
1936
1949
  config: z
1937
1950
  .record(z.string(), z.unknown())
1938
1951
  .describe(
1939
- "Executor-specific config. For agent-task: { template, outputSchema?, agentId?, tags?, priority?, dir?, vcsRepo?, model? }. " +
1952
+ "Executor-specific config. For agent-task: { template, outputSchema?, agentId?, routingReason?, routingNote?, tags?, priority?, dir?, vcsRepo?, model? }; configured agentId defaults routingReason to human_pinned. " +
1940
1953
  "For script: { runtime, script, args?, timeout? }. " +
1941
1954
  "For swarm-script: { scriptName, scope?, pinHash?, args?, fsMode?, timeoutMs? (1000-300000) }. " +
1942
1955
  "Agent-task templates and ordinary config values support {{interpolation}} from the node's inputs context, including trigger and declared upstream aliases. " +
@@ -5,6 +5,9 @@
5
5
  * key for a short-lived aseph_ bearer before handing credentials to the
6
6
  * ACP target process.
7
7
  */
8
+ import { scrubSecrets } from "./secret-scrubber";
9
+
10
+ const REVOKE_TIMEOUT_MS = 5000;
8
11
 
9
12
  /** Wall-clock TTL for a session token: 24 hours. Long enough for any realistic
10
13
  * ACP session; the token is actively revoked when the session finishes anyway. */
@@ -38,7 +41,8 @@ export async function mintAcpSessionToken(
38
41
 
39
42
  /**
40
43
  * Ask the swarm API to revoke a previously-minted `aseph_` token. Best-effort:
41
- * errors are logged but never propagated to the caller.
44
+ * the request is bounded, and errors are logged but never propagated to the
45
+ * caller. ACP completion does not wait for this cleanup.
42
46
  */
43
47
  export async function revokeAcpSessionToken(
44
48
  apiUrl: string,
@@ -46,11 +50,17 @@ export async function revokeAcpSessionToken(
46
50
  tokenId: string,
47
51
  ): Promise<void> {
48
52
  try {
49
- await fetch(`${apiUrl.replace(/\/+$/, "")}/api/sessions/tokens/${tokenId}`, {
53
+ const res = await fetch(`${apiUrl.replace(/\/+$/, "")}/api/sessions/tokens/${tokenId}`, {
50
54
  method: "DELETE",
51
55
  headers: { Authorization: `Bearer ${apiKey}` },
56
+ signal: AbortSignal.timeout(REVOKE_TIMEOUT_MS),
52
57
  });
53
- } catch {
54
- // best-effort — token will expire on its own
58
+ if (!res.ok) throw new Error(`HTTP ${res.status}`);
59
+ } catch (error) {
60
+ console.warn(
61
+ scrubSecrets(
62
+ `[acp] Session token revoke failed: ${error instanceof Error ? error.message : String(error)}`,
63
+ ),
64
+ );
55
65
  }
56
66
  }
@@ -104,6 +104,8 @@ const TOKEN_REGEXES: ReadonlyArray<{ name: string; re: RegExp }> = [
104
104
  { name: "github_pat", re: /github_pat_[A-Za-z0-9_]{20,}/g },
105
105
  // GitHub classic/OAuth tokens (ghp_, gho_, ghu_, ghs_, ghr_)
106
106
  { name: "github_token", re: new RegExp(String.raw`${TB}gh[pousr]_[A-Za-z0-9]{20,}\b`, "g") },
107
+ // ACP ephemeral session tokens (base62 payload)
108
+ { name: "acp_session_token", re: new RegExp(String.raw`${TB}aseph_[A-Za-z0-9]{20,}\b`, "g") },
107
109
  // GitLab personal access tokens
108
110
  { name: "gitlab_pat", re: new RegExp(String.raw`${TB}glpat-[A-Za-z0-9_-]{20,}\b`, "g") },
109
111
  // Anthropic API keys (must match before the generic sk- rule below)
@@ -0,0 +1,33 @@
1
+ import { z } from "zod";
2
+ import { ALL_TOOLS } from "../tools/tool-config";
3
+
4
+ const toolNames = z
5
+ .array(z.string().refine((name) => ALL_TOOLS.has(name), "Unknown swarm tool"))
6
+ .max(16);
7
+
8
+ /** Exact task-type and schedule keys; a schedule entry overrides its task type. */
9
+ export const taskToolManifestSchema = z.strictObject({
10
+ taskTypes: z.record(z.string().min(1).max(50), toolNames).optional(),
11
+ schedules: z.record(z.uuid(), toolNames).optional(),
12
+ });
13
+
14
+ export function parseTaskToolManifest(value: string) {
15
+ return taskToolManifestSchema.parse(JSON.parse(value));
16
+ }
17
+
18
+ export function selectTaskTools(
19
+ manifest: z.infer<typeof taskToolManifestSchema>,
20
+ task: { taskType?: string; scheduleId?: string; slackChannelId?: string },
21
+ ): string[] {
22
+ const fromSchedule =
23
+ task.scheduleId && Object.hasOwn(manifest.schedules ?? {}, task.scheduleId)
24
+ ? manifest.schedules?.[task.scheduleId]
25
+ : undefined;
26
+ const fromType =
27
+ task.taskType && Object.hasOwn(manifest.taskTypes ?? {}, task.taskType)
28
+ ? manifest.taskTypes?.[task.taskType]
29
+ : undefined;
30
+ return [...new Set(fromSchedule ?? fromType ?? [])].filter(
31
+ (name) => !name.startsWith("slack-") || Boolean(task.slackChannelId),
32
+ );
33
+ }
@@ -1,3 +1,4 @@
1
+ import { notifyAutomationPreflightFailure } from "../automation-preflight-alert";
1
2
  import {
2
3
  getAutomationSetupStates,
3
4
  preflightAutomation,
@@ -91,13 +92,15 @@ export async function startWorkflowExecution(
91
92
  await getAutomationSetupStates(),
92
93
  );
93
94
  if (preflight.state === "needs_setup") {
94
- return await recordWorkflowPreflightFailure({
95
+ const { runId, recorded } = await recordWorkflowPreflightFailure({
95
96
  workflowId: workflow.id,
96
97
  triggerType: options.triggerType ?? "manual",
97
98
  triggerData,
98
99
  failureReason: preflight.failureReason!,
99
100
  createdBy: options.requestedByUserId,
100
101
  });
102
+ if (recorded) await notifyAutomationPreflightFailure(preflight);
103
+ return runId;
101
104
  }
102
105
 
103
106
  // Templates can consume install params outside the graph definition (most
@@ -6,6 +6,7 @@ import {
6
6
  FollowUpConfigSchema,
7
7
  ModelTierSchema,
8
8
  ReasoningEffortSchema,
9
+ RoutingReasonSchema,
9
10
  splitLegacyModelAlias,
10
11
  } from "../../types";
11
12
  import type { ExecutorResult } from "./base";
@@ -13,11 +14,13 @@ import { BaseExecutor } from "./base";
13
14
 
14
15
  // ─── Config / Output Schemas ────────────────────────────────
15
16
 
16
- const AgentTaskConfigSchema = z.object({
17
+ export const AgentTaskConfigSchema = z.object({
17
18
  template: z.string(),
18
19
  // Plain string, NOT .uuid(): agents may join with custom IDs (AGENT_ID env /
19
20
  // join-swarm agentId), so a UUID filter would reject legitimate agents.
20
21
  agentId: z.string().optional(),
22
+ routingReason: RoutingReasonSchema.optional(),
23
+ routingNote: z.string().max(200).optional(),
21
24
  tags: z.array(z.string()).optional(),
22
25
  priority: z.number().int().min(0).max(100).optional(),
23
26
  offerMode: z.boolean().optional(),
@@ -98,6 +101,9 @@ export class AgentTaskExecutor extends BaseExecutor<
98
101
  {
99
102
  key: effectiveKey,
100
103
  agentId: config.agentId ?? null,
104
+ // A configured workflow target is an author pin, including existing definitions.
105
+ routingReason: config.routingReason ?? (config.agentId ? "human_pinned" : undefined),
106
+ routingNote: config.routingNote,
101
107
  source: "workflow",
102
108
  tags: config.tags,
103
109
  priority: config.priority,
@@ -1,5 +1,5 @@
1
1
  {
2
- "commit": "a4e5e77923eaabc644f8290396ef341d041cdce1",
2
+ "commit": "9748aabf7252c258378302240d73f7516d113777",
3
3
  "excludedSkills": [
4
4
  "feedback"
5
5
  ],
@@ -11,6 +11,10 @@
11
11
  "templates/skills/brainstorming/files/template.md": "ae0aabc02a307db0550f9026aca33fb471608c31f7a0b65ecc781073f17cc953",
12
12
  "templates/skills/code-reviewing/config.json": "dde1298b15d55428b898a0421033d1aa140ef152d335d49345059e29deab96f2",
13
13
  "templates/skills/code-reviewing/content.md": "85e9aa3b276384ab3f3bc570aece2e8c103a8614c19129378cf4dfec3457a18b",
14
+ "templates/skills/comms/config.json": "5561ee129f68a03e89b21458afebcd79afd2a8c177c0ba5dad00ffa21ee57bc3",
15
+ "templates/skills/comms/content.md": "bd1d33b4af13eb36dfd87552bb35ecd200ac0a0dd40d409b50dff448708ce5ea",
16
+ "templates/skills/comms/files/references/ste-rules.md": "6ee2ed5d6b49d2e421b6c9d22310a5b589fa1abe81de06213e4b89fbc1271e74",
17
+ "templates/skills/comms/files/references/visual-shapes.md": "5e07e9250ad4cdc877af2281525185f0de89c01f90a077463a2c77c8a7b998e8",
14
18
  "templates/skills/delegate-work/config.json": "5b1542eb0be359bb8a17117fcf87917e0607d1b939ae3266ddffacdc14efb281",
15
19
  "templates/skills/delegate-work/content.md": "c9562bd17846d3595d40172ecfdc6f85bb99eaf23597eb9d5aa9635d5c14d079",
16
20
  "templates/skills/delegate-work/files/scripts/codex-exec.sh": "c2222986c46ac608403f28f201230cb34c7d7ac41a2262dc2c8dbd99c8645dbb",
@@ -74,6 +78,7 @@
74
78
  "ask-user",
75
79
  "brainstorming",
76
80
  "code-reviewing",
81
+ "comms",
77
82
  "delegate-work",
78
83
  "design-docs",
79
84
  "engineering-standards",