@jesdi/skills 0.0.44 → 0.0.46

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.
Files changed (30) hide show
  1. package/package.json +1 -1
  2. package/skills/implement-spec/ARCHITECTURE.md +13 -6
  3. package/skills/implement-spec/README.md +9 -6
  4. package/skills/implement-spec/driver.py +82 -40
  5. package/skills/implement-spec/driver_final.py +9 -8
  6. package/skills/implement-spec/driver_fixloop.py +4 -1
  7. package/skills/implement-spec/driver_gate.py +5 -1
  8. package/skills/implement-spec/driver_legacy.py +92 -0
  9. package/skills/implement-spec/driver_merge.py +1 -5
  10. package/skills/implement-spec/driver_names.py +32 -0
  11. package/skills/implement-spec/driver_review.py +24 -2
  12. package/skills/implement-spec/driver_run.py +24 -13
  13. package/skills/implement-spec/driver_slots.py +13 -0
  14. package/skills/implement-spec/driver_state.py +12 -13
  15. package/skills/implement-spec/prompts/correctness.md +2 -0
  16. package/skills/implement-spec/prompts/final-reviewer.md +3 -1
  17. package/skills/implement-spec/prompts/pr-writer.md +1 -0
  18. package/skills/implement-spec/prompts/reviewer.md +2 -0
  19. package/skills/implement-spec/tests/test_agents.py +10 -10
  20. package/skills/implement-spec/tests/test_amendments.py +2 -2
  21. package/skills/implement-spec/tests/test_decisions.py +4 -4
  22. package/skills/implement-spec/tests/test_driver.py +183 -23
  23. package/skills/implement-spec/tests/test_final.py +5 -5
  24. package/skills/implement-spec/tests/test_fixloop.py +22 -5
  25. package/skills/implement-spec/tests/test_landing.py +26 -23
  26. package/skills/implement-spec/tests/test_legacy.py +139 -0
  27. package/skills/implement-spec/tests/test_stops.py +1 -1
  28. package/skills/implement-spec/tests/test_tidy.py +2 -2
  29. package/skills/implement-spec/tests/test_walk.py +10 -10
  30. package/skills-manifest.json +3 -3
@@ -7,8 +7,10 @@ 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
- from driver_slots import git, head, names
13
+ from driver_slots import git, head, names, on_branch
12
14
  from driver_tickets import Ticket
13
15
 
14
16
  # The correctness pass reads this file of the review-diff skill, which is installed next to this one.
@@ -131,7 +133,27 @@ def restore(slot: str, branch: str, tip: str) -> None:
131
133
 
132
134
  def disturbed(slot: str, branch: str, tip: str) -> str:
133
135
  """"" when the slot is still on `branch` at `tip` with a clean tree, else what the read-only agents left."""
134
- if git(slot, "symbolic-ref", "-q", "--short", "HEAD").stdout.strip() != branch or head(slot) != tip:
136
+ if on_branch(slot) != branch or head(slot) != tip:
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"
@@ -24,6 +24,7 @@ import driver_report
24
24
  import driver_review
25
25
  import driver_slots
26
26
  import driver_state
27
+ from driver_names import Names
27
28
  from driver_slots import git, names
28
29
 
29
30
  OUT = Path(".agent/implement")
@@ -88,6 +89,10 @@ class Run:
88
89
  asks: dict[str, Ask] = field(default_factory=dict) # ticket number -> its pending stop
89
90
  halted: list[str] = field(default_factory=list) # why the run starts no other agent; [] while it goes on
90
91
 
92
+ @property
93
+ def names(self) -> Names:
94
+ return Names(self.pr)
95
+
91
96
  def tip(self) -> str:
92
97
  return git(".", "rev-parse", f"refs/heads/{self.pr}").stdout.strip()
93
98
 
@@ -115,15 +120,15 @@ class Run:
115
120
  def read_only(self, number: str, slot: str, branch: str, tip: str, review: Callable[[], T]) -> T:
116
121
  """Run `review` (read-only agents) and return its result. A review that changes the slot runs once more."""
117
122
  for last in (False, True):
118
- error = ""
123
+ error, config = "", driver_review.shared_config(slot)
119
124
  try:
120
125
  result = review()
121
126
  except Blocked as blocked:
122
127
  error = str(blocked)
123
128
  except Halted:
124
- _restore_if_changed(number, slot, branch, tip, runs_again=False)
129
+ _restore_if_changed(number, slot, branch, tip, config, runs_again=False)
125
130
  raise
126
- 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))
127
132
  if error or not left:
128
133
  break
129
134
  if error or left:
@@ -177,22 +182,28 @@ class Run:
177
182
  return paths
178
183
 
179
184
 
180
- def _restore_if_changed(number: str, slot: str, branch: str, tip: str, runs_again: bool) -> str:
181
- """After read-only agents: "" when the slot is as it was; else restore it and return what they left."""
182
- left = driver_review.disturbed(slot, branch, tip)
183
- 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:
184
193
  driver_review.restore(slot, branch, tip)
185
- where, what = ("slot 1", "the final review") if number == FINAL else ("the slot", "the review")
186
- again = f" and runs {what} again" if runs_again else ""
187
- ledger(f"{_owner(number)}: a read-only role broke its rule: {left}; the driver restored {where} to "
188
- 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}.")
189
200
  return left
190
201
 
191
202
 
192
203
  def _reset_if_moved(slot: str, branch: str, tip: str) -> str:
193
204
  """After an agent: "" when the slot is on `branch` and still holds `tip`; else reset it and say what was wrong."""
194
- head = git(slot, "symbolic-ref", "-q", "--short", "HEAD").stdout.strip()
195
- if git(slot, "rev-parse", "-q", "--verify", f"refs/heads/{branch}").returncode:
205
+ head = driver_slots.on_branch(slot)
206
+ if not driver_slots.branch_exists(branch):
196
207
  problem = f"branch {branch} is missing"
197
208
  elif head != branch:
198
209
  problem = f"HEAD is on {head or 'a detached commit'} instead of {branch}"
@@ -22,6 +22,19 @@ def head(slot: str) -> str:
22
22
  return git(slot, "rev-parse", "HEAD").stdout.strip()
23
23
 
24
24
 
25
+ def on_branch(slot: str) -> str:
26
+ """The branch the slot has checked out; "" when its HEAD is detached."""
27
+ return git(slot, "symbolic-ref", "-q", "--short", "HEAD").stdout.strip()
28
+
29
+
30
+ def ancestor(commit: str, of: str) -> bool:
31
+ return git(".", "merge-base", "--is-ancestor", commit, of).returncode == 0
32
+
33
+
34
+ def branch_exists(branch: str) -> bool:
35
+ return git(".", "rev-parse", "-q", "--verify", f"refs/heads/{branch}").returncode == 0
36
+
37
+
25
38
  def path(number: int) -> str:
26
39
  return "." if number == 1 else f".agent/worktrees/slot-{number}"
27
40
 
@@ -1,6 +1,6 @@
1
1
  """state.json, the resume contract: one lock, every write a read-modify-write, published by rename.
2
2
 
3
- {"run": {revision, pr_branch, gates, stops, agents: [{ticket, role, entry, seconds, cost}], questions, start,
3
+ {"run": {revision, pr_branch, schema, gates, stops, agents: [{ticket, role, entry, seconds, cost}], questions, start,
4
4
  landed: [[ticket, the PR tip its merge made]], final},
5
5
  "tickets": {"NN": {phase, round, tip, test_writer_sha, locked, green_gates, slot, note, held}}}"""
6
6
  from __future__ import annotations
@@ -13,8 +13,10 @@ import uuid
13
13
  from pathlib import Path
14
14
  from typing import IO
15
15
 
