@jesdi/skills 0.0.43 → 0.0.44

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jesdi/skills",
3
- "version": "0.0.43",
3
+ "version": "0.0.44",
4
4
  "description": "jesdi's agent skills (content package consumed by @jesdi/skills-cli)",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -68,8 +68,15 @@ and do not know about the run, except `driver_run.py`, which the three deciding
68
68
  is not clean, so a restore left in the tree would turn the next gate red and use a fix round. The correction's
69
69
  prompt names the restored files (`prompts/restored.md`): this is the one path for a change of an earlier ticket's
70
70
  locked test that a later ticket's interface needs.
71
- - The locked files of merged tickets are checked when a ticket lands (`driver_merge.refused`): the tree that lands
72
- must have each of them as the PR tip has it. A ticket's test-writer may change one only when it lists it.
71
+ - The driver, not the correction, decides whether a restored file fails with the ticket's code: in the fix loop it
72
+ gates the restore commit, whose parent has a green gate. A red gate goes to the correction as evidence
73
+ (`prompts/restored-red.md`), and a DONE correction must then list a restored file. An agent's judgement of
74
+ "this test does not fail" missed a red file and led to a refused merge (issue #90). After `overrule-test` the
75
+ parent has no green gate, so a red gate proves nothing there: the next review catches the file instead.
76
+ - The locked files of merged tickets are checked before each review (`driver_review.merged_changes`): a change is a
77
+ `fixture` finding, so it goes to a correction, where the restore and its gate decide. They are checked again when
78
+ a ticket lands (`driver_merge.refused`): the tree that lands must have each of them as the PR tip has it. A
79
+ ticket's test-writer may change one only when it lists it.
73
80
  - A conflict resolution, of a merge or of the rebase, may not change a locked file.
74
81
 
75
82
  ### The PR branch
@@ -97,10 +97,11 @@ A ticket is ready when each ticket in its **Blocked by** line is merged and no r
97
97
  1. **test-writer** (`prompts/test-writer.md`). It writes one black-box test for each criterion through the Seam and commits them red. Those files are locked. A ticket with Seam `none — refactor` has no test-writer.
98
98
  2. **implementer** (`prompts/implementer.md`). It starts from that commit and makes the locked tests green. It makes the gate (`--gate`) green on its final commit before it reports DONE or DONE_WITH_CONCERNS.
99
99
  3. **Gate.** The driver runs `--gate` on the ticket tip.
100
- 4. **Review.** The driver compares each locked file of the ticket with the test-writer commit. A file that differs is a critical finding `locked file changed: <path>`. Then the **reviewer** (`prompts/reviewer.md`) runs, and for a ticket with a **Risk** line the **correctness** pass (`prompts/correctness.md`) runs at the same time in the same slot.
100
+ 4. **Review.** The driver compares each locked file of the ticket with the test-writer commit. A file that differs is a critical finding `locked file changed: <path>`. A locked file of a merged ticket that the ticket changed since its merge base with the PR branch is a critical `fixture` finding `the ticket changed the locked file of a merged ticket: <path>`, so it goes to a correction (see **Before a correction**). Then the **reviewer** (`prompts/reviewer.md`) runs, and for a ticket with a **Risk** line the **correctness** pass (`prompts/correctness.md`) runs at the same time in the same slot.
101
101
  5. **Merge.** The driver merges the reviewed tip into the PR branch with git plumbing, then deletes the ticket branch. A conflict goes to the **conflict** agent (`prompts/conflict.md`) once. Its resolution may change no locked file of the ticket. A second conflict blocks the ticket.
102
- **The landing check.** Merges are serial. Before the PR branch moves, the driver compares the tree that is about to land (the ticket tip of a fast-forward, or the merge commit) with the PR tip: each locked file of each merged ticket must be the same tree entry in both: the same blob with the same mode, so a file that became a symlink is a difference. A file that only one of them has is a difference. A file that the landing ticket lists in its own `locked` is not checked: the test-writer of a later ticket can add a test to a file that an earlier ticket locked, and must list that file. A test-writer report that changes such a file and does not list it is rejected, with the usual one rerun. A difference refuses the landing: the ticket is BLOCKED with `locked file changed: <path>` in the ledger, the PR branch does not move, and no fix round runs. The PR branch moves only from the tip that the check read; when another hand moved the branch in between, the driver makes the merge, the check and the gate again, one time. Only that case starts the landing again: a refused landing and a red merge gate block the ticket. The second try runs no conflict agent, so a conflict there blocks the ticket, and so does a branch that moved again.
102
+ **The landing check.** Merges are serial. Before the PR branch moves, the driver compares the tree that is about to land (the ticket tip of a fast-forward, or the merge commit) with the PR tip: each locked file of each merged ticket must be the same tree entry in both: the same blob with the same mode, so a file that became a symlink is a difference. A file that only one of them has is a difference. A file that the landing ticket lists in its own `locked` is not checked: the test-writer of a later ticket can add a test to a file that an earlier ticket locked, and must list that file. A test-writer report that changes such a file and does not list it is rejected, with the usual one rerun. The review finds such a change first; the landing check catches a change that no review saw, for example of a ticket that merged after the last review. A difference refuses the landing: the ticket is BLOCKED with `locked file changed: <path>` in the ledger, the PR branch does not move, and no fix round runs. The PR branch moves only from the tip that the check read; when another hand moved the branch in between, the driver makes the merge, the check and the gate again, one time. Only that case starts the landing again: a refused landing and a red merge gate block the ticket. The second try runs no conflict agent, so a conflict there blocks the ticket, and so does a branch that moved again.
103
103
  **Before a correction.** A test-writer that corrects locked tests (after `overrule-test`, or for a `fixture` finding or a `test` finding on a locked file) starts from the locked versions: the driver puts back each locked file of the ticket and each locked file of a merged ticket that the ticket changed, commits them in one commit of its own, and the ledger names each file. A file of a merged ticket goes back to the version of the PR branch. The commit keeps the tree of the next fixer and of the gate clean, whatever files the correction commits. The correction's prompt names the restored files: when a test in one of them fails only because the ticket changed an interface that the test uses, the correction updates the file to the new interface, keeps what each test asserts, and lists the file in `locked`.
104
+ **The gate of the restore.** In the fix loop, the tip before the restore has a green gate, so the driver gates the restore commit and does not leave the question to the correction. A red gate means that a restored file fails with the ticket's code: the correction gets the gate record (`prompts/restored-red.md`), and a DONE correction must list a restored file in `locked`; else it reports BLOCKED with `locked test contradicts spec:`. A green gate with only driver findings to correct closes them with the restore: no test-writer runs. After `overrule-test` the driver does not gate the restore, because the tip before it has no green gate; a change that the next implementer makes to a merged ticket's file is a finding of the next review.
104
105
 
105
106
  Rules for each agent:
106
107
 
@@ -172,7 +173,7 @@ Every report has `status` (`DONE`, `DONE_WITH_CONCERNS`, `NEEDS_CONTEXT`, `BLOCK
172
173
 
173
174
  | Role | Template | More fields | Checks after the schema |
174
175
  |---|---|---|---|
175
- | test-writer | `prompts/test-writer.md` | `locked` (test file paths), `red` (`test`, `line`) | The driver reads a test node id `<file>::<test>` in `locked` as its file. With DONE: one commit, one locked file and one red line or more. Each locked path is in a commit that the role made. Each `line` is in the log. A locked file of a merged ticket that the role changed is in `locked`. A correction (after `overrule-test`, or for a review finding) changes only the files that it lists in `locked` and the files that the driver restored before it. |
176
+ | test-writer | `prompts/test-writer.md` | `locked` (test file paths), `red` (`test`, `line`) | The driver reads a test node id `<file>::<test>` in `locked` as its file. With DONE: one commit, one locked file and one red line or more. Each locked path is in a commit that the role made. Each `line` is in the log. A locked file of a merged ticket that the role changed is in `locked`. A correction (after `overrule-test`, or for a review finding) changes only the files that it lists in `locked` and the files that the driver restored before it. When the gate is red on the restore commit, a DONE correction lists a restored file in `locked`. |
176
177
  | implementer | `prompts/implementer.md` | `green` | With DONE: one commit or more. |
177
178
  | reviewer | `prompts/reviewer.md` | `findings` (`severity`, `file`, `line`, `title`, `evidence`, `kind`), `mutations` (`test`, `file`, `line`, `red`); `commits` is empty | In the first review of a tip: `mutations` is required, with a proof for each locked test file (`test` is `<locked file>::<test name>`). Each `red` is in the log. |
178
179
  | correctness | `prompts/correctness.md` | the same as the reviewer; `commits` is empty | `mutations` is optional. |
@@ -202,7 +203,7 @@ Answer with `--answer <question id>=<option id>` on the next run, or through the
202
203
 
203
204
  | Check | Where it lives |
204
205
  |---|---|
205
- | Locked tests with a demonstrated red | `driver_report.py` `_proof_errors`: each red line of the test-writer report must be in its log. `driver_review.py` `locked_findings`: a locked file of the ticket that differs from the test-writer commit is a critical finding, before each review. `driver_merge.py` `refused`, called from `driver.py` `_checked_landing` under the merge lock: a locked file of a merged ticket that differs between the PR tip and the tree that is about to land refuses the landing, so a later ticket cannot change the locked test of an earlier one. `driver_report.py` `_merged_errors` rejects a test-writer report that changes such a file and does not list it. |
206
+ | Locked tests with a demonstrated red | `driver_report.py` `_proof_errors`: each red line of the test-writer report must be in its log. `driver_review.py` `locked_findings`: a locked file of the ticket that differs from the test-writer commit is a critical finding, before each review. `driver_review.py` `merged_changes`: a locked file of a merged ticket that the ticket changed is a critical `fixture` finding, before each review. `driver_merge.py` `refused`, called from `driver.py` `_checked_landing` under the merge lock: a locked file of a merged ticket that differs between the PR tip and the tree that is about to land refuses the landing, so a later ticket cannot change the locked test of an earlier one. `driver_report.py` `_merged_errors` rejects a test-writer report that changes such a file and does not list it. |
206
207
  | One mutation proof for each locked test | `prompts/reviewer.md` asks for one proof for each locked test. `driver_report.py` `_locked_errors` and `_proof_errors` reject a first review with no proof for a locked test file, or with a red line that is not in the log. `driver_review.py` `follow` finds the proofs that a fix makes due again. |
207
208
  | One independent correctness pass for each risky ticket | `driver_fixloop.py` `_review` adds the pass for a ticket with a **Risk** line. `driver_agents.py` `correctness_entry` picks the entry of another provider, and `load_models` stops the run when there is none or its CLI is not on PATH. |
208
209
  | One full gate for each candidate | `driver_gate.py` `run`, called from `driver_run.py` `Run.gate`: one record for each commit the driver is about to trust. Green is exit 0 on a tree that is clean before and after, with HEAD not moved. |
@@ -11,6 +11,7 @@ from concurrent.futures import ThreadPoolExecutor
11
11
  from dataclasses import dataclass, field
12
12
 
13
13
  import driver_agents
14
+ import driver_merge
14
15
  import driver_review
15
16
  import driver_slots
16
17
  from driver_run import OUT, Blocked, Run, during, ledger
@@ -86,7 +87,8 @@ def _review(job: Job) -> list[dict]:
86
87
  """Review `job.tip`. Returns the findings of the driver and of each reviewer."""
87
88
  run, number = job.run, job.ticket.number
88
89
  state = run.state.ticket(number, phase="review")
89
- found = _log_findings(job, driver_review.locked_findings(job.slot, state))
90
+ merged = driver_merge.changed(job.slot, run.tip(), driver_merge.protected(run.state.read()["tickets"], number))
91
+ found = _log_findings(job, driver_review.locked_findings(job.slot, state) + driver_review.merged_changes(merged))
90
92
  cast = [("reviewer", "")]
91
93
  if job.ticket.risk:
92
94
  cast.append(("correctness", driver_agents.correctness_entry(run.models)))
@@ -107,11 +109,21 @@ def _correct(job: Job, findings: list[dict]) -> None:
107
109
  raise Blocked(f"the review: a locked-test finding is open after the test-writer correction: {titles}")
108
110
  restored = run.restore_locked_files(number, job.slot)
109
111
  job.tip = driver_slots.head(job.slot) # the restore is a commit of the driver
110
- values = {**job.pointers(job.round), "restored": " ".join(restored)}
111
- notes = ["correction"] + ["restored"] * bool(restored)
112
+ # The gate was green on the tip before the restore, so a red gate here is a restored file that fails with the
113
+ # ticket's code. The driver decides that, not the correction (issue #90).
114
+ with during("the gate of the restore"):
115
+ record = run.red_gate(number, job.slot, job.tip) if restored else ""
116
+ if not record and all(finding["source"] == "driver" for finding in findings):
117
+ job.previous = "" # no review has seen the restore commit
118
+ ledger(f"Ticket {number}: the code does not need the change to {', '.join(restored)}: the restore closes it, "
119
+ "with no correction.")
120
+ return
121
+ values = {**job.pointers(job.round), "restored": " ".join(restored),
122
+ "record": record and os.path.relpath(record, job.slot)}
123
+ notes = ["correction"] + ["restored"] * bool(restored) + ["restored-red"] * bool(record)
112
124
  with during("the test-writer correction"):
113
125
  report = run.agent("test-writer", number, job.slot, values, notes=notes, fix_round=job.round + 1,
114
- check={"correction": True, "restored": restored})
126
+ check={"correction": True, "restored": restored, "restore_red": bool(record)})
115
127
  job.tip, job.previous, job.corrected = driver_slots.head(job.slot), "", True
116
128
  entry = run.state.read()["tickets"][number]
117
129
  run.state.ticket(number, tip=job.tip, **driver_review.written(job.slot, entry, report["locked"], job.tip))
@@ -153,7 +165,7 @@ def _act_on(job: Job, found: list[dict]) -> bool:
153
165
  # No correction after round 4: no fix round could make its new locked tests green, so `accept` could not merge.
154
166
  to_correct, to_fix = [], [finding for finding in found if finding in to_correct + to_fix]
155
167
  # A changed locked file is fixed first: a correction on top of it would make the changed file the reference.
156
- locked_file_changed = any(finding["source"] == "driver" for finding in found)
168
+ locked_file_changed = any(finding["source"] == "driver" for finding in to_fix)
157
169
  if to_correct and not locked_file_changed:
158
170
  _correct(job, to_correct)
159
171
  if not to_fix:
@@ -82,16 +82,21 @@ def refused(tickets: dict, number: str, old: str, new: str) -> str:
82
82
  if before.get(path) != after.get(path))
83
83
 
84
84
 
85
+ def changed(slot: str, pr_tip: str, paths: list[str]) -> list[str]:
86
+ """Each of `paths` that the ticket changed in the tree of `slot`."""
87
+ # Against the merge base: a file that only the PR branch changed since then is not the ticket's change.
88
+ base = git(slot, "merge-base", "HEAD", pr_tip).stdout.strip() or pr_tip
89
+ return names(git(slot, "diff", "--name-only", "-z", base, "--", *paths).stdout) if paths else []
90
+
91
+
85
92
  def restore(slot: str, pr_tip: str, paths: list[str]) -> list[str]:
86
93
  """Put each of `paths` that the ticket changed back to the version of the PR tip, in the tree of `slot` only; a
87
94
  file the PR tip does not have is removed. Returns those paths."""
88
- # Against the merge base: a file that only the PR branch changed since then is not the ticket's change.
89
- base = git(slot, "merge-base", "HEAD", pr_tip).stdout.strip() or pr_tip
90
- changed = names(git(slot, "diff", "--name-only", "-z", base, "--", *paths).stdout) if paths else []
91
- for path in changed:
95
+ restored = changed(slot, pr_tip, paths)
96
+ for path in restored:
92
97
  if git(slot, "checkout", "-q", pr_tip, "--", path).returncode:
93
98
  git(slot, "rm", "-q", "-f", "--", path)
94
- return changed
99
+ return restored
95
100
 
96
101
 
97
102
  def land(pr_branch: str, new: str, old: str) -> str:
@@ -2,6 +2,7 @@
2
2
 
3
3
  context: repo (slot path), branch, start (HEAD when the role started), log, round (1 = first review), locked,
4
4
  correction (a test-writer corrects locked tests), restored (locked files the driver put back before it),
5
+ restore_red (the gate is red on the commit of that restore),
5
6
  owners (ticket numbers, for the final fix batch), merged (locked files of merged tickets, not its own), pr (tip)."""
6
7
  from __future__ import annotations
7
8
 
@@ -66,8 +67,14 @@ def _correction_errors(report: dict, context: dict) -> list[str]:
66
67
  return []
67
68
  changed = _git(context["repo"], "diff", "--name-only", "-z", context["start"], context["branch"], end="\0")
68
69
  more = [path for path in changed if path not in report.get("locked", []) + context.get("restored", [])]
69
- return [f"a correction may change only the files it lists in `locked`: its commits also change {', '.join(more)}"
70
- ] if more else []
70
+ errors = [f"a correction may change only the files it lists in `locked`: its commits also change {', '.join(more)}"
71
+ ] if more else []
72
+ if context.get("restore_red") and report["status"] in ("DONE", "DONE_WITH_CONCERNS") and not (
73
+ set(report["locked"]) & set(context["restored"])):
74
+ errors.append("the gate is red on the restore commit, so a restored file fails with the code of this ticket: "
75
+ "update that file and list it in `locked`, or report BLOCKED with a concern that starts with "
76
+ "`locked test contradicts spec:`")
77
+ return errors
71
78
 
72
79
 
73
80
  def _merged_errors(report: dict, context: dict) -> list[str]:
@@ -38,6 +38,14 @@ def locked_changes(slot: str, sha: str | None, locked: list[str]) -> list[dict]:
38
38
  "evidence": f"git diff {sha}..HEAD -- {path}"} for path, title in titles.items()]
39
39
 
40
40
 
41
+ def merged_changes(paths: list[str]) -> list[dict]:
42
+ """One critical `fixture` finding per locked file of a merged ticket that the ticket changed. A fixture finding
43
+ goes to a test-writer correction: the driver puts the file back before it and gates that commit."""
44
+ return [{"severity": "critical", "file": path, "line": 1, "kind": "fixture", "source": "driver",
45
+ "title": f"the ticket changed the locked file of a merged ticket: {path}",
46
+ "evidence": f"git diff <merge base with the PR branch> -- {path}"} for path in paths]
47
+
48
+
41
49
  def _references(entry: dict) -> list[tuple[str | None, list[str]]]:
42
50
  """(reference commit, the locked files it is the reference of): `test_writer_sha`, or the older commit that
43
51
  `held` names for a file."""
@@ -0,0 +1,3 @@
1
+
2
+ The driver ran the gate on that commit, {start_sha}, and the gate is red: read the output in {record}. The gate was green before the restore, so a restored file fails with the code of this ticket.
3
+ Update each restored file that fails so that it works with the new interface: keep what each test asserts, and list the file in `locked`. When no such update can keep what a test asserts, report BLOCKED with a concern that starts with `locked test contradicts spec:`, then the test and the evidence.
@@ -238,7 +238,6 @@ def test_a_fixture_correction_that_is_rejected_blocks_the_ticket(tmp_path):
238
238
 
239
239
  WEAK = "def test_x():\n assert True\n"
240
240
  LOCKED_01 = tw("01")["files"]["tests/test_01.py"]
241
- REFUSED_01 = "Ticket 02: BLOCKED at the merge: locked file changed: tests/test_01.py."
242
241
  CHEAT = impl("02", files={"demo/export.py": "def fn_02():\n return '02'\n", "tests/test_01.py": WEAK})
243
242
 
244
243
 
@@ -246,14 +245,15 @@ def on_pr(rig, path):
246
245
  return subprocess.run(["git", "-C", str(rig.repo), "show", f"feat/csv:{path}"], capture_output=True, text=True).stdout
247
246
 
248
247
 
249
- def test_a_later_ticket_that_weakens_a_locked_test_of_a_merged_ticket_does_not_land(tmp_path):
248
+ def test_a_later_ticket_that_weakens_a_locked_test_of_a_merged_ticket_lands_only_with_the_test_put_back(tmp_path):
250
249
  """Final review, correctness 3: ticket 02 replaced the locked assertion of ticket 01, merged and was pushed.
251
- Ticket 09: the check is at the landing, on the tree that is about to land, so the review has no such finding."""
250
+ Issue #90: the review finds the change; the restore keeps the gate green, so it closes the finding alone."""
252
251
  rig = Inside(tmp_path, three()).run(plan(**{"02/implementer": [CHEAT]}))
253
- assert blocked(rig, "02") == REFUSED_01 and rig.led("locked file changed") == [REFUSED_01], rig.ledger()
254
- assert json.loads((rig.folder("02") / "findings-0.json").read_text()) == [] and not rig.calls("02", "fixer")
255
- assert on_pr(rig, "tests/test_01.py") == LOCKED_01 and "feat: 02 rows" not in rig.pr_subjects()
256
- assert [rig.state()["tickets"][nn]["phase"] for nn in ("01", "03")] == ["merged", "merged"]
252
+ assert rig.code == 0, rig.ledger()
253
+ assert rig.led("Ticket 02: critical finding of kind fixture from the driver at tests/test_01.py:1")
254
+ assert rig.led("Ticket 02:", "the restore closes it") and not rig.led("locked file changed")
255
+ assert len(rig.calls("02", "test-writer")) == 1 and not rig.calls("02", "fixer")
256
+ assert on_pr(rig, "tests/test_01.py") == LOCKED_01 and "feat: 02 rows" in rig.pr_subjects()
257
257
 
258
258
 
259
259
  def extends(text, listed):
@@ -286,11 +286,11 @@ def test_the_test_writer_of_a_later_ticket_may_extend_the_locked_file_of_a_merge
286
286
 
287
287
 
288
288
  def test_a_refactor_ticket_may_not_change_a_locked_test_of_a_merged_ticket(tmp_path):
289
- """It has no test-writer and no locked file of its own: the landing check is the one that protects ticket 01."""
289
+ """It has no test-writer and no locked file of its own: the review finds the change, and the restore closes it."""
290
290
  files = three(**{"02-quote-fields.md": test_review.ticket("02", "Quote fields", "01", seam="none — refactor", criteria=())})
291
291
  rig = Inside(tmp_path, files).run(plan(**{"02/implementer": [CHEAT]}))
292
- assert blocked(rig, "02") == REFUSED_01 and not rig.calls("02", "test-writer"), rig.ledger()
293
- assert on_pr(rig, "tests/test_01.py") == LOCKED_01
292
+ assert rig.code == 0 and not rig.calls("02", "test-writer"), rig.ledger()
293
+ assert rig.led("Ticket 02:", "the restore closes it") and on_pr(rig, "tests/test_01.py") == LOCKED_01
294
294
  clean = Inside(under(tmp_path, "clean"), files).run(plan())
295
295
  assert clean.code == 0 and not clean.led("locked file changed"), clean.ledger()
296
296
 
@@ -362,6 +362,35 @@ def test_the_restore_before_a_correction_is_committed_so_the_next_fixer_and_its_
362
362
  assert "The driver put back these files at their locked versions" in prompt and "tests/test_01.py" in prompt
363
363
 
364
364
 
365
+ ADAPTED_01 = LOCKED_01 + "# adapted to the interface of 02\n"
366
+ ADAPTS = impl("02", files={"demo/export.py": "def fn_02():\n return '02'\n", "tests/test_01.py": ADAPTED_01})
367
+ RESTORE = "implement-spec: restore the locked files before the correction"
368
+
369
+
370
+ def writes(nn, text, subject):
371
+ """A test-writer correction that writes tests/test_<nn>.py and lists it."""
372
+ return {**tw(nn), "files": {f"tests/test_{nn}.py": text}, "commit": subject}
373
+
374
+
375
+ def test_a_restored_locked_file_of_a_merged_ticket_that_fails_with_the_new_code_must_be_updated_by_the_correction(tmp_path):
376
+ """Issue #90: ticket 02 changes an interface that the locked file of merged ticket 01 uses, and its implementer
377
+ adapts that file. The review finds the change, the driver puts the file back in a commit and gates it: red. The
378
+ correction gets that gate record, and a correction that leaves the file as it is is rejected."""
379
+ leaves = writes("02", "def test_x():\n assert '02' == 'x' # stronger\n", "test: 02 stronger")
380
+ updates = writes("01", ADAPTED_01, "test: 01 adapted to 02")
381
+ updates["report"] = {**updates["report"], "locked": ["tests/test_01.py", "tests/test_02.py"]} # both attempts' files
382
+ rig = Inside(tmp_path, three(), gate={"subjects": [RESTORE]}).run(
383
+ plan(**{"02/implementer": [ADAPTS], "02/test-writer": [tw("02"), leaves, updates]}))
384
+ assert rig.code == 0, rig.ledger()
385
+ assert rig.led("Ticket 02: critical finding of kind fixture from the driver at tests/test_01.py:1")
386
+ assert not rig.calls("02", "fixer")
387
+ (restore,) = [gate["commit"] for gate in rig.gates() if gate["exit"]]
388
+ first, second = rig.calls("02", "test-writer")[1:]
389
+ assert f".agent/implement/02/gate-{restore[:7]}.log" in first["prompt"] and first["head"] == restore
390
+ assert "the gate is red on the restore commit" in second["prompt"] # the rejection of the first attempt
391
+ assert on_pr(rig, "tests/test_01.py") == ADAPTED_01 and "tests/test_01.py" in rig.state()["tickets"]["02"]["locked"]
392
+
393
+
365
394
  ACCENT = "tests/tést.py"
366
395
 
367
396
 
@@ -374,7 +403,7 @@ def accented(tmp_path, name, **steps):
374
403
  return Inside(under(tmp_path, name), three()).run(plan(**{"01/test-writer": [writer], "01/reviewer": proofs, **steps}))
375
404
 
376
405
 
377
- def test_a_locked_file_with_a_name_that_git_quotes_is_protected_at_review_and_at_the_landing(tmp_path):
406
+ def test_a_locked_file_with_a_name_that_git_quotes_is_protected_at_review_for_its_ticket_and_a_later_one(tmp_path):
378
407
  """Review round 1, I2: the quoted form of the path matched nothing, so no check saw the file."""
379
408
  own = impl("01", files={"demo/export.py": "def fn_01():\n return '01'\n", ACCENT: WEAK})
380
409
  rig = accented(tmp_path, "review", **{"01/implementer": [own], "01/fixer": [{}, {}]})
@@ -383,7 +412,7 @@ def test_a_locked_file_with_a_name_that_git_quotes_is_protected_at_review_and_at
383
412
  later = impl("02", files={"demo/export.py": "def fn_02():\n return '02'\n", ACCENT: WEAK})
384
413
  rig = accented(tmp_path, "landing", **{"02/implementer": [later]})
385
414
  assert rig.state()["tickets"]["01"]["locked"] == ["tests/test_01.py", ACCENT], rig.ledger()
386
- assert blocked(rig, "02") == f"Ticket 02: BLOCKED at the merge: locked file changed: {ACCENT}."
415
+ assert rig.code == 0 and rig.led(f"Ticket 02: critical finding of kind fixture from the driver at {ACCENT}:1")
387
416
  assert "assert 1 == 'y'" in on_pr(rig, ACCENT)
388
417
 
389
418
 
@@ -31,7 +31,7 @@ import time
31
31
  import pytest
32
32
 
33
33
  from test_cli import RUN, git, models_file, snapshot, three, ticket
34
- from test_final import Final, ref
34
+ from test_final import Final
35
35
  from test_review import HOOKED, Review, cli
36
36
  from test_stops import UNATTENDED, blocked as contradicted
37
37
  from test_walk import GATE, Rig, impl, make_env, plan, tw
@@ -74,6 +74,13 @@ def refused(rig, nn, path):
74
74
  assert rig.led(nn, f"locked file changed: {path}"), rig.ledger()
75
75
 
76
76
 
77
+ def restored(rig, nn, path):
78
+ """The review of ticket `nn` found its change to the locked file `path` of a merged ticket, and the restore
79
+ closed it."""
80
+ return (rig.led(f"Ticket {nn}: critical finding of kind fixture from the driver at {path}:1")
81
+ and rig.led(f"Ticket {nn}:", "the restore closes it"))
82
+
83
+
77
84
  def writer(nn, path, text, listed=True):
78
85
  """The test-writer of `nn` that also writes `path`, and lists it as locked or not."""
79
86
  step = tw(nn)
@@ -92,13 +99,13 @@ def seeded(rig):
92
99
 
93
100
  # ---------------------------------------------------------------- the implementer of a later ticket
94
101
 
95
- def test_an_implementer_that_weakens_the_locked_test_of_a_merged_ticket_does_not_land_and_nothing_is_pushed(tmp_path):
102
+ def test_an_implementer_that_weakens_the_locked_test_of_a_merged_ticket_lands_only_with_the_test_put_back(tmp_path):
103
+ """The review finds the change and the driver restores the file; the gate stays green, so no correction runs."""
96
104
  rig = Final(tmp_path, three())
97
105
  weak = impl("02", files={"demo/export.py": "def fn_02():\n return '02'\n", T01: WEAK})
98
106
  rig.run(plan(**{"02/implementer": [weak], "02/fixer": NO_REPORT}), *UNATTENDED)
99
- refused(rig, "02", T01)
100
- assert phase(rig, "01") == "merged" and on_pr(rig, T01) == LOCKED_01
101
- assert rig.pushes() == [] and not rig.prs() and not ref(rig.origin, "refs/heads/feat/csv")
107
+ assert rig.code == 0 and restored(rig, "02", T01), rig.output()
108
+ assert len(rig.calls("02", "test-writer")) == 1 and on_pr(rig, T01) == LOCKED_01
102
109
 
103
110
 
104
111
  # ---------------------------------------------------------------- a ticket branch that takes the PR branch in
@@ -117,12 +124,12 @@ def test_a_ticket_that_merged_the_pr_branch_into_itself_lands_with_the_tests_of_
117
124
  assert phase(rig, "03") == "merged" and on_pr(rig, T01) == LOCKED_01 and on_pr(rig, T03) == LOCKED_03
118
125
 
119
126
 
120
- def test_a_ticket_that_merged_the_pr_branch_and_deleted_a_locked_test_of_the_merged_ticket_does_not_land(tmp_path):
127
+ def test_a_ticket_that_merged_the_pr_branch_and_deleted_a_locked_test_of_the_merged_ticket_lands_with_it_put_back(tmp_path):
121
128
  rig = Review(tmp_path, three())
122
129
  rig.run(plan(**{"03/implementer": [takes_in(["rm", "-q", T01])], "03/fixer": NO_REPORT}))
123
130
  (call,) = rig.calls("03", "implementer")
124
131
  assert holds(rig, rig.sha("01", "implementer"), call["sha"]), "01 is in ticket/03"
125
- refused(rig, "03", T01)
132
+ assert rig.code == 0 and restored(rig, "03", T01), rig.output()
126
133
  assert phase(rig, "01") == "merged" and on_pr(rig, T01) == LOCKED_01
127
134
 
128
135
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "generatedAt": "2026-10-16T22:28:03.132Z",
3
+ "generatedAt": "2026-10-16T23:46:48.180Z",
4
4
  "skills": [
5
5
  {
6
6
  "name": "backlog",
@@ -46,8 +46,8 @@
46
46
  },
47
47
  {
48
48
  "name": "implement-spec",
49
- "version": "0.1.19",
50
- "hash": "sha256-42055b9530838eca23d48593e153ff5064a601e3c60240fb68f23b1793ae7223",
49
+ "version": "0.1.20",
50
+ "hash": "sha256-9c85f3c37deb0c2e9715172ccdfbb71ef65c066aa8e8c1505d9d2e968b84f0fd",
51
51
  "description": "Implement a specification in code."
52
52
  },
53
53
  {