slaude Docs

Field notes

2026-05-11 — structured-soul + channel-mode gate

  • Structured-soul shipped (v0.2.1). At boot, src/soul/extract.ts runs one ephemeral POST /v1/messages (no MCP, no tools) with RUNTIME_BASELINE + <persona> as system + an extraction prompt (src/soul/data.ts:EXTRACTION_PROMPT), parses the response as JSON, validates via zod (SoulDataSchema), and writes the result to $SLAUDE_HOME/cache/soul.<sha16>.json. sha16 = first 16 hex of sha256(SOUL.md) — operator edits SOUL → cache miss → re-extract next boot. Cache hits skip the LLM entirely. Fallback chain on extract failure: regex parser (loadApproverEntries / selectApprovers) so boot never blocks on provider availability.
  • Defense in depth on extraction: assertIdsGroundedInPersona() rejects the parsed SoulData if ANY Slack id (manager.userId, allowedChannels[], approvers[].userId) does not appear verbatim in SOUL.md. Stops the model from hallucinating an approver / whitelisted channel the operator never authorised. On reject → regex fallback, no cache write.
  • Approval gate now consumes soulData().approvers as tier 1; existing regex scope-tier / legacy category: ids / env fallback preserved. Logic stays in selectApproversFrom(entries, summary, hint?) — pure function shared between regex and structured paths. Identical behaviour when the LLM agrees with the regex parser; the win is robustness when persona prose drifts off the rigid bullet format.
  • BREAKING: allowedUsers / SLACK_ALLOWED_USERS removed entirely. Channel-mode engagement gate replaces it (adapter.ts):
    • Channel in soulData().allowedChannels → public zone, any user accepted.
    • Anything else (private channel manager added bot to ad-hoc, AND all DMs) → only soulData().manager.userId accepted. Approvers retain authority to click Approve / Deny on request_approval blocks but cannot chat.
    • env.slack.approvers() no longer falls back to SLACK_ALLOWED_USERS. Only SLAUDE_APPROVERS is honoured as env fallback when SOUL.md has no ## Approvers section.
  • soulData() memoization bug (caught in CI for v0.2.0, fixed in v0.2.1): the sync accessor memoized the regex fallback into module-level let memo, so the FIRST call locked the value for the process. Editing SOUL.md between calls (or between tests) returned stale state. Fix: only memoize what setSoulData() explicitly seeds (called once at boot in server.ts); re-derive cache-file/regex paths on every soulData() call. Tests now pass both as full suite and per-file.
  • zod regex landmine: ^[UW][A-Z0-9]{6,}$ rejected short synthetic test ids (U001, U002). Real Slack ids are 9–11 chars, but the strict floor was test-hostile and brittle if Slack ever ships shorter ids. Relaxed to ^[UW][A-Z0-9]+$ (and ^[CGD][A-Z0-9]+$ for channels). The grounding check (id-must-appear-in-persona) is the real safety net, not the length.
  • v0.2.0 tag was pushed with the failing tests; CI / release workflows failed but docker workflow built+published images anyway (it doesn't run tests). Took the forward-only route — v0.2.1 fixes the bugs, v0.2.0 has no GH release. Amended v0.2.1 notes to cover the full v0.1.1..v0.2.1 diff + migration guide so the gap is documented.
  • Live diagnostic: empty approver list in Slack was a data bug, not code — SOUL.md still held the company U0XXXXXXXXX id after the workspace pivot to personal-workspace. Slack rendered the unknown id as blank → looked like an empty allowlist. Fix was sed s/U0XXXXXXXXX/U0YYYYYYYYY/g ~/.slaude/SOUL.md + cache clear + restart. Lesson: workspace pivot needs a SOUL.md audit.