Watch
2
Make ward agent-agnostic: isolate per-agent code into one package (a folder + docs file per agent) #401
Closed
opened 2026-07-01 06:00:11 +00:00 by coilysiren
·
4 comments
No Branch/Tag specified
main
release
docs/readme-agents-surface
fix/pr-repair-verb-lookup
aos/claude/aw85-docs-bands
aos/claude/aw85-declare-band
aos/claude/bk79-autonomy-label-scope
fix/gofmt-runner
chore/umbra-rename
aos/claude/mg96-fm
claude/agents-temp-clone-note
aos/claude/qa57
remove-format-exec-gate-refusal
fix/ward-1649-ci-fixture
fix/detached-ci-exec
issue-1626-generic-agent-broker
issue-1177
issue-1160
issue-1501
ward-salvage/ward-12486bc7
ward-salvage/ward-babfa2ba
ward-salvage/ward-a832df03
ward-salvage/ward-85c795b2
ward-salvage/ward-b87f8859
issue-1484
ward-salvage/ward-86b72dcf
ward-salvage/ward-b7b26d1d
recovery/2026-07-28-triaged-branch-archive
recovery/2026-07-27-local-work
issue-1584
issue-1571
issue-1524
ward-salvage/ward-3800c2d1
ward-salvage/ward-70175da3
ward-salvage/ward-bcbfef78
issue-1298
issue-737-signoz-deferred
ward-salvage/ward-c6aa5da9
ward-salvage/ward-94dd7346
ward-salvage/ward-58aa3fe3
ward-salvage/ward-ff6c509f
ward-salvage/ward-e80f0460
ward-salvage/ward-3e2e4760
ward-salvage/ward-649addd7
ward-salvage/ward-890e4d29
ward-salvage/ward-645b750b
ward-salvage/ward-bf851a72
ward-salvage/ward-98c8652f
ward-salvage/ward-bb10b620
ward-salvage/ward-1ee9be1c
ward-salvage/ward-d18595f6
ward-salvage/ward-5f914692
ward-salvage/ward-6ca05cbd
ward-salvage/ward-14f676cf
ward-salvage/ward-de811c20
ward-salvage/ward-eba3e824
ward-salvage/ward-16eb4ad0
ward-salvage/ward-c58e9c43
ward-salvage/ward-7487270b
v0.890.0
v0.889.0
v0.888.0
v0.887.0
v0.886.0
v0.885.0
v0.884.0
v0.883.0
v0.882.0
v0.881.0
v0.880.0
v0.879.0
v0.878.0
v0.877.0
v0.876.0
v0.875.0
v0.874.0
v0.873.0
v0.872.0
v0.871.0
v0.870.0
v0.869.0
v0.868.0
v0.867.0
v0.866.0
v0.865.0
v0.864.0
v0.863.0
v0.862.0
v0.861.0
v0.860.0
v0.859.0
v0.858.0
v0.857.0
v0.856.0
v0.855.0
v0.854.0
v0.853.0
v0.852.0
v0.851.0
v0.850.0
v0.849.0
v0.848.0
v0.847.0
v0.846.0
v0.845.0
v0.844.0
v0.843.0
v0.842.0
v0.841.0
v0.840.0
v0.839.0
v0.838.0
v0.837.0
v0.836.0
v0.835.0
v0.834.0
v0.833.0
v0.832.0
v0.830.0
v0.831.0
v0.829.0
v0.828.0
v0.827.0
v0.826.0
v0.825.0
v0.824.0
v0.823.0
v0.822.0
v0.821.0
v0.820.0
v0.819.0
v0.818.0
v0.817.0
v0.816.0
v0.815.0
v0.814.0
v0.813.0
v0.812.0
v0.811.0
v0.810.0
v0.809.0
v0.808.0
v0.807.0
v0.806.0
v0.805.0
v0.804.0
v0.803.0
v0.802.0
v0.801.0
v0.800.0
v0.799.0
v0.798.0
v0.797.0
v0.796.0
v0.795.0
v0.794.0
v0.793.0
v0.792.0
v0.791.0
v0.790.0
v0.789.0
v0.788.0
v0.787.0
v0.786.0
v0.785.0
v0.784.0
v0.783.0
v0.782.0
v0.781.0
v0.780.0
v0.779.0
v0.778.0
v0.777.0
v0.775.0-tmp
v0.776.0
v0.775.0
v0.774.0
v0.773.0
v0.772.0
v0.771.0
v0.770.0
v0.769.0
v0.768.0
v0.767.0
v0.766.0
v0.765.0
v0.764.0
v0.763.0
v0.762.0
v0.761.0
v0.760.0
v0.759.0
v0.758.0
v0.757.0
v0.756.0
v0.755.0
v0.754.0
v0.753.0
v0.752.0
v0.751.0
v0.750.0
v0.749.0
v0.748.0
v0.747.0
v0.746.0
v0.745.0
v0.744.0
v0.743.0
v0.742.0
v0.741.0
v0.740.0
v0.739.0
v0.738.0
v0.737.0
v0.736.0
v0.735.0
v0.734.0
v0.733.0
v0.732.0
v0.731.0
v0.730.0
v0.729.0
v0.728.0
v0.727.0
v0.726.0
v0.725.0
v0.724.0
v0.723.0
v0.722.0
v0.721.0
v0.720.0
v0.719.0
v0.718.0
v0.717.0
v0.716.0
v0.715.0
v0.714.0
v0.713.0
v0.712.0
v0.711.0
v0.710.0
v0.709.0
v0.708.0
v0.707.0
v0.706.0
v0.705.0
v0.704.0
v0.703.0
v0.702.0
v0.701.0
v0.700.0
v0.699.0
v0.698.0
v0.697.0
v0.696.0
v0.695.0
v0.694.0
v0.693.0
v0.692.0
v0.691.0
v0.690.0
v0.689.0
v0.688.0
v0.687.0
v0.686.0
v0.685.0
v0.684.0
v0.683.0
v0.682.0
v0.681.0
v0.680.0
v0.679.0
v0.678.0
v0.677.0
v0.676.0
v0.675.0
v0.674.0
v0.673.0
v0.672.0
v0.671.0
v0.670.0
v0.669.0
v0.668.0
v0.667.0
v0.666.0
v0.665.0
v0.664.0
v0.663.0
v0.662.0
v0.661.0
v0.660.0
v0.659.0
v0.658.0
v0.657.0
v0.656.0
v0.655.0
v0.654.0
v0.653.0
v0.652.0
v0.651.0
v0.650.0
v0.649.0
v0.648.0
v0.647.0
v0.646.0
v0.645.0
v0.644.0
v0.643.0
v0.642.0
v0.641.0
v0.640.0
v0.639.0
v0.638.0
v0.637.0
v0.636.0
v0.635.0
v0.634.0
v0.633.0
v0.632.0
v0.631.0
v0.630.0
v0.629.0
v0.628.0
v0.627.0
v0.626.0
v0.625.0
v0.624.0
v0.623.0
v0.622.0
v0.621.0
v0.620.0
v0.619.0
v0.618.0
v0.617.0
v0.616.0
v0.615.0
v0.614.0
v0.613.0
v0.612.0
v0.611.0
v0.610.0
v0.609.0
v0.608.0
v0.607.0
v0.606.0
v0.605.0
v0.604.0
v0.603.0
v0.602.0
v0.601.0
v0.600.0
v0.599.0
v0.598.0
v0.597.0
v0.596.0
v0.595.0
v0.594.0
v0.593.0
v0.592.0
v0.591.0
v0.590.0
v0.589.0
v0.588.0
v0.587.0
v0.586.0
v0.585.0
v0.584.0
v0.583.0
v0.582.0
v0.581.0
v0.580.0
v0.579.0
v0.578.0
v0.577.0
v0.576.0
v0.575.0
v0.574.0
v0.573.0
v0.572.0
v0.571.0
v0.570.0
v0.569.0
v0.568.0
v0.567.0
v0.566.0
v0.565.0
v0.564.0
v0.563.0
v0.562.0
v0.561.0
v0.560.0
v0.559.0
v0.558.0
v0.557.0
v0.556.0
v0.555.0
v0.554.0
v0.553.0
v0.552.0
v0.551.0
v0.550.0
v0.549.0
v0.548.0
v0.547.0
v0.546.0
v0.545.0
v0.544.0
v0.543.0
v0.542.0
v0.541.0
v0.540.0
v0.539.0
v0.538.0
v0.537.0
v0.536.0
v0.535.0
v0.534.0
v0.533.0
v0.532.0
v0.531.0
v0.530.0
v0.529.0
v0.528.0
v0.527.0
v0.526.0
v0.525.0
v0.524.0
v0.523.0
v0.522.0
v0.521.0
v0.520.0
v0.519.0
v0.518.0
v0.517.0
v0.516.0
v0.515.0
v0.514.0
v0.513.0
v0.512.0
v0.511.0
v0.510.0
v0.509.0
v0.508.0
v0.507.0
v0.506.0
v0.505.0
v0.504.0
v0.503.0
v0.502.0
v0.501.0
v0.500.0
v0.499.0
v0.498.0
v0.497.0
v0.496.0
v0.495.0
v0.494.0
v0.493.0
v0.492.0
v0.491.0
v0.490.0
v0.489.0
v0.488.0
v0.487.0
v0.486.0
v0.485.0
v0.484.0
v0.483.0
v0.482.0
v0.481.0
v0.480.0
v0.479.0
v0.478.0
v0.477.0
v0.476.0
v0.475.0
v0.474.0
v0.473.0
v0.472.0
v0.471.0
v0.470.0
v0.469.0
v0.468.0
v0.467.0
v0.466.0
v0.465.0
v0.464.0
v0.463.0
v0.462.0
v0.461.0
v0.460.0
v0.459.0
v0.458.0
v0.457.0
v0.456.0
v0.455.0
v0.454.0
v0.453.0
v0.452.0
v0.451.0
v0.450.0
v0.449.0
v0.448.0
v0.447.0
v0.446.0
v0.445.0
v0.444.0
v0.443.0
v0.442.0
v0.441.0
v0.440.0
v0.439.0
v0.438.0
v0.437.0
v0.436.0
v0.435.0
v0.434.0
v0.433.0
v0.432.0
v0.431.0
v0.430.0
v0.429.0
v0.428.0
v0.427.0
v0.426.0
v0.425.0
v0.424.0
v0.423.0
v0.422.0
v0.421.0
v0.420.0
v0.419.0
v0.418.0
v0.417.0
v0.416.0
v0.415.0
v0.414.0
v0.413.0
v0.412.0
v0.411.0
v0.410.0
v0.409.0
v0.408.0
v0.407.0
v0.406.0
v0.405.0
v0.404.0
v0.403.0
v0.402.0
v0.401.0
v0.400.0
v0.399.0
v0.398.0
v0.397.0
v0.396.0
v0.395.0
v0.394.0
v0.393.0
v0.392.0
v0.391.0
v0.390.0
v0.389.0
v0.388.0
v0.387.0
v0.386.0
v0.385.0
v0.384.0
v0.383.0
v0.382.0
v0.381.0
v0.380.0
v0.379.0
v0.378.0
v0.377.0
v0.376.0
v0.375.0
v0.374.0
v0.373.0
v0.372.0
v0.371.0
v0.370.0
v0.369.0
v0.368.0
v0.367.0
v0.366.0
v0.365.0
v0.364.0
v0.363.0
v0.362.0
v0.361.0
v0.360.0
v0.359.0
v0.358.0
v0.357.0
v0.356.0
v0.355.0
v0.354.0
v0.353.0
v0.352.0
v0.351.0
v0.350.0
v0.349.0
v0.348.0
v0.347.0
v0.346.0
v0.345.0
v0.344.0
v0.343.0
v0.342.0
v0.341.0
v0.340.0
v0.339.0
v0.338.0
v0.337.0
v0.336.0
v0.335.0
v0.334.0
v0.333.0
v0.332.0
v0.331.0
v0.330.0
v0.329.0
v0.328.0
v0.327.0
v0.326.0
v0.325.0
v0.324.0
v0.323.0
v0.322.0
v0.321.0
v0.320.0
v0.319.0
v0.318.0
v0.317.0
v0.316.0
v0.315.0
v0.314.0
v0.313.0
v0.312.0
v0.311.0
v0.310.0
v0.309.0
v0.308.0
v0.307.0
v0.306.0
v0.305.0
v0.304.0
v0.303.0
v0.302.0
v0.301.0
v0.300.0
v0.299.0
v0.298.0
v0.297.0
v0.296.0
v0.295.0
v0.294.0
v0.293.0
v0.292.0
v0.291.0
v0.290.0
v0.289.0
v0.288.0
v0.287.0
v0.286.0
v0.285.0
v0.284.0
v0.283.0
v0.282.0
v0.281.0
v0.280.0
v0.279.0
v0.278.0
v0.277.0
v0.276.0
v0.275.0
v0.274.0
v0.273.0
v0.272.0
v0.271.0
v0.270.0
v0.269.0
v0.268.0
v0.267.0
v0.266.0
v0.265.0
v0.264.0
v0.263.0
v0.262.0
v0.261.0
v0.260.0
v0.259.0
v0.258.0
v0.257.0
v0.256.0
v0.255.0
v0.254.0
v0.253.0
v0.252.0
v0.251.0
v0.250.0
v0.249.0
v0.248.0
v0.247.0
v0.246.0
v0.245.0
v0.244.0
v0.243.0
v0.242.0
v0.241.0
v0.240.0
v0.239.0
v0.238.0
v0.237.0
v0.236.0
v0.235.0
v0.234.0
v0.233.0
v0.232.0
v0.231.0
v0.230.0
v0.229.0
v0.228.0
v0.227.0
v0.226.0
v0.225.0
v0.224.0
v0.223.0
v0.222.0
v0.221.0
v0.220.0
v0.219.0
v0.218.0
v0.217.0
v0.216.0
v0.215.0
v0.214.0
v0.213.0
v0.212.0
v0.211.0
v0.210.0
v0.209.0
v0.208.0
v0.207.0
v0.206.0
v0.205.0
v0.204.0
v0.203.0
v0.202.0
v0.201.0
v0.200.0
v0.199.0
v0.198.0
v0.197.0
v0.196.0
v0.195.0
v0.194.0
v0.193.0
v0.192.0
v0.191.0
v0.190.0
v0.189.0
v0.188.0
v0.187.0
v0.186.0
v0.185.0
v0.184.0
v0.183.0
v0.182.0
v0.181.0
v0.180.0
v0.179.0
v0.178.0
v0.177.0
v0.176.0
v0.175.0
v0.174.0
v0.173.0
v0.172.0
v0.171.0
v0.170.0
v0.169.0
v0.168.0
v0.167.0
v0.166.0
v0.165.0
v0.164.0
v0.163.0
v0.162.0
v0.161.0
v0.160.0
v0.159.0
v0.158.0
v0.157.0
v0.156.0
v0.155.0
v0.154.0
v0.153.0
v0.152.0
v0.151.0
v0.150.0
v0.149.0
v0.148.0
v0.147.0
v0.146.0
v0.145.0
v0.144.0
v0.143.0
v0.142.0
v0.141.0
v0.140.0
v0.139.0
v0.138.0
v0.137.0
v0.136.0
v0.135.0
v0.134.0
v0.133.0
v0.132.0
v0.131.0
v0.130.0
v0.129.0
v0.128.0
v0.127.0
v0.126.0
v0.125.0
v0.124.0
v0.123.0
v0.122.0
v0.121.0
v0.120.0
v0.119.0
v0.118.0
v0.117.0
v0.116.0
v0.115.0
v0.114.0
v0.113.0
v0.112.0
v0.111.0
v0.110.0
v0.109.0
v0.108.0
v0.107.0
v0.106.0
v0.105.0
v0.104.0
v0.103.0
v0.102.0
v0.101.0
v0.100.0
v0.99.0
v0.98.0
v0.97.0
v0.96.0
v0.95.0
v0.94.0
v0.93.0
v0.92.0
v0.91.0
v0.90.0
v0.89.0
v0.88.0
v0.87.0
v0.86.0
v0.85.0
v0.84.0
v0.83.0
v0.82.0
v0.81.0
v0.80.0
v0.79.0
v0.78.0
v0.77.0
v0.76.0
v0.75.0
v0.74.0
v0.73.0
v0.72.0
v0.71.0
v0.70.0
v0.69.0
v0.68.0
v0.67.0
v0.66.0
v0.65.0
v0.64.0
v0.63.0
v0.62.0
v0.61.0
v0.60.0
v0.59.0
v0.58.0
v0.57.0
v0.56.0
v0.55.0
v0.54.0
v0.53.0
v0.52.0
v0.51.0
v0.50.0
v0.49.0
v0.48.0
v0.47.0
v0.46.0
v0.45.0
v0.44.0
v0.43.0
v0.42.0
v0.41.0
v0.40.0
v0.39.0
v0.38.0
v0.37.0
v0.36.0
v0.35.0
v0.34.0
v0.33.0
v0.32.0
v0.31.0
v0.30.0
v0.29.0
v0.28.0
v0.27.0
v0.26.0
v0.25.0
v0.24.0
v0.23.0
v0.22.0
v0.21.0
v0.20.0
v0.19.0
v0.18.0
v0.17.0
v0.16.0
v0.15.0
v0.14.0
v0.13.0
v0.12.0
v0.11.0
v0.10.0
v0.9.0
v0.8.0
v0.7.0
v0.6.0
v0.5.8
v0.5.7
v0.5.6
v0.5.5
v0.5.4
v0.5.3
v0.5.2
v0.5.1
v0.5.0
v0.4.0
v0.3.0
v0.2.2
v0.2.1
v0.2.0
v0.1.3
v0.1.2
v0.1.1
v0.1.0
v0.0.18
v0.0.17
v0.0.16
v0.0.15
v0.0.14
v0.0.13
v0.0.12
v0.0.11
v0.0.10
v0.0.9
v0.0.8
v0.0.7
v0.0.6
v0.0.5
v0.0.4
v0.0.3
v0.0.2
v0.0.1
Labels
Clear labels
burndown-2026-06
Backlog burndown June 2026
pressure-test
Cold-read release pressure-test findings and coordination
sunday-sprint
Burn-down by Sunday 2026-06-07
autonomy
async-consult
A human needs to consult on the issue to upgrade it to headless
autonomy
epic
This issue has many units of sub work - its size makes it meaningfully exclusive with other autonomy types
autonomy
headless
The agent can perform the work on its own
autonomy
live-collab
The agent and the human need to work together in realtime
coherence-core
Core review set for the warded control plane coherence milestone. These issues form the release spine; adjacent milestone issues are stretch or supporting work.
priority
P0
priority tier
priority
P1
priority tier
priority
P2
priority tier
priority
P3
priority tier
priority
P4
priority tier
qa-fixture
Disposable issue admitted to the bounded Ward QA verification lane.
role/advocate
requires work from the Developer Advocate seat
role/director
requires work from the Portfolio Director seat
role/exec
requires work from the exec role
role/frontend
requires work from the Frontend Engineer seat
role/gamedev
requires work from the Game Developer seat
role/human
requires a person, and specifically not an agent seat
role/platform
requires work from the Platform Engineer seat
role/qa
requires work from the QA role
role/science
requires work from the Applied Scientist seat
role/sysadmin
requires work from the Systems Administrator seat
state
ambient
ambient and ephemeral work, held as a maintained document rather than a queue
No labels
burndown-2026-06
pressure-test
sunday-sprint
autonomy
async-consult
autonomy
epic
autonomy
headless
autonomy
live-collab
coherence-core
priority
P0
priority
P1
priority
P2
priority
P3
priority
P4
qa-fixture
role/advocate
role/director
role/exec
role/frontend
role/gamedev
role/human
role/platform
role/qa
role/science
role/sysadmin
state
ambient
Milestone
Clear milestone
No items
No milestone
Projects
Clear projects
No items
No project
Assignees
Clear assignees
No assignees
1 participant
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#401
Loading…
Reference in a new issue
No description provided.
Delete branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Design ticket. Filed from the read-only director surface (she/her).
Thesis
rg -i claude | wc -l=> 483. Per-agent logic - creds seeding, onboarding, config compose, install, vision capability, exec/argv, mode dispatch - is scattered acrosscontainer_bootstrap.go,container_compute.go,agent_signature.go,agent.go, and a dozen more files. ward's core should be agent-agnostic: the per-agent special logic isolated into a single package with a folder per agent that configures everything special about that agent, paired with a dedicated docs file per agent. Adding a harness should mean one folder + one docs file + one registry entry, not editing hundreds of sites.This is the code half. It is orthogonal to and composes with the data half already designed (see reconciliation below).
Grounding (the scatter, measured in /substrate/ward)
Per-driver hit counts, non-test,
cmd/ward: claude 124, codex 80, goose 72, ollama 47, qwen 37, opencode 30. Densest cluster:container_bootstrap.go(51 claude hits) - all the per-agent creds/config/install functions.The per-agent CODE seams (57 mode-dispatch sites) - the functions to consolidate behind an interface:
visionCapable()-container_compute.go:179parseMode()-container_compute.go:239agentIdentity()-agent_signature.go:20(mostly DATA - see #306; the lookup mechanism stays, the values move)installOpencode()-container_bootstrap.go:406writeClaudeCreds()-container_bootstrap.go:844seedClaudeOnboarding()-container_bootstrap.go:870writeCodexCreds()-container_bootstrap.go:903composeCodexConfig()-container_bootstrap.go:934composeGooseConfig()-container_bootstrap.go:998Note the qwen/opencode entanglement flagged in #308: ward has no
opencodemode name - it calls itmodeQwen-> "qwen" -> binaryopencode. The design should untangle the roster naming as part of this.Relationship to #306 / #308 (must not conflict)
agents.yaml(model, endpoint, identity, attribution) - channel B + cli-guard parser C, Kai-decided.The boundary the design must draw crisply: data -> agents.yaml (#306); code -> per-agent package (this). No site claimed by both.
agentIdentity()is the test case - the values are data (#306), the interface method that serves them is code (here).Deliverable (design comment, no code)
An advisor design that:
cmd/ward/agents/<name>/vs apkg/agents), and how it coexists withagent_adapter.go's existing manifest.mode->binarymapping, context-level ladder, env-var names - #308 already marks these as legitimately code-shaped).agents.yamland the package do not overlap.docs/agent-<name>.md) - reconcile with the agent docs that already exist (agent-credentials,agent-local-harnesses,agent-adapter-manifest,agent-roster).Then Kai picks the package boundary and the build issues get dispatched.
Mode
consult - design ticket, human picks the boundary. Companion to #306 (data) and #308 (ownership audit).
Recovered advisor design (posted from the director surface). The advisor produced this twice but could not post it - both runs died on ward#402 (
commentIssue --body-fileflag skew on the host write path). Kai recovered the streamed output from the host dispatch log. Reproducing the more refined of the two runs here so the deliverable is durable in-thread; an earlier run with a slightly different layout is in the host log if a second angle helps. One correction from Kai supersedes the design where they differ:visionCapableshould be retired, not moved toagents.yaml(its sole use is the ward#400 seed branch - see note at the end). Design below is the advisor's, verbatim.Advisor design: agent-agnostic ward via a per-agent SPI + folder-per-agent registry
Read live against
/substrate/wardat HEAD. ward is already mid-migration on exactly this pattern for the data half:agent_adapter.go+ the embeddedcontainerassets/agent-adapters.yaml+agent_adapter_test.goare the ward#152 precedent - a manifest mirroring the live Go switches, pinned entry-for-entry by a contract test, switches still live and dying last. This design generalizes that same three-part move from data to code, so it composes with #306/#308 by construction.Two ground-truth corrections up front:
agent.go:210:agentModes = {modeClaude, modeCodex, modeQwen, modeGoose}). Noollamamode, noopencodemode. The issue's hit counts (ollama 47, opencode 30) are backend/binary references, not roster entries. The package roster is four folders, not six.agent-roster.mdis a different axis. The roster (agent_roster.go) enumerates roles (engineer/director/advisor). This ticket is about harnesses (claude/codex/opencode/goose). Two orthogonal registries - keep them separate in the docs or they get conflated forever.The real shape of the scatter (two-host seam)
Per-agent code is a two-host seam: the launching host resolves+injects, the container writes+launches. Both sides branch per agent. An interface covering only the container half leaves half the scatter behind.
container.go):resolveClaudeCreds(:213, macOS keychain else~/.claude/.credentials.json),resolveCodexCreds(:275),resolveOllamaHost(:288, SSM),claudeCredsHealth(:243),credEnvLines(:300).container_bootstrap.go):installOpencode(:406),writeClaudeCreds(:844),seedClaudeOnboarding(:870),writeCodexCreds(:903),composeCodexConfig(:934),composeOpencodeConfig+opencodeConfigJSON(:961/978),composeGooseConfig+gooseConfigYAML(:998/1029),smokeTestClaudeAuth(:1094), per-mode arms ofbuildAgentArgv(:1304) andlogAgentArgv(:1338).container_compute.go):visionCapable(:179),agentBinary(:191),hostPreflightArgv(:208),contextLevel(:223),parseMode(:239).agent_signature.go):agentIdentity(:20) +agentSigner/signBody/commitTrailer.The anti-pattern tell: every container-side function is shaped
func (r *Runner) composeGooseConfig(e) { if e.Mode != "goose" { return }; ... }- dispatch inverted, the function self-skips;runContainerBootstrap(:207-225) calls all six unconditionally, five no-op. The registry inverts that: core holdsagent := registry.Lookup(mode)and only that agent's capabilities fire. Done-condition, grep-measurable: after migration noe.Mode == "..."/case modeX:exists in core.1. The per-agent interface (capability-shaped, not one god method)
Do not define a single fat interface - most agents no-op most methods (only claude onboards, only qwen installs, only claude smoke-tests). Model it as a small core record served from the manifest plus optional capability interfaces core feature-tests. Idiomatic Go (
io.ReaderFrom-style); "this agent doesn't do X" is the absence of an impl, not a guard clause.RunCtx/HostCtxare narrow structs carved out ofbootstrapEnv, carrying only what agents touch (AgentHome,TargetName, model/effort fields, ollama URL, abloglogger, anExec). Do not pass the wholeRunner/bootstrapEnvacross the boundary - that reintroduces the import cycle you just split.if c, ok := agent.(agentspi.Installer); ok { c.Install(rc) }, etc.runContainerBootstrapsequence, git auth, clone, pre-commit install, read-only push guard, extra-repo clone, substrate warm, context compose, permissions compose, reap, chown, setpriv drop, the stream-json wrapper,looksLikeAuthError/commandExists/lowDiskPaths/envOr/blog/base64, the manifest parse+validate, and theWARD_*env-var names (shared wire contract).2. Package layout + registry
Core imports only
agentspi+registry, never a concrete agent. Adding a harness = new folder + one registry line + one manifest entry + one docs file - the deliverable's success test, met literally. Recommendinternal/agents/<name>/overpkg/agents: these are ward-internal orchestration glue, not a reusable library; reservepkg/(cli-guard) for the genuinely reusable engine whereattribution.Signeralready lives.Honest cost: ward is today one flat
package mainincmd/wardwith unexportedRunner/bootstrapEnv. Sub-packages can't reach unexportedmainsymbols, so this requires carving the narrowRunCtx/HostCtxstructs intoagentspi- that carve is the first real unit of work, and why the contract-test net matters (moving behavior across a new boundary, not just renaming). Coexistence:agent_adapter.gobecomes the data layer -agentAdapter->agentspi.Manifest(superset: today's fields + #306 identity/model/endpoint),loadAgentManifest()stays the loader, the registry injects each agent's record at construction.agent-adapters.yamland #306'sagents.yamlconverge into one embedded manifest - do not ship two.3. Data-vs-code boundary vs #306 (no site claimed by both)
agents.yaml(#306): name, binary, contextLevel, stream-format id, auth-kind enum, argv dialects,identity{name,pronouns}, model defaults, ollama URL default, attribution email domain.agentIdentity()worked example: strings"Claude"/"she/her"are data (agents.yaml.identity, #306 owns the key); theSigner()method reading them intoattribution.Signeris code (this ticket owns the method). Symmetric for auth:auth: claude-keychainis data (#306);security find-generic-password -s "Claude Code-credentials"is code (here). One enum value, one procedure, no overlap. Explicitly not this ticket: #308 Bucket C (infra topology) -> infra/SSM; Bucket B (doctrine prose) -> referenced not recopied.4. Docs per agent
docs/agent-<name>.mddocumentsinternal/agents/<name>- capabilities, cred channel, config-file shape, install stance, launch dialect, smoke gate. One doc to one folder, guarded by a drift test (a registered agent with no doc = red build, theagent-rosterself-curing pattern). Roster of four:agent-claude.md,agent-codex.md,agent-opencode.md,agent-goose.md. Reconcile:agent-adapter-manifest.mdstays as the data schema doc (each agent doc links to its manifest entry);agent-credentials.mdkeeps the shared channel and delegates per-agent specifics;agent-local-harnesses.mdsplits intoagent-opencode.md+agent-goose.md;agent-roster.mdleft alone (roles axis) with one sentence added toagent.mddrawing the roles-vs-harnesses line.5. Migration order (ward#152 pattern, contract-green throughout, switch dies last)
Invariant: at no commit do the live switches and the new registry disagree - a contract test fails the build if they do (generalizing
TestAgentManifestMatchesHardcodedSwitchesfrom data to behavior).agents.yaml(identity/model/endpoint on the existing adapter manifest). Ward embeds. No control-flow change.internal/agentspi(interface +RunCtx/HostCtx). Pure extraction, core still switches. Green.mainfunctions (thin wrappers); build the registry; flip no call site; add the generalized contract test (registry output == live switch output). Both live, test pins them. Green.parseMode's switch with a membership check, delete the dead switches + orphaned funcs in one sweep. Contract test flips from "registry == switch" to "registry == golden fixtures". This is whererg -i claudedrops.6. qwen -> opencode roster untangle (#308's naming collision)
Category error: every sibling names its harness (claude/codex/goose);
qwennames a model. Mode string"qwen"(modeQwen) -> binaryopencode(agentBinary,container_compute.go:196) -> backendollama/qwen3-coder:30b. And goose also runsollama/qwen3-coder, so "qwen" can't identify a harness. Resolution, landed in Phase 2 so it rides the contract net: renamemodeQwen -> modeOpencode, roster"qwen" -> "opencode", packageinternal/agents/opencode/; the modelqwen3-coder:30bmoves toagents.yamlas a datamodelfield; keep a--mode qwenback-compat alias in the registry/parseModewith a deprecationblog, do not hard-break in the rename PR. One data decision for #306, flag don't decide: whether opencode signsOpencode,Qwen (via opencode), or keepsQwen- anagents.yamlidentity value.7. Implementation issues to file, sequenced, with landing repo
agents.yaml: add identity/model/endpoint/(vision - see Kai's note) to the adapter manifest. Blocks everything.internal/agentspi(interface +RunCtx/HostCtx); no behavior moves. (Phase 1)internal/agents/<name>packages delegating to live funcs + generalized contract test; registry built, no call sites flipped. (Phase 2)--mode qwenalias. (Phase 2)CredentialProvider. (Phase 3)ConfigComposer. (Phase 3)Record(); delete the dead switches +parseModeenumeration (switch dies last). (Phase 4)docs/agent-<name>.mdper package + drift test; reconcileagent-credentials.md, split/retireagent-local-harnesses.md, add the roles-vs-harnesses note. (Phase 5)Boundary for Kai to pick:
internal/agents/<name>sub-packages withHostCtx(compiler-enforced isolation, more plumbing) vs file-per-agent in one package holding*Runner(lighter, isolation by convention). Advisor recommends the sub-package form. Mode: consult.Kai's correction (supersedes the design on this point):
visionCapableis not moving toagents.yaml. Its only consumer is theagent.go:168seed branch that ward#400 is already rewriting to inline the body for all drivers. It is being retired as a rider on ward#400, so drop the proposedvision:manifest field from issue #1 above.Boundary decided (Kai, via director surface): sub-packages + HostCtx - the advisor's recommended form.
internal/agents/<name>/sub-packages implementing theagentspiSPI over narrowRunCtx/HostCtxstructs carved out ofbootstrapEnv. Isolation is compiler-enforced: core cannot reach per-agent internals, and the struct carve is accepted as the first real unit of work. NOT the file-per-agent-on-*Runnerform.Sequenced execution plan
Data prerequisite (aos, not this ticket): aos#306 (agents.yaml authoring) + aos#308 (ownership audit / move-list) are the DATA half. ward#401 Phase 2+ consumes the landed manifest; do not re-file that work here. Phase 1 below has NO data dependency.
Strict contract-green invariant throughout (generalize
TestAgentManifestMatchesHardcodedSwitchesfrom data to behavior): at no commit do the live switches and the registry disagree. The director cannot dispatch these out of order (no dependency graph), so each phase's issue is filed only once its predecessor merges - NOT all at once.internal/agentspi(theAgentinterface + capability interfaces +RunCtx/HostCtx/Manifest/EnvLinevalue types). Pure extraction, core still switches, no behavior moves. Green trivially.internal/agents/{claude,codex,opencode,goose}packages, each capability delegating to the still-livemainfuncs (thin wrappers); buildregistry.go; flip NO call site; add the generalized contract test. Fold the qwen->opencode roster untangle here (renamemodeQwen->modeOpencode, keep a--mode qwenback-compat alias with a deprecation log).CredentialProvider), then config (ConfigComposer), then install/onboarding/smoke-gate. Each its own small PR, old func unreferenced-but-present, contract green.parseMode's switch with a membership check and delete the dead switches + orphaned funcs in one sweep. Contract test flips from 'registry == switch' to 'registry == golden fixtures'. This is whererg -i claudedrops.docs/agent-<name>.mdper package + a drift test (registered agent with no doc = red build); reconcileagent-credentials.md, splitagent-local-harnesses.mdintoagent-opencode.md+agent-goose.md, add a roles-vs-harnesses note toagent.md(agent-roster.mdleft alone - it's the roles axis).Correction already folded:
visionCapableis retired as a rider on ward#400 (its sole consumer was the seed branch #400 rewrote), not moved to the manifest.Filing + dispatching Phase 1 now; subsequent phases get filed as each predecessor lands.
Manifest home decided upstream (aos#310): the 'one embedded manifest' this design converges on is authored in ward-kdl as a new config dialect (dialect 2) - compiled + cli-guard-validated + embedded into ward, NOT fetched. A director-surface consult with Kai locked a config-placement law ('config lives at the lowest layer that fully determines it, never fetched downward; aos authors zero ward-runtime config') and broadened ward-kdl from 'the audited-CLI generator' to 'the build-time authoring layer'. So the
internal/agents/<name>SPI reads its data (Record()/Manifest) from that ward-kdl-authored embedded manifest, andagent-adapters.yamlfolds into it. No change to this ticket's phases - Phase 1 (ward#410, in flight) carves the SPI types and is unaffected; later phases source manifest data from the ward-kdl dialect once aos#310's design lands. Full design + build sequence: aos#310.Complete - all 5 phases landed; ward is now agent-agnostic.
internal/agentspi(SPI +RunCtx/HostCtx) - ward#410internal/agents/{claude,codex,opencode,goose}packages + registry + contract test;qwen->opencodeuntangle with aqwenback-compat alias; capabilities forward via injectedagents_wire.goclosures (the boundary made "delegate to unexported main funcs" impossible - resolved cleanly) - ward#412runContainerBootstrap+ the data-read call sites now dispatch through the registry; switches retained - ward#418parseModecollapsed into afleetconfigmembership check, dead switches deleted, contract pinned to a golden fixture (switch died last) - ward#420docs/agent-<name>.md+ a registry-backed drift test; splitagent-local-harnesses.md; roles-vs-harnesses note - ward#422The SPI reads its per-agent data from the embedded
fleetconfig.Fleet(aos#310 dialect 2), so "one embedded manifest, do not ship two" is now sourced, not aspirational. Boundary chosen: sub-packages +HostCtx(compiler-enforced isolation).Follow-up: ward#423 (golden-fixture sync smell, low priority). Note: Phases 4-5 landed on
--driver codex(host Claude auth smoke test wedged - worth a re-auth). Closing.