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.pyand 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
- 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.
- Restore the 19-leaf module registry and the harness behavior approved in the 2026-07-20 design.
- Make a fresh checkout fail early with a precise control-plane error before Gradle configuration.
- Enable repository Actions on Gitea 1.27.0 and provide an isolated, repository-scoped runner.
- Keep
.github/workflowsas the canonical workflow directory while preventing an accidental.gitea/workflowsshadow. - Restore a single release-blocking fan-in status and the complementary vulnerability status.
- Make public-path, Trivy suppression, dependency-lock, and architecture gates fail closed.
- Make Docker builds consume the same root registry without duplicating the registry under
src/. - Pause Renovate automerge until required CI statuses and lockfile refresh behavior are proven.
- 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, andverify-gate-matrix.sh- structured Trivy validation through
verifyTrivyignore - Gradle wrapper launch and Java 21 assertion
verifyDependencyLocksverifyCleanArchitectureDependencies- focused ArchUnit coverage
verifyPublicPathSnapshottestcheck- 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:
.harness/project/modules.yamlto/build/.harness/project/modules.yaml;- wrapper, build descriptors, and lockfiles to
/build/src; - 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:
- repository API reports
has_actions: true; - an authenticated runner inventory reports an online runner;
- protected branches require both blocking statuses;
- a real Renovate dependency pull request updates strict lock state and passes;
- 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/workflowsas canonical from.gitea/workflowsas 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
- Mode A hashes match the authoritative export, or Mode B records the human decision, new hashes,
and
controlled-reconstructionprovenance. - Harness unit, mutation, schema, renderer, and parity tests pass under the selected mode.
- Control-plane validator passes on the complete tree and fails on each seeded missing/shadow mutation.
- 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.
- Gradle wrapper, project discovery, architecture dependency verification, focused ArchUnit,
dependency locks,
test, andcheckpass. - Public-path verification passes with the committed baseline and fails when it is absent or changed.
- Production and sample Docker images build from repository-root context.
- Gitea reports repository Actions enabled and at least one authorized runner online.
- The preflight and quality fan-in statuses appear on a real pull request.
- Protected branches reject seeded failures.
- 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/workflowsis absent and.github/workflowsis 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, andcheckpass. - 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-gateand 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
- Gitea Actions quick start: repository Actions
enablement, runner requirement, and the
.gitea/workflowsquick-start location. - Gitea configuration cheat sheet:
[actions].ENABLEDand the defaultWORKFLOW_DIRS=.gitea/workflows,.github/workflowsfirst-existing-directory behavior. - Gitea runner documentation: repository-scoped registration, runner modes, credential handling, and Docker isolation trade-offs.