diff --git a/README.md b/README.md
index 23d6b833..44bd2e27 100644
--- a/README.md
+++ b/README.md
@@ -42,6 +42,10 @@ npm install -g cue-ai
---
+
+
+
+
## Why this exists
If you've been using AI coding agents for a while, you've probably collected a pile of skills, MCP servers, and custom instructions. Maybe hundreds. Here's the problem:
@@ -57,6 +61,10 @@ cue fixes this by scoping everything per directory. Your Medusa shop loads the M
### Before vs after — in numbers
+
+
+
+
| Loadout | Always-on context | Cost / 100 msgs (Sonnet input) |
|---|---|---|
| Everything loaded (`full` profile) | ~81k tokens | ~$24 |
@@ -93,6 +101,10 @@ One cuecard per project. Your agent reads the right one the moment you launch it
No daemon, no background process. cue intercepts the *call* to your agent, resolves the directory's cuecard, materializes it once, then hands off to the real binary:
+
+
+
+
```
you type `claude`
│
@@ -249,6 +261,10 @@ cue failures --propose # let Claude draft profile improvements from failur
`cue --help` shows the full ~50-subcommand surface; the set above covers a typical week.
+
+
+
+
---
## API
diff --git a/evals/agent-eval/.env.example b/evals/agent-eval/.env.example
new file mode 100644
index 00000000..1ca5144a
--- /dev/null
+++ b/evals/agent-eval/.env.example
@@ -0,0 +1,16 @@
+# cue-agent-eval — credentials
+#
+# Pick ONE agent API key. The experiments default to `agent: 'claude-code'`
+# (direct Anthropic), so ANTHROPIC_API_KEY is the simplest path.
+ANTHROPIC_API_KEY=sk-ant-...
+
+# Or, to route through the Vercel AI Gateway instead, set this and change the
+# experiments to `agent: 'vercel-ai-gateway/claude-code'`. This key also enables
+# the optional failure classifier (infra/timeout vs model failures).
+# AI_GATEWAY_API_KEY=your-ai-gateway-api-key
+
+# Sandbox: the experiments use `sandbox: 'docker'`, so NO Vercel token is needed.
+# (Docker must be installed and running.) To use the Vercel sandbox instead,
+# set one of these and drop `sandbox: 'docker'` from the experiment configs.
+# VERCEL_TOKEN=your-vercel-token
+# VERCEL_OIDC_TOKEN=your-oidc-token
diff --git a/evals/agent-eval/.gitignore b/evals/agent-eval/.gitignore
new file mode 100644
index 00000000..43a3bf9b
--- /dev/null
+++ b/evals/agent-eval/.gitignore
@@ -0,0 +1,7 @@
+node_modules/
+dist/
+.env
+.env.local
+results/
+*.log
+.DS_Store
diff --git a/evals/agent-eval/README.md b/evals/agent-eval/README.md
new file mode 100644
index 00000000..cd3cfc86
--- /dev/null
+++ b/evals/agent-eval/README.md
@@ -0,0 +1,72 @@
+# cue-agent-eval
+
+A/B test cue **profiles** with [`@vercel/agent-eval`](https://github.com/vercel-labs/agent-eval).
+Same task, same model, run in an isolated sandbox — the **only variable is the
+cue profile's `CLAUDE.md`** (persona + rules + skill-routing). The output is a
+**pass rate** per profile, so "gstack is better than core for X" stops being a
+vibe and becomes a number.
+
+```
+experiments/core.ts baseline
+experiments/gstack.ts 58-role full profile
+experiments/improver.ts goal-with-a-check loop
+evals/create-button/ one starter task (PROMPT.md + EVAL.ts)
+lib/with-profile.ts injects a profile's CLAUDE.md into the sandbox
+```
+
+## How a profile becomes the variable
+
+`lib/with-profile.ts` adds a `setup()` that runs **on the host**:
+
+1. `cue launch --rematerialize` — materializes the profile's full
+ runtime (persona + rules + skill-routing + `_always` fragments → `CLAUDE.md`)
+ **without exec'ing claude**, and prints `{ runtimeDir }`.
+2. Reads that `CLAUDE.md` and writes it into the sandbox project root, so the
+ in-sandbox Claude Code runs under that profile's instructions.
+
+**Not injected** (by design): MCP servers, hooks, and the headroom proxy env.
+They need keys/network and would fail to start in a throwaway sandbox. The
+measured variable is the profile's *instructions*, not its infra. Skill **bodies**
+aren't injected yet either (the `CLAUDE.md` already carries each profile's skill
+list + routing) — see the extension note at the bottom of `lib/with-profile.ts`.
+
+## Prerequisites
+
+- `@vercel/agent-eval` — installed globally (`agent-eval --help`).
+- **Docker** — the experiments use `sandbox: 'docker'` (no Vercel token needed).
+- **An agent API key** — `ANTHROPIC_API_KEY` (direct, the default) **or**
+ `AI_GATEWAY_API_KEY` (gateway). Without one the sandbox can't authenticate, so
+ runs fail. `--dry` needs no key.
+- `cue` on `PATH` (this repo's CLI) — `with-profile.ts` shells out to it.
+ Run the eval from a **plain shell**, not from inside a cue-launched session:
+ `CUE_LAUNCHING=1` trips cue's recursion guard and `--rematerialize` would fail.
+
+## Run
+
+```bash
+cd evals/agent-eval
+cp .env.example .env # add ANTHROPIC_API_KEY (or AI_GATEWAY_API_KEY)
+npm install # only needed to typecheck / run EVAL.ts locally
+
+npx @vercel/agent-eval --dry # preview all 3 profiles — no API calls, no cost
+npx @vercel/agent-eval # run all 3 (core, gstack, improver)
+npx @vercel/agent-eval core # run just one
+npx @vercel/agent-eval --smoke # one run per experiment — verify keys/sandbox
+```
+
+Results land in `results///`. Compare `passRate` in each
+`summary.json` across the three profiles.
+
+## Extend
+
+- **More tasks:** add `evals//` with `PROMPT.md` + `EVAL.ts` (+ `package.json`,
+ `src/`). They run against every experiment automatically.
+- **More profiles:** copy an experiment file and change `withProfile('')`.
+- **All profiles:** map `withProfile` over `cue list` output from a generator
+ script (heavier; most configs you'd never run — start with the 3 here).
+
+## Cost note
+
+Each run spins up a sandbox, installs Claude Code, and runs the agent. `runs: 3`
+× 3 profiles × tasks = real API tokens. Start with `--dry`, then `--smoke`, then
+a full run on a small task set.
diff --git a/evals/agent-eval/evals/create-button/EVAL.ts b/evals/agent-eval/evals/create-button/EVAL.ts
new file mode 100644
index 00000000..2eabbb27
--- /dev/null
+++ b/evals/agent-eval/evals/create-button/EVAL.ts
@@ -0,0 +1,23 @@
+import { readFileSync, existsSync } from 'fs';
+import { execSync } from 'child_process';
+import { test, expect } from 'vitest';
+
+test('Button component file exists', () => {
+ expect(existsSync('src/Button.tsx')).toBe(true);
+});
+
+test('Button accepts label and onClick props', () => {
+ const src = readFileSync('src/Button.tsx', 'utf-8');
+ expect(src).toContain('label');
+ expect(src).toContain('onClick');
+});
+
+test('index re-exports Button', () => {
+ const idx = readFileSync('src/index.ts', 'utf-8');
+ expect(idx).toMatch(/Button/);
+});
+
+test('project builds', () => {
+ // Throws (and fails the test) if the build fails.
+ execSync('npm run build', { stdio: 'pipe' });
+});
diff --git a/evals/agent-eval/evals/create-button/PROMPT.md b/evals/agent-eval/evals/create-button/PROMPT.md
new file mode 100644
index 00000000..740180ec
--- /dev/null
+++ b/evals/agent-eval/evals/create-button/PROMPT.md
@@ -0,0 +1,8 @@
+Create a reusable Button component.
+
+Requirements:
+- Export a `Button` component from `src/Button.tsx`
+- It accepts two props: `label` (string) and `onClick` (() => void)
+- It renders a `` that displays `label` and calls `onClick` when clicked
+- Update `src/index.ts` to re-export `Button`
+- The project must build with `npm run build`
diff --git a/evals/agent-eval/evals/create-button/package.json b/evals/agent-eval/evals/create-button/package.json
new file mode 100644
index 00000000..bfcf67d5
--- /dev/null
+++ b/evals/agent-eval/evals/create-button/package.json
@@ -0,0 +1,16 @@
+{
+ "name": "create-button",
+ "type": "module",
+ "scripts": {
+ "build": "tsc"
+ },
+ "dependencies": {
+ "react": "^18.0.0"
+ },
+ "devDependencies": {
+ "@types/node": "^22.0.0",
+ "@types/react": "^18.0.0",
+ "typescript": "^5.0.0",
+ "vitest": "^2.1.0"
+ }
+}
diff --git a/evals/agent-eval/evals/create-button/src/index.ts b/evals/agent-eval/evals/create-button/src/index.ts
new file mode 100644
index 00000000..1a9683e5
--- /dev/null
+++ b/evals/agent-eval/evals/create-button/src/index.ts
@@ -0,0 +1,3 @@
+// Starter code. The agent should create ./Button and re-export it here.
+// TODO: export { Button } from './Button';
+export {};
diff --git a/evals/agent-eval/evals/create-button/tsconfig.json b/evals/agent-eval/evals/create-button/tsconfig.json
new file mode 100644
index 00000000..0c0083d9
--- /dev/null
+++ b/evals/agent-eval/evals/create-button/tsconfig.json
@@ -0,0 +1,12 @@
+{
+ "compilerOptions": {
+ "target": "ES2020",
+ "module": "ESNext",
+ "moduleResolution": "bundler",
+ "jsx": "react-jsx",
+ "strict": true,
+ "outDir": "dist",
+ "skipLibCheck": true
+ },
+ "include": ["src"]
+}
diff --git a/evals/agent-eval/experiments/core.ts b/evals/agent-eval/experiments/core.ts
new file mode 100644
index 00000000..1ab22086
--- /dev/null
+++ b/evals/agent-eval/experiments/core.ts
@@ -0,0 +1,20 @@
+import type { ExperimentConfig } from '@vercel/agent-eval';
+import { withProfile } from '../lib/with-profile.js';
+
+// A/B variable = the cue PROFILE. Same task, same model across all three
+// experiments — only the injected CLAUDE.md (persona + rules + skill-routing)
+// changes. Compare pass rates: core vs gstack vs improver.
+const config: ExperimentConfig = {
+ agent: 'claude-code', // direct Anthropic; needs ANTHROPIC_API_KEY.
+ // Swap to 'vercel-ai-gateway/claude-code' + AI_GATEWAY_API_KEY for the gateway.
+ // model omitted → the claude CLI's native default, so the comparison is about
+ // the profile, not the model.
+ runs: 3, // a pass RATE is the signal, not a single run
+ earlyExit: false, // run all N even after a success
+ sandbox: 'docker', // Docker is available locally; no Vercel token needed
+ scripts: ['build'],
+ timeout: 600,
+ ...withProfile('core'),
+};
+
+export default config;
diff --git a/evals/agent-eval/experiments/gstack.ts b/evals/agent-eval/experiments/gstack.ts
new file mode 100644
index 00000000..1bc6cb6f
--- /dev/null
+++ b/evals/agent-eval/experiments/gstack.ts
@@ -0,0 +1,16 @@
+import type { ExperimentConfig } from '@vercel/agent-eval';
+import { withProfile } from '../lib/with-profile.js';
+
+// Treatment B: the full gstack 58-role profile. Compare its pass rate on the
+// same task against core (baseline) and improver.
+const config: ExperimentConfig = {
+ agent: 'claude-code',
+ runs: 3,
+ earlyExit: false,
+ sandbox: 'docker',
+ scripts: ['build'],
+ timeout: 600,
+ ...withProfile('gstack'),
+};
+
+export default config;
diff --git a/evals/agent-eval/experiments/improver.ts b/evals/agent-eval/experiments/improver.ts
new file mode 100644
index 00000000..40bef3a5
--- /dev/null
+++ b/evals/agent-eval/experiments/improver.ts
@@ -0,0 +1,16 @@
+import type { ExperimentConfig } from '@vercel/agent-eval';
+import { withProfile } from '../lib/with-profile.js';
+
+// Treatment C: the improver profile (goal-with-a-check loop). Compare its pass
+// rate on the same task against core (baseline) and gstack.
+const config: ExperimentConfig = {
+ agent: 'claude-code',
+ runs: 3,
+ earlyExit: false,
+ sandbox: 'docker',
+ scripts: ['build'],
+ timeout: 600,
+ ...withProfile('improver'),
+};
+
+export default config;
diff --git a/evals/agent-eval/lib/with-profile.ts b/evals/agent-eval/lib/with-profile.ts
new file mode 100644
index 00000000..f84ebf04
--- /dev/null
+++ b/evals/agent-eval/lib/with-profile.ts
@@ -0,0 +1,109 @@
+import { execFileSync } from 'node:child_process';
+import { existsSync, readFileSync } from 'node:fs';
+import { join } from 'node:path';
+import type { ExperimentConfig, Sandbox } from '@vercel/agent-eval';
+
+/**
+ * Make a cue PROFILE the measured variable.
+ *
+ * `setup` runs on the HOST (where `cue` lives). It self-primes the profile's
+ * runtime with:
+ *
+ * cue launch --rematerialize
+ *
+ * which materializes the full runtime — persona + rules + skill-routing +
+ * `_always` fragments, rendered into CLAUDE.md — WITHOUT exec'ing claude, and
+ * prints `{ runtimeDir }` as JSON (see src/commands/launch.ts: materializeRuntime
+ * runs before the --rematerialize gate). We read that CLAUDE.md and write it into
+ * the sandbox so the in-sandbox Claude Code runs under the profile's instructions.
+ *
+ * Deliberately NOT injected: MCP servers, hooks, and the headroom proxy env.
+ * Those need keys / network and would fail to start in the throwaway sandbox.
+ * The measured variable here is the profile's INSTRUCTIONS, not its infra — which
+ * is exactly what differs most between core / gstack / improver.
+ *
+ * Skill BODIES are not injected yet (the CLAUDE.md already carries each profile's
+ * skill list + routing). Injecting the materialized SKILL.md files is the obvious
+ * next extension — see the note at the bottom of this file.
+ */
+export function withProfile(profile: string): Pick {
+ return {
+ setup: async (sandbox: Sandbox) => {
+ const claudeMd = renderProfileClaudeMd(profile);
+ await sandbox.writeFiles({ 'CLAUDE.md': claudeMd });
+ },
+ };
+}
+
+/** Materialize a cue profile on the host and return its rendered CLAUDE.md. */
+export function renderProfileClaudeMd(profile: string): string {
+ let out: string;
+ try {
+ // The agent positional MUST be "claude" (selects agentKind claude-code);
+ // the profile is forced regardless of cwd via `--cue-profile `.
+ // A bare `cue launch ` leaves agent null → "missing agent" exit 1.
+ // stderr is piped (not inherited) so cue's rebuild diagnostics don't bleed
+ // into the eval harness output; it surfaces in the error on failure.
+ out = execFileSync(
+ 'cue',
+ ['launch', 'claude', '--cue-profile', profile, '--rematerialize'],
+ { encoding: 'utf8', stdio: ['ignore', 'pipe', 'pipe'] },
+ );
+ } catch (err) {
+ const stderr = (err as { stderr?: string }).stderr ?? '';
+ throw new Error(
+ `withProfile("${profile}"): \`cue launch claude --cue-profile ${profile} --rematerialize\` failed. ` +
+ `Is the \`cue\` CLI on PATH and "${profile}" a valid profile? ` +
+ `Run from a PLAIN shell — inside a cue session CUE_LAUNCHING=1 trips the recursion guard. ` +
+ `${stderr.trim() ? `cue stderr: ${stderr.trim()} — ` : ''}` +
+ `Original error: ${err instanceof Error ? err.message : String(err)}`,
+ );
+ }
+
+ // --rematerialize prints pretty JSON to stdout FOLLOWED BY a status line
+ // (e.g. "✅ Rematerialized."), so JSON.parse(out) would throw. Slice out the
+ // object. The payload (profile/agent/runtimeDir/rebuilt/hash) is all scalars,
+ // so first "{" .. last "}" is the whole object.
+ const start = out.indexOf('{');
+ const end = out.lastIndexOf('}');
+ if (start === -1 || end === -1) {
+ throw new Error(
+ `withProfile("${profile}"): no JSON in \`cue --rematerialize\` output:\n${out}`,
+ );
+ }
+ let runtimeDir: string;
+ try {
+ runtimeDir = (JSON.parse(out.slice(start, end + 1)) as { runtimeDir?: string })
+ .runtimeDir as string;
+ } catch (err) {
+ throw new Error(
+ `withProfile("${profile}"): could not parse runtimeDir from cue output. ` +
+ `Original error: ${err instanceof Error ? err.message : String(err)}`,
+ );
+ }
+ if (!runtimeDir) {
+ throw new Error(`withProfile("${profile}"): cue did not report a runtimeDir.`);
+ }
+
+ // CLAUDE.md sits at runtimeDir/CLAUDE.md or runtimeDir/claude/CLAUDE.md
+ // depending on how runtimeDir is reported. Check both.
+ const claudeMdPath = [
+ join(runtimeDir, 'CLAUDE.md'),
+ join(runtimeDir, 'claude', 'CLAUDE.md'),
+ ].find(existsSync);
+
+ if (!claudeMdPath) {
+ throw new Error(
+ `withProfile("${profile}"): no CLAUDE.md found under ${runtimeDir}.`,
+ );
+ }
+ return readFileSync(claudeMdPath, 'utf8');
+}
+
+// Extension point — inject skill bodies as well as the CLAUDE.md routing:
+// 1. From the same runtimeDir, read `skills/**/SKILL.md` (materialize symlinks
+// them; deref with realpathSync before reading).
+// 2. Write each to `.claude/skills//SKILL.md` in the sandbox alongside
+// CLAUDE.md so Claude Code auto-discovers them.
+// Left out of the first cut to keep the measured variable clean and the scaffold
+// small; the CLAUDE.md already differs sharply across profiles.
diff --git a/evals/agent-eval/package.json b/evals/agent-eval/package.json
new file mode 100644
index 00000000..be1a7868
--- /dev/null
+++ b/evals/agent-eval/package.json
@@ -0,0 +1,18 @@
+{
+ "name": "cue-agent-eval",
+ "version": "0.0.1",
+ "private": true,
+ "type": "module",
+ "description": "A/B test cue PROFILES with @vercel/agent-eval — same task, same model, the profile's CLAUDE.md is the only variable.",
+ "scripts": {
+ "dry": "agent-eval --dry",
+ "smoke": "agent-eval --smoke",
+ "eval": "agent-eval"
+ },
+ "devDependencies": {
+ "@vercel/agent-eval": "^1.2.0",
+ "@types/node": "^22.0.0",
+ "typescript": "^5.6.0",
+ "vitest": "^2.1.0"
+ }
+}
diff --git a/evals/agent-eval/tsconfig.json b/evals/agent-eval/tsconfig.json
new file mode 100644
index 00000000..5e050eda
--- /dev/null
+++ b/evals/agent-eval/tsconfig.json
@@ -0,0 +1,12 @@
+{
+ "compilerOptions": {
+ "target": "ES2022",
+ "module": "NodeNext",
+ "moduleResolution": "NodeNext",
+ "strict": true,
+ "skipLibCheck": true,
+ "noEmit": true,
+ "lib": ["ES2022"]
+ },
+ "include": ["experiments", "lib"]
+}
diff --git a/profiles/core/profile.yaml b/profiles/core/profile.yaml
index ea78d9cd..078807d1 100644
--- a/profiles/core/profile.yaml
+++ b/profiles/core/profile.yaml
@@ -19,21 +19,10 @@ contextWindow: 256000
# "Model selection" persona block below for when to reach for Opus vs Sonnet.
env:
CLAUDE_CODE_SUBAGENT_MODEL: claude-sonnet-4-6
- # Full-traffic wrap (global): route every cue-launched Claude session through
- # the local headroom compression proxy (60–95% fewer tokens, reversible).
- # Surfaced into settings.json via the runtime allowlist (claude-code only;
- # codex uses OPENAI_BASE_URL and is unaffected). HEALTH-GATED by the
- # materializer: this URL is only written when 127.0.0.1:8787 actually answers,
- # so a down/absent proxy falls back to talking to Anthropic directly (no
- # compression) instead of bricking Claude. Run the proxy as the always-on
- # `headroom-proxy` user service (`systemctl --user status headroom-proxy`,
- # linger enabled) so compression is actually on. Unset to opt a launch out.
- ANTHROPIC_BASE_URL: "http://127.0.0.1:8787"
persona_includes:
- integrity-protocol-compact # resources/personas/integrity-protocol-compact.md — compact 7-tag system inline; full protocol + examples load on demand (integrity-protocol.md / meta/integrity-tags). Fans out via inheritance.
- codegraph-routing # resources/personas/codegraph-routing.md — compact source-navigation rule: use CodeGraph before broad repo reads/grep so code exploration burns fewer tokens. Fans out via inheritance.
- skill-evolution # resources/personas/skill-evolution.md — post-task learning capture via bin/cue-learnings, fans out to every core-inheriting profile
- - headroom-compression # resources/personas/headroom-compression.md — tells every profile's agent to actively use headroom (proxy wrap is automatic; reach for the MCP on big payloads) to cut tokens. Fans out via inheritance even to profiles with their own persona (persona_includes is additive; inline persona is leaf-wins).
- fable-5-prompting-compact # resources/personas/fable-5-prompting-compact.md — Fable 5 / Mythos 5 behavioral deltas (effort, longer turns, grounding, boundaries); full guide on demand at fable-5-prompting.md. Fans out via inheritance.
persona: |
You're a careful, experienced collaborator. You operate by these defaults
@@ -74,6 +63,7 @@ persona: |
incrementally. Don't accumulate a 400-line patch you can't bisect.
- **Voice: terse and specific.** Drop filler. Lead sentences with the verb
or the answer. Don't summarize what's visible in the diff.
+ - **Prefer /compact over /clear.** `/compact` preserves the prompt cache; `/clear` cold-starts it and loses the cache that saves ~$7K/session. Compact at natural task boundaries instead of clearing.
## Model routing — route by task hardness, delegate the cheap work
@@ -93,16 +83,7 @@ persona: |
| 🔍 **EASY / SEARCH** | web/internet search, "find/look up/what is", reading docs, scraping, summarizing, listing files, locating code | **Delegate to a Sonnet subagent** (`Agent`/`Explore`) — return the conclusion, not the file dumps. |
| ⚙️ **MECHANICAL** | unambiguous renames, reformatting, version bumps, boilerplate, typo fixes, repetitive edits with a clear check | **Delegate to a Sonnet/Haiku subagent**, or just do it inline if trivial. |
- - **Delegate, don't grind.** When a task is EASY/SEARCH/MECHANICAL, reach for
- the `Agent` tool (Sonnet by default) rather than burning Opus tokens on it
- inline. The expensive model's value is the hard reasoning, not the lookup.
- - **Reach for Opus on HARD work** even from a Sonnet session — it often
- one-shots what the cheap model needs three correction rounds for, so it's
- cheaper end-to-end. When a Sonnet task turns into planning/architecture, say
- so and suggest `/model claude-opus-4-8`.
- - **Steer the main session, don't flip it mid-task.** Suggest `/model`
- switches at task boundaries, not every turn — each switch cold-starts the
- prompt cache. Batch model-appropriate work, then switch.
+ - HARD → main session / Opus. EASY/SEARCH/MECHANICAL → delegate to `Agent` (Sonnet). `/model` switches cold-start the prompt cache — batch model-appropriate work, then switch.
## The cue sprint
@@ -203,10 +184,13 @@ skills:
# github-actions-docs dropped from core 2026-06-01 — CI-docs is not universal
# baseline. Add it back per-profile (ops, the deploy-touching ones) if needed.
mcps:
- - codegraph # Repo code intelligence MCP — use before broad Read/Grep for architecture, feature, bug-context, symbol, caller/callee, and impact questions. Available to Claude and Codex runtimes via the sanitized registry snapshots.
- - lightpanda # Fast headless browser MCP — fetch/render/scrape URLs without Chromium. Pairs with browser/lightpanda skill.
- - context7 # Up-to-date, version-specific library docs (Upstash @upstash/context7-mcp). No API key needed; set CONTEXT7_API_KEY for higher limits. Pairs with tools/context7 skill.
- - headroom # Context-compression MCP (headroom_compress/retrieve/stats) — 60–95% fewer tokens, reversible. Pairs with tools/headroom skill. Inert until `pip install "headroom-ai[mcp]"`. The full all-traffic wrap (ANTHROPIC_BASE_URL → local proxy) IS set in core's env above, but health-gated by the materializer — applied only when the proxy answers, so a down proxy falls back to direct Anthropic instead of bricking Claude.
+ - dataforseo
+ # pin: true marks an MCP the agent uses directly (not via a skill), so the
+ # launcher's smart-prune keeps it always-on instead of dropping it.
+ - { id: codegraph, pin: true } # Repo code intelligence MCP — use before broad Read/Grep for architecture, feature, bug-context, symbol, caller/callee, and impact questions. Available to Claude and Codex runtimes via the sanitized registry snapshots.
+ - lightpanda # Fast headless browser MCP — fetch/render/scrape URLs without Chromium. Pairs with browser/lightpanda skill (prunable when that skill is inactive).
+ - { id: context7, pin: true } # Up-to-date, version-specific library docs (Upstash @upstash/context7-mcp). No API key needed; set CONTEXT7_API_KEY for higher limits. Pairs with tools/context7 skill.
+ - { id: headroom, pin: true } # Context-compression MCP (headroom_compress/retrieve/stats) — reversible targeted compression for large payloads. The full all-traffic proxy wrap is opt-in via the `headroom` profile, not inherited by core.
plugins:
- claude-mem@thedotmack
# Disabled 2026-05-30: OMC's PreToolUse auto-mode classifier was intermittently
diff --git a/profiles/headroom/profile.yaml b/profiles/headroom/profile.yaml
index ed04d78b..1a171325 100644
--- a/profiles/headroom/profile.yaml
+++ b/profiles/headroom/profile.yaml
@@ -2,10 +2,11 @@ name: headroom
icon: "🪶"
description: "Context-compression loadout — wraps all Claude traffic through the local headroom proxy (60–95% fewer tokens, reversible) and exposes the headroom_compress/retrieve/stats MCP tools. Inherits core."
inherits: core
+persona_includes:
+ - headroom-compression
-# MCP + skill are also in core (so every profile has the tools); listed here too
-# so this profile is self-contained even before the core change lands. Merge is
-# union-by-id, so the duplication dedupes.
+# MCP + skill are also in core for targeted compression; listed here too so this
+# profile is self-contained. Merge is union-by-id, so the duplication dedupes.
mcps:
- headroom # headroom_compress / headroom_retrieve / headroom_stats (stdio: `headroom mcp serve`)
@@ -18,9 +19,10 @@ skills:
# machine. ANTHROPIC_BASE_URL is surfaced into the materialized settings.json
# `env` via cue's runtime allowlist.
#
-# REQUIRES the proxy to be up: `headroom proxy --port 8787` (or `headroom wrap
-# claude`, which starts it for you). If the proxy is down, Claude cannot reach
-# Anthropic at all — for everyday use, run the proxy as a persistent service.
+# REQUIRES a healthy proxy: `headroom proxy --port 8787` (or `headroom wrap
+# claude`, which starts it for you). Cue health-gates this URL before writing it
+# into Claude settings; if the proxy is down or not answering `/health`, the
+# wrapper is dropped and Claude talks to Anthropic directly.
env:
ANTHROPIC_BASE_URL: "http://127.0.0.1:8787"
diff --git a/profiles/medusa-vite/profile.yaml b/profiles/medusa-vite/profile.yaml
index 19446c70..418ebb2b 100644
--- a/profiles/medusa-vite/profile.yaml
+++ b/profiles/medusa-vite/profile.yaml
@@ -40,40 +40,7 @@ persona: |
## Medusa JS SDK setup with TanStack
- Create one SDK instance for the app:
-
- ```ts
- // src/lib/medusa.ts
- import Medusa from "@medusajs/js-sdk"
- export const sdk = new Medusa({
- baseUrl: import.meta.env.VITE_MEDUSA_BACKEND_URL,
- publishableKey: import.meta.env.VITE_MEDUSA_PUBLISHABLE_KEY,
- })
- ```
-
- In a router loader:
-
- ```ts
- // src/routes/products.$handle.tsx
- export const Route = createFileRoute("/products/$handle")({
- loader: ({ params }) => sdk.store.product.retrieve(params.handle),
- component: ProductDetail,
- })
- function ProductDetail() {
- const product = Route.useLoaderData()
- return
- }
- ```
-
- In a mutation component (TanStack Query):
-
- ```ts
- const addLine = useMutation({
- mutationFn: (variantId: string) =>
- sdk.store.cart.createLineItem(cartId, { variant_id: variantId, quantity: 1 }),
- onSuccess: () => queryClient.invalidateQueries({ queryKey: ['cart'] }),
- })
- ```
+ See `playbooks/medusa-dev-workflow.md` for the SDK instance, router loader, and mutation examples.
## Region resolution
diff --git a/profiles/ros2/profile.yaml b/profiles/ros2/profile.yaml
new file mode 100644
index 00000000..f7911352
--- /dev/null
+++ b/profiles/ros2/profile.yaml
@@ -0,0 +1,31 @@
+name: ros2
+icon: "🤖"
+description: "ROS 2 robot control via rosbridge — ros-mcp native tools + ros-skill CLI over WebSocket :9090. Inherits core."
+inherits: core
+
+skills:
+ local:
+ # lpigeon/ros-skill — ROS/ROS2 over rosbridge WebSocket; ros_cli.py emits JSON.
+ - robotics/ros-skill
+
+mcps:
+ # robotmcp/ros-mcp-server — native MCP tools for ROS topics/services/nodes/params/actions.
+ # `uvx ros-mcp --transport=stdio`; connects out to rosbridge on the robot. Tools: mcp__ros-mcp__*.
+ - ros-mcp
+
+persona: |
+ You operate ROS 2 robots. Assume ROS 2 Humble unless told otherwise.
+
+ Two interchangeable interfaces reach the robot through rosbridge
+ (WebSocket, default port 9090):
+ - **ros-mcp** (`mcp__ros-mcp__*`) — native MCP tools. Prefer for
+ interactive topic / service / node / param / action work.
+ - **ros-skill** (`robotics/ros-skill`) — `ros_cli.py` CLI wrapper on the
+ same rosbridge backend. Use when you want scriptable JSON output.
+
+ Both require rosbridge running on the robot:
+ `ros2 launch rosbridge_server rosbridge_websocket_launch.xml`.
+
+ Connect by giving the robot's IP (e.g. "connect to the robot on
+ 10.42.0.x"). Confirm before commanding motion (`/cmd_vel`, action goals)
+ or any state change, and verify topic echo readouts before trusting them.
diff --git a/resources/claude-md/_always/karpathy-guidelines.md b/resources/claude-md/_always/karpathy-guidelines.md
index 3525089e..5db83115 100644
--- a/resources/claude-md/_always/karpathy-guidelines.md
+++ b/resources/claude-md/_always/karpathy-guidelines.md
@@ -1,63 +1,8 @@
## Karpathy Coding Guidelines
-Behavioral guidelines to reduce common LLM coding mistakes.
+Four principles to reduce common LLM coding mistakes. Bias toward caution over speed; for trivial tasks, use judgment.
-**Tradeoff:** These guidelines bias toward caution over speed. For trivial tasks, use judgment.
-
-### 1. Think Before Coding
-
-**Don't assume. Don't hide confusion. Surface tradeoffs.**
-
-Before implementing:
-- State your assumptions explicitly. If uncertain, ask.
-- If multiple interpretations exist, present them - don't pick silently.
-- If a simpler approach exists, say so. Push back when warranted.
-- If something is unclear, stop. Name what's confusing. Ask.
-
-### 2. Simplicity First
-
-**Minimum code that solves the problem. Nothing speculative.**
-
-- No features beyond what was asked.
-- No abstractions for single-use code.
-- No "flexibility" or "configurability" that wasn't requested.
-- No error handling for impossible scenarios.
-- If you write 200 lines and it could be 50, rewrite it.
-
-Ask yourself: "Would a senior engineer say this is overcomplicated?" If yes, simplify.
-
-### 3. Surgical Changes
-
-**Touch only what you must. Clean up only your own mess.**
-
-When editing existing code:
-- Don't "improve" adjacent code, comments, or formatting.
-- Don't refactor things that aren't broken.
-- Match existing style, even if you'd do it differently.
-- If you notice unrelated dead code, mention it - don't delete it.
-
-When your changes create orphans:
-- Remove imports/variables/functions that YOUR changes made unused.
-- Don't remove pre-existing dead code unless asked.
-
-The test: Every changed line should trace directly to the user's request.
-
-### 4. Goal-Driven Execution
-
-**Define success criteria. Loop until verified.**
-
-Transform tasks into verifiable goals:
-- "Add validation" → "Write tests for invalid inputs, then make them pass"
-- "Fix the bug" → "Write a test that reproduces it, then make it pass"
-- "Refactor X" → "Ensure tests pass before and after"
-
-For multi-step tasks, state a brief plan:
-```
-1. [Step] → verify: [check]
-2. [Step] → verify: [check]
-3. [Step] → verify: [check]
-```
-
-Strong success criteria let you loop independently. Weak criteria ("make it work") require constant clarification.
-
-**These guidelines are working if:** fewer unnecessary changes in diffs, fewer rewrites due to overcomplication, and clarifying questions come before implementation rather than after mistakes.
+1. **Think first.** State assumptions explicitly; surface all interpretations; stop and ask when unclear.
+2. **Simplicity first.** Minimum code that solves the problem. No speculative features, abstractions, or error handling for impossible cases. If you write 200 lines and it could be 50, rewrite it.
+3. **Surgical changes.** Touch only what the task requires. Don't improve adjacent code; match existing style; remove only the imports/functions YOUR changes made unused.
+4. **Goal-driven.** Define a runnable check before starting. Loop until it passes. Multi-step tasks: state a brief plan `[step] → verify: [check]` first.
diff --git a/resources/claude-md/_always/measure-empirically.md b/resources/claude-md/_always/measure-empirically.md
new file mode 100644
index 00000000..3dbe2920
--- /dev/null
+++ b/resources/claude-md/_always/measure-empirically.md
@@ -0,0 +1,18 @@
+## Measure, don't vibe
+
+A claim about effectiveness — "this profile is better", "the skill helped",
+"the new prompt works" — is a hypothesis until a number backs it. Before you
+assert an improvement, name the check that would prove it, then run it.
+
+- **Code changes:** the check is a test, a build, a benchmark, or a before/after
+ metric. "Compiles" is not "works" (see Karpathy guideline #4: define a runnable
+ check before starting).
+- **Agent / profile / skill effectiveness:** measure with an eval, not a vibe.
+ This repo ships an A/B harness at `evals/agent-eval/` (built on
+ `@vercel/agent-eval`): same task, same model, one variable (the profile or
+ skill), reported as a pass RATE over N runs. Reach for it when comparing
+ profiles, judging whether an MCP or skill earns its tokens, or checking a model
+ swap. `npx @vercel/agent-eval --dry` previews with no cost.
+
+If you can't measure it yet, say so and name the check you would run — don't
+report an unverified win as a fact.
diff --git a/resources/commands/goal.md b/resources/commands/goal.md
index 68b9b01d..f402c11e 100644
--- a/resources/commands/goal.md
+++ b/resources/commands/goal.md
@@ -10,6 +10,27 @@ target and a check that closes it — and replaces "stop when done" with "prove
it, then propose the next goal and continue." Each passing check is a
checkpoint, not the exit.
+**TL;DR:** restate the ask as outcome + a runnable check → reach it by the
+highest-ROI path → loop (smallest change, then run the check) until it passes →
+commit → propose the next checkable goal and continue. Interactive asks before
+each new goal; `auto` self-picks. Three fails on one check is a hard stop.
+
+## Arguments
+
+`/goal [topic] [auto] [cap=N]` — all optional.
+
+- **topic** (free text) — the request to pursue. Omitted → use the current task
+ in context.
+- **auto** — run unattended: each round, pick the top-ROI next goal yourself and
+ continue without asking. Also implied when the user says "keep going", "work
+ longer", or "until I say stop". Absent → **interactive** (the default): ask
+ before starting each new goal.
+- **cap=N** — max continuation rounds before pausing to confirm (default 5).
+ `cap=1` runs one goal and stops at Step 5; "until I say stop" lifts the cap.
+
+The chosen mode (interactive vs auto) and cap are set here once and govern the
+Step 6 fork for the whole run.
+
## Step 1 — Restate as a measurable goal + a runnable check
Rewrite the ask as **outcome + a success criterion you can run.**
@@ -67,13 +88,14 @@ ending the session.
- **upside** — a sharper, more precise version of what you just shipped
3. **Rank by ROI.** Run `/roi-estimator` when there are 3+ so each carries a
`dimension +N% 🟢/🟡/🟠` tag.
-4. **Fork on the next goal:**
- - **Interactive:** ask via `AskUserQuestion` — "what to implement next?" with
- the top-ROI goal first (recommended) and **"Stop here"** as the last
- option. Name the check in each option so the user picks a provable goal.
- - **Auto mode:** take the top-ROI goal yourself, log one line (`picked X
- because Y`), and continue. Pause to ask only when the best remaining ROI is
- 🟠 or lower, or you're out of checkable ideas.
+4. **Fork on the next goal** (per the mode set in Arguments):
+ - **Interactive** (default): ask via `AskUserQuestion` — "what to implement
+ next?" with the top-ROI goal first (recommended) and **"Stop here"** as the
+ last option. Name the check in each option so the user picks a provable goal.
+ - **Auto** (`/goal auto`, or "keep going" / "work longer"): take the top-ROI
+ goal yourself, log one line (`picked X because Y`), and continue. Pause to
+ ask only when the best remaining ROI is 🟠 or lower, or you're out of
+ checkable ideas.
5. **Restart at Step 1** with the chosen goal as the new measurable goal. One
goal in flight at a time — chain them, never braid two into one diff.
@@ -84,10 +106,10 @@ ending the session.
- **3-strike rule is a hard stop**, not a suggestion. A wall *ends the
marathon*; it never gets auto-picked around — escalate to `/investigate` or
the user.
-- **Continuation is bounded and consensual.** Default cap is **5 rounds**, then
- summarize and ask whether to keep going (a yes resets the cap). User can set
- it: `/goal cap=N` or "until I say stop." Stop immediately on "stop", or when
- two rounds in a row produce no checkable idea.
+- **Continuation is bounded and consensual.** At the `cap` (see Arguments,
+ default 5 rounds), summarize and ask whether to keep going — a yes resets the
+ cap. Stop immediately on "stop", or when two rounds in a row produce no
+ checkable idea.
- **One goal in flight, atomic commit per round.** Chain goals; keep the loop
bisectable.
- A goal you "verified" by reading code instead of running the check is
diff --git a/resources/hooks/context-size-nudge.sh b/resources/hooks/context-size-nudge.sh
index daed0961..01e029ff 100755
--- a/resources/hooks/context-size-nudge.sh
+++ b/resources/hooks/context-size-nudge.sh
@@ -33,8 +33,8 @@ sid="$(printf '%s' "$input" | jq -r '.session_id // empty' 2>/dev/null)" || exit
bytes="$(stat -c %s "$transcript" 2>/dev/null || stat -f %z "$transcript" 2>/dev/null)" || exit 0
[ -n "$bytes" ] || exit 0
-mb1="${CUE_CONTEXT_NUDGE_MB:-3}"
-mb2="${CUE_CONTEXT_NUDGE_MB2:-8}"
+mb1="${CUE_CONTEXT_NUDGE_MB:-2}"
+mb2="${CUE_CONTEXT_NUDGE_MB2:-5}"
t1=$(( mb1 * 1024 * 1024 ))
t2=$(( mb2 * 1024 * 1024 ))
diff --git a/resources/personas/fable-5-prompting-compact.md b/resources/personas/fable-5-prompting-compact.md
index 4da25888..003c7207 100644
--- a/resources/personas/fable-5-prompting-compact.md
+++ b/resources/personas/fable-5-prompting-compact.md
@@ -1,14 +1,14 @@
## Prompting Claude Fable 5 / Mythos 5 (compact)
-When this session runs on **Claude Fable 5** or **Claude Mythos 5**, apply these behavioral deltas. Older, more prescriptive instructions can degrade Fable 5's output — steer briefly rather than enumerating every case. (On Opus/Sonnet/Haiku, ignore the effort and refusal notes; the rest is harmless.)
+When this session runs on **Claude Fable 5** or **Claude Mythos 5**, apply these behavioral deltas. (On Opus/Sonnet/Haiku, effort and refusal notes don't apply; the rest is harmless.)
-- **Effort is the main dial.** Default `high`; `xhigh` for the hardest, capability-sensitive work; `medium`/`low` for routine. Lower effort on Fable 5 often beats `xhigh` on prior models. Drop effort if a task finishes correctly but slower than needed.
-- **Longer turns by default.** Hard tasks run many minutes; autonomous runs, hours. Expect async; don't block. When you have enough to act, act — don't re-derive settled facts, re-litigate decided choices, or narrate options you won't pursue (thinking blocks excepted).
-- **No unrequested tidying.** At higher effort it over-builds. Don't add features, refactors, abstractions, or defensive error-handling beyond the task; trust internal guarantees and validate only at real boundaries (user input, external APIs).
-- **Ground progress claims.** On long runs, audit each status claim against a tool result from this session before reporting it; if unverified, say so. State outcomes plainly — failing tests with their output, skipped steps as skipped.
-- **State the boundaries.** When the user is thinking out loud or asking a question, the deliverable is your assessment — report and stop; don't apply a fix or take unrequested actions (drafting emails, backup branches) until asked. Before a state-changing command, check the evidence supports that specific action.
-- **Delegate readily.** Fable 5 dispatches parallel subagents reliably; prefer async over blocking on each, and intervene only if one goes off track. (cue already pins Task/Agent subagents to Sonnet.)
-- **Memory pays off.** Fable 5 reuses recorded lessons well — one lesson per note, one-line summary on top, record corrections and confirmed approaches with why; update rather than duplicate; delete what proves wrong.
-- **Don't ask it to echo its reasoning.** Instructions to transcribe or explain its internal thinking as response text can trigger Fable 5's `reasoning_extraction` refusal and fall back to Opus. Read structured `thinking` blocks instead.
+- **Effort is the main dial.** Default `high`; `xhigh` for hardest work; `medium`/`low` for routine. Drop effort if a task finishes correctly but slower than needed.
+- **Longer turns by default.** Hard tasks run minutes; autonomous runs, hours. When you have enough to act, act — don't re-derive settled facts or narrate options you won't pursue.
+- **No unrequested tidying.** Don't add features, refactors, or abstractions beyond the task; validate only at real boundaries (user input, external APIs).
+- **Ground progress claims.** Audit each status claim against a tool result from this session; if unverified, say so. State outcomes plainly — failing tests with output, skipped steps as skipped.
+- **State the boundaries.** When the user is thinking out loud, the deliverable is your assessment — report and stop; don't apply a fix or take unrequested actions until asked.
+- **Delegate readily.** Prefer async parallel subagents; intervene only if one goes off track. (cue pins Task/Agent subagents to Sonnet.)
+- **Memory pays off.** One lesson per note, one-line summary on top; record corrections and confirmed approaches with why; update rather than duplicate; delete what proves wrong.
+- **Don't echo reasoning.** Instructions to transcribe internal thinking can trigger `reasoning_extraction` refusal. Read structured `thinking` blocks instead.
-> Full guide (capability map, send-to-user tool, scaffolding + migration notes): `resources/personas/fable-5-prompting.md`.
+> Full guide: `resources/personas/fable-5-prompting.md`.
diff --git a/resources/personas/headroom-compression.md b/resources/personas/headroom-compression.md
index 7f48343e..2a215c4a 100644
--- a/resources/personas/headroom-compression.md
+++ b/resources/personas/headroom-compression.md
@@ -1,10 +1,10 @@
## Save tokens with headroom
-Every cue session routes Claude traffic through the local **headroom** compression
-proxy (`ANTHROPIC_BASE_URL` → 127.0.0.1:8787): prompts, tool outputs, and history
-are compressed before they reach the model (60–95% fewer tokens, reversible). The
-baseline wrap is automatic — on by default, nothing for you to do. It is
-health-gated, so a down proxy falls back to direct Anthropic instead of breaking.
+This session has opted into the local **headroom** compression proxy
+(`ANTHROPIC_BASE_URL` → 127.0.0.1:8787): prompts, tool outputs, and history are
+compressed before they reach the model (60–95% fewer tokens, reversible). The
+wrap is health-gated at launch, so a down or unresponsive proxy falls back to
+direct Anthropic instead of breaking.
**For large in-turn payloads, reach for the headroom MCP.** Before pouring a big
blob into context (long logs, file dumps, command output, RAG chunks), run it
@@ -13,5 +13,8 @@ with `headroom_retrieve` when you need a dropped detail; check savings with
`headroom_stats`. Compression is reversible (CCR) — prefer it over truncating or
guessing.
-Connection errors mean the proxy is down; the wrap fails open to direct Anthropic,
-so work continues. Restart it with `headroom proxy --port 8787`.
+**Threshold: compress any Bash output >2 KB or any file read >5 KB.** Call `headroom_compress(content=)` before reasoning over it; the compressed view is what stays in context.
+
+Connection or saturation errors mean the proxy is down, unhealthy, or overloaded.
+Restart it with `headroom proxy --port 8787`, or relaunch without the `headroom`
+profile for direct Anthropic.
diff --git a/skill-md-lint-action/README.md b/skill-md-lint-action/README.md
index f2f6d3b1..61ab32fe 100644
--- a/skill-md-lint-action/README.md
+++ b/skill-md-lint-action/README.md
@@ -69,6 +69,7 @@ That's it. On every PR you'll get a comment with the lint report; on push to mai
| R011 missing example block | info | no |
| R012 possible duplicate skill (needs `check-overlap: true`) | warning | no |
| R013 description/body word-overlap mismatch | info | no |
+| R015 security: dangerous markdown (`javascript:`/raw ` here.\n`;
+ expect(lint(live).diagnostics.find((d) => d.rule === "R015")?.severity).toBe("error");
+ const doc = fm + "```html\n\n```\n";
+ expect(lint(doc).diagnostics.find((d) => d.rule === "R015")).toBeUndefined();
+ });
+
+ test("invisible/bidi character anywhere is flagged AND auto-fixable", () => {
+ const md = fm + "A line with a word\u2060joiner hidden in it.\n";
+ const diag = lint(md).diagnostics.find((d) => d.rule === "R015");
+ expect(diag?.severity).toBe("error");
+ expect(diag?.fix).toBeDefined();
+ const { fixed } = applyFixes(md);
+ expect(/[\u200b-\u200f\u202a-\u202e\u2060-\u2064\u2066-\u2069\ufeff\u00ad]/.test(fixed)).toBe(false);
+ expect(fixed).toContain("wordjoiner");
+ });
+
+ test("a clean skill produces no R015 diagnostics", () => {
+ const md = fm + "Use https://example.com and a normal [link](https://example.com).\n";
+ expect(lint(md).diagnostics.find((d) => d.rule === "R015")).toBeUndefined();
+ });
+
+ test("lint-ignore R015 suppresses the finding (escape hatch for security-doc skills)", () => {
+ const md = `---\nname: x\ndescription: Use when X.\nlint-ignore: R015\n---\n\n# X\n\nRaw shown deliberately.\n`;
+ expect(lint(md).diagnostics.find((d) => d.rule === "R015")).toBeUndefined();
+ });
+
+ test("prose with an on-prefixed word assigned a value does NOT false-positive", () => {
+ for (const line of ["The job runs once = 'daily'.", "Set online = 'false' to disable.", "The only = 'admin' rule applies."]) {
+ const md = fm + line + "\n";
+ expect(lint(md).diagnostics.find((d) => d.rule === "R015")).toBeUndefined();
+ }
+ });
+
+ test("unquoted inline event handler inside a tag IS flagged", () => {
+ const md = fm + `An tag.\n`;
+ expect(lint(md).diagnostics.find((d) => d.rule === "R015")?.severity).toBe("error");
+ });
+
+ test("raw