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