Skip to content

SkillSpector install gate, atomic runtime swap, picker primitives, ego-browser always-on - #122

Merged
NagyVikt merged 20 commits into
mainfrom
fix/oauth-identity-desync
Aug 7, 2026
Merged

SkillSpector install gate, atomic runtime swap, picker primitives, ego-browser always-on#122
NagyVikt merged 20 commits into
mainfrom
fix/oauth-identity-desync

Conversation

@NagyVikt

@NagyVikt NagyVikt commented Aug 7, 2026

Copy link
Copy Markdown
Contributor

Consolidates the working branch. Fifteen commits; the six added here are the
previously-uncommitted working tree, grouped by area.

ego-browser actually loads now

core declared browser/ego-browser, but the per-project loadout deferred it in
every project where no signal matched — and signals come from package.json deps
and framework detection, none of which say "browser". The declaration was real
and the skill still never loaded. It joins ALWAYS_KEEP, plus the matching slug
set in profile-merge so a budgeted composite can't drop it either.

Verified on a neutral cwd with zero project signals: classifies full, where it
previously landed in deferred.

SkillSpector install gate

Every path that lands a new skill on disk (skills add, discover install,
marketplace install-skill) now scans it before anything registers it to a
profile. Policy reads the report's recommendation rather than the exit code, so
DO_NOT_INSTALL blocks, CAUTION registers with a warning, SAFE is quiet.
install-skill deliberately differs — the files are already on disk, so a block
reports and exits non-zero rather than deleting. Missing scanner degrades to
cue's own rules and says so.

Runtime swap no longer unresolves the live path

Rematerializing did rm -rf runtimeDir then renamed the new tree in, leaving
the live path nonexistent for the whole recursive delete. A running session
resolves its hooks through that path, so hooks firing in the gap died with
"No such file or directory" (nine Stop hooks at once, 2026-08-03). Now the old
tree moves aside first.

Picker primitives

card.ts and palette.ts had grown duplicate drawing code; both now hang off
one pure, TTY-free set of primitives in picker/ui.ts.

Verification

bun test on this branch: 3004 pass, 1 skip, 1 fail (3006 across 224 files).

The one failure is smart-loader-suggest hook > flags a skill whose MCP the active profile lacks. It is pre-existing, not introduced here — on a clean
main worktree (submodules initialised, deps linked) that same test file fails
9 times, including this exact test. The branch's failing set is a strict
subset of base's, so this adds zero new failures and clears 8.

Caveat worth knowing: this suite is flaky. Two consecutive runs of the same
tree gave 3 fail + 1 error, then 1 fail — the cue cli list --all-profiles
and the sub-200ms hook-budget assertions are timing-sensitive.

Not included: resources/skills and resources/mcps carry uncommitted
submodule content and are intentionally left out of this PR.

NagyVikt and others added 20 commits July 27, 2026 01:22
…ith rules

The suggestion engine ranked profiles from five hand-maintained sources —
dependency rules, path conventions, combo history, recents, featured. Between
them they cover 19 of 85 profiles. The other 66 could only ever surface if the
user had launched them before, so a directory that genuinely wanted one had no
way to say so, and cycling past the third suggestion ran out of answers.

profile-match scores every profile's OWN vocabulary (name, description, skill
ids, MCP ids) against what the directory reveals about itself (dependencies,
languages, marker files, entry names). Coverage goes to 85/85 and the card's
tail keeps landing on something the repo justifies. Wired in as a new `matched`
origin scored 8-30, so curation still leads: it can pass `featured` from ~0.32
strength but never outranks a detection, a confirmed combo, or something
launched in this very directory.

