Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
57 commits
Select commit Hold shift + click to select a range
7b3468c
feat: Add experimental feature transparency and version tracking
osterman Jan 7, 2026
dd8ecff
docs: Clarify experimental features may have unimplemented functionality
osterman Jan 7, 2026
b96af0c
docs: Clarify experimental features are intended to graduate to stable
osterman Jan 7, 2026
cb45a6f
feat: Add comprehensive experimental feature visibility across surfaces
osterman Jan 7, 2026
74e8670
feat: Log debug message when experimental warnings disabled
osterman Jan 7, 2026
42c5ffa
feat: Add EXPERIMENTAL badge to CLI help output
osterman Jan 7, 2026
2630977
fix: Improve Experimental badge WCAG AA accessibility
osterman Jan 7, 2026
1944192
fix: Mark toolchain command as experimental to match roadmap
osterman Jan 7, 2026
0028ab1
feat: Mark terraform backend command as experimental
osterman Jan 7, 2026
b60802d
fix: Address CodeRabbit review feedback
osterman Jan 7, 2026
7b1c1a5
feat: Mark terraform workdir command as experimental
osterman Jan 7, 2026
32a43c7
test: Regenerate snapshots for experimental setting
osterman Jan 7, 2026
feb59b5
docs: Add blog post and roadmap entry for experimental feature controls
osterman Jan 7, 2026
6e4a298
refactor: Use theme colors for experimental badge
osterman Jan 7, 2026
f550ffd
fix: Show experimental warning for annotation-based experimental comm…
osterman Jan 7, 2026
1d95cba
refactor: Simplify experimental feature message
osterman Jan 7, 2026
1f90443
docs: Update experimental feature example output
osterman Jan 7, 2026
e86216e
fix: Use correct /experimental URL in experimental feature message
osterman Jan 7, 2026
44bdd79
docs: Add link to /experimental from settings documentation
osterman Jan 7, 2026
657abac
docs: Add CLI configuration section to /experimental page
osterman Jan 7, 2026
47a795f
docs: Move experimental features list after "What Experimental Means"…
osterman Jan 7, 2026
23bcee1
docs: Update Slack links and add experimental docs to flag-handler agent
osterman Jan 7, 2026
3ffdf49
docs: Fix stacks sidebar sorting and improve Experimental badge
osterman Jan 7, 2026
408d91c
docs: Fix toolchain label capitalization in CLI configuration
osterman Jan 7, 2026
32421ed
docs: Fix toolchain subsection label capitalization
osterman Jan 7, 2026
e1bc7cf
docs: Fix Registry label capitalization in toolchain commands
osterman Jan 7, 2026
39e0455
refactor: Reduce flag-handler agent file size to meet limit
osterman Jan 7, 2026
953adcc
test: Regenerate CLI help snapshots
osterman Jan 7, 2026
45bf7a4
test: Regenerate snapshots for experimental settings
osterman Jan 7, 2026
0026c0b
feat(website): Add automatic 'Unreleased' badge for documentation pages
osterman Jan 7, 2026
4b98816
style(website): Update Unreleased badge to match Experimental style
osterman Jan 7, 2026
df475a3
feat(website): Add /unreleased page with index of unreleased document…
osterman Jan 7, 2026
f8cc60b
feat(website): Add confetti celebration when all docs are released
osterman Jan 7, 2026
fdf1b7f
docs: Add missing descriptions to terraform command docs
osterman Jan 7, 2026
7d42150
fix(website): Address CodeRabbit review feedback for unreleased badges
osterman Jan 7, 2026
37c917e
test: Regenerate CLI help snapshots for experimental features
osterman Jan 7, 2026
656ac7f
Merge branch 'main' into osterman/experimental-features
osterman Jan 8, 2026
590fd5a
Merge remote-tracking branch 'origin/main' into osterman/experimental…
osterman Jan 8, 2026
5c62a25
docs(roadmap): Add PR number to experimental feature controls milestone
osterman Jan 8, 2026
b893b8a
test: Add tests to increase code coverage
osterman Jan 8, 2026
6b533bf
test: Add comprehensive tests for root.go and help_template.go
osterman Jan 8, 2026
41eb843
test: Add more tests to increase patch coverage
osterman Jan 9, 2026
769d302
test: Add coverage tests for pkg/ui/formatter fallback and error paths
osterman Jan 9, 2026
111bf96
test: Add coverage for experimental command mode switch cases
osterman Jan 9, 2026
19b550f
Merge remote-tracking branch 'origin/main' into osterman/experimental…
osterman Jan 10, 2026
ace92a9
Merge remote-tracking branch 'origin/main' into osterman/experimental…
osterman Jan 10, 2026
3b23784
fix(roadmap): correct experimental feature controls status to in-prog…
osterman Jan 10, 2026
1c2b16d
test: regenerate snapshots for experimental feature changes
osterman Jan 10, 2026
2e5c112
test: regenerate terraform --help snapshot
osterman Jan 10, 2026
f307c11
Merge remote-tracking branch 'origin/main' into osterman/experimental…
osterman Jan 14, 2026
4a124bf
Merge remote-tracking branch 'origin/main' into osterman/experimental…
osterman Jan 14, 2026
0785a1d
Merge remote-tracking branch 'origin/main' into osterman/experimental…
osterman Jan 15, 2026
d892b89
Merge branch 'main' into osterman/experimental-features
aknysh Jan 16, 2026
70c941f
test: Regenerate snapshots for experimental badges
aknysh Jan 16, 2026
b9c8070
Merge branch 'main' into osterman/experimental-features
aknysh Jan 16, 2026
372405c
- PRD environment variable fixed (ATMOS_EXPERIMENTAL)
aknysh Jan 16, 2026
8f5c669
add tests
aknysh Jan 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
112 changes: 61 additions & 51 deletions .claude/agents/flag-handler.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,8 @@ description: >-
- --check, --format, --stack, or any flag name discussions
- Flag improvements, flag refactoring, flag migration
- Troubleshooting flags, flag issues, flag errors
- experimental, IsExperimental, experimental commands, ATMOS_EXPERIMENTAL
- settings.experimental, experimental feature handling

**CRITICAL: pkg/flags/ is FULLY IMPLEMENTED. This is NOT future architecture.**

Expand Down Expand Up @@ -563,6 +565,60 @@ type global.Flags struct {
}
```

## Experimental Feature Handling

Commands can be marked as experimental by implementing `IsExperimental() bool`:

```go
// IsExperimental returns whether this command is experimental.
func (t *MyCommandProvider) IsExperimental() bool {
return true
}
```

For subcommands, use the `experimental` annotation:

```go
mySubCmd := &cobra.Command{
Use: "affected",
Annotations: map[string]string{
"experimental": "true",
},
}
```

### Experimental Configuration

Users configure experimental feature handling via `settings.experimental` in `atmos.yaml`:

```yaml
settings:
# Control experimental feature handling
# Values: "silence", "disable", "warn" (default), "error"
experimental: warn
```

| Mode | Behavior |
|------|----------|
| `silence` | Run without any notification |
| `warn` | Show notification, then continue (default) |
| `error` | Show notification and exit with error |
| `disable` | Block experimental commands entirely |

Environment variable: `ATMOS_EXPERIMENTAL`

### How It Works

1. Commands declare experimental status via `IsExperimental() bool` method
2. Root command's `PersistentPreRunE` checks if any command in the tree is experimental
3. Based on `settings.experimental` value, it either:
- `silence`: Does nothing
- `warn`: Shows notification via `ui.Experimental()`, continues
- `error`: Shows notification, exits with error
- `disable`: Exits with error (no notification)

See [/experimental](https://atmos.tools/experimental) for full documentation.

Embed in options struct:

```go
Expand Down Expand Up @@ -684,11 +740,7 @@ When implementing complex commands, coordinate with other agents:

## Resources

**Primary PRDs:**
- `docs/prd/flag-handling/unified-flag-parsing.md` - Unified flag parsing architecture
- `docs/prd/flag-handling/strongly-typed-builder-pattern.md` - Builder pattern implementation
- `docs/prd/flag-handling/global-flags-pattern.md` - Global flags design
- `docs/prd/command-registry-pattern.md` - Command registry architecture
**PRDs:** `docs/prd/flag-handling/` (unified-flag-parsing.md, strongly-typed-builder-pattern.md, global-flags-pattern.md), `docs/prd/command-registry-pattern.md`

**Additional PRDs:**
- `docs/prd/flag-handling/README.md` - Overview of flag handling architecture
Expand All @@ -707,52 +759,10 @@ When implementing complex commands, coordinate with other agents:

## Key Principle

**Everything goes through the command registry.** There is no direct flag parsing - all commands MUST implement CommandProvider and register with `internal.Register()`.
**Everything goes through the command registry.** All commands MUST implement CommandProvider and register with `internal.Register()`.

## Self-Maintenance

This agent actively monitors and updates itself when dependencies change.

**Dependencies to monitor:**
- `docs/prd/flag-handling/unified-flag-parsing.md` - Core flag parsing architecture
- `docs/prd/flag-handling/strongly-typed-builder-pattern.md` - Builder pattern implementation
- `docs/prd/flag-handling/global-flags-pattern.md` - Global flags design
- `docs/prd/command-registry-pattern.md` - Command registry architecture
- `CLAUDE.md` - Core development patterns
- `cmd/internal/command.go` - CommandProvider interface definition
- `pkg/flags/builder.go` - Builder interface definition

**Update triggers:**
1. **PRD updated** - When flag-handling PRDs or command-registry PRD modified
2. **Interface changes** - When CommandProvider or Builder interfaces evolve
3. **Pattern maturity** - When new flag patterns emerge in implementations
4. **Invocation unclear** - When agent isn't triggered appropriately

**Update process:**
1. Detect change: `git log -1 --format="%ai" docs/prd/flag-handling/*.md`
2. Read updated documentation
3. Draft proposed changes to agent
4. **Present changes to user for confirmation**
5. Upon approval, apply updates
6. Test with sample command implementation
7. Commit with descriptive message referencing PRD version

**Self-check before each invocation:**
- Read latest version of unified-flag-parsing.md
- Verify CommandProvider interface hasn't changed
- Check for new flag patterns in recent command implementations

## Relevant PRDs

This agent implements patterns from:

- `docs/prd/flag-handling/unified-flag-parsing.md` - Unified flag parsing architecture
- `docs/prd/flag-handling/strongly-typed-builder-pattern.md` - Builder pattern
- `docs/prd/flag-handling/global-flags-pattern.md` - Global flags design
- `docs/prd/command-registry-pattern.md` - Command registry

**Before implementing:**
1. Check PRD modification date: `git log -1 --format="%ai" docs/prd/flag-handling/unified-flag-parsing.md`
2. Compare with last sync date
3. If newer, read full PRD before proceeding
4. Update this agent if patterns have changed
**Monitor:** `docs/prd/flag-handling/*.md`, `docs/prd/command-registry-pattern.md`, `cmd/internal/command.go`, `pkg/flags/builder.go`

**Before each invocation:** Check PRD dates with `git log -1 --format="%ai" docs/prd/flag-handling/unified-flag-parsing.md`. If newer than last sync, read full PRD and update agent if patterns changed.
3 changes: 3 additions & 0 deletions .github/settings.yml
Original file line number Diff line number Diff line change
Expand Up @@ -37,3 +37,6 @@ labels:
- name: go
color: '#00add8'
description: Pull requests that update Go code
- name: experimental
color: '#ff9800'
description: Feature is experimental and still being refined
5 changes: 5 additions & 0 deletions cmd/about/about.go
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,8 @@ func (a *AboutCommandProvider) GetCompatibilityFlags() map[string]compat.Compati
func (a *AboutCommandProvider) GetAliases() []internal.CommandAlias {
return nil
}

// IsExperimental returns whether this command is experimental.
func (a *AboutCommandProvider) IsExperimental() bool {
return false
}
6 changes: 6 additions & 0 deletions cmd/devcontainer/devcontainer.go
Original file line number Diff line number Diff line change
Expand Up @@ -77,3 +77,9 @@ func (d *DevcontainerCommandProvider) GetCompatibilityFlags() map[string]compat.
func (d *DevcontainerCommandProvider) GetAliases() []internal.CommandAlias {
return nil
}

// IsExperimental returns whether this command is experimental.
// Devcontainer support is currently experimental.
func (d *DevcontainerCommandProvider) IsExperimental() bool {
return true
}
5 changes: 5 additions & 0 deletions cmd/env/env.go
Original file line number Diff line number Diff line change
Expand Up @@ -283,3 +283,8 @@ func (e *EnvCommandProvider) GetCompatibilityFlags() map[string]compat.Compatibi
func (e *EnvCommandProvider) GetAliases() []internal.CommandAlias {
return nil
}

// IsExperimental returns whether this command is experimental.
func (e *EnvCommandProvider) IsExperimental() bool {
return false
}
86 changes: 56 additions & 30 deletions cmd/help_template.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,17 @@ const (
valueZero = "0"
)

// Command annotation constants.
const (
annotationExperimental = "experimental"
annotationValueTrue = "true"
)

// isExperimentalCommand checks if a command has the experimental annotation.
func isExperimentalCommand(cmd *cobra.Command) bool {
return cmd.Annotations != nil && cmd.Annotations[annotationExperimental] == annotationValueTrue
}

// colorConfig holds the color detection and environment variable configuration.
type colorConfig struct {
forceColor bool
Expand Down Expand Up @@ -358,6 +369,13 @@ func printLogoAndVersion(w io.Writer, styles *helpStyles) {
func printDescription(w io.Writer, cmd *cobra.Command, styles *helpStyles) {
defer perf.Track(nil, "cmd.printDescription")()

// Print experimental badge if command is experimental.
if isExperimentalCommand(cmd) {
badge := ui.FormatExperimentalBadge()
fmt.Fprintln(w, badge)
fmt.Fprintln(w)
}

var desc string
switch {
case cmd.Long != "":
Expand Down Expand Up @@ -496,82 +514,86 @@ func isConfigAlias(cmd *cobra.Command) bool {
}

// calculateCommandWidth calculates the display width of a command name including type suffix.
func calculateCommandWidth(cmd *cobra.Command) int {
// If parentExperimental is true, experimental badges won't be shown on subcommands.
func calculateCommandWidth(cmd *cobra.Command, parentExperimental bool) int {
width := len(cmd.Name())
if cmd.HasAvailableSubCommands() {
width += len(" [command]")
}
// Account for experimental badge if present and parent is not already experimental.
if isExperimentalCommand(cmd) && !parentExperimental {
width += len(" [EXPERIMENTAL]")
}
return width
}

// calculateMaxCommandWidth finds the maximum command name width for alignment.
// Config aliases are excluded from this calculation since they're shown in a separate section.
func calculateMaxCommandWidth(commands []*cobra.Command) int {
// If parentExperimental is true, experimental badges won't be included in width calculations.
func calculateMaxCommandWidth(commands []*cobra.Command, parentExperimental bool) int {
maxWidth := 0
for _, c := range commands {
if !isCommandAvailable(c) || isConfigAlias(c) {
continue
}
width := calculateCommandWidth(c)
width := calculateCommandWidth(c, parentExperimental)
if width > maxWidth {
maxWidth = width
}
}
return maxWidth
}

// getExperimentalBadge returns the styled and plain badge strings if command is experimental.
// Returns empty strings if parent is already experimental or command is not experimental.
func getExperimentalBadge(cmd *cobra.Command, parentExperimental bool) (styled, plain string) {
if isExperimentalCommand(cmd) && !parentExperimental {
return " " + ui.FormatExperimentalBadge(), " [EXPERIMENTAL]"
}
return "", ""
}

// formatCommandLine formats a single command line with proper padding and styling.
func formatCommandLine(ctx *helpRenderContext, cmd *cobra.Command, maxWidth int, mdRenderer *markdown.Renderer) {
func formatCommandLine(ctx *helpRenderContext, cmd *cobra.Command, maxWidth int, mdRenderer *markdown.Renderer, parentExperimental bool) {
cmdName := cmd.Name()
cmdTypePlain := ""
cmdTypeStyled := ""
cmdTypePlain, cmdTypeStyled := "", ""
if cmd.HasAvailableSubCommands() {
cmdTypePlain = " [command]"
cmdTypeStyled = " " + ctx.styles.flagName.Render("[command]")
}

padding := maxWidth - len(cmdName) - len(cmdTypePlain)
experimentalBadge, experimentalBadgePlain := getExperimentalBadge(cmd, parentExperimental)
padding := maxWidth - len(cmdName) - len(cmdTypePlain) - len(experimentalBadgePlain)

// Calculate where the description starts (left pad + command name + padding + spacing)
// Calculate description column position and width.
descColStart := commandListLeftPad + maxWidth + commandDescriptionSpacing

// Get terminal width and calculate available width for description
termWidth := getTerminalWidth()
descWidth := termWidth - descColStart
descWidth := getTerminalWidth() - descColStart
if descWidth < minDescriptionWidth {
descWidth = minDescriptionWidth
}

// Write command name and badges.
fmt.Fprint(ctx.writer, strings.Repeat(spaceChar, commandListLeftPad))
fmt.Fprint(ctx.writer, ctx.styles.commandName.Render(cmdName))
fmt.Fprint(ctx.writer, cmdTypeStyled)
fmt.Fprint(ctx.writer, strings.Repeat(spaceChar, padding))
fmt.Fprint(ctx.writer, strings.Repeat(spaceChar, commandDescriptionSpacing))
fmt.Fprint(ctx.writer, experimentalBadge)
fmt.Fprint(ctx.writer, strings.Repeat(spaceChar, padding+commandDescriptionSpacing))

// Wrap plain text first, then render markdown without additional wrapping.
// This matches the approach used by flag description rendering.
// Render description with word wrap and markdown.
wrapped := wordwrap.String(cmd.Short, descWidth)

if mdRenderer != nil {
rendered, err := mdRenderer.RenderWithoutWordWrap(wrapped)
if err == nil {
if rendered, err := mdRenderer.RenderWithoutWordWrap(wrapped); err == nil {
wrapped = strings.TrimSpace(rendered)
}
}

// Write description lines.
lines := strings.Split(wrapped, "\n")

// Print first line (already positioned)
if len(lines) > 0 {
fmt.Fprintf(ctx.writer, "%s\n", ctx.styles.commandDesc.Render(lines[0]))
}

// Print continuation lines with proper indentation
if len(lines) > 1 {
indentStr := strings.Repeat(spaceChar, descColStart)
for i := 1; i < len(lines); i++ {
fmt.Fprintf(ctx.writer, "%s%s\n", indentStr, ctx.styles.commandDesc.Render(lines[i]))
}
for i := 1; i < len(lines); i++ {
fmt.Fprintf(ctx.writer, "%s%s\n", strings.Repeat(spaceChar, descColStart), ctx.styles.commandDesc.Render(lines[i]))
}
}

Expand All @@ -586,7 +608,11 @@ func printAvailableCommands(ctx *helpRenderContext, cmd *cobra.Command) {
fmt.Fprintln(ctx.writer, ctx.styles.heading.Render("AVAILABLE COMMANDS"))
fmt.Fprintln(ctx.writer)

maxCmdWidth := calculateMaxCommandWidth(cmd.Commands())
// Check if the parent command is experimental.
// If so, subcommands don't need to repeat the badge since it's shown at the top.
parentExperimental := isExperimentalCommand(cmd)

maxCmdWidth := calculateMaxCommandWidth(cmd.Commands(), parentExperimental)

// Create markdown renderer for command descriptions (same approach as flag rendering).
var mdRenderer *markdown.Renderer
Expand All @@ -598,7 +624,7 @@ func printAvailableCommands(ctx *helpRenderContext, cmd *cobra.Command) {
if !isCommandAvailable(c) || isConfigAlias(c) {
continue
}
formatCommandLine(ctx, c, maxCmdWidth, mdRenderer)
formatCommandLine(ctx, c, maxCmdWidth, mdRenderer, parentExperimental)
}
fmt.Fprintln(ctx.writer)
}
Expand Down
Loading
Loading