tdd-cli 0.13.0__tar.gz → 0.14.1__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {tdd_cli-0.13.0 → tdd_cli-0.14.1}/CHANGELOG.md +40 -0
- {tdd_cli-0.13.0 → tdd_cli-0.14.1}/PKG-INFO +27 -2
- {tdd_cli-0.13.0 → tdd_cli-0.14.1}/README.md +26 -1
- {tdd_cli-0.13.0 → tdd_cli-0.14.1}/docs/PRD.md +22 -3
- {tdd_cli-0.13.0 → tdd_cli-0.14.1}/docs/harness-integration.md +2 -2
- {tdd_cli-0.13.0 → tdd_cli-0.14.1}/src/tddcli/__init__.py +1 -1
- {tdd_cli-0.13.0 → tdd_cli-0.14.1}/src/tddcli/adapters/vitest_adapter.py +52 -28
- {tdd_cli-0.13.0 → tdd_cli-0.14.1}/src/tddcli/advance.py +94 -69
- {tdd_cli-0.13.0 → tdd_cli-0.14.1}/src/tddcli/cli.py +93 -10
- {tdd_cli-0.13.0 → tdd_cli-0.14.1}/src/tddcli/ledger.py +10 -0
- {tdd_cli-0.13.0 → tdd_cli-0.14.1}/src/tddcli/render.py +10 -0
- {tdd_cli-0.13.0 → tdd_cli-0.14.1}/tests/conftest.py +33 -0
- tdd_cli-0.14.1/tests/fake_vitest.py +121 -0
- tdd_cli-0.14.1/tests/test_advance_adoption.py +278 -0
- {tdd_cli-0.13.0 → tdd_cli-0.14.1}/tests/test_batch_collection.py +35 -10
- tdd_cli-0.14.1/tests/test_run_abandon.py +256 -0
- {tdd_cli-0.13.0 → tdd_cli-0.14.1}/tests/test_suite_overrides.py +42 -2
- {tdd_cli-0.13.0 → tdd_cli-0.14.1}/tests/test_target_validation.py +33 -0
- {tdd_cli-0.13.0 → tdd_cli-0.14.1}/tests/test_vitest_adapter.py +66 -0
- tdd_cli-0.13.0/tests/test_advance_adoption.py +0 -129
- {tdd_cli-0.13.0 → tdd_cli-0.14.1}/.gitignore +0 -0
- {tdd_cli-0.13.0 → tdd_cli-0.14.1}/LICENSE +0 -0
- {tdd_cli-0.13.0 → tdd_cli-0.14.1}/SECURITY.md +0 -0
- {tdd_cli-0.13.0 → tdd_cli-0.14.1}/docs/split-runner.md +0 -0
- {tdd_cli-0.13.0 → tdd_cli-0.14.1}/examples/claude-code-hooks/README.md +0 -0
- {tdd_cli-0.13.0 → tdd_cli-0.14.1}/examples/claude-code-hooks/bash_hook.py +0 -0
- {tdd_cli-0.13.0 → tdd_cli-0.14.1}/examples/claude-code-hooks/stop_hook.py +0 -0
- {tdd_cli-0.13.0 → tdd_cli-0.14.1}/examples/plan.md +0 -0
- {tdd_cli-0.13.0 → tdd_cli-0.14.1}/examples/skills/tdd-drive/README.md +0 -0
- {tdd_cli-0.13.0 → tdd_cli-0.14.1}/examples/skills/tdd-drive/SKILL.md +0 -0
- {tdd_cli-0.13.0 → tdd_cli-0.14.1}/examples/skills/tdd-handoff/README.md +0 -0
- {tdd_cli-0.13.0 → tdd_cli-0.14.1}/examples/skills/tdd-handoff/SKILL.md +0 -0
- {tdd_cli-0.13.0 → tdd_cli-0.14.1}/pyproject.toml +0 -0
- {tdd_cli-0.13.0 → tdd_cli-0.14.1}/src/tddcli/actor.py +0 -0
- {tdd_cli-0.13.0 → tdd_cli-0.14.1}/src/tddcli/adapters/__init__.py +0 -0
- {tdd_cli-0.13.0 → tdd_cli-0.14.1}/src/tddcli/adapters/base.py +0 -0
- {tdd_cli-0.13.0 → tdd_cli-0.14.1}/src/tddcli/adapters/cargo_adapter.py +0 -0
- {tdd_cli-0.13.0 → tdd_cli-0.14.1}/src/tddcli/adapters/exec_adapter.py +0 -0
- {tdd_cli-0.13.0 → tdd_cli-0.14.1}/src/tddcli/adapters/gradle_adapter.py +0 -0
- {tdd_cli-0.13.0 → tdd_cli-0.14.1}/src/tddcli/adapters/pytest_adapter.py +0 -0
- {tdd_cli-0.13.0 → tdd_cli-0.14.1}/src/tddcli/adapters/xctest_adapter.py +0 -0
- {tdd_cli-0.13.0 → tdd_cli-0.14.1}/src/tddcli/agentfs.py +0 -0
- {tdd_cli-0.13.0 → tdd_cli-0.14.1}/src/tddcli/config.py +0 -0
- {tdd_cli-0.13.0 → tdd_cli-0.14.1}/src/tddcli/contract.py +0 -0
- {tdd_cli-0.13.0 → tdd_cli-0.14.1}/src/tddcli/docs.py +0 -0
- {tdd_cli-0.13.0 → tdd_cli-0.14.1}/src/tddcli/envelope.py +0 -0
- {tdd_cli-0.13.0 → tdd_cli-0.14.1}/src/tddcli/fleet.py +0 -0
- {tdd_cli-0.13.0 → tdd_cli-0.14.1}/src/tddcli/gitutil.py +0 -0
- {tdd_cli-0.13.0 → tdd_cli-0.14.1}/src/tddcli/identity.py +0 -0
- {tdd_cli-0.13.0 → tdd_cli-0.14.1}/src/tddcli/leases.py +0 -0
- {tdd_cli-0.13.0 → tdd_cli-0.14.1}/src/tddcli/machine.py +0 -0
- {tdd_cli-0.13.0 → tdd_cli-0.14.1}/src/tddcli/plan_paths.py +0 -0
- {tdd_cli-0.13.0 → tdd_cli-0.14.1}/src/tddcli/runner.py +0 -0
- {tdd_cli-0.13.0 → tdd_cli-0.14.1}/src/tddcli/snapshot.py +0 -0
- {tdd_cli-0.13.0 → tdd_cli-0.14.1}/src/tddcli/staging.py +0 -0
- {tdd_cli-0.13.0 → tdd_cli-0.14.1}/src/tddcli/target_lint.py +0 -0
- {tdd_cli-0.13.0 → tdd_cli-0.14.1}/tests/test_actor_seams.py +0 -0
- {tdd_cli-0.13.0 → tdd_cli-0.14.1}/tests/test_ancillary_files.py +0 -0
- {tdd_cli-0.13.0 → tdd_cli-0.14.1}/tests/test_artifact_regeneration.py +0 -0
- {tdd_cli-0.13.0 → tdd_cli-0.14.1}/tests/test_baseline_integrity.py +0 -0
- {tdd_cli-0.13.0 → tdd_cli-0.14.1}/tests/test_baseline_sanity.py +0 -0
- {tdd_cli-0.13.0 → tdd_cli-0.14.1}/tests/test_cargo_adapter.py +0 -0
- {tdd_cli-0.13.0 → tdd_cli-0.14.1}/tests/test_close_sweep_gates.py +0 -0
- {tdd_cli-0.13.0 → tdd_cli-0.14.1}/tests/test_concurrent_advance.py +0 -0
- {tdd_cli-0.13.0 → tdd_cli-0.14.1}/tests/test_config_and_staging.py +0 -0
- {tdd_cli-0.13.0 → tdd_cli-0.14.1}/tests/test_config_drift.py +0 -0
- {tdd_cli-0.13.0 → tdd_cli-0.14.1}/tests/test_contract.py +0 -0
- {tdd_cli-0.13.0 → tdd_cli-0.14.1}/tests/test_docs_command.py +0 -0
- {tdd_cli-0.13.0 → tdd_cli-0.14.1}/tests/test_doctor_attribution.py +0 -0
- {tdd_cli-0.13.0 → tdd_cli-0.14.1}/tests/test_doctor_blockers.py +0 -0
- {tdd_cli-0.13.0 → tdd_cli-0.14.1}/tests/test_end_to_end.py +0 -0
- {tdd_cli-0.13.0 → tdd_cli-0.14.1}/tests/test_evidence_extraction.py +0 -0
- {tdd_cli-0.13.0 → tdd_cli-0.14.1}/tests/test_example_plan.py +0 -0
- {tdd_cli-0.13.0 → tdd_cli-0.14.1}/tests/test_exec_adapter.py +0 -0
- {tdd_cli-0.13.0 → tdd_cli-0.14.1}/tests/test_executor_attribution.py +0 -0
- {tdd_cli-0.13.0 → tdd_cli-0.14.1}/tests/test_executor_notes.py +0 -0
- {tdd_cli-0.13.0 → tdd_cli-0.14.1}/tests/test_failure_clipping.py +0 -0
- {tdd_cli-0.13.0 → tdd_cli-0.14.1}/tests/test_fleet.py +0 -0
- {tdd_cli-0.13.0 → tdd_cli-0.14.1}/tests/test_gradle_adapter.py +0 -0
- {tdd_cli-0.13.0 → tdd_cli-0.14.1}/tests/test_heartbeat.py +0 -0
- {tdd_cli-0.13.0 → tdd_cli-0.14.1}/tests/test_id_normalisation.py +0 -0
- {tdd_cli-0.13.0 → tdd_cli-0.14.1}/tests/test_init_detection.py +0 -0
- {tdd_cli-0.13.0 → tdd_cli-0.14.1}/tests/test_late_baseline.py +0 -0
- {tdd_cli-0.13.0 → tdd_cli-0.14.1}/tests/test_named_leases_and_timeout.py +0 -0
- {tdd_cli-0.13.0 → tdd_cli-0.14.1}/tests/test_pin_cycles.py +0 -0
- {tdd_cli-0.13.0 → tdd_cli-0.14.1}/tests/test_plan_paths.py +0 -0
- {tdd_cli-0.13.0 → tdd_cli-0.14.1}/tests/test_progress.py +0 -0
- {tdd_cli-0.13.0 → tdd_cli-0.14.1}/tests/test_project_commands.py +0 -0
- {tdd_cli-0.13.0 → tdd_cli-0.14.1}/tests/test_project_env.py +0 -0
- {tdd_cli-0.13.0 → tdd_cli-0.14.1}/tests/test_pytest_xdist_group.py +0 -0
- {tdd_cli-0.13.0 → tdd_cli-0.14.1}/tests/test_python_env_managers.py +0 -0
- {tdd_cli-0.13.0 → tdd_cli-0.14.1}/tests/test_refactor_cycles.py +0 -0
- {tdd_cli-0.13.0 → tdd_cli-0.14.1}/tests/test_release_surface.py +0 -0
- {tdd_cli-0.13.0 → tdd_cli-0.14.1}/tests/test_run_claim.py +0 -0
- {tdd_cli-0.13.0 → tdd_cli-0.14.1}/tests/test_runner_import.py +0 -0
- {tdd_cli-0.13.0 → tdd_cli-0.14.1}/tests/test_sensitivity_evidence.py +0 -0
- {tdd_cli-0.13.0 → tdd_cli-0.14.1}/tests/test_single_project_repo.py +0 -0
- {tdd_cli-0.13.0 → tdd_cli-0.14.1}/tests/test_snapshot_and_identity.py +0 -0
- {tdd_cli-0.13.0 → tdd_cli-0.14.1}/tests/test_split_client.py +0 -0
- {tdd_cli-0.13.0 → tdd_cli-0.14.1}/tests/test_split_config.py +0 -0
- {tdd_cli-0.13.0 → tdd_cli-0.14.1}/tests/test_split_doctor.py +0 -0
- {tdd_cli-0.13.0 → tdd_cli-0.14.1}/tests/test_split_identity.py +0 -0
- {tdd_cli-0.13.0 → tdd_cli-0.14.1}/tests/test_split_ledger.py +0 -0
- {tdd_cli-0.13.0 → tdd_cli-0.14.1}/tests/test_split_runner.py +0 -0
- {tdd_cli-0.13.0 → tdd_cli-0.14.1}/tests/test_stub_hint.py +0 -0
- {tdd_cli-0.13.0 → tdd_cli-0.14.1}/tests/test_sudo_actor.py +0 -0
- {tdd_cli-0.13.0 → tdd_cli-0.14.1}/tests/test_suite_scope.py +0 -0
- {tdd_cli-0.13.0 → tdd_cli-0.14.1}/tests/test_target_lint.py +0 -0
- {tdd_cli-0.13.0 → tdd_cli-0.14.1}/tests/test_target_only_runs.py +0 -0
- {tdd_cli-0.13.0 → tdd_cli-0.14.1}/tests/test_time_report.py +0 -0
- {tdd_cli-0.13.0 → tdd_cli-0.14.1}/tests/test_timing_visibility.py +0 -0
- {tdd_cli-0.13.0 → tdd_cli-0.14.1}/tests/test_tree_hash.py +0 -0
- {tdd_cli-0.13.0 → tdd_cli-0.14.1}/tests/test_undeclared_close_gate.py +0 -0
- {tdd_cli-0.13.0 → tdd_cli-0.14.1}/tests/test_undeclared_dedup.py +0 -0
- {tdd_cli-0.13.0 → tdd_cli-0.14.1}/tests/test_worker_leases.py +0 -0
- {tdd_cli-0.13.0 → tdd_cli-0.14.1}/tests/test_xctest_adapter.py +0 -0
|
@@ -6,6 +6,46 @@ and the project adheres to [Semantic Versioning](https://semver.org/).
|
|
|
6
6
|
|
|
7
7
|
## [Unreleased]
|
|
8
8
|
|
|
9
|
+
## [0.14.1] - 2026-09-25
|
|
10
|
+
|
|
11
|
+
### Fixed
|
|
12
|
+
|
|
13
|
+
- **vitest ids from named vitest projects** (#163). Whole-suite collection read `vitest list`
|
|
14
|
+
text, which prefixes `[<project>] ` to every line of a named vitest project and prints paths
|
|
15
|
+
relative to that project's `root`, so it collected ids no run ever reports (and, via the
|
|
16
|
+
per-file loop, a second spelling of every test). It now reads `vitest list --json` and roots
|
|
17
|
+
each id at the test's own file. tdd appends `--json` itself; a collect command must not.
|
|
18
|
+
- **The vitest override-isolation check sees named vitest projects** (#163). `tdd doctor`'s
|
|
19
|
+
check that the default config does not reach an override's files read the same text
|
|
20
|
+
listing, so a `[<project>] ` prefix meant it could never match. It now reads
|
|
21
|
+
`vitest list --json`, and a listing that is not JSON fails the check, naming the command.
|
|
22
|
+
- **Adoption never picks another cycle's test** (#163). When a cycle's declared test was
|
|
23
|
+
missing, `tdd advance` adopted any test new since the run started, including an earlier
|
|
24
|
+
cycle's target, so a premature advance handed cycle 1's test to cycle 2. Every target in
|
|
25
|
+
the run is now excluded, compared by normalised id.
|
|
26
|
+
- **`tdd target` accepts the plan's spelling of a test** (#163). It matched only the exact
|
|
27
|
+
collected id, so it could not name the declared vitest id (`src/a.test.ts > math > adds`
|
|
28
|
+
for a collected `frontend::src/a.test.ts > math adds`). It now qualifies and normalises
|
|
29
|
+
its argument as a plan declaration is, and stores the collected id.
|
|
30
|
+
- **An adopted target the run cannot find is refused** (#163). A test that collection
|
|
31
|
+
listed but no run reported was recorded as the target and came back `not_found` on every
|
|
32
|
+
advance, re-adopting a different test each time. The adoption is now evaluated first; on
|
|
33
|
+
`not_found` the declared target is kept, `adopted_target_not_found` is recorded, and
|
|
34
|
+
`tdd advance` answers `resolve_blocker`.
|
|
35
|
+
|
|
36
|
+
## [0.14.0] - 2026-09-24
|
|
37
|
+
|
|
38
|
+
### Added
|
|
39
|
+
|
|
40
|
+
- **`tdd run abandon`: end a run that will not be finished** (#150). `tdd run abandon
|
|
41
|
+
--reason <text>` ends the worktree's live or blocked run with `outcome = abandoned`;
|
|
42
|
+
`--run <id>` does the same for a run whose worktree is gone. It records the reason and
|
|
43
|
+
who did it (account and executor) in a new `abandonment` table, counts as a human
|
|
44
|
+
intervention, and releases the worktree's claims. `tdd fleet` stops listing the run,
|
|
45
|
+
`tdd metrics` reports it with an `abandoned: {reason, by}` entry, and a new run can
|
|
46
|
+
start at the same path. The table is added on open; the ledger schema version is
|
|
47
|
+
unchanged.
|
|
48
|
+
|
|
9
49
|
## [0.13.0] - 2026-09-23
|
|
10
50
|
|
|
11
51
|
### Added
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: tdd-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.14.1
|
|
4
4
|
Summary: Ledger-backed TDD process controller for autonomous coding agents
|
|
5
5
|
Project-URL: Homepage, https://github.com/geuben/tdd-cli
|
|
6
6
|
Project-URL: Repository, https://github.com/geuben/tdd-cli
|
|
@@ -170,7 +170,11 @@ being repeated in `test_paths`. `env` values may reference `${VAR}`, expanded fr
|
|
|
170
170
|
environment at invocation. For pytest, `collect_command` is optional (`--collect-only`
|
|
171
171
|
composes with the run command); a vitest override must declare one — `vitest list` knows
|
|
172
172
|
nothing of the override config, and the mismatch is refused at `tdd run start`, not
|
|
173
|
-
mid-cycle.
|
|
173
|
+
mid-cycle. For the whole-suite listing tdd appends `--json` to each vitest collect command
|
|
174
|
+
(the default and every override's) and reads each test's file from vitest's JSON, so a
|
|
175
|
+
collect command must not carry `--json` itself: `vitest list --json --json` prints nothing.
|
|
176
|
+
The override-isolation check in `tdd doctor` reads the same JSON listing, and a listing it
|
|
177
|
+
cannot read fails the check, naming the command it ran.
|
|
174
178
|
|
|
175
179
|
## Sharing cores between concurrent agents
|
|
176
180
|
|
|
@@ -296,6 +300,26 @@ When the last declared cycle closes, the tool gathers every path that appeared i
|
|
|
296
300
|
emitted (visible in `tdd metrics` and the friction log) and the run completes. The event
|
|
297
301
|
makes a silent drop visible without blocking, since the file is already gone.
|
|
298
302
|
|
|
303
|
+
### Abandoning a run
|
|
304
|
+
|
|
305
|
+
A run that will not be finished — stopped on purpose, or blocked for good — is ended with
|
|
306
|
+
`tdd run abandon --reason "<why>"` rather than `resume --unblock`. It takes the worktree's
|
|
307
|
+
live run, else its most recent blocked run. It sets the run's `outcome` to `abandoned`,
|
|
308
|
+
records the reason and who abandoned it (the account, and the executor identity as `run
|
|
309
|
+
start` resolves it; on a split runner the account is the calling agent's `SUDO_USER`),
|
|
310
|
+
records a `human_intervention`, and releases the worktree's baseline and advance claims.
|
|
311
|
+
`tdd fleet` stops listing the run, `tdd metrics` reports it as `abandoned` with an
|
|
312
|
+
`abandoned: {reason, by}` entry, and a new run can start at the same worktree path.
|
|
313
|
+
|
|
314
|
+
When the worktree is gone, abandon by id from any worktree of the repo: `tdd run abandon
|
|
315
|
+
--run <id> --reason "<why>"`. `--run` is accepted only for a run whose worktree no longer
|
|
316
|
+
exists or is the caller's own, so one agent cannot end another's live run.
|
|
317
|
+
|
|
318
|
+
Refusals, under `result.reason`, write nothing: `reason_required` (blank reason),
|
|
319
|
+
`no_run` (no live or blocked run here), `run_not_found`, `run_ended` (already complete or
|
|
320
|
+
abandoned), `worktree_exists` (`--run` names another worktree that still exists), and
|
|
321
|
+
`advance_in_flight` (a live `tdd advance` holds the worktree; a stale claim is released).
|
|
322
|
+
|
|
299
323
|
The gate composes with `ancillary_files`: declared paths never fire `undeclared_file_touched`,
|
|
300
324
|
so they are never seen by the gate.
|
|
301
325
|
|
|
@@ -495,6 +519,7 @@ and is never reclassified as a pin.
|
|
|
495
519
|
| `tdd note "<text>"` | attach a free-text narrative note to the current cycle or run |
|
|
496
520
|
| `tdd blocker --kind --detail` | typed blocker; releases the stop hook |
|
|
497
521
|
| `tdd resume [--unblock --note]` | reconstruct position; human intervention |
|
|
522
|
+
| `tdd run abandon --reason <text> [--run <id>]` | end a run that will not be finished; human only |
|
|
498
523
|
| `tdd log render [--out]` | project the ledger into a friction log |
|
|
499
524
|
| `tdd metrics` | fidelity, attempts, violations, interventions, time |
|
|
500
525
|
| `tdd fleet [--json]` | all active runs across every worktree; read-only |
|
|
@@ -142,7 +142,11 @@ being repeated in `test_paths`. `env` values may reference `${VAR}`, expanded fr
|
|
|
142
142
|
environment at invocation. For pytest, `collect_command` is optional (`--collect-only`
|
|
143
143
|
composes with the run command); a vitest override must declare one — `vitest list` knows
|
|
144
144
|
nothing of the override config, and the mismatch is refused at `tdd run start`, not
|
|
145
|
-
mid-cycle.
|
|
145
|
+
mid-cycle. For the whole-suite listing tdd appends `--json` to each vitest collect command
|
|
146
|
+
(the default and every override's) and reads each test's file from vitest's JSON, so a
|
|
147
|
+
collect command must not carry `--json` itself: `vitest list --json --json` prints nothing.
|
|
148
|
+
The override-isolation check in `tdd doctor` reads the same JSON listing, and a listing it
|
|
149
|
+
cannot read fails the check, naming the command it ran.
|
|
146
150
|
|
|
147
151
|
## Sharing cores between concurrent agents
|
|
148
152
|
|
|
@@ -268,6 +272,26 @@ When the last declared cycle closes, the tool gathers every path that appeared i
|
|
|
268
272
|
emitted (visible in `tdd metrics` and the friction log) and the run completes. The event
|
|
269
273
|
makes a silent drop visible without blocking, since the file is already gone.
|
|
270
274
|
|
|
275
|
+
### Abandoning a run
|
|
276
|
+
|
|
277
|
+
A run that will not be finished — stopped on purpose, or blocked for good — is ended with
|
|
278
|
+
`tdd run abandon --reason "<why>"` rather than `resume --unblock`. It takes the worktree's
|
|
279
|
+
live run, else its most recent blocked run. It sets the run's `outcome` to `abandoned`,
|
|
280
|
+
records the reason and who abandoned it (the account, and the executor identity as `run
|
|
281
|
+
start` resolves it; on a split runner the account is the calling agent's `SUDO_USER`),
|
|
282
|
+
records a `human_intervention`, and releases the worktree's baseline and advance claims.
|
|
283
|
+
`tdd fleet` stops listing the run, `tdd metrics` reports it as `abandoned` with an
|
|
284
|
+
`abandoned: {reason, by}` entry, and a new run can start at the same worktree path.
|
|
285
|
+
|
|
286
|
+
When the worktree is gone, abandon by id from any worktree of the repo: `tdd run abandon
|
|
287
|
+
--run <id> --reason "<why>"`. `--run` is accepted only for a run whose worktree no longer
|
|
288
|
+
exists or is the caller's own, so one agent cannot end another's live run.
|
|
289
|
+
|
|
290
|
+
Refusals, under `result.reason`, write nothing: `reason_required` (blank reason),
|
|
291
|
+
`no_run` (no live or blocked run here), `run_not_found`, `run_ended` (already complete or
|
|
292
|
+
abandoned), `worktree_exists` (`--run` names another worktree that still exists), and
|
|
293
|
+
`advance_in_flight` (a live `tdd advance` holds the worktree; a stale claim is released).
|
|
294
|
+
|
|
271
295
|
The gate composes with `ancillary_files`: declared paths never fire `undeclared_file_touched`,
|
|
272
296
|
so they are never seen by the gate.
|
|
273
297
|
|
|
@@ -467,6 +491,7 @@ and is never reclassified as a pin.
|
|
|
467
491
|
| `tdd note "<text>"` | attach a free-text narrative note to the current cycle or run |
|
|
468
492
|
| `tdd blocker --kind --detail` | typed blocker; releases the stop hook |
|
|
469
493
|
| `tdd resume [--unblock --note]` | reconstruct position; human intervention |
|
|
494
|
+
| `tdd run abandon --reason <text> [--run <id>]` | end a run that will not be finished; human only |
|
|
470
495
|
| `tdd log render [--out]` | project the ledger into a friction log |
|
|
471
496
|
| `tdd metrics` | fidelity, attempts, violations, interventions, time |
|
|
472
497
|
| `tdd fleet [--json]` | all active runs across every worktree; read-only |
|
|
@@ -528,12 +528,16 @@ one has no move left but to re-run doctor and read the same output again.
|
|
|
528
528
|
| `tdd blocker --kind <k> --detail <text>` | record a typed blocker; set run `outcome = blocked`, releasing the stop hook (H1) |
|
|
529
529
|
| `tdd resume` | reconstruct position from the ledger and emit `next_action` |
|
|
530
530
|
| `tdd resume --unblock --note <text>` | **human only.** Reopen a blocked run, recording a `human_intervention` event with the note |
|
|
531
|
+
| `tdd run abandon --reason <text> [--run <id>]` | **human only.** End a live or blocked run that will not be finished: `outcome = abandoned`, an `abandonment` row (reason, account, executor), a `human_intervention` event, and the worktree's claims released. `--run` only for a run whose worktree is gone or is the caller's own |
|
|
531
532
|
|
|
532
533
|
- **R8.7** A blocked run is not live. H1 must permit the agent to stop, or a blocker traps it.
|
|
533
534
|
- **R8.8** `human_intervention` events are the input to interventions-per-run, the primary
|
|
534
535
|
autonomy metric.
|
|
535
|
-
- **R8.9** In `AWAITING_TEST`, `advance` resolves the target by diffing `collect()` against
|
|
536
|
-
|
|
536
|
+
- **R8.9** In `AWAITING_TEST`, `advance` resolves the target by diffing `collect()` against the
|
|
537
|
+
run's start, and never offers a test that another cycle of the run targets: an earlier cycle's
|
|
538
|
+
test is new since the start too, and adopting it would hand one test to two cycles (#163). The
|
|
539
|
+
exclusion compares `normalise_id` forms (R10.8), because a target may be stored in its declared
|
|
540
|
+
spelling. When the declared target is `not_found`, the adoption flow runs:
|
|
537
541
|
- **Single new test:** adopted as the target; `declared_test_mismatch` is recorded. The verdict
|
|
538
542
|
from the run that already happened is evaluated immediately in the same `advance` call — no
|
|
539
543
|
extra suite run. If the adopted test failed, the RED commit is made and the cycle moves to
|
|
@@ -543,6 +547,14 @@ one has no move left but to re-run doctor and read the same output again.
|
|
|
543
547
|
above; `declared_test_mismatch` is recorded with the full candidate list.
|
|
544
548
|
- **Multiple new tests — ambiguous:** recorded as `multiple_new_tests`; `next_action` requires
|
|
545
549
|
the agent to name the intended target with `tdd target <id>` rather than guessing.
|
|
550
|
+
`tdd target` qualifies its argument the way a plan declaration is qualified and matches it by
|
|
551
|
+
`normalise_id` (R10.8), so the plan's own spelling names the test; it stores the collected id,
|
|
552
|
+
and refuses an id that matches no collected test in any spelling (#163).
|
|
553
|
+
- **Adopted target the run cannot find:** the adoption is evaluated before it is recorded. If
|
|
554
|
+
the run reports the adopted test `not_found`, collection and execution disagree about its id,
|
|
555
|
+
and no test the agent writes can fix that: the declared target is kept, no
|
|
556
|
+
`declared_test_mismatch` is recorded, `adopted_target_not_found` is (`{declared, adopted}`),
|
|
557
|
+
and `next_action` is `resolve_blocker`, asking for a `tooling` blocker (#163).
|
|
546
558
|
The run that produced `not_found` already executed the new test; its verdict is retrieved from
|
|
547
559
|
the in-hand `Verdict` objects without an extra suite run.
|
|
548
560
|
|
|
@@ -860,7 +872,14 @@ Adapter.typecheck(project) -> GateResult
|
|
|
860
872
|
file it came from. `tdd doctor` reads the subprocess's **stdout**: `uv` writes
|
|
861
873
|
environment warnings (e.g. `VIRTUAL_ENV=... does not match ...`) to stderr, while pytest writes
|
|
862
874
|
the actual collection error (e.g. `ModuleNotFoundError`) to stdout. A check that reads stderr
|
|
863
|
-
reports the wrapper's noise and loses the real error underneath it.
|
|
875
|
+
reports the wrapper's noise and loses the real error underneath it. Collection's whole-suite
|
|
876
|
+
vitest listing is `vitest list --json`, not the text listing `collectable()` probes: the text
|
|
877
|
+
prefixes `[<project>] ` to every line of a named vitest project and prints paths relative to
|
|
878
|
+
that project's own `root`, while the JSON carries each test's absolute `file`, which the
|
|
879
|
+
adapter makes root-relative exactly as `run()` does (#163). The vitest override-isolation probe
|
|
880
|
+
reads the same `vitest list --json` and decides reach from each entry's root-relative `file`; a
|
|
881
|
+
listing that is not a JSON array fails the probe and names the command, since passing it would
|
|
882
|
+
disable the check without a signal.
|
|
864
883
|
|
|
865
884
|
---
|
|
866
885
|
|
|
@@ -74,11 +74,11 @@ skill that duplicates any of it will fight the ledger.
|
|
|
74
74
|
| `write_implementation` | RED is confirmed and committed, or the target still fails at GREEN (only the target ran) | write the minimum code to pass the target, then `tdd advance` |
|
|
75
75
|
| `fix_regression` | tests outside the cycle are failing at GREEN, or the close sweep / lint / typecheck gates failed | fix them without breaking the target, then `tdd advance` |
|
|
76
76
|
| `run_sensitivity_check` | a test passed where proof it *can* fail is required | `tdd sensitivity begin`, mutate the behaviour under test, `tdd sensitivity check`, then `tdd sensitivity end` |
|
|
77
|
-
| `name_target_test` | several new tests appeared; a cycle covers one behaviour | pick the intended one: `tdd target <id
|
|
77
|
+
| `name_target_test` | several new tests appeared; a cycle covers one behaviour | pick the intended one: `tdd target <id>`, in the collected spelling or the plan's own (`src/a.test.ts > math > adds`); the collected id is stored |
|
|
78
78
|
| `refactor_or_advance` | GREEN is committed, or the tool simply wants `tdd advance` next | refactor only if the plan calls for it, then `tdd advance` |
|
|
79
79
|
| `confirm_cycle_applicable` | a judgement point outside the cycle loop: config scaffolded, no active run, doctor passed | review / register / start as the `detail` names; if a cycle no longer applies, `tdd cycle skip --reason "..."` |
|
|
80
80
|
| `annotate_cycle` | the plan requires judgement annotations before the cycle closes | `tdd annotate --key <k> --value "..."` for each missing key, then `tdd advance` |
|
|
81
|
-
| `resolve_blocker` | wedged: three unchanged retries,
|
|
81
|
+
| `resolve_blocker` | wedged: three unchanged retries, failing environment checks, or an adopted target the run cannot find | fix the cause, or record it: `tdd blocker --kind <kind> --detail "..."` |
|
|
82
82
|
| `await_baseline` | baseline collection is in flight (collector pid alive) | poll `tdd progress`; **never** re-run `tdd run start` for a live claim |
|
|
83
83
|
| `complete` *(terminal)* | the run (or command) is finished | render the friction log if the `detail` asks, then stop |
|
|
84
84
|
| `blocked` *(terminal)* | a typed blocker was recorded | surface the blocker to the human and stop |
|
|
@@ -41,6 +41,18 @@ def _extract_json(raw: str) -> dict | None:
|
|
|
41
41
|
return None
|
|
42
42
|
|
|
43
43
|
|
|
44
|
+
def _json_listing(raw: str) -> list[dict] | None:
|
|
45
|
+
"""`vitest list --json`'s array of `{name, file, projectName}`, or None when the
|
|
46
|
+
output is not one."""
|
|
47
|
+
try:
|
|
48
|
+
payload = json.loads(raw)
|
|
49
|
+
except json.JSONDecodeError:
|
|
50
|
+
return None
|
|
51
|
+
if not isinstance(payload, list):
|
|
52
|
+
return None
|
|
53
|
+
return [entry for entry in payload if isinstance(entry, dict)]
|
|
54
|
+
|
|
55
|
+
|
|
44
56
|
#: The characters a JavaScript regex treats as syntax. Only these are escaped: vitest
|
|
45
57
|
#: may compile `-t` with the `u` flag, which rejects the identity escapes (`\ `, `\-`)
|
|
46
58
|
#: that Python's `re.escape` emits.
|
|
@@ -86,13 +98,14 @@ class VitestAdapter(Adapter):
|
|
|
86
98
|
name = " ".join(part.strip() for part in remainder.split(" > "))
|
|
87
99
|
return self.qualify(f"{file_part} > {name}")
|
|
88
100
|
|
|
89
|
-
def
|
|
90
|
-
abs_path = Path(suite_path)
|
|
101
|
+
def _rel(self, suite_path: str) -> str:
|
|
91
102
|
try:
|
|
92
|
-
|
|
103
|
+
return os.path.relpath(Path(suite_path), self.root)
|
|
93
104
|
except ValueError:
|
|
94
|
-
|
|
95
|
-
|
|
105
|
+
return suite_path
|
|
106
|
+
|
|
107
|
+
def _id_for(self, suite_path: str, full_name: str) -> str:
|
|
108
|
+
return self.qualify(f"{self._rel(suite_path)} > {full_name}")
|
|
96
109
|
|
|
97
110
|
def _test_cmd(self) -> str:
|
|
98
111
|
return self.project.test_command or "npx vitest run"
|
|
@@ -267,24 +280,31 @@ class VitestAdapter(Adapter):
|
|
|
267
280
|
return GateResult(ok=not chunks, output="\n\n".join(chunks)[:2000])
|
|
268
281
|
|
|
269
282
|
def override_isolation(self) -> GateResult:
|
|
270
|
-
"""Probes with the default `vitest list` — the same stand-in for the
|
|
283
|
+
"""Probes with the default `vitest list --json` — the same stand-in for the
|
|
271
284
|
default run config that `collectable()` already relies on (`vitest run`
|
|
272
285
|
has no listing mode, and running the suite just to enumerate it would
|
|
273
|
-
execute against whatever the tests need live).
|
|
286
|
+
execute against whatever the tests need live). Reach is decided from each
|
|
287
|
+
entry's root-relative `file`; a listing that is not JSON fails the gate
|
|
288
|
+
rather than passing it by reaching nothing."""
|
|
274
289
|
if not self.project.overrides:
|
|
275
290
|
return GateResult(ok=True)
|
|
291
|
+
command = f"{self._collect_cmd()} --json"
|
|
276
292
|
code, out, err = run_command(
|
|
277
|
-
|
|
278
|
-
)
|
|
279
|
-
reached = sorted(
|
|
280
|
-
{
|
|
281
|
-
f
|
|
282
|
-
for f in (
|
|
283
|
-
line.strip().partition(" > ")[0] for line in out.splitlines() if " > " in line
|
|
284
|
-
)
|
|
285
|
-
if self.project.override_for(f)
|
|
286
|
-
}
|
|
293
|
+
command, self.root, extra_env=self._suite_env(None), label="doctor"
|
|
287
294
|
)
|
|
295
|
+
listing = _json_listing(out)
|
|
296
|
+
if listing is None:
|
|
297
|
+
return GateResult(
|
|
298
|
+
ok=False,
|
|
299
|
+
output=(
|
|
300
|
+
f"`{command}` did not print a JSON array (exit {code}); tdd reads"
|
|
301
|
+
" vitest's JSON listing to check which files the default config"
|
|
302
|
+
" reaches, so a collect command must not already carry --json: "
|
|
303
|
+
+ (err or out).strip()[:600]
|
|
304
|
+
),
|
|
305
|
+
)
|
|
306
|
+
listed = {self._rel(entry["file"]) for entry in listing if entry.get("file")}
|
|
307
|
+
reached = sorted(f for f in listed if self.project.override_for(f))
|
|
288
308
|
if not reached:
|
|
289
309
|
return GateResult(ok=True)
|
|
290
310
|
return GateResult(
|
|
@@ -311,22 +331,26 @@ class VitestAdapter(Adapter):
|
|
|
311
331
|
self, command: str, env: dict[str, str] | None
|
|
312
332
|
) -> tuple[set[str], set[str]] | None:
|
|
313
333
|
"""Unlike `_parse_list_output`, which pins every id to the one file it was
|
|
314
|
-
given, a whole-suite listing must read
|
|
315
|
-
|
|
316
|
-
|
|
334
|
+
given, a whole-suite listing must read each test's file. The text listing
|
|
335
|
+
cannot supply it: a named vitest project prefixes `[<name>] ` to every line,
|
|
336
|
+
and a project with its own `root` prints paths relative to that root. The
|
|
337
|
+
JSON listing carries the absolute `file`, rooted here exactly as `run()`
|
|
338
|
+
roots its report. Anything that is not a JSON array is left to the loop."""
|
|
339
|
+
code, out, err = run_command(f"{command} --json", self.root, extra_env=env, label="collect")
|
|
317
340
|
if code != 0:
|
|
318
341
|
return None
|
|
342
|
+
listing = _json_listing(out)
|
|
343
|
+
if listing is None:
|
|
344
|
+
return None
|
|
319
345
|
tests: set[str] = set()
|
|
320
346
|
files: set[str] = set()
|
|
321
|
-
for
|
|
322
|
-
|
|
323
|
-
if
|
|
347
|
+
for entry in listing:
|
|
348
|
+
file, name = entry.get("file"), entry.get("name")
|
|
349
|
+
if not (file and name):
|
|
324
350
|
continue
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
tests.add(self.qualify(f"{rel.strip()} > {full_name}"))
|
|
329
|
-
files.add(rel.strip())
|
|
351
|
+
full_name = " ".join(part.strip() for part in name.split(" > "))
|
|
352
|
+
tests.add(self._id_for(file, full_name))
|
|
353
|
+
files.add(self._rel(file))
|
|
330
354
|
# An empty result needs no special case: it accounts for no files, so every
|
|
331
355
|
# file falls to the loop exactly as a failure would.
|
|
332
356
|
return tests, files
|
|
@@ -58,17 +58,30 @@ def _last_invocation_hash(engine: Engine, cycle) -> str | None:
|
|
|
58
58
|
|
|
59
59
|
|
|
60
60
|
def _adopt_target(engine: Engine, cycle, missing: list[str]) -> tuple[list[str], list[str]]:
|
|
61
|
-
"""R8.9 — resolve the target from newly collected tests when the declared id misses.
|
|
61
|
+
"""R8.9 — resolve the target from newly collected tests when the declared id misses.
|
|
62
|
+
|
|
63
|
+
A test another cycle of the run already targets is never a candidate: it is new
|
|
64
|
+
since the run started too. Targets may be stored in the declared spelling, so the
|
|
65
|
+
exclusion compares normalised ids (#163)."""
|
|
62
66
|
projects = json.loads(cycle["projects"])
|
|
63
67
|
at_start = engine.ledger.collection(engine.run["id"])
|
|
64
68
|
known = {t for tests in at_start.values() for t in tests}
|
|
65
|
-
|
|
69
|
+
run_targets = {
|
|
70
|
+
t
|
|
71
|
+
for row in engine.ledger.all(
|
|
72
|
+
"SELECT target_tests FROM cycle WHERE run_id = ?", (engine.run["id"],)
|
|
73
|
+
)
|
|
74
|
+
for t in json.loads(row["target_tests"])
|
|
75
|
+
}
|
|
66
76
|
|
|
67
77
|
new_tests: list[str] = []
|
|
68
78
|
for name in projects:
|
|
69
79
|
adapter = adapters.build(engine.config.project(name), engine.worktree)
|
|
80
|
+
claimed = {adapter.normalise_id(t) for t in run_targets}
|
|
70
81
|
current = adapter.collect()
|
|
71
|
-
new_tests.extend(
|
|
82
|
+
new_tests.extend(
|
|
83
|
+
sorted(t for t in current.tests - known if adapter.normalise_id(t) not in claimed)
|
|
84
|
+
)
|
|
72
85
|
return new_tests, missing
|
|
73
86
|
|
|
74
87
|
|
|
@@ -200,6 +213,60 @@ def _evaluate_adopted(
|
|
|
200
213
|
return outcomes.get(adopted), failure
|
|
201
214
|
|
|
202
215
|
|
|
216
|
+
def _adopt(
|
|
217
|
+
engine: Engine,
|
|
218
|
+
cycle,
|
|
219
|
+
projects: list[str],
|
|
220
|
+
targets: list[str],
|
|
221
|
+
missing: list[str],
|
|
222
|
+
adopted: str,
|
|
223
|
+
detail: dict,
|
|
224
|
+
verdicts,
|
|
225
|
+
retried: bool,
|
|
226
|
+
):
|
|
227
|
+
"""R8.9's one adoption path, whichever branch chose `adopted`: evaluate the new
|
|
228
|
+
target, then record the mismatch and retarget the cycle. Returns the reply when
|
|
229
|
+
the target is refused or has no outcome yet, else `(cycle, targets, outcome,
|
|
230
|
+
failure)`.
|
|
231
|
+
|
|
232
|
+
The evaluation comes first because a test that collection lists but the run
|
|
233
|
+
cannot find would otherwise become the target and come back `not_found` on every
|
|
234
|
+
advance (#163): collection and execution disagree about its id, which no test
|
|
235
|
+
the agent writes can fix."""
|
|
236
|
+
kept = [t for t in targets if t not in missing]
|
|
237
|
+
outcome, failure = _evaluate_adopted(engine, cycle, projects, kept, adopted, verdicts, retried)
|
|
238
|
+
if outcome == NOT_FOUND:
|
|
239
|
+
engine.ledger.event(
|
|
240
|
+
engine.run["id"],
|
|
241
|
+
cycle["id"],
|
|
242
|
+
"adopted_target_not_found",
|
|
243
|
+
json.dumps({"declared": missing, "adopted": [adopted]}),
|
|
244
|
+
)
|
|
245
|
+
return _reply(
|
|
246
|
+
engine,
|
|
247
|
+
cycle,
|
|
248
|
+
Verb.RESOLVE_BLOCKER,
|
|
249
|
+
f"{adopted} is collected but the run cannot find it: collection and execution"
|
|
250
|
+
" disagree about its id. The declared target is unchanged. Record it:"
|
|
251
|
+
" `tdd blocker --kind tooling --detail '...'`.",
|
|
252
|
+
adopted=[adopted],
|
|
253
|
+
declared=missing,
|
|
254
|
+
)
|
|
255
|
+
engine.ledger.event(engine.run["id"], cycle["id"], "declared_test_mismatch", json.dumps(detail))
|
|
256
|
+
engine.ledger.update("cycle", cycle["id"], target_tests=json.dumps(kept + [adopted]))
|
|
257
|
+
cycle = engine.ledger.one("SELECT * FROM cycle WHERE id = ?", (cycle["id"],))
|
|
258
|
+
if outcome is None:
|
|
259
|
+
return _reply(
|
|
260
|
+
engine,
|
|
261
|
+
cycle,
|
|
262
|
+
Verb.REFACTOR_OR_ADVANCE,
|
|
263
|
+
f"Adopted {adopted} as the target (declared {missing[0]} was not"
|
|
264
|
+
" collected). Run `tdd advance` again to evaluate it.",
|
|
265
|
+
adopted=[adopted],
|
|
266
|
+
)
|
|
267
|
+
return cycle, kept + [adopted], outcome, failure
|
|
268
|
+
|
|
269
|
+
|
|
203
270
|
# -- handlers ------------------------------------------------------------
|
|
204
271
|
|
|
205
272
|
|
|
@@ -219,67 +286,23 @@ def _handle_test_phase(engine: Engine, cycle, retried: bool, expect_pass: bool)
|
|
|
219
286
|
missing = [t for t, o in outcomes.items() if o == NOT_FOUND]
|
|
220
287
|
if missing:
|
|
221
288
|
candidates, _ = _adopt_target(engine, cycle, missing)
|
|
222
|
-
if
|
|
223
|
-
|
|
224
|
-
engine
|
|
225
|
-
cycle
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
engine.ledger.update("cycle", cycle["id"], target_tests=json.dumps(kept + candidates))
|
|
231
|
-
cycle = engine.ledger.one("SELECT * FROM cycle WHERE id = ?", (cycle["id"],))
|
|
232
|
-
adopted_outcome, adopted_failure = _evaluate_adopted(
|
|
233
|
-
engine, cycle, projects, kept, candidates[0], verdicts, retried
|
|
289
|
+
if not candidates:
|
|
290
|
+
return _reply(
|
|
291
|
+
engine,
|
|
292
|
+
cycle,
|
|
293
|
+
Verb.WRITE_TEST,
|
|
294
|
+
f"Target {missing[0]} was not collected and no new test was found."
|
|
295
|
+
" Write the failing test.",
|
|
296
|
+
missing=missing,
|
|
234
297
|
)
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
Verb.REFACTOR_OR_ADVANCE,
|
|
240
|
-
f"Adopted {candidates[0]} as the target (declared {missing[0]} was not"
|
|
241
|
-
" collected). Run `tdd advance` again to evaluate it.",
|
|
242
|
-
adopted=candidates,
|
|
243
|
-
)
|
|
244
|
-
targets = kept + candidates
|
|
245
|
-
outcomes = {candidates[0]: adopted_outcome}
|
|
246
|
-
failure = adopted_failure or failure
|
|
247
|
-
others = [t for t in others if t != candidates[0]]
|
|
248
|
-
elif len(candidates) > 1:
|
|
298
|
+
if len(candidates) == 1:
|
|
299
|
+
adopted = candidates[0]
|
|
300
|
+
detail = {"declared": missing, "adopted": candidates}
|
|
301
|
+
else:
|
|
249
302
|
owner = missing[0].split("::", 1)[0]
|
|
250
303
|
adapter = adapters.build(engine.config.project(owner), engine.worktree)
|
|
251
304
|
resolved = _disambiguate(candidates, missing[0], adapter)
|
|
252
|
-
if resolved is
|
|
253
|
-
engine.ledger.event(
|
|
254
|
-
engine.run["id"],
|
|
255
|
-
cycle["id"],
|
|
256
|
-
"declared_test_mismatch",
|
|
257
|
-
json.dumps(
|
|
258
|
-
{"declared": missing, "adopted": [resolved], "all_candidates": candidates}
|
|
259
|
-
),
|
|
260
|
-
)
|
|
261
|
-
kept = [t for t in targets if t not in missing]
|
|
262
|
-
engine.ledger.update(
|
|
263
|
-
"cycle", cycle["id"], target_tests=json.dumps(kept + [resolved])
|
|
264
|
-
)
|
|
265
|
-
cycle = engine.ledger.one("SELECT * FROM cycle WHERE id = ?", (cycle["id"],))
|
|
266
|
-
adopted_outcome, adopted_failure = _evaluate_adopted(
|
|
267
|
-
engine, cycle, projects, kept, resolved, verdicts, retried
|
|
268
|
-
)
|
|
269
|
-
if adopted_outcome is None:
|
|
270
|
-
return _reply(
|
|
271
|
-
engine,
|
|
272
|
-
cycle,
|
|
273
|
-
Verb.REFACTOR_OR_ADVANCE,
|
|
274
|
-
f"Adopted {resolved} as the target (declared {missing[0]} was not"
|
|
275
|
-
" collected). Run `tdd advance` again to evaluate it.",
|
|
276
|
-
adopted=[resolved],
|
|
277
|
-
)
|
|
278
|
-
targets = kept + [resolved]
|
|
279
|
-
outcomes = {resolved: adopted_outcome}
|
|
280
|
-
failure = adopted_failure or failure
|
|
281
|
-
others = [t for t in others if t != resolved]
|
|
282
|
-
else:
|
|
305
|
+
if resolved is None:
|
|
283
306
|
engine.ledger.event(
|
|
284
307
|
engine.run["id"],
|
|
285
308
|
cycle["id"],
|
|
@@ -294,15 +317,17 @@ def _handle_test_phase(engine: Engine, cycle, retried: bool, expect_pass: bool)
|
|
|
294
317
|
" intended target with `tdd target <id>`.",
|
|
295
318
|
candidates=candidates,
|
|
296
319
|
)
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
320
|
+
adopted = resolved
|
|
321
|
+
detail = {"declared": missing, "adopted": [resolved], "all_candidates": candidates}
|
|
322
|
+
adoption = _adopt(
|
|
323
|
+
engine, cycle, projects, targets, missing, adopted, detail, verdicts, retried
|
|
324
|
+
)
|
|
325
|
+
if isinstance(adoption, Envelope):
|
|
326
|
+
return adoption
|
|
327
|
+
cycle, targets, adopted_outcome, adopted_failure = adoption
|
|
328
|
+
outcomes = {adopted: adopted_outcome}
|
|
329
|
+
failure = adopted_failure or failure
|
|
330
|
+
others = [t for t in others if t != adopted]
|
|
306
331
|
|
|
307
332
|
not_collected = [t for t, o in outcomes.items() if o == NOT_COLLECTED]
|
|
308
333
|
if not_collected:
|