963 Commits
Author SHA1 Message Date
15b9863d13 v0.42.73.2 fix(security): fence dedup-resolved writes to the caller's own write scope (#3809)
* fix(security): fence the dedup-resolved slug under the caller's own confinement

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

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

Reported privately by Aleksei Razsadin.

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

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

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

VERSION + package.json + CHANGELOG for 0.42.73.2.

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

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

---------

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

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

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

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

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

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

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

This reverts commit 6d1232d5a6 (v0.42.72.1).

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

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

---------

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

94 tests pass, 0 fail.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

---------

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

BLOCKING

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

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

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

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

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

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

ALSO

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Five review rounds, including three clean-room passes by codex gpt-5.6-sol and Claude Fable 5 against an instruction-stripped tree.
v0.42.72.0
2026-08-02 07:05:13 +08:00
Garry Tan f07e9dff11 fix(ci): release build gates on the artifact, not a serial re-run of the whole suite
The first Release run (30698650484) failed before building anything: the
build job re-ran the entire unit suite serially on the release runner —
a different environment from the sharded Test workflow that had already
gated the exact SHA at merge — and died on ambient-env tests. The review
of #3573 predicted this ('a flaky test now blocks the release'). The
build job now compiles and smoke-tests the binary (--version must match
the VERSION file), which validates the actual artifact — something the
test suite never did. The Test workflow remains the code gate.
v0.42.71.0
2026-08-01 20:43:58 +08:00
Garry Tan 72ec53bb49 v0.42.71.0 feat(ci): publish GitHub releases on every version bump — organized notes + binaries, self-update unbroken (#3521 #3716) 2026-08-01 19:55:28 +08:00
Garry Tan 0244104b8d 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.
2026-08-01 19:55:28 +08:00
Garry TanandTime Attakc 48618bd4bf feat(ci): publish GitHub releases so binary self-update can work (#3521) (#3573)
Co-Authored-By: Time Attakc <89218912+time-attack@users.noreply.github.com>
2026-08-01 19:55:28 +08:00
Garry Tan dcaea7fdbb 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.
2026-08-01 10:06:17 +08:00
Garry Tan c6bfab582e v0.42.70.0 fix: community fix wave two — 18 contributed fixes for dead flags, dry-run safety, link resolution, and Windows support v0.42.70.0 2026-08-01 10:06:17 +08:00
Garry Tan c5ac3efe9f 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.
2026-08-01 10:06:17 +08:00
Garry Tan 0a6b697070 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.
2026-08-01 10:06:17 +08:00
Garry Tan 873587f831 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.
2026-08-01 10:06:17 +08:00
Garry Tan 78391e8b64 fix(extract): resolve cross-directory wikilinks via sync-consistent slugification (#3161)
Co-Authored-By: Garry Tan <garrytan@gmail.com>
2026-08-01 10:06:17 +08:00
Garry TanandTime Attakc f08a51d9de fix(extract): stop silently dropping links from non-whitelisted directories (#2576) (#3560)
Co-Authored-By: Time Attakc <89218912+time-attack@users.noreply.github.com>
2026-08-01 10:06:17 +08:00
Garry TanandTime Attakc 7cbb99ffef 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 <89218912+time-attack@users.noreply.github.com>
2026-08-01 10:06:17 +08:00
Garry TanandTime Attakc aa5b9e6e2d feat(doctor): tamper-evident skills/ manifest — generator, doctor drift check, CI freshness guard (#159) (#3453)
Co-Authored-By: Time Attakc <89218912+time-attack@users.noreply.github.com>
2026-08-01 10:06:17 +08:00
Garry Tan 8e3699156f fix(sources): honor unfederate for unqualified reads (#2928) (#3533)
Co-Authored-By: Garry Tan <garrytan@gmail.com>
2026-08-01 10:06:17 +08:00
Garry Tan cc1783c0e4 fix(cli): make --brain actually route to the named brain (#3576)
Co-Authored-By: Garry Tan <garrytan@gmail.com>
2026-08-01 10:06:17 +08:00
Garry TanandDiego e77a15f0d8 fix(windows): use native path boundaries and assertions (#3578)
Co-Authored-By: Diego <diegodearagao@gmail.com>
2026-08-01 10:06:17 +08:00
Garry TanandYMYD 522cfb032a fix(chunker): measured hard-split budgets + token-aware capByChars — the follow-up invited in #3477's merge review (#3564)
Co-Authored-By: YMYD <paul@ymyd.co.kr>
2026-08-01 10:06:17 +08:00
Garry Tananddialthewolff 273bd0e2be fix: scope residual federated reads (#3550)
Co-Authored-By: dialthewolff <jordan@jcwolff.com>
2026-08-01 10:06:17 +08:00
Garry Tan 7522b90db9 fix(search): implement documented relative since/until durations (#3442) (#3539)
Co-Authored-By: Garry Tan <garrytan@gmail.com>
2026-08-01 10:06:17 +08:00
Garry TanandTime Attakc 42f810c406 fix(search): raise hnsw.ef_search to match the vector candidate request (#3613)
Co-Authored-By: Time Attakc <89218912+time-attack@users.noreply.github.com>
2026-08-01 10:06:17 +08:00
Garry Tan 1ca1a14f24 fix(apply-migrations): stop reporting 'All migrations up to date' while schema is behind (#3085)
Co-Authored-By: Garry Tan <garrytan@gmail.com>
2026-08-01 10:06:17 +08:00
Garry TanandDiego 64c191b1d7 fix(build): force LF for Markdown too (frontmatter parsers anchor on LF) (#3517)
Co-Authored-By: Diego <diegodearagao@gmail.com>
2026-08-01 10:06:17 +08:00
Garry TanandDiego d49ea83db4 fix(minions): settle the spawn loop when a child never launches (#3536)
Co-Authored-By: Diego <diegodearagao@gmail.com>
2026-08-01 10:06:17 +08:00
Garry Tan d71d50503d fix(serve,import): stop the SIGINT orphan; quote imported frontmatter (#3618)
Co-Authored-By: Garry Tan <garrytan@gmail.com>
2026-08-01 10:06:17 +08:00
Garry TanandDiego a228776b8f docs(security): tighten responsible disclosure language (#3619)
Co-Authored-By: Diego <diegodearagao@gmail.com>
2026-08-01 10:06:17 +08:00
Garry TanandMammad M. c0b28f104d fix(sync): prevent dry-run pull and out-of-strategy page deletion (#3624)
Co-Authored-By: Mammad M. <mammad@digit.az>
2026-08-01 10:06:17 +08:00
Garry Tanandcvillarroel2 61ef727710 fix(contextual): route synopsis through configured models (#3678)
Co-Authored-By: cvillarroel2 <20239888+cvillarroel2@users.noreply.github.com>
2026-08-01 10:06:17 +08:00
Garry Tan 532e591655 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.
2026-08-01 07:40:48 +08:00