discoclaw 1.2.1 → 1.2.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.
@@ -96,6 +96,8 @@ Notes:
96
96
  - `generateImage` supports two providers: **OpenAI** (models: `dall-e-3`, `gpt-image-1`) and **Gemini** (models: `imagen-4.0-generate-001`, `imagen-4.0-fast-generate-001`, `imagen-4.0-ultra-generate-001`). Provider is auto-detected from the model prefix (`dall-e-*`/`gpt-image-*` → openai, `imagen-*` → gemini) or set explicitly via the `provider` field. OpenAI provider uses `OPENAI_API_KEY` (required) and optional `OPENAI_BASE_URL`. Gemini provider uses `IMAGEGEN_GEMINI_API_KEY`. At least one key must be set when `DISCOCLAW_DISCORD_ACTIONS_IMAGEGEN=1`. Default model is auto-detected: if only `IMAGEGEN_GEMINI_API_KEY` is set, defaults to `imagen-4.0-generate-001`; otherwise defaults to `dall-e-3`. Override with `IMAGEGEN_DEFAULT_MODEL`.
97
97
  - `spawnAgent` is enabled by default (`DISCOCLAW_DISCORD_ACTIONS_SPAWN=1`; set to 0 to disable). Spawned agents run fire-and-forget: each agent runs its prompt via the configured runtime and posts its output directly to the target channel. Multiple `spawnAgent` actions in a single response run in parallel (bounded by `DISCOCLAW_DISCORD_ACTIONS_SPAWN_MAX_CONCURRENT`, default 8). Spawn is disabled for bot-originated messages and excluded from cron flows to prevent recursive agent chains. Spawned agents run at recursion depth 1 and cannot themselves spawn further agents.
98
98
 
99
+ Action guard (false completion detection): When a reply's visible text claims Discord-managed work was performed or is being performed — in any tense (present progressive, future intent, past tense, or perfect tense) — but the turn produced zero actionable `<discord-action>` blocks and zero executed action results, the finalizer appends a visible warning. This catches fabricated completion claims ("Posted the plan", "I've sent the message") at the output boundary so the user sees the discrepancy immediately. The guard is implemented in `output-common.ts` (`claimsImmediateDiscordActionIntent` + `appendPromisedDiscordActionWithoutExecutionNotice`) and requires no prompt-layer changes — it operates purely on the finalized reply text and action execution counts.
100
+
99
101
  Auto-follow-up: When query actions (channelList, channelInfo, threadListArchived, forumTagList, readMessages, fetchMessage, listPins, memberInfo, roleInfo, searchMessages, eventList, taskList, taskShow, cronList, cronShow, planList, planShow, memoryShow, modelShow, forgeStatus) succeed, DiscoClaw automatically re-invokes Claude with the results. This allows Claude to reason about query results without requiring the user to send a follow-up message. Controlled by `DISCOCLAW_ACTION_FOLLOWUP_DEPTH` (default `3`, `0` disables). Mutation-only responses do not trigger follow-ups. Trivially short follow-up responses (<50 chars with no actions) are suppressed.
100
102
 
101
103
  Requirements:
@@ -236,6 +238,18 @@ grep DISCOCLAW_STATUS_CHANNEL .env
236
238
  journalctl --user -u discoclaw.service --since "5 min ago" --no-pager | grep -i "status"