Four properties earned their place by being wrong first:

  Absolute strength, not relative to the run's best hit. Normalizing against
  the top scorer manufactures confidence from noise — a directory with nothing
  to say still produced a 1.00 "match", because the weakest signal present is
  still the strongest signal present. cue and gitguardex now correctly match
  nothing.

  Corroboration: a filename alone never carries a match. Every repo here has a
  CLAUDE.md, so every repo matched `claude-api` — above a real ROS workspace
  backed by an actual robot.urdf. Chasing each such word with the stopword list
  was a losing game; requiring one dependency, language, or marker hit ends the
  class.

  IDF weighting plus size damping, so `gstack` (70 terms, mentions everything)
  cannot beat `rust` (18 terms, mentions Rust) on a Cargo.toml by surface area.

  Both sides normalize through the same `tokenize`. Skipping it on the evidence
  side meant a profile indexed "robotic" while EXT_LANGUAGE emitted "robotics" —
  they never met, silently. Same failure class as the bash/TS drift the hook
  guards against.

gstack itself goes to _featured.yaml rather than being made detectable, because
it structurally cannot be: it is a WORKFLOW profile, describing how you want to
work, and repo evidence only ever describes what the project IS. A .urdf says
robotics; nothing on disk says "role-routed engineering". It scored 0.27 at
rank #6 across the test repos; always-available is the honest mechanism.

Also: `-js` is no longer folded as a plural (medusajs -> medusaj, nextjs ->
nextj), which affected skill matching too; the df cut is skipped below 10
profiles, where it discarded every term shared by two and matched nothing; and
manifest metadata keys are filtered, so `requires-python` and `[urls] issues`
stop reading as dependencies named "research" and "linear".

Verified on real repos: agv_stack surfaces ros2 #1 (previously unreachable),
api-tester surfaces python + backend-base, kolarortopedia surfaces postgres +
supabase, cue and gitguardex correctly surface nothing. 2819 pass / 0 fail.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
src/commands/handoff.test.ts registered mock.module("../lib/handoff", ...).
Bun's module mock lives in a PROCESS-GLOBAL registry that outlives the file
that installs it, so whenever this file landed in the same worker before
src/lib/handoff.test.ts, that file asserted against the stub instead of the
real formatHandoffForAgent. The stub emitted only the header and the task
summary, so exactly the four sections it omitted failed:

  Most useful skills / Also helpful / MCPs used / Notes

Green locally, red in CI, purely on file ordering.

Drive the command through spawnSync with XDG_CONFIG_HOME pointed at a temp
dir instead. HANDOFFS_DIR is a module-level const baked from that env var at
import time, so a fresh child picks up the temp dir and no global state is
touched. Nothing is left to leak — this was the last mock.module in the repo.

Also stronger: the router now runs against the real lib rather than a stub.
10 -> 18 tests, covering the --json branches, --skills level parsing, the
--from default and the unknown-subcommand fallback.

Verified: bun test --timeout 30000 (the CI command), 4 consecutive runs,
2819 pass / 1 skip / 0 fail. typecheck clean, lint unchanged at 6 warnings.
Every deletion was re-verified by hand against a repo-wide fixed-string grep
(excluding node_modules/dist/vendor, but INCLUDING .sh, .yaml, .md and
extensionless bin scripts) plus a within-file usage check. Dead code's tests
go with it — a test for dead code is also dead.

Whole files, referenced by nothing but their own test:
  src/lib/incremental-materialize.ts   81 (+112 test)
  src/lib/skill-compressor.ts          68 (+59 test)
  src/lib/webhooks.ts                  58 (+175 test)
  scripts/_test-ecc-materialize.ts     20  (referenced by nothing at all)

Functions with exactly one occurrence in the repo — their own definition:
  discover.ts buildGemBadgeSvg        196
  skill-deps.ts topologicalSort        61 (+18 test)

Stale docs:
  bin/README.md                        32  describes a `soul` CLI and a
                                           bin/cli/lib/ layout, neither of
                                           which exists

