design: default-headless intake + tri-role relabel-down discipline #619

Open
opened 2026-07-06 16:58:20 +00:00 by coilysiren · 3 comments
Owner

Intent (Kai)

Flip the intake default so the director files issues headless by default, instead of today's fail-closed "unlabeled -> consult". Kai's own guardrail: default-headless is only safe if it is paired with a relabel-down discipline - engineers and advisors must learn to relabel an issue down (headless -> interactive/consult) when they determine it cannot actually be carried autonomously. Without that, "everything ends up incorrectly headless."

This is a design brief, not an implementation ticket. Post a concrete plan as a comment for Kai to approve before any implementation dispatches.

Current state (fail-closed - this inverts it)

  • Ceiling (cmd/ward/agent.go:583-626, ward#607 / agentic-os#246): modeCeilingLevels = [consult, interactive, headless]; an unlabeled issue is consult (unlabeled default), the floor. agentSurfaceCeiling gates only the engineer role at headless; director and advisor are ungated.
  • Director triage (cmd/ward/agent_director_triage.go): a startup pass surveys open issues missing a mode label and promotes only a confident headless/interactive; triageMode fails closed to consult otherwise. The consult/interactive/headless definitions the survey uses are the prompt at lines 171-176 - reuse them as the shared vocabulary.
  • Filing paths that would need the default: the director read-only surface capture-and-dispatch (broker OpFileIssue, currently stamps no label - this is what forces --force today), and the freeform intake that files-then-carries (cmd/ward/agent_route.go).

Design questions to resolve in the plan

  1. Scope of "headless by default" - which filing paths stamp headless at creation, and how it interacts with the existing triageMode pass. Does default-headless replace fail-closed triage, or only seed the label that triage/agents then correct? Name the interaction explicitly - do not leave two systems fighting over the same label.
  2. Downgrade criteria - the concrete, checkable signals an engineer/advisor uses to decide "not headless": needs a human decision or product call, ambiguous/underspecified requirements, destructive or irreversible, a genuine multi-path fork, or missing access. Anchor them to the existing consult/interactive definitions (triage prompt lines 171-176) so the vocabulary stays single-sourced.
  3. Downgrade mechanics + escalation - remove headless, set interactive or consult; who acts (an engineer that hits the wall mid-run; an advisor whose research concludes the issue is human-gated); and the escalation shape (post a comment explaining why it was downgraded, then stop - never --force a bad autonomous change past the wall).
  4. Where the per-role instruction lives so all three roles reliably read it - the ward seed prompt (agentSeedPrompt/role bodies in agent.go, agent_engineer.go, agent_advisor.go), the ward AGENTS.md container doctrine, or a skill. Capability check: engineers/advisors hold a write-tier token directly (not the broker), and the ward-kdl write guardfile already grants can add/set/remove issue-label - confirm the advisor role's tier actually includes issue-label add/remove, and flag it if not.
  5. Safe decomposition - Kai's framing implies a prerequisite ordering. Piece 1: the tri-role relabel-down doctrine + capability, which is additive and safe and good even under today's fail-closed default. Piece 2: the director default-headless inversion, which only becomes safe once Piece 1 exists. Recommend the sequencing and the concrete follow-up engineer issues (files, exact doctrine text).

Dependency note

The director read-only surface relabeling through the broker needs cli-guard#193 / ward#618 (the broker label op). Engineers and advisors do not - they relabel directly with their own token. So Piece 1 does not block on the broker work; call that out so the plan does not over-couple.

Deliverable

A comment carrying: the resolved answers to the five questions above, the recommended decomposition into landable engineer issues (with the exact doctrine text to add and the files each touches), and an explicit call on whether the fail-closed -> fail-open inversion is worth it or whether a middle path (e.g. default-headless only for a trusted intake class) is safer.

## Intent (Kai) Flip the intake default so the **director files issues `headless` by default**, instead of today's fail-closed "unlabeled -> consult". Kai's own guardrail: default-headless is only safe if it is paired with a **relabel-down discipline** - **engineers and advisors must learn to relabel an issue down** (headless -> interactive/consult) when they determine it cannot actually be carried autonomously. Without that, "everything ends up incorrectly headless." This is a **design brief, not an implementation ticket.** Post a concrete plan as a comment for Kai to approve before any implementation dispatches. ## Current state (fail-closed - this inverts it) - **Ceiling** (`cmd/ward/agent.go:583-626`, ward#607 / agentic-os#246): `modeCeilingLevels = [consult, interactive, headless]`; an unlabeled issue is `consult (unlabeled default)`, the floor. `agentSurfaceCeiling` gates **only the engineer** role at `headless`; **director and advisor are ungated**. - **Director triage** (`cmd/ward/agent_director_triage.go`): a startup pass surveys open issues missing a mode label and promotes only a *confident* headless/interactive; `triageMode` fails closed to consult otherwise. The consult/interactive/headless definitions the survey uses are the prompt at lines 171-176 - reuse them as the shared vocabulary. - **Filing paths that would need the default**: the director read-only surface capture-and-dispatch (broker `OpFileIssue`, currently stamps no label - this is what forces `--force` today), and the freeform intake that files-then-carries (`cmd/ward/agent_route.go`). ## Design questions to resolve in the plan 1. **Scope of "headless by default"** - which filing paths stamp `headless` at creation, and how it interacts with the existing `triageMode` pass. Does default-headless *replace* fail-closed triage, or only seed the label that triage/agents then correct? Name the interaction explicitly - do not leave two systems fighting over the same label. 2. **Downgrade criteria** - the concrete, checkable signals an engineer/advisor uses to decide "not headless": needs a human decision or product call, ambiguous/underspecified requirements, destructive or irreversible, a genuine multi-path fork, or missing access. Anchor them to the existing consult/interactive definitions (triage prompt lines 171-176) so the vocabulary stays single-sourced. 3. **Downgrade mechanics + escalation** - remove `headless`, set `interactive` or `consult`; **who** acts (an engineer that hits the wall mid-run; an advisor whose research concludes the issue is human-gated); and the escalation shape (post a comment explaining why it was downgraded, then **stop** - never `--force` a bad autonomous change past the wall). 4. **Where the per-role instruction lives** so all three roles reliably read it - the ward seed prompt (`agentSeedPrompt`/role bodies in `agent.go`, `agent_engineer.go`, `agent_advisor.go`), the ward `AGENTS.md` container doctrine, or a skill. **Capability check:** engineers/advisors hold a write-tier token directly (not the broker), and the ward-kdl write guardfile already grants `can add/set/remove issue-label` - confirm the advisor role's tier actually includes issue-label add/remove, and flag it if not. 5. **Safe decomposition** - Kai's framing implies a prerequisite ordering. Piece 1: the tri-role relabel-down doctrine + capability, which is additive and safe and good even under today's fail-closed default. Piece 2: the director default-headless inversion, which only becomes safe once Piece 1 exists. Recommend the sequencing and the concrete follow-up engineer issues (files, exact doctrine text). ## Dependency note The director *read-only surface* relabeling through the broker needs cli-guard#193 / ward#618 (the broker label op). **Engineers and advisors do not** - they relabel directly with their own token. So Piece 1 does not block on the broker work; call that out so the plan does not over-couple. ## Deliverable A comment carrying: the resolved answers to the five questions above, the recommended decomposition into landable engineer issues (with the exact doctrine text to add and the files each touches), and an explicit call on whether the fail-closed -> fail-open inversion is worth it or whether a middle path (e.g. default-headless only for a trusted intake class) is safer.
Member

🔎 ward agent advisor

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

Design the change described in this issue: default-headless intake paired with a tri-role (director/engineer/advisor) relabel-down discipline. Resolve the five design questions in the body, confirm the advisor role's tier permits issue-label add/remove, and post a concrete implementation plan as a comment - the landable engineer issues with exact doctrine text and files each touches, plus your call on whether the fail-closed to fail-open inversion is worth it vs a safer middle path. Do not implement; design only.


Design: default-headless intake + tri-role relabel-down discipline

TL;DR recommendation: land the relabel-down discipline (Piece 1) first and unconditionally - it is additive and correct even under today's fail-closed default. For Piece 2, do not flip the broad LLM-judged backlog default to fail-open. Take the middle path: default-headless only for the trusted intake class (issues the director itself files in the act of dispatching them), and leave the backlog-survey (triageMode) fail-closed. That captures Kai's intent ("the director files issues headless by default") on exactly the issues where headless is safe, without headless-stamping a heterogeneous backlog the moment the discipline ships.

Capability confirmed (the one blocking question): yes - both the engineer and the advisor in-container hold a write-capable FORGEJO_TOKEN and the embedded ward ops forgejo surface includes add/set/remove issue-label (cmd/ward/opsassets/forgejo.guardfile.generated.kdl:247-249, mirroring the write guardfile at .ward/ward-kdl/ward-kdl-write/ward-kdl.forgejo.write.guardfile.kdl:70-74). The advisor container is filesystem-read-only (plan.ReadOnly = true, no git push wiring) but that removes only code-push, not forge-label capability - a label add/remove is a forge API call through ward ops forgejo, not a git push. So an advisor can relabel down and comment with no code change. The only path that cannot relabel is the director read-only surface via the broker (writeTierOps in cmd/ward/broker_exec.go:160-165 is FileIssue/EditIssue/CommentIssue/Dispatch only - no label op), which is exactly the cli-guard#193 / ward#618 dependency the issue flags.


Answers to the five design questions

1. Scope of "headless by default" - and the two-systems interaction

There are three distinct filing/labeling surfaces today, and they must not fight over the label:

  • Startup backlog triage (agent_director_triage.go) - a host-side LLM survey that promotes only a confident headless/interactive and triageMode fails closed to consult (lines 85-92). Uses cl.addIssueLabels directly, fully capable.
  • Freeform route intake (agent_route.go) - files an intake record + a scoped child via the host client (signed.createIssue), then carries the child headless. It stamps no mode label on the child today.
  • Director read-only surface capture-and-dispatch - files via the broker OpFileIssue, which stamps no label (this is what forces --force on the follow-on engineer dispatch, since issueModeCeiling reads unlabeled as consult and the engineer ceiling gate wants headless).

Named interaction (no fighting): keep issueModeCeiling's unlabeled default fail-closed to consult as a permanent defense-in-depth floor (an issue that somehow escapes labeling stays gated, never silently headless). Default-headless is a seed at creation on the filing paths, not a change to how the ceiling reads an unlabeled issue. Agents are the only downward-correction mechanism after that. This gives one writer per moment (filing stamps headless, agents relabel down) and one reader (the ceiling gate), never two systems racing on the same label.

Recommendation on which paths stamp headless:

  • Now (no dependency): the freeform-route child (agent_route.go, host-side addIssueLabels right after the child is created) - it is already carried headless, so labeling it headless just makes the forge state honest and removes the --force need on any re-dispatch.
  • After ward#618 / cli-guard#193: the director-surface capture-and-dispatch, once the broker gains a label op.
  • Leave alone: triageMode's backlog default. Do not invert it to fail-open (see the verdict below).

2. Downgrade criteria (single-sourced to the triage vocabulary)

The canonical consult/interactive/headless definitions live in triagePrompt (agent_director_triage.go:170-176). Extract them into an exported Go const (modeLadderDefs) so triagePrompt, the seed prompt, and the doctrine all cite one source. An engineer/advisor downgrades out of headless when any of these hold (each anchored to a ladder rung):

  • -> consult - a human must decide/design/act first: ambiguous or underspecified requirements, a product/design call, or missing access/credentials.
  • -> interactive - the work is doable by an agent but must pause at a human checkpoint: a design choice mid-implementation, a destructive/irreversible step, or a human-only verification.
  • Either (pick the rung that fits) - a genuine multi-path fork where the wrong choice is costly to undo.

These are the same signals the container doctrine already treats as walls ("run until a wall worth a human"); relabel-down is what an agent does at that wall instead of --force-ing past it.

3. Downgrade mechanics + escalation

Who acts:

  • an engineer that hits the wall mid-run (discovers the ambiguity/fork/missing-access only once working the code);
  • an advisor whose research concludes the issue is human-gated.

Mechanics (in-container, token already present, no broker):

ward ops forgejo issue-label remove <owner> <repo> <N> --labels headless
ward ops forgejo issue-label add    <owner> <repo> <N> --labels interactive   # or: consult

(On a GitHub target: gh issue edit <N> --remove-label headless --add-label interactive.)

Escalation shape: post a comment naming the exact signal that made it un-headless, then stop - do not commit a partial/speculative change to "make progress", and never --force past the gate. A downgraded issue is a handoff to a human, and the comment is that handoff. This is a floor-only move: relabel down only. Relabeling up toward headless to clear the dispatch gate for yourself is forbidden - raising the ceiling is the director's or a human's call.

4. Where the per-role instruction lives

One primary home, read by all three roles, plus thin pointers:

  • Primary: a new section in cmd/ward/containerassets/AGENTS.container.md (the embedded container doctrine mounted into every role's container by writeContainerAssets). Exact text below. This is the one file all three roles reliably read.
  • Vocabulary single-source: inject the extracted modeLadderDefs const into the seed via agentSeedPromptWorkflow (cmd/ward/agent.go) for engineer/direct headless runs, and into askPrompt/interactivePrompt/replyResearchPrompt (agent_advisor.go, agent_reply.go) for the advisor, so the ladder definitions are never re-authored in prose.
  • Reference: docs/agent-dispatch-contract.md gets a cross-link, and a short docs/relabel-down.md walkthrough completes the docs trifecta.

Capability check result: no guardfile change needed. Both engineer and advisor already carry the issue-label add/set/remove surface + a live token in-container (confirmed above). The doctrine just needs to hand them the exact command. Flag: the director surface (broker) genuinely lacks the op - that stays a ward#618 item and is out of scope for Piece 1.

5. Safe decomposition (prerequisite ordering)

Kai's framing is right: Piece 1 gates Piece 2. Recommended landable engineer issues, in dependency order:

Issue A - Piece 1: tri-role relabel-down doctrine + single-sourced vocabulary (no dependency; safe and good even today)

  • cmd/ward/agent_director_triage.go - extract lines 170-176 into const modeLadderDefs; triagePrompt references it (pure refactor, no behavior change).
  • cmd/ward/containerassets/AGENTS.container.md - add the "Relabel an issue down" section (exact text below).
  • cmd/ward/agent.go - agentSeedPromptWorkflow injects modeLadderDefs + a one-line relabel-down pointer on headless runs.
  • cmd/ward/agent_advisor.go + cmd/ward/agent_reply.go - add the relabel-down clause to askPrompt/interactivePrompt/replyResearchPrompt so the advisor relabels down + comments when research concludes human-gated.
  • docs/agent-dispatch-contract.md (cross-link) + new docs/relabel-down.md; update docs/FEATURES.md.
  • Tests: extend agent_test.go / agent_director_test.go to assert the seed carries the relabel-down clause and the extracted const.

Issue B - Piece 2 (middle path): stamp headless on the director's own intake (depends on A)

  • cmd/ward/agent_route.go - after the child issue is created (line 134), signed.addIssueLabels(ctx, target.Owner, target.Name, childNum, []string{"headless"}), best-effort like triage.
  • Explicitly leave triageMode fail-closed - add a code comment recording that this is deliberate (the middle-path call), so a later reader does not "finish the inversion" by accident.
  • Tests: assert the route child is stamped headless.

Issue C - cli-guard: broker label op (separate repo; unblocks the director-surface capture path only)

  • coilyco-flight-deck/cli-guard - add a label op to the broker Op set + Executor/Authorizer (cli-guard#193). Downstream ward#618 then wires broker_exec.go writeTierOps to serve it and the director surface stamps headless at capture. Piece 1 does not wait on this - engineers and advisors relabel with their own token, per the dependency note.

Exact doctrine text for AGENTS.container.md

Add after the "Where the wall still is" section:

## Relabel an issue down when you can't carry it headless

The director now files issues **`headless` by default** - intake assumes an agent
can carry the issue from open to merged with no human. That default is only safe
because **you are expected to correct it.** If you determine an issue you were
dispatched on (or, as an advisor, one you researched) **cannot** actually be
carried autonomously, **relabel it down and stop** - never `--force` a bad
autonomous change past the gate.

The mode ladder, high to low autonomy:

- **headless** - self-contained code/docs/config, clear enough to act on now, no
  pending design call, no missing access, no destructive or externally-visible
  production step.
- **interactive** - an agent does the work but must pause at a human checkpoint:
  a design choice, a destructive/irreversible step, or a human-only verification.
- **consult** - a human must decide, design, or act first: ambiguous or
  underspecified requirements, a product call, or missing access.

Downgrade out of `headless` the moment you hit any of: a decision only a
human/product owner can make, requirements too ambiguous to implement without
guessing, a destructive or irreversible step, a genuine multi-path fork where the
wrong choice is costly, or missing access/credentials.

**How to relabel down** (your container already holds a forge token with this
capability - no broker needed):

    ward ops forgejo issue-label remove <owner> <repo> <N> --labels headless
    ward ops forgejo issue-label add    <owner> <repo> <N> --labels interactive   # or: consult

On a GitHub target: `gh issue edit <N> --remove-label headless --add-label interactive`.

Then **post a comment** naming the exact signal that made it un-headless, and
**stop**. Do not commit a partial or speculative change to "make progress" - a
downgraded issue is a handoff to a human, and the comment is that handoff.

**This is a floor-only move.** Relabel an issue *down* (less autonomy) when you
find it human-gated. Do **not** relabel it *up* toward headless to clear the
dispatch gate for yourself - raising the ceiling is the director's or a human's
call, and `--force` is never your escape hatch past a wall.

The advisor is read-only for code, not for the forge - it may add/remove a label and comment. Frame the advisor's clause accordingly ("if your research concludes the issue is human-gated, relabel it down and say why in your posted answer").


The verdict: is the fail-closed -> fail-open inversion worth it?

Partially - take the middle path, not the full inversion.

  • Worth it, do it: default-headless on the trusted intake class - issues the director files in the act of dispatching them (freeform route child now; capture-and-dispatch after ward#618). These are fresh, scoped, and dispatch-intended by construction, so headless is the honest label and stamping it removes today's --force friction. This is the core of Kai's intent and it is safe once Piece 1 exists.
  • Not worth it, skip it: flipping triageMode's backlog-survey default from consult to headless. That is a fail-open change over a large heterogeneous backlog judged by a low-confidence LLM pass - precisely the "everything ends up incorrectly headless" failure Kai names, and the relabel-down discipline only catches issues an agent actually gets dispatched on, not the long tail that sits labeled-but-untouched. Keep the confident-promotion behavior; keep the unconfident default at consult.

Net: the inversion is right where the label is a byproduct of a fresh, deliberate dispatch decision, and wrong where it is a bulk guess about dormant backlog. Piece 1 (relabel-down) is unconditionally worth landing first; Piece 2 should be scoped to the trusted intake class only.


Awaiting Kai's approval before dispatching implementation. On approval, the recommended firing order is Issue A, then Issue B, with Issue C (cli-guard#193 / ward#618) tracked in parallel since Piece 1 does not block on it.


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: > Design the change described in this issue: default-headless intake paired with a tri-role (director/engineer/advisor) relabel-down discipline. Resolve the five design questions in the body, confirm the advisor role's tier permits issue-label add/remove, and post a concrete implementation plan as a comment - the landable engineer issues with exact doctrine text and files each touches, plus your call on whether the fail-closed to fail-open inversion is worth it vs a safer middle path. Do not implement; design only. --- # Design: default-headless intake + tri-role relabel-down discipline **TL;DR recommendation:** land the relabel-down discipline (Piece 1) first and unconditionally - it is additive and correct even under today's fail-closed default. For Piece 2, do **not** flip the broad LLM-judged backlog default to fail-open. Take the **middle path**: default-headless only for the *trusted intake class* (issues the director itself files in the act of dispatching them), and leave the backlog-survey (`triageMode`) fail-closed. That captures Kai's intent ("the director files issues headless by default") on exactly the issues where headless is safe, without headless-stamping a heterogeneous backlog the moment the discipline ships. **Capability confirmed (the one blocking question):** yes - both the **engineer** and the **advisor** in-container hold a write-capable `FORGEJO_TOKEN` and the embedded `ward ops forgejo` surface includes `add`/`set`/`remove issue-label` (`cmd/ward/opsassets/forgejo.guardfile.generated.kdl:247-249`, mirroring the write guardfile at `.ward/ward-kdl/ward-kdl-write/ward-kdl.forgejo.write.guardfile.kdl:70-74`). The advisor container is filesystem-read-only (`plan.ReadOnly = true`, no git push wiring) but that removes only code-push, **not** forge-label capability - a label add/remove is a forge API call through `ward ops forgejo`, not a `git push`. So an advisor can relabel down and comment with no code change. **The only path that cannot relabel is the director read-only *surface* via the broker** (`writeTierOps` in `cmd/ward/broker_exec.go:160-165` is FileIssue/EditIssue/CommentIssue/Dispatch only - no label op), which is exactly the cli-guard#193 / ward#618 dependency the issue flags. --- ## Answers to the five design questions ### 1. Scope of "headless by default" - and the two-systems interaction There are three distinct filing/labeling surfaces today, and they must **not** fight over the label: * **Startup backlog triage** (`agent_director_triage.go`) - a host-side LLM survey that promotes only a *confident* headless/interactive and `triageMode` fails closed to consult (lines 85-92). Uses `cl.addIssueLabels` directly, fully capable. * **Freeform route intake** (`agent_route.go`) - files an intake record + a scoped child via the **host** client (`signed.createIssue`), then carries the child headless. It stamps **no** mode label on the child today. * **Director read-only surface capture-and-dispatch** - files via the **broker** `OpFileIssue`, which stamps no label (this is what forces `--force` on the follow-on engineer dispatch, since `issueModeCeiling` reads unlabeled as `consult` and the engineer ceiling gate wants `headless`). **Named interaction (no fighting):** keep `issueModeCeiling`'s unlabeled default fail-closed to `consult` as a permanent **defense-in-depth floor** (an issue that somehow escapes labeling stays gated, never silently headless). Default-headless is a **seed at creation** on the filing paths, not a change to how the ceiling reads an *unlabeled* issue. Agents are the **only** downward-correction mechanism after that. This gives one writer per moment (filing stamps headless, agents relabel down) and one reader (the ceiling gate), never two systems racing on the same label. **Recommendation on which paths stamp headless:** - **Now (no dependency):** the freeform-route child (`agent_route.go`, host-side `addIssueLabels` right after the child is created) - it is already carried headless, so labeling it headless just makes the forge state honest and removes the `--force` need on any re-dispatch. - **After ward#618 / cli-guard#193:** the director-surface capture-and-dispatch, once the broker gains a label op. - **Leave alone:** `triageMode`'s backlog default. Do not invert it to fail-open (see the verdict below). ### 2. Downgrade criteria (single-sourced to the triage vocabulary) The canonical consult/interactive/headless definitions live in `triagePrompt` (`agent_director_triage.go:170-176`). Extract them into an exported Go const (`modeLadderDefs`) so `triagePrompt`, the seed prompt, and the doctrine all cite one source. An engineer/advisor downgrades out of `headless` when **any** of these hold (each anchored to a ladder rung): * **-> consult** - a human must decide/design/act first: ambiguous or underspecified requirements, a product/design call, or missing access/credentials. * **-> interactive** - the work is doable by an agent but must pause at a human checkpoint: a design choice mid-implementation, a destructive/irreversible step, or a human-only verification. * **Either (pick the rung that fits)** - a genuine multi-path fork where the wrong choice is costly to undo. These are the same signals the container doctrine already treats as walls ("run until a wall worth a human"); relabel-down is what an agent *does* at that wall instead of `--force`-ing past it. ### 3. Downgrade mechanics + escalation **Who acts:** * an **engineer** that hits the wall mid-run (discovers the ambiguity/fork/missing-access only once working the code); * an **advisor** whose research concludes the issue is human-gated. **Mechanics** (in-container, token already present, no broker): ``` ward ops forgejo issue-label remove <owner> <repo> <N> --labels headless ward ops forgejo issue-label add <owner> <repo> <N> --labels interactive # or: consult ``` (On a GitHub target: `gh issue edit <N> --remove-label headless --add-label interactive`.) **Escalation shape:** post a comment naming the **exact** signal that made it un-headless, then **stop** - do not commit a partial/speculative change to "make progress", and **never `--force`** past the gate. A downgraded issue is a handoff to a human, and the comment is that handoff. This is a **floor-only** move: relabel *down* only. Relabeling *up* toward headless to clear the dispatch gate for yourself is forbidden - raising the ceiling is the director's or a human's call. ### 4. Where the per-role instruction lives One primary home, read by all three roles, plus thin pointers: * **Primary:** a new section in **`cmd/ward/containerassets/AGENTS.container.md`** (the embedded container doctrine mounted into every role's container by `writeContainerAssets`). Exact text below. This is the one file all three roles reliably read. * **Vocabulary single-source:** inject the extracted `modeLadderDefs` const into the seed via `agentSeedPromptWorkflow` (`cmd/ward/agent.go`) for engineer/direct headless runs, and into `askPrompt`/`interactivePrompt`/`replyResearchPrompt` (`agent_advisor.go`, `agent_reply.go`) for the advisor, so the ladder definitions are never re-authored in prose. * **Reference:** `docs/agent-dispatch-contract.md` gets a cross-link, and a short `docs/relabel-down.md` walkthrough completes the docs trifecta. **Capability check result:** no guardfile change needed. Both engineer and advisor already carry the `issue-label add/set/remove` surface + a live token in-container (confirmed above). The doctrine just needs to hand them the exact command. Flag: the **director surface** (broker) genuinely lacks the op - that stays a ward#618 item and is out of scope for Piece 1. ### 5. Safe decomposition (prerequisite ordering) Kai's framing is right: Piece 1 gates Piece 2. Recommended landable engineer issues, in dependency order: **Issue A - Piece 1: tri-role relabel-down doctrine + single-sourced vocabulary** *(no dependency; safe and good even today)* * `cmd/ward/agent_director_triage.go` - extract lines 170-176 into `const modeLadderDefs`; `triagePrompt` references it (pure refactor, no behavior change). * `cmd/ward/containerassets/AGENTS.container.md` - add the "Relabel an issue down" section (exact text below). * `cmd/ward/agent.go` - `agentSeedPromptWorkflow` injects `modeLadderDefs` + a one-line relabel-down pointer on headless runs. * `cmd/ward/agent_advisor.go` + `cmd/ward/agent_reply.go` - add the relabel-down clause to `askPrompt`/`interactivePrompt`/`replyResearchPrompt` so the advisor relabels down + comments when research concludes human-gated. * `docs/agent-dispatch-contract.md` (cross-link) + new `docs/relabel-down.md`; update `docs/FEATURES.md`. * Tests: extend `agent_test.go` / `agent_director_test.go` to assert the seed carries the relabel-down clause and the extracted const. **Issue B - Piece 2 (middle path): stamp headless on the director's own intake** *(depends on A)* * `cmd/ward/agent_route.go` - after the child issue is created (line 134), `signed.addIssueLabels(ctx, target.Owner, target.Name, childNum, []string{"headless"})`, best-effort like triage. * Explicitly **leave `triageMode` fail-closed** - add a code comment recording that this is deliberate (the middle-path call), so a later reader does not "finish the inversion" by accident. * Tests: assert the route child is stamped headless. **Issue C - cli-guard: broker label op** *(separate repo; unblocks the director-surface capture path only)* * `coilyco-flight-deck/cli-guard` - add a label op to the broker `Op` set + Executor/Authorizer (cli-guard#193). Downstream ward#618 then wires `broker_exec.go` `writeTierOps` to serve it and the director surface stamps headless at capture. **Piece 1 does not wait on this** - engineers and advisors relabel with their own token, per the dependency note. --- ## Exact doctrine text for `AGENTS.container.md` Add after the "Where the wall still is" section: ```markdown ## Relabel an issue down when you can't carry it headless The director now files issues **`headless` by default** - intake assumes an agent can carry the issue from open to merged with no human. That default is only safe because **you are expected to correct it.** If you determine an issue you were dispatched on (or, as an advisor, one you researched) **cannot** actually be carried autonomously, **relabel it down and stop** - never `--force` a bad autonomous change past the gate. The mode ladder, high to low autonomy: - **headless** - self-contained code/docs/config, clear enough to act on now, no pending design call, no missing access, no destructive or externally-visible production step. - **interactive** - an agent does the work but must pause at a human checkpoint: a design choice, a destructive/irreversible step, or a human-only verification. - **consult** - a human must decide, design, or act first: ambiguous or underspecified requirements, a product call, or missing access. Downgrade out of `headless` the moment you hit any of: a decision only a human/product owner can make, requirements too ambiguous to implement without guessing, a destructive or irreversible step, a genuine multi-path fork where the wrong choice is costly, or missing access/credentials. **How to relabel down** (your container already holds a forge token with this capability - no broker needed): ward ops forgejo issue-label remove <owner> <repo> <N> --labels headless ward ops forgejo issue-label add <owner> <repo> <N> --labels interactive # or: consult On a GitHub target: `gh issue edit <N> --remove-label headless --add-label interactive`. Then **post a comment** naming the exact signal that made it un-headless, and **stop**. Do not commit a partial or speculative change to "make progress" - a downgraded issue is a handoff to a human, and the comment is that handoff. **This is a floor-only move.** Relabel an issue *down* (less autonomy) when you find it human-gated. Do **not** relabel it *up* toward headless to clear the dispatch gate for yourself - raising the ceiling is the director's or a human's call, and `--force` is never your escape hatch past a wall. ``` The advisor is read-only for **code**, not for the forge - it may add/remove a label and comment. Frame the advisor's clause accordingly ("if your research concludes the issue is human-gated, relabel it down and say why in your posted answer"). --- ## The verdict: is the fail-closed -> fail-open inversion worth it? **Partially - take the middle path, not the full inversion.** * **Worth it, do it:** default-headless on the **trusted intake class** - issues the director files *in the act of dispatching them* (freeform route child now; capture-and-dispatch after ward#618). These are fresh, scoped, and dispatch-intended by construction, so headless is the honest label and stamping it removes today's `--force` friction. This is the core of Kai's intent and it is safe once Piece 1 exists. * **Not worth it, skip it:** flipping `triageMode`'s **backlog-survey** default from consult to headless. That is a fail-open change over a large heterogeneous backlog judged by a low-confidence LLM pass - precisely the "everything ends up incorrectly headless" failure Kai names, and the relabel-down discipline only catches issues an agent actually gets dispatched on, not the long tail that sits labeled-but-untouched. Keep the confident-promotion behavior; keep the unconfident default at consult. Net: the inversion is right where the label is a byproduct of a fresh, deliberate dispatch decision, and wrong where it is a bulk guess about dormant backlog. Piece 1 (relabel-down) is unconditionally worth landing first; Piece 2 should be scoped to the trusted intake class only. --- *Awaiting Kai's approval before dispatching implementation. On approval, the recommended firing order is Issue A, then Issue B, with Issue C (cli-guard#193 / ward#618) tracked in parallel since Piece 1 does not block on it.* --- 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`
Author
Owner

Scope extension: the dormant-backlog half of the same discipline (from a live incident)

Kai raised this after a director lost its dispatch-broker connection mid-drain and a follow-on seat re-fired the "ready" batch. The postmortem is the argument for widening #619, so it belongs here rather than in a fresh ticket.

What happened. A read-only director surface re-dispatched engineers on issues that still carried a headless label from an earlier triage flip. Several were old (filed hundreds of issues behind HEAD), two were already closed, and the dispatches passed --no-preflight. Two runs finished and pushed to main before they were stopped by hand. Not harmful, but awkward: an engineer rewrote live code against a stale premise, and one landed via a duplicate of #617.

Root cause (one bug, three faces). A freshness judgment gets cached at some earlier moment and trusted at dispatch without rechecking current reality:

  • the headless label is cached at triage. director-startup-triage.md line 39-40 makes it policy: "presence of a label is the freshness signal, no time-based re-triage." A stamp never expires.
  • --no-preflight caches "I decided this is GO" with no tie to the code state it was judged against.
  • the effort default never assesses this ticket's difficulty at all.

#619's relabel-down discipline fixes freshness for issues an agent gets dispatched on. The advisor plan above says so directly: it "only catches issues an agent actually gets dispatched on, not the long tail that sits labeled-but-untouched." That long tail is exactly what bit us. So #619 should grow three more pieces, all instances of compute freshness at dispatch, not at triage:

Piece 3 - dormant-backlog drift re-triage (closes the "no time-based re-triage" hole)

Invalidate a mode label when the code it was judged against has drifted, and re-judge. Drift, not wall-clock (time is a weak proxy): stamp each triage label with the HEAD commit or issue-number it was computed at, and when the distance since exceeds a threshold, drop the label back into the untriaged pool so the startup pass re-judges it. This is the minimal change to agent_director_triage.go's skip-already-triaged rule: skip a labeled issue only while its stamp is within drift tolerance, not forever.

Piece 4 - pre-flight hardening + retire the wholesale skip

  • Non-bypassable floor checks --no-preflight can never skip: the issue must be open (a closed issue is an automatic NO-GO - nothing should have dispatched an engineer at a closed ticket), plus the Piece 3 drift check.
  • Fix the real false-negative that --no-preflight exists to route around: pre-flight must reason from the resolved repo, not the host dispatcher cwd (the goose-wrong-cwd NO-GO). Fix the gate and the bypass loses its reason to exist.
  • Downgrade, not suppress. If a human override survives, make it proceed past a NO-GO with the read still produced and posted ("premise 64% stale", "issue closed"), never suppress the read. You override with the finding in front of you, not blind.

Piece 5 - director per-dispatch effort estimate (builds on the now-landed #617)

#617 shipped static per-role effort (director high, engineer medium, baked in the fleet KDL). Add a dynamic layer: the director emits an effort guess for this ticket at dispatch, overriding the role default. The value is the forcing function - producing the estimate requires a fresh read that sizes the ticket, which is the same medicine as mandatory pre-flight. A refused or low-confidence estimate is itself a hold signal: "I can't size this" means "I do not understand it well enough to fire it," so downgrade to consult rather than dispatch.

Sequencing. Piece 1 (relabel-down) still lands first and unblocks the rest. Pieces 3-5 are independent of each other and of the broker label op (#618). Piece 4's open-state floor check is the smallest and highest-value single fix - it alone would have stopped today's worst case.

Housekeeping fallout from the incident: #617 is now superseded by the #620 merge (per-role config landed); it needs closing, and the landed director effort value needs a check (it may have captured the pre-correction medium rather than the corrected high, since the engineer freezes the issue body at launch).

## Scope extension: the dormant-backlog half of the same discipline (from a live incident) Kai raised this after a director lost its dispatch-broker connection mid-drain and a follow-on seat re-fired the "ready" batch. The postmortem is the argument for widening #619, so it belongs here rather than in a fresh ticket. **What happened.** A read-only director surface re-dispatched engineers on issues that still carried a `headless` label from an earlier triage flip. Several were **old** (filed hundreds of issues behind HEAD), two were **already closed**, and the dispatches passed `--no-preflight`. Two runs finished and pushed to `main` before they were stopped by hand. Not harmful, but awkward: an engineer rewrote live code against a stale premise, and one landed via a **duplicate** of #617. **Root cause (one bug, three faces).** A freshness judgment gets **cached at some earlier moment and trusted at dispatch without rechecking current reality**: - the `headless` **label** is cached at triage. `director-startup-triage.md` line 39-40 makes it policy: "presence of a label is the freshness signal, **no time-based re-triage**." A stamp never expires. - `--no-preflight` caches "I decided this is GO" with no tie to the code state it was judged against. - the **effort default** never assesses this ticket's difficulty at all. #619's relabel-down discipline fixes freshness for issues an agent **gets dispatched on**. The advisor plan above says so directly: it "only catches issues an agent actually gets dispatched on, not the long tail that sits labeled-but-untouched." That long tail is exactly what bit us. So #619 should grow three more pieces, all instances of **compute freshness at dispatch, not at triage**: ### Piece 3 - dormant-backlog drift re-triage (closes the "no time-based re-triage" hole) Invalidate a mode label when the code it was judged against has **drifted**, and re-judge. Drift, not wall-clock (time is a weak proxy): stamp each triage label with the HEAD commit or issue-number it was computed at, and when the distance since exceeds a threshold, drop the label back into the untriaged pool so the startup pass re-judges it. This is the minimal change to `agent_director_triage.go`'s skip-already-triaged rule: skip a labeled issue only while its stamp is within drift tolerance, not forever. ### Piece 4 - pre-flight hardening + retire the wholesale skip - **Non-bypassable floor checks** `--no-preflight` can never skip: the issue must be **open** (a closed issue is an automatic NO-GO - nothing should have dispatched an engineer at a closed ticket), plus the Piece 3 drift check. - **Fix the real false-negative** that `--no-preflight` exists to route around: pre-flight must reason from the **resolved repo**, not the host dispatcher cwd (the goose-wrong-cwd NO-GO). Fix the gate and the bypass loses its reason to exist. - **Downgrade, not suppress.** If a human override survives, make it proceed **past** a NO-GO with the read still produced and posted ("premise 64% stale", "issue closed"), never suppress the read. You override with the finding in front of you, not blind. ### Piece 5 - director per-dispatch effort estimate (builds on the now-landed #617) #617 shipped **static** per-role effort (director high, engineer medium, baked in the fleet KDL). Add a **dynamic** layer: the director emits an effort guess for *this* ticket at dispatch, overriding the role default. The value is the forcing function - producing the estimate requires a fresh read that sizes the ticket, which is the same medicine as mandatory pre-flight. A **refused or low-confidence estimate is itself a hold signal**: "I can't size this" means "I do not understand it well enough to fire it," so downgrade to consult rather than dispatch. **Sequencing.** Piece 1 (relabel-down) still lands first and unblocks the rest. Pieces 3-5 are independent of each other and of the broker label op (#618). Piece 4's open-state floor check is the smallest and highest-value single fix - it alone would have stopped today's worst case. **Housekeeping fallout from the incident:** #617 is now superseded by the #620 merge (per-role config landed); it needs closing, and the landed director effort value needs a check (it may have captured the pre-correction `medium` rather than the corrected `high`, since the engineer freezes the issue body at launch).
Author
Owner

Correction to Piece 1's capability claim (from the #621 relabel attempt)

The advisor design above states engineers/advisors can relabel with their own in-container token and do not need the broker label op for relabel-down. Empirically that is false for a run dispatched from the read-only director surface: a ref-mode advisor at #621 had every issue-label add/remove refused out of tier - its writes route through the broker, not a direct token. So either (a) surface-originated dispatches are broker-gated where normal host dispatches are not, or (b) the direct-token claim is wrong generally. This needs verifying, because it decides whether the director-surface relabel-down path is blocked on #618 after all. Until resolved, treat #618 as load-bearing for any relabel that originates from the director surface.

## Correction to Piece 1's capability claim (from the #621 relabel attempt) The advisor design above states engineers/advisors can relabel with their own in-container token and do **not** need the broker label op for relabel-down. Empirically that is **false for a run dispatched from the read-only director surface**: a ref-mode advisor at #621 had every `issue-label add/remove` refused `out of tier` - its writes route through the broker, not a direct token. So either (a) surface-originated dispatches are broker-gated where normal host dispatches are not, or (b) the direct-token claim is wrong generally. This needs verifying, because it decides whether the director-surface relabel-down path is blocked on **#618** after all. Until resolved, treat #618 as load-bearing for any relabel that originates from the director surface.
coilyco-ops added
P4
and removed
P2
labels 2026-07-14 06:50:31 +00:00
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#619
No description provided.