@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.
@@ -8,11 +8,7 @@ from __future__ import annotations
8
8
  import os
9
9
  import subprocess
10
10
 
11
- from driver_slots import git, names
12
-
13
-
14
- def ancestor(commit: str, of: str) -> bool:
15
- return git(".", "merge-base", "--is-ancestor", commit, of).returncode == 0
11
+ from driver_slots import ancestor, git, names
16
12
 
17
13
 
18
14
  def identity(tip: str, slot: str = ".") -> dict[str, str]:
@@ -82,16 +78,21 @@ def refused(tickets: dict, number: str, old: str, new: str) -> str:
82
78
  if before.get(path) != after.get(path))
83
79
 
84
80
 
81
+ def changed(slot: str, pr_tip: str, paths: list[str]) -> list[str]:
82
+ """Each of `paths` that the ticket changed in the tree of `slot`."""
83
+ # Against the merge base: a file that only the PR branch changed since then is not the ticket's change.
84
+ base = git(slot, "merge-base", "HEAD", pr_tip).stdout.strip() or pr_tip
85
+ return names(git(slot, "diff", "--name-only", "-z", base, "--", *paths).stdout) if paths else []
86
+
87
+
85
88
  def restore(slot: str, pr_tip: str, paths: list[str]) -> list[str]:
86
89
  """Put each of `paths` that the ticket changed back to the version of the PR tip, in the tree of `slot` only; a
87
90
  file the PR tip does not have is removed. Returns those paths."""
88
- # Against the merge base: a file that only the PR branch changed since then is not the ticket's change.
89
- base = git(slot, "merge-base", "HEAD", pr_tip).stdout.strip() or pr_tip
90
- changed = names(git(slot, "diff", "--name-only", "-z", base, "--", *paths).stdout) if paths else []
91
- for path in changed:
91
+ restored = changed(slot, pr_tip, paths)
92
+ for path in restored:
92
93
  if git(slot, "checkout", "-q", pr_tip, "--", path).returncode:
93
94
  git(slot, "rm", "-q", "-f", "--", path)
94
- return changed
95
+ return restored
95
96
 
96
97
 
97
98
  def land(pr_branch: str, new: str, old: str) -> str:
@@ -0,0 +1,32 @@
1
+ """The names of one run: its ticket branches, its recovery ref and its lock file.
2
+
3
+ Each name carries the PR branch as one name component, so runs on other PR branches share none of them. One
4
+ component, because git refs are a tree: `ticket/feat/01` and `ticket/feat/01/02` cannot both exist. `%` and `/` are
5
+ escaped, so two PR branches never give the same component: `feat/x` gives `feat%2Fx`.
6
+ """
7
+ from __future__ import annotations
8
+
9
+ from dataclasses import dataclass
10
+ from pathlib import Path
11
+
12
+
13
+ @dataclass(frozen=True)
14
+ class Names:
15
+ pr: str # the PR branch
16
+
17
+ @property
18
+ def _key(self) -> str:
19
+ return self.pr.replace("%", "%25").replace("/", "%2F")
20
+
21
+ def ticket(self, number: str) -> str:
22
+ """The branch of ticket `number`."""
23
+ return f"ticket/{self._key}/{number}"
24
+
25
+ @property
26
+ def recovery(self) -> str:
27
+ """The ref that holds the PR tip before the tidy."""
28
+ return f"refs/implement-spec/{self._key}/recovery"
29
+
30
+ def lock_file(self, common_dir: Path) -> Path:
31
+ """The file one driver per PR branch locks, in the git common directory that every worktree shares."""
32
+ return common_dir / "implement-spec" / f"{self._key}.lock"
@@ -2,6 +2,7 @@
2
2
 
3
3
  context: repo (slot path), branch, start (HEAD when the role started), log, round (1 = first review), locked,
4
4
  correction (a test-writer corrects locked tests), restored (locked files the driver put back before it),
5
+ restore_red (the gate is red on the commit of that restore),
5
6
  owners (ticket numbers, for the final fix batch), merged (locked files of merged tickets, not its own), pr (tip)."""
6
7
  from __future__ import annotations
7
8
 
@@ -66,8 +67,14 @@ def _correction_errors(report: dict, context: dict) -> list[str]:
66
67
  return []
67
68
  changed = _git(context["repo"], "diff", "--name-only", "-z", context["start"], context["branch"], end="\0")
68
69
  more = [path for path in changed if path not in report.get("locked", []) + context.get("restored", [])]
69
- return [f"a correction may change only the files it lists in `locked`: its commits also change {', '.join(more)}"
70
- ] if more else []
70
+ errors = [f"a correction may change only the files it lists in `locked`: its commits also change {', '.join(more)}"
71
+ ] if more else []
72
+ if context.get("restore_red") and report["status"] in ("DONE", "DONE_WITH_CONCERNS") and not (
73
+ set(report["locked"]) & set(context["restored"])):
74
+ errors.append("the gate is red on the restore commit, so a restored file fails with the code of this ticket: "
75
+ "update that file and list it in `locked`, or report BLOCKED with a concern that starts with "
76
+ "`locked test contradicts spec:`")
77
+ return errors
71
78
 
72
79
 
73
80
  def _merged_errors(report: dict, context: dict) -> list[str]:
@@ -8,7 +8,7 @@ from __future__ import annotations
8
8
 
9
9
  import re
10
10
  import driver_agents
11
- from driver_slots import git, head, names
11
+ from driver_slots import git, head, names, on_branch
12
12
  from driver_tickets import Ticket
13
13
 
14
14
  # The correctness pass reads this file of the review-diff skill, which is installed next to this one.
@@ -38,6 +38,14 @@ def locked_changes(slot: str, sha: str | None, locked: list[str]) -> list[dict]:
38
38
  "evidence": f"git diff {sha}..HEAD -- {path}"} for path, title in titles.items()]
39
39
 
40
40
 
41
+ def merged_changes(paths: list[str]) -> list[dict]:
42
+ """One critical `fixture` finding per locked file of a merged ticket that the ticket changed. A fixture finding
43
+ goes to a test-writer correction: the driver puts the file back before it and gates that commit."""
44
+ return [{"severity": "critical", "file": path, "line": 1, "kind": "fixture", "source": "driver",
45
+ "title": f"the ticket changed the locked file of a merged ticket: {path}",
46
+ "evidence": f"git diff <merge base with the PR branch> -- {path}"} for path in paths]
47
+
48
+
41
49
  def _references(entry: dict) -> list[tuple[str | None, list[str]]]:
42
50
  """(reference commit, the locked files it is the reference of): `test_writer_sha`, or the older commit that
43
51
  `held` names for a file."""
@@ -123,7 +131,7 @@ def restore(slot: str, branch: str, tip: str) -> None:
123
131
 
124
132
  def disturbed(slot: str, branch: str, tip: str) -> str:
125
133
  """"" when the slot is still on `branch` at `tip` with a clean tree, else what the read-only agents left."""
126
- if git(slot, "symbolic-ref", "-q", "--short", "HEAD").stdout.strip() != branch or head(slot) != tip:
134
+ if on_branch(slot) != branch or head(slot) != tip:
127
135
  return f"the slot is no longer on {branch} at {tip[:7]}"
128
136
  left = [line[3:] for line in git(slot, "status", "--porcelain").stdout.splitlines()]
129
137
  return f"the tree is not as the review found it: {', '.join(left)}" if left else ""
@@ -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
 
@@ -191,8 +196,8 @@ def _restore_if_changed(number: str, slot: str, branch: str, tip: str, runs_agai
191
196
 
192
197
  def _reset_if_moved(slot: str, branch: str, tip: str) -> str:
193
198
  """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:
199
+ head = driver_slots.on_branch(slot)
200
+ if not driver_slots.branch_exists(branch):
196
201
  problem = f"branch {branch} is missing"
197
202
  elif head != branch:
198
203
  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
@@ -0,0 +1,3 @@
1
+
2
+ The driver ran the gate on that commit, {start_sha}, and the gate is red: read the output in {record}. The gate was green before the restore, so a restored file fails with the code of this ticket.
3
+ Update each restored file that fails so that it works with the new interface: keep what each test asserts, and list the file in `locked`. When no such update can keep what a test asserts, report BLOCKED with a concern that starts with `locked test contradicts spec:`, then the test and the evidence.
@@ -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")