feat(gcp-to-aws): live discovery via the authenticated gcloud CLI (stacked on #148) - #149
Conversation
…Discovery previously required Terraform with heroku_* resources, which most startups on Heroku don't have. Adds a consent-gated, read-only live discovery path: an interactive capture step (exact-command whitelist, config-var key names only, auth:token banned) writes raw CLI output to live-capture/, and a new 'live' fragment parses it into the inventory. When Terraform is also present, live wins for current state and disagreements surface as drift (unmanaged resources, plan changes, config conflicts) — never silently resolved. Includes replay fixtures with a drift-assertion set, validated by end-to-end fresh-agent runs of the Discover phase.
…covery previously required Terraform, app code, or billing exports. Adds a consent-gated, read-only live discovery sub-file: gcloud preflight, Cloud Asset Inventory fast path with a 16-row per-service fallback, --format=json projections that capture env var NAMES only, Terraform-style identity synthesis so downstream design-refs work unchanged, deterministic edge inference from resolved config, and Step 3S simplified clustering. When Terraform is also present, the IaC inventory is the base and disagreements surface as drift (unmanaged resources, console resizes, config conflicts) — with the rule that absence of evidence from a failed capture is not drift. Includes replay fixtures with a drift-assertion set, validated by end-to-end fresh-agent runs. Stacked on the Heroku live-discovery branch (awslabs#148).
…a dumps and relationship queries Two design decisions in discover-live.md were implicit and invite well-meaning 'optimization': (1) gcloud asset list --content-type=resource returns full resource.data in one call but includes env var values, breaking the values-never-captured contract; (2) CAI relationships.* queries parse on the standard search endpoint but the data requires SCC Premium/Enterprise or Gemini Cloud Assist — without the entitlement they return nothing, and most relationship types aren't search-available anyway. Both now stated inline with do-not-change guidance.
|
Follow-up commit: documented two deliberate design choices inline after reviewing the Cloud Asset Inventory docs — why the capture path avoids full-metadata dumps (env var values would land on disk) and why CAI relationship queries aren't used (the |
… close capture gaps - Restore graviton content in SKILL.md that a stale whole-file copy dropped (rebuilt from main + feature edits; conditional-load row, file tree, Defaults) - Offer live discovery whenever no Terraform is found, not only when NO files exist — billing-only and app-code-only workspaces now get the live path - Add bq ls fallback row so BigQuery (and its specialist gate) survives the common CAI-disabled case; mapping table references it - Drop undocumented 'gcloud run services list --region=-' (managed Cloud Run lists all regions by default); fix redis region-source wording for CAI path - Use --scope=projects/... for asset search (documented scoping flag); document why --asset-types stays unfiltered (unmapped_asset_types signal) - Mark networks/subnets/secrets/SA rows as enrichment so the CAI path keeps edge and name-inventory fidelity - Commit check_expected_drift.py asserter (stdlib-only) + README usage; validated PASS against a fresh fresh-agent scenario-B replay of this revision - Clarify merge rule 1 (overwrite any disagreeing config value, enrichment vs conflict distinction); note provisioned-vs-actual disk caveat and live-compute graviton gap as follow-ups
|
All review items addressed in 1198fb9 — point by point: 1. Graviton regression — confirmed and fixed. Root cause: SKILL.md was copied whole-file from a working tree based on a pre-#86 branch. Rebuilt from current main + the five feature edits, so nothing else regressed (verified: 4 graviton mentions restored, conditional-load row, file tree, Defaults bullet). 2. Live offer widened. Step 1d now offers live whenever no Terraform was found — billing-only and app-code-only workspaces get the primary offer; the STOP only fires when nothing at all will produce an artifact. 3. BigQuery fallback added. New row 17 ( |
…pping, SA naming Validated live discovery against a real GCP project (270 CAI assets, fast path; all six applicable enrichment rows including bq ls; env-name-only projections held with real secret-adjacent env names like DB_PASSWORD present as names only). Comparison against a prior IaC discovery run surfaced three spec bugs: - google_cloud_run_service (v1) vs google_cloud_run_v2_service caused FALSE drift on the same service — Step 6 now aliases v1/v2 Run and Functions type pairs when the GCP name matches - compute.googleapis.com/Firewall was unmapped, dropping what the IaC path classifies as network_path secondaries — mapping row added - Service account 'name' now mandated as the email local-part, never the display name, or merge matching breaks
|
Real-world validation (commit eb01165): ran the capture path against an actual startup GCP project and compared results to a prior IaC-based discovery of the same project. What held up: - CAI fast path returned 270 assets in one call; the >100 scale guard and extraction-script pattern engaged as designed - All six applicable enrichment rows succeeded, including the new |
…SKILL.md live-offer blurb - Reorder capture table so vertex (16) precedes bq (17); mapping-table row references unchanged - Mark bq ls as an E row so a CAI-success run still verifies BigQuery presence (specialist gate) even if thin search missed it - SKILL.md prerequisites blurb now matches discover.md Step 1d semantics: offer live whenever no Terraform is found, not only when no files exist - Deferred (follow-up): wiring check_expected_drift.py into the mise/CI test suite
|
Residual nits addressed in 94e7875: (1) capture table reordered so row 16 (vertex) precedes 17 (bq) — mapping references unchanged; (2) SKILL.md prerequisites blurb aligned to Step 1d semantics (offer live whenever no Terraform, not only when no files); (3) took the optional one now — |
…ge fix, v1/v2 alias lock - Row 18: firewall-rules list (minimal projection) so firewalls survive the per-service path, not CAI-only (same class as the earlier bq gap) - Row 19 + edge-rule rewrite: the vpc-access-connector annotation is a CONNECTOR id, not a network — resolve connector.network via a read-only describe before emitting a network_membership edge; never fabricate one - Fixture now declares orders-api as LEGACY google_cloud_run_service (v1) while live maps to v2, locking the Step 6 type-alias rule; expected-drift.json asserts one merged resource (IaC address, live_type, no false drift pair) - Asserter: recursive env-value payload detection (name+value/valueFrom objects, raw env keys, env-name-as-key patterns) - Discover summary surfaces unmapped_asset_types count + top types - Preflight ADC fallback: probe projects describe before demanding gcloud auth login (GOOGLE_APPLICATION_CREDENTIALS / impersonation setups) - SA merge matching clarified to config.account_id (fresh-agent replay judgment call folded back in) Validated: fresh-agent replay of updated scenario B — alias rule executed as designed (one merged resource, live_type recorded, image conflict), asserter PASS, full build green.
…heck_expected_drift.py (stdlib-only) machine-checks a scenario-B replay's heroku-resource-inventory.json against expected-drift.json: metadata, apps, merged resources, conflict fields, drift counts, forbidden clustering fields, and secret hygiene (no config-var value payloads, no raw config_vars, no secret-shaped strings). Validated PASS against a fresh fresh-agent replay of scenario B on this branch. Same pattern as the gcp-live-capture asserter (awslabs#149) so both live-discovery skills regress identically.
|
Review round addressed in 84f270a — all seven items taken: 1. Firewall fallback row (18) — minimal projection, E-marked; firewalls no longer CAI-only (same fix class as the earlier bq gap). 2. Connector edge corrected — the annotation is a CONNECTOR id, not a network. New row 19 resolves |
…be uses it The ADC fallback probe referenced $GCP_PROJECT one step before the project was set. Project resolution is a local config read plus user confirmation (no credentials needed), so it now runs as step 2, with the auth check + probe as step 3.
|
Good catch on the ADC probe ordering — it referenced $GCP_PROJECT one step before Step 0.3 set it. Fixed in c2dce5b by reordering: project resolution (a credential-free local config read + user confirmation) is now step 2, auth check + probe step 3, so the probe always has a resolved project. Took the reorder over the inline |
… the worker contract Three architecture contradictions made the skill unexecutable as written: prescan declared _interactive:false + _exec:rw while its collect fragment converses with the founder (token request), runs next build, and calls the Vercel API; four of discover's dispatched fragments needed shell or network the rw worker (Read/Grep/Glob/Write/Edit) does not have; and the token, collected interactively and never persisted, could not reach a dispatched worker at all. Retrofit (the pattern proven on heroku/gcp live discovery, awslabs#148/awslabs#149): - prescan runs INLINE (_interactive: true, _exec removed) — its work is conversational + shell + network by nature; scope boundary corrected (the Tier 1 build-health attempt belongs to prescan-collect per Requirements 1.1-1.2) - NEW discover-capture.md: main-window pre-work owning ALL shell/network — adapter/manifest build capture, a GET-only 8-endpoint API whitelist (Vercel tokens cannot be scoped read-only, so the whitelist IS the read-only guarantee), env keys projected to names before touching disk, headers-only probe capture, manifest.json index; VERCEL_TOKEN lives in an env var in the main window only - discover-adapter/manifests/api/probe become parse-only fragments reading capture/; triggers keyed to the capture manifest; new postconditions (no env values/token material anywhere; coupling items carry confidence) - prescan-collect token request rewritten per Vercel's token docs: read-only tokens do not exist — ask for project-scoped + shortest expiration, state the GET-whitelist enforcement honestly, give the revoke command - Vercel MCP positioned as optional read-only enrichment (runtime logs, access links) with the purchase-tool caution — not the discovery backbone Validated: fixtures/vercel-capture/ replay set (manifest-fallback path, ISR cross-check, unclassified Route Handlers, skipped crons/usage, names-only env keys) + fresh-agent Discover replay: HANDOFF_OK, zero network/build/token, check_expected_discovery.py PASS. Also: shared:sync refreshed the stale vendored INTERPRETER.md this branch carried.
Suggestion: soft-ask to enable CAI (user-driven), then fall backFor the startup audience this skill targets, enabling an API is usually low-friction — so when the Cloud Asset Inventory fast path fails because the API isn't enabled, consider offering a user-driven enable step before silently falling back to per-service rows. Proposed flow (agent never mutates):
Keeps the read-only contract (user runs the enable), spends the ask only when CAI would help, and still fail-softs if they decline. Happy to take this as a follow-up if you'd rather not expand this PR's scope. |
When asset search fails because Cloud Asset API is off (default for most startup projects), offer a user-driven enable with CLI + console how-to, retry once on yes, then fall back to per-service. Agent never mutates; permission-denied skips the ask. Record cai_enable_* in the manifest and live_metadata. Co-authored-by: Cursor <cursoragent@cursor.com>
|
Implemented in 666fa93. When the CAI fast path fails with an API-not-enabled signal ( plus the console path, then retries once on [Y] or falls through to per-service on [N]/still-failing. Permission-denied and other errors skip the ask. Agent never runs |
Enable alone is not enough — Cloud Asset Viewer (roles/cloudasset.viewer) is also required. Soft-ask now includes API enable, IAM guidance, and the view-assets docs link; permission-denied gets its own remediation offer. Co-authored-by: Cursor <cursoragent@cursor.com>
|
Follow-up in the tip of the branch: soft-ask now matches the official CAI setup — enable |
…rvice mode Review follow-up: rows 10/16 walked 'each region seen in the asset-search results or rows 1-9'. Fine in asset-search mode (project-wide coverage), but in per-service fallback mode the seen-region heuristic fails both ways: a Memorystore/Vertex instance in a region with no other footprint is silently missed on custom-mode VPCs, while auto-mode VPC subnets inflate the seen set to every region anyway. New whitelisted row 20 (gcloud compute regions list, names only) enumerates the walk in per-service mode; asset-search mode walks matching asset locations. If the regions list itself fails, fall back to the old heuristic WITH a capture warning naming the coverage limit. Fixtures model the walk (regions.json + empty-region redis captures) and drop the undocumented --region=- from the canned run.json command per the row-1 note.
|
Review follow-up pushed (ee0ebb7): deterministic region walk for the redis/vertex rows. The previous "each region seen in asset-search results or rows 1–9" heuristic was fine in asset-search mode (project-wide coverage) but failed both ways in per-service fallback mode: an instance in a region with no other footprint was silently missed on custom-mode VPCs, while auto-mode VPC subnets inflated the seen set to all ~40 regions anyway. Now: new whitelisted row 20 ( |
…148) * feat(heroku-to-aws): live discovery via the authenticated Heroku CLI Discovery previously required Terraform with heroku_* resources, which most startups on Heroku don't have. Adds a consent-gated, read-only live discovery path: an interactive capture step (exact-command whitelist, config-var key names only, auth:token banned) writes raw CLI output to live-capture/, and a new 'live' fragment parses it into the inventory. When Terraform is also present, live wins for current state and disagreements surface as drift (unmanaged resources, plan changes, config conflicts) — never silently resolved. Includes replay fixtures with a drift-assertion set, validated by end-to-end fresh-agent runs of the Discover phase. * test(heroku-to-aws): port the GCP drift asserter for fixture parity check_expected_drift.py (stdlib-only) machine-checks a scenario-B replay's heroku-resource-inventory.json against expected-drift.json: metadata, apps, merged resources, conflict fields, drift counts, forbidden clustering fields, and secret hygiene (no config-var value payloads, no raw config_vars, no secret-shaped strings). Validated PASS against a fresh fresh-agent replay of scenario B on this branch. Same pattern as the gcp-live-capture asserter (#149) so both live-discovery skills regress identically. --------- Co-authored-by: Logan Kleier <lkleier@amazon.com>
…stacked on #148) (#150) * feat(heroku-to-aws): live discovery via the authenticated Heroku CLI Discovery previously required Terraform with heroku_* resources, which most startups on Heroku don't have. Adds a consent-gated, read-only live discovery path: an interactive capture step (exact-command whitelist, config-var key names only, auth:token banned) writes raw CLI output to live-capture/, and a new 'live' fragment parses it into the inventory. When Terraform is also present, live wins for current state and disagreements surface as drift (unmanaged resources, plan changes, config conflicts) — never silently resolved. Includes replay fixtures with a drift-assertion set, validated by end-to-end fresh-agent runs of the Discover phase. * test(heroku-to-aws): port the GCP drift asserter for fixture parity check_expected_drift.py (stdlib-only) machine-checks a scenario-B replay's heroku-resource-inventory.json against expected-drift.json: metadata, apps, merged resources, conflict fields, drift counts, forbidden clustering fields, and secret hygiene (no config-var value payloads, no raw config_vars, no secret-shaped strings). Validated PASS against a fresh fresh-agent replay of scenario B on this branch. Same pattern as the gcp-live-capture asserter (#149) so both live-discovery skills regress identically. * feat(heroku-to-aws): use live-captured prices for the Estimate baseline Live discovery (#148) captures each add-on's actual billed plan price (monthly_price_usd), but Estimate could only build a current-spend baseline from a billing export -- and the migrate-or-stay comparison was gated on billing data, so live-discovered runs got projected AWS costs with no side-by-side at all. - New baseline rung between billing data and the rate-card cache: add-ons priced exactly from live-captured plan prices, dynos from the published-rate cache (the Platform API does not price formations). Distinct provenance (current_costs.source: live_prices_plus_cache) and a mandatory caveat: derived from plan prices, not an invoice; excludes usage-based charges. - The comparison (Part 3) and migration cost considerations (Part 4) now run for ANY determined baseline, labeled with baseline_source -- previously both were billing-only, which orphaned cache-derived baselines too. - Schema: add cached_stale to pricing_source.status (already mandated by both skills' estimate specs; the enum rejected it). - Fixtures: seed-estimate/ (the scenario-B inventory post-merge, plus clarify/design artifacts) + expected-estimate.json + check_expected_estimate.py asserting the exact $352 baseline math, provenance label, caveat, and unlocked comparison. Validated by a fresh-agent replay of the Estimate phase (asserter PASS). * fix(heroku-to-aws): gate the live-prices baseline rung on actual live prices Review follow-up: 'live discovery ran' was ambiguous when the run captured zero priced add-ons (e.g. a dyno-only app) -- rung 2 would fire, price everything from the cache via fallthrough, and still label the baseline live_prices_plus_cache with 'exact for add-ons' accuracy. Right number, wrong provenance. The rung now requires at least one add-on resource carrying config.monthly_price_usd; otherwise fall to rung 3, whose pricing_cache label describes that baseline honestly. * fix(heroku-to-aws): price EB root volumes; document sign conventions and the estimated rung Three review-observation cleanups from the estimate-phase replay: - EBS root volumes: new ebs block in the shared pricing file (gp3 $0.08/GB-mo, 8 GB AL2023 EB root default) + vendored sync. EB estimates now price root volumes instead of shipping a permanent 'known minor omission' warning. - Sign conventions: roi_analysis.monthly_difference_* (AWS minus Heroku, negative = AWS cheaper) and financial_summary.monthly_savings_* (Heroku minus AWS) are the same fact with opposite signs. Both specs now cross-reference the other, name their own convention, and require any presentation to label direction in words rather than print a bare signed value. - Pricing hierarchy: new row 4 'estimated' — the documented home of the services_by_source.estimated bucket the schema and assembler already carry. A rate stated verbatim by this file's own formulas may price a service (always with a warning naming the rate); only no-cache no-MCP no-formula is 'unavailable'. --------- Co-authored-by: Logan Kleier <lkleier@amazon.com> Co-authored-by: Karthik Balasubramanian <carthick@amazon.com>
* feat(migrate): add vercel-to-aws migration assessment skill Adds a new migration-to-aws skill: an honest Vercel-to-AWS assessment for Next.js apps (not a full migration plan). Built on the phase DSL (same architecture as heroku-to-aws), 5 backbone phases + 1 optional scaffold checkpoint: - PreScan -> Discover -> Clarify -> Recommend -> Report, with an optional Scaffold checkpoint - Discover computes a Coupling Score and 10 named Pre-Flight Checks unconditionally, before Recommend has run - Recommend is a fixed-precedence, three-outcome engine (OpenNext/SST, ECS Fargate, or a Vercel+AWS Hybrid) - every recommendation traces to exactly one rule, never a judgment call - Report renders a validated HTML assessment (new scripts/validate-assessment-report.py + pytest suite + reference/stub fixtures), with outcome-filtered findings, a reader-vocabulary rule, and a cost-labeling rule enforced by the validator - Scaffold (optional) emits a thin IaC skeleton matching the recommended outcome - SST/Terraform for OpenNext, Terraform-only for Fargate or the Hybrid path - and defaults compute to Graviton (ARM64) where SST/Terraform expose the option - assessment-state.json: a skill-owned resumability ledger (independent of the vendored .phase-status.json) supporting incremental, effort-for-confidence input collection across multiple sessions Registers the skill in the plugin's README, the top-level migrate README, and the Claude/Codex/Cursor plugin manifests. Verified: frontmatter validator (0 problems), pytest (39/39), dprint check and markdownlint-cli2 clean, reference fixture passes the validator (REPORT_OK), stub fixture fails with actionable errors (REPORT_FAIL). * chore: remove .kiro/specs/vercel-to-aws (local working scratch, not shipped) Per the precedent set in 033669c (removed heroku-eks-support, heroku-to-aws-migration, and migration-telemetry-sharing spec dirs from a prior PR), .kiro/specs/ is local Kiro working material, not project documentation this repo ships - no .kiro/specs/ directory exists anywhere on main today. The requirements/design/tasks docs used to drive this skill's build don't belong in the PR. * feat(vercel-to-aws): add estimate phase + promote scaffold to full generate phase Add a mandatory estimate phase (three-tier AWS cost projection vs. Vercel spend) and promote the optional scaffold checkpoint into a mandatory generate phase producing production-ready Terraform, including: - baseline.tf (GuardDuty, CloudTrail, IMDSv2, EBS encryption, budget alerts, Access Analyzer, remote-state backend, compliance-conditional Config/SecurityHub) - Full VPC with public/private subnets, NAT gateway, route tables - Outcome-specific compute: SST/OpenNext (A), Fargate+ALB+CloudFront (B), API Gateway+Lambda (C-A), Fargate backend (C-B) - Peripheral Terraform: RDS (Postgres), ElastiCache (KV), S3 (Blob), EventBridge (Cron) - Numbered migration scripts (01-06) with dry-run defaults - MIGRATION_GUIDE.md with phased timeline, verification, rollback, Go/No-Go gates - generation-warnings.json for unpriced/unmapped services Backbone changes: prescan -> discover -> clarify -> recommend -> estimate -> generate -> report -> complete (was: prescan -> discover -> clarify -> recommend -> report -> complete + scaffold checkpoint) Clarify extended with Q6 (Vercel spend), Q7 (database size), Q8 (compliance) to feed the estimate and generate phases. Vendored shared files: aws-infra-pricing.json, estimation-infra.schema.json, complexity-tiers.json — registered in sync-vendored-shared.ts auto-discovery. Breaking change: existing .phase-status.json files from the old backbone are not compatible; re-run from prescan. * fix(vercel-to-aws): complete generate-phase integration (SKILL.md, report, validator, READMEs, scaffold deletion) * fix(vercel-to-aws): capture/parse split — align prescan/discover with the worker contract Three architecture contradictions made the skill unexecutable as written: prescan declared _interactive:false + _exec:rw while its collect fragment converses with the founder (token request), runs next build, and calls the Vercel API; four of discover's dispatched fragments needed shell or network the rw worker (Read/Grep/Glob/Write/Edit) does not have; and the token, collected interactively and never persisted, could not reach a dispatched worker at all. Retrofit (the pattern proven on heroku/gcp live discovery, #148/#149): - prescan runs INLINE (_interactive: true, _exec removed) — its work is conversational + shell + network by nature; scope boundary corrected (the Tier 1 build-health attempt belongs to prescan-collect per Requirements 1.1-1.2) - NEW discover-capture.md: main-window pre-work owning ALL shell/network — adapter/manifest build capture, a GET-only 8-endpoint API whitelist (Vercel tokens cannot be scoped read-only, so the whitelist IS the read-only guarantee), env keys projected to names before touching disk, headers-only probe capture, manifest.json index; VERCEL_TOKEN lives in an env var in the main window only - discover-adapter/manifests/api/probe become parse-only fragments reading capture/; triggers keyed to the capture manifest; new postconditions (no env values/token material anywhere; coupling items carry confidence) - prescan-collect token request rewritten per Vercel's token docs: read-only tokens do not exist — ask for project-scoped + shortest expiration, state the GET-whitelist enforcement honestly, give the revoke command - Vercel MCP positioned as optional read-only enrichment (runtime logs, access links) with the purchase-tool caution — not the discovery backbone Validated: fixtures/vercel-capture/ replay set (manifest-fallback path, ISR cross-check, unclassified Route Handlers, skipped crons/usage, names-only env keys) + fresh-agent Discover replay: HANDOFF_OK, zero network/build/token, check_expected_discovery.py PASS. Also: shared:sync refreshed the stale vendored INTERPRETER.md this branch carried. * fix(vercel-to-aws): verify capture whitelist against Vercel REST API docs - Domains endpoint corrected to the documented /v9 (was /v5) - Env row note strengthened: the documented response schema carries value/ vsmValue/legacyValue fields even with decrypt=false — the key-name pipeline projection is the REAL protection, not the query param - Crons and storage rows reworded honestly: neither path is in the public REST reference; discover the GET endpoints via 'vercel api list' (the CLI's beta OpenAPI-backed api command), else record skipped — vercel.json and env-name/ dependency signals remain the fallbacks - Transport note added: prefer 'vercel api' (session/token auth, --paginate for the 100-per-page list endpoints, OpenAPI endpoint discovery), raw curl with manual pagination.next handling as fallback - Rate limits documented as a non-issue (200-1000 reads/min per endpoint vs this capture's small volume) - Fixture manifest endpoint strings synced * fix(vercel-to-aws): sync fixture manifest endpoints with the doc-verified whitelist The prior commit's message claimed this sync but the edit had failed on formatting drift — domains now /v9, crons/stores rows use the OpenAPI-discovered wording. * chore(vercel-to-aws): sync vendored aws-infra-pricing.json from canonical The merge from main brought the July 2026 canonical pricing update; the vercel skill's vendored copy was stale and failing shared:check. * style: dprint formatting after merge * fix(vercel-to-aws): constrain the OpenAPI-discovered capture rows Review follow-up: rows 6-7 (crons/stores) stay unpinned because Vercel does not document those paths -- pinning guesses would be fake precision that breaks silently. But the discovery step was looser than the pinned rows. New constraints: the resolved operation must be GET in the spec, its path must contain the row's subject segment (crons / stores|storage), the exact resolved path is recorded verbatim in the manifest entry with discovered_via: 'vercel api list' (audit trail), and no match means skipped -- never a close-enough substitute. Fixture manifest updated to model the audit fields. * feat(vercel-to-aws): best-effort terraform validate in the Generate assembler Review follow-up + parity fix: heroku (generate-terraform.md Step 12) and gcp (validation-report degraded-offline convention) both validate their generated Terraform; vercel's generate had static postconditions only. New assembler Step 6: static HCL checks (syntax, reference integrity, variable completeness, output references), then terraform init -backend=false && terraform validate when the binary is available. Failures and skipped runs land in generation-warnings.json with a founder-facing action line -- never a generation halt, matching the degraded-offline convention of the other two skills. * fix(vercel-to-aws): commit the missing capture/build manifest fixtures The capture manifest's build.files names three .next build manifests (routes/prerender/app-path-routes) that were never committed -- the validated replay ran against scratch copies. Reconstructed exactly per expected-discovery.json and discover-manifests.md: /blog/[slug] in dynamicRoutes with two prerendered entries at initialRevalidateSeconds 3600 (genuinely ISR), / static, /dashboard absent from prerender-manifest (dynamic), /api/checkout only in app-path-routes-manifest (Route Handler, unclassified by page manifests -> LOW-confidence dynamic). Preview keys are labeled fixture placeholders, not secrets. * fix(vercel-to-aws): un-ignore the fixture capture/build manifests The repo-root .gitignore's global build/ rule silently dropped the three canned .next manifests from the previous commit. Scoped negation inside the fixture directory re-includes them — they are committed test data mirroring the real capture layout, not build output. * fix(vercel-to-aws): routes-manifest arrays classify path shape, not rendering Fresh-agent replay against the restored build-manifest fixtures surfaced the ambiguity: staticRoutes/dynamicRoutes distinguish fixed paths from [param] segments -- a fixed-path page that renders per-request still sits in staticRoutes, and the replay classified /dashboard as static because of it. The rendering disposition comes from prerender-manifest membership: numeric initialRevalidateSeconds = isr, false = static, absent = dynamic. Spelled out the resolution table and extended the never-infer rule to cover static-from-staticRoutes as well as isr-from-revalidate-exports. * test(vercel-to-aws): estimate+generate replay fixtures; price both tiebreak paths Review follow-up: Discover had replay fixtures and an asserter; Estimate and Generate were spec-only. Now the whole pipeline is machine-checkable. Spec gap found while seeding: estimate had no handling for the unresolved [A,B] tiebreak that recommend legitimately produces and generate explicitly asks the founder about. Estimate is non-interactive, so it now prices BOTH candidate outcomes: projected_costs (and everything downstream) = Outcome A, a tiebreak_alternative section = Outcome B with its own Property-16 check, and mandatory tiebreak honesty in financial_summary/path_label (both balanced totals + how it gets resolved). The A-vs-B cost delta is itself a resolving input. Fixtures: seed-estimate/ and seed-generate/ are harvested from a validated fresh-agent chain replay (discover->clarify->recommend under documented-default answers, Q6 spend $200-1000, genuine [A,B] tiebreak; discover asserter PASS). expected-estimate.json pins the $600 midpoint baseline, both-path pricing, and comparison arithmetic; expected-generate.json pins the artifact set, OpenNext/Fargate mutual exclusion, provider pin, no-placeholder scan, compliance-none baseline, estimated-service coverage, and the Step 6 validate contract (pass = no warning; fail/skip = exactly one entry with a founder action). Both asserters PASS against fresh-agent replays of their phases (the generate replay's terraform init -backend=false + validate genuinely passed via a local provider cache). seed/.phase-status.json gains the estimate/generate phases this branch declares. * fix(vercel-to-aws): seed phase list includes the declared scaffold phase Caught by the new fixtures-check tool: the skill declares 6 phases (incl. scaffold) but the replay seed's .phase-status.json listed 5 -- phase-status.schema.json requires one entry per declared phase. (On the stacked generate-phase branch the seed already tracks that branch's estimate/generate phase set.) * fix(vercel-to-aws): Q7 confirms the discovered DB size; estimated rung; docs/spec cleanups Review-observation batch, replay-validated: - Q7 confirm-first: when discovery captured the postgres store's sizeBytes, Clarify derives the band (decimal GB) and presents it for founder confirmation instead of asking cold -- one less question per run, and the founder's correction wins when given. The phase postcondition now states the rule precisely: no COLD question whose answer discovery determined; confirmations are the sanctioned form. Validated by a fresh-agent discover->clarify replay (Q7 presented as a confirmation of band B from the ~3 GB store; HANDOFF_OK both phases). - Case fix folded back from that replay: five files matched peripherals[] against 'Postgres' while the discover schema emits lowercase 'postgres' -- a literal match would wrongly skip Q7, the DB migration script, and the has_databases complexity input. All now lowercase + case-insensitive. - Pricing hierarchy row 4 'estimated' (mirrors the heroku change on #150): the documented home of the services_by_source.estimated bucket; Lambda's formula constants are the canonical example. - generate-docs: guide step numbers count existing scripts (the file set is conditional, so numbering gaps are normal and must be called out), and a Provision Infrastructure slot between scripts 01 and 02 (terraform apply + sst deploy) -- both folded back from the generate replay. - SKILL.md: estimate/generate are read-only consumers of assessment-state.json (no phase file specifies a write; none should be improvised). - clarify.md: repaired the malformed declined-answer error-table row. * feat(vercel-to-aws): add post-Estimate what-if workshop checkpoint Freeze discovery/coupling/preflight and reprice traffic/outcome/region/arch via Recommend+Estimate inner runs with scenario snapshots. Outcome overrides use fired_rule workshop_override (declared contract) with transcript workshop_note provenance. Stacked on the generate-phase skill (#144). Co-authored-by: Cursor <cursoragent@cursor.com> * fix(vercel-to-aws): resume must not recompute Estimate mid-workshop Port INTERPRETER deferred-advance checkpoint resume and vercel SKILL workshop resume rule so warm starts never re-run Estimate while workshop is unresolved. Co-authored-by: Cursor <cursoragent@cursor.com> * feat(vercel-to-aws): render what-if scenarios in assessment report Add conditional what-if-scenarios section (workshop compare table) and enforce it in validate-assessment-report.py when scenarios/index.json has ≥2 entries. Co-authored-by: Cursor <cursoragent@cursor.com> * docs(vercel-to-aws): list scenarios/ in generated README artifacts Co-authored-by: Cursor <cursoragent@cursor.com> * fix(vercel-to-aws): rename assessment-report.html to migration-report.html Unify the stakeholder HTML filename with GCP and Heroku. Validator script name unchanged (validate-assessment-report.py) to avoid colliding with GCP's validate-migration-report.py. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(vercel-to-aws): make the workshop branch pass mise build Pre-existing CI failures on this branch, all fixed: (1) eight files committed with dprint drift (fmt:check); (2) markdownlint MD025/MD029/ MD031 — the fixtures README second H1, report-render's scenario table breaking an ordered list, workshop-assemble's fenced block splitting a list, workshop-refresh's unspaced fences; (3) shared:check — the workshop schema additions (estimation-infra workshop metadata) and the Fargate/EC2 arm64 rates were edited in the VENDORED copies instead of the canonical skills/shared files. Ported vendored -> canonical and ran shared:sync (heroku's vendored copies pick up the same additive changes). mise run build now exits 0. * feat(vercel-to-aws): shareable calculator.aws link per workshop scenario workshop-refresh 6b: after each snapshot, best-effort build_estimate via the aws-pricing-calculator MCP -> shareable calculator.aws URL stored as estimation_summary.calculator_url (PRIMARY outcome's Balanced services; tiebreak_alternative skipped; null + one chat note on failure; never blocks). Compare table and report scenario section render the link. AWS computes regional prices server-side when opened -- the honest region answer. Server registration (.mcp.json) rides the heroku workshop branch; degradation here is silent-null when unconfigured. --------- Co-authored-by: Logan Kleier <lkleier@amazon.com> Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: Karthik Balasubramanian <carthick@amazon.com>
* feat(heroku-to-aws): live discovery via the authenticated Heroku CLI Discovery previously required Terraform with heroku_* resources, which most startups on Heroku don't have. Adds a consent-gated, read-only live discovery path: an interactive capture step (exact-command whitelist, config-var key names only, auth:token banned) writes raw CLI output to live-capture/, and a new 'live' fragment parses it into the inventory. When Terraform is also present, live wins for current state and disagreements surface as drift (unmanaged resources, plan changes, config conflicts) — never silently resolved. Includes replay fixtures with a drift-assertion set, validated by end-to-end fresh-agent runs of the Discover phase. * test(heroku-to-aws): port the GCP drift asserter for fixture parity check_expected_drift.py (stdlib-only) machine-checks a scenario-B replay's heroku-resource-inventory.json against expected-drift.json: metadata, apps, merged resources, conflict fields, drift counts, forbidden clustering fields, and secret hygiene (no config-var value payloads, no raw config_vars, no secret-shaped strings). Validated PASS against a fresh fresh-agent replay of scenario B on this branch. Same pattern as the gcp-live-capture asserter (#149) so both live-discovery skills regress identically. * feat(heroku-to-aws): use live-captured prices for the Estimate baseline Live discovery (#148) captures each add-on's actual billed plan price (monthly_price_usd), but Estimate could only build a current-spend baseline from a billing export -- and the migrate-or-stay comparison was gated on billing data, so live-discovered runs got projected AWS costs with no side-by-side at all. - New baseline rung between billing data and the rate-card cache: add-ons priced exactly from live-captured plan prices, dynos from the published-rate cache (the Platform API does not price formations). Distinct provenance (current_costs.source: live_prices_plus_cache) and a mandatory caveat: derived from plan prices, not an invoice; excludes usage-based charges. - The comparison (Part 3) and migration cost considerations (Part 4) now run for ANY determined baseline, labeled with baseline_source -- previously both were billing-only, which orphaned cache-derived baselines too. - Schema: add cached_stale to pricing_source.status (already mandated by both skills' estimate specs; the enum rejected it). - Fixtures: seed-estimate/ (the scenario-B inventory post-merge, plus clarify/design artifacts) + expected-estimate.json + check_expected_estimate.py asserting the exact $352 baseline math, provenance label, caveat, and unlocked comparison. Validated by a fresh-agent replay of the Estimate phase (asserter PASS). * fix(heroku-to-aws): gate the live-prices baseline rung on actual live prices Review follow-up: 'live discovery ran' was ambiguous when the run captured zero priced add-ons (e.g. a dyno-only app) -- rung 2 would fire, price everything from the cache via fallthrough, and still label the baseline live_prices_plus_cache with 'exact for add-ons' accuracy. Right number, wrong provenance. The rung now requires at least one add-on resource carrying config.monthly_price_usd; otherwise fall to rung 3, whose pricing_cache label describes that baseline honestly. * fix(heroku-to-aws): price EB root volumes; document sign conventions and the estimated rung Three review-observation cleanups from the estimate-phase replay: - EBS root volumes: new ebs block in the shared pricing file (gp3 $0.08/GB-mo, 8 GB AL2023 EB root default) + vendored sync. EB estimates now price root volumes instead of shipping a permanent 'known minor omission' warning. - Sign conventions: roi_analysis.monthly_difference_* (AWS minus Heroku, negative = AWS cheaper) and financial_summary.monthly_savings_* (Heroku minus AWS) are the same fact with opposite signs. Both specs now cross-reference the other, name their own convention, and require any presentation to label direction in words rather than print a bare signed value. - Pricing hierarchy: new row 4 'estimated' — the documented home of the services_by_source.estimated bucket the schema and assembler already carry. A rate stated verbatim by this file's own formulas may price a service (always with a warning naming the rate); only no-cache no-MCP no-formula is 'unavailable'. * feat(heroku-to-aws): add post-Estimate what-if workshop checkpoint Freeze discovery and reprice region/HA/compute/Graviton via Design+Estimate inner runs, with scenario snapshots and a deferred Generate advance. Stacked on the live-prices Estimate baseline so schema/pricing merges cleanly. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(heroku-to-aws): resume must not recompute Estimate mid-workshop Teach INTERPRETER deferred-advance checkpoint resume (Estimate completed + workshop pending/in_progress) and mirror the rule in heroku SKILL orchestration. Co-authored-by: Cursor <cursoragent@cursor.com> * feat(heroku-to-aws): add stakeholder migration-report.html with scenarios Thin Generate report (decision, costs, optional what-if table) plus validator so workshop scenarios reach stakeholders instead of only scenarios/*.json. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(heroku-to-aws): report spec fold-backs from a fragment replay Fresh-agent replay of generate-report.md against the workshop after-arm64 fixture (validator REPORT_OK, all sections + scenario table rendered correctly) surfaced three spec gaps, all fixed: - the documented validator invocation omitted the required positional report path (exit 2 as written); full command + exit-code branching now inline instead of 'same way GCP does' - the Inputs table undersold what the scenario columns need: manifests carry cost tiers only; Region/HA/Compute/Arch come from each scenario's preferences/design copies -- now stated - exec-costs now states the fallback explicitly: Heroku-vs-AWS when a baseline exists, three-tier AWS table + note when current_costs.source is 'unavailable' * fix(heroku-to-aws): make the workshop branch pass mise build The branch failed CI three ways, all pre-existing: (1) ten files committed with dprint drift (fmt:check); (2) markdownlint MD025/MD029 in the fixtures README, generate-report scenario list, and workshop-assemble's fenced block splitting an ordered list; (3) the frontmatter validator rejects the workshop fragment files -- their _fragment ids (workshop-sheet/-refresh/-compare) did not match workshop.md's declared _ids (sheet/refresh/compare); renamed to match the house convention (fragment id == phase reference _id, as estimate/cost-engine does). mise run build now exits 0. * feat(heroku-to-aws): shareable calculator.aws link per workshop scenario Registers the aws-pricing-calculator MCP server (npx, no AWS credentials -- verified live: all 9 documented tools respond) in .mcp.json, and adds workshop-refresh 6b: after each scenario snapshot, best-effort build_estimate -> shareable calculator.aws URL stored as estimation_summary.calculator_url (null + one chat note on any failure; never blocks). The compare table and the stakeholder report render the link. This is the honest answer to the region knob's biggest limit: the cache is us-east-1-based, but the calculator computes REGIONAL prices server-side when the link is opened. Complements #49 (gcp Estimate-phase calculator integration -- same server, same degradation rules). --------- Co-authored-by: Logan Kleier <lkleier@amazon.com> Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: Karthik Balasubramanian <carthick@amazon.com>
Revert an out-of-scope GKE routing regression and apply reviewer polish (herosjourney, icarthick). - compute.md: revert the GKE Signals bullet to main's Fargate default — a google_container_cluster in inventory no longer forces EKS over the user's Fargate/Q8 preference (flagged by two reviewers; also contradicted the file's own 6-criteria rubric). Add 'preference override, not a technical blocker' note to the App Engine Eliminators row; add a one-line note that deterministic mappings omit rubric_applied. - elastic-beanstalk.md: add 'Discovery Inputs' section clarifying Terraform (full per-service EB) vs billing (coarse) vs app-code/gcloud-live (not yet wired — awslabs#149 follow-up); add Worker-tier note (skill uses WebServer tier only, aligning with heroku-to-aws). - design-billing.md: App Engine billing row Fargate -> Elastic Beanstalk so the billing path matches the new default. - design-infra.md: convert the App Engine fan-out step from a dense paragraph to an ordered sub-step list for reliable single-pass execution. - SKILL.md: tighten elastic-beanstalk.md load condition — require App Engine in inventory; skip even if compute_model is set. Verified: fmt:check, lint:md, lint:types, test 57/57, lint:frontmatter, gitleaks all pass.
Resolves conflicts after the whole workshop stack + awslabs#162 sidebar rename landed: - heroku-to-aws SKILL.md + heroku-live-capture/README.md: took main's version (this branch's stale awslabs#148-lineage copies; a GCP PR shouldn't alter merged heroku content — main has the newer workshop/what-if + Scenario C content). - migrate/README.md + plugin README: UNION — kept main's vercel/agent-advisor requirements AND this branch's GCP live-discovery additions (gcloud requirement lines, 'Live GCP discovery — how it works' subsection, gcloud/heroku input source). Full build to follow.
Resolves conflicts after the workshop stack + awslabs#149/awslabs#151/awslabs#155/awslabs#162 landed: - heroku-to-aws SKILL.md + heroku-live-capture/README.md: took main (stale awslabs#148/awslabs#150 lineage; main has the merged workshop/what-if + Scenario C content). - gcp-live-capture/README.md: kept THIS branch's added 'Scenario C — derived GCP baseline' block (its genuine new content; main's side empty there). - migrate/README.md: took main (adds vercel + agent-advisor reqs; awslabs#149's gcloud GCP requirement lines already present above the conflict). New gcp-infra-pricing-cache.md auto-enrolls in pricing:staleness (6/6 fresh) and the seed-baseline fixtures pass fixtures:check (141 json / 10 asserters). Full build green.
…te card (stacked on #149) (#156) * feat(heroku-to-aws): live discovery via the authenticated Heroku CLI Discovery previously required Terraform with heroku_* resources, which most startups on Heroku don't have. Adds a consent-gated, read-only live discovery path: an interactive capture step (exact-command whitelist, config-var key names only, auth:token banned) writes raw CLI output to live-capture/, and a new 'live' fragment parses it into the inventory. When Terraform is also present, live wins for current state and disagreements surface as drift (unmanaged resources, plan changes, config conflicts) — never silently resolved. Includes replay fixtures with a drift-assertion set, validated by end-to-end fresh-agent runs of the Discover phase. * feat(gcp-to-aws): live discovery via the authenticated gcloud CLI Discovery previously required Terraform, app code, or billing exports. Adds a consent-gated, read-only live discovery sub-file: gcloud preflight, Cloud Asset Inventory fast path with a 16-row per-service fallback, --format=json projections that capture env var NAMES only, Terraform-style identity synthesis so downstream design-refs work unchanged, deterministic edge inference from resolved config, and Step 3S simplified clustering. When Terraform is also present, the IaC inventory is the base and disagreements surface as drift (unmanaged resources, console resizes, config conflicts) — with the rule that absence of evidence from a failed capture is not drift. Includes replay fixtures with a drift-assertion set, validated by end-to-end fresh-agent runs. Stacked on the Heroku live-discovery branch (#148). * docs(gcp-to-aws): document why live discovery avoids CAI full-metadata dumps and relationship queries Two design decisions in discover-live.md were implicit and invite well-meaning 'optimization': (1) gcloud asset list --content-type=resource returns full resource.data in one call but includes env var values, breaking the values-never-captured contract; (2) CAI relationships.* queries parse on the standard search endpoint but the data requires SCC Premium/Enterprise or Gemini Cloud Assist — without the entitlement they return nothing, and most relationship types aren't search-available anyway. Both now stated inline with do-not-change guidance. * fix(gcp-to-aws): address review — restore graviton, widen live offer, close capture gaps - Restore graviton content in SKILL.md that a stale whole-file copy dropped (rebuilt from main + feature edits; conditional-load row, file tree, Defaults) - Offer live discovery whenever no Terraform is found, not only when NO files exist — billing-only and app-code-only workspaces now get the live path - Add bq ls fallback row so BigQuery (and its specialist gate) survives the common CAI-disabled case; mapping table references it - Drop undocumented 'gcloud run services list --region=-' (managed Cloud Run lists all regions by default); fix redis region-source wording for CAI path - Use --scope=projects/... for asset search (documented scoping flag); document why --asset-types stays unfiltered (unmapped_asset_types signal) - Mark networks/subnets/secrets/SA rows as enrichment so the CAI path keeps edge and name-inventory fidelity - Commit check_expected_drift.py asserter (stdlib-only) + README usage; validated PASS against a fresh fresh-agent scenario-B replay of this revision - Clarify merge rule 1 (overwrite any disagreeing config value, enrichment vs conflict distinction); note provisioned-vs-actual disk caveat and live-compute graviton gap as follow-ups * fix(gcp-to-aws): real-project findings — Run v1/v2 alias, firewall mapping, SA naming Validated live discovery against a real GCP project (270 CAI assets, fast path; all six applicable enrichment rows including bq ls; env-name-only projections held with real secret-adjacent env names like DB_PASSWORD present as names only). Comparison against a prior IaC discovery run surfaced three spec bugs: - google_cloud_run_service (v1) vs google_cloud_run_v2_service caused FALSE drift on the same service — Step 6 now aliases v1/v2 Run and Functions type pairs when the GCP name matches - compute.googleapis.com/Firewall was unmapped, dropping what the IaC path classifies as network_path secondaries — mapping row added - Service account 'name' now mandated as the email local-part, never the display name, or merge matching breaks * chore(gcp-to-aws): review nits — row order 16/17, bq as E row, align SKILL.md live-offer blurb - Reorder capture table so vertex (16) precedes bq (17); mapping-table row references unchanged - Mark bq ls as an E row so a CAI-success run still verifies BigQuery presence (specialist gate) even if thin search missed it - SKILL.md prerequisites blurb now matches discover.md Step 1d semantics: offer live whenever no Terraform is found, not only when no files exist - Deferred (follow-up): wiring check_expected_drift.py into the mise/CI test suite * fix(gcp-to-aws): review round 3 — firewall fallback row, connector edge fix, v1/v2 alias lock - Row 18: firewall-rules list (minimal projection) so firewalls survive the per-service path, not CAI-only (same class as the earlier bq gap) - Row 19 + edge-rule rewrite: the vpc-access-connector annotation is a CONNECTOR id, not a network — resolve connector.network via a read-only describe before emitting a network_membership edge; never fabricate one - Fixture now declares orders-api as LEGACY google_cloud_run_service (v1) while live maps to v2, locking the Step 6 type-alias rule; expected-drift.json asserts one merged resource (IaC address, live_type, no false drift pair) - Asserter: recursive env-value payload detection (name+value/valueFrom objects, raw env keys, env-name-as-key patterns) - Discover summary surfaces unmapped_asset_types count + top types - Preflight ADC fallback: probe projects describe before demanding gcloud auth login (GOOGLE_APPLICATION_CREDENTIALS / impersonation setups) - SA merge matching clarified to config.account_id (fresh-agent replay judgment call folded back in) Validated: fresh-agent replay of updated scenario B — alias rule executed as designed (one merged resource, live_type recorded, image conflict), asserter PASS, full build green. * fix(gcp-to-aws): preflight order — resolve project before the ADC probe uses it The ADC fallback probe referenced $GCP_PROJECT one step before the project was set. Project resolution is a local config read plus user confirmation (no credentials needed), so it now runs as step 2, with the auth check + probe as step 3. * feat(gcp-to-aws): soft-ask to enable CAI when the fast path is disabled When asset search fails because Cloud Asset API is off (default for most startup projects), offer a user-driven enable with CLI + console how-to, retry once on yes, then fall back to per-service. Agent never mutates; permission-denied skips the ask. Record cai_enable_* in the manifest and live_metadata. Co-authored-by: Cursor <cursoragent@cursor.com> * docs(gcp-to-aws): CAI soft-ask covers IAM viewer + official enable docs Enable alone is not enough — Cloud Asset Viewer (roles/cloudasset.viewer) is also required. Soft-ask now includes API enable, IAM guidance, and the view-assets docs link; permission-denied gets its own remediation offer. Co-authored-by: Cursor <cursoragent@cursor.com> * fix(gcp-to-aws): deterministic region walk for redis/vertex in per-service mode Review follow-up: rows 10/16 walked 'each region seen in the asset-search results or rows 1-9'. Fine in asset-search mode (project-wide coverage), but in per-service fallback mode the seen-region heuristic fails both ways: a Memorystore/Vertex instance in a region with no other footprint is silently missed on custom-mode VPCs, while auto-mode VPC subnets inflate the seen set to every region anyway. New whitelisted row 20 (gcloud compute regions list, names only) enumerates the walk in per-service mode; asset-search mode walks matching asset locations. If the regions list itself fails, fall back to the old heuristic WITH a capture warning naming the coverage limit. Fixtures model the walk (regions.json + empty-region redis captures) and drop the undocumented --region=- from the canned run.json command per the row-1 note. * feat(gcp-to-aws): ground the derived GCP baseline in a source-side rate card Estimate's Part 1 rung 2 ('estimate costs from discovered resource configurations') ran on remembered GCP prices -- ungrounded and unreproducible. With live discovery (#149) making TF-less runs the common case, that rung becomes the primary migrate-or-stay baseline for users without a billing export. - New references/shared/gcp-infra-pricing-cache.md: source-side GCP list rates (Cloud SQL Enterprise vCPU/RAM/storage/backup + tier decoding + HA multiplier, Memorystore capacity bands, GCE machine types, GKE cluster fee) with per-table sources, us-central1 basis, a NOT-derivable list (Cloud Run/Functions/PubSub/buckets/Autopilot/egress -> excluded with warnings, never silently), and the standard staleness contract -- the filename matches pricing-staleness.ts's pattern so the new card is freshness-gated automatically. - Part 1 rung 2 rewritten as a procedure: rate-card-only math (never remembered prices), capture-or-terraform-shaped config resolution, PD_SSD default, backup upper bound, exclusion reason enum (usage_based / no_standing_charge / unpriced_gcp), source 'inventory_estimate' (the schema's existing value), schema-shaped category breakdown + additive derivation[]/excluded_resources[]/ warnings[], mandatory not-a-bill caveat. - Part 4 egress: provisioned sizes now ground an explicit UPPER BOUND when billing is absent (previously omitted entirely); three-way gating recorded via baseline_available. - schema-estimate-infra.md: current_costs gains accuracy/derivation/ excluded_resources/warnings. - Fixtures: seed-baseline/ + expected-baseline.json (exact math: $149.45 = SQL $113.68 + Redis $35.77) + check_expected_baseline.py. Validated by two fresh-agent Part 1 replays (second run under the corrected spec: asserter PASS; first run folded back 7 findings incl. the source-enum conflict and the silent backup omission). --------- Co-authored-by: Logan Kleier <lkleier@amazon.com> Co-authored-by: Cursor <cursoragent@cursor.com> Co-authored-by: Karthik Balasubramanian <carthick@amazon.com>
Union-resolved mise.toml lint group: fixtures:assert (this PR) + fixtures:check + pricing:staleness (from awslabs#151/awslabs#155, now on main) all run in build. Registered the two gcp-live-capture asserters that landed via awslabs#149/awslabs#156 after this branch was cut (the PR's own registry-completeness guard correctly flagged them): - gcp-live-capture/check_expected_baseline.py (smoke — already exits 1 cleanly) - gcp-live-capture/check_expected_drift.py (smoke — added the same missing-artifact guard this PR applies to the other 4, replacing a FileNotFoundError traceback with a clean one-line FAIL + exit 1) fixtures:assert now 10/10 (3 golden, 7 smoke). Full build green.
Summary
gcloudCLI — read-only, consent-gated, env-var names only.gcloud. The dead-end "No GCP sources detected" STOP becomes a consent prompt.Changes
discover-live.md(new): consent gate → capture (whitelistedgcloudlist commands, projected fields only — no env values, no secret payloads, no full-metadata asset dumps) → parse intogcp-resource-inventory.json+ simplified clustering (Step 3S) → edge inference from resolved config only.gcloud compute regions list(row 20) in fallback mode.google_cloud_run_servicevs live v2 merged as one resource), console-resize conflicts recorded per field, absence-of-evidence-is-not-drift for failed captures.live_metadata.unmapped_asset_types), never guessed.Relationship to other PRs
Test plan
fixtures/gcp-live-capture/— syntheticacme-prodproject in per-service mode (CAI declined), covering Cloud Run/SQL/Redis/buckets/secrets + failed pubsub capture + stale Terraformpython3 fixtures/gcp-live-capture/check_expected_drift.py <run-dir>→ PASS (merge rules, type-alias lock, secret hygiene)sf-beach-appGCP project (three review rounds folded back)mise run buildgreenPre-existing tensions noted (not addressed here)
Live-only compute resources are not graviton-profiled in v1; SQL
dataDiskSizeGbis provisioned (not actual) size — both documented as caveats in the capture spec.