Files
tech-log-backend/docs/superpowers/specs/2026-07-25-ci-control-plane-recovery-design.md
T

25 KiB

CI Control Plane Recovery Design

  • Date: 2026-07-25
  • Status: Approved
  • Scope: repository control-plane recovery, Gitea Actions enablement, Gradle/CI gates, container build context, dependency automation, and documentation parity
  • Depends on: 2026-07-20-harness-policy-engine-design.md
  • Current revision audited: 821fe00c323b5335980f271c7ee47b92ac2168f2
  • Task-packet state: unavailable. .harness/validators/resolve_task.py and its policy inputs are absent from the audited revision, so no task-packet hash or formal evidence profile can be produced before control-plane recovery.

1. Problem Statement

The repository describes a high-assurance CI and architecture-governance control plane, but the audited Git tree does not contain the hidden root paths that implement it. The current revision contains no .harness, .agents, .claude, .codex, or .github tree. It also lacks the root .tool-versions, .trivyignore.yaml, and .gitattributes contracts referenced by the tracked guidance and Gradle build.

This is not a Gradle wrapper failure. The tracked wrapper downloads and starts Gradle 9.0.0 under Java 21, but every project task stops while evaluating src/settings.gradle because .harness/project/modules.yaml is missing. The same missing registry also prevents the architecture dependency gate and task-packet resolution from running.

The CI host is Gitea 1.27.0, not GitHub. The public repository API reports has_actions: false, so the repository Actions unit is disabled. An unauthenticated request to the runner API returns 401, which proves that runner state must be checked with repository or administrator authorization; it does not prove that a usable runner exists. Gitea Actions requires both the repository Actions unit and an online runner.

The container build has an additional independent defect. Compose and both Dockerfile examples use src/ as the build context, while Gradle resolves the registry from the repository root. Even after the hidden assets are restored, a build with the current context cannot copy the registry into the builder.

2. Audit Baseline

Surface Command or source Observed result
Git revision git rev-parse HEAD 821fe00c323b5335980f271c7ee47b92ac2168f2
Hidden assets git cat-file -e HEAD:.harness and equivalent checks .harness, .agents, .claude, .codex, .github absent
Ignore rules git check-ignore -v --no-index ... exit 1; missing paths are not ignored
Recoverable local objects git fsck --full --no-reflogs --unreachable exit 0; no unreachable objects reported
Gradle launcher cd src && ./gradlew --version exit 0; Gradle 9.0.0 and Java 21
Gradle project task cd src && ./gradlew tasks --console=plain exit 1; missing module registry at src/settings.gradle:12
Gradle release gate cd src && ./gradlew check --console=plain exit 1; same settings failure
Task resolver python3 .harness/validators/resolve_task.py ... impossible; resolver file absent
Compose syntax docker compose -f docker-compose.yml -f docker-compose.local.yml config --quiet exit 0
Gitea version GET /api/v1/version 1.27.0
Repository Actions public repository API has_actions: false
Runner API unauthenticated runner request 401; authorized runner inventory still required

The most recent commit added 971 files over a parent that contained only a two-line README. The root tree contains no dot-prefixed entry, while nested files such as src/.env, src/.dockerignore, and src/.gitignore were included. This is consistent with a top-level shell glob used during copying or staging. That is a falsifiable root-cause hypothesis, not proof of the exact command that was used.

3. Goals

  1. Prefer byte-for-byte recovery of the authoritative hidden control-plane assets; when that source is unavailable or incomplete, require an explicit human reconstruction decision and record new provenance without presenting reconstruction as restoration.
  2. Restore the 19-leaf module registry and the harness behavior approved in the 2026-07-20 design.
  3. Make a fresh checkout fail early with a precise control-plane error before Gradle configuration.
  4. Enable repository Actions on Gitea 1.27.0 and provide an isolated, repository-scoped runner.
  5. Keep .github/workflows as the canonical workflow directory while preventing an accidental .gitea/workflows shadow.
  6. Restore a single release-blocking fan-in status and the complementary vulnerability status.
  7. Make public-path, Trivy suppression, dependency-lock, and architecture gates fail closed.
  8. Make Docker builds consume the same root registry without duplicating the registry under src/.
  9. Pause Renovate automerge until required CI statuses and lockfile refresh behavior are proven.
  10. Bring README, gate-matrix, workflow, and physical-path claims back into parity.

