ward agent task: freeform handoff mis-parses a URL as a repo ref #234

Closed
opened 2026-06-23 08:08:34 +00:00 by coilyco-ops · 2 comments
Member

Problem

ward agent is becoming the exclusive entrypoint for work, and a growing class of it is transient: "here's a thing that's wrong - a red CI run, a failing job URL, a broken endpoint - go deal with it." The natural handoff is to paste the URL of the broken thing and let the agent take it from there.

ward should not grow special knowledge of CI runs (or any other artifact type) to serve this. It stays general. The right primitive already exists: the freeform task verb. The gap is that task is not actually good at being pointed at a transient thing - it mis-parses a URL.

Repro (the concrete friction)

$ ward agent claude task "https://forgejo.coilysiren.me/coilyco-flight-deck/ward/actions/runs/301"
ward: refusing untrusted owner "runs" (allowed: coilysiren, coilyco-bridge, coilyco-flight-deck, coilyco-gaming)

The freeform ROUTE parser reads the URL's path segments as owner/repo and extracts "runs" as the owner. So the one surface that should happily accept "here's a URL, go look at it" instead tries to coerce the URL into a repo ref and refuses.

A second repro shows the parser ignores leading prose and walks deep into the path - here it grabbed segment 6, "attempt", not the first path component:

$ ward agent claude task 'fix CI: https://forgejo.coilysiren.me/coilyco-flight-deck/ward/actions/runs/301/jobs/0/attempt/1'
ward: refusing untrusted owner "attempt" (allowed: coilysiren, coilyco-bridge, coilyco-flight-deck, coilyco-gaming)

So the bug is not "first token looks like a ref" - the parser scans the whole string for any .../<x>/<y>/... shape and coerces a pair out of it, defeating both a natural-language prefix and the URL's real owner/repo (which is right there: coilyco-flight-deck/ward).

For contrast, work correctly rejects a non-issue URL, but its error does not point at task as the freeform alternative:

$ ward agent claude work "<run-url>"
ward: cannot parse issue ref "<run-url>": want owner/repo#N or .../issues/N

Proposed fix (narrow, general) - the parse rule

Only coerce a token into a ref when it matches exactly one of:

  • owner/repo[#N] (the bare ref shape), or
  • a Forgejo issue URL (.../{owner}/{repo}/issues/{N}).

Everything else - URLs of any other shape (actions runs, jobs, attempts, commits), bare prose, anything - flows through unchanged as freeform task content. Do not scan a string for embedded .../x/y/... pairs and lift an owner/repo out of it; that is the current bug. The in-container agent receives the URL as context and fetches/diagnoses it with normal tools (curl, gh, log readers); ward never interprets the artifact.

Optional nicety: when work (issue-only) is handed a non-issue URL, hint that task accepts arbitrary pointers.

Acceptance:

  • ward agent claude task 'fix CI: https://forgejo.coilysiren.me/coilyco-flight-deck/ward/actions/runs/301/jobs/0/attempt/1' no longer refuses on a phantom owner; the URL is carried as task content and ROUTE picks the repo from the task semantics (or asks), not from URL path segments.
  • ward agent claude task coilyco-flight-deck/ward --instructions ... (DIRECT) and task '<bare prose>' (ROUTE) keep working unchanged.
  • A genuine owner/repo#N or .../issues/N token still resolves to that ref as before.
  • Add a parser unit test covering: actions-run URL, jobs/attempt URL, issue URL, bare owner/repo#N, and prose-with-embedded-URL.

ward learns nothing about CI runs. It just stops eating URLs, and task becomes good at the thing it is already shaped for.

Why this beats a typed surface

The earlier framing here (a triage verb that fetches run logs and classifies failures) baked a CI-run object into ward. That is the wrong layer: the classification and log-reading are the agent's job inside the container, done with general tools, against whatever the URL points at. ward's only job is to not mangle the pointer on the way in.

Out of scope

Container model, clone, reservation, reaper (ward container). The two real ward CI failures that surfaced this - the release.yml publish/tap reds on run 301, and the golangci-lint reds in #233 - are worked separately; this issue is only about the freeform-handoff parse.

## Problem `ward agent` is becoming the exclusive entrypoint for work, and a growing class of it is **transient**: "here's a thing that's wrong - a red CI run, a failing job URL, a broken endpoint - go deal with it." The natural handoff is to paste the URL of the broken thing and let the agent take it from there. ward should **not** grow special knowledge of CI runs (or any other artifact type) to serve this. It stays general. The right primitive already exists: the freeform `task` verb. The gap is that `task` is not actually good at being *pointed* at a transient thing - it mis-parses a URL. ## Repro (the concrete friction) ``` $ ward agent claude task "https://forgejo.coilysiren.me/coilyco-flight-deck/ward/actions/runs/301" ward: refusing untrusted owner "runs" (allowed: coilysiren, coilyco-bridge, coilyco-flight-deck, coilyco-gaming) ``` The freeform ROUTE parser reads the URL's path segments as `owner/repo` and extracts "runs" as the owner. So the one surface that should happily accept "here's a URL, go look at it" instead tries to coerce the URL into a repo ref and refuses. A second repro shows the parser ignores leading prose and walks *deep* into the path - here it grabbed segment 6, "attempt", not the first path component: ``` $ ward agent claude task 'fix CI: https://forgejo.coilysiren.me/coilyco-flight-deck/ward/actions/runs/301/jobs/0/attempt/1' ward: refusing untrusted owner "attempt" (allowed: coilysiren, coilyco-bridge, coilyco-flight-deck, coilyco-gaming) ``` So the bug is not "first token looks like a ref" - the parser scans the whole string for any `.../<x>/<y>/...` shape and coerces a pair out of it, defeating both a natural-language prefix and the URL's real owner/repo (which is right there: `coilyco-flight-deck/ward`). For contrast, `work` correctly rejects a non-issue URL, but its error does not point at `task` as the freeform alternative: ``` $ ward agent claude work "<run-url>" ward: cannot parse issue ref "<run-url>": want owner/repo#N or .../issues/N ``` ## Proposed fix (narrow, general) - the parse rule Only coerce a token into a ref when it matches exactly one of: - `owner/repo[#N]` (the bare ref shape), or - a Forgejo **issue** URL (`.../{owner}/{repo}/issues/{N}`). Everything else - URLs of any other shape (actions runs, jobs, attempts, commits), bare prose, anything - flows through unchanged as **freeform task content**. Do not scan a string for embedded `.../x/y/...` pairs and lift an owner/repo out of it; that is the current bug. The in-container agent receives the URL as context and fetches/diagnoses it with normal tools (curl, gh, log readers); ward never interprets the artifact. Optional nicety: when `work` (issue-only) is handed a non-issue URL, hint that `task` accepts arbitrary pointers. Acceptance: - `ward agent claude task 'fix CI: https://forgejo.coilysiren.me/coilyco-flight-deck/ward/actions/runs/301/jobs/0/attempt/1'` no longer refuses on a phantom owner; the URL is carried as task content and ROUTE picks the repo from the task semantics (or asks), not from URL path segments. - `ward agent claude task coilyco-flight-deck/ward --instructions ...` (DIRECT) and `task '<bare prose>'` (ROUTE) keep working unchanged. - A genuine `owner/repo#N` or `.../issues/N` token still resolves to that ref as before. - Add a parser unit test covering: actions-run URL, jobs/attempt URL, issue URL, bare `owner/repo#N`, and prose-with-embedded-URL. ward learns nothing about CI runs. It just stops eating URLs, and `task` becomes good at the thing it is already shaped for. ## Why this beats a typed surface The earlier framing here (a `triage` verb that fetches run logs and classifies failures) baked a CI-run object into ward. That is the wrong layer: the classification and log-reading are the agent's job inside the container, done with general tools, against whatever the URL points at. ward's only job is to not mangle the pointer on the way in. ## Out of scope Container model, clone, reservation, reaper (`ward container`). The two real ward CI failures that surfaced this - the `release.yml` publish/tap reds on run 301, and the golangci-lint reds in #233 - are worked separately; this issue is only about the freeform-handoff parse.
coilyco-ops changed title from ward agent: a run-aware triage surface for run-shaped (CI-red) work to ward agent task: freeform handoff mis-parses a URL as a repo ref 2026-06-23 08:13:26 +00:00
Author
Member

🔒 Reserved by ward agent claude — container ward-ward-issue-234-claude-3c4526be on host kais-macbook-pro-2.local is carrying this issue (reserved 2026-06-23T08:15:48Z). Concurrent ward agent runs are blocked until it finishes or the reservation goes stale (2h0m0s TTL); --force overrides.

— Claude (she/her), via ward agent

<!-- ward-agent-reservation --> 🔒 Reserved by `ward agent claude` — container `ward-ward-issue-234-claude-3c4526be` on host `kais-macbook-pro-2.local` is carrying this issue (reserved 2026-06-23T08:15:48Z). Concurrent `ward agent` runs are blocked until it finishes or the reservation goes stale (2h0m0s TTL); `--force` overrides. <!-- ward-agent-signature --> — Claude (she/her), via `ward agent`
Author
Member

🔒 Reserved by ward agent claude — container ward-ward-issue-234-claude-33f3e788 on host kais-macbook-pro-2.local is carrying this issue (reserved 2026-06-23T08:18:54Z). Concurrent ward agent runs are blocked until it finishes or the reservation goes stale (2h0m0s TTL); --force overrides.

— Claude (she/her), via ward agent

<!-- ward-agent-reservation --> 🔒 Reserved by `ward agent claude` — container `ward-ward-issue-234-claude-33f3e788` on host `kais-macbook-pro-2.local` is carrying this issue (reserved 2026-06-23T08:18:54Z). Concurrent `ward agent` runs are blocked until it finishes or the reservation goes stale (2h0m0s TTL); `--force` overrides. <!-- ward-agent-signature --> — Claude (she/her), via `ward agent`
Sign in to join this conversation.
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
coilyco-flight-deck/ward#234
No description provided.