@jesdi/skills 0.0.43 → 0.0.45
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 +18 -6
- package/skills/implement-spec/README.md +13 -9
- package/skills/implement-spec/driver.py +80 -39
- package/skills/implement-spec/driver_final.py +6 -6
- package/skills/implement-spec/driver_fixloop.py +17 -5
- package/skills/implement-spec/driver_legacy.py +92 -0
- package/skills/implement-spec/driver_merge.py +11 -10
- package/skills/implement-spec/driver_names.py +32 -0
- package/skills/implement-spec/driver_report.py +9 -2
- package/skills/implement-spec/driver_review.py +10 -2
- package/skills/implement-spec/driver_run.py +7 -2
- package/skills/implement-spec/driver_slots.py +13 -0
- package/skills/implement-spec/driver_state.py +12 -13
- package/skills/implement-spec/prompts/restored-red.md +3 -0
- package/skills/implement-spec/tests/test_agents.py +10 -10
- package/skills/implement-spec/tests/test_amendments.py +2 -2
- package/skills/implement-spec/tests/test_decisions.py +4 -4
- package/skills/implement-spec/tests/test_driver.py +183 -23
- package/skills/implement-spec/tests/test_final.py +5 -5
- package/skills/implement-spec/tests/test_fixloop.py +46 -17
- package/skills/implement-spec/tests/test_landing.py +40 -30
- package/skills/implement-spec/tests/test_legacy.py +139 -0
- package/skills/implement-spec/tests/test_stops.py +1 -1
- package/skills/implement-spec/tests/test_tidy.py +2 -2
- package/skills/implement-spec/tests/test_walk.py +10 -10
- package/skills-manifest.json +3 -3
|
@@ -8,11 +8,7 @@ from __future__ import annotations
|
|
|
8
8
|
import os
|
|
9
9
|
import subprocess
|
|
10
10
|
|
|
11
|
-
from driver_slots import git, names
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
def ancestor(commit: str, of: str) -> bool:
|
|
15
|
-
return git(".", "merge-base", "--is-ancestor", commit, of).returncode == 0
|
|
11
|
+
from driver_slots import ancestor, git, names
|
|
16
12
|
|
|
17
13
|
|
|
18
14
|
def identity(tip: str, slot: str = ".") -> dict[str, str]:
|
|
@@ -82,16 +78,21 @@ def refused(tickets: dict, number: str, old: str, new: str) -> str:
|
|
|
82
78
|
if before.get(path) != after.get(path))
|
|
83
79
|
|
|
84
80
|
|
|
81
|
+
def changed(slot: str, pr_tip: str, paths: list[str]) -> list[str]:
|
|
82
|
+
"""Each of `paths` that the ticket changed in the tree of `slot`."""
|
|
83
|
+
# Against the merge base: a file that only the PR branch changed since then is not the ticket's change.
|
|
84
|
+
base = git(slot, "merge-base", "HEAD", pr_tip).stdout.strip() or pr_tip
|
|
85
|
+
return names(git(slot, "diff", "--name-only", "-z", base, "--", *paths).stdout) if paths else []
|
|
86
|
+
|
|
87
|
+
|
|
85
88
|
def restore(slot: str, pr_tip: str, paths: list[str]) -> list[str]:
|
|
86
89
|
"""Put each of `paths` that the ticket changed back to the version of the PR tip, in the tree of `slot` only; a
|
|
87
90
|
file the PR tip does not have is removed. Returns those paths."""
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
changed = names(git(slot, "diff", "--name-only", "-z", base, "--", *paths).stdout) if paths else []
|
|
91
|
-
for path in changed:
|
|
91
|
+
restored = changed(slot, pr_tip, paths)
|
|
92
|
+
for path in restored:
|
|
92
93
|
if git(slot, "checkout", "-q", pr_tip, "--", path).returncode:
|
|
93
94
|
git(slot, "rm", "-q", "-f", "--", path)
|
|
94
|
-
return
|
|
95
|
+
return restored
|
|
95
96
|
|
|
96
97
|
|
|
97
98
|
def land(pr_branch: str, new: str, old: str) -> str:
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
"""The names of one run: its ticket branches, its recovery ref and its lock file.
|
|
2
|
+
|
|
3
|
+
Each name carries the PR branch as one name component, so runs on other PR branches share none of them. One
|
|
4
|
+
component, because git refs are a tree: `ticket/feat/01` and `ticket/feat/01/02` cannot both exist. `%` and `/` are
|
|
5
|
+
escaped, so two PR branches never give the same component: `feat/x` gives `feat%2Fx`.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
from dataclasses import dataclass
|
|
10
|
+
from pathlib import Path
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
@dataclass(frozen=True)
|
|
14
|
+
class Names:
|
|
15
|
+
pr: str # the PR branch
|
|
16
|
+
|
|
17
|
+
@property
|
|
18
|
+
def _key(self) -> str:
|
|
19
|
+
return self.pr.replace("%", "%25").replace("/", "%2F")
|
|
20
|
+
|
|
21
|
+
def ticket(self, number: str) -> str:
|
|
22
|
+
"""The branch of ticket `number`."""
|
|
23
|
+
return f"ticket/{self._key}/{number}"
|
|
24
|
+
|
|
25
|
+
@property
|
|
26
|
+
def recovery(self) -> str:
|
|
27
|
+
"""The ref that holds the PR tip before the tidy."""
|
|
28
|
+
return f"refs/implement-spec/{self._key}/recovery"
|
|
29
|
+
|
|
30
|
+
def lock_file(self, common_dir: Path) -> Path:
|
|
31
|
+
"""The file one driver per PR branch locks, in the git common directory that every worktree shares."""
|
|
32
|
+
return common_dir / "implement-spec" / f"{self._key}.lock"
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
context: repo (slot path), branch, start (HEAD when the role started), log, round (1 = first review), locked,
|
|
4
4
|
correction (a test-writer corrects locked tests), restored (locked files the driver put back before it),
|
|
5
|
+
restore_red (the gate is red on the commit of that restore),
|
|
5
6
|
owners (ticket numbers, for the final fix batch), merged (locked files of merged tickets, not its own), pr (tip)."""
|
|
6
7
|
from __future__ import annotations
|
|
7
8
|
|
|
@@ -66,8 +67,14 @@ def _correction_errors(report: dict, context: dict) -> list[str]:
|
|
|
66
67
|
return []
|
|
67
68
|
changed = _git(context["repo"], "diff", "--name-only", "-z", context["start"], context["branch"], end="\0")
|
|
68
69
|
more = [path for path in changed if path not in report.get("locked", []) + context.get("restored", [])]
|
|
69
|
-
|
|
70
|
-
|
|
70
|
+
errors = [f"a correction may change only the files it lists in `locked`: its commits also change {', '.join(more)}"
|
|
71
|
+
] if more else []
|
|
72
|
+
if context.get("restore_red") and report["status"] in ("DONE", "DONE_WITH_CONCERNS") and not (
|
|
73
|
+
set(report["locked"]) & set(context["restored"])):
|
|
74
|
+
errors.append("the gate is red on the restore commit, so a restored file fails with the code of this ticket: "
|
|
75
|
+
"update that file and list it in `locked`, or report BLOCKED with a concern that starts with "
|
|
76
|
+
"`locked test contradicts spec:`")
|
|
77
|
+
return errors
|
|
71
78
|
|
|
72
79
|
|
|
73
80
|
def _merged_errors(report: dict, context: dict) -> list[str]:
|
|
@@ -8,7 +8,7 @@ from __future__ import annotations
|
|
|
8
8
|
|
|
9
9
|
import re
|
|
10
10
|
import driver_agents
|
|
11
|
-
from driver_slots import git, head, names
|
|
11
|
+
from driver_slots import git, head, names, on_branch
|
|
12
12
|
from driver_tickets import Ticket
|
|
13
13
|
|
|
14
14
|
# The correctness pass reads this file of the review-diff skill, which is installed next to this one.
|
|
@@ -38,6 +38,14 @@ def locked_changes(slot: str, sha: str | None, locked: list[str]) -> list[dict]:
|
|
|
38
38
|
"evidence": f"git diff {sha}..HEAD -- {path}"} for path, title in titles.items()]
|
|
39
39
|
|
|
40
40
|
|
|
41
|
+
def merged_changes(paths: list[str]) -> list[dict]:
|
|
42
|
+
"""One critical `fixture` finding per locked file of a merged ticket that the ticket changed. A fixture finding
|
|
43
|
+
goes to a test-writer correction: the driver puts the file back before it and gates that commit."""
|
|
44
|
+
return [{"severity": "critical", "file": path, "line": 1, "kind": "fixture", "source": "driver",
|
|
45
|
+
"title": f"the ticket changed the locked file of a merged ticket: {path}",
|
|
46
|
+
"evidence": f"git diff <merge base with the PR branch> -- {path}"} for path in paths]
|
|
47
|
+
|
|
48
|
+
|
|
41
49
|
def _references(entry: dict) -> list[tuple[str | None, list[str]]]:
|
|
42
50
|
"""(reference commit, the locked files it is the reference of): `test_writer_sha`, or the older commit that
|
|
43
51
|
`held` names for a file."""
|
|
@@ -123,7 +131,7 @@ def restore(slot: str, branch: str, tip: str) -> None:
|
|
|
123
131
|
|
|
124
132
|
def disturbed(slot: str, branch: str, tip: str) -> str:
|
|
125
133
|
""""" when the slot is still on `branch` at `tip` with a clean tree, else what the read-only agents left."""
|
|
126
|
-
if
|
|
134
|
+
if on_branch(slot) != branch or head(slot) != tip:
|
|
127
135
|
return f"the slot is no longer on {branch} at {tip[:7]}"
|
|
128
136
|
left = [line[3:] for line in git(slot, "status", "--porcelain").stdout.splitlines()]
|
|
129
137
|
return f"the tree is not as the review found it: {', '.join(left)}" if left else ""
|
|
@@ -24,6 +24,7 @@ import driver_report
|
|
|
24
24
|
import driver_review
|
|
25
25
|
import driver_slots
|
|
26
26
|
import driver_state
|
|
27
|
+
from driver_names import Names
|
|
27
28
|
from driver_slots import git, names
|
|
28
29
|
|
|
29
30
|
OUT = Path(".agent/implement")
|
|
@@ -88,6 +89,10 @@ class Run:
|
|
|
88
89
|
asks: dict[str, Ask] = field(default_factory=dict) # ticket number -> its pending stop
|
|
89
90
|
halted: list[str] = field(default_factory=list) # why the run starts no other agent; [] while it goes on
|
|
90
91
|
|
|
92
|
+
@property
|
|
93
|
+
def names(self) -> Names:
|
|
94
|
+
return Names(self.pr)
|
|
95
|
+
|
|
91
96
|
def tip(self) -> str:
|
|
92
97
|
return git(".", "rev-parse", f"refs/heads/{self.pr}").stdout.strip()
|
|
93
98
|
|
|
@@ -191,8 +196,8 @@ def _restore_if_changed(number: str, slot: str, branch: str, tip: str, runs_agai
|
|
|
191
196
|
|
|
192
197
|
def _reset_if_moved(slot: str, branch: str, tip: str) -> str:
|
|
193
198
|
"""After an agent: "" when the slot is on `branch` and still holds `tip`; else reset it and say what was wrong."""
|
|
194
|
-
head =
|
|
195
|
-
if
|
|
199
|
+
head = driver_slots.on_branch(slot)
|
|
200
|
+
if not driver_slots.branch_exists(branch):
|
|
196
201
|
problem = f"branch {branch} is missing"
|
|
197
202
|
elif head != branch:
|
|
198
203
|
problem = f"HEAD is on {head or 'a detached commit'} instead of {branch}"
|
|
@@ -22,6 +22,19 @@ def head(slot: str) -> str:
|
|
|
22
22
|
return git(slot, "rev-parse", "HEAD").stdout.strip()
|
|
23
23
|
|
|
24
24
|
|
|
25
|
+
def on_branch(slot: str) -> str:
|
|
26
|
+
"""The branch the slot has checked out; "" when its HEAD is detached."""
|
|
27
|
+
return git(slot, "symbolic-ref", "-q", "--short", "HEAD").stdout.strip()
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def ancestor(commit: str, of: str) -> bool:
|
|
31
|
+
return git(".", "merge-base", "--is-ancestor", commit, of).returncode == 0
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def branch_exists(branch: str) -> bool:
|
|
35
|
+
return git(".", "rev-parse", "-q", "--verify", f"refs/heads/{branch}").returncode == 0
|
|
36
|
+
|
|
37
|
+
|
|
25
38
|
def path(number: int) -> str:
|
|
26
39
|
return "." if number == 1 else f".agent/worktrees/slot-{number}"
|
|
27
40
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""state.json, the resume contract: one lock, every write a read-modify-write, published by rename.
|
|
2
2
|
|
|
3
|
-
{"run": {revision, pr_branch, gates, stops, agents: [{ticket, role, entry, seconds, cost}], questions, start,
|
|
3
|
+
{"run": {revision, pr_branch, schema, gates, stops, agents: [{ticket, role, entry, seconds, cost}], questions, start,
|
|
4
4
|
landed: [[ticket, the PR tip its merge made]], final},
|
|
5
5
|
"tickets": {"NN": {phase, round, tip, test_writer_sha, locked, green_gates, slot, note, held}}}"""
|
|
6
6
|
from __future__ import annotations
|
|
@@ -13,8 +13,10 @@ import uuid
|
|
|
13
13
|
from pathlib import Path
|
|
14
14
|
from typing import IO
|
|
15
15
|
|
|
16
|
-
from
|
|
16
|
+
from driver_names import Names
|
|
17
|
+
from driver_slots import ancestor, git, on_branch, path
|
|
17
18
|
|
|
19
|
+
SCHEMA = 2 # the version of this contract; 2: the names of driver_names. A run of an older driver has none.
|
|
18
20
|
PHASES = ("pending", "test-writer", "implement", "gate", "review", "fix", "merge", "merged", "blocked")
|
|
19
21
|
|
|
20
22
|
|
|
@@ -42,7 +44,7 @@ class State:
|
|
|
42
44
|
"""Create the file for a new run, or add the tickets a resumed run does not know yet."""
|
|
43
45
|
def change(state: dict) -> None:
|
|
44
46
|
state.setdefault("run", {
|
|
45
|
-
"revision": uuid.uuid4().hex, "pr_branch": pr_branch, "gates": 0, "stops": 0, "agents": [],
|
|
47
|
+
"revision": uuid.uuid4().hex, "pr_branch": pr_branch, "schema": SCHEMA, "gates": 0, "stops": 0, "agents": [],
|
|
46
48
|
"questions": [], "start": git(".", "rev-parse", f"refs/heads/{pr_branch}").stdout.strip(),
|
|
47
49
|
"landed": [], "final": _final()})
|
|
48
50
|
for key, value in _final().items(): # a run of an older driver lacks the newer keys
|
|
@@ -67,22 +69,19 @@ class State:
|
|
|
67
69
|
return self.update(lambda state: state["tickets"][number].update(fields))["tickets"][number]
|
|
68
70
|
|
|
69
71
|
|
|
70
|
-
def lock(path: Path) -> IO | None:
|
|
71
|
-
"""The
|
|
72
|
+
def lock(path: Path, shared: bool = False) -> IO | None:
|
|
73
|
+
"""The kernel lock on `path`, exclusive or `shared`, held while the returned file is open; None when another
|
|
74
|
+
process has it in a way that excludes this one."""
|
|
72
75
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
73
76
|
held = path.open("a")
|
|
74
77
|
try:
|
|
75
|
-
fcntl.flock(held, fcntl.LOCK_EX | fcntl.LOCK_NB)
|
|
78
|
+
fcntl.flock(held, (fcntl.LOCK_SH if shared else fcntl.LOCK_EX) | fcntl.LOCK_NB)
|
|
76
79
|
except BlockingIOError:
|
|
77
80
|
held.close()
|
|
78
81
|
return None
|
|
79
82
|
return held
|
|
80
83
|
|
|
81
84
|
|
|
82
|
-
def _ancestor(commit: str, of: str) -> bool:
|
|
83
|
-
return git(".", "merge-base", "--is-ancestor", commit, of).returncode == 0
|
|
84
|
-
|
|
85
|
-
|
|
86
85
|
def verified(entry: dict, branch: str, pr_branch: str) -> tuple[dict, str]:
|
|
87
86
|
"""(the fields to resume from, why) when git contradicts the entry; ({}, "") when it does not."""
|
|
88
87
|
phase, tip = entry["phase"], str(entry["tip"])
|
|
@@ -90,7 +89,7 @@ def verified(entry: dict, branch: str, pr_branch: str) -> tuple[dict, str]:
|
|
|
90
89
|
return {}, ""
|
|
91
90
|
# The branch of a merged ticket is deleted, so its commits are trusted only while the PR branch has them.
|
|
92
91
|
where, word = (pr_branch, "in") if phase == "merged" else (branch, "on")
|
|
93
|
-
if
|
|
92
|
+
if ancestor(tip, where):
|
|
94
93
|
return {}, ""
|
|
95
94
|
return {"phase": "implement", "tip": entry["test_writer_sha"]}, f"the recorded tip {tip[:7]} is not {word} {where}"
|
|
96
95
|
|
|
@@ -102,7 +101,7 @@ def kept(state: dict) -> dict[str, int]:
|
|
|
102
101
|
for number, entry in state.get("tickets", {}).items():
|
|
103
102
|
if (entry["phase"] in ("test-writer", "implement") or number in asking) and entry["slot"]:
|
|
104
103
|
slot = path(entry["slot"])
|
|
105
|
-
on =
|
|
106
|
-
if on ==
|
|
104
|
+
on = on_branch(slot)
|
|
105
|
+
if on == Names(state["run"]["pr_branch"]).ticket(number) and git(slot, "status", "--porcelain").stdout.strip():
|
|
107
106
|
found[number] = entry["slot"]
|
|
108
107
|
return found
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
|
|
2
|
+
The driver ran the gate on that commit, {start_sha}, and the gate is red: read the output in {record}. The gate was green before the restore, so a restored file fails with the code of this ticket.
|
|
3
|
+
Update each restored file that fails so that it works with the new interface: keep what each test asserts, and list the file in `locked`. When no such update can keep what a test asserts, report BLOCKED with a concern that starts with `locked test contradicts spec:`, then the test and the evidence.
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
"""Agent launch acceptance tests, through `sh run.sh` with a fake runner script or fake claude/codex on PATH.
|
|
2
2
|
|
|
3
3
|
Interface choices (ticket 02): `--runner <script>` is called as `<script> <role> <prompt> <report>` in the slot
|
|
4
|
-
directory. A plain run (no --dry-run) branches ticket/NN from the PR tip in slot 1, runs the test-writer (unless the
|
|
4
|
+
directory. A plain run (no --dry-run) branches ticket/feat%2Fcsv/NN from the PR tip in slot 1, runs the test-writer (unless the
|
|
5
5
|
Seam is `none — refactor`) then the implementer; from ticket 03 on an accepted ticket goes on to the driver's gate (`true` here) and a
|
|
6
|
-
merge into the PR branch, which deletes `ticket/NN`. Exit 0 when the ticket merges, 1 on BLOCKED.
|
|
6
|
+
merge into the PR branch, which deletes `ticket/feat%2Fcsv/NN`. Exit 0 when the ticket merges, 1 on BLOCKED.
|
|
7
7
|
`--only <role>` runs that one role on the first ticket and stops. `--spec <path>` names the spec for the prompts.
|
|
8
8
|
`--timeout <role>=<minutes>` (repeatable, fractions allowed) overrides a role timeout.
|
|
9
9
|
Ticket 08: the fixture has no `origin`, and a run that merges its ticket ends with the tidy and the final review, so
|
|
10
|
-
`ticket_commits()` reads the ticket's commits from `refs/implement/recovery`, the PR tip from before the tidy.
|
|
10
|
+
`ticket_commits()` reads the ticket's commits from `refs/implement-spec/feat%2Fcsv/recovery`, the PR tip from before the tidy.
|
|
11
11
|
"""
|
|
12
12
|
import json
|
|
13
13
|
import os
|
|
@@ -134,7 +134,7 @@ class World:
|
|
|
134
134
|
return path.read_text() if path.exists() else ""
|
|
135
135
|
|
|
136
136
|
def ticket_commits(self):
|
|
137
|
-
made = subprocess.run(["git", "rev-parse", "--verify", "-q", "refs/implement/recovery"], cwd=self.repo,
|
|
137
|
+
made = subprocess.run(["git", "rev-parse", "--verify", "-q", "refs/implement-spec/feat%2Fcsv/recovery"], cwd=self.repo,
|
|
138
138
|
capture_output=True, text=True).stdout.strip() or "feat/csv"
|
|
139
139
|
return git(self.repo, "rev-list", "--reverse", "--no-merges", f"{self.base}..{made}").split()
|
|
140
140
|
|
|
@@ -175,7 +175,7 @@ def test_runner_is_called_with_role_prompt_report_in_the_slot_directory(env):
|
|
|
175
175
|
assert role == "implementer" and call["prompt"].strip()
|
|
176
176
|
assert Path(call["cwd"], report).resolve().as_posix().endswith(".agent/implement/01/implementer-0.json")
|
|
177
177
|
assert Path(call["cwd"], report).exists()
|
|
178
|
-
assert call["cwd"] == str(w.repo.resolve()) and call["branch"] == "ticket/01"
|
|
178
|
+
assert call["cwd"] == str(w.repo.resolve()) and call["branch"] == "ticket/feat%2Fcsv/01"
|
|
179
179
|
|
|
180
180
|
|
|
181
181
|
def test_default_models_file_names_every_role():
|
|
@@ -231,7 +231,7 @@ def test_unknown_provider_or_effort_exits_3_naming_role_and_entry_before_any_age
|
|
|
231
231
|
proc = w.run({"test-writer": [tw()]})
|
|
232
232
|
assert proc.returncode == 3 and not w.calls("test-writer")
|
|
233
233
|
assert not launches(w.tmp, "claude") and not launches(w.tmp, "codex")
|
|
234
|
-
assert "ticket/01" not in git(w.repo, "branch", "--list")
|
|
234
|
+
assert "ticket/feat%2Fcsv/01" not in git(w.repo, "branch", "--list")
|
|
235
235
|
|
|
236
236
|
|
|
237
237
|
def test_implementer_prompt_carries_pointers_and_only_template_sentences(env):
|
|
@@ -284,7 +284,7 @@ def test_a_model_the_account_cannot_use_fails_the_preflight_with_exit_3_before_a
|
|
|
284
284
|
assert f"model entry openai/gpt-astra@medium: the codex CLI failed the preflight (exit 1): {REFUSED}" \
|
|
285
285
|
in proc.stdout.splitlines(), proc.stdout
|
|
286
286
|
assert not launches(w.tmp, "claude") and not launches(w.tmp, "codex")
|
|
287
|
-
assert "ticket/01" not in git(w.repo, "branch", "--list") and not w.ledger()
|
|
287
|
+
assert "ticket/feat%2Fcsv/01" not in git(w.repo, "branch", "--list") and not w.ledger()
|
|
288
288
|
|
|
289
289
|
|
|
290
290
|
def test_a_cli_that_fails_with_no_report_stops_the_run_with_exit_4_and_the_same_command_resumes(env):
|
|
@@ -368,16 +368,16 @@ def test_head_moved_to_the_pr_branch_resets_the_slot_and_reruns_once(env):
|
|
|
368
368
|
ok(w.run({"test-writer": [tw()], "implementer": [{"git": [["checkout", "feat/csv"]]}, impl()]}))
|
|
369
369
|
first, second = w.calls("implementer")
|
|
370
370
|
tests_sha = w.ticket_commits()[0]
|
|
371
|
-
assert (second["branch"], second["head"]) == ("ticket/01", tests_sha)
|
|
371
|
+
assert (second["branch"], second["head"]) == ("ticket/feat%2Fcsv/01", tests_sha)
|
|
372
372
|
assert any("implementer" in l and "feat/csv" in l for l in w.ledger().splitlines()), w.ledger()
|
|
373
373
|
|
|
374
374
|
|
|
375
375
|
def test_a_missing_ticket_branch_is_a_violation_named_in_the_ledger(env):
|
|
376
376
|
w = World(env)
|
|
377
|
-
gone = {"git": [["checkout", "--detach"], ["branch", "-D", "ticket/01"]]}
|
|
377
|
+
gone = {"git": [["checkout", "--detach"], ["branch", "-D", "ticket/feat%2Fcsv/01"]]}
|
|
378
378
|
proc = w.run({"test-writer": [tw()], "implementer": [gone, impl()]})
|
|
379
379
|
assert "Traceback" not in proc.stderr
|
|
380
|
-
assert any("ticket/01" in l and "branch" in l for l in w.ledger().splitlines()), w.ledger()
|
|
380
|
+
assert any("ticket/feat%2Fcsv/01" in l and "branch" in l for l in w.ledger().splitlines()), w.ledger()
|
|
381
381
|
|
|
382
382
|
|
|
383
383
|
def test_log_next_to_report_and_ledger_records_role_entry_duration_and_cost(env):
|
|
@@ -5,7 +5,7 @@ with and without its bare `origin`, and the rig of test_review with and without
|
|
|
5
5
|
|
|
6
6
|
Interface choices (ticket 08):
|
|
7
7
|
- with no `origin`, the tidied commits sit on the commit the PR branch had when the run started (nothing to rebase
|
|
8
|
-
onto), and `refs/implement/recovery` holds the PR tip from before the tidy, as with an `origin`.
|
|
8
|
+
onto), and `refs/implement-spec/feat%2Fcsv/recovery` holds the PR tip from before the tidy, as with an `origin`.
|
|
9
9
|
- the one ledger sentence about the missing remote names `origin` and says that the rebase and the push were skipped.
|
|
10
10
|
- a refused start (exit 3) prints its reason on stdout or stderr: `origin` for a push that has no remote; for the
|
|
11
11
|
correctness role one line that names `correctness` and `codex`, or `correctness` and each entry of its list.
|
|
@@ -39,7 +39,7 @@ def test_with_no_origin_the_branch_is_tidied_gated_and_reviewed_with_no_rebase_a
|
|
|
39
39
|
rows = rig.commits(rig.start)
|
|
40
40
|
assert [r["subject"] for r in rows] == ["feat(01): Export rows", "feat(02): Quote fields"]
|
|
41
41
|
assert ref(rig.repo, f"{rows[0]['sha']}~1") == rig.start, "no rebase: the ticket commits sit on the branch's start"
|
|
42
|
-
assert ref(rig.repo, "feat/csv^{tree}") == ref(rig.repo, "refs/implement/recovery^{tree}")
|
|
42
|
+
assert ref(rig.repo, "feat/csv^{tree}") == ref(rig.repo, "refs/implement-spec/feat%2Fcsv/recovery^{tree}")
|
|
43
43
|
records = sorted(p.name for p in (rig.repo / ".agent/implement/final").glob("gate-*.log"))
|
|
44
44
|
assert records == [f"gate-{tip[:7]}.log"], records
|
|
45
45
|
(gate,) = [g for g in rig.gates() if g["commit"] == tip]
|
|
@@ -99,7 +99,7 @@ def test_an_answers_file_of_an_earlier_run_is_not_applied_to_the_same_question_o
|
|
|
99
99
|
write_answers(rig, {Q1: "overrule-test"})
|
|
100
100
|
# The operator starts again: the run directory goes, the answers file is outside it and stays.
|
|
101
101
|
shutil.rmtree(rig.repo / ".agent/implement")
|
|
102
|
-
git(rig.repo, "branch", "-D", "ticket/01")
|
|
102
|
+
git(rig.repo, "branch", "-D", "ticket/feat%2Fcsv/01")
|
|
103
103
|
again = steps(blocked(), blocked(), impl("01"))
|
|
104
104
|
rig.run(again)
|
|
105
105
|
new = decisions(rig)
|
|
@@ -144,7 +144,7 @@ def test_a_stop_with_uncommitted_work_keeps_its_slot_and_takes_the_answer(tmp_pa
|
|
|
144
144
|
rig.run(steps(dirty, impl("01")), *UNATTENDED)
|
|
145
145
|
assert rig.code == 0, rig.ledger()
|
|
146
146
|
second = rig.calls("01", "implementer")[1]
|
|
147
|
-
assert second["status"] == "M demo/export.py" and second["branch"] == "ticket/01", "the work is in the tree of the rerun"
|
|
147
|
+
assert second["status"] == "M demo/export.py" and second["branch"] == "ticket/feat%2Fcsv/01", "the work is in the tree of the rerun"
|
|
148
148
|
prompt = (rig.folder() / "implementer-0.prompt.md").read_text()
|
|
149
149
|
assert "the locked tests stand as they are" in prompt and "uncommitted work" in prompt
|
|
150
150
|
assert "feat: 01 rows" in rig.pr_subjects() and not git(rig.repo, "status", "--porcelain")
|
|
@@ -221,7 +221,7 @@ def test_a_correction_that_commits_more_than_the_files_it_lists_is_rejected(tmp_
|
|
|
221
221
|
assert rig.code == 1, rig.ledger()
|
|
222
222
|
assert rig.led("Ticket 01: BLOCKED at the test-writer: a correction may change only the files it lists in "
|
|
223
223
|
"`locked`: its commits also change demo/export.py.")
|
|
224
|
-
assert rig.led(restored(OTHER)) and git(rig.repo, "show", f"ticket/01:{OTHER}") == OTHER_TEST
|
|
224
|
+
assert rig.led(restored(OTHER)) and git(rig.repo, "show", f"ticket/feat%2Fcsv/01:{OTHER}") == OTHER_TEST
|
|
225
225
|
assert len(rig.calls("01", "test-writer")) == 3 and len(rig.calls("01", "implementer")) == 1, "one rerun, no implementer"
|
|
226
226
|
assert "its commits also change demo/export.py" in rig.prompt("test-writer-0")
|
|
227
227
|
assert "feat: 01 rows" not in rig.pr_subjects() and rig.state()["tickets"]["01"]["held"] == {}
|
|
@@ -370,7 +370,7 @@ def test_unattended_writes_the_stage_file_and_prints_no_question(tmp_path, capsy
|
|
|
370
370
|
|
|
371
371
|
|
|
372
372
|
def test_unattended_names_the_blocked_tickets_and_needs_a_stage_path_to_write(tmp_path):
|
|
373
|
-
rig = Pushing(tmp_path, ONE, gate={"red": ["ticket/01"]})
|
|
373
|
+
rig = Pushing(tmp_path, ONE, gate={"red": ["ticket/feat%2Fcsv/01"]})
|
|
374
374
|
rig.run(plan(), "--unattended")
|
|
375
375
|
assert rig.code == 1 and not (rig.repo / ".agent/stage.json").exists()
|
|
376
376
|
rig.run(plan(), "--unattended", "--stage-json", ".agent/out/stage.json")
|