@desplega.ai/agent-swarm 1.84.0 → 1.84.1

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 CHANGED
@@ -2,12 +2,10 @@
2
2
  <a href="https://github.com/desplega-ai/agent-swarm/stargazers"><img src="https://img.shields.io/github/stars/desplega-ai/agent-swarm?style=flat-square&color=yellow" alt="GitHub Stars"></a>
3
3
  <a href="https://github.com/desplega-ai/agent-swarm/blob/main/LICENSE"><img src="https://img.shields.io/github/license/desplega-ai/agent-swarm?style=flat-square" alt="MIT License"></a>
4
4
  <a href="https://github.com/desplega-ai/agent-swarm/pulls"><img src="https://img.shields.io/badge/PRs-welcome-brightgreen?style=flat-square" alt="PRs Welcome"></a>
5
- <a href="https://discord.gg/KZgfyyDVZa"><img src="https://img.shields.io/badge/Discord-Join%20us-5865F2?style=flat-square&logo=discord&logoColor=white" alt="Discord"></a>
6
- <a href="https://docs.agent-swarm.dev"><img src="https://img.shields.io/badge/docs-agent--swarm.dev-blue?style=flat-square" alt="Docs"></a>
7
5
  </p>
8
6
 
9
7
  <p align="center">
10
- <b>Multi-agent orchestration for Claude Code, Codex, Gemini CLI, and other AI coding assistants.</b><br/>
8
+ <b>An engine to make your company AI Native</b><br/>
11
9
  <sub>Built by <a href="https://desplega.sh">desplega.sh</a> — by builders, for builders.</sub>
12
10
  </p>
13
11
 
@@ -39,11 +37,19 @@
39
37
  </a>
40
38
  </p>
41
39
 
42
- > **What if your AI agents remembered everything, learned from every mistake, and got better with every task?**
40
+ > **Agent Swarm is your Company's Compounding Intelligence Layer. A system of AI agents that remember, reason, act and get better with every task.**
41
+
42
+ > AI-Native · Compounds · Presence · Harness & LLM-Agnostic · Your Infra · Your Memory ·
43
43
 
44
44
  ## What it does
45
45
 
46
- Agent Swarm runs a team of AI coding agents that coordinate autonomously. A **lead agent** receives tasks from Slack, GitHub, GitLab, email, or the API breaks them down, and delegates to **worker agents** running in Docker containers. Workers execute tasks, ship code, and write their learnings back to a shared memory so the whole swarm gets smarter every session.
46
+ Agent Swarm runs a team of AI agents that coordinate autonomously. A **lead agent** receives tasks ( from Slack, GitHub, GitLab, Linear, Jira, email, or the API) breaks them down, and delegates to **worker agents** running in isolated environments (Docker). Workers execute tasks, ship solutions, and write their learnings back to a shared memory so the whole swarm gets smarter every session.
47
+
48
+ You can run agents for Marketing, Product, UX, Engineering, Support, Operations, HR, Finance, or any role you can think of. A centralized Lead coordinates them, and they share the learnings horizontally. That's the true difference between [*AI First*](https://www.pleasedontdeploy.com/i/197193364/ai-first) and [*AI Native*](https://www.pleasedontdeploy.com/i/197193364/third-the-ai-native-metamorphosis).
49
+
50
+ Agent Swarm is the shared cloud brain and muscle that makes your whole company better every day.
51
+
52
+ Sometimes humans are the blocker. We can help you. Contact us [contact@desplega.sh](mailto:contact@desplega.sh).
47
53
 
