Skip to content

teamwright 0.11.0

Choose a tag to compare

@smykov smykov released this 24 Oct 21:18
· 13 commits to main since this release
e330c0d

Added

  • scripts/tw-gate <area> [--fresh], installed into projects: the full gate once per state of the code. In an autonomous run a small task took about 90 minutes, and its ten-minute area gate ran five times on the same code — twice by the developer (it cut the output with tail -8 and had to run it again to see the result), then by the flow's Gate step, the code-reviewer and the task-validator. tw-gate runs what the role files name as the gate (the area's generate_command, then its gate_command through scripts/tw-run, or the top-level gate_command as written), writes the whole output to .teamwright/logs/gate/<area>-<time>-<commit>.log ($TEAMWRIGHT_LOG_DIR respected), records the run in <area>.json and runs.jsonl (area, head, clean, tree, exit, duration, times, log, command), and prints PASS/FAIL, the exit code, the duration, the last 30 lines of a failure and the log path; its exit code is the gate's. A PASS is reused when the working tree has the same content (a git tree id of HEAD plus uncommitted and untracked changes, ignored files left out), root and command, and the tree did not change during the run: PASS (cached: same commit <sha>, <when>, log <path>). So the developer's PASS before the commit answers the flow, the reviewer and the validator once that content is committed. A FAIL is never reused; --fresh always runs. Tests: tests/test-tw-gate.sh (also under bash 3.2).

  • scripts/tw-mutants <area> <ID> --tree <copy>, installed into projects: the task's mutant catalog, in one runner start. Mutants are kept in the repository as docs/tasks/<ID>.mutants/: one <NN>-<slug>.mutant file each — guarantee: (the Acceptance/DoD line it breaks, - for none), file:, added_by:, optional test: and equivalent:, and the exact replacement between --- old / --- new / --- end — plus test-command, the command that runs only the task's tests. tw-mutants refuses the working tree, checks every entry against the throwaway copy (old text exactly once), and runs a baseline and every mutant in one scripts/tw-run <area> start with the copy as the project root (for runner: docker, the copy is what the container mounts). One line per mutant (killed, SURVIVED … breaks: <guarantee>, survived, follow-up, equivalent), a report in .teamwright/logs/mutants/; exit 0 when no survivor breaks a guarantee, 1 when one does, 2 for a stale or broken entry, 3 for a red baseline. Tests: tests/test-tw-mutants.sh (with a fake docker that counts starts and checks the mount).

  • tw-gate and tw-mutants outlive Claude Code's 10-minute Bash limit. In an autonomous run the gate was cut by the limit and started again; agents also killed the test container to restart, or polled the log in until grep …; sleep loops. Both scripts now start the real run detached — its own session and process group (scripts/hooks/_job.sh, Python's start_new_session, bash 3.2 and macOS included), output to the log, a job directory keyed like the cache under .teamwright/logs/{gate,mutants}/jobs/ — and wait in the foreground up to TW_WAIT_SECONDS (default 540). Done in time: the normal result and exit code. Not yet: RUNNING: gate <area> (pid N), started <when>, log <path> - run the same command again to keep waiting, exit 75. The same command again attaches to that run instead of starting another; a result that finished while nobody waited is printed by the next same call; a run of the same area (or task) on other content is waited for first; --fresh attaches to a running run rather than starting a second; a run killed without a result (pid gone) is reported, cleaned up and started again; a killed caller leaves the run going; scripts/tw-gate --stop <area> ends one. Role files, AGENTS.md and the flow: never wrap them in sleep loops or background calls — on RUNNING call the same command again; other long test commands run once through scripts/tw-run with output to a file. Tests: fake slow gates and catalogs with tiny wait budgets (attach, uncollected FAIL, --fresh, killed run, other content, killed caller, --stop).

  • scripts/tw-gate --for-task <ID> / --for-diff <base> and --top: gate the areas a task changed. In one project the validator ran the server suite for a CI-only change. The task's changed files (its own commits — on a branch, those since it left the default branch; everywhere, commits whose subject names the id first, so a commit that only mentions the task in its body does not count — plus uncommitted and untracked) map to the areas whose source_dirs, test_dirs or workdir hold them; a file outside every area means the top-level gate (--top, every area when there is none), except records, docs and Markdown, CI configuration and tool settings, which need no local gate ($TEAMWRIGHT_NO_GATE_RE). The flow's Gate step, the code-reviewer and the task-validator run exactly the listed gates.

  • Throwaway copies get generated code and a warm build cache. In one project every clean copy for mutants ran build_runner for ~5 minutes. New area keys: generated: [paths] (the outputs of generate_command) and optional generate_inputs: [paths] — tw-mutants copies the outputs from the working tree when the copy's inputs are unchanged, and otherwise runs generate_command in the copy inside the same runner start (exit 4 when it fails); copy_env: {VAR: value} — applied by scripts/tw-run under TW_RUN_COPY=1, {cache} = .teamwright/cache/<area>/copy of the main checkout (/tw-copy-cache in docker), e.g. CARGO_TARGET_DIR: "{cache}/target", so a copy neither builds from cold nor tramples the working tree's target dir; TW_CACHE_ROOT makes a copy's docker runs reuse the main checkout's package caches. scripts/tw-mutants <area> --tree <copy> --prepare does only this step for tests run by hand. The installer validates the keys. Tests in test-tw-mutants.sh and test-installer.sh.

  • scripts/tw-test <area> [tests ...] and the test guard: tests only through the wrapper. In one project's autonomous run the test-writer, after fixing one test, ran the same ~4-minute suite four times in a row: twice with the output cut by | grep … | tail -15 (the failures were lost), once with -p no:logging, which removed the caplog fixture and produced 25 false errors, once right — ~12 minutes, and with one worker instead of the configured PYTEST_WORKERS; the developer ran the gate script directly twice. The role files already said "never cut output"; agents ignored it, so the tooling enforces it. scripts/tw-test runs the area's new test_select_command ({tests} = the selectors, shell-quoted; workers stay in the config string, e.g. pytest -q -n ${PYTEST_WORKERS:-4} {tests}), else test_command with the selectors appended, through scripts/tw-run; the whole output goes to .teamwright/logs/test/, the screen gets PASS/FAIL, exit code, duration, the counts (pytest, cargo test, go test, flutter/dart test, jest, vitest), the failed tests with their first error line (at most 30) or the last 15 lines, and the log path. A repeat on the same area files (tw-gate's area-scoped key), command and selectors answers from the cache — a FAIL too: the answer to a failure is its log; --fresh reruns. Detached like tw-gate (RUNNING, exit 75, the same call attaches); --tree <copy> runs in a throwaway copy. The new PreToolUse hook scripts/hooks/test-guard.sh (Bash) refuses test runners and gates called directly — built-in patterns (pytest, python -m pytest, make test|check, flutter|dart|go|cargo test, npm|pnpm|yarn test, vitest, jest, phpunit, gradlew test, mvn test, …, also behind uv run/npx, sh -c, docker run and inside scripts/tw-run), every area's gate_command/test_command/test_select_command and the top-level gate_command (e.g. a project's tool/gate.sh), and the new top-level test_runner_patterns — and test or gate output piped into grep/tail/head/sed/awk, wrappers included; the message names the command to run instead (scripts/tw-test server tests/pipeline, scripts/tw-gate server) and says to read the log. It has its own switch, because gate-first only warns: process.test_guard: block | warn | off (default block, also in gate-first), TW_TEST_GUARD in the environment Claude Code starts with wins; the area's lint, codegen, API and UI test commands through scripts/tw-run stay allowed. The installer validates the new keys. Roles: developer, test-writer, code-reviewer and task-validator, AGENTS.md, gates.md (new section Test guard), tools.md, plugin.md, the settings, adopt and init skills and the README carry one rule — tests only via tw-test, gates via tw-gate, mutations via tw-mutants; the test-writer's wrong implementations and the validator's hand-run tests in a copy use tw-test --tree. Tests: tests/test-tw-test.sh, tests/test-test-guard.sh, tests/test-installer.sh. A config command whose head is a script that only wraps another one (tool/gate.sh sh -c '…') is not itself a signature — its inner command is checked, so lint, formatting and migrations through the same wrapper still run.

  • pre-push warns about a CI-skip marker on the pushed tip ([skip ci], [ci skip], [no ci], [skip actions], ***NO_CI***, skip-checks: true): CI decides by the tip's message, so the code under it and a pull request opened from it go unchecked — one project lost ~8 minutes on it. A warning, not a refusal. AGENTS.md, the flow's push step and docs/usage.md now say: never [skip ci] in a commit message, least of all on the last commit before a push; record-only pushes are skipped by the CI configuration (paths-ignore, scripts/tw-ci-changes).

Changed

  • process.batch.parallel up to 10 (was 1–4): an owner running more parallel worktree sessions (and per-worktree tool slots, e.g. Serena) on one project hit the cap. Default stays 1 (off).
  • Roles run the gate through tw-gate. The developer runs only the task's tests and the touched modules while working ({{TEST_COMMAND}} <files>, now in its parameters), and the full gate once before handing over, never cutting its output; the flow's Gate step, the code-reviewer and the task-validator run scripts/tw-gate <area> and accept a cached PASS on the same commit, still comparing git status before and after; their judgement stays independent. AGENTS.md, the role files, the flow, parallel and init skills, gates.md (new section Gate runs), lifecycle.md, sessions.md, tools.md, plugin.md, usage.md, design-decisions.md and the README describe it. Projects pick it up after /plugin update and tw-install.py apply.
  • A long gate is never run in the background. The developer gives it a long timeout instead: in a run without an owner the session ends with the turn, and a background gate dies with it (the same autonomous run: a background role ended the session at ANALYSIS).
  • Mutation checks: mutants on the diff are review evidence, not a gate on every line (generalized from a project's decision after one task failed acceptance four times, each on a new gap in one CI step, and one sweep ran ~109 mutants × 40 s). Following Google's practice — mutants on changed lines shown in code review as hints (Petrović, Ivanković, "State of Mutation Testing at Google", ICSE-SEIP 2018; Petrović, Ivanković, Fraser, Just, "Practical Mutation Testing at Scale: A view from Google", IEEE TSE 2021, arXiv 2102.11378) — the default for every project is now: (1) acceptance FAILs only on a survivor that breaks a stated Acceptance/DoD guarantee, named by the mutant; other survivors are follow-ups (the flow files them in the sprint's follow-up task); (2) a sample round adds 15–30 mutants aimed at the guarantees and the changed lines (was 3–6), a sweep — and the test-writer's checker: true sweep, now in its second spawn — covers the task's changed lines, not the whole seam; a whole-seam sweep only for a task marked critical: true (new task-template field: sync, recovery, data deletion); (3) the catalog docs/tasks/<ID>.mutants/ is re-run first every round and only added to; (4) after two sweep rounds with FAIL the next verdict is NEEDS_OWNER, not a third sweep — the verify gate now refuses anything else (counted since the last PASS or NEEDS_OWNER; tests in tests/test-verify-gate.sh), and the router says so (tests/test-tw-next.sh); (5) machine mutation tools (mutmut, PIT, Stryker) belong in an optional nightly CI job, off the acceptance path. test-writer, task-validator and code-reviewer role files, the task and validation templates, PRINCIPLES.md P-005, AGENTS.md, the flow skill, gates.md (new section Mutation checks), lifecycle.md, roles.md, sessions.md, tools.md, plugin.md and the README describe it.
  • Throwaway copies use mktemp -d, never a hardcoded /tmp. The test-writer's and the validator's copy snippets say why: when an area's tests run in containers started by the host's Docker (runner: docker, or a project wrapper), the copy must be on a path the host can mount, and mktemp -d honours $TMPDIR; a /tmp inside the agent's container is invisible to the host (an autonomous run lost ~6 minutes on a test container that got an empty directory from mktemp -d /tmp/...).
  • The gate cache keys on the area's files, not the whole tree. In one project a task's gate ran 5 times, 3 of them after docs-only changes that invalidated the cache (10–14 minutes each). The key is now the working tree restricted to the area's scope: source_dirs, test_dirs, workdir, the dependency manifests and lockfiles at the root and the workdir, .teamwright/config.yml, the gate scripts and every repository file the area's commands name; a commit outside it answers PASS (cached: <area> files unchanged since the run at commit <sha>, …). An area without source_dirs/test_dirs, an area running the top-level gate, and --top keep the whole-tree key. Records gain key, key_after and scope.
  • test-writer rules from the field. (a) Every failure branch gets a mandatory "swallowed error / skipped failure path" mutant in the catalog — in one project a swallowed ? was caught only by the validator, after deploy; (b) wrong implementations are minimal sketches of the one wrong behaviour, never a full or reference implementation — the same project's test-writer wrote 23 of them for one task, work the developer then redid; (c) before handing over it runs the area's lint_command and formatter on the files it wrote. Its copy snippets keep the copy when tw-mutants answers 75.
  • test-writer: one toolchain start for all mutants. The bite check, the checker: true sweep and the failure-branch mutations run as one loop — apply a mutant, run only the task's tests, revert — in one scripts/tw-run <area> call, never the full suite per mutant. On a docker area each start loads the image and its models; per-mutant starts were most of a 22-minute first spawn.

Fixed

  • scripts/hooks/_job.sh is committed executable. It shipped as 644 while install-hooks.sh makes scripts/hooks/*.sh executable, so every project's working tree showed a mode change that agents could commit. The kit now commits it as 755, so the installer (which copies the kit's mode) and install-hooks.sh agree; tests/test-installer.sh checks that no shipped script is committed without its executable bit.

Full changelog: CHANGELOG.md