Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
142 commits
Select commit Hold shift + click to select a range
3789703
docs(odd): add repo-agentic-setup feature task doc
MatiasxVarela Oct 21, 2026
901c00d
docs(agents): rewrite AGENTS.md as canonical index and add agent prot…
MatiasxVarela Oct 21, 2026
f271891
chore(tooling): add agent hooks, docs check, Makefile and fork CI
MatiasxVarela Oct 21, 2026
453773b
docs(skills): add purple-* project skills and Pi settings
MatiasxVarela Oct 21, 2026
f81c78e
Merge branches 'agentic/t1-docs', 'agentic/t2-skills' and 'agentic/t3…
MatiasxVarela Oct 21, 2026
c2aa1dd
docs(skills): point upstream references to evolution-foundation
MatiasxVarela Oct 21, 2026
631b7aa
docs(odd): record repo-agentic-setup evidence and follow-ups
MatiasxVarela Oct 21, 2026
a1ccb1d
docs(odd): record review outcome and follow-ups for repo-agentic-setup
MatiasxVarela Oct 21, 2026
b79b243
fix(tooling): address review findings in git-gate hook and skills
MatiasxVarela Oct 21, 2026
7e53d8f
fix(tooling): make git-gate fail closed on unparsed input and diff er…
MatiasxVarela Oct 21, 2026
a94b1e3
Merge pull request #1 from purple-dice/chore/agentic-setup
MatiasxVarela Oct 21, 2026
bff07d6
chore(odd): keep feature task docs local
MatiasxVarela Oct 21, 2026
e96ae58
Merge pull request #3 from purple-dice/chore/ignore-odd-tasks
MatiasxVarela Oct 21, 2026
5065c78
fix(auth): prevent cross-instance override via query on instance rout…
MatiasxVarela Oct 21, 2026
1db78d5
fix(deps): patch baileys rc14 to restore WhatsApp device pairing
MatiasxVarela Oct 21, 2026
1080622
chore(deps): drop comments from the vendored baileys patch
MatiasxVarela Oct 21, 2026
71a7ff6
fix(deps): install patch-package as a runtime dependency
MatiasxVarela Oct 21, 2026
f4d1cec
Merge pull request #4 from purple-dice/fix/baileys-pairing
MatiasxVarela Oct 21, 2026
1de35d0
fix(auth-state): restore AppStateSyncKeyData.fromObject for app-state…
tgiorgio Aug 12, 2026
2e79bbe
Merge pull request #5 from purple-dice/fix/app-state-sync-key
MatiasxVarela Oct 21, 2026
b6443b1
test: add Vitest unit test runner
MatiasxVarela Oct 22, 2026
9d3d1a0
test(utils): characterize createJid and getConversationMessage
MatiasxVarela Oct 22, 2026
668871d
test(utils): characterize findBotByTrigger, advancedOperatorsSearch a…
MatiasxVarela Oct 22, 2026
4120eb2
test(utils): characterize the Prisma, Redis and provider-files auth s…
MatiasxVarela Oct 22, 2026
74b5121
refactor(baileys): extract the @lid to phone JID resolution for MESSA…
MatiasxVarela Oct 22, 2026
d791377
chore(deps): upgrade lint-staged to 17 and run CI on Node 24
MatiasxVarela Oct 22, 2026
f513827
test: make the unit suite order-independent and shuffle every run
MatiasxVarela Oct 22, 2026
2aed552
test: mark pinned bugs as KNOWN BUG with it.fails contracts
MatiasxVarela Oct 22, 2026
ba26cf9
test(auth-state): run cache tests through the real CacheService
MatiasxVarela Oct 22, 2026
4680bd1
test(utils): cover the branches reviewers found unprotected
MatiasxVarela Oct 22, 2026
d8e3aac
test: lint and typecheck the test suite
MatiasxVarela Oct 22, 2026
c10d3e2
test(helpers): fail loudly when a logger lookup is ambiguous
MatiasxVarela Oct 22, 2026
a63026b
ci(security): allow braces GHSA-vfj7-8cjw-p6xm in dependency review
MatiasxVarela Oct 22, 2026
3cb7a56
Merge pull request #6 from purple-dice/test/vitest-unit-base
MatiasxVarela Oct 22, 2026
190e359
chore(release): v0.1.0
MatiasxVarela Oct 22, 2026
6f63d68
Merge pull request #7 from purple-dice/release/v0.1.0
MatiasxVarela Oct 22, 2026
bd82247
ci(release): stop publishing the latest image tag
MatiasxVarela Oct 22, 2026
2981174
Merge pull request #8 from purple-dice/ci/no-latest-image-tag
MatiasxVarela Oct 22, 2026
4b6124f
fix(baileys): close the connection state as soon as an instance logs out
MatiasxVarela Oct 22, 2026
dfebce7
fix(baileys): keep logout cleanup running when persisting the closed …
MatiasxVarela Oct 22, 2026
1edbd66
Merge pull request #9 from purple-dice/fix/logout-connection-state
MatiasxVarela Oct 22, 2026
89fe240
fix(utils): keep the mobile 9 in Argentine jids built by createJid
MatiasxVarela Oct 22, 2026
5dd4df7
test(utils): cover more Argentine formats in createJid
MatiasxVarela Oct 22, 2026
c7e69fc
Merge pull request #10 from purple-dice/fix/createjid-argentina
MatiasxVarela Oct 22, 2026
a6f7658
fix(baileys): return no message for unknown keys in getMessage
MatiasxVarela Oct 22, 2026
904d7d9
Merge pull request #11 from purple-dice/fix/getmessage-missing
MatiasxVarela Oct 22, 2026
1efc1ec
fix(baileys): store and emit each incoming message once
MatiasxVarela Oct 22, 2026
cb721b5
fix(baileys): mark history messages seen only after they are stored
MatiasxVarela Oct 22, 2026
ea460d3
fix(baileys): keep the phone copy of a history message within one batch
MatiasxVarela Oct 22, 2026
1d58af3
fix(baileys): only live upserts mark messages as seen
MatiasxVarela Oct 22, 2026
e4e269c
fix(baileys): dedupe messages per recipient phone
MatiasxVarela Oct 22, 2026
39b7a18
Merge pull request #12 from purple-dice/fix/duplicate-upserts
MatiasxVarela Oct 22, 2026
f54a5a2
fix(utils): build onWhatsapp cache jids with a single domain
MatiasxVarela Oct 22, 2026
32e9d7c
fix(utils): retry onWhatsapp cache saves that lose a create race
MatiasxVarela Oct 22, 2026
1e8f13a
test(utils): cover onWhatsapp cache retry error paths
MatiasxVarela Oct 22, 2026
035c5a8
Merge pull request #13 from purple-dice/fix/onwhatsapp-cache
MatiasxVarela Oct 22, 2026
5c19f3f
fix(baileys): share one transaction storage across sockets
MatiasxVarela Oct 22, 2026
7991933
test(baileys): restore spies and cover concurrent store transactions
MatiasxVarela Oct 22, 2026
c36410a
Merge pull request #14 from purple-dice/fix/baileys-als-leak
MatiasxVarela Oct 22, 2026
2c6409b
docs(changelog): release v0.1.1
MatiasxVarela Oct 22, 2026
b7d1f40
Merge pull request #15 from purple-dice/release/v0.1.1
MatiasxVarela Oct 22, 2026
9de623e
fix(utils): save onWhatsapp cache items with bounded concurrency
MatiasxVarela Oct 23, 2026
04571e7
fix(utils): keep onWhatsapp cache workers running past empty items
MatiasxVarela Oct 23, 2026
dd8e65d
fix(baileys): take the remote jid of a status update from the stored …
MatiasxVarela Oct 23, 2026
54a860a
refactor(utils): share one iterator between onWhatsapp cache workers
MatiasxVarela Oct 23, 2026
17a1d71
Merge pull request #16 from purple-dice/fix/onwhatsapp-cache-concurrency
MatiasxVarela Oct 23, 2026
f84aacd
Merge pull request #17 from purple-dice/fix/message-update-remotejid
MatiasxVarela Oct 23, 2026
bcbb7c9
fix(baileys): parse plain jid group stub parameters
MatiasxVarela Oct 23, 2026
64b5cad
fix(baileys): read non-object and phone stub parameters as participants
MatiasxVarela Oct 23, 2026
235b662
fix(baileys): build link previews for pages without an image
MatiasxVarela Oct 23, 2026
de1d2a1
Merge pull request #18 from purple-dice/fix/baileys-group-number-change
MatiasxVarela Oct 23, 2026
221dbed
Merge branch 'main' into fix/baileys-link-preview-without-image
MatiasxVarela Oct 23, 2026
56ef39c
Merge pull request #19 from purple-dice/fix/baileys-link-preview-with…
MatiasxVarela Oct 23, 2026
b7c6eb4
build(docker): pin the Node.js base image to 24.22.1 by digest
nogara Oct 23, 2026
fd083bd
fix(telemetry): send telemetry only when TELEMETRY_ENABLED=true
nogara Oct 23, 2026
9903e6e
build(docker): write the pinned Node.js image as literal FROM lines
nogara Oct 23, 2026
80784f9
feat(utils): store the onWhatsapp cache as one row per jid
MatiasxVarela Oct 23, 2026
7600ee5
Merge pull request #20 from nogara/purple/pin-node-image
MatiasxVarela Oct 23, 2026
590823b
Merge pull request #21 from nogara/purple/telemetry-opt-in
MatiasxVarela Oct 23, 2026
63861b6
Merge pull request #22 from purple-dice/feat/onwhatsapp-cache-per-jid
MatiasxVarela Oct 23, 2026
51cfa32
docs(changelog): release v0.1.2
MatiasxVarela Oct 23, 2026
3f37d9e
Merge pull request #23 from purple-dice/release/v0.1.2
MatiasxVarela Oct 23, 2026
55ebe72
ci: smoke-test the Docker image against Postgres
nogara Oct 23, 2026
4157b0b
ci: run the image smoke test on main, check migrations and pins, boun…
MatiasxVarela Oct 23, 2026
7264479
ci: scan the published image daily with Trivy
nogara Oct 23, 2026
a691515
ci: scan our latest final release in report-only mode
MatiasxVarela Oct 23, 2026
3fac75f
Merge pull request #24 from purple-dice/ci/image-smoke-test
MatiasxVarela Oct 23, 2026
28fc528
Merge pull request #25 from purple-dice/ci/image-scan
MatiasxVarela Oct 23, 2026
5c2703d
build(docker): ship only production dependencies in the image
MatiasxVarela Oct 23, 2026
4bc940d
build(deps): apply semver-compatible security updates
MatiasxVarela Oct 23, 2026
a83a22b
fix(baileys): keep the media mimetype when the file name has no exten…
MatiasxVarela Oct 24, 2026
a4b32dd
build(deps): clear transitive and npm CLI vulnerabilities
MatiasxVarela Oct 24, 2026
4ff29ee
build(deps): upgrade sharp to 0.35 and link-preview-js to 4
MatiasxVarela Oct 24, 2026
543a699
build(deps): upgrade node-cron to 4
MatiasxVarela Oct 24, 2026
9839112
build(deps): run jimp and openai on zod 4
MatiasxVarela Oct 24, 2026
792e360
build(docker): run the image without npm
MatiasxVarela Oct 24, 2026
5ecfbb9
build(deps): move minio to patched query-string 10 and stream-json 3
MatiasxVarela Oct 24, 2026
f85bf75
build(deps): apply semver-compatible fixes to development dependencies
MatiasxVarela Oct 24, 2026
c71a389
build(deps): upgrade vitest to 5
MatiasxVarela Oct 24, 2026
e26c2ea
Merge pull request #26 from purple-dice/build/prune-dev-dependencies
MatiasxVarela Oct 24, 2026
78496c9
Merge pull request #27 from purple-dice/build/audit-fix-semver
MatiasxVarela Oct 24, 2026
ce35092
Merge pull request #29 from purple-dice/build/clear-remaining-vulnera…
MatiasxVarela Oct 24, 2026
cba1389
Merge pull request #30 from purple-dice/build/dev-dependency-vulnerab…
MatiasxVarela Oct 24, 2026
211ac96
Merge branch 'main' into fix/media-mimetype-from-filename
MatiasxVarela Oct 24, 2026
e239a7a
Merge pull request #28 from purple-dice/fix/media-mimetype-from-filename
MatiasxVarela Oct 24, 2026
c9896b6
docs(changelog): release v0.1.3
MatiasxVarela Oct 24, 2026
e25f34b
Merge pull request #31 from purple-dice/release/v0.1.3
MatiasxVarela Oct 24, 2026
65a73b9
ci: fail the daily image scan on fixable critical or high findings
MatiasxVarela Oct 24, 2026
9208272
ci: keep the scan summary step working when no table was produced
MatiasxVarela Oct 24, 2026
bdd60e1
Merge pull request #32 from purple-dice/ci/image-scan-gate
MatiasxVarela Oct 24, 2026
58b9276
fix(baileys): fill remoteJidAlt for @lid history sync messages
MatiasxVarela Oct 24, 2026
acf5a4c
chore(scripts): add one-off backfill for @lid key.remoteJidAlt
MatiasxVarela Oct 24, 2026
dbb65f1
fix(baileys): skip payload contacts with conflicting lids
MatiasxVarela Oct 24, 2026
edf0512
chore(scripts): back up rows before the @lid backfill and add rollback
MatiasxVarela Oct 24, 2026
3a0e91a
chore(scripts): skip rows changed during the @lid backfill and back u…
MatiasxVarela Oct 24, 2026
c2c0811
docs(scripts): note row locks held by the @lid backfill
MatiasxVarela Oct 24, 2026
57ebcfb
Merge pull request #33 from purple-dice/fix/history-lid-remote-jid-alt
MatiasxVarela Oct 24, 2026
b999104
feat(baileys): backfill remoteJidAlt when a lid mapping is learned (#34)
MatiasxVarela Oct 24, 2026
0df9532
Merge pull request #35 from purple-dice/chore/backfill-lid-remote-jid…
MatiasxVarela Oct 24, 2026
85d0b1e
fix(baileys): advance the status of sent messages from acks
MatiasxVarela Oct 24, 2026
826c325
fix(baileys): ignore ERROR after delivery and keep the webhook when t…
MatiasxVarela Oct 24, 2026
bc38714
fix(baileys): advance sent message status with a conditional update
MatiasxVarela Oct 24, 2026
95e6919
fix(utils): never overwrite an unknown stored message status
MatiasxVarela Oct 24, 2026
2f7c7ee
Merge pull request #36 from purple-dice/fix/own-message-status
MatiasxVarela Oct 24, 2026
42eed01
fix(monitor): close unpaired instances when the QR code limit is reached
MatiasxVarela Oct 24, 2026
433170b
fix(baileys): do not resend messages deleted for everyone on retry re…
MatiasxVarela Oct 24, 2026
9457052
Merge pull request #37 from purple-dice/fix/qr-limit-close-state
MatiasxVarela Oct 24, 2026
507b101
Merge remote-tracking branch 'origin/main' into fix/no-resend-deleted…
MatiasxVarela Oct 24, 2026
4492eb2
Merge pull request #38 from purple-dice/fix/no-resend-deleted-messages
MatiasxVarela Oct 24, 2026
e84e590
perf(prisma): index message lookups by key id and remote jid
MatiasxVarela Oct 24, 2026
5409ff0
fix(baileys): stop printing message payloads and base64 media to stdout
MatiasxVarela Oct 24, 2026
e14ae1b
perf(prisma): build the message indexes inside the migration transaction
MatiasxVarela Oct 24, 2026
5e98cee
fix(baileys): scope unread counts and message edits to the instance
MatiasxVarela Oct 24, 2026
9f95ca1
fix(baileys): isolate errors per message, update and event type
MatiasxVarela Oct 24, 2026
5d120e1
Merge pull request #40 from purple-dice/perf/message-lookup-indexes
MatiasxVarela Oct 24, 2026
7f3c5c2
Merge pull request #39 from purple-dice/fix/instance-scoped-chat-and-…
MatiasxVarela Oct 24, 2026
d56a9bc
Merge remote-tracking branch 'origin/main' into fix/no-payload-consol…
MatiasxVarela Oct 24, 2026
116ab40
Merge pull request #41 from purple-dice/fix/no-payload-console-logs
MatiasxVarela Oct 24, 2026
82c6434
Merge remote-tracking branch 'origin/main' into fix/isolate-event-errors
MatiasxVarela Oct 24, 2026
938163c
Merge pull request #42 from purple-dice/fix/isolate-event-errors
MatiasxVarela Oct 24, 2026
ed9d5d8
fix(chat): find the last message by JSON path in archiveChat and mark…
MatiasxVarela Oct 26, 2026
0482cbd
docs(changelog): release v0.1.4
MatiasxVarela Nov 5, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
129 changes: 129 additions & 0 deletions .claude/hooks/git-gate.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,129 @@
#!/usr/bin/env bash
# Claude Code PreToolUse hook (Bash matcher).
# Denies git commit/push/merge/rebase when:
# - the command uses --no-verify (bypasses Husky hooks), or
# - `npx tsc --noEmit` fails, or
# - ESLint fails on the staged (commit), unpushed (push) or uncommitted (merge/rebase) .ts/.js
# files under src/; push with no upstream/origin/main base checks all of src/,
# - or the toolchain (node_modules/.bin/tsc, eslint) is missing: run `npm ci`.
# Fails closed: unparseable input that looks like a git write, or a failing git diff, is
# blocked or escalated to a full check instead of passing.
# Emits a PreToolUse deny decision as JSON; otherwise exits 0 silently.

set -o pipefail

INPUT=$(cat)
# Parse the tool input with jq, falling back to node (always present in this repo). If neither
# can parse it, fail closed: an unparsed command must never be allowed through silently.
if command -v jq >/dev/null 2>&1; then
CMD=$(printf '%s' "$INPUT" | jq -r '.tool_input.command // empty' 2>/dev/null) || CMD_PARSE_FAILED=1
elif command -v node >/dev/null 2>&1; then
CMD=$(printf '%s' "$INPUT" | node -e 'let s="";process.stdin.on("data",d=>s+=d).on("end",()=>{process.stdout.write(String((JSON.parse(s).tool_input||{}).command||""))})' 2>/dev/null) || CMD_PARSE_FAILED=1
else
CMD_PARSE_FAILED=1
fi
if [ -n "$CMD_PARSE_FAILED" ]; then
# Only block unparsed input that looks like a git write; other commands are not gated.
if printf '%s' "$INPUT" | grep -qE 'git[^a-z].*(commit|push|merge|rebase)'; then
printf '%s\n' "Git gate: could not parse the tool input (install jq or node). Git write blocked." >&2
exit 2
fi
exit 0
fi

GIT_WRITE='(^|[[:space:]&|;(])git([[:space:]]+-[^[:space:]]+)*[[:space:]]+(commit|push|merge|rebase)([[:space:]]|$)'
if ! printf '%s' "$CMD" | grep -qE "$GIT_WRITE"; then
exit 0
fi

deny() {
local reason=$1
if command -v jq >/dev/null 2>&1; then
jq -n --arg reason "$reason" \
'{"hookSpecificOutput":{"hookEventName":"PreToolUse","permissionDecision":"deny","permissionDecisionReason":$reason}}'
else
# Fallback: exit code 2 blocks the tool call and shows stderr to the model.
printf '%s\n' "$reason" >&2
exit 2
fi
exit 0
}

# Quoted strings (commit messages, echo text) are stripped before matching flags, so
# `git commit -m "fix -n parsing --no-verify"` is not mistaken for a bypass.
STRIPPED=$(printf '%s' "$CMD" | sed -E "s/\"([^\"\\\\]|\\\\.)*\"//g; s/'[^']*'//g")
if ! printf '%s' "$STRIPPED" | grep -qE "$GIT_WRITE"; then
exit 0
fi

# --no-verify on any git write, or `-n` (alone or in a short-flag cluster) on `git commit` only.
GIT_COMMIT='git([[:space:]]+-[^[:space:]]+)*[[:space:]]+commit'
if printf '%s' "$STRIPPED" | grep -qE -- '(^|[[:space:]])--no-verify([[:space:]=]|$)' \
|| printf '%s' "$STRIPPED" | grep -qE -- "${GIT_COMMIT}[^|;&]*[[:space:]]-[[:alpha:]]*n[[:alpha:]]*([[:space:]]|\$)"; then
deny "Git gate: --no-verify is not allowed. Fix the failing Husky hooks instead of bypassing them."
fi

ROOT=${CLAUDE_PROJECT_DIR:-$(git rev-parse --show-toplevel 2>/dev/null)}
[ -n "$ROOT" ] || exit 0
cd "$ROOT" || exit 0

# FULL=1 checks the whole project (tsc + eslint on src/) instead of the changed files only.
FULL=0
if printf '%s' "$STRIPPED" | grep -qE 'git([[:space:]]+-[^[:space:]]+)*[[:space:]]+push'; then
BASE=$(git rev-parse --abbrev-ref '@{u}' 2>/dev/null)
if [ -z "$BASE" ] && git rev-parse --verify --quiet origin/main >/dev/null; then
BASE=origin/main
fi
if [ -n "$BASE" ]; then
# A failing diff (e.g. no merge base) falls back to a full check instead of passing.
FILES=$(git diff --name-only --diff-filter=d "$BASE"...HEAD 2>/dev/null) || FULL=1
else
# No base ref to diff against: fail closed with a full check instead of passing.
FULL=1
fi
elif printf '%s' "$STRIPPED" | grep -qE "${GIT_COMMIT}([[:space:]]|\$)"; then
FILES=$(git diff --cached --name-only --diff-filter=d 2>/dev/null) || FULL=1
else
# merge/rebase: the incoming commits already passed their own gates; check that the local
# working tree (staged + unstaged src/ changes vs HEAD) is not broken before rewriting history.
FILES=$(git diff HEAD --name-only --diff-filter=d 2>/dev/null) || FULL=1
fi

if [ "$FULL" -eq 1 ]; then
CODE_FILES=src
else
CODE_FILES=$(printf '%s\n' "$FILES" | grep -E '^src/.*\.(ts|js)$' || true)
[ -n "$CODE_FILES" ] || exit 0
fi

if [ ! -x node_modules/.bin/tsc ] || [ ! -x node_modules/.bin/eslint ]; then
deny "Git gate: BLOCKED because the local toolchain is missing (node_modules/.bin/tsc or eslint not found). Run \`npm ci\` in ${ROOT} and retry."
fi

FAILURES=""

TSC=$(node_modules/.bin/tsc --noEmit 2>&1)
if [ $? -ne 0 ]; then
FAILURES="${FAILURES}--- TypeScript (tsc --noEmit) ---
${TSC}

"
fi

LINT=$(printf '%s\n' "$CODE_FILES" | xargs node_modules/.bin/eslint 2>&1)
if [ $? -ne 0 ]; then
FAILURES="${FAILURES}--- ESLint ---
${LINT}

"
fi

if [ -n "$FAILURES" ]; then
deny "Git gate: commit/push/merge/rebase BLOCKED by errors in changed files.

Fix the errors below and retry.

${FAILURES}"
fi

exit 0
35 changes: 35 additions & 0 deletions .claude/settings.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
{
"permissions": {
"allow": [
"mcp__plugin_engram_engram__mem_search",
"mcp__plugin_engram_engram__mem_get_observation",
"mcp__plugin_engram_engram__mem_save",
"mcp__plugin_engram_engram__mem_context",
"mcp__plugin_engram_engram__mem_session_summary"
]
},
"hooks": {
"PreToolUse": [
{
"matcher": "Bash",
"hooks": [
{
"type": "command",
"command": "bash \"$CLAUDE_PROJECT_DIR/.claude/hooks/git-gate.sh\""
}
]
}
],
"PostToolUse": [
{
"matcher": "Edit|Write|MultiEdit",
"hooks": [
{
"type": "command",
"command": "F=$(jq -r '.tool_input.file_path // empty'); [ -z \"$F\" ] && exit 0; case \"$F\" in *.ts|*.js|*.json|*.md) (cd \"$CLAUDE_PROJECT_DIR\" && npx --no prettier --config .prettierrc.js --write --log-level=silent \"$F\" >/dev/null 2>&1) || true ;; esac; exit 0"
}
]
}
]
}
}
69 changes: 69 additions & 0 deletions .claude/skills/purple-baileys/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
---
name: purple-baileys
description: >
Landmarks and invariants of the Baileys channel in evolution-in-purple:
whatsapp.baileys.service.ts entry points, onWhatsApp guards, the on-WhatsApp cache,
createJid, @lid (LID) addressing rules, QR / pairing code flow and the Baileys version pin.
Read BEFORE touching message receive/send, JID handling or connection logic.
Trigger: change in whatsapp.baileys.service.ts, onWhatsappCache.ts, createJid.ts, @lid / remoteJidAlt handling, QR or pairing, Baileys upgrade.
---

# Skill: purple-baileys

File: `src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts` (~5k lines).
Baileys is pinned exactly: `"baileys": "7.0.0-rc.9"` (`package.json:80`). Never use a caret;
an upgrade is its own task with manual QR + send/receive verification.

## Landmarks (verify line numbers before citing; they drift)

| Area | Location |
|---|---|
| QR / pairing in `connectionUpdate` | `:334`, pairing code `:375-380`, QR limit `:336` |
| Auth state selection (Redis / Prisma / provider files) | `defineAuthState` `:556` |
| Socket creation | `createClient` `:576` |
| Public connect entry | `connectToWhatsapp` `:723` |
| Inbound handler | `messageHandle['messages.upsert']` `:1082` |
| LID → PN swap before emit | `:1478-1480` |
| Contact upsert using `remoteJidAlt` | `:1512-1517` |
| Event dispatch (`connection.update`, `messages.upsert`) | `:1902-1919` |
| Outbound core | `sendMessageWithTyping` `:2289` |
| onWhatsApp guards (throw 400 when `!exists`) | `:2297`, `sendPresence` `:2571`, `blockUser` `:4118` |
| Number verification | `whatsappNumber` `:3507` (cache → Baileys `onWhatsApp` `:3566`) |

Helpers: `src/utils/onWhatsappCache.ts` (`saveOnWhatsappCache` `:73`, `getOnWhatsappCache`
`:182`, `normalizeJid` `:68`) and `src/utils/createJid.ts` (`createJid` `:35`).

## @lid rules (hard)

1. **LID → PN is NOT derivable.** A `<digits>@lid` carries no phone number. Never compute,
guess or strip the suffix to obtain one.
2. **PN is only learned** from: `key.remoteJidAlt` / `participantAlt` on received messages,
group participant metadata, or history sync. Persist the mapping when observed
(contact upsert at `:1512-1517`, `saveOnWhatsappCache`).
3. **Never strip `@lid` on outbound.** `createJid` returns `@lid` JIDs unchanged
(`createJid.ts:38-40`); `whatsappNumber` skips Baileys verification for `@lid` and treats an
uncached LID as existing (`:3562`, `:3586-3594`). Keep both behaviors.
4. The inbound swap at `:1478-1480` rewrites `remoteJid` to `remoteJidAlt` only when the alt is
present. Consumers (orbit, stenfar) also read `remoteJidAlt`/`addressingMode` themselves —
changing this is a contract change (`purple-contract`).

Detail, failure modes and examples: `references/lid.md`.

## onWhatsApp guards

`sendMessageWithTyping`, `sendPresence` and `blockUser` all call `whatsappNumber` and throw
`BadRequestException(isWA)` when `!isWA.exists` and the JID is not a group/broadcast. The 400
body (the `OnWhatsAppDto`) is what consumers see on "number not on WhatsApp". Do not bypass the
guard; fix the cache or the JID instead.

## QR / pairing

- `connectToWhatsapp(number?)` — passing a number enables pairing-code mode
(`this.phoneNumber`, `:375-380`); otherwise only QR.
- QR emits `qrcode.updated` with `{ pairingCode, code, base64 }` (`:392`, `:400`); the count is
capped by `QRCODE.LIMIT` (`:336`), after which the instance stops retrying.

## Verification

No automated tests. After changes: `npx tsc --noEmit`, `npm run build`, then a manual run
with a test instance — QR scan, inbound 1:1 (PN and LID contact), inbound group, outbound text.
44 changes: 44 additions & 0 deletions .claude/skills/purple-baileys/references/lid.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
# purple-baileys — @lid detail

## What a LID is

WhatsApp privacy addressing: some 1:1 and group participants appear as `<digits>@lid`
(`key.addressingMode === 'lid'`) instead of `<phone>@s.whatsapp.net`. The digits are an opaque
identifier, not a phone number.

## Where the phone number (PN) can come from

| Source | Field | Code |
|---|---|---|
| Received message key | `key.remoteJidAlt` (1:1), `key.participantAlt` (groups) | `whatsapp.baileys.service.ts:159-160` (key type), `:1250-1251`, `:1478`, `:1516-1517` |
| Group metadata | participant entries returned by Baileys group metadata | group fetch paths (shape depends on Baileys version — verify) |
| History sync | message keys in `messaging-history.set` | `whatsapp.baileys.service.ts:927` |
| On-WhatsApp cache | `jidOptions`, `lid` | `src/utils/onWhatsappCache.ts:73`, `:182` |

If none of these has been observed, the PN is unknown. Reply to the LID JID.

## Outbound

- `createJid` keeps `@lid`, `@g.us`, `@s.whatsapp.net`, `@broadcast` untouched
(`src/utils/createJid.ts:38-44`); only bare numbers get normalized (BR/MX/AR rules at `:2`, `:17`).
- `whatsappNumber` (`whatsapp.baileys.service.ts:3507`) resolves users from the cache first, then
calls `client.onWhatsApp` only for non-LID JIDs (`:3562-3566`). An uncached LID returns
`exists: true` with `lid: 'lid'` (`:3586-3594`).

## Known failure modes

- Stripping `@lid` and sending to `<digits>@s.whatsapp.net` → 400 "exists: false" from the guard
or delivery to the wrong account.
- Treating `remoteJid` as the stable chat key: the swap at `:1478-1480` means the same chat can
arrive as PN or LID depending on whether `remoteJidAlt` was present.
- Persisting a LID as a phone number in consumer CRMs. Consumers must read `remoteJidAlt`
(orbit: `orbit-backend/src/evolution-api-webhook/evolution-api-webhook.service.ts:227-231`;
stenfar: `canal-stenfar/src/services/whatsapp/phoneExtractor.js:35-44`).

## Manual verification matrix

1. 1:1 inbound from a PN contact → `remoteJid` is `@s.whatsapp.net`.
2. 1:1 inbound from a LID contact with alt → `remoteJid` swapped to PN, `remoteJidAlt` kept.
3. Group inbound with LID participant → `participant` LID, `participantAlt` PN when present.
4. Outbound to a LID JID → delivered; no 400.
5. Outbound to an unknown PN → `onWhatsApp` check, 400 when not registered.
72 changes: 72 additions & 0 deletions .claude/skills/purple-contract/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
---
name: purple-contract
description: >
The external contract of evolution-in-purple as consumed by orbit, canal-whatsapp and
stenfar: webhook payload envelope, event naming (MESSAGES_UPSERT vs messages-upsert with
webhookByEvents), and the REST routes each consumer calls. Read BEFORE changing any route,
DTO, response shape, webhook event or payload field.
Trigger: change to a REST route, request/response shape, webhook event name or payload, message key fields (remoteJid, remoteJidAlt), or instance/webhook config.
---

# Skill: purple-contract

This engine replaces Evolution API 2.3.7 for three consumers. **The contract is "behaves
exactly like Evolution 2.3.7"** unless a versioned breaking change says otherwise.
Consumer matrix (endpoint/event × consumer, with source paths): `references/consumer-matrix.md`.

## Webhook envelope

Built in `src/api/integrations/event/webhook/webhook.controller.ts:93-103`:

```ts
{ ...extra, event, instance, data, destination, date_time, sender, server_url, apikey }
```

- `event` is the dotted lowercase name (`messages.upsert`, `connection.update`, ...).
- Enabled-events list is stored uppercase with underscores (`MESSAGES_UPSERT`), derived at
`webhook.controller.ts:88`.
- With `webhookByEvents: true` the URL becomes `${url}/${event-kebab}` — e.g.
`/messages-upsert` (`webhook.controller.ts:89`, `:109-110`). orbit depends on this
(routes `whatsapp/messages-upsert`, `qrcode-updated`, `connection-update`).
- canal-whatsapp and stenfar use a single URL (`byEvents: false`) and switch on `event`.

## Message key fields consumers read

`data.key.remoteJid`, `remoteJidAlt`, `participant`, `participantAlt`, `addressingMode`,
`fromMe`, `id`. The LID → PN swap (`whatsapp.baileys.service.ts:1478-1480`) is part of the
contract (see `purple-baileys`).

## REST surface consumers use

Union of all consumers (details per consumer in the matrix):
`/instance/{create,fetchInstances,connect,connectionState,logout}`,
`/message/{sendText,sendMedia,sendWhatsAppAudio,sendReaction}`,
`/chat/{markMessageAsRead,sendPresence,getBase64FromMediaMessage,findMessages,findContacts}`,
`/group/findGroupInfos`, `/webhook/{set,find}`. Auth: `apikey` header.
canal also calls `/group/findGroupByJid`, which does **not** exist here (see matrix §Open questions).

## Rules

1. **Non-breaking by default.** Additive fields are allowed; renaming, removing, retyping a
field, changing a status code, an event name or a URL suffix is breaking.
2. **Breaking change ⇒ all of:** (a) new fork MAJOR/MINOR per `purple-release`, (b) a
`CHANGELOG.md` entry under the `evolution-in-purple` section with a migration note,
(c) a consumer check: grep each consumer listed in the matrix for the affected
route/event/field and record positives/negatives in the task doc.
3. Update `references/consumer-matrix.md` in the same change whenever a consumer starts or
stops using a route/event.
4. Consumers are read-only from this repo. Never edit orbit, canal-whatsapp or stenfar from
here; open a task in the consumer instead.

## Consumer check (procedure)

```bash
rg -n '<route-or-event-or-field>' \
../../orbit/orbit-backend/src \
../../canal-whatsapp/canal-whatsapp-backend/src \
../../stenfar/canal-stenfar/src
```

Paths are relative to a checkout under `~/Programacion/evolution/<worktree>`; adjust if the
layout differs. canal-whatsapp calls REST through `@alien-2-0-llc/sdk-evolution-api`, so also
grep that SDK's method names (`sendText`, `setWebhook`, ...).
Loading