@mindot/will 0.3.0 → 0.5.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 (57) hide show
  1. package/README.md +63 -21
  2. package/dist/channels/discord.d.ts +69 -0
  3. package/dist/channels/discord.js +193 -0
  4. package/dist/channels/discord.js.map +1 -0
  5. package/dist/channels/whatsapp.d.ts +72 -0
  6. package/dist/channels/whatsapp.js +252 -0
  7. package/dist/channels/whatsapp.js.map +1 -0
  8. package/dist/cli.js +904 -356
  9. package/dist/cli.js.map +1 -1
  10. package/dist/index.d.ts +141 -141
  11. package/dist/index.js +441 -342
  12. package/dist/index.js.map +1 -1
  13. package/dist/mcp/effectors.d.ts +1 -1
  14. package/dist/types-E9-HV-SW.d.ts +11 -0
  15. package/dist/{will-B5eKs3Wv.d.ts → will-BDq-TMQr.d.ts} +4013 -3916
  16. package/package.json +17 -3
  17. package/src/channels/discord.ts +214 -0
  18. package/src/channels/roster.ts +87 -0
  19. package/src/channels/types.ts +46 -0
  20. package/src/channels/whatsapp.ts +318 -0
  21. package/src/cli.ts +57 -9
  22. package/src/cognition/agency/engines/deliberation.engine.ts +7 -7
  23. package/src/cognition/agency/execution.primitives.ts +11 -11
  24. package/src/cognition/agency/proactive.communicator.ts +8 -8
  25. package/src/cognition/config.mirror.entities.ts +2 -2
  26. package/src/cognition/conversation.memory.ts +1 -1
  27. package/src/cognition/faculties/executive.engine/commands.ts +7 -15
  28. package/src/cognition/faculties/executive.engine/engine.ts +66 -35
  29. package/src/cognition/faculties/executive.engine/escalation.buffer.ts +1 -1
  30. package/src/cognition/faculties/executive.engine/facet.ts +1 -1
  31. package/src/cognition/faculties/executive.engine/prompt.factory.ts +76 -61
  32. package/src/cognition/faculties/executive.engine/types.ts +1 -1
  33. package/src/cognition/faculties/introspection.engine.ts +1 -2
  34. package/src/cognition/faculties/planning.engine/engine.ts +38 -1
  35. package/src/cognition/faculties/planning.engine/plan.store.ts +42 -0
  36. package/src/cognition/faculties/planning.engine/plan.supervision.ts +7 -7
  37. package/src/cognition/faculties/theory.of.mind.ts +2 -2
  38. package/src/cognition/senses/audition.engine/engine.ts +21 -21
  39. package/src/cognition/utilities/token.tracker.ts +6 -0
  40. package/src/host/boot.ts +95 -7
  41. package/src/llm/index.ts +75 -25
  42. package/src/llm/summarizer.ts +2 -2
  43. package/src/profiles/companion.ts +14 -14
  44. package/src/profiles/company-brain.ts +19 -19
  45. package/src/profiles/customer-service.ts +17 -17
  46. package/src/profiles/game-npc.ts +10 -10
  47. package/src/profiles/index.ts +2 -2
  48. package/src/profiles/smart-home.ts +16 -16
  49. package/src/runners/outreach.runner.ts +6 -9
  50. package/src/runners/social.runner.ts +1 -4
  51. package/src/runners/thin-shim.runner.ts +4 -6
  52. package/src/sdk/will.ts +42 -16
  53. package/src/stem/guards/identity.coherence.ts +9 -6
  54. package/src/stem/guards/identity.guard.ts +20 -9
  55. package/src/stem/index.ts +7 -7
  56. package/src/stem/mind.ts +182 -98
  57. package/src/stem/tracts/outbox.controller.ts +2 -2
