The second brain now feeds generation: strategy-advisor reads brain/profile.md as evidence-to-test (anti-sycophancy default, graceful absence on fresh installs). First end-to-end proof of capture → consolidate → read-back-into-generation. - agents/strategy-advisor.md: brain/profile.md added to Step 0 Load Context + a consumption subsection (Static/Dynamic layers, evidence_count/last_seen weighting, anti-sycophancy counter-pressure, silent degrade when the file is absent). - scripts/test-runner.sh: new Section 16d (Brain Profile Reader) — 2 UNCONDITIONAL checks (non-vacuity self-test with legacy-path/lowercase decoy + exact-literal wiring grep on strategy-advisor.md), ASSERT_BASELINE_FLOOR 78→80, header enumeration extended. Gate 93→95/0/0; brain floor 82 untouched. - docs/second-brain/consolidation-loop.md: reconciled the stale "no reader yet" honest-limit to the true partial state (one reader; broader consumption deferred). TDD: RED (only the wiring grep failed, exit 1) → GREEN (95/0/0). READ-only — the brain consolidate --apply --confirm gate stays the sole writer. SC4 (graceful absence) verified by inspection; SC5 (read-back) deferred to a reloaded session (honest — agent prompts load at session start). S3a scope held: one reader, no parser, no id-threading. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01RigJBiRFNtFZKCz21qNbQ4
76 lines
4.5 KiB
Markdown
76 lines
4.5 KiB
Markdown
# Consolidation loop — the compounding mechanism (SB-S2)
|
|
|
|
> How the second brain turns the published gold signal into an ever-improving, drift-resistant
|
|
> `brain/profile.md` — operator-invoked, operator-gated, deterministic. Part of the second-brain arc
|
|
> (`architecture.md`); landed in SB-S2.
|
|
|
|
## The shape
|
|
|
|
The loop is **operator-invoked** ("sleep-time" = when you run it), not automatic — the session-start
|
|
hook is zero-dep and cannot run AI, so it only **nudges** when consolidation is due. The pass itself is:
|
|
|
|
```
|
|
brain consolidate --gather # 1. dump new published deltas + the current profile
|
|
→ (the session reads them and extracts a Candidate[] JSON)
|
|
brain consolidate --propose --candidates cand.json # 2. deterministic diff → brain/pending-diff.{md,json}
|
|
→ (you review brain/pending-diff.md — the [OPERATØR] gate)
|
|
brain consolidate --apply --diff brain/pending-diff.json --confirm # 3. the ONLY write to profile.md
|
|
```
|
|
|
|
`--apply` records `brain/consolidation-state.json` `{last_run}`; the session-start nudge reads it +
|
|
counts `ingest/published/*.md` to know when to nudge again. Roll back any apply via git.
|
|
|
|
## The deterministic engine (`scripts/brain/src/consolidate.ts`)
|
|
|
|
`proposeDiff({current, candidates, today, opts})` classifies each candidate (matched to existing facts
|
|
by the candidate's `key`):
|
|
|
|
| Rule | Condition | Effect |
|
|
|------|-----------|--------|
|
|
| **reject** | `provenance: ai-draft` | dropped — never learns from the engine's own drafts (model-collapse guard) |
|
|
| **add** | no matching fact, provenance `published`/`human` | new dynamic fact, `evidence_count: 1` |
|
|
| **evidence-bump** | matching fact, same value | `evidence_count++`, `last_seen = today` |
|
|
| **promote** | a dynamic fact reaches `N = 3` observations | dynamic → static |
|
|
| **conflict** | matching key, different value | **keep both**, timestamped, with DISTINCT ids; the old fact is untouched (**no supersede** in S2 — that's S3) |
|
|
| **decay-flag** | a dynamic fact's `last_seen` > `90` days | listed in `staleFlags` (informational; never auto-removed) |
|
|
|
|
**Id model (no duplicate ids):** a concept's primary fact id is `mintEntityId({kind:'observed', key})`;
|
|
a conflict alt fact id is `mintContentId('observed-alt:'+key+'::'+value+'::'+date)` — byte-distinct, so
|
|
two facts never share an id. The SB-S0 folded `profile-field` static seeds use a different kind, so
|
|
consolidation never mutates them (immutable in S2). `applyDiff` produces a `ProfileDoc` that round-trips
|
|
exactly through the SB-S0 grammar; re-running is idempotent (bump, not duplicate).
|
|
|
|
Defaults: `promoteThreshold = 3`, `decayDays = 90` (operator-confirmed).
|
|
|
|
## The candidate file — the session↔engine contract
|
|
|
|
`--propose --candidates <file.json>` takes a JSON **array** of candidates; each is validated
|
|
(malformed → non-zero exit, nothing written):
|
|
|
|
```json
|
|
[
|
|
{ "key": "primary-expertise", "value": "AI governance in the public sector",
|
|
"provenance": "published", "source": "published:1a2b3c4d5e6f", "observed_date": "2026-05-26" }
|
|
]
|
|
```
|
|
|
|
- `key`, `value`, `source`, `observed_date` — non-empty strings; `provenance ∈ {human, published, ai-draft}`.
|
|
- `key` and `value` must be **single-line** (no newline/CR — the profile grammar is one fact per line).
|
|
- The session produces this from `--gather`'s output. The engine guarantees the *mechanics*; the *quality*
|
|
of the candidates is the session's job (see limits).
|
|
|
|
## Honest limits
|
|
|
|
- **The loop's value depends on the session's extraction.** The engine only guarantees threshold/conflict/
|
|
decay/provenance mechanics. Garbage candidates → a garbage diff. The operator gate + candidate-shape
|
|
validation catch shape errors, not insight quality.
|
|
- **`brain/profile.md` has one reader as of SB-S3a.** S2 evolved the profile motor-only; SB-S3a wired the
|
|
first consumer — `strategy-advisor` reads it as *evidence-to-test* (anti-sycophancy: counter-pressured, never
|
|
parroted). Broader consumption (more content agents, a hook-level prompt digest) remains later S3 work, and
|
|
the profile is still mutated ONLY via `brain consolidate --apply --confirm`.
|
|
- **No supersede / no auto-demotion.** Conflicts keep both; stale facts are flagged, never auto-removed —
|
|
the operator (or S3) reconciles. Conflict alt facts persist until then.
|
|
- **No AI at session-start.** The nudge is a deterministic file-count + sidecar read; the consolidation
|
|
pass is always operator-invoked.
|
|
- **The session-start nudge is consolidation-due only** — it counts published records + days since last run;
|
|
it does not parse `profile.md` for per-fact staleness (that cost/parser is deferred).
|