@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.
- package/README.md +13 -1
- package/dist/{acp-adapter-9n319wqc.js → acp-adapter-4jncb126.js} +12 -5
- package/dist/{actions-2vxqvpr9.js → actions-31vp3ae7.js} +10 -9
- package/dist/{app-w066xfy0.js → app-1hh67x8w.js} +6 -6
- package/dist/{assistant-kt31dj2h.js → assistant-z66dmz6p.js} +13 -11
- package/dist/{boot-reembed-az5rassp.js → boot-reembed-fqzxxzjh.js} +7 -7
- package/dist/{boot-reembed-j3mm3rfz.js → boot-reembed-wxa8ya0s.js} +8 -8
- package/dist/{boot-scrub-logs-b6h54817.js → boot-scrub-logs-jnyndh79.js} +5 -5
- package/dist/{claude-adapter-w1z04ann.js → claude-adapter-3qpa9w0j.js} +9 -9
- package/dist/{claude-managed-adapter-sactwn31.js → claude-managed-adapter-qvsmh6mv.js} +2 -2
- package/dist/{claude-sdk-session-f0cc1kec.js → claude-sdk-session-2yxgjs5d.js} +9 -9
- package/dist/{cli-y6b6fb76.js → cli-08b07b4r.js} +1 -1
- package/dist/{cli-8fc8de14.js → cli-0z2v4nhw.js} +1 -1
- package/dist/{cli-de14znh8.js → cli-19f354qr.js} +2 -2
- package/dist/{cli-hbv0kq6w.js → cli-2qts1hys.js} +3 -3
- package/dist/{cli-kpv03zhj.js → cli-3apbzgyk.js} +27 -15
- package/dist/{cli-1th1728g.js → cli-3q5ejx9p.js} +2 -2
- package/dist/{cli-d8brzxjb.js → cli-4wtq5jjv.js} +2 -2
- package/dist/{cli-ct0et58h.js → cli-5my3bjsd.js} +1 -1
- package/dist/{cli-cn6mmc7f.js → cli-6664w7y4.js} +1 -1
- package/dist/{cli-yz3djrm0.js → cli-6pqcb4q0.js} +53 -13
- package/dist/{cli-719k9j2c.js → cli-7smrsr25.js} +3 -3
- package/dist/{cli-rw51bq3j.js → cli-9fy6nk6g.js} +1 -1
- package/dist/{cli-zhjmqxzh.js → cli-9hspd3dp.js} +2 -2
- package/dist/{cli-3tgwnf43.js → cli-aae6hz6a.js} +4 -4
- package/dist/{cli-89xfd7f9.js → cli-cr12paw2.js} +5 -5
- package/dist/{cli-b5bq9crk.js → cli-d8ftsp62.js} +8 -8
- package/dist/{cli-4nr988h6.js → cli-e95cx1eb.js} +3 -3
- package/dist/{cli-n8508nre.js → cli-gjhjfeeg.js} +1 -1
- package/dist/{cli-p1d5b073.js → cli-gv88e1vf.js} +1 -1
- package/dist/{cli-rvqz34y5.js → cli-jecc01cb.js} +1 -1
- package/dist/{cli-8v0g1yc8.js → cli-k0cr4kat.js} +1 -1
- package/dist/{cli-mmemxxdc.js → cli-kgz4np97.js} +4 -4
- package/dist/{cli-w9kb3bk1.js → cli-kjwt6hdf.js} +3 -1
- package/dist/{cli-f7dsy61y.js → cli-mnmsfd1w.js} +6 -2
- package/dist/{cli-54wwve05.js → cli-n3dz2942.js} +37 -26
- package/dist/{cli-12fz972k.js → cli-nye12xk5.js} +8 -5
- package/dist/{cli-tzvk9haz.js → cli-phsxnkp6.js} +20 -20
- package/dist/{cli-53s590z8.js → cli-q50ef8g0.js} +1 -1
- package/dist/{cli-f146mn5s.js → cli-qaacms9y.js} +7 -6
- package/dist/{cli-xqcq9y5e.js → cli-r2ap2czm.js} +1 -0
- package/dist/{cli-m8yrsg97.js → cli-sd5pv3b1.js} +3 -3
- package/dist/{cli-0b6y7kxm.js → cli-xm87hzbw.js} +1 -1
- package/dist/{cli-ftht3zzj.js → cli-yab68w40.js} +285 -11
- package/dist/{cli-atcve9y8.js → cli-yb9qhqam.js} +227 -59
- package/dist/{cli-smby5m17.js → cli-ygweqcn4.js} +133 -22
- package/dist/{cli-dttph1bw.js → cli-ykxrnd1q.js} +2 -2
- package/dist/{cli-m9sxbkhm.js → cli-zvz62chv.js} +3 -3
- package/dist/cli.js +16 -14
- package/dist/{codex-adapter-zw4dwsx7.js → codex-adapter-8meawydj.js} +4 -4
- package/dist/{codex-hook-bw8p5nh4.js → codex-hook-hfrt9shr.js} +2 -2
- package/dist/{codex-session-runner-k981s65v.js → codex-session-runner-gjgxyp5j.js} +4 -4
- package/dist/{commands-sykrxx7e.js → commands-hz3nb95f.js} +5 -5
- package/dist/{db-sfbsc8wt.js → db-ggz97zbm.js} +5 -5
- package/dist/{e2b-6hb103d8.js → e2b-p04vmtx0.js} +1 -1
- package/dist/{handlers-0aspcejm.js → handlers-nbzdzn9k.js} +14 -11
- package/dist/{hook-hxccs7m5.js → hook-bks2q1f6.js} +7 -7
- package/dist/{hook-4nvqj2sb.js → hook-kq9wp0ep.js} +8 -8
- package/dist/{http-y06z517d.js → http-hrf77v87.js} +96 -45
- package/dist/{index-tce1rss4.js → index-6nsbg88z.js} +443 -144
- package/dist/{index-x5j894zf.js → index-8hmeg8j1.js} +14 -14
- package/dist/{index-k8znfx0z.js → index-cqtebbhb.js} +13 -13
- package/dist/{index-hssww7kj.js → index-n28pw1zt.js} +15 -15
- package/dist/{keepalive-w733ax66.js → keepalive-dxc12gvg.js} +7 -7
- package/dist/{lead-75bjqa9d.js → lead-yckfn18d.js} +32 -32
- package/dist/{maintenance-w4f1zjck.js → maintenance-6kfb2m5e.js} +8 -8
- package/dist/{oauth-refresh-sweep-wdetky26.js → oauth-refresh-sweep-1x1ac722.js} +6 -6
- package/dist/{onboard-bb5net8s.js → onboard-dcyt1609.js} +4 -3
- package/dist/{opencode-adapter-fef1mrn2.js → opencode-adapter-dce4wskd.js} +2 -2
- package/dist/{otel-impl-0w7c14ap.js → otel-impl-tbk3e5dr.js} +2 -2
- package/dist/{pi-mono-adapter-2has4gnp.js → pi-mono-adapter-zvshk5cj.js} +2 -2
- package/dist/{pricing-refresh-9nzsrgcq.js → pricing-refresh-kaqg567e.js} +7 -7
- package/dist/{rbac-roles-z1jrffbt.js → rbac-roles-ammq6rd2.js} +5 -5
- package/dist/{rbac-roles-r3hqjp09.js → rbac-roles-cf97v1vd.js} +6 -6
- package/dist/{render-v2-vbr89fnb.js → render-v2-80w603vy.js} +6 -6
- package/dist/{seed-pricing-g62hy1pk.js → seed-pricing-zbrn83xv.js} +6 -6
- package/dist/{setup-gnfqnptp.js → setup-fe66kczs.js} +2 -2
- package/dist/{worker-kznxp4qm.js → worker-qmb2d1e2.js} +32 -32
- package/dist/{x-n2phr5vm.js → x-n3e0v3xa.js} +2 -2
- package/openapi.json +46 -4
- package/package.json +3 -1
- package/src/agentmail/handlers.ts +5 -0
- package/src/automation-preflight-alert.ts +41 -0
- package/src/be/automation-preflight.ts +3 -3
- package/src/be/budget-refusal-notify.ts +1 -0
- package/src/be/db/tasks/read.ts +5 -0
- package/src/be/db.ts +132 -2
- package/src/be/migrations/150_deferred_task_waits.sql +18 -0
- package/src/be/migrations/151_repair_scheduled_task_required_params.sql +39 -0
- package/src/be/migrations/152_routing_decisions.sql +44 -0
- package/src/be/scripts/typecheck.ts +33 -22
- package/src/be/seed-scripts/catalog/delegate.ts +5 -1
- package/src/be/seed-skills/bundled-files.generated.json +10 -0
- package/src/be/seed-skills/index.ts +3 -0
- package/src/be/steering.ts +1 -0
- package/src/be/swarm-config-guard.ts +11 -0
- package/src/commands/onboard/steps/post-task.tsx +1 -0
- package/src/github/handlers.ts +9 -0
- package/src/gitlab/handlers.ts +4 -0
- package/src/heartbeat/heartbeat.ts +8 -0
- package/src/http/approval-requests.ts +1 -0
- package/src/http/apps.ts +1 -0
- package/src/http/config.ts +15 -6
- package/src/http/mcp.ts +31 -4
- package/src/http/script-runs.ts +6 -0
- package/src/http/tasks.ts +52 -30
- package/src/integrations/kapso/inbound.ts +1 -0
- package/src/jira/sync.ts +2 -0
- package/src/linear/sync.ts +2 -0
- package/src/prompts/session-templates.ts +29 -15
- package/src/scheduler/deferred-task-waits.ts +123 -0
- package/src/scheduler/schedule-task.ts +42 -0
- package/src/scheduler/scheduler.ts +22 -31
- package/src/script-workflows/workflow-ctx.ts +2 -0
- package/src/scripts-runtime/types/stdlib.d.ts +38 -24
- package/src/scripts-runtime/types/swarm-sdk.d.ts +38 -24
- package/src/server.ts +10 -1
- package/src/slack/actions.ts +1 -0
- package/src/slack/assistant.ts +2 -0
- package/src/slack/handlers.ts +3 -0
- package/src/slack/thread-buffer.ts +2 -0
- package/src/tasks/worker-follow-up.ts +9 -0
- package/src/tests/acp-adapter.test.ts +35 -6
- package/src/tests/acp-session-token.test.ts +67 -0
- package/src/tests/asset-key-api.test.ts +15 -2
- package/src/tests/asset-key-mcp.test.ts +2 -0
- package/src/tests/automation-preflight-alert.test.ts +91 -0
- package/src/tests/claude-worker-parity.test.ts +3 -0
- package/src/tests/config-session-auth.test.ts +143 -0
- package/src/tests/defer-task.test.ts +117 -6
- package/src/tests/deferred-task-wake.test.ts +456 -0
- package/src/tests/heartbeat-reroute-decision.test.ts +1 -0
- package/src/tests/http-api-integration.test.ts +33 -4
- package/src/tests/mcp-input-ergonomics.test.ts +386 -0
- package/src/tests/model-control.test.ts +18 -5
- package/src/tests/opencode-adapter.test.ts +2 -2
- package/src/tests/promote-draft-task-route.test.ts +4 -0
- package/src/tests/prompt-template-session.test.ts +13 -4
- package/src/tests/rbac-wire-e2e.test.ts +2 -2
- package/src/tests/routing-decision-persistence.test.ts +520 -0
- package/src/tests/routing-reason-contract.test.ts +89 -0
- package/src/tests/routing-reason-inventory.test.ts +55 -0
- package/src/tests/schedule-target-type.test.ts +48 -1
- package/src/tests/scheduled-task-required-params-migration.test.ts +182 -0
- package/src/tests/scheduled-tasks.test.ts +9 -5
- package/src/tests/script-connections.test.ts +4 -0
- package/src/tests/script-runs-http.test.ts +31 -0
- package/src/tests/scripts-typecheck.test.ts +11 -0
- package/src/tests/secret-scrubber.test.ts +12 -0
- package/src/tests/send-task-output-schema.test.ts +66 -1
- package/src/tests/send-task-requested-by.test.ts +1 -0
- package/src/tests/send-task-slack-routing-guard.test.ts +2 -0
- package/src/tests/store-progress-blocked-waiting-gate.test.ts +151 -0
- package/src/tests/swarm-tool-result-gate.test.ts +24 -0
- package/src/tests/task-tool-manifest.test.ts +47 -0
- package/src/tests/task-tool-preload-mcp.test.ts +125 -0
- package/src/tests/task-tools-ctx.test.ts +2 -0
- package/src/tests/tool-output-agent-id.test.ts +1 -0
- package/src/tests/ui-followup-lead-delegation.test.ts +6 -1
- package/src/tests/workflow-agent-task.test.ts +24 -1
- package/src/tests/workflow-engine-v2.test.ts +58 -2
- package/src/tools/accept-steer.ts +0 -1
- package/src/tools/defer-task.ts +73 -21
- package/src/tools/memory-get.ts +10 -3
- package/src/tools/memory-rate.ts +19 -7
- package/src/tools/memory-search.ts +2 -1
- package/src/tools/memory-store.ts +25 -5
- package/src/tools/send-task.ts +40 -2
- package/src/tools/skills/skill-publish.ts +1 -0
- package/src/tools/store-progress.ts +78 -4
- package/src/tools/templates.ts +2 -1
- package/src/tools/utils.ts +33 -2
- package/src/types.ts +14 -1
- package/src/utils/acp-session-token.ts +14 -4
- package/src/utils/secret-scrubber.ts +2 -0
- package/src/utils/task-tool-manifest.ts +33 -0
- package/src/workflows/engine.ts +4 -1
- package/src/workflows/executors/agent-task.ts +7 -1
- package/templates/ai-toolbox.manifest.json +6 -1
- package/templates/skills/comms/config.json +18 -0
- package/templates/skills/comms/content.md +74 -0
- package/templates/skills/comms/files/references/ste-rules.md +73 -0
- package/templates/skills/comms/files/references/visual-shapes.md +112 -0
- package/templates/skills/swarm-scripts/SKILL.md +3 -2
- package/templates/skills/swarm-scripts/content.md +3 -2
|
@@ -12,11 +12,22 @@
|
|
|
12
12
|
* - HTTP route cross-field validation (create + update) for workflow/script targets.
|
|
13
13
|
*/
|
|
14
14
|
import { Database } from "bun:sqlite";
|
|
15
|
-
import {
|
|
15
|
+
import {
|
|
16
|
+
afterAll,
|
|
17
|
+
afterEach,
|
|
18
|
+
beforeAll,
|
|
19
|
+
beforeEach,
|
|
20
|
+
describe,
|
|
21
|
+
expect,
|
|
22
|
+
setSystemTime,
|
|
23
|
+
spyOn,
|
|
24
|
+
test,
|
|
25
|
+
} from "bun:test";
|
|
16
26
|
import { unlink } from "node:fs/promises";
|
|
17
27
|
import type { IncomingMessage, ServerResponse } from "node:http";
|
|
18
28
|
import { Readable } from "node:stream";
|
|
19
29
|
import { z } from "zod";
|
|
30
|
+
import * as preflightAlerts from "../automation-preflight-alert";
|
|
20
31
|
import {
|
|
21
32
|
type AutomationSetupStates,
|
|
22
33
|
preflightAutomation,
|
|
@@ -88,6 +99,16 @@ class EchoExecutor extends BaseExecutor<typeof EchoExecutor.schema, typeof EchoE
|
|
|
88
99
|
|
|
89
100
|
let savedEnv: NodeJS.ProcessEnv;
|
|
90
101
|
let agentId: string;
|
|
102
|
+
let notify: ReturnType<typeof spyOn<typeof preflightAlerts, "notifyAutomationPreflightFailure">>;
|
|
103
|
+
|
|
104
|
+
beforeEach(() => {
|
|
105
|
+
notify = spyOn(preflightAlerts, "notifyAutomationPreflightFailure").mockResolvedValue(undefined);
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
afterEach(() => {
|
|
109
|
+
notify.mockRestore();
|
|
110
|
+
setSystemTime();
|
|
111
|
+
});
|
|
91
112
|
|
|
92
113
|
async function removeDbFiles(): Promise<void> {
|
|
93
114
|
for (const suffix of ["", "-wal", "-shm"]) {
|
|
@@ -617,6 +638,10 @@ describe("dispatchScheduleTarget — automation preflight", () => {
|
|
|
617
638
|
|
|
618
639
|
await executeSchedule(schedule);
|
|
619
640
|
const first = (await getScheduledTaskById(schedule.id))!;
|
|
641
|
+
expect(notify).toHaveBeenCalledTimes(1);
|
|
642
|
+
expect(notify).toHaveBeenCalledWith(
|
|
643
|
+
expect.objectContaining({ kind: "schedule", name: schedule.name, state: "needs_setup" }),
|
|
644
|
+
);
|
|
620
645
|
await executeSchedule(first);
|
|
621
646
|
const second = (await getScheduledTaskById(schedule.id))!;
|
|
622
647
|
const after = await getDbClient().get<{ count: number }>(
|
|
@@ -624,6 +649,7 @@ describe("dispatchScheduleTarget — automation preflight", () => {
|
|
|
624
649
|
);
|
|
625
650
|
|
|
626
651
|
expect(after?.count).toBe(before?.count);
|
|
652
|
+
expect(notify).toHaveBeenCalledTimes(1);
|
|
627
653
|
expect(second.lastErrorAt).toBe(first.lastErrorAt);
|
|
628
654
|
expect(second.lastErrorMessage).toBe("needs_setup: params=[REPO_URL] integrations=[]");
|
|
629
655
|
expect(second.consecutiveErrors).toBe(0);
|
|
@@ -632,6 +658,27 @@ describe("dispatchScheduleTarget — automation preflight", () => {
|
|
|
632
658
|
expect(new Date(second.nextRunAt!).getTime()).toBeGreaterThan(Date.now());
|
|
633
659
|
});
|
|
634
660
|
|
|
661
|
+
test("alerts once for concurrent schedule refusals and again after UTC midnight", async () => {
|
|
662
|
+
setSystemTime(new Date("2026-09-14T23:59:59Z"));
|
|
663
|
+
const schedule = await createScheduledTask({
|
|
664
|
+
name: `dispatch-daily-alert-${crypto.randomUUID()}`,
|
|
665
|
+
intervalMs: 60_000,
|
|
666
|
+
taskTemplate: "Review {{REPO_URL}}",
|
|
667
|
+
requiredParams: ["REPO_URL"],
|
|
668
|
+
});
|
|
669
|
+
|
|
670
|
+
const refusals = await Promise.allSettled([
|
|
671
|
+
dispatchScheduleTarget(schedule),
|
|
672
|
+
dispatchScheduleTarget(schedule),
|
|
673
|
+
]);
|
|
674
|
+
expect(refusals.every((result) => result.status === "rejected")).toBe(true);
|
|
675
|
+
expect(notify).toHaveBeenCalledTimes(1);
|
|
676
|
+
|
|
677
|
+
setSystemTime(new Date("2026-09-15T00:00:00Z"));
|
|
678
|
+
await expect(dispatchScheduleTarget(schedule)).rejects.toThrow("needs_setup:");
|
|
679
|
+
expect(notify).toHaveBeenCalledTimes(2);
|
|
680
|
+
});
|
|
681
|
+
|
|
635
682
|
test("advances a cron schedule while its required timezone is still unset", async () => {
|
|
636
683
|
const schedule = await createScheduledTask({
|
|
637
684
|
name: `dispatch-needs-timezone-${crypto.randomUUID()}`,
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
import { Database } from "bun:sqlite";
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, test } from "bun:test";
|
|
3
|
+
import { type AutomationSetupStates, preflightAutomation } from "../be/automation-preflight";
|
|
4
|
+
import { runMigrations } from "../be/migrations/runner";
|
|
5
|
+
import type { AutomationIntegrationId } from "../types";
|
|
6
|
+
|
|
7
|
+
const original = await Bun.file(
|
|
8
|
+
new URL("../be/migrations/141_scheduled_task_automation_preflight.sql", import.meta.url),
|
|
9
|
+
).text();
|
|
10
|
+
const repair = await Bun.file(
|
|
11
|
+
new URL("../be/migrations/151_repair_scheduled_task_required_params.sql", import.meta.url),
|
|
12
|
+
).text();
|
|
13
|
+
|
|
14
|
+
const canonicalNames = [
|
|
15
|
+
"daily-blocker-digest",
|
|
16
|
+
"daily-compounding-reflection",
|
|
17
|
+
"daily-status-report",
|
|
18
|
+
"daily-workflow-health-audit",
|
|
19
|
+
"weekly-harness-upgrade-check",
|
|
20
|
+
"weekly-dependabot-triage",
|
|
21
|
+
"weekly-code-health-reports",
|
|
22
|
+
"weekly-dora-metrics",
|
|
23
|
+
"daily-hn-briefing",
|
|
24
|
+
"gtm-weekly-review",
|
|
25
|
+
"dream-daily",
|
|
26
|
+
];
|
|
27
|
+
|
|
28
|
+
interface ScheduleRow {
|
|
29
|
+
name: string;
|
|
30
|
+
taskTemplate: string | null;
|
|
31
|
+
timezone: string;
|
|
32
|
+
enabled: number;
|
|
33
|
+
params: string;
|
|
34
|
+
requiredParams: string;
|
|
35
|
+
requires: string;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const setup: AutomationSetupStates = {
|
|
39
|
+
slack: "verified",
|
|
40
|
+
github: "verified",
|
|
41
|
+
linear: "verified",
|
|
42
|
+
jira: "verified",
|
|
43
|
+
gsc: "verified",
|
|
44
|
+
agentmail: "verified",
|
|
45
|
+
agentfs: "verified",
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
function preflight(row: ScheduleRow) {
|
|
49
|
+
return preflightAutomation(
|
|
50
|
+
{
|
|
51
|
+
id: row.name,
|
|
52
|
+
name: row.name,
|
|
53
|
+
kind: "schedule",
|
|
54
|
+
params: JSON.parse(row.params),
|
|
55
|
+
requiredParams: JSON.parse(row.requiredParams),
|
|
56
|
+
requires: JSON.parse(row.requires) as AutomationIntegrationId[],
|
|
57
|
+
},
|
|
58
|
+
setup,
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
describe("scheduled task required params repair", () => {
|
|
63
|
+
let db: Database;
|
|
64
|
+
|
|
65
|
+
beforeEach(() => {
|
|
66
|
+
db = new Database(":memory:");
|
|
67
|
+
// Only the pre-141 columns consumed by the two data migrations.
|
|
68
|
+
db.exec(`CREATE TABLE scheduled_tasks (
|
|
69
|
+
name TEXT PRIMARY KEY, taskTemplate TEXT, timezone TEXT NOT NULL DEFAULT 'UTC',
|
|
70
|
+
enabled INTEGER NOT NULL DEFAULT 1
|
|
71
|
+
)`);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
afterEach(() => db.close());
|
|
75
|
+
|
|
76
|
+
function insert(name: string, taskTemplate: string | null) {
|
|
77
|
+
db.run("INSERT INTO scheduled_tasks (name, taskTemplate) VALUES (?, ?)", [name, taskTemplate]);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function rows() {
|
|
81
|
+
return db.query<ScheduleRow, []>("SELECT * FROM scheduled_tasks ORDER BY name").all();
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
test("repairs concretized legacy schedules after 141 and is idempotent", () => {
|
|
85
|
+
for (const name of canonicalNames) {
|
|
86
|
+
insert(name, "Run the configured report for example/project and send it to ops@example.org.");
|
|
87
|
+
}
|
|
88
|
+
db.exec(original);
|
|
89
|
+
const before = rows();
|
|
90
|
+
expect(before.filter((row) => preflight(row).state === "needs_setup")).toHaveLength(6);
|
|
91
|
+
expect(before.find((row) => row.name === "weekly-dependabot-triage")?.timezone).toBe(
|
|
92
|
+
"{{TIMEZONE}}",
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
db.exec(repair);
|
|
96
|
+
expect(rows()).toEqual(
|
|
97
|
+
before.map((row) => ({ ...row, requiredParams: "[]", timezone: "UTC" })),
|
|
98
|
+
);
|
|
99
|
+
for (const row of rows()) expect(preflight(row).state).toBe("running");
|
|
100
|
+
|
|
101
|
+
const repaired = rows();
|
|
102
|
+
db.exec(repair);
|
|
103
|
+
expect(rows()).toEqual(repaired);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
test("keeps every requirement on genuinely tokenized fresh templates", async () => {
|
|
107
|
+
for (const name of canonicalNames.filter((name) => name !== "dream-daily")) {
|
|
108
|
+
const template = new URL(`../../templates/schedules/${name}/content.md`, import.meta.url);
|
|
109
|
+
insert(name, await Bun.file(template).text());
|
|
110
|
+
}
|
|
111
|
+
db.exec(original);
|
|
112
|
+
const before = rows();
|
|
113
|
+
|
|
114
|
+
db.exec(repair);
|
|
115
|
+
expect(rows()).toEqual(before.map((row) => ({ ...row, timezone: "UTC" })));
|
|
116
|
+
expect(rows().map((row) => preflight(row).missing)).toEqual(
|
|
117
|
+
before.map((row) => preflight(row).missing),
|
|
118
|
+
);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
test("keeps referenced keys in order and checks exact case-sensitive tokens", () => {
|
|
122
|
+
insert(
|
|
123
|
+
"weekly-code-health-reports",
|
|
124
|
+
"Report {{PAGE_ID}} for {{REPO_URL}}; {{BRANCH_SUFFIX}}, {{scope_path}}, REPORT_NAME",
|
|
125
|
+
);
|
|
126
|
+
db.exec(original);
|
|
127
|
+
db.exec(repair);
|
|
128
|
+
expect(rows()[0]?.requiredParams).toBe('["REPO_URL","PAGE_ID"]');
|
|
129
|
+
});
|
|
130
|
+
|
|
131
|
+
test("keeps timezone-only requirements when TIMEZONE has a binding", () => {
|
|
132
|
+
insert("weekly-dependabot-triage", null);
|
|
133
|
+
db.exec(original);
|
|
134
|
+
db.run("UPDATE scheduled_tasks SET params = ?", ['{"TIMEZONE":"Europe/Paris"}']);
|
|
135
|
+
db.exec(repair);
|
|
136
|
+
expect(rows()[0]).toMatchObject({
|
|
137
|
+
taskTemplate: null,
|
|
138
|
+
timezone: "{{TIMEZONE}}",
|
|
139
|
+
requiredParams: '["TIMEZONE"]',
|
|
140
|
+
params: '{"TIMEZONE":"Europe/Paris"}',
|
|
141
|
+
requires: '["github","slack"]',
|
|
142
|
+
});
|
|
143
|
+
expect(preflight(rows()[0]!).state).toBe("running");
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
test("restores an unbound timezone and removes unused requirements with a null body", () => {
|
|
147
|
+
insert("weekly-dependabot-triage", null);
|
|
148
|
+
db.exec(original);
|
|
149
|
+
db.run("UPDATE scheduled_tasks SET params = ?", ['{"TIMEZONE":null}']);
|
|
150
|
+
db.exec(repair);
|
|
151
|
+
expect(rows()[0]).toMatchObject({ timezone: "UTC", requiredParams: "[]" });
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
test("leaves manual repairs and schedules outside 141 unchanged", () => {
|
|
155
|
+
insert("weekly-dependabot-triage", "Review {{REPO_URL}}.");
|
|
156
|
+
insert("custom-schedule", null);
|
|
157
|
+
db.exec(original);
|
|
158
|
+
db.exec(`UPDATE scheduled_tasks SET
|
|
159
|
+
timezone = 'America/New_York', requiredParams = '[ "REPO_URL" ]',
|
|
160
|
+
params = '{"REPO_URL":"example/project"}', enabled = 0
|
|
161
|
+
WHERE name = 'weekly-dependabot-triage'`);
|
|
162
|
+
db.exec(`UPDATE scheduled_tasks SET
|
|
163
|
+
timezone = '{{TIMEZONE}}', requiredParams = '["TIMEZONE","CUSTOM"]'
|
|
164
|
+
WHERE name = 'custom-schedule'`);
|
|
165
|
+
const before = rows();
|
|
166
|
+
db.exec(repair);
|
|
167
|
+
expect(rows()).toEqual(before);
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
test("the migration runner applies the repair on a fresh database", () => {
|
|
172
|
+
const db = new Database(":memory:");
|
|
173
|
+
try {
|
|
174
|
+
runMigrations(db);
|
|
175
|
+
const applied = db.query("SELECT * FROM _migrations WHERE version = 151").get();
|
|
176
|
+
expect(applied).toMatchObject({ name: "151_repair_scheduled_task_required_params" });
|
|
177
|
+
runMigrations(db);
|
|
178
|
+
expect(db.query("SELECT * FROM _migrations WHERE version = 151").get()).toEqual(applied);
|
|
179
|
+
} finally {
|
|
180
|
+
db.close();
|
|
181
|
+
}
|
|
182
|
+
});
|
|
@@ -399,12 +399,13 @@ describe("Scheduled Tasks Integration", () => {
|
|
|
399
399
|
|
|
400
400
|
// Find the created task
|
|
401
401
|
const createdTask = (await getDbClient().get(
|
|
402
|
-
"SELECT
|
|
402
|
+
"SELECT *, routing_reason AS routingReason FROM agent_tasks WHERE task = ? ORDER BY createdAt DESC LIMIT 1",
|
|
403
403
|
[schedule.taskTemplate!],
|
|
404
|
-
)) as { agentId: string; status: string };
|
|
404
|
+
)) as { agentId: string; routingReason: string; status: string };
|
|
405
405
|
|
|
406
406
|
expect(createdTask).toBeDefined();
|
|
407
407
|
expect(createdTask.agentId).toBe(targetAgent.id);
|
|
408
|
+
expect(createdTask.routingReason).toBe("human_pinned");
|
|
408
409
|
expect(createdTask.status).toBe("pending"); // Should be pending when assigned to agent
|
|
409
410
|
});
|
|
410
411
|
|
|
@@ -420,12 +421,13 @@ describe("Scheduled Tasks Integration", () => {
|
|
|
420
421
|
|
|
421
422
|
// Find the created task
|
|
422
423
|
const createdTask = (await getDbClient().get(
|
|
423
|
-
"SELECT
|
|
424
|
+
"SELECT *, routing_reason AS routingReason FROM agent_tasks WHERE task = ? ORDER BY createdAt DESC LIMIT 1",
|
|
424
425
|
[schedule.taskTemplate!],
|
|
425
|
-
)) as { agentId: string | null; status: string };
|
|
426
|
+
)) as { agentId: string | null; routingReason: string | null; status: string };
|
|
426
427
|
|
|
427
428
|
expect(createdTask).toBeDefined();
|
|
428
429
|
expect(createdTask.agentId).toBeNull();
|
|
430
|
+
expect(createdTask.routingReason).toBeNull();
|
|
429
431
|
expect(createdTask.status).toBe("unassigned");
|
|
430
432
|
});
|
|
431
433
|
|
|
@@ -529,7 +531,7 @@ describe("Scheduled Tasks Integration", () => {
|
|
|
529
531
|
|
|
530
532
|
// Find the created task
|
|
531
533
|
const createdTask = (await getDbClient().get(
|
|
532
|
-
"SELECT
|
|
534
|
+
"SELECT *, routing_reason AS routingReason FROM agent_tasks WHERE task = ? ORDER BY createdAt DESC LIMIT 1",
|
|
533
535
|
[schedule.taskTemplate!],
|
|
534
536
|
)) as {
|
|
535
537
|
task: string;
|
|
@@ -537,6 +539,7 @@ describe("Scheduled Tasks Integration", () => {
|
|
|
537
539
|
tags: string;
|
|
538
540
|
priority: number;
|
|
539
541
|
agentId: string;
|
|
542
|
+
routingReason: string;
|
|
540
543
|
creatorAgentId: string;
|
|
541
544
|
};
|
|
542
545
|
|
|
@@ -544,6 +547,7 @@ describe("Scheduled Tasks Integration", () => {
|
|
|
544
547
|
expect(createdTask.taskType).toBe("comprehensive");
|
|
545
548
|
expect(createdTask.priority).toBe(90);
|
|
546
549
|
expect(createdTask.agentId).toBe(testAgent.id);
|
|
550
|
+
expect(createdTask.routingReason).toBe("human_pinned");
|
|
547
551
|
expect(createdTask.creatorAgentId).toBe(testAgent.id);
|
|
548
552
|
|
|
549
553
|
const tags = JSON.parse(createdTask.tags);
|
|
@@ -566,6 +566,10 @@ describe("script connections", () => {
|
|
|
566
566
|
markMigrationApplied(database, "147_scheduled_tasks_parent_task.sql");
|
|
567
567
|
// 148 rebuilds pages, which this migration-112-only fixture does not create.
|
|
568
568
|
markMigrationApplied(database, "148_pages_svg.sql");
|
|
569
|
+
// 151 repairs scheduled_tasks, which this migration-112-only fixture does not create.
|
|
570
|
+
markMigrationApplied(database, "151_repair_scheduled_task_required_params.sql");
|
|
571
|
+
// 152 indexes task routing context absent from this migration-112-only fixture.
|
|
572
|
+
markMigrationApplied(database, "152_routing_decisions.sql");
|
|
569
573
|
|
|
570
574
|
const id = crypto.randomUUID();
|
|
571
575
|
const now = new Date().toISOString();
|
|
@@ -401,6 +401,37 @@ describe("/api/script-runs HTTP", () => {
|
|
|
401
401
|
expect(stepCount.c).toBe(1);
|
|
402
402
|
});
|
|
403
403
|
|
|
404
|
+
test("agent-task dispatch preserves existing author pins and records human_pinned", async () => {
|
|
405
|
+
const created = await dispatch("/api/script-runs", {
|
|
406
|
+
method: "POST",
|
|
407
|
+
agentId,
|
|
408
|
+
body: createBody(),
|
|
409
|
+
});
|
|
410
|
+
const { id: runId } = (await created.json()) as { id: string };
|
|
411
|
+
|
|
412
|
+
const responsePromise = dispatch(`/api/internal/script-runs/${runId}/agent-task`, {
|
|
413
|
+
method: "POST",
|
|
414
|
+
agentId,
|
|
415
|
+
body: JSON.stringify({ stepKey: "pinned", task: "do pinned work", agentId }),
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
let dispatched = await getLatestScriptRunStepTaskByContextKey(`script-run:${runId}:pinned`);
|
|
419
|
+
for (let i = 0; i < 50 && !dispatched; i++) {
|
|
420
|
+
await Bun.sleep(20);
|
|
421
|
+
dispatched = await getLatestScriptRunStepTaskByContextKey(`script-run:${runId}:pinned`);
|
|
422
|
+
}
|
|
423
|
+
expect(dispatched).not.toBeNull();
|
|
424
|
+
await completeTask(dispatched!.id, "done");
|
|
425
|
+
const response = await responsePromise;
|
|
426
|
+
expect(response.status).toBe(200);
|
|
427
|
+
const { taskId } = (await response.json()) as { taskId: string };
|
|
428
|
+
expect(
|
|
429
|
+
await getDbClient().get("SELECT agentId, routing_reason FROM agent_tasks WHERE id = ?", [
|
|
430
|
+
taskId,
|
|
431
|
+
]),
|
|
432
|
+
).toEqual({ agentId, routing_reason: "human_pinned" });
|
|
433
|
+
});
|
|
434
|
+
|
|
404
435
|
test("agent-task polling returns the step output when a newer completed follow-up shares its context key", async () => {
|
|
405
436
|
const created = await dispatch("/api/script-runs", {
|
|
406
437
|
method: "POST",
|
|
@@ -47,6 +47,17 @@ afterAll(async () => {
|
|
|
47
47
|
});
|
|
48
48
|
|
|
49
49
|
describe("typecheckScript", () => {
|
|
50
|
+
test("task_defer accepts task wakeOn with a required caller-supplied ceiling", async () => {
|
|
51
|
+
const result = await typecheckScript(`
|
|
52
|
+
import type { ScriptContext } from "swarm-sdk";
|
|
53
|
+
export default async function(args: unknown, ctx: ScriptContext) {
|
|
54
|
+
return ctx.swarm.task_defer({ taskId: "parent", delayMs: 60000,
|
|
55
|
+
wakeOn: { event: "settled", taskId: "producer" }, summary: "submitted", note: "check result" });
|
|
56
|
+
}
|
|
57
|
+
`);
|
|
58
|
+
expect(result).toEqual({ ok: true });
|
|
59
|
+
});
|
|
60
|
+
|
|
50
61
|
test("accepts ES2022 globals: JSON, Math, Date, Number, String, Error, isFinite, encodeURIComponent, parseInt, parseFloat", async () => {
|
|
51
62
|
const source = `
|
|
52
63
|
export default async () => {
|
|
@@ -28,6 +28,18 @@ afterEach(() => {
|
|
|
28
28
|
});
|
|
29
29
|
|
|
30
30
|
describe("scrubSecrets — edge cases", () => {
|
|
31
|
+
test("redacts ACP session tokens in plain text and escaped session logs", () => {
|
|
32
|
+
const token = `aseph_${"aB3".repeat(11)}`;
|
|
33
|
+
const github = `ghp_${"gH4".repeat(12)}`;
|
|
34
|
+
for (const prefix of ["Bearer ", "\\n", "\\t", '"']) {
|
|
35
|
+
const input = `${prefix}${token} tail ${github}`;
|
|
36
|
+
const output = scrubSecrets(input);
|
|
37
|
+
expect(output).not.toContain(github);
|
|
38
|
+
expect(output).toBe(`${prefix}[REDACTED:acp_session_token] tail [REDACTED:github_token]`);
|
|
39
|
+
expect(scrubObject({ output: input })).toEqual({ output });
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
31
43
|
test("empty string passes through", () => {
|
|
32
44
|
expect(scrubSecrets("")).toBe("");
|
|
33
45
|
});
|
|
@@ -2,7 +2,14 @@ import { afterAll, beforeAll, describe, expect, test } from "bun:test";
|
|
|
2
2
|
import { unlink } from "node:fs/promises";
|
|
3
3
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4
4
|
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
|
5
|
-
import {
|
|
5
|
+
import {
|
|
6
|
+
closeDb,
|
|
7
|
+
createAgent,
|
|
8
|
+
createTaskExtended,
|
|
9
|
+
createUser,
|
|
10
|
+
getTaskById,
|
|
11
|
+
initDb,
|
|
12
|
+
} from "../be/db";
|
|
6
13
|
import { createUserServer, userSendTaskInputSchema } from "../server-user";
|
|
7
14
|
import { registerSendTaskTool, sendTaskInputSchema } from "../tools/send-task";
|
|
8
15
|
|
|
@@ -160,6 +167,7 @@ describe("send-task: outputSchema propagation", () => {
|
|
|
160
167
|
expect(s.success).toBe(true);
|
|
161
168
|
const created = await getTaskById(s.task!.id);
|
|
162
169
|
expect(created?.outputSchema).toEqual(schema);
|
|
170
|
+
expect(created?.routingReason).toBeUndefined();
|
|
163
171
|
});
|
|
164
172
|
|
|
165
173
|
test("a provided outputSchema is persisted when the task is offered to an agent", async () => {
|
|
@@ -175,6 +183,7 @@ describe("send-task: outputSchema propagation", () => {
|
|
|
175
183
|
{
|
|
176
184
|
task: "offered task with a schema",
|
|
177
185
|
agentId: worker.id,
|
|
186
|
+
routingReason: "human_pinned",
|
|
178
187
|
offerMode: true,
|
|
179
188
|
outputSchema: schema,
|
|
180
189
|
allowDuplicate: true,
|
|
@@ -204,6 +213,7 @@ describe("send-task: outputSchema propagation", () => {
|
|
|
204
213
|
{
|
|
205
214
|
task: "directly assigned task with a schema",
|
|
206
215
|
agentId: worker.id,
|
|
216
|
+
routingReason: "human_pinned",
|
|
207
217
|
outputSchema: schema,
|
|
208
218
|
allowDuplicate: true,
|
|
209
219
|
},
|
|
@@ -213,6 +223,61 @@ describe("send-task: outputSchema propagation", () => {
|
|
|
213
223
|
expect(s.success).toBe(true);
|
|
214
224
|
const created = await getTaskById(s.task!.id);
|
|
215
225
|
expect(created?.outputSchema).toEqual(schema);
|
|
226
|
+
expect(created?.routingReason).toBe("human_pinned");
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
test("implicit parent routing stamps continuity while pool creation omits a reason", async () => {
|
|
230
|
+
const worker = await createAgent({
|
|
231
|
+
name: "Continuity worker",
|
|
232
|
+
isLead: false,
|
|
233
|
+
status: "idle",
|
|
234
|
+
maxTasks: 2,
|
|
235
|
+
});
|
|
236
|
+
const parent = await createTaskExtended("continuity parent", {
|
|
237
|
+
agentId: worker.id,
|
|
238
|
+
routingReason: "human_pinned",
|
|
239
|
+
});
|
|
240
|
+
const continued = await callSendTask(
|
|
241
|
+
server,
|
|
242
|
+
{
|
|
243
|
+
task: "continue implicitly",
|
|
244
|
+
parentTaskId: parent.id,
|
|
245
|
+
allowDuplicate: true,
|
|
246
|
+
},
|
|
247
|
+
LEAD_ID,
|
|
248
|
+
);
|
|
249
|
+
const continuedTask = await getTaskById(structuredOf(continued).task!.id);
|
|
250
|
+
expect(continuedTask?.agentId).toBe(worker.id);
|
|
251
|
+
expect(continuedTask?.routingReason).toBe("continuity");
|
|
252
|
+
|
|
253
|
+
const pooled = await callSendTask(
|
|
254
|
+
server,
|
|
255
|
+
{ task: "stay pooled", allowDuplicate: true },
|
|
256
|
+
LEAD_ID,
|
|
257
|
+
);
|
|
258
|
+
expect((await getTaskById(structuredOf(pooled).task!.id))?.routingReason).toBeUndefined();
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
test("direct routing note is persisted", async () => {
|
|
262
|
+
const worker = await createAgent({
|
|
263
|
+
name: "Noted worker",
|
|
264
|
+
isLead: false,
|
|
265
|
+
status: "idle",
|
|
266
|
+
maxTasks: 1,
|
|
267
|
+
});
|
|
268
|
+
const result = await callSendTask(
|
|
269
|
+
server,
|
|
270
|
+
{
|
|
271
|
+
task: "direct task with routing note",
|
|
272
|
+
agentId: worker.id,
|
|
273
|
+
routingReason: "skill",
|
|
274
|
+
routingNote: "matches the required runtime specialization",
|
|
275
|
+
allowDuplicate: true,
|
|
276
|
+
},
|
|
277
|
+
LEAD_ID,
|
|
278
|
+
);
|
|
279
|
+
const created = await getTaskById(structuredOf(result).task!.id);
|
|
280
|
+
expect(created?.routingNote).toBe("matches the required runtime specialization");
|
|
216
281
|
});
|
|
217
282
|
});
|
|
218
283
|
|
|
@@ -275,6 +275,7 @@ describe("send-task: Slack-routing coherence guard", () => {
|
|
|
275
275
|
{
|
|
276
276
|
task: "direct assign with wrong channel",
|
|
277
277
|
agentId: WORKER_ID,
|
|
278
|
+
routingReason: "human_pinned",
|
|
278
279
|
slackChannelId: "C_WRONG_DIRECT",
|
|
279
280
|
slackThreadTs: "555.555",
|
|
280
281
|
allowDuplicate: true,
|
|
@@ -299,6 +300,7 @@ describe("send-task: Slack-routing coherence guard", () => {
|
|
|
299
300
|
{
|
|
300
301
|
task: "offer with wrong channel",
|
|
301
302
|
agentId: WORKER_ID,
|
|
303
|
+
routingReason: "human_pinned",
|
|
302
304
|
offerMode: true,
|
|
303
305
|
slackChannelId: "C_WRONG_OFFER",
|
|
304
306
|
slackThreadTs: "666.666",
|