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
78 changes: 78 additions & 0 deletions .claude/skills/t27o/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
---
id: t27o
name: t27o
description: Change, test and measure t27o (Ouroboros), the converter of foreign code into .t27 (Rust front end today, SPICE beside it for #10486). Use when a refusal class should become convertible, when quoting t27o coverage, when running `tri t27o --check`, or when a loop tick works epic #10522.
argument-hint: "[refusal class | measure | check <file.rs>]"
---

# /t27o -- foreign code into .t27, measured

<!-- only-t27-rule -->
> **Only t27: everything is written in `.t27`.** (Owner hard rule 2026-10-05; budget #7371; restated in every skill per #7588.)
> - New logic starts as a `.t27` spec with `test` / `invariant` blocks. It reaches Rust, C, Zig, JS or Python only through `t27c gen-*`, and generated files are never hand-edited (L2).
> - Hand-written code in any other language is denied by the hooks and by CI: `.rs .py .ts .js .sh .zig .c .go .v .yml .toml`, Dockerfile, Makefile; the full list is in `specs/policy/own_language.t27`.
> - Existing foreign code is debt that only shrinks. Delete it, or replace it with a spec and its generated output.
> - Thin hand-written glue is capped at 40 added lines per file and 80 per PR. No label lifts the cap. Never split a PR to slip under it.
> - What gen cannot express yet is a compiler defect. File it on #5980 with a minimal `.t27` repro instead of writing the code by hand.
> - This rule overrides any step below that says to write a script or code in another language. The full text is in `AGENTS.md`, "Only t27".

## Where the truth lives

| What | Where |
|---|---|
| The Rust front end: what converts, what is refused and why | `specs/tools/t27o/rust_convert.t27` (header comment, lines 1-110) |
| Rust std calls mapped to existing t27 fns | `specs/tools/t27o/rust_std.t27` |
| What a run's evidence is worth; which runner's exit code is a verdict | `specs/tools/t27o/ledger.t27` |
| Golden criteria, the service, the installer | `golden.t27`, `service.t27`, `install.t27` in the same directory |
| The SPICE front end (analog M2a) | `specs/tools/t27o/spice_convert.t27`, #10509 -- owned by the analog loop |
| Baseline numbers, the field, ten weaknesses | `docs/nona-03-manifest/T27O-COMPETITORS-AND-DOGFOOD.md` |
| The overnight loop's state | epic #10522, its "Loop ledger" comment |
| The CLI glue (reads the file, runs rustc, t27b and t27c, writes) | `cmd_t27o` in `cli/tri/src/main.rs`; every rule it applies is gen-rust of the specs above |

## Make a refusal class convertible

1. Find it in the numbers, not by intuition: the first-refusal table in the doc above, or a fresh measurement (below). Pick the class with the most first refusals that needs no new type in the subset.
2. Reproduce on a real file: `./target/release/tri t27o <file.rs> | grep -n "refused fn <name>"`.
3. Write the test first, in `rust_convert.t27`: the tests near the end call `body(...)` / `convert(...)` on a Rust text and compare the output text. Add the rule; add a negative control (a near-miss form that must stay refused, or a mutated copy that must fail).
4. Inner loop: `./target/release/t27b test specs/tools/t27o/rust_convert.t27` (66 tests in about 14 s). The final gate once: `t27c test-report` on it, `vacuous passes` must be 0.
5. Regenerate, never hand-edit: `./target/release/t27c gen-rust specs/tools/t27o/rust_convert.t27 > gen/rust/tools/t27o/rust_convert.rs`, check it compiles with `rustc --edition 2021 --crate-type lib`, then `t27c seal ... --save` and `--verify`.
6. Measure the gain on the same files and commits, before and after, and put both numbers in the PR.

DRY: a std call goes through `rust_std.t27`, which names an existing t27 fn with the same I/O; search `t27c dupes --name <fn>` before writing a new one.

## Measure coverage

- One file: the last line of `tri t27o <file.rs>` is its count: `t27o: N fn seen, M converted (a of b tests), R refused (S kept as stubs); coverage P%`.
- Many files: the same command per file, with a time cap, each output kept, then the counts summed and the `// t27o: refused ... : <kind> \`<name>\`` lines tallied. Cap every file (`perl -e 'alarm 30; exec @ARGV' ./target/release/tri t27o <f>`): run time grows faster than file size, and `bootstrap/src/compiler.rs` (1.9 MB) does not finish in 5 min.
- Count first refusals as what they are: the cascade (`uses refused ...`) is 13% of them and moves when a root refusal is fixed, so rank work by root reasons.
- Quote a number only with its repository, commit and binary.

## `tri t27o --check`

`--check` builds the oracle harness with rustc, writes `specs/t27o_check/<stem>.t27` with the oracle's vectors as tests, and judges it. Since #10525 it runs `t27b test` first and `t27c test-report` only when t27b's exit code is not a verdict (`t27b_outcome` in `ledger.t27`); the fallback says which code caused it. Golden means equivalent on the recorded vectors, never a proof for all inputs.

## The loop tick (epic #10522)

1. Read the ledger comment on #10522; `CronList` (delete duplicates after a restart); list running subagents. A lane a row calls running is not started again. The analog loop keeps its own ledger on #10486 and owns #10509 and `specs/analog/`: never start work there.
2. Anomalies before new work: open PRs of the epic that are red, conflicting or stale; a lane that reported done with no PR (`git ls-remote origin | grep t27o`); a seal that no longer verifies.
3. Opus plans and reviews, Sonnet implements (`specs/claw/model_route.t27`). Two lanes at most from this loop while the analog loop runs on the same 8 GB Mac; one lane at a time edits `rust_convert.t27`.
4. Merge only green; enable auto-merge after reading the diff. To change a PR under auto-merge, disable it, push, enable it again.
5. Edit the ledger row last; add a trap here when it cost more than one try.

## Hand-written glue must net to zero

The required check `own-language` (`check_budget` in `specs/policy/own_language.t27`) fails a pull request when the counted hand-written foreign files gain more lines than they lose over the whole diff -- on top of the 40-per-file / 80-per-PR cap. Specs and `gen/` do not count. A `#[path]` mount plus a few lines in `cmd_t27o` therefore needs an equal deletion in the same diff: dead code in `cli/tri/src` (`cargo check -p tri` lists it), or hand logic moved into a spec and replaced by a call. Check before the first push:

```bash
git diff --numstat origin/master...HEAD -- cli bootstrap/src tools scripts | awk '{a+=$1; d+=$2} END {print "added", a, "deleted", d}'
```

PR #10538 failed it with +9 / -0 at 1e20cfb34.

## Traps met on this Mac

- `.claude/skills/**` is not writable from the session's own worktree in the sandbox; a checkout that touches it half-applies (index changed, file unchanged). Work in a separate worktree: `git worktree add --no-track -b <branch> $TMPDIR/t27o-loop/wt-x origin/master`, then `git -C $TMPDIR/t27o-loop/wt-x config --worktree core.hooksPath .githooks`.
- The session scratchpad is wiped when the session resumes (2026-10-18 23:53: lane worktrees, the cargo target and raw measurements gone; pushed branches survived). `$TMPDIR` survived: keep worktrees and data under `$TMPDIR/t27o-loop/`, and commit + push after every working step.
- `--no-track` matters: the shared `.git/config` cannot be locked from the sandbox, so `-B` with tracking fails after creating the branch.
- `gh` fails on TLS in the sandbox: use the REST and GraphQL APIs with `curl -H "Authorization: Bearer $(gh auth token)"`; write bodies to a file and build JSON with `python3`, never with backticks inside double quotes.
- Auto-merge by API: the GraphQL mutation `enablePullRequestAutoMerge` with the PR's `node_id` and `mergeMethod: SQUASH`.
29 changes: 20 additions & 9 deletions .claude/skills/tri-pipeline/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,21 +25,30 @@ description: The exact commands of the t27 toolchain (tri, t27c, t27b, tri t27o)

Use `./target/release/tri` and `./target/release/t27c` by path when the difference matters.

## One spec, end to end
## One spec, end to end -- fast path first

The native JIT for the inner loop, the seal to skip what did not change, the Zig reference once at the end. Measured on the bench Mac, 2026-10-18, under load:

| Step | Command | Cost |
|---|---|---|
| parse + typecheck | `./target/release/t27c check <spec>` | under 1 s |
| inner loop, after every edit | `./target/release/t27b test <spec>` | rust_std.t27 0.89 s; rust_convert.t27 (66 tests) 13.5 s |
| specs you did not change | `./target/release/t27c frontier --only <specs> --list` | about 5 s for 3 specs; REUSE = seal, closure and toolchain still match, so the sealed verdict stands: do not re-run its tests |
| final gate, once | `./target/release/t27c test-report <spec>` and `tri test <spec>` | rust_std.t27 94 s; per-test assert counts and `vacuous passes N of M` |
| seal | `./target/release/t27c seal <spec> --save`, then `--verify` | writes `.trinity/seals/<dir>_<Module>.json`; must print "all hashes MATCH"; the seal is also the REUSE key |

```bash
export ZIG_GLOBAL_CACHE_DIR=$TMPDIR/zig-cache ZIG_LOCAL_CACHE_DIR=$TMPDIR/zig-local # sandboxed sessions
./target/release/t27c check specs/<dir>/<name>.t27 # parse + typecheck, fast
./target/release/t27c test-report specs/<dir>/<name>.t27 # runs every test; per-test assert counts; "vacuous passes N of M"
./target/release/tri test specs/<dir>/<name>.t27 # exit code gate over test-report (does not yet fail on vacuous: #10497)
./target/release/t27c seal specs/<dir>/<name>.t27 --save # writes .trinity/seals/<dir>_<Module>.json
./target/release/t27c seal specs/<dir>/<name>.t27 --verify # must print "all hashes MATCH"
export ZIG_GLOBAL_CACHE_DIR=$TMPDIR/zig-cache ZIG_LOCAL_CACHE_DIR=$TMPDIR/zig-local # every sandboxed shell, every time
```

- **Do not build what already exists.** If another worktree of this checkout has release binaries, symlink them: `mkdir -p target/release && ln -sf <that worktree>/target/release/{t27c,t27b,tri} target/release/`. Build only the crate you changed, into one shared `CARGO_TARGET_DIR` (cargo's lock serializes builds), and copy the binary out right after.
- **t27b's exit code** (`cli/t27b/README.md`): 0 pass, 1 a test failed, 2 unsupported construct, 3 front end, 4 JIT and interpreter disagree (`--check`), 5 codegen limit. Only 0 and 1 are a verdict; on any other code run `test-report` (the rule is `t27b_outcome` in `specs/tools/t27o/ledger.t27`, #10525).
- Without the two `ZIG_*` variables, `test-report` says BLOCKED and `seal --save` refuses with "does not compile: failed to create output directory ... PermissionDenied". That is the sandbox, not the spec.
- `t27c test <spec>` only LISTS the test blocks; it runs nothing. Never quote it as a test result.
- A pass with 0 runtime asserts is vacuous. Read the `vacuous passes` line of `test-report` yourself until #10497 lands.
- Negative control: copy the spec to `$TMPDIR`, break one constant, run `test-report`, and see a FAIL before you claim the tests mean anything.
- A pass with 0 runtime asserts is vacuous; `tri test` fails on it since #10507. t27b prints only the total assert count, so the vacuity number comes from `test-report`.
- Negative control: copy the spec to `$TMPDIR` (or beside it when it has a `use`), break one constant, run `t27b test`, and see a FAIL before you claim the tests mean anything.
- `t27c test-report` exits 1 on FAIL and 2 on BLOCKED (did not compile).
- `gen/` is in `.gitignore`: a generated file a crate includes by `#[path]` is committed with `git add -f`.

## Generate and lower

Expand All @@ -51,6 +60,7 @@ export ZIG_GLOBAL_CACHE_DIR=$TMPDIR/zig-cache ZIG_LOCAL_CACHE_DIR=$TMPDIR/zig-lo
## Foreign code in

- `tri t27o <file.rs> [--name N] [--oracle] [--check]` turns a Rust file into a `.t27` skeleton; refused bodies keep their signature and a comment naming the refused construct. Rules: `specs/tools/t27o/rust_convert.t27`. Rust is the only front end today; a new language is a new `specs/tools/t27o/<lang>_convert.t27` beside it (the SPICE front end for #10486 goes there).
- The last line of every conversion is the file's count (`t27o: N fn seen, M converted ...`). Coverage on the roadmap repositories, the refusal kinds and the ten weaknesses: `docs/nona-03-manifest/T27O-COMPETITORS-AND-DOGFOOD.md`. How to change the converter and measure it: the `t27o` skill.

## Is someone already on it?

Expand All @@ -59,6 +69,7 @@ export ZIG_GLOBAL_CACHE_DIR=$TMPDIR/zig-cache ZIG_LOCAL_CACHE_DIR=$TMPDIR/zig-lo

## Pull requests and CI

- The required check `own-language` fails a PR whose hand-written foreign lines (Rust, Python, shell, ...; not specs, not `gen/`) gain more than they lose over the whole diff, beside the 40-per-file / 80-per-PR cap: glue is paid for by deleting as much hand code in the same diff. Check with `git diff --numstat origin/master...HEAD` before the first push (the `t27o` skill has the one-liner).
- `tri pr ready <N> [--wait --required-only]` prints one `VERDICT:` line (safe to merge, DO NOT MERGE, WAIT, CANNOT TELL, NEW REASON) after classifying every failing check.
- `tri pr landed <N> --probe S --file P` -- did the content actually reach the default branch.
- `tri night`, `tri red now|why`, `tri ci baseline`, `tri gates ...`, `tri merging`, `tri lanes --repo t27 --base master`.
Expand Down
Loading