diff --git a/Taskfile.yml b/Taskfile.yml index bf612ee..c5627e2 100644 --- a/Taskfile.yml +++ b/Taskfile.yml @@ -103,6 +103,7 @@ tasks: - bash tests/smoke/caddy-mvp-s05-03-offline.sh - bash tests/smoke/e8-s03-01.sh - bash tests/meta/doc10-roadmap-status.yaml + - bash tests/meta/e1g-standing-policy.yaml - bash tests/meta/release-provenance.yaml - bash tests/meta/verify-fetch-depth.yaml - bash tests/meta/batch2-smoke-wired.yaml @@ -416,7 +417,7 @@ tasks: - bash tests/smoke/e8b-offline.sh e8b:up: - desc: "E8b — LIVE on-demand demo bring-up (COSTS MONEY; DECIDED-B on-demand, NOT standing). Composes the E1g GSK substrate, re-syncs the GitOps app-of-apps, waits healthy. Tear down with e8b:down after the demo." + desc: "E8b — LIVE on-demand demo bring-up (COSTS MONEY; DECIDED-B on-demand, ephemeral-by-default — a standing go-live substrate is a separate recorded and time-boxed carve-out, D-042/E1g-S07). Composes the E1g GSK substrate, re-syncs the GitOps app-of-apps, waits healthy. Tear down with e8b:down after the demo." env: KUBECONFIG: '{{.KUBECONFIG}}' cmds: @@ -448,7 +449,9 @@ tasks: - | set -euo pipefail # The demo surfaces are GitOps-managed on the ephemeral GSK cluster, so - # destroying the gridscale substrate reclaims everything. No standing env. + # destroying the gridscale substrate reclaims everything. The demo is + # ephemeral-by-default; a standing go-live substrate is a separate + # recorded and time-boxed carve-out (D-042 / E1g-S07), not this cycle. task e1g:down echo "E8b demo torn down (gridscale substrate destroyed via e1g:down)." diff --git a/docs/ROADMAP.md b/docs/ROADMAP.md index 84cc79b..78d5ce7 100644 --- a/docs/ROADMAP.md +++ b/docs/ROADMAP.md @@ -409,10 +409,19 @@ reviewer path. **Runbook:** [gridscale-live-demo.md](runbooks/gridscale-live-demo.md) DECIDED-B (operator-approved): E8b ships as a reproducible **on-demand** bring-up -(`task e8b:up` / `e8b:down`), proven ephemerally — **not a standing environment** -(the ruthless-teardown cost rule holds; the "interview window" is -operator-triggered). Offline-authored below; live bring-up/serve pending the live -cycle. +(`task e8b:up` / `e8b:down`), proven ephemerally — the ruthless-teardown cost rule +holds by default and the "interview window" is operator-triggered. + +**Go-live carve-out (D-042, supersedes the dev-phase absolute):** the project has +entered go-live, where a standing live substrate is intentionally permitted — but +**only when it is recorded and time-boxed**: what is up and since-when are captured +in `evidence/live/e1g-gsk-2026-07-18.md`; the teardown-by date and owner (with the +~1–2 week go-live window) will be captured in decision **D-042** (operator-placed) +and surfaced by **E1g-S07** (`task e1g:status`, soft WARN). +This does **not** make "always-on" acceptable and does **not** weaken the +create→verify→destroy discipline for per-story live proofs — the ephemeral +`e8b:up`/`e8b:down` cycle stays ephemeral-by-default. Offline-authored below; live +bring-up/serve pending the live cycle. | ID | Story | Status | | --- | --- | --- | diff --git a/docs/development/DEVELOPMENT.md b/docs/development/DEVELOPMENT.md index 3bb2370..fd4412b 100644 --- a/docs/development/DEVELOPMENT.md +++ b/docs/development/DEVELOPMENT.md @@ -39,6 +39,11 @@ task lint # markdown + shellcheck Implementation lanes add `task test` (L0–L2), promtool, and cluster integration checks per story — see [testing.md](testing.md) for the full matrix and the CI workflows that run it. +The go-live standing-substrate policy (a standing live env is permitted when **recorded and +time-boxed** — D-042, `tests/meta/e1g-standing-policy.yaml`, surfaced by E1g-S07's soft +`task e1g:status` WARN) is **cost-governance, not a blocker**: the doc-truth guard only reddens +on an unreconciled unqualified "no standing env" absolute, never on the substrate being up. + ## Testing Mandatory TDD — see [testing.md](testing.md) and ADR-0701. Summary: diff --git a/docs/runbooks/gridscale-day0.md b/docs/runbooks/gridscale-day0.md index ad8b474..d148e33 100644 --- a/docs/runbooks/gridscale-day0.md +++ b/docs/runbooks/gridscale-day0.md @@ -158,6 +158,17 @@ object-storage anchor is destroyed last because it holds the remote state; if you want to keep the anchor between tests (cheapest option) destroy only the workload stacks and leave `object-storage` up. +**Go-live standing carve-out (D-042).** The dev-phase "tear it all down every +time" rule was an overspend guard for the build phase. In **go-live** a standing +live substrate is intentionally permitted — but **only when it is +recorded and time-boxed**: what is up and since-when are captured in +`evidence/live/e1g-gsk-2026-07-18.md`; the teardown-by date and owner (with the +~1–2 week go-live window) will be captured in decision **D-042** (operator-placed) +and surfaced by **E1g-S07** (`task e1g:status`, a soft WARN once the default ~14-day +window is exceeded). This is cost-governance, not a blocker, and it does +**not** relax the ruthless per-test teardown above — that stays the default; the +carve-out covers only the one sanctioned, recorded standing substrate. + ## Phase-1 ↔ phase-2 deltas - **Substrate**: phase-1 kind cluster → phase-2 GSK managed cluster. Same diff --git a/docs/runbooks/gridscale-live-demo.md b/docs/runbooks/gridscale-live-demo.md index 9419f42..a479b75 100644 --- a/docs/runbooks/gridscale-live-demo.md +++ b/docs/runbooks/gridscale-live-demo.md @@ -6,18 +6,38 @@ demo (e.g. an interview walkthrough), show the read-only demo surfaces, then runbook ([gridscale-day0.md](gridscale-day0.md)) — read that first; this runbook only adds the demo layer and the demo choreography. -## Framing — on-demand, NOT standing (DECIDED-B) +## Framing — on-demand by default (DECIDED-B, reconciled by D-042) The E8b story was originally phrased as "keep the gridscale stack running through the interview window". That contradicts the project's ruthless-teardown cost rule (the gridscale GSK cluster + LBaaS cost real money every hour they are up). **Reconciled, operator-approved definition:** E8b is a **reproducible on-demand -bring-up**, proven ephemerally — **not a standing environment**. The "interview -window" is an **operator-triggered** `task e8b:up` … demo … `task e8b:down` -cycle, run just before the demo and torn down immediately after. It is **not -always-on** infra. If you need it live for a 30-minute interview, bring it up 10 -minutes before and tear it down the moment you are done. +bring-up**, proven ephemerally. The "interview window" is an +**operator-triggered** `task e8b:up` … demo … `task e8b:down` cycle, run just +before the demo and torn down immediately after — it is **not always-on** infra. +If you need it live for a 30-minute interview, bring it up 10 minutes before and +tear it down the moment you are done. + +### Go-live standing carve-out (D-042, supersedes the dev-phase absolute) + +The dev-phase DECIDED-B rule treated a standing environment as forbidden — an +overspend guard for the build phase, now superseded for go-live (see below). +The project has now entered **go-live**, +where a standing live substrate is intentional (planned ~1–2 weeks, decision +**D-042**). Under go-live a standing substrate is permitted, but **only when it +is recorded and time-boxed**: + +- **recorded** — what is up and since-when are captured in + `evidence/live/e1g-gsk-2026-07-18.md`; the teardown-by date and owner will be + captured in decision **D-042** (operator-placed) and surfaced by **E1g-S07**; and +- **time-boxed** — an explicit teardown deadline, surfaced by **E1g-S07** + (`task e1g:status`, a soft WARN once the default ~14-day window is exceeded). + +This carve-out is cost-governance, not a licence for "always-on": it does **not** +declare standing infra always fine, and it does **not** weaken the ephemeral +`e8b:up`/`e8b:down` demo cycle or the per-story create→verify→destroy live-proof +discipline, both of which stay ephemeral-by-default. ## TL;DR @@ -133,9 +153,11 @@ pool keeps billing). The demo runs on the same GSK node pool + LBaaS + public IPs as E1g (see the cost table in [gridscale-day0.md](gridscale-day0.md) — the GSK node pool dominates). -Because E8b is **on-demand, not standing**, the cost is bounded to the minutes the -demo actually runs. **Always `task e8b:down` immediately after the demo.** Never -leave the demo up "just in case" — bring it up again on demand next time. +Because the E8b demo is **on-demand and ephemeral-by-default**, its cost is bounded +to the minutes the demo actually runs. **Always `task e8b:down` immediately after +the demo.** Never leave the demo up "just in case" — bring it up again on demand +next time. (A deliberately *standing* go-live substrate is a separate, recorded and +time-boxed carve-out — see the go-live carve-out above and D-042.) ## Read-only posture (why the demo is safe to expose) diff --git a/openspec/changes/e1g-gridscale-day0/tasks.md b/openspec/changes/e1g-gridscale-day0/tasks.md index cf24d38..8d98bac 100644 --- a/openspec/changes/e1g-gridscale-day0/tasks.md +++ b/openspec/changes/e1g-gridscale-day0/tasks.md @@ -19,7 +19,7 @@ later serialized step (`task e1g:up`). See docs/runbooks/gridscale-day0.md. - [ ] E1g-S05g: Cloudflare DNS records → LBaaS IPs + LE staging→prod + live public serve verify — depends S05c/S05e/S05f - [ ] E1g-S05h: **Security spike** — investigate + mitigate GSK worker-node public-IP exposure (node came up EXTERNAL-IP `185.241.34.168`; `gridscale_k8s ~>2.2` exposes no disable-public-IP arg — confirm provider/API limit + safe mitigation) - Build order: **S05a → S05d → S05b → S05c → (S05e ∥ S05f) → S05g.** S05h is an independent security spike (no dep). - - [ ] E1g-S06: Reconcile the retired "no standing env" DECIDED-B prose → go-live recorded+time-boxed carve-out (INBOX/ROADMAP/runbooks/Taskfile e8b strings) + offline doc-truth guard `tests/meta/e1g-standing-policy.yaml`; forward-refs S07 + D-04x. Doc-truth/cost-governance, NOT a blocker. Closes audit WIP-D1 (doc half). Body in `agent-context/BACKLOG.md` § "Go-live cost governance". + - [x] E1g-S06: Reconcile the retired "no standing env" DECIDED-B prose → go-live recorded+time-boxed carve-out (ROADMAP/both gridscale runbooks/Taskfile e8b desc+comment strings; INBOX left to the edge session) + offline doc-truth guard `tests/meta/e1g-standing-policy.yaml` wired into `test:meta:ci`; forward-refs S07 + D-042 + `evidence/live/e1g-gsk-2026-07-18.md`. Doc-truth/cost-governance, NOT a blocker. Closes audit WIP-D1 (doc half). Body in `agent-context/BACKLOG.md` § "Go-live cost governance". - [ ] E1g-S07: Cost-visibility standing marker + `task e1g:status` + SOFT time-box WARN guardrail (~14d default, always exit 0, absent=no-op) wired into `test:meta:ci`; `task verify` EXIT 0 even with a stale marker (softness proof). Closes audit WIP-D1 (TTL half). Depends S06. - [x] Gate: `task test:smoke:e1g` (offline) — EXIT 0 - [ ] Live-proof: `task e1g:up` then re-sync app-of-apps (later serialized step, costs money) diff --git a/tests/meta/e1g-standing-policy.yaml b/tests/meta/e1g-standing-policy.yaml new file mode 100755 index 0000000..9b84bac --- /dev/null +++ b/tests/meta/e1g-standing-policy.yaml @@ -0,0 +1,65 @@ +#!/usr/bin/env bash +# E1g-S06 — go-live standing-substrate doc-truth guard (offline; WIP-D1). +# +# The dev-phase DECIDED-B rule ("on-demand, NOT standing / No standing env") was +# an overspend guard for the BUILD phase. The project is now in go-live (D-042), +# where a standing live substrate is intentional — permitted ONLY when it is +# RECORDED (what is up, since-when, teardown-by, owner) and TIME-BOXED (explicit +# teardown deadline), surfaced by E1g-S07's `task e1g:status` soft WARN. +# +# This guard keeps that reconciliation honest offline: +# (a) the go-live "recorded and time-boxed" carve-out phrasing is PRESENT in +# each governed durable home; and +# (b) no governed doc silently reintroduces an UNQUALIFIED standing-env +# absolute (an absolute with no go-live/recorded/time-boxed/carve-out +# qualifier nearby). +# It scans ONLY durable docs — never agent-context/ (coordination, not truth). +# Asserts on prose STRINGS, never line numbers (matches doc10-roadmap-status). +# Known limitation: the ABSOLUTE pattern matches only the specific removed +# phrasings ("no standing env" / "not a standing environment" / "NOT standing"). +# A reworded regression (e.g. "no standing infrastructure") is out of scope by +# design — the guard closes the exact reconciled contradiction, not every synonym. +set -euo pipefail + +ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)" +ROADMAP="$ROOT/docs/ROADMAP.md" +LIVEDEMO="$ROOT/docs/runbooks/gridscale-live-demo.md" +DAY0="$ROOT/docs/runbooks/gridscale-day0.md" +TASKFILE="$ROOT/Taskfile.yml" + +fail() { echo "FAIL: $*" >&2; exit 1; } + +# Stable phrase the reconcile introduces everywhere the carve-out is stated. +CARVEOUT='recorded and time-boxed' +# The dev-phase absolutes that, UNQUALIFIED, contradict the go-live carve-out. +ABSOLUTE='no standing env|not a standing environment|NOT standing' +# Qualifiers that make a standing statement a legitimate carve-out, not a false +# absolute (go-live permits a recorded, time-boxed standing substrate). +QUALIFIER='go-live|recorded|time-boxed|carve-out|D-042' + +# --- (a) carve-out phrasing PRESENT in each governed durable home ------------ +for DOC in "$ROADMAP" "$LIVEDEMO" "$DAY0"; do + [ -f "$DOC" ] || fail "governed doc missing: $DOC" + grep -qiF "$CARVEOUT" "$DOC" \ + || fail "$DOC must state the go-live '$CARVEOUT' standing carve-out" +done + +# The carve-out must forward-reference the enforcement story + the decision/record. +grep -qiF 'E1g-S07' "$LIVEDEMO" \ + || fail "gridscale-live-demo.md carve-out must forward-reference E1g-S07 (soft WARN time-box)" +grep -qiF 'D-042' "$LIVEDEMO" \ + || fail "gridscale-live-demo.md carve-out must cite the D-042 go-live decision" +grep -qiF 'e1g-gsk-2026-07-18' "$LIVEDEMO" \ + || fail "gridscale-live-demo.md carve-out must cite the evidence/live record" + +# --- (b) no UNQUALIFIED standing-env absolute in any governed doc ------------ +# A line carrying an absolute is only OK if it also carries a go-live qualifier. +for DOC in "$ROADMAP" "$LIVEDEMO" "$DAY0" "$TASKFILE"; do + [ -f "$DOC" ] || fail "governed doc missing: $DOC" + if grep -iE "$ABSOLUTE" "$DOC" | grep -qivE "$QUALIFIER"; then + grep -inE "$ABSOLUTE" "$DOC" | grep -ivE "$QUALIFIER" >&2 || true + fail "$DOC carries an UNQUALIFIED standing-env absolute (add a $QUALIFIER carve-out qualifier, or reconcile the line)" + fi +done + +echo "OK: E1g-S06 go-live standing-substrate doc-truth (recorded + time-boxed carve-out)"