Deliberately NOT deleted, though a generated report flagged them — 28 of its
76 non-picker findings were false positives, and deleting them blind would
have broken the build:

  analytics.ts recordSkillUsage  called by resources/hooks/skill-fire-tracker.sh
  bin/cue-slug                   called by bin/cue-learnings (the report's own
                                 grep passed --include='*.sh', so the
                                 extensionless caller never matched)
  runtime-gc.ts                  imported by commands/gc.ts and launch.ts
  handoff.ts (all 4 exports)     imported by commands/handoff.ts
  kitty-image probeKittyTerminal,
  clearKittyImagesSequence,
  skill-deps parseDependencies,
  skill-router Router* types     used within their own file; only the `export`
                                 keyword is redundant, which is churn, not dead
                                 code
  cloud.ts:255 "dead" branch     reachable: `cue cloud push x` gives
                                 argv[2]="cloud" and falls to the default arm

Also left alone: the picker block (~1057 lines) while that migration is in
flight, and launch.ts's token-budget re-export shim (all five symbols are used
inside launch.ts, so it is an export->import rewrite worth zero lines).

Verified: bun test --timeout 30000, 4 consecutive runs, 2819 pass / 1 skip /
0 fail across 219 files. typecheck exit 0. lint 0 errors, 6 warnings (baseline).
…e rate

28 of 76 non-picker findings were wrong. Record which ones and why, so the
next pass re-verifies instead of trusting the confidence column: a 'high'
rating only means the agent's grep came back empty, and those greps missed
shell hooks, extensionless bin scripts, dynamic imports, and string-keyed
command dispatch.
…nches in

A profile teaches the agent a domain. It cannot know that this repo runs on
bun rather than npm, that the tests are behind `just check`, or where the
entry point lives — so the agent guesses, and burns turns finding out.

`lib/project-brief` scans that off the filesystem and the launcher hands it
over: package manager (from the lockfile), the real test/build/lint/typecheck
commands (package.json scripts, Makefile, justfile, Cargo, pyproject),
entry points, layout, workspaces, data layer, what CI actually runs, and the
default branch. Verified only — nothing inferred, because a wrong fact costs
more than a missing one. `.env` values are never read; the scan notes only
that a committed `.env.example` exists.

Delivery is per process, deliberately NOT through the materialized memory
file: the runtime is keyed by profile and shared by every directory and every
parallel session using it, so repo-specific text there would leak across
projects and race between sessions. claude-code takes the brief inline via
`--append-system-prompt`; codex, which has no such flag, gets a per-cwd file
plus one *static* pointer line in AGENTS.md.

`cue brief` shows exactly what the agent receives. `--write` turns it into
`.cue/project.md`: a machine block that refreshes and a `## Notes` section
that never does — for the conventions no scanner can infer. `CUE_BRIEF=0`
opts out entirely.

Two bugs the real-repo smoke test caught, both fixed with tests: the layout
list sorted alphabetically and spent its budget on `action/ agentshield/…`
while cutting `src/`; and `--write` folded the notes back into the machine
block, duplicating them on every rewrite.

Tests: 33 new, driving the scanner through a stub probe (bun/pnpm/cargo/
python/go/monorepo fixtures, CI harvesting, caps, truncation, no-manifest
→ null, and an assertion that no `.env` is ever read), plus the brief-file
merge and the per-agent injection.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…ing on it

Every failure the lexical matcher has is the same shape: a word means one thing
in a manifest and another in a profile description, and the fix is another
stopword. CLAUDE.md, @clack/core, requires-python, base-template each cost a
round of tuning, and the list is unbounded. A model reads the same evidence and
doesn't make that class of mistake. So: lexical proposes, the model judges.

This fits here in a way it did not fit the skill matcher. That hook runs on
every prompt and the prompt is always different, so an LLM call is a per-message
tax with a near-zero cache hit rate — which is why it ended up as an opt-in
`--deep` escalation. A repo's shape is stable for weeks. Keyed on the evidence
rather than the clock, the cache hits ~always and the model is consulted about
once per project. Measured: 9.8s cold, 0.124s warm.

