agentm experience and dreaming - alexherrero/agentm GitHub Wiki
Note
LAUNCHED (approved 2026-06-21, lifted 2026-06-24, locked 2026-06-28). Child design of agentm HLD β the Experience pillar, tending the store the Memory System design defines. Crystallization, dreaming, and inbox triage have shipped; the forward pass is now the weekly field brief, which replaced the first forward learning on 2026-10-03. See the amendment log below for build history.
Experience is how agentm gets better over time β it turns the agent's own past sessions, and the wider world, into sharper judgment, so the next session starts further ahead than the last. It is what makes the memory grow rather than just persist.
Experience runs in two directions:
- Backward β learning from its own past. Every finished session leaves something behind that sharpens the next. Built today.
- Forward β learning from the world. Once a week the agent searches the sources you favour and writes a short brief of what is new, ranked by what it means for the work in flight. You decide what to keep. Built (task 185), replacing the retired first attempt.
-
Reflection (built). After a session you took part in ends, a pass mines its transcript for durable entries, each classified by type (preference Β· workflow Β· fix Β· β¦; see the Memory System for the taxonomy). A session with no operator turn is not mined, the same reading the trace writer uses. Only a HIGH candidate, a durability cue you typed, becomes a card,
unfiledfor the nightly pass to judge; everything below HIGH is a line in the session's trace, and nothing is filed for it. A question is not a directive, and a line the session already carried is not a sighting: its first request,~/.claude/CLAUDE.md, the repo's AGENTS.md, the always-load tier. Each is dropped and logged in the trace. An idea is recorded in the trace's## Ideas, word for word with the conversation around it, and filed nowhere at session end. The nightly idea judge (agentmd judge-ideas) decides whether each was steering the session, a repeat of an idea card, or a new idea for the inbox (amended 2026-10-16; AgentM Vault Β§ Capture has the detail). A Stop-event hook runs it at session end; an idle hook catches sessions that crashed before it could fire. -
Inbox triage (built 2026-07-11; auto-apply retired the confirm-gate the same day, second pass; retired 2026-09-06). Reflection's MEDIUM/LOW-confidence lane routes to
personal/_inbox/and stops there β nothing moved an entry out ofstatus: inboxagain until this landed; the backlog had grown to 1,565 notes (55% of the whole vault) with only 2 ever transitioning./memory inbox --bulk-review(harness/skills/memory/scripts/inbox_triage.py) scans the still-untriaged pool and proposes one of three dispositions per entry: promote (reinforced by repeated occurrence, or a real content match across 3+ entries, graduates to the same canonical destination reflection's own HIGH-confidence path already writes to), merge (a near-duplicate pair, reusing dreaming's owndifflibsimilarity stage against the inbox pool rather than a second implementation), or expire (stale and unreinforced past a TTL, archived in place β never deleted). Every proposal stages through the same_dream-staging/βdream_confirm.pyconfirm/revert-log path dreaming already built, unmodified. It shipped confirm-gated for its first-ever run: a first-run cutover marker (_meta/inbox-triage-cutover.json) separated the pre-existing backlog from anything captured afterward, and every disposition against the backlog stayed confirm-gated no matter what. The operator personally reviewed and confirmed that entire first-run backlog β 635 proposals across 1,565 entries, zero errors β and on the strength of that supervised pass directed that confirm-gating retire going forward: every disposition now auto-applies by default, promote/merge/expire alike, regardless of the source entry's era. The cutover marker still exists and still labels an expire proposal's era in the digest, but purely as information now, not a gate. As of 2026-07-18 (auto-organization part 3 task 4), the CLI is no longer the only door into this engine:dream.run_dream_and_auto_apply()β the wrapperdream.py's CLI and the weeklydream.yamljob actually invoke β now also runs it automatically, as a sibling sub-run inside every weekly dreaming cycle./memory inbox --bulk-reviewis unchanged and stays the on-demand door into the identical engine for anyone who wants to run it outside that cadence. See the amendment log for the full rationale of all three landings. - Heat-based curation (built). Which memories load every session changes with use: a heat policy promotes an on-demand entry into the always-load set once it's been hit enough times across enough distinct sessions, and demotes cold ones β down to a safety floor, leaving pinned entries alone. Built and tested; it keeps the always-load set earning its token cost.
-
Crystallization (built, on request). Distils an exploration into a structured digest β question Β· investigation Β· findings Β· lessons Β· open threads β instead of leaving raw transcript fragments behind.
harness/skills/memory/scripts/crystallize.pyships the five-field digest schema:crystallize_exploration()writes akind: crystallizedentry through the existingsave_entry()(no new store),parse_digest()reads one back and raises on a missing section, and itswrite/readCLI drives both by hand. Nothing composes a digest on its own. The vault design places crystallization in a weekly dreaming phase that reads closed work for recurrence, and that phase is built (v10.0.0):Runindaemon/internal/crystallize/crystallize.go:779, scheduled bytemplates/jobs/crystallize-weekly.yamland set out in Memory-Daemon. The phase-close trigger that staged a marker per session is retired; its section below is the record.
Backward learning can only sharpen what the agent has already seen; the forward pass brings in what it hasn't. The first version of it, forward learning, ran daily over operator-approved sources and wrote what it found to a watchlist. It collected 113 entries that nobody read, and on 2026-10-02 the operator scrapped it. Its goal stands: know, repeatably, what is new in agent harnesses, memory, automation and skills, and rank it by what it means for the work in flight. The weekly field brief carries that goal.
-
One note a week.
resources/briefs/<YYYY-MM-DD>-field-brief.md,kind: brief, holds at most ten items. Each has the link, two sentences on what it is, one on why it matters to the work in flight, and a line naming the source that surfaced it. The items are ranked againstprojects/agentm/docs/roadmap.mdΒ§ What remains and the frontmatter summaries of the open designs. The same text goes out by email through the autonomy mailer (scripts/health/session_email.py, Autonomy Β§ Daily email), and the engine answers an ad hoc question on demand with--ask, which writes no note. -
Sources and tuning. The social layer comes through the operator's installed
last30daysskill. Docs and papers come through web search and a short feed list: the Anthropic engineering blog and the Claude Code changelog, arXiv queries, GitHub release feeds, Simon Willison, Latent Space. One file the operator owns,projects/agentm/desk/field-brief.md, holds topics, favoured sources and ignored sources in plain markdown. A seen-list in the engine state directory (field-brief/seen.jsonl, never the vault) keeps an item from showing twice within 90 days, keyed on its canonical URL. -
Keep or let it expire. A
keepverb turns one item into a reference card through the capture door, with the operator's why. The rest expire with the note. -
Where it runs. On the runner as
templates/jobs/field-brief-weekly.yaml: weekly, in an18:00-23:00window with a two-day lookback and a declared token budget, routed to Sonnet; a--deepflag routes to the strong tier for an occasional monthly pass. The runner has no weekday key, so the first run sets the day: the first brief was run by hand on Saturday 2026-10-03, and the runner's first run is the Saturday evening a week later. -
How a run is built.
harness/skills/memory/scripts/field_brief.pydrives thelast30daysengine itself for the social layer (the skill cannot run headless), then runs oneclaude -pfrom a scratch directory with hooks off and onlyWebSearchandWebFetch, audits the stream, takes fixed JSON back and renders the note. The email issession_email.send(), the daily email's own config and sender; a note is mailed once, a failed send is retried without another model run, and an absent mail path is a logged skip.
What retired. forward_learning.py, the review CLI watchlist_review.py (which served both watchlists), the daily job template, and the reference-library watchlist: its 113 entries and the sources file moved to projects/agentm/completed/forward-learning-2026-10/, and nothing was deleted. What stays, for now. The skill-specific pool the first pipeline generalized β adapt_skills.py, the adapt-evaluator agent and Projects/agentm/_skill-watchlist/ β is not inert (its discovery scan last ran on 2026-09-30, and the idle hook and the orchestration briefing read it), so its retirement is its own backlog item. With the review CLI gone, its entries are reviewed by hand. Re-audit trigger: the first month of briefs. If the operator keeps almost none of the items, the ranking or the sources are wrong; if the Mac sleeps through the weekly window two weeks running, move the cadence or revisit a cloud-scheduled routine.
Note
Reconciled 2026-09-11 to agentm-vault plan 04. The rulings behind this section live in AgentM Vault Β§ Dreaming, and the amendment log below records each earlier stage and when it retired.
Memory's "sleep" is the night. Four steps start inside the 02:00β06:00 local window and run in order: the enrichment batch, the dreaming binary, the Python cycle, then the scorecards. The binary (agentmdream; see Memory daemon Β§ the dreaming binary) carries the maintenance lanes that change notes: the lifecycle axis, the copy collapse, refiling, promote, the calendar reviews, the maps and the date glosses. The Python cycle, harness/skills/memory/scripts/dream.py, carries the work that has no Go owner and whose output someone still reads. It reads, reports and proposes, and it changes no note.
run_dream(vault_path, *, run_id=None) -> DreamDigest runs these stages in order:
- The corpus meters count entries, organic connectivity, and the live, shelved and archived browse surface.
-
The filing contract is checked fail-closed. A
storage-rulesblock that will not parse halts every stage after it, and the digest says "Filing is halted" and why. -
Lint runs as a report: the same
lint.run_lint()that/memory lintcalls, with its findings counted and nothing repaired. -
Dedup pairs notes at the shipped 0.92
difflibsimilarity. Each pair is apossible-twinfinding with its similarity and both titles. -
Contradiction triage groups notes that share a
slug:and differ in body. Each group is asame-keyfinding. -
Facet promotion turns each diary label seen on three or more distinct days in thirty into a
proposed-facetfinding. -
The breaker and the correction loop run last, through
dream_stages.run_new_stages(), which reports the enrichment breaker's status and runs the correction loop.
The findings land in <engine state dir>/dreaming/review-proposals.json, and the needs-review map regenerates from that file with three sections of its own: Possible twins, Shared keys, different bodies, and Proposed facets. You act on them in Obsidian by merging, by superseding, or by registering a facet in the contract. Nothing applies a finding, and git is the undo.
The cycle also writes its report to <engine state dir>/dreaming/python-cycle.json, beside the binary's last-report.json, for the morning note. Each run leaves a digest at <engine state dir>/dream-runs/<run_id>/digest.md. The cycle makes no model call.
templates/jobs/dream.yaml registers the cycle on the Runner: schedule: daily, lookback: 3d, window 02:00-06:00, order 3, tier: T3, and no budget:. The template ships dry_run: true. To turn the cycle on in a repo, copy the template to .harness/jobs/dream.yaml and set that field to false; a repo that already has a live copy edits it in place. The cycle writes no note either way, so the flag decides only whether the map and the morning note get the night's findings. scripts/verify-dreaming.sh guards the wiring: a twin found, no note changed, the pair on the needs-review map, and the manifest inside the night's window.
Every earlier lane that applied or staged anything is retired, and the amendment log names each one.
What's learned routes into the general memory the same way anything else does β as kind-classified entries: a learned preference, workflow, fix, or domain-reference becomes a typed entry, classified by reflection on the way in (see the Memory System). Two other destinations sit alongside it:
- The idea incubator / skill watchlist β an ingested idea, article, or candidate skill lands here for later review, not adopted silently.
- The Opinion supplement β no capture path routes a lesson here today. The accumulate loop that sharpened an opinion's learned layer is retired; see the section below and the condensed twin in the Opinions design.
Everything routes through the existing memory engine β no new store; heat curation runs continuously over the always-load set.
Note
Retired; nothing runs it. Stage 1's routing left reflection in agentm-vault plan 03, and Stages 2-3 β the dreaming stage, opinion_supplement.py, its console section and verify-opinion-supplements.sh β retired in plan 04, after session 1 of the vault series retired the lanes they filled. The design below is the record of what was built.
Spec landed 2026-07-07 (PLAN-wave-e-experience task 3); Stage 1 built 2026-07-24 (v9.1.0); Stages 2-3 designed 2026-07-25 and built the same day (PR #380). This is the one pillar-to-pillar flow that had no contract: how a signal from Experience's capture paths becomes a lasting sharpening of an Opinion's learned supplement. What landed in 2026-07 was a design amendment rather than an implementable design β it named guards without naming the fields, stores, thresholds, and channels they run on. Stage 1 shipped the half that was buildable as written; the operator deferred the rest for a design pass rather than let a build guess at them, because this is self-modifying behavior and a wrong design accumulates wrong standards and then enforces them. The section below is the whole contract with those holes closed.
The one-sentence design: the accumulate loop is the style-learning loop generalized from voice to standards β the same edit-driven, operator-gated capture the wiki system already proved, with a recurrence gate in front and the coded base as an unoverridable floor behind.
1 β Route, don't invent (built, v9.1.0). No new pipeline. The loop is a routing rule inside the capture paths that already exist: when reflection (session-close), edit-driven capture, or the watchlist produces a candidate that is standard-shaped β a rule about how work should be judged or done, not a fact or preference β it targets an opinion supplement instead of general memory. harness/skills/memory/scripts/opinion_routing.py is that rule: a deterministic classifier that requires a candidate to read as a rule and be about doing work, checks voice lessons out first (they belong to the style overlay β one lesson, one home), and returns a target opinion or None. A supplement branch in reflect.route_candidates writes the hit to personal/_opinions/<name>/<slug>.md; nothing is ever written into a coded opinions/*.md. The tri-lane routing (MEMORY_REVIEW_MODE) is untouched and orthogonal β the classifier decides the destination, the confidence ladder decides the lane, and a LOW-confidence standard still routes to its opinion.
2 β Classification is text-shape. The signal β opinion map is retired. The original spec routed by where a lesson came from β a /review correction to good, a token-audit finding to efficient, a PII finding to private. Those sources emit nothing machine-readable, and three of the four named ones (/review, token-audit, the PII scrubber) live in crickets rather than agentm, so keying on them means a cross-repo instrumentation project standing between the loop and any of its guards. The map is dropped as a routing mechanism. The table below survives as a reader's aid β which opinion absorbs which kind of lesson β and nothing consults it:
| Kind of lesson | Lands in |
|---|---|
| quality bars, adversarial findings, defect patterns | good |
| gate and battery lessons, done-criteria misses | done |
| cost, model-tier, and routing lessons | efficient |
| process, architecture, convention, and incident lessons | how-we-engineer |
| reversibility lessons and safety-gate near-misses | recoverable |
| secret, credential, and PII lessons | private |
| voice and prose lessons | not an opinion β the style overlay owns them, and double-capture is forbidden |
Stage 1's existing optional source: frontmatter field records an origin tag when a caller happens to know one (the design pass named this as a new signal_source: field; the build kept source:, which already carried exactly that meaning β one field, one fact). It is provenance and a health-check input, never a routing key, so a source that starts emitting structured records later fills that field and nothing else in the loop changes.
3 β The anti-corruption contract (the heart of it). Three stacked guards, each now with the mechanism it runs on:
-
Recurrence gate. Promotion into a served supplement requires the same lesson surfacing from two distinct sessions β the heat-policy discipline applied to standards, since one incident is an anecdote. The lane is its own store: each entry carries a
sessions:list, entries in one opinion's lane that pass a similarity match are one lesson, and the group promotes only when the union of its session ids reaches two. A single occurrence parks in the lane, which is exactly where reflect already wrote it. -
Extend, never override. The fold serves base-then-supplement with the coded base authoritative. A supplement entry that contradicts its base never promotes: it parks, and surfaces as a proposed base change through the cycle digest, a stable
_meta/opinion-base-proposals.json, and the console. Applying one means a human editingopinions/<name>.mdand committing it β overriding a shipped standard is a check-in, not a memory write. Detection is narrow by design (see call 4); the guarantee is structural, so an undetected contradiction's worst case is additive noise served beneath an authoritative base, which is the corruption ceiling the spec names and which dreaming's own dedup and compression sweep. -
Provenance or it didn't happen. Every supplement entry carries the session ids it was mined from, optional commit/PR/incident refs, and the existing
supersedes:/superseded_by:chain. A supplement-health check reports provenance coverage and lane depth per opinion, so an unhealthy supplement is visible rather than discovered.
4 β Cadence. Capture is continuous and rides session-close reflection, as built. Promotion runs on dreaming's weekly cycle as one new stage β the named maintenance owner, no second mechanism. Base check-ins happen at release time by hand.
Where Stages 2-3 run. One new dreaming stage, _stage_opinion_supplement(), joining run_dream()'s existing hand-wired sequence the same way every stage since tidying has. It runs the recurrence gate, then the contradiction check, then composition, all deterministically β no model call, which is what makes it buildable at all: the orchestration chains fire outside the agent loop and cannot dispatch a sub-agent (orchestration_idle.py:24), the same blocker that stopped crystallization's distillation. It inherits staging, dream_confirm, the revert log, and the anomaly breaker unchanged.
1. The signal β opinion map is dropped; text-shape classification is the sole classifier. Why not instrument the sources: three of the four are cross-repo, and the source is a weaker prior than the text anyway β a /review correction can be about cost or privacy as easily as quality. Why not a hybrid keyed on the signals that already emit structured entries (kind: failure-incident, session-cost): more machinery for a prior that fires on a minority of candidates and still needs the text classifier underneath. Re-audit trigger: revisit only if the supplement-health check shows the operator re-filing promoted entries into a different opinion often enough to be a pattern β and then as a tiebreaker on source:, never as a primary key.
2. Supplement entry schema. A lane entry keeps Stage 1's frontmatter and gains: sessions: (a list of <project-slug>/<session-uuid> ids, from reflect._session_id_from_path β the build threads the session id through route_candidates into the supplement writer, which Stage 1 did not); source: (optional origin tag β Stage 1's existing field, kept as-is rather than shipping a redundant signal_source: beside it; reconciled as-built 2026-07-25); refs: (a flat list of free-form commit / PR / incident refs); promoted: (the timestamp it entered the served file); and the existing supersedes: / superseded_by: V6-1 convention reused as-is. status: takes proposed β promoted β parked β superseded. Why a flat refs: list rather than typed commit/incident fields: typed fields need each source to emit them, which call 1 just established doesn't happen; a flat list is what can actually be filled today, and an entry with none is visible in the health check rather than rejected. Re-audit trigger: type the refs when a source starts emitting them.
3. kind: opinion-supplement is registered in kind_registry.KNOWN_KINDS. Stage 1 already writes this kind and it is not registered β the vault is at zero unrecognized kinds only because no standard has been mined yet, so the first one to land breaks that invariant. One kind covers both lane entries and the composed served file; status: distinguishes them. Why not two kinds: they are the same content at two stages of one lifecycle, and a second kind buys nothing a status: value doesn't. Re-audit trigger: none β this is a registration the build owes, and check-kind-taxonomy (advisory) reports it either way.
4. Recurrence identity is same-opinion similarity at 0.85, over the lane itself. Two entries are the same lesson when they target the same opinion and their normalized bodies match at difflib.SequenceMatcher ratio β₯ 0.85; the group merges into the earliest entry, accruing the union of sessions: and refs:, with the rest marked superseded β dedup's own merge-into-earlier mutation shape, reused rather than reimplemented, exactly as inbox triage reuses _stage_dedup. Why 0.85 and not dedup's 0.92: 0.92 is tuned for near-verbatim duplicates, and the same lesson mined from two different sessions is paraphrased, not duplicated; 0.85 is the linker's own confident band, an existing number rather than a new one. Why the lane and not a new cross-session index: an index is a second source of truth that can drift from the lane, and the lane is small by construction β the ~20-entry trigger below is the point at which it stops being small. Why distinct sessions rather than entry count: two entries from one session are one occurrence, which is the spec's own anecdote test; a Stage-1-era entry with no sessions: value counts as zero and can join a group without satisfying the gate. Re-audit trigger: 0.85 is a calibration-era number, not measured against opinion prose β revisit once real lane volume exists, tightening it if unrelated standards merge and loosening it if the gate never fires while the lane grows.
5. Contradiction detection is narrow and high-precision; the guarantee is structural. The check is direct normative negation on a shared anchor: the entry and a base sentence share a work-domain anchor phrase and carry opposite normative polarity (always/must/required against never/don't/forbidden). It catches the flat reversal and misses subtler conflicts on purpose. What carries the rest is composition: nothing writes into opinions/*.md, opinion_resolve returns base and supplement as separate fields, the composed file leads with a header naming the base authoritative, and a suspected contradiction never promotes at all. Why not an LLM contradiction judge: it cannot be dispatched from the chain, and judging whether a learned standard contradicts a shipped one is the exact place the spec says deterministic-first matters most. Why not serve-with-a-warning instead of parking: a warning is text the agent may weight against the base it is meant to defer to; parking removes the question. Re-audit trigger: the spec's own β revisit the extend-never-override ceiling only if a supplement-vs-base contradiction recurs three times or more, which is the base being wrong rather than the loop.
6. The inbox lane is per-opinion β memory/crystallized/<name>/ since filing-v2 part 3 (built as personal/_opinions/<name>/; opinion_supplement.lane_base() is the one place the root is spelled) β and the lanes stay out of dream's general walk (_opinions/ in dream._EXCLUDE_DIRS, and the crystallized lanes plus served files by kind). The shared personal/_inbox/ keeps every non-standard candidate and is untouched. Why not the shared inbox: the ~20-untriaged trigger is per-opinion by construction and a shared inbox cannot express it, and inbox_triage.py's promote disposition would graduate a supplement to <vault>/personal/<kind>/<slug>.md β the wrong destination entirely. Why exclude the directory: _opinions/ is in the general corpus today, so link_improvement would write **Related:** wikilinks into a served supplement β changing text the agent reads as its own standards β tidying would shelve a year-old standard, and general dedup would merge across opinions at the wrong threshold. The new stage owns this directory and no other stage touches it, mirroring _inbox's own exclusion. The connectivity and browse-surface meters stop counting these entries, which is correct: they are served configuration, not notes to link. Re-audit trigger: revisit if a second consumer ever needs to read the lane, which would mean the exclusion is hiding it from something legitimate.
7. Composition regenerates the served file wholly from the lane, every cycle. memory/crystallized/<name>.md (personal/_opinions/<name>.md before part 3) is a pure function of that opinion's promoted set: a fixed header naming the base authoritative, then each promoted entry as a titled section with a one-line provenance footer, ordered oldest-first. An empty set means no served file at all, so opinion_resolve reports base-only honestly. The write goes through revert_log.record_and_apply like every other dreaming mutation. No resolver change: opinion_resolver._read_supplement already reads exactly <supplement_dir>/<name>.md, so the served file sits beside its lane directory and the caller keeps resolving <vault>/personal/_opinions β the resolver stays stdlib-only and pure, as Stage 1's own task-3 finding established. Why regenerate rather than append: it is idempotent and self-healing, so a superseded, parked, or operator-rejected entry disappears on the next cycle without a second retraction mechanism, and the file is always exactly what the lane says it should be, which removes a whole drift class. Re-audit trigger: revisit if a served supplement grows large enough that regenerating it every cycle is wasteful, which would mean the recurrence gate is too loose.
8. No LLM assist is built; the seam is tracked, not silent. The spec's "the LLM assists only at MEDIUM confidence" conflated two different things: reflect's routing lane (HIGH/MEDIUM/LOW, which decides where a candidate is stored) and a classifier confidence the classifier does not have β classify_standard_shaped returns a target or None, binary. The case the spec meant is a candidate that reads as a rule about work but matches no opinion's vocabulary. It routes normally, and the unbuilt assist is named as opinion_routing.assist_tier_available(), returning False unconditionally β the same seam shape as dream.cheap_model_tier_available() and dream_confirm.higher_tier_model_available(). Why not build it: the chain cannot dispatch a sub-agent, and inventing a confidence scale for a regex classifier to hang a dispatch off would be fiction. Re-audit trigger: flip all three seams together when a chain-time model primitive exists.
9. Promotion is confirm-gated through one supervised window, then auto-applies on a fresh operator ruling (operator ruling, 2026-07-25). _meta/opinion-promotion-cutover.json stamps the stage's first run against a vault and is never overwritten β ensure_cutover_marker()'s shape, reused. Through the window every promotion stages as a stage="opinion_promote" proposal requiring dream_confirm.confirm(), and opinion_promote is deliberately not in AUTO_APPLY_STAGES. Auto-apply turns on only on an explicit later ruling recorded here as an amendment β the standing that frozenset's own docstring already requires, and the path inbox triage walked. When it does, opinion_promote joins _ANOMALY_WATCHED_STAGES in the same change: a stage that rewrites the agent's own standards is the last place to accept link_improvement's cap-only-no-breaker gap. Why not auto-apply from the first cycle: every promotion is revertible, but the first pass over real content would self-modify before you had read a single proposal. Why not gate it permanently: a standards loop that needs a human every cycle stops running. Re-audit trigger: the ruling to widen auto-apply, once a supervised window has produced real proposals to judge it on.
10. Supplement health lands under the memory freshness+experience axis. The spec's "the dashboard's opinions family" names a family that does not exist β health_score.FAMILY_WEIGHTS locks exactly eight axis names, the same finding shape as the retired "FRIDAY family" reading. A verify-opinion-supplements.sh emits the usual jsonl_emit.sh health signal against that axis (the one dreaming and the auto-org meters already report to), and console.section_opinion_supplements renders it: per opinion, lane depth, promoted count, provenance coverage, parked and base-proposal counts. Why not add a ninth family: the eight are locked and a new one reweights every historical score. Re-audit trigger: the spec's own β if any opinion's lane exceeds roughly 20 untriaged entries, the recurrence threshold is too loose for whatever is feeding it.
Why not the alternatives (carried forward): a dedicated learning pipeline duplicates three shipped capture paths for zero new signal; LLM-judged auto-promotion violates deterministic-first exactly where corruption matters most; supplement-overrides-base inverts operator-voice-wins and would let two bad sessions rewrite a shipped standard.
Carried-forward re-audit trigger: when dreaming's consolidation starts writing live instead of only previewing, confirm it actually sweeps supplements β else schedule a lint.
As-built (2026-07-25, PR #380). All ten locked calls are implemented against this section verbatim: opinion_supplement.py (a stdlib-only leaf module β same-opinion clustering at RECURRENCE_SIMILARITY_THRESHOLD = 0.85, promotion gated on two distinct sessions: ids, the narrow direct-negation contradiction check against the coded base, whole-lane composition), dream._stage_opinion_supplement() staging stage="opinion_promote" proposals through the existing propose β dream_confirm.confirm() β revert-log path, kind: opinion-supplement registered in KNOWN_KINDS, _opinions/ in dream._EXCLUDE_DIRS, and the health surface (_meta/opinion-supplement-health-latest.json + console.section_opinion_supplements + scripts/verify-opinion-supplements.sh, wired into check-all.sh and CI). Two things stay deliberately open, exactly as call 9 and the calibration triggers require: opinion_promote is not in AUTO_APPLY_STAGES β every promotion needs an explicit confirm until a fresh operator ruling widens it, and it joins _ANOMALY_WATCHED_STAGES in that same change β and the two calibration numbers (0.85 similarity, the ~20-entry lane-depth warning) are unmeasured against real lane volume, which is zero today.
Note
Retired in agentm-vault plan 04. The staged-marker trigger below composed nothing in seven weeks (52 markers). The sibling step in orchestration_phase.py, crystallize.py's staging half, enable_crystallization_staging, the console section and the session brief's count are gone, and verify-phases.sh now proves a dispatch stages nothing. A crystallized note is a model-written synthesis with consolidated_from, from the weekly dreaming phase, built since v10.0.0 (AgentM Vault Β§ Dreaming), or on request through crystallize.py write. The binary's promote job writes semantic candidates and never a crystallized entry. The design below is the record of what was built.
Designed 2026-07-25; built 2026-07-26. Crystallization has had a digest schema and write/read primitives since 2026-07-07, and no way to fire. The note parked it on a premise: no bounded, detectable phase-close event existed to hang a trigger on. That premise expired. Crickets PR #214 (development-lifecycle 0.42.0) shipped agentm_bridge.py's sixth verb, phase-dispatch, and wired two call sites β work.md fires post-work after each task's commit, release.md fires post-release once the release lands. Both route through harness_memory.phase_dispatch() into orchestration_phase.py. The events crystallization was waiting for already fire today, several times per session, and now something listens on their behalf.
The one-sentence design: the two live phase-dispatch events are the trigger, and what they fire is staging β a small marker naming a session worth distilling later β because the judgment of whether a session was investigative, and the distillation that turns it into five fields, both need a model the chain cannot call.
Why staging rather than crystallizing. An orchestration chain fires outside the agent loop and cannot dispatch a sub-agent (orchestration_idle.py:24). That is the same wall the accumulate loop's LLM assist hit (call 8 there) and the same one that deferred distillation in the first place. A trigger that tried to decide "was this an exploration?" or compose the five fields at chain time would be inventing a judgment the chain has no instrument for. So the chain does the part it can do honestly and mechanically: it records that a phase closed and points at the transcript. The composing pass stays where it already is β an operator, or a future agent-side pass β and picks candidates up from a surface that now exists.
1. The trigger is both live phase-dispatch events: post-work and post-release (operator ruling). No new crickets-side hook, no new session-boundary marker, no change to work.md or release.md β this consumes a seam that already ships and already fires. post-work is correctly scoped by construction, since only work.md fires it; a /release session earns one too, because release-time debugging (red gates, a merge conflict, a tag-reachability surprise) is real investigation worth a digest. Why not post-work alone: it would silently drop the release-session case, and both events cost the same to listen for. Why not wait for a narrower future verb (a hypothetical post-bugfix-analyze or post-research): the narrower event does not exist and may never; these two do, they are the only bounded phase-close events in the system, and staging is cheap and reversible enough that a mechanical session's candidate is noise an operator dismisses, not damage. Re-audit trigger: if a narrower investigative-session event ever ships, prefer it and retire the broad post-work listen.
2. Staging is a sibling step at the subcommand level, never nested inside the reflect ladder. post_work_reflect() returns early on disabled, no-session, ambiguous-session, already-reflected, and cooldown. Since post-work fires after every task commit and reflect renames the session's .start marker to .reflected on its first success, every later commit in that session takes the already-reflected path β so staging placed as a step inside that function would never fire on what is, after the first commit, the common path. The post-work and post-release subcommand handlers each call staging as a sibling of the existing work, and merge its outcome into the result dict under a new crystallization key. Why additive under a new key rather than a restructured result: verify-phases.sh already asserts on the existing top-level "status" values (no-session among them), and those assertions should keep passing untouched β the trigger is new behavior, not a change to what the dispatches already promise. Re-audit trigger: none; this is a placement constraint the early-return ladder imposes.
3. Staging resolves from live .start markers only, and therefore runs before reflect. (Rewritten 2026-07-26 β the original version of this call shipped and was inert; see the correction below.) Staging gets its own resolver globbing session-id-*.start, filtered to markers whose transcript still exists before the count (call 9), keeping the refuse-when-ambiguous rule β zero or two-or-more live markers means skip, never guess. Its position in _main() is part of the contract: reflect renames a session's .start to .reflected on its first success, so staging must run first or the very first task commit β the one commit that needs to stage β finds nothing.
What the original call got wrong, and why no test caught it. It ordered staging after reflect and compensated by globbing .reflected too, reasoning that from the second task commit onward no .start remains. That reasoning was locally correct and globally wrong: .reflected markers accumulate for thirty days (their own GC threshold), so counting them means two reflected sessions in a month is enough to make every later resolution see multiple competing sessions. ambiguous-session becomes the permanent steady state and the trigger never fires at all. It shipped that way. Every fixture β unit and end-to-end β carried one or two markers, so nothing modeled a repo with ordinary history in it, and test_two_live_markers_is_ambiguous asserted that ambiguity is correct without ever asking whether it would be normal. Found by dry-running the shipped trigger against this repo, which carried 111 .reflected markers.
Why reorder rather than widen the glob further: reordering removes the dependence on marker history entirely, needs no freshness window or other calibration number, and needs no crickets change. What it costs: fire_count stays at 1, since later task commits find no .start and skip β the "how much work did this session do" prior is gone. That is an acceptable trade, because stage_candidate is idempotent and the candidate itself is already written; only the counter was lost. Why not thread the real session id through phase-dispatch instead (the unambiguous fix): orchestration_phase.py already accepts --session-id, but neither harness_memory.phase_dispatch() nor the crickets bridge forwards one, so it would mean a cross-repo change and tighter coupling between the two repos for a case that marker resolution now handles. Why not relax the ambiguity rule: guessing the wrong transcript is exactly the failure _resolve_session_marker's docstring refuses, and staging the wrong session is worse than staging none. Re-audit trigger: revisit if a host runs two genuinely concurrent live sessions per repo as the normal case β two agents sharing one checkout already happens here, though the live-transcript filter resolves it in practice when only one has a written transcript.
4. Candidates land in _crystallize-staging/<phase>-<session-id>.json, and that directory joins all three exclusion lists. Vault root, sibling to _dream-staging/ β the same "raw material for a later pass, not corpus content" precedent. It is added to dream._EXCLUDE_DIRS, vault_lint._EXCLUDE_DIRS, and frontmatter_validator._EXCLUDE_DIRS in one change, which keeps scripts/test_vault_lint.py:143-148's two parity assertions true rather than breaking them. Why not a kind: entry in the corpus: kind: crystallized is the composed digest, and a staged candidate is not a digest β writing one as an entry would put a contentless stub in the corpus and make parse_digest the thing that discovers it is empty. Why exclude from all three: a staged marker has no frontmatter conventions to validate, nothing worth linking, and no age worth shelving, so any stage that walked it would report a finding about a file that is deliberately not a note. Corrected 2026-07-26 after mutation-testing the built trigger: no walker reaches it today regardless β all three match *.md only while candidates are *.json, and two of the three (vault_lint, frontmatter_validator) scan personal/ and projects/ rather than the vault root. The exclusion is therefore defense-in-depth against a later change to either of those facts, not a live hazard; the original phrasing here overstated it. It is worth keeping precisely because both facts are incidental rather than guaranteed, and test_vault_lint.py now pins the membership so the guarantee cannot silently lapse. Re-audit trigger: revisit if a second consumer needs to read the staging directory, which would mean the exclusion is hiding it from something legitimate.
5. Staging is idempotent on (phase, session-id) with no cooldown. Re-firing refreshes a last_fired timestamp and increments fire_count; it never writes a second candidate for the same session. A pending cap (_MAX_PENDING_CANDIDATES, 50) skips staging when the directory is already that deep, recording the skip rather than growing without bound. Why no cooldown, unlike every sibling chain: a cooldown exists to stop expensive repeated work, and this write is a few hundred bytes β gating it on the shared phase_reflect clock would silently drop candidates for whole sessions, losing exactly the signal the trigger exists to catch, and gating it on a new clock buys nothing idempotence does not already give. Why fire_count rather than one-shot: post-work firing once per task commit makes the count a free record of how much work the session did, which is a real prior for whether it is worth distilling. Re-audit trigger: 50 is a calibration-era number with no measured pending volume behind it; revisit once real candidates accumulate, and treat a cap that trips as evidence the pickup surface is not being read rather than as a bound to raise.
6. Candidates surface as a count derived from the staging directory itself. session_brief.py gains a pending-candidate count beside count_parked and count_needs_your_eye, and console.py gains a matching section β the two surfaces that already carry "something waits for you." The count comes from globbing _crystallize-staging/*.json. Why not append to _meta/needs-your-eye.json, the channel that already feeds both surfaces: inbox_triage.py:791 overwrites that file wholesale on every run, so an appended item would be silently clobbered by the next dreaming cycle. count_parked's glob-the-directory shape is clobber-proof and needs no writer coordination; count_needs_your_eye's pointer-file shape is neither. Re-audit trigger: if the needs-your-eye pointer ever becomes multi-writer by design, fold this count into it and drop the separate glob.
7. The deferred judgment and distillation get one named seam: crystallize.exploration_judge_available(), returning False unconditionally. It covers both halves β whether a staged session merits a digest, and what the five field values are β because both need the same absent instrument, and two seams for one missing capability is two things to forget to flip. This is the fourth seam of exactly this shape, joining opinion_routing.assist_tier_available(), dream.cheap_model_tier_available(), and dream_confirm.higher_tier_model_available(). Why name it rather than leave the gap in prose: the accumulate loop's call 8 established that an unbuilt assist should be a callable returning False β it makes the gap greppable and gives the eventual build one place to change. Why not a heuristic in the meantime (transcript length, fire_count, files-touched): it would encode a guess about what makes a session investigative as though it were a measurement, and a wrong filter that runs silently is worse than an honest unfiltered list an operator skims. Re-audit trigger: flip all four seams together when a chain-time model primitive exists.
8. A picked-up or dismissed candidate is deleted, not archived. Composing a digest through crystallize.py write and dismissing a candidate both end the same way: the marker file is removed. Why no archive: the marker holds no content β a session id, a transcript path, a timestamp, a count β so once a digest exists or the operator has passed, there is nothing worth retaining, and an archive directory would accumulate stubs that mean nothing to a later reader. This is deliberately unlike _dream-staging/, whose proposals carry real proposed content that a revert or an audit may need to read back. Re-audit trigger: revisit if candidates ever carry distilled content, which would make deletion lossy.
9. The single-marker rule was unsatisfiable, and the cause was a broken transcript path β fixed separately, before this trigger builds (operator ruling β scope widened to fix the defect rather than work around it). The rule calls 2-3 depend on could not be met at all. .harness/ held 200 session-id-*.start markers and zero .reflected across seven weeks, so _resolve_session_marker() returned ambiguous-session unconditionally.
The reason was not marker churn or concurrent sessions. Both memory hooks computed the transcript path with an extra leading hyphen, so every marker ever written pointed at a directory that does not exist β and the orphan sweeper, which skips any marker whose transcript it cannot find, skipped all of them on every pass instead of ever clearing one. post_work_reflect() had therefore never once succeeded, nor had the Stop hook, which meant crickets PR #214's stated purpose β reflection on a host with no Stop hook β did not hold either. That fix landed on its own in PR #385; the 2026-07-26 amendment below carries the full account, including why a full green gate battery never caught it.
What remains locked here is the sweep this trigger depends on. The sweeper deletes a .start marker whose transcript cannot be found β past the idle threshold only, so a live session whose transcript has not been written yet is never destroyed β and any marker that keeps failing to reflect past a hard ceiling, reusing the existing 30-day .reflected GC threshold rather than inventing a number. Marker resolution then filters to markers whose transcript still exists before applying the exactly-one rule, so correctness does not depend on a sweep having run recently. Why delete rather than reflect: a transcript that cannot be found can never be reflected from, so the marker is not pending work, it is a dead pointer. Why the age gate licenses the delete rather than the missing transcript alone: a fresh marker's transcript may simply not exist yet. Why fix it rather than work around it: the trigger's entire value is a transcript pointer, and building on a rule that resolves ambiguous-session forever would ship a feature whose only observable behavior is reporting that it skipped. Re-audit trigger: if markers with resolvable transcripts start accumulating again, reflection is failing for a reason none of this found.
The live-transcript filter stays load-bearing after the 2026-08-01 payload change, but it now guards a smaller class. Reading transcript_path off the payload stops the hooks from minting dead pointers; it does not make an existing pointer immortal, since a transcript can still be deleted, rotated, or moved between the marker's write and the sweep that reads it. The filter is what keeps that ordinary decay from manufacturing false ambiguity, and it stays.
Verification lands in verify-phases.sh, not a new gate. That script already exercises both dispatches in dry-run across a vault-resident and a vault-less pass, and already covers the session-marker scenarios call 3 turns on. Extending it keeps the battery at its post-consolidation count instead of adding a 35th gate for behavior the existing phase gate is precisely scoped to.
No crickets change. This consumes phase-dispatch exactly as shipped; work.md and release.md keep their current text and their always-exit-0 contract. The one crickets-side obligation is prose: any place its development-lifecycle design describes what post-work/post-release do now names a second listener, so that description does not go stale.
As-built (2026-07-26). All nine locked calls are implemented against this section verbatim. crystallize.py gains the staging primitives it owns β STAGING_DIRNAME, _MAX_PENDING_CANDIDATES = 50, stage_candidate() (idempotent on (phase, session_id), refreshing last_fired and incrementing fire_count on re-fire), list_candidates(), drop_candidate() (delete, never archive β call 8), count_pending_candidates(), and exploration_judge_available() returning False (call 7). orchestration_phase.py gains _resolve_transcript_for_staging() β resolving from live .start markers only, filtered before the exactly-one rule (call 3 as rewritten; the original .reflected fallback shipped inert, see the amendment log) β and stage_crystallization_candidate(), called as a sibling step from _main()'s post-work and post-release branches, ahead of reflect so the marker still exists, merged under a new "crystallization" result key (call 2) with no cooldown (call 5). enable_crystallization_staging joins auto_orchestration.DEFAULT_CONFIG, default True. _crystallize-staging joins dream._EXCLUDE_DIRS, vault_lint._EXCLUDE_DIRS, and frontmatter_validator._EXCLUDE_DIRS in one change (call 4), keeping scripts/test_vault_lint.py's parity assertions true. session_brief.py and console.py each gain a directory-globbed pending count beside their existing push-surface signals (call 6). Tested by scripts/test_crystallize_staging.py (28 tests: staging primitives, the tolerant resolver's dead/live/ambiguous cases, and the dispatcher's disabled/no-session/staged/refreshed/capped/dry-run/ambiguous states) and a new section in scripts/verify-phases.sh (10 checks) proving the sibling-step wiring end-to-end through the real CLI β including the exact scenario call 2 exists to prevent: staging still fires once reflect has renamed a session's marker to .reflected. No new gate; verification rides the existing phase-lifecycle check. check-all.sh: 34/34 green, no new row.
- Writes to Memory β every direction lands in the Memory System, and Experience is the growth engine that grows it into the interconnected knowledge base (its How it grows section).
- Sharpens Opinions β the learned supplement is what makes an opinion's vault layer grow over time (the Opinions design); the accumulate loop that filled it is retired (see its section above).
- The runner is shared substrate, built 2026-07-06 β the Maintainer + Researcher persona loops (Personas design) run on it too, once each of those is built.
-
crickets touch is light β the memory-engine sub-agents (
memory-idea-researcher,adapt-evaluator) run read-only inside the harness; reflection mines sessions that used crickets tools but depends on none; the weekly field brief reaches out to sources. Experience keeps working on a bare agentm.
-
The weekly field brief is built, has been run once by hand, and has not yet been run by the runner; crystallization is built for use on request; dreaming's Python cycle is built and ships dry-run. The brief (task 185) replaces forward learning, which retired on 2026-10-03; its first brief was run by hand on 2026-10-03 and emailed, the job is registered with that run as its last run, and its first run on the runner (Saturday 2026-10-10 evening) needs the live clone advanced at the next release. Crystallization (
crystallize.py) ships the digest schema and its write/read CLI; the weekly crystallize phase the vault design names is built (v10.0.0;daemon/internal/crystallize/crystallize.go:779). The Python cycle (dream.py) reports and proposes and writes no note; its job template (templates/jobs/dream.yaml) shipsdry_run: true, and a repo copies it into.harness/jobs/by hand (install.shdoesn't seed it). The runner they all schedule on is built (2026-07-06) β no longer part of any of these gaps. - The accumulate β Opinions loop is retired. Session 1 of the vault series retired its lanes; Stage 1's routing left reflection in agentm-vault plan 03, and Stages 2-3 left in plan 04.
-
Re-audit triggers: revisit the forward-experience section once the brief has run for four weeks; revisit crystallization when the weekly crystallize phase ships; re-audit dreaming's dry-run/opt-in posture once
install.shseedstemplates/jobs/automatically or an operator promotes a job manifest'sdry_runtofalse.
-
harness/skills/memory/scripts/βheat_policy.py(promote/demote/pin),reflect.py(transcript mining),ideas_incubator.py,adapt_skills.py,field_brief.py(the weekly field brief: engine, note,keep; tested byscripts/test_field_brief.py),crystallize.py(the phase-close digest schema +crystallize_exploration()/parse_digest()primitives, operator-invoked,PLAN-wave-e-experiencetask 2; tested byscripts/test_crystallize.py),revert_log.py(the append-only undo journal; tested byscripts/test_revert_log.py),dream.py(the Python half of the night β reads, reports and proposes; possible twins, shared keys and proposed facets for the needs-review map; tested byscripts/test_dream.py),needs_review.py(the needs-review map, with the dream cycle's three sections; tested byscripts/test_needs_review.py) -
templates/jobs/field-brief-weekly.yamlβ the weekly field brief's job manifest for the Runner (schedule: weekly,lookback: 2d, window18:00-23:00,tier: T3,dry_run: false); tracked template, copied to.harness/jobs/on the day the first brief should run β tested byscripts/test_field_brief_job.py -
templates/jobs/dream.yamlβ the Python cycle's scheduled job manifest for the Runner (schedule: daily, window02:00-06:00, order 3,tier: T3,dry_run: true); tracked template, copied to.harness/jobs/dream.yamlby hand to opt in β tested byscripts/test_dream_job.py -
harness/hooks/βmemory-reflect-stop,memory-reflect-idle(the backward triggers) -
harness/agents/βmemory-idea-researcher,adapt-evaluator(read-only memory-engine sub-agents) - vault
decisions/research-dream-mode-design.mdβ the locked-but-unbuilt dreaming design -
~/.claude/CLAUDE.md(opusplan) + the heat policy β the token-cost lever behind curation - vault
_harness/designs/architecture-governance/ACCUMULATE-LOOP-SPEC-DRAFT.mdβ the landed accumulate-loop spec's original source draft (Fable-authored, 2026-07-06); the condensed twin lives in the Opinions design
2026-10-16 β reflection records ideas and the night judges them (task 192, backlog #676). The Backward experience reflection bullet is reconciled to what runs. It used to describe three confidence lanes into an inbox, a MEMORY_REVIEW_MODE that routed them, and reflection seeding idea incubation. The inbox lanes retired with filing v2, and the mode routes nothing since then; the incubator is not called from reflection, and its retirement is #674. What the bullet says now: an unattended session is not mined. Only HIGH becomes a card. A question or a line the session already carried is dropped with a reason. An idea is a ## Ideas entry for the nightly idea judge, which calls it steering, a repeat, or a new idea for the inbox. AgentM Vault's row of the same date is the governing amendment. Why not leave the stale bullet for the incubator's retirement: this is the design reflect.py is governed by, and it described a routing the code hasn't done since 2026-09-04. Re-audit trigger: the judge's verdicts read poorly on the supervised night, or a mined idea reaching personal/ideas/ without review.
2026-10-16 β the weekly crystallize phase's [PENDING-IMPL] flipped to as-built (v10.8.0 release sweep). The Backward experience bullet, the retirement note under Crystallization's phase-close trigger and the Risks bullet still said the weekly phase "is not built yet". It has been built since v10.0.0 (agentm-vault plan 11, PR #656): Run in daemon/internal/crystallize/crystallize.go:779 finds the recurrences and writes the lessons, templates/jobs/crystallize-weekly.yaml is its weekly job (it ships enabled: false; turning it on is two edits, the template's and daemon.crystallize_enabled), and Memory-Daemon is its reference. Task 191 touched the package again for its one-page-one-source bar, which is how the stale marker surfaced. No decision changed. The 2026-07-26 and 2026-07-07 entries below keep the wording of their day. Re-audit trigger: another [PENDING-IMPL] found on a surface that has shipped.
2026-10-03 β the first brief is run, and the weekly job is registered (task 185, step 4 close-out). The first brief was run by hand on Saturday 2026-10-03, at the operator's choice and with the real send approved for it: five items, $0.244, 32 seconds, one email that arrived, and a second --ask run on the same question with four new items and no overlap. The weekly job is registered on the primary clone with that run recorded as its last run, so its first run on the runner is Saturday 2026-10-10 evening and the job's day is Saturday; the section's earlier "supervised Sunday evening" is superseded, and the log row below it keeps what it said that day. Still unproven: the runner's own first run, which needs the live clone advanced at the next release (a range that also carries task 186's Go changes, so with the daemon rebuilt and restarted). Why a by-hand first run instead of a supervised runner run: the operator chose it, and it proved everything the runner does not (the engine, the audit, the note, the email, the seen-list); the runner adds only the schedule. Re-audit trigger: the first runner run, and the fleet ceiling if crystallize's Monday anchor drifts onto the brief's day.
2026-10-03 β the weekly field brief is built, and is the forward-experience pass (task 185, step 4). The Forward experience section drops its [PENDING-IMPL] marker and describes what ships: harness/skills/memory/scripts/field_brief.py writes the week's note, emails it through the autonomy mailer (session_email.send(), lifted out of the daily email without changing its send), and templates/jobs/field-brief-weekly.yaml runs it weekly in an evening window. The overview, the risks bullet, the diagram alt text and the references follow. What is still unproven: the first run on the runner, supervised on a Sunday evening with the operator confirming the email, which the plan makes its exit; this row does not claim it. Why a script that drives the engine, not the last30days skill: the skill's contract is interactive (a first-run wizard, AskUserQuestion, a stop-and-wait step) and cannot be followed headless; its engine runs through Bash in about four seconds, so the script builds the plan from the operator's topics and gives the model only WebSearch and WebFetch, with the stream audited afterward (task 185's engine-check.md). Why the model returns JSON: two tiers returned different prose shapes and one ended by asking a question, which cannot be parsed into the ten-item cap, the seen-list and the keep boxes. Re-audit trigger: as the section says, the first month of briefs; and the fleet ceiling when crystallize's weekly anchor lands on the brief's day.
2026-10-03 β forward learning retired; the weekly field brief replaces it (task 185, step 1; the operator's rulings of 2026-10-02). The Forward experience section is rewritten, and the overview, the incubator line, the risks and the references are reconciled with it. The pipeline's job, scripts and review surface are gone and its entries are archived, not deleted; the brief itself is [PENDING-IMPL] until steps 2 to 4 land. Why not keep the pipeline and add the Pass-2 judge this design named as its v2: the weakness was never the screening. The output went to a watchlist nobody opens, and a better-scored entry in the same place would have gone unread too; a brief that arrives weekly, ranked against the work in flight, changes where the output lands. Why not retire the skill-discovery twin in the same change: the inventory found it live, so only the review CLI the two pipelines shared went, and the twin's retirement is its own backlog item. Re-audit trigger: the first month of briefs, as the section says.
2026-09-28 β the watermark cache's documented home corrected (GH #711). The Forward experience section named _meta/forward-learning-cache/ for the per-source watermarks. The code keeps them in agentm's engine state directory (forward-learning-cache/state.json under engine_state.engine_state_dir()), which is machine state rather than vault content, and has since filing-v2 part 2a. The body now says so. No decision changed; the 2026-07-07 entry below keeps the path it launched with, as the record of that day. Re-audit trigger: the watermarks moving back into the vault.
2026-09-25 β the forward-learning watchlist's documented home corrected to resources/watchlist/<source>/ (task 176 step 8). The Forward experience section above still named the pipeline's 2026-07-07 launch location, personal/_watchlist/, stale against where it had been writing since: projects/agentm/_watchlist/. Task 176 step 8 moved it again, live on 2026-09-25 (vault commit 7ac40ea5cd): 101 items to resources/watchlist/<source>/ in the shared reference library. vault_layout.feature_state_candidates("_watchlist") now resolves only that path β the project-space fallback it carried during the migration window is removed. forward_learning.py and watchlist_review.py follow the same resolver. The skill-specific pool is untouched β Projects/agentm/_skill-watchlist/ stays where it was. Why not also correct the amendment log's own 2026-07-07 entry below: that row records what shipped that day β a dated entry is never rewritten to match a later move. Re-audit trigger: if the watchlist resolver grows a further fallback, or resources/watchlist/ changes shape again.
2026-09-11 β the Python cycle shrinks, and the body is reconciled (agentm-vault plan 04, task 5; landing group 9). dream.py now reads, reports and proposes, and changes no note. What stays: the corpus meters, the filing contract read fail-closed, lint as a report, possible twins (dedup at 0.92), shared keys (contradiction triage), proposed facets, and the breaker status and correction loop. The findings go to <engine state>/dreaming/review-proposals.json and become three needs-review sections, and python-cycle.json is the report the morning note will read. Retired, each confirmed caller-free first: the lifecycle stage and its archive helpers; tidying and its artifact shelf; compression; the lint repair lane; the opinion-supplement stage (opinion_supplement.py, its console section, verify-opinion-supplements.sh); insight generation and qualification; the sampled audit (model_tiers.py, its console section); entity rollups, stub synthesis and the unfiled drain; the confirm-and-revert path (dream_confirm.py, its staging, TTL, auto-apply batch and anomaly breaker); and crystallization staging (the sibling step, enable_crystallization_staging, the console section, the session brief's count). The template runs daily in the window at order 3 with no batch cap. verify-dreaming.sh narrows to the twin, no mutation, the map and the manifest, and verify-phases.sh now asserts a dispatch stages nothing. In this design, the Dreaming section is rewritten to what runs; the crystallization bullet, the trigger's note, the accumulate loop's note, the risks and the references are reconciled, and the references drop the deleted modules, inbox_triage.py among them. The overview diagram still draws dreaming consolidating into insights. Why not keep the confirm path as the safe default: 167 dedup proposals drew 11 confirmations in two months, and a proposal you act on by hand in Obsidian is the same act with git as the undo. Re-audit trigger: the Possible twins section holds more than twenty pairs for a month, the vault design's own trigger for fuzzy pairs.
2026-09-06 β session 3 of the vault-perfection series rules on the nightly cycle (AgentM Vault Β§ Dreaming). Measured that night: no step spends a token, because enrichment is the only model-calling code and has no scheduler; the Python cycle's cheap-model primitive returns false by construction and the sampled audit has sampled nothing on any run; the dedup confirm path took 167 proposals to 11 confirmations in two months; the promote job, reading ## Recalled, would have crystallized a test token. Ruled there: the night's order and window, a token budget per tier, the deep and light passes, crystallized as model syntheses at task close or on request, promote reading captured and candidate lines, eight Python stages and the confirm path retired, one morning note. Why not amend this design's body in the same change: the retirements land as one landing group with per-stage counts, and the body is reconciled when the code goes, not before; the two notes at the top of the affected sections carry the ruling until then. Re-audit trigger: landing group 9 merges.
2026-09-04 β the calendar's facet-promotion stage ships (filing v2 part 5 task 5, v9.15.0). _stage_facet_promotion() joins run_dream()'s hand-wired sequence: a diary label recurring on three or more distinct days within a 30-day window becomes a stage="facet_promotion" proposal β the contract with one line added under facets: β through the same propose β confirm β revert-log path every other proposal uses. Deliberately never in AUTO_APPLY_STAGES. Why not auto-apply, the way the recurrence gate does for consolidation: the facet registry is the operator's, per AgentM Filing v2's part-1 ruling β and even a quarter of uncontested confirms points toward a lighter confirm surface there, never toward auto-apply, the same standing this section already gives opinion_promote above. The register's weekly/monthly reviews (calendar_rollups.catch_up()) ride run_dream_and_auto_apply()'s existing cadence unconditionally, right after the needs-review MOC regenerates, and the digest gains a "Calendar rollups: N review(s) checked, M written" line. Re-audit trigger: none of its own β inherits the register's own triggers at AgentM Filing v2.
2026-09-03 β the lanes' home is memory/crystallized/<name>/ (filing-v2 part 3, v9.12.0). The corpus migration folded _opinions/ into the crystallized class as the design ruled, keeping the lane shape: each opinion's lane is a subdirectory, its served file sits beside the crystallized memories, kind kept. opinion_supplement.lane_base() resolves the root β the pre-migration _opinions/ while it exists, the class once moved β and reflect's writer, lane_dirs, the cycle and the served path all go through it; locked calls 6 and 7 above now read that way. The hazard locked call 6 answers follows the lanes: dream's general walk skips any subdirectory of crystallized/ and a served file by its kind, vault_lint and frontmatter_validator exempt kind: opinion-supplement wherever it sits, the console's curated count skips the lanes, and the daemon's meter sampler excludes them (MeterExcludedNested). Why not flatten the lanes into the class: nine consumers walk <root>/<opinion>/, and a directory exclusion was the only thing keeping a served supplement out of the general stages β a flat layout would have needed kind-based skips in every one of them and lost the per-opinion trigger. Why a resolver rather than a constant: the move ran live under quiesce with the readers deployed first (the 2b doctrine), so the one rule had to hold on both sides of the move. Re-audit trigger: a writer recreating _opinions/ after the move would split a lane across two roots β the resolver prefers the legacy root while it exists, so a stray write there wins until it is re-filed; if the Python capture path (part 4's) ever grows a lane writer, it must go through lane_base().
2026-08-01 β the hooks read transcript_path off the payload; the slug formula demotes to a fallback. Claude Code sends transcript_path on every hook event, SessionStart and Stop included, and its hook documentation says to use that path rather than constructing one from session_id β the correspondence between a session id and a transcript filename is not guaranteed. Both memory hooks now read the field and write it straight into the marker's transcript: line. Nothing downstream changed: memory-reflect-idle and orchestration_phase.py already read that line and neither knows where the value came from.
This is hygiene, not a repair. Reflection worked before this change and works after it. _resolve_transcript_for_staging() filters markers to those whose transcript actually exists before applying its exactly-one rule, so the phantom markers the computed formula produced were discarded correctly rather than causing a wrong resolution β verified end-to-end with two phantoms and one live marker present, where the trigger resolved the live session. What the change buys is that the phantoms stop being written at all, and that a whole class of path-construction bug leaves the codebase. That class has now cost this repo twice: the 57-day outage below, and a second instance found in the same sweep β .claude/hooks/session-cost-capture.sh had never had the . β - correction applied, having been missed by PR #385 because it lives under .claude/ rather than harness/hooks/, so cost capture had been silently skipping every worktree session. Fixed in this change alongside the two memory hooks.
The residual symptom that prompted it, measured 2026-08-02: SessionStart sometimes fires with a session id that has no transcript file anywhere on disk β resume, compact and fork are the likely sources, and /branch is documented to mint a new id. Of three markers written in this repo that evening, one had a live transcript and two were phantoms pointing at files that had never existed.
The fallback is kept, deliberately (operator recommendation, adopted). When the payload carries no transcript_path, the hooks still compute the corrected slug. Why not delete it outright: a host old enough to omit the field would then get no transcript pointer at all, turning "this Claude Code predates the field" into "reflection silently never runs" β the exact failure shape the 57-day outage had, arrived at from the opposite direction. The fallback costs one branch and is exercised on both ends: verify-hook-resolution.sh splits its payloads so checks A and C resolve through the computed path while F and G resolve through the payload's, and scripts/test_transcript_slug.py keeps pinning the formula against hand-written literals. A fallback nothing exercises is a fallback that rots, and the host that needs it is the one least able to report that it broke. Why a payload path that names a missing file is not retried against the computed one: that would be guessing at a path again, which is the thing being retired; the hook reports and skips.
source: joins the marker β startup, resume, clear, compact, fork, or unknown when absent. Nothing reads it. It earns its line because it is the field that would have made this investigation short: a phantom marker with source: compact explains itself, and the same marker without it is a mystery to be re-derived. Markers written before the field parse identically, since every reader keys on transcript: alone.
On what the tests are allowed to assert. test_transcript_slug.py exists because the old checks mirrored the implementation's formula instead of pinning it to reality, and that intent survives the demotion intact β it still pins literals, and it gains a check that each hook actually prefers the payload, so the two suites cannot drift apart while both stay green. The payload branch is proven behaviorally instead of structurally: its fixtures sit outside the fake HOME's projects tree entirely, at paths no slug on either platform could produce, so a hook that went back to computing cannot reach them by accident. Mutation-checked by reverting the branch on a scratch copy and confirming the new assertions fail. Re-audit trigger: when Claude Code's hook payload gains a field these hooks should prefer over one they compute, prefer it β and when the oldest supported Claude Code is newer than transcript_path, delete the fallback and the slug tests together, in one change, rather than leaving tests pinning a formula nothing reaches.
2026-07-26 β the trigger shipped inert; call 3 rewritten, staging reordered ahead of reflect. v9.4.0's trigger could not fire in any repo with ordinary history, and the mutation pass below did not catch it because it tested whether the code did what the design said, not whether the design was right. Caught instead by dry-running the shipped feature against this repo: ambiguous-session, unconditionally.
The defect. Call 3 ordered staging after reflect, then compensated for reflect's .startβ.reflected rename by globbing .reflected as well. But .reflected markers persist for thirty days by design β they are the dedup record, GC'd on their own schedule β so after the second reflected session in a month, resolution always sees multiple competing sessions and refuses. This repo had 111 of them, 110 from the same day's backlog recovery, and the trigger was dead from the moment it released. Nothing corrupted; it simply reported that it had skipped, forever.
Why the tests were green. Every fixture, unit and end-to-end, carried one or two markers. None modeled a repo with history. And test_two_live_markers_is_ambiguous asserted that ambiguity is the correct response to two live sessions β true, and beside the point, because the real question was whether ambiguity would be the normal condition. A suite can be exhaustive about the contract and still never ask whether the contract survives contact with a week-old working directory.
The fix (operator ruling): staging runs first in _main(), and resolves from live .start markers only. On the first task commit the marker still exists, so the candidate is written; on later commits there is no .start and staging skips, which is harmless because the candidate is already there and stage_candidate is idempotent. Accumulated .reflected history is now invisible to resolution. The cost is fire_count, which stays at 1 β the "how much work did this session do" prior is gone, traded for a resolver that does not degrade as a repo ages. Rejected alternatives, with reasons, are recorded in call 3 above: a freshness window (needs a calibration number and still fails on a long session) and threading the real session id through phase-dispatch (correct, but a cross-repo change for a case marker resolution now handles).
Verified against reality, not only fixtures. With 111 .reflected markers and a dead .start present in this repo, a dry-run now resolves the one live session correctly. Two new regression tests model a repo with history β one at the resolver level (40 .reflected plus one live .start), one end-to-end in verify-phases.sh (11 markers through the real CLI) β plus an assertion that the staging-before-reflect ordering actually held. Re-audit trigger: when a fixture's realism is the thing under question, dry-run against a real working directory before calling a trigger built; a scratch directory has no history and will never tell you that history matters.
2026-07-26 β mutation-tested the shipped trigger; one real coverage miss and one near-miss closed, and one design rationale corrected. Asked whether the battery would actually catch the trigger silently breaking, so twelve failure modes were introduced one at a time and the battery run against each. Eleven of twelve were caught, and three of those (severed sibling-step wiring, a STAGING_DIRNAME drift, an accidentally-added cooldown) were caught only by verify-phases.sh β the end-to-end CLI check is load-bearing rather than redundant, since the unit tests call stage_crystallization_candidate() directly and cannot see _main()'s wiring at all.
Two gaps, both the same shape as the slug bug found earlier the same day. The miss: dropping _crystallize-staging from all three _EXCLUDE_DIRS lists went entirely undetected, because test_vault_lint.py's parity tests assert the three lists agree with each other and say nothing about their contents β remove a directory from all three and they stay consistent. test_staging_and_lane_dirs_are_actually_excluded now pins membership for the directories whose exclusion is a stated guarantee (_crystallize-staging, _opinions, _inbox, _dream-staging) rather than an incidental default. The near-miss: the staging directory name lives as three independent copies (the constant, plus a hardcoded literal in each push surface), and renaming the constant left both surfaces globbing the old path and silently reporting zero β with both surface suites still green, each having written its fixture at its own literal and asserted the count, a mirror that can only prove a surface agrees with itself. Only verify-phases.sh's hardcoded path caught it, incidentally. TestStagingDirnameHasOneMeaning now pins all three, including a behavioral check that stages one candidate through the real writer and asserts both surfaces count it.
The rationale correction is in call 4 above: it claimed every stage that walked the staging directory would report a finding, but no stage reaches it today at all (*.md-only matching against *.json candidates; two of three walkers never scan the vault root). The exclusion is defense-in-depth, and the design now says so. Re-audit trigger: when adding a directory to the exclusion sets, add it to the membership test in the same change β the parity tests alone will not notice its absence.
2026-07-26 β crystallization's phase-close trigger built against all nine locked calls; [PENDING-IMPL] narrows to the distillation seam alone. Same-day follow-on to the design pass and the transcript-slug fix directly below, in that order β the trigger depends on marker resolution actually working, which the slug fix restored. crystallize.py gains stage_candidate() / list_candidates() / drop_candidate() / count_pending_candidates() / exploration_judge_available(); orchestration_phase.py gains the tolerant .start-or-.reflected resolver (call 3) and stage_crystallization_candidate(), wired as a sibling step under both post-work and post-release in _main(), merged under a new "crystallization" key (call 2) β proven necessary, not just prescribed, when the naive placement (a step inside post_work_reflect()) was checked against the real early-return ladder and found to never fire past the first task commit. _crystallize-staging joins all three _EXCLUDE_DIRS lists (call 4); session_brief.py and console.py each gain a directory-globbed count (call 6). 28 new tests in scripts/test_crystallize_staging.py plus a 10-check extension to scripts/verify-phases.sh β no new gate. check-all.sh: 34/34 green. Full as-built note lives in the Crystallization's phase-close trigger section above. Re-audit trigger: the section's own β the exploration_judge_available() seam, and call 1's preference for a narrower investigative-session event if one ever ships.
2026-07-26 β session reflection had never run; the transcript-path slug was wrong by one character. Backward experience is the pillar every other direction feeds from, and none of it had ever fired. Both memory hooks computed the transcript path as "-" plus the cwd with / turned into -, but an absolute path's own leading / already becomes that hyphen β the extra prefix turned a cwd of /a/b into --a-b, matching no directory Claude Code creates. memory-reflect-stop logged "transcript not found (skipping)" and exited on every session; every marker memory-recall-session-start wrote carried an unresolvable pointer; and the orphan sweeper skips any marker whose transcript is missing, so it skipped all of them, every pass, rather than ever clearing one. 200 markers across 57 days, zero .reflected β no session had ever been reflected. The formula was wrong a second way for worktree sessions, which this repo runs per plan: Claude Code converts . to - too, so .claude/worktrees/x slugs to --claude-worktrees-x. The fix is tr '/.' '--' with no prefix.
Why the gates were green the whole time, which is the more important lesson. verify-hook-resolution.sh computed the expected path with the same wrong formula and seeded its fixture there, and test_memory_reflect_stop_hook.py's helper did the same under a comment reading "mirror the hook's formula." A test that mirrors the implementation proves the two agree and says nothing about whether either is right; here both agreed with each other and neither agreed with Claude Code, for 57 days, across a full green battery. Both mirrors are replaced: the convention is stated independently, and scripts/test_transcript_slug.py pins it against literals taken from real ~/.claude/projects/ directory names, reading each script's own CWD_SLUG= line rather than reimplementing it. The sweeper also stops leaking β a marker past the idle threshold with a missing or absent transcript is deleted as a dead pointer rather than skipped forever, guarded by an age gate so a live session whose transcript has not been written yet is never destroyed, with a test for that guard specifically.
The formula itself was retired on 2026-08-01 β the hooks now read transcript_path off the hook payload, and the corrected slug survives only as the fallback for hosts too old to send it. See the amendment log entry of that date.
This is the cause the crystallization trigger's call 9 was written against. That call found the single-marker rule unsatisfiable and named the sweep needed to make it satisfiable again; this is the defect underneath it, fixed on its own so the trigger builds on a reflection path that actually runs rather than shipping alongside a broken one. Re-audit trigger: if markers with resolvable transcripts start accumulating again, reflection is failing for a reason none of this found; and any new check that computes a path the implementation also computes should be treated as a mirror until it is pinned to an independent fixture.
2026-07-25 β crystallization's phase-close trigger gets an implementable design; nine locked calls, no code (design pass). The trigger had been parked since 2026-07-07 on a premise that has since expired. That note deferred it because "the close of a completed exploration" named no bounded, detectable event anywhere in the codebase β true when written, and false now: crickets PR #214 (development-lifecycle 0.42.0) shipped agentm_bridge.py's phase-dispatch verb with two live call sites, so post-work fires after each task's commit and post-release after a release lands, both landing in orchestration_phase.py through harness_memory.phase_dispatch(). The events crystallization was waiting for have been firing for weeks with nothing listening on its behalf. Operator rulings, two: the trigger listens on both events, not post-work alone β a /release session's debugging is real investigation, and both events cost the same to listen for; and the trigger builds against these events now rather than waiting for a hypothetical narrower investigative-session verb, which does not exist and may never.
The reframing that made it buildable: the trigger stages, it does not crystallize. The chain fires outside the agent loop and cannot dispatch a sub-agent (orchestration_idle.py:24) β the same wall that deferred distillation originally, and the same one the accumulate loop's call 8 hit. So a trigger that decided "was this an exploration?" or composed the five fields at chain time would be inventing a judgment with no instrument behind it. What fires instead is a small marker naming a session and pointing at its transcript; the composing pass stays with an operator or a future agent-side pass, and now has a surface to pick candidates up from. Both deferred halves sit behind one named seam, crystallize.exploration_judge_available() returning False β the fourth seam of that shape, flipped with the other three when a chain-time model primitive exists.
Three calls exist because the obvious implementation is wrong, and the design pass caught it on paper. Staging cannot be a step inside post_work_reflect(): post-work fires after every task commit, reflect renames the session marker to .reflected on its first success, and every later commit therefore takes the already-reflected early return β so the natural placement would never fire on what is, after the first commit, the common path. Staging is a sibling step at the subcommand level instead, merged into the result under a new crystallization key so verify-phases.sh's existing top-level "status" assertions keep passing untouched. For the same renaming reason, staging needs its own marker resolver globbing .start and .reflected, keeping the refuse-when-ambiguous rule β without it a candidate carries no transcript pointer, which is the only thing making it actionable days later. And staging takes no cooldown, unlike every sibling chain: a cooldown protects against expensive repeated work, this write is a few hundred bytes, and gating it on the shared phase_reflect clock would silently drop whole sessions' candidates. Idempotence on (phase, session-id) gives the same safety, and the fire_count it refreshes is a free record of how much work the session did.
The design pass found a live defect, and the operator widened scope to fix it rather than work around it (call 9). The rule calls 2-3 lean on turns out to be unsatisfiable in practice: .harness/ holds 199 session-id-*.start markers and zero .reflected across seven weeks, so _resolve_session_marker()'s exactly-one requirement resolves to ambiguous-session unconditionally β which means post_work_reflect() has never once succeeded in this repo, and the zero-.reflected count is the proof rather than an inference. The orphan sweeper is installed and has fired every session throughout; it only renames on a successful reflect and skips any marker whose transcript is gone, so a marker outlives its transcript permanently and the pile only grows. Crickets PR #214's stated purpose β reflection on a host with no Stop hook β therefore does not currently hold. Operator ruling: fix it here. The sweeper deletes markers whose transcript no longer exists plus anything past a hard age ceiling (reusing the existing 30-day .reflected GC threshold rather than inventing a number), and resolution filters to live-transcript markers before counting, so correctness does not depend on a recent sweep. Reflect starting to work on this repo is the point, not a side effect. Why not a recency-window workaround instead: it leaves the leak growing and still trips whenever two sessions start in the same window β a feature whose only observable behavior would be reporting that it skipped.
The other calls, in one line each. Candidates land in _crystallize-staging/<phase>-<session-id>.json, sibling to _dream-staging/, and that directory joins dream, vault_lint, and frontmatter_validator's three _EXCLUDE_DIRS in one change β which keeps scripts/test_vault_lint.py:143-148's parity assertions true rather than breaking them; a staged marker is not a note, so no lint, link, or shelve stage may walk it. A pending cap of 50 bounds the directory, a calibration-era number whose tripping should be read as evidence the pickup surface is not being read. Candidates surface as a count globbed from the staging directory in session_brief.py and console.py β deliberately not appended to _meta/needs-your-eye.json, because inbox_triage.py:791 overwrites that file wholesale every run and an appended item would be silently clobbered; count_parked's glob shape is clobber-proof where count_needs_your_eye's pointer shape is not. A picked-up or dismissed candidate is deleted rather than archived, since the marker holds no content worth retaining β unlike _dream-staging/, whose proposals carry real content a revert may need. Verification extends verify-phases.sh, which already exercises both dispatches and the session-marker scenarios, rather than adding a 35th gate to a battery the consolidation arc deliberately merged down to 33.
No crickets code change. This consumes phase-dispatch exactly as shipped, with work.md and release.md unchanged and their always-exit-0 contract intact. The only crickets-side obligation is prose β its development-lifecycle design's description of what those dispatches do now names a second listener. No code ships in this pass; the [PENDING-IMPL] marker narrows from "trigger and distillation both missing" to "trigger designed, unbuilt; distillation deferred behind a named seam." Re-audit trigger: flip the marker when the trigger builds; revisit call 1 if a narrower investigative-session event ever ships, call 5's cap of 50 once real pending volume exists, and call 7 when a chain-time model primitive makes all four seams flippable.
2026-07-25 β Stages 2-3 built; the accumulate loop's marker flips to as-built (PR #380, commit 4f62c77, same-day follow-on to the design pass below). The build session implemented all ten locked calls against this section verbatim β no relitigation. The mechanism landed exactly where the design put it. opinion_supplement.py is a new stdlib-only leaf module carrying the recurrence gate (same-opinion difflib clustering at RECURRENCE_SIMILARITY_THRESHOLD = 0.85, promotion gated on RECURRENCE_SESSION_THRESHOLD = 2 distinct sessions β calls 4-5-7), the narrow direct-negation contradiction check (shared work-domain anchor plus opposite polarity, reusing opinion_routing's vocabulary), and whole-lane composition regenerating personal/_opinions/<name>.md every cycle. dream._stage_opinion_supplement() joins run_dream()'s hand-wired sequence and stages every promotion as a stage="opinion_promote" proposal through the existing propose β dream_confirm.confirm() β revert-log path β deliberately not in AUTO_APPLY_STAGES, per call 9. kind: opinion-supplement is registered in kind_registry.KNOWN_KINDS (call 3), _opinions/ joins dream._EXCLUDE_DIRS so no general dreaming stage touches a served supplement or lane entry (call 6), and session ids thread through reflect.route_candidates into the supplement writer's new sessions: field (calls 1-2). The health surface ships as _meta/opinion-supplement-health-latest.json + console.section_opinion_supplements + scripts/verify-opinion-supplements.sh β a new gate in check-all.sh (34/34 green) and a Linux/Mac CI step, with Windows covered by test_*.py auto-discovery on the verify-dreaming.sh precedent (call 10). CI green on all three OSes (Linux 1m30s / Mac 3m6s / Windows 3m35s). One reconciliation, blessed here: call 2 named a new signal_source: frontmatter field, but Stage 1's existing source: field already is that exact thing β an optional origin tag, provenance-only, never a routing key. The build kept source: and added nothing; this amendment reconciles the section's field name to match. Two fields carrying one fact is a drift generator, and every consumer of the field reads its meaning, not its name. Carried forward, not closed: the two calibration numbers (0.85 similarity, the ~20-entry lane-depth warning) are still unmeasured β nothing has been mined into _opinions/ on the real vault yet, so their first re-audit waits on real lane volume. Call 9's auto-apply ruling stays open by design: opinion_promote leaves the confirm-gated window only on a fresh, explicit operator ruling recorded here, and joins _ANOMALY_WATCHED_STAGES in that same change. One out-of-scope gap, tracked here and closed same-day: vault_lint.py kept its own _EXCLUDE_DIRS, distinct from dream.py's, and did not list _opinions/ β low-risk while lanes sat empty, but a served supplement is text the agent reads as its own standards, so no lint stage may touch it. PR #382 (2026-07-25) added the exclusion to vault_lint.py and to frontmatter_validator.py (a second mirror of the same set, caught in the same pass), and pinned the three lists together with parity tests in test_vault_lint.py. The lists stay deliberate standalone copies β dream.py keeps lint out of its module import graph on purpose β so the mirror is now enforced by a failing test rather than tracked in prose. Re-audit trigger: re-measure the two calibration numbers once real lane volume exists; record the auto-apply ruling here when the operator makes it.
2026-07-25 β the accumulate loop's Stages 2-3 get an implementable design; ten locked calls close the seven holes Stage 1 left open (design pass, no code). Stage 1 shipped in v9.1.0 against a spec that was a design amendment rather than a buildable design, and the operator deferred the rest rather than let a build guess β this is self-modifying behavior, so a wrong design accumulates wrong standards and then enforces them. This pass closes all seven holes the archived Stage 1 plan enumerated, and reconciles the section body to what Stage 1 actually shipped.
The disqualifying hole, and the reframing that closed it. The signal β opinion map keyed on sources that emit nothing machine-readable, and three of its four named sources (/review, token-audit, the PII scrubber) live in crickets rather than agentm β so building the map as specified meant a cross-repo instrumentation project standing between the loop and every one of its guards. Operator ruling: the map is dropped as a routing mechanism. Stage 1's text-shape classifier is the sole classifier β already shipped, already tested, and it caught two real over-routing bugs during its own build. The table survives as a reader's aid and nothing consults it; a new optional signal_source: field records provenance when a caller knows one, so a source that starts emitting structured records later fills that field and nothing else changes. Why not a hybrid keyed on the signals that already emit structured entries: more machinery for a prior that fires on a minority of candidates and still needs the text classifier underneath.
The second operator ruling β promotion is confirm-gated first, then auto-applies. A served supplement is text the agent reads as its own standard, which is higher stakes than a _shelf/ move, and AUTO_APPLY_STAGES' own docstring already requires a fresh ruling before a stage joins it. _meta/opinion-promotion-cutover.json stamps the first run (inbox triage's ensure_cutover_marker() shape, reused); every promotion stages as stage="opinion_promote" requiring dream_confirm.confirm(); auto-apply turns on only on an explicit later ruling recorded here, and opinion_promote joins _ANOMALY_WATCHED_STAGES in that same change. Why not auto-apply immediately: the first pass over real content would self-modify before a single proposal had been read. Why not gate it permanently: a standards loop that needs a human every cycle stops running.
The other eight calls, in one line each. Supplement schema β sessions: / signal_source: / refs: / promoted: on top of Stage 1's frontmatter, reusing the existing supersedes: chain, with a flat refs: list rather than typed fields because typed fields need emission that call 1 established doesn't happen. kind: opinion-supplement is registered in KNOWN_KINDS β a defect this pass surfaced rather than invented, since Stage 1 writes that kind today and nothing carries it, so the first mined standard would take the vault off zero unrecognized. Recurrence identity is same-opinion difflib similarity at 0.85 (the linker's confident band, not dedup's near-verbatim 0.92, because the same lesson from two sessions is paraphrased) over the lane itself rather than a new index, gated on two distinct session ids rather than two entries. Contradiction detection is narrow β direct normative negation on a shared anchor β with the real guarantee structural: nothing writes into opinions/*.md, a suspected contradiction parks instead of serving, and the channel is a digest section plus _meta/opinion-base-proposals.json plus the console, applied by a human editing the base and committing it. The inbox lane is per-opinion (as built), and _opinions/ joins dream._EXCLUDE_DIRS β a live hazard, since the directory is in the general corpus today and link_improvement would write **Related:** lines into a served supplement, changing text the agent reads as its standards. Composition regenerates personal/_opinions/<name>.md wholly from the lane each cycle β idempotent and self-healing, and needing no resolver change, since _read_supplement already reads exactly that path. No LLM assist is built: the chain cannot dispatch a sub-agent (orchestration_idle.py:24, the blocker crystallization's distillation hit), and the spec's "MEDIUM confidence" conflated reflect's routing lane with a classifier confidence the binary classifier does not have β the seam is named assist_tier_available(), returning False, matching the two existing seams of that shape. Supplement health lands under the existing memory freshness+experience axis, not the "opinions family" the spec named β FAMILY_WEIGHTS locks eight axes, the same finding shape as the retired "FRIDAY family" reading.
Where Stages 2-3 run: one new dreaming stage, _stage_opinion_supplement(), joining run_dream()'s hand-wired sequence the way every stage since tidying has β deterministic throughout, which is what makes it buildable at all, and inheriting staging, dream_confirm, the revert log, and the anomaly breaker unchanged. That honors Stage 4's "no second mechanism" without a new pipeline. No code ships in this pass; the [PENDING-IMPL] marker narrows from "Stages 1-3 unbuilt" to "Stage 1 shipped, Stages 2-3 designed." A condensed twin lands in the same commit at Opinions design Β§ How the supplement grows. Re-audit trigger: flip the marker once Stages 2-3 build; each locked call carries its own trigger in the section above, and the two calibration numbers (the 0.85 identity threshold, the ~20-entry lane depth) are the first to revisit once real lane volume exists.
2026-07-19 β the remaining automation guards ship; auto-organization part 3 is now fully shipped (task 9 of 9, the last task of PLAN-auto-org-dedup-and-lint.md) β reconciling the re-audit trigger directly below and the trailing ones further down. dream_confirm.check_tidying_anomaly generalizes into dream_confirm.check_stage_anomaly(vault_path, stage, current_count), taking the stage as a parameter and keeping a separate _meta/<stage>-cycle-history.json per stage; check_tidying_anomaly survives as a thin stage="tidying" wrapper, byte-identical to before. dream.run_dream_and_auto_apply() now runs _ANOMALY_WATCHED_STAGES = ("tidying", "suffix_backlog_drain", "lint") through this check before applying anything, excluding any tripped stage from that cycle's auto-apply set and flagging the digest per stage β link_improvement is deliberately left out of this watched-stage tuple, matching the already-documented gap the 2026-07-18 link_improvement entry below names (only its fixed per-cycle cap guards it, no anomaly breaker of its own, unchanged by this task). A new lint._graph_snapshot_cross_check() compares the persisted graph snapshot against a live re-extraction for a capped, rotation-cursor-tracked batch each cycle, flagging drift; it has to run before run_lint()'s own snapshot rebuild, which in turn meant reordering run_dream() so _stage_lint() runs FIRST in the sequence β ahead of _stage_link_improvement(), whose own full-vault rebuild would otherwise erase the very drift the cross-check exists to catch. And a new sampled higher-tier audit (dream_confirm.run_sampled_audit) reviews a capped sample of each cycle's applied link_improvement/inbox_merge items once a higher-tier model verdict is available (dream_confirm.higher_tier_model_available(), always False today β the identical seam shape as dream.cheap_model_tier_available()), narrowing write_time_linker's similarity floor automatically when disagreement crosses 20% and flagging the console (console.section_sampled_audit). Full detail, including the adversarial-review-caught starvation bug in the cross-check's original fixed-slice sampling (fixed with a persisted rotation cursor mirroring the link-backfill's own pattern), lives in AgentM Auto-Organization's own three task-9 as-built notes. With this, all 9 tasks of auto-organization part 3 are shipped, and all 3 parts of the whole AgentM Auto-Organization design are now built β that design's own status flips from final to launched. Re-audit trigger: none outstanding from this pipeline's side; the auto-organization design itself still carries its own calibration-only re-audit triggers (the stepped-schedule/floor/cap defaults, the first backlog-drain cycles).
2026-07-19 β the browse-surface counts land and the meters get a real dashboard home (auto-organization part 3 task 8 of 9, PLAN-auto-org-dedup-and-lint.md). Closes the re-audit trigger the 2026-07-18 connectivity-meter entry below left open. A new dream._browse_surface_counts(vault_path, entries) counts three states β live (not under _shelf), shelved (under _shelf, from this module's own existing corpus scan), and archived (its own separate walk, since _archive is deliberately excluded from every other stage's corpus) β and merges into run_dream()'s corpus_stats right beside _connectivity_meter's two numbers, rendering as a new digest line: "Browse surface: N live, N shelved, N archived." The design's own prose also promised both meters would land "in the health dashboard's FRIDAY family"; that family turned out not to exist. scripts/health/health_score.py locks exactly eight family/axis names, and there is no ninth "FRIDAY" axis. Read as informal plan language for "the health dashboard, in service of FRIDAY-ladder progress," both meters landed under the existing memory freshness+experience axis instead β the same axis verify-dreaming.sh already reports against. Verified end-to-end by a new scripts/verify-auto-org-meters.sh (same jsonl_emit.sh health-signal convention as verify-dreaming.sh, wired into check-all.sh and CI right after it), which seeds a scratch vault with a linked pair, an unlinked note, a shelved artifact, and an archived memory, and confirms β the plan's own acceptance test, verbatim β that the archived note's content stays readable on disk. See AgentM Auto-Organization for the full as-built note. Re-audit trigger: part 3's own task 9 (the remaining automation guards) is still open β reconcile here once it lands, since that's the task that closes out part 3 in full and would flip this design's status from final to launched.
2026-07-18 β inbox triage folds into the weekly dreaming cycle (auto-organization part 3 task 4 of 9, PLAN-auto-org-dedup-and-lint.md). Until this task, /memory inbox --bulk-review was the only door into the inbox-triage engine β the Inbox-triage bullet above (Backward experience) describes exactly that, and it was accurate through today. It no longer waits for that command by default: dream.run_dream_and_auto_apply() β the wrapper dream.py's CLI and the weekly dream.yaml job actually invoke, not run_dream() directly β now also calls inbox_triage.run_inbox_triage_and_auto_apply() as a sibling sub-run alongside dreaming's own run, each with its own run_id, staging directory, and digest, but sharing the one revert_log instance dreaming's own run already opened, so a single cycle's undo surface covers both. run_dream() itself, the propose-only entry point, is untouched by this task β the fold rides the auto-apply wrapper specifically because inbox triage's own engine is what applies, not because dreaming's propose-only contract changed. A new include_inbox_triage: bool = True keyword lets a caller opt out of the fold (today, only the test suite does); the fold itself is best-effort β a triage failure is caught and logged, never allowed to take the rest of a cycle down with it. DreamDigest.inbox_triage_run carries a small summary dict (run_id, proposals, auto_applied, needs_your_eye, digest_path) and renders as one line in dream's own digest header ("Inbox triage (folded into this cycle): ..."), so a cycle's digest names both runs without merging their content together. /memory inbox --bulk-review is unchanged and stays the on-demand door into the identical engine for anyone who wants to run it outside the weekly cadence β verified by a parity test that runs the same fixture both ways and asserts identical dispositions. See AgentM Auto-Organization for the full design and its own "As built (task 4 of 9)" note.
Landed: dream.py gains the include_inbox_triage keyword on run_dream_and_auto_apply() and the new DreamDigest.inbox_triage_run summary field; the Inbox-triage bullet above (Backward experience) updated in the same change to record the fold. Tested by 4 new tests (TriageFoldsIntoDreamingTests β a weekly cycle processes the inbox without an explicit invocation, CLI parity against the weekly fold, run_dream() alone still stays propose-only, the fold-disable kwarg), plus 1 pre-existing dream-job test updated intent-preservingly (a weekly cycle now stages two runs, not one, so the test selects dream's own digest by its header rather than assuming it's alone in the batch).
Re-audit trigger: re-audit here if a future change makes run_dream_and_auto_apply() β rather than run_dream() β no longer the default entry point the CLI and the scheduled job invoke, which would silently un-fold triage from the weekly cadence; or if the fold's best-effort failure isolation is ever tightened to surface a triage failure in the cycle's own digest as an error rather than swallowing it.
2026-07-18 β the connectivity meter lands; auto-organization part 2 is now fully shipped (task 7, the last task of PLAN-auto-org-write-time-linking.md). Not a new dreaming stage β a new pair of numbers computed inside run_dream()'s existing _stage_corpus_stats()/corpus_stats flow. dream._connectivity_meter(loaded) returns organic_connectivity (+ organically_linked_count) and generated_link_count as two independently-computed values: organic connectivity is the share of notes with β₯1 real, non-generated link β a wikilink or supersedes: frontmatter edge surviving after every **Related:** line is stripped out first (the only line the linker itself ever writes), with detection reusing graph.extract_edges on the stripped content so a fenced-code wikilink is excluded the same way the typed-edge graph already excludes it. generated_link_count totals the wikilinks on non-fenced **Related:** lines across the corpus β the linker's own contribution, counted separately so it can never inflate the organic number it's judged against, exactly the design's own stated requirement. Ingest batches' reading-order nav and doc-backlink wikilinks (task 5) count as organic, not generated β they're ingest's own structural links, not the linker's addition; the meter isolates specifically the linker's contribution, not every automated link in the vault. Both numbers merge into the digest header every cycle: "Connectivity: X% organic (N of M notes with β₯1 real, non-generated link) Β· K generated link(s) (counted separately)." Verified by 6 tests in scripts/test_dream.py::ConnectivityMeterTests β pure content parsing, no sqlite-vec dependency, never skips. See AgentM Auto-Organization for the full as-built note, including the honest scope note on what this task did not build (the browse-surface counts and the health-dashboard FRIDAY-family wiring the design's own Meters section also names β neither was in this part's task list). With this, all 7 tasks of auto-organization part 2 ("Notes that link themselves," FRIDAY ladder feature 6) are shipped β see AgentM Auto-Organization's own Document History for the part-level landing entry. Re-audit trigger: part 3 (dedup-plus-lint) extends this same pipeline further β reconcile here as it lands.
2026-07-18 β a sixth dreaming stage, link_improvement, ships (auto-organization part 2 task 4). The Dreaming section gains a new paragraph recording _stage_link_improvement(): for notes changed since the last sweep (tracked by a dedicated cursor, not graph_snapshot's touched_paths β an earlier version reused that signal and a review caught it silently consuming the write-time linker's own touch, so the weekly sweep never saw the very notes most likely to have qualifying neighbors), it proposes both-directions links for clear candidates through a new shared write_time_linker.merge_related_slugs() helper. link_improvement joined AUTO_APPLY_STAGES (now {"compression", "tidying", "link_improvement"}) on the same "reversible via the revert log" basis already established for compression and tidying β the plan's own "Additive only" Locked design call plus the design's already-approved "none of it waits on you" text is the fresh, separate operator ruling AUTO_APPLY_STAGES's docstring requires before a new stage joins it. Two departures from the design's literal wording: the cheap-model band for the ambiguous middle-similarity band was never built (dream.cheap_model_tier_available() always returns False, an explicit tracked seam, not a silent drop β every ambiguous candidate degrades to "left unlinked," the same fallback the design already names for a spent budget or an unavailable tier); and there is no anomaly breaker scoped to this stage yet, only the fixed 25-per-cycle cap. See AgentM Auto-Organization for the full design and its own "As built (task 4)" note. Re-audit trigger: part 3 (dedup-plus-lint) extends this same pipeline further β reconcile here as it lands, and again if a cheap-model primitive or a link-specific anomaly breaker is ever built.
2026-07-18 β a fifth dreaming stage, tidying, ships (auto-organization part 1, FRIDAY ladder feature 5). The Dreaming section gains a new paragraph recording _stage_tidying(): a memory-archive lane (five years without genuine recall, a 4.5-year digest preview first) and an artifact-shelf lane (a year untouched for any entry with no kind: frontmatter, returning automatically once touched again). Both lanes joined AUTO_APPLY_STAGES alongside compression on the same "reversible via the revert log, so it doesn't wait on confirmation" basis the 2026-07-11 ruling above already established for compression β no fresh confirm-gate debate needed, the precedent already covers a move that's always undoable. A new anomaly breaker (dream_confirm.check_tidying_anomaly) is scoped to tidying specifically: a cycle proposing several times a trailing baseline applies nothing from that stage rather than a capped partial batch, flags a digest section, and never lets its own anomalous count poison the baseline for the next cycle's comparison. See AgentM Auto-Organization for the full design; this section names what actually shipped. Re-audit trigger: parts 2 and 3 of that design extend this same pipeline further β reconcile here as each lands.
2026-07-11 β inbox triage's confirm-gate retires: every disposition now auto-applies (Consolidation-arc follow-up, second pass the same day as the landing below). The confirm-gate the landing below shipped was deliberately conservative for exactly one supervised pass, not a standing posture: the operator personally reviewed and confirmed the ENTIRE first-run backlog β 635 staged proposals across 1,565 entries (765 promoted, 255 superseded via merge, 289 expired, 255 left at status: inbox as the surviving canonical copy of each merge cluster with their own disposition still pending) β with zero errors. On the strength of that supervised first pass, the operator ruled: "moving forward no need to confirm gate the inbox." Confirm-gating retires entirely β every disposition (promote/merge/expire) now auto-applies by default, regardless of whether the source entry predates or postdates the cutover stamp.
What changed and what didn't. inbox_triage.py gains ALL_AUTO_APPLY_STAGES β frozenset({inbox_promote, inbox_merge, inbox_expire, inbox_expire_backlog}), all four stage names this module has ever proposed; run_inbox_triage_and_auto_apply() now passes this full set to dream_confirm.auto_apply_batch(stages=...) in place of the single {inbox_expire} set the original landing used β dream_confirm.py itself is completely untouched (its auto_apply_batch(stages=...) parameter already accepted an arbitrary stage set; this module just widens the one it passes). _is_pre_existing_backlog() and the cutover marker are unchanged as CODE β same signature, same classification logic β but their ROLE changed: they no longer gate whether a proposal auto-applies, only which era-label stage name (inbox_expire vs inbox_expire_backlog) and digest summary text an expire proposal gets. Kept as informational metadata rather than deleted outright, since a human reading a digest still benefits from knowing which entries predate the mechanism, even though nothing gates on it anymore. This also correctly resolves the 255 merge-cluster survivors left at status: inbox from the first supervised pass β entries with no created field that would otherwise have stayed confirm-gated forever under the retired rule β they now triage and auto-apply normally on a subsequent run, exactly like the rest of the backlog. RevertLog/record_and_apply is completely unchanged: every auto-applied mutation is still fully undoable via RevertLog(vault_path).revert(run_id, entry_id). The manual CLI paths β --confirm / --reject / --list and the interactive --bulk-review review loop β are unchanged and still available for anyone who wants to inspect or intervene on a specific proposal by hand; --no-auto-apply still exists as an explicit propose-only opt-out for that same purpose.
Landed: inbox_triage.py's stage vocabulary + auto-apply wiring (module docstring, _is_pre_existing_backlog's docstring, _build_expire_proposal's summary text, the digest-rendering text, and the CLI's argparse description / --no-auto-apply help text all reconciled to describe the new default rather than the retired confirm-gate); SKILL.md's /memory inbox section rewritten to match; this design's own Inbox triage bullet above updated in the same change. Tested by an expanded scripts/test_inbox_triage.py (23 tests total, up from 17): a pre-existing-backlog-shaped entry (no created field) with a promote-eligible or merge-eligible disposition now auto-applies without confirmation (BacklogPromoteAndMergeAutoApplyTests); the 255-survivor scenario specifically β a no-created entry left at status: inbox by a prior run β triages and auto-applies normally on a later run instead of staying stuck (SurvivorRetriageTests); the manual --list/--confirm/--reject CLI paths still round-trip correctly end-to-end (ManualCliPathsTests); --no-auto-apply still proposes without applying anything (NoAutoApplyStillProposesOnlyTests). The two test classes that asserted the now-retired confirm-gated-first-pass contract (FirstPassNeverAutoAppliesTests, and the backlog-never-auto-applies half of PostCutoverAutoApplyTests) were rewritten against the new contract rather than deleted, per this repo's testing convention β they still assert the same underlying property (a fresh mechanism's pass over untouched content is provably safe and fully revertible), just against the new "auto-applies, fully revertible" shape instead of "stays pending forever."
Re-audit trigger: revisit only on a fresh, explicit operator ruling if auto-apply itself should ever be scoped back for this mechanism (e.g. a new backlog-scale problem surfaces in a different corpus) β this ruling is specific to inbox triage's own now-proven-safe supervised first pass, and does NOT extend to dreaming's own dedup / contradiction_triage stages, which still require their own fresh ruling per AUTO_APPLY_STAGES's own docstring (see the compression amendment below), untouched by this change.
2026-07-11 β inbox triage lands: the long-promised _inbox/ bulk-review pass (Consolidation-arc follow-up, same day as the compression amendment below). personal/_inbox/ held 1,565 notes (55% of the whole vault) at review time, of which 1,563 still sat at status: inbox β reflection (reflect.py's _save_candidate_to_inbox) had been routing MEDIUM/LOW-confidence candidates here since it shipped, and nothing had ever moved one out. A bulk-review command was named as required follow-up as far back as the original MemoryVault design docs (wiki/designs/memoryvault/parts/reflection-and-recovery.md, discovery-mining.md) and repeated in this skill's own SKILL.md and a code comment in reflect.py itself β never built, until harness/skills/memory/scripts/inbox_triage.py and /memory inbox --bulk-review.
The three dispositions. Every still-untriaged inbox entry gets proposed exactly one of: promote (stage="inbox_promote") β reinforced by a high mining_occurrences count, or a similarity cluster of 3+ entries capturing the same content independently β graduates to <vault>/personal/<kind>/<slug>.md, reusing save._build_frontmatter (the exact convention reflect.py's own HIGH-confidence path already writes canonical entries with) rather than a second frontmatter shape. merge (stage="inbox_merge") β an ordinary near-duplicate pair β reuses dream._stage_dedup verbatim against the inbox pool: same difflib.SequenceMatcher comparison, same DEDUP_SIMILARITY_THRESHOLD (0.92), same merge-into-earlier / mark-later-superseded mutation shape dreaming's general corpus already uses. expire (stage="inbox_expire" post-cutover / "inbox_expire_backlog" pre-existing) β stale and unreinforced past a TTL (default 90 days since the entry's own created timestamp) β patches status: expired in place, never a physical delete or move, mirroring compression's own "retire it, never delete a source" convention. All three stage through the identical _dream-staging/<run_id>/ manifest schema dream_confirm.py already reads β list_pending / confirm / expire_stale / auto_apply_batch / render_auto_applied_json all run against an inbox-triage run completely unmodified; nothing in dream_confirm.py changed for this to work.
The cutover-marker rule β the one piece the operator specifically ruled on. Dreaming's general corpus never had a real backlog problem (366 entries, only 2 with a supersedes: chain, at the time compression's auto-apply landed); the inbox did, at scale, for the first time. The operator's ruling: every disposition proposed against the PRE-EXISTING backlog β every entry that existed before this mechanism's first-ever run β stays confirm-gated, full stop, regardless of kind; an irreversible-feeling first pass over untouched real content deserves a human read once, but ongoing operation doesn't need to re-litigate that every cycle. Only entries CREATED AFTER the mechanism first ran are eligible for the same auto-apply-on-expire treatment AUTO_APPLY_STAGES already grants compression β and even then, only for expire; promote and merge stay confirm-gated regardless of entry age, matching the same "promote/link stay gated" call the compression amendment below made for the general corpus.
The boundary is _meta/inbox-triage-cutover.json β ensure_cutover_marker() stamps an ISO-8601 UTC timestamp the FIRST time it's ever called against a vault, and never overwrites it again on any later call. reflect.py's _save_candidate_to_inbox gained a created frontmatter field in the same change (the pre-existing backlog never carried one β inbox entries previously had no per-entry timestamp at all): an entry's classification is pre-existing backlog if it has no created field, or created is at or before the cutover stamp; strictly after the stamp is the only way to be eligible for auto-apply. On this mechanism's actual first run, that means zero entries can possibly classify as post-cutover β every currently-existing entry necessarily predates (or exactly matches) the stamp being set β so the first pass provably never auto-applies anything, by construction, not by a separate check. Auto-apply itself reuses dream_confirm.auto_apply_batch() directly (never modifying the shared AUTO_APPLY_STAGES frozenset dream.py's own runs default to): inbox-triage passes its own stages={"inbox_expire"} explicitly per call, so only proposals staged under that one name ever auto-apply β "inbox_promote", "inbox_merge", and "inbox_expire_backlog" are simply never in that set, on any run, at any entry age.
The CLI. /memory inbox --bulk-review scans (+ auto-applies eligible expire proposals), then interactively walks every remaining pending proposal β [c]onfirm / [r]eject / [s]kip, non-TTY stdin defaulting every prompt to skip (the same never-silent-action contract watchlist_review.py already established, whose interaction shape this reuses as closely as the different disposition set allows). --list / --confirm <index> / --reject <index> give scriptable, non-interactive access to the same run. Reject is new relative to watchlist's promote/dismiss/defer verbs β it doesn't exist anywhere in dream_confirm.py's state machine (which only ever tracks pending/confirmed/expired), so it's implemented as a direct frontmatter patch (status: triage_rejected) on every path the proposal touches, outside the revert-log, matching how watchlist_review.py's own promote/dismiss/defer already annotate directly rather than through a journal. This also closes a real correctness gap: a fresh scan only ever considers status: inbox entries, so a rejected (or confirmed, or previously auto-applied) entry is never re-proposed, and the review loop double-checks each pending proposal's paths against their LIVE on-disk status before offering it, so a proposal made moot by a different, overlapping proposal from an earlier run is silently dropped rather than re-prompted or erroring.
Landed: inbox_triage.py (new), reflect.py gains _utcnow_iso() + a created field in _save_candidate_to_inbox's frontmatter (additive β no existing frontmatter field removed or reordered), SKILL.md gains the /memory inbox section + table row. Tested by 17 passing tests in scripts/test_inbox_triage.py: cutover classification (no-created / at-cutover / after-cutover), the first-pass-never-auto-applies guarantee against a mixed backlog fixture, post-cutover expire auto-applying while sibling promote/merge proposals on OTHER post-cutover entries stay pending, promote's byte-level frontmatter match against save._build_frontmatter's own shape (plus a collision-skip test), merge's mutation proven byte-identical to a direct dream._stage_dedup() call against the same fixture, and the full confirmβcanonical-writeβrevert round trip plus the rejectβno-re-proposal round trip.
Re-audit trigger: revisit the 90-day expire TTL and the 3-occurrence promote threshold once real bulk-review cycles run against the live 1,565-note backlog and report back β both are calibration-era judgment calls (documented in inbox_triage.py's own module constants), not locked design numbers, chosen for consistency with existing precedent (DEFAULT_TTL_DAYS's own 30-day default; COMPRESSION_CHAIN_MIN_LENGTH's 3) rather than measured against inbox-specific data. Revisit the cutover-marker rule itself only on a fresh, explicit operator ruling β same standing the compression amendment's own AUTO_APPLY_STAGES docstring already sets for extending auto-apply to a new stage.
2026-07-11 β expire (compression) switches from confirm-gated to automatic; promote (dedup) and link (contradiction-triage) are deliberately unchanged (Consolidation-arc follow-up). Dreaming's confirm/expire cycle (dream_confirm.py, landed 2026-07-07 below) required an explicit operator confirm() call before ANY staged proposal applied, regardless of stage. The operator ruled this loosens for exactly one of the pipeline's three actions: compression (stage="compression", kind="compress" β supersession-chain compaction, the action that retires already-superseded notes into a single compacted head without ever deleting a source) now applies automatically, no per-batch confirm required. Dedup (stage="dedup", merge β a freshly-inferred similarity match) and contradiction-triage (stage="contradiction_triage", keep-both β flags a relationship for the operator to resolve) are explicitly, deliberately untouched: both still require the exact same dream_confirm.confirm() call they always have. Why compression and not the other two: it is the one stage that acts on a relationship the vault ALREADY declared (an author- or prior-cycle-set supersedes: chain), never a relationship this cycle infers fresh from a similarity heuristic β so it carries materially less judgment risk than dedup's fuzzy match or contradiction-triage's flagged-but-unresolved ambiguity, and it is exactly what "retire it, reversible via the undo/revert log" describes. Every auto-applied mutation still runs through the identical revert_log.record_and_apply path a manual confirm uses β reversibility is unchanged, only the gate in front of it is gone for this one action.
Landed: dream_confirm.py gains AUTO_APPLY_STAGES (frozenset({"compression"})), DEFAULT_AUTO_APPLY_BATCH_CAP (25), the AutoAppliedBatch record, auto_apply_batch() (sweeps every pending proposal whose stage is in AUTO_APPLY_STAGES, oldest-index-first, applying up to batch_cap per call through the same confirm() a human uses β so an auto-applied item is provably indistinguishable, in the revert log, from a manually-confirmed one), and render_auto_applied_json() (the machine-readable batch record). dream.py gains run_dream_and_auto_apply() β calls the unchanged run_dream(), then auto_apply_batch(), then re-renders digest.md with a new "Auto-expired this run" section naming exactly what applied and its revert pointer; dream.py's CLI (main()) now calls this by default (--no-auto-apply opts back into the old propose-only behavior; --batch-cap overrides the bound; --log-root/--lock-root exist purely so tests never touch the real ~/.cache). Two new artifacts record the batch: the per-run _dream-staging/<run_id>/auto-expired.json (sibling to the existing digest.md/proposals.json/state.json) and the stable, run-id-free _meta/dream-auto-expired-latest.json β overwritten every cycle, including a zero-item one, so a reader (the observability console's own dreaming surface, when it lands, or an operator by hand) always finds a current record without enumerating _dream-staging/*/. Schema: {run_id, applied_at, stages, batch_cap, count, items: [{index, stage, kind, paths, summary, entry_id}], revert: {how, run_id}}. templates/jobs/dream.yaml's command: now passes --batch-cap 25 explicitly.
Cadence + batch-bound review (same follow-up). The Consolidation arc's proving window ran this pass on an alternate-day cadence with a temporary β€100-note batch cap for fast calibration feedback β explicitly a proving-window artifact, never a decided standing config. Reviewed against the real vault: _inbox/ (upstream of this pipeline's corpus) holds ~1,565 notes, growing at a noisy ~13/day baseline (one 585-note bulk-mining spike aside, itself a one-time backfill artifact, not organic growth) with a recent elevated ~66/day rate during this active build period; the corpus dream.py itself scans (the whole vault minus _inbox/_meta/_harness/_dream-staging/_archive/_dream/.obsidian, per its own _EXCLUDE_DIRS) held 366 entries at review time, only 2 of which carried a supersedes: link β nowhere near the β₯3-chain floor compression requires, so real auto-apply volume is ~0/cycle today. Standing decision: keep schedule: weekly / lookback: 14d unchanged (no organic pressure justifies anything tighter, and a slower cadence is fine now that the low-risk stage no longer waits on an operator's batch review), and set the standing batch cap at 25 (generous headroom over today's ~0/cycle real volume, while bounding worst-case blast radius to well under 10% of the 366-entry corpus β the proving window's β€100 would have been over a quarter of it). Re-audit trigger: revisit both numbers once real supersession-chain volume is observed over several live cycles (the proving-window data predates dry_run ever flipping to false, so there is no live-cycle evidence yet) β if the batch cap is regularly hit, raise it; if compression volume stays near zero for months, weekly may be safe to loosen further.
Re-audit trigger (this amendment): revisit if a future operator ruling extends auto-apply to dedup or contradiction-triage (requires its own fresh ruling, not inferred from this one β see AUTO_APPLY_STAGES's own docstring); flip this note once dry_run is promoted to false on a real runner and the auto-apply path has run live cycles to report on.
2026-07-07 β the Experience β Opinions accumulate loop spec landed, design amendment only (PLAN-wave-e-experience task 3, SPEC-FIRST). Lands ACCUMULATE-LOOP-SPEC-DRAFT.md (vault _harness/designs/architecture-governance/, Fable-authored, rendered 2026-07-06) as-is β the operator explicitly approved a go/no-go on the draft's contract before this landing (recorded approval, not inferred silence-as-consent, per the plan's own verification requirement). New section "The Experience β Opinions accumulate loop" added above: Stage 1 routes standard-shaped signals into the existing capture paths (reflection / edit-driven capture / watchlist) rather than a new pipeline, deterministic-first classification with LLM assist only at MEDIUM confidence. Stage 2 maps concrete signal sources to opinions (/review corrections β good, gate/battery misses β done, token-audit findings β efficient, process/retro lessons β how-we-engineer, recoverability incidents β recoverable, PII findings β private; voice/prose lessons explicitly excluded β they stay in the style overlay, double-capture forbidden). Stage 3 is three anti-corruption guards: a recurrence gate (two distinct sessions before auto-append, single occurrences park in an inbox lane), extend-never-override (a base-contradicting supplement surfaces as a proposed base change, never serves silently), and provenance-or-it-didn't-happen (every entry carries session/commit/incident refs + a supersedes chain, a dashboard supplement-health check). Stage 4 names dreaming's whole-corpus pass (this same wave's sibling plan, PLAN-wave-e-dreaming, shipped 2026-07-07 as PR #244) as the sole maintenance owner β no second mechanism. Landed verbatim β no redesign; the draft's rejected alternatives (a dedicated learning pipeline, LLM-judged auto-promotion, supplement-overrides-base) and its three re-audit triggers carried forward unchanged. No code ships in this task β Stages 1β3's routing rule, signal-map, and anti-corruption guards are all still [PENDING-IMPL], explicitly deferred to a follow-on plan per the draft's own "design amendment before code" framing; this section is that plan's contract to build against. A condensed twin lands in the same commit at Opinions design Β§ How the supplement grows. Re-audit trigger: once the follow-on implementation plan lands, flip this section's [PENDING-IMPL]; separately, once dreaming's consolidation runs live, confirm it actually sweeps opinion supplements (else schedule a lint) per the carried-forward re-audit trigger above.
2026-07-07 β crystallization's digest schema + write/read primitives landed, operator-invoked, no automatic trigger (PLAN-wave-e-experience task 2). harness/skills/memory/scripts/crystallize.py ships the five-field digest schema (question Β· investigation Β· findings Β· lessons Β· open threads) and its primitives: crystallize_exploration(vault_path, slug, digest, *, group="personal", tags=None) -> Path writes a kind: crystallized memory entry by calling the existing save.py's save_entry() β no new store, reused exactly like every other entry kind β and parse_digest(entry_path) -> CrystallizationDigest reads one back, raising MalformedDigestError if any of the five locked ## <Title> sections is missing. Entries land at <vault>/<group>/crystallized/<slug>.md, the as-built vault/group/kind/slug.md convention save.py already uses today β not the Memory/<kind>/<slug>.md three-tier layout agentm-memory-system.md sketches as designed-for-but-not-yet-migrated (confirmed: no current script writes to a literal Memory/ subfolder). The digest schema is shared forward with PLAN-wave-e-v6-index task 7's consolidation work (a sibling, concurrently-running plan on a different worktree); confirmed at task start that lane hadn't reached task 7 yet, so this authors the schema fresh rather than duplicating it. Two things this task deliberately did not build, both real gaps: the automatic phase-close trigger the existing [PENDING-IMPL] note named as a co-requirement β "the close of a completed exploration" is not a bounded, detectable event anywhere in this codebase (a dedicated research pass grepped wiki/, harness/, AGENTS.md; the only prior mention of "exploration" anywhere was this file's own Crystallization bullet), so wiring an automatic trigger off any phase boundary or session-lifecycle hook is deferred as a v2 concern needing coordination with the crickets development-lifecycle phase loop or a new session-boundary marker β matching the same thin-manual-path-first precedent this wave already set twice (dreaming's /dream before a scheduled trigger; forward learning's one deterministic pass before a semantic judge); and distillation β crystallize_exploration() takes an already-composed CrystallizationDigest (five plain strings), it does not read or mine a raw transcript itself, so whatever actually produces the five field values (an operator composing them by hand today, a future LLM-assisted pass later) stays out of this module's scope. Tested by 6 passing tests in scripts/test_crystallize.py: the plan's own red-test (crystallizing a fixture exploration round-trips through parse_digest to the exact five-field schema), a test proving no file other than the one crystallized entry (plus the harmless pre-existing async embedding-queue sidecar every save_entry call already appends to) is created, a malformed-entry test, a path-convention test, a section-headers-present test, and a collision test proving the existing FileExistsError-on-duplicate-slug contract is inherited from save_entry, not reimplemented. This flips Crystallization's [PENDING-IMPL] marker from "trigger + schema both missing" to a precise "designed and built, schema + primitive only, no automatic trigger" note β narrower than a bare as-built, since the trigger piece the original note named is still genuinely unbuilt. Re-audit trigger: revisit this note if a phase-close trigger is designed and wired, or if a distillation pass (raw transcript β five fields) is built on top of this primitive.
2026-07-07 β forward learning's approved-source pipeline landed, thin v1, opt-in (PLAN-wave-e-experience task 1). harness/skills/memory/scripts/forward_learning.py generalizes the import watchlist's find β screen β surface shape beyond skills to ideas, patterns, and references from operator-configured approved sources (feeds, named repos, the web). Sources live in _meta/forward-learning-sources.json, absent by default β no config, no scan output, opt-in by design. Unlike the import watchlist's two-pass split (deterministic rubric, then an adapt-evaluator semantic judge sub-agent), this v1 does find + screen + classify in one deterministic pass: a rubric reusing adapt_skills.py's score thresholds (β₯3 HIGH, β₯1 MEDIUM, else LOW) renders the verdict directly, with no semantic judge dispatch. A Pass-2 judge mirroring adapt-evaluator is a natural v2 graduation, not built here β matching the sibling dreaming plan's own "thin manual pass first, calibrate, then graduate" precedent. MEDIUM/HIGH candidates land in a new, generalized personal/_watchlist/ (sibling to personal/_skill-watchlist/); LOW is dropped, never written; per-source watermarks live in _meta/forward-learning-cache/. Nothing is ever auto-adopted outside those two locations. harness/skills/memory/scripts/watchlist_review.py was generalized in the same task (not replaced) so the existing operator CLI (list/review/promote/dismiss/defer) now scans both watchlist roots as one merged surface; this also fixed dismiss_entry, previously hardcoded to archive into the skill-watchlist archive regardless of an entry's actual source, to derive the correct archive root from the entry's own path so the two pools never cross-mix. templates/jobs/forward-learning.yaml registers the pipeline on the Runner as schedule: daily, dry_run: true, opt-in β no budget:, since the thin v1 pass makes zero LLM calls β following the same templates/ β gitignored .harness/ shipping precedent templates/jobs/dream.yaml established; this is the second job manifest any capability has shipped. Tested by 20 passing tests across three files: scripts/test_forward_learning.py (8, including the zero-auto-adoption red-test β every changed path is under _watchlist/ or the forward-learning cache, nothing else), scripts/test_watchlist_review.py (11, new coverage for a previously-untested module β merged-root listing, the per-root archive fix, promote/defer working regardless of source root), scripts/test_forward_learning_job.py (1, manifest parses and stays dry_run: true). This flips Forward Experience's [PENDING-IMPL] marker to a precise "designed and built, thin v1, opt-in" note rather than a bare as-built: no sources are configured out of the box, and screening is one rubric rather than the two-pass deterministic-then-semantic shape the design's own import-watchlist precedent uses. The plan's scorecard section deliberately does not flip a dark-checks.jsonl row for this landing β no forward-learning or crystallization row existed before this task, so there is nothing to graduate from dark to live (unlike the sibling dreaming plan's pre-registered row); whether a dark-checks entry should now be registered for forward learning is an open question this note flags rather than resolves. Re-audit trigger: revisit this note if a Pass-2 semantic judge is built, or once real approved sources are configured and calibration evidence justifies flipping the job manifest's dry_run to false.
2026-07-07 β scheduled runner pass landed, dreaming's four-task plan now complete (PLAN-wave-e-dreaming task 4, LAST task). templates/jobs/dream.yaml registers the whole-corpus pass as a job manifest on the Runner: command: python3 ../harness/skills/memory/scripts/dream.py wires task 2's CLI, schedule: weekly + lookback: 14d stand in for the design's "activity-gated" framing (the runner's gate: field is parsed by scripts/runner/manifest.py but not yet evaluated by scripts/runner/cycle.py, as-built), tier: T3 reflects that insight-candidate writes are new agent-generated content rather than operator-curated T2, and there's deliberately no budget: β the thin v1 pipeline makes zero LLM calls. It ships dry_run: true, per the plan's own Constraints: promoting to live-apply is a separate, explicit future decision gated on task 2's calibration evidence being recorded as sufficient, never an automatic consequence of the manifest existing. The plan's task text names the deliverable path as .harness/jobs/dream.yaml, but .harness/ is entirely gitignored in this repo (git ls-files .harness/ returns nothing), so nothing at that literal path can ever be tracked; the manifest instead ships as a tracked template at templates/jobs/dream.yaml, following the exact existing precedent for .harness/hooks/* and .harness/verify.sh (install.sh copies from templates/ at install time). This session deliberately did not add automatic copying to install.sh β wiring a real --runner/jobs install flag (opt-in vs default-on) is a separate, un-scoped decision task 4's text didn't ask for; a repo wanting the job active copies the template to .harness/jobs/dream.yaml by hand. This is also the first job manifest any capability has shipped β forward-learning and health-check have none yet. Tested by scripts/test_dream_job.py (3 passing tests): the manifest parses against manifest.py's schema; a due cycle against the shipped (dry_run: true) template reports ran=False, dry_run=True and never creates _dream-staging/ at all (not just "ran quietly"); and a cycle with dry_run overridden to False for the test only (the shipped template itself stays dry_run: true) produces a digest with the same structural shape as task 2's direct dream.run_dream() call, proving the wiring. With this, all four of the plan's tasks are built and tested β the revert-log safety net, the manual /dream pipeline, the confirm/expire flow, and now the scheduled job. Dreaming's [PENDING-IMPL] marker is retired in favor of a precise "designed and built, shipped dry-run / opt-in" note: the pipeline is real and fully wired end-to-end, but a repo must deliberately copy the manifest into .harness/jobs/ (no install.sh seeding yet) and even then nothing auto-applies until an operator promotes dry_run to false. Re-audit trigger: revisit this note once install.sh gains a jobs-seeding step, or once a repo's calibration evidence is recorded as sufficient to flip dry_run to false on a real runner.
2026-07-07 β dreaming's confirm/expire flow landed, manual pipeline now end-to-end (PLAN-wave-e-dreaming task 3). harness/skills/memory/scripts/dream_confirm.py ships the confirm/expire flow that closes the loop task 2 left open: list_pending(vault_path, run_id, *, ttl_days=30.0, now=None), confirm(vault_path, run_id, index, revert_log, *, ttl_days=30.0, now=None) -> entry_id, expire_stale(vault_path, run_id, *, ttl_days=30.0, now=None) -> list[int]. confirm() is the only path a staged proposal ever applies through β it calls revert_log.RevertLog.record_and_apply(run_id, stage, mutations), never a direct write to a source entry. An unconfirmed proposal has a TTL (default 30 days β a documented calibration-era judgment call, not a number specified anywhere in the design); past TTL, confirm() raises ExpiredProposalError and applies nothing, while expire_stale() is the separate bulk pass that marks stale proposals expired without attempting to apply them. Confirmation state (pending/confirmed/expired per proposal index) lives in a new sidecar, _dream-staging/<run_id>/state.json, alongside the staging directory β not in the revert-log journal, which only records dispositions actually applied. This task also extended task 2's dream.py with a machine-readable manifest (_dream-staging/<run_id>/proposals.json) written alongside the existing digest.md/.proposal.md files, since confirm needs exact structured mutation data that the markdown proposal files (content embedded in prose code-fences) aren't reliably re-parseable from; this didn't change any of task 2's existing behavior β scripts/test_dream.py's 12 tests still pass unmodified. Tested by scripts/test_dream_confirm.py (8 passing tests), including the plan's two red-tests: confirming applies the mutation and the result is provably undoable via the same RevertLog instance's revert() (proving the apply went through the journal, not around it), and an expired proposal's confirm() raises with source files staying byte-identical to pre-confirm (proving no silent apply on timeout). With this, the manual /dream path (stage β confirm β apply-through-revert-log) is fully wired and fully revertible β the design's Objective's "run /dream by hand" half is complete. The scheduled, unattended half (task 4) is still unbuilt; Dreaming's [PENDING-IMPL] marker stays until that lands. Re-audit trigger: flip Dreaming's [PENDING-IMPL] to as-built once task 4 ships.
2026-07-07 β thin manual /dream landed (PLAN-wave-e-dreaming task 2). harness/skills/memory/scripts/dream.py ships a one-shot, operator-invoked run of the dream pipeline (run_dream(vault_path, *, run_id=None) -> DreamDigest): corpus stats β dedup β contradiction triage β compression β crystallization β insight-generation β qualification β digest+staging, tested by scripts/test_dream.py (12 passing tests). Dedup, contradiction-triage, and compression only propose dispositions β a Proposal dataclass listing stage/kind/paths/summary/mutations β and never write to an existing entry; every proposal lands in a staged digest (_dream-staging/<run_id>/digest.md) with a prospective revert pointer for a later confirm step (task 3, not yet built) to apply through task 1's RevertLog. Insight-generation is the sole exception, writing new status: candidate files directly to _dream/insights/<run_id>.md β additive, non-source-touching, so it doesn't need staging per the design's own carve-out. This is a deliberately thin, calibration-era pass: dedup uses difflib text-similarity (not embeddings), contradiction triage is a same-slug-differing-body check with zero auto-resolution, compression only compacts supersedes:-chains of length β₯ 3 and never deletes a superseded file, and qualification defaults every insight's rung to retrieval. Both dreaming prerequisites now exist (the revert-log primitive and the manual pipeline); what remains before Dreaming can flip to as-built is the confirm step that wires /dream's proposals through revert_log (task 3) and the scheduled runner pass (task 4). Re-audit trigger: flip Dreaming's [PENDING-IMPL] to as-built once tasks 3 and 4 ship.
2026-07-07 β dreaming's prerequisite landed: the revert-log primitive (PLAN-wave-e-dreaming task 1). harness/skills/memory/scripts/revert_log.py ships an append-only, per-run undo journal for the dreaming pipeline's content-touching writes (dedup/merge/supersede/compress), tested by scripts/test_revert_log.py (11 passing tests). Public API: RevertLog(vault_path, *, log_root=None, lock_root=None, timeout=10.0, stale=10.0), RevertLog.record_and_apply(run_id, stage, mutations) -> entry_id, RevertLog.revert(run_id, entry_id=None). The journal lives on a local, non-synced path (~/.cache/agentm/dream/revert-log/<run_id>.jsonl, XDG_CACHE_HOME-honoring) β never in the synced vault β and reuses the existing vault_lock.py primitives rather than inventing new locking. This is only the prerequisite: the Dreaming section's [PENDING-IMPL] marker stays as-is, since /dream and the scheduled consolidation pass are still unbuilt. Re-audit trigger: flip Dreaming's [PENDING-IMPL] to as-built once /dream and the scheduled pass ship on top of this primitive.
2026-07-06 β the runner's own [PENDING-IMPL] flipped to as-built (see the Runner design). Reconciled this pillar's prose to match: removed "runner" from the shared "designed, not built" framing (the LAUNCHED note, the diagram caption, the runner bullet, Risks, the re-audit triggers) since it no longer applies β forward learning and dreaming remain designed, not built, on their own merits, not because the runner they'd schedule on is missing. This is the re-audit the 2026-06-28 entry below named.
2026-06-28 β lock-down sweep (operator review). Sized the diagram (width/height); confirmed the content (two-directional learning Β· the built-vs-designed split Β· the growth-engine framing) and the newest-first log. Locked as a v5βv8 guidepost. (The deferred memoryvault split will later fold its reflection-sidecar, idea-ledger, crash-recovery-marker, and internet-skill-discovery units in here β see MEMORYVAULT-SPLIT-MAP.)
2026-06-28 β scheduler β runner rename (Bucket-B / critique W1). Renamed the agentm background-job primitive scheduler β runner through the current-state prose β the LAUNCHED note, the diagram caption, the cron bullet, Risks, and the re-audit triggers β and pointed it at the new Runner design, which now owns it; the host's scheduler stays the cron that fires it. The historical 2026-06-21 entry keeps its original wording. Re-audit trigger: flip the runner's [PENDING-IMPL] to as-built when it ships.
2026-06-21 β authored, reviewed, and finalized.
Migrated from the agentm HLD and conformed to the abbreviated-design template. Documents the Experience pillar β how agentm gets better over time β as a lifecycle in two directions: backward (reflection, kind-classified, tri-modal routing under MEMORY_REVIEW_MODE; heat-based curation β both built) and forward (scheduled, opt-in approved-source + deep-research learning, surfaced never adopted silently β designed; only the import watchlist ships), plus dreaming (whole-corpus consolidation, gated on a revert-log) and crystallization (phase-close distillation) β both designed. What's learned lands in three places: the general kind-classified memory, the idea incubator/watchlist, and the Opinion supplement.
Operator-reviewed across several rounds and grounded against the live code before finalization (which corrected a misnamed routing knob to MEMORY_REVIEW_MODE and confirmed every built-vs-designed call). Named Experience as the growth engine that grows the Memory System into an interconnected knowledge base. Designed-for / [PENDING-IMPL]: the approved-source pipeline, the scheduler, dreaming + its revert-log prerequisite, and crystallization. Re-audit triggers: flip each to as-built as it ships; specify the Experience β Opinions accumulate loop when forward learning lands.
| 2026-09-06 | The inbox triage engine retires (v9.20.0, PR #575). It walked memory/_inbox/, which filing v2 removed and nothing writes β reflect.py's own comment calls the directory retired and inbox_digest.py has written to diagnostics/digests/ since its F2 fix β so promote / merge / expire had proposed nothing since. Each of the three has an owner now: the heat policy, the dreaming binary's copies job, and the lifecycle axis. The fold into the weekly cycle goes with it, and so does the needs-your-eye surface: inbox_triage.py was the sole writer of _meta/needs-your-eye.json, and its three readers (console section, morning-brief count, digest line) had reported dark or zero since filing v2 β a reader whose only writer is gone is not made honest by saying so. Why not re-point the engine at the live review queue: it would aim an auto-applying expire stage at memories that have never been triaged and put a second TTL beside the lifecycle axis. What is lost, recorded rather than absorbed: nothing now parks a fuzzy-similar, fingerprint-distinct pair for the operator. Re-audit if that case starts costing something. |