Skip to content

Commit c59958e

Browse files
authored
fix(release): decouple promotion readiness from project goals (#2727)
* fix(release): record promotion readiness outside goals * test(release): cover runtime readiness ledger * docs(release): define runtime readiness evidence * fix(release): make runtime readiness authoritative
1 parent 354ebfc commit c59958e

18 files changed

Lines changed: 480 additions & 195 deletions

docs/guides/getting-started.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -963,6 +963,11 @@ loopx promotion-gate --format json
963963
loopx upgrade-plan --format json
964964
```
965965

966+
When the dashboard source is present, the readiness smoke requires its npm
967+
dependencies so a dependency skip cannot be recorded as a pass. Use
968+
`--dashboard-mode=skip` only when intentionally qualifying a release boundary
969+
that omits dashboard validation; the runtime evidence records that skip.
970+
966971
## Documentation Map
967972

968973
Start here:

docs/quota-allocation.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -799,8 +799,9 @@ When the status payload has missing, stale, or unknown canary promotion
799799
readiness evidence, `quota should-run` also includes
800800
`promotion_readiness_warning`. This warning is additive: it does not change
801801
`should_run`, but it lets heartbeat workers and dashboards report release
802-
readiness blockers from the shared run-history projection without parsing
803-
`doctor`, dashboard copy, or chat reports. Before promoting the local release
802+
readiness blockers from the shared runtime release ledger without parsing
803+
`doctor`, dashboard copy, or chat reports. New evidence is stored there rather
804+
than in a project Goal. Before promoting the local release
804805
snapshot, run `python3 examples/canary/canary-promotion-readiness-smoke.py` and
805806
confirm fresh evidence in status or doctor output. The
806807
`--no-write-evidence` form remains the non-mutating validation path and therefore

docs/status-data-contract.md

Lines changed: 33 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -376,8 +376,10 @@ goals must stay out of the eligible lane even when they have a high
376376
},
377377
"promotion_readiness_summary": {
378378
"available": true,
379-
"source": "run_history",
380-
"goal_id": "loopx-meta",
379+
"source": "runtime_release_ledger",
380+
"evidence_scope": "runtime_release",
381+
"goal_id": null,
382+
"dashboard_readiness": "passed",
381383
"generated_at": "2026-06-01T00:08:00+00:00",
382384
"classification": "canary_promotion_readiness_smoke_group",
383385
"delivery_batch_scale": "multi_surface",
@@ -391,7 +393,7 @@ goals must stay out of the eligible lane even when they have a high
391393
"age_seconds": 120,
392394
"age_hours": 0.03,
393395
"sample_run_count": 1,
394-
"proxy_note": "canary promotion-readiness projection from append-only run history; exact evidence stays in run artifacts"
396+
"proxy_note": "canary promotion-readiness projection from the runtime release ledger with legacy goal-history fallback; exact evidence stays in append-only artifacts"
395397
},
396398
"promotion_gate": {
397399
"ok": true,
@@ -527,6 +529,18 @@ the stable fields.
527529
consume one state contract instead of re-deriving release-readiness state
528530
separately.
529531

532+
After the release checks pass, the canary uses the explicit write boundary:
533+
534+
```bash
535+
loopx promotion-readiness record \
536+
--dashboard-readiness passed \
537+
--execute
538+
```
539+
540+
Without `--execute`, this command only previews the runtime-level append. The
541+
canary supplies `skipped` only for an explicit dashboard omission; dependency
542+
failures on a present dashboard source must not be recorded as `passed`.
543+
530544
Fresh shape:
531545

532546
```json
@@ -542,7 +556,10 @@ Fresh shape:
542556
"recommended_action": "promotion readiness is fresh",
543557
"readiness": {
544558
"available": true,
545-
"goal_id": "loopx-meta",
559+
"source": "runtime_release_ledger",
560+
"evidence_scope": "runtime_release",
561+
"goal_id": null,
562+
"dashboard_readiness": "passed",
546563
"classification": "canary_promotion_readiness_smoke_group",
547564
"freshness_status": "fresh",
548565
"requires_readiness_run": false,
@@ -2170,10 +2187,14 @@ leaderboard claim.
21702187
## Promotion Readiness Summary
21712188

21722189
`promotion_readiness_summary` is an optional release-control projection over the
2173-
same sampled run history. It finds the latest
2190+
runtime release ledger, with legacy Goal run-history events retained as a
2191+
read-compatible fallback only when the runtime ledger has no valid readiness
2192+
event. It finds the latest
21742193
`canary_promotion_readiness_smoke_group` event and reports whether that evidence
21752194
is fresh enough to trust before promoting a live checkout into the default local
2176-
release snapshot.
2195+
release snapshot. New evidence is runtime-scoped because release readiness is
2196+
shared by every Goal using that local installation; it does not require or
2197+
mutate a project Goal.
21772198

21782199
The summary reports:
21792200

@@ -2185,8 +2206,11 @@ The summary reports:
21852206
parseable.
21862207
- `json_exists` / `markdown_exists`: whether the latest evidence artifacts still
21872208
exist.
2209+
- `dashboard_readiness`: `passed` or `skipped` for runtime-level evidence, so a
2210+
deliberate omission is never indistinguishable from a successful check.
21882211

2189-
This projection does not promote anything and does not replace the run artifact.
2212+
This projection does not promote anything and does not replace the append-only
2213+
release artifact under the LoopX runtime root.
21902214
`scripts/install-local.sh` consumes the same readiness fact only to print a
21912215
non-blocking warning; operators should still run `loopx doctor` or the
21922216
canary-promotion readiness smoke for exact local release evidence.
@@ -2234,8 +2258,8 @@ not rolling the repository or project back to the old chat context.
22342258
evidence is missing, stale, or unknown. The warning is a release-readiness guard
22352259
surface, not a scheduling decision: it does not flip `should_run`, but it lets a
22362260
heartbeat worker report that the release snapshot should not be promoted until
2237-
fresh canary promotion-readiness evidence is written back to the shared
2238-
run-history projection. This keeps release readiness in queryable control-plane
2261+
fresh canary promotion-readiness evidence is written to the shared runtime
2262+
release ledger. This keeps release readiness in queryable control-plane
22392263
state instead of relying on dashboard prose, `doctor` output, or a chat thread.
22402264
The warning message names the writeback command,
22412265
`python3 examples/canary/canary-promotion-readiness-smoke.py`. A run with

examples/canary/canary-promotion-no-write-contract-smoke.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@
55

66
from pathlib import Path
77

8-
98
REPO_ROOT = Path(__file__).resolve().parents[2]
109
CANARY_SMOKE = REPO_ROOT / "examples" / "canary" / "canary-promotion-readiness-smoke.py"
1110
DASHBOARD_DEMO_SMOKE = REPO_ROOT / "examples" / "dashboard-demo-readiness-smoke.py"
@@ -30,12 +29,13 @@ def main() -> int:
3029
dashboard_source = DASHBOARD_DEMO_SMOKE.read_text(encoding="utf-8")
3130

3231
assert_contains(canary_source, "--no-write-evidence", "canary no-write flag")
33-
assert_contains(canary_source, "--goal-id", "canary refresh-state goal id flag")
34-
assert_contains(canary_source, "DEFAULT_READINESS_GOAL_ID", "canary default writeback goal")
35-
assert_contains(canary_source, "--agent-id", "canary refresh-state agent id flag")
36-
assert_contains(canary_source, "DEFAULT_READINESS_AGENT_ID", "canary default writeback agent")
37-
assert_contains(canary_source, "DEFAULT_READINESS_AGENT_LANE", "canary default writeback lane")
32+
assert_contains(canary_source, '"promotion-readiness"', "runtime release evidence command")
33+
assert_contains(canary_source, '"record"', "promotion-readiness record action")
34+
assert_contains(canary_source, '"--execute"', "explicit evidence write flag")
35+
if "refresh-state" in canary_source or "--goal-id" in canary_source:
36+
raise AssertionError("promotion readiness evidence must not require a project Goal")
3837
assert_contains(canary_source, "dashboard-demo-readiness-smoke.py", "canary dashboard demo-readiness command")
38+
assert_contains(canary_source, "--require-dependencies", "dashboard dependency fail-closed flag")
3939
assert_contains(canary_source, 'commands.append(("dashboard demo readiness", dashboard_command))', "canary grouped path append")
4040
assert_before(
4141
canary_source,

examples/canary/canary-promotion-readiness-boundary-smoke.py

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
REPO_ROOT = Path(__file__).resolve().parents[2]
1212
CANARY_PATH = REPO_ROOT / "examples" / "canary" / "canary-promotion-readiness-smoke.py"
13+
DASHBOARD_PATH = REPO_ROOT / "examples" / "dashboard-demo-readiness-smoke.py"
1314

1415

1516
def load_canary_module():
@@ -23,8 +24,22 @@ def load_canary_module():
2324
return module
2425

2526

27+
def load_dashboard_module():
28+
spec = importlib.util.spec_from_file_location(
29+
"dashboard_demo_readiness_smoke",
30+
DASHBOARD_PATH,
31+
)
32+
assert spec is not None and spec.loader is not None
33+
module = importlib.util.module_from_spec(spec)
34+
spec.loader.exec_module(module)
35+
return module
36+
37+
2638
def main() -> int:
2739
canary = load_canary_module()
40+
dashboard = load_dashboard_module()
41+
assert dashboard.missing_dependency_exit_code(require_dependencies=False) == 0
42+
assert dashboard.missing_dependency_exit_code(require_dependencies=True) == 1
2843
with tempfile.TemporaryDirectory() as tmp:
2944
root = Path(tmp)
3045
release_dashboard = root / "release" / "apps" / "presentation" / "dashboard"
@@ -58,13 +73,15 @@ def main() -> int:
5873

5974
source_plan = canary.dashboard_readiness_plan(dashboard_dir=source_dashboard)
6075
assert source_plan["status"] == "run", source_plan
61-
assert source_plan["command"][-1] == "--skip-browser", source_plan
76+
assert "--require-dependencies" in source_plan["command"], source_plan
77+
assert "--skip-browser" in source_plan["command"], source_plan
6278

6379
source_browser_plan = canary.dashboard_readiness_plan(
6480
dashboard_dir=source_dashboard,
6581
include_browser=True,
6682
)
6783
assert source_browser_plan["status"] == "run", source_browser_plan
84+
assert "--require-dependencies" in source_browser_plan["command"], source_browser_plan
6885
assert "--skip-browser" not in source_browser_plan["command"], source_browser_plan
6986

7087
print("canary-promotion-readiness-boundary-smoke ok")

examples/canary/canary-promotion-readiness-smoke.py

Lines changed: 13 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -5,9 +5,9 @@
55
local release snapshot. It validates the public boundary, status projections,
66
installer wrappers, and dashboard demo-readiness path without mutating the
77
installed release. By default, a successful run appends one public-safe
8-
promotion-readiness evidence event to the LoopX run history so status,
9-
doctor, and quota guards can clear stale or missing readiness warnings from the
10-
append-only ledger.
8+
promotion-readiness evidence event to the runtime release ledger so status,
9+
doctor, and quota guards can clear stale or missing readiness warnings without
10+
requiring a project Goal.
1111
"""
1212

1313
from __future__ import annotations
@@ -18,27 +18,13 @@
1818
import sys
1919
from pathlib import Path
2020

21-
2221
REPO_ROOT = Path(__file__).resolve().parents[2]
2322
DASHBOARD_DIR = REPO_ROOT / "apps" / "presentation" / "dashboard"
2423

2524
COMMON_NODE_PATHS = [
2625
"/opt/homebrew/bin",
2726
"/usr/local/bin",
2827
]
29-
DEFAULT_READINESS_GOAL_ID = "loopx-meta"
30-
DEFAULT_READINESS_AGENT_ID = "codex-product-capability"
31-
DEFAULT_READINESS_AGENT_LANE = "product_capability_catalog_canary"
32-
READINESS_CLASSIFICATION = "canary_promotion_readiness_smoke_group"
33-
READINESS_DELIVERY_OUTCOME = "surface_only"
34-
READINESS_RECOMMENDED_ACTION = (
35-
"Canary promotion-readiness smoke passed; promotion may proceed after doctor/status reports fresh evidence."
36-
)
37-
READINESS_RECOMMENDED_ACTION_DASHBOARD_SKIPPED = (
38-
"Canary promotion-readiness smoke passed for the installed release boundary; "
39-
"dashboard readiness was skipped because apps/presentation/dashboard is not shipped in the release snapshot."
40-
)
41-
4228
BASE_COMMANDS = [
4329
(
4430
"public boundary contract",
@@ -71,30 +57,6 @@ def parse_args() -> argparse.Namespace:
7157
action="store_true",
7258
help="Run checks only; do not append the promotion-readiness evidence event.",
7359
)
74-
parser.add_argument(
75-
"--goal-id",
76-
default=os.environ.get("LOOPX_GOAL_ID") or DEFAULT_READINESS_GOAL_ID,
77-
help=(
78-
"Registered goal id used for the readiness evidence writeback. "
79-
"Defaults to LOOPX_GOAL_ID or loopx-meta."
80-
),
81-
)
82-
parser.add_argument(
83-
"--agent-id",
84-
default=os.environ.get("LOOPX_AGENT_ID") or DEFAULT_READINESS_AGENT_ID,
85-
help=(
86-
"Registered agent id used for the readiness evidence writeback. "
87-
"Defaults to LOOPX_AGENT_ID or codex-product-capability."
88-
),
89-
)
90-
parser.add_argument(
91-
"--agent-lane",
92-
default=os.environ.get("LOOPX_AGENT_LANE") or DEFAULT_READINESS_AGENT_LANE,
93-
help=(
94-
"Public-safe agent lane label used for the readiness evidence writeback. "
95-
"Defaults to LOOPX_AGENT_LANE or product_capability_catalog_canary."
96-
),
97-
)
9860
return parser.parse_args()
9961

10062

@@ -117,44 +79,17 @@ def write_readiness_evidence(
11779
env: dict[str, str],
11880
*,
11981
dashboard_skipped: bool,
120-
goal_id: str | None = None,
121-
agent_id: str | None = None,
122-
agent_lane: str | None = None,
12382
) -> None:
124-
recommended_action = (
125-
READINESS_RECOMMENDED_ACTION_DASHBOARD_SKIPPED
126-
if dashboard_skipped
127-
else READINESS_RECOMMENDED_ACTION
128-
)
129-
resolved_agent_id = (
130-
agent_id or os.environ.get("LOOPX_AGENT_ID") or DEFAULT_READINESS_AGENT_ID
131-
).strip()
132-
resolved_goal_id = (
133-
goal_id or os.environ.get("LOOPX_GOAL_ID") or DEFAULT_READINESS_GOAL_ID
134-
).strip()
135-
resolved_agent_lane = (
136-
agent_lane or os.environ.get("LOOPX_AGENT_LANE") or DEFAULT_READINESS_AGENT_LANE
137-
).strip()
13883
command = [
13984
sys.executable,
14085
"-m",
14186
"loopx.cli",
142-
"refresh-state",
143-
"--goal-id",
144-
resolved_goal_id,
145-
"--classification",
146-
READINESS_CLASSIFICATION,
147-
"--recommended-action",
148-
recommended_action,
149-
"--delivery-batch-scale",
150-
"multi_surface",
151-
"--delivery-outcome",
152-
READINESS_DELIVERY_OUTCOME,
87+
"promotion-readiness",
88+
"record",
89+
"--dashboard-readiness",
90+
"skipped" if dashboard_skipped else "passed",
91+
"--execute",
15392
]
154-
if resolved_agent_id:
155-
command.extend(["--agent-id", resolved_agent_id])
156-
if resolved_agent_lane:
157-
command.extend(["--agent-lane", resolved_agent_lane])
15893
run_command(
15994
"promotion readiness evidence writeback",
16095
command,
@@ -176,7 +111,11 @@ def dashboard_readiness_plan(
176111
"command": None,
177112
}
178113
if has_dashboard:
179-
command = [sys.executable, "examples/dashboard-demo-readiness-smoke.py"]
114+
command = [
115+
sys.executable,
116+
"examples/dashboard-demo-readiness-smoke.py",
117+
"--require-dependencies",
118+
]
180119
if not include_browser:
181120
command.append("--skip-browser")
182121
return {
@@ -229,9 +168,6 @@ def main() -> int:
229168
write_readiness_evidence(
230169
env,
231170
dashboard_skipped=dashboard_plan["status"] == "skip",
232-
goal_id=args.goal_id,
233-
agent_id=args.agent_id,
234-
agent_lane=args.agent_lane,
235171
)
236172
evidence_suffix = " with evidence writeback"
237173

0 commit comments

Comments
 (0)