Task — engineering-spec@1

Fail-closed machine gates - RED when zero gate commands are configured

doneTASK-CUO-302
module cuo · class product · priority p0 · created 2026-07-23 · shipped null
depends on none · blocks none

TASK-CUO-302: Fail-closed machine gates - RED when zero gate commands are configured

Summary

run-gates.sh is the machine-gate floor for every task lifecycle transition, and today it reports GATES: GREEN and exits 0 when not a single gate command is configured. This repo is itself the proof: autodetect returned unknown on the polyglot monorepo, all four floor commands are empty, and the flagship repo gates nothing while reporting green. This task makes the empty floor RED by default, adds an explicit acknowledged-empty escape hatch, teaches autodetect a monorepo fallback so this repo detects its own suite, and stops the gates.env header from inviting edits that the next reinstall wipes.

Problem

Three verified defects compound into audit finding C1 (a core safety promise is false):

  1. Fail-open floor. gate() treats an empty command as SKIP (tools/install/gates/run-gates.sh:57), and after the per-gate loop the all-empty case prints an advisory line (GATES: floor only - nothing detected and no overrides, line 75) and then falls through to GATES: GREEN with exit 0 (lines 77-79). Green-is-necessary doctrine (AGENT-ENTRY.md #3) assumes green means something; an all-skip green is vacuous, and both HITL acceptance gates downstream inherit that false confidence.
  2. Autodetect has no monorepo fallback. install.sh probes package.json scripts and per-ecosystem markers; on this polyglot repo it lands ECOSYSTEM=unknown and seeds nothing. The repo has an obvious canonical suite entrypoint (scripts/tests/run_all.sh, 42 suites) that autodetect never looks for.
  3. The header invites edits the machine wipes. The generated gates.env says (edit freely) (install.sh:299) while reinstall regenerates the file and moves the old one to a .bak (install.sh:326 even says durable overrides belong in config.yaml). An operator followed the header's advice; the reinstall wiped a working TEST_CMD which now survives only in gates.env.bak.1784761166.

Proposed Solution

Make the all-empty floor exit RED with a message that names the two real fixes (.cyberos/config.yaml gates.* keys, or re-running install after adding ecosystem markers) and the explicit escape hatch CYBEROS_ALLOW_EMPTY_GATES=1 for repos that genuinely have nothing to run (docs-only repos). The escape hatch prints a loud GATES: EMPTY-ACKNOWLEDGED line so an acknowledged-empty run is never confusable with a green one. Teach the install.sh autodetect an ordered monorepo fallback probe - scripts/tests/run_all.sh first, then Makefile test: target - seeding TEST_CMD with provenance SRC_TEST="fallback:<probe>" so run-gates.sh's existing provenance line shows where the command came from. Reword the generated gates.env header from "edit freely" to "machine-owned; regenerated on every install - durable overrides belong in .cyberos/config.yaml (gates.build/lint/test/coverage)". Ship a CHANGELOG entry marking the RED-on-empty behavior as breaking for consumer repos that relied on floor-only green.

Alternatives Considered

Success Metrics

Scope

In scope: tools/install/gates/run-gates.sh (RED-on-empty + acknowledged-empty ack line), tools/install/install.sh (fallback autodetect probes + gates.env header rewording), tools/install/README.md (document the new failure mode + escape hatch), CHANGELOG entry, and a new test suite covering all three behaviors.

Out of scope / Non-Goals

Dependencies

None blocking. Builds directly on TASK-CUO-207 (done), which shipped the .cyberos/config.yaml gates layer and the per-gate autodetect provenance (SRC_*) this task extends with a fallback: source tier. TASK-IMP-129 (draft) makes config.yaml survive uninstall - complementary, no ordering constraint. TASK-IMP-140's benchmark gate G1 ("Gate-floor non-vacuous") is verified by the test this task ships; listed in related_tasks as a soft forward reference, no cycle.

AI Authorship Disclosure

1. Description (normative)

2. Acceptance criteria

3. Edge cases

Audit

§1 — Verdict summary

Six §1 clauses, six ACs, six edge cases including a security-class row. Every clause traces 1:1 to an AC via traces_to; every factual claim in Problem/source_pages was re-verified against the working tree during authoring (fail-open exit path read in source, all-empty local gates.env confirmed, header/regen-notice contradiction confirmed at install.sh:299 vs :326). The two consequential findings were an unpinned exit code and undefined env-var value semantics — both would have shipped ambiguity into the exact automation surface (G1 checker) this task exists to serve.

§2 — Findings (all resolved)

ISS-001 — RED-on-empty exit code was unpinned (spec said only "non-zero")

run-gates.sh already uses exit 1 (gate failure) and exit 2 (missing gates.env / malformed config.yaml). A bare "non-zero" for the empty floor would collide with both, and TASK-IMP-140's G1 checker needs to distinguish "gates ran and failed" from "nothing configured" mechanically. Resolved: clause 1.1 pins exit 3 and names why it is distinct; AC 1 asserts the exact code.

ISS-002 — escape-hatch value semantics were undefined (TRACE-006-adjacent)

"CYBEROS_ALLOW_EMPTY_GATES=1 is set" left =true, =yes, =0 undefined — an operator exporting =true would get RED and reasonably call it a bug, or worse, a sloppy implementation would accept any non-empty value and =0 would acknowledge-empty. Resolved: clause 1.1 requires the literal 1 and requires every other value to behave as unset; AC 1 asserts =true and =0 still exit 3.

ISS-003 — non-execution of fallback probes needed an observable test method

Clause 1.4 demands the fallback MUST NOT execute probe targets at install time, but the first AC draft asserted only that the command string was seeded — a test that cannot see execution cannot verify a MUST NOT. Resolved: AC 4 asserts "install runs neither probe target", verifiable via sentinel fixtures (probe scripts that write a marker file when executed; the test asserts the marker is absent).

ISS-004 — acknowledged-empty was distinguishable only by exit code in the first draft

If the ack path printed the normal GATES: GREEN line, log readers (and the ship-tasks transcript) could not tell an acknowledged-empty run from a real green run — the exact conflation C1 is about. Resolved: clause 1.3 requires the distinct GATES: EMPTY-ACKNOWLEDGED line AND the absence of GATES: GREEN; AC 3 asserts both halves.

ISS-005 — fallback probe order was stated but not contractual

With both scripts/tests/run_all.sh and a Makefile present, the seeded command depended on implementation order. Resolved: clause 1.4 fixes the ordered, closed probe list; the edge-case section adds the both-present fixture; AC 4 asserts run_all wins.

ISS-006 — stale installed trees (old header text) vs new behavior needed an explicit posture

An upgraded payload enforces RED-on-empty while the consumer's gates.env still carries the old "edit freely" header until the next install — a reader could conclude enforcement waits on the header. Resolved: edge case states behavior ships with the vendored run-gates.sh regardless of header vintage; stale-header-only trees are acceptable, stale behavior is not.

§3 — TRACE-006 semantic sufficiency (per clause)

ClauseVerb demandCited test assertsVerdict
1.1 MUST exit 3 on empty floor; other env values behave as unsetexact exit code on empty; unchanged semantics when configured; literal-1 gateAC 1: exit==3 empty, ==0/==1 configured, =true/=0 still 3sufficient
1.2 MUST name both fixes + hatchthree actionable substrings present in RED outputAC 2: asserts all three substringssufficient
1.3 MUST print distinct ack line, not GREENpositive line present AND green line absentAC 3: asserts both halvessufficient
1.4 MUST seed fallback with provenance; MUST NOT executeseeded value + SRC_TEST provenance + non-execution + precedenceAC 4: asserts seed, provenance, sentinel non-execution, run_all-beats-Makefilesufficient after revision (ISS-003, ISS-005)
1.5 MUST NOT say "edit freely"; MUST state machine-ownednegative substring + positive replacement in generated fileAC 5: asserts both halves against a scratch installsufficient
1.6 MUST gain CHANGELOG entrypositive content present in top entryAC 6: asserts three required substringssufficient

§4 — Resolution

Six findings, all material, all resolved in the audited revision. Score = 10/10.

Status transition draft -> ready_to_implement is authorised by this verdict per STATUS-REFERENCE.md §1.1. The two human-acceptance gates in /ship-tasks (review acceptance, final acceptance) are unchanged and remain recorded human verdicts — this audit clears the spec-correctness gate only.


End of TASK-CUO-302 audit.