Skip to content

fix(tri tick): dirty work nobody holds is an anomaly, released claim or not - #5896

Merged
gHashTag merged 1 commit into
fix/tri-tick-released-claimfrom
fix/tri-tick-orphaned-dirty
Oct 4, 2026
Merged

gHashTag merged 1 commit into
fix/tri-tick-released-claimfrom
fix/tri-tick-orphaned-dirty

Conversation

@gHashTag

@gHashTag gHashTag commented Oct 4, 2026

Copy link
Copy Markdown
Owner

Closes #5895

Stacked on #5864 (base fix/tri-tick-released-claim). Merge #5787 -> #5824 -> #5864 first.

tri tick looked at a dirty worktree only while a claim was HELD and stale. Once a tick released its claim, no dirty tree was ever an anomaly. On cron 8782e5f8, tick 21 delegated two owner tasks to background agents, wrote held_by: "... live at tick 21 end" and released. The session ended, and both agents died with uncommitted work. Tick 22's card said ANOMALIES: 0, and the work was found by hand (it became trinity#1327 and t27#5894).

  • scripts/tri_loop/tick.py: new anomaly orphaned-dirty. It fires when no claim is held (released, or none), a worktree is dirty, its newest dirty file is older than --stale-minutes, and no process has its cwd inside it. The process check is one lsof -w -d cwd -Fpn, about 0.25 s on this Mac. Without lsof the card prints NOT RUN and does not claim "no process". Fresh writes or a live process print a DELEGATE: line, not an anomaly. The detail quotes held_by. The fix says to read the diff, names each file (never add -A), and clears held_by. Each dirty worktree's card line now shows its newest write, the process count and the holder.
  • scripts/ci/test_a_tick_resumes_from_what_it_reads.py (already in loop-tools-gate): cases orphaned, no claim, delegate (fresh write, negative control) and standing (a live sleep with its cwd in the tree, negative control, listed by pid).

Measured on 51dfc1f:

  • the test: all checks passed, the old cases unchanged
  • 7 mutations, each re-anchored (ORIG.count(a)==1), all red: anomaly removed (4 fails), age ignored (3), processes ignored (1), processes never found (2), held while released (6), holder not named (1), DELEGATE line removed (1); file restored byte-identical
  • live state of cron 8782e5f8: ANOMALIES 0 (both delegated trees committed by then, claim held)

Not established: that a process standing in a tree is its holder; a writer whose cwd is elsewhere is unseen; lsof on the GitHub runner is assumed, not verified (the CI run will say).

🤖 Generated with Claude Code

…or not

A released claim made every dirty worktree silent. Tick 21 of cron
8782e5f8 delegated two tasks to background agents and released; the
session ended, both agents died uncommitted, and tick 22's card said
ANOMALIES: 0.

New anomaly orphaned-dirty: no claim held, a worktree dirty, its newest
dirty file older than --stale-minutes, and no process with its cwd inside
it (lsof -d cwd; "NOT RUN" without lsof). Fresh writes or a live process
print a DELEGATE line instead. The detail quotes held_by; the fix names
the files, never add -A, and clears held_by.

Test: orphaned, no claim, and two negative controls (fresh write, a live
sleep standing in the tree). 7 mutations, all red.

Closes #5895

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 4, 2026

Copy link
Copy Markdown
Contributor

📓 NotebookLM Notebook linked to this PR

This notebook contains session context, decisions, and artifacts for this work.

