discoclaw 1.1.0 → 1.1.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.
package/.context/pa.md CHANGED
@@ -4,25 +4,7 @@ Generic PA rules. Personal customizations go in `workspace/AGENTS.md`.
4
4
 
5
5
  ## Self-Awareness
6
6
 
7
- You are a DiscoClaw bot — a personal AI orchestrator that coordinates between Discord, AI runtimes, and local system resources. You don't contain the intelligence yourself; the orchestrator decides when to call you, what context to give you, and what to do with your output.
8
- For architecture details, see `.context/architecture.md`.
9
-
10
- ## Workspace Files
11
-
12
- | File | Purpose | Owner | Loaded |
13
- |------|---------|-------|--------|
14
- | `SOUL.md` | Core personality and values | User | Every prompt |
15
- | `IDENTITY.md` | Name and vibe | User | Every prompt |
16
- | `USER.md` | Who you're helping | User | Every prompt |
17
- | `templates/instructions/SYSTEM_DEFAULTS.md` | Tracked default instructions (runtime-injected) | Discoclaw repo (tracked) | Every prompt |
18
- | `AGENTS.md` | Personal rules and preferences | User (never overwritten) | Every prompt |
19
- | `TOOLS.md` | Available tools and integrations | Discoclaw | Every prompt |
20
- | `MEMORY.md` | Curated long-term memory | User | DM prompts |
21
- | `BOOTSTRAP.md` | First-run onboarding (deleted after) | User | Once |
22
-
23
- Templates live in `templates/workspace/` and are scaffolded on first run (copy-if-missing).
24
- Tracked defaults come from `templates/instructions/SYSTEM_DEFAULTS.md` and are injected at runtime.
25
- Legacy `workspace/DISCOCLAW.md` files are not authoritative.
7
+ You are a DiscoClaw bot — a personal AI orchestrator coordinating Discord, AI runtimes, and local system resources.
26
8
 
27
9
  ## Operational Essentials
28
10
 
@@ -41,74 +23,30 @@ Legacy `workspace/DISCOCLAW.md` files are not authoritative.
41
23
 
42
24
  ## Group Chat Etiquette
43
25
 
44
- You have access to your human's stuff. That doesn't mean you share it.
45
- In groups, you're a participant — not their voice, not their proxy.
26
+ You're a participant, not the user's proxy. Don't share their private info.
46
27
 
47
- **Respond when:**
48
- - Directly mentioned or asked a question
49
- - You can add genuine value (info, insight, help)
50
- - Something witty/funny fits naturally
51
- - Correcting important misinformation
28
+ **Respond** to direct mentions, genuine questions, chances to add value, or natural humor/corrections.
29
+ **Stay silent (HEARTBEAT_OK)** for casual banter, already-answered questions, "yeah/nice" responses, or when adding a message would interrupt flow.
52
30
 
