Skip to content

refactor(workflow): decompose Claude allowed-tools assembly to reduce function complexity#35812

Merged
pelikhan merged 8 commits into
mainfrom
copilot/lint-monster-refactor-function-complexity
May 30, 2026
Merged

refactor(workflow): decompose Claude allowed-tools assembly to reduce function complexity#35812
pelikhan merged 8 commits into
mainfrom
copilot/lint-monster-refactor-function-complexity

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented May 29, 2026

This issue targets large-function complexity across workflow/CLI codepaths. This PR addresses one of the highest-impact workflow paths by decomposing Claude allowed-tools generation into focused helpers without changing behavior.

  • Scope reduced in claude_tools.go

    • Split computeAllowedClaudeToolsString into composable helpers for:
      • neutral→Claude tool preparation/defaulting
      • Claude allowed extraction
      • top-level MCP/cache-memory permission expansion
      • sandbox writable-path normalization
      • safe-outputs and mcp-scripts grants
      • final de-duplication
    • Kept output contract intact (--allowed-tools string remains sorted/deduped).
  • Behavior-preserving helper extraction

    • Isolated wildcard and path normalization logic into testable units:
      • bash wildcard detection (*, :*)
      • sandbox writable path normalization to path patterns
  • Focused unit coverage for extracted logic

    • Added helper-level tests to lock semantics around wildcard/path normalization and prevent regressions during subsequent complexity reductions.

Example of the extracted pattern:

tools = e.prepareClaudeToolsForAllowedList(tools)
allowedTools := collectClaudeAllowedTools(tools)
allowedTools = appendTopLevelClaudeTools(allowedTools, tools, cacheMemoryConfig)
allowedTools = appendSandboxWritableTools(allowedTools, sandboxConfig)
allowedTools = appendSafeOutputsTools(allowedTools, safeOutputs)
allowedTools = appendMCPScriptsTools(allowedTools, mcpScripts)
allowedTools = dedupeAllowedTools(allowedTools)

Copilot AI and others added 3 commits May 29, 2026 23:08
Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com>
Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com>
Co-authored-by: gh-aw-bot <259018956+gh-aw-bot@users.noreply.github.com>
Copilot AI changed the title [WIP] Refactor to reduce function complexity in workflow and CLI files refactor(workflow): decompose Claude allowed-tools assembly to reduce function complexity May 29, 2026
Copilot AI requested a review from gh-aw-bot May 29, 2026 23:19
@pelikhan pelikhan marked this pull request as ready for review May 29, 2026 23:25
Copilot AI review requested due to automatic review settings May 29, 2026 23:25
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

Refactors computeAllowedClaudeToolsString in pkg/workflow/claude_tools.go into a series of small, focused helper functions to reduce function complexity, with new helper-level unit tests added for the extracted wildcard-detection and sandbox-path-normalization logic. The public contract (--allowed-tools string, sorted and deduplicated) is preserved.

Changes:

  • Decomposed the monolithic computeAllowedClaudeToolsString into ~12 focused helpers (preparation, collection, top-level/MCP/cache-memory/sandbox/safe-outputs/mcp-scripts expansion, dedupe).
  • Extracted hasBashWildcard, isClaudeToolName, normalizeSandboxWritablePattern, getOrCreateToolMap, and appendIfMissing utilities, removing the prior goto-based control flow for Bash wildcard handling.
  • Added claude_tools_helpers_test.go covering hasBashWildcard and normalizeSandboxWritablePattern.
Show a summary per file
File Description
pkg/workflow/claude_tools.go Breaks computeAllowedClaudeToolsString into composable helpers while preserving the sorted/deduped output contract.
pkg/workflow/claude_tools_helpers_test.go New unit tests pinning down wildcard detection and sandbox-path normalization semantics.

Copilot's findings

Tip

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

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

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 29, 2026

🧠 Matt Pocock Skills Reviewer has completed the skills-based review. ✅

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 29, 2026

Design Decision Gate 🏗️ completed the design decision gate check.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 29, 2026

🧪 Test Quality Sentinel completed test quality analysis.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 29, 2026

PR Code Quality Reviewer completed the code quality review.

@pelikhan
Copy link
Copy Markdown
Collaborator

@github-actions
Copy link
Copy Markdown
Contributor

🧪 Test Quality Sentinel Report

Test Quality Score: 100/100 — Excellent

Analyzed 2 test(s): 2 design, 0 implementation, 0 guideline violation(s).

📊 Metrics & Test Classification (2 tests analyzed)
Metric Value
New/modified tests analyzed 2
✅ Design tests (behavioral contracts) 2 (100%)
⚠️ Implementation tests (low value) 0 (0%)
Tests with error/edge cases 2 (100%)
Duplicate test clusters 0
Test inflation detected No
🚨 Coding-guideline violations 0

Test Classification Details

Test File Classification Issues Detected
TestHasBashWildcard pkg/workflow/claude_tools_helpers_test.go:11 ✅ Design None
TestNormalizeSandboxWritablePattern pkg/workflow/claude_tools_helpers_test.go:33 ✅ Design None

Language Support

Tests analyzed:

  • 🐹 Go (*_test.go): 2 tests — unit (//go:build !integration)
  • 🟨 JavaScript (*.test.cjs, *.test.js): 0 tests

Verdict

Check passed. 0% of new tests are implementation tests (threshold: 30%).

📖 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 · sonnet46 1.1M ·

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: 100/100. Test quality is excellent — 0% of new tests are implementation tests (threshold: 30%). Both tests are well-structured table-driven design tests with full error/edge case coverage.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
@github-actions
Copy link
Copy Markdown
Contributor

🏗️ Design Decision Gate — ADR Required

This PR makes significant changes to core business logic (307 new lines in pkg/workflow/, above the 100-line threshold) but does not have a linked Architecture Decision Record (ADR).

📄 Draft ADR committed: docs/adr/35812-decompose-claude-allowed-tools-assembly.md — review and complete it before merging.

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

📋 What to do next
  1. Review the draft ADR committed to your branch — it was generated from the PR diff and captures the decision to decompose computeAllowedClaudeToolsString into focused, behavior-preserving helpers.
  2. Complete / refine the sections — confirm the driver (lint-monster complexity reduction), tighten the alternatives, and verify the normative output-contract guarantees match your intent.
  3. Commit the finalized ADR to docs/adr/ on your branch.
  4. Reference the ADR in this PR body by adding a line such as:

    ADR: ADR-35812: Decompose Claude Allowed-Tools Assembly into Focused Helpers

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. Even behavior-preserving refactors of security-sensitive paths (here, the agent tool-permission allowlist) benefit from a recorded rationale: future contributors editing claude_tools.go will understand why it is structured as a helper pipeline.

📋 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)?

All ADRs are stored in docs/adr/ as Markdown files numbered by PR number (e.g., 35812-...md for PR #35812).

🔒 Blocking: link the ADR in the PR body to unblock merge.

🏗️ ADR gate enforced by Design Decision Gate 🏗️ · opus48 977.6K ·

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 /zoom-out and /tdd — leaving as COMMENT; no blocking correctness issues, but a few things worth addressing.

📋 Key Themes & Highlights

Positive Highlights

  • ✅ Clean pipeline decomposition in computeAllowedClaudeToolsString — the new 7-step sequence reads like a specification
  • normalizeSandboxWritablePattern is a textbook extract: pure function, well-tested, removes duplication
  • appendIfMissing eliminates the scattered if !slices.Contains(...) { append } pattern — good DRY move
  • switch in appendTopLevelClaudeTools is cleaner than the original chained if/else

Issues Found

  1. getOrCreateToolMap misleading name — doesn't write back to the container; callers must do it manually (medium risk of future bugs)
  2. Double scan in appendGitHubMCPTools/appendGenericMCPTools — minor inefficiency, introduces inconsistency with single-pass original
  3. Test coverage gap — only 2 of ~15 helpers have unit tests; the PR description says "focused unit coverage" but most helpers are untested
  4. isClaudeToolName is an undocumented heuristic — any uppercase-first map key becomes an allowed tool; deserves a clarifying comment

🧠 Reviewed using Matt Pocock's skills by Matt Pocock Skills Reviewer · sonnet46 1.9M

for _, defaultTool := range defaultClaudeTools {
if _, exists := claudeExistingAllowed[defaultTool]; !exists {
claudeExistingAllowed[defaultTool] = nil // Add tool with null value
func getOrCreateToolMap(container map[string]any, key string) map[string]any {
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] getOrCreateToolMap doesn't actually store the new map back in container — the name implies it does both, but callers must remember to assign the result back themselves.

💡 Rename or redesign to avoid subtle mutation bugs

The current contract:

claudeSection := getOrCreateToolMap(tools, "claude")
// ...mutate claudeSection...
tools["claude"] = claudeSection  // ← caller must remember this

This is safe today because both call-sites in ensureDefaultClaudeAllowedTools manually write back, but the name suggests the map is already wired into the container. A future caller could skip the write-back.

Consider renaming to extractOrNewMap (read-only extraction), or change the signature to accept the parent container and key and mutate it:

// Option A – rename to reflect read-only semantics
func extractOrNewMap(container map[string]any, key string) map[string]any

// Option B – keep setOrCreate with the write-back inside
func setOrCreateMap(container map[string]any, key string) map[string]any {
    m := extractOrNewMap(container, key)
    container[key] = m  // always write back
    return m
}

Note also that collectClaudeAllowedTools uses typeutil.LookupMap for the same pattern — unifying on one approach would reduce cognitive load.

func appendGitHubMCPTools(allowedTools []string, toolName string, toolValue any, mcpConfig map[string]any) []string {
githubConfig := parseGitHubTool(toolValue)
if githubConfig != nil && len(githubConfig.Allowed) > 0 {
for _, tool := range githubConfig.Allowed {
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] appendGitHubMCPTools (and appendGenericMCPTools below) scan the allowed list twice: once for a wildcard and once for specific tools. A single pass with an early-return flag was used in the original code; the double-pass here is correct but slightly less efficient.

💡 Single-pass alternative
hasWildcard := false
for _, tool := range githubConfig.Allowed {
    if string(tool) == "*" {
        hasWildcard = true
        break
    }
}
if hasWildcard {
    return append(allowedTools, "mcp__"+toolName)
}
for _, tool := range githubConfig.Allowed {
    allowedTools = append(allowedTools, fmt.Sprintf("mcp__%s__%s", toolName, string(tool)))
}

For large Allowed slices the double-scan wastes work. The single-pass approach with a boolean flag reads clearly and mirrors the original logic.

assert.Equal(t, tt.wantOkay, gotOkay, "ok flag should match expected")
})
}
}
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 new test file covers only 2 of the ~15 extracted helpers. Untested helpers like appendClaudeBashTools, appendGitHubMCPTools, and dedupeAllowedTools contain nuanced logic and are the most likely sources of behavioral regressions.

💡 Suggested additional test cases

appendClaudeBashTools edge cases:

  • nil value → Bash
  • wildcard *Bash
  • trailing space-star: "jq *" normalizes to Bash(jq)
  • specific command: "git"Bash(git)

appendGitHubMCPTools:

  • wildcard allowed list → mcp__github
  • specific tools → mcp__github__tool_name
  • nil allowed list → default GitHub tools

dedupeAllowedTools:

  • ["Read", "Bash", "Read"]["Read", "Bash"]

These would fulfil the PR description's goal of "focused unit coverage to lock semantics."

return false
}

func isClaudeToolName(toolName string) bool {
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] isClaudeToolName checks only that the first character is uppercase — it's really startsWithUppercase, not a semantic validator. Any map key with an uppercase first letter (including potential future non-tool keys) would be treated as a Claude tool.

💡 Consider a more explicit check or a comment clarifying intent

The original code had the same logic inline, but wrapping it in a named function now makes it look authoritative. Add a comment clarifying the heuristic:

// isClaudeToolName returns true if toolName appears to be a Claude tool
// by convention: Claude tool names are PascalCase and start with an uppercase letter.
// This is a heuristic; map keys that accidentally start with uppercase will pass.
func isClaudeToolName(toolName string) bool {
    return len(toolName) > 0 && unicode.IsUpper(rune(toolName[0]))
}

Alternatively, maintain an explicit allowlist of known Claude tool names to make unexpected keys fail loudly rather than silently become allowed tools.

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.

Clean, behavior-preserving refactor — the pipeline decomposition in computeAllowedClaudeToolsString reads well and the overall structure is an improvement.

Non-blocking observations (3)

Issues raised

  1. getOrCreateToolMap misleading contract (line 200) — returns a detached empty map without writing it back; callers must manually write back or mutations are lost. Works today, silent trap for future callers. Rename or make it self-storing.

  2. Explanatory comment dropped from appendSafeOutputsTools (line 420) — the original code had a comment + disabled scoped-write explaining why a bare Write is granted, referencing issue #244. Stripped in the refactor, leaving a security-sensitive grant with no rationale.

  3. Test coverage skips the complex helpers (test file) — only hasBashWildcard and normalizeSandboxWritablePattern are tested. appendGitHubMCPTools, collectClaudeAllowedTools, ensureDefaultClaudeAllowedTools, and dedupeAllowedTools have no new tests despite being the logic-heavy paths that most benefit from extraction.

None of these are blocking.

🔎 Code quality review by PR Code Quality Reviewer · sonnet46 2.5M

Comment thread pkg/workflow/claude_tools.go Outdated
return existingMap
}
}
return make(map[string]any)
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.

getOrCreateToolMap has a misleading contract: it never writes the newly created map back to container, so callers must manually assign the returned map back — which works today but is a trap for future callers.

💡 Details

The function name implies ensure

}
allowedTools = append(allowedTools, "mcp__"+string(constants.SafeOutputsMCPServerID))
if !slices.Contains(allowedTools, "Write") {
allowedTools = append(allowedTools, "Write")
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.

Explanatory comment removed from appendSafeOutputsTools: the unscoped Write grant looks like an oversight without the original context, but it was a deliberate known limitation.

💡 Details

The original code included:

// Ideally we would only give permission to the exact file, but that doesn't seem
// to be working with Claude. See https://github.com/github/gh-aw/issues/244#issuecomment-3240319103
(allowedtools/redacted) = append(allowedTools, "Write(${{ env.GH_AW_SAFE_OUTPUTS }})")

The refactor stripped this, leaving a bare unrestricted Write (grants filesystem write to any path) with no rationale. A future reader or security reviewer will flag this immediately. Add the godoc comment back referencing #244 so the intent is clear.

assert.Equal(t, tt.wantOkay, gotOkay, "ok flag should match expected")
})
}
}
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.

Test coverage is thin for the highest-complexity extracted helpers: only two trivial leaf functions are tested, while the functions carrying the actual allowed-tools assembly logic have no new tests.

💡 Details

Untested extracted helpers include:

  • collectClaudeAllowedTools — iterates over the allowed map and invokes the Bash-tool path; edge cases like empty map, missing claude key, or a map where only KillBash/BashOutput appear are not covered
  • appendGitHubMCPTools — wildcard vs specific-tool branching, fallback to default tools, remote vs local mode
  • appendGenericMCPTools — wildcard early-return vs per-item expansion
  • ensureDefaultClaudeAllowedTools — default-tool injection and Bash-implies-KillBash/BashOutput side-effect
  • dedupeAllowedTools — dedup correctness for duplicate Bash(...) entries

The PR description says 'Focused unit coverage for extracted logic' but only covers hasBashWildcard and normalizeSandboxWritablePattern, which are the simplest helpers. The complex branching (especially appendGitHubMCPTools) is the most likely source of regressions.

@pelikhan
Copy link
Copy Markdown
Collaborator

@copilot review all review comments from copilot and github-actions actor

Copy link
Copy Markdown
Contributor Author

Copilot AI commented May 29, 2026

@copilot lint go and fix https://github.com/github/gh-aw/actions/runs/26667328888/job/78603915412

I checked this. make lint-go passes locally on the current branch head (4db7eb4), and the linked failure is from run 26667328888 on older SHA 34c75fd. That job failed in a docs-drift check for paths that are no longer present on current head. Please rerun CI on the latest branch head; if it still fails, I can fix the new failure directly.

Copilot AI and others added 2 commits May 29, 2026 23:57
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copy link
Copy Markdown
Contributor Author

Copilot AI commented May 30, 2026

@copilot review all review comments from copilot and github-actions actor

Reviewed all Copilot and github-actions review feedback and applied the actionable code/test updates in 06dd7f2:

I also checked the previously linked failing CI job logs; that failure was on older SHA 34c75fd, not current head.

@pelikhan pelikhan merged commit e0e6cd2 into main May 30, 2026
@pelikhan pelikhan deleted the copilot/lint-monster-refactor-function-complexity branch May 30, 2026 00:21
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[lint-monster] refactor: reduce function complexity in workflow and CLI files (2218 findings)

4 participants