discoclaw 0.9.0 → 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (208) hide show
  1. package/.context/automations.md +151 -0
  2. package/.context/bot-setup.md +268 -5
  3. package/.context/dev.md +121 -2
  4. package/.context/discord.md +104 -0
  5. package/.context/ops.md +188 -8
  6. package/.context/runtime.md +144 -34
  7. package/.context/tasks.md +142 -0
  8. package/.env.example +83 -17
  9. package/.env.example.full +70 -4
  10. package/README.md +159 -19
  11. package/dist/browser/managed-browser.js +867 -0
  12. package/dist/browser/managed-browser.test.js +386 -0
  13. package/dist/canvas/apps.js +291 -0
  14. package/dist/canvas/artifact-store.js +131 -0
  15. package/dist/canvas/artifact-store.test.js +57 -0
  16. package/dist/canvas/canvas-action.js +504 -0
  17. package/dist/canvas/canvas-action.test.js +431 -0
  18. package/dist/canvas/file-export.js +100 -0
  19. package/dist/canvas/file-export.test.js +74 -0
  20. package/dist/canvas/launch-store.js +207 -0
  21. package/dist/canvas/launch-store.test.js +115 -0
  22. package/dist/canvas/runtime-entry.test.js +35 -0
  23. package/dist/canvas/server.js +653 -0
  24. package/dist/canvas/server.test.js +352 -0
  25. package/dist/canvas/shell.js +584 -0
  26. package/dist/canvas/shell.test.js +14 -0
  27. package/dist/canvas/tokens.js +84 -0
  28. package/dist/cli/dashboard.js +6 -2
  29. package/dist/cli/dashboard.test.js +3 -2
  30. package/dist/cli/index.js +354 -3
  31. package/dist/cli/index.test.js +314 -1
  32. package/dist/cli/init-wizard.js +72 -42
  33. package/dist/cli/init-wizard.test.js +100 -17
  34. package/dist/config.js +247 -13
  35. package/dist/config.test.js +90 -19
  36. package/dist/cron/cron-prompt.js +67 -2
  37. package/dist/cron/cron-prompt.test.js +95 -0
  38. package/dist/cron/cron-sync.js +40 -8
  39. package/dist/cron/cron-sync.test.js +120 -4
  40. package/dist/cron/executor.js +130 -35
  41. package/dist/cron/executor.test.js +165 -0
  42. package/dist/cron/run-stats.js +15 -1
  43. package/dist/cron/run-stats.test.js +139 -8
  44. package/dist/dashboard/api/snapshot.test.js +1 -0
  45. package/dist/dashboard/auth-probe.js +110 -0
  46. package/dist/dashboard/auth-probe.test.js +157 -0
  47. package/dist/dashboard/page.js +980 -626
  48. package/dist/dashboard/page.test.js +9 -20
  49. package/dist/dashboard/server-errors.js +1 -1
  50. package/dist/dashboard/server.js +171 -23
  51. package/dist/dashboard/server.test.js +311 -12
  52. package/dist/dashboard/snapshot.js +96 -0
  53. package/dist/dashboard/snapshot.test.js +177 -0
  54. package/dist/discord/abort-registry.js +39 -10
  55. package/dist/discord/action-categories.js +36 -0
  56. package/dist/discord/action-categories.test.js +57 -0
  57. package/dist/discord/action-flags.js +1 -0
  58. package/dist/discord/actions-archive.js +5 -19
  59. package/dist/discord/actions-bot-profile.js +4 -16
  60. package/dist/discord/actions-channels.js +17 -72
  61. package/dist/discord/actions-config.js +106 -57
  62. package/dist/discord/actions-config.test.js +172 -35
  63. package/dist/discord/actions-crons.js +88 -80
  64. package/dist/discord/actions-crons.test.js +142 -0
  65. package/dist/discord/actions-forge.js +12 -27
  66. package/dist/discord/actions-forge.test.js +3 -10
  67. package/dist/discord/actions-guild.js +12 -53
  68. package/dist/discord/actions-imagegen.js +8 -35
  69. package/dist/discord/actions-imagegen.test.js +2 -3
  70. package/dist/discord/actions-loop.js +9 -0
  71. package/dist/discord/actions-loop.test.js +25 -1
  72. package/dist/discord/actions-memory.js +41 -25
  73. package/dist/discord/actions-memory.test.js +91 -6
  74. package/dist/discord/actions-messaging.js +22 -89
  75. package/dist/discord/actions-moderation.js +4 -17
  76. package/dist/discord/actions-plan.js +14 -40
  77. package/dist/discord/actions-plan.test.js +1 -1
  78. package/dist/discord/actions-poll.js +2 -9
  79. package/dist/discord/actions-spawn.js +3 -15
  80. package/dist/discord/actions-spawn.test.js +4 -5
  81. package/dist/discord/actions-voice.js +5 -24
  82. package/dist/discord/actions.js +39 -68
  83. package/dist/discord/actions.test.js +119 -11
  84. package/dist/discord/activity-launch.js +49 -0
  85. package/dist/discord/audit-handler.js +6 -7
  86. package/dist/discord/audit-handler.test.js +5 -0
  87. package/dist/discord/browser-command.js +173 -0
  88. package/dist/discord/browser-command.test.js +140 -0
  89. package/dist/discord/deferred-runner.js +12 -0
  90. package/dist/discord/deferred-runner.test.js +20 -1
  91. package/dist/discord/durable-consolidation.js +12 -3
  92. package/dist/discord/durable-memory.js +62 -2
  93. package/dist/discord/durable-memory.test.js +139 -1
  94. package/dist/discord/forge-auto-implement.js +4 -1
  95. package/dist/discord/forge-commands.js +8 -8
  96. package/dist/discord/forge-commands.test.js +3 -0
  97. package/dist/discord/health-command.js +6 -1
  98. package/dist/discord/health-command.test.js +94 -0
  99. package/dist/discord/help-command.js +1 -0
  100. package/dist/discord/help-command.test.js +6 -0
  101. package/dist/discord/long-run-watchdog-notice.js +69 -12
  102. package/dist/discord/long-run-watchdog-notice.test.js +156 -1
  103. package/dist/discord/long-run-watchdog.js +60 -1
  104. package/dist/discord/long-run-watchdog.test.js +198 -0
  105. package/dist/discord/message-coordinator.followup-lifecycle.test.js +383 -22
  106. package/dist/discord/message-coordinator.js +515 -79
  107. package/dist/discord/message-coordinator.onboarding.test.js +1 -1
  108. package/dist/discord/message-coordinator.run-state.test.js +2 -0
  109. package/dist/discord/message-coordinator.test.js +680 -2
  110. package/dist/discord/models-command.js +69 -15
  111. package/dist/discord/models-command.test.js +92 -2
  112. package/dist/discord/output-common.js +59 -0
  113. package/dist/discord/output-common.test.js +65 -1
  114. package/dist/discord/plan-commands.js +24 -2
  115. package/dist/discord/plan-forge-availability.js +22 -0
  116. package/dist/discord/plan-forge-availability.test.js +34 -0
  117. package/dist/discord/plan-manager.js +3 -2
  118. package/dist/discord/plan-manager.test.js +65 -0
  119. package/dist/discord/plan-parser.js +33 -0
  120. package/dist/discord/prompt-common.js +27 -0
  121. package/dist/discord/reaction-handler.js +43 -5
  122. package/dist/discord/reaction-handler.test.js +68 -1
  123. package/dist/discord/reaction-prompts.js +3 -15
  124. package/dist/discord/reaction-prompts.test.js +4 -4
  125. package/dist/discord/status-channel.js +16 -1
  126. package/dist/discord/status-channel.test.js +39 -2
  127. package/dist/discord/status-command.js +37 -5
  128. package/dist/discord/status-command.test.js +67 -1
  129. package/dist/discord/update-command.js +23 -0
  130. package/dist/discord/update-command.test.js +32 -0
  131. package/dist/discord/user-errors.test.js +2 -2
  132. package/dist/discord/verify-push.js +220 -0
  133. package/dist/discord/verify-push.test.js +260 -0
  134. package/dist/discord-followup.test.js +1 -1
  135. package/dist/discord.browser-command.integration.test.js +132 -0
  136. package/dist/discord.js +16 -0
  137. package/dist/health/config-doctor.js +142 -51
  138. package/dist/health/config-doctor.test.js +147 -5
  139. package/dist/health/credential-check.js +23 -4
  140. package/dist/health/credential-check.test.js +37 -15
  141. package/dist/index.js +328 -75
  142. package/dist/index.post-connect.js +21 -0
  143. package/dist/index.post-connect.test.js +62 -0
  144. package/dist/index.runtime.js +12 -0
  145. package/dist/index.runtime.test.js +35 -1
  146. package/dist/npm-managed.js +99 -0
  147. package/dist/npm-managed.test.js +48 -1
  148. package/dist/onboarding/onboarding-flow.js +7 -1
  149. package/dist/onboarding/onboarding-writer.js +10 -9
  150. package/dist/runtime/anthropic-rest.js +46 -3
  151. package/dist/runtime/anthropic-rest.test.js +2 -2
  152. package/dist/runtime/claude-code-cli.js +2 -1
  153. package/dist/runtime/cli-adapter.js +13 -4
  154. package/dist/runtime/cli-shared.js +65 -1
  155. package/dist/runtime/codex-cli.js +2 -1
  156. package/dist/runtime/long-running-process.js +9 -1
  157. package/dist/runtime/migration-hints.js +44 -0
  158. package/dist/runtime/migration-hints.test.js +40 -0
  159. package/dist/runtime/model-smoke-helpers.js +15 -15
  160. package/dist/runtime/model-smoke.test.js +68 -69
  161. package/dist/runtime/model-tiers.js +8 -1
  162. package/dist/runtime/model-tiers.test.js +38 -3
  163. package/dist/runtime/openai-compat.js +56 -0
  164. package/dist/runtime/openai-compat.test.js +97 -0
  165. package/dist/runtime/openrouter-smoke.test.js +206 -0
  166. package/dist/runtime/registry.test.js +4 -4
  167. package/dist/runtime/resolver.js +35 -0
  168. package/dist/runtime/resolver.test.js +71 -0
  169. package/dist/runtime/runtime-failure.js +0 -10
  170. package/dist/runtime/runtime-failure.test.js +7 -7
  171. package/dist/runtime/runtime-path-contract.js +147 -0
  172. package/dist/runtime/runtime-path-contract.test.js +133 -0
  173. package/dist/runtime/strategies/template-strategy.js +1 -1
  174. package/dist/runtime-overrides.js +16 -0
  175. package/dist/runtime-overrides.test.js +40 -9
  176. package/dist/service-control.js +18 -0
  177. package/dist/tasks/task-action-executor.test.js +6 -10
  178. package/dist/tasks/task-action-prompt.js +19 -58
  179. package/dist/vendor/canvas-runtime.js +2 -0
  180. package/dist/vendor/embedded-app-sdk.js +9981 -0
  181. package/dist/voice/voice-prompt-builder.js +1 -1
  182. package/dist/voice/voice-prompt-builder.test.js +2 -0
  183. package/docs/audit/claude-blank-machine-readiness.md +79 -0
  184. package/docs/audit/claude-npm-managed-path.md +110 -0
  185. package/docs/audit/codex-blank-machine-readiness.md +91 -0
  186. package/docs/audit/codex-npm-managed-path.md +125 -0
  187. package/docs/audit/gemini-support-boundary.md +55 -0
  188. package/docs/audit/openrouter-api-key-support-boundary.md +88 -0
  189. package/docs/audit/openrouter-parity-audit.md +102 -0
  190. package/docs/audit/provider-auth-1.0-matrix.md +71 -0
  191. package/docs/configuration.md +520 -0
  192. package/docs/discord-bot-setup.md +387 -0
  193. package/docs/official-docs.md +326 -1
  194. package/docs/runtime-switching.md +448 -0
  195. package/package.json +27 -6
  196. package/templates/canvas/README.md +11 -0
  197. package/templates/canvas/chart.html +74 -0
  198. package/templates/canvas/dashboard.html +34 -0
  199. package/templates/canvas/data-table.html +67 -0
  200. package/templates/canvas/diff-viewer.html +45 -0
  201. package/templates/canvas/form.html +86 -0
  202. package/templates/instructions/SYSTEM_DEFAULTS.md +25 -0
  203. package/templates/instructions/TOOLS.md +26 -3
  204. package/templates/instructions/canvas.md +32 -0
  205. package/dist/runtime/gemini-cli.js +0 -16
  206. package/dist/runtime/gemini-cli.test.js +0 -431
  207. package/dist/runtime/strategies/gemini-strategy.js +0 -63
  208. package/templates/instructions/SYSTEM_DEFAULTS.md.ledger.json +0 -5
@@ -85,3 +85,154 @@ creating forum threads.
85
85
  | Gated actions | `allowedActions` for least-privilege |
86
86
 
87
87
  See [docs/cron-patterns.md](../docs/cron-patterns.md) for full examples of each.
88
+
89
+ ## Data Directory
90
+
91
+ Cron data lives in `data/cron/`:
92
+
93
+ | File/Dir | Contents |
94
+ |----------|----------|
95
+ | `cron-run-stats.json` | Per-job run statistics: run count, last run time/status, schedule, model, channel, projection state. Rewritten on every run completion. |
96
+ | `tag-map.json` | Maps tag names (e.g. `report`, `monitor`) to Discord forum tag snowflake IDs. Used for auto-tagging cron threads. |
97
+ | `locks/` | Per-job lock directories. Each active job gets `<sanitized-id>.<hash>.lock/meta.json`. Prevents overlap (concurrent execution of the same job). |
98
+
99
+ ### Lock directory format
100
+ Each lock dir contains a `meta.json`:
101
+ ```json
102
+ {"pid":12345,"token":"a1b2c3...","acquiredAt":"2026-03-24T15:00:00.009Z","startTime":45779798}
103
+ ```
104
+ - `pid` — PID of the process holding the lock
105
+ - `token` — random token for safe release (prevents cross-process release)
106
+ - `startTime` — Linux `/proc/<pid>/stat` field 22 (jiffies); detects PID reuse
107
+
108
+ ### Exact commands for data inspection
109
+ ```bash
110
+ # View all job stats
111
+ jq . data/cron/cron-run-stats.json
112
+
113
+ # List jobs and their last run status
114
+ jq '.jobs | to_entries[] | {id: .key, status: .value.lastRunStatus, lastRun: .value.lastRunAt}' data/cron/cron-run-stats.json
115
+
116
+ # Check for stuck locks
117
+ ls -la data/cron/locks/
118
+
119
+ # Inspect a specific lock
120
+ cat data/cron/locks/*.lock/meta.json 2>/dev/null
121
+
122
+ # Check if a locked PID is still alive
123
+ for meta in data/cron/locks/*.lock/meta.json; do
124
+ pid=$(jq -r .pid "$meta" 2>/dev/null)
125
+ kill -0 "$pid" 2>/dev/null && echo "$meta: PID $pid alive" || echo "$meta: PID $pid STALE"
126
+ done
127
+
128
+ # View the tag map
129
+ jq . data/cron/tag-map.json
130
+
131
+ # Check run stats file size (large = many jobs or accumulating history)
132
+ ls -lh data/cron/cron-run-stats.json
133
+ ```
134
+
135
+ ## Known Footguns
136
+
137
+ - **`<cron-state>` replaces, does not merge:** If a job's state is `{"cursor": "abc", "count": 5}` and the AI outputs `<cron-state>{"cursor": "def"}</cron-state>`, the `count` key is lost. The AI must echo back all keys it wants to keep.
138
+ - **Manual thread creation is ignored:** Jobs must be created via the `cronCreate` action. Manually creating a forum thread will not register a job — the thread will sit inert. Use `cronCreate` or ask the bot to create it.
139
+ - **Archiving ≠ deleting:** The `cronDelete` action archives the thread (reversible). The job stops, but history is preserved. Actual thread deletion is permanent and removes all messages.
140
+ - **Chain depth silently caps at 10:** If a chain exceeds 10 hops, execution stops with no visible error in the target channel. Check the bot logs for `chain depth limit reached`.
141
+ - **Webhook jobs require `DISCOCLAW_WEBHOOK_ENABLED=true`:** Defining a webhook-triggered job without enabling the webhook server means the job exists but can never fire. No warning is logged at startup.
142
+ - **Timezone defaults to system timezone:** If `DEFAULT_TIMEZONE` is unset and the server's system timezone is UTC, all cron schedules without explicit timezones run in UTC. This catches people who expect local time.
143
+ - **Cron config changes in `.env` require restart:** Changing `DISCOCLAW_CRON_ENABLED`, `DISCOCLAW_CRON_FORUM`, or `DEFAULT_TIMEZONE` in `.env` has no effect until the service is restarted (`systemctl --user restart discoclaw.service`). The cron subsystem reads config once at startup.
144
+ - **`dist/` must be rebuilt for cron code changes:** Cron executor, parser, and scheduler code lives in `src/cron/`. After modifying these files, `pnpm build` must be run and the service restarted — the systemd service runs `dist/index.js`, not `src/`.
145
+
146
+ ## Common Failure Modes
147
+
148
+ ### Cron job not firing on schedule
149
+ **Symptom:** Job was created and confirmed, schedule looks correct, but no output appears at the expected time.
150
+ **Cause (in order of likelihood):**
151
+ 1. Thread is archived (job is paused).
152
+ 2. Timezone mismatch — job runs in UTC but user expects local time.
153
+ 3. Previous run is still active (overlap guard skipped this tick).
154
+ 4. `DISCOCLAW_CRON_ENABLED` is `0` or `DISCOCLAW_CRON_FORUM` is unset.
155
+ 5. Service is in `failed` state after crash loop — cron timers are not running.
156
+ **Recovery:**
157
+ ```bash
158
+ # First: is the service even running?
159
+ systemctl --user status discoclaw.service
160
+ # If "failed (Result: start-limit-hit)":
161
+ systemctl --user reset-failed discoclaw.service
162
+ systemctl --user start discoclaw.service
163
+
164
+ # Check if the thread is archived (in Discord, archived threads are hidden by default)
165
+ # Use the cronList action to see all jobs and their states
166
+
167
+ # Check bot logs for skip reasons
168
+ journalctl --user -u discoclaw.service --since "1 hour ago" --no-pager | grep -i "cron\|skip\|overlap"
169
+
170
+ # Verify cron config
171
+ grep -E 'DISCOCLAW_CRON_ENABLED|DISCOCLAW_CRON_FORUM|DEFAULT_TIMEZONE' .env
172
+
173
+ # Verify timezone
174
+ timedatectl | grep "Time zone"
175
+ ```
176
+
177
+ ### Cron job fires but output goes to wrong channel
178
+ **Symptom:** Job runs successfully but posts to the wrong Discord channel or to no channel.
179
+ **Cause:** The AI parsed the target channel incorrectly from the natural-language definition, or the channel ID in the parsed config is stale (channel was deleted/renamed).
180
+ **Recovery:**
181
+ ```bash
182
+ # Use cronShow to inspect the parsed config
183
+ # Ask the bot: "show cron <job-name>"
184
+ # Check the targetChannel field
185
+
186
+ # Update by editing the starter message in the forum thread, then:
187
+ # Ask the bot: "trigger cron <job-name>" to test the new config
188
+ ```
189
+
190
+ ### State corruption — job keeps repeating or skipping work
191
+ **Symptom:** A stateful polling job re-processes old items, or skips new ones.
192
+ **Cause:** The AI's `<cron-state>` output replaced state instead of merging, or the state hit the 4000-char injection cap and was truncated.
193
+ **Recovery:**
194
+ ```bash
195
+ # Check current state via cronShow
196
+ # Reset state by asking the bot:
197
+ # "update cron <job-name> with state {}"
198
+
199
+ # Or reset to a specific cursor:
200
+ # "update cron <job-name> with state {\"cursor\": \"known-good-value\"}"
201
+ ```
202
+
203
+ ### Cron job stuck — overlap guard never releases
204
+ **Symptom:** A job ran once and now never fires again. Logs show `Lock held by PID XXXXX` on every tick.
205
+ **Cause:** The previous execution crashed without releasing its lock in `data/cron/locks/`. The lock dir persists with a `meta.json` pointing to a dead (or reused) PID. Stale-lock detection usually catches this, but can fail if `/proc/<pid>/stat` is unreadable or the PID was reused by a long-lived process.
206
+ **Recovery:**
207
+ ```bash
208
+ # List all lock dirs
209
+ ls -la data/cron/locks/
210
+
211
+ # Find the stuck lock (match the cron ID from the log message)
212
+ # Lock dirs are named <sanitized-id>.<hash>.lock
213
+ cat data/cron/locks/*.lock/meta.json 2>/dev/null
214
+
215
+ # Check if the PID is alive
216
+ kill -0 <pid> 2>/dev/null && echo "alive" || echo "stale"
217
+
218
+ # If stale: remove the lock dir, the next tick will acquire fresh
219
+ rm -rf data/cron/locks/<lock-dir-name>.lock
220
+
221
+ # If the PID is alive but belongs to a different process (PID reuse):
222
+ # compare the startTime in meta.json with /proc/<pid>/stat field 22
223
+ cat /proc/<pid>/stat | awk '{print $22}'
224
+ # If they differ, the lock is stale — safe to remove
225
+ ```
226
+
227
+ ### Chain cascade — downstream jobs keep firing
228
+ **Symptom:** A chained pipeline fires repeatedly or produces unexpected output.
229
+ **Cause:** Cycle in the chain graph (should be caught at write time, but can occur if jobs were edited after initial creation without re-validation).
230
+ **Recovery:**
231
+ ```bash
232
+ # Check logs for chain-related entries
233
+ journalctl --user -u discoclaw.service --since "30 min ago" --no-pager | grep -i "chain"
234
+
235
+ # Break the cycle by removing the chain field from the looping job:
236
+ # "update cron <job-name> with chain: none"
237
+ # Then re-architect the chain graph
238
+ ```
@@ -5,7 +5,7 @@
5
5
  ## Quick reference
6
6
 
7
7
  1. **Developer Portal** → create application → Bot → enable **Message Content Intent** → copy token to `.env` (`DISCORD_TOKEN`).
8
- 2. **OAuth2 URL Generator** → scope `bot` pick permissions (see permission profiles in `docs/discord-bot-setup.md`) invite to server.
8
+ 2. **Invite** quickest: `https://discord.com/oauth2/authorize?client_id=CLIENT_ID&scope=bot&permissions=0` (replace `CLIENT_ID`). For a permanent link with pre-set permissions, use the **Installation page** set Install Link to "Discord Provided Link" → Default Install Settings → add scope `bot` and pick permissions (see profiles in `docs/discord-bot-setup.md`). The `bot` scope is required — `applications.commands` alone won't add the bot to a server.
9
9
  3. **Configure `.env`**:
10
10
  - *Global install:* `discoclaw init` — wizard creates `.env` with `DISCORD_TOKEN`, `DISCORD_ALLOW_USER_IDS`, and `DISCORD_CHANNEL_IDS`.
11
11
  - *From source:* `pnpm setup` for guided configuration, or copy `.env.example` → `.env` and set `DISCORD_TOKEN`, `DISCORD_ALLOW_USER_IDS` (fail-closed if empty), `DISCORD_CHANNEL_IDS` (recommended).
@@ -13,12 +13,275 @@
13
13
  - *Global install:* `discoclaw install-daemon` to register the systemd service, then DM the bot to confirm it responds.
14
14
  - *From source:* `pnpm dev`, DM the bot, post in allowed/disallowed channels.
15
15
 
16
+ ## Exact Command Reference
17
+
18
+ ### From source — setup and validation
19
+
20
+ ```bash
21
+ # Guided interactive setup (creates .env)
22
+ pnpm setup
23
+
24
+ # Manual .env creation (essentials only)
25
+ cp .env.example .env
26
+
27
+ # Preflight check — validates env, token format, snowflake IDs, config doctor
28
+ pnpm preflight
29
+
30
+ # Preflight for a fresh clone (ignores shell env, reads only checkout .env)
31
+ pnpm preflight:blank-machine
32
+
33
+ # Discord smoke test — verifies bot token and gateway connection
34
+ pnpm discord:smoke-test
35
+
36
+ # Discord smoke test with guild membership check
37
+ pnpm discord:smoke-test -- --guild-id 123456789012345678
38
+
39
+ # Claude runtime auth check
40
+ pnpm claude:auth-smoke
41
+
42
+ # Start the bot in dev mode
43
+ pnpm dev
44
+ ```
45
+
46
+ ### Global install — setup and validation
47
+
48
+ ```bash
49
+ # Interactive wizard — creates .env and workspace
50
+ discoclaw init
51
+
52
+ # Register as a user-level systemd service
53
+ discoclaw install-daemon
54
+
55
+ # Multi-instance: unique service name per instance
56
+ discoclaw install-daemon --service-name discoclaw-work
57
+
58
+ # Config/health check
59
+ discoclaw doctor
60
+
61
+ # Claude auth smoke test (npm-managed path)
62
+ discoclaw claude auth-smoke
63
+ ```
64
+
65
+ ### Discord smoke test arguments
66
+
67
+ ```bash
68
+ # Print guild IDs the bot is in (useful for debugging guild-id mismatches)
69
+ pnpm discord:smoke-test -- --print-guilds
70
+
71
+ # Override timeout (default 12s)
72
+ DISCORD_SMOKE_TEST_TIMEOUT_MS=30000 pnpm discord:smoke-test
73
+ ```
74
+
16
75
  ## Getting IDs
17
76
 
18
77
  Discord client: Settings → Advanced → Developer Mode, then right-click a user/channel → Copy ID.
19
78
 
20
- ## Common issues
79
+ IDs are "snowflakes" — 17–20 digit numbers. The preflight check validates format:
80
+ ```bash
81
+ # Preflight reports invalid snowflakes explicitly
82
+ pnpm preflight
83
+ # Example output: "DISCORD_ALLOW_USER_IDS contains invalid IDs: abc123"
84
+ ```
85
+
86
+ ## Known Footguns
87
+
88
+ - **Message Content Intent is invisible when missing.** If you skip enabling it in Developer Portal → Bot → Privileged Gateway Intents, the bot connects, appears online, and `msg.content` is silently empty in guild channels. No error is logged — the bot just ignores every guild message. DMs still work (they don't require the intent). This is the #1 setup failure mode.
89
+ - **`applications.commands` scope is not `bot` scope.** Using `scope=applications.commands` in the invite URL registers slash commands but does not add the bot to the server. The bot cannot connect, read messages, or respond. You must include `scope=bot` (or both).
90
+ - **Copying Application ID instead of Bot Token.** The Developer Portal shows the Application ID prominently on General Information. The Bot Token is on the Bot page. They look similar (long alphanumeric strings) but are not interchangeable. If you copy the Application ID into `DISCORD_TOKEN`, the bot fails at login with "An invalid token was provided."
91
+ - **Token revealed once, then hidden.** After creating the bot, the token is shown once. If you navigate away without copying it, you must click "Reset Token" to generate a new one — the old one is gone. Resetting invalidates the previous token immediately.
92
+ - **Empty `DISCORD_ALLOW_USER_IDS` = silent rejection.** The bot starts, connects, appears online, but responds to nobody. No error is logged. This is fail-closed by design, but it's surprising on first setup.
93
+ - **`DISCORD_CHANNEL_IDS` restricts guild channels only.** DMs always work regardless of this setting. If you set channel IDs and then test in a channel not in the list, the bot silently ignores you. Test in a listed channel or via DM.
94
+ - **`DISCORD_REQUIRE_CHANNEL_CONTEXT=1` (default) blocks unlisted channels.** Even if the channel is in `DISCORD_CHANNEL_IDS`, the bot won't respond in a guild channel without a matching context file under `content/discord/channels/`. Enable `DISCORD_AUTO_INDEX_CHANNEL_CONTEXT=1` (default) to auto-create stubs, or run `pnpm sync:discord-context` to scaffold them.
95
+ - **Role hierarchy blocks moderation actions.** The bot can only manage roles below its own role in Server Settings → Roles. If the bot role is at the bottom, actions like role assignment, timeout, and kick will fail with "Missing Permissions" even if the permission bits are granted.
96
+ - **Private threads require explicit addition.** The bot must be added to private threads manually regardless of its channel permissions. Public threads are auto-joined when `DISCORD_AUTO_JOIN_THREADS=1`.
97
+ - **Editing `.env.example` instead of `.env`.** `.env.example` is tracked in git and has no runtime effect. Your actual config is in `.env` (gitignored). A common mistake — changes to `.env.example` appear to do nothing.
98
+ - **`discoclaw install-daemon` PATH divergence.** The systemd service uses `/usr/bin/node` and a fixed `PATH`. The daemon may not find the same CLI binaries (e.g., `claude`, `codex`) that your interactive shell finds. Verify service logs after daemon install.
99
+ - **Service stuck after repeated setup failures.** If the service crashes 3 times within 10 minutes during initial setup (bad token, missing env, etc.), systemd marks it as `failed` and refuses further starts. Run `systemctl --user reset-failed discoclaw.service` to clear the failure counter before retrying.
100
+ - **Stale `dist/` after setup changes.** If you checked out a different branch or changed source files during setup, `dist/` may contain old compiled code. Run `rm -rf dist && pnpm build` before `pnpm dev` or service start to ensure fresh output.
101
+
102
+ ## Common Failure Modes
103
+
104
+ ### Bot token rejected at login
105
+ **Symptom:** `pnpm dev` or the systemd service exits immediately. Logs show `Login failed: Error [TOKEN_INVALID]: An invalid token was provided.`
106
+ **Cause:** Wrong value in `DISCORD_TOKEN` — usually the Application ID was copied instead of the Bot Token, or the token was reset in the Developer Portal.
107
+ **Recovery:**
108
+ ```bash
109
+ # Verify token is set and looks right (3 dot-separated base64url segments)
110
+ pnpm preflight
111
+
112
+ # If format is wrong: go to Developer Portal → Bot → Reset Token → copy new token
113
+ # Update .env:
114
+ # DISCORD_TOKEN=<new-token>
115
+
116
+ # Verify again
117
+ pnpm preflight
118
+
119
+ # Restart
120
+ pnpm dev
121
+ ```
122
+
123
+ ### Bot online but ignores all guild messages (Message Content Intent)
124
+ **Symptom:** Bot appears online in Discord. DMs work. Guild channel messages are completely ignored — no response, no error in logs.
125
+ **Cause:** Message Content Intent not enabled in the Developer Portal. Without it, `msg.content` is empty for guild messages, so the bot sees every message as blank and drops it.
126
+ **Recovery:**
127
+ 1. Go to Discord Developer Portal → your application → **Bot** → **Privileged Gateway Intents**
128
+ 2. Enable **Message Content Intent**
129
+ 3. Save Changes
130
+ 4. Restart the bot:
131
+ ```bash
132
+ # From source
133
+ pnpm dev
134
+
135
+ # Or systemd
136
+ systemctl --user restart discoclaw.service
137
+ ```
138
+ No code or `.env` change is needed — this is purely a Developer Portal toggle.
139
+
140
+ ### Bot online but ignores all messages (empty allowlist)
141
+ **Symptom:** Bot appears online. No response to DMs or guild messages. Logs show no errors.
142
+ **Cause:** `DISCORD_ALLOW_USER_IDS` is empty, missing, or set to a wrong user ID.
143
+ **Recovery:**
144
+ ```bash
145
+ # Check what's configured
146
+ grep DISCORD_ALLOW_USER_IDS .env
147
+
148
+ # If empty or wrong: get your Discord user ID
149
+ # (Discord → Settings → Advanced → Developer Mode → right-click yourself → Copy ID)
150
+ # Update .env:
151
+ # DISCORD_ALLOW_USER_IDS=123456789012345678
152
+
153
+ # Preflight validates the snowflake format
154
+ pnpm preflight
155
+
156
+ # Restart
157
+ pnpm dev
158
+ ```
159
+
160
+ ### Discord smoke test fails: "Used disallowed intents"
161
+ **Symptom:** `pnpm discord:smoke-test` exits with `Login failed: Error: Used disallowed intents`. Also appears in `journalctl` logs for the systemd service.
162
+ **Cause:** The bot requests `GatewayIntentBits.MessageContent` (a privileged intent) but the Developer Portal has it disabled.
163
+ **Recovery:** Same as "Bot online but ignores all guild messages" above — enable Message Content Intent in the Developer Portal. Then:
164
+ ```bash
165
+ pnpm discord:smoke-test
166
+ # Expected: "Discord bot ready (guilds: N)"
167
+ ```
168
+
169
+ ### Discord smoke test fails: "guild_not_in_cache"
170
+ **Symptom:** `pnpm discord:smoke-test -- --guild-id <ID>` prints `Discord bot ready, but it does not appear to be in guild <ID>`.
171
+ **Cause:** The bot is not in that guild, or the guild ID is wrong.
172
+ **Recovery:**
173
+ ```bash
174
+ # List guilds the bot is actually in
175
+ pnpm discord:smoke-test -- --print-guilds
176
+
177
+ # If the guild is missing: re-invite the bot to that server
178
+ # Use the invite URL from step 2 of the quick reference above
179
+
180
+ # If the guild ID is wrong: copy the correct one
181
+ # (right-click server name → Copy Server ID)
182
+ ```
183
+
184
+ ### Discord smoke test hangs then times out
185
+ **Symptom:** `pnpm discord:smoke-test` prints nothing for 12 seconds, then `Smoke test timed out after 12000ms`.
186
+ **Cause:** Network issue, firewall blocking Discord WebSocket, or the token is valid but the bot cannot establish a gateway connection.
187
+ **Recovery:**
188
+ ```bash
189
+ # Increase timeout to rule out slow networks
190
+ DISCORD_SMOKE_TEST_TIMEOUT_MS=30000 pnpm discord:smoke-test
191
+
192
+ # If it still times out: check network/firewall
193
+ # Discord WebSocket connects to gateway.discord.gg on port 443
194
+ curl -s -o /dev/null -w "%{http_code}" https://discord.com/api/v10/gateway
195
+ # Expected: 200
196
+ ```
197
+
198
+ ### Preflight fails: "DISCORD_TOKEN format invalid"
199
+ **Symptom:** `pnpm preflight` reports `DISCORD_TOKEN format invalid: <reason>`.
200
+ **Cause:** Token is malformed — not three dot-separated base64url segments. Common when the Application ID was pasted, or extra whitespace/quotes were included.
201
+ **Recovery:**
202
+ ```bash
203
+ # Check for accidental quotes or whitespace
204
+ grep DISCORD_TOKEN .env
205
+ # Must be: DISCORD_TOKEN=MTIz...abc (no quotes, no spaces)
206
+
207
+ # If the value looks wrong: regenerate in Developer Portal → Bot → Reset Token
208
+ # Paste the new token (no quotes around the value)
209
+ ```
210
+
211
+ ### Preflight fails: "DISCORD_ALLOW_USER_IDS contains invalid IDs"
212
+ **Symptom:** `pnpm preflight` reports specific IDs that are not valid snowflakes.
213
+ **Cause:** Non-numeric characters, a username instead of a numeric ID, or copy-paste artifacts (e.g., `<@123>` instead of `123`).
214
+ **Recovery:**
215
+ ```bash
216
+ # Discord snowflakes are 17-20 digit numbers
217
+ # Get the correct ID: Discord → right-click user → Copy ID
218
+ # IDs are comma or space separated in .env:
219
+ # DISCORD_ALLOW_USER_IDS=123456789012345678,987654321098765432
220
+ ```
221
+
222
+ ### Bot responds in DMs but not in guild channels
223
+ **Symptom:** DMs work. Guild channel messages are ignored. Message Content Intent is confirmed enabled.
224
+ **Cause:** `DISCORD_CHANNEL_IDS` is set but doesn't include the channel you're testing in, or `DISCORD_REQUIRE_CHANNEL_CONTEXT=1` and no context file exists for that channel.
225
+ **Recovery:**
226
+ ```bash
227
+ # Check channel restrictions
228
+ grep DISCORD_CHANNEL_IDS .env
229
+ # If set: add the channel ID, or clear it to allow all channels
230
+
231
+ # Check channel context requirement
232
+ grep DISCORD_REQUIRE_CHANNEL_CONTEXT .env
233
+ # If 1: ensure a context file exists for the channel
234
+
235
+ # Auto-scaffold missing context files
236
+ pnpm sync:discord-context
237
+
238
+ # Or disable the requirement (not recommended for production)
239
+ # DISCORD_REQUIRE_CHANNEL_CONTEXT=0
240
+ ```
241
+
242
+ ### Multi-instance: second instance uses wrong service name
243
+ **Symptom:** `!restart` or `!update apply` targets the wrong systemd unit. Or `discoclaw install-daemon --service-name X` doesn't take effect.
244
+ **Cause:** `DISCOCLAW_SERVICE_NAME` in `.env` doesn't match the name passed to `install-daemon`.
245
+ **Recovery:**
246
+ ```bash
247
+ # Check what's in .env
248
+ grep DISCOCLAW_SERVICE_NAME .env
249
+
250
+ # Re-run install-daemon (it replaces the line in-place, never duplicates)
251
+ discoclaw install-daemon --service-name discoclaw-work
252
+
253
+ # Verify
254
+ grep DISCOCLAW_SERVICE_NAME .env
255
+ # Expected: DISCOCLAW_SERVICE_NAME=discoclaw-work
256
+
257
+ # Each instance needs its own working directory with its own .env
258
+ ```
259
+
260
+ ## Validation Checklist (quick)
261
+
262
+ After setup, run through these in order. Each step should pass before moving on.
263
+
264
+ ```bash
265
+ # 1. Preflight — validates .env, token format, snowflake IDs
266
+ pnpm preflight # from source
267
+ discoclaw doctor # global install
268
+
269
+ # 2. Discord connection — verifies gateway login
270
+ pnpm discord:smoke-test # from source only
271
+
272
+ # 3. Guild membership (optional)
273
+ pnpm discord:smoke-test -- --guild-id <YOUR_SERVER_ID>
274
+
275
+ # 4. Runtime auth (Claude path)
276
+ pnpm claude:auth-smoke # from source
277
+ discoclaw claude auth-smoke # global install
278
+
279
+ # 5. Live test — start the bot and interact
280
+ pnpm dev # from source
281
+ systemctl --user start discoclaw.service # systemd
21
282
 
22
- - **Bot ignores guild messages**: Message Content Intent not enabled in Developer Portal.
23
- - **"Missing Permissions"**: Bot role is below the target role in Server Settings Roles. Drag it higher.
24
- - **Private threads**: Bot must be explicitly added to private threads regardless of permissions.
283
+ # Then in Discord:
284
+ # - DM the botshould respond (if allowlisted)
285
+ # - Post in an allowlisted channel should respond
286
+ # - Post in a non-allowlisted channel → should NOT respond
287
+ ```
package/.context/dev.md CHANGED
@@ -194,7 +194,8 @@ Two setup paths:
194
194
  | `OPENAI_MODEL` | `gpt-4o` | Default model for the OpenAI adapter |
195
195
  | `OPENROUTER_API_KEY` | *(empty)* | OpenRouter API key; required when `PRIMARY_RUNTIME=openrouter` |
196
196
  | `OPENROUTER_BASE_URL` | *(empty — OpenRouter default)* | OpenRouter API base URL override |
197
- | `OPENROUTER_MODEL` | `anthropic/claude-sonnet-4` | Default model for the OpenRouter adapter |
197
+ | `OPENROUTER_MODEL` | `anthropic/claude-sonnet-4.6` | Default model for the OpenRouter adapter |
198
+ | `OPENROUTER_PROVIDER_PREFERENCES` | *(empty)* | Optional JSON string forwarded as OpenRouter's request `provider` object |
198
199
  | `GEMINI_BIN` | `gemini` | Path/name of the Gemini CLI binary |
199
200
  | `GEMINI_MODEL` | `gemini-2.5-pro` | Default model for the Gemini adapter |
200
201
  | `CODEX_BIN` | `codex` | Path/name of the Codex CLI binary |
@@ -288,7 +289,8 @@ This is especially useful for systemd, where env loading can differ from your sh
288
289
  - **Bot not responding:** Check allowlist (`DISCORD_ALLOW_USER_IDS`), channel restrictions (`DISCORD_CHANNEL_IDS`), and channel context requirement (`DISCORD_REQUIRE_CHANNEL_CONTEXT`).
289
290
  - **Claude CLI errors:** Look for `runtime` or `spawn` in logs. Use `CLAUDE_DEBUG_FILE` to capture full CLI output.
290
291
  - **Timeout issues:** Look for `timeout` in logs. Adjust `RUNTIME_TIMEOUT_MS` if needed.
291
- - **PID lock conflicts:** Look for `pidlock` in logs. See ops.md for stale lock handling.
292
+ - **PID lock conflicts:** Look for `pidlock` or `pid lock` in logs. The lock is a directory at `data/discoclaw.pid.lock/` with `meta.json` inside. See ops.md for stale lock handling.
293
+ - **`.env` not loaded:** If env vars appear unset, check that `.env` exists (not `.env.example`) and is in the project root. `pnpm dev` loads it via dotenv; `node dist/index.js` does not.
292
294
 
293
295
  ## Task Auto-Sync
294
296
 
@@ -340,6 +342,123 @@ SMOKE_TEST_TIERS=fast SMOKE_TEST_TIMEOUT_MS=120000 pnpm test
340
342
 
341
343
  The suite uses your real `.env` — the same config that runs the bot is sufficient. No separate test credentials are needed.
342
344
 
345
+ ## Known Footguns
346
+
347
+ - **`pnpm build` caches stale output:** TypeScript's `tsc` writes to `dist/` incrementally. If you rename or delete a source file, the old `.js` remains in `dist/` and may be imported at runtime. **Symptoms:** mysterious `Cannot find module` errors for files that exist in source, or runtime behavior that doesn't match the code. **Fix:** `rm -rf dist && pnpm build`. Do this after any branch switch, file rename, or when `dist/` behavior doesn't match `src/`.
348
+ - **`.env` not loaded in subshells:** `pnpm dev` loads `.env` via dotenv, but raw `node dist/index.js` does not. Always use `pnpm dev` or `pnpm start` for local runs.
349
+ - **`pnpm i` after branch switch:** Switching branches that change `package.json` or `pnpm-lock.yaml` can leave `node_modules` in a stale state. Run `pnpm i` after checkout if you see unexpected import errors.
350
+ - **Port conflicts with webhook server:** If `DISCOCLAW_WEBHOOK_ENABLED=1` and another process holds port `9400` (or your configured `DISCOCLAW_WEBHOOK_PORT`), the bot crashes on startup with `EADDRINUSE`. Check with `lsof -i :9400`.
351
+ - **Editing `.env.example` vs `.env`:** `.env.example` is tracked in git and has no effect at runtime. Your actual config is in `.env` (gitignored). Editing the wrong file is a common mistake. **How to tell:** `git diff` shows changes → you edited the tracked `.env.example`. `.env` changes never appear in `git diff`.
352
+ - **`.env` values with spaces or special characters:** Values with spaces must be quoted (`VAR="value with spaces"`). Values with `#` are truncated at the `#` (treated as inline comment by dotenv). Wrap in double quotes to include literal `#` characters.
353
+ - **PID lock contention during rapid restarts:** If you `pnpm dev`, Ctrl-C, and immediately `pnpm dev` again, the lock directory (`data/discoclaw.pid.lock/`) may still exist from the previous process. The 2-second grace period can cause `PID lock initializing` errors. Wait 2 seconds or manually `rm -rf data/discoclaw.pid.lock` if it persists.
354
+
355
+ ## Common Failure Modes
356
+
357
+ ### `pnpm build` fails with type errors
358
+ **Symptom:** `tsc` reports type errors in `src/`. Build exits non-zero.
359
+ **Recovery:**
360
+ ```bash
361
+ # Check for stale dist artifacts
362
+ rm -rf dist
363
+ pnpm build
364
+
365
+ # If errors persist, check for missing deps
366
+ pnpm i
367
+ pnpm build
368
+
369
+ # For type errors in unchanged files, your deps may have updated types
370
+ # Check what changed:
371
+ git diff pnpm-lock.yaml
372
+ ```
373
+
374
+ ### `pnpm dev` exits immediately with "Missing DISCORD_TOKEN"
375
+ **Symptom:** Process exits within 1 second, logs `Missing required env: DISCORD_TOKEN`.
376
+ **Cause:** `.env` file is missing, misnamed, or the variable is commented out.
377
+ **Recovery:**
378
+ ```bash
379
+ # Verify .env exists and has the token
380
+ ls -la .env
381
+ grep DISCORD_TOKEN .env
382
+
383
+ # If missing, create from example
384
+ cp .env.example .env
385
+ # Then edit .env with your actual values
386
+ ```
387
+
388
+ ### `pnpm dev` starts but Claude CLI invocations fail
389
+ **Symptom:** Bot responds to messages but replies with an error like "Runtime invocation failed" or "spawn claude ENOENT".
390
+ **Cause:** Claude CLI binary not found on `PATH`, or wrong binary name in `CLAUDE_BIN`.
391
+ **Recovery:**
392
+ ```bash
393
+ # Verify the CLI is installed and reachable
394
+ which claude
395
+ claude --version
396
+
397
+ # If installed but not found, check CLAUDE_BIN in .env
398
+ grep CLAUDE_BIN .env
399
+
400
+ # If using a non-standard path
401
+ CLAUDE_BIN=/path/to/claude pnpm dev
402
+ ```
403
+
404
+ ### Tests fail with "Cannot find module" errors
405
+ **Symptom:** `pnpm test` crashes before tests run, with Node module resolution errors.
406
+ **Cause:** `dist/` is stale or `node_modules` is incomplete.
407
+ **Recovery:**
408
+ ```bash
409
+ rm -rf dist
410
+ pnpm i
411
+ pnpm build
412
+ pnpm test
413
+ ```
414
+
415
+ ### `dist/` contains ghost files from deleted/renamed source
416
+ **Symptom:** Runtime imports a module that no longer exists in `src/`, or old behavior persists after source changes. `pnpm build` succeeds (tsc only checks current source files — it doesn't clean up old output).
417
+ **Cause:** `tsc` incremental compilation never deletes output files. Renamed `src/foo.ts` → `src/bar.ts` leaves `dist/foo.js` behind.
418
+ **Recovery:**
419
+ ```bash
420
+ # Clean and rebuild
421
+ rm -rf dist
422
+ pnpm build
423
+
424
+ # Verify no ghosts: dist/ should only contain files corresponding to src/
425
+ # Quick check — file count should roughly match
426
+ ls src/**/*.ts | wc -l
427
+ ls dist/**/*.js | wc -l
428
+ ```
429
+
430
+ ### PID lock error on `pnpm dev` — "PID lock initializing" or "another instance is already running"
431
+ **Symptom:** `pnpm dev` exits immediately with `PID lock initializing (dir age: Xms)` or `Another discoclaw instance is already running (PID XXXXX)`.
432
+ **Cause:** Previous `pnpm dev` was killed (Ctrl-C / SIGKILL) before the lock was released, or another instance is genuinely running.
433
+ **Recovery:**
434
+ ```bash
435
+ # Check if another instance is actually running
436
+ cat data/discoclaw.pid.lock/meta.json 2>/dev/null
437
+ # If it shows a PID, check if alive:
438
+ kill -0 $(jq -r .pid data/discoclaw.pid.lock/meta.json) 2>/dev/null && echo "alive" || echo "stale"
439
+
440
+ # If stale or "initializing" error: remove and retry
441
+ rm -rf data/discoclaw.pid.lock
442
+ pnpm dev
443
+
444
+ # If alive: stop the other instance first, or use a different terminal
445
+ ```
446
+
447
+ ### `pnpm sync:discord-context` fails
448
+ **Symptom:** Command exits with an error about missing `DISCORD.md` or content directory.
449
+ **Cause:** `DISCOCLAW_CONTENT_DIR` or `DISCOCLAW_DATA_DIR` not set, or the content directory doesn't exist yet.
450
+ **Recovery:**
451
+ ```bash
452
+ # Check which content dir is configured
453
+ grep -E 'DISCOCLAW_CONTENT_DIR|DISCOCLAW_DATA_DIR' .env
454
+
455
+ # Create the directory structure if missing
456
+ mkdir -p data/content/discord/channels
457
+
458
+ # Re-run
459
+ pnpm sync:discord-context
460
+ ```
461
+
343
462
  ## Notes
344
463
  - Runtime invocation defaults are configurable via env (`RUNTIME_MODEL`, `RUNTIME_TOOLS`, `RUNTIME_TIMEOUT_MS`).
345
464
  - If `pnpm dev` fails with "Missing DISCORD_TOKEN", your `.env` isn't loaded or the var is unset.