tools/context-pack.py: carry cut_spec anchors to the worktree #8

Merged
eureka merged 6 commits from hands/context-pack-tool into main 2026-10-09 12:06:07 +00:00
5 changed files with 836 additions and 0 deletions

View file

@ -13,3 +13,4 @@ gets a Soul pass before merge.
| `tools/mutation-table.sh` | One verify job that runs a list of hand mutants (file, before, after, test) against a revision and returns a kill table: CAUGHT with the killing test, SURVIVED, UNVIABLE, TIMEOUT, NOAPPLY, ERROR. Refuses a red or zero-test baseline and a red BUILD; rows print live. Isolation is Docker's: the baselines run once in a container that holds the revision, that container is committed to a throwaway image, and every mutant runs in a fresh `--rm` container from it, so nothing a run writes (tracked, untracked or ignored, such as `target/`) reaches the next mutant; the image is removed when the job ends, killed or not. The shared registry and sccache volumes and the network are not isolated. A file-level load failure (vitest `FAIL <file> [ <file> ]`, jest `FAIL <file>` plus `Test suite failed to run`) reads UNVIABLE. Replaces per-agent runmut/mutrun/strykrun scripts. | Soul, Hands (scoped run) | `references/briefs.md` (Hands verification, Soul reruns) |
| `tools/testparse.sh` | Sourced library: failing test names and pass/fail/skip counts from cargo, dotnet, vitest, jest and pytest logs, and test files that failed to load (`LOADFAIL`). Shared by the summary and the kill table. | the two scripts above | not named by a brief; it lives and dies with them |
| `tools/test-tools.sh` | Local fixture check of the parsers and the summary over `tools/fixtures/logs` (real cargo and mutation logs, plus hand-written dotnet, vitest, jest, pytest, exit-0-but-failed and no-verdict logs), and of the mutation runner's host driver and container half under a mock docker. Run it before merging a change to the scripts above. | whoever changes those scripts | `SKILL.md` (Shared tools) |
| `tools/context-pack.py` | `py -3 tools/context-pack.py <cut_spec id> <worktree> [--since <sha>] [--only <k>[,<k>...]] [--spec-json <file>] [--max-lines <n>]`. Carries a cut_spec's `file_changes` anchors from the spec's base to the worktree by `git diff` hunks (the working tree included) and prints one header, a status table (SAME, MOVED, TOUCHED, RENAMED, NEW, GONE, OUT_OF_RANGE, UNRESOLVED) and windows of at most 100 lines numbered in worktree lines, within `--max-lines` (800). Exit 3 when an anchor cannot be carried, 4 when the spec is unreadable (fetched by id through the eureka-state binding, retried once on Unavailable; `--spec-json` takes a saved view). `--since <sha>` re-prints only the windows edited since that commit. Standard library plus git; writes nothing. | Hands, Soul, Imagination | the briefs that cut `context-pack-briefs` edits; not yet named |

475
tools/context-pack.py Normal file
View file

@ -0,0 +1,475 @@
#!/usr/bin/env python3
"""Carry a cut_spec's file_changes anchors from the spec's base to a worktree.
Usage: context-pack.py <cut_spec id> <worktree> [--since <sha>] [--only <k>[,<k>...]]
[--spec-json <file>] [--max-lines <n>]
Prints a header, a status table (one line per anchor) and bounded windows of the
code each anchor names, numbered in the worktree's lines. Exit: 0 every anchor
carried; 3 an anchor cannot be carried (OUT_OF_RANGE, UNRESOLVED, GONE outside
the spec's deletes); 2 usage; 4 spec unreadable. Standard library plus git.
Errors name a field and a code, never an input value.
--max-lines bounds the windows, not the table: the table always lists every
anchor, so a budget smaller than the table prints the table and no window.
"""
import argparse
import json
import os
import queue
import re
import subprocess
import sys
import threading
WINDOW_CAP = 100
CONTEXT = 3
DEFAULT_MAX_LINES = 800
ERROR_CODES = ("Unavailable", "Rejected", "Misconfigured", "TooLarge",
"Unencodable", "InvalidInput", "Internal")
HUNK = re.compile(r"^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@")
HEX = re.compile(r"^[0-9a-fA-F]{7,40}$")
class Fail(Exception):
def __init__(self, code, msg):
super().__init__(msg)
self.code, self.msg = code, msg
class Parser(argparse.ArgumentParser):
def error(self, message): # argparse would echo the offending value
sys.stderr.write("context-pack: usage error\n" + self.format_usage())
sys.exit(2)
def git(wt, *args):
"""The one git call: stdout bytes, or None when git failed; stderr is always
discarded. Paths are literal pathspecs here, so a spec path holding [ ] * ?
names that file and no sibling."""
try:
p = subprocess.run(["git", "-C", wt, "--literal-pathspecs", *args], stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL, stdin=subprocess.DEVNULL)
except OSError:
return None
return p.stdout if p.returncode == 0 else None
def text_lines(raw):
s = raw.decode("utf-8", "replace").replace("\r\n", "\n")
parts = s.split("\n")
if parts[-1] == "":
parts.pop()
return parts
# ---- the spec ------------------------------------------------------------
def spec_from_json(obj):
if isinstance(obj, dict) and "document" in obj:
obj = obj["document"]
if isinstance(obj, dict) and "kind" in obj and "value" in obj:
obj = obj["value"]
if not isinstance(obj, dict) or "base" not in obj or "file_changes" not in obj:
raise Fail(4, "spec: not a cut_spec value")
return check_spec_shape(obj)
def is_int(v):
return isinstance(v, int) and not isinstance(v, bool)
def check_spec_shape(spec):
"""A wrong-typed field is a typed refusal naming the field and InvalidInput,
never the value."""
def bad(field):
raise Fail(4, "spec: %s: InvalidInput" % field)
if not isinstance(spec["base"], str):
bad("base")
fcs = spec["file_changes"]
if not isinstance(fcs, list):
bad("file_changes")
for k, fc in enumerate(fcs, 1):
at = "file_changes[%d]" % k
if not isinstance(fc, dict):
bad(at)
loc = fc.get("location")
if not isinstance(loc, dict):
bad(at + ".location")
if not isinstance(loc.get("path"), str):
bad(at + ".location.path")
if not is_int(loc.get("line")):
bad(at + ".location.line")
if loc.get("end_line") is not None and not is_int(loc["end_line"]):
bad(at + ".location.end_line")
if "change" in fc and not isinstance(fc["change"], str):
bad(at + ".change")
dels = spec.get("deletes", [])
if not isinstance(dels, list):
bad("deletes")
for k, d in enumerate(dels, 1):
if not isinstance(d, dict) or not isinstance(d.get("path"), str):
bad("deletes[%d].path" % k)
return spec
def mcp_view_once(spec_id):
exe = os.environ.get("EUREKA_STATE_EXE")
env = dict(os.environ)
args = []
try:
with open(os.path.join(os.path.expanduser("~"), ".claude.json"), encoding="utf-8") as f:
binding = json.load(f)["mcpServers"]["eureka-state"]
exe = exe or binding.get("command")
args = list(binding.get("args", []))
for k, v in (binding.get("env") or {}).items():
env.setdefault(k, v)
except (OSError, ValueError, KeyError, TypeError):
if not exe:
raise Fail(4, "binding: eureka-state is not configured")
if not exe:
raise Fail(4, "binding: eureka-state has no command")
try:
proc = subprocess.Popen([exe, *args], stdin=subprocess.PIPE, stdout=subprocess.PIPE,
stderr=subprocess.DEVNULL, env=env)
except OSError:
raise Fail(4, "transport: Unavailable")
lines = queue.Queue()
def pump():
for ln in proc.stdout:
lines.put(ln)
lines.put(None)
threading.Thread(target=pump, daemon=True).start()
def send(obj):
proc.stdin.write((json.dumps(obj) + "\n").encode("utf-8"))
proc.stdin.flush()
def reply(want):
while True:
try:
ln = lines.get(timeout=30)
except queue.Empty:
raise Fail(4, "transport: Unavailable")
if ln is None:
raise Fail(4, "transport: Unavailable")
try:
msg = json.loads(ln)
except ValueError:
continue
if isinstance(msg, dict) and msg.get("id") == want:
return msg
try:
send({"jsonrpc": "2.0", "id": 1, "method": "initialize",
"params": {"protocolVersion": "2024-11-05", "capabilities": {},
"clientInfo": {"name": "context-pack", "version": "1"}}})
reply(1)
send({"jsonrpc": "2.0", "method": "notifications/initialized"})
send({"jsonrpc": "2.0", "id": 2, "method": "tools/call",
"params": {"name": "view", "arguments": {"id": {"kind": "CutSpec", "id": spec_id}}}})
res = reply(2).get("result") or {}
except OSError:
raise Fail(4, "transport: Unavailable")
finally:
try:
proc.stdin.close()
proc.terminate()
except OSError:
pass
body = "".join(c.get("text", "") for c in res.get("content", []) if isinstance(c, dict))
if res.get("isError"):
code = next((c for c in ERROR_CODES if c in body), "error")
raise Fail(4, "view: " + code)
try:
view = json.loads(body)
except ValueError:
raise Fail(4, "view: unparsable answer")
if view is None:
raise Fail(4, "view: no such cut_spec")
return spec_from_json(view)
def mcp_view(spec_id):
if ":cut_spec:" not in spec_id:
raise Fail(2, "spec id: not a cut_spec id")
try:
return mcp_view_once(spec_id)
except Fail as first:
if first.msg.split(": ")[-1] != "Unavailable":
raise
return mcp_view_once(spec_id)
def load_spec(spec_id, spec_json):
try:
if spec_json:
with open(spec_json, encoding="utf-8") as f:
return spec_from_json(json.load(f))
return mcp_view(spec_id)
except Fail as e:
if e.code == 4:
raise Fail(4, e.msg + "; --spec-json reads a saved view")
raise
except (OSError, ValueError):
raise Fail(4, "--spec-json: unreadable; --spec-json reads a saved view")
# ---- carrying an anchor --------------------------------------------------
def carry(s, e, hunks):
"""Map old lines s..e through -U0 hunks (a,b,c,d). Returns (ns, ne, ins, dele, touched)."""
shift = net_in = ins = dele = 0
touched = False
lo = hi = None
for a, b, c, d in hunks:
if b == 0: # insertion after old line a
if a < s:
shift += d
continue
if a >= e: # at the last line or after: outside
continue
touched = True
ins += d
net_in += d
else:
last = a + b - 1
if last < s:
shift += d - b
continue
if a > e:
continue
touched = True
ins += d
dele += b
net_in += d - b
if a < s: # the first surviving line: c, or c+1 after a pure deletion
lo = c if d > 0 else c + 1
if last > e: # the last surviving line: c+d-1, or c after a pure deletion
hi = c + d - 1 if d > 0 else c
ns, ne = s + shift, e + shift + net_in
if lo is not None:
ns = min(ns, lo)
if hi is not None:
ne = max(ne, hi)
return ns, max(ne, ns), ins, dele, touched
class Tree:
def __init__(self, wt, base):
self.wt, self.base = wt, base
self.changes = {}
self._hunks, self._basefile, self._wtfile = {}, {}, {}
def load_changes(self):
out = git(self.wt, "diff", "-M", "--name-status", "-z", "--no-color", "--no-ext-diff", self.base)
if out is None:
return False
parts = out.decode("utf-8", "replace").split("\0")
i = 0
while i < len(parts) and parts[i]:
code = parts[i]
if code[0] in "RC" and i + 2 < len(parts):
self.changes[parts[i + 1]] = (code[0], parts[i + 2])
i += 3
else:
self.changes[parts[i + 1]] = (code[0], None)
i += 2
return True
def hunks(self, since, *paths):
key = (since, paths)
if key not in self._hunks:
out = git(self.wt, "diff", "-M", "-U0", "--no-color", "--no-ext-diff", since, "--", *paths)
hs = None
if out is not None:
hs = []
for ln in out.decode("utf-8", "replace").split("\n"):
m = HUNK.match(ln)
if m:
a, b, c, d = m.groups()
hs.append((int(a), 1 if b is None else int(b), int(c), 1 if d is None else int(d)))
self._hunks[key] = hs
return self._hunks[key]
def base_lines(self, path):
"""None when the path is absent at base."""
if path not in self._basefile:
raw = git(self.wt, "show", self.base + ":" + path)
self._basefile[path] = None if raw is None else text_lines(raw)
return self._basefile[path]
def wt_lines(self, path):
if path not in self._wtfile:
try:
with open(os.path.join(self.wt, *path.split("/")), "rb") as f:
self._wtfile[path] = text_lines(f.read())
except OSError:
self._wtfile[path] = None
return self._wtfile[path]
def safe_path(path):
return bool(path) and not path.startswith(("/", "\\")) and ":" not in path \
and ".." not in path.replace("\\", "/").split("/")
def resolve(tree, k, fc, deleted_ok, base_ok, listed):
loc = fc["location"]
path, s = loc["path"], int(loc["line"])
e = int(loc.get("end_line") or s)
a = {"k": k, "path": path, "s": s, "e": e, "change": str(fc.get("change", "")),
"rank": 1, "exit": 0, "status": "", "now": None, "window": None}
if not base_ok:
a.update(rank=0, exit=3, status="UNRESOLVED base not a commit here: git fetch origin " + tree.base)
return a
if not listed or not safe_path(path):
a.update(rank=0, exit=3, status="UNRESOLVED path or git diff unusable")
return a
base = tree.base_lines(path)
if base is None:
sib = [p for p in tree.siblings() if p.rsplit("/", 1)[-1] == path.rsplit("/", 1)[-1]][:3]
a.update(rank=0, status="NEW" + (" (at base: " + ", ".join(sib) + ")" if sib else ""))
a["now"] = (path, s, e)
if tree.wt_lines(path) is not None:
a["window"] = ("wt", path, s, e)
return a
code, newpath = tree.changes.get(path, ("", None))
if code == "D":
ok = path in deleted_ok
a.update(rank=0, exit=0 if ok else 3, status="GONE" + ("" if ok else " (not named by the spec's deletes)"))
a["window"] = ("base", path, s, e)
return a
if s > len(base) or e > len(base):
a.update(rank=0, exit=3, status="OUT_OF_RANGE (base file has %d lines)" % len(base))
return a
now = newpath or path
hs = tree.hunks(tree.base, *dict.fromkeys([path, now]))
if hs is None:
a.update(rank=0, exit=3, status="UNRESOLVED git diff failed")
return a
ns, ne, ins, dele, touched = carry(s, e, hs)
if touched:
st = "TOUCHED(+%d/-%d inside)" % (ins, dele)
elif ns != s:
st = "MOVED(%+d)" % (ns - s)
else:
st = "SAME"
if newpath:
st = "RENAMED -> " + newpath + " " + st
a.update(status=st, now=(now, ns, ne))
a["window"] = ("wt", now, ns, ne)
return a
def table_line(a):
line = "[%d] %s:%d-%d %s" % (a["k"], a["path"], a["s"], a["e"], a["status"])
if a["now"]:
line += " -> %s:%d-%d" % a["now"]
return line + " # " + " ".join(a["change"].split())[:70]
def window_body(tree, a):
kind, path, s, e = a["window"]
lines = tree.base_lines(path) if kind == "base" else tree.wt_lines(path)
cap = "== [%d] %s:%d-%d%s %s" % (a["k"], path, s, e, " AT BASE" if kind == "base" else "", a["status"])
if lines is None:
return cap, ["(file not readable in the worktree)"]
lo, hi = max(1, s - CONTEXT), min(len(lines), e + CONTEXT)
body = ["%6d %s" % (n, lines[n - 1]) for n in range(lo, hi + 1)]
return cap, body or ["(no lines: file has %d lines)" % len(lines)]
def overlaps_since(tree, since, a):
kind, path, s, e = a["window"]
if kind != "wt":
return False
hs = tree.hunks(since, path)
if hs is None:
return False
for _, _, c, d in hs:
if d == 0:
if s - 1 <= c <= e:
return True
elif c <= e and c + d - 1 >= s:
return True
return False
def main(argv):
p = Parser(prog="context-pack.py")
p.add_argument("spec_id")
p.add_argument("worktree")
p.add_argument("--since")
p.add_argument("--only")
p.add_argument("--spec-json")
p.add_argument("--max-lines", type=int, default=DEFAULT_MAX_LINES)
ns = p.parse_args(argv)
if ns.max_lines < 1:
p.error("")
only = None
if ns.only is not None:
try:
only = {int(x) for x in ns.only.split(",")}
except ValueError:
p.error("")
wt = ns.worktree
if git(wt, "rev-parse", "--git-dir") is None:
raise Fail(2, "worktree: not a git worktree")
spec = load_spec(ns.spec_id, ns.spec_json)
fcs = spec["file_changes"]
if only is not None and not only <= set(range(1, len(fcs) + 1)):
raise Fail(2, "--only: index outside file_changes")
base = str(spec["base"])
resolved = HEX.match(base) and git(wt, "rev-parse", "--verify", "--quiet", base + "^{commit}")
tree = Tree(wt, base)
listed = bool(resolved) and tree.load_changes()
tree.siblings = lambda: [x for x in (git(wt, "ls-tree", "-r", "--name-only", base) or b"")
.decode("utf-8", "replace").split("\n") if x]
if ns.since is not None and (ns.since.startswith("-") or
git(wt, "rev-parse", "--verify", "--quiet", ns.since + "^{commit}") is None):
raise Fail(2, "--since: not a commit in this worktree")
deleted_ok = {d["path"] for d in spec.get("deletes", [])}
anchors = [resolve(tree, i + 1, fc, deleted_ok, bool(resolved), listed) for i, fc in enumerate(fcs)]
head = (git(wt, "rev-parse", "--short", "HEAD") or b"none").decode().strip()
out = ["context-pack %s base %s HEAD %s modified %d" % (
ns.spec_id, base[:12] if HEX.match(base) else "unresolvable", head, len(tree.changes))]
out += [table_line(a) for a in sorted(anchors, key=lambda a: a["rank"])]
out.append("")
want = [a for a in anchors if a["window"] and (only is None or a["k"] in only)
and (ns.since is None or overlaps_since(tree, ns.since, a))]
table_len = len(fcs)
skipped = []
left = ns.max_lines - len(out)
for idx, a in enumerate(want):
cap, body = window_body(tree, a)
full = len(body)
if only is None and full > WINDOW_CAP:
body = body[:WINDOW_CAP]
reserve = 1 if idx < len(want) - 1 or skipped else 0 # room for the not-expanded line
need = 1 + len(body) + (1 if len(body) < full else 0)
if only is not None and need > left - reserve and left - reserve >= 4:
body = body[:left - reserve - 2]
need = 1 + len(body) + 1
if need > left - reserve:
skipped.append(a["k"])
continue
out.append(cap)
out += body
if len(body) < full:
out.append("%d more lines: --only %d --max-lines %d" % (full - len(body), a["k"], table_len + full + 3))
left -= need
if skipped:
out.append("not expanded: --only " + ",".join(map(str, skipped)))
sys.stdout.reconfigure(encoding="utf-8", errors="replace", newline="\n")
sys.stdout.write("\n".join(out) + "\n")
return max([a["exit"] for a in anchors] or [0])
if __name__ == "__main__":
try:
sys.exit(main(sys.argv[1:]))
except Fail as f:
sys.stderr.write("context-pack: %s\n" % f.msg)
sys.exit(f.code)

