"Portability hardening - install.sh gate autodetect for Go/JVM/.NET/PHP/Ruby + per-repo .cyberos/config.yaml overrides"
TASK-CUO-207: Gate autodetect portability + per-repo config
§1 - Description
Make /install produce working gates on the stacks CyberSkill's client projects actually use, and give every repo one sanctioned place to override what detection gets wrong - so vendored files stay pristine and updates never clobber local decisions.
Normative clauses:
- Gate autodetection MUST extend to, in documented order after the existing Rust/Node/Python detectors: Go (
go.mod-> buildgo build ./..., lintgo vet ./..., testgo test ./..., coveragego test -coverprofile), Maven (pom.xml->mvn -q -DskipTests package/mvn -q verify), Gradle (build.gradleorbuild.gradle.kts, preferring./gradlewwhen present ->build/test), .NET (*.slnor*.csproj->dotnet build/dotnet test), PHP (composer.json->composer validate --strictplusvendor/bin/phpunitwhen present), Ruby (Gemfile->bundle exec rspecwhen spec/ exists, elsebundle exec rake testwhen a Rakefile exists). Multi-stack repos MUST union the detected gates; detection MUST never invent a command whose tool marker file is absent. - A per-repo config file
.cyberos/config.yamlMUST be honored byrun-gates.shwhen present, with keys:gates.build,gates.lint,gates.test,gates.coverage(string commands; each overrides ONLY its own gate),coverage_threshold(integer, default 90),task_template(engineering-spec@1|task@1, consumed by TASK-CUO-208),profile(full|reduced). Unknown keys MUST warn, not fail. install.shMUST scaffold.cyberos/config.yamlexactly once (never clobber an existing one, same discipline as BACKLOG/AGENTS), pre-filled with every value commented out and the DETECTED commands written as comments beside each key, so the file documents what will run by default.run-gates.shMUST resolve each gate as: config value if set, else autodetected, else absent - and MUST print one provenance line per gate before running it:gate <name>: <command> (source: config|autodetect:<stack>|absent).coverage_thresholdMUST flow to the coverage gate:run-gates.shexposes it (envCYBEROS_COVERAGE_THRESHOLD) and the coverage-gate skill contract reads it, defaulting to 90 when unset (hook already named by TASK-SKILL-118's rubric constants).- Repos where nothing is detected and no config exists MUST keep today's reduced-floor behavior with an explicit message naming the config file as the fix.
- Config parsing MUST be dependency-free (grep/sed-level YAML subset: top-level and one nesting level, scalar values); a malformed config MUST fail gates loudly with the offending line, never half-apply.
§2 - Why this design
Config-over-autodetect (per key, not all-or-nothing) matches how real repos deviate: usually one gate is special, the rest are standard. Scaffolding the config WITH detection results as comments makes /install self-documenting on day one and keeps the file inert until the operator uncomments a line - update-safe by construction. The provenance line kills the classic debugging question ("which command even ran?") across a fleet of differently-shaped repos.
§3 - Contract
# .cyberos/config.yaml (scaffolded form, everything commented)
# gates:
# build: "go build ./..." # autodetected: go
# lint: "go vet ./..." # autodetected: go
# test: "go test ./..." # autodetected: go
# coverage: "go test -coverprofile=coverage.out ./..." # autodetected: go
# coverage_threshold: 90
# task_template: engineering-spec@1
# profile: full
Provenance output: gate test: go test ./... (source: autodetect:go) | gate lint: make lint (source: config) | gate coverage: (source: absent).
§4 - Acceptance criteria
- Each new stack detects (§1 #1) - fixture repos for Go, Maven, Gradle (with and without wrapper), .NET, PHP (with and without phpunit), Ruby (rspec and rake variants) each yield the specified commands and nothing else.
- Multi-stack unions (§1 #1) - a fixture with
go.mod+package.jsonyields both stacks' gates, deduplicated by gate name with both provenance lines. - No marker, no command (§1 #1) - a PHP fixture without
vendor/bin/phpunitgetscomposer validate --strictonly. - Config overrides per key (§1 #2, #4) - config setting only
gates.lintleaves build/test/coverage autodetected; provenance lines showconfigfor lint andautodetectfor the rest. - Scaffold once, never clobber (§1 #3) - first init writes the commented config with detected values; a hand-edited config survives a re-install byte-identical.
- Threshold flows (§1 #5) -
coverage_threshold: 85surfaces asCYBEROS_COVERAGE_THRESHOLD=85in the gate environment; unset -> 90. - Reduced floor preserved with pointer (§1 #6) - an empty fixture repo reports the floor message naming
.cyberos/config.yaml. - Malformed config fails loudly (§1 #7) - a config with a tab-indented or unparseable line fails
run-gates.shciting the line number; no gate runs.
§5 - Verification
# tools/install/tests/test_gate_autodetect.sh
t01_stack_matrix() # AC 1 (8 fixture repos, expected command sets)
t02_multistack_union() # AC 2
t03_marker_gating() # AC 3
t04_config_per_key_override() # AC 4
t05_scaffold_once() # AC 5
t06_threshold_env() # AC 6
t07_reduced_floor_message() # AC 7
t08_malformed_config_loud() # AC 8
§6 - Implementation skeleton
install.sh: extend the detector case-block; emit the commented config via heredoc when absent. run-gates.sh: minimal reader (cfg_get key via awk over the two-level subset), resolution order per gate, provenance echo, threshold export. README: stack table + config reference.
§7 - Dependencies
Blocks TASK-CUO-208 (it reads task_template from this config). TASK-SKILL-118's coverage rubric names the threshold hook this task turns on. No dependency on Wave A.
§8 - Example payloads
$ bash .cyberos/cuo/gates/run-gates.sh
gate build: ./gradlew build (source: autodetect:gradle)
gate lint: (source: absent)
gate test: ./gradlew test (source: autodetect:gradle)
gate coverage: mvn -q verify (source: config)
§9 - Open questions
None blocking. Container-based stacks (Docker-only repos) stay reduced-floor by design; a gates.* config line is their sanctioned path - documenting a docker-compose example in README is part of #1's doc work, not a new detector. JVM coverage is deliberately undetected (jacoco/kover wiring is repo-specific); the scaffolded config's commented gates.coverage line is the sanctioned path there too.
§10 - Failure modes inventory
- Wrong tool version on the machine (gradle vs gradlew drift) - wrapper-preferred rule (#1) plus provenance line make the executed command visible; config overrides when the default is wrong.
- Autodetect finds a marker in a vendored subdir (
node_modules/package.json) - detectors MUST scan the repo root only (as today); fixture t03 includes a nested marker that must not fire. - Config injection (config runs arbitrary commands) - accepted by design: the config is repo-committed and operator-owned, same trust level as a Makefile; the provenance line keeps it visible.
- YAML subset surprises (quotes, colons in commands) - the reader supports quoted scalars; t08 covers an unsupported construct failing loudly rather than mis-parsing.
- Threshold set above 100 or non-integer - reader validates 1..100 integer; else loud fail (t08 family).
§11 - Implementation notes
Keep detector order stable and documented in README (first-party stacks first); the union rule means order only affects provenance labels, never which gates exist. The config reader lives in run-gates.sh (payload-vendored) so target repos need no extra file.
End of TASK-CUO-207.
Audit
TASK-CUO-207 audit
§1 - Verdict summary
Audited for detection honesty (never invent a command), override granularity, and update-safety of the scaffolded config. The per-key resolution model and the provenance line survived scrutiny; JVM coverage detection was correctly descoped rather than overpromised. Traceability closes over t01-t08 in tools/install/tests/test_gate_autodetect.sh.
§2 - Findings (all resolved)
ISS-001 all-or-nothing override was the wrong grain
Real repos deviate on ONE gate. Resolved: §1 #2 per-key override; AC 4 mixed-provenance fixture.
ISS-002 re-install clobber risk
A config the operator edited must survive updates byte-identically. Resolved: §1 #3 scaffold-once discipline (same rule as BACKLOG/AGENTS), AC 5.
ISS-003 vendored-marker false fires
node_modules/package.json would detect Node in every JS-adjacent repo. Resolved: root-only scanning pinned (§10 #2) with a nested-marker fixture in t03's family.
ISS-004 JVM coverage overpromise
Draft mapped a coverage command for Maven/Gradle; jacoco/kover wiring is repo-specific and a wrong default poisons the coverage gate. Resolved: deliberately undetected, config named as the sanctioned path (§9), keeping the never-guess rule intact.
ISS-005 malformed config half-apply
A partially-read config running SOME gates is worse than failing. Resolved: §1 #7 loud fail with line number, no gate runs, AC 8.
ISS-006 invisible command provenance
Fleet debugging dies on "which command even ran?". Resolved: §1 #4 one provenance line per gate (config|autodetect:<stack>|absent), asserted in AC 4.
§3 - Resolution
All six findings addressed as cited. Blocks TASK-CUO-208 as declared; threshold hook matches TASK-SKILL-118's rubric constant. Score = 10/10.
End of TASK-CUO-207 audit.
§4 - Ship record (2026-07-12)
- Implementation: union claim() detectors (9 stacks + make fallback), per-gate provenance, scaffold-once config.yaml, dependency-free yaml-subset reader, threshold flow to the coverage-gate contract, loud malformed-fail; commit d29532b. Phase artefacts: docs/tasks/.workflow/TASK-CUO-207/.
- Review: human verdict at gate 1 APPROVE + pre-authorize done (Stephen Cheng, in-chat).
- Testing: test_gate_autodetect.sh 8/8 (one per AC), 7/7 cyberos-install suites. Gate 2 recorded per pre-authorization. Manifest-tracked run (second production use of ship-manifest@1) - hitl.requested_at recorded at gate, approval taken in-chat per §1 #8 of TASK-CUO-206 (requested_at is never approval).
Verdict unchanged: PASS, Score = 10/10.