diff --git a/.claude-config/bin/tests/t3-html-render-test.py b/.claude-config/bin/tests/t3-html-render-test.py new file mode 100644 index 0000000..29e0ac3 --- /dev/null +++ b/.claude-config/bin/tests/t3-html-render-test.py @@ -0,0 +1,71 @@ +#!/usr/bin/env python3 +"""Check inline script syntax and the example's reduced-motion chart startup. + +Run the shipped renderCost function with a small SVG host. The intro's CSS +starts its marks hidden, so reduced motion must skip that class, not just the +animation. Node is already required by the repository's syntax gate. +""" +import json +import re +import subprocess +import unittest +from pathlib import Path + +ASSETS = Path(__file__).resolve().parents[3] / "SKILLS/t3-html-render/assets" + + +def node(source, payload): + result = subprocess.run( + ["node", "--input-type=commonjs", "-e", source], + input=json.dumps(payload), text=True, capture_output=True, timeout=15, + check=True, + ) + return json.loads(result.stdout) + + +class HtmlRender(unittest.TestCase): + def test_inline_scripts_parse(self): + for path in sorted(ASSETS.glob("*.html")): + with self.subTest(page=path.name): + scripts = re.findall(r"", path.read_text()) + self.assertTrue(scripts, "No inline script found") + self.assertEqual(node(""" +const scripts=JSON.parse(require('node:fs').readFileSync(0,'utf8')); +for(const script of scripts)new (require('node:vm').Script)(script); +console.log(JSON.stringify(scripts.length)); +""", scripts), len(scripts)) + + def test_chart_intro_respects_motion_and_only_runs_once(self): + html = (ASSETS / "example-report.html").read_text() + match = re.search( + r"(function renderCost\(\)\{[\s\S]*?\n\})\nfunction updateCost", html, + ) + self.assertIsNotNone(match, "renderCost not found") + for reduced in (False, True): + with self.subTest(reduced_motion=reduced): + result = node(""" +const {source,reduced}=JSON.parse(require('node:fs').readFileSync(0,'utf8')); +const classes=[]; +const box={clientWidth:344,textContent:''}; +const $=()=>box; +const window={matchMedia:query=>{ + if(query!=='(prefers-reduced-motion: reduce)')throw Error('Unexpected media query'); + return {matches:reduced}; +}}; +const OPT=['A','B','C'].map(k=>({k,c:'#888'})); +let firstCost=true,CR=null; +function el(tag,attrs,parent){ + const names=[]; + if(tag==='svg')classes.push(names); + return {style:{},classList:{add:name=>names.push(name)}, + addEventListener(){},setAttribute(){}}; +} +function updateCost(){} +eval(source+';renderCost();renderCost();'); +console.log(JSON.stringify(classes)); +""", {"source": match.group(1), "reduced": reduced}) + self.assertEqual(result, [([] if reduced else ["intro"]), []]) + + +if __name__ == "__main__": + unittest.main() diff --git a/.claude-config/bin/verify-ops-selftest.sh b/.claude-config/bin/verify-ops-selftest.sh index 31f699a..adca6aa 100755 --- a/.claude-config/bin/verify-ops-selftest.sh +++ b/.claude-config/bin/verify-ops-selftest.sh @@ -8,7 +8,8 @@ # the skill index and size checks (9, 9b), and the secrets scan exits (8) pass the shipped tree # against current Stage-1-shaped settings, and fail on each class they exist to # catch. It also pins the shipped .gitattributes rule that merges pool -# MEMORY.md indexes with the union driver. +# MEMORY.md indexes with the union driver, and the shipped .gitignore rules +# that keep SKILLS/synced and SKILLS/.trash out of the repo. # # Sources verify-ops.sh (its main runs only when executed) and drives single # checks against fixture trees under a throwaway HOME made by mktemp. It never @@ -441,18 +442,22 @@ run check_memory_links chk "dangling memory link -> WARN naming memory-init" "$([ "$WARNS" = 1 ] && printf '%s' "$OUT" | grep -q -- '-gone' && printf '%s' "$OUT" | grep -q 'memory-init'; echo $?)" # 26. SKILLS/synced (Claude Code's claude.ai skill sync) is gitignored and not an OPS skill: the -# index check skips it, and a copy already committed WARNs with the untrack command. +# index check skips it, and a copy already committed WARNs with the untrack command. The same +# sync moves a skill it drops into SKILLS/.trash/, which is gitignored too, so the daily +# backup's `git add -A` never commits it. new_case skills-synced -mkdir -p "$OPS/SKILLS/synced/org-skill" +mkdir -p "$OPS/SKILLS/synced/org-skill" "$OPS/SKILLS/.trash/old-org-skill" printf -- '---\nname: org-skill\n---\n' > "$OPS/SKILLS/synced/org-skill/SKILL.md" +printf -- '---\nname: old-org-skill\n---\n' > "$OPS/SKILLS/.trash/old-org-skill/SKILL.md" cp "$SRC_OPS/.gitignore" "$OPS/.gitignore" # The index check is a substring match and the shipped README says "un-synced"; reword it so # only the skip, not that word, can keep the synced dir out of the index. sed -i 's/synced/sync-ed/g' "$OPS/SKILLS/README.md" git -C "$OPS" init -q run check_skills_index -chk "untracked synced dir -> no WARN" "$([ "$WARNS" = 0 ]; echo $?)" +chk "untracked synced and .trash dirs -> no WARN" "$([ "$WARNS" = 0 ]; echo $?)" chk "the shipped .gitignore ignores SKILLS/synced" "$(git -C "$OPS" check-ignore -q SKILLS/synced/org-skill/SKILL.md; echo $?)" +chk "the shipped .gitignore ignores SKILLS/.trash" "$(git -C "$OPS" check-ignore -q SKILLS/.trash/old-org-skill/SKILL.md; echo $?)" git -C "$OPS" add -f SKILLS/synced/org-skill/SKILL.md git -C "$OPS" -c user.name=selftest -c user.email=selftest@example.invalid commit -q -m fixture run check_skills_index diff --git a/.gitignore b/.gitignore index f9ba0cc..831b68d 100644 --- a/.gitignore +++ b/.gitignore @@ -37,6 +37,9 @@ SKILLS/.vendor.*/ # deploy.sh links ~/.claude/skills to SKILLS/. Those skills belong to the org and refresh at # every session start; the daily backup's `git add -A` would commit them into this repo. SKILLS/synced/ +# The same sync deletes a skill it no longer lists by moving it into .trash/ beside it, and +# ~/.claude/skills follows the link into this repo, so that trash would be committed too. +SKILLS/.trash/ # --- PROJECTS: repo subdirs (2nd-level) are their own repos (ignored). # Cluster folders (1st-level: ExampleOrg/, AnotherOrg/, etc.) diff --git a/AGENTS.md b/AGENTS.md index 05962c1..e0b31c1 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -111,7 +111,7 @@ current task allows. | What exists, the folder tree | `README.md` | | Deploy (Stage 1 and 2), migration, T3 Code hosts | `DEPLOYMENT.md` | | First launch | `BOOTSTRAP.md` | -| Template sync from upstream | the `harness-update` skill | +| Template sync from upstream | the `harness-update` skill (slash-only: the Operator types `/harness-update`) | | Scripts, tests, and the gate | `.claude-config/bin/README.md` | | Porting rules, the CI gate, local test commands | `CONTRIBUTING.md`, `.github/workflows/gate.yml` | | Licenses of adapted third-party work | `SKILLS/THIRD_PARTY_NOTICES.md` | diff --git a/BOOTSTRAP.md b/BOOTSTRAP.md index eb3381e..93dff48 100644 --- a/BOOTSTRAP.md +++ b/BOOTSTRAP.md @@ -143,8 +143,11 @@ Constraints, Improvement Kata, and the Phoenix Project lens **Round E: Output defaults.** Confirm or override the default document format (`.docx` / `.md` / other), the conversation register (casual or -formal), and whether a rendered visual page for complex data is offered by -default. Record overrides as standing orders. +formal), and the visual page for data and reports: in a T3 Code thread it is +shown inline by default without asking (`t3-html-render`), and elsewhere it is +offered first (`how-i-work.md` § Reporting to the Operator). Offer: `Keep the +default` · `Only when I ask` · `Always offer first`. Record overrides as +standing orders. **Round F: Permission mode (always asked; never skipped or defaulted silently).** Before the question, tell the Operator plainly what the Stage-1 diff --git a/CHANGELOG.md b/CHANGELOG.md index 8379ec8..55ab31f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,64 @@ Notable changes to OPS, newest first. Format: the date, then what changed and wh matters. This file starts fresh at the public release; the harness's private prehistory is deliberately not part of it. +## 2026-11-09: inline pages in T3, slash-only sync skills, and a reporting and model-roles pack + +Features already in daily use in the harness OPS is ported from, brought to the template so +a T3 Code copy works the same way out of the box. + +### Skills + +- **`t3-html-render` is new.** In a T3 Code thread it builds one self-contained HTML page + for data, a report, findings, a comparison or a plan, checks it with `html_preview` at + 728 px dark and 390 px light (zero console errors), and shows it inline with + `html_render`, then links the saved file. It carries `assets/starter.html`, a worked + `assets/example-report.html` on synthetic data, and `references/palette.md`, whose + series colors pass a colorblind-safety validator on T3's light and dark surfaces (T3's + own chart variables do not). + The example keeps its cost chart visible with reduced motion, fits data tables at + 360 px, and keeps its recovery and cost explanations consistent with its data. + Render instructions cap tall pages at the accepted height before calling the tool. +- **`harness-update` and `remote-session` are slash-only** (`disable-model-invocation: + true`). Both run only when the Operator asks, so their descriptions no longer take room + in the skill listing; type `/harness-update` or `/remote-session`. Measured on the + template's own skills, the listing goes from 17 entries and 3,575 characters of name and + description to 16 entries and 3,366, with `t3-html-render` added. Delete the line in a copy to give the model those + skills back. `SKILLS/README.md`, `README.md` and `AGENTS.md` say so. +- **`SKILLS/.trash/` is gitignored.** Claude Code's skill sync moves a skill it drops into + that folder, and `~/.claude/skills` follows the link into the repo, so the daily + backup's `git add -A` would have committed it. `verify-ops-selftest.sh` now checks the + shipped `.gitignore` covers it. + +### Doctrine + +- **In T3, a page by default** (`CONTEXT/how-i-work.md`, Reporting to the Operator; + `core.md` Writing). Data, reports, research findings and comparisons are shown as an + inline page without asking first, on the turns where a picture reads faster than prose. + It replaces "offer a page and wait for a yes", which held a format the reader wants by + default behind a question; outside T3, where no inline page exists, the offer stays. + BOOTSTRAP Round E offers the choice. +- **Reporting.** At most two or three questions in a decision report, in prose rather + than a decision table; the agent takes and logs the other calls. When the next move is + the Operator's, the reply carries the whole procedure inline, never a pointer to a file. +- **Verify after any deploy** (Quality standards): read the change back and exercise the + changed path; a third-party update you take is a deploy too. +- **Lanes and resources are used freely with a clear purpose** (P11, `core.md` Intake). + Spend with no purpose still goes to the Operator, and "one capable lane beats three" + stays the cost rule. +- **Lanes never run `session-close`** (`core.md` Rhythm): a lane inherits the foreman's + session id, so a close run from a lane settles the foreman's session. +- **T3 wake-ups** (Hosted inside T3 Code): timers stay under about 115 minutes; a wake + further out, or one that must fire with no turn active, goes through T3's + `schedule_task`, with structured schedule shapes, UTC `nextRunAt`, and native + one-off triggers when available (a repeating-task fallback deletes itself). +- **`model-roles.md`.** Opus 5.5's price and the API behavior that touches the harness; + Sonnet 5.5's catalog figures and its overturn condition; Haiku 5.5's 100K price step, + where it runs and where it never does, how Stage-1 holds the 100K ceiling and what + would break it; Fable's cost in plain figures; and the T3 Code picker settings + (`customModels`, `hiddenModels`). + Catalog facts carry dated primary sources; Sonnet's API effort default is `high`, + while Claude Code's is `medium`. + ## 2026-11-07: foreman doctrine split out, lane-aware boots, a memory loader, stricter guards, and a deploy that offers instead of removing Every lane paid on every turn for core rules that bound only the main thread, lanes kept diff --git a/CONTEXT/core.md b/CONTEXT/core.md index a9af510..e8029b7 100644 --- a/CONTEXT/core.md +++ b/CONTEXT/core.md @@ -49,7 +49,8 @@ Intake, planning, and reporting rules: `CONTEXT/foreman.md`. Fill small reversible gaps with sensible defaults and log your calls so the Operator can audit them. Hold only on a genuine fork (ambiguous, irreversible, outward-facing, or low confidence), while working every independent thread. -Never ask permission to do assigned work. Investigation paths and rejected +Never ask permission to do assigned work, including the lanes and resources +it needs with a clear purpose (P11). Investigation paths and rejected options belong in the commit, doc, or changelog. Before ending a turn, a plan or promise about undone work means do that work, @@ -136,10 +137,12 @@ notes, tickets, PRs, commits, and briefs ("event", "defect", "finding", external writing. Dramatic wording in this layer has triggered model handoffs. Anything in the Operator's voice or sent on their behalf uses -`operator-voice` (`CONTEXT/voice.md`). Formats follow the task: `.docx` for -client deliverables, `.md` for notes and docs, `.csv` for data, native -extensions for code. Outputs land in the active project's directory, never -the OPS root; scratch and tool logs go to `$TMPDIR`. A ticket you write lets +`operator-voice` (`CONTEXT/voice.md`). Formats follow the task: in T3 Code, +an inline page (`t3-html-render`) for data, reports, and breakdowns, shown +without asking; `.docx` for client deliverables, `.md` for notes and docs, +`.csv` for data, native extensions for code. Outputs land in the active +project's directory, never the OPS root; scratch and tool logs go to +`$TMPDIR`. A ticket you write lets whoever picks it up start with zero questions (what happened, context, suggested next steps). Never compress code output or draft a long deliverable in reasoning and then repeat it in the reply. @@ -147,8 +150,9 @@ in reasoning and then repeat it in the reply. ## Rhythm and context Compaction, batons, rest stops, and teardown are the foreman's: -`CONTEXT/foreman.md`. A lane returns its result; it never compacts or closes a -thread. +`CONTEXT/foreman.md`. A lane returns its result; it never compacts, closes a +thread, or runs `session-close`: a lane inherits the foreman's session id, so +a close run from a lane settles the foreman's session. ## Memory and where knowledge goes diff --git a/CONTEXT/how-i-work.md b/CONTEXT/how-i-work.md index 80aadf3..2dd0f4c 100644 --- a/CONTEXT/how-i-work.md +++ b/CONTEXT/how-i-work.md @@ -45,14 +45,30 @@ When preparing a rollup: (deadline, size, owner, state) so the Operator still chooses, then give your recommendation after it, marked as yours. Analysis is exploration until the Operator rules it an action item. +- Ask at most two or three questions in a decision report, in plain prose + rather than a decision table. Every other decision is yours: take a + sensible default and log it in one line so the Operator can veto it + ([core: Intake](core.md#intake-then-autonomy)). Why: a long decision table + is hard to read and act on, and it hands back calls the agent was trusted + to make. +- When the next move is the Operator's, the reply carries the whole + procedure inline: numbered steps with the exact names, values, click + paths, and a direct link, plus any message text ready to paste. Never a + pointer to a file they must open to learn what to do. - Give Operator action items the smallest first step, a named finish line, and a date. Dated concrete moves get done; diffuse reminders drift. Track the loops they leave open. - Follow tangents, hold dropped threads, and resurface them at the rest stop with what mattered and what is next. -- Offer a rendered visual page for structured audits, research findings, or - comparisons, and wait for a yes. Once authorized for a workstream, continue - that format without asking again. +- In a T3 Code thread, data, reports, research findings, and comparisons + are shown as an inline page by default, without asking first + (`t3-html-render`): one page on the turns where a picture reads faster + than prose, never on every turn. Link the saved page so the Operator can + download or share it; open a browser preview or a local server only when + asked. Why: a format the reader wants by default should not wait behind a + question. Outside T3, where no inline page exists, offer a rendered page + and wait for a yes; once authorized for a workstream, continue that + format without asking again. - "Send me a file" on a headless box invokes `grabit`, not in-chat delivery. - File praise for a judgment call too, so the successful pattern survives. @@ -217,6 +233,13 @@ repo, lessons file, or the Operator's queue. Enforce conventions with a lint when fixing a class of drift. A cited reference must resolve on a fresh checkout; gitignored-but-cited material needs an explicit checkout or dependency route. +- A deploy is done when it is verified live. After any deploy to any + system, read the change back and exercise the changed path before calling + it done. A third-party update you take counts too: an image pull or an + automatic upgrade is a deploy. Why: an automatic upgrade of a production + workflow service once dropped one input field and silently stopped its + output for more than a day, because nothing exercised the path after the + update. - Before a research lane, audit prior findings, cite what is settled, and run only the open delta. Prefer collected data; justify any new external pull against existing coverage. @@ -334,6 +357,23 @@ background wait, watch loop, or gate run that can outlast 30 minutes. T3's headless browser needs a one-time setup on each host ([DEPLOYMENT.md](../DEPLOYMENT.md), "T3 Code hosts"). +Keep a timer under about 115 minutes, inside that 2-hour cap: a call +re-armed past it is stopped. A wake further out, or one that must fire while +no turn is active, goes through the T3 MCP `schedule_task`, a persistent +job in T3's own scheduler (`CronCreate` lives only as long as the session). +Its schedule is a structured object, never JSON text: +`{type:'fixed_time', timeOfDay:'10:00', weekdays:[0]}` (0 is Sunday) or +`{type:'interval', everyMs}` (one minute minimum); the tool's schema lists +any other trigger types your T3 version offers. `nextRunAt` comes back in +UTC, so convert it to the Operator's time zone when you report it. For a +one-off wake, prefer `{type:'once', runAt:''}` +when the tool's schema offers it. On older versions, `fixed_time` and +`interval` tasks repeat, so a one-off wake's prompt must call +`delete_scheduled_task` on its own task after it runs. Verified on a T3 +thread: a two-minute interval task bound to the thread fired seconds after +its `nextRunAt`, posted its prompt into the thread as a new turn while no +turn was active, and deleted itself as its prompt told it to. + ## Remote kill and launch Ship kill, launch, and poll logic as a remote script and invoke its path, @@ -513,7 +553,15 @@ errors. bounded work out of the long-lived main context; its economic value depends on the active tiers in [model-roles.md](model-roles.md). A session-scoped "do not delegate" line does not override the posture; check its provenance -before treating it as the Operator's intent. In Claude Code, thorough reads +before treating it as the Operator's intent. Spawn lanes and use subagents +and other resources freely when the work has a clear purpose, without asking +first. A purpose the assignment names is inside it; spend with no purpose is +the "spend or scope far beyond the assignment" case in +[core: Intake](core.md#intake-then-autonomy), which still goes to the +Operator, and "one capable lane beats three" +([foreman: Orchestration](foreman.md#orchestration)) stays the cost rule. +Why: a lane the work needs is part of the assigned work, and asking first +stalls work the Operator already handed over. In Claude Code, thorough reads use the `ops-*` lanes or `general-purpose`; reserve the built-in `Explore` for one-shot symbol lookups, because its excerpt reads have produced fabricated counts. diff --git a/CONTEXT/model-roles.md b/CONTEXT/model-roles.md index aa790c1..47446d0 100644 --- a/CONTEXT/model-roles.md +++ b/CONTEXT/model-roles.md @@ -4,6 +4,8 @@ Single source for the harness model tiers. This file is canonical. [Core: Models This is policy, not a per-operator config. The model *ids* below are the ones the Stage-1 `settings.json` (linuxploitacious) pins today. If your plan carries different tiers, see "Settings, pins, and the shared Stage-1 file" below before you change anything: that file is shared, and editing it in place has side effects. +Catalog facts checked 2026-11-09 against Anthropic's [model overview](https://platform.claude.com/docs/en/models/overview), [pricing](https://platform.claude.com/docs/en/about-claude/pricing), and [Opus 5.5 API changes](https://platform.claude.com/docs/en/models/opus-5-5/whats-new-opus-5-5). These are API list rates and defaults; subscription usage and Claude Code defaults can differ. Recheck these sources before a price or API behavior drives a new decision. + ## Tiers | Role | Model | Id | How it is reached | @@ -20,7 +22,14 @@ Sonnet 5.5 is the default lane, refutation lanes included: spawn on it without a ## Opus 5.5, the foreman and high-stakes lane -Opus 5.5 is the foreman (the `"model"` pin) and the lane for work where Sonnet is likely to get it wrong: guards and deploy code, secrets and auth logic, subtle concurrency or state-machine logic, novel design, a second attempt after a Sonnet lane stalled or got it wrong, and refuting a high-criticality claim set. Refutation otherwise runs on the Sonnet 5.5 `ops-auditor`; a spawn passes `model: "opus"` only for such a set, with the reason in the brief. Opus 5.5 is also the review for code where a miss is expensive when no second vendor's model is available: an Opus refutation lane plus the Operator's own read (`how-i-work.md`, Review pickup). With a second vendor, that code goes there instead; no Claude lane is that review. Its known weakness is big-picture judgment and fewer unprompted "there is a better way" suggestions; compensate with explicit written plans and the foreman's verification of every returned lane. Its API effort default is `medium`, so the explicit `xhigh` pin (see Effort) is what keeps it at the harness default. +Opus 5.5 is the foreman (the `"model"` pin) and the lane for work where Sonnet is likely to get it wrong: guards and deploy code, secrets and auth logic, subtle concurrency or state-machine logic, novel design, a second attempt after a Sonnet lane stalled or got it wrong, and refuting a high-criticality claim set. Refutation otherwise runs on the Sonnet 5.5 `ops-auditor`; a spawn passes `model: "opus"` only for such a set, with the reason in the brief. Opus 5.5 is also the review for code where a miss is expensive when no second vendor's model is available: an Opus refutation lane plus the Operator's own read (`how-i-work.md`, Review pickup). With a second vendor, that code goes there instead; no Claude lane is that review. Its known weakness is big-picture judgment and fewer unprompted "there is a better way" suggestions; compensate with explicit written plans and the foreman's verification of every returned lane. It lists at $4 / $20 per MTok (cache reads $0.20) with a 1M window and 128K output, twice Sonnet 5.5's price. + +API behavior that touches the harness: + +- Its effort default is `medium`, so the explicit `xhigh` pin (see Effort) is what keeps it at the harness default. +- Thinking cannot be disabled; the CLI never tries to. +- Forced `tool_choice` (`any` or `tool`) returns a 400. Claude Code's structured output does not depend on it; a script of yours that calls the API directly must use `auto` and say which tool to call. +- Thinking blocks are bound to the model and conversation that produced them; the CLI handles that binding. ## Context windows @@ -28,15 +37,36 @@ Opus 5.5, Sonnet 5.5, and Fable 5.1 run with a 1M-token window; the 4.6-era ids ## Sonnet 5.5, the workhorse -Sonnet 5.5 is the default lane for builds, fixes, docs, research, recon, migrations, mechanical work, and refutation. Claude Code learned the `claude-sonnet-5-5` id in 2.1.284; an older build runs it as an unknown model with a 200K window, so update Claude Code on a host before relying on the pin. Like Opus 5.5, its effort default is `medium`, so the `xhigh` pin stays explicit. +Sonnet 5.5 is the default lane for builds, fixes, docs, research, recon, migrations, mechanical work, and refutation. It has a 1M window and 128K max output and lists at $2 / $10 per MTok. Claude Code learned the `claude-sonnet-5-5` id in 2.1.284; an older build runs it as an unknown model with a 200K window, so update Claude Code on a host before relying on the pin. Its effort default on the [Claude API](https://platform.claude.com/docs/en/models/sonnet-5-5/overview) is `high` (Claude Code defaults to `medium`), so the `xhigh` pin stays explicit. + +Overturn condition (P14). The constraint is the shared usage window, and this default bets that Sonnet 5.5 holds quality on ordinary lane work at half Opus 5.5's list price. Two readings would falsify it: churn, meaning Sonnet 5.5 lanes write more cache per output token than Opus 5.5 lanes on comparable work (the reading that got Opus 5 banned); and rework, meaning lanes the foreman must respawn on Opus 5.5 after verification fails, against all Sonnet 5.5 lanes of the same week. Read churn with `.claude-config/bin/transcript-usage.py --since ~/.claude/projects` (lane rows: `ccr` against `out`, with the `models` column naming the model); rework is the foreman's own count of respawns, since no tool records it. A reasonable line: churn above Opus 5.5's, or more than one lane in four respawned on Opus. If either holds, bring the Operator the figures with a recommendation. New input that reopens the question: a cheaper model that clears the same bar, or a price change on any tier. ## Haiku 5.5, cheap reading Haiku 5.5 is for cheap, bounded reading: searches, summaries, digging through tedious material, a workflow stage whose input you can size. Keep its lanes under 100K tokens of context: the Stage-1 settings give `claude-haiku-5-5` a 100K auto-compaction window (`modelSettings`), so a Haiku lane compacts on its own before it reaches the long-context price, and its brief should still size the input to fit, because each compaction drops detail. Pins name the full id. The bare `haiku` alias resolves to Haiku 5.5 only while the Stage-1 `ANTHROPIC_DEFAULT_HAIKU_MODEL` pin is live and on Claude Code 2.1.293 or later; elsewhere it resolves to Haiku 4.5, which is banned. Never remove that env key: without it, the haiku alias and Claude Code's background requests (session titles, fetch summaries) fall back to Haiku 4.5. +Price and the 100K step. For a prompt up to 100K tokens Haiku 5.5 lists at $0.10 in and $0.50 out per MTok; past 100K every rate is five times that. A request's prompt length counts all of its input tokens, cache reads and writes included, and each request is priced on its own. Even past the step its input and output rates are a quarter of Sonnet 5.5's, so the 100K ceiling is a further saving, not a break-even line. + +Where it runs: + +- Background requests, through the pin. The haiku alias and Claude Code's small background requests (session titles, WebFetch page summaries) run on it. No code is involved, and these requests are small. +- Workflow stages that locate, count, extract named facts, answer explicit questions over two or three named documents, or classify into a closed label set whose definitions are in the brief, plus digests for the foreman's own use that keep the source's reason for each point. Name `model: 'claude-haiku-5-5'` on the stage (check 15b refuses the bare alias) and size the input so the stage stays under 100K tokens with its boot. The pattern that fits: a Haiku stage gathers quoted evidence, and a Sonnet or Opus stage decides on it. + +Measured, not assumed: a blind comparison in the harness OPS is ported from (tasks with independently checked answers, judged without model names) found Haiku 5.5 level with Sonnet 5.5 on locate, count, and fact extraction, with no fabrications, at about a twelfth of the cost. Its summaries dropped reasons more often, and one of its code-behavior answers inverted a rule, so summaries that are the deliverable or carry reasons into a decision, and code-behavior answers that gate a change, stay on Sonnet. + +Never on Haiku: builds, refutation, review, security triage, or other judgment-heavy work. It has no server-side refusal fallback, so a declined Haiku lane simply fails. + +How the 100K ceiling holds. Claude Code's own auto-compaction holds it, so a Haiku lane never has to watch its context. The Stage-1 settings turn auto-compaction on harness-wide (`autoCompactEnabled: true` and `DISABLE_AUTO_COMPACT` set to `"0"`; both switches are process-wide, and either one closed stops every compaction) and pin `modelSettings: {"claude-haiku-5-5": {"autoCompactWindow": 100000}}`. The env switch is `"0"` rather than absent because a removed key leaves an old `"1"` in force: a running session keeps it through a settings reload, and a `claude` started from that session inherits it. Opus and Sonnet keep their 1M window; for them auto-compaction near the end of the window is a backstop, and manual compaction at natural breaks stays the norm (`pre-compact-synthesis`). Stage-1 also sets `idleCompaction: false`, so an idle main thread is never compacted early. + +What would break it: `CLAUDE_CODE_AUTO_COMPACT_WINDOW` outranks every per-model window; `CLAUDE_AUTOCOMPACT_PCT_OVERRIDE` lowers every model's threshold; a removed `DISABLE_AUTO_COMPACT` leaves an inherited `"1"` in force; Claude Code drops a malformed per-model value without a word, and `claude doctor` does not flag it; `/autocompact` saves a window for the current model into the user settings file, which on a linuxploitacious host is the shared Stage-1 file; and a top-level `autoCompactWindow` in a higher-precedence settings file (`settings.local.json`, managed settings) replaces the per-model windows of lower files. No OPS check reads these keys today, so when a Haiku lane runs past 100K, read them in that order. + +Effort. `CLAUDE_CODE_EFFORT_LEVEL=xhigh` in the Stage-1 settings overrides an agent's `effort` frontmatter, so Haiku lanes run at `xhigh` like every tier. That spends more tokens than its API default (`medium`), which fills the 100K ceiling sooner. + +Overturn condition (P14). The constraint is Haiku's accuracy on retrieval and summaries against its price. A same-brief comparison in which Haiku misses or fabricates more than Sonnet on the same lookups would falsify a placement; a price change on either tier reopens it. + ## Fable 5.1, rare -Spawn Fable only for deep, high-impact design or infrastructure planning and for some front-end work. It lists at 2.5 times Opus 5.5's price and spends the shared usage window several times faster per token, and each lane carries a fixed overhead of system prompt and tool schemas, so give it substantive work and a complete brief. The cases that once justified a top-tier lane (concurrency, security, guards and deploy code, a second attempt) go to Opus 5.5. A code review is never a Fable lane. Nobody is required to have Fable: not every account or seat includes it, and a copy that lacks it loses nothing the harness needs, because Opus 5.5 takes that work. +Spawn Fable only for deep, high-impact design or infrastructure planning and for some front-end work. It lists at $10 / $50 per MTok (cache reads $0.25), 2.5 times Opus 5.5 and 5 times Sonnet 5.5, and spends the shared usage window several times faster per token: Fable lanes on ordinary work have used up a whole five-hour window overnight and stopped the foreman and its lanes with it. Each lane also carries about 30K tokens of fixed overhead (system prompt and tool schemas), so give it substantive work and a complete brief; a lane sees only its brief. The cases that once justified a top-tier lane (concurrency, security, guards and deploy code, a second attempt) go to Opus 5.5. A code review is never a Fable lane. Nobody is required to have Fable: not every account or seat includes it, and a copy that lacks it loses nothing the harness needs, because Opus 5.5 takes that work. ## Bans @@ -57,4 +87,5 @@ Effort is `xhigh` on every tier, Haiku 5.5 included (`effortLevel` and `CLAUDE_C - Pins are the owner's. Deploy and `harness-update` never edit a copy's `"model"` pin, its env keys, or an agent pin the owner changed. The drift gate FAILs on a retired or banned id, edits nothing, and says how to fix it (the stated exception in `how-i-work.md`, "The template and a copy's owner"). An administrator who wants every box on the current tiers can set them through managed settings (`/etc/claude-code/managed-settings.json` on Linux), which outrank the user's own settings without editing the user's files. Offer that route; do not impose it on a copy whose owner chose a different pin, and never require a tier the owner's plan lacks (Fable above all). - To check that the models your settings pin answer on your plan, run `.claude-config/bin/model-probe.sh` (one short request per distinct id; `--list` shows what it would probe without calling the API). It reports only; it never edits a settings file. - If a pinned model has genuinely gone unavailable, remove the `"model"` key so Claude Code falls back to the `ANTHROPIC_DEFAULT_*_MODEL` aliases; do not guess a replacement. +- T3 Code pickers. List `claude-opus-5-5` and `claude-sonnet-5-5` under `providerInstances..config.customModels` in `~/.t3/userdata/settings.json` (a plain list of id strings, hot-reloaded), so the live tiers stay pickable whatever T3's built-in catalog lists; T3 appends `[1m]` itself when the thread's 1M option is on. Haiku 5.5 is allowed and already in the built-in list, so it needs no entry. Hide the banned and retired ids per instance (`providerModelPreferences..hiddenModels`, set in the T3 UI and kept by the server in the same file since T3 0.1.0). The picker never hides a custom model, so never add a banned or retired id to `customModels`. - 1M fan-out works on both profiles. If 1M spawns ever start failing with `API Error: Usage credits required for 1M context`, a per-account credit gate is forcing subagents down to 200K or less; re-point the aliases at non-`[1m]` ids until it lifts. diff --git a/README.md b/README.md index 3560bce..bc4b26b 100644 --- a/README.md +++ b/README.md @@ -172,6 +172,7 @@ specific to T3: 1. In T3 Code, add `~/OPS` as a project and open a thread on it. The first thread finds no bootstrap marker and runs `BOOTSTRAP.md` (step 3). 2. Run T3's browser setup once per host. T3's page previews and browser tools + (the `t3-html-render` skill checks each inline page with `html_preview`) run a headless Chrome that needs system libraries and, on Ubuntu 23.10 and later, an AppArmor profile: @@ -320,7 +321,8 @@ source and its license. ## Template sync A copy made from the template shares no git history with it, so `git merge` has -no common ancestor. The `harness-update` skill ("update the harness") is the +no common ancestor. The `harness-update` skill (type `/harness-update`; it is +slash-only, so an agent never starts a template sync on its own) is the safe path across that gap, and `.claude-config/bin/harness-update-scan.sh` is its mechanical half. Run from inside your copy (`cd ~/OPS`), the scan adds an `ops-template` remote, fetches it, and classifies every file as NEW, UPDATE, diff --git a/SKILLS/README.md b/SKILLS/README.md index 01c3b9a..e3fb4e3 100644 --- a/SKILLS/README.md +++ b/SKILLS/README.md @@ -91,9 +91,9 @@ Local entries live in this folder. Mounted skills are listed in the second table | `memory-prune` | yes | no | Claude Code only: fans out a read-only memory audit via the Workflow tool (`audit_workflow.js`), plus a pool inventory (`scripts/inventory`, which reports a pool it cannot read as UNMEASURED, never as empty); executes after operator sign-off. No GUI Project half exists. | | `pre-compact-synthesis` | yes | no | Claude Code only: durable-state synthesis plus closeout hygiene, taken at natural rest stops and before a manual `/compact`. Context-watch and automatic compaction are safety nets, not the ritual. Also holds the ask card (at most three asks, each with a recommendation and a default, every claim marked) for "what do you need from me" questions and close receipts. No GUI Project half exists. | | `session-handoff` | yes | no | Claude Code only: write/read baton pair for handing off sessions across profiles and machines; pairs with `pre-compact-synthesis`. No GUI Project half exists. | -| `remote-session` | yes | no | Claude Code only: hand-run remote sessions. The operator asks for each start, park, revive, or end of a tmux `claude --remote-control` session, and the skill runs it through `.claude-config/remote-sessions/`. | +| `remote-session` | yes (slash-only: `/remote-session`) | no | Claude Code only: hand-run remote sessions. The operator asks for each start, park, revive, or end of a tmux `claude --remote-control` session, and the skill runs it through `.claude-config/remote-sessions/`. | | `session-close` | yes | no | Claude Code only: permanent end-to-end close (the session's own cleanup, scoped to what it touched and never another live session's work; the work-tracking reconciliation gate; the receipt as an ask card; then archive or deregister and tear down the backend). Runs only on the operator's explicit word. The terminal counterpart to `pre-compact-synthesis` (pause) and `session-handoff` (move). | -| `harness-update` | yes | no | Claude Code only: safe template-sync of a private OPS copy from the public upstream (no shared git history); the scan classifies files NEW/UPDATE/CONFLICT/IDENTICAL, hard-excludes identity, memory, and handoff surfaces, and never auto-applies a CONFLICT. See [[harness-update/SKILL.md]]. | +| `harness-update` | yes (slash-only: `/harness-update`) | no | Claude Code only: safe template-sync of a private OPS copy from the public upstream (no shared git history); the scan classifies files NEW/UPDATE/CONFLICT/IDENTICAL, hard-excludes identity, memory, and handoff surfaces, and never auto-applies a CONFLICT. See [[harness-update/SKILL.md]]. | | `operator-voice` | yes | no | Thin context skill: routes to the `CONTEXT/voice.md` slot when writing in the operator's voice; degrades to plain professional prose when unset. Also carries the story order for client documents that ask for decisions (decisions last). | | `project-kata` | yes | no | Thin context skill: routes to `CONTEXT/project-kata.md` when scaffolding or restructuring a repo. | | `projects-map` | yes | no | Thin context skill: routes to `PROJECTS/projects-map.md` for repo routing and portfolio layout. | @@ -101,6 +101,7 @@ Local entries live in this folder. Mounted skills are listed in the second table | `harness-deploy` | yes | no | Thin context skill: routes to `DEPLOYMENT.md` when changing Stage 1 / Stage 2 install. | | `harness-readme` | yes | no | Thin context skill: routes to `README.md` for "what exists / where does X live". | | `design-method` | yes | no | Design method (UI, web, landing pages, email templates) with an optional brand layer read from `design-method/brand.md`; free-design until the brand file is filled. | +| `t3-html-render` | yes | no | T3 Code only: builds one self-contained interactive HTML page for data, a report, research findings, a comparison, or a plan, checks it with `html_preview` at two widths and both themes, and shows it inline with `html_render`. Carries a starter page, a worked example (synthetic data), and a palette validated for colorblind safety on T3's surfaces. In a T3 thread the page is the default for data and reports (`CONTEXT/how-i-work.md`, "Reporting to the Operator"). | Mounted from [`Exploitacious/agent-skills`](https://github.com/Exploitacious/agent-skills) (public, MIT) through `VENDORED.tsv`. Edit these at the source, then sync; never in `SKILLS/`. @@ -123,9 +124,9 @@ scaffold your first one when it learns what you do. ## The skill listing budget (an option, never required) -Every model-invocable skill puts its name and description into the listing each session loads, and Claude Code drops descriptions from the tail of that listing above about 30,000 characters (built-in and account skills share the budget). A skill nobody loads can therefore cost a live one its description. The template sets nothing here and never removes a skill from your copy; the levers are yours, and a team can use them without touching OPS: +Every model-invocable skill puts its name and description into the listing each session loads, and Claude Code drops descriptions from the tail of that listing above about 30,000 characters (built-in and account skills share the budget). A skill nobody loads can therefore cost a live one its description. The template marks two of its own skills slash-only (below) and never removes a skill from your copy; the other levers are yours, and a team can use them without touching OPS: -- `disable-model-invocation: true` in a skill's frontmatter (a standalone `#` comment line or a trailing comment beside it parses) drops the skill from the model's listing and leaves only the human `/name` command; the model cannot load it through the Skill tool. It suits a skill the operator starts by name, such as `harness-update` or `remote-session`. Probed on Claude Code 2.1.319. An edit to a template skill's frontmatter is a local change: `harness-update` reports that file as a CONFLICT if upstream later changes it too. +- `disable-model-invocation: true` in a skill's frontmatter (a standalone `#` comment line or a trailing comment beside it parses) drops the skill from the model's listing and leaves only the human `/name` command; the model cannot load it through the Skill tool. It suits a skill the operator starts by name. The template ships it on `harness-update` and `remote-session`: a template sync and a remote session start only when the operator asks, so their descriptions bought nothing in the listing. Type `/harness-update` or `/remote-session` to run them; asked in plain words, an agent cannot load either through the Skill tool. To give the model those skills back, delete the line in your copy. Probed on Claude Code 2.1.319. An edit to a template skill's frontmatter is a local change: `harness-update` reports that file as a CONFLICT if upstream later changes it too. - The `skillOverrides` setting in your own Claude Code settings (`{"": "on" | "name-only" | "user-invocable-only" | "off"}`) changes how a skill is listed without editing its file, and also covers built-in and account skills. A `Skill(...)` permission deny does not hide a skill from the listing. `deploy.sh` never writes your settings, so this one is entirely yours. - A skill you never load can be deleted, local or mounted. For a mounted one, delete its row in `VENDORED.tsv` and its `SKILLS//` mirror folder: the sync writes mirrors and never removes one, so a dropped row leaves its folder in place. The thin context skills (`projects-map`, `harness-deploy`, `harness-readme`) and `grabit-screenshots` are the narrowest entries here; keeping or dropping them is your call. diff --git a/SKILLS/harness-update/SKILL.md b/SKILLS/harness-update/SKILL.md index efd462f..19db4d7 100644 --- a/SKILLS/harness-update/SKILL.md +++ b/SKILLS/harness-update/SKILL.md @@ -1,6 +1,7 @@ --- name: harness-update description: "Use when the Operator says update the harness, sync from the template, or pull upstream OPS changes. Scans, classifies, ports conflicts, lands one commit." +disable-model-invocation: true --- # Harness update diff --git a/SKILLS/remote-session/SKILL.md b/SKILLS/remote-session/SKILL.md index 35cac43..200fdfd 100644 --- a/SKILLS/remote-session/SKILL.md +++ b/SKILLS/remote-session/SKILL.md @@ -1,6 +1,7 @@ --- name: remote-session description: "Use to start, attach, list, park, revive, or end a one-off Claude Code session in tmux that the operator remote-controls from another device (the claude.ai apps). Triggers: 'start/spin up a session called X', 'give me a session for X', 'list my sessions', 'park/kill this session'. Manages the session container only; session-handoff carries the work baton. Claude Code only." +disable-model-invocation: true --- # Manage remote Claude Code sessions diff --git a/SKILLS/t3-html-render/SKILL.md b/SKILLS/t3-html-render/SKILL.md new file mode 100644 index 0000000..7e49f56 --- /dev/null +++ b/SKILLS/t3-html-render/SKILL.md @@ -0,0 +1,41 @@ +--- +name: t3-html-render +description: Use when a reply in a T3 Code thread breaks down data, a report, research findings, a comparison, a plan, or a decision that reads faster as a picture, or when the user asks for a chart, dashboard, visual, or artifact. Builds one self-contained interactive HTML page, checks it with html_preview, and shows it inline with html_render. +--- + +# T3 HTML render + +T3 Code's `html_render` tool shows a self-contained HTML page inline in the thread, above the text reply. It is how this harness does what claude.ai Artifacts did: charts, tables, diagrams, timelines, and small interactive what-ifs live in the chat itself. `html_preview` renders the same page headless and returns a screenshot, the content height, and the console log. + +Both tools exist only in a T3 Code thread. In a T3 thread the page is the default for data and reports, shown without asking first; elsewhere, offer a page and wait for a yes. Both rules, and the Operator's standing orders that can change them, are in `CONTEXT/how-i-work.md`, "Reporting to the Operator". + +## When to use it + +Use it when the content has a shape that prose makes the reader rebuild: a quantity over time, items compared on several measures, a ranking, a flow or architecture, a plan with dates, findings with confidence levels. Readers who scan before they read get the answer at a glance. + +Keep plain text when the answer is a sentence, a status line, a yes or no, a short list, or code. Use at most one page per reply, and only on the turns where a picture helps. + +## Build it + +1. Pick the form before the colors: the data's job picks the chart (a line for change over time, a dot plot or bars for comparison, a stat tile for one number, a drawn flow for steps that branch). The `dataviz` skill, when available, carries the form heuristic and mark specs. +2. Start from `assets/starter.html`, which wires the token block, theme fallbacks, tooltip helper, and resize hook. `assets/example-report.html` is a complete worked page: KPI tiles, an animated line chart with a toggle, a log-scale dot plot against targets, weight sliders that re-rank options, a flow diagram, a timeline, tabbed evidence, and a sortable table. Copy its patterns. +3. Take colors from `references/palette.md`. Its series slots are validated for colorblind safety against T3's light and dark surfaces, which T3's own `--chart-*` variables are not. +4. Fit the frame: + - Leave `html`, `body`, and the outer element without a background, card, border, or banner title; the page sits on the thread's own background. + - Use fluid width with no horizontal padding on the outer element, fixed pixel heights for charts, and never `100vh` or `height:100%`. + - Draw SVG in JavaScript from the container's width and redraw on width change, so a phone at about 360 px gets a real layout. + - Make every hover target answer a tap too (`pointerdown`), give every multi-series chart a legend, and put the numbers in a table somewhere on the page. +5. Label invented or illustrative numbers as example data on the page. +6. Apply the writing rules inside the page: no em dashes or en dashes, sentence-case headings, conclusion first, units on every axis and value. + +## Check it + +1. Run `html_preview` at width 728 in dark, then at width 390 in light. The console must show zero errors. Log a check for anything an intro animation hides from the screenshot, such as path lengths or row counts, because the screenshot is taken right after load. +2. Look at both screenshots for clipped labels, collisions, and overflow, and fix them before rendering. +3. Call `html_render` with `height` set to the 728 px `contentHeight`, clamped to 80 through 2000 px so it fits the tool's accepted range. Raise it toward 2000 px when the page can grow after load. The frame fits the page and `height` only caps it; past the cap the page scrolls inside its frame. + +## Reply after it + +The reader already sees the page, so the reply adds only what the page does not say: decisions, caveats, next steps. End it with a link to the saved file, `$T3CODE_HOME/userdata/attachments/.html` (`~/.t3` by default), using the `attachmentId` from the render result, so the reader can download or share the page. + +T3 prepends a theme style and a small script to the saved file. The style keeps the colors right in any browser (dark by default, light under `prefers-color-scheme: light`). The script follows live theme changes, opens `http(s)` links in the reader's browser, and reports height changes to the frame. The starter marks a top-level page `standalone` and centers it only at 960 px and up, because `html_preview` also renders top-level and must still match the chat frame at 728 and 390. It adds `bare` only when no theme was injected, and only then sets its own colors. Setting colors for every top-level page put light backgrounds under dark-theme text in preview. The page needs no server or browser tab; open a browser preview only when the reader asks to see it full size. diff --git a/SKILLS/t3-html-render/assets/example-report.html b/SKILLS/t3-html-render/assets/example-report.html new file mode 100644 index 0000000..443592a --- /dev/null +++ b/SKILLS/t3-html-render/assets/example-report.html @@ -0,0 +1,433 @@ + + +ExampleOrg: backup and DR options (example data) + + +
+
+
Example dataFictional company · research brief · October 2026
+

ExampleOrg: which backup and disaster recovery design?

+

Go with Option A, an on-site backup appliance at each of the 12 sites replicating to an immutable cloud copy. It is the only design that meets all five recovery targets. It costs $53.5K more than direct-to-cloud over five years, and that premium buys a full-site ransomware restore about six times faster.

+
+ +
+
Current leader
Option A
7.6 of 10, balanced weights
+
Recovery targets met
5 of 5
Option A. B meets 2, C meets 4
+
Five-year cost
$214.5K
Option A, labor in. $53.5K more than B
+
Ransomware site restore
3.2 h
Option A. About 6x faster than B
+
+ +
+

Where the money goes

+

Cumulative five-year cost per option. Toggle engineer labor to see the cost that never shows up on a quote.

+
+
+
+
+
+
+
+ +
+

How fast each option recovers

+

Time to restore service, on a log scale. The vertical tick is ExampleOrg's target for that scenario; a dot to the right of its tick misses.

+
+
+
+
+ +
+

Weigh it yourself

+

Each option is scored 0 to 10 per criterion. Drag a weight and the ranking re-sorts; the leader tile at the top follows it.

+
+
Presets
+
+
+
Option ALeadsHybrid appliance + cloud replica
0.0
+
Option BDirect-to-cloud backup
0.0
+
Option CSelf-hosted servers + object storage
0.0
+
+
+
+
+ +
+

How the data flows

+

Hover a lane to isolate it. RPO is the most recent work you could lose.

+
+
+ +
+

Rollout plan for Option A

+

Twelve weeks: a two-site pilot, then two waves of five sites, with restore drills running from week 6. Hover a bar for its dates.

+
+
+
+ +
+

Evidence, limits, and raw numbers

+

What the recommendation rests on, how sure we are, and what would change the call.

+
+ + + + +
+
+
+ + + +
+
+

Example data for a fictional company. One self-contained HTML file: no libraries, no network calls, follows the reader's light or dark theme.

+ +
+ + diff --git a/SKILLS/t3-html-render/assets/starter.html b/SKILLS/t3-html-render/assets/starter.html new file mode 100644 index 0000000..2b09647 --- /dev/null +++ b/SKILLS/t3-html-render/assets/starter.html @@ -0,0 +1,116 @@ + + +Page title + + +
+
+
Example dataContext line · date
+

The question, phrased as a headline

+

The answer first. One to three sentences with the number that carries it.

+
+
+
Metric label
42
Delta or context
+
Metric label
$1.2K
Delta or context
+
+
+

Chart heading that states the finding

+

What the chart shows and how to read it.

+
+
+
The one thing to notice.
+
+
+

The numbers

+
+
+ +
+ + diff --git a/SKILLS/t3-html-render/references/palette.md b/SKILLS/t3-html-render/references/palette.md new file mode 100644 index 0000000..17b7081 --- /dev/null +++ b/SKILLS/t3-html-render/references/palette.md @@ -0,0 +1,53 @@ +# Palette and tokens + +The house palette for T3 inline pages. Read it when choosing colors or adding a series. + +## The token block + +`assets/starter.html` already carries this block on its `.viz` root. T3 sets `color-scheme` on `:root` to match the reader's theme, so `light-dark()` follows a theme switch live, and the `var(--foreground, ...)` fallbacks keep a copy readable on hosts that inject nothing. + +```css +.viz{ + --fg:var(--foreground,light-dark(#27272a,#f5f5f5)); + --fg2:var(--muted-foreground,light-dark(#6b6b75,#8a8a8a)); + --bg:var(--background,light-dark(#fcfcfc,#0a0a0a)); + --panel:var(--card,light-dark(#ffffff,#111111)); + --line:color-mix(in oklab,var(--fg) 12%,transparent); + --grid:color-mix(in oklab,var(--fg) 9%,transparent); + --axis:color-mix(in oklab,var(--fg) 26%,transparent); + --rad:var(--radius,10px); + --s1:light-dark(#2a78d6,#3987e5); /* blue */ + --s2:light-dark(#eb6834,#d95926); /* orange */ + --s3:light-dark(#1baf7a,#199e70); /* aqua */ + --s4:light-dark(#eda100,#c98500); /* yellow */ + --s5:light-dark(#e87ba4,#d55181); /* magenta */ + --s6:#008300; /* green */ + --s7:light-dark(#6250d6,#9085e9); /* violet */ + --s8:light-dark(#e34948,#e66767); /* red */ + --ok:light-dark(#08794f,#10b981); + --bad:light-dark(#c8232c,#fb414a); + --amber:#fe9a00; +} +``` + +## Why these and not T3's `--chart-*` + +The series slots come from the `dataviz` skill's validated palette and were re-run through its validator against T3's real surfaces (`#fcfcfc` light, `#0a0a0a` dark) on 2026-10-19: + +- Slots 1 to 3, all pairs: pass in both modes (worst colorblind separation 9.2 light, 9.4 dark). +- Slots 1 to 8, adjacent pairs: pass in both modes (worst 9.1 light, 8.4 dark). +- T3's own `--chart-*` fail: dark slots 2 and 3 sit outside the lightness band, and light slots 1 and 4 fall under the colorblind floor when all pairs are in play. + +T3's dark `--border` (`#191919`) barely shows on a card, which is why `--line` mixes from the foreground instead. + +## Rules for using them + +1. Assign slots to entities in fixed order and keep them through filters and re-sorts. A color follows the thing, never its rank. +2. Scatter plots, dot plots, and anything where every pair can touch: three series at most. Lines and bars, where only neighbors touch: up to eight in slot order. Past that, fold into "Other" or split into small multiples. +3. Aqua, yellow, and magenta sit under 3:1 contrast on the light surface. Any chart using them carries direct labels or a table view. +4. Text, values, and legends use `--fg` or `--fg2`; a colored swatch beside the text carries identity. +5. Magnitude uses one hue, light to dark. A neutral ramp, `color-mix(in oklab, var(--fg) N%, transparent)` with N from 5 to 38, keeps `--fg` text legible on every step and never reads as a series. +6. `--ok`, `--bad`, and `--amber` are status colors. Pair each with an icon and a word, and never use one as a series. +7. Never two y-axes. Two measures of different scale get two charts. + +A new palette is validated, not eyeballed: with the `dataviz` skill available, run its `scripts/validate_palette.js "" --mode light --surface "#fcfcfc"`, then again with `--mode dark --surface "#0a0a0a"`.