diff --git a/.agents/skills/repo-conventions/references/cli-reference.md b/.agents/skills/repo-conventions/references/cli-reference.md index 4a62e65..639cda6 100644 --- a/.agents/skills/repo-conventions/references/cli-reference.md +++ b/.agents/skills/repo-conventions/references/cli-reference.md @@ -24,7 +24,7 @@ dnx repo-conventions validate [options] | `--draft` / `--no-draft` | Override configured draft behavior. These options cannot be used together. | | `--auto-merge` / `--no-auto-merge` | Override configured auto-merge behavior. These options cannot be used together. | | `--merge-method ` | Preferred auto-merge method. Must be `merge`, `squash`, or `rebase`. | -| `--git-no-verify` | Pass `--no-verify` to git commit and git push commands run by RepoConventions. | +| `--git-no-verify` | Pass `--no-verify` to git commit and git push commands run by RepoConventions, and expose `git.noVerify` in the JSON input to convention scripts so they can bypass the same hooks when they create their own commits. | ## `add` diff --git a/.agents/skills/repo-conventions/references/convention-authoring.md b/.agents/skills/repo-conventions/references/convention-authoring.md index 857aaeb..f0d57bc 100644 --- a/.agents/skills/repo-conventions/references/convention-authoring.md +++ b/.agents/skills/repo-conventions/references/convention-authoring.md @@ -152,7 +152,8 @@ Execution contract: - The current working directory is the target Git repository root, not the convention directory. - The first argument is the path to a JSON input file. - Use `$args[0]` to access the input path so future arguments do not break the script. -- The JSON input file contains a single `settings` property. +- The JSON input file contains a `settings` property and a `git` property. +- `git.noVerify` is a boolean that reflects the `--git-no-verify` option. When it is `true`, scripts that create their own commits or pushes must pass `--no-verify` so they bypass the same hooks RepoConventions bypasses. - RepoConventions captures stdout and stderr as UTF-8. Set `[Console]::OutputEncoding` before invoking native tools so their output is emitted as UTF-8 too. Standard header for `convention.ps1`: @@ -175,6 +176,17 @@ $conventionInput = Get-Content -Raw $args[0] | ConvertFrom-Json $settings = $conventionInput.settings ``` +When the script creates its own commits, honor `git.noVerify` so it matches the rest of the run: + +```pwsh +$conventionInput = Get-Content -Raw $args[0] | ConvertFrom-Json +$commitArguments = @('commit', '-m', 'Use LF') +if ($conventionInput.git.noVerify) { + $commitArguments += '--no-verify' +} +git @commitArguments +``` + Authoring expectations: - Read the JSON input only when settings are needed. @@ -185,7 +197,7 @@ Authoring expectations: - Prefer deterministic file writes, stable ordering, and stable line endings. - When the script has nothing to do, usually emit no output; already-compliant repositories are the most common case. - Emit focused output that explains what changed or why the convention cannot continue. -- If the convention naturally consists of multiple meaningful steps, the script may create its own commits with informative messages. +- If the convention naturally consists of multiple meaningful steps, the script may create its own commits with informative messages. When it does, pass `--no-verify` to those commits whenever `git.noVerify` is `true`. ## Commit and Failure Behavior @@ -215,6 +227,7 @@ Keep repository-level consumer docs focused on using RepoConventions. - Test both an already-compliant repository and a non-compliant repository. - Re-run after the first successful application to confirm idempotency. - If the convention has settings, exercise at least one non-default settings case. +- If the convention executes any git commits or pushes, test compliance with `git.noVerify` input against failing git hooks. - Test failure paths when settings are required or external tools may be unavailable. ## Agent Workflow diff --git a/apm.lock.yaml b/apm.lock.yaml index 0e0cd23..a291414 100644 --- a/apm.lock.yaml +++ b/apm.lock.yaml @@ -1,13 +1,13 @@ lockfile_version: '1' -generated_at: '2026-05-29T15:19:34.790709+00:00' -apm_version: 0.14.0 +generated_at: '2026-06-10T05:18:57.275411+00:00' +apm_version: 0.18.0 dependencies: - repo_url: Faithlife/RepoConventions host: github.com - resolved_commit: 791f7d38a3d916e8be7504f21192eb58205c9302 + resolved_commit: 7f58c0678f9efefb2453908032e0ee28a42d7097 virtual_path: skills/repo-conventions is_virtual: true package_type: claude_skill deployed_files: - .agents/skills/repo-conventions - content_hash: sha256:b4c71ea6793ab8bbc481cf7fa25cf86c7f5c36caefcfbe68053860082191af57 + content_hash: sha256:6e4dc45d96d7adc8f1d3e3300d9ee90eef9c2d0231ea83ad86fed64fca105034 diff --git a/conventions/agentic-repo/convention.yml b/conventions/agentic-repo/convention.yml index a65f565..4bb1646 100644 --- a/conventions/agentic-repo/convention.yml +++ b/conventions/agentic-repo/convention.yml @@ -25,6 +25,7 @@ conventions: name: agentic-repo text: | .agents/ + .github/instructions/ apm.lock.yaml apm.yml - path: ../apm diff --git a/conventions/copilot-lsp/convention.yml b/conventions/copilot-lsp/convention.yml index 4f7fefb..aab03d6 100644 --- a/conventions/copilot-lsp/convention.yml +++ b/conventions/copilot-lsp/convention.yml @@ -1,2 +1,11 @@ +conventions: + - path: ../prettierignore-section + commit: + message: Update .prettierignore for copilot-lsp + settings: + name: copilot-lsp + text: | + .github/lsp.json + pull-request: auto-merge: true diff --git a/conventions/gitattributes-lf/convention.Tests.ps1 b/conventions/gitattributes-lf/convention.Tests.ps1 index 8e2208b..58e6150 100644 --- a/conventions/gitattributes-lf/convention.Tests.ps1 +++ b/conventions/gitattributes-lf/convention.Tests.ps1 @@ -217,4 +217,79 @@ Describe 'gitattributes-lf convention' { } } + It 'bypasses commit hooks when git no-verify is requested' { + $testDirectory = New-TemporaryDirectory + + try { + # Arrange a noncompliant repository whose commit-msg hook rejects every commit. + Initialize-TestRepository -Path $testDirectory + $gitattributesPath = Join-Path $testDirectory '.gitattributes' + [System.IO.File]::WriteAllText($gitattributesPath, "* -text`n", $utf8) + $hookPath = Join-Path $testDirectory '.git' 'hooks' 'commit-msg' + [System.IO.File]::WriteAllText($hookPath, "#!/bin/sh`nexit 1`n", $utf8) + if ($IsLinux -or $IsMacOS) { + & chmod '+x' $hookPath + } + + Push-Location $testDirectory + try { + & git add -A + & git commit -m 'Add noncompliant gitattributes' --no-verify | Out-Null + } + finally { + Pop-Location + } + + # Build a RepoConventions input that requests the git hook bypass. + $inputPath = New-ConventionInputFile -Settings @{} -GitNoVerify + + try { + # Apply the convention; its commits must succeed despite the failing hook. + Invoke-ConventionScript -ScriptPath $script:conventionScriptPath -RepositoryRoot $testDirectory -InputPath $inputPath | Out-Null + } + finally { + Remove-Item -LiteralPath $inputPath -ErrorAction SilentlyContinue + } + + # Assert the convention created its bypassing commit and left the tree clean. + $commitSubjects = @(Get-CommitSubjects -TestDirectory $testDirectory -Count 1) + $commitSubjects[0] | Should -Be 'Use LF' + (@(Get-GitStatusLines -TestDirectory $testDirectory)).Count | Should -Be 0 + } + finally { + Remove-Item -LiteralPath $testDirectory -Recurse -Force + } + } + + It 'fails on commit hooks when git no-verify is not requested' { + $testDirectory = New-TemporaryDirectory + + try { + # Arrange a noncompliant repository whose commit-msg hook rejects every commit. + Initialize-TestRepository -Path $testDirectory + $gitattributesPath = Join-Path $testDirectory '.gitattributes' + [System.IO.File]::WriteAllText($gitattributesPath, "* -text`n", $utf8) + $hookPath = Join-Path $testDirectory '.git' 'hooks' 'commit-msg' + [System.IO.File]::WriteAllText($hookPath, "#!/bin/sh`nexit 1`n", $utf8) + if ($IsLinux -or $IsMacOS) { + & chmod '+x' $hookPath + } + + Push-Location $testDirectory + try { + & git add -A + & git commit -m 'Add noncompliant gitattributes' --no-verify | Out-Null + } + finally { + Pop-Location + } + + # Apply the convention without requesting the bypass; the hook must block the commit. + { InvokeGitattributesLfConvention -TestDirectory $testDirectory } | Should -Throw "*Failed to create commit 'Use LF'.*" + } + finally { + Remove-Item -LiteralPath $testDirectory -Recurse -Force + } + } + } diff --git a/conventions/gitattributes-lf/convention.ps1 b/conventions/gitattributes-lf/convention.ps1 index f3e3d8a..1c4905d 100644 --- a/conventions/gitattributes-lf/convention.ps1 +++ b/conventions/gitattributes-lf/convention.ps1 @@ -10,6 +10,9 @@ $OutputEncoding = $utf8 $helpersPath = Join-Path $PSScriptRoot '..' 'scripts' 'Helpers.ps1' . $helpersPath +# Honor the RepoConventions git hook bypass request +$gitNoVerify = Read-ConventionGitNoVerify -InputPath $args[0] + $requiredRule = '* text=auto eol=lf' $gitattributesPath = Join-Path -Path (Get-Location) -ChildPath '.gitattributes' $gitattributesDisplayPath = Format-RepositoryRelativePath -Path $gitattributesPath @@ -58,7 +61,12 @@ function NewCommitFromStagedChanges { return $null } - InvokeGit -Arguments @('commit', '-m', $Message) -FailureMessage "Failed to create commit '$Message'." + $commitArguments = @('commit', '-m', $Message) + if ($gitNoVerify) { + $commitArguments += '--no-verify' + } + + InvokeGit -Arguments $commitArguments -FailureMessage "Failed to create commit '$Message'." [string[]] $headLines = @(InvokeGit -Arguments @('rev-parse', 'HEAD') -CaptureOutput -FailureMessage 'Failed to read the current commit ID.') return $headLines[0] } diff --git a/conventions/scripts/Helpers.ps1 b/conventions/scripts/Helpers.ps1 index 2b0a123..2c52416 100644 --- a/conventions/scripts/Helpers.ps1 +++ b/conventions/scripts/Helpers.ps1 @@ -126,6 +126,20 @@ function Read-ConventionSettings { return (Get-Content -LiteralPath $InputPath -Raw | ConvertFrom-Json -AsHashtable).settings } +<# +.SYNOPSIS +Reads whether RepoConventions requested that git hooks be bypassed for this run. +#> +function Read-ConventionGitNoVerify { + param( + [Parameter(Mandatory = $true)] + [string] $InputPath + ) + + # RepoConventions exposes the --git-no-verify flag under git.noVerify. + return [bool] (Get-Content -LiteralPath $InputPath -Raw | ConvertFrom-Json -AsHashtable).git.noVerify +} + <# .SYNOPSIS Resolves a repository-root-relative path setting to a full path. diff --git a/conventions/scripts/TestHelpers.ps1 b/conventions/scripts/TestHelpers.ps1 index 2758c0d..8263414 100644 --- a/conventions/scripts/TestHelpers.ps1 +++ b/conventions/scripts/TestHelpers.ps1 @@ -19,23 +19,26 @@ $helpersPath = Join-Path $PSScriptRoot 'Helpers.ps1' Creates a temporary RepoConventions input file for a test. #> function New-ConventionInputFile { - [CmdletBinding(DefaultParameterSetName = 'Settings')] + [CmdletBinding(DefaultParameterSetName = 'Structured')] param( - [Parameter(Mandatory = $true, ParameterSetName = 'Settings')] + [Parameter(Mandatory = $true, ParameterSetName = 'Structured')] [hashtable] $Settings, - [Parameter(Mandatory = $true, ParameterSetName = 'Json')] + [Parameter(ParameterSetName = 'Structured')] + [switch] $GitNoVerify, + + [Parameter(Mandatory = $true, ParameterSetName = 'Verbatim')] [string] $InputJson ) # Place each generated RepoConventions input in a unique temp JSON file. $inputPath = Join-Path ([System.IO.Path]::GetTempPath()) ([System.Guid]::NewGuid().ToString('N') + '.json') - $content = if ($PSCmdlet.ParameterSetName -eq 'Settings') { - # Wrap settings in the input shape consumed by convention scripts. - @{ settings = $Settings } | ConvertTo-Json -Depth 10 -Compress + $content = if ($PSCmdlet.ParameterSetName -eq 'Structured') { + # Mirror the input shape RepoConventions produces. + @{ settings = $Settings; git = @{ noVerify = [bool] $GitNoVerify } } | ConvertTo-Json -Depth 10 -Compress } else { - # Use caller-supplied JSON verbatim for malformed-input tests. + # Use caller-supplied JSON verbatim for specialized (e.g., malformed) test cases. $InputJson }