Files
Oleks b77fbdddbb Fix marker-wrapper loss and add backward-compat match (kotkan/claude-plugin-inference-arbitrage#23)
Root cause: bin/filing-plan and references/issue-template.md always render
the ia-candidate marker HTML-comment-wrapped. A direct issue_write probe
confirmed the Gitea MCP write path preserves a literal <!-- --> unchanged, so
the loss on oleks/claude-plugin-cluster#56 and #57 happened on the
Agent(anxious:issuer-agent) relay hop: an HTML comment is invisible when its
surrounding text is read as markdown, so an agent relaying "what the body
should say" instead of pasting it verbatim can silently drop it.

Two fixes:
- bin/filing-plan: find_existing()'s marker_re() now also matches the bare,
  unwrapped "ia-candidate: <id>" line (end-of-line anchored, not \b, since
  candidate ids contain "-" and a word-boundary check would false-match a
  longer id sharing a prefix) so already-filed issues like cluster#56/#57
  are still recognized and not duplicate-filed.
- skills/offload-audit/SKILL.md: the issuer delegation prompt now fences the
  rendered body in its own code block and explicitly warns that the leading
  marker is an invisible-by-design HTML comment, not stray syntax to clean
  up, instead of inlining it as bare markdown the relaying agent could read
  right through.

references/issue-template.md documents both, and tests/filing.test.sh adds
coverage for the bare-marker match plus a same-prefix-different-id
non-match case.

Bumps plugin.json to 0.7.1.
2026-07-30 19:24:13 +03:00

470 lines
18 KiB
Python
Executable File

#!/usr/bin/env python3
"""Decides, deterministically, what output path A does with each candidate:
open a new issue, comment on the one that already exists, or skip it — and
renders the exact body text for each (FR-6.1..FR-6.4, design/plan.md §10).
Everything here is a `pure-script` step by the plugin's own rubric: the verdicts
were already graded by bin/boundary-classify, the marker match is a literal
string comparison, and rendering a template is a total function of its inputs.
None of it may be done by inference — an audit that decides duplicates by
"looking at" the tracker is exactly the shape this plugin reports on, and
getting it wrong spams a real human's tracker.
This script never touches Gitea. It consumes the results of a search that
`anxious:issuer-agent` -> `cluster:gitea-agent` performed, and emits a plan that
the same chain executes. Two subcommands, in order:
filing-plan queries --classified c.json --judgments j.json [--target-path P]
filing-plan plan --classified c.json --judgments j.json --existing e.json ...
`queries` resolves the target repo (or reports that filing is unavailable) and
emits the marker searches to run. `plan` consumes their merged results.
"""
import argparse
import json
import os
import re
import subprocess
import sys
PLUGIN_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
DEFAULT_TEMPLATE = os.path.join(PLUGIN_ROOT, "references", "issue-template.md")
DEFAULT_WORKSPACE = os.path.expanduser("~/projects/claude-plugins")
DEFAULT_GITEA_HOST = "git.oleks.space"
DEFAULT_WIKI_REPO = "kotkan/claude-plugin-inference-arbitrage"
OFFLOAD_LABEL = "token-offload"
TEMPLATE_SECTIONS = {"issue-title", "issue-body", "update-comment"}
# --------------------------------------------------------------------------
# template loading and rendering
# --------------------------------------------------------------------------
def load_templates(path):
"""Same contract as signals-catalog.md: one fenced block per recognized
`## <section>` heading, everything else prose."""
with open(path, encoding="utf-8") as fh:
text = fh.read()
out = {}
for m in re.finditer(r"^##\s+([a-z-]+)\s*$", text, re.MULTILINE):
name = m.group(1)
if name not in TEMPLATE_SECTIONS:
continue
block = re.search(r"```\S*\n(.*?)```", text[m.end():], re.DOTALL)
if block:
out[name] = block.group(1).strip("\n")
missing = TEMPLATE_SECTIONS - out.keys()
if missing:
sys.exit(f"error: {path} is missing section(s): {', '.join(sorted(missing))}")
return out
PLACEHOLDER = re.compile(r"\{\{([a-z_]+)\}\}")
def render(template, values):
"""Strict substitution. An unsupplied placeholder is an error, never an
empty string — a half-filled issue body is worse than a crashed audit."""
missing = sorted(
{k for k in PLACEHOLDER.findall(template) if values.get(k) in (None, "")}
)
if missing:
sys.exit(f"error: template placeholder(s) unsupplied: {', '.join(missing)}")
return PLACEHOLDER.sub(lambda m: str(values[m.group(1)]), template)
# --------------------------------------------------------------------------
# the marker (FR-6.3)
# --------------------------------------------------------------------------
def marker(candidate_id):
return f"<!-- ia-candidate: {candidate_id} -->"
def marker_re(candidate_id):
"""Tolerates whitespace drift inside the comment, and tolerates the
HTML-comment wrapper (`<!-- -->`) being absent entirely, nothing else. The
id itself must match exactly — a fuzzy id match would let two genuinely
different candidates collapse onto one issue.
The bare form (no `<!-- -->`) is a backward-compatibility case, not the
contract: kotkan/claude-plugin-inference-arbitrage#23 found that issues
filed on 2026-07-29 (oleks/claude-plugin-cluster#56, #57) lost the wrapper
somewhere between `filing-plan`'s render and the Gitea write — the
template and this script always render the wrapped form (see `marker()`
below and references/issue-template.md). Matching the bare line here is
purely so a *later* audit still recognizes those already-filed issues and
doesn't duplicate-file them; it does not license filing new issues without
the wrapper.
"""
wrapped = r"<!--\s*ia-candidate:\s*" + re.escape(candidate_id) + r"\s*-->"
# End-of-line anchored (not \b): candidate_id contains "/" and "-", so a
# word-boundary check would treat "…label-derivation" as a match inside
# "…label-derivation-longer" (the "n"/"-" transition IS a \b). Anchoring
# to end-of-line (re.MULTILINE) instead requires an exact id, matching
# what marker() actually renders as the line's sole content.
bare = r"^ia-candidate:\s*" + re.escape(candidate_id) + r"[ \t]*$"
return re.compile(wrapped + "|" + bare, re.MULTILINE)
def find_existing(candidate_id, issues):
"""Returns (issue_or_None, duplicates). Matches the body and every comment,
so an issue keeps its identity even if a human rewrites the body."""
pat = marker_re(candidate_id)
hits = []
for issue in issues:
haystacks = [issue.get("body") or ""]
haystacks += [c if isinstance(c, str) else (c.get("body") or "")
for c in issue.get("comments") or []]
if any(pat.search(h) for h in haystacks):
hits.append(issue)
if not hits:
return None, []
hits.sort(key=lambda i: i.get("number", 0))
return hits[0], hits[1:]
# --------------------------------------------------------------------------
# target repo resolution (FR-6.4)
# --------------------------------------------------------------------------
def git_remote(path):
if not os.path.isdir(os.path.join(path, ".git")):
return None
try:
out = subprocess.run(
["git", "-C", path, "remote", "get-url", "origin"],
capture_output=True, text=True, timeout=10,
)
except (OSError, subprocess.SubprocessError):
return None
return out.stdout.strip() if out.returncode == 0 else None
def parse_remote(url, host):
"""owner/repo if the remote points at our Gitea, else None."""
m = re.match(r"^(?:git@|ssh://git@|https://)([^/:]+)[:/](.+?)(?:\.git)?/?$", url)
if not m or m.group(1) != host:
return None
parts = m.group(2).split("/")
return "/".join(parts[-2:]) if len(parts) >= 2 else None
def resolve_repo(args, target_name):
"""(repo, filing, reason). `filing` is 'available' or 'unavailable'.
Order: explicit flag, the target checkout's own remote, then the workspace
sibling checkout — because an audit normally runs against
~/.claude/plugins/cache/... which carries no .git at all.
"""
if args.target_repo:
return args.target_repo, "available", None
tried = []
candidates = []
if args.target_path:
candidates.append(os.path.abspath(args.target_path))
if target_name and args.workspace:
sibling = os.path.join(args.workspace, target_name)
if sibling not in candidates:
candidates.append(sibling)
for path in candidates:
if not os.path.isdir(path):
continue
url = git_remote(path)
if url is None:
tried.append(f"{path}: no git remote")
continue
repo = parse_remote(url, args.gitea_host)
if repo:
return repo, "available", None
tried.append(f"{path}: remote {url} is not on {args.gitea_host}")
if not candidates:
detail = "no --target-repo, --target-path, or workspace checkout to resolve from"
else:
detail = "; ".join(tried) or "no checkout found at " + ", ".join(candidates)
return None, "unavailable", (
f"no writable repo for target {target_name!r} ({detail}). "
"Path A is skipped; the candidate is reported in the run summary only (FR-6.4)."
)
# --------------------------------------------------------------------------
# rendering values
# --------------------------------------------------------------------------
TICKS = {True: "", False: ""}
def fmt_tests(tests):
return " ".join(f"{t} {TICKS[tests.get(t) is True]}" for t in ("T1", "T2", "T3", "T4", "T5"))
def fmt_tokens(n):
if n >= 1_000_000:
return f"{n / 1_000_000:.1f}M"
if n >= 1_000:
return f"{n / 1_000:.1f}k"
return str(n)
def fmt_examples(examples):
return "\n".join(f"- {e}" for e in examples)
def fmt_evidence(strength, m):
inv, turns = m.get("invocations", 0), m.get("attributed_turns", 0)
if strength == "measured":
return f"`measured` — {inv} invocations, {turns} attributed turns."
if strength == "thin":
return (
f"`thin` — only {inv} invocation(s) and {turns} attributed turns in this "
"window. **The cost figure above is arithmetic, not evidence.** The "
"correctness case stands on the rubric; the value claim is unproven."
)
return (
"`unmeasured` — no transcript history for this target in the window. This "
"candidate rests on the plugin definition alone (FR-2.4)."
)
def fmt_cost(cand, cls, window):
"""Only states what was actually measured. Absent signals are omitted, never
guessed at — a fabricated MTR in a filed issue is unrecoverable."""
m = cand.get("measurement") or {}
bits = []
value = cls.get("offload_value") or 0
share = cls.get("share_of_audited_spend") or 0
inv = m.get("invocations", 0)
head = f"{fmt_tokens(value)} weighted tokens"
if inv:
head += f" over {inv} invocation{'s' if inv != 1 else ''}"
if window:
head += f" ({window})"
if share:
head += f", {share:.0%} of this plugin's audited spend"
bits.append(head + ".")
stats = []
if m.get("mtr") is not None:
stats.append(f"MTR {m['mtr']:.2f}")
if m.get("judgment_density") is not None:
stats.append(f"judgment density {m['judgment_density']:.2f}")
if stats:
bits.append(" ".join(stats) + ".")
ngram = m.get("ngram") or {}
if ngram.get("sig") and ngram.get("recurrences"):
seq = " -> ".join(ngram["sig"])
bits.append(f"Recurring sequence observed {ngram['recurrences']}x: {seq}.")
return " ".join(bits)
def headline(cand):
if cand.get("headline"):
return cand["headline"]
# Last path segment of the candidate id is its slug by construction
# (design/plan.md §6: "<plugin>/<skill>/<slug>").
return cand["candidate_id"].rsplit("/", 1)[-1].replace("-", " ")
# --------------------------------------------------------------------------
# the plan
# --------------------------------------------------------------------------
def index_judgments(doc):
return {c["candidate_id"]: c for c in doc.get("candidates", [])}
def build(args, cls_doc, jud_doc, existing, templates):
target = cls_doc.get("target") or jud_doc.get("target")
repo, filing, reason = resolve_repo(args, target)
judgments = index_judgments(jud_doc)
actions, warnings = [], []
for cls in cls_doc.get("candidates", []):
cid = cls["candidate_id"]
verdict = cls.get("verdict")
if verdict != "file":
actions.append({
"candidate_id": cid,
"action": "skip",
"reason": f"verdict {verdict!r} — path A files only 'file' verdicts "
"(FR-4.2 gate, enforced by boundary-classify)",
})
continue
if filing == "unavailable":
actions.append({
"candidate_id": cid,
"action": "skip",
"filing": "unavailable",
"reason": reason,
})
continue
cand = judgments.get(cid)
if cand is None:
sys.exit(f"error: candidate {cid!r} has verdict 'file' but no judgments entry")
f = cand.get("falsifiability") or {}
values = {
"candidate_id": cid,
"headline": headline(cand),
"skill": cand.get("skill") or cls.get("skill"),
"measured_cost": fmt_cost(cand, cls, args.window),
"evidence": fmt_evidence(cls.get("measurement_strength"), cand.get("measurement") or {}),
"position": cls.get("position"),
"boundary_confidence": cls.get("boundary_confidence"),
"determinism_tests": fmt_tests(cls.get("determinism_tests") or {}),
"signature": f.get("signature"),
"examples": fmt_examples(f.get("examples") or []),
"overrule_case": f.get("overrule_case"),
"escalation_path": cand.get("escalation_path"),
"digest_schema": cand.get("digest_schema") or "n/a — position 1, the whole step moves.",
"audit_page": args.audit_page,
"wiki_repo": args.wiki_repo,
"rubric_version": cls_doc.get("rubric_version", "1.0.0"),
"run_date": args.run_date,
"window": args.window,
"status": args.status,
}
found, dupes = find_existing(cid, existing)
if dupes:
numbers = ", ".join("#%s" % d.get("number") for d in [found] + dupes)
warnings.append(
f"{cid}: marker found on {1 + len(dupes)} issues ({numbers}) — "
"commenting on the lowest-numbered one; the rest want a human merge"
)
if found:
actions.append({
"candidate_id": cid,
"action": "comment",
"issue": f"{repo}#{found['number']}",
"issue_number": found["number"],
"issue_state": found.get("state", "unknown"),
"marker": marker(cid),
"body": render(templates["update-comment"], values),
"note": "existing issue is closed; issuer decides whether to reopen"
if found.get("state") == "closed" else None,
})
else:
actions.append({
"candidate_id": cid,
"action": "file",
"marker": marker(cid),
"proposed_title": render(templates["issue-title"], values),
"body": render(templates["issue-body"], values),
"required_labels": [OFFLOAD_LABEL],
"labels_note": "the four-axis anxious taxonomy is issuer's call; "
f"{OFFLOAD_LABEL} is required and issuer creates it if absent",
})
counts = {a: sum(1 for x in actions if x["action"] == a) for a in ("file", "comment", "skip")}
return {
"target": target,
"repo": repo,
"filing": filing,
"filing_unavailable_reason": reason,
"run_date": args.run_date,
"window": args.window,
"audit_page": args.audit_page,
"delegation": "offload-analyst -> Agent(anxious:issuer-agent) -> cluster:gitea-agent -> Gitea",
"summary": {
"to_file": counts["file"],
"to_comment": counts["comment"],
"skipped": counts["skip"],
"searched_issues": len(existing),
},
"warnings": warnings,
"actions": actions,
}
def build_queries(args, cls_doc, jud_doc):
target = cls_doc.get("target") or jud_doc.get("target")
repo, filing, reason = resolve_repo(args, target)
ids = [c["candidate_id"] for c in cls_doc.get("candidates", []) if c.get("verdict") == "file"]
doc = {
"target": target,
"repo": repo,
"filing": filing,
"filing_unavailable_reason": reason,
"candidates_to_search": ids,
}
if filing == "available" and ids:
doc["queries"] = {
"full_text": [
{"tool": "search_issues", "repo": repo, "state": "all",
"q": f"ia-candidate: {cid}"} for cid in ids
],
"label_scan": {"tool": "list_issues", "repo": repo, "state": "all",
"labels": [OFFLOAD_LABEL]},
}
doc["note"] = (
"Run BOTH and merge into one array of "
"{number, state, title, body, comments[]} for `filing-plan plan --existing`. "
"The label scan is authoritative; the full-text query is the fast path and "
"may miss on indexer tokenization. See references/issue-template.md."
)
return doc
def main():
ap = argparse.ArgumentParser(prog="filing-plan")
sub = ap.add_subparsers(dest="cmd", required=True)
def common(p):
p.add_argument("--classified", required=True, help="bin/boundary-classify output")
p.add_argument("--judgments", required=True, help="the analyst's judgments JSON")
p.add_argument("--target-repo", help="owner/repo, if already known")
p.add_argument("--target-path", help="the audited plugin's directory")
p.add_argument("--workspace", default=DEFAULT_WORKSPACE)
p.add_argument("--gitea-host", default=DEFAULT_GITEA_HOST)
p.add_argument("--json", action="store_true", help="compact single-line JSON")
q = sub.add_parser("queries", help="resolve the repo and emit the marker searches")
common(q)
p = sub.add_parser("plan", help="decide file/comment/skip per candidate")
common(p)
p.add_argument("--existing", required=True,
help="JSON array of issues from the marker search. Required even when "
"empty ([]): defaulting to 'nothing exists' would duplicate on re-run.")
p.add_argument("--audit-page", required=True, help="e.g. Audits/anxious/2026-07-29")
p.add_argument("--window", required=True, help="e.g. 2026-07-22 -> 2026-07-29")
p.add_argument("--run-date", required=True)
p.add_argument("--wiki-repo", default=DEFAULT_WIKI_REPO)
p.add_argument("--status", default="persisting",
help="snapshot-diff status for the update comment (audit-snapshot diff)")
p.add_argument("--template", default=DEFAULT_TEMPLATE)
args = ap.parse_args()
with open(args.classified) as fh:
cls_doc = json.load(fh)
with open(args.judgments) as fh:
jud_doc = json.load(fh)
if args.cmd == "queries":
out = build_queries(args, cls_doc, jud_doc)
else:
with open(args.existing) as fh:
existing = json.load(fh)
if not isinstance(existing, list):
sys.exit("error: --existing must be a JSON array of issues")
out = build(args, cls_doc, jud_doc, existing, load_templates(args.template))
json.dump(out, sys.stdout, indent=None if args.json else 2, ensure_ascii=False)
sys.stdout.write("\n")
if __name__ == "__main__":
main()