@jesdi/skills 0.0.40 → 0.0.42

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jesdi/skills",
3
- "version": "0.0.40",
3
+ "version": "0.0.42",
4
4
  "description": "jesdi's agent skills (content package consumed by @jesdi/skills-cli)",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -110,8 +110,10 @@ and do not know about the run, except `driver_run.py`, which the three deciding
110
110
  A timeout doubles the time of the rerun.
111
111
  - Fix rounds stop at 4, and round 3 goes to the next implementer entry. The correctness pass never runs on the
112
112
  provider of the implementer.
113
- - A red gate on a fixer's commit is a finding of the next round (`driver_fixloop._gate`), so it blocks only after
114
- round 4.
113
+ - A red gate on the implementer's or a fixer's commit is a finding of the next round (`driver_fixloop._gate`), so
114
+ it blocks only after round 4. A red gate on a test-writer correction blocks.
115
+ - The implementer and each fixer get the `--gate` command and run it before they report. The gate of the driver
116
+ stays the source of truth: an agent's run of the gate counts for nothing.
115
117
 
116
118
  ### Questions
117
119
 
@@ -95,7 +95,7 @@ Everything is under `.agent/`, which is never committed.
95
95
  A ticket is ready when each ticket in its **Blocked by** line is merged and no running ticket touches the same place. The places are the backticked entries of the **Touches** lines: `file` or `file: function`. Two entries are the same place when they name the same file and the same function, or when one of them names the whole file. Each ready ticket takes the lowest free slot.
96
96
 
97
97
  1. **test-writer** (`prompts/test-writer.md`). It writes one black-box test for each criterion through the Seam and commits them red. Those files are locked. A ticket with Seam `none — refactor` has no test-writer.
98
- 2. **implementer** (`prompts/implementer.md`). It starts from that commit and makes the locked tests green.
98
+ 2. **implementer** (`prompts/implementer.md`). It starts from that commit and makes the locked tests green. Its prompt gives the `--gate` command: it runs the gate on its final commit and makes it green before it reports DONE or DONE_WITH_CONCERNS.
99
99
  3. **Gate.** The driver runs `--gate` on the ticket tip.
100
100
  4. **Review.** The driver compares each locked file of the ticket with the test-writer commit. A file that differs is a critical finding `locked file changed: <path>`. Then the **reviewer** (`prompts/reviewer.md`) runs, and for a ticket with a **Risk** line the **correctness** pass (`prompts/correctness.md`) runs at the same time in the same slot.
101
101
  5. **Merge.** The driver merges the reviewed tip into the PR branch with git plumbing, then deletes the ticket branch. A conflict goes to the **conflict** agent (`prompts/conflict.md`) once. Its resolution may change no locked file of the ticket. A second conflict blocks the ticket.
@@ -117,9 +117,9 @@ Rules for each agent:
117
117
  | `critical` or `important` | One fix round. |
118
118
  | `minor` | It stays in the ledger and in the findings file. The final reviewer gets the list. |
119
119
 
120
- A fix round is one **fixer** (`prompts/fixer.md`), then the gate on the new tip, then a re-review of the range of the fix. A mutation proof is repeated only when the fix changed its production line.
120
+ A fix round is one **fixer** (`prompts/fixer.md`), which runs the gate on its final commit before it reports, as the implementer does, then the gate of the driver on the new tip, then a re-review of the range of the fix. A mutation proof is repeated only when the fix changed its production line.
121
121
 
122
- A red gate on the commit of a fixer is a critical finding with source `gate`. Its file is the gate record, and the record holds the output. No review runs on that tip, and the next round starts. The re-review after that round covers each fix since the last review and repeats each proof that one of them made due. A red gate after round 4 blocks the ticket, as does a red gate on the commit of the implementer or of a test-writer correction.
122
+ A red gate on the commit of the implementer or of a fixer is a critical finding with source `gate`. Its file is the gate record, and the record holds the output. No review runs on that tip, and the next round starts: a red gate on the commit of the implementer starts round 1. The review after that round covers each fix since the last review and repeats each proof that one of them made due. A red gate after round 4 blocks the ticket, as does a red gate on the commit of a test-writer correction: the correction is outside the fix rounds, and a fixer may not edit the locked files.
123
123
 
124
124
  - Rounds 1 and 2 run on the first entry of the implementer list.
125
125
  - Rounds 3 and 4 run on the second entry of the implementer list. When the list has one entry, they stay on it and the ledger says so.
@@ -81,7 +81,7 @@ def _prompt_values(ticket: Ticket, slot: str, run: Run) -> dict[str, str]:
81
81
  folder = OUT / ticket.number
82
82
  return {"spec": os.path.relpath(run.args.spec, slot), "seam": ticket.seam, "pr_branch": run.pr,
83
83
  "branch": f"ticket/{ticket.number}", "ticket": os.path.relpath(Path(run.args.tickets) / ticket.file, slot),
84
- "correctness": str(driver_review.CORRECTNESS),
84
+ "correctness": str(driver_review.CORRECTNESS), "gate": run.args.gate,
85
85
  "blocked_report": os.path.relpath(_blocked_report(ticket.number, run), slot),
86
86
  "writer_report": os.path.relpath(folder / "test-writer-0.json", slot)}
87
87
 
@@ -79,7 +79,8 @@ def finish_interrupted_move(state: State, pr: str) -> None:
79
79
 
80
80
  def _prompt_values(run: Run) -> dict[str, str]:
81
81
  """The pointers every prompt of the end of run gets. Its agents work in slot 1, the PR worktree."""
82
- return {"spec": run.args.spec, "tickets": run.args.tickets, "pr_branch": run.pr, "branch": run.pr}
82
+ return {"spec": run.args.spec, "tickets": run.args.tickets, "pr_branch": run.pr, "branch": run.pr,
83
+ "gate": run.args.gate}
83
84
 
84
85
 
85
86
  def _title(finding: dict) -> str:
@@ -164,12 +164,13 @@ def _act_on(job: Job, found: list[dict]) -> bool:
164
164
 
165
165
 
166
166
  def _gate(job: Job) -> list[dict]:
167
- """Gate the tip. A red gate on a fixer's commit before round 4 is a finding for the next round; any other red
168
- gate blocks. Returns the findings: the red gate, or none."""
167
+ """Gate the tip. A red gate on the implementer's commit or a fixer's commit before round 4 is a finding for the
168
+ next round; a red gate after round 4 or on a test-writer correction blocks. Returns the findings: the red gate,
169
+ or none."""
169
170
  run, number = job.run, job.ticket.number
170
171
  job.red = False
171
172
  with during("the gate"):
172
- if not 0 < job.round < ROUNDS or job.corrected:
173
+ if job.round == ROUNDS or job.corrected:
173
174
  run.gate(number, job.slot, job.tip)
174
175
  return []
175
176
  record = run.red_gate(number, job.slot, job.tip)
@@ -11,7 +11,8 @@ Findings to fix:
11
11
  Fix each finding in the list as one batch, in one commit or in several. Leave the minor findings of the findings file.
12
12
  Each commit belongs to one ticket: the ticket whose criterion or code the commit changes. A fix for two tickets is two commits.
13
13
  Never edit a locked file: each one stays as it is at {start_sha}, and a locked file that differs is a critical finding of the next review.
14
- Use targeted tests while you work. The driver runs the gate on your final commit, and a scoped review reads your commits.
14
+ Use targeted tests while you work.
15
+ Before you report DONE or DONE_WITH_CONCERNS, run the gate on your final commit, with IMPLEMENT_SPEC_BASE={pr_branch} in the environment: `{gate}`. Fix each failure, commit, and run it again until it is green. The driver runs the gate again on your final commit, and a scoped review reads your commits.
15
16
  Commit your work on the checked-out PR branch and change no commit that is on it now. Do not dispatch subagents.
16
17
  In `commits`, give every commit you made with the number of its ticket: the driver names each commit `fix(NN): <your subject>` from it, and a commit with no ticket is a violation. In `green`, give the summary lines of the passing test output.
17
18
 
@@ -10,7 +10,8 @@ Findings of the review: {findings}
10
10
  Fix each finding of severity critical or important in the findings file. Leave the minor findings: the final review gets them. Leave each finding of kind `fixture` and each finding of kind `test` on a locked file: the test-writer corrects those.
11
11
  Never edit a locked file. The one exception is a finding `locked file changed`: make that file equal to its content at {test_writer_sha}.
12
12
  If a locked test contradicts the spec or the ticket, so that no fix can make it green, stop and report BLOCKED with a concern that starts with `locked test contradicts spec:`, then the test and the evidence.
