Epic: rework the diagnostic pipeline, error reporting & renderer
Tracking issue for the foundation rework of how scampi talks to the
user. The current render/cli is convoluted spaghetti (imports
engine, hand-rolled concurrent TTY live-region, half-migrated dead
code), and the diagnostic pipeline needs reshaping to match how scampi
actually behaves.
The core insight: scampi is two tools wearing one CLI
- Stream surface (long-running:
apply, check) — a live narrative
of a run, carrying three interleaved kinds of event:
- diagnostics — warn/error as they happen
- progress — where are we: position (3 of 10), which step is running
- step results — what just finished and how it went
- One-shot surface (
plan, inspect, secrets list, …) — compute a
structured value, print it once. No stream, no goroutine, no live region.
The code half-believes this already (events vs returned PlanDetail),
but the implementation smears them together. The rework makes the
duality real in the architecture.
Design decisions
- Typed run context
diagnostic.Ctx — embeds context.Context and
carries the Emitter, so call sites thread one value, not (ctx, em)
pairs. It is itself an Emitter (free swapped/unswapped boundary).
Typed, not context.WithValue + SinkFrom (the v3 tribal-knowledge trap).
- Progress becomes typed —
{total, completed, current}, not a text blob.
- Step result is its own event, not folded into progress.
- Two consumer contracts — a stream sink (owns the isolated TTY
live-region) and plain one-shot formatters. Both consume types defined
below engine, so neither imports it. Severs render → engine by
construction and quarantines the concurrency.
- Home: grow
diagnostic — it already owns events, the sink, policy,
sourcestore. Absorb model (the report/result types) into the one-shot
surface; reassess signal's home. No new package.
Phases
- Phase 1 — pure plumbing (no behavior change). Introduce
diagnostic.Ctx wrapping the current Emitter; mechanically swap
(ctx, em, …) → (ctx diagnostic.Ctx, …) from the leaves up. Insulates
every call site so the phase-2 redesign stays narrow.
- Phase 2 — the redesign, all behind
Ctx: new event model (typed
Progress, distinct Result), split stream/one-shot surfaces, decoupled
renderer, killed/isolated concurrency.
Notes
- The
cause mechanism is live but minimal: engine/exec.go stamps
CauseHook directly onto Change events; render/cli reads it for hook
attribution. The old WithCause wrapper was dead (no callers) and was
kept out of Ctx. Phase 2 decides whether hook-originated errors also
want attribution (today only Changes get it).
Epic: rework the diagnostic pipeline, error reporting & renderer
Tracking issue for the foundation rework of how scampi talks to the
user. The current
render/cliis convoluted spaghetti (importsengine, hand-rolled concurrent TTY live-region, half-migrated deadcode), and the diagnostic pipeline needs reshaping to match how scampi
actually behaves.
The core insight: scampi is two tools wearing one CLI
apply,check) — a live narrativeof a run, carrying three interleaved kinds of event:
plan,inspect,secrets list, …) — compute astructured value, print it once. No stream, no goroutine, no live region.
The code half-believes this already (events vs returned
PlanDetail),but the implementation smears them together. The rework makes the
duality real in the architecture.
Design decisions
diagnostic.Ctx— embedscontext.Contextandcarries the
Emitter, so call sites thread one value, not(ctx, em)pairs. It is itself an
Emitter(free swapped/unswapped boundary).Typed, not
context.WithValue+SinkFrom(the v3 tribal-knowledge trap).{total, completed, current}, not a text blob.live-region) and plain one-shot formatters. Both consume types defined
below engine, so neither imports it. Severs
render → enginebyconstruction and quarantines the concurrency.
diagnostic— it already owns events, the sink, policy,sourcestore. Absorb
model(the report/result types) into the one-shotsurface; reassess
signal's home. No new package.Phases
diagnostic.Ctxwrapping the currentEmitter; mechanically swap(ctx, em, …)→(ctx diagnostic.Ctx, …)from the leaves up. Insulatesevery call site so the phase-2 redesign stays narrow.
diagnostic.Ctxtype (minimal:context.Context+em)NewCtx, temporary boundary wraps removedCtx: new event model (typedProgress, distinct Result), split stream/one-shot surfaces, decoupled
renderer, killed/isolated concurrency.
Notes
causemechanism is live but minimal:engine/exec.gostampsCauseHookdirectly ontoChangeevents;render/clireads it for hookattribution. The old
WithCausewrapper was dead (no callers) and waskept out of
Ctx. Phase 2 decides whether hook-originated errors alsowant attribution (today only Changes get it).