From 0a8f1740477dc37f48549dd723b8b928c6ab8b63 Mon Sep 17 00:00:00 2001 From: Logan Kleier Date: Sun, 19 Jul 2026 17:23:51 -0700 Subject: [PATCH 1/6] feat(gcp-to-aws): add post-Estimate what-if workshop checkpoint Port the Heroku workshop pattern for infra-route GCP migrations: freeze inventory, edit region/HA/compute/Graviton knobs, refresh Design+Estimate as artifact-only inner runs, and compare scenario snapshots. AI-only/billing-only runs skip the offer. Includes fixtures + asserter. Co-authored-by: Cursor --- .../migration-to-aws/fixtures/README.md | 6 ++ .../fixtures/gcp-workshop/README.md | 15 ++++ .../after-graviton-reprice/.phase-status.json | 14 ++++ .../after-graviton-reprice/aws-design.json | 19 +++++ .../estimation-infra.json | 22 ++++++ .../gcp-resource-inventory.json | 15 ++++ .../after-graviton-reprice/preferences.json | 33 ++++++++ .../scenarios/index.json | 22 ++++++ .../scenarios/scenario-001.aws-design.json | 19 +++++ .../scenario-001.estimation-infra.json | 22 ++++++ .../scenarios/scenario-001.json | 20 +++++ .../scenarios/scenario-001.preferences.json | 33 ++++++++ .../scenarios/scenario-002.aws-design.json | 19 +++++ .../scenario-002.estimation-infra.json | 22 ++++++ .../scenarios/scenario-002.json | 22 ++++++ .../scenarios/scenario-002.preferences.json | 33 ++++++++ .../gcp-workshop/check_expected_workshop.py | 47 ++++++++++++ .../gcp-workshop/expected-workshop.json | 12 +++ .../gcp-workshop/seed/.phase-status.json | 14 ++++ .../gcp-workshop/seed/aws-design.json | 19 +++++ .../gcp-workshop/seed/estimation-infra.json | 22 ++++++ .../seed/gcp-resource-inventory.json | 15 ++++ .../gcp-workshop/seed/preferences.json | 33 ++++++++ .../skills/gcp-to-aws/SKILL.md | 25 +++++- .../references/phases/discover/discover.md | 13 ++-- .../references/phases/estimate/estimate.md | 42 +++++++++- .../phases/workshop/workshop-assemble.md | 25 ++++++ .../phases/workshop/workshop-compare.md | 22 ++++++ .../phases/workshop/workshop-refresh.md | 76 +++++++++++++++++++ .../phases/workshop/workshop-sheet.md | 48 ++++++++++++ .../references/phases/workshop/workshop.md | 54 +++++++++++++ .../references/shared/schema-phase-status.md | 11 ++- .../shared/schema-workshop-scenarios.md | 53 +++++++++++++ 33 files changed, 849 insertions(+), 18 deletions(-) create mode 100644 migrate/plugins/migration-to-aws/fixtures/gcp-workshop/README.md create mode 100644 migrate/plugins/migration-to-aws/fixtures/gcp-workshop/after-graviton-reprice/.phase-status.json create mode 100644 migrate/plugins/migration-to-aws/fixtures/gcp-workshop/after-graviton-reprice/aws-design.json create mode 100644 migrate/plugins/migration-to-aws/fixtures/gcp-workshop/after-graviton-reprice/estimation-infra.json create mode 100644 migrate/plugins/migration-to-aws/fixtures/gcp-workshop/after-graviton-reprice/gcp-resource-inventory.json create mode 100644 migrate/plugins/migration-to-aws/fixtures/gcp-workshop/after-graviton-reprice/preferences.json create mode 100644 migrate/plugins/migration-to-aws/fixtures/gcp-workshop/after-graviton-reprice/scenarios/index.json create mode 100644 migrate/plugins/migration-to-aws/fixtures/gcp-workshop/after-graviton-reprice/scenarios/scenario-001.aws-design.json create mode 100644 migrate/plugins/migration-to-aws/fixtures/gcp-workshop/after-graviton-reprice/scenarios/scenario-001.estimation-infra.json create mode 100644 migrate/plugins/migration-to-aws/fixtures/gcp-workshop/after-graviton-reprice/scenarios/scenario-001.json create mode 100644 migrate/plugins/migration-to-aws/fixtures/gcp-workshop/after-graviton-reprice/scenarios/scenario-001.preferences.json create mode 100644 migrate/plugins/migration-to-aws/fixtures/gcp-workshop/after-graviton-reprice/scenarios/scenario-002.aws-design.json create mode 100644 migrate/plugins/migration-to-aws/fixtures/gcp-workshop/after-graviton-reprice/scenarios/scenario-002.estimation-infra.json create mode 100644 migrate/plugins/migration-to-aws/fixtures/gcp-workshop/after-graviton-reprice/scenarios/scenario-002.json create mode 100644 migrate/plugins/migration-to-aws/fixtures/gcp-workshop/after-graviton-reprice/scenarios/scenario-002.preferences.json create mode 100644 migrate/plugins/migration-to-aws/fixtures/gcp-workshop/check_expected_workshop.py create mode 100644 migrate/plugins/migration-to-aws/fixtures/gcp-workshop/expected-workshop.json create mode 100644 migrate/plugins/migration-to-aws/fixtures/gcp-workshop/seed/.phase-status.json create mode 100644 migrate/plugins/migration-to-aws/fixtures/gcp-workshop/seed/aws-design.json create mode 100644 migrate/plugins/migration-to-aws/fixtures/gcp-workshop/seed/estimation-infra.json create mode 100644 migrate/plugins/migration-to-aws/fixtures/gcp-workshop/seed/gcp-resource-inventory.json create mode 100644 migrate/plugins/migration-to-aws/fixtures/gcp-workshop/seed/preferences.json create mode 100644 migrate/plugins/migration-to-aws/skills/gcp-to-aws/references/phases/workshop/workshop-assemble.md create mode 100644 migrate/plugins/migration-to-aws/skills/gcp-to-aws/references/phases/workshop/workshop-compare.md create mode 100644 migrate/plugins/migration-to-aws/skills/gcp-to-aws/references/phases/workshop/workshop-refresh.md create mode 100644 migrate/plugins/migration-to-aws/skills/gcp-to-aws/references/phases/workshop/workshop-sheet.md create mode 100644 migrate/plugins/migration-to-aws/skills/gcp-to-aws/references/phases/workshop/workshop.md create mode 100644 migrate/plugins/migration-to-aws/skills/gcp-to-aws/references/shared/schema-workshop-scenarios.md diff --git a/migrate/plugins/migration-to-aws/fixtures/README.md b/migrate/plugins/migration-to-aws/fixtures/README.md index bedb1173..cc03f4d3 100644 --- a/migrate/plugins/migration-to-aws/fixtures/README.md +++ b/migrate/plugins/migration-to-aws/fixtures/README.md @@ -1,3 +1,9 @@ +# Migration fixtures + +Also see: + +- `gcp-workshop/` — GCP what-if workshop seed + graviton reprice + `check_expected_workshop.py` + # Migration report reference fixture `migration-report-reference.html` is a **structural reference** for the comprehensive `migration-report.html` output. It was derived from SF Beach migration artifacts (`0611-0606`) and uses canonical section IDs checked by `scripts/validate-migration-report.py`. diff --git a/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/README.md b/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/README.md new file mode 100644 index 00000000..3fa8ab29 --- /dev/null +++ b/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/README.md @@ -0,0 +1,15 @@ +# GCP what-if workshop fixtures + +Infra-route pilot for `gcp-to-aws` (`references/phases/workshop/`). + +| Path | Role | +| ---- | ---- | +| `seed/` | Post-Estimate baseline (`cpu_architecture=x86`) | +| `after-graviton-reprice/` | After Apply with `cpu_architecture=graviton` | +| `check_expected_workshop.py` | Stdlib asserter | + +```bash +python3 check_expected_workshop.py after-graviton-reprice +``` + +Fresh-agent replay bar: copy `seed/` → enter workshop → set arch graviton → Apply → Compare → asserter PASS. diff --git a/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/after-graviton-reprice/.phase-status.json b/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/after-graviton-reprice/.phase-status.json new file mode 100644 index 00000000..e50c539e --- /dev/null +++ b/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/after-graviton-reprice/.phase-status.json @@ -0,0 +1,14 @@ +{ + "migration_id": "0719-gcp-ws", + "last_updated": "2026-07-19T19:05:00Z", + "current_phase": "estimate", + "phases": { + "discover": "completed", + "clarify": "completed", + "design": "completed", + "estimate": "completed", + "workshop": "in_progress", + "generate": "pending", + "feedback": "pending" + } +} diff --git a/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/after-graviton-reprice/aws-design.json b/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/after-graviton-reprice/aws-design.json new file mode 100644 index 00000000..9b9a0965 --- /dev/null +++ b/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/after-graviton-reprice/aws-design.json @@ -0,0 +1,19 @@ +{ + "phase": "design", + "services": [ + { + "aws_service": "ECS Fargate", + "aws_config": { + "cpu_architecture": "ARM64", + "region": "us-east-1" + } + }, + { + "aws_service": "RDS PostgreSQL", + "aws_config": { + "instance_class": "db.t4g.micro", + "multi_az": false + } + } + ] +} diff --git a/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/after-graviton-reprice/estimation-infra.json b/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/after-graviton-reprice/estimation-infra.json new file mode 100644 index 00000000..219c845b --- /dev/null +++ b/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/after-graviton-reprice/estimation-infra.json @@ -0,0 +1,22 @@ +{ + "phase": "estimate", + "pricing_source": { + "status": "cached" + }, + "projected_costs": { + "aws_monthly_premium": 110, + "aws_monthly_balanced": 85, + "aws_monthly_optimized": 62 + }, + "complexity_tier": "small", + "recommendation": { + "path": "migrate_optimized", + "path_label": "Migrate", + "migrate_if": [ + "ok" + ], + "stay_if": [ + "not" + ] + } +} diff --git a/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/after-graviton-reprice/gcp-resource-inventory.json b/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/after-graviton-reprice/gcp-resource-inventory.json new file mode 100644 index 00000000..cd6cace6 --- /dev/null +++ b/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/after-graviton-reprice/gcp-resource-inventory.json @@ -0,0 +1,15 @@ +{ + "migration_id": "0719-gcp-ws", + "resources": [ + { + "id": "run:web", + "type": "google_cloud_run_v2_service", + "name": "web" + }, + { + "id": "sql:main", + "type": "google_sql_database_instance", + "name": "main" + } + ] +} diff --git a/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/after-graviton-reprice/preferences.json b/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/after-graviton-reprice/preferences.json new file mode 100644 index 00000000..73b833d5 --- /dev/null +++ b/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/after-graviton-reprice/preferences.json @@ -0,0 +1,33 @@ +{ + "design_constraints": { + "target_region": { + "value": "us-east-1", + "chosen_by": "default", + "prompt": "region", + "design_consequence": "deploy region" + }, + "availability": { + "value": "single-az", + "chosen_by": "default", + "prompt": "ha", + "design_consequence": "RDS AZ" + }, + "kubernetes": { + "value": "ecs-fargate", + "chosen_by": "user", + "prompt": "compute", + "design_consequence": "Fargate" + }, + "cpu_architecture": { + "value": "graviton", + "chosen_by": "user", + "prompt": "arch", + "design_consequence": "instance family" + } + }, + "workshop": { + "active": true, + "last_sheet_at": "2026-07-19T19:05:00Z", + "active_scenario_id": "scenario-002" + } +} diff --git a/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/after-graviton-reprice/scenarios/index.json b/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/after-graviton-reprice/scenarios/index.json new file mode 100644 index 00000000..e9869df2 --- /dev/null +++ b/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/after-graviton-reprice/scenarios/index.json @@ -0,0 +1,22 @@ +{ + "baseline_scenario_id": "scenario-001", + "active_scenario_id": "scenario-002", + "max_scenarios": 5, + "inventory_fingerprint": "0b5d7ee9a96ec6e4c48983e5dbc7fb7e52c134e66da297307d4f7681aeb6929c", + "scenarios": [ + { + "scenario_id": "scenario-001", + "label": "baseline", + "created_at": "2026-07-19T18:30:00Z", + "source": "baseline", + "manifest": "scenarios/scenario-001.json" + }, + { + "scenario_id": "scenario-002", + "label": "graviton", + "created_at": "2026-07-19T19:05:00Z", + "source": "workshop", + "manifest": "scenarios/scenario-002.json" + } + ] +} diff --git a/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/after-graviton-reprice/scenarios/scenario-001.aws-design.json b/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/after-graviton-reprice/scenarios/scenario-001.aws-design.json new file mode 100644 index 00000000..92da86f8 --- /dev/null +++ b/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/after-graviton-reprice/scenarios/scenario-001.aws-design.json @@ -0,0 +1,19 @@ +{ + "phase": "design", + "services": [ + { + "aws_service": "ECS Fargate", + "aws_config": { + "cpu_architecture": "X86_64", + "region": "us-east-1" + } + }, + { + "aws_service": "RDS PostgreSQL", + "aws_config": { + "instance_class": "db.t4g.micro", + "multi_az": false + } + } + ] +} diff --git a/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/after-graviton-reprice/scenarios/scenario-001.estimation-infra.json b/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/after-graviton-reprice/scenarios/scenario-001.estimation-infra.json new file mode 100644 index 00000000..35031e9c --- /dev/null +++ b/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/after-graviton-reprice/scenarios/scenario-001.estimation-infra.json @@ -0,0 +1,22 @@ +{ + "phase": "estimate", + "pricing_source": { + "status": "cached" + }, + "projected_costs": { + "aws_monthly_premium": 120, + "aws_monthly_balanced": 95, + "aws_monthly_optimized": 70 + }, + "complexity_tier": "small", + "recommendation": { + "path": "migrate_optimized", + "path_label": "Migrate", + "migrate_if": [ + "ok" + ], + "stay_if": [ + "not" + ] + } +} diff --git a/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/after-graviton-reprice/scenarios/scenario-001.json b/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/after-graviton-reprice/scenarios/scenario-001.json new file mode 100644 index 00000000..6601f827 --- /dev/null +++ b/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/after-graviton-reprice/scenarios/scenario-001.json @@ -0,0 +1,20 @@ +{ + "scenario_id": "scenario-001", + "label": "baseline", + "created_at": "2026-07-19T18:30:00Z", + "source": "baseline", + "preferences_subset": {}, + "estimation_summary": { + "aws_monthly_premium": 120, + "aws_monthly_balanced": 95, + "aws_monthly_optimized": 70, + "complexity_tier": "small", + "pricing_source": "cached", + "region_note": null + }, + "paths": { + "preferences": "scenarios/scenario-001.preferences.json", + "aws_design": "scenarios/scenario-001.aws-design.json", + "estimation_infra": "scenarios/scenario-001.estimation-infra.json" + } +} diff --git a/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/after-graviton-reprice/scenarios/scenario-001.preferences.json b/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/after-graviton-reprice/scenarios/scenario-001.preferences.json new file mode 100644 index 00000000..0b989680 --- /dev/null +++ b/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/after-graviton-reprice/scenarios/scenario-001.preferences.json @@ -0,0 +1,33 @@ +{ + "design_constraints": { + "target_region": { + "value": "us-east-1", + "chosen_by": "default", + "prompt": "region", + "design_consequence": "deploy region" + }, + "availability": { + "value": "single-az", + "chosen_by": "default", + "prompt": "ha", + "design_consequence": "RDS AZ" + }, + "kubernetes": { + "value": "ecs-fargate", + "chosen_by": "user", + "prompt": "compute", + "design_consequence": "Fargate" + }, + "cpu_architecture": { + "value": "x86", + "chosen_by": "default", + "prompt": "arch", + "design_consequence": "instance family" + } + }, + "workshop": { + "active": true, + "last_sheet_at": "2026-07-19T18:30:00Z", + "active_scenario_id": "scenario-001" + } +} diff --git a/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/after-graviton-reprice/scenarios/scenario-002.aws-design.json b/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/after-graviton-reprice/scenarios/scenario-002.aws-design.json new file mode 100644 index 00000000..9b9a0965 --- /dev/null +++ b/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/after-graviton-reprice/scenarios/scenario-002.aws-design.json @@ -0,0 +1,19 @@ +{ + "phase": "design", + "services": [ + { + "aws_service": "ECS Fargate", + "aws_config": { + "cpu_architecture": "ARM64", + "region": "us-east-1" + } + }, + { + "aws_service": "RDS PostgreSQL", + "aws_config": { + "instance_class": "db.t4g.micro", + "multi_az": false + } + } + ] +} diff --git a/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/after-graviton-reprice/scenarios/scenario-002.estimation-infra.json b/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/after-graviton-reprice/scenarios/scenario-002.estimation-infra.json new file mode 100644 index 00000000..219c845b --- /dev/null +++ b/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/after-graviton-reprice/scenarios/scenario-002.estimation-infra.json @@ -0,0 +1,22 @@ +{ + "phase": "estimate", + "pricing_source": { + "status": "cached" + }, + "projected_costs": { + "aws_monthly_premium": 110, + "aws_monthly_balanced": 85, + "aws_monthly_optimized": 62 + }, + "complexity_tier": "small", + "recommendation": { + "path": "migrate_optimized", + "path_label": "Migrate", + "migrate_if": [ + "ok" + ], + "stay_if": [ + "not" + ] + } +} diff --git a/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/after-graviton-reprice/scenarios/scenario-002.json b/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/after-graviton-reprice/scenarios/scenario-002.json new file mode 100644 index 00000000..65fe4731 --- /dev/null +++ b/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/after-graviton-reprice/scenarios/scenario-002.json @@ -0,0 +1,22 @@ +{ + "scenario_id": "scenario-002", + "label": "graviton", + "created_at": "2026-07-19T19:05:00Z", + "source": "workshop", + "preferences_subset": { + "design_constraints.cpu_architecture.value": "graviton" + }, + "estimation_summary": { + "aws_monthly_premium": 110, + "aws_monthly_balanced": 85, + "aws_monthly_optimized": 62, + "complexity_tier": "small", + "pricing_source": "cached", + "region_note": null + }, + "paths": { + "preferences": "scenarios/scenario-002.preferences.json", + "aws_design": "scenarios/scenario-002.aws-design.json", + "estimation_infra": "scenarios/scenario-002.estimation-infra.json" + } +} diff --git a/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/after-graviton-reprice/scenarios/scenario-002.preferences.json b/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/after-graviton-reprice/scenarios/scenario-002.preferences.json new file mode 100644 index 00000000..73b833d5 --- /dev/null +++ b/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/after-graviton-reprice/scenarios/scenario-002.preferences.json @@ -0,0 +1,33 @@ +{ + "design_constraints": { + "target_region": { + "value": "us-east-1", + "chosen_by": "default", + "prompt": "region", + "design_consequence": "deploy region" + }, + "availability": { + "value": "single-az", + "chosen_by": "default", + "prompt": "ha", + "design_consequence": "RDS AZ" + }, + "kubernetes": { + "value": "ecs-fargate", + "chosen_by": "user", + "prompt": "compute", + "design_consequence": "Fargate" + }, + "cpu_architecture": { + "value": "graviton", + "chosen_by": "user", + "prompt": "arch", + "design_consequence": "instance family" + } + }, + "workshop": { + "active": true, + "last_sheet_at": "2026-07-19T19:05:00Z", + "active_scenario_id": "scenario-002" + } +} diff --git a/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/check_expected_workshop.py b/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/check_expected_workshop.py new file mode 100644 index 00000000..757bb285 --- /dev/null +++ b/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/check_expected_workshop.py @@ -0,0 +1,47 @@ +#!/usr/bin/env python3 +"""Assert a GCP what-if workshop run against expected-workshop.json.""" +from __future__ import annotations +import json, sys +from pathlib import Path +FAILS=[] +def check(c,m): + if not c: FAILS.append(m) +def main(): + if len(sys.argv) not in (2,3): + print(__doc__); return 2 + run=Path(sys.argv[1]); fx=Path(__file__).resolve().parent + seed=Path(sys.argv[2]) if len(sys.argv)==3 else fx/'seed' + exp=json.loads((fx/'expected-workshop.json').read_text()) + inv_r, inv_s = run/'gcp-resource-inventory.json', seed/'gcp-resource-inventory.json' + check(inv_r.exists(), 'missing inventory') + if inv_r.exists() and inv_s.exists() and exp.get('inventory_must_match_seed_bytes'): + check(inv_r.read_bytes()==inv_s.read_bytes(), 'inventory bytes changed') + idxp=run/'scenarios'/'index.json'; check(idxp.exists(), 'missing index') + if not idxp.exists(): + print('FAIL'); [print(' -',f) for f in FAILS]; return 1 + idx=json.loads(idxp.read_text()); sc=idx.get('scenarios') or [] + check(len(sc)>=exp['min_scenarios'], 'too few scenarios') + check(idx.get('active_scenario_id')==exp['active_scenario_id'], 'active id') + prefs=json.loads((run/'preferences.json').read_text()) + arch=(((prefs.get('design_constraints') or {}).get('cpu_architecture') or {}).get('value')) + check(arch==exp['active_cpu_architecture'], f'arch={arch}') + est=json.loads((run/'estimation-infra.json').read_text()) + base_m=run/'scenarios'/f"{exp['baseline_scenario_id']}.json" + if base_m.exists() and exp.get('balanced_must_differ_from_baseline'): + b=json.loads(base_m.read_text())['estimation_summary']['aws_monthly_balanced'] + a=est['projected_costs']['aws_monthly_balanced'] + check(a!=b, f'balanced unchanged {a}') + ph=run/'.phase-status.json' + if ph.exists(): + p=json.loads(ph.read_text()) + if exp.get('current_phase_must_be'): + check(p.get('current_phase')==exp['current_phase_must_be'], 'current_phase') + if exp.get('workshop_phase_must_be'): + check(p.get('phases',{}).get('workshop')==exp['workshop_phase_must_be'], 'workshop phase') + if exp.get('generate_must_not_be_completed'): + check(p.get('phases',{}).get('generate')!='completed', 'generate completed') + if FAILS: + print(f'FAIL ({len(FAILS)}):'); [print(' -',f) for f in FAILS]; return 1 + print('PASS — expected-workshop.json assertions hold'); return 0 +if __name__=='__main__': + sys.exit(main()) diff --git a/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/expected-workshop.json b/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/expected-workshop.json new file mode 100644 index 00000000..2fa79588 --- /dev/null +++ b/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/expected-workshop.json @@ -0,0 +1,12 @@ +{ + "inventory_must_match_seed_bytes": true, + "min_scenarios": 2, + "baseline_scenario_id": "scenario-001", + "active_scenario_id": "scenario-002", + "active_cpu_architecture": "graviton", + "balanced_must_differ_from_baseline": true, + "generate_must_not_be_completed": true, + "current_phase_must_be": "estimate", + "workshop_phase_must_be": "in_progress", + "max_scenarios": 5 +} diff --git a/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/seed/.phase-status.json b/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/seed/.phase-status.json new file mode 100644 index 00000000..826750ce --- /dev/null +++ b/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/seed/.phase-status.json @@ -0,0 +1,14 @@ +{ + "migration_id": "0719-gcp-ws", + "last_updated": "2026-07-19T18:30:00Z", + "current_phase": "estimate", + "phases": { + "discover": "completed", + "clarify": "completed", + "design": "completed", + "estimate": "completed", + "workshop": "pending", + "generate": "pending", + "feedback": "pending" + } +} diff --git a/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/seed/aws-design.json b/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/seed/aws-design.json new file mode 100644 index 00000000..92da86f8 --- /dev/null +++ b/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/seed/aws-design.json @@ -0,0 +1,19 @@ +{ + "phase": "design", + "services": [ + { + "aws_service": "ECS Fargate", + "aws_config": { + "cpu_architecture": "X86_64", + "region": "us-east-1" + } + }, + { + "aws_service": "RDS PostgreSQL", + "aws_config": { + "instance_class": "db.t4g.micro", + "multi_az": false + } + } + ] +} diff --git a/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/seed/estimation-infra.json b/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/seed/estimation-infra.json new file mode 100644 index 00000000..35031e9c --- /dev/null +++ b/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/seed/estimation-infra.json @@ -0,0 +1,22 @@ +{ + "phase": "estimate", + "pricing_source": { + "status": "cached" + }, + "projected_costs": { + "aws_monthly_premium": 120, + "aws_monthly_balanced": 95, + "aws_monthly_optimized": 70 + }, + "complexity_tier": "small", + "recommendation": { + "path": "migrate_optimized", + "path_label": "Migrate", + "migrate_if": [ + "ok" + ], + "stay_if": [ + "not" + ] + } +} diff --git a/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/seed/gcp-resource-inventory.json b/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/seed/gcp-resource-inventory.json new file mode 100644 index 00000000..cd6cace6 --- /dev/null +++ b/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/seed/gcp-resource-inventory.json @@ -0,0 +1,15 @@ +{ + "migration_id": "0719-gcp-ws", + "resources": [ + { + "id": "run:web", + "type": "google_cloud_run_v2_service", + "name": "web" + }, + { + "id": "sql:main", + "type": "google_sql_database_instance", + "name": "main" + } + ] +} diff --git a/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/seed/preferences.json b/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/seed/preferences.json new file mode 100644 index 00000000..0b989680 --- /dev/null +++ b/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/seed/preferences.json @@ -0,0 +1,33 @@ +{ + "design_constraints": { + "target_region": { + "value": "us-east-1", + "chosen_by": "default", + "prompt": "region", + "design_consequence": "deploy region" + }, + "availability": { + "value": "single-az", + "chosen_by": "default", + "prompt": "ha", + "design_consequence": "RDS AZ" + }, + "kubernetes": { + "value": "ecs-fargate", + "chosen_by": "user", + "prompt": "compute", + "design_consequence": "Fargate" + }, + "cpu_architecture": { + "value": "x86", + "chosen_by": "default", + "prompt": "arch", + "design_consequence": "instance family" + } + }, + "workshop": { + "active": true, + "last_sheet_at": "2026-07-19T18:30:00Z", + "active_scenario_id": "scenario-001" + } +} diff --git a/migrate/plugins/migration-to-aws/skills/gcp-to-aws/SKILL.md b/migrate/plugins/migration-to-aws/skills/gcp-to-aws/SKILL.md index 5c0216ee..f60b765d 100644 --- a/migrate/plugins/migration-to-aws/skills/gcp-to-aws/SKILL.md +++ b/migrate/plugins/migration-to-aws/skills/gcp-to-aws/SKILL.md @@ -123,11 +123,11 @@ When reading `$MIGRATION_DIR/.phase-status.json`, validate before proceeding: 1. **Multiple sessions**: If multiple directories exist under `.migration/`, list them with their phase status and ask: [A] Resume latest, [B] Start fresh, [C] Cancel. 2. **Invalid JSON**: If `.phase-status.json` fails to parse, STOP. Output: "State file corrupted (invalid JSON). Delete the file and restart the current phase." -3. **Unrecognized phase**: If `phases` object contains a phase not in {discover, clarify, design, estimate, generate, feedback}, STOP. Output: "Unrecognized phase: [value]. Valid phases: discover, clarify, design, estimate, generate, feedback." +3. **Unrecognized phase**: If `phases` object contains a phase not in {discover, clarify, design, estimate, workshop, generate, feedback}, STOP. Output: "Unrecognized phase: [value]. Valid phases: discover, clarify, design, estimate, workshop, generate, feedback." 4. **Unrecognized status**: If any `phases.*` value is not in {pending, in_progress, completed}, STOP. Output: "Unrecognized status: [value]. Valid values: pending, in_progress, completed." -5. **Invalid `current_phase`** (if present): If `current_phase` is not in {discover, clarify, design, estimate, generate, complete}, STOP. Output: "Unrecognized current_phase: [value]. Valid values: discover, clarify, design, estimate, generate, complete." +5. **Invalid `current_phase`** (if present): If `current_phase` is not in {discover, clarify, design, estimate, generate, complete}, STOP. Output: "Unrecognized current_phase: [value]. Valid values: discover, clarify, design, estimate, generate, complete." (`workshop` and `feedback` are checkpoints — never `current_phase`.) 6. **Out-of-order completion**: For ordered phases [discover, clarify, design, estimate, generate], if any later phase is `"completed"` while an earlier phase is not `"completed"`, STOP. Output: "Inconsistent phase ordering detected. Reconcile `.phase-status.json` before resuming." -7. **Multiple active phases**: Across core phases {discover, clarify, design, estimate, generate}, at most one phase may be `"in_progress"`. If >1, STOP. Output: "Multiple phases are in_progress. Keep only one active phase before resuming." +7. **Multiple active phases**: Across core phases {discover, clarify, design, estimate, generate}, at most one phase may be `"in_progress"`. If >1, STOP. Output: "Multiple phases are in_progress. Keep only one active phase before resuming." (Checkpoint `workshop`/`feedback` may be `in_progress` while estimate is `completed`.) --- @@ -147,6 +147,7 @@ Migration state lives in `$MIGRATION_DIR` (`.migration/[MMDD-HHMM]/`), created b "clarify": "completed", "design": "in_progress", "estimate": "pending", + "workshop": "pending", "generate": "pending", "feedback": "pending" } @@ -155,6 +156,7 @@ Migration state lives in `$MIGRATION_DIR` (`.migration/[MMDD-HHMM]/`), created b **Status values:** `"pending"` → `"in_progress"` → `"completed"`. Never goes backward. For core phases (discover, clarify, design, estimate, generate), at most one phase may be `"in_progress"` at any time. +`workshop` and `feedback` are optional checkpoints (never `current_phase`). `current_phase` is optional but recommended; when present it is authoritative. The `.migration/` directory is automatically protected by a `.gitignore` file created in Phase 1. @@ -181,6 +183,7 @@ Example — after completing the Clarify phase, write `$MIGRATION_DIR/.phase-sta "clarify": "completed", "design": "pending", "estimate": "pending", + "workshop": "pending", "generate": "pending", "feedback": "pending" } @@ -199,6 +202,7 @@ Replace `MMDD-HHMM` with the actual migration ID, generate the `last_updated` IS | **Clarify** | Discovery artifacts (`gcp-resource-inventory.json`, `gcp-resource-clusters.json`, `ai-workload-profile.json`, `billing-profile.json` — whichever exist) | `preferences.json`, `.phase-status.json` updated | `references/phases/clarify/clarify.md` | | **Design** | `preferences.json` + discovery artifacts | `aws-design.json` (infra), `aws-design-ai.json` (AI), `aws-design-billing.json` (billing-only) | `references/phases/design/design.md` | | **Estimate** | `aws-design.json` or `aws-design-billing.json` or `aws-design-ai.json`, `preferences.json` | `estimation-infra.json` or `estimation-ai.json` or `estimation-billing.json`, `.phase-status.json` updated | `references/phases/estimate/estimate.md` | +| **Workshop** | Post-Estimate infra artifacts (`gcp-resource-inventory.json`, `preferences.json`, `aws-design.json`, `estimation-infra.json`) — optional checkpoint | `scenarios/`, patched `preferences.json` / design / estimate; `.phase-status.json` (`workshop`) | `references/phases/workshop/workshop.md` | | **Generate** | `estimation-infra.json` or `estimation-ai.json` or `estimation-billing.json`, `aws-design.json` or `aws-design-billing.json` or `aws-design-ai.json`, `preferences.json` | `generation-infra.json` or `generation-ai.json` or `generation-billing.json` + `terraform/`, `scripts/`, `ai-migration/`, `validation-report.json` (when infra route active), `MIGRATION_GUIDE.md`, `README.md`, `.phase-status.json` updated | `references/phases/generate/generate.md` | | **Feedback** | `.phase-status.json` (discover completed minimum), all existing migration artifacts | `feedback.json`, `trace.json`, `.phase-status.json` updated | `references/phases/feedback/feedback.md` | @@ -244,6 +248,12 @@ gcp-to-aws/ │ │ │ ├── estimate-infra.md # Infrastructure cost analysis │ │ │ ├── estimate-ai.md # AI workload cost analysis │ │ │ └── estimate-billing.md # Billing-only cost analysis +│ │ ├── workshop/ +│ │ │ ├── workshop.md # Checkpoint: optional post-Estimate what-if +│ │ │ ├── workshop-sheet.md # Assumption sheet knobs +│ │ │ ├── workshop-refresh.md # Patch prefs → Design → Estimate → snapshot +│ │ │ ├── workshop-compare.md # Side-by-side scenarios +│ │ │ └── workshop-assemble.md # Resolve checkpoint → return to Generate │ │ ├── generate/ │ │ │ ├── generate.md # Phase 5: Generate orchestrator │ │ │ ├── generate-infra.md # Infrastructure migration plan @@ -276,6 +286,7 @@ gcp-to-aws/ │ │ │ └── shared/ │ ├── schema-phase-status.md # .phase-status.json schema (canonical reference) +│ ├── schema-workshop-scenarios.md # scenarios/ + preferences.workshop contract │ ├── schema-discover-iac.md # gcp-resource-inventory + clusters schemas (loaded by discover-iac.md) │ ├── schema-discover-ai.md # ai-workload-profile schema (loaded by discover-app-code.md and discover-iac.md Step 7d) │ ├── schema-discover-billing.md # billing-profile schema (loaded by discover-billing.md) @@ -340,12 +351,18 @@ When invoked, the agent **MUST follow this exact sequence**: - If user picks **A** → Load `references/phases/feedback/feedback.md`, execute it, then continue to Clarify. - If user picks **B** → Continue to Clarify (feedback stays `"pending"`). - - **After Estimate** (if `phases.feedback` is `"pending"`): Output to user: + - **After Estimate**: First offer the what-if workshop per `estimate.md` + (Enter workshop / Proceed toward Generate) when the infra route is active. + Outer Estimate keeps `current_phase: estimate` until workshop is resolved. + Then, if `phases.feedback` is `"pending"`: "Would you like to share quick feedback now? (5 optional questions + anonymized usage data) [A] Yes, share feedback [B] No thanks, continue to Generate" - If user picks **A** → Load `references/phases/feedback/feedback.md`, execute it, then continue to Generate. - If user picks **B** → Use the Phase Status Update Protocol to set `phases.feedback` to `"completed"`. Continue to Generate. + - **Warm start / explicit what-if**: If the user says "what if", "reprice", + "workshop mode", or "compare scenarios" and infra Estimate artifacts exist, + load `references/phases/workshop/workshop.md` (respect Generate re-entry). - **After Generate**: No feedback offer. If `phases.feedback` is still `"pending"`, use the Phase Status Update Protocol to set it to `"completed"` (user had two chances and chose to defer/skip). diff --git a/migrate/plugins/migration-to-aws/skills/gcp-to-aws/references/phases/discover/discover.md b/migrate/plugins/migration-to-aws/skills/gcp-to-aws/references/phases/discover/discover.md index 283194c1..c62de89a 100644 --- a/migrate/plugins/migration-to-aws/skills/gcp-to-aws/references/phases/discover/discover.md +++ b/migrate/plugins/migration-to-aws/skills/gcp-to-aws/references/phases/discover/discover.md @@ -39,12 +39,13 @@ Multiple artifacts can be produced in a single run — they are not mutually exc "last_updated": "[ISO 8601 timestamp]", "current_phase": "discover", "phases": { - "discover": "in_progress", - "clarify": "pending", - "design": "pending", - "estimate": "pending", - "generate": "pending", - "feedback": "pending" + "discover": "in_progress", + "clarify": "pending", + "design": "pending", + "estimate": "pending", + "workshop": "pending", + "generate": "pending", + "feedback": "pending" } } ``` diff --git a/migrate/plugins/migration-to-aws/skills/gcp-to-aws/references/phases/estimate/estimate.md b/migrate/plugins/migration-to-aws/skills/gcp-to-aws/references/phases/estimate/estimate.md index 63e71c2f..68707635 100644 --- a/migrate/plugins/migration-to-aws/skills/gcp-to-aws/references/phases/estimate/estimate.md +++ b/migrate/plugins/migration-to-aws/skills/gcp-to-aws/references/phases/estimate/estimate.md @@ -131,12 +131,46 @@ Load `shared/handoff-gates.md`. **Re-read from disk** each active estimate artif **On PASS:** Emit `HANDOFF_OK | phase=estimate | artifacts=`. -After `HANDOFF_OK`, use the Phase Status Update Protocol (read-merge-write) to update `.phase-status.json` — **in the same turn** as the output message below: +### Inner workshop reprice — skip state transition -- Set `phases.estimate` to `"completed"` -- Set `current_phase` to `"generate"` +When Estimate is invoked from `workshop-refresh.md` (inner reprice): write the +estimate artifact(s), present a brief summary, then **return to the workshop +loop**. Do **not** emit `HANDOFF_OK`, do **not** update `.phase-status.json`, do +**not** offer the what-if workshop below. -Output to user: "Cost estimation complete. Proceeding to Phase 5: Generate Migration Artifacts." +### Outer Estimate — deferred Generate advance + +After outer-run `HANDOFF_OK`, use the Phase Status Update Protocol +(read-merge-write) — **in the same turn** as the summary: + +1. Set `phases.estimate` to `"completed"` +2. Ensure `phases.workshop` exists (seed `"pending"` if missing) +3. **Do not** set `current_phase` to `"generate"` yet — leave `current_phase` at + `"estimate"` until the workshop checkpoint is resolved (entered then exited, + or declined) +4. Offer the what-if workshop below (infra route only) + +### Post-Estimate: What-If Workshop Offer + +When `gcp-resource-inventory.json` + `aws-design.json` + `estimation-infra.json` +exist, offer: + +``` +Estimate complete. Before Generate, you can run a what-if workshop: +change region, availability/HA, compute (EKS/ECS), or Graviton preference +and compare priced scenarios without re-discovering inventory. + +[A] Enter what-if workshop +[B] Proceed toward Generate +``` + +- **A** → Load `references/phases/workshop/workshop.md`. Keep + `current_phase: estimate`; set `phases.workshop` → `"in_progress"`. +- **B** → Mark `phases.workshop` → `"completed"`. Set `current_phase` → + `"generate"`. Continue with Feedback/Generate checkpoints in `SKILL.md`. + +For AI-only / billing-only runs (no infra inventory), skip the workshop offer and +set `phases.workshop` → `"completed"`, `current_phase` → `"generate"`. ## Reference Files diff --git a/migrate/plugins/migration-to-aws/skills/gcp-to-aws/references/phases/workshop/workshop-assemble.md b/migrate/plugins/migration-to-aws/skills/gcp-to-aws/references/phases/workshop/workshop-assemble.md new file mode 100644 index 00000000..d526cb18 --- /dev/null +++ b/migrate/plugins/migration-to-aws/skills/gcp-to-aws/references/phases/workshop/workshop-assemble.md @@ -0,0 +1,25 @@ +# Workshop — Assemble (checkpoint resolve) + +> Marks the workshop checkpoint resolved and returns control to the backbone. +> Does **not** set `current_phase` to `workshop`. + +## When exiting to Generate + +1. Set `preferences.workshop.active` to `false` (keep `active_scenario_id`). +2. Ensure `scenarios/index.json` exists (baseline-only is enough). +3. Update `.phase-status.json` (read-merge-write): + - `phases.workshop` → `"completed"` + - `current_phase` → `"generate"` + - `last_updated` → now +4. Emit: + +``` +HANDOFF_OK | phase=workshop | artifacts=scenarios/index.json | return_to=generate +``` + +5. Output: "Workshop paused. Active scenario: `{id}`. Proceeding toward Generate." + +## Soft postcondition + +If scenarios are missing after an empty entry, warn and still mark workshop +`"completed"` + advance to generate — do not block Generate. diff --git a/migrate/plugins/migration-to-aws/skills/gcp-to-aws/references/phases/workshop/workshop-compare.md b/migrate/plugins/migration-to-aws/skills/gcp-to-aws/references/phases/workshop/workshop-compare.md new file mode 100644 index 00000000..12b54612 --- /dev/null +++ b/migrate/plugins/migration-to-aws/skills/gcp-to-aws/references/phases/workshop/workshop-compare.md @@ -0,0 +1,22 @@ +# Workshop — Compare Scenarios (GCP) + +> Read-only table from `scenarios/index.json`. + +## Steps + +1. Load `scenarios/index.json` (if missing, tell user to Apply once for baseline). +2. For each scenario (baseline first, then by `created_at`), build columns: + - Region ← `design_constraints.target_region.value` + - HA ← `design_constraints.availability.value` + - Compute ← `design_constraints.kubernetes.value` when present + - Arch ← `design_constraints.cpu_architecture.value` when present + - Premium / Balanced / Optimized $/mo ← `estimation_summary` tiers + - Complexity ← `estimation_summary.complexity_tier` +3. Mark the active row. +4. Present: + +| Scenario | Region | HA | Compute | Arch | Premium $/mo | Balanced $/mo | Optimized $/mo | Complexity | +| -------- | ------ | -- | ------- | ---- | ------------ | ------------- | -------------- | ---------- | + +Under the table: active vs baseline `preferences_subset`; any `region_note`; +reminder that inventory is frozen. Keep under 25 lines. diff --git a/migrate/plugins/migration-to-aws/skills/gcp-to-aws/references/phases/workshop/workshop-refresh.md b/migrate/plugins/migration-to-aws/skills/gcp-to-aws/references/phases/workshop/workshop-refresh.md new file mode 100644 index 00000000..0415c87d --- /dev/null +++ b/migrate/plugins/migration-to-aws/skills/gcp-to-aws/references/phases/workshop/workshop-refresh.md @@ -0,0 +1,76 @@ +# Workshop — Refresh (patch → Design → Estimate → snapshot) + +## Inner runs (artifact-only) — mandatory + +When re-running Design or Estimate from this file: **rewrite artifacts only**. +Do **not** advance the backbone mid-workshop. + +| Allowed | Forbidden | +| ------- | --------- | +| Overwrite `aws-design.json` / `estimation-infra.json` | Set design/estimate to `in_progress` | +| Soft-validate estimate invariants before snapshot | Emit `HANDOFF_OK` from Design or Estimate | +| Brief chat note that reprice finished | Touch `current_phase` or advance to `generate` | +| Keep `phases.workshop` as `in_progress` | Run Estimate's post-Estimate workshop offer (recursion) | + +Leave `phases.design` and `phases.estimate` as `"completed"`. Leave +`current_phase` at `"estimate"` until `workshop-assemble.md`. + +Concrete slices: + +1. **Design** — run `design.md` / `design-infra.md` enough to rewrite + `aws-design.json` (and siblings if the run uses them). Skip handoff and + phase-status updates. +2. **Estimate** — run `estimate.md` / `estimate-infra.md` enough to rewrite + `estimation-infra.json`. Skip `HANDOFF_OK`, phase-status, and the workshop offer. + +## Baseline capture + +When `scenarios/` or `scenarios/index.json` is absent: + +1. `inventory_fingerprint` = SHA-256 hex of `gcp-resource-inventory.json` bytes. +2. Create `scenarios/`. +3. Copy working-tree artifacts: + - `scenarios/scenario-001.preferences.json` + - `scenarios/scenario-001.aws-design.json` + - `scenarios/scenario-001.estimation-infra.json` +4. Write `scenarios/scenario-001.json` (`source: "baseline"`, summary from + estimation-infra including three monthly tiers). +5. Write `scenarios/index.json` (`baseline` / `active` = `scenario-001`, + `max_scenarios: 5`). +6. Ensure `preferences.workshop` exists: + `{ "active": true, "last_sheet_at": "", "active_scenario_id": "scenario-001" }` +7. If baseline-only, return to `workshop.md` for the sheet. + +## Apply & reprice + +### 1. Inventory guard + +If fingerprint ≠ `index.inventory_fingerprint`, **STOP**: + +> Inventory changed since baseline. Re-run Discover before workshop reprice. + +### 2. Stale Generate guard + +If generate completed, require re-entry confirm and reset generate to `pending`. + +### 3. Patch preferences + +Apply sheet edits. Set `workshop.active: true`, `workshop.last_sheet_at` now. +Leave non-knob fields (including AI/agentic constraints) untouched. + +### 4–5. Inner Design then Estimate + +Per **Inner runs**. Chat note after Estimate: +"Workshop reprice Estimate complete; returning to workshop loop." + +### 6. Snapshot + +1. Next id `scenario-00N`. +2. If length would exceed 5, **warn and name** oldest non-baseline before delete. +3. Copy prefs / design / estimation into `scenarios/{id}.*`. +4. `preferences_subset`: differing knob paths vs baseline. +5. Update index + `workshop.active_scenario_id`. + +### 7. Hand back + +Return to `workshop.md` → `workshop-compare.md`. diff --git a/migrate/plugins/migration-to-aws/skills/gcp-to-aws/references/phases/workshop/workshop-sheet.md b/migrate/plugins/migration-to-aws/skills/gcp-to-aws/references/phases/workshop/workshop-sheet.md new file mode 100644 index 00000000..558b742a --- /dev/null +++ b/migrate/plugins/migration-to-aws/skills/gcp-to-aws/references/phases/workshop/workshop-sheet.md @@ -0,0 +1,48 @@ +# Workshop — Assumption Sheet (GCP) + +> Confirm-first sheet (same UX as Clarify's Assumption-Sheet Wizard, but **post- +> Estimate** and limited to cost-shaping knobs). Not a full Clarify re-interview. + +## Step 1: Read current knobs + +From `$MIGRATION_DIR/preferences.json`: + +| Knob | Path | Allowed values | +| ---- | ---- | -------------- | +| Target region | `design_constraints.target_region.value` | Valid AWS region code | +| Availability | `design_constraints.availability.value` | `single-az`, `multi-az`, `multi-az-ha`, `multi-region` | +| Kubernetes / compute | `design_constraints.kubernetes.value` | `eks-managed`, `eks-or-ecs`, `ecs-fargate` — omit row if key absent | +| CPU architecture | `design_constraints.cpu_architecture.value` | `graviton`, `x86`, `mixed` — omit row if key absent | + +When patching wrapper objects, preserve `chosen_by`, `prompt`, and +`design_consequence` (set `chosen_by` to `"user"` on edit). + +## Step 2: Present + +Lead with: + +> **What-if workshop** — discovery is frozen. Edit assumptions to reprice. +> Generate/Terraform will be marked stale if you continue after Generate already ran. + +Show knob → current value. Invite confirm-or-change per row. + +**Region / pricing honesty:** + +> Region repricing needs the awspricing MCP for true regional rates. Without it, +> numbers stay based on the us-east-1 pricing cache. + +Actions (exactly one): + +- **[A] Apply & reprice** +- **[B] Compare scenarios** +- **[C] Exit to Generate** +- **[D] Exit to full Clarify** (danger — confirm first) + +## Step 3: Validate (Apply only) + +1. Region is a non-empty AWS region code. +2. Availability is one of the allowed values. +3. Kubernetes / cpu_architecture values are recognized when present. +4. Do not invent BigQuery warehouse targets or agentic migration_approach fields. + +On failure: re-present the sheet — do not Design. diff --git a/migrate/plugins/migration-to-aws/skills/gcp-to-aws/references/phases/workshop/workshop.md b/migrate/plugins/migration-to-aws/skills/gcp-to-aws/references/phases/workshop/workshop.md new file mode 100644 index 00000000..3dc49041 --- /dev/null +++ b/migrate/plugins/migration-to-aws/skills/gcp-to-aws/references/phases/workshop/workshop.md @@ -0,0 +1,54 @@ +# Phase: What-If Workshop (Optional Checkpoint) + +> **Checkpoint**, not a backbone step — same class as Feedback. Entered only when +> the user opts in after Estimate (or says what if / reprice / workshop mode / +> compare scenarios). Never becomes `current_phase`. Returns control to the +> Estimate→Generate flow. Contract: +> `references/shared/schema-workshop-scenarios.md`. + +**Execute ALL steps in order. Do not skip or deviate.** + +## Prerequisites + +1. `$MIGRATION_DIR/.phase-status.json` has `phases.estimate == "completed"`. +2. Infra route artifacts exist: `gcp-resource-inventory.json`, `preferences.json`, + `aws-design.json`, `estimation-infra.json`. +3. **AI-only / billing-only NO-OP for this pilot:** If `gcp-resource-inventory.json` + is missing (AI-only or billing-only run), do **not** enter the infra workshop. + Tell the user this pilot covers IaC infra repricing only; continue toward + Generate / Feedback. + +## Entry + +1. Do **not** re-run Discover / live CLI / Terraform parse. +2. If `phases.generate` is `completed`, require Estimate re-entry confirm and reset + generate (and later) to `pending` before refreshing. +3. Set `phases.workshop` to `"in_progress"`. Do **not** change `current_phase` + (leave at `"estimate"` until exit/decline). + +## Loop + +1. If `scenarios/index.json` missing → load `workshop-refresh.md` § Baseline capture. +2. Load `workshop-sheet.md` — present knobs + actions. +3. Branch: + - **Apply & reprice** → `workshop-refresh.md` → `workshop-compare.md` + - **Compare scenarios** → `workshop-compare.md` + - **Exit to Generate** → `workshop-assemble.md` → return + - **Exit to full Clarify** → danger; Clarify re-entry only on explicit confirm + +## Hard rules + +| Rule | Behavior | +| ---- | -------- | +| Inventory frozen | Never write `gcp-resource-inventory.json`, clusters, or `capture/` | +| Inner Design/Estimate | Artifact rewrite only — see `workshop-refresh.md` § Inner runs | +| Max 5 scenarios | Warn + name eviction before delete | +| Working tree = active | prefs / design / estimation match active scenario | +| No BigQuery target knobs | Keep deferred specialist rows; do not invent warehouse targets | +| No agentic outcome override | Do not mutate `ai_constraints.agentic.*` in v1 | + +## Decline without entering + +When Estimate offer **[B] Proceed toward Generate** is chosen, mark +`phases.workshop` `"completed"` (resolved/declined), set `current_phase` to +`"generate"`, then continue Feedback/Generate checkpoints in `SKILL.md`. diff --git a/migrate/plugins/migration-to-aws/skills/gcp-to-aws/references/shared/schema-phase-status.md b/migrate/plugins/migration-to-aws/skills/gcp-to-aws/references/shared/schema-phase-status.md index a8e6d209..8a650d50 100644 --- a/migrate/plugins/migration-to-aws/skills/gcp-to-aws/references/shared/schema-phase-status.md +++ b/migrate/plugins/migration-to-aws/skills/gcp-to-aws/references/shared/schema-phase-status.md @@ -10,9 +10,10 @@ Lightweight phase tracking. This is the SINGLE source of truth for the `.phase-s "discover": "completed", "clarify": "completed", "design": "in_progress", - "estimate": "pending", - "generate": "pending", - "feedback": "pending" + "estimate": "pending", + "workshop": "pending", + "generate": "pending", + "feedback": "pending" } } ``` @@ -28,5 +29,7 @@ Lightweight phase tracking. This is the SINGLE source of truth for the `.phase-s **Rules:** - Phase status progresses: `"pending"` → `"in_progress"` → `"completed"`. Never goes backward. -- Valid phase names: discover, clarify, design, estimate, generate, feedback. +- Valid phase names: discover, clarify, design, estimate, workshop, generate, feedback. +- `workshop` is an optional **checkpoint** (like feedback): never appears as + `current_phase`; `"completed"` means resolved (entered or declined). - `migration_id` matches the `$MIGRATION_DIR` folder name (e.g., `0226-1430`). diff --git a/migrate/plugins/migration-to-aws/skills/gcp-to-aws/references/shared/schema-workshop-scenarios.md b/migrate/plugins/migration-to-aws/skills/gcp-to-aws/references/shared/schema-workshop-scenarios.md new file mode 100644 index 00000000..dde640ab --- /dev/null +++ b/migrate/plugins/migration-to-aws/skills/gcp-to-aws/references/shared/schema-workshop-scenarios.md @@ -0,0 +1,53 @@ +# Workshop Scenarios — Artifact Contract (GCP) + +> Port of the Heroku what-if workshop contract for `gcp-to-aws`. Discovery +> inventory is **frozen**; workshop mutates `preferences.json` knobs, refreshes +> Design + Estimate, and snapshots the active priced design. + +## Directory layout + +``` +$MIGRATION_DIR/ +├── gcp-resource-inventory.json # FROZEN +├── preferences.json # active scenario preferences +├── aws-design.json # active scenario design +├── estimation-infra.json # active scenario estimate +└── scenarios/ + ├── index.json + ├── scenario-001.json + ├── scenario-001.preferences.json + ├── scenario-001.aws-design.json + ├── scenario-001.estimation-infra.json + └── … +``` + +Max **5** scenarios. Warn + name eviction before deleting oldest non-baseline. + +## `preferences.json` → `workshop` object + +```json +"workshop": { + "active": true, + "last_sheet_at": "2026-07-19T20:00:00Z", + "active_scenario_id": "scenario-002" +} +``` + +Clarify does **not** write this. Workshop creates/patches it. + +## v1 knobs (sheet) + +| Path | Notes | +| ---- | ----- | +| `design_constraints.target_region.value` | AWS region | +| `design_constraints.availability.value` | HA posture | +| `design_constraints.kubernetes.value` | When present | +| `design_constraints.cpu_architecture.value` | `graviton` \| `x86` \| `mixed` when present | + +Cross-skill arch defaults: heroku workshop defaults x86; vercel defaults arm64; +GCP uses Clarify's `graviton`/`x86`/`mixed` vocabulary. + +## Fingerprint + +`inventory_fingerprint` = SHA-256 hex of `gcp-resource-inventory.json` bytes. +Abort refresh on drift. From d7dc26c7506504ba5bb149ffbab8b6305168ddf3 Mon Sep 17 00:00:00 2001 From: Logan Kleier Date: Sun, 19 Jul 2026 17:37:10 -0700 Subject: [PATCH 2/6] =?UTF-8?q?fix(gcp-to-aws):=20address=20workshop=20rev?= =?UTF-8?q?iew=20=E2=80=94=20resume,=20Graviton=20caveat,=20fixtures?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Inner Design skips state transition; sheet/refresh surface graviton_profile risk; SKILL resume never recomputes Estimate when workshop is pending; fixtures use real Q11b prompts and keep incompatible worker on x86. Co-authored-by: Cursor --- .../fixtures/gcp-workshop/README.md | 19 ++++++++++-- .../after-graviton-reprice/aws-design.json | 23 +++++++++++++- .../estimation-infra.json | 19 ++++++++---- .../gcp-resource-inventory.json | 31 +++++++++++++++++++ .../after-graviton-reprice/preferences.json | 23 ++++++++------ .../scenarios/index.json | 4 +-- .../scenarios/scenario-001.aws-design.json | 23 +++++++++++++- .../scenario-001.estimation-infra.json | 12 +++---- .../scenarios/scenario-001.json | 6 ++-- .../scenarios/scenario-001.preferences.json | 22 +++++++------ .../scenarios/scenario-002.aws-design.json | 23 +++++++++++++- .../scenario-002.estimation-infra.json | 19 ++++++++---- .../scenarios/scenario-002.json | 11 ++++--- .../scenarios/scenario-002.preferences.json | 23 ++++++++------ .../gcp-workshop/check_expected_workshop.py | 16 ++++++++-- .../gcp-workshop/expected-workshop.json | 3 ++ .../gcp-workshop/seed/aws-design.json | 23 +++++++++++++- .../gcp-workshop/seed/estimation-infra.json | 12 +++---- .../seed/gcp-resource-inventory.json | 31 +++++++++++++++++++ .../gcp-workshop/seed/preferences.json | 22 +++++++------ .../skills/gcp-to-aws/SKILL.md | 14 ++++++--- .../references/phases/design/design.md | 15 ++++++++- .../phases/workshop/workshop-refresh.md | 17 ++++++++-- .../phases/workshop/workshop-sheet.md | 22 ++++++++++++- .../shared/schema-workshop-scenarios.md | 5 ++- 25 files changed, 350 insertions(+), 88 deletions(-) diff --git a/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/README.md b/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/README.md index 3fa8ab29..9ca05dad 100644 --- a/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/README.md +++ b/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/README.md @@ -4,12 +4,25 @@ Infra-route pilot for `gcp-to-aws` (`references/phases/workshop/`). | Path | Role | | ---- | ---- | -| `seed/` | Post-Estimate baseline (`cpu_architecture=x86`) | -| `after-graviton-reprice/` | After Apply with `cpu_architecture=graviton` | +| `seed/` | Post-Estimate baseline (`cpu_architecture=x86`, real Q11b prompt, mixed graviton_profile) | +| `after-graviton-reprice/` | After Apply with `graviton` + caveat (worker stays x86) | | `check_expected_workshop.py` | Stdlib asserter | ```bash python3 check_expected_workshop.py after-graviton-reprice ``` -Fresh-agent replay bar: copy `seed/` → enter workshop → set arch graviton → Apply → Compare → asserter PASS. +## House replay bar + +Before merge: from `seed/`, enter workshop → set CPU architecture to `graviton` +(sheet must show the incompatible-worker caveat) → Apply & reprice → Compare → +asserter PASS. The committed `after-graviton-reprice/` was produced by that +mechanical path (inventory bytes frozen; worker remains `X86_64`; +`workshop.graviton_note` set). Prefer also a fresh-agent transcript on the PR. + +## Merge coordination + +Land **#149** (GCP live discovery) before or rebase this branch onto it — both +touch `discover.md`. Workshop PRs (#152 Heroku, #153 Vercel, #154 GCP) all edit +`fixtures/README.md`; expect small registry conflicts — last merger wins by +keeping all three bullets. diff --git a/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/after-graviton-reprice/aws-design.json b/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/after-graviton-reprice/aws-design.json index 9b9a0965..6f06a7f6 100644 --- a/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/after-graviton-reprice/aws-design.json +++ b/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/after-graviton-reprice/aws-design.json @@ -3,16 +3,37 @@ "services": [ { "aws_service": "ECS Fargate", + "source_resource_id": "run:web", "aws_config": { "cpu_architecture": "ARM64", "region": "us-east-1" + }, + "graviton": { + "compatibility": "ready", + "target_architecture": "arm64", + "selected": true + } + }, + { + "aws_service": "ECS Fargate", + "source_resource_id": "run:worker", + "aws_config": { + "cpu_architecture": "X86_64", + "region": "us-east-1" + }, + "graviton": { + "compatibility": "incompatible", + "target_architecture": "x86_64", + "selected": false } }, { "aws_service": "RDS PostgreSQL", + "source_resource_id": "sql:main", "aws_config": { "instance_class": "db.t4g.micro", - "multi_az": false + "multi_az": false, + "region": "us-east-1" } } ] diff --git a/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/after-graviton-reprice/estimation-infra.json b/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/after-graviton-reprice/estimation-infra.json index 219c845b..6dd9700c 100644 --- a/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/after-graviton-reprice/estimation-infra.json +++ b/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/after-graviton-reprice/estimation-infra.json @@ -4,19 +4,26 @@ "status": "cached" }, "projected_costs": { - "aws_monthly_premium": 110, - "aws_monthly_balanced": 85, - "aws_monthly_optimized": 62 + "aws_monthly_premium": 110.0, + "aws_monthly_balanced": 85.0, + "aws_monthly_optimized": 62.0 }, "complexity_tier": "small", "recommendation": { "path": "migrate_optimized", - "path_label": "Migrate", + "path_label": "Migrate optimized", "migrate_if": [ - "ok" + "costs favor AWS" ], "stay_if": [ - "not" + "heavy custom networking" ] + }, + "architecture_comparison": { + "graviton_monthly": 85.0, + "x86_equivalent_monthly": 95.0, + "savings_amount": 10.0, + "savings_percent": 10.5, + "note": "Partial Graviton: worker remains x86 (incompatible). Hourly savings only on ready services." } } diff --git a/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/after-graviton-reprice/gcp-resource-inventory.json b/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/after-graviton-reprice/gcp-resource-inventory.json index cd6cace6..c187bd2d 100644 --- a/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/after-graviton-reprice/gcp-resource-inventory.json +++ b/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/after-graviton-reprice/gcp-resource-inventory.json @@ -6,10 +6,41 @@ "type": "google_cloud_run_v2_service", "name": "web" }, + { + "id": "run:worker", + "type": "google_cloud_run_v2_service", + "name": "worker" + }, { "id": "sql:main", "type": "google_sql_database_instance", "name": "main" } + ], + "graviton_profile": [ + { + "service_name": "web", + "tier": "ready", + "target_architecture": "arm64", + "signals": [ + "python-3.11", + "no-native-extensions" + ], + "caveats": [], + "source": "app_code" + }, + { + "service_name": "worker", + "tier": "incompatible", + "target_architecture": "x86_64", + "signals": [ + "platform: linux/amd64", + "native-c-extension" + ], + "caveats": [ + "Pinned linux/amd64 image; native extension lacks arm64 wheel" + ], + "source": "app_code" + } ] } diff --git a/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/after-graviton-reprice/preferences.json b/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/after-graviton-reprice/preferences.json index 73b833d5..515e5260 100644 --- a/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/after-graviton-reprice/preferences.json +++ b/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/after-graviton-reprice/preferences.json @@ -3,31 +3,36 @@ "target_region": { "value": "us-east-1", "chosen_by": "default", - "prompt": "region", - "design_consequence": "deploy region" + "question_id": "Q1", + "prompt": "Where are your users located?", + "design_consequence": "All resources deploy in us-east-1; Bedrock model availability checked for this region" }, "availability": { "value": "single-az", "chosen_by": "default", - "prompt": "ha", - "design_consequence": "RDS AZ" + "question_id": "Q6", + "prompt": "What level of uptime does your application require?", + "design_consequence": "single-az drives RDS single-AZ vs Multi-AZ vs Aurora selection" }, "kubernetes": { "value": "ecs-fargate", "chosen_by": "user", - "prompt": "compute", - "design_consequence": "Fargate" + "question_id": "Q8", + "prompt": "How do you feel about Kubernetes?", + "design_consequence": "ecs-fargate selects EKS vs ECS Fargate vs mixed posture" }, "cpu_architecture": { "value": "graviton", "chosen_by": "user", - "prompt": "arch", - "design_consequence": "instance family" + "question_id": "Q11b", + "prompt": "Some of your services have ARM64 compatibility considerations. Graviton (ARM64) instances are ~15\u201320% cheaper per hour. Your Python workloads appear compatible; worker has a linux/amd64 pin and native C extension. How would you like to proceed?", + "design_consequence": "Graviton for ready + conditional services; x86 only for incompatible ones (worker stays x86)" } }, "workshop": { "active": true, "last_sheet_at": "2026-07-19T19:05:00Z", - "active_scenario_id": "scenario-002" + "active_scenario_id": "scenario-002", + "graviton_note": "1 incompatible \u2014 graviton applies where tier: ready; incompatible entries stay x86" } } diff --git a/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/after-graviton-reprice/scenarios/index.json b/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/after-graviton-reprice/scenarios/index.json index e9869df2..612841cf 100644 --- a/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/after-graviton-reprice/scenarios/index.json +++ b/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/after-graviton-reprice/scenarios/index.json @@ -2,7 +2,7 @@ "baseline_scenario_id": "scenario-001", "active_scenario_id": "scenario-002", "max_scenarios": 5, - "inventory_fingerprint": "0b5d7ee9a96ec6e4c48983e5dbc7fb7e52c134e66da297307d4f7681aeb6929c", + "inventory_fingerprint": "816eb4c04f8d719c01705a86f250f1292937fa13533db7d38173d4182adf84ee", "scenarios": [ { "scenario_id": "scenario-001", @@ -13,7 +13,7 @@ }, { "scenario_id": "scenario-002", - "label": "graviton", + "label": "graviton (caveat: 1 incompatible \u2014 graviton applies where tier: ready; incompatible entries stay x86)", "created_at": "2026-07-19T19:05:00Z", "source": "workshop", "manifest": "scenarios/scenario-002.json" diff --git a/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/after-graviton-reprice/scenarios/scenario-001.aws-design.json b/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/after-graviton-reprice/scenarios/scenario-001.aws-design.json index 92da86f8..8c292de8 100644 --- a/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/after-graviton-reprice/scenarios/scenario-001.aws-design.json +++ b/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/after-graviton-reprice/scenarios/scenario-001.aws-design.json @@ -3,16 +3,37 @@ "services": [ { "aws_service": "ECS Fargate", + "source_resource_id": "run:web", "aws_config": { "cpu_architecture": "X86_64", "region": "us-east-1" + }, + "graviton": { + "compatibility": "ready", + "target_architecture": "arm64", + "selected": false + } + }, + { + "aws_service": "ECS Fargate", + "source_resource_id": "run:worker", + "aws_config": { + "cpu_architecture": "X86_64", + "region": "us-east-1" + }, + "graviton": { + "compatibility": "incompatible", + "target_architecture": "x86_64", + "selected": false } }, { "aws_service": "RDS PostgreSQL", + "source_resource_id": "sql:main", "aws_config": { "instance_class": "db.t4g.micro", - "multi_az": false + "multi_az": false, + "region": "us-east-1" } } ] diff --git a/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/after-graviton-reprice/scenarios/scenario-001.estimation-infra.json b/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/after-graviton-reprice/scenarios/scenario-001.estimation-infra.json index 35031e9c..ac966c0e 100644 --- a/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/after-graviton-reprice/scenarios/scenario-001.estimation-infra.json +++ b/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/after-graviton-reprice/scenarios/scenario-001.estimation-infra.json @@ -4,19 +4,19 @@ "status": "cached" }, "projected_costs": { - "aws_monthly_premium": 120, - "aws_monthly_balanced": 95, - "aws_monthly_optimized": 70 + "aws_monthly_premium": 120.0, + "aws_monthly_balanced": 95.0, + "aws_monthly_optimized": 70.0 }, "complexity_tier": "small", "recommendation": { "path": "migrate_optimized", - "path_label": "Migrate", + "path_label": "Migrate optimized", "migrate_if": [ - "ok" + "costs favor AWS" ], "stay_if": [ - "not" + "heavy custom networking" ] } } diff --git a/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/after-graviton-reprice/scenarios/scenario-001.json b/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/after-graviton-reprice/scenarios/scenario-001.json index 6601f827..c9d901c5 100644 --- a/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/after-graviton-reprice/scenarios/scenario-001.json +++ b/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/after-graviton-reprice/scenarios/scenario-001.json @@ -5,9 +5,9 @@ "source": "baseline", "preferences_subset": {}, "estimation_summary": { - "aws_monthly_premium": 120, - "aws_monthly_balanced": 95, - "aws_monthly_optimized": 70, + "aws_monthly_premium": 120.0, + "aws_monthly_balanced": 95.0, + "aws_monthly_optimized": 70.0, "complexity_tier": "small", "pricing_source": "cached", "region_note": null diff --git a/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/after-graviton-reprice/scenarios/scenario-001.preferences.json b/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/after-graviton-reprice/scenarios/scenario-001.preferences.json index 0b989680..2ec062c6 100644 --- a/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/after-graviton-reprice/scenarios/scenario-001.preferences.json +++ b/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/after-graviton-reprice/scenarios/scenario-001.preferences.json @@ -3,26 +3,30 @@ "target_region": { "value": "us-east-1", "chosen_by": "default", - "prompt": "region", - "design_consequence": "deploy region" + "question_id": "Q1", + "prompt": "Where are your users located?", + "design_consequence": "All resources deploy in us-east-1; Bedrock model availability checked for this region" }, "availability": { "value": "single-az", "chosen_by": "default", - "prompt": "ha", - "design_consequence": "RDS AZ" + "question_id": "Q6", + "prompt": "What level of uptime does your application require?", + "design_consequence": "single-az drives RDS single-AZ vs Multi-AZ vs Aurora selection" }, "kubernetes": { "value": "ecs-fargate", "chosen_by": "user", - "prompt": "compute", - "design_consequence": "Fargate" + "question_id": "Q8", + "prompt": "How do you feel about Kubernetes?", + "design_consequence": "ecs-fargate selects EKS vs ECS Fargate vs mixed posture" }, "cpu_architecture": { "value": "x86", - "chosen_by": "default", - "prompt": "arch", - "design_consequence": "instance family" + "chosen_by": "user", + "question_id": "Q11b", + "prompt": "Some of your services have ARM64 compatibility considerations. Graviton (ARM64) instances are ~15\u201320% cheaper per hour. Your Python workloads appear compatible; worker has a linux/amd64 pin and native C extension. How would you like to proceed?", + "design_consequence": "x86 everywhere; forgoes the ~15\u201320% hourly discount on eligible compute" } }, "workshop": { diff --git a/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/after-graviton-reprice/scenarios/scenario-002.aws-design.json b/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/after-graviton-reprice/scenarios/scenario-002.aws-design.json index 9b9a0965..6f06a7f6 100644 --- a/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/after-graviton-reprice/scenarios/scenario-002.aws-design.json +++ b/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/after-graviton-reprice/scenarios/scenario-002.aws-design.json @@ -3,16 +3,37 @@ "services": [ { "aws_service": "ECS Fargate", + "source_resource_id": "run:web", "aws_config": { "cpu_architecture": "ARM64", "region": "us-east-1" + }, + "graviton": { + "compatibility": "ready", + "target_architecture": "arm64", + "selected": true + } + }, + { + "aws_service": "ECS Fargate", + "source_resource_id": "run:worker", + "aws_config": { + "cpu_architecture": "X86_64", + "region": "us-east-1" + }, + "graviton": { + "compatibility": "incompatible", + "target_architecture": "x86_64", + "selected": false } }, { "aws_service": "RDS PostgreSQL", + "source_resource_id": "sql:main", "aws_config": { "instance_class": "db.t4g.micro", - "multi_az": false + "multi_az": false, + "region": "us-east-1" } } ] diff --git a/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/after-graviton-reprice/scenarios/scenario-002.estimation-infra.json b/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/after-graviton-reprice/scenarios/scenario-002.estimation-infra.json index 219c845b..6dd9700c 100644 --- a/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/after-graviton-reprice/scenarios/scenario-002.estimation-infra.json +++ b/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/after-graviton-reprice/scenarios/scenario-002.estimation-infra.json @@ -4,19 +4,26 @@ "status": "cached" }, "projected_costs": { - "aws_monthly_premium": 110, - "aws_monthly_balanced": 85, - "aws_monthly_optimized": 62 + "aws_monthly_premium": 110.0, + "aws_monthly_balanced": 85.0, + "aws_monthly_optimized": 62.0 }, "complexity_tier": "small", "recommendation": { "path": "migrate_optimized", - "path_label": "Migrate", + "path_label": "Migrate optimized", "migrate_if": [ - "ok" + "costs favor AWS" ], "stay_if": [ - "not" + "heavy custom networking" ] + }, + "architecture_comparison": { + "graviton_monthly": 85.0, + "x86_equivalent_monthly": 95.0, + "savings_amount": 10.0, + "savings_percent": 10.5, + "note": "Partial Graviton: worker remains x86 (incompatible). Hourly savings only on ready services." } } diff --git a/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/after-graviton-reprice/scenarios/scenario-002.json b/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/after-graviton-reprice/scenarios/scenario-002.json index 65fe4731..4f81ca59 100644 --- a/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/after-graviton-reprice/scenarios/scenario-002.json +++ b/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/after-graviton-reprice/scenarios/scenario-002.json @@ -1,15 +1,15 @@ { "scenario_id": "scenario-002", - "label": "graviton", + "label": "graviton (caveat: 1 incompatible \u2014 graviton applies where tier: ready; incompatible entries stay x86)", "created_at": "2026-07-19T19:05:00Z", "source": "workshop", "preferences_subset": { "design_constraints.cpu_architecture.value": "graviton" }, "estimation_summary": { - "aws_monthly_premium": 110, - "aws_monthly_balanced": 85, - "aws_monthly_optimized": 62, + "aws_monthly_premium": 110.0, + "aws_monthly_balanced": 85.0, + "aws_monthly_optimized": 62.0, "complexity_tier": "small", "pricing_source": "cached", "region_note": null @@ -18,5 +18,6 @@ "preferences": "scenarios/scenario-002.preferences.json", "aws_design": "scenarios/scenario-002.aws-design.json", "estimation_infra": "scenarios/scenario-002.estimation-infra.json" - } + }, + "graviton_note": "1 incompatible \u2014 graviton applies where tier: ready; incompatible entries stay x86" } diff --git a/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/after-graviton-reprice/scenarios/scenario-002.preferences.json b/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/after-graviton-reprice/scenarios/scenario-002.preferences.json index 73b833d5..515e5260 100644 --- a/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/after-graviton-reprice/scenarios/scenario-002.preferences.json +++ b/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/after-graviton-reprice/scenarios/scenario-002.preferences.json @@ -3,31 +3,36 @@ "target_region": { "value": "us-east-1", "chosen_by": "default", - "prompt": "region", - "design_consequence": "deploy region" + "question_id": "Q1", + "prompt": "Where are your users located?", + "design_consequence": "All resources deploy in us-east-1; Bedrock model availability checked for this region" }, "availability": { "value": "single-az", "chosen_by": "default", - "prompt": "ha", - "design_consequence": "RDS AZ" + "question_id": "Q6", + "prompt": "What level of uptime does your application require?", + "design_consequence": "single-az drives RDS single-AZ vs Multi-AZ vs Aurora selection" }, "kubernetes": { "value": "ecs-fargate", "chosen_by": "user", - "prompt": "compute", - "design_consequence": "Fargate" + "question_id": "Q8", + "prompt": "How do you feel about Kubernetes?", + "design_consequence": "ecs-fargate selects EKS vs ECS Fargate vs mixed posture" }, "cpu_architecture": { "value": "graviton", "chosen_by": "user", - "prompt": "arch", - "design_consequence": "instance family" + "question_id": "Q11b", + "prompt": "Some of your services have ARM64 compatibility considerations. Graviton (ARM64) instances are ~15\u201320% cheaper per hour. Your Python workloads appear compatible; worker has a linux/amd64 pin and native C extension. How would you like to proceed?", + "design_consequence": "Graviton for ready + conditional services; x86 only for incompatible ones (worker stays x86)" } }, "workshop": { "active": true, "last_sheet_at": "2026-07-19T19:05:00Z", - "active_scenario_id": "scenario-002" + "active_scenario_id": "scenario-002", + "graviton_note": "1 incompatible \u2014 graviton applies where tier: ready; incompatible entries stay x86" } } diff --git a/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/check_expected_workshop.py b/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/check_expected_workshop.py index 757bb285..6afa0998 100644 --- a/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/check_expected_workshop.py +++ b/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/check_expected_workshop.py @@ -23,8 +23,20 @@ def main(): check(len(sc)>=exp['min_scenarios'], 'too few scenarios') check(idx.get('active_scenario_id')==exp['active_scenario_id'], 'active id') prefs=json.loads((run/'preferences.json').read_text()) - arch=(((prefs.get('design_constraints') or {}).get('cpu_architecture') or {}).get('value')) - check(arch==exp['active_cpu_architecture'], f'arch={arch}') + ca=((prefs.get('design_constraints') or {}).get('cpu_architecture') or {}) + check(ca.get('value')==exp['active_cpu_architecture'], f"arch={ca.get('value')}") + if exp.get('q11b_prompt_must_contain'): + check(exp['q11b_prompt_must_contain'] in (ca.get('prompt') or ''), 'Q11b prompt missing real clarify text') + if exp.get('must_have_graviton_note'): + note=(prefs.get('workshop') or {}).get('graviton_note') or '' + check(bool(note), 'missing workshop.graviton_note') + check('incompatible' in note.lower(), 'graviton_note should mention incompatible') + if exp.get('must_keep_incompatible_worker_x86'): + design=json.loads((run/'aws-design.json').read_text()) + worker=next((s for s in design.get('services',[]) if s.get('source_resource_id')=='run:worker'), None) + check(worker is not None, 'missing worker service') + if worker: + check(worker.get('aws_config',{}).get('cpu_architecture')=='X86_64', 'incompatible worker must stay X86_64') est=json.loads((run/'estimation-infra.json').read_text()) base_m=run/'scenarios'/f"{exp['baseline_scenario_id']}.json" if base_m.exists() and exp.get('balanced_must_differ_from_baseline'): diff --git a/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/expected-workshop.json b/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/expected-workshop.json index 2fa79588..164b17eb 100644 --- a/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/expected-workshop.json +++ b/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/expected-workshop.json @@ -8,5 +8,8 @@ "generate_must_not_be_completed": true, "current_phase_must_be": "estimate", "workshop_phase_must_be": "in_progress", + "must_have_graviton_note": true, + "must_keep_incompatible_worker_x86": true, + "q11b_prompt_must_contain": "ARM64 compatibility", "max_scenarios": 5 } diff --git a/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/seed/aws-design.json b/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/seed/aws-design.json index 92da86f8..8c292de8 100644 --- a/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/seed/aws-design.json +++ b/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/seed/aws-design.json @@ -3,16 +3,37 @@ "services": [ { "aws_service": "ECS Fargate", + "source_resource_id": "run:web", "aws_config": { "cpu_architecture": "X86_64", "region": "us-east-1" + }, + "graviton": { + "compatibility": "ready", + "target_architecture": "arm64", + "selected": false + } + }, + { + "aws_service": "ECS Fargate", + "source_resource_id": "run:worker", + "aws_config": { + "cpu_architecture": "X86_64", + "region": "us-east-1" + }, + "graviton": { + "compatibility": "incompatible", + "target_architecture": "x86_64", + "selected": false } }, { "aws_service": "RDS PostgreSQL", + "source_resource_id": "sql:main", "aws_config": { "instance_class": "db.t4g.micro", - "multi_az": false + "multi_az": false, + "region": "us-east-1" } } ] diff --git a/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/seed/estimation-infra.json b/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/seed/estimation-infra.json index 35031e9c..ac966c0e 100644 --- a/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/seed/estimation-infra.json +++ b/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/seed/estimation-infra.json @@ -4,19 +4,19 @@ "status": "cached" }, "projected_costs": { - "aws_monthly_premium": 120, - "aws_monthly_balanced": 95, - "aws_monthly_optimized": 70 + "aws_monthly_premium": 120.0, + "aws_monthly_balanced": 95.0, + "aws_monthly_optimized": 70.0 }, "complexity_tier": "small", "recommendation": { "path": "migrate_optimized", - "path_label": "Migrate", + "path_label": "Migrate optimized", "migrate_if": [ - "ok" + "costs favor AWS" ], "stay_if": [ - "not" + "heavy custom networking" ] } } diff --git a/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/seed/gcp-resource-inventory.json b/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/seed/gcp-resource-inventory.json index cd6cace6..c187bd2d 100644 --- a/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/seed/gcp-resource-inventory.json +++ b/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/seed/gcp-resource-inventory.json @@ -6,10 +6,41 @@ "type": "google_cloud_run_v2_service", "name": "web" }, + { + "id": "run:worker", + "type": "google_cloud_run_v2_service", + "name": "worker" + }, { "id": "sql:main", "type": "google_sql_database_instance", "name": "main" } + ], + "graviton_profile": [ + { + "service_name": "web", + "tier": "ready", + "target_architecture": "arm64", + "signals": [ + "python-3.11", + "no-native-extensions" + ], + "caveats": [], + "source": "app_code" + }, + { + "service_name": "worker", + "tier": "incompatible", + "target_architecture": "x86_64", + "signals": [ + "platform: linux/amd64", + "native-c-extension" + ], + "caveats": [ + "Pinned linux/amd64 image; native extension lacks arm64 wheel" + ], + "source": "app_code" + } ] } diff --git a/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/seed/preferences.json b/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/seed/preferences.json index 0b989680..2ec062c6 100644 --- a/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/seed/preferences.json +++ b/migrate/plugins/migration-to-aws/fixtures/gcp-workshop/seed/preferences.json @@ -3,26 +3,30 @@ "target_region": { "value": "us-east-1", "chosen_by": "default", - "prompt": "region", - "design_consequence": "deploy region" + "question_id": "Q1", + "prompt": "Where are your users located?", + "design_consequence": "All resources deploy in us-east-1; Bedrock model availability checked for this region" }, "availability": { "value": "single-az", "chosen_by": "default", - "prompt": "ha", - "design_consequence": "RDS AZ" + "question_id": "Q6", + "prompt": "What level of uptime does your application require?", + "design_consequence": "single-az drives RDS single-AZ vs Multi-AZ vs Aurora selection" }, "kubernetes": { "value": "ecs-fargate", "chosen_by": "user", - "prompt": "compute", - "design_consequence": "Fargate" + "question_id": "Q8", + "prompt": "How do you feel about Kubernetes?", + "design_consequence": "ecs-fargate selects EKS vs ECS Fargate vs mixed posture" }, "cpu_architecture": { "value": "x86", - "chosen_by": "default", - "prompt": "arch", - "design_consequence": "instance family" + "chosen_by": "user", + "question_id": "Q11b", + "prompt": "Some of your services have ARM64 compatibility considerations. Graviton (ARM64) instances are ~15\u201320% cheaper per hour. Your Python workloads appear compatible; worker has a linux/amd64 pin and native C extension. How would you like to proceed?", + "design_consequence": "x86 everywhere; forgoes the ~15\u201320% hourly discount on eligible compute" } }, "workshop": { diff --git a/migrate/plugins/migration-to-aws/skills/gcp-to-aws/SKILL.md b/migrate/plugins/migration-to-aws/skills/gcp-to-aws/SKILL.md index f60b765d..a5b649bc 100644 --- a/migrate/plugins/migration-to-aws/skills/gcp-to-aws/SKILL.md +++ b/migrate/plugins/migration-to-aws/skills/gcp-to-aws/SKILL.md @@ -86,16 +86,22 @@ This is the execution controller. After completing each phase, consult this tabl | `clarify` | `phases.discover == "completed"` AND `phases.clarify != "completed"` | Load `references/phases/clarify/clarify.md` | | `design` | `phases.clarify == "completed"` AND `phases.design != "completed"` | Load `references/phases/design/design.md` | | `estimate` | `phases.design == "completed"` AND `phases.estimate != "completed"` | Load `references/phases/estimate/estimate.md` | -| `generate` | `phases.estimate == "completed"` AND `phases.generate != "completed"` | Load `references/phases/generate/generate.md` | +| `workshop` | `current_phase == "estimate"` AND `phases.estimate == "completed"` AND `phases.workshop` is `"pending"` or `"in_progress"` | **Do not recompute Estimate.** If `workshop` is `"pending"`, present the post-Estimate workshop offer from `estimate.md`. If `"in_progress"`, load `references/phases/workshop/workshop.md`. | +| `generate` | `phases.estimate == "completed"` AND `phases.workshop == "completed"` AND `phases.generate != "completed"` | Load `references/phases/generate/generate.md` (workshop resolved — entered+exited or declined) | | `complete` | `phases.generate == "completed"` AND `phases.feedback == "pending"` | Set `phases.feedback` to `"completed"` (user had two chances), then migration complete | | `complete` | `phases.generate == "completed"` AND `phases.feedback == "completed"` | Migration planning complete | **How to determine current state (deterministic):** 1. Read `$MIGRATION_DIR/.phase-status.json` -2. If `current_phase` exists, use it (must match one of: discover, clarify, design, estimate, generate, complete) -3. Otherwise use ordered phase evaluation: `discover` → `clarify` → `design` → `estimate` → `generate` -4. Pick the **first** phase in that order where `phases. != "completed"`; if none, state is `complete` +2. **Workshop resume (mandatory):** If `current_phase == "estimate"` AND + `phases.estimate == "completed"` AND `phases.workshop` is `"pending"` or + `"in_progress"`, follow the `workshop` row above — **never** re-run Estimate + on a plain "continue my migration" / resume. Explicit "what if" / "reprice" / + "workshop mode" phrases also load `workshop.md` when Estimate artifacts exist. +3. If `current_phase` exists (and step 2 did not apply), use it (must match one of: discover, clarify, design, estimate, generate, complete) +4. Otherwise use ordered phase evaluation: `discover` → `clarify` → `design` → `estimate` → `generate` +5. Pick the **first** phase in that order where `phases. != "completed"`; if none, state is `complete`. When evaluating `generate`, require `phases.workshop == "completed"` (seed `"pending"` on Discover so a missing key is not treated as resolved). **Phase gate checks**: If prior phase incomplete, do not advance (e.g., cannot enter estimate without completed design). diff --git a/migrate/plugins/migration-to-aws/skills/gcp-to-aws/references/phases/design/design.md b/migrate/plugins/migration-to-aws/skills/gcp-to-aws/references/phases/design/design.md index b3c73975..1d7ee4e3 100644 --- a/migrate/plugins/migration-to-aws/skills/gcp-to-aws/references/phases/design/design.md +++ b/migrate/plugins/migration-to-aws/skills/gcp-to-aws/references/phases/design/design.md @@ -72,7 +72,20 @@ Load `shared/handoff-gates.md`. **Re-read from disk** each active route artifact **On PASS:** Emit `HANDOFF_OK | phase=design | artifacts=`. -After `HANDOFF_OK`, use the Phase Status Update Protocol (read-merge-write) to update `.phase-status.json` — **in the same turn** as the output message below: +### Inner workshop reprice — skip state transition + +When Design is invoked from `workshop-refresh.md` (inner reprice): rewrite the +active design artifact(s) (`aws-design.json` and siblings as applicable), then +**return to the workshop loop**. Do **not** emit `HANDOFF_OK`, do **not** set +`phases.design` to `"in_progress"` or re-stamp `"completed"`, do **not** change +`current_phase`, and do **not** treat Estimate as stale for a Generate reset +unless the user is already past Generate (see workshop-refresh stale-Generate +guard). Leave `phases.design` and `phases.estimate` as `"completed"` and +`current_phase` at `"estimate"` while `phases.workshop` is `"in_progress"`. + +After outer-run `HANDOFF_OK`, use the Phase Status Update Protocol +(read-merge-write) to update `.phase-status.json` — **in the same turn** as the +output message below: - Set `phases.design` to `"completed"` - Set `current_phase` to `"estimate"` diff --git a/migrate/plugins/migration-to-aws/skills/gcp-to-aws/references/phases/workshop/workshop-refresh.md b/migrate/plugins/migration-to-aws/skills/gcp-to-aws/references/phases/workshop/workshop-refresh.md index 0415c87d..4ff89a21 100644 --- a/migrate/plugins/migration-to-aws/skills/gcp-to-aws/references/phases/workshop/workshop-refresh.md +++ b/migrate/plugins/migration-to-aws/skills/gcp-to-aws/references/phases/workshop/workshop-refresh.md @@ -58,9 +58,19 @@ If generate completed, require re-entry confirm and reset generate to `pending`. Apply sheet edits. Set `workshop.active: true`, `workshop.last_sheet_at` now. Leave non-knob fields (including AI/agentic constraints) untouched. +**Graviton caveat carry-forward:** If the sheet showed a Graviton risk-signal +caveat (any `graviton_profile` entry with `tier` in +`{incompatible, conditional, unknown}`) and the SA set +`cpu_architecture` to `graviton` or `mixed`, set +`preferences.workshop.graviton_note` to a short string naming the counts, e.g. +`"1 incompatible, 0 conditional — graviton applies where tier: ready"`. Clear +`graviton_note` when arch is `x86` or when no risk-signal tiers exist. + ### 4–5. Inner Design then Estimate -Per **Inner runs**. Chat note after Estimate: +Per **Inner runs**. Design must follow `design.md` § Inner workshop reprice +(skip handoff / phase-status). Estimate must follow `estimate.md` § Inner +workshop reprice. Chat note after Estimate: "Workshop reprice Estimate complete; returning to workshop loop." ### 6. Snapshot @@ -69,7 +79,10 @@ Per **Inner runs**. Chat note after Estimate: 2. If length would exceed 5, **warn and name** oldest non-baseline before delete. 3. Copy prefs / design / estimation into `scenarios/{id}.*`. 4. `preferences_subset`: differing knob paths vs baseline. -5. Update index + `workshop.active_scenario_id`. +5. Label: summarize the subset; if `workshop.graviton_note` is set, append + `(caveat: )` to the scenario `label` and copy the note into + the manifest as `graviton_note`. +6. Update index + `workshop.active_scenario_id`. ### 7. Hand back diff --git a/migrate/plugins/migration-to-aws/skills/gcp-to-aws/references/phases/workshop/workshop-sheet.md b/migrate/plugins/migration-to-aws/skills/gcp-to-aws/references/phases/workshop/workshop-sheet.md index 558b742a..7c69dc32 100644 --- a/migrate/plugins/migration-to-aws/skills/gcp-to-aws/references/phases/workshop/workshop-sheet.md +++ b/migrate/plugins/migration-to-aws/skills/gcp-to-aws/references/phases/workshop/workshop-sheet.md @@ -15,7 +15,27 @@ From `$MIGRATION_DIR/preferences.json`: | CPU architecture | `design_constraints.cpu_architecture.value` | `graviton`, `x86`, `mixed` — omit row if key absent | When patching wrapper objects, preserve `chosen_by`, `prompt`, and -`design_consequence` (set `chosen_by` to `"user"` on edit). +`design_consequence` (set `chosen_by` to `"user"` on edit). Prefer the catalog +prompts from `schema-preferences.md` / Q11b text from `clarify-compute.md` when +present on the wrappers — do not invent placeholder prompts. + +### Graviton evidence (CPU architecture row) + +Read `graviton_profile[]` from discovery artifacts (`gcp-resource-inventory.json` +and/or `ai-workload-profile.json` / merged discover outputs — wherever Discover +wrote the array). Count entries whose `tier` is `incompatible`, `conditional`, +or `unknown` (risk-signal tiers per `clarify-compute.md` Q11b). + +When presenting the CPU architecture row: + +- If any risk-signal tiers exist, **always** show a one-line caveat under the + row, e.g.: + > 2 entries are Graviton-incompatible / conditional — choosing `graviton` + > applies where `tier: ready`; incompatible entries stay x86 (same semantics + > as Clarify Q11b "all eligible"). Prefer `mixed` for an explicit split. +- Do **not** silently imply 100% Graviton savings when risk signals exist. +- SA may still pick `graviton` / `x86` / `mixed`; the caveat travels into the + scenario label/note on Apply (see `workshop-refresh.md`). ## Step 2: Present diff --git a/migrate/plugins/migration-to-aws/skills/gcp-to-aws/references/shared/schema-workshop-scenarios.md b/migrate/plugins/migration-to-aws/skills/gcp-to-aws/references/shared/schema-workshop-scenarios.md index dde640ab..9c47cfb2 100644 --- a/migrate/plugins/migration-to-aws/skills/gcp-to-aws/references/shared/schema-workshop-scenarios.md +++ b/migrate/plugins/migration-to-aws/skills/gcp-to-aws/references/shared/schema-workshop-scenarios.md @@ -29,11 +29,14 @@ Max **5** scenarios. Warn + name eviction before deleting oldest non-baseline. "workshop": { "active": true, "last_sheet_at": "2026-07-19T20:00:00Z", - "active_scenario_id": "scenario-002" + "active_scenario_id": "scenario-002", + "graviton_note": "1 incompatible — graviton applies where tier: ready" } ``` Clarify does **not** write this. Workshop creates/patches it. +`graviton_note` is optional — set when the sheet showed Graviton risk-signal +tiers and the SA picked `graviton` or `mixed` (see `workshop-sheet.md`). ## v1 knobs (sheet) From ead6b1f337aa5ab7de9ad164c19c6d9ebe6b4064 Mon Sep 17 00:00:00 2001 From: Logan Kleier Date: Sun, 19 Jul 2026 18:11:33 -0700 Subject: [PATCH 3/6] feat(gcp-to-aws): render what-if scenarios in migration report MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Add optional what-if-scenarios executive section from scenarios/index.json and enforce it in validate-migration-report.py when ≥2 workshop variants exist. Co-authored-by: Cursor --- .../scripts/validate-migration-report.py | 19 ++++++++++ .../generate/generate-artifacts-report.md | 35 +++++++++++++++++-- 2 files changed, 52 insertions(+), 2 deletions(-) diff --git a/migrate/plugins/migration-to-aws/scripts/validate-migration-report.py b/migrate/plugins/migration-to-aws/scripts/validate-migration-report.py index cc8ad9fe..15624ca5 100644 --- a/migrate/plugins/migration-to-aws/scripts/validate-migration-report.py +++ b/migrate/plugins/migration-to-aws/scripts/validate-migration-report.py @@ -44,6 +44,7 @@ "exec-tco", "exec-architecture", "exec-security-teaser", + "what-if-scenarios", "appendix-ai", "appendix-config", "appendix-security", @@ -89,6 +90,7 @@ "exec-costs", "exec-architecture", "exec-security-teaser", + "what-if-scenarios", "exec-timeline", "exec-risks", ) @@ -492,6 +494,23 @@ def validate_report( # Catch verbatim copies of the reference fixture into a real run. errors.extend(_validate_fixture_bleed(html, migration_dir)) + # What-if workshop scenario table — required when ≥2 scenarios were snapshotted. + if migration_dir is not None: + index_path = migration_dir / "scenarios" / "index.json" + if index_path.is_file(): + try: + index = json.loads(index_path.read_text(encoding="utf-8")) + except (OSError, json.JSONDecodeError): + index = None + scenarios = (index or {}).get("scenarios") or [] + counts = _section_id_counts(html) + if len(scenarios) >= 2 and counts.get("what-if-scenarios", 0) < 1: + errors.append( + 'scenarios/index.json has ≥2 scenarios but no ' + '
(workshop compare table is ' + "required in the migration report when variants exist)" + ) + return errors diff --git a/migrate/plugins/migration-to-aws/skills/gcp-to-aws/references/phases/generate/generate-artifacts-report.md b/migrate/plugins/migration-to-aws/skills/gcp-to-aws/references/phases/generate/generate-artifacts-report.md index aadf07e9..b48a3464 100644 --- a/migrate/plugins/migration-to-aws/skills/gcp-to-aws/references/phases/generate/generate-artifacts-report.md +++ b/migrate/plugins/migration-to-aws/skills/gcp-to-aws/references/phases/generate/generate-artifacts-report.md @@ -64,6 +64,7 @@ Gather data from all available artifacts. Each section below notes which artifac | **Terraform validation status** | `validation-report.json` → `status`, `provider_version` | — | | **Pricing confidence / staleness** | `estimation-infra.json` → `pricing_source`, `accuracy_confidence` | `estimation-ai.json` accuracy fields | | **AI optimization opportunities** | `estimation-ai.json` → `optimization_opportunities`, `optimized_projection` | — | +| **What-if workshop scenarios** | `scenarios/index.json` + each `scenarios/scenario-NNN.json` manifest (`estimation_summary`, `preferences_subset`, optional `graviton_note`) | — (omit section when workshop unused) | ## Step 1: Build Executive Summary Section @@ -175,6 +176,35 @@ Source: `aws-design.json`, `generation-infra.json` - Source: estimation artifact +**Section 3b — What-if scenarios (`what-if-scenarios`, OPTIONAL):** + +Render **only when** `$MIGRATION_DIR/scenarios/index.json` exists and +`scenarios[]` has **≥ 2** entries (baseline + at least one workshop variant). +Omit entirely when workshop was declined or never entered. + +1. Load `scenarios/index.json`. For each entry (baseline first, then by + `created_at`), read the manifest at `entry.manifest`. +2. HTML table matching `workshop-compare.md` columns: + +| Scenario | Region | HA | Compute | Arch | Premium $/mo | Balanced $/mo | Optimized $/mo | Complexity | +| -------- | ------ | -- | ------- | ---- | ------------ | ------------- | -------------- | ---------- | + + Resolve knobs from each scenario's preferences copy (or + `preferences_subset` / `estimation_summary` on the manifest): Region ← + `design_constraints.target_region.value`; HA ← + `design_constraints.availability.value`; Compute ← + `design_constraints.kubernetes.value` when present; Arch ← + `design_constraints.cpu_architecture.value`. Costs and complexity ← + `estimation_summary`. +3. Mark the active row (`scenario_id == index.active_scenario_id`) with + `class="active-scenario"` or an "(active)" label. +4. Under the table: active vs baseline knob deltas (plain language); any + `graviton_note` / `region_note`; remind that discovery inventory is frozen + and generated Terraform matches the **active** scenario only. +5. TOC: link `#what-if-scenarios` only when rendered. Place this section in the + executive flow immediately after `exec-costs` (before security teaser / + timeline). + **Section 4 — Security & Cost Guardrails (teaser — full table in Appendix G):** Show top controls as a compact teaser: @@ -520,7 +550,7 @@ The output MUST include these `id` attributes (content from Steps 1–2; gates c | `appendix-steps` | Appendix C | | `appendix-artifacts` | Appendix E | -Optional IDs (include when data exists): `exec-tco`, `exec-architecture`, `exec-security-teaser`, `appendix-ai`, `appendix-config`, `appendix-security`, `appendix-security-gap`, `appendix-assumptions`. +Optional IDs (include when data exists): `exec-tco`, `exec-architecture`, `exec-security-teaser`, `what-if-scenarios`, `appendix-ai`, `appendix-config`, `appendix-security`, `appendix-security-gap`, `appendix-assumptions`. ```html @@ -539,6 +569,7 @@ Optional IDs (include when data exists): `exec-tco`, `exec-architecture`, `exec-
+
@@ -636,7 +667,7 @@ These move from "example in the fixture" to enforced gate. See `references/share 4. **Expand acronyms** on first use and include a glossary (TCO, DMS, OAI, RTO, CUD, SCC, IMDSv2, P95, RAG) in the assumptions section — the audience is startup founders, not AWS specialists. 5. **Accessible tables and diagrams.** Every table has a `` and `scope="col"` on header cells. The architecture diagram is wrapped in `
` with a `
` text alternative. 6. **State the verdict.** The decision summary includes a one-sentence recommendation banner (e.g. "Recommendation: Migrate, phased over 10 weeks — ~$497/mo savings, BigQuery deferred") in addition to the `path_label` badges. -7. **Reader vocabulary in the executive flow.** Artifact filenames (`estimation-infra.json`) and Terraform resource IDs (`aws_guardduty_detector.baseline`) are internal build vocabulary. Use them only in the technical appendices (`appendix-services`, `appendix-costs`, `appendix-security`, `appendix-artifacts`, etc.). In the executive flow (`decision-summary`, `exec-tco`, `exec-costs`, `exec-services`, `exec-architecture`, `exec-security-teaser`, `exec-timeline`, `exec-risks`), name things by what the reader controls — "the generated security baseline", "the infrastructure cost estimate" — not by the file or resource that produced them. Rewrite tooling-availability notes (e.g. "awsknowledge MCP not invoked") to reader-facing impact, or drop them. The validator fails on a `*.json` artifact filename or an `aws_.` Terraform ID inside any `exec-*` or `decision-summary` section. +7. **Reader vocabulary in the executive flow.** Artifact filenames (`estimation-infra.json`) and Terraform resource IDs (`aws_guardduty_detector.baseline`) are internal build vocabulary. Use them only in the technical appendices (`appendix-services`, `appendix-costs`, `appendix-security`, `appendix-artifacts`, etc.). In the executive flow (`decision-summary`, `exec-tco`, `exec-costs`, `exec-services`, `exec-architecture`, `exec-security-teaser`, `what-if-scenarios`, `exec-timeline`, `exec-risks`), name things by what the reader controls — "the generated security baseline", "the infrastructure cost estimate", "workshop scenario comparison" — not by the file or resource that produced them. Rewrite tooling-availability notes (e.g. "awsknowledge MCP not invoked") to reader-facing impact, or drop them. The validator fails on a `*.json` artifact filename or an `aws_.` Terraform ID inside any `exec-*`, `what-if-scenarios`, or `decision-summary` section. 8. **One name per concept.** Use a single consistent label for each recommended choice across the whole report. The recommended Bedrock model and the chosen cost tier keep the same name in the verdict, tables, and appendices (always "Claude Sonnet 4.6 (recommended)", always "Balanced"). Do not alternate "recommended / selected target / design target / projected" for the same item — one label is how the reader keeps their bearings. 9. **Ordered action lists.** In `decision-summary`, `Key decisions ahead` and `Next steps` MUST use `
    `, not `
      `. The validator fails when either heading is followed by a bullet list. `Migrate if` / `Stay if` remain unordered lists. From 3de3532ba8b0923c8ec99f352182208c13bf5573 Mon Sep 17 00:00:00 2001 From: Logan Kleier Date: Sun, 19 Jul 2026 18:11:46 -0700 Subject: [PATCH 4/6] docs(gcp-to-aws): document what-if-scenarios report validation check Co-authored-by: Cursor --- .../gcp-to-aws/references/shared/validate-migration-report.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/migrate/plugins/migration-to-aws/skills/gcp-to-aws/references/shared/validate-migration-report.md b/migrate/plugins/migration-to-aws/skills/gcp-to-aws/references/shared/validate-migration-report.md index 72ab59ea..41c96a96 100644 --- a/migrate/plugins/migration-to-aws/skills/gcp-to-aws/references/shared/validate-migration-report.md +++ b/migrate/plugins/migration-to-aws/skills/gcp-to-aws/references/shared/validate-migration-report.md @@ -65,6 +65,7 @@ This validator is a **structural + readability completeness gate**. It does **no | 14 | Readability — reader vocabulary | No artifact filename (`*.json`) or Terraform resource ID (`aws_.`) inside any `exec-*` / `decision-summary` section. The executive flow names what the reader controls; those identifiers live only in the appendices | | 15 | Ordered action lists | When `Key decisions ahead` or `Next steps` headings exist in `decision-summary`, the following list is `
        `, not `
          ` | | 16 | Configuration provenance | When `
          ` exists: table includes Question/Assumption and Design consequence columns; ≥2 data rows | +| 17 | What-if scenarios | With `--migration-dir`: if `scenarios/index.json` lists ≥2 scenarios, exactly one `
          ` (workshop compare table) | Checks 9, 10, and 14 scan the `` with `