237
239
  ```
238
240
 
241
+ ### Bot claims it performed a Discord action but nothing happened
242
+ **Symptom:** Bot says "Posted the plan to #general" or "I've sent the message" but no message appears in the target channel.
243
+ **Cause:** The model fabricated a completion claim without emitting a `<discord-action>` block. The action guard should append a visible warning to the reply.
244
+ **Verification:**
245
+ 1. Check whether the reply ends with a warning starting with `Warning: this reply says Discord-managed work was performed`.
246
+ 2. If the warning is present, the guard is working — the model hallucinated the action. No code fix needed; the warning tells the user.
247
+ 3. If no warning is present but the action still didn't execute, check whether the action block was parsed but failed during execution (look for "Action Failed" in the status channel or bot logs).
248
+ ```bash
249
+ # Check recent action failures
250
+ journalctl --user -u discoclaw.service --since "5 min ago" --no-pager | grep -i "action.*fail\|warning.*discord-action"
251
+ ```
252
+
239
253
  ### Messages split awkwardly across Discord's 2000-char limit
240
254
  **Symptom:** Bot replies are split mid-sentence or mid-code-block, producing garbled formatting.
241
255
  **Cause:** The chunking algorithm tries to preserve code fences but can't always split cleanly if the response has deeply nested or very long code blocks.
package/.context/pa.md CHANGED
@@ -40,6 +40,12 @@ Your prompt may include:
40
40
  - **Durable memory** — Persistent user facts/preferences. Treat as ground truth unless contradicted.
41
41
  - **Conversation memory** — Rolling summary, lossy. Trust recent messages over summary if they conflict.
42
42
 
43
+ ## False Completion Claims
44
+
45
+ The runtime action guard (`output-common.ts`) catches both present-tense intent ("I'm posting…") and past-tense completion claims ("Posted the plan to the channel", "I've sent the message") that lack matching `<discord-action>` blocks. When the guard fires, it appends a visible warning to the reply so the user sees the discrepancy immediately.
46
+
47
+ No separate prompt or instruction change is needed to address this gap — the guard operates at the output finalization boundary and catches false claims regardless of how the model phrases them. If the model fabricates a completion claim, the warning surfaces it before the user has to ask why nothing happened.
48
+
43
49
  ## Autonomy Tiers
44
50
 
45
51
  **Always OK:** Read files, explore, search web, run diagnostics, send Discord messages, react, share finds, work within workspace.
package/README.md CHANGED
@@ -6,522 +6,85 @@
6
6
 
7
7
  A personal AI orchestrator that turns Discord into a persistent workspace — built on three pillars: **Memory**, **Tasks**, and **Automations**.
8
8
 
9
- DiscoClaw is an orchestrator: it coordinates between a user interface (Discord), one or more AI runtimes (Claude Code, OpenAI, Codex), and local system resources — managing conversation state, task routing, scheduling, and tool access. The intelligence is rented; the coordination is owned.
9
+ [![npm version](https://img.shields.io/npm/v/discoclaw)](https://www.npmjs.com/package/discoclaw)
10
+ [![license](https://img.shields.io/npm/l/discoclaw)](LICENSE)
11
+ [![node](https://img.shields.io/node/v/discoclaw)](package.json)
10
12
 
11
- It turns a private Discord server into a persistent AI workspace. Your assistant remembers you across sessions, tracks work in forum threads, and runs scheduled tasks autonomously — all through natural conversation.
13
+ > Turn Discord into a persistent AI workspace memory, tasks, automations, and voice, all through natural conversation.
12
14
 
13
- It's designed for a single user on a fresh, private server your own sandbox. Not a shared bot, not a multi-user platform. Just you and your assistant in a space you control.
15
+ DiscoClaw coordinates between Discord, AI runtimes (Claude Code, OpenAI, Codex, Gemini, OpenRouter), and local system resources. The intelligence is rented; the coordination is owned. Designed for a single user on a private server — your own sandbox.
14
16
 
15
- No gateways, no proxies, no web UI to deploy — Discord *is* the interface. Run DiscoClaw on a Linux or macOS machine (see [Platform support](#platform-support)) and talk to your assistant from anywhere Discord works: desktop, mobile, browser.
16
-
17
- The codebase is intentionally small — small enough to read, audit, and modify directly. Customization means changing the code, not configuring a plugin system.
18
-
19
- ## Why Discord?
20
-
21
- Discord gives you channels, forum threads, DMs, mobile access, and rich formatting for free. DiscoClaw maps its three core features onto Discord primitives so there's nothing extra to learn — channels become context boundaries, forum threads become task cards and job definitions, and conversation history is the raw material for memory.
17
+ No gateways, no proxies, no web UI. Discord *is* the interface.
22
18
 
23
19
  ## Memory — the bot knows you
24
20
 
25
- Your assistant carries context across every conversation, channel, and restart.
26
-
27
- - **Durable facts** — `!memory remember prefers dark mode` persists across sessions and channels
28
- - **Rolling summaries** — Compresses earlier conversation so context carries forward, even across restarts
29
- - **Cold storage** — Semantic search over past conversations using vector embeddings + keyword search. Relevant history is automatically retrieved and injected into the prompt (see [docs/memory.md](docs/memory.md))
30
- - **Per-channel context** — Each channel gets a markdown file shaping behavior (formal in #work, casual in #random)
31
- - **Customizable identity** — Personality, name, and values defined in workspace files (`SOUL.md`, `IDENTITY.md`, etc.)
32
- - **Group chat aware** — Knows when to speak up and when to stay quiet in shared channels
33
-
34
- **Why Discord fits:** channels = context boundaries, DMs = private deep context, conversation history is the raw material.
35
-
36
- ### YouTube transcripts
37
-
38
- When you share a YouTube link in a message, DiscoClaw automatically fetches the video's transcript and injects it into the AI's context. This lets the bot answer questions about video content, summarize talks, or reference specific points — without you needing to copy-paste anything. Up to 3 videos per message are processed, with a 15-second timeout per fetch. Transcripts are sanitized before injection to prevent prompt manipulation.
21
+ - **Durable facts** persist across sessions, channels, and restarts
22
+ - **Rolling summaries** — context carries forward, even across restarts
23
+ - **Semantic search** — vector + keyword search over past conversations, auto-retrieved
24
+ - **Per-channel personality** — markdown files shape behavior per channel
25
+ - **YouTube transcripts** — share a link, the bot reads the video
39
26
 
40
27
  ## Tasks — the bot tracks your work
41
28
 
42
- A lightweight in-process task store that syncs bidirectionally with Discord forum threads.
43
-
44
- - **Create from either side** — Ask your assistant in chat or use task commands
45
- - **Bidirectional sync** — Status, priority, and tags stay in sync between the task store and Discord threads
46
- - **Status emoji and auto-tagging** — Thread names show live status at a glance
47
- - **Discord actions** — Your assistant manages tasks through conversation: create channels, send messages, search history, run polls, and more (see [docs/discord-actions.md](docs/discord-actions.md))
48
-
49
- **Why Discord fits:** forum threads = task cards, archive = done, thread names show live status.
29
+ - **Bidirectional sync** task store and Discord forum threads stay in sync
30
+ - **Create from anywhere** — chat, commands, or the forum directly
31
+ - **Live status** — thread names show status emoji at a glance
32
+ - **Discord actions** — the bot manages channels, messages, polls, and more through conversation
50
33
 
51
34
  ## Automations — the bot acts on its own
52
35
 
53
- Recurring tasks defined as forum threads in plain language — no crontab, no separate scheduler UI.
54
-
55
- - **Plain-language schedules** — "every weekday at 7am, check the weather and post to #general"
56
- - **Edit to change, archive to pause, unarchive to resume**
57
- - **Full workspace access** — File I/O, web search, browser automation, Discord actions
58
- - **Multi-turn sessions** — A live process persists between runs, so context carries across executions
59
-
60
- **Why Discord fits:** forum threads = job definitions, archive/unarchive = pause/resume, no separate scheduler UI needed.
61
-
62
- For the managed Chrome/Chromium profile flow used by browser automation, see the [managed browser launcher guide](#managed-browser-launcher).
36
+ - **Plain-language schedules** "every weekday at 7am, check the weather"
37
+ - **Forum-thread definitions** — edit to change, archive to pause
38
+ - **Full workspace access** — files, web, browser automation, Discord actions
63
39
 
64
- <!-- source-of-truth: docs/voice.md -->
65
40
  ## Voice — the bot talks back
66
41
 
67
- DiscoClaw can join Discord voice channels for real-time conversation: listen via speech-to-text, think with the AI runtime, and speak the response via text-to-speech.
68
-
69
- - **STT** — Deepgram Nova-3 streaming transcription (WebSocket)
70
- - **TTS** — Cartesia Sonic-3 speech synthesis (WebSocket, 24 kHz PCM)
71
- - **Barge-in** — interrupt the bot mid-sentence by speaking; playback stops immediately
72
- - **Auto-join** — optionally join/leave channels automatically when you enter or leave
73
- - **Transcript mirror** — voice conversations are mirrored to a text channel for persistence
74
- - **Voice actions** — the AI can execute a restricted action subset (messaging, tasks, memory) during voice
75
-
76
- Voice is **off by default**. Enable with `DISCOCLAW_VOICE_ENABLED=1` plus API keys for your STT/TTS providers. Requires Node 22+ (for native WebSocket used by Cartesia TTS) and C++ build tools (for the `@discordjs/opus` native addon).
77
-
78
- Full setup guide: [docs/voice.md](docs/voice.md)
79
-
80
- ## Self-management — the bot maintains itself
81
-
82
- - **Self-update** — `!update` checks for new npm versions; `!update apply` downloads, installs, and restarts without leaving Discord
83
- - **Health checks** — `!health`, `!doctor`, `!status` for diagnostics
84
- - **Secret management** — `!secret` manages `.env` entries from DMs
85
- - **Model switching** — `!models` swaps AI models per role at runtime
86
- - **Restart** — `!restart` restarts the service on demand
87
-
88
- ## How it works
89
-
90
- DiscoClaw orchestrates the flow between Discord and AI runtimes (Claude Code by default, with `gemini-api`, OpenAI, Codex, and OpenRouter adapters available via `PRIMARY_RUNTIME`). For 1.0, `Claude CLI` on a source checkout is the explicitly supported default path, and `Codex CLI` on a source checkout is the explicitly supported secondary path. See [docs/audit/provider-auth-1.0-matrix.md](docs/audit/provider-auth-1.0-matrix.md) for the full consolidated matrix. The OpenAI-compatible and OpenRouter adapters can expose optional tool use when `OPENAI_COMPAT_TOOLS_ENABLED=1` is set, but OpenRouter support claims stop at the narrower audited boundary described below. It doesn't contain intelligence itself — it decides *when* to call the AI, *what context* to give it, and *what to do* with the output. When you send a message, the orchestrator:
91
-
92
- 1. Checks the user allowlist (fail-closed — empty list means respond to nobody)
93
- 2. Assembles context: per-channel rules, conversation history, rolling summary, and durable memory
94
- 3. Routes to the appropriate runtime adapter, running in your workspace directory
95
- 4. Streams the response back, chunked to fit Discord's message limits
96
- 5. Parses and executes any Discord actions the assistant emitted
97
-
98
- ### Instruction precedence
99
-
100
- Prompt assembly has two layers, each with its own ordering contract.
101
-
102
- **Preamble precedence** — the front of every prompt, in strict priority order:
103
-
104
- 1. **Immutable security policy** (hard-coded root rules)
105
- 2. **Tracked defaults** (runtime-injected from `templates/instructions/SYSTEM_DEFAULTS.md`)
106
- 3. **Tracked tools** (runtime-injected from `templates/instructions/TOOLS.md`)
107
- 4. **User rules override** (`workspace/AGENTS.md`)
108
- 5. **User tools override** (`workspace/TOOLS.md`, optional)
109
- 6. **Memory/context layers** (workspace identity files, channel context, durable/rolling memory, etc.)
110
-
111
- **Post-preamble section ordering** — the sections between the preamble and the user message are arranged to exploit primacy bias (high-signal sections first) and recency bias (action schemas and constraints near the end, just before the user message). Low-signal data sections sit in the middle. See [`docs/prompt-ordering.md`](docs/prompt-ordering.md) for the canonical order and rationale.
112
-
113
- `workspace/DISCOCLAW.md` is no longer a managed or authoritative instruction source.
114
- If you still have a legacy copy, treat it as historical reference only.
115
-
116
- ### Message batching
117
-
118
- When multiple messages arrive while the bot is thinking (i.e., an AI invocation is already active for that session), they're automatically combined into a single prompt rather than queued individually. This means rapid follow-up messages are processed together, giving the bot full context in one shot. Commands (`!`-prefixed messages) bypass batching and are always processed individually.
119
-
120
- ### OpenRouter
121
-
122
- Set `PRIMARY_RUNTIME=openrouter` to route requests through [OpenRouter](https://openrouter.ai), which provides access to models from Anthropic, OpenAI, Google, and others via a single API key.
123
-
124
- Required: `OPENROUTER_API_KEY`. Optional overrides: `OPENROUTER_BASE_URL` (default: `https://openrouter.ai/api/v1`), `OPENROUTER_MODEL` (default: `anthropic/claude-sonnet-4.6`), and `OPENROUTER_PROVIDER_PREFERENCES` (OpenRouter-only JSON routed through the request `provider` field). Treat `.env` presence, `PRIMARY_RUNTIME=openrouter`, and `!models set chat openrouter` as config/routing intent only until the running instance proves the shipped OpenRouter path with `!status` or the startup credential report showing `openrouter-key: ok`.
125
-
126
- For source checkouts, workload evidence beyond that key-visibility proof exists only through the repo smoke path, and read-only tool coverage is the only validated tool subset today. DiscoClaw now ships built-in OpenRouter tier defaults: `fast → openai/gpt-5-mini`, `capable → anthropic/claude-sonnet-4.6`, `deep → anthropic/claude-opus-4.6`. Override them with `DISCOCLAW_TIER_OPENROUTER_<TIER>` only when you need instance-specific routing. See [docs/audit/openrouter-api-key-support-boundary.md](docs/audit/openrouter-api-key-support-boundary.md) for the support boundary that npm installs and source checkouts both ship.
127
-
128
- ## Model Overrides
129
-
130
- The `!models` command lets you view and swap AI models per role at runtime — no restart needed. Per-role model values persist to `models.json` under the data dir, while fast/voice runtime overlays persist separately to `runtime-overrides.json`. Live runtime swaps like `!models set chat openrouter` are still live-only until the next restart, but they affect the main runtime path broadly, not just chat.
131
-
132
- For the full operator guide to install-mode detection, persistent adapter switches, OpenRouter tier overrides, fast/voice runtime behavior, and `!models reset` semantics, see [docs/runtime-switching.md](docs/runtime-switching.md).
133
-
134
- **Roles:** `chat`, `fast`, `forge-drafter`, `forge-auditor`, `summary`, `cron`, `cron-exec`, `voice`
135
-
136
- | Command | Description |
137
- |---------|-------------|
138
- | `!models` | Show current model assignments |
139
- | `!models set <role> <model>` | Change the model for a role |
140
- | `!models reset` | Revert all roles to startup defaults and clear overrides |
141
- | `!models reset <role>` | Revert a specific role to its startup default |
142
-
143
- **Examples:**
144
- - `!models set chat claude-sonnet-4` — use Sonnet for chat
145
- - `!models set chat openrouter` — live-switch the main runtime to OpenRouter until restart
146
- - `!models set cron-exec haiku` — run crons on a cheaper model
147
- - `!models set cron-exec default` — clear the cron-exec override and use the startup default again
148
- - `!models set voice sonnet` — use a specific model for voice
149
- - `!models reset` — clear all overrides and revert to startup defaults
150
-
151
- Setting `chat` to a runtime name (`openrouter`, `openai`, `gemini-api`, `codex-cli`, `claude-cli`) live-switches the main runtime path until restart; setting `voice` to a runtime name switches only voice. Legacy aliases (`claude`, `codex`, `anthropic`) are still accepted. Exact model-string runtime auto-switching is only implemented for `fast` and `voice`.
152
-
153
- ## Secret Management
154
-
155
- The `!secret` command lets you manage `.env` entries from Discord without touching the file directly. It works in DMs only — values are never echoed back.
156
-
157
- | Command | Description |
158
- |---------|-------------|
159
- | `!secret set KEY=value` | Add or update a `.env` entry |
160
- | `!secret unset KEY` | Remove a `.env` entry |
161
- | `!secret list` | List key names in `.env` (values hidden) |
162
- | `!secret help` | Show usage |
163
-
164
- Changes take effect after a restart (`!restart`). Writes are atomic — a partial write can't corrupt your `.env`.
165
-
166
- ## Customization
167
-
168
- ### Shareable integration recipes
169
-
170
- DiscoClaw supports a shareable markdown recipe format for passing integrations between users:
171
-
172
- - Spec: `docs/discoclaw-recipe-spec.md`
173
- - Template: `templates/recipes/integration.discoclaw-recipe.md`
174
- - Example files: `recipes/examples/*.discoclaw-recipe.md`
175
- - Skills:
176
- - `skills/discoclaw-recipe-generator/SKILL.md`
177
- - `skills/discoclaw-recipe-consumer/SKILL.md`
178
- - Install/refresh invocable skill symlinks:
179
- - `pnpm claude:install-skills`
42
+ Real-time voice with STT (Deepgram), TTS (Cartesia), barge-in, and transcript mirroring. Off by default. [Setup guide →](docs/voice.md)
180
43
 
181
- Author one recipe file for an integration, share it, then let another user's DiscoClaw agent consume it and produce a local implementation checklist before coding.
44
+ ## Self-management
182
45
 
183
- ### MCP (Model Context Protocol)
46
+ Self-update from Discord (`!update apply`), health checks (`!doctor`), secret management (`!secret`), runtime model switching (`!models`), and restart (`!restart`) — no SSH needed.
184
47
 
185
- When using the Claude runtime, you can connect external tool servers via MCP. Place a `.mcp.json` file in your workspace directory to configure servers — their tools become available during conversations. See [docs/mcp.md](docs/mcp.md) for the config format, examples, and troubleshooting.
186
-
187
- ## Managed browser launcher
188
-
189
- Browser automation uses a Discoclaw-managed Chrome/Chromium profile directly. There is no companion service, long-lived helper daemon, or localhost control service to keep running in the background.
190
-
191
- Use the launcher flow when you need a real browser profile for login-gated sites or later headless reuse:
192
-
193
- - `discoclaw browser setup` checks storage rules, browser discovery, and current managed-profile readiness
194
- - `discoclaw browser launch` opens the managed profile headed for one-time login and verifies CDP against that same browser instance
195
- - `discoclaw browser launch --headless` reuses the same verified profile later without opening a visible window
196
- - `!browser help` in Discord prints the same operator-facing setup, doctor, and launch commands
197
-
198
- For source checkouts, repo-local managed browser storage is supported only at the ignored default `data/browser/` path. Custom in-repo data dirs are refused for this feature; if you need a different browser data location, move `DISCOCLAW_DATA_DIR` outside the repo.
199
-
200
- ## Prerequisites
201
-
202
- **End users:**
203
- - **Node.js >=20** — check with `node --version`
204
- - One primary runtime:
205
- - **Claude CLI** on your `PATH` — check with `claude --version` (see [Claude CLI docs](https://docs.anthropic.com/en/docs/claude-code) to install), or
206
- - **Gemini API** via `PRIMARY_RUNTIME=gemini-api` plus `GEMINI_API_KEY`, or
207
- - **Codex CLI** on your `PATH` — check with `codex --version` (binary presence only; session auth is a separate proof gate), or
208
- - **OpenAI-compatible API key** via `OPENAI_API_KEY` (config presence only; live auth is a separate proof gate), or
209
- - **OpenRouter API key** via `OPENROUTER_API_KEY` (config presence only until `!status` or the startup credential report shows `openrouter-key: ok`)
210
- - Runtime-specific access for your chosen provider (Anthropic access for Claude CLI, Google API access for `gemini-api`, OpenAI access for Codex/OpenAI models)
211
-
212
- 1.0 provider/auth policy: `Claude CLI` on a source checkout is the explicitly supported default path, and `Codex CLI` on a source checkout is the explicitly supported secondary path. For every current provider/auth path, including `openai`, `openrouter`, `gemini-api`, and direct Anthropic, use the consolidated verdicts in [docs/audit/provider-auth-1.0-matrix.md](docs/audit/provider-auth-1.0-matrix.md) rather than inferring readiness from setup/config alone.
213
-
214
- `discoclaw init` scaffolds Claude CLI, Codex CLI, OpenAI, OpenRouter, and Gemini API runtime paths.
215
-
216
- For Codex and OpenAI paths, treat binary/key presence as readiness prerequisites only. The install-mode-specific support claims live in the [provider/auth 1.0 matrix](docs/audit/provider-auth-1.0-matrix.md), [Codex source-checkout audit](docs/audit/codex-blank-machine-readiness.md), and [Codex npm-managed audit](docs/audit/codex-npm-managed-path.md). For OpenRouter, the shipped support claim is narrower: [docs/audit/openrouter-api-key-support-boundary.md](docs/audit/openrouter-api-key-support-boundary.md) documents only the runtime-visible env-key proof boundary, with source-checkout workload evidence limited to the repo smoke path.
217
-
218
- **Contributors (from source):**
219
- - Everything above, plus **pnpm** — enable via Corepack (`corepack enable`) or install separately
220
-
221
- ### Model capability requirement
222
-
223
- DiscoClaw assumes reliable structured output for several runtime paths (for example: Discord actions, cron JSON routing, and tool-call loops).
224
-
225
- - For OpenAI-compatible and OpenRouter adapters, pick models that reliably support JSON-shaped output and function calling.
226
- - For OpenRouter-backed source-checkout workloads, treat repo smoke-path validation as the workload proof surface, and treat read-only tool coverage as the only validated tool subset today.
227
- - "OpenAI-compatible" API shape alone is not a capability guarantee.
228
- - If a model fails JSON/tool-call smoke tests, treat it as unsupported for DiscoClaw runtime use.
229
- - Use the [model validation smoke test checklist](docs/configuration.md#model-validation-smoke-test-recommended) before adopting a new model.
230
-
231
- <!-- source-of-truth: docs/discord-bot-setup.md -->
232
48
  ## Quick start
233
49
 
234
- ### Discord setup (private server + bot)
235
-
236
- 1. Create a **private Discord server** dedicated to DiscoClaw (not a shared/public server).
237
- 2. In the [Discord Developer Portal](https://discord.com/developers/applications), create an application, then go to **Bot** -> **Add Bot**.
238
- 3. Under **Bot** -> **Privileged Gateway Intents**, enable **Message Content Intent**.
239
- 4. Copy the bot token and set it in `.env` as `DISCORD_TOKEN=...`.
240
- 5. Invite the bot to your server:
241
- - Go to **Installation** (left sidebar) → set **Install Link** to **Discord Provided Link**
242
- - Under **Default Install Settings**, add scope `bot` under Guild Install
243
- - A **Permissions** selector appears. For a private server, pick `Administrator` — it's one selection and covers everything. For tighter permissions, see the [permission profiles](docs/discord-bot-setup.md#permission-profiles-choose-intentionally) in the full guide.
244
- - **Save Changes**, then open the install link, pick your server, and authorize
245
- 6. In Discord, enable **Developer Mode** (User Settings -> Advanced), then copy IDs and set:
246
- - `DISCORD_ALLOW_USER_IDS=<your user id>` (required; fail-closed if empty)
247
- - `DISCORD_GUILD_ID=<server id>` (required for `pnpm release:rehearsal`; also required for auto-creating forum channels)
248
-
249
- Full step-by-step guide: [docs/discord-bot-setup.md](docs/discord-bot-setup.md)
250
-
251
- ## Documentation
252
-
253
- ### Getting Started
254
-
255
- - [Discord bot setup](docs/discord-bot-setup.md) — create a bot, invite it, configure permissions
256
- - [MCP (Model Context Protocol)](docs/mcp.md) — connect external tool servers
257
-
258
- ### Features & Usage
259
-
260
- - [Memory system](docs/memory.md) — five-layer memory architecture, tuning, and troubleshooting
261
- - [Plan & Forge](docs/plan-and-forge.md) — autonomous planning and code generation
262
- - [Discord actions](docs/discord-actions.md) — channels, messaging, moderation, tasks, crons
263
- - [Cron / automations](docs/cron.md) — recurring task setup, advanced options, debugging
264
- - [Tasks](docs/tasks.md) — task lifecycle, bidirectional sync, tag maps
265
- - [Voice](docs/voice.md) — real-time voice chat setup (STT/TTS)
266
- - [Shareable recipes](docs/discoclaw-recipe-spec.md) — integration recipe format spec
267
-
268
- ### Development
269
-
270
- - [Philosophy](docs/philosophy.md) — design principles and trade-offs
271
- - [Releasing](docs/releasing.md) — npm publish workflow and versioning
272
- - [Inventory](docs/INVENTORY.md) — full component inventory and MVP status
273
-
274
- ### Operations
275
-
276
- - [Configuration reference](docs/configuration.md) — all environment variables indexed by category
277
- - [Provider/auth 1.0 matrix](docs/audit/provider-auth-1.0-matrix.md) — intended default path, supported secondary path, and the current `SUPPORTED FOR 1.0` / `PARTIAL` / `OUT OF SCOPE` verdicts for every implied provider/auth path
278
- - [Claude source-checkout status](CLAUDE%20SOURCE-CHECKOUT%20STATUS.md) — current closeout memo for the Claude source-checkout path, including what the latest throwaway-checkout rerun did and did not prove
279
- - [Claude source-checkout audit](docs/audit/claude-blank-machine-readiness.md) — current 1.0 verdict for the repo-owned `pnpm preflight*` + `pnpm claude:auth-smoke` path
280
- - [Claude npm-managed audit](docs/audit/claude-npm-managed-path.md) — current 1.0 verdict for `npm install -g discoclaw`, `discoclaw init`, and the daemon/runtime-path gap
281
- - [Codex source-checkout audit](docs/audit/codex-blank-machine-readiness.md) — current 1.0 verdict for the repo-owned `pnpm preflight*` path plus the separate Codex/OpenAI proof gates
282
- - [Codex npm-managed audit](docs/audit/codex-npm-managed-path.md) — current 1.0 verdict for `npm install -g discoclaw`, manual Codex login proof, optional OpenAI fast-path proof, and the daemon/runtime-path gap
283
- - [Managed browser launcher guide](#managed-browser-launcher) — dedicated profile flow, headed login, verified CDP handoff, and storage rules
284
- - [Runtime/model switching](docs/runtime-switching.md) — operator guide for switching adapters, models, and defaults safely
285
- - [Webhook exposure](docs/webhook-exposure.md) — tunnel/proxy setup and webhook security
286
- - [Data migration](docs/data-migration.md) — migrating task data between formats
287
-
288
- ### Install and run
289
-
290
- 1. **Install globally:**
291
- ```bash
292
- npm install -g discoclaw
293
- ```
294
-
295
- > **Fedora 43+ / GCC 14+ — `@discordjs/opus` build failure (resolved)**
296
- >
297
- > This was fixed upstream in `@discordjs/opus` 0.10.0. If you are pinned to an older version, set the flag before installing:
298
- > ```bash
299
- > CFLAGS="-Wno-error=incompatible-pointer-types" npm install -g discoclaw
300
- > ```
301
-
302
- 2. **Run the interactive setup wizard** (creates `.env` and scaffolds your workspace):
303
- ```bash
304
- discoclaw init
305
- ```
306
-
307
- 3. **Register the system service:**
308
- ```bash
309
- discoclaw install-daemon
310
- ```
311
- Optional: pass `--service-name <name>` to use a custom service name (useful on macOS when running multiple instances, or to match your own naming convention):
312
- ```bash
313
- discoclaw install-daemon --service-name personal
314
- ```
315
-
316
- 4. **Open the local operator dashboard:**
317
- ```bash
318
- discoclaw dashboard
319
- ```
320
- By default this listens on `127.0.0.1`. To reach it from a phone over Tailscale,
321
- set `DISCOCLAW_DASHBOARD_TRUSTED_HOSTS` to your tailnet IP or MagicDNS hostname.
322
- See [docs/dashboard-tailscale.md](docs/dashboard-tailscale.md).
323
-
324
- If you are validating runtime auth, keep the install mode and auth method separate:
325
-
326
- - Claude: global installs use the npm-managed path (`discoclaw init` guidance plus `discoclaw claude auth-smoke`), while source checkouts use `pnpm release:rehearsal` for the blessed full-path rehearsal or collect the same gates manually in order with `pnpm preflight:blank-machine`, `pnpm claude:auth-smoke`, `pnpm discord:smoke-test -- --guild-id <repo-local DISCORD_GUILD_ID>`, and `pnpm build`.
327
- - Codex from source: `pnpm preflight*` is config-only; prove the Codex session separately with `codex exec ...`, and prove any OpenAI fast/alternate path separately with `OPENAI_SMOKE_TEST_TIERS=... pnpm test`.
328
- - Codex from npm/global install: `discoclaw doctor` is config-only and no shipped `discoclaw codex auth-smoke` exists yet; use the same-shell `codex exec --skip-git-repo-check -- "Reply with OK"` login check, then confirm `openai-key: ok` separately if you enabled an OpenAI fast/alternate path.
329
-
330
- Current npm-managed blocker for both Claude and Codex daemon claims: `discoclaw install-daemon` still renders services with `/usr/bin/node` and a fixed service `PATH`. The interactive shell check can therefore pass while the installed daemon resolves different Node or runtime binaries.
331
-
332
- #### From source (contributors)
333
-
334
50
  ```bash
335
- git clone <repo-url> && cd discoclaw
336
- pnpm install --frozen-lockfile
337
- pnpm run setup # guided interactive setup that writes a real clone-local .env
338
- # Or manually: cp .env.example .env and fill in required vars:
339
- # DISCORD_TOKEN
340
- # DISCORD_ALLOW_USER_IDS
341
- # For all ~90 options: cp .env.example.full .env
342
- pnpm preflight:blank-machine
343
- pnpm claude:auth-smoke # if PRIMARY_RUNTIME=claude-cli, before login, from a shell/account with no active Claude session
344
- claude # if PRIMARY_RUNTIME=claude-cli, complete login in that same shell/account
345
- pnpm claude:auth-smoke # if PRIMARY_RUNTIME=claude-cli, rerun after login in that same shell/account
346
- pnpm release:rehearsal # blessed Claude 1.0 source-checkout rehearsal; requires repo-local .env with PRIMARY_RUNTIME=claude-cli
347
- codex exec -m gpt-5.4 --skip-git-repo-check --ephemeral -s read-only -- "Reply with OK" # if PRIMARY_RUNTIME=codex-cli
348
- OPENAI_SMOKE_TEST_TIERS=fast pnpm test # if any source-checkout path routes through OpenAI
349
- pnpm build && pnpm dev
51
+ npm install -g discoclaw
52
+ discoclaw init
53
+ discoclaw install-daemon
350
54
  ```
351
55
 
352
- Fresh clone is not enough evidence by itself for the Claude stranger path. A source checkout still needs a real clone-local `.env`, and the first-login claim only closes when the pre-login failure, interactive `claude` login, and post-login `pnpm claude:auth-smoke` rerun all happen in the same shell/account with no active Claude session. If the host shell was already logged into Claude, the passing smoke proves only the fresh-clone post-login path. When you want stranger-run evidence from a machine with existing DiscoClaw state, isolate `DISCOCLAW_DATA_DIR`, `WORKSPACE_CWD`, `GROUPS_DIR`, and `BEADS_DIR` to throwaway paths before you claim anything about the clone.
353
-
354
- `pnpm release:rehearsal` is the blessed source-checkout entrypoint for the full Claude 1.0 operational rehearsal. Its source of truth is the checkout's own `.env`: the harness reads the repo-local file, requires `PRIMARY_RUNTIME=claude-cli`, strips repo-local persistence/config path overrides that would escape the rehearsal temp root, and then applies explicit child-process overrides for `DISCOCLAW_DATA_DIR`, `WORKSPACE_CWD`, `GROUPS_DIR`, `BEADS_DIR`, and the rehearsal task prefix. The live operator loop it holds includes the first reply, a same-conversation follow-up reply, task sync, cron execution, and restart/recovery. Fresh-clone and first-login stranger evidence are established separately by the fresh-clone QA and auth-smoke path above; the rehearsal harness accepts checkout provenance as operator context when supplied, but it does not try to prove or gate on that provenance in code.
355
-
356
- If `PRIMARY_RUNTIME=claude-cli`, run `pnpm preflight:blank-machine`, capture the separate Claude auth evidence in the order shown above, then run `pnpm discord:smoke-test -- --guild-id <repo-local DISCORD_GUILD_ID>` and `pnpm build` before `pnpm dev`, or run the full `pnpm release:rehearsal` harness once the repo-local `.env` is ready. If `PRIMARY_RUNTIME=codex-cli`, treat `pnpm preflight:blank-machine` as config/bootstrap evidence only, then capture separate Codex session-auth evidence with the `codex exec ...` prompt. If any source-checkout route uses OpenAI, capture separate `OPENAI_API_KEY` evidence with `OPENAI_SMOKE_TEST_TIERS=... pnpm test`.
357
-
358
- ### Claude runtime validation
359
-
360
- Source-checkout 1.0 support status: `SUPPORTED FOR 1.0` for the repo-owned Claude path within the audited boundary: the same no-session shell or account recorded the pre-login unauthenticated result, completed interactive Claude CLI login, and then passed the post-login `pnpm claude:auth-smoke` rerun. The current closeout memo is [Claude source-checkout status](CLAUDE%20SOURCE-CHECKOUT%20STATUS.md), and the deeper audit record remains [docs/audit/claude-blank-machine-readiness.md](docs/audit/claude-blank-machine-readiness.md).
361
-
362
- Npm-managed 1.0 audit verdict: `NOT YET SUPPORT-CLAIMABLE`. The installed CLI now has a shell-level Claude check, but the daemon path still is not claimable because `discoclaw install-daemon` hardcodes `/usr/bin/node` plus a fixed service `PATH`, and `discoclaw init` does not persist `CLAUDE_BIN`. See [docs/audit/claude-npm-managed-path.md](docs/audit/claude-npm-managed-path.md).
363
-
364
- 1. Create a throwaway clone, run `pnpm install --frozen-lockfile`, and supply a real clone-local `.env` (`pnpm run setup` or copy from `.env.example` / `.env.example.full`).
365
- 2. If you are validating from a machine that already has DiscoClaw or Claude state, isolate `DISCOCLAW_DATA_DIR`, `WORKSPACE_CWD`, `GROUPS_DIR`, and `BEADS_DIR` to throwaway paths before claiming stranger-run evidence.
366
- 3. Run the automated config/bootstrap check:
367
- ```bash
368
- pnpm preflight:blank-machine
369
- ```
370
- Optional:
371
- ```bash
372
- pnpm preflight:blank-machine:online
373
- ```
374
- 4. Treat the result correctly:
375
- - `pnpm preflight:blank-machine` proves the local prerequisites against the current `.env` only, ignoring inherited shell env from the host machine.
376
- - `pnpm preflight:blank-machine:online` adds a live Discord login/gateway-intent check on top of that same blank-machine env boundary.
377
- - Plain `pnpm preflight` keeps its broader contributor-oriented behavior and can still be useful for checking the current shell environment.
378
- - Neither command proves Claude login/auth state.
379
- 5. From a shell or account with no active Claude session, run the required pre-login failure check:
380
- ```bash
381
- pnpm claude:auth-smoke
382
- ```
383
- Confirm it returns `Claude CLI appears installed but not authenticated.`
384
- 6. Log in interactively in that same shell or account:
385
- ```bash
386
- claude
387
- ```
388
- 7. Rerun the happy-path check in that same shell or account:
389
- ```bash
390
- pnpm claude:auth-smoke
391
- ```
392
- Confirm it returns `Claude CLI answered the minimal prompt.`
393
- 8. If step 7 was captured only from an already-logged-in shell or account, record that limitation and downgrade the claim to `fresh-clone post-login path only`.
394
- 9. For the full blessed source-checkout path, run the rehearsal harness and record whether the checkout was throwaway or reused:
395
- ```bash
396
- pnpm release:rehearsal
397
- ```
398
- The harness enforces the repo-local `.env` plus explicit child-process isolation overrides, writes a closeout under `docs/release-audit/`, and treats unresolved rehearsal-owned cleanup as a blocked verdict.
399
- 10. Start DiscoClaw after the source-checkout gates pass:
400
- ```bash
401
- pnpm build && pnpm dev
402
- ```
403
-
404
- ### Codex runtime validation
405
-
406
- Codex 1.0 support matrix:
407
-
408
- | Install mode | Config/bootstrap evidence only | Separate auth proof gates | 1.0 verdict |
409
- | --- | --- | --- | --- |
410
- | Source checkout | `pnpm preflight:blank-machine` / `pnpm preflight` prove local prerequisites only. They do not invoke Codex or OpenAI. | 1. Prove the Codex CLI session in the same shell with `codex exec -m gpt-5.4 --skip-git-repo-check --ephemeral -s read-only -- "Reply with OK"` before and after `codex` login. 2. If any source-checkout path routes through OpenAI, run `OPENAI_SMOKE_TEST_TIERS=fast pnpm test` or replace `fast` with your intended tier/model. | `PASS` for the repo-owned source path. See [docs/audit/codex-blank-machine-readiness.md](docs/audit/codex-blank-machine-readiness.md). |
411
- | npm / global install | `discoclaw doctor`, `!doctor`, and `discoclaw init` detection stay config-only. No shipped `discoclaw codex auth-smoke` exists yet. | 1. Run `codex exec --skip-git-repo-check -- "Reply with OK"` before and after `codex` login in the same installed shell. 2. If you enabled an OpenAI fast/alternate path, start DiscoClaw and confirm `!status` or the startup credential report shows `openai-key: ok`. | `NOT YET SUPPORT-CLAIMABLE` for the daemon path. See [docs/audit/codex-npm-managed-path.md](docs/audit/codex-npm-managed-path.md). |
412
-
413
- Do not treat `codex --version`, `OPENAI_API_KEY` presence, `pnpm preflight*`, or `discoclaw doctor` as full Codex readiness proof by themselves. Those are prerequisite/config surfaces only.
414
-
415
- ## Updating
416
-
417
- DiscoClaw can check for and apply updates from inside Discord — no SSH or terminal needed.
418
-
419
- | Command | Description |
420
- |---------|-------------|
421
- | `!update` | Check if a newer version is available on npm |
422
- | `!update apply` | Download the update, reinstall, and restart the service |
423
- | `!update audit` | Show npm-managed runtime audit details |
424
- | `!update help` | Show usage |
425
-
426
- **Global install (from Discord):**
427
-
428
- ```
429
- !update apply
430
- ```
431
-
432
- **Global install (from the command line):**
433
-
434
- ```bash
435
- npm update -g discoclaw
436
- discoclaw install-daemon # re-register the service after updating
437
- # If you used a custom service name, pass it again:
438
- # discoclaw install-daemon --service-name personal
439
- ```
440
-
441
- For Claude on npm-managed installs, keep using the installed-shell validation path after updates: rerun `discoclaw init` if you need the manual login guidance, and use `discoclaw claude auth-smoke` if you want the shipped shell-level smoke check. Do not treat `pnpm preflight*` or `pnpm claude:auth-smoke` as npm-managed evidence; those remain source-checkout-only. Re-registering the daemon also does not remove the current service-path blocker: the generated service still pins `/usr/bin/node` and a fixed `PATH`, so a passing interactive shell smoke test does not yet prove the daemon will resolve the same Node and Claude binaries.
442
-
443
- For Codex on npm-managed installs, `discoclaw doctor` remains config-only and there is still no shipped `discoclaw codex auth-smoke`. Repeat the same-shell `codex exec --skip-git-repo-check -- "Reply with OK"` login check after updates, and if you enabled an OpenAI fast/alternate path confirm the restarted instance reports `openai-key: ok`. Re-registering the daemon still does not remove the service-path blocker: the generated service pins `/usr/bin/node` and a fixed `PATH`, and init does not persist `CODEX_BIN`, so interactive shell success is not yet daemon-proof.
56
+ You'll need a [private Discord server and bot token](docs/discord-bot-setup.md) and at least one AI runtime ([configuration reference](docs/configuration.md)).
444
57
 
445
58
  **From source:**
446
59
 
447
60
  ```bash
448
- git pull
449
- pnpm install
450
- pnpm build
61
+ git clone https://github.com/DiscoClaw/discoclaw.git && cd discoclaw
62
+ pnpm install --frozen-lockfile
63
+ pnpm run setup
64
+ pnpm build && pnpm dev
451
65
  ```
452
66
 
453
- Run `pnpm preflight` after changes to validate the automated contract again. It checks the local prerequisites Discoclaw can prove today: Node, pnpm, runtime binary presence/version, `.env` presence, env formatting, forum bootstrap eligibility, and config-doctor findings. It does not verify Codex session auth, OpenAI runtime auth, or Claude login/auth. Use `pnpm preflight:blank-machine` when you need the audit to ignore inherited shell env and inspect only the current `.env`, `pnpm preflight:blank-machine:online` if you also want a live Discord token/intents check, `pnpm claude:auth-smoke` for the Claude login/auth smoke step, and the Codex/OpenAI proof gates above for those runtime paths.
67
+ See [CONTRIBUTING.md](CONTRIBUTING.md) for the full contributor setup including runtime validation.
454
68
 
455
- You can also run `discoclaw doctor` to inspect config drift and related issues, `discoclaw doctor --fix` to apply safe remediations, or use `!doctor` / `!doctor fix` from Discord (`!health doctor` / `!health doctor fix` remain supported). Restart the service afterward for fixed config to take effect.
69
+ ## Documentation
456
70
 
457
- For a local operator console, run `discoclaw dashboard` in the project directory. It shows the active service target, current model assignments, runtime overrides, config doctor status, and quick actions for status/logs/restart. It binds to `127.0.0.1` by default; configure `DISCOCLAW_DASHBOARD_TRUSTED_HOSTS` to allow Tailscale access via a tailnet IP or MagicDNS hostname while keeping Host-header checks in place for all other names. See [docs/dashboard-tailscale.md](docs/dashboard-tailscale.md).
71
+ **Getting started:** [Discord bot setup](docs/discord-bot-setup.md) · [Configuration](docs/configuration.md) · [MCP](docs/mcp.md)
458
72
 
459
- ### Restart and recovery verification
73
+ **Features:** [Memory](docs/memory.md) · [Tasks](docs/tasks.md) · [Crons](docs/cron.md) · [Voice](docs/voice.md) · [Discord actions](docs/discord-actions.md) · [Plan & Forge](docs/plan-and-forge.md) · [Browser automation](docs/browser.md) · [Recipes](docs/discoclaw-recipe-spec.md)
460
74
 
461
- Treat restart/recovery as a later closeout stage, not as a substitute for the source-checkout auth proof above. Record it only after the throwaway/source-checkout flow has already captured the correct Claude auth evidence and at least one normal reply. A clean restart from an already-authenticated shell does not retroactively prove the first-login stranger path.
75
+ **Operations:** [Runtime switching](docs/runtime-switching.md) · [Dashboard](docs/dashboard-tailscale.md) · [Webhook exposure](docs/webhook-exposure.md) · [Data migration](docs/data-migration.md)
462
76
 
463
- If running as a systemd service, restart it:
77
+ **Audits:** [Provider/auth matrix](docs/audit/provider-auth-1.0-matrix.md) · [Claude](docs/audit/claude-blank-machine-readiness.md) · [Codex](docs/audit/codex-blank-machine-readiness.md)
464
78
 
465
- ```bash
466
- systemctl --user restart discoclaw.service
467
- ```
468
-
469
- Then verify the recovery path in order:
470
-
471
- 1. Confirm the service is back:
472
- ```bash
473
- systemctl --user status discoclaw.service
474
- ```
475
- 2. Inspect recent logs for a clean startup:
476
- ```bash
477
- journalctl --user -u discoclaw.service -n 50 --no-pager
478
- ```
479
- 3. Send a short Discord message and confirm the already-validated runtime path answers normally after restart.
480
- 4. If a long-running reply was interrupted by the restart and `DISCOCLAW_COMPLETION_NOTIFY=1`, confirm startup recovery posts either:
481
- - the persisted recovery summary text, or
482
- - a generic completion notice ending with `Recovered after restart.`
483
- 5. Keep this evidence paired with the earlier Claude validation record; restart success does not close the separate first-login stranger gate by itself.
79
+ **Development:** [Philosophy](docs/philosophy.md) · [Releasing](docs/releasing.md) · [Inventory](docs/INVENTORY.md)
484
80
 
485
81
  ## Platform support
486
82
 
487
- - **All platforms** `pnpm dev` works everywhere Node.js runs (Linux, macOS, Windows)
488
- - **Linux** — systemd service file provided for production deployment (see `.context/ops.md`)
489
- - **macOS / Windows** — use pm2, screen, or another process manager for long-running deployment; or just `pnpm dev` in a terminal
490
-
491
- > Windows is not tested for production use in v0.x. The session scanner has known path-handling issues on Windows, and the Claude CLI primarily targets Linux and macOS.
83
+ Linux (systemd service included), macOS, Windows. Production daemon via systemd on Linux, or pm2/screen elsewhere.
492
84
 
493
85
  ## Safety
494
86
 
495
- DiscoClaw orchestrates powerful local tooling via AI runtimes, often with elevated permissions. Treat it like a local automation system connected to Discord.
496
-
497
- - Use a **private Discord server** — don't start in a shared or public server
498
- - Use **least-privilege** Discord permissions
499
- - Keep `DISCORD_ALLOW_USER_IDS` tight — this is the primary security boundary
500
- - Empty allowlist = respond to nobody (fail-closed)
501
- - Optionally restrict channels with `DISCORD_CHANNEL_IDS`
502
- - External content (Discord messages, web pages, files) is **data**, not instructions
503
-
504
- ## Workspace layout
505
-
506
- The orchestrator runs AI runtimes in a separate working directory (`WORKSPACE_CWD`), keeping the repo clean while giving your assistant a persistent workspace.
507
-
508
- - Set `DISCOCLAW_DATA_DIR` to use `$DISCOCLAW_DATA_DIR/workspace` (good for Dropbox-backed setups)
509
- - Or leave it unset to use `./workspace` relative to the repo
510
- - Content (channel context, Discord config) defaults to `$DISCOCLAW_DATA_DIR/content`
511
-
512
- ## Development
513
-
514
- ```bash
515
- pnpm preflight # automated prerequisite check; Claude auth remains manual
516
- pnpm preflight:blank-machine # same check, but ignore inherited shell env and use only .env
517
- pnpm preflight:online # adds live Discord login/intents validation
518
- pnpm preflight:blank-machine:online # blank-machine check plus live Discord login/intents validation
519
- pnpm claude:auth-smoke # Claude CLI login/auth smoke check
520
- pnpm release:rehearsal # full Claude source-checkout release rehearsal
521
- pnpm dev # start dev mode
522
- pnpm build # compile TypeScript
523
- pnpm test # run tests
524
- ```
87
+ Use a **private Discord server**, keep `DISCORD_ALLOW_USER_IDS` tight (fail-closed if empty), and use least-privilege Discord permissions. See [SECURITY.md](SECURITY.md).
525
88
 
526
89
  ## Built with
527
90
 
@@ -43,12 +43,20 @@ export const QUERY_ACTION_TYPES = new Set([
43
43
  // Archive
44
44
  'archiveList',
45
45
  ]);
46
+ // Actions that are not pure queries but still need a follow-up on success
47
+ // because they produce data the bot needs to act on (e.g. a downloaded file).
48
+ // Unlike QUERY_ACTION_TYPES, these also trigger follow-up on failure so the
49
+ // bot can report the error.
50
+ export const ALWAYS_FOLLOW_UP_TYPES = new Set([
51
+ 'downloadAttachment',
52
+ ]);
46
53
  export function hasQueryAction(actionTypes) {
47
54
  return actionTypes.some((t) => QUERY_ACTION_TYPES.has(t));
48
55
  }
49
56
  /**
50
57
  * Returns true when a follow-up AI invocation should be triggered:
51
58
  * - any query action succeeded (to process returned data), OR
59
+ * - any ALWAYS_FOLLOW_UP action ran (success or failure), OR
52
60
  * - any non-query action failed (so the bot can explain the failure).
53
61
  *
54
62
  * Query action failures are excluded because there is no useful result data
@@ -58,12 +66,15 @@ export function shouldTriggerFollowUp(actions, results) {
58
66
  const anyQuerySucceeded = actions.some((a, i) => QUERY_ACTION_TYPES.has(a.type) && results[i]?.ok);
59
67
  if (anyQuerySucceeded)
60
68
  return true;
69
+ const anyAlwaysFollowUp = actions.some((a) => ALWAYS_FOLLOW_UP_TYPES.has(a.type));
70
+ if (anyAlwaysFollowUp)
71
+ return true;
61
72
  const anyNonQueryActionFailed = results.some((r, i) => r && !r.ok && !QUERY_ACTION_TYPES.has(actions[i]?.type ?? ''));
62
73
  return anyNonQueryActionFailed;
63
74
  }
64
75
  /**
65
- * Returns true when the follow-up was triggered exclusively by a non-query
66
- * action failure — i.e. no query action succeeded in this round.
76
+ * Returns true when the follow-up was triggered exclusively by a failure
77
+ * — i.e. no query action succeeded and no ALWAYS_FOLLOW_UP action succeeded.
67
78
  *
68
79
  * Use this to select a failure-specific placeholder message and prompt suffix
69
80
  * rather than the generic "following up..." variants.
@@ -72,6 +83,11 @@ export function isFailureFollowUp(actions, results) {
72
83
  const anyQuerySucceeded = actions.some((a, i) => QUERY_ACTION_TYPES.has(a.type) && results[i]?.ok);
73
84
  if (anyQuerySucceeded)
74
85
  return false;
86
+ const anyAlwaysFollowUpSucceeded = actions.some((a, i) => ALWAYS_FOLLOW_UP_TYPES.has(a.type) && results[i]?.ok);
87
+ if (anyAlwaysFollowUpSucceeded)
88
+ return false;
89
+ // ALWAYS_FOLLOW_UP types are not in QUERY_ACTION_TYPES, so their failures
90
+ // are caught here alongside regular non-query failures.
75
91
  return results.some((r, i) => r && !r.ok && !QUERY_ACTION_TYPES.has(actions[i]?.type ?? ''));
76
92
  }
77
93
  /**
@@ -450,7 +450,7 @@ describe('image input precedence — direct > reply-ref > history', () => {
450
450
  });
451
451
  });
452
452
  describe('manual message finalization guard', () => {
453
- const PROMISED_ACTION_WARNING = 'Warning: this reply says Discord-managed work is starting or being handled now';
453
+ const PROMISED_ACTION_WARNING = 'Warning: this reply says Discord-managed work was performed or is being performed';
454
454
  beforeEach(() => {
455
455
  vi.clearAllMocks();
456
456
  resetAbortRegistry();
@@ -193,9 +193,11 @@ export function shouldSuppressFollowUp(processedText, actionsCount, imagesCount,
193
193
  }
194
194
  const DISCORD_ACTION_INTENT_BASE_VERBS = String.raw `create|send|edit|delete|close|open|post|react|launch|remember|forget|pin|unpin|crosspost|archive|ban|kick|timeout|set`;
195
195
  const DISCORD_ACTION_INTENT_PROGRESSIVE_VERBS = String.raw `creating|sending|editing|deleting|closing|opening|posting|reacting|launching|remembering|forgetting|pinning|unpinning|crossposting|archiving|banning|kicking|setting`;
196
+ const DISCORD_ACTION_INTENT_PAST_VERBS = String.raw `posted|sent|edited|deleted|closed|opened|created|reacted|launched|remembered|forgot|pinned|unpinned|crossposted|archived|banned|kicked|set`;
197
+ const DISCORD_ACTION_INTENT_PAST_PARTICIPLES = String.raw `posted|sent|edited|deleted|closed|opened|created|reacted|launched|remembered|forgotten|pinned|unpinned|crossposted|archived|banned|kicked|set`;
196
198
  const DISCORD_ACTION_INTENT_RESOURCE_NOUNS = String.raw `channel|thread|message|reply|task|plan|cron|poll|reaction|pin|user|member|nickname|status|activity|canvas|image|file|attachment|memory|preference|fact|note`;
197
199
  const DISCORD_ACTION_INTENT_NEGATION_RE = /\b(?:i have not started yet|i haven't started yet|have not started yet|haven't started yet|not started yet|i have not begun yet|i haven't begun yet|have not begun yet|haven't begun yet|i am not starting|i'm not starting|i am not doing that yet|i'm not doing that yet|not proceeding now|not handling it now)\b/i;
198
- const DISCORD_ACTION_INTENT_EXPLANATION_RE = /\b(?:example(?: only)?|for example|for instance|e\.g\.|i can|i could|i would|you can|you could|you would|if you want|when you're ready|would use|would emit|would send|would create|would run|do not run|don't run|not run)\b/i;
200
+ const DISCORD_ACTION_INTENT_EXPLANATION_RE = /\b(?:example(?: only)?|for example|for instance|e\.g\.|i can|i could|i would|you can|you could|you would|if you want|if I|when you're ready|would use|would emit|would send|would create|would run|do not run|don't run|not run)\b/i;
199
201
  const DISCORD_ACTION_INTENT_PATTERNS = [
200
202
  new RegExp(String.raw `\b(?:i am|i'm)\s+(?:${DISCORD_ACTION_INTENT_PROGRESSIVE_VERBS})\b[^.!?\n]{0,80}\b(?:${DISCORD_ACTION_INTENT_RESOURCE_NOUNS})s?\b(?:[^.!?\n]{0,40}\b(?:now|already)\b)?`, 'i'),
201
203
  new RegExp(String.raw `\b(?:i am|i'm)\s+going to\s+(?:${DISCORD_ACTION_INTENT_BASE_VERBS})\b[^.!?\n]{0,80}\b(?:${DISCORD_ACTION_INTENT_RESOURCE_NOUNS})s?\b[^.!?\n]{0,40}\b(?:now|for you|in this response)\b`, 'i'),
@@ -204,6 +206,14 @@ const DISCORD_ACTION_INTENT_PATTERNS = [
204
206
  /\b(?:(?:i am|i'm)\s+)?already handling (?:it|that|this)(?: now)?\b/i,
205
207
  /\b(?:(?:i am|i'm)\s+)?taking the next pass(?: now)?\b/i,
206
208
  /\b(?:(?:i am|i'm)\s+)?cleaning(?: [^.!?\n]{0,40})? up now\b/i,
209
+ // Headless past — sentence-initial past verb + resource noun, no subject.
210
+ new RegExp(String.raw `^(?:${DISCORD_ACTION_INTENT_PAST_VERBS})\b[^.!?\n]{0,80}\b(?:${DISCORD_ACTION_INTENT_RESOURCE_NOUNS})s?\b`, 'i'),
211
+ // First-person past — I + past verb + resource noun.
212
+ new RegExp(String.raw `\bI\s+(?:${DISCORD_ACTION_INTENT_PAST_VERBS})\b[^.!?\n]{0,80}\b(?:${DISCORD_ACTION_INTENT_RESOURCE_NOUNS})s?\b`, 'i'),
213
+ // Perfect tense — I've / I have + past participle + resource noun.
214
+ new RegExp(String.raw `\b(?:I've|I have)\s+(?:${DISCORD_ACTION_INTENT_PAST_PARTICIPLES})\b[^.!?\n]{0,80}\b(?:${DISCORD_ACTION_INTENT_RESOURCE_NOUNS})s?\b`, 'i'),
215
+ // Done-prefix — "Done" at sentence start followed by past verb + resource noun.
216
+ new RegExp(String.raw `^Done\b[^a-zA-Z]{0,10}(?:${DISCORD_ACTION_INTENT_PAST_VERBS})\b[^.!?\n]{0,80}\b(?:${DISCORD_ACTION_INTENT_RESOURCE_NOUNS})s?\b`, 'i'),
207
217
  ];
208
218
  function stripCodeLikeDiscordActionText(text) {
209
219
  return text
@@ -238,7 +248,7 @@ export function buildPromisedDiscordActionWithoutExecutionNotice(visibleReplyTex
238
248
  return '';
239
249
  if (!claimsImmediateDiscordActionIntent(visibleReplyText))
240
250
  return '';
241
- return 'Warning: this reply says Discord-managed work is starting or being handled now, but this turn ended with zero actionable `<discord-action>` blocks and zero executed action results. If work has not started yet, say that clearly instead.';
251
+ return 'Warning: this reply says Discord-managed work was performed or is being performed, but this turn ended with zero actionable `<discord-action>` blocks and zero executed action results. If work has not started yet, say that clearly instead.';
242
252
  }
243
253
  export function appendPromisedDiscordActionWithoutExecutionNotice(text, actionsCount, actionResultsCount) {
244
254
  const notice = buildPromisedDiscordActionWithoutExecutionNotice(text, actionsCount, actionResultsCount);
@@ -172,6 +172,36 @@ describe('claimsImmediateDiscordActionIntent', () => {
172
172
  expect(claimsImmediateDiscordActionIntent("I'm reading that now.")).toBe(false);
173
173
  expect(claimsImmediateDiscordActionIntent("I'm listing that now.")).toBe(false);
174
174
  });
175
+ it('matches headless past-tense claims (no subject)', () => {
176
+ expect(claimsImmediateDiscordActionIntent('Posted the plan to the channel')).toBe(true);
177
+ });
178
+ it('matches first-person past-tense claims', () => {
179
+ expect(claimsImmediateDiscordActionIntent('I posted the SEO plan to #website')).toBe(true);
180
+ expect(claimsImmediateDiscordActionIntent('I sent the message')).toBe(true);
181
+ });
182
+ it('matches perfect-tense claims', () => {
183
+ expect(claimsImmediateDiscordActionIntent("I've posted it to the channel")).toBe(true);
184
+ expect(claimsImmediateDiscordActionIntent('I have created the task')).toBe(true);
185
+ });
186
+ it('matches done-prefix past-tense claims', () => {
187
+ expect(claimsImmediateDiscordActionIntent('Done. Sent the message.')).toBe(true);
188
+ expect(claimsImmediateDiscordActionIntent('Done — posted the reply')).toBe(true);
189
+ });
190
+ it('does not match past-tense without a resource noun', () => {
191
+ expect(claimsImmediateDiscordActionIntent('I posted about this on my blog')).toBe(false);
192
+ });
193
+ it('does not match third-person past-tense (no "I" subject)', () => {
194
+ expect(claimsImmediateDiscordActionIntent('The user posted a message earlier')).toBe(false);
195
+ });
196
+ it('does not match conditional past-tense', () => {
197
+ expect(claimsImmediateDiscordActionIntent('If I posted the task, it would appear in the thread')).toBe(false);
198
+ });
199
+ it('does not match capability phrasing with past-tense resource nouns (regression)', () => {
200
+ expect(claimsImmediateDiscordActionIntent("I can post the message when you're ready")).toBe(false);
201
+ });
202
+ it('does not match example-prefixed past-tense claims', () => {
203
+ expect(claimsImmediateDiscordActionIntent('Example: I posted the reply')).toBe(false);
204
+ });
175
205
  });
176
206
  describe('buildPromisedDiscordActionWithoutExecutionNotice', () => {
177
207
  it('returns empty string when the reply does not claim immediate action intent', () => {
@@ -183,18 +213,24 @@ describe('buildPromisedDiscordActionWithoutExecutionNotice', () => {
183
213
  });
184
214
  it('returns a warning when the reply promises current work but nothing ran', () => {
185
215
  const out = buildPromisedDiscordActionWithoutExecutionNotice("I'm creating that task now.", 0, 0);
186
- expect(out).toContain('Discord-managed work is starting or being handled now');
216
+ expect(out).toContain('Discord-managed work was performed or is being performed');
187
217
  expect(out).toContain('zero actionable `<discord-action>` blocks');
188
218
  expect(out).toContain('zero executed action results');
189
219
  });
190
220
  it('returns a warning for progress phrases the prompt guidance already forbids without actions', () => {
191
221
  const out = buildPromisedDiscordActionWithoutExecutionNotice('Taking the next pass now.', 0, 0);
192
- expect(out).toContain('Discord-managed work is starting or being handled now');
222
+ expect(out).toContain('Discord-managed work was performed or is being performed');
193
223
  });
194
224
  it('returns empty string for generic assistant prose without Discord-action intent', () => {
195
225
  expect(buildPromisedDiscordActionWithoutExecutionNotice('Let me check that now.', 0, 0)).toBe('');
196
226
  expect(buildPromisedDiscordActionWithoutExecutionNotice("I'll show you that now.", 0, 0)).toBe('');
197
227
  });
228
+ it('returns a warning for a past-tense claim with zero actions/results', () => {
229
+ const out = buildPromisedDiscordActionWithoutExecutionNotice('I posted the plan to the channel.', 0, 0);
230
+ expect(out).toContain('Discord-managed work was performed or is being performed');
231
+ expect(out).toContain('zero actionable `<discord-action>` blocks');
232
+ expect(out).toContain('zero executed action results');
233
+ });
198
234
  });
199
235
  describe('appendPromisedDiscordActionWithoutExecutionNotice', () => {
200
236
  it('appends the warning beneath the visible reply text', () => {
package/package.json CHANGED
@@ -1,8 +1,23 @@
1
1
  {
2
2
  "name": "discoclaw",
3
- "version": "1.2.1",
3
+ "version": "1.2.2",
4
4
  "description": "Personal AI orchestrator that turns Discord into a persistent workspace",
5
5
  "license": "MIT",
6
+ "keywords": [
7
+ "discord",
8
+ "bot",
9
+ "ai",
10
+ "assistant",
11
+ "orchestrator",
12
+ "automation",
13
+ "claude",
14
+ "openai",
15
+ "memory",
16
+ "tasks",
17
+ "voice",
18
+ "personal-ai"
19
+ ],
20
+ "homepage": "https://discoclaw.ai",
6
21
  "repository": {
7
22
  "type": "git",
8
23
  "url": "https://github.com/DiscoClaw/discoclaw.git"