mirror of
https://github.com/garrytan/gbrain.git
synced 2026-08-14 00:48:18 +00:00
master
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 |
||
|
|
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
v0.42.73.1
|
||
|
|
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 |
||
|
|
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. |
||
|
|
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. |
||
|
|
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
|
||
|
|
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
|
||
|
|
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> |
||
|
|
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 <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. |
||
|
|
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>
|
||
|
|
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> |
||
|
|
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 ``, 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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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.
|
||
|
|
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. |
||
|
|
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> |
||
|
|
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. |
||
|
|
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. |
||
|
|
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> |
||
|
|
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 |
||
|
|
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 |
||
|
|
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 |
||
|
|
72ec53bb49 | v0.42.71.0 feat(ci): publish GitHub releases on every version bump — organized notes + binaries, self-update unbroken (#3521 #3716) | ||
|
|
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. |
||
|
|
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> |
||
|
|
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. |
||
|
|
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 | ||
|
|
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. |
||
|
|
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. |
||
|
|
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. |
||
|
|
78391e8b64 |
fix(extract): resolve cross-directory wikilinks via sync-consistent slugification (#3161)
Co-Authored-By: Garry Tan <garrytan@gmail.com> |
||
|
|
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> |
||
|
|
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> |
||
|
|
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> |
||
|
|
8e3699156f |
fix(sources): honor unfederate for unqualified reads (#2928) (#3533)
Co-Authored-By: Garry Tan <garrytan@gmail.com> |
||
|
|
cc1783c0e4 |
fix(cli): make --brain actually route to the named brain (#3576)
Co-Authored-By: Garry Tan <garrytan@gmail.com> |
||
|
|
e77a15f0d8 |
fix(windows): use native path boundaries and assertions (#3578)
Co-Authored-By: Diego <diegodearagao@gmail.com> |
||
|
|
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> |
||
|
|
273bd0e2be |
fix: scope residual federated reads (#3550)
Co-Authored-By: dialthewolff <jordan@jcwolff.com> |
||
|
|
7522b90db9 |
fix(search): implement documented relative since/until durations (#3442) (#3539)
Co-Authored-By: Garry Tan <garrytan@gmail.com> |
||
|
|
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> |
||
|
|
1ca1a14f24 |
fix(apply-migrations): stop reporting 'All migrations up to date' while schema is behind (#3085)
Co-Authored-By: Garry Tan <garrytan@gmail.com> |
||
|
|
64c191b1d7 |
fix(build): force LF for Markdown too (frontmatter parsers anchor on LF) (#3517)
Co-Authored-By: Diego <diegodearagao@gmail.com> |
||
|
|
d49ea83db4 |
fix(minions): settle the spawn loop when a child never launches (#3536)
Co-Authored-By: Diego <diegodearagao@gmail.com> |
||
|
|
d71d50503d |
fix(serve,import): stop the SIGINT orphan; quote imported frontmatter (#3618)
Co-Authored-By: Garry Tan <garrytan@gmail.com> |
||
|
|
a228776b8f |
docs(security): tighten responsible disclosure language (#3619)
Co-Authored-By: Diego <diegodearagao@gmail.com> |
||
|
|
c0b28f104d |
fix(sync): prevent dry-run pull and out-of-strategy page deletion (#3624)
Co-Authored-By: Mammad M. <mammad@digit.az> |
||
|
|
61ef727710 |
fix(contextual): route synopsis through configured models (#3678)
Co-Authored-By: cvillarroel2 <20239888+cvillarroel2@users.noreply.github.com> |
||
|
|
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. |