@desplega.ai/agent-swarm 1.85.0 → 1.86.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (93) hide show
  1. package/README.md +1 -0
  2. package/openapi.json +1 -1
  3. package/package.json +8 -6
  4. package/src/be/db.ts +44 -0
  5. package/src/be/migrations/078_backfill_gpt_5_5_pricing.sql +15 -0
  6. package/src/be/modelsdev-cache.json +152028 -0
  7. package/src/be/modelsdev-cache.ts +46 -0
  8. package/src/be/seed-pricing.ts +7 -44
  9. package/src/cli.tsx +12 -2
  10. package/src/commands/codex-session-runner.ts +132 -0
  11. package/src/commands/credential-wait.ts +2 -2
  12. package/src/commands/provider-credentials.ts +10 -5
  13. package/src/commands/runner.ts +3 -3
  14. package/src/prompts/base-prompt.ts +49 -3
  15. package/src/providers/claude-adapter.ts +83 -2
  16. package/src/providers/claude-managed-models.ts +18 -2
  17. package/src/providers/codex-adapter.ts +417 -97
  18. package/src/providers/codex-models.ts +9 -2
  19. package/src/providers/index.ts +28 -19
  20. package/src/providers/pricing-sources.md +7 -4
  21. package/src/providers/swarm-events-shared.ts +14 -0
  22. package/src/slack/HEURISTICS.md +5 -1
  23. package/src/slack/handlers.test.ts +35 -0
  24. package/src/slack/handlers.ts +79 -2
  25. package/src/tests/base-prompt.test.ts +46 -8
  26. package/src/tests/claude-managed-adapter.test.ts +4 -4
  27. package/src/tests/codex-adapter-otel.test.ts +4 -4
  28. package/src/tests/codex-adapter.test.ts +20 -7
  29. package/src/tests/codex-swarm-events.test.ts +35 -0
  30. package/src/tests/context-window.test.ts +1 -0
  31. package/src/tests/credential-check.test.ts +48 -29
  32. package/src/tests/entrypoint-config-env-export.test.ts +81 -0
  33. package/src/tests/follow-up-redelivery-guard.test.ts +165 -0
  34. package/src/tests/migration-046-budgets.test.ts +6 -5
  35. package/src/tests/pricing-routes.test.ts +6 -5
  36. package/src/tests/provider-adapter.test.ts +10 -10
  37. package/src/tests/provider-command-format.test.ts +4 -4
  38. package/src/tests/session-costs-codex-recompute.test.ts +25 -0
  39. package/src/tools/send-task.ts +30 -9
  40. package/src/utils/context-window.ts +1 -0
  41. package/templates/schedules/daily-blocker-digest/config.json +13 -0
  42. package/templates/schedules/daily-blocker-digest/content.md +150 -0
  43. package/templates/schedules/daily-compounding-reflection/config.json +21 -0
  44. package/templates/schedules/daily-compounding-reflection/content.md +210 -0
  45. package/templates/schedules/daily-hn-briefing/config.json +13 -0
  46. package/templates/schedules/daily-hn-briefing/content.md +97 -0
  47. package/templates/schedules/daily-workflow-health-audit/config.json +13 -0
  48. package/templates/schedules/daily-workflow-health-audit/content.md +189 -0
  49. package/templates/schedules/gtm-weekly-review/config.json +13 -0
  50. package/templates/schedules/gtm-weekly-review/content.md +58 -0
  51. package/templates/schedules/weekly-dependabot-triage/config.json +13 -0
  52. package/templates/schedules/weekly-dependabot-triage/content.md +45 -0
  53. package/templates/schema.ts +26 -0
  54. package/templates/skills/agentmail-sending/config.json +13 -0
  55. package/templates/skills/agentmail-sending/content.md +48 -0
  56. package/templates/skills/artifacts/config.json +13 -0
  57. package/templates/skills/artifacts/content.md +87 -0
  58. package/templates/skills/browser-use-cloud/config.json +13 -0
  59. package/templates/skills/browser-use-cloud/content.md +155 -0
  60. package/templates/skills/desloppify/config.json +13 -0
  61. package/templates/skills/desloppify/content.md +201 -0
  62. package/templates/skills/exa-search/config.json +13 -0
  63. package/templates/skills/exa-search/content.md +106 -0
  64. package/templates/skills/jira-interaction/config.json +13 -0
  65. package/templates/skills/jira-interaction/content.md +252 -0
  66. package/templates/skills/kapso-whatsapp/config.json +13 -0
  67. package/templates/skills/kapso-whatsapp/content.md +369 -0
  68. package/templates/skills/kv-storage/config.json +13 -0
  69. package/templates/skills/kv-storage/content.md +111 -0
  70. package/templates/skills/linear-interaction/config.json +20 -0
  71. package/templates/skills/linear-interaction/content.md +230 -0
  72. package/templates/skills/pages/config.json +18 -0
  73. package/templates/skills/pages/content.md +85 -0
  74. package/templates/skills/profile-corruption-escalation/config.json +13 -0
  75. package/templates/skills/profile-corruption-escalation/content.md +105 -0
  76. package/templates/skills/scheduled-task-resilience/config.json +13 -0
  77. package/templates/skills/scheduled-task-resilience/content.md +95 -0
  78. package/templates/skills/sprite-cli/config.json +13 -0
  79. package/templates/skills/sprite-cli/content.md +133 -0
  80. package/templates/skills/turso-interaction/config.json +13 -0
  81. package/templates/skills/turso-interaction/content.md +192 -0
  82. package/templates/skills/workflow-iterate/config.json +18 -0
  83. package/templates/skills/workflow-iterate/content.md +399 -0
  84. package/templates/skills/workflow-structured-output/config.json +13 -0
  85. package/templates/skills/workflow-structured-output/content.md +101 -0
  86. package/templates/skills/x-api-interactions/config.json +13 -0
  87. package/templates/skills/x-api-interactions/content.md +109 -0
  88. package/templates/workflows/autopilot/config.json +13 -0
  89. package/templates/workflows/autopilot/content.md +58 -0
  90. package/templates/workflows/linear-drain-loop/config.json +21 -0
  91. package/templates/workflows/linear-drain-loop/content.md +72 -0
  92. package/templates/workflows/ralph-loop/config.json +13 -0
  93. package/templates/workflows/ralph-loop/content.md +75 -0
