diff --git a/agents/strategy-advisor.md b/agents/strategy-advisor.md index 3f0dee0..8c0af91 100644 --- a/agents/strategy-advisor.md +++ b/agents/strategy-advisor.md @@ -45,6 +45,7 @@ ${LINKEDIN_STUDIO_DATA:-$HOME/.claude/linkedin-studio}/examples/high-engagement- ${CLAUDE_PLUGIN_ROOT}/references/trajectory-strategy-adjustments.md → trajectory-to-action mappings ~/.claude/linkedin-studio.local.md → user state + posting history ${LINKEDIN_STUDIO_DATA:-$HOME/.claude/linkedin-studio}/brain/profile.md → evolving second-brain profile (consolidated, evidence-weighted facts about the user) +${LINKEDIN_STUDIO_DATA:-$HOME/.claude/linkedin-studio}/brain/operations.md → the operations centre: dated "who I am now" anchor + plans + ideas (user-authored) ``` Use demographics data to compare the user's **intended** audience vs **actual** engagers when making strategic recommendations. @@ -62,6 +63,18 @@ Each fact line carries `evidence_count` and `last_seen`. Weight by strength and **Graceful absence:** If `brain/profile.md` is missing or empty (fresh installs have no brain yet), proceed silently on the other context sources — no error, and no note about a missing profile. +### Consuming the operations centre (brain/operations.md) + +The operations centre is the user-authored other half of the second brain (the brain motor never writes here). It has three sections: `## Who I am now (anchor)`, `## Plans`, and `## Ideas`. + +**The anchor is authoritative — it deprecates older inferences.** The `## Who I am now` anchor is the user's own *dated* declaration of current direction (a `_As of YYYY-MM-DD:_` line). When a `brain/profile.md` Dynamic fact predates the anchor's date or contradicts its stated direction, **prefer the anchor and flag the older fact as possibly-stale** — do not parrot the stale fact. This is the frozen-past-self guard: the user's current self overrides the machine's older read of them. + +**Inversion vs the profile (important):** unlike profile facts — which you treat as **evidence to TEST** (challenge them) — the anchor is user-declared direction, *not* an inference to challenge, so you **honour** it. It is direction, not praise: honouring it never licenses flattery, and it never excuses you from counter-pressuring the *profile* facts. + +**Plans vs Ideas:** `## Plans` are active commitments (current intent — weigh as what the user is actually doing); `## Ideas` are a parking lot (suggestions, not commitments — surface them as options, never as settled plans). + +**Graceful absence:** If `brain/operations.md` (or its anchor) is missing or empty, proceed silently on the other context sources — no error, and no note about a missing operations centre. + ### New Creator Advantage Detection From the state file, extract `first_post_date`. Calculate the creator window status: diff --git a/docs/second-brain/architecture.md b/docs/second-brain/architecture.md index 1f3b2a0..b3b9e49 100644 --- a/docs/second-brain/architecture.md +++ b/docs/second-brain/architecture.md @@ -77,7 +77,7 @@ Run on a cadence (reuse the existing session-start staleness-nudge surface) as a | **SB-S0 — Foundation** ✅ *landed 2026-06-23* | `brain/` scaffold + two-layer `profile.md` (fold in `user-profile.md`) + `index.md` MOC + `operations.md` + `journal/`; entity-id + provenance shape as a small typed, tested module; `ingest/` dirs + manual-import contract. **No loop yet.** Shipped as `scripts/brain/` (TS, 34 tests, gate-wired BRAIN floor); fold = P1 labeled-scalars + P2 expertise (checkbox-prefs deferred, §8 of `plan-sb-s0.md`). | Smallest thing that stands up and is testable; locks the id/provenance spine everything hangs on | | **SB-S1 — Ingest + gold signal** ✅ *landed 2026-06-23* | Manual import → `ingest/published/` with `provenance=published` (CLI `brain ingest`); voice-trainer wired to learn from published-only, never `ai-draft`, gate-enforced. Shipped as `scripts/brain/src/ingest.ts` + `docs/second-brain/ingest-manual-import.md` (v0.5.1). **No profile.md mutation** (SB-S2). | The gold signal before the loop that consumes it | | **SB-S2 — Evolution loop** ✅ *landed 2026-06-23* | Operator-invoked, operator-gated consolidation: `brain consolidate` (`--gather`/`--propose`/`--apply --confirm`) → profile **diff** w/ evidence_count/provenance/timestamps; threshold-promotion (N=3); contradiction → keep-both with distinct ids (no supersede); decay-flag (90d); `consolidation-state.json` sidecar; zero-dep session-start consolidation-due nudge + scaffold-ensure. Shipped as `scripts/brain/src/consolidate.ts` + `docs/second-brain/consolidation-loop.md` (v0.5.2). **Operator decisions:** journal deferred · no new agent (session extracts) · motor-only (no reader until S3). | The compounding mechanism | -| **SB-S3 — Cross-silo graph + ops centre** | Thread the id through tributaries (post↔specific↔trend↔analytics assemblable); flesh out `operations.md`; retire the dead `content-history.md` + triple-post reconciliation. **S3a ✅ first reader · S3b ✅ supersede · S3c ✅ cross-silo id-threading** (hub-side: the published record carries the `specifics`/`trends` ids it was built from + a pure analytics resolver — `scripts/brain/src/assemble.ts`, `brain assemble`; tributaries untouched). **S3d remains** (ops centre / `content-history.md` retirement / triple-post reconciliation). | Finally answers "which raw material performs?" | +| **SB-S3 — Cross-silo graph + ops centre** | Thread the id through tributaries (post↔specific↔trend↔analytics assemblable); flesh out `operations.md`; retire the dead `content-history.md` + triple-post reconciliation. **S3a ✅ first reader · S3b ✅ supersede · S3c ✅ cross-silo id-threading** (hub-side: the published record carries the `specifics`/`trends` ids it was built from + a pure analytics resolver — `scripts/brain/src/assemble.ts`, `brain assemble`; tributaries untouched). **S3d ✅ ops centre** (`operations.md` is a read tributary; `strategy-advisor` honours the dated "who I am now" anchor that *deprecates older inferences* — advisory/reader-side). **S3e remains** (dead `content-history.md` retirement + triple-post post-tracking↔published↔analytics reconciliation — the loose silo S3c kept out). | Finally answers "which raw material performs?" | | *(later / optional)* **SB-S4** | EU/EEA DMA portability API as an auto-tributary into `ingest/inbox/` | Additive; never a dependency | ## What's genuinely hard (honest flags) diff --git a/scripts/brain/src/scaffold.ts b/scripts/brain/src/scaffold.ts index 63903fb..d04e435 100644 --- a/scripts/brain/src/scaffold.ts +++ b/scripts/brain/src/scaffold.ts @@ -52,14 +52,25 @@ function indexSeed(): string { function operationsSeed(): string { return `# Operations +> The operations centre — where you're headed now, what you're working on, what you might +> do next. User-authored: the brain motor never writes here (a re-run of init never +> re-touches an existing file, so your edits are safe). + ## Who I am now (anchor) - + + +_As of YYYY-MM-DD:_ ## Plans + + ## Ideas + + `; } diff --git a/scripts/brain/tests/scaffold.test.ts b/scripts/brain/tests/scaffold.test.ts index f768ae5..d09bebe 100644 --- a/scripts/brain/tests/scaffold.test.ts +++ b/scripts/brain/tests/scaffold.test.ts @@ -57,6 +57,18 @@ describe("initBrain scaffold (SC1)", () => { assert.match(ops, /## Ideas/); }); + test("operations.md ships a dated frozen-past-self anchor convention (SB-S3d)", () => { + initBrain(); + const ops = readFileSync(join(root, "brain/operations.md"), "utf8"); + // RED-bearing literal: the dated-anchor convention is the genuinely-new SB-S3d seed + // content (absent pre-S3d). The frozen-past-self guard is temporal — the anchor must + // carry a date the reader can compare against a profile fact's last_seen. + assert.match(ops, /_As of YYYY-MM-DD:_/, "anchor ships the dated convention"); + // Non-RED companion (already shipped pre-S3d, asserted only to lock the guard wording + // the reader keys on — NOT the failing literal). + assert.match(ops, /deprecates older inferences/); + }); + test("a second invocation is a no-op — every target skipped, content unchanged", () => { initBrain(); const before = EXPECTED_FILES.map((f) => readFileSync(join(root, f), "utf8")); diff --git a/scripts/test-runner.sh b/scripts/test-runner.sh index 435b029..7aa785e 100755 --- a/scripts/test-runner.sh +++ b/scripts/test-runner.sh @@ -33,8 +33,10 @@ # never baked into the agent — with a non-vacuity self-test) in Section 17; the brain # profile-reader guard (SB-S3a: strategy-advisor names brain/profile.md AND carries the # anti-sycophancy literal 'evidence to TEST', with a non-vacuity self-test) in Section -# 16d; the assertion-count anti-erosion floor (SC6) in Section 18. All are live below -# (Sections 8–18). +# 16d; the brain operations-reader guard (SB-S3d: strategy-advisor names +# brain/operations.md AND carries the frozen-past-self literal 'deprecates older +# inferences', with a non-vacuity self-test) in Section 16e; the assertion-count +# anti-erosion floor (SC6) in Section 18. All are live below (Sections 8–18). # # Usage: bash scripts/test-runner.sh # bash 3.2-safe: plain arrays only, no `declare -A`, no `mapfile`/`readarray`. @@ -713,7 +715,7 @@ if [ -x "$BR_DIR/node_modules/.bin/tsx" ]; then BR_OUT=$( set +e; (cd "$BR_DIR" && npm test) 2>&1; echo "BR_EXIT:$?" ) BR_EXIT=$(echo "$BR_OUT" | grep -oE 'BR_EXIT:[0-9]+' | grep -oE '[0-9]+' | head -1) BR_TESTS=$(echo "$BR_OUT" | grep -oE 'tests [0-9]+' | grep -oE '[0-9]+' | tail -1) - BRAIN_TESTS_FLOOR=113 # SB-S0 34 [id(11)+profile(6)+fold(12)+scaffold(5)] + SB-S1 29 [ingest(14)+publish(9)+cli(6)] + SB-S2 19 [consolidate(12)+consolidate-cli(7)] + SB-S3b 12 [consolidate(10)+consolidate-cli(2)] + SB-S3c 19 [ingest(4)+publish(3)+assemble(8)+cli(4)] + BRAIN_TESTS_FLOOR=114 # SB-S0 34 [id(11)+profile(6)+fold(12)+scaffold(5)] + SB-S1 29 [ingest(14)+publish(9)+cli(6)] + SB-S2 19 [consolidate(12)+consolidate-cli(7)] + SB-S3b 12 [consolidate(10)+consolidate-cli(2)] + SB-S3c 19 [ingest(4)+publish(3)+assemble(8)+cli(4)] + SB-S3d 1 [scaffold dated-anchor seed] if [ "$BR_EXIT" = "0" ] && [ -n "$BR_TESTS" ] && [ "$BR_TESTS" -ge "$BRAIN_TESTS_FLOOR" ]; then pass "brain suite green: $BR_TESTS tests pass (floor $BRAIN_TESTS_FLOOR)" else @@ -831,6 +833,56 @@ fi echo "" +# --- Section 16e: Brain Operations Reader (SB-S3d) --- +echo "--- Brain Operations Reader ---" + +# SB-S3d wires strategy-advisor to read the second-brain OPERATIONS centre: it must +# (a) name brain/operations.md as a context source AND (b) carry the frozen-past-self +# literal 'deprecates older inferences' (architecture.md:59 — the dated "who I am now" +# anchor outranks/deprecates older profile inferences; advisory/reader-side). Both +# literals are required, grepped EXACT with grep -F: a loose 'operations' pattern would +# match generic prose, and naming the sibling brain/profile.md must NOT satisfy the +# operations literal. Non-vacuity self-test mirrors Sections 8/13/16c/16d: a probe counts +# as "wired" iff it carries BOTH exact literals; probes missing either (incl. a sibling- +# file decoy naming brain/profile.md) must NOT count as wired. The wiring is gate-enforced; +# the agent's runtime OBEDIENCE is verified manually (SC6/SC7 at land — agent-prompt +# behaviour is not unit-testable; the brief states this honestly). +OPS_LIT='brain/operations.md' +ANCHOR_LIT='deprecates older inferences' + +ops_reader_wired() { # $1 = text; wired iff BOTH literals present (echo twice — grep consumes stdin) + echo "$1" | grep -qF "$OPS_LIT" && echo "$1" | grep -qF "$ANCHOR_LIT" +} + +OPS_SELFTEST_OK=1 +if ! ops_reader_wired "reads brain/operations.md; the anchor deprecates older inferences"; then + OPS_SELFTEST_OK=0; echo " non-vacuity FAIL: a fully-wired probe was not detected" +fi +while IFS= read -r probe; do + [ -z "$probe" ] && continue + if ops_reader_wired "$probe"; then + OPS_SELFTEST_OK=0; echo " false-positive FAIL: under-wired probe accepted -> $probe" + fi +done <<'NEGATIVE16E' +reads brain/operations.md but never says the anchor outranks anything +the anchor deprecates older inferences but names no source file +reads brain/profile.md and tests the evidence +NEGATIVE16E +if [ "$OPS_SELFTEST_OK" -eq 1 ]; then + pass "ops-reader self-test: full wiring detected; 3 under-wired forms rejected (incl. sibling-file brain/profile.md decoy)" +else + fail "ops-reader self-test failed — the ops-reader-wiring lint is vacuous or over-eager" +fi + +SA_OPS="agents/strategy-advisor.md" +if grep -qF "$OPS_LIT" "$SA_OPS" && grep -qF "$ANCHOR_LIT" "$SA_OPS"; then + pass "strategy-advisor.md wired to brain operations reader (names '$OPS_LIT', frames '$ANCHOR_LIT')" +else + fail "strategy-advisor.md missing brain operations reader wiring — needs both '$OPS_LIT' and '$ANCHOR_LIT'" +fi + +echo "" + # --- Section 17: De-Niche Guard (B-S1 + B-S2) --- echo "--- De-Niche Guard ---" @@ -905,12 +957,13 @@ echo "" # brain-suite floor (Section 16b) = 75; +3 for SB-S1's three UNCONDITIONAL Section-16c # checks (published-only self-test + voice-trainer grep + contract-doc grep) = 78; # +2 for SB-S3a's two UNCONDITIONAL Section-16d checks (profile-reader self-test + -# strategy-advisor wiring grep) = 80. +# strategy-advisor wiring grep) = 80; +2 for SB-S3d's two UNCONDITIONAL Section-16e +# checks (ops-reader self-test + strategy-advisor ops-wiring grep) = 82. # NB: the floor tracks the deps-absent MINIMUM (conditional TS suites warn-skip and drop # the count), so it is bumped only by UNCONDITIONAL new checks — NOT pinned to the # deps-present TOTAL_CHECKS (that would zero the warn-skip margin and false-fail a fresh # clone). Runs last so TOTAL_CHECKS sees every prior check. -ASSERT_BASELINE_FLOOR=80 +ASSERT_BASELINE_FLOOR=82 TOTAL_CHECKS=$((PASS + FAIL)) if [ "$TOTAL_CHECKS" -ge "$ASSERT_BASELINE_FLOOR" ]; then pass "assertion-count anti-erosion: $TOTAL_CHECKS checks >= baseline floor $ASSERT_BASELINE_FLOOR"