AWS Tools — awsops — Implementation Plan (Go)
Source Design
See DESIGN.md and DECISIONS.md for the
complete design and decision rationale, including why this plan
retargets from Bash to Go (DECISIONS.md, 2026-07-01
“Retarget implementation from Bash to Go”).
Status (2026-07-01)
Starting from scratch. ec2_ami_manager.bash remains in
the repo as the working reference for expected behavior and stays in
place, untouched, until this plan’s Phase 14 (main menu) is complete and
TEST_PLAN_REAL_AWS.txt passes against the Go binary.
Nothing below has been implemented yet.
Unlike the Bash plan, the AMI-from-instance capabilities that were folded in later as “Phase 5b” (volume-size time estimate, fstrim/SSM step, unbounded polling, Invenio RDM crash-consistency guidance) are included in Phase 5 from the start — we already know they’re required.
Phases
TDD throughout: write the *_test.go file (with fakes for
the AWS SDK interfaces) before the implementation it covers.
Phase 0 — Project Setup
Effort: ~2 hours Priority: High
Tasks
Phase 1 — AWS Client Layer
Effort: ~3 hours Priority: High
Files: internal/awsclient/
Work Items
- Define the four configured regions as a package-level constant/slice
(
Regions = []string{"us-east-1", "us-east-2", "us-west-1", "us-west-2"}) NewEC2Client(ctx, region) (EC2API, error)— constructs a region-scoped EC2 client from the SDK’s default config/credential chainNewSSMClient(ctx, region) (SSMAPI, error)— same, for the fstrim stepNewS3Client(ctx, region) (S3API, error)— for Backup Archive & Trim’s independent verification (HeadObject) using the operator’s own credentials, distinct from the target instance’s own IAM instance profile (seeDESIGN.mdAssumptions)- Define
EC2API/SSMAPI/S3APIas narrow interfaces covering only the SDK methods actually used (enables fakes in tests without a real client) - Startup credential check: fail fast with a clear message if
sts:GetCallerIdentityfails (replacescheck_dependencies’s AWS CLI/jq checks — there’s no external binary to check for anymore)
Tests: credential-check failure path; region client construction; retry/backoff behavior on throttling errors (table-driven, using a fake that returns a throttling error N times then succeeds)
Dependency: Phase 0
Phase 2 — Resource Listing
Effort: ~4 hours Priority: High
Files: internal/inventory/
Work Items
ListInstances(ctx, clients map[string]EC2API) ([]Instance, error)— queriesDescribeInstancesper region concurrently (goroutines + errgroup or similar), aggregates, excludes terminated instancesListImages(ctx, clients map[string]EC2API) ([]Image, error)— queriesDescribeImageswithOwners: [self]per region, aggregates, filters toState == availableInstance/Imagestructs carry the same fields the Bash version displayed: InstanceId/ImageId, Name (from tags), State, ImageId, Region / Name, CreationDate, Region — plusProjectandEnvironment(from tags, empty string if untagged — display layer renders empty as “unknown”, seeDECISIONS.md“Introduce a light Project/Environment tagging convention”)- Group/filter helpers over the aggregated results: by
Project, byEnvironment— used by Phase 3’s display and the main menu’s listing
Tests: aggregation across regions; empty-region handling; terminated- instance filtering; Name/Project/Environment-tag extraction (present/ absent); group/filter by Project and by Environment
Dependency: Phase 1
Phase 3 — Terminal UI (done)
Effort: ~4 hours Priority: High
Files: internal/ui/
Built on github.com/rsdoiel/termlib rather than a
stdlib-only implementation — see DECISIONS.md, “Use
github.com/rsdoiel/termlib for the Terminal UI”.
Work Items
DisplayInstances(t *termlib.Terminal, []inventory.Instance)/DisplayImages(t *termlib.Terminal, []inventory.Image)— formatted table output viatermlib.PadRight/termlib.Truncate(replacesdisplay_instances/display_amis); untagged Project/Environment render as"unknown", untagged Name renders blank, matching the Bash versionPickList[T any](t *termlib.Terminal, le *termlib.LineEditor, items []T, label func(T) string, prompt string) (T, error)— generic numbered pick list; returns an error (not a panic/crash) on invalid input, re-prompts on out-of-range/non-numeric input, entering0cancels (ErrCancelled)Prompt(t *termlib.Terminal, le *termlib.LineEditor, label string, opts ...PromptOption) (string, error)— single free-text prompt with optional default value (WithDefault) and validator function (WithValidator), re-prompting on validation failure (replaces the repeatedecho -n ...; read -r ...pattern)
Tests: pick list with valid/invalid/cancel input,
and prompt with default/validator, driven through
termlib.LineEditor via an os.Pipe() (not a
TTY) rather than a real terminal – LineEditor.Prompt
detects the non-TTY input and falls back to plain line reading, which is
termlib’s own documented/intended way to drive it in tests.
This is the Go equivalent of the Bash version’s skipped “interactive
testing is hard” tests, and is not skipped here.
Dependency: Phase 2
Phase 4 — Create EC2 Instance from AMI (done)
Effort: ~6 hours Priority: High
Files:
internal/workflow/{launch_instance,launch_execute,cloud_init_check,confirm,ssm,userdata}.go
Implemented as several smaller files rather than one, each
independently tested: confirm.go (the reusable yes/no gate
— see below), ssm.go
(WaitForSSMOnline/RunShellCommand, generic
building blocks reused by Phases 10/12/13), userdata.go
(@file vs. inline text), cloud_init_check.go
(the launch-time completion check), launch_instance.go
(params struct + interactive collection), launch_execute.go
(Launch/WaitUntilRunning), and
create_instance_from_ami.go (the orchestrator PLAN.md
originally called out as this phase’s one file).
Confirm(t, le, question) (bool, error) is the first
instance of the “reusable confirmation/dry-run gate”
DECISIONS.md’s “Structure workflows for future
record/replay” calls for — a simple yes/no tier for reversible actions.
Phase 8/11/13’s heavier dry-run + type-to-confirm tier is a separate
function, added when those phases need it.
Work Items
- Pick an AMI (Phase 3’s
PickListoverListImagesresults) - Collect instance type, key pair, security group(s), subnet, IAM
instance profile (optional), user data (optional — accepts inline text
or a local file path, e.g. a file from a local clone of
cloud-init-examples), tags — mirroringcollect_instance_params()’s parameter set, plusProject(default: the source AMI’sProjecttag if set) andEnvironment(always an explicit prompt, no default — seeDECISIONS.md“Introduce a light Project/Environment tagging convention”) - Confirm and call
ec2.RunInstances - Poll
DescribeInstancesuntilrunning(bounded — 5 minutes, matching current Bash behavior) or report a timeout; display connection info (public/private IP, SSH command) on success - If user-data was provided: wait for SSM to report
Online, then runcloud-init status --waitvia SSM (bounded timeout — seeDECISIONS.md, “Enhance Create Instance from AMI: cloud-init file input + completion check”), reporting cloud-init’s actual completion status (donevserror). Skip cleanly (not an error) if SSM never comes online - Structure as: build a resolved
LaunchInstanceParamsstruct (from prompts) → reusable confirmation/dry-run gate → execute againstec2.RunInstances. This is the seam a future replay engine reuses rather than reimplementing (seeDECISIONS.md, “Structure workflows for future record/replay”)
Tests: parameter collection with a fake reader; user-data-from-file loading (success and file-not-found); launch success/failure; poll-until-running with a fake that transitions state after N calls; poll timeout path; cloud-init completion check (done/error/SSM-unavailable)
Dependency: Phase 1 (for the SSM client), Phase 3
Phase 5 — Create EC2 Instance from Cloud-Init YAML (done)
Effort: ~2 hours Priority: Medium
Files:
internal/workflow/launch_from_cloud_init.go,
create_instance_from_cloud_init.go; extracted the shared
execution logic from Phase 4’s create_instance_from_ami.go
into a new runLaunch function in
launch_execute.go, confirmed behavior-preserving against
Phase 4’s existing tests before adding this phase’s own.
Work Items
- Prompt for the cloud-init YAML first — inline text or a local file path (reuses Phase 4’s file-loading logic)
- Pick a base AMI (Phase 3’s
PickListoverListImages) - Collect the remaining parameters and execute via the same
LaunchInstanceParamsstruct and execution function Phase 4 uses — this phase only adds a different front-end prompt sequence, not new execution logic (seeDECISIONS.md, “Add Create EC2 Instance from Cloud-Init YAML as a v1 primitive”) - Same post-launch behavior as Phase 4: poll until
running, then wait for SSM +cloud-init status --wait, reporting completion status
Tests: prompt-ordering test confirming cloud-init is collected before the AMI pick list; otherwise covered by Phase 4’s execution-path tests (no new execution logic to test independently)
Dependency: Phase 4 (shares its execution path directly)
Phase 6 — Start EC2 Instance (done)
Effort: ~2 hours Priority: Medium
Files:
internal/workflow/power_state.go
Reuses Phase 4’s WaitUntilRunning (poll-until-running)
and the displayConnectionInfo helper extracted from
launch_execute.go when Phase 5 shared its execution path —
no new poll/display logic needed here, so the timeout path is covered by
Phase 4’s existing tests rather than re-tested at this orchestrator
level (same reuse pattern as Phase 5’s own Tests note).
Work Items
- Pick a stopped instance
- Confirm (simple yes/no, via the same reusable confirmation gate as every other workflow)
- Call
ec2.StartInstances - Poll
DescribeInstancesuntilrunning(bounded timeout) - Display connection info (public/private IP, SSH command) — note the public IP may have changed since the instance was last running, unless it uses an Elastic IP
Tests: start success/failure; poll-until-running with a fake state transition; poll timeout path; confirmation decline (no API call made)
Dependency: Phase 3
Phase 7 — Stop EC2 Instance (done)
Effort: ~2 hours Priority: Medium
Files:
internal/workflow/power_state.go
WaitUntilRunning (Phase 4) was refactored into a shared
waitUntilState helper in launch_execute.go
(confirmed behavior-preserving against Phase 4/6’s existing tests
first), so WaitUntilStopped here is a one-line wrapper
rather than duplicated polling logic.
Work Items
- Pick a running instance
- Confirm (simple yes/no — reversible, unlike Phase 8’s terminate)
- Call
ec2.StopInstances - Poll
DescribeInstancesuntilstopped(bounded timeout)
Tests: stop success/failure; poll-until-stopped with a fake state transition; poll timeout path; confirmation decline (no API call made)
Dependency: Phase 3
Phase 8 — Terminate EC2 Instance (done)
Effort: ~6 hours Priority: High
Files:
internal/workflow/terminate_instance.go; added
ConfirmDestructive to confirm.go – the heavier
type-to-confirm gate DECISIONS.md’s “Structure workflows
for future record/replay” anticipated. Single-shot (no retry loop on
mismatch), matching ec2_ami_manager.bash’s
type_to_confirm; accepts either the instance ID or Name
(DESIGN.md says “ID or name,” though the Bash version’s actual
remove_ami call site only ever passed the ID – Go version
honors the doc’s stated intent).
Work Items
- Pick an instance
- Dry-run display: what would be destroyed, including whether
any attached EBS volume has
DeleteOnTermination=true(queryBlockDeviceMappingson the instance) — that data is destroyed along with the instance, potentially including not-yet-archived backups (see Phase 13) - Environment check: if
Environment=production, show an additional explicit warning before type-to-confirm (seeDECISIONS.md, “Introduce a light Project/Environment tagging convention”) - Type-to-confirm: user must type the instance ID or name exactly
- Call
ec2.TerminateInstances - Structure as: build a resolved
TerminateInstanceParamsstruct → reusable confirmation/dry-run gate → execute againstec2.TerminateInstances(seeDECISIONS.md, “Structure workflows for future record/replay”)
Tests: dry-run display, including the
DeleteOnTermination warning present/absent; production-tag
warning shown/not-shown; type-to-confirm match/mismatch; termination
success/failure
Dependency: Phase 3
Phase 9 — Manage Tags (done)
Effort: ~4 hours Priority: Medium
Files:
internal/workflow/manage_tags.go
Current tags are fetched fresh via
ec2:DescribeInstances/DescribeImages (both
already return the full Tags list) rather than a separate
ec2:DescribeTags call. TagChangeParams stays
minimal (resource ID, action, key, value), matching Phase 8’s
TerminateInstanceParams precedent for the
params-struct/confirm-gate seam.
Work Items
- Pick a resource — an instance or an AMI
- Display its current tags
- Choose an action: add (new key/value), update (pick existing key, prompt new value), or remove (pick existing key)
- Confirm (simple yes/no — this is cheap and reversible, not the
dry-run/type-to-confirm tier used for AMI removal or backup deletion)
via the same reusable confirmation gate used throughout this project’s
workflows, for consistency and future replay support (see
DECISIONS.md, “Structure workflows for future record/replay”) - Call
ec2.CreateTags(add/update) orec2.DeleteTags(remove) - Renaming an instance is just updating its
Nametag through this same flow — no separate code path. This never touches an AMI’sNameattribute itself, which is immutable via the AWS API once set atCreateImagetime — seeDECISIONS.md, “Add Rename Instance as a v1 primitive; AMI Name is immutable” - If the edited key is
Environment, show a brief note that it’s the same tag used elsewhere in this tool (Phase 11, Remove AMI, and Phase 8, Terminate Instance) to gate production warnings
Tests: add/update/remove success paths, for both an
instance and an AMI target; confirmation decline (no API call made);
CreateTags/ DeleteTags failure handling
Dependency: Phase 3
Phase 10 — Create AMI from EC2 Instance (done)
Effort: ~8 hours Priority: High
Files:
internal/workflow/{volume_info,fstrim,create_ami_execute,create_ami_from_instance}.go
ec2:DescribeVolumes was missing from both the
EC2API interface and DESIGN.md’s Assumptions list – added
to both once this phase’s volume- info gathering needed it.
isSSMOnline is a single-shot check (not Phase 4’s
poll-based WaitForSSMOnline), matching
ec2_ami_manager.bash’s check_ssm_availability
for an instance that’s presumably been running a while already.
WaitForAMIAvailable has no internal timeout at all (unlike
every other poll in this codebase) – only the caller’s ctx
can end it – since the Bash version’s fixed 600-second timeout for this
exact operation was itself a correctness bug (DECISIONS.md,
2026-06-30).
Work Items
- Pick an instance (running or stopped)
- Gather attached volume info (sizes, prior-snapshot detection) and
show the volume-size time estimate table (see
DESIGN.md, “Domain Knowledge Carried Forward”) - If running: show Invenio RDM (Postgres/OpenSearch/Redis/Docker) crash-consistency guidance; offer an SSM fstrim pass (skip cleanly, not an error, if SSM is unavailable on the instance)
- Collect AMI name (default suggestion:
<instance-name-or-id>-copy-<date>, user may override), description, no-reboot flag (running instances only), tags —Projectdefaults to the source instance’sProjecttag if set;Environmentis always an explicit prompt, no default - Call
ec2.CreateImage - Poll
DescribeImagesunboundedly untilavailableorfailed(large Invenio RDM volumes: 20–60+ minutes) — no fixed timeout, matching the fix already made in the Bash version for this same reason - Build the
TagSpecificationsrequest field as a typed SDK struct, not a hand-built string — this is the exact bug class (malformed AWS CLI shorthand for tags) that broke the Bash version in real use - Structure as: build a resolved
CreateAMIParamsstruct → reusable confirmation/dry-run gate → execute againstec2.CreateImage(seeDECISIONS.md, “Structure workflows for future record/replay”)
Tests: volume-info gathering; SSM-unavailable path; fstrim success/decline; AMI-name default generation and validation; create success/failure; unbounded-poll transitions (available/failed) with a fake clock or call-count-based fake
Dependency: Phase 3
Phase 11 — Remove AMI (done)
Effort: ~6 hours Priority: High
Files: internal/workflow/remove_ami.go
The dependency check (instancesUsingAMI) filters the
already-fetched inventory listing by ImageID rather than
making a fresh AWS call – Phase 2’s ListInstances already
carries each instance’s ImageID. Reuses
ConfirmDestructive from Phase 8 unchanged.
Work Items
- Pick an owned AMI
- Dry-run display: what would be deleted
- Dependency check: list instances currently referencing this AMI’s
ImageId - Environment check: if the AMI’s
Environmenttag isproduction, show an additional explicit warning before type-to-confirm (seeDECISIONS.md“Introduce a light Project/Environment tagging convention”) - Type-to-confirm: user must type the AMI ID or name exactly
- Call
ec2.DeregisterImage - Structure as: build a resolved
RemoveAMIParamsstruct → reusable confirmation/dry-run gate → execute againstec2.DeregisterImage(seeDECISIONS.md, “Structure workflows for future record/replay”)
Tests: dry-run display; dependency detection (AMI in use / not in use); production-tag warning shown/not-shown; type-to-confirm match/mismatch; removal success/failure
Dependency: Phase 3
Phase 12 — Show/Export Cloud-Init (done)
Effort: ~8 hours Priority: High
Files:
internal/workflow/{cloud_init_instance,cloud_init_ami,cloud_init_export,show_cloud_init}.go
The always-terminate guarantee is a defer against a
cleanup-scoped
context.WithTimeout(context.Background(), 30s),
deliberately decoupled from the caller’s ctx so cleanup
isn’t skipped by an early return or by ctx itself
being cancelled. Verified with dedicated tests asserting
TerminateInstances is called exactly once across every
failure path (SSM never online, command fails, instance never reaches
running) and exactly zero times when launch itself fails (nothing to
clean up).
Work Items
- Pick an instance or an AMI (either is a valid target)
- Instance path: call
ec2.DescribeInstanceAttribute(attributeuserData), base64-decode, display; report plainly (not as an error) if no user-data was set - AMI path — costs real time/money, so requires its
own explicit confirmation before proceeding (see
DESIGN.mdSecurity Considerations):- Launch the smallest available instance type from the AMI, tagged to
mark it disposable
(e.g.
Purpose=cloud-init-extraction) - Poll until
runningand SSM reportsOnline, with a bounded timeout (this is a diagnostic side-operation, not core creation — unlike Phase 10’s unbounded poll, it should fail cleanly rather than wait indefinitely) ssm.SendCommandto read/var/lib/cloud/instance/user-data.txt, thenssm.GetCommandInvocationfor the output- Always call
ec2.TerminateInstanceson the temporary instance afterward — including when SSM never comes online or the command fails. This must be structured so cleanup cannot be skipped by an early return (Go’sdefer, or an equivalent explicit cleanup path covered by tests)
- Launch the smallest available instance type from the AMI, tagged to
mark it disposable
(e.g.
- The AMI path’s launch-confirmation step uses the same reusable
confirmation gate used throughout this project’s workflows, not a
one-off implementation (see
DECISIONS.md, “Structure workflows for future record/replay”) - Export: prompt for a local file path and write the
decoded YAML there, for manual comparison against a local clone of
caltechlibrary/cloud-init-examples. No inline fetch-and-diff against the GitHub repo in v1 (seeDECISIONS.md, “Add Show/Export Cloud-Init as a v1 primitive”, and the Deferred section below)
Tests: instance-path success; instance-path
no-user-data-set; AMI-path full happy path with a fake SSM client;
AMI-path cleanup-on-SSM-timeout (assert TerminateInstances
is still called); AMI-path cleanup-on-command-failure; export-to-file
success and path-error handling
Dependency: Phase 1, Phase 2, Phase 3
Phase 13 — Backup Archive & Trim (done, unit tests only)
Effort: ~10 hours Priority: High
Files:
internal/workflow/{backup_list,backup_upload,backup_verify,backup_delete,backup_archive}.go
Age filtering happens in Go (FilterByAge), not in the
remote find command – the SSM command only lists every
file’s size/mtime; all threshold logic is local and independently
testable, avoiding fragile shell-arithmetic date math. Building the
remote upload/delete scripts reintroduces the shell-quoting risk
category this whole rewrite exists to eliminate for local
command construction – SSM’s API only accepts a command string, so it’s
unavoidable here. Caught a real quoting bug in review: the upload
script’s S3 destination URI and echoed key were unquoted (only the
source path was), which would have broken on any filename containing a
single quote – fixed by routing every dynamic value through
shellQuote and passing it as a separate printf
argument rather than interpolating into a double-quoted string.
Real-AWS verification remains blocked on the S3 bucket and target
instance IAM policy the user still needs to set up outside this repo
(per DESIGN.md Assumptions #3-4) – flagged rather than
guessed at.
Work Items
- Pick an instance
- Prompt for the backup directory and an age threshold in days — both
explicit, no baked-in default (same reasoning as the
Environmenttag having no default: force a deliberate choice) - Dry-run list (SSM, read-only): candidate files matching the age threshold, with size and age, shown before anything else happens
- Type-to-confirm before proceeding — same safety tier as Phase 11’s (Remove AMI) destructive-action pattern
- Upload phase (SSM): instance uploads each candidate file to S3 via its own AWS CLI/credentials, reports back a small per-file JSON summary (S3 key, size). Nothing deleted yet
- Independent verification: the tool’s own
S3API.HeadObject(Phase 1), using the operator’s credentials, confirms each uploaded key exists with the expected size — this, not the instance’s self-report, is what authorizes deletion - Delete phase (a second, separate SSM command): instance deletes exactly the tool-verified file list, not a re-derived one
- fstrim (reuse Phase 10’s SSM fstrim mechanism), then report bytes freed and any files that failed verification (left untouched)
- Structure as: build a resolved
BackupArchiveParamsstruct (directory, age threshold, bucket) → reusable confirmation/dry-run gate → execute the upload/verify/delete/fstrim sequence (seeDECISIONS.md, “Structure workflows for future record/replay”)
Tests: dry-run empty result; dry-run with matches;
type-to-confirm mismatch; full happy path (all files verified, deleted,
fstrim runs) with fake
EC2API/SSMAPI/S3API;
partial-verification-failure path (only verified files deleted, failures
reported, fstrim still runs on whatever was freed); SSM-unavailable
path
Dependency: Phase 1 (including the
S3API), Phase 2, Phase 3. Real-AWS verification (Phase 16)
additionally depends on Phase 4, since the live test target is a
throwaway instance launched from an existing AMI that already has these
backups baked in — never tested directly against a production
instance.
Phase 14 — Main Menu and Integration (done)
Effort: ~4 hours Priority: High
Files: cmd/awsops/main.go,
internal/workflow/menu.go
Closed an architectural gap Phases 4-13 deliberately deferred here:
each of the ten orchestrators originally took a single-region
EC2API/ SSMAPI, but Phase 2 aggregates
instances/AMIs across all four regions, so the right client isn’t known
until after a resource is picked inside the orchestrator. Fixed
by changing each orchestrator to take per-region client maps and resolve
(internal/workflow/region_clients.go) by the picked
resource’s Region field immediately after the pick –
touched all ten orchestrator files and their tests, verified
build/vet/race clean after each one before moving to the next.
menu.go’s MenuActions struct of
func(ctx) error closures (bound by main.go to
live clients and a mutable instance/AMI snapshot) lets menu dispatch
itself be tested with fakes, without driving any workflow’s full
interactive prompt sequence. Ctrl+C between prompts cancels
ctx via signal.NotifyContext (every poll loop
already selects on ctx.Done()); Ctrl+C/Ctrl+D
during an active prompt surfaces as
termlib.ErrInterrupted/ io.EOF instead,
handled the same way in the menu loop.
Work Items
ShowMainMenu— header, live instance/AMI listings (Phase 2 + 3), 12-option menu, input validation loop- Main loop: dispatch to Phase 4/5/6/7/8/9/10/11/12/13 workflows,
refresh listings after each state-changing operation, handle
Ctrl+C(os/signal) for a clean exit - Wire real AWS clients (Phase 1) into the workflows at startup
Tests: menu navigation and dispatch (fake workflows); refresh-after- operation; clean exit; signal handling
Dependency: Phase 4, 5, 6, 7, 8, 9, 10, 11, 12, 13
Phase 15 — Polish and Error Handling (done)
Effort: ~4 hours Priority: Medium
Work Items
Dependency: Phase 14
Phase 15.1 — Debug Logging (-debug) (done)
Effort: ~2 hours Priority: Medium
Work Items
Dependency: Phase 1 (AWS Client Layer)
Phase 15.2 — Create Key Pair inline (done)
Effort: ~1.5 hours Priority: Medium
Work Items
Dependency: Phase 4 (Create EC2 Instance from AMI)
Phase 15.3 — IAM Instance Profile pick-or-create (done)
Effort: ~2 hours Priority: Medium
Real-AWS testing hit
InvalidParameterValue: ... Invalid IAM Instance Profile name
at launch – see DECISIONS.md, “Support picking or creating an IAM
instance profile from within awsops”.
Work Items
Dependency: Phase 4 (Create EC2 Instance from AMI)
Phase 15.4 — Derive key pair name from a private key filename/path (done)
Effort: ~1 hour Priority: Medium
Real-AWS testing hit InvalidKeyPair.NotFound from typing
a private key file path at “Key pair name” – see DECISIONS.md, “Derive
the AWS key pair name from a private key filename/path”.
Work Items
Dependency: Phase 15.2 (Create Key Pair inline)
Phase 15.5 — Pre-flight check: instance type vs. subnet Availability Zone (done)
Effort: ~2 hours Priority: Medium
Real-AWS testing (via the -debug log) hit
Unsupported: ... instance type ... is not supported in ... Availability Zone
– see DECISIONS.md, “Pre-flight check: instance type vs. subnet
Availability Zone”.
Work Items
Dependency: Phase 4 (Create EC2 Instance from AMI)
Phase 15.6 — Instance type: curated pick list (done)
Effort: ~1 hour Priority: Medium
See DECISIONS.md, “Instance type pick list: curated shortlist, not the full AWS catalog”.
Work Items
Dependency: Phase 4 (Create EC2 Instance from AMI)
Phase 15.7 — Pre-flight check: instance type vs. AMI ENA support (done)
Effort: ~2 hours Priority: Medium
Real-AWS testing hit
InvalidParameterCombination: Enhanced networking ... is required for the '<type>' instance type
– see DECISIONS.md, “Pre-flight check: instance type vs. AMI ENA
support”. Closes the ENA item queued in TODO.md since an earlier
session.
Work Items
Dependency: Phase 4 (Create EC2 Instance from AMI),
Phase 15.6 (reuses promptInstanceType)
Phase 15.8 — Tolerate post-launch/post-create eventual-consistency windows (done)
Effort: ~1 hour Priority: High
A real, confirmed launch failed immediately after
RunInstances succeeded – see DECISIONS.md, “Tolerate
DescribeInstances’ post- RunInstances eventual-consistency window”. Not
an edge case: a near-certain race on every launch that happened to hit
AWS’s brief propagation window.
Work Items
Dependency: Phase 4 (Create EC2 Instance from AMI), Phase 5 (Create AMI from EC2 Instance)
Phase 15.9 — Filter the subnet picker by instance-type Availability Zone support (done)
Effort: ~1.5 hours Priority: Medium
Real-AWS testing surfaced repeated instance-type/subnet back-and-forth – see DECISIONS.md, “Filter the subnet picker by instance-type Availability Zone support”.
Work Items
Dependency: Phase 15.5 (Pre-flight check: instance type vs. subnet Availability Zone)
Phase 15.10 — Move “Show resource lists” to the top of the Compute menu (done)
Effort: ~30 minutes Priority: Low
See DECISIONS.md, “Move ‘Show resource lists’ to the top of the Compute menu; rename from ‘Refresh’”.
Work Items
Dependency: Phase 14 (Main Menu and Integration)
Phase 15.11 — Auto-detect a bare existing-file path in User data / Cloud-init YAML input (done)
Effort: ~1 hour Priority: Medium
Real-world use: typing newt-machine.yaml (no
@ prefix) at the Cloud-init YAML prompt silently became the
instance’s literal user-data instead of loading the file – see
DECISIONS.md, “Auto-detect a bare existing-file path in User data /
Cloud-init YAML input”.
Work Items
Dependency: Phase 4 (Create EC2 Instance from AMI), Phase 5 (Create EC2 Instance from Cloud-Init YAML)
Phase 15.12 — Create EC2 Instance from Cloud-Init YAML always reads from a file (done)
Effort: ~1.5 hours Priority: Medium
Follow-up to Phase 15.11: for this specific prompt, “inline text or @file path” was itself the wrong shape – see DECISIONS.md, “Create EC2 Instance from Cloud-Init YAML always reads from a file”.
Work Items
Dependency: Phase 15.11 (Auto-detect a bare existing-file path in User data / Cloud-init YAML input)
Phase 15.13 — Offer official Ubuntu LTS AMIs alongside owned AMIs (done)
Effort: ~2 hours Priority: Medium
See DECISIONS.md, “Offer official Ubuntu LTS AMIs alongside owned AMIs when picking a base AMI”.
Work Items
Dependency: Phase 4 (Create EC2 Instance from AMI), Phase 15.6 (curated instance-type list), Phase 15.7 (ENA pre-flight check)
Phase 15.14 — Narrow configured regions to us-west-1/us-west-2 (done)
Effort: ~1 hour Priority: High
See DECISIONS.md, “Narrow configured regions to us-west-1/us-west-2”
– follow-up to the official-Ubuntu-AMI feature surfacing a region
(us-west-1) with no provisioned key pairs.
Work Items
Dependency: Phase 1 (region configuration)
Phase 15.15 — Validate key pair name against the AMI’s region (done)
Effort: ~3 hours Priority: High
A real launch failed with InvalidKeyPair.NotFound after
every prompt was already answered and confirmed – see DECISIONS.md,
“Validate key pair name against the AMI’s region”.
Work Items
Dependency: Phase 4 (Create EC2 Instance from AMI), Phase 15.2 (Create Key Pair inline), Phase 15.4 (key filename/path derivation)
Phase 15.16 — Tolerate GetCommandInvocation’s post-SendCommand eventual-consistency window (done)
Effort: ~1 hour Priority: High
Third instance of the same eventual-consistency bug pattern found this session – see DECISIONS.md, “Tolerate GetCommandInvocation’s post-SendCommand eventual-consistency window”.
Work Items
Dependency: Phase 4 (Create EC2 Instance from AMI –
introduced RunShellCommand/the cloud-init completion
check)
Phase 15.17 —
~/.awsops YAML config file (done)
Effort: ~2.5 hours Priority: Medium
See DECISIONS.md, “Add a ~/.awsops YAML config file for
awsops’ own operational settings”.
Work Items
Dependency: Phase 15.14 (Narrow configured regions to us-west-1/us-west-2)
Phase 15.18 — Highlight PickList’s prompt header (done)
Effort: ~1 hour Priority: Low
See DECISIONS.md, “Highlight PickList’s prompt header when color is enabled”.
Work Items
Dependency: Phase 15 (Color output for state –
ui.ColorEnabled(), NO_COLOR/non-TTY
fallback)
Phase 15.19 — Configure per-instance backup directories by Name pattern (done)
Effort: ~1.5 hours Priority: Medium
See DECISIONS.md, “Configure per-instance backup directories by Name pattern”.
Work Items
Dependency: Phase 15.17 (~/.awsops YAML
config file)
Phase 15.20 — Per-file upload progress for Backup Archive & Trim (done)
Effort: ~1.5 hours Priority: Medium
See DECISIONS.md, “Per-file upload progress for Backup Archive & Trim”.
Work Items
Dependency: Phase 11 (Backup Archive & Trim)
Phase 15.21 — Preflight check: S3 bucket access before Backup Archive & Trim’s dry-run list (done)
Effort: ~1 hour Priority: Medium
See DECISIONS.md, “Preflight check: S3 bucket access before Backup Archive & Trim’s dry-run list”.
Work Items
Dependency: Phase 11 (Backup Archive & Trim)
Phase 15.22 — Resolve a bucket’s actual region before Backup Archive & Trim’s access check (done)
Effort: ~1.5 hours Priority: High
Real-AWS regression found immediately after Phase 15.21 shipped – see DECISIONS.md, “Resolve a bucket’s actual region before Backup Archive & Trim’s access check”.
Work Items
Dependency: Phase 15.21 (Preflight check: S3 bucket access)
Phase 15.23 — Namespace backup uploads by instance (done)
Effort: ~1 hour Priority: Medium
See DECISIONS.md, “Namespace backup uploads by instance”.
Work Items
Dependency: Phase 11 (Backup Archive & Trim)
Phase 15.24 — Show instance IP addresses in the main listing (done)
Effort: ~1 hour Priority: Medium
See DECISIONS.md, “Show instance IP addresses in the main listing”.
Work Items
Dependency: Phase 1 (Unified Resource Listing)
Phase 15.25 — Suppress aws s3 cp’s progress output to avoid truncating the OK/FAIL signal (done)
Effort: ~1 hour Priority: High
Real-AWS regression found immediately after the IAM permission fix above – see DECISIONS.md, “Suppress aws s3 cp’s progress output to avoid truncating the OK/FAIL signal”.
Work Items
Dependency: Phase 15.20 (Per-file upload progress)
Phase 15.26 — Preflight check: AWS CLI availability before Backup Archive & Trim (done)
Effort: ~1 hour Priority: High
Hit twice in a row in real-AWS testing (newauthors, then data-new) – see DECISIONS.md, “Preflight check: AWS CLI availability before Backup Archive & Trim”.
Work Items
Dependency: Phase 11 (Backup Archive & Trim)
Phase 16 — Testing
Effort: ~6 hours Priority: High
Work Items
Dependency: Phase 14
Phase 17 — Documentation and Bash Retirement (done)
Effort: ~2 hours Priority: Medium
Work Items
Dependency: Phase 16
Phase 18 — Domain Picker Refactor
Effort: ~4 hours Priority: High
Files: internal/ui/domainmenu.go,
cmd/awsops/main.go,
internal/workflow/menu.go
Per DECISIONS.md, 2026-07-02 “Redesign navigation as a
domain picker; add Key Management, S3, and CloudFront domains”. Pure
navigation refactor — no new AWS calls, and Compute’s existing workflows
and tests are untouched. Runs alongside Phase 16/17, not blocking or
blocked by them.
Work Items
- New top-level domain picker: Compute / Key Management / S3 /
CloudFront / Exit (see
DESIGN.md, “Navigation: Domain Picker”) - Extract Compute’s existing “fetch resources → display → numbered
menu → dispatch → refresh → loop” pattern into a shared
internal/ui/domainmenu.goloop, parameterized by a resource-fetch function, a display function, and a menu-item list - Wire Compute’s existing
menu.gothrough the new shared loop as the first domain; behavior must be identical to today’s — this phase adds no new Compute-visible behavior - “Back to domain picker” in every domain menu; “Exit” from inside any domain menu exits the whole program, not just that domain
- Stub Key Management/S3/CloudFront picker entries as “not yet implemented” placeholders until Phases 19-21 land, so this phase can be merged and tested independently of them
Tests: domain picker dispatches to the right domain loop; Compute’s existing workflow tests continue to pass unmodified; “Back”/“Exit” behavior from within a domain menu
Dependency: Phase 14 (Main Menu and Integration)
Phase 19 — Key Management Domain (done)
Effort: ~6 hours Priority: Medium
Files: internal/inventory/keypairs.go,
internal/workflow/{keypair_create,keypair_import,keypair_delete,keymgmt_common,keymgmt_menu}.go
Implements DESIGN.md Features 13-16.
Work Items
Tests:
internal/inventory/keypairs_test.go,
internal/workflow/{keypair_create,keypair_import,keypair_delete,keymgmt_menu}_test.go
– fakes for
DescribeKeyPairs/CreateKeyPair/ImportKeyPair/DeleteKeyPair
covering success, name-collision re-prompt (Create and Import),
malformed public key file, dependent-instance detection, and the menu
loop’s dispatch/refresh/error/exit-signal behavior.
go build ./..., go vet ./...,
go test ./... -race, gofmt -l . all clean.
Tests: fakes for
DescribeKeyPairs/CreateKeyPair/ImportKeyPair/
DeleteKeyPair covering success, name-collision re-prompt,
malformed public key file, dependent-instance detection
Dependency: Phase 18; reuses Phase 15.2’s
CreateKeyPair wrapper
Phase 20 — S3 Domain (Buckets & Static Websites) (done)
Effort: ~16 hours (raised from ~10h after adding
Feature 21.1, 2026-07-08 — see DECISIONS.md) Priority:
Medium Files: internal/awsclient/s3.go
(broadened), internal/inventory/buckets.go,
internal/workflow/{bucket_create,bucket_website,bucket_sync,bucket_browse,bucket_lifecycle,s3_menu}.go
Implements DESIGN.md Features 17-21, 21.1, and the
2026-07-02 “CloudFront + OAC by default for static websites” decision,
with scope decisions made 2026-07-08 before implementation started (see
DECISIONS.md): public-read bucket policy opt-out deferred, key-prefix
filter added to Browse/Manage Objects, and Feature 21.1 (Manage Bucket
Lifecycle Policies) added as new scope with a Purpose-tag-driven
guided/generic split.
Work Items
-
backup: guided flow, two yes/no-shaped prompts (expire-after-days, transition-after-days + a curated storage-class pick list: Standard-IA, Intelligent-Tiering, Glacier Flexible Retrieval, Glacier Deep Archive), optional key-prefix scopeinternal/website/untagged: generic editor — named rules (unique ID), optional prefix, zero-or-more transitions from the fulltypes.TransitionStorageClassenum, optional expiration; add/edit/ remove by ID- both paths write the complete modified rule set via
s3:PutBucketLifecycleConfigurationin one call (the API has no per-rule operations); rule edit/remove is a plain yes/no confirm, with on-screen text noting this schedules future automated deletion (AWS’s own ~24-48h evaluation cadence), not an immediate one
Tests:
internal/inventory/buckets_test.go,
internal/workflow/{bucket_create,bucket_website,bucket_sync,bucket_browse,bucket_lifecycle,s3_menu}_test.go
– fakes for each new S3 call covering success/error paths
(bucket-name-taken, website-not-configured treated as non-error, sync
diff correctness, upload/delete confirmations never bundled, prefix
filter narrows the object listing, lifecycle guided-vs-generic branch
selection by Purpose tag, rule add/edit/remove round-trips
through the fetch-modify-PutBack cycle correctly) — TDD throughout.
go build ./..., go vet ./...,
go test ./... -race, gofmt -l . all clean.
Real-AWS verification (2026-07-08): created one
throwaway bucket per purpose, configured website hosting, ran Sync twice
(upload pass, then a locally-deleted-file pass exercising the separate
delete confirm), browsed with and without a prefix filter (view
metadata, delete an object), set and round-tripped a guided backup
lifecycle policy, and added/edited/removed a named rule via the generic
editor – all against account 074441911104. Found and fixed one real bug
along the way (empty- Rules
PutBucketLifecycleConfiguration on last-rule removal; see
DECISIONS.md) and left one open, documented gap (no local validation
that transition-days < expiration-days; see TODO.md). All throwaway
buckets and objects cleaned up afterward – no production bucket was
touched.
Dependency: Phase 18
Phase 20.1 — S3 Object Management: Interactive File Manager (huh + bubbletea)
Status: implemented and unit-tested 2026-07-09
(go build ./..., go vet ./...,
go test ./... -race all clean); not yet verified against
real AWS — see Phase 22. This is the “next release focuses on
improving UX and the UI” work flagged when 0.0.1 shipped (see
DECISIONS.md, “0.0.1 scope: ship on termlib as-is; postpone CloudFront
and the UI/UX overhaul”). Numbered 20.1 (inserted after Phase 20 without
renumbering CloudFront’s Phases 21-22 — same decimal-insertion
convention already used for Phase 15.1-15.26 and DESIGN.md’s Feature
21.1) because it revises Phase 20’s S3 domain rather than adding a new
one.
Effort: ~24 hours estimated; actual scope grew
somewhat during implementation (extracting internal/s3diff
and adding a dedicated Sync action — see below and DECISIONS.md).
Priority: High Files (as actually built,
package boundary resolved during implementation):
internal/awsclient/s3.go/logging_s3.go (added
GetObject + wrapper),
internal/workflow/s3_menu.go (revised menu),
internal/workflow/object_browser.go (huh pre-flight, new),
internal/filemanager/*.go (the bubbletea
Model — its own package, not folded into
internal/workflow, since internal/workflow now
depends on it via object_browser.go and a same-package
dependency the other way would cycle), internal/s3diff/*.go
(new — diffSync/walkLocalTree/
listAllBucketObjects/contentTypeFor extracted
here, out of the now- deleted bucket_sync.go, so both
internal/workflow and internal/filemanager can
depend on the same diff logic without a
workflow<->filemanager import cycle),
internal/workflow/bucket_browse.go (stripped to just
listBucketObjectsWithPrefix, still needed by Delete
Bucket’s empty-bucket check) and
internal/workflow/bucket_delete_objects.go (deleted) and
internal/workflow/bucket_sync.go (deleted, superseded by
internal/s3diff + the file manager’s Sync action).
Implements DESIGN.md Features 21.2-21.8.
Work Items
Tests: resolved —
github.com/charmbracelet/x/exp/teatest is real and usable
(confirmed against actual source, per this project’s evaluation
discipline): teatest.NewTestModel runs the
Model as a real bubbletea.Program against an
in-memory terminal, .Send injects key messages, and
teatest.WaitFor polls rendered output. One practical caveat
learned while writing these tests: bubbletea’s renderer only retransmits
screen lines that changed since the last frame, so asserting on
unchanged-but-still-visible text across two separate
WaitFor calls can race (the earlier call already drained
the frame that contained it) — check multiple substrings in one
WaitFor condition, or assert on the status line’s derived
text instead of raw row content, when that matters.
go test -race caught one genuine bug this surfaced: a
running action’s background goroutine (runDelete) was
mutating pane state (clearTags) directly instead of only
sending progress text over its channel, racing with the render loop’s
concurrent read — fixed by moving that mutation to the overlay-dismiss
handler, which runs on Update’s single goroutine. Diff/glob/listing
helpers are tested as plain Go functions independent of the
Model (internal/s3diff,
internal/filemanager/entry_test.go,
listing_test.go, pane_test.go).
Dependency: Phase 20 (done)
Phase 20.2 — S3 Menu: Convert RunS3Menu to huh.Select (done)
Status: implemented and unit-tested 2026-07-10
(go build ./..., go vet ./...,
go test ./... -race all clean). Continues
continue_next_time.txt’s next-up item from the Phase 20.1
session: “replace the S3 management menu and display of buckets with the
huh module” — this phase covers the menu half; bucket-selection call
sites are Phase 20.4 (below).
Files: internal/workflow/s3_menu.go,
s3_menu_test.go, huh_accessible_test.go (new —
reusable pipe-testable-input helper).
Work Items
Tests: all existing dispatch/refresh/error-handling
coverage carried over unchanged (same input strings — huh’s
accessible-mode 1-indexed numbering happens to match
s3MenuItems’ order); new
TestMapS3MenuPickerErr for the abort mapping.
Dependency: Phase 20.1 (done, established the huh call-site pattern); the pipe-testability resolution above (done, same session).
Phase 20.3 — S3 Domain: Paged, Accessible Resource List Display (superseded)
Superseded 2026-07-10, same day, by Phase 20.6
below. Screen-reader/accessible-mode compatibility – this
phase’s central constraint – turned out not to be an actual requirement
once discussed directly (DECISIONS.md, “Deprecate termlib; standardize
on huh/ bubbletea before 0.0.2”).
internal/ui.PagedTable/DisplayBuckets,
implemented below, are retired in favor of a
bubbletea-based List-tier component built on a new shared
internal/tui chrome package (Phase 20.5), less than a day
after landing. Left below as the accurate record of what was implemented
and why it changed, not deleted.
Status (as originally completed): implemented and unit-tested
2026-07-10 (go build ./...,
go vet ./..., go test ./... -race all clean).
Exposed by testing Phase 20.2: every successful S3 menu action called
actions.Refresh(ctx), which both re-fetched bucket data and
printed the entire bucket table
(ui.DisplayBuckets) unconditionally — cluttering the menu’s
redisplay after every action, with no pagination for a large bucket
count. Full design, mockup (approved before implementation started), and
rejected alternatives: DESIGN.md, “S3 Resource List Display — Paged,
Accessible- Compatible”; decision record: DECISIONS.md, “Decouple the S3
menu from resource-list display; add a generic paged table to
internal/ui”.
Priority: requested directly by the user, ahead of
Phase 20.4. Files:
internal/ui/paged_table.go (new, generic — not
bucket-specific), paged_table_test.go (new),
internal/ui/display.go (DisplayBuckets now
takes le and returns error, delegates to
PagedTable), display_test.go,
internal/workflow/s3_menu.go
(S3Actions.ShowResourceLists, new field; “Show resource
lists” entry now dispatches to it instead of Refresh),
s3_menu_test.go, cmd/clasm/main.go
(refreshS3 now silent-refetch-only;
showS3ResourceLists, new closure, wired to
ShowResourceLists).
Work Items
Tests: internal/ui/paged_table_test.go
(11 cases: single/multi page, next/previous navigation and their
at-boundary no-ops, page-back, invalid-command reprompt,
case-insensitive commands, empty rows, and read-error propagation — each
via Title’s recorded call args, not string-scraping
banners); internal/ui/display_test.go
(DisplayBuckets empty/populated/paginates-large-lists);
internal/workflow/s3_menu_test.go (new
TestRunS3Menu_ShowResourceListsDispatchesToItsOwnAction,
since no prior test exercised choosing menu item 1 at all). No new
testability question — PagedTable is plain
termlib/LineEditor.Prompt sequential printing
(no huh), pipe-testable the same way
PickList’s existing tests already are, reusing this
package’s own newPipeEditor helper.
Dependency: Phase 20.2 (done — this phase was found while testing it, not a prerequisite of its design).
Phase 20.4 — S3 Bucket Selection: Convert to tui.Picker (done)
Status: implemented and unit-tested 2026-07-10
(go build ./..., go vet ./...,
go test ./... -race, gofmt -l all clean).
Originally scoped to convert to huh.Select
(continue_next_time.txt‘s remaining next-up item from Phase
20.1’s session); retargeted before any code was written once the user
pointed out huh.Select’s rendering doesn’t match the
List/Manager tiers’ chrome (“this UI should feel the same whether I
select a bucket, an AMI or an EC2 instance”) — see DECISIONS.md, “Add a
Picker tier: resource selection gets its own internal/tui component, not
huh.Select,” and DESIGN.md’s “Picker tier” section (with the full map of
every current resource-selection call site across the app).
Converted the bucket-selection step inside
ConfigureBucketWebsite (bucket_website.go),
ManageBucketLifecyclePolicies
(bucket_lifecycle.go), and DeleteBucket
(bucket_delete.go) — previously
ui.PickList(t, le, buckets, bucketLabel, "Select a bucket")
— to a shared pickBucket helper
(bucket_website.go, next to bucketLabel) built
on tui.RunPicker. CreateBucket stayed out of
scope (it creates a new bucket, not select an existing one); the rest of
each workflow stays on termlib. object_browser.go’s
existing huh.Select-based bucket pre-flight was NOT touched
by this phase — whether it should also move to PickerModel
is a separate question, not decided here.
Testable-core split, since tui.RunPicker runs a
real bubbletea Program that can’t be driven by a test’s pipe
input (mirrors the
RunS3Menu/runS3Menu split from Phase 20.2):
each of the three exported functions now does the picker call, then
delegates to an unexported core taking the already-resolved
bucket directly (configureBucketWebsite,
manageBucketLifecyclePolicies, deleteBucket).
Every existing test for “pick a bucket, then do X” was rewritten to call
the unexported core with a bucket value instead of driving a
ui.PickList-shaped "1\n" pipe input; each
function’s own “cancel while picking a bucket” test
(TestConfigureBucketWebsite_ CancellationAbortsCleanly,
TestManageBucketLifecyclePolicies_ CancellationAtBucketPick,
and DeleteBucket’s equivalent) was retired — that tested
ui.PickList’s “0=Cancel” numbered-option convention, which
no longer exists once selection is tui.RunPicker-based. The
picker-selection step itself is covered only by manual/interactive
verification going forward, the same accepted limitation
object_browser.go’s huh-based bucket pre-flight already
has. cancelledIsNil (manage_tags.go) now also
recognizes tui.ErrCancelled alongside
ui.ErrCancelled, so cancelling either kind of picker
behaves identically from the operator’s point of view.
Dependency: Phase 20.8
(internal/tui.PickerModel itself, done).
Phase 20.5 — internal/tui: Shared Chrome Package (extracted from the file manager) (done)
Status: implemented and unit-tested 2026-07-10
(go build ./..., go vet ./...,
go test ./... -race all clean). First piece of DESIGN.md’s
“Terminal UI Architecture: Menus, Actions, Lists, and Managers”;
decision record: DECISIONS.md, “Terminal UI architecture: menu →
action/list/manager taxonomy; shared internal/tui chrome package.”
Files: new internal/tui package
(box.go, scroll.go, style.go +
their _test.go files, written test-first);
internal/filemanager/view.go (box-drawing/scroll/style
helpers removed, replaced with calls into internal/tui);
internal/filemanager/box_test.go/scroll_test.go
(moved tests removed, remaining Model-level tests updated to call
tui.RuneLen instead of the now-moved
stripANSI).
Work Items
Tests:
internal/tui/box_test.go/scroll_test.go/style_test.go
(20 cases total, several new beyond what
internal/filemanager already had indirectly — direct
coverage for TopBorder/BottomBorder/
Divider/SplitDivider/MergeDivider/BoxLine,
which previously only had indirect coverage via
filemanager.Model.View()’s tests), written before
box.go/scroll.go/style.go
existed. internal/filemanager’s existing test suite
(unchanged assertions,
box_test.go/scroll_test.go trimmed to their
Model-level cases) is the regression check that the extraction was
behavior-preserving.
Dependency: none (pure refactor, no new external dependency).
Phase 20.6 — S3 Domain: List Viewer bubbletea Component (replaces PagedTable) (done)
Status: implemented and unit-tested 2026-07-10
(go build ./..., go vet ./...,
go test ./... -race all clean). Replaces Phase 20.3
(superseded, above). Full design: DESIGN.md, “Terminal UI
Architecture…,” “List tier” section; decision record: DECISIONS.md,
“Terminal UI architecture…”.
Files: internal/tui/listview.go (new:
ListViewConfig, ListViewModel,
NewListViewModel, RunListView) +
listview_test.go (new, written test-first);
internal/ui/display.go (DisplayBuckets
rewritten around tui.RunListView;
bucketListViewConfig extracted as its testable core);
internal/ui/paged_table.go/paged_table_test.go
(removed); cmd/clasm/main.go
(showS3ResourceLists now calls
ui.DisplayBuckets(ctx, s3State.buckets) — no
term/le needed, huh/
termlib aren’t involved at all). The “List S3 Buckets”
rename and dropping “Back to domain picker” are Phase 20.7, not this
phase — this phase only replaces the rendering mechanism behind
“Show resource lists,” not its label or the surrounding menu.
Work Items
Tests: internal/tui/listview_test.go (9
cases). A real rendering lesson surfaced while writing them, worth
keeping in mind for any future internal/tui component: when
rendered content height exactly matches the declared terminal
height (this component’s own “fill the screen” design, by construction —
windowHeight = height - chrome), driving it through a real
teatest.NewTestModel Program can lose its own top line to
the emulated terminal’s scrolling, a known class of issue with inline
(non-tea.WithAltScreen) bubbletea rendering, not a bug in
this component specifically. internal/filemanager’s own
test suite already sidesteps this the same way:
exact-height/scroll-window assertions
(TestModel_LargeListing_*) drive Model
directly (set width/height, call
Update/View() synchronously) rather than
through teatest, reserving teatest for
key-driven behavior with content comfortably smaller than the terminal.
This phase’s tests follow the same split.
Dependency: Phase 20.5 (done — the shared chrome it’s built on).
Dependency: Phase 20.5 (the shared chrome it’s built on).
Phase 20.7 — S3 Menu: universal ‘q’ quit key; remove “Back to domain picker”; rename “Show resource lists” (done)
Status: implemented and unit-tested 2026-07-10
(go build ./..., go vet ./...,
go test ./... -race all clean). Applies DECISIONS.md’s “TUI
keybinding conventions” to the one menu converted so far.
Files: internal/workflow/s3_menu.go,
s3_menu_test.go.
Work Items
Tests: s3_menu_test.go rewritten around
a context.WithCancel +
cancel-from-within-the-test-action-closure pattern for every test that
previously chose “Back to domain picker” (item 7) to end the loop after
observing one dispatch — that menu item no longer exists, and accessible
mode has no way to simulate the q/ctrl+c abort that
replaces it (same limitation mapS3MenuPickerErr already
documented). New TestS3MenuItems_NoBackToDomainPickerEntry
(exactly 6 items, no nil action) and
TestS3MenuItems_FirstItemIsListS3Buckets guard the removal/
rename directly. The q-triggers-Quit behavior itself can
only be confirmed by real interactive use — not yet done, same class of
gap as this session’s other huh/bubbletea
work.
Dependency: Phase 20.2 (the menu this applies to).
Phase 20.8 — internal/tui: PickerModel (selectable, filterable List-tier component) (done)
Status: implemented and unit-tested 2026-07-10
(go build ./..., go vet ./...,
go test ./... -race, gofmt -l all clean). Full
design: DESIGN.md, “Terminal UI Architecture…,” “Picker tier” section;
decision record: DECISIONS.md, “Add a Picker tier: resource selection
gets its own internal/tui component, not huh.Select.”
Files: internal/tui/picker.go
(PickerConfig, PickerModel,
NewPickerModel, RunPicker,
ErrCancelled) + picker_test.go (12 cases,
written test-first).
Work Items
A real rendering finding, beyond what Phase 20.6 already
documented: the content area’s rendered height must be pinned
to the unfiltered row count (bounded by the window height), not
however many rows the current filter happens to match — otherwise the
box’s height shrinks and grows as the operator types a filter, which
reproduced the same class of inline-bubbletea-rendering hiccup Phase
20.6 found with exact/changing frame heights (confirmed by a failing
test before this fix, not just reasoned about). Fixed by padding the
content area with blank rows up to a stable height determined by the
total dataset size — incidentally also better UX (a fixed-height results
viewport while typing a filter, fzf-style, rather than the
box visibly resizing).
Tests: written before the implementation, following
listview_test.go’s established split (teatest
for key-driven behavior with content comfortably smaller than the
terminal, direct Model-driving for exact
scroll-window/height assertions). Two teatest-based filter
tests initially failed for a second, distinct reason: bubbletea only
retransmits screen lines that changed since the immediately
preceding frame, so checking for the same text across two separate
WaitFor calls (one already having drained it from the
stream) can race if a later frame doesn’t happen to change that
particular line again — fixed by combining assertions into single
WaitFor calls, exactly the workaround
internal/filemanager’s own tests already document for this
same class of issue.
Dependency: Phase 20.5 (the shared chrome it’s built on).
Phase 20.9 — Lifecycle Rules Action Menu: Convert to huh.Select (done)
Status: implemented and unit-tested 2026-07-10
(go build ./..., go vet ./...,
go test ./... -race, gofmt -l all clean).
Requested directly by the user after manually trying the S3 domain
(Phase 20.7’s q key took a moment to render the first time,
prompting a live check): convert
ManageBucketLifecyclePolicies’s “Choose an action” menu
(Add rule/Edit rule/Remove rule/View rule details) from
ui.PickList to huh.Select, matching
RunS3Menu’s Phase 20.2/20.7 pattern exactly — this is a
guide-menu-shaped choice (a small, fixed action set), not a Picker-tier
candidate (DESIGN.md’s “Picker tier” map already excluded it for this
reason).
Files:
internal/workflow/bucket_lifecycle.go
(pickLifecycleAction, new;
lifecycleActionLabel removed, no longer needed),
bucket_lifecycle_test.go (all 15 tests updated).
Work Items
Tests: action-menu selections now feed through a
separate newHuhAccessibleInput reader, not le
(which still feeds every other prompt in this function —
rule/storage-class PickLists, confirms, day-count/ID input,
unaffected by this phase). Several tests that used to select “Back”
(position 5) to end the loop cleanly were restructured around real
terminating actions instead, since that position no longer exists and
the q/ctrl+c abort that replaces it can’t be simulated in
accessible mode (same limitation mapS3MenuPickerErr already
documents) — e.g. choosing “Edit rule”/“View rule details” with zero
rules present returns immediately by construction, which several tests
already relied on incidentally and now rely on deliberately.
TestManageBucketLifecyclePolicies_BackActionSkipsPut
(tested the “Back” choice specifically) was retired, matching the
precedent set by
TestRunS3Menu_BackToDomainPickerDoesNotRefresh in Phase
20.2.
Dependency: Phase 20.2 (established the pattern this reuses).
Phase 20.10 — Menu Tier: Top-Level Navigation Menus (done)
Status: implemented and unit-tested 2026-07-10
(go build ./..., go vet ./...,
go test ./... -race, gofmt -l all clean).
First batch of DESIGN.md’s Menu-tier punch list, working through it in
the order the user requested (menu tier, then picker, then list).
Converts the three top-level navigation menus from
ui.PickList to huh.Select, each an exact copy
of RunS3Menu’s Phase 20.2/20.7 pattern: select by index
(each item’s action is a func, not comparable),
q bound alongside ctrl+c on Quit,
a printed hint above the menu, the redundant “Back”/“Exit” menu item
dropped.
Files:
domain_menu.go/domain_menu_test.go,
menu.go/ menu_test.go,
keymgmt_menu.go/keymgmt_menu_test.go.
Work Items
Tests: every test that used to select “Back”/“Exit”
(by position) to end a menu loop was rewritten around the
context.WithCancel +
cancel-from-within-the-test-action-closure pattern established in Phase
20.7 (cancelingAction, shared from
s3_menu_test.go rather than redefined per file); tests for
the removed-item’s own specific behavior
(TestRunMainMenu_BackToDomainPickerDoesNotRefresh,
TestRunKeyMgmtMenu_BackToDomainPickerDoesNotRefresh,
TestRunMainMenu_CleanExitOnCancelledPickList,
TestRunKeyMgmtMenu_CleanExitOnCancelledPickList,
TestRunDomainPicker_ExitEndsTheProgram,
TestRunDomainPicker_CleanExitOnCancelledPickList) were
retired, same precedent as Phase 20.2/20.7/20.9. New
TestDomainItems_NoExitEntry/
TestMainMenuItems_NoBackToDomainPickerEntry/
TestKeyMgmtMenuItems_NoBackToDomainPickerEntry guard each
menu’s item count and non-nil actions directly.
Dependency: Phase 20.2 (the pattern this reuses).
Phase 20.11 — Menu Tier: Remaining Punch-List Items (done)
Status: implemented and unit-tested 2026-07-10
(go build ./..., go vet ./...,
go test ./... -race, gofmt -l all clean).
Completes DESIGN.md’s Menu-tier punch list – every remaining
ui.PickList call site classified as Menu tier is now
huh.Select. Two shared helpers added to
domain_menu.go next to runMenuField/
menuQuitKeyMap: pickString (fixed
[]string options) and its generic backer
pickComparable[T comparable] (fixed []T
options with a caller-supplied label func) – covers every remaining site
without repeating the index-selection workaround
pickS3MenuItem/ pickMainMenuItem/etc. needed
only because their option types embed a func field.
Work Items
-
- Threading this up the call chain required splitting
CollectLaunchInstanceParams/CollectLaunchInstanceParamsFromCloudInit(launch_instance.go/launch_from_cloud_init.go) andCreateInstanceFromAMI/CreateInstanceFromCloudInit(create_instance_from_ami.go/create_instance_from_cloud_init.go) into entry points + testable cores, all sharing onemenuInput/menuOutputpair down topromptInstanceType
- Threading this up the call chain required splitting
Tests: every affected test call site now feeds its
huh.Select(s) via a separate newHuhAccessibleInput reader
instead of the numbered le-pipe selection
ui.PickList used to read; le still feeds every
other prompt unaffected by these conversions. Cancellation tests for
pickers that used to support a PickList
“0=Cancel”/le-driven abort
(TestShowCloudInit_CancelledKindPickList,
TestCreateBucket_RegionCancellationAbortsCleanly,
TestCreateKeyPairStandalone_CancelledRegionPick,
TestImportKeyPairStandalone_CancelledRegionPick) were
retired – q/ ctrl+c abort has no
accessible-mode keyboard to simulate it with, same precedent as every
prior phase’s menu conversions. cancelledIsNil
(manage_tags.go) now also matches
huh.ErrUserAborted, unifying it with
ui.ErrCancelled/tui.ErrCancelled as the one
cancellation-mapping policy for one-off Menu-tier pickers (as opposed to
mapMenuPickerErr, which is specific to domain-loop menus
backing out to ErrBackToDomainPicker).
Files: domain_menu.go (new
pickString/pickComparable helpers),
show_cloud_init.go/_test.go,
manage_tags.go/_test.go,
bucket_create.go/_test.go,
keymgmt_common.go, keypair_create.go/
_test.go,
keypair_import.go/_test.go,
launch_prompts.go/ _test.go,
instance_type_az_check.go/_test.go,
instance_type_ena_check.go/_test.go,
launch_instance.go/_test.go,
launch_from_cloud_init.go/_test.go,
create_instance_from_ami.go/ _test.go,
create_instance_from_cloud_init.go/_test.go,
bucket_lifecycle.go/_test.go.
Dependency: Phase 20.10 (the
runMenuField helper this builds on).
DESIGN.md’s Menu-tier punch list is now fully converted – next up is the Picker tier (8 remaining entries), per the user’s requested order (menu, then picker, then list).
Phase 20.12 — Picker Tier: Every Remaining Resource Selector (done)
Status: implemented and unit-tested 2026-07-10
(go build ./..., go vet ./...,
go test ./... -race, gofmt -l all clean).
Completes DESIGN.md’s Picker-tier punch list – every remaining
ui.PickList call site classified as Picker tier
(fetched/variable-length resource collections) is now
tui.RunPicker. Six new one-line-per-type picker helpers
added next to each resource’s own label function, all following
pickBucket’s exact shape (Phase 20.4): build
rows []string from the resource’s label func, call
tui.RunPicker, index back into the original slice –
pickInstance/pickImage (power_state.go/
launch_instance.go), pickSubnet (launch_prompts.go),
pickInstanceProfileChoice/pickRole
(create_instance_profile.go), pickKeyPairChoice
(create_key_pair.go), pickKeyPairForDeletion
(keypair_delete.go), pickLifecycleRule
(bucket_lifecycle.go).
Work Items
Tests: every affected call site’s
happy-path/error-path tests were rewritten to call the new testable core
with an already-resolved resource instead of driving
ui.PickList’s numbered selection through le; a
handful of fakes needed a forced-error field
(fakeIAMClientNoProfiles(),
errNoKeyPairsConfigured on fakeEC2Client) so
unrelated launch-params tests don’t themselves reach the now-bubbletea
IAM-profile/key-pair pickers. “0=Cancel”/ list-selection tests for every
converted picker were retired – a real bubbletea Program can’t be
pipe-tested, no keyboard to simulate an abort or a specific-item
selection with – the same precedent pickBucket (Phase 20.4)
already established; each retirement is commented in place noting what
still covers the underlying logic directly (the resource’s own
list/filter tests, or a newly-split testable core).
Files:
power_state.go/_test.go,
launch_instance.go/_test.go,
launch_from_cloud_init.go/_test.go,
create_instance_from_ami.go/ _test.go,
create_instance_from_cloud_init.go/_test.go,
terminate_instance.go/_test.go,
backup_archive.go/_test.go,
create_ami_from_instance.go/_test.go,
remove_ami.go/_test.go,
show_cloud_init.go/_test.go,
manage_tags.go/_test.go,
launch_prompts.go/_test.go,
create_instance_profile.go/_test.go,
create_key_pair.go/_test.go,
keypair_delete.go/_test.go,
bucket_lifecycle.go/_test.go,
userdata_test.go (gained
promptCloudInitYAMLFile’s own direct tests, migrated out of
launch_from_cloud_init_test.go).
Dependency: Phase 20.4 (pickBucket, the
pattern every helper here copies) and Phase 20.11 (the Menu-tier sweep
that had to finish first).
DESIGN.md’s Picker-tier punch list is now fully converted – next up is the List tier (3 remaining entries: EC2 instances, AMIs, Key pairs), per the user’s requested order (menu, then picker, then list).
Phase 20.13 — List Tier: EC2 Instances, AMIs, Key Pairs (done)
Status: implemented and unit-tested 2026-07-10
(go build ./..., go vet ./...,
go test ./... -race, gofmt -l all clean).
Completes DESIGN.md’s List-tier punch list – the last remaining tier
from the full termlib-to-huh/bubbletea conversion sweep.
DisplayInstances, DisplayImages, and
DisplayKeyPairs (internal/ui/display.go)
converted to tui.RunListView, mirroring
DisplayBuckets/ bucketListViewConfig (Phase
20.6) exactly: each gained an
instanceListViewConfig/imageListViewConfig/keyPairListViewConfig
builder (pure, unit-testable column formatting, reusing the existing
orUnknown/orNone/stateColor
helpers unchanged) and became a thin func(ctx, ...) error
wrapper.
Work Items
Tests: display_test.go’s
TestDisplayInstances_*/
TestDisplayImages_*/TestDisplayKeyPairs_*
(direct calls against a bytes.Buffer-backed
termlib.Terminal) replaced with
TestInstanceListViewConfig_*/TestImageListViewConfig_*/
TestKeyPairListViewConfig_* (direct calls against the new
pure builders, asserting on
cfg.Header/cfg.Rows/cfg.Title),
matching TestBucketListViewConfig_*’s existing style
exactly; the two color-specific tests became
TestInstanceRow_ColorEnabled_ AppliesStateColor/ColorDisabled_NoANSICodes,
calling instanceRow directly with an explicit bool instead
of relying on the ambient terminal state DisplayInstances
used to take as a parameter. New
TestStyleRow_CursorRowReassertsReverseVideoAfterEmbeddedReset
(internal/tui/style_test.go) covers the
reverseVideo fix directly.
menu_test.go/keymgmt_menu_test.go gained
TestRunMainMenu_ShowResourceListsDispatchesToItsOwnAction/
TestRunKeyMgmtMenu_ShowResourceListsDispatchesToItsOwnAction,
mirroring s3_menu_test.go’s existing
TestRunS3Menu_ShowResourceListsDispatches ToItsOwnAction –
neither menu’s “Show resource lists” dispatch had ever actually been
exercised by a test before (both testMenuActions/
testKeyMgmtActions helpers only wired
Refresh), a real coverage gap this phase closed along the
way.
Phase 20.14 — Chrome Consistency: Full-Height Rendering Fix + List-Tier Filtering (done)
Status: implemented and unit-tested 2026-07-10
(go build ./..., go vet ./...,
go test ./... -race, gofmt -l all clean).
Follow-up to Phase 20.13, from user-reported feedback after using the
newly converted List tier: (1) the List/Picker/file-manager boxes
weren’t using the full terminal height, and (2) List-tier filtering –
listed in DESIGN.md’s keybinding table (/ = Filter, “Menus,
pickers, lists, managers”) since Phase 20.8 but never built for lists –
was still missing.
Work Items
Tests: internal/tui/listview_test.go
gained TestListView_SlashEntersFilterModeAndNarrowsRows,
_FilterIsCaseInsensitive, _EscClearsFilter,
_LettersDuringFilterModeAreTextNotCommands – direct mirrors
of picker_test.go’s existing filter tests, minus selection
(List has nothing to choose).
TestListView_LegendShowsScrollAndQuit renamed to
_LegendShowsScrollFilterAndQuit and now also asserts
“filter” appears. All of picker_test.go’s existing filter
tests continued to pass unchanged against the refactored
filterState-backed PickerModel, confirming the
extraction didn’t change Picker’s own behavior.
Files:
internal/ui/display.go/display_test.go,
internal/tui/style.go/style_test.go,
internal/workflow/menu.go/ menu_test.go,
internal/workflow/keymgmt_menu.go/
keymgmt_menu_test.go, cmd/clasm/main.go.
Dependency: Phase 20.6
(bucketListViewConfig/DisplayBuckets, the
pattern every builder here copies) and Phase 20.12 (the Picker-tier
sweep that had to finish first).
DESIGN.md’s full termlib-to-huh/bubbletea conversion punch list
(Menu, Picker, and List tiers) is now completely converted. Remaining
termlib call sites outside this punch list
(e.g. ui.PickList/ui.Prompt/
Confirm calls not classified into any of the three tiers)
stay as-is per DESIGN.md’s own note that internal/ui
shrinks over the course of termlib removal rather than being replaced in
one step.
Phase 20.15 — Termlib Removal, Part 1: Foundational Helpers (done)
Status: implemented and unit-tested 2026-07-13
(go build ./..., go vet ./...,
go test ./... -race, gofmt -l all clean except
the pre-existing, unrelated version.go). Implements
DESIGN.md, “Removing termlib: Action Wizards and Output,” and
DECISIONS.md, “Remove termlib entirely: input via huh, output via
io.Writer.”
Landed differently from the original design in one real way, found
while implementing:
ui.Prompt/Confirm/ConfirmDestructive
don’t just need a testable core (the
RunXxx/runXxx split used everywhere else) –
they need their accessible-mode I/O exposed as a functional
option (ui.WithIO(input, output),
WithConfirmIO(input, output)), because these three are
called from dozens of call sites spread across both
internal/ui and internal/workflow, not from
one function with one obvious “core” to split.
ConfirmDestructive also changed shape slightly to make room
for the options param: mustMatch ...string became
mustMatch []string, opts ...ConfirmOption (Go allows only
one variadic per signature) – the ~6 call sites now wrap their arguments
in a slice literal instead of passing them bare. This option-based shape
is what let Phase 20.16 thread the same input/output pair through every
intermediate “leaf” prompt function (not just the handful anticipated
below) without a second, parallel mechanism.
Work Items
Tests:
prompt_test.go/confirm_test.go rewritten to
call the public
Prompt/Confirm/ConfirmDestructive
directly with WithIO/WithConfirmIO, matching
the accessible-mode pipe pattern already established for the Menu tier.
picklist_test.go deleted.
display_test.go/color_test.go updated for the
local constants, plus new
TestTruncate/TestPadRight parity tests (ported
from termlib’s own test cases) that weren’t there before.
progress_ticker_test.go updated to construct a
bytes.Buffer directly instead of
termlib.New(&buf), plus a new
TestFormatDuration (also ported from termlib’s own test
cases).
Files:
internal/ui/{prompt,picklist,color,display}.go (+ tests),
internal/workflow/{confirm,progress_ticker,domain_menu}.go
(+ tests).
Phase 20.16 — Termlib Removal, Part 2: Propagate Across Action Wizards (done)
Status: implemented and unit-tested 2026-07-13 (same
clean sweep as Phase 20.15 – the two landed together in one sitting,
since Go’s whole-module compilation meant Phase 20.15’s signature
changes and this phase’s propagation had to be internally consistent at
every commit boundary; see DESIGN.md’s “Sequencing” note). Propagated
le *termlib.LineEditor removal and
t *termlib.Terminal → io.Writer across every
remaining caller, then removed termlib from
go.mod.
The real scope turned out larger than the original work-item list
below:
ui.Prompt/Confirm/ConfirmDestructive’s
new WithIO/ WithConfirmIO options (Phase
20.15) meant every intermediate function between a workflow’s testable
core and its leaf prompt call also needed
input io.Reader, output io.Writer parameters threaded
through – promptKeyPairNameOrCreate,
promptSubnetID, promptSecurityGroupIDs,
promptIAMInstanceProfileOrCreate,
createInstanceProfileForRole,
createInstanceProfileInteractive,
promptCloudInitYAMLFile,
offerFstrimIfAvailable,
confirmLifecycleChange, promptOptionalDays,
promptPositiveDays,
removeLifecycleRule(ForRule),
startEC2Instance/stopEC2Instance,
terminateEC2Instance/ confirmTerminate,
removeAMI/confirmRemoveAMI,
deleteKeyPair/ confirmDeleteKeyPair,
runLaunch, createAMIFromInstance, and
createBucket/configureBucketWebsite (which
previously had no menu-tier huh.Select at all, so no
menuInput/menuOutput pair to reuse) – none of
these were anticipated in the original per-file list, found only by
tracing every Confirm(/ConfirmDestructive(/
ui.Prompt( call site after the mechanical sweep below and
checking whether its enclosing function had any way to reach it from a
test.
A second real gap, found only by actually running the test suite (not
by static analysis): huh’s accessible-mode input
(accessibility. PromptString, used by
Input.RunAccessible) has no way to surface EOF as
an error – it silently falls back to the field’s default (or
blank) forever, unlike termlib.LineEditor.Prompt, which
returned io.EOF once piped input ran out.
TestCreateInstanceProfileForRole_ NameCollisionRetries
relied on that old behavior (a fake that always errors, expecting the
retry loop to eventually surface an error once input was exhausted) and
hung indefinitely under the new behavior instead. Fixed by giving
fakeIAMClient the same
createInstanceProfile ErrOnce shape
fakeEC2Client already used for the analogous key-pair test,
and rewriting the test to expect a successful retry rather than an
eventual error – the more accurate reflection of real interactive
behavior anyway (an operator retrying a genuinely duplicate name would
keep being asked forever, not have the tool give up on their
behalf).
TestImportKeyPairStandalone_PromptLabelStaysShort (a
guard against a real, now-moot termlib bug –
LineEditor.Prompt’s raw-mode redraw math assumed a prompt
fit on one terminal row) was retired outright, same treatment as
ui.PickList’s own tests.
Work Items
Tests: every _test.go file that
constructed termlib.New(&buf) to capture output
rewritten to use a bytes.Buffer directly.
newPipeEditor (previously wrapping a real
os.Pipe + termlib. LineEditor) rewritten to
return a (io.Writer, io.Reader, *bytes.Buffer) trio backed
by newHuhAccessibleInput’s line-at-a-time reader, with the
writer and buffer deliberately the same value. Every test driving a
menu-tier huh.Select and one or more free-text prompts/confirms
in the same call had its two previously-independent input streams
(le for free text, a separate
newHuhAccessibleInput reader for the menu) merged into one
combined stream, in the exact order the production code actually reads
them – verified by running the suite, not just by tracing the code,
since a wrong merge order fails loudly (wrong field gets the wrong text)
rather than silently.
Files: all files listed above, plus each one’s
corresponding _test.go, plus
go.mod/go.sum.
Phase 20.17 — Chrome Standardization: Shared lipgloss Palette (done)
Status: implemented and verified 2026-07-13. Implements DESIGN.md, “Chrome Standardization: A Shared lipgloss Palette,” and DECISIONS.md, “Chrome standardization: one shared indigo accent via lipgloss.”
Work Items
Tests: internal/tui/box_test.go — the
four tests asserting exact/ literal border strings
(TestBottomBorder_MatchesInnerWidth,
TestDivider_MatchesInnerWidth,
TestTopBorder_TitleFitsWithinWidth,
TestSplitAndMergeDividers_JoinAtTheMiddleColumn) and
TestBoxLine_PadsToInnerWidthAndAddsBorders updated to
compare against StripANSI(got). New
internal/tui/theme_test.go confirms Theme() is
non-nil, Focused.Title’s foreground matches the shared
accentColor, and Blurred.Base’s border style
is lipgloss.HiddenBorder(). Full
go build ./..., go vet ./...,
go test ./... -race sweep green; gofmt -l .
clean except a pre-existing, unrelated version.go
(generated file).
Files:
internal/tui/{theme.go (new),theme_test.go (new),box.go, box_test.go},
internal/ui/prompt.go,
internal/workflow/{confirm, domain_menu,object_browser}.go,
go.mod/go.sum (lipgloss promoted
from indirect to direct via go mod tidy).
Dependency: None — additive styling, no signature changes.
Phase 20.18 — Progress Ticker: Real bubbletea Spinner (done)
Status: implemented and verified 2026-07-13.
Depended on Phase 20.17 for the shared accent color
(tui.SpinnerStyle()).
Work Items
Tests: progress_ticker_test.go’s two
async tests (TestStartProgressTicker_PrintsPeriodically,
TestStartProgressTicker_StopsCleanly) updated to drop the
removed interval argument and sleep in multiples of
DefaultSpinnerInterval instead of a caller-supplied fast
interval; TestFormatDuration unchanged. Full
go build ./..., go vet ./...,
go test ./... -race sweep green.
Files:
internal/workflow/progress_ticker.go (+ test),
internal/workflow/{create_ami_from_instance,show_cloud_init, backup_archive}.go,
internal/tui/theme.go.
Dependency: Phase 20.17 (soft).
Phase 20.19 — object_browser.go: Bucket Pre-flight onto pickBucket (done)
Status: implemented and verified 2026-07-13.
Work Items
Tests: No object_browser_test.go
existed before this change (bare huh.Select fields aren’t
unit-tested via the accessible-mode pipe path in this codebase the way
Menu-tier huh.Selects are – there was nothing to retire or
update). Full go build ./..., go vet ./...,
go test ./... -race sweep green.
Files:
internal/workflow/object_browser.go,
cmd/clasm/main.go.
Dependency: None.
Phase 20.20 — Backup Archive & Trim: Reorder Prompts (done)
Status: implemented and verified 2026-07-13. Implements DECISIONS.md, “Reorder Backup Archive & Trim’s prompts.”
Work Items
Tests: every existing
backup_archive_test.go test’s input string (four
\n-joined answers) reordered to match; assertions
unchanged.
Files:
internal/workflow/{backup_archive.go,backup_archive_test.go}.
Dependency: None.
Phase 20.21 — Backup Archive & Trim: Recall Instance/Directory (done)
Status: implemented and verified 2026-07-13. Implements DECISIONS.md, “Recall Backup Archive & Trim’s instance/directory choices per-instance.”
Work Items
Tests: new internal/state/state_test.go
(missing-file, malformed- YAML, save-then-load round-trip, overwrite,
DefaultPath); new internal/tui/picker_test.go
cases (InitialCursor positions the cursor; out-of-range
falls back to 0; Enter immediately selects the pre-positioned row); new
internal/workflow/backup_archive_test.go cases (recalled
directory takes priority over the Name-pattern rule; Save
is called with the right instance/directory; a Save error
is a warning, not fatal).
Files:
internal/state/{state.go,state_test.go} (new),
internal/tui/{picker.go,picker_test.go},
internal/workflow/ {power_state.go,backup_archive.go,backup_archive_test.go},
cmd/clasm/main.go.
Dependency: None.
Phase 20.22 — Contextual Description Text on Menu/Picker-tier Screens (done)
Status: implemented and verified 2026-07-13. Implements DECISIONS.md, “Contextual description text on Menu/Picker-tier screens.”
Work Items
Tests: new internal/tui/filter_test.go
(description costs two rows like a header; height never drops below the
floor); new TestPicker_DescriptionRendersBelowTopBorder in
picker_test.go. No new workflow-level tests – the
description strings are static text threaded through already-tested call
paths, not new branching logic.
Files:
internal/tui/{picker.go,filter.go,filter_test.go, picker_test.go,listview.go},
and every internal/workflow/*.go file containing a
Menu-tier huh.Select or one of the 9 Picker-tier
tui.PickerConfig constructor call sites (15 + 9 call sites
across ~20 files).
Dependency: None.
Phase 20.23 — huh Fields: Full Box Border to Match tui’s Chrome (done)
Status: implemented and verified 2026-07-13. Implements DECISIONS.md, “huh fields get a full box border to match tui’s chrome.”
Work Items
Tests: verified via a throwaway test rendering
Theme().Focused. Base.Render(...) directly with a forced
true-color profile and inspecting the raw ANSI output – confirms a full
┌─┐│ │└─┘ box in the shared accent, not huh’s default
left-bar. Not committed as a permanent test (a pure styling value, not
branching logic); full go build ./...,
go vet ./..., go test ./... -race sweep green
throughout.
Files: internal/tui/theme.go.
Dependency: None.
Phase 20.24 — Clear the Screen at Startup (done)
Status: implemented and verified 2026-07-13. Implements DECISIONS.md, “Clear the screen at startup.” Partially addresses a combined request (“clear the screen first and take up the full height of the terminal window”); the “full height” half was deliberately not implemented here – see the note below.
Work Items
Not done, deliberately deferred: making the domain
picker (or the Menu tier generally) visually fill the terminal’s full
height. The domain picker is a huh.Select (Menu tier),
which – unlike the Picker/List/Manager tier’s bubbletea components – has
no built-in concept of “occupy the full window height”; every other
Menu-tier screen in the app (S3/EC2/Key Management menus) is the same
compact, content-sized form, so making only the root domain picker
full-height would be visually inconsistent with every menu one level
deeper, the opposite of the consistency this session’s other chrome work
has been building toward. Revisit if this turns out to be what “full
height” specifically meant, once clarified.
Tests: internal/ui/clear_test.go
asserts the exact bytes written match
ansi.EraseEntireScreen + ansi.CursorHomePosition.
Files:
internal/ui/{clear.go,clear_test.go} (new),
cmd/clasm/main.go,
go.mod/go.sum.
Dependency: None.
Phase 20.25 — Bucket Picker for Backup Archive & Trim (done)
Status: implemented and verified 2026-07-13. Implements DECISIONS.md, “Bucket picker for Backup Archive & Trim.”
Work Items
Tests: three new cases in
backup_archive_test.go
(TestBackupArchiveAndTrim_BucketPickerOffersKnownBuckets,
..._BucketPickerOtherFallsBackToFreeText,
..._BucketPickerFallsBackToFreeTextOnListError), each
verifying the resulting bucket name via the upload command’s
s3://<bucket>/... destination. Every pre-existing
test in this file continues to pass unchanged – none populate
fakeS3Client.buckets, so ListBuckets returns
an empty list and they all naturally exercise the free-text fallback
branch, exactly as before this change.
Files:
internal/workflow/{backup_archive.go,backup_archive_test.go}.
Dependency: None.
Phase 20.26 — Full-height Menu Tier (done)
Status: implemented and unit-tested 2026-07-20
(go build ./..., go vet ./...,
go test ./... -race all clean; gofmt -l clean
except the pre-existing, unrelated version.go); not yet
verified against real AWS/a real terminal – see Phase 22 and the general
manual-verification gap every phase this session has carried. Resolves
Phase 20.24’s deferred “full height” request. Implements
DESIGN.md, “Full-height Menu Tier,” and
DECISIONS.md, “Full-height Menu tier via live
WindowSizeMsg tracking, applied at every depth.”
Work Items
Tests:
internal/workflow/domain_menu_height_test.go (new) –
TestQuitKeyGuard_WindowSizeMsgProducesFullTerminalHeight
asserts the combined on-screen output (hint line + form view) equals the
terminal height exactly, across several sizes;
TestQuitKeyGuard_ ShortContentStillFillsTheWindow confirms
a 2-option menu still renders padded close to full height at a 30-row
terminal, not shrunk to content;
TestQuitKeyGuard_TinyTerminalDoesNotPanic covers the
non-positive-height no-op path. Accessible-mode (pipe-tested) call sites
are unaffected – they never construct a tea.Program, so
this whole path doesn’t apply to them.
Files:
internal/workflow/domain_menu.go,
internal/workflow/domain_menu_height_test.go (new).
Dependency: None.
Phase 20.27 — Launch Templates (done)
Status: implemented and unit-tested 2026-07-20
(go build ./..., go vet ./...,
go test ./... -race all clean; gofmt -l clean
except the pre-existing, unrelated version.go); not yet
verified against real AWS – see Phase 22. v0.0.2’s headline feature,
confirmed directly 2026-07-20 (v0.0.1 is already piloting in production,
unreleased – no git tag yet). Folds in the IMDSv2 bug fix (TODO.md,
Bugs) as one design/ implementation pass, since both touch the same
MetadataOptions concept. Implements DESIGN.md,
“Launch Templates,” and DECISIONS.md, “Launch templates:
build directly from cloud-init YAML, diff-then-new- version sync, fold
in IMDSv2.” Deliberately excludes TODO.md’s tags-screen fix,
backup-bucket-default, and top-level cross-resource tag management –
those get their own design/decision/plan pass after this one lands.
Effort: ~24 hours estimated (comparable scope to
Phase 20.1: a new client surface, seven new interactive workflows, a
diff mechanism, plus three IMDSv2 call sites) Priority:
High Files:
internal/awsclient/ec2.go/logging_ec2.go (7
new EC2API methods + logging wrapper),
internal/inventory/launch_templates.go (new –
LaunchTemplate list-tier type, per-version detail type,
ListLaunchTemplates + version-detail fetch, aggregated
across regions like Image/ Instance),
internal/workflow/show_launch_template.go (new –
List/Show), internal/workflow/launch_template_create.go
(new – Create from Cloud-Init YAML),
internal/workflow/launch_from_template.go (new – Create EC2
Instance from Launch Template, naming to match the existing
launch_from_cloud_init.go convention),
internal/workflow/launch_template_sync.go (new –
Sync/diff/no-op detection),
internal/workflow/launch_template_manage.go (new – Promote
to Default, Delete Version(s), Delete Template),
internal/workflow/launch_execute.go (IMDSv2 on plain
RunInstances), internal/workflow/menu.go (7
new mainMenuItems entries + matching
MenuActions fields), cmd/clasm/main.go
(wiring), go.mod/go.sum
(github.com/aymanbagabas/go-udiff promoted from indirect to
direct – already present transitively via
charmbracelet/x/exp/teatest, used by
internal/filemanager’s own tests, so no new dependency is
actually being introduced).
Work Items
Tests: each new workflow gets an accessible-mode
pipe-tested core, matching every existing Menu-tier workflow’s
convention. launch_execute_test.go’s existing
fakeEC2Client embeds awsclient.EC2API, so
widening the interface doesn’t break any existing test; the new
launch-template methods get added to that same shared fake rather than a
second one. Specific cases to cover: Sync’s
identical-content-skips-a-version branch and different-content-shows-
a-diff-then-creates-a-version branch; the
plain-RunInstances and new-template
MetadataOptions both come back required in the
captured request; Delete Version(s)/Delete Template’s
Environment=production extra-warning gate.
Dependency: None (builds on the existing EC2 client/inventory/ Menu-tier conventions; does not depend on Phase 20.26).
Phase 20.28 — Launch Templates: Real-Usage Fixes and Version History
Status: implemented and unit-tested 2026-07-20
(go build ./..., go vet ./...,
go test ./... -race all clean; gofmt -l clean
except the pre-existing, unrelated version.go); not yet
re-verified against real AWS. Direct fallout from the operator’s first
real-AWS pass over Phase 20.27 (creating a template, launching from it,
syncing, promoting, listing, deleting) – one genuine bug found from the
debug log, three UX gaps from live use. Implements
DECISIONS.md, “Accept v-prefixed launch
template versions” and “Launch Template version history, scrollable
diffs, and split Show resource lists.”
Work Items
Tests:
TestShowLaunchTemplate_AcceptsVPrefixedVersion and
TestNormalizeVersionSelector reproduce the exact failure
from the debug log before fixing it;
TestDisplayDiff_AccessibleModeFallsBackToPlainDump covers
the test-mode branch;
TestShowLaunchTemplate_ListAllVersions/
_DiffTwoVersions/_DiffTwoVersions_IdenticalReportsNoDifference/
TestLaunchTemplateVersionRows cover the new sub-choices;
TestListLaunchTemplateVersions_* cover the new inventory
call; menu_test.go’s hardcoded menu-item indices and count
were updated for the reordering (matching the same maintenance every
prior menu-ordering change in this project has needed).
Files:
internal/workflow/show_launch_template.go,
internal/workflow/launch_template_manage.go,
internal/workflow/launch_template_sync.go,
internal/inventory/launch_templates.go,
internal/workflow/menu.go, cmd/clasm/main.go,
plus each file’s _test.go counterpart.
Dependency: Phase 20.27.
Phase 20.29 — Manage Tags: Loop Until ‘q’, Show Tags Choice
Status: implemented and unit-tested 2026-07-20
(go build ./..., go vet ./...,
go test ./... -race all clean; gofmt -l clean
except the pre-existing, unrelated version.go); not yet
re-verified against real AWS. Closes the TODO.md bug: “missing a ‘show
tags’ menu option and the tags shown at the top of the screen don’t
update on change.” Implements DECISIONS.md, “Manage Tags:
loop until ‘q’, always show current tags, add a Show tags choice.”
Work Items
Tests:
TestManageTags_AddOnInstance/_UpdateOnAMI/_RemoveOnInstance/
_EnvironmentNoteShown/_DeclinedConfirmationDoesNotApply/
_NoExistingTagsToUpdate/_RejectsBlankTagKeyOnAdd
now call applyOneTagChange directly (simpler, and sidesteps
the EOF gotcha entirely since there’s no loop involved).
TestManageTags_ShowTagsRedisplaysAndContinues and
TestManageTags_LoopRefreshesTagsAfterChange exercise the
actual loop via manageTagsForResource, using
cancelAfterNFetches to end it deterministically – the
latter is the actual bug-fix proof: it Adds a tag, then immediately
Updates it, which only succeeds if the loop refreshed tags from AWS in
between (via a new statefulTagsFakeEC2Client that, unlike
the shared fakeEC2Client, actually tracks tag state across
CreateTags/DescribeInstances calls).
Files:
internal/workflow/manage_tags.go,
internal/workflow/manage_tags_test.go.
Dependency: None.
Real-usage fix (2026-07-20)
Found during Phase 20.30’s real-terminal testing: Add/Update/Remove
all confirmed working (instances, AMIs, launch templates, key pairs),
but “Show tags” appeared to do nothing – the screen looked unchanged.
Root cause: the full-height “Choose an action” Select (Phase 20.26)
scrolls the separately-printed displayTags output out of
view the instant it renders, on every iteration – not specific to “Show
tags” itself, but most noticeable there since nothing else changes on
screen. Fixed by embedding the current tags directly in the Select’s own
Description (actionMenuDescription, tested),
so they’re guaranteed part of the same full-height chrome instead of
relying on scrollback. See DECISIONS.md, “Manage Tags: embed current
tags in the action Select’s own Description…”.
go build/vet/test -race/
gofmt all clean; confirmed against a real terminal
2026-07-20 for all four kinds (instance, AMI, launch template, key
pair).
Phase 20.30 — Tag Management Domain
Status: all five resource types (Instance/AMI/Launch
Template/Key Pair/S3 Bucket) implemented, unit-tested, and confirmed
against real AWS 2026-07-20 (go build ./...,
go vet ./..., go test ./... -race all clean;
gofmt -l clean except the pre-existing, unrelated
version.go; Manage Tags’ Add/Update/Remove/Show tags all
confirmed working at a real terminal for every kind, including S3
Bucket’s read-modify-write and the real-usage fix below). Phase 20.30 is
complete. Implements DESIGN.md, “Tag Management Domain
(Design Addendum, 2026-07-20)” and DECISIONS.md, “Tag
Management: a fourth domain, generalizing the Manage Tags loop across
five resource types” plus “Generalize applyOneTagChange for S3’s
read-modify-write tag semantics.” Closes the TODO.md requested feature:
“A top level menu item for managing tags across resources (EC2, AMI, S3,
etc).” Depended on Phase 20.29’s loop/manageTagsForResource
shape, reused as-is for the EC2-backed types and unchanged in shape for
S3 (only the apply closure it’s given differs).
Effort: Large (comparable to Phase 20.27, Launch Templates).
Priority: Requested feature, not a bug fix; no committed deadline.
Design
note: applyOneTagChange generalized only when S3 actually
needed it
The original plan (below) assumed applyOneTagChange
would need a pluggable apply closure from the start. In
practice, this phase shipped in two slices: the four EC2-backed kinds
first, then S3. ApplyTagChange (EC2’s
CreateTags/DeleteTags) already worked
unmodified for all four EC2-backed kinds – Launch Template and Key Pair
needed new fetch functions
(fetchLaunchTemplateTags/fetchKeyPairTags) and
new wiring, but not a new apply path – so the pluggable-apply-closure
generalization was deferred until S3 actually needed it, rather than
built speculatively up front. Rather than touch Compute’s existing
narrow ManageTags/manageTags (Instance/AMI
only) at all, a new, separate
ManageResourceTags/manageResourceTags (in the
new internal/workflow/tag_management.go) was added: it
picks a kind, then a resource, then hands off to
manageTagsForResource. Once the S3 slice began,
applyOneTagChange/manageTagsForResource were
generalized to take a tagApplyFunc closure instead of a
hardcoded awsclient.EC2API client (DECISIONS.md,
“Generalize applyOneTagChange for S3’s read-modify-write tag semantics”)
– EC2-backed callers now build
func(ctx, params) error { return ApplyTagChange(ctx, client, params) }
at their one remaining call site each, and S3 builds
applyBucketTagChange (bucket_tags.go)
instead.
Work Items (EC2-backed types)
Tests (EC2-backed types)
TestXFromSDK_CarriesFullTagMapper inventory type (Instance/Image/ LaunchTemplate/KeyPair), plus updated aggregate tests asserting the fullTagsmap viareflect.DeepEqual.TestFetchLaunchTemplateTags/TestFetchKeyPairTags(+_NotFoundvariants), matchingTestFetchInstanceTags/TestFetchImageTags’s shape.TestManageResourceTags_No{Instances,AMIs,LaunchTemplates,KeyPairs}Found– the only paths reachable via pipe input before a real Picker-tier call, matchingmanageTags’s own accepted untested-dispatch limitation.TestFlattenTags_*/TestTagsListViewConfig_*(internal/ui) andTest{Instance,Image,LaunchTemplate,KeyPair}TaggedResources(internal/workflow) – pure data-transform coverage for “Show all tags”, without drivingtui.RunListView’s interactive loop.tagmgmt_menu_test.go, a full mirror ofkeymgmt_menu_test.go’s suite (dispatch, refresh-after-action, error survival, clean exit on cancelled ctx/interrupt/EOF, no stray “Back to domain picker” entry).domain_menu_test.go:TestRunDomainPicker_DispatchesToTagManagement, andTestDomainItems_NoExitEntryupdated from 3 to 4 domains.
Files (EC2-backed types):
internal/inventory/{instances,images,launch_templates,keypairs}.go
and their _test.go files,
internal/workflow/manage_tags.go,
internal/workflow/tag_management.go (new),
internal/workflow/tag_management_test.go (new),
internal/workflow/tagmgmt_menu.go (new),
internal/workflow/tagmgmt_menu_test.go (new),
internal/workflow/domain_menu.go +
domain_menu_test.go,
internal/workflow/launch_execute_test.go (shared fake
gained DescribeLaunchTemplates),
internal/ui/display.go + display_test.go,
cmd/clasm/main.go.
Work Items (S3 Bucket)
Tests (S3 Bucket)
TestFetchBucketTags/_NoSuchTagSetIsEmptyNotError/_PropagatesOtherErrors(bucket_tags_test.go).- A new
statefulTagsFakeS3Client(mirroringstatefulTagsFakeEC2Client), proving:TestApplyBucketTagChange_Add/_Update;TestApplyBucketTagChange_RemoveLastTagUsesDeleteBucketTagging(the direct proof for theDeleteBucketTaggingchoice – assertsPutBucketTaggingis not called with an empty set);_RemoveOneOfSeveralTagsUsesPutBucketTagging(the non-empty-result case still usesPutBucketTagging);_AddThenUpdateRoundTrips(the actual read-modify-write proof, same role asTestManageTags_LoopRefreshesTagsAfterChangeplays for EC2). TestManageResourceTags_NoBucketsFound,TestBucketTaggedResources/_PropagatesClientError(tag_management_test.go), using the sharedfakeS3Client/newRegionS3Clienttest helpers.- Full existing EC2-backed test suite re-run unchanged after the
tagApplyFuncgeneralization (regression coverage; no existing test needed to change behaviorally, only its direct calls toapplyOneTagChange/manageTagsForResourcenow wrap the fake via a newec2Apply(client)test helper).
Files (S3 Bucket):
internal/awsclient/s3.go + logging_s3.go
(DeleteBucketTagging),
internal/workflow/manage_tags.go (tagApplyFunc
generalization), internal/workflow/manage_tags_test.go
(ec2Apply helper),
internal/workflow/bucket_tags.go (new),
internal/workflow/bucket_tags_test.go (new),
internal/workflow/tag_management.go +
tag_management_test.go,
internal/workflow/bucket_fakes_test.go +
backup_verify_test.go (shared fakeS3Client
gained DeleteBucketTagging),
internal/workflow/tagmgmt_menu.go,
cmd/clasm/main.go.
Dependency: Phase 20.29 (Manage Tags
loop/manageTagsForResource).
Phase 21 — CloudFront Domain
Status: someday/maybe – not on the active roadmap, no
committed timeline (revised 2026-07-09 from “postponed to a later
version,” see DECISIONS.md). No code written; the
CloudFront domain-picker entry was removed rather than left
wired to NotYetImplemented, so the 0.0.1 UI doesn’t expose
a menu item that goes nowhere. The design below stays valid reference
for if this is ever picked back up, but it is not queued as “next”
behind anything currently planned (Phase 20.1 is the active next-release
work).
Effort: ~8 hours (implementation) + real-AWS
verification, now folded into this phase’s own scope rather than Phase
22’s – see below Priority: Deferred (someday/maybe)
Files: internal/awsclient/cloudfront.go,
internal/inventory/distributions.go,
internal/workflow/{distribution_create,distribution_invalidate}.go
Implements DESIGN.md Features 22-25.
Work Items
CloudFrontAPIinterface:ListDistributions,GetDistribution,CreateDistribution,CreateOriginAccessControl,CreateInvalidation,GetInvalidation- Single
us-east-1client construction — no per-region fan-out; CloudFront’s control plane is global (seeDESIGN.md, “Navigation: Domain Picker”) ListDistributions(ctx)(internal/inventory)- Show Distribution Detail: read-only,
cloudfront:GetDistribution - Create Distribution: pick or create a bucket (hands off to Phase
20’s Create Bucket), create or reuse an Origin Access Control for that
bucket, prompt default root object + optional alternate domain name(s),
confirm (billable-infrastructure notice, plain confirm not
type-to-confirm),
cloudfront:CreateDistribution, then update the bucket policy scoped to this distribution’s ARN (s3:PutBucketPolicy), then poll (unbounded) untilDeployed - Invalidate Cache Paths: pick a distribution, prompt path pattern(s)
(default
/*), confirm,cloudfront:CreateInvalidation, poll untilCompleted - Wire into the domain picker from Phase 18
- Real-AWS verification for this domain (create a distribution for a real test bucket, verify it actually serves content, invalidate, confirm the cache refreshes) – moved here from Phase 22 (see below) now that CloudFront is someday/maybe rather than queued as “next”; Phase 22 no longer needs to wait on this phase to close out
Tests: fakes for each new CloudFront call;
OAC-then-bucket-policy sequencing;
poll-until-Deployed/Completed with bounded
test timeouts
Dependency: Phase 18, Phase 20 (Create Distribution hands off to Create Bucket)
Phase 22 — Real-AWS Testing: Key Management, S3
Effort: ~6 hours Priority: High
Files: TEST_PLAN_REAL_AWS.txt (extended
with new sections)
Mirrors Phase 16’s manual-verification approach, extended to Key
Management and S3. Independent of Phase 16/17 (Compute’s own
verification and Bash retirement) — see DECISIONS.md,
2026-07-02. No longer covers CloudFront (see DECISIONS.md,
“Demote CloudFront to someday/maybe…”) — that verification now lives in
Phase 21 itself, whenever it’s picked up, so this phase isn’t blocked on
a someday/maybe item.
Work Items
- Extend
TEST_PLAN_REAL_AWS.txtwith sections for Key Management (create/import/delete against real AWS, all four regions) and S3 (create bucket, configure website hosting, sync a small test site, browse/delete objects) - Run manually against the real AWS account, same
[ok]-marker convention as Phase 16 - Update
TEST_PLAN_REAL_AWS.txtif the Go CLI’s exact prompts/flow differ from what’s documented
Dependency: Phase 19, 20
Phase 20.31 — Configurable EBS Root Volume Size
Status: implemented and unit-tested 2026-07-21
(go build ./..., go vet ./...,
go test ./... -timeout 60s all clean; gofmt -l
clean). Implements DESIGN.md, “Configurable EBS Root Volume Size” and
DECISIONS.md, “Configurable EBS root volume size: scope, flow coverage,
and resize automation depth.” Closes TODO.md’s “Bug (confirmed in
production use, 2026-07-22)” entry. Built test-first throughout: every
new behavior (the missing BlockDeviceMappings, each
growRootFilesystem fallback branch, the menu dispatch) has
a test that failed before the corresponding code existed. Not yet
verified against real AWS.
Work Items
Part 1 — set root volume size at creation:
Part 2 — resize an already-running instance’s root volume:
Tests: test-first throughout –
TestLaunch_SetsRootVolumeSize/
TestBuildRequestLaunchTemplateData_SetsRootVolumeSize
reproduced the “no BlockDeviceMappings sent” gap (confirmed
failing before the fix, matching this project’s established convention)
before the fix;
TestCollectLaunchInstanceParams_SetsRootVolumeSize/
TestCollectLaunchInstanceParamsFromCloudInit_SetsRootVolumeSize
cover the prompt end-to-end;
TestDescribeLaunchTemplateVersion_DecodesRootVolumeSize/
TestShowLaunchTemplate_DisplaysRootVolumeSize cover the
display side;
TestRootVolumeInfo_*/TestPromptNewVolumeSizeGB_*/
TestModifyVolumeSize_*/TestWaitUntilVolumeModificationUsable_*/
TestResizeInstanceRootVolume_* cover Part 2’s own workflow;
TestSplitDiskAndPartition_*/TestParseFindmntOutput_*/
TestRootFilesystemGrowCommand_*/TestGrowRootFilesystem_*
cover the SSM automation, including every fallback branch. One real bug
caught during this phase:
growRootFilesystem/resizeInstanceRootVolume
initially hardcoded production SSM timeouts (2 min/10 min), making their
own “SSM not online” tests actually wait out those real timeouts – fixed
by threading onlineTimeout/commandTimeout/
pollInterval through explicitly
(growRootFilesystem) and by configuring the fake SSM client
to resolve immediately (resizeInstanceRootVolume’s own
test), matching checkCloudInitCompletion’s existing
pattern. menu_test.go’s hardcoded menu-item count (20 ->
21) was updated for the new entry, same maintenance cost every prior
menu-ordering change in this project has needed.
Files:
internal/workflow/launch_instance.go,
internal/workflow/launch_from_cloud_init.go,
internal/workflow/launch_execute.go,
internal/workflow/launch_template_create.go,
internal/inventory/launch_templates.go,
internal/workflow/show_launch_template.go,
internal/awsclient/ec2.go,
internal/awsclient/logging_ec2.go,
internal/workflow/menu.go, cmd/clasm/main.go,
new internal/workflow/ebs_size.go,
internal/workflow/resize_volume.go,
internal/workflow/ssm_grow.go, plus each file’s
_test.go counterpart.
Dependency: Phase 20.27 (Launch Templates).
Phase 20.32 — Pause for Acknowledgment Before Every Menu-Loop Redraw
Status: revised and re-implemented 2026-07-22,
superseding this same phase’s first pass from earlier the same day.
First pass (error/ refresh-error prints in all four menu loops, plus a
one-off pause inside resizeInstanceRootVolume) was
implemented, unit-tested, and live-verified working for its own two
original incidents – then live testing found a third incident
the first pass didn’t cover (runLaunch’s cloud-init-error
report, which returns nil, not an error, so the error-path-only pause
never fired). See DECISIONS.md, “Widen ‘pause for acknowledgment’ to
every action, not just errors.” Re-implemented per that revision:
go build ./..., go vet ./...,
go test ./... -race all clean; gofmt -l clean.
Not yet re-verified live against real AWS a second time, or
released.
Work Items
Tests: test-first, per this project’s standing
convention – every per-domain test that dispatches more than one action
in sequence (*_DispatchesToTheChosenAction,
*_ShowXDispatchesToItsOwnAction,
*_RefreshesAfterASuccessfulAction,
*_ActionErrorDoesNotCrashLoop) needed a blank input line
inserted after every dispatch now, not just the error ones,
since the pause is no longer conditional on err; the four
*_PausesForAcknowledgmentAfterARefreshError tests added in
the first pass still hold (a refresh error is itself dispatched via the
same unconditional call site); removed the pause assertion from
TestResizeInstanceRootVolume_HappyPath since that function
no longer pauses itself. All widened tests confirmed failing against the
first-pass code before the loop-placement change.
Files: internal/workflow/menu.go,
internal/workflow/s3_menu.go,
internal/workflow/keymgmt_menu.go,
internal/workflow/tagmgmt_menu.go,
internal/workflow/resize_volume.go, plus each file’s
_test.go counterpart.
Dependency: none (independent bugfix; targeted for v0.0.4).
Phase 20.33 — SSM-Capable Instance Profile Enforcement + Retrofit
Status: designed 2026-07-22 (DESIGN.md, “SSM-Capable Instance Profile Enforcement + Retrofit”; DECISIONS.md, same title). Targeted for v0.0.5, alongside the not-yet-started arm64/Graviton + Ubuntu 26.04 LTS work. Not yet implemented.
Work Items
Part 1 — SSM-capability verification helper: done, 2026-07-22.
Part 2 — Enforcement at launch: done, 2026-07-22, revised same day after live testing.
Tests (Part 2): test-first for the two builder
functions –
buildInstanceProfileChoices/buildRoleChoices
are independent of the Picker-tier UI (same “testable core” split as
every other Picker-tier conversion in this project) and got direct unit
tests covering the no-profiles/no-none-entry case, filtering out
non-capable entries (confirmed failing against the pre-revision
annotate-only code first), and IAM-error propagation. A new
TestCreateInstanceProfileInteractive_NoSSMCapableRolesReturnsWithoutError
covers the empty-after-filter case. Two more tests
(TestPromptIAMInstanceProfileOrCreate_PropagatesSSMCheckError,
TestCreateInstanceProfileInteractive_PropagatesSSMCheckError)
confirm the SSM-check runs before the (untestable) picker call,
so an IAM error there is still reachable and testable without touching
the UI. All existing tests (including the free-text-fallback and
no-roles-found paths) still pass unchanged.
go build/vet/test -race/gofmt
all clean.
Part 3 — Retrofit (general-purpose associate/replace): done, 2026-07-22.
Status: Phase 20.33 (all three parts) fully implemented,
unit-tested, and real-AWS-verified 2026-07-22.
go build/go vet/go test ./... -race/gofmt -l
all clean. Verified live: created ec2-granian-test-role
(EC2 trust policy + AmazonSSMManagedInstanceCore attached)
via AWS CLI since the account had no other general-purpose SSM-capable
role; Part 2’s filtered picker correctly showed only SSM-capable
profiles/roles (confirmed both before and after the new role existed);
launched test-clasm-granian successfully using it,
alongside Phase 20.34’s gzip fix (below) in the same
launch-template-from-cloud-init flow. Not yet released – targeted for
v0.0.5 alongside the not-yet-started arm64/Graviton + Ubuntu 26.04 LTS
work.
Tests: test-first throughout, per this project’s
standing convention – every new test confirmed failing (compile error,
the Go equivalent of a failing test) before its corresponding
implementation existed. Fixture-driven unit tests for
roleHasSSMPermissions/
instanceProfileIsSSMCapable against a fake IAM client (Part
1, no real AWS round-trip);
buildInstanceProfileChoices/buildRoleChoices
tests covering the no-profiles-still-offers-create-new case,
SSM-capability annotation, and IAM-error propagation, plus two
integration tests confirming the SSM-check runs before the
(untestable) Picker-tier call so an IAM error there is still reachable
without touching the UI (Part 2);
associate_instance_profile.go tests covering both the
associate-path and replace-path branches, resolve-error propagation,
associate-error propagation, and the missing-region-client case (fake
EC2 client, DescribeIamInstanceProfileAssociations
returning empty vs. non-empty) (Part 3).
Files: internal/awsclient/iam.go,
internal/awsclient/logging_iam.go,
internal/awsclient/ec2.go,
internal/awsclient/logging_ec2.go,
internal/workflow/create_instance_profile.go, new
internal/workflow/ssm_iam_check.go, new
internal/workflow/associate_instance_profile.go,
internal/workflow/menu.go, cmd/clasm/main.go,
plus each file’s _test.go counterpart.
Dependency: none (independent of the not-yet-started arm64/Graviton phase, though both target v0.0.5).
Phase 20.34 — Gzip-Compress User-Data Before Base64-Encoding
Status: designed, implemented, and real-AWS-verified
2026-07-22, live-testing-driven (see DECISIONS.md, “Always
gzip-compress user-data before base64-encoding it”). Closes
InvalidUserData.Malformed: User data is limited to 16384 bytes,
hit creating a launch template from
invenio-rdm-13-granian-init.yaml (16976 raw bytes, already
over the limit). Verified live: test-clasm-granian launch
template created successfully and an instance launched from it. Targeted
for v0.0.5.
Work Items
Tests: test-first –
encodeUserData/decodeUserData round-trip tests
(encode then decode returns the original text), a
decode-plain-base64-without-gzip-magic test (backward compatibility with
every resource created before this change), and decode-error tests for
malformed base64/corrupt gzip. All new tests confirmed failing
(undefined function, the Go equivalent of a failing test) before the
helpers existed. Existing tests at every read/write call site still pass
unchanged, since encode-then-decode round-trips to the same plain text.
go build/vet/test -race/gofmt
all clean. Manually confirmed the real 16976-byte granian file gzips to
5628 bytes via plain gzip -c | wc -c, not assumed.
Files: new
internal/workflow/userdata_gzip.go,
internal/workflow/launch_execute.go,
internal/workflow/launch_template_create.go,
internal/workflow/launch_template_sync.go,
internal/workflow/cloud_init_instance.go,
internal/workflow/show_launch_template.go, plus each file’s
_test.go counterpart.
Dependency: none.
Phase 20.35 — ARM64 (Graviton) Support + Ubuntu 26.04 LTS
Status: designed, implemented, and real-AWS-verified
2026-07-22 (DESIGN.md, “ARM64 (Graviton) Support + Ubuntu 26.04
LTS”; DECISIONS.md, “ARM64/Ubuntu 26.04: filter the instance-type list
by AMI architecture, no new pre-flight check”). Targeted for v0.0.5
alongside Phase 20.33/20.34 (also real-AWS-verified).
go build/go vet/go test ./... -race/gofmt -l
all clean. All three Phase 20.33/20.34/20.35 work targeted for v0.0.5 is
now implemented, unit-tested, and real-AWS-verified. Not yet
released.
Work Items
Tests: test-first, per this project’s standing
convention, every new test confirmed failing (compile error – undefined
field/wrong arg count, the Go equivalent of a failing test) before its
implementation existed.
imageFromSDK/listOfficialUbuntuAMIsInRegion
architecture-population tests; a curatedUbuntuReleases test
asserting every expected arm64/26.04 name pattern is present;
promptInstanceType filtering tests (arm64-only list when
filtered to arm64, x86_64-only when filtered to x86_64, “Other” always
present even when filtered, unfiltered when arch == "",
including the two existing “Other” free-text tests whose numeric index
shifted from 12 to 21 once the Graviton entries were appended); two
integration tests
(TestCollectLaunchInstanceParams_FiltersInstanceTypeByImageArchitecture,
the cloud-init path’s own mirror of it) confirming both collection
functions actually thread the picked AMI’s Architecture
through to the filter, not just that the filter works in isolation. All
prior existing tests (including the five original
promptInstanceType tests using positions 1/4/10, unaffected
by appending rather than interleaving) still pass unchanged.
Files: internal/inventory/images.go,
internal/workflow/official_ubuntu_amis.go,
internal/workflow/launch_prompts.go,
internal/workflow/launch_instance.go,
internal/workflow/launch_from_cloud_init.go, plus each
file’s _test.go counterpart.
Dependency: none.
Phase 20.36 — IAM Domain: Discovery, Categorization, and the Read-Only Guard
Status: designed 2026-07-23, implemented 2026-07-23, real-bug
found and fixed via live testing 2026-07-23 (DESIGN.md, “IAM
Profile & Role Management Domain”; DECISIONS.md, “IAM Profile &
Role Management: Origin tag revision…” and “Real bug:
ListRoles/ListInstanceProfiles/ ListPolicies don’t return tags inline”).
Targeted for v0.0.5, bundled alongside Phases 20.33-20.35 (already
implemented) rather than deferred to v0.0.6.
go build/go vet/go test ./... -race/gofmt -l
all clean. The user tagged a real role (air-sampling,
Origin=DLD in their own casing) and found Show Roles still
reported it (unset) – root-caused to a wrong assumption in
the original design (below), not the config layer (verified separately
and independently correct).
Work Items
Dependency: none. (The previously-planned “Tag as
DLD-owned” menu action is dropped — see DECISIONS.md, Decision 3 of the
revision entry — so this phase no longer depends on Phase 20.37 for a
dedicated action; it still shares Phase 20.37’s tag-write path for
editing Origin like any other tag.)
Files: internal/config/config.go,
internal/awsclient/iam.go,
internal/awsclient/logging_iam.go,
internal/workflow/create_instance_profile_test.go (shared
fakeIAMClient gained
ListPolicies/ListRoleTags/
ListInstanceProfileTags/ListPolicyTags stubs),
new internal/inventory/iam.go, new
internal/ui/iam_display.go, new
internal/workflow/iam_domain.go, new
internal/workflow/iam_menu.go,
internal/workflow/domain_menu.go,
cmd/clasm/main.go, plus each new file’s
_test.go counterpart.
Status update, same day: live-testing bug
(tags-not-inline, above) found and fixed after the initial
implementation; go build/go vet/
go test ./... -race/gofmt -l re-confirmed
clean after the fix. Real-AWS-verified 2026-07-23 –
user confirmed air-sampling now shows dld in
the Origin column after rebuilding, and ran all six manual test items
suggested after Phase 20.36’s initial implementation (config end-to-end
with a non-default key/value, Show Policies scoped to customer-managed
only, SSM-capable column accuracy, sort order,
filter/back-navigation/fresh-refetch, regression check on the other four
domains) – all passed.
Second real bug found via live testing, same day, while
testing Phase 20.37: the IAM Role picker took several seconds to
open in an account with dozens of roles –
ListIAMRoleSummaries/ListIAMInstanceProfileSummaries/
ListIAMPolicySummaries fetched each resource’s tags one at
a time, sequentially. Fixed by parallelizing with a bounded worker pool
(new fetchTagsConcurrently,
iamTagFetchConcurrency = 10), mirroring
inventory.ListImages’ own concurrent per-region fan-out.
See DECISIONS.md, “Parallelize per-resource IAM tag fetches.” All
existing correctness tests kept green; go test -race
confirms no data race. Real-AWS-verified 2026-07-23 –
user rebuilt and confirmed the IAM Role picker opens noticeably faster.
Not yet released.
Third real bug found via live testing, same day, while
testing Phase 20.40’s Delete Role: three real test roles were silently
missing from the Delete Role picker even though they were
correctly tagged origin=dld (confirmed via
aws iam list-role-tags). Root-caused via
aws iam list-roles --no-paginate (the AWS CLI
auto-paginates by default, which had been masking this): the account has
121 roles, IAM truncates ListRoles at 100 per page, and
ListIAMRoleSummaries made a single, un-paginated call with
no IsTruncated/Marker handling at all – any
account over 100 roles was silently missing some, with no error.
ListIAMInstanceProfileSummaries/ListIAMPolicySummaries
had the same gap, not yet triggered only because this account’s instance
profiles (4) and local policies (59) both stay under IAM’s page size.
Fixed by adding
listAllRoles/listAllInstanceProfiles/
listAllPolicies (internal/inventory/iam.go),
each paging via Marker until IsTruncated is
false, and switching all three ListIAM*Summaries functions
to use them. See DECISIONS.md, “Real bug:
ListRoles/ListInstanceProfiles/ListPolicies silently truncate past 100
items.” Test-first: a
TestListIAM*Summaries_PaginatesAcrossMultiplePages test per
kind, each confirmed failing (only the first page’s item returned)
before the fix.
go build/go vet/go test ./... -race/
gofmt -l all clean after the fix.
Real-AWS-verified 2026-07-23, immediately surfacing a
follow-on real bug: fetching all 121 (correctly-paginated) roles’ tags
at concurrency 10 reliably throttled
(Throttling: Rate exceeded), reproduced twice in a
row. Fixed by lowering iamTagFetchConcurrency from
10 to 4 and raising awsclient.NewIAMClient’s retry ceiling
to 8 attempts (config.WithRetryMaxAttempts(8),
IAM-client-only, since its N+1-calls-per-list shape is what makes it
uniquely prone to this). Re-verified against real AWS after both
changes: all 121 roles fetched, all 5 DLD-owned roles correctly
recognized (including the three test roles that prompted this
investigation), no throttling.
Phase 20.37 — Tag Management Domain Extension for IAM Resources
Status: designed 2026-07-23, implemented and unit-tested
2026-07-23 (DESIGN.md, “IAM Profile & Role Management
Domain”). Targeted for v0.0.5.
go build/go vet/go test ./... -race/gofmt -l
all clean. Not yet real-AWS-verified or released.
Work Items
Tests: test-first throughout – fetch/apply closures
tested against the shared fakeIAMClient (extended with
configurable per-name/ARN tag maps and last-input capture for Tag/Untag
calls); iamXxxTaggedResources conversions and three new “No
IAM X found” early-return paths tested directly, matching the existing
five kinds’ test shape.
Files: internal/awsclient/iam.go,
internal/awsclient/logging_iam.go,
internal/workflow/create_instance_profile_test.go (shared
fakeIAMClient gained configurable Tag/Untag fields), new
internal/workflow/iam_tags.go,
internal/inventory/iam.go (Tags field),
internal/workflow/tag_management.go,
cmd/clasm/main.go, plus each changed file’s
_test.go counterpart.
Dependency: Phase 20.36 (shares the IAM domain’s
resource-listing plumbing and the origin_tag config, though
the tagging mechanism itself is never gated by Phase 20.36’s read-only
check).
Phase 20.38 — IAM Detail View
Status: designed and implemented 2026-07-23
(DESIGN.md, “IAM Profile & Role Management Domain”). Targeted for
v0.0.5. go build/go vet/
go test ./... -race/gofmt -l all clean. Two
scope questions DESIGN.md left open were resolved by the user before
implementation: policy documents render as raw, pretty-printed JSON (not
a summarized rendering), and the “which running instance is associated”
half of the cross-reference is deferred – “which instance profiles
reference this role” (cheap, already-fetched data) ships now.
Real-AWS-verified 2026-07-23 – user confirmed View
Role Detail on air-sampling (trust policy, tags, all three
attached policies, the inline policy), drilling into both an attached
and the inline policy’s actual document, View Instance Profile Detail on
ec2-granian-test-role (correctly showing its one contained
role as SSM-capable, no tags), and clean q exit from the
policy-document drill-down back to the IAM menu. Not yet released.
Work Items
Tests: test-first throughout. Real bug
caught in the test suite itself, not production code:
runPolicyDocLoop’s tests initially relied on exhausted
accessible-mode input to end the loop after a single pick, which hung
indefinitely — huh’s accessible-mode Select has no way to
signal “input exhausted” as an error (the same documented gotcha as
manageTagsForResource’s own tests, Phase 20.29), so it
silently re-picks the default option forever rather than erroring. Fixed
by having the fake client’s relevant method (GetPolicy/
GetPolicyVersion/GetRolePolicy, via three
small wrapper types) cancel the context as a side effect, matching
domain_menu_test.go’s cancelingAction pattern
exactly. Caught before ever reaching the user by running tests with an
explicit -timeout 30s after the first hang.
Files: internal/awsclient/iam.go,
internal/awsclient/logging_iam.go,
internal/workflow/create_instance_profile_test.go (shared
fakeIAMClient gained
GetRole/GetInstanceProfile/ListRolePolicies/
GetRolePolicy/GetPolicy/GetPolicyVersion,
plus a PolicyName derived from ARN in
ListAttachedRolePolicies – a test-fixture gap, not a
production bug, since nothing needed the name before), new
internal/workflow/iam_detail.go,
internal/inventory/iam.go (RoleNames field),
internal/workflow/iam_menu.go,
cmd/clasm/main.go, plus each changed file’s
_test.go counterpart.
Dependency: Phase 20.36 (the detail view is reached from the discovery list).
Phase 20.39 — Curated Per-Use-Case Role/Policy Creation Templates
Status: designed and implemented 2026-07-23
(DESIGN.md, “IAM Profile & Role Management Domain”; DECISIONS.md,
“IAM Profile & Role Management: seven scoping decisions, bundled
into v0.0.5” and “…Origin tag revision…”). Targeted for v0.0.5.
go build/go vet/go test ./... -race/gofmt -l
all clean. Not yet real-AWS-verified or released. Reverses the
2026-07-02 “never creates a role, only attaches an existing one” scope
(DECISIONS.md, “Support picking or creating an IAM instance profile from
within awsops”) — deliberately, and only through curated templates.
Work Items
Tests: test-first throughout, including the full
guided flow (template pick → role name → params → confirm →
create/attach) driven end to end via accessible-mode pipe input –
auto-tagging when origin_tag.dld_value is set (and its
absence when unset), the SSM managed policy attaching alongside the
custom policy for RDM Repository, declined confirmation skipping
creation entirely, and CreateRole’s error propagating
before any AttachRolePolicy call is attempted.
Files: internal/awsclient/iam.go,
internal/awsclient/logging_iam.go,
internal/workflow/create_instance_profile_test.go (shared
fakeIAMClient gained
CreateRole/CreatePolicy/AttachRolePolicy),
new internal/workflow/iam_templates.go,
internal/workflow/iam_menu.go,
cmd/clasm/main.go, plus each changed file’s
_test.go counterpart.
Real-AWS testing in progress, 2026-07-23. RDM
Repository Instance confirmed fully – role created, tagged
origin=dld automatically,
AmazonSSMManagedInstanceCore + a correctly-scoped custom
policy both attached (s3:ListBucket on the bucket,
s3:GetObject/s3:PutObject on
<bucket>/*, nothing broader). Static Website
confirmed in read-only mode (bucket-only, no distribution).
Real bug (usability) found via that same live testing, fixed
same day: templates originally required hand-typed ARNs. Typing
a full S3 ARN forces an unnecessary mental reformatting step, and
CloudFront ARNs specifically can’t be found without leaving the tool
(Console or a separate CLI call) – breaking the interactive workflow.
Fixed by having each template param collect a plain name/ID (bucket
name, distribution ID, log group name, secret name) and having clasm
construct the ARN itself via a new BuildARN field, using
the account ID already resolved at startup
(sts:GetCallerIdentity) and the configured region. See
DECISIONS.md, “Phase 20.39 templates collect resource names/IDs, not
ARNs.” BuildPolicy functions themselves needed no changes –
only the collection step and prompt wording. Publish-mode Static Website
(with a distribution ID) and the thin templates (Bridge Service,
Patron-Facing, Data Processing) not yet real-AWS-verified.
Dependency: Phase 20.36 (origin_tag
config), Phase 20.37 (tag-write path used to auto-tag a newly-created
role, when configured).
Phase 20.40 — Delete Role, Attach/Detach Policy (CRUD Completion for DLD-Owned Roles)
Status: designed, implemented, and real-AWS-verified
2026-07-23 (DESIGN.md, “CRUD completion for DLD-owned roles”;
DECISIONS.md, “IAM Profile & Role Management: support CRUD for
DLD-owned roles”). Targeted for v0.0.5.
go build/go vet/go test ./... -race/gofmt -l
all clean. Motivated directly by live use: testing Phase 20.39’s
templates left behind test roles with no way to remove them from within
clasm.
Work Items
Tests: test-first for the core delete logic
(deleteIAMRole, deleteIAMPolicyIfUnused) —
every AWS-call failure point covered via subtests, plus
never-deletes-an-AWS-managed-policy and
leaves-the-dedicated-policy-if-used-elsewhere. Menu-tier flows
(*Confirmed cores) driven end to end via accessible-mode
pipe input: confirmed/declined/mismatched confirmation,
RequireDLDOwned blocking a non-DLD-owned role, each
AWS-call error propagating, and the
no-DLD-owned-roles/no-attached-policies early-return messages. The
Picker-tier entry points
(pickIAMRole/pickIAMPolicy) remain untested
directly, same accepted limitation as every other Picker-tier flow in
this domain (Phase 20.36/20.38/20.39’s own testable-core split).
Files: internal/awsclient/iam.go,
internal/awsclient/logging_iam.go,
internal/workflow/create_instance_profile_test.go (shared
fakeIAMClient gained
DeleteRole/DeleteRolePolicy/
DetachRolePolicy/DeletePolicy/DeletePolicyVersion/
ListPolicyVersions/ListEntitiesForPolicy), new
internal/workflow/iam_lifecycle.go,
internal/workflow/iam_menu.go,
cmd/clasm/main.go, plus each changed file’s
_test.go counterpart.
Dependency: Phase 20.36
(RequireDLDOwned, filterDLDOwnedRoles’
DLD-ownership data), Phase 20.38 (fetchIAMRoleDetail,
displayIAMRoleDetail,
IAMRoleDetail/IAMPolicyRef), Phase 20.39 (the
<role>-policy naming convention Delete Role’s cascade
relies on).
Real-AWS-verified 2026-07-23 – blocked initially by
the pagination/ throttling bugs above (both found and fixed the same
day, during this very verification attempt), then confirmed clean once
fixed: the user ran Delete Role against all three real test roles left
over from Phase 20.39’s template testing
(test-rdm-repo-role, test-static-site-role,
test-static-site-role-2). Independently confirmed via
aws iam get-role/get-policy afterward: all
three roles and their dedicated <role>-policy
policies are gone. This doubled as Delete Role’s real-AWS verification
and closed out the last open item from Phases 20.36-20.40. All
five IAM phases (20.36-20.40) are now implemented, unit-tested, and
real-AWS-verified.
Phase 20.41 — Instance/AMI Detail Views
Status: designed, implemented, and unit-tested 2026-07-24,
targeted for v0.0.5 (DESIGN.md, “Instance/AMI Detail Views”;
DECISIONS.md, “Instance/AMI Detail Views: on-demand describe calls,
appended menu placement”). Closes the outstanding TODO.md item left over
since Launch Templates gained its own detail view.
go build/go vet/go test ./... -race/gofmt -l
all clean. Not yet real-AWS-verified.
Work Items
Tests
Test-first throughout, confirmed failing (undefined symbols) before
each implementation existed, per this project’s convention
([[feedback-test-before-fix]] applies to new-feature work the same way
it applies to bug fixes).
DescribeInstanceDetail/DescribeImageDetail
covered via a fake EC2API client: found case (full field
mapping, including empty-tags/no-public-IP/no-IAM-profile edge cases)
and not-found/client-error cases.
showInstanceDetail/showAMIDetail testable
cores covered directly against the shared internal/workflow
fakeEC2Client (extended with a handful of new fields –
instanceDetail*/describeImages{Name,CreationDate,Architecture, EnaSupport}
– to synthesize the fuller field set this view needed; no existing
test’s behavior changed) for: full curated-field display,
no-volumes/no-block-device-mappings, unknown-region error, and each AWS
call’s error propagating.
ShowInstanceDetail/ShowAMIDetail’s
no-resources-found early return covered directly (no Picker-tier driving
needed, matching showLaunchTemplate’s own accepted
limitation).
Files
internal/inventory/instances.go (or new
instance_detail.go),
internal/inventory/images.go (or new
image_detail.go), new
internal/workflow/show_instance_detail.go,
internal/workflow/show_ami_detail.go,
internal/workflow/menu.go, cmd/clasm/main.go,
plus each new/changed file’s _test.go counterpart.
Dependency
Existing pickInstance/pickImage
(Picker-tier), GatherVolumeInfo
(internal/workflow/volume_info.go),
displayOrNone.
Phase 20.42 — Configure clasm Domain
Status: designed, implemented, and unit-tested 2026-07-24,
targeted for v0.0.5 (DESIGN.md, “Configure clasm Domain”;
DECISIONS.md, “Configure clasm domain: explicit Save, region changes
deferred to next launch”).
go build/go vet/go test ./... -race/gofmt -l
all clean. Not yet real-AWS-verified (this domain makes no AWS calls at
all, so “verified” here just means a live manual walkthrough).
Work Items
Tests
Test-first throughout, confirmed failing (undefined symbols) before
each implementation existed. config.Save covered directly:
round-trips a Config through Save then
Load, confirms 0o644 permissions, confirms an
unwritable path errors cleanly. editRegions/
editBackupDirectoryRules/editOriginTag/displayConfig/
warnIfDirtyOnQuit each covered directly and independently
(add, remove, blank-entry-skipped, no-entries-to-remove,
unchanged-values-not-marked- dirty, blank-key-falls-back-to-default).
runConfigureMenu’s dispatch loop covered the same way
runTagMgmtMenu already is: dispatch-to- chosen-action, Save
dispatches to its own action, Refresh runs once after a successful
action, a single action’s error doesn’t crash the loop,
pause-for-acknowledgment after a successful action – every case
terminates via ctx cancellation from within a test-supplied action
closure, never by letting scripted input run out.
Real bug found via review before real-AWS/live testing even
started, fixed same day:
editRegions/editBackupDirectoryRules
originally called
displayRegionsList/displayBackupDirectoryRulesList
(a plain print) immediately before their own action-menu
pickString – the same bug class as Phase 20.29’s “Show tags
appeared to do nothing” (a full-height Menu-tier Select
fills the whole terminal on every render and scrolls whatever was
printed just before it out of view, so the current region/rule list was
invisible in real terminal use even though it was always being
(re)computed correctly). Fixed the same way Phase 20.29 was: embed the
current list in the Select’s own Description
(regionsEditDescription/backupDirectoryRulesEditDescription)
in addition to (not instead of) the existing plain print, since
accessible mode never renders a field’s Description, only Title and
options – this means the fix itself is invisible to the existing
pipe-driven tests, same accepted limitation as
manage_tags.go’s own actionMenuDescription
fix. General lesson reinforced a third time in this project: any call
site that prints plain text immediately before a full-height Menu-tier
Select is at risk of this same silent-scroll bug – worth checking for it
by default in any new Menu-tier loop, not just after a live report.
Files
internal/config/config.go, new
internal/workflow/configure_menu.go,
internal/workflow/domain_menu.go,
cmd/clasm/main.go, plus each new/changed file’s
_test.go counterpart.
Dependency
Existing
config.Load/config.Config/config.BackupDirectoryFor,
ui.Prompt/ui.WithDefault, this project’s
Menu-tier pickString/ pickComparable
primitives, tagmgmt_menu.go‘s loop-until-’q’ shape as the
direct structural model.
Phase 20.43 — Menu Reordering and Terminology Cleanup
Status: designed, implemented, and unit-tested
2026-07-24 (see MENU_REVIEW.md for the full audit;
DECISIONS.md, “Regroup the Compute menu, and a terminology cleanup
across every domain menu”). go build/
go vet/go test ./... -race/gofmt -l
all clean. No AWS-facing behavior changed – this is entirely menu-item
order and label text.
Work Items
Tests
No new test files – every change here is a reorder or relabel of an
existing, already-tested menu, so the work was updating the existing
accessible-mode pipe-input index literals (and, for S3, one literal
label assertion) to match, then confirming the full suite still passes.
A background search across every _test.go file confirmed
only that one S3 label assertion existed anywhere in the suite before
starting, so the blast radius of the rename pass was known upfront
rather than discovered by trial and error.
Files
internal/workflow/menu.go, menu_test.go,
tagmgmt_menu.go, tagmgmt_menu_test.go,
bucket_lifecycle.go, bucket_lifecycle_test.go,
iam_menu.go, iam_menu_test.go,
s3_menu.go, s3_menu_test.go,
keymgmt_menu.go.
Not done, flagged as pre-existing gaps
user_manual.md and TUI_REFERENCE.md’s
screen-map are both already stale independent of this change (missing
three whole domains, wrong item counts) – out of scope for this pass,
noted in DECISIONS.md as a separate, larger follow-up if wanted.
Phase 20.44 — User-Data Pre-Flight Size Check
Status: designed, implemented, unit-tested, and
real-AWS-verified 2026-07-28, live-testing-driven (see
DESIGN.md, “User-Data Pre-Flight Size Check”; DECISIONS.md, “User-data
pre-flight size check: hard error, no remediation loop-back; switch to
gzip.BestCompression”). Closes a confirmed gap: Phase 20.34’s gzip fix
is not sufficient for large cloud-init files (the incident’s
granian-rdm-v14 file, 57849 raw bytes at the time, still
exceeded the 16384-byte limit even gzip’d), and
encodeUserData had no pre-flight size validation anywhere.
go build/go vet/go test ./... -race/gofmt -l
all clean. Targeted for v0.0.6.
Work Items
Tests
Test-first, per [[feedback-test-before-fix]]/this project’s standing
practice: every new/changed userdata_gzip_test.go,
launch_execute_test.go,
launch_template_create_test.go, and
launch_template_sync_test.go case confirmed failing
(compile error against the old single-return signatures) before the
implementation existed.
Files
internal/workflow/userdata_gzip.go,
userdata_gzip_test.go, launch_execute.go,
launch_execute_test.go,
launch_template_create.go,
launch_template_create_test.go,
launch_template_sync.go,
launch_template_sync_test.go.
Dependency
None. Independent of Phase 20.34 (already shipped in v0.0.5) other
than building directly on its userdata_gzip.go.
Phase 20.45 — Cloud-Init Picker: Discoverable Cancel + SyncLaunchTemplate’s Missing cancelledIsNil Wrap
Status: designed, implemented, unit-tested, and verified in a
real interactive terminal 2026-07-28, live-usage-driven (see
DESIGN.md, “Cloud-Init Picker: Discoverable Cancel + a Second, Related
Bug”; DECISIONS.md, “Cloud-init picker: a ‘q’ cancel sentinel, and a
second bug found while tracing it”). Two bugs, same reported symptom:
(1) promptCloudInitYAMLFile had no discoverable single-step
cancel – ‘q’ was typed as literal filename text; (2)
SyncLaunchTemplate never wrapped its testable core’s return
with cancelledIsNil, so any cancellation inside it exited
clasm entirely instead of returning to the Compute menu.
go build/go vet/go test ./... -race/gofmt -l
all clean. Targeted for v0.0.6.
Work Items
Tests
Test-first, per [[feedback-test-before-fix]]: confirmed failing
before either fix landed – the “q” tests hung (accessible-mode
PromptString re-prompting forever on exhausted input rather
than erroring, the same gotcha already documented in this project’s own
memory for looping huh-based workflows) rather than failing cleanly,
which is itself confirmation the old code had no cancel path at all.
SyncLaunchTemplate itself isn’t pipe-testable (its first
step, pickLaunchTemplate, is a Picker-tier bubbletea
program – same limitation as every other Picker-tier conversion in this
package), so coverage is at the testable layers directly, matching how
this package already tests around that limitation elsewhere:
Files
internal/workflow/userdata.go,
userdata_test.go (new),
launch_template_sync.go,
launch_template_sync_test.go.
Dependency
None.
Phase 20.46 — Modify Launch Template Size
Status: designed, implemented, unit-tested, and
real-AWS-verified 2026-07-28 (see DESIGN.md, “Modify Launch
Template Size”; DECISIONS.md, “Modify Launch Template Size: one combined
action, unfiltered instance-type picker, shrink to the AMI’s snapshot
floor”). New TODO.md-requested feature: change a launch template’s
instance type and/or EBS root volume size without touching
UserData, today only possible via Sync (which only ever
changes UserData) or by hand-editing in the AWS Console.
Expanded mid-design after the user’s own concrete example (switching
granian-rdm-v14-test from Graviton/arm64 to an x86_64
instance type) surfaced that this requires an AMI swap too, since
architecture is tied to the AMI and
CreateLaunchTemplateVersion doesn’t validate the mismatch
itself.
go build/go vet/go test ./... -race/gofmt -l
all clean. Targeted for v0.0.6.
Work Items
Tests
Test-first, per [[feedback-test-before-fix]]: every new/changed function’s tests confirmed failing (undefined symbol or assignment- mismatch compile errors) before the corresponding implementation existed.
Files
internal/workflow/ebs_size.go,
ebs_size_test.go, launch_from_cloud_init.go,
launch_instance.go, new
instance_type_arch_check.go/instance_type_arch_check_test.go,
new
modify_launch_template_size.go/modify_launch_template_size_test.go,
menu.go, menu_test.go,
cmd/clasm/main.go, launch_execute_test.go
(fake DescribeInstanceTypes extended with
instanceTypeArchitectures).
Dependency
None.
Phase 20.47 — SSH Connection Info: Key Path + Username Guess
Status: designed, implemented, unit-tested, and
real-AWS-verified 2026-07-28 (see DESIGN.md, “SSH Connection
Info: Key Path + Username Guess”; DECISIONS.md, “SSH connection info:
guess the key path (only if it exists on disk) and the login username”).
TODO.md-requested feature: displayConnectionInfo already
printed ssh ec2-user@<ip>, missing an
-i <key path> entirely and hardcoding “ec2-user” even
for this tool’s own Ubuntu-only curated AMI list.
go build/go vet/go test ./... -race/gofmt -l
all clean. Targeted for v0.0.6.
Work Items
Tests
Test-first, per [[feedback-test-before-fix]]: confirmed failing (undefined-symbol/assignment-mismatch compile errors) before the implementation existed.
Files
internal/workflow/launch_execute.go,
launch_execute_test.go,
launch_from_template.go, power_state.go, and
their _test.go counterparts.
Dependency
None.
Deferred to a Later Version (Phase 23+, not scheduled)
Not part of v1/v2 — see DECISIONS.md, “V1 scope: ship
the four primitives first, defer composite workflows”, “Add Show/Export
Cloud-Init as a v1 primitive”, “Add Backup Archive & Trim as a v1
primitive”, “Add Rename Instance as a v1 primitive; AMI Name is
immutable”, “Add Create EC2 Instance from Cloud-Init YAML as a v1
primitive”, “Add Start/Stop/ Terminate EC2 Instance as v1 primitives”,
“Structure workflows for future record/replay”, “Redesign navigation as
a domain picker…”, “CloudFront + OAC by default for static websites”,
and DESIGN.md, “Deferred to a Later Version”. Recorded here
so they’re scheduled deliberately once Phase 16/22 pass, not lost:
- Recorded Scripts (“session playbooks”) — capture an
interactive session’s actions as an editable, templated YAML script and
replay it later, with the same confirmation gates as interactive mode
always enforced (never bypassable for
Environment=production). Phases 4-13 are already structured (params-struct/confirm-gate seam) to support this without rework once it’s built; whether Phases 19-21’s new workflows get the same seam is an open question for when this is actually built, not decided now - Clone instance for testing, Upgrade with rollback point, and Bake AMI from cloud-init — composite sequences built from v1’s primitives (Phase 4 + Phase 10, plus Phase 12’s SSM/poll/cleanup pattern for the cloud-init case). Once Recorded Scripts exist, these likely become example saved scripts rather than bespoke Go workflows
- Inline diff against
cloud-init-examples— fetch a comparison file from the GitHub repo and show a unified diff in the tool, instead of Phase 12’s export-then-manual-diff. Deferred until the repo’s files have a clear mapping to this account’sProjecttag values - Edit AMI Description — an AMI’s
Nameis immutable, butDescriptioncan be changed after creation viaec2.ModifyImageAttribute. The closest thing to a “rename” AWS actually allows for an AMI. Not requested for v1; noted here so it isn’t lost (seeDECISIONS.md, “Add Rename Instance as a v1 primitive; AMI Name is immutable”) - ACM certificate provisioning for a CloudFront
distribution’s alternate domain name — Phase 21 assumes a matching
certificate already exists in
us-east-1; requesting/validating a new one is out of scope - CloudFront functions / Lambda@Edge / WAF association — Phase 21 creates a plain S3-origin distribution only
- S3 bucket versioning and lifecycle rules — Phase 20’s Create Bucket uses default settings (no versioning, no lifecycle policy)
Environment=productionsafety-gate extension to Delete Key Pair, bucket/object deletion, or distribution changes — not extended in Phases 19-21; a candidate for a later pass (seeDESIGN.mdFeature 26)
Priority Order for Implementation
| Phase | Priority | Effort | Dependencies |
|---|---|---|---|
| Phase 0 | High | 2h | None |
| Phase 1 | High | 3h | Phase 0 |
| Phase 2 | High | 4h | Phase 1 |
| Phase 3 | High | 4h | Phase 2 |
| Phase 4 | High | 6h | Phase 1, 3 |
| Phase 5 | Medium | 2h | Phase 4 |
| Phase 6 | Medium | 2h | Phase 3 |
| Phase 7 | Medium | 2h | Phase 3 |
| Phase 8 | High | 6h | Phase 3 |
| Phase 9 | Medium | 4h | Phase 3 |
| Phase 10 | High | 8h | Phase 3 |
| Phase 11 | High | 6h | Phase 3 |
| Phase 12 | High | 8h | Phase 1, 2, 3 |
| Phase 13 | High | 10h | Phase 1, 2, 3 |
| Phase 14 | High | 4h | Phase 4, 5, 6, 7, 8, 9, 10, 11, 12, 13 |
| Phase 15 | Medium | 4h | Phase 14 |
| Phase 16 | High | 6h | Phase 14 |
| Phase 17 | Medium | 2h | Phase 16 |
| Phase 18 | High | 4h | Phase 14 (runs alongside 16/17) |
| Phase 19 | Medium | 6h | Phase 18 |
| Phase 20 | Medium | 16h | Phase 18 |
| Phase 20.1 | High | 24h | Phase 20 |
| Phase 21 | Deferred (someday/maybe) | 8h | Phase 18, Phase 20 |
| Phase 22 | High | 6h | Phase 19, 20 |
| Phase 23+ | Deferred | — | Phase 16, 22 (see above) |