This was referenced Oct 4, 2026
@gHashTag
gHashTag merged commit 1691955 into fix/tri-tick-released-claim Oct 4, 2026
31 of 34 checks passed
gHashTag added a commit that referenced this pull request Oct 4, 2026
…mes its files (#5864)

* fix(tri tick): a released claim is not held, and the dead-tick fix names its files

tri tick never read claim.released. A tick that had finished read as dead
45 minutes later. On tick 18 of cron 8782e5f8, its fix line was
`git add -A && git commit` on PR #5839's worktree, whose only dirty files
were cron_tracking/ and .claude/launch.json, both untracked on purpose.

- A release that parses and is not older than `since` ends the hold. A
  leftover or unparseable release does not; the unparseable one is reported
  as claim-unparseable on claim.released.
- Dirty excludes the state directory and a worktree's keep_untracked paths,
  counted per file (--untracked-files=all). Every other untracked file
  still counts.
- The fix is `git add -- <each file, quoted>`, never `add -A`.
- Test 20 -> 36 checks; 12 mutations, each red.

Closes #5863
Refs #5786 #5823

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(tri tick): dirty work nobody holds is an anomaly, released claim or not (#5896)

A released claim made every dirty worktree silent. Tick 21 of cron
8782e5f8 delegated two tasks to background agents and released; the
session ended, both agents died uncommitted, and tick 22's card said
ANOMALIES: 0.

New anomaly orphaned-dirty: no claim held, a worktree dirty, its newest
dirty file older than --stale-minutes, and no process with its cwd inside
it (lsof -d cwd; "NOT RUN" without lsof). Fresh writes or a live process
print a DELEGATE line instead. The detail quotes held_by; the fix names
the files, never add -A, and clears held_by.

Test: orphaned, no claim, and two negative controls (fresh write, a live
sleep standing in the tree). 7 mutations, all red.

Closes #5895

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
gHashTag added a commit that referenced this pull request Oct 4, 2026
… "open" (#5824)

* tri pr-state: a loop's pull requests with tri pr ready's verdict, not "open"

A cron loop reported t27#5787 as "open" for six ticks while cli-tri was
red. tri pr-state reads every PR a tick-state.json names: gh pr view, then
tri pr ready while it is open, quoting the verdict and its exit code.
SETTLED only when every verdict is "safe to merge"; an open PR with a
running check exits 1. Anomalies: head-moved, not-open, unresolved (an
alias is never guessed), unreadable, no-verdict. Read-only; --answers
replaces gh and tri for the offline test.

tri tick's pick_id/usage take the command name so tri pr-state errors
under its own; its card points at tri pr-state for "still open?".

First real run, cron 8782e5f8, 9 PRs: all safe, exit 0, and one
head-moved on 999#3554 -- the loop's own tick-8 push, never recorded.

Census: shell run: steps 285 -> 286 and runner bash 264 -> 265, the
one new loop-tools-gate step for scripts/ci/test_an_open_pr_is_not_a_green_one.py.
Fetches unchanged: pr_state.py has no --limit read.

Closes #5823
Refs #5786

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* tri pr-state: the printed card says safe is not proof any check ran

The billing-blocked caveat lived only in the docstring, while #5824's
body said the card's NOT ESTABLISHED block carried it. Now the printed
footer says it, and the test asserts it on the settled (healthy) card;
seen red with the word removed.

No census moved.

Closes #5823

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(tri tick): a released claim is not held, and the dead-tick fix names its files (#5864)

* fix(tri tick): a released claim is not held, and the dead-tick fix names its files

tri tick never read claim.released. A tick that had finished read as dead
45 minutes later. On tick 18 of cron 8782e5f8, its fix line was
`git add -A && git commit` on PR #5839's worktree, whose only dirty files
were cron_tracking/ and .claude/launch.json, both untracked on purpose.

- A release that parses and is not older than `since` ends the hold. A
  leftover or unparseable release does not; the unparseable one is reported
  as claim-unparseable on claim.released.
- Dirty excludes the state directory and a worktree's keep_untracked paths,
  counted per file (--untracked-files=all). Every other untracked file
  still counts.
- The fix is `git add -- <each file, quoted>`, never `add -A`.
- Test 20 -> 36 checks; 12 mutations, each red.

Closes #5863
Refs #5786 #5823

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(tri tick): dirty work nobody holds is an anomaly, released claim or not (#5896)

A released claim made every dirty worktree silent. Tick 21 of cron
8782e5f8 delegated two tasks to background agents and released; the
session ended, both agents died uncommitted, and tick 22's card said
ANOMALIES: 0.

New anomaly orphaned-dirty: no claim held, a worktree dirty, its newest
dirty file older than --stale-minutes, and no process with its cwd inside
it (lsof -d cwd; "NOT RUN" without lsof). Fresh writes or a live process
print a DELEGATE line instead. The detail quotes held_by; the fix names
the files, never add -A, and clears held_by.

Test: orphaned, no claim, and two negative controls (fresh write, a live
sleep standing in the tree). 7 mutations, all red.

Closes #5895

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* feat(tri pr-state): --why, and each name under the heading tri printed it under (#5868)

tri pr-state could not pass --why to tri pr ready, so a failure red
elsewhere under the same check name read as pre-existing in the loop's
report. Its parser also put every "  - name" after the VERDICT into one
only_here list: CANNOT TELL's no-baseline names and --why's new-reason
names read as "only here", and the NOT compared list above the VERDICT
was never read.

- --why is passed through; a tri without the flag (clap usage error,
  exit 2 = WAIT's code) is a no-verdict that says so, never WAIT
- only_here / no_baseline / new_reason / not_compared are kept apart;
  an unknown heading, an unknown verdict, or a name under no heading is
  kept as `other` with its heading and printed
- with --why a REASONS line counts over the rows that got a verdict and
  names the rest as not compared (a live run with an old tri read
  "REASONS: 0" over 18 no-verdicts before this)
- test 27 -> 51 checks, #5452's real answer as a fixture, a fake gh and
  tri on PATH proving the flag reaches argv; 19 mutations, each red

Closes #5867
Refs #5823 #5852

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
gHashTag added a commit that referenced this pull request Oct 4, 2026
Catch-up for the squash merge of #5787 (carries #5824, #5864, #5868, #5896).
Conflicts: .github/workflows/loop-tools-gate.yml path filters -- both sides
added test files, kept all four; tools/census/shell.txt -- master's side,
then `tri census pin --bless`.

Census vs master after the bless: fetches.txt bounded reads in
scripts/tri_loop/*.py 4 -> 5 (pr_state.py reads the GitHub API, bounded);
shell.txt run: steps 285 -> 288 and runner-without-container 264 -> 267 (the
stranded, tick and pr-state steps in loop-tools-gate.yml). Gate: PASS.

Local on the merge: loop-tools-tracked.sh, test_stranded_work_is_found_
without_writing, test_a_tick_resumes_from_what_it_reads, test_an_open_pr_is_
not_a_green_one, test_damage_repair_snapshot_required, test_tri_says_it_
could_not_run all pass; every scripts/tri_loop/*.py compiles.
test_a_page_is_measured_at_both_widths hit a 600 s local cap; harness.py and
that test are byte-identical to master, where #5828's CI ran them.

Refs #5786

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
gHashTag added a commit that referenced this pull request Oct 4, 2026
* tri: stranded-work finder and cron tick resume card

tri stranded lists remote branches with commits off the default branch,
tips older than --hours, and no open same-repo PR. It skips dependabot/*,
patch-equivalent branches (git cherry) and squash-merged ones (merge-tree
result equals the default's tree), reports merge clean/conflict and
whether the branch ever had a PR, and runs merge-tree against a throwaway
object directory so the clone is never written. With no PR state nothing
is called stranded: the rows are "undecided" and the exit is 2.

tri tick is the resume card for a cron loop: claim and its age, the last
ledger section, each worktree's branch, dirty count, ahead/behind vs its
base and last commit age, and anomalies with a one-line fix each. git
runs with GIT_OPTIONAL_LOCKS=0; nothing is written.

Both self-tests run in loop-tools-gate, each with a negative control, and
each was seen to fail on a mutated copy of its tool.

Refs #5786

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* tri census: re-bless fetches 4->5 and shell 283->285 for tri stranded/tick

Two pinned numbers moved, both because of this PR:

- fetches: SURFACE bounded reads in scripts/tri_loop/*.py 4 -> 5. The new
  read is `gh pr list --limit PR_LIMIT` in scripts/tri_loop/stranded.py.
  It already refused to answer on a full page, but said so as "a lower
  bound" in lowercase, which the census (says_lower_bound, the Rust side's
  wording) does not read. The message now says LOWER BOUND, so the split
  is 5 guarded and 0 that do not, rather than 4 and 1.
- shell: run steps 283 -> 285 (runner bash 262 -> 264). The rise comes
  from loop-tools-gate.yml, which gains the two tri_loop tests
  (test_stranded_work_is_found_without_writing.py and
  test_a_tick_resumes_from_what_it_reads.py).

quiet was re-recorded byte-identical.

Refs #5786

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* tri pr-state: a loop's pull requests with tri pr ready's verdict, not "open" (#5824)

* tri pr-state: a loop's pull requests with tri pr ready's verdict, not "open"

A cron loop reported t27#5787 as "open" for six ticks while cli-tri was
red. tri pr-state reads every PR a tick-state.json names: gh pr view, then
tri pr ready while it is open, quoting the verdict and its exit code.
SETTLED only when every verdict is "safe to merge"; an open PR with a
running check exits 1. Anomalies: head-moved, not-open, unresolved (an
alias is never guessed), unreadable, no-verdict. Read-only; --answers
replaces gh and tri for the offline test.

tri tick's pick_id/usage take the command name so tri pr-state errors
under its own; its card points at tri pr-state for "still open?".

First real run, cron 8782e5f8, 9 PRs: all safe, exit 0, and one
head-moved on 999#3554 -- the loop's own tick-8 push, never recorded.

Census: shell run: steps 285 -> 286 and runner bash 264 -> 265, the
one new loop-tools-gate step for scripts/ci/test_an_open_pr_is_not_a_green_one.py.
Fetches unchanged: pr_state.py has no --limit read.

Closes #5823
Refs #5786

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* tri pr-state: the printed card says safe is not proof any check ran

The billing-blocked caveat lived only in the docstring, while #5824's
body said the card's NOT ESTABLISHED block carried it. Now the printed
footer says it, and the test asserts it on the settled (healthy) card;
seen red with the word removed.

No census moved.

Closes #5823

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(tri tick): a released claim is not held, and the dead-tick fix names its files (#5864)

* fix(tri tick): a released claim is not held, and the dead-tick fix names its files

tri tick never read claim.released. A tick that had finished read as dead
45 minutes later. On tick 18 of cron 8782e5f8, its fix line was
`git add -A && git commit` on PR #5839's worktree, whose only dirty files
were cron_tracking/ and .claude/launch.json, both untracked on purpose.

- A release that parses and is not older than `since` ends the hold. A
  leftover or unparseable release does not; the unparseable one is reported
  as claim-unparseable on claim.released.
- Dirty excludes the state directory and a worktree's keep_untracked paths,
  counted per file (--untracked-files=all). Every other untracked file
  still counts.
- The fix is `git add -- <each file, quoted>`, never `add -A`.
- Test 20 -> 36 checks; 12 mutations, each red.

Closes #5863
Refs #5786 #5823

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(tri tick): dirty work nobody holds is an anomaly, released claim or not (#5896)

A released claim made every dirty worktree silent. Tick 21 of cron
8782e5f8 delegated two tasks to background agents and released; the
session ended, both agents died uncommitted, and tick 22's card said
ANOMALIES: 0.

New anomaly orphaned-dirty: no claim held, a worktree dirty, its newest
dirty file older than --stale-minutes, and no process with its cwd inside
it (lsof -d cwd; "NOT RUN" without lsof). Fresh writes or a live process
print a DELEGATE line instead. The detail quotes held_by; the fix names
the files, never add -A, and clears held_by.

Test: orphaned, no claim, and two negative controls (fresh write, a live
sleep standing in the tree). 7 mutations, all red.

Closes #5895

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* feat(tri pr-state): --why, and each name under the heading tri printed it under (#5868)

tri pr-state could not pass --why to tri pr ready, so a failure red
elsewhere under the same check name read as pre-existing in the loop's
report. Its parser also put every "  - name" after the VERDICT into one
only_here list: CANNOT TELL's no-baseline names and --why's new-reason
names read as "only here", and the NOT compared list above the VERDICT
was never read.

- --why is passed through; a tri without the flag (clap usage error,
  exit 2 = WAIT's code) is a no-verdict that says so, never WAIT
- only_here / no_baseline / new_reason / not_compared are kept apart;
  an unknown heading, an unknown verdict, or a name under no heading is
  kept as `other` with its heading and printed
- with --why a REASONS line counts over the rows that got a verdict and
  names the rest as not compared (a live run with an old tri read
  "REASONS: 0" over 18 no-verdicts before this)
- test 27 -> 51 checks, #5452's real answer as a fixture, a fake gh and
  tri on PATH proving the flag reaches argv; 19 mutations, each red

Closes #5867
Refs #5823 #5852

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
gHashTag added a commit that referenced this pull request Oct 4, 2026
… nine as switch (#5964)

* fix(t27c): typecheck refuses a Rust match block the parser kept as text

Since W914 the parser captures a Rust `match` block verbatim into a
StmtExpr so parsing does not fail, and no backend reads that node: a
match used as a fn's value generated an empty body in every backend
with every gate green. check_captured_match now refuses each captured
block in typecheck, naming its line and its fn or test, and points at
t27's own `switch`. Parse still accepts it, so generation is unchanged.
`match` as an identifier, field, member or call is not refused.

Unit tests: tests_5949_captured_match (refusal names lines 4 and 10;
negative control: switch, var match, match = false, Hit{match:1},
g(h.match), match(n)).

Closes #5949

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(specs): rewrite nine match blocks as t27 switch; ledger git/diff

31 captured Rust match blocks in 7 specs. 9 are rewritten as switch
(or if) in auth/config, ar/restraint, ar/coa_planning, ar/composition,
igla/training/roadmap and igla/evaluation/multi_lang_harness, and their
seals re-saved. ar/composition execute_ar_component keeps its match:
its arms assign a tuple, which t27 has no statement for; that spec is
already red upstream. git/diff matches Ok/Err on Command::new("git") in
21 blocks and is ledgered at typecheck (issue 5949, expires 2026-11-30).

max_entries 112 -> 113 is a hand edit, recorded in _why_entries_rose:
no entry could be retired without inventing a value.

Closes #5949

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(now): t27c refuses a Rust match block the parser kept as text (Closes #5949)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(gen): signed and float % lowers to @Rem (Closes #5973)

Zig refuses infix `%` unless both operands are unsigned ("signed integers
and floats must use @Rem or @mod"), so `return a % b;` with `a: i32` made
the generated file fail to compile. The W593 `@divTrunc` arm for `/`
already had the right test; `%` now uses it and emits `@rem(a, b)`, the
truncated remainder (rem(-7, 2) == -1, as in C, Rust and t27b). Unsigned
`%` stays infix. `/` needed no change.

8 of 1184 specs change gen output and 0 change gen-c; none has a new zig
error, and compiler/optimizer.t27 now passes 14/14. 12 seals resealed.

Found by the t27b differential test. Part of #5905.

Closes #5973

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(compiler): type integral Rust repeat counts (Fixes #5986)

* feat(t27b): run invariant blocks like tests, report them apart

An `invariant` block lowers exactly like a `test` (a parameterless body,
the test binding rule) and runs through the same trap machinery. t27c's
Zig backend emits it as `comptime { ... }`, so a broken invariant is a
compile error there; t27b runs it at test time and prints INVARIANT
PASS / INVARIANT FAIL, plus an `invariants N held, M broken, K not
checked` line. An invariant whose body the front-end discarded (a
`forall`, an unparseable clause) is NOT CHECKED, never "held"; a
partially parsed one is rejected, as a partial test already was.

Clause-form blocks carry no line on any node, so their header line is
looked up in the source text.

Corpus: 48 supported (47 pass, 1 spec failure), was 37; 0 mismatches.
Differential: 0 mismatches, 2500 programs per mode.

Refs #5977

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(t27b): memory primitives -- frame slots, loads/stores, rodata, bounds

The IR primitives that structs, arrays, slices and strings lower onto,
end to end through the interpreter, the JIT and the Mach-O object:

- Ty::Ptr; ExprKind::Slot / Data / Load{addr,off} / Offset{base,idx,
  scale} / Bounds{idx,len,site}; Stmt::Store / Copy; Func::slots;
  Program::data; TrapKind::Bounds = 16 (numbered high, so the scalar
  lane can add kinds below it).
- eval (the oracle): a byte-addressed model with per-call slots, a gap
  between objects and per-byte written flags. It faults on a read of
  unwritten bytes, an access outside a live slot or blob, a store into
  data, and a bool load of anything but 0/1. A fault is a lowering
  defect and never agrees with the JIT.
- codegen:
  - an aggregate area below x29, up to 16 KiB;
  - x29-relative ldur/stur fast paths;
  - uimm, unscaled and register load/store forms, with signed narrow
    loads;
  - ADRP + ADD for data;
  - shifted add or madd for Offset;
  - cmp + b.hs to a Bounds trap stub;
  - Copy unrolled 8/4/2/1, or as a post-indexed loop past 64 bytes.
- JIT: data blobs appended to the image and patched in place.
- Mach-O: a __const section with PAGE21/PAGEOFF12 relocations against
  local l_t27b_data.N symbols.
- a64: sized load/store, adr/adrp encoders and disassembly, with 32
  new clang-verified encodings and an otool comparison.

Tests:
- The random differential generator covers every primitive: slots up
  to 9000 bytes, data blobs, loads and stores of every scalar type at
  constant, masked and bounds-checked indices, short and looped
  copies, and pointer parameters into the caller's frame (the
  hidden-pointer ABI). 0 mismatches over 2500 programs per overflow
  mode.
- tests/macho.rs links an object with clang and runs it against the
  interpreter.
- tests/memory.rs pins the interpreter's faults.

Corpus unchanged (47 pass): nothing lowers to these yet.

Refs #5977

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(compiler): preserve immutable constant names in Rust (Fixes #5994)

* gate(coq): re-bless the three trios-coq files #4754 built, and run on trios-coq/** (#6006)

Coq kernel has been red on master on every run since 2026-09-24 (six,
28fd522 through 558dab4), at "No new Coq file falls outside every
build": NullorReversible.v, SparsityMask.v and SpeculativeExit.v joined
trios-coq/_CoqProject in #4754 without a re-bless.

- tools/coq_outside_build_baseline.txt: `check_coq_in_build.py --bless`,
  18 -> 15. The three compile locally (Rocq 9.1.1, `coqc -Q . TriosCoq`,
  exit 0 each, 11 Qed each, `Admitted` only inside comments).
- coq-kernel.yml: `trios-coq/**` in both path lists. The checker reads
  every _CoqProject, but the workflow ran only for coq/ and proofs/, so
  #4754 started no run and #4755 inherited the red.

Negative control: putting SparsityMask.v back in the baseline exits 1
with "1 file(s) joined a build". `--self-check` 6/6, `tri census pin
--gate` PASS, check_pr_branch_filters.py exit 0.

Closes #6005

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* fix(c): initialize array-repeat wrappers by value (Fixes #6013)

* tri: stranded-work finder and cron tick resume card (#5787)

* tri: stranded-work finder and cron tick resume card

tri stranded lists remote branches with commits off the default branch,
tips older than --hours, and no open same-repo PR. It skips dependabot/*,
patch-equivalent branches (git cherry) and squash-merged ones (merge-tree
result equals the default's tree), reports merge clean/conflict and
whether the branch ever had a PR, and runs merge-tree against a throwaway
object directory so the clone is never written. With no PR state nothing
is called stranded: the rows are "undecided" and the exit is 2.

tri tick is the resume card for a cron loop: claim and its age, the last
ledger section, each worktree's branch, dirty count, ahead/behind vs its
base and last commit age, and anomalies with a one-line fix each. git
runs with GIT_OPTIONAL_LOCKS=0; nothing is written.

Both self-tests run in loop-tools-gate, each with a negative control, and
each was seen to fail on a mutated copy of its tool.

Refs #5786

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* tri census: re-bless fetches 4->5 and shell 283->285 for tri stranded/tick

Two pinned numbers moved, both because of this PR:

- fetches: SURFACE bounded reads in scripts/tri_loop/*.py 4 -> 5. The new
  read is `gh pr list --limit PR_LIMIT` in scripts/tri_loop/stranded.py.
  It already refused to answer on a full page, but said so as "a lower
  bound" in lowercase, which the census (says_lower_bound, the Rust side's
  wording) does not read. The message now says LOWER BOUND, so the split
  is 5 guarded and 0 that do not, rather than 4 and 1.
- shell: run steps 283 -> 285 (runner bash 262 -> 264). The rise comes
  from loop-tools-gate.yml, which gains the two tri_loop tests
  (test_stranded_work_is_found_without_writing.py and
  test_a_tick_resumes_from_what_it_reads.py).

quiet was re-recorded byte-identical.

Refs #5786

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* tri pr-state: a loop's pull requests with tri pr ready's verdict, not "open" (#5824)

* tri pr-state: a loop's pull requests with tri pr ready's verdict, not "open"

A cron loop reported t27#5787 as "open" for six ticks while cli-tri was
red. tri pr-state reads every PR a tick-state.json names: gh pr view, then
tri pr ready while it is open, quoting the verdict and its exit code.
SETTLED only when every verdict is "safe to merge"; an open PR with a
running check exits 1. Anomalies: head-moved, not-open, unresolved (an
alias is never guessed), unreadable, no-verdict. Read-only; --answers
replaces gh and tri for the offline test.

tri tick's pick_id/usage take the command name so tri pr-state errors
under its own; its card points at tri pr-state for "still open?".

First real run, cron 8782e5f8, 9 PRs: all safe, exit 0, and one
head-moved on 999#3554 -- the loop's own tick-8 push, never recorded.

Census: shell run: steps 285 -> 286 and runner bash 264 -> 265, the
one new loop-tools-gate step for scripts/ci/test_an_open_pr_is_not_a_green_one.py.
Fetches unchanged: pr_state.py has no --limit read.

Closes #5823
Refs #5786

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* tri pr-state: the printed card says safe is not proof any check ran

The billing-blocked caveat lived only in the docstring, while #5824's
body said the card's NOT ESTABLISHED block carried it. Now the printed
footer says it, and the test asserts it on the settled (healthy) card;
seen red with the word removed.

No census moved.

Closes #5823

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(tri tick): a released claim is not held, and the dead-tick fix names its files (#5864)

* fix(tri tick): a released claim is not held, and the dead-tick fix names its files

tri tick never read claim.released. A tick that had finished read as dead
45 minutes later. On tick 18 of cron 8782e5f8, its fix line was
`git add -A && git commit` on PR #5839's worktree, whose only dirty files
were cron_tracking/ and .claude/launch.json, both untracked on purpose.

- A release that parses and is not older than `since` ends the hold. A
  leftover or unparseable release does not; the unparseable one is reported
  as claim-unparseable on claim.released.
- Dirty excludes the state directory and a worktree's keep_untracked paths,
  counted per file (--untracked-files=all). Every other untracked file
  still counts.
- The fix is `git add -- <each file, quoted>`, never `add -A`.
- Test 20 -> 36 checks; 12 mutations, each red.

Closes #5863
Refs #5786 #5823

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(tri tick): dirty work nobody holds is an anomaly, released claim or not (#5896)

A released claim made every dirty worktree silent. Tick 21 of cron
8782e5f8 delegated two tasks to background agents and released; the
session ended, both agents died uncommitted, and tick 22's card said
ANOMALIES: 0.

New anomaly orphaned-dirty: no claim held, a worktree dirty, its newest
dirty file older than --stale-minutes, and no process with its cwd inside
it (lsof -d cwd; "NOT RUN" without lsof). Fresh writes or a live process
print a DELEGATE line instead. The detail quotes held_by; the fix names
the files, never add -A, and clears held_by.

Test: orphaned, no claim, and two negative controls (fresh write, a live
sleep standing in the tree). 7 mutations, all red.

Closes #5895

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* feat(tri pr-state): --why, and each name under the heading tri printed it under (#5868)

tri pr-state could not pass --why to tri pr ready, so a failure red
elsewhere under the same check name read as pre-existing in the loop's
report. Its parser also put every "  - name" after the VERDICT into one
only_here list: CANNOT TELL's no-baseline names and --why's new-reason
names read as "only here", and the NOT compared list above the VERDICT
was never read.

- --why is passed through; a tri without the flag (clap usage error,
  exit 2 = WAIT's code) is a no-verdict that says so, never WAIT
- only_here / no_baseline / new_reason / not_compared are kept apart;
  an unknown heading, an unknown verdict, or a name under no heading is
  kept as `other` with its heading and printed
- with --why a REASONS line counts over the rows that got a verdict and
  names the rest as not compared (a live run with an old tri read
  "REASONS: 0" over 18 no-verdicts before this)
- test 27 -> 51 checks, #5452's real answer as a fixture, a fake gh and
  tri on PATH proving the flag reaches argv; 19 mutations, each red

Closes #5867
Refs #5823 #5852

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* fix(zig): quote declared primitive value bindings (Fixes #6040)

* feat(t27b): structs, field access, pointers and struct results

Lower struct declarations, struct literals, field reads and writes,
`&x` / `p.*` / `*T`, struct parameters and struct results to the memory
primitives of the previous change. Structs always live in memory:

- A struct local or temporary lives in a frame slot. Its literal is built
  in place when the destination is fresh and its address is pure;
  otherwise it is built in a temporary and copied.
- A struct parameter is the caller's memory, passed by address and
  never written.
- A struct result is built through a hidden last pointer parameter.
  Such functions are listed in `Program.internal_abi`, and `t27b build`
  does not export them (it prints a note).
- A module struct constant goes to read-only data. A scalar field of it
  folds to a constant (`const OX: i32 = ORIGIN.x`).
- Layout follows the C rules and is computed on first use. A pointee is
  only named, so `next: *const Node` works. A struct that contains
  itself by value is rejected, and so is a generic struct.
- An address-taken scalar local or parameter lives in a slot.
- Compound assignment through an impure address evaluates the address
  once.

Every rejection is precise: a missing, unknown or duplicate field, a
positional initializer, an anonymous `.{}` with no result type, a write
through a constant, a struct or pointer operand, a non-value `X.y`, and
a struct that contains itself.

IR: new `ExprKind::Seq { stmts, value }` (stores and copies, then a
value), so a struct temporary is built exactly where its argument is
evaluated. The interpreter and the code generator both implement it, and
the random differential generator now emits it.

Corpus (specs/, --jobs 6): 47 -> 70 files supported with every test
passing, 0 JIT/interpreter mismatches.
- One newly reached file fails one test (fpga/verification/build_verify,
  module count 33 vs 31). t27c gen + zig test fails the same assertion,
  so the reference fails here too.
- Two files time out in the t27c parse phase alone at a load average
  near 780.

Refs #5977

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(ci): install Zig for native controls; shell run steps 264 to 265 (Refs #6040)

* fix(ci): install corpus Icarus prerequisite; shell 268 to 269 (Refs #6040)

* fix(verilog): preserve conditional return and assertion context; Fixes #6043

* fix(rust): preserve function and conditional tail values; Fixes #6062

* feat(core): t27core -- self-hosted compiler core MVP, fixpoint with gen-c (#6041)

A t27 compiler written in t27 (specs/compiler/core/t27core.t27): lexer,
parser, checks and C emitter for the subset `t27c gen-c` lowers without
loss. Built by gen-c and run on its own source, it writes gen-c's output
byte for byte.

bootstrap/tests/core_selfhost.rs gates the fixpoint, the core's own test
blocks, 6 fixtures, 16 refusals with their codes, and a differential over
specs/: 58 specs accepted, all byte-identical to gen-c (floor 50).

Shapes gen-c lowers with loss are refused, not copied: `- -x` as `--x`,
declarations after `endmodule`, top-level consts truncated after their
first token, and the earlier ones recorded on the epic.

Closes #5981
Refs #5980

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* feat(agents): Gamma holds a /self-host skill for epic #5980 (#6054)

The rules of the t27-in-t27 compiler had no home a bee loads: byte identity with gen-c, refusing the shapes gen-c lowers with loss, probing gen-c first, and the ;-alone-line trap. They now live in .claude/skills/self-host/SKILL.md. Its card is specs/skills/t27-self-host.t27, and agent C holds it through its prompt's Skills section and the alphabet's domain-lead table.

Closes #6053
Part of #5980

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* fix(t27c): refuse a colon inside a field type and @as to a slice type; fix the six silent specs (#5971)

* fix(t27c): typecheck refuses a colon inside a field type and @as to a slice type

Six specs parsed, typechecked and generated output their backend cannot
compile (`tri misread` silent pairs). Two defects:

- A struct field whose type, with `::` paths removed, still holds a `:`.
  Either the field has no `,` after it (missing, or inside a trailing `#`
  comment, which runs to the end of the line) and swallowed the fields
  after it, or it is a map type `[K: V]`, which t27 does not have.
- `@as(T, x)` with a slice or array type: in argument position the parser
  reads `[]u8` as an array literal, and every backend loses the type.

Both are refused in typecheck, naming the line. The parser now records a
struct field's line; no backend reads it, so generated output does not
move (seal currency: 0 stale generated-code hashes).

Closes #5968

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(specs): put the comma before the comment, drop the map type and slice casts

- ar/coa_planning: the missing `,` after `verification_status`.
- tri/agent/governance_agent, tri/utils/help: `field : T  # note,` is now
  `field : T,  // note`; the `#` comment had taken the `,` with it.
- git/schema: `env: [str: str]` is now `env: []EnvVar`; the one
  constructor, git/operations' options_construction test, builds EnvVar.
- port/tools/wp18: `_ssot` writes one `// CATALOG: id=<id>` line per id,
  as the Python original does, instead of an `@as([]u8, ..)` cast.
- tri/pipeline/builder: `@as([]T, &.{})` in a test is now a length check.

Re-sealed with `t27c seal --save`, including git/status and git/diff,
whose generated code inherits Options.

Closes #5968

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(now): t27c refuses a colon inside a field type and a slice cast (Closes #5968)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* fix(t27c): var tuple destructure silences never-mutated Zig locals (Closes #5886) (#5902)

`var (s, d) = f();` lowered to `var s, var d = f();` with no silencer, so
Zig rejected every element the function never reassigns ("local variable
is never mutated") and `t27c test-report` printed BLOCKED for
d_slow_blink.t27 (Refs #5682).

The StmtLocal tuple-destructure branch now emits, for a `var` destructure,
the same `_ = &name;` per named element that the single-name `var` path
already emits, and records the name in `discarded_by_ref` so W730 drops a
later `_ = name;` (a pointless discard in Zig). A bare `_` element gets no
silencer; `let (s, d)` is unchanged.

Test: test_var_destructure_never_reassigned_zig. Without the fix it fails
by assertion ("`_ = &s;` silencer missing"); with it, it passes, and the
whole t27c bin unit suite passes (1759, 0 failed, 2 ignored).

FROZEN_HASH resealed to the new compiler.rs digest.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* fix(seals): re-seal wp18 with this branch's compiler after the master merge

The master merge (2ec2d00) took master's wp18 seal, but this branch keeps
the #5971 spec (spec_hash c2c89bb3..., the CATALOG port of `_ssot`). Lab run
of 2ec2d00: seal-currency and seal-coverage red on this one file only.

Re-sealed on the Railway t27c lab: t27c built from 2ec2d00, zig 0.16.0 on
PATH, `t27c seal --save` + `tri seals sync-twins` (517 twins consistent, 0
written). gen_hash_zig is 7fd0ed8c, the same output #5971 sealed; the zig
test result is unchanged (expected `;` at the `sha256::Hasher` line, 86:24 in
the emitted source).

Refs #6092

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(gen-zig): keep `_ = call(..);` instead of deleting it (Closes #5984) (#6003)

* fix(gen-zig): keep `_ = call(..);` instead of deleting it (Closes #5984)

Dead-store elimination read `_` as a variable nobody reads and deleted every
top-level `_ = call(args);` in a fn body, call and all. The unused-parameter
pass then added `_ = p;` for a parameter that only the deleted call used, which
Zig rejects as a pointless discard once the call is back.

OptConfig gains keep_call_discards. Compiler::compile (gen-zig) sets it, so a
discard whose right-hand side holds a call survives. It defaults to false, so
the Verilog path is byte-identical (0 of 37 specs with a discarded call
differ).

Measured over 1297 tracked .t27 files: gen-zig output changes for 5 specs,
9 calls come back, 2 spurious `_ = param;` lines and 1 `_ = json_str;` line go.
Their 9 seals are re-sealed. FROZEN_HASH re-pinned.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* seal: re-seal wp18 selftest after the gen-zig discard-call fix

Spec unchanged; only gen_hash_zig moves (the emitter fix in this PR).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* seal: re-seal wp18 selftest with the merged compiler (Closes #5949)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Dmitrii Fedorov <dmitrii.f@t27.ai>
gHashTag added a commit that referenced this pull request Oct 4, 2026
…down specs (#5923) (#5947)

* fix(specs): rewrite eight Markdown documents named .t27 as t27 modules

Eight files under specs/ were Markdown with a .t27 name. The parser refused
each at its first line, so they generated nothing and their "tests" had
comment-only bodies. Each is now a module whose prose is `;` comments and
whose arithmetic is checked: 54 test blocks, all passing under gen-zig.

  physics/hslm_benchmark (7)  physics/e8_lqg_bridge (8)
  physics/lqg_cs_bridge (5)   physics/gamma-conflict (10, module gamma_conflict)
  benchmarks/bench_nn (6)     benchmarks/gf16_bfloat16_nmse (7)
  api/tri_net_api (5)         conformance/e2e_scenarios (6)

Where the document contradicted itself or the repository, the module
records the defect rather than the claim: gamma-conflict carries six
CORRECTIONS, tri_net_api states the FROZEN_HASH line format build.rs
actually reads, and e2e_scenarios pins a pack scenario that overwrites its
input before reading it and a total verdict its own rows do not imply.

Seals re-taken with `t27c seal <spec> --save`.

Refs #5923

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(t27c): refuse Rust macro calls in parse and typeless fields in typecheck

Two ways the compiler read a spec wrong while every gate stayed green.

parse: `format!("{}", a)` was read as `format` followed by the statement
`!("{}", a)`, which gen-rust lowered to `(("{}", a) == 0);`. `assert!(c)`
in a test became an assert of a negated tuple. t27 has no macros; the
parser now refuses an identifier glued to `!` and followed by `(`, `[` or
`{` on the same line, naming the macro. `a != b`, a prefix `!x`, strings
and comments are untouched (unit tests cover both sides).

typecheck: a struct field whose type is empty or an integer literal is an
error (#3225). The parser recovers list-valued declarations it does not
implement as fields, and gen-rust wrote them as `pub f: ,` / `pub f: 0,`.
Generation does not run typecheck, so those specs still generate exactly
as before; what changes is that a gate now goes red on them.

Specs that used macros are fixed in their source: auth/config `panic!` ->
`unreachable`, seven port specs `assert!(` -> `assert(`. The generate
baseline drops the eight rewritten Markdown specs and records the two
whose remaining failure is now the named `format!` refusal.

FROZEN_HASH updated for compiler.rs.

Refs #5923, #3225

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(tri): misread splits each pair into refused and silent; ledger the new rule

`tri misread` now typechecks every spec that carries a shape and reports
each spec-shape pair as REFUSED (typecheck goes red) or SILENT (every gate
green). A refusing typecheck is not trusted until a clean spec has been
seen to typecheck and show no shape -- a broken binary would otherwise
turn the whole table "refused" and read as progress.

Measured on the corpus: 41 pairs, 35 refused, 6 silent (one Vec<> type,
five colon-in-type specs).

Suite ledger: +25 typecheck entries for the specs the new rule refuses
(issue 3225, each naming its struct and fields), -8 parse entries for the
specs rewritten as modules; cap 126 -> 112. The tool card's ABOUT follows
the new clap doc.

Closes #5923

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(ledger): swap two Lean completeness entries the Markdown rewrite moved; add the NOW entry

corpus_classifier_matches_lean_completeness failed on CI. Two of the eight
Markdown files this PR rewrote as t27 modules changed their Rust verdict:

- physics_lqg_cs_bridge now agrees with its Lean theorem; retired.
- benchmarks_gf16_bfloat16_nmse became classifiable and disagrees. The
  spec computes in f64, which Icarus does not lower (with the f64 lines
  removed the classifier answers true); the theorem was written over
  the Markdown document. Entered with its reason, as #2884 did.

max_entries stays at 77 and max_vacuous at 44.

Closes #5923

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(t27c): refuse a Rust match block the parser kept as text; rewrite nine as switch (#5964)

* fix(t27c): typecheck refuses a Rust match block the parser kept as text

Since W914 the parser captures a Rust `match` block verbatim into a
StmtExpr so parsing does not fail, and no backend reads that node: a
match used as a fn's value generated an empty body in every backend
with every gate green. check_captured_match now refuses each captured
block in typecheck, naming its line and its fn or test, and points at
t27's own `switch`. Parse still accepts it, so generation is unchanged.
`match` as an identifier, field, member or call is not refused.

Unit tests: tests_5949_captured_match (refusal names lines 4 and 10;
negative control: switch, var match, match = false, Hit{match:1},
g(h.match), match(n)).

Closes #5949

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(specs): rewrite nine match blocks as t27 switch; ledger git/diff

31 captured Rust match blocks in 7 specs. 9 are rewritten as switch
(or if) in auth/config, ar/restraint, ar/coa_planning, ar/composition,
igla/training/roadmap and igla/evaluation/multi_lang_harness, and their
seals re-saved. ar/composition execute_ar_component keeps its match:
its arms assign a tuple, which t27 has no statement for; that spec is
already red upstream. git/diff matches Ok/Err on Command::new("git") in
21 blocks and is ledgered at typecheck (issue 5949, expires 2026-11-30).

max_entries 112 -> 113 is a hand edit, recorded in _why_entries_rose:
no entry could be retired without inventing a value.

Closes #5949

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(now): t27c refuses a Rust match block the parser kept as text (Closes #5949)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(gen): signed and float % lowers to @Rem (Closes #5973)

Zig refuses infix `%` unless both operands are unsigned ("signed integers
and floats must use @Rem or @mod"), so `return a % b;` with `a: i32` made
the generated file fail to compile. The W593 `@divTrunc` arm for `/`
already had the right test; `%` now uses it and emits `@rem(a, b)`, the
truncated remainder (rem(-7, 2) == -1, as in C, Rust and t27b). Unsigned
`%` stays infix. `/` needed no change.

8 of 1184 specs change gen output and 0 change gen-c; none has a new zig
error, and compiler/optimizer.t27 now passes 14/14. 12 seals resealed.

Found by the t27b differential test. Part of #5905.

Closes #5973

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(compiler): type integral Rust repeat counts (Fixes #5986)

* feat(t27b): run invariant blocks like tests, report them apart

An `invariant` block lowers exactly like a `test` (a parameterless body,
the test binding rule) and runs through the same trap machinery. t27c's
Zig backend emits it as `comptime { ... }`, so a broken invariant is a
compile error there; t27b runs it at test time and prints INVARIANT
PASS / INVARIANT FAIL, plus an `invariants N held, M broken, K not
checked` line. An invariant whose body the front-end discarded (a
`forall`, an unparseable clause) is NOT CHECKED, never "held"; a
partially parsed one is rejected, as a partial test already was.

Clause-form blocks carry no line on any node, so their header line is
looked up in the source text.

Corpus: 48 supported (47 pass, 1 spec failure), was 37; 0 mismatches.
Differential: 0 mismatches, 2500 programs per mode.

Refs #5977

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat(t27b): memory primitives -- frame slots, loads/stores, rodata, bounds

The IR primitives that structs, arrays, slices and strings lower onto,
end to end through the interpreter, the JIT and the Mach-O object:

- Ty::Ptr; ExprKind::Slot / Data / Load{addr,off} / Offset{base,idx,
  scale} / Bounds{idx,len,site}; Stmt::Store / Copy; Func::slots;
  Program::data; TrapKind::Bounds = 16 (numbered high, so the scalar
  lane can add kinds below it).
- eval (the oracle): a byte-addressed model with per-call slots, a gap
  between objects and per-byte written flags. It faults on a read of
  unwritten bytes, an access outside a live slot or blob, a store into
  data, and a bool load of anything but 0/1. A fault is a lowering
  defect and never agrees with the JIT.
- codegen:
  - an aggregate area below x29, up to 16 KiB;
  - x29-relative ldur/stur fast paths;
  - uimm, unscaled and register load/store forms, with signed narrow
    loads;
  - ADRP + ADD for data;
  - shifted add or madd for Offset;
  - cmp + b.hs to a Bounds trap stub;
  - Copy unrolled 8/4/2/1, or as a post-indexed loop past 64 bytes.
- JIT: data blobs appended to the image and patched in place.
- Mach-O: a __const section with PAGE21/PAGEOFF12 relocations against
  local l_t27b_data.N symbols.
- a64: sized load/store, adr/adrp encoders and disassembly, with 32
  new clang-verified encodings and an otool comparison.

Tests:
- The random differential generator covers every primitive: slots up
  to 9000 bytes, data blobs, loads and stores of every scalar type at
  constant, masked and bounds-checked indices, short and looped
  copies, and pointer parameters into the caller's frame (the
  hidden-pointer ABI). 0 mismatches over 2500 programs per overflow
  mode.
- tests/macho.rs links an object with clang and runs it against the
  interpreter.
- tests/memory.rs pins the interpreter's faults.

Corpus unchanged (47 pass): nothing lowers to these yet.

Refs #5977

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(compiler): preserve immutable constant names in Rust (Fixes #5994)

* gate(coq): re-bless the three trios-coq files #4754 built, and run on trios-coq/** (#6006)

Coq kernel has been red on master on every run since 2026-09-24 (six,
28fd522 through 558dab4), at "No new Coq file falls outside every
build": NullorReversible.v, SparsityMask.v and SpeculativeExit.v joined
trios-coq/_CoqProject in #4754 without a re-bless.

- tools/coq_outside_build_baseline.txt: `check_coq_in_build.py --bless`,
  18 -> 15. The three compile locally (Rocq 9.1.1, `coqc -Q . TriosCoq`,
  exit 0 each, 11 Qed each, `Admitted` only inside comments).
- coq-kernel.yml: `trios-coq/**` in both path lists. The checker reads
  every _CoqProject, but the workflow ran only for coq/ and proofs/, so
  #4754 started no run and #4755 inherited the red.

Negative control: putting SparsityMask.v back in the baseline exits 1
with "1 file(s) joined a build". `--self-check` 6/6, `tri census pin
--gate` PASS, check_pr_branch_filters.py exit 0.

Closes #6005

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* fix(c): initialize array-repeat wrappers by value (Fixes #6013)

* tri: stranded-work finder and cron tick resume card (#5787)

* tri: stranded-work finder and cron tick resume card

tri stranded lists remote branches with commits off the default branch,
tips older than --hours, and no open same-repo PR. It skips dependabot/*,
patch-equivalent branches (git cherry) and squash-merged ones (merge-tree
result equals the default's tree), reports merge clean/conflict and
whether the branch ever had a PR, and runs merge-tree against a throwaway
object directory so the clone is never written. With no PR state nothing
is called stranded: the rows are "undecided" and the exit is 2.

tri tick is the resume card for a cron loop: claim and its age, the last
ledger section, each worktree's branch, dirty count, ahead/behind vs its
base and last commit age, and anomalies with a one-line fix each. git
runs with GIT_OPTIONAL_LOCKS=0; nothing is written.

Both self-tests run in loop-tools-gate, each with a negative control, and
each was seen to fail on a mutated copy of its tool.

Refs #5786

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* tri census: re-bless fetches 4->5 and shell 283->285 for tri stranded/tick

Two pinned numbers moved, both because of this PR:

- fetches: SURFACE bounded reads in scripts/tri_loop/*.py 4 -> 5. The new
  read is `gh pr list --limit PR_LIMIT` in scripts/tri_loop/stranded.py.
  It already refused to answer on a full page, but said so as "a lower
  bound" in lowercase, which the census (says_lower_bound, the Rust side's
  wording) does not read. The message now says LOWER BOUND, so the split
  is 5 guarded and 0 that do not, rather than 4 and 1.
- shell: run steps 283 -> 285 (runner bash 262 -> 264). The rise comes
  from loop-tools-gate.yml, which gains the two tri_loop tests
  (test_stranded_work_is_found_without_writing.py and
  test_a_tick_resumes_from_what_it_reads.py).

quiet was re-recorded byte-identical.

Refs #5786

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* tri pr-state: a loop's pull requests with tri pr ready's verdict, not "open" (#5824)

* tri pr-state: a loop's pull requests with tri pr ready's verdict, not "open"

A cron loop reported t27#5787 as "open" for six ticks while cli-tri was
red. tri pr-state reads every PR a tick-state.json names: gh pr view, then
tri pr ready while it is open, quoting the verdict and its exit code.
SETTLED only when every verdict is "safe to merge"; an open PR with a
running check exits 1. Anomalies: head-moved, not-open, unresolved (an
alias is never guessed), unreadable, no-verdict. Read-only; --answers
replaces gh and tri for the offline test.

tri tick's pick_id/usage take the command name so tri pr-state errors
under its own; its card points at tri pr-state for "still open?".

First real run, cron 8782e5f8, 9 PRs: all safe, exit 0, and one
head-moved on 999#3554 -- the loop's own tick-8 push, never recorded.

Census: shell run: steps 285 -> 286 and runner bash 264 -> 265, the
one new loop-tools-gate step for scripts/ci/test_an_open_pr_is_not_a_green_one.py.
Fetches unchanged: pr_state.py has no --limit read.

Closes #5823
Refs #5786

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* tri pr-state: the printed card says safe is not proof any check ran

The billing-blocked caveat lived only in the docstring, while #5824's
body said the card's NOT ESTABLISHED block carried it. Now the printed
footer says it, and the test asserts it on the settled (healthy) card;
seen red with the word removed.

No census moved.

Closes #5823

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(tri tick): a released claim is not held, and the dead-tick fix names its files (#5864)

* fix(tri tick): a released claim is not held, and the dead-tick fix names its files

tri tick never read claim.released. A tick that had finished read as dead
45 minutes later. On tick 18 of cron 8782e5f8, its fix line was
`git add -A && git commit` on PR #5839's worktree, whose only dirty files
were cron_tracking/ and .claude/launch.json, both untracked on purpose.

- A release that parses and is not older than `since` ends the hold. A
  leftover or unparseable release does not; the unparseable one is reported
  as claim-unparseable on claim.released.
- Dirty excludes the state directory and a worktree's keep_untracked paths,
  counted per file (--untracked-files=all). Every other untracked file
  still counts.
- The fix is `git add -- <each file, quoted>`, never `add -A`.
- Test 20 -> 36 checks; 12 mutations, each red.

Closes #5863
Refs #5786 #5823

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(tri tick): dirty work nobody holds is an anomaly, released claim or not (#5896)

A released claim made every dirty worktree silent. Tick 21 of cron
8782e5f8 delegated two tasks to background agents and released; the
session ended, both agents died uncommitted, and tick 22's card said
ANOMALIES: 0.

New anomaly orphaned-dirty: no claim held, a worktree dirty, its newest
dirty file older than --stale-minutes, and no process with its cwd inside
it (lsof -d cwd; "NOT RUN" without lsof). Fresh writes or a live process
print a DELEGATE line instead. The detail quotes held_by; the fix names
the files, never add -A, and clears held_by.

Test: orphaned, no claim, and two negative controls (fresh write, a live
sleep standing in the tree). 7 mutations, all red.

Closes #5895

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* feat(tri pr-state): --why, and each name under the heading tri printed it under (#5868)

tri pr-state could not pass --why to tri pr ready, so a failure red
elsewhere under the same check name read as pre-existing in the loop's
report. Its parser also put every "  - name" after the VERDICT into one
only_here list: CANNOT TELL's no-baseline names and --why's new-reason
names read as "only here", and the NOT compared list above the VERDICT
was never read.

- --why is passed through; a tri without the flag (clap usage error,
  exit 2 = WAIT's code) is a no-verdict that says so, never WAIT
- only_here / no_baseline / new_reason / not_compared are kept apart;
  an unknown heading, an unknown verdict, or a name under no heading is
  kept as `other` with its heading and printed
- with --why a REASONS line counts over the rows that got a verdict and
  names the rest as not compared (a live run with an old tri read
  "REASONS: 0" over 18 no-verdicts before this)
- test 27 -> 51 checks, #5452's real answer as a fixture, a fake gh and
  tri on PATH proving the flag reaches argv; 19 mutations, each red

Closes #5867
Refs #5823 #5852

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* fix(zig): quote declared primitive value bindings (Fixes #6040)

* feat(t27b): structs, field access, pointers and struct results

Lower struct declarations, struct literals, field reads and writes,
`&x` / `p.*` / `*T`, struct parameters and struct results to the memory
primitives of the previous change. Structs always live in memory:

- A struct local or temporary lives in a frame slot. Its literal is built
  in place when the destination is fresh and its address is pure;
  otherwise it is built in a temporary and copied.
- A struct parameter is the caller's memory, passed by address and
  never written.
- A struct result is built through a hidden last pointer parameter.
  Such functions are listed in `Program.internal_abi`, and `t27b build`
  does not export them (it prints a note).
- A module struct constant goes to read-only data. A scalar field of it
  folds to a constant (`const OX: i32 = ORIGIN.x`).
- Layout follows the C rules and is computed on first use. A pointee is
  only named, so `next: *const Node` works. A struct that contains
  itself by value is rejected, and so is a generic struct.
- An address-taken scalar local or parameter lives in a slot.
- Compound assignment through an impure address evaluates the address
  once.

Every rejection is precise: a missing, unknown or duplicate field, a
positional initializer, an anonymous `.{}` with no result type, a write
through a constant, a struct or pointer operand, a non-value `X.y`, and
a struct that contains itself.

IR: new `ExprKind::Seq { stmts, value }` (stores and copies, then a
value), so a struct temporary is built exactly where its argument is
evaluated. The interpreter and the code generator both implement it, and
the random differential generator now emits it.

Corpus (specs/, --jobs 6): 47 -> 70 files supported with every test
passing, 0 JIT/interpreter mismatches.
- One newly reached file fails one test (fpga/verification/build_verify,
  module count 33 vs 31). t27c gen + zig test fails the same assertion,
  so the reference fails here too.
- Two files time out in the t27c parse phase alone at a load average
  near 780.

Refs #5977

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(ci): install Zig for native controls; shell run steps 264 to 265 (Refs #6040)

* fix(ci): install corpus Icarus prerequisite; shell 268 to 269 (Refs #6040)

* fix(verilog): preserve conditional return and assertion context; Fixes #6043

* fix(rust): preserve function and conditional tail values; Fixes #6062

* feat(core): t27core -- self-hosted compiler core MVP, fixpoint with gen-c (#6041)

A t27 compiler written in t27 (specs/compiler/core/t27core.t27): lexer,
parser, checks and C emitter for the subset `t27c gen-c` lowers without
loss. Built by gen-c and run on its own source, it writes gen-c's output
byte for byte.

bootstrap/tests/core_selfhost.rs gates the fixpoint, the core's own test
blocks, 6 fixtures, 16 refusals with their codes, and a differential over
specs/: 58 specs accepted, all byte-identical to gen-c (floor 50).

Shapes gen-c lowers with loss are refused, not copied: `- -x` as `--x`,
declarations after `endmodule`, top-level consts truncated after their
first token, and the earlier ones recorded on the epic.

Closes #5981
Refs #5980

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* feat(agents): Gamma holds a /self-host skill for epic #5980 (#6054)

The rules of the t27-in-t27 compiler had no home a bee loads: byte identity with gen-c, refusing the shapes gen-c lowers with loss, probing gen-c first, and the ;-alone-line trap. They now live in .claude/skills/self-host/SKILL.md. Its card is specs/skills/t27-self-host.t27, and agent C holds it through its prompt's Skills section and the alphabet's domain-lead table.

Closes #6053
Part of #5980

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* fix(t27c): refuse a colon inside a field type and @as to a slice type; fix the six silent specs (#5971)

* fix(t27c): typecheck refuses a colon inside a field type and @as to a slice type

Six specs parsed, typechecked and generated output their backend cannot
compile (`tri misread` silent pairs). Two defects:

- A struct field whose type, with `::` paths removed, still holds a `:`.
  Either the field has no `,` after it (missing, or inside a trailing `#`
  comment, which runs to the end of the line) and swallowed the fields
  after it, or it is a map type `[K: V]`, which t27 does not have.
- `@as(T, x)` with a slice or array type: in argument position the parser
  reads `[]u8` as an array literal, and every backend loses the type.

Both are refused in typecheck, naming the line. The parser now records a
struct field's line; no backend reads it, so generated output does not
move (seal currency: 0 stale generated-code hashes).

Closes #5968

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(specs): put the comma before the comment, drop the map type and slice casts

- ar/coa_planning: the missing `,` after `verification_status`.
- tri/agent/governance_agent, tri/utils/help: `field : T  # note,` is now
  `field : T,  // note`; the `#` comment had taken the `,` with it.
- git/schema: `env: [str: str]` is now `env: []EnvVar`; the one
  constructor, git/operations' options_construction test, builds EnvVar.
- port/tools/wp18: `_ssot` writes one `// CATALOG: id=<id>` line per id,
  as the Python original does, instead of an `@as([]u8, ..)` cast.
- tri/pipeline/builder: `@as([]T, &.{})` in a test is now a length check.

Re-sealed with `t27c seal --save`, including git/status and git/diff,
whose generated code inherits Options.

Closes #5968

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* docs(now): t27c refuses a colon inside a field type and a slice cast (Closes #5968)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* fix(t27c): var tuple destructure silences never-mutated Zig locals (Closes #5886) (#5902)

`var (s, d) = f();` lowered to `var s, var d = f();` with no silencer, so
Zig rejected every element the function never reassigns ("local variable
is never mutated") and `t27c test-report` printed BLOCKED for
d_slow_blink.t27 (Refs #5682).

The StmtLocal tuple-destructure branch now emits, for a `var` destructure,
the same `_ = &name;` per named element that the single-name `var` path
already emits, and records the name in `discarded_by_ref` so W730 drops a
later `_ = name;` (a pointless discard in Zig). A bare `_` element gets no
silencer; `let (s, d)` is unchanged.

Test: test_var_destructure_never_reassigned_zig. Without the fix it fails
by assertion ("`_ = &s;` silencer missing"); with it, it passes, and the
whole t27c bin unit suite passes (1759, 0 failed, 2 ignored).

FROZEN_HASH resealed to the new compiler.rs digest.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* fix(seals): re-seal wp18 with this branch's compiler after the master merge

The master merge (2ec2d00) took master's wp18 seal, but this branch keeps
the #5971 spec (spec_hash c2c89bb3..., the CATALOG port of `_ssot`). Lab run
of 2ec2d00: seal-currency and seal-coverage red on this one file only.

Re-sealed on the Railway t27c lab: t27c built from 2ec2d00, zig 0.16.0 on
PATH, `t27c seal --save` + `tri seals sync-twins` (517 twins consistent, 0
written). gen_hash_zig is 7fd0ed8c, the same output #5971 sealed; the zig
test result is unchanged (expected `;` at the `sha256::Hasher` line, 86:24 in
the emitted source).

Refs #6092

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(gen-zig): keep `_ = call(..);` instead of deleting it (Closes #5984) (#6003)

* fix(gen-zig): keep `_ = call(..);` instead of deleting it (Closes #5984)

Dead-store elimination read `_` as a variable nobody reads and deleted every
top-level `_ = call(args);` in a fn body, call and all. The unused-parameter
pass then added `_ = p;` for a parameter that only the deleted call used, which
Zig rejects as a pointless discard once the call is back.

OptConfig gains keep_call_discards. Compiler::compile (gen-zig) sets it, so a
discard whose right-hand side holds a call survives. It defaults to false, so
the Verilog path is byte-identical (0 of 37 specs with a discarded call
differ).

Measured over 1297 tracked .t27 files: gen-zig output changes for 5 specs,
9 calls come back, 2 spurious `_ = param;` lines and 1 `_ = json_str;` line go.
Their 9 seals are re-sealed. FROZEN_HASH re-pinned.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* seal: re-seal wp18 selftest after the gen-zig discard-call fix

Spec unchanged; only gen_hash_zig moves (the emitter fix in this PR).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* seal: re-seal wp18 selftest with the merged compiler (Closes #5949)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Dmitrii Fedorov <dmitrii.f@t27.ai>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Dmitrii Fedorov <dmitrii.f@t27.ai>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant