slack
A Slack account: its session and workspaces, chats, reading, the live websocket watch and sending.
Written by bun run docs:build from the code, the skills and AGENTS.md (rule 56). Not edited by hand: change the command's usage, its skill or the rule, and rebuild.
Use cases these tools are part of
When the owner asks for one of these, follow its page step by step.
| Use case | What it is | How the owner asks for it |
|---|---|---|
getGuide({ useCase: "inbox/bossMaxxing" }) | Bossmaxxing: giving out tasks to the team | "bossmaxxing", "give out the tasks", "send <person> the task in <group> and tag them", "tell <person> about everything I researched and give them an outline", "triage the jobs across the team", "walk <person> through setting up <tool>", "help <person> get set up like we did with <other person>" |
getGuide({ useCase: "inbox/reactions" }) | Reacting to a message | "react to that", "heart his message", "put a 😂 on what she said", "send a reaction to <person>" |
getGuide({ useCase: "inbox/voiceNote" }) | Sending a voice note | "send <person> a voice note saying …", "voice note him that …", "send a vn to the group saying …" |
getGuide({ useCase: "slack/dmReply" }) | Reading and answering a Slack DM | "read the messages with <someone> on slack", "reply to <someone> on slack", "what did <someone> say in <workspace>" |
The tools in this guide
| Tool | What it does |
|---|---|
slackSessionCheck | Check that Slack still accepts one account's saved session, sending… |
slackWorkspaces | The workspaces the account is signed in to, with their domains |
slackChats | The account's DMs and group DMs in a workspace, with their ids |
slackRead | A Slack chat's latest messages, oldest first, sending nothing |
slackWatch | Every Slack event as the workspace's websocket pushes it, sending… |
slackSend | Send a message on Slack, on the direct-API engine |
slackSendLive | Send a message on Slack, on the direct-API engine, live on Slack |
slackReact | React to a Slack message with an emoji, on the direct-API engine |
slackReactLive | React to a Slack message with an emoji, on the…, live on Slack |
slackVoice | Send a voice note in matt's cloned voice as Slack's own voice note… |
slackVoiceLive | Send a voice note in matt's cloned voice as Slack's…, live on Slack |
Every tool, in full
slackSessionCheck
Check that Slack still accepts one account's saved session, sending nothing. Check that Slack still accepts an account's saved session, on the direct-API engine, sending nothing; the answer goes to its row log slack session check --account <accountName> [--workspace <domain>] [--json] Checks that Slack still accepts the account's saved d cookie, on the direct-API engine (vendor/gabrielSlack, no browser): the workspaces it is signed in to, and with --workspace who it is there (auth.test). Sends nothing. Slack's own answer decides. The result goes to socialAccounts.sessionState, and refreshed cookies are merged back into the row. Fill the named fields; each value goes through whole. Use cases that call it, each a workflow the owner asks for, tool by tool: getGuide({ useCase: "slack/dmReply" }). Before first using this tool in a session, load its guide: getGuide({ topic: "slack" }) (A Slack account: its session and workspaces, chats, reading, the live websocket watch and sending). AGENTS.md (log://agents): rules 13, 63, 64, 73, 83, 125, 30, 38, 39, 40, 41. When matt says to do something, do it completely (rule 30).
slackWorkspaces
The workspaces the account is signed in to, with their domains. List the workspaces a Slack account is signed in to, on the direct-API engine, sending nothing log slack workspaces --account <accountName> [--json] Lists the workspaces the account's d cookie is signed in to, on the direct-API engine (Slack's own cookie exchange, vendor/gabrielSlack, no browser): each one's id, name and domain, the domain being what --workspace takes. Sends nothing. Fill the named fields; each value goes through whole. Use cases that call it, each a workflow the owner asks for, tool by tool: getGuide({ useCase: "slack/dmReply" }). Before first using this tool in a session, load its guide: getGuide({ topic: "slack" }) (A Slack account: its session and workspaces, chats, reading, the live websocket watch and sending). AGENTS.md (log://agents): rules 13, 63, 64, 73, 83, 125, 30, 38, 39, 40, 41. When matt says to do something, do it completely (rule 30).
slackChats
The account's DMs and group DMs in a workspace, with their ids. List a Slack account's DMs and group DMs in a workspace with their ids, on the direct-API engine, sending nothing log slack chats --account <accountName> [--workspace <domain>] [--limit <n>] [--json] Lists the account's DMs and group DMs in one workspace with their ids and who they are with, newest first, on the direct-API engine (vendor/gabrielSlack, conversations.list, no browser). Sends nothing. Fill the named fields; each value goes through whole. Use cases that call it, each a workflow the owner asks for, tool by tool: getGuide({ useCase: "inbox/bossMaxxing" }), getGuide({ useCase: "slack/dmReply" }). Before first using this tool in a session, load its guide: getGuide({ topic: "slack" }) (A Slack account: its session and workspaces, chats, reading, the live websocket watch and sending). AGENTS.md (log://agents): rules 13, 63, 64, 73, 83, 94, 125, 30, 38, 39, 40, 41. When matt says to do something, do it completely (rule 30).
slackRead
A Slack chat's latest messages, oldest first, sending nothing.
Read a Slack chat's latest messages, on the direct-API engine, sending nothing
log slack read --account <accountName> --chat <chatId|who it is with> [--workspace <domain>] [--limit <n>] [--json]
Reads a DM, group DM or channel's latest messages, oldest first, on the direct-API engine (vendor/gabrielSlack, conversations.history, no browser): who sent each, when, its text and its thread. Sends nothing, and marks nothing read.
Fill the named fields; each value goes through whole.
Runs for seconds to minutes: start a Monitor on tail -f ~/.logCli/mcp/<day>.log before calling, so matt watches it live (rule 26). A run that outlasts the call's wait (waitMs, default 20 s, at most 50 s) comes back as a job (exit -1, job.id): follow it with jobStatus { jobId, afterLine }, stop it at its next browser step with jobCancel.
Use cases that call it, each a workflow the owner asks for, tool by tool: getGuide({ useCase: "inbox/bossMaxxing" }), getGuide({ useCase: "inbox/reactions" }), getGuide({ useCase: "inbox/voiceNote" }), getGuide({ useCase: "slack/dmReply" }).
Before first using this tool in a session, load its guide: getGuide({ topic: "slack" }) (A Slack account: its session and workspaces, chats, reading, the live websocket watch and sending).
AGENTS.md (log://agents): rules 13, 63, 64, 73, 83, 94, 125, 126, 30, 38, 39, 40, 41. When matt says to do something, do it completely (rule 30).
slackWatch
Every Slack event as the workspace's websocket pushes it, sending nothing.
Watch a Slack workspace's events as its websocket pushes them, nothing polled, sending nothing
log slack watch --account <accountName> [--workspace <domain>] [--chat <chatId|who it is with>] [--seconds <s>] [--inbox] [--json]
Opens each workspace's own websocket on the direct-API engine (vendor/gabrielSlack, wss-primary.slack.com, no browser), every workspace the session is signed in to unless --workspace names one, and prints each message, typing, read marker, reaction and draft the moment Slack pushes it, nothing polled. --chat keeps one chat; --seconds stops after that long (at least 5), otherwise it runs until stopped. --inbox hands each new DM, group message or mention of the account from someone else to the inbox hook, which puts it in front of the agent with what to do next (rule 123). Sends nothing.
Fill the named fields; each value goes through whole.
Runs for seconds to minutes: start a Monitor on tail -f ~/.logCli/mcp/<day>.log before calling, so matt watches it live (rule 26). A run that outlasts the call's wait (waitMs, default 20 s, at most 50 s) comes back as a job (exit -1, job.id): follow it with jobStatus { jobId, afterLine }, stop it at its next browser step with jobCancel.
Before first using this tool in a session, load its guide: getGuide({ topic: "slack" }) (A Slack account: its session and workspaces, chats, reading, the live websocket watch and sending).
AGENTS.md (log://agents): rules 13, 26, 63, 64, 73, 83, 94, 123, 125, 131, 132, 30, 38, 39, 40, 41. When matt says to do something, do it completely (rule 30).
slackSend
Send a message on Slack, on the direct-API engine; a dry run unless --live.
Send a message to a Slack DM, group or channel, on the direct-API engine; a dry run unless --live
log slack send --account <accountName> --chat <chatId|who it is with> --text "<message>" [--workspace <domain>] [--asOne] [--approvedBy <who>] [--live] [--json]
Sends a message to a Slack DM, group DM or channel (by its id, or who it is with from log slack chats), on the direct-API engine (vendor/gabrielSlack, chat.postMessage, no browser).
A draft written on several lines goes out one message per line (rule 90); a one-line draft is split the way matt writes in that chat (log voice index), and --asOne always sends one. Without --live it is a dry run and Slack is not contacted. --live sends it, only when matt asked, and needs --approvedBy (rule 8). Slack's answer decides: a ts is sent, anything else stops the run with nothing retried (rule 94). The governor counts each message as a write. The copy is read for what AI chat interfaces leave in text first (rule 111): hidden characters, odd spaces or pasted Claude HTML refuse it with aiArtifacts.
Fill the named fields; each value goes through whole. What acts on Slack is slackSendLive.
Before first using this tool in a session, load its guide: getGuide({ topic: "slack" }) (A Slack account: its session and workspaces, chats, reading, the live websocket watch and sending).
AGENTS.md (log://agents): rules 8, 13, 63, 64, 73, 83, 90, 94, 111, 125, 130, 133, 134, 135, 137, 138, 30, 38, 39, 40, 41. When matt says to do something, do it completely (rule 30).
slackSendLive
Send a message on Slack, on the direct-API engine; a dry run unless --live. Live on Slack: it acts there (posts, clicks, joins or changes something). Call it only when matt asked for this live run, with confirmLive: true (rules 8, 10). Send a message to a Slack DM, group or channel, on the direct-API engine; a dry run unless --live log slack send --account <accountName> --chat <chatId|who it is with> --text "<message>" [--workspace <domain>] [--asOne] [--approvedBy <who>] [--live] [--json] Sends a message to a Slack DM, group DM or channel (by its id, or who it is with from log slack chats), on the direct-API engine (vendor/gabrielSlack, chat.postMessage, no browser). A draft written on several lines goes out one message per line (rule 90); a one-line draft is split the way matt writes in that chat (log voice index), and --asOne always sends one. Without --live it is a dry run and Slack is not contacted. --live sends it, only when matt asked, and needs --approvedBy (rule 8). Slack's answer decides: a ts is sent, anything else stops the run with nothing retried (rule 94). The governor counts each message as a write. The copy is read for what AI chat interfaces leave in text first (rule 111): hidden characters, odd spaces or pasted Claude HTML refuse it with aiArtifacts. Fill the named fields; each value goes through whole. Use cases that call it, each a workflow the owner asks for, tool by tool: getGuide({ useCase: "inbox/bossMaxxing" }), getGuide({ useCase: "slack/dmReply" }). Before first using this tool in a session, load its guide: getGuide({ topic: "slack" }) (A Slack account: its session and workspaces, chats, reading, the live websocket watch and sending). AGENTS.md (log://agents): rules 8, 13, 63, 64, 73, 83, 90, 94, 111, 125, 130, 133, 134, 135, 137, 138, 30, 38, 39, 40, 41. When matt says to do something, do it completely (rule 30).
slackReact
React to a Slack message with an emoji, on the direct-API engine; a dry run unless --live.
React to a Slack message with an emoji, on the direct-API engine; a dry run unless --live
log slack react --account <accountName> --chat <chat> --message <id> --emoji <emoji> [--workspace <workspace>] [--remove] [--approvedBy <who>] [--live] [--json]
Reacts to one message with an emoji, on the direct-API engine (vendor/gabrielSlack, reactions.add and reactions.remove, no browser), or takes the account's own reaction off with --remove. Slack reacts by name (joy, fire, eyes, or a workspace's own); the common emoji are turned into their names, and one it doesn't know is refused with reactInvalid, so give its name. Already reacted counts as done.
Without --live it is a dry run and nothing is contacted. --live reacts, only when matt asked, and needs --approvedBy (rule 8). The chat is claimed first, as for a send, and released once it is done (rule 133). The platform's own answer decides (rule 73), the governor counts it as a write, and a refusal stops with nothing retried (rule 94).
Fill the named fields; each value goes through whole. What acts on Slack is slackReactLive.
Use cases that call it, each a workflow the owner asks for, tool by tool: getGuide({ useCase: "inbox/reactions" }).
Before first using this tool in a session, load its guide: getGuide({ topic: "slack" }) (A Slack account: its session and workspaces, chats, reading, the live websocket watch and sending).
AGENTS.md (log://agents): rules 8, 13, 63, 64, 73, 83, 94, 125, 133, 140, 30, 38, 39, 40, 41. When matt says to do something, do it completely (rule 30).
slackReactLive
React to a Slack message with an emoji, on the direct-API engine; a dry run unless --live. Live on Slack: it acts there (posts, clicks, joins or changes something). Call it only when matt asked for this live run, with confirmLive: true (rules 8, 10). React to a Slack message with an emoji, on the direct-API engine; a dry run unless --live log slack react --account <accountName> --chat <chat> --message <id> --emoji <emoji> [--workspace <workspace>] [--remove] [--approvedBy <who>] [--live] [--json] Reacts to one message with an emoji, on the direct-API engine (vendor/gabrielSlack, reactions.add and reactions.remove, no browser), or takes the account's own reaction off with --remove. Slack reacts by name (joy, fire, eyes, or a workspace's own); the common emoji are turned into their names, and one it doesn't know is refused with reactInvalid, so give its name. Already reacted counts as done. Without --live it is a dry run and nothing is contacted. --live reacts, only when matt asked, and needs --approvedBy (rule 8). The chat is claimed first, as for a send, and released once it is done (rule 133). The platform's own answer decides (rule 73), the governor counts it as a write, and a refusal stops with nothing retried (rule 94). Fill the named fields; each value goes through whole. Use cases that call it, each a workflow the owner asks for, tool by tool: getGuide({ useCase: "inbox/reactions" }), getGuide({ useCase: "slack/dmReply" }). Before first using this tool in a session, load its guide: getGuide({ topic: "slack" }) (A Slack account: its session and workspaces, chats, reading, the live websocket watch and sending). AGENTS.md (log://agents): rules 8, 13, 63, 64, 73, 83, 94, 125, 133, 140, 30, 38, 39, 40, 41. When matt says to do something, do it completely (rule 30).
slackVoice
Send a voice note in matt's cloned voice as Slack's own voice note, from text; a dry run unless --live.
Send a voice note in matt's cloned voice as Slack's own voice note, from text; a dry run unless --live
log slack voice --account <accountName> --chat <chat> --text "<what matt says>" [--workspace <workspace>] [--provider inworld|fishAudio] [--mattsWords] [--translationOf "<what it translates>" --language <code>] [--approvedBy <who>] [--live] [--json]
Sends a voice note in matt's cloned voice (the voice provider, log clone voice) as slack's own voice note: an audio file shared into the chat through Slack's own upload (files.getUploadURLExternal, the bytes, files.completeUploadExternal), which Slack plays inline. It takes text only, never an audio file: the words are said in his voice, converted with ffmpeg to AAC in .m4a and uploaded on the direct-API engine through the account's one session (vendor/gabrielSlack on curl_cffi, no browser).
A voice note goes out only when matt asks for one (rule 142). The provider is --provider, else LOG_VOICE_PROVIDER, else inworld; the command only ever calls the shared voice layer (rule 143). Without --live it is a dry run: nothing is generated and nothing is contacted. --live needs --approvedBy (rule 8). It says and converts the note while the account catches up (rule 135) and the text goes through the voice gate every send runs (rule 137), so a note that passes goes out without waiting for it to be made; one the gate refuses is never sent. It then claims the chat (rule 133) and releases it after. The governor counts it as a write, the platform's answer decides (rule 73), and a refusal stops with nothing retried (rule 94).
--mattsWords says the text is matt's own words, dictated by him for this voice note: the voice gate's read is skipped, since it grades speech as typed text and as a reply, while the AI artifact check still runs and refuses aiArtifacts (rule 144).
--translationOf "<what it translates>" says the text is a translation, only when matt asked for one: the voice gate grades those original words (his own language, which the gate reads) instead of the translation, which it can't match to his writing; both are read for AI artifacts. --language <code> (zh-CN, es-ES ...) is the language the clone speaks it in, instead of the clone's own, and it is needed with --translationOf (rule 148).
Fill the named fields; each value goes through whole. What acts on Slack is slackVoiceLive.
Use cases that call it, each a workflow the owner asks for, tool by tool: getGuide({ useCase: "inbox/voiceNote" }).
Before first using this tool in a session, load its guide: getGuide({ topic: "slack" }) (A Slack account: its session and workspaces, chats, reading, the live websocket watch and sending).
AGENTS.md (log://agents): rules 8, 13, 63, 64, 73, 83, 94, 125, 133, 135, 137, 142, 143, 144, 145, 146, 147, 148, 149, 30, 38, 39, 40, 41. When matt says to do something, do it completely (rule 30).
slackVoiceLive
Send a voice note in matt's cloned voice as Slack's own voice note, from text; a dry run unless --live. Live on Slack: it acts there (posts, clicks, joins or changes something). Call it only when matt asked for this live run, with confirmLive: true (rules 8, 10). Send a voice note in matt's cloned voice as Slack's own voice note, from text; a dry run unless --live log slack voice --account <accountName> --chat <chat> --text "<what matt says>" [--workspace <workspace>] [--provider inworld|fishAudio] [--mattsWords] [--translationOf "<what it translates>" --language <code>] [--approvedBy <who>] [--live] [--json] Sends a voice note in matt's cloned voice (the voice provider, log clone voice) as slack's own voice note: an audio file shared into the chat through Slack's own upload (files.getUploadURLExternal, the bytes, files.completeUploadExternal), which Slack plays inline. It takes text only, never an audio file: the words are said in his voice, converted with ffmpeg to AAC in .m4a and uploaded on the direct-API engine through the account's one session (vendor/gabrielSlack on curl_cffi, no browser). A voice note goes out only when matt asks for one (rule 142). The provider is --provider, else LOG_VOICE_PROVIDER, else inworld; the command only ever calls the shared voice layer (rule 143). Without --live it is a dry run: nothing is generated and nothing is contacted. --live needs --approvedBy (rule 8). It says and converts the note while the account catches up (rule 135) and the text goes through the voice gate every send runs (rule 137), so a note that passes goes out without waiting for it to be made; one the gate refuses is never sent. It then claims the chat (rule 133) and releases it after. The governor counts it as a write, the platform's answer decides (rule 73), and a refusal stops with nothing retried (rule 94). --mattsWords says the text is matt's own words, dictated by him for this voice note: the voice gate's read is skipped, since it grades speech as typed text and as a reply, while the AI artifact check still runs and refuses aiArtifacts (rule 144). --translationOf "<what it translates>" says the text is a translation, only when matt asked for one: the voice gate grades those original words (his own language, which the gate reads) instead of the translation, which it can't match to his writing; both are read for AI artifacts. --language <code> (zh-CN, es-ES ...) is the language the clone speaks it in, instead of the clone's own, and it is needed with --translationOf (rule 148). Fill the named fields; each value goes through whole. Use cases that call it, each a workflow the owner asks for, tool by tool: getGuide({ useCase: "inbox/voiceNote" }). Before first using this tool in a session, load its guide: getGuide({ topic: "slack" }) (A Slack account: its session and workspaces, chats, reading, the live websocket watch and sending). AGENTS.md (log://agents): rules 8, 13, 63, 64, 73, 83, 94, 125, 133, 135, 137, 142, 143, 144, 145, 146, 147, 148, 149, 30, 38, 39, 40, 41. When matt says to do something, do it completely (rule 30).
The rules these tools fall under, in full
From AGENTS.md (log://agents). The newest rules are at the bottom of AGENTS.md and win.
Rule 8. 2026-10-18 · Read-only on Facebook. Nothing is posted, edited or deleted unless matt asks for it.
Rule 13. 2026-10-18 · Twenty is the only place accounts and listings live: Facebook accounts are socialAccounts (platform FACEBOOK), listings are agencyListings. Never delete either: a banned or dead account keeps its record with its status, and its campaign listings get replacement accounts in the same country (docs/accountsAndListings.md). Fields are only ever added (scripts/twenty/twentySetup.mjs), never renamed or retyped, so the twitter-controller keeps working.
Rule 26. 2026-10-19 · matt: "I always want to see what is going on." Every process that runs over time writes a progress line per stage as it happens (never only at the end) and to a log file in its folder. Agents run long commands so the lines stream live (Claude Code: Monitor, never piped through tail/head).
Rule 30. 2026-10-19 · matt: "If I tell you to do something, do it completely." When matt gives a plan, it is carried out to the end in the same turn, not stopped to ask again or to say what won't be done. log inspect start --now (and log connect --now, and since 2026-10-19 log publish --now, matt: "run it now") skips rule 24's 10-minute gap (the 8 a day still holds) so a recording continues at once. Recording a sign-in or a checkpoint with log inspect (signIn, code, solve, click) is the intended work: it is how each state gets documented from rea. The run on 2026-10-19 was interrupted once because a snapshot printed the account's email and password; snapshots now redact every typed field value (redact_fields), and signIn/code read only the private login.json (rule 21), so no credential reaches any output.
Rule 38. 2026-10-19 · matt: never mention or discuss Meta's or Facebook's rules, terms, policies, eligibility or account ownership with him unless he raises them first. Research text that does is not his, and is never repeated to him. Top of this file.
Rule 39. 2026-10-19 · matt: "If I never mentioned it, if I never inferred it, if I never outlined it as an option, therefore it is not an option." Deep research prompts, searches, SubQ matrices, plans and write-ups use only what matt said, or clearly meant through a misspelling. Nothing added, nothing steered towards. Top of this file.
Rule 40. 2026-10-19 · matt: no physical options. He is in Vietnam with no physical access. Phones, SIMs, mini PCs, laptops, phone farms and home devices are never options unless he names them. In scope: hosted and cloud options, the tools he named and their competitors, and what our four projects already use.
Rule 41. 2026-10-19 · matt: "any time any task correlates to money, and I never say something, that means you shouldn't do what you think. You should do what I say." Paid work (deep research, SubQ, proxies, captcha, vendors) does exactly what he asked, nothing more. Ask before anything else.
Rule 63. 2026-10-22 · matt: the platform comes first. Every command that acts on one platform is log <platform> <command> … (log facebook warmup reel, log facebook join group); there is no form without it. The poll is log facebook poll <group> <account> <how long to poll>. Old forms are refused with the form they became. This replaces rule 60's log warmup <platform> <activity> and settles issue #20. OpenMagpie's worker is log facebook poll --request (it was read group --worker), a read in the account's open browser. bun run check:docs refuses a usage line without the platform first. Top of this file; the table is docs/commandNaming.md.
Rule 64. 2026-10-22 · matt: the code is by platform first, as the commands are, "because each has different approaches": src/<platform>/<area>/<domain>. Facebook's groups are src/facebook/group/{join,read,answer} (were src/group/…, rule 45) and its warm-up src/facebook/warmup/activities and src/facebook/warmup/stages (were src/warmup/facebook and src/warmup/facebookStages, rule 49). What every platform's warm-up draws from stays shared: src/warmup/persona, src/warmup/viewer. Each area has an index.ts (check:naming). Top of this file.
Rule 73. 2026-10-23 · matt: "IT MUST ALWAYS READ THEM." The number one rule of the stack, above rule 30 and every other: Facebook's internal API answers are the decision-making factor of what and when something is relevant, and the deterministic factor of the system. Every browser step reads the internal API answers it triggers and the requests it sent, keeps them past the prune, and decides on them, never on a page loading, a timer or the step having run. The MCP instructions put it first. Inspect steps read theirs since rule 74, and every flow, warm-up and host step on AdsPower that makes a call since 2026-10-23; the Camoufox-only machines don't yet (#35). Top of this file.
Rule 83. 2026-10-25 · matt: a new platform is made with one command, written normally: log new platform <name> (MCP newPlatform). It writes the platform's account session on the shared src/platformKit/session, log <name> session check with its MCP tool, a direct-API engine in python/<name>Api on the shared python/platformKit/engine.py, and docs/platforms/<name>/README.md, and wires it into the repo by editing each file through its syntax tree (PLATFORMS, PLATFORM_LABELS, COMMANDS, createContext, CommandContext, DIRECT_ROUTES, COMMAND_RULES), checking every edit landed before anything is written. Without --apply it only lists what it would do; nothing is overwritten; it names what Twenty is missing (the socialAccounts.platform option) and never changes Twenty. Routes and rules are keyed by the platform-qualified name (commandKey: read, twitterSession, redditSession), so two platforms' commands never share one. lefthook's platforms (bun run check:platforms, pre-commit and pre-push) refuses a platform missing its label, src, command, engine, session route, rules or docs. src/scaffold/platform, src/cli/new.
Rule 90. 2026-10-28 · matt: "stop sending as one." A message drafted on several lines goes out one message per line, in every chat, whatever matt's habit there (splitPlan, src/voice/pairs). Only --asOne sends it as one, and only when matt asks for that. A one-line draft is still split the way he writes in that chat.
Rule 94. 2026-10-29 · matt: "whatever you did in such quick succession lead to my account getting disabled, never do that again." Never act on an account in quick succession. What happened (matt's Discord, sourcedandsoldtothehighestbidder, 2026-10-29 00:47-01:00 UTC): in about 13 minutes an agent replied in 3 chats, read 1,400 messages from a server (Finn Automated) with a scratch script that went around log-cli (.scratch/finn/dc.ts: its own fetch to discord.com/api/v9 with the session token, pages back to back, retrying straight through 429s), pulled the account's relationships, then opened new DMs to strangers from that server 30 s apart. Discord disabled the account. From now on: - Every call to a platform goes through log-cli's own commands and their governor. Never a hand-rolled script with an account's token or cookies. - A 429 or a failed send stops the run. It is never retried through, and the next action waits for matt. - Reading a server, group or member list in bulk is its own session. It never runs in the same session as sending. - Cold DMs to people the account has never talked to go out at most a few a day, minutes apart, never back to back. Each new DM channel opened counts as a write. - After a block, disable, captcha or cooldown, nothing resumes on that account until matt says so.
Rule 111. 2026-11-02 · matt: find GitHub repos "good for claude watermarking ... so we're able to pass it through context into the sends so that we can see how likely it is to be watermarked". Claude's own watermark is statistical (word choice under a secret key) and Anthropic's text detector is private preview, so no public repo can score it (RESEARCH/CLAUDE_WATERMARK_REPOS_2026_11_02.md). What can be read is what AI chat interfaces leave in text: OfirYC/claude-watermark-checker is vendored at vendor/claudeArtifactDetector (MIT, no dependencies) and every send's copy is read by it (watermarkReadingOf, src/twitter/copy/watermark): clean, stylistic (curly quotes, em dashes) or artifacts (zero-width or other hidden characters, odd spaces, pasted Claude HTML), with statistical: unknown always said. planSend refuses a send with artifacts (aiArtifacts) and carries the reading in its plan; log voice shows it and fails a draft with artifacts; a direct send and each queue send log it.
Rule 123. 2026-11-03 · matt: "make it a rule all platforms need the web socket hooks stuff there". Every platform log-cli talks to gets the live hooks X and Discord have. Its account's one session holds the platform's own push channel open: X Chat's socket, Discord's gateway, Slack's wss-primary.slack.com. That is never a poll from a second client. The inbox hook starts that watcher for every account and puts each new message, mention and reply in front of the agent with what to do (>>> NEW MESSAGE), per session (rules 105, 108, 115). It also stops a watcher running older code, skips an account cooling off, and stops on any refusal (rule 94). A platform isn't done until its watcher is in the inbox hook (scripts/hooks/inbox.ts, src/inbox/watch), starting with Slack. Reddit and LinkedIn aren't built yet. Spec: docs/specs/agentHooks.md (REQ-HOOKS-12).
Rule 125. 2026-11-03 · matt: "Check slack via cli it needs to be swapped to use curl_cffi same as X". Slack goes out looking like a browser, the way X (rule 81) and LinkedIn (rule 102) do. vendor/gabrielSlack reaches Slack, its web API, its cookie exchange and its websocket (wss-primary.slack.com), only through gabriel/core/transport.py: one curl_cffi AsyncSession made by new_session, pinned to one impersonation target (IMPERSONATE, chrome150), sending that target's own user agent (USER_AGENT, which the client's default metadata uses), never httpx, websockets, aiohttp, requests or a socket of its own. It keeps no cookies between calls: the d cookie is sent on each one. gabriel's own login CLI, which drove Chrome through httpx and websockets, is removed. python/checks/slackTransport.py maps every transport touchpoint by syntax tree (--map), rewrites what has a curl_cffi equivalent (--apply) and refuses the rest (--check, bun run check:slackTransport, pre-commit and pre-push). Spec: docs/specs/slack.md (REQ-SLACK-05).
Rule 126. 2026-11-03 · matt: every voice note he was sent or sent, transcribed with AssemblyAI, and "inside of the discord layer we have that and then the social media platforms at large, how can we detect when we have audio". Audio is detected once, in the shared message layer (python/platformKit/media.py, src/platformKit/media), never per platform: a content type of audio/* or video/ogg, an extension (.ogg .oga .opus .m4a .mp3 .wav .aac .webm), or the platform's own flag (Discord's voice-message flag 8192 with its duration and waveform, X Chat's audio media, Slack's slack_audio file). Every platform's messages keep their attachments, and an audio one adds (audio: <name>) to the text. Whatever is audio goes through one AssemblyAI path (log transcribe, MCP transcribe; src/media/voiceNotes): kept once by its file's hash in ~/.logCli/transcripts, written into the conversation store as (voice note: "<what was said>") so search, voice and SubQ see it, and done as they come in discordRead, twitterDms, slackRead and the inbox. Files come only through the platform's own engine and session (X's one session, Discord's sound search, Slack's own transport), and any refusal stops the run (rule 94). The key is ASSEMBLYAI_API_KEY or ASSEMBLY_AI_KEY (.env.local, ~/.buzz/.env.local or ~/filmcraft/.env.local), never printed. log new platform writes the attachments into a new platform's message shape, and check:platforms refuses an engine whose messages drop them. Spec: docs/specs/voiceNotes.md.
Rule 130. 2026-11-04 · matt: "we need to make it so we have a monitor on there that will then listen for the new info coming over via the websocket, because look, he responded halfway through ... he just asked something, it breaks the flow". On 2026-11-03 Abel wrote "is this ai" on Discord while an 8-line send was going out, and lines 6 to 8 went on top of his question. A message sent one line at a time stops the moment they write: before each line after the first, the send reads what the platform's push channel put in the inbox for that chat since the send began (incomingSince, src/inbox/watch, from pending.jsonl), and if they wrote, the rest is held and the answer leads with >>> INTERRUPTED: what they said and the lines not sent (interruptedLines). It holds for log discord send, log slack send, log twitter dm (old DMs and X Chat), the queue's dms and log instagram send (in its engine). What was sent stays sent, nothing is resent, and they are answered first (rule 109). An agent following a conversation keeps a Monitor on ~/.logCli/inbox/pending.jsonl so each push wakes it. Spec: docs/specs/messageSplit.md (REQ-SPLIT-08).
Rule 131. 2026-11-04 · matt: "WE NEED TO LOUDLY ALERT THAT THIS CANNOT HAPPEN ... WE CAN ONLY HAVE ONE PROCESS PER ACTUAL THING". On 2026-11-04 every watcher was running two or three times (X Chat for both X accounts, Discord and Slack): each new Claude session's hook started them again, the cleanup only killed the pid in watch.pid, which the new one had overwritten, and the doubled X watchers polled X's badge counts in bunches until X answered 429 on both accounts. One watcher process per account per platform, always, and every Claude session subscribes to it through the inbox (pending.jsonl), never by starting its own. Each watcher takes watch.lock (exclusive create, its pid) before it opens anything (claimWatcher, src/inbox/watch); a second one is refused with watcherAlreadyRunning and exits, and the refusal goes to ~/.logCli/inbox/alerts.jsonl, which the inbox hook puts first in the agent's context (>>> ONE PROCESS PER ACCOUNT, NEVER TWO) to be told to matt. A dead holder's lock is taken over, only the holder releases it, the hook never starts a watcher while a live one holds the lock, and the stale-code cleanup stops the holder as well as the pid file's process. Tests run the Instagram engine inline (LOG_INSTAGRAM_HOST=inline in the preload) so they never leave a real session host behind. Spec: docs/specs/agentHooks.md (REQ-HOOKS-13).
Rule 132. 2026-11-04 · matt: "this is what I expect to happen with all platforms": a singleton primary with an exclusive lock and a multi-subscriber relay. On every platform (X Chat, Discord, Slack, Instagram) the first watch to take an account's watch.lock is its one owner (ownOrAttach, src/inbox/relay): it opens the platform's push connection, writes session.json (pid, hostname, started, a heartbeat every 5 s, status connecting, streaming, reconnecting or error, its relay port and token, and how many are attached), writes the inbox (pending.jsonl) as its only writer, and sends every event to every attached subscriber over its local relay (127.0.0.1, the token first). log <platform> watch is the attach command: when an owner is live, a watch attaches to its relay and streams the same events without opening anything, so the hooks, a Monitor, Codex or another agent all see one stream; an inbox watch (--inbox) that loses is refused loudly (rule 131). An owner whose heartbeat is quiet for 60 s is stopped and alerted (>>> ONE PROCESS PER ACCOUNT, hungOwner), and the next start becomes the owner; when the owner closes, its session file goes with it. Spec: docs/specs/agentHooks.md (REQ-HOOKS-14).
Rule 133. 2026-11-04 · matt, to deepman: "whenever the agent wants to perform an action based on the streamed event information, it must first claim the relevant event ... only the session that successfully claims an event should be allowed to perform the corresponding action. Once the action has been executed through the state machine, the lock is released ... every time an action is executed, the system should also index and persist the associated information". Every message a push channel brings in has an event id (eventIdOf, shown in the hook's >>> CLAIM BEFORE YOU ACT beside who holds its chat). A session claims a chat before it drafts (log inbox claim, MCP inboxClaim, 10 minutes, renewed by claiming again; claimChat, src/inbox/claims, an exclusive claim file per chat, the session being its MCP server's process), and a second session is refused (chatClaimedElsewhere). Every send (log discord send, log slack send, log twitter dm, the queue's dms, log instagram send) claims the chat itself first (guardSend) and releases it once it is sent, interrupted or failed, so two sessions never answer the same chat. An X chat is one chat whether it is named by the person's id or the X Chat pair. A claim held by a session that ended, or past its time, frees itself. Every claim, renewal, refusal, send, interruption, failure and release is written to ~/.logCli/inbox/actions.jsonl, read with log inbox actions --chat <id> (MCP inboxActions) before acting; log inbox claims lists who holds what. Spec: docs/specs/agentHooks.md (REQ-HOOKS-15).
Rule 134. 2026-11-04 · matt: "Yes, go", moving the inbox's state out of loose files into one SQLite database, ~/.logCli/log.db (LOG_DB moves it; src/inbox/store, WAL, every write in a BEGIN IMMEDIATE transaction). The owner appends every event its push channel brings in to events with a sequence number, once (appendEvent, by its event id), and each session reads from its own cursor (cursors), so nothing is shown twice or lost between sessions; pending.jsonl is imported once and no longer written. An event type nobody has catalogued is stored with its raw payload and raised loudly (unknownEventType, eventTypes), to be named from rea, the docs and SubQ (rule one). The one owner per account (rules 131, 132) is a row in owners (pid, hostname, heartbeat, status, relay port and token, subscribers, a generation that rises with each new owner), never session.json, and a relay subscriber that falls more than 1 MB behind is dropped rather than slowing the owner. Claims (rule 133) are rows in claims, each with its chat's next generation, and a send checks before every part after the first that its claim is still the one held (stillHeld); a session whose claim was taken over stops, and the rest is held. Every claim, send and release goes to actions in the same transaction as the change it records, with what was done in the chat before it (stateBefore), and what was done is searchable with the events (searchIndex, FTS5). Each event's moments are timed (timings: received, appended, relayed, claimed, actionStart, confirmed, indexed). Alerts are rows in alerts. Spec: docs/specs/agentHooks.md (REQ-HOOKS-16).
Rule 135. 2026-11-04 · matt (through his side session): missing context "is filled by catching up, never by every caller re-reading the client". Each account's owner keeps the platform's own last position in the .db (positions: X Chat's sequence id, Discord's last message id per DM, Slack's last ts per chat, Instagram's last activity). A gap opens (gaps: from, to, reason, status) when an owner takes over from a stopped or dead one (from its last heartbeat), when the first owner starts on an account that already has events (from the newest), or when the push channel drops. While a gap is open the account is behind, and one catch-up job inside the owner (catchUpRunner, src/inbox/catchUp) pulls only the delta through the account's own session and governor, paced like reads (0.8 to 2.5 s): X's delta sync after its saved sequence id, Discord's DMs after each one's saved message id, Slack's history after each chat's saved ts (oldest), and Instagram's threads active since its last activity. What it pulls is appended to events like live pushes, marked source: catchUp, and the gap closes. A send (Discord, Slack, X dm and the queue's sends, Instagram) waits up to 90 s for the account to catch up (waitCaughtUp) and never reads the platform itself; still behind, it is refused with accountBehind. A refused catch-up (a 429 or any refusal) stops, keeps the account behind, alerts (catchUpRefused), and nothing resumes until matt says so (rule 94). Discord's gateway RESUME is not used yet. Spec: docs/specs/agentHooks.md (REQ-HOOKS-17).
Rule 137. 2026-11-04 · matt: keep every part of the voice gate ("necessary") and run it "as the whole voice process on the new .db", required inside every send, only at send time, scoped to that send, quiet; then "Yes, both" to jina-embeddings-v5-omni and the local media model. The voice gate is one step (gateSend, src/cli/twitter/helpers/voice.ts) inside log discord send, log slack send, log twitter dm, the queue's sends and log instagram send, after the account has caught up (rule 135) and before the chat is claimed (rule 133): a draft that fails is refused with voiceGateFailed whether or not log voice ran first, and in the X queue the item is held to be rewritten with twitterQueueEdit. All five checks stay: SubQ's read of matt's voice, the AI tells index with its reranker (rule 112), his own phrasing (rule 127), the watermark checker (rule 111) and the scrambler (rule 127). It reads the chat's context from the .db (rule 136), embeds it, ranks it against the moment being answered (cosine, then jina-reranker-v3.5) and gives SubQ the top moments in full (rankedContext, thisConversationRankedForThisMoment). Only embeddings are kept, in the .db (embeddings: the text's hash, model, dimensions and vector, each made once), with the AI tells index's phrase list and matt's baseline (aiIndexes); every result is worked out fresh for each send, never saved. Jina is jina-embeddings-v5-omni-small at 1024 dimensions (text-matching), and an index made with another model is rebuilt. The local embedder stays loaded in its process. A pass is one line; a fail names only what failed and why. Spec: docs/specs/voiceGate.md (REQ-VOICE-09 to 11), docs/specs/aiTells.md (REQ-AITELLS-05, 10, 12).
Rule 138. 2026-11-04 · matt: "a smaller local model" turns every piece of media into "a text representation", labelled with what it is, after a message went out answering only the text around a video someone sent; then "Yes, both" to downloading it. One media describer per machine (log media describer, src/media/describer), started by the inbox hook and locked like an owner (rule 131, a second is refused), loads onnx-community/LFM2.5-VL-450M-ONNX once (SubQ's pick, q4, on the transformers.js runtime log already uses) and keeps it warm. Every image, meme, gif and video a watch receives goes into the .db's media queue (enqueueAttachments). The describer takes one at a time, describes a video from three frames (ffmpeg), labels each description with its kind, stores it with its jina-embeddings-v5-omni-small vector, beats every 5 s, and stops loudly past its memory ceiling (describerMemory). The queue holds 20 at most: past that, an item is marked not described yet and alerted (mediaQueueFull). Media with no description is shown as [video clip.mp4 not described yet: …] in the inbox hook and in the voice gate's ranked context, so nobody replies to it blind; a description reaches both in its place. Audio stays with AssemblyAI (rule 126). A file the platform engine hasn't saved locally yet is listed as not described, with why. log media describe <path> queues one local file, and log media status shows the queue. Spec: docs/specs/mediaDescriber.md.
Rule 140. 2026-11-04 · matt: "we're able to send reactions as well, across all of the clients. We need to be able to send reactions to messages as well." Every chat platform reacts to a message with an emoji through its own engine and its one session, never the browser: log twitter react, log discord react, log slack react and log instagram react (MCP twitterReact, discordReact, slackReact, instagramReact, each with its …Live twin), --remove taking the account's own reaction off. Each calls the platform's own reaction call: X's DM reaction mutation in vendor/twikit (an X Chat id is tried as the same pair, and X's answer decides), endcord's send_reaction, Slack's reactions.add (by name; the common emoji are turned into their names, python/platformKit/reactions.py) and instagrapi's direct_send_reaction. A dry run unless --live with --approvedBy (rule 8). The chat is claimed first and released after, as for a send (rule 133), the governor counts it as a write, the platform's answer decides (rule 73), and a refusal stops with nothing retried (rule 94). src/cli/react. Spec: docs/specs/reactions.md.
Rule 142. 2026-11-05 · matt: "make it so that we can send audio, like upload natively into the platform, voice notes ... directly into the actual clients themselves ... we need to voice clone ourselves from audio ... it will then be an extension, like we have for the meme board stuff", with Fish Audio (hosted) his pick. A voice note only ever goes out when matt asks for one, in his cloned voice, as the platform's own voice note: never on the agent's own initiative and never any other audio. Fish Audio is an extension (src/extensions/fishAudio, docs/extensions/fishAudio.md): log fish clone makes his voice model once from the voice notes he sent himself (fromMe in the conversation store or his ownMessage in the .db; Discord voice messages and Instagram voice clips the engines kept, 10 to 60 s in all; nobody else's voice), a dry run listing them unless --apply, its id kept in ~/.logCli/voice/fishClone.json; log fish speak --text says words in it to a file under ~/.logCli/voiceNotes and sends nothing, on the free s2.1-pro-free model unless --model. log discord voice, log instagram voice and log slack voice take text only (an audio file is refused with voiceNoteInvalid), are dry runs unless --live with --approvedBy (rule 8), wait for the account to catch up (rule 135), run the text through the voice gate (rule 137), make it with the clone, convert it with ffmpeg (Discord ogg/opus, sent as a voice message with flags 8192, its waveform and duration; Instagram AAC in .m4a through instagrapi's direct_send_voice, its upload on curl_cffi under the one-call lock, rule 129; Slack AAC in .m4a through files.getUploadURLExternal, the upload on gabriel's curl_cffi transport and files.completeUploadExternal, rule 125), claim the chat (rule 133), count as a write, and stop on any refusal (rule 94). An engine sends only a file log made under ~/.logCli/voiceNotes. X isn't built: twikit has no audio send and no vendored code or doc shows X's DM voice upload, so nothing is guessed. src/cli/voiceNote. Spec: docs/specs/voiceClone.md (was fishVoice.md).
Rule 143. 2026-11-05 · matt: "the voice cloning and the voice note stuff will be agnostic", after Fish Audio refused the clone without API credit (403, model creation limit 0/0) and he had none to put in. The voice layer is provider-agnostic, the way proxies are (rule 52): src/extensions/ voiceClone holds one interface (its models, cheapest model, clone limits, the reference format it takes, clone and speak), and each provider is an extension of its own: src/extensions/inworld (the default: instant cloning free, about 70 free speech minutes, wav references 3 to 30 s, inworld-tts-2-flash, INWORLD_API_KEY) and src/extensions/fishAudio. The provider is --provider, else LOG_VOICE_PROVIDER in .env.local, else inworld. log fish clone|speak became log clone voice|speak (MCP cloneVoice, cloneSpeak); log fish is refused with the form it became (rule 63's pattern). Each provider's clone is kept in ~/.logCli/voice/clone.json, keyed by provider, made once. log discord|instagram|slack voice take --provider and call only the shared layer, never a provider. Everything else in rule 142 holds. Spec: docs/specs/voiceClone.md.
Rule 144. 2026-11-05 · matt: "bro fuckign send th efuckign shit", after the voice gate refused three times the voice note he dictated to deepman word for word ("my phone screen is broken the phone is fine like theres two big green lines going down the side of it and that but it still works thank the lord"). The gate grades speech as typed text and as a reply (voice 0.06 to 0.48, inContext 0.21), so it can't read a voice note in his own spoken words. --mattsWords on log discord|instagram|slack voice (MCP mattsWords) says the text is matt's own words, dictated by him for this voice note: the voice gate's SubQ read is skipped, and the AI artifact check (watermarkReadingOf) still runs and refuses aiArtifacts. Only when matt gave the exact words; words the agent drafted are always gated (rule 137). Sent live 2026-11-05 08:04 UTC, Discord message 1567811929500811294. Spec: docs/specs/voiceClone.md (REQ-VOICE-CLONE-10).
Rule 145. 2026-11-05 · matt: "Yes, re-clone it", because the Inworld clone was made with languageCode en-US, which pulled his voice towards an American accent, and he is Northern Irish. The voice is cloned and spoken in his accent: LOG_VOICE_ACCENT (.env.local, default Northern Irish) and LOG_VOICE_LANGUAGE (default en-IE, sent again as en-GB when Inworld answers INVALID_ARGUMENT). The clone sends that language, a description naming the accent and each sample's AssemblyAI transcript from ~/.logCli/transcripts when there is one (none is paid for: TTS-2 makes its own), and keeps the language Inworld accepted. log clone voice --apply --replace (MCP replace) makes it again over the old one, which stays in the clone file under previous, never deleted. Speech sends the clone's language; on inworld-tts-2 the text starts with [say it in a <accent> accent], an instruction Inworld removes before synthesis, and inworld-tts-2-flash, which ignores steering, gets none. Spec: docs/specs/voiceClone.md (REQ-VOICE-CLONE-11). (Its accent steer was replaced by rule 147, matt 2026-11-05.)
Rule 146. 2026-11-05 · matt: "the second one but why does it speak so slowly" and "use the discord audio for the samples". The clone speaks at LOG_VOICE_SPEED (.env.local, default 1.15, kept between 0.8 and 1.5), sent as Inworld's speakingRate, and a clone made on Inworld speaks with inworld-tts-2 (the provider's preferred model, which takes emotion tags), not the cheapest. log clone voice --from <file>[,<file>] clones from recordings of matt's own that he names instead of his voice notes, each one's words read from a .txt of the same name beside it. Spec: docs/specs/voiceClone.md (REQ-VOICE-CLONE-12).
Rule 147. 2026-11-05 · matt: "ITS IRISH", none of the accent-steered clips sounded like him, "11_script_long272_b ... was pretty good", then "go through my voice notes ... analyse it", "it still doesn't have that distortion", and "op is real like op not o p ... make it the gate for the voice ... or it won't pass". A voice note sounds like his own voice notes and says what was written, measured both times: - The clone is Inworld instant cloning from one continuous 30 s of his own Discord voice note (matt-long272_b, 2:30 into his 4.5 minute note), the accent Irish (LOG_VOICE_ACCENT). No accent instruction is sent with the text: the clone carries the accent. On inworld-tts-2 breaths are put in ([breathe] at the start and once at least 8 words have gone by, withBreaths). - Shaped like his notes (processLikeMatt, src/extensions/voiceClone/helpers/realism.ts), every number measured from five of his real Discord voice notes against the clone: phone squash (dynaudnorm), the phone mic's own tone curve measured from his notes (REALISM.phoneCurve: its low cut under 160 Hz, its dip around 2 to 3 kHz and its dull top over 5 kHz), his slow mic drift (1.9 dB), tone sway as the phone moves, phone-mic saturation on loud words, and his own room tone taken from the gaps in his own voice notes (never a sound effect), laid 22 dB under the voice so it lands where his do (30 to 33 dB). Seeded by the text, so the same words come out the same. - Listened back (readSpoken, spoken.ts): the shaped note is transcribed with AssemblyAI's word timings and lined up with the text. A word in the spoken lexicon (SPOKEN_LEXICON: op said as a word, written OP for the clone, since a lower-case op came out as "up") must be heard as itself, as one word under 400 ms; spelled out, heard as something else, or more than a quarter of the words different, and it is refused with spokenMismatch, naming what was heard. An acronym the lexicon doesn't have is refused before anything is spoken (pronunciationUnknown): how matt says it is never guessed. A note that can't be listened back is never sent (spokenCheckFailed). It runs in log clone speak and every log discord|instagram|slack voice. This replaces rule 145's accent steer. Spec: docs/specs/voiceClone.md (REQ-VOICE-CLONE-13, 14).
Rule 148. 2026-11-05 · matt: "send it in chinese aswell", then "ADD A BYPASS PERMISSION FOR IT BECAUSE OFC IT WONT MATCH UP", after the voice gate scored the Chinese translation of a voice note it had passed in English at 0.13: it compares a draft with matt's own writing, which is English. --translationOf "<the original>" on log discord|instagram|slack voice (MCP translationOf) says the text is a translation matt asked for: the voice gate grades the original words instead of the translation, both are read for AI artifacts, and it needs --language <code> (zh-CN, es-ES ...), the language the clone speaks it in instead of the clone's own (Inworld's language). With en-IE the clone said the Chinese with two words wrong; with zh-CN it said them right. It is only for a translation matt asked for, never paired with --mattsWords. The listen-back check (rule 147) reads Chinese, Japanese and Korean one character at a time (wordsOf), so a translated note is checked too. Spec: docs/specs/voiceClone.md (REQ-VOICE-CLONE-15).
Rule 149. 2026-11-05 · matt: "it keeps doing this weird exhale at the beginning ... the lengths of the things are always like 5 seconds", then "READ THE CONVERSATION ... I'M ASKING YOU TO RESPOND AT LENGTH ... BUILDING ON WHAT HE'S SAYING AS A 40 SECOND VOICE NOTE IN ENGLISH, YET YOU'RE JUST RE-EXPLAINING SOMETHING I ALREADY SAID". Three things: - No breath at the start. withBreaths never opens a note with [breathe] (that was the exhale). A breath goes in only between sentences (.!? and the Chinese 。!?), after a gap of 8 to 18 spoken words drawn from the text's own seed (BREATH_WORDS; a Chinese character counts half), so no two notes breathe alike. - The pace drifts. Each note is spoken at LOG_VOICE_SPEED moved by up to 7% from its text's seed (paceOf, PACE_JITTER), within the speed range. - A voice note is written at the length matt asks for and builds on the whole conversation. Read the whole chat first, then write to the length asked (a 40 s note is about 100 to 110 words), taking what they are talking about further and adding to it, never re-explaining what matt already told them. Note lengths vary: not every note is three short lines. The voice gate grades a voice note as spoken (spoken, set by every log discord|instagram|slack voice and by log voice --spoken): its length and punctuation never count against it, offProfile (his typed habits in the chat) isn't applied, and a new question, buildsOn (0.7 to pass), refuses a note that repeats what matt already said or ignores what they are talking about. Its voice question reads how matt actually talks first: the transcripts of his own voice notes from before the clone existed (howMattTalks, mattsOwnVoiceNotes), never his typing alone and never a note the clone made. Spec: docs/specs/voiceGate.md (REQ-VOICE-13), docs/specs/voiceClone.md (REQ-VOICE-CLONE-16).