Repository navigation
Scout 0.18.4
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 labelledprivate(itsvisibility:line, or a path default such asknowledge-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 changedscout-config.yaml(itsprivacy:overlay can relabel any file) drops the cache. Whenscoutctlfails, the tree shows no locks and the exit code is logged (stderr only as private log data). An engine withoutkb classify(exit 2 or "No such command") isn't asked again until thescoutctlexecutable changes.
Plugin and engine
Added
-
scout-askskill: 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 ofknowledge-base.md); gate on the file'slast_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 ontoscoutctl kbso 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.mdand sent only whenscoutctl kb check-outbound … --surface slackpasses. 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 throughkb check-queryfirst, 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(wasgit log,git diff HEAD~1); personal tasks, name lookups, validate and stats fromkb query,kb name-lookup,kb validate,kb stats(wasparser.py); searches fromkb search(wasgrepover 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 markedvisibility: private. - Eval seed: the
privacy-canaryscenarios (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 newfiles_from:key, and a dreaming scenario runsrun-dreaming.sh).privacy_canarypasses 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 ordrafts/;privacy_hygienefails on a raw shell read of the KB (cat/rg/grep/ls/find/wc/tree…,git show|diff|log -p, or aftercdinto it) aGrep/Globthere or over the whole vault, aReadof a KB file the run never edits, a Slack send without a precedingcheck-outbound, or--no-verify. The eval's newscoutctlstub recordsscoutctl notify telegraminstead of sending it, so no eval run can reach a real chat; a scenario'sbrain/<KIND>.mdis appended to that brain file. The brain size pins grow by about 1,100–1,250 tokens each.
Upgrade:
/scout-updatebrings the new phase and the edited ones into each vault'sSKILL.md,DREAMING.mdandRESEARCH.mdthrough the usual 3-way merge; a vault that edited the same lines gets a proposed-merge sidecar. Runs need thescoutctl kbread commands (this release) and the vault'sscripts/scoutctlshim. A vault that sends its wrap to Telegram keeps doing so; that chat may now name private items. - Slack goes through a draft check. Every Slack message is written to
-
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-mentionblock every commit, the owner's included. An added or changed line of a staged shared file (drafts/**, anyshared_files_addglob, or a note that declares afederation_scopeother thannone) 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, inreportmode too and whateverSCOUT_LINT_OVERRIDEsays. 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_MODEset, orSCOUT_READERset to anything but exactlyowner: 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 ofpersonal/included), drops thevisibility: privateline of a note whose path default is private, sets or widensfederation_scope, adds a marker anywhere in the checked files, or changes theprivacy:block ofscout-config.yaml. Narrowing is allowed. The owner's interactive commits aren't checked. - Warnings.
visibility-value,privacy-contradiction,frontmatter-unparseable(staged notes) andfederation-reserved(a stagedscout-config.yaml) warn without blocking.scoutctl kb lint --reportadds 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 intodrafts/, given afederation_scope) has every line checked, and a staged note declaring afederation_scopeis checked wherever it lives. Added lines are computed by Scout over the raw blobs, so a NUL byte or a.gitattributesbinary/-diffcan'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 blockingand the commit goes on. For the privacy checks (a checker error, a missingscoutctl, an engine crash before the check, any exit that isn't a pass or a lint verdict, and, in a run, a stagedscout-config.yamlthat doesn't parse) the hook printskb-lint: privacy check could not run, then (Jordan, 2026-10-22): a scheduled run's commit is refused (SCOUT_MODEset, orSCOUT_READERset to anything but exactlyowner, 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 eventprivacy.check_failedto.scout-logs/schedule-events-<date>.jsonl); your interactive commit goes through with a loudkb-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 yellowprivacy: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 atknowledge-base/andaction-items/markdown, everyshared_filesglob,scout-config.yamland any staged note declaring afederation_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 telegramclassifies 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@channelname) 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 aprivacy.outbound_blockedevent and a blocked line in.scout-logs/kb-reads.jsonl. A group send with no vault to check against is refused.scoutctl phases backportrefuses a bundle that names a private note. The lines it would write intophases/are checked for thepublic_reposurface, 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 yellowprivacy:warning (a run reached for a shared surface with private content); zero adds nothing.
Upgrade: the checks inside
scoutctl kb lint --stagedapply 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 withscoutctl kb install-hookin the vault; until then the old script lets every commit through when scoutctl can't run, silently. After it, a scheduled run whosescoutctlis missing or broken can't commit untilscoutctl doctoris 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 indrafts/(or another shared file) is now refused even inreportmode. 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 --reportlists 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 | unlinksyncs 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 plusprivacy.local_only_add, whichbootstrap privacy-migratewrites), 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, soTRADING/notes.mdis as local astrading/**. 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 underrefs/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) underpolicy, the Agnes list underhost_local_only_paths, and underlabelsevery mirrored file's label, the onekb classify --jsongives 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-updateStep 3b since it last changed (the consent markerprivacy-migrate --acceptwrites, #510, must be present andcurrent), and it re-checks the live report rather than trusting the marker. Link and sync refuse whilekb policywarnsprivacy-overlay-unreadable, and while a root-level file whose name containstrading,brokerage,broker,positions,portfolio,investment,investingorholdings(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_privacyin 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 inSCOUT_REPO_SYNC_TEST_VAULTcan link, and never~/Scout. - OTLP content off. Link merges
OTEL_LOG_USER_PROMPTS,OTEL_LOG_ASSISTANT_RESPONSES,OTEL_LOG_TOOL_DETAILSandOTEL_LOG_TOOL_CONTENT=0into the vault's.claude/settings.json, so telemetry such asagnes telemetry enableexports 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-updatefirst: link needs its privacy step's consent record. - What's filtered. The Local-Only set is the union of the vault's effective policy (
-
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(holderlaptop:<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 logslost. 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 aslease_unavailable, per the spec's call 6.scoutctl health --jsonshows them in each slot'slast_outcome. A skipped fire isn'tslot.fired_silent. There are two new findings:lease.held(note: lease-held skips in the last 24 h, by slot) andlease.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. Ascoutctl repo syncor 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 carryX-Agnes-Lease-TokenandX-Agnes-Lease-GenerationasGIT_CONFIG_KEY_nenvironment 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 --jsonshows the holder on Agnes and here, never the token). - Unlinked vaults are unchanged. A runner whose vault has no
.scout-state/repo-sync.jsonmakes no lease call.scoutctl repo unlinkreturns a vault to that.
Upgrade:
/scout-updaterenders the new runners andscout-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), soscoutctl repo syncstays manual. - Runs. After the session lock and before anything writes the vault, each runner calls
-
/scout-updatenormalises legacyvisibility: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-migrateprints a read-only report (--jsontoo) with three lists:- Legacy markers. Notes whose
visibility:value is something other thannormal/private(PRIVATE while drafting). They already count as private.--set-private <path>rewrites that one value toprivateand changes no other line; it keeps the key's spacing, a trailing comment and the line ending. It refuses a note with novisibility: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 alreadynormalorprivateis left alone. - Private links in shared files. Lines of shared files (
drafts/**, a declaredfederation_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-tradingmatches,trade-showdoesn't), proposed as<folder>/**unless already local-only.--local-only <glob>appends it toscout-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 theprivacy:block, with a warning that comments inside it may be lost; the result is checked to load back with no duplicate keys.
--set-privateand--local-onlyrefuse a scheduled or autonomous caller (SCOUT_MODEset, or a reader other thanowner) with exit 2. A reader other thanowner/autonomous(SCOUT_READER=agent:…) gets exit 2 and no output, report included, since it names private notes' paths./scout-updateexplainsvisibilityin one paragraph (thepersonal/and.scout-inbox/defaults,federation_scopereserved,scoutctl kb policy), reads the report, and asks the owner per note and per path before each write;/scout-setupgives the same paragraph.bootstrap upgradeprints 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 asTRADING.mdisn't proposed; add it by hand with--local-only TRADING.mdif it should stay local. - Legacy markers. Notes whose
-
scoutctl bootstrap privacy-migrate --acceptrecords 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-updateruns 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_onlylists 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--jsongainsaccepted:nullwithout a readable marker, else{accepted_at, engine_version, report_sha256, current, declined_local_only, outstanding_local_only}, wherecurrentis false once the report has changed and the owner must be asked again, andoutstanding_local_onlyis the live proposal list; the text report adds one line saying which and how many proposals were declined. -
scoutctl kbgains what a mature vault's hand-editedparser.pygrew, 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 declaredaliases:. A stem or alias two entities share stays unresolved, and a realnamebeats another entity's alias. - Archived snapshots are skipped. Files under
projects/**/archive/**no longer shadow the live entity. The vault'sschema.yamlcan set its own KB-relative globs ingraph: {exclude: [...]}([]skips nothing). - Malformed frontmatter is reported, not dropped.
validatelists each such file asYAML 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.
validatereports every relationship target that resolves to no entity. Its exceptions come from the vault'sschema.yaml, in avalidation:block:external_references(real things never modelled),known_gaps({target: reason}, reviewed and accepted) andprovenance_relationship_types(targets that point at dated artifacts). Known gaps are never errors: text output lists them in a "known entity gap(s)" section, andkb validate --jsonis now{"errors": [...], "known_gaps": [...]}(it was a bare list of errors). - Typed name lookup.
kb name-lookup --lookup-type person|organization|any|<csv>(defaultperson), andname_lookup(types=...). Each candidate carries itstype, 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 inphases/core/action-items.mdnow runskb name-lookup "<Token>" --lookup-type any, so a garbled company or product name is caught too. birthday_monthreadsMM-DD(and--MM-DD) birthdays as well asYYYY-MM-DD.- The shim passes
--lookup-typethrough, takes--rel-typesas another spelling of--rels(so doeskb traverse), and uses the vault'sscripts/scoutctlwhenscoutctlisn'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'svalidation:block, then take the shim from.scout-state/drift. Runkb validateonce after the engine update: targets that used to dangle silently now show. - Target resolution.
Changed
-
The wrap goes to Slack AND Telegram when both are enabled;
notify.wrap_channelspicks 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]inscout-config.yamlleaves 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 makesbootstrap upgradestop with anotify.wrap_channelserror, 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 slackand 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_channelsand the per-channel privacy rule (#485).
Upgrade: a vault with both
slackandtelegramenabled starts sending the wrap to both after/scout-update. To keep the 0.18.3 behaviour (Telegram only), addnotify: {wrap_channels: [telegram]}toscout-config.yamland run/scout-updateagain; Step 3c of/scout-updateasks about this. Vaults with only one of the two are unaffected. - One channel is a per-vault choice, made at assembly.
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 rawReads of people, topic and project notes: those sections said "add them topeople.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 withpeople.md" /channels.mdpointers and meeting prep's tracked-meetings list name the command too. A new test keeps every connectorupdatesection on these lookups. The SKILL brain file grows by about 420 tokens with every connector on.Upgrade:
/scout-updatebrings the edited sections into each vault'sSKILL.mdthrough 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 subtypesuccess, flaggedis_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 asapi_error(with the retry count): it blocks the verdict and stops new launches, like any harness failure. Every report adds anAPI retriesrow. 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; specdocs/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; awithin limitsrow scores the limit hits like a check (one extra on head isflaky 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 theprivacy-canaryscenarios 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 wherekb-managementbansls, they now namescoutctl kb files --glob 'action-items/context/*.md'to list them andscoutctl kb read <path>to read one. The privacy rule says to orient from.scout-cache/session-context.json, neverls,find,cator 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_tasksfrom.scout-cache/session-context.jsonand 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$3default):privacy-canaryandprivacy-canary-consolidationuse 4,privacy-canary-dreaming6.privacy_hygieneno longer counts a Read ofknowledge-base/ontology/schema.yamlorparser.py(tooling, not notes) as a raw KB read; entity notes underontology/entities/still count. Each sample'smeta.json, and an aborted sample's reason, record the budget it ran with.
- Runs get a sanctioned way to find and read context notes. Where the phases say to edit
Changed
recurring-task-status.pysays 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). Arecurring_taskcan record its author inobserved_authororlast_completed_by, but nothing read them, so everyduecadence became the owner's urgent item even when a teammate writes it. Each row now hasowner,owner_sourceandowner_is_user(observed_authorwins; a trailing# …note on the value is ignored), matched againstuser.name, its first word alone,user.email, and thegithub_username/user_slack_idconnector inputs inscout-config.yaml. The table gains an Owner column, and a list under it names eachdue/overduerow 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 readablescout-config.yamlevery 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; checkscoutctl bootstrap driftafterwards.
Full changelog: v0.18.3...v0.18.4
Install
- Download
Scout-0.18.4.dmgbelow. - Open it and drag Scout.app into Applications.
- 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