4. Non-Goals

  • This recovery does not change production Java behavior or module boundaries.
  • It does not redesign the 19-leaf registry approved in the 2026-07-20 harness policy design.
  • It does not silently synthesize hidden policy files or present reconstructed content as recovered authority.
  • It does not store a Gitea API token, runner registration token, or repository secret in Git.
  • It does not enable deployment to a production environment. Release artifact construction and scanning are restored, but a separate deployment decision remains human-owned.
  • It does not re-enable Renovate automerge merely because workflow files exist; branch protection and a successful dependency-update exercise are also required.

5. Governing Invariants

5.1 Recovery mode is an explicit human decision

The original working tree, archive, or source repository that produced the 2026-07-20 harness design is the preferred recovery authority. Before copying anything into this repository, the recovery source must be inventoried and hashed outside the worktree.

The minimum authoritative set is:

  • .harness/
  • .agents/
  • .claude/
  • .codex/
  • .github/
  • .tool-versions
  • .trivyignore.yaml
  • .gitattributes

The human owner chooses one of two modes and records it before repository writes:

Mode A — authoritative restore. All minimum paths exist in the recovery source. The executor hashes them, copies them byte-for-byte, proves source/destination equality, and preserves their native provenance.

Mode B — controlled reconstruction. The original source is unavailable or incomplete. The human records that fact and explicitly authorizes reconstruction from the approved 2026-07-20-harness-policy-engine-design.md, 2026-07-20-harness-policy-engine.md, the tracked Gradle/module sources, and this design. Reconstructed artifacts receive new hashes and a controlled-reconstruction provenance record. Schema, renderer parity, mutation coverage, Gradle project discovery, and architecture dependency checks must pass before the new artifacts can act as authority.

The first Mode B inventory covers the reconstructed harness and generated platform assets only. After Tasks 3-6 and every later change to a covered path are final, the executor regenerates one complete, sorted SHA-256 inventory for .harness, .agents, .claude, .codex, .github, .tool-versions, .trivyignore.yaml, .gitattributes, .dockerignore, and docs/security/public-paths-snapshot.txt. The provenance record names that final evidence path. Only this post-change inventory is used for the human handoff.

Implementation stops only until the human chooses Mode A or Mode B. An incomplete Mode A export must never be filled silently. It may instead cause the human to switch the recorded decision to Mode B. The tracked AGENTS.md, CLAUDE.md, and design documents are evidence of intended behavior, but reconstructed schemas, agents, registries, and workflows become authoritative only after the required new evidence passes.

5.2 Human-only commit policy

Recovery and implementation may leave reviewed changes in the working tree, but agents do not stage, commit, amend, or push. A human decides commit boundaries after reviewing recovery hashes, generated-file parity, test evidence, and Gitea status checks.

5.3 One registry and one workflow source

.harness/project/modules.yaml remains the only module-edge and focused-command registry. Container builds copy that file from the repository root; they do not create a second copy under src/.

.github/workflows remains the canonical workflow directory because the repository documentation and portability contract already point there. Gitea's default WORKFLOW_DIRS value is .gitea/workflows,.github/workflows, and Gitea uses the first directory that exists. Therefore .gitea/workflows must remain absent unless the project later adopts a generated-mirror design with an explicit parity check and a separate approved specification.

5.4 Fail closed before expensive work

CI runs a repository control-plane preflight before invoking build or container work. Missing policies, workflow shadowing, a missing committed public-path baseline, or an incomplete structured Trivy contract fail immediately. Once Java/Gradle is available, that same blocking preflight runs verifyTrivyignore; the vulnerability scanner must explicitly consume .trivyignore.yaml.

6. Target Architecture

authoritative hidden-asset export
            |
            v
recovery inventory + SHA-256 comparison
            |
            v
