@jesdi/skills 0.0.45 → 0.0.47

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.45",
3
+ "version": "0.0.47",
4
4
  "description": "jesdi's agent skills (content package consumed by @jesdi/skills-cli)",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -98,8 +98,10 @@ and do not know about the run, except `driver_run.py`, which the three deciding
98
98
  ### Work is never dropped
99
99
 
100
100
  - A role that commits keeps its tree after a timeout or an interrupt; its rerun continues from it.
101
- - A read-only role (reviewer, correctness, final-reviewer, pr-writer) has no work to keep. When it changes its slot, the
102
- driver restores the slot and runs the review one more time (`Run.read_only`).
101
+ - A read-only role (reviewer, correctness, final-reviewer, pr-writer) has no work to keep. When it changes its slot or
102
+ the shared git config, the driver restores them and runs the review one more time (`Run.read_only`). The config is
103
+ a byte copy taken before the role (`driver_review.shared_config`): every worktree reads that one file, so a key a
104
+ role writes there, such as a `core.worktree` on a temp directory, breaks git in all of them.
103
105
  - A slot with uncommitted work of an interrupted or parked ticket is kept for that ticket (`driver_state.kept`).
104
106
  - A blocked ticket keeps its slot when the slot is not clean.
105
107
 
@@ -100,7 +100,7 @@ A ticket is ready when each ticket in its **Blocked by** line is merged and no r
100
100
  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.
101
101
  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.
102
102
  3. **Gate.** The driver runs `--gate` on the ticket tip.
103
- 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.
103
+ 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. A read-only role (reviewer, correctness, final-reviewer, pr-writer) leaves its slot and the git config as it found them: all worktrees share one config, and one key written there, such as a `core.worktree` on a temp directory, breaks git in each of them. To run a check off the slot, a reviewer reads the driver's gate record or makes its own `git worktree add --detach` copy and removes it. After read-only roles, the driver checks the slot and the shared config (`git rev-parse --git-common-dir`, then `config`) against what they were before. When one of them changed, the driver restores it, writes `a read-only role broke its rule: ...` to the ledger, and runs the review one more time; a second change blocks the ticket.
104
104
  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.
105
105
  **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.
106
106
  **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`.
@@ -178,7 +178,7 @@ Every report has `status` (`DONE`, `DONE_WITH_CONCERNS`, `NEEDS_CONTEXT`, `BLOCK
178
178
  |---|---|---|---|
179
179
  | 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`. |
180
180
  | implementer | `prompts/implementer.md` | `green` | With DONE: one commit or more. |
181
- | 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. |
181
+ | reviewer | `prompts/reviewer.md` | `findings` (`severity`, `file`, `line`, `title`, `evidence`, `kind`), `mutations` (`test`, `file`, `line`, `red`); `commits` is empty | In a full review (one with no fix range: round 1, and the review after a test-writer correction): `mutations` is required, with a proof for each locked test file (`test` is `<locked file>::<test name>`). Each `red` is in the log. |
182
182
  | correctness | `prompts/correctness.md` | the same as the reviewer; `commits` is empty | `mutations` is optional. |
183
183
  | fixer | `prompts/fixer.md` | `green`, `ticket` | With DONE: one commit or more. |
184
184
  | final-reviewer | `prompts/final-reviewer.md` | `findings` | None. |
@@ -207,7 +207,7 @@ Answer with `--answer <question id>=<option id>` on the next run, or through the
207
207
  | Check | Where it lives |
208
208
  |---|---|
209
209
  | 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. |
210
- | 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. |
210
+ | One mutation proof for each locked test | `prompts/full-review.md`, the note of each full review, names the locked test files that need a proof. `driver_report.py` `_locked_errors` and `_proof_errors` reject a full 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. |
211
211
  | 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. |
212
212
  | 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. |
213
213
  | Post-merge check on the PR branch | `driver.py` `_checked_landing`: a merge commit is gated before the PR branch moves to it. A fast-forward of a gated tip uses the record of that tip. `driver_final.py` `_review_until_reviewed` gates the tip again after the tidy and after the fix batch. |
@@ -217,7 +217,7 @@ The driver enforces the mutation proof for each locked test file, not for each l
217
217
  ## Known limits
218
218
 
219
219
  - **A `codex` agent cannot commit.** A role that commits (test-writer, implementer, fixer, conflict) on an `openai` entry runs with `--sandbox workspace-write`, and that sandbox cannot run `git commit`. In the default `models.json` this is the second implementer entry, so fix rounds 3 and 4 fail. What to do: give a `--models` file with no `openai` entry in the `implementer`, `test-writer`, `fixer` and `conflict` lists.
220
- - **A `codex` reviewer cannot make a mutation proof.** A reviewer on an `openai` entry runs with `--sandbox read-only` and cannot make the temporary edit. The default `models.json` has such an entry first for the reviewer, and the first review needs the proofs. What to do: give a `--models` file whose reviewer list starts with an `anthropic` entry.
220
+ - **A `codex` reviewer cannot make a mutation proof.** A reviewer on an `openai` entry runs with `--sandbox read-only` and cannot make the temporary edit. The default `models.json` has such an entry first for the reviewer, and each full review needs the proofs. What to do: give a `--models` file whose reviewer list starts with an `anthropic` entry.
221
221
  - **The correctness pass reads a tree that the reviewer changes.** The two agents share one slot, and the reviewer makes temporary edits. `prompts/correctness.md` tells the pass to read from git at the reviewed commit and to run no tests. It reads only the spec, the ticket and the findings file from the working tree, because git can lack them: `.agent/` is ignored. That rule is the only guard: the driver cannot enforce it, so a pass that ignores it can report a false finding and start a fix round. What to do: when a correctness finding looks wrong, compare it with `git show <reviewed commit>:<file>` before you rule on it.
222
222
  - **A ticket that is not merged stops the end of run.** While one ticket is BLOCKED, there is no tidy and no final review of the merged tickets.
223
223
  - **A published PR branch keeps its history.** See "End of run".
@@ -26,6 +26,7 @@ import driver_answers
26
26
  import driver_decisions
27
27
  import driver_final
28
28
  import driver_fixloop
29
+ import driver_gate
29
30
  import driver_graph
30
31
  import driver_legacy
31
32
  import driver_merge
@@ -109,7 +110,7 @@ def _run_roles(ticket: Ticket, slot_number: int, run: Run) -> None:
109
110
  values = {**_prompt_values(ticket, slot, run), "locked": " ".join(entry["locked"])}
110
111
  for role in roles:
111
112
  run.state.ticket(number, phase=DURING.get(role, "review"))
112
- values["start_sha"] = start
113
+ values.update(start_sha=start, gate_record=os.path.relpath(driver_gate.record_path(OUT / number, start), slot))
113
114
  # A test-writer on a ticket that has locked tests is the correction after a ruling.
114
115
  check = {"correction": role == "test-writer" and bool(entry["test_writer_sha"])}
115
116
  if check["correction"] and (restored := run.restore_locked_files(number, slot)):
@@ -26,6 +26,7 @@ from pathlib import Path
26
26
 
27
27
  import driver_agents
28
28
  import driver_decisions
29
+ import driver_gate
29
30
  import driver_merge
30
31
  import driver_review
31
32
  import driver_tidy
@@ -325,7 +326,7 @@ def _review(run: Run, final: dict, tip: str) -> None:
325
326
  """One round of the final review on `tip`: the full review, or after it the scoped one."""
326
327
  done, folder = final["round"], OUT / FINAL
327
328
  values = {**_prompt_values(run), "start_sha": tip, "range": f"{final['reviewed'] or final['onto']}..{tip}",
328
- "gate": str(folder / f"gate-{tip[:7]}.log"), "review_diff": str(REVIEW_DIFF),
329
+ "gate": str(driver_gate.record_path(folder, tip)), "review_diff": str(REVIEW_DIFF),
329
330
  "ledger": str(OUT / "ledger.md"), "minors": _minor_findings(), "reviewed": str(final["reviewed"]),
330
331
  "rulings": "\n".join(driver_decisions.rulings(run.state.read()["run"]["questions"])),
331
332
  "findings": str(folder / f"findings-{done - 1}.json")}
@@ -441,7 +442,7 @@ def _write_body(run: Run, final: dict, tickets: list[Ticket]) -> Path:
441
442
  raise Blocked("the pr skill is missing: the PR body follows it; install it with "
442
443
  "`npx skills add mattpocock/skills --skill pr`")
443
444
  values = {**_prompt_values(run), "start_sha": tip, "range": f"{final['onto']}..{tip}",
444
- "gate": str(OUT / FINAL / f"gate-{tip[:7]}.log"), "pr_skill": str(method), "facts": facts}
445
+ "gate": str(driver_gate.record_path(OUT / FINAL, tip)), "pr_skill": str(method), "facts": facts}
445
446
  with during("the PR body"):
446
447
  report = run.read_only(FINAL, ".", run.pr, tip, lambda: run.agent("pr-writer", FINAL, ".", values))
447
448
  body.write_text(f"{report['body'].strip()}\n\n{facts}")
@@ -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_gate
14
15
  import driver_merge
15
16
  import driver_review
16
17
  import driver_slots
@@ -39,9 +40,11 @@ class Job:
39
40
  """The values of a prompt; `review` is the round whose findings the agent reads."""
40
41
  entry = self.run.state.read()["tickets"][self.ticket.number]
41
42
  findings = os.path.relpath(OUT / self.ticket.number / f"findings-{review}.json", self.slot)
43
+ gate = os.path.relpath(driver_gate.record_path(OUT / self.ticket.number, self.tip), self.slot)
42
44
  return {**self.values, "start_sha": self.tip, "from_sha": self.previous, "round": str(self.round),
43
45
  "locked": " ".join(entry["locked"]), "test_writer_sha": str(entry["test_writer_sha"] or ""),
44
- "findings": findings, "proofs": " ".join(f"{file}:{line}" for file, line in sorted(self.again))}
46
+ "findings": findings, "gate_record": gate,
47
+ "proofs": " ".join(f"{file}:{line}" for file, line in sorted(self.again))}
45
48
 
46
49
 
47
50
  def entry(models: dict[str, list[str]], round: int) -> str:
@@ -63,12 +66,12 @@ def _run_reviewers(job: Job, cast: list[tuple[str, str]], locked: list[str]) ->
63
66
  errors: dict[str, str] = {}
64
67
 
65
68
  def call(role: str, entry: str) -> dict:
66
- # The first review of a tip must give a mutation proof for each locked test.
67
- first_review = not job.previous and role == "reviewer"
69
+ # A full review (no fix range) must give a mutation proof for each locked test file; the prompt names them.
70
+ full = not job.previous and role == "reviewer"
71
+ notes = ["rereview"] if job.previous else ["full-review"] if full else []
68
72
  try:
69
73
  return job.run.agent(role, job.ticket.number, job.slot, values, entry=entry, fix_round=job.round,
70
- notes=["rereview"] if job.previous else [],
71
- check={"round": 1, "locked": locked} if first_review else {},
74
+ notes=notes, check={"full": True, "locked": locked} if full else {},
72
75
  alone=lambda: len(running) == 1)
73
76
  except Blocked as blocked:
74
77
  errors[role] = f"the {role}: {blocked}"
@@ -25,12 +25,16 @@ def _after(slot: str, commit: str) -> str:
25
25
  return "not clean\n" + "".join(f" {line}\n" for line in status.splitlines()).rstrip("\n") if status else "clean"
26
26
 
27
27
 
28
+ def record_path(folder: Path, commit: str) -> Path:
29
+ return folder / f"gate-{commit[:7]}.log"
30
+
31
+
28
32
  def run(command: str, slot: str, folder: Path, pr_branch: str) -> tuple[bool, Path]:
29
33
  """Gate the HEAD of `slot`. Returns (green, the record)."""
30
34
  commit = head(slot)
31
35
  status = git(slot, "status", "--short").stdout
32
36
  folder.mkdir(parents=True, exist_ok=True)
33
- record = folder / f"gate-{commit[:7]}.log"
37
+ record = record_path(folder, commit)
34
38
  # The cache of compiled Python stays out of the slot, so the gate leaves the tree clean.
35
39
  env = dict(os.environ, IMPLEMENT_SPEC_BASE=pr_branch, PYTHONPYCACHEPREFIX=str(folder.parent.resolve() / "pycache"))
36
40
  with record.open("w") as out:
@@ -1,6 +1,6 @@
1
1
  """Report validation: validate(role, path, context) -> list of errors, empty when the report is accepted.
2
2
 
3
- context: repo (slot path), branch, start (HEAD when the role started), log, round (1 = first review), locked,
3
+ context: repo (slot path), branch, start (HEAD when the role started), log, full (a full review: no fix range), locked,
4
4
  correction (a test-writer corrects locked tests), restored (locked files the driver put back before it),
5
5
  restore_red (the gate is red on the commit of that restore),
6
6
  owners (ticket numbers, for the final fix batch), merged (locked files of merged tickets, not its own), pr (tip)."""
@@ -93,14 +93,14 @@ def _proof_errors(report: dict, context: dict) -> list[str]:
93
93
  errors = []
94
94
  if lines and not all(_seen(line, Path(context["log"]).read_text(errors="replace")) for line in lines):
95
95
  errors.append(RED)
96
- if context.get("round") == 1 and "findings" in report and "mutations" not in report:
97
- errors.append("mutations: required in round 1")
96
+ if context.get("full") and "findings" in report and "mutations" not in report:
97
+ errors.append("mutations: required in a full review")
98
98
  return errors
99
99
 
100
100
 
101
101
  def _locked_errors(report: dict, context: dict) -> list[str]:
102
- """In the first review, each locked test file has a mutation proof (`mutations[].test` is `<file>::<test>`)."""
103
- if context.get("round") != 1 or "findings" not in report:
102
+ """In a full review, each locked test file has a mutation proof (`mutations[].test` is `<file>::<test>`)."""
103
+ if not context.get("full") or "findings" not in report:
104
104
  return []
105
105
  tests = [proof["test"] for proof in report.get("mutations", [])]
106
106
  return [f"mutations: no mutation proof for the locked test file {path}" for path in context.get("locked", [])
@@ -7,6 +7,8 @@ A mutation proof is keyed by (production file, line); the line is the one the fi
7
7
  from __future__ import annotations
8
8
 
9
9
  import re
10
+ from pathlib import Path
11
+
10
12
  import driver_agents
11
13
  from driver_slots import git, head, names, on_branch
12
14
  from driver_tickets import Ticket
@@ -135,3 +137,23 @@ def disturbed(slot: str, branch: str, tip: str) -> str:
135
137
  return f"the slot is no longer on {branch} at {tip[:7]}"
136
138
  left = [line[3:] for line in git(slot, "status", "--porcelain").stdout.splitlines()]
137
139
  return f"the tree is not as the review found it: {', '.join(left)}" if left else ""
140
+
141
+
142
+ def _read(config: Path) -> bytes:
143
+ return config.read_bytes() if config.exists() else b""
144
+
145
+
146
+ def shared_config(slot: str) -> tuple[Path, bytes]:
147
+ """(the git config file that every worktree of the repository shares, its bytes)."""
148
+ common = git(slot, "rev-parse", "--path-format=absolute", "--git-common-dir").stdout.strip()
149
+ return Path(common, "config"), _read(Path(common, "config"))
150
+
151
+
152
+ def restore_config(before: tuple[Path, bytes]) -> str:
153
+ """"" when the shared git config is still `before` (from `shared_config`), else write it back and say so.
154
+ One worktree that writes the config breaks git in all of them, as a core.worktree on a gone directory does."""
155
+ config, data = before
156
+ if _read(config) == data:
157
+ return ""
158
+ config.write_bytes(data)
159
+ return f"the shared git config {config} changed"
@@ -120,15 +120,15 @@ class Run:
120
120
  def read_only(self, number: str, slot: str, branch: str, tip: str, review: Callable[[], T]) -> T:
121
121
  """Run `review` (read-only agents) and return its result. A review that changes the slot runs once more."""
122
122
  for last in (False, True):
123
- error = ""
123
+ error, config = "", driver_review.shared_config(slot)
124
124
  try:
125
125
  result = review()
126
126
  except Blocked as blocked:
127
127
  error = str(blocked)
128
128
  except Halted:
129
- _restore_if_changed(number, slot, branch, tip, runs_again=False)
129
+ _restore_if_changed(number, slot, branch, tip, config, runs_again=False)
130
130
  raise
131
- left = _restore_if_changed(number, slot, branch, tip, runs_again=not (error or last))
131
+ left = _restore_if_changed(number, slot, branch, tip, config, runs_again=not (error or last))
132
132
  if error or not left:
133
133
  break
134
134
  if error or left:
@@ -182,15 +182,21 @@ class Run:
182
182
  return paths
183
183
 
184
184
 
185
- def _restore_if_changed(number: str, slot: str, branch: str, tip: str, runs_again: bool) -> str:
186
- """After read-only agents: "" when the slot is as it was; else restore it and return what they left."""
187
- left = driver_review.disturbed(slot, branch, tip)
188
- if left:
185
+ def _restore_if_changed(number: str, slot: str, branch: str, tip: str, config: tuple[Path, bytes],
186
+ runs_again: bool) -> str:
187
+ """After read-only agents: "" when the slot and the shared git config (`driver_review.shared_config` before
188
+ them) are as they were; else restore both and return what they left."""
189
+ # The config first: a bad core.worktree in it breaks the git commands of the slot check.
190
+ changed = driver_review.restore_config(config)
191
+ moved = driver_review.disturbed(slot, branch, tip)
192
+ if moved:
189
193
  driver_review.restore(slot, branch, tip)
190
- where, what = ("slot 1", "the final review") if number == FINAL else ("the slot", "the review")
191
- again = f" and runs {what} again" if runs_again else ""
192
- ledger(f"{_owner(number)}: a read-only role broke its rule: {left}; the driver restored {where} to "
193
- f"{tip[:7]}{again}.")
194
+ left = "; ".join(filter(None, (changed, moved)))
195
+ if left:
196
+ where = ("slot 1" if number == FINAL else "the slot") + f" to {tip[:7]}"
197
+ restored = " and ".join(filter(None, ("the shared git config" if changed else "", where if moved else "")))
198
+ again = f" and runs {'the final review' if number == FINAL else 'the review'} again" if runs_again else ""
199
+ ledger(f"{_owner(number)}: a read-only role broke its rule: {left}; the driver restored {restored}{again}.")
194
200
  return left
195
201
 
196
202
 
@@ -274,7 +280,7 @@ def _attempts(run: Run, role: str, number: str, slot: str, values: dict[str, str
274
280
  folder.mkdir(parents=True, exist_ok=True)
275
281
  report, log, prompt = (folder / f"{role}-{fix_round}{suffix}" for suffix in (".json", ".log", ".prompt.md"))
276
282
  report_in_slot = os.path.relpath(report, slot) # the agent works in its slot
277
- context = {"repo": slot, "branch": values["branch"], "start": values["start_sha"], "log": str(log), "round": 0,
283
+ context = {"repo": slot, "branch": values["branch"], "start": values["start_sha"], "log": str(log),
278
284
  "template": template or role, **(check or {})}
279
285
  argv = _command(run, role, entry, os.path.relpath(prompt, slot), report_in_slot)
280
286
  text = _first_prompt(context["template"], values, notes, report_in_slot)
@@ -32,4 +32,6 @@ Write one JSON report at {report}. When your sandbox is read-only, give the JSON
32
32
  }
33
33
  ```
