Repository navigation
feat: keep memory notes with the memory verb and recall them through the prompt hook - #1003
Merged
Merged
Conversation
The user layer's home-scoped read and the repository layer's guarded read move into two exported helpers, ReadUserLayerFile and ReadRepoLayerFile, which the rules loader now reads its own two files through. The refusals keep their wording for rules.json. The memory-notes store reads its memory.json layers through the same helpers, so the two files beside each other are held to one set of checks. Refs: itd-2610091918433290, itd-39 Assisted-by: Claude:claude-opus-5-5
An agent can now keep short notes it would otherwise relearn. `abcd memory add` writes one note, its body on stdin, to the machine layer ~/.abcd.noindex/memory.json and nowhere else. Each note is stamped with who wrote it, the clock's time and the host's session id, or records the session as unknown and says so. A gate refuses a note that would hold a secret, a network address or a private name before anything is written, naming the field and never the span: it runs the memory store's own redactor and the private-names banlist, the primary checkout's too in a linked worktree. A note is tied to its repository's root commit unless --everywhere, which says that only this checkout's private names were checked. Bare `abcd memory` counts the notes and names a layer it cannot read; `list` shows every note from both layers with its body, stamps and reach; `remove` takes any personal note out and refuses one in the project's .abcd/memory.json, which is read with the repository layer's checks and never written. `add` takes no path, so it never opens the host's memory files. The verb sits with the agents, naming its new page commands/memory.md, which is hidden from a person's menu. The memory name is no longer a retired token, since the verb split gave it to this verb. The brief gains the memory chapter and its register row, the release-gate pin names both, and the DOGFOODING domain recalls on the word. Refs: itd-2610091918433290, itd-39 Assisted-by: Claude:claude-opus-5-5
The memory verb resolved its repository root through rules.ResolveRoot, which drops the resolution's notes, so a checkout the resolver refused as another account's was used as a plain directory without a word: `memory add` then said only that there was no root commit. The verb now resolves through rulesRoot, which prints those notes to stderr first, as `abcd rules` does. The test stages the refusal through git's ownership check and a new rules.SwapOwnerUIDForTest, since a second uid cannot be created in a test. Refs: itd-2610091918433290, itd-39 Assisted-by: Claude:claude-opus-5-5
A memory write opened ~/.abcd.noindex through its descriptor and wrote through it, but re-read the file under the lock by walking the path again. A same-account rename of the directory between the two let the write read whatever now stood at the name and replace the old directory's notes with a one-note file. The re-read now goes through the descriptor the walk opened, with a new rules.ReadUserLayerFileIn built on fsutil.ReadHomeDeclarationDenyingIn, so the read keeps the user layer's guards and wording with no third copy of a guarded read. The lock stays on the directory at its path, as fsutil.WithDirLock allows, and the spec says exactly what is locked and what is read. In the rename race the shared read now refuses, so nothing is written. Refs: itd-2610091918433290, itd-39 Refs: iss-2609290300313698 Assisted-by: Claude:claude-opus-5-5
A memory write that could not create ~/.abcd.noindex, or could not write memory.json into it, said only that it could not, so a full disk and a permission refusal read the same. Both refusals now carry the cause, with the home shown as a tilde. Refs: itd-2610091918433290, itd-39 Assisted-by: Claude:claude-opus-5-5
The memory chapter says a machine file the read refuses exits 1, and a file failing a trust check did, but one that failed to parse exited 2 beside the field and gate refusals. It now exits 1 too, as `abcd rules` does for a rules.json layer that fails to load. The agents' page says so. Refs: itd-2610091918433290, itd-39 Assisted-by: Claude:claude-opus-5-5
The v0.14.0 upgrade guide sends `abcd memory` and `/abcd:memory` to `library`, which stays true of v0.14.0. Once a later release gives the name to memory notes, those spellings run instead of refusing, so a reader relying on the refusal would be misled. One paragraph after the table says the name is reused after v0.14.0 and to change the spellings now; the guide's v0.14.0 claims are left as they are. Refs: itd-2610091918433290, itd-39 Assisted-by: Claude:claude-opus-5-5
The memory-recall spec now records, as one decision of 2026-10-11, the limits step 1 set without a ruling: recall words, the trimmed body, the session id's shape, where the no-root-commit refusal points, how list names another repository's note, and that `mem` joins the in-text handle pattern in step 2. Refs: itd-2610091918433290, itd-39 Assisted-by: Claude:claude-opus-5-5
The first full preflight of the memory notes step tripped three gates on the step's own code. A gate test ran git with a hand-built environment instead of gittest.Env; two tests spelled the abcd home's folder name instead of building it through abcdhome; and the field checks and the id check were exported though nothing outside their package calls them, so they are unexported. The gate's use of banlist.PrimaryWorktreeRoot takes that function off the unreached baseline. Refs: itd-2610091918433290, itd-39 Assisted-by: Claude:claude-opus-5-5
The release-gate manifest gained the memory chapter, so the example receipt's manifest hash no longer matched it. The example now carries the committed manifest's hash. Refs: itd-2610091918433290, itd-39 Assisted-by: Claude:claude-opus-5-5
The memory recall needs to match a note's recall words exactly as a rule domain's are matched, and to remember per session what it already sent. The rules package now exports TermMatcher, its one term rule over an indexed prompt, and Hits, which names the words that made a domain match. SessionState gains a recalled map that Inject carries through untouched, since the rules ledger is pruned to active domains every turn. The reset and the fixed backstop clear both maps together. The per-turn budget, the user-layer home and the rule body's render-safety pass are exported for the recall to share rather than copy. Refs: itd-2610091918433290, itd-39 Assisted-by: Claude:claude-opus-5-5
A note one session writes now comes back in a later session when a prompt matches its recall words. The prompt router calls the new recall package after the rules, with the notes both memory.json layers hold: a personal note only in the repository it is tied to, or everywhere, and every note in the project's file. Matched notes come back once per session in a "## MEMORY" block after the rules, within what the rules left of the one per-turn budget, taken in id order until the ranking lands. The --json envelope names them in a new recalled list. The kill switch silences the recall, and a memory.json that cannot be read is named on stderr while the rules still inject. `abcd rules --prompt "<text>"` shows what one prompt would bring back to a fresh session: each matched domain with the words that hit, and each matched note with its layer, in full or over the budget. It writes nothing, and refuses a positional domain beside it. commands/ahoy.md documents it, and the CLI reference and surface pin are regenerated. Refs: itd-2610091918433290, itd-39 Assisted-by: Claude:claude-opus-5-5
Notes now come back into running text through the recall, so a mem id written in a sentence is a pointer into the record like any other family's. Step 1 left this to the step that first recalls notes. Refs: itd-2610091918433290, itd-39 Assisted-by: Claude:claude-opus-5-5
The exported-reach audit refused recall.NoteItems, which only the package itself calls: ReadNotes is the front doors' way in. It is now unexported. The audit also found rules.Label reached from outside its package by the rules --prompt diagnostic, so its baseline line goes. Refs: itd-2610091918433290, itd-39 Assisted-by: Claude:claude-opus-5-5
…tied note The recall asked git for the repository's root commit on every prompt whenever any machine note carried a root, before matching, while the live drain had already asked the same question for that prompt and thrown the answer away. A note's tie is now judged after matching, against a lookup the recall asks at most once and only when a tied note matched; the prompt router hands it the drain's own answer, so a prompt spawns that full-history rev-list once. Refs: itd-2610091918433290, itd-39 Assisted-by: Claude:claude-opus-5-5
The recall's root lookup dropped git's timeout, so a slow git left out every note tied to the repository that turn and said nothing. The lookup's error now reaches the recall's result, and the prompt router names it on stderr in the shape it already uses for an unreadable layer. The rules still inject, and the tied notes, unrecorded, are tried again on the next turn. Refs: itd-2610091918433290, itd-39 Assisted-by: Claude:claude-opus-5-5
The memory verb kept its own copy of the recall's layer reader, its refusal type and its reason text. The verb now reads through the recall's ReadLayers, whose Refusal and Reason it shares, and the copy is gone. The verb's output is unchanged, and its tests pass as they were. Refs: itd-2610091918433290, itd-39 Assisted-by: Claude:claude-opus-5-5
No hook test had rules and a note competing for the per-turn budget. This one sizes the rules to leave a little of it: the rules block and a memory block come back in one output, the note that does not fit is named on stderr and left unrecorded, and the turn stays within the budget. Handing the recall the whole budget in place of what the rules left makes it fail. Refs: itd-2610091918433290, itd-39 Assisted-by: Claude:claude-opus-5-5
The rules --prompt diagnostic matched domains against the whole prompt, while the hook scans only its leading bytes, so a recall word past the cap showed a domain the hook would never inject. One helper now caps every reader of a prompt, the diagnostic included. Refs: itd-2610091918433290, itd-39 Assisted-by: Claude:claude-opus-5-5
Once the rules --prompt diagnostic judged ties through Matching, the plain match had no caller outside the recall, which the exported-reach audit refuses. It is now the package's own, and Matching is the one the front doors use. Refs: itd-2610091918433290, itd-39 Assisted-by: Claude:claude-opus-5-5
Assisted-by: Claude:claude-opus-5-5
A memory note went into the prompt hook's output under a bare "## MEMORY" with no provenance and no fence. A note from a checkout's committed .abcd/memory.json, which a cloned or compromised repository controls, looked exactly like the person's own, and its body could make a heading a CommonMark reader sees: the continuation-line indent only defends the flush-left contract, and a two-space "## COMMITTING" still reads as one. Each recalled note's head now names its layer, "(project file, committed in this repository)" or "(personal note, agent-written)", as an override's rule heading says "(repo override)" (GHSA-22f8-qf5r-gjgq). Every body is fenced through the rules package's one fence, now exported as rules.FenceRuleBody rather than copied (iss-2609020219265817). The hook's stderr line names each id with its layer, and the layer joins the item's ledger signature, so a promoted note is sent again under its new label. The forge test now asserts, under every fence reading mdrecord knows, that the only live lines are the block heading and each item's head: indented headings, setext underlines, list items and fence-closing runs of tildes or backticks all stay inside the fence. Refs: spc-2610111627213138 Refs: iss-2609020219265817 Assisted-by: Claude:claude-opus-5-5
When the live drain made no root-commit lookup (rules --prompt, or a hook with no cwd), promptRoot asked git with context.Background(), so a git that never answered held the prompt for as long as it hung. The lookup it makes itself now runs under a two-second deadline. A lookup past it is a timeout wrapping gitutil.ErrGitTimedOut: the tied notes are left out this turn, unrecorded, and the recall's diagnostic names it on stderr. Refs: spc-2610111627213138 Assisted-by: Claude:claude-opus-5-5
recallMemory runs after the rules are rendered and had no recover, so a panic inside the recall dropped the rendered rules for that prompt with it. It now recovers inside itself: that prompt recalls nothing, the session's ledger is kept as it was, stderr names the failure, and the rules still inject. Refs: spc-2610111627213138 Assisted-by: Claude:claude-opus-5-5
drainWhileLive asked git for the root commit with context.Background() on the hook's main path, every prompt with a cwd, so a git that never answered held the prompt for as long as it hung. The drain and the recall now share one lookup, boundedRootCommit, under the same two-second deadline. On a timeout the drain drains nothing this turn and says so on stderr, and the recall, which reuses the answer, leaves the tied notes out and names it as before. Refs: spc-2610111627213138 Assisted-by: Claude:claude-opus-5-5
rules --prompt dropped the recall's RootErr and the error recall.Matching returned, so when git did not answer the root-commit lookup in time, the tied notes were missing from the diagnostic with nothing to say why. It now names the timeout on stderr, once, in the hook's own words (one helper, rootErrLine, serves both), and stdout stays one document. Refs: spc-2610111627213138 Assisted-by: Claude:claude-opus-5-5
Three findings from the round that labelled and fenced recalled notes, each left as is and filed so it is decided on its own: - the hook's --json recalled list carries bare ids with no layer, since adding it changes the envelope's schema (nitpick); - the "(personal note, agent-written)" label also covers notes moved in from the host's memory with --moved-from-host (nitpick); - a recalled note's title sits on the head line outside the fence, so inline markdown in a project note's title still renders (minor). Refs: spc-2610111627213138 Refs: iss-2610142220581750 Refs: iss-2610142221073140 Refs: iss-2610142221086043 Assisted-by: Claude:claude-opus-5-5
…gnature The layer is now part of a recalled item's ledger signature, so a session that recalled notes before the change gets each once more, labelled, on its next matching prompt. The spec's Provenance paragraph now says that this one-time re-send is expected, as it was for the rules' override label. Refs: spc-2610111627213138 Assisted-by: Claude:claude-opus-5-5
The prompt router's root-commit lookup is a full-history rev-list under a 2 s cap. On a very large repository without a commit-graph it could pass the cap on every prompt, so the live drain and the tied-note recall would skip every prompt with a stderr line each time. It measured 0.01 s here (7,873 commits). Filed with the remedy of caching the root commit per checkout, or reading it from the commit-graph. Refs: iss-2610142227520380 Assisted-by: Claude:claude-opus-5-5
Assisted-by: Claude:claude-opus-5-5
Only render.go calls it, and the exported-reach audit refuses an exported function no production code outside its package reaches. Refs: spc-2610111627213138 Assisted-by: Claude:claude-opus-5-5
The example receipt keeps main's closed-disposition example and takes the hash of the merged release-gate manifest. Assisted-by: Claude:claude-opus-5-5 # Conflicts: # .abcd/development/release-gate/receipt.example.json
github-merge-queue
Bot
removed this pull request from the merge queue due to a conflict with the base branch
Oct 15, 2026
The example receipt takes the hash of the merged release-gate manifest. Assisted-by: Claude:claude-opus-5-5
REPPL
enabled auto-merge
October 15, 2026 10:49
Main gained the embargo verb (#1015). Its brief chapter and memory's both take a row and an index entry in the surfaces register, kept side by side; the release-gate manifest takes main's copy plus memory's chapter and verb, with checkerCount (44) and promptHash recomputed, and the example receipt's manifestHash is the merged manifest's sha256. Three chapters now share number 37, as iss-2610151050383937 records. Refs: itd-2610091918433290 Refs: itd-39 Refs: iss-2610151050383937 Assisted-by: Claude:claude-opus-5-5
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This builds steps 1 and 2 of spc-2610111627213138 for itd-2610091918433290. Steps 3 and 4, memory pages in the recall ranked by BM25F, are #982, which rebases onto this branch once it lands. The spec stays open and the intent stays planned, so this PR carries no Delivers trailer.
abcd memorykeeps memory notes in a guarded store: the machine file under the person's home, and a project file.abcd/memory.json. Reads refuse symlinks, FIFOs and files another account can write. Writes go through a gate that fails closed when the secret scanner can't run.abcd rules --prompt, inside the same per-turn budget as the rules. A note's id counts as a record handle in running text.rules.FenceRuleBody), so a cloned repository's notes can't pass off text as abcd's own or as the person's.Review
--jsonrecalled list carries no layer;Refs: spc-2610111627213138
Refs: itd-2610091918433290
Refs: iss-2609020219265817
Refs: iss-2609290300313698
Refs: iss-2610142220581750
Refs: iss-2610142221073140
Refs: iss-2610142221086043
Refs: iss-2610142227520380
Assisted-by: Claude:claude-opus-5-5