The launch path still never waits. A warm entry is read in ~1ms; a cold one
serves the lexical answer immediately and spawns a detached process to fill the
cache for next time. Picker cost measured at 98ms warm, 45-74ms cold — the
model's 10 seconds are never on anyone's critical path.

claude-classifier extracts the spawn, the ephemeral CLAUDE_CONFIG_DIR isolation
and the credential copy-back out of skill-subset (477 -> 331 lines). That
machinery is subtle enough — the rotation race, the shared timeout budget across
the binary fallback — that a second copy would drift rather than stay honest.

`cue profile match [dir] --explain --deep` exists because the matcher's early
versions stayed wrong for as long as they did purely because nothing showed
which term caused a bad suggestion. It earned its keep within minutes: on a
small Python CLI it revealed "dependencies" named environment, intended,
operating, programming and topic — PyPI trove classifiers, read as packages by
the loose manifest scanner. Handed that same garbage, the model confidently
picked profiles about building cue itself. With the scanner fixed to skip `::`
lines it picks python + backend-base, which is right.

The scanner now also harvests inline dependency arrays (`dependencies = [...]`),
which are often the only place a pyproject declares anything real.

Verified live: agv_stack -> ros2 ("ROS 2 robot control, .urdf files, MRS
platform"); kolarortopedia -> backend + postgres, with the model correctly
dropping `vite` (matched on the vitest test runner) and `supabase` (matched on
the substring "postgre"). 2889 pass / 0 fail, tsc and lint clean.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The combine suggestion engine remembered every stack globally: combo-history
rows carried no directory, so "you launched this stack 4x" counted launches
from every project at once. At SCORE_COMBO (50-65 with the count bonus) that
outranked the cwd-scoped recent (max 50), so a favourite stack from an
unrelated repo led the picker in a repo that had never seen it.

Rows now record the launch directory, and readCombos scopes them by repository
root - a launch inside packages/core still sees the stack confirmed at the repo
root above it. Stacks confirmed here score 60-75; stacks known only from other
repos drop to 31-40, below anything cwd-scoped, and say so in their reason.

Backward compatible on both ends: an unattributed row (written before cwd was
recorded) never claims the current directory, and a caller that passes no scope
keeps the exact score and wording it had before.
growStack grafts your top historical partner onto every suggested stack, and it
does so with no reason line of its own - the profile just appears in the card.
That partner came from a global affinity map mined across every project, so a
pairing learned in one repo rode silently into the suggestions of an unrelated
one: open an API repo and backend+medusa-dev showed up because that is how you
work in a shop repo.

computeAffinityMap now takes an optional repo scope, and launch builds the
picker's partner map from the scoped one. Cross-repo habits still surface, but
only through channels that say where they came from - universalSuggestions, and
`cue suggest-pairs`, which now splits its table into pairings made here and
pairings made in other projects.

The scope predicate moves to lib/repo-scope, shared with combo-history so the
two history readers cannot drift into different notions of "here". Unscoped
callers - the universal-suggestion frequency pass, the dashboard - are
unchanged, and rows with no recorded directory are excluded under a scope
rather than credited to whichever repo happens to be open.
Recent was the last suggestion source still scoping by raw downward path
prefix, which the previous two rounds turned into an inconsistency: launch in
packages/core and cue would show you the stacks and pairings confirmed at the
repo root, but none of the sessions. The prefix rule cannot match a parent
path, so scoped Recent came back empty and launch silently fell back to the
global list - handing that subdirectory the profiles you use in other projects.

computeStats now takes the same repo scope as the other two readers, so all
three agree on what "here" means. The reason line becomes "last used in this
repo", which is what it now measures.

This path had no test coverage at all; it now has five, including the
subdirectory case that was broken and the outside-a-repository fallback.
Three defects made the card lead with a stack the user had never launched.

1. Saturation. The usage bonus was `min(sessions, 5) * 2`, so everything with
   five or more sessions scored exactly 50 - a stack launched 112 times in this
   repo tied with one launched five times, and the card's headline was decided
   by the alphabetical tie-break. Replaced with a logarithmic bonus: every
   doubling of use adds a fixed step, calibrated so the five-session case lands
   where it did before and everything above it keeps climbing.

2. Truncation. A recalled stack was grown and then cut to MAX_STACK_PARTS, so a
   four-part recent became a three-part stack that was never launched, captioned
   with the four-part stack's session count. Recollections are now shown as
   launched: conflicts still resolve, but no companions are bolted on and no
   parts are dropped. The cap still applies to stacks this module proposes.

3. First-write-wins dedup. Sources are scanned in a fixed order but are no
   longer ranked by it, so a foreign combo used 4x claimed the part-set and
   permanently suppressed the recent used 112x here. Dedup now keeps the
   best-scoring claim; ties keep the incumbent, preserving origin order where
   scores don't separate.

Measured in this repo, top suggestion before and after:
  before: career+skill-writer+core   (truncated from a 4-part stack, 5 sessions)
  after:  core+skill-writer          (112 sessions here)
`cue suggest` recommended a wedding-invitations skill because "date" appeared
195 times, and rated everything at confidence 1.00. Four compounding faults.

Counting the wrong text. It scanned raw transcript JSONL, so assistant prose,
tool names, tool output and file contents all counted as things the user
"mentioned" - "read" scored 798x because that is the Read tool. Only user text
parts are read now; tool_result parts arrive under role "user" too and are
excluded.

Substring matching. `indexOf` found "ops" inside "operations" and "and" inside
"command". Matching is now whole-word, via a single tokenizing pass that also
replaces a per-keyword regex compiled over megabytes of text - the command went
from seconds to ~0.2s end to end.

No stopword filter. Every SKILL.md description opens with "Use this when the
user asks...", so "use", "when" and "user" became keywords for the entire
catalogue. Filtered now, along with transcript-structure words, and the
remaining keywords are weighted by catalogue-wide rarity: a stopword list only
knows what is common in English, not that "mcp" appears in hundreds of these
skills and separates none of them.

Meaningless confidence. `min(1, mentions / 50)` reached 1.00 for every skill,
so the printed number carried no information and the ranking was arbitrary.
Score is now the strongest few signals - summing every match rewarded a long
description over a relevant one - on an asymptotic curve calibrated against a
real 355-candidate run. Measured confidence spread over this repo's
transcripts: p10 0.32, p50 0.54, max 0.72.

The reason line is computed from the same frequency map as the score, so it can
no longer name a keyword that never contributed - which is how "and" came to be
cited 8044 times.
Anthropic's OAuth rotates the refresh token on every refresh, and cue gives
each profile runtime its own copy of .credentials.json. Two sessions on
different profiles therefore hold two copies of one token: whichever refreshes
first silently revokes the other, which then hits a login prompt mid-session.
Measured on a real machine - 121 runtime copies, 75 distinct refresh tokens,
114 of 117 access tokens expired.

Sharing one file via symlink is the obvious fix and does not work: Claude Code
rewrites .credentials.json atomically (tmp -> rename), which replaces a symlink
with a regular file on the first refresh. Observable in any authmux runtime,
where cue symlinks .claude.json and every one has since become a plain file
while its neighbours (projects/, agents/) are still links.

So the sessions have to talk instead. cue already published a rotation to the
owning account dir on exit, which is too late - by then the sibling has already
been dropped. A live session now reconciles once a minute for as long as it
runs: it republishes its own rotation, and adopts anyone else's.

pullFreshestToRuntime is the missing inbound direction, gated on matching
accountUuid so alternating accounts can't hand each other tokens, and on
strictly newer expiresAt so two reconcilers settle rather than trading the file
back and forth. Polling rather than watching is deliberate: the same atomic
rename that defeats symlinks also breaks an inode watch.
…s it

The rotation heal added in ae59ebf never ran for the default account. Every
direction in credentials-sync is gated on a known accountUuid, and
readAccountUuid looked only at <dir>/.claude.json. With no CLAUDE_CONFIG_DIR
set, Claude Code keeps oauthAccount in the home-root ~/.claude.json and leaves
~/.claude/.claude.json a settings-only stub - so the default account read as
unknown and all three heals silently no-op'd: no candidates to sync from, no
publish to ~/.claude, no adopt out of it. Measured here: 76 distinct refresh
tokens across 123 runtimes, and 32 runtimes the heal could not see. The bug
hid because authmux account dirs DO carry identity in-dir, so they worked.

The basename gate keeps the fallback off those account dirs and off runtime
dirs, which all carry identity in-dir - a stray sibling .claude.json must
never be read as an account's identity or two accounts could trade tokens.

Second path to the same symptom: overlaySourceState copied .credentials.json
unconditionally, with none of the expiresAt comparison the rebuild path does.
It runs on every cache-hit launch, and cue sync / cue install resolve their
source with healFromRuntime: false - so one bulk sync could stamp a dead token
over every runtime at once. It now keeps whichever side is newer, scoped to a
single account so a deliberate account switch still re-seeds wholesale.

Last, the reconcile cadence. Copies of a blob share one expiresAt, so
concurrent sessions reach expiry together and refresh within moments of each
other; only the first rotation survives. A flat 60s poll cannot help when the
contended window is seconds wide, so the cadence now tightens to 5s across
that window and idles at a minute elsewhere. This narrows the race and does
not close it: cue does not perform the refresh, Claude Code does in-process,
so there is no point at which cue can serialize the two callers.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A skill maintained in its own repo can be linked into the tree rather than
copied — browser/ego-browser now points at ~/Documents/ego-lite-linux, so the
Linux port's skill has exactly one source of truth instead of a hand-synced
duplicate that silently drifts.

walk() filtered category entries on dirent.isDirectory(), which is false for the
symlink itself, so the linked skill vanished from the index and `cue validate`
failed it as E3 SKILL_NOT_FOUND. Stat through symlinks; dangling links are
treated as absent. The materializer already symlinks skills, so this only makes
the read path agree with the write path.

Also register ego-browser in KNOWN_CLIS so the skill's Prerequisites section is
picked up by the CLI extractor.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
core declared browser/ego-browser, but the per-project loadout deferred it
everywhere no project signal matched — which is most projects, since the signal
set comes from package.json deps and framework detection and nothing there says
"browser". The declaration was real and the skill still never loaded.

Deferring it does not save a browser session; it sends the agent to MCP
round-trips or web fetch instead, which costs more than this skill's
frontmatter. So it joins the operational primitives in ALWAYS_KEEP, and the
matching slug set in profile-merge so a budgeted composite can't drop it either.

Verified on a neutral cwd with zero project signals: browser/ego-browser now
classifies full, where it previously landed in deferred.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Every path that lands a new skill on disk — `skills add`, `discover install`,
`marketplace install-skill` — now scans it before anything registers it to a
profile. SkillSpector covers 68 vulnerability patterns across 17 categories
(prompt injection, data exfiltration, supply chain, dangerous code via AST,
YARA, MCP tool poisoning) on top of cue's own SEC1-3 criticals.

Policy reads the report's `recommendation` rather than the exit code, so the
three verdicts stay distinguishable: DO_NOT_INSTALL blocks, CAUTION registers
with a visible warning, SAFE is quiet. `--allow-unsafe` overrides the block.

install-skill differs from the others on purpose: the files are already on disk
by the time it runs, so a block reports findings and exits non-zero rather than
deleting anything, leaving them for review.

When the scanner isn't installed the gate degrades to cue's own rules and says
so, rather than silently passing everything.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Rematerializing did `rm -rf runtimeDir` and only then renamed the new tree into
place, so the live path stayed nonexistent for the whole recursive delete —
seconds, on a runtime carrying a plugin cache and a backup chain. A Claude Code
session already running against that profile resolves its hooks through exactly
that path, so every hook firing inside the gap died with "No such file or
directory" (observed 2026-08-03: nine Stop hooks at once, mid-session).

Move the old tree aside instead: the path is unresolvable only between two
renames, and the delete runs after the new runtime is live. The `.old-*` dir is
a sibling of the swap target, so it cannot cross a filesystem boundary and sits
one level below the root runtime-gc scans. Leftovers from a swap killed between
the renames are swept best-effort on the next materialize.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…alette

The v2 card and the stack palette had grown their own copies of the same
drawing code. Both now hang off one set of primitives in picker/ui.ts, styled
after iOS grouped-inset lists: one rounded card per idea, uppercase muted
section headers instead of heavy rules, a filled pill for the single primary
action, circular selection marks instead of ASCII brackets, page dots for
"there is more to see here".

Everything in ui.ts is pure — no I/O, no TTY — and styleText is a no-op off a
TTY, so the tests assert on plain text.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Collects the remaining working-tree changes from prior sessions:

- resources/personas/integrity-protocol{,-compact}.md — wording pass
- resources/hooks/{tag-audit,liedetector-tag-density}.sh — confidence-tag
  density checks
- profiles/frontend-design, profiles/reverse-skill — two new profiles
- README.md, .cue.profile (cue's own pin: core -> core+skill-writer)

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Brings 36 commits of main onto the branch, including the pane-badge fixes
(#111, #120) the branch predates. Six conflicts, all resolved to main's side:
main's SkillSpector (#108) is a strict superset of the branch's earlier take —
same exports plus baselineDirs/resolveBaselineFor and a --baseline runner arg —
and its security.ts keeps both `security scan` and `security baseline`.
README and the smart-loader perf test take main's wording likewise.

Full suite vs both parents, same conditions: 29 failures on the merge, 28 on
main, 34 on the branch, and zero that fail on the merge but on neither parent.
Nine smart-loader tests that were red on the branch go green.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
@NagyVikt

NagyVikt commented Aug 7, 2026

Copy link
Copy Markdown
Contributor Author

Investigation: what is actually new here, and why this can't merge yet

Checked every part of this branch against current main (36 commits ahead of
where this branch forked). Results, by content rather than by patch-id — git cherry marks all 19 commits as absent, which is wrong, because work that landed
via a different patch still shows as missing.

Already on main, so this branch's copy should be dropped:

  • The whole SkillSpector install gate — src/lib/skillspector.ts, its test,
    security.ts, security.test.ts, plus the wiring in skills.ts,
    discover.ts and marketplace.ts. It landed independently; this branch
    carries a parallel implementation, which is what produces the add/add conflict.
  • profiles/reverse-skill/.

Genuinely absent from main and worth keeping: everything else — the
materializer atomic swap, picker/ui.ts and the card/palette refactor, the
repo-scoped picker work, brief, the auth fixes, the symlinked-skill resolver
fix, profiles/frontend-design/, the persona and tag-hook edits.

browser/ego-browser has been split out to #123 and is not part of this
discussion.

The blocker

I built the merge (main into this branch, taking main's side for all four
SkillSpector files, main's README paragraph, and main's smart-loader-suggest
perf assertion) and ran the full suite against a main baseline in matched
worktrees — same submodule checkouts, same linked node_modules.

Result: one new failing test that main passes deterministically.

summon > mcp_status reflects the active session's loaded MCPs
  expected "ok", received "missing:lightpanda"

Reproduced 3/3 on the merge, 0/3 on main. git bisect across all 16 commits
plus the merge puts the first bad commit on the merge itself — the branch tip
alone is good and main alone is good, so this is an interaction, not a bad
commit. loadActiveMcpIds("core") stops returning lightpanda after the merge.

Things I ruled out: the ego-browser core declaration (removed it in a pristine
worktree — still fails), profiles/frontend-design, the .cue.profile pin flip,
resolver-local.ts, skill-subset.ts, profile-merge.ts, optimizer.ts
individually, and any submodule-checkout difference between the two worktrees
(both at 62049bb, identical lightpanda/SKILL.md).

One other thing the merge exposed and I did fix: frontend-design makes 87
shipped profiles, so docs-facts went red on the stale count of 86 in
README.md and llms.txt.

Not merging past this. Someone should trace loadActiveMcpIds against main's
newer MCP-resolution code before this branch lands.

Also worth knowing: this suite is flaky independently of the above. Two
consecutive runs of the same tree gave 3 fail + 1 error and then 1 fail; the
cue cli list --all-profiles and sub-200ms hook-budget assertions are
timing-sensitive.

@NagyVikt

NagyVikt commented Aug 7, 2026

Copy link
Copy Markdown
Contributor Author

Lead on the summon > mcp_status regression, found while bumping the
submodules — worth checking before anyone re-derives it from scratch.

18570880 chore(profiles): drop the lightpanda MCP from the profiles that pin it (#121) landed on main after this branch forked. That is precisely the surface
the failing assertion sits on:

// browser/lightpanda needs the `lightpanda` MCP; core loads it.
expect(lp(withCore.skills)?.mcp_status).toBe("ok");

If #121 removed lightpanda from core's MCP set and updated summon.test.ts
in the same commit, then main is self-consistent and this branch is too — but
the merge can land main's profile change beside a stale expectation, or the
reverse, without git seeing a textual conflict. That matches every symptom:
main alone green, branch tip alone green, merge red, and no single file revert
fixing it.

First thing to check is whether #121 touched summon.test.ts, and what
core's resolved MCP set is on each of the three trees.

@NagyVikt
NagyVikt merged commit 8840485 into main Aug 7, 2026
1 of 5 checks passed
@NagyVikt

NagyVikt commented Aug 7, 2026

Copy link
Copy Markdown
Contributor Author

Root cause found — the lead above was close, the mechanism is now confirmed.
Full diagnosis in #126, summarised here because it is the same blocker.

summon > mcp_status does not fail because of anything in this branch's source.
It fails because #121 dropped the lightpanda MCP from every profile including
core, while summon.test.ts still asserts "core loads it" — and the test kept
passing only because the committed catalog/index.json was stale and its
mcpProviders map still listed core as a provider.

Any tree that rebuilds or otherwise perturbs that index gets the honest answer
(mcpProviders["lightpanda"] absent entirely) and the stale assertion fails.
That is why main alone was green, this branch alone was green, and the merge
was red, with no single file revert fixing it — I was looking for a source
interaction when the disagreement was between a committed cache and the profiles
it was derived from.

browser/lightpanda/SKILL.md is byte-identical throughout; only the provider map
moved.

So this branch is not carrying a regression. Once the lightpanda/#121
inconsistency is settled (see #126 for the two options), re-run and this should
be clean.

NagyVikt pushed a commit that referenced this pull request Aug 7, 2026
main carries a squashed copy (#122) of commits also in this branch's
history, so git saw the shared liedetector work as two independent
edits and conflicted on all three files.

Resolved toward this branch for the calibration sources: main's side is
the superseded decile ladder (~50/60/70/80, ~20/30/40), this branch's is
the 5-point raster from #125. Everything unique to main's side of those
three files was old-ladder text, so nothing is lost.

Both submodules resolved to main's pointers (#126): resources/skills
ba3d06d over the branch's 5a49e6c. The two are identical in the raster
sources and differ only in the generated catalog, where ba3d06d is the
more complete regen and is the submodule's own main head.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
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.

1 participant