48
54
  Learn more in the [architecture overview](https://docs.agent-swarm.dev/docs/architecture/overview).
49
55
 
@@ -85,14 +91,39 @@ flowchart LR
85
91
  WORKERS --> OUT
86
92
  ```
87
93
 
94
+ ## Known Use Cases
95
+
96
+ Use cases that are used daily by ourselves and others.
97
+ Each playbook contains: the agents, the tools & skills, and workflows & schedules behind it. **[Browse all playbooks →](https://docs.agent-swarm.dev/docs/playbooks)**
98
+
99
+ - **[Feature Development](https://docs.agent-swarm.dev/docs/playbooks/feature-development)** — Integrated with Linear and GitHub to take feature requests from Slack and turn them into pull requests.
100
+ - **[Lead Prospecting](https://docs.agent-swarm.dev/docs/playbooks/lead-prospecting)** — Integrate your prospecting tools with the swarm and let agents handle outreach, scheduling, and follow-up.
101
+ - **[Content Generation](https://docs.agent-swarm.dev/docs/playbooks/content-generation)** — Generate engagement tools, blog posts, manage social media presence, update your website, and more.
102
+ - **[UX Command Center](https://docs.agent-swarm.dev/docs/playbooks/ux-command-center)** — Agents that keep your product usable: record agentic sessions, enforce your design system, and mine user logs to detect and propose UX improvements.
103
+ - **[Proactive Customer Support](https://docs.agent-swarm.dev/docs/playbooks/proactive-customer-support)** — Agents that oversee your top accounts, prepare scheduled reports, and leverage everything they know about your platform to keep those accounts up to date.
104
+ - **[Code Health & Alert Management](https://docs.agent-swarm.dev/docs/playbooks/code-health-alert-management)** — Datadog, New Relic, Sentry, or any alerting tool can kick off fixes or new proposals. Monitor code health and propose improvements weekly, daily, or hourly.
105
+ - **[Reports from Multiple Sources](https://docs.agent-swarm.dev/docs/playbooks/reports-multiple-sources)** — Integrate your data warehouse to generate tailored reports and answer the key questions your team has, with fresh data. Your BI tool may be a thing of the past.
106
+ - **[Self-Documenting & Release Reports](https://docs.agent-swarm.dev/docs/playbooks/self-documenting-release-reports)** — Update your docs and use frameworks like [Remotion](https://www.remotion.dev/), [qa-use](https://github.com/qa-use/qa-use), and [browser-use](https://github.com/browser-use/browser-use) to generate release videos and rich documentation in seconds, at the cadence you need.
107
+ - Do you have a cool playbook to share? Send us a PR!
108
+
109
+ > **The patterns that compound.** Five recipes show up in nearly every playbook — they're how the swarm stays reliable as it scales:
110
+ > **[Litmus Tests](https://docs.agent-swarm.dev/docs/playbooks/patterns/litmus-tests)** (LLM-as-judge quality gates) ·
111
+ > **[Drain Loops](https://docs.agent-swarm.dev/docs/playbooks/patterns/drain-loops)** (one ticket → a chain of reviewable PRs) ·
112
+ > **[HITL Gates](https://docs.agent-swarm.dev/docs/playbooks/patterns/hitl-gates)** (pause for human approval on irreversible steps) ·
113
+ > **[Per-Customer Working Directories](https://docs.agent-swarm.dev/docs/playbooks/patterns/per-customer-working-directories)** (context that compounds per account) ·
114
+ > **[No-op Workflows](https://docs.agent-swarm.dev/docs/playbooks/patterns/no-op-workflows)** (skip silently when nothing changed).
115
+ > **[See all patterns →](https://docs.agent-swarm.dev/docs/playbooks/patterns)**
116
+
117
+ Check [our templates](https://templates.agent-swarm.dev) for a quick start.
118
+
88
119
  ## Highlights
89
120
 
90
121
  - **Lead/worker orchestration in Docker** — isolated dev environments, priority queues, pause/resume across deploys. [Architecture →](https://docs.agent-swarm.dev/docs/architecture/overview)
91
122
  - **Compounding memory & persistent identity** — agents remember past sessions and evolve their own persona, expertise, and notes. [Memory →](https://docs.agent-swarm.dev/docs/architecture/memory) · [Agents →](https://docs.agent-swarm.dev/docs/architecture/agents)
92
- - **Multi-channel inputs** — Slack, GitHub, GitLab, email, Linear, Jira, and the HTTP API all create tasks. [Integrations](#integrations)
123
+ - **Multi-channel inputs** — Slack, GitHub, GitLab, email, WhatsApp, Linear, Jira, and the HTTP API all create tasks. [Integrations](#integrations)
93
124
  - **Workflow engine with Human-in-the-Loop** — DAG-based automation with approval gates, retries, and structured I/O. [Workflows →](https://docs.agent-swarm.dev/docs/concepts/workflows)
94
125
  - **Scheduled & recurring tasks** — cron-based automation for standing work. [Scheduling →](https://docs.agent-swarm.dev/docs/concepts/scheduling)
95
- - **Multi-provider** — run with Claude Code, OpenAI Codex, pi-mono, Devin, Claude Managed Agents, or opencode. [Harness config →](https://docs.agent-swarm.dev/docs/guides/harness-configuration) · [Add a new provider →](https://docs.agent-swarm.dev/docs/guides/harness-providers)
126
+ - **Harness & LLM agnostic** — run with Claude Code, OpenAI Codex, pi-mono, Devin, Claude Managed Agents, raw LLMs, or opencode. [Harness config →](https://docs.agent-swarm.dev/docs/guides/harness-configuration) · [Add a new provider →](https://docs.agent-swarm.dev/docs/guides/harness-providers)
96
127
  - **Skills & MCP servers** — reusable procedural knowledge and per-agent MCP servers with scope cascade. [MCP tools →](https://docs.agent-swarm.dev/docs/reference/mcp-tools)
97
128
  - **DB-backed pages** — agents publish HTML or JSON pages (reports, dashboards, action specs) via the `create_page` MCP tool with public / authed / password modes, version history, view counters, diff helpers, and PDF export. [MCP tools → Pages](https://docs.agent-swarm.dev/docs/reference/mcp-tools#pages-tools)
98
129
  - **KV store** — Redis-like namespaced key/value store with auto-scoped context (Slack thread / PR / Linear issue / page). [MCP tools → KV](https://docs.agent-swarm.dev/docs/reference/mcp-tools#kv-tools)
@@ -100,6 +131,8 @@ flowchart LR
100
131
 
101
132
  ## Quick Start
102
133
 
134
+ Need help? Contact us at [contact@desplega.sh](mailto:contact@desplega.sh).
135
+
103
136
  **Prerequisites:** [Docker](https://docker.com) and a [Claude Code](https://docs.anthropic.com/en/docs/claude-code) OAuth token (`claude setup-token`).
104
137
 
105
138
  The fastest way is the onboarding wizard — it collects credentials, picks presets, and generates a working `docker-compose.yml`:
@@ -144,22 +177,26 @@ Worker Worker Worker
144
177
  2. The lead plans and delegates subtasks to workers.
145
178
  3. Workers execute in isolated Docker containers (git, Node.js, Python, etc.).
146
179
  4. Progress streams to the dashboard, Slack threads, or the API.
147
- 5. Results ship back out as PRs, issue replies, or Slack messages.
180
+ 5. Results ship back out as PRs, custom pages, issue replies, or Slack messages.
148
181
  6. Session learnings are extracted and become memory for future tasks.
149
182
 
150
183
  More detail in the [task lifecycle docs](https://docs.agent-swarm.dev/docs/concepts/task-lifecycle).
151
184
 
152
185
  ## Integrations
153
186
 
187
+ Missing one? Ask the swarm to build it.
188
+
154
189
  | Integration | What it does | Setup |
155
190
  |---|---|---|
156
191
  | **Slack** | DM or @mention the bot to create tasks; workers reply in threads | [Guide](https://docs.agent-swarm.dev/docs/guides/slack-integration) |
157
192
  | **GitHub App** | @mention or assign the bot on issues/PRs; CI failures create follow-up tasks | [Guide](https://docs.agent-swarm.dev/docs/guides/github-integration) |
158
193
  | **GitLab** | Same model as GitHub — webhooks on issues/MRs, `glab` preinstalled in workers | [Guide](https://docs.agent-swarm.dev/docs/guides/gitlab-integration) |
159
194
  | **AgentMail** | Give each agent an inbox; emails become tasks or lead messages | [Guide](https://docs.agent-swarm.dev/docs/guides/agentmail-integration) |
195
+ | **Kapso (WhatsApp)** | Native inbound WhatsApp webhook routing; agents reply over WhatsApp with MCP tools or the `kapso-whatsapp` skill | [Guide](https://docs.agent-swarm.dev/docs/integrations/kapso) |
160
196
  | **Linear** | Bidirectional ticket sync via OAuth + webhooks | [Guide](https://docs.agent-swarm.dev/docs/guides/linear-integration) |
161
197
  | **Jira Cloud** | OAuth 3LO ticket sync — assignee/comment events create tasks; lifecycle posts comments back | [Guide](https://docs.agent-swarm.dev/docs/guides/jira-integration) |
162
198
  | **Sentry** | Workers can triage Sentry issues with the `/investigate-sentry-issue` command | [Guide](https://docs.agent-swarm.dev/docs/guides/sentry-integration) |
199
+ | **Devin** | Devin can be a node in your swarm — keep your existing configuration | [Guide](https://docs.agent-swarm.dev/docs/guides/harness-configuration#supported-providers) |
163
200
 
164
201
  ## Dashboard
165
202
 
@@ -196,6 +233,7 @@ Everything lives at **[docs.agent-swarm.dev](https://docs.agent-swarm.dev)**. Go
196
233
 
197
234
  - [Getting Started](https://docs.agent-swarm.dev/docs/getting-started) — install, configure, and run your first task
198
235
  - [Architecture overview](https://docs.agent-swarm.dev/docs/architecture/overview) — how the swarm is wired together
236
+ - [Playbooks](https://docs.agent-swarm.dev/docs/playbooks) — eight production flows we use to run Desplega, plus the patterns behind them
199
237
  - [CLI reference](https://docs.agent-swarm.dev/docs/reference/cli) and [Environment variables](https://docs.agent-swarm.dev/docs/reference/environment-variables)
200
238
  - [API reference](https://docs.agent-swarm.dev/docs/api-reference) — every HTTP endpoint
201
239
 
@@ -212,6 +250,8 @@ See [CONTRIBUTING.md](./CONTRIBUTING.md) to get started. The quickest way to con
212
250
 
213
251
  Join our [Discord](https://discord.gg/KZgfyyDVZa) if you have questions or want to discuss ideas.
214
252
 
253
+ > Are you an agent? Go to [agent-swarm.dev/skill.md](https://www.agent-swarm.dev/skill.md).
254
+
215
255
  ## Star History
216
256
 
217
257
  <a href="https://star-history.com/#desplega-ai/agent-swarm&Date">
package/openapi.json CHANGED
@@ -2,7 +2,7 @@
2
2
  "openapi": "3.1.0",
3
3
  "info": {
4
4
  "title": "Agent Swarm API",
5
- "version": "1.84.0",
5
+ "version": "1.84.1",
6
6
  "description": "Multi-agent orchestration API for Claude Code, Codex, and Gemini CLI. Enables task distribution, agent communication, and service discovery.\n\nMCP tools are documented separately in [MCP.md](./MCP.md)."
7
7
  },
8
8
  "servers": [
@@ -10839,11 +10839,13 @@
10839
10839
  "email": {
10840
10840
  "type": "string",
10841
10841
  "format": "email"
10842
+ },
10843
+ "notes": {
10844
+ "type": "string"
10842
10845
  }
10843
10846
  },
10844
10847
  "required": [
10845
- "name",
10846
- "email"
10848
+ "name"
10847
10849
  ]
10848
10850
  }
10849
10851
  ]
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@desplega.ai/agent-swarm",
3
- "version": "1.84.0",
3
+ "version": "1.84.1",
4
4
  "description": "Multi-agent orchestration for Claude Code, Codex, Gemini CLI, and other AI coding assistants",
5
5
  "license": "MIT",
6
6
  "author": "desplega.sh <contact@desplega.sh>",
@@ -0,0 +1,43 @@
1
+ -- Backfill Kapso/WhatsApp sender identities into the canonical user registry.
2
+ --
3
+ -- Native Kapso inbound messages resolve their sender through user_external_ids
4
+ -- using kind='kapso' and the normalized WhatsApp phone number from
5
+ -- message.from/conversation.phone_number. Existing user profiles can already
6
+ -- carry WhatsApp numbers in notes in the documented form:
7
+ --
8
+ -- WhatsApp: +34 ... (E.164: 346...)
9
+ --
10
+ -- Link those existing, human-curated profile rows instead of leaving inbound
11
+ -- Kapso sender rows unmapped. This is idempotent and preserves any existing
12
+ -- mapping for a phone number.
13
+
14
+ INSERT OR IGNORE INTO user_external_ids (kind, externalId, userId)
15
+ WITH raw_notes AS (
16
+ SELECT
17
+ id AS userId,
18
+ substr(notes, instr(notes, 'E.164:') + length('E.164:')) AS e164_suffix
19
+ FROM users
20
+ WHERE notes LIKE '%WhatsApp:%'
21
+ AND notes LIKE '%E.164:%'
22
+ ),
23
+ parsed AS (
24
+ SELECT
25
+ userId,
26
+ trim(
27
+ CASE
28
+ WHEN instr(e164_suffix, ')') > 0 THEN substr(e164_suffix, 1, instr(e164_suffix, ')') - 1)
29
+ ELSE e164_suffix
30
+ END
31
+ ) AS e164_value
32
+ FROM raw_notes
33
+ ),
34
+ normalized AS (
35
+ SELECT
36
+ userId,
37
+ replace(replace(replace(replace(e164_value, '+', ''), ' ', ''), '-', ''), '.', '') AS externalId
38
+ FROM parsed
39
+ )
40
+ SELECT 'kapso', externalId, userId
41
+ FROM normalized
42
+ WHERE externalId <> ''
43
+ AND externalId NOT GLOB '*[^0-9]*';
@@ -0,0 +1,178 @@
1
+ /**
2
+ * Universal context preamble for follow-up task continuity.
3
+ *
4
+ * Builds a bounded text summary of prior task context (parent → ancestor chain)
5
+ * and prepends it to the child task's prompt. This makes follow-up continuity
6
+ * uniform across ALL harness providers — not just those that support native
7
+ * session resume (claude/codex).
8
+ *
9
+ * Token budget (CONTEXT_PREAMBLE_MAX_TOKENS, default 2000) prevents the
10
+ * SIGTERM-143 context-saturation failure mode seen with unbounded session
11
+ * resumes (see swarm memory sigterm-143-resumed-session-context-saturation-2026-05-13).
12
+ */
13
+
14
+ export const CONTEXT_PREAMBLE_MAX_TOKENS = Number(
15
+ process.env.CONTEXT_PREAMBLE_MAX_TOKENS || "2000",
16
+ );
17
+ // ~4 chars per token (conservative approximation for mixed code/prose)
18
+ export const CONTEXT_PREAMBLE_MAX_CHARS = CONTEXT_PREAMBLE_MAX_TOKENS * 4;
19
+ export const CONTEXT_PREAMBLE_MAX_ANCESTORS = 5;
20
+
21
+ export interface TaskContextForPreamble {
22
+ id: string;
23
+ task: string;
24
+ output?: string;
25
+ progress?: string;
26
+ status?: string;
27
+ parentTaskId?: string;
28
+ attachments?: Array<{
29
+ kind: string;
30
+ name: string;
31
+ url?: string;
32
+ path?: string;
33
+ pageId?: string;
34
+ orgId?: string;
35
+ driveId?: string;
36
+ description?: string;
37
+ intent?: string;
38
+ isPrimary?: boolean;
39
+ }>;
40
+ }
41
+
42
+ /** Fetch minimal task context for preamble generation (worker-side, via HTTP). */
43
+ export async function fetchTaskContextForPreamble(
44
+ apiUrl: string,
45
+ apiKey: string,
46
+ taskId: string,
47
+ ): Promise<TaskContextForPreamble | null> {
48
+ const headers: Record<string, string> = {};
49
+ if (apiKey) headers.Authorization = `Bearer ${apiKey}`;
50
+ try {
51
+ const response = await fetch(`${apiUrl}/api/tasks/${taskId}`, { headers });
52
+ if (!response.ok) return null;
53
+ const data = (await response.json()) as TaskContextForPreamble;
54
+ return {
55
+ id: data.id,
56
+ task: data.task,
57
+ output: data.output,
58
+ progress: data.progress,
59
+ status: data.status,
60
+ parentTaskId: data.parentTaskId,
61
+ attachments: data.attachments,
62
+ };
63
+ } catch {
64
+ return null;
65
+ }
66
+ }
67
+
68
+ function formatAttachmentPointer(
69
+ att: NonNullable<TaskContextForPreamble["attachments"]>[number],
70
+ ): string {
71
+ if (att.kind === "agent-fs" && att.path) {
72
+ const liveHost = process.env.AGENT_FS_LIVE_URL ?? "https://live.agent-fs.dev";
73
+ if (att.orgId && att.driveId) {
74
+ return `${liveHost}/file/~/${att.orgId}/${att.driveId}/${att.path}`;
75
+ }
76
+ return att.path;
77
+ }
78
+ if (att.kind === "url" && att.url) return att.url;
79
+ if (att.kind === "page" && att.pageId) return `(page:${att.pageId})`;
80
+ if (att.kind === "shared-fs" && att.path) return att.path;
81
+ return "(no pointer)";
82
+ }
83
+
84
+ /**
85
+ * Build a bounded context preamble for a follow-up task.
86
+ *
87
+ * Walks the ancestor chain (up to CONTEXT_PREAMBLE_MAX_ANCESTORS) via the API
88
+ * and returns a formatted markdown block that is prepended to the child prompt.
89
+ *
90
+ * - Immediate parent: inline detail (subject + output + attachments)
91
+ * - Older ancestors: pointer-only (taskId + one-line subject)
92
+ *
93
+ * Hard-capped at CONTEXT_PREAMBLE_MAX_CHARS (~CONTEXT_PREAMBLE_MAX_TOKENS
94
+ * tokens) to prevent context saturation.
95
+ */
96
+ export async function buildContextPreamble(
97
+ apiUrl: string,
98
+ apiKey: string,
99
+ parentTaskId: string,
100
+ ): Promise<string | null> {
101
+ const ancestors: TaskContextForPreamble[] = [];
102
+ let currentId: string | undefined = parentTaskId;
103
+ while (currentId && ancestors.length < CONTEXT_PREAMBLE_MAX_ANCESTORS) {
104
+ const ctx = await fetchTaskContextForPreamble(apiUrl, apiKey, currentId);
105
+ if (!ctx) break;
106
+ ancestors.push(ctx);
107
+ currentId = ctx.parentTaskId;
108
+ }
109
+ if (ancestors.length === 0) return null;
110
+ // ancestors[0] is guaranteed by the length check above; TypeScript needs the guard.
111
+ const parent = ancestors[0];
112
+ if (!parent) return null;
113
+
114
+ const lines: string[] = [
115
+ "\n---",
116
+ "## Prior Conversation Context",
117
+ "",
118
+ "This task is a follow-up in an ongoing thread. Here is a summary of prior work to maintain continuity.",
119
+ "",
120
+ ];
121
+
122
+ const subjectPreview = parent.task.slice(0, 600).replace(/\n/g, " ");
123
+ lines.push(`### Immediate Prior Task (ID: \`${parent.id}\`)`);
124
+ lines.push(`**Task:** ${subjectPreview}`);
125
+ lines.push("");
126
+
127
+ const rawResult = parent.output || parent.progress;
128
+ if (rawResult) {
129
+ // Reserve ~55% of budget for the output content; rest for structure + older ancestors
130
+ const outputBudget = Math.floor(CONTEXT_PREAMBLE_MAX_CHARS * 0.55);
131
+ const truncated =
132
+ rawResult.length > outputBudget
133
+ ? `${rawResult.slice(0, outputBudget)}\n\n[output truncated — full history via \`get-task-details\` with taskId \`${parent.id}\`]`
134
+ : rawResult;
135
+ lines.push("**Outcome:**");
136
+ lines.push(truncated);
137
+ lines.push("");
138
+ } else {
139
+ lines.push("**Outcome:** (no output recorded yet — task may still be in progress)");
140
+ lines.push("");
141
+ }
142
+
143
+ const atts = parent.attachments?.filter((a) => a.name && (a.url || a.path || a.pageId));
144
+ if (atts && atts.length > 0) {
145
+ lines.push("**Artifacts from prior task:**");
146
+ for (const att of atts.slice(0, 10)) {
147
+ const pointer = formatAttachmentPointer(att);
148
+ const note = att.description || att.intent || "";
149
+ lines.push(` - **${att.name}**: \`${pointer}\`${note ? ` — ${note}` : ""}`);
150
+ }
151
+ lines.push("");
152
+ }
153
+
154
+ lines.push(
155
+ `To review the full prior conversation call \`get-task-details\` with taskId \`${parent.id}\`.`,
156
+ );
157
+
158
+ if (ancestors.length > 1) {
159
+ lines.push("");
160
+ lines.push(
161
+ "### Older Ancestor Tasks (pointers only — call `get-task-details` for full details)",
162
+ );
163
+ for (const ancestor of ancestors.slice(1)) {
164
+ const brief = ancestor.task.slice(0, 200).replace(/\n/g, " ");
165
+ lines.push(`- \`${ancestor.id}\` — ${brief}`);
166
+ }
167
+ }
168
+
169
+ lines.push("", "---", "");
170
+
171
+ let preamble = lines.join("\n");
172
+
173
+ if (preamble.length > CONTEXT_PREAMBLE_MAX_CHARS) {
174
+ preamble = `${preamble.slice(0, CONTEXT_PREAMBLE_MAX_CHARS)}\n\n[context preamble truncated to ${CONTEXT_PREAMBLE_MAX_TOKENS}-token budget]\n\n---\n`;
175
+ }
176
+
177
+ return preamble;
178
+ }
@@ -49,6 +49,7 @@ import { scrubSecrets } from "../utils/secret-scrubber.ts";
49
49
  import { refreshSkillsIfChanged } from "../utils/skills-refresh.ts";
50
50
  import { detectVcsProvider } from "../vcs/index.ts";
51
51
  import { interpolate } from "../workflows/template.ts";
52
+ import { buildContextPreamble } from "./context-preamble.ts";
52
53
  import { awaitCredentials, BootMaxWaitExceededError, EX_CONFIG } from "./credential-wait.ts";
53
54
  import {
54
55
  buildCredStatusReport,
@@ -3752,6 +3753,19 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
3752
3753
  console.log(`[${role}] Injected relevant memories into resumed task prompt`);
3753
3754
  }
3754
3755
 
3756
+ // Universal context preamble: inject for all providers when task is a follow-up.
3757
+ // Gives non-resumable providers (opencode/pi/devin) prior-task context; also
3758
+ // acts as a bounded safety net for resumable ones (claude/codex).
3759
+ if (task.parentTaskId && apiUrl) {
3760
+ const contextPreamble = await buildContextPreamble(apiUrl, apiKey, task.parentTaskId);
3761
+ if (contextPreamble) {
3762
+ resumePrompt = contextPreamble + resumePrompt;
3763
+ console.log(
3764
+ `[${role}] Injected context preamble into resumed follow-up task prompt (parent: ${task.parentTaskId.slice(0, 8)})`,
3765
+ );
3766
+ }
3767
+ }
3768
+
3755
3769
  // Resolve provider-aware resume: prefer own session, then parent.
3756
3770
  const resumeCandidates: ResumeSessionCandidate[] = [
3757
3771
  {
@@ -4107,9 +4121,22 @@ export async function runAgent(config: RunnerConfig, opts: RunnerOptions) {
4107
4121
  }
4108
4122
  }
4109
4123
 
4124
+ // Universal context preamble: inject for all providers when task is a follow-up.
4125
+ // Gives non-resumable providers (opencode/pi/devin) prior-task context; also
4126
+ // acts as a bounded safety net for resumable ones (claude/codex).
4127
+ const taskObj = trigger.task as { parentTaskId?: string } | undefined;
4128
+ if (taskObj?.parentTaskId && apiUrl) {
4129
+ const contextPreamble = await buildContextPreamble(apiUrl, apiKey, taskObj.parentTaskId);
4130
+ if (contextPreamble) {
4131
+ triggerPrompt = contextPreamble + triggerPrompt;
4132
+ console.log(
4133
+ `[${role}] Injected context preamble for follow-up task (parent: ${taskObj.parentTaskId.slice(0, 8)})`,
4134
+ );
4135
+ }
4136
+ }
4137
+
4110
4138
  // Resolve provider-aware resume for child tasks with parentTaskId.
4111
4139
  let resumeSessionId: string | undefined;
4112
- const taskObj = trigger.task as { parentTaskId?: string } | undefined;
4113
4140
  if (taskObj?.parentTaskId) {
4114
4141
  const parentSession = await fetchProviderSessionInfo(
4115
4142
  apiUrl,
package/src/http/users.ts CHANGED
@@ -151,7 +151,11 @@ const resolveUnmapped = route({
151
151
  params: z.object({ kind: z.string(), externalId: z.string() }),
152
152
  body: z.union([
153
153
  z.object({ userId: z.string().min(1) }),
154
- z.object({ name: z.string().min(1), email: z.string().email() }),
154
+ z.object({
155
+ name: z.string().min(1),
156
+ email: z.string().email().optional(),
157
+ notes: z.string().optional(),
158
+ }),
155
159
  ]),
156
160
  responses: {
157
161
  200: { description: "Identity linked + kv entries cleared" },
@@ -322,7 +326,7 @@ const deleteIdentityRoute = route({
322
326
 
323
327
  // ─── Helpers ─────────────────────────────────────────────────────────────────
324
328
 
325
- const UNMAPPED_KINDS = ["slack", "github", "gitlab", "linear"] as const;
329
+ const UNMAPPED_KINDS = ["slack", "github", "gitlab", "linear", "kapso"] as const;
326
330
 
327
331
  /**
328
332
  * Group the two-key-per-identity kv entries (`<externalId>:meta` json +
@@ -477,7 +481,11 @@ export async function handleUsers(
477
481
  }
478
482
  targetUserId = existing.id;
479
483
  } else {
480
- const created = createUser({ name: parsed.body.name, email: parsed.body.email });
484
+ const created = createUser({
485
+ name: parsed.body.name,
486
+ email: parsed.body.email,
487
+ notes: parsed.body.notes,
488
+ });
481
489
  targetUserId = created.id;
482
490
  }
483
491
  linkIdentity(targetUserId, kind, externalId, actor);
@@ -2,8 +2,13 @@ import { resolveTemplate } from "@/prompts/resolver";
2
2
  import { createTaskWithSiblingAwareness } from "@/tasks/sibling-awareness";
3
3
  import { workflowEventBus } from "@/workflows/event-bus";
4
4
  import "@/tools/templates";
5
+ import { recordUnmappedIdentity } from "@/be/unmapped-identities";
6
+ import { findUserByExternalId } from "@/be/users";
5
7
  import { getKapsoNumberMapping, markKapsoMessageSeen } from "./config";
6
8
 
9
+ const KAPSO_IDENTITY_KIND = "kapso";
10
+ const WHATSAPP_IDENTITY_KIND = "whatsapp";
11
+
7
12
  /** Minimal shape of the Kapso v2 inbound webhook payload (see the kapso-whatsapp skill). */
8
13
  export interface KapsoWebhookPayload {
9
14
  message?: {
@@ -50,6 +55,36 @@ function buildTaskDescription(payload: KapsoWebhookPayload): string {
50
55
  }).text;
51
56
  }
52
57
 
58
+ function normalizeKapsoSender(payload: KapsoWebhookPayload): string | null {
59
+ const raw = payload.message?.from ?? payload.conversation?.phone_number ?? "";
60
+ const digits = raw.replace(/\D/g, "");
61
+ return digits || null;
62
+ }
63
+
64
+ function resolveKapsoRequestedByUserId(payload: KapsoWebhookPayload): string | undefined {
65
+ const externalId = normalizeKapsoSender(payload);
66
+ if (!externalId) return undefined;
67
+
68
+ const mapped =
69
+ findUserByExternalId(KAPSO_IDENTITY_KIND, externalId) ??
70
+ findUserByExternalId(WHATSAPP_IDENTITY_KIND, externalId);
71
+ if (mapped) return mapped.id;
72
+
73
+ recordUnmappedIdentity(KAPSO_IDENTITY_KIND, externalId, {
74
+ sampleEventType: "kapso.message.received",
75
+ sampleContext: [
76
+ payload.conversation?.contact_name ? `contact=${payload.conversation.contact_name}` : null,
77
+ payload.conversation?.id ? `conversation=${payload.conversation.id}` : null,
78
+ payload.message?.id ? `message=${payload.message.id}` : null,
79
+ payload.phone_number_id ? `phone_number_id=${payload.phone_number_id}` : null,
80
+ ]
81
+ .filter(Boolean)
82
+ .join(" "),
83
+ });
84
+
85
+ return undefined;
86
+ }
87
+
53
88
  /**
54
89
  * Route one inbound Kapso webhook delivery. Pure of HTTP concerns — the caller
55
90
  * handles HMAC verification and the workflow-trigger dispatch (which needs the
@@ -104,6 +139,7 @@ export function routeKapsoInbound(payload: KapsoWebhookPayload): KapsoRouting {
104
139
  taskType: "kapso-inbound",
105
140
  tags: ["kapso-whatsapp", "inbound"],
106
141
  priority: 70,
142
+ requestedByUserId: resolveKapsoRequestedByUserId(payload),
107
143
  contextKey: `kapso:conversation:${payload.conversation?.id ?? messageId}`,
108
144
  });
109
145
 
@@ -23,6 +23,13 @@ const BOOTSTRAP_TOTAL_MAX_CHARS = 150_000;
23
23
  const truncationNotice = (file: string) =>
24
24
  `\n\n[...truncated, see /workspace/${file} for full content]\n`;
25
25
 
26
+ export function areSlackPromptToolsEnabled(env: NodeJS.ProcessEnv = process.env): boolean {
27
+ const slackDisable = env.SLACK_DISABLE;
28
+ if (slackDisable === "true" || slackDisable === "1") return false;
29
+
30
+ return Boolean(env.SLACK_BOT_TOKEN && env.SLACK_APP_TOKEN);
31
+ }
32
+
26
33
  export type BasePromptArgs = {
27
34
  role: string;
28
35
  agentId: string;
@@ -71,9 +78,15 @@ export const getBasePrompt = async (args: BasePromptArgs): Promise<string> => {
71
78
  const compositeResult = await resolveTemplateAsync(compositeEventType, vars);
72
79
  let prompt = compositeResult.text;
73
80
 
81
+ const slackPromptToolsEnabled = areSlackPromptToolsEnabled();
82
+
83
+ if (hasMcp && slackPromptToolsEnabled) {
84
+ const slackResult = await resolveTemplateAsync("system.agent.slack", {});
85
+ prompt += slackResult.text;
86
+ }
87
+
74
88
  // Conditionally inject Slack instructions for workers with Slack-originated tasks
75
- // Skip for providers without MCP they can't call Slack tools (slack-reply, etc.)
76
- if (role !== "lead" && args.slackContext && hasMcp) {
89
+ if (role !== "lead" && args.slackContext && hasMcp && slackPromptToolsEnabled) {
77
90
  const slackResult = await resolveTemplateAsync("system.agent.worker.slack", {
78
91
  slackChannelId: args.slackContext.channelId,
79
92
  slackThreadTs: args.slackContext.threadTs ?? "",
@@ -59,16 +59,11 @@ As the lead agent, you coordinate all worker agents in the swarm.
59
59
  - \`send-task\`: Assign a task to a specific worker or to the general pool. Slack/AgentMail metadata auto-inherits from parent task.
60
60
  - \`store-progress\`: Track coordination notes or update task status
61
61
 
62
- **User Registration:** When a task arrives from an unknown user (no \`requestedByUserId\`), use the \`manage-user\` tool to register them before proceeding. Resolve their identity from the Slack metadata (user ID, display name) attached to the task.
63
-
64
- **Slack:**
65
- - \`slack-reply\`: Reply to user in the Slack thread (use taskId for context)
66
- - \`slack-read\`: Read thread/channel history (use taskId or channelId)
67
- - \`slack-list-channels\`: Discover available Slack channels
62
+ **User Registration:** When a task arrives from an unknown user (no \`requestedByUserId\`), use the \`manage-user\` tool to register them before proceeding. Resolve their identity from the task metadata attached to the task.
68
63
 
69
64
  **Identity:**
70
65
  - \`update-profile\`: Update your own or other agents' profile fields (name, role, capabilities, soulMd, identityMd, heartbeatMd, claudeMd, toolsMd, setupScript)
71
- - \`manage-user\`: Register or update human users (resolve from Slack/GitHub/GitLab identity)
66
+ - \`manage-user\`: Register or update human users (resolve from GitHub/GitLab identity or other source metadata)
72
67
 
73
68
  #### Task Routing
74
69
 
@@ -85,16 +80,14 @@ When composing task descriptions: include the repo URL (if applicable), specific
85
80
  For follow-up tasks that should continue from previous work, pass \`parentTaskId\` with the previous task's ID:
86
81
  - Worker resumes the parent's Claude session (full conversation context preserved)
87
82
  - Child task is auto-routed to the same worker (session data is local)
88
- - Slack metadata (channelId, threadTs, userId) auto-inherits
89
83
 
90
84
  If you explicitly assign to a different worker, session resume gracefully falls back to a fresh session.
91
85
 
92
- #### Follow-Up Tasks & Slack
86
+ #### Follow-Up Tasks
93
87
 
94
88
  When a worker completes or fails a task, you receive an automatic follow-up task. Handle it by:
95
89
  1. Review the output/failure reason
96
- 2. If the task has Slack metadata, use \`slack-reply\` with the task's ID to post the result back to the originating thread
97
- 3. Complete this task. Do NOT re-delegate or create new worker tasks from a follow-up \u2014 the worker's result IS the answer. Only escalate to the stakeholder if the worker explicitly failed and the failure needs human attention.
90
+ 2. Complete this task. Do NOT re-delegate or create new worker tasks from a follow-up \u2014 the worker's result IS the answer. Only escalate to the stakeholder if the worker explicitly failed and the failure needs human attention.
98
91
 
99
92
  #### Heartbeat Checklist
100
93
 
@@ -106,7 +99,6 @@ The system reads your \`/workspace/HEARTBEAT.md\` every 30 minutes. If it has co
106
99
 
107
100
  **Example standing orders:**
108
101
  \`\`\`markdown
109
- - Check Slack for unaddressed requests older than 1 hour
110
102
  - Review active tasks for any that seem stuck or need follow-up
111
103
  - If idle workers exist and unassigned tasks are available, investigate why
112
104
  \`\`\`
@@ -122,6 +114,28 @@ The system reads your \`/workspace/HEARTBEAT.md\` every 30 minutes. If it has co
122
114
  category: "system",
123
115
  });
124
116
 
117
+ registerTemplate({
118
+ eventType: "system.agent.slack",
119
+ header: "",
120
+ defaultBody: `
121
+ #### Slack Tools
122
+
123
+ - \`slack-reply\`: Reply to user in the Slack thread (use taskId for context)
124
+ - \`slack-read\`: Read thread/channel history (use taskId or channelId)
125
+ - \`slack-list-channels\`: Discover available Slack channels
126
+
127
+ **Slack User Registration:** When a task arrives from an unknown user (no \`requestedByUserId\`) with Slack metadata, use the \`manage-user\` tool to register them before proceeding. Resolve their identity from the Slack metadata (user ID, display name) attached to the task.
128
+
129
+ **Slack context inheritance:** For follow-up tasks using \`parentTaskId\`, Slack metadata (channelId, threadTs, userId) auto-inherits.
130
+
131
+ **Slack follow-up tasks:** When a worker completes or fails a task that has Slack metadata, use \`slack-reply\` with the task's ID to post the result back to the originating thread before completing the follow-up task.
132
+
133
+ **Slack standing orders:** If you maintain heartbeat standing orders, check Slack for unaddressed requests older than 1 hour when appropriate.
134
+ `,
135
+ variables: [],
136
+ category: "system",
137
+ });
138
+
125
139
  registerTemplate({
126
140
  eventType: "system.agent.worker",
127
141
  header: "",