Skip to content

Add gh aw fix codemod to rewrite single-quoted tools.bash args safely#31874

Merged
pelikhan merged 5 commits into
mainfrom
copilot/add-codemod-bash-single-quoted-args
May 13, 2026
Merged

Add gh aw fix codemod to rewrite single-quoted tools.bash args safely#31874
pelikhan merged 5 commits into
mainfrom
copilot/add-codemod-bash-single-quoted-args

Conversation

Copy link
Copy Markdown
Contributor

Copilot AI commented May 13, 2026

Bug Fix

What was the bug?

tools.bash allowlist entries containing single-quoted args (for example grep -rn 'pattern' --include='*.lua') were later truncated to a prefix for Copilot shell prefix-matching, causing users to lose command precision. This change adds an automatic migration path in gh aw fix.

How did you fix it?

  • New codemod: bash-single-quoted-args-rewrite
    • Scans tools.bash list entries in frontmatter.
    • Rewrites safely parseable single-quoted segments to double-quoted equivalents while escaping \, ", $, and ` to preserve literal meaning.
  • Safety-first fallback
    • If a command has unmatched single quotes, it is left unchanged.
    • Emits a warning diagnostic for entries that cannot be safely rewritten.
  • Codemod pipeline integration
    • Registers the codemod in the fix registry so gh aw fix --write applies it.
    • Updates codemod order/registry assertions.

Testing

  • Added focused unit coverage for:
    • simple rewrite ('pattern'"pattern")
    • glob/flag rewrite (--include='*.lua'--include="*.lua")
    • no-op for already-safe entries
    • unmatched quote behavior (unchanged + non-applied)
    • rewrite helper escaping behavior
tools:
  bash:
    - grep -rn 'pattern' --include='*.lua'

rewrites to:

tools:
  bash:
    - grep -rn "pattern" --include="*.lua"

Co-authored-by: pelikhan <4175913+pelikhan@users.noreply.github.com>
Copilot AI changed the title [WIP] Add codemod to auto-rewrite single-quoted args in bash tool list Add gh aw fix codemod to rewrite single-quoted tools.bash args safely May 13, 2026
Copilot AI requested a review from pelikhan May 13, 2026 05:53
@pelikhan pelikhan marked this pull request as ready for review May 13, 2026 05:56
Copilot AI review requested due to automatic review settings May 13, 2026 05:56
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

Adds a new gh aw fix codemod to migrate tools.bash allowlist entries away from single-quoted shell arguments (which currently trigger Copilot CLI truncation/prefix sanitization), plus registry integration and unit tests.

Changes:

  • Registers a new codemod (bash-single-quoted-args-rewrite) in the fix codemod registry and expected execution order.
  • Implements the codemod to rewrite single-quoted segments in tools.bash entries into double-quoted equivalents with escaping.
  • Adds focused unit tests for the codemod and the rewrite helper.
Show a summary per file
File Description
pkg/cli/fix_codemods.go Registers the new codemod in the global codemod list.
pkg/cli/fix_codemods_test.go Updates registry/order assertions to include the new codemod ID.
pkg/cli/codemod_bash_single_quoted_args.go Implements the codemod and the single-quote rewrite helper.
pkg/cli/codemod_bash_single_quoted_args_test.go Adds unit tests validating codemod behavior and helper escaping.

Copilot's findings

Tip

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

Comments suppressed due to low confidence (1)

pkg/cli/codemod_bash_single_quoted_args.go:127

  • rewriteSingleQuotedBashArgs treats every ' byte as the start of a single-quoted segment without tracking shell context (e.g., inside double quotes/backticks) or unquoted escaping ('). This can incorrectly rewrite commands like echo "a'b'" and can also mis-classify escaped apostrophes as “unmatched”, changing semantics or emitting misleading diagnostics. Consider a small shell-quoting state machine (unquoted/single/double/backtick + escape handling) and mark complex constructs (e.g. ''' concatenation used to embed a quote) as unsafe/no-op rather than partially rewriting.
	for i := 0; i < len(cmd); {
		if cmd[i] != '\'' {
			b.WriteByte(cmd[i])
			i++
			continue
  • Files reviewed: 4/4 changed files
  • Comments generated: 3


for _, cmd := range unsafeCommands {
fmt.Fprintln(os.Stderr, console.FormatWarningMessage(
fmt.Sprintf("tools.bash entry %q contains unmatched single quotes and could not be safely rewritten; left unchanged", cmd)))
Comment on lines +94 to +98
var lines []string
lines = append(lines, "---")
if frontmatterStr != "" {
lines = append(lines, strings.Split(frontmatterStr, "\n")...)
}
Comment on lines +125 to +129
func TestRewriteSingleQuotedBashArgs(t *testing.T) {
tests := []struct {
name string
input string
want string
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 /diagnose and /tdd — this is a bug-fix PR with new unit coverage, so both skills are directly applicable.

Key Themes

  • Double-quote context gap (correctness): rewriteSingleQuotedBashArgs processes bytes without tracking whether the current position is inside a "-quoted segment. A command like grep 'pattern' "it's valid" causes a false-positive "unmatched single quotes" warning and is left unchanged, even though both single-quoted segments are valid. The fix is a small inDouble bool in the loop.
  • Missing regression test: No table entry covers the mixed double/single-quote scenario, so the above bug has no automated guard.
  • Frontmatter mutation side-effect: Apply writes back into the caller-supplied frontmatter map. Other codemods in the registry only read the map; this is an undocumented inconsistency that could silently corrupt state if the pipeline ordering or sharing ever changes.

Positive Highlights

  • ✅ Safety-first design: unmatched quotes are never rewritten, only warned about.
  • ✅ Escaping logic correctly handles \, ", $, and ``` to preserve literal semantics.
  • ✅ Table-driven tests are well-named and cover the happy-path cases cleanly.
  • ✅ Codemod registration and ordering tests updated in the same PR — no drift.

Verdict

Requesting changes on the double-quote-context correctness issue before merge; it affects real-world commands (anything with a contraction or possessive inside double quotes mixed with single-quoted args).

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

if frontmatterStr != "" {
lines = append(lines, strings.Split(frontmatterStr, "\n")...)
}
lines = append(lines, "---")
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.

[/diagnose] The byte scanner doesn't track whether it's currently inside a double-quoted segment, so a command like grep 'pattern' "it's valid" triggers a false-positive "unmatched single quotes" warning and is left unchanged — even though the single quotes are perfectly valid.

Walk-through: after rewriting 'pattern'"pattern", the loop hits the ' inside "it's" at the next iteration, fails to find a matching ', returns cmd, false, false, and the command ends up in unsafeCommands.

Consider tracking double-quote state:

inDouble := false
for i := 0; i < len(cmd); {
    if cmd[i] == '"' {
        b.WriteByte(cmd[i])
        i++
        continue
    }
    if cmd[i] != '\'' || inDouble {
        b.WriteByte(cmd[i])
        i++
        continue
    }
    // ... existing single-quote rewrite logic
}

A regression test covering grep 'pattern' "it's ok" would lock this in.

wantApply: false,
},
{
name: "unmatched single quote",
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 TestRewriteSingleQuotedBashArgs table is missing a case that would catch the double-quote-context bug: a command mixing a rewritable single-quoted segment with a single quote embedded inside a double-quoted word (e.g. grep 'pattern' "it's valid").

Suggested addition:

{
    name:      "single-quote inside double-quoted word is skipped",
    input:     "grep 'pattern' \"it's valid\"",
    want:      "grep \"pattern\" \"it's valid\"",
    wantSafe:  true,
    wantApply: true,
},

Without it, the double-quote-tracking fix has no automated guard and the regression can silently reappear.

}

if !changed {
return content, false, 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.

[/diagnose] Apply mutates the caller-supplied frontmatter map in place (toolsMap["bash"] = updated, frontmatter["tools"] = toolsMap). Every other codemod in the registry only reads the map; if the pipeline ever passes the same map to multiple codemods this will cause silent state contamination.

A quick defensive copy at the top of the Apply body, or assigning to a local rather than back-writing to frontmatter, would make the side-effect explicit and safe.

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

Commit pushed: 81c8331

🏗️ ADR gate enforced by Design Decision Gate 🏗️

@github-actions
Copy link
Copy Markdown
Contributor

🏗️ Design Decision Gate — ADR Required

This PR makes significant changes to core business logic (332 new lines in pkg/cli/, exceeding the 100-line threshold) 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/31874-codemod-rewrite-single-quoted-bash-tool-args.md

What to do next

  1. Review the draft ADR committed to your branch — it was generated from the PR diff
  2. Complete the missing sections — add context the AI couldn't infer, refine the decision rationale, and list real alternatives you considered
  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-31874: Codemod to Rewrite Single-Quoted tools.bash Args to Double-Quoted Form

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

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

References:

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

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

@github-actions
Copy link
Copy Markdown
Contributor

🧪 Test Quality Sentinel Report

Test Quality Score: 85/100

Excellent test quality

Metric Value
New/modified tests analyzed 6
✅ Design tests (behavioral contracts) 6 (100%)
⚠️ Implementation tests (low value) 0 (0%)
Tests with error/edge cases 3 (50%)
Duplicate test clusters 0
Test inflation detected No (1.08×)
🚨 Coding-guideline violations 1 — missing assertion messages

Test Classification Details

View all 6 test classifications
Test File Classification Issues Detected
TestGetBashSingleQuotedArgsCodemod codemod_bash_single_quoted_args_test.go:13 ✅ Design No edge cases; assertion messages missing
TestBashSingleQuotedArgsCodemod_RewritesSimpleSingleQuotedArg codemod_bash_single_quoted_args_test.go:21 ✅ Design Happy path only; assertion messages missing
TestBashSingleQuotedArgsCodemod_RewritesGlobPatterns codemod_bash_single_quoted_args_test.go:54 ✅ Design Happy path only; assertion messages missing
TestBashSingleQuotedArgsCodemod_NoOpForAlreadySafeEntry codemod_bash_single_quoted_args_test.go:87 ✅ Design ✓ Edge case covered (already-safe input)
TestBashSingleQuotedArgsCodemod_UnmatchedQuoteLeftUnchanged codemod_bash_single_quoted_args_test.go:115 ✅ Design ✓ Edge case covered (malformed input)
TestRewriteSingleQuotedBashArgs codemod_bash_single_quoted_args_test.go:142 ✅ Design ✓ Table-driven; includes error/edge row; assertion messages missing

Flagged Tests — Requires Review

⚠️ Missing assertion messages (guideline violation)

Most assertions throughout the new test file omit the required descriptive message argument. Examples:

// ❌ Current — no context when assertion fails
assert.Equal(t, "bash-single-quoted-args-rewrite", codemod.ID)
assert.Equal(t, tt.want, got)
assert.True(t, applied)

// ✅ Expected — descriptive message for each assertion
assert.Equal(t, "bash-single-quoted-args-rewrite", codemod.ID, "codemod ID must match registration key")
assert.Equal(t, tt.want, got, "rewritten bash arg should match expected output for case %q", tt.name)
assert.True(t, applied, "codemod should report applied=true when a rewrite was performed")

Affected tests: TestGetBashSingleQuotedArgsCodemod, TestBashSingleQuotedArgsCodemod_Rewrites*, TestRewriteSingleQuotedBashArgs.
Why it matters: When a test fails in CI, missing messages make it harder to diagnose root cause without re-reading the entire test body.


Positive Highlights

  • Build tag: ✅ //go:build !integration present on line 1
  • No mock libraries: ✅ No gomock, testify/mock, .EXPECT(), or .On() usage
  • Proportional growth: Test file 171 lines vs production file 158 lines (1.08× ratio — no inflation)
  • Table-driven test: TestRewriteSingleQuotedBashArgs correctly uses t.Run() over a cases slice with both happy-path and edge-case rows (unmatched quote)
  • End-to-end roundtrip: The codemod tests parse the output back through parser.ExtractFrontmatterFromContent() to assert on the rewritten YAML, verifying the actual user-visible contract rather than string manipulation internals

Score Calculation

Component Weight Score
Behavioral coverage (6/6 design tests) 40 40
Error/edge case coverage (3/6 tests) 30 15
Low duplication (0 clusters) 20 20
Proportional growth (ratio 1.08×) 10 10
Total 100 85

Language Support

Tests analyzed:

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

Verdict

Check passed. 0% of new tests are implementation tests (threshold: 30%). The single flagged item (missing assertion messages) is a style guideline to improve debuggability but does not block merging.


📖 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.

References: §25781277304

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

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: 85/100. Test quality is excellent — 0% of new tests are implementation tests (threshold: 30%). Minor: assertion messages are missing on most assertions; consider adding descriptive messages for easier CI debugging.

@pelikhan
Copy link
Copy Markdown
Collaborator

@copilot review all comments

@pelikhan
Copy link
Copy Markdown
Collaborator

@copilot review all comments

Copilot AI and others added 2 commits May 13, 2026 09:41
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 13, 2026

@copilot review all comments

Reviewed and addressed all actionable comments in commits 45eea14 and e568b2e. This includes: accurate skip-warning wording, reusing shared workflow reconstruction, and added edge-case coverage for apostrophes/escaped quote patterns with updated parser logic.

Copy link
Copy Markdown
Contributor Author

Copilot AI commented May 13, 2026

@copilot review all comments

Completed. All actionable review feedback has already been addressed in commits 45eea14 and e568b2e, and validations are passing.

@pelikhan pelikhan merged commit d613a02 into main May 13, 2026
1 check passed
@pelikhan pelikhan deleted the copilot/add-codemod-bash-single-quoted-args branch May 13, 2026 09:50
Copilot stopped work on behalf of pelikhan due to an error May 13, 2026 09:50
Copilot AI requested a review from pelikhan May 13, 2026 09:50
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.

[plan] Codemod: auto-rewrite single-quoted args in bash tool list entries

3 participants