Decisions

ADR-147: The `=== STAGE: ===` marker is the stable interface between procedural scripts and the Platform UI

`infrastructure/deploy.sh` has been emitting `=== STAGE: <Name> ===` lines since the Platform UI's Deploy tab shipped — the pipeline stepper in `ai-frontend/src/lib/infra/PipelineStepper.svelte` follows the stream by…

#147

ADR-147: The === STAGE: === marker is the stable interface between procedural scripts and the Platform UI

Status: Accepted Date: 2026-04-21

Context

infrastructure/deploy.sh has been emitting === STAGE: <Name> === lines since the Platform UI's Deploy tab shipped — the pipeline stepper in ai-frontend/src/lib/infra/PipelineStepper.svelte follows the stream by matching the regex ^=== STAGE: (.+?) ===\s*$ (see ansi-parse.ts#extractStageMarkers). The convention evolved organically: a few echo "=== STAGE: X ===" lines at the top of each phase in one script.

infrastructure/server-bootstrap/ is the second consumer. More will follow — DB migrations, one-shot repair scripts, onboarding runbooks. Each one should be able to light up the stepper without the UI team shipping a matching release.

Without a written contract, three things happen:

  1. A new script author chooses a subtly different marker (== STAGE ==, [STAGE] foo, STAGE: foo) and the UI silently shows no progress.
  2. Someone "fixes" the regex to also match === Step: X ===, which breaks the phase grouping in DeployCard.svelte's TOP_PHASES.
  3. A developer logs data alongside the marker (=== STAGE: Build (foo) ===) and the UI's dedupe-by-title logic stops working.

Decision

The stage marker contract is frozen at:

  • Regex ^=== STAGE: (.+?) ===\s*$
  • Side channel # INFO: <text> comments immediately following a # STAGE: <title> comment inside the script source (consumed by deploy.sh --info, rendered as tooltips)
  • Semantics the marker appears on stdout, on its own line, at the start of a logical stage. Capture group 1 is the stage title, free text excluding newlines and ===.
  • Ownership any script that operators monitor via the Platform UI emits these markers. deploy.sh and infrastructure/server-bootstrap/stages/*.sh are the first two; the convention is open-extension.

Rules:

  1. Additive-only evolution. A future marker family (e.g. === SUBSTAGE: X ===) may be added; the existing regex must never change.
  2. Title uniqueness per run. Two stages in the same run must not share a title. The UI uses title as identity — duplicates break the stepper's current-vs-done detection.
  3. Redaction. Secret values must never appear inside a marker. Stage titles are always safe to screenshot.
  4. Cross-repo duplication. The canonical copy of this ADR lives in dark-factory. The frontend pins it via ADR ai-frontend/docs/decisions/002-stage-marker-regex-as-stable-contract.md so the web build breaks loudly if the contract drifts.

Consequences

  • Pipeline stepper is reusable. Any new procedural script (bootstrap, migration, rebuild) gets live stepper UI for free — it's the content layer, the stepper is the chrome.
  • The regex is load-bearing. Changing it requires coordinated PRs across dark-factory, ai-frontend, and any consumer we don't yet have. The cost of the contract is paid once at design time and never again.
  • Title collisions are a script bug, not a UI bug. If two phases share a name, the UI will not "fix" it by disambiguating. Authors must choose unique names.
  • Dry-runs and interactive prompts preserve the marker. deploy.sh --dry-run still emits === STAGE: X === before printing [DRY RUN] explanations so the stepper still lights up — this is how we test the UI end-to-end without touching infra. Bootstrap follows the same rule (see stage 01's [DRY RUN] lines).
  • The contract survives refactors. Whoever rewrites deploy.sh in Go/Python/whatever must continue to emit this exact format on stdout.

Alternatives considered

  • Machine-readable JSON events on stderr (one JSON object per stage transition). More principled, harder to read during raw SSH sessions, requires a parser in the UI. The current regex is ugly but operators read the script output directly when debugging a deploy — dual-channel logging loses the "what did the server see?" guarantee.
  • Named-pipe side channel (script writes to /tmp/deploy-events.sock for the UI). Decouples UX from log format at the cost of a new runtime dependency and a new failure mode (UI sees nothing because the socket dropped). The stage marker is robust under | tee logfile, ssh -t, and tail -F alike.
  • Change the regex to tolerate minor variations. Looser is always cheaper short-term; every loosening becomes load-bearing within a week. We picked the bar above and hold it.

Supersedes: none Superseded by: