Migrate ward off cli-guard's removed cli/dispatch so it can reach fleetconfig's singular guardfile (unblocks the dev-base image) #1120

Closed
opened 2026-07-11 23:38:17 +00:00 by coilyco-ops · 4 comments
Member

Why

The aos dev-base image cannot publish. docker/dev-base/core/Dockerfile:182 gates the build on CLIGUARD_NO_SANDBOX=1 ward doctor, and doctor fails against the aos .ward bundle. aos is the canonical state; ward adapts to it.

Against the current aos bundle, both ward v0.626.0 and ward main HEAD fail identically:

FAIL smart defaults: repos body: unknown node "burndown" (want repo-authority; fail-closed)
FAIL fleet: role "engineer" body: unknown node "guardfile" (want guardfiles | agent; fail-closed)
FAIL ops bundle: guardfile base-url "git.example.com/api/v1" still looks like a placeholder
PASS exec bundle

The dependency knot (this issue)

The fleet failure is the one with a hard blocker underneath it.

  • cli-guard main already accepts singular guardfile nodes in a role body - case "guardfile": return st.applyGuardfile(c) in pkg/fleetconfig, added by f6063aa ("rename fleetconfig syntax to agents", 2026-07-10). guardfiles is kept as a legacy alias.
  • ward pins cli-guard v0.85.1-0.20260709002015-4744df17086f and still imports cli-guard/cli/dispatch.
  • cli-guard removed the legacy dispatch subsystem in 05e8b78 ("remove legacy dispatch subsystem", 2026-07-09) - i.e. before the guardfile support landed.

So go get cli-guard@main fails outright:

cmd/ward/agent.go:15:2: module cli-guard@latest found (v0.94.0),
  but does not contain package .../cli-guard/cli/dispatch

There is no cli-guard version that has both cli/dispatch and the singular-guardfile support. ward has to migrate off the removed dispatch subsystem first. Everything else is gated behind that.

Order to green

  1. this issue - migrate ward off cli-guard/cli/dispatch, then bump cli-guard to >= the f6063aa line. Clears the fleet failure.
  2. ward#1105 - teach repos {} the burndown block. ward's source has no burndown anywhere today; cmd/ward/smartdefaults.go accepts only repo-authority. Clears the smart defaults failure.
  3. doctor placeholder check - .ward/ops.forgejo.kdl and .ward/guardfile.forgejo.read.kdl deliberately carry git.example.com, restrict owner matches example*, and value ssm "/example/forgejo/api-token". Their own comments say the real values belong to the selected bundle, not the baked default. doctor must stop rejecting them. Clears the ops bundle failure.
  4. release ward, then in aos bump ARG WARD_VERSION and the hard-pinned literal in tests/test_dev_base_image.py (see aos#452 - that literal is what deadlocks dep-bump).

Context

  • aos#452 - publish-dev-base red on main (re-verified today with the live step log).
  • aos#471 - merged; unblocked the aos pytest gate, which is what let publish-dev-base run again and expose this.

Until this lands the published image stays at v0.245.0 with ward v0.529.0 baked in. It is pullable and ward runs inside it, it is just stale.

## Why The aos dev-base image cannot publish. `docker/dev-base/core/Dockerfile:182` gates the build on `CLIGUARD_NO_SANDBOX=1 ward doctor`, and doctor fails against the aos `.ward` bundle. **aos is the canonical state; ward adapts to it.** Against the current aos bundle, both **ward v0.626.0** and **ward main HEAD** fail identically: ``` FAIL smart defaults: repos body: unknown node "burndown" (want repo-authority; fail-closed) FAIL fleet: role "engineer" body: unknown node "guardfile" (want guardfiles | agent; fail-closed) FAIL ops bundle: guardfile base-url "git.example.com/api/v1" still looks like a placeholder PASS exec bundle ``` ## The dependency knot (this issue) The `fleet` failure is the one with a hard blocker underneath it. * cli-guard **main already accepts** singular `guardfile` nodes in a role body - `case "guardfile": return st.applyGuardfile(c)` in `pkg/fleetconfig`, added by `f6063aa` ("rename fleetconfig syntax to agents", **2026-07-10**). `guardfiles` is kept as a legacy alias. * ward pins cli-guard `v0.85.1-0.20260709002015-4744df17086f` and still imports `cli-guard/cli/dispatch`. * cli-guard **removed** the legacy dispatch subsystem in `05e8b78` ("remove legacy dispatch subsystem", **2026-07-09**) - i.e. *before* the guardfile support landed. So `go get cli-guard@main` fails outright: ``` cmd/ward/agent.go:15:2: module cli-guard@latest found (v0.94.0), but does not contain package .../cli-guard/cli/dispatch ``` **There is no cli-guard version that has both `cli/dispatch` and the singular-`guardfile` support.** ward has to migrate off the removed dispatch subsystem first. Everything else is gated behind that. ## Order to green 1. **this issue** - migrate ward off `cli-guard/cli/dispatch`, then bump cli-guard to >= the `f6063aa` line. Clears the `fleet` failure. 2. ward#1105 - teach `repos {}` the `burndown` block. ward's source has no `burndown` anywhere today; `cmd/ward/smartdefaults.go` accepts only `repo-authority`. Clears the `smart defaults` failure. 3. **doctor placeholder check** - `.ward/ops.forgejo.kdl` and `.ward/guardfile.forgejo.read.kdl` deliberately carry `git.example.com`, `restrict owner matches example*`, and `value ssm "/example/forgejo/api-token"`. Their own comments say the real values belong to the selected bundle, not the baked default. doctor must stop rejecting them. Clears the `ops bundle` failure. 4. release ward, then in aos bump `ARG WARD_VERSION` **and** the hard-pinned literal in `tests/test_dev_base_image.py` (see aos#452 - that literal is what deadlocks dep-bump). ## Context * aos#452 - publish-dev-base red on main (re-verified today with the live step log). * aos#471 - merged; unblocked the aos pytest gate, which is what let `publish-dev-base` run again and expose this. Until this lands the published image stays at v0.245.0 with ward v0.529.0 baked in. It is pullable and ward runs inside it, it is just stale.
Author
Member

Constraint from Kai: ward cannot embed any real coilyco / coilysiren config

That settles failure 3, and it is the opposite of what it first looked like.

.ward/ops.forgejo.kdl is the baked default that gets embedded into ward's release (the aos#332 spec-bundle overlay). Its base-url "git.example.com/api/v1", restrict owner matches example*, and value ssm "/example/forgejo/api-token" are correct and required - the shipped product must not carry Kai's deployment values. Its own comment already says so:

Auth as an example bot, not a personal PAT. Token + scopes are deployment specific and belong to the selected bundle, not the baked default.

The real coilyco values live in .ward/guardfile.forgejo.kdl (forgejo.coilysiren.me/api/v1, /forgejo/coilyco-ops/api-token, restrict owner matches coily*), which is the selected bundle for this deployment.

So the bug is in doctor, not in the bundle

validateForgejoOpsOperational -> loadForgejoGuardfileFrom short-circuits on the ward.bundle.kdl manifest (ops { forgejo "ops.forgejo.kdl" }) and validates the baked default in isolation, then runs containsExamplePlaceholder on it. That check is meant to catch a deployment that never melded its values - but it is reading the pre-meld layer, so it fires on the one file that is supposed to say example.

Both files declare the same wrap ward-kdl ops forgejo block, so the fix is for doctor to validate the melded/effective surface (deployment guardfile overriding the baked default's singletons - base-url, auth, restrict), not the embedded default. findMergedBundleNode in the non-manifest path already looks like the seam for this.

Do not rewire ward.bundle.kdl to point at guardfile.forgejo.kdl, and do not meld real values into ops.forgejo.kdl - either one would embed coilyco config into the shipped product. I nearly did the first and it would have been wrong.

Remaining to green

  1. singular guardfile - done, ward#1121 (PASS fleet).
  2. burndown - ward#1105.
  3. doctor placeholder check reads the baked default instead of the melded result - this comment.

Then release ward, and in aos bump ARG WARD_VERSION plus the hard-pinned literal in tests/test_dev_base_image.py (aos#452).

## Constraint from Kai: **ward cannot embed any real coilyco / coilysiren config** That settles failure 3, and it is the opposite of what it first looked like. `.ward/ops.forgejo.kdl` is the **baked default that gets embedded into ward's release** (the aos#332 spec-bundle overlay). Its `base-url "git.example.com/api/v1"`, `restrict owner matches example*`, and `value ssm "/example/forgejo/api-token"` are **correct and required** - the shipped product must not carry Kai's deployment values. Its own comment already says so: > Auth as an example bot, not a personal PAT. Token + scopes are deployment specific and belong to the selected bundle, not the baked default. The real coilyco values live in `.ward/guardfile.forgejo.kdl` (`forgejo.coilysiren.me/api/v1`, `/forgejo/coilyco-ops/api-token`, `restrict owner matches coily*`), which is the **selected bundle** for this deployment. ### So the bug is in doctor, not in the bundle `validateForgejoOpsOperational` -> `loadForgejoGuardfileFrom` short-circuits on the `ward.bundle.kdl` manifest (`ops { forgejo "ops.forgejo.kdl" }`) and validates the **baked default in isolation**, then runs `containsExamplePlaceholder` on it. That check is meant to catch a deployment that never melded its values - but it is reading the pre-meld layer, so it fires on the one file that is *supposed* to say `example`. Both files declare the same `wrap ward-kdl ops forgejo` block, so the fix is for doctor to validate the **melded/effective** surface (deployment guardfile overriding the baked default's singletons - base-url, auth, restrict), not the embedded default. `findMergedBundleNode` in the non-manifest path already looks like the seam for this. **Do not** rewire `ward.bundle.kdl` to point at `guardfile.forgejo.kdl`, and **do not** meld real values into `ops.forgejo.kdl` - either one would embed coilyco config into the shipped product. I nearly did the first and it would have been wrong. ### Remaining to green 1. ~~singular `guardfile`~~ - **done**, ward#1121 (`PASS fleet`). 2. `burndown` - ward#1105. 3. doctor placeholder check reads the baked default instead of the melded result - **this comment**. Then release ward, and in aos bump `ARG WARD_VERSION` plus the hard-pinned literal in `tests/test_dev_base_image.py` (aos#452).
Author
Member

Status: doctor is down to 2 of 3, and a fourth surfaced

ward#1121 lands two of the three: the cli-guard unstranding (PASS fleet) and burndown (ward#1105).

With burndown parsing, a new smart-defaults failure came out from behind it - it had been masked because the parse error aborted the whole block:

FAIL smart defaults: agent-reservation-ttl 1h must exceed role "engineer"
                     execution-time-limit 90m (fail-closed)

aos defaults.kdl sets agent-reservation-ttl "1h"; ward's embedded engineer role preset sets execution-time-limit 90m. The invariant is sound - a reservation must outlive the run it holds - so 1h < 90m is a genuine conflict, not a bad check. One of the two has to move:

  • raise agent-reservation-ttl in aos above 90m (e.g. "2h"), or
  • lower the engineer preset's execution-time-limit below 1h in ward.

Semantically the TTL looks like the wrong one: an engineer run is allowed 90m, so a 1h reservation expires mid-run. But the role presets are ward-embedded product data (AGENTS.md), and aos is canonical for the bundle, so this is Kai's call.

Remaining to a publishable image

  1. singular guardfile - done, ward#1121.
  2. burndown - done, ward#1121 (ward#1105).
  3. agent-reservation-ttl vs engineer execution-time-limit - needs a decision (above).
  4. doctor's placeholder check reads the baked default, not the melded surface - needs a decision (see previous comment; do not embed coilyco config into ward).

Then release ward, and in aos bump ARG WARD_VERSION plus the hard-pinned literal in tests/test_dev_base_image.py (aos#452).

## Status: doctor is down to **2 of 3**, and a fourth surfaced ward#1121 lands two of the three: the cli-guard unstranding (`PASS fleet`) and `burndown` (ward#1105). With `burndown` parsing, a **new** smart-defaults failure came out from behind it - it had been masked because the parse error aborted the whole block: ``` FAIL smart defaults: agent-reservation-ttl 1h must exceed role "engineer" execution-time-limit 90m (fail-closed) ``` aos `defaults.kdl` sets `agent-reservation-ttl "1h"`; ward's **embedded** engineer role preset sets `execution-time-limit 90m`. The invariant is sound - a reservation must outlive the run it holds - so `1h < 90m` is a genuine conflict, not a bad check. One of the two has to move: * raise `agent-reservation-ttl` in aos above 90m (e.g. `"2h"`), or * lower the engineer preset's `execution-time-limit` below 1h in ward. Semantically the TTL looks like the wrong one: an engineer run is allowed 90m, so a 1h reservation expires mid-run. But the role presets are ward-embedded product data (AGENTS.md), and aos is canonical for the bundle, so this is Kai's call. ### Remaining to a publishable image 1. ~~singular `guardfile`~~ - **done**, ward#1121. 2. ~~`burndown`~~ - **done**, ward#1121 (ward#1105). 3. `agent-reservation-ttl` vs engineer `execution-time-limit` - **needs a decision** (above). 4. doctor's placeholder check reads the baked default, not the melded surface - **needs a decision** (see previous comment; do not embed coilyco config into ward). Then release ward, and in aos bump `ARG WARD_VERSION` plus the hard-pinned literal in `tests/test_dev_base_image.py` (aos#452).
Author
Member

WARDED_WORKFLOW: reservation-held

reservation details

Holder: launch intent for container engineer-codex-ward-1120 on host kais-macbook-pro-2.local.

Accepted by ward agent --harness codex (reserved 2026-07-14T06:56:42Z). Concurrent ward agent runs are blocked until this intent becomes visible or the intent is released. The stale-intent fallback is still TTL-bounded (3h TTL). --override-reservation overrides.

Do not comment on or edit this issue to steer the run while it is reserved. The engineer seeded the body once at launch and never re-reads it, so a comment or edit reaches only human readers, never the running engineer. A correction goes to a new issue, dispatched fresh. That is the only channel that reaches a run in flight. Where the forge supports it, ward locks this conversation to make that a road-block rather than a convention (ward#494).

run seed context — what this run is carrying (ward#609)
  • Resolved: coilyco-flight-deck/ward#1120 · branch issue-1120 · harness codex · workflow pull-request-and-merge
  • Run: engineer-codex-ward-1120 · ward v0.667.0 · dispatched 2026-07-14T06:56:36Z
  • Comment thread: 2 included in the pre-flight read, 0 stripped (ward's own automated comments).

Static container doctrine and seed boilerplate are identical every run and omitted here (they ride ward v0.667.0).

— Codex, via ward agent

<!-- ward-agent-reservation --> WARDED_WORKFLOW: reservation-held <details><summary>reservation details</summary> Holder: launch intent for container `engineer-codex-ward-1120` on host `kais-macbook-pro-2.local`. Accepted by `ward agent --harness codex` (reserved 2026-07-14T06:56:42Z). Concurrent `ward agent` runs are blocked until this intent becomes visible or the intent is released. The stale-intent fallback is still TTL-bounded (3h TTL). `--override-reservation` overrides. **Do not comment on or edit this issue to steer the run while it is reserved.** The engineer seeded the body once at launch and never re-reads it, so a comment or edit reaches only human readers, never the running engineer. A correction goes to a **new issue, dispatched fresh**. That is the only channel that reaches a run in flight. Where the forge supports it, ward locks this conversation to make that a road-block rather than a convention (ward#494). <details><summary>run seed context — what this run is carrying (ward#609)</summary> - **Resolved:** `coilyco-flight-deck/ward#1120` · branch `issue-1120` · harness `codex` · workflow `pull-request-and-merge` - **Run:** `engineer-codex-ward-1120` · ward `v0.667.0` · dispatched `2026-07-14T06:56:36Z` - **Comment thread:** 2 included in the pre-flight read, 0 stripped (ward's own automated comments). - included: @coilyco-ops (2026-07-12T00:05:40Z), @coilyco-ops (2026-07-12T00:20:52Z) Static container doctrine and seed boilerplate are identical every run and omitted here (they ride ward v0.667.0). </details> </details> <!-- ward-agent-signature --> — Codex, via `ward agent`
Author
Member

WARDED_WORKFLOW: #1232

details

workflow: pull-request-and-merge; review summary: review gate skipped by ~/.ward/config.yaml default

This felt small and direct. Confidence is high because the change only retargets the parser comment and adds a regression test. Surprise: the repo already had the functional migration, so the work landed as a pin rather than a code-path swap. Follow-up: none.

WARDED_WORKFLOW: https://forgejo.coilysiren.me/coilyco-flight-deck/ward/pulls/1232 <details><summary>details</summary> workflow: pull-request-and-merge; review summary: review gate skipped by ~/.ward/config.yaml default This felt small and direct. Confidence is high because the change only retargets the parser comment and adds a regression test. Surprise: the repo already had the functional migration, so the work landed as a pin rather than a code-path swap. Follow-up: none. </details>
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#1120
No description provided.