@desplega.ai/agent-swarm 1.113.0 → 1.115.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 +2 -2
- package/dist/{actions-q4n7cz6e.js → actions-17ya5wde.js} +8 -9
- package/dist/{app-es4nzc71.js → app-4f718yk0.js} +6 -6
- package/dist/{assistant-0x8ey0vs.js → assistant-ah7csnjz.js} +19 -18
- package/dist/{boot-reembed-69agkhv6.js → boot-reembed-4jpxhb25.js} +6 -6
- package/dist/{boot-reembed-p0ny05t8.js → boot-reembed-qata34xa.js} +5 -5
- package/dist/{boot-scrub-logs-yybrkmvr.js → boot-scrub-logs-tv48nn3z.js} +4 -4
- package/dist/{claude-adapter-mma9a1yr.js → claude-adapter-3csn5bks.js} +3 -3
- package/dist/{claude-managed-adapter-pv2nbqy4.js → claude-managed-adapter-kysmkz89.js} +2 -2
- package/dist/cli-0pygq9r1.js +429 -0
- package/dist/{cli-hsetkkd3.js → cli-12vb0xg6.js} +33 -9
- package/dist/{cli-ncfgsqbd.js → cli-15y7qhzp.js} +262 -33
- package/dist/{cli-7ga43spr.js → cli-1yjrbg1f.js} +1 -1
- package/dist/{cli-36ymmpmp.js → cli-3samkyap.js} +2 -2
- package/dist/cli-52fg7s7x.js +269 -0
- package/dist/{cli-hjhvekf4.js → cli-65ekje01.js} +11 -8
- package/dist/{cli-h3k622d0.js → cli-6aq8xhvm.js} +90 -3
- package/dist/{cli-f3qa069v.js → cli-7xje5amz.js} +1 -1
- package/dist/cli-86fdnerz.js +405 -0
- package/dist/{cli-9cy6nc5m.js → cli-8fjwym53.js} +1 -1
- package/dist/{cli-c3frqf65.js → cli-b23axgz4.js} +3 -3
- package/dist/{cli-a0t7vapf.js → cli-bc89my0z.js} +1 -1
- package/dist/{cli-bnzbn8j8.js → cli-beea4a3y.js} +73 -24
- package/dist/{cli-q21d49ac.js → cli-bw80ck94.js} +1 -1
- package/dist/{cli-pwc5e83c.js → cli-cchksf5q.js} +12 -7
- package/dist/{cli-30bbaveh.js → cli-cmw9rzy5.js} +2 -2
- package/dist/{cli-y6mrptcn.js → cli-cnwssmsr.js} +1 -1
- package/dist/{cli-r5g6ngtn.js → cli-dcca6j8m.js} +46 -5
- package/dist/{cli-tkmm5nf9.js → cli-e61fs2hp.js} +59 -2
- package/dist/{cli-6wscj9z0.js → cli-e7306swv.js} +14 -14
- package/dist/{cli-dcfyyh3t.js → cli-ftyf58xj.js} +2 -2
- package/dist/{cli-4xyj9jtq.js → cli-hrhye2rm.js} +1062 -478
- package/dist/cli-jkhwmmgg.js +136 -0
- package/dist/{cli-y4ycrnjc.js → cli-k3y4z3h5.js} +1 -1
- package/dist/{cli-6xd0bvf8.js → cli-ma04faw5.js} +1 -1
- package/dist/{cli-2fc97xm5.js → cli-mnbgv3w7.js} +4 -4
- package/dist/{cli-trmapn9k.js → cli-pet3aev6.js} +95 -10
- package/dist/{cli-z46pnksz.js → cli-q1rx7g0d.js} +175 -28
- package/dist/{cli-qwgtacd6.js → cli-r9f6ngrf.js} +1244 -336
- package/dist/{cli-gtkqc144.js → cli-shvaxw27.js} +13 -7
- package/dist/{cli-ywez6yps.js → cli-sq7f72d6.js} +1 -1
- package/dist/cli-t0vqevnm.js +393 -0
- package/dist/{cli-tbw13sx8.js → cli-tece6zgm.js} +1 -1
- package/dist/{cli-bgvskydy.js → cli-tffvrh6j.js} +1 -1
- package/dist/cli-wh91zby6.js +187222 -0
- package/dist/{cli-anrj584m.js → cli-xz9aq0rf.js} +1 -1
- package/dist/{cli-mq580e06.js → cli-zsrdty74.js} +18 -18
- package/dist/cli.js +32 -15
- package/dist/{codex-adapter-5cmy858w.js → codex-adapter-de3w0nch.js} +10 -10
- package/dist/{codex-session-runner-46q8mrba.js → codex-session-runner-bnw8cysk.js} +10 -10
- package/dist/{commands-z5dbwta3.js → commands-hch4e2k1.js} +4 -4
- package/dist/{db-bbgahh4z.js → db-p3h2j06x.js} +14 -4
- package/dist/{handlers-6b44317z.js → handlers-9er4zx9q.js} +17 -31
- package/dist/{hook-sbp5fmps.js → hook-4axs756z.js} +7 -7
- package/dist/{http-2xz43gz3.js → http-809fvtz2.js} +1934 -886
- package/dist/{index-wz1ea15p.js → index-2dgfj9av.js} +3 -3
- package/dist/{index-nhat35em.js → index-47em3d2n.js} +14 -11
- package/dist/{index-5ghxn8s6.js → index-9fqaktjr.js} +12 -9
- package/dist/{index-0gzmqj4k.js → index-9ht8gr56.js} +13 -10
- package/dist/{index-vfedgdw6.js → index-rm85cfdx.js} +368 -40
- package/dist/{index-ykvs43z4.js → index-t2snazez.js} +4 -4
- package/dist/{keepalive-62gfj91d.js → keepalive-3cps6e3e.js} +7 -6
- package/dist/{lead-ag0akn0v.js → lead-fkk95wgq.js} +36 -32
- package/dist/{maintenance-xxrrzk5x.js → maintenance-fweqh0yj.js} +6 -6
- package/dist/{mistral-conversations-26yrnbf1.js → mistral-conversations-7gqnz2wt.js} +7 -7
- package/dist/oauth-refresh-sweep-sycqtbph.js +114 -0
- package/dist/{onboard-64zyfcbs.js → onboard-cy0rkgvx.js} +2 -2
- package/dist/{opencode-adapter-mqsxr170.js → opencode-adapter-p6bxnbm4.js} +2 -2
- package/dist/{otel-zjd9zaxs.js → otel-g299k41b.js} +2 -2
- package/dist/{otel-impl-b86qseaa.js → otel-impl-wbns8hf7.js} +1 -1
- package/dist/{pi-mono-adapter-kxjxpfgy.js → pi-mono-adapter-05sck1ds.js} +17 -109
- package/dist/{pricing-refresh-cgtkffpa.js → pricing-refresh-0hvazmkf.js} +6 -6
- package/dist/rbac-roles-kfesjy9y.js +28 -0
- package/dist/rbac-roles-mwcwhbxc.js +283 -0
- package/dist/{seed-pricing-1r1y1xkq.js → seed-pricing-h2kdq4cq.js} +5 -5
- package/dist/{setup-mkcgkjf7.js → setup-8506q97w.js} +2 -2
- package/dist/{worker-351sze63.js → worker-4r5c6zyb.js} +36 -32
- package/openapi.json +1327 -20
- package/package.json +3 -3
- package/src/agentmail/handlers.ts +16 -6
- package/src/agentmail/templates.ts +25 -5
- package/src/be/audit-user.ts +28 -4
- package/src/be/db-queries/oauth.ts +42 -0
- package/src/be/db.ts +314 -28
- package/src/be/identity.ts +79 -0
- package/src/be/mcp-proxy.ts +132 -0
- package/src/be/migrations/109_rbac_roles.sql +79 -0
- package/src/be/migrations/111_oauth_credential_bindings.sql +6 -0
- package/src/be/migrations/112_script_connections_graphql.sql +96 -0
- package/src/be/migrations/113_task_routing_affinity.sql +9 -0
- package/src/be/migrations/114_backfill_gpt_5_6_pricing.sql +15 -0
- package/src/be/modelsdev-cache.json +135 -0
- package/src/be/oauth-credential-bindings.ts +35 -0
- package/src/be/oauth-refresh-sweep.ts +159 -0
- package/src/be/rbac-audit.ts +52 -13
- package/src/be/rbac-roles.ts +424 -0
- package/src/be/script-connections.ts +459 -40
- package/src/be/script-credential-broker.ts +29 -3
- package/src/be/scripts/typecheck.ts +51 -7
- package/src/be/seed-scripts/catalog/catalog-report.inline.ts +325 -24
- package/src/be/seed-scripts/catalog/catalog-report.ts +325 -24
- package/src/be/users.ts +24 -0
- package/src/cli.tsx +17 -0
- package/src/github/handlers.ts +10 -9
- package/src/github/templates.ts +45 -9
- package/src/gitlab/handlers.ts +20 -8
- package/src/gitlab/templates.ts +18 -6
- package/src/heartbeat/heartbeat.ts +143 -21
- package/src/http/all-routes.ts +3 -0
- package/src/http/approval-requests.ts +12 -0
- package/src/http/config.ts +44 -10
- package/src/http/core.ts +30 -3
- package/src/http/favorites.ts +1 -5
- package/src/http/index.ts +25 -1
- package/src/http/mcp-oauth.ts +2 -0
- package/src/http/mcp-servers.ts +44 -3
- package/src/http/oauth-generic.ts +105 -0
- package/src/http/poll.ts +20 -3
- package/src/http/script-connection-proxy.ts +116 -0
- package/src/http/script-connections.ts +1657 -0
- package/src/http/scripts.ts +27 -7
- package/src/http/skills.ts +11 -0
- package/src/http/tasks.ts +27 -13
- package/src/http/x.ts +6 -2
- package/src/integrations/kapso/inbound.ts +23 -6
- package/src/jira/sync.ts +111 -17
- package/src/jira/templates.ts +10 -2
- package/src/mcp-client/http-client.ts +194 -0
- package/src/oauth/app-validation.ts +16 -0
- package/src/oauth/ensure-token.ts +69 -14
- package/src/oauth/mcp-wrapper.ts +15 -0
- package/src/oauth/wrapper.ts +48 -20
- package/src/prompts/session-templates.ts +3 -3
- package/src/providers/codex-models.ts +100 -30
- package/src/providers/modelsdev-reasoning.json +51 -0
- package/src/providers/pi-mono-mcp-client.ts +2 -133
- package/src/providers/reasoning-effort.ts +11 -5
- package/src/rbac/admission.ts +68 -0
- package/src/rbac/index.ts +2 -0
- package/src/rbac/legacy-policy.ts +12 -0
- package/src/rbac/permissions.ts +48 -0
- package/src/scheduler/scheduler.ts +9 -1
- package/src/scripts-runtime/api-client.ts +151 -19
- package/src/scripts-runtime/api-types.ts +38 -6
- package/src/scripts-runtime/credential-broker/default-bindings.ts +1 -0
- package/src/scripts-runtime/credential-broker/types.ts +8 -0
- package/src/scripts-runtime/ctx.ts +11 -1
- package/src/scripts-runtime/eval-harness.ts +5 -1
- package/src/scripts-runtime/executors/types.ts +2 -1
- package/src/scripts-runtime/loader.ts +3 -1
- package/src/scripts-runtime/mcp-client.ts +87 -0
- package/src/scripts-runtime/sdk-allowlist.ts +1 -0
- package/src/scripts-runtime/types/stdlib.d.ts +6 -0
- package/src/scripts-runtime/types/swarm-sdk.d.ts +19 -0
- package/src/server-user.ts +114 -59
- package/src/server.ts +12 -0
- package/src/slack/assistant.ts +14 -6
- package/src/slack/enrich.ts +17 -0
- package/src/slack/handlers.ts +8 -22
- package/src/slack/responses.ts +91 -1
- package/src/slack/thread-buffer.ts +6 -3
- package/src/slack/watcher.ts +9 -6
- package/src/tasks/worker-follow-up.ts +100 -3
- package/src/tests/agentmail-handlers.test.ts +32 -3
- package/src/tests/approval-requests.test.ts +61 -0
- package/src/tests/audit-user.test.ts +47 -1
- package/src/tests/codex-adapter.test.ts +46 -23
- package/src/tests/credential-broker.test.ts +5 -2
- package/src/tests/delete-page-tool.test.ts +197 -0
- package/src/tests/github-handlers.test.ts +28 -2
- package/src/tests/gitlab-handlers.test.ts +125 -0
- package/src/tests/heartbeat.test.ts +48 -3
- package/src/tests/identity.test.ts +117 -0
- package/src/tests/jira-sync.test.ts +99 -1
- package/src/tests/kapso-inbound.test.ts +7 -0
- package/src/tests/mcp-tools-user.test.ts +60 -6
- package/src/tests/model-control.test.ts +8 -2
- package/src/tests/oauth-credential-bindings.test.ts +490 -0
- package/src/tests/oauth-refresh-sweep.test.ts +160 -0
- package/src/tests/pool-affinity.test.ts +401 -0
- package/src/tests/prompt-template-remaining.test.ts +2 -2
- package/src/tests/rbac-admission-e2e.test.ts +386 -0
- package/src/tests/rbac-admission.test.ts +492 -0
- package/src/tests/rbac-e2e-helpers.ts +3 -0
- package/src/tests/rbac-engine.test.ts +50 -0
- package/src/tests/rbac-mcp-admission-e2e.test.ts +362 -0
- package/src/tests/rbac-roles.test.ts +328 -0
- package/src/tests/reasoning-effort.test.ts +15 -2
- package/src/tests/schedule-target-type.test.ts +50 -0
- package/src/tests/scheduled-tasks.test.ts +40 -0
- package/src/tests/script-apis-mcp.test.ts +37 -13
- package/src/tests/script-connections-http.test.ts +1158 -0
- package/src/tests/script-connections-mcp.test.ts +894 -0
- package/src/tests/script-connections.test.ts +754 -6
- package/src/tests/scripts-external-api.test.ts +49 -1
- package/src/tests/scripts-runtime-secret-egress.test.ts +1 -0
- package/src/tests/scripts-runtime.test.ts +291 -0
- package/src/tests/seed-scripts.test.ts +36 -0
- package/src/tests/slack-identity-resolution.test.ts +37 -3
- package/src/tests/slack-inline-output.test.ts +65 -0
- package/src/tests/slack-thread-buffer.test.ts +28 -0
- package/src/tests/tool-annotations.test.ts +1 -0
- package/src/tests/user-token-rest-auth.test.ts +140 -1
- package/src/tests/workflow-swarm-script.test.ts +44 -0
- package/src/tests/workflow-triggers-v2.test.ts +300 -1
- package/src/tools/credential-bindings/tool.ts +277 -18
- package/src/tools/delete-page.ts +145 -0
- package/src/tools/mcp-servers/mcp-server-delete.ts +7 -4
- package/src/tools/request-human-input.ts +2 -0
- package/src/tools/resolve-user.ts +77 -27
- package/src/tools/script-apis.ts +62 -3
- package/src/tools/script-connections/tool.ts +275 -31
- package/src/tools/send-task.ts +15 -1
- package/src/tools/task-action.ts +34 -2
- package/src/tools/templates.ts +53 -1
- package/src/tools/tool-config.ts +2 -1
- package/src/tools/utils.ts +2 -0
- package/src/tools/workflows/create-workflow.ts +8 -1
- package/src/tools/workflows/update-workflow.ts +11 -2
- package/src/types.ts +90 -11
- package/src/utils/scoped-resource.ts +25 -0
- package/src/workflows/executors/human-in-the-loop.ts +1 -0
- package/src/workflows/executors/swarm-script.ts +7 -1
- package/src/workflows/triggers.ts +146 -18
- package/templates/skills/swarm-scripts/content.md +33 -8
- package/dist/cli-bgef578c.js +0 -771
- package/dist/cli-c9mtm09b.js +0 -43
- package/dist/cli-ceewqajw.js +0 -1698
- package/dist/{anthropic-messages-8nc6r2dw.js → anthropic-messages-kq3pjhbp.js} +6 -6
- package/dist/{azure-openai-responses-6yfzw3qw.js → azure-openai-responses-gm1ejtmn.js} +7 -7
- package/dist/{cli-vbraamxr.js → cli-6b4wwmm8.js} +9 -9
- package/dist/{cli-2r7s8jhp.js → cli-sznberje.js} +3 -3
- package/dist/{google-generative-ai-vfq6ymz9.js → google-generative-ai-hyrra2jm.js} +3 -3
- package/dist/{google-vertex-mj5y5rfj.js → google-vertex-8bs2pzgn.js} +3 -3
- package/dist/{openai-codex-responses-8h6kvfv8.js → openai-codex-responses-3nkvje79.js} +4 -4
- package/dist/{openai-completions-x6kjnqjd.js → openai-completions-0asftkwg.js} +11 -11
- package/dist/{openai-responses-7jwyz3ds.js → openai-responses-pmatek45.js} +10 -10
package/src/server-user.ts
CHANGED
|
@@ -1,6 +1,15 @@
|
|
|
1
1
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
2
|
+
import type { CallToolResult, ToolAnnotations } from "@modelcontextprotocol/sdk/types.js";
|
|
2
3
|
import * as z from "zod";
|
|
3
4
|
import pkg from "../package.json";
|
|
5
|
+
import { enqueueAdmissionRow } from "./be/rbac-audit";
|
|
6
|
+
import { getUserGrant } from "./be/rbac-roles";
|
|
7
|
+
import {
|
|
8
|
+
type AdmissionRbac,
|
|
9
|
+
decideToolAdmission,
|
|
10
|
+
isRbacEnabled,
|
|
11
|
+
type PermissionVerb,
|
|
12
|
+
} from "./rbac";
|
|
4
13
|
import {
|
|
5
14
|
cancelTaskHandler,
|
|
6
15
|
cancelTaskInputSchema,
|
|
@@ -22,6 +31,11 @@ import { userCtx } from "./tools/task-tool-ctx";
|
|
|
22
31
|
import { createToolRegistrar } from "./tools/utils";
|
|
23
32
|
import { ModelTierSchema, type User } from "./types";
|
|
24
33
|
|
|
34
|
+
type UserToolAdmissionConfig = {
|
|
35
|
+
annotations?: ToolAnnotations;
|
|
36
|
+
rbac?: AdmissionRbac;
|
|
37
|
+
};
|
|
38
|
+
|
|
25
39
|
const userSendTaskInputSchema = z.object({
|
|
26
40
|
task: z.string().min(1).describe("The task description to send."),
|
|
27
41
|
taskType: z.string().max(50).optional().describe("Task type (e.g., 'bug', 'feature', 'review')."),
|
|
@@ -38,6 +52,40 @@ const userSendTaskInputSchema = z.object({
|
|
|
38
52
|
),
|
|
39
53
|
});
|
|
40
54
|
|
|
55
|
+
function permission(verb: PermissionVerb): AdmissionRbac {
|
|
56
|
+
return { permission: verb };
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
async function maybeDenyUserToolAdmission(
|
|
60
|
+
user: User,
|
|
61
|
+
toolName: string,
|
|
62
|
+
config: UserToolAdmissionConfig,
|
|
63
|
+
): Promise<CallToolResult | undefined> {
|
|
64
|
+
if (!isRbacEnabled()) return undefined;
|
|
65
|
+
|
|
66
|
+
const grant = getUserGrant(user.id);
|
|
67
|
+
if (grant.grantsAll) return undefined;
|
|
68
|
+
|
|
69
|
+
const decision = decideToolAdmission({
|
|
70
|
+
rbac: config.rbac,
|
|
71
|
+
readOnly: config.annotations?.readOnlyHint ?? false,
|
|
72
|
+
grant,
|
|
73
|
+
});
|
|
74
|
+
enqueueAdmissionRow({
|
|
75
|
+
userId: user.id,
|
|
76
|
+
decision,
|
|
77
|
+
source: "mcp",
|
|
78
|
+
toolName,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
if (decision.allow) return undefined;
|
|
82
|
+
|
|
83
|
+
return {
|
|
84
|
+
isError: true,
|
|
85
|
+
content: [{ type: "text", text: `Forbidden: ${decision.reason}` }],
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
|
|
41
89
|
export function createUserServer(user: User): McpServer {
|
|
42
90
|
const server = new McpServer(
|
|
43
91
|
{
|
|
@@ -54,69 +102,76 @@ export function createUserServer(user: User): McpServer {
|
|
|
54
102
|
|
|
55
103
|
const registerTool = createToolRegistrar(server);
|
|
56
104
|
|
|
57
|
-
|
|
58
|
-
"
|
|
59
|
-
{
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
105
|
+
const sendTaskConfig = {
|
|
106
|
+
title: "Send a task",
|
|
107
|
+
annotations: { destructiveHint: false },
|
|
108
|
+
rbac: permission("task.create.own"),
|
|
109
|
+
description: "Creates an unassigned task requested by the authenticated user.",
|
|
110
|
+
inputSchema: userSendTaskInputSchema,
|
|
111
|
+
outputSchema: sendTaskOutputSchema,
|
|
112
|
+
};
|
|
113
|
+
registerTool("send-task", sendTaskConfig, async (args, info, _meta) => {
|
|
114
|
+
const denied = await maybeDenyUserToolAdmission(user, "send-task", sendTaskConfig);
|
|
115
|
+
if (denied) return denied;
|
|
116
|
+
return sendTaskHandler(userCtx(user, info.sessionId), {
|
|
117
|
+
offerMode: false,
|
|
118
|
+
allowDuplicate: false,
|
|
119
|
+
...args,
|
|
120
|
+
});
|
|
121
|
+
});
|
|
73
122
|
|
|
74
|
-
|
|
75
|
-
"
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
123
|
+
const getTasksConfig = {
|
|
124
|
+
title: "Get tasks",
|
|
125
|
+
description: "Returns tasks requested by the authenticated user.",
|
|
126
|
+
annotations: { readOnlyHint: true },
|
|
127
|
+
inputSchema: getTasksInputSchema,
|
|
128
|
+
outputSchema: getTasksOutputSchema,
|
|
129
|
+
};
|
|
130
|
+
registerTool("get-tasks", getTasksConfig, async (args, info, _meta) => {
|
|
131
|
+
const denied = await maybeDenyUserToolAdmission(user, "get-tasks", getTasksConfig);
|
|
132
|
+
if (denied) return denied;
|
|
133
|
+
return getTasksHandler(userCtx(user, info.sessionId), args);
|
|
134
|
+
});
|
|
85
135
|
|
|
86
|
-
|
|
87
|
-
"
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
136
|
+
const getTaskDetailsConfig = {
|
|
137
|
+
title: "Get task details",
|
|
138
|
+
description: "Returns detailed information about one of your tasks.",
|
|
139
|
+
annotations: { readOnlyHint: true },
|
|
140
|
+
inputSchema: getTaskDetailsInputSchema,
|
|
141
|
+
outputSchema: getTaskDetailsOutputSchema,
|
|
142
|
+
};
|
|
143
|
+
registerTool("get-task-details", getTaskDetailsConfig, async (args, info, _meta) => {
|
|
144
|
+
const denied = await maybeDenyUserToolAdmission(user, "get-task-details", getTaskDetailsConfig);
|
|
145
|
+
if (denied) return denied;
|
|
146
|
+
return getTaskDetailsHandler(userCtx(user, info.sessionId), args);
|
|
147
|
+
});
|
|
97
148
|
|
|
98
|
-
|
|
99
|
-
"
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
149
|
+
const cancelTaskConfig = {
|
|
150
|
+
title: "Cancel Task",
|
|
151
|
+
description: "Cancel one of your pending or in-progress tasks.",
|
|
152
|
+
annotations: { destructiveHint: true },
|
|
153
|
+
rbac: permission("task.cancel.own"),
|
|
154
|
+
inputSchema: cancelTaskInputSchema,
|
|
155
|
+
outputSchema: cancelTaskOutputSchema,
|
|
156
|
+
};
|
|
157
|
+
registerTool("cancel-task", cancelTaskConfig, async (args, info, _meta) => {
|
|
158
|
+
const denied = await maybeDenyUserToolAdmission(user, "cancel-task", cancelTaskConfig);
|
|
159
|
+
if (denied) return denied;
|
|
160
|
+
return cancelTaskHandler(userCtx(user, info.sessionId), args);
|
|
161
|
+
});
|
|
109
162
|
|
|
110
|
-
|
|
111
|
-
"
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
163
|
+
const taskActionConfig = {
|
|
164
|
+
title: "Task Pool Action",
|
|
165
|
+
description: "Move one of your tasks to or from backlog.",
|
|
166
|
+
rbac: permission("task.action.own"),
|
|
167
|
+
inputSchema: taskActionInputSchema,
|
|
168
|
+
outputSchema: taskActionOutputSchema,
|
|
169
|
+
};
|
|
170
|
+
registerTool("task-action", taskActionConfig, async (args, info, _meta) => {
|
|
171
|
+
const denied = await maybeDenyUserToolAdmission(user, "task-action", taskActionConfig);
|
|
172
|
+
if (denied) return denied;
|
|
173
|
+
return taskActionHandler(userCtx(user, info.sessionId), args);
|
|
174
|
+
});
|
|
120
175
|
|
|
121
176
|
return server;
|
|
122
177
|
}
|
package/src/server.ts
CHANGED
|
@@ -2,8 +2,10 @@ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
|
2
2
|
import pkg from "../package.json";
|
|
3
3
|
import { initDb } from "./be/db";
|
|
4
4
|
import { startPricingRefreshLoop } from "./be/pricing-refresh";
|
|
5
|
+
import { ensureRbacSeedsSynced } from "./be/rbac-roles";
|
|
5
6
|
import { seedPricingFromModelsDev } from "./be/seed-pricing";
|
|
6
7
|
import { registerGithubTaskReactions } from "./github/task-reactions";
|
|
8
|
+
import { isRbacEnabled } from "./rbac";
|
|
7
9
|
import { registerCancelTaskTool } from "./tools/cancel-task";
|
|
8
10
|
import { registerContextDiffTool } from "./tools/context-diff";
|
|
9
11
|
import { registerContextHistoryTool } from "./tools/context-history";
|
|
@@ -13,6 +15,7 @@ import { registerCreatePageTool } from "./tools/create-page";
|
|
|
13
15
|
import { registerCredentialBindingsTool } from "./tools/credential-bindings";
|
|
14
16
|
import { registerDbQueryTool } from "./tools/db-query";
|
|
15
17
|
import { registerDeleteChannelTool } from "./tools/delete-channel";
|
|
18
|
+
import { registerDeletePageTool } from "./tools/delete-page";
|
|
16
19
|
import { registerGetMetricsTool } from "./tools/get-metrics";
|
|
17
20
|
import { registerGetSwarmTool } from "./tools/get-swarm";
|
|
18
21
|
import { registerGetTaskDetailsTool } from "./tools/get-task-details";
|
|
@@ -182,6 +185,14 @@ export function createServer() {
|
|
|
182
185
|
// and the manual-override constants for runtime-fee / ACU pricing.
|
|
183
186
|
seedPricingFromModelsDev();
|
|
184
187
|
startPricingRefreshLoop();
|
|
188
|
+
try {
|
|
189
|
+
ensureRbacSeedsSynced();
|
|
190
|
+
} catch (err) {
|
|
191
|
+
console.error("[startup] Failed to sync RBAC seed rows:", err);
|
|
192
|
+
// RBAC flag-on must fail closed; flag-off deployments should not be bricked
|
|
193
|
+
// by role-catalog drift for a disabled security feature.
|
|
194
|
+
if (isRbacEnabled()) throw err;
|
|
195
|
+
}
|
|
185
196
|
|
|
186
197
|
// Subscribe API-side integrations to task-lifecycle events. Idempotent.
|
|
187
198
|
// (Inverts the old be/db → github/task-reactions import; see cycle-break #4.)
|
|
@@ -366,6 +377,7 @@ export function createServer() {
|
|
|
366
377
|
// `CAPABILITIES=...` env without `pages`.
|
|
367
378
|
if (hasCapability("pages")) {
|
|
368
379
|
registerCreatePageTool(server);
|
|
380
|
+
registerDeletePageTool(server);
|
|
369
381
|
}
|
|
370
382
|
|
|
371
383
|
if (hasCapability("metrics")) {
|
package/src/slack/assistant.ts
CHANGED
|
@@ -3,7 +3,7 @@ import { getAgentWorkingOnThread, getLeadAgent, getMostRecentTaskInThread } from
|
|
|
3
3
|
import { resolveTemplate } from "../prompts/resolver";
|
|
4
4
|
import { slackContextKey } from "../tasks/context-key";
|
|
5
5
|
import { createTaskWithSiblingAwareness } from "../tasks/sibling-awareness";
|
|
6
|
-
import { resolveSlackUserId } from "./enrich";
|
|
6
|
+
import { resolveSlackUserId, rewriteSlackMentions } from "./enrich";
|
|
7
7
|
import { wasEventSeen } from "./event-dedup";
|
|
8
8
|
import { hasOtherUserMention } from "./router";
|
|
9
9
|
import { bufferThreadMessage } from "./thread-buffer";
|
|
@@ -76,6 +76,11 @@ export function createAssistant(): Assistant {
|
|
|
76
76
|
const channelId = message.channel;
|
|
77
77
|
const messageText = (msg.text as string) || "";
|
|
78
78
|
const userId = (msg.user as string) || "";
|
|
79
|
+
// Any in-body `<@U…>` mention the requester typed is rewritten via
|
|
80
|
+
// the identity primitive before it reaches agent-visible task text —
|
|
81
|
+
// never a raw Slack ID. Bot-mention routing checks below use the
|
|
82
|
+
// raw `messageText`, not this rendered copy.
|
|
83
|
+
const renderedMessageText = rewriteSlackMentions(messageText);
|
|
79
84
|
|
|
80
85
|
// Resolve the bot's own Slack user ID (cached after first call) so we can
|
|
81
86
|
// check whether this message is actually addressed to us.
|
|
@@ -123,7 +128,7 @@ export function createAssistant(): Assistant {
|
|
|
123
128
|
|
|
124
129
|
// Otherwise, create a follow-up task for the working agent
|
|
125
130
|
const latestTask = getMostRecentTaskInThread(channelId, threadTs);
|
|
126
|
-
createTaskWithSiblingAwareness(
|
|
131
|
+
createTaskWithSiblingAwareness(renderedMessageText, {
|
|
127
132
|
agentId: workingAgent.id,
|
|
128
133
|
source: "slack",
|
|
129
134
|
slackChannelId: channelId,
|
|
@@ -141,8 +146,11 @@ export function createAssistant(): Assistant {
|
|
|
141
146
|
// 2. First message in thread — create new task for lead
|
|
142
147
|
await safeSetStatus("Processing your request...");
|
|
143
148
|
|
|
144
|
-
if (
|
|
145
|
-
const title =
|
|
149
|
+
if (renderedMessageText) {
|
|
150
|
+
const title =
|
|
151
|
+
renderedMessageText.length > 50
|
|
152
|
+
? `${renderedMessageText.slice(0, 47)}...`
|
|
153
|
+
: renderedMessageText;
|
|
146
154
|
await safeSetTitle(title);
|
|
147
155
|
}
|
|
148
156
|
|
|
@@ -156,7 +164,7 @@ export function createAssistant(): Assistant {
|
|
|
156
164
|
const lead = getLeadAgent();
|
|
157
165
|
if (!lead) {
|
|
158
166
|
// No lead — still queue the task
|
|
159
|
-
createTaskWithSiblingAwareness(
|
|
167
|
+
createTaskWithSiblingAwareness(renderedMessageText + channelContext, {
|
|
160
168
|
source: "slack",
|
|
161
169
|
slackChannelId: channelId,
|
|
162
170
|
slackThreadTs: threadTs,
|
|
@@ -169,7 +177,7 @@ export function createAssistant(): Assistant {
|
|
|
169
177
|
return;
|
|
170
178
|
}
|
|
171
179
|
|
|
172
|
-
createTaskWithSiblingAwareness(
|
|
180
|
+
createTaskWithSiblingAwareness(renderedMessageText + channelContext, {
|
|
173
181
|
agentId: lead.id,
|
|
174
182
|
source: "slack",
|
|
175
183
|
slackChannelId: channelId,
|
package/src/slack/enrich.ts
CHANGED
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
|
|
24
24
|
import type { WebClient } from "@slack/web-api";
|
|
25
25
|
import { getKv, upsertKv } from "../be/db";
|
|
26
|
+
import { resolveIdentity } from "../be/identity";
|
|
26
27
|
import { recordUnmappedIdentity } from "../be/unmapped-identities";
|
|
27
28
|
import {
|
|
28
29
|
findOrCreateUserByEmail,
|
|
@@ -160,3 +161,19 @@ export async function resolveSlackUserId(
|
|
|
160
161
|
recordUnmappedIdentity("slack", slackUserId, eventContext);
|
|
161
162
|
return undefined;
|
|
162
163
|
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Rewrite every Slack `<@USERID>` mention token in `text` via the identity
|
|
167
|
+
* primitive: `<@USERID|Name>` when resolved, `<@USERID> (unknown user)` when
|
|
168
|
+
* not. Covers both author labels (`<@U…>: message`) and mentions embedded in
|
|
169
|
+
* message bodies — both are just this same token. Pure DB reads via
|
|
170
|
+
* `resolveIdentity`; zero Slack API calls, no cache.
|
|
171
|
+
*/
|
|
172
|
+
export function rewriteSlackMentions(text: string): string {
|
|
173
|
+
return text.replace(/<@([A-Z0-9]+)>/g, (_match, userId: string) => {
|
|
174
|
+
const resolution = resolveIdentity("slack", userId);
|
|
175
|
+
return resolution.status === "resolved"
|
|
176
|
+
? `<@${userId}|${resolution.name}>`
|
|
177
|
+
: `<@${userId}> (unknown user)`;
|
|
178
|
+
});
|
|
179
|
+
}
|
package/src/slack/handlers.ts
CHANGED
|
@@ -12,7 +12,7 @@ import { slackContextKey } from "../tasks/context-key";
|
|
|
12
12
|
import { createTaskWithSiblingAwareness } from "../tasks/sibling-awareness";
|
|
13
13
|
import { workflowEventBus } from "../workflows/event-bus";
|
|
14
14
|
import { buildTreeBlocks, type TreeNode } from "./blocks";
|
|
15
|
-
import { enrichSlackUserEmail, resolveSlackUserId } from "./enrich";
|
|
15
|
+
import { enrichSlackUserEmail, resolveSlackUserId, rewriteSlackMentions } from "./enrich";
|
|
16
16
|
import { wasEventSeen } from "./event-dedup";
|
|
17
17
|
import type { SlackFile } from "./files";
|
|
18
18
|
import { extractTaskFromMessage, hasOtherUserMention, routeMessage } from "./router";
|
|
@@ -250,23 +250,6 @@ async function wasThreadStartedBySwarm(
|
|
|
250
250
|
return startedBySwarm;
|
|
251
251
|
}
|
|
252
252
|
|
|
253
|
-
// Cache for user display names
|
|
254
|
-
const userNameCache = new Map<string, string>();
|
|
255
|
-
|
|
256
|
-
async function getUserDisplayName(client: WebClient, userId: string): Promise<string> {
|
|
257
|
-
if (userNameCache.has(userId)) {
|
|
258
|
-
return userNameCache.get(userId)!;
|
|
259
|
-
}
|
|
260
|
-
try {
|
|
261
|
-
const result = await client.users.info({ user: userId });
|
|
262
|
-
const name = result.user?.profile?.display_name || result.user?.real_name || userId;
|
|
263
|
-
userNameCache.set(userId, name);
|
|
264
|
-
return name;
|
|
265
|
-
} catch {
|
|
266
|
-
return userId;
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
|
|
270
253
|
/**
|
|
271
254
|
* Fetch thread history and format as context for the task.
|
|
272
255
|
* Returns empty string if not in a thread or no previous messages.
|
|
@@ -297,7 +280,10 @@ async function getThreadContext(
|
|
|
297
280
|
|
|
298
281
|
if (previousMessages.length === 0) return "";
|
|
299
282
|
|
|
300
|
-
// Format messages with user names or [Agent] for bot messages
|
|
283
|
+
// Format messages with user names or [Agent] for bot messages. Author
|
|
284
|
+
// identity is rendered via the identity primitive (resolved name or the
|
|
285
|
+
// UNKNOWN sentinel) — never a raw Slack API display name, and no
|
|
286
|
+
// `users.info` call.
|
|
301
287
|
const formattedMessages: string[] = [];
|
|
302
288
|
for (const m of previousMessages) {
|
|
303
289
|
// Check if this is a bot/agent message (multiple ways to identify)
|
|
@@ -310,12 +296,12 @@ async function getThreadContext(
|
|
|
310
296
|
const truncatedText = text.length > 500 ? `${text.slice(0, 500)}...` : text;
|
|
311
297
|
formattedMessages.push(`[Agent]: ${truncatedText}`);
|
|
312
298
|
} else {
|
|
313
|
-
const
|
|
314
|
-
formattedMessages.push(`${
|
|
299
|
+
const authorTag = m.user ? `<@${m.user}>` : "Unknown";
|
|
300
|
+
formattedMessages.push(`${authorTag}: ${text}`);
|
|
315
301
|
}
|
|
316
302
|
}
|
|
317
303
|
|
|
318
|
-
return formattedMessages.join("\n");
|
|
304
|
+
return rewriteSlackMentions(formattedMessages.join("\n"));
|
|
319
305
|
} catch (error) {
|
|
320
306
|
console.error("[Slack] Failed to fetch thread context:", error);
|
|
321
307
|
return "";
|
package/src/slack/responses.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { WebClient } from "@slack/web-api";
|
|
2
2
|
import { getAgentById, getTaskAttachments } from "../be/db";
|
|
3
|
-
import type { Agent, AgentTask } from "../types";
|
|
3
|
+
import type { Agent, AgentTask, TaskAttachment } from "../types";
|
|
4
4
|
import { getSlackApp } from "./app";
|
|
5
5
|
import {
|
|
6
6
|
buildCancelledBlocks,
|
|
@@ -10,7 +10,9 @@ import {
|
|
|
10
10
|
buildProgressBlocks,
|
|
11
11
|
formatAttachmentsBlockForSlack,
|
|
12
12
|
formatDuration,
|
|
13
|
+
getTaskLink,
|
|
13
14
|
markdownToSlack,
|
|
15
|
+
splitSlackSectionText,
|
|
14
16
|
} from "./blocks";
|
|
15
17
|
|
|
16
18
|
// Re-export for backward compatibility
|
|
@@ -31,6 +33,16 @@ function classifySlackUpdateError(error: unknown): SlackUpdateResult {
|
|
|
31
33
|
}
|
|
32
34
|
|
|
33
35
|
const isDev = process.env.ENV === "development";
|
|
36
|
+
const MIN_INLINE_OUTPUT_LENGTH = 20;
|
|
37
|
+
export const MAX_INLINE_OUTPUT_MESSAGE_LENGTH = 4000;
|
|
38
|
+
const INLINE_OUTPUT_FALLBACKS = new Set([
|
|
39
|
+
"done",
|
|
40
|
+
"completed",
|
|
41
|
+
"complete",
|
|
42
|
+
"task completed",
|
|
43
|
+
"process completed successfully",
|
|
44
|
+
"process completed successfully (no output captured)",
|
|
45
|
+
]);
|
|
34
46
|
|
|
35
47
|
/**
|
|
36
48
|
* Get the display name for an agent, with (dev) prefix if in development mode.
|
|
@@ -39,6 +51,84 @@ function getAgentDisplayName(agent: Agent): string {
|
|
|
39
51
|
return isDev ? `(dev) ${agent.name}` : agent.name;
|
|
40
52
|
}
|
|
41
53
|
|
|
54
|
+
export function shouldPostInlineCompletionOutput(
|
|
55
|
+
task: AgentTask,
|
|
56
|
+
attachments: readonly TaskAttachment[],
|
|
57
|
+
): boolean {
|
|
58
|
+
if (task.status !== "completed") return false;
|
|
59
|
+
if (!task.slackChannelId || !task.slackThreadTs) return false;
|
|
60
|
+
if (task.slackReplySent) return false;
|
|
61
|
+
|
|
62
|
+
const output = task.output?.trim();
|
|
63
|
+
if (!output || output.length < MIN_INLINE_OUTPUT_LENGTH) return false;
|
|
64
|
+
if (INLINE_OUTPUT_FALLBACKS.has(output.toLowerCase())) return false;
|
|
65
|
+
|
|
66
|
+
return !attachments.some((attachment) => attachment.isPrimary);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
export function formatInlineCompletionOutputText(opts: {
|
|
70
|
+
agentName: string;
|
|
71
|
+
taskId: string;
|
|
72
|
+
output: string;
|
|
73
|
+
}): string {
|
|
74
|
+
const header = `✅ *${opts.agentName}* completed with output (${getTaskLink(opts.taskId)}):\n\n`;
|
|
75
|
+
const tail = `\n\n…(full output in task ${getTaskLink(opts.taskId)})`;
|
|
76
|
+
const slackOutput = markdownToSlack(opts.output.trim());
|
|
77
|
+
const maxBodyLength = MAX_INLINE_OUTPUT_MESSAGE_LENGTH - header.length;
|
|
78
|
+
|
|
79
|
+
if (slackOutput.length <= maxBodyLength) {
|
|
80
|
+
return header + slackOutput;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
const maxTruncatedBodyLength = Math.max(0, maxBodyLength - tail.length);
|
|
84
|
+
let cut = slackOutput.lastIndexOf("\n", maxTruncatedBodyLength);
|
|
85
|
+
if (cut < maxTruncatedBodyLength / 2) {
|
|
86
|
+
cut = slackOutput.lastIndexOf(" ", maxTruncatedBodyLength);
|
|
87
|
+
}
|
|
88
|
+
if (cut < maxTruncatedBodyLength / 2) {
|
|
89
|
+
cut = maxTruncatedBodyLength;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
return header + slackOutput.slice(0, cut).trimEnd() + tail;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export async function sendInlineTaskOutput(task: AgentTask): Promise<boolean> {
|
|
96
|
+
const app = getSlackApp();
|
|
97
|
+
if (!app || !task.slackChannelId || !task.slackThreadTs || !task.agentId || !task.output) {
|
|
98
|
+
return false;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const agent = getAgentById(task.agentId);
|
|
102
|
+
if (!agent) {
|
|
103
|
+
console.error(`[Slack] Agent not found for task ${task.id}`);
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const text = formatInlineCompletionOutputText({
|
|
108
|
+
agentName: agent.name,
|
|
109
|
+
taskId: task.id,
|
|
110
|
+
output: task.output,
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
try {
|
|
114
|
+
await sendWithPersona(app.client, {
|
|
115
|
+
channel: task.slackChannelId,
|
|
116
|
+
thread_ts: task.slackThreadTs,
|
|
117
|
+
text,
|
|
118
|
+
username: getAgentDisplayName(agent),
|
|
119
|
+
icon_emoji: getAgentEmoji(agent),
|
|
120
|
+
blocks: splitSlackSectionText(text).map((chunk) => ({
|
|
121
|
+
type: "section",
|
|
122
|
+
text: { type: "mrkdwn", text: chunk },
|
|
123
|
+
})),
|
|
124
|
+
});
|
|
125
|
+
return true;
|
|
126
|
+
} catch (error) {
|
|
127
|
+
console.error(`[Slack] Failed to send inline output for task ${task.id}:`, error);
|
|
128
|
+
return false;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
42
132
|
/**
|
|
43
133
|
* Send a task completion message to Slack with the agent's persona.
|
|
44
134
|
*/
|
|
@@ -4,6 +4,7 @@ import { slackContextKey } from "../tasks/context-key";
|
|
|
4
4
|
import { createTaskWithSiblingAwareness } from "../tasks/sibling-awareness";
|
|
5
5
|
import { getSlackApp } from "./app";
|
|
6
6
|
import { buildBufferFlushBlocks } from "./blocks";
|
|
7
|
+
import { rewriteSlackMentions } from "./enrich";
|
|
7
8
|
import { extractSlackMessageText } from "./message-text";
|
|
8
9
|
import { registerTreeMessage } from "./watcher";
|
|
9
10
|
|
|
@@ -107,7 +108,7 @@ async function getThreadContextForBuffer(channelId: string, threadTs: string): P
|
|
|
107
108
|
})
|
|
108
109
|
.join("\n");
|
|
109
110
|
|
|
110
|
-
return formatted;
|
|
111
|
+
return rewriteSlackMentions(formatted);
|
|
111
112
|
} catch (error) {
|
|
112
113
|
console.error("[Slack] Failed to fetch thread context for buffer:", error);
|
|
113
114
|
return "";
|
|
@@ -137,8 +138,10 @@ async function slackFlush(
|
|
|
137
138
|
|
|
138
139
|
console.log(`[Slack] Flushing buffer: ${key} (${items.length} messages, immediate=${immediate})`);
|
|
139
140
|
|
|
140
|
-
// Build combined task description
|
|
141
|
-
|
|
141
|
+
// Build combined task description. Any in-body `<@U…>` mentions the
|
|
142
|
+
// requester typed are rewritten via the identity primitive so the agent
|
|
143
|
+
// sees a name or the explicit UNKNOWN sentinel — never a raw Slack ID.
|
|
144
|
+
const combinedText = rewriteSlackMentions(items.map((m) => m.text).join("\n---\n"));
|
|
142
145
|
const description = `[Thread follow-up — ${items.length} message(s) buffered]\n\n${combinedText}`;
|
|
143
146
|
|
|
144
147
|
// Find the latest active task in this thread for dependency chaining
|
package/src/slack/watcher.ts
CHANGED
|
@@ -12,8 +12,10 @@ import { getSlackApp } from "./app";
|
|
|
12
12
|
import type { TreeNode } from "./blocks";
|
|
13
13
|
import { buildTreeBlocks, formatDuration } from "./blocks";
|
|
14
14
|
import {
|
|
15
|
+
sendInlineTaskOutput,
|
|
15
16
|
sendProgressUpdate,
|
|
16
17
|
sendTaskResponse,
|
|
18
|
+
shouldPostInlineCompletionOutput,
|
|
17
19
|
updateProgressInPlace,
|
|
18
20
|
updateToFinal,
|
|
19
21
|
updateTreeMessage,
|
|
@@ -704,11 +706,11 @@ export function startTaskWatcher(intervalMs = 3000): void {
|
|
|
704
706
|
if (lastSent && now - lastSent < MIN_SEND_INTERVAL) continue;
|
|
705
707
|
|
|
706
708
|
// Tasks tracked in a tree are still rendered by processTreeMessages().
|
|
707
|
-
//
|
|
708
|
-
//
|
|
709
|
-
// only place their output appears.
|
|
709
|
+
// For prose-only completions, add one inline output reply so the user
|
|
710
|
+
// sees the actual answer instead of only the compact tree preview.
|
|
710
711
|
if (taskToTree.has(task.id)) {
|
|
711
|
-
|
|
712
|
+
const attachments = task.status === "completed" ? getTaskAttachments(task.id) : [];
|
|
713
|
+
if (!shouldPostInlineCompletionOutput(task, attachments)) {
|
|
712
714
|
notifiedCompletions.set(task.id, now);
|
|
713
715
|
continue;
|
|
714
716
|
}
|
|
@@ -717,9 +719,10 @@ export function startTaskWatcher(intervalMs = 3000): void {
|
|
|
717
719
|
notifiedCompletions.set(task.id, now);
|
|
718
720
|
lastSendTime.set(completionKey, now);
|
|
719
721
|
try {
|
|
720
|
-
await
|
|
722
|
+
const sent = await sendInlineTaskOutput(task);
|
|
723
|
+
if (!sent) throw new Error("sendInlineTaskOutput returned false");
|
|
721
724
|
console.log(
|
|
722
|
-
`[Slack] Sent
|
|
725
|
+
`[Slack] Sent inline output for tree-tracked completion ${task.id.slice(0, 8)}`,
|
|
723
726
|
);
|
|
724
727
|
} catch (error) {
|
|
725
728
|
notifiedCompletions.delete(task.id);
|