Skip to content

tri mutate spec --lab: run the mutation pass on the Railway lab from a Mac worktree #7050

Open

Description

@gHashTag

Problem

Every tick of the actors loop (epic #6971) runs tri mutate spec on the Railway lab by hand, because t27c and tri builds run there and never on the Mac. Each tick repeats the same five steps:

  • base64-encode the spec and a shell script;
  • send them over railway ssh, with 3 retries and a grep that drops the CLI's banner lines;
  • start the run with nohup so a dropped ssh does not kill it;
  • read the output on a later ssh call;
  • remove the files and check that no zig test process is left (trap T9 in the queen-actors-loop skill: a timed-out run once left orphans spinning at 100% CPU).

Tick 7 spent 6 of its ssh calls on this plumbing. One remote pkill with an unbracketed pattern also once killed its own shell (trap T10).

Evidence

  • scripts/tri_loop/t27b.py is the only tool that already speaks railway ssh, and only for t27b.
  • tri --help on the Mac build has no lab command (tri lab-exec is unrecognized there).

Proposal

Add tri mutate spec --lab [--fn NAME]. It ships the local spec to a fresh temp dir on the lab, runs the lab's own tri mutate spec with --jobs 8, waits, prints the report, removes the temp dir and reports any zig test process left behind as an error.

  • The lab ids come from the environment, never from the source.
  • No secret is printed.
  • Under specs/policy/own_language.t27, the command is specified first as a .t27 contract: steps, exit codes, and the cleanup rule. Any Rust carries the owner-approved-foreign label.

Acceptance

  • From a Mac worktree, tri mutate spec --lab --file specs/queen/actors.t27 --fn start_answer prints 4 of 4 killed. The lab command it runs by hand prints the same.
  • After the command, ls /data | grep -c tri-mutate- on the lab is 0, and ps aux | grep -c '[z]ig test' is 0.
  • A run cut by a dropped ssh is still read to the end by a second call, because the run sits under nohup and its output file has a known name.

Part of epic #6971. Depends on #7043 (tri mutate spec) merging.

Boundary

  • specs/policy/own_language.t27
  • specs/queen/actors.t27
  • scripts/tri_loop/t27b.py

User Scenarios

  • Given a Mac worktree with a spec file at specs/queen/actors.t27 and a Railway lab configured via environment, When the user runs tri mutate spec --lab --file specs/queen/actors.t27 --fn start_answer, Then the command ships the spec to a fresh temp dir on the lab, runs the lab's tri mutate spec --jobs 8, prints the mutation report 4 of 4 killed, removes the temp dir, and reports any leftover zig test processes as an error.

  • Given an SSH connection to the lab drops during a mutation run, When the user re-runs the command or reconnects, Then the nohup'd run continues on the lab and its output is read to completion on the next SSH call because the output file has a known name.

Requirements

  • FR-001: The command tri mutate spec --lab MUST accept --file and optional --fn arguments and MUST ship the local spec to a fresh temp directory on the Railway lab.
  • FR-002: The lab command MUST be invoked as tri mutate spec --jobs 8 with the shipped spec and function name.
  • FR-003: Lab identifiers (host, user, paths) MUST come from environment variables only, never from source code.
  • FR-004: No secret material MUST be printed to stdout or stderr at any point.
  • FR-005: After the remote run completes, the temp directory on the lab MUST be removed.
  • FR-006: Any zig test process left running on the lab after the mutation run MUST be reported as an error.
  • FR-007: The remote mutation run MUST be started under nohup so that a dropped SSH connection does not kill it, and its output MUST be written to a known file for later retrieval.
  • FR-008: The behavior of tri mutate spec --lab MUST be specified first in specs/policy/own_language.t27 as a .t27 contract covering steps, exit codes, and the cleanup rule; any Rust implementation carries the owner-approved-foreign label.

Success Criteria

  • tri mutate spec --lab --file specs/queen/actors.t27 --fn start_answer prints 4 of 4 killed and exits with exit 0.
  • After the command completes, ssh lab "ls /data | grep -c tri-mutate-" outputs 0 and exits with exit 0.
  • After the command completes, ssh lab "ps aux | grep -c '[z]ig test'" outputs 0 and exits with exit 0.
  • Simulating a dropped SSH during the run, a second SSH call retrieves the full mutation report from the known output file and the command exits with exit 0.
  • The spec specs/policy/own_language.t27 contains the contract for the --lab mutation pass with steps, exit codes, and cleanup rule.

Activity

  1. gHashTag commented on Oct 7, 2026

    @gHashTag
    OwnerAuthor

    Decomposition, and the first slice.

    Slice 1: the contract in t27. Committed locally as e8702ee0d on branch claude/mutate-lab-7050. The PR opens after the PRs queued ahead of it merge, one at a time. The new spec specs/tri/mutate/lab.t27 (module TriMutateLab) states what --lab decides:

    • run_state(dir_exists, exit_written, runner_alive) returns NONE, RUNNING, DONE or LOST. The run is a nohup'd job in its own directory, so a dropped ssh is read back from the pid file, the exit file and the output file. This is the issue's third acceptance line.
    • launch_allowed(state) holds only for NONE. A launch over a running run, an unread result or an uncleaned lost run is refused.
    • may_remove(state, orphans) holds only for DONE or LOST with no orphan left. Orphans are killed and recounted first (trap T9).
    • lab_exit(state, tool_rc, orphans): 0 OK, 1 tool failed, 2 no result, 3 orphans, 4 still running.
    • ssh_should_retry(attempts, transient, is_write): reads retry, 3 tries in all; writes never retry.
    • poll_wait_seconds: 5, 10, 20, 40, then 60.
    • zig_j(nproc, jobs) = max(1, nproc / jobs).
    • lab_jobs(requested, pids_max, pids_used, per_job) cuts the request to the pids left after a reserve of 104. 0 means do not start.

    The spec has 18 tests and 4 invariants. zig test passes 18/18 and test-report shows 0 vacuous. On the lab, tri mutate spec killed 48 of 52 mutants; the 4 survivors are equivalent, each reason written in the commit body. All 38 hand mutants were killed.

    Measurement behind zig_j and the reserve. On the lab, 2026-10-07: 48 cores, pids.max 1000, pids.current 713 before the probe. 8 parallel zig test compiles of specs/queen/actors.t27, two runs at each setting, 8/8 passing every time:

    zig setting pids added time per run
    default 104, 101 4396, 4341 ms
    -j6 20, 20 4371, 4486 ms
    -j1 10, 10 8918, 8706 ms

    -j6 (48 / 8) keeps the default's wall time with about a fifth of the pids. With around 700 of the lab's 1000 pids held by zombies (#7090), that is the difference between a run that starts and one that hits a spawn failure (trap T11).

    Slice 2: the Rust. The --lab flag in cli/tri/src/mutate.rs, with lab ids taken from the environment, upload, launch, poll and cleanup. It includes a test that asserts each scenario of lab.t27 row by row; that test is where the two meet. It needs the owner-approved-foreign label.

    Slice 3: pass -j zig_j(nproc, jobs) to each mutant's zig test. This applies to local runs too, not only --lab.

    Separate: tri mutate spec exits 0 with survivors, filed as #7303.

  2. gHashTag commented on Oct 7, 2026

    @gHashTag
    OwnerAuthor

    Slice 3 is up as #7366: the --lab flag, its railway ssh plumbing, and zig's -j for every mutant. It is stacked with slices 1 and 2 and with #7303's two slices, and has auto-merge on.

    Measured on the lab (2026-10-07, after its tini redeploy):

    • Acceptance 1. tri mutate spec --file specs/queen/actors.t27 --fn start_answer --lab prints 4 of 4 killed and exits 0. That was with --lab-wait 0 (exit 5, run left going) followed by a read-back.
    • Acceptance 2. After the runs there are 0 run directories, 0 zig test processes and 0 zombies.
      • Runs live in /tmp, not /data: /data had 1.4G free (97% used), so the check is ls -d /tmp/tri-mutate-*.
    • Acceptance 3. A run cut short by --lab-wait keeps going, and the same command reads it to the end. The directory name is a hash of the request.

    Weak point found and fixed in this slice: the pid model was wrong.

    • Slice 1's probe sampled pids.current too seldom. Sampled every 5 ms, 8 jobs over lab.t27's mutants added 394 pids at zig's default thread count, 110 at -j12, 58 at -j6 and 18 at -j1. Each zig thread is a pid.
    • -j6 took the same wall time as the default (4.74 s against 4.94 and 5.15 s).
    • So tri mutate spec now passes zig_j as -j (or --zig-threads N), and the spec gains job_pids(-j) = -j + 3.
    • PID_RESERVE goes from 104 to 408, which is one unbounded batch: 8 * (48 + 3).
    • The whole-file --lab run of lab.t27 peaked at 60 added pids.

    This issue stays open for the Mac-side run.

  3. gHashTag commented on Oct 7, 2026

    @gHashTag
    OwnerAuthor

    Reopened 2026-10-07: the bulk close of issues without a ## Boundary section was reverted by the owner. Nothing to do here; this comment replaces the earlier close notice.

  4. added
    needs-shapeIssue lacks Boundary/Scenarios/Requirements/Success criteria
    on Oct 7, 2026
  5. added
    needs-boundaryNo '## Boundary' section, so the Queen can reserve nothing for it and no bee can take it
    and removed
    needs-boundaryNo '## Boundary' section, so the Queen can reserve nothing for it and no bee can take it
    on Oct 7, 2026
  6. removed
    needs-shapeIssue lacks Boundary/Scenarios/Requirements/Success criteria
    on Oct 7, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions