@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@jesdi/skills",
3
- "version": "0.0.34",
3
+ "version": "0.0.35",
4
4
  "description": "jesdi's agent skills (content package consumed by @jesdi/skills-cli)",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -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: list[str] = field(default_factory=list) # `file:line` of each proof the last fix made due 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
- job.proofs = set(followed)
137
- job.again = [f"{file}:{line}" for (file, line), changed in sorted(followed.items()) if changed]
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
- with during("the gate"):
166
- run.gate(ticket.number, slot, job.tip)
167
- with during("the review"):
168
- found = _review(job)
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
- raise Blocked(f"the gate is red on {commit[:7]}, record {record}")
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
- code = 1 if conf.get("all") or branch in conf.get("red", []) else 0
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)
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "generatedAt": "2026-10-15T16:47:59.098Z",
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.10",
50
- "hash": "sha256-a0c42688cad43aece79d51d9e09644cc879518858781bf5563e6f8388d009a0c",
49
+ "version": "0.1.11",
50
+ "hash": "sha256-c58d05ad78d3d1ae1381c02a96f4e5ca64f6387cea2b16ac5e1997bf52767467",
51
51
  "description": "Implement a specification in code."
52
52
  },
53
53
  {