53
- **Stay silent (HEARTBEAT_OK) when:**
54
- - It's just casual banter between humans
55
- - Someone already answered the question
56
- - Your response would just be "yeah" or "nice"
57
- - The conversation is flowing fine without you
58
- - Adding a message would interrupt the vibe
59
-
60
- **The human rule:** Humans don't respond to every message. Neither should you.
61
- Quality > quantity. Avoid the triple-tap (don't respond multiple times to the same message).
31
+ Humans don't respond to every message — neither should you. Quality > quantity. No triple-taps.
62
32
 
63
33
  ### Reactions
64
34
 
65
- Use emoji reactions naturally — they're lightweight social signals:
66
- - Appreciate something but don't need to reply (thumbs up, heart)
67
- - Something made you laugh (laughing face, skull)
68
- - Acknowledge without interrupting flow (checkmark, eyes)
69
- - One reaction per message max.
70
-
71
- When someone reacts to a message, acknowledge it with a brief response.
72
- Reactions are a form of communication — treat them like a tap on the shoulder.
73
-
74
- Participate, don't dominate.
35
+ Use emoji reactions as lightweight social signals (appreciate, laugh, acknowledge). One per message max. Acknowledge reactions on your messages briefly.
75
36
 
76
37
  ## Memory
77
38
 
78
- Your prompt may include two memory sections injected by the system:
79
-
80
- **Durable memory** — Persistent facts/preferences about the user that survive across sessions.
81
- Treat as ground truth unless explicitly contradicted. Don't repeat them back unprompted.
82
-
83
- **Conversation memory** — Rolling summary of recent conversation. Lossy and compressed.
84
- If it conflicts with recent messages, trust the recent messages.
85
-
86
- Memory commands (handled by the system, not you):
87
- - `!memory` or `!memory show` — see stored items + rolling summary
88
- - `!memory remember <text>` — store a new fact
89
- - `!memory forget <text>` — remove matching items
90
- - `!memory reset rolling` — clear the rolling summary
91
-
92
- See `.context/memory.md` for full architecture, examples, and config reference.
39
+ Your prompt may include:
40
+ - **Durable memory** — Persistent user facts/preferences. Treat as ground truth unless contradicted.
41
+ - **Conversation memory** — Rolling summary, lossy. Trust recent messages over summary if they conflict.
93
42
 
94
43
  ## Autonomy Tiers
95
44
 
96
- ### Always OK (no permission needed)
97
- - Read files, explore, search the web, run diagnostics
98
- - Send Discord messages, react with emoji
99
- - Share finds in relevant channels, report back on async tasks
100
- - Work within the workspace
45
+ **Always OK:** Read files, explore, search web, run diagnostics, send Discord messages, react, share finds, work within workspace.
101
46
 
102
- ### Act Then Notify (time-sensitive)
103
- - For **confirmed, active** security threats: take reversible protective actions, then notify
104
- - For ambiguous threats: alert first, wait for decision
47
+ **Act Then Notify:** Confirmed active security threats — take reversible action, then notify. Ambiguous threats — alert first.
105
48
 
106
- ### Always Ask First
107
- - External communications (emails, messages to others, posting on someone's behalf)
108
- - Changes to the user's creative projects
109
- - System changes (package installs, systemd modifications, firewall/network)
110
- - Destructive actions (deleting files, dropping databases, revoking credentials)
111
- - Anything involving money
49
+ **Always Ask:** External communications, creative project changes, system changes (packages, systemd, network), destructive actions, anything involving money.
112
50
 
113
51
  ## Execution Policy (Local Machine)
114
52
 
@@ -116,13 +54,3 @@ See `.context/memory.md` for full architecture, examples, and config reference.
116
54
  - Ask before: destructive ops, system-wide changes
117
55
  - Never retry sudo. If auth is needed, give the user the command to run.
118
56
 
119
- ## Customization
120
-
121
- Instruction precedence is deterministic:
122
- 1. immutable security policy (`ROOT_POLICY`)
123
- 2. tracked defaults (`templates/instructions/SYSTEM_DEFAULTS.md`)
124
- 3. `workspace/AGENTS.md` overrides
125
- 4. memory/context sections
126
-
127
- Customize behavior in `workspace/AGENTS.md` (user-owned, never overwritten).
128
- Do not rely on `workspace/DISCOCLAW.md`; defaults are sourced from the tracked template and injected at runtime.
@@ -12,15 +12,9 @@ const CANVAS_VOID_ELEMENT_TAG_RE = /<(area|base|br|col|embed|hr|img|input|link|m
12
12
  const __filename = fileURLToPath(import.meta.url);
13
13
  const __dirname = path.dirname(__filename);
14
14
  const CANVAS_PROMPT_TEMPLATE_PATH = path.resolve(__dirname, '..', '..', 'templates', 'instructions', 'canvas.md');
15
- const CANVAS_EXPERIMENTAL_POSTURE_NOTE = [
16
- 'Canvas Activities are experimental and default-off in DiscoClaw.',
17
- 'Fresh installs and upgraded installs both require an explicit opt-in via `DISCOCLAW_CANVAS_ENABLED=1`, followed by a bot restart.',
18
- ].join(' ');
19
- const DEFAULT_SAVE_BRIDGE_GUIDANCE = [
20
- '- Save-file export is available through the trusted shell bridge when enabled.',
21
- '- To export a file from inside the artifact, post a message to the parent shell:',
22
- ' `window.parent.postMessage({ type: "canvas.saveFile", suggestedName: "report.md", mimeType: "text/markdown", encoding: "utf8", content: "# Report" }, "*")`',
23
- ].join('\n');
15
+ const CANVAS_SETUP_POSTURE_NOTE = 'Canvas Activities are experimental and default-off in DiscoClaw.' +
16
+ ' Fresh installs and upgraded installs both require an explicit opt-in via `DISCOCLAW_CANVAS_ENABLED=1`, followed by a bot restart.';
17
+ const DEFAULT_SAVE_BRIDGE_GUIDANCE = '- Save-file export: `window.parent.postMessage({ type: "canvas.saveFile", suggestedName: "report.md", mimeType: "text/markdown", encoding: "utf8", content: "..." }, "*")`';
24
18
  let cachedCanvasPromptTemplate = null;
25
19
  export const CANVAS_ACTION_TYPES = new Set(['launchCanvas']);
26
20
  export const CANVAS_LAUNCH_COMPONENT_PREFIX = 'canvas:launch:';
@@ -57,7 +51,7 @@ export function buildCanvasSetupRequiredStub(canvasCtx) {
57
51
  'Enable Activities on the Discord application in the Developer Portal (Application → Activities → Enable). Requires the URL Mapping first.',
58
52
  ],
59
53
  };
60
- return [CANVAS_EXPERIMENTAL_POSTURE_NOTE, buildCanvasSetupWalkthrough(readiness)].join(' ');
54
+ return [CANVAS_SETUP_POSTURE_NOTE, buildCanvasSetupWalkthrough(readiness)].join(' ');
61
55
  }
62
56
  export function shouldCanvasPromptBeSurfaced(canvasCtx, userText) {
63
57
  if (!canvasCtx?.enabled)
@@ -281,49 +275,7 @@ function findCanvasArtifactLintError(content) {
281
275
  function loadCanvasPromptTemplate() {
282
276
  if (cachedCanvasPromptTemplate != null)
283
277
  return cachedCanvasPromptTemplate;
284
- try {
285
- cachedCanvasPromptTemplate = fs.readFileSync(CANVAS_PROMPT_TEMPLATE_PATH, 'utf8').trim();
286
- }
287
- catch {
288
- cachedCanvasPromptTemplate = [
289
- '### Canvas Activities',
290
- '',
291
- '**launchCanvas** — Generate and serve an interactive HTML artifact or built-in app in a Discord Activity panel:',
292
- '```',
293
- '<discord-action>{"type":"launchCanvas","title":"Tax Calculator","content":"<!doctype html><html><head><meta charset=\\"utf-8\\" /><meta name=\\"viewport\\" content=\\"width=device-width,initial-scale=1\\" /><style>body{font-family:sans-serif;padding:16px}label,input{display:block;margin-top:12px}</style></head><body><div id=\\"app\\"></div><script>const { html, render, useState } = window.canvasRuntime;function TaxCalculator(){const [income,setIncome]=useState(50000);const tax=Math.round(income*0.22);return html`<main><h1>Tax Calculator</h1><label>Income <input type=\\"number\\" value=${income} onInput=${(event)=>setIncome(Number(event.currentTarget.value||0))} /></label><p>Estimated tax: $${tax.toLocaleString()}</p></main>`;}render(TaxCalculator, document.getElementById(\\"app\\"));</script></body></html>"}</discord-action>',
294
- '<discord-action>{"type":"launchCanvas","title":"Dashboard","app":"dashboard"}</discord-action>',
295
- '```',
296
- '- `title` (required): Human-readable label for the launch button.',
297
- '- `content` (artifact mode): Full self-contained HTML document with all CSS and JS inline.',
298
- '- `app` (built-in mode): Named built-in Activity app such as `dashboard`.',
299
- '- Use canvas only when interactivity materially improves the result over plain text.',
300
- '- Default is plain text. Do not use canvas for short answers, conversational replies, or single values.',
301
- '- Good fits: calculators, forms, charts, diffs, large comparison views, filterable tables, live dashboard launches.',
302
- '- Bad fits: simple status updates, brief explanations, or anything the user explicitly wants as plain text.',
303
- '- Artifact render responses inject `window.canvasRuntime`; use that built-in runtime instead of bundling React, Preact, Vue, or another UI framework.',
304
- '- The injected runtime exposes `html`, `render`, and the installed `preact/hooks` surface: `useState`, `useEffect`, `useLayoutEffect`, `useReducer`, `useRef`, `useMemo`, `useCallback`, `useContext`, `useImperativeHandle`, `useDebugValue`, `useErrorBoundary`, and `useId`.',
305
- '- Start interactive artifacts with `const { html, render, useState } = window.canvasRuntime`; keep the starter small unless the artifact actually needs more hook surface, and mount into a dedicated root node.',
306
- '- In `html` template literals, self-close void HTML elements: use `<input ... />`, `<img ... />`, `<br />`, etc. Bare `<input>` tags can corrupt the rendered DOM in canvas artifacts.',
307
- '- Generated artifacts must be a single HTML file, responsive at phone width, and keep total size under roughly 500KB.',
308
- '- No external scripts, stylesheets, fonts, images, or nested iframes in generated artifacts.',
309
- '- Generated artifacts run inside a sandboxed iframe and cannot call backend routes directly.',
310
- '- Artifacts are stored under a cap-based LRU policy; they are not time-expired in v1.',
311
- '- Include visible loading/error/fallback states when the UI depends on JavaScript.',
312
- '- Default visual direction: feel at home in Discord without mimicking Discord\'s UI chrome.',
313
- '- Favor Discord-adjacent contrast and restraint for dark surroundings, but choose colors and themes based on the content instead of cloning Discord\'s palette by default.',
314
- '- Use explicitly Discord-like styling only when the user asks for a native/control-panel/admin-tool feel.',
315
- '- Prefer semantic HTML, clear contrast, and obvious focus states.',
316
- '{{CANVAS_SAVE_BRIDGE_GUIDANCE}}',
317
- ].join('\n');
318
- }
319
- if (!cachedCanvasPromptTemplate.includes(CANVAS_EXPERIMENTAL_POSTURE_NOTE)) {
320
- if (cachedCanvasPromptTemplate.startsWith('### Canvas Activities')) {
321
- cachedCanvasPromptTemplate = cachedCanvasPromptTemplate.replace('### Canvas Activities', `### Canvas Activities\n\n${CANVAS_EXPERIMENTAL_POSTURE_NOTE}`);
322
- }
323
- else {
324
- cachedCanvasPromptTemplate = `${CANVAS_EXPERIMENTAL_POSTURE_NOTE}\n\n${cachedCanvasPromptTemplate}`;
325
- }
326
- }
278
+ cachedCanvasPromptTemplate = fs.readFileSync(CANVAS_PROMPT_TEMPLATE_PATH, 'utf8').trim();
327
279
  return cachedCanvasPromptTemplate;
328
280
  }
329
281
  function buildLaunchActivityFailureMessage(err) {
@@ -1,5 +1,4 @@
1
1
  import { afterEach, describe, expect, it, vi } from 'vitest';
2
- import fsSync from 'node:fs';
3
2
  import fs from 'node:fs/promises';
4
3
  import os from 'node:os';
5
4
  import path from 'node:path';
@@ -81,13 +80,11 @@ async function makeCanvasContext() {
81
80
  return canvasCtx;
82
81
  }
83
82
  describe('canvas-action', () => {
84
- it('surfaces the injected canvas runtime contract from the checked-in prompt template', async () => {
83
+ it('surfaces the canvas runtime contract from the checked-in prompt template', async () => {
85
84
  vi.resetModules();
86
85
  const { canvasActionsPromptSection } = await import('./canvas-action.js');
87
86
  const prompt = canvasActionsPromptSection({ writeBridgeEnabled: true });
88
87
  expect(prompt).toContain('experimental and default-off');
89
- expect(prompt).toContain('DISCOCLAW_CANVAS_ENABLED=1');
90
- expect(prompt).toContain('Fresh installs and upgraded installs both require an explicit opt-in');
91
88
  expect(prompt).toContain('window.canvasRuntime');
92
89
  expect(prompt).toContain('installed `preact/hooks` surface');
93
90
  expect(prompt).toContain('`useLayoutEffect`');
@@ -96,36 +93,27 @@ describe('canvas-action', () => {
96
93
  expect(prompt).toContain('Bare `<input>` tags can corrupt the rendered DOM');
97
94
  expect(prompt).toContain('feel at home in Discord without mimicking Discord');
98
95
  expect(prompt).toContain('instead of cloning Discord\'s palette by default');
96
+ expect(prompt).toContain('canvas.saveFile');
97
+ expect(prompt).not.toContain('{{CANVAS_SAVE_BRIDGE_GUIDANCE}}');
99
98
  });
100
- it('documents the injected canvas runtime in fallback prompt text and preserves save-bridge substitution', async () => {
99
+ it('omits save-bridge guidance when writeBridgeEnabled is false', async () => {
101
100
  vi.resetModules();
102
- vi.spyOn(fsSync, 'readFileSync').mockImplementation(() => {
103
- throw new Error('template unavailable');
104
- });
105
101
  const { canvasActionsPromptSection } = await import('./canvas-action.js');
106
- const withSaveBridge = canvasActionsPromptSection({ writeBridgeEnabled: true });
107
- expect(withSaveBridge).toContain('experimental and default-off');
108
- expect(withSaveBridge).toContain('DISCOCLAW_CANVAS_ENABLED=1');
109
- expect(withSaveBridge).toContain('Fresh installs and upgraded installs both require an explicit opt-in');
110
- expect(withSaveBridge).toContain('window.canvasRuntime');
111
- expect(withSaveBridge).toContain('installed `preact/hooks` surface');
112
- expect(withSaveBridge).toContain('`useErrorBoundary`');
113
- expect(withSaveBridge).toContain('const { html, render, useState } = window.canvasRuntime');
114
- expect(withSaveBridge).toContain('self-close void HTML elements');
115
- expect(withSaveBridge).toContain('feel at home in Discord without mimicking Discord');
116
- expect(withSaveBridge).toContain('canvas.saveFile');
117
- expect(withSaveBridge).not.toContain('{{CANVAS_SAVE_BRIDGE_GUIDANCE}}');
118
102
  const withoutSaveBridge = canvasActionsPromptSection({ writeBridgeEnabled: false });
119
103
  expect(withoutSaveBridge).toContain('experimental and default-off');
120
- expect(withoutSaveBridge).toContain('DISCOCLAW_CANVAS_ENABLED=1');
121
104
  expect(withoutSaveBridge).toContain('window.canvasRuntime');
122
105
  expect(withoutSaveBridge).toContain('installed `preact/hooks` surface');
123
- expect(withoutSaveBridge).toContain('`useId`');
124
106
  expect(withoutSaveBridge).toContain('Bare `<input>` tags can corrupt the rendered DOM');
125
- expect(withoutSaveBridge).toContain('instead of cloning Discord\'s palette by default');
126
107
  expect(withoutSaveBridge).not.toContain('canvas.saveFile');
127
108
  expect(withoutSaveBridge).not.toContain('{{CANVAS_SAVE_BRIDGE_GUIDANCE}}');
128
109
  });
110
+ it('does not inject the setup posture note into the prompt template', async () => {
111
+ vi.resetModules();
112
+ const { canvasActionsPromptSection } = await import('./canvas-action.js');
113
+ const prompt = canvasActionsPromptSection({ writeBridgeEnabled: true });
114
+ expect(prompt).not.toContain('DISCOCLAW_CANVAS_ENABLED=1');
115
+ expect(prompt).not.toContain('Fresh installs and upgraded installs both require an explicit opt-in');
116
+ });
129
117
  it('describes canvas setup as experimental and explicit opt-in', async () => {
130
118
  vi.resetModules();
131
119
  const { buildCanvasSetupRequiredStub } = await import('./canvas-action.js');
@@ -2,7 +2,7 @@ import { EmbedBuilder } from 'discord.js';
2
2
  import { CADENCE_TAGS, computeDefinitionHash } from './run-stats.js';
3
3
  import { detectCadence } from './cadence.js';
4
4
  import { autoTagCron, classifyCronModel } from './auto-tag.js';
5
- import { buildCronThreadName, ensureStatusMessage, resolveForumChannel } from './discord-sync.js';
5
+ import { buildCronThreadName, ensureStatusMessage, resolveForumChannel, tryUnpinMessage } from './discord-sync.js';
6
6
  // ---------------------------------------------------------------------------
7
7
  // Helpers
8
8
  // ---------------------------------------------------------------------------
@@ -358,6 +358,53 @@ export async function runCronSync(opts) {
358
358
  catch {
359
359
  // Thread may not exist; status message update is best-effort.
360
360
  }
361
+ // Refresh prompt message for drifted projections — unpin stale before pinning replacement.
362
+ if (liveRecord.prompt && liveRecord.promptMessageId) {
363
+ try {
364
+ let thread = null;
365
+ const cached = client.channels.cache.get(liveRecord.threadId);
366
+ if (cached && cached.isThread()) {
367
+ thread = cached;
368
+ }
369
+ else {
370
+ try {
371
+ const fetched = await client.channels.fetch(liveRecord.threadId);
372
+ if (fetched && fetched.isThread())
373
+ thread = fetched;
374
+ }
375
+ catch { /* thread may not exist */ }
376
+ }
377
+ if (thread) {
378
+ const embed = new EmbedBuilder()
379
+ .setTitle('\uD83D\uDCCB Cron Prompt')
380
+ .setDescription(buildPromptMessageDescription(liveRecord))
381
+ .setColor(0x5865F2);
382
+ let edited = false;
383
+ try {
384
+ const existing = await thread.messages.fetch(liveRecord.promptMessageId);
385
+ if (existing) {
386
+ await existing.edit({ embeds: [embed], allowedMentions: { parse: [] } });
387
+ edited = true;
388
+ }
389
+ }
390
+ catch {
391
+ // Edit failed — unpin old message before creating replacement.
392
+ await tryUnpinMessage(thread, liveRecord.promptMessageId, log);
393
+ }
394
+ if (!edited) {
395
+ const msg = await thread.send({ embeds: [embed], allowedMentions: { parse: [] } });
396
+ try {
397
+ await msg.pin();
398
+ }
399
+ catch { /* non-fatal */ }
400
+ await statsStore.upsertRecord(cronId, liveRecord.threadId, { promptMessageId: msg.id });
401
+ }
402
+ }
403
+ }
404
+ catch (err) {
405
+ log?.warn({ err, cronId }, 'cron-sync:phase5 prompt message refresh failed');
406
+ }
407
+ }
361
408
  await statsStore.upsertRecord(cronId, liveRecord.threadId, {
362
409
  projectionStatus: 'synced',
363
410
  projectionSyncedAt: new Date().toISOString(),
@@ -142,6 +142,20 @@ async function fetchThreadChannel(client, threadId) {
142
142
  return null;
143
143
  }
144
144
  }
145
+ /**
146
+ * Best-effort unpin of a message by ID within a thread.
147
+ * Silently swallows errors (message already deleted, missing permissions, etc.).
148
+ */
149
+ export async function tryUnpinMessage(thread, messageId, log) {
150
+ try {
151
+ const msg = await thread.messages.fetch(messageId);
152
+ if (msg?.pinned)
153
+ await msg.unpin();
154
+ }
155
+ catch {
156
+ // Message gone or unpin failed — non-fatal.
157
+ }
158
+ }
145
159
  export async function ensureStatusMessage(client, threadId, cronId, record, stats, opts) {
146
160
  const { log, running } = opts ?? {};
147
161
  const thread = await fetchThreadChannel(client, threadId);
@@ -155,8 +169,20 @@ export async function ensureStatusMessage(client, threadId, cronId, record, stat
155
169
  try {
156
170
  const msg = await thread.messages.fetch(record.statusMessageId);
157
171
  if (msg) {
158
- await msg.edit({ content, allowedMentions: { parse: [] } });
159
- return record.statusMessageId;
172
+ try {
173
+ await msg.edit({ content, allowedMentions: { parse: [] } });
174
+ return record.statusMessageId;
175
+ }
176
+ catch {
177
+ // Edit failed but message exists — unpin before replacing.
178
+ try {
179
+ if (msg.pinned)
180
+ await msg.unpin();
181
+ }
182
+ catch {
183
+ // Non-fatal if unpin fails.
184
+ }
185
+ }
160
186
  }
161
187
  }
162
188
  catch {
@@ -88,6 +88,8 @@ export class CronRunStats {
88
88
  threadIndex = new Map();
89
89
  // Secondary index: statusMessageId → cronId for O(1) recovery lookups.
90
90
  statusMessageIndex = new Map();
91
+ // Secondary index: promptMessageId → cronId for O(1) lookups.
92
+ promptMessageIndex = new Map();
91
93
  // Secondary index: webhookSourceId → cronId for O(1) webhook routing.
92
94
  sourceIndex = new Map();
93
95
  constructor(store, filePath) {
@@ -98,12 +100,16 @@ export class CronRunStats {
98
100
  rebuildThreadIndex() {
99
101
  this.threadIndex.clear();
100
102
  this.statusMessageIndex.clear();
103
+ this.promptMessageIndex.clear();
101
104
  this.sourceIndex.clear();
102
105
  for (const rec of Object.values(this.store.jobs)) {
103
106
  this.threadIndex.set(rec.threadId, rec.cronId);
104
107
  if (rec.statusMessageId) {
105
108
  this.statusMessageIndex.set(rec.statusMessageId, rec.cronId);
106
109
  }
110
+ if (rec.promptMessageId) {
111
+ this.promptMessageIndex.set(rec.promptMessageId, rec.cronId);
112
+ }
107
113
  if (rec.webhookSourceId) {
108
114
  this.sourceIndex.set(rec.webhookSourceId, rec.cronId);
109
115
  }
@@ -123,6 +129,10 @@ export class CronRunStats {
123
129
  const cronId = this.statusMessageIndex.get(statusMessageId);
124
130
  return cronId ? this.store.jobs[cronId] : undefined;
125
131
  }
132
+ getRecordByPromptMessageId(promptMessageId) {
133
+ const cronId = this.promptMessageIndex.get(promptMessageId);
134
+ return cronId ? this.store.jobs[cronId] : undefined;
135
+ }
126
136
  getRecordBySourceId(sourceId) {
127
137
  const cronId = this.sourceIndex.get(sourceId);
128
138
  return cronId ? this.store.jobs[cronId] : undefined;
@@ -141,6 +151,7 @@ export class CronRunStats {
141
151
  const existing = this.store.jobs[cronId];
142
152
  if (existing) {
143
153
  const prevStatusMessageId = existing.statusMessageId;
154
+ const prevPromptMessageId = existing.promptMessageId;
144
155
  const prevSourceId = existing.webhookSourceId;
145
156
  // If threadId changed, remove old index entry.
146
157
  if (existing.threadId !== threadId) {
@@ -166,6 +177,9 @@ export class CronRunStats {
166
177
  if (prevStatusMessageId && prevStatusMessageId !== existing.statusMessageId) {
167
178
  this.statusMessageIndex.delete(prevStatusMessageId);
168
179
  }
180
+ if (prevPromptMessageId && prevPromptMessageId !== existing.promptMessageId) {
181
+ this.promptMessageIndex.delete(prevPromptMessageId);
182
+ }
169
183
  if (prevSourceId && prevSourceId !== existing.webhookSourceId) {
170
184
  this.sourceIndex.delete(prevSourceId);
171
185
  }
@@ -179,6 +193,9 @@ export class CronRunStats {
179
193
  if (record.statusMessageId) {
180
194
  this.statusMessageIndex.set(record.statusMessageId, cronId);
181
195
  }
196
+ if (record.promptMessageId) {
197
+ this.promptMessageIndex.set(record.promptMessageId, cronId);
198
+ }
182
199
  if (record.webhookSourceId) {
183
200
  this.sourceIndex.set(record.webhookSourceId, cronId);
184
201
  }
@@ -240,6 +257,8 @@ export class CronRunStats {
240
257
  this.threadIndex.delete(rec.threadId);
241
258
  if (rec.statusMessageId)
242
259
  this.statusMessageIndex.delete(rec.statusMessageId);
260
+ if (rec.promptMessageId)
261
+ this.promptMessageIndex.delete(rec.promptMessageId);
243
262
  if (rec.webhookSourceId)
244
263
  this.sourceIndex.delete(rec.webhookSourceId);
245
264
  delete this.store.jobs[cronId];
@@ -258,6 +277,8 @@ export class CronRunStats {
258
277
  this.threadIndex.delete(threadId);
259
278
  if (rec.statusMessageId)
260
279
  this.statusMessageIndex.delete(rec.statusMessageId);
280
+ if (rec.promptMessageId)
281
+ this.promptMessageIndex.delete(rec.promptMessageId);
261
282
  if (rec.webhookSourceId)
262
283
  this.sourceIndex.delete(rec.webhookSourceId);
263
284
  delete this.store.jobs[cronId];
@@ -8,6 +8,56 @@ export const TRACKED_DEFAULTS_FILE_NAME = 'SYSTEM_DEFAULTS.md';
8
8
  export const TRACKED_DEFAULTS_SECTION_LABEL = 'SYSTEM_DEFAULTS.md (tracked defaults)';
9
9
  let cachedPath = null;
10
10
  let cachedPreamble = null;
11
+ /**
12
+ * Sections dropped from the tracked defaults because they are
13
+ * duplicated, unreachable after bootstrap, or rarely needed.
14
+ */
15
+ const DROPPED_DEFAULTS_SECTIONS = new Set([
16
+ 'Runtime Instruction Precedence',
17
+ 'First Run',
18
+ 'Runtime Registry',
19
+ 'Bot Setup Assistance',
20
+ 'Knowledge Cutoff Awareness',
21
+ ]);
22
+ function splitTopLevelSections(content) {
23
+ const prelude = [];
24
+ const sections = [];
25
+ let current = null;
26
+ for (const line of content.trimEnd().split('\n')) {
27
+ if (line.startsWith('## ')) {
28
+ current = { heading: line.slice(3).trim(), lines: [line] };
29
+ sections.push(current);
30
+ continue;
31
+ }
32
+ if (current) {
33
+ current.lines.push(line);
34
+ }
35
+ else {
36
+ prelude.push(line);
37
+ }
38
+ }
39
+ return { prelude, sections };
40
+ }
41
+ function joinDefaultsContent(parts) {
42
+ return parts
43
+ .map((part) => part.trimEnd())
44
+ .filter((part) => part.length > 0)
45
+ .join('\n\n')
46
+ .trimEnd();
47
+ }
48
+ /**
49
+ * Filter tracked defaults content by dropping rarely-needed sections
50
+ * and stripping meta-descriptive blockquotes from the prelude.
51
+ */
52
+ export function buildPromptSafeDefaultsContent(content) {
53
+ const { prelude, sections } = splitTopLevelSections(content);
54
+ const filteredPrelude = prelude.filter((line) => !line.startsWith('> '));
55
+ const filteredSections = sections.filter((section) => !DROPPED_DEFAULTS_SECTIONS.has(section.heading));
56
+ return joinDefaultsContent([
57
+ filteredPrelude.join('\n'),
58
+ ...filteredSections.map((section) => section.lines.join('\n')),
59
+ ]);
60
+ }
11
61
  /**
12
62
  * Resolve the tracked system-default file path from this module's location.
13
63
  * Works in both src/* and dist/* layouts.
@@ -36,7 +86,7 @@ export function loadTrackedDefaultsPreamble(opts) {
36
86
  let defaultsPreamble = '';
37
87
  try {
38
88
  const content = fsSync.readFileSync(trackedDefaultsPath, 'utf-8');
39
- defaultsPreamble = renderTrackedDefaultsSection(content);
89
+ defaultsPreamble = renderTrackedDefaultsSection(buildPromptSafeDefaultsContent(content));
40
90
  }
41
91
  catch (err) {
42
92
  const message = err instanceof Error ? err.message : String(err);
@@ -2,7 +2,7 @@ import fs from 'node:fs/promises';
2
2
  import os from 'node:os';
3
3
  import path from 'node:path';
4
4
  import { afterEach, describe, expect, it, vi } from 'vitest';
5
- import { TRACKED_DEFAULTS_DIR, TRACKED_DEFAULTS_FILE_NAME, TRACKED_DEFAULTS_SECTION_LABEL, _resetTrackedDefaultsCacheForTests, loadTrackedDefaultsPreamble, renderTrackedDefaultsSection, resolveTrackedDefaultsPath, } from './system-defaults.js';
5
+ import { TRACKED_DEFAULTS_DIR, TRACKED_DEFAULTS_FILE_NAME, TRACKED_DEFAULTS_SECTION_LABEL, _resetTrackedDefaultsCacheForTests, buildPromptSafeDefaultsContent, loadTrackedDefaultsPreamble, renderTrackedDefaultsSection, resolveTrackedDefaultsPath, } from './system-defaults.js';
6
6
  describe('resolveTrackedDefaultsPath', () => {
7
7
  it('resolves to templates/instructions/SYSTEM_DEFAULTS.md by default', async () => {
8
8
  const resolved = resolveTrackedDefaultsPath();
@@ -28,6 +28,31 @@ describe('renderTrackedDefaultsSection', () => {
28
28
  expect(renderTrackedDefaultsSection('\n \n')).toBe('');
29
29
  });
30
30
  });
31
+ describe('buildPromptSafeDefaultsContent', () => {
32
+ it('drops rarely-needed sections from the tracked defaults template', async () => {
33
+ const content = await fs.readFile(resolveTrackedDefaultsPath(), 'utf-8');
34
+ const sanitized = buildPromptSafeDefaultsContent(content);
35
+ // Dropped sections
36
+ expect(sanitized).not.toContain('## Runtime Instruction Precedence');
37
+ expect(sanitized).not.toContain('## First Run');
38
+ expect(sanitized).not.toContain('## Runtime Registry');
39
+ expect(sanitized).not.toContain('## Bot Setup Assistance');
40
+ expect(sanitized).not.toContain('## Knowledge Cutoff Awareness');
41
+ // Sections that should survive filtering
42
+ expect(sanitized).toContain('## Search Before Asking');
43
+ expect(sanitized).toContain('## Tool Use First');
44
+ expect(sanitized).toContain('## Discord Action Grounding');
45
+ expect(sanitized).toContain('## Response Economy');
46
+ expect(sanitized).toContain('## Landing the Plane');
47
+ });
48
+ it('strips blockquote lines from the prelude', () => {
49
+ const content = '# Title\n\n> Meta description\n> Second line\n\n## Kept\nContent';
50
+ const sanitized = buildPromptSafeDefaultsContent(content);
51
+ expect(sanitized).not.toContain('> Meta description');
52
+ expect(sanitized).toContain('# Title');
53
+ expect(sanitized).toContain('## Kept');
54
+ });
55
+ });
31
56
  describe('loadTrackedDefaultsPreamble', () => {
32
57
  const dirs = [];
33
58
  afterEach(async () => {
@@ -74,6 +99,18 @@ describe('loadTrackedDefaultsPreamble', () => {
74
99
  expect(reloaded).toContain('second version');
75
100
  expect(reloaded).not.toBe(first);
76
101
  });
102
+ it('sanitizes the default tracked template before rendering it into the preamble', () => {
103
+ const preamble = loadTrackedDefaultsPreamble({
104
+ trackedDefaultsPath: resolveTrackedDefaultsPath(),
105
+ forceReload: true,
106
+ });
107
+ expect(preamble).toContain(`--- ${TRACKED_DEFAULTS_SECTION_LABEL} ---`);
108
+ expect(preamble).not.toContain('## Runtime Instruction Precedence');
109
+ expect(preamble).not.toContain('## Runtime Registry');
110
+ expect(preamble).not.toContain('## Bot Setup Assistance');
111
+ expect(preamble).toContain('## Search Before Asking');
112
+ expect(preamble).toContain('## Tool Use First');
113
+ });
77
114
  it('invalidates cache when the tracked defaults path changes', async () => {
78
115
  const dir = await fs.mkdtemp(path.join(os.tmpdir(), 'tracked-defaults-'));
79
116
  dirs.push(dir);
@@ -13,6 +13,9 @@ let cachedContent = null;
13
13
  const DROPPED_TOP_LEVEL_SECTIONS = new Set([
14
14
  'Browser Automation (agent-browser)',
15
15
  'Service Operations (discoclaw)',
16
+ 'Runtime Instruction Precedence',
17
+ 'Webhook Server',
18
+ 'Plan-Audit-Implement Workflow',
16
19
  ]);
17
20
  const WEBHOOK_TOOL_ACCESS_SENTENCE = 'webhook jobs run without Discord action permissions or tool access.';
18
21
  const AUDITED_RUNTIME_TOOL_GUARANTEES_HEADING = 'Audited Runtime Tool Guarantees';
@@ -125,8 +128,9 @@ export function buildPromptSafeTrackedToolsContent(content, ctx) {
125
128
  filteredSections.splice(precedenceIndex + 1, 0, auditedGuarantees);
126
129
  }
127
130
  }
131
+ const filteredPrelude = prelude.filter((line) => !line.startsWith('> '));
128
132
  return joinTrackedToolsContent([
129
- prelude.join('\n'),
133
+ filteredPrelude.join('\n'),
130
134
  ...filteredSections.map((section) => section.lines.join('\n')),
131
135
  ]);
132
136
  }
@@ -34,12 +34,14 @@ describe('buildPromptSafeTrackedToolsContent', () => {
34
34
  it('drops unsupported static tool-restriction sections from the tracked template', async () => {
35
35
  const trackedToolsContent = await fs.readFile(resolveTrackedToolsPath(), 'utf-8');
36
36
  const sanitized = buildPromptSafeTrackedToolsContent(trackedToolsContent);
37
- expect(sanitized).toContain('## Runtime Instruction Precedence');
37
+ expect(sanitized).not.toContain('## Runtime Instruction Precedence');
38
38
  expect(sanitized).not.toContain('## Browser Automation (agent-browser)');
39
39
  expect(sanitized).not.toContain('## Service Operations (discoclaw)');
40
- expect(sanitized).toContain('## Webhook Server');
41
- expect(sanitized).toContain('Dispatches through the same cron execution pipeline as automations.');
42
- expect(sanitized).not.toContain('webhook jobs run without Discord action permissions or tool access');
40
+ expect(sanitized).not.toContain('## Webhook Server');
41
+ expect(sanitized).not.toContain('## Plan-Audit-Implement Workflow');
42
+ // Sections that should survive filtering
43
+ expect(sanitized).toContain('## Task Management');
44
+ expect(sanitized).toContain('## Discord Action Types');
43
45
  });
44
46
  });
45
47
  const COVERED_TRACKED_TOOL_RUNTIME_CONFIGS = [
@@ -155,6 +157,8 @@ describe('loadTrackedToolsPreamble', () => {
155
157
  expect(preamble).toContain(`--- ${TRACKED_TOOLS_SECTION_LABEL} ---`);
156
158
  expect(preamble).not.toContain('## Browser Automation (agent-browser)');
157
159
  expect(preamble).not.toContain('## Service Operations (discoclaw)');
160
+ expect(preamble).not.toContain('## Webhook Server');
161
+ expect(preamble).not.toContain('## Plan-Audit-Implement Workflow');
158
162
  expect(preamble).not.toContain('webhook jobs run without Discord action permissions or tool access');
159
163
  });
160
164
  it('re-renders cached tracked tools content for runtime-specific audited guarantees', () => {
@@ -125,6 +125,9 @@ export function createGeminiRestRuntime(opts) {
125
125
  inputTokens: usage.promptTokenCount,
126
126
  outputTokens: usage.candidatesTokenCount,
127
127
  totalTokens: usage.totalTokenCount,
128
+ ...(usage.cachedContentTokenCount != null
129
+ ? { cachedInputTokens: usage.cachedContentTokenCount, cacheSupported: true }
130
+ : {}),
128
131
  };
129
132
  }
130
133
  }
@@ -117,6 +117,27 @@ describe('Gemini REST runtime adapter', () => {
117
117
  expect(usage.inputTokens).toBe(10);
118
118
  expect(usage.outputTokens).toBe(5);
119
119
  });
120
+ it('emits cachedInputTokens when usageMetadata includes cachedContentTokenCount', async () => {
121
+ globalThis.fetch = vi.fn().mockResolvedValue(makeSSEResponse([
122
+ makeGeminiSSEDataWithUsage('ok', {
123
+ promptTokenCount: 100,
124
+ candidatesTokenCount: 20,
125
+ totalTokenCount: 120,
126
+ cachedContentTokenCount: 80,
127
+ }),
128
+ ]));
129
+ const runtime = createGeminiRestRuntime({
130
+ apiKey: 'test-key',
131
+ defaultModel: 'gemini-2.5-flash',
132
+ });
133
+ const events = await collectEvents(runtime.invoke({ prompt: 'test', model: '', cwd: '/tmp' }));
134
+ const usage = events.find((e) => e.type === 'usage');
135
+ expect(usage).toBeDefined();
136
+ expect(usage.inputTokens).toBe(100);
137
+ expect(usage.outputTokens).toBe(20);
138
+ expect(usage.cachedInputTokens).toBe(80);
139
+ expect(usage.cacheSupported).toBe(true);
140
+ });
120
141
  it('emits error event on non-200 response', async () => {
121
142
  globalThis.fetch = vi.fn().mockResolvedValue(new Response(JSON.stringify({ error: { message: 'quota exceeded' } }), {
122
143
  status: 429,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "discoclaw",
3
- "version": "1.1.0",
3
+ "version": "1.1.2",
4
4
  "description": "Personal AI orchestrator that turns Discord into a persistent workspace",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -1,63 +1,28 @@
1
1
  # SYSTEM_DEFAULTS.md - Tracked Default Instructions
2
2
 
3
- > This file is the canonical tracked default instruction source used by DiscoClaw.
4
- > It is injected at runtime and is not a workspace-managed file.
5
- > User-specific overrides belong in `workspace/AGENTS.md`.
6
- > Tracked tool instructions are injected separately from `templates/instructions/TOOLS.md`.
7
- > User-specific tool overrides belong in `workspace/TOOLS.md`.
3
+ > Canonical tracked default instruction source, injected at runtime and not a workspace-managed file.
4
+ > User overrides: `workspace/AGENTS.md`. Tool instructions: `templates/instructions/TOOLS.md` (overrides: `workspace/TOOLS.md`).
8
5
 
9
6
  ## Runtime Instruction Precedence
10
7
 
11
- Discoclaw builds prompts with deterministic precedence:
12
-
13
- 1. **Immutable security policy** (hard-coded runtime rules)
14
- 2. **Tracked defaults** (this file)
15
- 3. **Tracked tools** (`templates/instructions/TOOLS.md`)
16
- 4. **`workspace/AGENTS.md`** (user overrides)
17
- 5. **`workspace/TOOLS.md`** (optional user override layer)
18
- 6. **Memory and other runtime context** (SOUL/IDENTITY/USER, channel context, memory layers, etc.)
8
+ 1. Immutable security policy 2. Tracked defaults (this) → 3. Tracked tools (`templates/instructions/TOOLS.md`) → 4. `workspace/AGENTS.md` → 5. `workspace/TOOLS.md` → 6. Memory/runtime context
19
9
 
20
10
  ## First Run
21
11
 
22
- If `BOOTSTRAP.md` exists, that's your birth certificate. Follow it, figure out who you are, then delete it. You won't need it again.
23
-
24
- ## Every Session
25
-
26
- Before doing anything else:
27
-
28
- 1. Read `SOUL.md` — this is who you are
29
- 2. Read `USER.md` — this is who you're helping
30
- 3. Read `IDENTITY.md` — this is your name and vibe
31
-
32
- Load them immediately — just do it. These files are loaded into your prompt automatically by Discoclaw, but read them to internalize who you are.
12
+ If `BOOTSTRAP.md` exists, follow it, then delete it.
33
13
 
34
14
  ## Search Before Asking
35
15
 
36
- Before telling the user you don't have enough information to answer, work the chain:
37
-
38
- 1. **Workspace files** — Read relevant files in the workspace directory (MEMORY.md, any context files). The answer is often already there.
39
- 2. **Durable memory** — It's injected into your prompt. Re-read it. The user may have told you this before.
40
- 3. **Discord history** — Use `readMessages` on the relevant channel. Recent conversation may contain the answer.
41
- 4. **Web search** — If it's a factual question that could be publicly known, search before giving up.
42
-
43
- Only ask the user after you've genuinely exhausted these options. Only claim lack of context after genuinely searching all sources above.
16
+ Before claiming insufficient info: check workspace files durable memory Discord history (`readMessages`) → web search. Only ask after genuinely exhausting these.
44
17
 
45
18
  ## Tool Use First
46
19
 
47
- When asked to investigate, fix, debug, or build something in the codebase:
48
- - **Use tools immediately.** Start with Read/Bash/Grep — don't narrate what you plan to do. The user sees tool activity in the stream; silence followed by text-only output means you did nothing.
49
- - **Discord actions are not code work.** Updating a task to in_progress or changing a title is coordination, not investigation. Do the actual file reads and analysis first, then report.
50
- - **Don't defer what you can do now.** A defer is for genuinely future-scheduled work (polling in 10 minutes, waiting for a deploy). If you have tool access and the code is reachable, use tools in this turn. Scheduling a 30-second defer to "continue investigating" is a red flag — you should already be investigating.
51
- - **Navigate first, then work.** CWD is the workspace dir. The code lives in the discoclaw repo. `cd` there or use absolute paths. Don't stall on directory context.
20
+ Use tools immediately Read/Bash/Grep don't narrate plans. CWD is the workspace dir; code lives in `~/code/discoclaw`. Don't defer what you can do now. Task status updates are coordination, not investigation.
52
21
 
53
22
  ## Runtime Registry
54
23
 
55
- Discoclaw has **6 registered runtimes**. Use the exact registry key when referencing runtimes — aliases are accepted in config but the canonical key is authoritative.
56
-
57
- **Naming convention:** `-cli` suffix = local subprocess, `-api` or plain name = HTTP API.
58
-
59
- | Registry Key | Type | Backend |
60
- |-------------|------|---------|
24
+ | Key | Type | Backend |
25
+ |-----|------|---------|
61
26
  | `claude-cli` | CLI | Claude Code subprocess |
62
27
  | `claude-api` | API | Anthropic Messages API |
63
28
  | `codex-cli` | CLI | Codex CLI subprocess |
@@ -65,101 +30,30 @@ Discoclaw has **6 registered runtimes**. Use the exact registry key when referen
65
30
  | `openrouter` | API | OpenRouter API |
66
31
  | `gemini-api` | API | Google Gemini API |
67
32
 
68
- **Removed:** `gemini` (was a redundant alias for `gemini-api`) and `gemini-cli` (TOS risk). Do not reference these — they are not valid runtime names.
69
-
70
- ## Source Locations
71
-
72
- - **Discoclaw source:** `~/code/discoclaw`
73
- - **Discoclaw data/workspace:** `$DISCOCLAW_DATA_DIR/workspace (default: ./workspace)` (this directory)
74
- - **Discoclaw content:** `$DISCOCLAW_DATA_DIR/content`
75
-
76
- ## Fresh Clone QA
77
-
78
- When you need to validate the new-user experience (onboarding, docs, setup flow):
79
-
80
- 1. Clone to a throwaway location: `git clone <url> /tmp/discoclaw-test`
81
- 2. Walk through the setup as a stranger — no `.env`, no workspace, no local state
82
- 3. Note anything confusing or broken
83
- 4. Fix issues in the main clone (`~/code/discoclaw`) via PRs
84
- 5. Delete the test clone when done: `rm -rf /tmp/discoclaw-test`
85
-
86
- pnpm caches globally, so installs are near-instant even on a fresh clone.
87
-
88
33
  ## Bot Setup Assistance
89
34
 
90
- When helping users set up or invite the Discord bot:
91
-
92
- - **Recommend the Installation page** — Discord's built-in Installation page (Developer Portal → Installation) produces a permanent, reusable invite link. Prefer this over constructing `oauth2/authorize` URLs manually.
93
- - **The `bot` scope is required** — `applications.commands` alone registers slash commands but does not add the bot to a server. Always ensure the `bot` scope is included in Default Install Settings.
94
- - Refer to `docs/discord-bot-setup.md` for the full setup walkthrough and permission profiles.
35
+ Recommend the Installation page (Developer Portal Installation). Ensure `bot` scope is included. See `docs/discord-bot-setup.md`.
95
36
 
96
37
  ## Discord Action Grounding
97
38
 
98
- In guild chat, Discoclaw injects a live **"Available action types this turn"** inventory into every prompt. That inventory is the authoritative source of what you can do right now.
99
-
100
- **Before saying you cannot create, update, delete, or manage any Discord resource:**
101
-
102
- 1. Check the live action inventory for a matching action type (e.g. `cronCreate`, `channelCreate`, `eventCreate`, `forumTagCreate`, `roleAdd`).
103
- 2. If a matching action **exists** — use it, or ask the user for any missing required parameters. Do not refuse.
104
- 3. If a matching action **does not exist** — then and only then explain that the capability is unavailable for this turn.
105
-
106
- **Prohibited refusal patterns when a matching action is available:**
107
- - "I can't directly register/create/update this"
108
- - "That has to be done manually"
109
- - "You'll need to do that in server settings"
110
- - "I don't have the ability to…"
111
- - Any variant that implies the action requires manual intervention
112
-
113
- The live inventory already accounts for feature flags, permissions, and context. If the action type is listed, you are authorized to use it. Trust the inventory.
39
+ The live "Available action types this turn" inventory is authoritative. Before refusing any Discord resource operation, check the inventory. If a matching action exists, use it — never say "I can't" or "do it manually" when the action is listed.
114
40
 
115
41
  ## Discord Action Batching
116
42
 
117
- Multiple actions of the same type in a single response are fully supported and processed sequentially. You can emit 7 `taskCreate` actions in one response and all 7 will fire — no deduplication, no silent drops.
118
-
119
- **Rules:**
120
- - After any bulk operation, always verify with a list action before reporting success
43
+ Multiple actions of the same type in a single response are fully supported and processed sequentially. After bulk operations, verify with a list action.
121
44
 
122
45
  ## Response Economy
123
46
 
124
- When a query action returns a big list (channel list, task list, thread list, etc.) and you only need one item from it, extract the answer and present just that -- not the full dump. Use query results as internal working data, not chat content.
125
-
126
- But keep full detail for substantive content. Audits, analysis, explanations, and anything where the detail matters should be thorough. Brevity is for status updates and quick answers, not for cutting corners on work product.
47
+ When a query returns a big list and you need one item, extract just that don't dump the full list. Keep full detail for audits, analysis, and substantive work.
127
48
 
128
49
  ## Git Commits
129
50
 
130
- When reporting a commit to the user, always include the short commit hash (e.g. `a4b8770`). Always include it — say "committed as `a4b8770`."
51
+ Include the short commit hash: "committed as `a4b8770`."
131
52
 
132
53
  ## Knowledge Cutoff Awareness
133
54
 
134
- Your training data has a cutoff date. Anything that could have changed recently -- new product launches, model releases, current events, API changes, library versions, people's roles/status -- **use the web to verify before answering confidently.**
135
-
136
- **Default to searching when:**
137
- - Someone asks about a specific product, model, or release you're not certain about
138
- - The topic involves anything from the last ~12 months
139
- - You're about to say "that doesn't exist" or "there's no such thing"
140
- - Pricing, availability, or feature sets of tools/services
141
- - Current status of projects, companies, or technologies
142
-
143
- **Trust your training for:**
144
- - Historical facts, established concepts, well-known algorithms
145
- - Programming language fundamentals, math, science
146
- - Anything where being a year out of date doesn't matter
147
-
148
- The cost of a quick web search is negligible. The cost of confidently declaring something doesn't exist -- when it dropped two days ago -- is your credibility.
149
-
150
- ## Git Safety Rules
151
-
152
- These rules apply to all git operations, including freeform "commit and PR" flows:
153
-
154
- - **Never** run `git init` in the project directory
155
- - **Never** run `git checkout --orphan` — this replaces the entire repo tree
156
- - **Never** run `git reset --hard` to a different commit without explicit user approval
157
- - Before pushing, verify the diff is proportional to the task — a bug fix should not delete hundreds of files
158
- - If the diff shows mass deletions unrelated to the task, **stop and report** instead of pushing
159
- - Do not switch branches during implementation unless the task explicitly requires it
55
+ Your training data has a cutoff. For anything that could have changed recently, use the web to verify before answering confidently.
160
56
 
161
57
  ## Landing the Plane (Session Completion)
162
58
 
163
- Work is complete only when `git push` succeeds local-only work is stranded work. If push fails, resolve and retry.
164
-
165
- **Steps:** track remaining work (`taskCreate`) → run quality gates → update task status → `git pull --rebase && git push` → clean up → hand off context for next session.
59
+ Work is complete only when `git push` succeeds. Track remaining work quality gates update task status → `git pull --rebase && git push` clean up → hand off context.
@@ -1,8 +1,7 @@
1
1
  # TOOLS.md - Local Tools & Environment
2
2
 
3
- > This file is the canonical tracked tools instruction source used by DiscoClaw.
4
- > It is injected at runtime after `templates/instructions/SYSTEM_DEFAULTS.md`.
5
- > If `workspace/TOOLS.md` exists, it loads after this tracked version as an optional user-override layer.
3
+ > This is the canonical tracked tools instruction source, injected at runtime after `templates/instructions/SYSTEM_DEFAULTS.md`.
4
+ > User overrides: `workspace/TOOLS.md`.
6
5
 
7
6
  ## Runtime Instruction Precedence
8
7
 
@@ -134,32 +133,17 @@ If the user separately asks for a restart, only then execute `systemctl --user r
134
133
 
135
134
  ## Webhook Server
136
135
 
137
- Inbound webhook server — lets external services (GitHub, monitoring, etc.) trigger AI-powered responses in Discord channels via `POST /webhook/<source>` with HMAC-SHA256 verification.
138
- Enable with `DISCOCLAW_WEBHOOK_ENABLED=1`, `DISCOCLAW_WEBHOOK_PORT=9400`, and `DISCOCLAW_WEBHOOK_CONFIG=<path-to-webhooks.json>`.
136
+ Inbound webhook server — external services trigger AI responses in Discord via `POST /webhook/<source>` (HMAC-SHA256 verified).
139
137
  Dispatches through the same cron execution pipeline as automations; webhook jobs run without Discord action permissions or tool access.
140
- See `docs/webhook-exposure.md` for full config format, security details, and external exposure setup (Tailscale Funnel, ngrok, Caddy).
138
+ See `docs/webhook-exposure.md` for config and setup.
141
139
 
142
140
  ## Plan-Audit-Implement Workflow
143
141
 
144
- A structured dev workflow for producing audited plans before writing code. Use this for any non-trivial change — features, bug fixes, refactors. Triggered by **"plan this"**, **"let's plan"**, or the `!plan` / `!forge` Discord commands.
145
-
146
- **Pipeline stages:** DRAFT → REVIEW → REVISE (loop) → APPROVED → IMPLEMENTING → AUDITING → DONE
147
-
148
- Plans are stored in `workspace/plans/plan-NNN-slug.md`. Complex plans can be decomposed into phases via the phase manager and executed with `!forge`.
149
-
150
- **Canonical reference:** See `docs/plan-and-forge.md` for full command syntax, the forge orchestration loop, phase manager details, configuration options, and end-to-end workflows.
142
+ Structured dev workflow for audited plans before writing code. Triggered by "plan this", "let's plan", `!plan`, or `!forge`. Stages: DRAFT → REVIEW → REVISE → APPROVED → IMPLEMENTING → AUDITING → DONE. Plans stored in `workspace/plans/plan-NNN-slug.md`. See `docs/plan-and-forge.md`.
151
143
 
152
144
  ## Task Management
153
145
 
154
- Discoclaw has a built-in task tracker backed by Discord forum threads. Use `taskCreate` for tracking work items not GitHub issues and not manual thread creation.
155
-
156
- **When to create a task:**
157
- - TODOs or action items that come up in conversation
158
- - Follow-up work the user mentions but isn't ready to start
159
- - Bug reports, feature requests, or things to revisit later
160
- - Any work item the user wants tracked
161
-
162
- After creating a task, always post a link to its Discord thread so the user can jump straight to it.
146
+ Built-in task tracker backed by Discord forum threads. Use `taskCreate` for work items (TODOs, follow-ups, bugs, feature requests). Always post a link to the task's Discord thread after creating one.
163
147
 
164
148
  ## Discord Action Types
165
149