Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion .cue.profile
Original file line number Diff line number Diff line change
@@ -1 +1 @@
media
core
12 changes: 10 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,16 @@ Key paths:

- Preserve user work. Do not revert, reset, or overwrite unrelated changes.
- Prefer small, source-backed changes over broad rewrites.
- For repo architecture, feature-flow, or symbol-impact questions, use
CodeGraph before broad file reads or grep.
- For context-heavy files, inspect with `wc`, narrow `rg`, `sed -n`, `head`,
or `tail` before reading more.
- Do not paste large fixtures, catalogs, generated files, full logs, or full
setup manuals into chat.
- Verify with the smallest check that proves the touched surface. For profile
edits, start with `cue validate <profile>` plus targeted tests; run
`cue validate --all` only when requested or when a shared loader/materializer
change creates real cross-profile risk.
- For default-profile behavior, source of truth is `src/commands/init.ts` and
profile resolution tests under `src/lib/cwd-resolver.test.ts`.

Expand All @@ -55,10 +61,12 @@ Avoid reading these by default:
- `resources/skills/skills/**/test/fixtures/*`
- `resources/skills/skills/**/fixtures/*`
- `docs/assets/*.svg`
- `dist/`, `node_modules/`, coverage output, package-manager caches
- `dist/`, `node_modules/`, coverage output, generated output, submodule
dependency trees, package-manager caches
- `~/.config/cue/analytics.jsonl`, `~/.config/cue/session-log.jsonl`

If one is required, sample it first and cap output.
If one is required, sample it first and cap output. For broad `rg`, use
explicit `--glob` excludes for these traps.

## After Bootstrap

Expand Down
2 changes: 2 additions & 0 deletions profiles/core/profile.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ env:
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.
Expand Down Expand Up @@ -193,6 +194,7 @@ 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.
Expand Down
2 changes: 2 additions & 0 deletions profiles/seo/profile.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,8 @@ persona: |
need API keys. Wire them per engagement; the core skills work without keys.
- Inherits core's defaults (verify before done, minimum-viable change, review
the diff) and the integrity protocol.
hooks:
- seo-schema-validate.json
skills:
npx:
- repo: AgriciDaniel/claude-seo
Expand Down
17 changes: 17 additions & 0 deletions resources/hooks/seo-schema-validate.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
{
"hooks": {
"PostToolUse": [
{
"matcher": "Edit|Write",
"hooks": [
{
"type": "command",
"command": "bash ${CLAUDE_CONFIG_DIR}/hooks/seo-schema-validate.sh",
"description": "Validate JSON-LD schema markup after file edits — blocks on deprecated types, placeholder text, or broken @context/@type. From claude-seo.",
"id": "cue:post:write:seo-schema-validate"
}
]
}
]
}
}
132 changes: 132 additions & 0 deletions resources/hooks/seo-schema-validate.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
#!/usr/bin/env python3
"""Post-edit schema validation hook for Claude Code.

Validates JSON-LD schema after file edits. Returns exit code 2 to block
if critical validation errors found. Ported from AgriciDaniel/claude-seo.
"""

import json
import re
import sys
import os
from typing import List


def validate_jsonld(content: str) -> List[str]:
"""Validate JSON-LD blocks in HTML content."""
errors = []
pattern = r'<script\s+type=["\']application/ld\+json["\']\s*>(.*?)</script>'
blocks = re.findall(pattern, content, re.DOTALL | re.IGNORECASE)

if not blocks:
return []

for i, block in enumerate(blocks, 1):
block = block.strip()
try:
data = json.loads(block)
except json.JSONDecodeError as e:
errors.append(f"Block {i}: Invalid JSON; {e}")
continue

if isinstance(data, list):
for item in data:
errors.extend(_validate_schema_object(item, i))
elif isinstance(data, dict):
errors.extend(_validate_schema_object(data, i))

return errors


def _validate_schema_object(obj: dict, block_num: int) -> List[str]:
"""Validate a single schema object."""
errors = []
prefix = f"Block {block_num}"

if "@context" not in obj:
errors.append(f"{prefix}: Missing @context")
elif obj["@context"] not in ("https://schema.org", "http://schema.org"):
errors.append(f"{prefix}: @context should be 'https://schema.org'")

if "@type" not in obj:
errors.append(f"{prefix}: Missing @type")

placeholders = [
"[Business Name]", "[City]", "[State]", "[Phone]", "[Address]",
"[Your", "[INSERT", "REPLACE", "[URL]", "[Email]",
]
text = json.dumps(obj)
for p in placeholders:
if p.lower() in text.lower():
errors.append(f"{prefix}: Contains placeholder text: {p}")

schema_type = obj.get("@type", "")
deprecated = {
"HowTo": "deprecated September 2023",
"SpecialAnnouncement": "deprecated July 31, 2025",
"CourseInfo": "retired June 2025",
"EstimatedSalary": "retired June 2025",
"LearningVideo": "retired June 2025",
"ClaimReview": "retired June 2025; fact-check rich results discontinued",
"VehicleListing": "retired June 2025; vehicle listing structured data discontinued",
}
if schema_type in deprecated:
errors.append(f"{prefix}: @type '{schema_type}' is {deprecated[schema_type]}")

# FAQPage intentionally NOT flagged: Google retired FAQ rich results (May 2026)
# but markup still aids AI Mode / AI Overviews entity resolution.

return errors


def main():
if len(sys.argv) < 2:
sys.exit(0)

filepath = sys.argv[1]

if not os.path.isfile(filepath):
sys.exit(0)

valid_extensions = (".html", ".htm", ".jsx", ".tsx", ".vue", ".svelte", ".php", ".ejs")
if not filepath.lower().endswith(valid_extensions):
sys.exit(0)

MAX_FILE_BYTES = 10 * 1024 * 1024 # 10 MiB
try:
if os.path.getsize(filepath) > MAX_FILE_BYTES:
sys.exit(0)
except OSError:
sys.exit(0)

try:
with open(filepath, "r", encoding="utf-8", errors="ignore") as f:
content = f.read()
except (OSError, IOError):
sys.exit(0)

errors = validate_jsonld(content)

if not errors:
sys.exit(0)

critical_keywords = ["placeholder", "deprecated", "retired"]
critical = [e for e in errors if any(kw in e.lower() for kw in critical_keywords)]
warnings = [e for e in errors if e not in critical]

if warnings:
print("⚠️ Schema validation warnings:")
for w in warnings:
print(f" - {w}")

if critical:
print("🛑 Schema validation ERRORS (blocking):")
for e in critical:
print(f" - {e}")
sys.exit(2)

sys.exit(1)


if __name__ == "__main__":
main()
36 changes: 36 additions & 0 deletions resources/hooks/seo-schema-validate.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
#!/usr/bin/env bash
# PostToolUse:Edit|Write — validate JSON-LD schema after file edits.
#
# Reads the hook payload from stdin, extracts tool_input.file_path, then
# delegates to seo-schema-validate.py. Ported from AgriciDaniel/claude-seo.
#
# Exit 0 = ok, exit 1 = warnings (non-blocking), exit 2 = block.
set -euo pipefail

payload="$(cat -)"
filepath="$(printf '%s' "$payload" | python3 -c '
import sys, json
try:
d = json.load(sys.stdin)
print(d.get("tool_input", {}).get("file_path", ""))
except Exception:
pass
' 2>/dev/null)"

[[ -z "$filepath" ]] && exit 0

py_hook="${CLAUDE_CONFIG_DIR}/hooks/seo-schema-validate.py"
[[ -f "$py_hook" ]] || exit 0

# Prefer explicit override, then python3, then python.
if [[ -n "${CLAUDE_SEO_PYTHON:-}" ]]; then
python_bin="$CLAUDE_SEO_PYTHON"
elif command -v python3 &>/dev/null; then
python_bin="python3"
elif command -v python &>/dev/null; then
python_bin="python"
else
exit 0 # No Python found; skip validation rather than block.
fi

exec "$python_bin" "$py_hook" "$filepath"
11 changes: 11 additions & 0 deletions resources/personas/codegraph-routing.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
## Token-efficient repo workflow

For code questions in an indexed repo, keep exploration narrow:

- Start with `codegraph_context` for feature, architecture, bug-context, or "how does X work" questions.
- Use `codegraph_files` for structure and `codegraph_search` / `codegraph_explore` for targeted source. Prefer one batched explore over many reads.
- Fall back to `rg` or file reads only when CodeGraph lacks the file, the index is stale, or exact text/fixtures are needed.
- Do not run broad searches through `node_modules`, generated output, build/cache dirs, coverage, `.git`, or submodule dependency trees. Use targeted `rg --glob` excludes and cap output.
- Verify with the smallest proof first: focused test, touched command, or `cue validate <profile>` for profile edits. Do not run `cue validate --all`, full test suites, or broad audits unless requested, required by the touched surface, or a targeted check exposes cross-profile risk.

This keeps repo exploration out of the main transcript and lowers token burn.
2 changes: 1 addition & 1 deletion resources/skills
Submodule skills updated 1 files
+20 −3 catalog/catalog.json
12 changes: 12 additions & 0 deletions src/commands/launch.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -880,6 +880,18 @@ describe("formatTokenWarning", () => {
});
expect(out).toContain(" 💡 Run `cue skills audit` to trim unused skills.");
});

test("very heavy profiles get explicit launch guidance", () => {
const out = formatTokenWarning({
alwaysOn: 66000,
maxIfAllActivate: 520000,
totalSkills: 102,
byProfile: [],
heaviestBodies: [],
});
expect(out.join("\n")).toContain("Very heavy profile:");
expect(out.join("\n")).toContain('use `--subset "<task>"`');
});
});

describe("formatDoctorWarnings", () => {
Expand Down
5 changes: 5 additions & 0 deletions src/commands/launch.ts
Original file line number Diff line number Diff line change
Expand Up @@ -445,6 +445,11 @@ export function formatTokenWarning(b: TokenBreakdown): string[] {
lines.push(
`${level} Skill overhead: ${c.yellow(`~${alwaysK}`)} always-on (${b.totalSkills} skills)`,
);
if (b.alwaysOn >= 50_000) {
lines.push(
` ${c.yellow("Very heavy profile:")} prefer \`core\` or a narrow stack; use \`--subset "<task>"\` before launching broad composites.`,
);
}

// `byProfile[0]` is the primary (the profile the user actively picked);
// the rest are companions added via the multiselect. We tag whichever part
Expand Down
3 changes: 3 additions & 0 deletions src/lib/dashboard-server.ts
Original file line number Diff line number Diff line change
Expand Up @@ -422,6 +422,8 @@ interface ProfileDetail {
subagents: SubagentRef[];
/** External CLI tools the profile's skills declare (frontmatter Bash refs). */
clis: ProfileCli[];
/** Companion profiles the active profile recommends pairing with. */
recommends: string[];
}

/** Pull a `uses:` (or `mcps:`) frontmatter list out of a SKILL.md, if present. */
Expand Down Expand Up @@ -727,6 +729,7 @@ export async function handleProfileDetail(params: URLSearchParams): Promise<ApiR
playbooks,
subagents,
clis,
recommends: profile.recommends ?? [],
};

profileDetailCache.set(name, { ts: Date.now(), data });
Expand Down
37 changes: 32 additions & 5 deletions src/lib/pair-suggestions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -219,18 +219,43 @@ describe("buildUniversalSuggestions", () => {
]);
});

test("pinned companions (gstack) close the list when installed, after featured/frequent", () => {
const gstack = UNIVERSAL_COMPANIONS[0]!; // "gstack"
test("no heavy companion is pinned globally by default", () => {
expect(UNIVERSAL_COMPANIONS).toEqual([]);
const affinity = computeAffinityMap(lines(...Array(5).fill(row("a"))));
const out = buildUniversalSuggestions({
featured: ["f1"],
affinity,
known: known("f1", "a", gstack),
known: known("f1", "a", "gstack"),
});
expect(out).toEqual([
{ name: "f1", origin: "featured" },
{ name: "a", origin: "frequent" },
{ name: gstack, origin: "pinned" },
]);
});

test("a featured heavy profile can still be offered explicitly", () => {
const gstack = "gstack";
const out = buildUniversalSuggestions({
featured: [gstack],
affinity: new Map(),
known: known(gstack),
});
expect(out).toEqual([{ name: gstack, origin: "featured" }]);
});

test("pinned companions close the list when configured, after featured/frequent", () => {
const pinned = "lightweight-helper";
const affinity = computeAffinityMap(lines(...Array(5).fill(row("a"))));
const out = buildUniversalSuggestions({
featured: ["f1"],
affinity,
known: known("f1", "a", pinned),
pinnedCompanions: [pinned],
});
expect(out).toEqual([
{ name: "f1", origin: "featured" },
{ name: "a", origin: "frequent" },
{ name: pinned, origin: "pinned" },
]);
});

Expand All @@ -239,16 +264,18 @@ describe("buildUniversalSuggestions", () => {
featured: ["f1"],
affinity: new Map(),
known: known("f1"), // gstack not installed
pinnedCompanions: ["lightweight-helper"],
});
expect(out).toEqual([{ name: "f1", origin: "featured" }]);
});

test("a pinned companion already in featured keeps its featured origin (de-duped)", () => {
const gstack = UNIVERSAL_COMPANIONS[0]!;
const gstack = "gstack";
const out = buildUniversalSuggestions({
featured: [gstack],
affinity: new Map(),
known: known(gstack),
pinnedCompanions: [gstack],
});
expect(out).toEqual([{ name: gstack, origin: "featured" }]);
});
Expand Down
Loading
Loading