@jesdi/skills 0.0.34 → 0.0.36

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.36",
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.
@@ -172,8 +174,8 @@ Every report has `status` (`DONE`, `DONE_WITH_CONCERNS`, `NEEDS_CONTEXT`, `BLOCK
172
174
  |---|---|---|---|
173
175
  | test-writer | `prompts/test-writer.md` | `locked` (paths), `red` (`test`, `line`) | With DONE: one commit, one locked file and one red line or more. Each locked path is in a commit that the role made. Each `line` is in the log. A locked file of a merged ticket that the role changed is in `locked`. |
174
176
  | implementer | `prompts/implementer.md` | `green` | With DONE: one commit or more. |
175
- | reviewer | `prompts/reviewer.md` | `findings` (`severity`, `file`, `line`, `title`, `evidence`, `kind`), `mutations` (`test`, `file`, `line`, `red`) | In the first review of a tip: `mutations` is required, with a proof for each locked test file. Each `red` is in the log. |
176
- | correctness | `prompts/correctness.md` | the same as the reviewer | `mutations` is optional. |
177
+ | reviewer | `prompts/reviewer.md` | `findings` (`severity`, `file`, `line`, `title`, `evidence`, `kind`), `mutations` (`test`, `file`, `line`, `red`); `commits` is empty | In the first review of a tip: `mutations` is required, with a proof for each locked test file (`test` is `<locked file>::<test name>`). Each `red` is in the log. |
178
+ | correctness | `prompts/correctness.md` | the same as the reviewer; `commits` is empty | `mutations` is optional. |
177
179
  | fixer | `prompts/fixer.md` | `green`, `ticket` | With DONE: one commit or more. |
178
180
  | final-reviewer | `prompts/final-reviewer.md` | `findings` | None. |
179
181
  | pr-writer | `prompts/pr-writer.md` | `body`; `commits` is empty | None. |
@@ -212,6 +214,6 @@ The driver enforces the mutation proof for each locked test file, not for each l
212
214
 
213
215
  - **A `codex` agent cannot commit.** A role that commits (test-writer, implementer, fixer, conflict) on an `openai` entry runs with `--sandbox workspace-write`, and that sandbox cannot run `git commit`. In the default `models.json` this is the second implementer entry, so fix rounds 3 and 4 fail. What to do: give a `--models` file with no `openai` entry in the `implementer`, `test-writer`, `fixer` and `conflict` lists.
214
216
  - **A `codex` reviewer cannot make a mutation proof.** A reviewer on an `openai` entry runs with `--sandbox read-only` and cannot make the temporary edit. The default `models.json` has such an entry first for the reviewer, and the first review needs the proofs. What to do: give a `--models` file whose reviewer list starts with an `anthropic` entry.
215
- - **The correctness pass reads a tree that the reviewer changes.** The two agents share one slot, and the reviewer makes temporary edits. `prompts/correctness.md` tells the pass to read from git at the reviewed commit and to run no tests. That sentence is the only guard: the driver cannot enforce it, so a pass that ignores it can report a false finding and start a fix round. What to do: when a correctness finding looks wrong, compare it with `git show <reviewed commit>:<file>` before you rule on it.
217
+ - **The correctness pass reads a tree that the reviewer changes.** The two agents share one slot, and the reviewer makes temporary edits. `prompts/correctness.md` tells the pass to read from git at the reviewed commit and to run no tests. It reads only the spec, the ticket and the findings file from the working tree, because git can lack them: `.agent/` is ignored. That rule is the only guard: the driver cannot enforce it, so a pass that ignores it can report a false finding and start a fix round. What to do: when a correctness finding looks wrong, compare it with `git show <reviewed commit>:<file>` before you rule on it.
216
218
  - **A ticket that is not merged stops the end of run.** While one ticket is BLOCKED, there is no tidy and no final review of the merged tickets.
217
219
  - **A published PR branch keeps its history.** See "End of run".
@@ -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 @@ Checks: {correctness}
10
10
  Read the checks file and apply each of its checks to the diff of the range, with the Risk line of the ticket first: races, idempotency, time, authorization, persistence keys and identities, migrations, error paths.
11
11
  The output section of the checks file does not apply here: your findings go into the report.
12
12
  Read file content from git at {start_sha} (`git diff`, `git show {start_sha}:<path>`), never from the working tree, and run no tests: another agent makes temporary edits in this worktree while you work.
13
+ The one exception is the input of the driver, which git can lack because `.agent/` is ignored: read the spec, the ticket and the findings file from the working tree. The other agent does not edit them.
13
14
  Report only a defect that you can show with a concrete sequence of events. `mutations` is optional for this pass.
14
15
  Give each finding a severity (critical, important or minor), the file and line, a title, the evidence, and a kind: `code`, `test`, or `fixture` when a locked test encodes a stale path, format or setup detail.
15
16
  Do not commit and do not dispatch subagents. Leave the tree as you found it.
@@ -22,11 +23,11 @@ Write one JSON report at {report}. When your sandbox is read-only, give the JSON
22
23
  "required": ["status", "commits", "files", "concerns", "findings"],
23
24
  "properties": {
24
25
  "status": {"enum": ["DONE", "DONE_WITH_CONCERNS", "NEEDS_CONTEXT", "BLOCKED"]},
25
- "commits": {"type": "array", "items": {"type": "string", "pattern": "^[0-9a-f]{7,40}$"}},
26
+ "commits": {"description": "Only the commits you created. A review creates none, so this list is empty: never list a commit of the range.", "type": "array", "items": {"type": "string", "pattern": "^[0-9a-f]{7,40}$"}},
26
27
  "files": {"type": "array", "items": {"type": "string"}},
27
28
  "concerns": {"type": "array", "items": {"type": "string"}},
28
29
  "findings": {"type": "array", "items": {"type": "object", "required": ["severity", "file", "line", "title", "evidence", "kind"], "properties": {"severity": {"enum": ["critical", "important", "minor"]}, "file": {"type": "string"}, "line": {"type": "integer"}, "title": {"type": "string", "minLength": 1}, "evidence": {"type": "string", "minLength": 1}, "kind": {"enum": ["code", "fixture", "test"]}}}},
29
- "mutations": {"type": "array", "items": {"type": "object", "required": ["test", "file", "line", "red"], "properties": {"test": {"type": "string", "minLength": 1}, "file": {"type": "string", "minLength": 1}, "line": {"type": "integer"}, "red": {"type": "string", "minLength": 1}}}}
30
+ "mutations": {"description": "Optional for this pass.", "type": "array", "items": {"type": "object", "required": ["test", "file", "line", "red"], "properties": {"test": {"type": "string", "minLength": 1}, "file": {"type": "string", "minLength": 1}, "line": {"type": "integer"}, "red": {"type": "string", "minLength": 1}}}}
30
31
  }
31
32
  }
32
33
  ```
@@ -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
 
@@ -10,7 +10,7 @@ Test-writer report, when the ticket has a test-writer: {writer_report}
10
10
  Check the diff against the ticket and the spec, then for quality. Check that each criterion has a locked test. Test each suspicion that the ticket names in its Suspicions line.
11
11
  In round 1, for each locked test, break the production line the test protects, run the test, print its failing output, then revert the edit. A test that stays green is a critical finding. Run again each mutation that the test-writer report names in `concerns`.
12
12
  A mutation proof is keyed to its test and its production line: a later round repeats it only when that line changed.
13
- In `mutations`, give for each proof the test, the production file and line, and one failure line copied exactly from the output you printed.
13
+ In `mutations`, give for each proof the test as `<locked file>::<test name>`, the production file and line, and one failure line copied exactly from the output you printed.
14
14
  Give each finding a severity (critical, important or minor), the file and line, a title, the evidence, and a kind: `code`, `test`, or `fixture` when a locked test encodes a stale path, format or setup detail.
15
15
  Do not commit and do not dispatch subagents. Leave the tree as you found it.
16
16
 
@@ -22,11 +22,11 @@ Write one JSON report at {report}. When your sandbox is read-only, give the JSON
22
22
  "required": ["status", "commits", "files", "concerns", "findings"],
23
23
  "properties": {
24
24
  "status": {"enum": ["DONE", "DONE_WITH_CONCERNS", "NEEDS_CONTEXT", "BLOCKED"]},
25
- "commits": {"type": "array", "items": {"type": "string", "pattern": "^[0-9a-f]{7,40}$"}},
25
+ "commits": {"description": "Only the commits you created. A review creates none, so this list is empty: never list a commit of the range.", "type": "array", "items": {"type": "string", "pattern": "^[0-9a-f]{7,40}$"}},
26
26
  "files": {"type": "array", "items": {"type": "string"}},
27
27
  "concerns": {"type": "array", "items": {"type": "string"}},
28
28
  "findings": {"type": "array", "items": {"type": "object", "required": ["severity", "file", "line", "title", "evidence", "kind"], "properties": {"severity": {"enum": ["critical", "important", "minor"]}, "file": {"type": "string"}, "line": {"type": "integer"}, "title": {"type": "string", "minLength": 1}, "evidence": {"type": "string", "minLength": 1}, "kind": {"enum": ["code", "fixture", "test"]}}}},
29
- "mutations": {"type": "array", "items": {"type": "object", "required": ["test", "file", "line", "red"], "properties": {"test": {"type": "string", "minLength": 1}, "file": {"type": "string", "minLength": 1}, "line": {"type": "integer"}, "red": {"type": "string", "minLength": 1}}}}
29
+ "mutations": {"description": "Required in the first review of a tip: one proof or more for each locked test file, with `test` as `<locked file>::<test name>`. A review scoped to a fix gives the proofs that it names.", "type": "array", "items": {"type": "object", "required": ["test", "file", "line", "red"], "properties": {"test": {"type": "string", "minLength": 1}, "file": {"type": "string", "minLength": 1}, "line": {"type": "integer"}, "red": {"type": "string", "minLength": 1}}}}
30
30
  }
31
31
  }
32
32
  ```
@@ -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)]}))
@@ -516,6 +544,32 @@ def test_the_correctness_prompt_keeps_the_agent_off_the_tree_the_reviewer_edits(
516
544
  assert "run no tests" in text
517
545
 
518
546
 
547
+ def test_the_correctness_pass_reads_the_untracked_inputs_of_the_driver_from_the_working_tree(tmp_path):
548
+ """Issue 66: the ticket and the findings files are in the ignored .agent/, so git at the tip has none of them."""
549
+ rig = Inside(tmp_path, RISKY, {"correctness": [SOL, OPUS], "implementer": [OPUS]})
550
+ rig.run(plan(**{"02/correctness": [correct([finding("C-IMPORTANT")])], "02/fixer": [fix("02", 1)]}))
551
+ assert rig.code == 0, rig.ledger()
552
+ again = rig.prompt("correctness-1", "02")
553
+ inputs = (".agent/tickets/02-quote-fields.md", ".agent/implement/02/findings-0.json")
554
+ assert all(path in again for path in inputs), again
555
+ tracked = subprocess.run(["git", "-C", str(rig.repo), "ls-files", *inputs], capture_output=True, text=True).stdout
556
+ assert tracked == ""
557
+ assert "read the spec, the ticket and the findings file from the working tree" in again
558
+
559
+
560
+ @pytest.mark.parametrize("role", ["reviewer", "correctness"])
561
+ def test_the_schema_of_a_review_says_that_commits_holds_only_the_agents_own_commits(role):
562
+ """Issue 66: a reviewer that lists a commit of the range costs a rerun (`is not a new commit`)."""
563
+ description = driver_report.schema(role)["properties"]["commits"]["description"]
564
+ assert "only the commits you created" in description.lower() and "empty" in description
565
+
566
+
567
+ def test_the_reviewer_schema_asks_for_one_proof_for_each_locked_test_file_in_the_first_review():
568
+ """Issue 66: a first review with no proof for a locked test file costs a rerun."""
569
+ description = driver_report.schema("reviewer")["properties"]["mutations"]["description"]
570
+ assert "each locked test file" in description and "`<locked file>::<test name>`" in description
571
+
572
+
519
573
  def test_git_identity_is_not_needed_to_open_the_merge_for_the_conflict_agent(tmp_path):
520
574
  import driver_merge
521
575
  rig = Inside(tmp_path)
@@ -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:11:47.113Z",
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.12",
50
+ "hash": "sha256-d50cbfcb02b148c9f122d16333274b2d758efc8a1b35e0cf54eb831f34eba6de",
51
51
  "description": "Implement a specification in code."
52
52
  },
53
53
  {