# v0.10.8 Implementation Plan

> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.

**Goal:** Retire `event_log_append` from every agent-facing surface; add a Brain-native `brain_note` MCP tool; add a JSONL sidecar to every event written to `Brain/log/<date>.md`; switch `discipline-report` (and future machine consumers) to a reader that prefers JSONL with markdown fallback. MEMORY housekeeping removes language-anchored rules.

**Architecture:** One atomic writer (`appendLogEvent`) produces both markdown and JSONL inside a single `proper-lockfile` lock; one reader (`readLogDay`) prefers JSONL with markdown-fallback; the three identity-reminder templates, the Stop guardrail, and the hook event detector all stop mentioning `event_log_append`. The old MCP handlers are removed from every runtime (Claude Code, Codex, Hermes, OpenClaw); the bare CLI verb `o2b append-event` remains for human/cron use.

**Tech Stack:** TypeScript / Bun. `proper-lockfile` for cross-process write locks. Python shim (`plugins/hermes/__init__.py`) caches templates per process. Tests live in `tests/**/*.test.ts` and run under `bun test`.

**Companion spec:** `docs/plans/2026-05-19-v0.10.8-design.md`.

---

## File Structure

### Files created

- `src/core/brain/log-jsonl.ts` - the JSONL-preferred reader (`readLogDay`).
- `tests/core/brain.log-jsonl-reader.test.ts` - reader tests.
- `tests/cli/append-event.test.ts` - cmdAppendEvent fix test.

### Files modified

- `src/core/brain/log.ts` - dual writer (`.md` + `.jsonl` atomic via lock).
- `src/core/brain/types.ts` - `BRAIN_LOG_EVENT_KIND.note = "note"` + `BrainNoteLogEvent` union member.
- `src/mcp/brain-tools.ts` - new `toolBrainNote` handler + `BRAIN_TOOLS` entry.
- `src/mcp/tools.ts` - `WRITER_TOOL_NAMES` gains `"brain_note"`; remove `toolEventLogAppend`.
- `src/mcp/instructions.ts` - writer instructions list three tools.
- `src/openclaw/index.ts` - remove the `event_log_append` `registerTool` block.
- `src/cli/main.ts` - `cmdAppendEvent` resolves identity through `resolveAgentName`.
- `plugins/hermes/__init__.py` - docstring update; no functional change.
- `hooks/lib/detect.ts` - `BRAIN_EVENT_NAME_SUFFIX` and `BRAIN_EVENT_BASH_NEEDLES` drop event_log_append paths.
- `hooks/lib/messages.ts` - Stop guardrail mentions the three new triggers, not event_log_append.
- `hooks/README.md` - documentation rewrite for v0.10.8 surface.
- `templates/identity-reminder.txt` - new common reminder body.
- `templates/identity-reminder.hermes.txt` - Hermes-specific cadence on the new body.
- `templates/identity-reminder.openclaw.txt` - OpenClaw-specific cadence on the new body.
- `tests/core/brain.log.test.ts` - dual-write assertions.
- `tests/core/discipline/log-counts.test.ts` - counts via JSONL + fallback.
- `tests/hooks/detect.test.ts` - event_log_append no longer clears guardrail.
- `tests/hooks/stop-log-guardrail.test.ts` - new triggers clear the guardrail.
- `tests/hooks/post-write-reminder.test.ts` - reminder mentions brain_note.
- `tests/core/identity-reminder.test.ts` - byte-pinned new template text.
- `tests/fixtures/identity-reminder/hermes.txt` - regenerated.
- `tests/fixtures/identity-reminder/openclaw.txt` - regenerated.
- `tests/openclaw/bundle.test.ts` - drop `event_log_append` registration assertion.
- `tests/python/test_hermes_plugin.py` - drop `event_log_append` mention.
- `tests/mcp/mcp.test.ts` - check `brain_note` advertised; `event_log_append` is not.
- `tests/mcp/brain.test.ts` - new `brain_note` happy-path + sanitisation tests.
- `CHANGELOG.md` - new `[0.10.8]` section.
- `Projects/OpenSecondBrain/Features/_summary.md` (in `/root/vault/`) - mark §32 and §23 shipped.

### Files explicitly NOT touched

- `src/core/event-log.ts` - `appendEvent` stays for cron-jobs / `o2b append-event` CLI.
- `src/core/redactor.ts` - `sanitiseTextField` is already the shared sanitiser; no need to move.
- `src/core/brain/protect.ts` - protects `${dirs.log}/**` glob already; `.jsonl` covered automatically.

### MEMORY housekeeping

- `/root/.claude/projects/-root/memory/feedback_daily_log_strict_russian.md` - delete.
- `/root/.claude/projects/-root/memory/project_o2b_log_language.md` - delete.
- `/root/.claude/projects/-root/memory/feedback_always_russian.md` - rewrite (no Russian-anchored principle).
- `/root/.claude/projects/-root/memory/feedback_no_em_dashes.md` - rewrite (typography-anchored, not Russian-anchored).
- `/root/.claude/projects/-root/memory/MEMORY.md` - index update.

---

## Implementation Order

Tasks are listed in execution order. Tasks 1-6 land the core write path; 7-9 the new tool; 10-12 the agent-facing surface cleanup; 13-15 the CLI fix and old-handler drop; 16-17 wiring tests and documentation; 18 the release artifacts; 19 the MEMORY housekeeping.

Each task is independent enough to ship as one commit. The plan assumes commits accumulate on one branch; CI runs on each push.

---

## Task 1: JSONL writer - extend `appendLogEvent`

**Files:**
- Modify: `src/core/brain/log.ts`
- Test: `tests/core/brain.log.test.ts`

The current `appendLogEvent` writes one markdown file. We add a JSONL sidecar written in the same atomic step under a directory-level lock.

- [ ] **Step 1.1: Read the existing writer to understand atomicity guarantees**

Run: `grep -n "appendLogEvent\|atomicWriteFileSync" src/core/brain/log.ts`

Expected: confirm `appendLogEvent` is currently synchronous, reads-then-writes via `atomicWriteFileSync`, no lock.

- [ ] **Step 1.2: Write the failing test for dual write**

Add to `tests/core/brain.log.test.ts`:

```ts
import { existsSync, readFileSync } from "node:fs";
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { describe, expect, test } from "bun:test";
import { appendLogEvent } from "../../src/core/brain/log.ts";
import { BRAIN_LOG_EVENT_KIND, type BrainLogEntry } from "../../src/core/brain/types.ts";

describe("appendLogEvent dual write", () => {
  test("writes the same event to both .md and .jsonl", () => {
    const vault = mkdtempSync(join(tmpdir(), "osb-log-"));
    try {
      const entry: BrainLogEntry = {
        timestamp: "2026-05-19T10:00:00Z",
        eventType: BRAIN_LOG_EVENT_KIND.feedback,
        body: { signal: "[[sig-x]]", topic: "x", sign: "positive" },
      };
      appendLogEvent(vault, entry);
      const md = readFileSync(join(vault, "Brain/log/2026-05-19.md"), "utf8");
      const jsonl = readFileSync(join(vault, "Brain/log/2026-05-19.jsonl"), "utf8");
      expect(md).toContain("## 10:00:00Z - feedback");
      const lines = jsonl.trim().split("\n");
      expect(lines).toHaveLength(1);
      const parsed = JSON.parse(lines[0]!);
      expect(parsed).toEqual({
        ts: "2026-05-19T10:00:00Z",
        kind: "feedback",
        payload: { signal: "[[sig-x]]", topic: "x", sign: "positive" },
      });
    } finally {
      rmSync(vault, { recursive: true, force: true });
    }
  });

  test("appends a second event to both files", () => {
    const vault = mkdtempSync(join(tmpdir(), "osb-log-"));
    try {
      const first: BrainLogEntry = {
        timestamp: "2026-05-19T10:00:00Z",
        eventType: BRAIN_LOG_EVENT_KIND.feedback,
        body: { signal: "[[sig-a]]", topic: "a", sign: "positive" },
      };
      const second: BrainLogEntry = {
        timestamp: "2026-05-19T10:00:01Z",
        eventType: BRAIN_LOG_EVENT_KIND.applyEvidence,
        body: { preference: "[[pref-a]]", artifact: "[[file.ts]]", agent: "@x", result: "applied" },
      };
      appendLogEvent(vault, first);
      appendLogEvent(vault, second);
      const jsonl = readFileSync(join(vault, "Brain/log/2026-05-19.jsonl"), "utf8");
      const lines = jsonl.trim().split("\n");
      expect(lines).toHaveLength(2);
      expect(JSON.parse(lines[0]!).kind).toBe("feedback");
      expect(JSON.parse(lines[1]!).kind).toBe("apply-evidence");
    } finally {
      rmSync(vault, { recursive: true, force: true });
    }
  });

  test("encodes array payloads as JSON arrays, not repeated keys", () => {
    const vault = mkdtempSync(join(tmpdir(), "osb-log-"));
    try {
      const entry: BrainLogEntry = {
        timestamp: "2026-05-19T10:00:00Z",
        eventType: BRAIN_LOG_EVENT_KIND.dream,
        body: { run_id: "dream-1", clustered: ["sig-a", "sig-b"] },
      };
      appendLogEvent(vault, entry);
      const jsonl = readFileSync(join(vault, "Brain/log/2026-05-19.jsonl"), "utf8");
      const parsed = JSON.parse(jsonl.trim());
      expect(parsed.payload.clustered).toEqual(["sig-a", "sig-b"]);
    } finally {
      rmSync(vault, { recursive: true, force: true });
    }
  });
});
```

- [ ] **Step 1.3: Run the test - confirm failure**

Run: `bun test tests/core/brain.log.test.ts`

Expected: FAIL with "ENOENT: no such file or directory ... Brain/log/2026-05-19.jsonl".

- [ ] **Step 1.4: Update the writer**

Modify `src/core/brain/log.ts`. The new shape:

```ts
// At top of file, add import:
import lockfile from "proper-lockfile";
import { dirname } from "node:path";
import { mkdirSync } from "node:fs";

// Existing renderEventBlock(...) stays unchanged.

// Add new pure helper alongside `renderEventBlock`:
function renderJsonlLine(event: BrainLogEntry): string {
  // The JSONL row is a one-to-one projection of the markdown body:
  // every bullet becomes a key, array bullets become JSON arrays.
  // `BrainLogEntry.body` already has that shape, so the projection is
  // direct - no per-kind branching, which keeps the writer kind-agnostic.
  const payload: Record<string, string | ReadonlyArray<string>> = {};
  for (const [key, value] of Object.entries(event.body)) {
    payload[key] = value;
  }
  return JSON.stringify({
    ts: event.timestamp,
    kind: event.eventType,
    payload,
  });
}

// Existing `appendLogEvent` becomes an async-free function that uses a
// synchronous proper-lockfile lock - the public signature stays the same
// (synchronous), callers do not change.

export function appendLogEvent(
  vault: string,
  event: BrainLogEntry,
): AppendLogEventResult {
  if (!EVENT_KIND_VALUES.has(event.eventType)) {
    throw new Error(
      `appendLogEvent: unknown event kind '${event.eventType}' - must be one of ${Object.values(
        BRAIN_LOG_EVENT_KIND,
      ).join(", ")}`,
    );
  }

  const ts = parseIsoUtc(event.timestamp);
  const mdPath = logPath(vault, ts.date);
  const jsonlPath = logJsonlPath(vault, ts.date);

  // proper-lockfile requires the lock target to exist. Lock the log
  // directory itself, created up-front, the same scope used by
  // `src/core/event-log.ts:appendEvent`. The sync API forbids the
  // built-in `retries` option, so the call is wrapped in a small
  // ELOCKED-only retry helper (see `acquireLogLock` in the shipped
  // `log.ts`).
  const logDir = brainDirs(vault).log;
  mkdirSync(logDir, { recursive: true });

  const release = acquireLogLock(logDir);

  try {
    // ---- JSONL sidecar (machine surface, primary) ---------------------
    // Write JSONL first: it is the source of truth for `readLogDay`.
    // If the markdown write below fails, the human Obsidian view lags
    // by one event, but discipline-report / future machine consumers
    // keep returning the correct counts.
    const existingJsonl = existsSync(jsonlPath) ? readFileSync(jsonlPath, "utf8") : "";
    const line = renderJsonlLine(event);
    const nextJsonl =
      existingJsonl === ""
        ? line + "\n"
        : `${existingJsonl.replace(/\s+$/, "")}\n${line}\n`;
    atomicWriteFileSync(jsonlPath, nextJsonl);

    // ---- Markdown (human surface) -------------------------------------
    const existingMd = existsSync(mdPath) ? readFileSync(mdPath, "utf8") : "";
    const block = renderEventBlock(event, ts.hms);
    let nextMd: string;
    if (existingMd === "") {
      nextMd = renderFileHeader(ts.date) + "\n" + block;
    } else {
      nextMd = `${existingMd.replace(/\s+$/, "")}\n\n${block}`;
    }
    if (!nextMd.endsWith("\n")) nextMd += "\n";
    atomicWriteFileSync(mdPath, nextMd);

    return { logPath: mdPath };
  } finally {
    release();
  }
}
```

Note: `proper-lockfile` exposes `lockSync` (mirror of `lock`) that returns a release function; we use it to keep `appendLogEvent` synchronous (the existing public contract). The sync flavour does not support the `retries` option, so the shipped implementation wraps `lockSync` in a small ELOCKED-only retry loop (`acquireLogLock`) instead of passing `retries` directly.

- [ ] **Step 1.5: Run the tests - confirm pass**

Run: `bun test tests/core/brain.log.test.ts`

Expected: PASS for all dual-write cases.

- [ ] **Step 1.6: Commit**

```bash
git add src/core/brain/log.ts tests/core/brain.log.test.ts
git commit -m "$(cat <<'EOF'
feat(brain): write JSONL sidecar next to each Brain/log/<date>.md

§23 of OSB features summary. Dual-write under a proper-lockfile lock so
markdown and JSONL stay in lock-step; the JSONL row is a deterministic
projection of the markdown body. No reader change yet; callers stay on
parseLogDay until log-jsonl.ts lands.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
EOF
)"
```

(NOTE: this plan documents the suggested commits. Commits are only created when the user explicitly asks for them - per the standing rule, do not commit unprompted.)

---

## Task 2: JSONL reader - `src/core/brain/log-jsonl.ts`

**Files:**
- Create: `src/core/brain/log-jsonl.ts`
- Test: `tests/core/brain.log-jsonl-reader.test.ts`

- [ ] **Step 2.1: Write the failing reader test**

Create `tests/core/brain.log-jsonl-reader.test.ts`:

```ts
import { mkdirSync, writeFileSync } from "node:fs";
import { mkdtempSync, rmSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { describe, expect, test } from "bun:test";
import { readLogDay } from "../../src/core/brain/log-jsonl.ts";
import { appendLogEvent } from "../../src/core/brain/log.ts";
import { BRAIN_LOG_EVENT_KIND } from "../../src/core/brain/types.ts";

describe("readLogDay", () => {
  test("returns JSONL entries when .jsonl exists", () => {
    const vault = mkdtempSync(join(tmpdir(), "osb-read-"));
    try {
      appendLogEvent(vault, {
        timestamp: "2026-05-19T10:00:00Z",
        eventType: BRAIN_LOG_EVENT_KIND.feedback,
        body: { signal: "[[sig-x]]", topic: "x", sign: "positive" },
      });
      const res = readLogDay(vault, "2026-05-19");
      expect(res.source).toBe("jsonl");
      expect(res.entries).toHaveLength(1);
      expect(res.entries[0]!.eventType).toBe("feedback");
      expect(res.entries[0]!.body.topic).toBe("x");
    } finally {
      rmSync(vault, { recursive: true, force: true });
    }
  });

  test("falls back to markdown when .jsonl is absent", () => {
    const vault = mkdtempSync(join(tmpdir(), "osb-read-"));
    try {
      // Hand-craft a markdown-only day (simulating pre-v0.10.8 history).
      const dir = join(vault, "Brain/log");
      mkdirSync(dir, { recursive: true });
      const md = `---
kind: brain-log
date: 2026-05-10
tags: [brain, brain/log]
---

# Brain log - 2026-05-10

## 09:00:00Z - feedback
- signal: [[sig-old]]
- topic: old
- sign: positive
`;
      writeFileSync(join(dir, "2026-05-10.md"), md, "utf8");
      const res = readLogDay(vault, "2026-05-10");
      expect(res.source).toBe("markdown-fallback");
      expect(res.entries).toHaveLength(1);
      expect(res.entries[0]!.body.topic).toBe("old");
    } finally {
      rmSync(vault, { recursive: true, force: true });
    }
  });

  test("returns empty result when neither file exists", () => {
    const vault = mkdtempSync(join(tmpdir(), "osb-read-"));
    try {
      const res = readLogDay(vault, "2026-01-01");
      expect(res.entries).toHaveLength(0);
      expect(res.warnings).toHaveLength(0);
    } finally {
      rmSync(vault, { recursive: true, force: true });
    }
  });

  test("surfaces malformed JSONL lines as warnings", () => {
    const vault = mkdtempSync(join(tmpdir(), "osb-read-"));
    try {
      const dir = join(vault, "Brain/log");
      mkdirSync(dir, { recursive: true });
      const lines = [
        JSON.stringify({ ts: "2026-05-19T10:00:00Z", kind: "feedback", payload: { topic: "ok" } }),
        "not-valid-json",
        JSON.stringify({ ts: "2026-05-19T10:00:02Z", kind: "feedback", payload: { topic: "also-ok" } }),
      ].join("\n");
      writeFileSync(join(dir, "2026-05-19.jsonl"), lines + "\n", "utf8");
      const res = readLogDay(vault, "2026-05-19");
      expect(res.source).toBe("jsonl");
      expect(res.entries).toHaveLength(2);
      expect(res.warnings).toHaveLength(1);
      expect(res.warnings[0]!.message).toMatch(/malformed/i);
    } finally {
      rmSync(vault, { recursive: true, force: true });
    }
  });
});
```

- [ ] **Step 2.2: Run the test - confirm failure**

Run: `bun test tests/core/brain.log-jsonl-reader.test.ts`

Expected: FAIL with "Cannot find module .../log-jsonl.ts".

- [ ] **Step 2.3: Implement the reader**

Create `src/core/brain/log-jsonl.ts`:

```ts
/**
 * Brain log reader that prefers the v0.10.8 JSONL sidecar and falls
 * back to parsing the markdown on days without one. All machine
 * consumers (discipline-report today, future tooling tomorrow) read
 * through this entry point so they do not duplicate the fallback
 * logic.
 *
 * The reader returns the same `BrainLogEntry[]` shape that
 * `parseLogDay` returns, plus a `source` discriminator the caller can
 * surface in diagnostics. Malformed JSONL lines do not abort the
 * read; they surface as `warnings` so a partially-edited file is
 * still useful.
 */

import { existsSync, readFileSync } from "node:fs";

import { logPath, validateIsoDate } from "./paths.ts";
import {
  parseLogDay,
  type BrainLogEntry,
  type BrainLogParseWarning,
} from "./log.ts";
import {
  BRAIN_LOG_EVENT_KIND,
  type BrainLogEventKind,
} from "./types.ts";

export interface ReadLogDayResult {
  readonly entries: ReadonlyArray<BrainLogEntry>;
  readonly source: "jsonl" | "markdown-fallback";
  readonly warnings: ReadonlyArray<BrainLogParseWarning>;
}

const EVENT_KIND_VALUES: ReadonlySet<string> = new Set(
  Object.values(BRAIN_LOG_EVENT_KIND),
);

export function readLogDay(vault: string, date: string): ReadLogDayResult {
  const validDate = validateIsoDate(date);
  const mdPath = logPath(vault, validDate);
  const jsonlPath = mdPath.replace(/\.md$/, ".jsonl");

  if (existsSync(jsonlPath)) {
    return readJsonl(jsonlPath);
  }
  if (existsSync(mdPath)) {
    const parsed = parseLogDay(vault, validDate);
    return {
      entries: parsed.entries,
      source: "markdown-fallback",
      warnings: parsed.warnings,
    };
  }
  return { entries: [], source: "jsonl", warnings: [] };
}

function readJsonl(path: string): ReadLogDayResult {
  let text: string;
  try {
    text = readFileSync(path, "utf8");
  } catch (err) {
    // The caller already verified `existsSync(path)`; a read failure
    // here is anomalous (rotation race, permission flip, transient
    // fs error). Surface it as a warning so discipline-report and
    // doctor can flag the day instead of silently returning zero.
    const message = (err as NodeJS.ErrnoException).message ?? String(err);
    return {
      entries: [],
      source: "jsonl",
      warnings: [{ path, lineNumber: 0, message: `failed to read JSONL file: ${message}` }],
    };
  }

  const entries: BrainLogEntry[] = [];
  const warnings: BrainLogParseWarning[] = [];
  const lines = text.split(/\r?\n/);
  for (let i = 0; i < lines.length; i++) {
    const line = lines[i]!;
    if (line.trim() === "") continue;
    let parsed: unknown;
    try {
      parsed = JSON.parse(line);
    } catch {
      warnings.push({
        path,
        lineNumber: i + 1,
        message: `malformed JSONL line: ${line.slice(0, 80)}`,
      });
      continue;
    }
    const entry = coerceEntry(parsed, path, i + 1, warnings);
    if (entry !== null) entries.push(entry);
  }
  return { entries, source: "jsonl", warnings };
}

function coerceEntry(
  raw: unknown,
  path: string,
  lineNumber: number,
  warnings: BrainLogParseWarning[],
): BrainLogEntry | null {
  if (raw === null || typeof raw !== "object") {
    warnings.push({ path, lineNumber, message: "JSONL row is not an object" });
    return null;
  }
  const obj = raw as Record<string, unknown>;
  const ts = obj["ts"];
  const kind = obj["kind"];
  const payload = obj["payload"];
  if (typeof ts !== "string" || typeof kind !== "string") {
    warnings.push({
      path,
      lineNumber,
      message: "JSONL row missing ts/kind",
    });
    return null;
  }
  if (!EVENT_KIND_VALUES.has(kind)) {
    warnings.push({ path, lineNumber, message: `unknown event kind: ${kind}` });
    return null;
  }
  if (payload === null || typeof payload !== "object" || Array.isArray(payload)) {
    warnings.push({ path, lineNumber, message: "JSONL row missing payload object" });
    return null;
  }
  const body: Record<string, string | ReadonlyArray<string>> = {};
  for (const [k, v] of Object.entries(payload as Record<string, unknown>)) {
    if (typeof v === "string") {
      body[k] = v;
    } else if (Array.isArray(v) && v.every((x) => typeof x === "string")) {
      body[k] = v as string[];
    } else {
      warnings.push({
        path,
        lineNumber,
        message: `JSONL payload key '${k}' has unsupported value type`,
      });
    }
  }
  return {
    timestamp: ts,
    eventType: kind as BrainLogEventKind,
    body: Object.freeze(body),
  };
}
```

- [ ] **Step 2.4: Run the tests - confirm pass**

Run: `bun test tests/core/brain.log-jsonl-reader.test.ts`

Expected: PASS for all four scenarios.

- [ ] **Step 2.5: Commit (do not run without explicit user request)**

```bash
git add src/core/brain/log-jsonl.ts tests/core/brain.log-jsonl-reader.test.ts
git commit -m "feat(brain): add readLogDay with JSONL-preferred fallback to markdown"
```

---

## Task 3: Switch discipline-report to `readLogDay`

**Files:**
- Modify: `src/core/discipline/log-counts.ts`
- Test: `tests/core/discipline/log-counts.test.ts`

- [ ] **Step 3.1: Add failing test for JSONL-source counting**

In `tests/core/discipline/log-counts.test.ts`, add:

```ts
test("countBrainEvents uses JSONL when present", () => {
  const vault = mkdtempSync(join(tmpdir(), "osb-disc-"));
  try {
    appendLogEvent(vault, {
      timestamp: "2026-05-19T10:00:00Z",
      eventType: BRAIN_LOG_EVENT_KIND.feedback,
      body: { signal: "[[sig-x]]", topic: "x", sign: "positive", agent: "@a1" },
    });
    appendLogEvent(vault, {
      timestamp: "2026-05-19T10:00:01Z",
      eventType: BRAIN_LOG_EVENT_KIND.applyEvidence,
      body: { preference: "[[pref-x]]", artifact: "[[f.ts]]", agent: "@a1", result: "applied" },
    });
    // Wipe the markdown to prove JSONL is the source.
    rmSync(join(vault, "Brain/log/2026-05-19.md"));
    const counts = countBrainEvents(vault, "2026-05-19", ["@a1"]);
    expect(counts.byAgent["@a1"]!.feedback).toBe(1);
    expect(counts.byAgent["@a1"]!.apply_evidence).toBe(1);
    expect(counts.total).toBe(2);
  } finally {
    rmSync(vault, { recursive: true, force: true });
  }
});

test("countBrainEvents falls back to markdown when only markdown exists", () => {
  const vault = mkdtempSync(join(tmpdir(), "osb-disc-"));
  try {
    const dir = join(vault, "Brain/log");
    mkdirSync(dir, { recursive: true });
    writeFileSync(
      join(dir, "2026-05-10.md"),
      `---
kind: brain-log
date: 2026-05-10
tags: [brain, brain/log]
---

# Brain log - 2026-05-10

## 09:00:00Z - feedback
- signal: [[sig-old]]
- topic: old
- sign: positive
- agent: @a2
`,
      "utf8",
    );
    const counts = countBrainEvents(vault, "2026-05-10", ["@a2"]);
    expect(counts.byAgent["@a2"]!.feedback).toBe(1);
  } finally {
    rmSync(vault, { recursive: true, force: true });
  }
});
```

- [ ] **Step 3.2: Run - confirm failure (fallback test passes; jsonl test fails)**

Run: `bun test tests/core/discipline/log-counts.test.ts`

Expected: JSONL-source test FAILS (file deleted, `parseLogDay` returns empty).

- [ ] **Step 3.3: Update `countBrainEvents` to use `readLogDay`**

In `src/core/discipline/log-counts.ts`, replace the `parseLogDay` import and call:

```ts
import { readLogDay } from "../brain/log-jsonl.ts";

// ... inside countBrainEvents:
const { entries } = readLogDay(vault, date);
```

- [ ] **Step 3.4: Run tests - confirm pass**

Run: `bun test tests/core/discipline/log-counts.test.ts`

Expected: PASS for both scenarios.

- [ ] **Step 3.5: Commit (no auto-commit)**

---

## Task 4: Add the `note` event kind

**Files:**
- Modify: `src/core/brain/types.ts`

- [ ] **Step 4.1: Add the constant**

In `src/core/brain/types.ts`, inside the `BRAIN_LOG_EVENT_KIND` `as const` map, add (just before the closing `}`):

```ts
  /**
   * `note` (§32B v0.10.8) - narrative milestone written by `brain_note`.
   * One-line `text` plus `agent`; not consumed by dream beyond counting.
   */
  note: "note",
```

- [ ] **Step 4.2: Add the typed event interface**

Below the existing `BrainImportClaudeMemoryLogEvent` interface, add:

```ts
/**
 * `note` entry (§32B) - one narrative-milestone line. Not consumed by
 * the dream pass; it exists so an agent has a Brain-native home for
 * "I shipped X" / "PR Y merged" lines instead of falling back to the
 * deprecated Daily/ event log.
 */
export interface BrainNoteLogEvent extends BrainLogEventBase {
  readonly kind: typeof BRAIN_LOG_EVENT_KIND.note;
  readonly text: string;
  readonly agent: string;
}
```

Then in the `BrainLogEvent` union, append `| BrainNoteLogEvent`.

- [ ] **Step 4.3: Compile-check**

Run: `bun run tsc --noEmit`

Expected: no new errors.

- [ ] **Step 4.4: Commit (no auto-commit)**

---

## Task 5: `brain_note` MCP handler

**Files:**
- Modify: `src/mcp/brain-tools.ts`
- Modify: `src/mcp/tools.ts` (WRITER_TOOL_NAMES)
- Modify: `src/mcp/instructions.ts` (writer instructions text)
- Test: `tests/mcp/brain.test.ts`

- [ ] **Step 5.1: Write the failing tool test**

In `tests/mcp/brain.test.ts`, add a `describe("brain_note", ...)` block:

```ts
describe("brain_note", () => {
  test("happy path writes a note event with the resolved agent", async () => {
    const { ctx, vault } = makeCtx();
    try {
      const result = await callTool("brain_note", ctx, { text: "v0.10.7 shipped" });
      expect(result.agent).toBeTruthy();
      const md = readFileSync(
        join(vault, `Brain/log/${todayIso()}.md`),
        "utf8",
      );
      expect(md).toMatch(/- note$/m);
      expect(md).toContain("text: v0.10.7 shipped");
      const jsonl = readFileSync(
        join(vault, `Brain/log/${todayIso()}.jsonl`),
        "utf8",
      );
      const parsed = JSON.parse(jsonl.trim().split("\n").pop()!);
      expect(parsed.kind).toBe("note");
      expect(parsed.payload.text).toBe("v0.10.7 shipped");
    } finally {
      cleanup(vault);
    }
  });

  test("redacts secret-shaped tokens", async () => {
    const { ctx, vault } = makeCtx();
    try {
      await callTool("brain_note", ctx, {
        text: "deployed api_key=sk-abc123 to prod",
      });
      const md = readFileSync(
        join(vault, `Brain/log/${todayIso()}.md`),
        "utf8",
      );
      expect(md).toContain("***REDACTED***");
      expect(md).not.toContain("sk-abc123");
    } finally {
      cleanup(vault);
    }
  });

  test("collapses multi-line input to one space-joined line", async () => {
    const { ctx, vault } = makeCtx();
    try {
      await callTool("brain_note", ctx, { text: "line one\nline two" });
      const md = readFileSync(
        join(vault, `Brain/log/${todayIso()}.md`),
        "utf8",
      );
      expect(md).toContain("text: line one line two");
    } finally {
      cleanup(vault);
    }
  });

  test("rejects empty / whitespace-only text", async () => {
    const { ctx } = makeCtx();
    await expect(callTool("brain_note", ctx, { text: "   " })).rejects.toThrow(
      /text is required/,
    );
  });
});
```

Helpers `makeCtx` / `callTool` / `cleanup` / `todayIso` exist in the file already (reused by `brain_feedback` tests).

- [ ] **Step 5.2: Run - confirm failure**

Run: `bun test tests/mcp/brain.test.ts -t "brain_note"`

Expected: FAIL with "unknown tool: brain_note" or similar.

- [ ] **Step 5.3: Implement the handler**

Add to `src/mcp/brain-tools.ts`, near the existing `toolBrainApplyEvidence` block:

```ts
// ----- brain_note ----------------------------------------------------------

const NOTE_TEXT_MAX_LEN = 4096;

async function toolBrainNote(
  ctx: ServerContext,
  args: Record<string, unknown>,
): Promise<Record<string, unknown>> {
  const rawText = coerceStr(args, "text", true)!;
  const agentArg = coerceStr(args, "agent", false);
  const sanitised = sanitiseTextField(rawText, {
    maxLen: NOTE_TEXT_MAX_LEN,
    singleLine: true,
  });
  if (!sanitised || !sanitised.trim()) {
    throw new MCPError(INVALID_PARAMS, "brain_note: text is required");
  }

  const agent =
    normalizeAgentArgument(agentArg) ??
    resolveAgentName(ctx.configPath ?? undefined);
  const now = new Date();
  const timestamp = isoSecond(now);

  const entry: BrainLogEntry = {
    timestamp,
    eventType: BRAIN_LOG_EVENT_KIND.note,
    body: { text: sanitised.trim(), agent },
  };
  const result = appendLogEvent(ctx.vault, entry);
  return {
    logged_at: timestamp,
    log_path: vaultRelativeSafe(ctx.vault, result.logPath),
    absolute_log_path: resolve(result.logPath),
    agent,
  };
}
```

Add the import (top of file):

```ts
import { sanitiseTextField } from "../core/redactor.ts";
```

Add `BRAIN_TOOLS` entry (right after the `brain_apply_evidence` block, before the next tool):

```ts
{
  name: "brain_note",
  description:
    "Append one narrative-milestone line to today's Brain log (`Brain/log/<today>.md` plus the JSONL sidecar). Use for events that are neither a new preference nor evidence against an existing one - e.g. a release, a merged PR, a discovered fact.",
  inputSchema: {
    type: "object",
    properties: {
      text: {
        type: "string",
        description:
          "One-line narrative description. Newlines are collapsed to spaces; redactor strips secret-shaped tokens.",
      },
      agent: {
        type: "string",
        description:
          "Optional agent identity override; defaults to the server-resolved name.",
      },
    },
    required: ["text"],
    additionalProperties: false,
  },
  handler: toolBrainNote,
},
```

In `src/mcp/tools.ts`, extend `WRITER_TOOL_NAMES`:

```ts
const WRITER_TOOL_NAMES: ReadonlySet<string> = new Set([
  "brain_feedback",
  "brain_apply_evidence",
  "brain_note",
]);
```

In `src/mcp/instructions.ts`, update `WRITER_INSTRUCTIONS`:

```ts
const WRITER_INSTRUCTIONS = `Open Second Brain - writer surface (always-loaded).

Three tools live here:
  - brain_feedback        - record one new taste signal the user just expressed.
  - brain_apply_evidence  - record applied | violated | outdated against an
                            active preference for an artifact this turn produced.
  - brain_note            - record one narrative milestone (release, merged PR,
                            discovered fact) that fits neither category.

