@desplega.ai/agent-swarm 1.92.0 → 1.92.2

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 (90) hide show
  1. package/README.md +1 -1
  2. package/openapi.json +276 -3
  3. package/package.json +6 -6
  4. package/plugin/skills/pages/SKILL.md +5 -2
  5. package/src/be/db.ts +416 -20
  6. package/src/be/memory/boot-reembed.ts +85 -0
  7. package/src/be/memory/constants.ts +44 -2
  8. package/src/be/memory/providers/openai-embedding.ts +15 -5
  9. package/src/be/memory/providers/sqlite-store.ts +325 -76
  10. package/src/be/memory/reranker.ts +35 -17
  11. package/src/be/memory/types.ts +43 -0
  12. package/src/be/migrations/084_script_run_journal_duration.sql +5 -0
  13. package/src/be/migrations/085_script_runs_kind.sql +9 -0
  14. package/src/be/migrations/086_pages_default_authed.sql +64 -0
  15. package/src/be/migrations/087_skill_files.sql +19 -0
  16. package/src/be/modelsdev-cache.json +5622 -2543
  17. package/src/be/seed-scripts/catalog/boot-triage.ts +221 -0
  18. package/src/be/seed-scripts/catalog/catalog-report.ts +457 -0
  19. package/src/be/seed-scripts/catalog/compound-insights.ts +465 -0
  20. package/src/be/seed-scripts/catalog/gh-pr-snapshot.ts +1 -1
  21. package/src/be/seed-scripts/catalog/memory-eval.ts +1059 -0
  22. package/src/be/seed-scripts/catalog/ops-catalog-audit.ts +34 -439
  23. package/src/be/seed-scripts/catalog/schedule-health.ts +78 -2
  24. package/src/be/seed-scripts/catalog/task-failure-audit.ts +48 -1
  25. package/src/be/seed-scripts/index.ts +32 -4
  26. package/src/be/seed-skills/index.ts +0 -7
  27. package/src/be/skill-sync.ts +91 -7
  28. package/src/commands/runner.ts +6 -2
  29. package/src/heartbeat/templates.ts +20 -16
  30. package/src/http/index.ts +50 -7
  31. package/src/http/mcp-user.ts +23 -0
  32. package/src/http/mcp.ts +58 -0
  33. package/src/http/memory.ts +62 -0
  34. package/src/http/pages.ts +1 -1
  35. package/src/http/script-runs.ts +2 -0
  36. package/src/http/scripts.ts +39 -2
  37. package/src/http/skills.ts +225 -0
  38. package/src/providers/claude-adapter.ts +56 -24
  39. package/src/script-workflows/workflow-ctx.ts +7 -3
  40. package/src/scripts-runtime/sdk-allowlist.ts +1 -0
  41. package/src/scripts-runtime/swarm-sdk.ts +13 -0
  42. package/src/scripts-runtime/types/stdlib.d.ts +1 -0
  43. package/src/scripts-runtime/types/swarm-sdk.d.ts +1 -0
  44. package/src/server.ts +2 -0
  45. package/src/tasks/worker-follow-up.ts +12 -0
  46. package/src/tests/claude-adapter-binary.test.ts +135 -81
  47. package/src/tests/create-page-tool.test.ts +19 -2
  48. package/src/tests/heartbeat-checklist.test.ts +36 -0
  49. package/src/tests/mcp-transport-gc.test.ts +58 -0
  50. package/src/tests/memory-e2e.test.ts +6 -6
  51. package/src/tests/memory-health-endpoint.test.ts +78 -0
  52. package/src/tests/memory-rater-e2e.test.ts +4 -5
  53. package/src/tests/memory-reranker.test.ts +135 -124
  54. package/src/tests/memory-store.test.ts +221 -1
  55. package/src/tests/memory.test.ts +13 -12
  56. package/src/tests/pages-http.test.ts +20 -2
  57. package/src/tests/pages-storage.test.ts +26 -0
  58. package/src/tests/scripts-mcp-e2e.test.ts +53 -0
  59. package/src/tests/seed-scripts.test.ts +328 -3
  60. package/src/tests/skill-files-http.test.ts +171 -0
  61. package/src/tests/skill-files.test.ts +162 -0
  62. package/src/tests/skill-get-file-tool.test.ts +110 -0
  63. package/src/tests/skill-sync.test.ts +125 -6
  64. package/src/tests/task-cascade-fail.test.ts +304 -0
  65. package/src/tools/create-page.ts +2 -2
  66. package/src/tools/skills/index.ts +1 -0
  67. package/src/tools/skills/skill-get-file.ts +80 -0
  68. package/src/tools/tool-config.ts +2 -1
  69. package/src/types.ts +20 -0
  70. package/src/utils/internal-ai/complete-structured.ts +2 -2
  71. package/templates/schedules/daily-blocker-digest/content.md +68 -54
  72. package/templates/schedules/daily-compounding-reflection/content.md +4 -4
  73. package/templates/schedules/daily-hn-briefing/content.md +5 -5
  74. package/templates/schedules/daily-workflow-health-audit/content.md +6 -6
  75. package/templates/schedules/gtm-weekly-review/content.md +9 -9
  76. package/templates/schedules/weekly-dependabot-triage/content.md +24 -20
  77. package/templates/skills/agentmail-sending/content.md +6 -7
  78. package/templates/skills/desloppify/content.md +8 -9
  79. package/templates/skills/jira-interaction/content.md +25 -33
  80. package/templates/skills/kapso-whatsapp/content.md +29 -30
  81. package/templates/skills/linear-interaction/content.md +8 -9
  82. package/templates/skills/profile-corruption-escalation/content.md +44 -85
  83. package/templates/skills/sprite-cli/content.md +4 -5
  84. package/templates/skills/turso-interaction/content.md +14 -17
  85. package/templates/skills/workflow-iterate/content.md +38 -391
  86. package/templates/skills/x-api-interactions/content.md +4 -6
  87. package/templates/workflows/llm-safe-release-context/config.json +13 -0
  88. package/templates/workflows/llm-safe-release-context/content.md +69 -0
  89. package/templates/skills/scheduled-task-resilience/config.json +0 -14
  90. package/templates/skills/scheduled-task-resilience/content.md +0 -95
