@desplega.ai/agent-swarm 1.147.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 (185) hide show
  1. package/README.md +13 -1
  2. package/dist/{acp-adapter-9n319wqc.js → acp-adapter-4jncb126.js} +12 -5
  3. package/dist/{actions-2vxqvpr9.js → actions-31vp3ae7.js} +10 -9
  4. package/dist/{app-w066xfy0.js → app-1hh67x8w.js} +6 -6
  5. package/dist/{assistant-kt31dj2h.js → assistant-z66dmz6p.js} +13 -11
  6. package/dist/{boot-reembed-az5rassp.js → boot-reembed-fqzxxzjh.js} +7 -7
  7. package/dist/{boot-reembed-j3mm3rfz.js → boot-reembed-wxa8ya0s.js} +8 -8
  8. package/dist/{boot-scrub-logs-b6h54817.js → boot-scrub-logs-jnyndh79.js} +5 -5
  9. package/dist/{claude-adapter-w1z04ann.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-f0cc1kec.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-de14znh8.js → cli-19f354qr.js} +2 -2
  15. package/dist/{cli-hbv0kq6w.js → cli-2qts1hys.js} +3 -3
  16. package/dist/{cli-kpv03zhj.js → cli-3apbzgyk.js} +27 -15
  17. package/dist/{cli-1th1728g.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-yz3djrm0.js → cli-6pqcb4q0.js} +53 -13
  22. package/dist/{cli-719k9j2c.js → cli-7smrsr25.js} +3 -3
  23. package/dist/{cli-rw51bq3j.js → cli-9fy6nk6g.js} +1 -1
  24. package/dist/{cli-zhjmqxzh.js → cli-9hspd3dp.js} +2 -2
  25. package/dist/{cli-3tgwnf43.js → cli-aae6hz6a.js} +4 -4
  26. package/dist/{cli-89xfd7f9.js → cli-cr12paw2.js} +5 -5
  27. package/dist/{cli-b5bq9crk.js → cli-d8ftsp62.js} +8 -8
  28. package/dist/{cli-4nr988h6.js → cli-e95cx1eb.js} +3 -3
  29. package/dist/{cli-n8508nre.js → cli-gjhjfeeg.js} +1 -1
  30. package/dist/{cli-p1d5b073.js → cli-gv88e1vf.js} +1 -1
  31. package/dist/{cli-rvqz34y5.js → cli-jecc01cb.js} +1 -1
  32. package/dist/{cli-8v0g1yc8.js → cli-k0cr4kat.js} +1 -1
  33. package/dist/{cli-mmemxxdc.js → cli-kgz4np97.js} +4 -4
  34. package/dist/{cli-w9kb3bk1.js → cli-kjwt6hdf.js} +3 -1
  35. package/dist/{cli-f7dsy61y.js → cli-mnmsfd1w.js} +6 -2
  36. package/dist/{cli-54wwve05.js → cli-n3dz2942.js} +37 -26
  37. package/dist/{cli-12fz972k.js → cli-nye12xk5.js} +8 -5
  38. package/dist/{cli-tzvk9haz.js → cli-phsxnkp6.js} +20 -20
  39. package/dist/{cli-53s590z8.js → cli-q50ef8g0.js} +1 -1
  40. package/dist/{cli-f146mn5s.js → cli-qaacms9y.js} +7 -6
  41. package/dist/{cli-xqcq9y5e.js → cli-r2ap2czm.js} +1 -0
  42. package/dist/{cli-m8yrsg97.js → cli-sd5pv3b1.js} +3 -3
  43. package/dist/{cli-0b6y7kxm.js → cli-xm87hzbw.js} +1 -1
  44. package/dist/{cli-ftht3zzj.js → cli-yab68w40.js} +285 -11
  45. package/dist/{cli-atcve9y8.js → cli-yb9qhqam.js} +227 -59
  46. package/dist/{cli-smby5m17.js → cli-ygweqcn4.js} +133 -22
  47. package/dist/{cli-dttph1bw.js → cli-ykxrnd1q.js} +2 -2
  48. package/dist/{cli-m9sxbkhm.js → cli-zvz62chv.js} +3 -3
  49. package/dist/cli.js +16 -14
  50. package/dist/{codex-adapter-zw4dwsx7.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-k981s65v.js → codex-session-runner-gjgxyp5j.js} +4 -4
  53. package/dist/{commands-sykrxx7e.js → commands-hz3nb95f.js} +5 -5
  54. package/dist/{db-sfbsc8wt.js → db-ggz97zbm.js} +5 -5
  55. package/dist/{e2b-6hb103d8.js → e2b-p04vmtx0.js} +1 -1
  56. package/dist/{handlers-0aspcejm.js → handlers-nbzdzn9k.js} +14 -11
  57. package/dist/{hook-hxccs7m5.js → hook-bks2q1f6.js} +7 -7
  58. package/dist/{hook-4nvqj2sb.js → hook-kq9wp0ep.js} +8 -8
  59. package/dist/{http-y06z517d.js → http-hrf77v87.js} +96 -45
  60. package/dist/{index-tce1rss4.js → index-6nsbg88z.js} +443 -144
  61. package/dist/{index-x5j894zf.js → index-8hmeg8j1.js} +14 -14
  62. package/dist/{index-k8znfx0z.js → index-cqtebbhb.js} +13 -13
  63. package/dist/{index-hssww7kj.js → index-n28pw1zt.js} +15 -15
  64. package/dist/{keepalive-w733ax66.js → keepalive-dxc12gvg.js} +7 -7
  65. package/dist/{lead-75bjqa9d.js → lead-yckfn18d.js} +32 -32
  66. package/dist/{maintenance-w4f1zjck.js → maintenance-6kfb2m5e.js} +8 -8
  67. package/dist/{oauth-refresh-sweep-wdetky26.js → oauth-refresh-sweep-1x1ac722.js} +6 -6
  68. package/dist/{onboard-bb5net8s.js → onboard-dcyt1609.js} +4 -3
  69. package/dist/{opencode-adapter-fef1mrn2.js → opencode-adapter-dce4wskd.js} +2 -2
  70. package/dist/{otel-impl-0w7c14ap.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-9nzsrgcq.js → pricing-refresh-kaqg567e.js} +7 -7
  73. package/dist/{rbac-roles-z1jrffbt.js → rbac-roles-ammq6rd2.js} +5 -5
  74. package/dist/{rbac-roles-r3hqjp09.js → rbac-roles-cf97v1vd.js} +6 -6
  75. package/dist/{render-v2-vbr89fnb.js → render-v2-80w603vy.js} +6 -6
  76. package/dist/{seed-pricing-g62hy1pk.js → seed-pricing-zbrn83xv.js} +6 -6
  77. package/dist/{setup-gnfqnptp.js → setup-fe66kczs.js} +2 -2
  78. package/dist/{worker-kznxp4qm.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 +3 -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/mcp-input-ergonomics.test.ts +386 -0
  135. package/src/tests/model-control.test.ts +18 -5
  136. package/src/tests/opencode-adapter.test.ts +2 -2
  137. package/src/tests/promote-draft-task-route.test.ts +4 -0
  138. package/src/tests/prompt-template-session.test.ts +13 -4
  139. package/src/tests/rbac-wire-e2e.test.ts +2 -2
  140. package/src/tests/routing-decision-persistence.test.ts +520 -0
  141. package/src/tests/routing-reason-contract.test.ts +89 -0
  142. package/src/tests/routing-reason-inventory.test.ts +55 -0
  143. package/src/tests/schedule-target-type.test.ts +48 -1
  144. package/src/tests/scheduled-task-required-params-migration.test.ts +182 -0
  145. package/src/tests/scheduled-tasks.test.ts +9 -5
  146. package/src/tests/script-connections.test.ts +4 -0
  147. package/src/tests/script-runs-http.test.ts +31 -0
  148. package/src/tests/scripts-typecheck.test.ts +11 -0
  149. package/src/tests/secret-scrubber.test.ts +12 -0
  150. package/src/tests/send-task-output-schema.test.ts +66 -1
  151. package/src/tests/send-task-requested-by.test.ts +1 -0
  152. package/src/tests/send-task-slack-routing-guard.test.ts +2 -0
  153. package/src/tests/store-progress-blocked-waiting-gate.test.ts +151 -0
  154. package/src/tests/swarm-tool-result-gate.test.ts +24 -0
  155. package/src/tests/task-tool-manifest.test.ts +47 -0
  156. package/src/tests/task-tool-preload-mcp.test.ts +125 -0
  157. package/src/tests/task-tools-ctx.test.ts +2 -0
  158. package/src/tests/tool-output-agent-id.test.ts +1 -0
  159. package/src/tests/ui-followup-lead-delegation.test.ts +6 -1
  160. package/src/tests/workflow-agent-task.test.ts +24 -1
  161. package/src/tests/workflow-engine-v2.test.ts +58 -2
  162. package/src/tools/accept-steer.ts +0 -1
  163. package/src/tools/defer-task.ts +73 -21
  164. package/src/tools/memory-get.ts +10 -3
  165. package/src/tools/memory-rate.ts +19 -7
  166. package/src/tools/memory-search.ts +2 -1
  167. package/src/tools/memory-store.ts +25 -5
  168. package/src/tools/send-task.ts +40 -2
  169. package/src/tools/skills/skill-publish.ts +1 -0
  170. package/src/tools/store-progress.ts +78 -4
  171. package/src/tools/templates.ts +2 -1
  172. package/src/tools/utils.ts +33 -2
  173. package/src/types.ts +14 -1
  174. package/src/utils/acp-session-token.ts +14 -4
  175. package/src/utils/secret-scrubber.ts +2 -0
  176. package/src/utils/task-tool-manifest.ts +33 -0
  177. package/src/workflows/engine.ts +4 -1
  178. package/src/workflows/executors/agent-task.ts +7 -1
  179. package/templates/ai-toolbox.manifest.json +6 -1
  180. package/templates/skills/comms/config.json +18 -0
  181. package/templates/skills/comms/content.md +74 -0
  182. package/templates/skills/comms/files/references/ste-rules.md +73 -0
  183. package/templates/skills/comms/files/references/visual-shapes.md +112 -0
  184. package/templates/skills/swarm-scripts/SKILL.md +3 -2
  185. package/templates/skills/swarm-scripts/content.md +3 -2
@@ -0,0 +1,44 @@
1
+ ALTER TABLE agent_tasks ADD COLUMN routing_reason TEXT
2
+ CHECK (
3
+ routing_reason IS NULL OR routing_reason IN (
4
+ 'skill',
5
+ 'continuity',
6
+ 'overflow',
7
+ 'human_pinned',
8
+ 'reroute_fault'
9
+ )
10
+ );
11
+
12
+ ALTER TABLE agent_tasks ADD COLUMN routing_note TEXT
13
+ CHECK (routing_note IS NULL OR length(routing_note) <= 200);
14
+
15
+ -- Continuity snapshots run while task creation holds BEGIN IMMEDIATE. Keep
16
+ -- each context lookup filter-first and recency-ordered. SQLite stores rowid as
17
+ -- the implicit final index key, so a reverse scan also satisfies the
18
+ -- createdAt DESC, rowid DESC deterministic tie-break.
19
+ CREATE INDEX idx_agent_tasks_routing_pr_recency
20
+ ON agent_tasks(vcsRepo, vcsNumber, createdAt);
21
+
22
+ CREATE INDEX idx_agent_tasks_routing_repo_recency
23
+ ON agent_tasks(vcsRepo, createdAt);
24
+
25
+ CREATE INDEX idx_agent_tasks_routing_slack_thread_recency
26
+ ON agent_tasks(slackChannelId, slackThreadTs, createdAt);
27
+
28
+ CREATE INDEX idx_agent_tasks_routing_agentmail_thread_recency
29
+ ON agent_tasks(agentmailThreadId, createdAt);
30
+
31
+ -- Immutable decision-time telemetry. Agent ids deliberately are not foreign
32
+ -- keys: deleting an agent must not erase which target or candidate was visible
33
+ -- when the task was created.
34
+ CREATE TABLE routing_decisions (
35
+ task_id TEXT PRIMARY KEY REFERENCES agent_tasks(id) ON DELETE CASCADE,
36
+ selected_agent_id TEXT,
37
+ captured_at TEXT NOT NULL,
38
+ worker_statuses TEXT NOT NULL
39
+ CHECK (json_valid(worker_statuses) AND json_type(worker_statuses) = 'array'),
40
+ continuity_candidates TEXT NOT NULL
41
+ CHECK (json_valid(continuity_candidates) AND json_type(continuity_candidates) = 'object'),
42
+ created_by TEXT,
43
+ updated_by TEXT
44
+ );
@@ -35,6 +35,36 @@ export type ScriptScope = "agent" | "global";
35
35
  export type ScriptFsMode = "none" | "workspace-rw";
36
36
  export type ScriptApiRawOptions = { raw: true };
37
37
  export type ScriptApiDefaultOptions = { raw?: false };
38
+ export type RoutingReason = "skill" | "continuity" | "overflow" | "human_pinned" | "reroute_fault";
39
+ export type TaskSendArgs = Record<string, unknown> & {
40
+ task: string;
41
+ routingNote?: string;
42
+ } & (
43
+ | { agentId: string; routingReason: RoutingReason }
44
+ | { agentId?: never; routingReason?: never }
45
+ );
46
+ export type AgentTaskStepConfig = {
47
+ template?: string;
48
+ task?: string;
49
+ agentId?: string;
50
+ routingReason?: RoutingReason;
51
+ routingNote?: string;
52
+ tags?: string[];
53
+ priority?: number;
54
+ offerMode?: boolean;
55
+ dir?: string;
56
+ vcsRepo?: string;
57
+ model?: string;
58
+ parentTaskId?: string;
59
+ requestedByUserId?: string;
60
+ outputSchema?: Record<string, unknown>;
61
+ /** Wait for the dispatched task to reach a terminal status before resolving. Default: true. */
62
+ waitForCompletion?: boolean;
63
+ /** Max ms to wait for a terminal status before throwing. Default: 2h. Only used when waitForCompletion is true. */
64
+ timeoutMs?: number;
65
+ /** Throw when the task ends failed/cancelled/superseded (default), or resolve with {taskId,status,error} when false. */
66
+ failOnTaskFailure?: boolean;
67
+ };
38
68
 
39
69
  export interface ScriptApiRawResult {
40
70
  ok: boolean;
@@ -138,7 +168,7 @@ export interface SwarmSdk {
138
168
  task_list(args?: Record<string, unknown>): Promise<unknown>;
139
169
  task_get(args: { taskId: string }): Promise<unknown>;
140
170
  task_storeProgress(args: Record<string, unknown>): Promise<unknown>;
141
- task_defer(args: { taskId: string; delayMs?: number; runAt?: string; summary: string; note: string; checks?: string[] }): Promise<unknown>;
171
+ task_defer(args: { taskId: string; delayMs?: number; runAt?: string; wakeOn?: { event: "task.completed" | "task.failed" | "settled"; taskId: string }; summary: string; note: string; checks?: string[] }): Promise<unknown>;
142
172
  task_poll(args?: Record<string, unknown>): Promise<unknown>;
143
173
  // --- kv ---
144
174
  kv_get<T = unknown>(args: { key: string; namespace?: string }): Promise<KvSdkResponse<KvEntry<T>>>;
@@ -219,7 +249,7 @@ export interface SwarmSdk {
219
249
  inject_learning(args: { content: string; name?: string; scope?: "agent" | "swarm"; source?: string; tags?: string[] }): Promise<unknown>;
220
250
 
221
251
  // --- write: tasks ---
222
- task_send(args: Record<string, unknown>): Promise<unknown>;
252
+ task_send(args: TaskSendArgs): Promise<unknown>;
223
253
  task_cancel(args: { taskId: string }): Promise<unknown>;
224
254
  task_steer(args: { taskId: string; message: string; mode?: "steer" | "queue"; onUnsupported?: "degrade" | "fail" }): Promise<unknown>;
225
255
  task_action(args: Record<string, unknown>): Promise<unknown>;
@@ -374,26 +404,7 @@ export interface ScriptWorkflowSteps {
374
404
  ): Promise<unknown>;
375
405
  agentTask(
376
406
  label: string,
377
- config: {
378
- template?: string;
379
- task?: string;
380
- agentId?: string;
381
- tags?: string[];
382
- priority?: number;
383
- offerMode?: boolean;
384
- dir?: string;
385
- vcsRepo?: string;
386
- model?: string;
387
- parentTaskId?: string;
388
- requestedByUserId?: string;
389
- outputSchema?: Record<string, unknown>;
390
- /** Wait for the dispatched task to reach a terminal status before resolving. Default: true. */
391
- waitForCompletion?: boolean;
392
- /** Max ms to wait for a terminal status before throwing. Default: 2h. Only used when waitForCompletion is true. */
393
- timeoutMs?: number;
394
- /** Throw when the task ends failed/cancelled/superseded (default), or resolve with {taskId,status,error} when false. */
395
- failOnTaskFailure?: boolean;
396
- },
407
+ config: AgentTaskStepConfig,
397
408
  ): Promise<unknown>;
398
409
  swarmScript(
399
410
  label: string,
@@ -2,6 +2,8 @@ import { z } from "zod";
2
2
 
3
3
  export const argsSchema = z.object({
4
4
  agentName: z.string().describe("Target agent's name (case-insensitive) — resolved to its id"),
5
+ routingReason: z.enum(["skill", "continuity", "overflow", "human_pinned", "reroute_fault"]),
6
+ routingNote: z.string().max(200).optional(),
5
7
  task: z.string().describe("Full task prompt for the target agent"),
6
8
  parentTaskId: z
7
9
  .string()
@@ -15,7 +17,7 @@ export const argsSchema = z.object({
15
17
  export default async function delegate(args: any, ctx: any) {
16
18
  const parsed = argsSchema.safeParse(args);
17
19
  if (!parsed.success) return { ok: false, error: "invalid args: " + parsed.error.message };
18
- const { agentName, task, parentTaskId, priority, tags } = parsed.data;
20
+ const { agentName, task, routingReason, routingNote, parentTaskId, priority, tags } = parsed.data;
19
21
 
20
22
  const res: any = await ctx.swarm.swarm_get({ includeFull: true });
21
23
  const agents: any[] = res?.data?.agents ?? res?.agents ?? [];
@@ -26,6 +28,8 @@ export default async function delegate(args: any, ctx: any) {
26
28
 
27
29
  const sent: any = await ctx.swarm.task_send({
28
30
  agentId: agent.id,
31
+ routingReason,
32
+ routingNote,
29
33
  task,
30
34
  ...(parentTaskId ? { parentTaskId } : {}),
31
35
  ...(priority != null ? { priority } : {}),
@@ -23,6 +23,16 @@
23
23
  "content": "---\ndate: [Current date and time with timezone in ISO format]\nauthor: [Author name]\ntopic: \"[Topic]\"\ntags: [brainstorm, relevant-tags]\nstatus: in-progress\nexploration_type: [problem|idea|comparison|workflow|other]\nlast_updated: [Current date in YYYY-MM-DD format]\nlast_updated_by: [Author name]\n---\n\n# [Topic] — Brainstorm\n\n## Context\n\n[Initial framing: what we know, what prompted this exploration, any existing context or constraints]\n\n## Exploration\n\n[Q&A pairs accumulate here during the session]\n\n## Synthesis\n\n### Key Decisions\n- [Filled after exploration — includes \"Deferred: X, defaulting to Y\" entries for consciously parked decisions]\n\n### Open Questions\n- [Fact-shaped questions only (answerable by /research) — undecided decisions don't belong here]\n\n### Constraints Identified\n- [Filled after exploration]\n\n### Core Requirements\n- [Filled after exploration]\n\n## Next Steps\n\n- [Handoff decision: research, plan, or parked]\n"
24
24
  }
25
25
  ],
26
+ "comms": [
27
+ {
28
+ "path": "references/ste-rules.md",
29
+ "content": "# Precise mode — ASD-STE100 rules\n\nCondensed from [asd-ste100-skill](https://github.com/desplega-ai/asd-ste100-skill) (MIT), which encodes the rule categories of ASD-STE100 Issue 9 (Jan 2025).\n\nThis encodes the standard's rule *categories*, not ASD's ~900-word approved dictionary (free to obtain, not free to redistribute). Structural rules are checkable from the description alone — apply them with confidence. Lexical rules depend on the dictionary — apply them as a direction of travel, and never imply dictionary compliance.\n\n## Structural rules — apply these\n\n| Rule | Do | Don't |\n|---|---|---|\n| Active voice | \"The agent deletes the file.\" | \"The file is deleted (by the agent).\" — unless the actor is genuinely unknown or irrelevant |\n| No phrasal verbs | \"Remove the panel.\" / \"Start the job.\" | \"Take off the panel.\" / \"Spin up the job.\" |\n| One instruction per sentence | \"Open the file. Read line 3.\" | \"Open the file and read line 3, then check if it matches.\" |\n| Sentence length | ≤20 words for instructions, ≤25 for descriptions | Long compound/subordinate-clause sentences |\n| No semicolons | Split into separate sentences | Any semicolon at all |\n| Noun clusters | ≤3 words stacked as a noun phrase | 4+ word noun stacks (\"high pressure fuel pump inlet valve assembly\") |\n| No ellipsis | Keep subject, verb, and article explicit | Drop words to save space (\"Files not backed up will be lost\" → ambiguous which files) |\n| Keep modality | \"The request **may have** failed.\" stays \"may have\" | Promote a hedge to a fact, or invent a certainty the source did not state |\n| Paragraph limits | One topic per paragraph, ≤6 sentences | Multi-topic paragraphs |\n| Lists for sequences | Numbered/bulleted list for 3+ steps or conditions | A sequence buried in one prose sentence |\n\n## Lexical rules — direction of travel only\n\n| Rule | Do | Don't |\n|---|---|---|\n| One word, one meaning | Pick one verb for one action and reuse it every time | Rotate synonyms (\"check\"/\"verify\"/\"confirm\") for the same action |\n| One part of speech per word | \"Apply oil to the valve\" (oil = noun) | \"Oil the valve\" (oil = verb) |\n| Verb, not noun | \"Analyze the log.\" | \"Perform an analysis of the log.\" |\n| Domain terms | Keep needed technical terms; define each once if not common English | Jargon never defined |\n\n## Simple tenses — one exception\n\nSTE permits infinitive, imperative, simple present, simple past, simple future, and past participle as adjective. It excludes present perfect: \"we received the report\", not \"we have received the report\". Exception: where the compound form carries information the simple form cannot — current relevance (\"the job has completed\" = output available now), or a hedge like \"may have failed\" — keep it and flag the departure.\n\n## Scan checklist\n\nScan for all six before rewriting. Each is mechanical — you can point at the exact word that breaks it.\n\n1. **Synonym rotation** — the same thing has several names (\"the user\", \"the customer\", \"the client\"). Fix: one name, every time.\n2. **Hedge stacking** — qualifiers pile up until nothing is asserted (\"it is important to note that this may potentially help to improve\"). Fix: state the claim or delete it.\n3. **Nominalization** — an action frozen into a noun (\"perform an analysis of\"). Fix: use the verb.\n4. **Marketing adjectives** — seamless, robust, powerful, blazing-fast. Fix: delete, or replace with the measurement that earns the claim.\n5. **Run-on sentences** — ideas joined by semicolons or em dashes. Fix: one idea per sentence.\n6. **Soft phrasal verbs** — spin up, reach out, dive into, kick off. Fix: the single plain verb (start, contact, read, begin).\n\n## Process\n\n1. Pick the sub-mode (Strict or STE-flavored). Keep the choice internal unless asked.\n2. Read the input once for meaning before rewriting anything.\n3. Walk it sentence by sentence; flag every violation. In STE-flavored, flag lexical rules but do not enforce them.\n4. Rewrite each flagged sentence, preserving the original meaning exactly. If a rewrite would drop necessary precision (a safety condition, scope qualifier, number), keep the longer phrasing and flag it. Check modality before committing — a shorter sentence that upgrades a hedge to a fact is a different claim. Never add a fact the source did not state.\n5. Output the rewritten text alone. If the input already complies, say so — do not force changes.\n\n## Diff table format (on request)\n\nWhen asked to \"show the diff\" / \"which rules did it break\" / \"before/after\":\n\n```markdown\n| Rule violated | Original | Simplified |\n|---|---|---|\n| Present perfect tense | \"We have received your request.\" | \"We received your request.\" |\n| Noun cluster (4+ words) | \"the agent task queue priority handler\" | \"the handler that sets task-queue priority\" |\n\nMode: Strict. 7 violations found.\n```\n\nFollow with one line naming anything deliberately not simplified, and why.\n\n## Boundaries\n\n- Not a certified STE authoring tool — a clarity tool inspired by the standard. For aerospace-grade compliance, check word-by-word against the official dictionary from [asd-ste100.org](https://www.asd-ste100.org/STE_downloads.html).\n- Fixes form, not substance: a hollow paragraph rewritten under these rules is a clean hollow paragraph. Say so instead of polishing it.\n- Stop when the sentence is unambiguous, not when it is shortest.\n"
30
+ },
31
+ {
32
+ "path": "references/visual-shapes.md",
33
+ "content": "# Visual mode — example shapes\n\nConcrete examples for each view the Visual mode can produce. Source: the show-me skill (bundled into this skill).\n\nLogic or an algorithm as pseudocode:\n\n```text\non(save)\n if content is unchanged\n return cached result\n write new content\n return fresh result\n```\n\nRuntime control flow as a call tree:\n\n```text\nsubmitForm\n createSession\n persistPrompt\n launchAgent\n navigateToSession\n```\n\nUI structure as a component tree, including state and module boundaries that matter:\n\n```tsx\n<SessionPage> (apps/example/src/routes/session.tsx)\n useSessionEvents()\n <SessionToolbar>\n <RunSkillButton> (packages/ui)\n```\n\nFile responsibility or a broad refactor as a shallow file tree:\n\n```text\nsrc/\n├── commands/ # parses user actions\n├── sessions/ # owns session state\n└── transport/ # sends API requests\n```\n\nComponent interaction, control flow, or data flow with Mermaid:\n\n```mermaid\nsequenceDiagram\n participant User\n participant UI\n participant Daemon\n User->>UI: choose command\n UI->>Daemon: send expanded prompt\n Daemon-->>UI: stream result\n```\n\n`diff` shapes — match the diff to the topic.\n\nComponent change:\n\n```diff\n <SessionPage>\n useSessionEvents()\n <SessionToolbar>\n+ <RunSkillButton />\n <SessionTimeline>\n+ <SkillResultCard />\n```\n\nFile-layout change:\n\n```diff\n src/\n ├── commands/\n+│ └── show-me.ts # expands the slash command\n ├── sessions/\n-└── transport.ts\n+└── transport/\n+ ├── client.ts\n+ └── stream.ts\n```\n\nCall-tree change:\n\n```diff\n submitForm\n createSession\n persistPrompt\n+ expandSkillMention\n launchAgent\n- navigateToSession\n+ navigateToSession\n+ subscribeToEvents\n```\n\nState or control-flow change:\n\n```diff\n on(save)\n- write content\n+ if content is unchanged\n+ return cached result\n+ write new content\n+ invalidate cache\n```\n\nWhole block when most of it is new or a copyable target shape is needed:\n\n```ts\nfunction expandSkill(command: string): string {\n const skillName = command.slice(1)\n return `use the ${skillName} skill`\n}\n```\n"
34
+ }
35
+ ],
26
36
  "delegate-work": [
27
37
  {
28
38
  "path": "scripts/codex-exec.sh",
@@ -50,6 +50,8 @@ import codeReviewingConfig from "../../../templates/skills/code-reviewing/config
50
50
  import codeReviewingContent from "../../../templates/skills/code-reviewing/content.md" with {
51
51
  type: "text",
52
52
  };
53
+ import commsConfig from "../../../templates/skills/comms/config.json" with { type: "text" };
54
+ import commsContent from "../../../templates/skills/comms/content.md" with { type: "text" };
53
55
  import composioConfig from "../../../templates/skills/composio/config.json" with { type: "text" };
54
56
  import composioContent from "../../../templates/skills/composio/content.md" with { type: "text" };
55
57
  import composioGmailConfig from "../../../templates/skills/composio-gmail/config.json" with {
@@ -318,6 +320,7 @@ const BUILT_IN_SKILL_SOURCES = [
318
320
  { config: brainstormingConfig, body: brainstormingContent },
319
321
  { config: codeQualityConfig, body: codeQualityContent },
320
322
  { config: codeReviewingConfig, body: codeReviewingContent },
323
+ { config: commsConfig, body: commsContent },
321
324
  { config: composioConfig, body: composioContent },
322
325
  { config: composioGmailConfig, body: composioGmailContent },
323
326
  { config: composioGoogleCalendarConfig, body: composioGoogleCalendarContent },
@@ -75,6 +75,7 @@ export async function promoteSteeringToTask(
75
75
  ): Promise<AgentTask> {
76
76
  return await createTaskExtended(message.body, {
77
77
  agentId: task.agentId,
78
+ routingReason: task.agentId ? "continuity" : undefined,
78
79
  creatorAgentId: message.createdByAgentId,
79
80
  source: message.source === "script" ? "api" : message.source,
80
81
  taskType: "follow-up",
@@ -1,5 +1,6 @@
1
1
  import { normalizeSlackReactionShortcode } from "../slack/reaction-shortcode";
2
2
  import { ProviderNameSchema } from "../types";
3
+ import { parseTaskToolManifest } from "../utils/task-tool-manifest";
3
4
 
4
5
  /**
5
6
  * Guards against storing reserved keys in the swarm_config table.
@@ -163,6 +164,15 @@ function validateFloatRange(
163
164
  }
164
165
 
165
166
  const VALIDATED_KEYS: Record<string, ConfigValidator> = {
167
+ TASK_TOOL_MANIFESTS: (value) => {
168
+ try {
169
+ if (typeof value !== "string") throw new Error("Expected JSON string");
170
+ parseTaskToolManifest(value);
171
+ return null;
172
+ } catch {
173
+ return "Invalid TASK_TOOL_MANIFESTS (expected JSON with taskTypes and/or schedules maps, each selecting at most 16 known swarm tools)";
174
+ }
175
+ },
166
176
  FEEDBACK_ENDPOINT: (value) => {
167
177
  if (typeof value !== "string") {
168
178
  return "Invalid FEEDBACK_ENDPOINT (must use HTTPS, or HTTP on a loopback host)";
@@ -252,6 +262,7 @@ const VALIDATED_KEYS: Record<string, ConfigValidator> = {
252
262
  "HEARTBEAT_PIN_CRASH_RESUME",
253
263
  "POOL_AFFINITY_ENFORCEMENT",
254
264
  "SCRIPTS_ONLY_MCP",
265
+ "TASK_TOOL_PRELOAD_ENABLED",
255
266
  "SLACK_DISABLE",
256
267
  "SLACK_RENDER_V2",
257
268
  "SLACK_RENDER_V2_DELEGATION",
@@ -57,6 +57,7 @@ export function PostTaskStep({ state, addLog, goToNext }: StepProps) {
57
57
  body: JSON.stringify({
58
58
  task,
59
59
  agentId: leadAgentId || undefined,
60
+ routingReason: leadAgentId ? "skill" : undefined,
60
61
  }),
61
62
  });
62
63
  if (!res.ok) {
@@ -257,6 +257,7 @@ export async function handlePullRequest(
257
257
 
258
258
  const task = await createTaskWithSiblingAwareness(result.text, {
259
259
  agentId: lead?.id ?? "",
260
+ routingReason: lead ? "skill" : undefined,
260
261
  source: "github",
261
262
  vcsProvider: "github",
262
263
  taskType: "github-pr",
@@ -370,6 +371,7 @@ export async function handlePullRequest(
370
371
 
371
372
  const task = await createTaskWithSiblingAwareness(result.text, {
372
373
  agentId: lead?.id ?? "",
374
+ routingReason: lead ? "skill" : undefined,
373
375
  source: "github",
374
376
  vcsProvider: "github",
375
377
  taskType: "github-pr",
@@ -478,6 +480,7 @@ export async function handlePullRequest(
478
480
 
479
481
  const task = await createTaskWithSiblingAwareness(result.text, {
480
482
  agentId: lead?.id ?? "",
483
+ routingReason: lead ? "skill" : undefined,
481
484
  source: "github",
482
485
  vcsProvider: "github",
483
486
  taskType: "github-pr",
@@ -573,6 +576,7 @@ export async function handlePullRequest(
573
576
  // Create task (assigned to lead if available, otherwise unassigned)
574
577
  const task = await createTaskWithSiblingAwareness(result.text, {
575
578
  agentId: lead?.id ?? "",
579
+ routingReason: lead ? "skill" : undefined,
576
580
  source: "github",
577
581
  vcsProvider: "github",
578
582
  taskType: "github-pr",
@@ -657,6 +661,7 @@ export async function handleIssue(
657
661
 
658
662
  const task = await createTaskWithSiblingAwareness(result.text, {
659
663
  agentId: lead?.id ?? "",
664
+ routingReason: lead ? "skill" : undefined,
660
665
  source: "github",
661
666
  vcsProvider: "github",
662
667
  taskType: "github-issue",
@@ -758,6 +763,7 @@ export async function handleIssue(
758
763
 
759
764
  const task = await createTaskWithSiblingAwareness(result.text, {
760
765
  agentId: lead?.id ?? "",
766
+ routingReason: lead ? "skill" : undefined,
761
767
  source: "github",
762
768
  vcsProvider: "github",
763
769
  taskType: "github-issue",
@@ -835,6 +841,7 @@ export async function handleIssue(
835
841
  // Create task (assigned to lead if available, otherwise unassigned)
836
842
  const task = await createTaskWithSiblingAwareness(result.text, {
837
843
  agentId: lead?.id ?? "",
844
+ routingReason: lead ? "skill" : undefined,
838
845
  source: "github",
839
846
  vcsProvider: "github",
840
847
  taskType: "github-issue",
@@ -946,6 +953,7 @@ export async function handleComment(
946
953
  // Create task (assigned to lead if available, otherwise unassigned)
947
954
  const task = await createTaskWithSiblingAwareness(result.text, {
948
955
  agentId: lead?.id ?? "",
956
+ routingReason: lead ? "skill" : undefined,
949
957
  source: "github",
950
958
  vcsProvider: "github",
951
959
  taskType: "github-comment",
@@ -1260,6 +1268,7 @@ export async function handlePullRequestReview(
1260
1268
  // Create task (assigned to lead if available, otherwise unassigned)
1261
1269
  const task = await createTaskWithSiblingAwareness(result.text, {
1262
1270
  agentId: lead?.id ?? "",
1271
+ routingReason: lead ? "skill" : undefined,
1263
1272
  source: "github",
1264
1273
  vcsProvider: "github",
1265
1274
  taskType: "github-review",
@@ -175,6 +175,7 @@ export async function handleMergeRequest(
175
175
 
176
176
  const task = await createTaskWithSiblingAwareness(result.text, {
177
177
  agentId: lead?.id ?? null,
178
+ routingReason: lead ? "skill" : undefined,
178
179
  source: "gitlab",
179
180
  vcsProvider: "gitlab",
180
181
  taskType: "gitlab-mr",
@@ -282,6 +283,7 @@ export async function handleIssue(
282
283
 
283
284
  const task = await createTaskWithSiblingAwareness(result.text, {
284
285
  agentId: lead?.id ?? null,
286
+ routingReason: lead ? "skill" : undefined,
285
287
  source: "gitlab",
286
288
  vcsProvider: "gitlab",
287
289
  taskType: "gitlab-issue",
@@ -385,6 +387,7 @@ export async function handleNote(event: NoteEvent): Promise<{ created: boolean;
385
387
 
386
388
  const task = await createTaskWithSiblingAwareness(noteResult.text, {
387
389
  agentId: lead?.id ?? null,
390
+ routingReason: lead ? "skill" : undefined,
388
391
  source: "gitlab",
389
392
  vcsProvider: "gitlab",
390
393
  taskType: "gitlab-comment",
@@ -465,6 +468,7 @@ export async function handlePipeline(
465
468
 
466
469
  const task = await createTaskWithSiblingAwareness(pipelineResult.text, {
467
470
  agentId: lead?.id ?? null,
471
+ routingReason: lead ? "skill" : undefined,
468
472
  source: "gitlab",
469
473
  vcsProvider: "gitlab",
470
474
  taskType: "gitlab-ci",
@@ -760,6 +760,12 @@ export async function runRebootSweep(): Promise<void> {
760
760
  const retryTask = await createTaskExtended(task.task, {
761
761
  parentTaskId: task.id,
762
762
  agentId: preferredAgentId,
763
+ routingReason:
764
+ preferredAgentId === undefined
765
+ ? undefined
766
+ : preferredAgentId === task.agentId
767
+ ? "continuity"
768
+ : "reroute_fault",
763
769
  tags,
764
770
  priority: task.priority,
765
771
  source: task.source,
@@ -1368,6 +1374,7 @@ export async function checkHeartbeatChecklist(): Promise<void> {
1368
1374
 
1369
1375
  await createTaskExtended(result.text, {
1370
1376
  agentId: lead.id,
1377
+ routingReason: "skill",
1371
1378
  taskType: "heartbeat-checklist",
1372
1379
  tags: ["checklist", "auto-generated"],
1373
1380
  priority: 60,
@@ -1584,6 +1591,7 @@ export async function createBootTriageTask(): Promise<void> {
1584
1591
 
1585
1592
  await createTaskExtended(result.text, {
1586
1593
  agentId: lead.id,
1594
+ routingReason: "skill",
1587
1595
  taskType: "boot-triage",
1588
1596
  tags: ["boot", "triage", "auto-generated"],
1589
1597
  priority: 70, // Higher than regular checklist (60)
@@ -385,6 +385,7 @@ export async function handleApprovalRequests(
385
385
 
386
386
  await createTaskExtended(taskText, {
387
387
  agentId: sourceTask.agentId,
388
+ routingReason: sourceTask.agentId ? "continuity" : undefined,
388
389
  parentTaskId: updated.sourceTaskId,
389
390
  source: "system",
390
391
  taskType: "hitl-follow-up",
package/src/http/apps.ts CHANGED
@@ -1413,6 +1413,7 @@ export async function handleApps(
1413
1413
  const task = await createTaskWithSiblingAwareness(taskPrompt.text, {
1414
1414
  source: "api",
1415
1415
  agentId: action.agentId ?? lead?.id,
1416
+ routingReason: action.agentId ? "human_pinned" : lead ? "skill" : undefined,
1416
1417
  // App-spawned tasks group under the app's asset namespace so a
1417
1418
  // swarm-tasks source can pull them back via config.assetKey.
1418
1419
  key: normalizeAssetKey(`shared/app:${app.id}/action:${parsed.params.name}/`),
@@ -48,11 +48,19 @@ function singleHeader(req: IncomingMessage, name: string): string | undefined {
48
48
  return Array.isArray(raw) ? raw[0] : raw;
49
49
  }
50
50
 
51
- async function userMayReadSecrets(req: IncomingMessage): Promise<boolean> {
51
+ async function requestMayReadSecrets(req: IncomingMessage): Promise<boolean> {
52
52
  const auth = getRequestAuth(req);
53
53
  if (auth?.kind === "operator") return true;
54
- // Agent/no-user HTTP reads keep their pre-RBAC behavior; MCP get-config has
55
- // the per-agent config.read.secrets gate.
54
+ if (auth?.kind === "agent") {
55
+ const agent = await getAgentById(auth.agentId);
56
+ return can({
57
+ principal: { kind: "agent", agentId: auth.agentId, isLead: agent?.isLead ?? false },
58
+ verb: "config.read.secrets",
59
+ resource: { kind: "none" },
60
+ source: "http",
61
+ }).allow;
62
+ }
63
+ // Preserve legacy reads without a user or session-token principal.
56
64
  if (auth?.kind !== "user") return true;
57
65
  if (!isRbacEnabled()) return true;
58
66
 
@@ -61,7 +69,7 @@ async function userMayReadSecrets(req: IncomingMessage): Promise<boolean> {
61
69
  }
62
70
 
63
71
  async function resolveSecretsRead(req: IncomingMessage, includeSecrets: boolean) {
64
- if (!includeSecrets || (await userMayReadSecrets(req))) {
72
+ if (!includeSecrets || (await requestMayReadSecrets(req))) {
65
73
  return { effectiveIncludeSecrets: includeSecrets, secretsNote: "" };
66
74
  }
67
75
  return { effectiveIncludeSecrets: false, secretsNote: SECRETS_FORCE_MASK_NOTE };
@@ -75,7 +83,8 @@ async function resolveSecretsRead(req: IncomingMessage, includeSecrets: boolean)
75
83
  * operator/user-before-agent ordering used by fs.ts (Appendix A row 36). This
76
84
  * preserves every existing HTTP caller (dashboard, codex-oauth token refresh,
77
85
  * devin playbook cache — all operate as operator). Only an agent-context
78
- * principal (X-Agent-ID without operator/user request auth) is gated to lead;
86
+ * principal is gated to lead, using the authenticated session identity ahead
87
+ * of the legacy X-Agent-ID fallback;
79
88
  * the real per-agent enforcement lives on the MCP set-config/delete-config
80
89
  * tools, where the principal is always an agent.
81
90
  *
@@ -89,7 +98,7 @@ async function ensureConfigAdmin(
89
98
  ): Promise<boolean> {
90
99
  const auth = getRequestAuth(req);
91
100
  if (auth?.kind === "operator" || auth?.kind === "user") return true;
92
- const agentId = singleHeader(req, "x-agent-id");
101
+ const agentId = auth?.kind === "agent" ? auth.agentId : singleHeader(req, "x-agent-id");
93
102
  const agent = agentId ? await getAgentById(agentId) : undefined;
94
103
  const decision = can({
95
104
  principal: { kind: "agent", agentId: agentId ?? "", isLead: agent?.isLead ?? false },
package/src/http/mcp.ts CHANGED
@@ -2,10 +2,12 @@ import { randomUUID } from "node:crypto";
2
2
  import type { IncomingMessage, ServerResponse } from "node:http";
3
3
  import { StreamableHTTPServerTransport } from "@modelcontextprotocol/sdk/server/streamableHttp.js";
4
4
  import { isInitializeRequest } from "@modelcontextprotocol/sdk/types.js";
5
- import { getAgentById, getResolvedConfig } from "@/be/db";
5
+ import { getAgentById, getResolvedConfig, getTaskById } from "@/be/db";
6
6
  import { createServer } from "@/server";
7
+ import { parseEnvFlag } from "@/utils/env-flag";
7
8
  import { getRequestAuth } from "@/utils/request-auth-context";
8
9
  import { resolveScriptsOnlyMode } from "@/utils/scripts-only-mode";
10
+ import { parseTaskToolManifest, selectTaskTools } from "@/utils/task-tool-manifest";
9
11
 
10
12
  export type McpTransportActivity = Record<string, number>;
11
13
  export type McpSessionAgents = Record<string, string>;
@@ -196,10 +198,35 @@ export async function handleMcp(
196
198
  }
197
199
  };
198
200
 
199
- const configValue = (await getResolvedConfig(agentId)).find(
200
- (config) => config.key === "SCRIPTS_ONLY_MCP",
201
- )?.value;
201
+ const configs = await getResolvedConfig(agentId);
202
+ const configValue = configs.find((config) => config.key === "SCRIPTS_ONLY_MCP")?.value;
203
+ const preloadEnabled = parseEnvFlag(
204
+ configs.find((config) => config.key === "TASK_TOOL_PRELOAD_ENABLED")?.value ??
205
+ process.env.TASK_TOOL_PRELOAD_ENABLED,
206
+ false,
207
+ );
208
+ let preloadedTools: string[] = [];
209
+ if (preloadEnabled) {
210
+ const taskId = headerValue(req.headers["x-source-task-id"]);
211
+ const task = taskId ? await getTaskById(taskId) : undefined;
212
+ // Never select a manifest using another agent's task. Session-token
213
+ // identity is also checked above, before any MCP session is created.
214
+ if (task?.agentId === agentId) {
215
+ const manifestValue =
216
+ configs.find((config) => config.key === "TASK_TOOL_MANIFESTS")?.value ??
217
+ process.env.TASK_TOOL_MANIFESTS ??
218
+ "{}";
219
+ try {
220
+ preloadedTools = selectTaskTools(parseTaskToolManifest(manifestValue), task);
221
+ } catch {
222
+ // An invalid deployment value must not prevent tool discovery.
223
+ // Avoid logging its contents, which may contain misfiled secrets.
224
+ console.warn("[MCP] Invalid TASK_TOOL_MANIFESTS; using ordinary tool discovery");
225
+ }
226
+ }
227
+ }
202
228
  const server = await createServer({
229
+ preloadedTools,
203
230
  scriptsOnly: resolveScriptsOnlyMode({
204
231
  env: process.env.SCRIPTS_ONLY_MCP,
205
232
  configValue,
@@ -30,6 +30,7 @@ import {
30
30
  } from "../script-workflows/supervisor";
31
31
  import {
32
32
  AgentTaskStatusSchema,
33
+ RoutingReasonSchema,
33
34
  ScriptRunJournalEntrySchema,
34
35
  ScriptRunListItemSchema,
35
36
  ScriptRunSchema,
@@ -92,6 +93,8 @@ const agentTaskBodySchema = z.object({
92
93
  template: z.string().optional(),
93
94
  task: z.string().optional(),
94
95
  agentId: z.string().optional(),
96
+ routingReason: RoutingReasonSchema.optional(),
97
+ routingNote: z.string().max(200).optional(),
95
98
  tags: z.array(z.string()).optional(),
96
99
  priority: z.number().int().min(0).max(100).optional(),
97
100
  offerMode: z.boolean().optional(),
@@ -619,6 +622,9 @@ export async function handleScriptRuns(
619
622
  parsed.body.template ?? parsed.body.task ?? parsed.body.stepKey,
620
623
  {
621
624
  agentId: parsed.body.agentId,
625
+ routingReason:
626
+ parsed.body.routingReason ?? (parsed.body.agentId ? "human_pinned" : undefined),
627
+ routingNote: parsed.body.routingNote,
622
628
  tags: parsed.body.tags,
623
629
  priority: parsed.body.priority,
624
630
  offeredTo: parsed.body.offerMode ? parsed.body.agentId : undefined,
package/src/http/tasks.ts CHANGED
@@ -59,6 +59,7 @@ import {
59
59
  ProviderNameSchema,
60
60
  ReasoningEffortSchema,
61
61
  ResumeReasonSchema,
62
+ RoutingReasonSchema,
62
63
  SteeringMessageSchema,
63
64
  SteeringSourceSchema,
64
65
  SteerModeSchema,
@@ -216,33 +217,45 @@ const createTask = route({
216
217
  pattern: ["api", "tasks"],
217
218
  summary: "Create a new task",
218
219
  tags: ["Tasks"],
219
- body: z.object({
220
- task: z.string().min(1),
221
- agentId: z.string().optional(),
222
- taskType: z.string().optional(),
223
- tags: z.array(z.string()).optional(),
224
- priority: z.number().int().min(0).max(100).optional(),
225
- dependsOn: z.array(z.string()).optional(),
226
- offeredTo: z.string().optional(),
227
- dir: z.string().optional(),
228
- parentTaskId: z.string().optional(),
229
- key: AssetKeySchema.optional(),
230
- source: AgentTaskSourceSchema.optional(),
231
- outputSchema: z.record(z.string(), z.unknown()).optional(),
232
- contextKey: z.string().optional(),
233
- requestedByUserId: z.string().optional(),
234
- model: z.string().optional(),
235
- modelTier: ModelTierSchema.optional(),
236
- effort: ReasoningEffortSchema.optional(),
237
- /**
238
- * Create in `draft` status instead of the normal pending/unassigned/offered
239
- * status (#1240) — the task exists and is visible to its owner, but is not
240
- * dispatch-eligible. Used by the UI composer while attachments are still
241
- * uploading; the caller MUST promote it via `POST /api/tasks/{id}/promote-draft`
242
- * once the upload batch settles (or it self-promotes on a timeout).
243
- */
244
- draft: z.boolean().optional(),
245
- }),
220
+ body: z
221
+ .object({
222
+ task: z.string().min(1),
223
+ agentId: z.string().optional(),
224
+ routingReason: RoutingReasonSchema.optional(),
225
+ routingNote: z.string().max(200).optional(),
226
+ taskType: z.string().optional(),
227
+ tags: z.array(z.string()).optional(),
228
+ priority: z.number().int().min(0).max(100).optional(),
229
+ dependsOn: z.array(z.string()).optional(),
230
+ offeredTo: z.string().optional(),
231
+ dir: z.string().optional(),
232
+ parentTaskId: z.string().optional(),
233
+ key: AssetKeySchema.optional(),
234
+ source: AgentTaskSourceSchema.optional(),
235
+ outputSchema: z.record(z.string(), z.unknown()).optional(),
236
+ contextKey: z.string().optional(),
237
+ requestedByUserId: z.string().optional(),
238
+ model: z.string().optional(),
239
+ modelTier: ModelTierSchema.optional(),
240
+ effort: ReasoningEffortSchema.optional(),
241
+ /**
242
+ * Create in `draft` status instead of the normal pending/unassigned/offered
243
+ * status (#1240) — the task exists and is visible to its owner, but is not
244
+ * dispatch-eligible. Used by the UI composer while attachments are still
245
+ * uploading; the caller MUST promote it via `POST /api/tasks/{id}/promote-draft`
246
+ * once the upload batch settles (or it self-promotes on a timeout).
247
+ */
248
+ draft: z.boolean().optional(),
249
+ })
250
+ .superRefine((body, ctx) => {
251
+ if ((body.agentId !== undefined || body.offeredTo !== undefined) && !body.routingReason) {
252
+ ctx.addIssue({
253
+ code: z.ZodIssueCode.custom,
254
+ message: "routingReason is required when agentId or offeredTo is supplied.",
255
+ path: ["routingReason"],
256
+ });
257
+ }
258
+ }),
246
259
  responses: {
247
260
  201: { description: "Task created", schema: AgentTaskSchema },
248
261
  400: { description: "Validation error" },
@@ -804,11 +817,12 @@ export async function handleTasks(
804
817
  if (lead) defaultAgentId = lead.id;
805
818
  }
806
819
 
820
+ const parentTask = parsed.body.parentTaskId
821
+ ? await getTaskById(parsed.body.parentTaskId)
822
+ : null;
807
823
  let assetKey: string | undefined;
808
824
  try {
809
- const inheritedKey = parsed.body.parentTaskId
810
- ? (await getTaskById(parsed.body.parentTaskId))?.key
811
- : undefined;
825
+ const inheritedKey = parentTask?.key;
812
826
  const requestedKey = parsed.body.key ?? inheritedKey;
813
827
  assetKey = requestedKey
814
828
  ? await authorizeAssetKeyWrite(requestedKey, trustedUserId)
@@ -825,6 +839,14 @@ export async function handleTasks(
825
839
  const task = await createTaskWithSiblingAwareness(parsed.body.task, {
826
840
  key: assetKey,
827
841
  agentId: defaultAgentId,
842
+ routingReason:
843
+ parsed.body.routingReason ??
844
+ (defaultAgentId
845
+ ? parentTask?.agentId === defaultAgentId
846
+ ? "continuity"
847
+ : "skill"
848
+ : undefined),
849
+ routingNote: parsed.body.routingNote,
828
850
  creatorAgentId: myAgentId || undefined,
829
851
  taskType: parsed.body.taskType || undefined,
830
852
  tags: parsed.body.tags || undefined,