@jesdi/skills 0.0.44 → 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 +13 -6
- package/skills/implement-spec/README.md +9 -6
- package/skills/implement-spec/driver.py +82 -40
- package/skills/implement-spec/driver_final.py +9 -8
- package/skills/implement-spec/driver_fixloop.py +4 -1
- package/skills/implement-spec/driver_gate.py +5 -1
- package/skills/implement-spec/driver_legacy.py +92 -0
- package/skills/implement-spec/driver_merge.py +1 -5
- package/skills/implement-spec/driver_names.py +32 -0
- package/skills/implement-spec/driver_review.py +24 -2
- package/skills/implement-spec/driver_run.py +24 -13
- package/skills/implement-spec/driver_slots.py +13 -0
- package/skills/implement-spec/driver_state.py +12 -13
- 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_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 +22 -5
- package/skills/implement-spec/tests/test_landing.py +26 -23
- 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
|
@@ -7,8 +7,10 @@ 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
|
-
from driver_slots import git, head, names
|
|
13
|
+
from driver_slots import git, head, names, on_branch
|
|
12
14
|
from driver_tickets import Ticket
|
|
13
15
|
|
|
14
16
|
# The correctness pass reads this file of the review-diff skill, which is installed next to this one.
|
|
@@ -131,7 +133,27 @@ def restore(slot: str, branch: str, tip: str) -> None:
|
|
|
131
133
|
|
|
132
134
|
def disturbed(slot: str, branch: str, tip: str) -> str:
|
|
133
135
|
""""" when the slot is still on `branch` at `tip` with a clean tree, else what the read-only agents left."""
|
|
134
|
-
if
|
|
136
|
+
if on_branch(slot) != branch or head(slot) != tip:
|
|
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"
|
|
@@ -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
|
|
|
@@ -115,15 +120,15 @@ class Run:
|
|
|
115
120
|
def read_only(self, number: str, slot: str, branch: str, tip: str, review: Callable[[], T]) -> T:
|
|
116
121
|
"""Run `review` (read-only agents) and return its result. A review that changes the slot runs once more."""
|
|
117
122
|
for last in (False, True):
|
|
118
|
-
error = ""
|
|
123
|
+
error, config = "", driver_review.shared_config(slot)
|
|
119
124
|
try:
|
|
120
125
|
result = review()
|
|
121
126
|
except Blocked as blocked:
|
|
122
127
|
error = str(blocked)
|
|
123
128
|
except Halted:
|
|
124
|
-
_restore_if_changed(number, slot, branch, tip, runs_again=False)
|
|
129
|
+
_restore_if_changed(number, slot, branch, tip, config, runs_again=False)
|
|
125
130
|
raise
|
|
126
|
-
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))
|
|
127
132
|
if error or not left:
|
|
128
133
|
break
|
|
129
134
|
if error or left:
|
|
@@ -177,22 +182,28 @@ class Run:
|
|
|
177
182
|
return paths
|
|
178
183
|
|
|
179
184
|
|
|
180
|
-
def _restore_if_changed(number: str, slot: str, branch: str, tip: str,
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
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:
|
|
184
193
|
driver_review.restore(slot, branch, tip)
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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}.")
|
|
189
200
|
return left
|
|
190
201
|
|
|
191
202
|
|
|
192
203
|
def _reset_if_moved(slot: str, branch: str, tip: str) -> str:
|
|
193
204
|
"""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
|
|
205
|
+
head = driver_slots.on_branch(slot)
|
|
206
|
+
if not driver_slots.branch_exists(branch):
|
|
196
207
|
problem = f"branch {branch} is missing"
|
|
197
208
|
elif head != branch:
|
|
198
209
|
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
|
|
@@ -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.
|
|
@@ -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")
|