@jesdi/skills 0.0.42 → 0.0.43

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.42",
3
+ "version": "0.0.43",
4
4
  "description": "jesdi's agent skills (content package consumed by @jesdi/skills-cli)",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -60,10 +60,14 @@ and do not know about the run, except `driver_run.py`, which the three deciding
60
60
  - The commit of the test-writer is the reference of the ticket's locked files. Before each review, a locked file
61
61
  that differs from its reference is a critical finding (`driver_review.locked_findings`).
62
62
  - A correction of the test-writer is a new reference. For that reason the driver first puts each locked file back
63
- to its locked version (`Run.restore_locked_files`), and a correction may change only the files it lists, after
64
- `overrule-test` and in the fix loop. Its commit becomes the reference of each file it lists. A locked file that it
65
- does not list and that differs from the old reference keeps the old reference in the ticket's `held`, so a
66
- change of the implementer stays a finding (`driver_review.written`).
63
+ to its locked version and commits that (`Run.restore_locked_files`), and a correction may change only the files it
64
+ lists, after `overrule-test` and in the fix loop. Its commit becomes the reference of each file it lists. A locked
65
+ file that it does not list and that differs from the old reference keeps the old reference in the ticket's `held`,
66
+ so a change of the implementer stays a finding (`driver_review.written`).
67
+ - The restore is a commit, not a tree change: an agent commits only its own files, and a gate refuses a tree that
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
+ prompt names the restored files (`prompts/restored.md`): this is the one path for a change of an earlier ticket's
70
+ locked test that a later ticket's interface needs.
67
71
  - The locked files of merged tickets are checked when a ticket lands (`driver_merge.refused`): the tree that lands
68
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.
69
73
  - A conflict resolution, of a merge or of the rebase, may not change a locked file.
@@ -114,6 +118,9 @@ and do not know about the run, except `driver_run.py`, which the three deciding
114
118
  it blocks only after round 4. A red gate on a test-writer correction blocks.
115
119
  - The implementer and each fixer get the `--gate` command and run it before they report. The gate of the driver
116
120
  stays the source of truth: an agent's run of the gate counts for nothing.
121
+ - No test-writer correction runs after round 4 (`driver_fixloop._act_on`): its findings become `finding` questions
122
+ with the others. A correction there would add red locked tests that no fix round makes green, and `accept` could
123
+ not merge the ticket.
117
124
 
118
125
  ### Questions
119
126
 
@@ -95,12 +95,12 @@ Everything is under `.agent/`, which is never committed.
95
95
  A ticket is ready when each ticket in its **Blocked by** line is merged and no running ticket touches the same place. The places are the backticked entries of the **Touches** lines: `file` or `file: function`. Two entries are the same place when they name the same file and the same function, or when one of them names the whole file. Each ready ticket takes the lowest free slot.
96
96
 
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
- 2. **implementer** (`prompts/implementer.md`). It starts from that commit and makes the locked tests green. Its prompt gives the `--gate` command: it runs the gate on its final commit and makes it green before it reports DONE or DONE_WITH_CONCERNS.
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
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.
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
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.
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, in the tree, each locked file of the ticket and each locked file of a merged ticket that the ticket changed, and the ledger names each file. A file of a merged ticket goes back to the version of the PR branch.
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
104
 
105
105
  Rules for each agent:
106
106
 
@@ -113,18 +113,18 @@ Rules for each agent:
113
113
 
114
114
  | Finding | What the driver does |
115
115
  |---|---|
116
- | kind `fixture`, any severity, or kind `test`, `critical` or `important`, on a locked file of the ticket | One test-writer correction, outside the fix rounds. The fixer may not edit a locked file, so a fix round cannot close a `test` finding there. The corrected files are the new locked versions, and the next review is a full review. A finding of this kind that is open after the correction blocks the ticket. |
116
+ | kind `fixture`, any severity, or kind `test`, `critical` or `important`, on a locked file of the ticket | One test-writer correction, outside the fix rounds. The fixer may not edit a locked file, so a fix round cannot close a `test` finding there. The corrected files are the new locked versions, and the next review is a full review. A finding of this kind that is open after the correction blocks the ticket. No correction runs after round 4. |
117
117
  | `critical` or `important` | One fix round. |
118
118
  | `minor` | It stays in the ledger and in the findings file. The final reviewer gets the list. |
119
119
 
120
- A fix round is one **fixer** (`prompts/fixer.md`), which runs the gate on its final commit before it reports, as the implementer does, then the gate of the driver on the new tip, then a re-review of the range of the fix. A mutation proof is repeated only when the fix changed its production line.
120
+ A fix round has three steps: the **fixer** (`prompts/fixer.md`) makes the gate green on its final commit, as the implementer does; the driver runs the gate on the new tip; a re-review reads the range of the fix. A mutation proof is repeated only when the fix changed its production line.
121
121
 
122
- A red gate on the commit of the implementer or of a fixer is a critical finding with source `gate`. Its file is the gate record, and the record holds the output. No review runs on that tip, and the next round starts: a red gate on the commit of the implementer starts round 1. The review after that round covers each fix since the last review and repeats each proof that one of them made due. A red gate after round 4 blocks the ticket, as does a red gate on the commit of a test-writer correction: the correction is outside the fix rounds, and a fixer may not edit the locked files.
122
+ A red gate on the commit of the implementer or of a fixer is a critical finding with source `gate`. Its file is the gate record, and the record holds the output. No review runs on that tip, and the next round starts: a red gate on the commit of the implementer starts round 1. The review after that round covers each fix since the last review and repeats each proof that one of them made due. Two red gates block the ticket: one after round 4, and one on the commit of a test-writer correction, because the correction is outside the fix rounds and a fixer may not edit the locked files.
123
123
 
124
124
  - Rounds 1 and 2 run on the first entry of the implementer list.
125
125
  - Rounds 3 and 4 run on the second entry of the implementer list. When the list has one entry, they stay on it and the ledger says so.
126
126
  - There is no ruling at round 2.
127
- - Findings that are open after round 4 make the ticket BLOCKED. The run ends with exit 1, and the driver writes one question of kind `finding` for each.
127
+ - Findings that are open after round 4 make the ticket BLOCKED. The run ends with exit 1, and the driver writes one question of kind `finding` for each. A finding that would get a test-writer correction is one of them: no correction runs after round 4, because its new locked tests would have no fix round to make them green and the merge gate would be red. So `accept` on each question merges the reviewed tip.
128
128
 
129
129
  ### End of run
130
130
 
@@ -107,8 +107,9 @@ def _run_roles(ticket: Ticket, slot_number: int, run: Run) -> None:
107
107
  values["start_sha"] = start
108
108
  # A test-writer on a ticket that has locked tests is the correction after a ruling.
109
109
  check = {"correction": role == "test-writer" and bool(entry["test_writer_sha"])}
110
- if check["correction"]:
111
- check["restored"] = run.restore_locked_files(number, slot)
110
+ if check["correction"] and (restored := run.restore_locked_files(number, slot)):
111
+ values.update(start_sha=driver_slots.head(slot), restored=" ".join(restored))
112
+ notes, check["restored"] = [*notes, "restored"], restored
112
113
  with during(f"the {role}"):
113
114
  report = run.agent(role, number, slot, values, notes=notes, check=check)
114
115
  facts = {"tip": driver_slots.head(slot)}
@@ -105,10 +105,13 @@ def _correct(job: Job, findings: list[dict]) -> None:
105
105
  if job.corrected:
106
106
  titles = "; ".join(finding["title"] for finding in findings)
107
107
  raise Blocked(f"the review: a locked-test finding is open after the test-writer correction: {titles}")
108
- check = {"correction": True, "restored": run.restore_locked_files(number, job.slot)}
108
+ restored = run.restore_locked_files(number, job.slot)
109
+ 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)
109
112
  with during("the test-writer correction"):
110
- report = run.agent("test-writer", number, job.slot, job.pointers(job.round), notes=["correction"],
111
- fix_round=job.round + 1, check=check)
113
+ report = run.agent("test-writer", number, job.slot, values, notes=notes, fix_round=job.round + 1,
114
+ check={"correction": True, "restored": restored})
112
115
  job.tip, job.previous, job.corrected = driver_slots.head(job.slot), "", True
113
116
  entry = run.state.read()["tickets"][number]
114
117
  run.state.ticket(number, tip=job.tip, **driver_review.written(job.slot, entry, report["locked"], job.tip))
@@ -146,6 +149,9 @@ def _act_on(job: Job, found: list[dict]) -> bool:
146
149
  """Do what the findings of a review ask for: a correction, a fix round, or a stop. True: none is open."""
147
150
  locked = job.run.state.read()["tickets"][job.ticket.number]["locked"]
148
151
  to_correct, to_fix = driver_review.route(found, locked)
152
+ if job.round == ROUNDS:
153
+ # No correction after round 4: no fix round could make its new locked tests green, so `accept` could not merge.
154
+ to_correct, to_fix = [], [finding for finding in found if finding in to_correct + to_fix]
149
155
  # A changed locked file is fixed first: a correction on top of it would make the changed file the reference.
150
156
  locked_file_changed = any(finding["source"] == "driver" for finding in found)
151
157
  if to_correct and not locked_file_changed:
@@ -8,6 +8,7 @@ from __future__ import annotations
8
8
  import argparse
9
9
  import json
10
10
  import os
11
+ import subprocess
11
12
  import threading
12
13
  import time
13
14
  from contextlib import contextmanager
@@ -23,7 +24,7 @@ import driver_report
23
24
  import driver_review
24
25
  import driver_slots
25
26
  import driver_state
26
- from driver_slots import git
27
+ from driver_slots import git, names
27
28
 
28
29
  OUT = Path(".agent/implement")
29
30
  FINAL = "final" # the end of run has its reports and gate records under this name, as a ticket has under its number
@@ -156,13 +157,23 @@ class Run:
156
157
 
157
158
  def restore_locked_files(self, number: str, slot: str) -> list[str]:
