@jesdi/skills 0.0.38 → 0.0.40

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.38",
3
+ "version": "0.0.40",
4
4
  "description": "jesdi's agent skills (content package consumed by @jesdi/skills-cli)",
5
5
  "license": "MIT",
6
6
  "repository": {
@@ -60,7 +60,10 @@ and do not know about the run, except `driver_run.py`, which the three deciding
60
60
  - The commit of the test-writer is the reference of the ticket's locked files. Before each review, a locked file
61
61
  that differs from its reference is a critical finding (`driver_review.locked_findings`).
62
62
  - A correction of the test-writer is a new reference. For that reason the driver first puts each locked file back
63
- to its locked version (`Run.restore_locked_files`), and a correction may change only the files it lists.
63
+ to its locked version (`Run.restore_locked_files`), and a correction may change only the files it lists, after
64
+ `overrule-test` and in the fix loop. Its commit becomes the reference of each file it lists. A locked file that it
65
+ does not list and that differs from the old reference keeps the old reference in the ticket's `held`, so a
66
+ change of the implementer stays a finding (`driver_review.written`).
64
67
  - The locked files of merged tickets are checked when a ticket lands (`driver_merge.refused`): the tree that lands
65
68
  must have each of them as the PR tip has it. A ticket's test-writer may change one only when it lists it.
66
69
  - A conflict resolution, of a merge or of the rebase, may not change a locked file.
@@ -172,7 +172,7 @@ Every report has `status` (`DONE`, `DONE_WITH_CONCERNS`, `NEEDS_CONTEXT`, `BLOCK
172
172
 
173
173
  | Role | Template | More fields | Checks after the schema |
174
174
  |---|---|---|---|
175
- | test-writer | `prompts/test-writer.md` | `locked` (test file paths), `red` (`test`, `line`) | The driver reads a test node id `<file>::<test>` in `locked` as its file. 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`. |
175
+ | test-writer | `prompts/test-writer.md` | `locked` (test file paths), `red` (`test`, `line`) | The driver reads a test node id `<file>::<test>` in `locked` as its file. 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`. A correction (after `overrule-test`, or for a review finding) changes only the files that it lists in `locked` and the files that the driver restored before it. |
176
176
  | implementer | `prompts/implementer.md` | `green` | With DONE: one commit or more. |
177
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
178
  | correctness | `prompts/correctness.md` | the same as the reviewer; `commits` is empty | `mutations` is optional. |
@@ -105,10 +105,10 @@ def _correct(job: Job, findings: list[dict]) -> None:
105
105
  if job.corrected:
106
106
  titles = "; ".join(finding["title"] for finding in findings)
107
107
  raise Blocked(f"the review: a locked-test finding is open after the test-writer correction: {titles}")
108
- run.restore_locked_files(number, job.slot)
108
+ check = {"correction": True, "restored": run.restore_locked_files(number, job.slot)}
109
109
  with during("the test-writer correction"):
110
110
  report = run.agent("test-writer", number, job.slot, job.pointers(job.round), notes=["correction"],
111
- fix_round=job.round + 1)
111
+ fix_round=job.round + 1, check=check)
112
112
  job.tip, job.previous, job.corrected = driver_slots.head(job.slot), "", True
113
113
  entry = run.state.read()["tickets"][number]
114
114
  run.state.ticket(number, tip=job.tip, **driver_review.written(job.slot, entry, report["locked"], job.tip))
@@ -1,11 +1,11 @@
1
1
  {
2
- "explorer": ["claude-sonnet-5@medium"],
3
- "test-writer": ["claude-sonnet-5@medium", "openai/gpt-6.1-sol@medium"],
4
- "implementer": ["claude-sonnet-5@medium", "openai/gpt-6.1-sol@medium"],
5
- "reviewer": ["openai/gpt-6-astra@medium", "claude-opus-5@medium"],
6
- "correctness": ["openai/gpt-6-astra@medium", "claude-opus-5@medium"],
7
- "fixer": ["claude-opus-5@medium"],
8
- "final-reviewer": ["openai/gpt-6-astra@medium", "claude-opus-5@medium"],
9
- "conflict": ["claude-opus-5@medium"],
10
- "pr-writer": ["claude-opus-5@medium"]
2
+ "explorer": ["claude-sonnet-5-5@medium"],
3
+ "test-writer": ["claude-sonnet-5-5@medium", "openai/gpt-6.1-sol@medium"],
4
+ "implementer": ["claude-sonnet-5-5@medium", "openai/gpt-6.1-sol@medium"],
5
+ "reviewer": ["openai/gpt-6-astra@medium", "claude-opus-5-5@medium"],
6
+ "correctness": ["openai/gpt-6-astra@medium", "claude-opus-5-5@medium"],
7
+ "fixer": ["claude-opus-5-5@medium"],
8
+ "final-reviewer": ["openai/gpt-6-astra@medium", "claude-opus-5-5@medium"],
9
+ "conflict": ["claude-opus-5-5@medium"],
10
+ "pr-writer": ["claude-opus-5-5@medium"]
11
11
  }
@@ -244,6 +244,45 @@ def test_a_correction_of_its_own_file_leaves_the_work_in_the_tree_to_the_impleme
244
244
  assert git(rig.repo, "show", f"feat/csv:{OTHER}") == OTHER_TEST
245
245
 
246
246
 
247
+ def proved(findings=()):
248
+ """A reviewer step with a mutation proof for each of the two locked files."""
249
+ step = review("01", findings)
250
+ red = "FAILED tests/test_01b.py::test_y - AssertionError at demo/export.py:1"
251
+ step["report"]["mutations"].append(dict(test=f"{OTHER}::test_y", file="demo/export.py", line=1, red=red))
252
+ return {**step, "stdout": step["stdout"] + red + "\n"}
253
+
254
+
255
+ STRONGER = "def test_x():\n assert '01' == '01' # stronger\n"
256
+
257
+
258
+ @pytest.mark.parametrize("cheat", [False, True])
259
+ def test_a_second_correction_of_a_locked_file_is_its_new_reference(tmp_path, capsys, cheat):
260
+ # Issue #75: the review correction (#70) changes tests/test_01.py and first lists only the other file. Before
261
+ # the fix it was accepted, `held` kept the overrule commit as the reference, and each review after a fix that
262
+ # did not touch the file found "locked file changed". A correction now lists each file it changes.
263
+ vacuous = finding("VACUOUS", "critical", kind="test", file="tests/test_01.py")
264
+ files = {"tests/test_01.py": STRONGER.replace("stronger", "draft"), OTHER: OTHER_TEST + "# stronger\n"}
265
+ unlisted = {**correction(), "files": files, "commit": "test: 01 stronger"}
266
+ unlisted["report"] = {**unlisted["report"], "locked": [OTHER], "files": [OTHER]}
267
+ # The rerun lists both files: the commit of the rejected attempt is in its commits too.
268
+ listed = {**unlisted, "files": {"tests/test_01.py": STRONGER}}
269
+ listed["report"] = {**unlisted["report"], "locked": ["tests/test_01.py", OTHER]}
270
+ gutted = fix("01", 1, files={"demo/export.py": "x = 1\n", "tests/test_01.py": "# gutted\n"})
271
+ fixes = [gutted, fix("01", 2, files={"demo/export.py": "x = 2\n", "tests/test_01.py": STRONGER})] if cheat else [
272
+ fix("01", 1)]
273
+ scenario = plan(**{"01/test-writer": [two_locked(), correction(), unlisted, listed],
274
+ "01/implementer": [blocked(), impl("01")], "01/fixer": fixes,
275
+ "01/reviewer": [proved([vacuous]), proved([finding("STUCK")])] + [proved()] * 3})
276
+ rig = Inside(tmp_path, ONE).run(scenario)
277
+ rig.run(scenario, "--answer", f"{Q1}=overrule-test")
278
+ assert rig.code == 0, rig.ledger()
279
+ assert rig.led("BLOCKED", "correction") == [] and rig.led("its commits also change tests/test_01.py")
280
+ entry = rig.state()["tickets"]["01"]
281
+ assert entry["held"] == {} and entry["test_writer_sha"] == rig.calls("01", "test-writer")[3]["sha"]
282
+ assert len(rig.led("locked file changed: tests/test_01.py")) == cheat, "only a fixer's change is a finding"
283
+ assert git(rig.repo, "show", "feat/csv:tests/test_01.py") == STRONGER
284
+
285
+
247
286
  def test_a_locked_file_keeps_its_old_reference_until_a_test_writer_lists_it(repo, tmp_path):
248
287
  def commit(name, text):
249
288
  (tmp_path / name).write_text(text)
@@ -74,7 +74,7 @@ def test_models_file_checks_the_first_entry_of_each_role(repo, capsys, tmp_path,
74
74
  monkeypatch.setattr(driver_agents.shutil, "which", lambda cli: None if cli == "codex" else cli)
75
75
  models = tmp_path.parent / f"{tmp_path.name}-models.json"
76
76
  # The default reviewer and final-reviewer start on codex, so this file gives both a claude entry.
77
- claude = {"reviewer": ["claude-opus-5@medium"], "final-reviewer": ["claude-opus-5@medium"]}
77
+ claude = {"reviewer": ["claude-opus-5-5@medium"], "final-reviewer": ["claude-opus-5-5@medium"]}
78
78
  models.write_text(json.dumps({"implementer": ["openai/gpt-sol@medium"], **claude,
79
79
  "correctness": ["openai/gpt-sol@medium"], "fixer": ["other/x@low"]}))
80
80
  assert run(capsys, "--models", str(models)) == (
@@ -90,11 +90,14 @@ def test_models_file_checks_the_first_entry_of_each_role(repo, capsys, tmp_path,
90
90
  3, "correctness: codex is not on PATH (model entry openai/gpt-sol@low)\n")
91
91
 
92
92
 
93
- @pytest.mark.parametrize("entry", ["claude-opus-5@turbo", "openai/gpt-sol", "openai/@low"])
93
+ # Each entry is invalid on purpose: `turbo` is no effort of the anthropic provider, the second entry has no
94
+ # effort, and the third has no model.
95
+ @pytest.mark.parametrize("entry", ["claude-opus-5-5@turbo", "openai/gpt-sol", "openai/@low"],
96
+ ids=["unknown-effort", "no-effort", "no-model"])
94
97
  def test_an_entry_needs_a_model_and_an_effort_of_its_provider(repo, capsys, tmp_path, entry):
95
98
  repo(t01=ticket("01"))
96
99
  models = tmp_path.parent / f"{tmp_path.name}-models.json"
97
- models.write_text(json.dumps({"reviewer": ["claude-opus-5@low", entry]}))
100
+ models.write_text(json.dumps({"reviewer": ["claude-opus-5-5@low", entry]}))
98
101
  code, out = run(capsys, "--models", str(models))
99
102
  assert code == 3 and out.startswith(f"reviewer: model entry {entry} needs a model and an effort from ")
100
103
 
@@ -173,7 +176,7 @@ def test_accepted_reports_end_the_run_with_exit_0(repo, tmp_path, capsys):
173
176
  assert code == 0
174
177
  tip = walked("--short=7")
175
178
  assert [line.partition(" for ")[0] for line in ledger] == [
176
- "Ticket 01: test-writer ran on claude-sonnet-5@medium", "Ticket 01: implementer ran on claude-sonnet-5@medium",
179
+ "Ticket 01: test-writer ran on claude-sonnet-5-5@medium", "Ticket 01: implementer ran on claude-sonnet-5-5@medium",
177
180
  f"Ticket 01: the gate is green on {tip}, record .agent/implement/01/gate-{tip}.log.",
178
181
  "Ticket 01: reviewer ran on openai/gpt-6-astra@medium",
179
182
  f"Ticket 01: merged into feat/x as a fast-forward at {tip}."]
@@ -413,7 +416,7 @@ def test_a_merge_conflict_goes_to_the_conflict_agent_once_and_its_commit_is_gate
413
416
  number, tip = line[7:9], walked()
414
417
  assert line.endswith(f" at {tip[:7]}.") and len(out_of("git", "rev-list", "--parents", "-n1", tip).split()) == 3
415
418
  assert f"Ticket {number}: the gate is green on {tip[:7]}, record .agent/implement/{number}/gate-{tip[:7]}.log." in ledger
416
- assert "| gates run | 4 |" in out and "| agents, conflict on claude-opus-5@medium | 1 |" in out
419
+ assert "| gates run | 4 |" in out and "| agents, conflict on claude-opus-5-5@medium | 1 |" in out
417
420
  assert dict(state_of(tmp_path)["run"]["landed"])[number] == tip
418
421
  prompt = (tmp_path / f".agent/implement/{number}/conflict-0.prompt.md").read_text()
419
422
  assert "Files in conflict: work.txt\n" in prompt and prompt.rstrip().endswith("one ref store.")
@@ -794,7 +797,7 @@ def test_the_preflight_asks_each_used_entry_once_and_names_each_cli_that_fails(t
794
797
  monkeypatch.setattr(driver_agents, "PREFLIGHT_MINUTES", 0.02)
795
798
  models, _ = driver_agents.load_models(None)
796
799
  models["reviewer"] = ["openai/gpt-astra@medium"]
797
- assert driver_agents.used_entries(models) == ["claude-opus-5@medium", "claude-sonnet-5@medium",
800
+ assert driver_agents.used_entries(models) == ["claude-opus-5-5@medium", "claude-sonnet-5-5@medium",
798
801
  "openai/gpt-6-astra@medium", "openai/gpt-6.1-sol@medium",
799
802
  "openai/gpt-astra@medium"]
800
803
  assert driver_agents.preflight(models) == [
@@ -8,10 +8,10 @@ import pytest
8
8
  from test_cli import RUN, SKILL, drive, env, make_repo, three # noqa: F401 (env is a fixture)
9
9
 
10
10
  STANDARD = {
11
- "implementer": ["claude-sonnet-5@medium", "openai/gpt-6.1-sol@medium"],
12
- "test-writer": ["claude-sonnet-5@medium", "openai/gpt-6.1-sol@medium"],
13
- "reviewer": ["openai/gpt-6-astra@medium", "claude-opus-5@medium"],
14
- "final-reviewer": ["openai/gpt-6-astra@medium", "claude-opus-5@medium"],
11
+ "implementer": ["claude-sonnet-5-5@medium", "openai/gpt-6.1-sol@medium"],
12
+ "test-writer": ["claude-sonnet-5-5@medium", "openai/gpt-6.1-sol@medium"],
13
+ "reviewer": ["openai/gpt-6-astra@medium", "claude-opus-5-5@medium"],
14
+ "final-reviewer": ["openai/gpt-6-astra@medium", "claude-opus-5-5@medium"],
15
15
  }
16
16
  ENTRY = re.compile(r"^(?:(?:anthropic|openai)/)?[\w.-]+@(?:minimal|low|medium|high|xhigh|max)$")
17
17
 
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
- "generatedAt": "2026-10-15T21:59:51.763Z",
3
+ "generatedAt": "2026-10-15T23:44:37.469Z",
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.14",
50
- "hash": "sha256-79c7cae6498225802c8d12a8e5c58c74ec0f2ef3b8889bc1e529b3115407e3e9",
49
+ "version": "0.1.16",
50
+ "hash": "sha256-8f7e4b06072b26e3a6699974dced9afe2745917c01784f9f6166d972c3ba60f1",
51
51
  "description": "Implement a specification in code."
52
52
  },
53
53
  {