@jesdi/skills 0.0.34 → 0.0.35
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 -0
- package/skills/implement-spec/README.md +3 -1
- package/skills/implement-spec/driver_fixloop.py +34 -10
- package/skills/implement-spec/driver_run.py +8 -2
- package/skills/implement-spec/prompts/fixer.md +1 -0
- package/skills/implement-spec/tests/test_fixloop.py +28 -0
- package/skills/implement-spec/tests/test_walk.py +2 -1
- package/skills-manifest.json +3 -3
package/package.json
CHANGED
|
@@ -107,6 +107,8 @@ and do not know about the run, except `driver_run.py`, which the three deciding
|
|
|
107
107
|
A timeout doubles the time of the rerun.
|
|
108
108
|
- Fix rounds stop at 4, and round 3 goes to the next implementer entry. The correctness pass never runs on the
|
|
109
109
|
provider of the implementer.
|
|
110
|
+
- A red gate on a fixer's commit is a finding of the next round (`driver_fixloop._gate`), so it blocks only after
|
|
111
|
+
round 4.
|
|
110
112
|
|
|
111
113
|
### Questions
|
|
112
114
|
|
|
@@ -82,7 +82,7 @@ Everything is under `.agent/`, which is never committed.
|
|
|
82
82
|
| `.agent/implement/<NN>/` | One folder for each ticket. |
|
|
83
83
|
| `.agent/implement/<NN>/<role>-<K>.json`, `.log`, `.prompt.md` | The report, the log and the prompt of one agent. K is the fix round. |
|
|
84
84
|
| `.agent/implement/<NN>/gate-<sha7>.log` | One gate record: start, commit, status, command, output, exit, the state after the command, end. |
|
|
85
|
-
| `.agent/implement/<NN>/findings-<K>.json` | The merged findings of the review after fix round K. |
|
|
85
|
+
| `.agent/implement/<NN>/findings-<K>.json` | The merged findings of the review after fix round K, or the red gate of round K. |
|
|
86
86
|
| `.agent/implement/final/` | The same files for the end of run, and `pr-body.md`: the pr-writer's body, then the facts of the run. |
|
|
87
87
|
| `.agent/implement/pycache/` | Compiled Python of the gate, kept out of the slots. |
|
|
88
88
|
| `.agent/implement-review.html` | The page with the open questions. |
|
|
@@ -119,6 +119,8 @@ 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 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 fixture correction.
|
|
123
|
+
|
|
122
124
|
- Rounds 1 and 2 run on the first entry of the implementer list.
|
|
123
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.
|
|
124
126
|
- There is no ruling at round 2.
|
|
@@ -30,8 +30,9 @@ class Job:
|
|
|
30
30
|
tip: str
|
|
31
31
|
previous: str = "" # the tip before the last fix; "" makes the next review a full one
|
|
32
32
|
proofs: set[tuple[str, int]] = field(default_factory=set) # mutation proofs so far, at `tip`
|
|
33
|
-
again:
|
|
33
|
+
again: set[tuple[str, int]] = field(default_factory=set) # each proof at `tip` that a fix made due again
|
|
34
34
|
corrected: bool = False # the last step was a fixture correction
|
|
35
|
+
red: bool = False # the gate of `tip` is red, so no review saw it
|
|
35
36
|
|
|
36
37
|
def pointers(self, review: int) -> dict[str, str]:
|
|
37
38
|
"""The values of a prompt; `review` is the round whose findings the agent reads."""
|
|
@@ -39,7 +40,7 @@ class Job:
|
|
|
39
40
|
findings = os.path.relpath(OUT / self.ticket.number / f"findings-{review}.json", self.slot)
|
|
40
41
|
return {**self.values, "start_sha": self.tip, "from_sha": self.previous, "round": str(self.round),
|
|
41
42
|
"locked": " ".join(entry["locked"]), "test_writer_sha": str(entry["test_writer_sha"] or ""),
|
|
42
|
-
"findings": findings, "proofs": " ".join(self.again)}
|
|
43
|
+
"findings": findings, "proofs": " ".join(f"{file}:{line}" for file, line in sorted(self.again))}
|
|
43
44
|
|
|
44
45
|
|
|
45
46
|
def entry(models: dict[str, list[str]], round: int) -> str:
|
|
@@ -129,12 +130,16 @@ def _fix(job: Job) -> None:
|
|
|
129
130
|
with during(f"fix round {job.round}"):
|
|
130
131
|
run.agent("fixer", number, job.slot, job.pointers(job.round - 1), entry=on, fix_round=job.round)
|
|
131
132
|
before, job.tip = job.tip, driver_slots.head(job.slot)
|
|
132
|
-
# No review has seen a fixture correction, so the review after a fix on a corrected tip is a full one.
|
|
133
|
-
job.previous, job.corrected = "" if job.corrected else before, False
|
|
134
133
|
run.state.ticket(number, tip=job.tip)
|
|
135
134
|
followed = driver_review.follow(job.slot, before, job.tip, job.proofs)
|
|
136
|
-
|
|
137
|
-
job.
|
|
135
|
+
due = {proof for proof, changed in followed.items() if changed}
|
|
136
|
+
if job.red:
|
|
137
|
+
# No review saw `before`: the next review covers the fix before it too, and its due proofs stay due.
|
|
138
|
+
due |= set(driver_review.follow(job.slot, before, job.tip, job.again))
|
|
139
|
+
else:
|
|
140
|
+
# No review has seen a fixture correction, so the review after a fix on a corrected tip is a full one.
|
|
141
|
+
job.previous = "" if job.corrected else before
|
|
142
|
+
job.corrected, job.proofs, job.again = False, set(followed), due
|
|
138
143
|
|
|
139
144
|
|
|
140
145
|
def _act_on(job: Job, found: list[dict]) -> bool:
|
|
@@ -157,14 +162,33 @@ def _act_on(job: Job, found: list[dict]) -> bool:
|
|
|
157
162
|
return False
|
|
158
163
|
|
|
159
164
|
|
|
165
|
+
def _gate(job: Job) -> list[dict]:
|
|
166
|
+
"""Gate the tip. A red gate on a fixer's commit before round 4 is a finding for the next round; any other red
|
|
167
|
+
gate blocks. Returns the findings: the red gate, or none."""
|
|
168
|
+
run, number = job.run, job.ticket.number
|
|
169
|
+
job.red = False
|
|
170
|
+
with during("the gate"):
|
|
171
|
+
if not 0 < job.round < ROUNDS or job.corrected:
|
|
172
|
+
run.gate(number, job.slot, job.tip)
|
|
173
|
+
return []
|
|
174
|
+
record = run.red_gate(number, job.slot, job.tip)
|
|
175
|
+
if not record:
|
|
176
|
+
return []
|
|
177
|
+
job.red, path = True, os.path.relpath(record, job.slot)
|
|
178
|
+
found = [{"severity": "critical", "file": path, "line": 0, "title": f"the gate is red on {job.tip[:7]}",
|
|
179
|
+
"evidence": f"the output of the gate in {path}", "kind": "code", "source": "gate"}]
|
|
180
|
+
(OUT / number / f"findings-{job.round}.json").write_text(json.dumps(found, indent=1) + "\n")
|
|
181
|
+
return _log_findings(job, found)
|
|
182
|
+
|
|
183
|
+
|
|
160
184
|
def run(ticket: Ticket, slot: str, run: Run, values: dict[str, str]) -> None:
|
|
161
185
|
"""Gate, review and fix the ticket tip until a review leaves no finding open."""
|
|
162
186
|
state = run.state.read()["tickets"][ticket.number]
|
|
163
187
|
job = Job(ticket, slot, run, values, state["round"], state["tip"])
|
|
164
188
|
while True:
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
189
|
+
found = _gate(job)
|
|
190
|
+
if not found:
|
|
191
|
+
with during("the review"):
|
|
192
|
+
found = _review(job)
|
|
169
193
|
if _act_on(job, found):
|
|
170
194
|
return
|
|
@@ -129,8 +129,13 @@ class Run:
|
|
|
129
129
|
|
|
130
130
|
def gate(self, number: str, slot: str, commit: str) -> None:
|
|
131
131
|
"""Gate `commit` in the slot, unless a green record names it already."""
|
|
132
|
+
if record := self.red_gate(number, slot, commit):
|
|
133
|
+
raise Blocked(f"the gate is red on {commit[:7]}, record {record}")
|
|
134
|
+
|
|
135
|
+
def red_gate(self, number: str, slot: str, commit: str) -> str:
|
|
136
|
+
"""`gate`, for a caller that acts on a red gate: returns its record, or "" when the gate is green."""
|
|
132
137
|
if commit in _gate_record(self.state.read(), number)["green_gates"]:
|
|
133
|
-
return
|
|
138
|
+
return ""
|
|
134
139
|
at = driver_slots.head(slot)
|
|
135
140
|
if at != commit:
|
|
136
141
|
raise Blocked(f"the slot is at {at[:7]}, not at {commit[:7]}: the gate did not run")
|
|
@@ -143,8 +148,9 @@ class Run:
|
|
|
143
148
|
|
|
144
149
|
self.state.update(count)
|
|
145
150
|
if not green:
|
|
146
|
-
|
|
151
|
+
return str(record)
|
|
147
152
|
ledger(f"{_owner(number)}: the gate is green on {commit[:7]}, record {record}.")
|
|
153
|
+
return ""
|
|
148
154
|
|
|
149
155
|
def restore_locked_files(self, number: str, slot: str) -> list[str]:
|
|
150
156
|
"""Before a test-writer correction: put each locked file the implementer changed back to its locked
|
|
@@ -10,6 +10,7 @@ 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`: 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
|
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
|
+
A finding from the `gate` is a red gate: its file is the gate record. Read the output there and make the gate green.
|
|
13
14
|
Commit your work on the checked-out ticket branch. Do not dispatch subagents.
|
|
14
15
|
In `ticket`, give the number of the ticket that owns your commits. In `green`, give the summary lines of the passing test output.
|
|
15
16
|
|
|
@@ -311,6 +311,34 @@ def test_four_rounds_then_blocked_with_the_escalation_at_round_3(tmp_path):
|
|
|
311
311
|
assert rig.state()["tickets"]["03"]["phase"] == "merged"
|
|
312
312
|
|
|
313
313
|
|
|
314
|
+
def test_a_red_gate_on_a_fixer_commit_starts_the_next_round_with_the_record_as_evidence(tmp_path):
|
|
315
|
+
"""Issue 64: the gate of a fix round is red. That is a finding for the next round, not a BLOCKED ticket."""
|
|
316
|
+
rig = Inside(tmp_path, gate={"subjects": ["fix: 01 round 1"]})
|
|
317
|
+
rig.run(plan(**{"01/reviewer": [review("01", [STUCK], lines=(2,)), review("01", lines=(2,))],
|
|
318
|
+
"01/fixer": [fix("01", 1), fix("01", 2, files={"demo/other.py": "x = 1\n"})]}))
|
|
319
|
+
assert rig.code == 0, rig.ledger()
|
|
320
|
+
assert [call["role"] for call in rig.calls("01")] == ["test-writer", "implementer", "reviewer", "fixer", "fixer",
|
|
321
|
+
"reviewer"]
|
|
322
|
+
first, second = (call["sha"] for call in rig.calls("01", "fixer"))
|
|
323
|
+
record = f".agent/implement/01/gate-{first[:7]}.log"
|
|
324
|
+
(gate,) = json.loads((rig.folder() / "findings-1.json").read_text())
|
|
325
|
+
assert (gate["source"], gate["severity"], gate["kind"]) == ("gate", "critical", "code")
|
|
326
|
+
assert gate["title"] == f"the gate is red on {first[:7]}" and record in gate["evidence"]
|
|
327
|
+
assert rig.led(f"Ticket 01: critical finding of kind code from the gate at {record}:0: the gate is red on {first[:7]}.")
|
|
328
|
+
assert "findings-1.json" in rig.prompt("fixer-2") and rig.state()["tickets"]["01"]["round"] == 2
|
|
329
|
+
# No review saw the red tip: the re-review covers both fixes, and the proof that round 1 made due stays due.
|
|
330
|
+
again = rig.prompt("reviewer-2")
|
|
331
|
+
assert f"The fix is {rig.sha('01', 'implementer')}..{second}" in again and "demo/export.py:2" in again
|
|
332
|
+
|
|
333
|
+
|
|
334
|
+
def test_a_red_gate_on_the_fixer_commit_of_round_4_blocks_the_ticket(tmp_path):
|
|
335
|
+
rig = Inside(tmp_path, gate={"subjects": ["fix: 01 round 4"]})
|
|
336
|
+
rig.run(plan(**{"01/reviewer": [review("01", [STUCK])] * 4, "01/fixer": [fix("01", n) for n in (1, 2, 3, 4)]}))
|
|
337
|
+
last = rig.calls("01", "fixer")[-1]["sha"]
|
|
338
|
+
assert f"BLOCKED at the gate: the gate is red on {last[:7]}" in blocked(rig)
|
|
339
|
+
assert len(rig.calls("01", "reviewer")) == 4 and not (rig.folder() / "findings-4.json").exists()
|
|
340
|
+
|
|
341
|
+
|
|
314
342
|
def test_round_3_stays_on_its_entry_when_the_implementer_has_no_next_entry(tmp_path):
|
|
315
343
|
rig = Inside(tmp_path, ONE, {"implementer": [OPUS]})
|
|
316
344
|
rig.run(plan(**{"01/reviewer": [review("01", [STUCK])] * 3 + [review("01")], "01/fixer": [fix("01", n) for n in (1, 2, 3)]}))
|
|
@@ -102,7 +102,8 @@ def git(*a):
|
|
|
102
102
|
conf = json.loads((rig / "gate.json").read_text())
|
|
103
103
|
(rig / "gates").mkdir(exist_ok=True)
|
|
104
104
|
branch = git("rev-parse", "--abbrev-ref", "HEAD")
|
|
105
|
-
|
|
105
|
+
subject = git("log", "-1", "--format=%s")
|
|
106
|
+
code = 1 if conf.get("all") or branch in conf.get("red", []) or subject in conf.get("subjects", []) else 0
|
|
106
107
|
rec = dict(t0=time.time(), cwd=os.path.realpath(os.getcwd()), commit=git("rev-parse", "HEAD"), branch=branch,
|
|
107
108
|
status=git("status", "--short"), base=os.environ.get("IMPLEMENT_SPEC_BASE"),
|
|
108
109
|
pycache=os.environ.get("PYTHONPYCACHEPREFIX"), exit=code)
|
package/skills-manifest.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"generatedAt": "2026-10-
|
|
3
|
+
"generatedAt": "2026-10-15T17:09:44.505Z",
|
|
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.11",
|
|
50
|
+
"hash": "sha256-c58d05ad78d3d1ae1381c02a96f4e5ca64f6387cea2b16ac5e1997bf52767467",
|
|
51
51
|
"description": "Implement a specification in code."
|
|
52
52
|
},
|
|
53
53
|
{
|