The remaining Brain surface (digest, query, doctor, backlinks, search,
Pay Memory tools, vault_health, second_brain_status, second_brain_query,
and the scheduled learning pass) lives on the sibling
"open-second-brain" MCP server (deferred). Use ToolSearch to reach it.

Prefer the writer-server copies of brain_feedback / brain_apply_evidence /
brain_note over any duplicate exposed by the full server - both call the same
handler, but the writer copy is always available without ToolSearch.`;
```

- [ ] **Step 5.4: Run the tests - confirm pass**

Run: `bun test tests/mcp/brain.test.ts -t "brain_note"`

Expected: PASS for all four scenarios.

- [ ] **Step 5.5: Commit (no auto-commit)**

---

## Task 6: Discipline counts gain `note` field

**Files:**
- Modify: `src/core/discipline/log-counts.ts`
- Modify: `src/core/discipline/render.ts`
- Test: `tests/core/discipline/log-counts.test.ts`
- Test: `tests/core/discipline/render.test.ts`

- [ ] **Step 6.1: Add the failing test**

```ts
test("countBrainEvents counts note events", () => {
  const vault = mkdtempSync(join(tmpdir(), "osb-disc-"));
  try {
    appendLogEvent(vault, {
      timestamp: "2026-05-19T10:00:00Z",
      eventType: BRAIN_LOG_EVENT_KIND.note,
      body: { text: "v0.10.7 shipped", agent: "@a1" },
    });
    const counts = countBrainEvents(vault, "2026-05-19", ["@a1"]);
    expect(counts.byAgent["@a1"]!.note).toBe(1);
    expect(counts.byAgent["@a1"]!.total).toBe(1);
  } finally {
    rmSync(vault, { recursive: true, force: true });
  }
});
```

- [ ] **Step 6.2: Run - confirm failure**

Expected: FAIL (no `note` field on `AgentCounts`).

- [ ] **Step 6.3: Add the field and update the bumper**

`src/core/discipline/log-counts.ts`:

```ts
export interface AgentCounts {
  readonly feedback: number;
  readonly apply_evidence: number;
  readonly note: number;
  readonly other: number;
  readonly total: number;
}

function zero(): AgentCounts {
  return { feedback: 0, apply_evidence: 0, note: 0, other: 0, total: 0 };
}

function bump(c: AgentCounts, kind: string): AgentCounts {
  if (kind === "feedback") return { ...c, feedback: c.feedback + 1, total: c.total + 1 };
  if (kind === "apply-evidence") return { ...c, apply_evidence: c.apply_evidence + 1, total: c.total + 1 };
  if (kind === "note") return { ...c, note: c.note + 1, total: c.total + 1 };
  return { ...c, other: c.other + 1, total: c.total + 1 };
}
```

- [ ] **Step 6.4: Surface `note` in the renderer**

In `src/core/discipline/render.ts`, locate the block that renders feedback / apply_evidence per agent. Add a `note` column / line; keep the format symmetrical with the existing ones. Update both the markdown and JSON renderers if applicable. Confirm with `tests/core/discipline/render.test.ts` (add an assertion that note appears).

- [ ] **Step 6.5: Run tests - confirm pass**

Run: `bun test tests/core/discipline/`

Expected: PASS.

- [ ] **Step 6.6: Commit (no auto-commit)**

---

## Task 7: Hook detector drops `event_log_append`

**Files:**
- Modify: `hooks/lib/detect.ts`
- Test: `tests/hooks/detect.test.ts`

- [ ] **Step 7.1: Update the failing tests first**

In `tests/hooks/detect.test.ts`, find the cases that assert `event_log_append` clears the guardrail. Flip them so:

```ts
test("event_log_append no longer counts as a brain event", () => {
  expect(isBrainEventToolName("event_log_append")).toBe(false);
  expect(isBrainEventToolName("mcp__open-second-brain__event_log_append")).toBe(false);
});

test("brain_note counts as a brain event", () => {
  expect(isBrainEventToolName("brain_note")).toBe(true);
  expect(isBrainEventToolName("mcp__open-second-brain__brain_note")).toBe(true);
});

test("o2b append-event bash needle no longer counts as a brain event", () => {
  const summary = summarizeTurn([], ["o2b append-event 'something'"]);
  expect(summary.hadBrainEvent).toBe(false);
});
```

- [ ] **Step 7.2: Run - confirm failures (some tests fail)**

- [ ] **Step 7.3: Update `hooks/lib/detect.ts`**

```ts
const BRAIN_EVENT_NAME_SUFFIX =
  /(?:^|__)(brain_feedback|brain_apply_evidence|brain_note)$/;

const BRAIN_EVENT_BASH_NEEDLES = [
  "o2b brain feedback",
  "o2b brain apply-evidence",
];
```

Update the surrounding doc comment (the §30 §A paragraph) to mention `brain_note` instead of `event_log_append`.

- [ ] **Step 7.4: Run - confirm pass**

Run: `bun test tests/hooks/detect.test.ts`

Expected: PASS for all detector tests.

- [ ] **Step 7.5: Commit (no auto-commit)**

---

## Task 8: Stop guardrail message rewrite

**Files:**
- Modify: `hooks/lib/messages.ts`
- Test: `tests/hooks/stop-log-guardrail.test.ts`

- [ ] **Step 8.1: Failing tests**

Update assertions in `tests/hooks/stop-log-guardrail.test.ts` so:
- Guardrail message no longer mentions `event_log_append`.
- Guardrail message mentions all three (`brain_feedback`, `brain_apply_evidence`, `brain_note`).
- A turn with only a `brain_note` MCP call clears the guardrail.

- [ ] **Step 8.2: Rewrite `stopGuardrailReason` and cadence helpers**

```ts
function stopGuardrailCadenceLine(runtime: HookRuntime): string {
  switch (runtime) {
    case "claudecode":
      return "_This guardrail fires at most once per turn - send another reply (with or without a brain event) to clear it._";
    case "codex":
      return "_This `codex exec` is about to end - call `brain_feedback` / `brain_apply_evidence` / `brain_note` now or finish silently; no further guardrail will fire._";
    case "unknown":
      return "";
  }
}

export function stopGuardrailReason(runtime: HookRuntime = "unknown"): string {
  const cadence = stopGuardrailCadenceLine(runtime);
  const parts: string[] = [
    "Open Second Brain hook: this turn touched files",
    "(Write / Edit / MultiEdit / apply_patch) but did not call any of:",
    "",
    "- `brain_feedback` - new taste correction the user expressed in this",
    "  turn (one signal per file, see the `brain-memory` skill)",
    "- `brain_apply_evidence` - evidence trail when an active preference",
    "  in `Brain/preferences/` scopes to the artifact you just produced",
    "- `brain_note` - one-line narrative milestone (release, merged PR,",
    "  discovered fact) that fits neither category",
    "",
  ];
  if (cadence !== "") parts.push(cadence, "");
  parts.push(
    "Pick whichever fits this turn:",
    "- a new rule the user just stated -> `brain_feedback`",
    "- an active preference applied or violated by the change ->",
    "  `brain_apply_evidence` with `result: applied | violated`",
    "- a durable narrative milestone -> `brain_note`",
    "",
    "If the change is trivial and not worth recording, just send your",
    "reply again - this guardrail fires at most once per turn and the",
    "second Stop passes through silently.",
  );
  return parts.join("\n");
}
```

- [ ] **Step 8.3: Run - confirm pass**

Run: `bun test tests/hooks/`

Expected: PASS for both detect + stop-log-guardrail.

- [ ] **Step 8.4: Commit (no auto-commit)**

---

## Task 9: PostWrite reminder mentions `brain_note`

**Files:**
- Modify: `hooks/lib/messages.ts` (`postWriteReminder`)
- Test: `tests/hooks/post-write-reminder.test.ts`

- [ ] **Step 9.1: Failing test**

Assert the reminder text contains `brain_note`.

- [ ] **Step 9.2: Update `postWriteReminder`**

Insert a paragraph between the existing `brain_apply_evidence` block and the trivial-edits one:

```ts
"If neither a new preference nor an evidence event fits but this turn",
"produced a durable artifact worth referencing later (release, merged",
"PR, discovered fact), call `brain_note` with a one-line description.",
"",
```

- [ ] **Step 9.3: Run + commit (no auto-commit)**

---

## Task 10: Identity-reminder templates

**Files:**
- Modify: `templates/identity-reminder.txt`
- Modify: `templates/identity-reminder.hermes.txt`
- Modify: `templates/identity-reminder.openclaw.txt`
- Modify: `tests/fixtures/identity-reminder/hermes.txt`
- Modify: `tests/fixtures/identity-reminder/openclaw.txt`
- Test: `tests/core/identity-reminder.test.ts`

- [ ] **Step 10.1: Update common template**

`templates/identity-reminder.txt`:

```
[open-second-brain] Identity: @{agent}. After producing a durable artifact - code shipped, bug fixed, config or deployment change, instruction-file edit, content artifact created, research finding or design decision reached, investigation that surfaced a fact future sessions should know - record it through the Brain writer tools: call `brain_feedback` if the turn surfaced a new user preference; call `brain_apply_evidence` (`result: applied | violated | outdated`) if a preference in `Brain/preferences/` scopes to the artifact; call `brain_note` for narrative milestones that fit neither. Skip pure discussion, exploration, read-only queries, and planning that hasn't yet produced an artifact.
```

- [ ] **Step 10.2: Update Hermes template**

`templates/identity-reminder.hermes.txt` - same body as common, with the Hermes-specific note appended:

```
[open-second-brain] Identity: @{agent}. After every durable artifact this turn - code shipped, bug fixed, config or deployment change, instruction-file edit, content artifact created, research finding or design decision reached, investigation that surfaced a fact future sessions should know - record it through the Brain writer tools: call `brain_feedback` if the turn surfaced a new user preference; call `brain_apply_evidence` (`result: applied | violated | outdated`) if a preference in `Brain/preferences/` scopes to the artifact; call `brain_note` for narrative milestones that fit neither. Hermes turns are short; do not batch the calls across turns. Skip pure discussion, exploration, read-only queries, and planning that hasn't yet produced an artifact.
```

- [ ] **Step 10.3: Update OpenClaw template**

`templates/identity-reminder.openclaw.txt`:

```
[open-second-brain] Identity: @{agent}. After every durable in-process action - code shipped, bug fixed, config or deployment change, instruction-file edit, content artifact created, research finding or design decision reached, investigation that surfaced a fact future sessions should know - record it through the Brain writer tools: call `brain_feedback` if the turn surfaced a new user preference; call `brain_apply_evidence` (`result: applied | violated | outdated`) if a preference in `Brain/preferences/` scopes to the artifact; call `brain_note` for narrative milestones that fit neither. OpenClaw has no session boundary; log immediately. Skip pure discussion, exploration, read-only queries, and planning that hasn't yet produced an artifact.
```

- [ ] **Step 10.4: Regenerate fixtures**

Manual: copy the three new template bodies into `tests/fixtures/identity-reminder/hermes.txt` and `tests/fixtures/identity-reminder/openclaw.txt` (or remove `__resetEnvWarnedOnceForTests` cached values to force regeneration). Verify `bun test tests/core/identity-reminder.test.ts` passes.

- [ ] **Step 10.5: Run - confirm pass**

```
bun test tests/core/identity-reminder.test.ts
bun test tests/python/test_hermes_plugin.py
```

Expected: PASS for both.

- [ ] **Step 10.6: Commit (no auto-commit)**

---

## Task 11: Drop `event_log_append` from MCP / OpenClaw

**Files:**
- Modify: `src/mcp/tools.ts`
- Modify: `src/openclaw/index.ts`
- Modify: `plugins/hermes/__init__.py`
- Test: `tests/openclaw/bundle.test.ts`
- Test: `tests/python/test_hermes_plugin.py`
- Test: `tests/mcp/mcp.test.ts`

- [ ] **Step 11.1: Failing assertions**

- `tests/openclaw/bundle.test.ts`: invert any "registers event_log_append" check to "does NOT register event_log_append".
- `tests/mcp/mcp.test.ts`: assert `event_log_append` is absent from the full tool table.
- `tests/python/test_hermes_plugin.py`: drop or invert the equivalent assertion.

- [ ] **Step 11.2: Remove `toolEventLogAppend`**

In `src/mcp/tools.ts`:
- Delete the `async function toolEventLogAppend(...)` block (lines ~256-279 in v0.10.7).
- Delete the explanatory comment block that lives just above `...BRAIN_TOOLS` referencing it.
- Remove `appendEvent` and `validateEventTime` imports if they are no longer used (verify with `tsc --noEmit`).

- [ ] **Step 11.3: Remove the OpenClaw registration**

In `src/openclaw/index.ts`, delete the `api.registerTool({ name: "event_log_append", ... })` block in its entirety (~50 lines). Verify the surrounding tools (`save_note`, `vault_health`) still register cleanly.

- [ ] **Step 11.4: Update Hermes docstring**

In `plugins/hermes/__init__.py`, change the module docstring's tool list from:

```
(`second_brain_status`, `second_brain_query`, `second_brain_capture`, `event_log_append`, `vault_health`)
```

to:

```
(`second_brain_status`, `second_brain_query`, `vault_health`, plus the Brain writer surface)
```

No behavioural change; the Hermes shim only injects the reminder.

- [ ] **Step 11.5: Run - confirm pass**

```
bun test tests/openclaw/
bun test tests/mcp/
bun test tests/python/
```

Expected: all PASS.

- [ ] **Step 11.6: Commit (no auto-commit)**

---

## Task 12: `cmdAppendEvent` identity fix

**Files:**
- Modify: `src/cli/main.ts:313-341`
- Test: `tests/cli/append-event.test.ts` (new)

- [ ] **Step 12.1: Failing test**

Create `tests/cli/append-event.test.ts`:

```ts
import { mkdtempSync, rmSync, writeFileSync, readFileSync, mkdirSync } from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { describe, expect, test } from "bun:test";
import { spawnSync } from "node:child_process";

const O2B = join(import.meta.dir, "..", "..", "bin", "o2b");

describe("o2b append-event identity resolution", () => {
  test("uses config agent_name when --as and env are absent", () => {
    const vault = mkdtempSync(join(tmpdir(), "osb-cli-"));
    const configDir = mkdtempSync(join(tmpdir(), "osb-cfg-"));
    try {
      const configPath = join(configDir, "config.yaml");
      writeFileSync(
        configPath,
        `vault_path: ${vault}\nagent_name: cli-test-bot\n`,
        "utf8",
      );
      mkdirSync(join(vault, "Daily"), { recursive: true });
      const proc = spawnSync(O2B, ["append-event", "hello world"], {
        env: {
          ...process.env,
          XDG_CONFIG_HOME: configDir,
          VAULT_AGENT_NAME: "",
        },
        encoding: "utf8",
      });
      expect(proc.status).toBe(0);
      // The fixture-day path is whatever today is in the configured tz.
      const dailyFiles =
        spawnSync("ls", [join(vault, "Daily")], { encoding: "utf8" })
          .stdout?.trim().split("\n") ?? [];
      expect(dailyFiles).toHaveLength(1);
      const content = readFileSync(join(vault, "Daily", dailyFiles[0]!), "utf8");
      expect(content).toContain("@cli-test-bot");
    } finally {
      rmSync(vault, { recursive: true, force: true });
      rmSync(configDir, { recursive: true, force: true });
    }
  });
});
```

(If the project does not yet have a `tests/cli/` directory, create it with this single file.)

- [ ] **Step 12.2: Run - confirm failure**

Expected: FAIL with `@agent` literal in the Daily content.

- [ ] **Step 12.3: Fix `cmdAppendEvent`**

In `src/cli/main.ts:313-341`:

```ts
async function cmdAppendEvent(argv: string[]): Promise<number> {
  const { flags, positional } = parseFlags(argv, {
    vault: { type: "string" },
    as: { type: "string" },
    date: { type: "string" },
    time: { type: "string" },
    config: { type: "string" },
  });
  if (positional.length < 1) {
    process.stderr.write("error: append-event requires a message argument\n");
    return 2;
  }
  const message = positional[0]!;
  const config = (flags["config"] as string | undefined) ?? defaultConfigPath();
  const vault = requireVault(flags["vault"] as string | undefined, config);
  const tz = resolveTimezone(config);
  const explicit = flags["as"] as string | undefined;
  const agent = explicit ?? resolveAgentName(config);

  let path: string;
  try {
    path = await appendEvent(vault, agent, message, {
      date: (flags["date"] as string | undefined) ?? null,
      time: (flags["time"] as string | undefined) ?? null,
      tz,
    });
  } catch (exc) {
    process.stderr.write(`error: failed to append event: ${(exc as Error).message ?? exc}\n`);
    return 1;
  }
  process.stdout.write(`appended: ${resolve(path)}\n`);
  return 0;
}
```

Confirm `resolveAgentName` is imported at the top of the file (line ~18 already imports it).

- [ ] **Step 12.4: Run - confirm pass**

Run: `bun test tests/cli/append-event.test.ts`

Expected: PASS.

- [ ] **Step 12.5: Commit (no auto-commit)**

---

## Task 13: `hooks/README.md` rewrite

**Files:**
- Modify: `hooks/README.md`

- [ ] **Step 13.1: Read current README to scope the rewrite**

Run: `wc -l hooks/README.md`. The current file is ~120 lines; the rewrite preserves the "Files" and "How both runtimes find the hook commands" sections; the "What the hooks do" table is rewritten; a "JSONL sidecar" paragraph is added.

- [ ] **Step 13.2: New "What the hooks do" table**

```markdown
| Event         | Matcher                              | Behaviour                                                                                                                                |
|---------------|--------------------------------------|------------------------------------------------------------------------------------------------------------------------------------------|
| `PostToolUse` | `Write|Edit|MultiEdit|apply_patch`   | Emits `additionalContext` reminding the agent to call `brain_feedback`, `brain_apply_evidence`, or `brain_note` for a durable artifact.   |
| `Stop`        | (every Stop)                         | If the turn produced a durable artifact but no `brain_feedback` / `brain_apply_evidence` / `brain_note` call, returns `decision: "block"` once, then lets the second Stop pass. |
```

- [ ] **Step 13.3: Replace the introductory paragraph**

Remove the reference to `event_log_append`. New text:

```markdown
Plugin-bundled lifecycle hooks for Claude Code and Codex. The hooks
exist to make sure every turn that produces a durable artifact records
the corresponding event in `Brain/log/<date>.md` (and its JSONL sidecar).
Brain/log/ is the single agent-facing log surface; `Daily/` remains for
human-CLI use (cron-jobs, ad-hoc scripts) via `o2b append-event`.
```

- [ ] **Step 13.4: Add the JSONL sidecar paragraph**

After the table, add:

```markdown
## JSONL sidecar

Every event the writer lands in `Brain/log/<date>.md` is mirrored to
`Brain/log/<date>.jsonl` in the same atomic step. Machine consumers
(discipline-report today, future tooling tomorrow) read JSONL through
`src/core/brain/log-jsonl.ts:readLogDay`, which falls back to parsing
markdown on historical days that pre-date v0.10.8. Hand-editing the
markdown does not break the reader; deleting the JSONL forces the
fallback path until the next write rebuilds it.
```

- [ ] **Step 13.5: Manual inspection**

Run: `bun src/cli/main.ts --help` (no functional check; the README is documentation only). Confirm the file renders without dangling references to `event_log_append`.

- [ ] **Step 13.6: Commit (no auto-commit)**

---

## Task 14: Update `_summary.md` and CHANGELOG

**Files:**
- Modify: `Projects/OpenSecondBrain/Features/_summary.md` (in vault, `/root/vault/`)
- Modify: `CHANGELOG.md`

- [ ] **Step 14.1: Mark §32 shipped**

In `/root/vault/Projects/OpenSecondBrain/Features/_summary.md`, change the heading of §32 (around line 275) from:

```
### 32. Retire `event_log_append` for agent-facing surface, single source = `Brain/log/` — 8
```

to:

```
### ✅ 32. Retire `event_log_append` for agent-facing surface, single source = `Brain/log/` — 8 *(implemented in v0.10.8: §32A lightweight verification gate; §32B brain_note tool in the always-loaded writer scope; §32C-D drop event_log_append from stop-guardrail / detector / identity-reminder; §32E hooks/README.md rewritten; §32F cmdAppendEvent resolves identity via resolveAgentName; §32G full handler drop from all runtimes (Claude/Codex/Hermes/OpenClaw); §32H operator-driven drift-check, no auto-watcher)*
```

- [ ] **Step 14.2: Mark §23 shipped**

In the same file, change §23 (line 224) from:

```markdown
### 23. Structured JSONL log alongside Markdown — 6
```

to:

```markdown
### ✅ 23. Structured JSONL log alongside Markdown — 6 *(implemented in v0.10.8: Brain/log/<date>.jsonl next to the .md, atomic dual-write via a proper-lockfile lock; src/core/brain/log-jsonl.ts:readLogDay - JSONL-preferred reader with a markdown fallback; discipline-report switched over; no backfill - the fallback serves historical days)*
```

- [ ] **Step 14.3: Append Deferred-work entries**

In `## Deferred work`, add (in the same style as existing entries):

```markdown
- **§32B CLI verb `o2b brain note`.** The MCP tool shipped in v0.10.8, but the CLI verb for the cron-job / shell-script use-case is deferred. Return trigger: the first time a note must be written outside MCP (cron, ad-hoc script). The bash needle for the detector is added together with the CLI verb.

- **§32H auto-watcher for Daily/ entries from @agent-identity.** The operator check (`o2b discipline report` + grep over Daily/) is sufficient for now. Trigger: drift-check finds recurring cases the operator pass missed.

- **§23 one-shot JSONL backfill of historical markdown days.** The lazy fallback in readLogDay serves all pre-v0.10.8 days without a sidecar. Trigger: a machine consumer materially benefits from JSONL for days older than v0.10.8 (none named yet).
```

- [ ] **Step 14.4: Update `CHANGELOG.md`**

Insert a new `## [0.10.8] - <ISO date at ship>` section above the `[0.10.7]` block. Use this template (no `[Unreleased]` section, no version dates inside planning):

```markdown
## [0.10.8] - <ISO date at ship>

Closes §32 (retire `event_log_append` from every agent-facing surface
in favor of `Brain/log/` as the single source of truth) and §23 (write
a JSONL sidecar `Brain/log/<date>.jsonl` next to every markdown log
day, with discipline-report switched to read JSONL when present and
fall back to markdown when not). Companion design and impl plan at
`docs/plans/2026-05-19-v0.10.8-design.md` and `-impl.md`.

### Added

- §32B - `brain_note` MCP tool in the always-loaded writer-server
  scope. Records one narrative-milestone line under event kind `note`
  in `Brain/log/<today>.md` (+ JSONL sidecar). Text is run through
  `sanitiseTextField` (newline-collapse, secret redaction, 4096-char
  cap). Resolver chain: explicit `agent` argument > `resolveAgentName`
  from config. Discipline-report's `AgentCounts` gains a `note` field.
- §23 - `Brain/log/<date>.jsonl` written atomically next to
  `Brain/log/<date>.md` under a `proper-lockfile` lock. Each row
  carries `{ts, kind, payload}`; the payload is a one-to-one
  projection of the markdown bullet body.
- §23 - `src/core/brain/log-jsonl.ts:readLogDay` - the new
  JSONL-preferred reader. Returns `{entries, source, warnings}`;
  source is `"jsonl"` or `"markdown-fallback"`. Malformed JSONL lines
  surface as warnings, not errors.

### Changed

- §32C-D - the Stop guardrail and the PostToolUse reminder list three
  triggers (`brain_feedback`, `brain_apply_evidence`, `brain_note`) -
  no `event_log_append`. `hooks/lib/detect.ts` regex and bash needles
  match the same set.
- §32D - the three identity-reminder templates (common, Hermes,
  OpenClaw) point agents at the Brain writer tools; no
  `event_log_append`. Fixture files regenerated.
- §32E - `hooks/README.md` rewritten to describe v0.10.8 semantics
  (Brain/log/ as single agent-facing log; Daily/ as human-CLI;
  JSONL sidecar paragraph added).
- §32F - `cmdAppendEvent` (`o2b append-event` CLI verb) resolves the
  agent through `resolveAgentName` instead of the literal `"agent"`
  fallback. Cron-jobs that already set `VAULT_AGENT_NAME` are
  unaffected; cron-jobs that relied on the literal get the
  config-declared identity.
- §32G - `event_log_append` MCP tool removed from every runtime:
  the OpenClaw `registerTool` block, the `toolEventLogAppend` handler
  in `src/mcp/tools.ts`, and the Hermes docstring mention. The bare
  `appendEvent` function in `src/core/event-log.ts` stays for the
  CLI verb and any future shell-side use.
- §23 - discipline-report (`src/core/discipline/log-counts.ts`)
  reads through `readLogDay`. Same return shape; counts now include
  the new `note` kind.

### Migration

No vault data migration required. Historical `Brain/log/<date>.md`
files without a sidecar JSONL are served by the reader's
markdown-fallback path; the next write on any date produces both
files. No installer changes; no schema bump in `_brain.yaml`.

The Stop guardrail no longer clears on a bash `vault-log` /
`o2b append-event` invocation - the operator/agent must use one of
the three brain-event tools or finish the turn silently. Cron-jobs
that use `o2b append-event` keep working; they just no longer count
as a "brain event" for the guardrail.

### Release-checklist gate

Before shipping v0.10.8 from the working branch:

1. `bun test` - all suites pass.
2. `o2b discipline report` on `/root/vault` - status `ok` or `info`;
   every `known_agent` with activity since v0.10.7 has at least one
   brain event recorded.
3. Smoke: an MCP `brain_note` call produces both `Brain/log/<today>.md`
   and `Brain/log/<today>.jsonl`, with the same `ts`, `kind`, and
   payload keys.
4. Manual MCP probe: `event_log_append` is absent from `bun
   src/mcp/main.ts` (full), `--scope writer`, OpenClaw bundle, and the
   Hermes tool list.
```

- [ ] **Step 14.5: Commit (no auto-commit)**

---

## Task 15: MEMORY housekeeping

**Files:**
- Delete: `/root/.claude/projects/-root/memory/feedback_daily_log_strict_russian.md`
- Delete: `/root/.claude/projects/-root/memory/project_o2b_log_language.md`
- Modify: `/root/.claude/projects/-root/memory/feedback_always_russian.md`
- Modify: `/root/.claude/projects/-root/memory/feedback_no_em_dashes.md`
- Modify: `/root/.claude/projects/-root/memory/MEMORY.md`

- [ ] **Step 15.1: Delete the strict-Russian feedback file**

Run: `rm /root/.claude/projects/-root/memory/feedback_daily_log_strict_russian.md`

- [ ] **Step 15.2: Delete the o2b log-language project file**

Run: `rm /root/.claude/projects/-root/memory/project_o2b_log_language.md`

- [ ] **Step 15.3: Rewrite `feedback_always_russian.md`**

Replace contents:

```markdown
---
name: Reply in the user's current dialog language
description: Default chat language follows the user's dialog; repo artifacts (code, docs, commits) stay English per project convention.
metadata:
  type: feedback
---

Reply in the language the user is currently speaking in this dialog. For
the current operator the observed default is Russian, but this is an
observation about the user, not a hard-coded principle. If the user
switches dialog language, follow.

**Why:** Hard-coded language anchors (`feedback_daily_log_strict_russian`)
were rejected on 2026-05-19 as a class - the user explicitly asked that
all such anchors be removed or reframed as "language of the user".

**How to apply:**
- Chat, explanations, clarifying questions -> the language the user is
  speaking now.
- Repository artifacts (CLAUDE.md, AGENTS.md, code, docstrings,
  CHANGELOG, test descriptions, commit messages, PR titles/bodies) ->
  English, per the existing project convention.
- When mirrored docs (CLAUDE.md <-> AGENTS.md) get edited, the mirror
  rule (`feedback_mirror_claude_agents.md`) still wins; do not switch
  one mirror to a different language.
```

- [ ] **Step 15.4: Rewrite `feedback_no_em_dashes.md`**

Replace contents:

```markdown
---
name: Use the typographic conventions of the dialog language
description: Replace em-dashes with hyphen-with-spaces in prose written in languages where em-dash is not idiomatic.
metadata:
  type: feedback
---

When writing prose in a language where em-dash (`—`, U+2014) is not the
idiomatic typographic choice, use a regular hyphen surrounded by spaces
(` - `) instead. For the current operator this applies to Russian prose
(posts, documentation, chat replies, code comments written in Russian).
English prose and technical identifiers are unaffected; em-dash stays
where it is idiomatic.

**Why:** The user removed em-dashes from a drafted blog post and asked
to keep them out of all subsequent prose in the same language. The
principle is typographic, not language-bound: any language where
em-dash is not the convention falls under the same rule.

**How to apply:** when generating prose, choose the dash style by
language convention. Markdown posts, chat replies, descriptions, and
in-line comments in non-em-dash languages get ` - `. Technical
documentation and code stay as-is.

Related: [[feedback_always_russian]] (dialog language is the
default selector). Brain preference candidate: `pref-no-em-dashes`.
```

- [ ] **Step 15.5: Update MEMORY index**

Modify `/root/.claude/projects/-root/memory/MEMORY.md`:
- Remove the line `- [Daily/ entries - always log, always Russian](feedback_daily_log_strict_russian.md) ...`.
- Remove the line `- [Open Second Brain log language - future task](project_o2b_log_language.md) ...`.
- Update the line for `feedback_always_russian.md` to read:
  `- [Reply in dialog language](feedback_always_russian.md) - default to whatever language the user is currently speaking; repo artifacts stay English by project convention.`
- Update the line for `feedback_no_em_dashes.md` to read:
  `- [Typographic conventions per dialog language](feedback_no_em_dashes.md) - replace em-dash with hyphen-with-spaces in languages where em-dash is not idiomatic (currently relevant for Russian prose for this user).`

- [ ] **Step 15.6: Sanity check**

Run: `ls /root/.claude/projects/-root/memory/feedback_*` to confirm the two deleted files are gone and the two rewritten ones exist.

- [ ] **Step 15.7: No commit step - MEMORY is operator-private, not part of the project repo.**

---

## Task 16: Verification gate (§32A) - operator step

**Files:** none (release-checklist step).

- [ ] **Step 16.1: Run discipline report**

Run: `o2b discipline report --window since-v0.10.7`
(If the flag does not exist, run `o2b discipline report` with the default 1-day window and the operator manually expands to "since 2026-05-18" via repeat invocations.)

- [ ] **Step 16.2: Inspect output**

For each agent name in `_brain.yaml:discipline_report.known_agents` that shows non-zero activity, confirm the brain-event count is ≥ 1. Note any agent with activity but zero events; do not proceed to step 17 until each such gap has either:
- a recorded brain event (after a follow-up that legitimately required one),
- or an explicit operator note in this task's checklist explaining why the agent has activity but no brain event in the window.

- [ ] **Step 16.3: Verdict**

Document the gate outcome in the v0.10.8 commit message body (or PR description if applicable). Pass: proceed to release. Fail: open a new task track investigating the failing agent's logging path; do not ship.

---

## Task 17: Smoke test on the working branch

**Files:** none (operator step).

- [ ] **Step 17.1: Restart the local MCP servers**

Restart Hermes (`hermes gateway restart`), and reload Claude Code / Codex sessions in the working repo. Wait for the standard `Plugin discovery complete` log line in `/root/.hermes/logs/agent.log`.

- [ ] **Step 17.2: Probe each runtime**

For each of Claude Code, Codex, Hermes, OpenClaw, manually trigger a `brain_note` call (the simplest probe: a one-line `brain_note({ text: "v0.10.8 smoke" })` from each agent). Confirm:
- The event lands in `/root/vault/Brain/log/<today>.md` and `.jsonl`.
- The agent identity in both files matches the runtime's configured `agent_name` (no `@agent` literal).

- [ ] **Step 17.3: Probe each runtime for the absence of `event_log_append`**

In each runtime, attempt to call `event_log_append` (or scan the runtime's published tool list). Confirm the tool is not advertised. The expected failure mode is "unknown tool" - not a runtime crash.

---

## Self-Review

Spec coverage: every numbered spec subsection (§4.1 - §4.11) maps to one
or more tasks above. The tasks cover §32A (Task 16), §32B (Tasks 4, 5,
6), §32C+D (Tasks 7, 8, 9, 10), §32E (Task 13), §32F (Task 12), §32G
(Task 11), §32H (Task 16/17 operator step), §23 (Tasks 1, 2, 3), and
the MEMORY housekeeping side track (Task 15). Task 14 captures the
release-artifact updates (CHANGELOG, _summary.md). Task 17 is the
post-merge smoke. No spec section is uncovered.

Placeholder scan: no `TBD`, `TODO`, or "Add appropriate error handling"
in the plan. Every code block is complete; every test block shows the
actual assertion code. No "similar to Task N" without repeating the
code.

Type consistency: `BrainLogEntry`, `BrainLogParseWarning`, and
`BrainNoteLogEvent` names are used consistently. `readLogDay` and
`appendLogEvent` signatures match across Tasks 1-3. `AgentCounts.note`
is added in Task 6 and used in Task 6's test. `WRITER_TOOL_NAMES` in
Task 5 is the same set name as in `src/mcp/tools.ts`. `BRAIN_TOOLS` is
the same array name used by the existing brain tools.