@@ -5,15 +5,15 @@ The swarm has Jira OAuth connected but **no inbound sync** (unlike Linear). Ever
5
5
  ## TL;DR — minimum knowledge
6
6
 
7
7
  1. Pull the access token from `oauth_tokens` (provider = `jira`).
8
- 2. Hit `https://api.atlassian.com/ex/jira/<CLOUD_ID>/rest/api/3/...` — *not* `desplega.atlassian.net` directly. 3LO bearer tokens only work via the `api.atlassian.com` proxy.
8
+ 2. Hit `https://api.atlassian.com/ex/jira/<CLOUD_ID>/rest/api/3/...` — not your site hostname directly. 3LO bearer tokens only work via the `api.atlassian.com` proxy.
9
9
  3. Bodies for descriptions/comments must be in **ADF** (Atlassian Document Format), not plain text or markdown.
10
10
 
11
- ## Known constants (Desplega tenant)
11
+ ## Deployment constants
12
12
 
13
- - Site: `desplega.atlassian.net`
14
- - Cloud ID: `0054e739-8d39-4f01-8d6a-431619cae8fc`
15
- - Default project: `KAN` ("Swarm")
16
- - Scopes on the stored token: `manage:jira-webhook offline_access read:jira-work read:me write:jira-work`
13
+ - Site: `<your-site>.atlassian.net`
14
+ - Cloud ID: `<cloud-id>`
15
+ - Default project: `<PROJECT>`
16
+ - Scopes on the stored token: confirm from `oauth_tokens.scope` before write operations
17
17
 
18
18
  If the cloudId ever changes, rediscover it:
19
19
  ```bash
@@ -33,7 +33,7 @@ SELECT accessToken, expiresAt, scope FROM oauth_tokens WHERE provider = 'jira';
33
33
  **Always check `expiresAt` first.** Atlassian access tokens are short-lived (~1h). If expired, do NOT keep retrying — report it. Re-auth path:
34
34
 
35
35
  ```
36
- https://api.desplega.agent-swarm.dev/api/trackers/jira/authorize
36
+ <SWARM_API_BASE_URL>/api/trackers/jira/authorize
37
37
  ```
38
38
  (User may need to remove the app and re-auth.)
39
39
 
@@ -65,7 +65,7 @@ curl -s -H "Authorization: Bearer $TOKEN" -H "Accept: application/json" \
65
65
 
66
66
  ```bash
67
67
  curl -s -H "Authorization: Bearer $TOKEN" -H "Accept: application/json" \
68
- "https://api.atlassian.com/ex/jira/$CLOUD_ID/rest/api/3/project/KAN" \
68
+ "https://api.atlassian.com/ex/jira/$CLOUD_ID/rest/api/3/project/$PROJECT_KEY" \
69
69
  | jq '{key, name, lead: .lead.displayName, issueTypes: [.issueTypes[] | {id, name, subtask}]}'
70
70
  ```
71
71
 
@@ -76,7 +76,7 @@ Use the **`/search/jql`** endpoint (the older `/search` is deprecated for cloud)
76
76
  ```bash
77
77
  curl -s -G \
78
78
  -H "Authorization: Bearer $TOKEN" -H "Accept: application/json" \
79
- --data-urlencode 'jql=project = KAN AND statusCategory != Done' \
79
+ --data-urlencode 'jql=project = <PROJECT> AND statusCategory != Done' \
80
80
  --data-urlencode 'fields=summary,status,assignee,priority' \
81
81
  "https://api.atlassian.com/ex/jira/$CLOUD_ID/rest/api/3/search/jql" \
82
82
  | jq '[.issues[] | {key, summary: .fields.summary, status: .fields.status.name, assignee: .fields.assignee.displayName}]'
@@ -92,7 +92,7 @@ curl -s -X POST \
92
92
  "https://api.atlassian.com/ex/jira/$CLOUD_ID/rest/api/3/issue" \
93
93
  -d '{
94
94
  "fields": {
95
- "project": { "key": "KAN" },
95
+ "project": { "key": "<PROJECT>" },
96
96
  "summary": "Short title",
97
97
  "issuetype": { "name": "Task" },
98
98
  "description": {
@@ -106,9 +106,9 @@ curl -s -X POST \
106
106
  }'
107
107
  ```
108
108
 
109
- Returns `{ id, key, self }` on success (HTTP 201). The `key` (e.g. `KAN-7`) is what humans use; URL is `https://desplega.atlassian.net/browse/<KEY>`.
109
+ Returns `{ id, key, self }` on success (HTTP 201). The `key` (e.g. `<PROJECT>-7`) is what humans use; URL is `https://<your-site>.atlassian.net/browse/<KEY>`.
110
110
 
111
- Available issue types in `KAN` (verify per-project): `Epic, Subtask, Task, Story, Feature, Request, Bug`.
111
+ Available issue types are project-specific; list the project before creating issues.
112
112
 
113
113
  ### 5. Transition issue status (e.g. → Done)
114
114
 
@@ -120,23 +120,15 @@ curl -s -H "Authorization: Bearer $TOKEN" -H "Accept: application/json" \
120
120
  | jq '.transitions[] | {id, name, to: .to.name}'
121
121
  ```
122
122
 
123
- For project `KAN` (verified 2026-04-27), the workflow exposes ALL transitions from any state you do not need to walk through intermediate states:
124
-
125
- | Transition ID | Target state |
126
- |---|---|
127
- | `11` | To Do |
128
- | `21` | In Progress |
129
- | `31` | In Review |
130
- | `41` | Backlog |
131
- | `51` | **Done** |
123
+ Transition IDs are project-specific. Do not copy IDs between Jira projects; discover them for the issue you are about to update.
132
124
 
133
125
  Transition (returns HTTP 204 on success, no body):
134
126
 
135
127
  ```bash
136
128
  curl -s -X POST \
137
129
  -H "Authorization: Bearer $TOKEN" -H "Accept: application/json" -H "Content-Type: application/json" \
138
- "https://api.atlassian.com/ex/jira/$CLOUD_ID/rest/api/3/issue/KAN-3/transitions" \
139
- -d '{"transition":{"id":"51"}}'
130
+ "https://api.atlassian.com/ex/jira/$CLOUD_ID/rest/api/3/issue/<KEY>/transitions" \
131
+ -d '{"transition":{"id":"<TRANSITION_ID>"}}'
140
132
  ```
141
133
 
142
134
  ### 6. Comment on an issue
@@ -146,7 +138,7 @@ ADF body again:
146
138
  ```bash
147
139
  curl -s -X POST \
148
140
  -H "Authorization: Bearer $TOKEN" -H "Accept: application/json" -H "Content-Type: application/json" \
149
- "https://api.atlassian.com/ex/jira/$CLOUD_ID/rest/api/3/issue/KAN-3/comment" \
141
+ "https://api.atlassian.com/ex/jira/$CLOUD_ID/rest/api/3/issue/<KEY>/comment" \
150
142
  -d '{
151
143
  "body": {
152
144
  "type": "doc", "version": 1,
@@ -161,7 +153,7 @@ Atlassian Cloud uses **accountId**, not username. Find one via:
161
153
 
162
154
  ```bash
163
155
  curl -s -G -H "Authorization: Bearer $TOKEN" -H "Accept: application/json" \
164
- --data-urlencode 'query=taras' \
156
+ --data-urlencode 'query=<name-or-email>' \
165
157
  "https://api.atlassian.com/ex/jira/$CLOUD_ID/rest/api/3/user/search" \
166
158
  | jq '.[] | {accountId, displayName, emailAddress}'
167
159
  ```
@@ -170,7 +162,7 @@ Assign:
170
162
  ```bash
171
163
  curl -s -X PUT \
172
164
  -H "Authorization: Bearer $TOKEN" -H "Accept: application/json" -H "Content-Type: application/json" \
173
- "https://api.atlassian.com/ex/jira/$CLOUD_ID/rest/api/3/issue/KAN-3/assignee" \
165
+ "https://api.atlassian.com/ex/jira/$CLOUD_ID/rest/api/3/issue/<KEY>/assignee" \
174
166
  -d '{"accountId":"<ACCOUNT_ID>"}'
175
167
  ```
176
168
 
@@ -181,7 +173,7 @@ To unassign: `{"accountId": null}`.
181
173
  ```bash
182
174
  curl -s -X PUT \
183
175
  -H "Authorization: Bearer $TOKEN" -H "Accept: application/json" -H "Content-Type: application/json" \
184
- "https://api.atlassian.com/ex/jira/$CLOUD_ID/rest/api/3/issue/KAN-3" \
176
+ "https://api.atlassian.com/ex/jira/$CLOUD_ID/rest/api/3/issue/<KEY>" \
185
177
  -d '{ "fields": { "summary": "New summary", "labels": ["swarm","auto"] } }'
186
178
  ```
187
179
 
@@ -204,7 +196,7 @@ If you need rich content, build it in a script — don't try to write deep ADF i
204
196
  ## Operational rules
205
197
 
206
198
  - **Token-expiry first.** Always check `expiresAt`. Don't loop on 401s.
207
- - **Use the proxy.** All authenticated calls go through `api.atlassian.com/ex/jira/<cloudId>/...`. Hitting `desplega.atlassian.net/rest/api/3/...` with a 3LO bearer token will fail.
199
+ - **Use the proxy.** All authenticated calls go through `api.atlassian.com/ex/jira/<cloudId>/...`. Hitting `<your-site>.atlassian.net/rest/api/3/...` with a 3LO bearer token will fail.
208
200
  - **Discover transitions per issue** before transitioning — different projects/workflows have different IDs.
209
201
  - **Use `/search/jql`**, not the legacy `/search` (which is deprecated and may be removed).
210
202
  - **ADF is mandatory** for `description`, `comment`, and rich text fields. Plain strings will be rejected.
@@ -226,21 +218,22 @@ If you need rich content, build it in a script — don't try to write deep ADF i
226
218
 
227
219
  ```bash
228
220
  TOKEN=$(db-query "SELECT accessToken FROM oauth_tokens WHERE provider='jira'")
229
- CLOUD_ID="0054e739-8d39-4f01-8d6a-431619cae8fc"
221
+ CLOUD_ID="<cloud-id>"
222
+ PROJECT_KEY="<PROJECT>"
230
223
 
231
224
  # 1. List open issues in KAN
232
225
  curl -s -G -H "Authorization: Bearer $TOKEN" -H "Accept: application/json" \
233
- --data-urlencode 'jql=project = KAN AND statusCategory != Done' \
226
+ --data-urlencode "jql=project = $PROJECT_KEY AND statusCategory != Done" \
234
227
  --data-urlencode 'fields=summary,status' \
235
228
  "https://api.atlassian.com/ex/jira/$CLOUD_ID/rest/api/3/search/jql" \
236
229
  | jq -r '.issues[].key' > /tmp/keys.txt
237
230
 
238
- # 2. Transition each to Done (id 51 in KAN)
231
+ # 2. Transition each to Done using the transition ID discovered for this workflow
239
232
  for KEY in $(cat /tmp/keys.txt); do
240
233
  curl -s -X POST \
241
234
  -H "Authorization: Bearer $TOKEN" -H "Accept: application/json" -H "Content-Type: application/json" \
242
235
  "https://api.atlassian.com/ex/jira/$CLOUD_ID/rest/api/3/issue/$KEY/transitions" \
243
- -d '{"transition":{"id":"51"}}'
236
+ -d '{"transition":{"id":"<DONE_TRANSITION_ID>"}}'
244
237
  sleep 0.3
245
238
  done
246
239
  ```
@@ -249,4 +242,3 @@ done
249
242
 
250
243
  - The MCP tracker tools (`tracker-link-task`, `tracker-sync-status`, etc.) are designed for two-way sync mappings. Jira tracker support exists at the schema level but is not currently wired up to inbound webhooks. Until it is, all Jira interaction must go through this skill.
251
244
  - If/when inbound Jira webhooks land, this skill should add a "When to transition" section mirroring the Linear one.
252
-
@@ -1,10 +1,10 @@
1
1
  # Kapso WhatsApp
2
2
 
3
- Kapso (https://kapso.ai) is the WhatsApp platform vendor we use for the Meta Cloud API. The swarm has one phone number provisioned and either a native inbound handler (PR #560) or a workflow wired to dispatch a task per inbound message.
3
+ Kapso (https://kapso.ai) is a WhatsApp platform vendor for the Meta Cloud API. A swarm can provision one or more phone numbers and use either native inbound handlers or workflows to dispatch a task per inbound message.
4
4
 
5
5
  ## When to use MCP tools vs this skill's REST recipes
6
6
 
7
- PR #560 ships **thin MCP-tool wrappers for the common case only**:
7
+ Some deployments expose **thin MCP-tool wrappers for the common case only**:
8
8
 
9
9
  | Tool | Use for |
10
10
  |---|---|
@@ -32,7 +32,7 @@ Swarm config keys (resolve with `get-config key:<NAME> includeSecrets:true` —
32
32
  |---|---|
33
33
  | `KAPSO_API_BASE_URL` | `https://api.kapso.ai` (host only, no `/platform/v1`) |
34
34
  | `KAPSO_API_KEY` | API key (`X-API-Key` header) |
35
- | `KAPSO_PHONE_NUMBER_ID` | `1035039933036854` — our provisioned number's Meta ID |
35
+ | `KAPSO_PHONE_NUMBER_ID` | Provisioned number's Meta ID |
36
36
  | `KAPSO_WEBHOOK_HMAC_SECRET` | Shared HMAC secret. Kapso signs every webhook request with `X-Webhook-Signature: <hex>` |
37
37
 
38
38
  The Kapso CLI is NOT installed in worker containers. Use direct HTTP or clone the `gokapso/agent-skills` repo for fallback scripts.
@@ -46,7 +46,7 @@ The Meta Cloud API is proxied at `$KAPSO_API_BASE_URL/meta/whatsapp/v24.0/...` (
46
46
 
47
47
  ## Inbound webhook payload (v2)
48
48
 
49
- The `kapso-whatsapp-inbound-demo` workflow (`a5700897-8f6b-4e1e-9e6b-dcc10e479cb5`) receives `whatsapp.message.*` and `whatsapp.conversation.*` events at `POST https://api.desplega.agent-swarm.dev/api/webhooks/<workflow-id>`.
49
+ Your inbound workflow receives `whatsapp.message.*` and `whatsapp.conversation.*` events at `POST <SWARM_API_BASE_URL>/api/webhooks/<workflow-id>`.
50
50
 
51
51
  Shape (top-level keys):
52
52
 
@@ -54,7 +54,7 @@ Shape (top-level keys):
54
54
  {
55
55
  "message": {
56
56
  "id": "wamid.HBgL...", // Meta message id (WAMID)
57
- "from": "34679077777", // E.164 without +
57
+ "from": "15550100000", // dummy E.164 without +
58
58
  "from_user_id": "ES.26772...", // Meta-internal user id
59
59
  "timestamp": "1779281573", // unix seconds (string)
60
60
  "type": "text", // text | image | audio | video | document | sticker | location | contacts | reaction | ...
@@ -71,9 +71,9 @@ Shape (top-level keys):
71
71
  },
72
72
  "conversation": {
73
73
  "id": "bd7e888e-...",
74
- "phone_number": "34679077777",
75
- "phone_number_id": "1035039933036854",
76
- "contact_name": "Taras",
74
+ "phone_number": "15550100000",
75
+ "phone_number_id": "<phone-number-id>",
76
+ "contact_name": "Example Contact",
77
77
  "status": "active",
78
78
  "last_active_at": "...",
79
79
  "created_at": "...",
@@ -86,7 +86,7 @@ Shape (top-level keys):
86
86
  }
87
87
  },
88
88
  "is_new_conversation": false,
89
- "phone_number_id": "1035039933036854"
89
+ "phone_number_id": "<phone-number-id>"
90
90
  }
91
91
  ```
92
92
 
@@ -143,7 +143,7 @@ NB: verify the exact proxy path against a real media message — the swarm has o
143
143
 
144
144
  Two paths in order:
145
145
 
146
- 1. By name: `resolve-user name:"<contact_name>"` (fuzzy substring match). Returns the canonical profile if there's one. Useful for known team members (Taras / Eze).
146
+ 1. By name: `resolve-user name:"<contact_name>"` (fuzzy substring match). Returns the canonical profile if there's one. Useful for known team members.
147
147
  2. If no match — the contact is unknown. Lead can run `manage-user create name:"<contact_name>" notes:"WhatsApp +<phone>"` to register them. Workers should NOT create users autonomously; ask Lead.
148
148
 
149
149
  Always quote the phone number in `manage-user notes` so future lookups by phone work (until we add a `phone` column to the user registry).
@@ -158,7 +158,7 @@ API_KEY=$(get-config KAPSO_API_KEY)
158
158
 
159
159
  # List conversations for our number
160
160
  curl -s -H "X-API-Key: $API_KEY" \
161
- "$API_BASE/platform/v1/whatsapp/conversations?phone_number_id=1035039933036854&status=active" | jq
161
+ "$API_BASE/platform/v1/whatsapp/conversations?phone_number_id=$KAPSO_PHONE_NUMBER_ID&status=active" | jq
162
162
 
163
163
  # Get a single conversation
164
164
  curl -s -H "X-API-Key: $API_KEY" \
@@ -183,8 +183,8 @@ Per WhatsApp policy, free-form text is only allowed within 24h of the last inbou
183
183
  **Common case shortcut:** call the `send-whatsapp-message` MCP tool — it wraps exactly this REST call. The recipe below is the canonical reference and the fallback when you need fields the tool doesn't expose.
184
184
 
185
185
  ```bash
186
- TO="34679077777"
187
- TEXT="Hi Taras 👋"
186
+ TO="15550100000"
187
+ TEXT="Hi there"
188
188
 
189
189
  curl -s -X POST -H "X-API-Key: $API_KEY" -H "Content-Type: application/json" \
190
190
  -d "{
@@ -194,7 +194,7 @@ curl -s -X POST -H "X-API-Key: $API_KEY" -H "Content-Type: application/json" \
194
194
  \"type\": \"text\",
195
195
  \"text\": { \"preview_url\": false, \"body\": \"$TEXT\" }
196
196
  }" \
197
- "$API_BASE/meta/whatsapp/v24.0/1035039933036854/messages" | jq
197
+ "$API_BASE/meta/whatsapp/v24.0/$KAPSO_PHONE_NUMBER_ID/messages" | jq
198
198
  ```
199
199
 
200
200
  Returns `{ "messages": [{ "id": "wamid..." }] }` on success. Log the wamid.
@@ -218,7 +218,7 @@ Prefer quote-replies when answering a specific question — it keeps long conver
218
218
 
219
219
  ## Sending media (image, document, audio, video)
220
220
 
221
- Two-step pipeline through Kapso's Meta proxy: **upload, then send by id**. Sending by `id` is more reliable than `link` (no public-host requirement) validated 2026-05-20.
221
+ Two-step pipeline through Kapso's Meta proxy: **upload, then send by id**. Sending by `id` is more reliable than `link` because it does not require the media to be hosted at a public URL.
222
222
 
223
223
  ### 1. Upload
224
224
 
@@ -227,7 +227,7 @@ curl -s -X POST -H "X-API-Key: $API_KEY" \
227
227
  -F "messaging_product=whatsapp" \
228
228
  -F "type=<mime>" \
229
229
  -F "file=@/path/to/file.ext;type=<mime>" \
230
- "$API_BASE/meta/whatsapp/v24.0/1035039933036854/media"
230
+ "$API_BASE/meta/whatsapp/v24.0/$KAPSO_PHONE_NUMBER_ID/media"
231
231
  # → {"id":"<media-id>"}
232
232
  ```
233
233
 
@@ -242,7 +242,7 @@ curl -s -X POST -H "X-API-Key: $API_KEY" \
242
242
 
243
243
  Quote-reply works on media too — add `"context": { "message_id": "<wamid>" }` at the top level.
244
244
 
245
- ### Wide images: pad to ~square, send as image (validated 2026-05-20)
245
+ ### Wide images: pad to ~square, send as image
246
246
 
247
247
  WhatsApp scales `type:image` to bubble width + recompresses, so a wide 1200×630 social card renders as a tiny shrunken strip. **The fix is NOT `type:document`** — a `.png` sent as a document shows a plain file card with NO inline preview (must tap+download). Bad UX both ways.
248
248
 
@@ -280,7 +280,7 @@ curl -s -X POST -H "X-API-Key: $API_KEY" -H "Content-Type: application/json" \
280
280
  "message_id": "<inbound_wamid>",
281
281
  "typing_indicator": { "type": "text" }
282
282
  }' \
283
- "$API_BASE/meta/whatsapp/v24.0/1035039933036854/messages"
283
+ "$API_BASE/meta/whatsapp/v24.0/$KAPSO_PHONE_NUMBER_ID/messages"
284
284
  # → {"success":true}
285
285
  ```
286
286
 
@@ -297,7 +297,7 @@ curl -s -X POST -H "X-API-Key: $API_KEY" -H "Content-Type: application/json" \
297
297
  "type": "reaction",
298
298
  "reaction": { "message_id": "<wamid>", "emoji": "👀" }
299
299
  }' \
300
- "$API_BASE/meta/whatsapp/v24.0/1035039933036854/messages"
300
+ "$API_BASE/meta/whatsapp/v24.0/$KAPSO_PHONE_NUMBER_ID/messages"
301
301
  ```
302
302
 
303
303
  A user can have only ONE reaction per message — sending a new emoji REPLACES the previous one (no explicit remove needed). Send `"emoji": ""` to clear a reaction entirely.
@@ -310,21 +310,21 @@ If `send-whatsapp-message` returns `sessionWindowExpired: true`, fall through to
310
310
  curl -s -X POST -H "X-API-Key: $API_KEY" -H "Content-Type: application/json" \
311
311
  -d '{
312
312
  "messaging_product": "whatsapp",
313
- "to": "34679077777",
313
+ "to": "15550100000",
314
314
  "type": "template",
315
315
  "template": {
316
316
  "name": "<template_name>",
317
317
  "language": { "code": "en_US" }
318
318
  }
319
319
  }' \
320
- "$API_BASE/meta/whatsapp/v24.0/1035039933036854/messages"
320
+ "$API_BASE/meta/whatsapp/v24.0/$KAPSO_PHONE_NUMBER_ID/messages"
321
321
  ```
322
322
 
323
- List approved templates first: `GET $API_BASE/platform/v1/whatsapp/templates?phone_number_id=1035039933036854`.
323
+ List approved templates first: `GET $API_BASE/platform/v1/whatsapp/templates?phone_number_id=$KAPSO_PHONE_NUMBER_ID`.
324
324
 
325
325
  ## Webhook signature verification
326
326
 
327
- Every Kapso webhook delivery includes `X-Webhook-Signature: <hex>` (HMAC-SHA256 of the raw body using `KAPSO_WEBHOOK_HMAC_SECRET`). The native handler (`/api/integrations/kapso/webhook`, PR #560) and the workflow webhook trigger both verify automatically the trigger's `hmacHeader` is `X-Webhook-Signature` and `hmacSecret` resolves from swarm config.
327
+ Every Kapso webhook delivery includes `X-Webhook-Signature: <hex>` (HMAC-SHA256 of the raw body using `KAPSO_WEBHOOK_HMAC_SECRET`). Native handlers and workflow webhook triggers should verify the signature automatically; if you configure a custom trigger, set its HMAC header to `X-Webhook-Signature` and resolve the secret from swarm config.
328
328
 
329
329
  To verify manually:
330
330
 
@@ -337,17 +337,17 @@ echo -n "$RAW_BODY" | openssl dgst -sha256 -hmac "$HMAC_SECRET" -hex | awk '{pri
337
337
 
338
338
  - Same language as the inbound message (Spanish/English/Catalan — match what they wrote).
339
339
  - Brief. WhatsApp is not Slack — 1-3 short messages max.
340
- - Identify yourself if it's a first interaction in the conversation: "Hi! This is the Desplega agent Taras wired me up to handle WhatsApp."
340
+ - Identify yourself if it's a first interaction in the conversation: "Hi! This is the agent handling this WhatsApp inbox."
341
341
  - Quote-reply (`context.message_id`) when answering a specific question.
342
342
  - If you can't help (no skill for the request, out of scope) — say so and either escalate to Lead or ask the human to use Slack instead.
343
343
  - Always log the outbound wamid in your task output so it's traceable.
344
344
 
345
345
  ## Where this fits in the swarm
346
346
 
347
- Two inbound paths exist (PR #560 adds the first, the original demo workflow remains):
347
+ Two common inbound paths exist:
348
348
 
349
- - **Native handler** (`/api/integrations/kapso/webhook`, PR #560) — fires for any phone number registered via `register-kapso-number`. Verifies HMAC, dedupes by message id (KV `integrations:kapso:dedupe`, 24h TTL), reads the routing mapping from KV (`integrations:kapso:numbers`), and either dispatches a `kapso-inbound` task or delegates to a workflow trigger (advanced override). Also emits a `kapso.message.received` event on the workflow event bus.
350
- - **Workflow `kapso-whatsapp-inbound-demo`** (`a5700897-8f6b-4e1e-9e6b-dcc10e479cb5`) — fires for unregistered numbers (or numbers whose mapping points at this workflow). Pipeline: `react-eyes` (mark read + typing + 👀) `debounce` (collapse rapid-fire bursts) `gate-proceed` `notify-taras` (agent-task triage) `finalize` (✅/❌ reaction).
349
+ - **Native handler** (`/api/integrations/kapso/webhook`) — fires for any phone number registered via `register-kapso-number`. Verifies HMAC, dedupes by message id, reads the routing mapping, and either dispatches an inbound-message task or delegates to a workflow trigger.
350
+ - **Workflow trigger** (`<workflow-id>`) — useful when you want custom routing, batching, triage, or approval steps before a reply is sent. A typical pipeline marks the message read, debounces rapid-fire bursts, routes to an agent task, and optionally sends a final reaction or status update.
351
351
 
352
352
  **Debounce / batching:** the demo workflow's `debounce` node waits ~8s after each message and only the LAST message of a burst proceeds to the agent task — so a user firing 3 quick messages produces ONE task, not three. The agent is told the `batchSize` and should read trailing history and answer the whole burst in one reply. When >1 messages are collapsed, the user sees a "🧵 Got your N messages" note.
353
353
 
@@ -357,13 +357,12 @@ HMAC verification is enforced (signed mode) on both paths.
357
357
 
358
358
  ## Common gotchas
359
359
 
360
- - Phone numbers from Kapso are E.164 **without `+`** (e.g. `34679077777`). Add `+` when displaying to humans, drop it when calling the API.
360
+ - Phone numbers from Kapso are E.164 **without `+`** (e.g. `15550100000`). Add `+` when displaying to humans, drop it when calling the API.
361
361
  - `message.text.body` is only present for `type:"text"`. For other types read `message.<type>` (see the table above) or `message.kapso.content` for a text representation.
362
362
  - Outbound status events (`delivered`, `read`) are NOT a customer interaction — skip them. Filter by `message.kapso.direction == "inbound"`.
363
363
  - Real inbound messages commonly arrive with `status: "delivered"` (delivered to us). Do NOT skip on status — only `direction` signals inbound vs outbound.
364
364
  - Kapso sometimes sends test payloads with `"test": true` and `wamid.TEST_*` ids. Don't reply to test payloads — just complete the task with a note.
365
- - The provisioned phone number id (`1035039933036854`) is **our** number, not the recipient's. The recipient is in `message.from` / `conversation.phone_number`.
365
+ - The provisioned phone number id is your sender number, not the recipient's. The recipient is in `message.from` / `conversation.phone_number`.
366
366
  - The message-list endpoint is `/platform/v1/whatsapp/messages?conversation_id=X` — the conversation-scoped `/conversations/<id>/messages` path 404s.
367
367
  - **Wide images shrink — pad them, don't send as document.** `type:image` scales to bubble width; a wide social card becomes a strip. Sending it as `type:document` removes the preview entirely. Fix: pad onto a ~1:1/4:5 canvas and send as `type:image`. See "Sending media".
368
368
  - **MCP tools cover text-only.** `send-whatsapp-message` and `reply-whatsapp-message` are deliberately thin — templates / media / reactions / typing / mark-as-read are NOT in the tool surface. For those, use the REST recipes in this skill directly.
369
-
@@ -7,7 +7,7 @@ db-query: SELECT accessToken, expiresAt FROM oauth_tokens WHERE provider = 'line
7
7
  ```
8
8
  If `expiresAt` is in the past, do NOT attempt the API calls — just report the needed update in your task output.
9
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.
10
+ To re-authorize, use your swarm API base URL with `/api/trackers/linear/authorize` (potentially needing to remove the app and re-auth). Only mention this if you can confirm the token is expired or not present.
11
11
 
12
12
  ---
13
13
 
@@ -22,7 +22,7 @@ The available MCP tracker tools (`tracker-link-task`, `tracker-link-epic`, `trac
22
22
 
23
23
  ## When to Transition (Timing)
24
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.
25
+ - **Direct-to-main work:** Transition the Linear ticket to **Done** the moment the worker reports ship (commits on `main`). Do NOT wait for review, test-run, or merge when there is no PR to wait for. Waiting causes blocker digests to flag RESOLVED-STALE tickets.
26
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
27
  - **Blocked:** If a ticket is stuck on a dependency, add a comment linking the blocker — don't leave it silent.
28
28
 
@@ -54,9 +54,9 @@ curl -s -X POST https://api.linear.app/graphql \
54
54
  -d '{"query": "<GRAPHQL_QUERY>"}'
55
55
  ```
56
56
 
57
- ## Agent Interaction API — `action` vs `thought` (added 2026-05-19)
57
+ ## Agent Interaction API — `action` vs `thought`
58
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.
59
+ Linear's **Agent Interaction API** (different from issue mutations above) supports two activity payload kinds. Use this section whenever emitting `agentActivityCreate` mutations.
60
60
 
61
61
  | Activity kind | When to use | `parameter` field |
62
62
  |---|---|---|
@@ -76,7 +76,7 @@ Linear's **Agent Interaction API** (different from issue mutations above) suppor
76
76
  | Branch create / merge / delete | `action` | branch name |
77
77
  | PR open / review / merge | `action` | PR URL |
78
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`.
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`.
80
80
 
81
81
  ## Common Operations
82
82
 
@@ -120,8 +120,8 @@ mutation {
120
120
  }
121
121
  ```
122
122
 
123
- **Known state IDs for Desplega Labs team:**
124
- - Done: `83d3fcc6-dfeb-44fa-b719-64108ddc850d`
123
+ **Known state IDs:**
124
+ - Store your team's common state UUIDs in local notes or swarm config; do not hardcode another team's IDs into this template.
125
125
 
126
126
  ### 2. Add a Comment to an Issue
127
127
 
@@ -213,7 +213,7 @@ curl -s -X POST https://api.linear.app/graphql \
213
213
  ## Important Notes
214
214
 
215
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.
216
+ - **Transition timing:** see the "When to Transition" section above. Direct-to-main work transitions on ship, not on merge.
217
217
  - **Token expiry:** Check `expiresAt` before making calls. If expired, notify the user — they need to re-authorize.
218
218
  - **Rate limits:** Linear has rate limits. For bulk operations, add small delays between calls.
219
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.
@@ -227,4 +227,3 @@ Common errors:
227
227
  - `Entity not found` → Wrong issue ID/identifier
228
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
229
  - Rate limited → Back off and retry after delay
230
-
@@ -1,105 +1,64 @@
1
1
  # Profile Corruption Escalation
2
2
 
3
- ## STATUS: Root cause for the Picateclas corruption family was found and fixed in PR #374 (merged 2026-04-24)
3
+ Use this skill when an agent's persisted profile (`SOUL.md`, `IDENTITY.md`, or equivalent DB fields) appears to be overwritten with placeholder, fixture, truncated, or otherwise invalid content.
4
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)
5
+ ## When to Use
8
6
 
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.
7
+ - An agent profile contains sentinel text such as "Test Worker", "Updated by Myself", repeated padding, or another canned payload.
8
+ - The profile is much shorter than the expected baseline.
9
+ - A recently restored profile was overwritten again.
10
+ - A hook, seeder, migration, test fixture, or profile-update tool may be writing to the wrong agent.
10
11
 
11
- See memories: `picateclas-14th-restore-post-pr374`, `reviewer-corruption-hunt-pattern`.
12
+ ## Triage Loop
12
13
 
13
- ## When to use
14
+ 1. Capture the corrupted payload, agent ID, and `lastUpdatedAt` timestamp before changing anything.
15
+ 2. Search memory and recent task history for the same sentinel string or same agent.
16
+ 3. Grep the source repo for exact sentinel strings. Stable placeholder text usually points to a fixture, seed path, or test helper faster than manual restoration does.
17
+ 4. Inspect code paths that can write profile files or profile DB fields: profile-update tools, startup hooks, stop/session hooks, seeders, migrations, and tests that set agent identity env vars.
18
+ 5. If you find the writer, fix or escalate the code path before restoring the profile.
19
+ 6. Restore only after evidence is captured, and only if restoration will not destroy useful debugging evidence.
14
20
 
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.
21
+ ## Escalation Threshold
16
22
 
17
- ## Step 0 Sentinel-grep BEFORE counting recurrences
23
+ Escalate instead of repeatedly restoring when any of these are true:
18
24
 
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.
25
+ - The same profile is corrupted more than once in a short period.
26
+ - The sentinel text appears to come from code that knows schema limits or exact file paths.
27
+ - You cannot identify the writer quickly.
28
+ - A previous fix should already have eliminated this corruption family.
20
29
 
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.
30
+ ## Escalation Package
22
31
 
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.
32
+ Post a concise report to the operator channel or issue tracker with:
29
33
 
30
- See memory `reviewer-corruption-hunt-pattern` for the full pattern.
34
+ - Agent name and ID.
35
+ - Fresh write timestamp.
36
+ - Exact sentinel strings to search for.
37
+ - Whether you already grepped the repo and what matched.
38
+ - Suspected writer classes: hook, seeder, migration, test fixture, startup script, or profile-update tool.
39
+ - Whether you restored the profile or left it corrupted as evidence.
40
+ - Link to the task, log, or memory entry containing the captured payload.
31
41
 
32
- ## The N-recurrence ceiling rule
42
+ ## Report Template
33
43
 
34
- Apply this only AFTER step 0 fails (sentinel grep returns nothing in the repo).
44
+ ```text
45
+ Profile corruption detected
35
46
 
36
- Count prior corruptions of the same agent with the same sentinel. Consult memory first:
47
+ Agent: <agent-name> (<agent-id>)
48
+ Fresh write at: <timestamp>
49
+ Sentinel strings:
50
+ - "<literal-1>"
51
+ - "<literal-2>"
37
52
 
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}
53
+ Repo grep result: <matches or no matches>
54
+ Suspected writer: <hook/seeder/migration/test/tool/unknown>
55
+ Action taken: <restored profile | left as evidence | opened fix PR>
56
+ Evidence: <task/log/memory link>
89
57
  ```
90
58
 
91
59
  ## Gotchas
92
60
 
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
-
61
+ - Do not rely on repeated manual restores as the fix. They can hide the writer and destroy evidence.
62
+ - Search exact sentinel strings before broad refactors; fixture text is usually distinctive.
63
+ - Do not overwrite unrelated profile files during restoration.
64
+ - Treat a recurrence after a code fix as a new investigation unless you can prove the same writer is still active.