View file

@ -0,0 +1,42 @@
#!/usr/bin/env python3
"""A stand-in for eureka-state's stdio MCP: answers initialize and tools/call view.
FAKE_SPEC: the view result to return. FAKE_MODE=unavailable answers isError
Unavailable (naming HUGINN_ENDPOINT, as the real daemon's text might). FAKE_COUNT:
a file that gains one line per view call. FAKE_MODE=needenv answers the spec only when
EUREKA_INSTANCE is "t" and HUGINN_ENDPOINT equals FAKE_NEED, else isError Misconfigured."""
import json, os, sys
inited = False
for ln in sys.stdin:
msg = json.loads(ln)
if "id" not in msg:
inited = inited or msg.get("method") == "notifications/initialized"
continue
if msg["method"] == "initialize":
p = msg.get("params", {})
if msg["id"] != 1 or not p.get("protocolVersion") or not p.get("clientInfo", {}).get("name") or "capabilities" not in p:
sys.exit(1)
res = {"protocolVersion": "2024-11-05", "capabilities": {}, "serverInfo": {"name": "fake", "version": "0"}}
else:
p = msg.get("params", {})
if (not inited or msg["method"] != "tools/call" or p.get("name") != "view" or msg["id"] != 2
or p.get("arguments") != {"id": {"kind": "CutSpec", "id": os.environ["FAKE_WANT"]}}):
sys.exit(1)
with open(os.environ["FAKE_COUNT"], "a") as f:
f.write("view\n")
if os.environ.get("FAKE_MODE") == "null":
res = {"content": [{"type": "text", "text": "null"}]}
elif os.environ.get("FAKE_MODE") == "garbage":
res = {"content": [{"type": "text", "text": "not json"}]}
elif os.environ.get("FAKE_MODE") == "invalid":
res = {"isError": True, "content": [{"type": "text", "text": os.environ.get("FAKE_CODE", "InvalidInput") + ": " + os.environ.get("HUGINN_ENDPOINT", "")}]}
elif os.environ.get("FAKE_MODE") == "needenv" and not (
os.environ.get("EUREKA_INSTANCE") == "t" and os.environ.get("HUGINN_ENDPOINT") == os.environ["FAKE_NEED"]):
res = {"isError": True, "content": [{"type": "text", "text": "Misconfigured"}]}
elif os.environ.get("FAKE_MODE") == "unavailable":
res = {"isError": True, "content": [{"type": "text",
"text": "Unavailable: " + os.environ.get("HUGINN_ENDPOINT", "")}]}
else:
res = {"content": [{"type": "text", "text": open(os.environ["FAKE_SPEC"], encoding="utf-8").read()}]}
sys.stdout.write(json.dumps({"jsonrpc": "2.0", "id": msg["id"], "result": res}) + "\n")
sys.stdout.flush()

View file

@ -0,0 +1,30 @@
{"id": {"id": "t:cut_spec:cut-x.r1", "kind": "CutSpec"},
"status": "InForce",
"document": {"kind": "cut_spec", "value": {
"base": "BASE", "deletes": [],
"file_changes": [
{"change": "moved below the insertion", "location": {"path": "a.txt", "line": 20, "end_line": 22}},
{"change": "insertion after the last line is outside", "location": {"path": "a.txt", "line": 3, "end_line": 5}},
{"change": "insertion just before the range", "location": {"path": "a.txt", "line": 6, "end_line": 7}},
{"change": "insertion inside the range", "location": {"path": "a.txt", "line": 4, "end_line": 8}},
{"change": "delete above and edit inside", "location": {"path": "b.txt", "line": 8, "end_line": 12}},
{"change": "moved up by a deletion", "location": {"path": "b.txt", "line": 30, "end_line": 31}},
{"change": "deleted file", "location": {"path": "c.txt", "line": 2, "end_line": 3}},
{"change": "renamed file", "location": {"path": "d.txt", "line": 4, "end_line": 5}},
{"change": "path absent at base", "location": {"path": "sub/a.txt", "line": 1, "end_line": 2}},
{"change": "range past EOF", "location": {"path": "e.txt", "line": 5, "end_line": 50}},
{"change": "uncommitted edit", "location": {"path": "f.txt", "line": 5, "end_line": 6}},
{"change": "last line is in range", "location": {"path": "e.txt", "line": 10, "end_line": 10}},
{"change": "one past the last line", "location": {"path": "e.txt", "line": 10, "end_line": 11}},
{"change": "CRLF file", "location": {"path": "h.txt", "line": 2, "end_line": 3}},
{"change": "straddles the head of a replaced block", "location": {"path": "m.txt", "line": 12, "end_line": 13}},
{"change": "replaced block starts inside", "location": {"path": "m.txt", "line": 8, "end_line": 11}},
{"change": "just after a replaced block", "location": {"path": "m.txt", "line": 15, "end_line": 16}},
{"change": "deletion follows the range", "location": {"path": "m.txt", "line": 28, "end_line": 28}},
{"change": "deletion at the next line", "location": {"path": "m.txt", "line": 29, "end_line": 29}},
{"change": "deletion ends just before", "location": {"path": "m.txt", "line": 32, "end_line": 32}},
{"change": "after the deletion", "location": {"path": "m.txt", "line": 33, "end_line": 33}},
{"change": "insertion at the first line", "location": {"path": "a.txt", "line": 5, "end_line": 7}},
{"change": "deletion ends just before the range", "location": {"path": "b.txt", "line": 4, "end_line": 5}},
{"change": "untracked file the cut creates", "location": {"path": "n.txt", "line": 1, "end_line": 2}}
]}}}

View file

