Commit Graph
889 Commits
Author SHA1 Message Date
Garry TanandClaude Fable 5 c2cc8b0207 v0.45.1.0 feat(context): hook-lane feedback loop + cross-turn dedupe for the harness hooks (#4025)
* refactor(context): extract pure gateVolunteeredPointers (no behavior change)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(context): volunteer-shaped resolve IPC — channel/cwd/volunteer fields, handler-result widening, server-side gate + validated source + delivery-point logging; listener on both engines

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(cli): gbrain volunteer-hook — one-shot harness hook entry (IPC-first; Postgres-only fallback; startup-hook-exempt)

T0 evidence: a UserPromptSubmit hook's additionalContext is recorded in the
session transcript as a structured hook_additional_context attachment and
transcript_path is stable across turns — cross-turn dedupe reads ONLY those
attachments (never raw tail substrings). Live smoke: seeded brain + running
serve → pointer injected via IPC at 0.43s wall, turn 2 suppressed, one event
banked channel=claude-code.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(recipes): claude-code-reflex + codex-reflex (experimental) hook adapters

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(doctor): volunteer_channels per-adapter visibility

Engine-aware sibling of retrieval_reflex_health: groups
context_volunteer_events by channel (7d) so operators can see which push
adapters fire; message separates installed-but-unregistered from
registered-but-quiet; pre-v117 tolerant; info-only status.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(context): hook-lane feedback loop + cross-turn dedupe — converge cathedral-3 onto the #3975 harness hooks

The #3975 hook lane volunteered pages invisibly (nothing logged to
context_volunteer_events) and re-volunteered the same page every turn an
entity was mentioned (no priorContextText). This convergence ports the
cathedral-3 branch's two gap-fills into the blessed lane and retires its
parallel surface:

- transcripts: parseTranscript extracts injectedContextBlocks (structured
  hook_additional_context attachments — the T0-verified recorded shape of
  our own injections; never raw-turn substring matching)
- hook user-prompt: feeds those blocks as priorContextText (volunteer once
  per session, not once per mention) + sends a feedback-loop channel
  (--harness <claude-code|codex>, default claude-code); hook joins
  STARTUP_HOOK_SKIP_COMMANDS (per-prompt check-update spawn class, grep-pinned)
- turn-context: result exposes post-trim volunteered survivors
- resolve-ipc: additive TurnContextRequest.channel + onTurnContextDelivered
  seam (fires post-write only — abandoned blocks are never counted)
- serve: delivery callback logs volunteered pages + pointers per channel
- doctor: volunteer_channels check (per-channel 7d visibility; separates
  installed-but-unregistered from registered-but-quiet; pre-v117 tolerant)

Retired (never released): gbrain volunteer-hook, hook-adapters.ts,
claude-code-reflex + codex-reflex recipes, the volunteer-shaped resolve wire
extension. Kept: gateVolunteeredPointers extraction, VolunteerChannel
claude-code/codex values, the real hook-transcript fixture.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs: regenerate llms bundles for the hook-lane doc updates

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test: coverage for the hook-lane delivery wiring

Extract logTurnContextDeliveryFireAndForget into volunteer-events.ts so the
shipped serve callback is unit-testable (channel fallback, sessionId clamp,
pointer+page rows in ONE synchronous registration — the double dynamic import
could register the pending write too late to survive process exit). Direct
units for isVolunteerChannel, explicit-channel logDeliveredReflexPointers,
and the pure gate (idempotency pin; stale parity claim in its doc corrected).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: pre-landing review fixes — 7-reviewer batch

Multi-confirmed: volunteer_channels now runs on the LOCAL doctor path too
(buildChecks; docs pointed there); transient DB errors no longer misreported
as pre-v117; priorContextText deduped + 32KB-capped hook-side. Red team:
delivery counts reconciled against the hook heartbeat (CAUTION when mostly
degraded — write-accept isn't injection); IPC clamp drops advisory
priorContextText BEFORE any window turn; foreign hook_additional_context
blocks (no gbrain marker) excluded from dedupe input; engine-aware quiet
guidance (Postgres brains aren't told to chase hook registration); 2MB dedupe
horizon documented. Security: wire channel claims restricted to harness
channels. DRY: VOLUNTEER_CHANNELS single source, shared reflexPointerRationale,
SESSION_ID_MAX_LEN shared with the op clamp; dead channel param dropped from
logDeliveredReflexPointers. Tests: budget-trim volunteered invariant under
real trim pressure, poll/ordering-based delivery-seam tests (no fixed sleeps),
doctor branch + engine-parity-row tests, wiring source pins, cap pins;
fixture pruned to load-bearing lines (environment inventory removed).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: adversarial-review fixes — cross-model P1 + Claude adversarial batch

P1 (codex structured review + Claude adversarial, cross-model): the remote
run_doctor path now threads the caller's source scope into
volunteer_channels — a source-bound token can no longer see other sources'
activity counts/timestamps (must-never-violate source-isolation rule);
local trusted doctor stays brain-wide. Claude adversarial: IPC connection
handler gains a one-request re-entrancy guard (trailing bytes mid-await
double-processed the same line → duplicate handler work + duplicate
delivery logging); partial client-side trims now heartbeat as reason
'trimmed' (delivery-count drift visibility); heartbeat reconciliation uses
the same 7-day window as the counts + a 5-sample floor; one oversized
dedupe block no longer evicts all smaller ones (continue, not break);
quiet-channel guidance no longer claims old serves attribute hook traffic
to reflex (they log nothing); 'transient' claim softened to evidence;
marker-filter + unknown-channel-default comments state their honest limits.
Pins: scoped-query SQL/params, re-entrancy single-fire, updated wiring grep.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore: bump version and changelog (v0.45.1.0)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore: restamp bootstrap runbook + regenerate vendored template repo for v0.45.1.0

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs: update project documentation for v0.45.1.0

Post-ship /document-release pass: fold the post-doc review fixes into the
reference docs (KEY_FILES: source-scoped remote volunteer_channels + heartbeat
reconciliation, 32KB-capped dedupe input, marker filter, IPC one-request
guard + clamp order), fix BRAINBENCH's now-stale memoryless-hook rationale,
and mark the doctor-check half of the push-context TODO shipped.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs: cross-model doc-review fixes for v0.45.1.0

Codex doc review against the shipped diff: push-context channel table gains
the harness-hook row (three-channels claim was stale), the guide states the
PGLite-socket topology limit and the bounded dedupe horizon, delivery-point
claims are hedged to match the code (write-accept isn't proof of injection;
the doctor reconciles against the hook heartbeat, which can't inspect
registration), BRAINBENCH's future-tense hooks-PR sentence updated now the
hook has landed, and the adapter-flip + resolver-channel TODOS reflect the
current lane count. llms-full.txt regenerated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs(hook): document --harness in the user-prompt usage text

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
v0.45.1.0
2026-08-11 22:45:44 -07:00
Garry TanandClaude Fable 5 d35c9c9e44 v0.45.0.0 feat(bootstrap): paste-in personal-agent install for Codex + Claude Code (#3975)
* docs(designs): agent-bootstrap plan + design docs (normative, review-absorbed)

The scrubbed, in-repo sources of truth for the gbrain bootstrap wave:
AGENT_BOOTSTRAP_DESIGN.md (product scope/sequencing) and
AGENT_BOOTSTRAP_PLAN.md (implementation; all review-finding IDs inlined).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(bootstrap): format spec, question bank, identity templates, bundled assets

agent.json manifest (format_version 1, initialized sentinel) + machine-local
install receipt [CX2-1, CX2-12]; 12-question/6-required interview bank with
consent keys and a persist:false sink for the optional provider key [CX2-13];
ten {{TOKEN}} identity templates (generic, adapted to gbrain ops — gates call
recall/query/put_page, write-through-ops rule, keyless agent-authored facts,
silence contract); assets embedded compiled-binary-safe via file-type imports
[ENG-6].

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(bootstrap): runbook, README paste block, bootstrap guide, TODOS entries

BOOTSTRAP_FOR_AGENTS.md (agent-driven install runbook: CLI phase list is the
source of truth, never-invent rules, Codex approvals preflight, keyless posture,
failure-modes table, version stamp for the skew check); README gains the
full-agent paste block pinned to latest-stable inside the Claude Code/Codex
quick start (memory-only tier stays); docs/guides/bootstrap.md carries the full
install/security/consent/degradation/uninstall contract.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs(designs): spike instrument for the bootstrap wave (build order 0)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(bootstrap): interview + render engines

Interview gate with read-back confirm-hash (any later answer change clears the
confirmation — the hostile single-batch case is structurally impossible),
per-answer provenance, caps + escaping at set time, config-sink routing for the
provider key; renderer with hard-fail token sweep, subordinate fencing of
principal input, never-clobber + backups, deterministic minimal mode for the
template repo, scaled byte floors. 58 unit tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(bootstrap): private-repo lifecycle — repo create, attach, uninstall, run lock

gh-gated private repo creation with API-verified privacy (rate-limit distinct
from public), refuse-foreign-origin with attach as the sanctioned path, atomic
bootstrap mutex (pid liveness + age + token), receipt-keyed uninstall that
never wholesale-deletes the gbrain home and only offers --delete-brain for a
brain it created; read-only PGLite lock probe (never opens the engine).
54 unit tests, injectable exec seam throughout.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(release): latest-stable ref, template-repo publish job, bootstrap CI guards

release.yml advances the latest-stable tag only after assets publish (the paste
block's permanent ref — copies in the wild never rot) and gains a PAT-gated
publish-template job verified against the vendored tree; two skip-graceful
guards (sanctioned-ref + runbook stamp; template/token bijection + placeholder
assertion + generator byte-diff) wired into verify; README + runbook re-admitted
to the CI cache hash; vendored deterministic template tree generated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(context): IPC v2 turn_context + 8KB assembly + visibility resolver + session identity

Discriminated-union IPC with handler map, protocol echo (stale-serve detection),
shared-secret gate, server-side source binding, per-kind budgets; turn-context
assembly (reflex pointers + volunteered pages + world-only hot facts) under a
data-not-instructions envelope trimmed to the harness's 10KB hook-output cap;
facts.default_visibility resolved through one helper at all four sites (explicit
caller wins, typos fail closed); typed sessionId threads _meta.session_id into
the hot-memory cache key. 50 new tests; 180 adjacent tests confirmed green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(persistence): secret-scan, gbrain sources push, durability unification

Pattern secret scanner (own runtime allowlist, redacted previews, corpus-write
redaction mode); sources push runs the whole scan→stage→commit→pull→push
sequence under one cross-platform lock (mkdir-atomic, pid+age+token) with a
deny-glob backstop, commit-first divergence-safe pull, refuse-unverifiable
visibility, and push-status telemetry; gbrain-home choke point unifies
GBRAIN_HOME semantics with config (0700); durability is parent-repo-aware and
rotates its push log at 0600. 35 new tests; 200 existing green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(sources): harden/pull gates accept sources inside a parent git repo

The bootstrap workspace registers brain/ (a subdirectory) as the source; the
durability core already resolves the repo root, so the command gates now check
inside-a-repo rather than .git-right-here [CX2-3].

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(serve): resident maintenance sweep + keyless capability probe

The lock-owning serve process now closes the persistence loop: startup (3s
post-connect, best-effort, unref'd) and idle (10-min quiet intervals through
the injectable timer seam) sweeps run facts-fence reconciliation, deterministic
link/timeline extraction over recent workspace pages, and spend-gated corpus
ingest (skipped keyless — agent-authored fences cover it). gbrain sweep --once
is the trusted CLI seam bootstrap verify uses. Capability probe renders the
honest keyless/keyed report. Full reuse of the cycle extractor + extract cores;
26 new tests, neighbors green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(hooks): engine-free gbrain hook command, settings writers, transcript parser

Four hook events (session-start digest + crashed-session recovery push,
user-prompt turn-context injection under an 800ms deadline and the 10KB cap,
stop buffers, session-end corpus write with redaction/retention/dedup +
best-effort push); structural JSON settings merger keyed by a _gbrain marker
(foreign hooks and permissions survive); dated host-spec registry; Claude Code
.jsonl parser as a spec-target with a scrubbed 7-shape fixture. Heartbeat is
counters-only by construction. 59 tests; zero engine modules in the import
graph.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs(bootstrap): cross-link the full-agent path from the connection docs

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(bootstrap): dispatcher, verify, status — the command assembled

gbrain bootstrap {status,interview,render,repo,hooks,verify,uninstall,attach}:
engine-free except verify (owns its engine, in-process sweep — no live-serve
conflict); phase list is the TS source of truth with install.jsonl telemetry
and the support blob; verify's fail-soft check suite covers the real write path
(put_page → write-through file → sweep → graph floor → recall), passes keyless,
persists snapshots, and ends with the first-run tour. cli.ts wired per the
three-touchpoint rule; doctor gains the bootstrap check group (silent on
machines with no bootstrap state). 28 new tests; 353 adjacent green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs: KEY_FILES bootstrap cluster + CLAUDE.md dispatcher row (+ build:llms)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(bootstrap): e2e pins — hook-under-live-serve, attach, degraded modes, compiled binary, Docker harness

The permanent pins: a real serve holds the PGLite lock while the engine-free
hook completes (and a direct engine open provably throws LiveServeLockError);
stale-socket fail-open; machine-2 attach with marker-keyed hook repair;
decline-everything installs verify green with every degradation named; the
compiled binary renders bundled templates in an empty cwd. Offline Docker
harness (networkless, read-only) gated into heavy-tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(bootstrap): register doctor check categories + system-of-record allow comments

The six bootstrap doctor checks join OPS_CHECK_NAMES; the sweep's batch link/
timeline inserts carry the explicit extract-path allow comments (the sweep IS
the extraction path for workspace pages).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(test): shard wedge cap tracks suite growth (1500s -> 1800s)

At ~9000 tests a healthy shard finished at 1466s and two progressing shards
were false-killed at the old cap; 1800s restores ~25% headroom over the
slowest observed healthy shard. Real hangs still hit it.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(ci): cache-hash policy — README + runbook edits must invalidate [C2]

The old deny-list assertion predates the paste block; README.md and
BOOTSTRAP_FOR_AGENTS.md are policy-doc re-admissions now, so their edits must
change the hash (a paste-block edit shipping under a cached green was the C2
hole).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(test): classify post-suite exit-hangs as warn-pass; file the leak forensics

A shard killed by the wedge watchdog with every assigned file started and zero
fail markers did all its work and leaked a handle at exit — pre-existing and
master-reproducible (P1 TODO carries the full bisect forensics). Bun's per-test
timeout turns a hung test into a (fail), so the classifier cannot mask one.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(test): shard cap 2400s — the count-balanced heavy shard needs it under contention

Observed: the heavy shard still progressing 22s before an 1800s kill while
siblings finish at 1150-1550s (split balances file count, not weight). Filed
the load-sensitive WAL-repair flake (pre-existing, master's v0.42.75.0 wave).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(bootstrap): quarantine env-mutating suites to the serial lane

check-test-isolation R1: six new files mutate GBRAIN_HOME/env at module scope —
the serial lane (one process per file) is the guard's prescribed home for them.
All 114 tests pass post-rename.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs(readme): per-harness install sections — Codex, Claude Code, then OpenClaw/Hermes

Each harness gets its own complete paste-block section (desktop app first,
terminal noted — Claude Code CLI is the identical harness; Codex CLI works
pull-based today); the OpenClaw/Hermes platform path keeps equal weight with
its one-click deploys and INSTALL_FOR_AGENTS block intact; memory-only and
remote-connect tiers consolidated under 'Lighter ways in'. Supersedes the
review's D5 ordering by user direction; stale heading references updated.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs(readme): Codex as the recommended first step; OpenClaw/Hermes framed as-intended, high-cost

The install section now routes newcomers explicitly: Codex first
(subscription-priced, nothing to deploy), OpenClaw/Hermes as GBrain used the
way it was designed — always on, at real server + API cost.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: doc-audit code chasers — broken recovery hints, stale op description, auto_link key

Four small code fixes surfaced by the markdown accuracy audit:
- doctor's auto-RLS recovery hint pointed at `apply-migrations --force-retry 35`,
  which cannot work (--force-retry targets the vX.Y.Z orchestrator registry, not
  the numeric schema MIGRATIONS array). Hint now points at the recreate SQL in
  docs/guides/rls-and-you.md; test pins against regression.
- v0_11_0 migration printed the same broken-mechanism class of hint
  (`config set minion_mode` writes DB config nothing reads); now names
  `apply-migrations --mode` + preferences.json, the real setter.
- submit_job's op description hardcoded a stale handler list; now points at
  registerBuiltinHandlers as the source plus the --follow discovery trick.
- `auto_link` added to KNOWN_CONFIG_KEYS: read by link-extraction, reconcile-links,
  and sweep, and documented as the off-switch in brain-ops/maintain, but the
  allowlist rejected `config set auto_link false`.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs: repo-wide accuracy + MECE reform from the 9-bucket markdown audit

A code-grounded audit of every markdown file (root, architecture, guides,
mcp, tutorials, docs-root, operations/eval/designs, skills, recipes) followed
by a fix wave with per-bucket ownership. Four classes of change:

Accuracy — every documented command/flag verified against src/ before writing:
dead commands replaced with working ones (pages purge-deleted, jobs watch
--follow, gbrain restore, import-based Obsidian flow, space-separated --scopes,
real thin-client recipes, working isolation verification, real supervisor
restart procedure, curl-based ngrok health check, real minion_mode setter);
count drift fixed with rot-proof phrasing (100+ ops, 50+ bundled skills via
skills/manifest.json, 140+ engine methods, KNOBS_HASH_VERSION pointer instead
of hardcoded versions); stale claims corrected (search-mode defaults, RETRIEVAL
pipeline order incl. autocut, sentinel rules, refusal-list mechanism, engine
snapshot, shard cap 2400s + EXIT-HANG classifier in TESTING.md, latest-stable +
publish-template documented in RELEASING.md as release.yml promises).

MECE — one home per concept, pointers elsewhere: test isolation → TESTING.md;
OAuth registration + --bind/--public-url lore → DEPLOY.md; mode bundles →
guides/search-modes.md (the home the CLAUDE.md dispatcher always promised);
merge contract → schema-packs.md; WAL ladder → ENGINES.md; quiet-hours →
quiet-hours.md; capture taxonomy → entity-detection.md; person-page taxonomy →
compiled-truth.md; brain-first protocol → brain-first-lookup.md; refresh
semantics → refresh-algorithm.md; KEY_FILES.md deduplicated (58 extension
entries merged, one entry per file); infra-layer.md rewritten as a pointer page.

Privacy — placeholder sweep across guides, docs, skills, and recipes per the
iron rule; per-release narration stripped from reference docs (current-state
prose only).

Bootstrap coverage — AGENTS.md pointer, RESOLVER routing row, INSTALL.md path,
tutorial cross-links, keyless-mode sections in spend-controls/headless-install.

skills.lock.json regenerated; llms.txt/llms-full.txt rebuilt. Gates: verify
36/36, typecheck clean, doctor 96/96, skills-integrity + resolver + build-llms
+ config-set + migrations all green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs(readme): refresh production-brain stats to current brain-repo counts

155,795 pages / 24,589 people / 5,340 companies, counted from the brain
repo's current HEAD; the "100K-page brain" framing moves to 150K to match.
Cron-fleet count unchanged (its store lives on the deployment host, not in
the repos available for verification).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(scan,push): modern OpenAI/Voyage key patterns, scan staged blobs not disk

- secret-scan matches sk-proj-/sk-svcacct-/sk-None- and pa- Voyage keys
  (the bare sk- pattern missed every current OpenAI key format).
- workspacePush stages first, then scans the staged index blobs via
  git cat-file, closing the scan-then-stage TOCTOU where a file changed
  between snapshot and commit shipped unscanned.
- shared binary-sniff helper, memoized glob regexes, atomic push-status
  write, and tests for pull_conflict + gitignored deny-match paths.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(ci): regenerate flag registry for new commands, harden shard classifier + release token

- cli-flag-registry.generated.ts regenerated: bootstrap/hook/sweep and
  sources push --message/--allow-unverified-remote were missing, so the
  strict #2185 validator rejected real invocations and skipped the new
  commands entirely.
- EXIT-HANG shard classifier now requires every assigned file to have
  started before warn-passing a watchdog kill (was fail-open).
- release.yml passes TEMPLATE_REPO_PAT via http.extraheader, off the argv.
- compiled-binary e2e fails loud in CI instead of a silent permanent skip.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(hook,sweep,ipc): non-blocking hook pushes, bounded sweep + cache, source-bound resolve

- session-start/session-end no longer run synchronous git + inline push
  inside their self-deadline; a detached child does the push and the hook
  returns immediately (blocked Claude Code startup for minutes on a dirty
  tree before).
- serve sweep drops the unbounded listAllPageRefs, resolves only candidate
  targets, claims corpus files atomically (no double-LLM-spend race), and
  caps the fence LIKE scan; heartbeat writes are O_APPEND with rare compaction.
- hot-memory cache evicts expired entries and bounds entry count (the key is
  caller-controlled via _meta.session_id).
- v1 resolve IPC honors boundSourceId like turn_context; turn-context runs
  its arms concurrently. doctor reads push/heartbeat thresholds from hook.ts.
- new tests: doctor bootstrap checks, hook push-gate + deadline, concurrent
  sweep claims, cache eviction, bound-source resolve.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(bootstrap): origin-ownership gate, world visibility, collision-safe source id, consent + templates

- repo adoption requires an exact receipt repo_url match or authed-owner
  check (undefined repo_url was a wildcard); create verifies privacy BEFORE
  the first push.
- verify sets facts.default_visibility=world if unset, so agent-authored
  facts surface in per-turn context (they defaulted private before).
- source_id derives a path-hash suffix when 'workspace' is taken by another
  checkout; every consumer reads manifest.source_id.
- skipped HOOKS_CONSENT now declines (was falling through to default yes);
  --minimal refuses on an initialized manifest; tilde fences escaped.
- MCP registration pins --surface full; status hard-fails a public origin
  (template door); receipt writers guard against newer/corrupt receipts;
  uninstall only claims brain-deleted after a real rm.
- templates ship jobs disabled + provider-consent + support-relay lines;
  soul-audit re-runs over the shared interview bank. TODOS: 11 follow-ups.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(security): close adversarial-review findings — scan fails closed, whole-PEM redaction, bound repo push

Cross-model adversarial pass (Claude + Codex) on the bootstrap wave:

- secret scan fails CLOSED: an unreadable, oversized, or binary staged blob
  now blocks the push (blocked_unscannable, exit 5) instead of committing
  unscanned; only a confirmed staged deletion is skipped. This was the
  headline "block secrets before they leave the machine" property failing open.
- private-key redaction spans the whole PEM block (header+body+footer), not
  just the header line — the base64 body no longer survives into the corpus
  the sweep sends to an extraction provider.
- bootstrap repo commits the workspace (secret-scan-gated) before the first
  push and verifies the remote actually received it, so a push-fail retry
  can't adopt an empty remote as success.
- privacy verify is re-bound to origin immediately before push (a concurrent
  origin rewrite between verify and push is refused).
- session-end corpus write is atomic and clears the stale ingested/in-progress
  sidecars so a resumed session's appended transcript is re-ingested.
- public-origin refusal enforced at render (not only status); MCP "already
  registered" is verified to target this workspace, not blessed blindly;
  verify probe cleanup scopes deletes to its own slugs, not a token substring;
  allowlist fingerprint floor raised 8→16 hex.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* v0.45.0.0 feat(bootstrap): paste-in personal-agent install for Codex + Claude Code

Turns a Codex or Claude Code session into a persistent personal agent:
interview-rendered identity files, a local PGLite brain, per-turn context via
serve IPC (Claude Code hooks / Codex pull protocol), session-triggered
persistence, and a private GitHub repo as the agent's portable body. Keyless-
first (the harness model is the LLM; one optional key adds embeddings +
extraction). New `gbrain bootstrap` command family + `gbrain hook` + `gbrain
sweep`; doctor bootstrap health checks; latest-stable distribution ref +
template-repo publish job. Opt-in, additive — existing installs untouched.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix: regenerate flag registry for security-fix flags; drop fabricated gbrain capabilities doc ref

CI caught two real failures under the merged state:
- the flag registry lagged the blocked_unscannable/exit-5 flags the security
  round added, tripping the #2185 freshness guard.
- headless-install.md described the keyless capability report as a
  `gbrain capabilities` command, which the #3502 doc-command resolver
  rejects — reworded to prose (the real surface is bootstrap verify's report).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs: sync KEY_FILES + bootstrap plan to security-fix behavior

Cross-referenced the security-fix round against the reference docs and
corrected the drift those commits introduced:

- workspace-push.ts entry: stage-FIRST-then-scan order (the TOCTOU fix),
  fail-closed blocked_unscannable, and the sources-push status -> exit-code map.
- hooks.ts entry: MCP registration pins `serve --surface full`.
- hook.ts entry: session-start/session-end pushes run in a detached child
  (non-blocking); atomic corpus write clears stale sidecars.
- bootstrap.ts entry: render hard-refuses a public origin (template door).
- verify.ts entry: source_id collision resolution (workspace-<path-hash>).
- AGENT_BOOTSTRAP_PLAN as-shipped delta note for the scan/stage reorder.

llms bundle unchanged (KEY_FILES is link-only); build:llms and
test/build-llms.test.ts green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(ci): silence SC2016 on the intentional askpass literal in release.yml

The one-shot GIT_ASKPASS script must contain literal $1 and
$TEMPLATE_REPO_PAT so they expand when /bin/sh runs it at git's credential
prompt, not when the outer shell writes the file — single quotes are correct.
Add a scoped shellcheck disable so actionlint passes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(skills): declare 'bootstrap my data' trigger in cold-start frontmatter

The doc reform added 'bootstrap my data' to cold-start's RESOLVER.md row (to
disambiguate data-bootstrap from agent-bootstrap) but not to the skill's own
frontmatter triggers, tripping the RESOLVER↔frontmatter round-trip contract
(resolver.test.ts). Declare it; regenerate skills.lock.json.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(bootstrap): default per-turn hooks + search mode ON without a prompt

Installing gbrain for your coding agent IS the consent for the behaviors that
make it work, so stop re-litigating them with install-time questions whose
"no" defeats the product:

- Per-turn hooks (Claude Code) install ON by default — no prompt. Off-ramps:
  `--no-hooks` at install, `GBRAIN_HOOKS=0` at runtime, `bootstrap uninstall`.
  The "hooks installed" line now surfaces the kill switch so default-on is
  never silent. A persisted HOOKS_CONSENT=no (interview --skip) still declines.
- Search mode defaults to `balanced` silently (nobody knows the modes at
  install; `gbrain search modes` changes it any time).
- MCP scope stays the ONE deliberate prompt — project vs user is a real
  cross-repo privacy choice, not friction.

Marks the two consents `silent: true` in the question bank (new QuestionSpec
field), rewrites the runbook phases so the agent no longer asks them, adds the
`--no-hooks` flag (+ registry regen), and adds default-on / opt-out tests.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(bootstrap): real end-to-end coverage — cross-session recall, per-turn content, Codex door, realistic corpus

Closes the seven e2e gaps a coverage audit surfaced: the plumbing was
well-unit-tested but the product claims ("Codex works, context shows up every
turn with real content, it remembers across restarts, machine two recovers,
Postgres works") were unproven end to end. Test-only wave — zero src changes.

- Hermetic synthetic corpus (test/fixtures/bootstrap-corpus/ + a loader helper):
  12 interlinked pages (52 edges, timelines), 12 world/private beliefs, 8 gold
  queries — curated from the gbrain-evals synthetic corpora, 100% placeholder
  names, so recall is asserted on a real multi-entity brain instead of a
  2-node self-planted probe.
- GAP1 magic moment: author a fact via the real write path, disconnect the
  engine, reopen against the same DB, recall it — a real session boundary, not
  verify.ts's same-connection SQL read-back. Plus a source-isolation assertion.
- GAP2 per-turn content: hook-under-serve Pin 1 now seeds a known fact and
  asserts its text lands in the injected block AND private beliefs never do
  (was: empty brain, empty_block accepted as a pass).
- GAP3 Codex door: assert the rendered AGENTS.md carries the Gate-3 brain-first
  pull protocol; make the fake codex shim implement `mcp get` so the [FIX7]
  target-verification can actually fail; the Docker cold-machine harness now
  exercises the hooks/MCP registration step instead of skipping it.
- GAP4 corpus recall: turn-context + verify graph-floor/qrels run on the real
  multi-entity brain with real edges.
- GAP5 attach: machine-two now re-ingests the cloned brain/ into a fresh DB and
  recalls a fact authored only on machine one — the multi-device payoff.
- GAP6 keyed + Postgres (env-gated): real embeddings prove semantic recall a
  paraphrase query can reach but keyless BM25 cannot; bootstrap verify drives a
  real Postgres engine (skipIf DATABASE_URL/keys absent).
- GAP7 persistence: session-end runs the REAL push (not the mocked seam) to a
  local bare remote and the remote receives the content; a planted secret is
  blocked at the gate; the 15-min cron installs and fires a scan-gated push.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(bootstrap): real-agent e2e — drive the actual claude + codex binaries end to end

Closes the audit's biggest gap ("no real harness ever drives a turn"). Adapts
gstack's PTY/headless agent harness to prove the bootstrap install + smoke work
against the REAL binaries, not PATH shims. Test/CI/docs only — zero src changes.

- test/helpers/agent-harness.ts: hermetic clean-room child env (ported from
  gstack; drops CONDUCTOR_/CLAUDE_/GSTACK_/MCP_/GBRAIN_, promotes
  GSTACK_ANTHROPIC_API_KEY→ANTHROPIC_API_KEY), real-binary resolvers + auth
  probes, headless `claude -p --output-format stream-json` and `codex exec
  --json` turn runners, a gbrain stdio MCP-config writer, and a keyless brain
  seeder. + a fixture-parse unit test (no binary needed).
- test/e2e/bootstrap-real-claude.serial.test.ts: real `gbrain bootstrap`
  install → REAL `claude mcp add` (verified via `claude mcp get`) → verify
  exit 0 → a real `claude -p --mcp-config --strict-mcp-config` turn that
  invokes mcp__gbrain__search and answers from the brain (proven: toolCalls
  include mcp__gbrain__search, final text carries the seeded fact).
- test/e2e/bootstrap-real-codex.serial.test.ts: same install with REAL `codex
  mcp add` into a real ~/.codex/config.toml + Gate-3 pull-protocol assertion,
  then a real `codex exec --json` turn surfacing the fact (MCP or the pull-
  protocol shell path). Bounded retry absorbs codex's occasional MCP-call
  cancellation without softening the fact-requiring assertion.
- Everything hermetic (temp HOME/CLAUDE_CONFIG_DIR/CODEX_HOME/GBRAIN_HOME;
  real ~/.codex auth copied read-only) and skipIf-gated so it self-skips
  cleanly where the binaries/auth are absent.
- heavy-tests.yml: gated `real-agent-e2e` job (nightly/label, never the PR
  shard; no-op on a runner without authed binaries).
- TODOS: compiled `gbrain` binary can't serve a PGLite brain (bun compile
  omits the WASM/extension payloads); harness falls back to `bun run` serve.

Verified against live claude 4.6 + codex 0.147.0: 15 pass / 0 fail; verify
36/36; typecheck clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(ci): real-agent-e2e job — bash array + --timeout (actionlint SC2086 + bun-test-timeout guard)

The real-agent-e2e job's file loop used an unquoted $FILES (SC2086) and ran
`bun test` without --timeout (check-bun-test-timeout guard). Switch to a bash
array and add --timeout=600000 (real-agent turns are slow; the tests self-skip
without authed binaries so it's a no-op elsewhere).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(pglite): embed WASM + extension assets so the compiled binary can serve

A `bun build --compile` gbrain binary could not `serve` a PGLite brain: the
compile bundles JS but not PGLite's runtime payload (pglite.wasm, initdb.wasm,
pglite.data, vector/pg_trgm tarballs), so `serve` on PGLite died with a
bunfs/ENOENT. Now the assets ride inside the binary.

- src/core/pglite-embedded-assets.ts: embeds the five assets via
  `import … with { type: 'file' }` (the ENG-6 idiom) and exposes
  getEmbeddedPgliteOptions() → { pgliteWasmModule, initdbWasmModule, fsBundle,
  extensions:{vector,pg_trgm} }. WASM/fsBundle are consumed as bytes; the two
  extension tarballs are materialized to a content-addressed temp file (atomic,
  size-verified reuse) because PGLite reads them via fs.createReadStream, which
  cannot read a /$bunfs path. Unconditional (works in bun-run and compiled),
  so no fragile mode branch.
- src/core/pglite-engine.ts: static-import getEmbeddedPgliteOptions (engine
  path stays static per the engine-dynamic-import invariant); spread into both
  PGlite.create sites (initial + WAL-repair retry). The bunfs classifier stays
  as a backstop but no longer fires for a correct binary.
- scripts/check-pglite-embedded.sh (+ smoketest): compiles a focused binary and
  asserts it boots PGLite, CREATE EXTENSION vector/pg_trgm, and round-trips a
  page — wired into `bun run verify` (now 37 checks), check:all, and
  check:pglite-embedded. Fail-soft only when compile is unavailable.
- agent-harness.ts probeCompiledPglite now passes → the real-agent e2e uses the
  fast compiled MCP server. TODOS: the P2 "can't serve PGLite" item is closed.

Verified: fresh compiled binary ran `search`/`query` against a PGLite brain and
returned the seeded row (no bunfs/ENOENT); verify 37/37; pglite-engine 120/0
source-mode; typecheck clean; engine-dynamic-import + parity guards pass.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
v0.45.0.0
2026-08-11 16:42:11 -07:00
fc310db3ea v0.44.1.0 feat(ai): recipes resolve any model — remove the native-recipe runtime allowlist (#4014)
* feat(ai): recipes resolve any model — remove the native-recipe runtime allowlist

Frontier models ship weekly; a curated models: array can never stay current.
assertTouchpoint now checks only the provider's touchpoint capability (anthropic
has no embeddings, voyage has no chat) and never gates on the model id. Any id
the user names goes to the provider, which is the real authority on what
exists — a nonexistent model surfaces as the provider's own model_not_found at
call time, and gbrain models doctor live-probes the configured models for a
pre-flight check.

With the gate gone, the entire extendedModels bypass machinery is dead and
deleted: the _extendedModels registry, registerExtendedModel,
registerConfigSelectedChatModel (+ its one caller in the contextual-reindex
handler), both registration loops, and the tier-resolution loop that existed
only to feed them. This also structurally closes the per-task-key gap where
models.think / models.dream.* / facts.extraction_model selections were rejected
while identical models.default selections worked.

Recipe models: arrays remain informational — models[0] default selection for
--model <provider> shorthand, guard-test fixtures pinning the repo's own
hardcoded defaults, and gbrain providers list display. gateway.rerank() keeps
its own model-list check deliberately: each listed reranker id maps to a known
request/response wire shape.

unknown_model still fires for providers lacking the touchpoint, so every probe
reason stays reachable; tests that pinned the allowlist rejection now pin the
pass-through contract (or the missing-touchpoint trigger).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* v0.44.1.0 feat(ai): recipes resolve any model — remove the native-recipe runtime allowlist

Frontier models ship weekly; a curated models: array can never stay current.
assertTouchpoint now checks only the provider's touchpoint capability and never
gates on the model id — any id the user names goes to the provider, and a
nonexistent one surfaces as the provider's own model_not_found at call time
(gbrain models doctor stays the token-free pre-flight).

The extendedModels bypass machinery is deleted end to end, which also closes
the per-task-key gap: models.think / models.dream.* / facts.extraction_model
selections now behave exactly like models.default. think's graceful sentinel
surfaces the thrown AIConfigError's own message + fix instead of generic key
advice, so a provider 4xx is never key-blamed. gateway.rerank() keeps its own
model-list check (each listed id maps to a known wire shape).

Ship chores: VERSION/package.json → 0.44.1.0, CHANGELOG entry, gitleaks action
pin refreshed to current v2.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Garry Tan <garrytan@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
v0.44.1.0
2026-08-11 12:41:24 -07:00
Masa 52306ed438 fix(auth): accept comma-separated --scopes and reject zero-token input (#3990)
`gbrain auth register-client --scopes` only split on whitespace, so the
comma-joined form the CLI's own registration hint recommends
(`--scopes read,write,admin`, printed by init.ts) fell through as a
single unrecognized token and was rejected with `Unknown scope
"read,write,admin"` -- self-contradicting the hint.

parseRegisterClientArgs now normalizes comma- and/or whitespace-separated
--scopes input to the canonical space-joined form before it reaches
registerClientManual's assertAllowedScopes gate. The shared
parseScopeString (RFC 6749 space-delimited OAuth wire format, also used
for untrusted DCR/refresh/request-scope parsing) is left untouched.

Input that normalizes to zero tokens (comma-only, whitespace-only, or
empty) is rejected at the parser boundary with a clear usage error,
rather than silently registering a client with no usable scopes --
closing a hole that a comma-only first pass at this fix left open
(caught by review).

Tests cover comma/space/mixed forms parsing to the identical scope set,
a genuinely unknown scope still being rejected, and zero-token input
being rejected.
2026-08-11 04:48:11 -07:00
Masa a2521d0a32 fix(enrich): keep entity-name candidates on a single line (#3991)
* fix(enrich): keep entity-name candidates on a single line

extractEntities()'s namePattern used \s+ between capitalized words,
which matches across line breaks. A capitalized word ending one
paragraph and a capitalized word starting the next were spliced into
one bogus multi-word candidate whose name embedded the line break(s).

Scope the pattern to same-line whitespace: [^\S\r\n  ]+
(spaces, tabs, NBSP, other Unicode space separators, but not \n, \r,
or the Unicode line/paragraph separators U+2028/U+2029). Same-line
multi-word names and NBSP-separated names still match; any line break
between two capitalized words now stops the match, same as it already
does for punctuation.

Surfaced during single-user dogfooding on a ~100-page install
(v0.44.0.0): extractEntities produced ~1,620 raw candidates, and these
paragraph-spanning splices were the largest single source of noise.

* fix(enrich): exclude vertical tab and form feed from name-splice guard

The line-scoping fix in the prior commit excluded CR, LF, and the
Unicode line/paragraph separators (U+2028/U+2029) from the same-line
whitespace class, but still treated VT (U+000B) and FF (U+000C) as
same-line whitespace since \s matches them and they weren't in the
excluded set. Winters<VT>Reyes and Winters<FF>Reyes were still spliced
into one bogus candidate, contradicting the "any line break stops the
match" intent.

Add \v and \f to the excluded set, so the pattern now excludes every
vertical whitespace code point (CR, LF, VT, FF, U+2028, U+2029) while
still retaining horizontal whitespace (spaces, tabs, NBSP, other
Unicode space separators) between same-line words.
2026-08-11 04:24:00 -07:00
Garry Tan 75fae742d5 v0.44.0.0 feat(eval): BrainBench — cross-harness memory conformance suite (Cathedral 2) (#2158)
Public, reproducible, cross-harness benchmark for the four ways agent memory fails (know-to-ask, push precision/recall, write-back fidelity, cross-session continuity). Hermetic PGLite, sealed gold, deterministic corpus, CI gate vs master's committed baseline, published JSON-schema foreign-runner contract. See docs/eval/BRAINBENCH.md.
v0.44.0.0
2026-08-10 09:44:46 -07:00
MasaandClaude Opus 5 a13ecb1f0a fix(cycle): BudgetMeter prices from the derived Anthropic view, so the cap is off for canonically-priced models (#3951)
* fix(cycle): price BudgetMeter through the canonical table, not the derived Anthropic view

BudgetMeter estimated submit cost with `estimateMaxCostUsd`, which reads
ANTHROPIC_PRICING. CLAUDE.md defines that table as a DERIVED view of the one
canonical chat-pricing table, so any non-Anthropic model was unpriceable here
even when CANONICAL_PRICING carries its rates — and an unpriceable model does
not merely lose accuracy, it takes the `cost === null` branch and returns
`allowed: true` with cost 0. The cap is off for that model.

Measured on 130d321d: openai:gpt-5.2 and deepseek:deepseek-chat are both in
CANONICAL_PRICING and both absent from the derived view, so the gate was
disabled for them. claude-opus-4-7 resolves through either.

The estimate now goes through canonicalLookup, falling back to the existing
call for anything canonical does not carry. Anthropic numbers are unchanged
by construction — the derived view is generated from canonical — and a test
pins that parity.

Deliberately NOT changed: a model absent from canonical too keeps the
documented warn-and-allow bypass, with the same BUDGET_METER_NO_PRICING
warning, the same submit_unpriced ledger event, and the same
`unpricedSubmits` counter. Whether those should be priced at a conservative
fallback rate instead — as synthesize-concepts (#3915) and skillopt/preflight
already do — is a policy call, not this fix. The ledger schema is untouched;
`test/fixtures/dream-budget-schema-v1.jsonl` still describes it.

Related to #2149 (self-closed by its reporter without a fix; the pricing half
of what it described is still live). Distinct from #2504, which is the
opposite failure mode in BudgetTracker: hard-throw on a missing entry rather
than silent bypass.

Verification on 130d321d (v0.42.76.0):
- bun test budget-meter + auto-think-phase + propose-takes + model-pricing -> 93 pass / 0 fail
- red check: with upstream/master's budget-meter.ts -> 10 pass / 1 fail on the new gating test
- bun run typecheck -> clean
- bun run verify -> 34/34 green

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(cycle): guard the non-finite estimate, and correct the docs the change makes stale

Codex review, both verified before applying:

- Both pricing tables are object literals, so a model id colliding with an
  inherited key ('constructor', 'toString', '__proto__') resolves to a truthy
  Object.prototype value whose .input/.output are undefined and the rate
  arithmetic yields NaN. Measured: canonicalLookup('constructor') returns a
  function, and estimateMaxCostUsd('constructor', …) already returns NaN on
  master, so the shape predates this change — but it sits on the path this
  patch touches. `cumulative + NaN` is NaN and `NaN > budget` is false, so one
  such submit would disable the gate for the rest of the cycle. The estimate
  helper now treats a non-finite result as unpriceable, which routes that one
  submit into the documented warn-and-allow branch and leaves the running
  total finite. Regression test submits the poison id, then a submit that must
  be denied. The same shape on the estimateMaxCostUsd path is left alone.

- Two docs asserted the old behaviour and are now current-state: the
  anthropic-pricing.ts header ("non-Anthropic models … bypass … runs
  unbounded") and the KEY_FILES.md entry for the same file. Both now say the
  bypass applies only when canonical has no rates either. `bun run
  build:llms` regenerated; the bundles are unchanged because KEY_FILES.md is
  linked rather than inlined, and test/build-llms.test.ts passes.

Verification on 130d321d (v0.42.76.0):
- bun test budget-meter + auto-think-phase + propose-takes + model-pricing + build-llms -> 106 pass / 0 fail
- bun run typecheck -> clean
- bun run verify -> 34/34 green

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 21:32:46 +07:00
MasaandClaude Opus 5 e2ff128e35 fix(config): register sources.default as a known key + validate it at set time (#3941)
* fix(config): register sources.default as a known key + validate it at set time

`gbrain config set sources.default <id>` printed

    WARN: writing unknown key "sources.default" with --force.
    Nothing in gbrain reads this.

That last sentence is false. `source-resolver.ts` tier 5 reads
`getConfig('sources.default')` on every unqualified call, and
`gbrain sources default <id>` writes exactly this key. The key was
simply missing from KNOWN_CONFIG_KEYS.

The message matters more than the missing entry: pinning
`sources.default` is the documented way to stop tier 5.5
(sole-non-default-source auto-routing) from silently moving where
unqualified writes land. An operator who hits that and reaches for
this knob is told the knob does nothing.

- Register the exact key, not a `sources.` prefix. A prefix would
  bless arbitrary unread `sources.*` keys and weaken the unknown-key
  guard this list exists to provide.
- Validate at set time, mirroring `runDefault()`: reject a malformed
  id, and reject one that is not registered. Without this, registering
  the key would turn `config set` into a way around the check
  `sources default` already performs — and tier 5 calls
  assertSourceExists, so a typo would surface later as a throw on
  unrelated commands rather than at the point of the mistake.

Tests: membership, the absence of a blanket `sources.` prefix, and the
three set-time paths (malformed / unregistered / accepted).

* fix(config): keep flag literals out of the new error text + refresh flag registry

CI caught this: the #2185 freshness guard failed because the generated
flag registry no longer matched a fresh generator run.

Two separate causes, only one of them mine:

1. Mine. The "source is not registered" message named `--path` while
   suggesting `gbrain sources add`. The generator scans command sources
   for flag tokens, so a flag named in prose silently gets granted to
   `gbrain config` — exactly the prose-bleed class #3902 closed for
   safety flags. Reworded to carry no flag literals. (The first attempt
   at a warning comment reproduced the bug by quoting a placeholder
   flag in the comment itself; that is gone too.)

2. Pre-existing. Regenerating also adds `--federated-read`, which comes
   from `src/commands/auth.ts` and appears in no line this branch
   touches. The committed registry on master is stale with respect to
   its own sources, so the guard fails for any branch cut from master
   until it is refreshed. Committed here because the guard's contract is
   "committed registry == fresh generator run"; flagged in the PR body
   so it is not mistaken for part of this change.

`bun test test/cli-flag-validation.test.ts` — 24 pass / 0 fail.

* fix(config): propagate lookup failures, describe the real id grammar

Round-2 review findings:

- Dropped `.catch(() => null)` around `fetchSource`. It turned a
  connection failure / permission error / SQL regression into
  "source is not registered", sending the operator after a
  registration problem that doesn't exist while the real fault was
  swallowed. `fetchSource` already absorbs the one expected
  legacy-column case; everything else should surface as itself.
  Test added: `executeRaw` throws → the error propagates and nothing
  is written.
- The malformed-id message quoted `[a-z0-9-]{1,32}`, which is looser
  than `SOURCE_ID_RE` (`-wiki` reads as legal under the printed rule
  but is rejected). Now states the actual grammar.
- Tightened the accept-path assertion from `toContainEqual` to an
  exact `setCalls` equality.

Also corrects the previous commit's explanation of the flag-registry
drift: `--federated-read` is NOT pre-existing master drift. It enters
`config`'s flag set because this change adds
`await import('../core/source-id.ts')`, and that module's comment
mentions the flag — the generator follows imports, so prose-bleed
crosses module boundaries. Master is not stale; this branch pulled it
in. The regenerated entry is deterministic and harmless (`config` is
flag-validation-exempt), and stays in this PR because the guard's
contract is "committed registry == fresh generator run".

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 21:16:19 +07:00
MasaandClaude Opus 5 69472c24e7 fix(jobs): embed --dry-run --background embedded for real (#3594 class) (#3956)
* fix(jobs): embed --dry-run --background embedded for real

`runEmbed` serializes the flag into the job payload
(`dryRun: cleanArgs.includes('--dry-run')`), but the registered `embed` worker
handler never read it back — it forwards slug/slugs/all/stale/sourceId/pace and
onProgress, and nothing else. So `runEmbedCore` ran without `dryRun`: a
backgrounded preview called the embedding provider and wrote vectors. API spend
and NULL->vector mutation from an invocation whose entire purpose was neither.

One line: the handler now passes `dryRun: !!job.data.dryRun`.

Fourth instance of the class in #3594, which lists three (jobs prune, unify-types,
sync) and says "filing the class, because fixing them one at a time will not stop
the fourth". The shape here is a fourth variant: the guard is neither late nor
defaulted wrong — it simply is not wired to the flag the CLI already sends.

The test mirrors test/jobs-unify-types-default-dryrun.test.ts (the #1575 fix in
that same issue) and asserts the side effect rather than the return value, which
is #3594's stated reason this class escapes tests. It seeds real stale chunks
first and asserts countStaleChunks > 0, so the dry-run path cannot short-circuit
and pass the write assertion vacuously.

Two independent signals, so it bites in either environment. Without embedding
credentials a real run throws EmbeddingCredentialError from the preflight, so
reaching the assertion at all proves the dry-run branch was taken — a preview
has no business needing an API key. With credentials the run would succeed and
write, and the chunk count catches it.

Verification on 130d321d (v0.42.76.0):
- bun test jobs-embed-background-dryrun + jobs-unify-types-default-dryrun + jobs-nice-flag -> 7 pass / 0 fail
- red check: with upstream/master's jobs.ts -> 0 pass / 1 fail
- bun run typecheck -> clean
- bun run verify -> 34/34 green

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(jobs): report what the embed job did, and make the guard hermetic

Codex, both verified:

- The handler returned a constant `{ embedded: true }`, so after the dryRun
  fix a dry run reported that it embedded — the same lie in miniature, visible
  through `gbrain jobs get`. It now returns the real counts. `embedded` keeps
  its key and stays truthy on a real run (it is the count now, 0 on a dry run).

- The guard was only sound where no embedding provider is configured:
  runEmbedCore catches provider failures into `failures` and writes nothing, so
  an unfixed handler pointed at a broken provider would also leave the write
  count unchanged and pass. A mocked transport with a call counter closes that
  window — a real run must reach embedBatch, a dry run must not.

Verification on 130d321d: typecheck clean, verify 34/34, focused jobs+embed
tests 36 pass / 0 fail, and still 0 pass / 1 fail against upstream/master's
jobs.ts.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(jobs): rename to *.serial.test.ts — mock.module needs it

check:test-isolation rule R2: mock.module() leaks across files inside a shard
process, so a file using it must be serial. The hermetic embed transport added
in the previous commit introduced one; embed.serial.test.ts is serial for the
same reason.

Caught by `bun run verify`, which I ran after pushing rather than before —
same sequencing mistake as earlier on this branch. Gates now, all green:
typecheck clean, verify 34/34, guard 1 pass / 0 fail, and 0 pass / 1 fail
against upstream/master's jobs.ts.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 20:59:53 +07:00
MasaandClaude Opus 5 ea17f5911c fix(cli): --help required a configured brain for five self-help commands (#3954)
* fix(cli): --help required a configured brain for five commands

`gbrain models --help` on a machine that has not run `gbrain init` exits 1
with "No brain configured. Run: gbrain init". So do watch, skillopt, maintain
and extract-conversation-facts. That is the state a reader is most likely to
be in when they reach for --help.

Mechanism: cli.ts's per-command --help block prints the generic usage stub
only for CLI_ONLY commands NOT in CLI_ONLY_SELF_HELP. Members of that set are
meant to print their own help, so they fall through to the normal dispatch --
which connects the engine before the handler runs. Each of these five already
opens with a --help branch (models honours it first by explicit comment,
watch and skillopt on line 1 of the handler); the connect gate made that
branch unreachable.

Dispatched pre-connect now, through a small table keyed by command. The engine
is never read on the help path, so a placeholder is passed rather than
widening five signatures -- skillopt already declares `BrainEngine | null` for
this reason.

Measured on 130d321d with an empty GBRAIN_HOME:

  before: models/watch/skillopt/maintain/extract-conversation-facts -> exit 1, 0 lines
  after:  exit 0, 15 / 21 / 94 / 11 / 43 lines of their own help

Seven other members of the set (brainstorm, config, embed, lsd, migrate,
pages, retrieval-upgrade) still need a brain, because their handlers have no
--help branch to reach. Fixing those means either writing help or dropping
them from CLI_ONLY_SELF_HELP so the generic stub answers -- a call for the
maintainer, not something to guess at here. They are pinned in the test as a
tripwire rather than omitted, so the coverage list cannot drift in either
direction: fixing one fails the test until its entry moves.

The guard runs the CLI with an empty GBRAIN_HOME and asserts exit 0 plus real
help output. Behaviour, not a declaration -- membership in CLI_ONLY_SELF_HELP
was itself unverified, which is how five commands sat in a set whose whole
meaning is "prints its own help" while printing none.

Verification on 130d321d (v0.42.76.0):
- bun test cli + cli-flag-validation + cli-help-without-brain -> 54 pass / 0 fail
- red check: with upstream/master's cli.ts -> 5 fail / 1 pass
- bun run typecheck -> clean
- bun run verify -> 34/34 green
- bun run build:flag-registry -> no diff

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(cli): stop the no-brain guard inheriting a database URL

Codex: an empty GBRAIN_HOME is not sufficient. loadConfig also honours
GBRAIN_DATABASE_URL and DATABASE_URL (config.ts:550-551), so a developer or CI
runner exporting either would let the CLI connect, the positive assertions
would pass on master, and the guard would be inert.

Both are now unset for the spawned process. Verified the guard still regresses
under the leak it was vulnerable to: with DATABASE_URL exported and
upstream/master's cli.ts, the run is 5 fail / 1 pass.

Verification on 130d321d: 6 pass / 0 fail, typecheck clean, verify 34/34.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(test): type the spawn env so the delete compiles

process.env is narrowly typed here, so deleting GBRAIN_DATABASE_URL /
DATABASE_URL off a spread of it fails tsc with TS2339. Declared as
Record<string, string | undefined>.

My previous commit was pushed with this error present — typecheck and verify
were run after the push, not before. Verified now:
- bun run typecheck -> clean
- bun run verify -> 34/34 green
- bun test cli + cli-flag-validation + cli-help-without-brain -> 54 pass / 0 fail

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(cli): spawn with --no-env-file so a local .env cannot re-inject the URL

Codex: unsetting GBRAIN_DATABASE_URL / DATABASE_URL on the spawn env is not
enough, because bun auto-loads .env from cwd and GBRAIN_DATABASE_URL is
honored unconditionally (config.ts:550). A developer's local .env would put
back exactly what the deletes removed, and the guard would go inert. Flag
semantics confirmed from `bun --help`: '--no-env-file  Disable automatic
loading of .env files'.

Verified: typecheck clean, verify 34/34, guard 6 pass / 0 fail, and still
5 fail / 1 pass against upstream/master's cli.ts.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(cli): run the tripwire spawns concurrently

Seven sequential CLI spawns were most of this file's wall clock and each is
independent (own temp GBRAIN_HOME). 4.05s -> 2.87s locally; the saving is
larger on a loaded CI runner.

Gates run BEFORE this push, unlike twice earlier on this branch: typecheck
clean, verify 34/34, 6 pass / 0 fail, and still 5 fail / 1 pass against
upstream/master's cli.ts.

Note on the shard 9 failure on the previous run: it exited 123 with no
reported test failure, the last output being an unrelated capture test hitting
a placeholder API key. Shard 9 passes locally with this change, and passes on
three sibling PRs off the same base, so I have not been able to attribute it.
Pushing this change re-runs it; if it reproduces in the same place the cause is
here and I will chase it rather than re-run again.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(cli): make the guard serial so it does not re-shard the suite

Root cause of the shard 9 failure on the two previous runs, chased rather than
re-run as promised.

scripts/run-unit-shard.sh documents 'hash-disjoint' sharding but assigns by
sorted-index modulo: `i % shard_m + 1`. Adding ONE test file shifts every
index after it, so 933 of 1103 files change shard. Shard 9 kept
capture-runcapture.test.ts but got a different set of neighbours, and that file
does a real embed attempt — under CI's placeholder OPENAI_API_KEY it reached
the network, failed, and took the shard process down with exit 123 and no
reported test failure.

Not my code's behaviour: capture-runcapture passes in isolation with my cli.ts
AND with master's (26 pass each), and shard 9 passed on four sibling PRs off
the same base that add no test file. Any PR adding a sharded test file can
surface this.

Serial files are excluded from the partition (`-not -name '*.serial.test.ts'`),
so the sharded set is back to master's 1103 files and shard 9 to its original
composition. Serial is also the honest classification: this guard spawns twelve
CLI subprocesses.

Gates before push: typecheck clean, verify 34/34, 6 pass / 0 fail, still
5 fail / 1 pass against upstream/master's cli.ts.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 20:44:52 +07:00
MasaandClaude Opus 5 d050969da6 fix(frontmatter): drop five Apple Notes directory rules that cannot change an outcome (#3952)
* fix(frontmatter): drop five Apple Notes rules that cannot change an outcome

DIRECTORY_RULES carried five subfolder rules whose every field equals what
the generic 'apple notes/' rule already produces. The generic rule assigns
type/source/date/title and tags the first path segment lowercased and
hyphenated, which is exactly what these five spelled out by hand. They read
as deliberate overrides and are not.

Measured across prefix x body x filename (45 cases): title, type, date,
source and tags are identical with and without them. The only field that
moves is `matchedRule`, the debug label naming which rule fired -- and it
must move, because the rule that fires is now the generic one. Nothing
persists that field: serializeFrontmatter writes title/type/date/source/tags
only, and the one behavioural consumer (frontmatter.ts:475) compares it
against the literal '(default)', which neither value is.

Removing them exposed an existing assertion as vacuous: the ordering test
named 'apple notes/yc/' and asserted its index is below the generic rule's.
findIndex returns -1 for an absent prefix, and -1 is below any index, so the
test kept passing while testing nothing. Rewritten to require every subfolder
rule to precede the generic one, and to require that the set is non-empty.

Adds a guard that recomputes the equivalence rather than listing prefixes, so
a rule that re-derives the generic output cannot be added back silently.

Verification on 130d321d (v0.42.76.0):
- bun test frontmatter-inference + page-type-exhaustive -> 43 pass / 0 fail
- red check: with upstream/master's frontmatter-inference.ts -> 38 pass / 1 fail
- bun run typecheck -> clean
- bun run verify -> 34/34 green
- grep for each removed prefix across the tree -> 0 remaining references

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(frontmatter): mirror production normalization in the redundancy guard

Codex review, both gaps verified against the implementation before fixing:

- inferFrontmatter lowercases BOTH sides before `startsWith`
  (frontmatter-inference.ts:297), so a rule authored as 'Apple Notes/YC/'
  matches at runtime. The guard compared prefixes case-sensitively and
  skipped it.
- The generic rule tags parts[1] only (frontmatter-inference.ts:332), so a
  nested rule 'apple notes/yc/archive/' carrying tags ['yc'] is equally
  redundant. The guard derived from the whole remainder ('yc/archive') and
  did not match.

The predicate now lowercases the prefix and derives from the first segment,
and it is extracted so the guard can be pointed at synthetic rules. A second
test does exactly that: both missed spellings must be flagged, and three
genuine overrides (different tag set, differing field, non-Apple prefix) must
not be. A guard that never fires is indistinguishable from a guard that
cannot fire; this makes the difference observable.

Verification on 130d321d (v0.42.76.0):
- bun test test/frontmatter-inference.test.ts -> 40 pass / 0 fail
- red check: with upstream/master's frontmatter-inference.ts -> 39 pass / 1 fail
- bun run typecheck -> clean
- bun run verify -> 34/34 green

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(frontmatter): normalize the optional strategy fields in the guard

Codex round 3, verified: inferFrontmatter defaults datePattern and
titleStrategy to 'filename' (frontmatter-inference.ts:310, :317), so an
omitted field and an explicit 'filename' describe the same rule. The guard
compared them literally, which meant a redundant rule could escape detection
by simply leaving them out.

Both sides are now normalized through the same default, and the guard's own
self-test covers the escape: the same redundant rule with those two keys
deleted must still be flagged.

Verification on 130d321d (v0.42.76.0):
- bun test test/frontmatter-inference.test.ts -> 40 pass / 0 fail
- bun run typecheck -> clean
- bun run verify -> 34/34 green

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(frontmatter): pin the accepted matchedRule change instead of leaving it incidental

Codex: matchedRule surfaces as results[].rule in `gbrain frontmatter`
(frontmatter.ts:486), so deleting a rule is a consumed-output change even
though nothing persists the field. Asserted rather than merely described:
a path under a removed prefix now reports 'apple notes/', every field
serializeFrontmatter writes is unchanged, and the value is still not the
literal '(default)' that frontmatter.ts's catch-all skip keys on.

Verification on 130d321d: 41 pass / 0 fail, typecheck clean, verify 34/34.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 20:15:02 +07:00
MasaandClaude Opus 5 7c65bcee9b fix(embed): dry-run announced one page of progress for any stale count (#3955)
`gbrain embed --stale --dry-run` emitted a synthetic `onProgress(1, 1, 0)`.
The CLI latches its `embed.pages` total from the first callback, so the
progress stream announced a single page next to a summary naming every stale
chunk:

  {"event":"start","phase":"embed.pages","total":1}
  {"event":"tick","phase":"embed.pages","done":1,"total":1,"pct":100}
  [dry-run] Would embed 60 stale chunks

Measured on a fresh PGLite brain with 60 unembedded pages, v0.42.76.0.

docs/progress-events.md defines `total` as "the total item count if known at
start" and says it is omitted when the work has no total up front. Omission is
provided for; asserting a wrong value is not. The number is also internally
inconsistent with this same path's own result: the existing test asserts
`pages_processed === 0` for a dry run, with the comment "we don't enumerate
pages in dry-run (cheaper pre-flight)".

The synthetic call is dropped, so a dry run emits no `embed.pages` phase --
matching the zero pages it processes. No consumer requires the phase to appear:
progress-tail.ts names it only as an example, and the minion path
(jobs.ts:1553) passes its own callback from the real embed loop.

Adjacent but distinct from #1459, which is about the denominator of a REAL
`--stale` run being latched from the first callback while pages keep arriving
(numerator exceeds it). Same phase, different cause; this change does not
address that one.

Verification on 130d321d (v0.42.76.0):
- bun test test/embed.serial.test.ts -> 35 pass / 0 fail
- red check: with upstream/master's embed.ts -> 34 pass / 1 fail
- live: `embed --stale --dry-run --progress-json` on the 60-page brain now
  prints the summary only, no embed.pages events
- bun run typecheck -> clean
- bun run verify -> 34/34 green

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 19:47:07 +07:00
MasaandClaude Opus 5 9afbf4a09a docs(search): telemetry comments promised an exit flush the code deliberately removed (#3950)
* docs(search): telemetry header still promised an exit flush that was removed

The module header for the search-telemetry rollup writer describes the
buffer as "flushed periodically (60s OR 100 calls, whichever first) AND on
process exit via beforeExit/SIGINT/SIGTERM with a 2-second timeout cap."

`ensureExitHook` two hundred lines below says the opposite, and is the
current behaviour: "Lossy by design: skip the buffered drain entirely on
process exit", with the signal handlers dropped too. The beforeExit drain
was removed because it kept short-lived CLI processes alive waiting on a DB
write, which hung test/e2e/claw-test.test.ts.

So the file has documented an exit flush it does not perform. A reader who
stops at the header concludes every recorded search reaches search_telemetry.

The header now states current behaviour and names the consequence, pointing
at the existing ensureExitHook rationale rather than restating it. Comment
only; no code path changes.

Verification on 130d321d (v0.42.76.0):
- bun test test/search-telemetry.test.ts -> 16 pass / 0 fail
- bun run typecheck -> clean
- bun run verify -> 34/34 green

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(search): sweep the rest of the file's exit-hook claims

Codex review: correcting only the module header left three more comments in
the same file describing hooks that ensureExitHook does not install, and
stated the loss window too narrowly.

- header: loss is not "fewer than 100 buffered calls". flush() clears
  `buckets` before awaiting the write, so a snapshot handed to an in-flight
  flush is lost too, and coalesced flushes let the buffer exceed the
  threshold. Stated as: anything not yet committed is lost on exit.
- line ~26 attributed the downside to a "hard crash" and pointed at the
  methodology doc. Checked: docs/eval/SEARCH_MODE_METHODOLOGY.md contains no
  such explanation, and with no exit drain an ordinary exit loses data too.
- the TelemetryWriter docblock said the timer "and exit hooks" install
  lazily.
- stop() said it uninstalls exit hooks; it clears the timer and the buffer.

Still comment-only.

Verification on 130d321d (v0.42.76.0):
- bun test search-telemetry + search-telemetry-cache-wiring.serial -> 20 pass / 0 fail
- bun run typecheck -> clean
- bun run verify -> 34/34 green
- grep for 'uninstall' and 'exit hook' in the file -> only the one line that
  now states none are installed

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(search): qualify the threshold trigger and the test-only helpers

Codex round 3, both verified against the code before applying:

- `flush()` returns the in-flight promise at its first line, before the
  bucket swap, so a record that crosses FLUSH_THRESHOLD_CALLS while a write
  is running coalesces onto it and never drains the new bucket. "100 calls,
  whichever first" read as a guarantee; it is a trigger. The flush docstring
  now says the coalesced caller does not get its own drain, and notes that
  the pre-write swap is why an uncommitted snapshot cannot be recovered.
- `stop()` said "Called from tests / shutdown". Its only caller in src/ and
  test/ is `_resetTelemetryWriterForTest`; nothing shuts the writer down in
  production.
- `flushOnExitForTest()` said it was "retained as a no-op"; it calls
  `flush()`. It is also unreferenced — the only occurrence in the tree is
  its own definition. Comment corrected; left in place rather than deleted,
  since removing an exported test helper is beyond a comment fix.

Verification on 130d321d (v0.42.76.0):
- bun test search-telemetry + search-telemetry-cache-wiring.serial -> 20 pass / 0 fail
- bun run typecheck -> clean
- bun run verify -> 34/34 green

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(search): the flush timer installs on setEngine, not on the first record

Codex round 4, verified: ensureTimer() has exactly one call site and it is
inside setEngine(). A caller can wire an engine without ever recording, as
the telemetry tests do, so attributing installation to the first record()
was wrong.

Verification on 130d321d: typecheck clean, verify 34/34, telemetry tests
20 pass / 0 fail.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 19:30:43 +07:00
MasaandClaude Opus 5 e11e407752 fix(cli): global help advertises list -n N, a flag list does not accept (#3953)
`gbrain --help` documents `list [--type T] [--tag T] [-n N]`. The command does
not read `-n`; the limit flag is `--limit`, which `gbrain list --help` already
documents. Nothing in the tree parses `-n` for this command (the only `-n` in
src/ is integrations.ts's alias for --dry-run).

Measured on a fresh PGLite brain with 60 pages:

  list -n 5        -> 50 rows   (the default)
  list -n 300      -> 50 rows
  list --limit 5   ->  5 rows
  list --limit 300 -> 60 rows

Not silent, but not usable either. The run prints `output truncated at 50 rows
(default 50). Pass an explicit limit`, and a reader who reached for -n because
the help said so has already done exactly what that message asks.

Help text only; the flag registry regenerates unchanged.

Verification on 130d321d (v0.42.76.0):
- bun test test/cli.test.ts test/cli-flag-validation.test.ts -> 48 pass / 0 fail
- bun run typecheck -> clean
- bun run verify -> 34/34 green

Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-10 19:14:36 +07:00
Garry TanandClaude Fable 5 c5952b8714 v0.43.0.0 feat(mcp,search): MEMORY_VERBS v1 five-verb protocol + concept-question query steering (cathedral 1, #2416) (#2172)
* feat(verbs): MEMORY_VERBS v1 core — five frozen verbs over existing machinery

recall extended in place (G1B additive superset: protocol_version + string
fact_id/provenance on every response; query arm via cheap-hybrid with
keyword-only degradation when no embedding provider; op-layer budget packing
facts-first via the new generic packToBudget). remember/entity/synthesize/
forget as first-class Operations in src/core/verbs.ts (runtime leaf — TDZ-safe
both import directions) with MEMORY VERB descriptions, ToolAnnotations,
populated suggestions on every error (verbError + protocol ErrorCodes +
additive detail/protocol_version on OperationError.toJSON).

writeSingleFact (facts/write-single.ts): zero-LLM single-fact seam reusing the
pipeline's dedup; X1 deterministic supersession (same entity+kind, similar,
text differs -> supersedeId / fence expire+link); validUntil threaded through
FenceInputFact (was silently dropped). buildEntityCard (verbs/entity-card.ts):
reflex resolution arms + parallel depth-1 reads, per-arm degradation on
pre-page_aliases brains, near-miss suggestions with create_safety. runThink
gains additive usage aggregation; synthesize converts the no-LLM graceful
result to the unavailable protocol error and prices the cost block via
canonical model-pricing. RESPONSE_SCHEMAS registry + ERROR_SCHEMA for the
conformance kit.

Cathedral 1 stage 1/4 (#cathedral-1)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(mcp): MEMORY_VERBS v1 surface mode + fail-closed dispatch + usage sidecar

gbrain serve --surface verbs|full (config mcp_surface registered in the merge
allowlist; default full — existing installs unchanged). Enforcement is
two-layer and fail-closed: filtered ListTools AND dispatchToolCall's new
allowedOps set, applied on stdio + BOTH HTTP paths (serve-http.ts OAuth and
the second http-transport.ts). ToolAnnotations passthrough in buildToolDefs +
the HTTP inline mapper, emitted only when an op defines them (byte-stability
for existing tools). Verb validation/internal errors speak the protocol
envelope (suggestion + protocol_version) at the dispatch layer.

E4 observability: per-verb JSONL sidecar (usage-log.ts — local only, never
uploaded; O_APPEND line-atomic, lock-free 10MB rotation) written from dispatch
so param-validation failures count; brain_id via gbrainPath; doctor gains the
memory_verbs_usage check. CLI: human-readable rendering for remember/entity/
synthesize with trailing --json escape; gbrain recall gains --query +
--budget-tokens routed through the recall OP (same arm MCP exercises) with the
budget footer; the forget verb carries no cliHint (CLI_ONLY forget dispatches
first and would shadow it).

Cathedral 1 stage 2/4 (#cathedral-1)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(protocol): gbrain protocol — schemas, conformance kit, stats + TTHW

gbrain protocol [--json] emits the live verb input schemas + the hand-authored
RESPONSE_SCHEMAS + error contract + versioning policy. gbrain protocol
conformance certifies ANY MCP endpoint (default: self-spawned stdio; --target
http://...[--token] or a stdio command) against the embedded fixture set —
shape/enum/behavior/round-trip only, deterministic; entity-page cases seed via
put_page when available and skip honestly on verbs-only targets; synthesize is
cost-gated behind --synthesize (asserts the clean unavailable error on key-less
CI). Fixtures double as BrainBench seeds (test/fixtures/memory-verbs/cases.json
mirror). Self-certification passes 23/23 live.

gbrain protocol stats [--days N]: per-verb calls/error-rate/latency, budget
drops, entity hit rate, and measured TTHW (init stamps protocol_installed_at;
delta to the sidecar's first verb call) — local JSONL only, never uploaded.
init epilogues print the 3-command quickstart (mcp add --surface verbs +
key-free resurrection demo) with the visibility consent line. entity-card fix:
raw namespaced-slug input now matches exactly (slugify flattens slashes).

Cathedral 1 stage 3/4 (#cathedral-1)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test+docs(protocol): conformance suite, p99 gate, MEMORY_VERBS_v1 doc, funnel

test/memory-verbs-conformance.test.ts: 26 tests through dispatchToolCall —
G1B superset regression (legacy fields byte-equal, additions allowed), budget
math incl. budget<first-item, keyword degradation (never an error), remember
provenance/ttl-trap/null-echo, remote round-trip via the world default +
private facts hidden, entity card schema-validated with the chat gateway
rigged to throw (zero-LLM guard) + all arms + fence test, synthesize
[EXPENSIVE + annotations + clean unavailable, forget idempotency/not_found,
writeSingleFact X1 supersession via the embed-transport seam + degraded
dedup, the F3 negative self-test (the runner FAILS lying servers: dropped
fields, bad enums, re-typed ids), fixture-mirror drift guard.
test/mcp-surface.test.ts: verbs=exactly 5, full=identity, dispatch-layer
fail-closed, strict flag parsing. test/entity-card-perf.slow.test.ts: 20K
pages/100K links/30K aliases/40K facts — measured p99 22.9ms vs the 100ms
contract + a recalibrated 50x ratio guard; wired into the CI perf job along
with stdio self-certification (23/23 CONFORMANT live). tool-defs byte-
stability test now pins per-op: ops WITHOUT annotations keep the exact
pre-v1 shape.

docs/protocol/MEMORY_VERBS_v1.md: the frozen contract — envelopes, error
codes, additive-forever policy, ttl forms (P30D rejected), status-not-
status_text, surface modes + why default full, per-harness installs, consent
line, conformance pass criteria + copyable --target examples, local-only
stats privacy, 200K manual bench recipe. README quickstart + docs/mcp pages
moved to --surface verbs with the claude-not-found recovery; llms-config
registered + bundles rebuilt; KEY_FILES entries for the 7 new modules.

Cathedral 1 stage 4/4 (#cathedral-1)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(verbs): verify-gate compliance — test seam, allow-comment placement, import hygiene

usage-log gains __setUsageLogPathForTests so the verb tests redirect the
sidecar without mutating process.env.GBRAIN_HOME (test-isolation lint);
the gbrain-allow-direct-insert rationale moves onto the insertFact call line
(system-of-record guard greps per-line); protocol.ts drops its unused
`operations` import (operations-filter-bypass guard — operationsByName only).
The synthesize unit test now drives the chat-transport seam (deterministic
success envelope + priced cost block, zero real API spend); the keyless
`unavailable` conversion stays covered end-to-end by the CI conformance step,
which runs credential-free.

Cathedral 1 verify pass (#cathedral-1)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(verbs): close 3 ship-review P1s — forget cross-source, entity backlink leak, conformance exit code

Pre-landing review (codex, on the committed diff) caught three P1s the
plan-stage reviews flagged only in the abstract:

P1.1 trust boundary — forget expired by GLOBAL fact id with no source/visibility
scope: a remote caller in source A could expire facts in source B (incl.
private) by guessing ids. forgetFactInFence gains sourceId + worldOnly opts
(scope-check before any state inspection; out-of-scope → not_found, no
existence leak); the forget verb passes ctx.sourceId + worldOnly=remote.

P1.2 source isolation — entity card incoming edges + backlink_count leaked
across sources: getBacklinks(slug,{sourceId}) scopes only the TARGET page and
getBacklinkCounts has no source param, so a foreign-source page's slug surfaced
in a same-named entity's card. Replaced both with a both-sides-scoped query
(f.source_id = t.source_id = entity source, mentions excluded) inside the card
module — no engine-interface change.

P1.3 CLI exit code — protocol conformance/unknown-subcommand set
process.exitCode directly, which the PGLite/WASM force-exit seam ignores
(it reads the gbrain-owned setCliExitVerdict). A NOT CONFORMANT target printed
the banner but exited 0 — CI would green-light a non-conformant server. Now
routes through setCliExitVerdict; proven: unknown subcommand exits 1.

Regression tests: cross-source forget → not_found + foreign fact stays active;
remote can't forget a private fact; entity card omits foreign-source backlinks
from both edges and the count.

Cathedral 1 ship-review fixes (#cathedral-1)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* v0.43.0.0 feat(mcp): MEMORY_VERBS v1 — frozen five-verb memory protocol + conformance kit (cathedral 1)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs: surface --surface verbs in local-stdio quickstart paths for v0.43.0.0

The MEMORY_VERBS v1 quickstart flag (gbrain serve --surface verbs) was added to
the README quickstart, CLAUDE_CODE.md, and CODEX.md during the build but three
other docs that show the local-stdio wire-up still presented bare `gbrain serve`:
the connect-coding-agent Path B walkthrough (the doc the README links to),
INSTALL.md, and DEPLOY.md's Local stdio path. Brought them in line so the
five-verb surface is consistent across every local-quickstart entry point.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

* fix(doctor): assign memory_verbs_usage check to the SKILL category set

The new memory_verbs_usage doctor check (Cathedral 1, E4) was added without a
category, tripping the doctor-categories drift guard (every check name must
belong to exactly one category set). It's an agent-capability adoption signal,
a sibling of retrieval_reflex_health — assigned to SKILL_CHECK_NAMES.

Cathedral 1 ship-review fix (#cathedral-1)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(verbs): propagate MEMORY_VERBS v1 to existing harnesses on upgrade

The verbs shipped reachable + self-describing, but an EXISTING install that
upgrades to 0.43 and keeps its skills had no active push toward them: no
post-upgrade notice (default surface stays 'full'), and no skill/guidance
routing — agents kept calling search/query/get_page/extract_facts.

Two additive propagation fixes:

- Post-upgrade feature pitch: src/commands/migrations/v0_43_0.ts (pitch-only,
  no-op idempotent orchestrator — the verbs ride existing tables, no schema/
  data migration). Registered in the migrations index so
  and the self-upgrade NOTIFY channel announce the five verbs +  +  on any upgrade past 0.43.
  Verified live: the pitch fires for an install upgrading from 0.42.43.0.
- Skill routing: skills/query (memory reads) and skills/brain-ops (read/write
  cycle) now teach the verbs — recall/entity/synthesize vs search/get_page/
  query, remember vs extract_facts, forget — with a fall-back-to-classic-ops
  note for older brains / full surface. The in-MCP tool descriptions already
  steered tool-list readers; this adds the narrative layer the audit found
  missing across all 29 skills.

apply-migrations future-list fixtures updated for the new 0.43.0.0 registry
entry. No DB migration; no contract change; verbs unaffected.

Cathedral 1 propagation (#cathedral-1)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(verbs): green the 3 failing CI shards — 3-seg migration version, hermetic synthesize test, fresh llms

Three real CI-shard failures from the propagation commit + master merges:

- migrations-registry (shard 10): v0_43_0 used a 4-segment version 0.43.0.0,
  but the registry enforces 3-segment semver (/^\d+\.\d+\.\d+$/). Changed
  the migration version to 0.43.0 (the RELEASE stays 0.43.0.0; migrations have
  always used 3-seg, e.g. 0.32.2). apply-migrations future-list fixtures
  updated to match.
- synthesize conformance test (shard 5): asserted a successful answer via the
  chat seam, but runThink builds its client through a real-key check (not the
  seam), so CI (credential-free) took the NO_ANTHROPIC_API_KEY path → the
  verb's unavailable conversion → isError. Now wraps the call in withEnv with a
  fake key AND the chat seam, so it's deterministic regardless of ambient
  credentials.
- build-llms (shard 8): the v0.42.44.0 master merge changed an inlined doc;
  regenerated llms.txt/llms-full.txt.

Cathedral 1 CI fixes (#cathedral-1)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* feat(search): steer concept/landscape questions to query — descriptions, docs, lookup chain, CLI nudge (#2416)

The search/query routing guidance shipped a stale and backwards model:
SEARCH_DESCRIPTION still led with "Keyword search using full-text search"
(search has been cheap-hybrid — vector + keyword + RRF, expansion off —
since D4/D15), and the mandatory lookup chain in brain-first.md ordered
search-first with query only "if search is thin". For concept/landscape
questions ("all the X that do Y"), keyword+vector recall always returns
something, so the fallback never fires and expansion-recoverable
synonym-phrased matches drop silently.

- operations-descriptions.ts: fix the stale SEARCH lead (cheap-hybrid, no
  LLM expansion); both constants now route concept/synonym/landscape
  questions to query, cap the claim honestly (both are top-K — exhaustive
  enumeration belongs to list_pages), and keep every pinned redirect.
- brain-first.md: lookup chain is intent-driven (exact token → search;
  concept/landscape → query FIRST), cost claims corrected, and the
  anti-pattern is explicit: a nonzero search count is not a completeness
  signal. Same sweep in brain-ops/SKILL.md and query/SKILL.md, which
  duplicated the backwards order.
- search-modes.md: decision tree + quick-ref updated to the cheap-hybrid
  reality, new Tricky Spot #6 (count ≠ completeness), example names moved
  to placeholder people per the privacy convention. RETRIEVAL.md gets the
  matching one-liner.
- query-intent.ts: looksConceptShaped + conceptNudge — deterministic
  concept-shape classifier in the existing classifier home, tuned to favor
  false-negatives; cues owned by other routers (find_experts, salience
  ops) deliberately excluded. cli.ts prints the hint-only stderr nudge on
  BOTH result paths (local engine + thin-client routed), --quiet-gated;
  search stays the cheap hot path (never auto-reroutes).
- salience-llm-routing e2e: new concept-phrasings block asserts the
  descriptions actually route concept questions to query (verified live:
  20/20 — all 12 personal-query regressions still route to salience ops,
  all 8 concept phrasings route to query).
- KEY_FILES.md: query-intent entry updated to current state (the old
  entry pointed at a renamed file); llms bundle regenerated (no diff —
  entry is link-only).
- TODOS.md: MCP-envelope hint follow-up filed (remote agents are the
  higher-value half; needs a return-contract change kept out of this
  commit).

Verification: typecheck clean; 70 targeted unit tests + 20 live routing
evals green; all 24 check:all guards green; failing full-suite shards
triaged — think-pipeline.serial.test.ts:434 fails identically on the
clean tree (pre-existing on this branch, filed separately), the rest are
PGLite parallel-load contention that pass in isolation.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(test): pin think's stub-path usage at null per the E2 accounting contract

The MEMORY_VERBS [E2] usage-accounting change deliberately moved "no LLM
ran" from `undefined` to `null` (the frozen cost-block spelling), but the
older #1698 stub-path test still pinned `undefined` and failed on every
run of this branch. The protected semantic is unchanged: no fake usage or
cost on the stub path.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs(changelog): cover the #2416 query-steering wave under v0.43.0.0

The 0.43.0.0 entry described only MEMORY_VERBS; the #2416 retrieval-routing
wave rides the same release. Adds its user-facing summary and refreshes the
release date to the actual ship date.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs(protocol): reword so the #3502 docs-lint doesn't read a phantom command

Master's docs-lint (landed after this branch forked) scans for
`gbrain <verb>` references; the MEMORY_VERBS diagram's "gbrain reference
implementation" phrasing parsed as a nonexistent `gbrain reference`
command. Post-merge-only failure; both sides were green alone.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs: update project documentation for v0.43.0.0

Post-ship /document-release drift pass over the MEMORY_VERBS v1 +
#2416 query-steering waves:

- docs/tutorials/connect-coding-agent.md: the pasteable brain-first
  protocol still taught the pre-#2416 "Search first" rule and named
  only full-surface tools; step 1 now routes by question shape
  (exact tokens -> search, concept/landscape -> query first) and both
  steps map the five-verb surface names (recall/synthesize/remember).
  find_experts and query callouts marked with their surface.
- README.md + tutorial: "90-tool wall" count refreshed to 110 (the
  actual operation catalog).
- CLAUDE.md: contract-first op count ~90 -> ~110 with the v0.43.0.0
  MEMORY_VERBS clause; reference-map row for the memory-verbs
  protocol / --surface / conformance docs.
- TODOS.md: blank line before the v0.42.74.0 section header.
- llms-full.txt regenerated (build:llms; freshness test green).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs: apply cross-model doc-review fixes for v0.43.0.0

Verified findings from the /document-release Codex pass:

- MEMORY_VERBS_v1.md: disclose that conformance WRITES (marker-suffixed
  seed page + remember/forget facts, write-scope creds, artifacts not
  auto-deleted); quickstart header says 4 commands because it has 4.
- CHANGELOG v0.43.0.0: cost claim scoped per verb (budget/latency/cost
  block); remember "updates in place" -> supersedes-with-history (what
  write-single.ts actually does).
- DEPLOY.md: legacy bearer fallback works on BOTH engines now (both
  schemas carry access_tokens per http-transport.ts) — was Postgres-only.
- CLAUDE_CODE.md: list_skills/core-tools verify note marked full-surface
  (on --surface verbs those tools aren't on the surface).
- skills/query/SKILL.md: --surface full INCLUDES the verbs; fallback is
  for pre-0.43 servers only.
- skills/brain-ops/SKILL.md: search/query tool labels updated to the
  #2416 reality (cheap hybrid vs hybrid + LLM expansion).
- search-modes.md: stale "keyword search returns chunks" label.
- RETRIEVAL.md: note the query op defaults expand:true per call.
- Counts: 43 skills -> 52 (README x2, INSTALL), 30 skills -> 52
  (CLAUDE.md), "30+ tools over MCP" -> 110 (README).
- TODOS.md: filed 2 code-vs-spec follow-ups (recall filter composition +
  limit cap; synthesize unavailable-mapping breadth) instead of silently
  editing the frozen contract.
- llms-full.txt + skills.lock.json regenerated; freshness guards green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(cli): regenerate the flag registry for the cathedral-1 command set

Master's #2185 strict flag validation ships a committed, generated
CLI_FLAG_REGISTRY with drift + freshness guards keyed off CLI_ONLY. The
merge added this branch's `protocol` command to CLI_ONLY, but the
committed registry was generated on master without it, failing CI shard
7's drift guard. Regenerated via `bun run build:flag-registry` on the
merged tree (95 commands; picks up `protocol` and the `--surface`
help-text mentions from the verbs work).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
v0.43.0.0
2026-08-09 17:25:00 -07:00
Garry TanandClaude Fable 5 130d321d23 v0.42.76.0 fix(upgrade,security,cli): strict flag validation, bootstrap-wedge class kill, federated chunk scope (#3902)
* fix(schema): bootstrap timeline_entries.event_page_id forward reference — un-wedge pre-v121 upgrades (#2626 #2594 #2579 #2537 #2536)

v0.42.56.0 (Chronicle, migration v121) added timeline_entries.event_page_id
and two partial indexes in the embedded schema blobs without extending
applyForwardReferenceBootstrap — any brain whose timeline_entries predates
v121 wedged initSchema at blob replay ("column event_page_id does not
exist") before runMigrations could apply v121, with no in-band recovery.

- Add the timeline_entries.event_page_id probe + column-only ALTER to
  applyForwardReferenceBootstrap in BOTH engines; FK + partial indexes land
  via the idempotent v121 / blob replay afterwards. Stays in the always-run
  bootstrap (never a migration hook — those skip oddly-stamped brains).
- REQUIRED_BOOTSTRAP_COVERAGE entry + strip blocks in both runtime tests.
- e2e: pre-v121 rewind → full initSchema converges (indexes re-created);
  wedged-brain recovery — a brain that already FAILED the upgrade attempt
  converges on retry with full final shape (column + FK + both partial
  indexes) and no ledger residue.

Absorbs PR #2548 (@chetan-guevara) and the e2e test from PR #2623
(@colinagent) — thank you both.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test(schema): coverage guard cross-references migration-added columns — close the scan hole that shipped the v121 wedge

The A2 static check treated a column as covered when the current CREATE
TABLE body declared it. But a column that is BOTH in the blob's CREATE
TABLE AND added by a migration is a forward reference by definition — on
pre-existing tables CREATE TABLE IF NOT EXISTS no-ops and the blob's
CREATE INDEX crashes initSchema before runMigrations can help. That mask
is exactly how timeline_entries.event_page_id passed the guard while
wedging every pre-v121 brain.

- buildIndexRefCoveragePredicate: migration-added columns (from
  extractAddedColumnsFromMigrations over the MIGRATIONS array) require a
  bootstrap ALTER; CREATE TABLE presence no longer counts for them.
- Unit test pins the v121 regression shape red/green with synthetic inputs;
  the A2 test pins the incident triple directly (migration-added +
  blob-indexed + bootstrap-covered).
- The strengthened predicate immediately surfaced two more latent wedges of
  the same class: minion_jobs.timeout_at + minion_jobs.idempotency_key
  (migration v7, blob-indexed, unprobed). Added probes in both engines +
  coverage entries + runtime strip blocks.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(security): serve --http hides the generated admin token from non-TTY output by default (#2624)

Generated admin bootstrap tokens printed into container/log-aggregator
stdout on every headless start. shouldSuppressBootstrapPrint now defaults
to hidden unless stderr is an interactive TTY; env-sourced tokens are
never printed; --print-admin-token is the explicit escape hatch for
capturing the value on a trusted non-TTY start; --suppress-bootstrap-token
still overrides everything. Unit-tested across all five postures.

Absorbs PR #2625 (@irresi) — thank you.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(security): legacy bearer tokens honor permissions.takes_holders over serve --http (#2529)

GBrainOAuthProvider.verifyAccessToken never returned takesHoldersAllowList,
so the serve --http dispatch site always fell back to ['world'] — remote
MCP callers with an operator-configured takes_holders grant saw only
public takes. The legacy branch now extracts permissions.takes_holders
exactly like src/mcp/http-transport.ts (fail-safe ['world'] default,
non-string entries dropped, malformed permissions JSON fails closed
without throwing), and AuthInfo carries the field as a typed contract.

OAuth-registered clients have no takes_holders storage on oauth_clients;
that lane is design work tracked in TODOS (column migration + DCR/
registration surface), not part of this hotfix.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(security): get_chunks honors the federated source grant + stops shipping embedding vectors (#2555, getChunks half of #2544)

The get_chunks op still used the pre-#2200 scalar pattern
(ctx.sourceId ? {sourceId} : {}) and engine.getChunks had no sourceIds[]
support — a federated client that could read a page via get_page got []
from get_chunks. The op now routes through sourceScopeOpts (canonical
ladder: federated array > scalar floor > nothing) and both engines gain
the getPage-style sourceIds[] precedence branch; the unset-opts 'default'
floor is preserved for local callers (importCodeFile contract).

While in the function: SELECT cc.* pulled every embedding vector over the
wire per chunk only for rowToChunk to discard them — replaced with the
explicit non-vector column list in both engines (the getChunks half of
#2544; the per-put_page getAllSlugs half is tracked separately).

getChunksWithEmbeddings stays scalar-only by design (engine-internal,
zero remote-reachable callers — documented at the interface).

Tests: op-level federated repro + isolation + default-floor bleed guard
(PGLite), engine precedence + Chunk-shape pin, and a DATABASE_URL-gated
engine-parity test covering all three scope shapes.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(auth): /admin/api/register-client accepts source + federatedRead bindings (#2143 enabler)

The HTTP register endpoint hardcoded source_id='default' and
federated_read=undefined — only the CLI could mint a client bound to a
non-default source, so HTTP-registered MCP clients wrote into 'default'
regardless of intent. The endpoint now accepts optional source /
federatedRead body fields, validated via assertValidSourceId with a
structured 400 on bad input; omitting both preserves the historical
default. The admin-UI form layer is a tracked follow-up.

Absorbs PR #2016 — thank you.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(scope,calibration): think reads over MCP; source-scoped takes reads; calibration CLI reachability + model resolution (#2078-class #2451)

- think op → scope:'read' for OAuth/MCP clients: the handler already forces
  save/take off for remote callers before persistence, so a read-scoped
  token can think without a write grant; local CLI persistence unchanged.
  Scope-annotation test carries an explicit remote-gated allowlist.
- takes_list / takes_search / takes_scorecard / takes_calibration route
  through sourceScopeOpts (the #2200 class on the takes read lane) with
  engine support in BOTH engines + tests.
- 'calibration' added to CLI_ONLY (the command was registered but
  unreachable — dispatch-gap class) and calibration_profile/voice-gate
  resolve models through the canonical gateway tier resolver instead of
  bare ids that parseModelId rejects.
- BigInt-safe local-op output normalization (bigintToStringReplacer,
  postgres.js wire parity) — first half of the #2450 fix; the
  formatResult default case lands with the cli-output commit.

Absorbs PR #2598 (@colinagent) and PR #2452 (@spinsirr) — thank you both.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(cli): output correctness — BigInt-safe rendering, search --json, files bigint, and 5 unreachable commands (#2450 #2527 #2042 #2035-class)

- normalizeLocalResult wraps the local-op output round-trip with the
  bigint→string replacer (postgres.js wire parity; a bare stringify THROWS
  on BIGSERIAL keys); formatResult's default renderer gets the same
  replacer so nothing upstream can crash it.
- search/query --json: CLI-local formatter flag threaded through the shared
  formatter — stdout is a parseable result array, never human text on the
  --json path (the #2042 residual).
- file_list normalizes size_bytes (Postgres BIGINT → Number) so MCP
  serialization and the CLI KB math survive; null preserved.
- NEW dispatch-gap guard: every handleCliOnly top-level case label must be
  reachable via CLI_ONLY. It immediately caught FIVE live unreachable
  commands: pages, backfill, reconcile-links, notability-eval (added to
  CLI_ONLY), and the documented 'gbrain search modes|stats|tune' dashboards
  (pre-fix, 'search modes' silently keyword-searched the word "modes") —
  now routed via a pre-dispatch subcommand gate. 'whoknows' stays on its
  op-alias route (collision guard); tracked with PR #2509.

Absorbs PR #2494 and PR #2531 (@javieraldape) and adapts PR #472
(@vinsew) — thank you.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(db): updateSourceConfig survives mixed-array config rows + repair/doctor cover the subagent jsonb columns (#2251)

The array-coercion branch called jsonb_each(elem) bare — a mixed array
(e.g. '["x", {"last_full_cycle_at": ...}]') threw 'cannot call
jsonb_each on a non-object' DURING row production, permanently failing
every subsequent updateSourceConfig (last_full_cycle_at could never be
written again). Non-object elements are now neutralized inline via a
CASE-guarded jsonb_each; the row self-heals to a flat object on the next
write, object elements' keys recovered. Pinned ungated on PGLite (real
Postgres semantics) and via a DATABASE_URL-gated e2e on the real engine.

repair-jsonb + doctor's jsonb_integrity check extend from 5 to 8 columns
(subagent_messages.content_blocks, subagent_tool_executions.input/output
— historical damage rows from the pre-v0.42.53.0 positional double-encode;
the write paths themselves were fixed in #2375) with a to_regclass skip
for brains predating those tables.

Adapts the repair/doctor extension from PR #597 (@vinsew) — thank you.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(cli): dry-run honesty — strict unknown-flag rejection CLI-wide + unify-types worker defaults to dry-run (#2185 #1575)

#2185: 'gbrain init --migrate-only --dry-run' applied REAL migrations —
flags are read ad hoc (args.includes) so anything a handler doesn't look
for was silently ignored, including intent-bearing safety flags. The CLI
now validates every flag pre-dispatch and pre-engine:

- Op commands validate against the operation contract (op.params +
  CLI-local --json/--explain), mirroring parseOpArgs so flag values that
  begin with '--' are never misread. parseOpArgs also gains the
  --key=value inline form (previously parsed as a junk key that consumed
  the NEXT token).
- CLI_ONLY commands validate against a GENERATED per-command registry
  (scripts/generate-flag-registry.ts scans each command's case block +
  imported modules + one level of relative imports; deliberately
  over-inclusive so a missed flag can't break a working invocation).
  Committed as src/core/cli-flag-registry.generated.ts;
  'bun run build:flag-registry' regenerates.
- Passthrough by construction: everything after '--', plus call / config /
  'jobs submit' payloads (handler-defined params are their contract).
- Guards: sweep test (every command × nonsense flag → error), acceptance
  tests (real flags, --no- negation, = form, -- passthrough), drift guard
  (every CLI_ONLY member has an entry), freshness guard (committed
  registry == fresh generator run), subprocess smokes incl. the literal
  #2185 repro failing loud with zero engine work.

BREAKING: scripts passing stray flags now fail loud with
"Unknown flag --x for 'gbrain <cmd>'" — that is the point.

#1575: the unify-types worker registration passed apply ?? true while the
handler documents 'Default false (dry-run)' — the canonical operator
invocation destructively retyped 25K+ pages by default. Now ?? false with
a structural test; explicit --params '{"apply":true}' is the only way to
mutate.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs(todos): file fix-wave 1 follow-ups (OAuth takes_holders design, parseFlags end-state, whoknows routing, #2544 half, #1558 UI, #2536 diagnostics)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(test): memory-safe unit runner — adaptive concurrency + serial OOM rescue pass

A default run (4 shards × 4 intra-shard files) holds up to 16 concurrent
PGLite WASM instances (~1.5GB each). With sibling Conductor workspaces
running their own suites, PGLite connect failed with 'Out of memory'
across every shard at once — 369 phantom test failures on a healthy
branch, indistinguishable from real breakage at a glance.

Two default-on layers in scripts/run-unit-parallel.sh:
1. Memory-aware sizing: total concurrency is capped to available memory
   (vm_stat on macOS, MemAvailable on Linux) at GBRAIN_TEST_MEM_PER_FILE_MB
   (default 1536) per concurrent file, shedding shards before intra-shard
   width. Quiet machines are unaffected (banner: mem-ok); pressured ones
   degrade instead of OOMing (banner: mem-adapted AxB→CxD).
2. Serial OOM rescue: failures whose shard log carries the WASM
   out-of-memory signature are re-run at --max-concurrency 1 after the
   fan-out drains. Phantoms pass serially → run goes green with an
   oom_rescued note and the failure blocks marked superseded; real
   failures fail again and stay red. Plain assertion failures never match
   the signature and never enter the rescue lane (existing exit-code and
   failure-log contract tests unchanged).

Escape hatches: GBRAIN_TEST_NO_MEM_ADAPT=1, GBRAIN_TEST_NO_OOM_FALLBACK=1.
Tests: OOM-once fixture rescued to exit 0; kill-switch stays red; banner
advertises the sizing verdict. Documented in docs/TESTING.md.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(test,cli): merge-seam repairs — shard timeout for the tripled suite, init flag-error contract, think scope exemption

Three post-merge repairs surfaced by the first full-suite run:
- Shard timeout 1500s -> 3000s: the suite roughly tripled since the cap was
  sized (~3900 -> 11k+ tests; PGLite inits replay 120 migrations, was 92).
  Two shards were killed mid-progress at 1500s.
- The #2185 pre-dispatch validator now emits the same error contract as
  init.ts's in-handler check it preempts: lowercase 'unknown flag' on
  stderr + structured {status:'error', reason:'invalid_flag'} on stdout
  for --json callers (pinned by test/init-migrate-only.test.ts).
- test/operations-trust-boundary.test.ts gets the same documented
  remote-gated allowlist for think's read scope (#2598) that
  test/oauth.test.ts already carries.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(test): rescue lane also covers externally-killed shards (sibling-workspace pkill / memory jetsam)

Second phantom class observed on a multi-workspace Conductor machine:
3 shards SIGTERM'd + 1 SIGKILL'd at ~700s under a 3000s cap, all
mid-progress — an external killer, not a wedge. The dead shards then
poisoned the serial pass (lock/state residue → 18 more phantoms), and
every one of the 18 passed standalone.

The runner now stamps per-shard start/end epochs; a shard dying on
143/137 before 80% of SHARD_TIMEOUT is classified externally-killed and
its file list joins the serial rescue queue (real wedges die AT the cap
and stay red). Serial-pass failures that occur while any shard was
externally killed are treated as suspect residue and rescued too.
Structural tests pin the detector, threshold, and routing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(auth): keep tokenEndpointAuthMethod terminal in the register-client destructure (PKCE structural contract)

The #2016 absorb appended source/federatedRead after
tokenEndpointAuthMethod; test/fix-wave-structural.test.ts pins
tokenEndpointAuthMethod as the final destructured field (v0.36.1.x #1077
PKCE regression contract). The added fields move into the regex's
optional-middle slot.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(test): re-apply the #2598 remote-gated scope exemption to master's oauth scope-annotation test

Taking master's v0.42.74.0 oauth.test.ts (its #2529 implementation) dropped
the think read-scope allowlist that PR #2598 carries; re-applied to match
test/operations-trust-boundary.test.ts.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(review): pre-landing review batch — CLI-local flag acceptance, json= coherence, re-landed getChunks trim, payload-aware jsonb repair, rescue-lane tightening

Ship Step 9 findings (checklist pass + 4 specialists), all verified before fixing:
- CRITICAL: the #2185 validator rejected --source/--dry-run on op commands
  (makeContext CLI-locals consumed outside the op contract) — 'gbrain search
  "x" --source y' exited 1. Exempted with parser-mirroring value consumption
  + unit/subprocess tests incl. global-flag acceptance.
- CRITICAL: --json=<v> diverged between validator (accepted) and parseOpArgs
  (junk-key path consumed the NEXT token, corrupting positionals). Parser now
  handles --json=true|false; =-forms of bare-only CLI-locals reject loud.
  parseOpArgs inline-= suite added (regression rule).
- CRITICAL: the master merge silently restored SELECT cc.* in both engines'
  getChunks while docs claimed the #2544 trim. Re-landed the explicit
  non-vector column list + a source-level structural pin so a merge can't
  silently undo it again.
- repair-jsonb/doctor: the subagent columns legitimately hold jsonb string
  scalars (persistToolExec binds pre-serialized strings) — unconditional
  unwrap would abort the repair run or corrupt legit values. jsonPayloadOnly
  predicate (JSON-container content only) on those 3 targets, mirrored in
  doctor + parameterized to_regclass + behavioral test (damage flagged,
  legit string ignored, absent table skipped).
- runner rescue-lane tightening: serial failures rescue-eligible only with
  their own OOM signature or after an external shard kill (residue), never
  because a sibling shard OOM'd — flaky serial tests stay red. Rescue passes
  no longer double-count into TOTAL_PASS; shard timeout scales when
  mem-adaptation sheds shards; negative-path tests (mixed run stays red,
  deterministic OOM-signature failure stays red).
- fail-closed remote spelling at 2 forward sites (ctx.remote !== false per
  the CLAUDE.md invariant); registry generator drops template-literal flag
  prefixes; real-PG e2es for the v121 + minion_jobs wedge classes; stale
  comments corrected.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore: regenerate flag registry after master merge (#3864 added extract help flags)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(review): red-team batch — dispatch-order validation, real --dry-run boolean, rescue-lane timeout/isolation/cap, parseable-payload repair gate

Red-team pass (post-specialist) findings, all verified before fixing:
- CRITICAL: validateCommandFlags checked the op lane before CLI_ONLY while
  dispatch runs CLI_ONLY first — dual-lane commands (think/salience/
  anomalies) were validated against the WRONG contract, rejecting documented
  invocations ('salience --kind entity'). Lane order now mirrors dispatch.
- CRITICAL: --dry-run was blessed as legal on op commands but parseOpArgs
  never SET it (trailing → nothing → ctx.dryRun false → the REAL destructive
  action ran; leading → consumed the next token). Now a CLI-local boolean
  exactly like --json, with --dry-run=false support and regression tests.
- CRITICAL: the rescue lane ran bun test WITHOUT --timeout=60000 (bun default
  5s) — PGLite phantoms re-failed on timeout and were mislabeled 'confirmed
  real'. Both rescue invocations now mirror the shard flags; serial files
  re-run one process per file (run-serial-tests.sh isolation contract);
  rescue wallclock capped at 2x the shard timeout.
- CRITICAL: the jsonPayloadOnly probe matched container-LOOKING invalid JSON
  ('[INFO] fetch complete') whose repair cast would throw and abort the run
  mid-loop. Predicate now gates on pg_input_is_valid (PG16+, same floor as
  the existing IS JSON usage) + per-target catch records and continues;
  doctor mirrors; behavioral test covers the lookalike row.
- Registry generator bounded at handleCliOnly's closing brace (the LAST case
  block absorbed ~100 junk flags from the rest of cli.ts, neutering strict
  validation for it); uppercase flag typos reject loudly in both lanes
  (handlers are lowercase-sensitive); --json=true spelling gets the
  structured invalid_flag envelope; get_chunks __all__ narrowing filed as a
  Wave 3 TODO.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(engines): getChunks trimmed SELECT must carry modality (codex P1-B)

The #2544 egress trim replaced SELECT cc.* with an explicit column list but
omitted cc.modality — every rowToChunk field except the vector must survive
the trim, or the embed round-trip (getChunks -> upsertChunks) rewrites image
chunks as text. Both engines; the structural pin now iterates the full
rowToChunk field list instead of spot-checking.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(cli): safety flags require consumption evidence in the flag registry (codex P1-A)

upgrade.ts prints a help hint naming another command's --dry-run; that
literal is depth-0 text for post-upgrade, so the generator allowlisted
--dry-run there — recreating the exact #2185 repro this wave kills
(post-upgrade --dry-run accepted, ignored, migrations run for real).
Safety flags now need a tight-quoted standalone literal (an args read
like has('--dry-run')) before the registry grants them; prose bleed
embeds the flag inside a longer string and never qualifies. Regenerated
registry drops --dry-run from post-upgrade, keeps genuine consumers.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* v0.42.76.0 fix(upgrade,security,cli): strict flag validation, bootstrap-wedge class kill, federated chunk scope

Version bump + CHANGELOG for the fix wave: CLI-wide unknown-flag
rejection with a generated per-command registry (#2185, #1575 class),
minion_jobs bootstrap probes + migration-aware coverage guard (v121
wedge class), get_chunks federated scope + egress trim (#2555, half of
#2544), think read-scope over MCP (#2598), register-client source
bindings (#2016, #2143 enabler), repair-jsonb/doctor subagent columns
with a parse-validated damage predicate, memory-safe unit-test runner.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs: update project documentation for v0.42.76.0

CONTRIBUTING.md: test-runner claims match the memory-safe 4-shard default
(was 8-shard) and the CLI-only command recipe now includes the
build:flag-registry regen step. KEY_FILES.md: repair-jsonb entry updated
to the 8-column parse-gated current state; new entries for the strict
flag-validation subsystem and src/core/source-id.ts; operations/engine/
serve-http entries updated for get_chunks scope ladder + trimmed SELECT
and the register-client HTTP source bindings.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs: apply doc-review fixes for v0.42.76.0

Cross-model doc review caught stragglers: docs/TESTING.md still said
8-shard in the file taxonomy, carried a two-generations-stale shard
timeout default (600s -> 3000s), and didn't name the new
run-unit-parallel regression test or the remaining runner knobs;
CONTRIBUTING.md's fast-loop file count predated the tripled suite
(92+ -> 1000+); test-count claims unified at 3700+; the
CLIENT_FENCED_WRITE_OPS comment in operations.ts still described
think as scope write; KEY_FILES names the exported findUnknownFlag.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(test): rescue lane survives CI — strip ::group:: prefixes, satisfy the bun-test-timeout guard

Two CI-only breaks from the master merge: (1) under GITHUB_ACTIONS the
shard wraps file sections as ::group::path.test.ts, so the rescue pass
extracted literal ::group:: non-paths that matched zero test files —
failing_files_in_log now strips the prefix; (2) master's new
check-bun-test-timeout guard greps for bare 'bun test' and tripped on
run_rescue's comment text (the invocations themselves carry
--timeout=60000) — comment reworded.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(test): shard-mechanics tests disable mem-adaptation — CI's 7GB runner collapsed explicit 2 shards to 1

The runner deliberately adapts even explicit --shards to available memory
(GBRAIN_TEST_NO_MEM_ADAPT=1 is the escape hatch); on GitHub's ~7GB runners
that collapsed the tests' 2-shard sandbox runs to 1 shard, breaking every
'shard 1/2:' expectation while passing locally. The tests pin shard
MECHANICS with tiny synthetic files, so they now set the escape hatch;
the one test that checks the mem banner overrides it back on.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
v0.42.76.0
2026-08-09 09:49:54 -07:00
MasaandClaude Fable 5 485c4773ca fix(think): extend 16K output headroom to OpenAI reasoning models (#3916)
maxOutputTokensFor() gave the 16000-token output budget only to
thinking-by-default Anthropic Claude 5 models; every other model kept the
4000 default. OpenAI reasoning models (the gpt-5 family and the numbered
o-series) bill internal reasoning tokens as output tokens counting against
max_tokens, so the 4000 cap starved think's actual answer the same way.

Add OPENAI_REASONING_MODEL_RE alongside the existing Claude-5 regex,
deliberately scoped to the gpt-5 family + numbered o-series with the
non-reasoning ChatGPT -chat snapshots (gpt-5-chat-latest,
gpt-5.2-chat-latest) excluded. Defaults (4000/16000) are unchanged and no
config knob is added; gpt-4o, other providers, and provider-prefixed
spellings outside openai:/openai/ keep 4000.

Tests pin the new 16000 cases (colon + slash forms), the chat-snapshot and
codex-mini-latest exclusions, and name/version boundaries (gpt-50, o3foo,
bare model names, openrouter-nested ids).

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 19:14:00 +07:00
MasaandClaude Fable 5 f8878be54e fix(cycle): price synthesize_concepts estimates from the canonical table (#3915)
The T1/T2 narrative cost estimate hardcoded Sonnet rates ($3/M in,
$15/M out) regardless of which model actually answered, violating the
one-canonical-chat-pricing-table invariant. On non-Sonnet models
(e.g. openai:gpt-5.2) the ~2.4x over-estimate tripped the $1.50 budget
gate early — degrading remaining T1/T2 groups to the deterministic
template with budget still left — and persisted the inflated cost into
receipts and rollups.

The estimate now resolves through canonicalLookup(result.model), the
model that actually answered. A canonical miss falls back to Sonnet-tier
pricing (derived from the canonical table, not hand-copied), mirroring
skillopt/preflight's lookupPrice policy: conservative, non-throwing, and
it keeps the budget gate effective.

The $1.50 gate, concept-generation logic, and model resolution are
untouched (model resolution is in flight separately on the same file).

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 19:00:13 +07:00
MasaandClaude Fable 5 0df56f1efa docs(brainstorm): correct estimateCost's cost figures to match its own formula (#3905)
The doc comment claimed brainstorm ~$0.05-0.15 and lsd ~$0.20-0.40 while
stating it anchors on Sonnet pricing. Plugging the function's own token
budgets (3000 in/cross, 250 out/idea, 350 judge-in/idea, 200 judge-out/idea)
and the profile constants (brainstorm 4x6x3, lsd 2x12x4) into Sonnet's
canonical $3/M-in $15/M-out yields ~$0.78 and ~$0.96 — roughly 5x and
3x the stated ranges. No single model in the canonical pricing table makes
both stated ranges true, and the comment, formula, and profile constants
were all introduced in the same commit (613da940), so this was an arithmetic
slip rather than drift.

Comment-only; no behavior change. The runtime preview already prints the
real number via canonicalLookup.

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-09 18:46:05 +07:00
Ziyang Guo 01d9639204 fix(facts): salvage valid extractor candidates (#3894) 2026-08-09 18:31:44 +07:00
Garry TanandClaude Fable 5 f15480b9d0 v0.42.75.0 fix(pglite): in-place WAL auto-repair for the macOS Aborted() startup crash (#2575, #223, #1670) (#3901)
* fix(pglite): in-place WAL auto-repair for the Aborted() startup crash (#223, #1670, #2575)

The 'macOS 26.x WASM bug' was a misdiagnosis: an unclean shutdown (typically
the OS-upgrade reboot) tears the data dir's WAL, and every subsequent open
fails WAL replay inside WASM with an opaque RuntimeError: Aborted(). This
ports the pg_resetwal recovery upstream rejected (electric-sql/pglite#994,
by @yestheboxer) and wires it into connect() as bounded auto-repair:

- src/core/pglite-resetwal.ts: pg_resetwal for PG17 NodeFS dirs, fail-closed
  layout validation, atomic+durable writes (tmp+fsync+rename), idempotent.
- src/core/pglite-repair.ts: whole-pg_wal-dir rename backup (zero transient
  disk), overwrite-order restore with mtime guard, cooldown sidecar +
  episode-scoped backup retention (newest 3 episodes), and a never-throws
  engine seam. Kill-switch: GBRAIN_PGLITE_WAL_REPAIR=off.
- pglite-engine.ts: verdict rename macos-26-3 -> wasm-abort, classifier now
  matches the real production message (it previously fell to 'unknown'),
  corrupt-beats-wasm precedence preserved, honest per-outcome error copy
  incl. the failed-not-restored arm, and repair only under a cleanly-acquired
  lock (new LockHandle.reaped provenance; never after reaping a holder).
- gbrain pglite-repair: manual dry-run/repair command (validate-before-lock,
  serve/reaped refusals, no --force by design).
- doctor: pglite_data_dir fs-check with recurrence escalation and backup
  inventory when a PGLite brain fails to connect.
- reinit-pglite: embedding flags default from file-only config so the
  recovery ladder's rebuild rung works bare mid-outage.
- stringifyPgliteInitError: message-less Emscripten ErrnoError objects no
  longer surface as [object Object].

Regression-tested against real brains: corrupt every WAL segment (truncate
and garbage variants), reopen, auto-repair fires, original rows readable,
process.exitCode stays contained (#2084).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs(pglite): replace the macOS-26.x misdiagnosis with the corrupt-WAL recovery ladder

README + INSTALL.md shipped (via #1671) the claim that PGLite is incompatible
with macOS 26.x and that a Bun/WASM fix would restore it. The real cause is
torn WAL state from the upgrade reboot, now auto-repaired in place. Rewrites
those sections around the recovery ladder (auto-repair -> gbrain pglite-repair
-> reinit-pglite -> engine switch; native-Postgres recipe kept, credit
@roysaurav), adds the ENGINES.md troubleshooting section, updates the
KEY_FILES.md entries to current truth, files the two follow-up TODOs
(SIGTERM engine-close extension; pglite upgrade blocker), and regenerates
the llms bundles.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(pglite): harden WAL auto-repair (pre-landing + adversarial review)

Review-army (security/testing/maintainability/perf) + Claude & Codex
adversarial passes on the WAL-repair wave. Correctness + safety hardening,
no behavior change to the happy path:

- Live-writer safety: repair refuses any reaped lock acquisition, a corrupt
  (unknowable-liveness) reap writes a cross-process quarantine marker that
  gates auto-repair AND the manual command for 10 min, isProcessAlive treats
  only ESRCH as dead (EPERM/malformed-pid read as alive), and a live
  postmaster.pid (native Postgres) is refused. Lock heartbeat + initial write
  are atomic (tmp+rename) so a torn read can't misclassify a healthy holder;
  an in-flight acquisition is no longer mistaken for corrupt.
- resetWal verifies the stored pg_control CRC before trusting/re-signing it —
  a damaged control file routes to rebuild instead of laundering corrupt
  checkpoint counters under a fresh CRC. Atomic 'wx' writes (no symlink
  follow), whole-pg_wal-dir rename backup, 64MB seg-size cap.
- Honest failure reporting: repairPgliteWal threads the real restore result
  out via WalRepairError so the 'failed-restored' vs 'failed-not-restored'
  message never lies; the not-restored copy names the correct restore paths.
- Episode lifecycle: episodes close on the next healthy connect (not just on
  a verified repair), a gutted (restored) backup loses its pin, stale (>24h)
  episode backups aren't reused, and the cooldown also caps repaired-only
  crash loops. Empty backup dirs are pruned on refusal.
- Command: rejects unknown flags and valueless --path (a destructive command
  must not silently mis-parse), confirm prompt goes to stderr (stdout stays
  clean for --json), embedding-flag defaults come from the config file only.
- Symlink confinement extended to global/; sidecar reuse path validated
  (prefix + no '..' + must still hold pg_wal); sidecar writes atomic.
- doctor recurrence escalation counts all attempts; data dir absolutized.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs(pglite): current-state KEY_FILES + WAL-repair follow-up TODOs

KEY_FILES.md pglite entries updated to the hardened truth (reap marker +
quarantine, atomic writes, CRC gate, global-symlink refusal, WalRepairError,
episode lifecycle). TODOS.md files the deferred judgment-call follow-ups
(unclean-shutdown gate on auto-repair; non-gbrain pglite consumer boundary;
mixed-version torn-lock double-read).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore: bump version and changelog (v0.42.75.0)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
v0.42.75.0
2026-08-08 17:01:20 -07:00
Ziyang Guo 0b47afbf40 fix(doctor): resolve MSYS executable paths (#3865) 2026-08-08 19:37:38 +07:00
Ziyang Guo 9f81e1d8b2 fix(cli): expose extract flags in help (#3864) 2026-08-08 19:21:05 +07:00
Ziyang Guo 86555e39bb fix(autopilot): honor source pull policy (#3861)
Fixes #3835

Use a canonical positive pull flag for sync and autopilot-cycle jobs while preserving legacy noPull payloads. Parse source config consistently so local-only sources skip pulls and PGLite remote sources still pull.

Test: bun test test/job-pull-policy.test.ts test/autopilot-fanout.test.ts test/autopilot-fanout-wiring.test.ts test/sources-load.test.ts
2026-08-08 19:03:36 +07:00
zay 3257758492 fix(doctor): align maintenance scope with canonical files (#3854) 2026-08-08 18:45:53 +07:00
Garry TanandClaude Fable 5 a948dfd6e2 v0.42.74.0 fix(security): honor takes_holders over serve --http + agent-voice default-deny CORS (#2529 #2477) (#3868)
* fix(auth): honor permissions.takes_holders for legacy bearer tokens over serve --http (#2529)

The OAuth provider's legacy access_tokens branch parsed permissions.source_id
but never read permissions.takes_holders, so the /mcp dispatch site's
fail-closed default pinned every remote caller to world-only takes visibility
— set-takes-holders was a silent no-op over serve --http, in both directions
(grants above world never applied; restrictions below world didn't either).

- src/core/legacy-token-scope.ts: new parseTakesHoldersAllowList shared by
  BOTH transports (the drift between the legacy HTTP transport's correct
  inline parse and the OAuth provider is how this bug shipped). [] preserved
  as explicit deny-all; non-array → undefined → consumer defaults ['world'].
- src/core/operations.ts: AuthInfo.takesHoldersAllowList typed field
  (same ride-along as sourceId/allowedSources).
- src/core/oauth-provider.ts: legacy branch threads the stored grant.
  OAuth-client tokens unchanged (no per-client storage — TODO filed).
- src/mcp/http-transport.ts: converged on the shared helper (behavior no-op).
- src/commands/serve-http.ts: sidecar cast replaced by the typed field.
- src/core/facts/meta-hook.ts: hashAllowList gives [] its own cache key
  (cache identity only — payload filtering stays visibility-based).

Tests: 7 verifyAccessToken cases (grant/absent/garbage/deny-all/mixed/
oauth-client/column-default), pure-helper describe, meta-hook cache-key pin,
and a Postgres e2e (test/e2e/serve-http-takes-holders.test.ts) pinning the
issue repro end-to-end over POST /mcp — the seam that had no coverage.

Reported by @Derek95king.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(agent-voice): default-deny CORS + origin gate + loopback bind in reference server (#2477)

The recipe reference server reflected any Origin into
Access-Control-Allow-Origin on every route, including the unauthenticated
side-effectful POSTs. Recipe is install_kind: copy-into-host-repo, so the
reference is the source of truth for every future install.

- Default-deny CORS: headers emitted only for exact matches against the new
  AGENT_VOICE_CORS_ORIGIN allowlist (comma-separated), with Vary: Origin;
  Allow-Credentials never set. Same-origin /call needs no configuration.
- Origin gate on /session and /tool: CORS headers gate response reads, not
  request sends — a no-preflight "simple" cross-origin POST still executes.
  Disallowed Origins now 403 before any body read / upstream fetch / tool
  dispatch. No-Origin callers (curl, Twilio, native) and same-origin pages
  (Origin host == Host, tunnels included) pass. DNS rebinding stays a
  documented production-checklist item (TODO filed).
- Loopback-default bind: HOST env, default 127.0.0.1 (mirrors gbrain
  serve --http --bind default); HOST=0.0.0.0 for containers/LAN.
- Startup log prints the bind + CORS posture; recipe md + install manifest
  bumped to 0.1.1 with the production checklist rewritten to match.

Tests: test/agent-voice-cors.serial.test.ts spawns the real server twice and
pins default-deny, allowlist echo + trimming, preflight behavior, and the
gate's ordering (evil-origin 403 vs no-origin reaching the handler).

Reported by @sebastiondev.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test: agent-voice origin gate fails closed on malformed Origin (#2477)

Coverage-audit follow-up: a cross-origin POST with an unparseable Origin
header must 403 (new URL() throws → originAllowed returns false), never
fall through to the handler. A bypass here would defeat the gate.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* test: pin loopback-bind default + wire-level fail-closed takes default (#2529 #2477)

Pre-landing review (testing specialist) flagged two revert-catching gaps:

- #2477 HOST default (127.0.0.1) had no assertion — a regression to
  all-interfaces would pass every test. Capture the server's startup log
  and assert the loopback bind; add a HOST=0.0.0.0 override case.
- #2529 serve-http `?? ['world']` default branch was only unit-covered.
  Add a 4th e2e case: a legacy token with no takes_holders key sees
  world-held takes but NOT brain-held ones over POST /mcp, pinning the
  fail-closed default end-to-end. Also assert the deny-all case returns a
  successful (non-error) tool result so the negatives can't pass vacuously.

Verified: agent-voice 4/4, serve-http-takes-holders e2e 4/4 (real Postgres).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(auth): shared permissions decode + collision-free hot-memory cache key (#2529)

Adversarial-review hardening on the #2529 fix:

- The two transports shared parseTakesHoldersAllowList but still decoded the
  permissions column differently: the OAuth provider JSON.parse'd a
  string-typed value, the legacy HTTP transport didn't. On a double-encoded
  jsonb string scalar (#2339 class) a deny-all token would fail open to
  ['world'] on the HTTP transport while the provider honored it. Extract
  coerceLegacyPermissions into the shared module and route both through it, so
  "the two transports cannot drift" is literally true (shared decode + shared
  parse). Arrays/scalars/malformed strings → undefined (no grant).
- hashAllowList used bare sentinels ('_' for undefined, '(empty)' for []),
  which collided with real holder values ['_'] and ['(empty)']. Encode
  collision-free (undefined → 'none', else JSON.stringify(sorted)) so the
  []-vs-undefined cache separation the #2529 change relies on holds for every
  holder value.

Tests: coerceLegacyPermissions unit cases (object/JSON-string/malformed/
array/scalar), all existing takes-holders + meta-hook + e2e suites green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* v0.42.74.0 fix(security): honor takes_holders over serve --http + agent-voice default-deny CORS (#2529 #2477)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs: document agent-voice HOST + AGENT_VOICE_CORS_ORIGIN env vars in install hint (v0.42.74.0)

The post-install hint's env-var quick-start predated the #2477 hardening and
listed neither the loopback-default HOST bind nor the default-deny
AGENT_VOICE_CORS_ORIGIN allowlist. Add both as optional entries (safe by
default) and refresh the stale startup-log line to match the server's actual
loopback-bind output.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* docs: correct CHANGELOG command guidance + TODOS filing version (#2529 #2477)

Ship-stage document-release caught two wrong CLI invocations in the v0.42.74.0
CHANGELOG "To take advantage" block: `gbrain auth permissions <token>` has no
read-only view form (that shape errors + exits 1 — set the scope directly with
`set-takes-holders <values>`), and `integrations install agent-voice --refresh`
requires `--target <host-repo>`. Also correct the follow-ups TODO header from
the plan's stale v0.42.56.0 guess to the actual ship version v0.42.74.0.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore(deps): bump js-yaml to 3.15.1 — osv GHSA-5p4m-2wfm-xmqj (#2529 #2477)

osv-scan flagged js-yaml@3.15.0 (High, CVSS 7.5), fixed in 3.15.1. The
transitive copy (gray-matter → js-yaml) was pinned to ^3.15.0 by the
package.json `overrides` block; bump both the direct dependency and the
override to ^3.15.1 so the vulnerable version is gone from bun.lock entirely
(gray-matter/js-yaml now resolves to 3.15.1). Patch bump, in-range, frontmatter
parsing verified (markdown + frontmatter + import + oauth suites green).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* chore(deps): bump admin nanoid to 3.3.18 — osv GHSA-2v37-7h3g-55p8 (#2529 #2477)

osv-scan flagged nanoid@3.3.16 (High, CVSS 8.2) in admin/bun.lock, fixed in
3.3.17. nanoid is transitive (postcss → nanoid), so pin it in the admin
overrides block; refresh resolves to 3.3.18 (latest patched 3.x). Admin SPA
build verified green; both root and admin lockfiles now scan clean.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
v0.42.74.0
2026-08-07 15:31:53 -07:00
15b9863d13 v0.42.73.2 fix(security): fence dedup-resolved writes to the caller's own write scope (#3809)
* fix(security): fence the dedup-resolved slug under the caller's own confinement

put_page's resolved-slug re-check tested `ctx.auth.boundSlugPrefixes` only.
The delegated submit_agent -> subagent context carries `viaSubagent` +
`allowedSlugPrefixes` but no `auth`, so a slug-bound client holding `agent`
scope could delegate a write and have importFromContent's dedup pre-check
redirect it onto a page outside its grant — where the disk write-through
then re-rendered the victim's file with the caller's provenance.

The re-check now applies whichever confinement the caller is actually
under (OAuth binding and/or subagent allow-list / legacy namespace) via
`slugOutsideCallerFence`, which composes the existing match rules rather
than re-deriving them. Dedup returns status 'skipped' before any DB write,
so the throw still rolls nothing back. The denial does not name the
resolved slug (slug-enumeration oracle) and reads "your write scope",
since either confinement can trigger it.

Reported privately by Aleksei Razsadin.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test: coverage for the OAuth in-fence redirect and the missing-subagentId guard

* v0.42.73.2 fix(security): fence dedup-resolved writes to the caller's own write scope

VERSION + package.json + CHANGELOG for 0.42.73.2.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs: state that the write fence follows a delegated write

---------

Co-authored-by: Garry Tan <garrytan@gmail.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
v0.42.73.2
2026-08-05 07:59:50 +07:00
aecb33e795 v0.42.73.1 revert(ci): remove the PR gate and withdraw the v0.42.72.1 contribution requirements (#3805)
* v0.42.73.1 revert(ci): remove the PR gate — it cannot function on this repository

The gate needs an ANTHROPIC_API_KEY Actions secret for its verdict and
read-write workflow permissions to post a comment or set a label. This
repository grants neither, and both are admin-only, so it can only ever skip.

It also caused a real incident before that was understood: a read-only token
turned every API call into a 403, the code treated that as a crash, and the
check went red on an outside contributor's PR four times with no comment
explaining why. v0.42.73.0 fixed the crash, but a check that runs on every PR
and can never reach a verdict does not earn a place in the repo.

Removes the workflow, the script, its type surface, and its test file. The code
is preserved in git history at v0.42.73.0. If it is ever restored, the
mechanical half (intent/screenshot policy, title rule, red flags) should render
to the Actions job summary rather than a comment — that needs no token
permission and no API key.

CONTRIBUTING.md's intent-paragraph and screenshot requirement is unchanged and
stands as written; it is enforced by maintainers reading PRs, as before.

typecheck clean, verify 34/34, llms bundles regenerated.

* v0.42.73.1 revert(docs): withdraw the human-intent-paragraph + screenshot contribution requirement

This reverts commit 6d1232d5a6 (v0.42.72.1).

CONTRIBUTING.md, both issue templates, and the pull-request template return
to their pre-2026-08-02 state. VERSION/package.json/CHANGELOG keep moving
forward (0.42.73.1); the v0.42.72.1 CHANGELOG entry stays as historical
record, and the 0.42.73.1 entry now describes the withdrawal.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Garry Tan <garrytan@gmail.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
v0.42.73.1
2026-08-05 04:20:30 +07:00
Garry Tan e5dee4fb78 fix(deps): bump four override pins past newly-published CVEs
osv-scan passed on the last two release PRs and fails on this one, so these are
newly published, not tolerated debt: 2 High + 4 Medium across fast-uri (3.1.4 →
3.1.5), hono (4.12.25 → 4.12.34), ip-address (10.1.1 → 10.3.1), and admin's
postcss (8.5.10 → 8.5.23). All four are transitive and pinned through the
overrides block, so the fix is the pin, not a dependency addition.

Resolved: fast-uri@3.1.5, hono@4.13.0, ip-address@10.4.0, postcss@8.5.25.
v0.42.73.0
2026-08-04 22:25:25 +08:00
Garry Tan 3c26f2eaf2 fix(ci): a permission failure must never red-X a contributor's PR (live incident)
The gate's first live runs put a red X on an outside contributor's PR five
times over, with no comment explaining why. This repo's GITHUB_TOKEN is
read-only, so every comment and label call returned 403; the throw reached the
top-level handler as exit 2.

The gate is advisory. A repository permission problem is an operator condition,
never a statement about the PR under review. 401/403/404 from GitHub now emit a
loud operator-facing warning naming both blockers (workflow permissions and the
missing ANTHROPIC_API_KEY secret) and exit 0. A genuine outage or a bug in here
still fails visibly at exit 2. Pinned by four tests including the entry-handler
routing; mutation-tested.
2026-08-04 22:25:25 +08:00
Garry Tan 615c33e5b5 v0.42.73.0 feat(ci): strict PR usefulness gate + five contributed correctness fixes
Release-only commit: VERSION, package.json, CHANGELOG. All code already on
master. Covers the 8 gate commits (#3794, closes #3698), plus #3764 import
stdout, #3759 dry-run chmod, #3726 cycle model telemetry, #3751 integrity
counter, #3739 doc comment, and #2655 slug_filter.
2026-08-04 22:25:25 +08:00
40d1d4cabc fix(import): make imported pages readable by gbrain's own conversation parser (#3788)
* fix(envelope importer): honor the envelope's own counts, and stop a second import destroying the first

Two ways this importer lost conversation history at exit 0 with zero stderr
bytes. Both reproduced against output from the real memvelope converter, not
hand-authored envelopes — the blind spot that let them through review.

F1. envelope-v0 makes meta.conversation_count and meta.message_count mandatory:
the envelope states its own totals. The script read neither. A file declaring
353 conversations and 9412 messages that actually held one of each imported
one, exited 0, and printed a receipt internally consistent with the reduced
output. The receipt also counted only pages, never messages, so every
message-level loss was invisible by construction — a conversation arriving with
one turn instead of forty still writes exactly one page.

Each declared count is now checked on its own, before the first write. A count
that disagrees with the contents, or that is present but is not a non-negative
integer, refuses the import (exit 2). A count that is simply absent cannot be
compared, so the envelope still imports — but stderr names the field whose half
of the check was skipped, because an unchecked import must never look like a
checked one. The receipt reports messages as well as pages, and says so out
loud when messages read exceed messages written.

F2. filesWritten was per-process and the default outDir is a fixed literal, so
a second import into the same directory clobbered the first with no read-back
and no warning. The trigger is the positional fallback conv-N, which fires
whenever c.id is not a non-empty string — and the spec is explicit that id is
string | null and that a converter MUST NOT synthesize one, so null is the
CONFORMING shape, not malformed input. The reference converter emits it for any
ChatGPT export lacking conversation_id and id. Filenames then key off array
position rather than identity, and two unrelated exports both put their first
conversation at conv-1.

Target files are now examined before anything is written. Byte-identical
content is a re-import; a page this importer wrote from the SAME conversation
id is a refreshed export legitimately updating its own page. Anything else — a
foreign file, or one of ours whose conversation id cannot be matched — refuses
the import (exit 2) and writes nothing. Refusing rather than disambiguating
keeps the importer from inventing an identity the envelope declined to state,
and the remedy is a different output directory.

Both checks run before the first write, so a refused import leaves no partial
output to be mistaken for a whole one, and does not even create the directory.

Verified against all 12 golden fixtures from the memvelope reference converter
and 7 envelopes freshly produced by running that converter over synthetic
ChatGPT and Claude exports: every one imports at exit 0 with zero stderr bytes,
and all 19 produce byte-identical pages to the previous script. The guards do
not false-positive on legitimate producer output.

Known limit, documented in the header: the target-file check is check-then-
write, not atomic. Two simultaneous imports into one directory can both pass it
— measured 19 refused / 21 raced over 40 trials, against 0 / 40 before.

17 new tests; the 13 existing tests are unchanged and still pass. No new
dependencies: package.json and bun.lock are byte-identical.

* fix(envelope importer): close the three holes the adversarial pass found in the guards

An eight-agent refutation round attacked every claim made for the previous
commit. Four claims survived untouched; three did not, and all three were in
code that commit introduced. Each is reproduced, then closed, with tests.

Identity was compared on the TRIMMED conversation id, because that is what the
filename slugs and what the frontmatter recorded. So two ids differing only by
surrounding whitespace — both copied verbatim by the reference converter, both
schema-valid — looked like one conversation to the guard, and the second import
destroyed the first at exit 0 with zero stderr bytes. The surviving page then
recorded an id that appeared nowhere in the envelope that wrote it. The
frontmatter now records the id verbatim, as the spec requires, and identity is
matched raw. The filename still slugs the trimmed form, so no page's bytes
change for any id without surrounding whitespace.

The identity scan required a page to start with exactly `---\n`. A page THIS
IMPORTER WROTE that later picked up CRLF line endings or a UTF-8 BOM — a git
checkout with core.autocrlf, a cross-platform sync, an editor save — was
therefore reclassified as foreign, and one such page refused the ENTIRE
envelope at exit 2. The previous script treated the same mutation as a harmless
overwrite, so the guard had turned a cosmetic byte change into an unrecoverable
block. Both are normalized away before the scan. The message for a file that
genuinely cannot be recognized no longer asserts "was not written by this
importer" — a claim this code cannot make, and one that was false for exactly
the pages it was being printed about.

The new message-delta warning announced "N message(s) in the envelope are not on
disk" whenever pages collided. Duplicate ids are conforming input — the spec has
merging never deduplicate — so converting an old export together with a newer
one, which is what the memvelope CLI tells users to do, fired it routinely while
every unique turn was on disk. It now states what it knows: the overwritten
pages carried N messages that are not on disk, and if those were earlier copies
the surviving page may already hold them. The raw tally stays, since hiding it
is what made message-level loss invisible to begin with.

Two header claims were false as written and are corrected: message text is
byte-verbatim for 18 of the 19 producer envelopes, not all 19 — the
lone-surrogate fixture writes U+FFFD, unchanged from before and out of scope —
and sequential coverage is no longer described as complete. Three further limits
are now stated rather than left implicit: an id-less conversation cannot be
refreshed in place (refused, not applied — the deliberate trade, since the same
ambiguity resolved the other way is the defect being fixed); a conversation
whose created_at moves orphans its earlier page rather than updating it; and a
hand-written lookalike is indistinguishable from a page this importer wrote.

Rejected after reproducing it: a "silent loss" via a duplicate top-level
conversations key in the JSON text. JSON.parse keeps the last such key, which is
language semantics, is identical on the previous script, and cannot be produced
by JSON.stringify.

36 tests pass, 0 fail. The 13 pre-existing tests remain byte-identical as the
first 306 lines of the file. All 19 real-producer envelopes still produce
byte-identical pages. package.json and bun.lock unchanged.

* fix(envelope importer): write pages gbrain's own conversation parser can read

Every page this importer wrote declared `type: conversation` — which opens the
gate to conversation-facts extraction, chronicle eligibility and the
conversation_format_coverage check — and then presented a turn header

    **Assistant** (2025-11-02T14:22:51.000Z · m2):

matching none of the 17 built-in patterns in the conversation parser. The
extractor parsed zero messages, incremented `pages_skipped`, and said nothing.
Pages were stored and searchable; no fact was ever extracted from any of them.

Measured on two throwaway PGLite brains fed the same 13 conversations, one
written each way: `conversation-parser scan` goes from 13/13 `no_match` with 0
messages to 13/13 `imessage-slack` with 33; `extract-conversation-facts
--dry-run` goes from "Skipped 13 page(s)" to every page segmenting and reaching
the extractor; `doctor` conversation_format_coverage goes from warn "13/13 ...
match NO built-in pattern" to ok "13 pages: imessage-slack=13".

The turn header is now `**Me** (2025-11-02 14:22):`, the one shape that parser
reads. 24-hour, not 12-hour-with-AM/PM: both match and both were measured to
reconstruct all 24 hours exactly, so the tie is broken on the fact that 24-hour
is a substring of the envelope's own `ts` (no hour arithmetic, so the 12/0
boundary cannot be got wrong) and sorts chronologically within a day.

That header can carry a wall clock and nothing else, so per-message identity
moves to frontmatter as an array of maps:

    messages:
      - id: "m1"
        ts: "2025-11-02T14:22:51.000Z"

An array, not a map keyed by id — a map discards order and collapses the
duplicate ids the spec permits. Consumers index it by position.

Every scalar is JSON-encoded, so every timestamp is quoted. Unquoted, js-yaml
reads an RFC 3339 scalar as a JS Date: microseconds truncate, a +05:30 offset
normalises away, and gbrain's own coerceFrontmatterString slices a Date to 10
characters — the time of day gone. It is sticky, too. The suite fails if a
timestamp is ever emitted unquoted, and carries a sentinel proving that guard
fires.

Also drops the `---` rule between turns: it is a non-blank line matching no
pattern, so the parser appended it to the preceding message and every extracted
text ended `...\n---`.

Verified over all 12 golden fixtures from the memvelope reference converter
(exit 0, zero stderr, 33/33 messages, every id and ts byte-identical after the
round trip) and over fresh envelopes built by running that converter over
synthetic vendor exports. 52 tests green, up from 36.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(envelope importer): build the offset-shifted header clock without Date.UTC

Self-review of the branch, before the refutation round. `headerClock()` is the
only place in this script that does arithmetic on a timestamp, and its offset
branch had two ways to emit a header that is off — or that does not parse at
all.

`Date.UTC` applies MakeFullYear: a year of 0..99 is read as 1900+y. The shape
regex accepts any four digits, so `0050-01-01T00:30:00+05:30` was rendered as
1949-12-31 — the page moved by nineteen hundred years, silently, at exit 0.
Built with the UTC setters instead, which do not remap.

And `getUTCFullYear()` returns `49`, not `0049`. The pattern's regex requires
`\d{4}`, so an unpadded year emits a header that matches nothing and the turn is
appended to its neighbour as a continuation — one message where there were two.
The year is now padded like every other field.

Neither is reachable from the reference converter, which emits `Z`. Both are
reachable from a conforming envelope: the spec types `ts` as any date-time
string, and RFC 3339 offsets are legal.

18 new tests pin the clock on its own: every Z / offset / designator-less form,
both day-boundary crossings, `+0530` without a colon, the sub-100 year in both
branches, and the six unusable-`ts` shapes that must fall back to the
conversation's date at midnight rather than fabricate a clock or drop a turn.
Three more hostile ids added — U+2028, U+2029 and U+0085 are line breaks in
YAML 1.1 and JSON.stringify emits all three raw, so they are the sharpest
version of the injection the quoting exists to stop. js-yaml 3.14 does not
honor them as breaks; measured, not assumed.

72 tests pass, 0 fail. Adjacent suites (conversation-parser, markdown,
extract-conversation-facts, doctor backlog): 380 pass, 0 fail.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(envelope importer): name the built-in pattern count the header claims

'None of the built-in patterns' is a claim about a number the reader cannot
see. There are 17, and `gbrain conversation-parser list-builtins` is how to
count them.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(envelope importer): stop the conversation title manufacturing a turn

An adversarial round against the new format. Three claims in the file header
were wrong or overstated, and one of them was a defect.

The H1 heading is the only place a third-party string reaches the BODY — every
frontmatter value is JSON-escaped, the heading was interpolated raw. That was
merely untidy while nothing parsed the body. Now that the turn headers are
legible, a title carrying a newline manufactures a TURN, and one that lands
AHEAD of every real one, so `messages[0]` in frontmatter names content the user
never sent and every id after it is off by one. Measured: a 2-message
conversation titled "Real Title\n\n**Me** (2020-01-01 09:00): INJECTED" parses
to 3 turns, the first attributed to 2020. The heading is flattened to one line;
the verbatim title, newlines and all, is still recorded in frontmatter.

The empty-title fallback also disagreed with itself — "Untitled conversation"
in frontmatter, "Conversation" in the heading, two names for the same missing
thing, and parseMarkdown prefers the body's H1 when frontmatter has no title.
One fallback now.

Corrected in the header, because a false claim there is worse than a limit:

"the frontmatter array still holds exactly the real turns, so the two can be
reconciled by count" was FALSE. parse.ts picks one pattern per page, scored on
the first 10 body lines, and only re-scores full-body under 0.3 — so a pasted
Slack or Telegram snippet inside one message only has to win that window, and
the length of the real conversation is irrelevant. With four `**[09:0N] Name:**`
lines quoted inside message 1: 40 real turns are replaced by 4 fabricated
speakers at fabricated times, at exit 0, with phase regex_match, so
pages_skipped stays 0 and doctor's conversation_format_coverage reports OK. And
at 4 real turns against 4 pasted lines the COUNTS AGREE while every speaker and
timestamp is fabricated — which is exactly the check the old wording offered as
the remedy. The table is in the header now, with what count-comparison does and
does not catch. Closing it needs a parse.ts change, not an importer change.

"every timestamp is QUOTED" was true only for conforming input. JSON quotes
strings; an envelope whose `ts` is a number emits `ts: 1762093371000` unquoted.
That is a YAML integer, not a Date, so it is lossless and carries no truncation
hazard — but the claim as written was wrong, and the behavior is now pinned by
test rather than described.

Two more limits stated: minute resolution is the parser's ceiling, not this
format's — every branch of buildIso hardcodes :00 and no built-in captures
seconds, so two turns in the same minute collide (claude-basic m1 15:02:00 and
m2 15:02:31 both parse to 15:02:00Z) and frontmatter `ts` is the only
full-resolution value on the page. And the array survives a gbrain rewrite
semantically, not textually: serializeMarkdown re-emits `id: "m1"` as `id: m1`.

81 tests pass, 0 fail. Adjacent suites: 389 pass, 0 fail. All 12 golden
fixtures still import at exit 0 with zero stderr.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(envelope importer): range-check the per-message clock, and arm two guards

Second adversarial round. The shape regex counts digits; it does not know a
calendar. `2025-99-99T99:99:00Z` was copied straight into a turn header — and
`imessage-slack` MATCHES that header, so gbrain stored an instant no calendar
contains. `2025-02-30` is the sharper one: it yields a VALID JS Date, silently
shifted to March 2. The script already validates `created_at` before it reaches
a header, with a comment explaining why; the per-message clock is the same
untrusted surface and is used far more often. It is validated now, by round
trip — a date that does not survive its own UTC round trip was never a date.

Also removes a provably unreachable `Number.isFinite` check: over the whole
space the shape regex admits, the constructed instant is always finite.

Two guards had no test that fires. Both are now pinned:

- The `created_at` validation. Dropping it let a hostile value break the turn
  header it was supposed to anchor, and the whole suite stayed green.
- The determinism guard — the reason TS_SHAPE exists rather than
  `new Date(string)`, which parses a designator-less date-time as LOCAL time.
  The one test that touched it only caught the mutation because this box is
  America/New_York; on a UTC runner it passed. TZ is now pinned explicitly and
  the case runs under four zones including UTC and Pacific/Kiritimati.

94 tests pass, 0 fail.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* docs(envelope importer): correct five claims the adversarial round falsified

None of these change behavior. All five were statements in the file header that
an independent pass could not reproduce, and a false claim in a header is worse
than a limitation, because a reader has no way to tell.

"the field is read nowhere outside builtins.ts", of `time_format`, was flatly
false: src/commands/conversation-parser.ts reads it twice. Both are display —
`list-builtins` prints it — and the operative half stands, since parse.ts never
reads it and converts off the captured AM/PM group instead. Said that way now.

"turns averaging more than ~19 non-blank lines fall below the floor" was
optimistic by one line: measured line by line, 18 parses (0.0526) and 19 does
not (0.0499), because the H1 is in the denominator too.

"a fallback when `ts` is null" understated the condition. It fires for any
string this script will not read a clock out of — a date with no time, a basic-
format 20251102T142251Z, an impossible 2025-02-30 — silently preferring the
conversation's date over a value that looked like a time.

"Every emitted scalar is JSON-encoded" was false for the four keys the script
writes itself (`type:`, `origin:`, `date: null`, `messages: []`), which are
literals under its own control rather than envelope data.

"collapses the duplicate ids the spec permits" cited the wrong half of the
spec: duplicates are tolerated for CONVERSATION ids; message ids are positional
and unique within their conversation. The real argument for an array is that
order IS the join. And since a conforming envelope's message id is derivable
from its index, `ts` is the only genuinely new value the array carries — worth
saying rather than implying more is being rescued than is.

Also: the STATUS block quoted `gbrain conversation-parser scan` as producing a
13-page aggregate; it takes one slug and has no aggregate form. The numbers
were reproduced per-slug, so the receipt now shows the command that produced
them.

And two tests changed by this branch still used `page.split('---')[1]` to reach
the frontmatter — the exact idiom this branch's own helper documents as unsound,
since it cuts on the substring anywhere including inside a quoted value. Both
now go through that helper.

94 tests pass, 0 fail. Adjacent suites: 402 pass, 0 fail. All 12 golden
fixtures still exit 0 with zero stderr and 33/33 messages parsed.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* fix(envelope importer): break a duplicate id on updated_at, not array position

A merged re-export kept the STALE copy, deterministically.

The memvelope CLI's own USAGE tells users to pass every downloaded export
at once; folder expansion sorts by filename (cli/convert.mjs expandInputs)
and SPEC.md rule 8 forbids the converter from re-sorting conversations
afterwards. Every automatic duplicate-namer a browser or OS gives a second
download of `conversations.json` inserts a character that sorts below `.`
— ` (1)`, `(1)`, `-1`, ` 2` — so the RE-EXPORT lands first in
`conversations[]` and the ORIGINAL lands last. Resolving the collision by
array index therefore threw away the newer copy every time, not half the
time.

Reproduced with an envelope built by the reference converter from two
ChatGPT downloads named the way a browser names them: 4-turn re-export at
index 0, 2-turn original at index 1, and the 2-turn page reached disk at
exit 0.

`updated_at` is a required conversation key in envelope-v0, is populated
by both vendor paths of the reference converter, and was read by nothing
here. It now decides, compared as an INSTANT (a `+05:30` value sorts above
a `Z` value it precedes) at full sub-second resolution (the converter
emits milliseconds always).

The tiebreak applies only when BOTH copies carry an orderable value.
Equal, absent, non-string or unparseable on either side falls back to
array order — a missing timestamp is not evidence of being older, and
preferring the copy that has one is a guess dressed as a rule. `created_at`
is not a secondary key: it is identical in both copies of a re-export.

The collision warning is not quieter — it is louder. It now names which
copy survived, on what basis, and with both timestamps, because
"overwriting the earlier page" is false whenever the tiebreak fires. The
run summary says "discarded" rather than "overwritten" for the same
reason: the losing copy is now sometimes never written at all.

`updated_at` is still written to no page. That is a separate, tracked gap.

Receipts: 107 pass / 0 fail (94 pre-existing, unweakened; 13 new, red-first
against cd88d1de at 96 pass / 11 fail). Over all 12 golden fixtures from
the reference converter plus the repo's sample, output pages, stdout,
stderr and exit code are byte-identical to cd88d1de — this fires only on a
filename collision.

* docs(envelope importer): name the extraction path, the UTC day, and drop a contradicted key claim

R2. The header promised `type: conversation` "keeps pages eligible for
conversation-facts extraction ... after sync" and stopped there, which
reads as "sync extracts facts from these." It does not. The accepting
path is `gbrain extract-conversation-facts`, run by hand, and its
autopilot wrapper — the `conversation_facts_backfill` cycle phase — is
opt-in and OFF by default. Both names verified against upstream/master
before writing them: src/cli.ts:1964 dispatches the command,
src/core/cycle/conversation-facts-backfill.ts:4-6 and :34 document the
phase as "Default OFF" with `enabled (false)`, and doctor.ts:3373 reads
an absent key as disabled. What the type actually buys is admission to
`ALLOWED_TYPES`, and that is now what the header says.

R3. The page `date` and every turn header are the UTC calendar day, not
the user's, and the limits block did not say so. Reproduced on this box:
a conversation held at 19:30 on Sunday 2 November 2025 in California is
`created_at: "2025-11-03T03:30:00.000Z"`, and the page comes out
`date: "2025-11-03"` — a Monday — with a Monday turn header. The output
is byte-identical under TZ=UTC, America/Los_Angeles, Asia/Kolkata,
Pacific/Kiritimati, Pacific/Midway and Europe/London (one sha across all
six), so this is the format's day, not the importing machine's.

Named as unfixable, with the reason rather than an apology: envelope-v0
renders every timestamp as `...Z` and SPEC.md rule 3 says why — "The
source's true offset is unknowable, and UTC is the only
machine-independent choice." A source offset is normalised away before
this script opens the file, and the vendor fields the reference
converter reads carry none to preserve: over the 22 conversation-level
timestamps in its own input fixtures, 16 are bare unix-epoch numbers, 4
end in `Z`, 2 have no designator, and 0 carry a numeric offset. No fix
is invented; the one available guess (the importing machine's zone)
would break determinism.

R4. Deleted the claim that "the id is the natural key," which the header
disclaimed 127 lines later in its own identity limit. The key is the
PAIR — date and id name the file — and the surviving limit already says
so correctly. The same claim appeared a second time as an in-code
comment at the filename site ("the date only leads as a ... prefix; the
id carries uniqueness"); deleting one statement of a false claim and
leaving its twin is a half-correction, so both went. Flagged as a
deviation: R4 as written names the header only.

The bullet R4 emptied also still described the pre-R1 duplicate-id
behaviour, so it is rewritten to the behaviour that now ships.

Doc-only: 107 pass / 0 fail, unchanged.

* fix(envelope importer): correct what the adversarial gate falsified in R1-R4

Six independent refuters plus a completeness critic ran against 81deb83d.
Two of my own corrections were themselves false, one broke a repo-wide CI
gate, and one real defect nobody had measured turned up. Everything here
was re-derived on this box before it was written.

BLOCKER — test/fixtures/memvelope/merged-re-export.mve.json shipped with
no trailing newline, taking scripts/check-trailing-newline.sh from green
to red. It is wired into `bun run check:all`, `bun run check:newlines` and
three call sites in scripts/ci-local.sh, so the branch could not pass the
project's own local CI. Now `trailing-newline check: ok (1547 files)`.

BLOCKER — the R2 paragraph claimed `ALLOWED_TYPES` "requires"
`type: conversation`, "so a page typed anything else is not merely un-run,
it is ineligible." False. ALLOWED_TYPES (extract-conversation-facts.ts:142)
admits six types — conversation, meeting, slack, email, imessage,
imessage-daily — and :569 defaults to the whole list. A new false claim
about a named upstream constant, in a branch whose purpose is deleting
false claims. Rewritten to say what the type actually buys: admission, not
a trigger, and one admission among six.

Also in R2: "extracts no facts from any of them until one of those two is
invoked" is unsafe as an absolute. facts/eligibility.ts ORs the type test
with RESCUE_SLUG_PREFIXES = ['meetings/','personal/','daily/'], so a page
written into an outDir syncing under one of those IS picked up by a plain
sync — subject to the 80-char MIN_BODY_CHARS floor, which nobody in the
gate noticed either. Both named; the default outDir is unaffected and now
says so.

MAJOR — R4's replacement sentence, "Two conversations carrying the SAME id
land on one filename", is false and reinstated the exact contradiction R4
removed. Verified: one id at two created_at dates writes TWO files and
zero collisions. The filename is the PAIR.

MAJOR — the same bullet stated the fallback condition backwards ("when
neither carries an orderable one"). The code (`a === null || b === null ||
a === b`) falls back when EITHER side is unorderable, or when the two are
equal, and the function's own doc said so correctly 460 lines down.

MAJOR — the R3 census was wrong by four in both figures: 22/16 should be
26/20. chatgpt-split.json is an array of PARTS, each an array of
conversations, and my counter walked the parts as if they were
conversations and scored zero. Recounted with the level flattened: 13
conversations, 26 conversation-level timestamps, 20 bare unix-epoch, 4 Z,
2 no-designator, 0 numeric offset. The conclusion is unharmed and slightly
stronger. Also narrowed "not fixable, here or upstream" to "not fixable
HERE" — a converter CAN hold a source offset and this format drops it by
design, which is a decision, not an impossibility.

MINOR (found independently by four of six refuters) — the message-delta
warning still said "the overwritten page(s)", the exact word this branch
deleted from the summary line one commit earlier for being false whenever
the tiebreak keeps the earlier copy. Now "discarded" in both places.

R1's own limits, all newly documented rather than newly introduced:
  - when only ONE copy carries an orderable `updated_at` the rule changes
    nothing and the stale copy still wins. `updated_at: null` is
    producer-reachable (both normalizers end in `|| null`) though it is 0
    of 13 in the reference corpus. Refusing to promote a copy for merely
    HAVING a timestamp is the deliberate choice; the header now states
    what it costs instead of implying coverage the rule does not have.
    The argument is also repaired: both copies come from ONE converter
    run, so the asymmetry is the vendor's and its direction is
    unrecoverable — not the weaker "an older converter" reasoning.
  - the reduction is PAIRWISE. With every copy orderable it is a true
    maximum in all six permutations; with an unorderable copy in the
    middle transitivity is lost and [later, absent, earlier] keeps
    `earlier`. Now documented and pinned by a 7-case test.
  - the tiebreak is WITHIN one envelope. Across runs, check 2 still lets
    an older export refresh a newer page at exit 0 with no warning at all.

Test-file note, stated plainly: exactly ONE pre-existing test was touched
— 'the message-delta warning ties itself to the ... pages' — whose
assertion moved from 'overwritten page(s) carried' to 'discarded page(s)
carried' to track the deliberate message change. Its claim is unchanged
and nothing was loosened. Run against the untouched cd88d1de script in a
detached worktree, the shipped test file is 100 pass / 14 fail: that one
renamed pre-existing test plus 13 of the 20 new ones.

Receipts: 114 pass / 0 fail. Adjacent suites (conversation-parser x6,
markdown x3, extract-conversation-facts, doctor backlog) 301 pass / 0
fail. Over all 12 golden fixtures from the reference converter plus the
repo's sample, pages, stdout, stderr and exit code remain byte-identical
to cd88d1de.

NOT fixed here, escalated instead: a message body containing any of
gbrain's four timeline sentinels truncates the conversation at
markdown.ts before parse.ts runs. Reproduced against a pristine
`git archive upstream/master`: five sentinel forms each take a 4-turn
conversation to 2 parsed turns with the tail reclassified into the page's
timeline, at exit 0 with stdout reporting "4 message(s)"; two controls
stay clean. The re-grade graded this class NOT-A-DEFECT on a different
measurement and the packet puts its drop table out of scope, so this is
the RE's call, not a change smuggled in here.

* test(envelope importer): make the offset guard discriminate, cover the leap-second ceiling

The test named "an offset-bearing updated_at is compared as an instant, not
lexically" asserted only `kept === 'second'`. Array order keeps the second copy
too, so the assertion could not tell "compared as an instant" from "not compared
at all". Measured on the parent commit: dropping the offset alternation from
UPDATED_AT_SHAPE — so an offset-bearing value stops parsing and the pair falls
back to array order — leaves the file at 114 pass / 0 fail. The guard was
decorative against exactly the failure it is named for.

It now runs both offset signs and asserts the VERDICT STRING, which names the
branch that decided. The negative-offset case additionally makes the
offset-bearing copy win from array position 0, an outcome the fallback cannot
produce. Both cases fail under that mutant, and under the sign-arithmetic
mutant that the old test did already catch.

The `s > 60` leap-second ceiling had no test at all. Three cases now pin it.
Two of them pin the ROLL ACROSS THE MINUTE BOUNDARY rather than inside it:
23:59:60Z IS 2027-01-01T00:00:00.000Z, so a copy carrying each of those two
strings is a TIE and falls to array order, and 23:59:60Z outranks
23:59:59.999Z by one millisecond. Asserting only that 23:59:60Z beats
23:59:59Z would have pinned nothing but f(60) > f(59) — measured: making the
seconds field contribute milliseconds instead of seconds (`s * 1000` -> `s`)
destroys the date roll and still passes such a test at 117/0. The third case
is 23:59:61Z, the only value in the file rejected solely by the ceiling.

NOT CLOSED, so nobody reads this as a mutation-clean function: two sibling
guards in `updatedAtInstant` remain decorative on this commit. Deleting either
`if (h > 23 || mi > 59) return null` or the offset-range `if (oh > 23 || om >
59) return null` leaves the file green. They are the same class; they are not
this commit's scope.

Header prose only, no behavior change:
  - The Output-layout paragraph said the filename is the (date, id) PAIR. Both
    halves are slugged, so two DISTINCT ids reach one filename and the tiebreak
    then discards one of two UNRELATED conversations while stderr calls the id
    "not unique". Stated on the SLUG, not the raw id, because raw ids predict
    nothing either way: `AbC-123`/`abc-123` differ at character 1 and collide,
    while sixty `-` plus `a` and sixty `-` plus `b` agree on all of their first
    60 characters and do not collide — `slug` strips leading/trailing `-`
    before it truncates. Check 2 below already named this class; it REFUSES
    what the tiebreak resolves.
  - The header explains at length why check 2 refuses to resolve an id-less
    `conv-1` across runs, then the code resolves that same name WITHIN one
    envelope. Both sides are now stated together, with the asymmetry named:
    the within-run path is loud and has `updated_at` on both copies, which is
    exactly the evidence check 2 lacks. Also widened — any id that SLUGS to
    `conv-N` collides with the positional name, and any id that slugs to EMPTY
    enters that namespace while still recording a non-null id in frontmatter.
  - "two different values" -> "two different INSTANTS": two different strings
    can name one instant (`09:00:00Z` and `14:30:00+05:30`) and fall back.
  - The collision block's own comment still glossed "mapping to the same
    filename" as "carrying duplicate ids" — the equation the header now
    corrects. Fixed in place.

Suite: 114 pass / 0 fail before, 118 pass / 0 fail after.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>

* test(envelope importer): remove a duplicate helper tsc rejects

Two functions were both named writeEnvelope. Bun hoists the later declaration,
so the earlier raw-envelope form was already dead at runtime - every call was
executing the fields form, which rebuilds the envelope from .conversations and
.meta and therefore handles both call shapes. All 118 tests passing under bun
is the proof; tsc's TS2393 was the only honest complaint.

Deleting the dead function keeps runtime behavior byte-identical. Removing it
also unmasked the two type mismatches TS2393 had hidden: envelopeWith declared
a return type of unknown for a value with a concrete shape, and writeEnvelope's
parameter did not admit the memvelope key that full-envelope call sites pass.
Both signatures now state what the values actually are.

bun run verify: 2/34 failing, identical to upstream/master's own baseline
(check:skill-brain-first and check:conversation-parser fail on the untouched
base as well).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

* fix(envelope importer): recognize our own pages after gbrain re-serializes them

The pre-file sweep proved existingPageIdentity() accepted only this importer's
own JSON.stringify output. gbrain rewrites pages it holds - export --dir, the
DB-only restore path, and put_page write-through all re-emit frontmatter
through gray-matter, which writes a UUID as a plain unquoted scalar and an
all-digit or boolean-looking id single-quoted. 16 of 17 id shapes, and 13 of
13 golden-fixture ids, came back in a form JSON.parse rejects - so the page
turned foreign, a later refresh refused the whole envelope at exit 2, and the
error's remedy advised deleting gbrain's own copy, provenance and body edits
included.

idScalar() now reads the three shapes a YAML round trip produces: our own
JSON, YAML single-quoted (doubled-quote escape), and plain scalars. YAML
null shapes and the empty string stay foreign, exactly as before - refusal
remains the answer for anything not confidently recognized.

Red-first: both new tests (plain-scalar UUID, single-quoted digits) fail on
the parent and pass at this commit. 120 pass / 0 fail; typecheck clean;
bun run verify at the repo baseline.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>

---------

Co-authored-by: Sean Gearin <sean@virgilknows.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
2026-08-04 19:20:15 +08:00
Garry TanandClaude Fable 5 d610a845a8 fix(ci): gate — CommonMark info-string rule, hash the real model payload, close the YAML scanner gap (blind review round 5)
Three defects a second blind reviewer found that survived round 5. All three
reproduced against cf7a1616 before the fix and are pinned after it.

1. The fence scanner still falsely closed compliant descriptions. CommonMark 4.5
   forbids a backtick in a BACKTICK fence's info string, so ```foo`bar is an
   ordinary paragraph. The scanner opened a fence on it, never found a closer,
   and stripped the body to EOF:

     body = '```foo`bar' + three sentences of real prose + an image embed
     before: intentWordCount 0, misses [missing_intent, missing_screenshot]
     after:  intentWordCount 52, misses []

   Same class as round 5's closing-length bug and the same cost — opening a
   block CommonMark would not open deletes the author's prose exactly the way
   closing one late did. Tilde fences keep the permissive rule (4.5 restricts
   backtick fences only).

2. The spend guard hashed less than the model consumes. hashInputs covered
   title, modelBody, head.sha, exemption and policy ids — but the payload also
   carries the changed-file list and the diff, and the workflow degrades the
   diff to a marker line when the API 406s on a huge one. So a run that
   classified with no diff cached a diff-blind verdict, and the next run — real
   diff in hand, same title/body/sha — matched the hash and was served that
   verdict permanently (measured: anthropicCalls 1 across both runs). The
   assembled payload is now folded in as a fixed-width digest, inside the JSON
   tuple where quoting still makes a forged boundary impossible. Round 5's
   property is re-pinned: a policy fix past the model's body cap invalidates
   even when the payload is byte-identical.

3. The workflow's ${{ }}-in-run scanner missed a legal commented block header.
   `run: | # shell block` is valid YAML — js-yaml puts the following
   interpolation in the script — but the header fell through to the
   single-line branch, which captured `| # shell block` as the whole command
   and never looked at the block body. The rule protecting against shell
   injection reported clean over an interpolating workflow. The scanner now
   handles a trailing comment on the header, and scans the comment text too
   rather than leaving itself a hiding place. Guarded against js-yaml's actual
   parse, not against the scanner's own opinion.

Also corrected one overstated claim in the file's security block: a BARE url in
a sanitized string still autolinks under GFM. That is a self-labelled link and
the deliberate stopping point — the escaping targets the masking characters so
it cannot forge `![APPROVED](…)` or `[click to approve](…)` — but "no live
links" was too strong for what the code does.

Self-audit: all 13 interpolations into the sticky comment are either literals in
this file or pass sanitizeModelText/sanitizeList; the only unsanitized one
(titleCheck.reason) is a hardcoded literal and renders inside a code span. Every
rendered statement matches behavior the code performs. The four realistic-human
fixtures and the three new fence cases all PASS; empty, "fixes bug", the
unfilled template, ten words of lorem and a fenced-away screenshot all still
FAIL.

test/pr-gate-workflow.test.ts 158 -> 163 pass, 0 fail. Six mutations (the
CommonMark rule, the tilde exemption, the payload digest, the runGate wiring,
the scanner's comment group, the header-comment scan) each fail at least one
test. typecheck, actionlint, verify (34/34) green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-04 10:46:09 +08:00
Garry TanandClaude Fable 5 f87488ff36 fix(ci): gate — stop red-Xing genuine contributors; neutralize markdown embeds (blind review round 4)
A blind reviewer rejected this branch on false positives and the repro held up.
The gate's intent check deleted every list and blockquote LINE before counting
words, then measured what was left against an arbitrary 40. Real people write
their story as bullets. Measured on the four realistic-human descriptions from
the repro:

  description                              before  after  floor 40  floor 20
  own prose written as four bullets             0     55  CLOSE      PASS
  short non-native-English paragraph           38     38  CLOSE      PASS
  specific first-person bug report             34     34  CLOSE      PASS
  mostly a stack trace + a real explanation    28     27  CLOSE      PASS

The file's own exemption rationale already says a check that is red on every
release is a check somebody disables inside a week. Red on every terse-but-
genuine contribution dies the same way, and it costs real contributors on the
way there — worse than the forgery risks rounds 1-3 chased.

- intentWordCount strips the list/quote MARKER and keeps the author's words.
  Still stripped: fenced code, indented code (new — the other spelling of a
  fence, guarded so it never eats a list continuation), headings, HTML
  comments, raw HTML, bare URLs, inline code, link/image syntax, and the
  template's own bold prompts. An untouched pull_request_template.md still
  scores 0, pinned against the real file on disk.
- INTENT_MIN_WORDS 40 -> 20, documented as a floor against empty/boilerplate-
  only descriptions, not a quality bar. CONTRIBUTING.md documents no word count
  at all. Empty, "fixes bug", the unfilled template, 10 words of lorem and an
  indented log paste all still fail.
- The fix-it comment no longer tells contributors to reopen a PR that was never
  closed — the gate has no close call in it. It now says what actually happens:
  editing the description re-runs the check (the workflow triggers on `edited`),
  the PR stays open, a maintainer decides.
- sanitizeModelText backslash-escapes `[` and `]`. Escaping HTML killed the
  <details> forgery, but Markdown needs no angle brackets: an image embed
  renders a green "APPROVED" picture and a link renders a live phishing target
  inside a close-lane comment. The file asserted nothing attacker-controlled
  reaches Markdown unescaped; that is now true.
- A label-API failure during a NEUTRAL run no longer throws to exit 2 with no
  comment. NEUTRAL has no verdict to record, so it logs, still posts, exits 0 —
  and the comment stops claiming labels were cleared when they were not. The
  verdict path keeps failing loudly on purpose (ordering note).

Four verbatim human descriptions are pinned as PASSING forever. Eight mutations
(floor, list markers, quote markers, indented code, markdown escaping, the
reopen copy, the NEUTRAL try/catch, the label claim) each fail at least one test.

test/pr-gate-workflow.test.ts 145 -> 158 pass, 0 fail. typecheck, actionlint,
verify (34/34) green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-04 10:46:09 +08:00
Garry Tan ddb39df23a fix(ci): gate — escape comment HTML, tighten screenshot floor, fix fence false-positive, repair label ordering (blind review round 3)
Four findings from a second independent blind review, all reproduced against
HEAD before the fix and all pinned by a mutation-tested case.

- sanitizeModelText escapes &, < and > after the existing comment/mention/
  block-marker stripping, so raw HTML from any dynamic string (model output,
  mechanical flag details built from PR filenames, neutralReason, the
  policy-exempt note) renders as literal text. A <details open><summary>MERGE
  LANE — approved</summary> string is a forged verdict inside a close-lane
  comment; it now renders as &lt;details.
- hasScreenshot requires a markdown image whose URL looks like a URL or path,
  an <img> carrying a non-empty src=, or a bare paste URL, and strips HTML
  comments before scanning. `![proof](x)`, `<img alt=proof>` and an image
  hidden inside `<!-- -->` no longer clear it. Documented in the code as a
  FLOOR against zero-effort submissions, not proof.
- stripCodeFences is a line scanner following the CommonMark rule that a
  closing fence must be the same character and at least as long as the
  opening one. The old backreference read ```` as a NEW opening fence and
  stripped to EOF, so a compliant body documenting fence syntax lost its
  intent paragraph and was closed. The scanner is also linear, retiring the
  superlinear-backtracking hazard the 16KB cap was sized against.
- Labels are reconciled BEFORE the sticky comment carrying the cached state.
  Written the other way, one transient label-API 500 left stale/missing/
  duplicate labels forever: the rerun short-circuited on the persisted state
  and returned success without repairing them.

Also:
- hashInputs covers what the run consumes — the truncated model body plus the
  mechanical policy outcome — so an edit past the 6KB model cap no longer
  mints a new hash and buys an identical paid call, while a policy fix landing
  past that cap still invalidates the cached verdict.
- The test's YAML block-scalar scanner accepts indentation indicators in both
  legal orders (`>2-` as well as `|-2`), with a guard-the-guard case proving
  it sees a `run: >2-` block interpolating attacker-controlled text.
- A block at the top of the script states plainly that this gate is a triage
  signal and a reviewer checklist, not an authorization boundary; one line of
  the sticky comment footer says the same to the contributor.

145 pass / 0 fail (was 126), typecheck clean, actionlint clean, verify 34/34.
2026-08-04 10:46:09 +08:00
Garry TanandClaude Fable 5 88731d8cf3 fix(ci): gate — sanitize mechanical flag details, anchor path regexes, exempt maintainer/bot/draft from the policy check (blind review round 2)
Blind review round 2 rejected the branch. Five findings, all fixed.

BLOCKING 1 — the verdict was forgeable through a PR filename.
renderComment wrote mechanical red-flag details raw, and two of them
interpolate filenames (adds_recipe, deletes_tests). git allows a newline
inside a filename and JS `[^/]` matches one, so RECIPE_RE's anchors were
decorative: a PR adding `src/core/ai/recipes/x\n## PR Gate — ...\ncc
@octocat\n<!-- ...state... -->\nz.ts` put that text verbatim into the bot's
comment — forged heading, live third-party mention, and on the NEUTRAL
render (which writes no state block of its own) parseState() returned the
ATTACKER's block, so the next run hit the spend guard and silently skipped
the verdict with no label and exit 0. Three layers:
  (a) flag details go through sanitizeList, exactly like the model's
      strings. Audited every other interpolation into the comment; the rest
      are literals in the file, Number()-coerced, or already sanitized.
  (b) every path regex spells its segment class [^/\n], not [^/] —
      RECIPE_RE, SOURCE_EXT_RE and the test-path check.
  (c) parseState reads the state block only off line 2 of a marker-leading
      comment (where renderComment writes it) and STATE_RE is whole-line
      anchored. A block anywhere else is somebody else's text.

BLOCKING 2 — no exemption, so every release PR was close-lane.
Measured: 40 of the last 40 merged PRs would be close-lane on
missing_screenshot, including every /ship release PR. A check that is red
on every release gets switched off within a week, and then it filters
nothing. The #3745 policy exists to filter INCOMING OUTSIDE CONTRIBUTIONS;
release automation cannot take a screenshot of itself. It is now waived for
OWNER/MEMBER/COLLABORATOR, bot authors and drafts — the usefulness verdict,
the title rule and every mechanical red flag still run, and the sticky
comment says the check was skipped. author_association / draft / user.type
are read from the pr.json the workflow already fetches: no new API call, one
source of truth. `draft` is the one author-settable input, so
ready_for_review joins the trigger list and the exemption is folded into the
spend-guard hash — the draft-era verdict cannot be reused after the flip.
Stated as a deliberate decision in both the workflow header and the script.

3 — DOWNGRADE_FLAG_IDS omitted deletes_tests, adds_symlink and
adds_node_modules, so a PR deleting test/e2e/engine-parity.test.ts kept
merge-lane and a green check on the strength of its prose. All three added.
The old test used deletes_tests as its example of a NON-downgrading flag;
rewritten to pin the stronger invariant instead — every id detectRedFlags
can emit is a downgrade trigger (derived from the detector, so a new flag
fails until it is classified on purpose), and the set is still an allowlist
(an unrecognized id changes nothing).

4 — FENCE_RE backtracks superlinearly on a hostile body: 65KB of backticks
(GitHub's max body length) measured 8.2s across the two policy scans on a
pull_request_target runner. stripCodeFences now caps the scan at 16KB —
same input, 0.40s. Tradeoff documented at the constant: the intent
paragraph and the screenshot both sit near the top in practice (the PR
template puts them in the first two sections, and the model payload already
caps the same body at 6KB), so a real contributor is not judged on a
truncated tail.

Verified: test/pr-gate-workflow.test.ts 126 pass / 0 fail (was 95),
typecheck clean, actionlint clean, check-privacy /
check-no-tracked-symlinks / check-progress-to-stdout /
check-bun-test-timeout / check-key-files-current-state all exit 0. Each new
pin was mutation-tested against the pre-fix behavior: all six mutants fail
the suite.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-04 10:46:09 +08:00
Garry TanandClaude Fable 5 4a00c31b12 fix(ci): gate policy check survives an API outage; merge master; drop NUL separators
Three fixes on the strict PR usefulness gate (#3698), plus the merge that
brings in the policy it enforces.

1. The mechanical policy check now outlives the model. The
   ANTHROPIC_API_KEY guard used to sit above detectPolicyMisses, so a PR
   with no intent paragraph and no screenshot got a green NEUTRAL skip
   whenever the key was absent or Anthropic was down — "wait for a 500"
   was a documented way past the one hard requirement. The #3745 branch
   now sits above the key guard and the spend guard: a policy miss is
   close-lane + the friendly fix-it comment + exit 1 with no API
   dependency at all. A compliant PR that hits a missing key or a dead
   API keeps the round-1 NEUTRAL behavior unchanged (loud comment,
   ::warning::, exit 0, stale gate:* labels cleared) — and the NEUTRAL
   comment now says plainly that the *usefulness verdict* did not run,
   while still reporting the title check and mechanical red flags it was
   able to compute without a model.

2. Merged origin/master, which carries #3745's CONTRIBUTING.md section
   and .github/pull_request_template.md. No conflicts: this branch never
   touched VERSION / package.json / CHANGELOG.md, so master's 0.42.72.1
   carried through untouched — the feature branch adds no version bump.
   The test's inlined pull_request_template fallback (only needed while
   the branch predated the merge) is gone; it now reads the real file, so
   growing the template's own prose past the 40-word bar fails here
   instead of silently letting an untouched template through. The
   CONTRIBUTING_URL deep link is pinned against a GitHub-style slug of
   every heading in the merged CONTRIBUTING.md, with the slugger itself
   pinned so it cannot "pass" against an anchor GitHub never generates.

3. hashInputs joined its three fields with literal NUL bytes, which made
   grep treat the whole of scripts/pr-gate.mjs as binary — any future
   grep-based CI guard over that file would have matched nothing and
   passed silently. Replaced with JSON.stringify of the tuple: still
   unforgeable (each field is quoted and escaped), still stable by
   construction, and printable. `grep -c hashInputs scripts/pr-gate.mjs`
   now returns 2 instead of nothing. Existing sticky-comment state hashes
   are invalidated once, costing one re-verdict per open PR.

Tests: 95 pass / 0 fail in test/pr-gate-workflow.test.ts. The no-API-key
policy-miss case was verified to fail against the pre-fix ordering.
Verified live against the Anthropic API: HTTP 200, strict JSON, all seven
required keys, merge-lane on a compliant fixture.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-04 10:46:09 +08:00
Garry TanandClaude Fable 5 94662eb1e1 feat(ci): gate enforces the #3745 intent-paragraph + screenshot requirement
CONTRIBUTING.md (#3745) requires every PR to carry a paragraph the author
wrote themselves and a screenshot of gbrain actually in use. The gate now
checks both, mechanically, before it spends anything on review.

What it checks (no LLM, both exported for testing):
- hasScreenshot: markdown `![alt](url)`, a bare user-images.githubusercontent
  or github.com/user-attachments/assets URL, or an <img> tag. Anything inside
  a fenced code block does not count — pasting the syntax is not attaching
  the picture.
- hasIntentParagraph: >= 40 words of prose left after stripping fenced code,
  blockquotes, list items, headings, HTML comments, links and the PR
  template's own boilerplate. Per-character scripts are tokenized per
  character, so a paragraph written in Chinese counts as one.

The two lane consequences:
- missing_screenshot OR missing_intent forces close-lane from any recommended
  lane (exit 1) and skips the model call entirely — closed without review is
  the documented consequence, so there is nothing to spend a review on. The
  sticky comment leads with what is missing, how to fix it, and the reopen
  path; both misses are also recorded in the existing "Mechanical downgrades
  applied" section.
- The model's new advisory intent_authenticity verdict forces
  needs-maintainer (exit 0) when it reads "ai_generated", and never
  close-lane on that signal alone. The comment says only that a maintainer
  will read the paragraph personally; the model's reasoning is consumed and
  never published at the contributor.

Rubric + strict-JSON schema gain intent_authenticity and
intent_authenticity_reason, with explicit instructions that rough grammar,
terseness and non-native English are evidence of a HUMAN and that "unclear"
is the answer whenever the evidence is not clear-cut.

test/pr-gate-workflow.test.ts: 63 -> 88 tests, all green.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-04 10:46:09 +08:00
Garry TanandClaude Fable 5 2f65ed8da6 fix(ci): harden the PR gate — comment-ownership, output sanitization, deterministic lane downgrades (blind review round 1)
Two independent blind reviews of #3698 (one APPROVE, one REJECT). Every
confirmed finding from the reject side, plus the cheap hardening both
reviews flagged.

BLOCKING

1. Sticky-comment hijack. upsertStickyComment adopted ANY comment containing
   the marker, so a contributor could pre-post `<!-- gbrain-pr-gate -->`, have
   the gate PATCH it, then edit it into a fake green verdict. isOwnComment()
   now requires user.type === 'Bot' AND login === 'github-actions[bot]' AND
   the body to START WITH the marker; anything else gets a fresh comment.

2. LLM output injected as raw Markdown. Model-produced reasons[] and
   reviewer_checklist[] reached the comment unescaped — PR-body-driven
   injection could forge headings, a second marker, and live @mentions.
   sanitizeModelText()/sanitizeList() are now the single choke point in
   renderComment(): HTML comments stripped, mentions zero-width-broken,
   leading block markers removed, newlines collapsed, 300 chars per string,
   8 entries per list, both caps self-marking.

3. Lane was purely model-decided. A well-written feature pitch could talk
   itself into merge-lane. The model now RECOMMENDS; applyMechanicalDowngrades
   forces merge-lane -> needs-maintainer on any of: workflow edits, a new
   package.json dependency, a new src/core/ai/recipes/ provider file, new
   KNOWN_CONFIG_KEYS entries, >40 changed files, >400 net source lines outside
   test/, or a src/ change with no test file touched (#3665). The sticky
   comment reports them under "Mechanical downgrades applied".

4. The test file did not pin what it claimed. Added: no gh pr checkout /
   git fetch / refs/pull / pull/*/head in any spelling; exact permissions
   key->value map plus a single-permissions-block assertion so no job-level
   grant re-widens contents; the ${{ }}-in-run scanner now covers folded
   (`run: >`) and chomped blocks, with a guard-the-guard test; and mocked
   end-to-end runGate() runs for close-lane exit 1, marker hijack, sanitizer,
   truncation, refusal routing, NEUTRAL label clearing, label swap, and the
   spend guard.

5. Version-first title regex rejected the documented suffix form.
   `v0.31.1.1-fixwave fix: ...` now passes. VERSION_AT_END_RE no longer
   false-positives on `chore: bump zod (3.25.76)`: it fires only on a
   v-prefixed or 4-segment trailing version, i.e. this project's own shape.

6. Refusal fail-open. stop_reason=refusal exhausted retries into a green
   NEUTRAL — a deterministic way to dodge the red X. Refusal and
   schema-invalid output now route to needs-maintainer with an explicit
   note; only transport failure stays NEUTRAL. Refusal also short-circuits
   the retry loop, since retrying a deterministic refusal only burns spend.

ALSO

7. persist-credentials: false on the checkout step.
8. Dropped pull-requests:write. Everything the script calls is the issues
   API (comments, label create, label add/remove), so issues:write is the
   only grant that is actually needed.
9. Spend guard for the edited/synchronize amplification: the LLM call is
   skipped when sha256(title+body+head_sha) matches the hash recorded in the
   previous sticky comment's state block, and the stored lane's exit code is
   reused.
10. NEUTRAL runs now clear every gate:* label instead of leaving a stale
    verdict behind.

Verified: bun test test/pr-gate-workflow.test.ts 63 pass / 0 fail,
bun run typecheck clean, actionlint clean, check-privacy /
check-no-tracked-symlinks / check-progress-to-stdout /
check-bun-test-timeout clean, plus a live Anthropic smoke of the exact
request shape (HTTP 200, valid strict JSON, injection attempt rejected).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-04 10:46:09 +08:00
Garry TanandClaude Fable 5 ca1eed3e95 feat(ci): strict PR usefulness gate — merge-lane/close-lane verdict before any review effort (#3698)
Runs on pull_request_target against master. PR code is never checked out
or executed: metadata + a 120KB-capped diff come from the GitHub API only;
the base-repo checkout provides the rubric script. All interpolations are
env-bound; actions SHA-pinned; permissions limited to contents:read +
pull-requests:write + issues:write; per-PR concurrency with cancel.

scripts/pr-gate.mjs (no deps, fetch only) classifies via the strict
usefulness rubric (claude-sonnet-5, strict JSON schema, thinking disabled;
sampling params omitted — rejected on this model), posts one sticky
comment (<!-- gbrain-pr-gate -->), applies exactly one gate:* label, and
exits 1 only on close-lane. Missing ANTHROPIC_API_KEY or API failure after
2 retries NEUTRAL-skips loudly (comment + warning, exit 0). Mechanical
(no-LLM) checks: version-first title rule and diff red flags (>40 files,
node_modules, symlinks, workflow edits force needs-maintainer, new
package.json deps, deleted tests).

test/pr-gate-workflow.test.ts pins the security invariants and unit-tests
the exported title rule + red-flag detector (import side-effect guarded).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-08-04 10:46:09 +08:00
Sean Gearin 82fe0216ff fix(import): keep stdout JSON-only under --json (#3637) (#3764)
runImport printed its informational lines ('Found N markdown files' and four
siblings) through console.log unconditionally, so `gbrain import <dir> --json`
emitted them ahead of the payload and stdout did not parse as JSON. A consumer
parsing stdout reads that as zero imports while its own bookkeeping records the
files as ingested, so the next run skips them permanently.

Route those five lines through an `info` helper that switches to stderr when
--json is set. The lines are relocated, not removed: human mode is byte-for-byte
unchanged, and the JSON payload itself is untouched. This is the contract
CLAUDE.md already states ('Stdout stays clean for data output (--json
payloads)') and that import.ts's own progress comment repeats.

scripts/check-progress-to-stdout.sh only greps for process.stdout.write('\\r…),
so this class was never guarded; the new spawn-level test pins it.
2026-08-03 19:55:04 +08:00
Masa fa313f2969 fix(durability): gate installHelper's exec-bit chmod on dryRun (#3736) (#3759)
installHelper's "already current" fast path ran chmodSync(helperPath,
0o755) unconditionally, before the dryRun check further down — so
`gbrain sources harden --dry-run` mutated the helper's permissions even
though dry-run is documented as a pure preview. This is the 5th
instance of the #3594 class (see #3692 for the sibling fix to the
step-1 pull, same function).

Gate the chmod with `if (!dryRun)`, mirroring the guard pattern already
used in installLocalHook's own "already current" branch. Non-dry-run
behavior is unchanged.

Adds a regression pair to test/brain-repo-durability.serial.test.ts:
one asserting dry-run leaves a drifted-permission helper untouched,
and a control asserting the real run still restores the exec bit.
2026-08-03 19:37:18 +08:00
2e554500e2 fix(cycle): grade_takes and calibration_profile record the model they actually run — follow the gateway chat model (#3726)
Two cycle phases still carried the label-vs-actual defect that
propose_takes shed in v0.42.62 (and that the #2805 review noted was worth
tracking so it isn't lost):

- grade_takes hardcoded 'claude-sonnet-4-6' into judge_model_id, the
  evidence signature, and budget metering, while the default judge call
  passed NO model hint and rode the gateway's chat_model. On any brain
  with a non-default chat_model, the verdict cache and telemetry recorded
  a model that never ran.
- calibration_profile persisted TIER_DEFAULTS.reasoning to model_id but
  never passed a model to the patterns generator at all, so the recorded
  model and the executed one were unrelated.

Fix, matching the propose_takes convention: each phase resolves ONE
string via explicit override > getChatModel(), and that string drives the
chat call, the cache key, and the stored id.

- grade_takes: the judge hint gets the FULL provider-prefixed string; the
  stored judge_model_id and evidence signature keep the historical bare
  tail. Stock installs are unchanged: getChatModel() defaults to
  'anthropic:claude-sonnet-4-6', whose tail equals the old hardcoded
  value, so no verdict cache invalidates. A genuinely different
  chat_model invalidates, which is correct — the judge really changed.
- calibration_profile: getChatModel() is provider-prefixed, preserving
  the #2451 contract, and its default IS the old TIER_DEFAULTS.reasoning
  value — stock behavior unchanged. The generator now receives the same
  resolved string that is persisted to model_id.

Tests: regression per phase pinning configured-chat-model routing (full
string to the call, bare tail to the cache key for grade_takes, full
string persisted for calibration); all existing suites pass unchanged,
pinning the no-change-on-stock property. 112 tests across the four
affected suites; typecheck clean.

Co-authored-by: Paolo Belcastro <p3ob7o@users.noreply.github.com>
Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
2026-08-02 20:26:51 +08:00
Masa 630786dc65 docs(integrity): correct the --dry-run line in the subcommand comment (#3739)
The comment listed `gbrain integrity --dry-run` as a subcommand, but
runIntegrity dispatches on `check`/`auto` and parses --dry-run inside
the auto path, so that form exits 1 with "Unknown subcommand". The
runtime help already shows --dry-run indented under `auto [options]`;
only the source comment disagreed.

Addresses #3738.
2026-08-02 20:10:31 +08:00
Masa d92f0dc86f fix(integrity): stop counting dead-link findings as review-queue entries (#3750) (#3751)
The dead-link branch in cmdAuto called logSkip() (writing to
integrity.log.jsonl) but incremented bucketReview, the same counter fed by
the bare-tweet path's appendReview() calls (which write to
integrity-review.md). The printed "Review queue" summary line therefore
included findings that were never written to the review file, so
`gbrain integrity review` silently disagreed with `integrity auto`'s own
summary.

Give dead-link findings their own counter (bucketDeadLink) and print it as
a separate line. No change to bare-tweet bucketing or CLI surface.
2026-08-02 19:54:49 +08:00
a68379050e feat(schema-pack): add slug_filter to retype mapping rules (#2655)
retype mapping_rules' existing path_filter matches pages.source_path,
which is only populated for pages synced from a git repo. Pages ingested
via the put_page MCP tool (or any write path that doesn't go through
sync) have source_path = NULL, so path_filter can never disambiguate
them — a same-from_type retype rule targeting a slug prefix has no way
to address this class of page at all.

slug_filter adds an independent, orthogonal LIKE filter on pages.slug
(the field that's always populated), combinable with path_filter via AND
when both are given. Wired through the full call path: schema validation
(manifest-v1.ts), the RetypeRule interface + probeRule/applyRetypeRule
(retype.ts), and the pack-manifest → RetypeRule conversion in the
unify-types job handler (unify-types-handler.ts) — the last one matters
because a field only present in the zod schema but not carried through
that conversion would validate fine yet silently no-op at execution time.

- manifest-v1.ts: add optional slug_filter to RetypeMappingRuleSchema
- retype.ts: slug_filter on RetypeRule; probeRule + applyRetypeRule both
  apply `AND slug LIKE $N` when present, independent of path_filter
- unify-types-handler.ts: carry rule.slug_filter through the
  pack-mapping-rule → RetypeRule conversion
- tests: skips-outside-slug_filter, matches-despite-NULL-source_path
  (the motivating case), path_filter+slug_filter combined via AND

320/320 existing schema-pack tests pass; 3 new tests added; tsc --noEmit
clean.

Co-authored-by: Claude Sonnet 5 <noreply@anthropic.com>
Co-authored-by: Time Attakc <89218912+time-attack@users.noreply.github.com>
2026-08-02 08:12:46 +08:00
6d1232d5a6 v0.42.72.1 docs(contributing): require a human-written intent paragraph + live screenshot on every issue and PR (#3745)
Effective 2026-08-02. Every issue and PR must carry a paragraph the author
wrote themselves explaining why they are opening it, and a screenshot showing
gbrain actually in use in that situation. AI-generated or AI-polished intent
text is not accepted; AI assistance for the code is still fine. Missing either
one means closed without review, reopenable once added.

Stated in CONTRIBUTING.md and pre-filled in both issue templates plus a new
pull request template so the fields are in front of the author.

Co-authored-by: Garry Tan <garrytan@gmail.com>
Co-authored-by: Claude Opus 5 <noreply@anthropic.com>
v0.42.72.1
2026-08-02 07:47:23 +08:00
Sina Matian aff34a428a v0.42.72.0 feat(auth): server-enforced slug-prefix write fence for OAuth clients + qm-harness integration guide (#3712)
Registering an OAuth client with --bound-slug-prefixes now makes the write boundary real: writes outside the bound prefixes are refused on every op that can name a page, and ops that write by something other than a slug are refused outright rather than left unfenced. Deny-by-default at dispatch, so a write op added later is refused to bound clients until it is explicitly fenced.

Adds docs/integrations/qm-harness.md (gbrain as the company brain for a qm deployment) with a roster-driven provisioning script and deployment templates, plus a Known limitations section stating plainly that this is a write boundary and not a privacy boundary.

Five review rounds, including three clean-room passes by codex gpt-5.6-sol and Claude Fable 5 against an instruction-stripped tree.
v0.42.72.0
2026-08-02 07:05:13 +08:00
Garry Tan f07e9dff11 fix(ci): release build gates on the artifact, not a serial re-run of the whole suite
The first Release run (30698650484) failed before building anything: the
build job re-ran the entire unit suite serially on the release runner —
a different environment from the sharded Test workflow that had already
gated the exact SHA at merge — and died on ambient-env tests. The review
of #3573 predicted this ('a flaky test now blocks the release'). The
build job now compiles and smoke-tests the binary (--version must match
the VERSION file), which validates the actual artifact — something the
test suite never did. The Test workflow remains the code gate.
v0.42.71.0
2026-08-01 20:43:58 +08:00
Garry Tan 72ec53bb49 v0.42.71.0 feat(ci): publish GitHub releases on every version bump — organized notes + binaries, self-update unbroken (#3521 #3716) 2026-08-01 19:55:28 +08:00