Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

What is whisper

whisper (the turu binary) makes the incitaciones whisper skill deterministic.

The whisper skill asks an agent to manage ~/.whisper/ — canonical repo keys, branch slugs, worktree slots, and scope-based knowledge routing — by hand-rolling shell pipelines. Every invocation is a fresh chance for the model to mis-derive a path. This tool turns that mechanical layer into a binary: the skill prompts call the CLI, and the same inputs always produce the same outputs.

Why: agents accumulate operational knowledge across sessions, but “where does this note go?” is a question models answer unreliably. Canonical key derivation is a pure function of the remote URL — the same repo always yields the same key on every machine — so routing stops depending on prompt bookkeeping.

Install:

cargo install whisper-vibes

The command is turu — following the song’s turututu riff. turututu and whisper run as aliases, so existing scripts and skill prompts keep working.

Part of the charly-vibes tool suite; built on genesis-vibes for the JSON envelope, config, and CLI conventions shared across the family.

Ecosystem

whisper is part of the charly-vibes Tool Ecosystem — the map shows how it relates to its sibling tools.

Documentation provenance

Rendered docs are generated artifacts; the repository sources are authoritative — edit the repo sources, not the rendered output.

Release Status

Generated artifact (docs.yml, from Cargo.toml + git ref). Do not hand-edit — the repo source files remain authoritative.

  • Version: v0.11.0
  • Built from: main @ 9b5fa40

Getting started

Install

cargo install whisper-vibes

Or from source:

git clone git@cv:charly-vibes/whisper.git && cd whisper && just install

Create the workspace

turu init        # create the ~/.whisper layout (never overwrites)

Daily loop

Derive where you are, write what you learned, recall what you need:

turu key                                  # repo key / branch slug / worktree slot
turu resolve branch                       # the exact destination path for a scope

turu append repo --text "deploy requires vault login" --topic deploy
turu recall repo --topic deploy --budget 400   # slice, newest first, boundary reported

Every read and write is one JSON envelope; recall reports the context-horizon boundary (served_bytes, budget_unused, entries_skipped).

Share knowledge through git

By default, repo and branch scopes write into the checkout’s committed .whisper/ directory — the repo’s own git carries the shared knowledge. Read-only checkouts (CI, no-push contributors) use the --global escape hatch. Machine-specific knowledge (local paths, hostnames, credentials) belongs in the private zone: turu append <scope> --private.

Wire it into agents

turu sync         # inject the routing map into AGENTS.md as a TURU managed block
turu doctor       # verify layout, groups, managed block, privacy contract

Agents in any repo with a TURU managed block read the routing map straight from AGENTS.md — no prompt bookkeeping required.

Configuration

Where knowledge gets written is layered. Precedence: repo > group > global.

Global

~/.config/whisper/config.toml:

workspace_root = "~/.whisper"   # default

[groups.cv-tools]
root = "~/para/areas/dev/gh/charly/knowledge"
repos = ["cv/charly-vibes/whisper"]   # explicit membership (optional)

A group routes knowledge from a set of repos into one shared workspace directory instead of each repo’s private slot.

Repo (private)

<repo>/.whisper/config.toml (gitignored — never committed):

group = "cv-tools"              # join a group defined in the global config
workspace_root = "/path/to/dir" # or fully override the workspace root

A repo can join a group two ways: privately, via its own .whisper/config.toml, or centrally, by listing its canonical key in the group’s repos array.

Note: a repo-private workspace_root overrides everything — including the global scope, so the repo’s rules.md resolves inside the private root instead of the shared workspace. turu doctor reports this shadowing and warns when the two rules.md files diverge. A relative workspace_root is anchored to the directory containing the repo config (not the process cwd), so calls from any subdirectory resolve to the same root.

Knowledge transport & the private zone

ScopeIn a checkout (default)--global escape hatch
repo<repo>/.whisper/env.md (committed)<workspace>/repos/<key>/env.md
branch<repo>/.whisper/branches/<slug>/notes.md (committed)<workspace>/repos/<key>/branches/<slug>/notes.md
--private repo/branch<repo>/.whisper/private/… — gitignored, never pushed—

Transport decision: the repo’s own git carries the shared knowledge. Repo-local publication assumes a pushable checkout — --global is the fallback for read-only checkouts (CI, no-push contributors).

