@jesdi/skills 0.0.45 → 0.0.46
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 -2
- package/skills/implement-spec/README.md +1 -1
- package/skills/implement-spec/driver.py +2 -1
- package/skills/implement-spec/driver_final.py +3 -2
- package/skills/implement-spec/driver_fixloop.py +4 -1
- package/skills/implement-spec/driver_gate.py +5 -1
- package/skills/implement-spec/driver_review.py +22 -0
- package/skills/implement-spec/driver_run.py +17 -11
- package/skills/implement-spec/prompts/correctness.md +2 -0
- package/skills/implement-spec/prompts/final-reviewer.md +3 -1
- package/skills/implement-spec/prompts/pr-writer.md +1 -0
- package/skills/implement-spec/prompts/reviewer.md +2 -0
- package/skills/implement-spec/tests/test_fixloop.py +17 -0
- package/skills-manifest.json +3 -3
package/package.json
CHANGED
|
@@ -98,8 +98,10 @@ and do not know about the run, except `driver_run.py`, which the three deciding
|
|
|
98
98
|
### Work is never dropped
|
|
99
99
|
|
|
100
100
|
- A role that commits keeps its tree after a timeout or an interrupt; its rerun continues from it.
|
|
101
|
-
- A read-only role (reviewer, correctness, final-reviewer, pr-writer) has no work to keep. When it changes its slot
|
|
102
|
-
driver restores
|
|
101
|
+
- A read-only role (reviewer, correctness, final-reviewer, pr-writer) has no work to keep. When it changes its slot or
|
|
102
|
+
the shared git config, the driver restores them and runs the review one more time (`Run.read_only`). The config is
|
|
103
|
+
a byte copy taken before the role (`driver_review.shared_config`): every worktree reads that one file, so a key a
|
|
104
|
+
role writes there, such as a `core.worktree` on a temp directory, breaks git in all of them.
|
|
103
105
|
- A slot with uncommitted work of an interrupted or parked ticket is kept for that ticket (`driver_state.kept`).
|
|
104
106
|
- A blocked ticket keeps its slot when the slot is not clean.
|
|
105
107
|
|
|
@@ -100,7 +100,7 @@ A ticket is ready when each ticket in its **Blocked by** line is merged and no r
|
|
|
100
100
|
1. **test-writer** (`prompts/test-writer.md`). It writes one black-box test for each criterion through the Seam and commits them red. Those files are locked. A ticket with Seam `none — refactor` has no test-writer.
|
|
101
101
|
2. **implementer** (`prompts/implementer.md`). It starts from that commit and makes the locked tests green. It makes the gate (`--gate`) green on its final commit before it reports DONE or DONE_WITH_CONCERNS.
|
|
102
102
|
3. **Gate.** The driver runs `--gate` on the ticket tip.
|
|
103
|
-
4. **Review.** The driver compares each locked file of the ticket with the test-writer commit. A file that differs is a critical finding `locked file changed: <path>`. A locked file of a merged ticket that the ticket changed since its merge base with the PR branch is a critical `fixture` finding `the ticket changed the locked file of a merged ticket: <path>`, so it goes to a correction (see **Before a correction**). Then the **reviewer** (`prompts/reviewer.md`) runs, and for a ticket with a **Risk** line the **correctness** pass (`prompts/correctness.md`) runs at the same time in the same slot.
|
|
103
|
+
4. **Review.** The driver compares each locked file of the ticket with the test-writer commit. A file that differs is a critical finding `locked file changed: <path>`. A locked file of a merged ticket that the ticket changed since its merge base with the PR branch is a critical `fixture` finding `the ticket changed the locked file of a merged ticket: <path>`, so it goes to a correction (see **Before a correction**). Then the **reviewer** (`prompts/reviewer.md`) runs, and for a ticket with a **Risk** line the **correctness** pass (`prompts/correctness.md`) runs at the same time in the same slot. A read-only role (reviewer, correctness, final-reviewer, pr-writer) leaves its slot and the git config as it found them: all worktrees share one config, and one key written there, such as a `core.worktree` on a temp directory, breaks git in each of them. To run a check off the slot, a reviewer reads the driver's gate record or makes its own `git worktree add --detach` copy and removes it. After read-only roles, the driver checks the slot and the shared config (`git rev-parse --git-common-dir`, then `config`) against what they were before. When one of them changed, the driver restores it, writes `a read-only role broke its rule: ...` to the ledger, and runs the review one more time; a second change blocks the ticket.
|
|
104
104
|
5. **Merge.** The driver merges the reviewed tip into the PR branch with git plumbing, then deletes the ticket branch. A conflict goes to the **conflict** agent (`prompts/conflict.md`) once. Its resolution may change no locked file of the ticket. A second conflict blocks the ticket.
|
|
105
105
|
**The landing check.** Merges are serial. Before the PR branch moves, the driver compares the tree that is about to land (the ticket tip of a fast-forward, or the merge commit) with the PR tip: each locked file of each merged ticket must be the same tree entry in both: the same blob with the same mode, so a file that became a symlink is a difference. A file that only one of them has is a difference. A file that the landing ticket lists in its own `locked` is not checked: the test-writer of a later ticket can add a test to a file that an earlier ticket locked, and must list that file. A test-writer report that changes such a file and does not list it is rejected, with the usual one rerun. The review finds such a change first; the landing check catches a change that no review saw, for example of a ticket that merged after the last review. A difference refuses the landing: the ticket is BLOCKED with `locked file changed: <path>` in the ledger, the PR branch does not move, and no fix round runs. The PR branch moves only from the tip that the check read; when another hand moved the branch in between, the driver makes the merge, the check and the gate again, one time. Only that case starts the landing again: a refused landing and a red merge gate block the ticket. The second try runs no conflict agent, so a conflict there blocks the ticket, and so does a branch that moved again.
|
|
106
106
|
**Before a correction.** A test-writer that corrects locked tests (after `overrule-test`, or for a `fixture` finding or a `test` finding on a locked file) starts from the locked versions: the driver puts back each locked file of the ticket and each locked file of a merged ticket that the ticket changed, commits them in one commit of its own, and the ledger names each file. A file of a merged ticket goes back to the version of the PR branch. The commit keeps the tree of the next fixer and of the gate clean, whatever files the correction commits. The correction's prompt names the restored files: when a test in one of them fails only because the ticket changed an interface that the test uses, the correction updates the file to the new interface, keeps what each test asserts, and lists the file in `locked`.
|
|
@@ -26,6 +26,7 @@ import driver_answers
|
|
|
26
26
|
import driver_decisions
|
|
27
27
|
import driver_final
|
|
28
28
|
import driver_fixloop
|
|
29
|
+
import driver_gate
|
|
29
30
|
import driver_graph
|
|
30
31
|
import driver_legacy
|
|
31
32
|
import driver_merge
|
|
@@ -109,7 +110,7 @@ def _run_roles(ticket: Ticket, slot_number: int, run: Run) -> None:
|
|
|
109
110
|
values = {**_prompt_values(ticket, slot, run), "locked": " ".join(entry["locked"])}
|
|
110
111
|
for role in roles:
|
|
111
112
|
run.state.ticket(number, phase=DURING.get(role, "review"))
|
|
112
|
-
values
|
|
113
|
+
values.update(start_sha=start, gate_record=os.path.relpath(driver_gate.record_path(OUT / number, start), slot))
|
|
113
114
|
# A test-writer on a ticket that has locked tests is the correction after a ruling.
|
|
114
115
|
check = {"correction": role == "test-writer" and bool(entry["test_writer_sha"])}
|
|
115
116
|
if check["correction"] and (restored := run.restore_locked_files(number, slot)):
|
|
@@ -26,6 +26,7 @@ from pathlib import Path
|
|
|
26
26
|
|
|
27
27
|
import driver_agents
|
|
28
28
|
import driver_decisions
|
|
29
|
+
import driver_gate
|
|
29
30
|
import driver_merge
|
|
30
31
|
import driver_review
|
|
31
32
|
import driver_tidy
|
|
@@ -325,7 +326,7 @@ def _review(run: Run, final: dict, tip: str) -> None:
|
|
|
325
326
|
"""One round of the final review on `tip`: the full review, or after it the scoped one."""
|
|
326
327
|
done, folder = final["round"], OUT / FINAL
|
|
327
328
|
values = {**_prompt_values(run), "start_sha": tip, "range": f"{final['reviewed'] or final['onto']}..{tip}",
|
|
328
|
-
"gate": str(folder
|
|
329
|
+
"gate": str(driver_gate.record_path(folder, tip)), "review_diff": str(REVIEW_DIFF),
|
|
329
330
|
"ledger": str(OUT / "ledger.md"), "minors": _minor_findings(), "reviewed": str(final["reviewed"]),
|
|
330
331
|
"rulings": "\n".join(driver_decisions.rulings(run.state.read()["run"]["questions"])),
|
|
331
332
|
"findings": str(folder / f"findings-{done - 1}.json")}
|
|
@@ -441,7 +442,7 @@ def _write_body(run: Run, final: dict, tickets: list[Ticket]) -> Path:
|
|
|
441
442
|
raise Blocked("the pr skill is missing: the PR body follows it; install it with "
|
|
442
443
|
"`npx skills add mattpocock/skills --skill pr`")
|
|
443
444
|
values = {**_prompt_values(run), "start_sha": tip, "range": f"{final['onto']}..{tip}",
|
|
444
|
-
"gate": str(OUT / FINAL
|
|
445
|
+
"gate": str(driver_gate.record_path(OUT / FINAL, tip)), "pr_skill": str(method), "facts": facts}
|
|
445
446
|
with during("the PR body"):
|
|
446
447
|
report = run.read_only(FINAL, ".", run.pr, tip, lambda: run.agent("pr-writer", FINAL, ".", values))
|
|
447
448
|
body.write_text(f"{report['body'].strip()}\n\n{facts}")
|
|
@@ -11,6 +11,7 @@ from concurrent.futures import ThreadPoolExecutor
|
|
|
11
11
|
from dataclasses import dataclass, field
|
|
12
12
|
|
|
13
13
|
import driver_agents
|
|
14
|
+
import driver_gate
|
|
14
15
|
import driver_merge
|
|
15
16
|
import driver_review
|
|
16
17
|
import driver_slots
|
|
@@ -39,9 +40,11 @@ class Job:
|
|
|
39
40
|
"""The values of a prompt; `review` is the round whose findings the agent reads."""
|
|
40
41
|
entry = self.run.state.read()["tickets"][self.ticket.number]
|
|
41
42
|
findings = os.path.relpath(OUT / self.ticket.number / f"findings-{review}.json", self.slot)
|
|
43
|
+
gate = os.path.relpath(driver_gate.record_path(OUT / self.ticket.number, self.tip), self.slot)
|
|
42
44
|
return {**self.values, "start_sha": self.tip, "from_sha": self.previous, "round": str(self.round),
|
|
43
45
|
"locked": " ".join(entry["locked"]), "test_writer_sha": str(entry["test_writer_sha"] or ""),
|
|
44
|
-
"findings": findings, "
|
|
46
|
+
"findings": findings, "gate_record": gate,
|
|
47
|
+
"proofs": " ".join(f"{file}:{line}" for file, line in sorted(self.again))}
|
|
45
48
|
|
|
46
49
|
|
|
47
50
|
def entry(models: dict[str, list[str]], round: int) -> str:
|
|
@@ -25,12 +25,16 @@ def _after(slot: str, commit: str) -> str:
|
|
|
25
25
|
return "not clean\n" + "".join(f" {line}\n" for line in status.splitlines()).rstrip("\n") if status else "clean"
|
|
26
26
|
|
|
27
27
|
|
|
28
|
+
def record_path(folder: Path, commit: str) -> Path:
|
|
29
|
+
return folder / f"gate-{commit[:7]}.log"
|
|
30
|
+
|
|
31
|
+
|
|
28
32
|
def run(command: str, slot: str, folder: Path, pr_branch: str) -> tuple[bool, Path]:
|
|
29
33
|
"""Gate the HEAD of `slot`. Returns (green, the record)."""
|
|
30
34
|
commit = head(slot)
|
|
31
35
|
status = git(slot, "status", "--short").stdout
|
|
32
36
|
folder.mkdir(parents=True, exist_ok=True)
|
|
33
|
-
record = folder
|
|
37
|
+
record = record_path(folder, commit)
|
|
34
38
|
# The cache of compiled Python stays out of the slot, so the gate leaves the tree clean.
|
|
35
39
|
env = dict(os.environ, IMPLEMENT_SPEC_BASE=pr_branch, PYTHONPYCACHEPREFIX=str(folder.parent.resolve() / "pycache"))
|
|
36
40
|
with record.open("w") as out:
|
|
@@ -7,6 +7,8 @@ A mutation proof is keyed by (production file, line); the line is the one the fi
|
|
|
7
7
|
from __future__ import annotations
|
|
8
8
|
|
|
9
9
|
import re
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
10
12
|
import driver_agents
|
|
11
13
|
from driver_slots import git, head, names, on_branch
|
|
12
14
|
from driver_tickets import Ticket
|
|
@@ -135,3 +137,23 @@ def disturbed(slot: str, branch: str, tip: str) -> str:
|
|
|
135
137
|
return f"the slot is no longer on {branch} at {tip[:7]}"
|
|
136
138
|
left = [line[3:] for line in git(slot, "status", "--porcelain").stdout.splitlines()]
|
|
137
139
|
return f"the tree is not as the review found it: {', '.join(left)}" if left else ""
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _read(config: Path) -> bytes:
|
|
143
|
+
return config.read_bytes() if config.exists() else b""
|
|
144
|
+
|
|
145
|
+
|
|
146
|
+
def shared_config(slot: str) -> tuple[Path, bytes]:
|
|
147
|
+
"""(the git config file that every worktree of the repository shares, its bytes)."""
|
|
148
|
+
common = git(slot, "rev-parse", "--path-format=absolute", "--git-common-dir").stdout.strip()
|
|
149
|
+
return Path(common, "config"), _read(Path(common, "config"))
|
|
150
|
+
|
|
151
|
+
|
|
152
|
+
def restore_config(before: tuple[Path, bytes]) -> str:
|
|
153
|
+
""""" when the shared git config is still `before` (from `shared_config`), else write it back and say so.
|
|
154
|
+
One worktree that writes the config breaks git in all of them, as a core.worktree on a gone directory does."""
|
|
155
|
+
config, data = before
|
|
156
|
+
if _read(config) == data:
|
|
157
|
+
return ""
|
|
158
|
+
config.write_bytes(data)
|
|
159
|
+
return f"the shared git config {config} changed"
|
|
@@ -120,15 +120,15 @@ class Run:
|
|
|
120
120
|
def read_only(self, number: str, slot: str, branch: str, tip: str, review: Callable[[], T]) -> T:
|
|
121
121
|
"""Run `review` (read-only agents) and return its result. A review that changes the slot runs once more."""
|
|
122
122
|
for last in (False, True):
|
|
123
|
-
error = ""
|
|
123
|
+
error, config = "", driver_review.shared_config(slot)
|
|
124
124
|
try:
|
|
125
125
|
result = review()
|
|
126
126
|
except Blocked as blocked:
|
|
127
127
|
error = str(blocked)
|
|
128
128
|
except Halted:
|
|
129
|
-
_restore_if_changed(number, slot, branch, tip, runs_again=False)
|
|
129
|
+
_restore_if_changed(number, slot, branch, tip, config, runs_again=False)
|
|
130
130
|
raise
|
|
131
|
-
left = _restore_if_changed(number, slot, branch, tip, runs_again=not (error or last))
|
|
131
|
+
left = _restore_if_changed(number, slot, branch, tip, config, runs_again=not (error or last))
|
|
132
132
|
if error or not left:
|
|
133
133
|
break
|
|
134
134
|
if error or left:
|
|
@@ -182,15 +182,21 @@ class Run:
|
|
|
182
182
|
return paths
|
|
183
183
|
|
|
184
184
|
|
|
185
|
-
def _restore_if_changed(number: str, slot: str, branch: str, tip: str,
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
185
|
+
def _restore_if_changed(number: str, slot: str, branch: str, tip: str, config: tuple[Path, bytes],
|
|
186
|
+
runs_again: bool) -> str:
|
|
187
|
+
"""After read-only agents: "" when the slot and the shared git config (`driver_review.shared_config` before
|
|
188
|
+
them) are as they were; else restore both and return what they left."""
|
|
189
|
+
# The config first: a bad core.worktree in it breaks the git commands of the slot check.
|
|
190
|
+
changed = driver_review.restore_config(config)
|
|
191
|
+
moved = driver_review.disturbed(slot, branch, tip)
|
|
192
|
+
if moved:
|
|
189
193
|
driver_review.restore(slot, branch, tip)
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
+
left = "; ".join(filter(None, (changed, moved)))
|
|
195
|
+
if left:
|
|
196
|
+
where = ("slot 1" if number == FINAL else "the slot") + f" to {tip[:7]}"
|
|
197
|
+
restored = " and ".join(filter(None, ("the shared git config" if changed else "", where if moved else "")))
|
|
198
|
+
again = f" and runs {'the final review' if number == FINAL else 'the review'} again" if runs_again else ""
|
|
199
|
+
ledger(f"{_owner(number)}: a read-only role broke its rule: {left}; the driver restored {restored}{again}.")
|
|
194
200
|
return left
|
|
195
201
|
|
|
196
202
|
|
|
@@ -32,4 +32,6 @@ Write one JSON report at {report}. When your sandbox is read-only, give the JSON
|
|
|
32
32
|
}
|
|
33
33
|
```
|
|
34
34
|
|
|
35
|
+
Run git as plain `git` in this worktree, and leave the git config as it is: all worktrees read one shared config, and one key written there, such as a `core.worktree`, breaks git in each of them. So never set GIT_DIR or GIT_WORK_TREE, and never write with `git config`.
|
|
36
|
+
The gate record of the driver for {start_sha} is {gate_record}.
|
|
35
37
|
Never run `git stash`, never reset, and never check out another branch: worktrees share one stash and one ref store.
|
|
@@ -12,7 +12,7 @@ Minor findings that the ticket reviews left for this review, each with its findi
|
|
|
12
12
|
Rulings of the operator:
|
|
13
13
|
{rulings}
|
|
14
14
|
|
|
15
|
-
Review the range with the method of the review-diff skill in the file above, with the spec as its spec path. The gate of that method is done: check that the gate record names {start_sha}, a clean status and exit 0, and run the gate yourself only when it does not.
|
|
15
|
+
Review the range with the method of the review-diff skill in the file above, with the spec as its spec path. The gate of that method is done: check that the gate record names {start_sha}, a clean status and exit 0, and run the gate yourself, in a copy as below, only when it does not.
|
|
16
16
|
Decide for each minor finding in the list whether it is still true at {start_sha}; report each one that is, with the severity it has now. A ruling is settled: do not report what a ruling accepted.
|
|
17
17
|
Give each finding a severity (critical, important or minor), the file and line, a title, the evidence, and a kind: `code`, `test`, or `fixture`.
|
|
18
18
|
Do not commit. Leave the branch and the tree as you found them.
|
|
@@ -33,4 +33,6 @@ Write one JSON report at {report} that matches this schema:
|
|
|
33
33
|
}
|
|
34
34
|
```
|
|
35
35
|
|
|
36
|
+
Run git as plain `git` in this worktree, and leave the git config as it is: all worktrees read one shared config, and one key written there, such as a `core.worktree`, breaks git in each of them. So never set GIT_DIR or GIT_WORK_TREE, and never write with `git config`.
|
|
37
|
+
To run a check off this worktree, read the gate record of the driver, {gate}, or make a copy with `git worktree add --detach <temp dir> {start_sha}`, run the check there, and remove the copy with `git worktree remove --force <temp dir>` before you report.
|
|
36
38
|
Never run `git stash`, never reset, and never check out another branch: worktrees share one stash and one ref store.
|
|
@@ -28,4 +28,5 @@ Write one JSON report at {report} that matches this schema:
|
|
|
28
28
|
}
|
|
29
29
|
```
|
|
30
30
|
|
|
31
|
+
Run git as plain `git` in this worktree, and leave the git config as it is: all worktrees read one shared config, and one key written there, such as a `core.worktree`, breaks git in each of them. So never set GIT_DIR or GIT_WORK_TREE, and never write with `git config`.
|
|
31
32
|
Never run `git stash`, never reset, and never check out another branch: worktrees share one stash and one ref store.
|
|
@@ -31,4 +31,6 @@ Write one JSON report at {report}. When your sandbox is read-only, give the JSON
|
|
|
31
31
|
}
|
|
32
32
|
```
|
|
33
33
|
|
|
34
|
+
Run git as plain `git` in this worktree, and leave the git config as it is: all worktrees read one shared config, and one key written there, such as a `core.worktree`, breaks git in each of them. So never set GIT_DIR or GIT_WORK_TREE, and never write with `git config`.
|
|
35
|
+
To run a check off this worktree, read the gate record of the driver, {gate_record}, or make a copy with `git worktree add --detach <temp dir> {start_sha}`, run the check there, and remove the copy with `git worktree remove --force <temp dir>` before you report.
|
|
34
36
|
Never run `git stash`, never reset, and never check out another branch: worktrees share one stash and one ref store.
|
|
@@ -562,6 +562,23 @@ def test_a_second_review_that_changes_the_slot_blocks_the_ticket_with_the_slot_c
|
|
|
562
562
|
assert slot_state(rig) == ("", rig.sha("01", "implementer")) and "feat: 01 rows" not in rig.pr_subjects()
|
|
563
563
|
|
|
564
564
|
|
|
565
|
+
def test_a_reviewer_that_writes_the_shared_git_config_gets_it_restored_and_runs_once_more(tmp_path):
|
|
566
|
+
# Issue #94: a reviewer ran git with GIT_DIR on the real git dir and left core.worktree on a deleted temp dir.
|
|
567
|
+
rig = Inside(tmp_path)
|
|
568
|
+
shared = rig.repo / ".git" / "config"
|
|
569
|
+
config = shared.read_bytes()
|
|
570
|
+
gone = {"git": [["config", "core.worktree", str(tmp_path / "gone")]]}
|
|
571
|
+
rig.run(plan(**{"01/reviewer": [{**review("01"), **gone}, review("01")]}))
|
|
572
|
+
assert rig.code == 0, rig.ledger()
|
|
573
|
+
assert shared.read_bytes() == config
|
|
574
|
+
(line,) = rig.led("Ticket 01: a read-only role broke its rule:")
|
|
575
|
+
assert f"the shared git config {shared.resolve()} changed" in line
|
|
576
|
+
assert line.endswith("the driver restored the shared git config and runs the review again.")
|
|
577
|
+
tip, prompt = rig.sha("01", "implementer"), rig.prompt("reviewer-0")
|
|
578
|
+
assert len(rig.calls("01", "reviewer")) == 2 and rig.tip() == tip
|
|
579
|
+
assert f".agent/implement/01/gate-{tip[:7]}.log" in prompt and "never write with `git config`" in prompt
|
|
580
|
+
|
|
581
|
+
|
|
565
582
|
def test_a_reviewer_that_times_out_starts_again_from_the_clean_tip(tmp_path):
|
|
566
583
|
hang = {"files": {"demo/export.py": "mutated\n", "junk.txt": "x\n"}, "sleep": 60}
|
|
567
584
|
rig = Inside(tmp_path).run(plan(**{"01/reviewer": [hang, review("01")]}), "--timeout", "reviewer=0.03")
|
package/skills-manifest.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
|
-
"generatedAt": "2026-10-
|
|
3
|
+
"generatedAt": "2026-10-17T15:47:59.977Z",
|
|
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.22",
|
|
50
|
+
"hash": "sha256-66e2dec93c2b4ca09e8ca6693307e5e92f46fe1bd9b0a44379e7d4a4bd82d3f9",
|
|
51
51
|
"description": "Implement a specification in code."
|
|
52
52
|
},
|
|
53
53
|
{
|