34
34
 
35
+ Run git as plain `git` in this worktree, and leave the git config as it is: all worktrees read one shared config, and one key written there, such as a `core.worktree`, breaks git in each of them. So never set GIT_DIR or GIT_WORK_TREE, and never write with `git config`.
36
+ The gate record of the driver for {start_sha} is {gate_record}.
35
37
  Never run `git stash`, never reset, and never check out another branch: worktrees share one stash and one ref store.
@@ -12,7 +12,7 @@ Minor findings that the ticket reviews left for this review, each with its findi
12
12
  Rulings of the operator:
13
13
  {rulings}
14
14
 
15
- Review the range with the method of the review-diff skill in the file above, with the spec as its spec path. The gate of that method is done: check that the gate record names {start_sha}, a clean status and exit 0, and run the gate yourself only when it does not.
15
+ Review the range with the method of the review-diff skill in the file above, with the spec as its spec path. The gate of that method is done: check that the gate record names {start_sha}, a clean status and exit 0, and run the gate yourself, in a copy as below, only when it does not.
16
16
  Decide for each minor finding in the list whether it is still true at {start_sha}; report each one that is, with the severity it has now. A ruling is settled: do not report what a ruling accepted.
17
17
  Give each finding a severity (critical, important or minor), the file and line, a title, the evidence, and a kind: `code`, `test`, or `fixture`.
18
18
  Do not commit. Leave the branch and the tree as you found them.
@@ -33,4 +33,6 @@ Write one JSON report at {report} that matches this schema:
33
33
  }
34
34
  ```
35
35
 
36
+ Run git as plain `git` in this worktree, and leave the git config as it is: all worktrees read one shared config, and one key written there, such as a `core.worktree`, breaks git in each of them. So never set GIT_DIR or GIT_WORK_TREE, and never write with `git config`.
37
+ To run a check off this worktree, read the gate record of the driver, {gate}, or make a copy with `git worktree add --detach <temp dir> {start_sha}`, run the check there, and remove the copy with `git worktree remove --force <temp dir>` before you report.
36
38
  Never run `git stash`, never reset, and never check out another branch: worktrees share one stash and one ref store.
@@ -0,0 +1,2 @@
1
+
2
+ This is a full review of {start_sha}: give one mutation proof or more for each of these locked test files: {locked}
@@ -28,4 +28,5 @@ Write one JSON report at {report} that matches this schema:
28
28
  }
29
29
  ```
30
30
 
31
+ Run git as plain `git` in this worktree, and leave the git config as it is: all worktrees read one shared config, and one key written there, such as a `core.worktree`, breaks git in each of them. So never set GIT_DIR or GIT_WORK_TREE, and never write with `git config`.
31
32
  Never run `git stash`, never reset, and never check out another branch: worktrees share one stash and one ref store.
@@ -8,7 +8,7 @@ Locked files: {locked}
8
8
  Test-writer report, when the ticket has a test-writer: {writer_report}
9
9
 
10
10
  Check the diff against the ticket and the spec, then for quality. Check that each criterion has a locked test. Test each suspicion that the ticket names in its Suspicions line.
11
- In round 1, for each locked test, break the production line the test protects, run the test, print its failing output, then revert the edit. A test that stays green is a critical finding. Run again each mutation that the test-writer report names in `concerns`.
11
+ To make a mutation proof for a locked test, break the production line the test protects, run the test, print its failing output, then revert the edit. The end of this prompt names the proofs that this review needs. A test that stays green is a critical finding. Run again each mutation that the test-writer report names in `concerns`.
12
12
  A mutation proof is keyed to its test and its production line: a later round repeats it only when that line changed.
13
13
  In `mutations`, give for each proof the test as `<locked file>::<test name>`, the production file and line, and one failure line copied exactly from the output you printed.
14
14
  Give each finding a severity (critical, important or minor), the file and line, a title, the evidence, and a kind: `fixture` when a locked test encodes a stale path, format or setup detail; `test` when a test is weak or missing, as a locked test that stays green when you break the line it protects; `code` for anything else. The test-writer corrects each `fixture` finding and each critical or important `test` finding on a locked file, and the fixer gets the rest.
