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:
- A new script author chooses a subtly different marker (
== STAGE ==,[STAGE] foo,STAGE: foo) and the UI silently shows no progress. - Someone "fixes" the regex to also match
=== Step: X ===, which breaks the phase grouping inDeployCard.svelte'sTOP_PHASES. - 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 bydeploy.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.shandinfrastructure/server-bootstrap/stages/*.share the first two; the convention is open-extension.
Rules:
- Additive-only evolution. A future marker family (e.g.
=== SUBSTAGE: X ===) may be added; the existing regex must never change. - 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.
- Redaction. Secret values must never appear inside a marker. Stage titles are always safe to screenshot.
- Cross-repo duplication. The canonical copy of this ADR lives in
dark-factory. The frontend pins it via ADRai-frontend/docs/decisions/002-stage-marker-regex-as-stable-contract.mdso 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-runstill 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.shin 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.sockfor 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, andtail -Falike. - 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: