Releases: geuben/tdd-cli
Release list
v0.14.1
Fixed
- vitest ids from named vitest projects (#163). Whole-suite collection read
vitest list
text, which prefixes[<project>]to every line of a named vitest project and prints paths
relative to that project'sroot, so it collected ids no run ever reports (and, via the
per-file loop, a second spelling of every test). It now readsvitest list --jsonand roots
each id at the test's own file. tdd appends--jsonitself; a collect command must not. - The vitest override-isolation check sees named vitest projects (#163).
tdd doctor's
check that the default config does not reach an override's files read the same text
listing, so a[<project>]prefix meant it could never match. It now reads
vitest list --json, and a listing that is not JSON fails the check, naming the command. - Adoption never picks another cycle's test (#163). When a cycle's declared test was
missing,tdd advanceadopted any test new since the run started, including an earlier
cycle's target, so a premature advance handed cycle 1's test to cycle 2. Every target in
the run is now excluded, compared by normalised id. tdd targetaccepts the plan's spelling of a test (#163). It matched only the exact
collected id, so it could not name the declared vitest id (src/a.test.ts > math > adds
for a collectedfrontend::src/a.test.ts > math adds). It now qualifies and normalises
its argument as a plan declaration is, and stores the collected id.- An adopted target the run cannot find is refused (#163). A test that collection
listed but no run reported was recorded as the target and came backnot_foundon every
advance, re-adopting a different test each time. The adoption is now evaluated first; on
not_foundthe declared target is kept,adopted_target_not_foundis recorded, and
tdd advanceanswersresolve_blocker.
v0.14.0
Added
tdd run abandon: end a run that will not be finished (#150).tdd run abandon --reason <text>ends the worktree's live or blocked run withoutcome = abandoned;
--run <id>does the same for a run whose worktree is gone. It records the reason and
who did it (account and executor) in a newabandonmenttable, counts as a human
intervention, and releases the worktree's claims.tdd fleetstops listing the run,
tdd metricsreports it with anabandoned: {reason, by}entry, and a new run can
start at the same path. The table is added on open; the ledger schema version is
unchanged.
v0.13.0
Added
- Where a run's time went (#147).
tdd metricsgains a per-runtimeobject:
wall_clock_s,suite_s,suite_shareandby_phase(runs and suite seconds for
every phase recorded). A live run's wall clock runs to now. The friction log gains a
## Timesection with the same split, a per-phase table and a per-cycle table
(wall clock, suite time and suite runs). Nothing new is recorded: these are the
ledger's existing timestamps andinvocation.duration_ms. - Split mode: a ledger the agent's account cannot open (#142). With
/etc/tdd-cli/runner.tomlin place, a separate runner account owns the ledger
in a mode-700 directory. The agent'stddforwards every verb exceptdocs
andinitto it throughsudo, and the runner executes every suite, gate,
hook and git command as the agent. Executor identity is recorded asoperator
when the runner's config assigns the calling account a model, andclaimed
otherwise.tdd runner import <ledger>brings an existing ledger under the
runner, markedpre_split_import, whichtdd metricsreports. Setup and a
verification checklist:tdd docs split. tdd doctorreportsmode(singleorsplit). In split mode it probes,
live and as the agent, that the uid drop works, that the agent cannot read the
ledger, and that it cannot write the install.
Changed
- GREEN runs the target first (#149). An
AWAITING_IMPLadvance whose target
still fails now says so after running only the target. A passing target is followed
by the whole-suite run that decides GREEN, as before.tdd metricscounts
impl_attemptsfrom each advance's target-only run only, and still counts every
AWAITING_IMPLrun in cycles recorded before this change. tdd doctoron a single-user machine no longer lets "ledger outside
worktree" stand for isolation: aledger isolationnotice says the ledger is
owned by the same uid that runs the agent. The README and SECURITY.md say the
same. Single-user installs behave exactly as before.- RED runs only the target test (#148).
AWAITING_TEST,AWAITING_PINand
tdd sensitivity checknow run the target alone: pytest runs the owning suite's
collect command with the node id, and vitest filters to the file and an anchored,
escaped-tname. A failure elsewhere no longer blocks RED. It is caught at
GREEN, which runs the whole suite for every adapter. A collection error in
the target's own file is stillnot_collected. A target adopted in place of the
declared id is run in the same advance. The ledger is now schema 12:
invocation.others_observedis0for a target-only run.
Fixed
-
GREEN ran only the target on cargo, gradle, xctest and exec (#153), so a
regression elsewhere reached the close sweep unseen, and the sweep could skip the
cycle's own project without the whole suite ever running. These adapters now
narrow only when asked for a target-only run. -
The close sweep re-ran a tree that had just passed (#146):
gitutil.tree_hashhashed git
state (index entries plus the unstaged diff), so the same content hashed differently before
and after the GREEN commit, and the §6.1 skip of a cycle's own suites never fired. It now
hashes working-tree content through a throwaway index, independent of what is staged or
committed. A skipped close sweep still runs the cycle project's lint/typecheck gates; before
this fix, the skip would have dropped them too.--reuse-baselinescache entries written by
earlier versions no longer match and are re-probed once.
v0.12.2
Fixed
- A wildcard directory in
test_pathsmatched nothing (#136): a pattern ending in/was
compared as a literal prefix byProject.is_test_fileand joined as a literal path by the cargo
adapter's_test_files, socrates/*/tests/— how a workspace declares one pattern instead of
one per crate — matched no file at all. Every test file in such a project classified as
implementation, so a RED commit touching only tests was recorded asimplementation_during_red,
reporting the agent for writing production code it had not written;_test_filesreturned an
empty list, leaving the unaccounted-files check with nothing to compare. A directory pattern is
now matched per path segment (crates/*/tests/coverscrates/x/tests/main.rs, not
crates/x/src/tests/main.rs), and the adapter globs it.
v0.12.1
Fixed
- cargo: a test target whose name differs from its file's stem (#134): the adapter derived the
target from theRunning tests/<file>.rsheader's path, which is only the target's name under
cargo's default of one binary pertests/*.rs. A crate that setsautotests = falseand declares
its own[[test]]— many files compiled into one binary named something else — was unreachable:
collection ran--test <stem>and cargo answeredno test target named `<stem>` in default-run packages, so every cycle target came backnot_collected, whatever id the plan declared. The
target is now read from the artifact the header names (…/deps/bridge_tests-<hash>), which is
also the only thing distinguishing two crates in a workspace whose test files are both
tests/main.rs. The crate manifests supply the mapping back: target → the file it is built from
fortarget_pathand the collection file set, and the declared spelling for--test, since cargo
writes a target's-as_in the artifact. Manifests are located by walking up from the declared
test_paths, not from_test_files(), which does not expand a wildcard directory pattern such as
crates/*/tests/. Under the default layout target and stem still agree and nothing changes.
v0.12.0
Changed
- Close sweep runs all gates before any suite (#129): lint and typecheck now run for every
project in the sweep set before any project's suite executes. A failing gate short-circuits the
sweep immediately — no suite runs, andnext_actionreturnsfix_regressionwith the failing
gate entry. Previously each project ran its suite first and only then checked its gates. Adapter._gatestops at the first failing command (#129): the first non-zero exit ends the
gate; subsequent commands in the same gate list are not executed.- Gate memoisation (#129): a gate whose project tree hash is unchanged since it last passed
within the same run is skipped (no command executed) and recorded withskipped = 1in
gate_result. A failed gate or a changed tree always re-runs. fix_regressiondetail wording (#129): the reply for a gate failure now reads
"Close sweep stopped before the suite: lint/typecheck gates failed."(was"Close sweep is green but lint/typecheck gates failed.").- Schema v11 (#129):
gate_resultgainstree_hash TEXTand
skipped INTEGER NOT NULL DEFAULT 0. Existing ledgers are migrated on open viaMIGRATIONS[10].
v0.11.0
Added
-
tdd fleetreports baseline-collector and advance-holder liveness (follow-up to #115,
fixes #122):fleet --jsonnow includesstaleandpidon everycollectingrow,
matching the fields already ontdd status/tdd progressresult objects. A new
top-leveladvancinglist exposes in-flighttdd advanceclaims with the same liveness
fields. Human text gains— collector (pid N) is dead/— holder (pid N) is dead
suffixes;no active runsis suppressed when an advance claim is held. -
run.start_sha(schema v10,MIGRATIONS[9]): the HEAD commit at run start is now
recorded on the run row and used as the reference point for late probes and
--accept-failuresgating. -
gitutil.temporary_worktree: context manager that checks out a given sha into a
detached temporary worktree and removes it on exit; used by the late-probe and
--accept-failuresimplementations. -
baseline_late_probe/baseline_late_probe_unobservedintegrity events: emitted
when the close sweep late-probes an un-baselined project atstart_sha(observed) or
cannot observe it (unobserved). -
resumeresultrefused_from_baseline: lists per-project tests that--accept-failures
refused because they pass atstart_sha. -
New
baseline_amendedevent detail shape:{project: {start_sha, accepted: {id: verdict}, refused: {id: verdict}}}with per-test verdicts. -
Friction-log lines for amended baselines: accepted/refused tests appear under Human
interventions; the run header gains a "Late baselines" line separate from "Baseline failures
at start". -
tdd docs [topic]prints the documentation shipped with this version. The wheel now
carriesdocs/harness-integration.md, both example skills, the hooks README, the example
plan, and the README undertddcli/_docs/. An agent meeting the tool in an unfamiliar
project can read the protocol without network access — and without the version hazard of
fetching a skill frommainthat targets a differentverb_set_versionthan the installed
binary emits.tdd docswith no topic returns a machine-readable index;tdd --helpand
tdd initnow point at it. -
artifact_regenerate_failedintegrity event. When aregeneratehook exits non-zero,
anartifact_regenerate_failedevent is emitted on the cycle (or run, forrun start)
with the artifact name, exit code, and last 2000 chars of stderr. It is listed in the
friction log under its cycle via the existing per-cycle event renderer. -
artifact_check.regenerate_failedcolumn (ledger schema v9). A non-zero hook exit
sets this flag on theartifact_checkrow. Existing ledgers are migrated on open via
MIGRATIONS[8].
Fixed
--accept-failuresis now gated onrun.start_sha: only tests that also fail at the
start sha are accepted into the baseline; tests that pass there are refused and listed in
result.refused_from_baseline. This prevents laundering run-introduced regressions.- Un-baselined projects are late-probed at
start_shain the close sweep instead of
accepting HEAD failures verbatim. An observed probe inserts alate_probebaseline row and
subtracts it like a normal baseline; an unobservable probe blocks withresolve_blocker
and no longer recommends--accept-failures. - The
no_baseline_for_projectreply no longer recommends--accept-failuresfor
unobservable projects. - A dead baseline collector no longer bricks the worktree. When a
tdd run start
process is killed during baseline collection, the stalebaseline_claimrow was left
behind.tdd statusandtdd progresscomputedstale: truecorrectly but emitted
await_baselineanyway — a non-terminal verb that instructed autonomous executors to
poll forever. Both commands now emitconfirm_cycle_applicablewithresult.stale == true
andresult.pidwhen the collector pid is dead, and thedetailnames the recovery
action: re-runtdd run start --plan <path>. Thecollecting_baselineresult body always
carriesstaleandpidso callers can inspect liveness without a separatetdd fleet
call. (Fixes #115.) check_artifacts: a failed regenerate hook is now a hard close-sweep failure.
Previously, aregeneratehook that exited non-zero left the artifact path untouched
(tree hash unchanged), so the staleness probe reported "fresh" andtdd advancereplied
complete. The exit code is now captured; a non-zero exit surfacesfix_regressionwith
result.artifact_failureslisting the affected artifact(s) and their stderr.run startrefuses when a regenerate hook fails before the first cycle. The run row
is ended withoutcome = "refused"sotdd statusreports no active run.
v0.10.1
Fixed
- pytest adapter:
--dist loadgroupnode ids. pytest-xdist reports a test marked
xdist_group("<g>")as<nodeid>@<g>and pytest-json-report records that spelling, so a
grouped target wasnot_foundon every suite run even though it was collected and
passed, and itspassed/failedids never matched collected ids. The adapter now strips
the trailing@<group>when reading report and--collect-onlyids. Only an@after
the last]is treated as the suffix, so parametrised ids such as
test_x[user@example.com]are left intact.
v0.10.0
[0.10.0] - 2026-09-05
Added
- cargo adapter — Rust driver through
cargo test. Ids are<target>::<path>
(lib::…for unit tests,<tests-file-stem>::…for integration tests) and compose
to--lib/--test <name>+-- --exact <path>for targeted runs. Verdicts are
parsed from cargo's per-test console lines, attributed to targets by the
Running …headers with stderr merged; doc-tests are excluded. A compile error maps
tonot_collected, matching gradle/xctest. Collection iscargo test --tests -- --listwith a per-file--test <stem>fallback;tdd doctor's collectable gate is
cargo test --no-run. Target lint requires the::separator.
v0.9.0
Added
-
tdd note— an executor-narrative channel.tdd note "<text>"attaches
a phase-stamped note to the current cycle, or a run-level note once the run
has ended (it falls back to the latest run, so no active run is required).
Cycle notes render in the friction log as blockquote claims under their cycle;
run-level notes render under a new## Executor narrativesection, which is
omitted when there are none. Envelopes that carry an integrity event now nudge
for a note while the reason is fresh (silenced once the cycle has one), and
the terminalCOMPLETEenvelope — viaadvanceor a final-cycle skip — asks
for a closing narrative before rendering. Stored in a newnotetable via a
v7→v8 ledger migration (#77, #94). -
Baseline sanity gate.
run startrefuses a baseline whose failing ratio
exceeds a threshold (default 0.5, suites under 10 collected tests exempt) with
reason: "baseline_implausible", on the grounds that a mostly-red suite means
the environment is broken rather than the code. A per-project
baseline_max_failure_ratiointdd.tomloverrides the default, and
--accept-baselinebypasses the gate and logs abaseline_acceptedintegrity
event (#67, #84). -
Standing-failure delta. A non-empty baseline is now compared against the
previous run's baseline for the same worktree and abaseline_standing_delta
event partitions the failing set into new versus inherited failures, so a
pre-existing red test is distinguishable from one that broke since the last
run (#67, #84). -
Per-project
health_command. An optional command intdd.tomlthat is
probed before baseline capture; if it fails,run startrefuses with
reason: "services_unreachable"instead of recording a baseline against a
down dependency (#67, #84). -
Declared-target lint.
plan registerandrun startnow lint every
declared target against the adapter's id grammar (pytest::, vitest>,
gradleClass/method, xctestBundle/Class/testMethod) and against project
roots: a target path that duplicates a non-.project root prefix is refused
unless the nested path genuinely exists in the worktree. Findings are returned
underreason: "target_lint";run startre-lints the stored contract
against the current config before claiming a baseline (#71, #88). -
Per-adapter sensitivity evidence line. Each adapter now extracts a
target_evidenceline from a failing target — the firstEline for pytest
(skipping the xdist worker header), the first: error:line in the test's
window for xctest,failureMessages[0]for vitest, the junit failure message
for gradle, and the last non-empty output line for exec. The sensitivity check
persists it assensitivity_check.evidence_line(v6→v7 migration) and the
friction log's observed snippet prefers it over the raw first line, rendering
<no assertion line captured>when empty and keeping the tail of over-long
lines. Legacy rows with no stored evidence keep the first-line fallback (#68,
#90). -
Diagnosable executor attribution.
TDD_EXECUTOR_MODELlets a harness
declare the executor identity (source: declared), taking precedence over
transcript detection. When identity cannot be resolved,Executor.reason
says why — session id unset, transcript not found, or transcript without a
model record — andrun startlogs anexecutor_unknownevent and surfaces
executor_warningin its envelope.tdd doctorgains an informational
executor-identity check (#74, #92).
Changed
-
Target adoption is evaluated in the same
advance. When a cycle's
declared target is missing and exactly one new test appears,advance
adopts it (loggingdeclared_test_mismatch) and judges RED — or drives the
sensitivity check when it passed — from the suite run that already happened,
instead of asking for a re-run. A declared id that differs
from a single same-file candidate only by separator normalisation is
disambiguated and adopted without asking; two same-file candidates still
require an explicittdd target(#72, #91). -
CI now tests on Python 3.11 and 3.14 only (#87).