13
- Use targeted tests while you work. The driver runs the gate on your final commit, and a re-review reads the lines you changed.
13
+ Use targeted tests while you work.
14
+ Before you report DONE or DONE_WITH_CONCERNS, run the gate on your final commit, with IMPLEMENT_SPEC_BASE={pr_branch} in the environment: `{gate}`. Fix each failure, commit, and run it again until it is green. The driver runs the gate again on your final commit, and a re-review reads the lines you changed.
14
15
  A finding from the `gate` is a red gate: its file is the gate record. Read the output there and make the gate green.
15
16
  Commit your work on the checked-out ticket branch. Do not dispatch subagents.
16
17
  In `ticket`, give the number of the ticket that owns your commits. In `green`, give the summary lines of the passing test output.
@@ -9,7 +9,8 @@ Locked files: {locked}
9
9
  Make the locked tests green and meet each criterion of the ticket. For internal code, work in vertical test-first slices: one failing test, the smallest code that passes it, then the next slice.
10
10
  Never edit a locked file. If a locked test contradicts the spec or the ticket, stop and report BLOCKED with a concern that starts with `locked test contradicts spec:`, then the test and the evidence.
11
11
  Commit your work on the checked-out ticket branch. Do not dispatch subagents.
12
- Run targeted tests while you work. The driver runs the gate on your final commit.
12
+ Run targeted tests while you work.
13
+ Before you report DONE or DONE_WITH_CONCERNS, run the gate on your final commit, with IMPLEMENT_SPEC_BASE={pr_branch} in the environment: `{gate}`. Fix each failure, commit, and run it again until it is green. The driver runs the gate again on your final commit.
13
14
  In `green`, give the summary lines of the passing test output.
14
15
 
15
16
  Write one JSON report at {report} that matches this schema:
@@ -247,7 +247,7 @@ def test_implementer_prompt_carries_pointers_and_only_template_sentences(env):
247
247
  for prefix in (str(w.repo.resolve()) + "/", str(w.repo) + "/"):
248
248
  prompt = prompt.replace(prefix, "")
249
249
  for value in (report, "specs/csv/spec.md", ".agent/tickets/01-export-rows.md", "`export_csv()`", sha,
250
- "tests/test_export.py"):
250
+ "tests/test_export.py", "feat/csv", "true"):
251
251
  prompt = prompt.replace(value, "")
252
252
  lines = lambda text: {" ".join(l.split()) for l in text.splitlines() if l.strip()}
253
253
  assert lines(prompt) <= lines(re.sub(r"\{[a-z_]+\}", "", template))
