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.
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 hop —
Agent(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.
marker-search
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-agent → cluster: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_issuesnorlist_issuesreturns the issuebody. 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 withissue_read(method="get"), and comments withissue_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.