Skip to content

Adopt Lipgloss tree rendering for MCP hierarchy and status dependencies#33276

Merged
pelikhan merged 9 commits into
mainfrom
copilot/use-lipgloss-tree-for-hierarchy
May 19, 2026
Merged

Adopt Lipgloss tree rendering for MCP hierarchy and status dependencies#33276
pelikhan merged 9 commits into
mainfrom
copilot/use-lipgloss-tree-for-hierarchy

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented May 19, 2026

Current CLI output mixed manual hierarchy formatting with table-only views, making nested relationships harder to read. This change introduces lipgloss tree rendering for MCP inspection hierarchy and workflow dependency visibility in status output.

  • MCP inspect: structured hierarchy via Lipgloss Tree

    • Added a tree-rendered summary in mcp inspect showing:
      • workflow identity
      • resolved engine
      • MCP servers (name + type)
    • Uses rounded enumerators and existing tree styles (TreeEnumerator, TreeNode) for consistent terminal rendering.
  • Status command: dependency tree in verbose mode

    • Extended workflow status data with derived dependency metadata.
    • Extracts dependencies from:
      • frontmatter imports (string/list/object forms)
      • inline directives (@include, @import)
    • Normalizes (#section stripping), deduplicates, and sorts dependencies before rendering.
    • Renders a workflow dependency tree when verbose text output is used.
  • Supporting helpers and coverage

    • Added helper functions for engine resolution, dependency extraction/normalization, and tree composition.
    • Added focused unit tests for:
      • MCP hierarchy tree content
      • dependency extraction behavior
      • empty/non-empty dependency tree rendering
hierarchy := tree.
    Root("Workflow: " + workflowLabel).
    Child("Engine: " + resolveWorkflowEngineID(workflowData)).
    Child(
        tree.Root("MCP Servers").
            Child("github (stdio)").
            Child("playwright (http)"),
    ).
    Enumerator(tree.RoundedEnumerator).
    EnumeratorStyle(styles.TreeEnumerator).
    ItemStyle(styles.TreeNode)

Copilot AI and others added 3 commits May 19, 2026 10:16
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
@pelikhan pelikhan marked this pull request as ready for review May 19, 2026 10:42
Copilot AI review requested due to automatic review settings May 19, 2026 10:42
Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Introduces Lipgloss tree rendering to make nested CLI output easier to read, adding (1) an MCP inspection hierarchy summary and (2) a verbose workflow dependency tree in status, plus accompanying unit tests.

Changes:

  • Add a tree-rendered MCP “server hierarchy” summary to mcp inspect.
  • Extend status to derive workflow dependencies (frontmatter + inline directives) and render them as a dependency tree in verbose mode.
  • Add focused unit tests for MCP tree content and dependency extraction/tree rendering; fix a Datadog env var reference in a locked workflow.
Show a summary per file
File Description
pkg/cli/status_command.go Adds dependency extraction/normalization and verbose dependency-tree rendering; extends WorkflowStatus with derived dependencies.
pkg/cli/status_dependency_tree_test.go Adds unit tests for dependency extraction and dependency tree rendering behavior.
pkg/cli/mcp_inspect.go Adds Lipgloss-based hierarchy tree rendering for MCP inspection output and engine resolution helper.
pkg/cli/mcp_inspect_tree_test.go Adds unit tests for MCP hierarchy tree output and engine ID resolution.
.github/workflows/smoke-otel-backends.lock.yml Updates Datadog header env var interpolation to use DD_APPLICATION_KEY.

Copilot's findings

Tip

Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

  • Files reviewed: 5/5 changed files
  • Comments generated: 2

Comment thread pkg/cli/status_command.go Outdated
Comment on lines +343 to +358
if frontmatter != nil {
switch imports := frontmatter["imports"].(type) {
case string:
addDependency(imports)
case []any:
for _, item := range imports {
switch importItem := item.(type) {
case string:
addDependency(importItem)
case map[string]any:
if uses, ok := importItem["uses"].(string); ok {
addDependency(uses)
}
}
}
}
Comment thread pkg/cli/mcp_inspect.go
Comment on lines +196 to +199
serversTree := tree.Root("MCP Servers")
for _, config := range mcpConfigs {
serversTree.Child(fmt.Sprintf("%s (%s)", config.Name, config.Type))
}
Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

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

Skills-Based Review 🧠

Applied /tdd and /zoom-out based on this being a refactor + new feature (tree rendering + dependency extraction).

Key Themes

Test Coverage Gaps — The PR includes tests but misses important edge cases:

  • extractWorkflowDependencies handles complex union types (string | array | map) but tests only cover the happy path
  • resolveWorkflowEngineID has an untested fallback edge case (non-nil EngineConfig with empty ID)
  • Missing tests for error scenarios in dependency extraction

Silent Error HandlingfindIncludesInContent errors are swallowed without logging, creating an observability gap. Users won't know why their dependency tree is incomplete if inline directive parsing fails.

Implicit Design Decisions — The tree hierarchy rendering is conditionally suppressed when filters are applied, but this behavior isn't documented in code. Future maintainers may be confused.

Architectural Question — Fragment identifiers (#section) are stripped during normalization. This may be fine now, but if section-level dependency analysis becomes valuable later, the data will be lost.

Positive Highlights

  • Clean tree composition — Excellent use of Lipgloss tree builder API with method chaining
  • Consistent styling — Reuses existing styles.TreeEnumerator and styles.TreeNode for visual consistency
  • Proper separation — New functions are focused and single-purpose (resolveWorkflowEngineID, normalizeWorkflowDependency)
  • Thoughtful UX — Only showing hierarchy in unfiltered mode avoids visual clutter
  • Good test structure — Table-driven tests with descriptive names in mcp_inspect_tree_test.go

Verdict

Comments only — no blocking issues. The changes are solid; suggestions focus on hardening edge cases and improving observability for future maintainers.


Learn more about these skills:

  • /tdd — Test-driven development principles
  • /zoom-out — Architectural perspective and broader context

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · ● 4.4M

Comment thread pkg/cli/status_command.go
return isCompiledUpToDateWithCache(workflowPath, lockFilePath, parser.NewImportCache(""))
}

func extractWorkflowDependencies(content string, frontmatter map[string]any) []string {
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.

[/tdd] The extractWorkflowDependencies function has complex branching logic (string vs array vs map imports, content parsing) but isn't directly tested with unit tests. The test in status_dependency_tree_test.go covers the happy path but doesn't validate edge cases like:

  • Malformed frontmatter imports
  • Empty strings in import arrays
  • findIncludesInContent returning an error
  • Duplicate dependencies from different sources

Consider adding a dedicated test file extract_workflow_dependencies_test.go with table-driven tests:

func TestExtractWorkflowDependencies_EdgeCases(t *testing.T) {
    tests := []struct {
        name        string
        content     string
        frontmatter map[string]any
        want        []string
    }{
        {
            name:        "handles empty import strings",
            frontmatter: map[string]any{"imports": []any{"", "  ", "valid.md"}},
            want:        []string{"valid.md"},
        },
        {
            name:        "deduplicates across sources",
            frontmatter: map[string]any{"imports": "base.md#section"},
            content:     "`@include` base.md\n",
            want:        []string{"base.md"},
        },
    }
    // ...
}

This would catch future regressions when the import parsing logic evolves.

Comment thread pkg/cli/status_command.go
}

includes, err := findIncludesInContent(content)
if err == nil {
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.

[/zoom-out] Silent error swallowing here: findIncludesInContent errors are ignored with if err == nil. This pattern hides potential parsing failures from users.

From the broader architecture perspective, this creates an inconsistency: frontmatter import parsing failures would likely surface elsewhere, but inline directive failures are silently dropped. Users might see incomplete dependency trees without understanding why.

Consider at minimum logging the error:

includes, err := findIncludesInContent(content)
if err != nil {
    statusLog.Printf("failed to extract includes from content: %v", err)
} else {
    for _, include := range includes {
        addDependency(include)
    }
}

This maintains the graceful degradation while providing observability.

Comment thread pkg/cli/mcp_inspect.go
String()
}

func resolveWorkflowEngineID(workflowData *workflow.WorkflowData) string {
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.

[/tdd] The resolveWorkflowEngineID function has a clear fallback chain but the test only validates one path through the logic per test case. The function returns "unknown" in two scenarios:

  1. workflowData == nil
  2. workflowData.EngineConfig == nil && workflowData.AI == ""

But there's an untested edge case: what if workflowData.EngineConfig is non-nil but ID is empty string? Add a test case:

{
    name: "empty engine config id falls back to ai",
    workflowData: &workflow.WorkflowData{
        EngineConfig: &workflow.EngineConfig{ID: ""},
        AI:           "claude",
    },
    want: "claude",
},

This documents the expected behavior and prevents future refactors from accidentally breaking the fallback logic.

Comment thread pkg/cli/status_command.go
return dependencies
}

func normalizeWorkflowDependency(dependency string) string {
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.

[/zoom-out] The normalizeWorkflowDependency function uses strings.Cut to strip fragment identifiers (#section), but there's an architectural question: should fragment information be preserved somewhere?

In a larger codebase, workflow dependencies on specific sections might be valuable for impact analysis ("which workflows depend on section X of base.md?"). The current implementation discards that granularity.

If section-level dependencies aren't needed, this is fine. But if they matter for future features (e.g., gh aw dependencies --section base.md#helpers), consider preserving the raw dependency alongside the normalized one:

type WorkflowDependency struct {
    Path     string  // normalized: "base.md"
    Fragment string  // optional: "helpers"
}

No immediate action needed, but document this design decision for future maintainers.

Comment thread pkg/cli/mcp_inspect.go
} else {
fmt.Fprintln(os.Stderr, console.FormatInfoMessage(fmt.Sprintf("Found %d MCP server(s) to inspect", len(mcpConfigs))))
}
if toolFilter == "" {
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.

[/zoom-out] Conditional rendering based on toolFilter == "" creates an implicit coupling: the tree hierarchy is only shown when no tool filter is applied.

This makes sense (filtered views shouldn't show full hierarchy), but the logic is implicit. A future maintainer might wonder why the hierarchy disappeared when they added a tool filter.

Consider adding a comment explaining the design decision:

// Only render hierarchy in unfiltered mode - filtered views focus on specific tools
if toolFilter == "" {
    if hierarchy := renderMCPInspectionTree(...

Or extract to a named boolean:

showHierarchy := toolFilter == "" && serverFilter == ""
if showHierarchy {
    // ...
}

Generated by the Design Decision Gate workflow to record the
architectural decision to adopt charm.land/lipgloss/v2/tree for
hierarchical CLI output in mcp inspect and status commands.

This is a DRAFT — PR author must review and finalize before merge.
@github-actions
Copy link
Copy Markdown
Contributor

Commit pushed: f48dca0

🏗️ ADR gate enforced by Design Decision Gate 🏗️ · ● 5.4M

@github-actions
Copy link
Copy Markdown
Contributor

🏗️ Design Decision Gate — ADR Required

This PR makes significant changes to core business logic (277 new lines in pkg/cli/) but does not have a linked Architecture Decision Record (ADR).

AI has analyzed the PR diff and generated a draft ADR to help you get started:

📄 Draft ADR: docs/adr/33276-adopt-lipgloss-tree-for-cli-hierarchies.md

What to do next

  1. Review the draft ADR committed to your branch — it was generated from the PR diff and frames the decision as "adopt charm.land/lipgloss/v2/tree as the canonical primitive for hierarchical CLI output in pkg/cli."
  2. Complete the missing sections — confirm the rationale matches the team's intent, refine the alternatives (especially Alternative 2 if a custom tree printer was considered more seriously), and adjust the normative requirements if any MUST is too strict.
  3. Commit the finalized ADR to docs/adr/ on your branch (you can amend the existing file).
  4. Reference the ADR in this PR body by adding a line such as:

    ADR: ADR-33276: Adopt Lipgloss Tree Rendering for CLI Hierarchies

Once an ADR is linked in the PR body, this gate will re-run and verify the implementation matches the decision.

Why ADRs Matter

"AI made me procrastinate on key design decisions. Because refactoring was cheap, I could always say 'I'll deal with this later.' Deferring decisions corroded my ability to think clearly."

ADRs create a searchable, permanent record of why the codebase looks the way it does. Future contributors (and your future self) will thank you.

📋 Michael Nygard ADR Format Reference

An ADR must contain these four sections to be considered complete:

  • Context — What is the problem? What forces are at play?
  • Decision — What did you decide? Why?
  • Alternatives Considered — What else could have been done?
  • Consequences — What are the trade-offs (positive and negative)?

This repository's ADRs are stored in docs/adr/ as Markdown files numbered by PR number (e.g., 33276-...md for this PR) and include a Part 2 with RFC 2119 normative requirements.

🔎 Why this PR triggered the gate
  • has_implementation_label: false
  • default_business_additions: 277 (threshold: 100) — additions in pkg/cli/ exceeded the default volume threshold
  • No existing docs/adr/33276-*.md file and no ADR link found in the PR body

🔒 This PR cannot merge until an ADR is linked in the PR body.

References:

🏗️ ADR gate enforced by Design Decision Gate 🏗️ · ● 5.4M ·

@pelikhan
Copy link
Copy Markdown
Collaborator

@copilot review all comments

@github-actions
Copy link
Copy Markdown
Contributor

🧪 Test Quality Sentinel Report

Test Quality Score: 82/100

Excellent test quality

Metric Value
New/modified tests analyzed 5
✅ Design tests (behavioral contracts) 5 (100%)
⚠️ Implementation tests (low value) 0 (0%)
Tests with error/edge cases 2 (40%)
Duplicate test clusters 0
Test inflation detected No
🚨 Coding-guideline violations 3 violations (missing build tags + missing assertion messages)

Test Classification Details

Test File Classification Issues Detected
TestRenderMCPInspectionTree pkg/cli/mcp_inspect_tree_test.go:12 ✅ Design No error/edge cases; missing assertion messages
TestResolveWorkflowEngineID pkg/cli/mcp_inspect_tree_test.go:38 ✅ Design Missing assertion message
TestExtractWorkflowDependencies pkg/cli/status_dependency_tree_test.go:9 ✅ Design No error/edge cases; missing assertion messages
TestRenderWorkflowDependencyTree pkg/cli/status_dependency_tree_test.go:34 ✅ Design No error/edge cases; missing assertion message
TestRenderWorkflowDependencyTree_Empty pkg/cli/status_dependency_tree_test.go:49 ✅ Design Missing assertion message

Flagged Tests — Requires Review

🚨 Missing Build Tags (Coding Guideline Violation)

Both new test files are missing the mandatory //go:build directive on line 1:

  • pkg/cli/mcp_inspect_tree_test.go - Must begin with //go:build !integration
  • pkg/cli/status_dependency_tree_test.go - Must begin with //go:build !integration

Required fix: Add //go:build !integration as the very first line of each test file, followed by a blank line, then the package cli declaration.

Why this matters: The build tag determines which test suite (unit vs integration) the tests belong to. Without it, the tests cannot be properly categorized and may be skipped by CI.

🚨 Missing Assertion Messages (Coding Guideline Violation)

All 5 test functions use bare assertions without descriptive message arguments. The project guideline requires every testify assertion to include a descriptive message.

Examples from the code:

// ❌ INCORRECT - no message
assert.Contains(t, result, part)
assert.Equal(t, tt.want, resolveWorkflowEngineID(tt.workflowData))
assert.Len(t, got, len(want))

// ✅ CORRECT - includes descriptive context
assert.Contains(t, result, part, "tree output should include expected hierarchy node")
assert.Equal(t, tt.want, resolveWorkflowEngineID(tt.workflowData), "engine ID resolution should match expected fallback behavior")
assert.Len(t, got, len(want), "dependency count should match expected unique set")

Required fix: Add descriptive message arguments to all assertion calls explaining what the assertion validates in business terms.

⚠️ TestRenderMCPInspectionTree (pkg/cli/mcp_inspect_tree_test.go:12)

Classification: Design test

Issue: Only tests happy path with well-formed inputs. No error or edge case coverage.

What design invariant does this test enforce? The tree rendering function must include expected hierarchical nodes (workflow name, engine ID, MCP server details) in its output.

What would break if deleted? A regression where the tree renderer omits required nodes or produces malformed output would go undetected.

Suggested improvement: Add test cases for error/edge scenarios:

  • Nil workflowData input
  • Empty mcpConfigs slice
  • Workflow with missing EngineConfig
  • MCP configs with empty/missing Name or Type fields

Example:

func TestRenderMCPInspectionTree_EmptyConfigs(t *testing.T) {
    workflowData := &workflow.WorkflowData{WorkflowID: "test"}
    result := renderMCPInspectionTree("/tmp/test.md", workflowData, []parser.RegistryMCPServerConfig{})
    assert.Contains(t, result, "test", "should include workflow name even with no MCP servers")
    assert.NotContains(t, result, "MCP Servers", "should not show MCP section when configs are empty")
}

⚠️ TestExtractWorkflowDependencies (pkg/cli/status_dependency_tree_test.go:9)

Classification: Design test

Issue: Only tests happy path. No error handling for malformed inputs.

What design invariant does this test enforce? The dependency extractor must find all dependencies from frontmatter imports and content directives, normalize them (remove fragments), deduplicate, and sort them.

What would break if deleted? A bug where dependencies are missed, duplicated, or returned in wrong order would go undetected.

Suggested improvement: Add test cases for:

  • Malformed frontmatter (non-string imports, invalid map structures)
  • Invalid import syntax in content (@include without path, @import with special chars)
  • Circular dependencies (A imports B, B imports A)
  • Empty frontmatter and content

Example:

func TestExtractWorkflowDependencies_InvalidFrontmatter(t *testing.T) {
    frontmatter := map[string]any{
        "imports": "not-a-slice",  // Invalid type
    }
    got := extractWorkflowDependencies("", frontmatter)
    assert.Empty(t, got, "should return empty slice when frontmatter imports are malformed")
}

⚠️ TestRenderWorkflowDependencyTree (pkg/cli/status_dependency_tree_test.go:34)

Classification: Design test

Issue: Only tests happy path. No edge case coverage.

What design invariant does this test enforce? The dependency tree renderer must include workflow name and all dependency paths in the output.

What would break if deleted? A regression where dependencies are omitted or tree structure is broken would go undetected.

Suggested improvement: Add test cases for:

  • Nil statuses input
  • Workflow with malformed dependency paths
  • Multiple workflows with overlapping dependencies

Example:

func TestRenderWorkflowDependencyTree_NilInput(t *testing.T) {
    result := renderWorkflowDependencyTree(nil)
    assert.Empty(t, result, "should return empty string for nil input")
}

Language Support

Tests analyzed:

  • 🐹 Go (*_test.go): 5 tests — unit (//go:build !integration) [MISSING BUILD TAGS]

Verdict

Check failed. Coding guideline violations detected:

  1. Missing build tags (2 files): Both test files lack the mandatory //go:build !integration directive on line 1
  2. Missing assertion messages (5 tests): All assertions use bare form without descriptive message arguments

Please fix these violations before merging. The build tag issue will cause CI failures. The missing assertion messages reduce test maintainability and debugging clarity.


📖 Understanding Test Classifications

Design Tests (High Value) verify what the system does:

  • Assert on observable outputs, return values, or state changes
  • Cover error paths and boundary conditions
  • Would catch a behavioral regression if deleted
  • Remain valid even after internal refactoring

Implementation Tests (Low Value) verify how the system does it:

  • Assert on internal function calls (mocking internals)
  • Only test the happy path with typical inputs
  • Break during legitimate refactoring even when behavior is correct
  • Give false assurance: they pass even when the system is wrong

Goal: Shift toward tests that describe the system's behavioral contract — the promises it makes to its users and collaborators.

🧪 Test quality analysis by Test Quality Sentinel · ● 6.2M ·

Copy link
Copy Markdown
Contributor

@github-actions github-actions Bot left a comment

Choose a reason for hiding this comment

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

❌ Test Quality Sentinel: Coding guideline violations detected. Both new test files are missing the mandatory //go:build !integration build tag on line 1, and all 5 tests use bare assertions without descriptive message arguments. Please fix these violations before merging — the build tag issue will cause CI failures.

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
…e order

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented May 19, 2026

@copilot review all comments

Addressed the review items in commit c1db3d0.

  • extractWorkflowDependencies now handles additional imports shapes, including []string, object entries with path/uses, and imports.aw object form.
  • renderMCPInspectionTree now sorts MCP servers by Name then Type to keep output deterministic.
  • Added tests covering imports.aw parsing and deterministic MCP tree ordering.

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copilot AI requested a review from pelikhan May 19, 2026 11:12
@pelikhan
Copy link
Copy Markdown
Collaborator

@copilot merge main and recompile fix conflicts

Copilot AI and others added 2 commits May 19, 2026 13:57
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
…tree-for-hierarchy

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented May 19, 2026

@copilot merge main and recompile fix conflicts

Merged main into this branch in commit 00c0f3a and recompiled successfully (go build ./cmd/gh-aw), with CLI tests also passing (go test ./pkg/cli). There were no merge conflicts to fix.

@pelikhan pelikhan merged commit 3e88a77 into main May 19, 2026
@pelikhan pelikhan deleted the copilot/use-lipgloss-tree-for-hierarchy branch May 19, 2026 15:44
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants