Skip to content

fix: allow custom commands to merge into built-in namespaces at any level - #2191

Merged
Andriy Knysh (aknysh) merged 5 commits into
mainfrom
osterman/aws-cmd-registry
Mar 15, 2026
Merged

Andriy Knysh (aknysh) merged 5 commits into
mainfrom
osterman/aws-cmd-registry

Conversation

@osterman

@osterman Erik Osterman (Cloud Posse) (osterman) commented Mar 13, 2026 •

Copy link
Copy Markdown
Member

what

  • Custom commands in atmos.yaml can now be merged into built-in command namespaces at any nesting depth
  • Built-in commands are preserved when custom commands collide (no silent replacement)
  • When a custom command with steps conflicts with an existing command, a warning is emitted explaining which steps are ignored
  • Added comprehensive tests verifying namespace merging, collision handling, and deep nesting

why

Custom commands silently failed when sharing a namespace with built-in commands. For example, users couldn't define mcp aws install because the built-in mcp command existed. The root causes were:

  1. Top-level collision detection only: The old code only checked for collisions at the top level (topLevel=true branch) using a separate getTopLevelCommands() map
  2. No non-top-level detection: When recursing into nested subcommands, there was no collision check at all, allowing Cobra's AddCommand to silently replace existing subcommands

This fix generalizes collision detection to all nesting depths by using a universal findSubcommand(parent, name) check that works on any cobra command's children.

how

  • Removed the topLevel bool parameter from processCustomCommands()
  • Replaced top-level-only collision detection with findSubcommand() that checks the parent command's subcommands at any level
  • Extracted command creation logic into createCustomCommand() helper to reduce nesting complexity (fixes golangci-lint nestif warning)
  • When a collision is detected, the existing command is reused and nested custom subcommands are merged into it
  • All call sites and tests updated to use the new signature

Summary by CodeRabbit

  • Bug Fixes

    • Custom commands that collide with built-in commands are now merged into existing namespaces; when collisions occur, custom execution steps are skipped and a user-facing warning is shown.
    • Flag and shorthand conflicts are validated earlier to prevent registration issues and clearer UI feedback.
  • Tests

    • Added tests covering collision handling, namespace merging, and deep-nesting preservation of built-in handlers.

…at any level

What changed:
- Generalized collision detection in processCustomCommands to work at any nesting depth
- Custom commands can now extend built-in command trees (e.g., mcp aws install alongside mcp start)
- Built-in commands are preserved when custom commands collide; warning emitted if custom has conflicting steps
- Removed topLevel bool parameter in favor of universal findSubcommand() check on parent
- Extracted command creation logic into createCustomCommand() helper to reduce complexity

Why:
Custom commands in atmos.yaml silently failed when sharing a namespace with built-in commands.
- Top-level collisions were detected but only at the top level (topLevel=true branch)
- Non-top-level collisions had no detection; Cobra's AddCommand silently replaced built-in subcommands
This prevented users from extending built-in namespaces with custom subcommands (e.g., mcp aws install).

Tests added:
- TestCustomCommand_NamespaceMerge_SubcommandsAdded: custom subcommands merge into built-in namespace
- TestCustomCommand_NonTopLevelCollision_BuiltinPreserved: built-in subcommands preserved, not replaced
- TestCustomCommand_DeepNesting_MergeWorks: merging works at arbitrary nesting depth

All existing custom command tests pass.

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>
@github-actions github-actions Bot added the size/m Medium size PR label Mar 13, 2026
@github-actions

github-actions Bot commented Mar 13, 2026 •

Copy link
Copy Markdown

Dependency Review

✅ No vulnerabilities or license issues found.

Scanned Files

None

@osterman Erik Osterman (Cloud Posse) (osterman) added the patch A minor, backward compatible change label Mar 13, 2026
@coderabbitai

coderabbitai Bot commented Mar 14, 2026 •

Copy link
Copy Markdown
Contributor
📝 Walkthrough

Walkthrough

Refactors custom command processing: removes the topLevel parameter, centralizes command creation and flag validation into createCustomCommand and helpers, reuses existing subcommands via findSubcommand, switches some logging to ui.Writeln, and adds collision-handling tests. Call sites updated to the new processCustomCommands arity.

Changes

Cohort / File(s) Summary
Custom Command Processing
cmd/cmd_utils.go
Removed top-level handling in processCustomCommands; added findSubcommand, createCustomCommand, centralized flag validation (validateCustomCommandFlags, validateFlag*) and registration helpers (registerCustomCommandFlags, registerFlag*); validations run before registration; some logs replaced with ui.Writeln; added pflag and ui imports.
Collision Handling Tests
cmd/custom_command_collision_test.go
Added three tests validating merge/collision behavior between custom and built-in commands (namespace merge, non-top-level collision preserved, deep nesting merge).
Test Callsite & Main Update
cmd/custom_command_flag_conflict_test.go, cmd/custom_command_identity_test.go, cmd/custom_command_integration_test.go, cmd/root.go
Removed final boolean argument from all processCustomCommands calls (signature arity changed from 4 to 3); updated test helpers and call sites accordingly.
Behavioral Note
cmd/...
When a custom command collides with an existing built-in subcommand, custom steps are warned via UI and ignored; command creation now applies identity, descriptions, and pre-run/run wiring consistently across new commands.

Sequence Diagram(s)

sequenceDiagram
    participant Config as AtmosConfig
    participant Processor as processCustomCommands
    participant Cobra as Cobra RootCmd
    participant UI as ui.Writeln
    participant Flags as pflag Registry

    Config->>Processor: provide custom command definitions
    Processor->>Cobra: findSubcommand(name) ?
    alt subcommand exists
        Processor->>UI: warn about step conflict (if any)
        Processor->>Cobra: reuse existing subcommand (merge children)
    else no existing
        Processor->>Processor: createCustomCommand(config)
        Processor->>Flags: validateCustomCommandFlags(config.flags)
        Flags-->>Processor: validation result
        Processor->>Cobra: registerCustomCommandFlags + add command
    end
    Processor->>Processor: recurse into nested command configs with parent cmd
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • aknysh
  • gberenice
🚥 Pre-merge checks | ✅ 3
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: enabling custom commands to merge into built-in namespaces at any nesting level, which is the core refactoring removing the topLevel parameter.
Docstring Coverage ✅ Passed Docstring coverage is 95.83% which is sufficient. The required threshold is 80.00%.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch osterman/aws-cmd-registry
📝 Coding Plan
  • Generate coding plan for human review comments

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@codecov

codecov Bot commented Mar 14, 2026 •

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 91.44737% with 13 lines in your changes missing coverage. Please review.
✅ Project coverage is 77.40%. Comparing base (d68c499) to head (53026ef).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
cmd/cmd_utils.go 91.39% 10 Missing and 3 partials ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #2191      +/-   ##
==========================================
+ Coverage   77.38%   77.40%   +0.02%     
==========================================
  Files         962      962              
  Lines       91239    91284      +45     
==========================================
+ Hits        70601    70655      +54     
+ Misses      16558    16548      -10     
- Partials     4080     4081       +1     
Flag Coverage Δ
unittests 77.40% <91.44%> (+0.02%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
cmd/root.go 67.58% <100.00%> (ø)
cmd/cmd_utils.go 56.98% <91.39%> (+1.22%) ⬆️

... and 2 files with indirect coverage changes

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🧹 Nitpick comments (2)
cmd/custom_command_collision_test.go (1)

130-132: Consider asserting the conflict warning path too.

This test triggers the “custom steps ignored on collision” branch, but it currently validates only command behavior. Adding a stderr/UI warning assertion would lock in the full contract.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmd/custom_command_collision_test.go` around lines 130 - 132, The test
currently calls processCustomCommands(atmosConfig, atmosConfig.Commands,
RootCmd) and only asserts no error; add an assertion that the collision warning
UI/stderr was emitted by capturing the command output (or the test UI/stderr
buffer used by RootCmd) and checking it contains the expected "custom steps
ignored on collision" (or similar) warning text so the collision branch is
verified; place this capture/assert immediately after the processCustomCommands
call and keep the existing require.NoError assertion.
cmd/cmd_utils.go (1)

519-522: Wrap required-flag registration failures with sentinel/context.

At Line 521, returning raw err loses structured context and diverges from project error-handling policy.

Proposed patch
 		if flag.Required {
 			err := customCommand.MarkPersistentFlagRequired(flag.Name)
 			if err != nil {
-				return nil, err
+				return nil, errUtils.Build(errUtils.ErrInvalidFlag).
+					WithCause(err).
+					WithExplanationf("Failed to mark flag '--%s' as required for custom command '%s'", flag.Name, commandConfig.Name).
+					WithContext("command", commandConfig.Name).
+					WithContext("flag", flag.Name).
+					Err()
 			}
 		}
As per coding guidelines: "Error Handling (MANDATORY): All errors MUST be wrapped using static errors defined in `errors/errors.go`."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmd/cmd_utils.go` around lines 519 - 522, Replace the raw return of err from
customCommand.MarkPersistentFlagRequired(flag.Name) with a wrapped error using
the static sentinel from errors/errors.go (e.g. errors.ErrRegisterFlag or the
appropriate sentinel defined there), include the flag.Name and the original err
as context, and use Go error wrapping (fmt.Errorf("%w: ...", sentinel, err) or
equivalent) so callers receive the sentinel plus original detail; also add any
required imports (fmt and the errors package) and update the return to return
nil, wrappedErr instead of nil, err.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@cmd/cmd_utils.go`:
- Around line 95-99: The format string passed to ui.Warningf (in the block
referencing commandConfig.Name and existing.CommandPath()) includes a trailing
"\n" which produces an extra blank line; remove the explicit newline from the
format string so ui.Warningf's built-in newline is used instead and the warning
prints with a single line.

---

Nitpick comments:
In `@cmd/cmd_utils.go`:
- Around line 519-522: Replace the raw return of err from
customCommand.MarkPersistentFlagRequired(flag.Name) with a wrapped error using
the static sentinel from errors/errors.go (e.g. errors.ErrRegisterFlag or the
appropriate sentinel defined there), include the flag.Name and the original err
as context, and use Go error wrapping (fmt.Errorf("%w: ...", sentinel, err) or
equivalent) so callers receive the sentinel plus original detail; also add any
required imports (fmt and the errors package) and update the return to return
nil, wrappedErr instead of nil, err.

In `@cmd/custom_command_collision_test.go`:
- Around line 130-132: The test currently calls
processCustomCommands(atmosConfig, atmosConfig.Commands, RootCmd) and only
asserts no error; add an assertion that the collision warning UI/stderr was
emitted by capturing the command output (or the test UI/stderr buffer used by
RootCmd) and checking it contains the expected "custom steps ignored on
collision" (or similar) warning text so the collision branch is verified; place
this capture/assert immediately after the processCustomCommands call and keep
the existing require.NoError assertion.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 9a91a132-1168-4ec3-9091-a486f37c22a2

📥 Commits

Reviewing files that changed from the base of the PR and between 76a269b and c8cf4bb.

📒 Files selected for processing (6)
  • cmd/cmd_utils.go
  • cmd/custom_command_collision_test.go
  • cmd/custom_command_flag_conflict_test.go
  • cmd/custom_command_identity_test.go
  • cmd/custom_command_integration_test.go
  • cmd/root.go

Comment thread cmd/cmd_utils.go
coderabbitai[bot]
coderabbitai Bot previously approved these changes Mar 14, 2026
…and code

- Remove trailing \n from Warningf format string (ui.Warningf already appends newline)
- Pass atmosConfig by pointer to avoid copying 7KB struct (gocritic hugeParam)
- Pass schema.CommandFlag by pointer in all helpers (gocritic hugeParam)
- Extract flag validation into validateCustomCommandFlags/validateFlag helpers (gocognit)
- Extract flag registration into registerCustomCommandFlags/registerFlag helpers (nestif)

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@cmd/cmd_utils.go`:
- Around line 462-566: The shorthand validation must enforce that
flag.Shorthand, when non-empty, is exactly one rune to avoid pflag panics;
update validateFlagShorthand to check len([]rune(flag.Shorthand)) == 1 and if
not return a structured error (similar style to existing errors) indicating
invalid shorthand for the command (include cmdName, shorthand, and config path
"commands.<cmdName>.flags"), so invalid shorthands are caught at parse time
before registerBoolFlag/registerStringFlag call sites that call BoolP/StringP.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: c54d35d5-ed78-492f-bebb-119435dce5e7

📥 Commits

Reviewing files that changed from the base of the PR and between c8cf4bb and e0e619e.

📒 Files selected for processing (1)
  • cmd/cmd_utils.go

Comment thread cmd/cmd_utils.go
pflag panics when BoolP/StringP receives a shorthand longer than one
character. Add validation in validateFlagShorthand to catch this at
config parsing time and return a structured error instead.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (1)
cmd/cmd_utils.go (1)

91-116: Clean refactor for namespace merging.

The collision detection now works at any nesting depth. The warning for conflicting steps is helpful.

One consideration: when a custom command collides with an existing command, flags from the custom config are silently ignored while only steps trigger a warning. Consider warning about ignored flags too if any are defined.

Optional: Warn about ignored flags on collision
 		existing := findSubcommand(parentCommand, commandConfig.Name)
 		if existing != nil {
 			if len(commandConfig.Steps) > 0 {
 				ui.Warningf(
 					"Custom command %q defines steps that conflict with built-in command %q; "+
 						"built-in behavior preserved, custom steps ignored",
 					commandConfig.Name, existing.CommandPath(),
 				)
 			}
+			if len(commandConfig.Flags) > 0 {
+				ui.Warningf(
+					"Custom command %q defines flags that conflict with built-in command %q; "+
+						"built-in behavior preserved, custom flags ignored",
+					commandConfig.Name, existing.CommandPath(),
+				)
+			}
 			command = existing
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@cmd/cmd_utils.go` around lines 91 - 116, When a custom command collides with
an existing command (detected by findSubcommand returning non-nil), we currently
warn only if commandConfig.Steps is non-empty; extend this to also warn when
commandConfig.Flags (or any flag-like field) is non-empty so custom flags aren’t
silently ignored. Update the collision branch that uses existing :=
findSubcommand(parentCommand, commandConfig.Name) to inspect commandConfig.Flags
(and commandConfig.Args/options if applicable) and call ui.Warningf with a clear
message referencing commandConfig.Name and existing.CommandPath() (similar style
to the steps warning) whenever flags are present; leave reuse of existing and
subsequent calls to processCustomCommands/createCustomCommand unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@cmd/cmd_utils.go`:
- Around line 91-116: When a custom command collides with an existing command
(detected by findSubcommand returning non-nil), we currently warn only if
commandConfig.Steps is non-empty; extend this to also warn when
commandConfig.Flags (or any flag-like field) is non-empty so custom flags aren’t
silently ignored. Update the collision branch that uses existing :=
findSubcommand(parentCommand, commandConfig.Name) to inspect commandConfig.Flags
(and commandConfig.Args/options if applicable) and call ui.Warningf with a clear
message referencing commandConfig.Name and existing.CommandPath() (similar style
to the steps warning) whenever flags are present; leave reuse of existing and
subsequent calls to processCustomCommands/createCustomCommand unchanged.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: a3c077ba-9b8d-4378-9f96-1ed70d507dfd

📥 Commits

Reviewing files that changed from the base of the PR and between e0e619e and bb5c27b.

📒 Files selected for processing (1)
  • cmd/cmd_utils.go

@aknysh
Andriy Knysh (aknysh) merged commit 1f4ac96 into main Mar 15, 2026
91 of 92 checks passed
@aknysh
Andriy Knysh (aknysh) deleted the osterman/aws-cmd-registry branch March 15, 2026 18:20
@github-actions

Copy link
Copy Markdown

These changes were released in v1.210.0-rc.0.

@github-actions

Copy link
Copy Markdown

These changes were released in v1.210.0-test.25.

Erik Osterman (Cloud Posse) (osterman) added a commit that referenced this pull request May 30, 2026
…de claims

Add docs/prd/custom-command-builtin-override.md specifying how a step-based
custom command can override a user-facing built-in (opt-in `override: true`)
and call the native built-in via an `invoke: built-in` step modifier on
`type: atmos` steps — cycle-proof by invoking the retained handler in-process
(no ATMOS_REEXEC_DEPTH counter).

Correct command-registry-pattern.md (Scenario 1 / Why This Works / FAQ) to
reflect shipped behavior since #2191 (built-in preserved, custom steps ignored
on collision) and document `invoke:` on the atmos step in workflow-step-types.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Andriy Knysh (aknysh) added a commit that referenced this pull request May 30, 2026
…EV-2969) (#1899)

* feat: Implement workflow step types with registry pattern (DEV-263, DEV-2969)

Add 20+ step types across 4 categories (Interactive, Output, UI, Command) with
extensible registry pattern. Supports Go template variable passing between steps
and per-step output modes (viewport, raw, log, none) for flexible result display.

Key features:
- StepHandler interface with auto-registration via init()
- Variables struct supporting Go template resolution of step outputs
- Interactive handlers (input, confirm, choose, filter, file, write) with TTY detection
- UI message handlers (success, info, warn, error, markdown)
- Output handlers (spin, table, pager, format, join, style)
- Command handlers (atmos, shell) with output mode support
- OutputModeWriter for viewport paging, raw passthrough, grouped logging, or silent modes
- Full test coverage for registry, variables, handlers, and output modes
- Schema extensions for new step fields (prompt, options, content, viewport config, env vars)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>

* feat: Add input type and workflow step types with complete TUI support (DEV-263, DEV-2969)

- Implement workflow step registry pattern for extensible step types
- Add style step (renamed from box) with configurable borders, padding, margin, width, alignment, text decorations
- Add markdown field to style step for rendering markdown within styled containers with width-aware rendering
- Add log step with structured logging support (level, fields) using atmos logger
- Add linebreak step for spacing with configurable count
- Add pager step displaying file content with scrolling and search
- Add DefaultThemeName constant to theme package for proper theme management
- Implement width-aware markdown rendering in style step using glamour
- Pre-populate Variables with OS environment variables for template access via {{ .env.USER }}
- Add comprehensive demo workflow showing all step types with real-world deployment scenario
- Update schema to support new step configurations and input type
- Add complete documentation for workflow step types and usage
- Implement CategoryUI for display-only steps vs CategoryOutput for output-producing steps

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Haiku 4.5 <noreply@anthropic.com>

* refactor: Consolidate success/info/warn/error steps into unified toast step type

Replace individual success, info, warn, and error step types with a single
toast step type that uses a level field to specify the message style. This
reduces code duplication and simplifies the step registry.

Usage:
  - type: toast
    level: success  # success, info, warning, error (default: info)
    content: "Deployment complete!"

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: Render markdown in pager step for .md files

The pager step now automatically renders markdown content when:
- The `markdown: true` flag is explicitly set
- The file has a .md or .markdown extension (auto-detected)

This ensures README.md and other markdown files display properly
with styled headings, links, code blocks, etc.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* refactor: Address CodeRabbit review feedback for workflow steps

- Add performance tracking to all workflow step handlers
  (AtmosHandler, FilterHandler, InputHandler, MarkdownHandler,
  ShellHandler, StyleHandler, SpinHandler)
- Remove dead code in containsStackFlag function
- Fix SpinHandler to use prepared environment from Variables.Env
  instead of os.Environ()
- Update executor to use static ErrWorkflowStepFailed error
- Update tests to use errors.Is() instead of string matching
- Add comprehensive test coverage for RunAll function
- Fix pager resolveTitle to use resolved path for template paths

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* docs: Add separator field to join step and complete step type documentation

- Add separator field to WorkflowStep schema for join step type
- Update join handler to use configurable separator (default: newline)
- Add missing documentation for pager, format, style, linebreak, log steps
- Fix join step docs to show correct options field usage
- Update PRD with correct join step example
- Fix blog post step count ("18 interactive" → "18")
- Add tests for join separator functionality

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat: Add alert, title, clear, env, and exit workflow step types

Add five new workflow step types that expose existing atmos terminal/UI
capabilities:

- alert: Plays terminal bell to notify user (uses terminal.Alert())
- title: Sets/restores terminal window title (uses terminal.SetTitle())
- clear: Clears current terminal line (uses ui.ClearLine())
- env: Sets environment variables for subsequent steps
- exit: Exits workflow with specific exit code (returns error with code)

The exit step returns an ErrWorkflowExit error with an exit code attached
via WithExitCode(), following the pattern established in the
avoiding-deep-exits-pattern.md PRD.

This increases the total step count from 18 to 23, organized across
five categories: Command, Interactive, UI Messages, Output, and Terminal.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: Update workflow error snapshots to use correct explanation/hint separation

Regenerate golden snapshots to reflect the correct semantic separation:
- Explanation section: "The following command failed to execute" (what happened)
- Hints section: "To resume the workflow" with 💡 prefix (what to do)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat: Add show configuration, sleep, stage step types for workflows

Add automatic workflow formatting with show configuration:
- header: Display workflow description as styled markdown header
- flags: Show command-line flags (stack, identity, etc.)
- count: Show step count prefix [1/5] with step name in muted style
- progress: Show progress bar (TTY only)
- command: Show command before execution

Add new step types:
- sleep: Pause execution for specified duration (for demos)
- stage: Display position among stage steps only [Stage 1/3]

Integrate show features into legacy workflow executor with
ShowRenderer for header/flags and ProgressRenderer for progress bar.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: Correct progress bar rendering to clear line before step output

- Change RenderWithLabel to render without newline for in-place display
- Add RenderPermanent method to render with newline as permanent record
- Clear progress line before step execution, then re-render as permanent
- Ensures progress line appears as header with step output below it

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* docs: Add workflow step test coverage improvement plan

Add comprehensive PRD for increasing pkg/workflow/step/ test coverage
from 26.3% to 80%+. The plan identifies critical files (output_mode.go,
shell.go, atmos.go) and outlines a 4-phase approach focusing on
Execute() method testing across all step handlers.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* test: Increase pkg/workflow/step test coverage to 77%

Add comprehensive tests for workflow step handlers:
- Add Execute() tests for UI handlers (alert, clear, env, linebreak, etc.)
- Add Execute() tests for output handlers (format, join, style, table, etc.)
- Add helper method tests for interactive handlers (choose, filter, input, file)
- Add shell handler execution tests with real commands
- Add atmos handler helper method tests
- Add output mode execution tests
- Add registry tests for handler registration and lookup
- Fix errorlint issues in config/load.go (use errors.As instead of type switch)

Coverage improved from 26.3% to 77.3%. Remaining uncovered code
is in TTY-dependent interactive handlers and external dependency code.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* chore: Fix NOTICE file license URLs

Update license URLs to correct paths:
- aws-sdk-go-v2/internal/endpoints: Remove extra v2/ in path
- googleapis/gax-go: Remove v2/ subdirectory from path

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* [autofix.ci] apply automated fixes

* fix: Address CodeRabbit review comments

- Add trailing punctuation to README.md list item
- Clarify misleading conditional execution comment in interactive.yaml
- Add perf.Track to ToastHandler.Execute per coding guidelines

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* [autofix.ci] apply automated fixes

* [autofix.ci] apply automated fixes (attempt 2/3)

* fix: Make file_test.go cross-platform for Windows

Use t.TempDir() instead of hardcoded /tmp paths that don't exist on Windows.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* docs: Update roadmap with detailed workflow step types

Expand workflow milestones to list all step types introduced in PR #1899:
- Output step types: toast, alert, title, clear, linebreak, markdown, style, table, format, join, pager, spin
- Interactive step types: input, confirm, choose, filter, file, write
- Flow control: sleep, stage, exit, env, show

Update quarter to q4-2025 and add PR reference.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: Address CodeRabbit review comments for workflow execution

- Move RenderHeaderIfNeeded call outside the step loop
- Add perf.Track to public functions in step/executor.go
- Add perf.Track to public functions in step/output_mode.go
- Replace direct os.Stdout/Stderr with I/O context abstraction

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: Add perf.Track to remaining public functions

- Add perf.Track to NewStepExecutorWithVars
- Add perf.Track to StepExecutor.RunAll
- Add perf.Track to OutputModeWriter.Execute
- Add perf.Track to RenderCommand

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat: Move step handlers to pkg/runner for unified execution

This commit integrates workflow step handlers into the unified pkg/runner
package, enabling both workflows AND custom commands to use extended step
types (input, choose, confirm, markdown, etc.).

Key changes:
- Move all 65+ step handler files from pkg/workflow/step/ to pkg/runner/step/
- Extend schema.Task with all interactive/UI step fields
- Add step handler registry lookup in pkg/runner.Run()
- Update custom commands to support extended step types
- Add perf.Track to all public step handler functions
- Update imports in workflow executor and related files

This enables a unified experience where custom commands defined in atmos.yaml
can now use interactive step types like:
  steps:
    - type: input
      prompt: "Enter your name"
      name: user_name
    - type: confirm
      prompt: "Deploy to production?"
      name: confirmed

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* docs: Add step types partial for workflows and custom commands

- Create reusable _step-types.mdx partial with all 25+ step types documentation
- Update workflows.mdx to import partial instead of inline content
- Add "Extended Step Types" section to custom commands docs
- Add examples showing interactive deployment wizards in custom commands
- Both workflows and custom commands now share the same step types reference

This ensures consistent documentation across both features and reduces
duplication. Custom commands can now use input, choose, confirm, and all
other extended step types just like workflows.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: Address CodeRabbit and security scanning review comments

- choose.go: Fix Option.Selected() return value assignment
  The huh.Option.Selected() method returns a new Option rather than
  modifying in place, so the return value must be assigned back to
  the slice for the default selection to take effect.

- log.go: Guard against integer overflow in slice capacity
  Add bounds check before computing len(step.Fields)*2 to prevent
  potential overflow for extremely large field maps.

- spin.go: Guard against integer overflow in slice capacity
  Add bounds check before computing len(vars.Env)+len(step.Env) to
  prevent potential overflow for extremely large environment maps.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* docs: Add blog post and update roadmap for custom commands step types

- Add blog post announcing custom commands now support 25+ interactive step types
- Update roadmap milestone with correct quarter, docs link, changelog, and PR reference
- Include practical examples for deploy-wizard, multi-select, and input collection

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* chore: Remove duplicate blog post and update roadmap changelog refs

- Remove 2025-12-20-interactive-workflow-steps.mdx (superseded by
  2026-01-03-custom-commands-step-types.mdx which covers both
  workflows and custom commands)
- Add changelog references to workflow milestones in roadmap

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: Apply Copilot suggestions for overflow protection

- log.go: Check fieldsLen >= maxCapacity/2 before multiplication
- spin.go: Extract safeEnvCapacity helper to clamp values before adding,
  reducing cyclomatic complexity while ensuring sum can't overflow

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: Use min() for cleaner overflow protection

Using Go's builtin min() makes the overflow protection clearer
and more analyzable by static analysis tools like CodeQL.

- log.go: fieldsLen := min(len(step.Fields), maxCapacity/2)
- spin.go: Use min() to clamp both inputs before addition

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: Make SpinHandler tests platform-aware for Windows compatibility

Tests in spin_test.go used Unix-specific commands (pwd, echo $VAR) that
fail on Windows. Added platform-specific helper functions that use
appropriate commands for each platform:
- pwd -> cd on Windows
- echo $VAR -> echo %VAR% on Windows
- /tmp -> C:\Windows\Temp on Windows

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: Add overflow protection to PrepareEnvironment map allocation

CodeQL flagged that len(environ)+6 could overflow on 32-bit systems.
Applied the same min() clamping pattern used in other overflow fixes.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: Address CodeQL overflow alert and CodeRabbit refactor suggestion

1. Simplified overflow protection in env.go to avoid any arithmetic that
   CodeQL might flag - just clamp environLen directly without addition.

2. Removed duplicate containsStr function from spin_test.go and use
   strings.Contains from stdlib instead (CodeRabbit suggestion).

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: Use explicit if-guard for CodeQL overflow detection in log.go

CodeQL couldn't infer that min(x, maxCapacity/2) * 2 is safe because it
doesn't trace the relationship between clamped values and subsequent
multiplication. Changed to an explicit if-guard pattern that CodeQL
recognizes:

  if fieldsLen > maxFields {
      fieldsLen = maxFields
  }

This pattern is explicitly documented in CodeQL's recommendation for
guarding against allocation size overflow.

Fixes: https://github.com/cloudposse/atmos/security/code-scanning/5211

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: Extract safeKeyvalsCapacity helper for CodeQL overflow detection

CodeQL couldn't trace that an if-guard on fieldsLen bounds the subsequent
multiplication. Moving the guarded multiplication into a helper function
with an explicit if-guard pattern allows CodeQL to recognize the safety:

  if fieldsLen > maxCapacity/2 {
      return maxCapacity
  }
  return fieldsLen * 2

This pattern explicitly shows that fieldsLen*2 only executes when
fieldsLen <= maxCapacity/2, which CodeQL can verify is safe.

Fixes: https://github.com/cloudposse/atmos/security/code-scanning/5211

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* docs: Rename demo-interactive-workflows to interactive-workflows

- Rename examples/demo-interactive-workflows to examples/interactive-workflows
- Add EmbedExample for interactive-workflows to workflow documentation

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* docs: Add custom-commands example with interactive and advanced commands

- Rename demo-custom-command to custom-commands
- Add .atmos.d/interactive.yaml with interactive step types (choose, confirm, input, etc.)
- Add .atmos.d/advanced.yaml with boolean flags, env vars, and working directory examples
- Update README.md with comprehensive documentation
- Add EmbedExample component to commands.mdx documentation

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* docs: Update file-browser plugin for renamed examples

- Rename demo-custom-command to custom-commands in TAGS_MAP and DOCS_MAP
- Add interactive-workflows mapping (renamed from demo-interactive-workflows)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: Update test paths for renamed custom-commands example directory

The examples/demo-custom-command directory was renamed to
examples/custom-commands but test files were not updated,
causing CI failures on all platforms.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* fix: Address CodeRabbit review feedback

- Reuse single StepExecutor across custom command steps for variable persistence
- Fix directory path in interactive-workflows README
- Move header rendering before step loop in workflow_utils.go
- Wrap confirmation errors with static ErrWorkflowStepFailed sentinel
- Fix multi-select limit to allow unlimited when Multiple=true and Limit<=0
- Wrap format.go errors with static error sentinels
- Validate stdin TTY (not just stdout) for interactive steps
- Use static error sentinels for template resolution failures
- Wrap join.go errors with static error sentinels
- Assert actual log level values in log_test.go
- Implement per-line prefixing in StreamingOutputWriter
- Wrap sleep.go duration/context errors with static sentinels
- Surface invalid timeout values instead of silently ignoring
- Wrap style.go errors with static error sentinels
- Update spacing docs to include 3-value form
- Make LoadOSEnv test portable with t.Setenv

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* [autofix.ci] apply automated fixes

* [autofix.ci] apply automated fixes (attempt 2/3)

* [autofix.ci] apply automated fixes (attempt 3/3)

* [autofix.ci] apply automated fixes

* fix: Address CodeRabbit review comments for workflow execution

- Propagate working_directory to extended steps so handlers like spin
  run from the correct directory
- Always print step labels when progress is disabled, not just when
  show.count is true
- Fall back to log output when viewport commands fail to ensure
  diagnostic output is visible

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>

* feat: Add viewport dimension constraints to pager

Allow the pager to respect configurable max height/width instead of
always using full terminal dimensions. This enables workflow viewport
settings to control the pager display area.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>

* style: apply gofumpt formatting to pkg/pager

Fixes the go-fumpt pre-commit CI hook, which reformatted pager.go and
model.go and failed because the committed source was not gofumpt-canonical.

Extract the note-width max() into a variable so the //nolint:gosec
directive stays on the uint() conversion line after gofumpt reflows the
call, keeping both gofumpt and golangci-lint (gosec/nolintlint) happy.

Formatting-only change, no behavior impact.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(prd): add built-in command override design; correct stale override claims

Add docs/prd/custom-command-builtin-override.md specifying how a step-based
custom command can override a user-facing built-in (opt-in `override: true`)
and call the native built-in via an `invoke: built-in` step modifier on
`type: atmos` steps — cycle-proof by invoking the retained handler in-process
(no ATMOS_REEXEC_DEPTH counter).

Correct command-registry-pattern.md (Scenario 1 / Why This Works / FAQ) to
reflect shipped behavior since #2191 (built-in preserved, custom steps ignored
on collision) and document `invoke:` on the atmos step in workflow-step-types.md.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(prd): clarify nested-collision and invoke schema; add step Execute tests

Address CodeRabbit feedback on PR #1899:
- command-registry-pattern.md: split the nested custom-command bullet to call
  out the name-collision case (findSubcommand reuses the existing command and
  ignores custom steps with a warning at every level, not just top-level).
- workflow-step-types.md: add the `invoke` field to the WorkflowStep schema
  block for consistency with the atmos step reference.

Increase pkg/runner/step coverage 78.9% -> 82.3% with hang-safe Execute tests:
- spin: full Execute orchestration (non-TTY runs the command directly).
- input/confirm/choose/write/filter/file: fail-fast Execute contract.
- pager: Execute error paths before the pager launches.
- atmos: Execute resolution errors before re-exec.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* test(workflow): raise step-type patch coverage above 80%

Add unit tests for the new workflow step-type subsystem to clear the
Codecov patch target (was 73.94%, target 80%).

- pkg/workflow/progress_test.go: inject a fake terminal.Terminal to cover
  formatProgressLine, renderers, Done/IsEnabled, and nil/disabled guards
  (8.6% -> ~100%).
- pkg/workflow/show_renderer_test.go: cover formatHeader, formatFlags
  (sort + styled/nil branches), and RenderHeaderIfNeeded paths (17% -> 100%).
- pkg/runner/step/interactive_execute_test.go: assert each interactive
  Execute returns ErrStepTTYRequired under go test (no TTY), covering the
  CheckTTY guard for confirm/write/input/choose/filter/file.
- pkg/runner/step/testmain_test.go + atmos_test.go: a sentinel-gated
  TestMain lets the test binary impersonate atmos so Execute,
  runAtmosCommand, and ExecuteWithWorkflow are tested cross-platform
  (0% -> ~85%).

Package coverage: pkg/workflow 76.5% -> 96.9%, pkg/runner/step 78.9% -> 82.3%.
No production code changed.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* updates

* test: skip interactive Execute TTY test when stdin/stdout are TTYs

Address CodeRabbit feedback on PR #1899: TestInteractiveHandlers_ExecuteWithoutTTY
asserts ErrStepTTYRequired, but CheckTTY only triggers when stdin or stdout is
not a TTY. Run from an interactive terminal (both TTYs), Execute would fall
through and block on the huh prompt, hanging the test. Add a pre-check that skips
in that case; the hang-proof sibling TestInteractiveHandlersExecuteFailFast
covers the TTY-present path.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>

* fix(workflow): only print step label when show.count is enabled

The workflow executor printed the bare step label (e.g. "step1") to stderr
on every run when the progress bar was disabled, breaking backward
compatibility — the show.* features are documented as opt-in (default false).
Gate the label print on show.count so default output is unchanged while the
count feature still works in non-progress/non-TTY mode.

Also regenerate the workflow-retries snapshot to match the intended
WithExplanationf error formatting (## Explanation before ## Hints), and add
the invoke field to the WorkflowStep schema block in the PRD for consistency
with the documented exec/built-in behavior.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* docs(prd): remove invoke field from WorkflowStep schema

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>

* updates

---------

Co-authored-by: Claude Haiku 4.5 <noreply@anthropic.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: Andriy Knysh <aknysh@users.noreply.github.com>
Co-authored-by: aknysh <andriy.knysh@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

patch A minor, backward compatible change size/m Medium size PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants