Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
fc73457
feat(picker): match every profile against the repo, not just the 19 w…
Imdeadpool1 Jul 26, 2026
cce1777
fix(test): make cue handoff tests e2e, drop the global mock.module leak
Imdeadpool1 Jul 26, 2026
36e5075
chore: delete 733 lines of verified dead code
Imdeadpool1 Jul 26, 2026
7e337c7
docs(dead-code): mark the report superseded, record the false-positiv…
Imdeadpool1 Jul 26, 2026
7d34aeb
feat(brief): hand the agent verified facts about the directory it lau…
Imdeadpool1 Jul 27, 2026
7973afe
feat(picker): let the model rerank profile matches, without ever wait…
Imdeadpool1 Jul 27, 2026
7c8494a
feat(picker): scope remembered stacks to the repo you launch in
Imdeadpool1 Jul 27, 2026
b029942
feat(picker): scope pair affinity to the repo you launch in
Imdeadpool1 Jul 27, 2026
c157281
feat(picker): scope Recent by repository, not by path prefix
Imdeadpool1 Jul 27, 2026
5eb0ccd
fix(picker): rank suggested stacks by what you actually launch here
Imdeadpool1 Jul 27, 2026
fdd083b
fix(suggest): score skills on what the user actually said
Imdeadpool1 Jul 27, 2026
ae59ebf
fix(auth): keep concurrent sessions from revoking each other's tokens
Imdeadpool1 Jul 27, 2026
cbeeb6b
fix(auth): read the default account's identity where Claude Code keep…
Imdeadpool1 Jul 28, 2026
6385de5
fix(resolver): follow symlinked skill directories
Imdeadpool1 Aug 5, 2026
120e327
feat(core): keep ego-browser loaded in every project
Imdeadpool1 Aug 7, 2026
788d72d
feat(security): gate freshly-fetched skills through NVIDIA SkillSpector
Imdeadpool1 Aug 7, 2026
8112f7b
fix(materializer): stop unresolving the live runtime path mid-swap
Imdeadpool1 Aug 7, 2026
ceebde6
refactor(picker): pull the shared visual primitives out of card and p…
Imdeadpool1 Aug 7, 2026
062b40a
chore: integrity-protocol wording, tag hooks, two new profiles
Imdeadpool1 Aug 7, 2026
0c0f25b
Merge origin/main into fix/oauth-identity-desync
Imdeadpool1 Aug 7, 2026
c495340
fix(liedetector): one ~N% raster, a drift guard, and hook test covera…
NagyVikt Aug 7, 2026
b0c3400
Merge origin/main into fix/oauth-identity-desync
Imdeadpool1 Aug 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -189,7 +189,7 @@ Cold start 50–200 ms, warm start under 5 ms. Nothing stays resident. Full flow

---

## 86 ready-made cuecards
## 87 ready-made cuecards

cue ships with pre-built profiles for common stacks and workflows. A taste:

Expand All @@ -208,7 +208,7 @@ cue ships with pre-built profiles for common stacks and workflows. A taste:
| 🏢 **agency** | 63 delegatable subagents — design, sales, product, PM, QA |

```bash
cue list # see all 86
cue list # see all 87
cue auto-detect # suggest the right one for the current directory
cue use <name> # pin it
```
Expand Down
2 changes: 1 addition & 1 deletion llms.txt
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ Key facts an LLM should know up front:

- Install: `npm install -g cue-ai`. Pin a profile to a repo: `echo <profile> > .cue.profile`. Then type `claude` or `codex` — the shim launches the real binary with the scoped runtime.
- Architecture: resolve → materialize → exec. Hash-cached: warm start <5 ms. No daemon, no background process.
- Profiles inherit from a `core` baseline. 86 profiles ship by default (backend, frontend, marketing, cybersecurity, medusa-dev, …).
- Profiles inherit from a `core` baseline. 87 profiles ship by default (backend, frontend, marketing, cybersecurity, medusa-dev, …).
- Ten agents supported via `cue materialize <agent>`: claude, codex, cursor, cline, gemini, copilot, windsurf, roo, amp, aider.
- License: MIT. Repo: https://github.com/opencue/cuecards. Package: https://www.npmjs.com/package/cue-ai.

Expand Down
22 changes: 13 additions & 9 deletions resources/hooks/liedetector-tag-density.sh
Original file line number Diff line number Diff line change
Expand Up @@ -59,14 +59,18 @@ TAGS = ("VERIFIED", "KNOWN", "INFERRED", "ASSUMED",
"GUESSED", "STALE", "UNKNOWN", "CORRECTION")
TAG_RE = re.compile(r"\[(?:%s)[^\]]*\]" % "|".join(TAGS))

# The protocol requires a ~N% on every yellow and orange tag, drawn from that
# tier's ladder. Yellow spans ~50-85%, orange ~20-45%, so the ladders don't
# overlap each other or green (>=90%).
# The protocol requires a ~N% on every yellow and orange tag, snapped to a
# 5-point raster. Yellow spans ~50-85%, orange ~20-45%, so the tiers don't
# overlap each other or green (>=90%). The raster is deliberately coarser than
# the model's apparent precision: self-reported LLM confidence is miscalibrated
# in absolute terms, so ~67% would read as a measurement where none happened.
# 14 steps is enough to ORDER claims against each other, which is all the number
# is for.
LADDER = {
"INFERRED": {"50", "60", "70", "80"},
"ASSUMED": {"50", "60", "70", "80"},
"GUESSED": {"20", "30", "40"},
"STALE": {"20", "30", "40"},
"INFERRED": {"50", "55", "60", "65", "70", "75", "80", "85"},
"ASSUMED": {"50", "55", "60", "65", "70", "75", "80", "85"},
"GUESSED": {"20", "25", "30", "35", "40", "45"},
"STALE": {"20", "25", "30", "35", "40", "45"},
}
CAL_RE = re.compile(r"\[(%s)([^\]]*)\]" % "|".join(LADDER))
PCT_RE = re.compile(r"~\s*(\d+)\s*%")
Expand Down Expand Up @@ -142,8 +146,8 @@ if missing or offladder:
if offladder:
parts.append("%d off-ladder (%s)"
% (len(offladder), ", ".join(sorted(set(offladder)))))
print("liedetector: %s. Yellow ([INFERRED]/[ASSUMED]) takes ~50/60/70/80%%, "
"orange ([GUESSED]/[STALE]) takes ~20/30/40%% — nothing else. "
print("liedetector: %s. Yellow ([INFERRED]/[ASSUMED]) takes ~50-85%%, "
"orange ([GUESSED]/[STALE]) takes ~20-45%%, both in 5-point steps. "
"Skip with [skip-tag-density]." % "; ".join(parts))
sys.exit(0)

Expand Down
2 changes: 1 addition & 1 deletion resources/personas/integrity-protocol-compact.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ Applies to every response. Flag uncertainty *before* the claim, never bury hedge

Pick the *most specific* tag and **downgrade when in doubt** — false confidence hurts more than false hedging.

**Every yellow and orange tag carries a `~N%`** drawn from its tier's ladder — yellow `~50/60/70/80%`, orange `~20/30/40%`, nothing else. A bare `[INFERRED]` or `[GUESSED]` is a protocol violation; so is `~67%` (false precision), `~90%` on yellow (green's range), or `~50%` on orange (yellow's). Skip the % on green and red — the tier already says it. Can't pick a value? You're in the wrong tier: downgrade. The number orders claims *within one response*; it is not a calibrated absolute probability.
**Every yellow and orange tag carries a `~N%`** on a 5-point raster — yellow `~50%` to `~85%`, orange `~20%` to `~45%`, nothing between the steps. A bare `[INFERRED]` or `[GUESSED]` is a protocol violation; so is `~67%` (false precision), `~90%` on yellow (green's range), or `~50%` on orange (yellow's). Skip the % on green and red — the tier already says it. Can't pick a value? You're in the wrong tier: downgrade. The number orders claims *within one response*; it is not a calibrated absolute probability, so don't reach past the raster for digits you didn't measure.

**Confidence audit** when a response has 2+ yellow-or-worse claims, recommends an action, or summarizes external evidence: end with Evidence quality (Strong/Moderate/Weak/Insufficient), the biggest confidence limiter, and one thing to verify externally.

Expand Down
7 changes: 4 additions & 3 deletions resources/personas/integrity-protocol.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,9 +25,10 @@ Rewritten by Claude (Opus 4.7) from your hallucination-reduction draft. Applies
- 🔴 `[UNKNOWN]` — outside my reliable knowledge. I'm saying so instead of fabricating an answer. Hand off to a search or to the user.

**Required percentage calibration on yellow/orange tags.** Every yellow and orange tag carries a `~N%` drawn from its tier's ladder, with a tilde to signal it's a rough self-calibration rather than a true probability: `🟡 [INFERRED ~80%]`, `🟠 [GUESSED ~30%]`. Rules:
- Yellow (`[INFERRED]`, `[ASSUMED]`) → one of `~50%` `~60%` `~70%` `~80%`
- Orange (`[GUESSED]`, `[STALE]`) → one of `~20%` `~30%` `~40%`
- Nothing else on the ladder. Never `~67%` or `~73%` (false precision), never `~90%` on yellow (that's green's range) or `~50%` on orange (that's yellow's)
- Yellow (`[INFERRED]`, `[ASSUMED]`) → `~50%` to `~85%` in 5-point steps: `~50%` `~55%` `~60%` `~65%` `~70%` `~75%` `~80%` `~85%`
- Orange (`[GUESSED]`, `[STALE]`) → `~20%` to `~45%` in 5-point steps: `~20%` `~25%` `~30%` `~35%` `~40%` `~45%`
- Nothing between the steps. Never `~67%` or `~73%` (false precision), never `~90%` on yellow (that's green's range) or `~50%` on orange (that's yellow's)
- The raster is coarser than your apparent precision on purpose. Self-reported confidence is miscalibrated in absolute terms, so a digit you didn't measure reads as a measurement. 14 steps is enough to *order* claims, which is all the number does
- A bare `[INFERRED]` / `[ASSUMED]` / `[GUESSED]` / `[STALE]` is a protocol violation
- Always prefix `~` so the reader knows it's an estimate
- Skip the % on green and red — the tier already says it
Expand Down
73 changes: 63 additions & 10 deletions src/commands/summon.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,54 @@ import { tmpdir } from "node:os";
import { join } from "node:path";

import { summon, detectActiveProfile, REEXEC_CMD } from "./summon";
import { existsSync } from "node:fs";
import { loadProfile } from "../lib/profile-loader";
import { getSkillDependencies } from "../lib/skill-dependencies";
import { existsSync, readdirSync } from "node:fs";

// `resources/skills` is a git submodule. The mcp_status assertion below reads a
// skill's `requires_mcps` from disk; without it checked out (`git submodule
// update --init`) the deps come back empty and every skill reads "ok". Skip
// rather than fail spuriously.
const SKILLS_PRESENT = existsSync(join(import.meta.dir, "../../resources/skills/skills"));

/**
* Find a live (profile with an MCP-gated skill, other profile supplying that
* skill's MCPs) pairing. Returns null when no such pairing exists.
*/
async function findMcpGatedPair(): Promise<
{ summonProfile: string; skillId: string; deps: string[]; provider: string } | null
> {
const names = readdirSync(join(import.meta.dir, "../../profiles"), { withFileTypes: true })
.filter((d) => d.isDirectory())
.map((d) => d.name)
.sort();

const loaded = new Map<string, Awaited<ReturnType<typeof loadProfile>>>();
for (const n of names) {
try {
loaded.set(n, await loadProfile(n));
} catch {
// Unloadable profile — not this test's problem; profile-loader has its own.
}
}

for (const [name, profile] of loaded) {
for (const s of profile.skills.local) {
// Keep the raw ids: summon reports `missing:<id>` with original casing.
const deps = [...new Set(getSkillDependencies(s.id).map((d) => d.mcpId))];
if (deps.length === 0) continue;
for (const [providerName, provider] of loaded) {
if (providerName === name) continue;
const ids = new Set(provider.mcps.map((m) => m.id.toLowerCase()));
if (deps.every((d) => ids.has(d.toLowerCase()))) {
return { summonProfile: name, skillId: s.id, deps, provider: providerName };
}
}
}
}
return null;
}

let dir: string;
beforeEach(async () => { dir = await mkdtemp(join(tmpdir(), "cue-summon-")); });
afterEach(async () => { await rm(dir, { recursive: true, force: true }); });
Expand Down Expand Up @@ -110,14 +150,27 @@ describe("summon", () => {
});

test.skipIf(!SKILLS_PRESENT)("mcp_status reflects the active session's loaded MCPs", async () => {
// browser/lightpanda needs the `lightpanda` MCP; core loads it.
const lp = (skills: { id: string; mcp_status: string }[]) =>
skills.find((s) => s.id === "browser/lightpanda");

const noActive = await summon({ cwd: dir, profile: "vercel", active: null, noPin: true });
const withCore = await summon({ cwd: dir, profile: "vercel", active: "core", noPin: true });

expect(lp(noActive.skills)?.mcp_status).toBe("missing:lightpanda");
expect(lp(withCore.skills)?.mcp_status).toBe("ok");
// The pairing is derived from live profile data, not hardcoded. This test
// used to pin browser/lightpanda + core; 18570880 (#121) dropped the
// lightpanda MCP from every profile that pinned it, so the assertion named
// a pairing that no longer existed and failed for a reason unrelated to
// what it tests. The behaviour under test is mcp_status resolution, so
// derive any still-valid pairing and assert against that.
const pair = await findMcpGatedPair();
// Loud rather than a silent skip: if nothing is satisfiable, that is itself
// worth a human look, not a vacuous green.
expect(pair).not.toBeNull();
const { summonProfile, skillId, deps, provider } = pair!;

const find = (skills: { id: string; mcp_status: string }[]) =>
skills.find((s) => s.id === skillId);
const missing = `missing:${deps.join(",")}`;
const opts = { cwd: dir, profile: summonProfile, noPin: true };

const noActive = await summon({ ...opts, active: null });
const withProvider = await summon({ ...opts, active: provider });

expect(find(noActive.skills)?.mcp_status).toBe(missing);
expect(find(withProvider.skills)?.mcp_status).toBe("ok");
});
});
120 changes: 120 additions & 0 deletions src/lib/integrity-ladder.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
import { describe, expect, test } from "bun:test";
import { readFile } from "node:fs/promises";
import { join } from "node:path";

/**
* Drift guard for the integrity protocol's ~N% ladder.
*
* The ladder (yellow [INFERRED]/[ASSUMED] → ~50/60/70/80%, orange
* [GUESSED]/[STALE] → ~20/30/40%) is defined in two places that CANNOT import
* from each other:
*
* - resources/hooks/liedetector-tag-density.sh — the Stop hook, lives in the
* cue repo.
* - resources/skills/skills/meta/liedetector/scripts/run-evals.sh — the eval
* grader, lives in the resources/skills SUBMODULE and ships standalone via
* `npx agent-liedetector-skill` to agents that have no cue tree at all.
*
* Duplication is therefore the correct design, but silent divergence is not:
* the whole point of the ladder is that one rule governs every surface. This
* test reads both definitions and asserts they agree. It also asserts the
* prose sources still state the same values, so a doc edit can't drift from
* the code either.
*/

const REPO = join(import.meta.dir, "../..");
const HOOK = join(REPO, "resources/hooks/liedetector-tag-density.sh");
const GRADER = join(
REPO,
"resources/skills/skills/meta/liedetector/scripts/run-evals.sh",
);

type Ladder = Record<string, string[]>;

/** Parse the `LADDER = { "INFERRED": {"50", ...}, ... }` python literal. */
function parseLadder(source: string, file: string): Ladder {
const block = source.match(/LADDER\s*=\s*\{([\s\S]*?)\n\}/);
if (!block) throw new Error(`no LADDER definition found in ${file}`);

const ladder: Ladder = {};
const entry = /"(\w+)"\s*:\s*\{([^}]*)\}/g;
for (const m of block[1].matchAll(entry)) {
const values = [...m[2].matchAll(/"(\d+)"/g)].map((v) => v[1]);
ladder[m[1]] = values.sort();
}
if (Object.keys(ladder).length === 0) {
throw new Error(`LADDER in ${file} parsed to zero entries`);
}
return ladder;
}

/**
* The one true ladder: a 5-point raster, yellow ~50-85%, orange ~20-45%.
* Changing it here means changing it in both scripts and all four prose files.
*/
const YELLOW = ["50", "55", "60", "65", "70", "75", "80", "85"];
const ORANGE = ["20", "25", "30", "35", "40", "45"];
const EXPECTED: Ladder = {
INFERRED: YELLOW,
ASSUMED: YELLOW,
GUESSED: ORANGE,
STALE: ORANGE,
};

describe("integrity protocol ~N% ladder", () => {
test("the hook and the eval grader define the same ladder", async () => {
const hook = parseLadder(await readFile(HOOK, "utf8"), HOOK);
const grader = parseLadder(await readFile(GRADER, "utf8"), GRADER);
expect(hook).toEqual(grader);
});

test("both match the canonical ladder", async () => {
expect(parseLadder(await readFile(HOOK, "utf8"), HOOK)).toEqual(EXPECTED);
expect(parseLadder(await readFile(GRADER, "utf8"), GRADER)).toEqual(EXPECTED);
});

test("tiers do not overlap each other or green", () => {
const yellow = EXPECTED.INFERRED.map(Number);
const orange = EXPECTED.GUESSED.map(Number);
// Yellow spans ~50-85%, orange ~20-45%, green starts at 90%.
expect(Math.max(...orange)).toBeLessThan(Math.min(...yellow));
expect(Math.max(...yellow)).toBeLessThan(90);
expect(EXPECTED.ASSUMED).toEqual(EXPECTED.INFERRED);
expect(EXPECTED.STALE).toEqual(EXPECTED.GUESSED);
});

// The prose sources are what the model actually reads. If a doc says ~90% is
// legal on yellow while the hook flags it, the model gets nudged for obeying
// its own instructions — the exact failure this ladder was introduced to fix.
const PROSE = [
"resources/personas/integrity-protocol.md",
"resources/personas/integrity-protocol-compact.md",
"resources/skills/skills/meta/liedetector/SKILL.md",
"resources/skills/skills/meta/integrity-tags/SKILL.md",
];

// The compact persona states the raster as a range ("~50% to ~85% in 5-point
// steps") to stay short; the long-form docs also spell every step out. So
// assert the RULE — both tier boundaries plus the step size, which together
// pin all 14 values — rather than demanding 14 literals in every file.
test.each(PROSE)("%s states the 5-point raster, no stale values", async (rel) => {
const text = await readFile(join(REPO, rel), "utf8");

for (const v of ["50", "85"]) expect(text).toContain(`~${v}%`); // yellow bounds
for (const v of ["20", "45"]) expect(text).toContain(`~${v}%`); // orange bounds
expect(text).toMatch(/5-point/); // the step size fills in between

// Both retired ladders are gone: the original decile list, and the 4-value
// yellow ladder that briefly replaced it.
expect(text).not.toContain("20 / 30 / 40 / 60 / 80 / 90");
expect(text).not.toMatch(/~50%`?[,\s]*`?~60%`?[,\s]*`?~70%`?[,\s]*`?~80%/);
});

test("calibration is stated as required, not optional", async () => {
for (const rel of PROSE) {
const text = await readFile(join(REPO, rel), "utf8");
expect(text).not.toMatch(/Optional\s+(decile\s+|percentage\s+)?calibration/i);
expect(text).not.toMatch(/##\s*Optional\s+`?~N%`?/i);
}
});
});
Loading
Loading