@@ -229,6 +229,7 @@ def test_a_rerun_prompt_holds_only_template_lines_and_keeps_its_ending(repo, tmp
229
229
  prompt = prompt_of(tmp_path)
230
230
  first = driver_agents.render("implementer", {
231
231
  "spec": "spec.md", "ticket": ".agent/tickets/01-t.md", "seam": "none — refactor", "locked": "",
232
+ "gate": "true", "pr_branch": "feat/x",
232
233
  "start_sha": subprocess.run(["git", "rev-parse", "HEAD"], capture_output=True, text=True).stdout.strip(),
233
234
  "report": ".agent/implement/01/implementer-0.json"})
234
235
  ending = first[first.index("\nWrite one JSON report"):]
@@ -358,6 +359,9 @@ echo "$nn $PWD" >> "$0.calls"
358
359
  # Ticket 01 waits until ticket 02 has its branch, so both start from the same PR tip.
359
360
  TOGETHER = """[ "$nn" != 01 ] || until git rev-parse -q --verify refs/heads/ticket/02 > /dev/null; do sleep 0.02; done\n"""
360
361
  REFACTOR = {"seam": "none — refactor"}
362
+ # A fixer that cannot make the red gate green: the ticket blocks at fix round 1.
363
+ GIVE_UP = """[ $role != fixer ] || { printf '{"status": "BLOCKED", "commits": [], "files": [], "concerns": ["the gate stays red"], "green": [], "ticket": "01"}' > "$report"; exit 0; }\n"""
364
+ GAVE_UP = "Ticket 01: BLOCKED at fix round 1: the fixer reported BLOCKED: the gate stays red."
361
365
 
362
366
 
363
367
  def walk(tmp_path, capsys, body, *args, gate="true"):
@@ -452,13 +456,13 @@ def test_a_conflict_cli_that_fails_stops_the_run_and_leaves_no_open_merge(repo,
452
456
  assert out_of("git", "-C", slot, "rev-parse", "-q", "--verify", "MERGE_HEAD") == ""
453
457
 
454
458
 
455
- def test_a_red_gate_blocks_the_ticket_and_the_ticket_it_blocks_never_starts(repo, tmp_path, capsys):
459
+ def test_a_red_gate_is_a_finding_and_the_ticket_it_blocks_never_starts(repo, tmp_path, capsys):
456
460
  two(repo, second="01")
457
- code, ledger, out = walk(tmp_path, capsys, WORK + REPORT, gate="echo CHECK-OUTPUT; exit 1")
461
+ code, ledger, out = walk(tmp_path, capsys, GIVE_UP + WORK + REPORT, gate="echo CHECK-OUTPUT; exit 1")
458
462
  tip = out_of("git", "rev-parse", "--short=7", "ticket/01")
459
- assert code == 1 and len(ledger) == 2, out
460
- assert ledger[1] == (f"Ticket 01: BLOCKED at the gate: the gate is red on {tip}, "
461
- f"record .agent/implement/01/gate-{tip}.log.")
463
+ assert code == 1 and ledger[-1] == GAVE_UP, out
464
+ assert ledger[1] == (f"Ticket 01: critical finding of kind code from the gate at "
465
+ f".agent/implement/01/gate-{tip}.log:0: the gate is red on {tip}.")
462
466
  record = (tmp_path / f".agent/implement/01/gate-{tip}.log").read_text()
463
467
  assert "status: clean\n" in record and "CHECK-OUTPUT\n\nexit: 1\nafter: clean\nend: " in record
464
468
  assert "Ticket 02 is not merged: its phase is pending." in out and "| gates run | 1 |" in out
@@ -479,8 +483,8 @@ def test_a_red_merge_gate_blocks_the_ticket_and_the_pr_branch_does_not_move(repo
479
483
 
480
484
  def test_a_tree_that_is_not_clean_is_a_red_gate_and_the_slot_keeps_the_work(repo, tmp_path, capsys):
481
485
  repo(t01=ticket("01", **REFACTOR))
482
- code, ledger, out = walk(tmp_path, capsys, WORK + REPORT + "echo left > junk.txt\n", gate="echo NEVER")
483
- assert code == 1 and "BLOCKED at the gate" in ledger[-1], out
486
+ code, ledger, out = walk(tmp_path, capsys, GIVE_UP + WORK + REPORT + "echo left > junk.txt\n", gate="echo NEVER")
487
+ assert code == 1 and "from the gate at .agent/implement/01/gate-" in ledger[-3] and ledger[-1] == GAVE_UP, out
484
488
  record = next((tmp_path / ".agent/implement/01").glob("gate-*.log")).read_text()
485
489
  assert "status: not clean\n ?? junk.txt\n" in record and "exit: not run" in record and "\nNEVER\n" not in record
486
490
  assert out_of("git", "symbolic-ref", "--short", "HEAD") == "ticket/01" and (tmp_path / "junk.txt").exists()
@@ -729,8 +733,8 @@ def test_a_merge_commit_the_slot_cannot_check_out_is_not_gated_and_does_not_land
729
733
  ])
730
734
  def test_a_gate_that_changes_the_tree_or_moves_head_is_red(repo, tmp_path, capsys, gate, after):
731
735
  repo(t01=ticket("01", **REFACTOR))
732
- code, ledger, out = walk(tmp_path, capsys, WORK + REPORT, gate=gate)
733
- assert code == 1 and "BLOCKED at the gate: the gate is red on " in ledger[-1], out
736
+ code, ledger, out = walk(tmp_path, capsys, GIVE_UP + WORK + REPORT, gate=gate)
737
+ assert code == 1 and "from the gate at .agent/implement/01/gate-" in ledger[-3] and ledger[-1] == GAVE_UP, out
734
738
  record = next((tmp_path / ".agent/implement/01").glob("gate-*.log")).read_text()
735
739
  assert "\nexit: 0\n" in record and after in record
736
740
  assert state_of(tmp_path)["tickets"]["01"]["green_gates"] == []
@@ -31,7 +31,7 @@ from types import SimpleNamespace
31
31
  import pytest
32
32
 
33
33
  from test_cli import git, ticket
34
- from test_review import FABLE, HOOKED, ONE, OPUS, Review, finding, review
34
+ from test_review import FABLE, HOOKED, ONE, OPUS, Review, finding, gate_rule, review
35
35
  from test_stops import UNATTENDED, decisions, stage
36
36
  from test_walk import impl, plan, tw
37
37
 
@@ -296,6 +296,11 @@ def test_two_findings_make_one_fixer_batch_of_fix_commits_on_top_of_the_unchange
296
296
  assert ref(fixed.repo, "feat/csv~2") == reviewed
297
297
 
298
298
 
299
+ def test_the_fixer_of_the_final_review_gets_the_gate_command_and_runs_it_before_it_reports(fixed):
300
+ (batch,) = fixed.final_calls("fixer")
301
+ gate_rule(batch["prompt"], str(fixed.rig / "gate"))
302
+
303
+
299
304
  def test_a_scoped_review_covers_the_range_after_the_reviewed_tip_and_one_gate_runs_on_the_new_tip(fixed):
300
305
  reviewed, tip = fixed.final_calls("final-reviewer")[0]["head"], fixed.branch()
301
306
  first, scoped = fixed.final_calls("final-reviewer")
@@ -399,6 +399,21 @@ def test_a_red_gate_on_a_fixer_commit_starts_the_next_round_with_the_record_as_e
399
399
  assert f"The fix is {rig.sha('01', 'implementer')}..{second}" in again and "demo/export.py:2" in again
400
400
 
401
401
 
402
+ def test_a_red_gate_on_the_implementer_commit_starts_fix_round_1_with_the_record_as_evidence(tmp_path):
403
+ """Issue 80: the gate of the implementer commit is red. That is a finding for round 1, not a BLOCKED ticket."""
404
+ rig = Inside(tmp_path, gate={"subjects": ["feat: 01 rows"]})
405
+ rig.run(plan(**{"01/reviewer": [review("01")], "01/fixer": [fix("01", 1)]}))
406
+ assert rig.code == 0, rig.ledger()
407
+ assert [call["role"] for call in rig.calls("01")] == ["test-writer", "implementer", "fixer", "reviewer"]
408
+ tip = rig.sha("01", "implementer")
409
+ record = f".agent/implement/01/gate-{tip[:7]}.log"
410
+ (gate,) = json.loads((rig.folder() / "findings-0.json").read_text())
411
+ assert (gate["source"], gate["severity"], gate["kind"]) == ("gate", "critical", "code")
412
+ assert gate["title"] == f"the gate is red on {tip[:7]}" and record in gate["evidence"]
413
+ assert "findings-0.json" in rig.prompt("fixer-1") and rig.state()["tickets"]["01"]["phase"] == "merged"
414
+ assert "The fix is" not in rig.prompt("reviewer-1") # no review saw the implementer commit: a full review
415
+
416
+
402
417
  def test_a_red_gate_on_the_fixer_commit_of_round_4_blocks_the_ticket(tmp_path):
403
418
  rig = Inside(tmp_path, gate={"subjects": ["fix: 01 round 4"]})
404
419
  rig.run(plan(**{"01/reviewer": [review("01", [STUCK])] * 4, "01/fixer": [fix("01", n) for n in (1, 2, 3, 4)]}))
@@ -22,7 +22,7 @@ from pathlib import Path
22
22
  import pytest
23
23
 
24
24
  from test_cli import SKILL, ticket, three, git, models_file
25
- from test_walk import RUNNER, Rig, impl, plan, tw
25
+ from test_walk import RUNNER, Rig, fix, impl, plan, tw
26
26
 
27
27
  # The fake runner of test_walk plus two hooks: `git` runs before the step's files, `git_after` right after them.
28
28
  HOOKED = RUNNER.replace(
@@ -76,13 +76,6 @@ def correct(findings=(), **extra):
76
76
  return {"report": report, **extra}
77
77
 
78
78
 
79
- def fix(nn, n, text=None, **changes):
80
- """A fixer step: one commit on the ticket branch."""
81
- step = impl(nn, text=text or f"def fn_{nn}():\n return 'fix{n}'\n", commit=f"fix: {nn} round {n}", **changes)
82
- step["report"]["ticket"] = nn
83
- return step
84
-
85
-
86
79
  def edit(text, **new):
87
80
  """`text` with line N replaced (keyword `l<N>`)."""
88
81
  lines = text.splitlines(keepends=True)
@@ -201,6 +194,21 @@ def test_each_round_ends_with_one_gate_record_and_a_re_review_scoped_to_the_fix(
201
194
  assert tips[k - 1][:7] in reviews[k]["prompt"] and tips[k][:7] in reviews[k]["prompt"], k
202
195
 
203
196
 
197
+ def gate_rule(prompt, gate):
198
+ """The line of `prompt` that gives the gate command; it must say to run it before the report."""
199
+ (line,) = [l for l in prompt.splitlines() if f"`{gate}`" in l]
200
+ assert "IMPLEMENT_SPEC_BASE=feat/csv" in line and "Before you report DONE" in line, line
201
+ return line
202
+
203
+
204
+ def test_the_implementer_and_each_fixer_get_the_gate_command_and_run_it_before_they_report(escalated):
205
+ gate = str(escalated.rig / "gate")
206
+ agents = escalated.calls("01", "implementer") + escalated.calls("01", "fixer")
207
+ assert len(agents) == 4, "the implementer and the fixers of rounds 1, 2 and 3 (the next model)"
208
+ for call in agents:
209
+ gate_rule(call["prompt"], gate)
210
+
211
+
204
212
  def test_reports_are_named_by_role_and_round(escalated):
205
213
  for name in ("reviewer-0", "reviewer-1", "reviewer-2", "reviewer-3", "fixer-1", "fixer-2", "fixer-3"):
206
214
  assert (escalated.folder() / f"{name}.json").exists(), name
@@ -130,6 +130,13 @@ def impl(nn, path="demo/export.py", text=None, **changes):
130
130
  return {**step, **changes}
131
131
 
132
132
 
133
+ def fix(nn, n, text=None, **changes):
134
+ """A fixer step: one commit on the ticket branch."""
135
+ step = impl(nn, text=text or f"def fn_{nn}():\n return 'fix{n}'\n", commit=f"fix: {nn} round {n}", **changes)
136
+ step["report"]["ticket"] = nn
137
+ return step
138
+
139
+
133
140
  PATHS = {"01": "demo/export.py", "02": "demo/export.py", "03": "demo/cli.py"}
134
141
 
135
142
 
@@ -264,8 +271,9 @@ def std(tmp_path_factory):
264
271
 
265
272
  @pytest.fixture(scope="module")
266
273
  def blocked(tmp_path_factory):
267
- """The default scenario with a red gate on ticket/01."""
268
- return Rig(tmp_path_factory.mktemp("blocked"), gate={"red": ["ticket/01"]}).run(plan())
274
+ """The default scenario with a red gate on ticket/01 that four fix rounds do not make green."""
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}))
269
277
 
270
278
 
271
279
  def test_01_starts_in_slot_1_03_in_slot_2_and_02_after_the_merge_of_01(std):
@@ -331,12 +339,15 @@ def test_gate_environment_names_the_pr_branch_and_a_pycache_prefix_under_agent_i
331
339
  assert os.path.realpath(gate["pycache"]).startswith(str((rig.repo / ".agent/implement").resolve()) + os.sep)
332
340
 
333
341
 
334
- def test_red_gate_blocks_the_ticket_names_the_record_and_launches_no_later_role(blocked):
342
+ def test_red_gate_blocks_the_ticket_after_round_4_names_the_record_and_launches_no_review(blocked):
335
343
  assert blocked.code == 1, blocked.output()
336
344
  a1 = blocked.sha("01", "implementer")
337
- assert [l for l in blocked.ledger() if "01" in l and "BLOCKED" in l and f"gate-{a1[:7]}.log" in l], blocked.ledger()
345
+ finding = f"Ticket 01: critical finding of kind code from the gate at .agent/implement/01/gate-{a1[:7]}.log:0: "
346
+ assert [l for l in blocked.ledger() if l.startswith(finding)], blocked.ledger()
338
347
  assert re.search(r"exit\D*1", (blocked.repo / f".agent/implement/01/gate-{a1[:7]}.log").read_text())
339
- assert {c["role"] for c in blocked.calls("01")} == {"test-writer", "implementer"}
348
+ last = blocked.calls("01", "fixer")[-1]["sha"]
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
+ assert [c["role"] for c in blocked.calls("01")] == ["test-writer", "implementer"] + ["fixer"] * 4
340
351
  assert blocked.state()["tickets"]["01"]["phase"] == "blocked"
341
352
  assert "ticket/01" in blocked.branches() and "feat: 01 rows" not in blocked.pr_subjects()
342
353
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "generatedAt": "2026-10-15T23:44:37.469Z",
3
+ "generatedAt": "2026-10-16T10:22:13.328Z",
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.16",
50
- "hash": "sha256-8f7e4b06072b26e3a6699974dced9afe2745917c01784f9f6166d972c3ba60f1",
49
+ "version": "0.1.18",
50
+ "hash": "sha256-b10cd79532e2c8a33ddb8c55934ee26ee7f98d104ff7805bc3e2b819d0bb9241",
51
51
  "description": "Implement a specification in code."
52
52
  },
53
53
  {