16
- from driver_slots import git, path
16
+ from driver_names import Names
17
+ from driver_slots import ancestor, git, on_branch, path
17
18
 
19
+ SCHEMA = 2 # the version of this contract; 2: the names of driver_names. A run of an older driver has none.
18
20
  PHASES = ("pending", "test-writer", "implement", "gate", "review", "fix", "merge", "merged", "blocked")
19
21
 
20
22
 
@@ -42,7 +44,7 @@ class State:
42
44
  """Create the file for a new run, or add the tickets a resumed run does not know yet."""
43
45
  def change(state: dict) -> None:
44
46
  state.setdefault("run", {
45
- "revision": uuid.uuid4().hex, "pr_branch": pr_branch, "gates": 0, "stops": 0, "agents": [],
47
+ "revision": uuid.uuid4().hex, "pr_branch": pr_branch, "schema": SCHEMA, "gates": 0, "stops": 0, "agents": [],
46
48
  "questions": [], "start": git(".", "rev-parse", f"refs/heads/{pr_branch}").stdout.strip(),
47
49
  "landed": [], "final": _final()})
48
50
  for key, value in _final().items(): # a run of an older driver lacks the newer keys
@@ -67,22 +69,19 @@ class State:
67
69
  return self.update(lambda state: state["tickets"][number].update(fields))["tickets"][number]
68
70
 
69
71
 
70
- def lock(path: Path) -> IO | None:
71
- """The exclusive kernel lock on `path`, held while the returned file is open; None when another process has it."""
72
+ def lock(path: Path, shared: bool = False) -> IO | None:
73
+ """The kernel lock on `path`, exclusive or `shared`, held while the returned file is open; None when another
74
+ process has it in a way that excludes this one."""
72
75
  path.parent.mkdir(parents=True, exist_ok=True)
73
76
  held = path.open("a")
74
77
  try:
75
- fcntl.flock(held, fcntl.LOCK_EX | fcntl.LOCK_NB)
78
+ fcntl.flock(held, (fcntl.LOCK_SH if shared else fcntl.LOCK_EX) | fcntl.LOCK_NB)
76
79
  except BlockingIOError:
77
80
  held.close()
78
81
  return None
79
82
  return held
80
83
 
81
84
 
82
- def _ancestor(commit: str, of: str) -> bool:
83
- return git(".", "merge-base", "--is-ancestor", commit, of).returncode == 0
84
-
85
-
86
85
  def verified(entry: dict, branch: str, pr_branch: str) -> tuple[dict, str]:
87
86
  """(the fields to resume from, why) when git contradicts the entry; ({}, "") when it does not."""
88
87
  phase, tip = entry["phase"], str(entry["tip"])
@@ -90,7 +89,7 @@ def verified(entry: dict, branch: str, pr_branch: str) -> tuple[dict, str]:
90
89
  return {}, ""
91
90
  # The branch of a merged ticket is deleted, so its commits are trusted only while the PR branch has them.
92
91
  where, word = (pr_branch, "in") if phase == "merged" else (branch, "on")
93
- if _ancestor(tip, where):
92
+ if ancestor(tip, where):
94
93
  return {}, ""
95
94
  return {"phase": "implement", "tip": entry["test_writer_sha"]}, f"the recorded tip {tip[:7]} is not {word} {where}"
96
95
 
@@ -102,7 +101,7 @@ def kept(state: dict) -> dict[str, int]:
102
101
  for number, entry in state.get("tickets", {}).items():
103
102
  if (entry["phase"] in ("test-writer", "implement") or number in asking) and entry["slot"]:
104
103
  slot = path(entry["slot"])
105
- on = git(slot, "symbolic-ref", "-q", "--short", "HEAD").stdout.strip()
106
- if on == f"ticket/{number}" and git(slot, "status", "--porcelain").stdout.strip():
104
+ on = on_branch(slot)
105
+ if on == Names(state["run"]["pr_branch"]).ticket(number) and git(slot, "status", "--porcelain").stdout.strip():
107
106
  found[number] = entry["slot"]
108
107
  return found
@@ -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.
@@ -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.
@@ -31,4 +31,6 @@ Write one JSON report at {report}. When your sandbox is read-only, give the JSON
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.
@@ -1,13 +1,13 @@
1
1
  """Agent launch acceptance tests, through `sh run.sh` with a fake runner script or fake claude/codex on PATH.
2
2
 
3
3
  Interface choices (ticket 02): `--runner <script>` is called as `<script> <role> <prompt> <report>` in the slot
4
- directory. A plain run (no --dry-run) branches ticket/NN from the PR tip in slot 1, runs the test-writer (unless the
4
+ directory. A plain run (no --dry-run) branches ticket/feat%2Fcsv/NN from the PR tip in slot 1, runs the test-writer (unless the
5
5
  Seam is `none — refactor`) then the implementer; from ticket 03 on an accepted ticket goes on to the driver's gate (`true` here) and a
6
- merge into the PR branch, which deletes `ticket/NN`. Exit 0 when the ticket merges, 1 on BLOCKED.
6
+ merge into the PR branch, which deletes `ticket/feat%2Fcsv/NN`. Exit 0 when the ticket merges, 1 on BLOCKED.
7
7
  `--only <role>` runs that one role on the first ticket and stops. `--spec <path>` names the spec for the prompts.
8
8
  `--timeout <role>=<minutes>` (repeatable, fractions allowed) overrides a role timeout.
9
9
  Ticket 08: the fixture has no `origin`, and a run that merges its ticket ends with the tidy and the final review, so
10
- `ticket_commits()` reads the ticket's commits from `refs/implement/recovery`, the PR tip from before the tidy.
10
+ `ticket_commits()` reads the ticket's commits from `refs/implement-spec/feat%2Fcsv/recovery`, the PR tip from before the tidy.
11
11
  """
12
12
  import json
13
13
  import os
@@ -134,7 +134,7 @@ class World:
134
134
  return path.read_text() if path.exists() else ""
135
135
 
136
136
  def ticket_commits(self):
137
- made = subprocess.run(["git", "rev-parse", "--verify", "-q", "refs/implement/recovery"], cwd=self.repo,
137
+ made = subprocess.run(["git", "rev-parse", "--verify", "-q", "refs/implement-spec/feat%2Fcsv/recovery"], cwd=self.repo,
138
138
  capture_output=True, text=True).stdout.strip() or "feat/csv"
139
139
  return git(self.repo, "rev-list", "--reverse", "--no-merges", f"{self.base}..{made}").split()
140
140
 
@@ -175,7 +175,7 @@ def test_runner_is_called_with_role_prompt_report_in_the_slot_directory(env):
175
175
  assert role == "implementer" and call["prompt"].strip()
176
176
  assert Path(call["cwd"], report).resolve().as_posix().endswith(".agent/implement/01/implementer-0.json")
177
177
  assert Path(call["cwd"], report).exists()
178
- assert call["cwd"] == str(w.repo.resolve()) and call["branch"] == "ticket/01"
178
+ assert call["cwd"] == str(w.repo.resolve()) and call["branch"] == "ticket/feat%2Fcsv/01"
179
179
 
180
180
 
181
181
  def test_default_models_file_names_every_role():
@@ -231,7 +231,7 @@ def test_unknown_provider_or_effort_exits_3_naming_role_and_entry_before_any_age
231
231
  proc = w.run({"test-writer": [tw()]})
232
232
  assert proc.returncode == 3 and not w.calls("test-writer")
233
233
  assert not launches(w.tmp, "claude") and not launches(w.tmp, "codex")
234
- assert "ticket/01" not in git(w.repo, "branch", "--list")
234
+ assert "ticket/feat%2Fcsv/01" not in git(w.repo, "branch", "--list")
235
235
 
236
236
 
237
237
  def test_implementer_prompt_carries_pointers_and_only_template_sentences(env):
@@ -284,7 +284,7 @@ def test_a_model_the_account_cannot_use_fails_the_preflight_with_exit_3_before_a
284
284
  assert f"model entry openai/gpt-astra@medium: the codex CLI failed the preflight (exit 1): {REFUSED}" \
285
285
  in proc.stdout.splitlines(), proc.stdout
286
286
  assert not launches(w.tmp, "claude") and not launches(w.tmp, "codex")
287
- assert "ticket/01" not in git(w.repo, "branch", "--list") and not w.ledger()
287
+ assert "ticket/feat%2Fcsv/01" not in git(w.repo, "branch", "--list") and not w.ledger()
288
288
 
289
289
 
290
290
  def test_a_cli_that_fails_with_no_report_stops_the_run_with_exit_4_and_the_same_command_resumes(env):
@@ -368,16 +368,16 @@ def test_head_moved_to_the_pr_branch_resets_the_slot_and_reruns_once(env):
368
368
  ok(w.run({"test-writer": [tw()], "implementer": [{"git": [["checkout", "feat/csv"]]}, impl()]}))
369
369
  first, second = w.calls("implementer")
370
370
  tests_sha = w.ticket_commits()[0]
371
- assert (second["branch"], second["head"]) == ("ticket/01", tests_sha)
371
+ assert (second["branch"], second["head"]) == ("ticket/feat%2Fcsv/01", tests_sha)
372
372
  assert any("implementer" in l and "feat/csv" in l for l in w.ledger().splitlines()), w.ledger()
373
373
 
374
374
 
375
375
  def test_a_missing_ticket_branch_is_a_violation_named_in_the_ledger(env):
376
376
  w = World(env)
377
- gone = {"git": [["checkout", "--detach"], ["branch", "-D", "ticket/01"]]}
377
+ gone = {"git": [["checkout", "--detach"], ["branch", "-D", "ticket/feat%2Fcsv/01"]]}
378
378
  proc = w.run({"test-writer": [tw()], "implementer": [gone, impl()]})
379
379
  assert "Traceback" not in proc.stderr
380
- assert any("ticket/01" in l and "branch" in l for l in w.ledger().splitlines()), w.ledger()
380
+ assert any("ticket/feat%2Fcsv/01" in l and "branch" in l for l in w.ledger().splitlines()), w.ledger()
381
381
 
382
382
 
383
383
  def test_log_next_to_report_and_ledger_records_role_entry_duration_and_cost(env):
@@ -5,7 +5,7 @@ with and without its bare `origin`, and the rig of test_review with and without
5
5
 
6
6
  Interface choices (ticket 08):
7
7
  - with no `origin`, the tidied commits sit on the commit the PR branch had when the run started (nothing to rebase
8
- onto), and `refs/implement/recovery` holds the PR tip from before the tidy, as with an `origin`.
8
+ onto), and `refs/implement-spec/feat%2Fcsv/recovery` holds the PR tip from before the tidy, as with an `origin`.
9
9
  - the one ledger sentence about the missing remote names `origin` and says that the rebase and the push were skipped.
10
10
  - a refused start (exit 3) prints its reason on stdout or stderr: `origin` for a push that has no remote; for the
11
11
  correctness role one line that names `correctness` and `codex`, or `correctness` and each entry of its list.
@@ -39,7 +39,7 @@ def test_with_no_origin_the_branch_is_tidied_gated_and_reviewed_with_no_rebase_a
39
39
  rows = rig.commits(rig.start)
40
40
  assert [r["subject"] for r in rows] == ["feat(01): Export rows", "feat(02): Quote fields"]
41
41
  assert ref(rig.repo, f"{rows[0]['sha']}~1") == rig.start, "no rebase: the ticket commits sit on the branch's start"