@@ -26,9 +26,11 @@ Write one JSON report at {report}. When your sandbox is read-only, give the JSON
26
26
  "files": {"type": "array", "items": {"type": "string"}},
27
27
  "concerns": {"type": "array", "items": {"type": "string"}},
28
28
  "findings": {"type": "array", "items": {"type": "object", "required": ["severity", "file", "line", "title", "evidence", "kind"], "properties": {"severity": {"enum": ["critical", "important", "minor"]}, "file": {"type": "string"}, "line": {"type": "integer"}, "title": {"type": "string", "minLength": 1}, "evidence": {"type": "string", "minLength": 1}, "kind": {"enum": ["code", "fixture", "test"]}}}},
29
- "mutations": {"description": "Required in the first review of a tip: one proof or more for each locked test file, with `test` as `<locked file>::<test name>`. A review scoped to a fix gives the proofs that it names.", "type": "array", "items": {"type": "object", "required": ["test", "file", "line", "red"], "properties": {"test": {"type": "string", "minLength": 1}, "file": {"type": "string", "minLength": 1}, "line": {"type": "integer"}, "red": {"type": "string", "minLength": 1}}}}
29
+ "mutations": {"description": "Required in a full review: one proof or more for each locked test file, with `test` as `<locked file>::<test name>`. A review scoped to a fix gives the proofs that it names.", "type": "array", "items": {"type": "object", "required": ["test", "file", "line", "red"], "properties": {"test": {"type": "string", "minLength": 1}, "file": {"type": "string", "minLength": 1}, "line": {"type": "integer"}, "red": {"type": "string", "minLength": 1}}}}
30
30
  }
31
31
  }
32
32
  ```
33
33
 
34
+ Run git as plain `git` in this worktree, and leave the git config as it is: all worktrees read one shared config, and one key written there, such as a `core.worktree`, breaks git in each of them. So never set GIT_DIR or GIT_WORK_TREE, and never write with `git config`.
35
+ To run a check off this worktree, read the gate record of the driver, {gate_record}, or make a copy with `git worktree add --detach <temp dir> {start_sha}`, run the check there, and remove the copy with `git worktree remove --force <temp dir>` before you report.
34
36
  Never run `git stash`, never reset, and never check out another branch: worktrees share one stash and one ref store.
@@ -506,16 +506,38 @@ def test_a_first_review_proves_each_locked_file_and_a_second_bad_report_blocks(t
506
506
  assert "feat: 01 rows" not in rig.pr_subjects()
507
507
 
508
508
 
509
+ def test_a_full_review_after_a_correction_names_each_locked_file_that_needs_a_proof(tmp_path):
510
+ """Issue #92: the full review after a correction in a later round must prove each locked file, not only round 1."""
511
+ two = ("tests/test_01.py", "tests/test_01b.py")
512
+ first = {**tw("01"), "files": {path: "def test_x():\n assert 0\n" for path in two}}
513
+ first["report"] = {**first["report"], "files": list(two), "locked": list(two)}
514
+ proofs = [dict(test=f"{path}::test_x", file="demo/export.py", line=1, red=f"FAILED {path}::test_x") for path in two]
515
+ both = lambda found=(): {"stdout": "".join(p["red"] + "\n" for p in proofs),
516
+ "report": {**review("01", found)["report"], "mutations": proofs}}
517
+ vacuous = finding("VACUOUS", "critical", kind="test", file="tests/test_01.py")
518
+ correction = {**first, "files": {"tests/test_01.py": "# stronger\n"}, "commit": "test: 01 stronger test"}
519
+ correction["report"] = {**first["report"], "files": ["tests/test_01.py"], "locked": ["tests/test_01.py"]}
520
+ rig = Inside(tmp_path).run(plan(**{"01/test-writer": [first, correction], "01/fixer": [fix("01", 1)],
521
+ "01/reviewer": [both([STUCK]), review("01", [vacuous], lines=()), both()]}))
522
+ assert rig.code == 0, rig.ledger()
523
+ order = [call["role"] for call in rig.calls("01")]
524
+ assert order == ["test-writer", "implementer", "reviewer", "fixer", "reviewer", "test-writer", "reviewer"]
525
+ due = "give one mutation proof or more for each of these locked test files: tests/test_01.py tests/test_01b.py\n"
526
+ initial, again, full = (call["prompt"] for call in rig.calls("01", "reviewer"))
527
+ assert due in initial and due in full, full
528
+ assert "In round 1" not in full and due not in again
529
+
530
+
509
531
  def test_the_locked_file_rule_is_for_the_first_review_only(tmp_path):
510
532
  report = dict(status="DONE", commits=[], files=[], concerns=[], findings=[], mutations=[])
511
533
  path = tmp_path / "reviewer-0.json"
512
534
  path.write_text(json.dumps(report))
513
535
  context = {"repo": ".", "branch": "main", "start": "HEAD", "log": str(path), "locked": ["tests/a.py", "tests/b.py"]}
514
- assert driver_report.validate("reviewer", str(path), {**context, "round": 0}) == []
515
- assert driver_report.validate("reviewer", str(path), {**context, "round": 1}) == [
536
+ assert driver_report.validate("reviewer", str(path), context) == []
537
+ assert driver_report.validate("reviewer", str(path), {**context, "full": True}) == [
516
538
  "mutations: no mutation proof for the locked test file tests/a.py",
517
539
  "mutations: no mutation proof for the locked test file tests/b.py"]
518
- assert driver_report.validate("correctness", str(path), {**context, "round": 0}) == []
540
+ assert driver_report.validate("correctness", str(path), context) == []
519
541
 
520
542
 
521
543
  def test_a_timed_out_read_only_role_is_restored_only_when_no_other_agent_is_in_the_slot(tmp_path):
@@ -562,6 +584,23 @@ def test_a_second_review_that_changes_the_slot_blocks_the_ticket_with_the_slot_c
562
584
  assert slot_state(rig) == ("", rig.sha("01", "implementer")) and "feat: 01 rows" not in rig.pr_subjects()
563
585
 
564
586
 
587
+ def test_a_reviewer_that_writes_the_shared_git_config_gets_it_restored_and_runs_once_more(tmp_path):
588
+ # Issue #94: a reviewer ran git with GIT_DIR on the real git dir and left core.worktree on a deleted temp dir.
589
+ rig = Inside(tmp_path)
590
+ shared = rig.repo / ".git" / "config"
591
+ config = shared.read_bytes()
592
+ gone = {"git": [["config", "core.worktree", str(tmp_path / "gone")]]}
593
+ rig.run(plan(**{"01/reviewer": [{**review("01"), **gone}, review("01")]}))
594
+ assert rig.code == 0, rig.ledger()
595
+ assert shared.read_bytes() == config
596
+ (line,) = rig.led("Ticket 01: a read-only role broke its rule:")
597
+ assert f"the shared git config {shared.resolve()} changed" in line
598
+ assert line.endswith("the driver restored the shared git config and runs the review again.")
599
+ tip, prompt = rig.sha("01", "implementer"), rig.prompt("reviewer-0")
600
+ assert len(rig.calls("01", "reviewer")) == 2 and rig.tip() == tip
601
+ assert f".agent/implement/01/gate-{tip[:7]}.log" in prompt and "never write with `git config`" in prompt
602
+
603
+
565
604
  def test_a_reviewer_that_times_out_starts_again_from_the_clean_tip(tmp_path):
566
605
  hang = {"files": {"demo/export.py": "mutated\n", "junk.txt": "x\n"}, "sleep": 60}
567
606
  rig = Inside(tmp_path).run(plan(**{"01/reviewer": [hang, review("01")]}), "--timeout", "reviewer=0.03")
@@ -1,7 +1,7 @@
1
1
  """Report validation acceptance tests, through driver_report.validate only.
2
2
 
3
3
  context is a dict: repo (slot path), branch, start (HEAD sha when the role started),
4
- log (the agent's log file), round (fix-loop round, default 0).
4
+ log (the agent's log file), full (a full review of the reviewer).
5
5
  """
6
6
  import json
7
7
  import subprocess
@@ -45,7 +45,7 @@ def world(tmp_path):
45
45
  def check(role, report, **context):
46
46
  path = tmp_path / f"{role}-0.json"
47
47
  path.write_text(report if isinstance(report, str) else json.dumps(report))
48
- ctx = {"repo": str(root), "branch": "ticket/01", "log": str(tmp_path / "agent.log"), "round": 0}
48
+ ctx = {"repo": str(root), "branch": "ticket/01", "log": str(tmp_path / "agent.log")}
49
49
  return validate(role, str(path), {**ctx, **context})
50
50
  return shas, check, tmp_path
51
51
 
@@ -151,12 +151,12 @@ def test_red_line_absent_from_the_log_is_a_violation(world):
151
151
 
152
152
  def test_reviewer_fields_and_mutation_proofs(world):
153
153
  shas, check, _ = world
154
- assert check("reviewer", reviewer(), start=shas["impl"], round=1) == []
154
+ assert check("reviewer", reviewer(), start=shas["impl"], full=True) == []
155
155
  assert any("severity" in e for e in check("reviewer", reviewer(findings=[finding(severity="huge")]),
156
156
  start=shas["impl"]))
157
157
  assert any("kind" in e for e in check("reviewer", reviewer(findings=[finding(kind="style")]), start=shas["impl"]))
158
158
  no_proof = reviewer()
159
159
  del no_proof["mutations"]
160
- assert any("mutations" in e for e in check("reviewer", no_proof, start=shas["impl"], round=1))
160
+ assert any("mutations" in e for e in check("reviewer", no_proof, start=shas["impl"], full=True))
161
161
  fake = reviewer(mutations=[{"test": "t", "file": "demo/export.py", "line": 2, "red": "never ran"}])
162
- assert any("not found in log" in e for e in check("reviewer", fake, start=shas["impl"], round=1))
162
+ assert any("not found in log" in e for e in check("reviewer", fake, start=shas["impl"], full=True))
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "generatedAt": "2026-10-17T06:40:10.976Z",
3
+ "generatedAt": "2026-10-17T15:52:14.213Z",
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.21",
50
- "hash": "sha256-ffca735547ff5f7664f80fe98071d1956f839858ee4f1964ef9b7957af41a4cc",
49
+ "version": "0.1.23",
50
+ "hash": "sha256-c0a64ae43e2ba3c2e100b4d0a78085fdab558f8ee4eda01d7ae15eb677b8dbb5",
51
51
  "description": "Implement a specification in code."
52
52
  },
53
53
  {