design: decouple ward core from ward-kdl runtime/specgen assumptions #721

Closed
opened 2026-07-09 00:32:55 +00:00 by coilyco-ops · 2 comments
Member

Context

Kai corrected the direction while reading ward#407: the earlier issue assumed ward should remove Go grafts by pushing behavior down into ward-kdl/specgen. That is now backwards.

KDL specgen has become an end-user configuration layer. Because of that, ward core should not actively rely on a KDL runtime binary or on ward-kdl being present as an internal implementation dependency. KDL is now significantly detached from ward core.

Problem

ward#407 framed the cleanup as "make ward-kdl/specverb generate the desired leaves so ward can delete Go overrides." That was reasonable for the old architecture, but it conflicts with the current boundary:

  • ward internals should own their core behavior directly.
  • ward-kdl should configure and constrain surfaces at the edge, not become a required runtime subsystem for ward's internal command behavior.
  • Go-side ops behavior that belongs to ward should not be forced downward into KDL just to remove an override shape.
  • Any remaining KDL/specgen coupling should be audited as a boundary question, not treated as the destination for core logic.

Ask

Produce a concrete design proposal for the new direction:

  • Identify the ward paths that still actively depend on ward-kdl/specgen/KDL runtime behavior.
  • Separate acceptable edge configuration from improper ward-core dependency.
  • Reframe the ward#407 graft inventory under this new boundary: which grafts should stay ward-owned, which should become a small ward-native helper, and which, if any, still belong in config/specgen.
  • Propose a migration path that reduces runtime reliance on KDL without regressing the ward ops forgejo user surface.
  • Call out tests or guardrails that should replace the old "push it into KDL" plan.

No code yet. Post the design as a comment, then leave implementation follow-ups if the proposal splits cleanly.

  • ward#407 - prior closed cleanup issue, now directionally stale.
  • agentic-os#306 / agentic-os#308 - adjacent four-layer cleanup references from the older framing.
## Context Kai corrected the direction while reading ward#407: the earlier issue assumed ward should remove Go grafts by pushing behavior down into ward-kdl/specgen. That is now backwards. KDL specgen has become an end-user configuration layer. Because of that, ward core should not actively rely on a KDL runtime binary or on ward-kdl being present as an internal implementation dependency. KDL is now significantly detached from ward core. ## Problem ward#407 framed the cleanup as "make ward-kdl/specverb generate the desired leaves so ward can delete Go overrides." That was reasonable for the old architecture, but it conflicts with the current boundary: * ward internals should own their core behavior directly. * ward-kdl should configure and constrain surfaces at the edge, not become a required runtime subsystem for ward's internal command behavior. * Go-side ops behavior that belongs to ward should not be forced downward into KDL just to remove an override shape. * Any remaining KDL/specgen coupling should be audited as a boundary question, not treated as the destination for core logic. ## Ask Produce a concrete design proposal for the new direction: * Identify the ward paths that still actively depend on ward-kdl/specgen/KDL runtime behavior. * Separate acceptable edge configuration from improper ward-core dependency. * Reframe the ward#407 graft inventory under this new boundary: which grafts should stay ward-owned, which should become a small ward-native helper, and which, if any, still belong in config/specgen. * Propose a migration path that reduces runtime reliance on KDL without regressing the `ward ops forgejo` user surface. * Call out tests or guardrails that should replace the old "push it into KDL" plan. No code yet. Post the design as a comment, then leave implementation follow-ups if the proposal splits cleanly. ## Related * ward#407 - prior closed cleanup issue, now directionally stale. * agentic-os#306 / agentic-os#308 - adjacent four-layer cleanup references from the older framing.
Author
Member

🔎 ward agent advisor

ward agent advisor --harness codex ran a one-shot standard research pass on this question:

Read the issue title, body, and comment thread below as the research brief. Identify the decisions, options, open questions, and risks, then give a clear advisory recommendation that can be posted back on the issue.


Recommendation

The clean boundary is: keep KDL and specgen at the operator-surface edge, and pull ward-core policy and behavior back into ward. Concretely, ward ops ... and other opt-in guarded command surfaces may continue to be authored from guardfiles, but ward agent, container behavior, fleet selection, runtime defaults, and product-specific Forgejo behavior should not require launch-time KDL parsing, remote bundle fetches, or .ward/ward-kdl as the canonical runtime source.

ward#407 should therefore be treated as directionally stale. The right goal is no longer "delete Go by pushing behavior downward into KDL". The right goal is "make the remaining KDL seam explicitly edge-only, and keep ward-owned product behavior in ward even when the base surface is generated."

What still depends on ward-kdl or KDL runtime today

  • cmd/ward/ops.go builds ward ops forgejo at runtime from a selected guardfile + spec lock, then patches the result with four ward-side grafts: overrideForgejoViewIssue, overrideForgejoCreateIssue, overrideForgejoCommentIssue, and graftForgejoAdminExec.
  • cmd/ward/wardkdl_exec.go auto-mounts exec-dialect guardfiles at startup, so ward docker, ward ops {aws,kubectl}, and ward agents <tool> are runtime-built from the selected bundle.
  • cmd/ward/fleet.go parses ward-kdl.fleet.kdl at runtime for the agent roster and role overlays.
  • cmd/ward/smartdefaults.go parses ward-kdl.defaults.kdl at runtime for reservation TTLs, director cadence, review timeout, config-bundle TTL, container retention, and workflow defaults.
  • cmd/ward/container_topology.go parses ward-kdl.topology.kdl at runtime for tailnet, tower, and substrate topology.
  • cmd/ward/configsource.go puts all of the above behind WARD_CONFIG_REF, including git-ref resolution and cache sync in cmd/ward/configref.go, so a remote bundle can change or break core behavior at launch.
  • The drift tests opsassets_test.go, execassets_test.go, fleetassets_test.go, defaultsassets_test.go, and topologyassets_test.go still treat .ward/ward-kdl as the canonical source for both edge surfaces and core runtime data.

That is the real coupling inventory. The key problem is not a ward-kdl binary dependency anymore. It is that ward core still treats ward-kdl-authored KDL as a runtime source of truth.

Boundary split

Acceptable edge configuration

  • Guardfiles and spec locks that define user-facing audited operator surfaces.
  • Exec-dialect mounts that only add or constrain CLI surface area.
  • Optional live bundle selection for those edge surfaces, if ward still wants that flexibility.

Improper ward-core dependency

  • Agent roster and role selection.
  • Runtime defaults like reap thresholds, review timeout, workflow mode, and bundle TTL.
  • Container topology and substrate wiring.
  • Product-specific ops forgejo behavior that exists because ward wants a particular UX or transport composition, not because the guardfile can express it.

The litmus test is simple: if a malformed or unreachable KDL bundle can break ward agent, ward agents list, reservation logic, review timing, or container bring-up, that behavior is too deep in core.

Reframing the ward#407 graft inventory

Keep ward-owned

  • overrideForgejoViewIssue should stay ward-owned. The lean {issue, comments} projection is product behavior, not permission policy.
  • overrideForgejoCreateIssue should stay ward-owned. --quiet is a ward UX and machine-contract choice, not a guardfile concern.

Move to a small ward-native helper, not to KDL

  • overrideForgejoCommentIssue should become a small ward-native helper around generated write/action leaves. The concern is safe body transport and parity with generated write leaves, not spec authoring.
  • graftForgejoAdminExec should become a small ward-native mixed-transport composer. Joining REST and remote-exec under one ward ops forgejo tree is a product-composition concern.

Leave in config or specgen only

  • The raw REST leaf inventory, auth sources, argument constraints, and exec transport policy.
  • In other words, KDL should keep declaring what is allowed at the edge, not how ward core behaves internally.

So the end-state answer for the current four grafts is: none of them should be pushed into KDL as the destination architecture. Two stay explicit ward behavior, two become reusable ward-native composition helpers.

Migration path

  1. Narrow WARD_CONFIG_REF scope first.

    • Split the current single config-source seam into edge surface source and core runtime source.
    • WARD_CONFIG_REF should stop steering fleet/defaults/topology in the steady state.
    • A broken external bundle should degrade ops and other mounted surfaces, not core agent/container behavior.
  2. Pull core runtime config out of KDL.

    • smartDefaults: replace runtime KDL parsing with ward-native typed defaults.
    • containerTopology: make env vars plus Go defaults canonical. The file is tiny and already mostly mirrors constants.
    • fleet: compile a ward-native manifest into the binary, either as generated JSON/Go data or direct Go literals. Keep the existing runtime semantics, but stop live bundle replacement of the roster.
    • fleet.local.kdl can remain for now if desired. It is explicit operator-local preference, not ward-kdl-authored product internals.
  3. Keep generated ops surfaces, but make ward compose them explicitly.

    • Base forgejo REST surface can still come from guardfile + spec.
    • Ward then applies its own stable overlays for projection, machine-output, safe-body transport, and mixed transport.
    • This preserves ward ops forgejo without pretending those behaviors belong in KDL.
  4. Invert the source of truth for core assets.

    • fleetassets, defaultsassets, and topologyassets should stop mirroring .ward/ward-kdl as canonical.
    • Either move the canonical source into ward-native files, or generate runtime-native assets from a ward-owned source at build time.
  5. Only after that, decide whether runtime specverb.Build is worth eliminating.

    • It is not the first problem. The larger mistake today is that ward core is configurable through a remote KDL bundle.
    • If later desired, ward can explore embedding a prebuilt command artifact for ops surfaces, but that is optional and separate.

Guardrails that should replace the old plan

  • Add a boundary test that a broken or unreachable WARD_CONFIG_REF no longer breaks core paths like ward agent, ward agents list, reservation/default logic, or container topology.
  • Keep and expand TestForgejoGraftInventory-style contract tests, but reframe them as ward surface guarantees, not temporary removal scaffolding.
  • Add contract tests for the mixed-transport ops forgejo tree so admin and doctor remain present without depending on specgen evolution.
  • Add native-loader tests for whichever replacement holds fleet/defaults/topology, with fail-closed validation matching the current parser strictness.
  • Restrict drift tests to genuine edge assets only: guardfiles, spec locks, and exec surface mirrors that ward intentionally consumes as edge inputs.

Options

Option A: continue the old ward#407 plan and push the grafts into KDL/specgen

Reject. It keeps the wrong dependency direction and turns ward-owned product behavior into generator behavior just to delete Go.

Option B: remove all KDL runtime use immediately, including generated ops surfaces

Too large for the first cut. It solves more than the issue asks and risks unnecessary ward ops forgejo churn.

Option C: keep KDL as edge authoring only, move core policy and behavior into ward, and preserve ward ops forgejo with ward-native composition

Recommended. It matches the corrected architecture, shrinks runtime blast radius, and preserves the user surface.

Open questions

  • Is live WARD_CONFIG_REF override of the fleet roster still a required product feature, or can it shrink to edge surfaces only?
  • Should fleet.local.kdl remain KDL as explicit operator-local preference, or later move to a simpler native format too?
  • Do ward agents <tool> exec guardfiles count as long-term stable product surface or temporary delegation? If they are long-term, keep them as edge-mounted surfaces with strong contract tests.

Risks

  • Narrowing WARD_CONFIG_REF is a behavior change for anyone using it to steer agent or container internals today.
  • If ward-owned overlays are not locked by tests, the surface can drift in subtle ways while still "building fine".
  • Moving fleet/defaults/topology out of .ward/ward-kdl needs a doc pass, because several current docs still describe ward-kdl as the source for those runtime knobs.

Bottom line

The design answer is to stop treating ward-kdl as ward core's runtime control plane. Keep KDL where it is strongest, at the permission and surface-authoring edge. Move core runtime data and ward-specific Forgejo behavior back into ward. Preserve the existing ward ops forgejo UX by composing generated edge surfaces with explicit ward-native helpers, not by teaching KDL to own more of ward's product logic.


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

— Codex, via ward agent

### 🔎 ward agent advisor `ward agent advisor --harness codex` ran a one-shot **standard** research pass on this question: > Read the issue title, body, and comment thread below as the research brief. Identify the decisions, options, open questions, and risks, then give a clear advisory recommendation that can be posted back on the issue. --- ## Recommendation The clean boundary is: keep KDL and specgen at the **operator-surface edge**, and pull ward-core policy and behavior back into **ward**. Concretely, `ward ops ...` and other opt-in guarded command surfaces may continue to be **authored** from guardfiles, but `ward agent`, container behavior, fleet selection, runtime defaults, and product-specific Forgejo behavior should not require launch-time KDL parsing, remote bundle fetches, or `.ward/ward-kdl` as the canonical runtime source. `ward#407` should therefore be treated as directionally stale. The right goal is no longer "delete Go by pushing behavior downward into KDL". The right goal is "make the remaining KDL seam explicitly edge-only, and keep ward-owned product behavior in ward even when the base surface is generated." ## What still depends on ward-kdl or KDL runtime today - `cmd/ward/ops.go` builds `ward ops forgejo` at runtime from a selected guardfile + spec lock, then patches the result with four ward-side grafts: `overrideForgejoViewIssue`, `overrideForgejoCreateIssue`, `overrideForgejoCommentIssue`, and `graftForgejoAdminExec`. - `cmd/ward/wardkdl_exec.go` auto-mounts exec-dialect guardfiles at startup, so `ward docker`, `ward ops {aws,kubectl}`, and `ward agents <tool>` are runtime-built from the selected bundle. - `cmd/ward/fleet.go` parses `ward-kdl.fleet.kdl` at runtime for the agent roster and role overlays. - `cmd/ward/smartdefaults.go` parses `ward-kdl.defaults.kdl` at runtime for reservation TTLs, director cadence, review timeout, config-bundle TTL, container retention, and workflow defaults. - `cmd/ward/container_topology.go` parses `ward-kdl.topology.kdl` at runtime for tailnet, tower, and substrate topology. - `cmd/ward/configsource.go` puts all of the above behind `WARD_CONFIG_REF`, including git-ref resolution and cache sync in `cmd/ward/configref.go`, so a remote bundle can change or break core behavior at launch. - The drift tests `opsassets_test.go`, `execassets_test.go`, `fleetassets_test.go`, `defaultsassets_test.go`, and `topologyassets_test.go` still treat `.ward/ward-kdl` as the canonical source for both edge surfaces and core runtime data. That is the real coupling inventory. The key problem is not a `ward-kdl` binary dependency anymore. It is that ward core still treats ward-kdl-authored KDL as a **runtime source of truth**. ## Boundary split **Acceptable edge configuration** - Guardfiles and spec locks that define user-facing audited operator surfaces. - Exec-dialect mounts that only add or constrain CLI surface area. - Optional live bundle selection for those edge surfaces, if ward still wants that flexibility. **Improper ward-core dependency** - Agent roster and role selection. - Runtime defaults like reap thresholds, review timeout, workflow mode, and bundle TTL. - Container topology and substrate wiring. - Product-specific `ops forgejo` behavior that exists because ward wants a particular UX or transport composition, not because the guardfile can express it. The litmus test is simple: if a malformed or unreachable KDL bundle can break `ward agent`, `ward agents list`, reservation logic, review timing, or container bring-up, that behavior is too deep in core. ## Reframing the `ward#407` graft inventory ### Keep ward-owned - `overrideForgejoViewIssue` should stay ward-owned. The lean `{issue, comments}` projection is product behavior, not permission policy. - `overrideForgejoCreateIssue` should stay ward-owned. `--quiet` is a ward UX and machine-contract choice, not a guardfile concern. ### Move to a small ward-native helper, not to KDL - `overrideForgejoCommentIssue` should become a small ward-native helper around generated write/action leaves. The concern is safe body transport and parity with generated write leaves, not spec authoring. - `graftForgejoAdminExec` should become a small ward-native mixed-transport composer. Joining REST and remote-exec under one `ward ops forgejo` tree is a product-composition concern. ### Leave in config or specgen only - The raw REST leaf inventory, auth sources, argument constraints, and exec transport policy. - In other words, KDL should keep declaring **what is allowed at the edge**, not **how ward core behaves internally**. So the end-state answer for the current four grafts is: **none of them should be pushed into KDL as the destination architecture**. Two stay explicit ward behavior, two become reusable ward-native composition helpers. ## Migration path 1. **Narrow `WARD_CONFIG_REF` scope first.** - Split the current single config-source seam into `edge surface source` and `core runtime source`. - `WARD_CONFIG_REF` should stop steering fleet/defaults/topology in the steady state. - A broken external bundle should degrade `ops` and other mounted surfaces, not core agent/container behavior. 2. **Pull core runtime config out of KDL.** - `smartDefaults`: replace runtime KDL parsing with ward-native typed defaults. - `containerTopology`: make env vars plus Go defaults canonical. The file is tiny and already mostly mirrors constants. - `fleet`: compile a ward-native manifest into the binary, either as generated JSON/Go data or direct Go literals. Keep the existing runtime semantics, but stop live bundle replacement of the roster. - `fleet.local.kdl` can remain for now if desired. It is explicit operator-local preference, not ward-kdl-authored product internals. 3. **Keep generated ops surfaces, but make ward compose them explicitly.** - Base `forgejo` REST surface can still come from guardfile + spec. - Ward then applies its own stable overlays for projection, machine-output, safe-body transport, and mixed transport. - This preserves `ward ops forgejo` without pretending those behaviors belong in KDL. 4. **Invert the source of truth for core assets.** - `fleetassets`, `defaultsassets`, and `topologyassets` should stop mirroring `.ward/ward-kdl` as canonical. - Either move the canonical source into ward-native files, or generate runtime-native assets from a ward-owned source at build time. 5. **Only after that, decide whether runtime `specverb.Build` is worth eliminating.** - It is not the first problem. The larger mistake today is that ward core is configurable through a remote KDL bundle. - If later desired, ward can explore embedding a prebuilt command artifact for ops surfaces, but that is optional and separate. ## Guardrails that should replace the old plan - Add a boundary test that a broken or unreachable `WARD_CONFIG_REF` no longer breaks core paths like `ward agent`, `ward agents list`, reservation/default logic, or container topology. - Keep and expand `TestForgejoGraftInventory`-style contract tests, but reframe them as **ward surface guarantees**, not temporary removal scaffolding. - Add contract tests for the mixed-transport `ops forgejo` tree so `admin` and `doctor` remain present without depending on specgen evolution. - Add native-loader tests for whichever replacement holds fleet/defaults/topology, with fail-closed validation matching the current parser strictness. - Restrict drift tests to genuine edge assets only: guardfiles, spec locks, and exec surface mirrors that ward intentionally consumes as edge inputs. ## Options ### Option A: continue the old `ward#407` plan and push the grafts into KDL/specgen Reject. It keeps the wrong dependency direction and turns ward-owned product behavior into generator behavior just to delete Go. ### Option B: remove all KDL runtime use immediately, including generated ops surfaces Too large for the first cut. It solves more than the issue asks and risks unnecessary `ward ops forgejo` churn. ### Option C: keep KDL as edge authoring only, move core policy and behavior into ward, and preserve `ward ops forgejo` with ward-native composition Recommended. It matches the corrected architecture, shrinks runtime blast radius, and preserves the user surface. ## Open questions - Is live `WARD_CONFIG_REF` override of the fleet roster still a required product feature, or can it shrink to edge surfaces only? - Should `fleet.local.kdl` remain KDL as explicit operator-local preference, or later move to a simpler native format too? - Do `ward agents <tool>` exec guardfiles count as long-term stable product surface or temporary delegation? If they are long-term, keep them as edge-mounted surfaces with strong contract tests. ## Risks - Narrowing `WARD_CONFIG_REF` is a behavior change for anyone using it to steer agent or container internals today. - If ward-owned overlays are not locked by tests, the surface can drift in subtle ways while still "building fine". - Moving fleet/defaults/topology out of `.ward/ward-kdl` needs a doc pass, because several current docs still describe ward-kdl as the source for those runtime knobs. ## Bottom line The design answer is to **stop treating ward-kdl as ward core's runtime control plane**. Keep KDL where it is strongest, at the permission and surface-authoring edge. Move core runtime data and ward-specific Forgejo behavior back into ward. Preserve the existing `ward ops forgejo` UX by composing generated edge surfaces with explicit ward-native helpers, not by teaching KDL to own more of ward's product logic. --- Researched and posted automatically by `ward agent advisor --harness codex` (ward#179). This is one-shot research, not a carried change - verify before acting on it. <!-- ward-agent-reply --> <!-- ward-agent-signature --> — Codex, via `ward agent`
Author
Member

Consult interrogation: design work is complete. The first carrying implementation slice is #845, narrowing WARD_CONFIG_REF to edge surfaces while protecting ward core defaults. Closing this consult shell.

Consult interrogation: design work is complete. The first carrying implementation slice is #845, narrowing WARD_CONFIG_REF to edge surfaces while protecting ward core defaults. Closing this consult shell.
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#721
No description provided.