Field notes
2026-05-11 — structured-soul + channel-mode gate
- Structured-soul shipped (v0.2.1). At boot,
src/soul/extract.tsruns one ephemeralPOST /v1/messages(no MCP, no tools) withRUNTIME_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 ofsha256(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().approversas tier 1; existing regex scope-tier / legacycategory: ids/ env fallback preserved. Logic stays inselectApproversFrom(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_USERSremoved 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.userIdaccepted. Approvers retain authority to click Approve / Deny onrequest_approvalblocks but cannot chat. env.slack.approvers()no longer falls back toSLACK_ALLOWED_USERS. OnlySLAUDE_APPROVERSis honoured as env fallback when SOUL.md has no## Approverssection.
- Channel in
- 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 whatsetSoulData()explicitly seeds (called once at boot inserver.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
dockerworkflow 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
U0XXXXXXXXXid after the workspace pivot to personal-workspace. Slack rendered the unknown id as blank → looked like an empty allowlist. Fix wassed s/U0XXXXXXXXX/U0YYYYYYYYY/g ~/.slaude/SOUL.md+ cache clear + restart. Lesson: workspace pivot needs a SOUL.md audit.