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

7.9 KiB

issue-template.md — the issue body contract

The shape of every issue this plugin causes to be filed on a target plugin's own repo (output path A, design/plan.md §10, FR-6). Loaded at runtime by bin/filing-plan, which renders it; it is kept here rather than in the script body so that a change to what an issue says is a reviewable diff.

Format is the same as signals-catalog.md: each ## <section> heading below is followed by exactly one fenced block, and the fenced block is the only part the parser reads. Prose outside the fences is for humans.

Placeholders are {{name}}. Every placeholder in a template must be supplied — filing-plan fails loudly on an unknown or missing one rather than rendering a half-filled issue.

Who decides what

filing-plan renders the body, because the body is the contract: the marker, the measurement, the rubric verdict, and the falsifiability triple are all already-computed values and rendering them is a pure-script step.

It does not decide where the issue goes. Repo, final title, labels beyond token-offload, and milestone are anxious:issuer-agent's call, per its own taxonomy. The issue-title section below is a proposal passed to issuer, not an instruction.

The marker is load-bearing

The first line of every body — and of every update comment — is

<!-- ia-candidate: <candidate_id> -->

It is how a re-run recognizes its own prior work (FR-6.3). It renders invisible in Gitea, is stable across runs because candidate_id is, and must never be edited by hand on a filed issue. Removing it from an issue causes the next audit to file a duplicate — which the spec names as the single most likely way for this plugin to become hated.

Known failure mode (kotkan/claude-plugin-inference-arbitrage#23). Two issues filed on 2026-07-29 (oleks/claude-plugin-cluster#56, #57) reached Gitea with the <!-- --> wrapper stripped — the body opened with the bare text ia-candidate: <id> instead. bin/filing-plan always renders the wrapped form (verified: a direct issue_write probe with a literal <!-- --> in the body round-trips through the Gitea MCP write path unchanged, so the API/MCP layer is not the cause). The loss happens on the relay hopAgent(anxious:issuer-agent) composing the actual Gitea call from the prompt it's handed — because an HTML comment is, by design, invisible when the prompt text is read as markdown; an agent relaying "what the issue should say" rather than pasting the string byte-for-byte can drop a line it never visually saw. That is why skills/offload-audit/SKILL.md §5d now fences the rendered body in the delegation prompt instead of inlining it as bare markdown — a fenced block reads as literal text, not as renderable markup, to the relaying agent. bin/filing-plan's find_existing() also now matches the bare, unwrapped form as a backward-compatibility fallback, so issues already filed without the wrapper (like cluster#56/#57) are still found — but that fallback exists only to avoid duplicate-filing on already-broken issues, not as a second acceptable format going forward.

issue-title

A proposal. issuer may rewrite it.

token-offload: {{headline}} ({{skill}})

issue-body

{{examples}} renders as a markdown list, one input→output pair per line, with the edge case marked. {{determinism_tests}} renders as T1 ✓ T2 ✓ …, using ✗ for a failed test. {{evidence}} always renders — it states whether the cost claim is measured, thin, or unmeasured, so a reader never has to guess whether the number is evidence or arithmetic.

<!-- ia-candidate: {{candidate_id}} -->

**Measured cost.** {{measured_cost}}

**Evidence strength.** {{evidence}}

**Boundary position.** {{position}} — confidence {{boundary_confidence}}.
Determinism tests: {{determinism_tests}}

**Proposed signature.**
`{{signature}}`

**Examples.**

{{examples}}

**When a human would overrule this.** {{overrule_case}}

**Escalation path.** {{escalation_path}}

**What crosses the boundary.** {{digest_schema}}

---

This is a proposal from an audit, not an accepted design. It is deliberately
falsifiable: if the overrule case above is *common*, the cut belongs earlier in
the pipeline than proposed, and this issue should be reshaped rather than
implemented. Closing this as `wontfix` is a useful outcome and is tracked as a
calibration signal.

Audit: `{{audit_page}}` on the {{wiki_repo}} wiki. Rubric v{{rubric_version}}.

update-comment

Posted instead of a new issue when the marker is already present on the repo (FR-6.3). It carries the marker again so that the search finds this issue whether the marker lives in the body or only in a comment.

<!-- ia-candidate: {{candidate_id}} -->

**Re-measured {{run_date}}** — window {{window}}. Status: {{status}}.

{{measured_cost}}

Evidence strength: {{evidence}}

Boundary position {{position}}, confidence {{boundary_confidence}} (rubric
v{{rubric_version}}).

Audit: `{{audit_page}}` on the {{wiki_repo}} wiki.

How the pre-filing search is actually performed (FR-6.3). This is not hand-waved: both queries below run against the target repo, through anxious:issuer-agentcluster:gitea-agent, and their merged results are what filing-plan plan --existing consumes.

Three steps, not one. The obvious one-query design does not work, and the reason was established empirically against the live Gitea (2026-07-29, probe issue kotkan/claude-plugin-inference-arbitrage#9, since closed):

Neither search_issues nor list_issues returns the issue body. Both return metadata only — number, title, state, labels, timestamps. A marker match therefore cannot be made from a search result. The body must be fetched per issue with issue_read(method="get"), and comments with issue_read(method="get_comments").

Also established by the same probe: search_issues does full-text match on body content, including text inside an HTML comment (a query for a string that appeared only in the body, never in the title, returned the issue). So the full-text path works — it just cannot be trusted to confirm a match, only to suggest one.

1. candidates   list_issues(owner, repo, labels=["token-offload"], state="all")
                search_issues(query="ia-candidate: <candidate_id>", owner, state="all")
                -> union of issue numbers; bodies are NOT in these responses
2. fetch        issue_read(method="get", index=N)           -> body
                issue_read(method="get_comments", index=N)  -> comment bodies
                (comments only needed when the body has no marker)
3. match        filing-plan plan --existing <merged.json>   -> local literal match

Step 1's label scan is the authoritative half: every issue this plugin files carries token-offload, so it is a bounded, exact superset of the issues that can carry a marker. The full-text query is the safety net for an issue whose label a human removed. Step 3's match is a literal string comparison and is the only thing that decides identity — the search engine never does.

If step 1 finds no token-offload label on the target repo, that is the first-audit case: there are no prior issues, issuer creates the label with the first filing, and the empty result is correct.

The merged shape handed to --existing is a JSON array of {number, state, title, body, comments: [...]}. filing-plan matches <!--\s*ia-candidate:\s*<id>\s*--> against the body and each comment, so an issue keeps its identity even if a human rewrites the body entirely, as long as one update comment survives — also verified against the live probe issue.

--existing is mandatory, even when empty. filing-plan plan refuses to run without it rather than defaulting to "nothing exists", because that default turns a failed search into a duplicate-filing spree — the exact outcome FR-6.3 exists to prevent.