zdd-6: Astra's final review findings, rounds 1-3
Round 1: ACCEPT WITH FIXES (9 findings); round 2 and 3 scoped re-reviews of fix waves 1 and 2. Open: the unsigned host retry (flag-on blocker in the roll notes) and the skipped-record completeness gap (round 3, flag-on only).
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
3 files changed,
+87
−0
docs/superpowers/plans/CODEX-FINDINGS-ZDD-IMPL-6-R2.md+27 −0docs/superpowers/plans/CODEX-FINDINGS-ZDD-IMPL-6-R3.md+17 −0docs/superpowers/plans/CODEX-FINDINGS-ZDD-IMPL-6.md+43 −0
CI passed — run details
CI log
=== gate ===
cd launchctx && go vet ./... && go test ./... -count=1
ok amadan.net/carlos/platform.git/launchctx 0.012s
? amadan.net/carlos/platform.git/launchctx/controlname [no test files]
? amadan.net/carlos/platform.git/launchctx/wireid [no test files]
go vet ./...
gofmt -l .: clean
go test ./... -count=1 -timeout 30m
ok github.com/carlosframework/platform 329.491s
? github.com/carlosframework/platform/cmd/changelog [no test files]
ok github.com/carlosframework/platform/cmd/console 2.951s
ok github.com/carlosframework/platform/internal/activator 134.302s
ok github.com/carlosframework/platform/internal/agentsock 0.016s
ok github.com/carlosframework/platform/internal/alerting 0.004s
ok github.com/carlosframework/platform/internal/analytics 0.018s
ok github.com/carlosframework/platform/internal/appconfig 2.208s
ok github.com/carlosframework/platform/internal/apperr 0.073s
ok github.com/carlosframework/platform/internal/applog 0.124s
ok github.com/carlosframework/platform/internal/awsinv 0.005s
ok github.com/carlosframework/platform/internal/boxchoice 0.005s
ok github.com/carlosframework/platform/internal/boxid 0.544s
? github.com/carlosframework/platform/internal/boxid/boxidtest [no test files]
? github.com/carlosframework/platform/internal/boxid/internal/signtest [no test files]
ok github.com/carlosframework/platform/internal/boxvolumes 0.008s
ok github.com/carlosframework/platform/internal/caps 0.012s
ok github.com/carlosframework/platform/internal/certwarm 0.002s
ok github.com/carlosframework/platform/internal/cgroup 0.186s
ok github.com/carlosframework/platform/internal/changelog 0.244s
ok github.com/carlosframework/platform/internal/clamd 0.055s
? github.com/carlosframework/platform/internal/cliflag [no test files]
ok github.com/carlosframework/platform/internal/console 323.655s
? github.com/carlosframework/platform/internal/controlname [no test files]
? github.com/carlosframework/platform/internal/controlsock [no test files]
ok github.com/carlosframework/platform/internal/dbdoor 6.366s
ok github.com/carlosframework/platform/internal/dbdoor/dbdoortest 0.010s
ok github.com/carlosframework/platform/internal/dbverify 0.007s
ok github.com/carlosframework/platform/internal/dnsrec 0.007s
ok github.com/carlosframework/platform/internal/docsite 0.007s
ok github.com/carlosframework/platform/internal/domains 1.987s
ok github.com/carlosframework/platform/internal/doorkey 0.007s
ok github.com/carlosframework/platform/internal/economics 0.007s
ok github.com/carlosframework/platform/internal/edge 26.161s
? github.com/carlosframework/platform/internal/edgeproof [no test files]
ok github.com/carlosframework/platform/internal/edgesfile 0.007s
ok github.com/carlosframework/platform/internal/fleetapi 0.130s
ok github.com/carlosframework/platform/internal/gate 1.658s
ok github.com/carlosframework/platform/internal/genselect 0.025s
ok github.com/carlosframework/platform/internal/geo 0.010s
ok github.com/carlosframework/platform/internal/geoip 0.039s
ok github.com/carlosframework/platform/internal/host 45.400s
ok github.com/carlosframework/platform/internal/hostactors 0.324s
ok github.com/carlosframework/platform/internal/hostbox 1.362s
ok github.com/carlosframework/platform/internal/hostclaim 0.228s
ok github.com/carlosframework/platform/internal/hostevidence 0.008s
ok github.com/carlosframework/platform/internal/hostexec 0.310s
ok github.com/carlosframework/platform/internal/hostfence 0.037s
ok github.com/carlosframework/platform/internal/hostfiles 4.084s
ok github.com/carlosframework/platform/internal/hostfloor 0.018s
ok github.com/carlosframework/platform/internal/hostguard 0.142s
ok github.com/carlosframework/platform/internal/hostops 1.266s
ok github.com/carlosframework/platform/internal/hostquarantine 0.002s
ok github.com/carlosframework/platform/internal/hostresolve 0.153s
ok github.com/carlosframework/platform/internal/hostwrite 0.002s
ok github.com/carlosframework/platform/internal/infraview 0.002s
ok github.com/carlosframework/platform/internal/instancectl 1.703s
ok github.com/carlosframework/platform/internal/instanceexec 2.700s
ok github.com/carlosframework/platform/internal/instancefs 0.005s
ok github.com/carlosframework/platform/internal/layout 1.345s
ok github.com/carlosframework/platform/internal/lease 9.445s
ok github.com/carlosframework/platform/internal/lease/leasetest 0.007s
ok github.com/carlosframework/platform/internal/ledger 0.014s
ok github.com/carlosframework/platform/internal/mail 0.159s
ok github.com/carlosframework/platform/internal/maintenance 0.091s
ok github.com/carlosframework/platform/internal/maintenanceexec 5.516s
ok github.com/carlosframework/platform/internal/maintenanceguard 0.064s
ok github.com/carlosframework/platform/internal/metrics 0.005s
ok github.com/carlosframework/platform/internal/moves 20.874s
ok github.com/carlosframework/platform/internal/moves/archive 0.753s
ok github.com/carlosframework/platform/internal/objstore 0.188s
ok github.com/carlosframework/platform/internal/ops 0.161s
ok github.com/carlosframework/platform/internal/park 0.178s
ok github.com/carlosframework/platform/internal/privatefile 0.004s
ok github.com/carlosframework/platform/internal/provision 3.069s
ok github.com/carlosframework/platform/internal/publicrelease 0.006s
ok github.com/carlosframework/platform/internal/registry 3.487s
ok github.com/carlosframework/platform/internal/relay 7.228s
ok github.com/carlosframework/platform/internal/release 18.319s
ok github.com/carlosframework/platform/internal/rollout 1.501s
ok github.com/carlosframework/platform/internal/rolloutwords 0.003s
ok github.com/carlosframework/platform/internal/rootaws 0.007s
ok github.com/carlosframework/platform/internal/s3compat 0.005s
ok github.com/carlosframework/platform/internal/safefile 0.004s
ok github.com/carlosframework/platform/internal/schedule 2.217s
ok github.com/carlosframework/platform/internal/sim/hostlife 2.509s
ok github.com/carlosframework/platform/internal/sim/litestreamfacts 4.086s
ok github.com/carlosframework/platform/internal/status 0.036s
ok github.com/carlosframework/platform/internal/steering 0.005s
ok github.com/carlosframework/platform/internal/stopevidence 0.113s
ok github.com/carlosframework/platform/internal/svcrole 0.002s
ok github.com/carlosframework/platform/internal/telemetry 0.004s
ok github.com/carlosframework/platform/internal/termfence 0.004s
ok github.com/carlosframework/platform/internal/tunnelclient 0.004s
ok github.com/carlosframework/platform/internal/unixdial 0.003s
ok github.com/carlosframework/platform/internal/updatewords 0.002s
ok github.com/carlosframework/platform/internal/usage 0.004s
ok github.com/carlosframework/platform/internal/version 0.002s
ok github.com/carlosframework/platform/internal/vet 1.546s
=== cross ===
--- shard 0 of 5 ---
cross-compiling darwin/arm64
--- shard 1 of 5 ---
cross-compiling darwin/amd64
--- shard 2 of 5 ---
cross-compiling linux/arm64
--- shard 3 of 5 ---
cross-compiling windows/amd64
--- shard 4 of 5 ---
cross-compiling windows/arm64
=== minio-contract ===
minio contract: arm64, minio RELEASE.2025-09-07T16-13-09Z, mc RELEASE.2025-08-13T08-35-41Z
minio: cached, digest verified
mc: cached, digest verified
minio healthy on 127.0.0.1:20999
Added `local` successfully.
Bucket created successfully `local/contract-bucket`.
Bucket created successfully `local/denied-bucket`.
Created policy `contract-only` successfully.
Added user `scopeduser` successfully.
Attached Policies: [contract-only]
To User: scopeduser
buckets and scoped user ready
=== RUN TestDeliveredContractMinio
=== RUN TestDeliveredContractMinio/put
=== RUN TestDeliveredContractMinio/get_round-trips_the_body
=== RUN TestDeliveredContractMinio/list_shows_the_key
=== RUN TestDeliveredContractMinio/head_and_list_agree_on_the_version
=== RUN TestDeliveredContractMinio/the_app_cannot_tag_its_own_object
=== RUN TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/not_by_PutObjectTagging
=== RUN TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/and_not_by_x-amz-tagging_on_upload
=== RUN TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route
=== RUN TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CreateMultipartUpload
=== RUN TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CopyObject_with_a_REPLACE_tagging_directive
=== RUN TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/but_multipart_itself_is_allowed
=== RUN TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_copy_itself_is_allowed
=== RUN TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/a_POST-policy_forged_tag_with_a_matching_version_is_still_not_a_verdict
=== RUN TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_an_ordinary_tagged-with-something-else_upload_is_allowed
=== RUN TestDeliveredContractMinio/delete_removes_it
=== RUN TestDeliveredContractMinio/cross-bucket_denial
--- PASS: TestDeliveredContractMinio (0.02s)
--- PASS: TestDeliveredContractMinio/put (0.00s)
--- PASS: TestDeliveredContractMinio/get_round-trips_the_body (0.00s)
--- PASS: TestDeliveredContractMinio/list_shows_the_key (0.00s)
--- PASS: TestDeliveredContractMinio/head_and_list_agree_on_the_version (0.00s)
--- PASS: TestDeliveredContractMinio/the_app_cannot_tag_its_own_object (0.00s)
--- PASS: TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/not_by_PutObjectTagging (0.00s)
--- PASS: TestDeliveredContractMinio/the_app_cannot_tag_its_own_object/and_not_by_x-amz-tagging_on_upload (0.00s)
--- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route (0.01s)
--- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CreateMultipartUpload (0.00s)
--- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/not_by_CopyObject_with_a_REPLACE_tagging_directive (0.00s)
--- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/but_multipart_itself_is_allowed (0.00s)
--- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_copy_itself_is_allowed (0.00s)
--- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/a_POST-policy_forged_tag_with_a_matching_version_is_still_not_a_verdict (0.00s)
--- PASS: TestDeliveredContractMinio/the_app_cannot_tag_by_any_other_route/and_an_ordinary_tagged-with-something-else_upload_is_allowed (0.00s)
--- PASS: TestDeliveredContractMinio/delete_removes_it (0.00s)
--- PASS: TestDeliveredContractMinio/cross-bucket_denial (0.00s)
PASS
ok github.com/carlosframework/platform/internal/objstore 0.025s
=== RUN TestT4StorageProbeMinio
2026/10/11 03:57:07 moves: T4: STORE: list: operation error S3: ListObjectsV2, https response error StatusCode: 403, RequestID: 18DD5D7F398C96B9, HostID: 7c420053e0fbcd7f41653fdd92ffa2802c903d05303b1aa15adc2e8bb65d183f, api error SignatureDoesNotMatch: The request signature we calculated does not match the signature you provided. Check your key and signing method.
--- PASS: TestT4StorageProbeMinio (0.01s)
PASS
ok github.com/carlosframework/platform/internal/host 0.016s
minio contract: PASS
diff --git a/docs/superpowers/plans/CODEX-FINDINGS-ZDD-IMPL-6-R2.md b/docs/superpowers/plans/CODEX-FINDINGS-ZDD-IMPL-6-R2.md| new file mode 100644 |
| index 000000000..a1e146d27 |
| --- /dev/null |
| +++ b/docs/superpowers/plans/CODEX-FINDINGS-ZDD-IMPL-6-R2.md |
| @@ -0,0 +1,27 @@ |
| +ACCEPT WITH FIXES for a DORMANT merge; this does not authorize enabling `CARLOS_SWAP`. |
| + |
| +## Round-1 findings |
| + |
| +1. **ADDRESSED** — `internal/appconfig/syncexec.go:1253,1397`: policy-only empty renders leave absent files absent. Adding the first variable still writes, delivers and acknowledges config; removing the last variable empties the existing file and delivers the new revision. Instance overlays and held renders bypass the skip. |
| +2. **ADDRESSED** — `deploywatch.go:279,290,305,684`: discovery requests and retry waits share one deadline; legacy fallback receives the remaining duration. |
| +3. **ADDRESSED** — `deploywatch.go:474,490`: later reports reclassify URL-completed hosts, and explicit installation of another build invalidates successful completion. Missing observations preserve state. |
| +4. **NOT ADDRESSED** — `deploywatch.go:107–109`: 5xx/network failures and incomplete joins now behave conservatively, but **401/403 still cause URL polling**. An expired credential or denied list request says nothing about whether targeted instances report rollouts; polling can wake them. The controller’s broad 4xx exception is therefore unacceptable evidence. Smallest fix: fail on authentication/authorization errors and restrict compatibility fallback to explicitly supported absence responses. |
| +5. **PARKED-ACCEPTABLE** — `internal/host/restart.go:852` still accepts unsigned retries; `docs/superpowers/plans/2026-10-07-zero-downtime-deploys-6-roll-notes.md:89` explicitly records the flag-on blocker. Acceptable only for this dormant merge. |
| +6. **ADDRESSED** — `internal/console/apiinstances.go:461,480`, `internal/console/apiretry.go:51`, `internal/console/instancepage.go:245`: list, page and retry share the account-filtered fleet loader. The completeness scope needs the correction below. |
| +7. **ADDRESSED** — `deploywatch.go:413`: truncation no longer blocks legacy fallback when every target is present and unreported; `deploywatch_test.go:717` compares output and errors with the legacy watch. |
| +8. **ADDRESSED** — `internal/console/instancepage.go:44`, `internal/console/swapcopy.go:78,90`, `internal/rolloutwords/words.go:61`: down-state wording is explicit, retry no longer promises fallback, and swapping wording covers hold mode and workers. |
| +9. **ADDRESSED** — `apps_swap.go:35,59`, `internal/console/apiswap.go:30`, `internal/console/templates/app_settings.html:380`: the identified app-specific strings have draft markers, and the rate-limit sentence loses its em dash. Shared existing flag help and the accepted table placeholder remain unchanged. |
| + |
| +The ordinary flag-off case is preserved: with all targets listed, a complete join and no rollout reports, `legacyOnly` selects the original `watchVersion` at `deploywatch.go:305`, preserving its output and diagnostics. |
| + |
| +## New findings |
| + |
| +- **Medium — [internal/console/apiinstances.go:489](internal/console/apiinstances.go:489): unrelated boxes can disable a healthy app’s legacy watch.** Completeness combines every platform snapshot and every fleet belonging to the account. For example, an account’s unused fleet contains corrupt JSON while its target app runs entirely on healthy platform boxes with the flag off. The list returns `rollout_incomplete`; `legacyOnly` refuses fallback, and the deploy times out without checking the app’s URLs. The loader receives neither the app nor its placement records, so it cannot distinguish relevant failures. **Smallest fix:** compute completeness for the targets’ possible placements, including relevant move locations, and retain unknown state only where an unreadable source could contain their reports. Add an unrelated-fleet regression test. |
| + |
| + Another account’s fleet snapshot does **not** cause this: line 480 excludes it before reading. Retry also does not reject a known hold merely because another snapshot made the join incomplete; it ignores the aggregate flag and uses the available host report. |
| + |
| +## Out of scope |
| + |
| +Legacy URL polling retains its existing HTTP timeout and sleep behavior (`deploy.go:1472,1530`); the discovery fix does not make those operations deadline-bound. |
| + |
| +I inspected tip `3625973c` and the supplied test evidence. My targeted test rerun was blocked because the read-only sandbox prevented Go from creating its work directory. No files were changed. |
| \ No newline at end of file |
diff --git a/docs/superpowers/plans/CODEX-FINDINGS-ZDD-IMPL-6-R3.md b/docs/superpowers/plans/CODEX-FINDINGS-ZDD-IMPL-6-R3.md| new file mode 100644 |
| index 000000000..dd3793f32 |
| --- /dev/null |
| +++ b/docs/superpowers/plans/CODEX-FINDINGS-ZDD-IMPL-6-R3.md |
| @@ -0,0 +1,17 @@ |
| +ACCEPT WITH FIXES for a DORMANT merge. |
| + |
| +## Open items |
| + |
| +- **X1 / R2 #4 — ADDRESSED.** A 401 now retries until the deadline without URL polling. I accept the controller’s 403 ruling for this dormant merge: it preserves the deliberate legacy-watch exception. |
| +- **X2 / R2 new Medium — NOT ADDRESSED fully.** The unrelated-fleet case is fixed, but incomplete record reads can incorrectly exclude a relevant fleet. I accept the controller’s 403 ruling; this defect occurs on a successful list response. |
| + |
| +## New findings |
| + |
| +- **High — [internal/console/apiinstances.go:636](internal/console/apiinstances.go:636): skipped records can authorize URL polling of a sleeper.** |
| + Scenario: deploy’s initial target discovery succeeds for a sleeping instance on `fleet/pi`. During watch discovery, that record’s read fails. `provision.ListRecords` silently skips failed reads (`internal/provision/provision.go:2619`), returning success and `truncated:false` for an app below the cap. Consequently, `recordFleets(recs)` omits `pi`. If its snapshot is also unreadable, lines 494–497 ignore that failure and return a complete join. The target is absent, so `legacyOnly` refuses whole-watch fallback, but `rolloutWatch.apply` reaches `modeURL` at `deploywatch.go:509` and polls it anyway. Before X2, that fleet failure kept the join incomplete and prevented polling. |
| + |
| + **Smallest fix:** expose record-read completeness to this endpoint and prevent a complete-join response when records were skipped, or fail the list read. Add a regression covering successful initial discovery followed by an unreadable target record and fleet snapshot, asserting zero origin requests. |
| + |
| +Tenant isolation remains intact in the changed lookup: record identities are checked against their account/app paths, and fleet ownership is checked before snapshot reads. |
| + |
| +Reviewed tip `971470f3`. The targeted test rerun was blocked by the read-only sandbox before compilation; no files changed. |
| \ No newline at end of file |
diff --git a/docs/superpowers/plans/CODEX-FINDINGS-ZDD-IMPL-6.md b/docs/superpowers/plans/CODEX-FINDINGS-ZDD-IMPL-6.md| new file mode 100644 |
| index 000000000..2ea5de905 |
| --- /dev/null |
| +++ b/docs/superpowers/plans/CODEX-FINDINGS-ZDD-IMPL-6.md |
| @@ -0,0 +1,43 @@ |
| +ACCEPT WITH FIXES for a DORMANT merge; this is not approval to enable `CARLOS_SWAP`. |
| + |
| +1. **Medium — [internal/appconfig/swap.go:106](internal/appconfig/swap.go:106): setting policy can restart instances with the flag off.** On an app without `config.json`, setting `hold` or concurrency above one creates a bundle and touches its generation. `SyncExec` then creates the config file and invokes the restart callback. A member configuring future swap behavior can therefore interrupt today’s running instances. Q3 remains an unresolved behavioral decision, not a harmless deferral. **Smallest fix:** preserve absent-config delivery semantics for a bundle containing only swap policy; test that the first policy change causes no restart. |
| + |
| +2. **Medium — [deploywatch.go:223](deploywatch.go:223): initial discovery does not respect the watch deadline.** Three failed lists incur two five-second sleeps plus request time, then `watchVersion` receives the original timeout again. Even flag-off `deploy --timeout 1s` can wait over eleven seconds with immediate list failures, substantially longer with slow requests. **Smallest fix:** carry one deadline through list requests, retry waits and fallback; pass only the remaining duration to the legacy watch. |
| + |
| +3. **High — [deploywatch.go:378](deploywatch.go:378): sticky completion can report success after the watched build was replaced.** Instance A reaches B while another instance is settling. A then reports C installed and serving. Its state remains “swapped”; once the other instance finishes, the watch succeeds for B. `TestWatchRolloutFinalIsStickyThroughADarkBoxAndANewerBuild` explicitly requires this incorrect result. The `modeURL && urlLive` arm also ignores a later rollout report entirely. **Smallest fix:** retain state across missing observations, but invalidate success on explicit supersession and process later rollout reports even after URL success. Change the tests to reject these false successes. |
| + |
| +4. **High — [deploywatch.go:235](deploywatch.go:235): failed observation still triggers HTTP polling of reporting hosts.** Three failed lists cause unconditional legacy fallback, waking sleeping instances on an armed fleet. `TestWatchRolloutThreeFailedFirstListsFallBack` pins that behavior. There is also a shorter route: `loadBoxJoin` suppresses status-read errors, so a successful instance-list response can falsely appear to contain no reports. **Smallest fix:** propagate status-read completeness, using `LoadBoxesComplete`, and retain an unknown state on observation failures. Fall back only on affirmative evidence that the targeted hosts use the legacy path. Test both endpoint failures and failed heartbeat reads with zero origin requests. |
| + |
| +5. **High — [internal/host/restart.go:852](internal/host/restart.go:852): unsigned requests can clear another box’s failure holds.** The agent passes a host from `restart-scope.json` directly to `RetryHost`. The repository’s region-host IAM policy grants writes throughout the deployment bucket. A compromised box can append retries naming another box’s hosts and bump the serial, bypassing console membership and the operate-role check. Restricting the request to locally owned routes does not authenticate its author. **Smallest fix:** authenticate retry entries with a console-held signing key and verify their account, app, host and failure identity before clearing anything. Keep unsigned `retry` entries inert. Q4 must be resolved before enabling the feature. |
| + |
| +6. **Medium — [internal/console/apiinstances.go:422](internal/console/apiinstances.go:422): customer-fleet rollout reports are omitted.** All three surfaces now agree by reading only platform boxes. A fleet box can report a held build under its fleet prefix while the instance list omits the hold, the retry door returns 409, and deploy falls back to HTTP polling. This loses required behavior rather than merely hiding a button. **Smallest fix:** give list, page and retry one fleet-aware loader, preserving account/app checks and reporting incomplete reads. |
| + |
| +7. **Medium — [deploywatch.go:235](deploywatch.go:235): a truncated flag-off list changes legacy watch output.** With more than 500 records and no rollout reports, `truncated` prevents fallback even when every targeted host is present. The command prints the new “roll out box by box” introduction although every target uses URL polling. This violates the required byte-for-byte no-report behavior. **Smallest fix:** establish completeness for the target set, rather than requiring an untruncated app-wide response, or paginate discovery. Add an exact-output comparison for this case. |
| + |
| +8. **Medium — [internal/console/swapcopy.go:84](internal/console/swapcopy.go:84): hold-mode behavior is described falsely.** Retry confirmation promises fallback to the last good build if retry fails; a hold-mode failure stays down. `HoldText` likewise says the instance “stays on” its good build when `AttemptDown` means nothing serves. Separately, [internal/rolloutwords/words.go:58](internal/rolloutwords/words.go:58) says the new build starts beside the running one for hold swaps and socketless worker replacements too. **Smallest fix:** use mode-neutral drafts where mode is unavailable and explicit down-state text where `AttemptDown` is known. Test the actual hold and worker emit/render paths. |
| + |
| +9. **Low — [internal/console/apiswap.go:40](internal/console/apiswap.go:40): the draft-marker and em-dash requirements are incomplete.** The new rate-limit response contains an em dash and has no marker. Other examples include the unmarked `Save` button in `templates/app_settings.html:380`, new flag help and decoder errors in `apps_swap.go`, and the new em-dash literal in `instances_cli.go:452`. These appear through ordinary settings, help, list or error paths. **Smallest fix:** sweep added member-facing literals, assign review IDs, and remove new em dashes. The preserved pre-existing sidecar sentence is a separate, accepted exception. |
| + |
| +## Rulings |
| + |
| +References below identify each `Ruling:` entry by its ledger line. |
| + |
| +- **L12 — Accept:** deferring the review server follows the controller’s explicit instruction. |
| +- **L13 — Accept:** marked drafts are authorized for this review stage; finding 9 identifies incomplete application. |
| +- **L14 — Accept:** the approved-copy pin cannot honestly precede approval; it remains required before shipping approved copy. |
| +- **L15 — Accept:** preserving existing flag-off text while marking replacement drafts maintains compatibility. |
| +- **L21 — Accept:** callers already supply reason words; mapping them twice corrupts classification. Keeping the emit-site diagnostic test is appropriate. |
| +- **L22 — Accept:** route registration, mutation wrappers and census entries follow current main’s authority checks. |
| +- **L23 — Accept:** placing new ship refusals outside the legacy-exemption file preserves that gate. |
| +- **L24 — Accept:** a five-minute heartbeat interval cannot support a reliable short-watch death heuristic. Observation errors still need explicit treatment. |
| +- **L25 — Reject:** truncation handling and five-second cadence are reasonable; unconditional sticky completion contradicts watched-build completion. Finding 3. |
| +- **L26 — Reject:** consistency between surfaces does not justify excluding customer-fleet boxes. Finding 6. |
| +- **L27 — Accept:** person-only policy changes fit §11 and the existing policy-door boundary. |
| +- **L28 — Reject as a complete disposition:** Q1’s resolution and Q2’s join are sound; Q3 affects dormant operation, and Q4 weakens retry authorization. Findings 1 and 5 need explicit resolution. |
| +- **L29 — Accept:** Task 14b supplies the index’s missing refusal and wake-reason obligations. |
| +- **L30 — Accept:** distinguishing known declines from refusals is appropriate; deferring the optional `FailureBook` display does not itself violate the surface contract. |
| +- **L31 — Accept:** retaining the existing sidecar sentence preserves flag-off behavior. |
| +- **L77 — Accept:** a blank REASON cell accurately represents no reported reason. |
| +- **L83 — Reject:** three failed observations do not establish that HTTP polling is safe. Truncation protection and treating unknown unavailable codes conservatively are sound. |
| +- **L91 — Accept:** retaining the pushed WIP commit is harmless provided the required squash happens before landing. |
| +- **L125 — Accept:** CI may supply the full-suite gate, but landing still requires green CI on the final squashed tip; the ledger’s local-check summary is not that evidence. |
| \ No newline at end of file |