Files
orca/.ciagent/PLAN_v0.8.md
T
Jon Chery 97a10353da docs(P00): grill v0.8 plan — PROCEED-WITH-CONDITION (4 binding fixes applied)
GRILL_v0.8.md (30KB): 9-axis adversarial review, overall verdict
PROCEED-WITH-CONDITION (confidence 0.78). 7 PROCEED + 4
PROCEED-WITH-CONDITION + 0 REPLAN findings.

4 binding plan changes applied to PLAN_v0.8.md:
  #1 T02.6 relabeled as v0.6 ship-defect bugfix (not v0.8 feature);
     P04 audit must record ship-defect closure
  #2 T02.9 doctor proxmox parity — P02 not complete until both
     bootstrap + doctor callbacks use capture-fix wrapper
  #3 T01.6 cli coverage escape valve — ship at 65% if 70% not reached
     after Wave 2 (RESEARCH §1.4 flags 55-65% realistic); do not block
     P02/P03 on the last 5%
  #4 T03.1 verify-reqs regex substring-tolerant (matches v0.2 header
     variant) + reverse-direction assertion (REQUIREMENTS Complete ↔
     ROADMAP COMPLETE); scope note: doc-vs-doc drift only

T02.10 case 7 added (known_hosts pre-populated v0.6→v0.8 migration
path). No escalations; all axes resolved at confidence ≥ 0.60.

---ci---
project: orca
phase: 0
milestone: v0.8
status: grill
---/ci---
2026-08-04 00:49:07 +00:00

34 KiB

Phase Plans: Orca v0.8 — Coverage & Trust Hardening

All 4 execution phases + final review with vertical-slice structure, wave ordering, persona assignment, and REQ-ID mapping. v0.8 scope: Coverage & Trust Hardening — round-2 test coverage uplift across 9 packages (tiered floor: ≥70% for 6 retested, ≥50% for 3 zero-test per D-047), SSH trust hardening (--host-key-fingerprint pre-pin + orca node key-reset + latent TOFU capture-fix + Result.HostKeyFingerprint population), and a requirements-hygiene gate (make verify-reqs).

Branching: phase/01-coverage-round2..phase/04-final-review-ship on the milestone/v0.8-coverage-trust-hardening branch (numbering restarts per milestone per branch-strategy.md).

Milestone type: NFR (P01 test, P02 chore on the trust surface per D-043, P03 chore, P04 docs/review). Tags run on the v0.7.x patch line: v0.7.0 (P0) … v0.7.4 (P04 = milestone release).

Vertical-slice integrity: each phase is independently shippable.

  • P01 ships tests-only (no production code changes except the proxmox sessionRunner seam, a backward-compatible interface extraction, and the engine peerDispatcher seam per RESEARCH §1.3).
  • P02 ships the SSH trust features + TOFI bugfix + Result population.
  • P03 ships the hygiene gate (Go program + Makefile + CI hook).
  • P04 is review + ship + audit (no new REQs).

Out of scope for v0.8 (candidate for v0.9, noted not added):

  • Lifting the 3 zero-test packages from 50% → 70% (D-047 explicitly toes-holds them; v0.9 can raise the floor).
  • A peerDispatcher interface seam in engine beyond what P01 needs for 70% coverage (httptest.NewTLSServer suffices; the seam is only added if coverage cannot otherwise hit 70%).
  • Pre-populating known_hosts from a remote keyscan API (TOFU + manual --host-key-fingerprint cover the v0.8 trust surface).
  • verify-reqs reverse-direction check (REQUIREMENTS Complete ↔ ROADMAP COMPLETE both ways) — forward direction (ROADMAP-shipped → REQUIREMENTS Complete) is the priority per the v0.7 drift that motivated REQ-060.

Carried-forward research findings (RESEARCH_v0.8.md, must incorporate):

  • §1.1 per-package coverage strategies + tiered floors (D-047).
  • §1.3 injected seams: reuse sshDialer (proxmox), LocalExecutor (engine), Dispatcher (transport), watchInterval (store), openTestDB/withFastWatch/initTestEnv/resetRootFlags/stubDispatcher helpers.
  • §1.4 realism flags: cli excludes daemon.go; cmd/orca 50% toe-hold only; proxmox needs the sessionRunner seam to hit 70%.
  • §2.1 latent TOFU capture bug (knownhosts.New returns KeyError{Want:[]} on first connect and does NOT auto-write — current BootstrapProxmox treats it as a dial failure).
  • §2.2 Result.HostKeyFingerprint is declared but never populated (always ""); P02 must add ssh.FingerprintSHA256 computation.
  • §2.3 --host-key-fingerprint plugs in at internal/cli/node.go (flag) + internal/proxmox/bootstrap.go (pinned callback).
  • §2.4 key-reset is local-known_hosts-only (D-046), atomic rewrite (AD-029).
  • §3 verify-reqs is a Go program at cmd/verify-reqs/main.go (~80 LOC, stdlib only, AD-030) + make verify-reqs + .coreci.yml validate hook.
  • §4 AD-025..AD-030 (renumbered AD-027..AD-030 in research for SSH/trust; AD-025/AD-026 from earlier milestones are stable).
  • §5 10 pitfalls carried into the risk register at the end of this file.

Dependencies (RESEARCH §6): v0.8 adds zero new direct dependencies. ssh.FingerprintSHA256, knownhosts.Line/Normalize/KeyError are in the existing golang.org/x/crypto v0.54.0 dep. verify-reqs is stdlib-only. go.mod is unchanged by v0.8.


Phase 1: Coverage Uplift Round 2 (REQ-057)

Branch: phase/01-coverage-round2 REQ Coverage: REQ-057 Tag: v0.7.1 Depends on: Phase 0 (this plan + clarify + research) Source research: RESEARCH_v0.8.md §1 (per-package strategies, helpers, seams)

Tiered floor (D-047)

Package Current Floor Owner persona
internal/engine 8.3% ≥ 70% backend-engineer
internal/proxmox 5.1% ≥ 70% backend-engineer
internal/cli 27.6% ≥ 70% (excluding daemon.go) lead-developer
internal/transport 26.3% ≥ 70% backend-engineer
internal/store 47.2% ≥ 70% data-engineer
internal/jobspec 47.6% ≥ 70% data-engineer
internal/audit 0% (no tests) ≥ 50% toe-hold data-engineer
internal/certpaths 0% (no tests) ≥ 50% toe-hold data-engineer
cmd/orca 0% (no tests) ≥ 50% toe-hold lead-developer

Wave 1 — Seams + foundational test helpers (no production logic changes)

These are backward-compatible interface extractions that unlock the bulk of coverage in Wave 2. They are the only production-code changes in P01; all other P01 tasks add _test.go files only.

