From ac086b65b9935dc4ff61198f4d39024537d25c9a Mon Sep 17 00:00:00 2001 From: Mathias Date: Mon, 20 Jul 2026 09:46:42 +0200 Subject: [PATCH] =?UTF-8?q?fix(atlas):=20UX=20polish=20=E2=80=94=20stage-0?= =?UTF-8?q?6=20plain=20nodes=20+=20shorter=20arrow=20labels=20(#7)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Stage 06 no longer renders bare in Plain: Build is now tolerant of a jobless/ absent workflow (keeps an authored plain placeholder instead of erroring), and generated CI job nodes carry a plain hint. Shortened the two overlong transition labels ("Order written, sealed, agent-ready" → "Sealed & agent-ready", "Agents produced a change" → "Change proposed") so they stop clipping. Build tolerance test-first. build/vet/lint(0)/test green; screenshot-verified. Co-Authored-By: Claude Opus 4.8 (1M context) --- internal/atlas/atlas.json | 8 +++++--- internal/atlas/build_test.go | 14 ++++++++++++++ internal/atlas/model.go | 6 +++--- 3 files changed, 22 insertions(+), 6 deletions(-) diff --git a/internal/atlas/atlas.json b/internal/atlas/atlas.json index 273ed58..26445f5 100644 --- a/internal/atlas/atlas.json +++ b/internal/atlas/atlas.json @@ -24,7 +24,7 @@ {"t":"Autoresearch Council","plain":"A parallel version of the same review that vets research findings before they're allowed through.","cls":"council","pill":"var(--violet)","d":"Sibling pipe — ratifies research before the gate.","tags":["proposed: → standalone svc"]} ]}, {"no":"STAGE 03","title":"Spec → Gitea issue","plain_title":"Write the work order","plain":"The decision is turned into a precise, self-contained work order an AI agent can execute unsupervised — with a pass/fail definition of done, a risk rating, and a tamper-proof seal.","path":"agent-ready contract", - "trans_label":"Order written, sealed, agent-ready","trans":"Advances to the gate only when the spec is a complete contract: a pass/fail test, a risk tier, a regulatory note, no open human dependencies, one embedded Oath, and a valid cryptographic signature. A malformed or unsigned order fails closed and never reaches the gate.","nodes":[ + "trans_label":"Sealed & agent-ready","trans":"Advances to the gate only when the spec is a complete contract: a pass/fail test, a risk tier, a regulatory note, no open human dependencies, one embedded Oath, and a valid cryptographic signature. A malformed or unsigned order fails closed and never reaches the gate.","nodes":[ {"t":"Contract enforced","plain":"The work order must have a clear pass/fail test, a risk rating, a regulatory-risk note, and no unfinished human dependencies before it counts as agent-ready.","d":"Binary ISC · declared risk tier · reg-risk assessment · no open human deps.","tags":["LOW / MED / HIGH"]}, {"t":"Admission controller","plain":"The work order is cryptographically signed when created, so any later tampering is detectable and the eventual change can be checked against it.","d":"Ed25519-sign issue body at creation (#36). Verify sig + PR alignment at infra boundary.","tags":["chain of custody"]}, {"t":"⚖️ var-go Oath","plain":"A machine-checkable \"definition of done\" is embedded in the work order — exactly one, or the order is rejected — later used to prove the result actually meets the spec.","cls":"oath","pill":"var(--gold)","d":"Acceptance contract embedded in the issue as a var fenced block. Exactly one — zero/multiple fail closed. Prose → typed steps; failures anchored to byte spans.","tags":["swedsl · var-go","defined here → enforced @06"]} @@ -35,14 +35,16 @@ {"t":"Session-Dispatch bridge","plain":"The approval flips a switch that hands the signed work order over to the agents to start execution.","cls":"bridge","pill":"var(--blue)","d":"claude.ai MCP → gitea:workflow_run_trigger → cad-dispatch.yml → agentsquad. The final design→execution bridge.","tags":["workflow_dispatch"]} ]}, {"no":"STAGE 05","cls":"exec","title":"Execute · agentsquad","plain_title":"Agents do the work","plain":"AI agents actually build the thing — one writes, a second independent one reviews it to avoid marking its own homework — and every step is logged for the audit trail.","path":"koala · cmd/agentsquad-serve", - "trans_label":"Agents produced a change","trans":"Advances when the agents finish and open a proposed change (a PR) with its audit log attached. Until there's a concrete change to test, nothing moves.","nodes":[ + "trans_label":"Change proposed","trans":"Advances when the agents finish and open a proposed change (a PR) with its audit log attached. Until there's a concrete change to test, nothing moves.","nodes":[ {"t":"Task API","plain":"A request kicks off a job and hands back an id you can poll for progress.","pill":"var(--coral)","d":"POST /tasks → job id · GET /tasks/{id}. taskqueue + serve (v0.12+).","tags":["single agentsquad.yaml"]}, {"t":"Executor + reviewer loop","plain":"One agent does the work; a second, independent agent on a different model reviews it — so nothing marks its own homework.","cls":"win","pill":"var(--coral)","d":"ADK Go + LiteLLM. Frontier models (local qwen spirals). Reviewer on distinct tier — echo-chamber prevention.","risk":true}, {"t":"dma-cli · routing + scope","plain":"A router sends each agent to the right AI backend and enforces what it is and isn't allowed to touch, with a confirmation gate as a guardrail.","cls":"bridge","pill":"var(--blue)","d":"Harness-config arm: routes agents to the right LLM backend. Three-layer scope policy + confirmation gate = CAD guardrail.","tags":["backend routing","scope guardrail"]}, {"t":"assessor-loop ledger","plain":"Every step is recorded in a tamper-evident log so the whole run can be audited afterwards.","d":"Attestation ledger (audit trail) + brain session_log on completion.","tags":["audit package"]} ]}, {"no":"STAGE 06","title":"PR → CI","plain_title":"Automatic quality checks","plain":"The proposed change is run through automated tests and safety checks — including a check that it actually satisfies the work order's definition of done — and only a clean pass lets it continue.","path":"Gitea Actions · cd.yml (live)","generate":"ci-jobs", - "trans_label":"All checks green","trans":"Advances only if every automated check passes — tests, linters, security scan, and the Oath check proving it meets the original work order. Any red gate stops it here; a passing reviewer is not enough to override a failed Oath.","nodes":[]}, + "trans_label":"All checks green","trans":"Advances only if every automated check passes — tests, linters, security scan, and the Oath check proving it meets the original work order. Any red gate stops it here; a passing reviewer is not enough to override a failed Oath.","nodes":[ + {"t":"Automated checks","plain":"Tests, linters, a security scan, plus a check that the change actually meets the work order — all must pass to continue.","d":"go test · vet · lint · govulncheck + var-go/oath gate.","tags":["green = proceed"]} + ]}, {"no":"STAGE 07","cls":"cd","title":"CD → pod","plain_title":"Ship it","plain":"Once everything is green, the change is deployed automatically to the live server — with the rule that merging code alone doesn't ship it; the release has to be pointed at the new version.","path":"Flux GitOps → k3s","generate":"deploy-state", "trans_label":"It's live","trans":"Once the new version is actually running on the server, the deployed outcome becomes the input to scoring. Advancing means shipped and observable, not just merged.","nodes":[ {"t":"Deploy on green","plain":"When all checks pass, the release system rolls the new version onto the live server automatically — but only once the release is pointed at that version (merging code alone doesn't ship it).","pill":"var(--green)","d":"Flux reconciles image → k3s pod on koala. Push ≠ deploy: bump tag in mathias/infra.","tags":["ntfy on deploy"]} diff --git a/internal/atlas/build_test.go b/internal/atlas/build_test.go index 138e7c6..d417fe5 100644 --- a/internal/atlas/build_test.go +++ b/internal/atlas/build_test.go @@ -64,6 +64,20 @@ func TestDefault_HasPlainLayerAndTransitions(t *testing.T) { } } +func TestBuild_KeepsCIPlaceholderWhenNoJobs(t *testing.T) { + atlasJSON := []byte(`{"substrate":[],"stages":[ + {"no":"STAGE 06","title":"PR → CI","generate":"ci-jobs","nodes":[{"t":"Automated checks","plain":"checks run here"}]} + ]}`) + // A workflow with no jobs block must NOT error and must keep the placeholder. + a, err := atlas.Build(atlasJSON, []byte("name: cd\n")) + if err != nil { + t.Fatalf("Build should tolerate a no-jobs workflow, got: %v", err) + } + if len(a.Stages[0].Nodes) != 1 || a.Stages[0].Nodes[0].Title != "Automated checks" { + t.Fatalf("placeholder not kept: %+v", a.Stages[0].Nodes) + } +} + func TestBuild_ErrorsOnBadAtlasJSON(t *testing.T) { if _, err := atlas.Build([]byte("{not json"), []byte("jobs:\n x:\n a: 1\n")); err == nil { t.Fatal("expected error on bad atlas JSON, got nil") diff --git a/internal/atlas/model.go b/internal/atlas/model.go index 2c69f9c..c86a406 100644 --- a/internal/atlas/model.go +++ b/internal/atlas/model.go @@ -64,12 +64,12 @@ func Build(atlasJSON, workflow []byte) (Atlas, error) { continue } jobs, err := JobsFromWorkflow(workflow) - if err != nil { - return Atlas{}, fmt.Errorf("stage %s: %w", a.Stages[i].No, err) + if err != nil || len(jobs) == 0 { + continue // no CI job list available — keep the authored plain placeholder } nodes := make([]Node, 0, len(jobs)) for _, j := range jobs { - nodes = append(nodes, Node{Title: j}) + nodes = append(nodes, Node{Title: j, Plain: "An automated check that must pass."}) } a.Stages[i].Nodes = nodes }