158
159
  """Before a test-writer correction: put each locked file the implementer changed back to its locked
159
- version in the tree, so the correction's commit carries no change of the implementer. Returns the paths."""
160
+ version in one commit of the driver, so the correction starts from the locked versions on a clean index and
161
+ its commit carries no change of the implementer. Other work in the tree stays. Returns the paths."""
160
162
  tickets = self.state.read()["tickets"]
161
163
  paths = (driver_review.restore_locked(slot, tickets[number])
162
164
  + driver_merge.restore(slot, self.tip(), driver_merge.protected(tickets, number)))
165
+ # A path whose change was never committed is back at HEAD already, and `git commit -- <path>` refuses it.
166
+ changed = names(git(slot, "diff", "--name-only", "-z", "HEAD", "--", *paths).stdout) if paths else []
167
+ if changed:
168
+ words = "implement-spec: restore the locked files before the correction"
169
+ made = subprocess.run(["git", "-C", slot, "commit", "-q", "--no-verify", "-m", words, "--", *changed],
170
+ capture_output=True, text=True, env={**os.environ, **driver_merge.identity("HEAD", slot)})
171
+ if made.returncode:
172
+ raise Blocked(f"the driver cannot commit the restored locked files: {made.stderr.strip()}")
173
+ self.state.ticket(number, tip=driver_slots.head(slot))
163
174
  for path in paths:
164
175
  ledger(f"Ticket {number}: {path} was not the locked version at the correction: the driver restored it in "
165
- "the tree, so the correction starts from the locked version.")
176
+ "a commit, so the correction starts from the locked version.")
166
177
  return paths
167
178
 
168
179
 
@@ -12,7 +12,7 @@ Fix each finding in the list as one batch, in one commit or in several. Leave th
12
12
  Each commit belongs to one ticket: the ticket whose criterion or code the commit changes. A fix for two tickets is two commits.
13
13
  Never edit a locked file: each one stays as it is at {start_sha}, and a locked file that differs is a critical finding of the next review.
14
14
  Use targeted tests while you work.
15
- Before you report DONE or DONE_WITH_CONCERNS, run the gate on your final commit, with IMPLEMENT_SPEC_BASE={pr_branch} in the environment: `{gate}`. Fix each failure, commit, and run it again until it is green. The driver runs the gate again on your final commit, and a scoped review reads your commits.
15
+ Before you report DONE or DONE_WITH_CONCERNS, make the gate green on your final commit: run `{gate}` with IMPLEMENT_SPEC_BASE={pr_branch} in the environment, then fix, commit, and run it again until it passes. The driver runs the gate again on that commit, and a scoped review reads your commits.
16
16
  Commit your work on the checked-out PR branch and change no commit that is on it now. Do not dispatch subagents.
17
17
  In `commits`, give every commit you made with the number of its ticket: the driver names each commit `fix(NN): <your subject>` from it, and a commit with no ticket is a violation. In `green`, give the summary lines of the passing test output.
18
18
 
@@ -11,7 +11,7 @@ Fix each finding of severity critical or important in the findings file. Leave t
11
11
  Never edit a locked file. The one exception is a finding `locked file changed`: make that file equal to its content at {test_writer_sha}.
12
12
  If a locked test contradicts the spec or the ticket, so that no fix can make it green, stop and report BLOCKED with a concern that starts with `locked test contradicts spec:`, then the test and the evidence.
13
13
  Use targeted tests while you work.
14
- Before you report DONE or DONE_WITH_CONCERNS, run the gate on your final commit, with IMPLEMENT_SPEC_BASE={pr_branch} in the environment: `{gate}`. Fix each failure, commit, and run it again until it is green. The driver runs the gate again on your final commit, and a re-review reads the lines you changed.
14
+ Before you report DONE or DONE_WITH_CONCERNS, make the gate green on your final commit: run `{gate}` with IMPLEMENT_SPEC_BASE={pr_branch} in the environment, then fix, commit, and run it again until it passes. The driver runs the gate again on that commit, and a re-review reads the lines you changed.
15
15
  A finding from the `gate` is a red gate: its file is the gate record. Read the output there and make the gate green.
16
16
  Commit your work on the checked-out ticket branch. Do not dispatch subagents.
17
17
  In `ticket`, give the number of the ticket that owns your commits. In `green`, give the summary lines of the passing test output.
@@ -9,8 +9,8 @@ Locked files: {locked}
9
9
  Make the locked tests green and meet each criterion of the ticket. For internal code, work in vertical test-first slices: one failing test, the smallest code that passes it, then the next slice.
10
10
  Never edit a locked file. If a locked test contradicts the spec or the ticket, stop and report BLOCKED with a concern that starts with `locked test contradicts spec:`, then the test and the evidence.
11
11
  Commit your work on the checked-out ticket branch. Do not dispatch subagents.
12
- Run targeted tests while you work.
13
- Before you report DONE or DONE_WITH_CONCERNS, run the gate on your final commit, with IMPLEMENT_SPEC_BASE={pr_branch} in the environment: `{gate}`. Fix each failure, commit, and run it again until it is green. The driver runs the gate again on your final commit.
12
+ Use targeted tests while you work.
13
+ Before you report DONE or DONE_WITH_CONCERNS, make the gate green on your final commit: run `{gate}` with IMPLEMENT_SPEC_BASE={pr_branch} in the environment, then fix, commit, and run it again until it passes. The driver runs the gate again on that commit.
14
14
  In `green`, give the summary lines of the passing test output.
15
15
 
16
16
  Write one JSON report at {report} that matches this schema:
@@ -0,0 +1,3 @@
1
+
2
+ The driver put back these files at their locked versions, in a commit before this call, because the implementer or a fixer had changed them: {restored}.
3
+ A fixer may not change a locked file. When a test in one of these files now fails only because this ticket changed an interface that the test uses, update that file so it works with the new interface: keep what each test asserts, and list the file in `locked`. Else leave the file as it is.
@@ -165,7 +165,7 @@ def two_locked():
165
165
 
166
166
 
167
167
  def restored(path):
168
- return f"Ticket 01: {path} was not the locked version at the correction: the driver restored it in the tree"
168
+ return f"Ticket 01: {path} was not the locked version at the correction: the driver restored it in a commit"
169
169
 
170
170
 
171
171
  TWO_TESTS = "def test_x():\n assert 01 == 'x'\n\n\ndef test_y():\n assert 1 == 'y'\n"
@@ -330,6 +330,22 @@ def test_round_4_findings_wait_for_every_answer_before_the_ticket_merges(tmp_pat
330
330
  assert rig.code == 0 and rig.state()["tickets"]["01"]["phase"] == "merged" and len(rig.calls("01", "fixer")) == 4
331
331
 
332
332
 
333
+ def test_a_test_finding_after_round_4_is_a_question_and_accepting_every_question_merges(tmp_path):
334
+ """Issue 84: after round 4 no test-writer correction runs. Its red locked tests would make `accept` a lie."""
335
+ locks = {**tw("01"), "files": {"tests/test_01.py": "# red lock\n"}, "commit": "test: 01 lock"}
336
+ rig = Inside(tmp_path, ONE, gate={"subjects": ["test: 01 lock"]})
337
+ vacuous = finding("VACUOUS", "critical", kind="test", file="tests/test_01.py", line=1)
338
+ last = review("01", [finding("STUCK"), vacuous])
339
+ rig.run(plan(**{"01/test-writer": [tw("01"), locks], "01/reviewer": [review("01", [finding("STUCK")])] * 4 + [last],
340
+ "01/fixer": [fix("01", n) for n in (1, 2, 3, 4)]}))
341
+ assert rig.code == 1 and len(rig.calls("01", "test-writer")) == 1
342
+ assert [q["subject"] for q in rig.state()["run"]["questions"]] == ["STUCK (demo/export.py:99)",
343
+ "VACUOUS (tests/test_01.py:1)"]
344
+ rig.run({}, "--answer", "01-finding-1=accept", "--answer", "01-finding-2=accept")
345
+ assert rig.code == 0, rig.ledger()
346
+ assert rig.state()["tickets"]["01"]["phase"] == "merged"
347
+
348
+
333
349
  def test_one_answer_that_keeps_the_ticket_blocked_wins_over_an_accept():
334
350
  one = asked("finding", subject="A")
335
351
  two = asked("finding", subject="B", before=[one])
@@ -296,15 +296,17 @@ def test_a_refactor_ticket_may_not_change_a_locked_test_of_a_merged_ticket(tmp_p
296
296
 
297
297
 
298
298
  # A correction is a test-writer: a file that it lists is its ticket's own. It must start from the locked version.
299
- APPEND = "--- a/tests/test_01.py\n+++ b/tests/test_01.py\n@@ -1,2 +1,3 @@\n def test_x():\n assert True\n+# corrected\n"
300
- RESTORED_01 = ("Ticket 02: tests/test_01.py was not the locked version at the correction: the driver restored it in "
301
- "the tree, so the correction starts from the locked version.")
299
+ # The edit has no context lines, so it applies to the file whatever version the tree holds.
300
+ APPEND = "--- a/tests/test_01.py\n+++ b/tests/test_01.py\n@@ -2,0 +3 @@\n+# corrected\n"
301
+ CORRECTED_01 = LOCKED_01 + "# corrected\n"
302
+ RESTORED_01 = ("Ticket 02: tests/test_01.py was not the locked version at the correction: the driver restored it in a "
303
+ "commit, so the correction starts from the locked version.")
302
304
 
303
305
 
304
306
  def appending(tmp_path, **changes):
305
- """A correction of 02 that appends a line to tests/test_01.py as the implementer of 02 left it, and lists it."""
307
+ """A correction of 02 that appends a line to tests/test_01.py as the tree holds it, and lists it."""
306
308
  (tmp_path / "append.patch").write_text(APPEND)
307
- step = {**tw("02"), "git": [["apply", str(tmp_path / "append.patch")]], "commit": "test: 02 corrected", **changes}
309
+ step = {**tw("02"), "git": [["apply", "--unidiff-zero", str(tmp_path / "append.patch")]], "commit": "test: 02 corrected", **changes}
308
310
  step["report"] = {**step["report"], "locked": ["tests/test_02.py", "tests/test_01.py"]}
309
311
  return step
310
312
 
@@ -325,7 +327,7 @@ def test_an_overrule_correction_starts_from_the_locked_file_of_the_merged_ticket
325
327
  rig = Inside(tmp_path, three()).run(steps)
326
328
  assert rig.code == 2, rig.ledger()
327
329
  rig.run(steps, "--answer", "02-locked-test-1=overrule-test")
328
- assert on_pr(rig, "tests/test_01.py") == LOCKED_01, rig.ledger()
330
+ assert on_pr(rig, "tests/test_01.py") == CORRECTED_01, rig.ledger()
329
331
  assert rig.led("Ticket 02:", "restored") == [RESTORED_01] and rig.code == 0
330
332
 
331
333
 
@@ -335,10 +337,31 @@ def test_a_fixture_correction_starts_from_the_locked_file_of_the_merged_ticket(t
335
337
  correction = appending(tmp_path, files={"tests/test_02.py": "def test_x():\n assert '02' == 'x' # corrected\n"})
336
338
  rig = Inside(tmp_path, three()).run(plan(**{"02/implementer": [CHEAT], "02/test-writer": [tw("02"), correction],
337
339
  "02/reviewer": [review("02", [stale]), proves("02", "tests/test_01.py")]}))
338
- assert on_pr(rig, "tests/test_01.py") == LOCKED_01, rig.ledger()
340
+ assert on_pr(rig, "tests/test_01.py") == CORRECTED_01, rig.ledger()
339
341
  assert rig.led("Ticket 02:", "restored") == [RESTORED_01] and rig.code == 0
340
342
 
341
343
 
344
+ def test_the_restore_before_a_correction_is_committed_so_the_next_fixer_and_its_gate_find_a_clean_tree(tmp_path):
345
+ """Issue #86: the correction and the fixer each commit only their own files. The restore of the merged ticket's
346
+ file must not stay in the tree, where the gate of the fixer's commit is `not clean` and burns a round."""
347
+ weak = finding("WEAK", kind="test", file="tests/test_02.py")
348
+ stronger = "def test_x():\n assert '02' == 'x' # stronger\n"
349
+ correction = {**tw("02"), "files": {"tests/test_02.py": stronger}, "commit": "test: 02 stronger",
350
+ "only": ["tests/test_02.py"]}
351
+ rig = Inside(tmp_path, three()).run(plan(**{
352
+ "02/implementer": [CHEAT], "02/test-writer": [tw("02"), correction],
353
+ "02/reviewer": [review("02", [weak, STUCK]), review("02")], "02/fixer": [fix("02", 1, only=["demo/export.py"])]}))
354
+ assert rig.code == 0, rig.ledger()
355
+ assert rig.led("Ticket 02:", "restored") == [RESTORED_01]
356
+ (fixer,) = rig.calls("02", "fixer")
357
+ assert fixer["status"] == "" and fixer["head"] == rig.calls("02", "test-writer")[1]["sha"]
358
+ assert not [gate for gate in rig.folder("02").glob("gate-*.log") if "not clean" in gate.read_text()]
359
+ assert rig.state()["tickets"]["02"]["round"] == 1 and on_pr(rig, "tests/test_01.py") == LOCKED_01
360
+ # The correction is told which files the driver put back, so it may update one compatibly when it lists it.
361
+ prompt = rig.prompt("test-writer-1", "02")
362
+ assert "The driver put back these files at their locked versions" in prompt and "tests/test_01.py" in prompt
363
+
364
+
342
365
  ACCENT = "tests/tést.py"
343
366
 
344
367
 
@@ -24,13 +24,16 @@ import pytest
24
24
  from test_cli import SKILL, ticket, three, git, models_file
25
25
  from test_walk import RUNNER, Rig, fix, impl, plan, tw
26
26
 
27
- # The fake runner of test_walk plus two hooks: `git` runs before the step's files, `git_after` right after them.
27
+ # The fake runner of test_walk plus three hooks: `git` runs before the step's files, `git_after` right after them,
28
+ # and `only` names the paths the commit takes, as an agent that commits its own files only.
28
29
  HOOKED = RUNNER.replace(
29
30
  'for path, text in step.get("files", {}).items():',
30
31
  'for cmd in step.get("git", []):\n git(*cmd)\nfor path, text in step.get("files", {}).items():', 1).replace(
31
32
  'sys.stdout.write(step.get("stdout", ""))',
32
- 'for cmd in step.get("git_after", []):\n git(*cmd)\nsys.stdout.write(step.get("stdout", ""))', 1)
33
- assert HOOKED.count("git_after") == 1 and HOOKED.count('step.get("git", [])') == 1
33
+ 'for cmd in step.get("git_after", []):\n git(*cmd)\nsys.stdout.write(step.get("stdout", ""))', 1).replace(
34
+ 'git("commit", "-q", "-m", step["commit"])',
35
+ 'git("commit", "-q", "-m", step["commit"], *(["--", *step["only"]] if "only" in step else []))', 1)
36
+ assert HOOKED.count("git_after") == 1 and HOOKED.count('step.get("git", [])') == 1 and HOOKED.count('step["only"]') == 1
34
37
 
35
38
  ONE = {"01-export-rows.md": ticket("01", "Export rows")}
36
39
  RISKY = {"02-quote-fields.md": ticket("02", "Quote fields", risk="persistence")}
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "generatedAt": "2026-10-16T10:22:13.328Z",
3
+ "generatedAt": "2026-10-16T22:28:03.132Z",
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.18",
50
- "hash": "sha256-b10cd79532e2c8a33ddb8c55934ee26ee7f98d104ff7805bc3e2b819d0bb9241",
49
+ "version": "0.1.19",
50
+ "hash": "sha256-42055b9530838eca23d48593e153ff5064a601e3c60240fb68f23b1793ae7223",
51
51
  "description": "Implement a specification in code."
52
52
  },
53
53
  {