release: mex 0.8.0 - #167
Merged
Merged
Conversation
First phase of the MEX Wiki Engine: a Markdown-canonical knowledge graph
over the .mex scaffold. Markdown stays the source of truth and stays
Git-tracked; .mex/wiki.db will be a disposable projection of it.
This lands identifiers, the canonical TypeScript model and its validators,
plus the architectural lint that protects the phases building on it. No
Markdown parsing, no SQLite and no CLI yet — those need this model first.
Model (src/wiki/model/):
- ULID + branded EntityId. Monotonic within a millisecond, so ids sort in
creation order; never derived from a title or path, so renaming a
heading or moving a file costs nothing.
- Entity, relation, topic, source, grounding and operation vocabularies,
each with validators that return diagnostics rather than throwing — a
malformed file reports every problem in one pass, with stable codes,
locations and remediation.
- Lifecycle (governance) and grounding health (per-checkout, derived) as
two enums that cannot be assigned to one another, so local drift can
never rewrite what the team has agreed is current.
- Entity-scoped and file-scoped content hashes kept apart by name: the
first is an operation precondition, the second only a change signal.
Positions are UTF-16 code-unit offsets into decoded text, never byte
offsets. Markdown AST positions index a JavaScript string, and the two
diverge on any file containing non-ASCII prose; treating them as bytes
would corrupt documents in a content-dependent way. Nothing indexes a
Buffer with a parsed offset.
Architectural lint (test/wiki-architecture.test.ts):
- the model layer stays free of I/O, the code graph and higher layers;
- nothing re-serializes Markdown or a whole YAML map, so unrelated bytes
survive every write (one pre-existing exception is named and pinned);
- nothing under src/wiki/ writes a file outside the operation pipeline.
Each rule has a negative test proving it fails on a planted violation.
Adds .mex/wiki.db* to .gitignore. No new dependencies. 282 tests.
The oracle for the codec, built before the codec so it cannot be shaped
around an implementation.
Two harnesses, as pure functions with no test-framework dependency:
- checkRangePartition asserts that parsed ranges form a complete,
non-overlapping, in-order cover of the source. This is what catches
off-by-one errors; a round-trip test structurally cannot see them,
because re-emitting an unmodified buffer is that buffer.
- checkOnlyRangesChanged asserts that a write touched nothing outside
the ranges it declared, by comparing the complement. Every later
byte-preservation claim rests on it, so it treats a whitespace-only
or line-ending change outside the declared ranges as a failure —
reformatting is the bug it exists to catch.
Both report the offending range by label and the first divergence by
offset, so a failure says what moved and where rather than that something
did. 32 tests, including one deliberately broken example per failure mode.
The contract fixes the parse result shape: entities with populated
locations, explicit gaps, anchors, frontmatter key order and legacy
root-level fields, plus diagnostics. parseWikiMarkdown throws until the
next phase; partitionRanges is implemented, being pure bookkeeping over
the result, so "covered" cannot be quietly redefined later.
Two rules recorded there that the spec does not state. Positions are
UTF-16 code-unit indices, never byte offsets. And a body ends at the next
entity's metadata as well as at the next heading of equal or shallower
depth — forced by the partition property, since a file-level entity would
otherwise swallow the block entities inside it and the ranges overlap.
…he codec The oracle's data, written before the parser so it cannot be shaped around one. 37 fixtures on disk, an expectation for each, and the acceptance tests they feed — skipped and tagged TODO(P2b-codec) until the parser lands. The corpus covers structure (file-level, block-level, mixed, nested, sibling boundaries, EOF without a trailing newline, prose outside every entity, empty and frontmatter-only files), the parser-breakers (metadata inside fenced and indented code blocks, fences holding --- and ## lines, setext headings, non-ASCII, CRLF, a BOM, unusual spacing, real HTML), malformed input (bad YAML in both carriers, unbound and doubly-bound metadata, cross-file duplicate ids, merge-conflict markers), legacy fields, and a small realistic scaffold. Expectations name ranges by anchor text rather than by bare offsets, so they read as what they assert and a typo fails as "not found" instead of as a number that is quietly wrong. The encoding fixtures additionally carry absolute offsets, cross-checked against their anchors, because that is where an off-by-N would otherwise hide: the non-ASCII fixture is 294 UTF-16 units against 351 UTF-8 bytes, so a byte-oriented parser is off by 57 on it. The meta-tests are not skipped and must stay green: every fixture is claimed exactly once, every anchor resolves and is unique, described ranges are ordered and non-overlapping, heading depths agree with their markers, and the encoding fixtures still contain what makes them interesting. They caught three defects in these expectations while they were being written. .gitattributes marks the corpus -text. core.autocrlf is on here, and without it Git would normalize the CRLF and BOM fixtures on checkout — silently rewriting the thing under test and turning the CRLF fixture into an LF one that passes everywhere.
Implements parseWikiMarkdown against the P2a oracle: one AST pass for positions, metadata-to-heading binding, entity construction, explicit gaps, and inline anchor association. Adds the positions-only patch primitives the write path will use. Three position facts the AST does not give you for free, each verified against the corpus rather than assumed: - remark heading nodes stop before the line terminator, while the contract's range includes it — one unit for LF, two for CRLF, none at EOF. - remark strips a leading BOM, so every offset it reports is short by one for a file that has one. Corrected in a single seam (positions.ts) rather than at each call site, and never by stripping the BOM, which would leave the offsets addressing a different string than the caller passed. - a Git conflict marker is not inert prose: `=======` is a valid setext underline, so a conflict region parses as a depth-1 heading and would silently truncate whatever entity body contains it. Heading detection is suppressed inside conflict regions; the markers survive verbatim. Two binding rules the spec does not state, both forced by the partition property: a body also ends at the next entity's metadata (bound or not), and an intervening metadata block does not break the "only blank lines" rule — without that, the first of two stacked blocks reads as merely unbound and the second binds unopposed, which is the guess the rule exists to forbid. 342 of the 343 corpus tests pass. The one failure is an oracle defect, not a codec defect, and is reported rather than worked around: all eight scaffold fixture ids are 24-25 characters and four use O, U or L, so none is a valid 26-character Crockford Base32 ULID. Fixing it means editing the corpus, which this phase may not do. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017MH4JvoVPFK5Yc1BGQAz38
`writeGroundings` rewrote the whole frontmatter block through `YAML.stringify` on every grounding write, losing comment placement, quoting style and key order. It now splices the `grounds_to` key's own range, so a one-line grounding update produces a one-line diff and the scaffold stays reviewable in an ordinary pull request. Its lint exception is removed and the exception list is empty; the guard test was replaced rather than deleted, so the count can only stay at zero. Adds the nine-case `spliceTopLevelKey` matrix, each case asserted with the P2a scoped-mutation harness rather than by eyeballing output — a whole-map rewrite can produce byte-identical output on a simple fixture and still destroy a comment in the next file along. The lint rule then caught a real violation in the codec itself: it was rendering the parsed `mex` value back to YAML text just to parse it again. Frontmatter metadata is now passed to the binder already parsed, so nothing round-trips through a serializer. Emits the three P2-owned diagnostic codes and removes them from NOT_YET_EMITTED. ANCHOR_GROUNDING_MISMATCH is retagged P9: the codec associates anchors with entities but cannot compare one against a grounding without a declared equivalence and a live graph, and no fixture asks for it. Full suite: 1150 pass, 3 skipped, 4 fail — the documented pre-existing set (cli, cli-agent, config x2, and graph-integration under parallel load, all verified in isolation) plus the scaffold-id corpus defect reported in the previous commit. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017MH4JvoVPFK5Yc1BGQAz38
…gion rule Direct tests for the splice primitives, the position seam (BOM shift, terminator widths, line mapping, conflict regions) and anchor association and rewrite -- 14 tests. The read side was covered only through the corpus until now, so a regression in the seam would have surfaced as a puzzling fixture failure rather than as the thing that broke. Records the conflict-region rule in contract.ts as the third rule the spec does not state, and updates WikiEntityLocation.bodyEnd to document the three-way body extent, so the model and the contract no longer disagree. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017MH4JvoVPFK5Yc1BGQAz38
…le to catch them A sanctioned oracle correction, authorised by the director session and recorded in the implementation plan at §3c.1. The corpus is normally immutable to the phase being judged by it — that immutability is what makes the P2a oracle worth having — so the reason it moved belongs in the history. **What was wrong.** All eight entity ids declared by the scaffold fixture were not entity ids. Six were 25 characters and two were 24, against the 26 that spec §8.1 and `model/ids.ts` require, and four used `O`, `U` or `L`, which Crockford Base32 excludes precisely because they are confusable. They had been hand-typed to read as words — ROUTER, GATEWAY, DECISION — which is how the defect got in and why it survived review: they look like ids. The codec runs the model's entity validator, as it must — `explicit-null.md` exists to assert that a validation failure becomes `WIKI_PARSE_ERROR` — so all eight entities were rejected and the scaffold acceptance test could not pass for any implementation. P2b left it red and reported it rather than weakening id validation to go green, which would have let malformed ids into the index and defeated a HARD invariant. **The fix.** Eight ids generated with the model's own `createUlidGenerator`, spread across simulated days so they differ visibly rather than only in their last two characters — near-identical ids make a copy-paste slip between two entities invisible to a reader. Replaced across all 23 occurrences in six files: these ids appear as relation targets and topic references, not only as declarations, and the topic id is referenced from five files. **The root cause, which is the more important half.** P2a's meta-tests asserted that each id appeared exactly once, but never that it was well-formed. So eight invalid ids passed a full verification pass — the builder's, and the director's. An oracle that cannot fail on its own data is not an oracle. Two meta-tests now assert id format using the model's own `isEntityId` guard rather than a regex written locally, so the corpus and the engine can never drift to different notions of a valid id. One covers the scaffold, one covers every per-fixture expectation, because the gap was generic and not specific to the scaffold. **Two tests stopped being vacuous.** With every entity dropped, "partitions every scaffold file" held trivially over an all-gaps file, and "finds every declared entity" compared two empty lists. Both are now genuinely exercised: the scaffold yields 8 entities across 6 of its 8 files, zero diagnostics, and every file partitions with real entity ranges. No codec defect surfaced in multi-file handling. Corpus diff against e177d85 is confined to the eight ids: 23 changed lines, every one of them an id line, in the scaffold files and `SCAFFOLD_FIXTURE.entityIds` only. All 181 acceptance tests now pass. Suite: 1167 pass, 3 skipped, standing failures only. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_017MH4JvoVPFK5Yc1BGQAz38
… publish, refresh The write side of P3. The index holds no truth of its own: delete `wiki.db`, rebuild from Markdown alone, and every entity, relation, topic, source, grounding and diagnostic comes back. **The determinism test drove the design, and the dump was built before the refresh path.** A normalized dump after 50 incremental mutations — file creation and deletion, entity addition and removal, and a relation whose target is deleted and then restored — is byte-identical to a dump after a clean rebuild of the same final state. Written first on purpose: a dump built afterwards gets shaped, one reasonable concession at a time, into something that agrees with whatever refresh happens to produce. **What makes refresh safe to believe is the split, not the diffing.** Everything one file determines is written per file and dies with it. Everything the *set* determines — which claimant of a duplicated id wins, whether a relation target exists, whether a topic reference resolves — is recomputed from the rows by `resolveIndexState`, which both the rebuild and the refresh call. The only thing refresh skips is parsing. So a deleted file dangles the references into it and a restored file resolves them again with no case handling for either, and there is no second resolver to disagree with the first. Refresh re-walks for the same reason: a cheaper answer that differs from the rebuild's is worth nothing. **Entity rows are keyed by location, not by id.** `DUPLICATE_ENTITY_ID` is a required diagnostic and a duplicate is normally two files copied from one another, so the index has to be able to *hold* both claimants in order to report them; keying on the id would make the second insert fail and degrade the report into whichever was written first. `entity_key` is `<file>#<padded offset>`, so the winner is `MIN(entity_key)` — a total order over content, not over insertion. `shadowed` marks the rest and is recomputed, so deleting the winner's file promotes the survivor with no reparse. **The schema is an inlined string, not a `.sql` asset.** `src/graph/assets.ts` exists because a runtime-resolved schema is not in `dist/` unless a build script copies it — its own header calls that "the #1 way a tree-sitter CLI ships broken". A constant cannot fail to be packaged. The test asserts there is nothing left to resolve, and that the declared tables match the enumerated ones. **FTS covers §3c finding 23.** A file-level entity's body stops at the first nested entity and never resumes, so indexing `entity.body` alone leaves every paragraph after the first nested heading unreachable through the entity that presides over the file. The file-level entity adopts the file's gaps — the regions no entity claimed — and nested bodies are *not* copied upward, which would destroy title-beats-body ranking the moment a file gets long. The fixture fails under the naive implementation. Also here: - `wiki.exclude` / `wiki.readOnly` (D10) loaded in `src/config.ts` beside their siblings, with the defaults always populated so no consumer has to remember them, and every malformed shape degrading to defaults rather than throwing — config parsing is on the path of every mex command. - Discovery sorts explicitly. `readdir` order differs between this box and CI, and the failure it causes presents as an incremental-refresh bug. - A symlink escaping the scaffold is a diagnostic, not a followed link, matching the rule P5 applies to writes. New code `PATH_OUTSIDE_SCAFFOLD`; the read side and the write side must not disagree about what "inside the scaffold" means. - `src/wiki/index/dbfile.ts` is the one module that touches the filesystem, and the lint's exemption for it is narrower than the hole it fills: every mutation routes through a runtime guard that rejects any path which is not a database file, the rule asserts the guard is in front of everything, and the write-call list now covers `renameSync`. `src/wiki/query/` is also barred from importing the rebuild, refresh and publish paths, so "a read never rebuilds" cannot be undone by a later convenience. - The diagnostic coverage test moved to `src/wiki/__tests__/` — §3c finding 27 named a third emitting layer as the condition, and P3 is it. Three codes left `NOT_YET_EMITTED`; the map only shrank. Grounding is stored and not resolved: `wiki_groundings.health` stays NULL, because resolving it needs a live graph and that is P4's. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…ds that hold P3's second half. Every §10.4 ordering rule, each with a test that fails if the rule is dropped, plus the two bounds that are stated as prohibitions and are therefore the easiest to lose. **Ordering lives in pure functions, not in SQL.** Ordering is where a retrieval system rots quietly — nothing throws, results still arrive, and the right answer is merely second — so each rule is a comparator that can be asserted directly. The order is total at every tier and none of them falls through to rowid: exact id, then match field, then lifecycle, then health, then title, then id. **Search precedence is tiered queries, not a relevance score.** §10.4 requires a categorical order — no quantity of body matches may outrank one title match — and a score always eventually lets it. Three column-filtered FTS queries in sequence give the required order by construction. User text is escaped into a quoted AND expression before it reaches MATCH, so `NEAR(` and an unbalanced quote are text rather than a syntax error or somebody else's query. **Stale lowers rank and never hides.** It is a tiebreaker and no code path turns it into a filter: an entity that vanishes because it is stale is indistinguishable, to the user, from one that does not exist — and that sends them off to write a second copy of something they already have. P3 resolves no grounding, so `health` is null here in practice; the rule is implemented and tested anyway, because retrofitting a ranking rule once the data arrives is how it gets missed. P4 populates the column. **Unbounded is inexpressible, not merely discouraged.** `resolveBounds` has no "all" case, every statement carries a `LIMIT`, and a test scans the directory's SQL and fails on a `SELECT` from a wiki table without one — so the invariant is checked over the code rather than over the paths someone thought to exercise. A behavioural test builds more entities than the hard maximum and asserts the maximum holds. Budget composes with the graph rather than competing with it: `estimateTokens` and `BudgetLedger` come from `src/graph/agent-protocol.ts`, because P7 has to fuse wiki and graph results under one ceiling and two estimators would make that response's accounting wrong in both directions. What is added is D10's 4,000-token neighbourhood default and the count bounds. One bug the tests caught, worth recording because the symptom was invisible: edge lists were fetched with the *result* limit, so asking for one node also fetched one relation — a bound quietly changing the answer instead of truncating it. Edges now have their own bound. Also here: `ENTITY_NOT_FOUND` emitted and removed from `NOT_YET_EMITTED`, and a calibration test that fails at **10x** D10's targets rather than at target, because a perf test that fails on a loaded CI box gets disabled and then protects nothing. It catches the accidental O(n²) — a refresh that reparses the scaffold, a query that lost its index — which are one to three orders of magnitude and show up plainly. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
# Conflicts: # .mex/ROUTER.md # .mex/patterns/INDEX.md # package.json
… tighten the guard regex
Three defects from the P3 review. All three were real; verified rather than
taken on report.
**Refresh dropped read-error diagnostics for files outside the changed set.**
`resolveIndexState` rewrites the whole global diagnostic set on every build. A
rebuild supplied read errors for every file; a refresh built its list inside the
loop over the changed set. So a file that is present but unreadable — a
permissions change, or a walk/read race — was reported by a rebuild and silently
dropped by any later refresh of an unrelated file. That is a refresh/rebuild
divergence, which is precisely what D5 exists to exclude.
Read errors are now file-scoped, which is also the more honest shape: "this file
could not be read" is a fact about the file, not about the build that happened to
notice. It survives a refresh that does not touch it, is cleared when the file is
next read successfully, and a clean rebuild derives an identical row. `parseAll`
returns `{ parsed, unreadable }` so a caller cannot route a read error back into
the global set by accident.
**The oracle could not see it, and now can.** The 50-mutation script never makes
a file unreadable. Two cases added, with the failure injected through an
optional `readFile` on both option types rather than staged on disk — there is no
portable way to make a file unreadable on every machine the suite runs on, and a
test that is inert on the dev box protects nothing there. Verified by reverting
both halves of the fix and watching the first case go red, then restoring.
Stated in the handoff rather than glossed: only the first of the two new cases
is a regression detector for this defect. The second passed even with the defect
present; it earns its place for the clearing behaviour.
**The dump's table coverage was correct but unasserted.** `DUMP_TABLES` is
maintained by hand, so a table added to the schema would fall outside the oracle
with nothing failing — and P4 adds one. Three tests now derive both sides:
tables against the schema, and columns against `PRAGMA table_info` on the live
database rather than against the SQL text, so a column added by a later
migration is caught too. That forced a correction to `DumpExclusion` — three of
its four entries are `wiki_meta` rows, not columns, and conflating the two would
have made the column check expect columns that do not exist.
**A regex nit that weakened a rule in the dangerous direction.**
`/\assertIndexPath\s*\(/g` — `\a` is an identity escape for a literal "a", so
`xassertIndexPath(` matched. It counts guards, so a false match inflates the
count and hides violations. Now `/\bassertIndexPath\s*\(/gu`, with a negative
case. Confirmed by hand that the old pattern matched and that adding `u` to it
threw.
Suite: 1229 pass, 3 skipped, standing failures only. Typecheck and build clean.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The value that answers "has this grounded code changed?" was captured at saveCurrentBaseline and written only into `_mex_grounded_source` inside .mex/graph.db. That index is gitignored and disposable by invariant -- `mex graph rebuild` is offered as a routine repair -- so the drift baseline did not survive a rebuild and never reached a teammate who cloned. The groundings still resolved, so nothing looked broken; they simply stopped being able to detect a body change. Add an optional `bodyHash` to the shared `Grounding` type and populate it at both graph-lane write sites from the hash saveCurrentBaseline already reads. Optional and additive because every grounding written before this lacks it, and a required field would turn each of those scaffolds into a parse error. `refreshGroundingBaselines` writes it in two cases only: backfill, when Markdown carries none -- which asserts nothing new, since the graph.db row written on the same line already holds exactly that value from exactly that moment -- and re-baseline, when an authorized `updateFingerprints` has just moved the identity signal too. A hash that merely differs is left alone: that difference is the drift, and overwriting it would erase the finding. `persistMovedGroundings` re-derives it on a confirmed rebind, alongside the fingerprint, because a symbol's name is part of its body. Two reads had to follow. The drift checker took the baseline only from graph.db, so after a rebuild it found none and reported nothing at all -- without this the field would have been inert for `mex check`, which is the command that actually reports drift. It now prefers the committed hash and falls back to the cache for groundings authored before the field existed. `isGroundingArray` already tolerated the key and is documented rather than widened; the round trip through writeGroundings needed no change. The graph.db row stays. It is now a cache of a canonical value, and it still carries the body text that drift review needs for a diff.
…graph.db Four cases, run against the thing that used to destroy the baseline: delete .mex/graph.db, rebuild it from source, then ask whether an edited constant is still reported as drift. Two of them fail on the pre-fix tree and two pass, deliberately. The two that fail are the fix. The two that pass are the backward-compatibility pins: a grounding with no committed hash still parses and resolves and simply reports nothing after a rebuild -- which is the defect stated as a test -- and the same grounding against a live index still detects drift from the cache, so an existing scaffold that never rebuilds behaves exactly as it did. The fourth checks the case that would have been easy to get wrong: a capture pass run over a grounding whose body has already drifted must not adopt the new body as the baseline. The difference is the finding.
…d at `groundingsUnverified` means "there were groundings and not one produced a verdict", and that has two causes: no graph was supplied, or one was and nothing could be compared against it. The notice asserted the first unconditionally, so a reader was sent to inspect a code graph nobody had looked at -- the same shape as the OperationLogPathError mislabelling, a message stating a cause nobody checked. Carry the discriminator. It has to be carried rather than inferred from the grounding diagnostics, because those are subject to the same bound as every other diagnostic and a truncated report would lose the evidence. The wording of the no-graph branch was also wrong, and that is the half that was costing something. `runValidate` builds its options from serviceOptions, which carries no graph, so wiki validate has never loaded one and this is the branch every CLI run takes. "No code graph in this checkout" is a claim about the repository that the command never checked, and it was printed at a user whose graph was fresh and 7 MB. It now says what was true of the pass and names `mex check`, which is the command that does resolve groundings. That wiki validate cannot check groundings on the CLI at all is a separate finding and is left as a residue; this commit only stops it lying about why.
… root
Found on a real migrated scaffold, and it makes the rest of this branch inert
for the only population that matters in practice.
`mex check` read `frontmatter.grounds_to` directly. A pre-wiki scaffold keeps
the key there, but once `wiki migrate` adopts a file as an entity, section
13.4 moves it under the `mex` map -- and the direct read then finds nothing.
Measured on a migrated scaffold with a fresh graph and sixteen groundings
across nine files: the root key parsed as null, `extractGroundings` found
four in one file alone, and `mex check` returned zero GROUNDING_ codes of any
kind. Not stale, not missing, not gone. Silent.
That is worse than a false positive, because a scaffold that checks clean is
one nobody looks at.
Two reads, both now going through `extractGroundings`, which resolves the key
path the same way the writer does so the two ends cannot disagree:
- the checker's own loop, which ran zero times;
- the `hasGroundings` gate in the drift pipeline, which decided the
grounding runtime was irrelevant and never opened it, so the checker was
not even constructed.
The frontmatter value stays as the fallback for a file that cannot be re-read
at that point, which is the only case the old path still covers.
Measured end to end on a copy of that scaffold, before and after. Both report
"16 captured, 0 skipped". Before: 0 groundings carry a body hash, and an edit
inside a grounded function body after a graph rebuild yields 0 issues. After:
16 carry one, and the same edit yields 2 GROUNDING_DRIFT -- two because that
function is grounded from two files.
… gap The pattern is the part of this round worth reusing: a value that answers "has this changed?" belongs in Git, the index copy is a cache of it, every reader must resolve the same key path as the writer, and a fix that ships only the write half looks complete while doing nothing. ROUTER records what now works and adds the known issue this round measured but did not fix: the Wiki CLI's serviceOptions carries no code graph, so validate cannot resolve a grounding and migrate's backfill never runs. Both degrade silently.
MALFORMED_GROUNDING is one code over six problems -- a missing node id, a missing fingerprint, a malformed node id, a malformed fingerprint, a non-canonical `file`, a bad `verifiedAt`, and finding 39's missing body hash. Remediation is attached to the code, and the registry text is written for the first two: "a grounding needs both a code-graph node id and a fingerprint". The body-hash warning has both. Reported on a real scaffold where all sixteen warnings carried that line, telling the reader to supply fields that were already in the file and never naming the one that was absent. It is the same shape as the notice fixed two commits ago: advice about a cause nobody checked. `remediation` is already a per-call override on `diagnostic()` and the call site simply never used it. One option, no new code, no new field, no change to the registry -- the other five cases keep the text that does fit them. The replacement names what is missing, says plainly that no amount of hand editing supplies it, and names the commands that do.
…ry normalizes
`68c2663` and `342ca8b` fixed the Windows lockout by comparing `scaffold_id`
instead of comparing the working `.mex/config.json` against its blob byte for
byte. That fixed the defect and silently dropped a property the byte comparison
was also carrying.
The whole tracked config is attested, not just its identity. A local edit to any
field — `scaffold_name`, say — means teammates are reading something this
checkout is not, and Team workflows are correctly unavailable until it is
committed. `test/cli.test.ts` asserts exactly that, and CI on ubuntu caught it:
keeps advertised grounded Spec reads available when only Team config
attestation changes
→ expected { id: 'team_workstreams' } to match { availability: 'unavailable' }
Neither Windows run could have caught it, because both were failing earlier for
the reason those commits were fixing.
Byte equality is restored in both places, and it now compares cleanly across
platforms for a different reason: `baf5698` landed afterwards and undoes Git's
checkout line-ending conversion inside `tryReadContainedArtifact`, which is how
both of these read the working config. The CRLF working copy and its LF blob
therefore agree here without either check knowing anything about line endings.
That is the better division. One boundary knows about Git's transform; every
consumer keeps its own exact semantics. Comparing `scaffold_id` alone would also
have survived the line endings — it just quietly answered a weaker question.
`scaffoldIdentityOf` stays: it still replaces the duplicated inline parse-and-
bound of `scaffold_id`, which was the other half of those commits and is
unaffected.
Verified: the failing CI test passes; both CRLF checkout tests still pass; the
identity-changed suppression test still passes; and a real macOS-authored
repository cloned to Windows, still CRLF on disk, still reports all eleven
capabilities available.
Restore whole-config attestation after the Windows identity fix. CI on ubuntu failed the previous merge with one test: "keeps advertised grounded Spec reads available when only Team config attestation changes", expecting team_workstreams to be unavailable when the working .mex/config.json has drifted from HEAD. The Windows fix had replaced that byte comparison with a scaffold_id comparison, which fixed the lockout and silently answered a weaker question -- the whole tracked config is attested, not just its identity, and a local edit to any field means teammates are reading something this checkout is not. Neither Windows run could have caught it: both were failing earlier, for the reason those commits were fixing. Byte equality is restored in both call sites. It compares cleanly across platforms now for a different reason: baf5698 undoes Git's checkout line-ending conversion inside tryReadContainedArtifact, which is how both reads obtain the working config, so a CRLF working copy and its LF blob agree without either check knowing anything about line endings. One boundary knows about Git's transform; every consumer keeps its own exact semantics. Verified: the failing CI test passes; both CRLF checkout tests pass; the identity-changed suppression test passes; and a real macOS-authored repository cloned to Windows, still CRLF on disk, reports all eleven capabilities available.
The Hub had no outbound link of any kind. This is the first, and it sits in the repository bar ahead of the branch so it reads as ambient rather than as a prompt — a step quieter than the repository facts beside it, reaching full contrast only on hover. The invite is a permanent vanity link on purpose. A default Discord invite expires, and this URL is compiled into published builds that people keep running for months after they install them. The mark is inlined rather than imported: lucide-react carries no brand icons, and a brand glyph is not worth a dependency. Unlike every other icon in the bar it is filled rather than stroked, so it sets fill and clears stroke explicitly. Below 1190px the label is hidden and the mark stands alone, so the accessible name is set explicitly and matches the visible label word for word — without it the link would go nameless at exactly the width where it is hardest to guess. The test asserts rel="noopener noreferrer" rather than assuming it: target "_blank" without it hands the opened tab a handle on this one. Verified by removing the attribute and watching the assertion fail.
…lour It is the only control in the repository bar — everything beside it is a readout — so it is now drawn as one, and that difference is visible before the label is read. It borrows the status pill's metrics to sit on the same optical line and takes a squarer radius so it does not read as one more status. The mark carries Discord's blurple and the label does not. The glyph is what identifies the destination; a fully branded button would outrank the repository state, which is what the bar is actually for. The colour is converted from #5865F2 rather than approximated, and lives as its own token: it is a brand colour, not a semantic one, so nothing else should reach for it as an accent. Narrow layouts keep the box and square up around the glyph rather than collapsing to a bare icon, so it still reads as something to press.
A card below Latest team memory, opening a hosted form. The Hub never sees the address, and that is structural rather than a shortcut: it serves itself under connect-src 'self' and form-action 'self', so an input here could not submit anywhere without loosening the rule that makes "Runs locally" in the sidebar true. Opening a link is a navigation, not a connection, so it stays inside the policy. The aside is now an explicit column. Activity used to be the only card on that side and placed itself; stacking a second span-4 card by auto-placement would have dropped it to the next row's first column rather than under the one above. Drawn quieter than the panels above it — no fill, dimmer border, secondary body copy. It is the one card here that asks for something rather than reporting something, and it should never read as a status the reader has to resolve. "Not now" persists in this browser and nowhere else. Reads and writes are both guarded: storage throws outright in a private window or where site data is blocked, and a card that cannot remember a dismissal is a far smaller problem than an Overview that will not render.
"Not now" and the stored preference behind it are gone; the card stays on every visit. That is a trade rather than a simplification, and it moves the burden onto the card's tone. Anything that cannot be put away has to be worth living with, so this one states its offer once and never asks twice — no badge, no count, nothing that reads as unresolved work sitting in the aside.
…he fingerprint `setup-grounding-e2e` caught this on CI and it is the same coupling this field exists to break, committed by the writer that introduces the field. The re-baseline was gated on the fingerprint having changed. Editing a constant changes the body and leaves the fingerprint identical -- by construction, since the fingerprint is an identity signal over normalized structure. So on exactly the edit a body hash is for, the gate never opened, the new baseline was never written, and drift could never clear. `mex sync` would have reported the same drift forever after an agent had already repaired the prose. Authorization is the caller's `updateFingerprints`, which is what actually carries the intent: `mex sync` passes true after an agent pass and re-baselines deliberately; `mex ground` and setup pass false and must never overwrite a hash that differs, because that difference is the finding. The new tests now pass `updateFingerprints: false` explicitly. That is what `captureGroundingBaselines` normalizes a missing value to before it reaches this function, so the tests exercise the real command path instead of the looser default a direct caller gets.
…rkdown fix(grounding): commit the body hash to Markdown, and make the reads that use it agree
The card previously led with a warning about pre-1.0 breakage and asked for an email. It now asks for the thing actually wanted — answers — and says so in the project's own voice. The mail glyph goes with it: what the button opens is a form, not a mailbox, and an envelope promised the wrong thing. The trailing arrow stays, because it is now the only cue that the button leaves the Hub for a new tab. The footnote about nothing being sent from this machine is gone too. It was answering a worry the card no longer invites: with no address asked for on this side, there is nothing to reassure anyone about.
…rawn one The first version restyled the link from scratch and put 705 bytes of rules into the entry stylesheet — enough on its own to breach the initial-CSS budget in CI, which had been passing on the base commit with 406 bytes of headroom. Reusing the outline Button removes all of it but one declaration. Measured on the built bundle: initial CSS 71270 -> 70628 against a 70902 budget. It is also the more consistent answer, since every other link-shaped control in the Hub is already that button. Only the mark's colour stays ours, and `role="link"` is set back deliberately. The Button announces as a button even when it renders an anchor, which is right for the in-app navigations that use it — but this one leaves the Hub for another site, and that is exactly when a reader needs to hear "link" before following. Worth recording for the next person who adds to this bar: `shell.module.css` is in the entry chunk and its cost is charged to every page, while a page's own module is not. The card added in this branch is larger and cost nothing here.
Hub: Discord invite and a feedback card
feat: ship managed MEX agent skills
…bility ci: confirm release performance on fresh runners
…' into codex/setup-onboarding-v08
Complete fresh setup onboarding through Hub readiness
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Validation
Notes
See RELEASE_NOTES.md for the concise release overview.