@jesdi/skills 0.0.44 → 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.
@@ -426,7 +426,7 @@ def test_four_rounds_then_blocked_with_the_escalation_at_round_3(tmp_path):
426
426
  assert rig.ledger().count(f"Ticket 01 round 3: escalated to {FABLE}.") == 1
427
427
  assert [len(rig.led("fixer ran on", entry)) for entry in (OPUS, FABLE)] == [2, 2]
428
428
  tips = [rig.sha("01", "implementer")] + [call["sha"] for call in rig.calls("01", "fixer")]
429
- assert [gate["commit"] for gate in rig.gates() if gate["branch"] == "ticket/01"] == tips
429
+ assert [gate["commit"] for gate in rig.gates() if gate["branch"] == "ticket/feat%2Fcsv/01"] == tips
430
430
  assert rig.state()["tickets"]["01"]["tip"] == tips[-1] and not rig.calls("02")
431
431
  assert rig.state()["tickets"]["03"]["phase"] == "merged"
432
432
 
@@ -533,13 +533,13 @@ def test_a_timed_out_read_only_role_is_restored_only_when_no_other_agent_is_in_t
533
533
 
534
534
  DIRTY = [({"files": {"junk.txt": "x\n", "demo/export.py": "mutated\n"}},
535
535
  "the tree is not as the review found it: demo/export.py, junk.txt"),
536
- ({"git": [["checkout", "-q", "--detach"]]}, "the slot is no longer on ticket/01 at "),
537
- ({"commit": "sneak", "files": {"demo/export.py": "x = 2\n"}}, "the slot is no longer on ticket/01 at ")]
536
+ ({"git": [["checkout", "-q", "--detach"]]}, "the slot is no longer on ticket/feat%2Fcsv/01 at "),
537
+ ({"commit": "sneak", "files": {"demo/export.py": "x = 2\n"}}, "the slot is no longer on ticket/feat%2Fcsv/01 at ")]
538
538
 
539
539
 
540
540
  def slot_state(rig):
541
541
  git = lambda *args: subprocess.run(["git", "-C", str(rig.repo), *args], capture_output=True, text=True).stdout.strip()
542
- return git("status", "--porcelain"), git("rev-parse", "ticket/01")
542
+ return git("status", "--porcelain"), git("rev-parse", "ticket/feat%2Fcsv/01")
543
543
 
544
544
 
545
545
  @pytest.mark.parametrize("step, left", DIRTY)
@@ -550,7 +550,7 @@ def test_a_reviewer_that_changes_the_slot_is_run_once_more_on_the_restored_tip(t
550
550
  (line,) = rig.led("Ticket 01: a read-only role broke its rule:")
551
551
  assert left in line and line.endswith(f"the driver restored the slot to {tip[:7]} and runs the review again.")
552
552
  first, second = rig.calls("01", "reviewer")
553
- assert (second["branch"], second["head"], second["status"]) == ("ticket/01", tip, "")
553
+ assert (second["branch"], second["head"], second["status"]) == ("ticket/feat%2Fcsv/01", tip, "")
554
554
  assert rig.tip() == tip and not (rig.repo / "junk.txt").exists()
555
555
 
556
556
 
@@ -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,6 +29,8 @@ 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
35
  from test_final import Final
35
36
  from test_review import HOOKED, Review, cli
@@ -111,14 +112,14 @@ def test_an_implementer_that_weakens_the_locked_test_of_a_merged_ticket_lands_on
111
112
  # ---------------------------------------------------------------- a ticket branch that takes the PR branch in
112
113
 
113
114
  def takes_in(*more):
114
- """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."""
115
116
  return {**impl("03", "demo/cli.py"), "wait_pr_moves": True, "git": [["merge", "-q", "--no-edit", "feat/csv"], *more]}
116
117
 
117
118
 
118
119
  def test_a_ticket_that_merged_the_pr_branch_into_itself_lands_with_the_tests_of_the_merged_ticket(tmp_path):
119
120
  rig = Review(tmp_path, three()).run(plan(**{"03/implementer": [takes_in()], "03/fixer": NO_REPORT}))
120
121
  (call,) = rig.calls("03", "implementer")
121
- 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"
122
123
  assert not rig.led("locked file changed"), rig.ledger()
123
124
  assert rig.code == 0, rig.output()
124
125
  assert phase(rig, "03") == "merged" and on_pr(rig, T01) == LOCKED_01 and on_pr(rig, T03) == LOCKED_03
@@ -128,7 +129,7 @@ def test_a_ticket_that_merged_the_pr_branch_and_deleted_a_locked_test_of_the_mer
128
129
  rig = Review(tmp_path, three())
129
130
  rig.run(plan(**{"03/implementer": [takes_in(["rm", "-q", T01])], "03/fixer": NO_REPORT}))
130
131
  (call,) = rig.calls("03", "implementer")
131
- assert holds(rig, rig.sha("01", "implementer"), call["sha"]), "01 is in ticket/03"
132
+ assert holds(rig, rig.sha("01", "implementer"), call["sha"]), "01 is in ticket/feat%2Fcsv/03"
132
133
  assert rig.code == 0 and restored(rig, "03", T01), rig.output()
133
134
  assert phase(rig, "01") == "merged" and on_pr(rig, T01) == LOCKED_01
134
135
 
@@ -297,44 +298,46 @@ def until(done, seconds=30):
297
298
  time.sleep(0.02)
298
299
 
299
300
 
300
- def world(*rigs):
301
- """Every file of each worktree, and every ref and worktree HEAD of the one repository."""
302
- return ([snapshot(r.repo) for r in rigs], git(rigs[0].repo, "for-each-ref"), git(rigs[0].repo, "worktree", "list"))
303
-
304
-
305
301
  def no_lock_is_dirty(*rigs):
306
302
  for rig in rigs:
307
303
  status = git(rig.repo, "status", "--short")
308
304
  assert "lock" not in status, status
309
305
 
310
306
 
311
- 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."""
312
318
  first, second = pair(tmp_path)
313
319
  held = {f"{nn}/test-writer": [{**tw(nn), "wait": ["go"]}] for nn in ("01", "03")}
314
320
  proc = start(first, plan(**held))
315
321
  try:
316
322
  until(lambda: len(first.calls(role="test-writer")) == 2) # the first driver holds both its slots in a role
317
- 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
318
325
  second.run(OTHER)
319
- assert second.code == 3, second.output()
320
- (sentence,) = second.out.splitlines()
321
- named = [word.rstrip(".,;:") for word in sentence.split() if "/" in word or word.rstrip(".,;:").endswith("lock")]
322
- assert "lock" in sentence and any((second.repo / word).is_file() for word in named), sentence
323
- 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
324
329
  no_lock_is_dirty(first, second)
325
330
  (first.rig / "sig/go").touch()
326
331
  out, _ = proc.communicate(timeout=120)
327
332
  finally:
328
333
  ended(proc)
329
334
  assert proc.returncode == 0, out
330
- second.run(OTHER)
331
- assert second.code == 0 and second.calls("04", "implementer"), second.output()
332
335
  no_lock_is_dirty(first, second)
333
336
 
334
337
 
335
338
  ENDS = {
336
339
  0: (plan(), {}, ()),
337
- 1: (plan(), {"red": ["ticket/01"]}, ()),
340
+ 1: (plan(), {"red": ["ticket/feat%2Fcsv/01"]}, ()),
338
341
  2: (plan(**{"01/implementer": [contradicted()]}), {}, ()),
339
342
  3: (plan(), {}, ("--prepare", "false")), # slot 2 cannot be made: the run has started and holds the lock
340
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-16T23:46:48.180Z",
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.20",
50
- "hash": "sha256-9c85f3c37deb0c2e9715172ccdfbb71ef65c066aa8e8c1505d9d2e968b84f0fd",
49
+ "version": "0.1.21",
50
+ "hash": "sha256-ffca735547ff5f7664f80fe98071d1956f839858ee4f1964ef9b7957af41a4cc",
51
51
  "description": "Implement a specification in code."
52
52
  },
53
53
  {