advisor ref mode runs native (host one-shot); containerize it like engineer + freeform modes #411

Closed
opened 2026-07-01 08:13:59 +00:00 by coilysiren · 5 comments
Owner

Problem

runAgentAdvisor (cmd/ward/agent_advisor.go:69-79) splits by argument type:

  • Ref mode (arg is an issue ref owner/repo#N) -> runAgentReply = a host one-shot research pass, native, no container (agent_reply.go:14, ward#179).
  • Freeform mode (bare quoted question) -> runAgentAsk = a fresh attached ephemeral container (agent_advisor.go:82-120).

Engineer always runs in a container. So ref-mode advisor is the lone native outlier among the agent modes. Kai wants it containerized like the others.

Why change it

  • Consistency - one execution model across agent modes (container), not a native special-case.
  • Isolation - a native host one-shot reads the host's live /substrate + clones; a container gives it the same clean, reproducible working tree every other mode gets.
  • Durability gap - a native ref run's ONLY artifact is the posted comment. If the post fails, the work survives only in a host dispatch log - exactly what bit the #306/#401 advisor runs (ward#402 commentIssue --body-file skew). A container lifecycle can land its artifact more robustly.

Decision owner: Kai (deferred)

The approach is a design call Kai will make. Open questions to settle then:

  • Does ref mode reuse the engineer container bring-up, the freeform-advisor (runAgentAsk) bring-up, or a shared extraction?
  • How does 'research + post a comment' map onto the container lifecycle - post from inside the container before exit (like engineer emits its WARD-OUTCOME), or stream back to a host poster?
  • Still comment-only, or also leave a branch/artifact like engineer?
  • Trust gate + the per-advisory resource cost of a full container.

Dispatched sub-component (now)

A grounding research pass (dispatched against this issue) to arm the decision above: enumerate the current ref-vs-freeform mechanism, the reuse surface (engineer vs runAgentAsk container bring-up), the comment-from-container question, and 2-3 concrete design options with tradeoffs. The build waits on Kai's pick.

Mode

consult.

## Problem `runAgentAdvisor` (`cmd/ward/agent_advisor.go:69-79`) splits by argument type: - **Ref mode** (arg is an issue ref `owner/repo#N`) -> `runAgentReply` = a **host one-shot research pass, native, no container** (`agent_reply.go:14`, ward#179). - **Freeform mode** (bare quoted question) -> `runAgentAsk` = a **fresh attached ephemeral container** (`agent_advisor.go:82-120`). Engineer always runs in a container. So **ref-mode advisor is the lone native outlier** among the agent modes. Kai wants it containerized like the others. ## Why change it - **Consistency** - one execution model across agent modes (container), not a native special-case. - **Isolation** - a native host one-shot reads the host's live `/substrate` + clones; a container gives it the same clean, reproducible working tree every other mode gets. - **Durability gap** - a native ref run's ONLY artifact is the posted comment. If the post fails, the work survives only in a host dispatch log - exactly what bit the #306/#401 advisor runs (ward#402 `commentIssue --body-file` skew). A container lifecycle can land its artifact more robustly. ## Decision owner: Kai (deferred) The approach is a design call Kai will make. Open questions to settle then: - Does ref mode reuse the **engineer** container bring-up, the **freeform-advisor** (`runAgentAsk`) bring-up, or a shared extraction? - How does 'research + post a comment' map onto the container lifecycle - post from inside the container before exit (like engineer emits its WARD-OUTCOME), or stream back to a host poster? - Still comment-only, or also leave a branch/artifact like engineer? - Trust gate + the per-advisory resource cost of a full container. ## Dispatched sub-component (now) A grounding research pass (dispatched against this issue) to arm the decision above: enumerate the current ref-vs-freeform mechanism, the reuse surface (engineer vs `runAgentAsk` container bring-up), the comment-from-container question, and 2-3 concrete design options with tradeoffs. **The build waits on Kai's pick.** ## Mode consult.
Member

🔎 ward agent advisor

ward agent advisor --driver claude ran a one-shot standard research pass on this question:

Grounding research only - do NOT decide the approach (Kai owns that). Read live against /substrate/ward. Deliver: (1) a precise map of how ref mode (runAgentReply/agent_reply.go, host one-shot) and freeform mode (runAgentAsk, ephemeral container) each run today, with file:line; (2) the reuse surface for containerizing ref mode - compare the engineer container bring-up vs runAgentAsk's bring-up, what's shared, what a shared extraction would look like; (3) the 'post a comment from inside a container' question - how engineer emits WARD-OUTCOME and posts, whether ref mode can post its research comment pre-exit the same way, and the failure-durability angle (ward#402); (4) 2-3 concrete design options with tradeoffs and a resource-cost note (a full container per advisory). Post as a comment on ward#411.


Now I have a complete, precise picture. Writing the research comment.

1. How each mode runs today

Both modes hang off runAgentAdvisor (cmd/ward/agent_advisor.go:71-80), which splits purely on whether the first arg parses as an issue ref:

Ref mode (owner/repo#N or issue URL) → runAgentReply (cmd/ward/agent_reply.go:77-123). This is the native host one-shot:

  • validateReplyInputs (agent_reply.go:127-161) parses the ref, joins the tail as the prompt, resolves --thoroughness to a replyThoroughness rung (quick/standard/deep, with 3m/8m/15m timeouts, agent_reply.go:30-51), trust-gates the owner against the primary-org set, and asserts the mode has a host one-shot slot. Only claude and goose qualify - hostPreflightArgv (container_compute.go:195-206) returns claude -p <prompt> / goose run -t <prompt>, and codex/qwen return false. It also requires hostHasBinary(bin) - the agent CLI must be installed on the host.
  • It fetches the issue + comment thread host-side (agent_reply.go:86-94), builds the research prompt (replyResearchPrompt, agent_reply.go:187-222 - this is the exact prompt you are reading right now), then captureReplyResearch (agent_reply.go:165-183) runs the argv via capturePreflight (agent.go:612-621) in a throwaway os.MkdirTemp cwd, bounded by the level timeout.
  • The captured stdout is the artifact. runAgentReply gets a hostForgejoClient and calls commentIssue(...) host-side (agent_reply.go:114-120), wrapping the read in a header + <!-- ward-agent-reply --> footer (replyComment, agent_reply.go:230-247).
  • No container, no clone, no branch. The clone in replyResearchPrompt is optional - the in-prompt agent may git clone into scratch if depth warrants, but nothing is provisioned.

Freeform mode (bare quoted question) → runAgentAsk (agent_advisor.go:84-160). This is a fresh attached ephemeral container:

  • Resolves --repo (or infers from cwd git origin, agent_advisor.go:96), trust-gates the owner, decides oneshot from --oneshot || !terminalAttached() (agent_advisor.go:109), and seeds either interactivePrompt (default TTY session) or askPrompt (agent_advisor.go:111-114).
  • writeContainerAssets() + buildUpPlan(...) (agent_advisor.go:116-123, container.go:99-146) build the plan, re-cast as roleAdvisor with an issueless machine-id name (agent_advisor.go:132-133). plan.Ask = oneshot toggles WARD_ASK.
  • It sweeps stale containers, pulls the image, writes the token envfile, and createAgentContainer runs it attached (agent_advisor.go:141-159). Output streams to the terminal. It never posts a comment - the answer's only home is the operator's screen.

So the two ref/freeform halves of one command already run on two entirely different execution models, and only ref mode produces a durable issue artifact.

2. Reuse surface for containerizing ref mode

The good news: the container bring-up is already factored, and both existing container paths converge on the same core. buildAgentPlan (engineer, agent.go:939-963) is literally buildUpPlan(...) plus a detached/headless re-cast (Role=roleEngineer, Headless=true, Interactive=false, TTY=false, Branch=issue-N). runAgentAsk (freeform advisor) is the same buildUpPlan(...) core plus an attached/ephemeral re-cast (Role=roleAdvisor, plan.Ask). Both end at createAgentContainer (agent.go:1100).

What a containerized ref mode would share regardless of which parent it copies:

  • buildUpPlancreateAgentContainer (the whole plan/mount/pull/envfile spine).
  • The trust gate (ownerAllowed), image flags, --no-pull/--print, writeTokenEnvFile, sweepStaleContainers.
  • The dispatch-broker forwarding it already gets - maybeForwardAgentDispatchToHostBroker(..., "advisor", ...) (agent_advisor.go:74-76, agent_dispatch_broker.go:292-297) already forwards an in-container advisor ref-dispatch to host ward. That layer is orthogonal to whether the host-side execution is native or containerized.

Engineer bring-up vs runAgentAsk bring-up - what differs, and which to lean on:

  • Engineer is detached, headless, writable, branch-carrying, reserves the issue (launchAgentContainer, agent.go:983-1043), runs a pre-flight, and emits WARD-OUTCOME. That is a lot of machinery ref mode does not want (no reservation contention for a read-only research pass, no issue-N branch, no director reconciliation).
  • runAgentAsk is the closer relative: ephemeral, role=advisor, no reservation, no branch, read-only intent. The gap is that it runs attached and streams rather than detached and posts. Its askPrompt is a "stream to terminal" contract, not a "post a comment" contract.

A clean shared extraction would be: keep buildUpPlan as the base (already shared), and give ref mode a third plan re-cast - roleAdvisor like freeform, but detached like engineer, carrying the replyResearchPrompt as its seed and a new "post the comment yourself" contract. The thoroughness ladder (agent_reply.go:30-73) is pure and portable as-is - it just needs to map onto a container timeout instead of a context.WithTimeout around a host capture.

The one hard constraint that loosens under containerization: the claude|goose-only restriction and hostHasBinary check (agent_reply.go:153-159) exist solely because it runs a host one-shot. In a container the agent binary is baked into the image, so ref mode could gain codex/qwen support for free - the same reason freeform mode already supports all four drivers.

3. Posting a comment from inside a container

How engineer does it today: the container is never asked by ward to post. The in-container agent posts, as the last line of its own seeded instructions. headlessReflectionAction (agent.go:101-115) tells the agent that its final step is to post a retrospective comment beginning with a WARD-OUTCOME: done|blocked|failed line. The agent uses the forge token mounted in the container (the same credential it pushes with) to call the forge. Ward's host side never posts - it later reads that comment back (parseBacklogOutcome, agent_director.go:667-711; backlogReconcile parks a container that exited with no WARD-OUTCOME as failed, agent_director.go:980-1000).

Can ref mode post its research comment the same way? Yes, structurally. The clean-container equivalent of askPrompt/replyResearchPrompt would contract: "research this, then post your answer as a comment on owner/repo#N and exit." The forge token is already in the container for any writable role, and even a read-only advisor container can post through the credential broker (start_broker / revoke_push_credential, entrypoint.sh:885-890) - the broker keeps commenting ability while dropping push. So a research-and-post-from-inside path is wired-adjacent, not greenfield.

The failure-durability angle (ward#402): today's native ref post is a single point of failure. The research lives only in host stdout, and the one commentIssue call is the only thing that turns it into a durable artifact. That is exactly what the #306/#401 advisor runs hit - the ward#402 commentIssue --body-file skew meant the host-side post silently failed and the entire research pass survived only in a host dispatch log. Containerizing changes the durability calculus in two ways worth naming for the decision:

  • Post-from-inside does not by itself fix it - if the in-container post is still one call and it fails, you are back to "only the container log has it," except now the log is a drained container log (agent_log_drain.go) rather than a broker log. Marginally more discoverable, not categorically safer.
  • What actually hardens it is giving the run a second artifact home. A container that commits the research to a branch/file (engineer-style) before/alongside posting means a failed comment still leaves a recoverable artifact on a ref. That is the real durability argument for the container, and it is why "still comment-only, or also leave a branch?" is the load-bearing open question, not a side detail.

4. Design options

Option A - Minimal: detached advisor container that posts from inside (reuse runAgentAsk bring-up).
Add a detached, role=advisor plan variant. Seed it with replyResearchPrompt plus a "post the answer as a comment on this ref, then exit" tail (the ref-mode analog of headlessReflectionAction). Ward fires and forgets. Comment-only, no branch.

  • Pros - smallest diff, reuses the closest existing path, drops the claude|goose-only limit (all four drivers work), gives the clean reproducible tree the issue wants. Isolation win is real.
  • Cons - durability barely improves over native (still one post, now from inside a container whose only other trace is a drained log). Loses the synchronous UX - native ref mode posts before the command returns and prints the URL (agent_reply.go:121), whereas fire-and-forget means the operator watches the issue, not the terminal. The thoroughness timeout has to move from a host context.WithTimeout to a container-lifetime cap.

Option B - Durable: detached advisor container that commits the research artifact AND posts (reuse engineer bring-up).
Copy the engineer path (launchAgentContainerbuildAgentPlan), but with a read-only-plus-comment credential posture and a research seed. The agent writes its research to a file on an advisory-N branch (or a commit), pushes it, then posts the comment referencing it, then emits a WARD-OUTCOME-style line.

  • Pros - directly closes the ward#402 durability gap: a failed post still leaves the research on a pushed ref. Symmetric with engineer, so the director's reconciliation machinery (agent_director.go) can observe advisory runs the same way. Strongest isolation and auditability.
  • Cons - heaviest. It manufactures a branch/commit for what is conceptually a read-only research pass, which muddies "advisor never writes code." Needs a writable-push credential posture on an advisor, cutting against the read-only guardrails in askPrompt/interactivePrompt. Pulls in reservation/branch/reaper concerns ref mode currently sidesteps. Arguably over-built if the only goal is consistency.

Option C - Shared extraction: one roleAdvisor container helper, comment-only, host-side post-with-fallback.
Factor a single advisor-container helper both freeform and ref mode call, parameterized by attached-vs-detached and stream-vs-post. Ref mode runs it detached; the in-container agent researches in the clean tree, then hands the result back and the host posts it (via a broker line or a drained-output contract), with the drained container log as the guaranteed fallback artifact if the post fails. This keeps posting on ward's tested commentIssue path (one place to fix ward#402-class bugs) instead of trusting the model to post correctly.

  • Pros - unifies the two advisor halves behind one bring-up, so freeform and ref share a code path for the first time. Keeps the durable-fallback property (log always has it) without a branch. Post stays on ward's own forge client, so header/footer/marker (replyComment) formatting and any future --body-file fix live in one host-side place rather than in a prompt the model has to obey.
  • Cons - most design work up front (the host-side "stream research back out of a detached container" contract does not exist yet - engineer's model is agent-posts, not host-posts). Risks reintroducing exactly the host-side-post fragility ward#402 was about, unless the fallback is solid.

Resource cost note (applies to all three): every native ref reply today costs a host process and a temp dir for 3-15 minutes. Every containerized advisory instead costs a full image pull (amortized by --no-pull and the local cache), a clone of the repo, container create/run, and a writable layer that the stale-container sweeper (sweepStaleContainers, ward#272) later reclaims. For a quick 3-minute research pass that may not even clone, that is a large fixed overhead per advisory - a container spun up, cloned into, and torn down to run what is often a few-paragraph answer. This is the resource tradeoff to weigh against the consistency/isolation/durability wins, and it argues for at least keeping --no-pull fast-path behavior and possibly a quick-stays-native carve-out if the overhead proves not worth it for the shallow rungs.


Summary for the decision: the container spine is already shared (buildUpPlancreateAgentContainer), so this is a plan-variant + prompt-contract change, not new infrastructure. The three real forks Kai owns are: (1) reuse the freeform-advisor re-cast (lightest, Option A) vs the engineer re-cast (heaviest, Option B) vs a new shared extraction (Option C), (2) post-from-inside (agent posts, engineer-style) vs host-posts-with-fallback (keeps ward#402 fixes in one place), and (3) comment-only vs also-commit-an-artifact, where only the commit path actually closes the durability gap that motivated the issue. Containerizing also incidentally lifts the claude|goose-only host-binary restriction, which is a free win for codex/qwen under any option.


Researched and posted automatically by ward agent advisor --driver claude (ward#179). This is one-shot research, not a carried change - verify before acting on it.

— Claude (she/her), via ward agent

### 🔎 ward agent advisor `ward agent advisor --driver claude` ran a one-shot **standard** research pass on this question: > Grounding research only - do NOT decide the approach (Kai owns that). Read live against /substrate/ward. Deliver: (1) a precise map of how ref mode (runAgentReply/agent_reply.go, host one-shot) and freeform mode (runAgentAsk, ephemeral container) each run today, with file:line; (2) the reuse surface for containerizing ref mode - compare the engineer container bring-up vs runAgentAsk's bring-up, what's shared, what a shared extraction would look like; (3) the 'post a comment from inside a container' question - how engineer emits WARD-OUTCOME and posts, whether ref mode can post its research comment pre-exit the same way, and the failure-durability angle (ward#402); (4) 2-3 concrete design options with tradeoffs and a resource-cost note (a full container per advisory). Post as a comment on ward#411. --- Now I have a complete, precise picture. Writing the research comment. ## 1. How each mode runs today Both modes hang off `runAgentAdvisor` (`cmd/ward/agent_advisor.go:71-80`), which splits purely on whether the first arg parses as an issue ref: **Ref mode** (`owner/repo#N` or issue URL) → `runAgentReply` (`cmd/ward/agent_reply.go:77-123`). This is the native host one-shot: - `validateReplyInputs` (`agent_reply.go:127-161`) parses the ref, joins the tail as the prompt, resolves `--thoroughness` to a `replyThoroughness` rung (`quick`/`standard`/`deep`, with `3m`/`8m`/`15m` timeouts, `agent_reply.go:30-51`), trust-gates the owner against the primary-org set, and asserts the mode has a host one-shot slot. Only `claude` and `goose` qualify - `hostPreflightArgv` (`container_compute.go:195-206`) returns `claude -p <prompt>` / `goose run -t <prompt>`, and `codex`/`qwen` return `false`. It also requires `hostHasBinary(bin)` - the agent CLI must be installed on the host. - It fetches the issue + comment thread host-side (`agent_reply.go:86-94`), builds the research prompt (`replyResearchPrompt`, `agent_reply.go:187-222` - this is the exact prompt you are reading right now), then `captureReplyResearch` (`agent_reply.go:165-183`) runs the argv via `capturePreflight` (`agent.go:612-621`) in a throwaway `os.MkdirTemp` cwd, bounded by the level timeout. - The captured stdout is the artifact. `runAgentReply` gets a `hostForgejoClient` and calls `commentIssue(...)` host-side (`agent_reply.go:114-120`), wrapping the read in a header + `<!-- ward-agent-reply -->` footer (`replyComment`, `agent_reply.go:230-247`). - No container, no clone, no branch. The clone in `replyResearchPrompt` is optional - the in-prompt agent may `git clone` into scratch if depth warrants, but nothing is provisioned. **Freeform mode** (bare quoted question) → `runAgentAsk` (`agent_advisor.go:84-160`). This is a fresh attached ephemeral container: - Resolves `--repo` (or infers from cwd git origin, `agent_advisor.go:96`), trust-gates the owner, decides `oneshot` from `--oneshot || !terminalAttached()` (`agent_advisor.go:109`), and seeds either `interactivePrompt` (default TTY session) or `askPrompt` (`agent_advisor.go:111-114`). - `writeContainerAssets()` + `buildUpPlan(...)` (`agent_advisor.go:116-123`, `container.go:99-146`) build the plan, re-cast as `roleAdvisor` with an issueless machine-id name (`agent_advisor.go:132-133`). `plan.Ask = oneshot` toggles `WARD_ASK`. - It sweeps stale containers, pulls the image, writes the token envfile, and `createAgentContainer` runs it **attached** (`agent_advisor.go:141-159`). Output streams to the terminal. **It never posts a comment** - the answer's only home is the operator's screen. So the two ref/freeform halves of one command already run on two entirely different execution models, and only ref mode produces a durable issue artifact. ## 2. Reuse surface for containerizing ref mode The good news: the container bring-up is already factored, and both existing container paths converge on the same core. `buildAgentPlan` (engineer, `agent.go:939-963`) is literally `buildUpPlan(...)` plus a detached/headless re-cast (`Role=roleEngineer`, `Headless=true`, `Interactive=false`, `TTY=false`, `Branch=issue-N`). `runAgentAsk` (freeform advisor) is the same `buildUpPlan(...)` core plus an attached/ephemeral re-cast (`Role=roleAdvisor`, `plan.Ask`). Both end at `createAgentContainer` (`agent.go:1100`). What a containerized ref mode would share regardless of which parent it copies: - `buildUpPlan` → `createAgentContainer` (the whole plan/mount/pull/envfile spine). - The trust gate (`ownerAllowed`), image flags, `--no-pull`/`--print`, `writeTokenEnvFile`, `sweepStaleContainers`. - The dispatch-broker forwarding it already gets - `maybeForwardAgentDispatchToHostBroker(..., "advisor", ...)` (`agent_advisor.go:74-76`, `agent_dispatch_broker.go:292-297`) already forwards an in-container advisor ref-dispatch to host ward. That layer is orthogonal to whether the host-side execution is native or containerized. **Engineer bring-up vs `runAgentAsk` bring-up - what differs, and which to lean on:** - **Engineer** is detached, headless, writable, branch-carrying, reserves the issue (`launchAgentContainer`, `agent.go:983-1043`), runs a pre-flight, and emits WARD-OUTCOME. That is a lot of machinery ref mode does not want (no reservation contention for a read-only research pass, no `issue-N` branch, no director reconciliation). - **`runAgentAsk`** is the closer relative: ephemeral, role=advisor, no reservation, no branch, read-only intent. The gap is that it runs **attached and streams** rather than **detached and posts**. Its `askPrompt` is a "stream to terminal" contract, not a "post a comment" contract. A clean shared extraction would be: keep `buildUpPlan` as the base (already shared), and give ref mode a third plan re-cast - `roleAdvisor` like freeform, but detached like engineer, carrying the `replyResearchPrompt` as its seed and a new "post the comment yourself" contract. The thoroughness ladder (`agent_reply.go:30-73`) is pure and portable as-is - it just needs to map onto a container timeout instead of a `context.WithTimeout` around a host capture. The one hard constraint that **loosens** under containerization: the `claude|goose`-only restriction and `hostHasBinary` check (`agent_reply.go:153-159`) exist solely because it runs a host one-shot. In a container the agent binary is baked into the image, so ref mode could gain `codex`/`qwen` support for free - the same reason freeform mode already supports all four drivers. ## 3. Posting a comment from inside a container **How engineer does it today:** the container is never asked by ward to post. The in-container agent posts, as the last line of its own seeded instructions. `headlessReflectionAction` (`agent.go:101-115`) tells the agent that its final step is to post a retrospective comment beginning with a `WARD-OUTCOME: done|blocked|failed` line. The agent uses the forge token mounted in the container (the same credential it pushes with) to call the forge. Ward's host side never posts - it later *reads* that comment back (`parseBacklogOutcome`, `agent_director.go:667-711`; `backlogReconcile` parks a container that exited with no WARD-OUTCOME as `failed`, `agent_director.go:980-1000`). **Can ref mode post its research comment the same way?** Yes, structurally. The clean-container equivalent of `askPrompt`/`replyResearchPrompt` would contract: "research this, then post your answer as a comment on `owner/repo#N` and exit." The forge token is already in the container for any writable role, and even a read-only advisor container can post through the credential broker (`start_broker` / `revoke_push_credential`, `entrypoint.sh:885-890`) - the broker keeps commenting ability while dropping push. So a research-and-post-from-inside path is wired-adjacent, not greenfield. **The failure-durability angle (ward#402):** today's native ref post is a single point of failure. The research lives only in host stdout, and the one `commentIssue` call is the only thing that turns it into a durable artifact. That is exactly what the #306/#401 advisor runs hit - the ward#402 `commentIssue --body-file` skew meant the host-side post silently failed and the entire research pass survived only in a host dispatch log. Containerizing changes the durability calculus in two ways worth naming for the decision: - **Post-from-inside does not by itself fix it** - if the in-container post is still one call and it fails, you are back to "only the container log has it," except now the log is a drained container log (`agent_log_drain.go`) rather than a broker log. Marginally more discoverable, not categorically safer. - **What actually hardens it** is giving the run a *second* artifact home. A container that commits the research to a branch/file (engineer-style) before/alongside posting means a failed comment still leaves a recoverable artifact on a ref. That is the real durability argument for the container, and it is why "still comment-only, or also leave a branch?" is the load-bearing open question, not a side detail. ## 4. Design options **Option A - Minimal: detached advisor container that posts from inside (reuse `runAgentAsk` bring-up).** Add a detached, role=advisor plan variant. Seed it with `replyResearchPrompt` plus a "post the answer as a comment on this ref, then exit" tail (the ref-mode analog of `headlessReflectionAction`). Ward fires and forgets. Comment-only, no branch. - **Pros** - smallest diff, reuses the closest existing path, drops the `claude|goose`-only limit (all four drivers work), gives the clean reproducible tree the issue wants. Isolation win is real. - **Cons** - durability barely improves over native (still one post, now from inside a container whose only other trace is a drained log). Loses the synchronous UX - native ref mode posts before the command returns and prints the URL (`agent_reply.go:121`), whereas fire-and-forget means the operator watches the issue, not the terminal. The thoroughness timeout has to move from a host `context.WithTimeout` to a container-lifetime cap. **Option B - Durable: detached advisor container that commits the research artifact AND posts (reuse engineer bring-up).** Copy the engineer path (`launchAgentContainer` → `buildAgentPlan`), but with a read-only-plus-comment credential posture and a research seed. The agent writes its research to a file on an `advisory-N` branch (or a commit), pushes it, then posts the comment referencing it, then emits a WARD-OUTCOME-style line. - **Pros** - directly closes the ward#402 durability gap: a failed post still leaves the research on a pushed ref. Symmetric with engineer, so the director's reconciliation machinery (`agent_director.go`) can observe advisory runs the same way. Strongest isolation and auditability. - **Cons** - heaviest. It manufactures a branch/commit for what is conceptually a read-only research pass, which muddies "advisor never writes code." Needs a writable-push credential posture on an advisor, cutting against the read-only guardrails in `askPrompt`/`interactivePrompt`. Pulls in reservation/branch/reaper concerns ref mode currently sidesteps. Arguably over-built if the only goal is consistency. **Option C - Shared extraction: one `roleAdvisor` container helper, comment-only, host-side post-with-fallback.** Factor a single advisor-container helper both freeform and ref mode call, parameterized by attached-vs-detached and stream-vs-post. Ref mode runs it detached; the in-container agent researches in the clean tree, then hands the result back and the **host** posts it (via a broker line or a drained-output contract), with the drained container log as the guaranteed fallback artifact if the post fails. This keeps posting on ward's tested `commentIssue` path (one place to fix ward#402-class bugs) instead of trusting the model to post correctly. - **Pros** - unifies the two advisor halves behind one bring-up, so freeform and ref share a code path for the first time. Keeps the durable-fallback property (log always has it) without a branch. Post stays on ward's own forge client, so header/footer/marker (`replyComment`) formatting and any future `--body-file` fix live in one host-side place rather than in a prompt the model has to obey. - **Cons** - most design work up front (the host-side "stream research back out of a detached container" contract does not exist yet - engineer's model is agent-posts, not host-posts). Risks reintroducing exactly the host-side-post fragility ward#402 was about, unless the fallback is solid. **Resource cost note (applies to all three):** every native ref reply today costs a host process and a temp dir for 3-15 minutes. Every containerized advisory instead costs a full image pull (amortized by `--no-pull` and the local cache), a clone of the repo, container create/run, and a writable layer that the stale-container sweeper (`sweepStaleContainers`, ward#272) later reclaims. For a `quick` 3-minute research pass that may not even clone, that is a large fixed overhead per advisory - a container spun up, cloned into, and torn down to run what is often a few-paragraph answer. This is the resource tradeoff to weigh against the consistency/isolation/durability wins, and it argues for at least keeping `--no-pull` fast-path behavior and possibly a `quick`-stays-native carve-out if the overhead proves not worth it for the shallow rungs. --- **Summary for the decision:** the container spine is already shared (`buildUpPlan` → `createAgentContainer`), so this is a plan-variant + prompt-contract change, not new infrastructure. The three real forks Kai owns are: (1) reuse the **freeform-advisor** re-cast (lightest, Option A) vs the **engineer** re-cast (heaviest, Option B) vs a **new shared extraction** (Option C), (2) **post-from-inside** (agent posts, engineer-style) vs **host-posts-with-fallback** (keeps ward#402 fixes in one place), and (3) **comment-only** vs **also-commit-an-artifact**, where only the commit path actually closes the durability gap that motivated the issue. Containerizing also incidentally lifts the `claude|goose`-only host-binary restriction, which is a free win for `codex`/`qwen` under any option. --- Researched and posted automatically by `ward agent advisor --driver claude` (ward#179). This is one-shot research, not a carried change - verify before acting on it. <!-- ward-agent-reply --> <!-- ward-agent-signature --> — Claude (she/her), via `ward agent`
Member

Labeled headless/P3: direction already stated in the body (containerize ref-mode advisor like the other agent modes). Recorded by Claude Code (Fable) during the 2026-07-01 ward launch triage session with Kai.

Labeled headless/P3: direction already stated in the body (containerize ref-mode advisor like the other agent modes). Recorded by Claude Code (Fable) during the 2026-07-01 ward launch triage session with Kai.
Member

🔒 Reserved by ward agent --driver claude — container engineer-claude-ward-411 on host KAI-DESKTOP-TOWER is carrying this issue (reserved 2026-07-03T06:44:25Z). 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 --driver claude` — container `engineer-claude-ward-411` on host `KAI-DESKTOP-TOWER` is carrying this issue (reserved 2026-07-03T06:44:25Z). 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`
Member

🔒 Reserved by ward agent --driver claude — container engineer-claude-ward-411 on host KAI-DESKTOP-TOWER is carrying this issue (reserved 2026-07-03T07:19:32Z). 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 --driver claude` — container `engineer-claude-ward-411` on host `KAI-DESKTOP-TOWER` is carrying this issue (reserved 2026-07-03T07:19:32Z). 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
Owner

WARD-OUTCOME: done - ref-mode advisor now researches in an ephemeral container (host captures its stdout, host-side ward#424 fan-out unchanged). Landed on main, closes #411.

Honest retro: this read as a big architectural fork but collapsed into a small, low-risk change once I traced the primitives. The WARD_ASK freeform container already does most of the work. claude -p puts the answer on stdout while every bring-up line goes to stderr, so an attached docker run -i captured through the existing dockerCapture hands back exactly the research text. No entrypoint changes, no new capture machinery.

The one real design call (named in the commit): only containerize the execution and keep the cross-repo fan-out deterministic host-side. The container researches and streams stdout back, it never posts. That is the research pass's Option C, not "post from inside", because moving fan-out into an in-container agent would trade away the trust-gating for nothing.

What fought back was not the Go but the doc-size cap. agent-advisor.md sat 58 chars under the 4000 limit before I touched it, and the mandatory full-URL issue refs run ~68 chars each, so I spent a few rounds tightening prose back under. Confidence is high on the plan-shaping and gating (unit-tested, --print renders the new container framing correctly, all hooks green), medium on the live capture path. I could not safely spin a real nested container from in here, so end-to-end capture is reasoned-and-tested rather than observed.

Follow-ups worth filing: ward#402's durability gap is only half-closed (a failed post still loses the research to the container log rather than a branch artifact, which the research pass's Option B would fully close), and now that any wired harness can run ref mode in the sandbox someone should confirm a local model actually honors the structured-JSON emit contract.

WARD-OUTCOME: done - ref-mode advisor now researches in an ephemeral container (host captures its stdout, host-side ward#424 fan-out unchanged). Landed on main, closes #411. Honest retro: this read as a big architectural fork but collapsed into a small, low-risk change once I traced the primitives. The WARD_ASK freeform container already does most of the work. `claude -p` puts the answer on stdout while every bring-up line goes to stderr, so an attached `docker run -i` captured through the existing dockerCapture hands back exactly the research text. No entrypoint changes, no new capture machinery. The one real design call (named in the commit): only containerize the execution and keep the cross-repo fan-out deterministic host-side. The container researches and streams stdout back, it never posts. That is the research pass's Option C, not "post from inside", because moving fan-out into an in-container agent would trade away the trust-gating for nothing. What fought back was not the Go but the doc-size cap. agent-advisor.md sat 58 chars under the 4000 limit before I touched it, and the mandatory full-URL issue refs run ~68 chars each, so I spent a few rounds tightening prose back under. Confidence is high on the plan-shaping and gating (unit-tested, `--print` renders the new container framing correctly, all hooks green), medium on the live capture path. I could not safely spin a real nested container from in here, so end-to-end capture is reasoned-and-tested rather than observed. Follow-ups worth filing: ward#402's durability gap is only half-closed (a failed post still loses the research to the container log rather than a branch artifact, which the research pass's Option B would fully close), and now that any wired harness can run ref mode in the sandbox someone should confirm a local model actually honors the structured-JSON emit contract.
Sign in to join this conversation.
No milestone
No project
No assignees
2 participants
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#411
No description provided.