The private zone is enforced by turu doctor with three checks:

  • turu.private-ignore — the zone is effectively ignored (warning + fix hint otherwise)
  • turu.private-tracked — fails prominently if any file under private/ is tracked
  • turu.private-leaks — advisory lint for machine paths, hostnames, and token-shaped strings in public repo-local files (surfaces via warnings[])

Git history is forever — prevention only. No turu verb can retract committed knowledge; that is exactly why the private zone exists and why the tracked-files check fails instead of warning.

CLI reference

All commands emit a genesis JSON envelope. Scopes: global, repo, branch, worktree, group (recall also accepts all).

turu key

Canonical repo key, branch slug, and worktree slot for the current checkout (pure determinism core — a pure function of the remote URL).

turu resolve <scope>

The exact destination path for a knowledge scope.

  • --global — write to the machine-local store instead of the checkout’s repo-local .whisper/ (repo/branch scopes only)
  • --private — write to the checkout’s private zone .whisper/private/ — readable locally, structurally never pushed (repo/branch only)

turu append <scope> --text "..."

Append text to a scope’s file (extend, don’t duplicate). Entries are structured and idempotent: sha2 ids over scope + second-precision UTC + text; freeform content preserved verbatim.

  • --text — text to append (repeatable)
  • --stdin — read the text from stdin instead of --text
  • --topic <k> — optional topic key (bare word, no whitespace/parens/#)
  • --supersedes <id> — mark the entry with this id as superseded by the new entry
  • --global / --private — as resolve

turu recall <scope>

Serve a ranked, budget-bounded slice of a scope’s entries. Whole entries only (never truncated); reports served_bytes, budget_unused, entries_skipped.

  • scope may be all — composes scopes in precedence order
  • --topic <k> — only entries with this topic key
  • --budget <bytes> — byte budget
  • --include-superseded — include superseded entries
  • --read-only — skip usage telemetry (sidecar untouched)

turu distill <scope>

Two-phase distill contract: snapshot, agent rewrites, guarded commit.

  • --begin — snapshot the current state and return the working paths
  • --commit --revision <id> — install the distilled working file for this revision
  • Guards the commit against concurrent drift between snapshot and commit

turu bundle pack|unpack

Deterministic, transport-agnostic knowledge bundles keyed by repo key. Unpack merges extend-without-duplicate and never overwrites.

turu init

Create the workspace layout for this checkout (never overwrites).

turu status

One envelope with every relevant path and existence flag.

turu check

Detect legacy key variants, undefined groups, and missing files.

turu consolidate

Migrate legacy repo-key directories into the canonical key.

turu doctor

Deep workspace diagnostics: layout integrity, group health, legacy keys, managed block, skill pack staleness, and the private-zone contract (private-ignore, private-tracked, private-leaks).

turu sync

Inject/refresh the turu managed block (<!-- TURU:START -->) in the agent-facing file (default: AGENTS.md at the repo root), so agents read paths from a file instead of re-deriving them.

turu feedback <kind>

File a well-contexted GitHub issue (bug, feature, question, chore): redacted context bundle, optional --from-last-error scratch recall, --dry-run preview (via genesis::feedback).

turu skill install

Ship the whisper skill from this repo: router + sub-skills (including manual fallbacks), content-hashed so turu doctor flags staleness. Targets ~/.claude/skills or any dir; incitaciones keeps a thin pointer to it.

Status

whisper current release — Status: beta. Core (key/resolve/append/recall/distill) works and is used in anger by other ecosystem tools; surface may still shift before 1.0.

Implemented

CommandStatusNotes
turu keystablepure function of remote URL; determinism core
turu resolvestableall five scopes + --global/--private
turu appendstableidempotent sha2 ids, topics, supersede chain
turu recallstablebudget-bounded slices, all precedence composition, horizon boundary reported
turu distillstabletwo-phase begin/commit with drift guard
turu bundlestablepack/unpack, extend-without-duplicate merge
turu initstablenever overwrites
turu statusstablesingle envelope, existence flags
turu checkstablelegacy keys, undefined groups, missing files
turu consolidatebetamigration of legacy repo-key dirs
turu doctorstablelayout, groups, managed block, skill staleness, privacy contract
turu syncstableTURU managed block in AGENTS.md
turu feedbackbetagenesis::feedback, kinds bug/feature/question/chore
turu skill installbetarouter + sub-skills, content-hashed staleness check

In progress

  • Consolidate rollout across repos with legacy key directories (see the whisper workspace hygiene item in incitaciones’ inbox).

Mapped to specs

  • Envelope/config/feedback conventions: genesis-vibes
  • Skill contract: incitaciones whisper skill (thin pointer to turu skill install output)

Dogfooding

  • incitaciones — the distilled whisper skill is a thin pointer to this repo’s skill pack (turu skill install)
  • session/renew skill (org agent config) — calls turu key, turu recall repo/branch for session history before journal greps
  • whisper’s own CI runs just ci (fmt, clippy, tests, locked build); pinned ecosystem installs tracked in versions.ddl.toml

config-precedence Specification

Purpose

Baseline spec for existing behavior: layered configuration — global (~/.config/whisper/config.toml, or $XDG_CONFIG_HOME), group workspaces, and private per-repo overrides. Precedence: repo > group > global.

Requirements

Requirement: Layered resolution with repo > group > global

Configuration SHALL resolve in layers: the global file provides the default workspace root and named [groups.*] workspaces; a group routes a set of repos into one shared knowledge directory; the private repo config (.whisper/config.toml, found by walking up from the working directory) may join a group or override the workspace root. Repo overrides SHALL win over global defaults.

Scenario: Repo joins a group

  • WHEN the repo config sets group = "team" and the global config defines [groups.team] with a root
  • THEN repo, branch, and worktree scopes resolve under the group root
  • AND the global scope still resolves to the workspace root

Scenario: Repo overrides the workspace root

  • WHEN the repo config sets workspace_root and no group is active
  • THEN all scopes resolve under that private root

Requirement: Repo config discovery walks up

The repo config SHALL be discovered by walking up from the working directory to the first .whisper/config.toml; files above that first hit are not consulted for repo overrides.

Scenario: Config found in a parent

  • WHEN the working directory is <repo>/src/module and .whisper/config.toml exists at <repo>/
  • THEN that config is applied

Scenario: Nested config shadows the repo root

  • WHEN .whisper/config.toml exists both in the current working directory’s subtree root and at the repo root
  • THEN the first one found walking up from the working directory wins
  • AND the repo root’s config is not consulted for that invocation

Requirement: Global default root

With no repo config and no group membership, the workspace root SHALL default to ~/.whisper (honoring a workspace_root set in the global config, with ~ expansion).

Scenario: Fresh machine

  • WHEN no config files exist anywhere
  • THEN scopes resolve under ~/.whisper

knowledge-append Specification

Purpose

Baseline spec for existing behavior: turu append — extending a scope’s knowledge file without duplication or clobbering. (Structured entry ids, idempotency, and supersede marks are proposed separately in add-entry-model.)

Requirements

Requirement: Append creates then extends

turu append <scope> SHALL create the scope’s file (with parent directories) if missing, and otherwise append to it in place. Existing content SHALL never be rewritten or truncated.

Scenario: First append creates the file

  • WHEN the scope’s file does not exist
  • THEN turu append --text "first fact" creates it containing the text
  • AND the envelope reports the target path and appended_bytes

Scenario: Subsequent appends extend

  • WHEN the scope’s file already contains content
  • THEN turu append --text "second fact" adds the text after the existing content
  • AND existing content is byte-identical to before

Scenario: Empty input rejected

  • WHEN turu append runs with blank --text or empty stdin
  • THEN the command fails with an error and a next-step suggestion
  • AND the scope file is unchanged

Requirement: Newline normalization

Appended text SHALL be trimmed of trailing whitespace and written with exactly one trailing newline.

Scenario: Trailing whitespace collapse

  • WHEN turu append --text "fact\n\n" runs
  • THEN the file ends with fact and a single newline

Requirement: Stdin input

turu append SHALL accept text from stdin via --stdin as an alternative to repeatable --text.

Scenario: Piped input

  • WHEN text is piped to turu append repo --stdin
  • THEN the piped text is appended exactly as --text would append it

managed-block Specification

Purpose

Baseline spec for existing behavior: the <!-- TURU:START --> managed block injected into the agent-facing file — the contract that lets agents read deterministic routing paths from a file instead of re-deriving them.

Requirements

Requirement: Inject or refresh the managed block

turu sync SHALL inject the <!-- TURU:START --> / <!-- TURU:END --> block into AGENTS.md at the repo root (or the --file override), containing the workspace root, repo key, branch slug, worktree slot, and the deterministic routing map for all scopes. Content outside the block SHALL never be modified.

Scenario: First sync creates the block

  • WHEN the target file exists without a managed block
  • THEN the block is injected and the envelope reports the action
  • AND existing file content is preserved

Scenario: Target file missing

  • WHEN the target file does not exist
  • THEN turu sync creates it containing the managed block
  • AND the envelope reports the action as created

Scenario: Re-sync updates in place

  • WHEN the target file already contains a managed block and routing facts changed (e.g. new branch)
  • THEN only the block content is replaced
  • AND content outside the block is byte-identical to before

Scenario: File override

  • WHEN turu sync --file CLAUDE.md runs
  • THEN the block is injected into that file instead of AGENTS.md

Requirement: Block content is generated, never hand-edited

The managed block SHALL be regenerated only by turu sync; its content is derived entirely from checkout facts and resolved config.

Scenario: Content matches resolve

  • WHEN the managed block lists a scope path
  • THEN it is identical to the path turu resolve <scope> returns for the same checkout

shared-store Specification

Purpose

TBD - created by archiving change add-shared-bundle. Update Purpose after archive.

Requirements

Requirement: Deterministic knowledge bundle

turu bundle pack <scope> SHALL produce a single archive keyed by the canonical repo key containing the scope’s entries with ids, ordered deterministically such that packing identical state yields identical bytes.

Scenario: Deterministic pack

  • WHEN the same workspace state is packed twice
  • THEN the two bundles are byte-identical

Scenario: Round trip into a fresh workspace

  • WHEN a bundle is packed and then unpacked into a fresh workspace
  • THEN entries are present with identical ids

Requirement: Non-destructive unpack

turu bundle unpack SHALL merge using extend-without-duplicate rules (mirroring turu consolidate), skip entries whose ids already exist locally, and never overwrite existing file content.

Scenario: Merge into an existing workspace

  • WHEN a bundle is unpacked into a workspace that already has some of the entries
  • THEN existing entries are skipped and reported as duplicates_skipped
  • AND existing file content is extended, not overwritten

Requirement: Transport-agnostic bundles

The bundle format SHALL make no assumption about transport; how bundles travel between workspaces is out of scope for the bundle capability.

Scenario: Bundle carries no transport state

  • WHEN a bundle is produced
  • THEN it contains only knowledge content and addressing (repo key, scope, entry ids)
  • AND no transport-specific fields are required to unpack it

Scenario: Pack group scope with no active group

  • WHEN turu bundle pack group runs while no group is active for this repo
  • THEN the command fails with the same error and suggestion as turu resolve group

Requirement: Bundles never carry the private zone

turu bundle pack SHALL exclude everything under the checkout’s .whisper/private/ tree from the produced bundle, and SHALL refuse — with a clear error and no bundle produced — any scope whose resolved path lies under .whisper/private/ (a guard binding future routing changes; no shipped scope resolves there today). turu bundle unpack SHALL never create, extend, or modify files under .whisper/private/, and SHALL place entries at the paths resolved under current resolution rules — a bundle packed before this change unpacks into the repo-local root. The exclusion SHALL apply to the exact top-level path only; a private/ directory nested deeper under .whisper/ is ordinary content.

Scenario: Pack refuses private-resolved scopes

  • WHEN turu bundle pack is requested for a scope whose resolved path lies under .whisper/private/
  • THEN the command fails with an error explaining the private zone is never transported
  • AND no bundle is produced

Scenario: Pack with private content

  • WHEN turu bundle pack branch runs in a checkout whose .whisper/private/branches/main/notes.md has entries
  • THEN the bundle contains no path under .whisper/private/
  • AND the envelope reports the excluded private entries count

Scenario: Unpack never writes into the private zone

  • WHEN a bundle containing an entry that would target a path under .whisper/private/ is unpacked
  • THEN that entry is skipped and reported in the envelope
  • AND no file under .whisper/private/ is created or modified

Scenario: Legacy bundle unpacks into current resolution

  • WHEN a bundle packed before this change (entries addressed to workspace-root paths) is unpacked in a checkout
  • THEN entries are placed at the paths resolved under current rules
  • AND existing-content rules (extend, never overwrite) are unchanged

workspace-maintenance Specification

Purpose

Baseline spec for existing behavior: workspace lifecycle commands — init (never-overwriting layout creation), consolidate (legacy repo-key migration), and read-only diagnostics (status, check, doctor).

Requirements

Requirement: Init creates the layout without overwriting

turu init SHALL create the checkout’s workspace slot: rules.md, the repo slot (env.md), the branch slot (notes.md, context.md, plan.md), and the worktree slot (env.md). Existing files SHALL be reported as existing and left untouched.

Scenario: Fresh init

  • WHEN turu init runs against a new checkout
  • THEN all slot files are created and reported as created

Scenario: Idempotent re-init

  • WHEN turu init runs again after a previous init
  • THEN no file is modified or overwritten
  • AND all files are reported as existing

Requirement: Consolidate migrates legacy repo-key dirs non-destructively

turu consolidate SHALL migrate legacy repo-key directory variants (bare repo name, host:name colon form matching the bare name) into the canonical key dir: rename the whole dir when the canonical dir is absent; otherwise move entries without conflict or merge text files by extending with non-duplicate lines. Colon dirs that do not match this repo’s bare name SHALL never be touched.

Scenario: Canonical dir absent

  • WHEN a legacy variant exists and the canonical dir does not
  • THEN the legacy dir is renamed to the canonical dir
  • AND the envelope reports it as moved

Scenario: Merge extends without duplicating

  • WHEN a legacy variant exists alongside the canonical dir and both contain overlapping lines in a text file
  • THEN the merged file contains each line exactly once
  • AND the legacy dir is removed afterwards

Scenario: Unrelated colon dirs are safe

  • WHEN repos/ contains a colon-form dir belonging to a different repo
  • THEN turu consolidate leaves it untouched

Requirement: Diagnostics are read-only

turu status, turu check, and turu doctor SHALL report workspace state (paths, existence flags, legacy variants, group definitions, missing files, managed-block state) without modifying any file.

Scenario: Status is side-effect free

  • WHEN turu status runs
  • THEN the workspace layout is unchanged afterwards

Scenario: Check flags legacy variants

  • WHEN legacy repo-key dirs exist
  • THEN turu check reports them together with the canonical key to route new knowledge into

Requirement: Init and sync ensure the private-zone ignore rule

turu init and turu sync SHALL ensure the checkout’s .whisper/private/ is effectively ignored: appending the rule .whisper/private/ to the root .gitignore when git check-ignore does not already report the path as ignored (e.g. when covered by a nested rule), never duplicating an existing rule, and never modifying or removing any other entry.

Scenario: Fresh init adds the rule

  • WHEN turu init runs in a checkout whose .gitignore lacks .whisper/private/
  • THEN the rule is appended exactly once
  • AND the envelope reports it as created

Scenario: Idempotent ignore rule

  • WHEN turu init runs again after the rule was added
  • THEN no duplicate rule is appended
  • AND existing .gitignore content is unchanged

Requirement: Doctor reports privacy integrity

turu doctor SHALL run three privacy checks and report them read-only via the envelope: (a) .whisper/private/ is effectively ignored — verified with git check-ignore, not by text presence in a .gitignore file (warning with a fix hint when not ignored); (b) no tracked files exist under .whisper/private/ (git ls-files — a prominent failure, since gitignore is not a hard guarantee and committed history cannot be retracted); (c) an advisory leak-shape lint on public repo-local knowledge files flagging machine-specific paths, hostnames, and token-shaped strings — reported via envelope warnings[], never blocking and never modifying files.

Scenario: Private zone not effectively ignored

  • WHEN turu doctor runs in a checkout where git check-ignore .whisper/private/ does not report the path (rule absent, or present only in a nested .gitignore whose scope does not cover it)
  • THEN the check reports a warning with the fix hint (turu init or turu sync)

Scenario: Tracked file under the private zone

  • WHEN .whisper/private/env.md is tracked by git (committed before the ignore rule, or force-added)
  • THEN the doctor check fails prominently, stating that the file is published in history and prevention cannot be retroactive

Scenario: Leak-shaped content in public files

  • WHEN a public repo-local knowledge file contains machine-specific paths such as /home/<user>/ or token-shaped strings
  • THEN doctor emits an advisory warning naming the file and match shape
  • AND the command still exits successfully

workspace-routing Specification

Purpose

Baseline spec for existing behavior: canonical repo-key derivation, branch slugs, worktree slots, and scope resolution — the deterministic addressing that makes knowledge routing a pure function of the checkout, never a guess.

Requirements

Requirement: Canonical repo key is a pure function of the remote URL

The repo key SHALL be derived only from the origin remote URL: scheme and git@ prefixes stripped, first colon replaced with a slash, trailing .git and slashes removed (git@host:org/repo.git → host/org/repo). The derivation SHALL NOT depend on machine identity.

Scenario: SSH remote

  • WHEN origin is git@github.com:org/repo.git
  • THEN the repo key is github.com/org/repo

Scenario: HTTPS remote

  • WHEN origin is https://host/org/repo.git
  • THEN the repo key is host/org/repo

Scenario: No remote

  • WHEN the checkout has no origin remote
  • THEN the repo key is local/<basename-of-repo-root>
  • AND the key is the same on every machine for the same directory name

Requirement: Branch slug normalization

The branch slug SHALL be git rev-parse --abbrev-ref HEAD with / replaced by --; outside a repo the slug SHALL be none.

Scenario: Feature branch

  • WHEN HEAD is feature/login-fix
  • THEN the branch slug is feature--login-fix

Requirement: Worktree slot

The worktree slot SHALL be the basename of the git common dir, falling back to the basename of the working directory outside a repo. In practice the common dir basename is .git and is shared by every worktree of the same repo — the slot does not distinguish linked worktrees from the main checkout (the worktree scope’s value is machine/checkout-local setup, not per-worktree identity).

Scenario: Linked worktree shares the slot

  • WHEN a linked worktree and the main checkout of the same repo both resolve the worktree scope
  • THEN both get the same slot (basename of the shared common dir, typically .git)
  • AND their worktree env resolves to the same path

Scenario: Bare repo

  • WHEN the checkout is a bare repo whose common dir is the bare dir itself
  • THEN the slot is the bare dir’s basename

Requirement: Scope resolution is exact and total

Each scope SHALL resolve to exactly one path. global → <workspace-root>/rules.md; worktree → <workspace-root>/repos/<repo-key>/worktrees/<slot>/env.md; group → <group-root>/repos/<repo-key>/env.md. For repo and branch, the knowledge root SHALL be the checkout’s repo-local .whisper/ directory: repo → .whisper/env.md; branch → .whisper/branches/<branch-slug>/notes.md. The private destination mirrors the repo-local layout under .whisper/private/ (private/env.md, private/branches/<branch-slug>/notes.md) and SHALL be reachable deterministically via the --private routing flag on append/resolve. The global-store equivalents (repos/<repo-key>/env.md, repos/<repo-key>/branches/<branch-slug>/notes.md) SHALL remain valid destinations via the explicit --global escape hatch. Resolution SHALL never guess or search.

Scenario: Branch scope resolves into the checkout

  • WHEN turu resolve branch runs in a repo on branch main
  • THEN the target is <repo-root>/.whisper/branches/main/notes.md

Scenario: Private routing flag

  • WHEN turu append branch --private --text ... runs on branch main
  • THEN the entry is written to <repo-root>/.whisper/private/branches/main/notes.md

Scenario: Global escape hatch

  • WHEN turu append branch --global --text ... runs in a repo with key github.com/org/repo
  • THEN the entry is written to <workspace-root>/repos/github.com/org/repo/branches/main/notes.md

Scenario: Group scope without an active group

  • WHEN turu resolve group runs and no group is configured for this repo
  • THEN the command fails with an error and a next-step suggestion
  • AND no path is returned

Scenario: Unknown scope rejected

  • WHEN turu resolve nonsense runs
  • THEN the command fails with an error listing the valid scopes

Requirement: Recall composes repo-local and global stores in store-major order

turu recall for repo and branch scopes SHALL serve entries from the checkout’s repo-local root (including the private zone) and from the workspace root for the repo’s key, in store-major order: all repo-local entries by recency first, then all workspace-root entries by recency, under the whole-entry byte budget. Entries SHALL be deduplicated by id across stores. Knowledge stored in the workspace root before this change SHALL remain servable.

Scenario: Pre-existing global-store knowledge stays servable

  • WHEN an entry exists only in <workspace-root>/repos/<repo-key>/env.md and turu recall repo runs in the checkout
  • THEN the entry is served from the global store

Scenario: Store-major composition order

  • WHEN repo-local and workspace-root stores both contain entries for the repo scope and a workspace-root entry is more recent than some repo-local entries
  • THEN all repo-local entries are served before any workspace-root entry
  • AND within each store, entries are ordered by recency
  • AND no entry appears twice

Requirement: Private zone is read-included and publish-excluded

.whisper/private/ SHALL be the canonical private zone of a repo workspace: included in recall composition, and excluded — by the exact top-level path .whisper/private/ — from every publication and transport verb (bundle pack, bundle unpack, managed-block generation). No verb SHALL write into the private zone except explicit private-scope operations. turu consolidate operates on the workspace root only and structurally cannot reach the checkout; if it is ever extended to checkout-local knowledge, the private zone SHALL remain excluded from its moves and merges. The exclusion SHALL NOT apply to any other path, including a private/ directory nested deeper under .whisper/.

Scenario: Recall serves private knowledge locally

  • WHEN an entry exists under .whisper/private/branches/main/notes.md and turu recall branch runs on main
  • THEN the entry is served

Scenario: Consolidate cannot reach the private zone

  • WHEN turu consolidate runs against the workspace root
  • THEN it scans only legacy repo-key variants under the workspace root
  • AND if it is ever extended to checkout-local knowledge, no file under .whisper/private/ is moved, merged, or removed

Requirement: Declarative per-repo routing mode

<repo>/.whisper/config.toml SHALL accept a routing key with value repo-local or machine-local, defaulting to repo-local. The mode SHALL be the persistent default destination for the repo and branch scopes: repo-local resolves them into the checkout’s committed .whisper/ zone (decision A); machine-local resolves them into the machine-local store — the persistent equivalent of the --global escape hatch. Per-invocation overrides compose on top: --private keeps routing into the checkout’s private zone and group routing keeps priority when a group is active. An unknown mode value SHALL fail the invocation with an error naming the valid values.

Scenario: Machine-local routing is the persistent default

  • WHEN .whisper/config.toml sets routing = "machine-local" and turu append branch --text ... runs
  • THEN the entry is written to the machine-local store without any flag

Scenario: Repo-local remains the default without a routing key

  • WHEN the repo config has no routing key
  • THEN repo/branch scopes resolve into the checkout’s committed zone (decision A)

Scenario: Unknown routing mode is rejected with the valid values

  • WHEN the repo config sets routing = "cloud"
  • THEN the invocation fails with an error naming repo-local and machine-local

Scenario: The private zone override composes over the declared mode

  • WHEN turu append branch --private --text ... runs under machine-local routing
  • THEN the entry is written to <repo-root>/.whisper/private/branches/main/notes.md

Requirement: Init provisions the declared routing layer

turu init SHALL provision exactly the layer the declared routing mode resolves to for repo/branch scopes: both checkout and store layers under repo-local (recall composes both), only the store layer under machine-local — never both provisioning passes for the same scope.

Scenario: Init under machine-local routing provisions only the store

  • WHEN turu init runs with routing = "machine-local"
  • THEN the store slots for repo/branch are created
  • AND no checkout repo/branch slot files are created

Requirement: Doctor reports the declared routing mode

turu doctor SHALL report the declared routing mode and the effective repo/branch route via a check, and SHALL warn when machine-local routing is active while the checkout layer still carries content (the mode switch is a config edit; leftovers must be surfaced, not silent).

Scenario: Doctor flags stale checkout-layer content under machine-local

  • WHEN turu doctor runs with routing = "machine-local" and the checkout’s repo slot file has content
  • THEN the routing check warns naming the stale files with a migration hint
  • AND a clean switch reports the store route and no warning