Problem
redeployPreflight.mjs authorizes a container-affecting deploy against a bundle that cannot restore the Knowledge Base.
Reproduction (read-only, ~40s):
node ai/scripts/maintenance/redeployPreflight.mjs --compose-project <project> --json
Observed:
decision : PROCEED_VERIFIED
proceed : true
reason : "A verified, non-empty, restorable pre-transition bundle exists."
verdictCode : RESTORABLE
rowTotal : 34239
Ground truth for the bundle it selected:
bundle-meta.json → {"kb":0,"memories":32462,"summaries":1777}
kb/ payload → 0B, 0 entriesrowTotal 34239 is memories + summaries alone. The KB contributes zero, on a plane whose KB corpus is the recovery target. A deploy that went wrong would have had nothing to roll back to.
The obvious fix is WRONG — it breaks a documented safety interlock
⚠️ Do not implement "refuse when any substrate is empty". restore.mjs:1043-1061 records that the aggregate is deliberate:
restorable deliberately still keys on the aggregate, and NOT because the question is merely unsettled: making a partially-empty bundle non-RESTORABLE would break a safety interlock. evaluateRedeployPreconditions reads this verdict in TWO incompatible roles — as PROOF OF PRIOR STATE (any populated collection shows the plane existed; it is one of the priorEvidence items that REFUSE --initialize), and as AUTHORIZATION TO PROCEED with a container-affecting redeploy (which does want completeness).
And the concrete regression it names:
a host with a KB-only bundle and no marker would fall through to REFUSE_NO_VERIFIED_BUNDLE, whose message instructs the operator to pass --initialize — and --initialize would then PROCEED, because the bundle no longer counts as prior evidence, discarding a plane that had a real backup.
Tightening the boolean trades a false PROCEED for a false --initialize authorization. That is strictly worse: the second destroys data.
The comment also pre-empts the consumer-side workaround:
emptyCollections is a REPORTING fact only: a consumer that treats it as an authorization signal recreates the same interlock break from the outside.
So collectionCounts and emptyCollections are already published (#16510 / #16520). The defect is not missing information. It is that one boolean answers two different questions.
The real repair — split the verdict's two roles
Named by the code itself:
Splitting the verdict's two roles is the real repair — it changes a consumed surface and does not belong under an incident-time additive change.
Restated as scope:
evaluateRedeployPreconditions asks two questions instead of reading one boolean twice:
- Prior-state evidence — "did a plane exist here?" Satisfied by ANY populated collection. Keeps refusing
--initialize exactly as today. Must not tighten.
- Recovery-source authorization — "is this bundle a complete rollback target?" Requires per-substrate completeness. This is the question that should refuse a
kb:0 bundle.
restorable splits into two named fields, or gains an explicit role parameter. This is a consumed-surface change — every caller of verifyLatestBackupRestorable needs auditing, not just the preflight.
- The refusal for an incomplete-but-real bundle must NOT emit the
--initialize instruction. That instruction is what makes today's wrong answer dangerous rather than merely wrong.
The class worth fixing, not just the instance
Any aggregate-over-substrates verdict can hide a fully-empty substrate. Same family as #16569 (a plane-id default valid for exactly one deployment) and #16568 (runtimeFreshness reporting "current" while far behind): each returns a well-formed pass about the wrong subject.
Acceptance criteria
Out of scope
- The producer side — #16563 (KB export reports success on zero rows). Even once producers are fixed, the gate must not bless an empty substrate.
- Any change to
restorable's prior-evidence semantics.
Related
- #16563 — producer side: success-shaped bundles with zero KB rows (5 consecutive observed).
- #16510 / #16520 — per-collection restorability facts; already available, deliberately reporting-only.
- #16568, #16569 — same failure family.
Note on this ticket's history
Filed from the symptom with an AC that was the interlock break, because I read the observed output and not the decision recorded at the decision site. Corrected here. The live observation stands; only the prescription was wrong.
Evidence
L1 — live tool invocation plus the on-disk manifest of the selected bundle, 2026-08-06.
Authored by @neo-opus-vega (Claude Opus 5).
Problem
redeployPreflight.mjsauthorizes a container-affecting deploy against a bundle that cannot restore the Knowledge Base.Reproduction (read-only, ~40s):
Observed:
Ground truth for the bundle it selected:
bundle-meta.json → {"kb":0,"memories":32462,"summaries":1777} kb/ payload → 0B, 0 entriesrowTotal 34239ismemories + summariesalone. The KB contributes zero, on a plane whose KB corpus is the recovery target. A deploy that went wrong would have had nothing to roll back to.The obvious fix is WRONG — it breaks a documented safety interlock
⚠️ Do not implement "refuse when any substrate is empty".
restore.mjs:1043-1061records that the aggregate is deliberate:And the concrete regression it names:
Tightening the boolean trades a false PROCEED for a false
--initializeauthorization. That is strictly worse: the second destroys data.The comment also pre-empts the consumer-side workaround:
So
collectionCountsandemptyCollectionsare already published (#16510 / #16520). The defect is not missing information. It is that one boolean answers two different questions.The real repair — split the verdict's two roles
Named by the code itself:
Restated as scope:
evaluateRedeployPreconditionsasks two questions instead of reading one boolean twice:--initializeexactly as today. Must not tighten.kb:0bundle.restorablesplits into two named fields, or gains an explicit role parameter. This is a consumed-surface change — every caller ofverifyLatestBackupRestorableneeds auditing, not just the preflight.--initializeinstruction. That instruction is what makes today's wrong answer dangerous rather than merely wrong.The class worth fixing, not just the instance
Any aggregate-over-substrates verdict can hide a fully-empty substrate. Same family as #16569 (a plane-id default valid for exactly one deployment) and #16568 (
runtimeFreshnessreporting "current" while far behind): each returns a well-formed pass about the wrong subject.Acceptance criteria
--initialize— the interlock proven unchanged.kb:0bundle fails the authorization question, and the refusal names the empty substrate.--initialize.verifyLatestBackupRestorableaudited for which role it wants; recorded even where unchanged.emptyCollectionsstays reporting-only until the split lands, per the existing comment.{"kb":0,...}with healthy siblings, which returnsPROCEED_VERIFIEDtoday.Out of scope
restorable's prior-evidence semantics.Related
Note on this ticket's history
Filed from the symptom with an AC that was the interlock break, because I read the observed output and not the decision recorded at the decision site. Corrected here. The live observation stands; only the prescription was wrong.
Evidence
L1 — live tool invocation plus the on-disk manifest of the selected bundle, 2026-08-06.
Authored by @neo-opus-vega (Claude Opus 5).