Task ID Owner Wave Must Title Files touched Acceptance criterion
T01.1 backend-engineer 1 Y Add sessionRunner interface seam to proxmox internal/proxmox/bootstrap.go Extract a sessionRunner interface (CombinedOutput(cmd string) ([]byte, error)) ~10 LOC; default impl wraps *ssh.Client.NewSession().CombinedOutput(...); runRemote/deployPubKey/createLinuxUser/createPVERole/createPVEUser/assignPVEACL/writeSudoers/validateSudoers use the seam. Backward compatible: existing callers unchanged. go build ./internal/proxmox PASS. (RESEARCH §1.3 gap #1, §5 pitfall #3)
T01.2 backend-engineer 1 N Add peerDispatcher seam to engine (only if needed for 70%) internal/engine/dispatcher.go Extract a peerDispatcher interface (Submit(ctx, spec, key) (*SubmitResponse, error)) so dispatchToPeer is testable without httptest.NewTLSServer. Only add if T01.5 cannot otherwise hit 70% via httptest.NewTLSServer alone. If added, backward compatible. (RESEARCH §1.3 gap #2, §5 pitfall #8)

Wave 2 — Per-package coverage tests (build on Wave 1 seams)

Task ID Owner Wave Must Title Files touched Acceptance criterion
T01.3 backend-engineer 2 Y internal/transport tests → ≥ 70% internal/transport/mtls_test.go (NEW), internal/transport/dispatch_test.go (NEW), internal/transport/handshake_log_test.go (NEW), internal/transport/retry_test.go (NEW, extend) httptest.NewTLSServer with a test CA (reuse security.CAInit/GenerateCSR/SignCSR per RESEARCH §1.2) for mTLS handshake paths; stubDispatcher (daemon/dispatch_test.go:24) pattern for Dispatch RPC; capture slog via a test slog.Handler for handshake_log. go test -cover ./internal/transport → ≥ 70% (was 26.3%).
T01.4 backend-engineer 2 Y internal/engine tests → ≥ 70% internal/engine/executor_test.go (NEW), internal/engine/dispatcher_test.go (NEW), internal/engine/peer_test.go (NEW), internal/engine/scheduler_test.go (extend), internal/engine/registry_test.go (NEW, if registry exists) Executor.Start/Wait lifecycle (echo/false/ctx-cancel/Env propagation per REQ-021); Dispatcher.Submit with stubbed LocalExecutor + (if T01.2 added) stubbed peerDispatcher OR httptest.NewTLSServer; PeerRegistry in-memory Add/Remove/All/Get. Reuse openTestDB (node_repo_test.go:12). go test -cover ./internal/engine → ≥ 70% (was 8.3%).
T01.5 backend-engineer 2 Y internal/proxmox tests → ≥ 70% internal/proxmox/bootstrap_test.go (extend) Swap sshDialer (existing seam) for a fake returning a mock *ssh.Client; swap sessionRunner (T01.1 seam) for a fake that returns canned CombinedOutput bytes. Assert full bootstrap sequence calls the right shell commands in order; idempotent re-run ("already exists" → no-op); SSH auth failure → wrapped error; no password logged (D-031). go test -cover ./internal/proxmox → ≥ 70% (was 5.1%).
T01.6 lead-developer 2 Y internal/cli tests → ≥ 70% (excluding daemon.go) with GRILL condition #3 escape valve internal/cli/node_test.go (NEW), internal/cli/job_test.go (NEW), internal/cli/cert_test.go (NEW), internal/cli/doctor_test.go (NEW), internal/cli/audit_test.go (NEW), internal/cli/status_test.go (NEW), internal/cli/version_test.go (NEW), internal/cli/node_capacity_test.go (NEW) Table-driven rootCmd.Execute() against temp ORCA_HOME per subcommand (reuse initTestEnv/resetRootFlags/discardWriter per RESEARCH §1.2). Mock the proxmox path via sshDialer + sessionRunner seams. daemon.go is excluded — covered by internal/daemon/server_test.go. go test -cover ./internal/cli → ≥ 70% of non-daemon files (document the exclusion in a test-file comment). GRILL condition #3 escape valve: if 70% is not reached after Wave 2 effort and ≥ 65% is achieved (RESEARCH §1.4 flags 55-65% as realistic for one phase), ship cli at 65% and do NOT block P02/P03 on the last 5%; record the shortfall + rationale in the P01 verification commit.
T01.7 data-engineer 2 Y internal/store tests → ≥ 70% (incl. missing cert_repo_test.go) internal/store/cert_repo_test.go (NEW — v0.7 P01 leftover, RESEARCH §1.1), internal/store/node_repo_test.go (extend), internal/store/job_task_repo_test.go (extend), internal/store/audit_repo_test.go (extend), internal/store/capacity_repo_test.go (extend) cert_repo_test.go: Insert/Get/List/ListByNode/LatestForKind/PruneOlderThan/Delete + N=3 rotation history per REQ-025 + duplicate-serial error. Reuse openTestDB/withFastWatch (RESEARCH §1.2). go test -cover ./internal/store → ≥ 70% (was 47.2%).
T01.8 data-engineer 2 Y internal/jobspec tests → ≥ 70% internal/jobspec/spec_test.go (extend), internal/jobspec/testdata/*.hcl (NEW golden fixtures) Golden-file HCL fixtures (multi-task, env vars, args) + error-path table (missing job, no tasks, missing command, malformed HCL, empty file, nonexistent file for ParseFile). go test -cover ./internal/jobspec → ≥ 70% (was 47.6%).
T01.9 data-engineer 2 Y internal/audit first tests → ≥ 50% toe-hold internal/audit/audit_test.go (NEW) Construct Audit with real engine.Audit backed by :memory: sqlite (via store.NewAuditRepo + engine.NewAudit); assert rows in audit_log table; capture slog via a test slog.Handler for LogHandshakeOK/LogHandshakeFailed. go test -cover ./internal/audit → ≥ 50% (was 0%).
T01.10 data-engineer 2 Y internal/certpaths first tests → ≥ 50% toe-hold internal/certpaths/certpaths_test.go (NEW) Temp dir + t.Setenv("ORCA_HOME", dir) + assert each *Path() returns filepath.Join(dir, <file>); test ORCA_DB override; test default-to-~/.orca fallback. Model on namespace_test.go (cli). go test -cover ./internal/certpaths → ≥ 50% (was 0%).
T01.11 lead-developer 2 Y cmd/orca smoke test → ≥ 50% toe-hold cmd/orca/main_test.go (NEW), possibly cmd/orca/main.go (refactor main() into run() int for testability) Refactor main() to run() int (returns exit code; main() calls os.Exit(run())) so the test can call run() directly with a forced error path and assert non-zero exit + stderr contains "error:". Low-effort toe-hold — do NOT over-invest (RESEARCH §1.1, §5 pitfall #6). go test -cover ./cmd/orca → ≥ 50% (was 0%).

Wave 3 — Coverage gate verification

Task ID Owner Wave Must Title Files touched Acceptance criterion
T01.12 lead-developer 3 Y Coverage-gate verification (all 9 packages hit tiered floor) none (verification only) go test -cover ./internal/engine ./internal/proxmox ./internal/cli ./internal/transport ./internal/store ./internal/jobspec → each ≥ 70%; go test -cover ./internal/audit ./internal/certpaths ./cmd/orca → each ≥ 50%. go test -race ./... PASS. Any races fixed in-phase (not deferred).

Phase 1 Must-Haves (summary)

All 9 packages hit their tiered floor (D-047): T01.1, T01.3, T01.4, T01.5, T01.6, T01.7, T01.8, T01.9, T01.10, T01.11, T01.12. T01.2 is conditional (only if needed for engine 70%).

Phase 1 Verification

  • go build ./... PASS
  • go vet ./... PASS
  • go test -race ./... PASS
  • Per-package coverage hits the tiered floor (T01.12)
  • The proxmox sessionRunner seam is backward compatible (existing BootstrapProxmox callers unchanged)
  • No new direct deps (go.mod unchanged)

Phase 2: SSH Trust Hardening (REQ-058, REQ-059)

Branch: phase/02-ssh-trust-hardening REQ Coverage: REQ-058, REQ-059 Tag: v0.7.2 Depends on: Phase 1 (proxmox sessionRunner seam from T01.1 is in place; the trust-surface code is now testable) Source research: RESEARCH_v0.8.md §2 (TOFU bug, fingerprint computation, flag wiring, key-reset atomic rewrite) + §4 AD-027..AD-029 Phase type: chore (trust-surface hardening per D-043 — refines existing orca node join --type proxmox flow + existing TOFU known_hosts store; no new orchestration capability)

Wave 1 — Trust-surface foundations (security helpers + flag declarations)

Task ID Owner Wave Must Title Files touched Acceptance criterion
T02.1 backend-engineer 1 Y Add security.SSHFingerprintSHA256 helper (AD-027) internal/security/sshkey.go (extend) OR internal/security/fingerprint.go (extend) Thin wrapper over ssh.FingerprintSHA256(pubKey ssh.PublicKey) string returning the canonical SHA256:base64 string. Do NOT reuse security.Fingerprint (X.509 hex — different domain per RESEARCH §2.2). Unit test: known Ed25519 pub key → known SHA256: string.
T02.2 backend-engineer 1 Y Export security.WriteAtomic (AD-029 enabler) internal/security/ca.go Rename writeAtomicWriteAtomic (export) + update existing in-package callers. The key-reset atomic known_hosts rewrite (T02.7) needs it. Alternatively copy the ~20-LOC pattern into proxmox if export is undesirable — recommend export (RESEARCH §5 pitfall #10). go build ./internal/security PASS.
T02.3 backend-engineer 1 Y Add --host-key-fingerprint flag on orca node join (D-044) internal/cli/node.go nodeJoinCmd.Flags().StringVar(&joinHostKeyFP, "host-key-fingerprint", "", "SSH host key SHA256:base64 fingerprint (pre-pin; supersedes TOFU for --type proxmox)") in the flag-registration block (node.go:344-354). Add joinHostKeyFP string to the var block (node.go:47-60). Validation in RunE: if joinHostKeyFP != "" and --type != proxmox, emit a clear error ("--host-key-fingerprint requires --type proxmox today"). Flag is generic for future SSH-joined kinds (D-044).
T02.4 backend-engineer 1 Y Add HostKeyFingerprint field to proxmox.Options internal/proxmox/bootstrap.go Add HostKeyFingerprint string to the Options struct (bootstrap.go:55). Pass-through from internal/cli/node.go joinProxmox (node.go:158-166): HostKeyFingerprint: joinHostKeyFP.

Wave 2 — Trust features + bugfix (build on Wave 1)

Task ID Owner Wave Must Title Files touched Acceptance criterion
T02.5 backend-engineer 2 Y Implement pinnedHostKeyCallback (REQ-058, AD-028) internal/proxmox/bootstrap.go pinnedHostKeyCallback(expectedSHA256Base64 string) (ssh.HostKeyCallback, error): validate SHA256: prefix up front (reject raw hex with a clear error per D-045); callback receives server's ssh.PublicKey, computes ssh.FingerprintSHA256(key) (via T02.1 helper or inline), compares full strings to the operator-supplied value; returns nil on match, error on mismatch (fail closed). In BootstrapProxmox: if opts.HostKeyFingerprint != "" use pinnedHostKeyCallback, else fall back to the TOFU callback (T02.6). Unit test: match → callback returns nil; mismatch → returns error mentioning REQ-058; non-SHA256:-prefixed input → constructor returns error.
T02.6 backend-engineer 2 Y BUGFIX (v0.6 ship-defect): FIX the latent TOFU capture bug (RESEARCH §2.1, §5 pitfall #1, GRILL condition #1) internal/proxmox/bootstrap.go Wrap knownhosts.New(...) with a custom callback that: on *knownhosts.KeyError{Want: []} (host unknown) captures the server-presented ssh.PublicKey, writes a line via knownhosts.Line([]string{knownhosts.Normalize(addr)}, key) to certpaths.KnownHostsPath() using security.WriteAtomic (T02.2, AD-029), and returns nil (allow the dial to proceed). On *knownhosts.KeyError{Want: [knownKey]} (mismatch) returns the error (MITM detection). On nil (host present + match) returns nil. This fixes the v0.6 latent ship-defect where first-connect Proxmox join always failed (verified against golang.org/x/crypto@v0.54.0/ssh/knownhosts/knownhosts.go:370-385). P04 audit must record this as ship-defect closure. Unit test: first-connect captures the key + writes known_hosts; second-connect matches; mismatch-connect fails.
T02.7 backend-engineer 2 Y Populate Result.HostKeyFingerprint (RESEARCH §2.2, §5 pitfall #2) internal/proxmox/bootstrap.go In the capture path (T02.6) and the pinned path (T02.5), set Result.HostKeyFingerprint = ssh.FingerprintSHA256(hostKey) (via T02.1). The field is currently declared (bootstrap.go:83-85) but always "". After T02.7, orca node join --type proxmox output includes the real fingerprint. Unit test: Result.HostKeyFingerprint is non-empty + SHA256:-prefixed after a successful bootstrap.
T02.8 backend-engineer 2 Y Implement orca node key-reset <node> (REQ-059, D-046, AD-029) internal/cli/node.go, internal/proxmox/bootstrap.go (new ResetHostKey helper OR inline in cli) New nodeKeyResetCmd (&cobra.Command{Use: "key-reset <node>", Args: cobra.ExactArgs(1), RunE: ...}) registered via nodeCmd.AddCommand(nodeKeyResetCmd) (node.go:358-360). RunE: (1) resolve <node> arg via nodeRegistry() (node.go:37) → get node row → use node.Name (the host address for proxmox nodes) as the known_hosts match key; (2) call proxmox.ResetHostKey(host) error which reads certpaths.KnownHostsPath(), filters lines whose host field (before first whitespace, normalized via knownhosts.Normalize) matches, rewrites via security.WriteAtomic (T02.2); (3) audit-log event=node.key_reset with actor+node+host via engine.Audit.Record; (4) print ✓ Host key reset for <node> (next connect will re-pin via TOFU or --host-key-fingerprint). Local only — do NOT revoke remote authorized_keys (D-046). Unit test: known_hosts with 2 entries for the target host + 1 for another host → after reset, target's 2 lines removed, other host's line intact; audit row inserted.
T02.9 backend-engineer 2 Y Apply the TOFU capture-fix to doctor proxmox probe (GRILL condition #2 — doctor parity with bootstrap) internal/doctor/doctor.go The doctor proxmox probe (doctor.go:412-415) uses the same knownhosts.New(...) callback pattern as bootstrap. Apply the same capture-fix wrapper (T02.6) so doctor proxmox on a first-connect node doesn't fail. P02 is not complete until both bootstrap (T02.6) and doctor (T02.9) callbacks use the capture-fix wrapper — GRILL condition #2 binding parity check. (If the doctor probe already relies on a prior node join having populated known_hosts, the fix is still correct — it makes the doctor robust to a missing entry.)

Wave 3 — End-to-end integration + verification

Task ID Owner Wave Must Title Files touched Acceptance criterion
T02.10 backend-engineer 3 Y End-to-end trust-surface integration tests internal/proxmox/bootstrap_test.go (extend), internal/cli/node_test.go (extend) (1) --host-key-fingerprint with a correct pin → bootstrap succeeds + Result.HostKeyFingerprint matches the pin; (2) --host-key-fingerprint with a wrong pin → bootstrap fails fast with the REQ-058 mismatch error; (3) no --host-key-fingerprint + first connect (empty known_hosts) → TOFU captures the key + writes known_hosts + bootstrap succeeds; (4) no flag + second connect (known_hosts has the key) → matches + succeeds; (5) no flag + mismatch (known_hosts has a different key) → fails with MITM error; (6) orca node key-reset <node> → known_hosts entry removed + audit row inserted + next connect re-pins; (7) known_hosts pre-populated (v0.6→v0.8 migration path: existing entry from a prior join) → second-connect matches without re-capture, covering the upgrade path.
T02.11 backend-engineer 3 Y --host-key-fingerprint non-proxmox type validation test internal/cli/node_test.go (extend) orca node join --type linux --host-key-fingerprint SHA256:... → clear error ("--host-key-fingerprint requires --type proxmox today"). Validates D-044 RunE check from T02.3.

Phase 2 Must-Haves (summary)

  • T02.1, T02.2, T02.3, T02.4 (Wave 1 foundations)
  • T02.5 (--host-key-fingerprint pinned callback — REQ-058)
  • T02.6 (TOFU capture-fix — latent bug)
  • T02.7 (Result.HostKeyFingerprint populated)
  • T02.8 (orca node key-reset — REQ-059)
  • T02.9 (doctor proxmox TOFU fix)
  • T02.10, T02.11 (integration + validation)

Phase 2 Verification

  • go build ./... PASS
  • go vet ./... PASS
  • go test -race ./internal/proxmox/... ./internal/cli/... ./internal/doctor/... ./internal/security/... PASS
  • ./bin/orca node join --help shows --host-key-fingerprint flag
  • ./bin/orca node key-reset --help shows the key-reset subcommand
  • Pinned mismatch → fail closed (T02.10 case 2)
  • TOFU first-connect → captures + succeeds (T02.10 case 3)
  • Result.HostKeyFingerprint is non-empty after bootstrap (T02.7)
  • key-reset removes only the target host's known_hosts lines + audit-logs (T02.8)
  • No new direct deps

Phase 3: Requirements-Hygiene Gate (REQ-060)

Branch: phase/03-verify-reqs REQ Coverage: REQ-060 Tag: v0.7.3 Depends on: Phase 2 (P03 is independent of P02 code, but ships after per ROADMAP ordering; the verify-reqs program parses the .ciagent/ markdown which is stable by P03) Source research: RESEARCH_v0.8.md §3 (Makefile, .coreci.yml, parsing approach, AD-030) + §4 AD-030

Wave 1 — Go program

Task ID Owner Wave Must Title Files touched Acceptance criterion
T03.1 lead-developer 1 Y cmd/verify-reqs/main.go — Go program (~80 LOC, stdlib only, AD-030, GRILL condition #4 regex + reverse direction) cmd/verify-reqs/main.go (NEW) Parses .ciagent/ROADMAP.md + .ciagent/REQUIREMENTS.md using regexp (stdlib). Forward assertion: for every REQ-ID in REQUIREMENTS.md whose Phase column references a milestone that ROADMAP marks COMPLETE (substring-match COMPLETE within the bold span — NOT exact \*\*COMPLETE\*\* which misses v0.2's **COMPLETE (merged to main via v0.3)** header at ROADMAP.md:23), the REQUIREMENTS Status must be Complete. Reverse assertion (GRILL condition #4): for every REQ-ID in REQUIREMENTS.md marked Complete, the corresponding milestone in ROADMAP.md must be marked COMPLETE. Regex: REQUIREMENTS row ^|\s*(REQ-\d+)\s*|.*?|\s*\*\*(Complete|Pending)\*\*\s*|; ROADMAP milestone-complete ^##\s*Milestone\s+v0\.\d+:.*—\s*\*\*COMPLETE[^\*]*\*\* (substring tolerant); map milestone → REQs via the REQUIREMENTS Phase column (e.g. v0.7 P1 → milestone v0.7). Exit 0 on consistency; exit 1 with a diff listing (REQ-ID + current status + expected status + direction) on drift. CLI: go run ./cmd/verify-reqs .ciagent/ROADMAP.md .ciagent/REQUIREMENTS.md (args optional; defaults to those paths). Scope note (GRILL): REQ-060 catches doc-vs-doc drift only; code-vs-doc drift (e.g. the REQ-053 cert_repo_test.go omission — verified missing) is out of scope for this gate and handled by P04 ciagent-audit.
T03.2 lead-developer 1 Y cmd/verify-reqs/main_test.go — golden-file tests cmd/verify-reqs/main_test.go (NEW), cmd/verify-reqs/testdata/ (NEW: roadmap_clean.md, requirements_clean.md, roadmap_drift.md, requirements_drift.md) (1) Clean pair (ROADMAP v0.X COMPLETE + REQUIREMENTS REQ-XXX Complete) → exit 0, no diff; (2) Drift pair (ROADMAP v0.X COMPLETE + REQUIREMENTS REQ-XXX Pending) → exit 1 + diff lists the stale REQ; (3) Multiple drifts → all reported; (4) Missing args → uses defaults; (5) Malformed markdown → clear error (not a silent pass).

Wave 2 — Makefile + CI hook

Task ID Owner Wave Must Title Files touched Acceptance criterion
T03.3 lead-developer 2 Y make verify-reqs target Makefile Add verify-reqs target: go run ./cmd/verify-reqs .ciagent/ROADMAP.md .ciagent/REQUIREMENTS.md. Add to .PHONY. make verify-reqs exits 0 on the current repo (REQUIREMENTS was corrected during v0.8 SPECIFY).
T03.4 lead-developer 2 Y .coreci.yml validate-pipeline hook .coreci.yml Add a verify-reqs step to the validate pipeline (after go-version, alongside gosec/govulncheck/gitleaks per RESEARCH §3.2): image: golang:1.25, commands: [make verify-reqs]. Pipeline fails on drift.

Wave 3 — Synthetic drift verification

Task ID Owner Wave Must Title Files touched Acceptance criterion
T03.5 lead-developer 3 Y Synthetic drift verification (REQ-060 acceptance) none (verification only; temporarily flip a REQUIREMENTS row to Pending in a scratch commit, run make verify-reqs, assert exit 1 + diff, then revert) (1) make verify-reqs on the current repo → exit 0; (2) flip one v0.7 REQ row to Pending in a scratch edit → make verify-reqs → exit 1 + diff lists that REQ-ID; (3) revert the scratch edit → exit 0. This is the REQ-060 acceptance criterion ("passes on current repo + fails on synthetic drift").

Phase 3 Must-Haves (summary)

T03.1, T03.2, T03.3, T03.4, T03.5 — all must complete for the hygiene gate to ship.

Phase 3 Verification

  • go build ./cmd/verify-reqs PASS
  • go test ./cmd/verify-reqs/... PASS (golden-file tests)
  • make verify-reqs → exit 0 on the current repo
  • Synthetic drift → make verify-reqs exit 1 + diff (T03.5)
  • .coreci.yml validate pipeline includes the verify-reqs step
  • No new direct deps (stdlib only)

Phase 4: Final Review + Ship + Audit (no new REQs)

Branch: phase/04-final-review-ship REQ Coverage: all (REQ-057..060) Tag: v0.7.4 (milestone release) Depends on: Phase 1 + Phase 2 + Phase 3 Source: milestone-release checklist (matches PLAN_v0.7 P05 structure)

Wave 1 — Review + audit

Task ID Owner Wave Must Title Files touched Acceptance criterion
T04.1 lead-developer 1 Y Multi-persona code review across all v0.8 phases none (review only) ciagent-review across P01..P03; P0 issues fixed in-phase; P1+ recorded in .ciagent/ for post-hoc.
T04.2 lead-developer 1 Y Audit: reconstruction test + branch hygiene + commit discipline none (audit only) ciagent-audit: git log matches .ciagent/ files; branch hygiene clean; commit discipline enforced.

Wave 2 — Ship

Task ID Owner Wave Must Title Files touched Acceptance criterion
T04.3 lead-developer 2 Y Merge phase/04 → milestone/v0.8-coverage-trust-hardening none Fast-forward merge (or rebase-then-fast-forward per config).
T04.4 lead-developer 2 Y Merge milestone/v0.8 → main none Rebase-then-fast-forward per config.
T04.5 lead-developer 2 Y Tag v0.7.4 (milestone release) none git tag v0.7.4 on the merged main HEAD. Per-phase tags v0.7.0..v0.7.4 all present.
T04.6 lead-developer 2 Y Create Gitea release v0.7.4 with milestone summary none Release notes cover all 4 phases + REQ-057..060 + coverage deltas + trust-surface additions.

Wave 3 — Post-ship bookkeeping

Task ID Owner Wave Must Title Files touched Acceptance criterion
T04.7 lead-developer 3 Y Update REQUIREMENTS.md — mark REQ-057..060 Complete .ciagent/REQUIREMENTS.md All 4 v0.8 REQ rows show **Complete** with phase + ship tag. make verify-reqs still passes (self-consistency).
T04.8 lead-developer 3 Y Update ROADMAP.md — mark v0.8 COMPLETE .ciagent/ROADMAP.md v0.8 milestone section shows **COMPLETE**; all phase checkboxes [x]. make verify-reqs still passes.
T04.9 lead-developer 3 Y Write + clear checkpoint .ciagent/ checkpoint {phase: 4, stage: "complete", phase_role: "final", milestone_complete: true}; then clear checkpoint (milestone complete; next run starts a new milestone).

Phase 4 Must-Haves (summary)

All tasks (T04.1..T04.9) are must-haves — the final-review phase has no optional work.

Phase 4 Verification

  • make build PASS
  • make test PASS
  • make lint PASS
  • make verify-reqs PASS
  • go vet ./... PASS
  • git log on main shows all v0.8 phase commits
  • git tag --list 'v0.7.*' shows v0.7.0..v0.7.4
  • REQUIREMENTS.md shows REQ-057..060 as Complete
  • ROADMAP.md shows v0.8 as COMPLETE
  • Gitea release v0.7.4 published with milestone summary

Phase 5: Final Review (next milestone, not part of v0.8 execution)

Per the v0.8 ROADMAP, there are 4 execution phases (P01..P04). P04 IS the final review + ship + audit phase. There is no separate P05 in v0.8 (unlike v0.7 which had P05). The orchestrator's next-milestone P0 begins after T04.9 clears the checkpoint.


Risk Register (carried forward from RESEARCH_v0.8.md §5)

# Pitfall Phase(s) affected Mitigation
1 TOFU capture is currently BROKEN: knownhosts.New returns KeyError{Want:[]} on first connect and does NOT auto-write; current BootstrapProxmox treats it as a dial failure. P02 T02.6 wraps the callback to capture-and-persist on KeyError{Want:[]} via knownhosts.Line + security.WriteAtomic. This is a v0.6 latent bug that P02 closes.
2 Result.HostKeyFingerprint is declared but never populated (always ""). D-045's rationale references "existing output" that doesn't exist. P02 T02.7 adds ssh.FingerprintSHA256(hostKey) computation in both the capture and pinned paths. 1-line addition once the host key is available.
3 No sessionRunner seam in proxmox — testing the SSH command sequence without a real SSH server is impossible. P01 T01.1 adds a 1-interface ~10-LOC sessionRunner seam in Wave 1. Unlocks ~40% of proxmox coverage. Backward compatible.
4 internal/store/cert_repo.go has NO test — v0.7 P01 REQ-053 was supposed to add cert_repo_test.go but it's missing (v0.7 leftover). P01 T01.7 adds cert_repo_test.go (Insert/Get/List/ListByNode/LatestForKind/PruneOlderThan/Delete + N=3 rotation). Directly lifts store coverage toward 70%.
5 internal/cli/daemon.go starts a long-running mTLS server — testing it in cli requires a lifecycle harness; it's already covered by internal/daemon/server_test.go. P01 T01.6 excludes daemon.go from the cli 70% target; documents the exclusion in a test-file comment. Avoids double-testing.
6 cmd/orca 50% toe-hold is low-value (15 LOC of glue; effort:coverage ratio is poor). P01 T01.11 keeps it at the 50% toe-hold per D-047; does NOT over-invest. A small run() int refactor enables a smoke test.
7 go: no such tool "covdata" for zero-test packages — a Go toolchain quirk when a package has no test files; NOT a real 0% number. P01 T01.9, T01.10, T01.11 each add a _test.go file, which makes coverage computable. Don't treat the tooling error as a measurement.
8 transport.dispatchToPeer has no seam — testing the remote-dispatch branch requires a new interface OR httptest.NewTLSServer. P01 T01.3 uses httptest.NewTLSServer (no refactor needed). T01.2 (conditional peerDispatcher seam) is only added if engine cannot otherwise hit 70%.
9 knownhosts.Line + knownhosts.Normalize are the helpers for the TOFU-capture fix and key-reset matching. P02 T02.6 + T02.8 use Normalize to match host strings consistently (handles host:22 vs host).
10 security.writeAtomic is unexported (ca.go:305); key-reset's atomic known_hosts rewrite needs it. P02 T02.2 exports WriteAtomic (recommended) OR copies the ~20-LOC pattern. Export is preferred — it's already used across ca.go + sshkey.go.

REQ-ID → Task mapping (traceability)

REQ-ID Phase Tasks
REQ-057 P01 T01.1, T01.2 (conditional), T01.3, T01.4, T01.5, T01.6, T01.7, T01.8, T01.9, T01.10, T01.11, T01.12
REQ-058 P02 T02.1, T02.3, T02.4, T02.5, T02.7, T02.10, T02.11
REQ-059 P02 T02.2, T02.8, T02.10
REQ-060 P03 T03.1, T03.2, T03.3, T03.4, T03.5
(latent TOFU bug) P02 T02.6, T02.9 (not a REQ — closes a v0.6 gap surfaced by RESEARCH §2.1)
(milestone release) P04 T04.1..T04.9

Task counts

Phase Tasks Must-haves Waves
P01 12 11 (T01.2 conditional) 3
P02 11 11 3
P03 5 5 3
P04 9 9 3
Total 37 36