# PLAN — v1.28 CLI Canonicalization + Identity Layer

> **Milestone:** v1.28 (feature — CLI substrate + Nova-idp identity
> layer). Tags on the **v1.27.x** line: `v1.27.0` (P0) →
> `v1.27.1..v1.27.6` (P1..P6) → `v1.27.7` (P7 final = milestone
> release). The final phase's patch IS the milestone release.
> **Branch:** `milestone/v1.28-cli-identity`. Phase branches:
> `phase/00-pre-execution`, `phase/01-cli-substrate`,
> `phase/02-lambda-packaging`, `phase/03-idp-auth`,
> `phase/04-token-vend-pat`, `phase/05-docs-integration`,
> `phase/06-final-review-ship`.
>
> **Tags:** `v1.27.0` (P0) → `v1.27.1..v1.27.5` (P1..P5) →
> `v1.27.6` (P6 final = milestone release). 6 execution phases
> (P5 idp-setup folded into P4 Wave 8 per grill C-2.1).

## Milestone goal

The Nova CLI is installable from internal PyPI (CodeArtifact); every
`core/` module is reachable as a `nova <subcommand>`; the CLI and
Lambda functions share a single `core/` source tree; and Nova owns its
identity layer end-to-end (Nova-idp: `nova-idp-auth` +
`nova-idp-token-vend` Lambdas, KMS-signed OIDC tokens, kyverno-json
ABAC token vending, PAT lifecycle). No AWS-managed identity services
in the path (INV-15).

## Requirements

31 requirements: REQ-323..REQ-353 (full text in
`.ciagent/REQUIREMENTS.md` §v1.28). 6 capabilities: CAP-033..CAP-038.
6 invariants: INV-12..INV-17. 6 decisions: D-226..D-231 (CLARIFY) +
RESEARCH amendments (D-228 fail-closed, D-229 strong-read-on-PK).

## Phase breakdown

### Phase P1 — cli-substrate (REQ-323..REQ-328)

**Goal:** CodeArtifact wheel + Lambda layer pipeline; `nova/` CLI
package with a subcommand per `core/` module; `nova init`; `nova
cli-action` composite action; `core/mode_resolver.py`; audit emission
with `mode` + `selection_reason`. The CLI is installable and every
`core/` module is reachable.

**Exit criterion:** CAP-033 + CAP-034 + CAP-035 Verified + all REQ-323..328
tests pass. CodeArtifact provisioned (Wave 0 gate).

#### Wave 0 — CodeArtifact provisioning (backend-engineer) [C-3.2/C-8.1]
- **Task 0.1** (backend-engineer): provision CodeArtifact domain
  (`nova`) + repository (`nova-pypi`) in `581513795199`. Verify
  `codeartifact:*` IAM grant on `nova-spike-runner`. **Binary go/no-go
  gate for Wave 4.** If fail: activate Gitea wheel index fallback
  (CLARIFY assumption #1) and document in PLAN.md.

#### Wave 1 — pyproject + entry point (cli-engineer)
- **Task 1.1** (cli-engineer): `pyproject.toml` — add
  `[project.scripts] nova = "nova.cli:main"`; add
  `[tool.setuptools.packages.find]` including `nova`, `nova.*`, `core`,
  `core.*`, `adapters.*`; bump `requires-python` to `>=3.12`; add
  `argon2-cffi`, `cryptography`, `pyjwt`, `hypothesis` to deps/test-deps.
  Verify `pip install -e .` produces a `nova` executable.

#### Wave 2 — CLI dispatch + subcommands (cli-engineer)
- **Task 2.1** (cli-engineer): `nova/__init__.py` + `nova/cli.py`
  (~80 lines, auto-discovers `nova/<module>.py` via `pkgutil.iter_modules`,
  dispatches, emits `cli.invocation` audit event stub with INV-12 fields).
- **Task 2.2** (cli-engineer): `nova/<module>.py` for each `core/`
  module (≤50 lines, `add_parser` + `run` delegates to `core/`). Cover:
  `resolve`, `decommission`, `env-transition`, `env-check`, `hitl`,
  `onboard`, `outbox`, `publish-outputs`, `policy`, `regression`, `sod`,
  `readiness`, `attestation-matrix`, `confidence`. Skip internal-only
  (`env`, `local_emulators`, `output_publisher` if not user-facing).
- **Task 2.3** (cli-engineer): `nova/init.py` (REQ-325) — scaffolds
  `.nova/`, `.nova/contract.yml.attestations/`, `.gitignore` (excludes
  secrets, `~/.nova/credentials.json`).

#### Wave 3 — mode_resolver + audit (cli-engineer)
- **Task 3.1** (cli-engineer): `core/mode_resolver.py` —
  `resolve_mode(flag, env_var, credential_type, stdin_isatty)` per D-226.
  `sys.stdin.isatty()` is the TTY check (RESEARCH §11). Invalid env →
  warn + fall through. Returns `(mode, selection_reason)`.
- **Task 3.2** (cli-engineer): wire `mode_resolver` into `nova/cli.py`
  — resolve mode before dispatch, emit `cli.invocation` with `mode`,
  `selection_reason`, `credential_type`, `command`, `args` (INV-12,
  REQ-328).
- **Task 3.3** (cli-engineer): `tests/test_mode_resolver.py` —
  `hypothesis` property tests (REQ-349): deterministic, flag-wins,
  invalid-env-ignored, no-silent-fallback. Edge cases: TTY + piped
  stdout, missing credential, conflicting flag/env, invalid env value.

#### Wave 4 — CodeArtifact + layer pipeline (backend-engineer)
- **Task 4.1** (backend-engineer): `.gitea/workflows/publish.yml` +
  `.github/workflows/publish.yml` (byte-identical) — build wheel →
  CodeArtifact `twine upload` → build layer (`pip install --target
  layer/python/` + `argon2-cffi` + `cryptography` + `pyjwt`) →
  `lambda publish-layer-version` → SSM `/nova/layer/nova-cli/version`
  mapping (CAP-035). Fail either → job fails (merge blocked, REQ-323).
  Pin version to `<semver>+<sha7>` for idempotent re-runs.

#### Wave 5 — composite action (cli-engineer + backend-engineer)
- **Task 5.1** (cli-engineer): `.github/actions/nova-cli/action.yml` —
  composite action, `setup-python@v5` (3.12), CodeArtifact login +
  `pip install nova`, `nova ${{ inputs.command }}`. `NOVA_CLIENT_MODE`
  from input.
- **Task 5.2** (backend-engineer): byte-identical integration test —
  CI matrix runs the action on GitHub `ubuntu-latest` + Gitea
  `act_runner`; assert same stdout/exit code (REQ-326 AC2, NFR-11).

#### Wave 6 — CAP-033/034 gate (cli-engineer)
- **Task 6.1** (cli-engineer): `tests/test_cli_subcommands.py` —
  CAP-033 (`nova --help` lists a subcommand for every `core/` module)
  + CAP-034 (AST scan: ≤50 lines, ≤3 defs, all calls resolve to `core.`,
  no conditionals beyond `if __name__`). Wire into CI merge gate.

### Phase P2 — lambda-packaging (REQ-329, REQ-330, REQ-331)

**Goal:** Dual-use `core/lambda/contract_ingestor.py` (Lambda + CLI
paths share ≥80% code); `core/env.py:+synthesize_local_env()` for
`nova apply --local`; `.nova/contract.yml.attestations/` scaffolded;
JWS-from-PAT key derivation (C-5.2).

**Exit criterion:** all REQ-329..331 tests pass + JWS-from-PAT KDF
specified.

#### Wave 1 — dual-use refactor (backend-engineer)
- **Task 1.1** (backend-engineer): refactor
  `core/lambda/contract_ingestor.py` — extract the shared logic into
  importable functions; the Lambda handler + the CLI `__main__` block
  both call them. The `__main__` block already exists (the dual-use
  precedent per RESEARCH §1.2). Verify ≥80% code share (CAP-034 / code
  review). Local path via `core/local_emulators.py:LocalLambdaStub`.

#### Wave 2 — local env synthesizer + JWS KDF (backend-engineer)
- **Task 2.1** (backend-engineer): `core/env.py:+synthesize_local_env()
  ` — produces a local env dict (account_id placeholder, region local,
  no real AWS) from a contract + `--local` flag. Mirrors
  `core/onboarding.py:generate_env_file()`.
- **Task 2.2** (cli-engineer): `nova/apply.py` (≤50 lines) — `nova
  apply --local` delegates to `core.env.synthesize_local_env()` +
  `core.contract_resolver.resolve()`.
- **Task 2.3** (security-engineer): JWS-from-PAT key derivation
  (C-5.2). HKDF-SHA256(PAT_bytes, salt='nova-local-attestation',
  info='jws-signing-key') → 32-byte symmetric key. The JWS is
  HMAC-SHA256 (symmetric, not asymmetric). The "public key derivable
  from the PAT" AC (REQ-332) is re-interpreted: the *verification key*
  is derived from the PAT via the same KDF (the PAT is the shared
  secret). Document in `docs/developer-guide-auth.md`. Update REQ-332
  AC accordingly.

#### Wave 3 — attestations dir (cli-engineer)
- **Task 3.1** (cli-engineer): verify `nova init` (P1 Wave 2 Task 2.3)
  creates `.nova/contract.yml.attestations/` (empty). REQ-331 test.

### Phase P3 — idp-auth (REQ-333, REQ-334, REQ-335)

**Goal:** `nova-idp-auth` Lambda (sign-up, sign-in, session) with
Argon2id hashing + DynamoDB tables. CAP-036 target.

**Exit criterion:** CAP-036 Verified (E2E sign-up → sign-in → session
passes in CI).

#### Wave 1 — DynamoDB schema (backend-engineer)
- **Task 1.1** (backend-engineer): define the 4 DynamoDB table schemas
  (`nova-users`, `nova-sessions`, `nova-password-resets`, `nova-pats`)
  in a CloudFormation snippet (reused by P4 Wave 8 `nova idp setup`).
  PITR enabled on each (REQ-335).

#### Wave 2 — Argon2id (security-engineer) [C-1.2/C-7.2]
- **Task 2.1** (security-engineer): `core/lambda/nova_idp_auth.py` —
  Argon2id password hashing via `argon2-cffi` (D-228: bundled abi3
  wheel; **fail-closed on `ImportError` → 503, no pure-Python
  fallback**). **Parameters: t=3, m=65536 KiB, p=1** (OWASP-recommended
  minimum). Lambda memory ≥512 MB (m=64 MiB + Python overhead fits).
  Raw passwords never in logs/traces/env/DDB (INV-16, REQ-334).
- **Task 2.2** (security-engineer): `tests/test_argon2_fail_closed.py`
  — mock `argon2.low_level` import failure → assert auth Lambda
  returns 503 (not a crash, not a weak hash). C-1.2.

#### Wave 3 — auth Lambda (backend-engineer + security-engineer)
- **Task 3.1** (backend-engineer): `nova-idp-auth` Lambda handler —
  sign-up, sign-in, session creation endpoints. Function URL + IAM
  auth. DynamoDB via lazy `boto3.resource` (the existing pattern).
- **Task 3.2** (security-engineer): session token issuance + session
  storage in `nova-sessions` (TTL `expires_at`). Password reset flow
  in `nova-password-resets` (TTL 15m).

#### Wave 4 — CAP-036 E2E (backend-engineer)
- **Task 4.1** (backend-engineer): `tests/test_idp_auth.py` — sign-up
  → sign-in → session round-trip (moto[dynamodb] for local; deployed
  for CI). CAP-036 verification.

### Phase P4 — token-vend-pat (REQ-336..REQ-344, REQ-340, REQ-341) [was P4+P5]

**Goal:** `nova-idp-token-vend` Lambda (KMS-signed OIDC, kyverno-json
ABAC), JWKS endpoint, PAT lifecycle, `nova auth` commands, **and**
`nova idp setup` (folded from P5 per C-2.1). CAP-037 + CAP-038 target.
**Highest-risk phase — critical-path.** The kj-binary spike (Wave 1)
is the single highest-probability schedule slip; Fargate fallback adds
~1 week (D-227). This is a **double-length phase** (8 waves).

**Exit criterion:** CAP-037 + CAP-038 Verified + `nova idp setup
--check/--apply/--verify` works against a fresh AWS account.

#### Wave 1 — kj-binary spike (backend-engineer + security-engineer) [C-8.2]
- **Task 1.1** (backend-engineer): confirm the `kj` Go binary
  (~40 MB Linux amd64) runs in the Lambda Python 3.12 runtime on
  AL2023. Bundle it in the `nova-cli` layer (`wget` a **pinned
  release** (e.g. `kj@v1.x.y`) + record SHA256 into `layer/kj.sha256`
  — C-8.2, supply-chain safety) into `layer/bin/kj`, `chmod +x`.
  Verify `KyvernoJsonEngine.is_configured()` finds `/opt/bin/kj`.
  **If this fails:** fall back to Fargate for the token-vend Lambda
  (D-227 risk, RESEARCH §7). Escalate to user only if both fail (full
  autonomy: log assumption + proceed with Fargate).

#### Wave 2 — ABAC policy (security-engineer) [C-5.1]
- **Task 2.1** (security-engineer): `platform/abac/token-vend.policy`
  — kyverno-json `ValidatingPolicy` (D-227). Payload:
  `{subject, requested_claims, target_resource, environment, pat_jti,
  policy_version}`. **`requested_claims` = list of claim names** (the
  policy asserts the subject is *allowed* to request those claims; the
  values are assigned by the Lambda, not the requestor — C-5.1).
  JMESPath checks for role/scope/env/owner. Severity `critical` = deny
  on fail.
- **Task 2.2** (security-engineer): `policy_version` = git SHA of the
  policy file, baked into the Lambda layer (D-231). Recorded in every
  `token.vend.allowed/denied` audit event.

#### Wave 3 — KMS signing (security-engineer) [C-1.1]
- **Task 3.1** (security-engineer): **verify KMS asymmetric key
  support** before implementation: `aws kms create-key --key-spec
  ECC_NIST_P256 --key-usage SIGN_VERIFY` in the target account
  (C-1.1). If fail: fall back to RSA-2048 (also supported, larger
  tokens) or escalate. Do not discover this mid-Wave.
- **Task 3.2** (security-engineer): KMS key `alias/nova-oidc-signing`
  (ECC_NIST_P256, SIGN_VERIFY). Token-vend Lambda signs via
  `kms.sign(SigningAlgorithm="ECDSA_SHA_256")` → DER→raw ECDSA
  conversion (`decode_dss_signature` → `r.to_bytes(32) + s.to_bytes(32)`,
  RESEARCH §5). JWT header `{"alg":"ES256","typ":"JWT","kid":"..."}`.

#### Wave 4 — token-vend Lambda (backend-engineer + security-engineer) [C-6.1/C-7.1 ABAC FAIL-CLOSED]
- **Task 4.1** (backend-engineer): `core/lambda/nova_idp_token_vend.py`
  — accepts PAT/session, validates revocation (`nova-pats.GetItem(jti,
  ConsistentRead=True)` — D-229), evaluates ABAC (Wave 2), signs (Wave
  3), returns OIDC JWT. Audit at every step.
- **Task 4.2** (security-engineer): token claims `sub, aud, iss, exp,
  iat, jti, roles` (REQ-336).
- **Task 4.3** (security-engineer) 🔴: **ABAC fail-closed.** If
  `KyvernoJsonEngine.is_configured()` returns false or `evaluate()`
  raises, return 403 + audit `token.vend.denied` (reason:
  `abac_eval_failed`). **Never fail open.** This is INV-17's runtime
  enforcement (C-6.1/C-7.1 — the grill's #1 finding). Test:
  `tests/test_abac_fail_closed.py` — mock `kj` absent → assert 403 +
  audit event.

#### Wave 5 — JWKS endpoint (backend-engineer)
- **Task 5.1** (backend-engineer): `core/lambda/nova_idp_jwks.py` —
  function URL `AuthType: NONE`, `Cache-Control: max-age=3600`.
  `kms.get_public_key` → DER SPKI → JWK via `cryptography`. Returns
  `{"keys":[...]}`. Custom domain + WAF = optional (D-230).

#### Wave 6 — PAT lifecycle (security-engineer + cli-engineer) [C-7.3]
- **Task 6.1** (security-engineer): PAT issuance — signed JWT
  (`typ: "developer_pat"`, INV-14), `nova-pats` PutItem (jti, pat_hash,
  status=active). Only hash stored (REQ-343). Revoked PATs retained.
  **Max TTL: ≤24h for developer PATs, ≤1h for service-account PATs**
  (C-6.2 threat model).
- **Task 6.2** (cli-engineer): `nova/auth/{login,revoke,status}.py` —
  `nova auth login` (session→OIDC token, store in
  `~/.nova/credentials.json` 0600), `nova auth revoke --pat <jti>`,
  `nova auth status` (active credential, mode, selection_reason).
  All emit audit events (REQ-344). **C-7.3: `~/.nova/credentials.json`
  stores the OIDC token + PAT metadata (jti, exp, type) ONLY — NOT the
  raw PAT.** The raw PAT is entered once at `nova auth login` and not
  persisted (reduces filesystem-compromise blast radius).

#### Wave 7 — CAP-037/038 (security-engineer)
- **Task 7.1** (security-engineer): `tests/test_kms_roundtrip.py`
  (REQ-350, CAP-037) — sign test JWT via token-vend, fetch JWKS,
  verify with `pyjwt`. `tests/test_pat_revocation.py` (REQ-351,
  CAP-038) — issue → vend → revoke → assert 403 within 60s P95.

#### Wave 8 — nova idp setup (cli-engineer + backend-engineer) [folded from P5 per C-2.1]

**Goal:** `nova idp setup` command with `--check/--apply/--verify`
modes; CloudFormation template generation + review (REQ-340, REQ-341).

- **Task 8.1** (backend-engineer): `nova/idp/setup.py` (+ backend
  helper) — generates the Nova-idp CloudFormation template (raw dict →
  JSON): 2-3 Lambdas, 4 DDB tables, KMS key, function URLs, IAM roles,
  optional CloudFront/WAF/ACM (`--public-jwks-domain` flag).
- **Task 8.2** (cli-engineer): `--check` (prerequisites + IAM policy
  delta), `--apply` (generate → `$PAGER` → `y/N` → `cloudformation
  deploy --capabilities CAPABILITY_IAM`, NFR-10), `--dry-run` (resource
  list only), `--verify` (KMS round-trip, delegates to REQ-350 test).
- **Task 8.3** (backend-engineer): IAM policy delta computation —
  compares current `nova-spike-runner` grants to required
  `cloudformation:*` + `codeartifact:*` + `kms:*` + `lambda:*` +
  `dynamodb:*` + `ssm:*`.

### Phase P5 — docs-integration (REQ-345..REQ-351)

**Goal:** Operator guide, developer guide, threat model; E2E
integration test; property tests; KMS round-trip; PAT revocation SLO.

**Exit criterion:** all REQ-345..351 tests pass + docs published +
threat model reviewed.

#### Wave 1 — docs (lead-developer + security-engineer) [C-6.2/C-6.3/C-9.2]
- **Task 1.1** (lead-developer): `docs/operator-guide-idp.md` (REQ-345)
  — `nova idp setup --check/--apply/--verify`, prerequisite IAM policy,
  CloudFormation review flow. **C-6.3 additions:** KMS key rotation
  procedure (90 days), Lambda layer update procedure, DDB PITR restore
  procedure, emergency PAT revocation (DDB-level, not CLI).
- **Task 1.2** (lead-developer): `docs/developer-guide-auth.md`
  (REQ-346) — signup, signin, login, mode resolution, TTY vs piped
  stdout behavior, JWS-from-PAT KDF (P2 Wave 2 Task 2.3).
- **Task 1.3** (security-engineer): `docs/threat-model.md` (REQ-347) —
  Argon2id storage, KMS signing, JWKS exposure, PAT revocation SLO,
  ABAC token vending, no-AWS-managed-identity (INV-15), DER→raw ECDSA
  gotcha. **C-6.2 additions:** (a) JWKS unauthenticated endpoint DDoS
  surface + reserved-concurrency mitigation; (b) PAT theft + max TTL
  (≤24h dev, ≤1h service-account); (c) ABAC fail-closed guarantee
  (C-6.1). **C-9.2 addition:** INV-18..21 compression audit — verify
  the spec's attestation invariant semantics are fully captured by
  INV-15/16/17 + REQ-332.

#### Wave 2 — integration tests (backend-engineer + security-engineer)
- **Task 2.1** (backend-engineer): `tests/test_e2e_idp.py` (REQ-348) —
  sign-up → sign-in → token-vend → apply → audit. Verifiable audit
  chain. Runs in CI against deployed Nova-idp.
- **Task 2.2** (security-engineer): verify REQ-349 (mode_resolver
  property tests, P1 Wave 3 Task 3.3) + REQ-350 (KMS round-trip, P4
  Wave 7 Task 7.1) + REQ-351 (PAT revocation SLO, P4 Wave 7 Task 7.1)
  pass in CI.

### Phase P6 — final-review-ship (Final Phase)

**Goal:** Multi-persona code review across P1..P5; project-health
audit; milestone ship to main; CAP-033..038 Verified.

#### Wave 1 — review (lead-developer)
- **Task 1.1** (lead-developer): `ciagent-review` across all phases.
  Auto-fix P0; flag P1+ for post-hoc. If P1+ found, fix in this phase.

#### Wave 2 — audit (lead-developer)
- **Task 2.1** (lead-developer): `ciagent-audit` — reconstruction test
  (git log ↔ `.ciagent/`), file/branch/commit discipline. Fix critical
  issues in this phase.

#### Wave 3 — milestone ship (lead-developer)
- **Task 3.1** (lead-developer): `ciagent-ship` — merge `phase/06` →
  `milestone/v1.28-cli-identity` → `main`; tag `v1.27.6` (= the v1.28
  release); Gitea release with full milestone summary; delete all
  milestone branches. Update REQUIREMENTS.md (mark REQ-323..353
  complete), ROADMAP.md (mark v1.28 complete), STATE.md (append
  CAP-033..038 + INV-12..17), NORTH_STAR.md.

---

## User-Facing Surface

> MVP/UX CHECK §1 (REQ-MVP-UX-001).

1. **CLI flag:** `nova --help` lists every subcommand; `nova init`
   scaffolds a project; `nova auth login` authenticates; `nova apply
   --local` runs locally; `nova idp setup` deploys the identity stack.
2. **README quickstart:** `docs/developer-guide-auth.md` (REQ-346)
   documents signup → signin → login → `nova apply` in a quickstart.
3. **`.feature` Scenario:** `tests/test_e2e_idp.py` (REQ-348) is the
   E2E happy path (sign-up → sign-in → token-vend → apply → audit).

## Happy Path

> MVP/UX CHECK §2 (REQ-MVP-UX-001). End-to-end scenario written BEFORE
> execute.

**Journey 2 — Dev authenticates and deploys locally:**
1. `nova auth signup` → `nova-idp-auth` Lambda → Argon2id hash →
   `nova-users` PutItem → session token.
2. `nova auth signin` → `nova-idp-auth` → Argon2id verify → session.
3. `nova auth login` → `nova-idp-token-vend` (exchanges session for
   Nova OIDC token; stores in `~/.nova/credentials.json` 0600).
4. `nova init` in a project dir → `.nova/`, `.gitignore`,
   `.nova/contract.yml.attestations/`.
5. `nova apply --local --sign-local-review` →
   `core.env.synthesize_local_env()` → `core.contract_resolver.resolve()`
   → JWS attestation signed with a key derived from the PAT → local
   ledger entry.

The E2E test (`tests/test_e2e_idp.py`, REQ-348) verifies this chain +
the audit event chain in CI against a deployed Nova-idp.

## UX Acceptance Criteria

> MVP/UX CHECK §3 (REQ-MVP-UX-001).

1. `nova --help` exits 0 and lists a subcommand for every `core/`
   module (CAP-033).
2. `nova init` in an empty dir creates `.nova/`,
   `.nova/contract.yml.attestations/`, `.gitignore` (secrets excluded).
3. `nova auth login` at a TTY resolves `mode=interactive,
   selection_reason=credential:developer_pat` (INV-12, INV-14).
4. `nova apply --local` produces a JWS attestation verifiable with the
   public key derived from the PAT (REQ-332).
5. `nova idp setup --check` reports prerequisites + IAM policy delta;
   `--apply` presents the CloudFormation template for review before any
   resource is created (NFR-10); `--verify` confirms the KMS round-trip.
6. The Forge action (`nova cli-action`) runs `nova apply` in
   `mode=agent, selection_reason=credential:service_account_pat` with
   no TTY dependency (Journey 3, INV-12).
7. PAT revocation takes effect within 60s P95 (NFR-4, CAP-038).

---

## Capability gate (CAP-033..CAP-038)

| CAP | Name | Phase | Gate rule |
|-----|------|-------|-----------|
| CAP-033 | CLI subcommand surface exists | P1 | `nova --help` lists a subcommand for every `core/` module |
| CAP-034 | Subcommand delegates to `core/` | P1 | Every `nova/<module>.py` ≤50 lines, no business logic, AST scan |
| CAP-035 | Layer matches wheel | P1 | Lambda layer ARN version matches `nova-cli` wheel version (SSM mapping) |
| CAP-036 | Nova-idp auth flow works | P3 | E2E test (sign-up → sign-in → session) passes in CI |
| CAP-037 | Token-vend signs via KMS | P4 | KMS round-trip test (REQ-350) passes in CI |
| CAP-038 | PAT issuance + revocation | P4 | Issue → vend → revoke → 403 within 60s P95 (REQ-351) passes in CI |
**Release gate (§6 of the spec):** CAP-001..CAP-032 remain Verified;
CAP-033..CAP-038 are Verified; all v1.28 release-gate criteria met.

---

## Test evidence required for v1.28 release

- [ ] Code coverage ≥ 80% on new modules (`mode_resolver.py`,
      `nova-idp-auth`, `nova-idp-token-vend`, PAT lifecycle).
- [ ] CI/CD pipeline GREEN: wheel + Lambda layer publish on every merge
      (REQ-323, CAP-035).
- [ ] QA sign-off: all four happy-path journeys (J1–J4) pass integration
      tests in CI.
- [ ] Security/compliance review: threat model published, Argon2id
      verified, ABAC policy reviewed.
- [ ] Capability gate GREEN: CAP-001..032 remain Verified; CAP-033..038
      Verified.
- [ ] Mode resolver property tests pass (all four priority levels + edge
      cases; REQ-349).
- [ ] KMS round-trip test passes against deployed JWKS (REQ-350).
- [ ] PAT revocation SLO verified: ≤60s P95 in CI (REQ-351, NFR-4).
- [ ] Operator + developer guides published.
- [ ] `nova idp setup` succeeds in a fresh AWS account.
- [ ] Byte-identical Forge action on GitHub + Gitea (REQ-326, NFR-11).

---

## Plan completeness checklist

- [x] Every REQ-323..353 mapped to a phase + wave + task.
- [x] Every CAP-033..038 mapped to a phase + gate rule.
- [x] Every INV-12..17 referenced in persona constraints.
- [x] Every D-226..231 referenced in task rationale.
- [x] Vertical slices: each phase ships independently (P1 CLI substrate
      is useful before P2 packaging; P2 before P3 auth; etc.).
- [x] Wave ordering within phases (no wave N+1 depends on wave N work
      in the same phase).
- [x] Persona assignments per task (4 active personas).
- [x] MVP/UX CHECK: 3 sections present (User-Facing Surface, Happy Path,
      UX Acceptance Criteria).
- [x] Highest-risk item flagged (P4 Wave 1 kj-binary spike).
- [x] Grill conditions applied (3 critical + 16 tracked; see GRILL.md).

---

## Cost envelope (C-3.1)

Monthly estimate for the default (no CloudFront) Nova-idp deployment in
account `581513795199`:

| Resource | Quantity | Pricing | Est. monthly |
|----------|----------|---------|-------------|
| DynamoDB (on-demand) | 4 tables | $1.25/1M write, $0.25/1M read | ~$1 (pilot volume) |
| DynamoDB PITR | 4 tables | $0.20/GB-month | ~$1 (small tables) |
| KMS asymmetric key | 1 key | $1/key-month + $0.03/10k signs | ~$1 |
| Lambda invocations | 3 Lambdas | $0.20/1M req + $0.000016/GB-s | ~$2 (low volume) |
| Lambda layer storage | ~50 MB | $0.02/GB-month | <$1 |
| CodeArtifact | 1 domain + 1 repo | $1/domain + $1/repo | $2 |
| SSM Parameter | 1 | $0.05/param (advanced) | <$1 |
| **Total (default)** | | | **~$9/month** |

Optional CloudFront + WAF + ACM (if `--public-jwks-domain`): +~$3/month
at pilot volume. ACM is free for CloudFront-attached certs.

This is a pilot-scale cost envelope. Production scale (100x volume)
would still be <$50/month. No hidden costs identified.

---

# PLAN — v1.29 Reposplit + Identity Layer Bring-Live

> **Milestone:** v1.29 (feature — reposplit + identity layer bring-live).
> Tags on the **v1.28.x** line: `v1.28.0` (P0) → `v1.28.1..v1.28.5`
> (P1..P5) → `v1.28.6` (P6 final = milestone release). The final phase's
> patch IS the milestone release.
> **Branch:** `milestone/v1.29-reposplit-identity`. Phase branches:
> `phase/00-pre-execution` (complete), `phase/01-publish-pipeline`,
> `phase/02-gitea-scrub-decisions`, `phase/03-cfn-archive-tf-delegation`,
> `phase/04-operator-guide-reference-tracking`,
> `nova-blockchain-exchange/phase/05-consumer-deploy-bump` (cross-project),
> `phase/06-final-review-ship`.
>
> **Scope split (CLARIFY-grounded):** Terraform modules authored
> out-of-band in `nova-platform-ops`. CIAgent in `acdl` authors only the
> acdl-side REQs (354, 367, 368, 369, REQ-OPS-GUIDE, REQ-CONSUMER-BUMP).
> Covered-reference REQs (355-366, 371) verified via cutover gates
> documented in the operator guide (P4).

## Milestone goal

v1.29 makes platform operations a Terraform-controlled discipline that
lives outside the engineering repo, with a narrow-IAM `kj` substrate
shared by the primary runtime and its defensive fallback. `acdl/acdl`
standardizes on GitHub (Gitea scrub); Nova-idp is brought live in
account `581513795199` (code complete since v1.28, unverified in-account
at Phase 0); `kj` has exactly one identity (one ECR image digest) shared
by both substrates (KJ-LOCKSTEP, REQ-371).

## Requirements

17 requirements: REQ-354..REQ-369 + REQ-371 + REQ-363b + REQ-OPS-GUIDE
+ REQ-CONSUMER-BUMP (full text in `.ciagent/REQUIREMENTS.md` §v1.29).
1 invariant: INV-18 (JWKS-EDGE-ONLY). 10 NFR constraints: KJ-STATIC,
KJ-LOCKSTEP, KJ-WARMUP-HEALTH, OPER-PRIV, IAM-NARROW, DRIFT-DETECT,
IMPORT-IDEMPOTENT, TFM-HITL, JWKS-SLO, JWKS-ROTATION. 9 decisions:
D-232..D-238 (CLARIFY) + D-239/D-240 (RESEARCH spec corrections).

## Phase breakdown

### Phase P1 — publish-pipeline (REQ-354)

**Goal:** `publish.yml` attaches Lambda zip + layer wheel + Python wheel
+ ECR container image (static `kj`, `CGO_ENABLED=0`, tag
`v1.29.x-kj-<sha>`) to GitHub Release for each tag, with matching
SHA-256 in the body. The M1.5 verification gate tests
(`test_idp_auth`, `test_kms_roundtrip`, ABAC E2E) are authored.

**Exit criterion:** REQ-354 criteria 1-4 pass; KJ-STATIC audit (file(1)
asserts `statically linked`) runs in CI; ECR image pushed with tag
`v1.29.x-kj-<sha>` (D-239); GitHub Release body lists image URI + digest
alongside wheel + layer + Lambda zip; M1.5 gate tests exist + pass in
moto-DDB (live KMS round-trip is covered-reference, runs in
nova-platform-ops CI).

**Branch:** `phase/01-publish-pipeline`. **Tag:** `v1.28.1`.

#### Wave 0 — publish.yml trigger model (backend-engineer)
- **Task 0.1** (backend-engineer): change `.github/workflows/publish.yml`
  trigger from `push: branches: [main]` to `push: tags: ['v1.29.*']`.
  Preserve the existing wheel + Lambda layer publish steps (REQ-323/
  CAP-035). Add the Lambda zip packaging step
  (`nova-lambda-token-vend-v1.29.x.zip`). Verify the trigger fires on
  `git tag v1.29.0 && git push --tags`.

#### Wave 1 — kj source confirmation + static build + ECR image (backend-engineer, security-engineer)
- **Task 1.0** (backend-engineer): **kj source-fetch confirmation
  (grill CF-4/G-4 — binary go/no-go gate before Wave 1).** Confirm the
  `kj` Go source repo URL + commit at SHA `4ebb9a19...` (read from
  `platform/abac/kj-version.txt`). Record the repo URL as a 3rd line
  in `platform/abac/kj-version.txt`. If unfetchable → P1 fails closed
  → escalate (this is a spec dependency, not a CIAgent ambiguity). The
  source repo is the `kyverno-json/kj` Go binary project (distinct
  from the kyverno-json Python engine adapter in `adapters/kyverno-
  json/`).
- **Task 1.1** (backend-engineer): add a `build-kj-image` job to
  `publish.yml` that:
  (a) reads `platform/abac/kj-version.txt` (v0.0.3 + SHA
  `4ebb9a19...`);
  (b) fetches the `kj` Go source at the pinned SHA (RESEARCH §7 —
  source repo confirmed in P1 RESEARCH);
  (c) builds with `CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build
  -ldflags="-s -w" -o kj ./…`;
  (d) runs `file kj` and asserts output contains `statically linked`
  AND does NOT contain `shared library` (KJ-STATIC — fail build
  otherwise);
  (e) builds the container image from
  `public.ecr.aws/lambda/python:3.12-al2023`, copying `kj` to
  `/opt/kj/kj` with `chmod 0555` owned by `sbx_user:1051`;
  (f) pushes the image to ECR with tag `v1.29.x-kj-<kj-source-sha>`
  (D-239 — assert tag matches `^[a-zA-Z0-9._-]+$` before push);
  (g) records the image URI + digest for the GitHub Release body.
- **Task 1.2** (security-engineer): add a KJ-STATIC audit step that
  runs `file(1)` + `readelf -d kj` (assert no `NEEDED` entries) as a
  CI gate. If either fails, the publish job fails closed. This is the
  mechanical enforcement of KJ-STATIC (not just a human review).

#### Wave 2 — GitHub Release body + SHA-256 (backend-engineer)
- **Task 2.1** (backend-engineer): extend the `publish.yml` release step
  to attach: (a) `nova-lambda-token-vend-v1.29.x.zip`; (b)
  `nova-cli-layer-v1.29.x.zip`; (c) `nova-1.29.x-py3-none-any.whl`; (d)
  the ECR image URI + digest. Compute SHA-256 for each artifact + list
  in the release body. Verify REQ-354 criteria 1, 2, 4 (artifacts
  appear, independent per tag, image URI + digest listed).

#### Wave 3 — M1.5 verification gate tests (security-engineer)
- **Task 3.1** (security-engineer): author `tests/test_idp_auth.py` —
  sign-up → sign-in → session flow against moto-DDB (covers Edge 5
  item 5). Skip live-KMS assertions (covered-reference — runs in
  nova-platform-ops CI).
- **Task 3.2** (security-engineer): author
  `tests/test_kms_roundtrip.py` — sign/verify round-trip against
  `alias/nova-oidc-signing`. Mark as `@pytest.mark.live_aws` (skipped in
  acdl CI; runs in nova-platform-ops CI against the live key, REQ-362).
- **Task 3.3** (security-engineer): author
  `tests/test_abac_e2e.py` — known PAT → ABAC-allowed action → signed
  OIDC token → `jose` verification → green; known PAT + ABAC-denied
  action → 403 with deny reason logged (INV-17 fail-closed, Edge 5
  item 7). Uses moto-DDB + mock KMS.

#### Wave 4 — regression + ship (lead-developer)
- **Task 4.1** (lead-developer): run full test suite; verify 1000+
  tests still pass (no regressions from publish.yml changes). Verify
  CAP-001..038 regression gate green. **Push a `v1.29.0` intermediate
  tag at P1 ship** (grill CF-3/G-3) to trigger `publish.yml` + produce
  the v1.29 artifacts (Lambda zip + layer wheel + Python wheel + ECR
  image). The milestone release tag remains `v1.28.6`; the `v1.29.0`
  artifact tag is a P1 intermediate to produce publish artifacts for
  P5's smoke test. Ship P1 → `v1.28.1`.

### Phase P2 — gitea-scrub-decisions (REQ-367, REQ-368)

**Goal:** Hard scrub of all Gitea references in `acdl/acdl`; `.gitea/`
removed; `forge_parity_disabled` CI assertion; pyproject → 1.29.0;
decisions D-232..238 recorded in PROJECT.md + CLARIFY (already done in
P0; this phase adds the CI assertion + the actual file scrub).

**Exit criterion:** `grep -rni gitea .github/ docs/ pyproject.toml
README.md .ciagent/` returns zero matches outside the spec archive
section; `find .gitea` returns nothing; CI `forge_parity_disabled`
assertion passes; pyproject.toml version = 1.29.0.

**Branch:** `phase/02-gitea-scrub-decisions`. **Tag:** `v1.28.2`.

#### Wave 0 — pyproject bump (lead-developer)
- **Task 0.1** (lead-developer): bump `pyproject.toml` version →
  `1.29.0` (spec §7.2). Verify `nova --version` reports `1.29.0`.

#### Wave 1 — .gitea/ removal (lead-developer)
- **Task 1.1** (lead-developer): `rm -rf .gitea/` (7 workflow files +
  README.md, RESEARCH §9d). Remove `scripts/sync_workflows.py` (the
  byte-identical-forges generator — central removal target, D-232).
  Remove Gitea references from `scripts/sync_to_nova.sh` (line 201:
  `--exclude=/.gitea`) + `scripts/rotate_spike_key.sh` (Gitea API
  secret upload). Scrub `terraform/bootstrap/` Gitea OIDC references
  (the OIDC role for act_runner moves to nova-platform-ops; the
  bootstrap here becomes archived reference).

#### Wave 2 — Gitea reference scrub (lead-developer)
- **Task 2.1** (lead-developer): `grep -rni gitea .github/ docs/
  pyproject.toml README.md .ciagent/` — scrub all matches outside the
  spec archive section (`.ciagent/REQUIREMENTS.md` §v1.29 + CLARIFY §v1.29
  + RESEARCH §v1.29 retain "Gitea" as historical/reference text; these
  are the "spec archive section" exemption per REQ-367 AC 1). Update
  `.github/workflows/ci.yml` to remove any Gitea-specific steps.

#### Wave 3 — forge_parity_disabled CI assertion (lead-developer)
- **Task 3.1** (lead-developer): add a CI step to `.github/workflows/ci.yml`
  that asserts `forge_parity_disabled` — the step runs
  `test ! -d .gitea/` and `! grep -rqi gitea .github/workflows/` and
  exits 0 on success, non-zero with `forge_parity_disabled` message on
  failure (REQ-367 AC 3, D-232). This is the deliberate CI failure that
  documents the abandoned parity.

#### Wave 4 — decisions verification + ship (lead-developer)
- **Task 4.1** (lead-developer): verify D-232..238 + D-239/240 are
  present in PROJECT.md + CLARIFY.md + REQUIREMENTS.md (REQ-368 AC 1-2,
  already authored in P0; this task is a verification, not re-authoring).
  Run full test suite; ship P2 → `v1.28.2`.

### Phase P3 — cfn-archive-tf-delegation (REQ-369)

**Goal:** Archive the CFN template in `nova/idp/setup.py` +
`core/lambda/nova_idp_setup.py` to `docs/archive/nova-idp-cfn-v1.28.md`
(read-only reference); `nova idp setup --apply` delegates to `terraform
apply` (the CLI detects terraform via `which terraform`; if absent,
falls back to the CFN path with a deprecation warning).

**Exit criterion:** `docs/archive/nova-idp-cfn-v1.28.md` exists +
contains the CFN template as read-only reference; `nova idp setup
--apply` invokes `terraform apply` when terraform is on PATH (tested
with a mock terraform binary); the CFN path emits a deprecation warning
when terraform is absent.

**Branch:** `phase/03-cfn-archive-tf-delegation`. **Tag:** `v1.28.3`.

#### Wave 0 — CFN archive (cli-engineer, backend-engineer)
- **Task 0.1** (backend-engineer): extract the CFN template from
  `core/lambda/nova_idp_setup.py` + write it to
  `docs/archive/nova-idp-cfn-v1.28.md` as a fenced code block with a
  read-only header ("Archived at v1.29.0 — the active path is
  `terraform apply` in `nova-platform-ops`. Deletion is a follow-up
  after Terraform parity is verified.").
- **Task 0.2** (cli-engineer): mark the CFN generation code path in
  `core/lambda/nova_idp_setup.py` as deprecated (add a
  `DeprecationWarning` when the CFN path is invoked + a docstring
  pointing to the archive + the terraform delegation path).

#### Wave 1 — terraform delegation (cli-engineer)
- **Task 1.1** (cli-engineer): modify `nova/idp/setup.py` `--apply` to
  detect terraform via `shutil.which("terraform")`. If terraform is on
  PATH: delegate to `subprocess.run(["terraform", "apply",
  "-auto-approve"])` in the `nova-platform-ops` checkout (the operator
  runs this from the ops repo root). If terraform is absent: fall back
  to the CFN path with a `DeprecationWarning` ("CFN path is archived;
  install terraform or use nova-platform-ops. See
  docs/archive/nova-idp-cfn-v1.28.md.").
- **Task 1.2** (cli-engineer): add `nova idp setup --verify` delegation
  to `terraform plan` (same `which terraform` detection). The verify
  path runs `terraform plan` + reports the diff.

#### Wave 2 — tests (cli-engineer)
- **Task 2.1** (cli-engineer): author
  `tests/test_idp_setup_tf_delegation.py` — test the `--apply` path
  with a mock terraform binary on PATH (assert `subprocess.run` called
  with `["terraform", "apply", "-auto-approve"]`); test the fallback
  path with terraform absent (assert `DeprecationWarning` raised + CFN
  path invoked); test `--verify` delegates to `terraform plan`.

#### Wave 3 — ship (lead-developer)
- **Task 3.1** (lead-developer): run full test suite; ship P3 →
  `v1.28.3`.

### Phase P4 — operator-guide-reference-tracking (REQ-OPS-GUIDE)

**Goal:** `docs/operator-guide-platform-ops.md` covering KMS rotation,
JWKS reachability via CloudFront edge, PITR restore, PAT revocation,
edge configuration, Fargate standby health, cost section, artifact-
mirror fallback, and the M1/M1.5/M2 cutover gates as release-gate
entries for the covered-reference REQs. ARCHITECTURE.md §12.9. STATE.md
v1.29 CAPs + invariants. REQUIREMENTS.md covered-reference markers.

**Exit criterion:** operator guide exists + covers all sections per
REQ-OPS-GUIDE AC; ARCHITECTURE.md §12.9 added; STATE.md updated with
v1.29 rows; covered-reference REQs in REQUIREMENTS.md marked with their
cutover gate.

**Branch:** `phase/04-operator-guide-reference-tracking`. **Tag:**
`v1.28.4`.

#### Wave 0 — operator guide (lead-developer, data-engineer, security-engineer)
- **Task 0.1** (lead-developer): author
  `docs/operator-guide-platform-ops.md` sections: (a) Overview + the
  reposplit rationale (Vision §4); (b) Day-0 cutover procedure (M1
  steps from spec §3.2 Journey 2); (c) M1.5 verification gate (8-item
  spike, 3 consecutive rebuilds); (d) M2 operational handoff loop
  (tag-pin bump → plan → HITL approval → apply); (e) M2a Fargate
  activation (conditional on M1.5 failure); (f) Rollback procedure
  (D-236 — revert `nova_platform_version` pin); (g) cost section (WAF
  ~$5-10/month + Fargate ~$15-20/month, REQ-363b AC 4); (h) artifact-
  mirror fallback (operator-local mirror by SHA-256 when Gitea
  act_runner cannot reach GitHub Releases, Edge 6).
- **Task 0.2** (data-engineer): author the operator guide data
  sections: (a) DynamoDB PITR restore procedure (per-table); (b)
  DynamoDB import addresses (nova-contracts, nova-change-requests,
  nova-outbox, nova-users, nova-sessions, nova-pats — the
  `importable-resources.tf` map, REQ-361 covered-reference); (c) audit
  outbox bootstrap; (d) JWKS-ROTATION (24-hour overlap window on key
  rotation).
- **Task 0.3** (security-engineer): author the operator guide security
  sections: (a) KMS rotation (90-day cadence, `alias/nova-oidc-
  signing`, `ECC_NIST_P256`, D-234); (b) JWKS reachability via
  CloudFront edge (OAC pinning, `AuthType: AWS_IAM`, direct Function
  URL → 403, INV-18); (c) PAT revocation (60s SLO, D-229); (d) edge
  configuration (CloudFront + WAF + ACM + Route53 — REQ-364/365/366
  covered-reference); (e) Fargate standby health checks (`GET /health`
  every 10s, `KJ-WARMUP-HEALTH`, 3 consecutive probe failures → alert +
  token-vend fails closed, REQ-363b AC 2); (f) Fargate sunset
  discipline (D-237 — ≥30 consecutive days green before deletion +
  architecture review); (g) IAM scope (IAM-NARROW, REQ-360 covered-
  reference — no `Action: "*"` or `Resource: "*"`); (h) the
  `route53_record_not_resolvable` debugging path (ACM cert status
  check).

#### Wave 1 — covered-reference cutover gates (lead-developer)
- **Task 1.1** (lead-developer): add a "Cutover Gates" section to the
  operator guide listing each covered-reference REQ (355, 356, 357,
  358, 359, 360, 361, 362, 363, 363b, 364, 365, 366, 371) with its
  gate entry (M1/M1.5/M2) + the verification command + a **"Result"
  column** (grill CF-2/G-5). P6 audit verifies every covered-reference
  REQ has a non-empty, green Result. Empty/red → P6 blocks. The Result
  column is populated by the operator attestation (the operator runs
  the verification command in `nova-platform-ops` CI + records the
  outcome). This is the acdl-side evidence surface for covered-
  reference REQs.
- **Task 1.2** (lead-developer): update REQUIREMENTS.md §v1.29 traceability
  table — mark each covered-reference REQ with its cutover gate in the
  Status column (e.g., `planned (M1 gate: nova-platform-ops)`).

#### Wave 2 — ARCHITECTURE.md + STATE.md (lead-developer)
- **Task 2.1** (lead-developer): add ARCHITECTURE.md §12.9 (Platform
  Ops Reposplit) — the domain boundary (engineering ends at the
  compiled artifact; operations begins at the live platform under
  guardrails), the `kj` substrate (one ECR image digest, KJ-LOCKSTEP),
  the covered-reference REQ tracking pattern, the operator guide
  pointer.
- **Task 2.2** (lead-developer): update STATE.md — append v1.29
  capability rows (CAP-039: platform-ops-reposplit, CAP-040:
  kj-substrate-lockstep, CAP-041: jwks-edge-only) + bump invariants
  (INV-18 JWKS-EDGE-ONLY + the 10 NFR constraints). Bump "Last
  milestone ship" to v1.29 (pending).

#### Wave 3 — ship (lead-developer)
- **Task 3.1** (lead-developer): run full test suite; ship P4 →
  `v1.28.4`.

### Phase P5 — consumer-deploy-bump (REQ-CONSUMER-BUMP, cross-project)

**Goal:** Bump `nova-blockchain-exchange` deploy.yml `@v1.25` → `@v1.29`
in both `.github/workflows/deploy.yml` + `.gitea/workflows/deploy.yml`
+ smoke test (sign-up → sign-in → token-vend → apply → audit against
v1.29 publish artifacts).

**Exit criterion:** both deploy.yml files reference `@v1.29`; smoke
test passes (the chain completes against v1.29 publish artifacts).

**Branch:** `nova-blockchain-exchange/phase/05-consumer-deploy-bump`
(cross-project, multi-project branch naming per branch-strategy.md).
**Tag:** `v1.28.5`.

#### Wave 0 — deploy.yml bump (lead-developer)
- **Task 0.1** (lead-developer): in the `nova-blockchain-exchange`
  project, update `.github/workflows/deploy.yml` + `.gitea/workflows/
  deploy.yml` `uses:` ref from `acdl/.github/workflows/deploy.yml@v1.25`
  → `@v1.29` (RESEARCH §9e — the consumer's `.gitea/` is out of scope
  for the acdl REQ-367 scrub; the consumer may keep its Gitea mirror or
  follow suit — this is a consumer-repo decision, not an acdl one).

#### Wave 1 — smoke test (lead-developer, security-engineer)
- **Task 1.1** (security-engineer): author
  `nova-blockchain-exchange/tests/test_v1.29_smoke.py` — sign-up →
  sign-in → token-vend → apply → audit chain against the v1.29 publish
  artifacts (the consumer's contract → `deploy.yml@v1.29` mode=full →
  apply → attest → record against `581513795199`). Uses the existing
  CAP-025 round-trip assertion (v1.26).
- **Task 1.2** (lead-developer): run the smoke test; verify the chain
  completes against the real v1.29.0 publish artifacts (produced by
  P1's intermediate tag, grill CF-3/G-3). **No hedge** — the smoke
  test MUST run against the published v1.29.x artifacts or P5 fails
  closed. If the artifacts are not available (P1 did not push the
  intermediate tag), P5 blocks until P1 re-ships.

#### Wave 2 — ship (lead-developer)
- **Task 2.1** (lead-developer): ship P5 → `v1.28.5`. The consumer
  project ships independently (merge to the consumer's main, not
  acdl's milestone branch).

### Phase P6 — final-review-ship (Final Phase)

**Goal:** Multi-persona code review across P1..P5; audit (reconstruction
test, branch hygiene, commit discipline, file discipline); milestone
ship (merge `phase/06` → `milestone/v1.29-reposplit-identity` → `main`;
tag `v1.28.6` = the v1.29 release; Gitea release; delete all milestone
branches); mark all v1.29 REQs complete in REQUIREMENTS.md + ROADMAP.md.

**Exit criterion:** review P0 issues auto-fixed, P1+ flagged; audit
PASS; milestone merged to main; tag `v1.28.6` created; Gitea release
published; milestone branches deleted; REQUIREMENTS.md + ROADMAP.md
marked complete.

**Branch:** `phase/06-final-review-ship`. **Tag:** `v1.28.6` =
milestone release.

#### Wave 0 — review (lead-developer)
- **Task 0.1** (lead-developer): delegate to `ciagent-review` —
  multi-persona review (lead-developer, backend-engineer, security-
  engineer, data-engineer, cli-engineer) across P1..P5. Auto-apply P0
  fixes; flag P1+ for post-hoc review. If P1+ issues found: fix them
  in this phase.

#### Wave 1 — audit (lead-developer)
- **Task 1.1** (lead-developer): delegate to `ciagent-audit` —
  reconstruction test (git log ↔ `.ciagent/`), branch hygiene, commit
  discipline, file discipline. If critical issues found: fix them in
  this phase.

#### Wave 2 — milestone ship (lead-developer)
- **Task 2.1** (lead-developer): delegate to `ciagent-ship` — merge
  `phase/06` → `milestone/v1.29-reposplit-identity` → `main`; tag
  `v1.28.6`; Gitea release with full milestone summary; delete all
  milestone branches (phase/00..06 + milestone/v1.29-reposplit-
  identity).

#### Wave 3 — milestone completion (lead-developer)
- **Task 3.1** (lead-developer): update REQUIREMENTS.md (all v1.29 REQs
  → complete), ROADMAP.md (v1.29 → complete), NORTH_STAR.md (note
  Strategic Objective — platform operations as a Terraform-controlled
  discipline), STATE.md (bump "Last milestone ship" to v1.29, tag
  `v1.28.6`). Commit `docs(milestone): complete v1.29-reposplit-
  identity`.

---

## User-Facing Surface

1. **CLI flag:** `nova idp setup --apply` now delegates to `terraform
   apply` (REQ-369 AC 2) — the operator runs this from the
   `nova-platform-ops` checkout. `nova idp setup --verify` delegates to
   `terraform plan`.
2. **GitHub Release artifacts page:** each `v1.29.x` tag's GitHub
   Release page lists the Lambda zip + layer wheel + Python wheel + ECR
   image URI/digest with SHA-256 (REQ-354) — this is the engineering-
   to-ops handoff surface (D-235 tag-pin handoff).
3. **Operator guide:** `docs/operator-guide-platform-ops.md` — the
   operator-facing runbook covering KMS rotation, JWKS reachability,
   PITR restore, PAT revocation, edge config, Fargate standby, cost,
   artifact-mirror fallback, and the M1/M1.5/M2 cutover gates.
4. **CI assertion:** `forge_parity_disabled` — the deliberate CI
   failure documenting the abandoned byte-identical-forges parity
   (D-232, REQ-367 AC 3).

## Happy Path

**M1.5 verification gate (spec §3.3 Edge 5, 12-item spike — written
BEFORE execute, extended per grill CF-1):**

1. `kj` v0.0.3 (pinned SHA in `platform/abac/kj-version.txt`) compiles
   with `CGO_ENABLED=0 GOOS=linux GOARCH=amd64`.
2. Resulting binary reports `file kj → ELF 64-bit LSB executable,
   x86-64, statically linked, no shared library` (KJ-STATIC).
3. Container image built from
   `public.ecr.aws/lambda/python:3.12-al2023` with the binary copied
   to `/opt/kj/kj`, `chmod 0555`, owned by `sbx_user:1051`.
4. Lambda runtime `python3.12` executes
   `nova_idp_token_vend.handler`; the handler invokes
   `subprocess.run(['/opt/kj/kj', 'apply', ...])` and parses stdout
   JSON.
5. `tests/test_idp_auth.py` passes against the live image in moto-DDB.
6. `tests/test_kms_roundtrip.py` passes against the live KMS key
   (REQ-362 path — covered-reference, runs in nova-platform-ops CI).
7. End-to-end: known PAT → known ABAC-allowed action → signed OIDC
   token → `jose` verification → green. Known PAT + ABAC-denied action
   → 403 with deny reason logged (INV-17).
8. Image URI is recorded in Terraform state and in the operator guide.
9. **(grill CF-1)** Direct JWKS Function URL → 403 / via-CloudFront →
   200 (INV-18, JWKS-EDGE-ONLY — `AuthType: AWS_IAM` verified, not
   prose).
10. **(grill CF-1)** IAM-NARROW: no `Action: "*"` or `Resource: "*"`
    in the Gitea OIDC role effective permissions (REQ-360).
11. **(grill CF-1)** TFM-HITL: self-approval rejected —
    `gitea.triggering_actor == pull_request.user.login` → apply fails
    closed (REQ-357, INV-3).
12. **(grill CF-1)** Rollback drill — revert `nova_platform_version`
    pin → prior digest runs (D-236 cutover shape + rollback procedure).

If items 1-7 fail three consecutive rebuilds, M2a activates REQ-363b
(Fargate toggle) with the same image — no warmup hit because the
standby is always running the same digest.

**HARD P6 SHIP GATE (grill CF-1/G-2.1):** P6 must not ship `v1.28.6`
until the operator guide contains an operator-attested "M1.5
Verification Gate Result" row (3 consecutive green rebuilds, run
IDs/SHAs, attestor identity). P6 audit verifies the row exists. The
M1.5 gate is verified in `nova-platform-ops` CI (out-of-band); the
operator attestation in the guide is the acdl-side evidence surface.

## UX Acceptance Criteria

1. **M1 acceptance gate (spec §2.3):** `terraform apply` from `main`
   brings the live AWS account to a state where Nova-idp identity
   tables exist, JWT-issuing paths are wired but not yet consuming
   container images, JWKS infrastructure is in place, WAF + OAC pinning
   the CloudFront edge; `acdl/acdl v1.29.0` ships with zero `.gitea/`
   references and zero platform-infra files; D-232..238 recorded in
   PROJECT.md/CLARIFY.
2. **M1.5 acceptance gate:** items 1-12 of the Edge 5 spike all green
   on three consecutive rebuilds; image digest resolvable via
   `data.aws_ecr_image.kj_image`; sign/verify round-trip passes; ABAC
   fail-closed path verified against live policy; JWKS-EDGE-ONLY
   verified (item 9); IAM-NARROW verified (item 10); TFM-HITL
   self-approval rejected (item 11); rollback drill passes (item 12).
   **HARD P6 ship gate** — operator-attested "M1.5 Verification Gate
   Result" row in the operator guide (grill CF-1/G-2.1).
3. **M2 acceptance gate:** Bumping `local.nova_platform_version` in a
   PR and merging it results in `terraform apply` updating both
   `aws_lambda_function.nova_idp_token_vend.image_uri` and
   `aws_ecs_task_definition.kj.container_definitions[0].image` to the
   same digest (KJ-LOCKSTEP, REQ-371), with zero diff on KMS, DDB,
   IAM, edge.

## Test evidence required for v1.29 release

- [ ] Code coverage ≥ 80% on new modules (the acdl-side files:
      `publish.yml` changes, `nova/idp/setup.py` terraform delegation,
      `docs/operator-guide-platform-ops.md` is docs — no coverage
      requirement; the M1.5 gate tests).
- [ ] CI/CD pipeline GREEN for `acdl/acdl` (the `nova-platform-ops`
      pipeline is out-of-band).
- [ ] M1.5 verification gate green: items 1-12 of §3.3 Edge 5 spike
      pass on three consecutive rebuilds (covered-reference — verified
      in nova-platform-ops CI; acdl authors the tests in P1; operator
      attests in the guide, P4; P6 audit verifies the attestation row,
      grill CF-1/G-2.1).
- [ ] Covered-reference REQs (355-366, 371) have non-empty, green
      Result in the operator guide "Cutover Gates" section (grill
      CF-2/G-5 — P6 audit verifies).
- [ ] `lifecycle.precondition` enforced on both image-bearing resources
      (REQ-371 mechanical proof — covered-reference in
      nova-platform-ops).
- [ ] Live KMS sign/verify round-trip verified in account
      `581513795199` (covered-reference).
- [ ] Live ABAC sign/verify round-trip verified against the production
      policy (covered-reference).
- [ ] Pilot consumer (`nova-blockchain-exchange`) smoke test green:
      sign-up → sign-in → token-vend → apply → audit chain (P5).
- [ ] All existing capabilities (CAP-001..038) still pass the
      regression gate.
- [ ] Drift-detection baseline: `terraform plan` exit 0 against live
      AWS state, captured at cutover (covered-reference).
- [ ] `kj` standby Fargate task health `READY` before M1 cutover
      (covered-reference, KJ-WARMUP-HEALTH).
- [ ] Fargate standby sunset discipline documented in operator-guide
      (D-237, P4).
- [ ] `forge_parity_disabled` CI assertion passes (P2, REQ-367 AC 3).
- [ ] `grep -rni gitea .github/ docs/ pyproject.toml README.md
      .ciagent/` returns zero matches outside the spec archive section
      (P2, REQ-367 AC 1).

## Plan completeness checklist

- [x] Every REQ mapped to a phase + wave + task.
- [x] Covered-reference REQs identified + their verification surface
      documented (operator guide P4, cutover gates).
- [x] Decisions D-232..240 referenced in the plan.
- [x] Invariants + NFR constraints referenced (KJ-STATIC, KJ-LOCKSTEP,
      INV-18, etc.).
- [x] Personas assigned to every task (lead-developer, backend-engineer,
      security-engineer, cli-engineer, data-engineer).
- [x] User-Facing Surface section (3 surfaces named).
- [x] Happy Path section (M1.5 8-item spike, written before execute).
- [x] UX Acceptance Criteria section (M1, M1.5, M2 gates).
- [x] Test evidence checklist.
- [x] Phase boundaries + tags (v1.28.0 → v1.28.6).
- [x] Cross-project phase (P5, nova-blockchain-exchange) identified.

## Cost envelope (v1.29)

Monthly estimate for the `nova-platform-ops` live platform (documented
in the operator guide, P4):

| Resource | Quantity | Est. monthly |
|----------|----------|-------------|
| WAF WebACL (CloudFront-scoped) | 1 | ~$5-10/month (+ per-request) |
| Fargate standby (0.25 vCPU, 512 MB) | 1 task | ~$15-20/month (REQ-363b AC 4) |
| KMS asymmetric key | 1 | ~$1/month |
| DynamoDB (on-demand, 7 tables) | 7 | ~$2/month (pilot volume) |
| DynamoDB PITR | 7 tables | ~$2/month |
| Lambda invocations (3 Lambdas) | 3 | ~$2/month |
| ECR image storage | ~100 MB | <$1/month |
| S3 state bucket + access logs | 1 | <$1/month |
| CloudFront + ACM + Route53 | 1 distribution | ~$1/month (ACM free) |
| **Total** | | **~$30-40/month** |

This is the pilot-scale ops cost envelope. The Fargate standby
(~$15-20/month) is the largest line item + is explicitly documented in
the operator guide (REQ-363b AC 4) with the D-237 sunset discipline
(≥30 consecutive days green before deletion + architecture review).