@jesdi/skills 0.0.30 → 0.0.31
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/README.md +12 -6
- package/package.json +2 -2
- package/skills/implement-spec/ARCHITECTURE.md +113 -0
- package/skills/implement-spec/README.md +214 -0
- package/skills/implement-spec/SKILL.md +18 -70
- package/skills/implement-spec/driver.py +580 -0
- package/skills/implement-spec/driver_agents.py +145 -0
- package/skills/implement-spec/driver_answers.py +79 -0
- package/skills/implement-spec/driver_decisions.py +152 -0
- package/skills/implement-spec/driver_final.py +505 -0
- package/skills/implement-spec/driver_fixloop.py +170 -0
- package/skills/implement-spec/driver_gate.py +47 -0
- package/skills/implement-spec/driver_graph.py +42 -0
- package/skills/implement-spec/driver_merge.py +105 -0
- package/skills/implement-spec/driver_report.py +115 -0
- package/skills/implement-spec/driver_review.py +126 -0
- package/skills/implement-spec/driver_review_page.py +51 -0
- package/skills/implement-spec/driver_run.py +262 -0
- package/skills/implement-spec/driver_slots.py +64 -0
- package/skills/implement-spec/driver_state.py +105 -0
- package/skills/implement-spec/driver_summary.py +40 -0
- package/skills/implement-spec/driver_tickets.py +132 -0
- package/skills/implement-spec/driver_tidy.py +57 -0
- package/skills/implement-spec/models.json +10 -0
- package/skills/implement-spec/prompts/conflict.md +32 -0
- package/skills/implement-spec/prompts/correction.md +4 -0
- package/skills/implement-spec/prompts/correctness.md +34 -0
- package/skills/implement-spec/prompts/final-fixer.md +36 -0
- package/skills/implement-spec/prompts/final-reviewer.md +36 -0
- package/skills/implement-spec/prompts/final-scoped.md +1 -0
- package/skills/implement-spec/prompts/fixer.md +35 -0
- package/skills/implement-spec/prompts/implementer.md +33 -0
- package/skills/implement-spec/prompts/interrupted.md +1 -0
- package/skills/implement-spec/prompts/moved.md +2 -0
- package/skills/implement-spec/prompts/overruled.md +5 -0
- package/skills/implement-spec/prompts/pr-body.md +19 -0
- package/skills/implement-spec/prompts/rebase-conflict.md +33 -0
- package/skills/implement-spec/prompts/rereview.md +3 -0
- package/skills/implement-spec/prompts/rerun.md +2 -0
- package/skills/implement-spec/prompts/reviewer.md +34 -0
- package/skills/implement-spec/prompts/stands.md +2 -0
- package/skills/implement-spec/prompts/test-writer.md +34 -0
- package/skills/implement-spec/prompts/timeout-restored.md +2 -0
- package/skills/implement-spec/prompts/timeout-shared.md +2 -0
- package/skills/implement-spec/prompts/timeout.md +2 -0
- package/skills/implement-spec/run.sh +101 -0
- package/skills/implement-spec/tests/conftest.py +38 -0
- package/skills/implement-spec/tests/test_agents.py +358 -0
- package/skills/implement-spec/tests/test_amendments.py +117 -0
- package/skills/implement-spec/tests/test_cli.py +220 -0
- package/skills/implement-spec/tests/test_decisions.py +434 -0
- package/skills/implement-spec/tests/test_driver.py +767 -0
- package/skills/implement-spec/tests/test_final.py +442 -0
- package/skills/implement-spec/tests/test_fixloop.py +523 -0
- package/skills/implement-spec/tests/test_graph.py +32 -0
- package/skills/implement-spec/tests/test_landing.py +355 -0
- package/skills/implement-spec/tests/test_merge.py +144 -0
- package/skills/implement-spec/tests/test_models.py +47 -0
- package/skills/implement-spec/tests/test_report.py +162 -0
- package/skills/implement-spec/tests/test_review.py +338 -0
- package/skills/implement-spec/tests/test_review_page.py +67 -0
- package/skills/implement-spec/tests/test_slots.py +32 -0
- package/skills/implement-spec/tests/test_state.py +68 -0
- package/skills/implement-spec/tests/test_stops.py +272 -0
- package/skills/implement-spec/tests/test_summary.py +28 -0
- package/skills/implement-spec/tests/test_tickets.py +72 -0
- package/skills/implement-spec/tests/test_tidy.py +582 -0
- package/skills/implement-spec/tests/test_walk.py +457 -0
- package/skills/review-page/template.html +1 -1
- package/skills/to-tickets/SKILL.md +19 -2
- package/skills-manifest.json +7 -7
- package/skills/implement-spec/history.md +0 -52
|
@@ -0,0 +1,457 @@
|
|
|
1
|
+
"""Ticket 03 acceptance tests: walk the graph with slots, gate by the driver, merge, resume.
|
|
2
|
+
|
|
3
|
+
All through `sh run.sh --runner <script> --parallel N [--prepare <cmd>] --gate <cmd>` on the three-ticket fixture
|
|
4
|
+
repository. The fake runner plays test-writer and implementer per ticket; it logs, per call, its cwd, branch, HEAD,
|
|
5
|
+
start and end time into `<rig>/calls/`. The gate is a small script that logs its commit, status, branch and
|
|
6
|
+
environment into `<rig>/gates/`. Concurrency is steered with signal files (`touch` / `wait` plan keys), never sleeps.
|
|
7
|
+
|
|
8
|
+
Interface choices (ticket 03):
|
|
9
|
+
- gate record: `.agent/implement/<NN>/gate-<first 7 hex of the commit>.log`.
|
|
10
|
+
- `summary.md` sits in `.agent/implement/` and its table is repeated on stdout.
|
|
11
|
+
- `state.json` is `{"tickets": {"NN": {phase, round, tip, test_writer_sha, locked, green_gates, slot}}}`;
|
|
12
|
+
`green_gates` holds full SHAs (a dict with a `commit` key is accepted too).
|
|
13
|
+
|
|
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
|
|
16
|
+
that tip, `Rig.pr_subjects()` reads it, and `Rig.walk_gates()` leaves out the one gate of the end of run.
|
|
17
|
+
"""
|
|
18
|
+
import json
|
|
19
|
+
import os
|
|
20
|
+
import re
|
|
21
|
+
import shutil
|
|
22
|
+
import signal
|
|
23
|
+
import subprocess
|
|
24
|
+
import sys
|
|
25
|
+
import time
|
|
26
|
+
from pathlib import Path
|
|
27
|
+
|
|
28
|
+
import pytest
|
|
29
|
+
|
|
30
|
+
from test_cli import RUN, TOOLS, git, make_repo, models_file, ticket, three
|
|
31
|
+
|
|
32
|
+
RUNNER = f"""#!{sys.executable}
|
|
33
|
+
import json, os, subprocess, sys, time
|
|
34
|
+
from pathlib import Path
|
|
35
|
+
rig = Path(__file__).resolve().parent
|
|
36
|
+
role, prompt, report = sys.argv[1:4]
|
|
37
|
+
nn = Path(report).parent.name
|
|
38
|
+
key = f"{{nn}}/{{role}}"
|
|
39
|
+
def git(*a):
|
|
40
|
+
cmd = ["git", "-c", "user.name=t", "-c", "user.email=t@example.com", "-c", "commit.gpgsign=false", *a]
|
|
41
|
+
return subprocess.run(cmd, capture_output=True, text=True).stdout.strip()
|
|
42
|
+
def save(rec):
|
|
43
|
+
tmp = rig / "calls" / (rec["id"] + ".tmp")
|
|
44
|
+
tmp.write_text(json.dumps(rec))
|
|
45
|
+
os.replace(tmp, rig / "calls" / (rec["id"] + ".json"))
|
|
46
|
+
def where():
|
|
47
|
+
return dict(branch=git("rev-parse", "--abbrev-ref", "HEAD"), head=git("rev-parse", "HEAD"),
|
|
48
|
+
status=git("status", "--porcelain"))
|
|
49
|
+
def wait(done):
|
|
50
|
+
end = time.time() + 6
|
|
51
|
+
while not done():
|
|
52
|
+
if time.time() > end:
|
|
53
|
+
sys.exit(7)
|
|
54
|
+
time.sleep(0.02)
|
|
55
|
+
(rig / "calls").mkdir(exist_ok=True)
|
|
56
|
+
(rig / "sig").mkdir(exist_ok=True)
|
|
57
|
+
n = sum(1 for f in (rig / "calls").glob("*.json") if json.loads(f.read_text())["key"] == key)
|
|
58
|
+
rec = dict(id=f"{{time.time_ns()}}-{{os.getpid()}}", key=key, nn=nn, role=role, t0=time.time(),
|
|
59
|
+
cwd=os.path.realpath(os.getcwd()), prompt=Path(prompt).read_text(), **where(),
|
|
60
|
+
log=git("log", "--format=%s", "HEAD"), pr_log=git("log", "--format=%s", "feat/csv"))
|
|
61
|
+
save(rec)
|
|
62
|
+
step = json.loads((rig / "plan.json").read_text()).get(key, [])
|
|
63
|
+
step = step[n] if n < len(step) else {{}}
|
|
64
|
+
if role in ("reviewer", "correctness", "final-reviewer") and "report" not in step: # a role the test does not script reports no findings
|
|
65
|
+
reds = [f"FAILED {{p}}::test_x - AttributeError" for p in git("diff", "--name-only", "feat/csv...HEAD", "--", "tests").split()]
|
|
66
|
+
proofs = [dict(test=r.split()[1], file="demo/export.py", line=1, red=r) for r in reds]
|
|
67
|
+
step = dict(step, stdout="".join(r + "\\n" for r in reds), report=dict(
|
|
68
|
+
status="DONE", commits=[], files=[], concerns=[], findings=[], mutations=proofs))
|
|
69
|
+
for name in step.get("touch", []):
|
|
70
|
+
(rig / "sig" / name).touch()
|
|
71
|
+
for name in step.get("wait", []):
|
|
72
|
+
wait(lambda: (rig / "sig" / name).exists())
|
|
73
|
+
if step.get("wait_pr_moves"):
|
|
74
|
+
wait(lambda: git("rev-parse", "feat/csv") != git("merge-base", "HEAD", "feat/csv"))
|
|
75
|
+
rec["after_wait"] = where()
|
|
76
|
+
for path, text in step.get("files", {{}}).items():
|
|
77
|
+
Path(path).parent.mkdir(parents=True, exist_ok=True)
|
|
78
|
+
Path(path).write_text(text)
|
|
79
|
+
sys.stdout.write(step.get("stdout", ""))
|
|
80
|
+
sys.stdout.flush()
|
|
81
|
+
time.sleep(step.get("sleep", 0))
|
|
82
|
+
sha = ""
|
|
83
|
+
if "commit" in step:
|
|
84
|
+
git("add", "-A")
|
|
85
|
+
git("commit", "-q", "-m", step["commit"])
|
|
86
|
+
sha = git("rev-parse", "HEAD")
|
|
87
|
+
if step.get("report") is not None:
|
|
88
|
+
Path(report).parent.mkdir(parents=True, exist_ok=True)
|
|
89
|
+
Path(report).write_text(json.dumps(step["report"]).replace("$SHA", sha))
|
|
90
|
+
rec.update(t1=time.time(), sha=sha)
|
|
91
|
+
save(rec)
|
|
92
|
+
"""
|
|
93
|
+
|
|
94
|
+
GATE = f"""#!{sys.executable}
|
|
95
|
+
import json, os, subprocess, sys, time
|
|
96
|
+
from pathlib import Path
|
|
97
|
+
rig = Path(__file__).resolve().parent
|
|
98
|
+
def git(*a):
|
|
99
|
+
return subprocess.run(["git", *a], capture_output=True, text=True).stdout.strip()
|
|
100
|
+
conf = json.loads((rig / "gate.json").read_text())
|
|
101
|
+
(rig / "gates").mkdir(exist_ok=True)
|
|
102
|
+
branch = git("rev-parse", "--abbrev-ref", "HEAD")
|
|
103
|
+
code = 1 if conf.get("all") or branch in conf.get("red", []) else 0
|
|
104
|
+
rec = dict(t0=time.time(), cwd=os.path.realpath(os.getcwd()), commit=git("rev-parse", "HEAD"), branch=branch,
|
|
105
|
+
status=git("status", "--short"), base=os.environ.get("IMPLEMENT_SPEC_BASE"),
|
|
106
|
+
pycache=os.environ.get("PYTHONPYCACHEPREFIX"), exit=code)
|
|
107
|
+
print("GATE-OUTPUT-MARK", rec["commit"])
|
|
108
|
+
(rig / "gates" / f"{{time.time_ns()}}-{{os.getpid()}}.json").write_text(json.dumps(rec))
|
|
109
|
+
sys.exit(code)
|
|
110
|
+
"""
|
|
111
|
+
|
|
112
|
+
COST = '{"total_cost_usd": 0.25}\n'
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
def tw(nn):
|
|
116
|
+
path, red = f"tests/test_{nn}.py", f"FAILED tests/test_{nn}.py::test_x - AttributeError"
|
|
117
|
+
return {"files": {path: f"def test_x():\n assert {nn} == 'x'\n"}, "stdout": red + "\n" + COST,
|
|
118
|
+
"commit": f"test: {nn} red",
|
|
119
|
+
"report": {"status": "DONE", "commits": ["$SHA"], "files": [path], "concerns": [], "locked": [path],
|
|
120
|
+
"red": [{"test": f"{path}::test_x", "line": red}]}}
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def impl(nn, path="demo/export.py", text=None, **changes):
|
|
124
|
+
step = {"files": {path: text or f"def fn_{nn}():\n return {nn!r}\n"}, "stdout": COST,
|
|
125
|
+
"commit": f"feat: {nn} rows",
|
|
126
|
+
"report": {"status": "DONE", "commits": ["$SHA"], "files": [path], "concerns": [], "green": ["1 passed"]}}
|
|
127
|
+
return {**step, **changes}
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
PATHS = {"01": "demo/export.py", "02": "demo/export.py", "03": "demo/cli.py"}
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def plan(**changes):
|
|
134
|
+
"""Every ticket gets a test-writer and an implementer step; `changes` replace or extend single steps."""
|
|
135
|
+
steps = {}
|
|
136
|
+
for nn, path in PATHS.items():
|
|
137
|
+
steps[f"{nn}/test-writer"] = [tw(nn)]
|
|
138
|
+
steps[f"{nn}/implementer"] = [impl(nn, path)]
|
|
139
|
+
steps.update({key.replace("_", "/", 1).replace("_", "-"): value for key, value in changes.items()})
|
|
140
|
+
return steps
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def make_env(tmp):
|
|
144
|
+
bin_dir = tmp / "bin"
|
|
145
|
+
bin_dir.mkdir()
|
|
146
|
+
for name in TOOLS:
|
|
147
|
+
if shutil.which(name):
|
|
148
|
+
(bin_dir / name).symlink_to(shutil.which(name))
|
|
149
|
+
for cli in ("claude", "codex"):
|
|
150
|
+
(bin_dir / cli).write_text("#!/bin/sh\nexit 1\n")
|
|
151
|
+
(bin_dir / cli).chmod(0o755)
|
|
152
|
+
return dict(os.environ, PATH=str(bin_dir))
|
|
153
|
+
|
|
154
|
+
|
|
155
|
+
class Rig:
|
|
156
|
+
def __init__(self, tmp, files=None, gate=None, ignore=None):
|
|
157
|
+
self.tmp, self.env = tmp, make_env(tmp)
|
|
158
|
+
self.repo = make_repo(tmp / "demo", files or three())
|
|
159
|
+
(self.repo / "specs/csv").mkdir(parents=True)
|
|
160
|
+
(self.repo / "specs/csv/spec.md").write_text("# csv spec\n")
|
|
161
|
+
if ignore:
|
|
162
|
+
with (self.repo / ".gitignore").open("a") as f:
|
|
163
|
+
f.write(ignore + "\n")
|
|
164
|
+
git(self.repo, "add", "-A")
|
|
165
|
+
git(self.repo, "commit", "-q", "-m", "spec")
|
|
166
|
+
self.rig = tmp / "rig"
|
|
167
|
+
self.rig.mkdir()
|
|
168
|
+
for name, text in (("run-agent", RUNNER), ("gate", GATE)):
|
|
169
|
+
(self.rig / name).write_text(text)
|
|
170
|
+
(self.rig / name).chmod(0o755)
|
|
171
|
+
self.models = models_file(tmp)
|
|
172
|
+
self.gate_conf(**(gate or {}))
|
|
173
|
+
self.slot2 = (self.repo / ".agent/worktrees/slot-2").resolve()
|
|
174
|
+
|
|
175
|
+
def gate_conf(self, **conf):
|
|
176
|
+
(self.rig / "gate.json").write_text(json.dumps(conf))
|
|
177
|
+
|
|
178
|
+
def run(self, steps, *extra, parallel="2", watch=False):
|
|
179
|
+
"""Run the driver once; with `watch`, read state.json in a tight loop meanwhile (torn reads, snapshots)."""
|
|
180
|
+
(self.rig / "plan.json").write_text(json.dumps(steps))
|
|
181
|
+
args = ["--tickets", ".agent/tickets", "--gate", str(self.rig / "gate"), "--spec", "specs/csv/spec.md",
|
|
182
|
+
"--models", str(self.models), "--runner", str(self.rig / "run-agent"), "--parallel", parallel, *extra]
|
|
183
|
+
out, err = self.tmp / "out.txt", self.tmp / "err.txt"
|
|
184
|
+
with out.open("w") as o, err.open("w") as e:
|
|
185
|
+
proc = subprocess.Popen(["sh", str(RUN), *args], cwd=self.repo, env=self.env, text=True, stdout=o,
|
|
186
|
+
stderr=e, start_new_session=True)
|
|
187
|
+
self.snaps, self.torn = [], []
|
|
188
|
+
state, end = self.repo / ".agent/implement/state.json", time.time() + 120
|
|
189
|
+
while proc.poll() is None and time.time() < end:
|
|
190
|
+
if watch:
|
|
191
|
+
try:
|
|
192
|
+
self.snaps.append(json.loads(state.read_text()))
|
|
193
|
+
except FileNotFoundError:
|
|
194
|
+
pass
|
|
195
|
+
except json.JSONDecodeError as exc:
|
|
196
|
+
self.torn.append(str(exc))
|
|
197
|
+
time.sleep(0.002)
|
|
198
|
+
if proc.poll() is None:
|
|
199
|
+
os.killpg(proc.pid, signal.SIGKILL)
|
|
200
|
+
proc.wait()
|
|
201
|
+
self.code, self.out, self.err = proc.returncode, out.read_text(), err.read_text()
|
|
202
|
+
return self
|
|
203
|
+
|
|
204
|
+
def calls(self, nn=None, role=None):
|
|
205
|
+
folder = self.rig / "calls"
|
|
206
|
+
rows = [json.loads(p.read_text()) for p in folder.glob("*.json")] if folder.exists() else []
|
|
207
|
+
rows = [r for r in rows if nn in (None, r["nn"]) and role in (None, r["role"])]
|
|
208
|
+
return sorted(rows, key=lambda r: r["t0"])
|
|
209
|
+
|
|
210
|
+
def sha(self, nn, role):
|
|
211
|
+
return self.calls(nn, role)[0]["sha"]
|
|
212
|
+
|
|
213
|
+
def gates(self):
|
|
214
|
+
folder = self.rig / "gates"
|
|
215
|
+
rows = [json.loads(p.read_text()) for p in folder.glob("*.json")] if folder.exists() else []
|
|
216
|
+
return sorted(rows, key=lambda r: r["t0"])
|
|
217
|
+
|
|
218
|
+
def ledger(self):
|
|
219
|
+
path = self.repo / ".agent/implement/ledger.md"
|
|
220
|
+
return path.read_text().splitlines() if path.exists() else []
|
|
221
|
+
|
|
222
|
+
def state(self):
|
|
223
|
+
return json.loads((self.repo / ".agent/implement/state.json").read_text())
|
|
224
|
+
|
|
225
|
+
def walked(self):
|
|
226
|
+
"""The PR tip as the walk left it: the recovery point of the end-of-run tidy, or the branch with no tidy."""
|
|
227
|
+
out = subprocess.run(["git", "rev-parse", "--verify", "-q", "refs/implement/recovery"], cwd=self.repo,
|
|
228
|
+
capture_output=True, text=True).stdout.strip()
|
|
229
|
+
return out or git(self.repo, "rev-parse", "feat/csv").strip()
|
|
230
|
+
|
|
231
|
+
def walk_gates(self):
|
|
232
|
+
"""The gates of the walk: all but the gate of the end of run, which is on a tidied tip the walk never made."""
|
|
233
|
+
made = git(self.repo, "rev-list", self.walked()).split()
|
|
234
|
+
return [g for g in self.gates() if g["commit"] in made]
|
|
235
|
+
|
|
236
|
+
def pr_subjects(self):
|
|
237
|
+
return git(self.repo, "log", "--format=%s", self.walked()).splitlines()
|
|
238
|
+
|
|
239
|
+
def branches(self):
|
|
240
|
+
return git(self.repo, "branch", "--list", "ticket/*").split()
|
|
241
|
+
|
|
242
|
+
def output(self):
|
|
243
|
+
return f"exit {self.code}\n{self.out}{self.err}"
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
def commit_of(entry):
|
|
247
|
+
return entry if isinstance(entry, str) else entry["commit"]
|
|
248
|
+
|
|
249
|
+
|
|
250
|
+
def green(state, nn):
|
|
251
|
+
return {commit_of(e) for e in state["tickets"][nn]["green_gates"]}
|
|
252
|
+
|
|
253
|
+
|
|
254
|
+
@pytest.fixture(scope="module")
|
|
255
|
+
def std(tmp_path_factory):
|
|
256
|
+
"""The default scenario: three tickets, --parallel 2, 01's implementer holds until 03 is in flight."""
|
|
257
|
+
rig = Rig(tmp_path_factory.mktemp("std"))
|
|
258
|
+
steps = plan(**{"03_test-writer": [{**tw("03"), "touch": ["s3"]}], "01_implementer": [{**impl("01"), "wait": ["s3"]}]})
|
|
259
|
+
return rig.run(steps, watch=True)
|
|
260
|
+
|
|
261
|
+
|
|
262
|
+
@pytest.fixture(scope="module")
|
|
263
|
+
def blocked(tmp_path_factory):
|
|
264
|
+
"""The default scenario with a red gate on ticket/01."""
|
|
265
|
+
return Rig(tmp_path_factory.mktemp("blocked"), gate={"red": ["ticket/01"]}).run(plan())
|
|
266
|
+
|
|
267
|
+
|
|
268
|
+
def test_01_starts_in_slot_1_03_in_slot_2_and_02_after_the_merge_of_01(std):
|
|
269
|
+
assert std.code == 0, std.output()
|
|
270
|
+
first = lambda nn: std.calls(nn)[0]
|
|
271
|
+
assert first("01")["cwd"] == str(std.repo.resolve()) and first("03")["cwd"] == str(std.slot2)
|
|
272
|
+
assert "feat: 01 rows" in first("02")["pr_log"] and "feat: 01 rows" in first("02")["log"]
|
|
273
|
+
assert first("02")["t0"] > std.calls("01")[-1]["t1"] and first("02")["branch"] == "ticket/02"
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def test_touches_overlap_holds_03_until_01_is_merged(tmp_path):
|
|
277
|
+
files = three(**{"03-cli-flag.md": ticket("03", "CLI flag", seam="`demo export` CLI",
|
|
278
|
+
touches="`demo/export.py: export_csv`")})
|
|
279
|
+
rig = Rig(tmp_path, files).run(plan())
|
|
280
|
+
assert rig.code == 0, rig.output()
|
|
281
|
+
assert rig.calls("03"), "03 never started"
|
|
282
|
+
third = rig.calls("03")[0]
|
|
283
|
+
assert third["t0"] >= rig.calls("01")[-1]["t1"]
|
|
284
|
+
assert "feat: 01 rows" in third["pr_log"] and "feat: 01 rows" in third["log"]
|
|
285
|
+
|
|
286
|
+
|
|
287
|
+
def test_prepare_runs_once_in_slot_2_and_the_slot_is_reused(tmp_path):
|
|
288
|
+
files = three(**{"02-quote-fields.md": ticket("02", "Quote fields", "03", touches="`demo/other.py: g`")})
|
|
289
|
+
rig = Rig(tmp_path, files, ignore="node_modules")
|
|
290
|
+
(rig.repo / "node_modules").mkdir()
|
|
291
|
+
prepare = f"ln -s ../../../node_modules node_modules && pwd -P >> {rig.tmp}/prepare.log"
|
|
292
|
+
steps = plan(**{"02_test-writer": [{**tw("02"), "touch": ["s2"]}],
|
|
293
|
+
"01_implementer": [{**impl("01"), "wait": ["s2"]}],
|
|
294
|
+
"02_implementer": [impl("02", "demo/other.py")]})
|
|
295
|
+
rig.run(steps, "--prepare", prepare)
|
|
296
|
+
assert rig.code == 0, rig.output()
|
|
297
|
+
assert (rig.tmp / "prepare.log").read_text().split() == [str(rig.slot2)]
|
|
298
|
+
assert rig.calls("03")[0]["cwd"] == rig.calls("02")[0]["cwd"] == str(rig.slot2)
|
|
299
|
+
assert (rig.slot2 / "node_modules").is_symlink() and all(g["status"] == "" for g in rig.gates())
|
|
300
|
+
|
|
301
|
+
|
|
302
|
+
def test_failing_prepare_exits_3_before_any_agent_naming_slot_and_command(tmp_path):
|
|
303
|
+
rig = Rig(tmp_path).run(plan(), "--prepare", "false")
|
|
304
|
+
assert rig.code == 3, rig.output()
|
|
305
|
+
assert re.search(r"slot[- ]?2", rig.out + rig.err) and "false" in rig.out + rig.err
|
|
306
|
+
assert not rig.calls() and not rig.gates()
|
|
307
|
+
|
|
308
|
+
|
|
309
|
+
def test_fast_forward_merge_has_exactly_one_gate_record_and_no_merge_gate(tmp_path):
|
|
310
|
+
rig = Rig(tmp_path, {"01-export-rows.md": ticket("01", "Export rows")}).run(plan())
|
|
311
|
+
assert rig.code == 0, rig.output()
|
|
312
|
+
a1 = rig.sha("01", "implementer")
|
|
313
|
+
assert rig.walked() == a1
|
|
314
|
+
records = sorted((rig.repo / ".agent/implement/01").glob("gate-*.log"))
|
|
315
|
+
assert [p.name for p in records] == [f"gate-{a1[:7]}.log"]
|
|
316
|
+
text = records[0].read_text()
|
|
317
|
+
assert a1[:7] in text and "GATE-OUTPUT-MARK" in text and len(re.findall(r"\d\d:\d\d:\d\d", text)) >= 2
|
|
318
|
+
assert re.search(r"exit\D*0", text) and not re.search(r"^(\?\?| M|M |A )", text, re.M)
|
|
319
|
+
(gate,) = rig.walk_gates()
|
|
320
|
+
assert (gate["commit"], gate["status"], gate["cwd"]) == (a1, "", str(rig.repo.resolve()))
|
|
321
|
+
|
|
322
|
+
|
|
323
|
+
def test_gate_environment_names_the_pr_branch_and_a_pycache_prefix_under_agent_implement(tmp_path):
|
|
324
|
+
rig = Rig(tmp_path, {"01-export-rows.md": ticket("01", "Export rows")}).run(plan())
|
|
325
|
+
assert rig.code == 0, rig.output()
|
|
326
|
+
(gate,) = rig.walk_gates()
|
|
327
|
+
assert gate["base"] == "feat/csv" and gate["pycache"]
|
|
328
|
+
assert os.path.realpath(gate["pycache"]).startswith(str((rig.repo / ".agent/implement").resolve()) + os.sep)
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def test_red_gate_blocks_the_ticket_names_the_record_and_launches_no_later_role(blocked):
|
|
332
|
+
assert blocked.code == 1, blocked.output()
|
|
333
|
+
a1 = blocked.sha("01", "implementer")
|
|
334
|
+
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()
|
|
335
|
+
assert re.search(r"exit\D*1", (blocked.repo / f".agent/implement/01/gate-{a1[:7]}.log").read_text())
|
|
336
|
+
assert {c["role"] for c in blocked.calls("01")} == {"test-writer", "implementer"}
|
|
337
|
+
assert blocked.state()["tickets"]["01"]["phase"] == "blocked"
|
|
338
|
+
assert "ticket/01" in blocked.branches() and "feat: 01 rows" not in blocked.pr_subjects()
|
|
339
|
+
|
|
340
|
+
|
|
341
|
+
def test_blocked_ticket_stops_only_the_tickets_it_blocks(blocked):
|
|
342
|
+
assert not blocked.calls("02"), "02 is blocked by 01 and must not start"
|
|
343
|
+
assert "feat: 03 rows" in blocked.pr_subjects() and blocked.state()["tickets"]["03"]["phase"] == "merged"
|
|
344
|
+
assert blocked.code == 1
|
|
345
|
+
|
|
346
|
+
|
|
347
|
+
def test_non_fast_forward_merge_is_a_plumbing_merge_commit_gated_once(tmp_path):
|
|
348
|
+
files = {k: v for k, v in three().items() if not k.startswith("02")}
|
|
349
|
+
rig = Rig(tmp_path, files)
|
|
350
|
+
steps = plan(**{"01_test-writer": [{**tw("01"), "touch": ["s1"]}],
|
|
351
|
+
"03_implementer": [{**impl("03", "demo/cli.py"), "wait": ["s1"]}],
|
|
352
|
+
"01_implementer": [{**impl("01"), "wait_pr_moves": True}]})
|
|
353
|
+
rig.run(steps)
|
|
354
|
+
assert rig.code == 0, rig.output()
|
|
355
|
+
a01, a03 = rig.sha("01", "implementer"), rig.sha("03", "implementer")
|
|
356
|
+
tip = rig.walked()
|
|
357
|
+
assert len(git(rig.repo, "rev-list", "--parents", "-n1", tip).split()) == 3, "tip is a merge commit"
|
|
358
|
+
commits = [g["commit"] for g in rig.walk_gates()]
|
|
359
|
+
assert sorted(commits) == sorted([a01, a03, tip]), commits
|
|
360
|
+
waited = rig.calls("01", "implementer")[0]
|
|
361
|
+
assert waited["after_wait"]["branch"] == "ticket/01" and waited["after_wait"]["head"] == waited["head"]
|
|
362
|
+
assert waited["after_wait"]["status"] == ""
|
|
363
|
+
assert git(rig.repo, "rev-parse", "--abbrev-ref", "HEAD").strip() == "feat/csv"
|
|
364
|
+
assert git(rig.repo, "status", "--short") == "" and (rig.repo / "demo/cli.py").exists()
|
|
365
|
+
assert "fn_01" in (rig.repo / "demo/export.py").read_text()
|
|
366
|
+
|
|
367
|
+
|
|
368
|
+
def test_merged_ticket_branches_are_deleted_and_their_commits_are_ancestors(std):
|
|
369
|
+
assert std.code == 0, std.output()
|
|
370
|
+
assert std.branches() == []
|
|
371
|
+
for nn in ("01", "02", "03"):
|
|
372
|
+
for role in ("test-writer", "implementer"):
|
|
373
|
+
git(std.repo, "merge-base", "--is-ancestor", std.sha(nn, role), std.walked())
|
|
374
|
+
assert git(std.repo, "rev-parse", "--abbrev-ref", "HEAD").strip() == "feat/csv"
|
|
375
|
+
assert git(std.repo, "status", "--short") == ""
|
|
376
|
+
|
|
377
|
+
|
|
378
|
+
def test_state_json_is_atomic_and_no_transition_is_lost_with_two_tickets_in_flight(std):
|
|
379
|
+
assert std.code == 0, std.output()
|
|
380
|
+
assert not std.torn and std.snaps, std.torn
|
|
381
|
+
seen_phase, seen_green = {}, {}
|
|
382
|
+
for snap in std.snaps:
|
|
383
|
+
for nn, entry in snap["tickets"].items():
|
|
384
|
+
if seen_phase.get(nn) == "merged":
|
|
385
|
+
assert entry["phase"] == "merged", f"{nn} regressed to {entry['phase']}"
|
|
386
|
+
seen_phase[nn] = entry["phase"]
|
|
387
|
+
assert seen_green.get(nn, set()) <= green(snap, nn), f"{nn} lost a recorded gate"
|
|
388
|
+
seen_green[nn] = green(snap, nn)
|
|
389
|
+
final = std.state()
|
|
390
|
+
for nn in ("01", "02", "03"):
|
|
391
|
+
entry = final["tickets"][nn]
|
|
392
|
+
assert entry["phase"] == "merged" and entry["round"] == 0
|
|
393
|
+
assert std.sha(nn, "implementer") in green(final, nn)
|
|
394
|
+
assert entry["test_writer_sha"] == std.sha(nn, "test-writer") and entry["locked"] == [f"tests/test_{nn}.py"]
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
def test_ledger_is_plain_sentences_written_by_the_driver_and_named_in_no_prompt(std):
|
|
398
|
+
ledger = std.ledger()
|
|
399
|
+
assert ledger and all(l.strip() == l and l and not l.startswith(("#", "|", "-", "*", "{")) for l in ledger), ledger
|
|
400
|
+
for nn in ("01", "02", "03"):
|
|
401
|
+
assert [l for l in ledger if nn in l and "merged" in l.lower()], (nn, ledger)
|
|
402
|
+
# the final reviewer is the one agent that reads it (requirement 19): it runs here since ticket 08
|
|
403
|
+
assert not [c for c in std.calls() if c["nn"] != "final" and "ledger" in c["prompt"].lower()]
|
|
404
|
+
|
|
405
|
+
|
|
406
|
+
def single_blocked(tmp_path):
|
|
407
|
+
"""Run 1 of a resume test: one ticket, red gate, so state.json holds a test-writer commit and a tip."""
|
|
408
|
+
rig = Rig(tmp_path, {"01-export-rows.md": ticket("01", "Export rows")}, gate={"all": True})
|
|
409
|
+
rig.run({**plan(), "01/implementer": [impl("01"), impl("01", text="def fn_01():\n return 'again'\n",
|
|
410
|
+
commit="feat: 01 again")]})
|
|
411
|
+
assert rig.code == 1, rig.output()
|
|
412
|
+
return rig
|
|
413
|
+
|
|
414
|
+
|
|
415
|
+
def test_resume_returns_a_ticket_with_an_unverifiable_tip_to_implement_from_the_test_writer_commit(tmp_path):
|
|
416
|
+
rig = single_blocked(tmp_path)
|
|
417
|
+
state, fake = rig.state(), "1" * 40
|
|
418
|
+
tw_sha = rig.sha("01", "test-writer")
|
|
419
|
+
assert state["tickets"]["01"]["test_writer_sha"] == tw_sha
|
|
420
|
+
state["tickets"]["01"].update(phase="review", tip=fake, green_gates=[])
|
|
421
|
+
(rig.repo / ".agent/implement/state.json").write_text(json.dumps(state))
|
|
422
|
+
before = rig.ledger()
|
|
423
|
+
rig.gate_conf()
|
|
424
|
+
rig.run({**plan(), "01/implementer": [impl("01"), impl("01", text="def fn_01():\n return 'again'\n",
|
|
425
|
+
commit="feat: 01 again")]})
|
|
426
|
+
assert rig.code == 0, rig.output()
|
|
427
|
+
again = rig.calls("01", "implementer")[1]
|
|
428
|
+
assert (again["branch"], again["head"]) == ("ticket/01", tw_sha) and len(rig.calls("01", "test-writer")) == 1
|
|
429
|
+
assert [l for l in rig.ledger()[len(before):] if "01" in l and fake[:7] in l], rig.ledger()
|
|
430
|
+
assert "feat: 01 again" in rig.pr_subjects()
|
|
431
|
+
|
|
432
|
+
|
|
433
|
+
def test_resume_keeps_a_settled_gate_and_runs_no_gate_again(tmp_path):
|
|
434
|
+
rig = single_blocked(tmp_path)
|
|
435
|
+
a1, state = rig.sha("01", "implementer"), rig.state()
|
|
436
|
+
state["tickets"]["01"].update(phase="merge", tip=a1, green_gates=[a1])
|
|
437
|
+
(rig.repo / ".agent/implement/state.json").write_text(json.dumps(state))
|
|
438
|
+
gates_before = len(rig.gates())
|
|
439
|
+
rig.gate_conf(red=["ticket/01"]) # any gate run on the ticket would block it; the end of run gates feat/csv
|
|
440
|
+
rig.run(plan())
|
|
441
|
+
assert rig.code == 0, rig.output()
|
|
442
|
+
assert len(rig.walk_gates()) == gates_before and rig.walked() == a1
|
|
443
|
+
assert [g["commit"] for g in rig.gates()].count(a1) == 1, "the settled commit is gated once in all"
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
def test_summary_md_and_stdout_carry_the_measurement_table(std):
|
|
447
|
+
summary = (std.repo / ".agent/implement/summary.md").read_text()
|
|
448
|
+
rows = [l for l in summary.splitlines() if l.startswith("|")]
|
|
449
|
+
assert rows and all(row in std.out for row in rows), summary
|
|
450
|
+
lines = summary.splitlines()
|
|
451
|
+
line = lambda *words: [l for l in lines if all(w in l.lower() for w in words)]
|
|
452
|
+
assert [l for l in line("gate") if re.search(rf"\b{len(std.gates())}\b", l)], summary
|
|
453
|
+
for role in ("test-writer", "implementer"):
|
|
454
|
+
assert [l for l in line(role) if "anthropic/claude-opus-5@medium" in l and re.search(r"\b3\b", l)], summary
|
|
455
|
+
assert [l for l in line(role) if re.search(r"\d+(\.\d+)?\s*s\b", l)], summary
|
|
456
|
+
assert all(line(f"{nn}: 0") for nn in ("01", "02", "03")) and [l for l in line("stop") if "0" in l], summary
|
|
457
|
+
assert re.search(r"1\.5", summary), "six agents at 0.25 each"
|
|
@@ -55,7 +55,7 @@ ol.tickets .n { font: 500 12px var(--mono); color: var(--muted); font-variant-nu
|
|
|
55
55
|
ol.tickets .by { color: var(--muted); font-size: 12px }
|
|
56
56
|
|
|
57
57
|
/* Questions: the one boxed element, because it takes input. */
|
|
58
|
-
.q { background: var(--raised); border: 1px solid var(--border); border-radius: 6px; padding: 12px }
|
|
58
|
+
.q { background: var(--raised); border: 1px solid var(--border); border-radius: 6px; padding: 12px; overflow-wrap: anywhere }
|
|
59
59
|
.q.answered { border-color: color-mix(in srgb, var(--running-fg) 40%, var(--border)) }
|
|
60
60
|
.q header { display: flex; justify-content: space-between; gap: 8px; align-items: baseline }
|
|
61
61
|
.q h3 { font-size: 14px; font-weight: 600; margin: 0; text-wrap: balance }
|
|
@@ -10,7 +10,8 @@ Changes: tickets always go to `.agent/tickets/<NN>-<slug>.md` (no tracker path);
|
|
|
10
10
|
numbering is contiguous from 01 in dependency order; acceptance criteria must state
|
|
11
11
|
observable behavior; every ticket names its **Seam** (the public interface its
|
|
12
12
|
acceptance tests hit) and what it **Touches** (files and functions, for
|
|
13
|
-
scheduling)
|
|
13
|
+
scheduling) and may carry optional **Risk** and **Suspicions** lines for
|
|
14
|
+
the review; trivial slices fold into a neighbour; tickets never carry the
|
|
14
15
|
implementation; the approval quiz is skipped when unattended; model invocation is
|
|
15
16
|
allowed.
|
|
16
17
|
-->
|
|
@@ -41,7 +42,7 @@ Break the work into **tracer bullet** tickets.
|
|
|
41
42
|
- A completed slice is demoable or verifiable on its own
|
|
42
43
|
- Each slice is sized to fit in a single fresh context window
|
|
43
44
|
- Any prefactoring should be done first
|
|
44
|
-
- No trivial tickets: when a slice's whole change is a small guard or fix (a few lines) and no other ticket is blocked by it, fold its acceptance criteria into the neighbouring ticket that touches the same behaviour. Every ticket costs a test-writer, implementer
|
|
45
|
+
- No trivial tickets: when a slice's whole change is a small guard or fix (a few lines) and no other ticket is blocked by it, fold its acceptance criteria into the neighbouring ticket that touches the same behaviour. Every ticket costs a test-writer, an implementer and a reviewer; a two-line guard doesn't earn three agents
|
|
45
46
|
|
|
46
47
|
</vertical-slice-rules>
|
|
47
48
|
|
|
@@ -93,6 +94,10 @@ Do NOT close or modify any parent issue.
|
|
|
93
94
|
|
|
94
95
|
**Touches:** the files, and the functions within them, this ticket expects to change.
|
|
95
96
|
|
|
97
|
+
**Risk:** optional. The shared state, persistence, concurrency, money or auth this ticket touches. Omit the line when there is none.
|
|
98
|
+
|
|
99
|
+
**Suspicions:** optional. What the reviewer should test first: a sibling caller, a key built from the old identity, a guard another ticket added.
|
|
100
|
+
|
|
96
101
|
- [ ] Acceptance criterion 1
|
|
97
102
|
- [ ] Acceptance criterion 2
|
|
98
103
|
|
|
@@ -116,6 +121,18 @@ The **Touches** line is a best guess, for scheduling: `implement-spec` never run
|
|
|
116
121
|
|
|
117
122
|
</touches-rule>
|
|
118
123
|
|
|
124
|
+
<risk-rule>
|
|
125
|
+
|
|
126
|
+
The optional **Risk** line names the shared state, persistence keys or identities, concurrency, money or auth the ticket touches, and turns on `implement-spec`'s correctness pass: a second, independent reviewer on another provider. The driver does not start when it has no CLI of another provider: the reviewer never does this pass. Any text on the line turns the pass on, so a ticket with no such risk has no **Risk** line, never `**Risk:** none`.
|
|
127
|
+
|
|
128
|
+
</risk-rule>
|
|
129
|
+
|
|
130
|
+
<suspicions-rule>
|
|
131
|
+
|
|
132
|
+
The optional **Suspicions** line is passed verbatim to `implement-spec`'s reviewer, which tests each one: named suspicions find regressions a generic review misses.
|
|
133
|
+
|
|
134
|
+
</suspicions-rule>
|
|
135
|
+
|
|
119
136
|
Never write the implementation into a ticket. `implement-spec`'s test-writer reads the ticket and must not know the solution, or its acceptance tests stop being independent of the code they judge. Describe behaviour, not the fix.
|
|
120
137
|
|
|
121
138
|
Outside the **Touches** line, avoid specific file paths or code snippets: they go stale fast. Exception: if a prototype produced a snippet that encodes a decision more precisely than prose can (state machine, reducer, schema, type shape), inline it and note briefly that it came from a prototype. Trim to the decision-rich parts, the shape of the decision, never the code that implements the ticket.
|
package/skills-manifest.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"generatedAt": "2026-10-
|
|
3
|
+
"generatedAt": "2026-10-14T03:34:13.674Z",
|
|
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.8",
|
|
50
|
+
"hash": "sha256-cafda530227caa971251cbe0d677f758630f96d6178f75a5e32b93417efe033e",
|
|
51
51
|
"description": "Implement a specification in code."
|
|
52
52
|
},
|
|
53
53
|
{
|
|
@@ -76,8 +76,8 @@
|
|
|
76
76
|
},
|
|
77
77
|
{
|
|
78
78
|
"name": "review-page",
|
|
79
|
-
"version": "0.1.
|
|
80
|
-
"hash": "sha256-
|
|
79
|
+
"version": "0.1.1",
|
|
80
|
+
"hash": "sha256-e8204e4c6f05e2721a20d9ac3f72b8ff84a2854647688bb9bf057312b53a59f4",
|
|
81
81
|
"description": "One interactive HTML page for a plan review or a questionnaire, built from a fixed template. Use when a pipeline session must deliver a plan review or a questionnaire as an interactive page the console shows."
|
|
82
82
|
},
|
|
83
83
|
{
|
|
@@ -94,8 +94,8 @@
|
|
|
94
94
|
},
|
|
95
95
|
{
|
|
96
96
|
"name": "to-tickets",
|
|
97
|
-
"version": "0.1.
|
|
98
|
-
"hash": "sha256-
|
|
97
|
+
"version": "0.1.5",
|
|
98
|
+
"hash": "sha256-58d4b24183ba3ff59a97de963ab0cd8bbac91d526941eeb7ec0a84274e918faf",
|
|
99
99
|
"description": "Break a spec, plan, or the current conversation into numbered tracer-bullet ticket files under .agent/tickets/, each declaring its blocking edges and acceptance criteria phrased as observable behavior. Use after a spec is approved and before implementation starts."
|
|
100
100
|
},
|
|
101
101
|
{
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
# History by ticket
|
|
2
|
-
|
|
3
|
-
Tidy the unpublished integration branch into one coherent commit per ticket,
|
|
4
|
-
ordered so each ticket follows its dependencies. Each commit contains that
|
|
5
|
-
ticket's acceptance tests, implementation, and all review or integration fixes.
|
|
6
|
-
The reviewer's commit list should tell the story of the tickets, with the
|
|
7
|
-
final behavior at each step.
|
|
8
|
-
|
|
9
|
-
## Before rewriting
|
|
10
|
-
|
|
11
|
-
- Confirm all ticket worktrees and branches were cleaned up after their merges,
|
|
12
|
-
and that the integration worktree is clean.
|
|
13
|
-
- Confirm the branch has never been pushed. Once pushed, preserve its history
|
|
14
|
-
and add reviewed follow-up commits; rewriting a published branch requires
|
|
15
|
-
explicit user authorization.
|
|
16
|
-
- Record the current tip and `HEAD^{tree}` in the ledger as the recovery point.
|
|
17
|
-
Use the ticket reports and ledger to map every working commit and every fix
|
|
18
|
-
to its owning ticket before editing history.
|
|
19
|
-
|
|
20
|
-
## Group the changes
|
|
21
|
-
|
|
22
|
-
Use an interactive rebase or reconstruct the commits in an isolated worktree,
|
|
23
|
-
operating only on branch-owned commits after the base. Fold red-test commits,
|
|
24
|
-
implementation commits, and later fixes into their ticket commit. Flatten
|
|
25
|
-
branch-only merge commits while preserving their conflict-resolution changes.
|
|
26
|
-
Keep the test-writer's acceptance files unchanged from their locked versions.
|
|
27
|
-
|
|
28
|
-
A fix touching several tickets is split by ownership when the resulting
|
|
29
|
-
commits can stand alone in dependency order. If it cannot be split safely,
|
|
30
|
-
combine the affected tickets into one coherent commit, name them in the
|
|
31
|
-
message, and record the reason in the ledger. Every final change has a ticket
|
|
32
|
-
owner; an out-of-scope change needs a ruling before inclusion.
|
|
33
|
-
|
|
34
|
-
Use a commit subject that names the ticket's delivered behavior and put its
|
|
35
|
-
ticket number or file pointer in the body. Keep a map from the old working
|
|
36
|
-
commits to the final ticket commits in the ledger; ticket reports and locked
|
|
37
|
-
test SHAs still refer to the original history.
|
|
38
|
-
|
|
39
|
-
## Completion
|
|
40
|
-
|
|
41
|
-
The rewritten tip's tree must equal the saved tree: history cleanup changes
|
|
42
|
-
the presentation of the work, not its files. A mismatch must be resolved
|
|
43
|
-
before review. Inspect each final commit's diff against its ticket to confirm
|
|
44
|
-
the tests, implementation, and fixes stayed together, dependencies precede
|
|
45
|
-
their consumers, and no temporary red-test or fix-only commits remain.
|
|
46
|
-
|
|
47
|
-
Use targeted checks where needed to verify a regrouped ticket. Run the full
|
|
48
|
-
gate once at the completed branch tip, through /review-diff. Rewriting changes
|
|
49
|
-
the review endpoints, so the next review covers the full rewritten branch;
|
|
50
|
-
mark the old coverage superseded even if the tip's tree is identical. After a
|
|
51
|
-
review requests fixes, finish the whole fix batch, fold it into these same
|
|
52
|
-
ticket commits, and repeat this completion check before the next review.
|