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
71 changes: 71 additions & 0 deletions .claude-config/bin/tests/t3-html-render-test.py
Original file line number Diff line number Diff line change
@@ -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"<script>([\s\S]*?)</script>", 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()
13 changes: 9 additions & 4 deletions .claude-config/bin/verify-ops-selftest.sh
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -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.)
Expand Down
2 changes: 1 addition & 1 deletion AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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` |
Expand Down
7 changes: 5 additions & 2 deletions BOOTSTRAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
58 changes: 58 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
18 changes: 11 additions & 7 deletions CONTEXT/core.md
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -136,19 +137,22 @@ 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.

## 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

Expand Down
56 changes: 52 additions & 4 deletions CONTEXT/how-i-work.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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:'<ISO timestamp with offset>'}`
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,
Expand Down Expand Up @@ -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.
Expand Down
Loading