This is the record of an abcd-managed repository

abcdBetav0.16.2 · 2026-11-06

Development record

memory-recall

specopenspc-2610111627213138

Summary

This spec delivers the memory-recall bundle: memory notes (itd-2610091918433290) and memory pages recalled at the right moment (itd-39). An agent keeps short notes with /abcd:memory, and the loader that already brings in the project's rules brings a note, and any longer memory page that matches, back into the session when a prompt matches their words. Notes start on the person's machine. Reviewing them and promoting one into the project's record are a later dashboard's (itd-2610111655362931, the product thinker's ruling of 2026-10-11); until it ships, the review is memory list and memory remove, and this bundle reads a note in the project's file wherever one is.

The approach, in plain words:

  • A memory.json beside each rules.json. The machine's ~/.abcd.noindex/memory.json holds personal notes and the project's .abcd/memory.json holds promoted ones (the facilitator's ruling of 2026-10-11). The loader reads both with the trust checks it already applies to the two rules.json layers. Agents write the machine's memory.json; people curate rules.json, so no agent's note ever edits the file that holds reviewed conventions (one-writer-per-file).
  • Write first, review later. An agent writes any note without asking. Each note is stamped with who wrote it, when and in which session, and the redactor and the private-names banlist refuse a note that would hold a secret, a private name or a network address (adr-2610091918443997). Notes ship before the dashboard's review: until then, the review is memory list and memory remove, which removes any personal note, run by a person or by an agent at a person's request (the product thinker's ruling of 2026-10-11).
  • One recall for notes and pages. The prompt hook recall-matches the prompt against rules, notes and pages in one pass. Notes and pages come back together in one block after the rules, deduplicated per session the way rules are.
  • Everything shrinks as the session fills, and nothing goes silent. One table of context brackets in the loader serves rules, notes and pages. Notes and pages come back in full, then as one line each, and stay at one line to the end of the session; a rule domain comes back in full, then as its heading and first rule. A relevance ranking chooses which items survive when only some fit. The hook learns how full the session is from the status line's fill file when there is one (the one file the status line may write, by the product thinker's ruling of 2026-10-11), and otherwise from a bounded read of the transcript's tail. When neither tells it, rules come in full, as today, and notes and pages as one line each. The loader has no brackets today (see Design, "Context brackets"), so this bundle builds them once, in the loader.
  • One diagnostic. abcd rules --prompt "<text>" shows the rules, notes and pages one prompt would bring back, with the ranking's reasons.
  • No promotion here. This bundle has no promote operation, in the store or on the verb, so there is nothing an agent could promote with; the dashboard intent carries promotion and its person's-act rule.

How each acceptance criterion is met. N1 to N8 are the notes intent's, in the order it lists them since its review and promotion criteria moved to itd-2610111655362931; P1 to P5 are itd-39's.

Criterion Met by
N1, a note written unasked comes back next session on a prompt about tests Step 1 writes it with no question; step 2 recalls it; step 8's live session shows it across two sessions.
N2, a matching note and a matching page come back together through one recall Step 3 adds pages to the recall step 2 built; one call renders one block.
N3, a lesson the person points at in the host's memory files is filed as a note; nothing reads or changes those files on its own Step 1, memory add --moved-from-host, which takes text only and opens no path.
N4, the docs set memory beside the library in plain words Step 7.
N5, personal notes in the machine's file, promoted notes in the project's Step 1's two layers, add writing only the machine's; step 2 recalls a note from the project's file.
N6, a note never holds a secret, a private name or a network address Step 1's write gate, run on every write.
N7, every note records who wrote it, when and in which session Step 1's stamps, with its page passing the host's session id; a host without one is recorded as unknown, said on every write.
N8, recall shrinks as the session fills: full, then one line, to the end of the session Step 6, over the fill step 5 reads.
P1, a page declaring recall: [auth, login] comes back on a prompt containing "auth", and no other page Step 3.
P2, below 40% free, three matched pages come back as their index lines only Step 6, the depleted bracket.
P3, a repeated prompt injects matched memory once, until the refresh Step 2's ledger, which step 3's pages share.
P4, a keyword matching a page in both scopes brings the workspace page and suppresses the user page Step 3, narrower scope wins.
P5, the host's memory directory is never read or written by the retrieval path Step 3's test over the recall's file opens, extended in step 5 to the fill reader's; N3's test covers the write side.

Where the two intents' criteria overlap (the brackets in N8 and P2; the host's memory files in N3 and P5), both criteria stand and one mechanism serves both, as itd-39's Decisions record.

Parts of itd-39's body predate the shipped loader and the verb split (itd-2610090831227812, shipped by pull request #936 on 2026-10-11), and this spec reads them as follows without editing the intent: its hooks/prompt_router_hook.py is the binary's hook prompt-router; its /abcd:memory ask is library ask; its abcd memory recall diagnostic is rules --prompt "<text>", as its Decisions now record; and its reconciliation with itd-3's rejection of a personal rules.json is moot, since the machine layer ~/.abcd.noindex/rules.json has since shipped.

Design

The memory.json file and its two layers

One schema serves both layers. The machine layer is ~/.abcd.noindex/memory.json; the repository layer is <repo>/.abcd/memory.json.

{
  "schema_version": 1,
  "notes": [
    {
      "id": "mem-2610111200001234",
      "title": "The tests need the race flag",
      "body": "Run go test -race ./internal/... before saying the tests pass; the plain run misses the data race in the hook tests.",
      "recall": ["test", "tests", "race"],
      "root": "<the repository's root commit, full object name>",
      "origin": "agent",
      "written_by": "Claude:claude-opus-5-5",
      "written_at": "2026-10-11T12:00:00Z",
      "session": "<the host's session id, or null where the host supplies none>"
    }
  ]
}
  • id is minted through the shared record-id seam (recordid.Minter, adr-45) with the family mem, so two sessions writing at once never collide and no coordination is needed.
  • title is one line of at most 120 characters, body at most 2 KiB, and recall one to twelve words or short phrases, matched by the loader's own term rule (word boundary, stems, multi-word phrases). The figures are this spec's, set in one place, and a write past them is refused naming the limit.
  • root ties a personal note to the repository it was written in: it is recalled only in that repository. A note written with --everywhere carries "root": null and is recalled in every repository on the machine (the facilitator's ruling of 2026-10-11). The repository layer carries no root: the file is already the project's.
  • origin is agent or host-memory-move (the manual move, below).
  • written_by takes the shape of the Assisted-by: trailer (<Vendor>:<model-version>), so who wrote a note reads the same way as who assisted a commit.
  • session is the host's own identifier for the session, which the command page passes (below). Where the host supplies none it is null, rendered "session unknown" wherever the note is shown.
  • The review's markers (when a note was last reviewed) and the promotion's stamps (who promoted it and who agreed) are not in this schema. The dashboard intent adds them, under a schema_version the reader then accepts.

Reading follows the rules.json layers exactly. The machine file is read only through fsutil.ReadHomeDeclaration: a regular file this account owns that no one else can write, within 256 KiB, never behind a symlinked ~/.abcd.noindex; a file failing those checks or failing to parse is a loud error naming the file in tilde form, and the hook then recalls no notes from it, while rules still inject. Absent, it costs nothing and nothing is created. The repository file is read through the repository layer's guarded read, from the root the rules loader resolved (a refused foreign-uid root reads no .abcd/memory.json above the working directory, as with rules.json). The kill switch in either rules.json silences note and page recall as well as rules: it is the loader's switch, and recall is the loader's.

Writing touches the machine layer only; nothing in this bundle writes the repository layer. Until the dashboard's promotion ships, a note reaches the project's file only through a person's change to it, reviewed like any other change to a committed file, and the loader recalls whatever valid notes that file holds. A write opens ~/.abcd.noindex in one walk from the home's descriptor (fsutil.EnsureHomeScope), takes an exclusive flock(2) on the directory at that path (fsutil.WithDirLock, which opens it O_NOFOLLOW and leaves no lock file), re-reads the file under the lock relative to the descriptor the walk opened (rules.ReadUserLayerFileIn), applies the change, and writes through that same descriptor with fsutil.WriteFileAtomicInRoot with mode 0600, so two agents writing at once both land. A directory renamed after the walk makes the re-read refuse, so nothing is written rather than one directory's notes replacing another's. The machine file is never created by a read and is created 0600 by the first write.

The write gate: redaction, banlist and stamps

Every note write passes one gate in internal/core/memnote before anything is written:

  1. The shared scanner (internal/adapter/scanner), the one canonical detector capture, history and the memory-page writer already route through, reached through the memory-page writer's own redactor, exported for it. The gate refuses on a degraded scanner, then scans the note's title, body and each recall word, and refuses on any span the memory-page writer would have to rewrite: any hard-fail secret, any identity or network span whatever its severity (scanner.BlockingResidual), and the literal home-directory backstop. Unlike the page writer, it never redacts and writes: the refusal names the kind of span and the field, never the span, and the agent rewrites the note in its own words, since a lesson with a span cut out of it may say something wrong.
  2. The private-names banlist, through banlist.MatchPrivate, which the implement loop already uses. A store that cannot be read is treated as a match, as that function's contract requires. The banlist's private layer is per checkout (.abcd/.work.local/private-names.txt); in a linked worktree the gate also judges the note against the primary checkout's store, found through banlist.PrimaryWorktreeRoot, which is the layer the committed guard enforces there. There is no machine-wide banlist layer, so a note written with --everywhere, recalled in every repository, has been judged against the writing checkout's names only. add --everywhere says so on every write, and the command page says it too. Judging it against every checkout's names instead would need a machine-wide banlist layer that does not exist, which is a banlist change outside this bundle.
  3. The stamps. written_at comes from the clock, never from the caller. written_by and session come from the caller (step 1's page), are shape-checked (a session that looks like a link is refused, so no session URL can enter a note), and are judged by the gate like any other field.

The gate is the only path into the machine file. How the dashboard's promotion judges a note entering the project's file is that intent's to plan.

One recall for notes and pages

A new package, internal/core/recall, holds what notes and pages share: candidate items, matching, ranking and rendering. The prompt router calls it once per prompt, after rules.Inject, with the items both note layers and both page scopes yield. It returns one block, headed ## MEMORY, appended after the rules block in the same hook output.

  • Matching is the loader's. internal/core/rules exports its prompt index and term rule (indexPrompt, termHit, the stemmer) as one small function, so a note's recall word and a rule domain's recall word hit on exactly the same prompts. An item is a candidate when any of its recall words hits; the ranking orders candidates and never adds one.
  • Items. A note yields its title, body and recall words. A page yields its filename, its index.md summary line as its title, its body, and its recall: frontmatter list, which the memory-page schema already carries (internal/core/memory/schema.go). A page without a recall: list is never a candidate, so pages already written are silent until a recall list is added.
  • Scopes. Pages are read from the checkout's .abcd/memory/ and, when it exists, the machine's ~/.abcd.noindex/memory/ (read only, through fsutil.OpenHomeScope; nothing in this bundle writes it). When a recall word hits a page in both scopes with the same <type>_<domain>_<slug> name, the workspace page is the candidate and the machine page is suppressed (P4). Notes follow the same rule: an id is minted once, so a note in the project's file and one in the machine's share an id only if someone copied it by hand, and then the project's is the candidate and the machine's is suppressed.
  • Dedup. The session ledger gains a second map, recalled, keyed by item (mem-… or page:<filename>) with a signature of the item's content, not of the form it was rendered in, so a bracket change never sends again an item already sent in any form (Context brackets, below). It is separate from the rules ledger because rules.Inject prunes any ledger name that is not an active domain, which would forget every item each turn. The same resets apply: SessionStart and PreCompact clear both maps, and the fixed backstop clears both on the same prompt (P3).
  • Budget. Rules and memory share the loader's 64 KiB per-turn injection budget. Rules render first and memory takes what remains; an item that does not fit is not recorded in the ledger, so it is retried next turn, as a truncated rule domain is today. Until step 4's ranking lands, the candidates that fit are taken in id order (mem-… before page:…), and step 4 replaces that order with the ranking.
  • The host's memory files are never among the paths the recall opens. The recall reads exactly four places: the two memory.json layers and the two page scopes. A test runs the recall through a filesystem seam that records every open, with a home directory holding a host memory directory full of matching text, and asserts the recorded opens are only those four (P5). Step 5 extends the same test to the fill reader, whose opens are the status line's fill file and the one transcript the hook's payload names, and nothing under the host's memory directory.

The --json envelope of hook prompt-router gains a recalled list naming each item injected, beside injected for domains, and (step 5) a fill object naming the fill and where it came from, so a reader can tell what came back and why.

Ranking

When only some matched items fit, a relevance ranking chooses which ones stay, as the facilitator ruled. The ranking is BM25 with per-field weights (BM25F), computed in about a hundred lines of standard-library Go, with no new dependency.

  • Query terms are the prompt's stemmed tokens, from the same prompt index the matcher builds.
  • Fields and weights. A term found in an item's recall words counts three times, in its title twice, and in its body once. The weighted frequencies are summed per term before saturation, as BM25F prescribes.
  • Score. For each query term t present in the item, idf(t) · tf_w / (tf_w + k1 · (1 − b + b · len / avg_len)), summed over the terms, with k1 = 1.2, b = 0.75, and idf(t) = ln(1 + (N − n_t + 0.5) / (n_t + 0.5)) over every note and page in reach (N items, n_t of them holding t). A word every item shares counts for little; a rare word that matches counts for a lot.
  • Ties break, in order, on the narrower scope (repository before machine), then the more recent written_at (or page modification), then the id, so the order is the same on every machine and every run.
  • Explainable. abcd rules --prompt "<prompt>" prints, for each candidate, its score, each matched term with its field and its contribution, the bracket in force, and whether it would come back in full or as one line, or not fit the budget. Nothing about the choice is hidden.

Prior art: BM25 is the probabilistic relevance framework's ranking function (Robertson and Zaragoza, "The Probabilistic Relevance Framework: BM25 and Beyond", 2009), and BM25F its multi-field extension (Robertson, Zaragoza and Taylor, "Simple BM25 Extension to Multiple Weighted Fields", 2004). It is the default similarity in Lucene since version 6 and the ranking SQLite's FTS5 bm25() exposes, with per-column weights. The fit challenge the prefer-sota principle asks for set aside the two stronger-sounding options: dense embeddings need a model or a network call inside a hook that runs on every prompt, against the host-delegated and minimal-dependency stances and the hook's fail-closed, never-blocking contract; a learned re-ranker needs training data abcd does not have. A raw count of matching words, the order library ask uses today, is what the ruling rejected: it lets a common word outrank a rare, telling one. library ask keeps its order; changing it is not in this bundle.

Context brackets

The facilitator ruled that memory recall shares the rules loader's context brackets (itd-3) with no thresholds of its own, and then that the brackets gate rules too, with the same thresholds. The shipped loader has no brackets: it has a fixed per-turn byte budget, and nothing in it reads how full the session is. The brackets exist only in itd-39's design (the brief's 05-internals/07-memory.md § 9). So this bundle builds one bracket table in internal/core/rules, beside the byte budget, and the rules injection and the recall both read it from there, so no payload can grow thresholds of its own.

The table starts from itd-39's figures, read as the fraction of the context window still free. There are three brackets of fill and no fourth in which recall sends nothing: the facilitator ruled that recall never sends nothing, because a compaction or a new session resets recall anyway, so the one-line forms cost little and a session never loses sight of what matches.

Bracket Free Rules Notes and pages
Fresh 70% or more Every matched domain in full, as today. Every candidate in full, in ranked order, within the budget.
Moderate 40% to 70% Each matched domain as its heading and first rule, then a line saying how many rules it holds and that abcd rules <DOMAIN> renders the rest. The top-ranked candidates in full within 8 KiB, the rest as one line each.
Depleted below 40%, to the end of the session As in moderate. One line each (a note's title and id, a page's index line), the top-ranked lines within 2 KiB, and always at least the top-ranked one.
Unknown no source tells (How full the session is, below) As in fresh. As in depleted.

The unknown row is not a bracket of fill but the technical facilitator's ruling of 2026-10-11 on what to do without one: rules keep the form they have today, and notes and pages come back as one line each.

  • A shrunk domain is never a heading alone: the loader already skips a domain with no rules, because a heading-only block reads as a domain that says nothing, and the first rule plus the pointer line keep that true. Rules are not reordered to choose the first. In a domain written in a rules.json layer, the first rule is the one its author put first. SHELL's and GRILL's rules are generated, so for them no author chooses: SHELL's first rule is the first hazard-registry entry in id order (internal/core/rules/shell.go), and GRILL's the first rule question.AskingRules returns (internal/core/rules/grill.go). A shrunk domain appears only under a known fill in the moderate or depleted bracket; an unknown fill keeps every domain in full.
  • An item, rule domain or note or page, is sent once per session in whatever form its bracket gave it, and is not sent again in a larger or smaller form when the bracket changes: the ledgers key on content, not on form. The next reset (a compaction or a new session) clears the ledgers, and the item comes back in the form the bracket then in force gives.
  • The 8 KiB and 2 KiB figures are this spec's starting points, set beside the thresholds in one place.

How full the session is

The prompt hook's payload carries no measure of the context window. The facilitator ruled for both sources, the status line's first (2026-10-11):

  1. The status line's fill file. The statusline verb parses five values from the host's status payload today (internal/core/statusline/payload.go): the model, the context percentage (context_window.used_percentage), the two rate-limit percentages, and the working directory. It does not parse the session id. Step 5 adds the payload's session_id to that parse, kept out of every rendered row and JSON envelope as the working directory is, and used only to name the fill file. When the verb renders abcd's own row (never when it passes through to the person's previous status command) and the payload carries both the session id and the context percentage, it writes {"used_percentage": …, "at": …} to ~/.abcd.noindex/fill/<sha256(session)>.json, mode 0600, atomically, through the home scope's guarded write (fsutil.OpenHomeScope, so never behind a symlinked ~/.abcd.noindex). The file lives in the user-level abcd home, never in the host's configuration folder and never in a working tree. The writer and the file name live in internal/core/rules, beside the fill reader, so one package owns the file.

This is an exception the product thinker granted on 2026-10-11 ("Allow one fill file") to the ruling of 2026-10-05 that the status line writes nothing (iss-2610050556383525). It covers exactly that one figure, in one file per session, and nothing else: no cache, lock, log or ledger, and no directory. The loader creates ~/.abcd.noindex/fill/ (0700) on its session-start reset, through fsutil.EnsureHomeScope rather than os.MkdirAll, so a symlinked ~/.abcd.noindex is refused quietly and no fill/ lands under its target; the status line writes only into a directory that is already there. The two read-only tests narrow to that one file and still refuse every other write: TestStatuslineWritesNothing (internal/surface/cli) allows one changed entry across its snapshot, the session's fill file, holding only those two fields, and still requires every other case, the pass-through path among them, to leave the snapshot unchanged; TestStatusVerbReachesNoWritePrimitive (internal/core/statusline) admits exactly one call, the fill writer, from one function in internal/surface/cli/statusline.go, and refuses every other write primitive and any second call site by name, as it refuses mode.Set. The verb's help text, which says "It writes nothing", and the doc comment above newStatuslineCommand change to say it writes only the session's fill file and where. An absent directory (the session-start hook not installed, or the folder swept) is a skipped write with no notice, since a stateless verb cannot say it once; any other failed write is named on stderr, and the row still renders either way. The status line runs only where the person consented to it at install, so this source is present only there. 2. The transcript's tail. Without a fill file for the session, the hook reads the transcript its payload names (transcript_path), at most its last 256 KiB, through the guarded read (a regular file this account owns, never followed through a symlink). It takes the last assistant entry's usage, in the token-count shape internal/core/history already parses (input, cache-creation and cache-read input tokens), and divides it by the context window a per-model table in internal/core/rules declares for that entry's model, beside the bracket table. Where one model is offered with more than one window size, the table holds the smaller, so a doubt errs towards shrinking. Reading the transcript the host hands over has precedent in the transcript capture. The comment on hookInput.TranscriptPath in internal/surface/cli/cli.go, which says the field is supplied by the Stop hook and read by hook session-end only, changes to name the prompt router as its second reader. 3. Unknown fill. No fill file, no transcript, a transcript that fails the guarded read or holds no usage, or a model the table does not name: the fill is unknown. Rules then inject in the fresh form, exactly as they do today, and notes and pages come back as one line each (the technical facilitator's ruling of 2026-10-11, "Rules full, notes short"). Grounds: rules on a host where the fill is never known do not regress from today, and notes and pages, which are new, cannot flood a session that may be nearly full, while every match still shows as a line carrying its id and rules --prompt shows everything in full. The hook says the fill is unknown: the --json envelope's fill names the source as unknown on every prompt, and one line on stderr says it on the first prompt of each session (the ledger records that it was said).

The reset triggers delete the session's fill file with its ledgers, so a compaction does not leave the hook reading the full session's figure until the status line next refreshes, and the loader's existing sweep of stale session ledgers sweeps the fill directory by the same age.

The recall diagnostic: rules --prompt

The facilitator placed the diagnostic on the existing rules verb, which the verb split put with the agents (groupAgents, page commands/ahoy.md). abcd rules --prompt "<text>" shows what that one prompt would bring back:

  • the bracket in force and how it was chosen (--fill <percent-used> sets it; without the flag the diagnostic has no session, so the fill is unknown and it says it is rendering the unknown-fill form: rules in full, notes and pages as one line each);
  • each rule domain the prompt matches, with the recall words that hit and the form the bracket gives it;
  • each candidate note and page, with its score, each matched term with its field and contribution, and its form: full, one line, or not fitting the budget.

It reads against an empty ledger, so it shows what a fresh session would get, and writes nothing. --prompt with a positional domain is refused, exit 2, naming the two forms. --json carries the same facts. The verb's help text and commands/ahoy.md document it.

The /abcd:memory agents' command

/abcd:memory is an agents' page under the verb split's rules (itd-2610090831227812): block: agents, user-invocable: false, kept in the flat commands/ directory, named on an agents-block line of abcd --help --agent, and held there by TestCommandPagesMatchTheMenu. The binary verb is abcd memory, placed in groupAgents naming commands/memory.md. Every sub-verb writes only through the core in internal/core/memnote; the page carries the judgement half.

Sub-verb What it does Writes
(bare) Counts personal notes for this repository and everywhere, and notes in the project's file; names any layer that could not be read and why. nothing
add Writes one note to the machine layer: --title, --recall (repeatable), the body on stdin, --by <Vendor>:<model>, optional --session <id>, --everywhere and --moved-from-host. Prints the minted id; without --session, also prints that the session is recorded as unknown; with --everywhere, also prints that only this checkout's private names were checked. the machine layer
list Lists notes from both layers, each with its title, body, recall words, stamps, and whether it is tied to this repository or recalled everywhere: enough to review them by. nothing
remove <mem-id> Removes any personal note, whoever wrote it and for whatever reason, run by a person or by an agent at a person's request. A note in the project's file is removed by editing that file in a reviewed change, never by this verb. the machine layer

list and remove are the review until the dashboard ships (the product thinker's ruling of 2026-10-11, "Ship, list and remove"): a person reads the notes with list and removes any they do not want, or asks an agent to.

The page tells the agent when to write a note (a lesson it would otherwise relearn: a flag the tests need, a preference the person stated, a trap in the tooling), to choose recall words a later prompt would actually use, to keep one lesson per note, to add --everywhere only for a lesson that holds in every project, and never to put a secret, a private name or a network address in one, since the binary refuses it. It says that an --everywhere note has been checked against the writing checkout's private names only, so a name another project bans is the agent's to keep out. It tells the agent to run remove only when the person asks. It passes --by with the agent's own vendor and model, and --session with the host's own substitution of its session id written into the page's text, so the host fills it in and the agent never guesses it (the facilitator's ruling of 2026-10-11). Step 1's live trial confirms the substitution on the first host. Where a host offers no substitution, the page leaves --session out, and the note is recorded as session unknown, said on every write.

The manual move from the host's memory files

The person points at a lesson in the host's own memory files, or pastes it. The agent, at that word and only then, reads the passage the person named with the host's own tools, and files it with abcd memory add --moved-from-host, the text on stdin. The binary stamps origin: host-memory-move and judges it through the same gate. The binary takes no path operand on add, so it never opens the host's files at all, on this verb or any other; the page tells the agent never to scan those files for candidates, and never to edit or delete the original. Removing it there is the person's choice, in the host. abcd's ruling of 2026-10-05, that abcd never touches the person's host configuration folder, stands unchanged.

Review and promotion: on the dashboard

The product thinker moved the review and promotion to a later dashboard where the product thinker and the technical facilitator click the notes they want to promote (2026-10-11). The two criteria and the two open questions on the maintainer count and the agreement are carried, verbatim, by the draft itd-2610111655362931, which builds on the notes intent and the dashboard intent (itd-2610032150577708).

Notes ship before that review (the product thinker's ruling of 2026-10-11, "Ship, list and remove"). Until the dashboard ships, the review is memory list and memory remove, for any personal note, run by a person or by an agent at a person's request; the dashboard draft is the review's later home, as adr-2610091918443997's Consequences record.

This bundle keeps no promote operation, neither on the verb nor in the store for the dashboard to call later: a store operation with no caller until the dashboard ships is unwired code, which the wired-or-it-isn't-done boundary refuses. With no promote operation there is nothing for an agent to promote with, so this bundle adds no guard entry; the guard entry and terminal check the earlier draft of this spec designed (after the source ledger's flip, adr-41) go to the promotion intent as its starting design. What remains here is the repository layer's read, so a note a person puts in the project's file is recalled, and the schema the dashboard will extend.

Provenance

Every note answers who, when and in which session, in the file, in list and in the rules --prompt output. written_by and session are the writer's stamps, and written_at the clock's. The stamps are never rewritten after the fact. The session id is the host's identifier for the session, never a link to it, so it carries no session URL; where the host supplies none, the note says "session unknown" rather than guessing.

A recalled note shows its source wherever it comes back. In the ## MEMORY block each note's head names its id and its layer: (project file, committed in this repository) for a note in the project's .abcd/memory.json, and (personal note, agent-written) for one in the machine's file. An override's rule heading says (repo override) in the same way (GHSA-22f8-qf5r-gjgq). The project's file holds whatever the repository's authors committed, so a cloned or compromised repository controls it, and its notes must never read as the person's own. Each body sits in a tilde fence under its head, through the rules package's one fence for another party's text (rules.FenceRuleBody, iss-2609020219265817). A heading, a list item or a setext underline in a body is then text to a markdown reader, not structure. The hook's stderr line names each recalled id with its layer, for example abcd memory: recalled 2 note(s) [mem-… (project), mem-… (machine)]. The layer is part of an item's ledger signature, so a note promoted from the machine's file into the project's comes back again under its new label. For the same reason, a session that recalled notes before the layer joined the signature gets each of them once more on its next matching prompt, now labelled. That one-time re-send is expected, as it was when the rules' override label joined their signature.

How the steps land

Each step below lands green and wired on its own. Step 1's store has its verb from the first commit. Step 2's recall runs in the hook and on rules --prompt, choosing survivors in id order. Steps 3 and 4 extend that same path. Step 5's fill is written by the status line, read by the hook and shown in the envelope before anything acts on it, and step 6 acts on it. Steps 7 and 8 change no code.

Steps

  1. The notes store and the abcd memory verb: the memory.json schema, its two layers, the write gate, and bare, add, list, remove and the manual move
    • packages: internal/core/memnote (new: schema.go, layers.go, write.go, gate.go), internal/core/recordid (the mem family named where families are listed), internal/core/rules (exported layer-read helpers the store reuses, no behaviour change), internal/surface/cli (memory.go, helpgroups.go placing memory in groupAgents naming commands/memory.md), internal/core/surface (sentences.go, examples.go), commands/memory.md (new, block: agents, user-invocable: false, passing the host's session substitution, saying that --everywhere notes were checked against this checkout's private names only, and that remove runs at the person's request), .abcd/development/brief/04-surfaces (a memory chapter with its ## Sub-verbs table, the README registry row), .abcd/rules.json (DOGFOODING recall gains memory), .abcd/development/release/surface.json, docs/reference/cli/commands.md
    • tests: TestNoteSchemaRefusesEachMalformedField; TestMachineLayerReadRefusesWhatRulesJSONRefuses (symlinked home, foreign owner, writable by others, over 256 KiB, not a regular file), mirroring the user_layer tests; TestAbsentLayersCostNothingAndCreateNothing; TestWriteStampsTimeAndMintsAMemID; TestWriteWithoutASessionRecordsItUnknown; TestASessionShapedLikeALinkIsRefused; TestEverywhereNoteCarriesNoRoot; TestWriteTouchesOnlyTheMachineLayer (N5); TestConcurrentWritesBothLand; TestGateRefusesASecretANetworkAddressAndAPrivateName with secret-shaped fixtures built at run time, each watched fail before the gate exists (N6); TestGateInALinkedWorktreeJudgesThePrimarysPrivateNames; TestGateRefusesOnADegradedScanner; TestGateNamesTheFieldNeverTheSpan; TestMemoryAddWritesTheMachineLayer; TestMemoryAddTakesNoPathOperand and TestMovedFromHostStampsTheOrigin (N3), watched fail first; TestMemoryAddWithoutSessionSaysUnknown (N7); TestMemoryAddEverywhereSaysOnlyThisCheckoutsNamesWereChecked; TestMemoryListShowsEachNotesBodyStampsAndReach; TestMemoryRemoveRemovesAnyPersonalNote, watched fail first; TestMemoryRemoveRefusesANoteInTheProjectsFile; TestBareMemoryNamesAnUnreadableLayer; TestCommandPagesMatchTheMenu, TestPluginPersonListEqualsTheCLIs and TestCommandPagesDeclareTheirBlock pass with the new page; the sentence, appendix, snapshot and dogfooding tests pass after go generate ./internal/surface/cli
    • the memory store's redactor (storeRedactor in internal/core/memory) is exported for the gate rather than a second one written (one-canonical-primitive)
    • the store and its verb land together, so the store has a caller from its first commit and no unwired code reaches main; a note written here is listed and removed at once, and is recalled from step 2
    • a live trial in a host session checks that the host substitutes its session id into the command page, as the verb split's trial checked hiding; the capture goes under this spec's ## Live captures. If the first host offers no substitution, the page leaves --session out and every note on that host is recorded as session unknown, which is the ruled fallback, not a reopened question
    • landed: c8d1ab0df, 9ae13776b
  2. One recall: notes come back through the prompt hook, and rules --prompt shows what a prompt brings back
    • packages: internal/core/recall (new: items, matching, rendering, the recalled ledger keyed on content; candidates that do not all fit the budget are taken in id order until step 4), internal/core/rules (exported prompt-index and term-match function; SessionState gains recalled; the reset clears it), internal/surface/cli (the prompt router calls recall after rules.Inject; the --json envelope gains recalled; rules --prompt listing matched domains with their hitting words, and matched notes), commands/ahoy.md (documents rules --prompt), .abcd/development/release/surface.json and docs/reference/cli/commands.md (the new flag)
    • tests: TestANoteComesBackOnAPromptAboutTests over two simulated sessions (N1), watched fail first; TestNoteRecallUsesTheRuleTermMatch (stems and phrases hit as a domain's recall words do); TestANoteInTheProjectsFileIsRecalled (N5); TestNoteTiedToAnotherRootIsNotRecalled and TestEverywhereNoteIsRecalledInAnyRepository; TestRecalledLedgerSurvivesTheRulesPrune; TestResetClearsBothLedgers; TestKillSwitchSilencesRecall; TestARefusedMachineLayerStillInjectsRules; TestMemoryTakesOnlyWhatTheRulesLeaveOfTheBudget; TestCandidatesThatFitAreTakenInIDOrder (replaced by step 4's ranking tests); TestRulesPromptShowsMatchedDomainsAndNotes and TestRulesPromptWritesNothing, watched fail first; TestRulesPromptRefusesAPositionalDomain; the existing inject and hook tests pass unchanged when no note matches (a healthy no-match still renders zero tokens)
    • landed: e6d2a7df7, 59b69f398
  3. Pages join the same recall, from both scopes
    • packages: internal/core/memory (an exported read of each page's filename, index line, body and recall: list, through the existing store handle; a read-only open of ~/.abcd.noindex/memory/), internal/core/recall (page items, narrower scope wins; pages join the id order as page:<filename>, after notes, until step 4), internal/surface/cli (the router and rules --prompt hand pages to recall)
    • tests: TestAPageDeclaringAuthComesBackAndNoOther (P1); TestANoteAndAPageComeBackTogether (N2); TestRepeatedPromptInjectsMemoryOnce (P3); TestWorkspacePageSuppressesTheMachinePage (P4); TestRecallOpensOnlyTheDeclaredPlaces with a host memory directory of matching text under the test home (P5), each watched fail first; TestPageWithoutRecallIsNeverACandidate; TestRulesPromptShowsMatchedPages; the memory store's symlink and guarded-read tests pass against the new read
    • landed: 57b55ed1f
  4. The relevance ranking and its reasons
    • packages: internal/core/recall (ranking.go: BM25F as designed, ties; the ranked order replaces the id order in deciding which items fit the budget), internal/surface/cli (rules --prompt prints each candidate's score and each matched term's field and contribution)
    • tests: TestRareMatchingWordOutranksACommonOne, the case a raw count gets wrong, watched fail against a count-based order; TestRecallFieldOutweighsBody; TestTiesBreakOnScopeThenRecencyThenID; TestRankingIsDeterministicAcrossRuns; TestRankingDecidesWhichItemsFitTheBudget, watched fail against step 2's id order; TestRulesPromptShowsEachTermsContribution; golden scores for a small fixed corpus so a change to the formula is a visible diff
    • landed: f752318ff
  5. How full the session is: the status line's fill file, the transcript's tail, and unknown
    • packages: internal/core/statusline (payload.go parses the payload's session_id, never rendered or echoed), internal/surface/cli (statusline.go writes the fill file through the one fill writer, only on the path that renders abcd's own row; its help text and the doc comment above newStatuslineCommand say it writes only that file and where; cli.go's comment on hookInput.TranscriptPath names the prompt router as a second reader; the prompt router reads the fill and reports it in the --json envelope's fill; the session-start reset creates ~/.abcd.noindex/fill/ and the resets delete the session's file), internal/core/rules (the fill writer and reader, the file name under ~/.abcd.noindex/fill/, the per-model window table, the stale sweep extended to the fill directory), internal/core/history (the token-count shape exported for the fill reader, no behaviour change)
    • tests: TestStatuslineRecordsTheFillForItsSession, watched fail first; TestStatuslineWritesOnlyTheFillFile (the narrowed TestStatuslineWritesNothing: one changed entry, the session's fill file under ~/.abcd.noindex/fill/ holding only used_percentage and at, and every other case, the pass-through path among them, unchanged); the narrowed TestStatusVerbReachesNoWritePrimitive (exactly one fill-writer call from one function in statusline.go; any other write primitive or second call site refused by name), with a fixture of a second write watched fail; TestStatuslineCreatesNoDirectory; TestStatuslineWritesNothingWithoutASessionID; TestStatuslineRowRendersWhenTheFillWriteFails; TestFillFileIsRefusedBehindASymlinkedHome; TestFillPrefersTheStatusLineFile; TestFillFromTheTranscriptTail, watched fail first; TestTranscriptReadIsBoundedToTheTail; TestRefusedTranscriptYieldsUnknownFill (symlink, foreign owner); TestUnnamedModelYieldsUnknownFill; TestAmbiguousWindowTakesTheSmaller; TestResetDeletesTheFillFile; TestResetCreatesTheFillDirectoryAndRefusesASymlinkedHome; TestAbsentFillDirectoryIsASilentSkip; TestStaleSweepReachesTheFillDirectory; TestHookEnvelopeNamesTheFillAndItsSource; TestRecallOpensOnlyTheDeclaredPlaces extended to the fill reader's two (P5); the statusline help snapshot updated
    • the hook reports the fill and does not yet act on it, so this step changes no injection and lands green on its own, wired through the envelope and through the status line's write, which the hook reads
    • landed: a5b89898725f
  6. Context brackets: rules, notes and pages shrink as the session fills
    • packages: internal/core/rules (the one bracket table beside the byte budget, its unknown row included; rules shrink to heading, first rule and the pointer line under a known moderate or depleted fill; an unknown fill keeps rules in full and is said once per session), internal/core/recall (full or one line per bracket, one line under an unknown fill, at least the top-ranked line; the ranking chooses survivors), internal/surface/cli (the router passes the fill; rules --prompt names the bracket and gains --fill), commands/ahoy.md (--fill), .abcd/development/release/surface.json and docs/reference/cli/commands.md (the new flag)
    • tests: TestRecallShrinksFullThenOneLineToTheEndOfTheSession over the three brackets (N8), watched fail first; TestNoBracketSendsNothing; TestDepletedBracketInjectsIndexLinesOnly with three matched pages (P2); TestTopRankedLineAlwaysComesBack; TestRankingChoosesWhichLinesFit; TestRulesShrinkToHeadingAndFirstRule and TestShrunkDomainNamesTheRulesCommand, watched fail first; TestGeneratedDomainsShrinkToTheirFirstGeneratedRule (SHELL's first registry entry in id order, GRILL's first asking rule); TestABracketChangeNeverResendsAnItem; TestUnknownFillKeepsRulesFull and TestUnknownFillShortensNotesAndPages, watched fail first; TestUnknownFillIsSaidOncePerSession; TestBracketTableHasOneSource (no threshold literal outside rules); TestRulesPromptFillSetsTheBracket and TestRulesPromptWithoutFillRendersTheUnknownForm; the rules golden tests and the existing inject and hook tests pass unchanged, since a test that supplies no fill gets the unknown form and that keeps rules in full
    • landed: acb18d1836fc
  7. The docs and the brief
    • packages: docs/explanation (memory beside the library in plain words: what each holds, who adds to it, how it comes back, where it can go; N4), docs/reference/terminology.md, docs/how-to (keeping memory notes, reviewing them with memory list and memory remove until the dashboard, and reading what a prompt brings back with rules --prompt), mkdocs.yml, README.md if it lists the commands, .abcd/development/brief/04-surfaces/README.md (the library-and-memory section's last sentence at about line 430, which still says the notes intent's open questions name where notes are stored and what an agent may write without the person's yes, though both are decided; the table's "Where it can go" row, so promotion reads as the dashboard's; the rules row gains --prompt), .abcd/development/brief/05-internals/07-memory.md (§ 0 and § 9 rewritten to the shipped recall, the brackets, the fill reader and the ranking; in § 9, about lines 185 to 197, the memory-only bracket table, "Per-prompt, not sticky" where recall now deduplicates per session, the recall sub-verb of abcd library that rules --prompt replaces, and the link text that still names itd-39 under drafts/), .abcd/development/brief/05-internals/03-configuration.md (the two memory.json layers beside the two rules.json layers; the fill file under ~/.abcd.noindex/fill/ and the status line's one write), .abcd/development/brief/glossary/core/memory-note.md (status: draft to stable; the commands it names; and its example "is promoted into the project's rules", since a promoted note lives in the project's .abcd/memory.json, not in rules.json), the status line's pages in docs and the brief wherever they say it writes nothing, AGENTS.md's rule-loader paragraph and the marker block ahoy installs, since the loader now injects notes and pages and shrinks rules by the brackets
    • tests: docs-lint and site-render clean; record-lint clean; a docs-currency review of the changed pages; the docs page read against the brief's library-and-memory table so the two say the same thing
    • the verb split's open question 12 (spc-2610100613109045) was answered "Wait for memory notes", and its close in pull request #936 struck A8 there: the notes intent's criterion N4 carries the docs comparison alone, and this step meets it
  8. The live session, the doc-fidelity review and the close
    • packages: this spec (the live captures under a ## Live captures heading after Steps), .abcd/development/intents (both intents move to shipped by the close)
    • tests: a live host session in which an agent writes a note that the tests need the race flag, and a second session in which a prompt about tests brings it back (N1) together with a matching page (N2), with the session id the host substituted shown on the note; the hook's --json envelope for that prompt showing the fill and its source; the rules --prompt output for that prompt attached; the note removed with memory remove at the person's word and gone from memory list; a docs review recorded for HEAD with go run ./cmd/abcd docs fidelity record; then go run ./cmd/abcd spec close spc-2610111627213138 with Delivers: itd-2610091918433290 and Delivers: itd-39 trailers (both intents already declare impact: additive, so no --impact is passed)
    • the live session is the only evidence that recall reaches a real host's context; script output does not stand in for it

Footprint

  • packages: internal/core/memnote, internal/core/recall, internal/core/rules, internal/core/memory, internal/core/recordid, internal/core/statusline, internal/core/history, internal/core/surface, internal/surface/cli, commands/memory.md, commands/ahoy.md, .abcd/rules.json, .abcd/development/release/surface.json, docs/reference, docs/explanation, docs/how-to, mkdocs.yml, README.md, AGENTS.md, .abcd/development/brief/04-surfaces, .abcd/development/brief/05-internals, .abcd/development/brief/glossary
  • files outside the working tree: ~/.abcd.noindex/memory.json (written by memory add and memory remove) and ~/.abcd.noindex/fill/<sha256(session)>.json (written by the status line, created as a directory by the loader); nothing under the host's configuration folder
  • tests: the store's schema, layer, write, session and gate tests, the linked-worktree banlist test among them; the memory verb's add, list, remove, manual-move, everywhere and bare tests; the recall's note, page, ledger, budget, id-order, scope, root and file-open tests; the rules --prompt tests; the ranking's order, tie, determinism, budget, reasons and golden tests; the status line's fill-file tests and its narrowed write-nothing tests, structural and end to end; the fill reader's file, transcript, bound, refusal, model, reset and sweep tests; the bracket tests for rules, generated domains, notes and pages, and the unknown-fill tests; the menu, parity, sentence, appendix, snapshot and dogfooding tests after go generate ./internal/surface/cli; record-lint, docs-lint and site-render clean; the live session captures

Decisions

The rulings this spec carries, all of 2026-10-11 and recorded in the intents' Decisions and adr-2610091918443997:

  • Agents capture notes automatically and every note is stamped (product thinker).
  • Only a person promotes; above two maintainers another must agree (product thinker). Carried by itd-2610111655362931 since the move below.
  • Notes and pages are built together and share one recall (product thinker).
  • The host's memory files move by hand only, and abcd never reads or writes them on its own (product thinker).
  • A memory.json beside each rules.json layer (technical facilitator).
  • Recall shares the loader's brackets, with no thresholds of its own (technical facilitator).
  • A relevance ranking chooses the survivors, with no new dependency unless the person signs off (technical facilitator).
  • Release placement no longer waits on itd-36 (settled on the record).
  • A note never holds a secret, a private name or a live network identifier (settled on the record).

The nine open questions first left here, all answered on 2026-10-11:

  1. The brackets gate rules too, with the same thresholds (technical facilitator, option b). A matched domain comes back as its heading and first rule in the moderate bracket, as notes and pages shrink to one line; one bracket table in the loader serves rules, notes and pages. Step 6 and the Context brackets section.
  2. Context fill: both, the status line first (technical facilitator, option c). The hook uses the status line's used percentage, from its fill file, when present, and otherwise a bounded read of the transcript's tail against a per-model window. Step 5 and "How full the session is". What an unknown fill does is ruling 12 below.
  3. Never send nothing: one line to the end (technical facilitator, after the explanation that a compaction and a new session reset recall). Recall goes full, then one-line titles, and stays at them to the end of the session; there is no bracket in which matches come back as nothing. This changed the notes intent's last criterion and itd-39's press release, each recorded in its Decisions.
  4. to 6. Review and promotion move to a later dashboard (product thinker: "this will be part of a later PT / TF dashboard where the PT can click on those they want to promote", then "Move them to the dashboard"). The notes intent's review and promotion criteria, and the questions on the maintainer count (4) and the agreement (5), moved verbatim to the draft itd-2610111655362931; question 6, where the person runs them, is answered by the move. The earlier draft's promotion step is dropped; no promote operation remains in this bundle, so no guard entry is added (Review and promotion: on the dashboard).
  5. The host fills in the session id (technical facilitator, option a). The command page passes the host's own substitution of its session id; where a host offers none, the note records the session as unknown and says so on every write. Step 1's live trial confirms the substitution.
  6. Personal notes are tied to their repository unless flagged (technical facilitator, option a): recalled only in the repository they were written in, and everywhere with --everywhere.
  7. The recall diagnostic is rules --prompt "<text>" (technical facilitator, option c), showing the rules, notes and pages one prompt would bring back, with the ranking's reasons, on the existing rules verb. Steps 2 to 4 and 6 build it up.

Three rulings of 2026-10-11 that answered the spec review:

  1. The status line may save one fill file (product thinker, asked "May the status line save that one figure?", answer "Allow one fill file"). An exception to the product thinker's ruling of 2026-10-05 that the status line writes nothing (iss-2610050556383525), covering exactly the session's context fill in ~/.abcd.noindex/fill/<sha256(session)>.json and nothing else: no cache, lock, log or ledger. The two read-only tests narrow to that one file and still refuse every other write, and the help text says so. Step 5 and "How full the session is". Recorded in the notes intent's Decisions.
  2. Ship notes before the dashboard's review, with list and remove (product thinker, answer "Ship, list and remove"). Until the dashboard draft itd-2610111655362931 ships, the review is memory list and memory remove, which removes any personal note, run by a person or by an agent at a person's request. Step 1 and "Review and promotion: on the dashboard". Recorded in the notes intent's Decisions and in adr-2610091918443997's Consequences.
  3. An unknown fill leaves rules full, and notes and pages short (technical facilitator, answer "Rules full, notes short"). With no source for the fill, rules inject in the fresh form, with no regression from today, and notes and pages come back as one-line titles. The unknown fill is still reported, in the --json envelope on every prompt and on stderr once a session. This replaces the facilitator-proposed default this spec first carried, that an unknown fill is treated as depleted. Steps 5 and 6.

Design choices this spec makes inside those rulings, each with its grounds and each open to the facilitator's veto at review:

  • A shrunk rule domain ends with a pointer line naming how many rules it holds and abcd rules <DOMAIN>. Grounds: a guardrail domain shrunk to its first rule must still say where the rest is, and the loader already skips a domain that would render as a heading alone.
  • The ledgers key on content, not on rendered form. Grounds: a bracket change would otherwise send again, in a smaller form, an item already in the context in full.
  • The fill file lives under ~/.abcd.noindex/fill/, created by the loader. Grounds: the question behind ruling 10 offered the local tier or the user-level abcd home, and this location is the facilitator's choice, recorded in the notes intent's Decisions as the ruling's place; a session is not bound to one checkout, so the user-level home keeps one file per session wherever its working directory is, and no status refresh writes into a working tree. The loader creates the directory so the status line's one write stays one file.
  • --everywhere notes are judged against the writing checkout's private names only, and say so. Grounds: the reviewer offered judging them against a machine-wide banlist layer as well, if cheap; no such layer exists (the private layer is per checkout, a linked worktree inheriting its primary's), so building one is a banlist change outside this bundle. The gate reads the primary checkout's store in a linked worktree, which is cheap, and add --everywhere and the command page say which names were checked.
  • Survivors are taken in id order until the ranking lands. Grounds: step 2 must choose when only some candidates fit before step 4 builds the ranking; id order is deterministic and is replaced, not kept beside it.
  • Where a model has more than one window size, the smaller. Grounds: a doubt about the window should shrink recall, not flood the context.
  • No promote operation, and so no guard entry, in this bundle. Grounds: a store operation with no caller until the dashboard ships is unwired code.
  • BM25F with recall words weighted three, title two, body one. Grounds: the prior art above; standard library only; deterministic and explainable.
  • The gate refuses rather than redacts a note. Grounds: a lesson with a span cut out of it may say something wrong, and the agent can always rewrite it.
  • The bracket table and the window table live in internal/core/rules. Grounds: the ruling's "share the brackets" read literally, so no payload can grow a second set.
  • Rules and memory share the 64 KiB per-turn budget, rules first. Grounds: the budget is the loader's flood guard, and a second budget would double it.
  • A separate recalled ledger. Grounds: the rules ledger is pruned to active domains every turn.
  • The figures (120-character titles, 2 KiB bodies, twelve recall words, the 8 KiB and 2 KiB bracket budgets, the 256 KiB transcript tail) are starting points, each set in one place.
  • Step 1's own limits, chosen while building it (2026-10-11). A recall word is at most 64 characters, holds a letter or digit, and is unique ignoring case; the body is trimmed and must not be empty; a session id is letters, digits, - and _ ([A-Za-z0-9_-], starting with a letter or digit), at most 128 characters; the no-root-commit refusal points at --everywhere; memory list shows a note tied to another repository as "another repository"; and mem joins recordid.handleInTextRe in step 2, where notes first come back in running text, not in step 1. Grounds: each bounds what a note can carry or how it is named without a ruling of its own, and is open to the facilitator's veto at review like the choices above.

Settled here

Choices an agent made while building steps 3 and 4 where this spec is silent, each the narrowest reading the agent found that meets the criteria, and each an agent's choice for review, open to the facilitator's veto like the design choices above.

  • Which pages the recall reads (2026-10-12, step 3, agent's choice for review). In each scope, the pages index.md lists, the page files at the store's top level, whose names are <type>_<domain>_<slug>.md. A page's title is its index line's summary, derived from the page the way the index derives it, so the recall reads no index.md and opens only the four places it declares; its body is the rest of the page after the line the title comes from, so the title is counted and sent once. Grounds: P4 compares pages by that name, and the name's characters keep a page's key (page:<filename>) safe to print in the hook's stderr line and envelope.
  • A workspace page shadows the machine page of the same name, whether or not it carries a recall list, and when the read refuses it (2026-10-12, step 3, agent's choice for review). Grounds: the narrower scope's page is the page of that name, as a repository layer's field replaces the machine's and a note in the project's file suppresses the machine's copy of its id; it also keeps one page:<filename> key per name in the ledger and the envelope.
  • A machine page is read with the guards the machine memory.json gets (2026-10-12, step 3, agent's choice for review): the directory through fsutil.OpenHomeDeclarationScope, the home scope's walk with the directory judgement fsutil.ReadHomeDeclaration makes (a symlinked directory, or one another account can change, refused), and each page a regular file this account owns that no one else can write (fsutil.ReadHomeDeclarationDenyingIn). A page failing them is named on stderr and brings nothing back, while the scope's other pages still come back. Grounds: a machine page injects text into every session as a machine note does; the spec names only the directory's open, and reading it alone would trust a page another account could write while the notes file beside it is refused for the same thing.
  • A page store or a page that cannot be read is named (2026-10-12, step 3, agent's choice for review): on the hook's stderr with "recalling nothing from it", and in rules --prompt's unreadable list, which gains a kind (note or page); a refused memory.json keeps step 2's words.
  • What a page's recency is (2026-10-12, step 4, agent's choice for review). The tie-break's "page modification" is the latest ingested_at the page's source block records, a date, and a page that records none sorts as the oldest. Grounds: a checkout stamps each file with the time it wrote it out, so a file's modification time differs between clones and the order would not be the same on every machine, as Ranking requires; ingested_at is in the page and travels with it.
  • How the ranking counts terms and lengths (2026-10-12, step 4, agent's choice for review). Each distinct word of the prompt is one query term, however often or in whatever inflection the prompt repeats it (tokens whose candidate roots overlap, "merge" and "merging", are one term), and a word in an item counts as that term when its stem is among the roots the loader's term rule matches the prompt's tokens by, so the ranking finds a term exactly where the matcher hits, and under one term at most. That takes two small exports from internal/core/rules beside TermMatcher (PromptTerms, read from the same prompt index, and TextStems), though step 4's packages line names only the recall and the CLI. An item's length is its weighted length, recall words three times, title twice, body once, BM25F's one pseudo-document of repeated fields; avg_len and N run over every item read after the narrower-scope suppression, so a page without a recall list, never an item, counts in neither. A personal note tied to another repository counts in both (amended 2026-10-17 on merging main, agent's choice for review): step 2's review made the tie a lazy judgement of the matched candidates alone (the recall package's matching, which asks git only when a tied note matched), so the corpus is fixed before git is asked and never depends on whether it was.
  • The ranked order keeps step 2's take (2026-10-12, step 4, agent's choice for review): candidates are taken in ranked order while they fit, and the first that does not fit stops the take, so nothing comes back ahead of an item ranked above it. The one exception is an item whose block alone is larger than the whole per-turn budget, which no turn can carry: it is set aside as too large, never recorded, named on the hook's stderr and given the form too-large in rules --prompt, and it does not stop the take. Grounds: a page can be as large as 4 MiB, and one that ranks first would otherwise hold back every item below it on every turn.
  • Scores tie at a billionth (2026-10-12, step 4, agent's choice for review): two scores within a billionth of each other are a tie, broken by scope, recency and key. Grounds: arithmetic equal on paper can differ in its last bits between machines, and the order must be the same on every machine.
  • The diagnostic ranks notes and pages together (2026-10-12, step 4, agent's choice for review): rules --prompt keeps its notes and pages lists, and each entry carries its rank in the one order they share, its score, and its terms, each with its counts in recall, title and body and its contribution.

Open Questions

None. The nine questions first left here, and the three the spec review raised, were answered on 2026-10-11 and are recorded under Decisions.

Frontmatter

idspc-2610111627213138
typespec
lifecycleopen

References2

mentions17