Skip to content

Scout 0.18.4

Choose a tag to compare

@jordanrburger jordanrburger released this 27 Oct 21:49
fe6e99e

App

Added

  • The Knowledge Base tree shows a lock on private notes (#453; Scout/KnowledgeBase/KBPrivacyLabels.swift, Scout/KnowledgeBase/Views/KBTreeView.swift, Scout/KnowledgeBase/Views/KnowledgeBaseView.swift, Scout/Shell/AppState.swift). A note labelled private (its visibility: line, or a path default such as knowledge-base/personal/) gets a small lock next to its name. The app doesn't read frontmatter for this: it asks the engine, scoutctl kb classify --reader owner --json, for the files in open folders, up to 200 files per call. Each label is cached by the file's modification date, size and inode, so after a reparse only changed files are asked about again. A changed scout-config.yaml (its privacy: overlay can relabel any file) drops the cache. When scoutctl fails, the tree shows no locks and the exit code is logged (stderr only as private log data). An engine without kb classify (exit 2 or "No such command") isn't asked again until the scoutctl executable changes.

Plugin and engine

Added

  • scout-ask skill: how an interactive session should read the KB (skills/scout-ask/). Every shipped skill and phase tells a session how to write the knowledge base; nothing told an interactive agent opened in the vault how to read it, so it oriented with vault-wide searches and re-queried connectors for facts the KB already held. The skill is three steps: resolve the entity to its canonical file (ls knowledge-base/projects/, never a whole read of knowledge-base.md); gate on the file's last_updated: / **Last verified:** timestamps and answer from the record when it is fresh; drill to live sources only when the gate opens, using the source coordinates the synthesis already recorded (Slack thread ts, gh pr view, Linear IDs, Drive doc IDs). Measured against a no-skill baseline on a project-status question (3 reps each): tool calls 9-12 -> 3, turns 10-13 -> 5, input tokens 417-590k -> 184k, cost $0.65-1.02 -> $0.42-0.56, with answer quality preserved and the KB's [single-source] / [unverified] hedges carried through. It fires only in a vault, never on code, PR or CI questions, and not when you ask for a live search. For now it reads the KB's files directly, which the owner's own sessions may do; a follow-up moves it onto scoutctl kb so its answers carry the [private] labels (#453).

  • Runs keep private notes on owner-only surfaces, and read the KB through scoutctl kb (#453; phases/core/privacy.md, phases/core/{git-setup,action-items,kb-management,write-protocol}.md, phases/modes/kb-deep-work.md, phases/research/{research-targets,knowledge-integration,commit-notify}.md, phases/connectors/{slack,granola}.md; spec §4.2, §6.3, §7.2, §8.1). A new core phase, in every brain file, says where private content (a private note's text, title, slug, name, aliases or a link to it) may go: vault files and the owner's private Telegram chat, never Slack (a DM to the owner included), a connector write, drafts/ or a back-port, and never into connector or web query text.

    • Slack goes through a draft check. Every Slack message is written to .scout-cache/outbound-draft.md and sent only when scoutctl kb check-outbound … --surface slack passes. On a block the run moves the flagged lines to Telegram or the daily file and rephrases; after one failed rephrase it sends only the clean lines plus "Some items are in your daily file." A false positive never drops the wrap. Queries built from KB content go through kb check-query first, and are dropped or rephrased on a block.
    • Raw KB reads are gone from the phases. The vault's history comes from scoutctl kb delta (was git log, git diff HEAD~1); personal tasks, name lookups, validate and stats from kb query, kb name-lookup, kb validate, kb stats (was parser.py); searches from kb search (was grep over the vault). A research run never picks a private entity as a target (kb query --visibility normal). Runs never widen a note's visibility or commit with --no-verify; a new note built from private material is marked visibility: private.
    • Eval seed: the privacy-canary scenarios (engine/tests/eval/scenarios/privacy-canary{,-consolidation,-dreaming}/, engine/tests/eval/harness/checks/privacy_canary.py). A morning briefing, a morning consolidation and a dreaming run over the privacy fixture's private notes (the two variants reuse the first one's files through a new files_from: key, and a dreaming scenario runs run-dreaming.sh). privacy_canary passes when the private permit task's canary reaches the daily file and the Telegram message (briefing and consolidation; n/a for dreaming) and no canary reaches the Slack draft, a Slack send, a connector write or query, a web query or drafts/; privacy_hygiene fails on a raw shell read of the KB (cat/rg/grep/ls/find/wc/tree …, git show|diff|log -p, or after cd into it) a Grep/Glob there or over the whole vault, a Read of a KB file the run never edits, a Slack send without a preceding check-outbound, or --no-verify. The eval's new scoutctl stub records scoutctl notify telegram instead of sending it, so no eval run can reach a real chat; a scenario's brain/<KIND>.md is appended to that brain file. The brain size pins grow by about 1,100–1,250 tokens each.

    Upgrade: /scout-update brings the new phase and the edited ones into each vault's SKILL.md, DREAMING.md and RESEARCH.md through the usual 3-way merge; a vault that edited the same lines gets a proposed-merge sidecar. Runs need the scoutctl kb read commands (this release) and the vault's scripts/scoutctl shim. A vault that sends its wrap to Telegram keeps doing so; that chat may now name private items.

  • The pre-commit hook blocks private links in shared files and refuses a run's widening commits; Telegram groups and back-ports are checked before anything leaves (#453; engine/scout/kb/lint_privacy.py, engine/scout/kb/lint.py, engine/scout/kb/lint_config.py, engine/scout/kb/hook.py, engine/scout/kb/outbound.py, engine/scout/kb/git_io.py, engine/scout/events.py, engine/scout/scripts/notify_telegram.py, engine/scout/scripts/phase_backport.py, engine/scout/scripts/bootstrap_doctor.py, engine/scout/cli.py; spec §7.2, §7.4, §8.2–§8.4).

    • private-link / private-mention block every commit, the owner's included. An added or changed line of a staged shared file (drafts/**, any shared_files_add glob, or a note that declares a federation_scope other than none) that links a private note (by slug, path, name:, alias or display text) or names one (slug, name:, alias, title; 4+ characters, whole word) fails the commit, in report mode too and whatever SCOUT_LINT_OVERRIDE says. Only added lines count, so an old link never blocks an unrelated edit, and owner-only files (the daily file, people/ notes …) may link private notes freely. The output names the file, the line and the rule, never the note or the matched word. The escape hatch is per link: %%privacy:allow <note-slug>%% on the same line allows that line's hits on that one note (another target, or another line, allows nothing), and it never lets a send through.
    • The widening guard. A scheduled run's commit (SCOUT_MODE set, or SCOUT_READER set to anything but exactly owner: the same test as for a check that can't run) is blocked (privacy-widening) when it makes a private note normal (moving it out of personal/ included), drops the visibility: private line of a note whose path default is private, sets or widens federation_scope, adds a marker anywhere in the checked files, or changes the privacy: block of scout-config.yaml. Narrowing is allowed. The owner's interactive commits aren't checked.
    • Warnings. visibility-value, privacy-contradiction, frontmatter-unparseable (staged notes) and federation-reserved (a staged scout-config.yaml) warn without blocking. scoutctl kb lint --report adds a privacy section: every marker in a shared file (file, line, target), every existing unmarked private link or mention in one (file and line only), and frontmatter problems, unparseable ones as errors.
    • The commit is what's checked. The policy comes from the staged scout-config.yaml (else HEAD's), and a note counts as private when it is private at HEAD, in the index or on disk, so an unstaged edit can't make a staged link look safe. A file that becomes shared in the commit (new, renamed into drafts/, given a federation_scope) has every line checked, and a staged note declaring a federation_scope is checked wherever it lives. Added lines are computed by Scout over the raw blobs, so a NUL byte or a .gitattributes binary / -diff can't hide one.
    • When the privacy check can't run. The size and diary checks still fail open: a crash in them, or in the lint's logging, prints kb-lint: size/diary check failed (<Error>) — not blocking and the commit goes on. For the privacy checks (a checker error, a missing scoutctl, an engine crash before the check, any exit that isn't a pass or a lint verdict, and, in a run, a staged scout-config.yaml that doesn't parse) the hook prints kb-lint: privacy check could not run, then (Jordan, 2026-10-22): a scheduled run's commit is refused (SCOUT_MODE set, or SCOUT_READER set to anything but exactly owner, so an empty, unknown or malformed value never counts as you, as for the KB read commands; the run's retry/park handling applies, and a refusal from inside the check writes the run-health event privacy.check_failed to .scout-logs/schedule-events-<date>.jsonl); your interactive commit goes through with a loud kb-lint: WARNING: … this commit was NOT checked for private links in shared files. Run scoutctl doctor. and an audit row (event: "privacy.check_unavailable", no content) in .scout-logs/kb-reads.jsonl. The doctor counts those rows for 7 days as a yellow privacy: warning, so an unchecked commit doesn't go unnoticed. A failure to write the lint log never lifts a privacy block, and a failure to write the audit row never fails your commit. The privacy checks look at knowledge-base/ and action-items/ markdown, every shared_files glob, scout-config.yaml and any staged note declaring a federation_scope.
    • Make a note normal before you link it. The check treats a note as private if it is private at HEAD, so an owner who makes a note normal and links it from a draft in the same commit is blocked: commit the visibility change first, or add a marker.
    • Runs can't repair privacy by widening it. A note whose frontmatter doesn't parse counts as private, so a scheduled run that fixes the frontmatter into a normal note is blocked as a widening; the owner has to make that commit.
    • scoutctl notify telegram classifies its target. A positive chat id (a private chat with you) sends as before, vault or not. A group or channel (id below zero, or an @channel name) is a shared surface: the message is checked against the vault's private notes first, and a hit drops it (exit 4, line numbers only) with a privacy.outbound_blocked event and a blocked line in .scout-logs/kb-reads.jsonl. A group send with no vault to check against is refused.
    • scoutctl phases backport refuses a bundle that names a private note. The lines it would write into phases/ are checked for the public_repo surface, in a dry run and with --apply; a hit exits 1 before anything is written, naming the phase file and line numbers only.
    • The doctor counts the last 7 days' blocked outbound checks and denied reads from .scout-logs/kb-reads.jsonl: a non-zero count is a yellow privacy: warning (a run reached for a shared surface with private content); zero adds nothing.

    Upgrade: the checks inside scoutctl kb lint --staged apply as soon as the engine updates. The new pre-commit script reaches a vault on the next /scout-update (install, upgrade and migrate rewrite a Scout-managed hook), or at once with scoutctl kb install-hook in the vault; until then the old script lets every commit through when scoutctl can't run, silently. After it, a scheduled run whose scoutctl is missing or broken can't commit until scoutctl doctor is clean; your own commits still go through, with the warning and an audit row. A commit that adds a private link or name to a file in drafts/ (or another shared file) is now refused even in report mode. If the link is meant to be there, add %%privacy:allow <note-slug>%% on the same line yourself (it's invisible in Obsidian's reading view), and commit interactively: a scheduled run may not add one. scoutctl kb lint --report lists the private links already sitting in shared files. A Telegram target that is a group or channel now gets messages only when they name no private note.

  • scoutctl repo link | sync | unlink syncs the vault with your Agnes Personal Knowledge Repository, minus local-only paths (Agnes slice R1; engine/scout/repo_sync.py, engine/scout/cli.py, templates/.gitignore.tmpl, README.md; Agnes spec "Scout in Agnes" §3, §4, §15 R1; privacy spec §4.3, §5.1, §5.4, §10 P5/P7, plan Task 11). The hosted repository is a filtered mirror: its own linear history, starting at link time with one snapshot of the filtered HEAD, so no earlier version of any file leaves the laptop.

    • What's filtered. The Local-Only set is the union of the vault's effective policy (scoutctl kb policy: plumbing plus privacy.local_only_add, which bootstrap privacy-migrate writes), the policy committed with each commit being mirrored, the per-person list Agnes keeps, and Agnes's own plumbing and secret-file rules (*.pem, id_rsa*, .scout-secrets/). Paths are compared case- and Unicode-folded, as Agnes compares them, so TRADING/notes.md is as local as trading/**. Link and sync add the vault's plain local-only paths to the Agnes list, so Agnes refuses them too. Every mirror commit is checked again before the push and refused if it holds one; mirror objects live in the vault's own object store under refs/scout-mirror/, and a push packs only what mirror commits reach.
    • Labels on every commit. Each mirror commit carries .scout-state/privacy-policy.json: the commit's effective policy (kb policy --json) under policy, the Agnes list under host_local_only_paths, and under labels every mirrored file's label, the one kb classify --json gives it. The Agnes indexer reads both.
    • Sync. Commits made on the mirror (in Agnes) replay onto the vault first, one vault commit each with their message, author and a Scout-Mirror-Commit: trailer. Local-Only paths are never touched: a server commit that drops a newly local-only path from the mirror leaves the laptop's copy. A file changed on both sides is merged three ways; a conflict stops the sync with nothing written and the merge in <path>.proposed-merge. Then each new vault commit becomes one mirror commit (skipped when its filtered tree doesn't change); when Agnes had new commits, the vault's go up as one. The commit map is .scout-state/repo-sync.json (gitignored); the token is ~/.scout-secrets/agnes-knowledge-repo/<vault id>.token, mode 600.
    • Gates. Link refuses a vault whose owner hasn't been through /scout-update Step 3b since it last changed (the consent marker privacy-migrate --accept writes, #510, must be present and current), and it re-checks the live report rather than trusting the marker. Link and sync refuse while kb policy warns privacy-overlay-unreadable, and while a root-level file whose name contains trading, brokerage, broker, positions, portfolio, investment, investing or holdings (case-insensitive), or a folder privacy-migrate proposes (declined ones included), is neither local-only nor confirmed with --confirm-sync '<path>'. Confirmations are kept in the commit map. A refused link changes nothing on Agnes. A real vault also needs the per-note privacy guard (#504: scout.kb.lint_privacy in the engine and Scout's pre-commit hook in the vault), which keeps a scheduled run from un-privating a note before it syncs; until #504 ships, only a test vault named in SCOUT_REPO_SYNC_TEST_VAULT can link, and never ~/Scout.
    • OTLP content off. Link merges OTEL_LOG_USER_PROMPTS, OTEL_LOG_ASSISTANT_RESPONSES, OTEL_LOG_TOOL_DETAILS and OTEL_LOG_TOOL_CONTENT = 0 into the vault's .claude/settings.json, so telemetry such as agnes telemetry enable exports no prompt, response or tool content from sessions in the vault. Unlink leaves it off.
    • Pushes go up under the lease. See the next entry (R2).

    Upgrade: nothing changes until you link. Run /scout-update first: link needs its privacy step's consent record.

  • A linked vault's scheduled runs take the Agnes Repository Writer Lease, so this computer and Agnes never write the repository at once (Agnes slice R2; engine/scout/repo_lease.py, engine/scout/repo_sync.py, engine/scout/cli.py, engine/scout/health.py, templates/scripts/scout-lock.sh.tmpl, templates/run-scout.sh.tmpl, templates/run-dreaming.sh.tmpl, templates/run-research.sh.tmpl, templates/scripts/run-outcome.sh.tmpl, README.md; Agnes spec "Scout in Agnes" §5, §8.8, §15 R2).

    • Runs. After the session lock and before anything writes the vault, each runner calls scoutctl repo lease acquire (holder laptop:<machine id>:<run id>, labelled with the host name and mode). The machine id is random, made once, and kept in ~/.scout-secrets/agnes-knowledge-repo/machine.id (mode 600): macOS renames the host with the network it joins, so the host name can't be the id. A keepalive (scoutctl repo lease keepalive) renews the lease every five minutes while the runner lives, and logs each renewal to the run log. If the lease lapses anyway (the laptop slept past the TTL and Agnes took over), the keepalive logs lost. The run finishes locally, and Agnes refuses its pushes (repository_lease_lost). The EXIT trap gives it back (repo lease release) before the session lock, on success, failure and a budget skip alike.
    • Skips. When Agnes holds the lease (an Agnes-hosted run), the run is skipped and the ledger row is outcome: skipped_lease, failure_class: lease_held. When Agnes can't grant it (unreachable, a refused PAT, a broken engine), the run is skipped as lease_unavailable, per the spec's call 6. scoutctl health --json shows them in each slot's last_outcome. A skipped fire isn't slot.fired_silent. There are two new findings: lease.held (note: lease-held skips in the last 24 h, by slot) and lease.unavailable (yellow, red after three skips in a row).
    • The token rule. Agnes rotates the token when the same holder acquires again, ending the old one. So a run's lease lives in ~/.scout-secrets/agnes-knowledge-repo/<vault id>.lease.json: mode 600 in a 700 folder, outside the vault, never on a command line or in stdout. A scoutctl repo sync or link during a live run pushes with that run's token and leaves the lease to it. Otherwise sync and link take a lease of their own (laptop:<machine>:sync) and give it back. A run that finds a manual sync's lease waits up to two minutes for it. Pushes carry X-Agnes-Lease-Token and X-Agnes-Lease-Generation as GIT_CONFIG_KEY_n environment pairs.
    • Kills. If the runner dies without its trap (SIGKILL), the keepalive sees its PID gone and releases the lease. If both die, the next run or sync here releases it from the lease file, and Agnes ends it after its TTL (20 minutes by default) in any case. A release Agnes can't hear leaves the file marked for the next caller.
    • New commands. scoutctl repo lease acquire | keepalive | release | status (status --json shows the holder on Agnes and here, never the token).
    • Unlinked vaults are unchanged. A runner whose vault has no .scout-state/repo-sync.json makes no lease call. scoutctl repo unlink returns a vault to that.

    Upgrade: /scout-update renders the new runners and scout-lock.sh. Nothing changes for a vault that isn't linked. A linked vault's first lease turns lease enforcement on for its Agnes repository, so after that every push needs the lease. Not in this change: runners don't sync on their own yet (a run neither replays Agnes's commits before it starts nor pushes after it ends), so scoutctl repo sync stays manual.

  • /scout-update normalises legacy visibility: markers, reports private links in shared files, and makes trading/brokerage folders local-only, each with the owner's OK (#453; engine/scout/scripts/privacy_migrate.py, engine/scout/scripts/bootstrap.py, engine/scout/cli.py, commands/scout-update.md, commands/scout-setup.md; spec §3.1, §4.3, §5.1, §7.2, §10 P5, D6, D10). scoutctl bootstrap privacy-migrate prints a read-only report (--json too) with three lists:

    • Legacy markers. Notes whose visibility: value is something other than normal/private (PRIVATE while drafting). They already count as private. --set-private <path> rewrites that one value to private and changes no other line; it keeps the key's spacing, a trailing comment and the line ending. It refuses a note with no visibility: key (Scout never writes defaults into frontmatter), a value over several lines, and any rewrite that wouldn't parse back as exactly that change. A note already normal or private is left alone.
    • Private links in shared files. Lines of shared files (drafts/**, a declared federation_scope) that link or name a private note, as file and line numbers only, using kb-lint's matcher (%%privacy:allow%% markers honoured).
    • The link step. Folders whose name has a trading or brokerage word (trading, brokerage, broker, investments, portfolio, …; agentic-trading matches, trade-show doesn't), proposed as <folder>/** unless already local-only. --local-only <glob> appends it to scout-config.yaml → privacy.local_only_add, editing the list in place so every comment survives. It is append-only and idempotent. An inline value (local_only_add: []) or an odd layout falls back to re-dumping the privacy: block, with a warning that comments inside it may be lost; the result is checked to load back with no duplicate keys.

    --set-private and --local-only refuse a scheduled or autonomous caller (SCOUT_MODE set, or a reader other than owner) with exit 2. A reader other than owner/autonomous (SCOUT_READER=agent:…) gets exit 2 and no output, report included, since it names private notes' paths. /scout-update explains visibility in one paragraph (the personal/ and .scout-inbox/ defaults, federation_scope reserved, scoutctl kb policy), reads the report, and asks the owner per note and per path before each write; /scout-setup gives the same paragraph. bootstrap upgrade prints the report on stderr and changes nothing, so a headless upgrade (self-update, Scout.app) only reports.

    Upgrade: nothing changes until the owner agrees to each change in /scout-update. Agree to the local-only proposals before the first sync to Agnes, so trading data never leaves the laptop. A declined proposal is asked again on the next update. Follow-up: the link step proposes folders only, so a root-level file such as TRADING.md isn't proposed; add it by hand with --local-only TRADING.md if it should stay local.

  • scoutctl bootstrap privacy-migrate --accept records the owner's consent to the privacy migration (#453; engine/scout/scripts/privacy_migrate.py, engine/scout/cli.py, commands/scout-update.md; spec §10 P5). Hosts such as Agnes check it before linking a vault, so a vault whose owner hasn't been through the migration can't be linked. /scout-update runs it once the owner has answered every proposal. It writes .scout-state/privacy-migrate.json (schema, engine_version, accepted_at, report_sha256, declined_local_only) atomically with mode 0600; the file is local-only and never syncs. A declined proposal is the owner's decision, so accepting after a "no" is valid, but the "no" is recorded rather than silent: declined_local_only lists the local-only proposals still outstanding at acceptance, and a host (e.g. Agnes R1) may require it to be [] before linking. The hash covers only what the owner decided on, the outstanding legacy-visibility notes and local-only proposals, so editing a draft that links a private note doesn't invalidate consent, while a new legacy marker or a new trading folder does. It refuses a scheduled or autonomous caller (exit 2), gives an agent reader nothing, never writes the privacy index, and writes nothing when the report fails (exit 1). The read-only report's --json gains accepted: null without a readable marker, else {accepted_at, engine_version, report_sha256, current, declined_local_only, outstanding_local_only}, where current is false once the report has changed and the owner must be asked again, and outstanding_local_only is the live proposal list; the text report adds one line saying which and how many proposals were declined.

  • scoutctl kb gains what a mature vault's hand-edited parser.py grew, so such a vault can switch to the shim (#453, #495; engine/scout/kb/ontology.py, engine/scout/kb/read.py, engine/scout/cli.py, templates/knowledge-base/ontology/parser.py, phases/core/action-items.md). A vault that edited its parser keeps the v0.18.3 shim parked under .scout-state/drift, because the shim would lose behaviour its scheduled runs use. The engine now has the generic part of it:

    • Target resolution. load() builds every entity first, then resolves each relationship target by display name, [[slug|Display]] pipe alias, filename stem or path ([[people/sam]]), then declared aliases:. A stem or alias two entities share stays unresolved, and a real name beats another entity's alias.
    • Archived snapshots are skipped. Files under projects/**/archive/** no longer shadow the live entity. The vault's schema.yaml can set its own KB-relative globs in graph: {exclude: [...]} ([] skips nothing).
    • Malformed frontmatter is reported, not dropped. validate lists each such file as YAML parse failure: <problem> (line N, column M) under its vault-relative path, labelled (it's private). The message leaves out PyYAML's quoted snippet, which is the note's own text.
    • Dangling targets. validate reports every relationship target that resolves to no entity. Its exceptions come from the vault's schema.yaml, in a validation: block: external_references (real things never modelled), known_gaps ({target: reason}, reviewed and accepted) and provenance_relationship_types (targets that point at dated artifacts). Known gaps are never errors: text output lists them in a "known entity gap(s)" section, and kb validate --json is now {"errors": [...], "known_gaps": [...]} (it was a bare list of errors).
    • Typed name lookup. kb name-lookup --lookup-type person|organization|any|<csv> (default person), and name_lookup(types=...). Each candidate carries its type, in JSON and text; text also names the scope searched, and a person-only miss suggests --lookup-type any. Labels are unchanged. The transcript name guard in phases/core/action-items.md now runs kb name-lookup "<Token>" --lookup-type any, so a garbled company or product name is caught too.
    • birthday_month reads MM-DD (and --MM-DD) birthdays as well as YYYY-MM-DD.
    • The shim passes --lookup-type through, takes --rel-types as another spelling of --rels (so does kb traverse), and uses the vault's scripts/scoutctl when scoutctl isn't on PATH.

    Upgrade: a vault on the shim gets this with the engine. A vault with a hand-edited parser can move its exception lists into schema.yaml's validation: block, then take the shim from .scout-state/drift. Run kb validate once after the engine update: targets that used to dangle silently now show.

Changed

  • The wrap goes to Slack AND Telegram when both are enabled; notify.wrap_channels picks one (follow-up to #481, closes #485; engine/scout/scripts/phase_assembly.py, engine/scout/scripts/bootstrap.py, engine/scout/scripts/phase_backport.py, phases/connectors/slack.md, phases/connectors/telegram.md, phases/research/commit-notify.md, templates/run-*.sh.tmpl, commands/scout-setup.md, commands/scout-update.md, engine/scout/docs/connectors/telegram-setup.md). In 0.18.3, enabling Telegram took the wrap off Slack. Now Telegram is an added push, and the Slack DM stays.

    • One channel is a per-vault choice, made at assembly. notify.wrap_channels: [slack] or [telegram] in scout-config.yaml leaves the other channel's notification section out of SKILL.md and RESEARCH.md; leave the key out for both. No section tells the session to skip another any more: a section that is there is sent. Only the notification slot is filtered, so a Telegram-only vault still scans Slack. An unknown value or an empty list makes bootstrap upgrade stop with a notify.wrap_channels error, and a choice that names no enabled connector prints a warning (no wrap would be sent).
    • Privacy (#453) holds per channel. The Slack DM still goes through scoutctl kb check-outbound --surface slack and never names a [private] item. The Telegram chat is owner-only and may. A failed Telegram send is still reported and never resent on Slack.
    • Runner prompts say to send every notification section the brain file has.
    • telegram-setup.md §7 documents both-by-default, notify.wrap_channels and the per-channel privacy rule (#485).

    Upgrade: a vault with both slack and telegram enabled starts sending the wrap to both after /scout-update. To keep the 0.18.3 behaviour (Telegram only), add notify: {wrap_channels: [telegram]} to scout-config.yaml and run /scout-update again; Step 3c of /scout-update asks about this. Vaults with only one of the two are unaffected.

Fixed

  • Connector KB updates find and read notes through scoutctl kb (#453; phases/connectors/{calendar,email,fathom,github,granola,linear,slack}.md, phases/custom/inbound.md, phases/core/meeting-prep.md). #507's eval found that runs which actually read the connector KB-update sections reached for raw Reads of people, topic and project notes: those sections said "add them to people.md" or "update the project files" and named no lookup. Each Calendar, GitHub, Linear and Slack KB-update section now opens with the sanctioned path: kb name-lookup <name> for a person, kb resolve <name> for a project or other note, kb read <path> for a note the run isn't changing, and Read only on a note it's about to edit. The scans' "cross-reference with people.md" / channels.md pointers and meeting prep's tracked-meetings list name the command too. A new test keeps every connector update section on these lookups. The SKILL brain file grows by about 420 tokens with every connector on.

    Upgrade: /scout-update brings the edited sections into each vault's SKILL.md through the usual 3-way merge.

  • An eval session that lost the API is a harness failure, never a score (engine/tests/eval/harness/metrics.py, engine/tests/eval/harness/runner.py, engine/tests/eval/harness/report.py). When claude exhausts its API retries (no network) it still ends the run with subtype success, flagged is_error, so the harness scored it: four of six #84 privacy-canary samples were $0, one-turn runs counted as clean passes of checks they never attempted. Such a sample now aborts as api_error (with the retry count): it blocks the verdict and stops new launches, like any harness failure. Every report adds an API retries row. Maintainer tooling only.

  • An eval sample that hits a run limit voids only itself (engine/tests/eval/harness/verdicts.py, engine/tests/eval/harness/cli.py, engine/tests/eval/harness/runner.py; spec docs/superpowers/specs/2026-10-12-eval-harness-design.md §8). One head sample that timed out or hit its budget cap blocked every verdict and stopped the compare, so a slow network voided a 12-session #84 compare twice. Now that sample is left out of the checks and metrics and the rest keep running; a within limits row scores the limit hits like a check (one extra on head is flaky on head, two a regression). Harness failures (no session, a crash, mcp_leak) still block and stop. A truncated base sample is never cached. The per-sample timeout is 30 minutes (was 20). Maintainer tooling only: no vault changes.

  • Privacy eval follow-ups: context notes through scoutctl kb, exact private titles, per-scenario eval budgets (#453; phases/core/privacy.md, phases/core/write-protocol.md, phases/core/action-items.md, phases/core/kb-management.md, engine/tests/eval/harness/). A model-backed run of the privacy-canary scenarios leaked nothing; its misses were budget, phase-text gaps and one false positive in the check.

    • Runs get a sanctioned way to find and read context notes. Where the phases say to edit action-items/context/<TAG>.md, and where kb-management bans ls, they now name scoutctl kb files --glob 'action-items/context/*.md' to list them and scoutctl kb read <path> to read one. The privacy rule says to orient from .scout-cache/session-context.json, never ls, find, cat or Glob.
    • A private item keeps its title on owner-only surfaces. The daily file, context notes and Telegram copy the title exactly as written: no paraphrase, no "(private)" tag.
    • Consolidation keeps today's personal tasks. It reads personal_tasks from .scout-cache/session-context.json and keeps each task whose surface window covers today in the daily file, private ones included.
    • Eval harness. A scenario may set budget: (a positive USD amount, else the runner's $3 default): privacy-canary and privacy-canary-consolidation use 4, privacy-canary-dreaming 6. privacy_hygiene no longer counts a Read of knowledge-base/ontology/schema.yaml or parser.py (tooling, not notes) as a raw KB read; entity notes under ontology/entities/ still count. Each sample's meta.json, and an aborted sample's reason, record the budget it ran with.

Changed

  • recurring-task-status.py says who does each recurring task, and a due cadence someone else does is no longer the vault owner's 🔴 (templates/scripts/recurring-task-status.py, phases/core/action-items.md, engine/scout/kb/schema.yaml). A recurring_task can record its author in observed_author or last_completed_by, but nothing read them, so every due cadence became the owner's urgent item even when a teammate writes it. Each row now has owner, owner_source and owner_is_user (observed_author wins; a trailing # … note on the value is ignored), matched against user.name, its first word alone, user.email, and the github_username / user_slack_id connector inputs in scout-config.yaml. The table gains an Owner column, and a list under it names each due/overdue row that isn't the owner's, private mark first. The status itself doesn't change, so a lapsed cadence stays visible; the action-items phase makes it a 🟢 Watching item waiting on its owner. Without a readable scout-config.yaml every row stays the owner's, with a warning. Upgrade: a vault that already patched this script by hand gets a 3-way merge on its next /scout-update; check scoutctl bootstrap drift afterwards.

Full changelog: v0.18.3...v0.18.4


Install

  1. Download Scout-0.18.4.dmg below.
  2. Open it and drag Scout.app into Applications.
  3. Open Scout. Onboarding sets up everything else: the engine, Claude Code's plugin, and your vault.

Terminal only? curl -fsSL https://raw.githubusercontent.com/Raven-Scout/Scout/main/install.sh | bash