@@ -0,0 +1,318 @@
1
+ // ─────────────────────────────────────────────────────────────
2
+ // src/channels/whatsapp.ts — a Will present on WhatsApp
3
+ // ─────────────────────────────────────────────────────────────
4
+ //
5
+ // Same paradigm as the Discord bridge, different room:
6
+ //
7
+ // inbound DM / group message → will.perceive({ from, speaker, text, thread })
8
+ // — every author is `whatsapp:<number>` (stable across chats), the
9
+ // push name is *learned* by the mind, and each chat (DM or group)
10
+ // is its own conversation thread.
11
+ // outbound will.on('message') → the addressee's last shared group, else
12
+ // their DM (a WhatsApp DM jid is derivable from the number — no
13
+ // roster miss is fatal), else the home chat.
14
+ //
15
+ // TRANSPORT — read before deploying. This bridge speaks the *linked-device*
16
+ // protocol via Baileys: the Will pairs to a WhatsApp account by QR scan, like
17
+ // WhatsApp Web. That is not a sanctioned bot API — WhatsApp's terms don't
18
+ // allow automation on personal accounts, and accounts doing it can be banned.
19
+ // Meta's sanctioned path (the Business Cloud API) needs a business account,
20
+ // webhook hosting, and app review — no 2-minute pairing. Operator's choice;
21
+ // docs/channels/whatsapp.md is explicit about the trade. Use a spare number.
22
+ //
23
+ // Baileys is imported lazily inside `createWhatsAppSocket` — tests (and hosts
24
+ // that bring their own transport) inject `socket`, and the structural
25
+ // `WaLikeSocket` type keeps the dependency out of the type graph.
26
+ // ─────────────────────────────────────────────────────────────
27
+
28
+ import type { Will, WillMessage } from '#sdk/will'
29
+ import { ChannelRoster } from '#channels/roster'
30
+ import { chunkText, type ChannelBridge } from '#channels/types'
31
+
32
+ /** WhatsApp's hard per-message cap. */
33
+ const WHATSAPP_MESSAGE_LIMIT = 65_536
34
+
35
+ // ── The slice of a Baileys socket the bridge actually uses (structural) ──────
36
+
37
+ export interface WaLikeMessage {
38
+ key: {
39
+ remoteJid?: string | null
40
+ fromMe?: boolean | null
41
+ /** Sender jid inside a group (absent in DMs). */
42
+ participant?: string | null
43
+ }
44
+ /** Sender's display ("push") name. */
45
+ pushName?: string | null
46
+ messageStubType?: number | null
47
+ message?: {
48
+ conversation?: string | null
49
+ extendedTextMessage?: { text?: string | null; contextInfo?: { mentionedJid?: string[] | null } | null } | null
50
+ imageMessage?: { caption?: string | null } | null
51
+ videoMessage?: { caption?: string | null } | null
52
+ } | null
53
+ }
54
+
55
+ export interface WaLikeSocket {
56
+ /** The paired account, once connected. id like '4915…:12@s.whatsapp.net'. */
57
+ user?: { id: string; name?: string } | null
58
+ ev: { on( event: 'messages.upsert', fn: ( u: { messages: WaLikeMessage[]; type?: string } ) => void ): unknown }
59
+ sendMessage( jid: string, content: { text: string } ): Promise<unknown>
60
+ sendPresenceUpdate?( state: 'composing' | 'paused', jid?: string ): Promise<unknown>
61
+ /** Tear the connection down (does not unlink the device). */
62
+ end?( err?: Error ): void
63
+ }
64
+
65
+ // ── Options ──────────────────────────────────────────────────────────────────
66
+
67
+ export interface WhatsAppBridgeOptions {
68
+ /** Chat jids the Will inhabits (groups `…@g.us`, DMs `…@s.whatsapp.net`). Unset = every chat. */
69
+ chats?: string[]
70
+ /** Perceive group messages only when the Will is @mentioned (DMs always perceived). */
71
+ mentionOnly?: boolean
72
+ /** Fallback chat jid for utterances with no reachable addressee. */
73
+ homeChatId?: string
74
+ /** Linked-device credentials dir (default: ./.will/<willId>.wa-auth). QR pairs on first run. */
75
+ authPath?: string
76
+ /** Roster path (default: ./.will/<willId>.whatsapp.json). */
77
+ rosterPath?: string
78
+ /** Test / power-user seam: bring your own socket; Baileys is never imported. */
79
+ socket?: WaLikeSocket
80
+ log?: ( msg: string ) => void
81
+ }
82
+
83
+ // ── Helpers ──────────────────────────────────────────────────────────────────
84
+
85
+ const isGroupJid = ( jid: string ): boolean => jid.endsWith( '@g.us' )
86
+
87
+ /** '4915123:7@s.whatsapp.net' → '4915123' (device + server stripped). */
88
+ const bareId = ( jid: string ): string => jid.split( '@' )[0]!.split( ':' )[0]!
89
+
90
+ /** A DM jid is derivable from the bare number — WhatsApp's gift to proactivity. */
91
+ const dmJidFor = ( userId: string ): string => `${ userId }@s.whatsapp.net`
92
+
93
+ function textOf( m: WaLikeMessage ): string {
94
+ const msg = m.message
95
+ return msg?.conversation
96
+ ?? msg?.extendedTextMessage?.text
97
+ ?? msg?.imageMessage?.caption
98
+ ?? msg?.videoMessage?.caption
99
+ ?? ''
100
+ }
101
+
102
+ // ── The bridge ───────────────────────────────────────────────────────────────
103
+
104
+ /**
105
+ * Connect a Will to WhatsApp over the linked-device protocol. First run prints
106
+ * a QR to pair (Settings → Linked devices → Link a device); credentials persist
107
+ * in `authPath` so later runs reconnect silently. Close via the returned
108
+ * `ChannelBridge.close()` — the Will keeps ticking; it only loses this surface.
109
+ */
110
+ export async function connectWhatsApp( will: Will, opts: WhatsAppBridgeOptions = {} ): Promise<ChannelBridge> {
111
+ const log = opts.log ?? ( ( m: string ) => console.error( `[will:whatsapp] ${ m }` ) )
112
+ const roster = new ChannelRoster( opts.rosterPath ?? `.will/${ will.id }.whatsapp.json` )
113
+ const allowed = opts.chats?.length ? new Set( opts.chats ) : null
114
+
115
+ let closed = false
116
+ const socket = opts.socket ?? await createWhatsAppSocket( {
117
+ authPath: opts.authPath ?? `.will/${ will.id }.wa-auth`,
118
+ log,
119
+ stillOpen: () => !closed,
120
+ } )
121
+
122
+ /** The most recently active allowed chat — last-resort proactive target. */
123
+ let lastActiveChatId: string | null = opts.homeChatId ?? null
124
+
125
+ // ── inbound: platform message → stimulus ──────────────────────────────────
126
+ socket.ev.on( 'messages.upsert', ( { messages, type } ) => {
127
+ if( type && type !== 'notify' ) return // history sync/corrections are not new percepts
128
+ for( const m of messages ) void onMessage( m )
129
+ } )
130
+
131
+ async function onMessage( m: WaLikeMessage ): Promise<void> {
132
+ const jid = m.key.remoteJid
133
+ if( !jid || m.key.fromMe || !m.message || m.messageStubType ) return
134
+ if( jid.endsWith( '@broadcast' ) || jid.endsWith( '@newsletter' ) ) return // stories/broadcasts aren't a room the Will is in
135
+ if( allowed && !allowed.has( jid ) ) return
136
+
137
+ const isGroup = isGroupJid( jid )
138
+ const senderJid = isGroup ? m.key.participant : jid
139
+ if( !senderJid ) return
140
+
141
+ const selfId = socket.user ? bareId( socket.user.id ) : null
142
+ const mentioned = m.message.extendedTextMessage?.contextInfo?.mentionedJid ?? []
143
+ const addressed = !isGroup || ( selfId != null && mentioned.some( j => bareId( j ) === selfId ) )
144
+ if( opts.mentionOnly && !addressed ) return
145
+
146
+ const userId = bareId( senderJid )
147
+ const entityId = `whatsapp:${ userId }`
148
+ const speaker = m.pushName ?? undefined
149
+
150
+ roster.record( {
151
+ entityId,
152
+ userId,
153
+ ...( speaker ? { displayName: speaker } : {} ),
154
+ ...( isGroup ? { lastChannelId: jid } : { dmChannelId: jid } ),
155
+ } )
156
+ if( isGroup ) lastActiveChatId = jid
157
+
158
+ // Same rule as Discord: a presence cue only when addressed — the mind may
159
+ // still choose silence, and 'composing' expires on its own.
160
+ if( addressed ) await socket.sendPresenceUpdate?.( 'composing', jid ).catch( () => {} )
161
+
162
+ const text = textOf( m )
163
+ if( !text.trim() ) return
164
+
165
+ await will.perceive( {
166
+ text,
167
+ from: entityId,
168
+ thread: `whatsapp:${ jid }`,
169
+ ...( speaker ? { speaker } : {} ),
170
+ } )
171
+ }
172
+
173
+ // ── outbound: projected utterance → the addressee ─────────────────────────
174
+ will.on( 'message', ( m: WillMessage ) => { if( !closed ) void deliver( m ) } )
175
+
176
+ async function deliver( m: WillMessage ): Promise<void> {
177
+ const peer = m.to ? roster.resolve( m.to ) : undefined
178
+ const chunks = chunkText( m.content, WHATSAPP_MESSAGE_LIMIT )
179
+
180
+ // Last shared group → known DM → DM derived from the entity id itself →
181
+ // home chat → last active chat. Unlike Discord, an unmet-but-addressed
182
+ // entity is still reachable: `whatsapp:<number>` implies its DM jid.
183
+ const derivedDm = m.to?.startsWith( 'whatsapp:' ) ? dmJidFor( m.to.slice( 'whatsapp:'.length ) ) : undefined
184
+ const targets = [ peer?.lastChannelId, peer?.dmChannelId, derivedDm, opts.homeChatId ?? undefined, lastActiveChatId ?? undefined ]
185
+ for( const jid of targets ){
186
+ if( !jid ) continue
187
+ try {
188
+ for( const chunk of chunks ) await socket.sendMessage( jid, { text: chunk } )
189
+ return
190
+ }
191
+ catch { /* try the next route */ }
192
+ }
193
+ log( `no route for utterance to '${ m.to }' — dropped (${ m.content.length } chars)` )
194
+ }
195
+
196
+ // ── lifecycle ──────────────────────────────────────────────────────────────
197
+ return {
198
+ kind: 'whatsapp',
199
+ async start(): Promise<void> {
200
+ // createWhatsAppSocket resolves already-open; an injected socket is the
201
+ // caller's to have readied.
202
+ log( `${ will.name } is present on WhatsApp${ socket.user ? ` as ${ bareId( socket.user.id ) }` : '' }` )
203
+ },
204
+ async close(): Promise<void> {
205
+ if( closed ) return
206
+ closed = true
207
+ roster.flush()
208
+ try { socket.end?.() } catch { /* already down */ }
209
+ },
210
+ }
211
+ }
212
+
213
+ // ── The real transport (Baileys, lazily imported) ────────────────────────────
214
+
215
+ interface SocketFactoryOpts {
216
+ authPath: string
217
+ log: ( msg: string ) => void
218
+ /** Reconnect only while the bridge is open. */
219
+ stillOpen: () => boolean
220
+ }
221
+
222
+ /**
223
+ * A reconnect-stable facade over Baileys. Baileys' own pattern replaces the
224
+ * socket object on every reconnect; the facade keeps one stable identity the
225
+ * bridge can hold, re-attaching its subscribers to each inner incarnation.
226
+ * Resolves once the first connection is open (after QR pairing on first run).
227
+ */
228
+ async function createWhatsAppSocket( o: SocketFactoryOpts ): Promise<WaLikeSocket> {
229
+ let baileys: typeof import( 'baileys' )
230
+ try { baileys = await import( 'baileys' ) }
231
+ catch {
232
+ throw new Error( 'baileys is not installed (it is an optionalDependency) — run `bun add baileys` / `npm i baileys` and retry.' )
233
+ }
234
+ const { default: makeWASocket, useMultiFileAuthState, DisconnectReason } = baileys
235
+
236
+ // Baileys logs through pino at info by default — JSON noise all over the
237
+ // host's stderr. A minimal pino-shaped silent logger (child() returns
238
+ // itself, every level a no-op) is what it actually needs; hand-rolling it
239
+ // beats importing a transitive dep that may not resolve from dist/.
240
+ const noop = (): void => {}
241
+ const logger = {
242
+ level: 'silent',
243
+ child(){ return logger },
244
+ trace: noop, debug: noop, info: noop, warn: noop, error: noop, fatal: noop,
245
+ }
246
+
247
+ type Handler = ( u: { messages: WaLikeMessage[]; type?: string } ) => void
248
+ const subscribers: Handler[] = []
249
+ let inner: ReturnType<typeof makeWASocket> | null = null
250
+
251
+ const facade: WaLikeSocket = {
252
+ get user(){ return inner?.user ? { id: inner.user.id, name: inner.user.name ?? undefined } : null },
253
+ ev: { on: ( _e, fn ) => { subscribers.push( fn ) } },
254
+ sendMessage: ( jid, content ) => {
255
+ if( !inner ) return Promise.reject( new Error( 'whatsapp socket not connected' ) )
256
+ return inner.sendMessage( jid, content )
257
+ },
258
+ sendPresenceUpdate: ( state, jid ) => inner?.sendPresenceUpdate( state, jid ) ?? Promise.resolve(),
259
+ end: () => inner?.end( undefined ),
260
+ }
261
+
262
+ const { state, saveCreds } = await useMultiFileAuthState( o.authPath )
263
+
264
+ await new Promise<void>( ( resolveOpen, rejectOpen ) => {
265
+ let opened = false
266
+
267
+ function connect(): void {
268
+ const sock = makeWASocket( { auth: state, logger: logger as never } )
269
+ inner = sock
270
+
271
+ sock.ev.on( 'creds.update', saveCreds )
272
+ sock.ev.on( 'messages.upsert', u => { for( const fn of subscribers ) fn( u as never ) } )
273
+
274
+ sock.ev.on( 'connection.update', update => {
275
+ const { connection, lastDisconnect, qr } = update as {
276
+ connection?: string; lastDisconnect?: { error?: unknown }; qr?: string
277
+ }
278
+
279
+ if( qr ) void printQr( qr, o.log )
280
+
281
+ if( connection === 'open' && !opened ){
282
+ opened = true
283
+ resolveOpen()
284
+ }
285
+ if( connection === 'close' ){
286
+ const code = ( lastDisconnect?.error as { output?: { statusCode?: number } } | undefined )?.output?.statusCode
287
+ if( code === DisconnectReason.loggedOut ){
288
+ const err = new Error( 'WhatsApp unlinked this device (logged out) — delete the auth dir and pair again.' )
289
+ o.log( err.message )
290
+ if( !opened ) rejectOpen( err )
291
+ return
292
+ }
293
+ if( o.stillOpen() ){
294
+ o.log( `connection closed (status ${ code ?? '?' }) — reconnecting…` )
295
+ setTimeout( connect, 3_000 )
296
+ }
297
+ }
298
+ } )
299
+ }
300
+
301
+ connect()
302
+ } )
303
+
304
+ return facade
305
+ }
306
+
307
+ /** QR to the terminal; falls back to the raw pairing string if the tiny renderer is absent. */
308
+ async function printQr( qr: string, log: ( m: string ) => void ): Promise<void> {
309
+ log( 'pair this device: WhatsApp → Settings → Linked devices → Link a device' )
310
+ try {
311
+ type QrModule = { generate( s: string, o: { small: boolean } ): void }
312
+ const qrt = ( await import( 'qrcode-terminal' as string ) ) as QrModule & { default?: QrModule }
313
+ ;( qrt.default ?? qrt ).generate( qr, { small: true } )
314
+ }
315
+ catch {
316
+ log( `qrcode-terminal not installed — raw pairing code:\n${ qr }` )
317
+ }
318
+ }
package/src/cli.ts CHANGED
@@ -3,8 +3,11 @@
3
3
  // src/cli.ts — the `will` command: host a persistent mind
4
4
  // ─────────────────────────────────────────────────────────────
5
5
  //
6
- // will mcp host over MCP stdio (Claude Desktop / Claude Code / IDEs)
7
- // will serve host over HTTP (any language; the sidecar) — WILL_PORT/WILL_HOST
6
+ // will mcp host over MCP stdio (Claude Desktop / Claude Code / IDEs)
7
+ // will serve host over HTTP (any language; the sidecar) — WILL_PORT/WILL_HOST
8
+ // will discord a presence in a Discord server — DISCORD_BOT_TOKEN
9
+ // will whatsapp a presence on WhatsApp — QR-pairs a linked device (unofficial
10
+ // protocol; see docs/channels/whatsapp.md before deploying)
8
11
  //
9
12
  // Both hosts raise the same mind the same way (see host/boot.ts): env-configured,
10
13
  // woken from its PMA artifact when one exists, hibernated back on the way out —
@@ -24,14 +27,21 @@ import { StdioServerTransport } from '@modelcontextprotocol/sdk/server/stdio.js'
24
27
  import { routeLogsToStderr, bootWillFromEnv } from '#root/host/boot'
25
28
  import { buildWillMcpServer } from '#root/mcp/server'
26
29
  import { buildWillHttpServer } from '#root/serve/server'
30
+ import { connectDiscord } from '#channels/discord'
31
+ import { connectWhatsApp } from '#channels/whatsapp'
27
32
 
28
33
  // stdout is the MCP protocol channel under `will mcp` — route logs FIRST.
29
34
  routeLogsToStderr()
30
35
 
31
- const USAGE = `usage: will <mcp | serve>
36
+ const USAGE = `usage: will <mcp | serve | discord | whatsapp>
32
37
 
33
- mcp host a persistent mind over MCP stdio (Claude Desktop / Claude Code)
34
- serve host a persistent mind over HTTP (any language; WILL_PORT, default 7777)
38
+ mcp host a persistent mind over MCP stdio (Claude Desktop / Claude Code)
39
+ serve host a persistent mind over HTTP (any language; WILL_PORT, default 7777)
40
+ discord put a persistent mind in a Discord server (DISCORD_BOT_TOKEN; optional
41
+ WILL_DISCORD_CHANNELS, WILL_DISCORD_MENTION_ONLY, WILL_DISCORD_HOME_CHANNEL)
42
+ whatsapp put a persistent mind on WhatsApp — QR-pairs as a linked device; no token.
43
+ UNOFFICIAL protocol (ban risk; use a spare number — docs/channels/whatsapp.md).
44
+ Optional WILL_WHATSAPP_CHATS, WILL_WHATSAPP_MENTION_ONLY, WILL_WHATSAPP_HOME_CHAT
35
45
 
36
46
  Shared env: WILL_NAME, WILL_IDENTITY, WILL_TIER, WILL_LLM, WILL_TICK_MS,
37
47
  WILL_SEED, WILL_PMA_PATH, WILL_MCP_SERVERS. The mind persists across runs via
@@ -40,19 +50,57 @@ its PMA artifact.`
40
50
  async function main(): Promise<void> {
41
51
  const sub = process.argv[2]
42
52
 
43
- if( sub !== 'mcp' && sub !== 'serve' ){
53
+ if( sub !== 'mcp' && sub !== 'serve' && sub !== 'discord' && sub !== 'whatsapp' ){
44
54
  console.error( sub ? `unknown subcommand: ${ sub }\n\n${ USAGE }` : USAGE )
45
55
  process.exit( sub ? 2 : 0 )
46
56
  }
47
57
 
48
- const { will, name, pmaPath, tickMs, engineTier, onCleanup, shutdown } = await bootWillFromEnv()
58
+ // Fail on missing platform credentials BEFORE raising a mind.
59
+ if( sub === 'discord' && !process.env.DISCORD_BOT_TOKEN ){
60
+ console.error( '[will] DISCORD_BOT_TOKEN is required for `will discord` — create a bot at https://discord.com/developers/applications (enable the Message Content intent) and set the token.' )
61
+ process.exit( 2 )
62
+ }
63
+
64
+ const { will, name, pmaPath, tickMs, anatomy, onCleanup, shutdown } = await bootWillFromEnv()
49
65
 
50
66
  if( sub === 'mcp' ){
51
67
  // The MCP client owns our stdin — its disconnect is the shutdown signal.
52
68
  process.stdin.on( 'end', () => void shutdown( 'client disconnected' ) )
53
69
  const server = buildWillMcpServer( will, { pmaPath } )
54
70
  await server.connect( new StdioServerTransport() )
55
- console.error( `[will] ${ name } is listening on MCP stdio (tick ${ tickMs }ms, tier ${ engineTier })` )
71
+ console.error( `[will] ${ name } is listening on MCP stdio (tick ${ tickMs }ms, anatomy ${ anatomy })` )
72
+ return
73
+ }
74
+
75
+ if( sub === 'discord' ){
76
+ const csv = ( v?: string ) => v?.split( ',' ).map( s => s.trim() ).filter( Boolean )
77
+ const bridge = await connectDiscord( will, {
78
+ token: process.env.DISCORD_BOT_TOKEN!,
79
+ channels: csv( process.env.WILL_DISCORD_CHANNELS ),
80
+ mentionOnly: /^(1|true|yes)$/i.test( process.env.WILL_DISCORD_MENTION_ONLY ?? '' ),
81
+ homeChannelId: process.env.WILL_DISCORD_HOME_CHANNEL,
82
+ rosterPath: pmaPath.replace( /(\.pma)?\.json$/, '' ) + '.discord.json',
83
+ } )
84
+ onCleanup( () => bridge.close() )
85
+ await bridge.start()
86
+ console.error( `[will] ${ name } is present on Discord (tick ${ tickMs }ms, anatomy ${ anatomy }) — it speaks when it decides to.` )
87
+ return
88
+ }
89
+
90
+ if( sub === 'whatsapp' ){
91
+ const csv = ( v?: string ) => v?.split( ',' ).map( s => s.trim() ).filter( Boolean )
92
+ const stem = pmaPath.replace( /(\.pma)?\.json$/, '' )
93
+ // connectWhatsApp blocks here on first run until the printed QR is scanned.
94
+ const bridge = await connectWhatsApp( will, {
95
+ chats: csv( process.env.WILL_WHATSAPP_CHATS ),
96
+ mentionOnly: /^(1|true|yes)$/i.test( process.env.WILL_WHATSAPP_MENTION_ONLY ?? '' ),
97
+ homeChatId: process.env.WILL_WHATSAPP_HOME_CHAT,
98
+ authPath: stem + '.wa-auth',
99
+ rosterPath: stem + '.whatsapp.json',
100
+ } )
101
+ onCleanup( () => bridge.close() )
102
+ await bridge.start()
103
+ console.error( `[will] ${ name } is present on WhatsApp (tick ${ tickMs }ms, anatomy ${ anatomy }) — it speaks when it decides to.` )
56
104
  return
57
105
  }
58
106
 
@@ -65,7 +113,7 @@ async function main(): Promise<void> {
65
113
  server.once( 'error', reject )
66
114
  server.listen( port, host, () => resolve() )
67
115
  } )
68
- console.error( `[will] ${ name } is listening on http://${ host }:${ port } (tick ${ tickMs }ms, tier ${ engineTier })` )
116
+ console.error( `[will] ${ name } is listening on http://${ host }:${ port } (tick ${ tickMs }ms, anatomy ${ anatomy })` )
69
117
  console.error( `[will] try: curl -X POST http://${ host }:${ port }/perceive -H 'content-type: application/json' -d '{"text":"Hello"}'` )
70
118
  }
71
119
 
@@ -42,7 +42,7 @@ interface DeliberationFacetHandle {
42
42
  destroy(): void
43
43
  }
44
44
  export interface DeliberationFacetProvider {
45
- spawnFacet(): { attention: 'available' | 'full'; handle?: DeliberationFacetHandle }
45
+ spawnFacet( role?: 'deliberation' ): { attention: 'available' | 'full'; handle?: DeliberationFacetHandle }
46
46
  }
47
47
 
48
48
  interface Candidate {
@@ -58,8 +58,8 @@ interface Candidate {
58
58
 
59
59
  const DELIBERATION_INSTRUCTIONS =
60
60
  'Automatic action-selection was uncertain or the stakes were high. From the candidate actions ' +
61
- 'listed above, choose the ONE that best fits who you are and your situation. Do not invent ' +
62
- 'actions that are not listed. Put your chosen action as your single action; its "type" must be ' +
61
+ 'listed above, choose the ONE that best fits who I am and my situation. Do not invent ' +
62
+ 'actions that are not listed. Put my chosen action as my single action; its "type" must be ' +
63
63
  'exactly one of the candidate names.'
64
64
 
65
65
  export class DeliberationEngine implements CognitiveEngine {
@@ -143,7 +143,7 @@ export class DeliberationEngine implements CognitiveEngine {
143
143
  ): Promise<string> {
144
144
  try {
145
145
  if( !this._handle ){
146
- const spawned = this._provider!.spawnFacet()
146
+ const spawned = this._provider!.spawnFacet('deliberation')
147
147
  if( spawned.attention === 'full' || !spawned.handle ){
148
148
  logger.info( '[deliberation] facet budget full — confirming substrate winner' )
149
149
  return provisional
@@ -209,9 +209,9 @@ export class DeliberationEngine implements CognitiveEngine {
209
209
  // facet owns the interruption in-character rather than reasoning in a vacuum.
210
210
  const preemptedFrom = str( meta['preemptedFrom'] )
211
211
  if( preemptedFrom )
212
- lines.push( `You just broke off a pending action ("${ preemptedFrom }") because something more pressing pulled at you. Decide what to do now:` )
212
+ lines.push( `I just broke off a pending action ("${ preemptedFrom }") because something more pressing pulled at me. Decide what to do now:` )
213
213
  else
214
- lines.push( 'Your automatic action-selection was uncertain. Candidate actions:' )
214
+ lines.push( 'My automatic action-selection was uncertain. Candidate actions:' )
215
215
  candidates.forEach( ( c, i ) => {
216
216
  const to = c.targetEntityId ? ` toward ${ c.targetEntityId }` : ''
217
217
  // The ability's meaning, so the facet weighs what each option IS FOR rather
@@ -219,7 +219,7 @@ export class DeliberationEngine implements CognitiveEngine {
219
219
  const what = c.description ? ` — ${ c.description }` : ''
220
220
  // Channel B: name the plan link so the facet chooses as the self pursuing it,
221
221
  // not blindly among labels ("this one is the next step of the plan I'm on").
222
- const plan = c.fromPlan ? " (your current plan's next step)" : ''
222
+ const plan = c.fromPlan ? " (my current plan's next step)" : ''
223
223
  lines.push( `${ i + 1 }. ${ c.schema }${ to }${ what }${ plan }` )
224
224
  })
225
225
  return lines.join( '\n' )
@@ -59,7 +59,7 @@ export function enact( ctx: EnactionContext ): Enaction {
59
59
  const name = str( ctx.parameters['targetEntityName'] ) ?? ctx.targetEntityId ?? 'them'
60
60
  return {
61
61
  mode, success: true, outcomeQuality: 0.7, valence: 0.1,
62
- description: `You reach toward ${ name }. The words are sent; their effect is not yet known.`,
62
+ description: `I reach toward ${ name }. The words are sent; their effect is not yet known.`,
63
63
  }
64
64
  }
65
65
 
@@ -81,26 +81,26 @@ function syncStance( ctx: EnactionContext ): Enaction {
81
81
 
82
82
  switch( schema.id ){
83
83
  case 'rest':
84
- // More restorative the more depleted you were.
85
- return sync( 0.5 + ( 1 - e01 ) * 0.4, 0.15, 'You let yourself recover; the pressure eases a little.' )
84
+ // More restorative the more depleted the Will was.
85
+ return sync( 0.5 + ( 1 - e01 ) * 0.4, 0.15, 'I let myself recover; the pressure eases a little.' )
86
86
  case 'withdraw':
87
- return sync( 0.5 + s01 * 0.3, 0.05 + s01 * 0.1, 'You pull back from the press of things; the world quietens.' )
87
+ return sync( 0.5 + s01 * 0.3, 0.05 + s01 * 0.1, 'I pull back from the press of things; the world quietens.' )
88
88
  case 'reflect':
89
- return sync( 0.6, 0.05, 'You turn inward; patterns from recent events settle into place.' )
89
+ return sync( 0.6, 0.05, 'I turn inward; patterns from recent events settle into place.' )
90
90
  case 'attend':
91
- return sync( 0.6, 0.0, 'You concentrate, mobilizing more of your attention.' )
91
+ return sync( 0.6, 0.0, 'I concentrate, mobilizing more of my attention.' )
92
92
  case 'orient':
93
- return sync( 0.5, 0.0, 'Your awareness sweeps the situation, taking its measure.' )
93
+ return sync( 0.5, 0.0, 'My awareness sweeps the situation, taking its measure.' )
94
94
  case 'wait':
95
- return sync( 0.5, 0.0, 'You let time pass; regulatory processes continue their quiet work.' )
95
+ return sync( 0.5, 0.0, 'I let time pass; regulatory processes continue their quiet work.' )
96
96
  case 'express':
97
- return sync( 0.6, 0.1, 'Your inner state becomes outwardly visible.' )
97
+ return sync( 0.6, 0.1, 'My inner state becomes outwardly visible.' )
98
98
  case 'inspect': {
99
99
  const focus = str( parameters['focus'] ) ?? 'it'
100
- return sync( 0.65, 0.05, `You examine ${ focus } closely; more of its detail resolves.` )
100
+ return sync( 0.65, 0.05, `I examine ${ focus } closely; more of its detail resolves.` )
101
101
  }
102
102
  default:
103
- return sync( 0.5, 0.0, `You enact ${ schema.id }.` )
103
+ return sync( 0.5, 0.0, `I enact ${ schema.id }.` )
104
104
  }
105
105
  }
106
106
 
@@ -77,12 +77,12 @@ export class ProactiveCommunicator {
77
77
  ): Promise<ActionResult> {
78
78
  return {
79
79
  success: true,
80
- description: `You open yourself to incoming communication. Others may now reach you through available channels.`,
80
+ description: `I open myself to incoming communication. Others may now reach me through available channels.`,
81
81
  commands,
82
82
  feedback: {
83
83
  outcomeQuality: 1.0,
84
84
  surprise: 0.05,
85
- lessons: [ 'Being reachable allows others to connect with you.' ],
85
+ lessons: [ 'Being reachable allows others to connect with me.' ],
86
86
  },
87
87
  }
88
88
  }
@@ -105,7 +105,7 @@ export class ProactiveCommunicator {
105
105
 
106
106
  return {
107
107
  success: true,
108
- description: `You ${gestureType} toward ${targetEntityId}. The gesture is directed and sincere.`,
108
+ description: `I ${gestureType} toward ${targetEntityId}. The gesture is directed and sincere.`,
109
109
  commands,
110
110
  feedback: {
111
111
  outcomeQuality: 0.8,
@@ -137,7 +137,7 @@ export class ProactiveCommunicator {
137
137
 
138
138
  return {
139
139
  success: true,
140
- description: `You broadcast: "${finalContent.slice( 0, 80 )}${finalContent.length > 80 ? '…' : ''}"`,
140
+ description: `I broadcast: "${finalContent.slice( 0, 80 )}${finalContent.length > 80 ? '…' : ''}"`,
141
141
  commands,
142
142
  feedback: {
143
143
  outcomeQuality: 0.75,
@@ -168,7 +168,7 @@ export class ProactiveCommunicator {
168
168
  if( !targetEntityId ){
169
169
  return {
170
170
  success: false,
171
- description: `You want to ${effectorName} but there is no one specific to reach out to.`,
171
+ description: `I want to ${effectorName} but there is no one specific to reach out to.`,
172
172
  commands,
173
173
  feedback: {
174
174
  outcomeQuality: 0,
@@ -181,7 +181,7 @@ export class ProactiveCommunicator {
181
181
  if( bubbles.length === 0 ){
182
182
  return {
183
183
  success: false,
184
- description: `You wanted to ${effectorName} ${targetEntityName} but didn't write anything.`,
184
+ description: `I wanted to ${effectorName} ${targetEntityName} but didn't write anything.`,
185
185
  commands,
186
186
  feedback: { outcomeQuality: 0, surprise: 0.1, lessons: [ 'Provide a messages array with the actual words.' ] },
187
187
  }
@@ -261,12 +261,12 @@ export class ProactiveCommunicator {
261
261
 
262
262
  return {
263
263
  success: true,
264
- description: `You reach out to ${targetEntityName}: "${fullReply.slice( 0, 80 )}${fullReply.length > 80 ? '…' : ''}"`,
264
+ description: `I reach out to ${targetEntityName}: "${fullReply.slice( 0, 80 )}${fullReply.length > 80 ? '…' : ''}"`,
265
265
  commands,
266
266
  feedback: {
267
267
  outcomeQuality: 0.85,
268
268
  surprise: 0.15,
269
- lessons: [ `Your message is queued for delivery to ${targetEntityName}.` ],
269
+ lessons: [ `My message is queued for delivery to ${targetEntityName}.` ],
270
270
  },
271
271
  }
272
272
  }
@@ -33,8 +33,8 @@ export function buildEngineConfigEntities( config: WillConfig, executiveInterval
33
33
  id: 'engine-config-system',
34
34
  engine: 'system',
35
35
  params: {
36
- engineTier: config.engineTier,
37
- modelTier: config.modelTier,
36
+ anatomy: config.anatomy ?? 'mind',
37
+ model: config.model ?? '',
38
38
  tickIntervalMs: config.tickIntervalMs ?? 1000
39
39
  }
40
40
  },
@@ -69,7 +69,7 @@ export function buildConversationExchange( input: ConversationExchangeInput ): C
69
69
  tags: [ 'conversation', 'exchange', `entity:${ entityId }` ],
70
70
  summary: userMessage
71
71
  ? `${ name }: "${ userMessage.slice( 0, 100 ) }" → "${ willReply.slice( 0, 100 ) }"`
72
- : `You → ${ name }: "${ willReply.slice( 0, 140 ) }"`,
72
+ : `I → ${ name }: "${ willReply.slice( 0, 140 ) }"`,
73
73
  entityId,
74
74
  entityName: name,
75
75
  userMessage,
@@ -97,21 +97,13 @@ export function buildStateCommands(
97
97
  commands.set!.push( ...ideo.set )
98
98
  commands.delete!.push( ...ideo.delete )
99
99
 
100
- // ── Apply Plans ────────────────────────────────────────────
101
- if( output.plans )
102
- for( const plan of output.plans )
103
- commands.set!.push({
104
- id: `plan-executive-${plan.goalId}-${footprint.tickObserved}`,
105
- type: 'plan',
106
- metadata: {
107
- goalId: plan.goalId,
108
- steps: plan.steps.map( ( s, i ) => ({ ...s, order: i }) ),
109
- estimatedCost: plan.estimatedCost,
110
- confidence: plan.feasibility,
111
- status: 'ready',
112
- source: 'executive'
113
- }
114
- })
100
+ // ── Plans ──────────────────────────────────────────────────
101
+ // No entity write here: the PlanningEngine is the single ingest path for
102
+ // `output.plans` ( PlanStore lifecycle + persistence). The former raw
103
+ // `plan-executive-<goal>-<tick>` records froze at status 'ready' forever,
104
+ // piled up per re-authoring, and fed back into the executive's own
105
+ // Active-Plans awareness as phantom drafts — encouraging yet another
106
+ // re-authoring. (The PlanningEngine sweeps legacy ones on wake.)
115
107
 
116
108
  // ── Apply New Beliefs ──────────────────────────────────────
117
109
  if( output.newBeliefs && deps.semanticIntegrator ){