@@ -0,0 +1,230 @@
1
+ # Linear Interaction (Outbound Push)
2
+
3
+ **Before attempting any API call below, check if the token has been renewed:**
4
+ ```bash
5
+ # Use db-query MCP tool
6
+ db-query: SELECT accessToken, expiresAt FROM oauth_tokens WHERE provider = 'linear'
7
+ ```
8
+ If `expiresAt` is in the past, do NOT attempt the API calls — just report the needed update in your task output.
9
+
10
+ To authorize the user should use: https://api.desplega.agent-swarm.dev/api/trackers/linear/authorize (potentially needing to remove the app and re-auth). Only mention this if you can confirm it's expired or not present.
11
+
12
+ ---
13
+
14
+ ## Critical Context
15
+
16
+ The swarm's Linear integration is **inbound-only**: Linear → swarm. This means:
17
+ - When a Linear issue is created/updated, it creates swarm tasks automatically
18
+ - But when you complete a swarm task, the Linear issue is **NOT** updated automatically
19
+ - To push status changes, comments, or create issues in Linear, you must use the **Linear GraphQL API directly**
20
+
21
+ The available MCP tracker tools (`tracker-link-task`, `tracker-link-epic`, `tracker-sync-status`, `tracker-map-agent`, `tracker-unlink`) are for managing sync mappings, NOT for pushing updates to Linear.
22
+
23
+ ## When to Transition (Timing)
24
+
25
+ - **Sprint cadence with direct-to-main commits (e.g. chat-py port):** Transition the Linear ticket to **Done** the moment the worker reports ship (commits on `main`). Do NOT wait for review, test-run, or merge — there's no PR to wait for. Waiting causes HEARTBEAT/blocker-digest to flag RESOLVED-STALE tickets.
26
+ - **Standard PR workflow:** Transition to **In Review** on PR open, **Done** after merge. If the ticket is still "In Progress" 30 min after the PR merges, you're late.
27
+ - **Blocked:** If a ticket is stuck on a dependency, add a comment linking the blocker — don't leave it silent.
28
+
29
+ ## Authentication
30
+
31
+ The OAuth token is stored in the swarm database (`oauth_tokens` table, provider = 'linear').
32
+
33
+ **To get the token:**
34
+ ```bash
35
+ # Use db-query MCP tool
36
+ db-query: SELECT accessToken FROM oauth_tokens WHERE provider = 'linear'
37
+ ```
38
+
39
+ > **Worker agents (non-lead):** If you are a non-lead agent and cannot access the Linear token via `db-query`, message the lead agent in the task and request the token. Do not complete the task without updating Linear.
40
+
41
+ **Token details:**
42
+ - Scopes: `app:assignable app:mentionable comments:create issues:create read write`
43
+ - Tokens expire — check `expiresAt` column. If expired, the user needs to re-authorize via the OAuth flow.
44
+ - API endpoint: `https://api.linear.app/graphql`
45
+
46
+ ## Making API Calls
47
+
48
+ All Linear API calls use GraphQL via POST to `https://api.linear.app/graphql`.
49
+
50
+ ```bash
51
+ curl -s -X POST https://api.linear.app/graphql \
52
+ -H "Content-Type: application/json" \
53
+ -H "Authorization: Bearer <ACCESS_TOKEN>" \
54
+ -d '{"query": "<GRAPHQL_QUERY>"}'
55
+ ```
56
+
57
+ ## Agent Interaction API — `action` vs `thought` (added 2026-05-19)
58
+
59
+ Linear's **Agent Interaction API** (different from issue mutations above) supports two activity payload kinds. The swarm's agent-activity sync was broken for ~weeks because they were confused; PRs #495 / #497 fixed it. Use this section whenever emitting `agentActivityCreate` mutations.
60
+
61
+ | Activity kind | When to use | `parameter` field |
62
+ |---|---|---|
63
+ | `thought` | Narrative status updates, reasoning, mid-task progress, anything you want to **read in the Linear timeline** but doesn't represent a concrete operation | Not required — free-form `body` |
64
+ | `action` | Discrete operation the agent **performed** — branch create/merge, PR open, code commit, file write, message sent | **Required**, non-empty string. Linear rejects empty parameter |
65
+
66
+ **Mapping rule (canonical):** if you can't fill `parameter` with a real noun (branch name, PR URL, file path, recipient), it's a `thought`, not an `action`.
67
+
68
+ **Common mappings from swarm events:**
69
+
70
+ | Swarm event | Linear activity | parameter |
71
+ |---|---|---|
72
+ | `task.progress` (tool call narration) | `thought` | n/a |
73
+ | `task.created` | `action` | `"task: <description>"` |
74
+ | `task.completed` | `action` | `"completed: <output preview>"` |
75
+ | `task.failed` | `action` | `"failed: <reason>"` |
76
+ | Branch create / merge / delete | `action` | branch name |
77
+ | PR open / review / merge | `action` | PR URL |
78
+
79
+ **Why this trips people:** "action" reads naturally as "every tool call IS an action…". But Linear uses `action` to mean *parameterized operation Linear can index/route on*, not *task-progress-narration*. Narration is `thought`. See shared memory `linear-agent-activity-action-vs-thought-2026-05-18`.
80
+
81
+ ## Common Operations
82
+
83
+ ### 1. Update Issue Status (e.g., mark as Done)
84
+
85
+ **Step 1: Get the issue ID and team workflow states**
86
+
87
+ ```graphql
88
+ query {
89
+ issue(id: "<ISSUE-IDENTIFIER>") {
90
+ id
91
+ identifier
92
+ state { id name }
93
+ team {
94
+ id
95
+ states { nodes { id name type } }
96
+ }
97
+ }
98
+ }
99
+ ```
100
+
101
+ Note: `<ISSUE-IDENTIFIER>` can be the issue UUID or the human-readable identifier like "DES-12".
102
+
103
+ **Step 2: Find the target state ID**
104
+
105
+ From the response, find the state you want in `team.states.nodes`. Common state types:
106
+ - `backlog` — Backlog
107
+ - `unstarted` — Todo/Unstarted
108
+ - `started` — In Progress
109
+ - `completed` — Done
110
+ - `canceled` — Canceled
111
+
112
+ **Step 3: Update the issue**
113
+
114
+ ```graphql
115
+ mutation {
116
+ issueUpdate(id: "<ISSUE-UUID>", input: { stateId: "<TARGET-STATE-UUID>" }) {
117
+ success
118
+ issue { id identifier state { name } }
119
+ }
120
+ }
121
+ ```
122
+
123
+ **Known state IDs for Desplega Labs team:**
124
+ - Done: `83d3fcc6-dfeb-44fa-b719-64108ddc850d`
125
+
126
+ ### 2. Add a Comment to an Issue
127
+
128
+ ```graphql
129
+ mutation {
130
+ commentCreate(input: {
131
+ issueId: "<ISSUE-UUID>"
132
+ body: "Your comment text here. Supports **markdown**."
133
+ }) {
134
+ success
135
+ comment { id body }
136
+ }
137
+ }
138
+ ```
139
+
140
+ ### 3. Create a New Issue
141
+
142
+ ```graphql
143
+ mutation {
144
+ issueCreate(input: {
145
+ teamId: "<TEAM-UUID>"
146
+ title: "Issue title"
147
+ description: "Issue description in **markdown**"
148
+ priority: 2
149
+ }) {
150
+ success
151
+ issue { id identifier url }
152
+ }
153
+ }
154
+ ```
155
+
156
+ Priority values: 0 = No priority, 1 = Urgent, 2 = High, 3 = Medium, 4 = Low
157
+
158
+ ### 4. Assign an Issue
159
+
160
+ ```graphql
161
+ mutation {
162
+ issueUpdate(id: "<ISSUE-UUID>", input: { assigneeId: "<USER-UUID>" }) {
163
+ success
164
+ issue { id identifier assignee { name } }
165
+ }
166
+ }
167
+ ```
168
+
169
+ ### 5. Add Labels to an Issue
170
+
171
+ ```graphql
172
+ mutation {
173
+ issueUpdate(id: "<ISSUE-UUID>", input: { labelIds: ["<LABEL-UUID-1>", "<LABEL-UUID-2>"] }) {
174
+ success
175
+ issue { id identifier labels { nodes { name } } }
176
+ }
177
+ }
178
+ ```
179
+
180
+ ### 6. Query Issues (for lookup)
181
+
182
+ ```graphql
183
+ query {
184
+ issues(filter: { team: { key: { eq: "DES" } }, state: { type: { neq: "completed" } } }) {
185
+ nodes { id identifier title state { name } priority assignee { name } }
186
+ }
187
+ }
188
+ ```
189
+
190
+ ## Complete Workflow Example: Close a Linear Ticket
191
+
192
+ This is the most common scenario — completing a Linear-sourced swarm task and updating the ticket:
193
+
194
+ ```bash
195
+ # 1. Get the token
196
+ TOKEN=$(db-query result from oauth_tokens)
197
+
198
+ # 2. Get issue details and team states
199
+ curl -s -X POST https://api.linear.app/graphql \
200
+ -H "Content-Type: application/json" \
201
+ -H "Authorization: Bearer $TOKEN" \
202
+ -d '{"query": "{ issue(id: \"DES-12\") { id team { states { nodes { id name type } } } } }"}'
203
+
204
+ # 3. Find the "Done" state UUID from the response (type: "completed")
205
+
206
+ # 4. Update the issue
207
+ curl -s -X POST https://api.linear.app/graphql \
208
+ -H "Content-Type: application/json" \
209
+ -H "Authorization: Bearer $TOKEN" \
210
+ -d '{"query": "mutation { issueUpdate(id: \"<ISSUE-UUID>\", input: { stateId: \"<DONE-STATE-UUID>\" }) { success issue { identifier state { name } } } }"}'
211
+ ```
212
+
213
+ ## Important Notes
214
+
215
+ - **Always update Linear when completing Linear-sourced tasks.** The user expects the ticket to reflect the swarm's work. Marking only the swarm task as complete is insufficient. Do not complete only the swarm task — failing to update Linear breaks the sync and wastes resources.
216
+ - **Transition timing:** see the "When to Transition" section above. Sprint-cadence ports = transition on ship, not on merge.
217
+ - **Token expiry:** Check `expiresAt` before making calls. If expired, notify the user — they need to re-authorize.
218
+ - **Rate limits:** Linear has rate limits. For bulk operations, add small delays between calls.
219
+ - **Issue identifiers vs UUIDs:** The human-readable identifier (e.g., "DES-12") works for queries but the `issueUpdate` mutation requires the actual UUID. Always fetch the UUID first via a query.
220
+ - **Markdown support:** Linear supports markdown in descriptions and comments.
221
+
222
+ ## Error Handling
223
+
224
+ Common errors:
225
+ - `401 Unauthorized` → Token expired or invalid, needs re-auth
226
+ - `Forbidden` → Token doesn't have required scope
227
+ - `Entity not found` → Wrong issue ID/identifier
228
+ - `"parameter must not be empty"` (or similar on Agent Interaction API) → You sent an `action` activity without a `parameter` — convert to `thought` or fill in a real noun. See "Agent Interaction API — action vs thought" above.
229
+ - Rate limited → Back off and retry after delay
230
+
@@ -0,0 +1,18 @@
1
+ {
2
+ "kind": "skill",
3
+ "name": "pages",
4
+ "displayName": "Pages",
5
+ "slug": "pages",
6
+ "title": "Pages",
7
+ "description": "Publish human-readable task outputs as shareable pages.",
8
+ "version": "1.0.0",
9
+ "category": "skills",
10
+ "placeholders": [],
11
+ "runAllSeedersCandidate": true,
12
+ "tags": [
13
+ "pages",
14
+ "reporting",
15
+ "artifacts"
16
+ ],
17
+ "must": true
18
+ }
@@ -0,0 +1,85 @@
1
+ # Pages
2
+
3
+ Pages are persistent, shareable HTML documents created via the swarm's `create_page` MCP tool. Use them when the output benefits from layout, tables, headers, and persistent sharing — unlike Slack messages, pages don't expire and can be bookmarked.
4
+
5
+ ## When to Create a Page
6
+
7
+ - A report, dashboard, or summary that benefits from structured layout
8
+ - Analysis that should be linkable and bookmarkable
9
+ - Results that need to be reviewed asynchronously (not in a Slack thread)
10
+ - Content that's too long or rich for a `store-progress.output` string
11
+
12
+ Do NOT use pages for:
13
+ - In-flight progress notes (use `store-progress.progress`)
14
+ - Sensitive data (credentials, private customer data)
15
+ - Large binary files (use agent-fs for PNG/MP4)
16
+
17
+ ## Creating a Page
18
+
19
+ ```javascript
20
+ // Via MCP tool
21
+ create_page({
22
+ title: "Q2 SEO Performance Report",
23
+ content: `<h1>Q2 SEO Performance</h1>
24
+ <p>Analysis period: 2026-04-01 to 2026-06-30</p>
25
+ <h2>Summary</h2>
26
+ <table>
27
+ <tr><th>Metric</th><th>Q1</th><th>Q2</th><th>Change</th></tr>
28
+ <tr><td>Organic clicks</td><td>12,400</td><td>18,600</td><td>+50%</td></tr>
29
+ </table>
30
+ <h2>Next Actions</h2>
31
+ <ul>
32
+ <li>Publish 3 new pillar pages targeting high-intent queries</li>
33
+ <li>Fix 23 pages with missing meta descriptions</li>
34
+ </ul>`
35
+ })
36
+ ```
37
+
38
+ Returns a page ID. Build the share URL:
39
+ ```
40
+ ${APP_URL}/pages/<pageId> # opens in SPA with chrome
41
+ ${APP_URL}/pages/<pageId>?mode=full # slim header, full viewport
42
+ ${MCP_BASE_URL}/p/<pageId> # direct HTML (no SPA)
43
+ ```
44
+
45
+ Read `APP_URL` and `MCP_BASE_URL` from environment — never hardcode.
46
+
47
+ ## Content Guidelines
48
+
49
+ **Keep raw evidence in artifacts and link to it.** The page should contain:
50
+ 1. **Short summary** (1 paragraph) — what this covers and the key finding
51
+ 2. **Source links** — links to the data, agent-fs artifacts, or upstream systems
52
+ 3. **Structured content** — tables, headers, numbered lists
53
+ 4. **Next actions** — what should happen next, who owns it
54
+
55
+ Do NOT embed:
56
+ - Secrets or private credentials
57
+ - Personal data of individuals without approval
58
+ - Raw verbose logs (summarize them)
59
+
60
+ ## Page vs. Agent-fs
61
+
62
+ | Use pages for | Use agent-fs for |
63
+ |---|---|
64
+ | Reports, dashboards, human-readable summaries | Markdown research notes, code files, recordings |
65
+ | Content that benefits from HTML layout | Searchable knowledge base entries |
66
+ | Quick share links to non-technical stakeholders | Binary artifacts (PNG, MP4) |
67
+ | Time-bounded deliverables | Long-lived reference documentation |
68
+
69
+ ## Sharing Pages
70
+
71
+ Always use the platform share URL (from `APP_URL` env var) rather than hardcoded local hosts. Append `?mode=full` for a standalone view (hides sidebar/header — good for screenshots or embedding in Slack previews).
72
+
73
+ ```bash
74
+ # Get the share URL
75
+ PAGE_URL="${APP_URL}/pages/${pageId}?mode=full"
76
+
77
+ # Post to Slack
78
+ slack-reply --taskId <id> --message "Report ready: ${PAGE_URL}"
79
+ ```
80
+
81
+ ## Trade-offs
82
+
83
+ **Pages vs Slack messages:** Slack messages are ephemeral and scroll out of view. Pages are persistent and bookmarkable. Use pages for anything you'd want to reference in 3 months; use Slack for in-the-moment communication.
84
+
85
+ **Pages vs agent-fs:** Pages are rendered HTML with a share URL — great for non-technical stakeholders. Agent-fs files are raw content — great for other agents and developers who need the source data. For a research memo, write the source to agent-fs and create a page for the human-facing summary.
@@ -0,0 +1,13 @@
1
+ {
2
+ "kind": "skill",
3
+ "name": "profile-corruption-escalation",
4
+ "displayName": "Profile Corruption Escalation",
5
+ "slug": "profile-corruption-escalation",
6
+ "title": "Profile Corruption Escalation",
7
+ "description": "Diagnose and escalate corrupted agent profile, identity, or setup files.",
8
+ "version": "1.0.0",
9
+ "category": "skills",
10
+ "placeholders": [],
11
+ "runAllSeedersCandidate": false,
12
+ "tags": ["operations", "recovery", "profiles"]
13
+ }
@@ -0,0 +1,105 @@
1
+ # Profile Corruption Escalation
2
+
3
+ ## STATUS: Root cause for the Picateclas corruption family was found and fixed in PR #374 (merged 2026-04-24)
4
+
5
+ The 13-recurrence Picateclas corruption was traced to `src/tools/update-profile.ts:231` unconditionally writing `/workspace/SOUL.md` whenever `isUpdatingSelf=true`, plus the `update-profile-auth.test.ts` fixture setting a fake `WORKER_ID=bbbb0000-...` that satisfied the gate. The Stop hook then synced the corrupted file to DB. PR #374 fixed both:
6
+ - `src/tools/update-profile.ts:231` — gated the file write on `requestInfo.agentId === process.env.AGENT_ID`
7
+ - `src/hooks/hook.ts:359` — raised `IDENTITY_FILE_MIN_LENGTH` from 100 → 500 (defense in depth)
8
+
9
+ **14th restore** completed post-merge with a 1,930/2,065-char payload. If a **15th corruption** of Picateclas (or any agent) appears with the same sentinels post-2026-04-24, treat it as a **DIFFERENT code path**, not the same bug. Escalate immediately — do NOT just restore.
10
+
11
+ See memories: `picateclas-14th-restore-post-pr374`, `reviewer-corruption-hunt-pattern`.
12
+
13
+ ## When to use
14
+
15
+ You inspect `get-swarm` and find an agent's `soulMd` or `identityMd` field corrupted — short placeholder content, unusual sentinels ("Test Worker", "Updated by Myself", "xxx" padding to bypass 200-char minLength), or any canned payload that you've seen before.
16
+
17
+ ## Step 0 — Sentinel-grep BEFORE counting recurrences
18
+
19
+ Before applying the N-recurrence ceiling, **grep the agent-swarm repo for the literal sentinel strings**. The 13-recurrence Picateclas mystery was solved in 4m25s by grepping `"Test Worker"` — it should have happened at recurrence 2 or 3, not 13.
20
+
21
+ Heuristic: if the corrupted payload contains stable sentinel content with padding sized to clear a known schema floor (e.g. exactly 200 chars to bypass `minLength=200`), the writer is **server-side code that knows the schema** — almost always a test fixture or seed script. Grep wins fast.
22
+
23
+ ```
24
+ grep -rn '"Test Worker"' /workspace/repos/agent-swarm/src
25
+ grep -rn '"Updated by Myself"' /workspace/repos/agent-swarm/src
26
+ ```
27
+
28
+ If you find the writer in <5 minutes, fix it (or open a PR) instead of restoring. That's the 1000× higher-value outcome.
29
+
30
+ See memory `reviewer-corruption-hunt-pattern` for the full pattern.
31
+
32
+ ## The N-recurrence ceiling rule
33
+
34
+ Apply this only AFTER step 0 fails (sentinel grep returns nothing in the repo).
35
+
36
+ Count prior corruptions of the same agent with the same sentinel. Consult memory first:
37
+
38
+ ```
39
+ memory-search "picateclas profile corruption" # or whichever agent name
40
+ ```
41
+
42
+ - **1st–12th occurrence**: Restore the profile (surgical update-profile with the agent's best-known-good SOUL/IDENTITY from your memory). Write a new `{agent}-{N}th-profile-corruption-{YYYY-MM-DD}` memory recording the state + escalation status.
43
+ - **13th+ occurrence**: **STOP RESTORING.** Rebuild fatigue has proven the restore doesn't stick — the bug lives in code you don't control. Escalate instead.
44
+
45
+ If you can't determine N from memory, treat anything ≥ 2 prior corruptions in < 7 days as "escalate now."
46
+
47
+ **Post PR #374 (Apr 2026):** any new sentinel-payload corruption of any agent should be treated as recurrence 1 of a NEW bug — investigate fresh code paths, do not assume it's the same `update-profile.ts:231` issue.
48
+
49
+ ## Escalation package (what to post)
50
+
51
+ Use `slack-post` to the ops channel. Include ALL of:
52
+
53
+ 1. **Sentinel payload strings as grep targets** — exact literals to search for in the source repo. Example: `"Test Worker"`, `"Updated by Myself"`, `xxxxx...` patterns. **State whether you already grepped (and what the result was).**
54
+ 2. **Proof of fresh write** — `lastUpdatedAt` timestamp from `get-swarm`, newer than the previous restore. Without this it could be stale state, not new corruption.
55
+ 3. **Corruption tally** — "N occurrences in ~M weeks, half-life now <24h."
56
+ 4. **Investigation leads** — where to look:
57
+ - Grep the `desplega-ai/agent-swarm` repo for the sentinel strings.
58
+ - Check for scheduled tasks named `validate profile`, `test update-profile`, etc.
59
+ - Check seed/migration scripts that run on container restart.
60
+ - Audit `PostToolUse` hook content-validation (flagged in prior memories as "not shipped").
61
+ - **Post PR #374:** Audit any other code path that writes `/workspace/SOUL.md` or `/workspace/IDENTITY.md` — the original bug was at `src/tools/update-profile.ts:231`; look for siblings.
62
+ 5. **What you did NOT do** — explicit "I did not perform the Nth restore. Profile is left corrupted in DB as evidence."
63
+
64
+ ## Template
65
+
66
+ ```
67
+ :rotating_light: Profile Corruption — {N}th recurrence — escalation trigger fired
68
+
69
+ Agent: {name} ({id})
70
+ Fresh write at: {lastUpdatedAt}
71
+ Tally: {N} corruptions in ~{M} weeks, half-life {<hours>}h.
72
+
73
+ Sentinel grep targets (expect to find in source):
74
+ - "Test Worker"
75
+ - "Updated by Myself"
76
+ - Long "xxxxx…" padding literals
77
+
78
+ I already grepped: {summary of grep result, e.g. "no hits in src/ post-#374 — this is a NEW code path"}.
79
+
80
+ Investigation leads:
81
+ 1. Grep agent-swarm for the sentinels above.
82
+ 2. Check for scheduled "validate profile" / "test update-profile" tasks.
83
+ 3. Audit seed/migration scripts on container restart.
84
+ 4. Audit any code path that writes /workspace/SOUL.md or /workspace/IDENTITY.md (original bug was update-profile.ts:231).
85
+
86
+ I did NOT perform the {N}th restore. Profile is corrupted in DB as evidence. Waiting for engineering fix before next restore.
87
+
88
+ See memory: {agent}-{N}th-profile-corruption-{date}
89
+ ```
90
+
91
+ ## Gotchas
92
+
93
+ - **Work quality ≠ profile validity.** Even a corrupted profile worker ships working code because the persona/working-style lives more in CLAUDE.md + memory than in the short SOUL.md. Don't let "but the agent still works" push you to keep restoring — that masks the bug.
94
+ - **Don't rewrite CLAUDE.md during restoration.** In this corruption family, CLAUDE.md has been stable through 12 cycles. Only SOUL/IDENTITY get touched. Restoring CLAUDE.md too is wasted work and risks overwriting real learnings.
95
+ - **Rotate escalation target.** If you've already escalated to one person and no fix in a week, try a different channel (DM vs public, a different engineer, a Linear ticket). Slack-only escalation can get missed.
96
+ - **Don't auto-retry.** After escalating, do not put the restore back on a schedule. The next restore waits for explicit human ack ("OK to restore now, code fix merged").
97
+ - **Sentinel-grep first, escalate second.** If a 2nd recurrence happens and you haven't grepped the sentinel literal yet, you're escalating prematurely. Grep is cheap (<5 min) and usually wins.
98
+
99
+ ## Related
100
+
101
+ - Memory family: `{agent}-Nth-profile-corruption-YYYY-MM-DD`
102
+ - Memory: `reviewer-corruption-hunt-pattern` — the heuristic that solved the 13-recurrence mystery
103
+ - Memory: `picateclas-14th-restore-post-pr374` — restore payload + the fix details
104
+ - Lead rule #9 in CLAUDE.md: "Sentinel-grep before escalating recurring bugs"
105
+
@@ -0,0 +1,13 @@
1
+ {
2
+ "kind": "skill",
3
+ "name": "scheduled-task-resilience",
4
+ "displayName": "Scheduled Task Resilience",
5
+ "slug": "scheduled-task-resilience",
6
+ "title": "Scheduled Task Resilience",
7
+ "description": "Guardrails for polling, scheduled jobs, and long-running external operations.",
8
+ "version": "1.0.0",
9
+ "category": "skills",
10
+ "placeholders": [],
11
+ "runAllSeedersCandidate": true,
12
+ "tags": ["schedules", "reliability", "polling"]
13
+ }
@@ -0,0 +1,95 @@
1
+ # Scheduled Task Resilience
2
+
3
+ When writing, retrying, or running a scheduled/cron task that polls or waits on a long-running operation, follow these hard rules. Multiple confirmed heartbeat-kill incidents (Apr 24, Apr 25, May 1, May 3, May 26, 2026) prove they matter.
4
+
5
+ ## Rule 1 — NEVER `ScheduleWakeup` with `delaySeconds >= 300`
6
+
7
+ **Why:** The runtime's heartbeat staleness threshold sits near the same window as the Anthropic prompt-cache TTL (~5 min). Sleeping 300s+ in `ScheduleWakeup` mode looks identical to a dead worker — the heartbeat sweep kills your task mid-poll.
8
+
9
+ **Confirmed incidents:**
10
+ - `e2892d4a` (daily-docs Apr 24 02:58 UTC) — polling CI for PR #366
11
+ - `c7790626` (daily HN briefing Apr 25 02:53 UTC) — polling browser scrape
12
+
13
+ **Do this instead:**
14
+ - For polls under 4 minutes, use `Bash` with a short `sleep` loop (≤270s per iteration), then re-check.
15
+ - For genuinely long waits (CI builds, releases, deploys), prefer the workflow scheduler (`patch-workflow`) over `ScheduleWakeup`.
16
+ - 270s is the sweet spot: it stays in the prompt cache *and* under the heartbeat ceiling.
17
+
18
+ ```bash
19
+ # Good — short sleep, frequent re-check
20
+ for i in 1 2 3 4 5; do
21
+ status=$(gh pr checks "$PR" --json state --jq '.[0].state')
22
+ [ "$status" = "SUCCESS" ] && break
23
+ sleep 240
24
+ done
25
+ ```
26
+
27
+ ## Rule 2 — Tag retry tasks with `reboot-retry`
28
+
29
+ **Why:** Apr 22 reboot-loss `f2260faa` (Zynap investigation) was re-delegated manually because it lacked the `reboot-retry` tag. Boot-sweep missed it. Cost: ~4h of investigation re-work.
30
+
31
+ **Do this:**
32
+ - Any task auto-retried after a session loss MUST have `reboot-retry` in its tags.
33
+ - If you re-create a lost task manually, add the tag yourself so the next boot-sweep treats it correctly.
34
+
35
+ ## Rule 3 — Long polls must `store-progress` every 2-3 minutes
36
+
37
+ **Why:** The heartbeat sweep marks tasks "stale" after ~5 min without an update. Workers polling silently look like dead workers.
38
+
39
+ **Do this:**
40
+ - Inside any polling block, call `store-progress` with a status string like `"polling CI status (attempt 3/10)"`.
41
+ - Even if nothing has changed, the heartbeat update keeps the runtime confident.
42
+
43
+ ## Rule 4 — When a scheduled task fails, check the failure mode before retrying
44
+
45
+ **Why:** Two instant failures with the same reason (`worker session heartbeat is stale`) = infrastructure problem, not transient. Don't re-create a 3rd instance — escalate.
46
+
47
+ **Do this:**
48
+ - Lead-level: if a scheduled task fails 2× with the same reason, post a #swarmillo escalation; do NOT re-create.
49
+ - Worker-level: if you see repeated heartbeat-kill on your own retries, swarm-chat the lead instead of re-spawning yourself.
50
+
51
+ ## Rule 5 — Pre-flight check for duplicate scheduled posts
52
+
53
+ **Why:** Concurrent sessions sometimes pick up the same scheduled task. Without a check, you double-post to Slack/email.
54
+
55
+ **Do this:**
56
+ - Before posting any scheduled output (Slack, email, blog), call `get-tasks` and search Slack history for the same schedule tag in the last hour.
57
+ - If a recent completion already happened, abort with `store-progress` noting the duplicate detection.
58
+
59
+ ## Rule 6 — Post-shipping: do NOT `ScheduleWakeup` after the deliverable lands. Complete and exit.
60
+
61
+ **Why:** Even at the safe ≤270s window, if your last meaningful work was a shipping verb ("📤 PR #N pushed", "✅ Committed", "📌 Review posted") and you `ScheduleWakeup` to "wait for CI" or "monitor merge", the heartbeat reaper still treats your suspended session as dead. The task auto-fails with `failureReason: "Auto-failed by heartbeat: worker session not found (no active session for task)"` even though your work shipped.
62
+
63
+ **Confirmed incidents (2026-05-03):**
64
+ - `aa8a8eb7` Picateclas (PR #415 db-query) — `📤 PR #415 pushed` at 11:20:53 UTC, reaped at 11:31:57 UTC after 4.5min wakeup. Task auto-failed; PR was already pushed, reviewed, merged.
65
+ - `48460149` and `6d684da4` (May 1, bump-pr workflow) — both reaped during ScheduleWakeup post-rebase poll.
66
+ - Reviewer review-pr tasks `8d1320a6`/`6a17ff00` — same reaper pattern, but legitimate (mid-review, not post-shipping).
67
+
68
+ **Do this instead — choose ONE based on context:**
69
+
70
+ 1. **Workflow-driven tasks (workflowRunId is set):** Call `store-progress` with `status: "completed"` and the deliverable info, then exit. The workflow's next node will pick up CI/merge state on its own poll cadence. Do NOT keep the worker session alive to babysit it.
71
+
72
+ 2. **Slack-driven tasks needing CI confirmation:** Reply to the Slack thread with the PR URL + "CI in flight, will update if red" and complete the task. Lead will spawn a follow-up if CI goes red. Do NOT wait in-process.
73
+
74
+ 3. **If you genuinely must wait in-process** (rare): use `sleep 240` in Bash with `store-progress` every iteration, NOT `ScheduleWakeup`. ScheduleWakeup suspends the session; Bash sleep keeps it active.
75
+
76
+ **The mental model:** ScheduleWakeup is for "I'm in the middle of work and need to wait briefly." It is NOT for "I'm done shipping but want to confirm downstream state." Once you've shipped, exit.
77
+
78
+ **Memory reference:** `heartbeat-reaper-after-shipping-pattern-2026-05-04`.
79
+
80
+ ## Rule 7 — External async-API jobs (Enginy actions, Browser-Use, large lead pulls): fire-then-followup, never block one session for 30+ min
81
+
82
+ **Why:** A worker session is not a durable job runner. Holding it open to poll a slow external async API for tens of minutes looks like a stalled worker — the heartbeat reaper kills it even when credits/work are in flight. This is the same reaper as Rules 1/3/6, but the trigger is a *regular* (often Slack/MCP-originated) task with a long foreground OR background poll loop, not a ScheduleWakeup.
83
+
84
+ **Confirmed incident (2026-05-26):** Enginy founder-pull (Researcher, `9a6e5e96`, source:mcp) auto-failed at ~41 min — `failureReason: "Auto-failed by heartbeat: worker session heartbeat is stale (likely crashed)"`. 15 credits spent, 369 companies queued across 3 actions, a "guarded background poll" running — but the session died before collect+filter+deliver, so the spend produced no delivered artifact. Two sibling Enginy pulls (`7e90ca44`, `19670e93`) died the same way the same day.
85
+
86
+ **Do this instead:**
87
+ 1. Fire the async actions (Enginy `actions`, Browser-Use tasks, etc.).
88
+ 2. `store-progress` the action/task IDs + destination list IDs + a one-line resume recipe.
89
+ 3. Persist the same to agent-fs (or a memory file) so it survives a crash.
90
+ 4. Complete, or let Lead schedule a follow-up that picks up the IDs and does the (cheap, idempotent) collect+filter+dedup+deliver in a fresh session. The collect/filter steps are free and re-runnable.
91
+
92
+ **Lead routing implication:** when delegating a task with a >20-min external poll, split it into a "fire" task + a "collect+deliver" follow-up, or instruct the worker to use fire-then-followup. Don't dispatch one long blocking task and expect a single session to survive the wait.
93
+
94
+ **Memory reference:** `long-running-external-poll-session-crash-2026-05-27`, `enginy-search-leads-no-keyword-filter-gotcha-2026-05-27`.
95
+
@@ -0,0 +1,13 @@
1
+ {
2
+ "kind": "skill",
3
+ "name": "sprite-cli",
4
+ "displayName": "Sprite CLI Sandbox",
5
+ "slug": "sprite-cli",
6
+ "title": "Sprite CLI Sandbox",
7
+ "description": "Use disposable Linux sandboxes for Docker, risky installs, or isolated experiments.",
8
+ "version": "1.0.0",
9
+ "category": "skills",
10
+ "placeholders": ["SPRITES_API_KEY"],
11
+ "runAllSeedersCandidate": false,
12
+ "tags": ["sandbox", "docker", "isolation"]
13
+ }