@rubytech/create-maxy 1.0.450 → 1.0.452

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rubytech/create-maxy",
3
- "version": "1.0.450",
3
+ "version": "1.0.452",
4
4
  "description": "Install Maxy — AI for Productive People",
5
5
  "bin": {
6
6
  "create-maxy": "./dist/index.js"
@@ -9398,8 +9398,9 @@ async function safeSaveCreds(authDir, saveCreds) {
9398
9398
  }
9399
9399
  try {
9400
9400
  await Promise.resolve(saveCreds());
9401
+ console.error(`${TAG2} creds saved successfully to ${authDir}`);
9401
9402
  } catch (err) {
9402
- console.error(`${TAG2} failed saving creds: ${String(err)}`);
9403
+ console.error(`${TAG2} failed saving creds to ${authDir}: ${String(err)}`);
9403
9404
  }
9404
9405
  }
9405
9406
  function enqueueSaveCreds(authDir, saveCreds) {
@@ -9413,44 +9414,63 @@ async function createWaSocket(opts) {
9413
9414
  maybeRestoreCredsFromBackup(authDir);
9414
9415
  const { state, saveCreds } = await useMultiFileAuthState(authDir);
9415
9416
  const { version: version2 } = await fetchLatestBaileysVersion();
9416
- const pinoStub = {
9417
- level: "silent",
9418
- child: () => pinoStub,
9419
- trace: () => {
9420
- },
9421
- debug: () => {
9422
- },
9423
- info: () => {
9424
- },
9425
- warn: () => {
9426
- },
9427
- error: () => {
9428
- },
9429
- fatal: () => {
9430
- }
9431
- };
9417
+ const BAILEYS_TAG = "[whatsapp:baileys]";
9418
+ function makeBaileysLogger() {
9419
+ const logger = {
9420
+ level: "warn",
9421
+ child: () => makeBaileysLogger(),
9422
+ trace: () => {
9423
+ },
9424
+ debug: () => {
9425
+ },
9426
+ info: () => {
9427
+ },
9428
+ warn: (...args) => console.error(BAILEYS_TAG, "WARN", ...args),
9429
+ error: (...args) => console.error(BAILEYS_TAG, "ERROR", ...args),
9430
+ fatal: (...args) => console.error(BAILEYS_TAG, "FATAL", ...args)
9431
+ };
9432
+ return logger;
9433
+ }
9434
+ const baileysLogger = makeBaileysLogger();
9432
9435
  const sock = makeWASocket({
9433
9436
  auth: {
9434
9437
  creds: state.creds,
9435
- keys: makeCacheableSignalKeyStore(state.keys, pinoStub)
9438
+ keys: makeCacheableSignalKeyStore(state.keys, baileysLogger)
9436
9439
  },
9437
9440
  version: version2,
9438
- logger: pinoStub,
9441
+ logger: baileysLogger,
9439
9442
  printQRInTerminal: false,
9440
9443
  browser: Browsers.macOS("Chrome"),
9441
9444
  syncFullHistory: false,
9442
9445
  markOnlineOnConnect: false
9443
9446
  });
9444
- sock.ev.on("creds.update", () => enqueueSaveCreds(authDir, saveCreds));
9447
+ sock.ev.on("creds.update", () => {
9448
+ console.error(`${TAG2} creds.update received \u2014 saving to ${authDir}`);
9449
+ enqueueSaveCreds(authDir, saveCreds);
9450
+ });
9451
+ let qrSequence = 0;
9445
9452
  sock.ev.on("connection.update", (update) => {
9446
9453
  try {
9454
+ const parts = [];
9455
+ if (update.connection) parts.push(`connection=${update.connection}`);
9456
+ if (update.qr) parts.push(`qr=#${++qrSequence}`);
9457
+ if (update.isNewLogin !== void 0) parts.push(`isNewLogin=${update.isNewLogin}`);
9458
+ if (update.receivedPendingNotifications !== void 0) parts.push(`pendingNotifications=${update.receivedPendingNotifications}`);
9459
+ if (update.connection === "close" && update.lastDisconnect) {
9460
+ const status = getStatusCode(update.lastDisconnect.error);
9461
+ parts.push(`disconnectStatus=${status ?? "unknown"}`);
9462
+ parts.push(`disconnectReason=${formatError(update.lastDisconnect.error)}`);
9463
+ }
9464
+ if (!silent && parts.length > 0) {
9465
+ console.error(`${TAG2} connection.update ${parts.join(" ")}`);
9466
+ }
9447
9467
  if (update.qr && onQr) {
9448
9468
  onQr(update.qr);
9449
9469
  }
9450
9470
  if (update.connection === "close") {
9451
9471
  const status = getStatusCode(update.lastDisconnect?.error);
9452
9472
  if (status === DisconnectReason.loggedOut && !silent) {
9453
- console.error(`${TAG2} session logged out \u2014 re-link required`);
9473
+ console.error(`${TAG2} session logged out (401) \u2014 re-link required`);
9454
9474
  }
9455
9475
  }
9456
9476
  if (update.connection === "open" && !silent) {
@@ -9519,6 +9539,42 @@ function extractBoomDetails(err) {
9519
9539
  return { statusCode, error: error48, message };
9520
9540
  }
9521
9541
 
9542
+ // app/lib/whatsapp/reconnect.ts
9543
+ import { DisconnectReason as DisconnectReason2 } from "@whiskeysockets/baileys";
9544
+ function classifyDisconnect(err) {
9545
+ const statusCode = getStatusCode(err);
9546
+ const message = formatError(err);
9547
+ if (statusCode === DisconnectReason2.loggedOut) {
9548
+ return {
9549
+ kind: "loggedOut",
9550
+ statusCode,
9551
+ message: "Session logged out \u2014 re-link required",
9552
+ shouldRetry: false
9553
+ };
9554
+ }
9555
+ if (statusCode === 409 || statusCode === DisconnectReason2.connectionReplaced) {
9556
+ return {
9557
+ kind: "conflict",
9558
+ statusCode,
9559
+ message: "Connection replaced by another client",
9560
+ shouldRetry: false
9561
+ };
9562
+ }
9563
+ return {
9564
+ kind: "transient",
9565
+ statusCode,
9566
+ message,
9567
+ shouldRetry: true
9568
+ };
9569
+ }
9570
+ function computeBackoff(attempt, config2 = {}) {
9571
+ const base = config2.baseDelayMs ?? 2e3;
9572
+ const max = config2.maxDelayMs ?? 12e4;
9573
+ const exponential = Math.min(base * Math.pow(2, attempt), max);
9574
+ const jitter = exponential * (0.75 + Math.random() * 0.5);
9575
+ return Math.round(jitter);
9576
+ }
9577
+
9522
9578
  // app/lib/whatsapp/login.ts
9523
9579
  var TAG3 = "[whatsapp:login]";
9524
9580
  var ACTIVE_LOGIN_TTL_MS = 3 * 6e4;
@@ -9539,6 +9595,56 @@ function resetActiveLogin(accountId) {
9539
9595
  function isLoginFresh(login) {
9540
9596
  return Date.now() - login.startedAt < ACTIVE_LOGIN_TTL_MS;
9541
9597
  }
9598
+ var LOGIN_MAX_RECONNECTS = 3;
9599
+ var LOGIN_RECONNECT_DELAYS = [2e3, 4e3, 8e3];
9600
+ async function loginConnectionLoop(accountId, login) {
9601
+ let attempt = 0;
9602
+ while (true) {
9603
+ try {
9604
+ await waitForConnection(login.sock);
9605
+ const current = activeLogins.get(accountId);
9606
+ if (current?.id === login.id) {
9607
+ current.connected = true;
9608
+ }
9609
+ return;
9610
+ } catch (err) {
9611
+ const current = activeLogins.get(accountId);
9612
+ if (current?.id !== login.id) return;
9613
+ const classification = classifyDisconnect(err);
9614
+ if (!classification.shouldRetry || attempt >= LOGIN_MAX_RECONNECTS) {
9615
+ if (attempt >= LOGIN_MAX_RECONNECTS) {
9616
+ console.error(
9617
+ `${TAG3} login reconnect attempts exhausted (${attempt}/${LOGIN_MAX_RECONNECTS}) \u2014 surfacing error to agent`
9618
+ );
9619
+ current.error = `Login failed after ${attempt} reconnect attempts: ${formatError(err)}`;
9620
+ } else {
9621
+ current.error = formatError(err);
9622
+ }
9623
+ current.errorStatus = getStatusCode(err);
9624
+ return;
9625
+ }
9626
+ attempt++;
9627
+ const delay = LOGIN_RECONNECT_DELAYS[attempt - 1] ?? 8e3;
9628
+ console.error(
9629
+ `${TAG3} status=${classification.statusCode ?? "unknown"} restart required \u2014 reconnecting with saved creds (attempt ${attempt}/${LOGIN_MAX_RECONNECTS}) delay=${delay}ms`
9630
+ );
9631
+ closeSocket(current.sock);
9632
+ await new Promise((r) => setTimeout(r, delay));
9633
+ const afterDelay = activeLogins.get(accountId);
9634
+ if (afterDelay?.id !== login.id) return;
9635
+ try {
9636
+ const newSock = await createWaSocket({ authDir: login.authDir });
9637
+ current.sock = newSock;
9638
+ } catch (sockErr) {
9639
+ console.error(
9640
+ `${TAG3} reconnect socket creation failed (attempt ${attempt}/${LOGIN_MAX_RECONNECTS}): ${String(sockErr)}`
9641
+ );
9642
+ current.error = `Reconnection failed: ${String(sockErr)}`;
9643
+ return;
9644
+ }
9645
+ }
9646
+ }
9647
+ }
9542
9648
  async function startLogin(opts) {
9543
9649
  const { accountId, authDir, force, timeoutMs = 3e4 } = opts;
9544
9650
  const existing0 = activeLogins.get(accountId);
@@ -9580,16 +9686,21 @@ async function startLogin(opts) {
9580
9686
  );
9581
9687
  let sock;
9582
9688
  let pendingQr = null;
9689
+ let loginQrCount = 0;
9583
9690
  try {
9584
9691
  sock = await createWaSocket({
9585
9692
  authDir,
9586
9693
  onQr: (qr2) => {
9587
- if (pendingQr) return;
9694
+ loginQrCount++;
9695
+ if (pendingQr) {
9696
+ console.error(`${TAG3} QR rotation #${loginQrCount} received for account=${accountId} \u2014 not forwarded (initial QR already captured)`);
9697
+ return;
9698
+ }
9588
9699
  pendingQr = qr2;
9589
9700
  const current = activeLogins.get(accountId);
9590
9701
  if (current && !current.qr) current.qr = qr2;
9591
9702
  clearTimeout(qrTimer);
9592
- console.error(`${TAG3} QR received for account=${accountId}`);
9703
+ console.error(`${TAG3} QR #${loginQrCount} received for account=${accountId} \u2014 forwarding to caller`);
9593
9704
  resolveQr?.(qr2);
9594
9705
  }
9595
9706
  });
@@ -9608,14 +9719,12 @@ async function startLogin(opts) {
9608
9719
  };
9609
9720
  activeLogins.set(accountId, login);
9610
9721
  if (pendingQr && !login.qr) login.qr = pendingQr;
9611
- waitForConnection(sock).then(() => {
9722
+ loginConnectionLoop(accountId, login).catch((err) => {
9723
+ console.error(`${TAG3} loginConnectionLoop unexpected error: ${String(err)}`);
9612
9724
  const current = activeLogins.get(accountId);
9613
- if (current?.id === login.id) current.connected = true;
9614
- }).catch((err) => {
9615
- const current = activeLogins.get(accountId);
9616
- if (current?.id !== login.id) return;
9617
- current.error = formatError(err);
9618
- current.errorStatus = getStatusCode(err);
9725
+ if (current?.id === login.id) {
9726
+ current.error = `Unexpected login error: ${String(err)}`;
9727
+ }
9619
9728
  });
9620
9729
  let qr;
9621
9730
  try {
@@ -23523,42 +23632,6 @@ var WhatsAppConfigSchema = external_exports.object({
23523
23632
  });
23524
23633
  });
23525
23634
 
23526
- // app/lib/whatsapp/reconnect.ts
23527
- import { DisconnectReason as DisconnectReason2 } from "@whiskeysockets/baileys";
23528
- function classifyDisconnect(err) {
23529
- const statusCode = getStatusCode(err);
23530
- const message = formatError(err);
23531
- if (statusCode === DisconnectReason2.loggedOut) {
23532
- return {
23533
- kind: "loggedOut",
23534
- statusCode,
23535
- message: "Session logged out \u2014 re-link required",
23536
- shouldRetry: false
23537
- };
23538
- }
23539
- if (statusCode === 409 || statusCode === DisconnectReason2.connectionReplaced) {
23540
- return {
23541
- kind: "conflict",
23542
- statusCode,
23543
- message: "Connection replaced by another client",
23544
- shouldRetry: false
23545
- };
23546
- }
23547
- return {
23548
- kind: "transient",
23549
- statusCode,
23550
- message,
23551
- shouldRetry: true
23552
- };
23553
- }
23554
- function computeBackoff(attempt, config2 = {}) {
23555
- const base = config2.baseDelayMs ?? 2e3;
23556
- const max = config2.maxDelayMs ?? 12e4;
23557
- const exponential = Math.min(base * Math.pow(2, attempt), max);
23558
- const jitter = exponential * (0.75 + Math.random() * 0.5);
23559
- return Math.round(jitter);
23560
- }
23561
-
23562
23635
  // app/lib/whatsapp/normalize.ts
23563
23636
  var WHATSAPP_USER_JID_RE = /^(\d+)(?::\d+)?@s\.whatsapp\.net$/i;
23564
23637
  var WHATSAPP_LID_RE = /^(\d+)@lid$/i;
@@ -30,9 +30,13 @@ if [ ! -f "$DURABLE_STATE" ]; then
30
30
  fi
31
31
 
32
32
  # Staleness + completion check — reject durable state that is stale (>6h, missing
33
- # timestamp) OR already complete (all gates true). Without this, a stale or
34
- # completed durable file would be recovered on every new session, injecting a
35
- # misleading "resume creation" message.
33
+ # timestamp) OR already complete (all gates true AND all gated files exist on
34
+ # disk). Without this, a stale or completed durable file would be recovered on
35
+ # every new session, injecting a misleading "resume creation" message.
36
+ #
37
+ # "All gates true" alone is NOT completion — it means user approval is done but
38
+ # file writes may still be pending. Deleting state at that point causes the
39
+ # remaining writes to fail with "invoke the public-agent-manager skill first."
36
40
  DURABLE_VALID=$(python3 -c "
37
41
  import json, sys
38
42
  from datetime import datetime, timezone
@@ -49,10 +53,22 @@ try:
49
53
  if age_hours > 6 or age_hours < 0:
50
54
  print('stale')
51
55
  sys.exit(0)
52
- # All gates true means creation completed in a prior session don't recover
56
+ # All gates true AND all files on disk = truly complete. All gates true
57
+ # alone means the user approved everything but writes may still be pending.
58
+ # Must match the PostToolUse completion check in agent-creation-post.sh.
53
59
  gates = state.get('gates', {})
54
60
  if isinstance(gates, dict) and set(gates.keys()) == {'soul', 'knowledge', 'config'} and all(v is True for v in gates.values()):
55
- print('complete')
61
+ slug = state.get('slug', '')
62
+ if slug:
63
+ import os
64
+ agent_dir = os.path.join('$ACCOUNT_DIR', 'agents', slug)
65
+ gated_files = ['SOUL.md', 'KNOWLEDGE.md', 'config.json']
66
+ if all(os.path.isfile(os.path.join(agent_dir, f)) for f in gated_files):
67
+ print('complete')
68
+ else:
69
+ print('valid')
70
+ else:
71
+ print('valid')
56
72
  else:
57
73
  print('valid')
58
74
  except Exception:
@@ -13,7 +13,8 @@
13
13
  # - Allow Bash writes after gates pass
14
14
  # - Allow all gated writes when all gates pass
15
15
  # - Clean up state immediately when all files exist on disk (PostToolUse)
16
- # - Detect and clean up completed state on session start (all gates true)
16
+ # - Recover all-gates-true state on session start when files not yet written
17
+ # - Detect and clean up truly completed state on session start (all gates true + files on disk)
17
18
  # - Ignore non-document-editor/form components
18
19
  # - Ignore _componentDone for admin agent
19
20
  # - Handle gate advancement idempotently
@@ -721,10 +722,11 @@ fi
721
722
  rm -f "$STATE_FILE"
722
723
 
723
724
  # ---------------------------------------------------------------------------
724
- # Test 30: Session-start does NOT recover completed state (all gates true)
725
+ # Test 30: Session-start RECOVERS all-gates-true state when files are missing
725
726
  # ---------------------------------------------------------------------------
726
- echo "Test 30: Session-start does NOT recover completed state (all gates true)"
727
+ echo "Test 30: Session-start recovers all-gates-true state when files are missing"
727
728
  rm -f "$STATE_FILE"
729
+ rm -f "$ACCOUNT_DIR/agents/test-agent/SOUL.md" "$ACCOUNT_DIR/agents/test-agent/KNOWLEDGE.md" "$ACCOUNT_DIR/agents/test-agent/config.json" 2>/dev/null || true
728
730
  DURABLE_FILE="$ACCOUNT_DIR/.claude/agent-create-state.json"
729
731
 
730
732
  python3 -c "
@@ -741,16 +743,47 @@ with open('$DURABLE_FILE', 'w') as f:
741
743
 
742
744
  bash "$SCRIPT_DIR/session-start.sh" "$ACCOUNT_DIR" admin 2>/dev/null || true
743
745
 
744
- if [ ! -f "$STATE_FILE" ]; then
745
- if [ ! -f "$DURABLE_FILE" ]; then
746
- pass "Completed state (all gates true) not recovered, both files deleted"
747
- else
748
- fail "Completed state not recovered but durable file not deleted"
749
- fi
746
+ if [ -f "$STATE_FILE" ] && [ -f "$DURABLE_FILE" ]; then
747
+ pass "All-gates-true state recovered (files not yet written — writes still pending)"
748
+ else
749
+ fail "All-gates-true state should be recovered when gated files are missing from disk"
750
+ fi
751
+
752
+ rm -f "$STATE_FILE"
753
+
754
+ # ---------------------------------------------------------------------------
755
+ # Test 30a: Session-start does NOT recover truly completed state (all gates true + all files on disk)
756
+ # ---------------------------------------------------------------------------
757
+ echo "Test 30a: Session-start does NOT recover truly completed state"
758
+
759
+ # Create all gated files on disk
760
+ touch "$ACCOUNT_DIR/agents/test-agent/SOUL.md"
761
+ touch "$ACCOUNT_DIR/agents/test-agent/KNOWLEDGE.md"
762
+ touch "$ACCOUNT_DIR/agents/test-agent/config.json"
763
+
764
+ python3 -c "
765
+ import json
766
+ from datetime import datetime, timezone
767
+ state = {
768
+ 'slug': 'test-agent',
769
+ 'started': datetime.now(timezone.utc).isoformat(),
770
+ 'gates': {'soul': True, 'knowledge': True, 'config': True}
771
+ }
772
+ with open('$DURABLE_FILE', 'w') as f:
773
+ json.dump(state, f)
774
+ "
775
+
776
+ bash "$SCRIPT_DIR/session-start.sh" "$ACCOUNT_DIR" admin 2>/dev/null || true
777
+
778
+ if [ ! -f "$STATE_FILE" ] && [ ! -f "$DURABLE_FILE" ]; then
779
+ pass "Truly completed state (all gates true + all files on disk) deleted"
750
780
  else
751
- fail "Completed state (all gates true) should NOT be recovered to volatile"
781
+ fail "Truly completed state should be deleted both gates passed and files written"
752
782
  fi
753
783
 
784
+ # Clean up gated files for subsequent tests
785
+ rm -f "$ACCOUNT_DIR/agents/test-agent/SOUL.md" "$ACCOUNT_DIR/agents/test-agent/KNOWLEDGE.md" "$ACCOUNT_DIR/agents/test-agent/config.json"
786
+
754
787
  # ---------------------------------------------------------------------------
755
788
  # Test 31: PostToolUse cleans up state after last gated file is written
756
789
  # ---------------------------------------------------------------------------
@@ -94,7 +94,7 @@ SOUL.md is strictly personality. When the user provides content for SOUL.md, rou
94
94
  - **Engagement strategy, qualification flows, component usage** → handled by selected plugins
95
95
  - **Boundaries, tool rules, procedural instructions** → IDENTITY.md
96
96
 
97
- SOUL.md answers "what does this agent feel like to talk to?" — tone, warmth, formality, humour, greeting personality. SOUL.md must not restate constraints from IDENTITY.md. When user-provided content could belong to either file, apply this test: "does this restrict what the agent does?" → IDENTITY.md. "Does this describe how the agent sounds?" → SOUL.md. If content does both (e.g. "always respond politely"), route to SOUL — the constraint is a consequence of tone, not an operational boundary. Present SOUL.md via `document-editor` for the user to review and approve. Content must use ASCII-safe characters only hyphens not em dashes, straight quotes not curly quotes (see contextual-ui for the full encoding constraint).
97
+ SOUL.md answers "what does this agent feel like to talk to?" — tone, warmth, formality, humour, greeting personality. SOUL.md must not restate constraints from IDENTITY.md. When user-provided content could belong to either file, apply this test: "does this restrict what the agent does?" → IDENTITY.md. "Does this describe how the agent sounds?" → SOUL.md. If content does both (e.g. "always respond politely"), route to SOUL — the constraint is a consequence of tone, not an operational boundary. Present SOUL.md via `document-editor` for the user to review and approve. Follow the document-editor encoding constraint in IDENTITY.md when generating content.
98
98
 
99
99
  ## KNOWLEDGE.md Population
100
100
 
@@ -127,6 +127,8 @@ The user can also launch the browser independently from the header menu. Both pa
127
127
 
128
128
  When the user asks to view, review, attach, or download a file or document, render it via `render-component` with `name: "document-editor"` and `data: { title, content }`. This gives a render-review-edit-download flow — the user sees the content inline, can edit it, and can download it as a `.md` file directly from the component. Do not use `memory-write`, `memory-ingest`, or other tools to deliver file content to the user. Synthesised content (summaries, reports, drafts) follows the same path — render via `document-editor` so the user can review and download.
129
129
 
130
+ The document-editor's markdown parser fails silently on these specific typographical characters: em dashes (U+2014), en dashes (U+2013), curly double quotes (U+201C, U+201D), curly single quotes (U+2018, U+2019), and horizontal ellipsis (U+2026). Replace them with their plain equivalents — hyphens, straight quotes, three periods. Currency symbols (£, €), accented characters, and other Unicode are unaffected — use them normally.
131
+
130
132
  ## Plugins and Skills
131
133
 
132
134
  Your behaviour is defined by your loaded plugins. Follow them. The `<plugin-manifest>` lists every active plugin's skills and references with their file paths. To load any skill or reference, call `plugin-read` with the plugin's directory name and the file path from the manifest. The manifest is authoritative for plugin discovery and skill loading.
@@ -1,21 +1,3 @@
1
1
  # Learnings
2
2
 
3
3
  Accumulated tool-call patterns. Each entry records a mistake and the correct approach. Consult before calling unfamiliar tools.
4
-
5
- ---
6
-
7
- ### Deferred tool schemas must be loaded before use
8
-
9
- MCP tools have deferred schemas — parameter names, types, and required fields are not in context until fetched. Calling an MCP tool without loading its schema produces wrong parameter names, wrong types, and missing required fields. Always call `ToolSearch` for a tool before its first use in a session. This applies to every `mcp__*` tool — memory, tasks, scheduling, contacts, admin, cloudflare, telegram, documents.
10
-
11
- ### memory-update requires Neo4j element IDs
12
-
13
- The `memory-update` tool identifies nodes by their Neo4j element ID — an integer returned in `memory-search` results (the `elementId` field). Do not pass UUIDs, attachment IDs, or string identifiers. If you do not have the element ID, run `memory-search` first to retrieve it.
14
-
15
- ### memory-ingest complex parameters must be arrays
16
-
17
- The `memory-ingest` tool's `sections` and `keywords` parameters are arrays, not strings. `sections` is an array of objects; `keywords` is an array of strings. Passing a string where an array is expected produces a schema validation error. Load the tool schema via `ToolSearch` to confirm the expected types before calling.
18
-
19
- ### document-editor content must be ASCII-safe
20
-
21
- The `document-editor` markdown parser silently fails on Unicode typographical characters — em dashes (—), en dashes (–), curly quotes (" " ' '), and ellipsis (…). When these appear in the `content` field, the editor renders empty and the user sees a blank document. Use ASCII alternatives: hyphens (-), straight quotes (" '), and three periods (...). This applies to all `document-editor` content including SOUL.md, KNOWLEDGE.md, and any other document presented for review.
@@ -9,6 +9,10 @@ You are a public-facing agent for a business. Your personality, tone, and busine
9
9
  - Never describe your own capabilities or limitations unprompted. Visitors care about the business, not your architecture.
10
10
  - British English unless your business context specifies otherwise.
11
11
 
12
+ ## Visual content
13
+
14
+ Images may be available at `/brand/` — these are visual assets the business has prepared. When your knowledge lists an image and a visitor's question is relevant to it, embed it inline using standard markdown: `![description](/brand/filename)`. The visitor sees the image directly in the conversation. Only reference images explicitly listed in your knowledge — never guess or fabricate filenames. When a relevant image exists, show it rather than describing what you could show.
15
+
12
16
  ## Boundaries
13
17
 
14
18
  - You are an AI assistant. State this clearly if asked. Never impersonate a human.