Imported from aws/agent-toolkit-for-aws (
plugins/aws-startup-advisor/skills/llm-to-bedrock/SKILL.md). Install upstream withnpx skills add aws/agent-toolkit-for-aws --skill llm-to-bedrock. Copyright stays with the author.
Migrate to Bedrock (Assess + Execute)
Single-command AI migration: OpenAI / Gemini / Anthropic → Amazon Bedrock.
Requires the gcp-to-aws OR the azure-to-aws skill installed alongside this one. This
skill has no standalone Assess implementation — Phase A below runs Assess by directly reading
and executing whichever of those two skills is present's own phase instruction files in this
same session (the same inline-execution pattern agent-advisor uses for its migration-plan
phase: no cross-skill tool call, no turn boundary, and no dependency on your agent supporting a
Skill/subagent dispatch mechanism — reading a file works on any agent). There is no fallback
path that performs Assess itself if neither sibling is present. If you installed this skill on
its own (e.g. a single-skill npx skills add), install gcp-to-aws or azure-to-aws too
before using it.
The skill base directory is given in the "Base directory for this skill: X" line the harness
emits at load time. Call it <SKILL_BASE>. Derived paths:
$SCRIPTS=<SKILL_BASE>/scripts$HELPERS=<SKILL_BASE>/references/helpers(the former helper skills, now references)$ASSESS_SKILL/$ASSESS_BASE— resolved once, in Step 0b:$ASSESS_SKILLis whichever ofgcp-to-aws/azure-to-awsis installed alongside this skill (gcp-to-awspreferred when both are present), and$ASSESS_BASE=<SKILL_BASE>/../$ASSESS_SKILL— that sibling skill's own directory. Phase A reads its instruction files directly off this path; every relative reference inside an$ASSESS_SKILLfile (references/shared/...,references/phases/..., etc.) resolves under$ASSESS_BASE, exactly as it would if$ASSESS_SKILLwere running standalone.
Optional usage telemetry
Before starting or resuming, load references/vendored/telemetry/PROTOCOL.md and
run its read-only status check. Use the returned reporting mode rather than the
model's identity. Complete the existing notice exchange only when that protocol
requires it; unavailable or declined telemetry never blocks this skill.
Step 0 — Check prerequisites
0a. Check that uv is available
uv --version 2>/dev/null || echo "MISSING"
If missing: "Install uv first — see the official install guide: https://docs.astral.sh/uv/getting-started/installation/ (e.g. brew install uv or pipx install uv)". Stop.
0a-bis. Route: full migration vs. access-only
This skill has two modes. Decide which the user wants before the Assess-sibling check (0b)
— the access-only mode does not use gcp-to-aws/azure-to-aws at all, so requiring one of them
there would block a user who only wants access enablement.
Route to Access-only mode (jump to the "## Access-only mode" section below, skip 0b and Steps 1+) when either is true:
$ARGUMENTScontains an explicit access-request phrase —model access,request access,enable model access,just access, orpreflight(and no source-code path). Do not route on the bare wordsenableoraccessalone — "enable the Bedrock rewrite" is a full migration, not an access request. When in doubt, use the AskUserQuestion below rather than the keyword. Or- the user's request is about getting Bedrock model access enabled rather than rewriting code — e.g. "help me request access to Claude on Bedrock", "enable GPT models on Bedrock", "which models do I need to turn on", "I just want access, my engineers will do the migration".
If it is ambiguous (the user mentions both a codebase and access), AskUserQuestion:
"Two things I can do — which do you want? [Full migration] Rewrite your OpenAI/Gemini/Anthropic calls for Bedrock end-to-end. [Just model access] Check and walk you through enabling Bedrock access for specific models, no code changes."
[Just model access] → Access-only mode. [Full migration] → continue to 0b.
Otherwise (a code path / clear rewrite intent) → continue to 0b for the full migration.
0b. Check that a gcp-to-aws or azure-to-aws sibling skill is installed
Phase A below runs Assess by directly reading and executing $ASSESS_SKILL's own phase
instruction files (Discover → Clarify → Design → Estimate, AI-only path) in this same
session — there is no Assess logic in this skill to fall back to. gcp-to-aws and
azure-to-aws are separate skills, not bundled inside this one — a single-skill install
(e.g. npx skills add ... --skill llm-to-bedrock) does not bring either along automatically.
Check for them now, before promising the user an Assess phase this deployment cannot run:
if [ -f "<SKILL_BASE>/../gcp-to-aws/SKILL.md" ]; then
ASSESS_SKILL=gcp-to-aws
elif [ -f "<SKILL_BASE>/../azure-to-aws/SKILL.md" ]; then
ASSESS_SKILL=azure-to-aws
else
echo ASSESS_SIBLING_MISSING
fi
$ASSESS_BASE = <SKILL_BASE>/../$ASSESS_SKILL. This checks the sibling directories relative
to <SKILL_BASE> (defined above), which works under any install path — native plugin install
and npx skills add --skill '*' both place gcp-to-aws/azure-to-aws as siblings of this
skill's own directory. It does not depend on ${CLAUDE_PLUGIN_ROOT}. When both are
installed, gcp-to-aws wins — it is this skill's original, more deeply tested integration,
so this preserves today's default behavior for every existing user with both skills installed.
-
$ASSESS_SKILLresolved (either value) → proceed to Step 1. -
ASSESS_SIBLING_MISSING→ stop here — do not proceed. Tell the user:"This migration needs either the
gcp-to-awsor theazure-to-awsskill installed alongside this one — it handles code scanning, AI-workload detection, and Bedrock model design; I can't do that part myself without it. Both ship in the sameaws-startup-advisorplugin as this skill, so if you installed the whole plugin one of them should already be next to me — it looks like only some of the plugin's skills were installed. Install the pair together with:npx skills add aws/agent-toolkit-for-aws/plugins/aws-startup-advisor/skills --skill llm-to-bedrock --skill gcp-to-aws(or substituteazure-to-awsif your source infra runs on Azure; use the same--agentand--global/project scope you used for this skill), or run that samenpx skills addcommand with--skill '*'instead of naming individual skills to get every skill at once. Then restart your agent and ask me to migrate again."Do not perform the Assess phase yourself as a workaround — Phase A below is explicit that Assess logic lives only in
$ASSESS_SKILL; re-implementing it here would drift out of sync with that skill's Discover/Clarify/Design logic over time. There is no standalone Assess for this skill — this check exists to fail fast and clearly, not to unlock alternate behavior.
Phase A reads $ASSESS_SKILL's files directly off disk rather than invoking it as a skill, so
there is no separate agent-capability requirement here — any agent that can read a file and
run Bash can execute this. (An earlier version of this skill invoked gcp-to-aws via a
cross-skill Skill-tool call; that mechanism is Claude-Code-specific and unverified elsewhere,
which is exactly why Phase A no longer uses it.)
Step 1 — Collect source code path
If $ARGUMENTS contains a path, use it as $REPO. Otherwise use AskUserQuestion:
"Where is your source code? Enter a local path or GitHub URL."
If a GitHub URL, git clone it to a temp dir; use that path as $REPO.
Checks on $REPO:
-
Git-root check (compare resolved paths — on macOS
/tmpresolves to/private/tmp, so a raw string comparison false-positives):[ "$(git -C <REPO> rev-parse --show-toplevel 2>/dev/null)" = "$(cd <REPO> && pwd -P)" ] && echo GIT_ROOT_OK || echo GIT_ROOT_MISMATCHGIT_ROOT_OK→ proceed.GIT_ROOT_MISMATCHand the command errored (not a git repo at all) → tell the user the path must be a git repository (the deliverable is a git branch); re-ask.GIT_ROOT_MISMATCHbut inside a repo (user pointed at a subdirectory) → AskUserQuestion: "Use the repo root instead" (recommended) / "Continue with this subdirectory" / "Abort".
-
Dirty-tree check:
git -C <REPO> status --porcelainIf uncommitted changes exist, show them and AskUserQuestion: "Continue anyway" or "Let me clean up first".
Record $REPO for all subsequent steps.
Step 1.5 — Check for an existing usage profile
Before Phase A begins, check whether a prior Discover run (from either gcp-to-aws or
azure-to-aws) already captured real OpenAI/OpenRouter/Anthropic usage-API spend for this
repo — if so, reuse it as the cost baseline instead of later falling back to golden-dataset
extrapolation. Phase A's own $MIGRATION_DIR resolution and resume logic start fresh each
time Phase A is entered, so this step must run strictly before that to have a chance to
short-circuit it.
-
Glob every
.migration/<run-dir>/<provider>-usage-profile.jsonunder$REPO, excluding this skill's own output directory:for p in anthropic openai openrouter; do find "$REPO/.migration" -mindepth 2 -maxdepth 2 -name "${p}-usage-profile.json" \ -not -path "*/.bedrock-*/*" 2>/dev/null done-mindepth 2 -maxdepth 2matches exactly.migration/<run-dir>/<provider>-usage-profile.json.-not -path "*/.bedrock-*/*"excludes llm-to-bedrock's own$BEDROCK_RUN_DIR(.migration/.bedrock-<id>/) — that directory is this skill's OWN output, never a gcp/azure Discover output. This glob is provider-named, not skill-named — it finds a profile regardless of whethergcp-to-aws's orazure-to-aws's Discover wrote it, since both write to the same filename convention under the same$REPO/.migration/tree. -
Zero matches across all three → no change to current behavior; proceed to Phase A exactly as today.
-
Determine which run's artifact to use (deterministic tiebreak, source-run status surfaced): for each distinct run directory with at least one match, read that directory's
.phase-status.json→last_updated(a full ISO-8601 timestamp, which DOES carry the year) and use it as the primary sort key — the greatestlast_updatedwins. Do not treat the directory's own<MMDD-HHMM>name as chronological on its own: a plain lexicographic comparison of<MMDD-HHMM>is only chronological WITHIN a single calendar year —1231-1200(Dec 31) lexicographically sorts after0102-1200(Jan 2) of the FOLLOWING, objectively later year, so using the name alone can pick a stale run. Fall back to the<MMDD-HHMM>directory name ONLY when a candidate'slast_updatedis missing or unparseable, and when you do, say so explicitly — that fallback is a same-year assumption, not a general chronological guarantee, and must never be presented to the user as equivalent to a real timestamp comparison. Use.phase-status.json→current_phaseas a secondary piece of information to SURFACE to the user (not to break ties) — e.g. a run whosecurrent_phaseshows Discover never advanced past Clarify tells the user this was likely an abandoned/declined session, even though its usage-profile file is still the most recent one. State both pieces to the user explicitly before adopting the figures: "Found an existing [provider] usage profile from your<MMDD-HHMM>run (status:<current_phase>) — using that one." If a run directory's.phase-status.jsonis missing/unparseable entirely, still allow it as a candidate via the<MMDD-HHMM>fallback, but surfacestatus: unknowninstead of a phase name. Genuine tie or ambiguity (two or more candidates all lack a usablelast_updated, so only the same-year-limited<MMDD-HHMM>fallback is available for them, or theirlast_updatedvalues are otherwise ambiguous): do NOT silently guess — surface every ambiguous candidate to the user (directory name + whatever status is known for each) and ask which one is actually the one to use, reusing the "state both pieces to the user explicitly" pattern above rather than picking one automatically. -
For the winning run directory, read every usage-profile file present in it (a run can have multiple providers' profiles simultaneously — including a mix of gcp-produced and azure-produced profiles — B1 does not need to distinguish which skill wrote which file, since the profile schema itself is identical regardless of producer).
-
For each profile: check
metadata.capture_warningsfirst — non-empty means some endpoint failed and that category's volume is UNKNOWN, not zero; propagate the caveat forward. -
Compute
summary.monthly_cost_usdSUMMED across every profile in the winning run (never max/pick-one, reusingestimate-ai.md's documented SUM discipline from both skills — gcp's and azure'sestimate-ai.mdapply the identical rule), combiningusage_by_model[]with the same normalization rules (OpenRouterprompt_tokens/completion_tokens→input_tokens/output_tokens; Anthropiccache_read_tokens/cache_creation_tokensINCLUDED in volume sums — these are additional token pools the Admin API reports separately frominput_tokens, not a sub-accounting of it; seeestimate-ai.md's Prerequisites section in either sibling skill for the fulluncached_input_tokens + cache_read_input_tokens + cache_creation_input_tokenscontract). Apply themetadata.partial_windowexception: a partial-window profile's figures are a reference figure labeled withactive_days, never blended into the monthly baseline (schema consequence specified below). Second, parallel exception —metadata.cost_status(Anthropic profile only, currently the only profile that writes this field): a profile whosecost_statusis"cost_unavailable"hasnullforsummary.monthly_cost_usd— EXCLUDE it from this dollar SUM entirely (never addnull, never substitute0), but STILL include itsusage_by_model[]token totals inusage_by_model[]below — usage succeeded for that profile, only its cost side failed. This mirrors thepartial_windowexception's shape (one disqualifies dollars only, the other disqualifies dollars only) but is a distinct, independent condition — a profile can bepartial_window: falseand stillcost_status: "cost_unavailable". Third exception — OpenRouter BYOK/overlap de-duplication: when BOTH anopenrouter-usage-profile.jsonand a direct provider profile (anthropic-usage-profile.jsonoropenai-usage-profile.json) exist in the winning run directory, resolve the overlap. Match models across the two profiles the same way regardless of which branch below applies — OpenRouter'susage_by_model[].modeluses aprovider/model-nameform (e.g."openai/gpt-4.1"); a model is "the same" as the direct profile's bare model name (e.g."claude-sonnet-5") if the direct name equals the OpenRouter model name's substring after its last/.Gate on field presence, not on the profile-level value, for backward compatibility: if the OpenRouter profile has NO
usage_by_model[].cost_provenancefield at all (an OLD profile written before per-model detection existed), fall straight into the pre-existing blanket-unknown behavior described below — do not require the per-model field to be present, since old.migration/run directories on disk will not have it. If the field IS present (a new-format profile), use the per-model branch instead:- For each OpenRouter
usage_by_model[]row whose model name-matches a model on the direct profile:- row's
cost_provenanceis"byok_passthrough"→ EXCLUDE this row's dollar AND token figures from this SUM entirely (not dollars alone — the direct profile already counts that traffic's tokens, so including OpenRouter's figures too would double-count both; the same underlying provider spend would otherwise be counted twice — once via the direct provider's own usage API, once via OpenRouter routing the same traffic). Prefer the direct provider's own figure for this model as ground truth. Exception — OpenRouter's own router/credit fee is a REAL, distinct charge, not a duplicate: a pure-BYOK row'smonthly_cost_usdis OpenRouter's own/activity.usagefield (per https://openrouter.ai/docs/api/api-reference/analytics/get-user-activity.md,usage= "Total cost in USD (OpenRouter credits spent)"), which is SEPARATE from the external provider's own inference bill (/activity.byok_usage_inference— "BYOK inference cost in USD (external credits spent)") that the direct profile already counts. Excluding the row's TOKEN figures is always correct (the direct profile already counts those tokens). But if this row'smonthly_cost_usd(i.e. its summedusage) is nonzero, that dollar amount is OpenRouter's router fee, NOT the duplicated inference cost — dropping it silently would undercount by exactly that fee. When excluding abyok_passthroughrow with a nonzeromonthly_cost_usd, append acapture_warningsentry (reusing the exact mechanism round 4's"mixed"-row disclosure already established):"<model> classified byok_passthrough — its $<amount> OpenRouter router/credit fee was excluded from this baseline along with its (correctly excluded, already-duplicated) token figures; this fee is a real charge NOT reflected in the figure above."Whenmonthly_cost_usdis zero (or negligible, same $0.005 thresholddiscover-openrouter-api.md's classification already uses), no disclosure is needed — there is no fee to lose, matching today's behavior exactly. Do NOT add this fee back INTO the baseline sum (option (a) from the task brief) —monthly_cost_usdfor this row is captured as one combined inference+fee figure, not split into a separate fee sub-amount, so adding "just the fee" back in is not representable from this schema without re-deriving it; disclosure (option (b)) is the simpler, schema-accurate fix. - row's
cost_provenanceis"mixed"→ EXCLUDE this row's dollar AND token figures from this SUM entirely, for the same double-counting reason asbyok_passthroughabove. UNLIKEbyok_passthrough, do NOT silently drop the independent portion: a"mixed"row has BOTH BYOK-passthrough traffic (already counted via the direct profile) AND genuinely independent, OpenRouter-credit-funded traffic that this exclusion also removes with no other home — that independent portion is real spend missing from the baseline, not spend double-counted elsewhere. Carry an explicit undercount disclosure for every excluded"mixed"row into both the computed baseline's warnings (append tocapture_warnings— see below) and the user-facing summary:"<model> classified mixed (BYOK + independent OpenRouter usage) — excluded entirely to avoid double-counting the BYOK portion, but this also drops its independent OpenRouter-funded usage from the baseline, which is NOT reflected in the figure above."Do NOT attempt to estimate or back out the independent portion's dollar/token split from the enum alone — the enum has no signal for how much of the row's usage is which; only disclose that it is missing. Do NOT make any unsupported "complete cost" or "savings" claim when this warning fires — match the honest-disclosure pattern already used forcost_unavailable/partial_windowabove. - row's
cost_provenanceis"no_byok"→ do NOT exclude; this is a confirmed non-overlapping-provider case, so OpenRouter's own figures for this model are the correct spend to sum (more precise than the blanket-unknown fallback below). - row's
cost_provenanceis"unknown"→ fall back to the conservative "match by name, exclude everything that matches" behavior for this row only (same as the old-profile path below), and keep the same caveat sentence, but scope it to just the rows that actually used the fallback this run (not every overlapping model, since some may have resolved viano_byok/byok_passthrough/mixed). - row's
cost_provenanceis"uncertain"→ apply the SAME conservative handling as"unknown"immediately above: EXCLUDE this row's dollar AND token figures from this SUM entirely (match-by-name exclusion), using the identical caveat sentence scoped to rows that used this fallback this run. Do NOT reuse"mixed"'s undercount-disclosure branch —"uncertain"means at least one contributing permaslug had NO Analytics signal (partial verification), not a confirmed independent OpenRouter-funded portion like"mixed"has; there is nothing concrete to disclose beyond whatdiscover-openrouter-api.md's owncapture_warningsentry for this model already states at capture time. Reusing"unknown"'s policy here, rather than inventing a distinct"uncertain"rule, keeps the SAME double-counting protection for a value that is strictly less certain than"unknown"'s already-conservative default.
- row's
- Keep every OpenRouter-only model (no match on the direct profile) in the sum as before.
- The profile-level
metadata.cost_provenancefield is NOT read to decide whether this per-model branch runs — only the presence of the per-model field decides that (see gate above).
Old-profile fallback (no per-model field present): there is no reliable signal to resolve the overlap automatically — match models across the two profiles (same name-match rule above). For every OpenRouter
usage_by_model[]row whose model matches a model present on the direct profile, EXCLUDE that row's dollar AND token figures from this SUM entirely. Keep every OpenRouter-only model (no match on the direct profile) in the sum as before. Flag the exclusion in the summary presented to the user as "OpenRouter and <provider> usage profiles both present; N overlapping model(s) excluded from OpenRouter's totals to avoid double-counting BYOK traffic — provenance is unknown, so this exclusion assumes full overlap for those models, which may undercount if OpenRouter is only partially BYOK-routing them." - For each OpenRouter
-
Hold these computed figures — do not write them yet.
$BEDROCK_RUN_DIRdoes not exist at this point in the document (it is only established in "### A3 — Locate Assess output, then establish this skill's own run state" below, after Phase A runs); writing to it here would write to a directory variable that is not yet assigned. Carry the computedsource_run_dir,source_profiles,captured_at,capture_warnings,windows,summary, andusage_by_model[]values forward (the shape is specified in "New artifact" below) and present the short summary now: "Found existing usage data: $X/month across N models (source: [provider list], captured<date>). I'll use this as your current-cost baseline instead of estimating from sampled golden-dataset traffic." The actual file write happens in A3, immediately after$BEDROCK_RUN_DIRis set — see the cross-reference there. -
This step never prompts for consent — it only reads a file a prior, already-consented run already wrote. No new attack surface; no new consent gate needed.
-
Idempotency: this step is safe to re-run on every Phase-B start, including a resumed session (A2's resume path). It performs read-only file operations only — the actual write of
usage-baseline.jsonhappens later, in A3 (see step 7 above), and that write is itself idempotent — overwritingusage-baseline.jsonon a rerun with the same inputs produces the same output, and a rerun with DIFFERENT inputs (e.g. a newer profile appeared since the last run) correctly re-resolves and overwrites. Re-running this step on a resume where it already ran is therefore harmless; it is not gated behind any "already ran" check.
New artifact: $BEDROCK_RUN_DIR/usage-baseline.json
Written to $BEDROCK_RUN_DIR ($REPO/.migration/.bedrock-<id>/) in "### A3 — Locate Assess
output, then establish this skill's own run state" below, immediately after
$BEDROCK_RUN_DIR is set — not at Step 1.5 itself, since $BEDROCK_RUN_DIR does not
exist yet when Step 1.5 runs (Step 1.5's steps 1–6 above still run here, before Phase A, so
Phase A1's own fresh-Discover fallback decision can short-circuit correctly; only the file
WRITE is deferred). This gives Phase C's report-generator a stable, already-resolved place
to read from without re-globbing .migration/*/ itself (that glob belongs only to this
step).
Schema (per-provider window-status map, so a mixed partial/full combination is representable):
{
"source_run_dir": ".migration/0315-1030",
"source_profiles": ["anthropic-usage-profile.json", "openai-usage-profile.json"],
"captured_at": "<ISO 8601, max across source profiles>",
"capture_warnings": [],
"windows": {
"anthropic": { "window_days": 30, "active_days": 12, "partial_window": true, "cost_status": "partial_window" },
"openai": { "window_days": 30, "active_days": 30, "partial_window": false, "cost_status": "complete" }
},
"summary": {
"monthly_cost_usd": 0.0,
"monthly_cost_usd_is_blended_estimate": true,
"currency": "USD",
"models_seen": 0
},
"usage_by_model": [
{
"model": "string",
"provider": "openai|anthropic|openrouter",
"partial_window": true,
"input_tokens": 0,
"output_tokens": 0,
"num_model_requests": 0
}
]
}
Validation rules:
source_profilesis non-empty (the file is only written when this step found at least one profile — absence of the file, not an empty array, signals "no existing profile found").windowsis keyed by every provider contributing tousage_by_model[]; a provider absent fromwindowsmust not appear inusage_by_model[]either.usage_by_model[].providerdisambiguates source;usage_by_model[].partial_windowis a direct copy of that row's provider'swindows.<provider>.partial_window— carried per-row (not only per-provider) so a consumer readingusage_by_model[]alone, without cross-referencingwindows, can still tell which rows are reference-only.summary.monthly_cost_usd_is_blended_estimate:truewheneverwindowscontains ANY provider withpartial_window: true— whentrue, the consumer (llm2bedrock-report-generator.md§6) MUST presentmonthly_cost_usdas a labeled reference figure (with the partial provider'sactive_daysshown), never as an unqualified monthly baseline. When every contributing provider is full-window, this flag isfalseandmonthly_cost_usdis a normal blended monthly baseline. This satisfies step 6 above's discipline WITHOUT splittingsummaryinto two separate top-level figures — the onemonthly_cost_usdnumber remains the SUM step 6 computes, and the boolean tells the consumer how to present it.windows.<provider>.cost_status:"complete" | "partial_window" | "cost_unavailable", copied straight from that provider's source usage-profilemetadata.cost_status(currently only the Anthropic usage profile writes this field — seediscover-anthropic-api.mdStep 3 in either sibling skill; OpenAI/OpenRouter profiles that don't yet write it default to"complete"whenpartial_windowisfalseand"partial_window"when it istrue, since for those providers window-completeness and cost-completeness haven't been split apart yet). When ANYwindows.<provider>.cost_statusis"cost_unavailable", that provider's dollars were already excluded fromsummary.monthly_cost_usdin step 6 above — a consumer readingwindowsto explain a dollar total that is lower than the token volume would suggest finds the reason here, per-provider, without re-opening the source profile file.source_run_dirpath resolution, stated explicitly:source_run_diris POSIX, relative to$REPO(the same$REPOpassed in every subagent's context block as theRepository:line) — never an absolute path, and never relative to$BEDROCK_RUN_DIR. This keeps the artifact portable across a cloned/moved repo. Both the writer (step 7 above, which MUST strip the$REPO/prefix from the winning run directory's absolutefind-produced path before writing it here) and the reader (llm2bedrock-report-generator.md§6) resolve it the same way: join$REPO+source_run_dirto get the absolute path, when either needs to re-open a source profile file directly rather than trustingusage-baseline.json's own already-summed figures.
Standalone invocation with no existing profile
When this step finds zero matches, rely entirely on Phase A's existing inline execution of
$ASSESS_BASE's own discover.md (gcp-to-aws's or azure-to-aws's, whichever Step 0b
resolved $ASSESS_SKILL to for this run), which already offers OpenAI/OpenRouter/Anthropic
usage discovery via its own consent/trigger logic — gcp-to-aws has this today in its Step
1e–1g; azure-to-aws has the identical capability. No new "offer live discovery" entry point
is designed here — re-implementing it in this step would duplicate the ~400-line
security-contract capture logic AND the offer/consent flow a third/fourth/fifth time (once
per cloud), and risk drift between call sites. Instead, Phase A's prose already names
$ASSESS_BASE/references/phases/discover/discover.md by path and tells the reader to
execute it in full — that is the "reference by path, don't reimplement" mechanism, already
in place, covering both clouds.
One adjustment: when Phase A's inline Discover captures a FRESH profile in a NEW
$MIGRATION_DIR, re-run steps 4–7 above against that run's own directory (no cross-run
search needed) at the end of Phase A's Discover sub-step, and hold the resulting figures the
same way (step 7's deferred-write discipline applies here too — $BEDROCK_RUN_DIR still does
not exist at this point, since A3 has not run yet) — so Phase C always reads from one
consistent place regardless of whether the profile came from this step (reuse) or Phase A's
fresh capture, and regardless of which cloud's Assess skill produced it.
Additive, optional — existing flow unaffected: usage-baseline.json absent means no
behavior change — the existing behavior-delta comparison and golden-dataset-sample
extrapolated cost table (llm2bedrock-report-generator.md §6.3) run exactly as today.
Phase A — Assess (runs $ASSESS_SKILL's own AI-path files, inline)
Do NOT read source code, detect AI SDKs, or ask Clarify questions yourself from scratch.
This phase reads $ASSESS_SKILL's own phase instruction files off disk and follows them
exactly as if $ASSESS_SKILL were running standalone — the same content, the same state file,
the same artifacts. The only difference from invoking $ASSESS_SKILL as a separate skill is
that there is no tool call and no turn boundary: everything below runs inline, in this session.
Step 1.5 above already checked for and, when found, computed the figures for an existing
usage-API profile (from either gcp-to-aws's or azure-to-aws's Discover) as the Bedrock
cost baseline — held in memory until "### A3 — Locate Assess output, then establish this
skill's own run state" below writes them to $BEDROCK_RUN_DIR/usage-baseline.json once
$BEDROCK_RUN_DIR exists; Phase A
below still runs fully regardless — it only falls back to Discover's own fresh usage-API
capture or golden-dataset extrapolation when no existing profile was found.
Path resolution. $ASSESS_SKILL instruction files use relative references
(references/phases/..., references/shared/..., references/vendored/...,
references/design-refs/..., shared/..., design-refs/..., phases/...,
data/... — including the short forms). Resolve every one of them under $ASSESS_BASE
(defined above), exactly the prefix it's written with, e.g. shared/pricing-cache.md →
$ASSESS_BASE/references/shared/pricing-cache.md. $MIGRATION_DIR is the one path that does
not resolve under $ASSESS_BASE — it stays under $REPO per $ASSESS_SKILL's own
convention (A1 below). $ASSESS_SKILL's files are read-only here — this phase never edits
them.
A1 — Run Discover, Clarify, Design, and Estimate
Tell the user, before starting:
"I'm now running the Discover → Clarify → Design → Estimate assessment (the same logic
$ASSESS_SKILLuses standalone) to detect your AI workloads and design the Bedrock migration. It'll ask you some questions — please answer them."
Then, in order:
-
Resolve
$MIGRATION_DIR. Check for an existing.migration/directory at$REPOexactly as$ASSESS_BASE/references/phases/discover/discover.mdStep 0 describes (list existing runs and offer Resume/Fresh/Cancel if any exist; otherwise create$REPO/.migration/<MMDD-HHMM>/with the current timestamp and set$MIGRATION_DIRto it). A directory that exists but has no.phase-status.jsonyet is NOT an existing run — treat it as fresh and skip discover.md's Resume/Fresh/Cancel prompt for it (same exceptionagent-advisor'smigration-plan.mdPhase A documents for its own inline gcp-to-aws delegation). This matters here because llm-to-bedrock does not pre-create$MIGRATION_DIRbefore this step — but a run interrupted after this step creates the directory and before discover.md's Step 0 writes.phase-status.jsonwould otherwise leave exactly this directory (present, but state-less) for a later resume, and discover.md's Resume branch would then try to read a.phase-status.jsonthat doesn't exist. -
Read and execute
$ASSESS_BASE/references/phases/discover/discover.mdin full, exactly as written, including its own Step 0 state-file initialization (skip Step 0 if resuming —$MIGRATION_DIRalready has a.phase-status.json) and its Step 1 sub-discovery gates (1a–1e). Those gates already key off what's actually present in$REPO— IaC discovery only runs if Terraform files exist there, billing discovery only if billing exports exist, and so on; you do not need to steer it.discover.md's own usage-API consent/capture actions (OpenAI, OpenRouter, and — for azure-to-aws — Anthropic) will offer the matching Admin-API usage discovery when applicable; accepting any of them gives Estimate real spend and token volumes without manual exports. (Anthropic's Admin key has NO selectable scopes — it is all-or-nothing; the old sentence's "Usage set to Read" phrasing was already wrong for OpenAI's own key in a narrower way worth not perpetuating into this generalized sentence — note this as an adjacent, pre-existing inaccuracy fixed at its source rather than carried forward.)When Discover's Step 0 writes the run's
.phase-status.json, it must record"initiated_by": "LLM_TO_BEDROCK"besideowning_skill(which is set to whichever ofGCP_TO_AWS/AZURE_TO_AWSactually ran — i.e.$ASSESS_SKILL's own identifier — perdiscover.md's own instruction for a run started by another skill): this run was started by llm-to-bedrock, and that is how telemetry attributes it.On
HANDOFF_OK: at least one ofai-workload-profile.json,gcp-resource-inventory.json/azure-resource-inventory.json, orbilling-profile.jsonis present in$MIGRATION_DIR. -
Read and execute
$ASSESS_BASE/references/phases/clarify/clarify.mdin full. It routes itself — whenai-workload-profile.jsonis the only discovery artifact, it readsclarify-ai-only.mdand runs that standalone flow; if infra artifacts also exist, it runs the fragment/assembler split instead. Either way, follow what it loads exactly. OnHANDOFF_OK:preferences.jsonis present in$MIGRATION_DIR. -
Read and execute
$ASSESS_BASE/references/phases/design/design.mdin full. It routes todesign-ai.mdwhenai-workload-profile.jsonexists — that is the file this skill's Execute phase depends on. OnHANDOFF_OK:aws-design-ai.jsonis present in$MIGRATION_DIR(plusaws-design.json/aws-design-billing.jsontoo, if an infra or billing route also ran). -
Read and execute
$ASSESS_BASE/references/phases/estimate/estimate.mdin full. You do not need to continue past Estimate — this skill only needs the Assess artifacts (aws-design-ai.json,ai-workload-profile.json,preferences.json), so onceestimate.mdreaches its post-Estimate decision gate, choosing not to generate infra (or simply stopping at the decision pack) is enough; you never read anythinggenerate.mdwould produce (Terraform,MIGRATION_GUIDE.md,migration-report.html).
Each file above manages $MIGRATION_DIR/.phase-status.json itself, per its own protocol —
do not write to that file yourself, other than the initiated_by field named in step 2 above.
If any file's own _postconditions/handoff checks fail (GATE_FAIL), stop and show the user
exactly what that file reported; do not patch an artifact to force a gate to pass.
A2 — Confirm Assess is complete
Since Phase A above ran every step through Estimate inline (not across separate
invocations), Assess is complete once step 5 of A1 finishes without a GATE_FAIL. This
check exists as a backstop in case the session was interrupted partway through A1 (e.g. the
user stopped mid-Clarify and is resuming later) — re-read $MIGRATION_DIR/.phase-status.json
and resume A1 at whichever step in phases is not yet "completed", rather than restarting
from Discover.
Before trusting that re-read, apply $ASSESS_BASE/SKILL.md § State Validation (the same
contract A1's own phase files rely on when THEY read this state, so this wrapper-level
backstop re-read must not be held to a looser standard). In particular: if
.phase-status.json fails to parse (an interrupted write left it invalid — e.g. the session
was interrupted mid-write, not just mid-phase), § State Validation check 2's reconstruction
procedure is the one and only sanctioned way to recover it — infer completed phases from the
artifacts actually present in $MIGRATION_DIR, present the inferred status to the user for
confirmation, and rewrite .phase-status.json only on that confirmation. This is a narrow,
explicit exception to A1's "do not write to that file yourself" rule: .phase-status.json
recovery per this contract is not the same act as a phase file's own state management, and
proceeding here without it would mean this wrapper resumes on state it never validated,
unlike every phase file it delegates to.
Workshop guard: if phases.workshop == "in_progress", design.md's inner-workshop path
is actively repricing (workshop-refresh.md is rewriting aws-design-ai.json between an old
and a new mapping) — finish that loop (it resolves phases.workshop back to "completed" on
exit or decline) before treating Design as done.
A3 — Locate Assess output, then establish this skill's own run state
Use the SAME $MIGRATION_DIR A1/A2 already resolved and confirmed — do NOT re-select a
directory here. $MIGRATION_DIR is already set from A1 step 1 (and re-confirmed, not
replaced, by A2's re-read on a resumed session); re-running a "newest directory" lookup
(ls -td "$REPO/.migration"/*/ | head -1) at this point can select a DIFFERENT, newer run
than the one A2 just verified was complete — e.g. a sibling run whose own Design is still
in progress. B1 would then read that sibling's unfinished model map instead of the run this
phase actually assessed, with no error raised (the newer directory can genuinely contain all
three files below, just from a different, incomplete assessment). If $MIGRATION_DIR is
somehow unset here (it should never be, given A1/A2 above), that is a bug in this phase's own
state tracking — stop and report it rather than guessing a directory from ls -td.
Verify all three of these files exist in $MIGRATION_DIR (Phase B reads every one):
aws-design-ai.json(model mapping + architecture)ai-workload-profile.json(detected workloads)preferences.json(user preferences from Clarify)
If any of the three is missing, Assess did not complete the AI path correctly — name the
missing file(s), show the error, and stop. A1 running to completion without a GATE_FAIL
should already guarantee this; this step is the backstop that confirms it before Execute
reads them.
Establish this skill's own run state (read-merge-write). This skill keeps a thin
run-state file of its own, separate from the delegated run's, so the AI migration appears in
the usage funnel under its own name. It declares its own state shape (phases assess,
execute); the shared DSL's read-merge-write rule applies. Leave the delegated run's
.phase-status.json untouched here: a run created by this invocation already carries
initiated_by (A1), and rewriting an older run's file would make its history look like new
work to the telemetry hooks.
-
Set
$BEDROCK_RUN_DIR=$REPO/.migration/.bedrock-<id>/, where<id>is the basename of$MIGRATION_DIR(e.g..migration/.bedrock-0910-1100/). The leading dot keeps it out of thels -td "$REPO/.migration"/*/lookup above, so it can never be mistaken for the Assess run directory; keying it to the delegated run means a resumed migration reuses it.$BEDROCK_RUN_DIRalso holdsusage-baseline.jsonwhen Step 1.5 (or Phase A's own fresh Discover capture) found an existing usage-API profile — written in step 2 below, once this directory exists (see Step 1.5's "New artifact" section above for the schema).-
If
$BEDROCK_RUN_DIR/.phase-status.jsonalready exists, this migration is being resumed: keep the file (including itsrun_id) and continue. -
Otherwise create the directory and write
.phase-status.json:{ "migration_id": ".bedrock-<id>", "last_updated": "<ISO 8601 now>", "current_phase": "execute", "run_id": "<fresh UUID from uuidgen>", "owning_skill": "LLM_TO_BEDROCK", "phases": { "assess": "completed", "execute": "in_progress" } }
The
assess/executephase names are not in the telemetry API model yet, so only the run-level events (RUN_STARTED,RUN_COMPLETED) leave the machine for this run; the AI journey's entry point is already visible through the delegated run's events, which carryinitiatingSkill. -
-
Write
usage-baseline.json, now that$BEDROCK_RUN_DIRexists. If Step 1.5 (or Phase A's own fresh Discover capture, per Step 1.5's "Standalone invocation" note) held computed figures in step 7, write them to$BEDROCK_RUN_DIR/usage-baseline.jsonnow, using the schema in Step 1.5's "New artifact" section above. If no figures were held (zero matches in Step 1.5, and no fresh profile from Phase A's Discover either), skip this write entirely —usage-baseline.json's absence is itself the "no baseline" signalllm2bedrock-report-generator.md§6.3 checks for. -
After creating or validating the resumed Bedrock run, reconcile in
clireporting mode perreferences/vendored/telemetry/PROTOCOL.md. Inhookmode, leave reporting to the host hooks.
Phase B — Execute Prep
B1 — Read Assess outputs
Read $MIGRATION_DIR/aws-design-ai.json and extract:
ai_architecture.bedrock_models[]→ array of{source_model, aws_model_id, use_case}- Collect all
aws_model_idvalues into$TARGET_MODELS(array). Keep theuse_caseof each: the preflight script probes each model by the right API automatically (Converse for chat, InvokeModel for embeddings), but the evaluator's quality scoring only applies to chat models — embedding targets get format/dimension validation only.
Read $MIGRATION_DIR/ai-workload-profile.json and extract:
summary.ai_source→ source provider
Read $MIGRATION_DIR/preferences.json and extract:
design_constraints.target_region→$REGION(defaultus-east-1if absent)
Validation: If aws-design-ai.json has no ai_architecture.bedrock_models[] array, or the
array is empty, STOP: "Assess output incomplete — model mapping missing."
B2 — AWS identity confirmation
aws sts get-caller-identity 2>&1
If the command fails (no credentials, expired SSO token): show the error and tell the user
to run aws configure or aws sso login (suggest typing ! aws sso login to run it in this
session), then re-run B2. Do not proceed without a confirmed identity.
On success, show Account, Arn, UserId via AskUserQuestion: "This AWS identity will be used for Bedrock calls. Is this correct?"
Options:
- Yes, use this identity → proceed
- Use a different AWS profile → ask which profile, record it as
$AWS_PROFILE_CHOICE, re-run B2 asaws sts get-caller-identity --profile $AWS_PROFILE_CHOICE, and re-confirm. Do NOT rely on exportingAWS_PROFILE— env vars do not persist across Bash tool calls or into workflow subagents (see B3). Instead pass the choice explicitly everywhere:--profileon every aws CLI call, and prependAWS_PROFILE=$AWS_PROFILE_CHOICEinline on the B4 preflight command and inside the workflow args (awsProfilefield) so subagents can do the same.
Also confirm region: "Bedrock region will be $REGION. OK or override?"
B3 — Source API key (optional)
First, create the artifact directory and make it self-ignoring IMMEDIATELY — before any key exists, so the secret is never sitting in an unignored working tree (even if the user aborts before the rewriter runs):
mkdir -p "$REPO/.saws-migrate" && printf '*\n' > "$REPO/.saws-migrate/.gitignore"
Determine $KEY_ENV_VAR from B1's source provider (this is the env-var name the baseline
skill's parser expects — a bare key without the NAME= prefix will NOT be parsed):
openai→OPENAI_API_KEYanthropic→ANTHROPIC_API_KEYgoogle/gemini→GEMINI_API_KEY
The key must never enter this conversation (HARD RULE). Do not ask the user to paste the key in chat, and never echo, cat, or interpolate its VALUE into any command, question, or output — the agent only ever handles the file path. If the user pastes a key into the chat unprompted, do not use it: tell them it is now part of the transcript, recommend rotating it, and continue with one of the paths below.
First check whether the key is already present in the shell environment (each Bash call
initializes from the user's profile, so a profile-exported key is visible to every call).
POSIX-safe — printenv works in bash and zsh alike (${!VAR} indirection is bash-only and
zsh errors on it). This prints presence only, never the value:
[ -n "$(printenv "$KEY_ENV_VAR")" ] && echo ENV_KEY_PRESENT || echo ENV_KEY_ABSENT
AskUserQuestion: "Do you have an API key for the source model (e.g. OpenAI key for GPT-4o)? Providing it enables side-by-side quality comparison. Without it, evaluation uses absolute scoring only."
Options (offer the first only on ENV_KEY_PRESENT):
-
Use the
$KEY_ENV_VARalready in my environment → materialize env var to file in one command — the value never appears in the transcript:printf '%s=%s\n' "$KEY_ENV_VAR" "$(printenv "$KEY_ENV_VAR")" > "$REPO/.saws-migrate/.source-provider-env" && chmod 600 "$REPO/.saws-migrate/.source-provider-env"Then run the format check below and set
sourceBaselineAvailable = true,sourceKeyRef = "$REPO/.saws-migrate/.source-provider-env". -
I'll write it to a file myself → give the user this command to run in THEIR OWN terminal (not through the agent; in Claude Code an
!prefix runs it in-session) —read -rscollects the key without echoing it:read -rs k && printf '%s=%s\n' "<KEY_ENV_VAR>" "$k" > "<REPO>/.saws-migrate/.source-provider-env" && chmod 600 "<REPO>/.saws-migrate/.source-provider-env" && unset kSubstitute the literal env-var name and repo path when presenting it (those are not secrets). Then run the format check below and set the same flags as above.
-
Skip →
sourceBaselineAvailable = false,sourceKeyRef = "".
Whichever path wrote the file, verify the format (never prints the key; catches a value
written without the NAME= prefix, which the baseline parser would silently miss):
grep -qE '^(OPENAI|ANTHROPIC|GEMINI)_API_KEY=.+' "$REPO/.saws-migrate/.source-provider-env" && echo KEY_FORMAT_OK || echo KEY_FORMAT_BAD
On KEY_FORMAT_BAD, have the same path that wrote the file rewrite it (do not echo its
contents).
(.saws-migrate/ is already self-ignoring from the first command above; the rewriter
re-asserts this before any commit as a second layer.)
IMPORTANT: The file is the handoff mechanism — do NOT rely on export to carry the key
into later steps. Shell state set in one Bash call does not persist into other calls or into
workflow subagents; only a profile-exported variable (the ENV_KEY_PRESENT path above) is
reliably visible, and even that must be materialized to the file for the baseline runner.
B4 — Bedrock preflight
uv run --project $SCRIPTS python $SCRIPTS/preflight_bedrock.py --region $REGION --models <comma-separated $TARGET_MODELS> --dataset-size 200 --runtime-api <selected bedrock_models[].required_client_api; legacy runtime_api or the saved-plan default>
(--dataset-size 200 matches the golden-dataset cap, so the quota warning reflects the worst case. Prefix with AWS_PROFILE=$AWS_PROFILE_CHOICE if B2 chose a non-default profile.)
Parse the JSON output. On failure the TOP LEVEL carries reason/detail (lifted from the
first failing model) plus failing_models (all failing ids); per-model verdicts are in models[]:
ok == false+reason: credentials→ show the detail (configure/refresh credentials), stop; user re-runs after fixing.ok == false+reason: model_access→ model access not enabled in the Bedrock console (NOT an IAM problem): point the user at the console Model access page for the failing models, stop; re-run B4 after they enable it.ok == false+reason: authz→ IAM denies inference. For a Converse/InvokeModel target the action to grant isbedrock:InvokeModel; for a bare proprietary GPT target (GPT-5.x oropenai.gpt-6-astra) it is thebedrock-mantle:*set (see B4a). Thedetailnames which. Tell the user the action to grant; stop.ok == false+reason: mantle_deps_missing→ the pinned scripts environment lacksopenai/aws-bedrock-token-generator, so a mantle-only target could not be probed at all. This is an environment fault, not a Bedrock verdict: tell the user to re-sync (uv sync --project $SCRIPTS) and stop. Do NOT proceed — access was never verified.ok == false+reason: openai_deps_missing→ the selected runtime Chat/Responses API could not be probed because its SDK is missing. Re-sync the same scripts environment and stop; do not substitute Converse.ok == false+reason: model_unavailable→ Read theresolve-bedrock-model-idreference at$HELPERS/resolve-bedrock-model-id/resolve-bedrock-model-id.mdand follow its procedure with each ID fromfailing_models+ region. AskUserQuestion with the candidates: "Use<candidate>(cross-region inference profile)" / "Paste a different model ID" / "Abort". On a choice, replace the ID in$TARGET_MODELSand re-run B4.ok == false+ any otherreason→ showdetailand stop.ok == true→ proceed. Surface anyquota_warning, and any model whosereasonisembedding_unprobed(embedding family the preflight can't probe — remind the user to confirm model access in the console).
For the supported Opus 5.5 London in-region route, retain the validated bare ID in
$TARGET_MODELS, C0's saved/current contexts and C1's source→target pairs. The mandatory
resolver has a matching Step 0 catalog path; neither initial analysis nor a resume should
replace this target with a cross-region profile merely because a profile listing lacks it.
Phase C — Execute
Phase C dispatches the five plugin agents sequentially via the Agent tool (subagent types
aws-startup-advisor:llm2bedrock-code-analyzer, aws-startup-advisor:llm2bedrock-log-ingestor, aws-startup-advisor:llm2bedrock-prompt-evaluator,
aws-startup-advisor:llm2bedrock-code-rewriter, aws-startup-advisor:llm2bedrock-report-generator). Each agent writes its result to
a file under $PHASE_DIR = $REPO/.saws-migrate/phase-results/; you validate every file with
the bundled validator before moving on. There is no workflow runtime — the files ARE the state.
The validator (used at every step)
uv run --project $SCRIPTS python $SCRIPTS/validate_result.py --schema <analysis|ingestion|eval|rewrite|delta-decisions> <file>
- Exit 0 +
RESULT=valid CONTROL=ok→ phase completed; proceed. - Exit 0 +
CONTROL=blocked REASON=<r>→ blocked flow (below). - Exit 0 +
CONTROL=partial COMPLETED=<n> TOTAL=<m>→ partial flow (eval only). - Exit 1 (
RESULT=invalid+ error lines) or exit 2 (file missing) → stateless fixer retry: dispatch a FRESH agent of the same type whose prompt is the original context block + the file path + the validator's verbatim error output + the instruction "fix ONLY the output file at<path>so it validates; do not redo the phase's work unless a required field is genuinely missing from it". Cap 2 retries per phase; then stop and show the errors.
The context block (instantiated at every dispatch)
Before any C1–C6 dispatch, normalize the API path from the saved plan and the validated
$TARGET_MODELS using preflight_bedrock.normalize_api_path from $SCRIPTS:
import sys
sys.path.insert(0, "<absolute path to $SCRIPTS>")
from preflight_bedrock import normalize_api_path
resolved_api_path = normalize_api_path(
plan_migration_path, validated_target_model_ids, plan_runtime_api)
Read plan_runtime_api from the selected model row's required_client_api. For older
plans, fall back to that row's runtime_api, then ai_architecture.code_migration.runtime_api.
A current Design CRIS plan without an explicit API uses responses; only a legacy
Converse plan retains None. An explicit invoke surface is unsupported by this
execution lane: stop for an explicit supported API decision rather than treating it as Converse. A runtime_openai_cris
plan resolves to runtime_openai_chat or runtime_openai_responses when that API is explicit,
and retains the Converse default otherwise. Do not ignore this field on dispatch or resume.
The legacy migration_path: "mantle" alias with bare proprietary GPT targets resolves to
mantle_openai_responses, so evaluator and rewriter keep the documented Responses default.
Other explicit migration_path values are preserved. With no path, an all-bare proprietary GPT target
set (including Astra) resolves to mantle_openai_responses; runtime targets use converse.
A mixed legacy target set is model_unresolvable: stop before evaluation and request an
explicit plan instead of guessing. Recompute the same value on every dispatch/resume from
the same saved inputs; the existing target-model and assess_design_sha256 checks detect
changed inputs. Do not let C3 and C5 choose separate missing-path defaults.
Build this exact line format (agents parse the labels). Omit lines marked optional when empty:
Repository: <$REPO>
AWS region: <$REGION>
AWS profile (pass as --profile / AWS_PROFILE= inline on every aws/boto3 invocation): <$AWS_PROFILE_CHOICE — omit line if default>
Target Bedrock model(s): <comma-joined $TARGET_MODELS, with any resolved overrides already applied>
Target API path: <resolved_api_path from normalize_api_path above>
Target API surface: <chat_completions for *_openai_chat, responses for *_openai_responses, otherwise converse>
Migration plan dir: <$MIGRATION_DIR>
Resolved target model id: <override for the primary chat model — omit if none>
Scripts directory (pinned uv toolchain): <$SCRIPTS>
Report date suffix: <saved suffix from run-context — C5/C6 dispatches only>
Usage baseline path: <$BEDROCK_RUN_DIR/usage-baseline.json — C5/C6 dispatches only, omit line if the file is absent>
Source baseline available: <true|false>
Source provider env file: <path — omit if none>
User-supplied log files: <comma-joined — omit if none>
Golden dataset cap (max cases the ingestor may emit): 200
Phase results directory: <$PHASE_DIR>
Prior phase results (Read these files): <paths of already-validated phase JSONs>
Confirmed behavior-delta decisions file (Read it): <$PHASE_DIR/delta-decisions.json — C5 only>
<helper-reference lines — inject ONLY the ones this agent needs, per the table below>
Prior-phase results are passed as FILE PATHS — never inline their JSON into the prompt.
Helper references (the former helper skills, now under $HELPERS). Agents no longer
load skills by name; instead the agent Reads a helper reference at an absolute path you
inject. For each dispatch, add ONLY the helper lines that agent uses (per its # 4 section):
| Agent (dispatch) | Helper-reference lines to add |
|---|---|
| C1 llm2bedrock-code-analyzer | behavior-delta-detection reference: $HELPERS/behavior-delta-detection/behavior-delta-detection.md; resolve-bedrock-model-id reference: $HELPERS/resolve-bedrock-model-id/resolve-bedrock-model-id.md |
| C5 llm2bedrock-code-rewriter | bedrock-known-fixes reference: $HELPERS/bedrock-known-fixes/bedrock-known-fixes.md; behavior-delta-detection reference: $HELPERS/behavior-delta-detection/behavior-delta-detection.md; dependency-conflict-resolution reference: $HELPERS/dependency-conflict-resolution/dependency-conflict-resolution.md |
| C3 llm2bedrock-prompt-evaluator | bedrock-known-fixes reference: $HELPERS/bedrock-known-fixes/bedrock-known-fixes.md; resolve-bedrock-model-id reference: $HELPERS/resolve-bedrock-model-id/resolve-bedrock-model-id.md; `run-source-model-baseline reference: $HELPERS/run-source-model-baseline/run-source-model-base |
Truncated - read the full file at https://github.com/aws/agent-toolkit-for-aws/blob/HEAD/plugins/aws-startup-advisor/skills/llm-to-bedrock/SKILL.md.
