@jesdi/skills 0.0.43 → 0.0.45

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.
@@ -1,7 +1,7 @@
1
- """Ticket 09 acceptance tests: locked tests are checked on the merge result; one driver per repository.
1
+ """Ticket 09 acceptance tests: locked tests are checked on the merge result; one driver per PR branch.
2
2
 
3
3
  All through `sh run.sh --runner <script> --parallel 2` on the three-ticket fixture repository (the rigs of test_review
4
- and test_final), and through two worktrees of one fixture repository for the run lock. Tickets are ordered with signal
4
+ and test_final), and through two worktrees of one fixture repository, on two PR branches, for the run lock. Tickets are ordered with signal
5
5
  files and with `wait_pr_moves`, never with a sleep.
6
6
 
7
7
  Interface choices (ticket 09):
@@ -17,9 +17,8 @@ Interface choices (ticket 09):
17
17
  commit stays open until the reviewer of the other ticket ended, plus a bounded time for that ticket to reach its
18
18
  landing. Either order of the two landings is correct, and so is a driver that gives the stale merge commit up;
19
19
  a landing after the lock with the weakened file is not, and a landing that is lost is not.
20
- - the lock sentence is the whole stdout of the second driver, one line, and it names the lock by a path that exists
21
- when it is resolved in the second worktree (an absolute path, or a path relative to that worktree).
22
- - "the second one starts" and "the lock is released": a driver in the other worktree runs its one ticket to exit 0.
20
+ - a driver on another PR branch in another worktree runs its one ticket to exit 0 while the first driver runs, and
21
+ changes no file and no ref of the first run. The lock of the same PR branch is in test_driver.
23
22
  - "never reported as an uncommitted change": no line of `git status --short` in either worktree has `lock` in it.
24
23
  """
25
24
  import json
@@ -30,8 +29,10 @@ import time
30
29
 
31
30
  import pytest
32
31
 
32
+ from driver_names import Names
33
+
33
34
  from test_cli import RUN, git, models_file, snapshot, three, ticket
34
- from test_final import Final, ref
35
+ from test_final import Final
35
36
  from test_review import HOOKED, Review, cli
36
37
  from test_stops import UNATTENDED, blocked as contradicted
37
38
  from test_walk import GATE, Rig, impl, make_env, plan, tw
@@ -74,6 +75,13 @@ def refused(rig, nn, path):
74
75
  assert rig.led(nn, f"locked file changed: {path}"), rig.ledger()
75
76
 
76
77
 
78
+ def restored(rig, nn, path):
79
+ """The review of ticket `nn` found its change to the locked file `path` of a merged ticket, and the restore
80
+ closed it."""
81
+ return (rig.led(f"Ticket {nn}: critical finding of kind fixture from the driver at {path}:1")
82
+ and rig.led(f"Ticket {nn}:", "the restore closes it"))
83
+
84
+
77
85
  def writer(nn, path, text, listed=True):
78
86
  """The test-writer of `nn` that also writes `path`, and lists it as locked or not."""
79
87
  step = tw(nn)
@@ -92,37 +100,37 @@ def seeded(rig):
92
100
 
93
101
  # ---------------------------------------------------------------- the implementer of a later ticket
94
102
 
95
- def test_an_implementer_that_weakens_the_locked_test_of_a_merged_ticket_does_not_land_and_nothing_is_pushed(tmp_path):
103
+ def test_an_implementer_that_weakens_the_locked_test_of_a_merged_ticket_lands_only_with_the_test_put_back(tmp_path):
104
+ """The review finds the change and the driver restores the file; the gate stays green, so no correction runs."""
96
105
  rig = Final(tmp_path, three())
97
106
  weak = impl("02", files={"demo/export.py": "def fn_02():\n return '02'\n", T01: WEAK})
98
107
  rig.run(plan(**{"02/implementer": [weak], "02/fixer": NO_REPORT}), *UNATTENDED)
99
- refused(rig, "02", T01)
100
- assert phase(rig, "01") == "merged" and on_pr(rig, T01) == LOCKED_01
101
- assert rig.pushes() == [] and not rig.prs() and not ref(rig.origin, "refs/heads/feat/csv")
108
+ assert rig.code == 0 and restored(rig, "02", T01), rig.output()
109
+ assert len(rig.calls("02", "test-writer")) == 1 and on_pr(rig, T01) == LOCKED_01
102
110
 
103
111
 
104
112
  # ---------------------------------------------------------------- a ticket branch that takes the PR branch in
105
113
 
106
114
  def takes_in(*more):
107
- """03's implementer in slot 2: it waits until the PR branch moved, merges it into ticket/03, then commits."""
115
+ """03's implementer in slot 2: it waits until the PR branch moved, merges it into ticket/feat%2Fcsv/03, then commits."""
108
116
  return {**impl("03", "demo/cli.py"), "wait_pr_moves": True, "git": [["merge", "-q", "--no-edit", "feat/csv"], *more]}
109
117
 
110
118
 
111
119
  def test_a_ticket_that_merged_the_pr_branch_into_itself_lands_with_the_tests_of_the_merged_ticket(tmp_path):
112
120
  rig = Review(tmp_path, three()).run(plan(**{"03/implementer": [takes_in()], "03/fixer": NO_REPORT}))
113
121
  (call,) = rig.calls("03", "implementer")
114
- assert call["cwd"] == str(rig.slot2) and holds(rig, rig.sha("01", "implementer"), call["sha"]), "01 is in ticket/03"
122
+ assert call["cwd"] == str(rig.slot2) and holds(rig, rig.sha("01", "implementer"), call["sha"]), "01 is in ticket/feat%2Fcsv/03"
115
123
  assert not rig.led("locked file changed"), rig.ledger()
116
124
  assert rig.code == 0, rig.output()
117
125
  assert phase(rig, "03") == "merged" and on_pr(rig, T01) == LOCKED_01 and on_pr(rig, T03) == LOCKED_03
118
126
 
119
127
 
120
- def test_a_ticket_that_merged_the_pr_branch_and_deleted_a_locked_test_of_the_merged_ticket_does_not_land(tmp_path):
128
+ def test_a_ticket_that_merged_the_pr_branch_and_deleted_a_locked_test_of_the_merged_ticket_lands_with_it_put_back(tmp_path):
121
129
  rig = Review(tmp_path, three())
122
130
  rig.run(plan(**{"03/implementer": [takes_in(["rm", "-q", T01])], "03/fixer": NO_REPORT}))
123
131
  (call,) = rig.calls("03", "implementer")
124
- assert holds(rig, rig.sha("01", "implementer"), call["sha"]), "01 is in ticket/03"
125
- refused(rig, "03", T01)
132
+ assert holds(rig, rig.sha("01", "implementer"), call["sha"]), "01 is in ticket/feat%2Fcsv/03"
133
+ assert rig.code == 0 and restored(rig, "03", T01), rig.output()
126
134
  assert phase(rig, "01") == "merged" and on_pr(rig, T01) == LOCKED_01
127
135
 
128
136
 
@@ -290,44 +298,46 @@ def until(done, seconds=30):
290
298
  time.sleep(0.02)
291
299
 
292
300
 
293
- def world(*rigs):
294
- """Every file of each worktree, and every ref and worktree HEAD of the one repository."""
295
- return ([snapshot(r.repo) for r in rigs], git(rigs[0].repo, "for-each-ref"), git(rigs[0].repo, "worktree", "list"))
296
-
297
-
298
301
  def no_lock_is_dirty(*rigs):
299
302
  for rig in rigs:
300
303
  status = git(rig.repo, "status", "--short")
301
304
  assert "lock" not in status, status
302
305
 
303
306
 
304
- def test_a_second_driver_in_another_worktree_exits_3_naming_the_lock_and_starts_when_the_first_ended(tmp_path):
307
+ def first_run_refs(rig):
308
+ """The refs of the run on feat/csv: its PR branch, its ticket branches and its recovery ref."""
309
+ names = Names("feat/csv")
310
+ refs = git(rig.repo, "for-each-ref", "--format=%(refname) %(objectname)").splitlines()
311
+ return [ref for ref in refs if ref.split()[0] in ("refs/heads/feat/csv", names.recovery)
312
+ or ref.startswith(f"refs/heads/{names.ticket('')}")]
313
+
314
+
315
+ def test_a_driver_on_another_pr_branch_in_another_worktree_runs_while_the_first_runs(tmp_path):
316
+ """Runs on different PR branches share no state.json, no slot and no ref: the second one ends while the first
317
+ holds both its slots, and leaves each file and each ref of the first as it was."""
305
318
  first, second = pair(tmp_path)
306
319
  held = {f"{nn}/test-writer": [{**tw(nn), "wait": ["go"]}] for nn in ("01", "03")}
307
320
  proc = start(first, plan(**held))
308
321
  try:
309
322
  until(lambda: len(first.calls(role="test-writer")) == 2) # the first driver holds both its slots in a role
310
- before = world(first, second)
323
+ before = (snapshot(first.repo), first_run_refs(first))
324
+ assert [ref for ref in before[1] if "/ticket/" in ref], before[1] # the first run has its ticket branches
311
325
  second.run(OTHER)
312
- assert second.code == 3, second.output()
313
- (sentence,) = second.out.splitlines()
314
- named = [word.rstrip(".,;:") for word in sentence.split() if "/" in word or word.rstrip(".,;:").endswith("lock")]
315
- assert "lock" in sentence and any((second.repo / word).is_file() for word in named), sentence
316
- assert world(first, second) == before and not second.calls()
326
+ assert second.code == 0 and second.calls("04", "implementer"), second.output()
327
+ assert (snapshot(first.repo), first_run_refs(first)) == before
328
+ assert not git(second.repo, "branch", "--list", "ticket/feat%2Fother/*") # the merge deleted ticket 04's branch
317
329
  no_lock_is_dirty(first, second)
318
330
  (first.rig / "sig/go").touch()
319
331
  out, _ = proc.communicate(timeout=120)
320
332
  finally:
321
333
  ended(proc)
322
334
  assert proc.returncode == 0, out
323
- second.run(OTHER)
324
- assert second.code == 0 and second.calls("04", "implementer"), second.output()
325
335
  no_lock_is_dirty(first, second)
326
336
 
327
337
 
328
338
  ENDS = {
329
339
  0: (plan(), {}, ()),
330
- 1: (plan(), {"red": ["ticket/01"]}, ()),
340
+ 1: (plan(), {"red": ["ticket/feat%2Fcsv/01"]}, ()),
331
341
  2: (plan(**{"01/implementer": [contradicted()]}), {}, ()),
332
342
  3: (plan(), {}, ("--prepare", "false")), # slot 2 cannot be made: the run has started and holds the lock
333
343
  130: (plan(**{"01/test-writer": [{**tw("01"), "wait": ["never"]}]}), {}, ()),
@@ -0,0 +1,139 @@
1
+ """The names of a run, and the take-over of a run that an older driver started (driver_names, driver_legacy)."""
2
+ import subprocess
3
+
4
+ import driver_legacy
5
+ import driver_state
6
+ from conftest import GIT
7
+ from driver_names import Names
8
+
9
+
10
+ def sha(ref="HEAD"):
11
+ return subprocess.run(["git", "rev-parse", "-q", "--verify", ref], capture_output=True, text=True).stdout.strip()
12
+
13
+
14
+ def test_the_names_of_a_run_carry_its_pr_branch_as_one_ref_component(repo, tmp_path):
15
+ """Structure review: git refs are a tree, so `ticket/feat/01` and `ticket/feat/01/02` cannot both exist. The PR
16
+ branch is one escaped component, so runs on two PR branches never collide."""
17
+ names = Names("feat/x")
18
+ assert (names.ticket("01"), names.recovery) == ("ticket/feat%2Fx/01", "refs/implement-spec/feat%2Fx/recovery")
19
+ assert names.lock_file(tmp_path) == tmp_path / "implement-spec/feat%2Fx.lock"
20
+ assert Names("a%2Fb").ticket("01") != Names("a/b").ticket("01")
21
+ repo()
22
+ for name in (Names("feat").ticket("01"), Names("feat/01").ticket("02"), Names("x/recovery").ticket("01")):
23
+ subprocess.run(["git", "branch", name], check=True)
24
+ subprocess.run(["git", "update-ref", "refs/implement/recovery", "HEAD"], check=True) # of an older driver
25
+ for pr in ("feat", "feat/recovery", "recovery"):
26
+ subprocess.run(["git", "update-ref", Names(pr).recovery, "HEAD"], check=True)
27
+ assert sha(Names("feat/recovery").recovery) == sha()
28
+
29
+
30
+ def older_run(repo, tmp_path):
31
+ """A repository with the start of a run, its recorded tip on `ticket/01` and the state.json of an older driver:
32
+ no `schema`."""
33
+ repo()
34
+ start = sha()
35
+ subprocess.run([*GIT, "commit", "-q", "--allow-empty", "-m", "work"], check=True)
36
+ tip = sha()
37
+ subprocess.run(["git", "branch", "ticket/01", tip], check=True)
38
+ state = driver_state.State(tmp_path / ".agent/implement/state.json")
39
+ state.update(lambda s: s.update(
40
+ run={"pr_branch": "feat/x", "start": start, "final": {"recovery": None}},
41
+ tickets={"01": {"phase": "review", "tip": tip}, "02": {"phase": "pending", "tip": None}}))
42
+ return state, start, tip
43
+
44
+
45
+ def resume(state, tmp_path):
46
+ """driver_legacy.resume; (the errors, the ledger lines)."""
47
+ errors = driver_legacy.resume(state)
48
+ ledger = tmp_path / ".agent/implement/ledger.md"
49
+ return errors, ledger.read_text().splitlines() if ledger.exists() else []
50
+
51
+
52
+ def edit(state, ticket=None, **run):
53
+ """Change the record of the run and of ticket 01, and take the `schema` away: the run is the older driver's
54
+ again."""
55
+ state.update(lambda s: (s["run"].update(run), s["run"].pop("schema", None), s["tickets"]["01"].update(ticket or {})))
56
+
57
+
58
+ def test_resume_renames_the_ticket_branches_of_this_run_only_and_marks_the_run(repo, tmp_path):
59
+ """A `ticket/NN` is this run's only when it holds the start of the run and the recorded tip: a `ticket/NN` of
60
+ another run stays as it is."""
61
+ state, start, tip = older_run(repo, tmp_path)
62
+ subprocess.run(["git", "branch", "-f", "ticket/01", start], check=True)
63
+ assert resume(state, tmp_path) == ([], []) # it does not hold the recorded tip
64
+ subprocess.run([*GIT, "checkout", "-q", "--orphan", "other"], check=True)
65
+ subprocess.run([*GIT, "commit", "-q", "--allow-empty", "-m", "other run"], check=True)
66
+ subprocess.run(["git", "branch", "-f", "ticket/01", "other"], check=True)
67
+ edit(state, {"tip": None})
68
+ assert resume(state, tmp_path) == ([], []) # it does not hold the start of the run
69
+ subprocess.run(["git", "branch", "-f", "ticket/01", tip], check=True)
70
+ for phase in ("pending", "merged"):
71
+ edit(state, {"tip": tip, "phase": phase})
72
+ assert resume(state, tmp_path) == ([], [])
73
+ edit(state, {"phase": "review"})
74
+ assert resume(state, tmp_path) == (
75
+ [], ["Ticket 01: renamed the branch ticket/01 of an older driver to ticket/feat%2Fx/01."])
76
+ assert (sha("refs/heads/ticket/01"), sha("refs/heads/ticket/feat%2Fx/01")) == ("", tip)
77
+ assert state.read()["run"]["schema"] == driver_state.SCHEMA
78
+ subprocess.run(["git", "branch", "ticket/01", tip], check=True)
79
+ assert resume(state, tmp_path)[0] == [] and sha("refs/heads/ticket/01") == tip # a marked run is not touched
80
+
81
+
82
+ def test_resume_keeps_a_ticket_branch_that_a_worktree_outside_the_run_has_checked_out(repo, tmp_path):
83
+ """Correctness review: a common start does not prove whose branch it is. A `ticket/NN` that another worktree has
84
+ checked out is that worktree's."""
85
+ state, _, tip = older_run(repo, tmp_path)
86
+ subprocess.run(["git", "worktree", "add", "-q", str(tmp_path.parent / f"{tmp_path.name}-other"), "ticket/01"],
87
+ check=True)
88
+ assert resume(state, tmp_path) == ([], []) and sha("refs/heads/ticket/01") == tip
89
+ edit(state)
90
+ subprocess.run(["git", "worktree", "remove", "--force", str(tmp_path.parent / f"{tmp_path.name}-other")],
91
+ check=True)
92
+ subprocess.run(["git", "worktree", "add", "-q", str(tmp_path / ".agent/worktrees/slot-2"), "ticket/01"],
93
+ check=True)
94
+ assert resume(state, tmp_path)[1] == ["Ticket 01: renamed the branch ticket/01 of an older driver to "
95
+ "ticket/feat%2Fx/01."] # a slot of this run
96
+
97
+
98
+ def test_resume_stops_on_a_rename_that_git_refuses_and_loses_nothing(repo, tmp_path):
99
+ state, _, _ = older_run(repo, tmp_path)
100
+ subprocess.run(["git", "branch", "ticket/feat%2Fx"], check=True) # a branch where the folder of the new name goes
101
+ errors, ledger = resume(state, tmp_path)
102
+ assert ledger == [] and len(errors) == 1 and "schema" not in state.read()["run"]
103
+ assert errors[0].startswith("cannot rename the branch ticket/01 of an older driver to ticket/feat%2Fx/01: ")
104
+ assert sha("refs/heads/ticket/01")
105
+
106
+
107
+ def test_resume_moves_the_recovery_ref_that_this_run_recorded(repo, tmp_path):
108
+ state, start, tip = older_run(repo, tmp_path)
109
+ subprocess.run(["git", "branch", "-D", "-q", "ticket/01"], check=True)
110
+ subprocess.run(["git", "update-ref", "refs/implement/recovery", start], check=True)
111
+ edit(state, final={"recovery": tip})
112
+ assert resume(state, tmp_path) == ([], []) # it holds another commit: not this run's
113
+ edit(state, final={"recovery": start})
114
+ assert resume(state, tmp_path) == (
115
+ [], ["Recovery point: moved refs/implement/recovery of an older driver to refs/implement-spec/feat%2Fx/recovery."])
116
+ assert (sha("refs/implement/recovery"), sha("refs/implement-spec/feat%2Fx/recovery")) == ("", start)
117
+
118
+
119
+ def test_resume_keeps_the_recovery_ref_when_git_refuses_the_new_one(repo, tmp_path):
120
+ state, start, _ = older_run(repo, tmp_path)
121
+ subprocess.run(["git", "branch", "-D", "-q", "ticket/01"], check=True)
122
+ subprocess.run(["git", "update-ref", "refs/implement/recovery", start], check=True)
123
+ subprocess.run(["git", "update-ref", "refs/implement-spec/feat%2Fx", start], check=True) # a file where a folder goes
124
+ edit(state, final={"recovery": start})
125
+ errors, ledger = resume(state, tmp_path)
126
+ assert ledger == [] and errors[0].startswith("cannot move refs/implement/recovery of an older driver to ")
127
+ assert sha("refs/implement/recovery") == start
128
+
129
+
130
+ def test_the_lock_of_an_older_driver_is_shared_by_newer_drivers_and_excludes_an_older_one(tmp_path):
131
+ """Correctness review: an older driver holds `implement-spec.lock` alone for its whole run."""
132
+ (one, _), (two, _) = driver_legacy.lock(tmp_path), driver_legacy.lock(tmp_path)
133
+ with one, two:
134
+ assert driver_state.lock(tmp_path / "implement-spec.lock") is None
135
+ older = driver_state.lock(tmp_path / "implement-spec.lock")
136
+ assert older is not None and driver_legacy.lock(tmp_path) == (None, (
137
+ f"a driver of an older version runs in this repository: it holds the lock {tmp_path / 'implement-spec.lock'}. "
138
+ "Wait until it ends, then run the driver again."))
139
+ older.close()
@@ -266,7 +266,7 @@ def test_unattended_rerun_with_a_submitted_answers_file_at_the_current_revision_
266
266
 
267
267
  def test_unattended_blocked_ticket_ends_with_stage_json_blocked_naming_it_and_exit_1(tmp_path):
268
268
  from test_final import Final
269
- rig = Final(tmp_path, gate={"red": ["ticket/01"]})
269
+ rig = Final(tmp_path, gate={"red": ["ticket/feat%2Fcsv/01"]})
270
270
  rig.run(plan(), *UNATTENDED)
271
271
  assert rig.code == 1, rig.output()
272
272
  assert stage(rig) == {"stage": "implement", "status": "blocked", "blocked": ["01"]}
@@ -78,7 +78,7 @@ def test_a_local_run_stops_before_the_push_and_a_push_rerun_pushes_and_opens_the
78
78
  assert [(r["subject"], r["body"].strip()) for r in rows] == [
79
79
  (f"feat({nn}): {TITLES[nn]}", f"Ticket: {FILES[nn]}") for nn in order] and order[2] == "02"
80
80
  assert len(rig.commits(rig.start, rig.recovery())) == 6
81
- assert ref(rig.repo, "feat/csv^{tree}") == ref(rig.repo, "refs/implement/recovery^{tree}")
81
+ assert ref(rig.repo, "feat/csv^{tree}") == ref(rig.repo, "refs/implement-spec/feat%2Fcsv/recovery^{tree}")
82
82
  assert rig.led("Recovery point: feat/csv was at " + rig.recovery(), ref(rig.repo, "feat/csv^{tree}"))
83
83
  assert rig.led("End of run: final-reviewer ran on", FABLE) and rig.led("End of run: the gate is green on " + tip[:7])
84
84
  assert {nn: e["tip"] for nn, e in rig.state()["tickets"].items()} == {nn: r["sha"] for nn, r in zip(order, rows)}
@@ -111,7 +111,7 @@ def test_a_local_run_stops_before_the_push_and_a_push_rerun_pushes_and_opens_the
111
111
 
112
112
 
113
113
  def test_the_final_review_does_not_start_while_a_ticket_is_not_merged_and_the_ledger_says_so_once(tmp_path):
114
- rig = Inside(tmp_path, THREE, gate={"red": ["ticket/01"]})
114
+ rig = Inside(tmp_path, THREE, gate={"red": ["ticket/feat%2Fcsv/01"]})
115
115
  rig.run(plan())
116
116
  rig.run(plan(), *UNATTENDED)
117
117
  assert rig.code == 1 and not rig.final_calls("final-reviewer") and not rig.recovery()
@@ -12,7 +12,7 @@ Interface choices (ticket 03):
12
12
  `green_gates` holds full SHAs (a dict with a `commit` key is accepted too).
13
13
 
14
14
  Ticket 08: this fixture has no `origin`, and a run that merges every ticket now ends with the tidy, one gate on the
15
- tidied tip and the final review. The history as the walk made it is at `refs/implement/recovery`: `Rig.walked()` is
15
+ tidied tip and the final review. The history as the walk made it is at `refs/implement-spec/feat%2Fcsv/recovery`: `Rig.walked()` is
16
16
  that tip, `Rig.pr_subjects()` reads it, and `Rig.walk_gates()` leaves out the one gate of the end of run.
17
17
  """
18
18
  import json
@@ -234,7 +234,7 @@ class Rig:
234
234
 
235
235
  def walked(self):
236
236
  """The PR tip as the walk left it: the recovery point of the end-of-run tidy, or the branch with no tidy."""
237
- out = subprocess.run(["git", "rev-parse", "--verify", "-q", "refs/implement/recovery"], cwd=self.repo,
237
+ out = subprocess.run(["git", "rev-parse", "--verify", "-q", "refs/implement-spec/feat%2Fcsv/recovery"], cwd=self.repo,
238
238
  capture_output=True, text=True).stdout.strip()
239
239
  return out or git(self.repo, "rev-parse", "feat/csv").strip()
240
240
 
@@ -247,7 +247,7 @@ class Rig:
247
247
  return git(self.repo, "log", "--format=%s", self.walked()).splitlines()
248
248
 
249
249
  def branches(self):
250
- return git(self.repo, "branch", "--list", "ticket/*").split()
250
+ return git(self.repo, "branch", "--list", "ticket/feat%2Fcsv/*").split()
251
251
 
252
252
  def output(self):
253
253
  return f"exit {self.code}\n{self.out}{self.err}"
@@ -271,9 +271,9 @@ def std(tmp_path_factory):
271
271
 
272
272
  @pytest.fixture(scope="module")
273
273
  def blocked(tmp_path_factory):
274
- """The default scenario with a red gate on ticket/01 that four fix rounds do not make green."""
274
+ """The default scenario with a red gate on ticket/feat%2Fcsv/01 that four fix rounds do not make green."""
275
275
  fixes = [fix("01", n) for n in (1, 2, 3, 4)]
276
- return Rig(tmp_path_factory.mktemp("blocked"), gate={"red": ["ticket/01"]}).run(plan(**{"01/fixer": fixes}))
276
+ return Rig(tmp_path_factory.mktemp("blocked"), gate={"red": ["ticket/feat%2Fcsv/01"]}).run(plan(**{"01/fixer": fixes}))
277
277
 
278
278
 
279
279
  def test_01_starts_in_slot_1_03_in_slot_2_and_02_after_the_merge_of_01(std):
@@ -281,7 +281,7 @@ def test_01_starts_in_slot_1_03_in_slot_2_and_02_after_the_merge_of_01(std):
281
281
  first = lambda nn: std.calls(nn)[0]
282
282
  assert first("01")["cwd"] == str(std.repo.resolve()) and first("03")["cwd"] == str(std.slot2)
283
283
  assert "feat: 01 rows" in first("02")["pr_log"] and "feat: 01 rows" in first("02")["log"]
284
- assert first("02")["t0"] > std.calls("01")[-1]["t1"] and first("02")["branch"] == "ticket/02"
284
+ assert first("02")["t0"] > std.calls("01")[-1]["t1"] and first("02")["branch"] == "ticket/feat%2Fcsv/02"
285
285
 
286
286
 
287
287
  def test_touches_overlap_holds_03_until_01_is_merged(tmp_path):
@@ -349,7 +349,7 @@ def test_red_gate_blocks_the_ticket_after_round_4_names_the_record_and_launches_
349
349
  assert [l for l in blocked.ledger() if "01" in l and "BLOCKED" in l and f"gate-{last[:7]}.log" in l], blocked.ledger()
350
350
  assert [c["role"] for c in blocked.calls("01")] == ["test-writer", "implementer"] + ["fixer"] * 4
351
351
  assert blocked.state()["tickets"]["01"]["phase"] == "blocked"
352
- assert "ticket/01" in blocked.branches() and "feat: 01 rows" not in blocked.pr_subjects()
352
+ assert "ticket/feat%2Fcsv/01" in blocked.branches() and "feat: 01 rows" not in blocked.pr_subjects()
353
353
 
354
354
 
355
355
  def test_blocked_ticket_stops_only_the_tickets_it_blocks(blocked):
@@ -372,7 +372,7 @@ def test_non_fast_forward_merge_is_a_plumbing_merge_commit_gated_once(tmp_path):
372
372
  commits = [g["commit"] for g in rig.walk_gates()]
373
373
  assert sorted(commits) == sorted([a01, a03, tip]), commits
374
374
  waited = rig.calls("01", "implementer")[0]
375
- assert waited["after_wait"]["branch"] == "ticket/01" and waited["after_wait"]["head"] == waited["head"]
375
+ assert waited["after_wait"]["branch"] == "ticket/feat%2Fcsv/01" and waited["after_wait"]["head"] == waited["head"]
376
376
  assert waited["after_wait"]["status"] == ""
377
377
  assert git(rig.repo, "rev-parse", "--abbrev-ref", "HEAD").strip() == "feat/csv"
378
378
  assert git(rig.repo, "status", "--short") == "" and (rig.repo / "demo/cli.py").exists()
@@ -439,7 +439,7 @@ def test_resume_returns_a_ticket_with_an_unverifiable_tip_to_implement_from_the_
439
439
  commit="feat: 01 again")]})
440
440
  assert rig.code == 0, rig.output()
441
441
  again = rig.calls("01", "implementer")[1]
442
- assert (again["branch"], again["head"]) == ("ticket/01", tw_sha) and len(rig.calls("01", "test-writer")) == 1
442
+ assert (again["branch"], again["head"]) == ("ticket/feat%2Fcsv/01", tw_sha) and len(rig.calls("01", "test-writer")) == 1
443
443
  assert [l for l in rig.ledger()[len(before):] if "01" in l and fake[:7] in l], rig.ledger()
444
444
  assert "feat: 01 again" in rig.pr_subjects()
445
445
 
@@ -450,7 +450,7 @@ def test_resume_keeps_a_settled_gate_and_runs_no_gate_again(tmp_path):
450
450
  state["tickets"]["01"].update(phase="merge", tip=a1, green_gates=[a1])
451
451
  (rig.repo / ".agent/implement/state.json").write_text(json.dumps(state))
452
452
  gates_before = len(rig.gates())
453
- rig.gate_conf(red=["ticket/01"]) # any gate run on the ticket would block it; the end of run gates feat/csv
453
+ rig.gate_conf(red=["ticket/feat%2Fcsv/01"]) # any gate run on the ticket would block it; the end of run gates feat/csv
454
454
  rig.run(plan())
455
455
  assert rig.code == 0, rig.output()
456
456
  assert len(rig.walk_gates()) == gates_before and rig.walked() == a1
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "generatedAt": "2026-10-16T22:28:03.132Z",
3
+ "generatedAt": "2026-10-17T06:40:10.976Z",
4
4
  "skills": [
5
5
  {
6
6
  "name": "backlog",
@@ -46,8 +46,8 @@
46
46
  },
47
47
  {
48
48
  "name": "implement-spec",
49
- "version": "0.1.19",
50
- "hash": "sha256-42055b9530838eca23d48593e153ff5064a601e3c60240fb68f23b1793ae7223",
49
+ "version": "0.1.21",
50
+ "hash": "sha256-ffca735547ff5f7664f80fe98071d1956f839858ee4f1964ef9b7957af41a4cc",
51
51
  "description": "Implement a specification in code."
52
52
  },
53
53
  {