Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
21 commits
Select commit Hold shift + click to select a range
276fe8f
bump: version to v1.5.2
supasympa May 7, 2026
f8bc123
fix: filter review comments to only files in PR diff
supasympa May 7, 2026
6a64476
fix: use vars.ACIG_APP_ID instead of secrets for app-id
supasympa May 7, 2026
db45c97
fix: read all struct fields to satisfy unusedwrite linter
supasympa May 7, 2026
51c341c
add check target that runs lint and test
supasympa May 7, 2026
72445f4
fix: restore conditional for app token step
supasympa May 7, 2026
e813027
fix: remove invalid if condition from workflow
supasympa May 7, 2026
8c53f22
fix: skip app token step when app-id is empty
supasympa May 7, 2026
b3bc3ad
fix: retry review without inline comments on position errors
supasympa May 7, 2026
c2c8657
test: add integration tests for Report with mock GitHub client
supasympa May 7, 2026
9f7962f
ci: fetch latest ACIG version instead of hardcoding
supasympa May 7, 2026
37f08d1
fix: validate line numbers against PR diff before commenting
supasympa May 7, 2026
cb6f9f4
fix: ignore review when empty
supasympa May 7, 2026
7a1c11e
fix: resolve 422 Path could not be resolved errors in PR reviews
supasympa May 8, 2026
f08cc8e
chore: bump version to 1.5.5
supasympa May 8, 2026
e18dc93
fix: use hunk ranges for line validation, fix Line could not be resolved
supasympa May 8, 2026
972087a
fix: add auth headers and debug output to acig install script
supasympa May 8, 2026
ecf5991
fix: use gh CLI for authenticated release downloads
supasympa May 8, 2026
a3c02aa
fix: filter hallucinated file paths from critic findings
supasympa May 8, 2026
cf8aaaa
fix: categorize findings into in-diff, dangling, and hallucinated
supasympa May 8, 2026
d0344f2
fix: post inline review comments for findings with no line number
supasympa May 8, 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
22 changes: 15 additions & 7 deletions .github/workflows/acig.yml
Original file line number Diff line number Diff line change
Expand Up @@ -19,23 +19,31 @@ jobs:
- name: Get GitHub App token
id: app-token
uses: actions/create-github-app-token@v2
if: ${{ secrets.ACIG_APP_ID != '' && secrets.ACIG_APP_PRIVATE_KEY != '' }}
if: ${{ vars.ACIG_APP_ID != '' }}
with:
app-id: ${{ secrets.ACIG_APP_ID }}
app-id: ${{ vars.ACIG_APP_ID }}
private-key: ${{ secrets.ACIG_APP_PRIVATE_KEY }}

- name: Install acig
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
ACIG_VERSION="1.4.1"
RELEASE_URL="https://github.com/helloodokai/acig/releases/download/v${ACIG_VERSION}"
ACIG_VERSION=$(gh release view --repo helloodokai/acig --json tagName --jq '.tagName' 2>/dev/null || echo "")
if [ -z "$ACIG_VERSION" ]; then
echo "::error::could not determine latest acig version"
exit 1
fi
echo "Using ACIG version ${ACIG_VERSION}"

curl -sL "${RELEASE_URL}/checksums.txt" -o /tmp/checksums.txt
curl -sL "${RELEASE_URL}/acig_linux_amd64.tar.gz" -o /tmp/acig_linux_amd64.tar.gz
gh release download "${ACIG_VERSION}" --repo helloodokai/acig -p "checksums.txt" -p "acig_linux_amd64.tar.gz" -D /tmp

cd /tmp
EXPECTED=$(grep acig_linux_amd64.tar.gz checksums.txt | awk '{print $1}')
EXPECTED=$(grep "acig_linux_amd64.tar.gz" checksums.txt | awk '{print $1}')
if [ -z "$EXPECTED" ]; then
echo "::error::checksums.txt does not contain acig_linux_amd64.tar.gz"
cat checksums.txt
exit 1
fi
ACTUAL=$(sha256sum acig_linux_amd64.tar.gz | awk '{print $1}')
if [ "$EXPECTED" != "$ACTUAL" ]; then
echo "::error::checksum mismatch: expected $EXPECTED got $ACTUAL"
Expand Down
4 changes: 3 additions & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,6 @@ Thumbs.db
.acig.toml

# Build
/acig
/acig

.charter.toml
2 changes: 2 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ test:
lint:
golangci-lint run

check: lint test

vet:
go vet ./...

Expand Down
11 changes: 11 additions & 0 deletions internal/critics/helpers.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ type promptData struct {
CriticalPaths string
CriticResults string
MaxFindings int
ChangedFiles string
}

func runCritic(
Expand Down Expand Up @@ -221,11 +222,21 @@ func diffToPromptData(d *diff.Diff, cfg *Context) promptData {
stats += fmt.Sprintf(" (showing first ~%d chars)", maxDiffChars)
}

changedFiles := ""
if len(d.Files) > 0 {
var paths []string
for _, f := range d.Files {
paths = append(paths, f.Path)
}
changedFiles = strings.Join(paths, ", ")
}

return promptData{
Patch: patch,
Stats: stats,
CriticalPaths: criticalPaths,
MaxFindings: 8,
ChangedFiles: changedFiles,
}
}

Expand Down
2 changes: 2 additions & 0 deletions internal/critics/prompts/adjudicator.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ Your job is to:
4. Add any critical findings the other critics may have missed.
5. Produce a final, authoritative set of findings.

CRITICAL: The "file" field in each finding MUST be an exact path from the original diff below. Do NOT invent or guess file paths that are not shown in the diff. Only use paths from this list: {{.ChangedFiles}}. Findings with fabricated file paths will be discarded.

IMPORTANT: Return at most {{.MaxFindings}} findings. Be concise. Use "blocking" only for findings that must prevent merge. Dismiss findings that are false positives.

Previous critic results:
Expand Down
2 changes: 2 additions & 0 deletions internal/critics/prompts/perf_smell.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ Focus on:
- Inefficient string concatenation in loops
- Missing context cancellation checks

CRITICAL: The "file" field in each finding MUST be an exact path from the diff below. Do NOT invent or guess file paths that are not shown in the diff. Only use paths from this list: {{.ChangedFiles}}. Findings with fabricated file paths will be discarded.

IMPORTANT: Return at most {{.MaxFindings}} findings. Be concise. Only report issues with measurable impact.
Don't flag premature optimizations. If no issues, return: `{"findings": []}`

Expand Down
2 changes: 2 additions & 0 deletions internal/critics/prompts/risk_classifier.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ Consider:
- Are there SQL injections, XSS, or other vulnerability patterns?
- Are there unsafe operations (file I/O without checks, naked goroutines, etc.)?

CRITICAL: The "file" field in each finding MUST be an exact path from the diff below. Do NOT invent or guess file paths that are not shown in the diff. Only use paths from this list: {{.ChangedFiles}}. Findings with fabricated file paths will be discarded.

IMPORTANT: Return at most {{.MaxFindings}} findings. Be concise. Focus on the most significant issues only.

Respond in JSON format:
Expand Down
2 changes: 2 additions & 0 deletions internal/critics/prompts/security_smell.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ Focus on:
- Authorization issues (missing permission checks)
- Sensitive data exposure in logs or responses

CRITICAL: The "file" field in each finding MUST be an exact path from the diff below. Do NOT invent or guess file paths that are not shown in the diff. Only use paths from this list: {{.ChangedFiles}}. Findings with fabricated file paths will be discarded.

IMPORTANT: Return at most {{.MaxFindings}} findings. Be concise. Only report GENUINE security concerns, not theoretical ones.
Use "blocking" severity only for exploitable vulnerabilities. If no issues, return: `{"findings": []}`

Expand Down
2 changes: 2 additions & 0 deletions internal/critics/prompts/style_conformance.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ Focus on:
- Magic numbers or strings that should be constants
- Dead code or unreachable paths

CRITICAL: The "file" field in each finding MUST be an exact path from the diff below. Do NOT invent or guess file paths that are not shown in the diff. Only use paths from this list: {{.ChangedFiles}}. Findings with fabricated file paths will be discarded.

IMPORTANT: Return at most {{.MaxFindings}} findings. Be concise. Only report actual issues.
If the code looks fine, return: `{"findings": []}`

Expand Down
2 changes: 2 additions & 0 deletions internal/critics/prompts/test_coverage_smell.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ Focus on:
- New API endpoints without integration tests
- Complex conditions needing boundary tests

CRITICAL: The "file" field in each finding MUST be an exact path from the diff below. Do NOT invent or guess file paths that are not shown in the diff. Only use paths from this list: {{.ChangedFiles}}. Findings with fabricated file paths will be discarded.

IMPORTANT: Return at most {{.MaxFindings}} findings. Be concise. Only report meaningful gaps.
Trivial changes (constants, comments) don't need tests. If coverage is adequate, return: `{"findings": []}`

Expand Down
19 changes: 13 additions & 6 deletions internal/diff/diff.go
Original file line number Diff line number Diff line change
@@ -1,12 +1,19 @@
package diff

type HunkRange struct {
Start int
End int
}

type FileDiff struct {
Path string
Added []string
Removed []string
Patch string
IsNew bool
IsDelete bool
Path string
HunkRanges []HunkRange
StartLine int
Added []string
Removed []string
Patch string
IsNew bool
IsDelete bool
}

type Diff struct {
Expand Down
45 changes: 45 additions & 0 deletions internal/diff/diff_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,12 @@ func TestParse(t *testing.T) {
require.Len(t, d.Files, 2)
require.Equal(t, "main.go", d.Files[0].Path)
require.True(t, d.Files[0].IsNew)
require.Equal(t, 1, d.Files[0].StartLine)
require.Equal(t, []HunkRange{{Start: 1, End: 5}}, d.Files[0].HunkRanges)
require.Equal(t, "auth/login.go", d.Files[1].Path)
require.False(t, d.Files[1].IsNew)
require.Equal(t, 10, d.Files[1].StartLine)
require.Equal(t, []HunkRange{{Start: 10, End: 17}}, d.Files[1].HunkRanges)
require.Equal(t, 2, d.Stats.FilesChanged)
}

Expand All @@ -49,6 +53,47 @@ func TestParseEmpty(t *testing.T) {
require.Len(t, d.Files, 0)
}

func TestParseStartLine(t *testing.T) {
patch := `diff --git a/foo.go b/foo.go
index abc1234..def5678 100644
--- a/foo.go
+++ b/foo.go
@@ -50,3 +50,5 @@ func existing() {
+ added line 1
+ added line 2
}
`
d, err := Parse(patch)
require.NoError(t, err)
require.Len(t, d.Files, 1)
require.Equal(t, "foo.go", d.Files[0].Path)
require.Equal(t, 50, d.Files[0].StartLine)
require.Equal(t, []HunkRange{{Start: 50, End: 54}}, d.Files[0].HunkRanges)
require.Equal(t, []string{"\tadded line 1", "\tadded line 2"}, d.Files[0].Added)
}

func TestParseMultiHunkRanges(t *testing.T) {
patch := `diff --git a/bar.go b/bar.go
index 1111111..2222222 100644
--- a/bar.go
+++ b/bar.go
@@ -10,3 +10,5 @@ func first() {
context1
+ added1
context2
@@ -50,3 +50,4 @@ func second() {
context3
+ added2
context4
}
`
d, err := Parse(patch)
require.NoError(t, err)
require.Len(t, d.Files, 1)
require.Equal(t, 10, d.Files[0].StartLine)
require.Equal(t, []HunkRange{{Start: 10, End: 14}, {Start: 50, End: 53}}, d.Files[0].HunkRanges)
}

func TestNewPath(t *testing.T) {
tests := []struct {
name string
Expand Down
9 changes: 9 additions & 0 deletions internal/diff/git.go
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,15 @@ func Parse(patch string) (*Diff, error) {
}

for _, h := range f.Hunks {
if h.NewStartLine > 0 && h.NewLines > 0 {
fd.HunkRanges = append(fd.HunkRanges, HunkRange{
Start: int(h.NewStartLine),
End: int(h.NewStartLine) + int(h.NewLines) - 1,
})
}
if fd.StartLine == 0 && h.NewStartLine > 0 {
fd.StartLine = int(h.NewStartLine)
}
lines := bytes.Split(h.Body, []byte{'\n'})
for _, l := range lines {
line := string(l)
Expand Down
37 changes: 37 additions & 0 deletions internal/githubclient/review.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"strings"

"github.com/google/go-github/v66/github"
"github.com/helloodokai/acig/internal/diff"
)

func (c *Client) ListReviews(ctx context.Context, owner, repo string, prNumber int) ([]*github.PullRequestReview, error) {
Expand Down Expand Up @@ -99,4 +100,40 @@ func (c *Client) RemoveStaleAcigComments(ctx context.Context, owner, repo string
}
}
}
}

func (c *Client) ListPRFiles(ctx context.Context, owner, repo string, prNumber int) ([]string, error) {
files, _, err := c.client.PullRequests.ListFiles(ctx, owner, repo, prNumber, &github.ListOptions{PerPage: 100})
if err != nil {
return nil, fmt.Errorf("listing PR files: %w", err)
}
paths := make([]string, len(files))
for i, f := range files {
paths[i] = f.GetFilename()
}
return paths, nil
}

func (c *Client) GetPRFileDiffs(ctx context.Context, owner, repo string, prNumber int) (map[string]*diff.FileDiff, error) {
files, _, err := c.client.PullRequests.ListFiles(ctx, owner, repo, prNumber, &github.ListOptions{PerPage: 100})
if err != nil {
return nil, fmt.Errorf("listing PR files for diffs: %w", err)
}

result := make(map[string]*diff.FileDiff)
for _, f := range files {
if f.Patch == nil || *f.Patch == "" {
continue
}
d, parseErr := diff.FromFiles(*f.Patch)
if parseErr != nil || len(d.Files) == 0 {
continue
}
fd := &d.Files[0]
if fd.IsDelete {
continue
}
result[fd.Path] = fd
}
return result, nil
}
Loading
Loading