Repository navigation
feat(import): standalone importer seeding a brain from envelope-v0 chat-history files - #3549
Conversation
…at-history files One Markdown page per conversation from an envelope-v0 file (format spec: github.com/memvelope/memvelope), written into a directory gbrain sync ingests. Zero dependencies, deterministic, no network; does not call gbrain. Filenames are date + conversation id (collision-proof natural key; duplicate ids overwrite their own file and warn on stderr). Frontmatter carries type: conversation, source provider, conversation id, and origin. Bodies keep message-id citations per speaker turn. Ships as script + test + fixture only; usage and verification steps live in the script header.
|
Taking it. You did what was asked on #2568 — neutral spec link, standalone script, zero dependencies, no network, no change to any existing path — and the maintainer's call is to accept it. Two small things worth a follow-up PR whenever convenient. Neither blocks the merge:
Both are one-liners. Happy to take them together in a small follow-up. For the record on why this went to the maintainer rather than being auto-closed: gbrain's bar for new surface is deliberately high and third-party-format importers carry a permanent maintenance obligation to a spec someone else controls. But you were explicitly invited to resubmit, and closing invited work is a cost that belongs to the maintainer to spend, not to a triage pass. That's now happened twice in two days, so it's worth the project settling a standing rule for invited resubmissions. |
|
Appreciate you taking it, and genuinely appreciate the reasoning you put in the record about invited resubmissions - that was more than the PR needed, and I noticed. I'd support a standing rule whichever way it lands. Both findings are right and I'm taking both, so please don't spend your own time on them. On the second one, the reason it's worth fixing rather than tidying: Root cause on both is the same shape of miss. PR shortly. Happy to keep taking review notes at this level of detail - it has made the script better both times, and I'd like to keep contributing here. |
…#3600) Review follow-up to #3549. An envelope is a third-party file, so interpolating source_provider raw let a provider string carrying a newline close the scalar and inject arbitrary frontmatter keys. title: on the line above was already quoted; source: now matches it. memvelope_conversation_id emitted the literal string undefined when a conversation carried no id, which asserts a value rather than reporting absence: every id-less conversation claims the same id, so anything grouping or deduping on that key merges unrelated pages. The key is now omitted. Filename and frontmatter read one hasId predicate so they cannot disagree about whether an id exists. Tests add both cases; the injection case parses emitted frontmatter with js-yaml rather than substring-matching it.
* fix(files): display zero-byte file sizes (#3608) * fix(import): quote frontmatter values and omit absent conversation id (#3600) Review follow-up to #3549. An envelope is a third-party file, so interpolating source_provider raw let a provider string carrying a newline close the scalar and inject arbitrary frontmatter keys. title: on the line above was already quoted; source: now matches it. memvelope_conversation_id emitted the literal string undefined when a conversation carried no id, which asserts a value rather than reporting absence: every id-less conversation claims the same id, so anything grouping or deduping on that key merges unrelated pages. The key is now omitted. Filename and frontmatter read one hasId predicate so they cannot disagree about whether an id exists. Tests add both cases; the injection case parses emitted frontmatter with js-yaml rather than substring-matching it. * fix HTTP server lifecycle retention (#3599) * fix admin SSE handshake through reverse proxies (#3598) * fix(engine): enforce static engine-live import boundaries (#3596) * docs: design engine dynamic-import reconciliation Co-Authored-By: Claude <[email protected]> * fix(engine): reconcile dynamic import hardening Co-Authored-By: Claude <[email protected]> * test(engine): guard dynamic import policy * docs: plan engine dynamic-import reconciliation Record the approved TDD sequence for selective engine-path hardening, repository guard wiring, documentation, and local verification. Preserve the no-version-bump and no-publication boundaries for the remaining work. Co-Authored-By: Claude <[email protected]> * docs(engine): record static import invariant * fix(engine): parse block comments in import guard Co-Authored-By: Claude <[email protected]> * fix(engine): parse dynamic imports with TypeScript Co-Authored-By: Claude <[email protected]> * fix(engine): close import guard bypasses Co-Authored-By: Claude <[email protected]> * fix(engine): close parser guard edge cases Co-Authored-By: Claude <[email protected]> * fix(engine): aggregate parser diagnostics Co-Authored-By: Claude <[email protected]> * fix(engine): bound dynamic import marker directive Require the line-level opt-out marker to be standalone inside real comment trivia so negated or incidental longer tokens cannot authorize an import. Preserve the existing general marked-line contract and pin it with focused regression coverage. Co-Authored-By: Claude <[email protected]> * fix(engine): close Unicode marker boundary bypasses Treat Unicode identifier continuations as marker-token characters and inspect adjacent text by code point so supplementary-plane characters cannot turn longer comment tokens into approvals.\n\nCo-Authored-By: Claude <[email protected]> --------- Co-authored-by: Claude <[email protected]> * fix(test): repair typecheck failures in admin-sse and lifecycle tests (#3598, #3599) (#3610) Export AdminSseResponse, HttpServerLifecycle, and SignalSource from serve-http.ts so test fakes can reference them. Cast structural fakes through `as unknown as T` where the fake return types (EventEmitter, plain object) cannot structurally match the full Node/Express originals. Co-authored-by: Garry Tan <[email protected]> Co-authored-by: Mastra Code (anthropic/claude-opus-4-6) <[email protected]> * fix(reindex-frontmatter): reuse the connected engine instead of self-deadlocking on the PGLite lock (#1963) (#3558) Adversarial review: survived two independent refuters — the only PR of 32 reviewed this way to do so. The bug: `gbrain reindex-frontmatter` and `gbrain backfill <kind>` were 100% dead on PGLite. cli.ts takes the data-dir lock, the command modules built a second engine on the same dir, and acquireLock never reaps a live PID — 30s timeout, exit 1, with the error naming the waiting process itself as the holder. Reproduced on the parent commit at 33.2s; passes in 5.0s with the fix. Root-cause fix at the dispatch layer, not a softening of the lock, and the sibling census confirmed these were the only two affected callers. Postgres path verified before merge (it was the review's one open gap, since the bug is PGLite-only and all verification had gone there while the change itself is connection-teardown ownership). Against real Postgres 16 + pgvector: reindex-frontmatter and all three registered backfills exit 0 with zero residual connections, zero advisory locks, and zero cycle-lock rows — byte-identical output and identical teardown to master on the same database, confirming the change is behavior-neutral there. Merged tree re-verified after rebase: typecheck clean, pglite-lock + reindex-frontmatter 16 pass, llms bundle fresh, 23/23 CI green. * fix(search): fold the FTS configuration name into knobs_hash — stop stale rows surviving a reindex-search-vector language switch (#3677) Adversarial review: survived a hostile reviewer plus two independent refuters, each told to assume the PR was broken and to default to refuting when uncertain. `GBRAIN_FTS_LANGUAGE` was absent from the query-cache key, so a language switch served stale pre-switch rows. The hash now folds it (v14→15) with all five pin sites updated; reverting the fix fails 4 of 15 tests at the exact claimed step. Landing first in the knobs_hash cluster — the constant is single-writer, so #3617 rebases onto this and takes 16. Verified before merge: the PR's own tests fail when the production change is reverted (11 of the previous 32 PRs failed exactly there — one had 7 of 8 new tests passing on master); typecheck clean; MERGEABLE/CLEAN with 22/22 checks green on the current base, not a stale one. * fix(cli): stop rerouting sync --timeout into dispatchReadOnlyCommand (#3013) (#3650) Adversarial review: survived a hostile reviewer plus two independent refuters, each told to assume the PR was broken and to default to refuting when uncertain. Both #3013 defects reproduced live: `sync --dry-run --timeout` reported "unsupported command", and a bare `--timeout 60` was mis-scaled to 60ms. Fixed with a per-command dispatch gate plus timeout handback to its two owners, with every `cliOpts.timeoutMs` consumer audited. Landing first in the cli.ts and sync.ts clusters. Verified before merge: the PR's own tests fail when the production change is reverted (11 of the previous 32 PRs failed exactly there — one had 7 of 8 new tests passing on master); typecheck clean; MERGEABLE/CLEAN with 22/22 checks green on the current base, not a stale one. * fix(doctor): probe-health 'Latest' reports the true newest run — sort audit events chronologically (#3366) Adversarial review: survived a hostile reviewer plus two independent refuters, each told to assume the PR was broken and to default to refuting when uncertain. doctor's probe-health "Latest" tail-picked the oldest cross-week event instead of the newest. Chronological sort at the reader seam, matching the writer's own documented contract, with all 14 consumers audited. Follow-up to file: `doctor.ts:1017` `self_upgrade_health` has the identical bug class. Verified before merge: the PR's own tests fail when the production change is reverted (11 of the previous 32 PRs failed exactly there — one had 7 of 8 new tests passing on master); typecheck clean; MERGEABLE/CLEAN with 22/22 checks green on the current base, not a stale one. * fix(extract): stop asserting works_at from bare people/->companies/ adjacency (#3466) (#3642) Adversarial review: survived a hostile reviewer plus two independent refuters, each told to assume the PR was broken and to default to refuting when uncertain. 19,497 false `works_at` edges were being asserted from bare people/→companies/ directory adjacency. This is the exact fix prescribed when #3495 was closed — bare `mentions` plus an extractor version bump — and stubbing the old behavior back fails the new test. Retroactive cleanup of already-written rows is explicitly out of scope; filing that follow-up. Verified before merge: the PR's own tests fail when the production change is reverted (11 of the previous 32 PRs failed exactly there — one had 7 of 8 new tests passing on master); typecheck clean; MERGEABLE/CLEAN with 22/22 checks green on the current base, not a stale one. * fix(ai): fold dashscope + google keys into gateway env, drop the retired Gemini default (#3500, #3510) (#3531) Adversarial review: survived a hostile reviewer plus two independent refuters, each told to assume the PR was broken and to default to refuting when uncertain. 19 of 62 PRs cleared that bar. dashscope and google keys now fold into the gateway env — verified live end-to-end — and the retired gemini-1.5-pro default is swept from 9 files. Reverting the change fails 11 of 98 tests at fixed seams, and the budget-cap claim in the description reproduced. Landing first in the gateway/config-key seam, so #3648 rebases onto it. Verified before merge: the PR's own tests fail when the production change is reverted; typecheck clean; MERGEABLE/CLEAN on the current base after batch 1 landed, not a stale one. Known gap, recorded rather than hidden: no live provider call was made; the gemini retirement was taken from issue history rather than a vendor check. Minor follow-up to file: deriveEnvKey('google_api_key') yields a dead GOOGLE_API_KEY in the minion shell-inherit path. * fix(cycle): interleave transcript/page work items so a budget cap can't starve the doctor-visible page backlog (#3384) Adversarial review: survived a hostile reviewer plus two independent refuters, each told to assume the PR was broken and to default to refuting when uncertain. 19 of 62 PRs cleared that bar. Transcripts-first work ordering permanently starved the doctor-visible page backlog whenever the budget cap bit — the pages never got reached. Page-first interleave at the single merge point, with spend proven order-independent, and stubbing the old ordering back fails 4 of 5 tests. Landing first among the extract-atoms.ts PRs, so #3691 and #3654 rebase onto it. Verified before merge: the PR's own tests fail when the production change is reverted; typecheck clean; MERGEABLE/CLEAN on the current base after batch 1 landed, not a stale one. Known gap, recorded rather than hidden: no real-LLM budget run; the identical error path was driven synthetically. * fix(search): give query embeds a fresh floored deadline (#3690) Adversarial review: survived a hostile reviewer plus two independent refuters, each told to assume the PR was broken and to default to refuting when uncertain. 19 of 62 PRs cleared that bar. The shared AbortSignal arrived already aborted, which made the 2s embed floor dead code and silently degraded hybrid search to keyword-only — users got results that looked complete and were not. Fixed with a fresh AbortSignal.timeout(remaining) at the single shared seam; stubbing the old behavior back fails exactly the new test. This also closes verified issue #2028. Verified before merge: the PR's own tests fail when the production change is reverted; typecheck clean; MERGEABLE/CLEAN on the current base after batch 1 landed, not a stale one. Known gap, recorded rather than hidden: the DATABASE_URL e2e claims in the description were not re-run, though no SQL is touched. * fix(search): classify first-person "what do I know about X" as an entity query (#3615) (#3616) Adversarial review: survived a hostile reviewer plus two independent refuters, each told to assume the PR was broken and to default to refuting when uncertain. 19 of 62 PRs cleared that bar. One-character regex fix with a real user-visible effect: "what do **I** know about X" was classified as a general query while the you/we phrasings were correctly classified as entity queries. The new alternation is a strict superset, so no previously-matching phrasing changes, and stubbing the old regex back fails at the exact assertion. Closes verified issue #3615. Verified before merge: the PR's own tests fail when the production change is reverted; typecheck clean; MERGEABLE/CLEAN on the current base after batch 1 landed, not a stale one. * fix(integrations): resolve secrets through buildGatewayConfig's config→env folding (#3648) Adversarial review: survived a hostile reviewer plus two independent refuters, each told to assume the PR was broken and to default to refuting when uncertain. 19 of 62 PRs cleared that bar. `integrations show` printed `[missing]` for config-plane keys that the runtime gateway resolves perfectly well — so the status display disagreed with reality and sent people hunting for a problem that did not exist. Fixed with a single `secretEnv()` helper at all four read sites, preserving precedence. The spawn environment is deliberately left unchanged, which is the correct posture. Sequenced after #3531, which refactored the `buildGatewayConfig` internals this consumes. Verified before merge: the PR's own tests fail when the production change is reverted; typecheck clean; MERGEABLE/CLEAN on the current base after batches 1 and 2 landed, not a stale one. Known gap, recorded rather than hidden: the full-suite env-mutation interaction was not run locally; CI shards are green. * fix(validation): qualify backlink endpoint identity (#3667) Adversarial review: survived a hostile reviewer plus two independent refuters, each told to assume the PR was broken and to default to refuting when uncertain. 19 of 62 PRs cleared that bar. The back-link validator compared bare slugs, so a same-slug page in another source masked a genuinely missing reverse edge — silent under-reporting in exactly the multi-source setup where it matters. Now keyed on the full 4-tuple, per the `(source_id, slug)` uniqueness invariant. Verified on real Docker Postgres with 28/28 parity, which the PR itself had skipped. Verified before merge: the PR's own tests fail when the production change is reverted; typecheck clean; MERGEABLE/CLEAN on the current base after batches 1 and 2 landed, not a stale one. Known gap, recorded rather than hidden: remote MCP serialization of the additive Link fields is untested; the fields are additive JSON. * fix(recipes): align the X secret name with the resolver — X_API_BEARER_TOKEN (#2789 defect 2) (#3649) Adversarial review: survived a hostile reviewer plus two independent refuters, each told to assume the PR was broken and to default to refuting when uncertain. 19 of 62 PRs cleared that bar. The docs and recipe pinned `X_BEARER_TOKEN` while the resolver only ever read `X_API_BEARER_TOKEN` — so no single name worked and the integration could not be configured by following its own documentation. Renamed the dead documented side; reverting fails exactly 2 of the 3 new tests. Verified before merge: the PR's own tests fail when the production change is reverted; typecheck clean; MERGEABLE/CLEAN on the current base after batches 1 and 2 landed, not a stale one. Known gap, recorded rather than hidden: no live X API call was made. * fix(autopilot): derive the full-cycle timeout floor from the handler anchors (#2781) (#3656) Adversarial review: survived a hostile reviewer plus two independent refuters, each told to assume the PR was broken and to default to refuting when uncertain. 19 of 62 PRs cleared that bar. Full-cycle maintenance jobs were stamped with the outer 600s timeout instead of the 30-minute handler anchor — a regression from #3338 that killed long cycles mid-run. Fixed with a named `fullCycleTimeoutMs` derived from the handler anchors, which now fail loudly rather than silently defaulting; reverting fails 3 of 8 tests. Verified before merge: the PR's own tests fail when the production change is reverted; typecheck clean; MERGEABLE/CLEAN on the current base after batches 1 and 2 landed, not a stale one. Known gap, recorded rather than hidden: the '38 dead cycles in 24h' figure from the description was not reproduced; the stamp arithmetic was verified by code inspection. * feat(recipes): declare Gemini embedding batch-token budget (#3651) Adversarial review: survived a hostile reviewer plus two independent refuters, each told to assume the PR was broken and to default to refuting when uncertain. 19 of 62 PRs cleared that bar. The google embedding recipe declared no batch caps, so it rode the no-cap fast path with error backstops shaped for Voyage and OpenAI. Caps verified by behavioral probe — 40 texts split into 3 sub-batches matching the declared math. Sequenced after #3531, which touched the same recipe file. Verified before merge: the PR's own tests fail when the production change is reverted; typecheck clean; MERGEABLE/CLEAN with 22/22 checks on the current base after batches 1-3 landed. Known gap, recorded rather than hidden: Gemini's actual 20k limit was taken from vendor docs rather than a live call; being wrong in either direction is bounded by the cap itself. * fix(index): preserve code files containing NUL bytes (#3483) Adversarial review: survived a hostile reviewer plus two independent refuters, each told to assume the PR was broken and to default to refuting when uncertain. 19 of 62 PRs cleared that bar. Code files containing raw NUL bytes hard-failed UTF-8 encoding on import, so they silently never indexed. Sanitized at the single choke point both callers route through, with offsets kept in one coordinate space, and exercised end-to-end on a real engine. Verified before merge: the PR's own tests fail when the production change is reverted; typecheck clean; MERGEABLE/CLEAN with 22/22 checks on the current base after batches 1-3 landed. Known gap, recorded rather than hidden: follow-up to file: reindex-code hash ping-pongs on NUL-containing files — reproduced, bounded, and causes no data loss. * fix(doctor,docs): warn that the npm name 'gbrain' is unrelated + detect a shadowing npm install (#505) (#3454) Adversarial review: survived a hostile reviewer plus two independent refuters, each told to assume the PR was broken and to default to refuting when uncertain. 19 of 62 PRs cleared that bar. The npm package named `gbrain` is an unrelated squatted package, so `npm install gbrain` gives users something that is not this project. Adds doctor detection that classifies real checkouts correctly, fails open, and is try/catch'd throughout. Classification rests on the bin-shape marker since this repo has no `repository` field — verified e2e. Verified before merge: the PR's own tests fail when the production change is reverted; typecheck clean; MERGEABLE/CLEAN with 22/22 checks on the current base after batches 1-3 landed. Known gap, recorded rather than hidden: the remediation commands were not executed against a real global install, and the Windows `which -a` path is unexercised. * fix(migrate): apply the #1178 invalid-index-guard fix to 10 more historical sites (#3192) Adversarial review: survived a hostile reviewer plus two independent refuters, each told to assume the PR was broken and to default to refuting when uncertain. 19 of 62 PRs cleared that bar. Mechanically applies merged #3191's `dropInvalidConcurrentIndex` to the 10 remaining historical migrations that still had the broken DO-block form. Migration ordering and numbering are untouched — this only changes how each guards its own index creation. Verified before merge: the PR's own tests fail when the production change is reverted; typecheck clean; MERGEABLE/CLEAN with 22/22 checks on the current base after batches 1-3 landed. Known gap, recorded rather than hidden: verified by sequence inspection and the migration suite rather than by replaying all 120 migrations against every engine. * fix(pricing): correct voyage-4-large rate and add the missing voyage-4 family entries (#3480) Adversarial review: survived a hostile reviewer plus two independent refuters, each told to assume the PR was broken and to default to refuting when uncertain. 19 of 62 PRs cleared that bar. voyage-4-large was billed at the voyage-3-large rate — $0.18 against a published $0.12 — so every cost estimate using it was wrong by 50%. Corrected in the canonical table only, per CLAUDE.md's rule that every other pricing table is a derived view, and the drift guard passes. Rate checked against the live vendor page. Verified before merge: the PR's own tests fail when the production change is reverted; typecheck clean; MERGEABLE/CLEAN with 22/22 checks on the current base after batches 1-3 landed. Known gap, recorded rather than hidden: this PR previously failed the JSONB parity guard on a 32-commit-stale base. I rebased it onto current master and re-ran rather than accepting 'flaky' — the guard passes on the real base, 22/22 green. * fix(schema): apply mutation batches atomically so a mid-batch failure leaves the pack untouched (#2581) (#3446) Adversarial review: survived a hostile reviewer plus two independent refuters, each told to assume the PR was broken and to default to refuting when uncertain. 19 of 62 PRs cleared that bar. Applying a schema mutation batch was not atomic: a failure partway through left earlier mutations permanently written. Reproduced on disk — a failure at index 2 left mutation 0 applied with no way to tell from the pack's state that it was half-done. The fix validates the whole batch first and writes once, which makes partial application impossible by construction rather than by careful ordering. Verified before merge: the failure was reproduced by injecting one rather than reasoning about it; the PR's own tests fail when the fix is reverted; typecheck clean; MERGEABLE/CLEAN at 22/22 on the current base after batches 1-4 landed. Sequenced last deliberately — it collides with #3531 on docs/architecture/KEY_FILES.md and with #3667 on src/core/operations.ts, both of which landed earlier today. Known gap, recorded rather than hidden: lock contention under concurrent writers was reasoned about, not stress-tested. * fix(cycle): thread cycleSourceId into the schema-suggest phase (#3701) The phase was calling runSchemaSuggestPhase(engine, { dryRun }) with no sourceId, so it silently fell back to 'default' on every source's dream cycle -- same bug class as upstream #1586 (synthesize) and #2666 (patterns/synthesize), just an undiscovered instance for this phase. Confirmed live: schema-events audit log shows only source=default across 41 entries this week despite calendar/mail/mem/social cycles all running the phase. Co-authored-by: Claude Sonnet 5 <[email protected]> Co-authored-by: Time Attakc <[email protected]> * fix(facts): preserve remote fence writes (#3659) Co-authored-by: gbrain contributor <[email protected]> Co-authored-by: Time Attakc <[email protected]> * fix(test): make typecheck hermetic to ambient parent openclaw SDK types (#2729) (#3641) The real plugin-load e2e test dynamically imports 'openclaw/plugin-sdk'. TypeScript still resolves and type-checks that bare specifier via upward node_modules resolution, so 'bunx tsc --noEmit' on a clean checkout could fail (TS2339 on sdk.registerContextEngine) or pass depending on whichever undeclared openclaw package happened to exist in an ancestor directory. The existing @ts-ignore only covered the import line, not the property access on the following line. Cast the awaited import to a local structural interface declaring the one member the test uses (registerContextEngine, optional). TypeScript never consults the ambient module's types for the property access, so typecheck output is identical regardless of ancestor node_modules state. The @ts-ignore stays on the import statement itself and stays @ts-ignore (not @ts-expect-error) because whether TS2307 fires there is itself ambient-dependent. Runtime behavior is unchanged: the cast erases at compile time and the export's presence is still verified at runtime. Fixes #2729 Co-authored-by: Claude Fable 5 <[email protected]> Co-authored-by: Time Attakc <[email protected]> * fix(config): render object-valued fields as JSON in config show (#575) (#3575) Non-string values interpolated into the template literal printed '[object Object]' (e.g. provider_base_urls). Objects now render via JSON.stringify; objects under a sensitive key redact to '***' like their string counterparts. Co-authored-by: Garry Tan <[email protected]> Co-authored-by: Claude Opus 5 <[email protected]> * fix(cli): route init --help to its own usage text (#3652) Co-Authored-By: Time Attakc <[email protected]> * fix(operations): sync_brain threads ctx.sourceId to performSync (#2830) (#3568) Co-Authored-By: Time Attakc <[email protected]> * fix(cycle): log swallowed lock.release() failures (#1470) (#3572) Co-Authored-By: Time Attakc <[email protected]> * fix(entities): resolveEntitySlug fallback keeps the path separator (#3447) (#3567) Co-Authored-By: Time Attakc <[email protected]> * fix(minions): reconnect the stall timer, stop failJob masking job errors, unify retry matchers (#1720) (#3555) Co-Authored-By: Time Attakc <[email protected]> * fix(dream): honor a configured 0 in synthesize + auto_think config resolution (stop coercing to the default) (#3552) Co-Authored-By: Time Attakc <[email protected]> * fix(doctor): stop reporting Windows-drive image paths as missing under WSL (#1835) (#3523) Co-Authored-By: Time Attakc <[email protected]> * fix(doctor): bound embedding provider health probe (#3364) Co-Authored-By: Time Attakc <[email protected]> * fix(models): hint the /v1 base-URL suffix when a doctor chat probe 401s on an openai-compatible proxy (#3553) Co-Authored-By: Brett <[email protected]> * fix(skillpack): resolve gbrain root from module path when cwd walk fails (#3144) Co-Authored-By: Time Attakc <[email protected]> * fix(jobs): unify-types worker defaults to dry-run per its handler contract (#1575) (#3574) Co-Authored-By: Garry Tan <[email protected]> * fix(cycle): surface stamp-write failure on CycleReport and degrade status (#3504) (#3589) Co-Authored-By: Ryan Ayers <[email protected]> * fix(embed): stop one bad chunk darkening a page, and exit non-zero on failures (#3037) (#3532) Co-Authored-By: Garry Tan <[email protected]> * fix(docs): remove references to the nonexistent gbrain install command (#3502) (#3545) Co-Authored-By: Time Attakc <[email protected]> * fix(dims): skip dimensions param when it equals qwen3-embedding native width (#3699) Co-Authored-By: zenspam <[email protected]> * fix(cycle): stop extract_atoms silently skipping all work on unpriced models (#3691) Co-Authored-By: Austin Wilhite <[email protected]> * fix(budget): price free local chat providers at $0, matching embed and rerank (#3541) Co-Authored-By: Ben Young <[email protected]> * fix: health metric that cannot converge, and facts lost to a row_num collision (#3634) Co-Authored-By: Alexey (CTO) <[email protected]> * fix(test): kill timeout-resistant unit shards (#3631) Co-Authored-By: Vyacheslav Zakharov <[email protected]> * fix(import): checkpoint on a time interval and before preserve, not only every 100 files (#3585) Co-Authored-By: Kyzcreig <[email protected]> * test(cli): cover import side-effect guard (#3581) Co-Authored-By: 老赵 <[email protected]> * feat(by-mention): Unicode-aware word tokenizer — Vietnamese/diacritic name extraction (#3563) Co-Authored-By: Christoph <[email protected]> * fix(budget): reconcile #3691 and #3541 — free local providers are priceable at $0 #3691's regression test used llama-server as an 'unknown provider' example. #3541 (same wave) prices ollama/llama-server at $0 via FREE_LOCAL_CHAT_PROVIDERS, so that example is now priceable and the assertion inverted. Swapped in groq — the paid-but-unpriced case #3691's own description cites — and added the positive assertion that free local providers keep their cap enforced. * fix(skills,docs): unify-types playbooks must pass apply:true after the #3574 default flip #3574 flipped the unify-types worker default to dry-run (jobs.ts:2221, apply: data.apply ?? false) and updated the architecture docs, but three agent-facing surfaces still presented the bare submit as the Apply step: skills/schema-unify/SKILL.md 'Phase 3: Apply', skills/conventions/ schema-evolution.md, and README.md. Because #3545 also edited SKILL.md in this wave, each PR looked self-consistent in isolation — only the composed branch shipped a playbook whose apply step silently retypes nothing and never flips the active pack. Skills distribute downstream via the skillpack, so this would have propagated. Found by an independent cross-PR review pass. * v0.42.69.0 fix: community fix wave — 22 contributed fixes for silent-failure paths, local-model support, and multi-source routing * fix(test): recipe-ollama-dims pins a truncated width — native-width requests now omit the param (#3699) Master's #1072 test asserted bare qwen3-embedding@1024 threads dimensions:1024; wave PR #3699 suppresses the param when the request equals the native width (1024 for the bare id), so the same input now correctly returns undefined — pinned by dims-qwen3-native.test.ts. Moved this test to 512 to keep its actual intent (bare id recognized as Matryoshka-capable) without contradicting the suppression. Third composition defect of the wave; caught by CI shard 8. * fix(contextual): route synopsis through configured models (#3678) Co-Authored-By: cvillarroel2 <[email protected]> * fix(sync): prevent dry-run pull and out-of-strategy page deletion (#3624) Co-Authored-By: Mammad M. <[email protected]> * docs(security): tighten responsible disclosure language (#3619) Co-Authored-By: Diego <[email protected]> * fix(serve,import): stop the SIGINT orphan; quote imported frontmatter (#3618) Co-Authored-By: Garry Tan <[email protected]> * fix(minions): settle the spawn loop when a child never launches (#3536) Co-Authored-By: Diego <[email protected]> * fix(build): force LF for Markdown too (frontmatter parsers anchor on LF) (#3517) Co-Authored-By: Diego <[email protected]> * fix(apply-migrations): stop reporting 'All migrations up to date' while schema is behind (#3085) Co-Authored-By: Garry Tan <[email protected]> * fix(search): raise hnsw.ef_search to match the vector candidate request (#3613) Co-Authored-By: Time Attakc <[email protected]> * fix(search): implement documented relative since/until durations (#3442) (#3539) Co-Authored-By: Garry Tan <[email protected]> * fix: scope residual federated reads (#3550) Co-Authored-By: dialthewolff <[email protected]> * fix(chunker): measured hard-split budgets + token-aware capByChars — the follow-up invited in #3477's merge review (#3564) Co-Authored-By: YMYD <[email protected]> * fix(windows): use native path boundaries and assertions (#3578) Co-Authored-By: Diego <[email protected]> * fix(cli): make --brain actually route to the named brain (#3576) Co-Authored-By: Garry Tan <[email protected]> * fix(sources): honor unfederate for unqualified reads (#2928) (#3533) Co-Authored-By: Garry Tan <[email protected]> * feat(doctor): tamper-evident skills/ manifest — generator, doctor drift check, CI freshness guard (#159) (#3453) Co-Authored-By: Time Attakc <[email protected]> * feat(doctor): silent-failure check batch — content-hash duplicates, undeclared DB-only pages, heartbeat staleness, db_only collector collision (#2250 #2784 #2787 #2788) (#3457) Co-Authored-By: Time Attakc <[email protected]> * fix(extract): stop silently dropping links from non-whitelisted directories (#2576) (#3560) Co-Authored-By: Time Attakc <[email protected]> * fix(extract): resolve cross-directory wikilinks via sync-consistent slugification (#3161) Co-Authored-By: Garry Tan <[email protected]> * fixup(#3453): regenerate skills.lock.json against current master skills/ The PR's committed manifest predates master's latest SKILL.md edits; regenerated with the PR's own generator per the check's instructions. * fixup(#3161): pin #1964 assertions to the wikilink-resolved lane In-wave interaction with #3560: extractPageLinks now also emits raw-literal and bare-path candidates that downstream existence checks drop; the tests' whole-list toEqual predates that. Filter to linkSource='wikilink-resolved' — the PR's resolution + no-cross-dir-leak claims are still fully pinned. * fix(wave): composite-review findings — mask wikilink interiors from the bare-path scan; future-safe extractor watermark Two cross-PR findings from the wave-i hostile review (Codex xhigh): 1. The #3560-ungated bare-path pass scanned inside [[...]] spans, so a dir-qualified wikilink's lowercase prefix (its parent page) became a spurious 'markdown' edge whenever the parent existed. Wikilink spans are now masked with equal-length blanks before pass 2; discriminating test added (fails without the mask). 2. LINK_EXTRACTOR_VERSION_TS was midnight today with a strict-< staleness predicate, so same-day stamps from pre-wave code read as fresh and never re-extracted. Bumped to 2026-08-02T00:00:00Z. Plus two comment corrections from both reviewers: upgrade.ts's X1 hook rationale (stale after #3085) and PGLiteEngine.transaction's tx-engine db-proxy hazard for #3613's searchVector wrapper. * v0.42.70.0 fix: community fix wave two — 18 contributed fixes for dead flags, dry-run safety, link resolution, and Windows support * fix(wave): revert extractor watermark to merge-day midnight — a future watermark masks concurrent edits (D4) CI caught what the composite review's watermark 'fix' actually did: the stamp path clamps links_extracted_at up to the watermark (GREATEST(updated_at, versionTs), extract.ts:1835), so a future watermark stamps every page at 2026-08-02 and masks any concurrent edit until then — the exact race D4 guards — and makes the doctor lag check warn on fresh brains. The limitation the future date tried to cover (stamps written by pre-wave code after the watermark) is inherent: no fixed watermark covers code that keeps running past it. Documented instead. * feat(ci): publish GitHub releases so binary self-update can work (#3521) (#3573) Co-Authored-By: Time Attakc <[email protected]> * fix(ci): env-bind the release-notes version interpolation Review hardening from the #3573 gatekeeper pass: a ${{ }} inside run: is shell injection by construction; bind through env instead. Not attacker-reachable today (VERSION comes from master), correct anyway. * v0.42.71.0 feat(ci): publish GitHub releases on every version bump — organized notes + binaries, self-update unbroken (#3521 #3716) * fix(ci): release build gates on the artifact, not a serial re-run of the whole suite The first Release run (30698650484) failed before building anything: the build job re-ran the entire unit suite serially on the release runner — a different environment from the sharded Test workflow that had already gated the exact SHA at merge — and died on ambient-env tests. The review of #3573 predicted this ('a flaky test now blocks the release'). The build job now compiles and smoke-tests the binary (--version must match the VERSION file), which validates the actual artifact — something the test suite never did. The Test workflow remains the code gate. * v0.42.72.0 feat(auth): server-enforced slug-prefix write fence for OAuth clients + qm-harness integration guide (#3712) Registering an OAuth client with --bound-slug-prefixes now makes the write boundary real: writes outside the bound prefixes are refused on every op that can name a page, and ops that write by something other than a slug are refused outright rather than left unfenced. Deny-by-default at dispatch, so a write op added later is refused to bound clients until it is explicitly fenced. Adds docs/integrations/qm-harness.md (gbrain as the company brain for a qm deployment) with a roster-driven provisioning script and deployment templates, plus a Known limitations section stating plainly that this is a write boundary and not a privacy boundary. Five review rounds, including three clean-room passes by codex gpt-5.6-sol and Claude Fable 5 against an instruction-stripped tree. * v0.42.72.1 docs(contributing): require a human-written intent paragraph + live screenshot on every issue and PR (#3745) Effective 2026-08-02. Every issue and PR must carry a paragraph the author wrote themselves explaining why they are opening it, and a screenshot showing gbrain actually in use in that situation. AI-generated or AI-polished intent text is not accepted; AI assistance for the code is still fine. Missing either one means closed without review, reopenable once added. Stated in CONTRIBUTING.md and pre-filled in both issue templates plus a new pull request template so the fields are in front of the author. Co-authored-by: Garry Tan <[email protected]> Co-authored-by: Claude Opus 5 <[email protected]> * feat(schema-pack): add slug_filter to retype mapping rules (#2655) retype mapping_rules' existing path_filter matches pages.source_path, which is only populated for pages synced from a git repo. Pages ingested via the put_page MCP tool (or any write path that doesn't go through sync) have source_path = NULL, so path_filter can never disambiguate them — a same-from_type retype rule targeting a slug prefix has no way to address this class of page at all. slug_filter adds an independent, orthogonal LIKE filter on pages.slug (the field that's always populated), combinable with path_filter via AND when both are given. Wired through the full call path: schema validation (manifest-v1.ts), the RetypeRule interface + probeRule/applyRetypeRule (retype.ts), and the pack-manifest → RetypeRule conversion in the unify-types job handler (unify-types-handler.ts) — the last one matters because a field only present in the zod schema but not carried through that conversion would validate fine yet silently no-op at execution time. - manifest-v1.ts: add optional slug_filter to RetypeMappingRuleSchema - retype.ts: slug_filter on RetypeRule; probeRule + applyRetypeRule both apply `AND slug LIKE $N` when present, independent of path_filter - unify-types-handler.ts: carry rule.slug_filter through the pack-mapping-rule → RetypeRule conversion - tests: skips-outside-slug_filter, matches-despite-NULL-source_path (the motivating case), path_filter+slug_filter combined via AND 320/320 existing schema-pack tests pass; 3 new tests added; tsc --noEmit clean. Co-authored-by: Claude Sonnet 5 <[email protected]> Co-authored-by: Time Attakc <[email protected]> * fix(integrity): stop counting dead-link findings as review-queue entries (#3750) (#3751) The dead-link branch in cmdAuto called logSkip() (writing to integrity.log.jsonl) but incremented bucketReview, the same counter fed by the bare-tweet path's appendReview() calls (which write to integrity-review.md). The printed "Review queue" summary line therefore included findings that were never written to the review file, so `gbrain integrity review` silently disagreed with `integrity auto`'s own summary. Give dead-link findings their own counter (bucketDeadLink) and print it as a separate line. No change to bare-tweet bucketing or CLI surface. * docs(integrity): correct the --dry-run line in the subcommand comment (#3739) The comment listed `gbrain integrity --dry-run` as a subcommand, but runIntegrity dispatches on `check`/`auto` and parses --dry-run inside the auto path, so that form exits 1 with "Unknown subcommand". The runtime help already shows --dry-run indented under `auto [options]`; only the source comment disagreed. Addresses #3738. * fix(cycle): grade_takes and calibration_profile record the model they actually run — follow the gateway chat model (#3726) Two cycle phases still carried the label-vs-actual defect that propose_takes shed in v0.42.62 (and that the #2805 review noted was worth tracking so it isn't lost): - grade_takes hardcoded 'claude-sonnet-4-6' into judge_model_id, the evidence signature, and budget metering, while the default judge call passed NO model hint and rode the gateway's chat_model. On any brain with a non-default chat_model, the verdict cache and telemetry recorded a model that never ran. - calibration_profile persisted TIER_DEFAULTS.reasoning to model_id but never passed a model to the patterns generator at all, so the recorded model and the executed one were unrelated. Fix, matching the propose_takes convention: each phase resolves ONE string via explicit override > getChatModel(), and that string drives the chat call, the cache key, and the stored id. - grade_takes: the judge hint gets the FULL provider-prefixed string; the stored judge_model_id and evidence signature keep the historical bare tail. Stock installs are unchanged: getChatModel() defaults to 'anthropic:claude-sonnet-4-6', whose tail equals the old hardcoded value, so no verdict cache invalidates. A genuinely different chat_model invalidates, which is correct — the judge really changed. - calibration_profile: getChatModel() is provider-prefixed, preserving the #2451 contract, and its default IS the old TIER_DEFAULTS.reasoning value — stock behavior unchanged. The generator now receives the same resolved string that is persisted to model_id. Tests: regression per phase pinning configured-chat-model routing (full string to the call, bare tail to the cache key for grade_takes, full string persisted for calibration); all existing suites pass unchanged, pinning the no-change-on-stock property. 112 tests across the four affected suites; typecheck clean. Co-authored-by: Paolo Belcastro <[email protected]> Co-authored-by: Claude Fable 5 <[email protected]> * fix(durability): gate installHelper's exec-bit chmod on dryRun (#3736) (#3759) installHelper's "already current" fast path ran chmodSync(helperPath, 0o755) unconditionally, before the dryRun check further down — so `gbrain sources harden --dry-run` mutated the helper's permissions even though dry-run is documented as a pure preview. This is the 5th instance of the #3594 class (see #3692 for the sibling fix to the step-1 pull, same function). Gate the chmod with `if (!dryRun)`, mirroring the guard pattern already used in installLocalHook's own "already current" branch. Non-dry-run behavior is unchanged. Adds a regression pair to test/brain-repo-durability.serial.test.ts: one asserting dry-run leaves a drifted-permission helper untouched, and a control asserting the real run still restores the exec bit. * fix(import): keep stdout JSON-only under --json (#3637) (#3764) runImport printed its informational lines ('Found N markdown files' and four siblings) through console.log unconditionally, so `gbrain import <dir> --json` emitted them ahead of the payload and stdout did not parse as JSON. A consumer parsing stdout reads that as zero imports while its own bookkeeping records the files as ingested, so the next run skips them permanently. Route those five lines through an `info` helper that switches to stderr when --json is set. The lines are relocated, not removed: human mode is byte-for-byte unchanged, and the JSON payload itself is untouched. This is the contract CLAUDE.md already states ('Stdout stays clean for data output (--json payloads)') and that import.ts's own progress comment repeats. scripts/check-progress-to-stdout.sh only greps for process.stdout.write('\\r…), so this class was never guarded; the new spawn-level test pins it. * feat(ci): strict PR usefulness gate — merge-lane/close-lane verdict before any review effort (#3698) Runs on pull_request_target against master. PR code is never checked out or executed: metadata + a 120KB-capped diff come from the GitHub API only; the base-repo checkout provides the rubric script. All interpolations are env-bound; actions SHA-pinned; permissions limited to contents:read + pull-requests:write + issues:write; per-PR concurrency with cancel. scripts/pr-gate.mjs (no deps, fetch only) classifies via the strict usefulness rubric (claude-sonnet-5, strict JSON schema, thinking disabled; sampling params omitted — rejected on this model), posts one sticky comment (<!-- gbrain-pr-gate -->), applies exactly one gate:* label, and exits 1 only on close-lane. Missing ANTHROPIC_API_KEY or API failure after 2 retries NEUTRAL-skips loudly (comment + warning, exit 0). Mechanical (no-LLM) checks: version-first title rule and diff red flags (>40 files, node_modules, symlinks, workflow edits force needs-maintainer, new package.json deps, deleted tests). test/pr-gate-workflow.test.ts pins the security invariants and unit-tests the exported title rule + red-flag detector (import side-effect guarded). Co-Authored-By: Claude Fable 5 <[email protected]> * fix(ci): harden the PR gate — comment-ownership, output sanitization, deterministic lane downgrades (blind review round 1) Two independent blind reviews of #3698 (one APPROVE, one REJECT). Every confirmed finding from the reject side, plus the cheap hardening both reviews flagged. BLOCKING 1. Sticky-comment hijack. upsertStickyComment adopted ANY comment containing the marker, so a contributor could pre-post `<!-- gbrain-pr-gate -->`, have the gate PATCH it, then edit it into a fake green verdict. isOwnComment() now requires user.type === 'Bot' AND login === 'github-actions[bot]' AND the body to START WITH the marker; anything else gets a fresh comment. 2. LLM output injected as raw Markdown. Model-produced reasons[] and reviewer_checklist[] reached the comment unescaped — PR-body-driven injection could forge headings, a second marker, and live @mentions. sanitizeModelText()/sanitizeList() are now the single choke point in renderComment(): HTML comments stripped, mentions zero-width-broken, leading block markers removed, newlines collapsed, 300 chars per string, 8 entries per list, both caps self-marking. 3. Lane was purely model-decided. A well-written feature pitch could talk itself into merge-lane. The model now RECOMMENDS; applyMechanicalDowngrades forces merge-lane -> needs-maintainer on any of: workflow edits, a new package.json dependency, a new src/core/ai/recipes/ provider file, new KNOWN_CONFIG_KEYS entries, >40 changed files, >400 net source lines outside test/, or a src/ change with no test file touched (#3665). The sticky comment reports them under "Mechanical downgrades applied". 4. The test file did not pin what it claimed. Added: no gh pr checkout / git fetch / refs/pull / pull/*/head in any spelling; exact permissions key->value map plus a single-permissions-block assertion so no job-level grant re-widens contents; the ${{ }}-in-run scanner now covers folded (`run: >`) and chomped blocks, with a guard-the-guard test; and mocked end-to-end runGate() runs for close-lane exit 1, marker hijack, sanitizer, truncation, refusal routing, NEUTRAL label clearing, label swap, and the spend guard. 5. Version-first title regex rejected the documented suffix form. `v0.31.1.1-fixwave fix: ...` now passes. VERSION_AT_END_RE no longer false-positives on `chore: bump zod (3.25.76)`: it fires only on a v-prefixed or 4-segment trailing version, i.e. this project's own shape. 6. Refusal fail-open. stop_reason=refusal exhausted retries into a green NEUTRAL — a deterministic way to dodge the red X. Refusal and schema-invalid output now route to needs-maintainer with an explicit note; only transport failure stays NEUTRAL. Refusal also short-circuits the retry loop, since retrying a deterministic refusal only burns spend. ALSO 7. persist-credentials: false on the checkout step. 8. Dropped pull-requests:write. Everything the script calls is the issues API (comments, label create, label add/remove), so issues:write is the only grant that is actually needed. 9. Spend guard for the edited/synchronize amplification: the LLM call is skipped when sha256(title+body+head_sha) matches the hash recorded in the previous sticky comment's state block, and the stored lane's exit code is reused. 10. NEUTRAL runs now clear every gate:* label instead of leaving a stale verdict behind. Verified: bun test test/pr-gate-workflow.test.ts 63 pass / 0 fail, bun run typecheck clean, actionlint clean, check-privacy / check-no-tracked-symlinks / check-progress-to-stdout / check-bun-test-timeout clean, plus a live Anthropic smoke of the exact request shape (HTTP 200, valid strict JSON, injection attempt rejected). Co-Authored-By: Claude Fable 5 <[email protected]> * feat(ci): gate enforces the #3745 intent-paragraph + screenshot requirement CONTRIBUTING.md (#3745) requires every PR to carry a paragraph the author wrote themselves and a screenshot of gbrain actually in use. The gate now checks both, mechanically, before it spends anything on review. What it checks (no LLM, both exported for testing): - hasScreenshot: markdown ``, a bare user-images.githubusercontent or github.com/user-attachments/assets URL, or an <img> tag. Anything inside a fenced code block does not count — pasting the syntax is not attaching the picture. - hasIntentParagraph: >= 40 words of prose left after stripping fenced code, blockquotes, list items, headings, HTML comments, links and the PR template's own boilerplate. Per-character scripts are tokenized per character, so a paragraph written in Chinese counts as one. The two lane consequences: - missing_screenshot OR missing_intent forces close-lane from any recommended lane (exit 1) and skips the model call entirely — closed without review is the documented consequence, so there is nothing to spend a review on. The sticky comment leads with what is missing, how to fix it, and the reopen path; both misses are also recorded in the existing "Mechanical downgrades applied" section. - The model's new advisory intent_authenticity verdict forces needs-maintainer (exit 0) when it reads "ai_generated", and never close-lane on that signal alone. The comment says only that a maintainer will read the paragraph personally; the model's reasoning is consumed and never published at the contributor. Rubric + strict-JSON schema gain intent_authenticity and intent_authenticity_reason, with explicit instructions that rough grammar, terseness and non-native English are evidence of a HUMAN and that "unclear" is the answer whenever the evidence is not clear-cut. test/pr-gate-workflow.test.ts: 63 -> 88 tests, all green. Co-Authored-By: Claude Fable 5 <[email protected]> * fix(ci): gate policy check survives an API outage; merge master; drop NUL separators Three fixes on the strict PR usefulness gate (#3698), plus the merge that brings in the policy it enforces. 1. The mechanical policy check now outlives the model. The ANTHROPIC_API_KEY guard used to sit above detectPolicyMisses, so a PR with no intent paragraph and no screenshot got a green NEUTRAL skip whenever the key was absent or Anthropic was down — "wait for a 500" was a documented way past the one hard requirement. The #3745 branch now sits above the key guard and the spend guard: a policy miss is close-lane + the friendly fix-it comment + exit 1 with no API dependency at all. A compliant PR that hits a missing key or a dead API keeps the round-1 NEUTRAL behavior unchanged (loud comment, ::warning::, exit 0, stale gate:* labels cleared) — and the NEUTRAL comment now says plainly that the *usefulness verdict* did not run, while still reporting the title check and mechanical red flags it was able to compute without a model. 2. Merged origin/master, which carries #3745's CONTRIBUTING.md section and .github/pull_request_template.md. No conflicts: this branch never touched VERSION / package.json / CHANGELOG.md, so master's 0.42.72.1 carried through untouched — the feature branch adds no version bump. The test's inlined pull_request_template fallback (only needed while the branch predated the merge) is gone; it now reads the real file, so growing the template's own prose past the 40-word bar fails here instead of silently letting an untouched template through. The CONTRIBUTING_URL deep link is pinned against a GitHub-style slug of every heading in the merged CONTRIBUTING.md, with the slugger itself pinned so it cannot "pass" against an anchor GitHub never generates. 3. hashInputs joined its three fields with literal NUL bytes, which made grep treat the whole of scripts/pr-gate.mjs as binary — any future grep-based CI guard over that file would have matched nothing and passed silently. Replaced with JSON.stringify of the tuple: still unforgeable (each field is quoted and escaped), still stable by construction, and printable. `grep -c hashInputs scripts/pr-gate.mjs` now returns 2 instead of nothing. Existing sticky-comment state hashes are invalidated once, costing one re-verdict per open PR. Tests: 95 pass / 0 fail in test/pr-gate-workflow.test.ts. The no-API-key policy-miss case was verified to fail against the pre-fix ordering. Verified live against the Anthropic API: HTTP 200, strict JSON, all seven required keys, merge-lane on a compliant fixture. Co-Authored-By: Claude Fable 5 <[email protected]> * fix(ci): gate — sanitize mechanical flag details, anchor path regexes, exempt maintainer/bot/draft from the policy check (blind review round 2) Blind review round 2 rejected the branch. Five findings, all fixed. BLOCKING 1 — the verdict was forgeable through a PR filename. renderComment wrote mechanical red-flag details raw, and two of them interpolate filenames (adds_recipe, deletes_tests). git allows a newline inside a filename and JS `[^/]` matches one, so RECIPE_RE's anchors were decorative: a PR adding `src/core/ai/recipes/x\n## PR Gate — ...\ncc @octocat\n<!-- ...state... -->\nz.ts` put that text verbatim into the bot's comment — forged heading, live third-party mention, and on the NEUTRAL render (which writes no state block of its own) parseState() returned the ATTACKER's block, so the next run hit the spend guard and silently skipped the verdict with no label and exit 0. Three layers: (a) flag details go through sanitizeList, exactly like the model's strings. Audited every other interpolation into the comment; the rest are literals in the file, Number()-coerced, or already sanitized. (b) every path regex spells its segment class [^/\n], not [^/] — RECIPE_RE, SOURCE_EXT_RE and the test-path check. (c) parseState reads the state block only off line 2 of a marker-leading comment (where renderComment writes it) and STATE_RE is whole-line anchored. A block anywhere else is somebody else's text. BLOCKING 2 — no exemption, so every release PR was close-lane. Measured: 40 of the last 40 merged PRs would be close-lane on missing_screenshot, including every /ship release PR. A check that is red on every release gets switched off within a week, and then it filters nothing. The #3745 policy exists to filter INCOMING OUTSIDE CONTRIBUTIONS; release automation cannot take a screenshot of itself. It is now waived for OWNER/MEMBER/COLLABORATOR, bot authors and drafts — the usefulness verdict, the title rule and every mechanical red flag still run, and the sticky comment says the check was skipped. author_association / draft / user.type are read from the pr.json the workflow already fetches: no new API call, one source of truth. `draft` is the one author-settable input, so ready_for_review joins the trigger list and the exemption is folded into the spend-guard hash — the draft-era verdict cannot be reused after the flip. Stated as a deliberate decision in both the workflow header and the script. 3 — DOWNGRADE_FLAG_IDS omitted deletes_tests, adds_symlink and adds_node_modules, so a PR deleting test/e2e/engine-parity.test.ts kept merge-lane and a green check on the strength of its prose. All three added. The old test used deletes_tests as its example of a NON-downgrading flag; rewritten to pin the stronger invariant instead — every id detectRedFlags can emit is a downgrade trigger (derived from the detector, so a new flag fails until it is classified on purpose), and the set is still an allowlist (an unrecognized id changes nothing). 4 — FENCE_RE backtracks superlinearly on a hostile body: 65KB of backticks (GitHub's max body length) measured 8.2s across the two policy scans on a pull_request_target runner. stripCodeFences now caps the scan at 16KB — same input, 0.40s. Tradeoff documented at the constant: the intent paragraph and the screenshot both sit near the top in practice (the PR template puts them in the first two sections, and the model payload already caps the same body at 6KB), so a real contributor is not judged on a truncated tail. Verified: test/pr-gate-workflow.test.ts 126 pass / 0 fail (was 95), typecheck clean, actionlint clean, check-privacy / check-no-tracked-symlinks / check-progress-to-stdout / check-bun-test-timeout / check-key-files-current-state all exit 0. Each new pin was mutation-tested against the pre-fix behavior: all six mutants fail the suite. Co-Authored-By: Claude Fable 5 <[email protected]> * fix(ci): gate — escape comment HTML, tighten screenshot floor, fix fence false-positive, repair label ordering (blind review round 3) Four findings from a second independent blind review, all reproduced against HEAD before the fix and all pinned by a mutation-tested case. - sanitizeModelText escapes &, < and > after the existing comment/mention/ block-marker stripping, so raw HTML from any dynamic string (model output, mechanical flag details built from PR filenames, neutralReason, the policy-exempt note) renders as literal text. A <details open><summary>MERGE LANE — approved</summary> string is a forged verdict inside a close-lane comment; it now renders as <details. - hasScreenshot requires a markdown image whose URL looks like a URL or path, an <img> carrying a non-empty src=, or a bare paste URL, and strips HTML comments before scanning. ``, `<img alt=proof>` and an image hidden inside `<!-- -->` no longer clear it. Documented in the code as a FLOOR against zero-effort submissions, not proof. - stripCodeFences is a line scanner following the CommonMark rule that a closing fence must be the same character and at least as long as the opening one. The old backreference read ```` as a NEW opening fence and stripped to EOF, so a compliant body documenting fence syntax lost its intent paragraph and was closed. The scanner is also linear, retiring the superlinear-backtracking hazard the 16KB cap was sized against. - Labels are reconciled BEFORE the sticky comment carrying the cached state. Written the other way, one transient label-API 500 left stale/missing/ duplicate labels forever: the rerun short-circuited on the persisted state and returned success without repairing them. Also: - hashInputs covers what the run consumes — the truncated model body plus the mechanical policy outcome — so an edit past the 6KB model cap no longer mints a new hash and buys an identical paid call, while a policy fix landing past that cap still invalidates the cached verdict. - The test's YAML block-scalar scanner accepts indentation indicators in both legal orders (`>2-` as well as `|-2`), with a guard-the-guard case proving it sees a `run: >2-` block interpolating attacker-controlled text. - A block at the top of the script states plainly that this gate is a triage signal and a reviewer checklist, not an authorization boundary; one line of the sticky comment footer says the same to the contributor. 145 pass / 0 fail (was 126), typecheck clean, actionlint clean, verify 34/34. * fix(ci): gate — stop red-Xing genuine contributors; neutralize markdown embeds (blind review round 4) A blind reviewer rejected this branch on false positives and the repro held up. The gate's intent check deleted every list and blockquote LINE before counting words, then measured what was left against an arbitrary 40. Real people write their story as bullets. Measured on the four realistic-human descriptions from the repro: description before after floor 40 floor 20 own prose written as four bullets 0 55 CLOSE PASS short non-native-English paragraph 38 38 CLOSE PASS specific first-person bug report 34 34 CLOSE PASS mostly a stack trace + a real explanation 28 27 CLOSE PASS The file's own exemption rationale already says a check that is red on every release is a check somebody disables inside a week. Red on every terse-but- genuine contribution dies the same way, and it costs real contributors on the way there — worse than the forgery risks rounds 1-3 chased. - intentWordCount strips the list/quote MARKER and keeps the author's words. Still stripped: fenced code, indented code (new — the other spelling of a fence, guarded so it never eats a list continuation), headings, HTML comments, raw HTML, bare URLs, inline code, link/image syntax, and the template's own bold prompts. An untouched pull_request_template.md still scores 0, pinned against the real file on disk. - INTENT_MIN_WORDS 40 -> 20, documented as a floor against empty/boilerplate- only descriptions, not a quality bar. CONTRIBUTING.md documents no word count at all. Empty, "fixes bug", the unfilled template, 10 words of lorem and an indented log paste all still fail. - The fix-it comment no longer tells contributors to reopen a PR that was never closed — the gate has no close call in it. It now says what actually happens: editing the description re-runs the check (the workflow triggers on `edited`), the PR stays open, a maintainer decides. - sanitizeModelText backslash-escapes `[` and `]`. Escaping HTML killed the <details> forgery, but Markdown needs no angle brackets: an image embed renders a green "APPROVED" picture and a link renders a live phishing target inside a close-lane comment. The file asserted nothing attacker-controlled reaches Markdown unescaped; that is now true. - A label-API failure during a NEUTRAL run no longer throws to exit 2 with no comment. NEUTRAL has no verdict to record, so it logs, still posts, exits 0 — and the comment stops claiming labels were cleared when they were not. The verdict path keeps failing loudly on purpose (ordering note). Four verbatim human descriptions are pinned as PASSING forever. Eight mutations (floor, list markers, quote markers, indented code, markdown escaping, the reopen copy, the NEUTRAL try/catch, the label claim) each fail at least one test. test/pr-gate-workflow.test.ts 145 -> 158 pass, 0 fail. typecheck, actionlint, verify (34/34) green. Co-Authored-By: Claude Fable 5 <[email protected]> * fix(ci): gate — CommonMark info-string rule, hash the real model payload, close the YAML scanner gap (blind review round 5) Three defects a second blind reviewer found that survived round 5. All three reproduced against cf7a1616 before the fix and are pinned after it. 1. The fence scanner still falsely closed compliant descriptions. CommonMark 4.5 forbids a backtick in a BACKTICK fence's info string, so ```foo`bar is an ordinary paragraph. The scanner opened a fence on it, never found a closer, and stripped the body to EOF: body = '```foo`bar' + three sentences of real prose + an image embed before: intentWordCount 0, misses [missing_intent, missing_screenshot] after: intentWordCount 52, misses [] Same class as round 5's closing-length bug and the same cost — opening a block CommonMark would not open deletes the author's prose exactly the way closing one late did. Tilde fences keep the permissive rule (4.5 restricts backtick fences only). 2. The spend guard hashed less than the model consumes. hashInputs covered title, modelBody, head.sha, exemption and policy ids — but the payload also carries the changed-file list and the diff, and the workflow degrades the diff to a marker line when the API 406s on a huge one. So a run that classified with no diff cached a diff-blind verdict, and the next run — real diff in hand, same title/body/sha — matched the hash and was served that verdict permanently (measured: anthropicCalls 1 across both runs). The assembled payload is now folded in as a fixed-width digest, inside the JSON tuple where quoting still makes a forged boundary impossible. Round 5's property is re-pinned: a policy fix past the model's body cap inval…
closes #2567
Resubmission of #2568 per the closure guidance: script standalone, endorsement stripped to a spec link.
What this adds:
scripts/envelope-to-gbrain.mjs- a zero-dependency, deterministic, no-network importer that turns an envelope-v0 file (format spec: github.com/memvelope/memvelope) into one Markdown page per conversation, written into a directorygbrain syncingests. Solves cold-start: a new brain can ingest years of existing ChatGPT/Claude history instead of starting empty.What lands: the script, a self-contained test (8 cases), and a small fixture envelope. No docs guide - usage, output layout, edge cases, and verification steps are in the script header. No behavior change to existing paths, no new deps.
Receipts: rebased and re-verified on current master -
bun test test/envelope-to-gbrain.test.ts8/8; fixture run writes 1 page. Previously live-verified end-to-end: a real 662MB Claude export -> 353 conversations = 353 distinct pages, searchable after sync, provenance and message-id citations intact.Happy to adjust naming/placement to repo conventions and to maintain this going forward.