@ -225,4 +225,292 @@ r=$(WM=2 wo log "$T/l.log" '(unclosed'); wchk "invalid regex" 3 "$r"
grep -q 'not a valid extended regex' "$T/wo.out" || { echo "FAIL wait-on: no regex message"; fail=1; }
r=$(wo bogus); wchk "bad usage" 2 "$r"
wait
# context-pack.py: a spec's anchors carried from its base to the worktree (no network)
CP=$here/context-pack.py; CF=$here/fixtures/context-pack
if py -3 -c pass >/dev/null 2>&1; then PY="py -3"; elif python3 -c pass >/dev/null 2>&1; then PY=python3; else PY=python; fi
wp() { cygpath -m "$1" 2>/dev/null || echo "$1"; }
R=$T/cp; mkdir -p "$R"; Rw=$(wp "$R")
g() { git -C "$R" -c user.name=t -c user.email=t@t -c core.autocrlf=false "$@" >/dev/null 2>&1; }
seqf() { local i; for i in $(seq 1 "$2"); do echo "$1$i"; done; }
g init -q
seqf a 40 >"$R/a.txt"; seqf b 40 >"$R/b.txt"; seqf c 10 >"$R/c.txt"; seqf d 20 >"$R/d.txt"
seqf e 10 >"$R/e.txt"; seqf f 20 >"$R/f.txt"; seqf g 300 >"$R/big.txt"; seqf h 5 | sed 's/$/ /' >"$R/h.txt"; seqf m 40 >"$R/m.txt"
g add -A; g commit -q -m base; BASE=$(git -C "$R" rev-parse HEAD)
{ sed -n 1,5p "$R/a.txt"; printf 'ins1\nins2\nins3\n'; sed -n '6,$p' "$R/a.txt"; } >"$R/a.new"; mv "$R/a.new" "$R/a.txt"
sed -e '2,3d' -e 's/^b10$/b10-EDITED/' "$R/b.txt" >"$R/b.new"; mv "$R/b.new" "$R/b.txt"
sed -e '10,14c\X1\nX2' -e '30,31d' "$R/m.txt" >"$R/m.new"; mv "$R/m.new" "$R/m.txt"
g rm -q c.txt; g mv d.txt d2.txt; g add -A; g commit -q -m second
sed -i 's/^f5$/f5-UNCOMMITTED/' "$R/f.txt"; printf 'n1\nn2\nn3\n' >"$R/n.txt"
sed "s/\"BASE\"/\"$BASE\"/" "$CF/spec.json" >"$T/cp-spec.json"; SP=$(wp "$T/cp-spec.json")
cpo() { $PY "$CP" t:cut_spec:cut-x.r1 "$Rw" "$@" >"$T/cp.out" 2>"$T/cp.err"; echo $?; }
cchk() { if [ "$3" = "$2" ]; then echo "ok context-pack $1: exit $3"; else echo "FAIL context-pack $1: exit $3, want $2"; fail=1; fi; }
cin() { grep -qF -- "$2" "$T/cp.out" && echo "ok context-pack $1" || { echo "FAIL context-pack $1: missing: $2"; fail=1; }; }
cnot() { if grep -qF -- "$2" "$T/cp.out" "$T/cp.err"; then echo "FAIL context-pack $1: must not contain: $2"; fail=1; else echo "ok context-pack $1"; fi; }
r=$(cpo --spec-json "$SP"); cchk "fixture (GONE and OUT_OF_RANGE)" 3 "$r"
cin "header" "context-pack t:cut_spec:cut-x.r1 base ${BASE:0:12} HEAD"
cin "header counts modified files" " modified 6"
cin "MOVED(+3)" "[1] a.txt:20-22 MOVED(+3) -> a.txt:23-25"
cin "insertion at the last line is outside" "[2] a.txt:3-5 SAME -> a.txt:3-5"
cin "insertion before the range shifts it" "[3] a.txt:6-7 MOVED(+3) -> a.txt:9-10"
cin "insertion inside widens" "[4] a.txt:4-8 TOUCHED(+3/-0 inside) -> a.txt:4-11"
cin "delete above, edit inside" "[5] b.txt:8-12 TOUCHED(+1/-1 inside) -> b.txt:6-10"
cin "MOVED(-2)" "[6] b.txt:30-31 MOVED(-2) -> b.txt:28-29"
cin "GONE" "[7] c.txt:2-3 GONE (not named by the spec's deletes)"
cin "RENAMED" "[8] d.txt:4-5 RENAMED -> d2.txt SAME -> d2.txt:4-5"
cin "NEW with basename candidates" "[9] sub/a.txt:1-2 NEW (at base: a.txt)"
cin "OUT_OF_RANGE" "[10] e.txt:5-50 OUT_OF_RANGE (base file has 10 lines)"
cin "uncommitted edit is TOUCHED" "[11] f.txt:5-6 TOUCHED(+1/-1 inside)"
cin "last line of the file is in range" "[12] e.txt:10-10 SAME"
cin "one past the last line is out of range" "[13] e.txt:10-11 OUT_OF_RANGE (base file has 10 lines)"
cin "CRLF file reads SAME" "[14] h.txt:2-3 SAME"
cin "hunk straddling the head clamps to the new block" "[15] m.txt:12-13 TOUCHED(+2/-5 inside) -> m.txt:10-11"
cin "hunk starting inside and ending past the range" "[16] m.txt:8-11 TOUCHED(+2/-5 inside) -> m.txt:8-11"
cin "range just after a replaced block shifts" "[17] m.txt:15-16 MOVED(-3) -> m.txt:12-13"
cin "deletion after the range moves nothing else" "[18] m.txt:28-28 MOVED(-3) -> m.txt:25-25"
cin "deletion at the next line" "[19] m.txt:29-29 MOVED(-3) -> m.txt:26-26"
cin "deletion ending just before the range" "[20] m.txt:32-32 MOVED(-5) -> m.txt:27-27"
cin "range after both hunks" "[21] m.txt:33-33 MOVED(-5) -> m.txt:28-28"
cin "insertion at the first line is inside" "[22] a.txt:5-7 TOUCHED(+3/-0 inside) -> a.txt:5-10"
cin "hunk ending just before the range shifts it" "[23] b.txt:4-5 MOVED(-2) -> b.txt:2-3"
cin "NEW file present in the worktree gets a window" "== [24] n.txt:1-2"
cin "NEW window shows the file" " 2 n2"
cin "CRLF window text" " 2 h2"
{ tr -d ' ' <"$T/cp.out" | cmp -s - "$T/cp.out"; } && echo "ok context-pack CRLF is stripped from windows" || { echo "FAIL context-pack CR in output"; fail=1; }
cin "window numbers are worktree lines" " 6 b8"
cin "window shows the edit" "b10-EDITED"
cin "GONE window is labelled AT BASE" "== [7] c.txt:2-3 AT BASE"
cin "GONE window shows base text" " 2 c2"
cin "renamed window reads the new path" "== [8] d2.txt:4-5"
[ "$(grep -c '^\[' "$T/cp.out")" = 24 ] && echo "ok context-pack table lists every anchor" || { echo "FAIL context-pack table count"; fail=1; }
{ sed -n 2p "$T/cp.out" | grep -q '^\[7\]' && sed -n 6p "$T/cp.out" | grep -q '^\[24\]' && sed -n 7p "$T/cp.out" | grep -q '^\[1\]'; } && echo "ok context-pack problem anchors first" || { echo "FAIL context-pack problem anchors first"; fail=1; }
# deletes naming the file, and a valid range, make the same spec exit 0
sed -e 's/"deletes": \[\]/"deletes": [{"path": "c.txt"}]/' -e 's/"end_line": 50/"end_line": 6/' -e 's/"end_line": 11/"end_line": 10/' "$T/cp-spec.json" >"$T/cp-spec0.json"
r=$(cpo --spec-json "$(wp "$T/cp-spec0.json")"); cchk "GONE named by deletes, no OUT_OF_RANGE" 0 "$r"
cin "GONE in deletes has no qualifier" "[7] c.txt:2-3 GONE #"
# unreachable base
sed "s/\"$BASE\"/\"deadbeef\"/" "$T/cp-spec.json" >"$T/cp-specb.json"
r=$(cpo --spec-json "$(wp "$T/cp-specb.json")"); cchk "unreachable base" 3 "$r"
cin "UNRESOLVED hint" "UNRESOLVED base not a commit here: git fetch origin deadbeef"
[ "$(grep -c 'UNRESOLVED' "$T/cp.out")" = 24 ] && echo "ok context-pack every anchor UNRESOLVED" || { echo "FAIL context-pack UNRESOLVED count"; fail=1; }
cnot "UNRESOLVED prints no window" "== ["
# --since: every anchor stays in the table; only the edited one has a window
r=$(cpo --spec-json "$SP" --since HEAD); cchk "--since" 3 "$r"
cin "--since keeps the table" "[1] a.txt:20-22 MOVED(+3)"
cin "--since prints the edited window" "== [11] f.txt:5-6"
cin "--since window shows the edit" "f5-UNCOMMITTED"
cnot "--since omits untouched windows" "== [1] "
cnot "--since omits the GONE window" "== [7] "
r=$(cpo --spec-json "$SP" --since "$BASE"); cin "--since an older commit widens" "== [4] a.txt:4-11"
cnot "--since an older commit still omits untouched windows" "== [2] "
r=$(cpo --spec-json "$SP" --since "$BASE" --only 15,16,17,18,19,20,21)
for k in 15 16 19 20; do cin "--since window $k overlaps a hunk" "== [$k] "; done
for k in 17 18 21; do cnot "--since window $k does not overlap" "== [$k] "; done
r=$(cpo --spec-json "$SP" --only 2,11); cin "--only prints the named window" "== [2] "
cnot "--only omits the others" "== [1] "
r=$(cpo --spec-json "$SP" --only 99); cchk "--only outside file_changes" 2 "$r"; grep -qF "context-pack: --only: index outside file_changes" "$T/cp.err" && echo "ok context-pack names --only" || { echo "FAIL context-pack names --only"; fail=1; }
r=$(cpo --spec-json "$SP" --since feedface); cchk "--since not a commit" 2 "$r"; grep -qF "context-pack: --since: not a commit in this worktree" "$T/cp.err" && echo "ok context-pack names --since" || { echo "FAIL context-pack names --since"; fail=1; }
Z70=$(printf 'z%.0s' $(seq 1 70))
echo '{"base": "'$BASE'", "deletes": [], "file_changes": [
{"change": "up", "location": {"path": "../a.txt", "line": 1, "end_line": 1}},
{"change": "abs", "location": {"path": "/a.txt", "line": 1, "end_line": 1}},
{"change": "drive", "location": {"path": "c:a.txt", "line": 1, "end_line": 1}},
{"change": "'$Z70'zzzz", "location": {"path": "a.txt", "line": 1, "end_line": 1}}]}' >"$T/cp-specu.json"
r=$(cpo --spec-json "$(wp "$T/cp-specu.json")"); cchk "unsafe paths" 3 "$r"
[ "$(grep -c 'UNRESOLVED path or git diff unusable' "$T/cp.out")" = 3 ] && echo "ok context-pack unsafe paths are UNRESOLVED" || { echo "FAIL context-pack unsafe paths"; fail=1; }
cin "a long change is cut at 70 characters" "# $Z70"
cnot "a long change is cut at 70 characters (not 71)" "${Z70}z"
# caps
{ echo '{"base": "'$BASE'", "deletes": [], "file_changes": ['
for i in $(seq 1 30); do echo '{"change": "c'$i'", "location": {"path": "big.txt", "line": '$((i*5))', "end_line": '$((i*5+9))'}}'$([ $i -lt 30 ] && echo ,); done
echo ']}'; } >"$T/cp-spec30.json"
r=$(cpo --spec-json "$(wp "$T/cp-spec30.json")" --max-lines 200); cchk "30 anchors" 0 "$r"
[ "$(wc -l <"$T/cp.out")" -le 200 ] && echo "ok context-pack output within --max-lines" || { echo "FAIL context-pack over 200 lines"; fail=1; }
[ "$(grep -c '^\[' "$T/cp.out")" = 30 ] && echo "ok context-pack capped output keeps the full table" || { echo "FAIL context-pack capped table"; fail=1; }
[ "$(grep -c '^== \[' "$T/cp.out")" = 9 ] && echo "ok context-pack expands 9 windows of 17 lines" || { echo "FAIL context-pack window count: $(grep -c '^== \[' "$T/cp.out")"; fail=1; }
cin "rest listed as not expanded" "not expanded: --only 10,11,12"
echo '{"base": "'$BASE'", "deletes": [], "file_changes": [{"change": "long", "location": {"path": "big.txt", "line": 20, "end_line": 169}}]}' >"$T/cp-spec1.json"
S1=$(wp "$T/cp-spec1.json")
r=$(cpo --spec-json "$S1"); cchk "150-line range" 0 "$r"
cin "window cut at 100 lines" "56 more lines: --only 1 --max-lines 160"
[ "$(grep -c '^ *[0-9]* g' "$T/cp.out")" = 100 ] && echo "ok context-pack long window prints 100 lines" || { echo "FAIL context-pack long window"; fail=1; }
r=$(cpo --spec-json "$S1" --only 1 --max-lines 160); cchk "--only with the suggested --max-lines" 0 "$r"
cnot "full window needs no more-lines note" "more lines"
[ "$(wc -l <"$T/cp.out" | tr -d ' ')" = 160 ] && echo "ok context-pack suggested --max-lines is exact" || { echo "FAIL context-pack suggested max: $(wc -l <"$T/cp.out")"; fail=1; }
# usage and unreadable spec; no error echoes an input value
r=$(cpo --spec-json "$SP" --max-lines CANARY-usage); cchk "bad --max-lines" 2 "$r"; cnot "usage error echoes no value" "CANARY-usage"
r=$(cpo --spec-json "$T/CANARY-nofile.json"); cchk "unreadable --spec-json" 4 "$r"; grep -qF "context-pack: --spec-json: unreadable; --spec-json reads a saved view" "$T/cp.err" && echo "ok context-pack names the unreadable --spec-json" || { echo "FAIL context-pack names the unreadable --spec-json"; fail=1; }; cnot "unreadable --spec-json echoes no path" "CANARY-nofile"
grep -qF -- "--spec-json" "$T/cp.err" && echo "ok context-pack names --spec-json" || { echo "FAIL context-pack error does not name --spec-json"; fail=1; }
r=$($PY "$CP" CANARY-id "$Rw" >"$T/cp.out" 2>"$T/cp.err"; echo $?); cchk "spec id that is no cut_spec id" 2 "$r"; grep -qF "context-pack: spec id: not a cut_spec id" "$T/cp.err" && echo "ok context-pack names the spec id field" || { echo "FAIL context-pack names the spec id field"; fail=1; }; cnot "bad id echoes no value" "CANARY-id"
r=$($PY "$CP" t:cut_spec:cut-x.r1 "$T/CANARY-notrepo" --spec-json "$SP" >"$T/cp.out" 2>"$T/cp.err"; echo $?); cchk "worktree is not a repo" 2 "$r"; grep -qF "context-pack: worktree: not a git worktree" "$T/cp.err" && echo "ok context-pack names the worktree field" || { echo "FAIL context-pack names the worktree field"; fail=1; }; cnot "worktree error echoes no path" "CANARY-notrepo"
# the spec by id through the stdio MCP binding (a fake eureka-state), retried once on Unavailable
H=$T/cphome; mkdir -p "$H"; FK=$(wp "$CF/fake-eureka-state.py")
$PY - "$(wp "$H")/.claude.json" "$($PY -c 'import sys;print(sys.executable)')" "$FK" <<'PYE'
import json, sys
json.dump({"mcpServers": {"eureka-state": {"type": "stdio", "command": sys.argv[2], "args": [sys.argv[3]],
"env": {"EUREKA_INSTANCE": "t", "HUGINN_ENDPOINT": "rudp://binding"}}}}, open(sys.argv[1], "w"))
PYE
Hw=$(wp "$H")
mcp() { HOME="$Hw" USERPROFILE="$Hw" FAKE_WANT=t:cut_spec:cut-x.r1 FAKE_SPEC="$SP" FAKE_COUNT="$(wp "$T/cp.count")" "$@"; }
$PY "$CP" t:cut_spec:cut-x.r1 "$Rw" --spec-json "$SP" >"$T/cp.want" 2>/dev/null
: >"$T/cp.count"
mcp $PY "$CP" t:cut_spec:cut-x.r1 "$Rw" >"$T/cp.out" 2>"$T/cp.err"; r=$?
cchk "spec by id through the binding" 3 "$r"
cmp -s "$T/cp.out" "$T/cp.want" && echo "ok context-pack by id equals --spec-json" || { echo "FAIL context-pack by id differs from --spec-json"; fail=1; }
: >"$T/cp.count"
mcp env FAKE_MODE=unavailable HUGINN_ENDPOINT=rudp://CANARY-endpoint $PY "$CP" t:cut_spec:cut-x.r1 "$Rw" >"$T/cp.out" 2>"$T/cp.err"; r=$?
cchk "Unavailable twice" 4 "$r"; cnot "Unavailable echoes no endpoint" "CANARY-endpoint"
[ "$(wc -l <"$T/cp.count" | tr -d ' ')" = 2 ] && echo "ok context-pack retried Unavailable exactly once" || { echo "FAIL context-pack view attempts: $(wc -l <"$T/cp.count")"; fail=1; }
grep -qF "context-pack: view: Unavailable; --spec-json reads a saved view" "$T/cp.err" && echo "ok context-pack Unavailable names --spec-json" || { echo "FAIL context-pack Unavailable hint"; fail=1; }
mcp env EUREKA_STATE_EXE="$T/CANARY-exe" $PY "$CP" t:cut_spec:cut-x.r1 "$Rw" >"$T/cp.out" 2>"$T/cp.err"; r=$?
cchk "unspawnable EUREKA_STATE_EXE" 4 "$r"; grep -qF "context-pack: transport: Unavailable; --spec-json reads a saved view" "$T/cp.err" && echo "ok context-pack names the transport failure" || { echo "FAIL context-pack names the transport failure"; fail=1; }; cnot "spawn error echoes no path" "CANARY-exe"
: >"$T/cp.count"
mcp env FAKE_MODE=invalid HUGINN_ENDPOINT=rudp://CANARY-endpoint2 $PY "$CP" t:cut_spec:cut-x.r1 "$Rw" >"$T/cp.out" 2>"$T/cp.err"; r=$?
cchk "a refusal that is not Unavailable" 4 "$r"; [ "$(wc -l <"$T/cp.count" | tr -d " ")" = 1 ] && echo "ok context-pack does not retry a refusal" || { echo "FAIL context-pack retried a refusal"; fail=1; }; cnot "refusal echoes no endpoint" "CANARY-endpoint2"
grep -qF "context-pack: view: InvalidInput; --spec-json reads a saved view" "$T/cp.err" && echo "ok context-pack names the refusal code" || { echo "FAIL context-pack refusal code"; fail=1; }
for code in Rejected Misconfigured TooLarge Unencodable Internal; do
mcp env FAKE_MODE=invalid FAKE_CODE=$code $PY "$CP" t:cut_spec:cut-x.r1 "$Rw" >"$T/cp.out" 2>"$T/cp.err"; r=$?
grep -qF "context-pack: view: $code; --spec-json reads a saved view" "$T/cp.err" && [ $r = 4 ] && echo "ok context-pack names the code $code" || { echo "FAIL context-pack code $code"; fail=1; }
done
mcp env FAKE_MODE=garbage $PY "$CP" t:cut_spec:cut-x.r1 "$Rw" >"$T/cp.out" 2>"$T/cp.err"; r=$?
cchk "unparsable answer" 4 "$r"; grep -qF "context-pack: view: unparsable answer; --spec-json reads a saved view" "$T/cp.err" && echo "ok context-pack names the unparsable answer" || { echo "FAIL context-pack unparsable message"; fail=1; }
mcp env FAKE_MODE=null $PY "$CP" t:cut_spec:cut-x.r1 "$Rw" >"$T/cp.out" 2>"$T/cp.err"; r=$?
cchk "no such cut_spec" 4 "$r"; grep -qF "context-pack: view: no such cut_spec; --spec-json reads a saved view" "$T/cp.err" && echo "ok context-pack names the missing spec" || { echo "FAIL context-pack missing spec message"; fail=1; }
mcp env HOME="$(wp "$T")/nohome" USERPROFILE="$(wp "$T")/nohome" $PY "$CP" t:cut_spec:cut-x.r1 "$Rw" >"$T/cp.out" 2>"$T/cp.err"; r=$?
cchk "no binding and no EUREKA_STATE_EXE" 4 "$r"; grep -qF "context-pack: binding: eureka-state is not configured; --spec-json reads a saved view" "$T/cp.err" && echo "ok context-pack names the missing binding" || { echo "FAIL context-pack binding message"; fail=1; }
echo '{}' >"$T/cp-empty.json"; r=$(cpo --spec-json "$(wp "$T/cp-empty.json")"); cchk "spec-json that is no cut_spec" 4 "$r"
grep -qF "context-pack: spec: not a cut_spec value; --spec-json reads a saved view" "$T/cp.err" && echo "ok context-pack names the spec shape" || { echo "FAIL context-pack spec shape message"; fail=1; }
grep -qF "usage: " "$T/cp.err" || echo "ok context-pack spec error prints no usage text"
r=$(cpo --max-lines 0); cchk "usage: no spec source" 2 "$r"
grep -qF "context-pack: usage error" "$T/cp.err" && echo "ok context-pack usage error text" || { echo "FAIL context-pack usage text"; fail=1; }
# glob characters in a spec path name that file only; straddling hunks; --since boundaries
R2=$T/cp2; mkdir -p "$R2/src/[a]" "$R2/src/a"; R2w=$(wp "$R2")
g2() { git -C "$R2" -c user.name=t -c user.email=t@t -c core.autocrlf=false "$@" >/dev/null 2>&1; }
g2 init -q
seqf x 30 >"$R2/src/[a]/f.txt"; seqf x 30 >"$R2/src/a/f.txt"
for n in s t u v; do seqf x 30 >"$R2/$n.txt"; done; seqf w 12 >"$R2/w.txt"; seqf g 200 >"$R2/big.txt"; echo q1 >"$R2/one.txt"
g2 add -A; g2 commit -q -m base; B2=$(git -C "$R2" rev-parse HEAD)
{ printf 'new1\nnew2\n'; cat "$R2/src/a/f.txt"; } >"$R2/src/a/f.new"; mv "$R2/src/a/f.new" "$R2/src/a/f.txt"
sed -i 8,12d "$R2/s.txt"; sed -i 15,25d "$R2/t.txt"
sed -i '8,12c\NEW' "$R2/u.txt"; sed -i '15,25c\NEW' "$R2/v.txt"
g2 add -A; g2 commit -q -m second; S2=$(git -C "$R2" rev-parse HEAD)
sed -i 's/^w5$/w5-EDITED/' "$R2/w.txt"
echo '{"base": "'$B2'", "deletes": [], "file_changes": [
{"change": "glob path", "location": {"path": "src/[a]/f.txt", "line": 10, "end_line": 12}},
{"change": "pure deletion above the start", "location": {"path": "s.txt", "line": 10, "end_line": 20}},
{"change": "pure deletion past the end", "location": {"path": "t.txt", "line": 10, "end_line": 20}},
{"change": "one-line replacement above the start", "location": {"path": "u.txt", "line": 10, "end_line": 20}},
{"change": "one-line replacement past the end", "location": {"path": "v.txt", "line": 10, "end_line": 20}},
{"change": "edit on the last line", "location": {"path": "w.txt", "line": 3, "end_line": 5}},
{"change": "edit on the first line", "location": {"path": "w.txt", "line": 5, "end_line": 7}},
{"change": "after the edit", "location": {"path": "w.txt", "line": 6, "end_line": 8}},
{"change": "before the edit", "location": {"path": "w.txt", "line": 1, "end_line": 4}}]}' >"$T/cp-spec2.json"
SP2=$(wp "$T/cp-spec2.json")
cp2() { $PY "$CP" t:cut_spec:cut-x.r1 "$R2w" "$@" >"$T/cp.out" 2>"$T/cp.err"; echo $?; }
r=$(cp2 --spec-json "$SP2"); cchk "straddle fixture" 0 "$r"
cin "a glob path reads its own file: SAME" "[1] src/[a]/f.txt:10-12 SAME -> src/[a]/f.txt:10-12"
cin "pure deletion starting above the anchor starts at the first survivor" "[2] s.txt:10-20 TOUCHED(+0/-5 inside) -> s.txt:8-15"
cin "pure deletion ending past the anchor ends at the last survivor" "[3] t.txt:10-20 TOUCHED(+0/-11 inside) -> t.txt:10-14"
cin "one-line replacement above the start begins at the replacement" "[4] u.txt:10-20 TOUCHED(+1/-5 inside) -> u.txt:8-16"
cin "one-line replacement past the end ends at the replacement" "[5] v.txt:10-20 TOUCHED(+1/-11 inside) -> v.txt:10-15"
cin "straddle window starts at the mapped range" "== [2] s.txt:8-15"
r=$(cp2 --spec-json "$SP2" --since "$S2" --only 6,7,8,9)
cin "--since: an edit on the anchor's last line is inside" "== [6] w.txt:3-5"
cin "--since: an edit on the anchor's first line is inside" "== [7] w.txt:5-7"
cnot "--since: an anchor after the edit has no window" "== [8] "
cnot "--since: an anchor before the edit has no window" "== [9] "
cin "--since keeps every anchor in the table" "[9] w.txt:1-4 SAME"
# a --since commit whose tree is unreadable: git diff fails, so no anchor is claimed edited
BL=$(echo only | git -C "$R2" hash-object -w --stdin); TR=$(printf '100644 blob %s\tonly.txt\n' "$BL" | git -C "$R2" mktree)
CM=$(git -C "$R2" -c user.name=t -c user.email=t@t commit-tree "$TR" -m orphan)
chmod u+w "$R2/.git/objects/${TR:0:2}/${TR:2}" 2>/dev/null; rm -f "$R2/.git/objects/${TR:0:2}/${TR:2}"
r=$(cp2 --spec-json "$SP2" --since "$CM"); cchk "--since with an unreadable tree" 0 "$r"
cnot "--since: a failed git diff claims no edited window" "== ["
cin "--since: a failed git diff keeps the table" "[6] w.txt:3-5"
# a spec value of the wrong type is a typed refusal naming the field, never a traceback or the value
$PY - "$T" "$SP" <<'PYE'
import copy, json, sys
t, sp = sys.argv[1], sys.argv[2]
spec = json.load(open(sp, encoding="utf-8"))
cases = {
"line": (lambda v: v["file_changes"][1]["location"].update(line="CANARY-line"), "file_changes[2].location.line"),
"line-bool": (lambda v: v["file_changes"][0]["location"].update(line=True), "file_changes[1].location.line"),
"end_line": (lambda v: v["file_changes"][2]["location"].update(end_line="CANARY-end"), "file_changes[3].location.end_line"),
"path": (lambda v: v["file_changes"][0]["location"].update(path=["CANARY-path"]), "file_changes[1].location.path"),
"nolocation": (lambda v: v["file_changes"][3].pop("location"), "file_changes[4].location"),
"location-str": (lambda v: v["file_changes"][0].update(location="CANARY-loc"), "file_changes[1].location"),
"entry-str": (lambda v: v["file_changes"].__setitem__(0, "CANARY-str"), "file_changes[1]"),
"change": (lambda v: v["file_changes"][0].update(change={"CANARY": 1}), "file_changes[1].change"),
"fcs-str": (lambda v: v.update(file_changes="CANARY-fcs"), "file_changes"),
"base": (lambda v: v.update(base=12345), "base"),
"deletes-str": (lambda v: v.update(deletes="CANARY-del"), "deletes"),
"deletes-nopath": (lambda v: v.update(deletes=[{"note": "CANARY-note"}]), "deletes[1].path"),
"deletes-entry-str": (lambda v: v.update(deletes=["CANARY-d"]), "deletes[1].path"),
}
names = []
for name, (edit, field) in cases.items():
d = copy.deepcopy(spec)
edit(d["document"]["value"])
json.dump(d, open("%s/cp-bad-%s.json" % (t, name), "w"))
names.append("%s %s" % (name, field))
open(t + "/cp-bad.list", "w", newline=chr(10)).write("\n".join(names) + "\n")
PYE
while read -r nm fld; do
r=$(cpo --spec-json "$(wp "$T/cp-bad-$nm.json")"); cchk "wrong-typed spec: $nm" 4 "$r"
grep -qF "context-pack: spec: $fld: InvalidInput; --spec-json reads a saved view" "$T/cp.err" && echo "ok context-pack names $fld" || { echo "FAIL context-pack message for $nm: $(cat "$T/cp.err")"; fail=1; }
cnot "wrong-typed spec $nm: no value" "CANARY"; cnot "wrong-typed spec $nm: no traceback" "Traceback"
done <"$T/cp-bad.list"
echo '{"base": "CANARY-base", "deletes": [], "file_changes": [{"change": "c", "location": {"path": "a.txt", "line": 1, "end_line": 2}}]}' >"$T/cp-specnb.json"
r=$(cpo --spec-json "$(wp "$T/cp-specnb.json")"); cchk "a base that is no sha" 3 "$r"
cin "a base that is no sha reads UNRESOLVED" "UNRESOLVED base not a commit here"
cin "a base that is no sha is not echoed in the header" "base unresolvable HEAD"
echo '{"file_changes": [{"change": "c", "location": {"path": "a.txt", "line": 1}}]}' >"$T/cp-spec-nobase.json"
r=$(cpo --spec-json "$(wp "$T/cp-spec-nobase.json")"); cchk "a spec value without a base" 4 "$r"
grep -qF "context-pack: spec: not a cut_spec value; --spec-json reads a saved view" "$T/cp.err" && echo "ok context-pack a missing base is a typed refusal" || { echo "FAIL context-pack missing base: $(cat "$T/cp.err")"; fail=1; }
# a --max-lines below the table: the table always lists every anchor and no window prints
r=$(cpo --spec-json "$(wp "$T/cp-spec30.json")" --max-lines 5); cchk "--max-lines below the table" 0 "$r"
[ "$(grep -c '^\[' "$T/cp.out")" = 30 ] && echo "ok context-pack a small budget keeps the full table" || { echo "FAIL context-pack small budget table"; fail=1; }
cnot "a small budget prints no window" "== ["
[ -z "$(sed -n 32p "$T/cp.out")" ] && echo "ok context-pack a blank line follows the table" || { echo "FAIL context-pack blank line after the table"; fail=1; }
cin "a small budget lists every window as not expanded" "not expanded: --only 1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20,21,22,23,24,25,26,27,28,29,30"
# --only truncates a window to the budget and says how to see the rest
r=$(cpo --spec-json "$S1" --only 1 --max-lines 50); cchk "--only truncation" 0 "$r"
[ "$(wc -l <"$T/cp.out" | tr -d ' ')" = 50 ] && echo "ok context-pack --only fills the budget exactly" || { echo "FAIL context-pack --only budget: $(wc -l <"$T/cp.out")"; fail=1; }
[ "$(grep -c '^ *[0-9]* g' "$T/cp.out")" = 45 ] && echo "ok context-pack --only truncated window has 45 lines" || { echo "FAIL context-pack --only window lines"; fail=1; }
cin "--only truncation note counts the rest" "111 more lines: --only 1 --max-lines 160"
r=$(cpo --spec-json "$S1" --only 1 --max-lines 7); cchk "--only at the smallest truncatable budget" 0 "$r"
[ "$(wc -l <"$T/cp.out" | tr -d ' ')" = 7 ] && [ "$(grep -c '^ *[0-9]* g' "$T/cp.out")" = 2 ] && echo "ok context-pack --only budget 7 prints 2 window lines" || { echo "FAIL context-pack --only budget 7"; fail=1; }
r=$(cpo --spec-json "$S1" --only 1 --max-lines 6); cin "--only below the smallest truncation is not expanded" "not expanded: --only 1"
cnot "--only below the smallest truncation prints no window" "== ["
echo '{"base": "'$BASE'", "deletes": [], "file_changes": [
{"change": "long", "location": {"path": "big.txt", "line": 20, "end_line": 169}},
{"change": "short", "location": {"path": "big.txt", "line": 200, "end_line": 210}}]}' >"$T/cp-spec2w.json"
r=$(cpo --spec-json "$(wp "$T/cp-spec2w.json")" --only 1,2 --max-lines 50); cchk "--only two windows" 0 "$r"
[ "$(wc -l <"$T/cp.out" | tr -d ' ')" = 50 ] && echo "ok context-pack --only keeps room for the not-expanded line" || { echo "FAIL context-pack --only reserve: $(wc -l <"$T/cp.out")"; fail=1; }
cin "--only: the window that no longer fits is listed" "not expanded: --only 2"
r=$(cpo --spec-json "$(wp "$T/cp-spec2w.json")" --only 1,2 --max-lines 161); cchk "--only first window needs exactly the remaining budget" 0 "$r"
cin "--only: a window that fills the budget minus the reserve is cut, not skipped" "== [1] big.txt:20-169"
[ "$(wc -l <"$T/cp.out" | tr -d ' ')" = 161 ] && echo "ok context-pack --only cut window keeps the reserve line" || { echo "FAIL context-pack --only exact need: $(wc -l <"$T/cp.out")"; fail=1; }
r=$(cpo --spec-json "$(wp "$T/cp-spec2w.json")" --only 1,2 --max-lines 8); cchk "--only two windows, too small to cut" 0 "$r"
cnot "--only: below four lines after the reserve nothing is cut" "== ["
cin "--only: both are listed as not expanded" "not expanded: --only 1,2"
echo '{"base": "'$B2'", "deletes": [], "file_changes": [
{"change": "long", "location": {"path": "big.txt", "line": 20, "end_line": 169}},
{"change": "tiny", "location": {"path": "one.txt", "line": 1, "end_line": 1}}]}' >"$T/cp-spec3.json"
r=$(cp2 --spec-json "$(wp "$T/cp-spec3.json")" --only 1,2 --max-lines 100); cchk "--only cut window then a tiny one" 0 "$r"
[ "$(wc -l <"$T/cp.out" | tr -d ' ')" = 100 ] && echo "ok context-pack a cut window is charged what it prints" || { echo "FAIL context-pack cut window charge: $(wc -l <"$T/cp.out")"; fail=1; }
cin "a cut window leaves no room for the next one" "not expanded: --only 2"
r=$(cpo --spec-json "$S1" --max-lines 104); cin "a 100-line cut window needs 102 lines of budget" "not expanded: --only 1"
r=$(cpo --spec-json "$S1" --max-lines 105); cin "a 100-line cut window fits in 105 lines" "56 more lines: --only 1 --max-lines 160"
[ "$(wc -l <"$T/cp.out" | tr -d ' ')" = 105 ] && echo "ok context-pack a 100-line cut window fills 105 lines" || { echo "FAIL context-pack cut window at 105: $(wc -l <"$T/cp.out")"; fail=1; }
grep -qxF "56 more lines: --only 1 --max-lines 160" "$T/cp.out" && echo "ok context-pack the more-lines note is its own line" || { echo "FAIL context-pack more-lines note line"; fail=1; }
r=$(cpo --spec-json "$(wp "$T/cp-spec30.json")" --max-lines 202); cchk "reserve line at the budget edge" 0 "$r"
[ "$(grep -c '^== \[' "$T/cp.out")" = 9 ] && [ "$(wc -l <"$T/cp.out" | tr -d ' ')" -le 202 ] && echo "ok context-pack a window that would crowd out the not-expanded line is skipped" || { echo "FAIL context-pack reserve at 202: $(wc -l <"$T/cp.out")"; fail=1; }
# the binding env block reaches the server; the caller environment overrides it
mcp env -u HUGINN_ENDPOINT -u EUREKA_INSTANCE FAKE_MODE=needenv FAKE_NEED=rudp://binding $PY "$CP" t:cut_spec:cut-x.r1 "$Rw" >"$T/cp.out" 2>"$T/cp.err"; r=$?
cchk "env from the binding reaches the server" 3 "$r"; cmp -s "$T/cp.out" "$T/cp.want" && echo "ok context-pack binding env supplies the server environment" || { echo "FAIL context-pack binding env not applied"; fail=1; }
mcp env -u EUREKA_INSTANCE HUGINN_ENDPOINT=rudp://caller FAKE_MODE=needenv FAKE_NEED=rudp://caller $PY "$CP" t:cut_spec:cut-x.r1 "$Rw" >"$T/cp.out" 2>"$T/cp.err"; r=$?
cchk "the caller environment overrides the binding" 3 "$r"; cmp -s "$T/cp.out" "$T/cp.want" && echo "ok context-pack caller env wins over binding env" || { echo "FAIL context-pack caller env lost"; fail=1; }
# standard library plus git only
if grep -qE '^import (requests|yaml|tree_sitter)|^from (requests|yaml|tree_sitter)|ctags|tree-sitter|"rg"' "$CP"; then echo "FAIL context-pack: a dependency beyond git"; fail=1; else echo "ok context-pack: standard library and git only"; fi
exit $fail