@jesdi/skills 0.0.37 → 0.0.39
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 +4 -1
- package/skills/implement-spec/README.md +1 -1
- package/skills/implement-spec/driver_fixloop.py +2 -2
- package/skills/implement-spec/driver_report.py +7 -0
- package/skills/implement-spec/driver_run.py +2 -0
- package/skills/implement-spec/prompts/correction.md +1 -1
- package/skills/implement-spec/prompts/test-writer.md +1 -1
- package/skills/implement-spec/tests/test_decisions.py +39 -0
- package/skills/implement-spec/tests/test_fixloop.py +13 -0
- package/skills-manifest.json +3 -3
package/package.json
CHANGED
|
@@ -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` (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`. |
|
|
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))
|
|
@@ -100,6 +100,11 @@ def _locked_errors(report: dict, context: dict) -> list[str]:
|
|
|
100
100
|
if not any(test.startswith(path) for test in tests)]
|
|
101
101
|
|
|
102
102
|
|
|
103
|
+
def locked_files(listed: list[str]) -> list[str]:
|
|
104
|
+
"""The files of a `locked` list: a test node id `<file>::<test>` names its file. In order, each file once."""
|
|
105
|
+
return list(dict.fromkeys(entry.partition("::")[0] for entry in listed))
|
|
106
|
+
|
|
107
|
+
|
|
103
108
|
def validate(role: str, path: str, context: dict) -> list[str]:
|
|
104
109
|
try:
|
|
105
110
|
report = json.loads(Path(path).read_text())
|
|
@@ -111,5 +116,7 @@ def validate(role: str, path: str, context: dict) -> list[str]:
|
|
|
111
116
|
return [f"{'.'.join(map(str, e.path)) or 'report'}: {e.message}" for e in found]
|
|
112
117
|
# The schema types only the role's own fields: any other field of the report is dropped.
|
|
113
118
|
report = {name: value for name, value in report.items() if name in rules["properties"]}
|
|
119
|
+
if "locked" in report:
|
|
120
|
+
report["locked"] = locked_files(report["locked"])
|
|
114
121
|
return (_commit_errors(report, context) + _owner_errors(report, context) + _correction_errors(report, context)
|
|
115
122
|
+ _merged_errors(report, context) + _proof_errors(report, context) + _locked_errors(report, context))
|
|
@@ -100,6 +100,8 @@ class Run:
|
|
|
100
100
|
merged = driver_merge.protected(self.state.read()["tickets"], number)
|
|
101
101
|
options["check"] = {**(options.get("check") or {}), "merged": merged, "pr": self.tip()}
|
|
102
102
|
report = _attempts(self, role, number, slot, values, **options)
|
|
103
|
+
if role == "test-writer":
|
|
104
|
+
report["locked"] = driver_report.locked_files(report["locked"]) # as validate read it
|
|
103
105
|
if subject := driver_decisions.contradicted(role, report):
|
|
104
106
|
# The implementer's commits stay: after the ruling the ticket goes on from them.
|
|
105
107
|
evidence = OUT / number / f"{role}-{options.get('fix_round', 0)}.json"
|
|
@@ -3,4 +3,4 @@ This call is a correction of the locked tests, not a new test set. Read in {find
|
|
|
3
3
|
A `fixture` finding: a locked test encodes a stale path, format, fixture layout or setup detail. Correct only that detail and keep what each test asserts.
|
|
4
4
|
A `test` finding: a locked test is too weak, for example it stays green when the production line it protects is broken. Make the test assert the behavior of its criterion so that it fails on that break. Do not remove an assertion.
|
|
5
5
|
For this you may read the implementation.
|
|
6
|
-
The implementation exists, so a corrected test is green: for `red`, break the production line that the test protects, run the test, print its failing output, then revert the edit. In `locked`, list each test file you corrected.
|
|
6
|
+
The implementation exists, so a corrected test is green: for `red`, break the production line that the test protects, run the test, print its failing output, then revert the edit. In `locked`, list each test file you corrected: test file paths only, not test node ids. Unlike `mutations[].test` of the reviewer, `locked` takes no `::<test name>`.
|
|
@@ -10,7 +10,7 @@ Write one black-box acceptance test for each criterion of the ticket, through th
|
|
|
10
10
|
Run each test and confirm that it fails because the behavior is missing, not because of an import error or a syntax error. Print that failing output.
|
|
11
11
|
A test that is green before any implementation is not a proof. Rewrite it until it fails, or name in `concerns` the production-code mutation that turns it red.
|
|
12
12
|
Commit the tests red on the checked-out branch. Do not write implementation code and do not dispatch subagents.
|
|
13
|
-
In `locked`, list each test file of your commit. In `red`, give for each test one failure line, copied exactly from the output you printed.
|
|
13
|
+
In `locked`, list each test file of your commit: test file paths only, not test node ids (`tests/x.py`, not `tests/x.py::test_a`). In `red`, give for each test one failure line, copied exactly from the output you printed.
|
|
14
14
|
|
|
15
15
|
Write one JSON report at {report} that matches this schema:
|
|
16
16
|
|
|
@@ -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)
|
|
@@ -177,6 +177,19 @@ def test_a_test_finding_on_a_locked_file_gets_the_test_writer_correction_not_a_f
|
|
|
177
177
|
assert driver_review.route([vacuous, minor, elsewhere], ["tests/test_01.py"]) == ([vacuous], [elsewhere])
|
|
178
178
|
|
|
179
179
|
|
|
180
|
+
def test_a_test_writer_that_lists_test_node_ids_in_locked_locks_their_files(tmp_path):
|
|
181
|
+
# Issue #71: a node id `<file>::<test>` in `locked` names its file, for the first run and for a correction.
|
|
182
|
+
vacuous = finding("VACUOUS", "critical", kind="test", file="tests/test_01.py")
|
|
183
|
+
ids = ["tests/test_01.py::test_x", "tests/test_01.py::test_y"]
|
|
184
|
+
first = {**tw("01"), "report": {**tw("01")["report"], "locked": ids}}
|
|
185
|
+
correction = {**first, "files": {"tests/test_01.py": "# stronger\n"}, "commit": "test: 01 stronger test"}
|
|
186
|
+
rig = Inside(tmp_path).run(plan(**{"01/test-writer": [first, correction],
|
|
187
|
+
"01/reviewer": [review("01", [vacuous]), review("01")]}))
|
|
188
|
+
assert rig.code == 0, rig.ledger()
|
|
189
|
+
assert [call["role"] for call in rig.calls("01")].count("test-writer") == 2 # no rerun
|
|
190
|
+
assert rig.state()["tickets"]["01"]["locked"] == ["tests/test_01.py"]
|
|
191
|
+
|
|
192
|
+
|
|
180
193
|
CONTRA = "locked test contradicts spec: tests/test_01.py::test_x"
|
|
181
194
|
OVERRULED = {**tw("01"), "files": {"tests/test_01.py": "def test_x():\n assert '01' == '01'\n"},
|
|
182
195
|
"commit": "test: 01 overruled"}
|
package/skills-manifest.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"generatedAt": "2026-10-
|
|
3
|
+
"generatedAt": "2026-10-15T23:43:23.097Z",
|
|
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.15",
|
|
50
|
+
"hash": "sha256-22559be058dfe337417851a3116ec6a05d9272e7c65510e3f3d02467d1c5c8ca",
|
|
51
51
|
"description": "Implement a specification in code."
|
|
52
52
|
},
|
|
53
53
|
{
|