.harness/.agents/.claude/.codex/.github restored
            |
            v
control-plane preflight
  |-- required paths
  |-- generated-agent parity
  |-- canonical workflow directory
  |-- committed security baselines
  `-- task-packet resolver availability
            |
            v
resolved high-risk CI/deployment task packet
            |
            +-----------------------------+
            |                             |
            v                             v
Gradle quality gates                Docker root-context builds
  |-- dependency locks                |-- production bootJar
  |-- architecture edges              `-- sample bootJar
  |-- focused/ArchUnit tests
  |-- test/check
  `-- public/env/security contracts
            |                             |
            +--------------+--------------+
                           v
                 CI quality release-gate
                           +
          dependency-vulnerability required status
                           |
                           v
             Gitea protected-branch decision

7. Design Decisions

7.1 Recovery gate and physical control-plane manifest

After Mode A restore parity or Mode B reconstruction evidence passes, the harness gains a small physical manifest at .harness/project/control-plane.yaml. It lists required files, required directories, the canonical workflow directory, and the forbidden shadow directory. The file uses JSON syntax, matching the 2026-07-20 design's stdlib-only JSON-as-YAML convention.

.harness/validators/validate_control_plane.py reads the manifest and reports every missing path in one deterministic result. It also rejects .gitea/workflows. Its tests live at .harness/tests/test_control_plane.py.

This validator checks physical availability only. It does not duplicate module edges, risk rules, or workflow gate semantics. Module semantics stay in modules.yaml; the gate matrix stays in .github/ci-gate-matrix.yml.

7.2 Gitea Actions and runner control

The repository owner enables Enable Repository Actions in the repository settings. The public API must then report has_actions: true.

The runner is registered at repository scope, uses an isolated Docker execution mode, and exposes the exact ubuntu-22.04 label used by the workflows. Registration credentials remain in the runner host's protected state or secret manager. They never enter workflow YAML, shell history captured by CI, Docker image layers, or repository files.

An authenticated repository runner inventory must show at least one enabled, online runner before the first required workflow is treated as operational. The previous unauthenticated 401 remains an expected access-control result.

7.3 Canonical workflow directory and Gitea shadowing

The recovery restores canonical workflows under:

  • .github/workflows/ci-quality-gates.yml
  • .github/workflows/build-release-supply-chain.yml
  • .github/workflows/dependency-vulnerability.yml

No workflow is copied to .gitea/workflows. With Gitea's default directory ordering, the mere existence of .gitea/workflows would cause .github/workflows to be ignored. The preflight validator makes that shadow a blocking failure.

Instance administration must confirm that [actions].WORKFLOW_DIRS still contains .github/workflows. If the instance has a non-default value that excludes it, the administrator changes the instance setting or the project stops before enabling required checks.

7.4 Preflight and release-gate topology

ci-quality-gates.yml starts with control-plane-preflight. No Gradle, test, or Docker job runs unless preflight succeeds.

The release-blocking fan-out includes:

  • restored harness unit and mutation suite
  • validate_modules.py, renderer parity, policy parity, and verify-gate-matrix.sh
  • structured Trivy validation through verifyTrivyignore
  • Gradle wrapper launch and Java 21 assertion
  • verifyDependencyLocks
  • verifyCleanArchitectureDependencies
  • focused ArchUnit coverage
  • verifyPublicPathSnapshot
  • test
  • check
  • production and sample Docker builds
  • reproducible artifact verification when owned by the restored workflow contract

The workflow ends with a single release-gate job that uses if: always() and fails unless every release-blocking dependency succeeded. Quarantine remains non-blocking and is intentionally absent from the fan-in.

Gitea cannot express needs across separate workflow files. The dependency vulnerability workflow therefore publishes its own blocking status. Protected branches require both the quality release-gate status and the vulnerability status.

7.5 Missing contracts

The recovery must restore .tool-versions, .gitattributes, .trivyignore.yaml, workflow scripts, gate matrix, CODEOWNERS, and vulnerability policy from the authoritative source.

The structured empty Trivy contract is retained even when there are no suppressions:

vulnerabilities: []
licenses: []
misconfigurations: []
secrets: []

The quality preflight and release-tag preflight both run verifyTrivyignore. The independent vulnerability workflow also runs that verifier and passes trivyignores: .trivyignore.yaml to the pinned Trivy action, so a present-but-unconsumed or malformed suppression file cannot satisfy a required status.

docs/security/public-paths-snapshot.txt becomes a committed baseline. The current approved value derived from src/.env is /api/healthcheck. A missing baseline is a failure, not an instruction to create one during verification.

The read-only verifyPublicPathSnapshot task compares the committed baseline to src/.env. Generation moves to a separate, explicitly approved update task. check and the quality workflow both depend on the read-only verification task.

7.6 Dependency locks

All 19 leaf modules retain Gradle strict locking. CI runs verifyDependencyLocks before compile or test jobs so an incomplete Renovate update fails with a direct lock error.

The only supported lock refresh command remains:

cd src
./gradlew resolveAndLockAll --write-locks --console=plain

A dependency update is acceptable only when the declaration, all affected gradle.lockfile files, and the quality gate agree. CI never runs --write-locks.

7.7 Docker root context

Compose changes the app build context from src/ to the repository root and addresses the Dockerfile as src/Dockerfile. Both Dockerfiles keep the Gradle project at /build/src and copy:

  1. .harness/project/modules.yaml to /build/.harness/project/modules.yaml;
  2. wrapper, build descriptors, and lockfiles to /build/src;
  3. the complete src/ tree only after dependency verification.

A root .dockerignore replaces the context role previously owned by src/.dockerignore. It excludes Git metadata, build output, IDE state, environment files, and secrets, while explicitly allowing the module registry, wrapper, build descriptors, lockfiles, and Java/resources trees.

This preserves a single registry and makes local Compose, production image, sample image, and CI use the same context contract.

7.8 Renovate safety state

renovate.json sets automerge: false for every update type during recovery. The current comment already states that automerge requires trustworthy CI, while the repository currently has no operational Actions unit.

Limited patch/pin/digest automerge can be reconsidered only after all of the following are observed:

  1. repository API reports has_actions: true;
  2. an authenticated runner inventory reports an online runner;
  3. protected branches require both blocking statuses;
  4. a real Renovate dependency pull request updates strict lock state and passes;
  5. a deliberately stale lockfile fails verifyDependencyLocks.

The configuration description must also stop claiming that the project has gradle/libs.versions.toml unless the project separately adopts a version catalog.

7.9 Documentation parity

Root and src/ README files must point to paths that exist in Git and commands that pass from a fresh checkout. The control-plane validator covers required physical paths, and the restored README-command and gate-matrix checks cover executable behavior.

The documentation must distinguish:

  • Gitea repository Actions enablement from workflow files;
  • unauthenticated runner API access from authorized runner health;
  • .github/workflows as canonical from .gitea/workflows as a shadow risk;
  • lock verification from lock regeneration;
  • read-only public-path verification from approved baseline update.

8. Phased Recovery

Phase 0 — Preserve evidence

Capture the current revision, clean status, missing-path evidence, Gitea version, repository Actions state, and runner authorization behavior. Hash the authoritative recovery source before copying it.

Phase 1 — Establish authority

The human chooses Mode A or Mode B. Mode A restores the hidden asset set byte-for-byte and verifies source equality. Mode B reconstructs from the two approved 2026-07-20 documents, records new provenance/hashes, and runs schema, renderer-parity, mutation, Gradle discovery, and architecture checks. The executor then writes a controller-approved overlay to the recorded recovery evidence path, invokes the recovered resolver with that file, persists the resolved packet plus packet/rule checksums, and proves deterministic re-resolution. Task 3 cannot start until those exact artifacts verify.

Phase 2 — Establish fail-fast local gates

Add the physical control-plane manifest and validator. Restore missing security contracts and make the public-path baseline fail closed. Run harness checks before Gradle.

Phase 3 — Repair build paths

Switch Docker to the repository-root context, add the root ignore contract, and verify production and sample images.

Phase 4 — Activate Gitea

Enable repository Actions, register the isolated runner, confirm workflow directory configuration, and run the preflight workflow. Do not configure required statuses until job names are stable and a successful run exists.

Phase 5 — Enforce merge controls

Enable the quality release-gate and vulnerability status as protected-branch requirements. Seed negative exercises for a missing required path, public-path drift, forbidden module edge, stale lockfile, and failed Docker build.

Phase 6 — Reassess automation

Run a real Renovate dependency update with automerge disabled. Re-enable limited automerge only by a separate human decision backed by the acceptance evidence.

9. Verification Strategy

  1. Mode A hashes match the authoritative export, or Mode B records the human decision, new hashes, and controlled-reconstruction provenance.
  2. Harness unit, mutation, schema, renderer, and parity tests pass under the selected mode.
  3. Control-plane validator passes on the complete tree and fails on each seeded missing/shadow mutation.
  4. Task-packet resolver emits a stable high-risk CI/deployment packet from the recorded overlay; the packet and governing-rule checksums verify again at the Task 3 boundary.
  5. Gradle wrapper, project discovery, architecture dependency verification, focused ArchUnit, dependency locks, test, and check pass.
  6. Public-path verification passes with the committed baseline and fails when it is absent or changed.
  7. Production and sample Docker images build from repository-root context.
  8. Gitea reports repository Actions enabled and at least one authorized runner online.
  9. The preflight and quality fan-in statuses appear on a real pull request.
  10. Protected branches reject seeded failures.
  11. Renovate config validation and a real dependency update pass without automerge.

10. Risks and Countermeasures

Risk Countermeasure
Reconstructed policy differs from the lost authority Require the human Mode B decision, label provenance as reconstruction, assign new hashes, and require schema/parity/mutation/Gradle evidence
.gitea/workflows silently shadows canonical workflows Block the directory in the physical preflight and confirm instance WORKFLOW_DIRS
Runner can expose host Docker authority Use a repository-scoped isolated runner, restrict fork execution, and keep registration credentials outside jobs
Workflow exists but repository Actions remains disabled Require API has_actions: true and a real run before branch-protection setup
Required status name changes and bypasses protection Keep stable job names in the gate matrix and verify protection after workflow changes
Public-path baseline is regenerated in CI Separate update and verify tasks; verification fails when the committed file is missing
Docker root context sends secrets Root .dockerignore excludes environment/secret paths and CI checks the context contract
Renovate updates declarations without strict locks Run verifyDependencyLocks before tests and keep automerge disabled through a real update exercise
Restored workflows assume GitHub-only behavior Exercise every event, context, action, and fan-in on Gitea 1.27.0 before making the status required

11. Acceptance Criteria

  • Mode A has a byte-identical authoritative inventory, or Mode B has a human-recorded reconstruction decision, new provenance, preliminary hashes, and a post-change complete hash inventory.
  • The 2026-07-20 harness registry, validators, generated agents, mutation suite, and parity checks pass under the selected mode.
  • A stable task packet is resolved after recovery; no implementation-complete claim relies on the pre-recovery state. Its overlay, output packet, packet checksum, and rule checksums are retained in the recovery evidence directory.
  • Fresh checkout control-plane preflight reports no missing required path.
  • .gitea/workflows is absent and .github/workflows is recognized by the Gitea instance.
  • Repository API reports has_actions: true.
  • An authorized runner inventory reports an enabled online runner with the workflow label.
  • Gradle projects, dependency locks, architecture gates, focused tests, test, and check pass.
  • Missing or changed public-path baseline fails read-only verification.
  • Production and sample images build from repository-root context without a duplicated registry.
  • The quality release-gate and vulnerability status are required on the protected branch.
  • Release-blocking preflights run the full harness unit/mutation, module validation, renderer/parity, gate-matrix, and structured Trivy checks; Trivy explicitly consumes .trivyignore.yaml.
  • Renovate automerge remains disabled until the explicit five-part re-enable condition is met.
  • README, gate matrix, workflow jobs, and physical repository paths agree.
  • No agent stages, commits, amends, or pushes the recovery.

12. External Authorities