Commit Graph
8 Commits
Author SHA1 Message Date
d4211f4176 v0.42.11.0 feat(skillopt): held-out eval gate, honest receipts, ENFORCE + ablation opts (#1759)
* feat(skillopt): wire held-out gate, honest receipts, ENFORCE + ablation opts

Wire the F11 held-out gate into the orchestrator at checkpoint acceptance
(runHeldOutGate was dead code); parse + thread --held-out through CLI, batch,
fleet, background job, and the run_skillopt MCP op. Populate the real
receipt.baseline_sel_score (was hardcoded 0) and add a final-test eval
(test_score + baseline_test_score) via a shared scoreSkillOnTasks primitive.
Fix the --no-mutate proposed.md write (was a stub) and enforce maxRuntimeMin.

D16 ENFORCE in core mutation policy (assertBundledMutationHeldOut): mutating a
bundled skill in place requires a non-empty (>=5), benchmark-disjoint held-out
set or hard-refuses. Add three eval-internal ablation opts (reflectMode,
disableValidationGate, optimizerMode='one-shot-rewrite') recorded in the
receipt + audit; ROLLOUT_SUCCESS_THRESHOLD named constant.

Security: run_skillopt MCP op validates skill_name (kebab-only) and confines
caller-supplied benchmark/held-out paths to the skills dir for remote callers.

* test(skillopt): held-out gate, ENFORCE, one-shot rewrite, runtime + receipt honesty

New test/skillopt/rollout.test.ts (rollout had zero coverage). Held-out ENFORCE
unit cases + one-shot-rewrite fence handling (whole-response unwrap, embedded-fence
preserved, error path). E2E: F11 held-out BLOCKS/ALLOWS, bundled no-mutate write,
reflectMode/disableValidationGate/optimizerMode, maxRuntimeMin abort, receipt
baseline/test-score honesty, held-out/benchmark disjointness, D2 no-DB-pollution.

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

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs: document skillopt held-out gate + bundled mutation requirement for v0.42.9.0

Wire --held-out into the skill-optimizer SKILL.md, guide flags/safety tables, and
the tutorial's bundled-skill step: mutating a bundled skill in place now requires
--allow-mutate-bundled AND --held-out (>=5 benchmark-disjoint tasks) or it
hard-refuses. Add the --held-out flag row + F11 held-out gate to the guide; update
the receipt contract to the honest baseline/test-score fields.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(gateway): AI SDK v6 toolLoop compat — multi-turn tool calls work again

The ai@6.x bump tightened ModelMessage + tool-schema validation, which
silently broke every multi-turn tool loop. Both `gbrain skillopt` rollouts
and production background `subagent` jobs route through `chat()`/`toolLoop`
and crashed the moment the model called a tool ("messages do not match the
ModelMessage[] schema" / "schema is not a function"). Surfaced end-to-end
by the SkillOpt real-LLM eval.

Three fixes:
- chat(): wrap tool defs with the SDK's `jsonSchema()` helper instead of a
  bare `{jsonSchema}` object (v6 asSchema() treated the bare object as a
  thunk and threw).
- chat(): new exported pure `toModelMessages()` converts gbrain's
  provider-neutral ChatMessage[] into v6 ModelMessage[] — tool results ride
  a dedicated `role:'tool'` message with structured `{type,value}` output;
  null output preserved as json null. Load-bearing for the production
  subagent path, not just skillopt.
- rollout.ts: replace the inline params→schema mapper (dropped `items` on
  array params) with the shared `paramDefToSchema` single source of truth.

Pinned by test/gateway-model-messages.test.ts (8 cases). Folds into the
open v0.42.9.0 PR (#1759) — these complete the eval-readiness wave by
making skillopt actually run against a live model.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(skillopt): budget no-pricing for Haiku silently scored every rollout 0

Surfaced by the SkillOpt real-LLM eval (Track B). Two coupled bugs that made
a budget-capped Haiku run report a vacuous "0/N" measurement in ~2ms with
zero LLM calls — indistinguishable from a real deficient-skill score:

1. Claude Haiku 4.5's canonical dateless id (`claude-haiku-4-5`) was missing
   from anthropic-pricing.ts (only the dated `-20251001` was present). With
   `--max-cost` set, BudgetTracker.reserve() threw no_pricing on the FIRST
   chat() of every rollout. Added the dateless entry (sonnet already had its
   dateless form).
2. runValidationGate swallowed that BUDGET_EXHAUSTED error — runWithLimit
   settled it as {ok:false}, which the gate turned into median:0. A pricing/cap
   crash became a fake score. The gate now scans settled results for
   isMustAbortError() and re-throws so the caller aborts loudly; ordinary
   (non-abort) rollout errors still fail-open to 0 (judge-hiccup posture kept).

Pinned by test/skillopt/validate-gate-abort.test.ts (3 cases). Folds into the
open v0.42.9.0 PR (#1759).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(ci): llms-full.txt over size budget — drop what-schemas-unlock from full bundle

The toolLoop + budget bug-fix annotations grew CLAUDE.md, pushing llms-full.txt
to 756KB over the 750KB FULL_SIZE_BUDGET (the `build-llms > size budget` test
failed, failing the `test` CI job). CLAUDE.md stays inlined by design (it's the
point of the one-fetch bundle), so per the budget comment's own guidance ("ship
with includeInFull=false exclusions") this excludes docs/what-schemas-unlock.md
(15.4KB value-explainer, not load-bearing operational reference) from
llms-full.txt; it stays linked in llms.txt. Bundle now 740KB with ~9KB headroom.
No budget bump — 750KB is near the ~190k-token-context fit ceiling.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* chore(ci): re-admit policy docs into ci-cache-hash before doc relocation

docs/**/*.md is deny-listed from the CI cache hash (test-irrelevant). The
CLAUDE.md restructure moves test/release POLICY into docs/TESTING.md +
docs/RELEASING.md, which DO carry contracts the test suite reads. Without
re-admitting them, a policy-only edit would produce the same cache hash and
skip the test shard that runs the build-llms + doc-history guards (false-pass).

Adds an ALLOW_PATTERNS re-admit step after the deny, scoped to the named
policy docs (not a blanket docs un-deny). Lands FIRST, before any doc moves.

Pinned by 3 new cases in test/scripts/ci-cache-hash.test.ts: TESTING.md +
RELEASING.md edits MUST change the hash; docs/guide.md still must not.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(docs): relocate Key files / thin-client / Testing out of CLAUDE.md (verbatim)

CLAUDE.md had grown to 592KB / ~147k tokens auto-loaded every session (~77% of
the llms-full.txt single-fetch bundle). The per-file index was append-only by
mandate. This is the exact thin-dispatcher-vs-fat-blob anti-pattern gbrain exists
to fix, so CLAUDE.md becomes a thin orientation + resolver that points at
on-demand docs.

This commit is the VERBATIM move (content-preserving — the next commit compresses):
- docs/architecture/KEY_FILES.md   <- ## Key files + the calibration key-files
  cluster + Schema Cathedral v3 impl detail
- docs/architecture/thin-client.md <- ## Thin-client routing
- docs/TESTING.md                   <- ## Testing
- ## Commands DROPPED (18 'added in vX.Y' history blocks; current surface is
  gbrain 0.41.38.0 -- personal knowledge brain

USAGE
  gbrain <command> [options]

SETUP
  init [--pglite|--supabase|--url]   Create brain (PGLite default, no server)
  migrate --to <supabase|pglite>     Transfer brain between engines
  upgrade                            Self-update
  check-update [--json]              Check for new versions
  doctor [--json] [--fast]            Health check (resolver, skills, pgvector, RLS, embeddings)
  integrations [subcommand]          Manage integration recipes (senses + reflexes)

PAGES
  get <slug>                         Read a page
  put <slug> [< file.md]             Write/update a page
  delete <slug>                      Delete a page
  list [--type T] [--tag T] [-n N]   List pages

SEARCH
  search <query>                     Keyword search (tsvector)
  query <question> [--no-expand]     Hybrid search (RRF + expansion)
  ask <question> [--no-expand]       Alias for query

IMPORT/EXPORT
  import <dir> [--no-embed]          Import markdown directory
  sync [--repo <path>] [flags]       Git-to-brain incremental sync
  sync --watch [--interval N]        Continuous sync (loops until stopped)
  sync --install-cron                Install persistent sync daemon
  export [--dir ./out/]              Export to markdown
  export --restore-only [--repo <p>] Restore missing supabase-only files
        [--type T] [--slug-prefix S] With optional filters

FILES
  files list [slug]                  List stored files
  files upload <file> --page <slug>  Upload file to storage
  files upload-raw <file> --page <s> Smart upload (size routing + .redirect.yaml)
  files signed-url <path>            Generate signed URL (1-hour)
  files sync <dir>                   Bulk upload directory
  files verify                       Verify all uploads

EMBEDDINGS
  embed [<slug>|--all|--stale]       Generate/refresh embeddings

LINKS
  link <from> <to> [--type T]        Create typed link
  unlink <from> <to>                 Remove link
  backlinks <slug>                   Incoming links
  graph <slug> [--depth N]           Traverse link graph (returns nodes)
  graph-query <slug> [--type T]      Edge-based traversal with type/direction filters
        [--depth N] [--direction in|out|both]

TAGS
  tags <slug>                        List tags
  tag <slug> <tag>                   Add tag
  untag <slug> <tag>                 Remove tag

TIMELINE
  timeline [<slug>]                  View timeline
  timeline-add <slug> <date> <text>  Add timeline entry

TOOLS
  extract <links|timeline|all>       Extract links/timeline (idempotent)
        [--source fs|db]             fs (default) walks .md files; db iterates engine pages
        [--dir <brain>]              brain dir for fs source
        [--type T] [--since DATE]    filters (db source)
        [--dry-run] [--json]
  publish <page.md> [--password]     Shareable HTML (strips private data, optional AES-256)
  check-backlinks <check|fix> [dir]  Find/fix missing back-links across brain
  lint <dir|file> [--fix]            Catch LLM artifacts, placeholder dates, bad frontmatter
  orphans [--json] [--count]         Find pages with no inbound wikilinks
  salience [--days N] [--kind P]     v0.29: pages ranked by emotional + activity salience
  anomalies [--since D] [--sigma N]  v0.29: cohort-based statistical anomalies (tag, type)
  transcripts recent [--days N]      v0.29: recent raw .txt transcripts (local-only)
  dream [--dry-run] [--json]         Run the overnight maintenance cycle once (cron-friendly).
                                     See also: autopilot --install (continuous daemon).
  check-resolvable [--json] [--fix]  Validate skill tree (reachability/MECE/DRY)
  report --type <name> --content ... Save timestamped report to brain/reports/

BRAIN (capture / ideate / explore — v0.37/v0.38)
  capture [content] [--file PATH]    Single entrypoint for getting content into the brain
        [--stdin] [--slug s] [--type t]   Inline content / file / stdin; writes to inbox/ by default
        [--source ID] [--quiet|--json]    Multi-source brains: route to a non-default source
  brainstorm <question> [--json]     Bisociation idea generator (hybrid search + far-set + judge)
        [--save|--no-save] [--limit N]
  lsd <question> [--json]            Lateral Synaptic Drift: inverted-judge brainstorm
        [--save|--no-save] [--limit N]    rewarding far-from-obvious + axiomatic inversions

SOURCES (multi-repo / multi-brain)
  sources list                       Show registered sources
  sources add <id> --path <p>        Register a source (id = short name, e.g. 'wiki')
  sources remove <id>                Remove a source + its pages
  sync --all                         Sync all sources with a local_path
  sync --source <id>                 Sync one specific source
  repos ...                          DEPRECATED alias for 'sources' (v0.19.0)

CODE INDEXING (v0.19.0 / v0.20.0 Cathedral II)
  code-def <symbol> [--lang l]       Find the definition of a symbol across code pages
  code-refs <symbol> [--lang l]      Find all references to a symbol (JSON-first)
  code-callers <symbol>              Who calls this symbol? (v0.20.0 A1)
  code-callees <symbol>              What does this symbol call? (v0.20.0 A1)
  query <q> --lang <l>               Filter hybrid search to one language (v0.20.0)
  query <q> --symbol-kind <k>        Filter to symbol type (function|class|method|...) (v0.20.0)
  reconcile-links [--dry-run]        Batch-recompute doc↔impl edges (v0.20.0)
  reindex-code [--source id] [--yes] Explicit code-page reindex (v0.20.0)
  sync --strategy code               Sync code files into the brain

JOBS (Minions)
  jobs submit <name> [--params JSON]  Submit background job [--follow] [--dry-run]
  jobs list [--status S] [--limit N]  List jobs
  jobs get <id>                       Job details + history
  jobs cancel <id>                    Cancel job
  jobs retry <id>                     Re-queue failed/dead job
  jobs prune [--older-than 30d]       Clean old jobs
  jobs stats                          Job health dashboard
  jobs work [--queue Q]               Start worker daemon (Postgres only)

ADMIN
  stats                              Brain statistics
  health                             Brain health dashboard
  history <slug>                     Page version history
  revert <slug> <version-id>         Revert to version
  features [--json] [--auto-fix]     Scan usage + recommend unused features
  autopilot [--repo] [--interval N]  Self-maintaining brain daemon
  config [show|get|set] <key> [val]  Brain config
  storage status [--repo <path>]     Storage tier status and health
        [--json]                     (git-tracked vs supabase-only)
  serve                              MCP server (stdio)
  serve --http [--port N]            HTTP MCP server with OAuth 2.1
    --token-ttl N                    Access token TTL in seconds (default: 3600)
    --enable-dcr                     Enable Dynamic Client Registration
    --public-url URL                 Public issuer URL (required behind proxy/tunnel)
  call <tool> '<json>'               Raw tool invocation
  version                            Version info
  --tools-json                       Tool discovery (JSON)

Run gbrain <command> --help for command-specific help. + the per-command KEY_FILES entries; content stays in git)

CLAUDE.md gains: a Reference map (resolver), a Maintaining section (the
anti-disease rule), and a Cross-cutting invariants subsection under Architecture
so the must-never-violate rules (trust fail-closed, sourceScopeOpts isolation,
JSONB trap, engine parity, contract-first, migrations, multi-source) still
auto-load after the index moved out.

Result: CLAUDE.md 592KB -> 61KB; llms-full.txt 740KB -> 210KB (new docs link-only
until compressed). build-llms drift + budget test green; verify 29/29 green.
The pre-move content is recoverable at git show <this^>:CLAUDE.md.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(docs): compress relocated docs to current-state + add recurrence guard

Compresses the verbatim-relocated reference docs from append-only release-history
to current-state-only (the disease cure), then makes recurrence structurally
impossible via a CI guard.

Compression (fan-out subagents + adversarial verify, audited mechanically):
- KEY_FILES.md 453KB -> 356KB; TESTING.md 42KB -> 38KB; thin-client.md already clean.
- 393/393 entries preserved; every src/test/scripts path from the verbatim original
  survives (mechanical comm-check); zero bolded **v0. markers remain.
- Conservative ratio (~22%) because the content is invariant-dense — correctness
  over brevity. Dropped: **vX.Y.Z (#NNN):** clauses, codex/review tags, contributor
  credits, PR-numbers-as-ids, pre-fix/then/was-now history deltas. Kept: every
  exported symbol, invariant, and Pinned-by reference. Verbatim original recoverable
  at git show <relocation-commit>:docs/architecture/KEY_FILES.md.

Recurrence guard (scripts/check-key-files-current-state.sh, wired into verify + check:all):
- HARD: bans the bolded **v0.<digit> marker in the reference docs (scoped — plain
  'as of pgvector 0.7' prose is fine, no false positives).
- HARD: CLAUDE.md size cap (90KB; currently 61KB) — the structural backstop.
- Pinned by test/scripts/check-key-files-current-state.test.ts (7 cases).

Content contracts (test/build-llms.test.ts, +5 cases per codex outside-voice):
CLAUDE.md keeps inline ship IRON RULES (version format, document-release,
never-hand-roll); AGENTS.md keeps its boot order; llms indexes the new docs;
KEY_FILES stays link-only (not inlined).

Privacy: scrubbed the relocated 'wintermute/chat/' source-boost examples + the
literal harvest-lint regex to generic placeholders (legitimate in allowlisted
CLAUDE.md; genericized for the new public docs per the privacy rule).

Reverts the 284c50a4 band-aid: re-inlines docs/what-schemas-unlock.md now that the
restructure freed ~530KB of bundle headroom (llms-full.txt 740KB -> 225KB).

verify 30/30 green (incl. new check:doc-history).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* refactor(docs): relocate verbose release process to docs/RELEASING.md

The highest-/ship-risk commit (isolated so it can revert alone). Moves the verbose
release + contributor procedure out of CLAUDE.md, keeping every ship-critical IRON
RULE inline so /ship + /document-release (which read CLAUDE.md) cannot regress.

Moved to docs/RELEASING.md: pre-ship test requirements; the CHANGELOG-branch-scoped
+ CHANGELOG voice + release-summary template; the 'To take advantage of vX' block
spec; version migrations + migration-is-canonical; schema state tracking; GitHub
Actions SHA maintenance; PR-descriptions-cover-the-branch; community-PR-wave;
checking-out-PRs-from-garrytan-agents.

Kept INLINE in CLAUDE.md (ship-critical IRON RULES — do NOT move):
- the Version-locations table (5-file sync) + the 3-line consistency audit
- Conductor branch=workspace
- Post-ship /document-release (MANDATORY)
- Privacy + Responsible-disclosure rules (Privacy also anchors the check-privacy
  allowlist — the only place allowed to name the fork)
- PR-title-version-first
- never-hand-roll-ship (Skill routing)
Plus a new ## Releasing pointer ('Before any ship, read docs/RELEASING.md in full')
and a resolver row.

CLAUDE.md 61KB -> 39KB (592KB -> 39KB overall, 93% cut; ~9k tokens auto-loaded vs
~147k). CLAUDE.md size-gate tightened 90KB -> 60KB. The content-contract tests pin
that the inline IRON RULES (MAJOR.MINOR.PATCH.MICRO, document-release, hand-roll
ship) did NOT move out. The moved ranges carry no banned fork name, so RELEASING.md
needs no privacy allowlist entry. verify 30/30; bundle 225KB -> 204KB.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* docs(changelog): note CLAUDE.md restructure in v0.42.9.0

The CLAUDE.md thin-resolver restructure (592KB → 39KB) rides in this
release; record it under the existing v0.42.9.0 For-contributors section.
No version bump — v0.42.9.0 is unreleased and already allocated to this PR.

* fix(ci): ci-cache-hash re-admit matched a literal \t, a no-op on GNU grep

The policy-doc re-admit (75992b77) put `\t` inline in the ALLOW patterns
passed to `grep -E`. BSD grep (macOS local) treats `\t` as a tab so it
worked locally; GNU grep (Ubuntu CI) treats it as literal `t`, so nothing
re-admitted and docs/TESTING.md / docs/RELEASING.md stayed deny-listed —
the two policy-doc tests failed on CI shard 6 (1097 pass / 2 fail).

Build ALLOW_RE with `printf '\t(%s)'` so the tab is a real byte, identical
in construction to DENY_RE (line 117), which the CI log shows matches
correctly on GNU grep. End-to-end: editing docs/TESTING.md now flips the
hash; a normal docs/*.md add still does not (deny stays scoped).

* fix(skillopt): feed the scorer's success criteria to the optimizer

Surfaced by the SkillOpt real-LLM eval (Track B). The reflect step was shown
only a pass/fail score and the agent transcript — never WHAT the benchmark
judge rewards. On a skill judged by structure (e.g. "must include a
Confidence: line") the optimizer proposed plausible-but-off edits ("close with
a synthesis") that never satisfied the literal check; every candidate scored 0
on D_sel, the validation gate rejected them all, and the skill text never
changed (optimized === baseline === 0).

Fix: render each benchmark Judge (rule checks / llm rubric / qrels) into
plain-English criteria via new exported describeJudge / describeJudges, and
thread them into the reflect prompt (a SUCCESS CRITERIA block) for both the
loop reflect calls and the one-shot-rewrite path. The orchestrator computes the
distinct criteria across train+sel+test once. The optimizer system prompt now
instructs it to satisfy the criteria through genuine content, never empty
keywords — reward-hacking stays defended by the independent held-out gate
(cat32 confirms the gate catches a keyword-stuffing hack).

End-to-end this took a deficient skill from 0.00 to 1.00 on a held-out set it
never trained on. Pinned by test/skillopt/reflect.test.ts (describeJudge per
kind, describeJudges dedup, criteria present/absent in the prompt). Folds into
the open v0.42.9.0 PR (#1759).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-03 05:50:21 -07:00
Garry TanGitHub@garrytan-agents <noreply@github.com>Claude Opus 4.7
6ae94301a6 v0.41.26.1 fix: lock-renewal cathedral — closes ~39 worker crashes/day (supersedes #1567) (#1572)
* v0.41.26.1 fix: lock-renewal cathedral — closes ~39 worker crashes/day (supersedes #1567)

Production worker daemons against Supabase / PgBouncer were crashing
~39 times/day with `unhandledRejection at renewLock`. PR #1567
proposed the right try/catch shape; this wave incorporates it and
closes the entire bug class (4 inside-review + 8 outside-voice
findings absorbed via 9 locked design decisions).

What's fixed:

- `setInterval(async () => await renewLock(...))` replaced with a
  sync wrapper around the new pure `runLockRenewalTick` function.
  No more unhandled rejections escaping the timer callback.
- Second crash vector closed: `.catch()` on the stored
  `executeJob(...).finally(...)` promise so failJob/completeJob
  throws during the same outage can't propagate to
  `process.on('unhandledRejection')`.
- Per-call `Promise.race` timeout (default `lockDuration/3`) bounds
  hung renewLock calls so the re-entrancy guard can't wedge
  indefinitely.
- Time-based abort (NOT count-based) so the worker releases its
  lock BEFORE another worker can reclaim. With the prior 3-strike
  count + 30s lockDuration, a 15s window let other workers race.
- Infrastructure aborts (`lock-renewal-failed`, `lock-lost`) don't
  burn job attempts — `executeJob`'s catch consults the exported
  `INFRASTRUCTURE_ABORT_REASONS` set and skips `failJob` so the
  stall detector reclaims cleanly.
- Universal grace-eviction: 30s force-evict safety net now fires
  for ANY abort reason, not just `job.timeout_ms`.

What's added:

- `src/core/minions/lock-renewal-tick.ts` (NEW): pure extracted
  state-machine function + env-knob resolver. Three operator-tunable
  knobs via env (max-failures-for-audit, call-timeout-ms,
  safety-margin-ms) with stderr-warn-once on bad input + default
  fallback.
- `src/core/audit/lock-renewal-audit.ts` (NEW): sibling of
  `batch-retry-audit.ts`. Four outcomes: failure /
  success_after_failure / gave_up / executeJob_rejected. JSONL at
  `~/.gbrain/audit/lock-renewal-YYYY-Www.jsonl`.
- `src/core/audit/redact-connection-info.ts` (NEW): shared privacy
  helper. Strips Postgres URLs, host=, user=, password=, IPv4 from
  error messages before they hit audit JSONL. Wired into BOTH the
  new lock-renewal audit AND the existing batch-retry audit
  (privacy backfill — same risk class).
- `scripts/check-worker-lock-renewal-shape.sh` (NEW): CI guard
  wired into `bun run verify`. Asserts the v0.41.22.1 bug pattern
  (`lockTimer = setInterval(async ...)`) stays absent AND the pure
  function call site survives refactors. Bug-pattern-specific so it
  doesn't fight legitimate refactors (codex C12).

Tests: 64 new cases across 5 new test files. 182 existing minion +
worker tests still pass. All hermetic — no PGLite, no real network,
no `mock.module`.

Plan + 9 decisions + codex outside-voice review at
~/.claude/plans/system-instruction-you-are-working-humming-nygaard.md

Closes #1567 (incorporates the contributor's try/catch shape; closes
the bug class structurally).

Co-Authored-By: @garrytan-agents <noreply@github.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* test: fill A-H gaps for v0.41.26.1 lock-renewal cathedral

The original v0.41.26.1 wave shipped 64 hermetic unit tests on the
pure tick function, audit primitives, and redactor. Post-ship audit
flagged 8 wiring gaps the pure tests can't see — A (launchJob
wiring), B (executeJob skip-failJob), C (.catch on stored promise),
D (INFRASTRUCTURE_ABORT_REASONS export), E (universal grace-evict),
F (executeJob_rejected end-to-end), G (re-entrancy guard at worker
layer), H (gold-standard E2E regression).

Now closed:

- **test/worker-lock-renewal-e2e.serial.test.ts** (1 test, gap H):
  the headline gold-standard regression. Real PGLite + real
  MinionWorker + executeRaw wrap that injects renewLock failures on
  demand. Pins that the worker process DOES NOT crash via
  unhandledRejection under sustained renewLock throws, the handler
  observes abort.signal.aborted = true with reason
  'lock-renewal-failed', and the audit JSONL contains both `failure`
  and `gave_up` events. The exact v0.41.22.1 production bug class.
  Quarantined to its own file because bun:test serial + PGLite has an
  unresolved interaction with multiple MinionWorker-driven tests in
  the same file (second test's queue.add hangs indefinitely).

- **test/worker-lock-renewal-shape.test.ts** (18 tests, gaps A-G):
  source-shape behavioral pins. Greps worker.ts function bodies for
  the patterns the locked decisions promised: launchJob calls
  runLockRenewalTick + resolveLockRenewalKnobs + uses
  lockRenewalAudit; tickInFlight declared and gated correctly; stored
  executeJob promise has .catch with logExecuteJobRejected + console
  stderr; abort.signal.addEventListener fires for any abort (not just
  timeout_ms); INFRASTRUCTURE_ABORT_REASONS used inside executeJob's
  catch with return-early shape. Bug-pattern-specific so a refactor
  that genuinely improves the shape passes; a refactor that
  accidentally strips a guarantee fails loud.

All 83 lock-renewal wave tests pass in 5.2s. 205 existing minion +
worker tests still green. No production code changes.

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

* fix: typecheck errors in worker-lock-renewal-e2e.serial.test.ts

CI verify failed on the new E2E gap-fill test (commit a8b282d4) due
to two TS errors that bun's runtime accepts but tsc rejects:

1. line 92: `originalExecuteRaw(sql, ...args)` with `args: unknown[]`
   — TS can't prove args has <=2 elements, so the call site looks
   like 1+1+N args against a function that accepts 1-3. Fixed by
   destructuring the wrap params explicitly: `(sql, params?, opts?)`
   matching the executeRaw signature, then calling
   `originalExecuteRaw(sql, params, opts)` with named args.

2. line 145: `expect(abortReason).toBe('lock-renewal-failed')` where
   `abortReason: string | null = null`. TS narrows the variable to
   `null` because the closure assignment in worker.register isn't
   observable to the inferrer. bun:test's `.toBe` overload then
   picks the null variant and rejects the string literal. Fixed by
   `as unknown as string` cast — the preceding `handlerAbortObserved`
   assertion guarantees we entered the branch where abortReason was
   assigned. Documented inline.

Local verify (29 checks) now green; E2E test still passes in 5.0s.

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

---------

Co-authored-by: @garrytan-agents <noreply@github.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-28 08:20:58 -07:00
a7b79b66d4 feat: v0.41.19.0 Supavisor Retry Cathedral (#1537)
Engine-level retry primitive that closes the v0.41.17 production incident
where ~3,000 wiki links + timeline entries were silently lost per dream
cycle on a 16K-page brain. Supavisor's circuit-breaker takes 5-10s to
recover; the prior single-500ms-retry shape couldn't survive it.

ARCHITECTURE
============

Retry becomes a data-primitive contract, not a caller responsibility.
postgres-engine.ts + pglite-engine.ts now self-retry inside addLinksBatch,
addTimelineEntriesBatch, and upsertChunks. Every caller — current AND
future — inherits retry-for-free. CI lint guard `scripts/check-no-double-retry.sh`
fails the build if anyone re-wraps an engine batch method (preventing
3×3=9 retry amplification on incomplete reverts).

CODEX-HARDENED DEFAULTS
=======================

BULK_RETRY_OPTS = {maxRetries:3, delayMs:1000, delayMaxMs:10000,
jitter:'decorrelated'}. Total worst-case wait ≈12s covers full Supavisor
recovery window. Decorrelated jitter (AWS-style uniform(base, prevDelay*3)
capped at maxDelay) replaces 'full' which allowed near-zero retries that
re-hit the still-recovering breaker.

AbortSignal threading from MinionWorker.shutdownAbort.signal through
engine method opts → withRetry → abortableSleep. SIGTERM aborts sleeping
retries instead of blocking deploys for up to delayMaxMs.

OBSERVABILITY
=============

`~/.gbrain/audit/batch-retry-YYYY-Www.jsonl` records every retry event
(success-after-blip AND exhausted-retries). Built on the v0.40.4.0
audit-writer cathedral. Privacy posture: never logs slugs / page IDs /
content (mirrors shell-audit.ts).

`gbrain doctor` learns `batch_retry_health` check. Reads last 24h
(not 7d — codex H-9: avoid permanent noise from one historical blip).
Thresholds: ok (zero or <3 same-site), warn (>=3 same-site OR >=5
cross-site), fail (>=20 sustained breaker). Surfaces bad GBRAIN_BULK_*
env at startup (codex M-10). Corrupt-JSONL tolerant.

30-day audit pruning hooked into the dream cycle's purge phase (codex H-8
— implements the 'pruning convention' for real).

OPERATOR TUNING
===============

GBRAIN_BULK_MAX_RETRIES (int >= 0; 0 disables retries for debugging)
GBRAIN_BULK_RETRY_BASE_MS (int > 0)
GBRAIN_BULK_RETRY_MAX_MS (int >= base)

Bad values throw GBrainError with paste-ready fix hints at doctor startup,
not at first-retry mid-cycle.

VERIFICATION
============

- bun run verify: 28/28 checks green (includes 2 new lint guards:
  check-no-double-retry, check-batch-audit-site)
- bun run test: 11453 pass / 1 pre-existing flake (schema-cli.test.ts —
  confirmed by running on clean master, NOT introduced by this wave)
- bun run test:slow: 40/40 including new test/core/retry-stress.slow.test.ts
  (100 batches × 30% blip rate × decorrelated jitter, zero row loss)
- bunx tsc --noEmit: 0 errors

REVIEWS
=======

- CEO review (SELECTIVE EXPANSION): 4 cherry-picks proposed, 4 accepted
- Eng review (2 passes): 10 findings, 0 critical gaps, architectural
  pivot from per-site to engine-level wrap
- Codex independent review: 23 findings; 10 critical/high absorbed
  (decorrelated jitter, 12s backoff window, AbortSignal, idempotency
  proof, backfill unification, typed audit-site enum, doctor expiry
  thresholds, audit pruning, env validation at doctor startup)

PR #1523 closed and absorbed (@garrytan-agents original extract.ts fix
preserved via co-author trailer; 5 test cases moved to test/core/retry.test.ts
with assertions adjusted for the v0.41.19.0 BULK_RETRY_OPTS defaults).

Co-authored-by: garrytan-agents <noreply@anthropic.com>
2026-05-26 23:15:44 -07:00
10816cba38 v0.41.18.0: gbrain onboard — the activation surface gbrain didn't have before (#1521)
* feat(schema): migrations v98/v99/v100 for onboard wave (A6 A10 A11 A13 A25, codex #1 #9 #10 #11 #12)

Three schema additions supporting the gbrain onboard wave:

v98 — links.link_kind nullable column (A10, codex finding #12).
The NER extraction was originally going to add a new link_source='ner'
provenance, but that would have forced every existing link_source='mentions'
query (backlink-count filter, orphan-ratio, doctor checks) to update or
metrics would drift across the cutover. Instead: keep link_source='mentions'
for the storage layer AND add a nullable link_kind column. Three kinds:
'plain', 'typed_ner', NULL (legacy/unknown — semantically 'plain'). NOT in
the links UNIQUE constraint so the storage shape stays compatible.

v99 — timeline_entries dedup widening (A11, codex finding #11).
Pre-v99 dedup key was (page_id, date, summary). The new --from-meetings
extraction writes timeline entries with source='extract-timeline-from-
meetings:<meeting-slug>', and codex caught that two meetings with the same
date+summary on the same entity page would silently DO NOTHING — the
second meeting's provenance is lost. Widened to (page_id, date, summary,
source). Legacy rows (source='') preserve current dedup behavior.

v100 — migration_impact_log table + content_chunks_stale_idx partial
(A6 + A25 + A13 + codex findings #10 + #9). Bundled because both are
consumed by the onboard pipeline and ship together. Impact log captures
before/after metric stats so gbrain onboard --history shows real deltas;
attribution columns (job_id, source_id, brain_id, started_at,
idempotency_key) prevent concurrent runs misattributing to wrong
migrations. content_chunks_stale_idx partial WHERE embedding IS NULL
supports gbrain embed --stale + --priority recent (outer ORDER BY
p.updated_at DESC uses existing idx_pages_updated_at_desc via JOIN).
Plain NUMERIC columns; delta computed at read time (NOT a stored
GENERATED column per eng-review D2 — zero PGLite parity risk).

Slot history note: plan originally proposed v97/v98/v99 but master had
already used v95 (links 'mentions' CHECK widening), v96 (facts conversation
session index), and v97 (pages_dedup_partial_index) by ship time. Codex
caught the collision; renumbered to v98/v99/v100.

Test pin: test/schema-bootstrap-coverage.test.ts (100/100 migrations
apply clean on PGLite), test/migrate.test.ts (152 cases pass).

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

* refactor(remediation): extract doctor remediation library (A1, codex finding #2)

Pre-fix: src/commands/doctor.ts contained two CLI-shaped functions
(runRemediationPlan + runRemediate) with hardcoded argv parsing,
process.exit calls, and console.log emission. Onboard CLI shell and the
upcoming MCP run_onboard op couldn't compose against them — the plan
file's "100-LOC thin wrapper" assumption didn't survive codex's review
of the actual source.

Post-fix: src/core/remediation/ exports a library shape that all three
consumers (doctor CLI, onboard CLI, MCP run_onboard) wrap.

  src/core/remediation/types.ts
    RemediationPlanOpts, RemediationPlan, RemediationOpts,
    RemediationResult, StepResult, RemediationHooks (the observability
    seam — library never calls console.* itself).

  src/core/remediation/context.ts
    loadRecommendationContext moved verbatim from doctor.ts. Re-exports
    RecommendationContext from brain-score-recommendations.ts since
    that's still the canonical home for the type (consumed by
    computeRecommendations).

  src/core/remediation/plan.ts
    computeRemediationPlan(engine, opts): Promise<RemediationPlan>.
    Pure read; produces the stable JSON envelope downstream agents
    bind to. Pulls in computeRecommendations + classifyChecks +
    maxReachableScore behind one library entry point.

  src/core/remediation/run.ts
    runRemediation(engine, opts, hooks): Promise<RemediationResult>.
    Orchestrator with BudgetTracker, checkpoint resume, D5 dep
    cascade, D7 per-step recheck. Returns a result object instead
    of process.exit calls; the CLI shell maps result.budget_exhausted
    / .target_unreachable / .submitted to exit codes.

  src/core/remediation/index.ts
    Barrel for the three modules above.

doctor.ts is now a thin wrapper:
  runRemediationPlan: parse argv → computeRemediationPlan → human/JSON render
  runRemediate: parse argv → TTY confirm gate → runRemediation(hooks: console.*)
The TTY confirmation step deliberately stays in the CLI shell — the library
never asks for confirmation; that's a CLI concern.

Net: ~340 LOC removed from doctor.ts; ~470 LOC added across the library
module (with full JSDoc + per-A-decision rationale comments). Functional
behavior preserved bit-for-bit: 67 tests pass across doctor.test.ts +
v0_37_gap_fill.serial.test.ts.

The Lane E.4 source-text test (test/v0_37_gap_fill.serial.test.ts:329)
followed loadRecommendationContext to its new home at
src/core/remediation/context.ts — assertions otherwise unchanged.

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

* refactor(remediation): generalize computeRecommendations to accept extras (A2, codex finding #3)

Pre-fix: computeRecommendations at brain-score-recommendations.ts:170 was a
hardcoded planner for 5 synthetic check categories. Adding a Check.remediation
field to a new doctor check would NOT auto-wire into --remediation-plan —
the planner simply ignored it. Codex caught this when reviewing the plan's
"checks ARE specs" framing.

Post-fix: optional third arg `extraRemediations: RemediationStep[]` lets
callers inject step entries discovered outside the hardcoded planner. The
existing 5-category surface is preserved bit-for-bit; on id collision the
hardcoded entry wins, so an extra accidentally duplicating a hardcoded id
doesn't shadow legacy behavior.

RemediationPlanOpts gains the matching field; computeRemediationPlan in
src/core/remediation/plan.ts threads opts.extraRemediations through. The
4 new doctor checks (T4) will produce per-check helper functions that
return RemediationStep[]; onboard's render layer (T12) aggregates them
into the opts.extraRemediations slot. doctor's existing
--remediation-plan call passes empty (no behavior change for legacy CLI).

84 tests pass across brain-score-recommendations + doctor suites.

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

* feat(doctor): 4 new onboard checks (embed_staleness, link_coverage, timeline_coverage, takes_count) (A16, T4)

Adds src/core/onboard/checks.ts: 4 check helpers + a runAllOnboardChecks
aggregator. Each helper returns {check, remediations}, so doctor pushes
the Check entry (for human/JSON rendering) AND onboard's plan path
collects the RemediationStep[] (via T3's new extraRemediations seam in
computeRecommendations).

embed_staleness: COUNT(*) on content_chunks WHERE embedding IS NULL.
  Cheap thanks to content_chunks_stale_idx partial (v100).
  warn at 1+ stale, fail at 1000+; remediation points at embed-catch-up
  handler (built in T6).

entity_link_coverage: fraction of entity pages with inbound links.
  Per A21 + codex #15: TABLESAMPLE BERNOULLI on PG when total_pages > 50K
  with pinned sample formula (LEAST 100, GREATEST 2, target ~5000 rows)
  AND ±sqrt(p(1-p)/n) confidence interval embedded in message
  ("coverage: 31% ± 1.3%") so warn/fail decisions show their margin of error.
  PGLite path: full scan (rare >50K).
  warn <70%, fail <40%; remediation points at extract-ner handler.

timeline_coverage: same TABLESAMPLE policy. warn <90%, fail <70%;
  remediation points at extract-timeline-from-meetings handler.

takes_count: COUNT(*) on takes table. Per A12 two-gate consent: the
  remediation only emits when `takes.bootstrap_enabled` config is true.
  Otherwise the check shows "0 takes (takes.bootstrap_enabled is false;
  opt in to enable)" without an autopilot-eligible remediation. Prevents
  unattended LLM-bearing extractions on brains that haven't opted in.

runDoctor wires runAllOnboardChecks at the end of the DB-checks block
(after stale_locks); fast-mode skipped to preserve --fast UX.

Thin-client parity (A16 spec) deferred to T16 — the MCP run_onboard op
will run these helpers server-side where engine.executeRaw works,
which is the real federated path. Adding them to doctor-remote.ts
would duplicate the logic without functional benefit since the helpers
are server-side queries.

55 doctor tests pass; typecheck clean.

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

* feat(engine): listStaleChunks --priority recent + executeRaw AbortSignal (A13/A20, codex #7 #9)

Two interface extensions on BrainEngine, with parity across postgres-engine
and pglite-engine. Plus a follow-on fix for v99's timeline_entries dedup
widening.

listStaleChunks gains:
  - orderBy?: 'page_id' | 'updated_desc' (default 'page_id' = legacy)
  - afterUpdatedAt?: string | null (composite cursor for updated_desc)

When orderBy === 'updated_desc' the query JOINs pages and orders by
  p.updated_at DESC NULLS LAST, p.id ASC, cc.chunk_index ASC
backed by idx_pages_updated_at_desc + content_chunks_stale_idx partial
(both indexes added in v100). The cursor "next row" semantic with DESC
NULLS LAST + ASC tiebreakers is:
  (updated_at < prev) OR
  (updated_at = prev AND page_id > prev_page_id) OR
  (updated_at = prev AND page_id = prev_page_id AND chunk_index > prev_chunk_index)
First page (afterUpdatedAt undefined AND afterPageId 0) bypasses the
cursor predicate. Both engines parity-tested via 100/100 pglite-engine
tests; Postgres path mirrors the same WHERE clause structure.

executeRaw gains:
  - opts?: {signal?: AbortSignal}

Postgres impl: real cancellation via postgres.js's .cancel() on the
pending query. Pre-aborted signal short-circuits before the network
round-trip; mid-flight abort fires .cancel(). The query throws on
abort which the caller catches.

PGLite impl: in-process WASM has no kernel-level cancellation.
Best-effort: pre-check, then race the query against a signal-rejection
promise. The query keeps running in WASM but the awaited result is
discarded (DOMException AbortError thrown). Documented gap.

ReservedConnection.executeRaw extends the signature for type
compatibility but doesn't wire the signal (its only callers are
migrations + cycle-lock writes that explicitly don't want cancellation).

V99 timeline dedup follow-on: the dedup widening in migration v99
changed the unique index from (page_id, date, summary) to
(page_id, date, summary, source). The ON CONFLICT clauses in both
engines' addTimelineEntriesBatch + addTimelineEntry impls were still
using the old 3-tuple, causing 12 PGLite tests to fail with SQLSTATE
42P10 "no unique constraint matching ON CONFLICT specification".
Updated all 4 sites (2 per engine) to the 4-tuple.

Typecheck clean, 100/100 PGLite engine tests pass.

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

* feat(embed): --batch-size + --priority recent + --catch-up + embed-catch-up handler (A13)

CLI surface on gbrain embed gains 3 flags:
  --batch-size N       Override hardcoded PAGE_SIZE=2000 (clamped 1..10000)
  --priority recent    Walk stale chunks newest-first (page.updated_at DESC)
                       backed by content_chunks_stale_idx + idx_pages_updated_at_desc
                       via T5's listStaleChunks(orderBy='updated_desc') extension.
                       Composite cursor (updated_at, page_id, chunk_index).
  --catch-up           Removes the GBRAIN_EMBED_TIME_BUDGET_MS wall-clock cap;
                       loops until countStaleChunks() returns 0.

EmbedOpts gains matching fields; embedAll + embedAllStale plumb them through.
The cursor tracking in embedAllStale now advances (afterUpdatedAt, afterPageId,
afterChunkIndex) instead of just (afterPageId, afterChunkIndex) when in
'updated_desc' mode. The engine returns p.updated_at as Date|string; the
caller normalizes to ISO string for the next page's cursor.

New Minion handler `embed-catch-up` registered in jobs.ts. Wraps runEmbedCore
with stale=true + catchUp=true + the priority/batchSize the caller supplies.
NOT in PROTECTED_JOB_NAMES (embedding spend only — same posture as the
existing embed-backfill handler). Consumed by the gbrain onboard remediation
pipeline (T11) when embed_staleness check fires.

63 embed tests pass; typecheck clean.

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

* feat(extract): NER link extraction via schema-pack inference.regex (A10, T7, codex #12)

NEW src/core/extract-ner.ts: extractNerLinks(engine, opts). Walks pages,
reuses the by-mention gazetteer, applies the active schema-pack's
link_types[].inference.regex patterns to assign a typed verb to each
mention ("CEO of Acme" + Acme is a company → 'works_at' linking the
source page to Acme).

Codex finding #12 design: do NOT split link_source='ner' as a new
provenance. NER is still mention-derived; splitting would break every
existing link_source='mentions' query (backlink-count, orphan-ratio,
doctor checks). Instead: keep link_source='mentions' AND set
link_kind='typed_ner' (v98 column).

LinkBatchInput type gains link_kind field. Both engines'
addLinksBatch impls add the column to the INSERT projection + unnest()
tuple (column #11). The links UNIQUE constraint excludes link_kind so
an existing plain mention row + a typed_ner row for the same (from, to,
type, source, origin) collide DO NOTHING; the typed link goes in as a
separate row with a DIFFERENT link_type (the inferred verb), so they
don't collide on the typical case.

CLI: `gbrain extract links --ner` (DB source only). Combined
`--by-mention --ner` walk shares ONE gazetteer build across both passes
— saves a full walk on big brains. Either flag alone runs its pass
solo. Each gets its own --source-id filter inheritance.

Minion handler: `extract-ner` (NOT in PROTECTED_JOB_NAMES — regex-only,
no LLM spend). Consumed by onboard's entity_link_coverage remediation
when coverage <70%.

Target-type lookup: one round-trip SELECT slug, source_id, type FROM
pages WHERE type IN ('person', 'company', 'organization', 'entity')
AND deleted_at IS NULL — built once at extraction start, consulted
per-mention. Avoids the N+1 getPage cost.

Pack best-effort: when no active pack OR no link_types declared OR
no inference.regex on any link_type, returns pack_unavailable=true and
0 created. CLI prints a one-line note; handler returns silently.

122 tests pass (pglite-engine + by-mention); typecheck clean.

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

* feat(extract): timeline from meetings — gbrain extract timeline --from-meetings (A11, T8, codex #11)

NEW src/core/extract-timeline-from-meetings.ts:
extractTimelineFromMeetings(engine, opts). Walks meeting pages, finds
discussed entities via two sources, writes a timeline entry on each
entity page.

Discussed-entity sources merged:
  1. Existing 'attended' links from the meeting (canonical attendees).
     One round-trip SELECT pulls all attended edges for the loaded
     meeting set; in-memory Map<meetingSlug → attendees[]> for O(1)
     lookup per meeting.
  2. Body-text mentions via the existing by-mention gazetteer
     (findMentionedEntities + cross-source guard). Catches entities
     discussed in the meeting body even when no explicit 'attended'
     link exists.

De-duped via Map<sourceId::slug → entity> within each meeting so a
person who's both an attendee AND mentioned in the body gets exactly
one timeline row per meeting, not two.

Timeline write uses TimelineBatchInput with:
  source = 'extract-timeline-from-meetings:<meeting-slug>'
  summary = 'Discussed in <meeting-title>'
  date = meeting.effective_date

Per v99 dedup widening (codex #11): the source field is now in the
uniqueness key (page_id, date, summary, source). Two meetings on the
same date with the same summary on the same entity page survive as
distinct rows — the second meeting's provenance is no longer silently
dropped.

CLI: `gbrain extract timeline --from-meetings` (DB source only). Mode
dispatch — runs SOLO (does not combine with --by-mention/--ner; those
are links passes).

Minion handler: `extract-timeline-from-meetings` (NOT in
PROTECTED_JOB_NAMES — pure SQL + string scan). Consumed by onboard's
timeline_coverage remediation when coverage <90%.

Typecheck clean.

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

* feat(takes): takes-bootstrap from concept/atom/lore pages (A12, A24, T9)

NEW src/core/extract-takes-from-pages.ts: Haiku classifier loop. Walks
pages WHERE type IN ('concept','atom','lore','briefing','writing',
'originals') AND deleted_at IS NULL AND length(compiled_truth) > 200,
ordered by updated_at DESC. Each page is truncated to 20K chars and
sent to Haiku with a strict-JSON classifier prompt:
  {"claim", "kind": fact|take|bet|hunch, "weight": 0..1}

Inserts via addTakesBatch with source='cli:takes-bootstrap-from-pages'.

Two-gate consent per A12:
  1. `takes.bootstrap_enabled` config (default false) — even the manual
     CLI refuses without it explicitly set.
  2. --yes flag (CLI) — interactive confirmation that this sends content
     to Haiku.

The handler-side gate also reads takes.bootstrap_enabled, so even a
trusted local Minion submitter (allowProtectedSubmit=true) cannot
fire takes-bootstrap on a brain that hasn't opted in.

CLI: `gbrain takes extract --from-pages [--yes] [--dry-run] [--source-id X]
[--max-pages N] [--holder name]`. Surfaces consent-gate-blocked vs
llm-unavailable distinctly so users see the actual blocker.

Minion handler `extract-takes-from-pages` added to PROTECTED_JOB_NAMES.
Consumed by onboard's takes_count remediation when count=0 AND
takes.bootstrap_enabled=true (handler-side double-check).

Per A24: ships with classifier infrastructure ONLY. Per-prompt eval suite
deferred to v0.42.1 follow-up; autopilot remediation tier for takes-bootstrap
stays manual_only until eval coverage catches up. Manual `gbrain takes
extract --from-pages --yes` is the only path that triggers it in v0.42.0.

parseClaimsJson exported for unit testing — strict JSON parse + ```json
fence strip + kind allowlist filter, returns [] on any parse failure.

Typecheck clean.

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

* feat(minions): recordMinionJobSpend primitive for MCP client_id attribution (A7+A23, codex finding #4)

NEW src/core/minion-spend.ts: small primitive that closes the per-OAuth-
client spend chain gap codex flagged when MCP run_onboard submits child
Minion jobs.

Pre-fix: only subagent loops via budget-meter.ts recorded spend against
the originating OAuth client. Generic Minion handlers (embed-catch-up,
extract-ner, extract-timeline-from-meetings, extract-takes-from-pages)
wrote to the gateway with no per-client attribution — admin-scope tokens
would have unbounded indirect spend via the run_onboard fan-out.

Convention for v0.42.0 (deferred schema column to v0.42.1):
  - run_onboard MCP op sets job.data.client_id when submitting each
    child handler.
  - Handlers that spend LLM/embedding budget call
    recordMinionJobSpend(engine, job, {operation, spendCents, ...})
    which reads job.data.client_id and writes mcp_spend_log with
    the right attribution.
  - Local-submitted jobs (CLI, autopilot tick) pass no client_id;
    the row still lands with client_id=null for global accounting.

Two exports:
  getJobClientId(job): undefined for local jobs; the OAuth client_id
    string for MCP-submitted ones.
  recordMinionJobSpend(engine, job, entry): wraps recordSpend with
    job-aware attribution. Best-effort throughout — spend telemetry
    failures MUST NOT fail the user's call.

A23 full schema column (minion_jobs.client_id + index) deferred to
v0.42.1; today's JSONB-pass-through is sufficient for the MCP
run_onboard chain to land per-client attribution end-to-end. Handlers
adopt the primitive over time; no behavior change for callers that
haven't migrated.

Typecheck clean.

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

* feat(onboard): impact capture module + writeImpactLogRow primitive (A6 + A25 + A17, T11)

NEW src/core/onboard/impact-capture.ts. Three exports:

captureMetric(engine, metric)
  Pure-ish: returns the current numeric value for one of 5 metrics
  (orphan_count, stale_count, entity_link_coverage, timeline_coverage,
  takes_count). Returns null on any throw per A17 best-effort posture
  — a stat-query failure MUST NOT block the extraction itself.

writeImpactLogRow(engine, attribution, metric, before, after, details?)
  Best-effort INSERT into v100's migration_impact_log table. Attribution
  columns (job_id, source_id, brain_id, started_at, idempotency_key,
  applied_by) per A25 + codex finding #10 so concurrent runs can't
  misattribute deltas.

withImpactCapture(engine, attribution, metric, runner, details?)
  Convenience: capture-before → run → capture-after → write log row.
  Per A17 the log row lands even when the runner throws (after-on-fail
  + error in details), so downstream consumers see a "ran but impact
  unknown" entry instead of silent loss.

Designed to be picked up by the 4 new Minion handlers (embed-catch-up,
extract-ner, extract-timeline-from-meetings, extract-takes-from-pages)
when they wrap their main runner. Handlers stay decoupled from the
log-write path — they just call withImpactCapture with the metric they
move. Per-handler integration follows in T12/T13/T15 as those wrappers
land.

Typecheck clean.

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

* feat(onboard): types + render layer (A8, T12)

NEW src/core/onboard/types.ts: OnboardRecommendation (extends
RemediationStep with apply_policy + prompt_text + migration_id),
OnboardReport (stable JSON envelope), OnboardOpts.

NEW src/core/onboard/render.ts:
  toOnboardRecommendation(step): RemediationStep → OnboardRecommendation
    Sets apply_policy per A8 tiered rules:
      - protected + job === extract-takes-from-pages → 'manual_only' (A12/A24)
      - protected + other → 'prompt_required'
      - non-protected → 'auto_apply'
  buildOnboardReport(plan, opts?): assembles the stable JSON envelope.
  renderHuman(report): string. Echoes the "Recommendation + WHY" framing
    the CEO + Eng + Codex reviews settled on; CLI shell prints to stdout.

Stable JSON envelope shape:
  schema_version: 1
  brain_id?: string
  recommendations: OnboardRecommendation[]
  summary: { total, auto_eligible, prompt_required, manual_only,
             est_total_usd }
  history?: Array<{ remediation_id, metric_name, metric_before,
                    metric_after, delta, applied_at }>

Library-shaped — no console.* / process.exit. T13 (onboard CLI shell)
calls these from the wrapping CLI. MCP run_onboard (T16) returns the
JSON envelope unmodified.

Typecheck clean.

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

* feat(onboard): gbrain onboard CLI shell (A1, T13)

NEW src/commands/onboard.ts (~180 LOC). Thin wrapper that composes:
  - T2 library (computeRemediationPlan + runRemediation)
  - T4 onboard checks (runAllOnboardChecks → extraRemediations)
  - T12 render layer (buildOnboardReport + renderHuman)

Three modes:
  --check    (default): print plan, no submission. Computes plan via
             T2 library with T4 check-derived extraRemediations.
             Renders human (default) or JSON envelope (--json).
  --auto:    submit auto_apply tier. Requires --max-usd N (cron-safety
             per A12 + A20 — refuses without explicit cap to avoid
             surprise spend).
  --auto --yes: also submit prompt_required tier.
  --history: dump last 50 migration_impact_log entries.

Library hooks wired into stderr (per CLI/library separation): onStepStart,
onStepEnd, onBudgetRefused, onBudgetExhausted, onNothingToDo,
onTargetUnreachable. Final JSON envelope (--json) or human summary
lands on stdout.

CLI dispatch: registered in src/cli.ts CLI_ONLY set + case dispatch
between 'takes' and 'founder'.

Typecheck clean. Manual smoke-test pending T20 E2E (DATABASE_URL gated).

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

* feat(onboard): init nudge + upgrade banner (A4, A18, A20, T14)

NEW src/core/onboard/init-nudge.ts exports two fail-open hooks:

runInitNudge(engine):
  Post-initSchema 5-query AbortSignal-bound parallel check against a
  3-second wallclock budget. Per A20: uses REAL cancellation via the
  T5 executeRaw signal extension — Promise.race against a timer was
  codex's #7 wrong shape. Postgres queries actually .cancel(); PGLite
  documented gap.
  Partial-results path: if some checks complete and the budget fires
  on others, prints what landed + a fallthrough hint pointing at
  `gbrain onboard --check` for the full picture.
  Per A18: fail-open — ANY throw is caught, logged to stderr, and
  suppressed so init returns successfully.
  Bypass: GBRAIN_NO_ONBOARD_NUDGE=1 short-circuits. Non-TTY default
  short-circuits too (CI/scripted callers see nothing).
  Nudge format: one-line summary of opportunities ("Brain has
  opportunities: 23000 stale chunks, link coverage 32%, 0 takes")
  + a 'gbrain onboard --check' nudge.

runUpgradeBanner(_engine):
  Lighter post-upgrade banner. Doesn't engine-query — just prints a
  one-line nudge that upgrades may surface new opportunities. Same
  fail-open posture.

Wired into:
  src/commands/init.ts:initPGLite (end-of-function, after reportModStatus)
  src/commands/init.ts:initPostgres (same)
  src/commands/upgrade.ts:runPostUpgrade (end-of-function, after
  postUpgradeReferenceSweep)

Each wire site uses dynamic import + try/catch so even an import
failure can't crash init/upgrade.

Typecheck clean.

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

* feat(autopilot): tick consults onboard recommendations (A5, A19, A22, T15)

Pre-fix: autopilot tick's per-source recommendation walk called
computeRecommendations(health, ctx) — doctor's hardcoded 5-category
planner. The 4 new onboard checks (embed_staleness,
entity_link_coverage, timeline_coverage, takes_count) had nowhere to
hook in, so even with takes.bootstrap_enabled flipped on, autopilot
never noticed 0 takes and never proposed bootstrap.

Post-fix: tick body now ALSO calls runAllOnboardChecks(engine) and
threads the result's RemediationStep[] into the T3-generalized third
arg of computeRecommendations. The planner merges onboard's extras
with the legacy hardcoded entries (hardcoded wins on id collision).

Per A19 fail-open: any throw in the onboard-checks path is caught,
logged to stderr, and suppressed. The legacy plan (without extras)
runs as before — autopilot can't crash from an onboard-check failure.

A22 (idempotency-key dedupe across concurrent manual + autopilot
runs): inherits from the existing computeRecommendations →
remediation.idempotency_key chain. T7-T9 handlers each get their
content-hash key from the makeRemediationStep factory; an autopilot
tick + a manual `gbrain onboard --auto` submitting the same step
in the same brain produce the SAME key, so queue.add(...) dedupes.

No behavior change for brains where all 4 onboard metrics already
look healthy (extras=[]; legacy plan unchanged).

Typecheck clean.

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

* feat(mcp): run_onboard op with run_protected_onboard scope binding (A7, T16, codex finding #5)

NEW MCP op `run_onboard`. Admin scope (NOT localOnly) so federated /
thin-client brain installs can probe brain health + submit auto-eligible
remediation handlers over OAuth-authenticated MCP.

Two-tier authorization per A7 + codex #5:
  - Admin scope: sufficient for mode='check' (read-only OnboardReport JSON)
    AND for submitting non-protected handlers in mode='auto'/'auto-with-prompt'.
  - run_protected_onboard scope (NEW, additive): MUST be granted in
    addition to admin for any PROTECTED_JOB_NAMES handler to fire
    (synthesize, patterns, consolidate, extract-takes-from-pages,
    contextual_reindex_per_chunk).

Without the new scope tier, an admin-scoped OAuth token would silently
bypass the same protected-name gate `submit_job` enforces at
operations.ts:2288. The codex finding #5 caught this: admin scope alone
was insufficient guard. Now the run_onboard op explicitly FILTERS
protected extras from the recommendation plan when the caller lacks
run_protected_onboard; filtered items appear in the response as
skipped_missing_scope[] so the caller knows what would have been
available with the right grants.

Modes:
  check               — read-only OnboardReport JSON envelope.
  auto                — submits auto_apply tier (plus prompt_required
                        when --yes/auto-with-prompt).
  auto-with-prompt    — adds prompt_required tier.

Both auto modes REQUIRE max_usd per A12 + A20 cron-safety (rejects
with invalid_params if missing).

Per A26 source-scope: future extension will scope plans by ctx.sourceId
/ ctx.auth.allowedSources. Today the recommendation planner is
brain-wide; the source-scope thread doesn't change correctness, just
optimization.

Per A19 fail-open: any error in runAllOnboardChecks during plan-build
caught + suppressed; the plan still returns with extras=[] rather than
crashing the op.

Typecheck clean.

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

* chore(verify): add check-source-scope-onboard lint (A26, T17)

NEW scripts/check-source-scope-onboard.sh. Grep guard for SQL sites in
onboard surfaces (src/core/onboard/, src/commands/onboard.ts) that
touch source_id-bearing tables (pages, content_chunks, takes, links,
timeline_entries) WITHOUT either:
  (a) source_id / sourceIds in the WHERE clause, OR
  (b) the opt-out marker `sourcescope:brain-wide` within 4 lines above
      the SQL.

File-level opt-out: `sourcescope:file-brain-wide` in the file header
(first 30 lines) treats every SQL site in that file as intentionally
brain-wide. Used by onboard/checks.ts, onboard/impact-capture.ts, and
commands/onboard.ts because the onboard CHECKS are explicitly brain-wide
aggregates (orphan_count, stale_count, link_coverage are reported
across all sources by design).

Wired into bun run verify (23 checks total now, all green).

Without this gate, any future onboard SQL touching per-source data
without source-scoping would silently leak rows across sources —
exactly the class of bug v0.34.1's P0 seal closed at the engine layer.
The lint adds an explicit forcing function for new code in the onboard
surface.

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

* docs(install): onboard surface agent prescription (D13, T18)

Adds a v0.42.0+ section to INSTALL_FOR_AGENTS.md describing:
  - First-connect probe: gbrain onboard --check --json
  - Post-upgrade re-probe (after gbrain upgrade)
  - Unattended remediation: gbrain onboard --auto --max-usd 5
  - MCP run_onboard op for federated/thin-client installs
  - run_protected_onboard scope requirement for LLM-bearing handlers
  - Two-gate consent for takes-bootstrap (takes.bootstrap_enabled + --yes)
  - GBRAIN_NO_ONBOARD_NUDGE=1 bypass for CI

Per D13: agents should run --check on first connect AND after every
upgrade as a hygiene step. The autopilot path makes this auto-improve
on a 24h cycle; the explicit agent probe surfaces opportunities
immediately on connect rather than waiting for the next autopilot tick.

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

* test(e2e): hermetic onboard surface contracts (T20)

NEW test/e2e/onboard-full-flow.test.ts. 13 hermetic PGLite cases
(no DATABASE_URL needed) covering the key onboard contracts:

  captureMetric — all 5 metrics return expected values on empty brain
    (0 for counts; 1 for coverage = vacuous truth).

  runAllOnboardChecks — returns exactly 4 results with correct names;
    empty brain shows stale/link/timeline ok BUT takes_count warns
    (0 takes); 0 remediations emitted because takes.bootstrap_enabled
    defaults to false per A12 two-gate consent.

  computeRemediationPlan — extras (T3 generalization) thread through to
    plan.plan output; stable schema_version: 2 envelope.

  buildOnboardReport — stable schema_version: 1 envelope with the right
    summary fields populated.

  toOnboardRecommendation tier policy (A8):
    - non-protected job → auto_apply
    - extract-takes-from-pages → manual_only (A12 + A24)
    - other protected jobs (synthesize, patterns, ...) → prompt_required

Full DATABASE_URL-gated end-to-end (real Postgres, actual extractions
through Minion handlers) deferred to v0.42.1 once the per-handler test
seam lands; the hermetic suite covers the data-shape contracts that
matter for downstream consumers binding to the JSON envelopes.

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

* v0.42.0.0 gbrain onboard mega PR — activation surface (closes #1383, completes #1409)

VERSION + package.json bumped to 0.42.0.0. CHANGELOG with full ELI10 lead
+ "What you can do that you couldn't before" itemized list + "To take
advantage of v0.42.0.0" upgrade steps per CLAUDE.md voice rules.

TODOS.md: 9 follow-up items filed (TODO-A through TODO-I) for the
v0.42.1+ wave: pack-aware linkable types, LLM-disambiguation NER,
onboard --explain, live-brain impact measurement, 100+-case takes
classifier eval, admin SPA UI, full DATABASE_URL E2E, minion_jobs
client_id schema column, thin-client doctor-remote parity.

llms-full.txt regenerated per CLAUDE.md rule (every CHANGELOG edit
followed by bun run build:llms in the same commit).

23/23 verify checks pass.

Full implementation across 21 commits on this branch (T0-T21):
  T0  merge master
  T1  schema migrations v98/v99/v100
  T2  extract doctor remediation library
  T3  generalize computeRecommendations
  T4  4 new doctor checks
  T5  engine API: listStaleChunks orderBy + executeRaw AbortSignal
  T6  embed --batch-size / --priority recent / --catch-up
  T7  NER extraction + extract-ner handler
  T8  timeline-from-meetings + extract-timeline-from-meetings handler
  T9  takes-bootstrap + extract-takes-from-pages handler
  T10 recordMinionJobSpend primitive
  T11 impact capture module + writeImpactLogRow
  T12 onboard render layer (types + render)
  T13 gbrain onboard CLI shell
  T14 init nudge + upgrade banner
  T15 autopilot tick consults onboard
  T16 MCP run_onboard + run_protected_onboard scope
  T17 check-source-scope-onboard lint
  T18 INSTALL_FOR_AGENTS.md agent prescription
  T20 hermetic PGLite E2E (13 cases)
  T21 ship (this commit)

Reviews: CEO + Eng + Codex on plan
~/.claude/plans/system-instruction-you-are-working-lively-hollerith.md.
27 A-decisions locked; 18 codex findings absorbed.

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

* fix(ci): connection-resilience regex + doctor warn-not-fail + v0.41.18.0

Two CI fixes from PR #1521 + version renumber per user request.

Why fix #1 (connection-resilience.test.ts): T5/A20 extended
PostgresEngine.executeRaw signature to accept an optional
`opts?: { signal?: AbortSignal }` 3rd arg and rewrote the body as
multi-line. The regression test's regex was anchored to the legacy
single-line `(sql: string, params?: unknown[])` shape and the
assertions banned `try {` / `catch` (which T5 legitimately added for
AbortSignal cancellation swallow, NOT for retry). Updated regex to
tolerate both shapes; replaced the wrong `not.toContain('conn.unsafe(
sql, params')` assertion (which incorrectly flagged the legitimate
single call) with a count assertion: `conn.unsafe(` must appear
exactly ONCE in the body. Preserves the original D3 intent (no
per-call retry — recovery is supervisor-driven via reconnect()) while
accepting the new try/catch shape that swallows AbortSignal aborts.

Why fix #2 (src/core/onboard/checks.ts): Three of the four new
onboard doctor checks (entity_link_coverage, timeline_coverage,
embed_staleness) emitted `status = 'fail'` on healthy DBs that simply
hadn't run extractions yet. This flipped `gbrain doctor`'s exit code
to non-zero on freshly initialized brains, breaking
test/e2e/mechanical.test.ts:1280 ("gbrain doctor exits 0 on healthy
DB"). Downgraded all three to `status = 'warn'` — these are
remediation opportunities, not assertion failures. Doctor exit
codes are reserved for actual failures; remediation surfaces use
warn-level signaling so they can be picked up by `--remediate`
without polluting the exit code.

Why fix #3 (version renumber 0.42.0.0 → 0.41.18.0): Per user
directive, this wave ships as v0.41.18.0 rather than v0.42.0.0.
Master is at 0.41.16.0; 0.41.17.0 is reserved for an in-flight
wave. Renamed every reference my branch added (54 files touched):
VERSION, package.json, CHANGELOG.md header, TODOS.md, plus inline
version-stamp comments across src/, test/, and scripts/. Preserved
13 files with PRE-EXISTING `v0.42.0.0` references on master (from
earlier waves originally planned for v0.42 that landed at v0.41.x —
those stay as historical record). Verified via per-file diff against
origin/master: every renamed reference is one I added in this branch.

Audit trio aligned: VERSION=0.41.18.0, package.json=0.41.18.0,
CHANGELOG topmost entry=[0.41.18.0]. llms-full.txt regenerated to
match CLAUDE.md updates.

Bisect contract: this commit fixes CI test failures from PR #1521's
landing. Typecheck clean; connection-resilience suite 26/26 pass.

Refs A20 (executeRaw AbortSignal), A16 (4 new onboard checks),
codex #1 (master collision avoidance via renumber).

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 18:59:17 -07:00
8ab733471b v0.41.17.0 feat: --workers N on every bulk command + facts dim doctor parity (#1519)
* feat(worker-pool): shared sliding pool + bounded semaphore + PGLite-clamp wrapper

T1 + T2 of the v0.41.16.0 workers cathedral. New src/core/worker-pool.ts is
the canonical primitive every --workers N bulk command in this wave (and
future bulk commands) builds on. Atomic-claim invariant enforced by
scripts/check-worker-pool-atomicity.sh (wired into bun run verify).
BudgetExhausted bypass + AbortSignal composition baked into the helper so
budget caps are a structural ceiling under concurrency, not a per-caller
convention.

The new resolveWorkersWithClamp wrapper composes existing autoConcurrency
with PGLite-clamp + per-(command, requested) stderr dedup. Deliberately
NOT a modification to shared autoConcurrency (silent today, used by sync
+ import); embed.ts keeps GBRAIN_EMBED_CONCURRENCY || 20 default per
codex #13.

23 + 12 + 9 = 44 hermetic tests pin every contract.

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

* test: structural + dim-check regression suites for v0.41.16.0 wave

- test/embed-helper-migration.test.ts (T3): asserts embed.ts's two
  sliding-pool sites are migrated to runSlidingPool, pre-migration
  shapes (let nextIdx = 0, Promise.all(Array.from(...))) are gone,
  GBRAIN_EMBED_CONCURRENCY || 20 default preserved, failureLabel
  threads page.slug. Per codex #16/#17 these are invariant assertions,
  not byte-equality on progress event ORDERING.
- test/embedding-dim-check-facts.test.ts (T6): readFactsEmbeddingDim
  covers vector(N) + halfvec(N), halfvec-before-vector regex ordering
  pinned (codex #19), buildFactsAlterRecipe emits DROP INDEX + ALTER
  USING + CREATE INDEX (codex #18, not bare REINDEX),
  FactsEmbeddingDimMismatchError tagged class shape,
  assertFactsEmbeddingDimMatchesConfig PGLite skip + Postgres absent-
  column skip, doctor check + insert-cast wiring assertions.
- test/extract-conversation-facts-workers.test.ts (T5): helper
  exports (extractConversationFactsLockId, PER_PAGE_LOCK_TTL_MINUTES),
  structural wiring (runSlidingPool, resolveWorkersWithClamp,
  withRefreshingLock, LockUnavailableError, delete-orphans-first
  before segment loop, preflight before pool, exit 3 when lock_skipped
  > 0), Minion handler round-trip.
- test/extract-workers.test.ts (T7): --workers wiring on all 3 inner
  fs-walk loops (extractForSlugs, extractLinksFromDir,
  extractTimelineFromDir) + CLI parse + opts threading through
  runExtractCore.

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

* chore: rebump v0.41.16.0 → v0.41.17.0 (queue collision with PR #1510)

PR #1510 (garrytan/dynamic-regex-conversation-formats) claimed v0.41.16.0
on master in parallel. Advancing this wave to v0.41.17.0 so both can land
cleanly. Pure mechanical version bump:

- VERSION + package.json → 0.41.17.0
- CHANGELOG.md header + "To take advantage of v0.41.17.0" block
- TODOS.md section header + v0.41.18+ forward references
- CLAUDE.md inline version tags
- Regenerated llms-full.txt / llms.txt

No code changes. The actual workers cathedral feature set is unchanged
from the two prior commits in this branch.

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

* fix(test): search-image-column probes column dim at runtime

CI shard 5 failed on `searchVector column routing (v0.27.1)` with:
  error: expected 1280 dimensions, not 1536

The test had a hardcoded `fakeText1536` helper that seeded chunks at
1536-d vectors. Master's default embedding model switched from OpenAI
text-embedding-3-large (1536) to ZeroEntropy zembed-1 (1280) so a fresh
PGLite brain on CI now sizes content_chunks.embedding at 1280; the
test's 1536-d INSERT trips pgvector's CheckExpectedDim.

Fix: probe `content_chunks.embedding` width via
`readContentChunksEmbeddingDim(engine)` in `beforeAll`, store in
`TEXT_DIM`, and build `fakeTextDefault(seed)` at that width. The test
now passes regardless of which default ships (the model has flipped
twice and may flip again). Local dev (1536 from older config) and CI
fresh-install (1280 from new default) both pass.

Image-side vectors stay at 1024 (matches Voyage multimodal-3 + the
column's fixed width on the image side).

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

* fix(test): bump PGLite hook timeout for shard-4 deep-process files

facts-anti-loop.test.ts and ingest-capture.test.ts were timing out in CI
shard 4 with "beforeEach/afterEach hook timed out" after the v0.41.16.0
master merge brought migration count to 99. When these files run deep in
a shard process that has already created ~20 PGLite engines, the WASM
cold-start + 95-migration replay legitimately exceeds bun's 5s default
hook timeout (observed 5.6s and 7.3s locally when reproducing).

Bun's --timeout=60000 from scripts/test-shard.sh covers TEST timeouts
but NOT hook timeouts; those default to 5s and must be set per-hook via
the optional 2nd arg to beforeAll/afterAll.

Reproduced locally by running the first 21 shard-4 files via
  head -21 /tmp/shard4-list.txt | xargs bun test
  → 179 pass, 2 fail (both with hook-timeout error)

After fix:
  → 198 pass, 0 fail (the 4 anti-loop + 15 ingest-capture tests recover)

Full shard 4 with fix:  955 pass, 0 fail.
Full shard 5 with fix:  1261 pass, 0 fail.

Also added a defensive diagnostic to the two put_page tests: if
facts_backstop is missing in the response payload, throw with the full
payload + isError so future failures surface the actual handler error
instead of a bare "expected {...} got undefined" assertion. No-op when
the test passes.

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 18:29:03 -07:00
f702ec053b v0.41.16.0 feat: conversation parser cathedral + progressive-batch primitive (closes #1461) (#1510)
* v0.41.15.0 feat: conversation parser cathedral + progressive-batch primitive (closes #1461)

Replaces PR #1461's single-format Telegram regex with a 12-pattern
built-in registry covering iMessage/Slack, Telegram (×2), Discord
(×2), WhatsApp (×2 locales), Signal, Matrix/Element, IRC (×2), Teams.
Each pattern is hand-vetted from public format docs (signal-cli,
DiscordChatExporter, Telegram Desktop, WhatsApp export docs, Element
matrix-archive, irssi/weechat defaults); module-load validation runs
test_positive[] + test_negative[] for every pattern at startup so a
typo makes gbrain refuse to start.

PR #1461 contributor's BRACKET_TIME_RX + cleanSpeaker survive verbatim
as the `telegram-bracket` built-in pattern + DEFAULT_SPEAKER_CLEAN
export. All 33 of their test cases pass against the new orchestrator.

Three layers per page (orchestrator chooses):
  1. Built-in pattern registry (zero-cost, deterministic)
  2. User-declared simple_pattern via config (deferred to v0.42+)
  3. Opt-IN LLM polish + fallback (privacy-first; chat content goes
     to Anthropic only when user explicitly enables)

D18 priority scoring picks the highest-match-rate pattern across the
first 10 lines (not first-wins) so overlapping formats don't silently
mis-route. D5 multi_line per-pattern + D11 quick_reject prefix screen
+ D19 timezone_policy per-pattern complete the registry shape.

Companion: src/core/progressive-batch/ primitive (rule of three
satisfied across 12+ ad-hoc cost-prompt sites). Wintermute-inspired
ramp shape (trial 10 → 100 → 500 → full with verification at each
stage), productionized with verifier+policy injection (callers
describe HOW TO MEASURE SUCCESS, not WHEN TO WAIT FOR CTRL-C). D3
fail-closed budget gate: null tracker + null Policy.maxCostUsd →
abort_cost_cap reason='no_budget_safety_net'. D20 discriminated
Verifier union (output_count | idempotent_mutation | noop).
extract-conversation-facts is the one proven consumer in v0.41.15.0;
9-site retrofit deferred to v0.41.16.0+ per TODOS.md.

Codex outside-voice review absorbed 8 substantive findings:
  - Privacy posture (LLM polish/fallback flipped to opt-IN)
  - ReDoS theater (dropped arbitrary user regex; v0.42+ uses RE2)
  - LLM-inferred-regex persistence as silent-corruption machine
  - Pattern priority scoring across first 10 lines
  - Timezone policy on every PatternEntry
  - Verifier shape discriminated union
  - Behavior parity for sites that "jumped straight to full"
  - Real-corpus-redacted fixture gap (v0.42+ TODO)

CI gates:
  - bun run check:conversation-parser (13 fixtures, --no-llm, deterministic)
  - bun run check:fixture-privacy (banned-token grep)

Doctor surfaces 3 new checks: conversation_format_coverage,
progressive_batch_audit_health, conversation_parser_probe_health.

Tests: 198/198 across primitive + parser + LLM + nightly probe + eval
CLI + debug CLI + doctor checks + migration v97 round-trip + E2E
parser ↔ engine integration. Real bug caught + fixed during gap audit:
IdempotentMutationVerifier was comparing absolute mutated-count vs
per-stage expected (failed silently on stage 2+); now uses per-stage
delta semantics matching OutputCountVerifier.

Schema migration v97: conversation_parser_llm_cache table with
(content_sha256, model_id, call_shape) composite key. NO
inferred_patterns table (D17: silent-corruption machine).

Plan + 23 decisions + codex outside-voice absorption at
~/.claude/plans/system-instruction-you-are-working-cuddly-hollerith.md.

Co-Authored-By: garrytan-agents (PR #1461) <noreply@github.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(check-privacy): allowlist scripts/check-fixture-privacy.sh

The new sibling privacy guard literally names the banned tokens in its
BANNED_TOKENS array — same meta-exception that check-privacy.sh itself
gets. Without this allowlist entry, bun run verify rejects the file
post-merge because the banned name appears in the rule-definition script.

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

* chore: renumber v0.41.15.0 → v0.41.16.0 (queue drift)

Mechanical rename across all surfaces: VERSION, package.json,
CHANGELOG (header + body refs), CLAUDE.md, TODOS.md, src/core/
migrate.ts (migration v98 comment), all src/core/conversation-parser/*
and src/core/progressive-batch/* file headers, all test/ headers,
scripts/check-privacy.sh allowlist comment, llms-full.txt regenerated.

Audit clean: VERSION + package.json + CHANGELOG header all show
0.41.16.0. verify 24/24, touched tests 179/179.

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

---------

Co-authored-by: garrytan-agents (PR #1461) <noreply@github.com>
Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-26 17:31:48 -07:00
32f8be96c2 v0.41.14.0 fix(#1451): close RESOLVER.md drift bug class structurally (#1458)
* feat(core): loadSkillTriggerIndex shared primitive (closes #1451 drift class)

Single loader that unions per-skill SKILL.md frontmatter triggers: with
curated RESOLVER.md / AGENTS.md rows. UNION semantics — explicit
RESOLVER.md rows ADD to frontmatter triggers for the same skill (don't
replace). Dedup keyed on (skillPath, normalized trigger string) so case
or whitespace drift between the two surfaces collapses to one entry.

This is the structural foundation for #1451: pre-fix, gbrain skills
declared triggers in two places (per-skill frontmatter and a curated
RESOLVER.md table) that could silently drift. Three consumers
(checkResolvable, routing-eval CLI, mounts-cache.composeResolvers) each
built their own resolver index from RESOLVER.md only, so fixing
frontmatter would have closed doctor's warning without closing the
other two surfaces. This primitive becomes the single join point for
all three; consumers are wired in the next commit.

Tests: 18 hermetic cases pinning frontmatter auto-registration,
RESOLVER.md/AGENTS.md merge, case-insensitive dedupe, OpenClaw
workspace-root layout (../AGENTS.md), graceful skip of conventions /
deprecated skills / non-directory entries / missing skillsDir, plus
synthesis round-trip and findPrimaryResolverPath.

Plan: ~/.claude/plans/system-instruction-you-are-working-tidy-storm.md

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

* refactor: wire 3 consumers through loadSkillTriggerIndex (#1451)

Replace the three independent resolver-content loaders with calls to
the v0.41.11 shared primitive so frontmatter triggers propagate to
every dispatch surface, not just doctor.

Before: checkResolvable, runRoutingEvalCli, and mounts-cache each
walked RESOLVER.md / AGENTS.md files separately. Adding frontmatter
triggers to one consumer (e.g. checkResolvable) wouldn't have reached
the routing-eval CLI or cross-brain composed dispatchers — the same
drift bug class as #1451 in cross-consumer form. Codex caught this in
plan-eng-review.

After: all three consumers fold through loadSkillTriggerIndex. UNION
semantics across both surfaces means new skills with frontmatter
triggers are reachable everywhere without editing RESOLVER.md.

Also updates:
- check-resolvable action text on routing_miss to point at the
  canonical surface (SKILL.md frontmatter triggers) first, with
  RESOLVER.md row as secondary.
- test/resolver-merge.test.ts to test BOTH the legacy
  RESOLVER.md-only authority path (skills with no frontmatter
  triggers) AND the new auto-registration path (skills reachable via
  frontmatter alone, no RESOLVER.md needed).
- 3 routing-eval.jsonl fixtures (voice-note-ingest, brain-taxonomist,
  strategic-reading) gain `ambiguous_with` declarations for skill
  overlaps that auto-registration newly exposes. These overlaps are
  legitimate (voice-note vs idea-ingest on audio notes,
  brain-taxonomist vs repo-architecture on filing, strategic-reading
  vs idea-ingest on reading-through-a-lens) — the agent picks based
  on context.

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

* fix(#1451): broaden skillpack-harvest triggers + negative fixtures + tighten gate

Closes the 7 residual routing_miss warnings on skillpack-harvest that
gbrain doctor reported on every fresh install (resolver_health: WARN,
~5 health-score points).

Three changes:

1. Broaden skills/skillpack-harvest/SKILL.md frontmatter triggers from
   5 narrow to 10 realistic phrasings. Each new trigger is a
   contiguous substring of one of the 7 shipped routing-eval.jsonl
   intents (per kylma-code's design in PR #1331; moved from
   RESOLVER.md to frontmatter under the v0.41.11 frontmatter-
   authoritative contract). Existing RESOLVER.md row stays for
   human-readability of the dispatcher map.

2. Add 4 negative-fixture cases to skills/skillpack-harvest/
   routing-eval.jsonl with expected_skill=null to defend against
   false positives the broader triggers might introduce
   ("publish this report to the team", "promote my role on
   LinkedIn", "bundle these screenshots into a deck", "lift weights
   at the gym"). Two candidate negatives ("save this report as PDF",
   "share this article with the channel") were excluded — they trip
   idea-ingest's existing "save this"/"share" triggers, a real
   overlap but a separate v0.42+ concern.

3. Tighten test/check-resolvable.test.ts's "repo skills/ pass cleanly"
   assertion: the v0.25.1 carve-out that allowed routing_miss as
   informational is removed. The contract is back to zero errors AND
   zero warnings — the CI gate (next commit) enforces this for PRs
   so future drift fails the build instead of degrading user-install
   resolver_health silently.

Co-Authored-By: kylma-code <noreply@github.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* fix(cli): register reindex in CLI_ONLY so --help works (closes part of #1354)

Pre-fix: src/cli.ts had a `case 'reindex':` handler at line 1334 that
dispatched to reindex-multimodal or reindex.ts based on flags, but
'reindex' was missing from the CLI_ONLY Set at line 38. The dispatcher
rejected the command with "Unknown command: reindex" before the handler
ever ran.

Post-fix: 'reindex' is in CLI_ONLY (recognized as a registered command).
NOT added to CLI_ONLY_SELF_HELP — the handler doesn't have its own
--help branch, so the dispatcher's generic printCliOnlyHelp() shows
"gbrain reindex - run gbrain --help for the full command list."
Polishing this to per-flag help text (--multimodal, --markdown, --code)
is a follow-up TODO.

Regression test in test/cli.test.ts asserts `'reindex'` is in the
CLI_ONLY Set source string. Mirrors the existing pattern for
'reinit-pglite' in test/v0_37_fix_wave.serial.test.ts:284 and
'book-mirror' in test/book-mirror.test.ts:73.

Cherry-picked from lost9999's PR #1354 (which bundled this fix with
their fixture-rewrite approach to #1451 — the routing-eval half of
that PR was superseded by kylma-code's trigger-broadening direction
in #1331, which we took structurally in the previous commits).

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

* chore(ci): wire check:resolver into bun run verify

Adds `bun run check:resolver` (= `bun src/cli.ts check-resolvable
--strict --skills-dir skills/`) to package.json scripts and registers
it in scripts/run-verify-parallel.sh's CHECKS array.

This gates PR CI on resolver health: any future drift between a
skill's frontmatter triggers and its routing-eval.jsonl fixtures
fails the build, instead of silently degrading the resolver_health
score on user installs after merge. The --strict flag exits non-zero
on warnings (not just errors), so routing_miss / routing_ambiguous /
routing_false_positive all block.

Closes the CI half of #1451's structural fix: doctor catches drift
at runtime, this gate catches drift at PR time.

Local pre-flight: `bun run check:resolver`.

Codex finding #9 from plan review: scripts/run-verify-parallel.sh
invokes entries as `bun run <script-name>`, not raw shell. The
package.json script name + CHECKS-array entry is the correct shape.

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

* fix(test): update CLI unreachable test for v0.41.11 contract change

The prior fixture used `triggers: ['alpha']` + `inResolver: false` to
simulate an unreachable skill. Under v0.41.11's structural fix,
frontmatter triggers auto-register the skill independently of
RESOLVER.md, so this skill is reachable now — the assertion
`errors.length > 0` failed.

Drop the `triggers:` array from the fixture so the skill is genuinely
unreachable (neither frontmatter nor RESOLVER.md row), preserving the
test's regression-guard intent: doctor/check-resolvable still exits 1
when a manifest skill is truly unreachable.

Caught by the full unit test suite after the merge.

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

* docs: CLAUDE.md Key Files note for skill-trigger-index + regen llms.txt

Document the v0.41.11 shared primitive (loadSkillTriggerIndex) in
the Key Files section so future contributors find it before they
reach for parseResolverEntries directly. Notes the 3 consumers
(checkResolvable, runRoutingEvalCli, mounts-cache.composeResolvers),
the UNION semantics, skip rules, parseSkillFrontmatter dependency,
test coverage, and the CI gate wiring.

Regenerated llms.txt + llms-full.txt per the CLAUDE.md edit rule.

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

* v0.41.14.0 fix(#1451): close RESOLVER.md drift bug class structurally

Frontmatter triggers + RESOLVER.md / AGENTS.md rows now union into one
unified index via the new loadSkillTriggerIndex primitive, consumed by
all three dispatch surfaces (checkResolvable, routing-eval CLI,
mounts-cache.composeResolvers). Closes the 7 residual routing_miss
warnings #1451 reported on every fresh install, and the drift bug class
that produced them.

Highlights:
- New shared primitive src/core/skill-trigger-index.ts (252 lines + 361
  lines of tests across 18 cases). UNION semantics, case-insensitive
  dedupe keyed on (skillPath, normalized trigger).
- Three consumers wired through the primitive — fixing frontmatter
  triggers for doctor now also fixes routing-eval CLI and
  cross-brain mounted dispatch (codex outside-voice catch).
- skillpack-harvest frontmatter broadened from 5 to 10 triggers per
  kylma-code's design in #1331, plus 4 negative-fixture cases for
  false-positive defense.
- reindex CLI added to CLI_ONLY set so `gbrain reindex --help` works
  instead of "Unknown command: reindex" (lost9999's #1354 hunk).
- check:resolver wired into bun run verify CI gate so future drift
  fails PR CI instead of silently degrading user-install
  resolver_health.
- check-resolvable's repo skills/ test tightened from "warn-tolerant"
  to "zero errors AND zero warnings" — the carve-out was a stop-gap
  pre-structural-fix.

Plan + 5 decisions + codex outside-voice recalibration captured at
~/.claude/plans/system-instruction-you-are-working-tidy-storm.md.

Co-Authored-By: kylma-code <noreply@github.com>
Co-Authored-By: lost9999 <noreply@github.com>
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>

* docs: update project documentation for v0.41.14.0

- CLAUDE.md: tag skill-trigger-index entry with correct shipped version
  (v0.41.14.0, closes #1451) instead of the stale v0.41.11 draft tag.
- CONTRIBUTING.md: list the new `check:resolver` gate in the `bun run
  verify` chain so contributors know to expect resolver-drift failures
  in PR CI.
- llms-full.txt: regenerated from updated CLAUDE.md (mandatory per
  CLAUDE.md's auto-derived files rule; CI shard 1 fails the build
  otherwise).

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

---------

Co-authored-by: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Co-authored-by: kylma-code <noreply@github.com>
2026-05-25 20:00:03 -07:00
3a2605e9a0 v0.41.6.0 feat(ci): CI test speedup — 23min → ~9min via matrix 4→6 + weight-aware sharding + auto SHA cache + parallel verify (#1444)
* feat(ci): scripts/run-verify-parallel.sh — parallel verify dispatcher

Fans out the 21 pre-test grep guards via & + wait, captures per-check
exit codes in a tempdir, aggregates failures with named check + log
tail to stderr on miss. Wallclock 27s sequential → 13s parallel
locally (2x). Bigger CI win is shard 1 deload (workflow restructure
in a later commit).

Pinned by test/scripts/run-verify-parallel.test.ts (6 cases: CLI
contract + synthetic dispatcher failure-surfacing).

* feat(ci): weight-aware LPT bin-packer + auto SHA cache hash

scripts/sharding.ts (NEW) — pure TypeScript LPT bin-packer. Sort
weights desc, assign each file to the shard with current minimum total.
Worst-case makespan within 4/3 of optimal, O(n log n). Missing weights
fall back to corpus median (not 0). New test file → ships immediately
without regenerating weights. Pinned by test/scripts/sharding.test.ts
(23 cases).

scripts/mine-shard-weights.ts (NEW) — scrapes per-file timing from
gh run view --log via timestamp delta between ##[group]test/foo.test.ts:
headers within a shard. Three input modes: --run <ID>, --from-file
<PATH>, stdin. Stable JSON output (sorted keys). Initial weights mined
from run 26398061007. Pinned by test/scripts/mine-shard-weights.test.ts
(15 cases).

scripts/ci-cache-hash.sh (NEW) — deterministic 16-char sha256 over
git ls-files -s minus deny-list (CHANGELOG/TODOS/README/LICENSE/
docs/**/*.md). CLAUDE.md, AGENTS.md, skills/**/* deliberately
INCLUDED (8+ test files read them; deny-listing would create
false-pass holes). ~40ms on 1891 files. Pinned by
test/scripts/ci-cache-hash.test.ts (24 cases: 8 CRITICAL false-pass
guards + 7 SAFE deny-list invariants + 9 edge cases).

scripts/test-weights.json (NEW) — 712 weights. Total 3306s observed
runtime; median 30ms; max 6 min outlier.

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

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

---------

Co-authored-by: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-25 13:15:11 -07:00