42
- assert ref(rig.repo, "feat/csv^{tree}") == ref(rig.repo, "refs/implement/recovery^{tree}")
42
+ assert ref(rig.repo, "feat/csv^{tree}") == ref(rig.repo, "refs/implement-spec/feat%2Fcsv/recovery^{tree}")
43
43
  records = sorted(p.name for p in (rig.repo / ".agent/implement/final").glob("gate-*.log"))
44
44
  assert records == [f"gate-{tip[:7]}.log"], records
45
45
  (gate,) = [g for g in rig.gates() if g["commit"] == tip]
@@ -99,7 +99,7 @@ def test_an_answers_file_of_an_earlier_run_is_not_applied_to_the_same_question_o
99
99
  write_answers(rig, {Q1: "overrule-test"})
100
100
  # The operator starts again: the run directory goes, the answers file is outside it and stays.
101
101
  shutil.rmtree(rig.repo / ".agent/implement")
102
- git(rig.repo, "branch", "-D", "ticket/01")
102
+ git(rig.repo, "branch", "-D", "ticket/feat%2Fcsv/01")
103
103
  again = steps(blocked(), blocked(), impl("01"))
104
104
  rig.run(again)
105
105
  new = decisions(rig)
@@ -144,7 +144,7 @@ def test_a_stop_with_uncommitted_work_keeps_its_slot_and_takes_the_answer(tmp_pa
144
144
  rig.run(steps(dirty, impl("01")), *UNATTENDED)
145
145
  assert rig.code == 0, rig.ledger()
146
146
  second = rig.calls("01", "implementer")[1]
147
- assert second["status"] == "M demo/export.py" and second["branch"] == "ticket/01", "the work is in the tree of the rerun"
147
+ assert second["status"] == "M demo/export.py" and second["branch"] == "ticket/feat%2Fcsv/01", "the work is in the tree of the rerun"
148
148
  prompt = (rig.folder() / "implementer-0.prompt.md").read_text()
149
149
  assert "the locked tests stand as they are" in prompt and "uncommitted work" in prompt
150
150
  assert "feat: 01 rows" in rig.pr_subjects() and not git(rig.repo, "status", "--porcelain")
@@ -221,7 +221,7 @@ def test_a_correction_that_commits_more_than_the_files_it_lists_is_rejected(tmp_
221
221
  assert rig.code == 1, rig.ledger()
222
222
  assert rig.led("Ticket 01: BLOCKED at the test-writer: a correction may change only the files it lists in "
223
223
  "`locked`: its commits also change demo/export.py.")
224
- assert rig.led(restored(OTHER)) and git(rig.repo, "show", f"ticket/01:{OTHER}") == OTHER_TEST
224
+ assert rig.led(restored(OTHER)) and git(rig.repo, "show", f"ticket/feat%2Fcsv/01:{OTHER}") == OTHER_TEST
225
225
  assert len(rig.calls("01", "test-writer")) == 3 and len(rig.calls("01", "implementer")) == 1, "one rerun, no implementer"
226
226
  assert "its commits also change demo/export.py" in rig.prompt("test-writer-0")
227
227
  assert "feat: 01 rows" not in rig.pr_subjects() and rig.state()["tickets"]["01"]["held"] == {}
@@ -370,7 +370,7 @@ def test_unattended_writes_the_stage_file_and_prints_no_question(tmp_path, capsy
370
370
 
371
371
 
372
372
  def test_unattended_names_the_blocked_tickets_and_needs_a_stage_path_to_write(tmp_path):
373
- rig = Pushing(tmp_path, ONE, gate={"red": ["ticket/01"]})
373
+ rig = Pushing(tmp_path, ONE, gate={"red": ["ticket/feat%2Fcsv/01"]})
374
374
  rig.run(plan(), "--unattended")
375
375
  assert rig.code == 1 and not (rig.repo / ".agent/stage.json").exists()
376
376
  rig.run(plan(), "--unattended", "--stage-json", ".agent/out/stage.json")