@jesdi/skills 0.0.39 → 0.0.41
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 +1 -1
- package/skills/implement-spec/ARCHITECTURE.md +2 -2
- package/skills/implement-spec/README.md +1 -1
- package/skills/implement-spec/driver_fixloop.py +4 -3
- package/skills/implement-spec/models.json +9 -9
- package/skills/implement-spec/tests/test_driver.py +21 -15
- package/skills/implement-spec/tests/test_fixloop.py +15 -0
- package/skills/implement-spec/tests/test_models.py +4 -4
- package/skills/implement-spec/tests/test_review.py +1 -8
- package/skills/implement-spec/tests/test_walk.py +16 -5
- package/skills-manifest.json +3 -3
package/package.json
CHANGED
|
@@ -110,8 +110,8 @@ 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
|
|
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
115
|
|
|
116
116
|
### Questions
|
|
117
117
|
|
|
@@ -119,7 +119,7 @@ Rules for each agent:
|
|
|
119
119
|
|
|
120
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.
|
|
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
|
|
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.
|
|
@@ -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
|
|
168
|
-
gate blocks. Returns the findings: the red gate,
|
|
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
|
|
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)
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
|
-
"explorer": ["claude-sonnet-5@medium"],
|
|
3
|
-
"test-writer": ["claude-sonnet-5@medium", "openai/gpt-6.1-sol@medium"],
|
|
4
|
-
"implementer": ["claude-sonnet-5@medium", "openai/gpt-6.1-sol@medium"],
|
|
5
|
-
"reviewer": ["openai/gpt-6-astra@medium", "claude-opus-5@medium"],
|
|
6
|
-
"correctness": ["openai/gpt-6-astra@medium", "claude-opus-5@medium"],
|
|
7
|
-
"fixer": ["claude-opus-5@medium"],
|
|
8
|
-
"final-reviewer": ["openai/gpt-6-astra@medium", "claude-opus-5@medium"],
|
|
9
|
-
"conflict": ["claude-opus-5@medium"],
|
|
10
|
-
"pr-writer": ["claude-opus-5@medium"]
|
|
2
|
+
"explorer": ["claude-sonnet-5-5@medium"],
|
|
3
|
+
"test-writer": ["claude-sonnet-5-5@medium", "openai/gpt-6.1-sol@medium"],
|
|
4
|
+
"implementer": ["claude-sonnet-5-5@medium", "openai/gpt-6.1-sol@medium"],
|
|
5
|
+
"reviewer": ["openai/gpt-6-astra@medium", "claude-opus-5-5@medium"],
|
|
6
|
+
"correctness": ["openai/gpt-6-astra@medium", "claude-opus-5-5@medium"],
|
|
7
|
+
"fixer": ["claude-opus-5-5@medium"],
|
|
8
|
+
"final-reviewer": ["openai/gpt-6-astra@medium", "claude-opus-5-5@medium"],
|
|
9
|
+
"conflict": ["claude-opus-5-5@medium"],
|
|
10
|
+
"pr-writer": ["claude-opus-5-5@medium"]
|
|
11
11
|
}
|
|
@@ -74,7 +74,7 @@ def test_models_file_checks_the_first_entry_of_each_role(repo, capsys, tmp_path,
|
|
|
74
74
|
monkeypatch.setattr(driver_agents.shutil, "which", lambda cli: None if cli == "codex" else cli)
|
|
75
75
|
models = tmp_path.parent / f"{tmp_path.name}-models.json"
|
|
76
76
|
# The default reviewer and final-reviewer start on codex, so this file gives both a claude entry.
|
|
77
|
-
claude = {"reviewer": ["claude-opus-5@medium"], "final-reviewer": ["claude-opus-5@medium"]}
|
|
77
|
+
claude = {"reviewer": ["claude-opus-5-5@medium"], "final-reviewer": ["claude-opus-5-5@medium"]}
|
|
78
78
|
models.write_text(json.dumps({"implementer": ["openai/gpt-sol@medium"], **claude,
|
|
79
79
|
"correctness": ["openai/gpt-sol@medium"], "fixer": ["other/x@low"]}))
|
|
80
80
|
assert run(capsys, "--models", str(models)) == (
|
|
@@ -90,11 +90,14 @@ def test_models_file_checks_the_first_entry_of_each_role(repo, capsys, tmp_path,
|
|
|
90
90
|
3, "correctness: codex is not on PATH (model entry openai/gpt-sol@low)\n")
|
|
91
91
|
|
|
92
92
|
|
|
93
|
-
|
|
93
|
+
# Each entry is invalid on purpose: `turbo` is no effort of the anthropic provider, the second entry has no
|
|
94
|
+
# effort, and the third has no model.
|
|
95
|
+
@pytest.mark.parametrize("entry", ["claude-opus-5-5@turbo", "openai/gpt-sol", "openai/@low"],
|
|
96
|
+
ids=["unknown-effort", "no-effort", "no-model"])
|
|
94
97
|
def test_an_entry_needs_a_model_and_an_effort_of_its_provider(repo, capsys, tmp_path, entry):
|
|
95
98
|
repo(t01=ticket("01"))
|
|
96
99
|
models = tmp_path.parent / f"{tmp_path.name}-models.json"
|
|
97
|
-
models.write_text(json.dumps({"reviewer": ["claude-opus-5@low", entry]}))
|
|
100
|
+
models.write_text(json.dumps({"reviewer": ["claude-opus-5-5@low", entry]}))
|
|
98
101
|
code, out = run(capsys, "--models", str(models))
|
|
99
102
|
assert code == 3 and out.startswith(f"reviewer: model entry {entry} needs a model and an effort from ")
|
|
100
103
|
|
|
@@ -173,7 +176,7 @@ def test_accepted_reports_end_the_run_with_exit_0(repo, tmp_path, capsys):
|
|
|
173
176
|
assert code == 0
|
|
174
177
|
tip = walked("--short=7")
|
|
175
178
|
assert [line.partition(" for ")[0] for line in ledger] == [
|
|
176
|
-
"Ticket 01: test-writer ran on claude-sonnet-5@medium", "Ticket 01: implementer ran on claude-sonnet-5@medium",
|
|
179
|
+
"Ticket 01: test-writer ran on claude-sonnet-5-5@medium", "Ticket 01: implementer ran on claude-sonnet-5-5@medium",
|
|
177
180
|
f"Ticket 01: the gate is green on {tip}, record .agent/implement/01/gate-{tip}.log.",
|
|
178
181
|
"Ticket 01: reviewer ran on openai/gpt-6-astra@medium",
|
|
179
182
|
f"Ticket 01: merged into feat/x as a fast-forward at {tip}."]
|
|
@@ -355,6 +358,9 @@ echo "$nn $PWD" >> "$0.calls"
|
|
|
355
358
|
# Ticket 01 waits until ticket 02 has its branch, so both start from the same PR tip.
|
|
356
359
|
TOGETHER = """[ "$nn" != 01 ] || until git rev-parse -q --verify refs/heads/ticket/02 > /dev/null; do sleep 0.02; done\n"""
|
|
357
360
|
REFACTOR = {"seam": "none — refactor"}
|
|
361
|
+
# A fixer that cannot make the red gate green: the ticket blocks at fix round 1.
|
|
362
|
+
GIVE_UP = """[ $role != fixer ] || { printf '{"status": "BLOCKED", "commits": [], "files": [], "concerns": ["the gate stays red"], "green": [], "ticket": "01"}' > "$report"; exit 0; }\n"""
|
|
363
|
+
GAVE_UP = "Ticket 01: BLOCKED at fix round 1: the fixer reported BLOCKED: the gate stays red."
|
|
358
364
|
|
|
359
365
|
|
|
360
366
|
def walk(tmp_path, capsys, body, *args, gate="true"):
|
|
@@ -413,7 +419,7 @@ def test_a_merge_conflict_goes_to_the_conflict_agent_once_and_its_commit_is_gate
|
|
|
413
419
|
number, tip = line[7:9], walked()
|
|
414
420
|
assert line.endswith(f" at {tip[:7]}.") and len(out_of("git", "rev-list", "--parents", "-n1", tip).split()) == 3
|
|
415
421
|
assert f"Ticket {number}: the gate is green on {tip[:7]}, record .agent/implement/{number}/gate-{tip[:7]}.log." in ledger
|
|
416
|
-
assert "| gates run | 4 |" in out and "| agents, conflict on claude-opus-5@medium | 1 |" in out
|
|
422
|
+
assert "| gates run | 4 |" in out and "| agents, conflict on claude-opus-5-5@medium | 1 |" in out
|
|
417
423
|
assert dict(state_of(tmp_path)["run"]["landed"])[number] == tip
|
|
418
424
|
prompt = (tmp_path / f".agent/implement/{number}/conflict-0.prompt.md").read_text()
|
|
419
425
|
assert "Files in conflict: work.txt\n" in prompt and prompt.rstrip().endswith("one ref store.")
|
|
@@ -449,13 +455,13 @@ def test_a_conflict_cli_that_fails_stops_the_run_and_leaves_no_open_merge(repo,
|
|
|
449
455
|
assert out_of("git", "-C", slot, "rev-parse", "-q", "--verify", "MERGE_HEAD") == ""
|
|
450
456
|
|
|
451
457
|
|
|
452
|
-
def
|
|
458
|
+
def test_a_red_gate_is_a_finding_and_the_ticket_it_blocks_never_starts(repo, tmp_path, capsys):
|
|
453
459
|
two(repo, second="01")
|
|
454
|
-
code, ledger, out = walk(tmp_path, capsys, WORK + REPORT, gate="echo CHECK-OUTPUT; exit 1")
|
|
460
|
+
code, ledger, out = walk(tmp_path, capsys, GIVE_UP + WORK + REPORT, gate="echo CHECK-OUTPUT; exit 1")
|
|
455
461
|
tip = out_of("git", "rev-parse", "--short=7", "ticket/01")
|
|
456
|
-
assert code == 1 and
|
|
457
|
-
assert ledger[1] == (f"Ticket 01:
|
|
458
|
-
f"
|
|
462
|
+
assert code == 1 and ledger[-1] == GAVE_UP, out
|
|
463
|
+
assert ledger[1] == (f"Ticket 01: critical finding of kind code from the gate at "
|
|
464
|
+
f".agent/implement/01/gate-{tip}.log:0: the gate is red on {tip}.")
|
|
459
465
|
record = (tmp_path / f".agent/implement/01/gate-{tip}.log").read_text()
|
|
460
466
|
assert "status: clean\n" in record and "CHECK-OUTPUT\n\nexit: 1\nafter: clean\nend: " in record
|
|
461
467
|
assert "Ticket 02 is not merged: its phase is pending." in out and "| gates run | 1 |" in out
|
|
@@ -476,8 +482,8 @@ def test_a_red_merge_gate_blocks_the_ticket_and_the_pr_branch_does_not_move(repo
|
|
|
476
482
|
|
|
477
483
|
def test_a_tree_that_is_not_clean_is_a_red_gate_and_the_slot_keeps_the_work(repo, tmp_path, capsys):
|
|
478
484
|
repo(t01=ticket("01", **REFACTOR))
|
|
479
|
-
code, ledger, out = walk(tmp_path, capsys, WORK + REPORT + "echo left > junk.txt\n", gate="echo NEVER")
|
|
480
|
-
assert code == 1 and "
|
|
485
|
+
code, ledger, out = walk(tmp_path, capsys, GIVE_UP + WORK + REPORT + "echo left > junk.txt\n", gate="echo NEVER")
|
|
486
|
+
assert code == 1 and "from the gate at .agent/implement/01/gate-" in ledger[-3] and ledger[-1] == GAVE_UP, out
|
|
481
487
|
record = next((tmp_path / ".agent/implement/01").glob("gate-*.log")).read_text()
|
|
482
488
|
assert "status: not clean\n ?? junk.txt\n" in record and "exit: not run" in record and "\nNEVER\n" not in record
|
|
483
489
|
assert out_of("git", "symbolic-ref", "--short", "HEAD") == "ticket/01" and (tmp_path / "junk.txt").exists()
|
|
@@ -726,8 +732,8 @@ def test_a_merge_commit_the_slot_cannot_check_out_is_not_gated_and_does_not_land
|
|
|
726
732
|
])
|
|
727
733
|
def test_a_gate_that_changes_the_tree_or_moves_head_is_red(repo, tmp_path, capsys, gate, after):
|
|
728
734
|
repo(t01=ticket("01", **REFACTOR))
|
|
729
|
-
code, ledger, out = walk(tmp_path, capsys, WORK + REPORT, gate=gate)
|
|
730
|
-
assert code == 1 and "
|
|
735
|
+
code, ledger, out = walk(tmp_path, capsys, GIVE_UP + WORK + REPORT, gate=gate)
|
|
736
|
+
assert code == 1 and "from the gate at .agent/implement/01/gate-" in ledger[-3] and ledger[-1] == GAVE_UP, out
|
|
731
737
|
record = next((tmp_path / ".agent/implement/01").glob("gate-*.log")).read_text()
|
|
732
738
|
assert "\nexit: 0\n" in record and after in record
|
|
733
739
|
assert state_of(tmp_path)["tickets"]["01"]["green_gates"] == []
|
|
@@ -794,7 +800,7 @@ def test_the_preflight_asks_each_used_entry_once_and_names_each_cli_that_fails(t
|
|
|
794
800
|
monkeypatch.setattr(driver_agents, "PREFLIGHT_MINUTES", 0.02)
|
|
795
801
|
models, _ = driver_agents.load_models(None)
|
|
796
802
|
models["reviewer"] = ["openai/gpt-astra@medium"]
|
|
797
|
-
assert driver_agents.used_entries(models) == ["claude-opus-5@medium", "claude-sonnet-5@medium",
|
|
803
|
+
assert driver_agents.used_entries(models) == ["claude-opus-5-5@medium", "claude-sonnet-5-5@medium",
|
|
798
804
|
"openai/gpt-6-astra@medium", "openai/gpt-6.1-sol@medium",
|
|
799
805
|
"openai/gpt-astra@medium"]
|
|
800
806
|
assert driver_agents.preflight(models) == [
|
|
@@ -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)]}))
|
|
@@ -8,10 +8,10 @@ import pytest
|
|
|
8
8
|
from test_cli import RUN, SKILL, drive, env, make_repo, three # noqa: F401 (env is a fixture)
|
|
9
9
|
|
|
10
10
|
STANDARD = {
|
|
11
|
-
"implementer": ["claude-sonnet-5@medium", "openai/gpt-6.1-sol@medium"],
|
|
12
|
-
"test-writer": ["claude-sonnet-5@medium", "openai/gpt-6.1-sol@medium"],
|
|
13
|
-
"reviewer": ["openai/gpt-6-astra@medium", "claude-opus-5@medium"],
|
|
14
|
-
"final-reviewer": ["openai/gpt-6-astra@medium", "claude-opus-5@medium"],
|
|
11
|
+
"implementer": ["claude-sonnet-5-5@medium", "openai/gpt-6.1-sol@medium"],
|
|
12
|
+
"test-writer": ["claude-sonnet-5-5@medium", "openai/gpt-6.1-sol@medium"],
|
|
13
|
+
"reviewer": ["openai/gpt-6-astra@medium", "claude-opus-5-5@medium"],
|
|
14
|
+
"final-reviewer": ["openai/gpt-6-astra@medium", "claude-opus-5-5@medium"],
|
|
15
15
|
}
|
|
16
16
|
ENTRY = re.compile(r"^(?:(?:anthropic|openai)/)?[\w.-]+@(?:minimal|low|medium|high|xhigh|max)$")
|
|
17
17
|
|
|
@@ -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)
|
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
package/skills-manifest.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"generatedAt": "2026-10-
|
|
3
|
+
"generatedAt": "2026-10-16T09:21:05.102Z",
|
|
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.
|
|
50
|
-
"hash": "sha256-
|
|
49
|
+
"version": "0.1.17",
|
|
50
|
+
"hash": "sha256-3e93a264e28048f143a4d39786bb30974e42b4da6a6f5da36a1ea3418b840632",
|
|
51
51
|
"description": "Implement a specification in code."
|
|
52
52
|
},
|
|
53
53
|
{
|