1 Commits

Author SHA1 Message Date
CIAgent b624317048 docs(P00): complete pre-execution phase
---ci---
phase: 0
milestone: v0.1
status: complete
decisions:
  - id: D-001..D-028
    decision: phase-0 decision record carried forward from squashed history (gitea classification, v0.1 minor pin, go-live slug, single-project mode, 6-phase roadmap, secrets scopes, env-discovery split, adapter pattern, clarified scope D-009..D-022, research decisions D-023..D-027, mvp/ux gate D-028)
    rationale: squash merge collapses phase-0 commits; decision details live in .ciagent/{PROJECT,RESEARCH,GRILL,PLAN}.md committed in this tree
    confidence: 1.0
    alternatives: []
  - id: G-001..G-012
    decision: grill binding decisions carried forward (verdict proceed at 0.65; prerelease=true for v0.1.0..v0.1.6 per G-003; container artifact conditionality per G-004; mock-only terraform validation per G-005)
    rationale: details in .ciagent/GRILL.md committed in this tree
    confidence: 0.65
    alternatives: []
---/ci---
2026-10-14 22:12:59 +00:00
11 changed files with 1151 additions and 0 deletions
+64
View File
@@ -0,0 +1,64 @@
# Nova CLI — Architecture (P0 refined)
## Overview
nova-cli is a Python CLI that mirrors Docker / Docker Compose ergonomics for infrastructure
deployment. Commands flow from a docker-like command surface through file parsers (Novafile,
compose.yml) and an environment discovery layer into a pluggable IaC adapter (Terraform is the
first adapter) that deploys infrastructure modules to cloud providers (AWS, Azure, GCP). Policy
checks run through vendored c7n + c7n-left. Refined during phase-0 research (see RESEARCH.md).
## Module Layout (src-layout, single package `nova`)
| Path | Contents |
|------|----------|
| `src/nova/cli/` | click entry point (`nova.cli:main`), command groups (build, up, down, ps, ..., policy, env, version), docker-parity flag layer (`-f/-p/-d/--env-file/--profile`), help styling |
| `src/nova/parsers/` | `novafile.py` (Dockerfile-grammar parser: case-insensitive instructions, comments, `\` continuations, ENV k=v / k v, ARG, FROM...AS, LABEL, WORKDIR; BuildKit-style `Novafile:<l>:<c>` errors), `compose.py` (compose-spec schema subset: name, services, environment list/map, env_file, depends_on short/long, working_dir, profiles, providers block), `interp.py` (`${VAR}` `${VAR:-def}` `${VAR:?err}` `${VAR:+alt}` + `.env` loading), `moduleref.py` (registry shorthand `[host/]ns/name/provider[//subdir][@ver]`, registry URLs, local `./` `../`) |
| `src/nova/env/` | `git.py` (pure-python `.git` parsing: config INI, HEAD, loose refs + packed-refs, zlib commit objects; `git` subprocess fallback; auto-init with notice), `credentials.py` (gh hosts.yml, glab config.yml, AWS credentials file/env, ARM_* env + azure profile, GOOGLE_*/gcloud ADC; masked fingerprints), `pipeline.py` (GITHUB_ACTIONS / GITEA_ACTIONS / GITLAB_CI detection → PrefillContext), `forge.py` (stdlib-HTTP GitHub + Gitea clients; shared read surface, per-forge auth/pagination/payload normalization) |
| `src/nova/engine/` | deployment orchestration: dependency ordering from depends_on, lifecycle mapping (up = init+plan+apply, down = destroy, ps = state listing), project naming precedence, state dir (WORKDIR) management |
| `src/nova/iac/` | `adapter.py` (IaCAdapter protocol) + `terraform.py` (first adapter: shells out to the `terraform` binary; detects missing binary with install hint) |
| `src/nova/providers/` | provider engine: compose `providers:` block + discovered credentials → generated Terraform provider configuration |
| `src/nova/policy/` | c7n/c7n-left vendoring wrapper (pinned `c7n-left==0.3.39`), builtin policy pack (`src/nova/data/policies/`), `policy check` gate for `up` (`--skip-policy` bypass) |
| `src/nova/data/` | package data: builtin policy pack, version metadata |
## Components
| Component | Description | Boundaries | Depends On |
|-----------|-------------|------------|------------|
| Command Surface | Docker-ergonomic CLI commands (build, up, down, ps, stop, start, restart, rm, logs, config, policy, env, version) with compose-parity flags and help layout | Argument parsing and routing only; no IaC/provider logic | — |
| Novafile Parser | Dockerfile-analog parser; Terraform module registry paths replace container images; container-only instructions warn, COPY/ADD error | Produces module build definitions (stages, ARG defaults, ENV input defaults, WORKDIR state dir) | — |
| Compose Parser | compose.yml loader; compose-indistinguishable schema plus `module` (replaces image) and top-level `providers`; `${VAR}` interpolation with compose operator set and precedence (shell > --env-file > .env) | Produces module deployment specs; discovery mirrors compose (upward search, NOVA_COMPOSE_FILE, -f merge) | — |
| Environment Discovery | Git-context (repo detect/auto-create, remotes, commits), GitHub/Gitea API clients, credential discovery (gh/glab files, cloud creds/env), pipeline detection with PrefillContext | Read-only environment introspection; secrets masked; no deployment logic | — |
| IaC Adapter Interface | Protocol with `init / plan / apply / destroy / show` methods (Terraform first; other IaC types plug in later without redesign) | Contract only; engines implement it | — |
| Terraform Adapter | Implements the protocol by invoking the external `terraform` binary; generates module blocks (`source` + registry `version`) and reads `show -json` for ps | Terraform-specific; kept behind the interface; binary prerequisite detected, never bundled | IaC Adapter Interface |
| Provider Engine | Maps compose `providers:` + discovered credentials to provider-correct Terraform (aws, azurerm, google) | Provider targeting and credential injection only | Terraform Adapter, Compose Parser, Environment Discovery |
| Deployment Engine | Lifecycle semantics (up = init+plan+apply with approval unless -d; down = destroy, -v purges state), depends_on ordering, project naming | Orchestration only; no parsing, no provider details | Parsers, IaC Adapter |
| Policy Engine | Vendored c7n + c7n-left as pinned deps; `c7n-left run -p <policies> -d <tf-dir> --output json` via subprocess; violations block `up` | Policy evaluation; invoked from the command surface | Provider Engine (generated Terraform as input) |
| Release Engineering | Wheel + sdist + PyInstaller one-file linux binary + slim container; gitea release API (POST /repos/{owner}/{repo}/releases, multipart asset attach) | Packaging and release only | — |
## Provider Mapping
| compose `providers` key | Terraform provider source | Credential inputs (discovered) |
|------------------------|--------------------------|-------------------------------|
| `aws` | `hashicorp/aws` | AWS_ACCESS_KEY_ID/SECRET/SESSION_TOKEN env or `~/.aws/credentials` profile; AWS_PROFILE, region |
| `azure` | `hashicorp/azurerm` | ARM_SUBSCRIPTION_ID, ARM_TENANT_ID, ARM_CLIENT_ID, ARM_CLIENT_SECRET env; azureProfile.json for non-secret context |
| `gcp` | `hashicorp/google` | GOOGLE_APPLICATION_CREDENTIALS / gcloud ADC file; GOOGLE_PROJECT/GOOGLE_CLOUD_PROJECT, GOOGLE_REGION |
## Data Flow
User command -> Command Surface -> parse Novafile / compose.yml (interpolation over env + .env +
pipeline prefill) -> Environment Discovery (git context, credentials, pipeline PrefillContext,
forge clients) -> Deployment Engine (ordering, project name) -> Provider Engine (credentials ->
provider config) -> IaC Adapter Interface -> Terraform Adapter -> external `terraform` binary ->
cloud provider (AWS/Azure/GCP). Policy checks (c7n/c7n-left) gate `up` between plan and apply.
`nova env` reports discovery; `nova config` prints the normalized model; release engineering ships
binary + container via the gitea API.
## Build Order
1. Command Surface (P1)
2. Novafile + Compose Parsers (P2)
3. Environment Discovery (P3)
4. IaC Adapter Interface + Terraform Adapter + Provider Engine + Deployment Engine (P4)
5. Policy Engine (P5)
6. Release Engineering (P6)
+8
View File
@@ -0,0 +1,8 @@
{
"phase": 0,
"stage": "ship_pending",
"milestone": "v0.1",
"phase_role": "pre_execution",
"attempts": 0,
"updated_at": "2026-10-14T23:00:00Z"
}
+177
View File
@@ -0,0 +1,177 @@
# CIAgent Grill Report
## Run: 2026-10-14 (mode: mechanical, focus: all)
### Verdict: Proceed (confidence: 0.65)
Pre-implementation review of nova-cli (codebase intentionally empty; implementation starts P1). For a
project with zero code, the evidence base is unusually strong: 27 recorded decisions with rationale,
alternatives, and confidence scores (2cc5e4f, 31722f5, f6954b6); research verified against primary
sources with a 10-row assumption table where every row carries a fallback (RESEARCH.md:336-350); an
explicit out-of-scope table (REQUIREMENTS.md:45-51); task-level done criteria (PLAN.md:5-9); and
per-phase ships under the sponsor-pinned v0.1 minor (D-002, D-019, D-020). The failure modes a status
report would hide are real and concentrated in three places — the release container artifact (cannot
be built in this environment), the never-exercised real-deployment path (mock-only), and release
hygiene on the phase patch line — all received forced dispositions, not scope cuts. No scope creep
detected between specify (2cc5e4f) and plan (50d99ed): REQ-01..11 are unchanged; clarify only narrowed.
Forcing questions scored: 40. Challenges raised: 10 (all forced to dispositions at >=0.60). Escalations: 0.
### Axis 1 — Business Case
- **Q1**: What problem does this solve, and is it still the top priority?
- Evidence: PROJECT.md:5-9; init spec 2cc5e4f. Answer: infra deployment with zero new ergonomics (docker mental model; Terraform module as unit). Unchanged since specify. Conf: 0.70.
- Resolved: sponsor-validated; no competing priority exists in-repo.
- **Q2**: Named sponsor who decides under pressure?
- Evidence: D-002 (2cc5e4f — user override of the computed v1.0 default); clarify session 31722f5: 14 binding scope decisions (D-009..D-022) in one pass. Conf: 0.75.
- Resolved: sponsor demonstrably arbitrates; "support" is evidenced by override behavior, not assertion.
- **Q3**: Impact if cancelled?
- Evidence: PROJECT.md:59 — "nova-cli is the command-line interface for nova-platform". Conf: 0.65.
- Resolved: not a zombie — the platform loses its only CLI front door.
- **Q4**: ROI against a counterfactual?
- Evidence: MISSING — no ROI/cost-benefit artifact anywhere. Answer: none computed; value is sponsor-attested ergonomic leverage. Conf: 0.60.
- Challenge -> G-012: no financial ROI required for internal tooling; go-live adoption review is the payback checkpoint.
### Axis 2 — Scope & Requirements
- **Q1**: Scope expanding, contracting, or stable?
- Evidence: REQ-01..11 identical in 2cc5e4f and 50d99ed; D-009 deferred run/exec/cp. Answer: monotonically contracted, then frozen. Conf: 0.85.
- Resolved: no creep against the D-002 v0.1 pin.
- **Q2**: Requirements owned and frozen — or a moving target? Is "practically indistinguishable" honest?
- Evidence: sponsor clarify 31722f5; but PROJECT.md:5 says "practically the same and almost indistinguishable" while D-010 hard-errors COPY/ADD, D-011 adds module/providers keys, D-015 makes stop/start/rm not-applicable. Conf: 0.75.
- Challenge -> G-002: the claim is one-directional and aspirational; the honest subset is the binding definition and must be enumerated in README + error messages.
- **Q3**: Explicit out-of-scope list?
- Evidence: REQUIREMENTS.md:45-51 (3-row table) + PROJECT.md:44-47. Answer: lifecycle gaps, non-Terraform adapters, run/exec/cp. Conf: 0.85.
- Resolved: someone can answer the question, in writing.
- **Q4**: Hidden requirements disclosed late?
- Evidence: terraform BUSL licensing surfaced pre-build (D-027, f6954b6); secrets masking (D-013); linux-only binary (D-017). Residual late-disclosure risk: gitea runner existence (A8 @0.75). Conf: 0.70.
- Resolved with note: A8 feeds the container-artifact challenge (G-004).
### Axis 3 — Architecture & Feasibility
- **Q1**: Validated by the builders, or only the sellers?
- Evidence: ARCHITECTURE.md refined during research (f6954b6, "P0 refined"); RESEARCH.md verified against HashiCorp/compose-spec/gh/act_runner/PyPI; persona territories match module layout (PERSONAS.md:76-81). Conf: 0.70.
- Resolved: builder == seller here (agent pipeline), so validation is external-source-based; this grill adds the adversarial pass.
- **Q2**: Integration surface enumerated?
- Evidence: ARCHITECTURE.md:24-37 — terraform subprocess, c7n-left subprocess, forge HTTP, credential chains, gitea release API. Each has a RESEARCH section with pitfalls (§1-10). Conf: 0.75.
- Resolved: surface is large but every edge is pre-verified with concrete failure modes.
- **Q3**: Existing system being replaced?
- Evidence: repo root contains only .ciagent/ + README.md — greenfield. Answer: nothing replaced; alternative is raw terraform CLI; no data migration. Conf: 0.80.
- Resolved.
- **Q4**: Inherited technical debt budgeted?
- Evidence: zero code debt (empty src/); carrying costs documented: c7n vendoring ~60-80 MB installed, binary +100-150 MB, container 200-300 MB (RESEARCH.md:286-288, §10). Conf: 0.75.
- Resolved: dependency weight is the accepted, documented cost.
### Axis 4 — People, Skills, Organization
- **Q1**: Key-person dependency?
- Evidence: config.json:15-21 (full autonomy); all five active personas are one agent (PERSONAS.md); planner subagent failed twice and was auto-recovered inline (50d99ed lessons). Conf: 0.60.
- Challenge -> G-010: maximal concentration — one executor. Mitigation: all state is committed artifacts; any agent instance can resume from git.
- **Q2**: Resources actually allocated?
- Evidence: P0 ran specify->clarify->research->plan in 51 minutes across 4 substantive commits (543975c -> 50d99ed, git log). Conf: 0.80.
- Resolved: 100% allocation, evidenced by velocity — not "in name only".
- **Q3**: Product owner with authority?
- Evidence: 31722f5 (14 decisions); D-002 user override of a computed default. Conf: 0.75.
- Resolved: single owner with demonstrated override authority; no committee.
- **Q4**: Building capability they don't have?
- Evidence: every unknown converted to verified fact pre-build — module grammar (D-023), c7n-left `-d` flag catch (D-025), commit.id vs commit.sha (D-026), packed-object fallback requirement (RESEARCH §8). Conf: 0.75.
- Resolved: research-first discipline closed the gaps before code exists.
### Axis 5 — Timeline & Estimates
- **Q1**: Deadline set before scope was understood?
- Evidence: no dates anywhere (ROADMAP.md, PLAN.md). Answer: no calendar deadline exists; execution is phase-gated. Conf: 0.65.
- Resolved: no reverse-engineered date pressure; slippage is visible per phase ship instead.
- **Q2**: Critical path and the 3-month pusher?
- Evidence: PLAN deps P2,P3->P4->P5->P6->P7; P4 is 5 tasks across 3 waves incl. adapter, 3 providers, engine, wiring (PLAN.md:85-99). Conf: 0.70.
- Resolved: P4 is the risk concentration; a provider-generation defect found late is the schedule-killer; mitigation = adapter conformance harness (P04-01-01) + E2E gates per wave.
- **Q3**: Evidence-based estimates?
- Evidence: MISSING — no effort/size estimates per task. Answer: tasks sized as single executor passes with explicit done criteria. Conf: 0.60.
- Challenge -> forced: estimates are absent by design (agent passes, not hours); progress unit = shipped phase patch (D-019/D-020); P4 wave-3 E2E flagged as the estimate-risk concentration.
- **Q4**: Working definition of done?
- Evidence: PLAN.md:5-9 must_haves; per-task Done: lines; phase must-haves; REQ->task traceability (PLAN.md:194-208); allow_skip=false (config.json:23). Conf: 0.85.
- Resolved: "done" is testable and enforced, not whatever the demo shows.
### Axis 6 — Budget & Financial Realism
- **Q1**: Spent vs remaining?
- Evidence: MISSING monetary budget. Answer: none exists; cost is pipeline compute/tokens. Conf: 0.60.
- Challenge -> forced: non-monetary internal project; P0 burn (~1 session) is the baseline; low materiality.
- **Q2**: Cost drivers not in the original budget?
- Evidence: RESEARCH.md:286-288 (29 packages, boto3/botocore, compiled tfparse wheel), §10 (onefile 1-3 s startup, 200-300 MB container). Conf: 0.75.
- Resolved: all predictable drivers documented before first spend.
- **Q3**: Burn rate and runway?
- Evidence: 4 substantive commits in 51 minutes (21:18 -> 22:09, git log). Conf: 0.60.
- Resolved: no runway constraint at current burn.
- **Q4**: Budget contingent on something unbudgeted?
- Evidence: A8 (gitea runner @0.75) with local scripts/release.sh + GITEA_TOKEN fallback (RESEARCH.md:332-334); release_blocking=false. Conf: 0.70.
- Resolved: release path survives the A8 failure mode without new spend.
### Axis 7 — Risks, Assumptions, Dependencies
- **Q1**: Top assumptions with evidence?
- Evidence: RESEARCH.md:336-350 — A1..A10, each with confidence and consequence-if-wrong + fallback. Conf: 0.80.
- Resolved: the best assumption discipline I have seen in a P0 artifact.
- **Q2**: External dependencies?
- Evidence: terraform binary (D-027, external prerequisite), PyPI pin (D-025), gitea API/runner (D-026, A8), cloud credential chains (RESEARCH §5). Conf: 0.75.
- Resolved: each has detection, hint, or fallback.
- **Q3**: The single kill-risk — mitigated or hoped away?
- Evidence: D-027 ("dev environment has no terraform binary") + PLAN.md:165 ("terraform mocked") — the generated-Terraform-to-real-terraform contract is never exercised before go-live. Kill-risk: adapter emits terraform real terraform rejects (or accepts wrongly). Conf: 0.60.
- Challenge -> G-005: environment-forced; compensating controls are verified grammar (§1, D-023), conformance harness, and documented first-real-apply as post-go-live validation.
- **Q4**: Pre-mortem done?
- Evidence: this grill + the assumption-consequence table (RESEARCH.md:336-350) which functions as one. Conf: 0.65.
- Resolved: this report completes it.
### Axis 8 — Governance & Decision-Making
- **Q1**: Decision-maker when executives disagree?
- Evidence: config.json:15-21 — full autonomy, threshold 0.6, hooks [deploy, delete_data, merge_to_main]; D-002 sponsor-override precedent. Conf: 0.70.
- Resolved: sponsor is arbiter; agent auto-decides >=0.6 and records in ---ci--- blocks.
- **Q2**: Cadence and escalation pattern?
- Evidence: stage pipeline specify->clarify->research->plan->grill->execute; max_revision_iterations=3; escalation hooks configured. Conf: 0.70.
- Resolved: per-stage gates; the observed planner failure (50d99ed) was auto-recovered and recorded transparently.
- **Q3**: Status-report omissions?
- Evidence: 50d99ed ---ci--- lessons honestly records a double subagent failure. MISSING: per-phase token/cost and coverage metrics. Conf: 0.65.
- Resolved with note: decisions transparent; operational metrics absent — add coverage % to phase ships.
- **Q4**: Agreed stop-the-project trigger?
- Evidence: MISSING — no explicit milestone kill criteria. Answer: implicit gates only (must-haves, allow_skip=false). Conf: 0.60.
- Challenge -> G-008: bind a standing stop trigger; ensure release_blocking=false (which masks release failures) can never mask a failed phase gate.
### Axis 9 — Adoption & Operational Readiness
- **Q1**: Who uses it, what changes, what's in it for them?
- Evidence: PROJECT.md:5,9; PLAN.md:149-161 user surface; README quickstart task P06-01-05. Answer: docker-fluent developers; they write Novafile/compose.yml instead of .tf; incentive = zero new ergonomics. Conf: 0.65.
- Resolved with note: no beta users planned before go-live; adoption is asserted, not evidenced.
- **Q2**: Ops involved now or handed a finished product?
- Evidence: the product is itself an infra tool; release-engineer persona owns operational artifacts from P6 start (PERSONAS.md:41-44). Conf: 0.60.
- Resolved-ish: no separate ops team exists; packaging owner is in the loop at build time.
- **Q3**: Rollback plan?
- Evidence: PLAN.md:178 (down -v reverse-order destroy, state purge); D-015 explicit not-applicable errors. MISSING: state-recovery documentation; remote state backends unaddressed. Conf: 0.60.
- Challenge -> G-009: terraform-native recovery from local WORKDIR state is the only path; must be documented; remote backends stay post-go-live.
- **Q4**: Success criteria validated with the judges?
- Evidence: sponsor-validated requirements (PROJECT.md:13-24); PLAN UX acceptance criteria; this grill binds go-live criteria. Conf: 0.65.
- Resolved: the sponsor is the judge and has seen the criteria; no external judges exist.
### Meta — Closing Review
- **Q1**: Auditor flags?
- Evidence: three material flags: (1) REQUIREMENTS.md:35 "Dockerfile-built container image" vs no docker daemon in dev env + A8 @0.75 (RESEARCH.md:325-326); (2) real deploy never exercised (D-027); (3) D-019 ships v0.1.0..v0.1.6 as releases while P1 commands emit "landing in P4" errors (PLAN.md:30). Conf: 0.65.
- Challenge -> G-004, G-005, G-003 respectively.
- **Q2**: What is the project NOT doing?
- Evidence: no remote-state backends (local WORKDIR state only), no real-cloud smoke, no macOS/Windows binaries (D-017 linux-only), no behavior benchmark against real docker compose, no token-cost tracking. Conf: 0.70.
- Resolved as flags: most consequential omission is remote state — acceptable under the deferred-lifecycle out-of-scope row, but it caps serious production adoption until post-go-live.
- **Q3**: Simplest 80% version?
- Evidence: derivable — parsers + lifecycle + AWS-only adapter + wheel-only releases (drop forge/pipeline/policy/Azure/GCP/binary/container). Conf: 0.70.
- Challenge -> G-011: the fallback exists and is real, but sponsor scope stands; reduction requires sponsor instruction, not agent initiative.
- **Q4**: What must be true for 90-day success — is it true today?
- Evidence: P0 velocity (51 min, 4 commits); conditions: c7n-left==0.3.39 resolves on target (D-025 verified via PyPI), mocks hold through P4, A8 true or release.sh fallback works. Conf: 0.70.
- Resolved: all conditions have recorded, verified fallbacks; nothing required is currently false.
### Binding Decisions
| ID | Axis | Decision | Confidence |
|-----|------|----------|-----------|
| G-001 | 2 | Scope frozen at sponsor-validated v0.1 (D-002); no growth detected specify->plan; the out-of-scope table binds all later phases | 0.85 |
| G-002 | 2 | "Practically indistinguishable" is a one-directional subset claim; D-010/D-011/D-015 deltas are the binding definition and must be enumerated in README + error messages | 0.75 |
| G-003 | 8 | Phase patches v0.1.0..v0.1.6 marked prerelease=true on gitea; v0.1.7 is the first stable go-live release (D-019 intact; gitea API supports prerelease, RESEARCH.md:321) | 0.70 |
| G-004 | 6/9 | REQ-06 container artifact = shipped Dockerfile + build script guaranteed; image build/push conditional on daemon or runner (A8); image outcome reported in every release summary — never silently absent | 0.60 |
| G-005 | 7 | Go-live ships with mock-only deployment validation (no terraform/cloud in dev env, D-027); first real apply is an explicit documented post-go-live validation step (README + release notes) | 0.60 |
| G-006 | 3 | Terraform stays external (BUSL, D-027/A5); never bundled in binary or container; install-hint error is the tested path; OpenTofu bundling is a post-go-live decision | 0.80 |
| G-007 | 6 | c7n-left==0.3.39 pin holds (D-025) with dot-notation builtin policies; binary bloat (+100-150 MB) accepted | 0.85 |
| G-008 | 8 | Standing stop trigger: 3 failed revision iterations on any task, or a Rethink grill verdict, halts the milestone and returns to the sponsor; release_blocking=false masks release failures only — never gate failures | 0.60 |
| G-009 | 9 | v0.1 is local-state only (WORKDIR state dir); README must document state location, terraform-native recovery, and down -v semantics; remote backends remain post-go-live scope | 0.65 |
| G-010 | 4 | Single-agent key-person risk accepted; the committed artifact trail + stage gates make the pipeline resumable by any agent instance | 0.65 |
| G-011 | Meta | 80% fallback (parsers + lifecycle + AWS-only adapter + wheel releases) recorded as contingency only; activation requires sponsor instruction | 0.70 |
| G-012 | 1 | No financial ROI computed; sponsor attestation + cancellation-impact test substitutes; post-go-live adoption review validates the case | 0.60 |
### Escalations
- None. All 10 challenges were forced to dispositions at >=0.60 confidence per the full-autonomy preference. The weakest bindings (G-004, G-005, G-008 at 0.60) should be re-checked at the P4 and P6 gates: G-004 at the P6 release dry-run, G-005 at the P4 adapter conformance pass, G-008 at every phase review.
+91
View File
@@ -0,0 +1,91 @@
---
# Nova CLI — Persona configuration (produced at end of P0 RESEARCH)
# Format: YAML frontmatter with active, phase_specific, reason fields.
personas:
lead-developer:
domain: coordination
active: true
phase_specific: false
reason: Task decomposition, territory arbitration, and phase coordination
frameworks: [pytest, click, pyyaml]
constraints: [docker-ergonomic-parity, vertical-slices, minimal-dependencies]
territory: ["src/nova/**", "tests/**", "pyproject.toml"]
backend-engineer:
domain: backend
active: true
phase_specific: false
reason: Environment discovery (git-context, forge clients, credentials, pipeline detection) and the IaC adapter + provider engine are backend services
frameworks: [click, pyyaml, urllib, zlib, subprocess]
constraints: [secrets-masked, read-only-introspection, adapter-behind-interface, stdlib-http]
territory: ["src/nova/env/**", "src/nova/iac/**", "src/nova/providers/**", "src/nova/engine/**", "tests/env/**", "tests/iac/**", "tests/providers/**", "tests/engine/**"]
cli-engineer:
domain: backend
active: true
phase_specific: false
reason: Custom persona — CLI-only project; owns the docker-ergonomic command surface, flag layer, and file parsers (UX-first)
frameworks: [click, pyyaml]
constraints: [ux-first, docker-flag-parity, compose-style-errors, minimal-dependencies]
territory: ["src/nova/cli/**", "src/nova/parsers/**", "tests/cli/**", "tests/parsers/**"]
policy-engineer:
domain: backend
active: true
phase_specific: false
reason: Custom persona — owns the vendored c7n/c7n-left policy engine and builtin policy pack (P5)
frameworks: [c7n, c7n-left, subprocess]
constraints: [pinned-vendor-versions, gate-up-unless-skip, dot-notation-resources]
territory: ["src/nova/policy/**", "src/nova/data/policies/**", "tests/policy/**"]
release-engineer:
domain: backend
active: true
phase_specific: false
reason: Custom persona — owns packaging, PyInstaller binary, container image, and gitea release automation (P6)
frameworks: [pyinstaller, setuptools, gitea-api]
constraints: [binary-plus-container-every-release, reproducible-release-script, no-secrets-in-artifacts]
territory: ["scripts/**", "build/**", "Dockerfile", ".gitea/workflows/**", "pyproject.toml", "tests/release/**"]
data-engineer:
domain: data
active: false
phase_specific: false
reason: Deactivated — no persistence layer, ORM, or schema migrations in nova-cli; file parsing is owned by cli-engineer
frameworks: []
constraints: []
territory: []
frontend-engineer:
domain: frontend
active: false
phase_specific: false
reason: Deactivated — no UI components, pages/, or frontend frameworks; nova-cli is a terminal CLI
frameworks: []
constraints: []
territory: []
---
# Nova CLI — Personas
## Roster rationale
- The four default personas were assessed against the actual project shape: a Python src-layout
CLI with no UI and no datastore. `frontend-engineer` is deactivated per the deactivation rule
(no React/Vue/Svelte files, no pages/components dirs, no frontend frameworks). `data-engineer`
is deactivated with cause: there is no persistence layer; compose/Novafile parsing is UX surface
owned by `cli-engineer`.
- Three custom personas were created for domains beyond the default four (activation rule):
`cli-engineer` (command surface + parsers, UX-first), `policy-engineer` (vendored c7n/c7n-left
gating), and `release-engineer` (binary + container release engineering).
- Frameworks were aligned to actual project dependencies (click, PyYAML, pytest; pinned
c7n-left==0.3.39 per RESEARCH.md) rather than static defaults.
- Territories were aligned to the ARCHITECTURE.md module layout (`src/nova/{cli,parsers,env,iac,
providers,engine,policy,data}`); no persona claims paths that do not exist in the plan.
- `pyproject.toml` is shared between `lead-developer` and `release-engineer` (packaging metadata);
conflicts resolve to `release-engineer` ownership for packaging changes, `lead-developer` for
dependency pins — recorded to prevent territory drift.
- No phase-specific personas were needed: P3 credential handling is covered by backend-engineer
under the `secrets-masked` constraint, and verification layers run STRIDE analysis on every
phase anyway.
## Execution order (domain priority)
coordination → backend → custom domains alphabetical (cli-engineer, policy-engineer, release-engineer).
Deactivated personas (data-engineer, frontend-engineer) receive no tasks; they are listed here with
`active: false` and the reason recorded above.
+208
View File
@@ -0,0 +1,208 @@
---
milestone: v0.1
milestone_type: feature
tag_line: "phase N ships v0.1.N; final phase patch v0.1.7 IS the go-live release (D-019)"
must_haves:
- ".venv/bin/python -m pytest -q is green across tests/cli, tests/parsers, tests/env, tests/iac, tests/providers, tests/engine, tests/policy, tests/release"
- "Every REQ-01..REQ-11 success criterion is demonstrated by at least one passing test or verifiable artifact"
- "nova --help, nova config, nova env git, nova policy version, nova version all work against a fixture project"
- "Wheel + dist/nova PyInstaller binary build and pass their smoke tests; scripts/release.sh --dry-run completes"
---
# Nova CLI — Milestone v0.1 Execution Plan
Vertical-slice plan for P1..P6. Each task is one executor pass, ends testable, maps to REQ IDs,
and is owned by one active persona (PERSONAS.md). Test command: `.venv/bin/python -m pytest`.
Task ID format: `P##-##-##` (phase-plan-task). Execution order: coordination -> backend ->
custom alphabetical (cli-engineer, policy-engineer, release-engineer).
## Phase 1 — CLI Skeleton & Docker-Ergonomic Command Surface
- Goal: installable `nova` CLI with the full docker compose lifecycle surface, routing wired, config discovery working
- Requirements: REQ-01, REQ-05
- Depends on: none
- Owner: cli-engineer
### Wave 1
- **P01-01-01** Scaffold Python package + packaging — create `pyproject.toml` (src-layout, `nova = nova.cli:main`, deps click+PyYAML, pytest/mypy config), `src/nova/__init__.py` (`__version__`), `src/nova/cli/__init__.py`, `src/nova/py.typed`. REQ-05. Done: `.venv/bin/pip install -e .` succeeds; `.venv/bin/python -c "import nova; assert nova.__version__"`.
- **P01-01-02** Click command surface — `src/nova/cli/main.py` + `src/nova/cli/commands/`: groups `build, up, down, ps, stop, start, restart, rm, logs, config` + `policy`, `env` groups + `version`; docker-parity options `-f/--file`, `-p/--project-name`, `-d/--detach`, `--env-file`, `--profile`; docker-style help. REQ-01. Done: `pytest tests/cli/test_main.py -q` green (registration, help shape, flag parsing).
- **P01-01-03** Config file discovery — `src/nova/cli/config_files.py`: compose.yaml > compose.yml preference, `-f` override (multi-file accept), `NOVA_COMPOSE_FILE` env, upward directory search; Novafile discovery (`./Novafile`, `-f` override). REQ-01. Done: `pytest tests/cli/test_config_files.py -q` green.
- **P01-01-04** Command behaviors + version — `src/nova/cli/commands/version.py` (package metadata); engine-pending commands validate args, run discovery, and emit precise compose-style errors naming the landing phase (P2 parsers / P4 engine) — deliberate, tested UX. REQ-01. Done: `pytest tests/cli/test_commands.py -q` green.
### Phase must-haves
- `nova --help` lists every lifecycle command with docker-parity flags
- `nova version` prints the package version
- `pytest tests/cli -q` green
## Phase 2 — Novafile + Compose.yml Parsers
- Goal: Novafile parses as a Dockerfile analog; compose.yml loads a compose-indistinguishable schema; `nova config` prints normalized config
- Requirements: REQ-02, REQ-03
- Depends on: P1
- Owner: cli-engineer
### Wave 1
- **P02-01-01** Module reference resolver — `src/nova/parsers/moduleref.py` + `tests/parsers/test_moduleref.py`: parse/normalize `[host/]ns/name/provider[//subdir][@ver]`, registry URLs → shorthand, local `./` `../` absolute; `@ver` sugar → version field (registry-only) per D-023; compose-style errors for 2-segment refs. REQ-02, REQ-04 (grammar basis). Done: `pytest tests/parsers/test_moduleref.py -q` green.
- **P02-01-02** Novafile parser — `src/nova/parsers/novafile.py` + `tests/parsers/test_novafile.py`: case-insensitive instructions, `#` comments, `\` continuations, `ENV k=v`/`ENV k v`, `ARG name[=default]`, `FROM <source> [AS name]`, `LABEL k=v`, `WORKDIR`; BuildKit-style `Novafile:<line>:<col>` errors; container-only instructions (RUN/CMD/ENTRYPOINT/HEALTHCHECK/EXPOSE) warn; COPY/ADD hard error; emits `NovafileModel` (stages, args, env defaults, labels, workdir). REQ-02. Done: `pytest tests/parsers/test_novafile.py -q` green.
### Wave 2
- **P02-01-03** Compose schema + interpolation + models — `src/nova/parsers/compose.py`, `src/nova/parsers/interp.py` (`${VAR}`, `${VAR:-def}`, `${VAR:?err}`, `${VAR:+alt}`, `.env` + `--env-file` loading, precedence shell > env-file > .env), `src/nova/parsers/models.py` (ComposeSpec, ServiceSpec, ProviderConfig dataclasses with validation; keys: name, services.module|build, environment map/list, env_file, depends_on short/long, working_dir, profiles, providers per D-011/D-024) + tests. REQ-03. Done: `pytest tests/parsers/test_compose.py tests/parsers/test_interp.py -q` green.
- **P02-01-04** Wire config/build commands — `src/nova/cli/commands/config.py` prints normalized compose (like `docker compose config`); `src/nova/cli/commands/build.py` parses Novafile → resolved module manifest; both use P1 discovery. Done: `pytest tests/cli/test_config_cmd.py -q` green; fixture compose.yml round-trips through `nova config`.
### Phase must-haves
- `pytest tests/parsers tests/cli -q` green
- `nova config` on a fixture project prints normalized, interpolated compose config
- Novafile parse errors carry `Novafile:<line>:<col>` positions
## Phase 3 — Environment Discovery Layer
- Goal: git-context, forge API clients, credential discovery, pipeline detection with env prefill
- Requirements: REQ-08, REQ-09, REQ-10, REQ-11
- Depends on: P1
- Owner: backend-engineer
### Wave 1
- **P03-01-01** Git context — `src/nova/env/git.py` + `tests/env/test_git.py`: pure-python `.git` parse (config INI, HEAD, refs/heads, packed-refs, loose zlib commit objects), `git` subprocess fallback for packed objects, repo detection walking up, auto-init with visible notice (D-021); GitContext (is_repo, remotes, branch, commit sha/message). REQ-09. Done: `pytest tests/env/test_git.py -q` green against fixture repos (real `git init` fixture + synthetic loose-object fixture).
- **P03-01-02** Credential discovery — `src/nova/env/credentials.py` + `tests/env/test_credentials.py`: gh `~/.config/gh/hosts.yml`, glab `~/.config/glab-cli/config.yml`, AWS `~/.aws/credentials` + `AWS_*` env, Azure `ARM_*`/`AZURE_*` env + profile existence, GCP gcloud ADC + `GOOGLE_*` env (D-013); CredentialReport with found/missing per source, values masked. REQ-10. Done: `pytest tests/env/test_credentials.py -q` green with monkeypatched HOME/env; output never contains raw secret values.
### Wave 2
- **P03-01-03** Pipeline detection + prefill — `src/nova/env/pipeline.py` + `tests/env/test_pipeline.py`: detect GITHUB_ACTIONS / GITEA_ACTIONS / GITLAB_CI; PrefillContext (repo slug, ref, sha, server URL, API URL, token hint, platform) per D-014/D-026; prefill values feed interpolation. REQ-11. Done: `pytest tests/env/test_pipeline.py -q` green.
- **P03-01-04** Forge clients — `src/nova/env/forge.py` + `tests/env/test_forge.py`: stdlib HTTP; GitHub (api.github.com, Bearer, `per_page`), Gitea (`<host>/api/v1`, `token` auth, `limit`, commit.id→sha normalization); read surface: get_repo, get_branch, list_commits, list_releases/latest_release (D-022). REQ-08. Done: `pytest tests/env/test_forge.py -q` green with mocked urlopen.
- **P03-01-05** `nova env` command group — `src/nova/cli/commands/env.py` + `tests/cli/test_env_cmd.py`: subcommands git (status/remotes/commit + `env git init`), credentials (masked report), pipeline (detected context), forge (connectivity/auth probe). REQ-09, REQ-10, REQ-11 surfaces. Done: `pytest tests/cli/test_env_cmd.py -q` green; `nova env git` in the nova-cli repo prints the origin remote.
### Phase must-haves
- `pytest tests/env tests/cli -q` green
- `nova env credentials` masks all secret values
- `nova env git` works on the nova-cli repository itself
## Phase 4 — IaC Adapter Interface + Terraform Adapter + Provider Engine
- Goal: modules deploy to AWS/Azure/GCP through compose-like lifecycle commands behind a pluggable adapter
- Requirements: REQ-04
- Depends on: P2, P3
- Owner: backend-engineer
### Wave 1
- **P04-01-01** Adapter interface — `src/nova/iac/adapter.py` + `tests/iac/test_adapter.py`: `IaCAdapter` protocol (`init, plan, apply, destroy, show`), `BackendContext` dataclass, conformance test harness (future IaC types implement it without redesign, D-008). Done: `pytest tests/iac/test_adapter.py -q` green (NullAdapter conformance).
- **P04-01-02** Provider engine — `src/nova/providers/{base,aws,azure,gcp}.py` + `tests/providers/test_providers.py`: compose `providers:` + CredentialReport → `required_providers` + provider blocks; aws→hashicorp/aws, azure→hashicorp/azurerm, gcp→hashicorp/google with credential env mapping (ARCHITECTURE.md table). REQ-04. Done: `pytest tests/providers/test_providers.py -q` green.
### Wave 2
- **P04-01-03** Terraform adapter — `src/nova/iac/terraform.py` + `tests/iac/test_terraform.py`: main.tf generation (terraform block, required_providers, provider configs, module blocks with source/version from ModuleRef), subprocess invocation (`init -input=false`, `plan -detailed-exitcode -out=`, `apply [-auto-approve]`, `destroy -auto-approve`, `show -json`), missing-binary error with install hint (D-027), JSON output parsing. REQ-04. Done: `pytest tests/iac/test_terraform.py -q` green (argv/cwd/env assertions + generated tf content with mocked subprocess).
- **P04-01-04** Deployment engine — `src/nova/engine/lifecycle.py` + `tests/engine/test_lifecycle.py`: depends_on topological order; `up` = init+plan+apply (approval unless `-d`; policy-gate hook point), `down` = reverse destroy (`-v` purges state dir), `ps` = aggregate `show -json` outputs, `logs` = apply log tail, `stop/start/restart/rm` = explicit not-applicable errors (D-015); project naming `-p` precedence. REQ-04. Done: `pytest tests/engine/test_lifecycle.py -q` green with a fake adapter.
### Wave 3
- **P04-01-05** Wire lifecycle commands — `src/nova/cli/commands/{up,down,ps,logs,stop,start,restart,rm}.py`: replace P1 pending errors with engine calls. Done: `pytest tests/cli -q` green; end-to-end: fixture compose.yml + mocked terraform → `nova up -d` succeeds and `nova ps` lists the service.
### Phase must-haves
- `pytest tests/iac tests/providers tests/engine tests/cli -q` green
- TerraformAdapter passes the adapter conformance harness
- End-to-end `up`/`ps`/`down` happy path with mocked terraform binary
## Phase 5 — Vendored c7n + c7n-left Policy Checks
- Goal: policy checks gate deployments from engines vendored into the CLI
- Requirements: REQ-07
- Depends on: P4
- Owner: policy-engineer
### Wave 1
- **P05-01-01** Policy engine — pin `c7n-left==0.3.39` (propagates `c7n==0.9.53`, D-025) in `pyproject.toml`; install into venv; `src/nova/policy/engine.py` + `tests/policy/test_engine.py`: policy pack resolution (user `.nova/policies/` > builtin `src/nova/data/policies/`), subprocess `c7n-left run -p <policies> -d <tf-dir> --output json`, violation parsing → PolicyResult. REQ-07. Done: `pytest tests/policy/test_engine.py -q` green; `.venv/bin/c7n-left --help` exits 0.
- **P05-01-02** Builtin pack + commands — `src/nova/policy/pack.py`, `src/nova/data/policies/*.yaml` (dot notation `terraform.<type>` per D-025; e.g. required tags, no unencrypted storage), `src/nova/cli/commands/policy.py` + `tests/policy/test_pack.py`: `nova policy check [target]`, `nova policy version` (reports vendored c7n/c7n-left versions). REQ-07. Done: `pytest tests/policy -q` green with mocked subprocess.
### Wave 2
- **P05-01-03** Deployment gate — `src/nova/engine/lifecycle.py` update + `tests/policy/test_gate.py`: policy check runs between plan and apply; violations block `up` with compose-style error; `--skip-policy` bypasses. REQ-07. Done: `pytest tests/policy/test_gate.py -q` green.
### Phase must-haves
- c7n + c7n-left vendored as pinned dependencies, importable in the venv
- `pytest tests/policy tests/engine -q` green
- policy gate blocks `up` on violations; `--skip-policy` proceeds
## Phase 6 — Release Engineering & Go-Live Prep
- Goal: every release ships the CLI binary plus the CLI in a container, reproducibly
- Requirements: REQ-06
- Depends on: P1, P2, P3, P4, P5
- Owner: release-engineer
### Wave 1
- **P06-01-01** Packaging finalize + wheel — `pyproject.toml` (include policy pack + py.typed as package data), `scripts/build.sh` (clean + build sdist/wheel); verify the wheel installs into a scratch venv and `nova version` runs from it. REQ-06. Done: build succeeds; scratch-venv smoke passes.
- **P06-01-02** PyInstaller binary — `build/nova.spec` + `scripts/build-binary.sh` → `dist/nova` one-file linux binary; smoke `dist/nova version`. REQ-06. Done: script completes; `dist/nova version` prints the version.
### Wave 2
- **P06-01-03** Container image definition — `Dockerfile` (multi-stage: builder installs wheel, runtime `python:3.11-slim` + nova, `ENTRYPOINT ["nova"]`), `scripts/docker-build.sh` (image tar when docker available), `tests/release/test_dockerfile.py` (structure/parse assertions — no docker daemon in dev env). REQ-06. Done: `pytest tests/release/test_dockerfile.py -q` green.
- **P06-01-04** Release pipeline — `scripts/release.sh`: pytest → wheel → binary → docker (if available) → gitea release create + asset upload via API (GITEA_TOKEN resolved only from `.ciagent/.env.secrets` / `.env*` files, bounded retries per D-020); `.gitea/workflows/release.yml` (on tag push, same pipeline); `tests/release/test_release_script.py` (dry-run assertions + YAML parse). REQ-06. Done: `pytest tests/release -q` green; `scripts/release.sh --dry-run` prints planned steps.
- **P06-01-05** README quickstart — `README.md`: install from release, Novafile + compose.yml example, `nova build/config/env credentials/up -d/ps/down -v` walkthrough, artifact inventory. REQ-01 surface. Done: quickstart commands verified against `nova --help`.
### Phase must-haves
- wheel + `dist/nova` binary build and pass smoke tests
- `scripts/release.sh --dry-run` completes end-to-end
- `.gitea/workflows/release.yml` parses as YAML; README quickstart present
## Final Phase (P7)
Orchestrated by the run workflow — no implementation tasks: multi-persona code review across the
milestone, project health audit, P0/P1 fixes in-phase, then milestone ship: merge phase/07 →
milestone/v0.1-go-live → main, tag `v0.1.7` (the go-live release per D-019), create the gitea
release with full milestone summary + artifacts, delete all milestone/phase branches, mark
REQ-01..REQ-11 complete in REQUIREMENTS.md, mark the milestone complete in ROADMAP.md.
## User-Facing Surface
The milestone's user-facing surface is the `nova` CLI (docker-ergonomic):
- Lifecycle commands mirroring docker compose: `nova build`, `nova up [-d]`, `nova down [-v]`,
`nova ps`, `nova stop`, `nova start`, `nova restart`, `nova rm`, `nova logs`, `nova config`
- Docker-parity flags: `-f/--file`, `-p/--project-name`, `-d/--detach`, `--env-file`, `--profile`
- Discovery: `nova env git [init]`, `nova env credentials`, `nova env pipeline`, `nova env forge`
- Policy: `nova policy check`, `nova policy version`; gate flag `--skip-policy` on `up`
- Meta: `nova version`, `nova --help` (docker-style help layout)
- Files: `Novafile` (Dockerfile analog), `compose.yml` (compose-indistinguishable), `.env`
- README quickstart documenting install + first deploy; shipped release artifacts: wheel, one-file
linux binary, container image definition + build script
## Happy Path
Written BEFORE execution; exercised end-to-end (terraform mocked) in P4/P5 tests:
1. `mkdir demo && cd demo` — developer writes `Novafile` (`FROM terraform-aws-modules/vpc/aws@5.0.0`)
and `compose.yml` (`services: vpc: {module: ./, environment: {name: demo}, provider: aws}`,
`providers: aws: {region: us-east-1}`)
2. `nova build` → parses Novafile, resolves the registry module ref, prints the module manifest
3. `nova config` → prints the normalized, interpolated compose config (like `docker compose config`)
4. `nova env credentials` → reports gh/glab/cloud credential sources, values masked
5. `nova up -d` → discovers compose.yml, orders services, generates Terraform (provider aws +
module block), runs policy check (c7n-left), then init + plan + apply through the Terraform
adapter; detached mode auto-approves; success message lists deployed services
6. `nova ps` → lists deployed services with outputs from `terraform show -json`
7. `nova logs <service>` → tails the adapter's apply log
8. `nova down -v` → destroys in reverse dependency order, purges state directory
Observable outcomes: each command prints docker-style progress and errors; a missing terraform
binary yields a precise install-hint error; policy violations block `up` unless `--skip-policy`.
## UX Acceptance Criteria
- `nova --help` and per-command help mirror docker compose structure and terminology
- Flags match docker parity: `-f`, `-p`, `-d`, `--env-file`, `--profile`, `-v` on down
- File errors are compose-style and name the file + reason (e.g. missing compose.yml, bad
Novafile line with `Novafile:<line>:<col>`)
- `nova config` output is valid, normalized YAML a docker compose user can read at a glance
- No secret value is ever printed: `nova env credentials` masks all discovered tokens/keys
- Missing external prerequisites (terraform, c7n-left) produce actionable errors with install
hints, never stack traces
- Command failures exit non-zero with concise docker-style messages; success output is terse
## Traceability
| REQ | Phase | Tasks |
|-----|-------|-------|
| REQ-01 | P1 (surface), P2 (config), P4 (wiring), P6 (README) | P01-01-02, P01-01-03, P01-01-04, P02-01-04, P04-01-05, P06-01-05 |
| REQ-02 | P2 | P02-01-01, P02-01-02, P02-01-04 |
| REQ-03 | P2 | P02-01-03, P02-01-04 |
| REQ-04 | P4 | P04-01-01, P04-01-02, P04-01-03, P04-01-04, P04-01-05 |
| REQ-05 | P1 | P01-01-01 (also every phase's pyproject/test discipline) |
| REQ-06 | P6 | P06-01-01, P06-01-02, P06-01-03, P06-01-04 |
| REQ-07 | P5 | P05-01-01, P05-01-02, P05-01-03 |
| REQ-08 | P3 | P03-01-04, P03-01-05 |
| REQ-09 | P3 | P03-01-01, P03-01-05 |
| REQ-10 | P3 | P03-01-02, P03-01-05 |
| REQ-11 | P3 | P03-01-03, P03-01-05 |
+69
View File
@@ -0,0 +1,69 @@
# Nova CLI
## Vision
Nova CLI brings Docker and Docker Compose ergonomics to infrastructure deployment. Instead of deploying containers, nova deploys infrastructure modules — referenced by Terraform module registry paths — to cloud providers (AWS, Azure, GCP). The Novafile (the Dockerfile analog) and compose.yml are practically the same and almost indistinguishable from their Docker counterparts, so developers use the mental model, file formats, and commands they already know.
## Core Value
Infrastructure deployment with zero new ergonomics: nova commands mirror their docker equivalents, but the unit of deployment is a Terraform module, not a container.
## Requirements
### Validated
- Complete adoption of Docker and Docker Compose ergonomics (commands, files, workflows)
- Novafile and compose.yml practically the same and almost indistinguishable from Docker equivalents, with Terraform module registry paths instead of containers
- Deploy infrastructure modules to AWS, Azure, GCP
- Native GitHub and Gitea API support
- Local .git config support: detect whether in a git repo (create one if not); discover remotes and commits
- Credential discovery: gh / glab CLI stored credentials; AWS / Azure / GCP stored credentials or env variables
- Pipeline detection (e.g., GitHub Actions): retrieve env / rely on env to pre-fill missing data for infrastructure modules
- Vendored c7n + c7n-left into the CLI for policy checks
- Python implementation
- Every release includes the CLI binary plus the CLI deployed in a container
### Active
The v0.1 scope is clarified (D-009..D-022). Refined scope:
- Command surface mirrors the docker compose core lifecycle set: `build`, `up`, `down`, `ps`, `stop`, `start`, `restart`, `rm`, `logs`, `config`, plus `nova policy` (REQ-07), `nova env` (REQ-08..11 discovery), and `nova version` — with docker-parity flags (`-f/--file`, `-p/--project-name`, `-d/--detach`, `--env-file`, `--profile`). `docker run/exec/cp` semantics are deferred: containers are not the unit of deployment (D-009)
- Novafile supports a Dockerfile-compatible instruction subset: `FROM` (module source, `AS` alias), `ARG`, `ENV` (module input defaults), `LABEL`, `WORKDIR` (state directory), comments, line continuations; container-only instructions (`RUN`, `CMD`, `ENTRYPOINT`, `HEALTHCHECK`, `EXPOSE`) are accepted with warnings; `COPY`/`ADD` are hard errors (D-010)
- compose.yml keeps docker compose shape: top-level `name` and `services`, each service with `module` (replaces `image`) or `build` (Novafile), `environment` (module input vars), `env_file`, `depends_on`, `workdir`, `profiles`; top-level `providers` block (aws/azure/gcp) is the minimal extension; `${VAR}` / `${VAR:-default}` interpolation; `vars` accepted as documented alias of `environment` (D-011)
- Module references: local paths (`./`, `../`, absolute) and Terraform registry paths — shorthand `namespace/module/provider[//subpath][@version]` and full registry URLs — with version pinning via `version:` (D-012)
- Lifecycle semantics: `up` = init + plan + apply (interactive approval unless `-d`), `down` = destroy (`-v` purges state), `ps` = deployment/state listing, `build` = Novafile → resolved module manifest; `stop`/`start`/`restart` return explicit "not applicable to infrastructure modules" errors in v0.1 (D-015)
- Credential discovery is file + environment based only (gh/glab config files; AWS credentials file/env; Azure env/profile; GCP gcloud ADC/env); no OS keychain; secret values are never printed (D-013)
- Pipeline detection covers GitHub Actions, Gitea Actions, GitLab CI; CI env pre-fills forge context (repo, ref, sha, API base, token) (D-014)
- Native GitHub + Gitea API clients (stdlib HTTP) with read surface: repo info, branches, commits, releases (D-022)
- Git-context: pure-python `.git` parsing (config, HEAD, refs, commit objects) with `git` subprocess fallback; repo auto-creation only when a command requires git context and none exists, with visible notice (D-021)
- c7n + c7n-left vendored as pinned Python dependencies of the CLI; `nova policy check` gates deployments (skippable via `--skip-policy`) (D-018)
- Python 3.10+ (3.11 dev), click + PyYAML, pyproject packaging, console entry point `nova` (D-016)
- Every release ships: wheel + sdist + PyInstaller one-file binary (linux amd64) + container image built from a Dockerfile; distributed via Gitea releases (D-017)
- Tag line: all phases ship patches on `v0.1.x`; the final phase patch IS the go-live release (D-019, per D-002 "remain on this minor until go-live")
### Out of Scope
- Full infrastructure lifecycle support — known gaps are handled in a future milestone after go-live
- Non-Terraform IaC adapters — the interface/adapter pattern ships in v0.1; additional IaC types (e.g., Pulumi) are future work
## Constraints
- Code must be Python
- Novafile and compose.yml must remain practically the same and almost indistinguishable from Docker Compose
- The IaC engine must follow an interface/adapter pattern — Terraform is the first adapter; other IaC types must be pluggable later without redesign
- First milestone is v0.1 and the project remains on this minor until go-live
- Releases must always include the CLI binary and the CLI deployed in a container
## Context
nova-cli is the command-line interface for nova-platform. Hosted on gitea at git.coreci.dev (owner: coreci, repo: nova-cli). Milestone v0.1-go-live is the first milestone; phase 0 (pre-execution) captures the specify -> clarify -> research -> plan -> grill pipeline artifacts. Obvious gaps in full infrastructure lifecycle support are intentionally deferred to a post-go-live milestone.
## Key Decisions
| Decision | Rationale | Outcome |
|----------|-----------|---------|
| Milestone v0.1, remain on minor until go-live | User-specified versioning strategy | Recorded in config.json; branch milestone/v0.1-go-live; tag line v0.1.x (D-002, D-019) |
| IaC interface/adapter pattern, Terraform first | Begin with Terraform, other IaC types later, without redesign | ARCHITECTURE.md defines the adapter boundary |
| Docker-ergonomic parity over lifecycle completeness | Known lifecycle gaps deferred post-go-live | Scope constraint for v0.1 |
| Environment discovery layer | git-context, forge APIs, credentials, pipeline env prefill as one subsystem | Phase 3 of the roadmap |
| Ship per phase, release best-effort (D-020) | config.ship: per_phase=true, allow_skip=false, max_release_retries=3, release_blocking=false | Pipeline ships v0.1.0..v0.1.7 without halting on release failure |
+67
View File
@@ -0,0 +1,67 @@
# Nova CLI — Requirements
## v1
### Ergonomics & Formats
- REQ-01: Completely adopt Docker and Docker Compose ergonomics — command structure and UX practically indistinguishable from `docker` / `docker compose`
- Success criteria: `nova` exposes the docker compose core lifecycle set (build, up, down, ps, stop, start, restart, rm, logs, config) with docker-parity flags (`-f`, `-p`, `-d`, `--env-file`, `--profile`); `--help` output mirrors docker compose help structure; config discovery follows compose rules (cwd, `-f` override, `NOVA_COMPOSE_FILE`, upward search)
- REQ-02: Novafile (the Dockerfile analog) — practically the same as a Dockerfile, except Terraform module registry paths replace container images
- Success criteria: Dockerfile-compatible line grammar (case-insensitive instructions, `#` comments, `\` continuations); FROM/ARG/ENV/LABEL/WORKDIR supported; registry paths/local paths in FROM; Dockerfile-style error messages with line numbers; container-only instructions warn, COPY/ADD error
- REQ-03: compose.yml — practically the same and almost indistinguishable from docker compose, deploying infrastructure modules
- Success criteria: YAML loads via docker-compose-shaped schema (name, services with `module`|`build`, environment, env_file, depends_on, workdir, profiles; providers block); `${VAR}`/`${VAR:-default}` interpolation; schema violations produce compose-style errors; `nova config` prints the normalized config like `docker compose config`
### Deployment
- REQ-04: Deploy infrastructure modules to cloud providers: AWS, Azure, GCP
- Success criteria: IaC adapter interface ships with Terraform as first adapter; provider engine generates provider-correct Terraform (aws, azurerm, google) from compose `providers` config + discovered credentials; `up/down/ps` run init/plan/apply/destroy/show through the adapter in dependency order; future IaC types implement the same interface without redesign
### Environment Discovery
- REQ-08: Native support for GitHub and Gitea APIs
- Success criteria: stdlib HTTP clients for GitHub (api.github.com) and Gitea (<host>/api/v1) with token auth from discovered credentials; read surface: repo info, branch, commits, releases; `nova env forge` reports connectivity/auth
- REQ-09: Local .git config support — determine whether the working directory is a git repo (create one if not); discover remotes and commits from the configured .git
- Success criteria: pure-python .git parsing (config remotes, HEAD, branch, commit hash/message) with `git` subprocess fallback; `nova env git` prints repo status, remotes, current commit; repo auto-created with visible notice when a command requires git context and none exists
- REQ-10: Credential discovery — gh and glab CLI stored credentials; AWS, Azure, GCP stored credentials or env variables
- Success criteria: discovers gh `hosts.yml` and glab `config.yml` tokens; AWS `~/.aws/credentials` + `AWS_*` env; Azure env + `~/.azure` profile; GCP gcloud ADC file + `GOOGLE_APPLICATION_CREDENTIALS`/env; `nova env credentials` reports sources as found/missing with masked values
- REQ-11: Pipeline detection — discover whether running in a CI pipeline (e.g., GitHub Actions); retrieve env / rely on env to pre-fill missing data for infrastructure modules
- Success criteria: detects GITHUB_ACTIONS, GITEA_ACTIONS, GITLAB_CI; extracts repo slug, ref, sha, server/API URLs, token into a prefill context consumed by compose interpolation and forge clients; `nova env pipeline` prints the detected context
### Implementation & Release
- REQ-05: Code must be Python
- Success criteria: pure Python 3.10+ implementation (3.11 dev environment); pyproject.toml packaging with `nova` console entry point; minimal deps (click, PyYAML); pytest suite green
- REQ-06: Every release includes the CLI binary plus the CLI deployed in a container
- Success criteria: release pipeline produces wheel + sdist + PyInstaller one-file linux binary + Dockerfile-built container image; smoke test runs the built binary (`nova version`); artifacts attach to the Gitea release; pipeline reproducible via `scripts/release.sh` and `.gitea/workflows/release.yml`
- REQ-07: Vendor c7n + c7n-left into the CLI for policy checks
- Success criteria: c7n and c7n-left ship as pinned dependencies of the nova package; `nova policy check` runs c7n-left against generated Terraform; policy violations block `up` unless `--skip-policy`; `nova policy version` reports vendored versions; builtin default policy pack ships with the package
## v2
- Additional IaC adapters beyond Terraform (e.g., Pulumi) via the IaC adapter interface
- Full infrastructure lifecycle gap-filling (post-go-live milestone)
- OS keychain credential discovery; expanded CI platform detection
## Out of Scope
| Feature | Reason |
|---------|--------|
| Full infrastructure lifecycle support | Deferred to a future milestone after go-live (user-specified) |
| Non-Terraform IaC adapters | The adapter interface ships in v0.1; other IaC types are future work |
| `docker run/exec/cp` command analogs | Container semantics do not map to infrastructure modules in v0.1 (D-009) |
## Traceability
| Requirement | Phase | Status |
|-------------|-------|--------|
| REQ-01 | P1 | pending |
| REQ-02 | P2 | pending |
| REQ-03 | P2 | pending |
| REQ-04 | P4 | pending |
| REQ-05 | P1 | pending |
| REQ-06 | P6 | pending |
| REQ-07 | P5 | pending |
| REQ-08 | P3 | pending |
| REQ-09 | P3 | pending |
| REQ-10 | P3 | pending |
| REQ-11 | P3 | pending |
+351
View File
@@ -0,0 +1,351 @@
# Nova CLI — Phase 0 Domain Research
Conclusion-oriented research for the v0.1 implementation; key external facts verified against
official sources (HashiCorp, Docker/compose-spec, gh/glab/act_runner source, Gitea docs, PyPI).
Verification date: 2026-10-14.
## 1. Terraform module source grammar
Facts (verified: developer.hashicorp.com/terraform/language/block/module):
- Public registry shorthand: `<namespace>/<name>/<provider>` — exactly 3 segments, e.g.
`terraform-aws-modules/vpc/aws`. 2 or 5 segments is an error.
- Private registry: `<hostname>/<namespace>/<name>/<provider>` (4 segments), e.g.
`app.terraform.io/example-corp/k8s-cluster/azurerm` (HCP Terraform) or any self-hosted registry
hostname. `localterraform.com` is a generic TFE hostname.
- Local paths: MUST start with `./` or `../` (or be absolute: `/` or drive letter; absolute is
discouraged). Local sources do not support `version`.
- Submodules: append `//subdir` to any package source, e.g. `hashicorp/consul/aws//modules/consul-cluster`,
`git::https://example.com/network.git//modules/vpc`. Query params go AFTER the subdir:
`git::...git//modules/vpc?ref=v1.2.0`.
- Version: a separate `version` argument in the module block (constraint string, registry sources
only): `version = "~> 6.0"`, `">= 1.2"`, `"1.2.3"`. There is NO `@version` suffix in Terraform's
grammar — `@x.y.z` is nova sugar that must be translated to the `version` field.
- Other source schemes nova should recognize but may defer: `git::` (+`?ref=`), `github.com/...`,
`bitbucket.org/...`, `git@github.com:...`, `s3::`, `gcs::`, `hg::`, https archive URLs.
- Provider source strings: `hashicorp/aws`, `hashicorp/azurerm`, `hashicorp/google`
(`<namespace>/<type>` in required_providers; fully qualified `registry.terraform.io/hashicorp/aws`).
Pitfalls:
- A 2-segment reference (`hashicorp/vpc`) is invalid in Terraform — reject with a clear error.
- `//` (double slash) selects a subdir; a single `/` does not. Naive path splitting breaks.
- `version` is only valid for registry sources; local/git sources ignore it (git uses `?ref=`).
- Full registry URLs (`registry.terraform.io/...`, `registry.terraform.io/modules/...`) are web URLs,
not source grammar — nova must normalize them to shorthand before emitting module blocks.
v0.1 implication: a ModuleRef resolver accepting shorthand `[host/]ns/name/provider[//subdir][@ver]`,
registry URLs (normalized to shorthand), and local `./` `../` absolute paths; emit Terraform module
blocks with `source` + `version` (registry only); map providers aws→hashicorp/aws,
azure→hashicorp/azurerm, gcp→hashicorp/google.
## 2. Dockerfile grammar essentials
Facts (docs.docker.com/reference/dockerfile):
- Instruction keywords are case-insensitive (conventionally uppercase). Leading whitespace on a
line is stripped before keyword matching.
- Comments: lines whose first non-whitespace char is `#`. No inline comments. Parser directives
(`# syntax=`, `# escape=`) are `#`-lines that must precede all instructions.
- Line continuations: `\` at end of line joins the next line. `\` inside double-quoted values is an
escape. (A `# escape=` directive can change the escape char — treat non-default as warning in v0.1.)
- `ENV key value` (legacy): exactly ONE pair; value is everything after the first space, inner
whitespace preserved, no quote processing. `ENV key=value [k2=v2 ...]`: multiple pairs, quotes and
`\` escapes supported.
- `ARG name[=default]` — no default means must be provided at build time. ARG before the first FROM
is only usable by FROM lines; ARG after FROM is stage-scoped.
- `FROM [--platform=<p>] <image> [AS <name>]`; image may be `name:tag` or `name@digest`; a later
`FROM <stage-alias>` reuses a previously aliased stage.
- `LABEL k=v [k2=v2 ...]` (equals form only). `WORKDIR /path` — relative paths accumulate on the
previous WORKDIR; supports variable interpolation.
- First instruction must be FROM (ARG allowed before it). Everything else is an error.
- Error style: BuildKit (current docker) reports `Dockerfile:<line>:<col>: parser: <message>` with a
source code-frame, e.g. `Dockerfile:4:1: parser: unknown instruction: FOO (did you mean FROM?)`.
Classic builder used `Dockerfile parse error line <N>: <message>`.
Pitfalls: the ENV legacy form's "value is the whole remainder" rule trips naive `split(' ')`
parsers; resolve continuations before tokenizing (a `#` line following a `\` is instruction data,
not a comment); track physical line numbers for messages.
v0.1 implication: a line-oriented parser — strip comments, join continuations, match the keyword
case-insensitively, keep 1-based line numbers, produce BuildKit-style errors
(`Novafile:<line>:<col>: ...`); ENV/ARG/LABEL/FROM/WORKDIR supported, container-only instructions
warn, COPY/ADD hard-error (D-010).
## 3. Compose file essentials for schema parity
Facts (verified against compose-spec schema + docs.docker.com):
- Top-level keys (compose-spec, verified): `name`, `services`, `include`, `volumes`, `networks`,
`configs`, `secrets` (plus newer `models`/`jobs`; `version` is obsolete — accept with warning).
**`profiles` is NOT top-level** — it is a service-level key only.
- Service keys (verified schema shapes): `image` (string) — nova's analog is `module`;
`build` (string context path or map {context, dockerfile, args}) — nova points at a Novafile dir;
`environment` (list `["K=V", "K"]` OR map `{K: V, K2: null}`; null/no value = resolve from env);
`env_file` (single string or list); `depends_on` — short form list of service names, long form map
`{svc: {condition: service_started|service_healthy|service_completed_successfully, required: bool
(default true), restart: bool|string}}`; `working_dir` (string); `profiles` (list; no-profile
services always active, services activate when ANY of their profiles is enabled).
- Interpolation (verified): `$VAR`, `${VAR}`, `${VAR:-default}` (default when unset OR empty),
`${VAR-default}` (unset only), `${VAR:?err}` / `${VAR?err}` (error when unset/empty),
`${VAR:+alt}` / `${VAR+alt}`. Applied to unquoted and double-quoted YAML scalars; single-quoted
are literal; unset vars interpolate to empty string.
- Interpolation source precedence (verified): shell env > `--env-file` files > default `.env` in the
project directory. `.env` syntax: `KEY=VAL`, `#` comments, single/double quotes (`'...'` literal),
escapes (`\n`, `\"`), inline comments after space; `=` or `:` delimiters.
- Config discovery (verified): without `-f`, Compose searches the working directory and traverses
parent directories looking for `compose.yaml` (preference: compose.yaml > compose.yml >
docker-compose.yaml > docker-compose.yml). `COMPOSE_FILE` env var = `-f` (multiple paths separated
by `COMPOSE_PATH_SEPARATOR`, default `:`). `--env-file` overrides the default `.env`.
- Project name precedence (verified): `-p` flag > `COMPOSE_PROJECT_NAME` > top-level `name:` >
basename of project directory > basename of cwd. Name charset: lowercase letters, digits, `-`,
`_`, must start with lowercase letter or digit.
- `docker compose config`: parses, validates, resolves interpolation, merges multiple `-f` files
(later files override/add), prints normalized canonical YAML. Flags: `--services`, `--profiles`,
`--format json`, `--no-interpolate`, `--environment`.
Pitfalls: `${VAR-default}` vs `${VAR:-default}` differ on empty strings — a common bug; multiple
`-f` files merge recursively, later wins; `depends_on.required: false` marks optional deps and
`service_completed_successfully` can gate sequential apply.
v0.1 implication: mirror compose discovery (upward search, `NOVA_COMPOSE_FILE` env, `-f` merge),
service-level profiles, full `${...}` operator set, and `nova config` printing the normalized model;
the only schema extensions are `module:` (instead of image) and the top-level `providers:` block.
## 4. Docker CLI command/flag ergonomics
Facts (verified: docs.docker.com/reference/cli/docker/compose):
- Nova's command set (D-009) matches real compose subcommands: build, up, down, ps, stop, start,
restart, rm, logs, config — all exist in docker compose v2. Nova adds `policy`, `env`, `version`
as top-level groups (docker has `docker compose version`).
- Top-level flags (before the subcommand, repeatable where noted): `-f/--file` (repeatable),
`-p/--project-name`, `--profile` (repeatable), `--env-file` (repeatable), `--project-directory`,
`--ansi`, `--parallel`, `--dry-run`.
- Subcommand flags worth mirroring: `up`: `-d/--detach`, `--build`, `--no-deps`, `--wait`,
`--force-recreate`, `--pull`; `down`: `-v/--volumes`, `--remove-orphans`, `-t`; `ps`: `-a`,
`--format (table|json)`; `logs`: `-f/--follow`, `--tail N`, `--since`, `--no-log-prefix`;
`rm`: `-f`, `-s/--stop`, `-v`; `build`: `--pull`, `--no-cache`, `--build-arg`; `config`:
`--services`, `--profiles`, `--format json`; `restart`/`stop`/`start`: `-t/--timeout`.
- Env var equivalents (verified): `COMPOSE_FILE`→`-f`, `COMPOSE_PROJECT_NAME`→`-p`,
`COMPOSE_PROFILES`→`--profile`, `COMPOSE_PARALLEL_LIMIT`. Nova analogs: `NOVA_COMPOSE_FILE`,
`NOVA_PROJECT_NAME`, `NOVA_PROFILES` (REQ-01 names NOVA_COMPOSE_FILE).
- Help style: `Usage: docker compose [OPTIONS] COMMAND [ARGS]` with Description/Options/Commands
sections, two-column command list. Click reproduces this layout with little tuning.
Pitfalls: `-v` is per-subcommand (down: volumes) — do NOT hoist it to nova's top level (docker's
top-level `-v` means version); docker compose v2 accepts `-f/-p` both before and after the
subcommand — mirror with group-level options.
v0.1 implication: a click group `nova` with group-level `-f/-p/--profile/--env-file` and
subcommand-level docker-parity flags; `--help` output mirrors docker compose structure.
## 5. Credential discovery formats
Facts (gh/glab verified from source; cloud providers verified from provider docs):
- gh CLI: `~/.config/gh/hosts.yml` (override: `GH_CONFIG_DIR`/`XDG_CONFIG_HOME`). YAML structure:
`hosts.<host>.oauth_token`, `hosts.<host>.user`, `hosts.<host>.users.<user>.oauth_token`
(verified in gh internal/config). Token precedence: `GH_TOKEN`/`GITHUB_TOKEN` (per-host:
`GH_ENTERPRISE_TOKEN`) > hosts.yml > OS keyring (out of scope, D-013; keyring-managed tokens are
absent from hosts.yml — report "stored in keyring, unreadable", not "missing").
- glab: `~/.config/glab-cli/config.yml` (override `GLAB_CONFIG_DIR`, then XDG; verified in glab
internal/config). Structure: top-level `hosts:` map; per-host keys `token` (aliases
`gitlab_token`/`oauth_token`), `job_token`, `user`, `api_host`, `use_keyring`. Env
`GITLAB_TOKEN`/`GITLAB_ACCESS_TOKEN` beats the file; `use_keyring: true` means keyring (report like gh).
- AWS: `~/.aws/credentials` INI with bare section headers (`[default]`, `[profile-name]`); keys
`aws_access_key_id`, `aws_secret_access_key`, `aws_session_token`. `~/.aws/config` uses
`[profile name]` sections (`region`). Path overrides: `AWS_SHARED_CREDENTIALS_FILE`,
`AWS_CONFIG_FILE`. Env: `AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `AWS_SESSION_TOKEN`,
`AWS_PROFILE`, `AWS_REGION`/`AWS_DEFAULT_REGION`. The Terraform AWS provider honors the standard
chain: env > profile file > instance role. env credentials beat `AWS_PROFILE`.
- Azure (azurerm provider, verified): env `ARM_SUBSCRIPTION_ID`, `ARM_TENANT_ID`, `ARM_CLIENT_ID`,
`ARM_CLIENT_SECRET`, plus `ARM_USE_CLI/MSI/OIDC`, `ARM_ENVIRONMENT` and cert/oidc variants. az
profile `~/.azure/azureProfile.json` = best-effort non-secret context; MSAL token cache
`~/.azure/msal_token_cache.json` is encrypted in newer az — do not parse (ARM_* env is the secret source).
- GCP (google provider, verified): `GOOGLE_APPLICATION_CREDENTIALS` (path to SA key JSON or ADC
file), `GOOGLE_CREDENTIALS` (key content), `GOOGLE_PROJECT`/`GOOGLE_CLOUD_PROJECT`,
`GOOGLE_REGION`, `GOOGLE_ZONE`, `GOOGLE_OAUTH_ACCESS_TOKEN`. gcloud ADC:
`~/.config/gcloud/application_default_credentials.json` with `type: authorized_user`
(refresh_token) or `service_account` (private_key) or `external_account`. `CLOUDSDK_CORE_PROJECT`
supplies project for gcloud.
- All discovered secret values are masked in output (D-013): report `{source, found: bool,
fingerprint}` (e.g. last-4 or hash prefix), never the value.
Pitfalls: INI parsing of AWS files must tolerate `#`/`;` comments, spaces around `=`, and profiles
with hyphens; multiple gh users under one host; ADC file may be a SA key with a project_id field —
read project from the key when present.
v0.1 implication: per-provider discovery classes with a uniform `discover() -> CredentialResult`
(surface: source path/env var, found, masked fingerprint); resolution order mirrors each cloud's
own provider chain so `up` just injects the right env vars/files into the Terraform adapter.
## 6. CI environment detection
Facts (GitHub docs; Gitea act_runner source verified; GitLab docs):
- GitHub Actions: `GITHUB_ACTIONS=true`; context: `GITHUB_REPOSITORY` (owner/repo), `GITHUB_REF`
(`refs/heads/<b>` | `refs/tags/<t>` | `refs/pull/N/merge`), `GITHUB_REF_NAME` (short), `GITHUB_SHA`,
`GITHUB_SERVER_URL`, `GITHUB_API_URL` (https://api.github.com), `GITHUB_TOKEN`,
`GITHUB_EVENT_NAME`, `GITHUB_EVENT_PATH` (JSON payload), `GITHUB_ACTOR`, `GITHUB_RUN_ID`.
- Gitea Actions (verified in act_runner `act/runner/run_context.go`): sets `CI=true`,
`GITHUB_ACTIONS=true` plus the same GITHUB_* contract (`GITHUB_REPOSITORY`, `GITHUB_SHA`,
`GITHUB_REF`, `GITHUB_REF_NAME`, `GITHUB_EVENT_NAME`, `GITHUB_WORKFLOW`, `GITHUB_WORKSPACE`,
`GITHUB_SERVER_URL`, `GITHUB_API_URL`, `GITHUB_ACTOR`, `GITHUB_RUN_ID`); job token `GITEA_TOKEN`
(repo-scoped). `GITHUB_ACTIONS=true` alone therefore CANNOT distinguish Gitea from GitHub.
- GitLab CI: `GITLAB_CI=true`, `CI=true`; `CI_PROJECT_PATH` (owner/repo), `CI_COMMIT_SHA`,
`CI_COMMIT_REF_NAME` (short, no `refs/` prefix), `CI_SERVER_URL`, `CI_API_V4_URL`, `CI_JOB_TOKEN`.
Pitfalls: GITHUB_REF has a `refs/` prefix while GitLab's CI_COMMIT_REF_NAME does not — normalize;
Gitea may leave `GITHUB_API_URL` unset (runner deletes it when empty) — derive `<server>/api/v1`
from GITHUB_SERVER_URL.
v0.1 implication: a pipeline module producing a normalized PrefillContext
`{platform, repo_slug, branch/ref, sha, server_url, api_base, token}` consumed by compose
interpolation and the forge clients; Gitea API base = `GITHUB_SERVER_URL + /api/v1`.
## 7. GitHub vs Gitea REST API
Facts (Gitea auth/pagination verified at docs.gitea.com/development/api-usage):
- Base: GitHub `https://api.github.com` (GHES: `https://<host>/api/v3`); Gitea `<host>/api/v1`
(nova's forge: `https://git.coreci.dev/api/v1`). Swagger: `<host>/swagger.v1.json`.
- Auth headers: GitHub `Authorization: Bearer <token>` (classic `token <t>` also works); Gitea
`Authorization: token <TOKEN>` (API-key tokens; OAuth2 tokens use `Bearer`; Basic auth also
accepted; `token=`/`access_token=` query params exist but leak — do not use).
- Read surface (same path shapes, different field details):
- Repo: `GET /repos/{owner}/{repo}` — both return `full_name`, `default_branch`, `html_url`,
`clone_url`, `private`.
- Branch: `GET /repos/{owner}/{repo}/branches/{branch}` — GitHub: `commit.sha`; Gitea:
`commit.id` + `commit.message`/`commit.timestamp` (normalize `sha` vs `id`!).
- Commits: GitHub `...commits?sha=<ref>&per_page=N`, Gitea `...commits?sha=<ref>&limit=N` —
both list `{sha, commit.message, commit.author/committer, html_url}`.
- Releases: `GET /repos/{owner}/{repo}/releases` — both `{tag_name, name, draft, prerelease,
assets[]}`; GitHub also has `/releases/latest`.
- Pagination: GitHub `page`/`per_page` (max 100), `Link` header, `X-RateLimit-*` headers; Gitea
`page`/`limit` (server default `MAX_RESPONSE_ITEMS=50`), `Link` header + `x-total-count`.
- Token scopes: GitHub classic `repo` (or fine-grained Contents + Metadata); Gitea scoped tokens
`read:/write:repository`, `write:package`, `all` (verified); Gitea CI job tokens are repo-limited.
- Errors: both return JSON `{"message": ...}`; GitHub 403 with `X-RateLimit-Remaining: 0` for rate
limit; 401 for bad token on both.
Pitfalls: branch shape divergence (commit.sha vs commit.id) is the #1 normalization bug; GitHub
expects `Accept: application/vnd.github+json` (+ `X-GitHub-Api-Version: 2022-11-28`) for stable v3.
v0.1 implication: a stdlib-HTTP `ForgeClient` base with `GitHubClient`/`GiteaClient` subclasses
(urllib, D-022); normalize branch/commit/release payloads into common dataclasses; auth header and
pagination differ per forge.
## 8. .git internals for pure-python parsing
Facts:
- Repo detection: `.git/` directory, OR a `.git` FILE containing `gitdir: <path>` (worktrees,
submodules). No `.git` at all → nova may auto-create with `git init` subprocess + visible notice
(D-021).
- `.git/config`: INI. `[remote "origin"]` with `url = ...`, `fetch = +refs/heads/*:...`;
`[branch "main"]` with `remote`/`merge`. Parse with `configparser(strict=False, delimiters="=")`.
- `HEAD`: either `ref: refs/heads/main` (on-branch) or a raw 40-hex sha (detached).
`refs/remotes/origin/HEAD` may indicate the default remote branch.
- Refs: loose ref files `.git/refs/heads/<branch>` (content: 40-hex + newline); tags under
`refs/tags/`; `packed-refs` file lines `<sha> <refname>` with `^<peeled-sha>` lines directly after
annotated tags (header line `# pack-refs with: ...`). Loose refs override packed-refs.
- Loose objects: `.git/objects/<2-char>/<38-hex>`, zlib-compressed (stdlib `zlib.decompress`);
payload `<type> <size>\x00<content>`. Commit headers: `tree`, `parent` (0..n),
`author Name <email> <unix-ts> <±hhmm>`, `committer`, optional `gpgsig`, blank line, message.
Tag objects: `object`, `type commit`, `tag <name>`, `tagger`.
- Subprocess fallback is REQUIRED for packed objects (`.git/objects/pack/*.pack` — no stdlib
reader), worktrees/submodules, reftable, shallow clones, or any object missing from the loose
store. Fallbacks: `git rev-parse HEAD`, `git config --get remote.origin.url`, `git log -1`,
`git branch --show-current`.
Pitfalls: HEAD may point at a ref that only exists in packed-refs; commits in fresh clones are
almost always packed (loose-object parsing works best on locally-created commits); never crash a
deployment because git context is unavailable — degrade to "no git context".
v0.1 implication: `nova.env.git` = pure-python first (config, HEAD, loose refs/objects), subprocess
fallback on any miss; outputs a `GitContext{root, branch, head_sha, remote_url, remote_host}`
consumed by the forge client (remote host → gitea/github/gitlab classification).
## 9. c7n + c7n-left packaging
Facts (verified from PyPI metadata + wheel source):
- PyPI: `c7n` 0.9.53 (console script `custodian`) and `c7n-left` 0.3.39 (console script `c7n-left`,
entry `c7n_left.cli:cli`). c7n-left pins EXACT versions: `c7n==0.9.53`, `click==8.5.0`,
`python-hcl2==8.1.4`, `rich==14.3.4`, `tfparse==0.6.22`; requires-python `>=3.9.2,<4`.
- Invocation (verified from c7n_left/cli.py — NOTE: the Terraform dir flag is `-d`, not `-t`):
`c7n-left run -p <policy-dir> -d <terraform-dir> [--filters "k=v,glob"] [--warn-on "k=v"]`
`[--output cli|json] [--output-file F] [--output-query <jmespath>] [--var-file F ...]`
`[--summary policy|resource]`. Subcommands: `run`, `test`, `validate`. Any policy match → exit
code 1 (configurable via `--warn-on`).
- Policy file syntax (verified): YAML
`policies: [{name, resource: terraform.<tf_type>, metadata: {severity}, filters: [...]}]` —
resource form is DOT notation (`resource: terraform.aws_s3_bucket`); match_type splits on the
first `.` and fnmatches the remainder against the raw Terraform resource type. The underscore form
`terraform_aws_s3_bucket` does NOT match in 0.3.39.
- Vendoring weight (verified via pip --dry-run): c7n-left pulls 29 packages including
boto3/botocore (~60-80 MB installed), cryptography (C extension), tfparse (compiled Rust manylinux
wheel), rich, python-hcl2, jsonschema. The c7n-left wheel itself is 46 KB pure-python.
Pitfalls: nova must pin `c7n-left==0.3.39` exactly (its c7n pin propagates — an independent c7n pin
breaks resolution); tfparse is a compiled wheel (PyInstaller bundles its `.so`, but builds must be
linux amd64 — already the target; binary grows ~100-150 MB with c7n vendored); the
`resource: terraform_<type>` underscore form silently matches nothing — builtin policies must use
the dot form.
v0.1 implication: pin `c7n-left==0.3.39` as a nova dependency (D-018); `nova policy check` shells
out to the `c7n-left` console script (subprocess isolation) with `-p <builtin-or-user-policy-dir>
-d <generated-tf-dir> --output json`, blocks `up` on exit 1 unless `--skip-policy`; builtin policy
pack ships as nova package data (dot-notation resources); `nova policy version` reads
`importlib.metadata`.
## 10. Python distribution & release
Facts:
- pyproject.toml (setuptools, src-layout):
`[build-system] requires=["setuptools>=68"], build-backend="setuptools.build_meta"`;
`[project] name="nova-cli", requires-python=">=3.10", dependencies=["click>=8.1,<9",
"PyYAML>=6,<7", "c7n-left==0.3.39"]`; `[project.scripts] nova="nova.cli:main"`;
`[tool.setuptools.packages.find] where=["src"]`; package-data for the builtin policy pack.
Artifacts via `python -m build` (wheel + sdist).
- PyInstaller one-file: `pyinstaller --onefile --name nova --strip` from the project venv. Pitfalls:
console-script indirection (point PyInstaller at a real module function), c7n dynamic imports +
data files need `--collect-submodules`/`--collect-data c7n c7n_left`, tfparse `.so` is collected
from the venv, onefile self-extracts to a tempdir (1-3 s startup), no cross-compilation (build on
linux amd64 in python:3.11-slim to match target glibc).
- Container: `FROM python:3.11-slim`, `pip install --no-cache-dir dist/nova*.whl`,
`ENTRYPOINT ["nova"]` (~200-300 MB with c7n vendored). Do NOT bundle the Terraform binary (BUSL):
the adapter detects a missing binary and errors with an install hint.
- Gitea release API (auth verified): `POST /api/v1/repos/{owner}/{repo}/releases` with
`Authorization: token <GITEA_TOKEN>`, JSON `{tag_name, name, body, draft, prerelease,
target_commitish}` — the tag is auto-created if absent. Response contains `id`. Asset upload:
`POST /api/v1/repos/{owner}/{repo}/releases/{id}/assets?name=<file>` as multipart/form-data field
`attachment`. Token scope: `write:repository` (releases live under the repository scope).
- Gitea also ships a container registry at `<host>/v2/<owner>/...` — an alternative to attaching
image tars to releases when the runner has no docker daemon (this dev environment has none).
Pitfalls: tag-less repo means the first `tag_name` (v0.1.0) is created from the release API — do not
`git tag` separately; gitea `MAX_RESPONSE_ITEMS=50` pagination on release/asset listings; the
binary must be smoke-tested (`nova version`) before attaching (REQ-06).
v0.1 implication: `scripts/release.sh` + `.gitea/workflows/release.yml` build wheel/sdist/binary/
container, then a stdlib-HTTP release step (reusing the gitea client) creates the release and
attaches artifacts; release failure retries 3x, never blocks the phase (D-020).
## Assumptions
| # | Assumption | Confidence | Consequence if wrong |
|---|------------|-----------|----------------------|
| A1 | Gitea Actions sets a `GITEA_ACTIONS` marker env (presence-based detection); robust fallback is `GITHUB_ACTIONS=true` + `GITHUB_SERVER_URL` host != github.com (act_runner's GITHUB_* contract is verified) | 0.80 | Detection falls back to server-URL host check; no functional loss |
| A2 | Gitea asset upload uses multipart field name `attachment` (per Gitea swagger, not re-verified live) | 0.85 | Release attach step fails; fix is a field rename, caught in P6 smoke test |
| A3 | Gitea branch payload uses `commit.id` (PayloadCommit) vs GitHub `commit.sha` | 0.85 | Branch listing shows empty sha; one-line normalization fix |
| A4 | Exact docker parse-error wording (`Dockerfile:<l>:<c>: parser: ...`, `unknown instruction: X`) for message parity | 0.70 | Error wording drifts from docker style; cosmetic only |
| A5 | Terraform binary is an external runtime prerequisite (BUSL; not bundled in nova's container) | 0.90 | If bundling were required, container must switch to OpenTofu — post-go-live decision |
| A6 | az CLI profile `~/.azure/azureProfile.json` is best-effort context only (no secret extraction); secrets come from ARM_* env | 0.70 | Azure credential discovery reports less context; no auth impact |
| A7 | `nova policy check` invokes the installed `c7n-left` console script via subprocess rather than programmatic import (import surface may change across 0.3.x) | 0.90 | Switch to programmatic entry — contained in one wrapper module |
| A8 | Gitea at git.coreci.dev has Actions enabled with a runner (release workflow depends on it) | 0.75 | Releases run locally via `scripts/release.sh` + GITEA_TOKEN instead |
| A9 | Compose file preference order compose.yaml > compose.yml > docker-compose.yaml > docker-compose.yml for nova's discovery (docs confirm compose.yaml preferred; exact fallback order beyond it is conventional) | 0.90 | A file with both names present resolves differently; warn on ambiguity |
| A10 | GitLab CI default variables as listed (CI_PROJECT_PATH, CI_API_V4_URL, ...) — stable for years | 0.95 | Detection misses GitLab prefill; low impact for a gitea-hosted project |
All other facts above were verified against primary sources during this research pass.
+49
View File
@@ -0,0 +1,49 @@
# Nova CLI — Roadmap
## Overview
Milestone v0.1-go-live: deliver Docker / Docker Compose ergonomics for deploying Terraform module registry paths to AWS, Azure, and GCP, with an environment discovery layer (git-context, forge APIs, credentials, pipeline detection), vendored policy checks (c7n + c7n-left), and full release engineering (binary + container). The project remains on the v0.1 minor line until go-live. This roadmap is initial and is refined by the roadmapper during /ci-run planning.
## Phases
### Phase 1 — CLI Skeleton & Docker-Ergonomic Command Surface
- Status: not_started
- Depends on: (none)
- Requirements: REQ-01, REQ-05
- Success criteria: Python project scaffold with packaging; docker-ergonomic command surface (build/up/down/ps/...) with routing wired
### Phase 2 — Novafile + Compose.yml Parsers
- Status: not_started
- Depends on: P1
- Requirements: REQ-02, REQ-03
- Success criteria: Novafile parses as a Dockerfile analog with Terraform module registry paths; compose.yml loads a compose-indistinguishable schema; parser tests pass
### Phase 3 — Environment Discovery Layer
- Status: not_started
- Depends on: P1
- Requirements: REQ-08, REQ-09, REQ-10, REQ-11
- Success criteria: git repo detection (create one if absent); remote/commit discovery from .git config; native GitHub + Gitea API clients; gh/glab stored-credential discovery; AWS/Azure/GCP stored credentials or env variables; pipeline detection (e.g., GitHub Actions) with env-based prefill for infrastructure modules
### Phase 4 — IaC Adapter Interface + Terraform Adapter + Provider Engine
- Status: not_started
- Depends on: P2, P3
- Requirements: REQ-04
- Success criteria: IaC adapter interface defined; Terraform adapter implements it; modules deploy to AWS/Azure/GCP through compose-like lifecycle commands; adapter pattern allows future IaC types without redesign
### Phase 5 — Vendored c7n + c7n-left Policy Checks
- Status: not_started
- Depends on: P4
- Requirements: REQ-07
- Success criteria: c7n + c7n-left vendored into the CLI; policy checks execute against module specs and deployments
### Phase 6 — Release Engineering & Go-Live Prep
- Status: not_started
- Depends on: P1, P2, P3, P4, P5
- Requirements: REQ-06
- Success criteria: release pipeline ships the CLI binary + CLI container image to gitea releases; v0.1 go-live criteria met
+45
View File
@@ -0,0 +1,45 @@
{
"project": {
"name": "Nova CLI",
"slug": "nova-cli"
},
"projects": [],
"active_project": null,
"milestone": "v0.1",
"release": {
"forge": "gitea",
"base_url": "https://git.coreci.dev",
"owner": "coreci",
"repo": "nova-cli"
},
"autonomy": {
"level": "full",
"escalation_hooks": ["deploy", "delete_data", "merge_to_main"],
"decision_confidence_threshold": 0.6,
"max_revision_iterations": 3,
"clarify_budget": 10
},
"ship": {
"per_phase": true,
"allow_skip": false,
"max_release_retries": 3,
"release_blocking": false
},
"personas": {
"enabled": true,
"territory_enforcement": "warn"
},
"verification": {
"bdd_default": false
},
"mvp_ux": {
"enabled": true
},
"secrets": {
"env_file": ".ciagent/.env.secrets",
"scopes": {
"release": ["GITEA_TOKEN"],
"forge": ["GITHUB_TOKEN", "GITEA_TOKEN"]
}
}
}
+22
View File
@@ -0,0 +1,22 @@
# secrets
.env
.env.secrets
.env.*
# python
__pycache__/
*.py[cod]
*.egg-info/
dist/
build/
.venv/
.pytest_cache/
.mypy_cache/
.coverage
htmlcov/
# nova local state
.nova/
*.tfstate
*.tfstate.*
.terraform/