Skip to content

release: mex 0.8.0 - #167

Merged
theDakshJaitly merged 272 commits into
mainfrom
integration/human-team-memory-v1
Sep 2, 2026
Merged

release: mex 0.8.0#167
theDakshJaitly merged 272 commits into
mainfrom
integration/human-team-memory-v1

Conversation

@theDakshJaitly

Copy link
Copy Markdown
Collaborator

Summary

  • release mex-agent 0.8.0 with Project Hub and governed team workflows
  • complete fresh setup through agent population, Graph and Wiki readiness, Git checkpoint, and Hub handoff
  • ship official mex-inbox and mex-relay skills for Claude Code and Codex
  • promote the changelog, refresh compatibility metadata, and add concise 0.8.0 release notes

Validation

  • npm run typecheck
  • 51 focused version, capability, and CLI tests
  • packed npm install, setup, Graph, Wiki, Hub, and agent-skills smoke
  • manual fresh setup against a throwaway local project
  • final package inspection: 160 files, approximately 3.69 MB packed

Notes

  • Node.js 22.5 or newer remains required
  • Nix package version is updated to 0.8.0; nix build was not run locally because Nix was unavailable
  • publish only the root mex-agent package after this PR is green and merged

See RELEASE_NOTES.md for the concise release overview.

theDakshJaitly and others added 30 commits August 23, 2026 04:13
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>
Yashasvi2229 and others added 26 commits September 1, 2026 23:10
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
Complete fresh setup onboarding through Hub readiness
Copilot AI lite review requested due to automatic review settings September 2, 2026 15:38

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

@theDakshJaitly
theDakshJaitly merged commit c3383a1 into main Sep 2, 2026
16 of 18 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants